@cdc/map 4.25.8 → 4.25.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/.claude/settings.local.json +30 -0
  2. package/dist/cdcmap.js +54263 -52600
  3. package/examples/private/c.json +290 -0
  4. package/examples/private/canvas-city-hover.json +787 -0
  5. package/examples/private/d.json +345 -0
  6. package/examples/private/g.json +1 -0
  7. package/examples/private/h.json +105911 -0
  8. package/examples/private/measles-data.json +378 -0
  9. package/examples/private/measles.json +211 -0
  10. package/examples/private/north-dakota.json +1132 -0
  11. package/examples/private/state-with-pattern.json +883 -0
  12. package/index.html +35 -34
  13. package/package.json +26 -5
  14. package/src/CdcMap.tsx +23 -8
  15. package/src/CdcMapComponent.tsx +215 -309
  16. package/src/_stories/CdcMap.Filters.stories.tsx +2 -2
  17. package/src/_stories/CdcMap.Legend.Gradient.stories.tsx +3 -3
  18. package/src/_stories/CdcMap.Legend.stories.tsx +7 -4
  19. package/src/_stories/CdcMap.Patterns.stories.tsx +2 -2
  20. package/src/_stories/CdcMap.Table.stories.tsx +2 -2
  21. package/src/_stories/CdcMap.stories.tsx +15 -5
  22. package/src/_stories/GoogleMap.stories.tsx +2 -2
  23. package/src/_stories/UsaMap.NoData.stories.tsx +2 -2
  24. package/src/_stories/_mock/equal-number.json +1109 -0
  25. package/src/_stories/_mock/us-bubble-cities.json +306 -0
  26. package/src/components/BubbleList.tsx +16 -12
  27. package/src/components/CityList.tsx +85 -107
  28. package/src/components/DataTable.tsx +37 -9
  29. package/src/components/EditorPanel/components/EditorPanel.tsx +177 -165
  30. package/src/components/EditorPanel/components/HexShapeSettings.tsx +3 -2
  31. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +7 -5
  32. package/src/components/Geo.tsx +2 -0
  33. package/src/components/Legend/components/Legend.tsx +109 -73
  34. package/src/components/Legend/components/LegendGroup/Legend.Group.tsx +10 -7
  35. package/src/components/MapContainer.tsx +52 -0
  36. package/src/components/MapControls.tsx +44 -0
  37. package/src/components/NavigationMenu.tsx +11 -2
  38. package/src/components/UsaMap/components/SingleState/SingleState.CountyOutput.tsx +24 -7
  39. package/src/components/UsaMap/components/UsaMap.County.tsx +111 -37
  40. package/src/components/UsaMap/components/UsaMap.Region.tsx +23 -5
  41. package/src/components/UsaMap/components/UsaMap.SingleState.tsx +6 -6
  42. package/src/components/UsaMap/components/UsaMap.State.tsx +28 -10
  43. package/src/components/UsaMap/helpers/map.ts +2 -2
  44. package/src/components/WorldMap/WorldMap.tsx +113 -25
  45. package/src/components/ZoomControls.tsx +6 -9
  46. package/src/context/LegendMemoContext.tsx +30 -0
  47. package/src/context.ts +1 -40
  48. package/src/data/initial-state.js +143 -130
  49. package/src/data/supported-geos.js +17 -2
  50. package/src/helpers/applyColorToLegend.ts +116 -20
  51. package/src/helpers/applyLegendToRow.ts +10 -6
  52. package/src/helpers/componentHelpers.ts +8 -0
  53. package/src/helpers/constants.ts +12 -0
  54. package/src/helpers/dataTableHelpers.ts +6 -0
  55. package/src/helpers/displayGeoName.ts +1 -1
  56. package/src/helpers/generateRuntimeLegend.ts +44 -8
  57. package/src/helpers/generateRuntimeLegendHash.ts +4 -2
  58. package/src/helpers/getColumnNames.ts +1 -1
  59. package/src/helpers/getPatternForRow.ts +36 -0
  60. package/src/helpers/getStatesPicked.ts +8 -5
  61. package/src/helpers/index.ts +11 -3
  62. package/src/helpers/isLegendItemDisabled.ts +16 -0
  63. package/src/helpers/mapObserverHelpers.ts +40 -0
  64. package/src/helpers/resetLegendToggles.ts +3 -2
  65. package/src/helpers/toggleLegendActive.ts +6 -11
  66. package/src/helpers/urlDataHelpers.ts +70 -0
  67. package/src/hooks/useGeoClickHandler.ts +35 -1
  68. package/src/hooks/useLegendMemo.ts +17 -0
  69. package/src/hooks/useMapLayers.tsx +5 -4
  70. package/src/hooks/useStateZoom.tsx +25 -6
  71. package/src/hooks/useTooltip.ts +1 -2
  72. package/src/index.jsx +0 -2
  73. package/src/store/map.reducer.ts +17 -6
  74. package/src/test/CdcMap.test.jsx +11 -0
  75. package/src/types/MapConfig.ts +23 -14
  76. package/src/types/MapContext.ts +0 -7
  77. package/src/types/runtimeLegend.ts +17 -1
  78. package/vite.config.js +2 -7
  79. package/vitest.config.ts +16 -0
  80. package/src/coreStyles_map.scss +0 -3
  81. package/src/helpers/colorDistributions.ts +0 -12
  82. package/src/helpers/generateColorsArray.ts +0 -14
  83. package/src/helpers/tests/generateColorsArray.test.ts +0 -18
  84. package/src/helpers/tests/generateRuntimeLegendHash.test.ts +0 -11
