@cdc/map 4.25.7 → 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 (99) hide show
  1. package/.claude/settings.local.json +30 -0
  2. package/CLAUDE.local.md +0 -0
  3. package/dist/cdcmap.js +54785 -53159
  4. package/examples/private/c.json +290 -0
  5. package/examples/private/canvas-city-hover.json +787 -0
  6. package/examples/private/d.json +345 -0
  7. package/examples/private/filter-map.json +909 -0
  8. package/examples/private/g.json +1 -0
  9. package/examples/private/h.json +105911 -0
  10. package/examples/private/measles-data.json +378 -0
  11. package/examples/private/measles.json +211 -0
  12. package/examples/private/north-dakota.json +1132 -0
  13. package/examples/private/rsv-data.json +532 -0
  14. package/examples/private/state-with-pattern.json +883 -0
  15. package/examples/private/test.json +222 -640
  16. package/index.html +1 -1
  17. package/package.json +26 -5
  18. package/src/CdcMap.tsx +28 -8
  19. package/src/CdcMapComponent.tsx +230 -306
  20. package/src/_stories/CdcMap.Filters.stories.tsx +2 -2
  21. package/src/_stories/CdcMap.Legend.Gradient.stories.tsx +3 -3
  22. package/src/_stories/CdcMap.Legend.stories.tsx +7 -4
  23. package/src/_stories/CdcMap.Patterns.stories.tsx +2 -2
  24. package/src/_stories/CdcMap.Table.stories.tsx +2 -2
  25. package/src/_stories/CdcMap.stories.tsx +18 -11
  26. package/src/_stories/GoogleMap.stories.tsx +2 -2
  27. package/src/_stories/UsaMap.NoData.stories.tsx +2 -2
  28. package/src/_stories/_mock/equal-number.json +1109 -0
  29. package/src/_stories/_mock/multi-state.json +21389 -0
  30. package/src/_stories/_mock/us-bubble-cities.json +306 -0
  31. package/src/components/BubbleList.tsx +16 -12
  32. package/src/components/CityList.tsx +88 -110
  33. package/src/components/DataTable.tsx +44 -12
  34. package/src/components/EditorPanel/components/EditorPanel.tsx +201 -203
  35. package/src/components/EditorPanel/components/HexShapeSettings.tsx +3 -2
  36. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +7 -5
  37. package/src/components/Geo.tsx +2 -0
  38. package/src/components/Legend/components/Legend.tsx +117 -93
  39. package/src/components/Legend/components/LegendGroup/Legend.Group.tsx +10 -7
  40. package/src/components/MapContainer.tsx +52 -0
  41. package/src/components/MapControls.tsx +44 -0
  42. package/src/components/Modal.tsx +2 -8
  43. package/src/components/NavigationMenu.tsx +13 -1
  44. package/src/components/UsaMap/components/SingleState/SingleState.CountyOutput.tsx +24 -7
  45. package/src/components/UsaMap/components/SingleState/SingleState.StateOutput.tsx +21 -15
  46. package/src/components/UsaMap/components/TerritoriesSection.tsx +2 -2
  47. package/src/components/UsaMap/components/UsaMap.County.tsx +112 -33
  48. package/src/components/UsaMap/components/UsaMap.Region.tsx +23 -5
  49. package/src/components/UsaMap/components/UsaMap.SingleState.tsx +38 -26
  50. package/src/components/UsaMap/components/UsaMap.State.tsx +28 -10
  51. package/src/components/UsaMap/helpers/map.ts +16 -8
  52. package/src/components/WorldMap/WorldMap.tsx +116 -11
  53. package/src/components/ZoomControls.tsx +6 -9
  54. package/src/context/LegendMemoContext.tsx +30 -0
  55. package/src/context.ts +1 -39
  56. package/src/data/initial-state.js +143 -128
  57. package/src/data/supported-geos.js +202 -4
  58. package/src/helpers/addUIDs.ts +8 -8
  59. package/src/helpers/applyColorToLegend.ts +122 -45
  60. package/src/helpers/applyLegendToRow.ts +15 -13
  61. package/src/helpers/componentHelpers.ts +8 -0
  62. package/src/helpers/constants.ts +12 -0
  63. package/src/helpers/dataTableHelpers.ts +6 -0
  64. package/src/helpers/displayGeoName.ts +12 -7
  65. package/src/helpers/formatLegendLocation.ts +1 -3
  66. package/src/helpers/generateRuntimeLegend.ts +192 -340
  67. package/src/helpers/generateRuntimeLegendHash.ts +4 -2
  68. package/src/helpers/getColumnNames.ts +1 -1
  69. package/src/helpers/getPatternForRow.ts +36 -0
  70. package/src/helpers/getStatesPicked.ts +14 -0
  71. package/src/helpers/handleMapAriaLabels.ts +2 -2
  72. package/src/helpers/index.ts +11 -3
  73. package/src/helpers/isLegendItemDisabled.ts +16 -0
  74. package/src/helpers/mapObserverHelpers.ts +40 -0
  75. package/src/helpers/resetLegendToggles.ts +3 -2
  76. package/src/helpers/toggleLegendActive.ts +6 -11
  77. package/src/helpers/urlDataHelpers.ts +70 -0
  78. package/src/hooks/useGeoClickHandler.ts +35 -1
  79. package/src/hooks/useLegendMemo.ts +17 -0
  80. package/src/hooks/useMapLayers.tsx +5 -4
  81. package/src/hooks/useStateZoom.tsx +137 -88
  82. package/src/hooks/useTooltip.ts +1 -2
  83. package/src/index.jsx +6 -3
  84. package/src/scss/main.scss +23 -12
  85. package/src/store/map.actions.ts +2 -2
  86. package/src/store/map.reducer.ts +21 -10
  87. package/src/test/CdcMap.test.jsx +11 -0
  88. package/src/types/MapConfig.ts +25 -17
  89. package/src/types/MapContext.ts +2 -8
  90. package/src/types/runtimeLegend.ts +12 -10
  91. package/vite.config.js +2 -7
  92. package/vitest.config.ts +16 -0
  93. package/src/_stories/_mock/floating-point.json +0 -427
  94. package/src/coreStyles_map.scss +0 -3
  95. package/src/helpers/colorDistributions.ts +0 -12
  96. package/src/helpers/generateColorsArray.ts +0 -14
  97. package/src/helpers/getStatePicked.ts +0 -8
  98. package/src/helpers/tests/generateColorsArray.test.ts +0 -18
  99. package/src/helpers/tests/generateRuntimeLegendHash.test.ts +0 -11
