@cdc/map 4.26.3 → 4.26.4

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 (79) hide show
  1. package/CONFIG.md +235 -0
  2. package/README.md +70 -24
  3. package/dist/cdcmap-CY9IcPSi.es.js +6 -0
  4. package/dist/cdcmap-DlpiY3fQ.es.js +4 -0
  5. package/dist/cdcmap.js +27405 -26257
  6. package/examples/{testing-layer-2.json → __data__/testing-layer-2.json} +1 -1
  7. package/examples/{testing-layer.json → __data__/testing-layer.json} +1 -1
  8. package/examples/county-hsa-toggle.json +51993 -0
  9. package/examples/custom-map-layers.json +2 -2
  10. package/examples/default-county.json +3 -3
  11. package/examples/minimal-example.json +69 -0
  12. package/examples/private/annotation-bug.json +2 -2
  13. package/examples/private/css-issue.json +314 -0
  14. package/examples/private/region-breaking.json +1639 -0
  15. package/examples/private/test1.json +27247 -0
  16. package/package.json +4 -4
  17. package/src/CdcMapComponent.tsx +96 -13
  18. package/src/_stories/CdcMap.Editor.ColumnsSectionTests.stories.tsx +601 -0
  19. package/src/_stories/CdcMap.Editor.DataTableSectionTests.stories.tsx +404 -0
  20. package/src/_stories/CdcMap.Editor.FiltersSectionTests.stories.tsx +229 -0
  21. package/src/_stories/CdcMap.Editor.GeneralSectionTests.stories.tsx +262 -0
  22. package/src/_stories/CdcMap.Editor.LegendSectionTests.stories.tsx +541 -0
  23. package/src/_stories/CdcMap.Editor.MultiCountryWorldMapTests.stories.tsx +359 -0
  24. package/src/_stories/CdcMap.Editor.PatternSettingsSectionTests.stories.tsx +516 -0
  25. package/src/_stories/CdcMap.Editor.SmallMultiplesSectionTests.stories.tsx +165 -0
  26. package/src/_stories/CdcMap.Editor.TextAnnotationsSectionTests.stories.tsx +145 -0
  27. package/src/_stories/CdcMap.Editor.TypeSectionTests.stories.tsx +312 -0
  28. package/src/_stories/CdcMap.Editor.VisualSectionTests.stories.tsx +359 -0
  29. package/src/_stories/CdcMap.Editor.ZoomControlsTests.stories.tsx +88 -0
  30. package/src/_stories/{CdcMap.stories.tsx → CdcMap.smoke.stories.tsx} +12 -0
  31. package/src/_stories/_mock/legends/legend-tests.json +3 -3
  32. package/src/components/Annotation/AnnotationList.tsx +1 -1
  33. package/src/components/EditorPanel/components/EditorPanel.tsx +504 -383
  34. package/src/components/EditorPanel/components/HexShapeSettings.tsx +1 -1
  35. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +112 -117
  36. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +26 -13
  37. package/src/components/EditorPanel/components/editorPanel.styles.css +22 -2
  38. package/src/components/Legend/components/Legend.tsx +3 -3
  39. package/src/components/Legend/components/LegendItem.Hex.tsx +4 -2
  40. package/src/components/SmallMultiples/SynchronizedTooltip.tsx +1 -1
  41. package/src/components/UsaMap/components/UsaMap.County.tsx +271 -100
  42. package/src/components/UsaMap/components/UsaMap.State.tsx +1 -1
  43. package/src/components/UsaMap/data/cb_2019_us_county_20m.json +75817 -1
  44. package/src/components/UsaMap/data/hsa_fips_mapping.json +3144 -0
  45. package/src/components/WorldMap/data/world-topo.json +1 -1
  46. package/src/data/initial-state.js +1 -0
  47. package/src/data/supported-counties.json +1 -1
  48. package/src/helpers/countyTerritories.ts +38 -0
  49. package/src/helpers/dataTableHelpers.ts +35 -6
  50. package/src/helpers/tests/countyTerritories.test.ts +87 -0
  51. package/src/hooks/useApplyTooltipsToGeo.tsx +7 -4
  52. package/src/hooks/useMapLayers.tsx +1 -1
  53. package/src/hooks/useTooltip.ts +18 -7
  54. package/src/store/map.actions.ts +5 -2
  55. package/src/store/map.reducer.ts +12 -3
  56. package/src/test/CdcMap.test.jsx +24 -0
  57. package/src/types/MapConfig.ts +6 -0
  58. package/src/types/MapContext.ts +3 -1
  59. package/topojson-updater/README.txt +1 -1
  60. package/LICENSE +0 -201
  61. package/dist/cdcmap-vr9HZwRt.es.js +0 -6
  62. package/examples/__data__/city-state-data.json +0 -668
  63. package/examples/city-state.json +0 -434
  64. package/examples/default-world-data.json +0 -1450
  65. package/examples/new-cities.json +0 -656
  66. package/src/_stories/CdcMap.Editor.stories.tsx +0 -3648
  67. package/topojson-updater/package-lock.json +0 -223
  68. /package/src/_stories/{CdcMap.ColumnWrap.stories.tsx → CdcMap.ColumnWrap.smoke.stories.tsx} +0 -0
  69. /package/src/_stories/{CdcMap.Defaults.stories.tsx → CdcMap.Defaults.smoke.stories.tsx} +0 -0
  70. /package/src/_stories/{CdcMap.DistrictOfColumbia.stories.tsx → CdcMap.DistrictOfColumbia.smoke.stories.tsx} +0 -0
  71. /package/src/_stories/{CdcMap.Filters.stories.tsx → CdcMap.Filters.smoke.stories.tsx} +0 -0
  72. /package/src/_stories/{CdcMap.Legend.Gradient.stories.tsx → CdcMap.Legend.Gradient.smoke.stories.tsx} +0 -0
  73. /package/src/_stories/{CdcMap.Legend.stories.tsx → CdcMap.Legend.smoke.stories.tsx} +0 -0
  74. /package/src/_stories/{CdcMap.Patterns.stories.tsx → CdcMap.Patterns.smoke.stories.tsx} +0 -0
  75. /package/src/_stories/{CdcMap.SmallMultiples.stories.tsx → CdcMap.SmallMultiples.smoke.stories.tsx} +0 -0
  76. /package/src/_stories/{CdcMap.Table.stories.tsx → CdcMap.Table.smoke.stories.tsx} +0 -0
  77. /package/src/_stories/{CdcMap.ZeroColor.stories.tsx → CdcMap.ZeroColor.smoke.stories.tsx} +0 -0
  78. /package/src/_stories/{GoogleMap.stories.tsx → GoogleMap.smoke.stories.tsx} +0 -0
  79. /package/src/_stories/{UsaMap.NoData.stories.tsx → UsaMap.NoData.smoke.stories.tsx} +0 -0
