@cdc/map 4.25.7 → 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.
Files changed (44) hide show
  1. package/CLAUDE.local.md +0 -0
  2. package/dist/cdcmap.js +22037 -22074
  3. package/examples/private/filter-map.json +909 -0
  4. package/examples/private/rsv-data.json +532 -0
  5. package/examples/private/test.json +222 -640
  6. package/index.html +34 -35
  7. package/package.json +3 -3
  8. package/src/CdcMap.tsx +7 -2
  9. package/src/CdcMapComponent.tsx +26 -8
  10. package/src/_stories/CdcMap.stories.tsx +8 -11
  11. package/src/_stories/_mock/multi-state.json +21389 -0
  12. package/src/components/CityList.tsx +4 -4
  13. package/src/components/DataTable.tsx +8 -4
  14. package/src/components/EditorPanel/components/EditorPanel.tsx +24 -38
  15. package/src/components/Legend/components/Legend.tsx +23 -35
  16. package/src/components/Modal.tsx +2 -8
  17. package/src/components/NavigationMenu.tsx +4 -1
  18. package/src/components/UsaMap/components/SingleState/SingleState.StateOutput.tsx +21 -15
  19. package/src/components/UsaMap/components/TerritoriesSection.tsx +2 -2
  20. package/src/components/UsaMap/components/UsaMap.County.tsx +6 -1
  21. package/src/components/UsaMap/components/UsaMap.SingleState.tsx +36 -24
  22. package/src/components/UsaMap/helpers/map.ts +16 -8
  23. package/src/components/WorldMap/WorldMap.tsx +17 -0
  24. package/src/context.ts +1 -0
  25. package/src/data/initial-state.js +8 -6
  26. package/src/data/supported-geos.js +185 -2
  27. package/src/helpers/addUIDs.ts +8 -8
  28. package/src/helpers/applyColorToLegend.ts +24 -43
  29. package/src/helpers/applyLegendToRow.ts +5 -7
  30. package/src/helpers/displayGeoName.ts +11 -6
  31. package/src/helpers/formatLegendLocation.ts +1 -3
  32. package/src/helpers/generateRuntimeLegend.ts +149 -333
  33. package/src/helpers/getStatesPicked.ts +11 -0
  34. package/src/helpers/handleMapAriaLabels.ts +2 -2
  35. package/src/hooks/useStateZoom.tsx +116 -86
  36. package/src/index.jsx +6 -1
  37. package/src/scss/main.scss +23 -12
  38. package/src/store/map.actions.ts +2 -2
  39. package/src/store/map.reducer.ts +4 -4
  40. package/src/types/MapConfig.ts +2 -3
  41. package/src/types/MapContext.ts +2 -1
  42. package/src/types/runtimeLegend.ts +1 -15
  43. package/src/_stories/_mock/floating-point.json +0 -427
  44. 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 { getFilterControllingStatePicked } from './UsaMap/helpers/map'
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 statePicked = getFilterControllingStatePicked(config, runtimeData)
133
- const _statePickedData = topoData?.states?.find(s => s.properties.name === statePicked)
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
- _statePickedData
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={saveBlob}
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 fipsCode = Object.keys(supportedStatesFipsCodes).find(key => supportedStatesFipsCodes[key] === value)
688
- let stateName = value
689
- let stateData = { fipsCode, stateName }
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
- statePicked: stateData
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 'filterControlsStatePicked':
724
+ case 'filterControlsStatesPicked':
732
725
  setConfig({
733
726
  ...config,
734
727
  general: {
735
728
  ...config.general,
736
- filterControlsStatePicked: value
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.filterControlsStatePicked || ''}
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('filterControlsStatePicked', event.target.value)
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
- <Select
1216
- label='State Selector'
1217
- value={config.general.statePicked?.stateName || ''}
1218
- options={StateOptionList().map(option => ({
1219
- value: option.props.value,
1220
- label: option.props.children
1221
- }))}
1222
- onChange={event => {
1223
- handleEditorChanges('chooseState', event.target.value)
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, isMobileHeightViewport } from '@cdc/core/helpers/viewports'
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,
@@ -94,8 +96,7 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
94
96
  label: parse(legendLabel),
95
97
  disabled: entry.disabled,
96
98
  special: entry.hasOwnProperty('special'),
97
- value: legend.type === 'category' ? entry.value : [entry.min, entry.max],
98
- categoryValue: legend.type === 'category' ? entry.value : undefined
99
+ value: [entry.min, entry.max]
99
100
  }
100
101
  })
