@cdc/map 4.25.8 → 4.25.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 (137) hide show
  1. package/.claude/agents/typescript-organizer.md +118 -0
  2. package/.claude/settings.local.json +30 -0
  3. package/dist/{cdcmap-fce76882.es.js → cdcmap-BnB1QM5d.es.js} +6 -13
  4. package/dist/{cdcmap-c55ac1ea.es.js → cdcmap-D6CG2-Hb.es.js} +5 -12
  5. package/dist/{cdcmap-31a33da1.es.js → cdcmap-MXgURbdZ.es.js} +6 -13
  6. package/dist/{cdcmap-1a1724a1.es.js → cdcmap-dgT_1dIT.es.js} +136 -151
  7. package/dist/cdcmap.js +56991 -53706
  8. package/examples/example-city-state.json +9 -1
  9. package/examples/multi-country-centering.json +45 -0
  10. package/examples/private/c.json +290 -0
  11. package/examples/private/canvas-city-hover.json +787 -0
  12. package/examples/private/colors-2.json +221 -0
  13. package/examples/private/colors.json +221 -0
  14. package/examples/private/d.json +345 -0
  15. package/examples/private/g.json +1 -0
  16. package/examples/private/h.json +105911 -0
  17. package/examples/private/measles-data.json +378 -0
  18. package/examples/private/measles.json +211 -0
  19. package/examples/private/north-dakota.json +1132 -0
  20. package/examples/private/state-with-pattern.json +883 -0
  21. package/index.html +36 -34
  22. package/package.json +26 -5
  23. package/src/CdcMap.tsx +23 -8
  24. package/src/CdcMapComponent.tsx +238 -308
  25. package/src/_stories/CdcMap.ColumnWrap.stories.tsx +31 -0
  26. package/src/_stories/CdcMap.DistrictOfColumbia.stories.tsx +320 -0
  27. package/src/_stories/CdcMap.Editor.stories.tsx +3371 -0
  28. package/src/_stories/CdcMap.Filters.stories.tsx +2 -2
  29. package/src/_stories/CdcMap.Legend.Gradient.stories.tsx +3 -3
  30. package/src/_stories/CdcMap.Legend.stories.tsx +7 -4
  31. package/src/_stories/CdcMap.Patterns.stories.tsx +2 -2
  32. package/src/_stories/CdcMap.SmallMultiples.stories.tsx +35 -0
  33. package/src/_stories/CdcMap.Table.stories.tsx +2 -2
  34. package/src/_stories/CdcMap.stories.tsx +37 -9
  35. package/src/_stories/GoogleMap.stories.tsx +2 -2
  36. package/src/_stories/UsaMap.NoData.stories.tsx +2 -2
  37. package/src/_stories/_mock/column-wrap-test.json +265 -0
  38. package/src/_stories/_mock/equal-number.json +1109 -0
  39. package/src/_stories/_mock/multi-country-hide.json +78 -0
  40. package/src/_stories/_mock/multi-country.json +95 -0
  41. package/src/_stories/_mock/multi-state.json +887 -20403
  42. package/src/_stories/_mock/small_multiples/multi-state-small-multiples.json +8399 -0
  43. package/src/_stories/_mock/small_multiples/region-small-multiples.json +657 -0
  44. package/src/_stories/_mock/small_multiples/wastewater-map-small-multiples.json +221 -0
  45. package/src/_stories/_mock/us-bubble-cities.json +306 -0
  46. package/src/_stories/_mock/usa-state-gradient.json +2 -4
  47. package/src/components/BubbleList.tsx +17 -13
  48. package/src/components/CityList.tsx +85 -107
  49. package/src/components/EditorPanel/components/EditorPanel.tsx +787 -709
  50. package/src/components/EditorPanel/components/HexShapeSettings.tsx +58 -95
  51. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +34 -42
  52. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +354 -0
  53. package/src/components/EditorPanel/components/Panels/index.tsx +3 -1
  54. package/src/components/Geo.tsx +22 -3
  55. package/src/components/Legend/components/Legend.tsx +76 -40
  56. package/src/components/Legend/components/LegendGroup/Legend.Group.tsx +10 -7
  57. package/src/components/Legend/components/index.scss +1 -1
  58. package/src/components/MapContainer.tsx +52 -0
  59. package/src/components/MapControls.tsx +44 -0
  60. package/src/components/NavigationMenu.tsx +27 -15
  61. package/src/components/SmallMultiples/SmallMultipleTile.tsx +163 -0
  62. package/src/components/SmallMultiples/SmallMultiples.css +32 -0
  63. package/src/components/SmallMultiples/SmallMultiples.tsx +150 -0
  64. package/src/components/SmallMultiples/SynchronizedTooltip.tsx +105 -0
  65. package/src/components/SmallMultiples/index.tsx +3 -0
  66. package/src/components/UsaMap/components/SingleState/SingleState.CountyOutput.tsx +36 -4
  67. package/src/components/UsaMap/components/TerritoriesSection.tsx +26 -12
  68. package/src/components/UsaMap/components/Territory/Territory.Hexagon.tsx +30 -4
  69. package/src/components/UsaMap/components/Territory/Territory.Rectangle.tsx +23 -4
  70. package/src/components/UsaMap/components/Territory/TerritoryShape.ts +6 -0
  71. package/src/components/UsaMap/components/UsaMap.County.tsx +123 -37
  72. package/src/components/UsaMap/components/UsaMap.Region.tsx +36 -5
  73. package/src/components/UsaMap/components/UsaMap.SingleState.tsx +30 -10
  74. package/src/components/UsaMap/components/UsaMap.State.tsx +53 -12
  75. package/src/components/UsaMap/helpers/map.ts +4 -4
  76. package/src/components/UsaMap/helpers/shapes.ts +9 -6
  77. package/src/components/WorldMap/WorldMap.tsx +193 -35
  78. package/src/components/ZoomControls.tsx +6 -9
  79. package/src/context/LegendMemoContext.tsx +30 -0
  80. package/src/context.ts +1 -40
  81. package/src/data/initial-state.js +153 -130
  82. package/src/data/supported-geos.js +25 -78
  83. package/src/helpers/addUIDs.ts +13 -2
  84. package/src/helpers/applyColorToLegend.ts +140 -20
  85. package/src/helpers/applyLegendToRow.ts +10 -6
  86. package/src/helpers/componentHelpers.ts +8 -0
  87. package/src/helpers/constants.ts +12 -14
  88. package/src/helpers/dataTableHelpers.ts +6 -0
  89. package/src/helpers/displayGeoName.ts +18 -3
  90. package/src/helpers/generateRuntimeLegend.ts +44 -10
  91. package/src/helpers/generateRuntimeLegendHash.ts +4 -2
  92. package/src/helpers/getColumnNames.ts +1 -1
  93. package/src/helpers/getCountriesPicked.ts +103 -0
  94. package/src/helpers/getMapContainerClasses.ts +7 -0
  95. package/src/helpers/getPatternForRow.ts +33 -0
  96. package/src/helpers/getStatesPicked.ts +8 -5
  97. package/src/helpers/index.ts +3 -3
  98. package/src/helpers/isLegendItemDisabled.ts +16 -0
  99. package/src/helpers/mapObserverHelpers.ts +40 -0
  100. package/src/helpers/resetLegendToggles.ts +3 -2
  101. package/src/helpers/smallMultiplesHelpers.ts +359 -0
  102. package/src/helpers/tests/titleCase.test.ts +76 -0
  103. package/src/helpers/titleCase.ts +13 -13
  104. package/src/helpers/toggleLegendActive.ts +6 -11
  105. package/src/helpers/urlDataHelpers.ts +70 -0
  106. package/src/hooks/useCountryZoom.tsx +241 -0
  107. package/src/hooks/useGeoClickHandler.ts +36 -2
  108. package/src/hooks/useLegendMemo.ts +17 -0
  109. package/src/hooks/useMapLayers.tsx +5 -4
  110. package/src/hooks/useProgrammaticMapTooltip.ts +110 -0
  111. package/src/hooks/useResizeObserver.ts +5 -2
  112. package/src/hooks/useStateZoom.tsx +30 -8
  113. package/src/hooks/useSynchronizedGeographies.ts +56 -0
  114. package/src/hooks/useTooltip.ts +1 -2
  115. package/src/index.jsx +1 -2
  116. package/src/scss/editor-panel.scss +4 -440
  117. package/src/scss/main.scss +1 -1
  118. package/src/scss/map.scss +12 -15
  119. package/src/store/map.actions.ts +7 -7
  120. package/src/store/map.reducer.ts +17 -6
  121. package/src/test/CdcMap.test.jsx +11 -0
  122. package/src/types/MapConfig.ts +46 -18
  123. package/src/types/MapContext.ts +6 -7
  124. package/src/types/runtimeLegend.ts +17 -1
  125. package/vite.config.js +2 -7
  126. package/vitest.config.ts +16 -0
  127. package/src/components/DataTable.tsx +0 -385
  128. package/src/components/EditorPanel/components/Inputs.tsx +0 -59
  129. package/src/coreStyles_map.scss +0 -3
  130. package/src/helpers/colorDistributions.ts +0 -12
  131. package/src/helpers/generateColorsArray.ts +0 -14
  132. package/src/helpers/tests/generateColorsArray.test.ts +0 -18
  133. package/src/helpers/tests/generateRuntimeLegendHash.test.ts +0 -11
  134. package/src/hooks/useActiveElement.ts +0 -19
  135. package/src/scss/mixins.scss +0 -47
  136. package/src/types/Annotations.ts +0 -24
  137. /package/dist/{cdcmap-548642e6.es.js → cdcmap-Ct2SB0vL.es.js} +0 -0
