@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,37 +1,96 @@
1
- import React, { useContext, useEffect } from 'react'
1
+ import React, { useContext } from 'react'
2
2
  import { scaleLinear } from 'd3-scale'
3
3
  import { countryCoordinates } from '../data/country-coordinates'
4
4
  import stateCoordinates from '../data/state-coordinates'
5
- import ConfigContext from '../context'
5
+ import ConfigContext, { MapDispatchContext } from '../context'
6
+ import { type Coordinate, DataRow } from '../types/MapConfig'
7
+ import useApplyTooltipsToGeo from '../hooks/useApplyTooltipsToGeo'
8
+ import { applyLegendToRow } from '../helpers/applyLegendToRow'
9
+ import { displayGeoName, SVG_HEIGHT, SVG_WIDTH } from '../helpers'
10
+ import { geoMercator, geoAlbersUsa, type GeoProjection } from 'd3-geo'
11
+ import { getColumnNames } from '../helpers/getColumnNames'
12
+ import { MapContext } from '../types/MapContext'
13
+ import useGeoClickHandler from '../hooks/useGeoClickHandler'
6
14
 
7
- export const BubbleList = ({ data: dataImport, state, projection, applyLegendToRow, applyTooltipsToGeo, handleCircleClick, runtimeData, displayGeoName }) => {
8
- const maxDataValue = Math.max(...dataImport.map(d => d[state.columns.primary.name]))
9
- const { tooltipId } = useContext(ConfigContext)
10
-
11
- const hasBubblesWithZeroOnMap = state.visual.showBubbleZeros ? 0 : 1
12
- // sort runtime data. Smaller bubbles should appear on top.
13
- const sortedRuntimeData = Object.values(runtimeData).sort((a, b) => (a[state.columns.primary.name] < b[state.columns.primary.name] ? 1 : -1))
14
- if (!sortedRuntimeData) return
15
+ type BubbleListProps = {
16
+ customProjection?: GeoProjection
17
+ }
15
18
 
19
+ export const BubbleList: React.FC<BubbleListProps> = ({ customProjection }) => {
20
+ const { config, tooltipId, legendMemo, legendSpecialClassLastMemo, setRuntimeData, runtimeData, runtimeLegend } =
21
+ useContext<MapContext>(ConfigContext)
22
+ const { columns, data, general, visual } = config
23
+ const { geoType, allowMapZoom } = general
24
+ const { minBubbleSize, maxBubbleSize, showBubbleZeros, extraBubbleBorder } = visual
25
+ const hasBubblesWithZeroOnMap = showBubbleZeros ? 0 : 1
16
26
  const clickTolerance = 10
17
- // Set bubble sizes
18
- var size = scaleLinear().domain([hasBubblesWithZeroOnMap, maxDataValue]).range([state.visual.minBubbleSize, state.visual.maxBubbleSize])
27
+ const dispatch = useContext(MapDispatchContext)
28
+ const { geoClickHandler } = useGeoClickHandler()
29
+
30
+ // hooks
31
+ const { applyTooltipsToGeo } = useApplyTooltipsToGeo()
32
+ const { primaryColumnName, geoColumnName } = getColumnNames(columns)
33
+
34
+ const maxDataValue = Math.max(...data.map(d => d[primaryColumnName]))
35
+ const size = scaleLinear().domain([hasBubblesWithZeroOnMap, maxDataValue]).range([minBubbleSize, maxBubbleSize])
36
+
37
+ const getProjection = () => {
38
+ try {
39
+ if (geoType === 'world') return geoMercator()
40
+ if (geoType === 'us') return geoAlbersUsa().translate([SVG_WIDTH / 2 + 15, SVG_HEIGHT / 2]) // translate is half of each svg x/y viewbox values
41
+ if (customProjection) return customProjection
42
+ throw new Error('No projection found in BubbleList component')
43
+ } catch (e) {
44
+ console.error(e)
45
+ }
46
+ }
47
+
48
+ const projection = getProjection()
19
49
 
20
- // Start looping through the countries to create the bubbles.
21
- if (state.general.geoType === 'world') {
22
- const countries =
50
+ const handleBubbleClick = (dataRow: DataRow) => {
51
+ if (!allowMapZoom) return
52
+ const newRuntimeData = data.filter(item => item[geoColumnName] === dataRow[geoColumnName])
53
+ const _filteredCountryCode = newRuntimeData[0]?.uid
54
+ if (!_filteredCountryCode) return null
55
+ const coordinates = countryCoordinates[_filteredCountryCode]
56
+ const long = coordinates[1]
57
+ const lat = coordinates[0]
58
+ const reversedCoordinates: Coordinate = [long, lat]
59
+ const filteredCountryObj = runtimeData[_filteredCountryCode]
60
+ const _tempRuntimeData = {
61
+ [_filteredCountryCode]: filteredCountryObj
62
+ }
63
+
64
+ // Zoom the map in...
65
+ dispatch({ type: 'SET_POSITION', payload: { coordinates: reversedCoordinates, zoom: 3 } })
66
+
67
+ // ...and show the data for the clicked country
68
+ setRuntimeData(_tempRuntimeData)
69
+ }
70
+
71
+ const sortedRuntimeData: DataRow = Object.values(runtimeData).sort((a, b) =>
72
+ a[primaryColumnName] < b[primaryColumnName] ? 1 : -1
73
+ )
74
+
75
+ if (!sortedRuntimeData) return
76
+
77
+ if (geoType === 'world') {
78
+ return (
23
79
  sortedRuntimeData &&
24
- sortedRuntimeData.map((country, index) => {
80
+ sortedRuntimeData.map(country => {
25
81
  let coordinates = countryCoordinates[country.uid]
26
82
 
27
83
  if (!coordinates) return true
28
84
 
29
- const countryName = displayGeoName(country[state.columns.geo.name])
85
+ const countryName = displayGeoName(country[geoColumnName])
30
86
  const toolTip = applyTooltipsToGeo(countryName, country)
31
- const legendColors = applyLegendToRow(country)
87
+ const legendColors = applyLegendToRow(country, config, runtimeLegend, legendMemo, legendSpecialClassLastMemo)
32
88
 
33
- let primaryKey = state.columns.primary.name
34
- if ((Math.floor(Number(country[primaryKey])) === 0 || country[primaryKey] === '') && !state.visual.showBubbleZeros) return
89
+ if (
90
+ (Math.floor(Number(country[primaryColumnName])) === 0 || country[primaryColumnName] === '') &&
91
+ !showBubbleZeros
92
+ )
93
+ return
35
94
 
36
95
  let transform = `translate(${projection([coordinates[1], coordinates[0]])})`
37
96
 
@@ -45,9 +104,9 @@ export const BubbleList = ({ data: dataImport, state, projection, applyLegendToR
45
104
  tabIndex={-1}
46
105
  key={`circle-${countryName.replace(' ', '')}`}
47
106
  className={`bubble country--${countryName}`}
48
- cx={Number(projection(coordinates[1], coordinates[0])[0]) || 0} // || 0 handles error on loads where the data isn't ready
107
+ cx={Number(projection(coordinates[1], coordinates[0])[0]) || 0}
49
108
  cy={Number(projection(coordinates[1], coordinates[0])[1]) || 0}
50
- r={Number(size(country[primaryKey]))}
109
+ r={Number(size(country[primaryColumnName]))}
51
110
  fill={legendColors[0]}
52
111
  stroke={legendColors[0]}
53
112
  strokeWidth={1.25}
@@ -57,8 +116,15 @@ export const BubbleList = ({ data: dataImport, state, projection, applyLegendToR
57
116
  pointerY = e.clientY
58
117
  }}
59
118
  onPointerUp={e => {
60
- if (pointerX && pointerY && e.clientX > pointerX - clickTolerance && e.clientX < pointerX + clickTolerance && e.clientY > pointerY - clickTolerance && e.clientY < pointerY + clickTolerance) {
61
- handleCircleClick(country)
119
+ if (
120
+ pointerX &&
121
+ pointerY &&
122
+ e.clientX > pointerX - clickTolerance &&
123
+ e.clientX < pointerX + clickTolerance &&
124
+ e.clientY > pointerY - clickTolerance &&
125
+ e.clientY < pointerY + clickTolerance
126
+ ) {
127
+ handleBubbleClick(country)
62
128
  pointerX = undefined
63
129
  pointerY = undefined
64
130
  }
@@ -69,14 +135,14 @@ export const BubbleList = ({ data: dataImport, state, projection, applyLegendToR
69
135
  data-tooltip-html={toolTip}
70
136
  />
71
137
 
72
- {state.visual.extraBubbleBorder && (
138
+ {extraBubbleBorder && (
73
139
  <circle
74
140
  tabIndex={-1}
75
141
  key={`circle-${countryName.replace(' ', '')}`}
76
142
  className='bubble'
77
- cx={Number(projection(coordinates[1], coordinates[0])[0]) || 0} // || 0 handles error on loads where the data isn't ready
143
+ cx={Number(projection(coordinates[1], coordinates[0])[0]) || 0}
78
144
  cy={Number(projection(coordinates[1], coordinates[0])[1]) || 0}
79
- r={Number(size(country[primaryKey])) + 1}
145
+ r={Number(size(country[primaryColumnName])) + 1}
80
146
  fill={'transparent'}
81
147
  stroke={'white'}
82
148
  strokeWidth={0.5}
@@ -85,8 +151,15 @@ export const BubbleList = ({ data: dataImport, state, projection, applyLegendToR
85
151
  pointerY = e.clientY
86
152
  }}
87
153
  onPointerUp={e => {
88
- if (pointerX && pointerY && e.clientX > pointerX - clickTolerance && e.clientX < pointerX + clickTolerance && e.clientY > pointerY - clickTolerance && e.clientY < pointerY + clickTolerance) {
89
- handleCircleClick(country)
154
+ if (
155
+ pointerX &&
156
+ pointerY &&
157
+ e.clientX > pointerX - clickTolerance &&
158
+ e.clientX < pointerX + clickTolerance &&
159
+ e.clientY > pointerY - clickTolerance &&
160
+ e.clientY < pointerY + clickTolerance
161
+ ) {
162
+ handleBubbleClick(country)
90
163
  pointerX = undefined
91
164
  pointerY = undefined
92
165
  }
@@ -106,21 +179,20 @@ export const BubbleList = ({ data: dataImport, state, projection, applyLegendToR
106
179
  </g>
107
180
  )
108
181
  })
109
- return countries
182
+ )
110
183
  }
111
184
 
112
- if (state.general.geoType === 'us') {
113
- const bubbles =
185
+ if (geoType === 'us') {
186
+ return (
114
187
  sortedRuntimeData &&
115
- sortedRuntimeData.map((item, index) => {
188
+ sortedRuntimeData.map(item => {
116
189
  let stateData = stateCoordinates[item.uid]
117
- let primaryKey = state?.columns?.primary?.name
118
- if (Number(size(item[primaryKey])) === 0) return
190
+ if (Number(size(item[primaryColumnName])) === 0) return
119
191
 
120
- if (item[primaryKey] === null) item[primaryKey] = ''
192
+ if (item[primaryColumnName] === null) item[primaryColumnName] = ''
121
193
 
122
- // Return if hiding zeros on the map
123
- if ((Math.floor(Number(item[primaryKey])) === 0 || item[primaryKey] === '') && !state.visual.showBubbleZeros) return
194
+ if ((Math.floor(Number(item[primaryColumnName])) === 0 || item[primaryColumnName] === '') && !showBubbleZeros)
195
+ return
124
196
 
125
197
  if (!stateData) return true
126
198
  let longitude = Number(stateData.Longitude)
@@ -131,7 +203,7 @@ export const BubbleList = ({ data: dataImport, state, projection, applyLegendToR
131
203
 
132
204
  stateName = displayGeoName(stateName)
133
205
  const toolTip = applyTooltipsToGeo(stateName, item)
134
- const legendColors = applyLegendToRow(item)
206
+ const legendColors = applyLegendToRow(item, config, runtimeLegend, legendMemo, legendSpecialClassLastMemo)
135
207
 
136
208
  let transform = `translate(${projection([coordinates[1], coordinates[0]])})`
137
209
 
@@ -144,9 +216,9 @@ export const BubbleList = ({ data: dataImport, state, projection, applyLegendToR
144
216
  tabIndex={-1}
145
217
  key={`circle-${stateName.replace(' ', '')}`}
146
218
  className='bubble'
147
- cx={projection(coordinates)[0] || 0} // || 0 handles error on loads where the data isn't ready
219
+ cx={projection(coordinates)[0] || 0}
148
220
  cy={projection(coordinates)[1] || 0}
149
- r={Number(size(item[primaryKey]))}
221
+ r={Number(size(item[primaryColumnName]))}
150
222
  fill={legendColors[0]}
151
223
  stroke={legendColors[0]}
152
224
  strokeWidth={1.25}
@@ -156,8 +228,15 @@ export const BubbleList = ({ data: dataImport, state, projection, applyLegendToR
156
228
  pointerY = e.clientY
157
229
  }}
158
230
  onPointerUp={e => {
159
- if (pointerX && pointerY && e.clientX > pointerX - clickTolerance && e.clientX < pointerX + clickTolerance && e.clientY > pointerY - clickTolerance && e.clientY < pointerY + clickTolerance) {
160
- handleCircleClick(state)
231
+ if (
232
+ pointerX &&
233
+ pointerY &&
234
+ e.clientX > pointerX - clickTolerance &&
235
+ e.clientX < pointerX + clickTolerance &&
236
+ e.clientY > pointerY - clickTolerance &&
237
+ e.clientY < pointerY + clickTolerance
238
+ ) {
239
+ geoClickHandler(stateName, stateData)
161
240
  pointerX = undefined
162
241
  pointerY = undefined
163
242
  }
@@ -167,14 +246,14 @@ export const BubbleList = ({ data: dataImport, state, projection, applyLegendToR
167
246
  data-tooltip-id={`tooltip__${tooltipId}`}
168
247
  data-tooltip-html={toolTip}
169
248
  />
170
- {state.visual.extraBubbleBorder && (
249
+ {extraBubbleBorder && (
171
250
  <circle
172
251
  tabIndex={-1}
173
252
  key={`circle-${stateName.replace(' ', '')}`}
174
253
  className='bubble'
175
- cx={projection(coordinates)[0] || 0} // || 0 handles error on loads where the data isn't ready
254
+ cx={projection(coordinates)[0] || 0}
176
255
  cy={projection(coordinates)[1] || 0}
177
- r={Number(size(item[primaryKey])) + 1}
256
+ r={Number(size(item[primaryColumnName])) + 1}
178
257
  fill={'transparent'}
179
258
  stroke={'white'}
180
259
  strokeWidth={0.5}
@@ -184,8 +263,15 @@ export const BubbleList = ({ data: dataImport, state, projection, applyLegendToR
184
263
  pointerY = e.clientY
185
264
  }}
186
265
  onPointerUp={e => {
187
- if (pointerX && pointerY && e.clientX > pointerX - clickTolerance && e.clientX < pointerX + clickTolerance && e.clientY > pointerY - clickTolerance && e.clientY < pointerY + clickTolerance) {
188
- handleCircleClick(state)
266
+ if (
267
+ pointerX &&
268
+ pointerY &&
269
+ e.clientX > pointerX - clickTolerance &&
270
+ e.clientX < pointerX + clickTolerance &&
271
+ e.clientY > pointerY - clickTolerance &&
272
+ e.clientY < pointerY + clickTolerance
273
+ ) {
274
+ geoClickHandler(stateName, stateData)
189
275
  pointerX = undefined
190
276
  pointerY = undefined
191
277
  }
@@ -201,7 +287,7 @@ export const BubbleList = ({ data: dataImport, state, projection, applyLegendToR
201
287
 
202
288
  return <g key={`group-${stateName.replace(' ', '')}`}>{circle}</g>
203
289
  })
204
- return bubbles
290
+ )
205
291
  }
206
292
  }
207
293
  export default BubbleList
@@ -1,86 +1,97 @@
1
- import { useState, useEffect, useContext } from 'react'
2
-
3
- import { jsx } from '@emotion/react'
4
- import { supportedCities } from '../data/supported-geos'
1
+ import { useContext, useEffect, useState } from 'react'
5
2
  import { scaleLinear } from 'd3-scale'
6
- import { GlyphStar, GlyphTriangle, GlyphDiamond, GlyphSquare, GlyphCircle } from '@visx/glyph'
7
- import { getFilterControllingStatePicked } from './UsaMap/helpers/map'
8
- import { titleCase } from '../helpers/titleCase'
9
-
3
+ import { GlyphCircle, GlyphDiamond, GlyphSquare, GlyphStar, GlyphTriangle } from '@visx/glyph'
10
4
  import ConfigContext from '../context'
11
- import { getGeoStrokeColor } from '../helpers/colors'
12
-
13
- const CityList = ({
14
- data,
15
- geoClickHandler,
16
- applyTooltipsToGeo,
17
- displayGeoName,
18
- applyLegendToRow,
19
- setSharedFilterValue,
20
- isFilterValueSupported,
21
- tooltipId,
22
- projection
23
- }) => {
24
- const [citiesData, setCitiesData] = useState({})
25
- const { state, topoData, runtimeData, position } = useContext(ConfigContext)
26
- if (!projection) return
27
-
28
- useEffect(() => {
29
- const citiesDictionary = {}
5
+ import { supportedCities } from '../data/supported-geos'
6
+ import { getFilterControllingStatePicked } from './UsaMap/helpers/map'
7
+ import { displayGeoName, getGeoStrokeColor, SVG_HEIGHT, SVG_PADDING, SVG_WIDTH, titleCase } from '../helpers'
8
+ import useGeoClickHandler from '../hooks/useGeoClickHandler'
9
+ import useApplyTooltipsToGeo from '../hooks/useApplyTooltipsToGeo'
10
+ import { applyLegendToRow } from '../helpers/applyLegendToRow'
11
+ import { getColumnNames } from '../helpers/getColumnNames'
12
+
13
+ type CityListProps = {
14
+ setSharedFilterValue: string
15
+ isFilterValueSupported: boolean
16
+ tooltipId: string
17
+ projection: any
18
+ }
30
19
 
31
- if (data) {
32
- Object.keys(data).forEach(key => {
33
- const city = data[key]
34
- citiesDictionary[city[state.columns.geo.name]] = city
35
- })
36
- }
20
+ const CityList: React.FC<CityListProps> = ({ setSharedFilterValue, isFilterValueSupported, tooltipId, projection }) => {
21
+ const {
22
+ config,
23
+ topoData,
24
+ data: runtimeData,
25
+ position,
26
+ legendMemo,
27
+ legendSpecialClassLastMemo,
28
+ runtimeLegend
29
+ } = useContext(ConfigContext)
30
+ const { geoClickHandler } = useGeoClickHandler()
31
+ const { applyTooltipsToGeo } = useApplyTooltipsToGeo()
32
+
33
+ const { geoColumnName, latitudeColumnName, longitudeColumnName, primaryColumnName } = getColumnNames(config.columns)
34
+ const { additionalCityStyles } = config.visual || []
37
35
 
38
- setCitiesData(citiesDictionary)
39
- }, [data])
36
+ if (!projection) return
40
37
 
41
- if (state.general.type === 'bubble') {
42
- const maxDataValue = Math.max(...(data ? Object.keys(data).map(key => data[key][state.columns.primary.name]) : [0]))
43
- const sortedRuntimeData = Object.values(data).sort((a, b) =>
44
- a[state.columns.primary.name] < b[state.columns.primary.name] ? 1 : -1
38
+ const citiesData = runtimeData
39
+ ? Object.keys(runtimeData).reduce((acc, key) => {
40
+ const city = runtimeData[key]
41
+ acc[city[geoColumnName]] = city
42
+ return acc
43
+ }, {})
44
+ : {}
45
+
46
+ if (config.general.type === 'bubble') {
47
+ const maxDataValue = Math.max(
48
+ ...(runtimeData ? Object.keys(runtimeData).map(key => runtimeData[key][config.columns.primary.name]) : [0])
49
+ )
50
+ const sortedRuntimeData = Object.values(runtimeData).sort((a, b) =>
51
+ a[primaryColumnName] < b[primaryColumnName] ? 1 : -1
45
52
  )
46
53
  if (!sortedRuntimeData) return
47
54
 
48
55
  // Set bubble sizes
49
- var size = scaleLinear().domain([1, maxDataValue]).range([state.visual.minBubbleSize, state.visual.maxBubbleSize])
56
+ var size = scaleLinear().domain([1, maxDataValue]).range([config.visual.minBubbleSize, config.visual.maxBubbleSize])
50
57
  }
51
- let cityList = Object.keys(citiesData).filter(c => undefined !== c || undefined !== data[c])
58
+ const cityList = Object.keys(citiesData).filter(c => undefined !== c || undefined !== runtimeData[c])
52
59
  if (!cityList) return true
53
60
 
54
61
  // Cities output
55
- const cities = cityList.map((city, i) => {
56
- let geoData
57
- if (data) {
58
- Object.keys(data).forEach(key => {
59
- if (city === data[key][state.columns.geo.name]) {
60
- geoData = data[key]
62
+ return cityList.map((city, i) => {
63
+ let geoData: Object
64
+ if (runtimeData) {
65
+ Object.keys(runtimeData).forEach(key => {
66
+ if (city === runtimeData[key][config.columns.geo.name]) {
67
+ geoData = runtimeData[key]
61
68
  }
62
69
  })
63
70
  }
64
71
  if (!geoData) {
65
- geoData = data ? data[city] : undefined
72
+ geoData = runtimeData ? runtimeData[city] : undefined
66
73
  }
67
74
  const cityDisplayName = titleCase(displayGeoName(city))
68
75
 
69
- const legendColors = geoData ? applyLegendToRow(geoData) : data[city] ? applyLegendToRow(data[city]) : false
76
+ const legendColors = geoData
77
+ ? applyLegendToRow(geoData, config, runtimeLegend, legendMemo, legendSpecialClassLastMemo)
78
+ : runtimeData[city]
79
+ ? applyLegendToRow(runtimeData[city], config, runtimeLegend, legendMemo, legendSpecialClassLastMemo)
80
+ : false
70
81
 
71
82
  if (legendColors === false) {
72
83
  return true
73
84
  }
74
85
 
75
- const toolTip = applyTooltipsToGeo(cityDisplayName, geoData || data[city])
86
+ const toolTip = applyTooltipsToGeo(cityDisplayName, geoData || runtimeData[city])
76
87
 
77
- const radius = state.visual.geoCodeCircleSize || 8
88
+ const radius = config.visual.geoCodeCircleSize || 8
78
89
 
79
90
  const additionalProps = {
80
- fillOpacity: state.general.type === 'bubble' ? 0.4 : 1
91
+ fillOpacity: config.general.type === 'bubble' ? 0.4 : 1
81
92
  }
82
93
 
83
- const geoStrokeColor = getGeoStrokeColor(state)
94
+ const geoStrokeColor = getGeoStrokeColor(config)
84
95
 
85
96
  const pin = (
86
97
  <path
@@ -101,8 +112,8 @@ const CityList = ({
101
112
  let transform = ''
102
113
 
103
114
  if (
104
- !geoData?.[state.columns.longitude.name] &&
105
- !geoData?.[state.columns.latitude.name] &&
115
+ !geoData?.[longitudeColumnName] &&
116
+ !geoData?.[latitudeColumnName] &&
106
117
  city &&
107
118
  supportedCities[city.toUpperCase()]
108
119
  ) {
@@ -111,34 +122,26 @@ const CityList = ({
111
122
 
112
123
  let needsPointer = false
113
124
 
114
- if (geoData?.[state.columns.longitude.name] && geoData?.[state.columns.latitude.name]) {
115
- let coords = [Number(geoData?.[state.columns.longitude.name]), Number(geoData?.[state.columns.latitude.name])]
125
+ if (geoData?.[longitudeColumnName] && geoData?.[latitudeColumnName]) {
126
+ let coords = [Number(geoData?.[longitudeColumnName]), Number(geoData?.[latitudeColumnName])]
116
127
  transform = `translate(${projection(coords)})`
117
128
  needsPointer = true
118
129
  }
119
130
 
120
- if (
121
- geoData?.[state.columns.longitude.name] &&
122
- geoData?.[state.columns.latitude.name] &&
123
- state.general.geoType === 'single-state'
124
- ) {
125
- const statePicked = getFilterControllingStatePicked(state, runtimeData)
131
+ if (geoData?.[longitudeColumnName] && geoData?.[latitudeColumnName] && config.general.geoType === 'single-state') {
132
+ const statePicked = getFilterControllingStatePicked(config, runtimeData)
126
133
  const _statePickedData = topoData?.states?.find(s => s.properties.name === statePicked)
127
- // SVG ITEMS
128
- const WIDTH = 880
129
- const HEIGHT = 500
130
- const PADDING = 50
131
134
 
132
135
  const newProjection = projection.fitExtent(
133
136
  [
134
- [PADDING, PADDING],
135
- [WIDTH - PADDING, HEIGHT - PADDING]
137
+ [SVG_PADDING, SVG_PADDING],
138
+ [SVG_WIDTH - SVG_PADDING, SVG_HEIGHT - SVG_PADDING]
136
139
  ],
137
140
  _statePickedData
138
141
  )
139
- let coords = [Number(geoData?.[state.columns.longitude.name]), Number(geoData?.[state.columns.latitude.name])]
142
+ let coords = [Number(geoData?.[longitudeColumnName]), Number(geoData?.[latitudeColumnName])]
140
143
  transform = `translate(${newProjection(coords)}) scale(${
141
- state.visual.geoCodeCircleSize / (position.zoom > 1 ? position.zoom : 1)
144
+ config.visual.geoCodeCircleSize / (position.zoom > 1 ? position.zoom : 1)
142
145
  })`
143
146
  needsPointer = true
144
147
  }
@@ -153,8 +156,8 @@ const CityList = ({
153
156
  stroke:
154
157
  setSharedFilterValue &&
155
158
  isFilterValueSupported &&
156
- data[city] &&
157
- data[city][state.columns.geo.name] === setSharedFilterValue
159
+ runtimeData[city] &&
160
+ runtimeData[city][config.columns.geo.name] === setSharedFilterValue
158
161
  ? 'rgba(0, 0, 0, 1)'
159
162
  : 'rgba(0, 0, 0, 0.4)',
160
163
  '&:hover': {
@@ -170,15 +173,15 @@ const CityList = ({
170
173
 
171
174
  // If we need to add a cursor pointer
172
175
  if (
173
- (state.columns.navigate && geoData?.[state.columns.navigate.name] && geoData[state.columns.navigate.name]) ||
174
- state.tooltips.appearanceType === 'click'
176
+ (config.columns.navigate && geoData?.[config.columns.navigate.name] && geoData[config.columns.navigate.name]) ||
177
+ config.tooltips.appearanceType === 'click'
175
178
  ) {
176
179
  styles.cursor = 'pointer'
177
180
  }
178
181
 
179
182
  const shapeProps = {
180
183
  onClick: () => geoClickHandler(cityDisplayName, geoData),
181
- size: state.general.type === 'bubble' ? size(geoData[state.columns.primary.name]) : radius * 30,
184
+ size: config.general.type === 'bubble' ? size(geoData[primaryColumnName]) : radius * 30,
182
185
  title: 'Select for more information',
183
186
  'data-tooltip-id': `tooltip__${tooltipId}`,
184
187
  'data-tooltip-html': toolTip,
@@ -197,9 +200,8 @@ const CityList = ({
197
200
  triangle: <GlyphTriangle {...shapeProps} />
198
201
  }
199
202
 
200
- const { additionalCityStyles } = state.visual || []
201
- const cityStyle = Object.values(data)
202
- .filter(d => additionalCityStyles.some(style => String(d[style.column]) === String(style.value)))
203
+ const cityStyle = Object.values(runtimeData)
204
+ .filter(d => additionalCityStyles?.some(style => String(d[style.column]) === String(style.value)))
203
205
  .map(d => {
204
206
  const conditionsMatched = additionalCityStyles.find(style => String(d[style.column]) === String(style.value))
205
207
  return { ...conditionsMatched, ...d }
@@ -210,8 +212,8 @@ const CityList = ({
210
212
 
211
213
  if (cityStyle !== undefined && cityStyle.shape) {
212
214
  if (
213
- !geoData?.[state.columns.longitude.name] &&
214
- !geoData?.[state.columns.latitude.name] &&
215
+ !geoData?.[longitudeColumnName] &&
216
+ !geoData?.[latitudeColumnName] &&
215
217
  city &&
216
218
  supportedCities[city.toUpperCase()]
217
219
  ) {
@@ -224,10 +226,10 @@ const CityList = ({
224
226
  )
225
227
  }
226
228
 
227
- if (geoData?.[state.columns.longitude.name] && geoData?.[state.columns.latitude.name]) {
228
- const coords = [Number(geoData?.[state.columns.longitude.name]), Number(geoData?.[state.columns.latitude.name])]
229
+ if (geoData?.[longitudeColumnName] && geoData?.[latitudeColumnName]) {
230
+ const coords = [Number(geoData?.[longitudeColumnName]), Number(geoData?.[latitudeColumnName])]
229
231
  let translate = `translate(${projection(coords)})`
230
- console.log(translate, 'translate')
232
+
231
233
  return (
232
234
  <g key={i} transform={translate} style={styles} className='geo-point' tabIndex={-1}>
233
235
  {cityStyleShapes[cityStyle.shape.toLowerCase()]}
@@ -235,14 +237,14 @@ const CityList = ({
235
237
  )
236
238
  }
237
239
  }
240
+ if (legendColors?.[0] === '#000000') return
241
+
238
242
  return (
239
243
  <g key={i} transform={transform} style={styles} className='geo-point' tabIndex={-1}>
240
- {cityStyleShapes[state.visual.cityStyle.toLowerCase()]}
244
+ {cityStyleShapes[config.visual.cityStyle.toLowerCase()]}
241
245
  </g>
242
246
  )
243
247
  })
244
-
245
- return cities
246
248
  }
247
249
 
248
250
  export default CityList
@@ -10,8 +10,8 @@ import MediaControls from '@cdc/core/components/MediaControls'
10
10
  import SkipTo from '@cdc/core/components/elements/SkipTo'
11
11
 
12
12
  import Loading from '@cdc/core/components/Loading'
13
- import { navigationHandler } from '../helpers/navigationHandler'
14
- import ConfigContext from '../context'
13
+ import { navigationHandler } from '../helpers'
14
+ import ConfigContext, { MapDispatchContext } from '../context'
15
15
 
16
16
  /* eslint-disable jsx-a11y/no-noninteractive-tabindex, jsx-a11y/no-static-element-interactions */
17
17
  const DataTable = props => {
@@ -29,10 +29,10 @@ const DataTable = props => {
29
29
  applyLegendToRow,
30
30
  displayGeoName,
31
31
  formatLegendLocation,
32
- tabbingId,
33
- setFilteredCountryCode
32
+ tabbingId
34
33
  } = props
35
34
 
35
+ const dispatch = useContext(MapDispatchContext)
36
36
  const { currentViewport: viewport } = useContext(ConfigContext)
37
37
  const [expanded, setExpanded] = useState(expandDataTable)
38
38
  const [sortBy, setSortBy] = useState({ column: 'geo', asc: false })
@@ -181,7 +181,7 @@ const DataTable = props => {
181
181
  aria-label='Download this data in a CSV file format.'
182
182
  className={`${headerColor} no-border`}
183
183
  id={`${skipId}`}
184
- data-html2canvas-ignore
184
+ data-html2canvas-ignore={true}
185
185
  role='button'
186
186
  >
187
187
  Download Data (CSV)
@@ -217,7 +217,7 @@ const DataTable = props => {
217
217
  if (!state.data) return <Loading />
218
218
 
219
219
  const rows = Object.keys(runtimeData)
220
- .filter(row => applyLegendToRow(runtimeData[row]))
220
+ .filter(row => applyLegendToRow(runtimeData[row], state))
221
221
  .sort((a, b) => {
222
222
  const sortVal = customSort(
223
223
  runtimeData[a][state.columns[sortBy.column].name],
@@ -326,7 +326,7 @@ const DataTable = props => {
326
326
 
327
327
  if (column === 'geo') {
328
328
  const rowObj = runtimeData[row]
329
- const legendColor = applyLegendToRow(rowObj)
329
+ const legendColor = applyLegendToRow(rowObj, state)
330
330
 
331
331
  var labelValue
332
332
  if (state.general.geoType !== 'us-county' || state.general.type === 'us-geocode') {
@@ -355,7 +355,7 @@ const DataTable = props => {
355
355
  state.general.type === 'bubble' &&
356
356
  state.general.allowMapZoom &&
357
357
  state.general.geoType === 'world'
358
- ? setFilteredCountryCode(row)
358
+ ? dispatch({ type: 'SET_FILTERED_COUNTRY_CODE', payload: row })
359
359
  : true
360
360
  }
361
361
  >