@@ -1,10 +1,13 @@
1
- import React, { useContext } from 'react'
1
+ import React, { useContext, useState } from 'react'
2
2
  import ConfigContext from '../../../../context'
3
+ import { useLegendMemoContext } from '../../../../context/LegendMemoContext'
3
4
  import { MapContext } from '../../../../types/MapContext'
4
5
  import { getGeoFillColor, displayGeoName } from '../../../../helpers'
5
6
  import useApplyTooltipsToGeo from '../../../../hooks/useApplyTooltipsToGeo'
6
7
  import { applyLegendToRow } from '../../../../helpers/applyLegendToRow'
7
8
  import useGeoClickHandler, { geoClickHandler } from '././../../../../hooks/useGeoClickHandler'
9
+ import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
10
+ import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
8
11
 
9
12
  interface CountyOutputProps {
10
13
  counties: any[]
@@ -15,14 +18,15 @@ interface CountyOutputProps {
15
18
  }
16
19
 
17
20
  const CountyOutput: React.FC<CountyOutputProps> = ({ path, counties, scale, geoStrokeColor, tooltipId }) => {
18
- const { config, data, legendMemo, legendSpecialClassLastMemo, runtimeLegend } = useContext<MapContext>(ConfigContext)
21
+ const { config, runtimeData, runtimeLegend, interactionLabel } = useContext<MapContext>(ConfigContext)
22
+ const { legendMemo, legendSpecialClassLastMemo } = useLegendMemoContext()
19
23
  const { applyTooltipsToGeo } = useApplyTooltipsToGeo()
20
24
  const geoFillColor = getGeoFillColor(config)
21
25
  const { geoClickHandler } = useGeoClickHandler()
22
26
 
23
27
  return (
24
28
  <>
25
- {counties.map(county => {
29
+ {counties.map((county, countyIndex) => {
26
30
  // Map the name from the geo data with the appropriate key for the processed data
27
31
  const geoKey = county.id
28
32
 
@@ -30,7 +34,7 @@ const CountyOutput: React.FC<CountyOutputProps> = ({ path, counties, scale, geoS
30
34
 
31
35
  const countyPath = path(county)
32
36
 
33
- const geoData = data[county.id]
37
+ const geoData = runtimeData[county.id]
34
38
  let legendColors
35
39
 
36
40
  // Once we receive data for this geographic item, setup variables.
@@ -68,13 +72,26 @@ const CountyOutput: React.FC<CountyOutputProps> = ({ path, counties, scale, geoS
68
72
  return (
69
73
  <g
70
74
  key={`key--${county.id}`}
71
- className={`county county--${geoDisplayName.split(' ').join('')} county--${
72
- geoData[config.columns.geo.name]
73
- }`}
75
+ className={`county county--${geoDisplayName.split(' ').join('')} county--${geoData[config.columns.geo.name]
76
+ }`}
74
77
  style={styles}
75
78
  onClick={() => geoClickHandler(geoDisplayName, geoData)}
76
79
  data-tooltip-id={`tooltip__${tooltipId}`}
77
80
  data-tooltip-html={toolTip}
81
+ onMouseEnter={() => {
82
+ // Track hover analytics event if this is a new location
83
+ const locationName = geoDisplayName.replace(/[^a-zA-Z0-9]/g, '_')
84
+ publishAnalyticsEvent({
85
+ vizType: config.type,
86
+ vizSubType: getVizSubType(config),
87
+ eventType: `map_hover`,
88
+ eventAction: 'hover',
89
+ eventLabel: interactionLabel,
90
+ vizTitle: getVizTitle(config),
91
+ location: geoDisplayName,
92
+ specifics: `location: ${locationName?.toLowerCase()}`
93
+ })
94
+ }}
78
95
  >
79
96
  <path
80
97
  tabIndex={-1}
@@ -1,37 +1,43 @@
1
1
  import { useContext } from 'react'
2
- import { mesh, Topology } from 'topojson-client'
2
+ import { Topology } from 'topojson-client'
3
3
  import ConfigContext from '../../../../context'
4
- import { getGeoFillColor, getGeoStrokeColor } from '../../../../helpers/colors'
4
+ import { getGeoStrokeColor } from '../../../../helpers/colors'
5
+ import { getStatesPicked } from '../../../../helpers/getStatesPicked'
5
6
 
6
7
  type StateOutputProps = {
7
8
  topoData: Topology
8
9
  path: any
9
10
  scale: any
11
+ runtimeData?: any
10
12
  }
11
13
 
12
- const StateOutput: React.FC<StateOutputProps> = ({ topoData, path, scale, stateToShow }: StateOutputProps) => {
14
+ const StateOutput: React.FC<StateOutputProps> = ({ topoData, path, scale, runtimeData }: StateOutputProps) => {
13
15
  const { config } = useContext(ConfigContext)
14
- if (!topoData?.objects?.states) return null
15
- let geo = topoData.objects.states.geometries.filter(s => {
16
- return s.properties.name === config.general.statePicked.stateName
16
+ if (!topoData?.states) return null
17
+
18
+ // Use filter-aware state selection instead of direct config access
19
+ const statesPickedData = getStatesPicked(config, runtimeData)
20
+ const stateNames = statesPickedData.map(sp => sp.stateName)
21
+
22
+ const statesPicked = topoData.states.filter(s => {
23
+ return stateNames.includes(s.properties.name)
17
24
  })
18
25
 
19
26
  const geoStrokeColor = getGeoStrokeColor(config)
20
- const geoFillColor = getGeoFillColor(config)
21
27
 
22
- let stateLines = path(mesh(topoData, geo[0]))
28
+ const stateLines = statesPicked.map(s => path(s.geometry))
23
29
 
24
- return (
30
+ return stateLines.map((line, index) => (
25
31
  <g
26
- key={'single-state'}
27
- className='single-state'
28
- style={{ fill: geoFillColor }}
32
+ key={`single-state-${index}`}
33
+ className='single-state pe-none'
34
+ style={{ fill: 'transparent' }}
29
35
  stroke={geoStrokeColor}
30
- strokeWidth={0.95 / scale}
36
+ strokeWidth={2 / scale}
31
37
  >
32
- <path tabIndex={-1} className='state-path' d={stateLines} />
38
+ <path tabIndex={-1} className='state-path' d={line} />
33
39
  </g>
34
- )
40
+ ))
35
41
  }
36
42
 
37
43
  export default StateOutput
@@ -55,7 +55,7 @@ const TerritoriesSection: React.FC<TerritoriesSectionProps> = ({ territories, lo
55
55
  <div className='d-flex flex-wrap' style={{ columnGap: '1.5rem' }}>
56
56
  {(usTerritories.length > 0 || config.general.territoriesAlwaysShow) && (
57
57
  <div>
58
- <h5 className='territories-label'>U.S. territories</h5>
58
+ <span className='territories-label'>U.S. territories</span>
59
59
  <span
60
60
  className={`mt-2 ${isMobileViewport ? 'mb-3' : 'mb-4'} d-flex territories`}
61
61
  style={{ minWidth: `${usTerritories.length * SVG_WIDTH + (usTerritories.length - 1) * SVG_GAP}px` }}
@@ -66,7 +66,7 @@ const TerritoriesSection: React.FC<TerritoriesSectionProps> = ({ territories, lo
66
66
  )}
67
67
  {(freelyAssociatedStates.length > 0 || config.general.territoriesAlwaysShow) && (
68
68
  <div>
69
- <h5 className='territories-label'>Freely associated states</h5>
69
+ <span className='territories-label'>Freely associated states</span>
70
70
  <span
71
71
  className={`mt-2 ${isMobileViewport ? 'mb-3' : 'mb-4'} d-flex territories`}
72
72
  style={{
@@ -7,13 +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'
19
+ import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
20
+ import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
17
21
 
18
22
  const getCountyTopoURL = year => {
19
23
  return `https://www.cdc.gov/TemplatePackage/contrib/data/county-topography/cb_${year}_us_county_20m.json`
@@ -130,17 +134,18 @@ const CountyMap = () => {
130
134
  const {
131
135
  container,
132
136
  containerEl,
133
- data,
137
+ runtimeData,
134
138
  runtimeFilters,
135
139
  runtimeLegend,
136
140
  setConfig,
137
141
  config,
138
142
  tooltipId,
139
143
  tooltipRef,
140
- legendMemo,
141
- legendSpecialClassLastMemo
144
+ interactionLabel
142
145
  } = useContext(ConfigContext)
143
146
 
147
+ const { legendMemo, legendSpecialClassLastMemo } = useLegendMemoContext()
148
+
144
149
  // CREATE STATE LINES
145
150
  const geoStrokeColor = getGeoStrokeColor(config)
146
151
  const { geoClickHandler } = useGeoClickHandler()
@@ -198,7 +203,7 @@ const CountyMap = () => {
198
203
  const canvasRef = useRef()
199
204
 
200
205
  // If runtimeData is not defined, show loader
201
- if (!data || !isTopoReady(topoData, config, runtimeFilters)) {
206
+ if (!runtimeData || !isTopoReady(topoData, config, runtimeFilters)) {
202
207
  return (
203
208
  <div style={{ height: 300 }}>
204
209
  <Loading />
@@ -206,10 +211,18 @@ const CountyMap = () => {
206
211
  )
207
212
  }
208
213
 
209
- const runtimeKeys = Object.keys(data)
214
+ const runtimeKeys = Object.keys(runtimeData)
210
215
  const lineWidth = 1
211
216
 
212
217
  const onReset = () => {
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
+ })
213
226
  setConfig({
214
227
  ...config,
215
228
  mapPosition: { coordinates: [0, 30], zoom: 1 }
@@ -253,8 +266,8 @@ const CountyMap = () => {
253
266
  break
254
267
  }
255
268
  }
256
- if (county && data[county.id]) {
257
- geoClickHandler(displayGeoName(county.id), data[county.id])
269
+ if (county && runtimeData[county.id]) {
270
+ geoClickHandler(displayGeoName(county.id), runtimeData[county.id])
258
271
  }
259
272
  }
260
273
 
@@ -268,17 +281,36 @@ const CountyMap = () => {
268
281
 
269
282
  // Redraw with focus on state
270
283
  setFocus({ id: clickedState.id, index: focusIndex, center: geoCentroid(clickedState), feature: clickedState })
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
+ })
271
293
  }
272
294
  if (config.general.type === 'us-geocode') {
273
295
  const geoRadius = (config.visual.geoCodeCircleSize || 5) * (focus.id ? 2 : 1)
274
296
  let clickedGeo
275
297
  for (let i = 0; i < runtimeKeys.length; i++) {
276
298
  const pixelCoords = topoData.projection([
277
- data[runtimeKeys[i]][config.columns.longitude.name],
278
- data[runtimeKeys[i]][config.columns.latitude.name]
299
+ runtimeData[runtimeKeys[i]][config.columns.longitude.name],
300
+ runtimeData[runtimeKeys[i]][config.columns.latitude.name]
279
301
  ])
280
- if (pixelCoords && Math.sqrt(Math.pow(pixelCoords[0] - x, 2) + Math.pow(pixelCoords[1] - y, 2)) < geoRadius) {
281
- 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]]
282
314
  break
283
315
  }
284
316
  }
@@ -319,7 +351,7 @@ const CountyMap = () => {
319
351
  if (
320
352
  !isNaN(currentTooltipIndex) &&
321
353
  applyLegendToRow(
322
- data[topoData.mapData[currentTooltipIndex].id],
354
+ runtimeData[topoData.mapData[currentTooltipIndex].id],
323
355
  config,
324
356
  runtimeLegend,
325
357
  legendMemo,
@@ -327,7 +359,7 @@ const CountyMap = () => {
327
359
  )
328
360
  ) {
329
361
  context.fillStyle = applyLegendToRow(
330
- data[topoData.mapData[currentTooltipIndex].id],
362
+ runtimeData[topoData.mapData[currentTooltipIndex].id],
331
363
  config,
332
364
  runtimeLegend,
333
365
  legendMemo,
@@ -364,10 +396,10 @@ const CountyMap = () => {
364
396
  }
365
397
 
366
398
  // If the hovered county is found, show the tooltip for that county, otherwise hide the tooltip
367
- if (county && data[county.id]) {
368
- 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)) {
369
401
  let fillColor = applyLegendToRow(
370
- data[county.id],
402
+ runtimeData[county.id],
371
403
  config,
372
404
  runtimeLegend,
373
405
  legendMemo,
@@ -384,6 +416,24 @@ const CountyMap = () => {
384
416
  context.stroke()
385
417
  }
386
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
+
387
437
  tooltipRef.current.style.display = 'block'
388
438
  tooltipRef.current.style.top = tooltipY + 'px'
389
439
  if (tooltipX > containerBounds.width / 2) {
@@ -393,7 +443,7 @@ const CountyMap = () => {
393
443
  tooltipRef.current.style.transform = 'translate(0, -50%)'
394
444
  tooltipRef.current.style.left = tooltipX + 5 + 'px'
395
445
  }
396
- tooltipRef.current.innerHTML = applyTooltipsToGeo(displayGeoName(county.id), data[county.id])
446
+ tooltipRef.current.innerHTML = applyTooltipsToGeo(displayGeoName(county.id), runtimeData[county.id])
397
447
  tooltipRef.current.setAttribute('data-index', countyIndex)
398
448
  } else {
399
449
  tooltipRef.current.style.display = 'none'
@@ -404,8 +454,8 @@ const CountyMap = () => {
404
454
  // Handle geo map hover
405
455
  if (!isNaN(currentTooltipIndex)) {
406
456
  const pixelCoords = topoData.projection([
407
- data[runtimeKeys[currentTooltipIndex]][config.columns.longitude.name],
408
- data[runtimeKeys[currentTooltipIndex]][config.columns.latitude.name]
457
+ runtimeData[runtimeKeys[currentTooltipIndex]][config.columns.longitude.name],
458
+ runtimeData[runtimeKeys[currentTooltipIndex]][config.columns.latitude.name]
409
459
  ])
410
460
  if (pixelCoords && Math.sqrt(Math.pow(pixelCoords[0] - x, 2) + Math.pow(pixelCoords[1] - y, 2)) < geoRadius) {
411
461
  return // The user is still hovering over the previous geo point, don't redraw tooltip
@@ -419,17 +469,30 @@ const CountyMap = () => {
419
469
  let hoveredGeoIndex
420
470
  for (let i = 0; i < runtimeKeys.length; i++) {
421
471
  const pixelCoords = topoData.projection([
422
- data[runtimeKeys[i]][config.columns.longitude.name],
423
- data[runtimeKeys[i]][config.columns.latitude.name]
472
+ runtimeData[runtimeKeys[i]][config.columns.longitude.name],
473
+ runtimeData[runtimeKeys[i]][config.columns.latitude.name]
424
474
  ])
425
475
  let includedShapes = ['circle', 'diamond', 'star', 'triangle', 'square'].includes(config.visual.cityStyle)
426
476
  if (
427
477
  includedShapes &&
428
478
  pixelCoords &&
429
479
  Math.sqrt(Math.pow(pixelCoords[0] - x, 2) + Math.pow(pixelCoords[1] - y, 2)) < geoRadius &&
430
- 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
+ )
431
494
  ) {
432
- hoveredGeo = data[runtimeKeys[i]]
495
+ hoveredGeo = runtimeData[runtimeKeys[i]]
433
496
  hoveredGeoIndex = i
434
497
  break
435
498
  }
@@ -438,9 +501,10 @@ const CountyMap = () => {
438
501
  const distance = Math.hypot(pixelCoords[0] - x, pixelCoords[1] - y)
439
502
  if (
440
503
  distance < 15 &&
441
- 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)
442
506
  ) {
443
- hoveredGeo = data[runtimeKeys[i]]
507
+ hoveredGeo = runtimeData[runtimeKeys[i]]
444
508
  hoveredGeoIndex = i
445
509
  break
446
510
  }
@@ -448,6 +512,21 @@ const CountyMap = () => {
448
512
  }
449
513
 
450
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
+
451
530
  tooltipRef.current.style.display = 'block'
452
531
  tooltipRef.current.style.top = tooltipY + 'px'
453
532
  if (tooltipX > containerBounds.width / 2) {
@@ -524,7 +603,7 @@ const CountyMap = () => {
524
603
  if (!focus.id && config.general.type === 'us-geocode' && geo.id.length > 2) return
525
604
 
526
605
  // Gets numeric data associated with the topo data for this state/county
527
- const geoData = data[geo.id]
606
+ const geoData = runtimeData[geo.id]
528
607
 
529
608
  // Renders state/county
530
609
  const legendValues =
@@ -569,7 +648,7 @@ const CountyMap = () => {
569
648
  context.strokeStyle = geoStrokeColor
570
649
  const geoRadius = (config.visual.geoCodeCircleSize || 5) * (focus.id ? 2 : 1)
571
650
  const { additionalCityStyles } = config.visual || []
572
- const cityStyles = Object.values(data)
651
+ const cityStyles = Object.values(runtimeData)
573
652
  .filter(d => additionalCityStyles.some(style => String(d[style.column]) === String(style.value)))
574
653
  .map(d => {
575
654
  const conditionsMatched = additionalCityStyles.find(
@@ -587,7 +666,7 @@ const CountyMap = () => {
587
666
 
588
667
  if (cityPixelCoords) {
589
668
  const legendValues = applyLegendToRow(
590
- data[city?.value],
669
+ runtimeData[city?.value],
591
670
  config,
592
671
  runtimeLegend,
593
672
  legendMemo,
@@ -608,13 +687,13 @@ const CountyMap = () => {
608
687
  const citiesList = new Set(cityStyles.map(item => item.value))
609
688
 
610
689
  const pixelCoords = topoData.projection([
611
- data[key][config.columns.longitude.name],
612
- data[key][config.columns.latitude.name]
690
+ runtimeData[key][config.columns.longitude.name],
691
+ runtimeData[key][config.columns.latitude.name]
613
692
  ])
614
693
  if (pixelCoords && !citiesList.has(key)) {
615
694
  const legendValues =
616
- data[key] !== undefined
617
- ? applyLegendToRow(data[key], config, runtimeLegend, legendMemo, legendSpecialClassLastMemo)
695
+ runtimeData[key] !== undefined
696
+ ? applyLegendToRow(runtimeData[key], config, runtimeLegend, legendMemo, legendSpecialClassLastMemo)
618
697
  : false
619
698
  if (legendValues) {
620
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`}>