@cdc/map 4.22.10-alpha.1 → 4.22.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/cdcmap.js +10 -10
  2. package/examples/private/atsdr.json +19 -29
  3. package/examples/private/atsdr_new.json +1 -1
  4. package/examples/private/bubble.json +282 -284
  5. package/examples/private/city-state.json +427 -427
  6. package/examples/private/city-state2.json +433 -433
  7. package/examples/private/cty-issue.json +42765 -42768
  8. package/examples/private/default-usa.json +2 -5
  9. package/examples/private/default-world-data.json +1443 -1443
  10. package/examples/private/default.json +965 -965
  11. package/examples/private/diff.json +226 -0
  12. package/examples/private/filters.json +1 -0
  13. package/examples/private/legend-issue.json +3271 -1
  14. package/examples/private/map-issue.json +166 -0
  15. package/examples/private/map-rounding-error.json +42756 -42759
  16. package/examples/private/mdx.json +209 -209
  17. package/examples/private/monkeypox.json +375 -375
  18. package/examples/private/regions.json +51 -51
  19. package/examples/private/wcmsrd-13881-data.json +2856 -2856
  20. package/examples/private/wcmsrd-13881.json +5818 -5822
  21. package/examples/private/wcmsrd-14492-data.json +291 -291
  22. package/examples/private/wcmsrd-14492.json +103 -113
  23. package/examples/private/wcmsrd-test.json +264 -267
  24. package/examples/private/world.json +1579 -1579
  25. package/examples/private/worldmap.json +1489 -1489
  26. package/package.json +3 -3
  27. package/src/CdcMap.js +231 -315
  28. package/src/components/BubbleList.js +199 -240
  29. package/src/components/CityList.js +50 -96
  30. package/src/components/CountyMap.js +511 -600
  31. package/src/components/DataTable.js +218 -253
  32. package/src/components/EditorPanel.js +2338 -2551
  33. package/src/components/Geo.js +4 -14
  34. package/src/components/Modal.js +13 -23
  35. package/src/components/NavigationMenu.js +43 -39
  36. package/src/components/Sidebar.js +83 -93
  37. package/src/components/SingleStateMap.js +95 -151
  38. package/src/components/UsaMap.js +165 -214
  39. package/src/components/UsaRegionMap.js +122 -160
  40. package/src/components/WorldMap.js +96 -179
  41. package/src/components/ZoomableGroup.js +6 -26
  42. package/src/data/initial-state.js +1 -0
  43. package/src/hooks/useActiveElement.js +13 -13
  44. package/src/hooks/useColorPalette.ts +66 -74
  45. package/src/hooks/useZoomPan.js +22 -23
  46. package/src/index.html +1 -2
  47. package/src/scss/sidebar.scss +22 -0
@@ -1,616 +1,527 @@
1
- import React, { useState, useEffect, memo, useRef } from 'react';
2
- import Loading from '@cdc/core/components/Loading';
1
+ import React, { useState, useEffect, memo, useRef } from 'react'
2
+ import Loading from '@cdc/core/components/Loading'
3
3
  /** @jsx jsx */
4
- import { jsx } from '@emotion/react';
5
- import ErrorBoundary from '@cdc/core/components/ErrorBoundary';
6
- import { geoCentroid, geoPath } from 'd3-geo';
7
- import { feature, mesh } from 'topojson-client';
8
- import { CustomProjection } from '@visx/geo';
4
+ import { jsx } from '@emotion/react'
5
+ import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
6
+ import { geoCentroid, geoPath } from 'd3-geo'
7
+ import { feature, mesh } from 'topojson-client'
8
+ import { CustomProjection } from '@visx/geo'
9
9
  import colorPalettes from '../../../core/data/colorPalettes'
10
- import { geoAlbersUsaTerritories } from 'd3-composite-projections';
11
- import testJSON from '../data/county-map.json';
12
- import { abbrs } from '../data/abbreviations';
13
- import CityList from './CityList';
10
+ import { geoAlbersUsaTerritories } from 'd3-composite-projections'
11
+ import testJSON from '../data/county-map.json'
12
+ import { abbrs } from '../data/abbreviations'
13
+ import CityList from './CityList'
14
14
 
15
15
  const offsets = {
16
- Vermont: [50, -8],
17
- 'New Hampshire': [34, 5],
18
- Massachusetts: [30, -5],
19
- 'Rhode Island': [28, 4],
20
- Connecticut: [35, 16],
21
- 'New Jersey': [42, 0],
22
- Delaware: [33, 0],
23
- Maryland: [47, 10],
24
- 'District of Columbia': [30, 20],
25
- 'Puerto Rico': [10, -20],
26
- 'Virgin Islands': [10, -10],
27
- Guam: [10, -5],
28
- 'American Samoa': [10, 0],
29
- };
16
+ Vermont: [50, -8],
17
+ 'New Hampshire': [34, 5],
18
+ Massachusetts: [30, -5],
19
+ 'Rhode Island': [28, 4],
20
+ Connecticut: [35, 16],
21
+ 'New Jersey': [42, 0],
22
+ Delaware: [33, 0],
23
+ Maryland: [47, 10],
24
+ 'District of Columbia': [30, 20],
25
+ 'Puerto Rico': [10, -20],
26
+ 'Virgin Islands': [10, -10],
27
+ Guam: [10, -5],
28
+ 'American Samoa': [10, 0]
29
+ }
30
30
 
31
31
  // SVG ITEMS
32
- const WIDTH = 880;
33
- const HEIGHT = 500;
34
- const PADDING = 25;
32
+ const WIDTH = 880
33
+ const HEIGHT = 500
34
+ const PADDING = 25
35
35
 
36
36
  // DATA
37
- let { features: counties } = feature(testJSON, testJSON.objects.counties);
38
- let { features: states } = feature(testJSON, testJSON.objects.states);
37
+ let { features: counties } = feature(testJSON, testJSON.objects.counties)
38
+ let { features: states } = feature(testJSON, testJSON.objects.states)
39
39
 
40
40
  // CONSTANTS
41
- const STATE_BORDER = '#c0cad4';
42
- const STATE_INACTIVE_FILL = '#F4F7FA';
41
+ const STATE_BORDER = '#c0cad4'
42
+ const STATE_INACTIVE_FILL = '#F4F7FA'
43
43
 
