@cdc/map 4.24.5 → 4.24.9

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 (67) hide show
  1. package/dist/cdcmap.js +71853 -64936
  2. package/examples/annotation/index.json +552 -0
  3. package/examples/annotation/usa-map.json +900 -0
  4. package/examples/county-year.csv +10 -0
  5. package/examples/default-geocode.json +44 -10
  6. package/examples/default-patterns.json +0 -2
  7. package/examples/default-single-state.json +279 -108
  8. package/examples/map-issue-3.json +646 -0
  9. package/examples/single-state-filter.json +153 -0
  10. package/index.html +10 -6
  11. package/package.json +6 -5
  12. package/src/CdcMap.tsx +367 -199
  13. package/src/_stories/CdcMap.stories.tsx +14 -0
  14. package/src/_stories/_mock/DEV-7286.json +165 -0
  15. package/src/_stories/_mock/DEV-8942.json +270 -0
  16. package/src/components/Annotation/Annotation.Draggable.styles.css +18 -0
  17. package/src/components/Annotation/Annotation.Draggable.tsx +152 -0
  18. package/src/components/Annotation/AnnotationDropdown.styles.css +14 -0
  19. package/src/components/Annotation/AnnotationDropdown.tsx +70 -0
  20. package/src/components/Annotation/AnnotationList.styles.css +45 -0
  21. package/src/components/Annotation/AnnotationList.tsx +42 -0
  22. package/src/components/Annotation/index.tsx +11 -0
  23. package/src/components/{BubbleList.jsx → BubbleList.tsx} +1 -1
  24. package/src/components/{CityList.jsx → CityList.tsx} +28 -2
  25. package/src/components/{DataTable.jsx → DataTable.tsx} +2 -2
  26. package/src/components/EditorPanel/components/EditorPanel.tsx +650 -129
  27. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +336 -0
  28. package/src/components/EditorPanel/components/{Panel.PatternSettings.tsx → Panels/Panel.PatternSettings.tsx} +63 -13
  29. package/src/components/EditorPanel/components/{Panels.tsx → Panels/index.tsx} +3 -0
  30. package/src/components/Legend/components/Legend.tsx +125 -42
  31. package/src/components/Legend/components/index.scss +42 -42
  32. package/src/components/Modal.tsx +25 -0
  33. package/src/components/UsaMap/components/SingleState/SingleState.CountyOutput.tsx +74 -0
  34. package/src/components/UsaMap/components/SingleState/SingleState.StateOutput.tsx +29 -0
  35. package/src/components/UsaMap/components/SingleState/index.tsx +9 -0
  36. package/src/components/UsaMap/components/Territory/Territory.Rectangle.tsx +4 -3
  37. package/src/components/UsaMap/components/UsaMap.County.tsx +114 -36
  38. package/src/components/UsaMap/components/UsaMap.Region.tsx +2 -0
  39. package/src/components/UsaMap/components/UsaMap.SingleState.tsx +175 -206
  40. package/src/components/UsaMap/components/UsaMap.State.tsx +188 -44
  41. package/src/components/UsaMap/data/us-extended-geography.json +1 -0
  42. package/src/components/UsaMap/helpers/map.ts +111 -0
  43. package/src/components/WorldMap/WorldMap.tsx +17 -32
  44. package/src/components/ZoomControls.tsx +41 -0
  45. package/src/data/initial-state.js +11 -2
  46. package/src/data/supported-geos.js +15 -4
  47. package/src/helpers/generateColorsArray.ts +13 -0
  48. package/src/helpers/generateRuntimeLegendHash.ts +23 -0
  49. package/src/helpers/getUniqueValues.ts +19 -0
  50. package/src/helpers/hashObj.ts +25 -0
  51. package/src/helpers/tests/generateColorsArray.test.ts +18 -0
  52. package/src/helpers/tests/generateRuntimeLegendHash.test.ts +11 -0
  53. package/src/helpers/tests/hashObj.test.ts +10 -0
  54. package/src/hooks/useStateZoom.tsx +157 -0
  55. package/src/hooks/{useZoomPan.js → useZoomPan.ts} +6 -5
  56. package/src/scss/editor-panel.scss +0 -4
  57. package/src/scss/main.scss +23 -1
  58. package/src/scss/map.scss +14 -3
  59. package/src/types/MapConfig.ts +9 -1
  60. package/src/types/MapContext.ts +16 -2
  61. package/LICENSE +0 -201
  62. package/src/components/Modal.jsx +0 -22
  63. package/src/test/CdcMap.test.jsx +0 -19
  64. /package/src/components/EditorPanel/components/{Panel.PatternSettings-style.css → Panels/Panel.PatternSettings-style.css} +0 -0
  65. /package/src/components/{Geo.jsx → Geo.tsx} +0 -0
  66. /package/src/components/{NavigationMenu.jsx → NavigationMenu.tsx} +0 -0
  67. /package/src/components/{ZoomableGroup.jsx → ZoomableGroup.tsx} +0 -0
@@ -16,13 +16,14 @@ import { geoAlbersUsa } from 'd3-composite-projections'
16
16
  import { PatternLines, PatternCircles, PatternWaves } from '@visx/pattern'
