@cdc/map 4.25.7-2 → 4.25.8
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/CLAUDE.local.md +0 -0
- package/dist/cdcmap.js +19761 -19662
- package/examples/private/filter-map.json +909 -0
- package/examples/private/rsv-data.json +532 -0
- package/examples/private/test.json +222 -640
- package/index.html +34 -35
- package/package.json +3 -3
- package/src/CdcMap.tsx +7 -2
- package/src/CdcMapComponent.tsx +26 -8
- package/src/_stories/CdcMap.stories.tsx +7 -0
- package/src/_stories/_mock/multi-state.json +21389 -0
- package/src/components/CityList.tsx +4 -4
- package/src/components/DataTable.tsx +8 -4
- package/src/components/EditorPanel/components/EditorPanel.tsx +24 -38
- package/src/components/Legend/components/Legend.tsx +56 -39
- package/src/components/Modal.tsx +2 -8
- package/src/components/NavigationMenu.tsx +4 -1
- package/src/components/UsaMap/components/SingleState/SingleState.StateOutput.tsx +21 -15
- package/src/components/UsaMap/components/TerritoriesSection.tsx +2 -2
- package/src/components/UsaMap/components/UsaMap.County.tsx +6 -1
- package/src/components/UsaMap/components/UsaMap.SingleState.tsx +36 -24
- package/src/components/UsaMap/helpers/map.ts +16 -8
- package/src/components/WorldMap/WorldMap.tsx +17 -0
- package/src/context.ts +1 -0
- package/src/data/initial-state.js +8 -6
- package/src/data/supported-geos.js +185 -2
- package/src/helpers/addUIDs.ts +8 -8
- package/src/helpers/displayGeoName.ts +11 -6
- package/src/helpers/formatLegendLocation.ts +1 -3
- package/src/helpers/getStatesPicked.ts +11 -0
- package/src/helpers/handleMapAriaLabels.ts +2 -2
- package/src/hooks/useStateZoom.tsx +116 -86
- package/src/index.jsx +6 -1
- package/src/scss/main.scss +23 -12
- package/src/store/map.actions.ts +2 -2
- package/src/store/map.reducer.ts +4 -4
- package/src/types/MapConfig.ts +2 -3
- package/src/types/MapContext.ts +2 -1
- package/src/helpers/getStatePicked.ts +0 -8
|
@@ -3,7 +3,7 @@ import { scaleLinear } from 'd3-scale'
|
|
|
3
3
|
import { GlyphCircle, GlyphDiamond, GlyphSquare, GlyphStar, GlyphTriangle } from '@visx/glyph'
|
|
4
4
|
import ConfigContext from '../context'
|
|
5
5
|
import { supportedCities } from '../data/supported-geos'
|
|
6
|
-
import {
|
|
6
|
+
import { getFilterControllingStatesPicked } from './UsaMap/helpers/map'
|
|
7
7
|
import { displayGeoName, getGeoStrokeColor, SVG_HEIGHT, SVG_PADDING, SVG_WIDTH, titleCase } from '../helpers'
|
|
8
8
|
import useGeoClickHandler from '../hooks/useGeoClickHandler'
|
|
9
9
|
import useApplyTooltipsToGeo from '../hooks/useApplyTooltipsToGeo'
|
|
@@ -129,15 +129,15 @@ const CityList: React.FC<CityListProps> = ({ setSharedFilterValue, isFilterValue
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
if (geoData?.[longitudeColumnName] && geoData?.[latitudeColumnName] && config.general.geoType === 'single-state') {
|
|
132
|
-
const
|
|
133
|
-
const
|
|
132
|
+
const statesPicked = getFilterControllingStatesPicked(config, runtimeData)
|
|
133
|
+
const _statesPickedData = topoData?.states?.find(s => statesPicked.includes(s.properties.name))
|
|
134
134
|
|
|
135
135
|
const newProjection = projection.fitExtent(
|
|
136
136
|
[
|
|
137
137
|
[SVG_PADDING, SVG_PADDING],
|
|
138
138
|
[SVG_WIDTH - SVG_PADDING, SVG_HEIGHT - SVG_PADDING]
|
|
139
139
|
],
|
|
140
|
-
|
|
140
|
+
_statesPickedData
|
|
141
141
|
)
|
|
142
142
|
let coords = [Number(geoData?.[longitudeColumnName]), Number(geoData?.[latitudeColumnName])]
|
|
143
143
|
transform = `translate(${newProjection(coords)}) scale(${
|
|
@@ -12,8 +12,8 @@ import SkipTo from '@cdc/core/components/elements/SkipTo'
|
|
|
12
12
|
import Loading from '@cdc/core/components/Loading'
|
|
13
13
|
import { navigationHandler } from '../helpers'
|
|
14
14
|
import ConfigContext, { MapDispatchContext } from '../context'
|
|
15
|
+
import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
|
|
15
16
|
|
|
16
|
-
/* eslint-disable jsx-a11y/no-noninteractive-tabindex, jsx-a11y/no-static-element-interactions */
|
|
17
17
|
const DataTable = props => {
|
|
18
18
|
const {
|
|
19
19
|
state,
|
|
@@ -29,7 +29,8 @@ const DataTable = props => {
|
|
|
29
29
|
applyLegendToRow,
|
|
30
30
|
displayGeoName,
|
|
31
31
|
formatLegendLocation,
|
|
32
|
-
tabbingId
|
|
32
|
+
tabbingId,
|
|
33
|
+
interactionLabel
|
|
33
34
|
} = props
|
|
34
35
|
|
|
35
36
|
const dispatch = useContext(MapDispatchContext)
|
|
@@ -176,7 +177,10 @@ const DataTable = props => {
|
|
|
176
177
|
<a
|
|
177
178
|
download={fileName}
|
|
178
179
|
type='button'
|
|
179
|
-
onClick={
|
|
180
|
+
onClick={() => {
|
|
181
|
+
saveBlob
|
|
182
|
+
publishAnalyticsEvent('data_downloaded', 'click', interactionLabel)
|
|
183
|
+
}}
|
|
180
184
|
href={URL.createObjectURL(blob)}
|
|
181
185
|
aria-label='Download this data in a CSV file format.'
|
|
182
186
|
className={`${headerColor} no-border`}
|
|
@@ -192,7 +196,7 @@ const DataTable = props => {
|
|
|
192
196
|
const TableMediaControls = ({ belowTable }) => {
|
|
193
197
|
return (
|
|
194
198
|
<MediaControls.Section classes={['download-links']}>
|
|
195
|
-
<MediaControls.Link config={state} />
|
|
199
|
+
<MediaControls.Link config={state} interactionLabel={interactionLabel} />
|
|
196
200
|
{state.table.download && <DownloadButton />}
|
|
197
201
|
</MediaControls.Section>
|
|
198
202
|
)
|
|
@@ -48,6 +48,7 @@ import { addUIDs, HEADER_COLORS } from '../../../helpers'
|
|
|
48
48
|
import './editorPanel.styles.css'
|
|
49
49
|
import FootnotesEditor from '@cdc/core/components/EditorPanel/FootnotesEditor'
|
|
50
50
|
import { Datasets } from '@cdc/core/types/DataSet'
|
|
51
|
+
import MultiSelect from '@cdc/core/components/MultiSelect'
|
|
51
52
|
|
|
52
53
|
type MapEditorPanelProps = {
|
|
53
54
|
datasets?: Datasets
|
|
@@ -656,15 +657,6 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
|
|
|
656
657
|
}
|
|
657
658
|
})
|
|
658
659
|
break
|
|
659
|
-
case 'capitalizeLabels':
|
|
660
|
-
setConfig({
|
|
661
|
-
...config,
|
|
662
|
-
tooltips: {
|
|
663
|
-
...config.tooltips,
|
|
664
|
-
capitalizeLabels: value
|
|
665
|
-
}
|
|
666
|
-
})
|
|
667
|
-
break
|
|
668
660
|
case 'showDataTable':
|
|
669
661
|
setConfig({
|
|
670
662
|
...config,
|
|
@@ -684,15 +676,16 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
|
|
|
684
676
|
})
|
|
685
677
|
break
|
|
686
678
|
case 'chooseState':
|
|
687
|
-
let
|
|
688
|
-
|
|
689
|
-
|
|
679
|
+
let stateData = value.map(state => ({
|
|
680
|
+
fipsCode: Object.keys(supportedStatesFipsCodes).find(key => supportedStatesFipsCodes[key] === state),
|
|
681
|
+
stateName: state
|
|
682
|
+
}))
|
|
690
683
|
|
|
691
684
|
setConfig({
|
|
692
685
|
...config,
|
|
693
686
|
general: {
|
|
694
687
|
...config.general,
|
|
695
|
-
|
|
688
|
+
statesPicked: stateData
|
|
696
689
|
}
|
|
697
690
|
})
|
|
698
691
|
|
|
@@ -728,12 +721,12 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
|
|
|
728
721
|
}
|
|
729
722
|
})
|
|
730
723
|
break
|
|
731
|
-
case '
|
|
724
|
+
case 'filterControlsStatesPicked':
|
|
732
725
|
setConfig({
|
|
733
726
|
...config,
|
|
734
727
|
general: {
|
|
735
728
|
...config.general,
|
|
736
|
-
|
|
729
|
+
filterControlsStatesPicked: value
|
|
737
730
|
}
|
|
738
731
|
})
|
|
739
732
|
break
|
|
@@ -1198,13 +1191,13 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
|
|
|
1198
1191
|
{config.general.geoType === 'single-state' && runtimeData && (
|
|
1199
1192
|
<Select
|
|
1200
1193
|
label='Filter Controlling State Picked'
|
|
1201
|
-
value={config.general.
|
|
1194
|
+
value={config.general.filterControlsStatesPicked || ''}
|
|
1202
1195
|
options={[
|
|
1203
1196
|
{ value: '', label: 'None' },
|
|
1204
1197
|
...(runtimeData && columnsInData?.map(col => ({ value: col, label: col })))
|
|
1205
1198
|
]}
|
|
1206
1199
|
onChange={event => {
|
|
1207
|
-
handleEditorChanges('
|
|
1200
|
+
handleEditorChanges('filterControlsStatesPicked', event.target.value)
|
|
1208
1201
|
}}
|
|
1209
1202
|
/>
|
|
1210
1203
|
)}
|
|
@@ -1212,17 +1205,20 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
|
|
|
1212
1205
|
{/* Type */}
|
|
1213
1206
|
{/* Select > Filter a state */}
|
|
1214
1207
|
{config.general.geoType === 'single-state' && (
|
|
1215
|
-
<
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1208
|
+
<label>
|
|
1209
|
+
<span>States Selector</span>
|
|
1210
|
+
<MultiSelect
|
|
1211
|
+
selected={config.general.statesPicked.map(state => state.stateName)}
|
|
1212
|
+
options={StateOptionList().map(option => ({
|
|
1213
|
+
value: option.props.value,
|
|
1214
|
+
label: option.props.children
|
|
1215
|
+
}))}
|
|
1216
|
+
fieldName={'statesPicked'}
|
|
1217
|
+
updateField={(_, __, ___, selectedOptions) => {
|
|
1218
|
+
handleEditorChanges('chooseState', selectedOptions)
|
|
1219
|
+
}}
|
|
1220
|
+
/>
|
|
1221
|
+
</label>
|
|
1226
1222
|
)}
|
|
1227
1223
|
{/* Type */}
|
|
1228
1224
|
<Select
|
|
@@ -2872,16 +2868,6 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
|
|
|
2872
2868
|
updateField={updateField}
|
|
2873
2869
|
/>
|
|
2874
2870
|
)}
|
|
2875
|
-
<label className='checkbox'>
|
|
2876
|
-
<input
|
|
2877
|
-
type='checkbox'
|
|
2878
|
-
checked={config.tooltips.capitalizeLabels}
|
|
2879
|
-
onChange={event => {
|
|
2880
|
-
handleEditorChanges('capitalizeLabels', event.target.checked)
|
|
2881
|
-
}}
|
|
2882
|
-
/>
|
|
2883
|
-
<span className='edit-label'>Capitalize text inside tooltip</span>
|
|
2884
|
-
</label>
|
|
2885
2871
|
</AccordionItemPanel>
|
|
2886
2872
|
</AccordionItem>
|
|
2887
2873
|
<AccordionItem>
|
|
@@ -18,12 +18,13 @@ import { GlyphStar, GlyphTriangle, GlyphDiamond, GlyphSquare, GlyphCircle } from
|
|
|
18
18
|
import { Group } from '@visx/group'
|
|
19
19
|
import './index.scss'
|
|
20
20
|
import { type ViewPort } from '@cdc/core/types/ViewPort'
|
|
21
|
-
import { isBelowBreakpoint,
|
|
21
|
+
import { isBelowBreakpoint, isMobileFontViewport } from '@cdc/core/helpers/viewports'
|
|
22
22
|
import { displayDataAsText } from '@cdc/core/helpers/displayDataAsText'
|
|
23
23
|
import { toggleLegendActive } from '@cdc/map/src/helpers/toggleLegendActive'
|
|
24
24
|
import { resetLegendToggles } from '../../../helpers'
|
|
25
25
|
import { MapContext } from '../../../types/MapContext'
|
|
26
26
|
import LegendGroup from './LegendGroup/Legend.Group'
|
|
27
|
+
import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
|
|
27
28
|
|
|
28
29
|
const LEGEND_PADDING = 30
|
|
29
30
|
|
|
@@ -32,10 +33,11 @@ type LegendProps = {
|
|
|
32
33
|
dimensions: DimensionsType
|
|
33
34
|
containerWidthPadding: number
|
|
34
35
|
currentViewport: ViewPort
|
|
36
|
+
interactionLabel: string
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
|
|
38
|
-
const { skipId, containerWidthPadding } = props
|
|
40
|
+
const { skipId, containerWidthPadding, interactionLabel } = props
|
|
39
41
|
|
|
40
42
|
const {
|
|
41
43
|
config,
|
|
@@ -105,7 +107,7 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
|
|
|
105
107
|
|
|
106
108
|
const legendList = (patternsOnly = false) => {
|
|
107
109
|
const formattedItems = patternsOnly ? [] : getFormattedLegendItems()
|
|
108
|
-
const patternsOnlyFont =
|
|
110
|
+
const patternsOnlyFont = isMobileFontViewport(viewport) ? '12px' : '14px'
|
|
109
111
|
const hasDisabledItems = formattedItems.some(item => item.disabled)
|
|
110
112
|
let legendItems
|
|
111
113
|
|
|
@@ -127,11 +129,25 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
|
|
|
127
129
|
className={handleListItemClass()}
|
|
128
130
|
key={idx}
|
|
129
131
|
title={`Legend item ${item.label} - Click to disable`}
|
|
130
|
-
onClick={() =>
|
|
132
|
+
onClick={() => {
|
|
133
|
+
toggleLegendActive(idx, item.label, runtimeLegend, setRuntimeLegend, setAccessibleStatus)
|
|
134
|
+
publishAnalyticsEvent(
|
|
135
|
+
`map_legend_item_toggled--isolate-mode`,
|
|
136
|
+
'click',
|
|
137
|
+
`${interactionLabel}|${item.label}`,
|
|
138
|
+
'map'
|
|
139
|
+
)
|
|
140
|
+
}}
|
|
131
141
|
onKeyDown={e => {
|
|
132
142
|
if (e.key === 'Enter') {
|
|
133
143
|
e.preventDefault()
|
|
134
144
|
toggleLegendActive(idx, item.label, runtimeLegend, setRuntimeLegend, setAccessibleStatus)
|
|
145
|
+
publishAnalyticsEvent(
|
|
146
|
+
`map_legend_item_toggled--isolate-mode`,
|
|
147
|
+
'keydown',
|
|
148
|
+
`${interactionLabel}|${item.label}`,
|
|
149
|
+
'map'
|
|
150
|
+
)
|
|
135
151
|
}
|
|
136
152
|
}}
|
|
137
153
|
tabIndex={0}
|
|
@@ -224,6 +240,7 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
|
|
|
224
240
|
if (e) {
|
|
225
241
|
e.preventDefault()
|
|
226
242
|
}
|
|
243
|
+
publishAnalyticsEvent('map_legend_reset', 'click', interactionLabel, 'map')
|
|
227
244
|
resetLegendToggles(runtimeLegend, setRuntimeLegend)
|
|
228
245
|
dispatch({
|
|
229
246
|
type: 'SET_ACCESSIBLE_STATUS',
|
|
@@ -314,41 +331,41 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
|
|
|
314
331
|
|
|
315
332
|
{((config.visual.additionalCityStyles && config.visual.additionalCityStyles.some(c => c.label)) ||
|
|
316
333
|
config.visual.cityStyleLabel) && (
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
334
|
+
<>
|
|
335
|
+
<hr />
|
|
336
|
+
<div className={legendClasses.div.join(' ') || ''}>
|
|
337
|
+
{config.visual.cityStyleLabel && (
|
|
338
|
+
<div>
|
|
339
|
+
<svg>
|
|
340
|
+
<Group
|
|
341
|
+
top={
|
|
342
|
+
config.visual.cityStyle === 'pin' ? 19 : config.visual.cityStyle === 'triangle' ? 13 : 11
|
|
343
|
+
}
|
|
344
|
+
left={10}
|
|
345
|
+
>
|
|
346
|
+
{cityStyleShapes[config.visual.cityStyle.toLowerCase()]}
|
|
347
|
+
</Group>
|
|
348
|
+
</svg>
|
|
349
|
+
<p>{config.visual.cityStyleLabel}</p>
|
|
350
|
+
</div>
|
|
351
|
+
)}
|
|
352
|
+
|
|
353
|
+
{config.visual.additionalCityStyles.map(
|
|
354
|
+
({ shape, label }) =>
|
|
355
|
+
label && (
|
|
356
|
+
<div>
|
|
357
|
+
<svg>
|
|
358
|
+
<Group top={shape === 'Pin' ? 19 : shape === 'Triangle' ? 13 : 11} left={10}>
|
|
359
|
+
{cityStyleShapes[shape.toLowerCase()]}
|
|
360
|
+
</Group>
|
|
361
|
+
</svg>
|
|
362
|
+
<p>{label}</p>
|
|
363
|
+
</div>
|
|
364
|
+
)
|
|
365
|
+
)}
|
|
366
|
+
</div>
|
|
367
|
+
</>
|
|
368
|
+
)}
|
|
352
369
|
{runtimeLegend.disabledAmt > 0 && (
|
|
353
370
|
<Button className={legendClasses.showAllButton.join(' ')} onClick={handleReset}>
|
|
354
371
|
Show All
|
package/src/components/Modal.tsx
CHANGED
|
@@ -5,19 +5,13 @@ import useApplyTooltipsToGeo from '../hooks/useApplyTooltipsToGeo'
|
|
|
5
5
|
import { MapContext } from '../types/MapContext'
|
|
6
6
|
|
|
7
7
|
const Modal = () => {
|
|
8
|
-
const { content,
|
|
9
|
-
const { capitalizeLabels } = config.tooltips
|
|
8
|
+
const { content, currentViewport: viewport } = useContext<MapContext>(ConfigContext)
|
|
10
9
|
const { applyTooltipsToGeo } = useApplyTooltipsToGeo()
|
|
11
10
|
const tooltip = applyTooltipsToGeo(content.geoName, content.keyedData, 'jsx')
|
|
12
11
|
const dispatch = useContext(MapDispatchContext)
|
|
13
12
|
|
|
14
13
|
return (
|
|
15
|
-
<section
|
|
16
|
-
className={
|
|
17
|
-
capitalizeLabels ? 'modal-content tooltip capitalize ' + viewport : 'modal-content tooltip ' + viewport
|
|
18
|
-
}
|
|
19
|
-
aria-hidden='true'
|
|
20
|
-
>
|
|
14
|
+
<section className={'modal-content tooltip ' + viewport} aria-hidden='true'>
|
|
21
15
|
<div className='content'>{tooltip}</div>
|
|
22
16
|
<Icon
|
|
23
17
|
display='close'
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import React, { useContext, useEffect, useState } from 'react'
|
|
2
2
|
import ConfigContext from '../context'
|
|
3
|
+
import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
|
|
3
4
|
|
|
4
5
|
const NavigationMenu = ({ data, navigationHandler, options, columns, displayGeoName, mapTabbingID }) => {
|
|
5
|
-
const {
|
|
6
|
+
const { interactionLabel } = useContext(ConfigContext)
|
|
6
7
|
const [activeGeo, setActiveGeo] = useState('')
|
|
7
8
|
const [dropdownItems, setDropdownItems] = useState({})
|
|
8
9
|
|
|
@@ -11,6 +12,8 @@ const NavigationMenu = ({ data, navigationHandler, options, columns, displayGeoN
|
|
|
11
12
|
if (activeGeo !== '') {
|
|
12
13
|
const urlString = data[dropdownItems[activeGeo]][columns.navigate.name]
|
|
13
14
|
|
|
15
|
+
publishAnalyticsEvent('map_navigation_menu', 'submit', `${interactionLabel}|${urlString}`)
|
|
16
|
+
|
|
14
17
|
navigationHandler(urlString)
|
|
15
18
|
}
|
|
16
19
|
}
|
|
@@ -1,37 +1,43 @@
|
|
|
1
1
|
import { useContext } from 'react'
|
|
2
|
-
import {
|
|
2
|
+
import { Topology } from 'topojson-client'
|
|
3
3
|
import ConfigContext from '../../../../context'
|
|
4
|
-
import {
|
|
4
|
+
import { getGeoStrokeColor } from '../../../../helpers/colors'
|
|
5
|
+
import { getStatesPicked } from '../../../../helpers/getStatesPicked'
|
|
5
6
|
|
|
6
7
|
type StateOutputProps = {
|
|
7
8
|
topoData: Topology
|
|
8
9
|
path: any
|
|
9
10
|
scale: any
|
|
11
|
+
runtimeData?: any
|
|
10
12
|
}
|
|
11
13
|
|
|
12
|
-
const StateOutput: React.FC<StateOutputProps> = ({ topoData, path, scale,
|
|
14
|
+
const StateOutput: React.FC<StateOutputProps> = ({ topoData, path, scale, runtimeData }: StateOutputProps) => {
|
|
13
15
|
const { config } = useContext(ConfigContext)
|
|
14
|
-
if (!topoData?.
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
if (!topoData?.states) return null
|
|
17
|
+
|
|
18
|
+
// Use filter-aware state selection instead of direct config access
|
|
19
|
+
const statesPickedData = getStatesPicked(config, runtimeData)
|
|
20
|
+
const stateNames = statesPickedData.map(sp => sp.stateName)
|
|
21
|
+
|
|
22
|
+
const statesPicked = topoData.states.filter(s => {
|
|
23
|
+
return stateNames.includes(s.properties.name)
|
|
17
24
|
})
|
|
18
25
|
|
|
19
26
|
const geoStrokeColor = getGeoStrokeColor(config)
|
|
20
|
-
const geoFillColor = getGeoFillColor(config)
|
|
21
27
|
|
|
22
|
-
|
|
28
|
+
const stateLines = statesPicked.map(s => path(s.geometry))
|
|
23
29
|
|
|
24
|
-
return (
|
|
30
|
+
return stateLines.map((line, index) => (
|
|
25
31
|
<g
|
|
26
|
-
key={
|
|
27
|
-
className='single-state'
|
|
28
|
-
style={{ fill:
|
|
32
|
+
key={`single-state-${index}`}
|
|
33
|
+
className='single-state pe-none'
|
|
34
|
+
style={{ fill: 'transparent' }}
|
|
29
35
|
stroke={geoStrokeColor}
|
|
30
|
-
strokeWidth={
|
|
36
|
+
strokeWidth={2 / scale}
|
|
31
37
|
>
|
|
32
|
-
<path tabIndex={-1} className='state-path' d={
|
|
38
|
+
<path tabIndex={-1} className='state-path' d={line} />
|
|
33
39
|
</g>
|
|
34
|
-
)
|
|
40
|
+
))
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
export default StateOutput
|
|
@@ -55,7 +55,7 @@ const TerritoriesSection: React.FC<TerritoriesSectionProps> = ({ territories, lo
|
|
|
55
55
|
<div className='d-flex flex-wrap' style={{ columnGap: '1.5rem' }}>
|
|
56
56
|
{(usTerritories.length > 0 || config.general.territoriesAlwaysShow) && (
|
|
57
57
|
<div>
|
|
58
|
-
<
|
|
58
|
+
<span className='territories-label'>U.S. territories</span>
|
|
59
59
|
<span
|
|
60
60
|
className={`mt-2 ${isMobileViewport ? 'mb-3' : 'mb-4'} d-flex territories`}
|
|
61
61
|
style={{ minWidth: `${usTerritories.length * SVG_WIDTH + (usTerritories.length - 1) * SVG_GAP}px` }}
|
|
@@ -66,7 +66,7 @@ const TerritoriesSection: React.FC<TerritoriesSectionProps> = ({ territories, lo
|
|
|
66
66
|
)}
|
|
67
67
|
{(freelyAssociatedStates.length > 0 || config.general.territoriesAlwaysShow) && (
|
|
68
68
|
<div>
|
|
69
|
-
<
|
|
69
|
+
<span className='territories-label'>Freely associated states</span>
|
|
70
70
|
<span
|
|
71
71
|
className={`mt-2 ${isMobileViewport ? 'mb-3' : 'mb-4'} d-flex territories`}
|
|
72
72
|
style={{
|
|
@@ -14,6 +14,7 @@ import { applyLegendToRow } from '../../../helpers/applyLegendToRow'
|
|
|
14
14
|
import useApplyTooltipsToGeo from '../../../hooks/useApplyTooltipsToGeo'
|
|
15
15
|
import { MapConfig } from '../../../types/MapConfig'
|
|
16
16
|
import { DEFAULT_MAP_BACKGROUND } from '../../../helpers/constants'
|
|
17
|
+
import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
|
|
17
18
|
|
|
18
19
|
const getCountyTopoURL = year => {
|
|
19
20
|
return `https://www.cdc.gov/TemplatePackage/contrib/data/county-topography/cb_${year}_us_county_20m.json`
|
|
@@ -138,7 +139,8 @@ const CountyMap = () => {
|
|
|
138
139
|
tooltipId,
|
|
139
140
|
tooltipRef,
|
|
140
141
|
legendMemo,
|
|
141
|
-
legendSpecialClassLastMemo
|
|
142
|
+
legendSpecialClassLastMemo,
|
|
143
|
+
configUrl
|
|
142
144
|
} = useContext(ConfigContext)
|
|
143
145
|
|
|
144
146
|
// CREATE STATE LINES
|
|
@@ -210,6 +212,7 @@ const CountyMap = () => {
|
|
|
210
212
|
const lineWidth = 1
|
|
211
213
|
|
|
212
214
|
const onReset = () => {
|
|
215
|
+
publishAnalyticsEvent('map_reset_zoom_level', 'click', configUrl, 'map')
|
|
213
216
|
setConfig({
|
|
214
217
|
...config,
|
|
215
218
|
mapPosition: { coordinates: [0, 30], zoom: 1 }
|
|
@@ -268,6 +271,8 @@ const CountyMap = () => {
|
|
|
268
271
|
|
|
269
272
|
// Redraw with focus on state
|
|
270
273
|
setFocus({ id: clickedState.id, index: focusIndex, center: geoCentroid(clickedState), feature: clickedState })
|
|
274
|
+
publishAnalyticsEvent('map_zoomed_in', 'click', `${configUrl}|zoom_level_3|${clickedState.properties.name}`, 'map')
|
|
275
|
+
|
|
271
276
|
}
|
|
272
277
|
if (config.general.type === 'us-geocode') {
|
|
273
278
|
const geoRadius = (config.visual.geoCodeCircleSize || 5) * (focus.id ? 2 : 1)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, memo, useContext } from 'react'
|
|
1
|
+
import { useEffect, memo, useContext, useMemo } from 'react'
|
|
2
2
|
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
3
3
|
import { geoPath } from 'd3-geo'
|
|
4
4
|
import { CustomProjection } from '@visx/geo'
|
|
@@ -24,7 +24,7 @@ import { getTopoData, getCurrentTopoYear, isTopoReady } from '../helpers/map'
|
|
|
24
24
|
import useGeoClickHandler from '../../../hooks/useGeoClickHandler'
|
|
25
25
|
import { SVG_WIDTH, SVG_HEIGHT, SVG_PADDING, SVG_VIEWBOX } from '../../../helpers'
|
|
26
26
|
import _ from 'lodash'
|
|
27
|
-
import {
|
|
27
|
+
import { getStatesPicked } from '../../../helpers/getStatesPicked'
|
|
28
28
|
|
|
29
29
|
const SingleStateMap: React.FC = () => {
|
|
30
30
|
const {
|
|
@@ -42,8 +42,17 @@ const SingleStateMap: React.FC = () => {
|
|
|
42
42
|
|
|
43
43
|
const dispatch = useContext(MapDispatchContext)
|
|
44
44
|
const { handleMoveEnd, handleZoomIn, handleZoomOut, handleReset, projection } = useStateZoom(topoData)
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
|
|
46
|
+
// Memoize statesPicked to prevent creating new arrays on every render
|
|
47
|
+
const statesPicked = useMemo(() => {
|
|
48
|
+
return getStatesPicked(config, runtimeData)
|
|
49
|
+
}, [
|
|
50
|
+
config.general.statesPicked?.length,
|
|
51
|
+
config.general.statesPicked?.[0]?.stateName
|
|
52
|
+
// Don't include runtimeData as it causes excessive re-renders
|
|
53
|
+
])
|
|
54
|
+
|
|
55
|
+
const statesToShow = topoData?.states?.find(s => statesPicked.map(sp => sp.stateName).includes(s.properties.name))
|
|
47
56
|
|
|
48
57
|
const { geoClickHandler } = useGeoClickHandler()
|
|
49
58
|
|
|
@@ -61,7 +70,7 @@ const SingleStateMap: React.FC = () => {
|
|
|
61
70
|
dispatch({ type: 'SET_TOPO_DATA', payload: response })
|
|
62
71
|
})
|
|
63
72
|
}
|
|
64
|
-
}, [
|
|
73
|
+
}, [runtimeFilters?.length, topoData?.year])
|
|
65
74
|
|
|
66
75
|
if (!isTopoReady(topoData, config, runtimeFilters)) {
|
|
67
76
|
return (
|
|
@@ -72,8 +81,8 @@ const SingleStateMap: React.FC = () => {
|
|
|
72
81
|
}
|
|
73
82
|
|
|
74
83
|
const checkForNoData = () => {
|
|
75
|
-
// If no
|
|
76
|
-
if (!
|
|
84
|
+
// If no statesPicked, return true
|
|
85
|
+
if (statesPicked?.every(sp => !sp.fipsCode)) return true
|
|
77
86
|
}
|
|
78
87
|
|
|
79
88
|
// Constructs and displays markup for all geos on the map (except territories right now)
|
|
@@ -81,17 +90,6 @@ const SingleStateMap: React.FC = () => {
|
|
|
81
90
|
const counties = geographies[0].feature.counties
|
|
82
91
|
|
|
83
92
|
let geosJsx = []
|
|
84
|
-
|
|
85
|
-
// Push config lines
|
|
86
|
-
geosJsx.push(
|
|
87
|
-
// prettier-ignore
|
|
88
|
-
<SingleState.StateOutput
|
|
89
|
-
topoData={topoData}
|
|
90
|
-
path={path}
|
|
91
|
-
scale={scale}
|
|
92
|
-
/>
|
|
93
|
-
)
|
|
94
|
-
|
|
95
93
|
// Push county lines
|
|
96
94
|
geosJsx.push(
|
|
97
95
|
// prettier-ignore
|
|
@@ -103,6 +101,16 @@ const SingleStateMap: React.FC = () => {
|
|
|
103
101
|
path={path}
|
|
104
102
|
/>
|
|
105
103
|
)
|
|
104
|
+
// Push config lines
|
|
105
|
+
geosJsx.push(
|
|
106
|
+
// prettier-ignore
|
|
107
|
+
<SingleState.StateOutput
|
|
108
|
+
topoData={topoData}
|
|
109
|
+
path={path}
|
|
110
|
+
scale={scale}
|
|
111
|
+
runtimeData={runtimeData}
|
|
112
|
+
/>
|
|
113
|
+
)
|
|
106
114
|
|
|
107
115
|
// Push city list
|
|
108
116
|
geosJsx.push(
|
|
@@ -121,7 +129,7 @@ const SingleStateMap: React.FC = () => {
|
|
|
121
129
|
}
|
|
122
130
|
return (
|
|
123
131
|
<ErrorBoundary component='SingleStateMap'>
|
|
124
|
-
{
|
|
132
|
+
{statesPicked.length && config.general.allowMapZoom && statesPicked.some(sp => sp.fipsCode) && (
|
|
125
133
|
<svg
|
|
126
134
|
viewBox={SVG_VIEWBOX}
|
|
127
135
|
preserveAspectRatio='xMinYMin'
|
|
@@ -150,7 +158,9 @@ const SingleStateMap: React.FC = () => {
|
|
|
150
158
|
data={[
|
|
151
159
|
{
|
|
152
160
|
states: topoData?.states,
|
|
153
|
-
counties: topoData.counties.filter(c =>
|
|
161
|
+
counties: topoData.counties.filter(c =>
|
|
162
|
+
statesPicked.map(sp => sp.fipsCode).includes(c.id.substring(0, 2))
|
|
163
|
+
)
|
|
154
164
|
}
|
|
155
165
|
]}
|
|
156
166
|
projection={geoAlbersUsaTerritories}
|
|
@@ -159,7 +169,7 @@ const SingleStateMap: React.FC = () => {
|
|
|
159
169
|
[SVG_PADDING, SVG_PADDING],
|
|
160
170
|
[SVG_WIDTH - SVG_PADDING, SVG_HEIGHT - SVG_PADDING]
|
|
161
171
|
],
|
|
162
|
-
|
|
172
|
+
statesToShow
|
|
163
173
|
]}
|
|
164
174
|
>
|
|
165
175
|
{({ features, projection }) => {
|
|
@@ -182,7 +192,7 @@ const SingleStateMap: React.FC = () => {
|
|
|
182
192
|
</ZoomableGroup>
|
|
183
193
|
</svg>
|
|
184
194
|
)}
|
|
185
|
-
{
|
|
195
|
+
{statesPicked && !config.general.allowMapZoom && statesPicked.some(sp => sp.fipsCode) && (
|
|
186
196
|
<svg
|
|
187
197
|
viewBox={SVG_VIEWBOX}
|
|
188
198
|
preserveAspectRatio='xMinYMin'
|
|
@@ -201,7 +211,9 @@ const SingleStateMap: React.FC = () => {
|
|
|
201
211
|
data={[
|
|
202
212
|
{
|
|
203
213
|
states: topoData?.states,
|
|
204
|
-
counties: topoData.counties.filter(c =>
|
|
214
|
+
counties: topoData.counties.filter(c =>
|
|
215
|
+
statesPicked.map(sp => sp.fipsCode).includes(c.id.substring(0, 2))
|
|
216
|
+
)
|
|
205
217
|
}
|
|
206
218
|
]}
|
|
207
219
|
projection={geoAlbersUsaTerritories}
|
|
@@ -210,7 +222,7 @@ const SingleStateMap: React.FC = () => {
|
|
|
210
222
|
[SVG_PADDING, SVG_PADDING],
|
|
211
223
|
[SVG_WIDTH - SVG_PADDING, SVG_HEIGHT - SVG_PADDING]
|
|
212
224
|
],
|
|
213
|
-
|
|
225
|
+
statesToShow
|
|
214
226
|
]}
|
|
215
227
|
>
|
|
216
228
|
{({ features }) => {
|