44
44
  // CREATE STATE LINES
45
- const projection = geoAlbersUsaTerritories().translate([WIDTH / 2, HEIGHT / 2]);
46
- const path = geoPath().projection(projection);
47
- const stateLines = path(mesh(testJSON, testJSON.objects.states));
48
- const countyLines = path(mesh(testJSON, testJSON.objects.counties));
49
-
45
+ const projection = geoAlbersUsaTerritories().translate([WIDTH / 2, HEIGHT / 2])
46
+ const path = geoPath().projection(projection)
47
+ const stateLines = path(mesh(testJSON, testJSON.objects.states))
48
+ const countyLines = path(mesh(testJSON, testJSON.objects.counties))
50
49
 
51
50
  function CountyMapChecks(prevState, nextState) {
52
- const equalNumberOptIn = prevState.state.general.equalNumberOptIn && nextState.state.general.equalNumberOptIn;
53
- const equalColumnName = prevState.state.general.type && nextState.state.general.type;
54
- const equalNavColumn = prevState.state.columns.navigate && nextState.state.columns.navigate;
55
- const equalLegend = prevState.runtimeLegend === nextState.runtimeLegend;
56
- const equalBorderColors = prevState.state.general.geoBorderColor === nextState.state.general.geoBorderColor; // update when geoborder color changes
57
- const equalMapColors = prevState.state.color === nextState.state.color; // update when map colors change
58
- const equalData = prevState.data === nextState.data; // update when data changes
59
- return equalMapColors && equalData && equalBorderColors && equalLegend && equalColumnName && equalNavColumn && equalNumberOptIn ? true : false;
51
+ const equalNumberOptIn = prevState.state.general.equalNumberOptIn && nextState.state.general.equalNumberOptIn
52
+ const equalColumnName = prevState.state.general.type && nextState.state.general.type
53
+ const equalNavColumn = prevState.state.columns.navigate && nextState.state.columns.navigate
54
+ const equalLegend = prevState.runtimeLegend === nextState.runtimeLegend
55
+ const equalBorderColors = prevState.state.general.geoBorderColor === nextState.state.general.geoBorderColor // update when geoborder color changes
56
+ const equalMapColors = prevState.state.color === nextState.state.color // update when map colors change
57
+ const equalData = prevState.data === nextState.data // update when data changes
58
+ return equalMapColors && equalData && equalBorderColors && equalLegend && equalColumnName && equalNavColumn && equalNumberOptIn ? true : false
59
+ }
60
+
61
+ const CountyMap = props => {
62
+ let mapData = states.concat(counties)
63
+
64
+ const { state, applyTooltipsToGeo, data, geoClickHandler, applyLegendToRow, displayGeoName, rebuildTooltips, containerEl, handleMapAriaLabels, titleCase, setSharedFilterValue, isFilterValueSupported } = props
65
+
66
+ useEffect(() => {
67
+ if (containerEl) {
68
+ if (containerEl.className.indexOf('loaded') === -1) {
69
+ containerEl.className += ' loaded'
70
+ }
71
+ }
72
+ })
73
+
74
+ // Use State
75
+ const [scale, setScale] = useState(0.85)
76
+ const [startingLineWidth, setStartingLineWidth] = useState(1.3)
77
+ const [translate, setTranslate] = useState([0, 0])
78
+ const [mapColorPalette, setMapColorPalette] = useState(colorPalettes[state.color] || '#fff')
79
+ const [focusedState, setFocusedState] = useState(null)
80
+ const [showLabel, setShowLabels] = useState(true)
81
+
82
+ const resetButton = useRef()
83
+ const focusedBorderPath = useRef()
84
+ const stateLinesPath = useRef()
85
+ const mapGroup = useRef()
86
+
87
+ let focusedBorderColor = mapColorPalette[3]
88
+ let geoStrokeColor = state.general.geoBorderColor === 'darkGray' ? 'rgba(0, 0, 0, 0.2)' : 'rgba(255,255,255,0.7)'
89
+
90
+ // Use Effect
91
+ useEffect(() => rebuildTooltips())
92
+
93
+ const geoLabel = (geo, projection) => {
94
+ let [x, y] = projection(geoCentroid(geo))
95
+ let abbr = abbrs[geo.properties.name]
96
+ if (abbr === 'NJ') x += 3
97
+ if (undefined === abbr) return null
98
+ let [dx, dy] = offsets[geo.properties.name]
99
+
100
+ return (
101
+ <>
102
+ <line className='abbrLine' x1={x} y1={y} x2={x + dx} y2={y + dy} stroke='black' strokeWidth={0.85} />
103
+ <text className='abbrText' x={4} strokeWidth='0' fontSize={13} style={{ fill: '#202020' }} alignmentBaseline='middle' transform={`translate(${x + dx}, ${y + dy})`}>
104
+ {abbr}
105
+ </text>
106
+ </>
107
+ )
108
+ }
109
+
110
+ const focusGeo = (geoKey, geo) => {
111
+ if (!geoKey) {
112
+ console.log('County Map: no geoKey provided to focusGeo')
113
+ return
114
+ }
115
+
116
+ // 1) Get the state the county is in.
117
+ let myState = states.find(s => s.id === geoKey)
118
+
119
+ // 2) Set projections translation & scale to the geographic center of the passed geo.
120
+ const projection = geoAlbersUsaTerritories().translate([WIDTH / 2, HEIGHT / 2])
121
+ const newProjection = projection.fitExtent(
122
+ [
123
+ [PADDING, PADDING],
124
+ [WIDTH - PADDING, HEIGHT - PADDING]
125
+ ],
126
+ myState
127
+ )
128
+
129
+ // 3) Gets the new scale
130
+ const newScale = newProjection.scale()
131
+ const hypot = Math.hypot(880, 500)
132
+ const newScaleWithHypot = newScale / 1070
133
+
134
+ // 4) Pull the x & y out, divide by half the viewport for some reason
135
+ let [x, y] = newProjection.translate()
136
+ x = x - WIDTH / 2
137
+ y = y - HEIGHT / 2
138
+
139
+ // 5) Debug if needed
140
+ const debug = {
141
+ width: WIDTH,
142
+ height: HEIGHT,
143
+ beginX: 0,
144
+ beginY: 0,
145
+ hypot: hypot,
146
+ x: x,
147
+ y: y,
148
+ newScale: newScale,
149
+ newScaleWithHypot: newScaleWithHypot,
150
+ geoKey: geoKey,
151
+ geo: geo
152
+ }
153
+ //console.table(debug)
154
+
155
+ mapGroup.current.setAttribute('transform', `translate(${[x, y]}) scale(${newScaleWithHypot})`)
156
+ resetButton.current.style.display = 'block'
157
+
158
+ // set the states border
159
+ let allStates = document.querySelectorAll('.state path')
160
+ let allCounties = document.querySelectorAll('.county path')
161
+ let currentState = document.querySelector(`.state--${myState.id}`)
162
+ let otherStates = document.querySelectorAll(`.state:not(.state--${myState.id})`)
163
+ let svgContainer = document.querySelector('.svg-container')
164
+ svgContainer.setAttribute('data-scaleZoom', newScaleWithHypot)
165
+
166
+ const state = testJSON.objects.states.geometries.filter((el, index) => {
167
+ return el.id === myState.id
168
+ })
169
+
170
+ const focusedStateLine = path(mesh(testJSON, state[0]))
171
+
172
+ currentState.style.display = 'none'
173
+
174
+ allStates.forEach(state => (state.style.strokeWidth = 0.75 / newScaleWithHypot))
175
+ allCounties.forEach(county => (county.style.strokeWidth = 0.75 / newScaleWithHypot))
176
+ otherStates.forEach(el => (el.style.display = 'block'))
177
+
178
+ // State Line Updates
179
+ stateLinesPath.current.setAttribute('stroke-width', 0.75 / newScaleWithHypot)
180
+ stateLinesPath.current.setAttribute('stroke', geoStrokeColor)
181
+
182
+ // Set Focus Border
183
+ focusedBorderPath.current.style.display = 'block'
184
+ focusedBorderPath.current.setAttribute('d', focusedStateLine)
185
+ //focusedBorderPath.current.setAttribute('stroke-width', 0.75 / newScaleWithHypot);
186
+ //focusedBorderPath.current.setAttribute('stroke', focusedBorderColor)
187
+ }
188
+
189
+ const onReset = e => {
190
+ if (state.general.type !== 'us-geocode') {
191
+ e.preventDefault()
192
+ const svg = document.querySelector('.svg-container')
193
+
194
+ svg.setAttribute('data-scaleZoom', 0)
195
+
196
+ const allStates = document.querySelectorAll('.state path')
197
+ const allCounties = document.querySelectorAll('.county path')
198
+
199
+ stateLinesPath.current.setAttribute('stroke', geoStrokeColor)
200
+ stateLinesPath.current.setAttribute('stroke-width', startingLineWidth)
201
+
202
+ let otherStates = document.querySelectorAll(`.state--inactive`)
203
+ otherStates.forEach(el => (el.style.display = 'none'))
204
+ allCounties.forEach(el => (el.style.strokeWidth = 0.85))
205
+ allStates.forEach(state => state.setAttribute('stroke-width', 0.75 / 0.85))
206
+
207
+ mapGroup.current.setAttribute('transform', `translate(${[0, 0]}) scale(${0.85})`)
208
+
209
+ // reset button
210
+ resetButton.current.style.display = 'none'
211
+ } else {
212
+ const svg = document.querySelector('.svg-container')
213
+ const allStates = document.querySelectorAll('.state')
214
+ document.querySelector('#focusedBorder path').style.stroke = 'none'
215
+ allStates.forEach(item => item.classList.remove('state--inactive'))
216
+ //document.querySelectorAll('.state path').forEach(item => item.style.fill = 'rgb(244, 247, 250)')
217
+ document.querySelectorAll('.state').forEach(item => (item.style.display = 'block'))
218
+ stateLinesPath.current.setAttribute('stroke', geoStrokeColor)
219
+ stateLinesPath.current.setAttribute('stroke-width', startingLineWidth)
220
+ svg.setAttribute('data-scaleZoom', 0)
221
+ mapGroup.current.setAttribute('transform', `translate(${[0, 0]}) scale(${0.85})`)
222
+ resetButton.current.style.display = 'none'
223
+ }
224
+ }
225
+
226
+ function setStateLeave() {
227
+ focusedBorderPath.current.setAttribute('d', '')
228
+ focusedBorderPath.current.setAttribute('stroke', '')
229
+ focusedBorderPath.current.setAttribute('stroke-width', 0.75 / scale)
230
+ }
231
+
232
+ function setStateEnter(id) {
233
+ const svg = document.querySelector('.svg-container')
234
+ const scale = svg.getAttribute('data-scaleZoom')
235
+
236
+ let myState = id.substring(0, 2)
237
+ const allStates = document.querySelectorAll('.state path')
238
+
239
+ let state = testJSON.objects.states.geometries.filter((el, index) => {
240
+ return el.id === myState
241
+ })
242
+
243
+ let focusedStateLine = path(mesh(testJSON, state[0]))
244
+ focusedBorderPath.current.style.display = 'block'
245
+ focusedBorderPath.current.setAttribute('d', focusedStateLine)
246
+ focusedBorderPath.current.setAttribute('stroke', '#000')
247
+
248
+ if (scale) {
249
+ allStates.forEach(state => state.setAttribute('stroke-width', 0.75 / scale))
250
+ focusedBorderPath.current.setAttribute('stroke-width', 0.75 / scale)
251
+ }
252
+ }
253
+
254
+ const StateLines = memo(({ stateLines, lineWidth, geoStrokeColor }) => {
255
+ return (
256
+ <g className='stateLines' key='state-line'>
257
+ <path id='stateLinesPath' ref={stateLinesPath} d={stateLines} strokeWidth={lineWidth} stroke={geoStrokeColor} fill='none' fillOpacity='1' />
258
+ </g>
259
+ )
260
+ })
261
+
262
+ const FocusedStateBorder = memo(() => {
263
+ return (
264
+ <g id='focusedBorder' key='focusedStateBorder'>
265
+ <path ref={focusedBorderPath} d='' strokeWidth='' stroke={focusedBorderColor} fill='none' fillOpacity='1' />
266
+ </g>
267
+ )
268
+ })
269
+
270
+ const CountyOutput = memo(({ geographies, counties }) => {
271
+ let output = []
272
+ output.push(
273
+ counties.map(({ feature: geo, path = '' }) => {
274
+ const key = geo.id + '-group'
275
+
276
+ // COUNTY GROUPS
277
+ let styles = {
278
+ fillOpacity: '1',
279
+ fill: '#E6E6E6',
280
+ cursor: 'default'
281
+ }
282
+
283
+ // Map the name from the geo data with the appropriate key for the processed data
284
+ let geoKey = geo.id
285
+
286
+ if (!geoKey) return null
287
+
288
+ const geoData = data[geoKey]
289
+
290
+ let legendColors
291
+
292
+ // Once we receive data for this geographic item, setup variables.
293
+ if (geoData !== undefined) {
294
+ legendColors = applyLegendToRow(geoData)
295
+ }
296
+
297
+ const geoDisplayName = displayGeoName(geoKey)
298
+
299
+ // For some reason, these two geos are breaking the display.
300
+ if (geoDisplayName === 'Franklin City' || geoDisplayName === 'Waynesboro') return null
301
+
302
+ // If a legend applies, return it with appropriate information.
303
+ if (legendColors && legendColors[0] !== '#000000') {
304
+ const tooltip = applyTooltipsToGeo(geoDisplayName, geoData)
305
+
306
+ styles = {
307
+ fill: legendColors[0],
308
+ cursor: 'default',
309
+ '&:hover': {
310
+ fill: legendColors[1]
311
+ },
312
+ '&:active': {
313
+ fill: legendColors[2]
314
+ }
315
+ }
316
+
317
+ // When to add pointer cursor
318
+ if ((state.columns.navigate && geoData[state.columns.navigate.name]) || state.tooltips.appearanceType === 'hover') {
319
+ styles.cursor = 'pointer'
320
+ }
321
+ let stateFipsCode = geoData[state.columns.geo.name].substring(0, 2)
322
+
323
+ return (
324
+ <g
325
+ tabIndex='-1'
326
+ data-for='tooltip'
327
+ data-tip={tooltip}
328
+ key={`county--${key}`}
329
+ className={`county county--${geoDisplayName.split(' ').join('')} county--${geoData[state.columns.geo.name]}`}
330
+ css={styles}
331
+ onMouseEnter={() => {
332
+ setStateEnter(geo.id)
333
+ }}
334
+ onMouseLeave={() => {
335
+ setStateLeave()
336
+ }}
337
+ onClick={
338
+ // default
339
+ e => {
340
+ e.stopPropagation()
341
+ e.nativeEvent.stopImmediatePropagation()
342
+ geoClickHandler(geoDisplayName, geoData)
343
+ focusGeo(stateFipsCode, geo)
344
+ }
345
+ }
346
+ >
347
+ <path tabIndex={-1} className={`county county--${geoDisplayName}`} stroke={geoStrokeColor} d={path} strokeWidth='.5' />
348
+ </g>
349
+ )
350
+ }
351
+
352
+ // default county
353
+ return (
354
+ <g
355
+ key={`county--default-${key}`}
356
+ className={`county county--${geoDisplayName}`}
357
+ css={styles}
358
+ strokeWidth=''
359
+ onMouseEnter={() => {
360
+ setStateEnter(geo.id)
361
+ }}
362
+ onMouseLeave={() => {
363
+ setStateLeave()
364
+ }}
365
+ onClick={
366
+ // default
367
+ e => {
368
+ e.stopPropagation()
369
+ e.nativeEvent.stopImmediatePropagation()
370
+ let countyFipsCode = geo.id
371
+ let stateFipsCode = countyFipsCode.substring(0, 2)
372
+ focusGeo(stateFipsCode, geo)
373
+ }
374
+ }
375
+ >
376
+ <path tabIndex={-1} className='single-geo' stroke={geoStrokeColor} d={path} strokeWidth='.85' />
377
+ </g>
378
+ )
379
+ })
380
+ )
381
+ return output
382
+ })
383
+
384
+ const GeoCodeCountyLines = memo(() => {
385
+ return <path d={countyLines} className='county-borders' style={{ stroke: geoStrokeColor }} />
386
+ })
387
+
388
+ const StateOutput = memo(({ geographies, states }) => {
389
+ let output = []
390
+ output.push(
391
+ states.map(({ feature: geo, path = '' }) => {
392
+ const key = geo.id + '-group'
393
+
394
+ // Map the name from the geo data with the appropriate key for the processed data
395
+ let geoKey = geo.id
396
+
397
+ if (!geoKey) return
398
+
399
+ const geoData = data[geoKey]
400
+
401
+ let legendColors
402
+
403
+ // Once we receive data for this geographic item, setup variables.
404
+ if (geoData !== undefined) {
405
+ legendColors = applyLegendToRow(geoData)
406
+ }
407
+
408
+ const geoDisplayName = displayGeoName(geoKey)
409
+
410
+ let stateStyles = {}
411
+
412
+ if (state.general.type !== 'us-geocode') {
413
+ stateStyles = {
414
+ cursor: 'default',
415
+ stroke: STATE_BORDER,
416
+ strokeWidth: 0.75 / scale,
417
+ display: !focusedState ? 'none' : focusedState && focusedState !== geo.id ? 'block' : 'none',
418
+ fill: focusedState && focusedState !== geo.id ? STATE_INACTIVE_FILL : 'none'
419
+ }
420
+ } else {
421
+ stateStyles = {
422
+ cursor: 'default',
423
+ stroke: STATE_BORDER,
424
+ strokeWidth: 0.75 / scale,
425
+ display: 'block',
426
+ fill: '#f4f7fa'
427
+ }
428
+ }
429
+
430
+ let stateSelectedStyles = {
431
+ fillOpacity: 1,
432
+ cursor: 'default'
433
+ }
434
+
435
+ let stateClasses = ['state', `state--${geo.properties.name}`, `state--${geo.id}`]
436
+ focusedState === geo.id ? stateClasses.push('state--focused') : stateClasses.push('state--inactive')
437
+
438
+ return (
439
+ <React.Fragment key={`state--${key}`}>
440
+ <g key={`state--${key}`} className={stateClasses.join(' ')} style={stateStyles} tabIndex='-1'>
441
+ <>
442
+ <path
443
+ tabIndex={-1}
444
+ className='state-path'
445
+ d={path}
446
+ fillOpacity={`${focusedState !== geo.id ? '1' : '0'}`}
447
+ fill={STATE_INACTIVE_FILL}
448
+ css={stateSelectedStyles}
449
+ onClick={e => {
450
+ e.stopPropagation()
451
+ e.nativeEvent.stopImmediatePropagation()
452
+ focusGeo(geo.id, geo)
453
+ }}
454
+ onMouseEnter={e => {
455
+ e.target.attributes.fill.value = colorPalettes[state.color][3]
456
+ }}
457
+ onMouseLeave={e => {
458
+ e.target.attributes.fill.value = STATE_INACTIVE_FILL
459
+ }}
460
+ />
461
+ </>
462
+ </g>
463
+ <g key={`label--${key}`}>{offsets[geo.properties.name] && geoLabel(geo, geoAlbersUsaTerritories().translate([WIDTH / 2, HEIGHT / 2]))}</g>
464
+ </React.Fragment>
465
+ )
466
+ })
467
+ )
468
+ return output
469
+ })
470
+
471
+ // Constructs and displays markup for all geos on the map (except territories right now)
472
+ const constructGeoJsx = (geographies, projection) => {
473
+ const states = geographies.slice(0, 56)
474
+ const counties = geographies.slice(56)
475
+ let geosJsx = []
476
+ {
477
+ 'us-geocode' !== state.general.type && geosJsx.push(<CountyOutput geographies={geographies} counties={counties} key='county-key' />)
478
+ }
479
+ {
480
+ 'us-geocode' === state.general.type && geosJsx.push(<GeoCodeCountyLines />)
481
+ }
482
+ geosJsx.push(<StateOutput geographies={geographies} states={states} key='state-key' />)
483
+ geosJsx.push(<StateLines key='stateLines' lineWidth={startingLineWidth} geoStrokeColor={geoStrokeColor} stateLines={stateLines} />)
484
+ geosJsx.push(<FocusedStateBorder key='focused-border-key' />)
485
+ geosJsx.push(
486
+ <CityList
487
+ projection={projection}
488
+ key='cities'
489
+ data={data}
490
+ state={state}
491
+ geoClickHandler={geoClickHandler}
492
+ applyTooltipsToGeo={applyTooltipsToGeo}
493
+ displayGeoName={displayGeoName}
494
+ applyLegendToRow={applyLegendToRow}
495
+ titleCase={titleCase}
496
+ setSharedFilterValue={setSharedFilterValue}
497
+ isFilterValueSupported={isFilterValueSupported}
498
+ isGeoCodeMap={state.general.type === 'us-geocode'}
499
+ />
500
+ )
501
+ return geosJsx
502
+ }
503
+ if (!data) <Loading />
504
+ return (
505
+ <ErrorBoundary component='CountyMap'>
506
+ <svg viewBox={`0 0 ${WIDTH} ${HEIGHT}`} preserveAspectRatio='xMinYMin' className='svg-container' data-scale={scale ? scale : ''} data-translate={translate ? translate : ''} role='img' aria-label={handleMapAriaLabels(state)}>
507
+ <rect className='background center-container ocean' width={WIDTH} height={HEIGHT} fillOpacity={1} fill='white' onClick={e => onReset(e)} tabIndex='0'></rect>
508
+ <CustomProjection data={mapData} translate={[WIDTH / 2, HEIGHT / 2]} projection={geoAlbersUsaTerritories}>
509
+ {({ features, projection }) => {
510
+ return (
511
+ <g ref={mapGroup} className='countyMapGroup' transform={`translate(${translate}) scale(${scale})`} key='countyMapGroup'>
512
+ {constructGeoJsx(features, projection)}
513
+ </g>
514
+ )
515
+ }}
516
+ </CustomProjection>
517
+ </svg>
518
+
519
+ {/* TODO: Refactor to COVE button */}
520
+ <button className={`btn btn--reset`} onClick={onReset} ref={resetButton} style={{ display: 'none' }} tabIndex='0'>
521
+ Reset Zoom
522
+ </button>
523
+ </ErrorBoundary>
524
+ )
60
525
  }
