@cdc/map 4.26.3 → 4.26.5
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 +268 -0
- package/README.md +74 -24
- package/dist/cdcmap-CY9IcPSi.es.js +6 -0
- package/dist/cdcmap-DlpiY3fQ.es.js +4 -0
- package/dist/cdcmap.js +29168 -27482
- 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 +6 -3
- package/examples/minimal-example.json +73 -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 +107 -14
- package/src/_stories/CdcMap.AltText.stories.tsx +122 -0
- package/src/_stories/CdcMap.Editor.ColumnsSectionTests.stories.tsx +600 -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.FocusVisibility.stories.tsx +87 -0
- package/src/_stories/CdcMap.HiddenMount.stories.tsx +69 -0
- package/src/_stories/CdcMap.ResetBehavior.stories.tsx +32 -0
- package/src/_stories/CdcMap.Zoom.stories.tsx +111 -0
- package/src/_stories/{CdcMap.stories.tsx → CdcMap.smoke.stories.tsx} +60 -0
- package/src/_stories/_mock/alt_text_metadata.json +65 -0
- package/src/_stories/_mock/legends/legend-tests.json +3 -3
- package/src/_stories/_mock/world-bubble-reset.json +138 -0
- package/src/_stories/_mock/world-data-zoom-filters.json +166 -0
- package/src/components/Annotation/AnnotationList.tsx +1 -1
- package/src/components/BubbleList.tsx +13 -0
- package/src/components/EditorPanel/components/EditorPanel.tsx +637 -382
- 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/FilterControls.tsx +21 -0
- package/src/components/Legend/components/Legend.tsx +3 -3
- package/src/components/Legend/components/LegendItem.Hex.tsx +4 -2
- package/src/components/SmallMultiples/SmallMultiples.tsx +2 -2
- package/src/components/SmallMultiples/SynchronizedTooltip.tsx +1 -1
- package/src/components/UsaMap/components/UsaMap.County.tsx +309 -108
- package/src/components/UsaMap/components/UsaMap.Region.tsx +5 -2
- package/src/components/UsaMap/components/UsaMap.SingleState.tsx +33 -10
- package/src/components/UsaMap/components/UsaMap.State.tsx +10 -3
- 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/WorldMap.tsx +37 -4
- package/src/components/WorldMap/data/world-topo.json +1 -1
- package/src/components/ZoomableGroup.tsx +23 -3
- package/src/components/filterControls.styles.css +6 -0
- package/src/data/initial-state.js +3 -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/generateRuntimeFilters.ts +2 -1
- package/src/helpers/handleMapAriaLabels.ts +45 -30
- package/src/helpers/shouldAutoResetSingleStateZoom.ts +22 -0
- package/src/helpers/tests/countyTerritories.test.ts +87 -0
- package/src/helpers/tests/handleMapAriaLabels.test.ts +71 -0
- package/src/helpers/tests/shouldAutoResetSingleStateZoom.test.ts +71 -0
- package/src/hooks/useApplyTooltipsToGeo.tsx +7 -4
- package/src/hooks/useGeoClickHandler.ts +13 -1
- package/src/hooks/useMapLayers.tsx +1 -1
- package/src/hooks/useStateZoom.tsx +39 -20
- package/src/hooks/useTooltip.test.tsx +2 -16
- package/src/hooks/useTooltip.ts +18 -7
- package/src/index.jsx +5 -2
- package/src/scss/main.scss +6 -21
- package/src/scss/map.scss +20 -0
- 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 +11 -0
- package/src/types/MapContext.ts +6 -1
- package/topojson-updater/README.txt +1 -1
- 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,55 @@ 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
|
+
geoPathCacheRef.current.clear()
|
|
299
|
+
geoPathCacheKeyRef.current = ''
|
|
300
|
+
setTopoData(response)
|
|
301
|
+
})
|
|
302
|
+
}
|
|
303
|
+
|
|
180
304
|
useEffect(() => {
|
|
181
|
-
|
|
305
|
+
const currentYear = getCurrentTopoYear(config, runtimeFilters)
|
|
182
306
|
|
|
183
307
|
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
|
-
})
|
|
308
|
+
getAndSetTopoData(currentYear)
|
|
191
309
|
}
|
|
192
|
-
}, [
|
|
310
|
+
}, [
|
|
311
|
+
config.general.countyCensusYear,
|
|
312
|
+
config.general.filterControlsCountyYear,
|
|
313
|
+
JSON.stringify(runtimeFilters),
|
|
314
|
+
territoryVisibility.key
|
|
315
|
+
])
|
|
316
|
+
|
|
317
|
+
const prevShowHSABoundariesRef = useRef(config.general.showHSABoundaries)
|
|
318
|
+
useEffect(() => {
|
|
319
|
+
if (prevShowHSABoundariesRef.current === config.general.showHSABoundaries) return
|
|
320
|
+
const currentYear = getCurrentTopoYear(config, runtimeFilters)
|
|
321
|
+
getAndSetTopoData(currentYear)
|
|
322
|
+
prevShowHSABoundariesRef.current = config.general.showHSABoundaries
|
|
323
|
+
}, [config.general.showHSABoundaries])
|
|
324
|
+
|
|
325
|
+
const prevTerritoryVisibilityRef = useRef(territoryVisibility.key)
|
|
326
|
+
useEffect(() => {
|
|
327
|
+
if (prevTerritoryVisibilityRef.current === territoryVisibility.key) return
|
|
328
|
+
const currentYear = getCurrentTopoYear(config, runtimeFilters)
|
|
329
|
+
getAndSetTopoData(currentYear)
|
|
330
|
+
prevTerritoryVisibilityRef.current = territoryVisibility.key
|
|
331
|
+
}, [territoryVisibility.key])
|
|
193
332
|
|
|
194
333
|
// Whenever the memo at the top is triggered and the map is called to re-render, call drawCanvas and update
|
|
195
334
|
// The resize function so it includes the latest state variables
|
|
196
335
|
useEffect(() => {
|
|
197
|
-
if (isTopoReady(topoData, config, runtimeFilters)) {
|
|
198
|
-
drawCanvas()
|
|
199
|
-
}
|
|
200
|
-
|
|
201
336
|
const onResize = () => {
|
|
202
337
|
if (canvasRef.current && isTopoReady(topoData, config, runtimeFilters)) {
|
|
203
338
|
drawCanvas()
|
|
@@ -217,6 +352,7 @@ const CountyMap = () => {
|
|
|
217
352
|
const zoomBehaviorRef = useRef()
|
|
218
353
|
const zoomFrameRef = useRef<number | null>(null)
|
|
219
354
|
const geoPathCacheRef = useRef<Map<string, Path2D>>(new Map())
|
|
355
|
+
const geoPathCacheKeyRef = useRef('')
|
|
220
356
|
|
|
221
357
|
// Clear pattern cache when pattern configuration changes
|
|
222
358
|
useEffect(() => {
|
|
@@ -226,8 +362,20 @@ const CountyMap = () => {
|
|
|
226
362
|
const runtimeKeys = runtimeData ? Object.keys(runtimeData) : []
|
|
227
363
|
const lineWidth = 1
|
|
228
364
|
|
|
365
|
+
const getPathCacheKey = (canvas: HTMLCanvasElement) =>
|
|
366
|
+
[
|
|
367
|
+
topoData.year,
|
|
368
|
+
topoData.mapData?.length || 0,
|
|
369
|
+
topoData.states?.length || 0,
|
|
370
|
+
topoData.hsas?.length || 0,
|
|
371
|
+
focus.id || '',
|
|
372
|
+
canvas.clientWidth,
|
|
373
|
+
config.general.showHSABoundaries ? 'hsa' : 'county',
|
|
374
|
+
territoryVisibility.key
|
|
375
|
+
].join('|')
|
|
376
|
+
|
|
229
377
|
// Pre-compute Path2D objects for all geo features — avoids expensive geoPath projection on every zoom frame
|
|
230
|
-
const buildPathCache = () => {
|
|
378
|
+
const buildPathCache = (cacheKey: string) => {
|
|
231
379
|
const pathGen = geoPath(topoData.projection)
|
|
232
380
|
const cache = new Map<string, Path2D>()
|
|
233
381
|
topoData.mapData.forEach(geo => {
|
|
@@ -240,7 +388,14 @@ const CountyMap = () => {
|
|
|
240
388
|
const d = pathGen(state)
|
|
241
389
|
if (d) cache.set('state_border_' + state.id, new Path2D(d))
|
|
242
390
|
})
|
|
391
|
+
topoData.hsas.forEach(hsa => {
|
|
392
|
+
if (!hsa?.groupId || !hsa?.feature) return
|
|
393
|
+
const d = pathGen(hsa.feature as any)
|
|
394
|
+
if (d) cache.set('hsa_border_' + hsa.groupId, new Path2D(d))
|
|
395
|
+
})
|
|
396
|
+
geoPathCacheRef.current.clear()
|
|
243
397
|
geoPathCacheRef.current = cache
|
|
398
|
+
geoPathCacheKeyRef.current = cacheKey
|
|
244
399
|
}
|
|
245
400
|
|
|
246
401
|
const resetZoomTransform = () => {
|
|
@@ -253,8 +408,10 @@ const CountyMap = () => {
|
|
|
253
408
|
const getCanvasPoints = e => {
|
|
254
409
|
const canvas = e.target
|
|
255
410
|
const canvasBounds = canvas.getBoundingClientRect()
|
|
256
|
-
const
|
|
257
|
-
const
|
|
411
|
+
const scaleX = canvasBounds.width ? canvas.width / canvasBounds.width : 1
|
|
412
|
+
const scaleY = canvasBounds.height ? canvas.height / canvasBounds.height : 1
|
|
413
|
+
const x = (e.clientX - canvasBounds.left) * scaleX
|
|
414
|
+
const y = (e.clientY - canvasBounds.top) * scaleY
|
|
258
415
|
const [mapX, mapY] = zoomTransformRef.current.invert([x, y])
|
|
259
416
|
return { canvas, mapX, mapY }
|
|
260
417
|
}
|
|
@@ -265,8 +422,19 @@ const CountyMap = () => {
|
|
|
265
422
|
}
|
|
266
423
|
|
|
267
424
|
const getZoomScale = () => zoomTransformRef.current?.k || 1
|
|
425
|
+
const getZoomTransformString = () => {
|
|
426
|
+
const { x, y, k } = zoomTransformRef.current || d3ZoomIdentity
|
|
427
|
+
return `translate(${x} ${y}) scale(${k})`
|
|
428
|
+
}
|
|
268
429
|
|
|
269
|
-
const paintCountyGeo = (
|
|
430
|
+
const paintCountyGeo = (
|
|
431
|
+
context,
|
|
432
|
+
path2d: Path2D,
|
|
433
|
+
geoData,
|
|
434
|
+
canvasWidth: number,
|
|
435
|
+
strokeWidth?: number,
|
|
436
|
+
strokeColor?: string
|
|
437
|
+
) => {
|
|
270
438
|
const legendValues =
|
|
271
439
|
geoData !== undefined
|
|
272
440
|
? applyLegendToRow(geoData, config, runtimeLegend, legendMemo, legendSpecialClassLastMemo)
|
|
@@ -312,7 +480,7 @@ const CountyMap = () => {
|
|
|
312
480
|
}
|
|
313
481
|
}
|
|
314
482
|
|
|
315
|
-
context.strokeStyle = geoStrokeColor
|
|
483
|
+
context.strokeStyle = strokeColor ?? geoStrokeColor
|
|
316
484
|
context.lineWidth = strokeWidth ?? lineWidth
|
|
317
485
|
context.stroke(path2d)
|
|
318
486
|
|
|
@@ -332,8 +500,7 @@ const CountyMap = () => {
|
|
|
332
500
|
...config,
|
|
333
501
|
mapPosition: { coordinates: [0, 30], zoom: 1 }
|
|
334
502
|
})
|
|
335
|
-
|
|
336
|
-
setFocus({})
|
|
503
|
+
setFilteredStateCountyCode('')
|
|
337
504
|
resetZoomTransform()
|
|
338
505
|
}
|
|
339
506
|
|
|
@@ -411,7 +578,7 @@ const CountyMap = () => {
|
|
|
411
578
|
const pointCoordinates = topoData.projection.invert([mapX, mapY])
|
|
412
579
|
|
|
413
580
|
// Use d3 geoContains method to find the state geo data that the user clicked inside
|
|
414
|
-
let clickedState
|
|
581
|
+
let clickedState: Geometry
|
|
415
582
|
for (let i = 0; i < topoData.states.length; i++) {
|
|
416
583
|
if (geoContains(topoData.states[i], pointCoordinates)) {
|
|
417
584
|
clickedState = topoData.states[i]
|
|
@@ -420,6 +587,7 @@ const CountyMap = () => {
|
|
|
420
587
|
}
|
|
421
588
|
|
|
422
589
|
// If the user clicked outside of all states, no behavior
|
|
590
|
+
let clickedCounty = ''
|
|
423
591
|
if (clickedState) {
|
|
424
592
|
// If a county within the state was clicked and has data, call parent click handler
|
|
425
593
|
if (topoData.countyIndecies[clickedState.id]) {
|
|
@@ -436,6 +604,11 @@ const CountyMap = () => {
|
|
|
436
604
|
}
|
|
437
605
|
if (county && runtimeData[county.id]) {
|
|
438
606
|
geoClickHandler(displayGeoName(county.id), runtimeData[county.id])
|
|
607
|
+
if (filteredStateCode) {
|
|
608
|
+
if (filteredStateCode === clickedState.id) {
|
|
609
|
+
clickedCounty = county.id || ''
|
|
610
|
+
}
|
|
611
|
+
}
|
|
439
612
|
}
|
|
440
613
|
}
|
|
441
614
|
|
|
@@ -445,17 +618,8 @@ const CountyMap = () => {
|
|
|
445
618
|
...config,
|
|
446
619
|
mapPosition: { coordinates: [0, 30], zoom: 3 }
|
|
447
620
|
})
|
|
448
|
-
|
|
621
|
+
setFilteredStateCountyCode(clickedState.id, clickedCounty)
|
|
449
622
|
|
|
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
623
|
publishAnalyticsEvent({
|
|
460
624
|
vizType: config.type,
|
|
461
625
|
vizSubType: getVizSubType(config),
|
|
@@ -524,28 +688,6 @@ const CountyMap = () => {
|
|
|
524
688
|
if (config.general.type !== 'us-geocode') {
|
|
525
689
|
//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
690
|
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
691
|
let hoveredState
|
|
550
692
|
let county
|
|
551
693
|
let countyIndex
|
|
@@ -583,10 +725,6 @@ const CountyMap = () => {
|
|
|
583
725
|
return
|
|
584
726
|
}
|
|
585
727
|
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
728
|
}
|
|
591
729
|
|
|
592
730
|
// Track hover analytics event if this is a new location
|
|
@@ -736,7 +874,7 @@ const CountyMap = () => {
|
|
|
736
874
|
}
|
|
737
875
|
}
|
|
738
876
|
|
|
739
|
-
if (focus.index !== -1) {
|
|
877
|
+
if (focus.index !== -1 && !config.general.showHSABoundaries) {
|
|
740
878
|
const focusPath2d = geoPathCacheRef.current.get(topoData.mapData[focus.index]?.id)
|
|
741
879
|
if (focusPath2d) {
|
|
742
880
|
context.strokeStyle = geoStrokeColor
|
|
@@ -747,14 +885,17 @@ const CountyMap = () => {
|
|
|
747
885
|
context.restore()
|
|
748
886
|
}
|
|
749
887
|
|
|
750
|
-
// Sets up canvas dimensions
|
|
888
|
+
// Sets up canvas dimensions and projection, rebuilds the Path2D cache only when geometry changes, then renders.
|
|
751
889
|
// Called on data change, resize, focus change — NOT during zoom/pan.
|
|
752
890
|
const drawCanvas = () => {
|
|
753
891
|
if (canvasRef.current && runtimeLegend.items.length > 0) {
|
|
754
892
|
const canvas = canvasRef.current
|
|
893
|
+
const canvasWidth = canvas.clientWidth
|
|
894
|
+
if (canvasWidth <= 0) return
|
|
895
|
+
const canvasHeight = canvasWidth * 0.6
|
|
755
896
|
|
|
756
|
-
canvas.width
|
|
757
|
-
canvas.height
|
|
897
|
+
if (canvas.width !== canvasWidth) canvas.width = canvasWidth
|
|
898
|
+
if (canvas.height !== canvasHeight) canvas.height = canvasHeight
|
|
758
899
|
|
|
759
900
|
topoData.projection.scale(canvas.width * 1.25).translate([canvas.width / 2, canvas.height / 2])
|
|
760
901
|
|
|
@@ -768,8 +909,10 @@ const CountyMap = () => {
|
|
|
768
909
|
topoData.projection.fitExtent(fitExtent, focus.feature)
|
|
769
910
|
}
|
|
770
911
|
|
|
771
|
-
|
|
772
|
-
|
|
912
|
+
const pathCacheKey = getPathCacheKey(canvas)
|
|
913
|
+
if (geoPathCacheKeyRef.current !== pathCacheKey || geoPathCacheRef.current.size === 0) {
|
|
914
|
+
buildPathCache(pathCacheKey)
|
|
915
|
+
}
|
|
773
916
|
|
|
774
917
|
// Render the map
|
|
775
918
|
renderFrame()
|
|
@@ -779,7 +922,7 @@ const CountyMap = () => {
|
|
|
779
922
|
// Fast render using cached Path2D objects — called during zoom/pan for smooth performance.
|
|
780
923
|
// Skips canvas resize and projection setup; only applies the current zoom transform and redraws.
|
|
781
924
|
const renderFrame = () => {
|
|
782
|
-
if (!canvasRef.current || !runtimeLegend.items.length) return
|
|
925
|
+
if (!canvasRef.current || !runtimeLegend.items.length || !topoData.mapData) return
|
|
783
926
|
|
|
784
927
|
const canvas = canvasRef.current
|
|
785
928
|
const context = canvas.getContext('2d')
|
|
@@ -792,27 +935,65 @@ const CountyMap = () => {
|
|
|
792
935
|
applyZoomTransform(context)
|
|
793
936
|
const zoomScale = getZoomScale()
|
|
794
937
|
const strokeScale = zoomScale ? 1 / zoomScale : 1
|
|
795
|
-
const
|
|
938
|
+
const useHsaStrokeStyling = config.general.showHSABoundaries
|
|
939
|
+
const countyStrokeWidth = lineWidth * (useHsaStrokeStyling ? 0.45 : 0.8) * strokeScale
|
|
940
|
+
const hsaStrokeWidth = lineWidth * 0.7 * strokeScale
|
|
941
|
+
const countyStrokeColor = useHsaStrokeStyling ? '#a9aeb1' : geoStrokeColor
|
|
942
|
+
const isZoomedIntoState = focus.id
|
|
943
|
+
const hsaStrokeColor = isZoomedIntoState ? '#000' : '#303030'
|
|
944
|
+
const stateStrokeColor = useHsaStrokeStyling ? '#000000' : '#1c1d1f'
|
|
796
945
|
|
|
797
946
|
// Enforces stroke style of the county lines
|
|
798
|
-
context.strokeStyle =
|
|
947
|
+
context.strokeStyle = countyStrokeColor
|
|
799
948
|
context.lineWidth = countyStrokeWidth
|
|
800
949
|
|
|
801
950
|
// Iterates through each state/county topo and renders it using cached Path2D
|
|
951
|
+
let countyHighlight = null
|
|
802
952
|
topoData.mapData.forEach(geo => {
|
|
803
953
|
if (!geo.id) return
|
|
804
|
-
|
|
954
|
+
const hideCounty =
|
|
955
|
+
!config.general.showNeighboringStates && focus.id && geo.id.length > 2 && geo.id.indexOf(focus.id) !== 0
|
|
956
|
+
if (hideCounty) return
|
|
805
957
|
if (!focus.id && config.general.type === 'us-geocode' && geo.id.length > 2) return
|
|
806
958
|
|
|
807
959
|
const path2d = cache.get(geo.id)
|
|
808
960
|
if (!path2d) return
|
|
809
961
|
|
|
810
962
|
const geoData = runtimeData[geo.id]
|
|
811
|
-
|
|
963
|
+
if (!config.general.showHSABoundaries && filteredCountyCode && geo.id === filteredCountyCode) {
|
|
964
|
+
countyHighlight = {
|
|
965
|
+
context,
|
|
966
|
+
path2d,
|
|
967
|
+
geoData,
|
|
968
|
+
canvasWidth: canvas.width,
|
|
969
|
+
strokeWidth: 2,
|
|
970
|
+
strokeColor: '#000000'
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
paintCountyGeo(context, path2d, geoData, canvas.width, countyStrokeWidth, countyStrokeColor)
|
|
812
974
|
})
|
|
813
975
|
|
|
976
|
+
let hsaHighlight = null
|
|
977
|
+
if (config.general.showHSABoundaries) {
|
|
978
|
+
context.strokeStyle = hsaStrokeColor
|
|
979
|
+
context.lineWidth = hsaStrokeWidth
|
|
980
|
+
|
|
981
|
+
topoData.hsas.forEach(hsa => {
|
|
982
|
+
if (!hsa?.groupId) return
|
|
983
|
+
const cacheKey = 'hsa_border_' + hsa.groupId
|
|
984
|
+
const path2d = cache.get(cacheKey)
|
|
985
|
+
if (path2d) {
|
|
986
|
+
if (filteredCountyCode && topoData.hsaMapping[filteredCountyCode] === hsa.groupId) {
|
|
987
|
+
hsaHighlight = path2d
|
|
988
|
+
} else {
|
|
989
|
+
context.stroke(path2d)
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
})
|
|
993
|
+
}
|
|
994
|
+
|
|
814
995
|
// State borders
|
|
815
|
-
context.strokeStyle =
|
|
996
|
+
context.strokeStyle = stateStrokeColor
|
|
816
997
|
context.lineWidth = lineWidth * 1.25 * strokeScale
|
|
817
998
|
topoData.states.forEach(state => {
|
|
818
999
|
if (!state.id) return
|
|
@@ -827,7 +1008,7 @@ const CountyMap = () => {
|
|
|
827
1008
|
const focusGeoId = topoData.mapData[focus.index]?.id
|
|
828
1009
|
const path2d = focusGeoId && cache.get(focusGeoId)
|
|
829
1010
|
if (path2d) {
|
|
830
|
-
context.strokeStyle = geoStrokeColor
|
|
1011
|
+
context.strokeStyle = config.general.showNeighboringStates ? '#000000' : geoStrokeColor
|
|
831
1012
|
context.lineWidth = lineWidth * 2 * strokeScale
|
|
832
1013
|
context.stroke(path2d)
|
|
833
1014
|
}
|
|
@@ -905,13 +1086,24 @@ const CountyMap = () => {
|
|
|
905
1086
|
}
|
|
906
1087
|
})
|
|
907
1088
|
}
|
|
1089
|
+
|
|
1090
|
+
// Highlight county last so it is visible on top of all other layers
|
|
1091
|
+
if (countyHighlight) {
|
|
1092
|
+
const { context, path2d, geoData, canvasWidth, strokeWidth, strokeColor } = countyHighlight
|
|
1093
|
+
paintCountyGeo(context, path2d, geoData, canvasWidth, strokeWidth, strokeColor)
|
|
1094
|
+
}
|
|
1095
|
+
// Highlight HSA boundary if applicable
|
|
1096
|
+
if (hsaHighlight) {
|
|
1097
|
+
context.lineWidth = 2.5 * strokeScale
|
|
1098
|
+
context.strokeStyle = hsaStrokeColor
|
|
1099
|
+
context.stroke(hsaHighlight)
|
|
1100
|
+
}
|
|
908
1101
|
context.restore()
|
|
909
1102
|
}
|
|
910
1103
|
|
|
911
1104
|
useEffect(() => {
|
|
912
1105
|
if (!config.general.allowMapZoom) {
|
|
913
|
-
|
|
914
|
-
setFocus({})
|
|
1106
|
+
setFilteredStateCountyCode('')
|
|
915
1107
|
setHasMoved(false)
|
|
916
1108
|
resetZoomTransform()
|
|
917
1109
|
}
|
|
@@ -951,14 +1143,15 @@ const CountyMap = () => {
|
|
|
951
1143
|
resetZoomTransform()
|
|
952
1144
|
}, [focus?.id])
|
|
953
1145
|
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1146
|
+
const isLoading = !runtimeData || !isTopoReady(topoData, config, runtimeFilters) || !canvasRef.current
|
|
1147
|
+
|
|
1148
|
+
useEffect(() => {
|
|
1149
|
+
if (isLoading) {
|
|
1150
|
+
return
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
drawCanvas()
|
|
1154
|
+
}, [isLoading, topoData, focus, runtimeLegend, runtimeData, featureArray, config, filteredCountyCode])
|
|
962
1155
|
|
|
963
1156
|
const showManualZoomControls = config.general.allowMapZoom
|
|
964
1157
|
const showResetControl = (hasMoved || focus.id) && (showManualZoomControls || config.general.type === 'us-geocode')
|
|
@@ -967,21 +1160,29 @@ const CountyMap = () => {
|
|
|
967
1160
|
|
|
968
1161
|
return (
|
|
969
1162
|
<ErrorBoundary component='CountyMap'>
|
|
1163
|
+
{isLoading && (
|
|
1164
|
+
<div style={{ height: 300 }}>
|
|
1165
|
+
<Loading />
|
|
1166
|
+
</div>
|
|
1167
|
+
)}
|
|
970
1168
|
<canvas
|
|
971
1169
|
ref={canvasRef}
|
|
1170
|
+
role='img'
|
|
972
1171
|
aria-label={handleMapAriaLabels(config)}
|
|
1172
|
+
data-zoom-transform={getZoomTransformString()}
|
|
1173
|
+
data-zoom-scale={getZoomScale()}
|
|
973
1174
|
onMouseMove={canvasHover}
|
|
974
1175
|
onMouseOut={() => {
|
|
975
1176
|
tooltipRef.current.style.display = 'none'
|
|
976
1177
|
tooltipRef.current.setAttribute('data-index', null)
|
|
977
1178
|
}}
|
|
978
1179
|
onClick={canvasClick}
|
|
979
|
-
className='county-map-canvas'
|
|
1180
|
+
className={'county-map-canvas' + (isLoading ? ' d-none' : '')}
|
|
980
1181
|
style={config.general.allowMapZoom ? undefined : { cursor: 'default' }}
|
|
981
1182
|
></canvas>
|
|
982
1183
|
|
|
983
1184
|
{showManualZoomControls && (
|
|
984
|
-
<div className='zoom-controls' data-html2canvas-ignore='true'>
|
|
1185
|
+
<div className={'zoom-controls' + (isLoading ? ' d-none' : '')} data-html2canvas-ignore='true'>
|
|
985
1186
|
<button onClick={handleZoomIn} aria-label='Zoom In'>
|
|
986
1187
|
<svg viewBox='0 0 24 24' stroke='currentColor' strokeWidth='3'>
|
|
987
1188
|
<line x1='12' y1='5' x2='12' y2='19' />
|