@@ -1,10 +1,14 @@
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'
11
+ import { useSynchronizedGeographies } from '../../../../hooks/useSynchronizedGeographies'
8
12
 
9
13
  interface CountyOutputProps {
10
14
  counties: any[]
@@ -15,14 +19,16 @@ interface CountyOutputProps {
15
19
  }
16
20
 
17
21
  const CountyOutput: React.FC<CountyOutputProps> = ({ path, counties, scale, geoStrokeColor, tooltipId }) => {
18
- const { config, data, legendMemo, legendSpecialClassLastMemo, runtimeLegend } = useContext<MapContext>(ConfigContext)
22
+ const { config, runtimeData, runtimeLegend, interactionLabel } = useContext<MapContext>(ConfigContext)
23
+ const { legendMemo, legendSpecialClassLastMemo } = useLegendMemoContext()
19
24
  const { applyTooltipsToGeo } = useApplyTooltipsToGeo()
20
25
  const geoFillColor = getGeoFillColor(config)
21
26
  const { geoClickHandler } = useGeoClickHandler()
27
+ const { getSyncProps, syncHandlers } = useSynchronizedGeographies()
22
28
 
23
29
  return (
24
30
  <>
25
- {counties.map(county => {
31
+ {counties.map((county, countyIndex) => {
26
32
  // Map the name from the geo data with the appropriate key for the processed data
27
33
  const geoKey = county.id
28
34
 
@@ -30,7 +36,7 @@ const CountyOutput: React.FC<CountyOutputProps> = ({ path, counties, scale, geoS
30
36
 
31
37
  const countyPath = path(county)
32
38
 
33
- const geoData = data[county.id]
39
+ const geoData = runtimeData[county.id]
34
40
  let legendColors
35
41
 
36
42
  // Once we receive data for this geographic item, setup variables.
@@ -67,6 +73,7 @@ const CountyOutput: React.FC<CountyOutputProps> = ({ path, counties, scale, geoS
67
73
 
68
74
  return (
69
75
  <g
76
+ {...getSyncProps(geoKey)}
70
77
  key={`key--${county.id}`}
71
78
  className={`county county--${geoDisplayName.split(' ').join('')} county--${
72
79
  geoData[config.columns.geo.name]
@@ -75,6 +82,24 @@ const CountyOutput: React.FC<CountyOutputProps> = ({ path, counties, scale, geoS
75
82
  onClick={() => geoClickHandler(geoDisplayName, geoData)}
76
83
  data-tooltip-id={`tooltip__${tooltipId}`}
77
84
  data-tooltip-html={toolTip}
85
+ onMouseEnter={e => {
86
+ // Track hover analytics event if this is a new location
87
+ const locationName = geoDisplayName.replace(/[^a-zA-Z0-9]/g, '_')
88
+ publishAnalyticsEvent({
89
+ vizType: config.type,
90
+ vizSubType: getVizSubType(config),
91
+ eventType: `map_hover`,
92
+ eventAction: 'hover',
93
+ eventLabel: interactionLabel,
94
+ vizTitle: getVizTitle(config),
95
+ location: geoDisplayName,
96
+ specifics: `location: ${locationName?.toLowerCase()}`
97
+ })
98
+ syncHandlers.onMouseEnter(geoKey, e.clientY)
99
+ }}
100
+ onMouseLeave={() => {
101
+ syncHandlers.onMouseLeave()
102
+ }}
78
103
  >
79
104
  <path
80
105
  tabIndex={-1}
@@ -88,11 +113,18 @@ const CountyOutput: React.FC<CountyOutputProps> = ({ path, counties, scale, geoS
88
113
  } else {
89
114
  return (
90
115
  <g
116
+ {...getSyncProps(geoKey)}
91
117
  key={`key--${county.id}`}
92
118
  className={`county county--${geoDisplayName.split(' ').join('')}`}
93
119
  style={{ fill: geoFillColor }}
94
120
  data-tooltip-id={`tooltip__${tooltipId}`}
95
121
  data-tooltip-html={toolTip}
122
+ onMouseEnter={e => {
123
+ syncHandlers.onMouseEnter(geoKey, e.clientY)
124
+ }}
125
+ onMouseLeave={() => {
126
+ syncHandlers.onMouseLeave()
127
+ }}
96
128
  >
97
129
  <path
98
130
  tabIndex={-1}
@@ -12,7 +12,7 @@ type TerritoriesSectionProps = {
12
12
  }
13
13
 
14
14
  const TerritoriesSection: React.FC<TerritoriesSectionProps> = ({ territories, logo, config, territoriesData }) => {
15
- const { currentViewport } = useContext<MapContext>(ConfigContext)
15
+ const { currentViewport, vizViewport } = useContext<MapContext>(ConfigContext)
16
16
 
17
17
  // filter territioriesData into the two groups below
18
18
  const freelyAssociatedKeys = territoriesData.filter(territory => {
@@ -38,7 +38,7 @@ const TerritoriesSection: React.FC<TerritoriesSectionProps> = ({ territories, lo
38
38
  return a.props.label.localeCompare(b.props.label)
39
39
  })
40
40
 
41
- const isMobileViewport = isMobileTerritoryViewport(currentViewport)
41
+ const isMobileViewport = isMobileTerritoryViewport(vizViewport)
42
42
  const SVG_GAP = 9
43
43
  const SVG_WIDTH = isMobileViewport ? 30 : 45
44
44
 
@@ -55,10 +55,18 @@ 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
- <span className='territories-label'>U.S. territories</span>
58
+ <span className='territories-label' style={{ fontSize: isMobileViewport ? '0.8rem' : '1rem' }}>
59
+ U.S. territories
60
+ </span>
59
61
  <span
60
- className={`mt-2 ${isMobileViewport ? 'mb-3' : 'mb-4'} d-flex territories`}
61
- style={{ minWidth: `${usTerritories.length * SVG_WIDTH + (usTerritories.length - 1) * SVG_GAP}px` }}
62
+ className={`${isMobileViewport ? 'mt-1 mb-3' : 'mt-2 mb-4'} d-flex territories`}
63
+ style={
64
+ {
65
+ minWidth: `${usTerritories.length * SVG_WIDTH + (usTerritories.length - 1) * SVG_GAP}px`,
66
+ '--territory-svg-max-width': `${SVG_WIDTH}px`,
67
+ '--territory-svg-min-width': `${SVG_WIDTH}px`
68
+ } as React.CSSProperties
69
+ }
62
70
  >
63
71
  {usTerritories}
64
72
  </span>
@@ -66,14 +74,20 @@ const TerritoriesSection: React.FC<TerritoriesSectionProps> = ({ territories, lo
66
74
  )}
67
75
  {(freelyAssociatedStates.length > 0 || config.general.territoriesAlwaysShow) && (
68
76
  <div>
69
- <span className='territories-label'>Freely associated states</span>
77
+ <span className='territories-label' style={{ fontSize: isMobileViewport ? '0.8rem' : '1rem' }}>
78
+ Freely associated states
79
+ </span>
70
80
  <span
71
- className={`mt-2 ${isMobileViewport ? 'mb-3' : 'mb-4'} d-flex territories`}
72
- style={{
73
- minWidth: `${
74
- freelyAssociatedStates.length * SVG_WIDTH + (freelyAssociatedStates.length - 1) * SVG_GAP
75
- }px`
76
- }}
81
+ className={`${isMobileViewport ? 'mt-1 mb-3' : 'mt-2 mb-4'} d-flex territories`}
82
+ style={
83
+ {
84
+ minWidth: `${
85
+ freelyAssociatedStates.length * SVG_WIDTH + (freelyAssociatedStates.length - 1) * SVG_GAP
86
+ }px`,
87
+ '--territory-svg-max-width': `${SVG_WIDTH}px`,
88
+ '--territory-svg-min-width': `${SVG_WIDTH}px`
89
+ } as React.CSSProperties
90
+ }
77
91
  >
78
92
  {freelyAssociatedStates}
79
93
  </span>
@@ -43,12 +43,17 @@ const TerritoryHexagon = ({
43
43
  territory,
44
44
  territoryData,
45
45
  textColor,
46
+ getSyncProps,
47
+ syncHandlers,
46
48
  ...props
47
49
  }) => {
48
50
  const { config } = useContext<MapContext>(ConfigContext)
49
51
 
50
52
  const isHex = config.general.displayAsHex
51
53
 
54
+ // Construct geography key: use territory prop if available, otherwise construct from label
55
+ const geoKey = territory || `US-${label}`
56
+
52
57
  // Labels
53
58
  const hexagonLabel = (geo, bgColor = '#FFFFFF', projection) => {
54
59
  let centroid = projection ? projection(geoCentroid(geo)) : [22, 17.5]
@@ -133,7 +138,14 @@ const TerritoryHexagon = ({
133
138
  fontSize={14}
134
139
  x={'50%'}
135
140
  y={y}
136
- style={{ fill: 'currentColor', stroke: strokeColor, fontWeight: 900, opacity: 1, fillOpacity: 1 }}
141
+ style={{
142
+ fill: 'currentColor',
143
+ stroke: strokeColor,
144
+ fontWeight: 900,
145
+ opacity: 1,
146
+ fillOpacity: 1,
147
+ pointerEvents: 'none'
148
+ }}
137
149
  paintOrder='stroke'
138
150
  textAnchor='middle'
139
151
  verticalAnchor='middle'
@@ -143,7 +155,9 @@ const TerritoryHexagon = ({
143
155
  >
144
156
  {abbr.substring(3)}
145
157
  </Text>
146
- {config.general.displayAsHex && config.hexMap.type === 'shapes' && getArrowDirection(territoryData, geo, true)}
158
+ {config.general.displayAsHex &&
159
+ config.hexMap.type === 'shapes' &&
160
+ getArrowDirection(territoryData, geo, true)}
147
161
  </>
148
162
  )
149
163
  }
@@ -151,7 +165,7 @@ const TerritoryHexagon = ({
151
165
  let [dx, dy] = offsets[abbr]
152
166
 
153
167
  return (
154
- <g>
168
+ <g style={{ pointerEvents: 'none' }}>
155
169
  <line
156
170
  x1={centroid[0]}
157
171
  y1={centroid[1]}
@@ -179,11 +193,23 @@ const TerritoryHexagon = ({
179
193
 
180
194
  return (
181
195
  <svg viewBox='-1 -1 46 53' className='territory-wrapper--hex'>
182
- <g {...props} data-tooltip-html={dataTooltipHtml} data-tooltip-id={dataTooltipId} onClick={handleShapeClick}>
196
+ <g
197
+ {...(getSyncProps ? getSyncProps(geoKey) : {})}
198
+ {...props}
199
+ data-tooltip-html={dataTooltipHtml}
200
+ data-tooltip-id={dataTooltipId}
201
+ onClick={handleShapeClick}
202
+ >
183
203
  <polygon
184
204
  stroke={stroke}
185
205
  strokeWidth={strokeWidth}
186
206
  points='22 0 44 12.702 44 38.105 22 50.807 0 38.105 0 12.702'
207
+ onMouseEnter={e => {
208
+ syncHandlers?.onMouseEnter(geoKey, e.clientY)
209
+ }}
210
+ onMouseLeave={() => {
211
+ syncHandlers?.onMouseLeave()
212
+ }}
187
213
  />
188
214
  {config.general.displayAsHex && hexagonLabel(territoryData, stroke, false)}
189
215
  </g>
@@ -18,6 +18,9 @@ const TerritoryRectangle: React.FC<TerritoryShape> = ({
18
18
  territory,
19
19
  textColor,
20
20
  backgroundColor,
21
+ svgStyle,
22
+ getSyncProps,
23
+ syncHandlers,
21
24
  ...props
22
25
  }) => {
23
26
  const { config } = useContext<MapContext>(ConfigContext)
@@ -25,17 +28,32 @@ const TerritoryRectangle: React.FC<TerritoryShape> = ({
25
28
  const rectanglePath =
26
29
  'M42,0.5 C42.8284271,0.5 43.5,1.17157288 43.5,2 L43.5,2 L43.5,26 C43.5,26.8284271 42.8284271,27.5 42,27.5 L42,27.5 L3,27.5 C2.17157288,27.5 1.5,26.8284271 1.5,26 L1.5,26 L1.5,2 C1.5,1.17157288 2.17157288,0.5 3,0.5 L3,0.5 Z'
27
30
 
31
+ const geoKey = territory || `US-${label}`
32
+
28
33
  return (
29
- <svg viewBox='0 0 45 29' key={territory} className={territory}>
34
+ <svg viewBox='0 0 45 29' key={geoKey} className={geoKey} style={svgStyle}>
30
35
  <g
36
+ {...(getSyncProps ? getSyncProps(geoKey) : {})}
31
37
  {...otherProps}
32
38
  strokeLinejoin='round'
33
39
  tabIndex={-1}
34
40
  onClick={handleShapeClick}
35
41
  data-tooltip-id={dataTooltipId}
36
42
  data-tooltip-html={dataTooltipHtml}
43
+ onMouseEnter={e => {
44
+ syncHandlers?.onMouseEnter(geoKey, e.clientY)
45
+ }}
46
+ onMouseLeave={() => {
47
+ syncHandlers?.onMouseLeave()
48
+ }}
37
49
  >
38
- <path stroke={stroke} strokeWidth={strokeWidth} d={rectanglePath} {...otherProps} />
50
+ <path
51
+ stroke={stroke}
52
+ strokeWidth={strokeWidth}
53
+ d={rectanglePath}
54
+ style={{ pointerEvents: 'none' }}
55
+ {...otherProps}
56
+ />
39
57
  <text
40
58
  textAnchor='middle'
41
59
  dominantBaseline='middle'
@@ -45,7 +63,7 @@ const TerritoryRectangle: React.FC<TerritoryShape> = ({
45
63
  stroke={strokeColor}
46
64
  className='territory-text'
47
65
  paintOrder='stroke'
48
- onClick={handleShapeClick}
66
+ style={{ pointerEvents: 'none' }}
49
67
  data-tooltip-id={dataTooltipId}
50
68
  data-tooltip-html={dataTooltipHtml}
51
69
  >
@@ -97,6 +115,7 @@ const TerritoryRectangle: React.FC<TerritoryShape> = ({
97
115
  strokeWidth={strokeWidth}
98
116
  d={rectanglePath}
99
117
  fill={`url(#territory-${territory}-${patternData?.dataKey}--${patternIndex})`}
118
+ style={{ pointerEvents: 'none' }}
100
119
  className={[
101
120
  `territory-pattern-${patternData?.dataKey}`,
102
121
  `territory-pattern-${patternData?.dataKey}--${patternData.dataValue}`
@@ -111,7 +130,7 @@ const TerritoryRectangle: React.FC<TerritoryShape> = ({
111
130
  stroke={strokeColor}
112
131
  className='territory-text'
113
132
  paintOrder='stroke'
114
- onClick={handleShapeClick}
133
+ style={{ pointerEvents: 'none' }}
115
134
  data-tooltip-id={dataTooltipId}
116
135
  data-tooltip-html={dataTooltipHtml}
117
136
  >
@@ -11,4 +11,10 @@ export type TerritoryShape = {
11
11
  territory: string
12
12
  territoryData: object
13
13
  textColor: string
14
+ svgStyle?: React.CSSProperties
15
+ getSyncProps?: (geoKey: string) => any
16
+ syncHandlers?: {
17
+ onMouseEnter: (geoKey: string, clientY: number) => void
18
+ onMouseLeave: () => void
19
+ }
14
20
  }
@@ -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,22 @@ 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(
505
+ runtimeData[runtimeKeys[i]],
506
+ config,
507
+ runtimeLegend,
508
+ legendMemo,
509
+ legendSpecialClassLastMemo
510
+ ) &&
511
+ !isLegendItemDisabled(
512
+ runtimeData[runtimeKeys[i]],
513
+ runtimeLegend,
514
+ legendMemo,
515
+ legendSpecialClassLastMemo,
516
+ config
517
+ )
447
518
  ) {
448
- hoveredGeo = data[runtimeKeys[i]]
519
+ hoveredGeo = runtimeData[runtimeKeys[i]]
449
520
  hoveredGeoIndex = i
450
521
  break
451
522
  }
@@ -453,6 +524,21 @@ const CountyMap = () => {
453
524
  }
454
525
 
455
526
  if (hoveredGeo) {
527
+ // Track hover analytics event if this is a new location
528
+ if (isNaN(currentTooltipIndex) || currentTooltipIndex !== hoveredGeoIndex) {
529
+ const locationName = displayGeoName(hoveredGeo[config.columns.geo.name]).replace(/[^a-zA-Z0-9]/g, '_')
530
+ publishAnalyticsEvent({
531
+ vizType: config.type,
532
+ vizSubType: getVizSubType(config),
533
+ eventType: `map_hover`,
534
+ eventAction: 'hover',
535
+ eventLabel: interactionLabel,
536
+ vizTitle: getVizTitle(config),
537
+ location: displayGeoName(hoveredGeo[config.columns.geo.name]),
538
+ specifics: `location: ${locationName?.toLowerCase()}`
539
+ })
540
+ }
541
+
456
542
  tooltipRef.current.style.display = 'block'
457
543
  tooltipRef.current.style.top = tooltipY + 'px'
458
544
  if (tooltipX > containerBounds.width / 2) {
@@ -529,7 +615,7 @@ const CountyMap = () => {
529
615
  if (!focus.id && config.general.type === 'us-geocode' && geo.id.length > 2) return
530
616
 
531
617
  // Gets numeric data associated with the topo data for this state/county
532
- const geoData = data[geo.id]
618
+ const geoData = runtimeData[geo.id]
533
619
 
534
620
  // Renders state/county
535
621
  const legendValues =
@@ -574,7 +660,7 @@ const CountyMap = () => {
574
660
  context.strokeStyle = geoStrokeColor
575
661
  const geoRadius = (config.visual.geoCodeCircleSize || 5) * (focus.id ? 2 : 1)
576
662
  const { additionalCityStyles } = config.visual || []
577
- const cityStyles = Object.values(data)
663
+ const cityStyles = Object.values(runtimeData)
578
664
  .filter(d => additionalCityStyles.some(style => String(d[style.column]) === String(style.value)))
579
665
  .map(d => {
580
666
  const conditionsMatched = additionalCityStyles.find(
@@ -592,7 +678,7 @@ const CountyMap = () => {
592
678
 
593
679
  if (cityPixelCoords) {
594
680
  const legendValues = applyLegendToRow(
595
- data[city?.value],
681
+ runtimeData[city?.value],
596
682
  config,
597
683
  runtimeLegend,
598
684
  legendMemo,
@@ -613,13 +699,13 @@ const CountyMap = () => {
613
699
  const citiesList = new Set(cityStyles.map(item => item.value))
614
700
 
615
701
  const pixelCoords = topoData.projection([
616
- data[key][config.columns.longitude.name],
617
- data[key][config.columns.latitude.name]
702
+ runtimeData[key][config.columns.longitude.name],
703
+ runtimeData[key][config.columns.latitude.name]
618
704
  ])
619
705
  if (pixelCoords && !citiesList.has(key)) {
620
706
  const legendValues =
621
- data[key] !== undefined
622
- ? applyLegendToRow(data[key], config, runtimeLegend, legendMemo, legendSpecialClassLastMemo)
707
+ runtimeData[key] !== undefined
708
+ ? applyLegendToRow(runtimeData[key], config, runtimeLegend, legendMemo, legendSpecialClassLastMemo)
623
709
  : false
624
710
  if (legendValues) {
625
711
  if (legendValues?.[0] === '#000000' || legendValues?.[0] === DEFAULT_MAP_BACKGROUND) return