1// Sets up event handlers for states in the map 2function setupStateInteractions(selection) { 3 4 // Highlight states in red upon hover and make 5 // outlet labels (if used on this map) appear 6 selection.on('mouseover', function(event, data) { 7 if (!STATE_OUTLETS[data.id]) { 8 return; 9 } 10 d3.select(this) 11 .transition() 12 .duration(200) 13 .attr('fill', 'rgba(235, 76, 4, 1)'); 14 d3.select('#outletLabel' + data.id).style('visibility', 'visible'); 15 }) 16 17 // Remove highlight when not hovering and make outlet labels 18 // (if present) disappear 19 .on('mouseleave', function(event, data) { 20 if (!STATE_OUTLETS[data.id]) { 21 return; 22 } 23 d3.select(this) 24 .transition() 25 .duration(200) 26 .attr('fill', 'rgba(56, 78, 119, 1)'); 27 d3.select('#outletLabel' + data.id).style('visibility', 'hidden'); 28 }) 29 30 // Display outlet info upon click - turns off currently 31 // displayed containers and turns on container corresponding 32 // to the clicked state's "FIPS" ID 33 .on('click', function(event, data) { 34 d3.selectAll('.mapNonDefault').style('display', 'none'); 35 d3.select('#mapInformationDefaultContainer').style('display', !STATE_OUTLETS[data.id] ? 'flex' : 'none'); 36 d3.select('#mapInformationContainer' + data.id).style('display', 'flex'); 37 }); 38} 39 40// Retrieves outlet name from provided D3 data 41function getOutletName(d) { 42 const outlet = STATE_OUTLETS[d.id]; 43 return outlet ? outlet.outletName.toUpperCase() : ''; 44} 45 46// Generates newsroom map from given data 47function generateNewsroomMap(mapdata) { 48 49 // Define how high above the state labels should float 50 const floatFactor = 35; 51 52 // Define some constants to properly adapt label boxes to 53 // the size of the label text 54 const labelBoxVerticalPadding = 30; 55 const labelBoxHorizontalPadding = 25; 56 const labelBoxVerticalOffset = 18; 57 58 // Define the width of the label box's highlight bar 59 const labelBoxHighLightWidth = 4.5; 60 61 // Define the amount of padding between lines of the label 62 // (i.e. between outlet name and partner tag) 63 const labelItemPadding = 5; 64 65 // Append SVG with dimensions corresponding to U.S. Atlas TopoJSON 66 // specifications (i.e. 975x610 viewport) 67 const svg = d3.select('#interactiveMapContainerNewsroomPageV3, #interactiveMapContainerImpactReport').append('svg') 68 .attr('viewBox', [0 ,0, 975, 610]) 69 70 // Define a drop-shadow effect for use with state labels 71 const shadows = svg 72 .append('defs') 73 .append('filter') 74 .attr('id', 'dropShadow') 75 .append('feDropShadow') 76 .attr('dx', '1') 77 .attr('dy', '1') 78 .attr('stdDeviation', '5.0') 79 .attr('flood-color', 'rgba(0, 0, 0, 0.30)'); 80 81 // Add state shapes from the U.S. Atlas TopoJSON dataset 82 const stateMap = svg.append('g') 83 84 // Set up some basic configuration for the overall set of states 85 .attr('class', 'newsroomStateGroup') 86 .attr('stroke', 'rgba(255, 255, 255, 1)') 87 .attr('stroke-linejoin', 'round') 88 .attr('stroke-linecap', 'round') 89 90 // Apply data and distinguish between newsroom states and non-newsroom 91 .selectAll('g') 92 .data(topojson.feature(mapdata, mapdata.objects.states).features) 93 .join('g') 94 .attr('class', function(d) { 95 return STATE_OUTLETS[d.id] ? 'newsroomState' : 'otherState'; 96 }) 97 98 // Add a URL to the attributes so we can use it for clicking 99 .attr('data-url', function(d) { 100 return STATE_OUTLETS[d.id] ? STATE_OUTLETS[d.id].url : ''; 101 }) 102 103 // Add the SVG path element for creating the actual shape, 104 // and color it according to whether outlet exists 105 .append('path') 106 .attr('fill', function(d) { 107 return STATE_OUTLETS[d.id] ? 'rgba(56, 78, 119, 1)' : '#929AAA'; 108 }) 109 110 // Define state SVG path properties 111 .attr('class', 'newsroomStateShape') 112 .attr('stroke-width', '0.75') 113 114 // Render the paths 115 .attr('d', d3.geoPath()) 116 117 // Set up interaction events 118 .call(setupStateInteractions); 119 120 // Tweak viewBox and add labels to the map when appearing on impact report 121 const impactLabelGroups = d3.select('#interactiveMapContainerImpactReport svg') 122 123 // Allow labels to extend outside the viewBox 124 // to prevent clipping 125 .attr('overflow', 'visible') 126 127 // Add a group to contain the labels 128 .append('g') 129 .attr('class', 'outletLabelSet') 130 131 // Add individual groups for each label, and apply the U.S. 132 // Atlas TopJSON dataset 133 .selectAll('g') 134 .data(topojson.feature(mapdata, mapdata.objects.states).features) 135 .join('g') 136 137 // Add some atributes to the individual label groups
138 .style('visibility', 'hidden') // Should start out hidden (using "visibility" rather than "display" for Firefox compatibility) 139 .attr('class', 'outletLabel') 140 .attr('id', function(d) { 141 return 'outletLabel' + d.id; // ID is used for displaying/hiding on hover 142 }) 143 .style('pointer-events', 'none') // Prevents interfering with hover effects 144 145 // Calculate the starting coordinates for labels (e.g. 146 // floating a little above the upper edge of the shape's 147 // bounding pox, and positioned at the horizontal midpoint 148 .each(function(d) { 149 d.labelAnchorX = d3.geoPath().centroid(d)[0]; 150 d.labelAnchorY = d3.geoPath().bounds(d)[0][1] - floatFactor; 151 }) 152 153 // Add "CONTENT PARTNER" tag to partner state labels 154 const partnerTexts = impactLabelGroups.append('text') 155 156 // Populate the text tag only if this is a partner state 157 .text(function(d) { 158 const outlet = STATE_OUTLETS[d.id]; 159 return ( outlet && outlet.partner) ? 'CONTENT PARTNER' : ''; 160 }) 161 162 // Format the partner tag 163 .attr('font-family', 'sans-serif') 164 .attr('font-size', '20') 165 .attr('font-weight', '400') 166 .attr('letter-spacing', '2') 167 .attr('fill', 'rgba(134, 147, 170, 1)') 168 169 // Determine the size of the formatted text 170 .each(function(d) { 171 d.partnerFormattedNameWidth = this.getBBox().width; 172 d.partnerFormattedNameHeight = this.getBBox().height ? this.getBBox().height + labelItemPadding : 0; 173 }); 174 175 // Add a little triangle/pointer for label boxes 176 // (Rendered first so boxes layer on top of it.) 177 impactLabelGroups.append('polygon') 178 .attr('points', function(d) { 179 const baseY = d.labelAnchorY + 25; 180 const heightY = baseY + 17; 181 const centerX = d.labelAnchorX; 182 const baseX1 = d.labelAnchorX - 15; 183 const baseX2 = d.labelAnchorX + 15; 184 return baseX1 + ',' + baseY + ' ' + baseX2 + ',' + baseY + ' ' + centerX + ',' + heightY; 185 }) 186 .attr('fill', 'black'); 187 188 // Add outlet names to the label groups 189 const labelTexts = impactLabelGroups.append('text') 190 191 // Retrieve and assign the outlet names 192 .text(getOutletName) 193 194 // Format the outlet names 195 .attr('font-family', '"Barlow Condensed", sans-serif') 196 .attr('font-size', '35') 197 .attr('font-weight', '700') 198 .attr('letter-spacing', '4') 199 .attr('fill', 'rgba(56, 78, 119, 1)') 200 201 // Determine the width of the newly formatted outlet names 202 // and calculate their expected starting coordinates 203 .each(function(d) { 204 205 // Width and height are equivalent to the text's bounding box 206 d.formattedNameWidth = this.getBBox().width; 207 d.formattedNameHeight = this.getBBox().height; 208 209 // Starting coordinates are always going to be 210 // shifted to the left by half the width of the 211 // text (so that the label is centered with the anchor point). 212 // If this position would cause the label to extend past 213 // the right or left edge of the graphic, then adjust the 214 // position so that it does not! (Even though we've enabled 215 // overflow, this can be an issue at very small screen sizes.) 216 d.labelStartingPointX = Math.max(0, d.labelAnchorX - d.formattedNameWidth / 2); 217 const labelApproxEndPoint = d.labelStartingPointX + Math.max(d.formattedNameWidth, d.partnerFormattedNameWidth) 218 if (labelApproxEndPoint > 975) { 219 d.labelStartingPointX = d.labelStartingPointX - (labelApproxEndPoint - 975); 220 } 221 }) 222 223 // Position the label text(s) so that they sit on 224 // and are centered with the anchor point 225 labelTexts.attr('x', function(d) { 226 return d.labelStartingPointX; 227 }) 228 .attr('y', function(d) { 229 // Main label y-coordinate must account for presence 230 // of partner tag 231 return d.labelAnchorY - d.partnerFormattedNameHeight; 232 }) 233 partnerTexts.attr('x', function(d) { 234 return d.labelStartingPointX; 235 }) 236 .attr('y', function(d) { 237 return d.labelAnchorY; 238 }) 239 240 // Position a white box behind the label text 241 // using the same coordinates/dimensions, but 242 // adding a little padding on all sidees 243 // (account for size of main label AND tag if needed) 244 impactLabelGroups.insert('rect', 'text') 245 .attr('x', function(d) { 246 return d.labelStartingPointX - labelBoxHorizontalPadding; 247 }) 248 .attr('y', function(d) { 249 return d.labelAnchorY - d.partnerFormattedNameHeight - labelBoxVerticalPadding - labelBoxVerticalOffset; 250 }) 251 .attr('width', function(d) { 252 return Math.max(d.formattedNameWidth, d.partnerFormattedNameWidth) + ( labelBoxHorizontalPadding * 2 ) - labelBoxHighLightWidth; 253 }) 254 .attr('height', function(d) { 255 return d.formattedNameHeight + d.partnerFormattedNameHeight + labelBoxVerticalPadding; 256 }) 257 .attr('fill', 'rgba(255, 255, 255, 1)') 258 .attr('filter', 'url(#dropShadow)'); 259 260 // Add a small red highlight bar at the left of the white box 261 // using the same starting points and sizes as above 262 impactLabelGroups.append('rect') 263 .attr('x', function(d) { 264 return d.labelStartingPointX - labelBoxHorizontalPadding; 265 }) 266 .attr('y', function(d) { 267 return d.labelAnchorY - d.partnerFormattedNameHeight - labelBoxVerticalPadding - labelBoxVerticalOffset; 268 }) 269 .attr('width', labelBoxHighLightWidth) 270 .attr('height', function(d) { 271 return d.formattedNameHeight + d.partnerFormattedNameHeight + labelBoxVerticalPadding; 272 }) 273 .attr('fill', 'var(--fiery_red)'); 274 275 // On impact map only, allow cursor to change to "pointer" 276 // when the state has a newsroom, and open that page on click 277 d3.selectAll('#interactiveMapContainerImpactReport .newsroomState') 278 .attr('cursor', 'pointer') 279 .on('click', function() { 280 window.open(this.getAttribute('data-url'), '_blank'); 281 }); 282} 283 284// Fetch U.S. Atlas TopoJSON data for map of the states, 285// and use this data to generate the newsroom map upon 286// successful retrieval 287window.addEventListener('DOMContentLoaded', async (event) => { 288 const res = await fetch('https://cdn.jsdelivr.net/npm/us-atlas@3/states-albers-10m.json'); 289 const mapJson = await res.json(); 290 generateNewsroomMap(mapJson); 291});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.