@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.
Files changed (119) hide show
  1. package/.idea/map.iml +12 -0
  2. package/.idea/modules.xml +8 -0
  3. package/.idea/vcs.xml +6 -0
  4. package/dist/cdcmap.js +31254 -32242
  5. package/examples/hex-colors.json +3 -3
  6. package/examples/m2.json +32904 -0
  7. package/examples/private/test.json +470 -1457
  8. package/examples/private/{mmr.json → wastewatermap.json} +86 -115
  9. package/index.html +36 -63
  10. package/package.json +7 -19
  11. package/src/CdcMap.tsx +56 -1552
  12. package/src/CdcMapComponent.tsx +608 -0
  13. package/src/_stories/CdcMap.Legend.Gradient.stories.tsx +10 -0
  14. package/src/_stories/CdcMap.Legend.stories.tsx +67 -0
  15. package/src/_stories/CdcMap.Table.stories.tsx +19 -0
  16. package/src/_stories/CdcMap.stories.tsx +12 -1
  17. package/src/_stories/UsaMap.NoData.stories.tsx +4 -4
  18. package/src/_stories/_mock/default-patterns.json +8 -5
  19. package/src/_stories/_mock/legend-bins.json +428 -0
  20. package/{examples/private/default-patterns.json → src/_stories/_mock/legends/legend-tests.json} +36 -131
  21. package/src/cdcMapComponent.styles.css +9 -0
  22. package/src/components/Annotation/Annotation.Draggable.tsx +27 -26
  23. package/src/components/Annotation/AnnotationDropdown.tsx +5 -6
  24. package/src/components/BubbleList.tsx +135 -49
  25. package/src/components/CityList.tsx +89 -87
  26. package/src/components/DataTable.tsx +8 -8
  27. package/src/components/EditorPanel/components/EditorPanel.tsx +823 -885
  28. package/src/components/EditorPanel/components/Error.tsx +9 -2
  29. package/src/components/EditorPanel/components/HexShapeSettings.tsx +127 -141
  30. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +55 -86
  31. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +89 -75
  32. package/src/components/EditorPanel/components/editorPanel.styles.css +95 -0
  33. package/src/components/Geo.tsx +9 -1
  34. package/src/components/GoogleMap/components/GoogleMap.tsx +1 -1
  35. package/src/components/Legend/components/Legend.tsx +92 -87
  36. package/src/components/Legend/components/LegendGroup/Legend.Group.tsx +128 -0
  37. package/src/components/Legend/components/LegendGroup/legend.group.css +27 -0
  38. package/src/components/Legend/components/LegendItem.Hex.tsx +4 -1
  39. package/src/components/Legend/components/index.scss +74 -17
  40. package/src/components/Modal.tsx +17 -7
  41. package/src/components/NavigationMenu.tsx +11 -9
  42. package/src/components/UsaMap/components/SingleState/SingleState.CountyOutput.tsx +12 -8
  43. package/src/components/UsaMap/components/SingleState/SingleState.StateOutput.tsx +4 -4
  44. package/src/components/UsaMap/components/TerritoriesSection.tsx +33 -10
  45. package/src/components/UsaMap/components/Territory/Territory.Hexagon.tsx +12 -10
  46. package/src/components/UsaMap/components/Territory/Territory.Rectangle.tsx +12 -14
  47. package/src/components/UsaMap/components/Territory/TerritoryShape.ts +2 -1
  48. package/src/components/UsaMap/components/UsaMap.County.tsx +138 -96
  49. package/src/components/UsaMap/components/UsaMap.Region.styles.css +72 -0
  50. package/src/components/UsaMap/components/UsaMap.Region.tsx +56 -103
  51. package/src/components/UsaMap/components/UsaMap.SingleState.styles.css +10 -0
  52. package/src/components/UsaMap/components/UsaMap.SingleState.tsx +65 -74
  53. package/src/components/UsaMap/components/UsaMap.State.tsx +112 -91
  54. package/src/components/UsaMap/helpers/map.ts +1 -1
  55. package/src/components/UsaMap/helpers/shapes.ts +20 -7
  56. package/src/components/WorldMap/WorldMap.tsx +64 -118
  57. package/src/components/WorldMap/worldMap.styles.css +28 -0
  58. package/src/components/ZoomControls.tsx +15 -13
  59. package/src/components/zoomControls.styles.css +53 -0
  60. package/src/context.ts +17 -9
  61. package/src/data/initial-state.js +5 -2
  62. package/src/helpers/addUIDs.ts +150 -0
  63. package/src/helpers/applyColorToLegend.ts +39 -64
  64. package/src/helpers/applyLegendToRow.ts +51 -0
  65. package/src/helpers/colorDistributions.ts +12 -0
  66. package/src/helpers/constants.ts +44 -0
  67. package/src/helpers/displayGeoName.ts +9 -2
  68. package/src/helpers/formatLegendLocation.ts +3 -2
  69. package/src/helpers/generateColorsArray.ts +2 -1
  70. package/src/helpers/generateRuntimeData.ts +78 -0
  71. package/src/helpers/generateRuntimeFilters.ts +63 -0
  72. package/src/helpers/generateRuntimeLegend.ts +566 -0
  73. package/src/helpers/generateRuntimeLegendHash.ts +16 -15
  74. package/src/helpers/getColumnNames.ts +19 -0
  75. package/src/helpers/getMapContainerClasses.ts +23 -0
  76. package/src/helpers/getStatePicked.ts +8 -0
  77. package/src/helpers/handleMapTabbing.ts +31 -0
  78. package/src/helpers/hashObj.ts +1 -1
  79. package/src/helpers/index.ts +22 -0
  80. package/src/helpers/navigationHandler.ts +3 -3
  81. package/src/helpers/resetLegendToggles.ts +13 -0
  82. package/src/helpers/setBinNumbers.ts +5 -0
  83. package/src/helpers/sortSpecialClassesLast.ts +7 -0
  84. package/src/helpers/tests/getColumnNames.test.ts +52 -0
  85. package/src/helpers/titleCase.ts +1 -1
  86. package/src/helpers/toggleLegendActive.ts +25 -0
  87. package/src/hooks/useApplyTooltipsToGeo.tsx +51 -0
  88. package/src/hooks/useColumnsRequiredChecker.ts +51 -0
  89. package/src/hooks/useGeoClickHandler.ts +45 -0
  90. package/src/hooks/useLegendSeparators.ts +26 -0
  91. package/src/hooks/useMapLayers.tsx +34 -60
  92. package/src/hooks/useModal.ts +22 -0
  93. package/src/hooks/useResizeObserver.ts +4 -5
  94. package/src/hooks/useStateZoom.tsx +52 -75
  95. package/src/hooks/useTooltip.ts +2 -3
  96. package/src/index.jsx +3 -9
  97. package/src/scss/editor-panel.scss +3 -99
  98. package/src/scss/main.scss +1 -19
  99. package/src/scss/map.scss +15 -220
  100. package/src/store/map.actions.ts +46 -0
  101. package/src/store/map.reducer.ts +96 -0
  102. package/src/types/Annotations.ts +24 -0
  103. package/src/types/MapConfig.ts +23 -3
  104. package/src/types/MapContext.ts +36 -35
  105. package/src/types/Modal.ts +1 -0
  106. package/src/types/RuntimeData.ts +3 -0
  107. package/examples/private/DEV-9644.json +0 -184
  108. package/examples/private/DEV-9989.json +0 -229
  109. package/examples/private/ardi.json +0 -180
  110. package/examples/private/colors 2.json +0 -416
  111. package/examples/private/colors.json +0 -416
  112. package/examples/private/colors.json.zip +0 -0
  113. package/examples/private/customColors.json +0 -45348
  114. package/examples/test.json +0 -183
  115. package/src/helpers/closeModal.ts +0 -9
  116. package/src/scss/btn.scss +0 -69
  117. package/src/scss/filters.scss +0 -27
  118. package/src/scss/variables.scss +0 -1
  119. /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 { handleMapAriaLabels } from '../../helpers/handleMapAriaLabels'