@@ -1,23 +1,19 @@
1
- import { useEffect, useState, useRef, useContext } from 'react'
1
+ import { useEffect, useState, useRef, useContext, useMemo } from 'react'
2
2
  import { geoCentroid, geoPath, geoContains } from 'd3-geo'
3
+ import type { GeoGeometryObjects } from 'd3-geo'
3
4
  import { zoom as d3Zoom, zoomIdentity as d3ZoomIdentity } from 'd3-zoom'
4
5
  import { select as d3Select } from 'd3-selection'
5
- import { feature } from 'topojson-client'
6
+ import { feature, merge } from 'topojson-client'
6
7
  import { geoAlbersUsaTerritories } from 'd3-composite-projections'
7
8
  import debounce from 'lodash.debounce'
8
9
  import Loading from '@cdc/core/components/Loading'
9
10
  import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
11
+ import usExtendedGeography from '../data/us-extended-geography.json'
10
12
  import useMapLayers from '../../../hooks/useMapLayers'
11
13
  import ConfigContext from '../../../context'
12
14
  import { useLegendMemoContext } from '../../../context/LegendMemoContext'
13
15
  import { drawShape, createShapeProperties } from '../helpers/shapes'
14
- import {
15
- getGeoStrokeColor,
16
- handleMapAriaLabels,
17
- displayGeoName,
18
- isLegendItemDisabled,
19
- MAX_ZOOM_LEVEL
20
- } from '../../../helpers'
16
+ import { getGeoStrokeColor, handleMapAriaLabels, displayGeoName, isLegendItemDisabled } from '../../../helpers'
21
17
  import { supportedStatesFipsCodes } from '../../../data/supported-geos'
22
18
  import useGeoClickHandler from '../../../hooks/useGeoClickHandler'
23
19
  import { applyLegendToRow } from '../../../helpers/applyLegendToRow'
@@ -28,6 +24,40 @@ import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
28
24
  import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
29
25
  import { createCanvasPattern, PatternType } from '../../../helpers/createCanvasPattern'
30
26
  import { getPatternForRow } from '../../../helpers/getPatternForRow'
