@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.
Files changed (39) hide show
  1. package/CLAUDE.local.md +0 -0
  2. package/dist/cdcmap.js +19761 -19662
  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 +7 -0
  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 +56 -39
  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/displayGeoName.ts +11 -6
  29. package/src/helpers/formatLegendLocation.ts +1 -3
  30. package/src/helpers/getStatesPicked.ts +11 -0
  31. package/src/helpers/handleMapAriaLabels.ts +2 -2
  32. package/src/hooks/useStateZoom.tsx +116 -86
  33. package/src/index.jsx +6 -1
  34. package/src/scss/main.scss +23 -12
  35. package/src/store/map.actions.ts +2 -2
  36. package/src/store/map.reducer.ts +4 -4
  37. package/src/types/MapConfig.ts +2 -3
  38. package/src/types/MapContext.ts +2 -1
  39. 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,
@@ -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 = isMobileHeightViewport(viewport) ? '12px' : '14px'
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={() => toggleLegendActive(idx, item.label, runtimeLegend, setRuntimeLegend, setAccessibleStatus)}
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
- <hr />
319
- <div className={legendClasses.div.join(' ') || ''}>
320
- {config.visual.cityStyleLabel && (
321
- <div>
322
- <svg>
323
- <Group
324
- top={
325
- config.visual.cityStyle === 'pin' ? 19 : config.visual.cityStyle === 'triangle' ? 13 : 11
326
- }
327
- left={10}
328
- >
329
- {cityStyleShapes[config.visual.cityStyle.toLowerCase()]}
330
- </Group>
331
- </svg>
332
- <p>{config.visual.cityStyleLabel}</p>
333
- </div>
334
- )}
335
-
336
- {config.visual.additionalCityStyles.map(
337
- ({ shape, label }) =>
338
- label && (
339
- <div>
340
- <svg>
341
- <Group top={shape === 'Pin' ? 19 : shape === 'Triangle' ? 13 : 11} left={10}>
342
- {cityStyleShapes[shape.toLowerCase()]}
343
- </Group>
344
- </svg>
345
- <p>{label}</p>
346
- </div>
347
- )
348
- )}
349
- </div>
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
@@ -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 }) => {