61
526
 
62
- const CountyMap = (props) => {
63
- let mapData = states.concat(counties);
64
-
65
- const {
66
- state,
67
- applyTooltipsToGeo,
68
- data,
69
- geoClickHandler,
70
- applyLegendToRow,
71
- displayGeoName,
72
- rebuildTooltips,
73
- containerEl,
74
- handleMapAriaLabels,
75
- titleCase,
76
- setSharedFilterValue,
77
- isFilterValueSupported
78
- } = props;
79
-
80
- useEffect(() => {
81
- if(containerEl) {
82
- if (containerEl.className.indexOf('loaded') === -1) {
83
- containerEl.className += ' loaded';
84
- }
85
- }
86
- });
87
-
88
- // Use State
89
- const [scale, setScale] = useState(0.85);
90
- const [startingLineWidth, setStartingLineWidth] = useState(1.3);
91
- const [translate, setTranslate] = useState([0, 0]);
92
- const [mapColorPalette, setMapColorPalette] = useState(colorPalettes[state.color] || '#fff');
93
- const [focusedState, setFocusedState] = useState(null);
94
- const [showLabel, setShowLabels] = useState(true);
95
-
96
- const resetButton = useRef();
97
- const focusedBorderPath = useRef();
98
- const stateLinesPath = useRef();
99
- const mapGroup = useRef();
100
-
101
- let focusedBorderColor = mapColorPalette[3];
102
- let geoStrokeColor = state.general.geoBorderColor === 'darkGray' ? 'rgba(0, 0, 0, 0.2)' : 'rgba(255,255,255,0.7)';
103
-
104
- // Use Effect
105
- useEffect(() => rebuildTooltips());
106
-
107
- const geoLabel = (geo, projection) => {
108
- let [x, y] = projection(geoCentroid(geo));
109
- let abbr = abbrs[geo.properties.name];
110
- if (abbr === 'NJ') x += 3;
111
- if (undefined === abbr) return null;
112
- let [dx, dy] = offsets[geo.properties.name];
113
-
114
- return (
115
- <>
116
- <line className='abbrLine' x1={x} y1={y} x2={x + dx} y2={y + dy} stroke='black' strokeWidth={0.85} />
117
- <text
118
- className='abbrText'
119
- x={4}
120
- strokeWidth='0'
121
- fontSize={13}
122
- style={{ fill: '#202020' }}
123
- alignmentBaseline='middle'
124
- transform={`translate(${x + dx}, ${y + dy})`}
125
- >
126
- {abbr}
127
- </text>
128
- </>
129
- );
130
- };
131
-
132
- const focusGeo = (geoKey, geo) => {
133
- if (!geoKey) {
134
- console.log('County Map: no geoKey provided to focusGeo');
135
- return;
136
- }
137
-
138
- // 1) Get the state the county is in.
139
- let myState = states.find((s) => s.id === geoKey);
140
-
141
- // 2) Set projections translation & scale to the geographic center of the passed geo.
142
- const projection = geoAlbersUsaTerritories().translate([WIDTH / 2, HEIGHT / 2]);
143
- const newProjection = projection.fitExtent(
144
- [
145
- [PADDING, PADDING],
146
- [WIDTH - PADDING, HEIGHT - PADDING],
147
- ],
148
- myState
149
- );
150
-
151
- // 3) Gets the new scale
152
- const newScale = newProjection.scale();
153
- const hypot = Math.hypot(880, 500);
154
- const newScaleWithHypot = newScale / 1070;
155
-
156
- // 4) Pull the x & y out, divide by half the viewport for some reason
157
- let [x, y] = newProjection.translate();
158
- x = x - WIDTH / 2;
159
- y = y - HEIGHT / 2;
160
-
161
- // 5) Debug if needed
162
- const debug = {
163
- width: WIDTH,
164
- height: HEIGHT,
165
- beginX: 0,
166
- beginY: 0,
167
- hypot: hypot,
168
- x: x,
169
- y: y,
170
- newScale: newScale,
171
- newScaleWithHypot: newScaleWithHypot,
172
- geoKey: geoKey,
173
- geo: geo,
174
- };
175
- //console.table(debug)
176
-
177
- mapGroup.current.setAttribute('transform', `translate(${[x, y]}) scale(${newScaleWithHypot})`);
178
- resetButton.current.style.display = 'block';
179
-
180
- // set the states border
181
- let allStates = document.querySelectorAll('.state path');
182
- let allCounties = document.querySelectorAll('.county path');
183
- let currentState = document.querySelector(`.state--${myState.id}`);
184
- let otherStates = document.querySelectorAll(`.state:not(.state--${myState.id})`);
185
- let svgContainer = document.querySelector('.svg-container')
186
- svgContainer.setAttribute('data-scaleZoom', newScaleWithHypot)
187
-
188
- const state = testJSON.objects.states.geometries.filter((el, index) => {
189
- return el.id === myState.id;
190
- });
191
-
192
- const focusedStateLine = path(mesh(testJSON, state[0]));
193
-
194
- currentState.style.display = 'none';
195
-
196
- allStates.forEach((state) => (state.style.strokeWidth = 0.75 / newScaleWithHypot));
197
- allCounties.forEach((county) => (county.style.strokeWidth = 0.75 / newScaleWithHypot));
198
- otherStates.forEach((el) => (el.style.display = 'block'));
199
-
200
- // State Line Updates
201
- stateLinesPath.current.setAttribute('stroke-width', 0.75 / newScaleWithHypot);
202
- stateLinesPath.current.setAttribute('stroke', geoStrokeColor);
203
-
204
- // Set Focus Border
205
- focusedBorderPath.current.style.display = 'block';
206
- focusedBorderPath.current.setAttribute('d', focusedStateLine);
207
- //focusedBorderPath.current.setAttribute('stroke-width', 0.75 / newScaleWithHypot);
208
- //focusedBorderPath.current.setAttribute('stroke', focusedBorderColor)
209
- };
210
-
211
- const onReset = (e) => {
212
-
213
- if (state.general.type !== 'us-geocode') {
214
- e.preventDefault();
215
- const svg = document.querySelector('.svg-container')
216
-
217
- svg.setAttribute('data-scaleZoom', 0)
218
-
219
-
220
- const allStates = document.querySelectorAll('.state path');
221
- const allCounties = document.querySelectorAll('.county path');
222
-
223
- stateLinesPath.current.setAttribute('stroke', geoStrokeColor);
224
- stateLinesPath.current.setAttribute('stroke-width', startingLineWidth);
225
-
226
- let otherStates = document.querySelectorAll(`.state--inactive`);
227
- otherStates.forEach((el) => (el.style.display = 'none'));
228
- allCounties.forEach((el) => (el.style.strokeWidth = 0.85));
229
- allStates.forEach((state) => state.setAttribute('stroke-width', .75 / .85));
230
-
231
- mapGroup.current.setAttribute('transform', `translate(${[0, 0]}) scale(${0.85})`);
232
-
233
- // reset button
234
- resetButton.current.style.display = 'none';
235
- } else {
236
- const svg = document.querySelector('.svg-container')
237
- const allStates = document.querySelectorAll('.state');
238
- document.querySelector('#focusedBorder path').style.stroke = 'none';
239
- allStates.forEach(item => item.classList.remove('state--inactive'))
240
- //document.querySelectorAll('.state path').forEach(item => item.style.fill = 'rgb(244, 247, 250)')
241
- document.querySelectorAll('.state').forEach(item => item.style.display = 'block')
242
- stateLinesPath.current.setAttribute('stroke', geoStrokeColor);
243
- stateLinesPath.current.setAttribute('stroke-width', startingLineWidth);
244
- svg.setAttribute('data-scaleZoom', 0)
245
- mapGroup.current.setAttribute('transform', `translate(${[0, 0]}) scale(${0.85})`);
246
- resetButton.current.style.display = 'none';
247
- }
248
- };
249
-
250
- function setStateLeave() {
251
- focusedBorderPath.current.setAttribute('d', '');
252
- focusedBorderPath.current.setAttribute('stroke', '');
253
- focusedBorderPath.current.setAttribute('stroke-width', 0.75 / scale);
254
- }
255
-
256
- function setStateEnter(id) {
257
- const svg = document.querySelector('.svg-container')
258
- const scale = svg.getAttribute('data-scaleZoom');
259
-
260
- let myState = id.substring(0, 2);
261
- const allStates = document.querySelectorAll('.state path');
262
-
263
-
264
- let state = testJSON.objects.states.geometries.filter((el, index) => {
265
- return el.id === myState;
266
- });
267
-
268
- let focusedStateLine = path(mesh(testJSON, state[0]));
269
- focusedBorderPath.current.style.display = 'block';
270
- focusedBorderPath.current.setAttribute('d', focusedStateLine);
271
- focusedBorderPath.current.setAttribute('stroke', '#000');
272
-
273
- if (scale) {
274
- allStates.forEach(state => state.setAttribute('stroke-width', 0.75 / scale))
275
- focusedBorderPath.current.setAttribute('stroke-width', 0.75 / scale);
276
- }
277
-
278
- }
279
-
280
- const StateLines = memo(({ stateLines, lineWidth, geoStrokeColor }) => {
281
- return (
282
- <g className='stateLines' key='state-line'>
283
- <path
284
- id='stateLinesPath'
285
- ref={stateLinesPath}
286
- d={stateLines}
287
- strokeWidth={lineWidth}
288
- stroke={geoStrokeColor}
289
- fill='none'
290
- fillOpacity='1'
291
- />
292
- </g>
293
- );
294
- });
295
-
296
- const FocusedStateBorder = memo(() => {
297
- return (
298
- <g id='focusedBorder' key='focusedStateBorder'>
299
- <path
300
- ref={focusedBorderPath}
301
- d=''
302
- strokeWidth=''
303
- stroke={focusedBorderColor}
304
- fill='none'
305
- fillOpacity='1'
306
- />
307
- </g>
308
- );
309
- });
310
-
311
- const CountyOutput = memo(({ geographies, counties }) => {
312
- let output = [];
313
- output.push(
314
- counties.map(({ feature: geo, path = '' }) => {
315
- const key = geo.id + '-group';
316
-
317
- // COUNTY GROUPS
318
- let styles = {
319
- fillOpacity: '1',
320
- fill: '#E6E6E6',
321
- cursor: 'default',
322
- };
323
-
324
- // Map the name from the geo data with the appropriate key for the processed data
325
- let geoKey = geo.id;
326
-
327
- if (!geoKey) return null;
328
-
329
- const geoData = data[geoKey];
330
-
331
- let legendColors;
332
-
333
- // Once we receive data for this geographic item, setup variables.
334
- if (geoData !== undefined) {
335
- legendColors = applyLegendToRow(geoData);
336
- }
337
-
338
- const geoDisplayName = displayGeoName(geoKey);
339
-
340
- // For some reason, these two geos are breaking the display.
341
- if (geoDisplayName === 'Franklin City' || geoDisplayName === 'Waynesboro') return null;
342
-
343
- // If a legend applies, return it with appropriate information.
344
- if (legendColors && legendColors[0] !== '#000000') {
345
- const tooltip = applyTooltipsToGeo(geoDisplayName, geoData);
346
-
347
- styles = {
348
- fill: legendColors[0],
349
- cursor: 'default',
350
- '&:hover': {
351
- fill: legendColors[1],
352
- },
353
- '&:active': {
354
- fill: legendColors[2],
355
- },
356
- };
357
-
358
- // When to add pointer cursor
359
- if (
360
- (state.columns.navigate && geoData[state.columns.navigate.name]) ||
361
- state.tooltips.appearanceType === 'hover'
362
- ) {
363
- styles.cursor = 'pointer';
364
- }
365
- let stateFipsCode = geoData[state.columns.geo.name].substring(0, 2);
366
-
367
- return (
368
- <g
369
- tabIndex="-1"
370
- data-for='tooltip'
371
- data-tip={tooltip}
372
- key={`county--${key}`}
373
- className={`county county--${geoDisplayName.split(' ').join('')} county--${
374
- geoData[state.columns.geo.name]
375
- }`}
376
- css={styles}
377
- onMouseEnter={() => {
378
- setStateEnter(geo.id);
379
- }}
380
- onMouseLeave={() => {
381
- setStateLeave();
382
- }}
383
- onClick={
384
- // default
385
- (e) => {
386
- e.stopPropagation();
387
- e.nativeEvent.stopImmediatePropagation();
388
- geoClickHandler(geoDisplayName, geoData);
389
- focusGeo(stateFipsCode, geo);
390
- }
391
- }
392
- >
393
- <path
394
- tabIndex={-1}
395
- className={`county county--${geoDisplayName}`}
396
- stroke={geoStrokeColor}
397
- d={path}
398
- strokeWidth='.5'
399
- />
400
- </g>
401
- );
402
- }
403
-
404
- // default county
405
- return (
406
- <g
407
- key={`county--default-${key}`}
408
- className={`county county--${geoDisplayName}`}
409
- css={styles}
410
- strokeWidth=''
411
- onMouseEnter={() => {
412
- setStateEnter(geo.id);
413
- }}
414
- onMouseLeave={() => {
415
- setStateLeave();
416
- }}
417
- onClick={
418
- // default
419
- (e) => {
420
- e.stopPropagation();
421
- e.nativeEvent.stopImmediatePropagation();
422
- let countyFipsCode = geo.id;
423
- let stateFipsCode = countyFipsCode.substring(0, 2);
424
- focusGeo(stateFipsCode, geo);
425
- }
426
- }
427
- >
428
- <path tabIndex={-1} className='single-geo' stroke={geoStrokeColor} d={path} strokeWidth='.85' />
429
- </g>
430
- );
431
- })
432
- );
433
- return output;
434
- });
435
-
436
- const GeoCodeCountyLines = memo(() => {
437
- return (
438
- <path d={countyLines} className="county-borders" style={{ stroke: geoStrokeColor}} />
439
- )
440
- })
441
-
442
- const StateOutput = memo(({ geographies, states }) => {
443
- let output = [];
444
- output.push(
445
- states.map(({ feature: geo, path = '' }) => {
446
- const key = geo.id + '-group';
447
-
448
- // Map the name from the geo data with the appropriate key for the processed data
449
- let geoKey = geo.id;
450
-
451
- if (!geoKey) return;
452
-
453
- const geoData = data[geoKey];
454
-
455
- let legendColors;
456
-
457
- // Once we receive data for this geographic item, setup variables.
458
- if (geoData !== undefined) {
459
- legendColors = applyLegendToRow(geoData);
460
- }
461
-
462
- const geoDisplayName = displayGeoName(geoKey);
463
-
464
- let stateStyles = {}
465
-
466
- if (state.general.type !== 'us-geocode') {
467
- stateStyles = {
468
- cursor: 'default',
469
- stroke: STATE_BORDER,
470
- strokeWidth: 0.75 / scale,
471
- display: !focusedState ? 'none' : focusedState && focusedState !== geo.id ? 'block' : 'none',
472
- fill: focusedState && focusedState !== geo.id ? STATE_INACTIVE_FILL : 'none',
473
- };
474
- } else {
475
- stateStyles = {
476
- cursor: 'default',
477
- stroke: STATE_BORDER,
478
- strokeWidth: 0.75 / scale,
479
- display: 'block',
480
- fill: '#f4f7fa',
481
- };
482
- }
483
-
484
- let stateSelectedStyles = {
485
- fillOpacity: 1,
486
- cursor: 'default',
487
- };
488
-
489
- let stateClasses = ['state', `state--${geo.properties.name}`, `state--${geo.id}`];
490
- focusedState === geo.id ? stateClasses.push('state--focused') : stateClasses.push('state--inactive');
491
-
492
- return (
493
- <React.Fragment key={`state--${key}`}>
494
- <g key={`state--${key}`} className={stateClasses.join(' ')} style={stateStyles} tabIndex="-1">
495
- <>
496
- <path
497
- tabIndex={-1}
498
- className='state-path'
499
- d={path}
500
- fillOpacity={`${focusedState !== geo.id ? '1' : '0'}`}
501
- fill={STATE_INACTIVE_FILL}
502
- css={stateSelectedStyles}
503
- onClick={(e) => {
504
- e.stopPropagation();
505
- e.nativeEvent.stopImmediatePropagation();
506
- focusGeo(geo.id, geo);
507
- }}
508
- onMouseEnter={(e) => {
509
- e.target.attributes.fill.value = colorPalettes[state.color][3];
510
- }}
511
- onMouseLeave={(e) => {
512
- e.target.attributes.fill.value = STATE_INACTIVE_FILL;
513
- }}
514
- />
515
- </>
516
- </g>
517
- <g key={`label--${key}`}>
518
- {offsets[geo.properties.name] &&
519
- geoLabel(geo, geoAlbersUsaTerritories().translate([WIDTH / 2, HEIGHT / 2]))}
520
- </g>
521
- </React.Fragment>
522
- );
523
- })
524
- );
525
- return output;
526
- });
527
-
528
- // Constructs and displays markup for all geos on the map (except territories right now)
529
- const constructGeoJsx = (geographies, projection) => {
530
- const states = geographies.slice(0, 56);
531
- const counties = geographies.slice(56);
532
- let geosJsx = [];
533
- {
534
- 'us-geocode' !== state.general.type &&
535
- geosJsx.push(<CountyOutput geographies={geographies} counties={counties} key="county-key" />);
536
- }
537
- {
538
- 'us-geocode' === state.general.type &&
539
- geosJsx.push(<GeoCodeCountyLines />);
540
- }
541
- geosJsx.push(<StateOutput geographies={geographies} states={states} key="state-key" />);
542
- geosJsx.push(
543
- <StateLines
544
- key='stateLines'
545
- lineWidth={startingLineWidth}
546
- geoStrokeColor={geoStrokeColor}
547
- stateLines={stateLines}
548
- />
549
- );
550
- geosJsx.push(<FocusedStateBorder key="focused-border-key" />);
551
- geosJsx.push(<CityList
552
- projection={projection}
553
- key="cities"
554
- data={data}
555
- state={state}
556
- geoClickHandler={geoClickHandler}
557
- applyTooltipsToGeo={applyTooltipsToGeo}
558
- displayGeoName={displayGeoName}
559
- applyLegendToRow={applyLegendToRow}
560
- titleCase={titleCase}
561
- setSharedFilterValue={setSharedFilterValue}
562
- isFilterValueSupported={isFilterValueSupported}
563
- isGeoCodeMap={state.general.type === 'us-geocode'}
564
- />)
565
- return geosJsx;
566
- };
567
- if(!data) <Loading />
568
- return (
569
- <ErrorBoundary component='CountyMap'>
570
- <svg
571
- viewBox={`0 0 ${WIDTH} ${HEIGHT}`}
572
- preserveAspectRatio='xMinYMin'
573
- className='svg-container'
574
- data-scale={scale ? scale : ''}
575
- data-translate={translate ? translate : ''}
576
- role="img"
577
- aria-label={handleMapAriaLabels(state)}
578
- >
579
- <rect
580
- className='background center-container ocean'
581
- width={WIDTH}
582
- height={HEIGHT}
583
- fillOpacity={1}
584
- fill='white'
585
- onClick={(e) => onReset(e)}
586
- tabIndex="0"
587
- ></rect>
588
- <CustomProjection
589
- data={mapData}
590
- translate={[WIDTH / 2, HEIGHT / 2]}
591
- projection={geoAlbersUsaTerritories}
592
- >
593
- {({ features, projection }) => {
594
- return (
595
- <g
596
- ref={mapGroup}
597
- className='countyMapGroup'
598
- transform={`translate(${translate}) scale(${scale})`}
599
- key='countyMapGroup'
600
- >
601
- {constructGeoJsx(features, projection)}
602
- </g>
603
- );
604
- }}
605
- </CustomProjection>
606
- </svg>
607
-
608
- {/* TODO: Refactor to COVE button */}
609
- <button className={`btn btn--reset`} onClick={onReset} ref={resetButton} style={{ display: 'none' }} tabIndex="0">
610
- Reset Zoom
611
- </button>
612
- </ErrorBoundary>
613
- );
614
- };
615
-
616
- export default memo(CountyMap, CountyMapChecks);
527
+ export default memo(CountyMap, CountyMapChecks)