@cdc/map 4.25.8 → 4.25.10

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 (84) hide show
  1. package/.claude/settings.local.json +30 -0
  2. package/dist/cdcmap.js +54263 -52600
  3. package/examples/private/c.json +290 -0
  4. package/examples/private/canvas-city-hover.json +787 -0
  5. package/examples/private/d.json +345 -0
  6. package/examples/private/g.json +1 -0
  7. package/examples/private/h.json +105911 -0
  8. package/examples/private/measles-data.json +378 -0
  9. package/examples/private/measles.json +211 -0
  10. package/examples/private/north-dakota.json +1132 -0
  11. package/examples/private/state-with-pattern.json +883 -0
  12. package/index.html +35 -34
  13. package/package.json +26 -5
  14. package/src/CdcMap.tsx +23 -8
  15. package/src/CdcMapComponent.tsx +215 -309
  16. package/src/_stories/CdcMap.Filters.stories.tsx +2 -2
  17. package/src/_stories/CdcMap.Legend.Gradient.stories.tsx +3 -3
  18. package/src/_stories/CdcMap.Legend.stories.tsx +7 -4
  19. package/src/_stories/CdcMap.Patterns.stories.tsx +2 -2
  20. package/src/_stories/CdcMap.Table.stories.tsx +2 -2
  21. package/src/_stories/CdcMap.stories.tsx +15 -5
  22. package/src/_stories/GoogleMap.stories.tsx +2 -2
  23. package/src/_stories/UsaMap.NoData.stories.tsx +2 -2
  24. package/src/_stories/_mock/equal-number.json +1109 -0
  25. package/src/_stories/_mock/us-bubble-cities.json +306 -0
  26. package/src/components/BubbleList.tsx +16 -12
  27. package/src/components/CityList.tsx +85 -107
  28. package/src/components/DataTable.tsx +37 -9
  29. package/src/components/EditorPanel/components/EditorPanel.tsx +177 -165
  30. package/src/components/EditorPanel/components/HexShapeSettings.tsx +3 -2
  31. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +7 -5
  32. package/src/components/Geo.tsx +2 -0
  33. package/src/components/Legend/components/Legend.tsx +109 -73
  34. package/src/components/Legend/components/LegendGroup/Legend.Group.tsx +10 -7
  35. package/src/components/MapContainer.tsx +52 -0
  36. package/src/components/MapControls.tsx +44 -0
  37. package/src/components/NavigationMenu.tsx +11 -2
  38. package/src/components/UsaMap/components/SingleState/SingleState.CountyOutput.tsx +24 -7
  39. package/src/components/UsaMap/components/UsaMap.County.tsx +111 -37
  40. package/src/components/UsaMap/components/UsaMap.Region.tsx +23 -5
  41. package/src/components/UsaMap/components/UsaMap.SingleState.tsx +6 -6
  42. package/src/components/UsaMap/components/UsaMap.State.tsx +28 -10
  43. package/src/components/UsaMap/helpers/map.ts +2 -2
  44. package/src/components/WorldMap/WorldMap.tsx +113 -25
  45. package/src/components/ZoomControls.tsx +6 -9
  46. package/src/context/LegendMemoContext.tsx +30 -0
  47. package/src/context.ts +1 -40
  48. package/src/data/initial-state.js +143 -130
  49. package/src/data/supported-geos.js +17 -2
  50. package/src/helpers/applyColorToLegend.ts +116 -20
  51. package/src/helpers/applyLegendToRow.ts +10 -6
  52. package/src/helpers/componentHelpers.ts +8 -0
  53. package/src/helpers/constants.ts +12 -0
  54. package/src/helpers/dataTableHelpers.ts +6 -0
  55. package/src/helpers/displayGeoName.ts +1 -1
  56. package/src/helpers/generateRuntimeLegend.ts +44 -8
  57. package/src/helpers/generateRuntimeLegendHash.ts +4 -2
  58. package/src/helpers/getColumnNames.ts +1 -1
  59. package/src/helpers/getPatternForRow.ts +36 -0
  60. package/src/helpers/getStatesPicked.ts +8 -5
  61. package/src/helpers/index.ts +11 -3
  62. package/src/helpers/isLegendItemDisabled.ts +16 -0
  63. package/src/helpers/mapObserverHelpers.ts +40 -0
  64. package/src/helpers/resetLegendToggles.ts +3 -2
  65. package/src/helpers/toggleLegendActive.ts +6 -11
  66. package/src/helpers/urlDataHelpers.ts +70 -0
  67. package/src/hooks/useGeoClickHandler.ts +35 -1
  68. package/src/hooks/useLegendMemo.ts +17 -0
  69. package/src/hooks/useMapLayers.tsx +5 -4
  70. package/src/hooks/useStateZoom.tsx +25 -6
  71. package/src/hooks/useTooltip.ts +1 -2
  72. package/src/index.jsx +0 -2
  73. package/src/store/map.reducer.ts +17 -6
  74. package/src/test/CdcMap.test.jsx +11 -0
  75. package/src/types/MapConfig.ts +23 -14
  76. package/src/types/MapContext.ts +0 -7
  77. package/src/types/runtimeLegend.ts +17 -1
  78. package/vite.config.js +2 -7
  79. package/vitest.config.ts +16 -0
  80. package/src/coreStyles_map.scss +0 -3
  81. package/src/helpers/colorDistributions.ts +0 -12
  82. package/src/helpers/generateColorsArray.ts +0 -14
  83. package/src/helpers/tests/generateColorsArray.test.ts +0 -18
  84. package/src/helpers/tests/generateRuntimeLegendHash.test.ts +0 -11
