@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,4 +1,4 @@
1
- import { useEffect, memo, useContext } from 'react'
1
+ import { useEffect, memo, useContext, useMemo } from 'react'
2
2
  import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
3
3
  import { geoPath } from 'd3-geo'
4
4
  import { CustomProjection } from '@visx/geo'
@@ -24,7 +24,7 @@ import { getTopoData, getCurrentTopoYear, isTopoReady } from '../helpers/map'
24
24
  import useGeoClickHandler from '../../../hooks/useGeoClickHandler'
25
25
  import { SVG_WIDTH, SVG_HEIGHT, SVG_PADDING, SVG_VIEWBOX } from '../../../helpers'
26
26
  import _ from 'lodash'
27
- import { getStatePicked } from '../../../helpers/getStatePicked'
27
+ import { getStatesPicked } from '../../../helpers/getStatesPicked'
28
28
 
29
29
  const SingleStateMap: React.FC = () => {
30
30
  const {
@@ -41,9 +41,18 @@ 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)
45
- const statePicked = getStatePicked(config, runtimeData)
46
- const stateToShow = topoData?.states?.find(s => s.properties.name === statePicked.stateName)
44
+ const { handleMoveEnd, handleZoomIn, handleZoomOut, handleZoomReset, projection } = useStateZoom(topoData)
45
+
46
+ // Memoize statesPicked to prevent creating new arrays on every render
47
+ const statesPicked = useMemo(() => {
48
+ return getStatesPicked(config, runtimeData)
49
+ }, [
50
+ config.general.statesPicked?.length,
51
+ config.general.statesPicked?.[0]?.stateName
52
+ // Don't include runtimeData as it causes excessive re-renders
53
+ ])
54
+
55
+ const statesToShow = topoData?.states?.find(s => statesPicked.map(sp => sp.stateName).includes(s.properties.name))
47
56
 
48
57
  const { geoClickHandler } = useGeoClickHandler()
49
58
 
@@ -72,8 +81,8 @@ const SingleStateMap: React.FC = () => {
72
81
  }
73
82
 
74
83
  const checkForNoData = () => {
75
- // If no statePicked, return true
76
- if (!statePicked.fipsCode) return true
84
+ // If no statesPicked, return true
85
+ if (statesPicked?.every(sp => !sp.fipsCode)) return true
77
86
  }
78
87
 
79
88
  // Constructs and displays markup for all geos on the map (except territories right now)
@@ -81,17 +90,6 @@ const SingleStateMap: React.FC = () => {
81
90
  const counties = geographies[0].feature.counties
82
91
 
83
92
  let geosJsx = []
84
-
85
- // Push config lines
86
- geosJsx.push(
87
- // prettier-ignore
88
- <SingleState.StateOutput
89
- topoData={topoData}
90
- path={path}
91
- scale={scale}
92
- />
93
- )
94
-
95
93
  // Push county lines
96
94
  geosJsx.push(
97
95
  // prettier-ignore
@@ -103,6 +101,16 @@ const SingleStateMap: React.FC = () => {
103
101
  path={path}
104
102
  />
105
103
  )
104
+ // Push config lines
105
+ geosJsx.push(
106
+ // prettier-ignore
107
+ <SingleState.StateOutput
108
+ topoData={topoData}
109
+ path={path}
110
+ scale={scale}
111
+ runtimeData={runtimeData}
112
+ />
113
+ )
106
114
 
107
115
  // Push city list
108
116
  geosJsx.push(
@@ -121,7 +129,7 @@ const SingleStateMap: React.FC = () => {
121
129
  }
122
130
  return (
123
131
  <ErrorBoundary component='SingleStateMap'>
124
- {statePicked && config.general.allowMapZoom && statePicked.fipsCode && (
132
+ {!!statesPicked.length && config.general.allowMapZoom && statesPicked.some(sp => sp.fipsCode) && (
125
133
  <svg
126
134
  viewBox={SVG_VIEWBOX}
127
135
  preserveAspectRatio='xMinYMin'
@@ -150,7 +158,9 @@ const SingleStateMap: React.FC = () => {
150
158
  data={[
151
159
  {
152
160
  states: topoData?.states,
153
- counties: topoData.counties.filter(c => c.id.substring(0, 2) === statePicked.fipsCode)
161
+ counties: topoData.counties.filter(c =>
162
+ statesPicked.map(sp => sp.fipsCode).includes(c.id.substring(0, 2))
163
+ )
154
164
  }
155
165
  ]}
156
166
  projection={geoAlbersUsaTerritories}
@@ -159,7 +169,7 @@ const SingleStateMap: React.FC = () => {
159
169
  [SVG_PADDING, SVG_PADDING],
160
170
  [SVG_WIDTH - SVG_PADDING, SVG_HEIGHT - SVG_PADDING]
161
171
  ],
162
- stateToShow
172
+ statesToShow
163
173
  ]}
164
174
  >
165
175
  {({ features, projection }) => {
@@ -182,7 +192,7 @@ const SingleStateMap: React.FC = () => {
182
192
  </ZoomableGroup>
183
193
  </svg>
184
194
  )}
185
- {statePicked && !config.general.allowMapZoom && statePicked.fipsCode && (
195
+ {!!statesPicked && !config.general.allowMapZoom && statesPicked.some(sp => sp.fipsCode) && (
186
196
  <svg
187
197
  viewBox={SVG_VIEWBOX}
188
198
  preserveAspectRatio='xMinYMin'
@@ -201,7 +211,9 @@ const SingleStateMap: React.FC = () => {
201
211
  data={[
202
212
  {
203
213
  states: topoData?.states,
204
- counties: topoData.counties.filter(c => c.id.substring(0, 2) === statePicked.fipsCode)
214
+ counties: topoData.counties.filter(c =>
215
+ statesPicked.map(sp => sp.fipsCode).includes(c.id.substring(0, 2))
216
+ )
205
217
  }
206
218
  ]}
207
219
  projection={geoAlbersUsaTerritories}
@@ -210,7 +222,7 @@ const SingleStateMap: React.FC = () => {
210
222
  [SVG_PADDING, SVG_PADDING],
211
223
  [SVG_WIDTH - SVG_PADDING, SVG_HEIGHT - SVG_PADDING]
212
224
  ],
213
- stateToShow
225
+ statesToShow
214
226
  ]}
215
227
  >
216
228
  {({ features }) => {
@@ -250,7 +262,7 @@ const SingleStateMap: React.FC = () => {
250
262
  fontSize={18}
251
263
  style={{ fontSize: '28px', height: '18px' }}
252
264
  >
253
- {config.general.noStateFoundMessage}
265
+ {config.general.noDataMessage}
254
266
  </Text>
255
267
  </svg>
256
268
  )}
@@ -258,7 +270,7 @@ const SingleStateMap: React.FC = () => {
258
270
  // prettier-ignore
259
271
  handleZoomIn={handleZoomIn}
260
272
  handleZoomOut={handleZoomOut}
261
- handleReset={handleReset}
273
+ handleZoomReset={handleZoomReset}
262
274
  />
263
275
  </ErrorBoundary>
264
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,16 +96,24 @@ export const hasMoreThanFromHash = (data: { [key: string]: any }): boolean => {
96
96
  return otherKeys.length > 0
97
97
  }
98
98
 
99
- export const getFilterControllingStatePicked = (state, runtimeData) => {
100
- if (!state.general.filterControlsStatePicked || !runtimeData) {
101
- const statePicked = state?.general?.statePicked?.stateName
102
- return statePicked
99
+ export const getFilterControllingStatesPicked = (state, runtimeData): string[] => {
100
+ if (!state.general.filterControlsStatesPicked || !runtimeData) {
101
+ return state?.general?.statesPicked?.map(sp => sp.stateName) || []
103
102
  } else {
104
103
  if (hasMoreThanFromHash(runtimeData)) {
105
- let statePickedFromFilter = Object.values(runtimeData)?.map(s => s[state.general.filterControlsStatePicked])?.[0]
106
- const statePicked = statePickedFromFilter || state.general.statePicked.stateName || 'Alabama'
107
- return statePicked
104
+ let statesPickedFromFilter = Object.values(runtimeData)?.map(
105
+ s => s[state.general.filterControlsStatesPicked]
106
+ )?.[0]
107
+
108
+ // Only need to check if filter result is an array since it could be a single value
109
+ if (Array.isArray(statesPickedFromFilter)) {
110
+ return statesPickedFromFilter
111
+ } else if (statesPickedFromFilter) {
112
+ return [statesPickedFromFilter]
113
+ } else {
114
+ return state?.general?.statesPicked?.map(sp => sp.stateName) || []
115
+ }
108
116
  }
109
- return null
117
+ return state?.general?.statesPicked?.map(sp => sp.stateName) || []
110
118
  }
111
119
  }
@@ -3,6 +3,7 @@ import { geoMercator } from 'd3-geo'
3
3
  import { Mercator } from '@visx/geo'
4
4
  import { feature } from 'topojson-client'
5
5
  import ConfigContext, { MapDispatchContext } from '../../context'
6
+ import { useLegendMemoContext } from '../../context/LegendMemoContext'
6
7
  import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
7
8
  import ZoomableGroup from '../ZoomableGroup'
8
9
  import Geo from '../Geo'
@@ -27,22 +28,24 @@ import generateRuntimeData from '../../helpers/generateRuntimeData'
27
28
  import { applyLegendToRow } from '../../helpers/applyLegendToRow'
28
29
 
29
30
  import './worldMap.styles.css'
31
+ import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
32
+ import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
30
33
 
31
34
  let projection = geoMercator()
32
35
 
33
36
  const WorldMap = () => {
34
37
  // prettier-ignore
35
38
  const {
36
- data,
39
+ runtimeData,
37
40
  position,
38
- setRuntimeData,
39
41
  config,
40
42
  tooltipId,
41
43
  runtimeLegend,
42
- legendMemo,
43
- legendSpecialClassLastMemo,
44
+ interactionLabel
44
45
  } = useContext(ConfigContext)
45
46
 
47
+ const { legendMemo, legendSpecialClassLastMemo } = useLegendMemoContext()
48
+
46
49
  const { type, allowMapZoom } = config.general
47
50
 
48
51
  const [world, setWorld] = useState(null)
@@ -63,23 +66,97 @@ const WorldMap = () => {
63
66
  return <></>
64
67
  }
65
68
 
66
- const handleReset = () => {
69
+ const handleFiltersReset = () => {
67
70
  const newRuntimeData = generateRuntimeData(config)
68
- dispatch({ type: 'SET_POSITION', payload: { coordinates: [0, 30], zoom: 1 } })
71
+ publishAnalyticsEvent({
72
+ vizType: config.type,
73
+ vizSubType: getVizSubType(config),
74
+ eventType: 'map_filter_reset',
75
+ eventAction: 'click',
76
+ eventLabel: interactionLabel,
77
+ vizTitle: getVizTitle(config)
78
+ })
69
79
  dispatch({ type: 'SET_FILTERED_COUNTRY_CODE', payload: '' })
70
- setRuntimeData(newRuntimeData)
80
+ dispatch({ type: 'SET_RUNTIME_DATA', payload: newRuntimeData })
81
+ }
82
+
83
+ const handleZoomReset = _setRuntimeData => {
84
+ publishAnalyticsEvent({
85
+ vizType: config.type,
86
+ vizSubType: getVizSubType(config),
87
+ eventType: 'map_reset_zoom_level',
88
+ eventAction: 'click',
89
+ eventLabel: interactionLabel,
90
+ vizTitle: getVizTitle(config)
91
+ })
92
+ dispatch({ type: 'SET_POSITION', payload: { coordinates: [0, 30], zoom: 1 } })
71
93
  }
94
+
72
95
  const handleZoomIn = position => {
73
96
  if (position.zoom >= 4) return
97
+ publishAnalyticsEvent({
98
+ vizType: config.type,
99
+ vizSubType: getVizSubType(config),
100
+ eventType: `zoom_in`,
101
+ eventAction: 'click',
102
+ eventLabel: interactionLabel,
103
+ vizTitle: getVizTitle(config),
104
+ specifics: `zoom_level: ${Math.floor(position.zoom * 1.5)}`
105
+ })
106
+ publishAnalyticsEvent({
107
+ vizType: config.type,
108
+ vizSubType: getVizSubType(config),
109
+ eventType: `zoom_in`,
110
+ eventAction: 'click',
111
+ eventLabel: interactionLabel,
112
+ vizTitle: getVizTitle(config),
113
+ specifics: `location: ${position.coordinates}`
114
+ })
74
115
  dispatch({ type: 'SET_POSITION', payload: { coordinates: position.coordinates, zoom: position.zoom * 1.5 } })
75
116
  }
76
117
 
77
118
  const handleZoomOut = position => {
78
119
  if (position.zoom <= 1) return
120
+ publishAnalyticsEvent({
121
+ vizType: config.type,
122
+ vizSubType: getVizSubType(config),
123
+ eventType: `zoom_out`,
124
+ eventAction: 'click',
125
+ eventLabel: interactionLabel,
126
+ vizTitle: getVizTitle(config),
127
+ specifics: `zoom_level: ${Math.floor(position.zoom / 1.5)}`
128
+ })
129
+ publishAnalyticsEvent({
130
+ vizType: config.type,
131
+ vizSubType: getVizSubType(config),
132
+ eventType: `zoom_out`,
133
+ eventAction: 'click',
134
+ eventLabel: interactionLabel,
135
+ vizTitle: getVizTitle(config),
136
+ specifics: `location: ${position.coordinates}`
137
+ })
79
138
  dispatch({ type: 'SET_POSITION', payload: { coordinates: position.coordinates, zoom: position.zoom / 1.5 } })
80
139
  }
81
140
 
82
141
  const handleMoveEnd = position => {
142
+ publishAnalyticsEvent({
143
+ vizType: config.type,
144
+ vizSubType: getVizSubType(config),
145
+ eventType: 'map_panned',
146
+ eventAction: 'drag',
147
+ eventLabel: interactionLabel,
148
+ vizTitle: getVizTitle(config),
149
+ specifics: `zoom: ${position.zoom}`
150
+ })
151
+ publishAnalyticsEvent({
152
+ vizType: config.type,
153
+ vizSubType: getVizSubType(config),
154
+ eventType: 'map_panned',
155
+ eventAction: 'drag',
156
+ eventLabel: interactionLabel,
157
+ vizTitle: getVizTitle(config),
158
+ specifics: `coordinates: ${position.coordinates}`
159
+ })
83
160
  dispatch({ type: 'SET_POSITION', payload: position })
84
161
  }
85
162
 
@@ -88,7 +165,7 @@ const WorldMap = () => {
88
165
  // If the geo.properties.config value is found in the data use that, otherwise fall back to geo.properties.iso
89
166
  const dataHasStateName = config.data.some(d => d[config.columns.geo.name] === geo.properties.state)
90
167
  const geoKey =
91
- geo.properties.state && data[geo.properties.state]
168
+ geo.properties.state && runtimeData[geo.properties.state]
92
169
  ? geo.properties.state
93
170
  : geo.properties.name
94
171
  ? geo.properties.name
@@ -99,7 +176,7 @@ const WorldMap = () => {
99
176
  }
100
177
  if (!geoKey) return null
101
178
 
102
- let geoData = data[geoKey]
179
+ let geoData = runtimeData[geoKey]
103
180
 
104
181
  const geoDisplayName = displayGeoName(supportedCountries[geoKey]?.[0])
105
182
  let legendColors
@@ -152,6 +229,20 @@ const WorldMap = () => {
152
229
  stroke={geoStrokeColor}
153
230
  strokeWidth={strokeWidth}
154
231
  onClick={() => geoClickHandler(geoDisplayName, geoData)}
232
+ onMouseEnter={() => {
233
+ // Track hover analytics event if this is a new location
234
+ const locationName = geoDisplayName.replace(/[^a-zA-Z0-9]/g, '_')
235
+ publishAnalyticsEvent({
236
+ vizType: config.type,
237
+ vizSubType: getVizSubType(config),
238
+ eventType: `map_hover`,
239
+ eventAction: 'hover',
240
+ eventLabel: interactionLabel,
241
+ vizTitle: getVizTitle(config),
242
+ location: geoDisplayName,
243
+ specifics: `location: ${locationName?.toLowerCase()}`
244
+ })
245
+ }}
155
246
  data-tooltip-id={`tooltip__${tooltipId}`}
156
247
  data-tooltip-html={toolTip}
157
248
  tabIndex={-1}
@@ -169,6 +260,20 @@ const WorldMap = () => {
169
260
  strokeWidth={strokeWidth}
170
261
  styles={styles}
171
262
  path={path}
263
+ onMouseEnter={() => {
264
+ // Track hover analytics event if this is a new location
265
+ const locationName = geoDisplayName.replace(/[^a-zA-Z0-9]/g, '_')
266
+ publishAnalyticsEvent({
267
+ vizType: config.type,
268
+ vizSubType: getVizSubType(config),
269
+ eventType: `map_hover`,
270
+ eventAction: 'hover',
271
+ eventLabel: interactionLabel,
272
+ vizTitle: getVizTitle(config),
273
+ location: geoDisplayName,
274
+ specifics: `location: ${locationName?.toLowerCase()}`
275
+ })
276
+ }}
172
277
  data-tooltip-id={`tooltip__${tooltipId}`}
173
278
  data-tooltip-html={toolTip}
174
279
  />
@@ -190,7 +295,7 @@ const WorldMap = () => {
190
295
  <ErrorBoundary component='WorldMap'>
191
296
  {allowMapZoom ? (
192
297
  <svg viewBox={SVG_VIEWBOX} role='img' aria-label={handleMapAriaLabels(config)}>
193
- <rect height={SVG_HEIGHT} width={SVG_WIDTH} onClick={handleReset} fill='white' />
298
+ <rect height={SVG_HEIGHT} width={SVG_WIDTH} onClick={handleFiltersReset} fill='white' />
194
299
  <ZoomableGroup
195
300
  zoom={position.zoom}
196
301
  center={position.coordinates}
@@ -219,7 +324,7 @@ const WorldMap = () => {
219
324
  </svg>
220
325
  )}
221
326
  {(type === 'data' || (type === 'world-geocode' && allowMapZoom) || (type === 'bubble' && allowMapZoom)) && (
222
- <ZoomControls handleZoomIn={handleZoomIn} handleZoomOut={handleZoomOut} handleReset={handleReset} />
327
+ <ZoomControls handleZoomIn={handleZoomIn} handleZoomOut={handleZoomOut} handleZoomReset={handleZoomReset} />
223
328
  )}
224
329
  </ErrorBoundary>
225
330
  )
@@ -7,10 +7,10 @@ import './zoomControls.styles.css'
7
7
  type ZoomControlsProps = {
8
8
  handleZoomIn: (coordinates: [Number, Number]) => void
9
9
  handleZoomOut: (coordinates: [Number, Number]) => void
10
- handleReset: (setRuntimeData: Function) => void
10
+ handleZoomReset: (setRuntimeData: Function) => void
11
11
  }
12
12
 
13
- const ZoomControls: React.FC<ZoomControlsProps> = ({ handleZoomIn, handleZoomOut, handleReset }) => {
13
+ const ZoomControls: React.FC<ZoomControlsProps> = ({ handleZoomIn, handleZoomOut, handleZoomReset }) => {
14
14
  const { config, setRuntimeData, position } = useContext<MapContext>(ConfigContext)
15
15
  if (!config.general.allowMapZoom) return
16
16
  return (
@@ -26,13 +26,10 @@ const ZoomControls: React.FC<ZoomControlsProps> = ({ handleZoomIn, handleZoomOut
26
26
  <line x1='5' y1='12' x2='19' y2='12' />
27
27
  </svg>
28
28
  </button>
29
- {config.general.type === 'bubble' && (
30
- <button onClick={() => handleReset(setRuntimeData)} className='reset' aria-label='Reset Zoom and Map Filters'>
31
- Reset Filters
32
- </button>
33
- )}
34
- {(config.general.type === 'world-geocode' || config.general.geoType === 'single-state') && (
35
- <button onClick={() => handleReset(setRuntimeData)} className='reset' aria-label='Reset Zoom'>
29
+ {(config.general.type === 'world-geocode' ||
30
+ config.general.geoType === 'single-state' ||
31
+ config.general.type === 'bubble') && (
32
+ <button onClick={() => handleZoomReset(setRuntimeData)} className='reset' aria-label='Reset Zoom'>
36
33
  Reset Zoom
37
34
  </button>
38
35
  )}
@@ -0,0 +1,30 @@
1
+ import React, { createContext, useContext } from 'react'
2
+
3
+ interface LegendMemoContextType {
4
+ legendMemo: React.RefObject<Map<any, any>>
5
+ legendSpecialClassLastMemo: React.RefObject<Map<any, any>>
6
+ }
7
+
8
+ const LegendMemoContext = createContext<LegendMemoContextType | null>(null)
9
+
10
+ export const LegendMemoProvider: React.FC<{
11
+ children: React.ReactNode
12
+ legendMemo: React.RefObject<Map<any, any>>
13
+ legendSpecialClassLastMemo: React.RefObject<Map<any, any>>
14
+ }> = ({ children, legendMemo, legendSpecialClassLastMemo }) => {
15
+ return (
16
+ <LegendMemoContext.Provider value={{ legendMemo, legendSpecialClassLastMemo }}>
17
+ {children}
18
+ </LegendMemoContext.Provider>
19
+ )
20
+ }
21
+
22
+ export const useLegendMemoContext = () => {
23
+ const context = useContext(LegendMemoContext)
24
+ if (!context) {
25
+ throw new Error('useLegendMemoContext must be used within a LegendMemoProvider')
26
+ }
27
+ return context
28
+ }
29
+
30
+ export default LegendMemoContext
package/src/context.ts CHANGED
@@ -1,45 +1,7 @@
1
1
  import { createContext, Dispatch } from 'react'
2
- import { MapConfig } from './types/MapConfig'
2
+ import { MapContext } from './types/MapContext'
3
3
  import MapActions from './store/map.actions'
4
4
 
5
- type MapContext = {
6
- container
7
- setSharedFilter
8
- customNavigationHandler
9
- tooltipRef
10
- containerEl
11
- applyLegendToRow
12
- data
13
- displayGeoName
14
- filteredCountryCode
15
- generateColorsArray
16
- generateRuntimeData
17
- geoClickHandler
18
- handleCircleClick: Function
19
- innerContainerRef
20
- isDashboard
21
- isEditor
22
- mapId: string
23
- loadConfig
24
- position
25
- resetLegendToggles
26
- runtimeFilters
27
- runtimeLegend
28
- setParentConfig
29
- setRuntimeData
30
- setRuntimeFilters
31
- setRuntimeLegend
32
- setSharedFilterValue
33
- setConfig: Function
34
- config: MapConfig
35
- tooltipId: string
36
- legendMemo
37
- legendSpecialClassLastMemo
38
- translate
39
- scale
40
- annotations
41
- }
42
-
43
5
  export const MapDispatchContext = createContext<Dispatch<MapActions>>(() => {})
44
6
 
45
7
  const ConfigContext = createContext({} as MapContext)