@@ -0,0 +1,306 @@
1
+ {
2
+ "annotations": [],
3
+ "general": {
4
+ "title": "US Cities Bubble Map - CityList Test",
5
+ "subtext": "This bubble map uses CityList component to render bubble sizes based on data values",
6
+ "type": "bubble",
7
+ "geoType": "us",
8
+ "headerColor": "theme-green",
9
+ "showSidebar": true,
10
+ "showTitle": true,
11
+ "showDownloadButton": true,
12
+ "expandDataTable": false,
13
+ "backgroundColor": "#ffffff",
14
+ "geoBorderColor": "darkGray",
15
+ "territoriesLabel": "Territories",
16
+ "language": "en",
17
+ "hasRegions": false,
18
+ "showDownloadMediaButton": false,
19
+ "displayAsHex": false,
20
+ "displayStateLabels": false,
21
+ "fullBorder": false,
22
+ "palette": {
23
+ "isReversed": false
24
+ },
25
+ "allowMapZoom": true,
26
+ "hideGeoColumnInTooltip": false,
27
+ "hidePrimaryColumnInTooltip": false,
28
+ "territoriesAlwaysShow": false,
29
+ "geoLabelOverride": "",
30
+ "noDataFoundMessage": "No data found",
31
+ "convertFipsCodes": false
32
+ },
33
+ "type": "map",
34
+ "color": "orangered",
35
+ "columns": {
36
+ "geo": {
37
+ "name": "city",
38
+ "label": "City",
39
+ "tooltip": true,
40
+ "dataTable": true
41
+ },
42
+ "primary": {
43
+ "dataTable": true,
44
+ "tooltip": true,
45
+ "prefix": "",
46
+ "suffix": " cases",
47
+ "name": "covid_cases",
48
+ "label": "COVID-19 Cases"
49
+ },
50
+ "latitude": {
51
+ "name": "latitude",
52
+ "label": "Latitude"
53
+ },
54
+ "longitude": {
55
+ "name": "longitude",
56
+ "label": "Longitude"
57
+ },
58
+ "navigate": {
59
+ "name": ""
60
+ }
61
+ },
62
+ "legend": {
63
+ "descriptions": {},
64
+ "specialClasses": [],
65
+ "unified": false,
66
+ "singleColumn": true,
67
+ "dynamicDescription": false,
68
+ "type": "equalinterval",
69
+ "numberOfItems": 5,
70
+ "position": "side",
71
+ "title": "COVID-19 Cases",
72
+ "showTitle": true,
73
+ "reversedOrder": false,
74
+ "singleColumnLegend": false,
75
+ "hideBorder": false
76
+ },
77
+ "filters": [],
78
+ "table": {
79
+ "label": "Data Table",
80
+ "expanded": false,
81
+ "limitHeight": false,
82
+ "height": "",
83
+ "caption": "",
84
+ "showDownloadUrl": false,
85
+ "showDataTableLink": true,
86
+ "showFullGeoNameInCSV": false,
87
+ "forceDisplay": true,
88
+ "download": true,
89
+ "indexLabel": "",
90
+ "wrapColumns": false,
91
+ "showDownloadLinkBelow": true
92
+ },
93
+ "tooltips": {
94
+ "appearanceType": "hover",
95
+ "linkLabel": "Learn More",
96
+ "capitalizeLabels": true,
97
+ "opacity": 90
98
+ },
99
+ "runtime": {
100
+ "editorErrorMessage": []
101
+ },
102
+ "visual": {
103
+ "minBubbleSize": 5,
104
+ "maxBubbleSize": 40,
105
+ "extraBubbleBorder": true,
106
+ "cityStyle": "circle",
107
+ "geoCodeCircleSize": 12,
108
+ "showBubbleZeros": true,
109
+ "cityStyleLabel": "City Bubbles",
110
+ "additionalCityStyles": []
111
+ },
112
+ "mapPosition": {
113
+ "coordinates": [0, 39],
114
+ "zoom": 1
115
+ },
116
+ "map": {
117
+ "layers": [],
118
+ "patterns": []
119
+ },
120
+ "hexMap": {
121
+ "type": "",
122
+ "shapeGroups": []
123
+ },
124
+ "data": [
125
+ {
126
+ "city": "New York",
127
+ "latitude": 40.7128,
128
+ "longitude": -74.0060,
129
+ "covid_cases": 2845
130
+ },
131
+ {
132
+ "city": "Los Angeles",
133
+ "latitude": 34.0522,
134
+ "longitude": -118.2437,
135
+ "covid_cases": 2234
136
+ },
137
+ {
138
+ "city": "Chicago",
139
+ "latitude": 41.8781,
140
+ "longitude": -87.6298,
141
+ "covid_cases": 1567
142
+ },
143
+ {
144
+ "city": "Houston",
145
+ "latitude": 29.7604,
146
+ "longitude": -95.3698,
147
+ "covid_cases": 1890
148
+ },
149
+ {
150
+ "city": "Phoenix",
151
+ "latitude": 33.4484,
152
+ "longitude": -112.0740,
153
+ "covid_cases": 1234
154
+ },
155
+ {
156
+ "city": "Philadelphia",
157
+ "latitude": 39.9526,
158
+ "longitude": -75.1652,
159
+ "covid_cases": 1678
160
+ },
161
+ {
162
+ "city": "San Antonio",
163
+ "latitude": 29.4241,
164
+ "longitude": -98.4936,
165
+ "covid_cases": 987
166
+ },
167
+ {
168
+ "city": "San Diego",
169
+ "latitude": 32.7157,
170
+ "longitude": -117.1611,
171
+ "covid_cases": 1456
172
+ },
173
+ {
174
+ "city": "Dallas",
175
+ "latitude": 32.7767,
176
+ "longitude": -96.7970,
177
+ "covid_cases": 1789
178
+ },
179
+ {
180
+ "city": "San Jose",
181
+ "latitude": 37.3382,
182
+ "longitude": -121.8863,
183
+ "covid_cases": 934
184
+ },
185
+ {
186
+ "city": "Austin",
187
+ "latitude": 30.2672,
188
+ "longitude": -97.7431,
189
+ "covid_cases": 876
190
+ },
191
+ {
192
+ "city": "Jacksonville",
193
+ "latitude": 30.3322,
194
+ "longitude": -81.6557,
195
+ "covid_cases": 743
196
+ },
197
+ {
198
+ "city": "Fort Worth",
199
+ "latitude": 32.7555,
200
+ "longitude": -97.3308,
201
+ "covid_cases": 654
202
+ },
203
+ {
204
+ "city": "Columbus",
205
+ "latitude": 39.9612,
206
+ "longitude": -82.9988,
207
+ "covid_cases": 567
208
+ },
209
+ {
210
+ "city": "Charlotte",
211
+ "latitude": 35.2271,
212
+ "longitude": -80.8431,
213
+ "covid_cases": 789
214
+ },
215
+ {
216
+ "city": "San Francisco",
217
+ "latitude": 37.7749,
218
+ "longitude": -122.4194,
219
+ "covid_cases": 1123
220
+ },
221
+ {
222
+ "city": "Indianapolis",
223
+ "latitude": 39.7684,
224
+ "longitude": -86.1581,
225
+ "covid_cases": 445
226
+ },
227
+ {
228
+ "city": "Seattle",
229
+ "latitude": 47.6062,
230
+ "longitude": -122.3321,
231
+ "covid_cases": 823
232
+ },
233
+ {
234
+ "city": "Denver",
235
+ "latitude": 39.7392,
236
+ "longitude": -104.9903,
237
+ "covid_cases": 634
238
+ },
239
+ {
240
+ "city": "Boston",
241
+ "latitude": 42.3601,
242
+ "longitude": -71.0589,
243
+ "covid_cases": 976
244
+ },
245
+ {
246
+ "city": "El Paso",
247
+ "latitude": 31.7619,
248
+ "longitude": -106.4850,
249
+ "covid_cases": 312
250
+ },
251
+ {
252
+ "city": "Nashville",
253
+ "latitude": 36.1627,
254
+ "longitude": -86.7816,
255
+ "covid_cases": 456
256
+ },
257
+ {
258
+ "city": "Detroit",
259
+ "latitude": 42.3314,
260
+ "longitude": -83.0458,
261
+ "covid_cases": 578
262
+ },
263
+ {
264
+ "city": "Oklahoma City",
265
+ "latitude": 35.4676,
266
+ "longitude": -97.5164,
267
+ "covid_cases": 289
268
+ },
269
+ {
270
+ "city": "Portland",
271
+ "latitude": 45.5152,
272
+ "longitude": -122.6784,
273
+ "covid_cases": 387
274
+ },
275
+ {
276
+ "city": "Las Vegas",
277
+ "latitude": 36.1699,
278
+ "longitude": -115.1398,
279
+ "covid_cases": 534
280
+ },
281
+ {
282
+ "city": "Memphis",
283
+ "latitude": 35.1495,
284
+ "longitude": -90.0490,
285
+ "covid_cases": 267
286
+ },
287
+ {
288
+ "city": "Louisville",
289
+ "latitude": 38.2527,
290
+ "longitude": -85.7585,
291
+ "covid_cases": 189
292
+ },
293
+ {
294
+ "city": "Baltimore",
295
+ "latitude": 39.2904,
296
+ "longitude": -76.6122,
297
+ "covid_cases": 445
298
+ },
299
+ {
300
+ "city": "Milwaukee",
301
+ "latitude": 43.0389,
302
+ "longitude": -87.9065,
303
+ "covid_cases": 334
304
+ }
305
+ ]
306
+ }
@@ -3,6 +3,7 @@ import { scaleLinear } from 'd3-scale'
3
3
  import { countryCoordinates } from '../data/country-coordinates'