@@ -7,14 +7,17 @@ import Loading from '@cdc/core/components/Loading'
7
7
  import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
8
8
  import useMapLayers from '../../../hooks/useMapLayers'
9
9
  import ConfigContext from '../../../context'
10
+ import { useLegendMemoContext } from '../../../context/LegendMemoContext'
10
11
  import { drawShape, createShapeProperties } from '../helpers/shapes'
11
- import { getGeoStrokeColor, handleMapAriaLabels, displayGeoName } from '../../../helpers'
12
+ import { getGeoStrokeColor, handleMapAriaLabels, displayGeoName, isLegendItemDisabled } from '../../../helpers'
13
+ import { supportedStatesFipsCodes } from '../../../data/supported-geos'
12
14
  import useGeoClickHandler from '../../../hooks/useGeoClickHandler'
13
15
  import { applyLegendToRow } from '../../../helpers/applyLegendToRow'
14
16
  import useApplyTooltipsToGeo from '../../../hooks/useApplyTooltipsToGeo'
15
17
  import { MapConfig } from '../../../types/MapConfig'
16
18
  import { DEFAULT_MAP_BACKGROUND } from '../../../helpers/constants'
17
19
  import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
20
+ import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
18
21
 
19
22
  const getCountyTopoURL = year => {
20
23
  return `https://www.cdc.gov/TemplatePackage/contrib/data/county-topography/cb_${year}_us_county_20m.json`
@@ -131,18 +134,18 @@ const CountyMap = () => {
131
134
  const {
132
135
  container,
133
136
  containerEl,
134
- data,
137
+ runtimeData,
135
138
  runtimeFilters,
136
139
  runtimeLegend,
137
140
  setConfig,
138
141
  config,
139
142
  tooltipId,
140
143
  tooltipRef,
141
- legendMemo,
142
- legendSpecialClassLastMemo,
143
- configUrl
144
+ interactionLabel
144
145
  } = useContext(ConfigContext)
145
146
 
147
+ const { legendMemo, legendSpecialClassLastMemo } = useLegendMemoContext()
148
+
146
149
  // CREATE STATE LINES
147
150
  const geoStrokeColor = getGeoStrokeColor(config)
148
151
  const { geoClickHandler } = useGeoClickHandler()
@@ -200,7 +203,7 @@ const CountyMap = () => {
200
203
  const canvasRef = useRef()
201
204
 
202
205
  // If runtimeData is not defined, show loader
203
- if (!data || !isTopoReady(topoData, config, runtimeFilters)) {
206
+ if (!runtimeData || !isTopoReady(topoData, config, runtimeFilters)) {
204
207
  return (
205
208
  <div style={{ height: 300 }}>
206
209
  <Loading />
@@ -208,11 +211,18 @@ const CountyMap = () => {
208
211
  )
209
212
  }
210
213
 
211
- const runtimeKeys = Object.keys(data)
214
+ const runtimeKeys = Object.keys(runtimeData)
212
215
  const lineWidth = 1
213
216
 
214
217
  const onReset = () => {
215
- publishAnalyticsEvent('map_reset_zoom_level', 'click', configUrl, 'map')
218
+ publishAnalyticsEvent({
219
+ vizType: config.type,
220
+ vizSubType: getVizSubType(config),
221
+ eventType: 'map_reset_zoom_level',
222
+ eventAction: 'click',
223
+ eventLabel: interactionLabel,
224
+ vizTitle: getVizTitle(config)
225
+ })
216
226
  setConfig({
217
227
  ...config,
218
228
  mapPosition: { coordinates: [0, 30], zoom: 1 }
@@ -256,8 +266,8 @@ const CountyMap = () => {
256
266
  break
257
267
  }
258
268
  }
259
- if (county && data[county.id]) {
260
- geoClickHandler(displayGeoName(county.id), data[county.id])
269
+ if (county && runtimeData[county.id]) {
270
+ geoClickHandler(displayGeoName(county.id), runtimeData[county.id])
261
271
  }
262
272
  }
263
273
 
@@ -271,19 +281,36 @@ const CountyMap = () => {
271
281
 
272
282
  // Redraw with focus on state
273
283
  setFocus({ id: clickedState.id, index: focusIndex, center: geoCentroid(clickedState), feature: clickedState })
274
- publishAnalyticsEvent('map_zoomed_in', 'click', `${configUrl}|zoom_level_3|${clickedState.properties.name}`, 'map')
275
-
284
+ publishAnalyticsEvent({
285
+ vizType: config.type,
286
+ vizSubType: getVizSubType(config),
287
+ eventType: `zoom_in`,
288
+ eventAction: 'click',
289
+ eventLabel: interactionLabel,
290
+ vizTitle: getVizTitle(config),
291
+ specifics: `zoom_level: 3, location: ${clickedState.properties.name}`
292
+ })
276
293
  }
277
294
  if (config.general.type === 'us-geocode') {
278
295
  const geoRadius = (config.visual.geoCodeCircleSize || 5) * (focus.id ? 2 : 1)
279
296
  let clickedGeo
280
297
  for (let i = 0; i < runtimeKeys.length; i++) {
281
298
  const pixelCoords = topoData.projection([
282
- data[runtimeKeys[i]][config.columns.longitude.name],
283
- data[runtimeKeys[i]][config.columns.latitude.name]
299
+ runtimeData[runtimeKeys[i]][config.columns.longitude.name],
300
+ runtimeData[runtimeKeys[i]][config.columns.latitude.name]
284
301
  ])
285
- if (pixelCoords && Math.sqrt(Math.pow(pixelCoords[0] - x, 2) + Math.pow(pixelCoords[1] - y, 2)) < geoRadius) {
286
- clickedGeo = data[runtimeKeys[i]]
302
+ if (
303
+ pixelCoords &&
304
+ Math.sqrt(Math.pow(pixelCoords[0] - x, 2) + Math.pow(pixelCoords[1] - y, 2)) < geoRadius &&
305
+ !isLegendItemDisabled(
306
+ runtimeData[runtimeKeys[i]],
307
+ runtimeLegend,
308
+ legendMemo,
309
+ legendSpecialClassLastMemo,
310
+ config
311
+ )
312
+ ) {
313
+ clickedGeo = runtimeData[runtimeKeys[i]]
287
314
  break
288
315
  }
289
316
  }
@@ -324,7 +351,7 @@ const CountyMap = () => {
324
351
  if (
325
352
  !isNaN(currentTooltipIndex) &&
326
353
  applyLegendToRow(
327
- data[topoData.mapData[currentTooltipIndex].id],
354
+ runtimeData[topoData.mapData[currentTooltipIndex].id],
328
355
  config,
329
356
  runtimeLegend,
330
357
  legendMemo,
@@ -332,7 +359,7 @@ const CountyMap = () => {
332
359
  )
333
360
  ) {
334
361
  context.fillStyle = applyLegendToRow(
335
- data[topoData.mapData[currentTooltipIndex].id],
362
+ runtimeData[topoData.mapData[currentTooltipIndex].id],
336
363
  config,
337
364
  runtimeLegend,
338
365
  legendMemo,
@@ -369,10 +396,10 @@ const CountyMap = () => {
369
396
  }
370
397
 
371
398
  // If the hovered county is found, show the tooltip for that county, otherwise hide the tooltip
372
- if (county && data[county.id]) {
373
- if (applyLegendToRow(data[county.id], config, runtimeLegend, legendMemo, legendSpecialClassLastMemo)) {
399
+ if (county && runtimeData[county.id]) {
400
+ if (applyLegendToRow(runtimeData[county.id], config, runtimeLegend, legendMemo, legendSpecialClassLastMemo)) {
374
401
  let fillColor = applyLegendToRow(
375
- data[county.id],
402
+ runtimeData[county.id],
376
403
  config,
377
404
  runtimeLegend,
378
405
  legendMemo,
@@ -389,6 +416,24 @@ const CountyMap = () => {
389
416
  context.stroke()
390
417
  }
391
418
 
419
+ // Track hover analytics event if this is a new location
420
+ if (isNaN(currentTooltipIndex) || currentTooltipIndex !== countyIndex) {
421
+ const countyName = displayGeoName(county.id).replace(/[^a-zA-Z0-9]/g, ' ')
422
+ const stateFips = county.id.slice(0, 2)
423
+ const stateName = supportedStatesFipsCodes[stateFips]?.replace(/[^a-zA-Z0-9]/g, '_') || 'unknown'
424
+ const locationName = `${countyName}, ${stateName}`
425
+ publishAnalyticsEvent({
426
+ vizType: config.type,
427
+ vizSubType: getVizSubType(config),
428
+ eventType: `map_hover`,
429
+ eventAction: 'hover',
430
+ eventLabel: interactionLabel,
431
+ vizTitle: getVizTitle(config),
432
+ location: displayGeoName(county.id),
433
+ specifics: `location: ${locationName?.toLowerCase()}`
434
+ })
435
+ }
436
+
392
437
  tooltipRef.current.style.display = 'block'
393
438
  tooltipRef.current.style.top = tooltipY + 'px'
394
439
  if (tooltipX > containerBounds.width / 2) {
@@ -398,7 +443,7 @@ const CountyMap = () => {
398
443
  tooltipRef.current.style.transform = 'translate(0, -50%)'
399
444
  tooltipRef.current.style.left = tooltipX + 5 + 'px'
400
445
  }
401
- tooltipRef.current.innerHTML = applyTooltipsToGeo(displayGeoName(county.id), data[county.id])
446
+ tooltipRef.current.innerHTML = applyTooltipsToGeo(displayGeoName(county.id), runtimeData[county.id])
402
447
  tooltipRef.current.setAttribute('data-index', countyIndex)
403
448
  } else {
404
449
  tooltipRef.current.style.display = 'none'
@@ -409,8 +454,8 @@ const CountyMap = () => {
409
454
  // Handle geo map hover
410
455
  if (!isNaN(currentTooltipIndex)) {
411
456
  const pixelCoords = topoData.projection([
412
- data[runtimeKeys[currentTooltipIndex]][config.columns.longitude.name],
413
- data[runtimeKeys[currentTooltipIndex]][config.columns.latitude.name]
457
+ runtimeData[runtimeKeys[currentTooltipIndex]][config.columns.longitude.name],
458
+ runtimeData[runtimeKeys[currentTooltipIndex]][config.columns.latitude.name]
414
459
  ])
415
460
  if (pixelCoords && Math.sqrt(Math.pow(pixelCoords[0] - x, 2) + Math.pow(pixelCoords[1] - y, 2)) < geoRadius) {
416
461
  return // The user is still hovering over the previous geo point, don't redraw tooltip
@@ -424,17 +469,30 @@ const CountyMap = () => {
424
469
  let hoveredGeoIndex
425
470
  for (let i = 0; i < runtimeKeys.length; i++) {
426
471
  const pixelCoords = topoData.projection([
427
- data[runtimeKeys[i]][config.columns.longitude.name],
428
- data[runtimeKeys[i]][config.columns.latitude.name]
472
+ runtimeData[runtimeKeys[i]][config.columns.longitude.name],
473
+ runtimeData[runtimeKeys[i]][config.columns.latitude.name]
429
474
  ])
430
475
  let includedShapes = ['circle', 'diamond', 'star', 'triangle', 'square'].includes(config.visual.cityStyle)
431
476
  if (
432
477
  includedShapes &&
433
478
  pixelCoords &&
434
479
  Math.sqrt(Math.pow(pixelCoords[0] - x, 2) + Math.pow(pixelCoords[1] - y, 2)) < geoRadius &&
435
- applyLegendToRow(data[runtimeKeys[i]], config, runtimeLegend, legendMemo, legendSpecialClassLastMemo)
480
+ applyLegendToRow(
481
+ runtimeData[runtimeKeys[i]],
482
+ config,
483
+ runtimeLegend,
484
+ legendMemo,
485
+ legendSpecialClassLastMemo
486
+ ) &&
487
+ !isLegendItemDisabled(
488
+ runtimeData[runtimeKeys[i]],
489
+ runtimeLegend,
490
+ legendMemo,
491
+ legendSpecialClassLastMemo,
492
+ config
493
+ )
436
494
  ) {
437
- hoveredGeo = data[runtimeKeys[i]]
495
+ hoveredGeo = runtimeData[runtimeKeys[i]]
438
496
  hoveredGeoIndex = i
439
497
  break
440
498
  }
@@ -443,9 +501,10 @@ const CountyMap = () => {
443
501
  const distance = Math.hypot(pixelCoords[0] - x, pixelCoords[1] - y)
444
502
  if (
445
503
  distance < 15 &&
446
- applyLegendToRow(data[runtimeKeys[i]], config, runtimeLegend, legendMemo, legendSpecialClassLastMemo)
504
+ applyLegendToRow(runtimeData[runtimeKeys[i]], config, runtimeLegend, legendMemo, legendSpecialClassLastMemo) &&
505
+ !isLegendItemDisabled(runtimeData[runtimeKeys[i]], runtimeLegend, legendMemo, legendSpecialClassLastMemo, config)
447
506
  ) {
448
- hoveredGeo = data[runtimeKeys[i]]
507
+ hoveredGeo = runtimeData[runtimeKeys[i]]
449
508
  hoveredGeoIndex = i
450
509
  break
451
510
  }
@@ -453,6 +512,21 @@ const CountyMap = () => {
453
512
  }
454
513
 
455
514
  if (hoveredGeo) {
515
+ // Track hover analytics event if this is a new location
516
+ if (isNaN(currentTooltipIndex) || currentTooltipIndex !== hoveredGeoIndex) {
517
+ const locationName = displayGeoName(hoveredGeo[config.columns.geo.name]).replace(/[^a-zA-Z0-9]/g, '_')
518
+ publishAnalyticsEvent({
519
+ vizType: config.type,
520
+ vizSubType: getVizSubType(config),
521
+ eventType: `map_hover`,
522
+ eventAction: 'hover',
523
+ eventLabel: interactionLabel,
524
+ vizTitle: getVizTitle(config),
525
+ location: displayGeoName(hoveredGeo[config.columns.geo.name]),
526
+ specifics: `location: ${locationName?.toLowerCase()}`
527
+ })
528
+ }
529
+
456
530
  tooltipRef.current.style.display = 'block'
457
531
  tooltipRef.current.style.top = tooltipY + 'px'
458
532
  if (tooltipX > containerBounds.width / 2) {
@@ -529,7 +603,7 @@ const CountyMap = () => {
529
603
  if (!focus.id && config.general.type === 'us-geocode' && geo.id.length > 2) return
530
604
 
531
605
  // Gets numeric data associated with the topo data for this state/county
532
- const geoData = data[geo.id]
606
+ const geoData = runtimeData[geo.id]
533
607
 
534
608
  // Renders state/county
535
609
  const legendValues =
@@ -574,7 +648,7 @@ const CountyMap = () => {
574
648
  context.strokeStyle = geoStrokeColor
575
649
  const geoRadius = (config.visual.geoCodeCircleSize || 5) * (focus.id ? 2 : 1)
576
650
  const { additionalCityStyles } = config.visual || []
577
- const cityStyles = Object.values(data)
651
+ const cityStyles = Object.values(runtimeData)
578
652
  .filter(d => additionalCityStyles.some(style => String(d[style.column]) === String(style.value)))
579
653
  .map(d => {
580
654
  const conditionsMatched = additionalCityStyles.find(
@@ -592,7 +666,7 @@ const CountyMap = () => {
592
666
 
593
667
  if (cityPixelCoords) {
594
668
  const legendValues = applyLegendToRow(
595
- data[city?.value],
669
+ runtimeData[city?.value],
596
670
  config,
597
671
  runtimeLegend,
598
672
  legendMemo,
@@ -613,13 +687,13 @@ const CountyMap = () => {
613
687
  const citiesList = new Set(cityStyles.map(item => item.value))
614
688
 
615
689
  const pixelCoords = topoData.projection([
616
- data[key][config.columns.longitude.name],
617
- data[key][config.columns.latitude.name]
690
+ runtimeData[key][config.columns.longitude.name],
691
+ runtimeData[key][config.columns.latitude.name]
618
692
  ])
619
693
  if (pixelCoords && !citiesList.has(key)) {
620
694
  const legendValues =
621
- data[key] !== undefined
622
- ? applyLegendToRow(data[key], config, runtimeLegend, legendMemo, legendSpecialClassLastMemo)
695
+ runtimeData[key] !== undefined
696
+ ? applyLegendToRow(runtimeData[key], config, runtimeLegend, legendMemo, legendSpecialClassLastMemo)
623
697
  : false
624
698
  if (legendValues) {
625
699
  if (legendValues?.[0] === '#000000' || legendValues?.[0] === DEFAULT_MAP_BACKGROUND) return
@@ -7,7 +7,10 @@ import { Mercator } from '@visx/geo'
7
7
 
8
8
  // Cdc Components
9
9
  import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
10
+ import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
11
+ import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
10
12
  import ConfigContext from '../../../context'
13
+ import { useLegendMemoContext } from '../../../context/LegendMemoContext'
11
14
  import Annotation from '../../Annotation'
12
15
 
13
16
  // Data
@@ -51,7 +54,8 @@ const Rect: React.FC<RectProps> = ({ label, text, stroke, strokeWidth, ...props
51
54
  }
52
55
 
53
56
  const UsaRegionMap = () => {
54
- const { data, config, tooltipId, legendMemo, legendSpecialClassLastMemo, runtimeLegend } = useContext(ConfigContext)
57
+ const { runtimeData, config, tooltipId, runtimeLegend, interactionLabel } = useContext(ConfigContext)
58
+ const { legendMemo, legendSpecialClassLastMemo } = useLegendMemoContext()
55
59
  const [focusedStates, setFocusedStates] = useState(null)
56
60
  const { geoClickHandler } = useGeoClickHandler()
57
61
  const { applyTooltipsToGeo } = useApplyTooltipsToGeo()
@@ -75,10 +79,10 @@ const UsaRegionMap = () => {
75
79
 
76
80
  useEffect(() => {
77
81
  // Territories need to show up if they're in the data at all, not just if they're "active". That's why this is different from Cities
78
- const territoriesList = territoriesKeys.filter(key => data[key])
82
+ const territoriesList = territoriesKeys.filter(key => runtimeData[key])
79
83
 
80
84
  setTerritoriesData(territoriesList)
81
- }, [data])
85
+ }, [runtimeData])
82
86
 
83
87
  if (!focusedStates) {
84
88
  return <></>
@@ -90,7 +94,7 @@ const UsaRegionMap = () => {
90
94
  const territories = territoriesData.map(territory => {
91
95
  const Shape = Rect
92
96
 
93
- const territoryData = data[territory]
97
+ const territoryData = runtimeData[territory]
94
98
 
95
99
  let toolTip: string
96
100
 
@@ -162,7 +166,7 @@ const UsaRegionMap = () => {
162
166
 
163
167
  if (!geoKey) return
164
168
 
165
- const geoData = data[geoKey]
169
+ const geoData = runtimeData[geoKey]
166
170
 
167
171
  let legendColors
168
172
  // Once we receive data for this geographic item, setup variables.
@@ -215,6 +219,20 @@ const UsaRegionMap = () => {
215
219
  data-tooltip-id={`tooltip__${tooltipId}`}
216
220
  data-tooltip-html={toolTip}
217
221
  tabIndex={-1}
222
+ onMouseEnter={() => {
223
+ // Track hover analytics event if this is a new location
224
+ const locationName = geoDisplayName.replace(/[^a-zA-Z0-9]/g, '_')
225
+ publishAnalyticsEvent({
226
+ vizType: config.type,
227
+ vizSubType: getVizSubType(config),
228
+ eventType: `map_hover`,
229
+ eventAction: 'hover',
230
+ eventLabel: interactionLabel,
231
+ vizTitle: getVizTitle(config),
232
+ location: geoDisplayName,
233
+ specifics: `location: ${locationName?.toLowerCase()}`
234
+ })
235
+ }}
218
236
  >
219
237
  <path tabIndex={-1} className='single-geo' stroke={geoStrokeColor} strokeWidth={1} d={path} />
220
238
  <g id={`region-${index + 1}-label`}>
@@ -41,7 +41,7 @@ const SingleStateMap: React.FC = () => {
41
41
  } = useContext<MapContext>(ConfigContext)
42
42
 
43
43
  const dispatch = useContext(MapDispatchContext)
44
- const { handleMoveEnd, handleZoomIn, handleZoomOut, handleReset, projection } = useStateZoom(topoData)
44
+ const { handleMoveEnd, handleZoomIn, handleZoomOut, handleZoomReset, projection } = useStateZoom(topoData)
45
45
 
46
46
  // Memoize statesPicked to prevent creating new arrays on every render
47
47
  const statesPicked = useMemo(() => {
@@ -70,7 +70,7 @@ const SingleStateMap: React.FC = () => {
70
70
  dispatch({ type: 'SET_TOPO_DATA', payload: response })
71
71
  })
72
72
  }
73
- }, [runtimeFilters?.length, topoData?.year])
73
+ }, [config.general.countyCensusYear, config.general.filterControlsCountyYear, JSON.stringify(runtimeFilters)])
74
74
 
75
75
  if (!isTopoReady(topoData, config, runtimeFilters)) {
76
76
  return (
@@ -129,7 +129,7 @@ const SingleStateMap: React.FC = () => {
129
129
  }
130
130
  return (
131
131
  <ErrorBoundary component='SingleStateMap'>
132
- {statesPicked.length && config.general.allowMapZoom && statesPicked.some(sp => sp.fipsCode) && (
132
+ {!!statesPicked.length && config.general.allowMapZoom && statesPicked.some(sp => sp.fipsCode) && (
133
133
  <svg
134
134
  viewBox={SVG_VIEWBOX}
135
135
  preserveAspectRatio='xMinYMin'
@@ -192,7 +192,7 @@ const SingleStateMap: React.FC = () => {
192
192
  </ZoomableGroup>
193
193
  </svg>
194
194
  )}
195
- {statesPicked && !config.general.allowMapZoom && statesPicked.some(sp => sp.fipsCode) && (
195
+ {!!statesPicked && !config.general.allowMapZoom && statesPicked.some(sp => sp.fipsCode) && (
196
196
  <svg
197
197
  viewBox={SVG_VIEWBOX}
198
198
  preserveAspectRatio='xMinYMin'
@@ -262,7 +262,7 @@ const SingleStateMap: React.FC = () => {
262
262
  fontSize={18}
263
263
  style={{ fontSize: '28px', height: '18px' }}
264
264
  >
265
- {config.general.noStateFoundMessage}
265
+ {config.general.noDataMessage}
266
266
  </Text>
267
267
  </svg>
268
268
  )}
@@ -270,7 +270,7 @@ const SingleStateMap: React.FC = () => {
270
270
  // prettier-ignore
271
271
  handleZoomIn={handleZoomIn}
272
272
  handleZoomOut={handleZoomOut}
273
- handleReset={handleReset}
273
+ handleZoomReset={handleZoomReset}
274
274
  />
275
275
  </ErrorBoundary>
276
276
  )
@@ -1,6 +1,8 @@
1
1
  import React, { useState, useEffect, useContext, useRef } from 'react'
2
2
 
3
3
  import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
4
+ import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
5
+ import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
4
6
 
5
7
  // United States Topojson resources
6
8
  import hexTopoJSON from '../data/us-hex-topo.json'
@@ -20,6 +22,7 @@ import Annotation from '../../Annotation'
20
22
  import Territory from './Territory'
21
23
 
22
24
  import ConfigContext, { MapDispatchContext } from '../../../context'
25
+ import { useLegendMemoContext } from '../../../context/LegendMemoContext'
23
26
  import { MapContext } from '../../../types/MapContext'
24
27
  import { checkColorContrast, getContrastColor, outlinedTextColor } from '@cdc/core/helpers/cove/accessibility'
25
28
  import TerritoriesSection from './TerritoriesSection'
@@ -69,20 +72,21 @@ const nudges = {
69
72
 
70
73
  const UsaMap = () => {
71
74
  const {
72
- data,
75
+ runtimeData,
73
76
  setSharedFilterValue,
74
77
  config,
75
78
  setConfig,
76
79
  tooltipId,
77
80
  mapId,
78
81
  logo,
79
- legendMemo,
80
- legendSpecialClassLastMemo,
81
82
  currentViewport,
82
83
  translate,
83
- runtimeLegend
84
+ runtimeLegend,
85
+ interactionLabel
84
86
  } = useContext<MapContext>(ConfigContext)
85
87
 
88
+ const { legendMemo, legendSpecialClassLastMemo } = useLegendMemoContext()
89
+
86
90
  let isFilterValueSupported = false
87
91
  const { general, columns, tooltips, hexMap, map, annotations } = config
88
92
  const { displayAsHex } = general
@@ -120,14 +124,14 @@ const UsaMap = () => {
120
124
 
121
125
  const legendMemoUpdated = focusedStates?.every(geo => {
122
126
  const geoKey = geo.properties.iso
123
- const geoData = data[geoKey]
127
+ const geoData = runtimeData[geoKey]
124
128
  const hash = hashObj(geoData)
125
129
  return legendMemo.current.has(hash)
126
130
  })
127
131
 
128
132
  // we use dataRef so that we can use the old data when legendMemo has not been updated yet
129
133
  // prevents flickering of the map when filter is changed
130
- if (legendMemoUpdated) dataRef.current = data
134
+ if (legendMemoUpdated) dataRef.current = runtimeData
131
135
 
132
136
  useEffect(() => {
133
137
  const fetchData = async () => {
@@ -153,10 +157,10 @@ const UsaMap = () => {
153
157
  setTerritoriesData(territoriesKeys)
154
158
  } else {
155
159
  // Territories need to show up if they're in the data at all, not just if they're "active". That's why this is different from Cities
156
- const territoriesList = territoriesKeys.filter(key => data?.[key])
160
+ const territoriesList = territoriesKeys.filter(key => runtimeData?.[key])
157
161
  setTerritoriesData(territoriesList)
158
162
  }
159
- }, [data, dataRef.current, general.territoriesAlwaysShow])
163
+ }, [runtimeData, dataRef.current, general.territoriesAlwaysShow])
160
164
 
161
165
  const geoStrokeColor = getGeoStrokeColor(config)
162
166
  const geoFillColor = getGeoFillColor(config)
@@ -164,7 +168,7 @@ const UsaMap = () => {
164
168
  const territories = territoriesData.map((territory, territoryIndex) => {
165
169
  const Shape = displayAsHex ? Territory.Hexagon : Territory.Rectangle
166
170
 
167
- const territoryData = data?.[territory]
171
+ const territoryData = runtimeData?.[territory]
168
172
 
169
173
  let toolTip
170
174
 
@@ -290,7 +294,7 @@ const UsaMap = () => {
290
294
 
291
295
  if (!geoKey) return
292
296
 
293
- const geoData = data?.[geoKey]
297
+ const geoData = runtimeData?.[geoKey]
294
298
 
295
299
  let legendColors
296
300
 
@@ -440,6 +444,20 @@ const UsaMap = () => {
440
444
  data-tooltip-id={`tooltip__${tooltipId}`}
441
445
  data-tooltip-html={tooltip}
442
446
  tabIndex={-1}
447
+ onMouseEnter={() => {
448
+ // Track hover analytics event if this is a new location
449
+ const locationName = geoDisplayName.replace(/[^a-zA-Z0-9]/g, '_')
450
+ publishAnalyticsEvent({
451
+ vizType: config.type,
452
+ vizSubType: getVizSubType(config),
453
+ eventType: `map_hover`,
454
+ eventAction: 'hover',
455
+ eventLabel: interactionLabel,
456
+ vizTitle: getVizTitle(config),
457
+ location: geoDisplayName,
458
+ specifics: `location: ${locationName?.toLowerCase()}`
459
+ })
460
+ }}
443
461
  >
444
462
  {/* state path */}
445
463
  <path tabIndex={-1} className='single-geo' strokeWidth={1} d={path} />
@@ -96,7 +96,7 @@ export const hasMoreThanFromHash = (data: { [key: string]: any }): boolean => {
96
96
  return otherKeys.length > 0
97
97
  }
98
98
 
99
- export const getFilterControllingStatesPicked = (state, runtimeData) => {
99
+ export const getFilterControllingStatesPicked = (state, runtimeData): string[] => {
100
100
  if (!state.general.filterControlsStatesPicked || !runtimeData) {
101
101
  return state?.general?.statesPicked?.map(sp => sp.stateName) || []
102
102
  } else {
@@ -111,7 +111,7 @@ export const getFilterControllingStatesPicked = (state, runtimeData) => {
111
111
  } else if (statesPickedFromFilter) {
112
112
  return [statesPickedFromFilter]
113
113
  } else {
114
- return state?.general?.statesPicked?.map(sp => sp.stateName) || ['Alabama']
114
+ return state?.general?.statesPicked?.map(sp => sp.stateName) || []
115
115
  }
116
116
  }
117
117
  return state?.general?.statesPicked?.map(sp => sp.stateName) || []