17
17
  import HexIcon from './HexIcon'
18
18
  import { patternSizes } from '../helpers/patternSizes'
19
+ import Annotation from '../../Annotation'
19
20
 
20
21
  import Territory from './Territory'
21
22
 
22
23
  import useMapLayers from '../../../hooks/useMapLayers'
23
24
  import ConfigContext from '../../../context'
24
25
  import { MapContext } from '../../../types/MapContext'
25
- import { getContrastColor } from '@cdc/core/helpers/cove/accessibility'
26
+ import { checkColorContrast, getContrastColor, getColorContrast } from '@cdc/core/helpers/cove/accessibility'
26
27
 
27
28
  const { features: unitedStates } = feature(topoJSON, topoJSON.objects.states)
28
29
  const { features: unitedStatesHex } = feature(hexTopoJSON, hexTopoJSON.objects.states)
@@ -64,7 +65,10 @@ const UsaMap = () => {
64
65
  state,
65
66
  supportedTerritories,
66
67
  titleCase,
67
- tooltipId
68
+ tooltipId,
69
+ handleDragStateChange,
70
+ setState,
71
+ mapId
68
72
  } = useContext<MapContext>(ConfigContext)
69
73
 
70
74
  let isFilterValueSupported = false
@@ -118,7 +122,15 @@ const UsaMap = () => {
118
122
 
119
123
  const geoStrokeColor = state.general.geoBorderColor === 'darkGray' ? 'rgba(0, 0, 0, 0.2)' : 'rgba(255,255,255,0.7)'
120
124
 
121
- const territories = territoriesData.map(territory => {
125
+ const getTerritoriesClasses = () => {
126
+ const screenWidth = window?.visualViewport?.width
127
+ let className = 'territories'
128
+ if (screenWidth < 700) return 'territories--mobile'
129
+ if (screenWidth < 900) return 'territories--tablet'
130
+ return className
131
+ }
132
+
133
+ const territories = territoriesData.map((territory, territoryIndex) => {
122
134
  const Shape = isHex ? Territory.Hexagon : Territory.Rectangle
123
135
 
124
136
  const territoryData = data[territory]
@@ -144,15 +156,28 @@ const UsaMap = () => {
144
156
  let needsPointer = false
145
157
 
146
158
  // If we need to add a pointer cursor
147
- if ((state.columns.navigate && territoryData[state.columns.navigate.name]) || state.tooltips.appearanceType === 'click') {
159
+ if (
160
+ (state.columns.navigate && territoryData[state.columns.navigate.name]) ||
161
+ state.tooltips.appearanceType === 'click'
162
+ ) {
148
163
  needsPointer = true
149
164
  }
150
165
 
151
166
  styles = {
152
167
  color: textColor,
153
168
  fill: legendColors[0],
154
- opacity: setSharedFilterValue && isFilterValueSupported && setSharedFilterValue !== territoryData[state.columns.geo.name] ? 0.5 : 1,
155
- stroke: setSharedFilterValue && isFilterValueSupported && setSharedFilterValue === territoryData[state.columns.geo.name] ? 'rgba(0, 0, 0, 1)' : geoStrokeColor,
169
+ opacity:
170
+ setSharedFilterValue &&
171
+ isFilterValueSupported &&
172
+ setSharedFilterValue !== territoryData[state.columns.geo.name]
173
+ ? 0.5
174
+ : 1,
175
+ stroke:
176
+ setSharedFilterValue &&
177
+ isFilterValueSupported &&
178
+ setSharedFilterValue === territoryData[state.columns.geo.name]
179
+ ? 'rgba(0, 0, 0, 1)'
180
+ : geoStrokeColor,
156
181
  cursor: needsPointer ? 'pointer' : 'default',
157
182
  '&:hover': {
158
183
  fill: legendColors[1]
@@ -163,22 +188,20 @@ const UsaMap = () => {
163
188
  }
164
189
 
165
190
  return (
166
- <>
167
- <Shape
168
- key={label}
169
- label={label}
170
- style={styles}
171
- text={styles.color}
172
- strokeWidth={1.5}
173
- textColor={textColor}
174
- onClick={() => geoClickHandler(territory, territoryData)}
175
- data-tooltip-id={`tooltip__${tooltipId}`}
176
- data-tooltip-html={toolTip}
177
- territory={territory}
178
- territoryData={territoryData}
179
- tabIndex={-1}
180
- />
181
- </>
191
+ <Shape
192
+ key={`label__${territoryIndex}`}
193
+ label={label}
194
+ style={styles}
195
+ text={styles.color}
196
+ strokeWidth={1.5}
197
+ textColor={textColor}
198
+ onClick={() => geoClickHandler(territory, territoryData)}
199
+ data-tooltip-id={`tooltip__${tooltipId}`}
200
+ data-tooltip-html={toolTip}
201
+ territory={territory}
202
+ territoryData={territoryData}
203
+ tabIndex={-1}
204
+ />
182
205
  )
183
206
  }
184
207
  })
@@ -245,8 +268,14 @@ const UsaMap = () => {
245
268
 
246
269
  styles = {
247
270
  fill: state.general.type !== 'bubble' ? legendColors[0] : '#E6E6E6',
248
- opacity: setSharedFilterValue && isFilterValueSupported && setSharedFilterValue !== geoData[state.columns.geo.name] ? 0.5 : 1,
249
- stroke: setSharedFilterValue && isFilterValueSupported && setSharedFilterValue === geoData[state.columns.geo.name] ? 'rgba(0, 0, 0, 1)' : geoStrokeColor,
271
+ opacity:
272
+ setSharedFilterValue && isFilterValueSupported && setSharedFilterValue !== geoData[state.columns.geo.name]
273
+ ? 0.5
274
+ : 1,
275
+ stroke:
276
+ setSharedFilterValue && isFilterValueSupported && setSharedFilterValue === geoData[state.columns.geo.name]
277
+ ? 'rgba(0, 0, 0, 1)'
278
+ : geoStrokeColor,
250
279
  cursor: 'default',
251
280
  '&:hover': {
252
281
  fill: state.general.type !== 'bubble' ? legendColors[1] : '#e6e6e6'
@@ -257,7 +286,10 @@ const UsaMap = () => {
257
286
  }
258
287
 
259
288
  // When to add pointer cursor
260
- if ((state.columns.navigate && geoData[state.columns.navigate.name]) || state.tooltips.appearanceType === 'click') {
289
+ if (
290
+ (state.columns.navigate && geoData[state.columns.navigate.name]) ||
291
+ state.tooltips.appearanceType === 'click'
292
+ ) {
261
293
  styles.cursor = 'pointer'
262
294
  }
263
295
 
@@ -275,33 +307,81 @@ const UsaMap = () => {
275
307
  switch (item.operator) {
276
308
  case '=':
277
309
  if (geoData[item.key] === item.value || Number(geoData[item.key]) === Number(item.value)) {
278
- return <HexIcon textColor={textColor} item={item} index={itemIndex} centroid={centroid} iconSize={iconSize} />
310
+ return (
311
+ <HexIcon
312
+ textColor={textColor}
313
+ item={item}
314
+ index={itemIndex}
315
+ centroid={centroid}
316
+ iconSize={iconSize}
317
+ />
318
+ )
279
319
  }
280
320
  break
281
321
  case '≠':
282
322
  if (geoData[item.key] !== item.value && Number(geoData[item.key]) !== Number(item.value)) {
283
- return <HexIcon textColor={textColor} item={item} index={itemIndex} centroid={centroid} iconSize={iconSize} />
323
+ return (
324
+ <HexIcon
325
+ textColor={textColor}
326
+ item={item}
327
+ index={itemIndex}
328
+ centroid={centroid}
329
+ iconSize={iconSize}
330
+ />
331
+ )
284
332
  }
285
333
  break
286
334
  case '<':
287
335
  if (Number(geoData[item.key]) < Number(item.value)) {
288
- return <HexIcon textColor={textColor} item={item} index={itemIndex} centroid={centroid} iconSize={iconSize} />
336
+ return (
337
+ <HexIcon
338
+ textColor={textColor}
339
+ item={item}
340
+ index={itemIndex}
341
+ centroid={centroid}
342
+ iconSize={iconSize}
343
+ />
344
+ )
289
345
  }
290
346
  break
291
347
  case '>':
292
348
  if (Number(geoData[item.key]) > Number(item.value)) {
293
- return <HexIcon textColor={textColor} item={item} index={itemIndex} centroid={centroid} iconSize={iconSize} />
349
+ return (
350
+ <HexIcon
351
+ textColor={textColor}
352
+ item={item}
353
+ index={itemIndex}
354
+ centroid={centroid}
355
+ iconSize={iconSize}
356
+ />
357
+ )
294
358
  }
295
359
  break
296
360
  case '<=':
297
361
  if (Number(geoData[item.key]) <= Number(item.value)) {
298
- return <HexIcon textColor={textColor} item={item} index={itemIndex} centroid={centroid} iconSize={iconSize} />
362
+ return (
363
+ <HexIcon
364
+ textColor={textColor}
365
+ item={item}
366
+ index={itemIndex}
367
+ centroid={centroid}
368
+ iconSize={iconSize}
369
+ />
370
+ )
299
371
  }
300
372
  break
301
373
  case '>=':
302
374
  if (item.operator === '>=') {
303
375
  if (Number(geoData[item.key]) >= Number(item.value)) {
304
- return <HexIcon textColor={textColor} item={item} index={itemIndex} centroid={centroid} iconSize={iconSize} />
376
+ return (
377
+ <HexIcon
378
+ textColor={textColor}
379
+ item={item}
380
+ index={itemIndex}
381
+ centroid={centroid}
382
+ iconSize={iconSize}
383
+ />
384
+ )
305
385
  }
306
386
  }
307
387
  break
@@ -316,7 +396,15 @@ const UsaMap = () => {
316
396
 
317
397
  return (
318
398
  <g data-name={geoName} key={key} tabIndex={-1}>
319
- <g className='geo-group' style={styles} onClick={() => geoClickHandler(geoDisplayName, geoData)} id={geoName} data-tooltip-id={`tooltip__${tooltipId}`} data-tooltip-html={tooltip} tabIndex={-1}>
399
+ <g
400
+ className='geo-group'
401
+ style={styles}
402
+ onClick={() => geoClickHandler(geoDisplayName, geoData)}
403
+ id={geoName}
404
+ data-tooltip-id={`tooltip__${tooltipId}`}
405
+ data-tooltip-html={tooltip}
406
+ tabIndex={-1}
407
+ >
320
408
  {/* state path */}
321
409
  <path tabIndex={-1} className='single-geo' strokeWidth={1.3} d={path} />
322
410
 
@@ -327,15 +415,45 @@ const UsaMap = () => {
327
415
  const hasMatchingValues = patternData.dataValue === geoData[patternData.dataKey]
328
416
  const patternColor = patternData.color || getContrastColor('#000', currentFill)
329
417
 
418
+ if (!hasMatchingValues) return
419
+ checkColorContrast(currentFill, patternColor)
420
+
330
421
  return (
331
- hasMatchingValues && (
332
- <>
333
- {pattern === 'waves' && <PatternWaves id={`${dataKey}--${geoIndex}`} height={patternSizes[size] ?? 10} width={patternSizes[size] ?? 10} fill={patternColor} />}
334
- {pattern === 'circles' && <PatternCircles id={`${dataKey}--${geoIndex}`} height={patternSizes[size] ?? 10} width={patternSizes[size] ?? 10} fill={patternColor} />}
335
- {pattern === 'lines' && <PatternLines id={`${dataKey}--${geoIndex}`} height={patternSizes[size] ?? 6} width={patternSizes[size] ?? 6} stroke={patternColor} strokeWidth={1} orientation={['diagonalRightToLeft']} />}
336
- <path className={`pattern-geoKey--${dataKey}`} tabIndex={-1} stroke='transparent' d={path} fill={`url(#${dataKey}--${geoIndex})`} />
337
- </>
338
- )
422
+ <>
423
+ {pattern === 'waves' && (
424
+ <PatternWaves
425
+ id={`${mapId}--${dataKey}--${geoIndex}`}
426
+ height={patternSizes[size] ?? 10}
427
+ width={patternSizes[size] ?? 10}
428
+ fill={patternColor}
429
+ />
430
+ )}
431
+ {pattern === 'circles' && (
432
+ <PatternCircles
433
+ id={`${mapId}--${dataKey}--${geoIndex}`}
434
+ height={patternSizes[size] ?? 10}
435
+ width={patternSizes[size] ?? 10}
436
+ fill={patternColor}
437
+ />
438
+ )}
439
+ {pattern === 'lines' && (
440
+ <PatternLines
441
+ id={`${mapId}--${dataKey}--${geoIndex}`}
442
+ height={patternSizes[size] ?? 6}
443
+ width={patternSizes[size] ?? 6}
444
+ stroke={patternColor}
445
+ strokeWidth={1}
446
+ orientation={['diagonalRightToLeft']}
447
+ />
448
+ )}
449
+ <path
450
+ className={`pattern-geoKey--${dataKey}`}
451
+ tabIndex={-1}
452
+ stroke='transparent'
453
+ d={path}
454
+ fill={`url(#${mapId}--${dataKey}--${geoIndex})`}
455
+ />
456
+ </>
339
457
  )
340
458
  })}
341
459
  {(isHex || showLabel) && geoLabel(geo, legendColors[0], projection)}
@@ -378,7 +496,18 @@ const UsaMap = () => {
378
496
 
379
497
  // Bubbles
380
498
  if (state.general.type === 'bubble') {
381
- geosJsx.push(<BubbleList key='bubbles' data={state.data} runtimeData={data} state={state} projection={projection} applyLegendToRow={applyLegendToRow} applyTooltipsToGeo={applyTooltipsToGeo} displayGeoName={displayGeoName} />)
499
+ geosJsx.push(
500
+ <BubbleList
501
+ key='bubbles'
502
+ data={state.data}
503
+ runtimeData={data}
504
+ state={state}
505
+ projection={projection}
506
+ applyLegendToRow={applyLegendToRow}
507
+ applyTooltipsToGeo={applyTooltipsToGeo}
508
+ displayGeoName={displayGeoName}
509
+ />
510
+ )
382
511
  }
383
512
 
384
513
  // })
@@ -427,8 +556,22 @@ const UsaMap = () => {
427
556
 
428
557
  return (
429
558
  <g tabIndex={-1}>
430
- <line x1={centroid[0]} y1={centroid[1]} x2={centroid[0] + dx} y2={centroid[1] + dy} stroke='rgba(0,0,0,.5)' strokeWidth={1} />
431
- <text x={4} strokeWidth='0' fontSize={13} style={{ fill: '#202020' }} alignmentBaseline='middle' transform={`translate(${centroid[0] + dx}, ${centroid[1] + dy})`}>
559
+ <line
560
+ x1={centroid[0]}
561
+ y1={centroid[1]}
562
+ x2={centroid[0] + dx}
563
+ y2={centroid[1] + dy}
564
+ stroke='rgba(0,0,0,.5)'
565
+ strokeWidth={1}
566
+ />
567
+ <text
568
+ x={4}
569
+ strokeWidth='0'
570
+ fontSize={13}
571
+ style={{ fill: '#202020' }}
572
+ alignmentBaseline='middle'
573
+ transform={`translate(${centroid[0] + dx}, ${centroid[1] + dy})`}
574
+ >
432
575
  {abbr.substring(3)}
433
576
  </text>
434
577
  </g>
@@ -447,6 +590,7 @@ const UsaMap = () => {
447
590
  {({ features, projection }) => constructGeoJsx(features, projection)}
448
591
  </AlbersUsa>
449
592
  )}
593
+ {state.annotations.length > 0 && <Annotation.Draggable onDragStateChange={handleDragStateChange} />}
450
594
  </svg>
451
595
 
452
596
  {territories.length > 0 && (
@@ -456,7 +600,7 @@ const UsaMap = () => {
456
600
  <span className='territories-label label'>{state.general.territoriesLabel}</span>
457
601
  </div>
458
602
  <div>
459
- <span className={window.visualViewport.width < 700 ? 'territories--mobile' : 'territories'}>{territories}</span>
603
+ <span className={getTerritoriesClasses()}>{territories}</span>
460
604
  </div>
461
605
  </div>
462
606
  </>
@@ -0,0 +1 @@
1
+ {"type":"Topology","arcs":[[[128628,58684],[30,57],[-17,63],[30,47],[44,-21],[35,35],[74,5],[26,-46],[170,8],[9,-28],[-140,-76],[-178,-47],[-83,3]],[[1608,280],[113,17],[5,-80],[-39,13],[-35,-44],[-44,94]],[[1408,384],[91,12],[-17,-33],[-57,-19],[-17,40]],[[122,156],[48,-77]],[[170,79],[-65,-79],[-48,74],[-57,25],[57,55],[65,2]],[[128706,59939],[0,-30]],[[128706,59909],[26,-69],[-69,-17],[-66,36],[-30,-32],[-52,78],[-105,-23],[30,85],[44,-36],[52,41],[83,-33],[87,0]],[[384328,54018],[17,44],[9,159],[39,-19],[87,105],[22,-34],[-61,-126],[17,-99],[-56,11],[0,-115],[-53,28],[-21,46]],[[128759,59886],[56,61],[48,-6],[66,-44],[-57,-7],[4,-77],[-39,32],[-57,-8],[-21,49]],[[128706,59939],[57,10],[-9,-37],[-48,-3]],[[383033,50942],[22,23],[148,25],[92,216],[26,102],[30,-17],[31,-68],[61,-17],[-44,-139],[-139,-171],[-39,-69],[-9,-175],[-52,-88],[-79,47],[-30,159],[35,79],[-53,93]],[[384201,53806],[18,70],[56,85],[22,-85],[-17,-37],[30,-63],[-9,-96],[-39,-46],[-61,172]],[[384746,55614],[44,68],[17,-25],[-35,-71],[-26,28]],[[384485,58543],[21,31],[5,-77],[-26,46]],[[384354,59393],[61,84],[0,93],[48,0],[0,-104],[-40,-21],[-43,-86],[-26,34]],[[384267,60694],[26,61],[48,-44],[-5,-80],[-35,-36],[-30,39],[-4,60]],[[384262,56279],[87,4],[13,-57],[-82,-13],[-18,66]],[[122,156],[87,108],[18,-31],[117,3],[18,-69],[-31,20],[-126,-47],[-35,-61]],[[383639,52173],[48,80],[92,55],[65,-14],[-9,-55],[-52,-20],[-35,-62],[-48,-9],[-22,36],[-39,-11]],[[113802,97593],[91,110],[48,-53],[113,-133],[-156,-185],[-5,88],[5,58],[-96,115]],[[126645,59224],[-48,-26],[-61,38],[-52,-30],[-31,21],[-74,37],[-96,-88],[-17,10],[-9,-2],[-65,-12],[-18,-38],[-65,47],[-4,27],[-92,12],[-61,-52],[-70,32],[-52,-19],[4,83],[44,49],[-48,19],[13,102],[22,24],[4,118],[31,49],[-40,132],[-47,55],[-57,119],[39,24],[96,74],[-9,115],[31,50],[48,16],[100,-7],[78,-39],[66,-9],[78,6],[87,-6],[70,-22],[105,40],[43,-17],[66,-6],[78,-24],[35,32],[109,0],[43,-21],[53,10],[87,-25],[48,-24],[39,35],[109,-41],[57,14],[104,-13],[92,-50],[43,-2],[53,-69],[100,-39],[52,44],[-9,-72],[14,-109],[56,-71],[-30,-67],[-48,29],[13,-44],[-39,4],[-35,-51],[-35,21],[-26,-32],[-44,-80],[-26,-110],[-39,-30],[-26,-75],[-74,-57],[-79,-24],[-52,19],[-31,-42],[-30,20],[-105,-67],[-43,14],[-31,-40],[-30,-3],[-27,57],[-91,58],[-57,-69],[-78,82],[-48,12],[-61,-26]],[[128105,59906],[48,-33],[6,2],[46,18],[39,-56],[-74,-57],[-11,7],[-41,23],[-13,96]],[[124954,59472],[87,19],[22,-68],[-65,-60],[-53,51],[9,58]],[[127817,59480],[214,85],[152,-51],[-78,-40],[-92,-11],[-48,-38],[-34,19],[-75,-27],[-39,63]],[[127795,59986],[31,-22],[0,-69],[-31,91]]],"transform":{"scale":[0.000823601132486181,0.000546229125192084],"translate":[-170.84530299432993,-14.373864584355845]},"objects":{"counties":{"type":"GeometryCollection","geometries":[{"arcs":[[[1]],[[2]]],"type":"MultiPolygon","properties":{"name":"Manu'a"},"id":"60020"},{"arcs":[[3,4]],"type":"Polygon","properties":{"name":"Western"},"id":"60050"},{"arcs":[[5,6]],"type":"Polygon","properties":{"name":"St. Thomas"},"id":"78030"},{"arcs":[[7]],"type":"Polygon","properties":{"name":"Saipan"},"id":"69110"},{"arcs":[[[8]],[[9,-6]]],"type":"MultiPolygon","properties":{"name":"St. John"},"id":"78020"},{"arcs":[[10]],"type":"Polygon","properties":{"name":"Guam"},"id":"66010"},{"arcs":[[11]],"type":"Polygon","properties":{"name":"Tinian"},"id":"69120"},{"arcs":[[[12]],[[13]],[[14]],[[15]],[[16]]],"type":"MultiPolygon","properties":{"name":"Northern Islands"},"id":"69085"},{"arcs":[[-4,17]],"type":"Polygon","properties":{"name":"Eastern"},"id":"60010"},{"arcs":[[18]],"type":"Polygon","properties":{"name":"Rota"},"id":"69100"},{"arcs":[[0]],"type":"Polygon","properties":{"name":"St. Croix"},"id":"78010"},{"arcs":[[0]],"type":"Polygon","properties":{"name":"District of Columbia"},"id":"11"}]},"states":{"type":"GeometryCollection","geometries":[{"arcs":[[[20]],[[21]],[[22]],[[23]],[[24]]],"type":"MultiPolygon","properties":{"name":"Puerto Rico"},"id":"72"},{"arcs":[[[1]],[[2]],[[4,17]]],"type":"MultiPolygon","properties":{"name":"American Samoa"},"id":"60"},{"arcs":[[[6,9]],[[0]],[[8]]],"type":"MultiPolygon","properties":{"name":"United States Virgin Islands"},"id":"78"},{"arcs":[[[7]],[[11]],[[12]],[[13]],[[14]],[[15]],[[16]],[[18]]],"type":"MultiPolygon","properties":{"name":"Commonwealth of the Northern Mariana Islands"},"id":"69"},{"arcs":[[10]],"type":"Polygon","properties":{"name":"Guam"},"id":"66"}]}}}
@@ -0,0 +1,111 @@
1
+ import { feature } from 'topojson-client'
2
+ import usExtendedGeography from './../data/us-extended-geography.json'
3
+
4
+ export const getCountyTopoURL = year => {
5
+ return `https://www.cdc.gov/TemplatePackage/contrib/data/county-topography/cb_${year}_us_county_20m.json`
6
+ }
7
+
8
+ export const getTopoData = year => {
9
+ return new Promise((resolve, reject) => {
10
+ const resolveWithTopo = async response => {
11
+ if (response.status !== 200) {
12
+ response = await import('./../data/cb_2019_us_county_20m.json')
13
+ } else {
14
+ response = await response.json()
15
+ }
16
+
17
+ const counties = [response, usExtendedGeography].flatMap(topo => feature(topo, topo.objects.counties).features)
18
+ const states = [response, usExtendedGeography].flatMap(topo => feature(topo, topo.objects.states).features)
19
+
20
+ const topoData = {
21
+ year: year || 'default',
22
+ fulljson: response,
23
+ counties,
24
+ states
25
+ }
26
+
27
+ resolve(topoData)
28
+ }
29
+
30
+ const numericYear = parseInt(year)
31
+
32
+ if (isNaN(numericYear)) {
33
+ fetch(getCountyTopoURL(2019)).then(resolveWithTopo)
34
+ } else if (numericYear > 2022) {
35
+ fetch(getCountyTopoURL(2022)).then(resolveWithTopo)
36
+ } else if (numericYear < 2013) {
37
+ fetch(getCountyTopoURL(2013)).then(resolveWithTopo)
38
+ } else {
39
+ switch (numericYear) {
40
+ case 2022:
41
+ fetch(getCountyTopoURL(2022)).then(resolveWithTopo)
42
+ break
43
+ case 2021:
44
+ fetch(getCountyTopoURL(2021)).then(resolveWithTopo)
45
+ break
46
+ case 2020:
47
+ fetch(getCountyTopoURL(2020)).then(resolveWithTopo)
48
+ break
49
+ case 2018:
50
+ case 2017:
51
+ case 2016:
52
+ case 2015:
53
+ fetch(getCountyTopoURL(2015)).then(resolveWithTopo)
54
+ break
55
+ case 2014:
56
+ fetch(getCountyTopoURL(2014)).then(resolveWithTopo)
57
+ break
58
+ case 2013:
59
+ fetch(getCountyTopoURL(2013)).then(resolveWithTopo)
60
+ break
61
+ default:
62
+ fetch(getCountyTopoURL(2019)).then(resolveWithTopo)
63
+ break
64
+ }
65
+ }
66
+ })
67
+ }
68
+
69
+ export const getCurrentTopoYear = (state, runtimeFilters) => {
70
+ let currentYear = state.general.countyCensusYear
71
+
72
+ if (state.general.filterControlsCountyYear && runtimeFilters && runtimeFilters.length > 0) {
73
+ let yearFilter = runtimeFilters.filter(filter => filter.columnName === state.general.filterControlsCountyYear)
74
+ if (yearFilter.length > 0 && yearFilter[0].active) {
75
+ currentYear = yearFilter[0].active
76
+ }
77
+ }
78
+
79
+ return currentYear || 'default'
80
+ }
81
+
82
+ export const isTopoReady = (topoData, state, runtimeFilters) => {
83
+ let currentYear = getCurrentTopoYear(state, runtimeFilters)
84
+
85
+ return topoData.year && (!currentYear || currentYear === topoData.year)
86
+ }
87
+
88
+ export const hasMoreThanFromHash = (data: { [key: string]: any }): boolean => {
89
+ // Get all keys of the data object
90
+ const keys = Object.keys(data)
91
+
92
+ // Filter out the 'fromHash' key
93
+ const otherKeys = keys.filter(key => key !== 'fromHash')
94
+
95
+ // Check if there are any other keys left
96
+ return otherKeys.length > 0
97
+ }
98
+
99
+ export const getFilterControllingStatePicked = (state, runtimeData) => {
100
+ if (!state.general.filterControlsStatePicked || !runtimeData) {
101
+ const statePicked = state?.general?.statePicked?.stateName
102
+ return statePicked
103
+ } else {
104
+ if (hasMoreThanFromHash(runtimeData)) {
105
+ let statePickedFromFilter = Object.values(runtimeData)?.map(s => s[state.general.filterControlsStatePicked])?.[0]
106
+ const statePicked = statePickedFromFilter || state.general.statePicked.stateName || 'Alabama'
107
+ return statePicked
108
+ }
109
+ return null
110
+ }
111
+ }
@@ -10,6 +10,7 @@ import Geo from '../Geo'
10
10
  import CityList from '../CityList'
11
11
  import BubbleList from '../BubbleList'
12
12
  import ConfigContext from '../../context'
13
+ import ZoomControls from '../ZoomControls'
13
14
 
14
15
  const { features: world } = feature(topoJSON, topoJSON.objects.countries)
15
16
 
@@ -34,7 +35,9 @@ const WorldMap = () => {
34
35
  state,
35
36
  supportedCountries,
36
37
  titleCase,
37
- tooltipId
38
+ tooltipId,
39
+ setScale,
40
+ setTranslate
38
41
  } = useContext(ConfigContext)
39
42
 
40
43
  // TODO Refactor - state should be set together here to avoid rerenders
@@ -59,35 +62,6 @@ const WorldMap = () => {
59
62
  setPosition(pos => ({ ...pos, zoom: pos.zoom / 1.5 }))
60
63
  }
61
64
 
62
- const ZoomControls = ({ position, setPosition, state, setState, setRuntimeData, generateRuntimeData }) => {
63
- if (!state.general.allowMapZoom) return
64
- return (
65
- <div className='zoom-controls' data-html2canvas-ignore>
66
- <button onClick={() => handleZoomIn(position, setPosition)} aria-label='Zoom In'>
67
- <svg viewBox='0 0 24 24' stroke='currentColor' strokeWidth='3'>
68
- <line x1='12' y1='5' x2='12' y2='19' />
69
- <line x1='5' y1='12' x2='19' y2='12' />
70
- </svg>
71
- </button>
72
- <button onClick={() => handleZoomOut(position, setPosition)} aria-label='Zoom Out'>
73
- <svg viewBox='0 0 24 24' stroke='currentColor' strokeWidth='3'>
74
- <line x1='5' y1='12' x2='19' y2='12' />
75
- </svg>
76
- </button>
77
- {state.general.type === 'bubble' && (
78
- <button onClick={() => handleReset(state, setState, setRuntimeData, generateRuntimeData)} className='reset' aria-label='Reset Zoom and Map Filters'>
79
- Reset Filters
80
- </button>
81
- )}
82
- {state.general.type === 'world-geocode' && (
83
- <button onClick={() => handleReset(state, setState, setRuntimeData, generateRuntimeData)} className='reset' aria-label='Reset Zoom'>
84
- Reset Zoom
85
- </button>
86
- )}
87
- </div>
88
- )
89
- }
90
-
91
65
  // TODO Refactor - state should be set together here to avoid rerenders
92
66
  const handleCircleClick = (country, state, setState, setRuntimeData, generateRuntimeData) => {
93
67
  if (!state.general.allowMapZoom) return
@@ -173,7 +147,7 @@ const WorldMap = () => {
173
147
  }
174
148
 
175
149
  // Default return state, just geo with no additional information
176
- return <Geo additionalData={additionalData} geoData={geoData} state={state} key={i + '-geo'} stroke={geoStrokeColor} strokeWidth={strokeWidth} style={styles} path={path} />
150
+ return <Geo additionaldata={JSON.stringify(additionalData)} geodata={JSON.stringify(geoData)} state={state} key={i + '-geo'} stroke={geoStrokeColor} strokeWidth={strokeWidth} style={styles} path={path} />
177
151
  })
178
152
 
179
153
  // Cities
@@ -217,7 +191,18 @@ const WorldMap = () => {
217
191
  </svg>
218
192
  )}
219
193
  {(state.general.type === 'data' || (state.general.type === 'world-geocode' && hasZoom) || (state.general.type === 'bubble' && hasZoom)) && (
220
- <ZoomControls position={position} setPosition={setPosition} setRuntimeData={setRuntimeData} state={state} setState={setState} generateRuntimeData={generateRuntimeData} />
194
+ <ZoomControls
195
+ // prettier-ignore
196
+ generateRuntimeData={generateRuntimeData}
197
+ handleZoomIn={handleZoomIn}
198
+ handleZoomOut={handleZoomOut}
199
+ position={position}
200
+ setPosition={setPosition}
201
+ setRuntimeData={setRuntimeData}
202
+ setState={setState}
203
+ state={state}
204
+ handleReset={handleReset}
205
+ />
221
206
  )}
222
207
  </ErrorBoundary>
223
208
  )
@@ -0,0 +1,41 @@
1
+ import React, { useContext } from 'react'
2
+ import { MapConfig } from '../types/MapConfig'
3
+ import ConfigContext from '../context'
4
+
5
+ type ZoomControlsProps = {
6
+ handleZoomIn: (coordinates: [Number, Number], setPosition: Function) => void
7
+ handleZoomOut: (coordinates: [Number, Number], setPosition: Function) => void
8
+ handleReset: (coordinates: [Number, Number], setPosition: Function) => void
9
+ }
10
+
11
+ const ZoomControls: React.FC<ZoomControlsProps> = ({ handleZoomIn, handleZoomOut, handleReset }) => {
12
+ const { state, setState, setRuntimeData, setPosition, position, generateRuntimeData } = useContext<MapContext>(ConfigContext)
13
+ if (!state.general.allowMapZoom) return
14
+ return (
15
+ <div className='zoom-controls' data-html2canvas-ignore>
16
+ <button onClick={() => handleZoomIn(position, setPosition)} aria-label='Zoom In'>
17
+ <svg viewBox='0 0 24 24' stroke='currentColor' strokeWidth='3'>
18
+ <line x1='12' y1='5' x2='12' y2='19' />
19
+ <line x1='5' y1='12' x2='19' y2='12' />
20
+ </svg>
21
+ </button>
22
+ <button onClick={() => handleZoomOut(position, setPosition)} aria-label='Zoom Out'>
23
+ <svg viewBox='0 0 24 24' stroke='currentColor' strokeWidth='3'>
24
+ <line x1='5' y1='12' x2='19' y2='12' />
25
+ </svg>
26
+ </button>
27
+ {state.general.type === 'bubble' && (
28
+ <button onClick={() => handleReset(state, setState, setRuntimeData, generateRuntimeData)} className='reset' aria-label='Reset Zoom and Map Filters'>
29
+ Reset Filters
30
+ </button>
31
+ )}
32
+ {(state.general.type === 'world-geocode' || state.general.geoType === 'single-state') && (
33
+ <button onClick={() => handleReset(state, setState, setRuntimeData, generateRuntimeData)} className='reset' aria-label='Reset Zoom'>
34
+ Reset Zoom
35
+ </button>
36
+ )}
37
+ </div>
38
+ )
39
+ }
40
+
41
+ export default ZoomControls
@@ -1,5 +1,8 @@
1
1
  export default {
2
+ annotations: [],
2
3
  general: {
4
+ noStateFoundMessage: 'Map Unavailable',
5
+ annotationDropdownText: 'Annotations',
3
6
  geoBorderColor: 'darkGray',
4
7
  headerColor: 'theme-blue',
5
8
  title: '',
@@ -44,7 +47,8 @@ export default {
44
47
  prefix: '',
45
48
  suffix: '',
46
49
  name: '',
47
- label: ''
50
+ label: '',
51
+ roundToPlace: 0
48
52
  },
49
53
  navigate: {
50
54
  name: ''
@@ -64,7 +68,12 @@ export default {
64
68
  type: 'equalnumber',
65
69
  numberOfItems: 3,
66
70
  position: 'side',
67
- title: 'Legend'
71
+ title: '',
72
+ style: 'circles',
73
+ subStyle: 'linear blocks',
74
+ tickRotation: '',
75
+ singleColumnLegend: false,
76
+ hideBorder: false
68
77
  },
69
78
  filters: [],
70
79
  table: {