4
4
  import stateCoordinates from '../data/state-coordinates'
5
5
  import ConfigContext, { MapDispatchContext } from '../context'
6
+ import { useLegendMemoContext } from '../context/LegendMemoContext'
6
7
  import { type Coordinate, DataRow } from '../types/MapConfig'
7
8
  import useApplyTooltipsToGeo from '../hooks/useApplyTooltipsToGeo'
8
9
  import { applyLegendToRow } from '../helpers/applyLegendToRow'
@@ -17,8 +18,8 @@ type BubbleListProps = {
17
18
  }
18
19
 
19
20
  export const BubbleList: React.FC<BubbleListProps> = ({ customProjection }) => {
20
- const { config, tooltipId, legendMemo, legendSpecialClassLastMemo, setRuntimeData, runtimeData, runtimeLegend } =
21
- useContext<MapContext>(ConfigContext)
21
+ const { config, tooltipId, runtimeData, runtimeLegend } = useContext<MapContext>(ConfigContext)
22
+ const { legendMemo, legendSpecialClassLastMemo } = useLegendMemoContext()
22
23
  const { columns, data, general, visual } = config
23
24
  const { geoType, allowMapZoom } = general
24
25
  const { minBubbleSize, maxBubbleSize, showBubbleZeros, extraBubbleBorder } = visual
@@ -65,7 +66,7 @@ export const BubbleList: React.FC<BubbleListProps> = ({ customProjection }) => {
65
66
  dispatch({ type: 'SET_POSITION', payload: { coordinates: reversedCoordinates, zoom: 3 } })
66
67
 
67
68
  // ...and show the data for the clicked country
68
- setRuntimeData(_tempRuntimeData)
69
+ dispatch({ type: 'SET_RUNTIME_DATA', payload: _tempRuntimeData })
69
70
  }
70
71
 
71
72
  const sortedRuntimeData: DataRow = Object.values(runtimeData).sort((a, b) =>
@@ -77,7 +78,7 @@ export const BubbleList: React.FC<BubbleListProps> = ({ customProjection }) => {
77
78
  if (geoType === 'world') {
78
79
  return (
79
80
  sortedRuntimeData &&
80
- sortedRuntimeData.map(country => {
81
+ sortedRuntimeData.map((country, index) => {
81
82
  let coordinates = countryCoordinates[country.uid]
82
83
 
83
84
  if (!coordinates) return true
@@ -99,10 +100,9 @@ export const BubbleList: React.FC<BubbleListProps> = ({ customProjection }) => {
99
100
  if (!projection(coordinates)) return true
100
101
 
101
102
  const circle = (
102
- <>
103
+ <React.Fragment key={`circle-fragment-${countryName.replace(' ', '')}`}>
103
104
  <circle
104
105
  tabIndex={-1}
105
- key={`circle-${countryName.replace(' ', '')}`}
106
106
  className={`bubble country--${countryName}`}
107
107
  cx={Number(projection(coordinates[1], coordinates[0])[0]) || 0}
108
108
  cy={Number(projection(coordinates[1], coordinates[0])[1]) || 0}
@@ -111,6 +111,7 @@ export const BubbleList: React.FC<BubbleListProps> = ({ customProjection }) => {
111
111
  stroke={legendColors[0]}
112
112
  strokeWidth={1.25}
113
113
  fillOpacity={0.4}
114
+ onMouseEnter={() => {}}
114
115
  onPointerDown={e => {
115
116
  pointerX = e.clientX
116
117
  pointerY = e.clientY
@@ -138,7 +139,7 @@ export const BubbleList: React.FC<BubbleListProps> = ({ customProjection }) => {
138
139
  {extraBubbleBorder && (
139
140
  <circle
140
141
  tabIndex={-1}
141
- key={`circle-${countryName.replace(' ', '')}`}
142
+ key={`circle-border-${countryName.replace(' ', '')}`}
142
143
  className='bubble'
143
144
  cx={Number(projection(coordinates[1], coordinates[0])[0]) || 0}
144
145
  cy={Number(projection(coordinates[1], coordinates[0])[1]) || 0}
@@ -146,6 +147,7 @@ export const BubbleList: React.FC<BubbleListProps> = ({ customProjection }) => {
146
147
  fill={'transparent'}
147
148
  stroke={'white'}
148
149
  strokeWidth={0.5}
150
+ onMouseEnter={() => {}}
149
151
  onPointerDown={e => {
150
152
  pointerX = e.clientX
151
153
  pointerY = e.clientY
@@ -170,11 +172,11 @@ export const BubbleList: React.FC<BubbleListProps> = ({ customProjection }) => {
170
172
  data-tooltip-html={toolTip}
171
173
  />
172
174
  )}
173
- </>
175
+ </React.Fragment>
174
176
  )
175
177
 
176
178
  return (
177
- <g key={`group-${countryName.replace(' ', '')}`} tabIndex={-1}>
179
+ <g key={`group-${index}-${countryName.replace(' ', '')}`} tabIndex={-1}>
178
180
  {circle}
179
181
  </g>
180
182
  )
@@ -185,7 +187,7 @@ export const BubbleList: React.FC<BubbleListProps> = ({ customProjection }) => {
185
187
  if (geoType === 'us') {
186
188
  return (
187
189
  sortedRuntimeData &&
188
- sortedRuntimeData.map(item => {
190
+ sortedRuntimeData.map((item, index) => {
189
191
  let stateData = stateCoordinates[item.uid]
190
192
  if (Number(size(item[primaryColumnName])) === 0) return
191
193
 
@@ -223,6 +225,7 @@ export const BubbleList: React.FC<BubbleListProps> = ({ customProjection }) => {
223
225
  stroke={legendColors[0]}
224
226
  strokeWidth={1.25}
225
227
  fillOpacity={0.4}
228
+ onMouseEnter={() => {}}
226
229
  onPointerDown={e => {
227
230
  pointerX = e.clientX
228
231
  pointerY = e.clientY
@@ -249,7 +252,7 @@ export const BubbleList: React.FC<BubbleListProps> = ({ customProjection }) => {
249
252
  {extraBubbleBorder && (
250
253
  <circle
251
254
  tabIndex={-1}
252
- key={`circle-${stateName.replace(' ', '')}`}
255
+ key={`circle-border-${stateName.replace(' ', '')}`}
253
256
  className='bubble'
254
257
  cx={projection(coordinates)[0] || 0}
255
258
  cy={projection(coordinates)[1] || 0}
@@ -258,6 +261,7 @@ export const BubbleList: React.FC<BubbleListProps> = ({ customProjection }) => {
258
261
  stroke={'white'}
259
262
  strokeWidth={0.5}
260
263
  fillOpacity={0.4}
264
+ onMouseEnter={() => {}}
261
265
  onPointerDown={e => {
262
266
  pointerX = e.clientX
263
267
  pointerY = e.clientY
@@ -285,7 +289,7 @@ export const BubbleList: React.FC<BubbleListProps> = ({ customProjection }) => {
285
289
  </>
286
290
  )
287
291
 
288
- return <g key={`group-${stateName.replace(' ', '')}`}>{circle}</g>
292
+ return <g key={`group-${index}-${stateName.replace(' ', '')}`}>{circle}</g>
289
293
  })
290
294
  )
291
295
  }
@@ -1,10 +1,11 @@
1
- import { useContext, useEffect, useState } from 'react'
1
+ import { useContext, useMemo } from 'react'
2
2
  import { scaleLinear } from 'd3-scale'
3
3
  import { GlyphCircle, GlyphDiamond, GlyphSquare, GlyphStar, GlyphTriangle } from '@visx/glyph'
4
4
  import ConfigContext from '../context'
5
+ import { useLegendMemoContext } from '../context/LegendMemoContext'
5
6
  import { supportedCities } from '../data/supported-geos'
6
7
  import { getFilterControllingStatesPicked } from './UsaMap/helpers/map'
7
- import { displayGeoName, getGeoStrokeColor, SVG_HEIGHT, SVG_PADDING, SVG_WIDTH, titleCase } from '../helpers'
8
+ import { displayGeoName, getGeoStrokeColor, SVG_HEIGHT, SVG_PADDING, SVG_WIDTH, isLegendItemDisabled } from '../helpers'
8
9
  import useGeoClickHandler from '../hooks/useGeoClickHandler'
9
10
  import useApplyTooltipsToGeo from '../hooks/useApplyTooltipsToGeo'
10
11
  import { applyLegendToRow } from '../helpers/applyLegendToRow'
@@ -18,72 +19,86 @@ type CityListProps = {
18
19
  }
19
20
 
20
21
  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)
22
+ const { config, topoData, runtimeData, position, runtimeLegend } = useContext(ConfigContext)
23
+ const { legendMemo, legendSpecialClassLastMemo } = useLegendMemoContext()
30
24
  const { geoClickHandler } = useGeoClickHandler()
31
25
  const { applyTooltipsToGeo } = useApplyTooltipsToGeo()
32
26
 
33
- const { geoColumnName, latitudeColumnName, longitudeColumnName, primaryColumnName } = getColumnNames(config.columns)
34
- const { additionalCityStyles } = config.visual || []
27
+ const { geoColumnName, latitudeColumnName, longitudeColumnName, primaryColumnName } =
28
+ getColumnNames(config.columns) || {}
35
29
 
36
- if (!projection) return
30
+ // Memoize expensive city data creation
31
+ const citiesData = useMemo(() => {
32
+ if (!runtimeData) return {}
37
33
 
38
- const citiesData = runtimeData
39
- ? Object.keys(runtimeData).reduce((acc, key) => {
40
- const city = runtimeData[key]
34
+ return Object.keys(runtimeData).reduce((acc, key) => {
35
+ const city = runtimeData[key]
36
+ if (city && city[geoColumnName]) {
41
37
  acc[city[geoColumnName]] = city
42
- return acc
43
- }, {})
44
- : {}
38
+ }
39
+ return acc
40
+ }, {})
41
+ }, [runtimeData, geoColumnName])
42
+
43
+ // Memoize bubble size calculation
44
+ const size = useMemo(() => {
45
+ if (config.general.type !== 'bubble' || !runtimeData) {
46
+ return null
47
+ }
45
48
 
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
52
- )
53
- if (!sortedRuntimeData) return
49
+ const maxVal = Math.max(...Object.keys(runtimeData).map(key => runtimeData[key][config.columns.primary.name]))
50
+
51
+ if (maxVal <= 0) {
52
+ return null
53
+ }
54
+
55
+ return scaleLinear().domain([1, maxVal]).range([config.visual.minBubbleSize, config.visual.maxBubbleSize])
56
+ }, [
57
+ config.general.type,
58
+ config.columns.primary.name,
59
+ config.visual.minBubbleSize,
60
+ config.visual.maxBubbleSize,
61
+ runtimeData
62
+ ])
63
+
64
+ // Get the list of cities to render
65
+ const cityList = useMemo(() => {
66
+ return Object.keys(citiesData).filter(cityName => cityName && citiesData[cityName])
67
+ }, [citiesData])
68
+
69
+ // Early exit for map types that don't use city rendering
70
+ if (!projection) {
71
+ return null
72
+ }
54
73
 
55
- // Set bubble sizes
56
- var size = scaleLinear().domain([1, maxDataValue]).range([config.visual.minBubbleSize, config.visual.maxBubbleSize])
74
+ // Early exit if no cities to render
75
+ if (!cityList.length) {
76
+ return null
57
77
  }
58
- const cityList = Object.keys(citiesData).filter(c => undefined !== c || undefined !== runtimeData[c])
59
- if (!cityList) return true
60
78
 
61
79
  // Cities output
62
80
  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]
68
- }
69
- })
70
- }
81
+ // Get the city data directly from our memoized citiesData
82
+ const geoData = citiesData[city]
83
+
71
84
  if (!geoData) {
72
- geoData = runtimeData ? runtimeData[city] : undefined
85
+ return null
73
86
  }
74
- const cityDisplayName = titleCase(displayGeoName(city))
75
87
 
76
- const legendColors = geoData
77
- ? applyLegendToRow(geoData, config, runtimeLegend, legendMemo, legendSpecialClassLastMemo)
78
- : runtimeData[city]
79
- ? applyLegendToRow(runtimeData[city], config, runtimeLegend, legendMemo, legendSpecialClassLastMemo)
80
- : false
88
+ const cityDisplayName = displayGeoName(city)
81
89
 
82
- if (legendColors === false) {
83
- return true
90
+ const legendColors = applyLegendToRow(geoData, config, runtimeLegend, legendMemo, legendSpecialClassLastMemo)
91
+
92
+ if (!legendColors || legendColors.length === 0) {
93
+ return null
94
+ }
95
+
96
+ // Don't render if legend item is disabled
97
+ if (isLegendItemDisabled(geoData, runtimeLegend, legendMemo, legendSpecialClassLastMemo, config)) {
98
+ return null
84
99
  }
85
100
 
86
- const toolTip = applyTooltipsToGeo(cityDisplayName, geoData || runtimeData[city])
101
+ const toolTip = applyTooltipsToGeo(cityDisplayName, geoData)
87
102
 
88
103
  const radius = config.visual.geoCodeCircleSize || 8
89
104
 
@@ -94,19 +109,21 @@ const CityList: React.FC<CityListProps> = ({ setSharedFilterValue, isFilterValue
94
109
  const geoStrokeColor = getGeoStrokeColor(config)
95
110
 
96
111
  const pin = (
97
- <path
98
- className='marker'
99
- d='M0,0l-8.8-17.7C-12.1-24.3-7.4-32,0-32h0c7.4,0,12.1,7.7,8.8,14.3L0,0z'
100
- title='Select for more information'
101
- onClick={() => geoClickHandler(cityDisplayName, geoData)}
102
- data-tooltip-id={`tooltip__${tooltipId}`}
103
- data-tooltip-html={toolTip}
104
- transform={`scale(${radius / 7.5})`}
105
- stroke={geoStrokeColor}
106
- strokeWidth={'2px'}
107
- tabIndex='-1'
108
- {...additionalProps}
109
- />
112
+ <g>
113
+ <title>Select for more information</title>
114
+ <path
115
+ className='marker'
116
+ d='M0,0l-8.8-17.7C-12.1-24.3-7.4-32,0-32h0c7.4,0,12.1,7.7,8.8,14.3L0,0z'
117
+ onClick={() => geoClickHandler(cityDisplayName, geoData)}
118
+ data-tooltip-id={`tooltip__${tooltipId}`}
119
+ data-tooltip-html={toolTip}
120
+ transform={`scale(${radius / 7.5})`}
121
+ stroke={geoStrokeColor}
122
+ strokeWidth={'2px'}
123
+ tabIndex={-1}
124
+ {...additionalProps}
125
+ />
126
+ </g>
110
127
  )
111
128
 
112
129
  let transform = ''
@@ -130,7 +147,7 @@ const CityList: React.FC<CityListProps> = ({ setSharedFilterValue, isFilterValue
130
147
 
131
148
  if (geoData?.[longitudeColumnName] && geoData?.[latitudeColumnName] && config.general.geoType === 'single-state') {
132
149
  const statesPicked = getFilterControllingStatesPicked(config, runtimeData)
133
- const _statesPickedData = topoData?.states?.find(s => statesPicked.includes(s.properties.name))
150
+ const _statesPickedData = (topoData as any)?.states?.find(s => statesPicked.includes(s.properties.name))
134
151
 
135
152
  const newProjection = projection.fitExtent(
136
153
  [
@@ -141,7 +158,7 @@ const CityList: React.FC<CityListProps> = ({ setSharedFilterValue, isFilterValue
141
158
  )
142
159
  let coords = [Number(geoData?.[longitudeColumnName]), Number(geoData?.[latitudeColumnName])]
143
160
  transform = `translate(${newProjection(coords)}) scale(${
144
- config.visual.geoCodeCircleSize / (position.zoom > 1 ? position.zoom : 1)
161
+ config.visual.geoCodeCircleSize / ((position as any).zoom > 1 ? (position as any).zoom : 1)
145
162
  })`
146
163
  needsPointer = true
147
164
  }
@@ -181,8 +198,7 @@ const CityList: React.FC<CityListProps> = ({ setSharedFilterValue, isFilterValue
181
198
 
182
199
  const shapeProps = {
183
200
  onClick: () => geoClickHandler(cityDisplayName, geoData),
184
- size: config.general.type === 'bubble' ? size(geoData[primaryColumnName]) : radius * 30,
185
- title: 'Select for more information',
201
+ size: config.general.type === 'bubble' && size ? size(geoData[primaryColumnName]) : radius * 30,
186
202
  'data-tooltip-id': `tooltip__${tooltipId}`,
187
203
  'data-tooltip-html': toolTip,
188
204
  stroke: geoStrokeColor,
@@ -200,48 +216,10 @@ const CityList: React.FC<CityListProps> = ({ setSharedFilterValue, isFilterValue
200
216
  triangle: <GlyphTriangle {...shapeProps} />
201
217
  }
202
218
 
203
- const cityStyle = Object.values(runtimeData)
204
- .filter(d => additionalCityStyles?.some(style => String(d[style.column]) === String(style.value)))
205
- .map(d => {
206
- const conditionsMatched = additionalCityStyles.find(style => String(d[style.column]) === String(style.value))
207
- return { ...conditionsMatched, ...d }
208
- })
209
- .find(item => {
210
- return Object.keys(item).find(key => item[key] === city)
211
- })
212
-
213
- if (cityStyle !== undefined && cityStyle.shape) {
214
- if (
215
- !geoData?.[longitudeColumnName] &&
216
- !geoData?.[latitudeColumnName] &&
217
- city &&
218
- supportedCities[city.toUpperCase()]
219
- ) {
220
- let translate = `translate(${projection(supportedCities[city.toUpperCase()])})`
221
-
222
- return (
223
- <g key={i} transform={translate} style={styles} className='geo-point' tabIndex={-1}>
224
- {cityStyleShapes[cityStyle.shape.toLowerCase()]}
225
- </g>
226
- )
227
- }
228
-
229
- if (geoData?.[longitudeColumnName] && geoData?.[latitudeColumnName]) {
230
- const coords = [Number(geoData?.[longitudeColumnName]), Number(geoData?.[latitudeColumnName])]
231
- let translate = `translate(${projection(coords)})`
232
-
233
- return (
234
- <g key={i} transform={translate} style={styles} className='geo-point' tabIndex={-1}>
235
- {cityStyleShapes[cityStyle.shape.toLowerCase()]}
236
- </g>
237
- )
238
- }
239
- }
240
- if (legendColors?.[0] === '#000000') return
241
-
219
+ // Render the city marker
242
220
  return (
243
221
  <g key={i} transform={transform} style={styles} className='geo-point' tabIndex={-1}>
244
- {cityStyleShapes[config.visual.cityStyle.toLowerCase()]}
222
+ {cityStyleShapes[config.visual.cityStyle?.toLowerCase() || 'circle']}
245
223
  </g>
246
224
  )
247
225
  })