16
- import { titleCase } from '../../helpers/titleCase'
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
- setState,
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
- // TODO Refactor - state should be set together here to avoid rerenders
55
- // Resets to original data & zooms out
56
- const handleReset = (state, setState, setRuntimeData, generateRuntimeData) => {
57
- let reRun = generateRuntimeData(state)
58
- setRuntimeData(reRun)
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 = (position, setPosition) => {
72
+ const handleZoomIn = position => {
67
73
  if (position.zoom >= 4) return
68
- setPosition(pos => ({ ...pos, zoom: pos.zoom * 1.5 }))
74
+ dispatch({ type: 'SET_POSITION', payload: { coordinates: position.coordinates, zoom: position.zoom * 1.5 } })
69
75
  }
70
76
 
71
- const handleZoomOut = (position, setPosition) => {
77
+ const handleZoomOut = position => {
72
78
  if (position.zoom <= 1) return
73
- setPosition(pos => ({ ...pos, zoom: pos.zoom / 1.5 }))
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
- setPosition(position)
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.state value is found in the data use that, otherwise fall back to geo.properties.iso
90
- const dataHasStateName = state.data.some(d => d[state.columns.geo.name] === geo.properties.state)
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(state)
118
- const geoFillColor = getGeoFillColor(state)
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' && state.general.type !== 'bubble') {
124
+ if (legendColors && legendColors[0] !== '#000000' && type !== 'bubble') {
130
125
  styles = {
131
126
  ...styles,
132
- fill: state.general.type !== 'world-geocode' ? legendColors[0] : geoFillColor,
127
+ fill: type !== 'world-geocode' ? legendColors[0] : geoFillColor,
133
128
  cursor: 'default',
134
129
  '&:hover': {
135
- fill: state.general.type !== 'world-geocode' ? legendColors[1] : geoFillColor
130
+ fill: type !== 'world-geocode' ? legendColors[1] : geoFillColor
136
131
  },
137
132
  '&:active': {
138
- fill: state.general.type !== 'world-geocode' ? legendColors[2] : geoFillColor
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
- (state.columns.navigate && geoData[state.columns.navigate.name]) ||
145
- state.tooltips.appearanceType === 'click'
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
- style={styles}
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 (state.general.type === 'bubble') {
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
- {hasZoom ? (
228
- <svg viewBox='0 0 880 500' role='img' aria-label={handleMapAriaLabels(state)}>
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={4}
198
+ maxZoom={MAX_ZOOM_LEVEL}
240
199
  projection={projection}
241
- width={880}
242
- height={500}
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='0 0 880 500'>
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={880}
256
- height={500}
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
- {(state.general.type === 'data' ||
263
- (state.general.type === 'world-geocode' && hasZoom) ||
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], setPosition: Function) => void
7
- handleZoomOut: (coordinates: [Number, Number], setPosition: Function) => void
8
- handleReset: (coordinates: [Number, Number], setPosition: Function) => void
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 { state, setState, setRuntimeData, setPosition, position, generateRuntimeData } = useContext<MapContext>(ConfigContext)
13
- if (!state.general.allowMapZoom) return
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, setPosition)} aria-label='Zoom In'>
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, setPosition)} aria-label='Zoom Out'>
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
- {state.general.type === 'bubble' && (
28
- <button onClick={() => handleReset(state, setState, setRuntimeData, generateRuntimeData)} className='reset' aria-label='Reset Zoom and Map Filters'>
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
- {(state.general.type === 'world-geocode' || state.general.geoType === 'single-state') && (
33
- <button onClick={() => handleReset(state, setState, setRuntimeData, generateRuntimeData)} className='reset' aria-label='Reset Zoom'>
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
- setState
33
- state: MapConfig
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
+ }