27
+ import {
28
+ getCountyTerritoryVisibility,
29
+ type CountyTerritoryVisibility,
30
+ US_TERRITORY_STATE_FIPS_PREFIXES
31
+ } from '../../../helpers/countyTerritories'
32
+
33
+ type Geometry = GeoGeometryObjects & { id?: string; properties: { name: string } }
34
+ type Focus = {
35
+ id: string
36
+ index: number
37
+ center: [number, number]
38
+ feature: Geometry
39
+ }
40
+ type TopoData = {
41
+ year?: string
42
+ counties: Geometry[]
43
+ states: Geometry[]
44
+ hsas: { groupId: string; feature: Geometry }[]
45
+ mapData: Geometry[]
46
+ countyIndecies: Record<string, [number, number]>
47
+ projection: any
48
+ hsaMapping: Record<string, string>
49
+ }
50
+
51
+ const dedupeFeaturesById = <T extends { id?: string }>(features: T[]): T[] => {
52
+ const seenIds = new Set<string>()
53
+
54
+ return features.filter(feature => {
55
+ if (!feature.id) return true
56
+ if (seenIds.has(feature.id)) return false
57
+ seenIds.add(feature.id)
58
+ return true
59
+ })
60
+ }
31
61
 
32
62
  const getCountyTopoURL = year => {
33
63
  return `https://www.cdc.gov/TemplatePackage/contrib/data/county-topography/cb_${year}_us_county_20m.json`
@@ -39,7 +69,7 @@ const sortById = (a, b) => {
39
69
  return 0
40
70
  }
41
71
 
42
- const getTopoData = year => {
72
+ const getTopoData = (year, showHSABoundaries, territoryVisibility: CountyTerritoryVisibility, showPuertoRico) => {
43
73
  return new Promise(resolve => {
44
74
  const resolveWithTopo = async response => {
45
75
  if (response.status !== 200) {
@@ -48,28 +78,91 @@ const getTopoData = year => {
48
78
  response = await response.json()
49
79
  }
50
80
 
51
- let topoData = {
81
+ let topoData: TopoData = {
52
82
  year: undefined,
53
- counties: undefined,
54
- states: undefined,
55
- mapData: undefined,
56
- countyIndecies: undefined,
57
- projection: undefined
83
+ counties: [],
84
+ states: [],
85
+ hsas: [],
86
+ mapData: [],
87
+ countyIndecies: {},
88
+ projection: undefined,
89
+ hsaMapping: {}
58
90
  }
59
91
 
60
92
  topoData.year = year || 'default'
61
- topoData.counties = feature(response, response.objects.counties).features
62
- topoData.states = feature(response, response.objects.states).features
93
+ const topoSources = territoryVisibility.showTerritories ? [response, usExtendedGeography] : [response]
94
+ topoData.counties = dedupeFeaturesById(
95
+ topoSources
96
+ .flatMap(topo => feature(topo, topo.objects.counties).features)
97
+ .filter(county => typeof county.id === 'string' && county.id.length > 2)
98
+ )
99
+ topoData.states = dedupeFeaturesById(topoSources.flatMap(topo => feature(topo, topo.objects.states).features))
100
+ // Additional filtering removes territory features that may still be present in the topology data when territories are hidden.
101
+ if (!territoryVisibility.showTerritories) {
102
+ topoData.states = topoData.states.filter(state => {
103
+ const statePrefix = state.id?.substring(0, 2)
104
+ if (statePrefix === '72') return showPuertoRico
105
+ return !statePrefix || !US_TERRITORY_STATE_FIPS_PREFIXES.has(statePrefix)
106
+ })
107
+ topoData.counties = topoData.counties.filter(county => {
108
+ const countyPrefix = county.id?.substring(0, 2)
109
+ if (countyPrefix === '72') return showPuertoRico
110
+ if (showPuertoRico) {
111
+ return !countyPrefix || !US_TERRITORY_STATE_FIPS_PREFIXES.has(countyPrefix)
112
+ }
113
+
114
+ return !countyPrefix || !US_TERRITORY_STATE_FIPS_PREFIXES.has(countyPrefix)
115
+ })
116
+ } else {
117
+ topoData.states = topoData.states.filter(state => {
118
+ const statePrefix = state.id?.substring(0, 2)
119
+ return (
120
+ !statePrefix ||
121
+ !US_TERRITORY_STATE_FIPS_PREFIXES.has(statePrefix) ||
122
+ territoryVisibility.statePrefixes.has(statePrefix)
123
+ )
124
+ })
125
+ topoData.counties = topoData.counties.filter(county => {
126
+ const countyPrefix = county.id?.substring(0, 2)
127
+ return (
128
+ !countyPrefix ||
129
+ !US_TERRITORY_STATE_FIPS_PREFIXES.has(countyPrefix) ||
130
+ territoryVisibility.countyIds.has(county.id)
131
+ )
132
+ })
133
+ }
134
+ if (showHSABoundaries) {
135
+ const mappingResponse = await import(
136
+ /* webpackChunkName: "hsa_fips_mapping" */ './../data/hsa_fips_mapping.json'
137
+ )
138
+ const hsaMapping = mappingResponse.default.reduce((acc, curr) => {
139
+ acc[curr.county_fips] = curr.hsa_no
140
+ return acc
141
+ }, {} as Record<string, string>)
142
+ const countyGeometries = response.objects.counties.geometries
143
+ const geometriesByGroup = countyGeometries.reduce((acc, geometry) => {
144
+ const groupId = hsaMapping[geometry.id]
145
+ if (!groupId) return acc
146
+
147
+ if (!acc[groupId]) acc[groupId] = []
148
+ acc[groupId].push(geometry)
149
+ return acc
150
+ }, {} as Record<string, any[]>)
151
+ topoData.hsas = Object.entries(geometriesByGroup).map(([groupId, geometries]) => ({
152
+ groupId,
153
+ feature: merge(response as any, geometries)
154
+ }))
155
+ topoData.hsaMapping = hsaMapping
156
+ }
63
157
  topoData.states.sort(sortById)
64
158
  topoData.counties.sort(sortById)
65
159
  topoData.mapData = topoData.states.concat(topoData.counties).filter(geo => geo.id !== '51620') //Not sure why, but Franklin City, VA is very broken and messes up the rendering
66
- topoData.countyIndecies = {}
67
160
  topoData.states.forEach(state => {
68
161
  let minIndex = topoData.mapData.length - 1
69
162
  let maxIndex = 0
70
163
 
71
164
  for (let i = 0; i < topoData.mapData.length; i++) {
72
- if (topoData.mapData[i].id.length > 2 && topoData.mapData[i].id.indexOf(state.id) === 0) {
165
+ if (topoData.mapData[i].id?.length > 2 && topoData.mapData[i].id?.indexOf(state.id) === 0) {
73
166
  if (i < minIndex) minIndex = i
74
167
  if (i > maxIndex) maxIndex = i
75
168
  }
@@ -78,7 +171,6 @@ const getTopoData = year => {
78
171
  topoData.countyIndecies[state.id] = [minIndex, maxIndex]
79
172
  })
80
173
  topoData.projection = geoAlbersUsaTerritories()
81
-
82
174
  resolve(topoData)
83
175
  }
84
176
 
@@ -135,8 +227,7 @@ const getCurrentTopoYear = (config: MapConfig, runtimeFilters) => {
135
227
  }
136
228
 
137
229
  const isTopoReady = (topoData, config: MapConfig, runtimeFilters) => {
138
- let currentYear = getCurrentTopoYear(config, runtimeFilters)
139
-
230
+ const currentYear = getCurrentTopoYear(config, runtimeFilters)
140
231
  return topoData.year && (!currentYear || currentYear === topoData.year)
141
232
  }
142
233
 
@@ -148,7 +239,9 @@ const CountyMap = () => {
148
239
  runtimeFilters,
149
240
  runtimeLegend,
150
241
  setConfig,
151
- setFilteredStateCode,
242
+ filteredStateCode,
243
+ setFilteredStateCountyCode,
244
+ filteredCountyCode,
152
245
  config,
153
246
  tooltipId,
154
247
  tooltipRef,
@@ -161,13 +254,27 @@ const CountyMap = () => {
161
254
  const geoStrokeColor = getGeoStrokeColor(config)
162
255
  const { geoClickHandler } = useGeoClickHandler()
163
256
  const { applyTooltipsToGeo } = useApplyTooltipsToGeo()
164
- const [focus, setFocus] = useState({})
165
- const [topoData, setTopoData] = useState({})
257
+ const [topoData, setTopoData] = useState<TopoData>({} as TopoData)
258
+ const focus = useMemo(() => {
259
+ if (!isTopoReady(topoData, config, runtimeFilters) || !filteredStateCode) return {} as Focus
260
+ const stateGeo = topoData.states.find(state => state.id === filteredStateCode)
261
+ if (!stateGeo) return {} as Focus
262
+ return {
263
+ id: stateGeo.id,
264
+ index: topoData.mapData.findIndex(geo => geo.id === stateGeo.id),
265
+ center: geoCentroid(stateGeo),
266
+ feature: stateGeo
267
+ } as Focus
268
+ }, [filteredStateCode, topoData, config, runtimeFilters])
166
269
  const [hasMoved, setHasMoved] = useState(false)
167
270
 
168
271
  const pathGenerator = geoPath().projection(geoAlbersUsaTerritories())
169
272
 
170
273
  const { featureArray } = useMapLayers(config, setConfig, pathGenerator, tooltipId)
274
+ const territoryVisibility = useMemo(
275
+ () => getCountyTerritoryVisibility(config.general.territoriesAlwaysShow, runtimeData),
276
+ [config.general.territoriesAlwaysShow, runtimeData]
277
+ )
171
278
 
172
279
  useEffect(() => {
173
280
  if (containerEl) {
@@ -177,27 +284,53 @@ const CountyMap = () => {
177
284
  }
178
285
  })
179
286
 
287
+ const getAndSetTopoData = currentYear => {
288
+ getTopoData(
289
+ currentYear,
290
+ config.general.showHSABoundaries,
291
+ territoryVisibility,
292
+ config.migrations.showPuertoRico
293
+ ).then(response => {
294
+ if (canvasRef.current) {
295
+ const context = canvasRef.current.getContext('2d') as CanvasRenderingContext2D
296
+ context.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height)
297
+ }
298
+ setTopoData(response)
299
+ })
300
+ }
301
+
180
302
  useEffect(() => {
181
- let currentYear = getCurrentTopoYear(config, runtimeFilters)
303
+ const currentYear = getCurrentTopoYear(config, runtimeFilters)
182
304
 
183
305
  if (currentYear !== topoData.year) {
184
- getTopoData(currentYear).then(response => {
185
- if (canvasRef.current) {
186
- const context = canvasRef.current.getContext('2d')
187
- context.clearRect(canvasRef.current.width, canvasRef.current.height)
188
- }
189
- setTopoData(response)
190
- })
306
+ getAndSetTopoData(currentYear)
191
307
  }
192
- }, [config.general.countyCensusYear, config.general.filterControlsCountyYear, JSON.stringify(runtimeFilters)])
308
+ }, [
309
+ config.general.countyCensusYear,
310
+ config.general.filterControlsCountyYear,
311
+ JSON.stringify(runtimeFilters),
312
+ territoryVisibility.key
313
+ ])
314
+
315
+ const prevShowHSABoundariesRef = useRef(config.general.showHSABoundaries)
316
+ useEffect(() => {
317
+ if (prevShowHSABoundariesRef.current === config.general.showHSABoundaries) return
318
+ const currentYear = getCurrentTopoYear(config, runtimeFilters)
319
+ getAndSetTopoData(currentYear)
320
+ prevShowHSABoundariesRef.current = config.general.showHSABoundaries
321
+ }, [config.general.showHSABoundaries])
322
+
323
+ const prevTerritoryVisibilityRef = useRef(territoryVisibility.key)
324
+ useEffect(() => {
325
+ if (prevTerritoryVisibilityRef.current === territoryVisibility.key) return
326
+ const currentYear = getCurrentTopoYear(config, runtimeFilters)
327
+ getAndSetTopoData(currentYear)
328
+ prevTerritoryVisibilityRef.current = territoryVisibility.key
329
+ }, [territoryVisibility.key])
193
330
 
194
331
  // Whenever the memo at the top is triggered and the map is called to re-render, call drawCanvas and update
195
332
  // The resize function so it includes the latest state variables
196
333
  useEffect(() => {
197
- if (isTopoReady(topoData, config, runtimeFilters)) {
198
- drawCanvas()
199
- }
200
-
201
334
  const onResize = () => {
202
335
  if (canvasRef.current && isTopoReady(topoData, config, runtimeFilters)) {
203
336
  drawCanvas()
@@ -240,6 +373,11 @@ const CountyMap = () => {
240
373
  const d = pathGen(state)
241
374
  if (d) cache.set('state_border_' + state.id, new Path2D(d))
242
375
  })
376
+ topoData.hsas.forEach(hsa => {
377
+ if (!hsa?.groupId || !hsa?.feature) return
378
+ const d = pathGen(hsa.feature as any)
379
+ if (d) cache.set('hsa_border_' + hsa.groupId, new Path2D(d))
380
+ })
243
381
  geoPathCacheRef.current = cache
244
382
  }
245
383
 
@@ -266,7 +404,14 @@ const CountyMap = () => {
266
404
 
267
405
  const getZoomScale = () => zoomTransformRef.current?.k || 1
268
406
 
269
- const paintCountyGeo = (context, path2d: Path2D, geoData, canvasWidth: number, strokeWidth?: number) => {
407
+ const paintCountyGeo = (
408
+ context,
409
+ path2d: Path2D,
410
+ geoData,
411
+ canvasWidth: number,
412
+ strokeWidth?: number,
413
+ strokeColor?: string
414
+ ) => {
270
415
  const legendValues =
271
416
  geoData !== undefined
272
417
  ? applyLegendToRow(geoData, config, runtimeLegend, legendMemo, legendSpecialClassLastMemo)
@@ -312,7 +457,7 @@ const CountyMap = () => {
312
457
  }
313
458
  }
314
459
 
315
- context.strokeStyle = geoStrokeColor
460
+ context.strokeStyle = strokeColor ?? geoStrokeColor
316
461
  context.lineWidth = strokeWidth ?? lineWidth
317
462
  context.stroke(path2d)
318
463
 
@@ -332,8 +477,7 @@ const CountyMap = () => {
332
477
  ...config,
333
478
  mapPosition: { coordinates: [0, 30], zoom: 1 }
334
479
  })
335
- setFilteredStateCode('')
336
- setFocus({})
480
+ setFilteredStateCountyCode('')
337
481
  resetZoomTransform()
338
482
  }
339
483
 
@@ -411,7 +555,7 @@ const CountyMap = () => {
411
555
  const pointCoordinates = topoData.projection.invert([mapX, mapY])
412
556
 
413
557
  // Use d3 geoContains method to find the state geo data that the user clicked inside
414
- let clickedState
558
+ let clickedState: Geometry
415
559
  for (let i = 0; i < topoData.states.length; i++) {
416
560
  if (geoContains(topoData.states[i], pointCoordinates)) {
417
561
  clickedState = topoData.states[i]
@@ -420,6 +564,7 @@ const CountyMap = () => {
420
564
  }
421
565
 
422
566
  // If the user clicked outside of all states, no behavior
567
+ let clickedCounty = ''
423
568
  if (clickedState) {
424
569
  // If a county within the state was clicked and has data, call parent click handler
425
570
  if (topoData.countyIndecies[clickedState.id]) {
@@ -436,6 +581,11 @@ const CountyMap = () => {
436
581
  }
437
582
  if (county && runtimeData[county.id]) {
438
583
  geoClickHandler(displayGeoName(county.id), runtimeData[county.id])
584
+ if (filteredStateCode) {
585
+ if (filteredStateCode === clickedState.id) {
586
+ clickedCounty = county.id || ''
587
+ }
588
+ }
439
589
  }
440
590
  }
441
591
 
@@ -445,17 +595,8 @@ const CountyMap = () => {
445
595
  ...config,
446
596
  mapPosition: { coordinates: [0, 30], zoom: 3 }
447
597
  })
448
- setFilteredStateCode(clickedState.id)
598
+ setFilteredStateCountyCode(clickedState.id, clickedCounty)
449
599
 
450
- let focusIndex = -1
451
- for (let i = 0; i < topoData.mapData.length; i++) {
452
- if (topoData.mapData[i].id === clickedState.id) {
453
- focusIndex = i
454
- break
455
- }
456
- }
457
-
458
- setFocus({ id: clickedState.id, index: focusIndex, center: geoCentroid(clickedState), feature: clickedState })
459
600
  publishAnalyticsEvent({
460
601
  vizType: config.type,
461
602
  vizSubType: getVizSubType(config),
@@ -524,28 +665,6 @@ const CountyMap = () => {
524
665
  if (config.general.type !== 'us-geocode') {
525
666
  //If no tooltip is shown, or if the current geo associated with the tooltip shown is no longer containing the mouse, then rerender the tooltip
526
667
  if (isNaN(currentTooltipIndex) || !geoContains(topoData.mapData[currentTooltipIndex], pointCoordinates)) {
527
- if (
528
- !isNaN(currentTooltipIndex) &&
529
- applyLegendToRow(
530
- runtimeData[topoData.mapData[currentTooltipIndex].id],
531
- config,
532
- runtimeLegend,
533
- legendMemo,
534
- legendSpecialClassLastMemo
535
- )
536
- ) {
537
- const prevPath2d = geoPathCacheRef.current.get(topoData.mapData[currentTooltipIndex].id)
538
- if (prevPath2d) {
539
- paintCountyGeo(
540
- context,
541
- prevPath2d,
542
- runtimeData[topoData.mapData[currentTooltipIndex].id],
543
- canvas.width,
544
- lineWidth * strokeScale
545
- )
546
- }
547
- }
548
-
549
668
  let hoveredState
550
669
  let county
551
670
  let countyIndex
@@ -583,10 +702,6 @@ const CountyMap = () => {
583
702
  return
584
703
  }
585
704
  context.globalAlpha = 1
586
- const hoverPath2d = geoPathCacheRef.current.get(county.id)
587
- if (hoverPath2d) {
588
- paintCountyGeo(context, hoverPath2d, runtimeData[county.id], canvas.width, lineWidth * strokeScale)
589
- }
590
705
  }
591
706
 
592
707
  // Track hover analytics event if this is a new location
@@ -736,7 +851,7 @@ const CountyMap = () => {
736
851
  }
737
852
  }
738
853
 
739
- if (focus.index !== -1) {
854
+ if (focus.index !== -1 && !config.general.showHSABoundaries) {
740
855
  const focusPath2d = geoPathCacheRef.current.get(topoData.mapData[focus.index]?.id)
741
856
  if (focusPath2d) {
742
857
  context.strokeStyle = geoStrokeColor
@@ -779,7 +894,7 @@ const CountyMap = () => {
779
894
  // Fast render using cached Path2D objects — called during zoom/pan for smooth performance.
780
895
  // Skips canvas resize and projection setup; only applies the current zoom transform and redraws.
781
896
  const renderFrame = () => {
782
- if (!canvasRef.current || !runtimeLegend.items.length) return
897
+ if (!canvasRef.current || !runtimeLegend.items.length || !topoData.mapData) return
783
898
 
784
899
  const canvas = canvasRef.current
785
900
  const context = canvas.getContext('2d')
@@ -792,29 +907,68 @@ const CountyMap = () => {
792
907
  applyZoomTransform(context)
793
908
  const zoomScale = getZoomScale()
794
909
  const strokeScale = zoomScale ? 1 / zoomScale : 1
795
- const countyStrokeWidth = lineWidth * 0.8 * strokeScale
910
+ const useHsaStrokeStyling = config.general.showHSABoundaries
911
+ const countyStrokeWidth = lineWidth * (useHsaStrokeStyling ? 0.45 : 0.8) * strokeScale
912
+ const hsaStrokeWidth = lineWidth * 0.7 * strokeScale
913
+ const countyStrokeColor = useHsaStrokeStyling ? '#a9aeb1' : geoStrokeColor
914
+ const isZoomedIntoState = focus.id
915
+ const hsaStrokeColor = isZoomedIntoState ? '#000' : '#303030'
916
+ const stateStrokeColor = useHsaStrokeStyling ? '#000000' : '#1c1d1f'
796
917
 
797
918
  // Enforces stroke style of the county lines
798
- context.strokeStyle = geoStrokeColor
919
+ context.strokeStyle = countyStrokeColor
799
920
  context.lineWidth = countyStrokeWidth
800
921
 
801
922
  // Iterates through each state/county topo and renders it using cached Path2D
923
+ let countyHighlight = null
802
924
  topoData.mapData.forEach(geo => {
803
925
  if (!geo.id) return
804
- if (focus.id && geo.id.length > 2 && geo.id.indexOf(focus.id) !== 0) return
926
+ const hideCounty =
927
+ !config.general.showNeighboringStates && focus.id && geo.id.length > 2 && geo.id.indexOf(focus.id) !== 0
928
+ if (hideCounty) return
805
929
  if (!focus.id && config.general.type === 'us-geocode' && geo.id.length > 2) return
806
930
 
807
931
  const path2d = cache.get(geo.id)
808
932
  if (!path2d) return
809
933
 
810
934
  const geoData = runtimeData[geo.id]
811
- paintCountyGeo(context, path2d, geoData, canvas.width, countyStrokeWidth)
935
+ if (!config.general.showHSABoundaries && filteredCountyCode && geo.id === filteredCountyCode) {
936
+ countyHighlight = {
937
+ context,
938
+ path2d,
939
+ geoData,
940
+ canvasWidth: canvas.width,
941
+ strokeWidth: 2,
942
+ strokeColor: '#000000'
943
+ }
944
+ }
945
+ paintCountyGeo(context, path2d, geoData, canvas.width, countyStrokeWidth, countyStrokeColor)
812
946
  })
813
947
 
948
+ let hsaHighlight = null
949
+ if (config.general.showHSABoundaries) {
950
+ context.strokeStyle = hsaStrokeColor
951
+ context.lineWidth = hsaStrokeWidth
952
+
953
+ topoData.hsas.forEach(hsa => {
954
+ if (!hsa?.groupId) return
955
+ const cacheKey = 'hsa_border_' + hsa.groupId
956
+ const path2d = cache.get(cacheKey)
957
+ if (path2d) {
958
+ if (filteredCountyCode && topoData.hsaMapping[filteredCountyCode] === hsa.groupId) {
959
+ hsaHighlight = path2d
960
+ } else {
961
+ context.stroke(path2d)
962
+ }
963
+ }
964
+ })
965
+ }
966
+
814
967
  // State borders
815
- context.strokeStyle = '#1c1d1f'
968
+ context.strokeStyle = stateStrokeColor
816
969
  context.lineWidth = lineWidth * 1.25 * strokeScale
817
970
  topoData.states.forEach(state => {
971
+ if (config.migrations.showPuertoRico == false) return
818
972
  if (!state.id) return
819
973
  const path2d = cache.get('state_border_' + state.id)
820
974
  if (path2d) {
@@ -827,7 +981,7 @@ const CountyMap = () => {
827
981
  const focusGeoId = topoData.mapData[focus.index]?.id
828
982
  const path2d = focusGeoId && cache.get(focusGeoId)
829
983
  if (path2d) {
830
- context.strokeStyle = geoStrokeColor
984
+ context.strokeStyle = config.general.showNeighboringStates ? '#000000' : geoStrokeColor
831
985
  context.lineWidth = lineWidth * 2 * strokeScale
832
986
  context.stroke(path2d)
833
987
  }
@@ -905,13 +1059,24 @@ const CountyMap = () => {
905
1059
  }
906
1060
  })
907
1061
  }
1062
+
1063
+ // Highlight county last so it is visible on top of all other layers
1064
+ if (countyHighlight) {
1065
+ const { context, path2d, geoData, canvasWidth, strokeWidth, strokeColor } = countyHighlight
1066
+ paintCountyGeo(context, path2d, geoData, canvasWidth, strokeWidth, strokeColor)
1067
+ }
1068
+ // Highlight HSA boundary if applicable
1069
+ if (hsaHighlight) {
1070
+ context.lineWidth = 2.5 * strokeScale
1071
+ context.strokeStyle = hsaStrokeColor
1072
+ context.stroke(hsaHighlight)
1073
+ }
908
1074
  context.restore()
909
1075
  }
910
1076
 
911
1077
  useEffect(() => {
912
1078
  if (!config.general.allowMapZoom) {
913
- setFilteredStateCode('')
914
- setFocus({})
1079
+ setFilteredStateCountyCode('')
915
1080
  setHasMoved(false)
916
1081
  resetZoomTransform()
917
1082
  }
@@ -951,14 +1116,15 @@ const CountyMap = () => {
951
1116
  resetZoomTransform()
952
1117
  }, [focus?.id])
953
1118
 
954
- // If runtimeData is not defined, show loader
955
- if (!runtimeData || !isTopoReady(topoData, config, runtimeFilters)) {
956
- return (
957
- <div style={{ height: 300 }}>
958
- <Loading />
959
- </div>
960
- )
961
- }
1119
+ const isLoading = !runtimeData || !isTopoReady(topoData, config, runtimeFilters) || !canvasRef.current
1120
+
1121
+ useEffect(() => {
1122
+ if (isLoading) {
1123
+ return
1124
+ }
1125
+
1126
+ drawCanvas()
1127
+ }, [isLoading, topoData, focus, runtimeLegend, runtimeData, featureArray, config, filteredCountyCode])
962
1128
 
963
1129
  const showManualZoomControls = config.general.allowMapZoom
964
1130
  const showResetControl = (hasMoved || focus.id) && (showManualZoomControls || config.general.type === 'us-geocode')
@@ -967,6 +1133,11 @@ const CountyMap = () => {
967
1133
 
968
1134
  return (
969
1135
  <ErrorBoundary component='CountyMap'>
1136
+ {isLoading && (
1137
+ <div style={{ height: 300 }}>
1138
+ <Loading />
1139
+ </div>
1140
+ )}
970
1141
  <canvas
971
1142
  ref={canvasRef}
972
1143
  aria-label={handleMapAriaLabels(config)}
@@ -976,12 +1147,12 @@ const CountyMap = () => {
976
1147
  tooltipRef.current.setAttribute('data-index', null)
977
1148
  }}
978
1149
  onClick={canvasClick}
979
- className='county-map-canvas'
1150
+ className={'county-map-canvas' + (isLoading ? ' d-none' : '')}
980
1151
  style={config.general.allowMapZoom ? undefined : { cursor: 'default' }}
981
1152
  ></canvas>
982
1153
 
983
1154
  {showManualZoomControls && (
984
- <div className='zoom-controls' data-html2canvas-ignore='true'>
1155
+ <div className={'zoom-controls' + (isLoading ? ' d-none' : '')} data-html2canvas-ignore='true'>
985
1156
  <button onClick={handleZoomIn} aria-label='Zoom In'>
986
1157
  <svg viewBox='0 0 24 24' stroke='currentColor' strokeWidth='3'>
987
1158
  <line x1='12' y1='5' x2='12' y2='19' />
@@ -161,7 +161,7 @@ const UsaMap = () => {
161
161
 
162
162
  useEffect(() => {
163
163
  if (general.territoriesAlwaysShow) {
164
- // show all Territories whether in the data or not
164
+ // Show Available Territories whether in the data or not
165
165
  setTerritoriesData(territoriesKeys)
166
166
  } else {
167
167
  // Territories need to show up if they're in the data at all, not just if they're "active". That's why this is different from Cities