@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.
- package/.claude/settings.local.json +30 -0
- package/CLAUDE.local.md +0 -0
- package/dist/cdcmap.js +54785 -53159
- package/examples/private/c.json +290 -0
- package/examples/private/canvas-city-hover.json +787 -0
- package/examples/private/d.json +345 -0
- package/examples/private/filter-map.json +909 -0
- package/examples/private/g.json +1 -0
- package/examples/private/h.json +105911 -0
- package/examples/private/measles-data.json +378 -0
- package/examples/private/measles.json +211 -0
- package/examples/private/north-dakota.json +1132 -0
- package/examples/private/rsv-data.json +532 -0
- package/examples/private/state-with-pattern.json +883 -0
- package/examples/private/test.json +222 -640
- package/index.html +1 -1
- package/package.json +26 -5
- package/src/CdcMap.tsx +28 -8
- package/src/CdcMapComponent.tsx +230 -306
- package/src/_stories/CdcMap.Filters.stories.tsx +2 -2
- package/src/_stories/CdcMap.Legend.Gradient.stories.tsx +3 -3
- package/src/_stories/CdcMap.Legend.stories.tsx +7 -4
- package/src/_stories/CdcMap.Patterns.stories.tsx +2 -2
- package/src/_stories/CdcMap.Table.stories.tsx +2 -2
- package/src/_stories/CdcMap.stories.tsx +18 -11
- package/src/_stories/GoogleMap.stories.tsx +2 -2
- package/src/_stories/UsaMap.NoData.stories.tsx +2 -2
- package/src/_stories/_mock/equal-number.json +1109 -0
- package/src/_stories/_mock/multi-state.json +21389 -0
- package/src/_stories/_mock/us-bubble-cities.json +306 -0
- package/src/components/BubbleList.tsx +16 -12
- package/src/components/CityList.tsx +88 -110
- package/src/components/DataTable.tsx +44 -12
- package/src/components/EditorPanel/components/EditorPanel.tsx +201 -203
- package/src/components/EditorPanel/components/HexShapeSettings.tsx +3 -2
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +7 -5
- package/src/components/Geo.tsx +2 -0
- package/src/components/Legend/components/Legend.tsx +117 -93
- package/src/components/Legend/components/LegendGroup/Legend.Group.tsx +10 -7
- package/src/components/MapContainer.tsx +52 -0
- package/src/components/MapControls.tsx +44 -0
- package/src/components/Modal.tsx +2 -8
- package/src/components/NavigationMenu.tsx +13 -1
- package/src/components/UsaMap/components/SingleState/SingleState.CountyOutput.tsx +24 -7
- package/src/components/UsaMap/components/SingleState/SingleState.StateOutput.tsx +21 -15
- package/src/components/UsaMap/components/TerritoriesSection.tsx +2 -2
- package/src/components/UsaMap/components/UsaMap.County.tsx +112 -33
- package/src/components/UsaMap/components/UsaMap.Region.tsx +23 -5
- package/src/components/UsaMap/components/UsaMap.SingleState.tsx +38 -26
- package/src/components/UsaMap/components/UsaMap.State.tsx +28 -10
- package/src/components/UsaMap/helpers/map.ts +16 -8
- package/src/components/WorldMap/WorldMap.tsx +116 -11
- package/src/components/ZoomControls.tsx +6 -9
- package/src/context/LegendMemoContext.tsx +30 -0
- package/src/context.ts +1 -39
- package/src/data/initial-state.js +143 -128
- package/src/data/supported-geos.js +202 -4
- package/src/helpers/addUIDs.ts +8 -8
- package/src/helpers/applyColorToLegend.ts +122 -45
- package/src/helpers/applyLegendToRow.ts +15 -13
- package/src/helpers/componentHelpers.ts +8 -0
- package/src/helpers/constants.ts +12 -0
- package/src/helpers/dataTableHelpers.ts +6 -0
- package/src/helpers/displayGeoName.ts +12 -7
- package/src/helpers/formatLegendLocation.ts +1 -3
- package/src/helpers/generateRuntimeLegend.ts +192 -340
- package/src/helpers/generateRuntimeLegendHash.ts +4 -2
- package/src/helpers/getColumnNames.ts +1 -1
- package/src/helpers/getPatternForRow.ts +36 -0
- package/src/helpers/getStatesPicked.ts +14 -0
- package/src/helpers/handleMapAriaLabels.ts +2 -2
- package/src/helpers/index.ts +11 -3
- package/src/helpers/isLegendItemDisabled.ts +16 -0
- package/src/helpers/mapObserverHelpers.ts +40 -0
- package/src/helpers/resetLegendToggles.ts +3 -2
- package/src/helpers/toggleLegendActive.ts +6 -11
- package/src/helpers/urlDataHelpers.ts +70 -0
- package/src/hooks/useGeoClickHandler.ts +35 -1
- package/src/hooks/useLegendMemo.ts +17 -0
- package/src/hooks/useMapLayers.tsx +5 -4
- package/src/hooks/useStateZoom.tsx +137 -88
- package/src/hooks/useTooltip.ts +1 -2
- package/src/index.jsx +6 -3
- package/src/scss/main.scss +23 -12
- package/src/store/map.actions.ts +2 -2
- package/src/store/map.reducer.ts +21 -10
- package/src/test/CdcMap.test.jsx +11 -0
- package/src/types/MapConfig.ts +25 -17
- package/src/types/MapContext.ts +2 -8
- package/src/types/runtimeLegend.ts +12 -10
- package/vite.config.js +2 -7
- package/vitest.config.ts +16 -0
- package/src/_stories/_mock/floating-point.json +0 -427
- package/src/coreStyles_map.scss +0 -3
- package/src/helpers/colorDistributions.ts +0 -12
- package/src/helpers/generateColorsArray.ts +0 -14
- package/src/helpers/getStatePicked.ts +0 -8
- package/src/helpers/tests/generateColorsArray.test.ts +0 -18
- 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 {
|
|
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,
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
76
|
-
if (!
|
|
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
|
-
{
|
|
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 =>
|
|
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
|
-
|
|
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
|
-
{
|
|
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 =>
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =>
|
|
160
|
+
const territoriesList = territoriesKeys.filter(key => runtimeData?.[key])
|
|
157
161
|
setTerritoriesData(territoriesList)
|
|
158
162
|
}
|
|
159
|
-
}, [
|
|
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 =
|
|
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 =
|
|
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
|
|
100
|
-
if (!state.general.
|
|
101
|
-
|
|
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
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
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
|
-
|
|
39
|
+
runtimeData,
|
|
37
40
|
position,
|
|
38
|
-
setRuntimeData,
|
|
39
41
|
config,
|
|
40
42
|
tooltipId,
|
|
41
43
|
runtimeLegend,
|
|
42
|
-
|
|
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
|
|
69
|
+
const handleFiltersReset = () => {
|
|
67
70
|
const newRuntimeData = generateRuntimeData(config)
|
|
68
|
-
|
|
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
|
-
|
|
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 &&
|
|
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 =
|
|
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={
|
|
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}
|
|
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
|
-
|
|
10
|
+
handleZoomReset: (setRuntimeData: Function) => void
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
const ZoomControls: React.FC<ZoomControlsProps> = ({ handleZoomIn, handleZoomOut,
|
|
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 === '
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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 {
|
|
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)
|