@cdc/map 4.25.3 → 4.25.6
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/.idea/map.iml +12 -0
- package/.idea/modules.xml +8 -0
- package/.idea/vcs.xml +6 -0
- package/dist/cdcmap.js +31254 -32242
- package/examples/hex-colors.json +3 -3
- package/examples/m2.json +32904 -0
- package/examples/private/test.json +470 -1457
- package/examples/private/{mmr.json → wastewatermap.json} +86 -115
- package/index.html +36 -63
- package/package.json +7 -19
- package/src/CdcMap.tsx +56 -1552
- package/src/CdcMapComponent.tsx +608 -0
- package/src/_stories/CdcMap.Legend.Gradient.stories.tsx +10 -0
- package/src/_stories/CdcMap.Legend.stories.tsx +67 -0
- package/src/_stories/CdcMap.Table.stories.tsx +19 -0
- package/src/_stories/CdcMap.stories.tsx +12 -1
- package/src/_stories/UsaMap.NoData.stories.tsx +4 -4
- package/src/_stories/_mock/default-patterns.json +8 -5
- package/src/_stories/_mock/legend-bins.json +428 -0
- package/{examples/private/default-patterns.json → src/_stories/_mock/legends/legend-tests.json} +36 -131
- package/src/cdcMapComponent.styles.css +9 -0
- package/src/components/Annotation/Annotation.Draggable.tsx +27 -26
- package/src/components/Annotation/AnnotationDropdown.tsx +5 -6
- package/src/components/BubbleList.tsx +135 -49
- package/src/components/CityList.tsx +89 -87
- package/src/components/DataTable.tsx +8 -8
- package/src/components/EditorPanel/components/EditorPanel.tsx +823 -885
- package/src/components/EditorPanel/components/Error.tsx +9 -2
- package/src/components/EditorPanel/components/HexShapeSettings.tsx +127 -141
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +55 -86
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +89 -75
- package/src/components/EditorPanel/components/editorPanel.styles.css +95 -0
- package/src/components/Geo.tsx +9 -1
- package/src/components/GoogleMap/components/GoogleMap.tsx +1 -1
- package/src/components/Legend/components/Legend.tsx +92 -87
- package/src/components/Legend/components/LegendGroup/Legend.Group.tsx +128 -0
- package/src/components/Legend/components/LegendGroup/legend.group.css +27 -0
- package/src/components/Legend/components/LegendItem.Hex.tsx +4 -1
- package/src/components/Legend/components/index.scss +74 -17
- package/src/components/Modal.tsx +17 -7
- package/src/components/NavigationMenu.tsx +11 -9
- package/src/components/UsaMap/components/SingleState/SingleState.CountyOutput.tsx +12 -8
- package/src/components/UsaMap/components/SingleState/SingleState.StateOutput.tsx +4 -4
- package/src/components/UsaMap/components/TerritoriesSection.tsx +33 -10
- package/src/components/UsaMap/components/Territory/Territory.Hexagon.tsx +12 -10
- package/src/components/UsaMap/components/Territory/Territory.Rectangle.tsx +12 -14
- package/src/components/UsaMap/components/Territory/TerritoryShape.ts +2 -1
- package/src/components/UsaMap/components/UsaMap.County.tsx +138 -96
- package/src/components/UsaMap/components/UsaMap.Region.styles.css +72 -0
- package/src/components/UsaMap/components/UsaMap.Region.tsx +56 -103
- package/src/components/UsaMap/components/UsaMap.SingleState.styles.css +10 -0
- package/src/components/UsaMap/components/UsaMap.SingleState.tsx +65 -74
- package/src/components/UsaMap/components/UsaMap.State.tsx +112 -91
- package/src/components/UsaMap/helpers/map.ts +1 -1
- package/src/components/UsaMap/helpers/shapes.ts +20 -7
- package/src/components/WorldMap/WorldMap.tsx +64 -118
- package/src/components/WorldMap/worldMap.styles.css +28 -0
- package/src/components/ZoomControls.tsx +15 -13
- package/src/components/zoomControls.styles.css +53 -0
- package/src/context.ts +17 -9
- package/src/data/initial-state.js +5 -2
- package/src/helpers/addUIDs.ts +150 -0
- package/src/helpers/applyColorToLegend.ts +39 -64
- package/src/helpers/applyLegendToRow.ts +51 -0
- package/src/helpers/colorDistributions.ts +12 -0
- package/src/helpers/constants.ts +44 -0
- package/src/helpers/displayGeoName.ts +9 -2
- package/src/helpers/formatLegendLocation.ts +3 -2
- package/src/helpers/generateColorsArray.ts +2 -1
- package/src/helpers/generateRuntimeData.ts +78 -0
- package/src/helpers/generateRuntimeFilters.ts +63 -0
- package/src/helpers/generateRuntimeLegend.ts +566 -0
- package/src/helpers/generateRuntimeLegendHash.ts +16 -15
- package/src/helpers/getColumnNames.ts +19 -0
- package/src/helpers/getMapContainerClasses.ts +23 -0
- package/src/helpers/getStatePicked.ts +8 -0
- package/src/helpers/handleMapTabbing.ts +31 -0
- package/src/helpers/hashObj.ts +1 -1
- package/src/helpers/index.ts +22 -0
- package/src/helpers/navigationHandler.ts +3 -3
- package/src/helpers/resetLegendToggles.ts +13 -0
- package/src/helpers/setBinNumbers.ts +5 -0
- package/src/helpers/sortSpecialClassesLast.ts +7 -0
- package/src/helpers/tests/getColumnNames.test.ts +52 -0
- package/src/helpers/titleCase.ts +1 -1
- package/src/helpers/toggleLegendActive.ts +25 -0
- package/src/hooks/useApplyTooltipsToGeo.tsx +51 -0
- package/src/hooks/useColumnsRequiredChecker.ts +51 -0
- package/src/hooks/useGeoClickHandler.ts +45 -0
- package/src/hooks/useLegendSeparators.ts +26 -0
- package/src/hooks/useMapLayers.tsx +34 -60
- package/src/hooks/useModal.ts +22 -0
- package/src/hooks/useResizeObserver.ts +4 -5
- package/src/hooks/useStateZoom.tsx +52 -75
- package/src/hooks/useTooltip.ts +2 -3
- package/src/index.jsx +3 -9
- package/src/scss/editor-panel.scss +3 -99
- package/src/scss/main.scss +1 -19
- package/src/scss/map.scss +15 -220
- package/src/store/map.actions.ts +46 -0
- package/src/store/map.reducer.ts +96 -0
- package/src/types/Annotations.ts +24 -0
- package/src/types/MapConfig.ts +23 -3
- package/src/types/MapContext.ts +36 -35
- package/src/types/Modal.ts +1 -0
- package/src/types/RuntimeData.ts +3 -0
- package/examples/private/DEV-9644.json +0 -184
- package/examples/private/DEV-9989.json +0 -229
- package/examples/private/ardi.json +0 -180
- package/examples/private/colors 2.json +0 -416
- package/examples/private/colors.json +0 -416
- package/examples/private/colors.json.zip +0 -0
- package/examples/private/customColors.json +0 -45348
- package/examples/test.json +0 -183
- package/src/helpers/closeModal.ts +0 -9
- package/src/scss/btn.scss +0 -69
- package/src/scss/filters.scss +0 -27
- package/src/scss/variables.scss +0 -1
- /package/src/hooks/{useActiveElement.js → useActiveElement.ts} +0 -0
|
@@ -1,42 +1,54 @@
|
|
|
1
1
|
import { memo, useContext, useState, useEffect } from 'react'
|
|
2
|
-
|
|
3
|
-
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
4
2
|
import { geoMercator } from 'd3-geo'
|
|
5
3
|
import { Mercator } from '@visx/geo'
|
|
6
4
|
import { feature } from 'topojson-client'
|
|
5
|
+
import ConfigContext, { MapDispatchContext } from '../../context'
|
|
6
|
+
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
7
7
|
import ZoomableGroup from '../ZoomableGroup'
|
|
8
8
|
import Geo from '../Geo'
|
|
9
9
|
import CityList from '../CityList'
|
|
10
10
|
import BubbleList from '../BubbleList'
|
|
11
|
-
import ConfigContext from '../../context'
|
|
12
11
|
import ZoomControls from '../ZoomControls'
|
|
13
|
-
import { getGeoFillColor, getGeoStrokeColor } from '../../helpers/colors'
|
|
14
12
|
import { supportedCountries } from '../../data/supported-geos'
|
|
15
|
-
import {
|
|
16
|
-
|
|
13
|
+
import {
|
|
14
|
+
getGeoFillColor,
|
|
15
|
+
getGeoStrokeColor,
|
|
16
|
+
handleMapAriaLabels,
|
|
17
|
+
titleCase,
|
|
18
|
+
displayGeoName,
|
|
19
|
+
SVG_VIEWBOX,
|
|
20
|
+
SVG_WIDTH,
|
|
21
|
+
SVG_HEIGHT,
|
|
22
|
+
MAX_ZOOM_LEVEL
|
|
23
|
+
} from '../../helpers'
|
|
24
|
+
import useGeoClickHandler from '../../hooks/useGeoClickHandler'
|
|
25
|
+
import useApplyTooltipsToGeo from '../../hooks/useApplyTooltipsToGeo'
|
|
26
|
+
import generateRuntimeData from '../../helpers/generateRuntimeData'
|
|
27
|
+
import { applyLegendToRow } from '../../helpers/applyLegendToRow'
|
|
28
|
+
|
|
29
|
+
import './worldMap.styles.css'
|
|
17
30
|
|
|
18
31
|
let projection = geoMercator()
|
|
19
32
|
|
|
20
33
|
const WorldMap = () => {
|
|
21
34
|
// prettier-ignore
|
|
22
35
|
const {
|
|
23
|
-
applyLegendToRow,
|
|
24
|
-
applyTooltipsToGeo,
|
|
25
36
|
data,
|
|
26
|
-
displayGeoName,
|
|
27
|
-
generateRuntimeData,
|
|
28
|
-
geoClickHandler,
|
|
29
|
-
hasZoom,
|
|
30
37
|
position,
|
|
31
|
-
setFilteredCountryCode,
|
|
32
|
-
setPosition,
|
|
33
38
|
setRuntimeData,
|
|
34
|
-
|
|
35
|
-
state,
|
|
39
|
+
config,
|
|
36
40
|
tooltipId,
|
|
41
|
+
runtimeLegend,
|
|
42
|
+
legendMemo,
|
|
43
|
+
legendSpecialClassLastMemo,
|
|
37
44
|
} = useContext(ConfigContext)
|
|
38
45
|
|
|
46
|
+
const { type, allowMapZoom } = config.general
|
|
47
|
+
|
|
39
48
|
const [world, setWorld] = useState(null)
|
|
49
|
+
const { geoClickHandler } = useGeoClickHandler()
|
|
50
|
+
const { applyTooltipsToGeo } = useApplyTooltipsToGeo()
|
|
51
|
+
const dispatch = useContext(MapDispatchContext)
|
|
40
52
|
|
|
41
53
|
useEffect(() => {
|
|
42
54
|
const fetchData = async () => {
|
|
@@ -51,43 +63,30 @@ const WorldMap = () => {
|
|
|
51
63
|
return <></>
|
|
52
64
|
}
|
|
53
65
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
setRuntimeData(
|
|
59
|
-
setState({
|
|
60
|
-
...state,
|
|
61
|
-
focusedCountry: false,
|
|
62
|
-
mapPosition: { coordinates: [0, 30], zoom: 1 }
|
|
63
|
-
})
|
|
64
|
-
setFilteredCountryCode('')
|
|
66
|
+
const handleReset = () => {
|
|
67
|
+
const newRuntimeData = generateRuntimeData(config)
|
|
68
|
+
dispatch({ type: 'SET_POSITION', payload: { coordinates: [0, 30], zoom: 1 } })
|
|
69
|
+
dispatch({ type: 'SET_FILTERED_COUNTRY_CODE', payload: '' })
|
|
70
|
+
setRuntimeData(newRuntimeData)
|
|
65
71
|
}
|
|
66
|
-
const handleZoomIn =
|
|
72
|
+
const handleZoomIn = position => {
|
|
67
73
|
if (position.zoom >= 4) return
|
|
68
|
-
|
|
74
|
+
dispatch({ type: 'SET_POSITION', payload: { coordinates: position.coordinates, zoom: position.zoom * 1.5 } })
|
|
69
75
|
}
|
|
70
76
|
|
|
71
|
-
const handleZoomOut =
|
|
77
|
+
const handleZoomOut = position => {
|
|
72
78
|
if (position.zoom <= 1) return
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// TODO Refactor - state should be set together here to avoid rerenders
|
|
77
|
-
const handleCircleClick = (country, state, setState, setRuntimeData, generateRuntimeData) => {
|
|
78
|
-
if (!state.general.allowMapZoom) return
|
|
79
|
-
let newRuntimeData = state.data.filter(item => item[state.columns.geo.name] === country[state.columns.geo.name])
|
|
80
|
-
setFilteredCountryCode(newRuntimeData[0].uid)
|
|
79
|
+
dispatch({ type: 'SET_POSITION', payload: { coordinates: position.coordinates, zoom: position.zoom / 1.5 } })
|
|
81
80
|
}
|
|
82
81
|
|
|
83
82
|
const handleMoveEnd = position => {
|
|
84
|
-
|
|
83
|
+
dispatch({ type: 'SET_POSITION', payload: position })
|
|
85
84
|
}
|
|
86
85
|
|
|
87
86
|
const constructGeoJsx = geographies => {
|
|
88
87
|
const geosJsx = geographies.map(({ feature: geo, path }, i) => {
|
|
89
|
-
// If the geo.properties.
|
|
90
|
-
const dataHasStateName =
|
|
88
|
+
// If the geo.properties.config value is found in the data use that, otherwise fall back to geo.properties.iso
|
|
89
|
+
const dataHasStateName = config.data.some(d => d[config.columns.geo.name] === geo.properties.state)
|
|
91
90
|
const geoKey =
|
|
92
91
|
geo.properties.state && data[geo.properties.state]
|
|
93
92
|
? geo.properties.state
|
|
@@ -102,20 +101,16 @@ const WorldMap = () => {
|
|
|
102
101
|
|
|
103
102
|
let geoData = data[geoKey]
|
|
104
103
|
|
|
105
|
-
// if ((geoKey === 'Alaska' || geoKey === 'Hawaii') && !geoData) {
|
|
106
|
-
// geoData = data['United States']
|
|
107
|
-
// }
|
|
108
|
-
|
|
109
104
|
const geoDisplayName = displayGeoName(supportedCountries[geoKey]?.[0])
|
|
110
105
|
let legendColors
|
|
111
106
|
|
|
112
107
|
// Once we receive data for this geographic item, setup variables.
|
|
113
108
|
if (geoData !== undefined) {
|
|
114
|
-
legendColors = applyLegendToRow(geoData)
|
|
109
|
+
legendColors = applyLegendToRow(geoData, config, runtimeLegend, legendMemo, legendSpecialClassLastMemo)
|
|
115
110
|
}
|
|
116
111
|
|
|
117
|
-
const geoStrokeColor = getGeoStrokeColor(
|
|
118
|
-
const geoFillColor = getGeoFillColor(
|
|
112
|
+
const geoStrokeColor = getGeoStrokeColor(config)
|
|
113
|
+
const geoFillColor = getGeoFillColor(config)
|
|
119
114
|
|
|
120
115
|
let styles: Record<string, string | Record<string, string>> = {
|
|
121
116
|
fill: geoFillColor,
|
|
@@ -126,23 +121,23 @@ const WorldMap = () => {
|
|
|
126
121
|
|
|
127
122
|
// If a legend applies, return it with appropriate information.
|
|
128
123
|
const toolTip = applyTooltipsToGeo(geoDisplayName, geoData)
|
|
129
|
-
if (legendColors && legendColors[0] !== '#000000' &&
|
|
124
|
+
if (legendColors && legendColors[0] !== '#000000' && type !== 'bubble') {
|
|
130
125
|
styles = {
|
|
131
126
|
...styles,
|
|
132
|
-
fill:
|
|
127
|
+
fill: type !== 'world-geocode' ? legendColors[0] : geoFillColor,
|
|
133
128
|
cursor: 'default',
|
|
134
129
|
'&:hover': {
|
|
135
|
-
fill:
|
|
130
|
+
fill: type !== 'world-geocode' ? legendColors[1] : geoFillColor
|
|
136
131
|
},
|
|
137
132
|
'&:active': {
|
|
138
|
-
fill:
|
|
133
|
+
fill: type !== 'world-geocode' ? legendColors[2] : geoFillColor
|
|
139
134
|
}
|
|
140
135
|
}
|
|
141
136
|
|
|
142
137
|
// When to add pointer cursor
|
|
143
138
|
if (
|
|
144
|
-
(
|
|
145
|
-
|
|
139
|
+
(config.columns.navigate && geoData[config.columns.navigate.name]) ||
|
|
140
|
+
config.tooltips.appearanceType === 'click'
|
|
146
141
|
) {
|
|
147
142
|
styles.cursor = 'pointer'
|
|
148
143
|
}
|
|
@@ -151,9 +146,8 @@ const WorldMap = () => {
|
|
|
151
146
|
<Geo
|
|
152
147
|
additionalData={additionalData}
|
|
153
148
|
geoData={geoData}
|
|
154
|
-
state={state}
|
|
155
149
|
key={i + '-geo'}
|
|
156
|
-
|
|
150
|
+
styles={styles}
|
|
157
151
|
path={path}
|
|
158
152
|
stroke={geoStrokeColor}
|
|
159
153
|
strokeWidth={strokeWidth}
|
|
@@ -170,11 +164,9 @@ const WorldMap = () => {
|
|
|
170
164
|
<Geo
|
|
171
165
|
additionaldata={JSON.stringify(additionalData)}
|
|
172
166
|
geodata={JSON.stringify(geoData)}
|
|
173
|
-
state={state}
|
|
174
167
|
key={i + '-geo'}
|
|
175
168
|
stroke={geoStrokeColor}
|
|
176
169
|
strokeWidth={strokeWidth}
|
|
177
|
-
style={styles}
|
|
178
170
|
styles={styles}
|
|
179
171
|
path={path}
|
|
180
172
|
data-tooltip-id={`tooltip__${tooltipId}`}
|
|
@@ -184,39 +176,11 @@ const WorldMap = () => {
|
|
|
184
176
|
})
|
|
185
177
|
|
|
186
178
|
// Cities
|
|
187
|
-
geosJsx.push(
|
|
188
|
-
<CityList
|
|
189
|
-
applyLegendToRow={applyLegendToRow}
|
|
190
|
-
applyTooltipsToGeo={applyTooltipsToGeo}
|
|
191
|
-
data={data}
|
|
192
|
-
displayGeoName={displayGeoName}
|
|
193
|
-
geoClickHandler={geoClickHandler}
|
|
194
|
-
key='cities'
|
|
195
|
-
projection={projection}
|
|
196
|
-
state={state}
|
|
197
|
-
titleCase={titleCase}
|
|
198
|
-
tooltipId={tooltipId}
|
|
199
|
-
/>
|
|
200
|
-
)
|
|
179
|
+
geosJsx.push(<CityList key='cities' projection={projection} tooltipId={tooltipId} />)
|
|
201
180
|
|
|
202
181
|
// Bubbles
|
|
203
|
-
if (
|
|
204
|
-
geosJsx.push(
|
|
205
|
-
<BubbleList
|
|
206
|
-
key='bubbles'
|
|
207
|
-
data={state.data}
|
|
208
|
-
runtimeData={data}
|
|
209
|
-
state={state}
|
|
210
|
-
projection={projection}
|
|
211
|
-
applyLegendToRow={applyLegendToRow}
|
|
212
|
-
applyTooltipsToGeo={applyTooltipsToGeo}
|
|
213
|
-
displayGeoName={displayGeoName}
|
|
214
|
-
tooltipId={tooltipId}
|
|
215
|
-
handleCircleClick={country =>
|
|
216
|
-
handleCircleClick(country, state, setState, setRuntimeData, generateRuntimeData)
|
|
217
|
-
}
|
|
218
|
-
/>
|
|
219
|
-
)
|
|
182
|
+
if (type === 'bubble') {
|
|
183
|
+
geosJsx.push(<BubbleList />)
|
|
220
184
|
}
|
|
221
185
|
|
|
222
186
|
return geosJsx
|
|
@@ -224,56 +188,38 @@ const WorldMap = () => {
|
|
|
224
188
|
|
|
225
189
|
return (
|
|
226
190
|
<ErrorBoundary component='WorldMap'>
|
|
227
|
-
{
|
|
228
|
-
<svg viewBox=
|
|
229
|
-
<rect
|
|
230
|
-
height={500}
|
|
231
|
-
width={880}
|
|
232
|
-
onClick={() => handleReset(state, setState, setRuntimeData, generateRuntimeData)}
|
|
233
|
-
fill='white'
|
|
234
|
-
/>
|
|
191
|
+
{allowMapZoom ? (
|
|
192
|
+
<svg viewBox={SVG_VIEWBOX} role='img' aria-label={handleMapAriaLabels(config)}>
|
|
193
|
+
<rect height={SVG_HEIGHT} width={SVG_WIDTH} onClick={handleReset} fill='white' />
|
|
235
194
|
<ZoomableGroup
|
|
236
195
|
zoom={position.zoom}
|
|
237
196
|
center={position.coordinates}
|
|
238
197
|
onMoveEnd={handleMoveEnd}
|
|
239
|
-
maxZoom={
|
|
198
|
+
maxZoom={MAX_ZOOM_LEVEL}
|
|
240
199
|
projection={projection}
|
|
241
|
-
width={
|
|
242
|
-
height={
|
|
200
|
+
width={SVG_WIDTH}
|
|
201
|
+
height={SVG_HEIGHT}
|
|
243
202
|
>
|
|
244
203
|
<Mercator data={world}>{({ features }) => constructGeoJsx(features)}</Mercator>
|
|
245
204
|
</ZoomableGroup>
|
|
246
205
|
</svg>
|
|
247
206
|
) : (
|
|
248
|
-
<svg viewBox=
|
|
207
|
+
<svg viewBox={SVG_VIEWBOX}>
|
|
249
208
|
<ZoomableGroup
|
|
250
209
|
zoom={1}
|
|
251
210
|
center={position.coordinates}
|
|
252
211
|
onMoveEnd={handleMoveEnd}
|
|
253
212
|
maxZoom={0}
|
|
254
213
|
projection={projection}
|
|
255
|
-
width={
|
|
256
|
-
height={
|
|
214
|
+
width={SVG_WIDTH}
|
|
215
|
+
height={SVG_HEIGHT}
|
|
257
216
|
>
|
|
258
217
|
<Mercator data={world}>{({ features }) => constructGeoJsx(features)}</Mercator>
|
|
259
218
|
</ZoomableGroup>
|
|
260
219
|
</svg>
|
|
261
220
|
)}
|
|
262
|
-
{(
|
|
263
|
-
|
|
264
|
-
(state.general.type === 'bubble' && hasZoom)) && (
|
|
265
|
-
<ZoomControls
|
|
266
|
-
// prettier-ignore
|
|
267
|
-
generateRuntimeData={generateRuntimeData}
|
|
268
|
-
handleZoomIn={handleZoomIn}
|
|
269
|
-
handleZoomOut={handleZoomOut}
|
|
270
|
-
position={position}
|
|
271
|
-
setPosition={setPosition}
|
|
272
|
-
setRuntimeData={setRuntimeData}
|
|
273
|
-
setState={setState}
|
|
274
|
-
state={state}
|
|
275
|
-
handleReset={handleReset}
|
|
276
|
-
/>
|
|
221
|
+
{(type === 'data' || (type === 'world-geocode' && allowMapZoom) || (type === 'bubble' && allowMapZoom)) && (
|
|
222
|
+
<ZoomControls handleZoomIn={handleZoomIn} handleZoomOut={handleZoomOut} handleReset={handleReset} />
|
|
277
223
|
)}
|
|
278
224
|
</ErrorBoundary>
|
|
279
225
|
)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
.world {
|
|
2
|
+
&.data .geography-container {
|
|
3
|
+
border-bottom: var(--lightGray) 1px solid;
|
|
4
|
+
}
|
|
5
|
+
.geography-container {
|
|
6
|
+
cursor: move;
|
|
7
|
+
position: relative;
|
|
8
|
+
flex-grow: 1;
|
|
9
|
+
width: 100%;
|
|
10
|
+
.geo-point {
|
|
11
|
+
transition: 0.3s all;
|
|
12
|
+
circle {
|
|
13
|
+
fill: inherit;
|
|
14
|
+
transition: 0.1s transform;
|
|
15
|
+
}
|
|
16
|
+
&:hover {
|
|
17
|
+
transition: 0.2s all;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
.map-logo {
|
|
21
|
+
display: block;
|
|
22
|
+
margin: 0 0 0 auto;
|
|
23
|
+
max-height: 35px;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
@@ -1,36 +1,38 @@
|
|
|
1
1
|
import React, { useContext } from 'react'
|
|
2
|
-
import { MapConfig } from '../types/MapConfig'
|
|
3
2
|
import ConfigContext from '../context'
|
|
3
|
+
import { type MapConfig } from '../types/MapConfig'
|
|
4
|
+
import { MapContext } from '../types/MapContext'
|
|
5
|
+
import './zoomControls.styles.css'
|
|
4
6
|
|
|
5
7
|
type ZoomControlsProps = {
|
|
6
|
-
handleZoomIn: (coordinates: [Number, Number]
|
|
7
|
-
handleZoomOut: (coordinates: [Number, Number]
|
|
8
|
-
handleReset: (
|
|
8
|
+
handleZoomIn: (coordinates: [Number, Number]) => void
|
|
9
|
+
handleZoomOut: (coordinates: [Number, Number]) => void
|
|
10
|
+
handleReset: (setRuntimeData: Function) => void
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
const ZoomControls: React.FC<ZoomControlsProps> = ({ handleZoomIn, handleZoomOut, handleReset }) => {
|
|
12
|
-
const {
|
|
13
|
-
if (!
|
|
14
|
+
const { config, setRuntimeData, position } = useContext<MapContext>(ConfigContext)
|
|
15
|
+
if (!config.general.allowMapZoom) return
|
|
14
16
|
return (
|
|
15
|
-
<div className='zoom-controls' data-html2canvas-ignore>
|
|
16
|
-
<button onClick={() => handleZoomIn(position
|
|
17
|
+
<div className='zoom-controls' data-html2canvas-ignore='true'>
|
|
18
|
+
<button onClick={() => handleZoomIn(position)} aria-label='Zoom In'>
|
|
17
19
|
<svg viewBox='0 0 24 24' stroke='currentColor' strokeWidth='3'>
|
|
18
20
|
<line x1='12' y1='5' x2='12' y2='19' />
|
|
19
21
|
<line x1='5' y1='12' x2='19' y2='12' />
|
|
20
22
|
</svg>
|
|
21
23
|
</button>
|
|
22
|
-
<button onClick={() => handleZoomOut(position
|
|
24
|
+
<button onClick={() => handleZoomOut(position)} aria-label='Zoom Out'>
|
|
23
25
|
<svg viewBox='0 0 24 24' stroke='currentColor' strokeWidth='3'>
|
|
24
26
|
<line x1='5' y1='12' x2='19' y2='12' />
|
|
25
27
|
</svg>
|
|
26
28
|
</button>
|
|
27
|
-
{
|
|
28
|
-
<button onClick={() => handleReset(
|
|
29
|
+
{config.general.type === 'bubble' && (
|
|
30
|
+
<button onClick={() => handleReset(setRuntimeData)} className='reset' aria-label='Reset Zoom and Map Filters'>
|
|
29
31
|
Reset Filters
|
|
30
32
|
</button>
|
|
31
33
|
)}
|
|
32
|
-
{(
|
|
33
|
-
<button onClick={() => handleReset(
|
|
34
|
+
{(config.general.type === 'world-geocode' || config.general.geoType === 'single-state') && (
|
|
35
|
+
<button onClick={() => handleReset(setRuntimeData)} className='reset' aria-label='Reset Zoom'>
|
|
34
36
|
Reset Zoom
|
|
35
37
|
</button>
|
|
36
38
|
)}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
.zoom-controls {
|
|
2
|
+
display: flex;
|
|
3
|
+
position: absolute;
|
|
4
|
+
bottom: 2em;
|
|
5
|
+
left: 1em;
|
|
6
|
+
z-index: 4;
|
|
7
|
+
> button.reset {
|
|
8
|
+
margin-left: 5px;
|
|
9
|
+
background: rgba(0, 0, 0, 0.65);
|
|
10
|
+
transition: 0.2s all;
|
|
11
|
+
color: #fff;
|
|
12
|
+
&:hover {
|
|
13
|
+
background: rgba(0, 0, 0, 0.8);
|
|
14
|
+
transition: 0.2s all;
|
|
15
|
+
}
|
|
16
|
+
&:active {
|
|
17
|
+
transform: scale(0.9);
|
|
18
|
+
}
|
|
19
|
+
&:focus {
|
|
20
|
+
background: #005eaa;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
> button:not(.reset) {
|
|
24
|
+
display: flex;
|
|
25
|
+
align-items: center;
|
|
26
|
+
justify-content: center;
|
|
27
|
+
padding: 0.2em;
|
|
28
|
+
height: 1.75em;
|
|
29
|
+
width: 1.75em;
|
|
30
|
+
background: rgba(0, 0, 0, 0.65);
|
|
31
|
+
transition: 0.2s all;
|
|
32
|
+
color: #fff;
|
|
33
|
+
border-radius: 100%;
|
|
34
|
+
border: 0;
|
|
35
|
+
&:hover {
|
|
36
|
+
background: rgba(0, 0, 0, 0.8);
|
|
37
|
+
transition: 0.2s all;
|
|
38
|
+
}
|
|
39
|
+
&:active {
|
|
40
|
+
transform: scale(0.9);
|
|
41
|
+
}
|
|
42
|
+
svg {
|
|
43
|
+
height: 1.75em;
|
|
44
|
+
width: 1.75em;
|
|
45
|
+
}
|
|
46
|
+
&:focus {
|
|
47
|
+
background: #005eaa;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
> button:first-child {
|
|
51
|
+
margin-right: 0.25em;
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/context.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import { createContext } from 'react'
|
|
1
|
+
import { createContext, Dispatch } from 'react'
|
|
2
2
|
import { MapConfig } from './types/MapConfig'
|
|
3
|
+
import MapActions from './store/map.actions'
|
|
3
4
|
|
|
4
5
|
type MapContext = {
|
|
6
|
+
container
|
|
7
|
+
setSharedFilter
|
|
8
|
+
customNavigationHandler
|
|
9
|
+
tooltipRef
|
|
10
|
+
containerEl
|
|
5
11
|
applyLegendToRow
|
|
6
|
-
applyTooltipsToGeo
|
|
7
12
|
data
|
|
8
13
|
displayGeoName
|
|
9
14
|
filteredCountryCode
|
|
@@ -11,29 +16,32 @@ type MapContext = {
|
|
|
11
16
|
generateRuntimeData
|
|
12
17
|
geoClickHandler
|
|
13
18
|
handleCircleClick: Function
|
|
14
|
-
hasZoom
|
|
15
19
|
innerContainerRef
|
|
16
20
|
isDashboard
|
|
17
|
-
isDebug
|
|
18
21
|
isEditor
|
|
22
|
+
mapId: string
|
|
19
23
|
loadConfig
|
|
20
24
|
position
|
|
21
25
|
resetLegendToggles
|
|
22
26
|
runtimeFilters
|
|
23
27
|
runtimeLegend
|
|
24
|
-
setAccessibleStatus
|
|
25
|
-
setFilteredCountryCode
|
|
26
28
|
setParentConfig
|
|
27
|
-
setPosition
|
|
28
29
|
setRuntimeData
|
|
29
30
|
setRuntimeFilters
|
|
30
31
|
setRuntimeLegend
|
|
31
32
|
setSharedFilterValue
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
setConfig: Function
|
|
34
|
+
config: MapConfig
|
|
34
35
|
tooltipId: string
|
|
36
|
+
legendMemo
|
|
37
|
+
legendSpecialClassLastMemo
|
|
38
|
+
translate
|
|
39
|
+
scale
|
|
40
|
+
annotations
|
|
35
41
|
}
|
|
36
42
|
|
|
43
|
+
export const MapDispatchContext = createContext<Dispatch<MapActions>>(() => {})
|
|
44
|
+
|
|
37
45
|
const ConfigContext = createContext({} as MapContext)
|
|
38
46
|
|
|
39
47
|
export default ConfigContext
|
|
@@ -72,9 +72,11 @@ export default {
|
|
|
72
72
|
subStyle: 'linear blocks',
|
|
73
73
|
tickRotation: '',
|
|
74
74
|
singleColumnLegend: false,
|
|
75
|
-
hideBorder: false
|
|
75
|
+
hideBorder: false,
|
|
76
|
+
groupBy: ''
|
|
76
77
|
},
|
|
77
78
|
filters: [],
|
|
79
|
+
data: [],
|
|
78
80
|
table: {
|
|
79
81
|
wrapColumns: false,
|
|
80
82
|
label: 'Data Table',
|
|
@@ -88,7 +90,8 @@ export default {
|
|
|
88
90
|
showFullGeoNameInCSV: false,
|
|
89
91
|
forceDisplay: true,
|
|
90
92
|
download: false,
|
|
91
|
-
indexLabel: ''
|
|
93
|
+
indexLabel: '',
|
|
94
|
+
cellMinWidth: '0'
|
|
92
95
|
},
|
|
93
96
|
tooltips: {
|
|
94
97
|
appearanceType: 'hover',
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import {
|
|
2
|
+
supportedCities,
|
|
3
|
+
supportedCounties,
|
|
4
|
+
supportedCountries,
|
|
5
|
+
supportedRegions,
|
|
6
|
+
supportedStates,
|
|
7
|
+
supportedTerritories
|
|
8
|
+
} from './../data/supported-geos'
|
|
9
|
+
|
|
10
|
+
import { SUPPORTED_DC_NAMES, GEO_TYPES, GEOCODE_TYPES } from './constants'
|
|
11
|
+
import { DataRow, MapConfig } from '../types/MapConfig'
|
|
12
|
+
|
|
13
|
+
// Data props
|
|
14
|
+
const stateKeys = Object.keys(supportedStates)
|
|
15
|
+
const territoryKeys = Object.keys(supportedTerritories)
|
|
16
|
+
const regionKeys = Object.keys(supportedRegions)
|
|
17
|
+
const countryKeys = Object.keys(supportedCountries)
|
|
18
|
+
const countyKeys = Object.keys(supportedCounties)
|
|
19
|
+
const cityKeys = Object.keys(supportedCities)
|
|
20
|
+
|
|
21
|
+
const geoLookups: Record<string, GeoLookup> = {
|
|
22
|
+
state: { keys: stateKeys, data: supportedStates },
|
|
23
|
+
territory: { keys: territoryKeys, data: supportedTerritories },
|
|
24
|
+
region: { keys: regionKeys, data: supportedRegions },
|
|
25
|
+
country: { keys: countryKeys, data: supportedCountries }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const memoizedFindUID = (geoName: string, type: keyof typeof geoLookups): string | undefined => {
|
|
29
|
+
const lookup = geoLookups[type]
|
|
30
|
+
return lookup.keys.find(key => lookup.data[key].includes(geoName))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const hasValidCoordinates = (row: Row, columns: GeoConfig['columns']): boolean => {
|
|
34
|
+
return !!(
|
|
35
|
+
columns.latitude?.name &&
|
|
36
|
+
columns.longitude?.name &&
|
|
37
|
+
row[columns.latitude.name] &&
|
|
38
|
+
row[columns.longitude.name]
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const normalizeGeoName = (value: unknown): string => {
|
|
43
|
+
if (value == null) return ''
|
|
44
|
+
return String(value).toUpperCase()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const findCityUID = (geoName: string): string | undefined => {
|
|
48
|
+
if (!geoName) return undefined
|
|
49
|
+
return cityKeys.find(key => key === geoName.toUpperCase())
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const handleDCDisplay = (geoName: string, displayAsHex: boolean): string | null => {
|
|
53
|
+
if (displayAsHex && SUPPORTED_DC_NAMES.includes(geoName)) {
|
|
54
|
+
return 'US-DC'
|
|
55
|
+
}
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const handleUSLocation = (row: DataRow, geoColumn: string, displayAsHex: boolean): string | null => {
|
|
60
|
+
const geoName = normalizeGeoName(row[geoColumn])
|
|
61
|
+
|
|
62
|
+
let uid = memoizedFindUID(geoName, 'state')
|
|
63
|
+
if (!uid) {
|
|
64
|
+
uid = memoizedFindUID(geoName, 'territory')
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!uid) uid = handleDCDisplay(geoName, displayAsHex)
|
|
68
|
+
if (!uid) uid = findCityUID(geoName)
|
|
69
|
+
|
|
70
|
+
return uid
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const handleWorldLocation = (row: DataRow, geoColumn: string, isWorldGeocodeType: boolean): string | null => {
|
|
74
|
+
const geoName = row[geoColumn]
|
|
75
|
+
let uid = memoizedFindUID(geoName, 'country')
|
|
76
|
+
if (!uid && (isWorldGeocodeType || geoName)) {
|
|
77
|
+
uid = findCityUID(geoName)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return uid
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const handleCountyLocation = (row: DataRow, geoColumn: string): string | undefined => {
|
|
84
|
+
const fips = row[geoColumn]
|
|
85
|
+
return countyKeys.find(key => key === fips)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const setRowUID = (row: DataRow, uid: string | null): void => {
|
|
89
|
+
if (uid) {
|
|
90
|
+
Object.defineProperty(row, 'uid', {
|
|
91
|
+
value: uid,
|
|
92
|
+
writable: true
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Adds unique identifiers to geographic data rows based on their location type and name.
|
|
99
|
+
* @param {MapConfig} configObj - Configuration object containing data and processing rules
|
|
100
|
+
* @param {string} fromColumn - Source column identifier
|
|
101
|
+
* @throws {Error} When configuration is invalid or required data is missing
|
|
102
|
+
*/
|
|
103
|
+
export const addUIDs = (configObj: MapConfig, fromColumn: string) => {
|
|
104
|
+
const { general, columns, data } = configObj
|
|
105
|
+
const { displayAsHex, geoType, type: geocodeType } = general
|
|
106
|
+
const { geo } = columns
|
|
107
|
+
|
|
108
|
+
data.forEach(row => {
|
|
109
|
+
let uid = null
|
|
110
|
+
if (row.uid) {
|
|
111
|
+
row.uid = null // Reset existing UID
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!geo.name) return
|
|
115
|
+
|
|
116
|
+
switch (geoType) {
|
|
117
|
+
case GEO_TYPES.US:
|
|
118
|
+
uid = handleUSLocation(row, geo.name, displayAsHex)
|
|
119
|
+
break
|
|
120
|
+
|
|
121
|
+
case GEO_TYPES.US_REGION:
|
|
122
|
+
uid = memoizedFindUID(normalizeGeoName(row[geo.name]), 'region')
|
|
123
|
+
break
|
|
124
|
+
|
|
125
|
+
case GEO_TYPES.WORLD:
|
|
126
|
+
uid = handleWorldLocation(row, geo.name, geocodeType === GEOCODE_TYPES.WORLD)
|
|
127
|
+
break
|
|
128
|
+
|
|
129
|
+
case GEO_TYPES.US_COUNTY:
|
|
130
|
+
case GEO_TYPES.SINGLE_STATE:
|
|
131
|
+
if (geocodeType !== GEOCODE_TYPES.US) {
|
|
132
|
+
uid = handleCountyLocation(row, geo.name)
|
|
133
|
+
}
|
|
134
|
+
break
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Handle special cases
|
|
138
|
+
if (!uid) {
|
|
139
|
+
if (geocodeType === GEOCODE_TYPES.US) {
|
|
140
|
+
uid = row[geo.name]
|
|
141
|
+
} else if (hasValidCoordinates(row, columns)) {
|
|
142
|
+
uid = `${row[geo.name]}`
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
setRowUID(row, uid)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
configObj.data.fromColumn = fromColumn
|
|
150
|
+
}
|