@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.
- package/CONFIG.md +235 -0
- package/README.md +70 -24
- package/dist/cdcmap-CY9IcPSi.es.js +6 -0
- package/dist/cdcmap-DlpiY3fQ.es.js +4 -0
- package/dist/cdcmap.js +27405 -26257
- package/examples/{testing-layer-2.json → __data__/testing-layer-2.json} +1 -1
- package/examples/{testing-layer.json → __data__/testing-layer.json} +1 -1
- package/examples/county-hsa-toggle.json +51993 -0
- package/examples/custom-map-layers.json +2 -2
- package/examples/default-county.json +3 -3
- package/examples/minimal-example.json +69 -0
- package/examples/private/annotation-bug.json +2 -2
- package/examples/private/css-issue.json +314 -0
- package/examples/private/region-breaking.json +1639 -0
- package/examples/private/test1.json +27247 -0
- package/package.json +4 -4
- package/src/CdcMapComponent.tsx +96 -13
- package/src/_stories/CdcMap.Editor.ColumnsSectionTests.stories.tsx +601 -0
- package/src/_stories/CdcMap.Editor.DataTableSectionTests.stories.tsx +404 -0
- package/src/_stories/CdcMap.Editor.FiltersSectionTests.stories.tsx +229 -0
- package/src/_stories/CdcMap.Editor.GeneralSectionTests.stories.tsx +262 -0
- package/src/_stories/CdcMap.Editor.LegendSectionTests.stories.tsx +541 -0
- package/src/_stories/CdcMap.Editor.MultiCountryWorldMapTests.stories.tsx +359 -0
- package/src/_stories/CdcMap.Editor.PatternSettingsSectionTests.stories.tsx +516 -0
- package/src/_stories/CdcMap.Editor.SmallMultiplesSectionTests.stories.tsx +165 -0
- package/src/_stories/CdcMap.Editor.TextAnnotationsSectionTests.stories.tsx +145 -0
- package/src/_stories/CdcMap.Editor.TypeSectionTests.stories.tsx +312 -0
- package/src/_stories/CdcMap.Editor.VisualSectionTests.stories.tsx +359 -0
- package/src/_stories/CdcMap.Editor.ZoomControlsTests.stories.tsx +88 -0
- package/src/_stories/{CdcMap.stories.tsx → CdcMap.smoke.stories.tsx} +12 -0
- package/src/_stories/_mock/legends/legend-tests.json +3 -3
- package/src/components/Annotation/AnnotationList.tsx +1 -1
- package/src/components/EditorPanel/components/EditorPanel.tsx +504 -383
- package/src/components/EditorPanel/components/HexShapeSettings.tsx +1 -1
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +112 -117
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +26 -13
- package/src/components/EditorPanel/components/editorPanel.styles.css +22 -2
- package/src/components/Legend/components/Legend.tsx +3 -3
- package/src/components/Legend/components/LegendItem.Hex.tsx +4 -2
- package/src/components/SmallMultiples/SynchronizedTooltip.tsx +1 -1
- package/src/components/UsaMap/components/UsaMap.County.tsx +271 -100
- package/src/components/UsaMap/components/UsaMap.State.tsx +1 -1
- package/src/components/UsaMap/data/cb_2019_us_county_20m.json +75817 -1
- package/src/components/UsaMap/data/hsa_fips_mapping.json +3144 -0
- package/src/components/WorldMap/data/world-topo.json +1 -1
- package/src/data/initial-state.js +1 -0
- package/src/data/supported-counties.json +1 -1
- package/src/helpers/countyTerritories.ts +38 -0
- package/src/helpers/dataTableHelpers.ts +35 -6
- package/src/helpers/tests/countyTerritories.test.ts +87 -0
- package/src/hooks/useApplyTooltipsToGeo.tsx +7 -4
- package/src/hooks/useMapLayers.tsx +1 -1
- package/src/hooks/useTooltip.ts +18 -7
- package/src/store/map.actions.ts +5 -2
- package/src/store/map.reducer.ts +12 -3
- package/src/test/CdcMap.test.jsx +24 -0
- package/src/types/MapConfig.ts +6 -0
- package/src/types/MapContext.ts +3 -1
- package/topojson-updater/README.txt +1 -1
- package/LICENSE +0 -201
- package/dist/cdcmap-vr9HZwRt.es.js +0 -6
- package/examples/__data__/city-state-data.json +0 -668
- package/examples/city-state.json +0 -434
- package/examples/default-world-data.json +0 -1450
- package/examples/new-cities.json +0 -656
- package/src/_stories/CdcMap.Editor.stories.tsx +0 -3648
- package/topojson-updater/package-lock.json +0 -223
- /package/src/_stories/{CdcMap.ColumnWrap.stories.tsx → CdcMap.ColumnWrap.smoke.stories.tsx} +0 -0
- /package/src/_stories/{CdcMap.Defaults.stories.tsx → CdcMap.Defaults.smoke.stories.tsx} +0 -0
- /package/src/_stories/{CdcMap.DistrictOfColumbia.stories.tsx → CdcMap.DistrictOfColumbia.smoke.stories.tsx} +0 -0
- /package/src/_stories/{CdcMap.Filters.stories.tsx → CdcMap.Filters.smoke.stories.tsx} +0 -0
- /package/src/_stories/{CdcMap.Legend.Gradient.stories.tsx → CdcMap.Legend.Gradient.smoke.stories.tsx} +0 -0
- /package/src/_stories/{CdcMap.Legend.stories.tsx → CdcMap.Legend.smoke.stories.tsx} +0 -0
- /package/src/_stories/{CdcMap.Patterns.stories.tsx → CdcMap.Patterns.smoke.stories.tsx} +0 -0
- /package/src/_stories/{CdcMap.SmallMultiples.stories.tsx → CdcMap.SmallMultiples.smoke.stories.tsx} +0 -0
- /package/src/_stories/{CdcMap.Table.stories.tsx → CdcMap.Table.smoke.stories.tsx} +0 -0
- /package/src/_stories/{CdcMap.ZeroColor.stories.tsx → CdcMap.ZeroColor.smoke.stories.tsx} +0 -0
- /package/src/_stories/{GoogleMap.stories.tsx → GoogleMap.smoke.stories.tsx} +0 -0
- /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:
|
|
54
|
-
states:
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
62
|
-
topoData.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 [
|
|
165
|
-
const
|
|
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
|
-
|
|
303
|
+
const currentYear = getCurrentTopoYear(config, runtimeFilters)
|
|
182
304
|
|
|
183
305
|
if (currentYear !== topoData.year) {
|
|
184
|
-
|
|
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
|
-
}, [
|
|
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 = (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
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
|
-
//
|
|
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
|