101
102
  } catch (e) {
@@ -106,7 +107,7 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
106
107
 
107
108
  const legendList = (patternsOnly = false) => {
108
109
  const formattedItems = patternsOnly ? [] : getFormattedLegendItems()
109
- const patternsOnlyFont = isMobileHeightViewport(viewport) ? '12px' : '14px'
110
+ const patternsOnlyFont = isMobileFontViewport(viewport) ? '12px' : '14px'
110
111
  const hasDisabledItems = formattedItems.some(item => item.disabled)
111
112
  let legendItems
112
113
 
@@ -123,44 +124,30 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
123
124
  dispatch({ type: 'SET_ACCESSIBLE_STATUS', payload: message })
124
125
  }
125
126
 
126
- // Find the correct runtime index for toggling
127
- // This is needed because special classes may have been moved to the end
128
- const findRuntimeIndex = () => {
129
- if (!runtimeLegend.items) return idx
130
-
131
- return runtimeLegend.items.findIndex(runtimeItem => {
132
- if (item.special && runtimeItem.special) {
133
- // For special classes, match by label (since formatted item label comes from runtime item)
134
- const runtimeLabel = runtimeItem.label || runtimeItem.value
135
- const itemLabel = typeof item.label === 'string' ? item.label : item.label?.props?.children || item.label
136
- return runtimeLabel === itemLabel
137
- } else if (!item.special && !runtimeItem.special) {
138
- // For categorical/qualitative items, match by single value
139
- if (config.legend.type === 'category' && item.categoryValue !== undefined) {
140
- return runtimeItem.value === item.categoryValue
141
- }
142
- // For numeric items, match by min/max values
143
- return runtimeItem.min === item.value?.[0] && runtimeItem.max === item.value?.[1]
144
- }
145
- return false
146
- })
147
- }
148
-
149
- const runtimeIndex = findRuntimeIndex()
150
- const safeRuntimeIndex = runtimeIndex >= 0 ? runtimeIndex : idx
151
-
152
127
  return (
153
128
  <li
154
129
  className={handleListItemClass()}
155
130
  key={idx}
156
131
  title={`Legend item ${item.label} - Click to disable`}
157
- onClick={() =>
158
- toggleLegendActive(safeRuntimeIndex, item.label, runtimeLegend, setRuntimeLegend, setAccessibleStatus)
159
- }
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
+ }}
160
141
  onKeyDown={e => {
161
142
  if (e.key === 'Enter') {
162
143
  e.preventDefault()
163
- toggleLegendActive(safeRuntimeIndex, item.label, runtimeLegend, setRuntimeLegend, setAccessibleStatus)
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
+ )
164
151
  }
165
152
  }}
166
153
  tabIndex={0}
@@ -253,6 +240,7 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
253
240
  if (e) {
254
241
  e.preventDefault()
255
242
  }
243
+ publishAnalyticsEvent('map_legend_reset', 'click', interactionLabel, 'map')
256
244
  resetLegendToggles(runtimeLegend, setRuntimeLegend)
257
245
  dispatch({
258
246
  type: 'SET_ACCESSIBLE_STATUS',
@@ -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, config, currentViewport: viewport } = useContext<MapContext>(ConfigContext)
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 { state } = useContext(ConfigContext)
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 { mesh, Topology } from 'topojson-client'
2
+ import { Topology } from 'topojson-client'
3
3
  import ConfigContext from '../../../../context'
4
- import { getGeoFillColor, getGeoStrokeColor } from '../../../../helpers/colors'
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, stateToShow }: StateOutputProps) => {
14
+ const StateOutput: React.FC<StateOutputProps> = ({ topoData, path, scale, runtimeData }: StateOutputProps) => {
13
15
  const { config } = useContext(ConfigContext)
14
- if (!topoData?.objects?.states) return null
15
- let geo = topoData.objects.states.geometries.filter(s => {
16
- return s.properties.name === config.general.statePicked.stateName
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
- let stateLines = path(mesh(topoData, geo[0]))
28
+ const stateLines = statesPicked.map(s => path(s.geometry))
23
29
 
24
- return (
30
+ return stateLines.map((line, index) => (
25
31
  <g
26
- key={'single-state'}
27
- className='single-state'
28
- style={{ fill: geoFillColor }}
32
+ key={`single-state-${index}`}
33
+ className='single-state pe-none'
34
+ style={{ fill: 'transparent' }}
29
35
  stroke={geoStrokeColor}
30
- strokeWidth={0.95 / scale}
36
+ strokeWidth={2 / scale}
31
37
  >
32
- <path tabIndex={-1} className='state-path' d={stateLines} />
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
- <h5 className='territories-label'>U.S. territories</h5>
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
- <h5 className='territories-label'>Freely associated states</h5>
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 { getStatePicked } from '../../../helpers/getStatePicked'
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
- const statePicked = getStatePicked(config, runtimeData)
46
- const stateToShow = topoData?.states?.find(s => s.properties.name === statePicked.stateName)
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
- }, [config.general.countyCensusYear, config.general.filterControlsCountyYear, JSON.stringify(runtimeFilters)])
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 statePicked, return true
76
- if (!statePicked.fipsCode) return true
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
- {statePicked && config.general.allowMapZoom && statePicked.fipsCode && (
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 => c.id.substring(0, 2) === statePicked.fipsCode)
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
- stateToShow
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
- {statePicked && !config.general.allowMapZoom && statePicked.fipsCode && (
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 => c.id.substring(0, 2) === statePicked.fipsCode)
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
- stateToShow
225
+ statesToShow
214
226
  ]}
215
227
  >
216
228
  {({ features }) => {
@@ -96,16 +96,24 @@ export const hasMoreThanFromHash = (data: { [key: string]: any }): boolean => {
96
96
  return otherKeys.length > 0
97
97
  }
98
98
 
99
- export const getFilterControllingStatePicked = (state, runtimeData) => {
100
- if (!state.general.filterControlsStatePicked || !runtimeData) {
101
- const statePicked = state?.general?.statePicked?.stateName
102
- return statePicked
99
+ export const getFilterControllingStatesPicked = (state, runtimeData) => {
100
+ if (!state.general.filterControlsStatesPicked || !runtimeData) {
101
+ return state?.general?.statesPicked?.map(sp => sp.stateName) || []
103
102
  } else {
104
103
  if (hasMoreThanFromHash(runtimeData)) {
105
- let statePickedFromFilter = Object.values(runtimeData)?.map(s => s[state.general.filterControlsStatePicked])?.[0]
106
- const statePicked = statePickedFromFilter || state.general.statePicked.stateName || 'Alabama'
107
- return statePicked
104
+ let statesPickedFromFilter = Object.values(runtimeData)?.map(
105
+ s => s[state.general.filterControlsStatesPicked]
106
+ )?.[0]
107
+
108
+ // Only need to check if filter result is an array since it could be a single value
109
+ if (Array.isArray(statesPickedFromFilter)) {
110
+ return statesPickedFromFilter
111
+ } else if (statesPickedFromFilter) {
112
+ return [statesPickedFromFilter]
113
+ } else {
114
+ return state?.general?.statesPicked?.map(sp => sp.stateName) || ['Alabama']
115
+ }
108
116
  }
109
- return null
117
+ return state?.general?.statesPicked?.map(sp => sp.stateName) || []
110
118
  }
111
119
  }