@cdc/map 4.25.7 → 4.25.10

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 (99) hide show
  1. package/.claude/settings.local.json +30 -0
  2. package/CLAUDE.local.md +0 -0
  3. package/dist/cdcmap.js +54785 -53159
  4. package/examples/private/c.json +290 -0
  5. package/examples/private/canvas-city-hover.json +787 -0
  6. package/examples/private/d.json +345 -0
  7. package/examples/private/filter-map.json +909 -0
  8. package/examples/private/g.json +1 -0
  9. package/examples/private/h.json +105911 -0
  10. package/examples/private/measles-data.json +378 -0
  11. package/examples/private/measles.json +211 -0
  12. package/examples/private/north-dakota.json +1132 -0
  13. package/examples/private/rsv-data.json +532 -0
  14. package/examples/private/state-with-pattern.json +883 -0
  15. package/examples/private/test.json +222 -640
  16. package/index.html +1 -1
  17. package/package.json +26 -5
  18. package/src/CdcMap.tsx +28 -8
  19. package/src/CdcMapComponent.tsx +230 -306
  20. package/src/_stories/CdcMap.Filters.stories.tsx +2 -2
  21. package/src/_stories/CdcMap.Legend.Gradient.stories.tsx +3 -3
  22. package/src/_stories/CdcMap.Legend.stories.tsx +7 -4
  23. package/src/_stories/CdcMap.Patterns.stories.tsx +2 -2
  24. package/src/_stories/CdcMap.Table.stories.tsx +2 -2
  25. package/src/_stories/CdcMap.stories.tsx +18 -11
  26. package/src/_stories/GoogleMap.stories.tsx +2 -2
  27. package/src/_stories/UsaMap.NoData.stories.tsx +2 -2
  28. package/src/_stories/_mock/equal-number.json +1109 -0
  29. package/src/_stories/_mock/multi-state.json +21389 -0
  30. package/src/_stories/_mock/us-bubble-cities.json +306 -0
  31. package/src/components/BubbleList.tsx +16 -12
  32. package/src/components/CityList.tsx +88 -110
  33. package/src/components/DataTable.tsx +44 -12
  34. package/src/components/EditorPanel/components/EditorPanel.tsx +201 -203
  35. package/src/components/EditorPanel/components/HexShapeSettings.tsx +3 -2
  36. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +7 -5
  37. package/src/components/Geo.tsx +2 -0
  38. package/src/components/Legend/components/Legend.tsx +117 -93
  39. package/src/components/Legend/components/LegendGroup/Legend.Group.tsx +10 -7
  40. package/src/components/MapContainer.tsx +52 -0
  41. package/src/components/MapControls.tsx +44 -0
  42. package/src/components/Modal.tsx +2 -8
  43. package/src/components/NavigationMenu.tsx +13 -1
  44. package/src/components/UsaMap/components/SingleState/SingleState.CountyOutput.tsx +24 -7
  45. package/src/components/UsaMap/components/SingleState/SingleState.StateOutput.tsx +21 -15
  46. package/src/components/UsaMap/components/TerritoriesSection.tsx +2 -2
  47. package/src/components/UsaMap/components/UsaMap.County.tsx +112 -33
  48. package/src/components/UsaMap/components/UsaMap.Region.tsx +23 -5
  49. package/src/components/UsaMap/components/UsaMap.SingleState.tsx +38 -26
  50. package/src/components/UsaMap/components/UsaMap.State.tsx +28 -10
  51. package/src/components/UsaMap/helpers/map.ts +16 -8
  52. package/src/components/WorldMap/WorldMap.tsx +116 -11
  53. package/src/components/ZoomControls.tsx +6 -9
  54. package/src/context/LegendMemoContext.tsx +30 -0
  55. package/src/context.ts +1 -39
  56. package/src/data/initial-state.js +143 -128
  57. package/src/data/supported-geos.js +202 -4
  58. package/src/helpers/addUIDs.ts +8 -8
  59. package/src/helpers/applyColorToLegend.ts +122 -45
  60. package/src/helpers/applyLegendToRow.ts +15 -13
  61. package/src/helpers/componentHelpers.ts +8 -0
  62. package/src/helpers/constants.ts +12 -0
  63. package/src/helpers/dataTableHelpers.ts +6 -0
  64. package/src/helpers/displayGeoName.ts +12 -7
  65. package/src/helpers/formatLegendLocation.ts +1 -3
  66. package/src/helpers/generateRuntimeLegend.ts +192 -340
  67. package/src/helpers/generateRuntimeLegendHash.ts +4 -2
  68. package/src/helpers/getColumnNames.ts +1 -1
  69. package/src/helpers/getPatternForRow.ts +36 -0
  70. package/src/helpers/getStatesPicked.ts +14 -0
  71. package/src/helpers/handleMapAriaLabels.ts +2 -2
  72. package/src/helpers/index.ts +11 -3
  73. package/src/helpers/isLegendItemDisabled.ts +16 -0
  74. package/src/helpers/mapObserverHelpers.ts +40 -0
  75. package/src/helpers/resetLegendToggles.ts +3 -2
  76. package/src/helpers/toggleLegendActive.ts +6 -11
  77. package/src/helpers/urlDataHelpers.ts +70 -0
  78. package/src/hooks/useGeoClickHandler.ts +35 -1
  79. package/src/hooks/useLegendMemo.ts +17 -0
  80. package/src/hooks/useMapLayers.tsx +5 -4
  81. package/src/hooks/useStateZoom.tsx +137 -88
  82. package/src/hooks/useTooltip.ts +1 -2
  83. package/src/index.jsx +6 -3
  84. package/src/scss/main.scss +23 -12
  85. package/src/store/map.actions.ts +2 -2
  86. package/src/store/map.reducer.ts +21 -10
  87. package/src/test/CdcMap.test.jsx +11 -0
  88. package/src/types/MapConfig.ts +25 -17
  89. package/src/types/MapContext.ts +2 -8
  90. package/src/types/runtimeLegend.ts +12 -10
  91. package/vite.config.js +2 -7
  92. package/vitest.config.ts +16 -0
  93. package/src/_stories/_mock/floating-point.json +0 -427
  94. package/src/coreStyles_map.scss +0 -3
  95. package/src/helpers/colorDistributions.ts +0 -12
  96. package/src/helpers/generateColorsArray.ts +0 -14
  97. package/src/helpers/getStatePicked.ts +0 -8
  98. package/src/helpers/tests/generateColorsArray.test.ts +0 -18
  99. package/src/helpers/tests/generateRuntimeLegendHash.test.ts +0 -11
@@ -8,6 +8,7 @@ import {
8
8
  } from 'react-accessible-accordion'
9
9
  import ConfigContext from '../../../context'
10
10
  import _ from 'lodash'
11
+ import { cloneConfig } from '@cdc/core/helpers/cloneConfig'
11
12
 
12
13
  const shapeOptions = ['Arrow Up', 'Arrow Down', 'Arrow Right', 'Arrow Left', 'None']
13
14
 
@@ -105,7 +106,7 @@ const HexSettingShapeColumns = props => {
105
106
  type='text'
106
107
  value={shapeGroup.legendTitle || ''}
107
108
  onChange={e => {
108
- const newConfig = _.cloneDeep(config)
109
+ const newConfig = cloneConfig(config)
109
110
  newConfig.hexMap.shapeGroups[shapeGroupIndex].legendTitle = e.target.value
110
111
  setConfig(newConfig)
111
112
  }}
@@ -243,7 +244,7 @@ const HexSettingShapeColumns = props => {
243
244
  className='cove-button'
244
245
  style={{ marginTop: '15px' }}
245
246
  onClick={() => {
246
- const newConfig = _.cloneDeep(config)
247
+ const newConfig = cloneConfig(config)
247
248
  _.set(
248
249
  newConfig,
249
250
  'hexMap.shapeGroups',
@@ -7,6 +7,7 @@ import {
7
7
  AccordionItemButton
8
8
  } from 'react-accessible-accordion'
9
9
  import ConfigContext from '../../../../context'
10
+ import { useLegendMemoContext } from '../../../../context/LegendMemoContext'
10
11
  import { type MapContext } from '../../../../types/MapContext'
11
12
  import Button from '@cdc/core/components/elements/Button'
12
13
  import Tooltip from '@cdc/core/components/ui/Tooltip'
@@ -14,6 +15,7 @@ import Icon from '@cdc/core/components/ui/Icon'
14
15
  import './Panel.PatternSettings-style.css'
15
16
  import Alert from '@cdc/core/components/Alert'
16
17
  import _ from 'lodash'
18
+ import { cloneConfig } from '@cdc/core/helpers/cloneConfig'
17
19
 
18
20
  // topojson helpers for checking color contrasts
19
21
  import { feature } from 'topojson-client'
@@ -26,8 +28,8 @@ type PanelProps = {
26
28
  }
27
29
 
28
30
  const PatternSettings = ({ name }: PanelProps) => {
29
- const { config, setConfig, runtimeData, legendMemo, legendSpecialClassLastMemo, runtimeLegend } =
30
- useContext<MapContext>(ConfigContext)
31
+ const { config, setConfig, runtimeData, runtimeLegend } = useContext<MapContext>(ConfigContext)
32
+ const { legendMemo, legendSpecialClassLastMemo } = useLegendMemoContext()
31
33
  const defaultPattern = 'circles'
32
34
  const patternTypes = ['circles', 'waves', 'lines']
33
35
 
@@ -114,14 +116,14 @@ const PatternSettings = ({ name }: PanelProps) => {
114
116
  }
115
117
 
116
118
  const handlePatternFieldUpdate = (field: string, color: string, patternIndex: number) => {
117
- const _newConfig = _.cloneDeep(config)
119
+ const _newConfig = cloneConfig(config)
118
120
  _newConfig.map.patterns[patternIndex][field] = color
119
121
  reviewColorContrast(_newConfig, patternIndex)
120
122
  setConfig(_newConfig)
121
123
  }
122
124
 
123
125
  const handleRemovePattern = index => {
124
- const _newConfig = _.cloneDeep(config)
126
+ const _newConfig = cloneConfig(config)
125
127
  const updatedPatterns = config.map.patterns.filter((pattern, i) => i !== index)
126
128
  _newConfig.map.patterns = updatedPatterns
127
129
  if (checkPatternContrasts()) {
@@ -161,7 +163,7 @@ const PatternSettings = ({ name }: PanelProps) => {
161
163
  {patterns.length > 0 && (
162
164
  <Alert
163
165
  type={checkPatternContrasts() ? 'success' : 'danger'}
164
- message='Pattern colors must comply with <br /> <a href="https://www.w3.org/TR/WCAG21/">WCAG 2.1</a> 3:1 contrast ratio.'
166
+ message='Pattern colors must comply with <a href="https://www.w3.org/TR/WCAG21/">WCAG 2.1</a> 3:1 contrast ratio.'
165
167
  />
166
168
  )}
167
169
  <br />
@@ -6,6 +6,8 @@ type GeoProps = {
6
6
  strokeWidth?: number
7
7
  path?: string
8
8
  className?: string
9
+ onMouseEnter?: () => void
10
+ onClick?: () => void
9
11
  }
10
12
 
11
13
  const Geo: React.FC<GeoProps> = ({ path, styles, stroke, strokeWidth, ...props }) => {
@@ -1,6 +1,7 @@
1
1
  //TODO: Move legends to core
2
- import { forwardRef, useContext } from 'react'
2
+ import { forwardRef, useContext, useMemo } from 'react'
3
3
  import parse from 'html-react-parser'
4
+ import { processMarkupVariables } from '@cdc/core/helpers/markupProcessor'
4
5
 
5
6
  //types
6
7
  import { DimensionsType } from '@cdc/core/types/Dimensions'
@@ -18,12 +19,14 @@ import { GlyphStar, GlyphTriangle, GlyphDiamond, GlyphSquare, GlyphCircle } from
18
19
  import { Group } from '@visx/group'
19
20
  import './index.scss'
20
21
  import { type ViewPort } from '@cdc/core/types/ViewPort'
21
- import { isBelowBreakpoint, isMobileHeightViewport } from '@cdc/core/helpers/viewports'
22
+ import { isBelowBreakpoint, isMobileFontViewport } from '@cdc/core/helpers/viewports'
22
23
  import { displayDataAsText } from '@cdc/core/helpers/displayDataAsText'
23
- import { toggleLegendActive } from '@cdc/map/src/helpers/toggleLegendActive'
24
+ import { toggleLegendActive } from '../../../helpers/toggleLegendActive'
24
25
  import { resetLegendToggles } from '../../../helpers'
25
26
  import { MapContext } from '../../../types/MapContext'
26
27
  import LegendGroup from './LegendGroup/Legend.Group'
28
+ import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
29
+ import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
27
30
 
28
31
  const LEGEND_PADDING = 30
29
32
 
@@ -32,10 +35,11 @@ type LegendProps = {
32
35
  dimensions: DimensionsType
33
36
  containerWidthPadding: number
34
37
  currentViewport: ViewPort
38
+ interactionLabel: string
35
39
  }
36
40
 
37
41
  const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
38
- const { skipId, containerWidthPadding } = props
42
+ const { skipId, containerWidthPadding, interactionLabel } = props
39
43
 
40
44
  const {
41
45
  config,
@@ -43,8 +47,7 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
43
47
  dimensions,
44
48
  mapId,
45
49
  runtimeFilters,
46
- runtimeLegend,
47
- setRuntimeLegend
50
+ runtimeLegend
48
51
  } = useContext<MapContext>(ConfigContext)
49
52
 
50
53
  const dispatch = useContext(MapDispatchContext)
@@ -59,7 +62,10 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
59
62
 
60
63
  const getFormattedLegendItems = () => {
61
64
  try {
62
- if (!runtimeLegend.items) Error('No runtime legend data')
65
+ if (!runtimeLegend.items) {
66
+ console.warn('No runtime legend data available')
67
+ return []
68
+ }
63
69
  return runtimeLegend.items.map((entry, idx) => {
64
70
  const entryMax = displayDataAsText(entry.max, 'primary', config)
65
71
 
@@ -94,8 +100,7 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
94
100
  label: parse(legendLabel),
95
101
  disabled: entry.disabled,
96
102
  special: entry.hasOwnProperty('special'),
97
- value: legend.type === 'category' ? entry.value : [entry.min, entry.max],
98
- categoryValue: legend.type === 'category' ? entry.value : undefined
103
+ value: [entry.min, entry.max]
99
104
  }
100
105
  })
101
106
  } catch (e) {
@@ -106,7 +111,7 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
106
111
 
107
112
  const legendList = (patternsOnly = false) => {
108
113
  const formattedItems = patternsOnly ? [] : getFormattedLegendItems()
109
- const patternsOnlyFont = isMobileHeightViewport(viewport) ? '12px' : '14px'
114
+ const patternsOnlyFont = isMobileFontViewport(viewport) ? '12px' : '14px'
110
115
  const hasDisabledItems = formattedItems.some(item => item.disabled)
111
116
  let legendItems
112
117
 
@@ -119,48 +124,36 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
119
124
  return classes.join(' ')
120
125
  }
121
126
 
122
- const setAccessibleStatus = (message: string) => {
123
- dispatch({ type: 'SET_ACCESSIBLE_STATUS', payload: message })
124
- }
125
-
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, dispatch)
134
+ publishAnalyticsEvent({
135
+ vizType: config.type,
136
+ vizSubType: getVizSubType(config),
137
+ eventType: `map_legend_item_toggled`,
138
+ eventAction: 'click',
139
+ eventLabel: `${interactionLabel}`,
140
+ vizTitle: getVizTitle(config),
141
+ specifics: `mode: isolate, label: ${item.label}`
142
+ })
143
+ }}
160
144
  onKeyDown={e => {
161
145
  if (e.key === 'Enter') {
162
146
  e.preventDefault()
163
- toggleLegendActive(safeRuntimeIndex, item.label, runtimeLegend, setRuntimeLegend, setAccessibleStatus)
147
+ toggleLegendActive(idx, item.label, runtimeLegend, dispatch)
148
+ publishAnalyticsEvent({
149
+ vizType: config.type,
150
+ vizSubType: getVizSubType(config),
151
+ eventType: `map_legend_item_toggled`,
152
+ eventAction: 'keydown',
153
+ eventLabel: `${interactionLabel}`,
154
+ vizTitle: getVizTitle(config),
155
+ specifics: `mode: isolate, label: ${item.label}`
156
+ })
164
157
  }
165
158
  }}
166
159
  tabIndex={0}
@@ -178,9 +171,9 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
178
171
  const { pattern, dataKey, size } = patternData
179
172
  let defaultPatternColor = 'black'
180
173
  const sizes = {
181
- small: '8',
182
- medium: '10',
183
- large: '12'
174
+ small: 8,
175
+ medium: 10,
176
+ large: 12
184
177
  }
185
178
 
186
179
  const legendSize = 16
@@ -253,7 +246,15 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
253
246
  if (e) {
254
247
  e.preventDefault()
255
248
  }
256
- resetLegendToggles(runtimeLegend, setRuntimeLegend)
249
+ publishAnalyticsEvent({
250
+ vizType: config.type,
251
+ vizSubType: getVizSubType(config),
252
+ eventType: 'map_legend_reset',
253
+ eventAction: 'click',
254
+ eventLabel: interactionLabel,
255
+ vizTitle: getVizTitle(config)
256
+ })
257
+ resetLegendToggles(runtimeLegend, dispatch)
257
258
  dispatch({
258
259
  type: 'SET_ACCESSIBLE_STATUS',
259
260
  payload: 'Legend has been reset, please reference the data table to see updated values.'
@@ -273,14 +274,17 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
273
274
  />
274
275
  )
275
276
 
276
- const cityStyleShapes = {
277
- pin: pin,
278
- circle: <GlyphCircle color='#000' size={150} />,
279
- square: <GlyphSquare color='#000' size={150} />,
280
- diamond: <GlyphDiamond color='#000' size={150} />,
281
- star: <GlyphStar color='#000' size={150} />,
282
- triangle: <GlyphTriangle color='#000' size={150} />
283
- }
277
+ const cityStyleShapes = useMemo(
278
+ () => ({
279
+ pin: pin,
280
+ circle: <GlyphCircle color='#000' size={150} />,
281
+ square: <GlyphSquare color='#000' size={150} />,
282
+ diamond: <GlyphDiamond color='#000' size={150} />,
283
+ star: <GlyphStar color='#000' size={150} />,
284
+ triangle: <GlyphTriangle color='#000' size={150} />
285
+ }),
286
+ [pin]
287
+ )
284
288
 
285
289
  const shouldRenderLegendList = legendListItems.length > 0 && ['Select Option', ''].includes(config.legend.groupBy)
286
290
 
@@ -298,9 +302,29 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
298
302
  <section className={legendClasses.section.join(' ') || ''} aria-label='Map Legend'>
299
303
  {(legend.title || legend.description || legend.dynamicDescription) && (
300
304
  <div className='mb-3'>
301
- {legend.title && <h3 className={legendClasses.title.join(' ') || ''}>{parse(legend.title)}</h3>}
305
+ {legend.title && (
306
+ <h3 className={legendClasses.title.join(' ') || ''}>
307
+ {parse(
308
+ config.enableMarkupVariables && config.markupVariables?.length > 0
309
+ ? processMarkupVariables(legend.title, config.data || [], config.markupVariables, {
310
+ isEditor: false,
311
+ filters: config.filters || []
312
+ }).processedContent
313
+ : legend.title
314
+ )}
315
+ </h3>
316
+ )}
302
317
  {legend.dynamicDescription === false && legend.description && (
303
- <p className={legendClasses.description.join(' ') || ''}>{parse(legend.description)}</p>
318
+ <p className={legendClasses.description.join(' ') || ''}>
319
+ {parse(
320
+ config.enableMarkupVariables && config.markupVariables?.length > 0
321
+ ? processMarkupVariables(legend.description, config.data || [], config.markupVariables, {
322
+ isEditor: false,
323
+ filters: config.filters || []
324
+ }).processedContent
325
+ : legend.description
326
+ )}
327
+ </p>
304
328
  )}
305
329
  {legend.dynamicDescription === true &&
306
330
  runtimeFilters.map((filter, idx) => {
@@ -343,41 +367,41 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
343
367
 
344
368
  {((config.visual.additionalCityStyles && config.visual.additionalCityStyles.some(c => c.label)) ||
345
369
  config.visual.cityStyleLabel) && (
346
- <>
347
- <hr />
348
- <div className={legendClasses.div.join(' ') || ''}>
349
- {config.visual.cityStyleLabel && (
350
- <div>
351
- <svg>
352
- <Group
353
- top={
354
- config.visual.cityStyle === 'pin' ? 19 : config.visual.cityStyle === 'triangle' ? 13 : 11
355
- }
356
- left={10}
357
- >
358
- {cityStyleShapes[config.visual.cityStyle.toLowerCase()]}
359
- </Group>
360
- </svg>
361
- <p>{config.visual.cityStyleLabel}</p>
362
- </div>
363
- )}
364
-
365
- {config.visual.additionalCityStyles.map(
366
- ({ shape, label }) =>
367
- label && (
368
- <div>
369
- <svg>
370
- <Group top={shape === 'Pin' ? 19 : shape === 'Triangle' ? 13 : 11} left={10}>
371
- {cityStyleShapes[shape.toLowerCase()]}
372
- </Group>
373
- </svg>
374
- <p>{label}</p>
375
- </div>
376
- )
377
- )}
378
- </div>
379
- </>
380
- )}
370
+ <>
371
+ <hr />
372
+ <div className={legendClasses.div.join(' ') || ''}>
373
+ {config.visual.cityStyleLabel && (
374
+ <div>
375
+ <svg>
376
+ <Group
377
+ top={
378
+ config.visual.cityStyle === 'pin' ? 19 : config.visual.cityStyle === 'triangle' ? 13 : 11
379
+ }
380
+ left={10}
381
+ >
382
+ {cityStyleShapes[config.visual.cityStyle.toLowerCase()]}
383
+ </Group>
384
+ </svg>
385
+ <p>{config.visual.cityStyleLabel}</p>
386
+ </div>
387
+ )}
388
+
389
+ {config.visual.additionalCityStyles.map(
390
+ ({ shape, label }, index) =>
391
+ label && (
392
+ <div key={`additional-city-style-${index}-${shape}`}>
393
+ <svg>
394
+ <Group top={shape === 'Pin' ? 19 : shape === 'Triangle' ? 13 : 11} left={10}>
395
+ {cityStyleShapes[shape.toLowerCase()]}
396
+ </Group>
397
+ </svg>
398
+ <p>{label}</p>
399
+ </div>
400
+ )
401
+ )}
402
+ </div>
403
+ </>
404
+ )}
381
405
  {runtimeLegend.disabledAmt > 0 && (
382
406
  <Button className={legendClasses.showAllButton.join(' ')} onClick={handleReset}>
383
407
  Show All
@@ -1,7 +1,7 @@
1
1
  import { useContext, useMemo } from 'react'
2
2
  import './legend.group.css'
3
3
  import LegendShape from '@cdc/core/components/LegendShape'
4
- import { toggleLegendActive } from '@cdc/map/src/helpers/toggleLegendActive'
4
+ import { toggleLegendActive } from '../../../../helpers/toggleLegendActive'
5
5
  import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
6
6
  import ConfigContext, { MapDispatchContext } from '../../../../context'
7
7
 
@@ -17,7 +17,7 @@ interface GroupedData {
17
17
  }
18
18
 
19
19
  const LegendGroup = ({ legendItems }) => {
20
- const { runtimeLegend, setRuntimeLegend, config } = useContext(ConfigContext)
20
+ const { runtimeLegend, config } = useContext(ConfigContext)
21
21
  const dispatch = useContext(MapDispatchContext)
22
22
  const groupLegendItems = (items: LegendItem[], data: object[], groupByKey: string): GroupedData => {
23
23
  if (!groupByKey || !data || !items) return {}
@@ -59,10 +59,13 @@ const LegendGroup = ({ legendItems }) => {
59
59
  const wasDisabled = runtimeLegend.items.find(i => i.value === item.label)?.disabled
60
60
  const delta = wasDisabled ? -1 : 1
61
61
 
62
- setRuntimeLegend({
63
- ...runtimeLegend,
64
- items: newItems,
65
- disabledAmt: (runtimeLegend.disabledAmt ?? 0) + delta
62
+ dispatch({
63
+ type: 'SET_RUNTIME_LEGEND',
64
+ payload: {
65
+ ...runtimeLegend,
66
+ items: newItems,
67
+ disabledAmt: (runtimeLegend.disabledAmt ?? 0) + delta
68
+ }
66
69
  })
67
70
  const message = `${wasDisabled ? 'Enabled' : 'Disabled'} legend item ${
68
71
  item.label
@@ -109,7 +112,7 @@ const LegendGroup = ({ legendItems }) => {
109
112
  onKeyDown={e => {
110
113
  if (e.key === 'Enter' || e.key === ' ') {
111
114
  e.preventDefault()
112
- toggleLegendActive(index, item.label, runtimeLegend, setRuntimeLegend, setAccessibleStatus)
115
+ toggleLegendActive(index, item.label, runtimeLegend, dispatch)
113
116
  }
114
117
  }}
115
118
  >
@@ -0,0 +1,52 @@
1
+ import React from 'react'
2
+ import Modal from './Modal'
3
+ import UsaMap from './UsaMap'
4
+ import WorldMap from './WorldMap'
5
+ import GoogleMap from './GoogleMap'
6
+ import { MapConfig } from '../types/MapConfig'
7
+ import { LOGO_MAX_WIDTH } from '../helpers/constants'
8
+
9
+ interface MapContainerProps {
10
+ config: MapConfig
11
+ modal: any
12
+ currentViewport: string
13
+ geoType: string
14
+ general: any
15
+ logo: string
16
+ mapSvgRef: React.RefObject<HTMLElement>
17
+ }
18
+
19
+ const MapContainer: React.FC<MapContainerProps> = ({
20
+ config,
21
+ modal,
22
+ currentViewport,
23
+ geoType,
24
+ general,
25
+ logo,
26
+ mapSvgRef
27
+ }) => {
28
+ return (
29
+ <section className='outline-none geography-container w-100 position-relative' ref={mapSvgRef} tabIndex={0}>
30
+ {currentViewport && (
31
+ <>
32
+ {modal && <Modal />}
33
+ {'single-state' === geoType && <UsaMap.SingleState />}
34
+ {'us' === geoType && 'us-geocode' !== config.general.type && <UsaMap.State />}
35
+ {'us-region' === geoType && <UsaMap.Region />}
36
+ {'us-county' === geoType && <UsaMap.County />}
37
+ {'world' === geoType && <WorldMap />}
38
+ {'google-map' === geoType && <GoogleMap />}
39
+ {
40
+ /* logo is handled in UsaMap.State when applicable */
41
+ // prettier-ignore
42
+ 'data' === general.type && logo && ('us' !== geoType || 'us-geocode' === general.type) && (
43
+ <img src={logo} alt='' className='map-logo' style={{ maxWidth: LOGO_MAX_WIDTH }} />
44
+ )
45
+ }
46
+ </>
47
+ )}
48
+ </section>
49
+ )
50
+ }
51
+
52
+ export default MapContainer
@@ -0,0 +1,44 @@
1
+ import React from 'react'
2
+ import MediaControls from '@cdc/core/components/MediaControls'
3
+ import { MapConfig } from '../types/MapConfig'
4
+
5
+ interface MapControlsProps {
6
+ config: MapConfig
7
+ imageId: string
8
+ interactionLabel: string
9
+ }
10
+
11
+ const MapControls: React.FC<MapControlsProps> = ({ config, imageId, interactionLabel }) => {
12
+ const { showDownloadImgButton, showDownloadPdfButton } = config.general
13
+
14
+ if (!showDownloadImgButton && !showDownloadPdfButton) {
15
+ return null
16
+ }
17
+
18
+ return (
19
+ <MediaControls.Section classes={['download-buttons']}>
20
+ {showDownloadImgButton && (
21
+ <MediaControls.Button
22
+ text='Download Image'
23
+ title='Download Chart as Image'
24
+ type='image'
25
+ state={config}
26
+ elementToCapture={imageId}
27
+ interactionLabel={interactionLabel}
28
+ />
29
+ )}
30
+ {showDownloadPdfButton && (
31
+ <MediaControls.Button
32
+ text='Download PDF'
33
+ title='Download Chart as PDF'
34
+ type='pdf'
35
+ state={config}
36
+ interactionLabel={interactionLabel}
37
+ elementToCapture={imageId}
38
+ />
39
+ )}
40
+ </MediaControls.Section>
41
+ )
42
+ }
43
+
44
+ export default MapControls
@@ -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,10 @@
1
1
  import React, { useContext, useEffect, useState } from 'react'
2
2
  import ConfigContext from '../context'
3
+ import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
4
+ import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
3
5
 
4
6
  const NavigationMenu = ({ data, navigationHandler, options, columns, displayGeoName, mapTabbingID }) => {
5
- const { state } = useContext(ConfigContext)
7
+ const { interactionLabel, config } = useContext(ConfigContext)
6
8
  const [activeGeo, setActiveGeo] = useState('')
7
9
  const [dropdownItems, setDropdownItems] = useState({})
8
10
 
@@ -11,6 +13,16 @@ const NavigationMenu = ({ data, navigationHandler, options, columns, displayGeoN
11
13
  if (activeGeo !== '') {
12
14
  const urlString = data[dropdownItems[activeGeo]][columns.navigate.name]
13
15
 
16
+ publishAnalyticsEvent({
17
+ vizType: config.type,
18
+ vizSubType: getVizSubType(config),
19
+ eventType: `map_navigation_menu`,
20
+ eventAction: 'submit',
21
+ eventLabel: `${interactionLabel}`,
22
+ vizTitle: getVizTitle(config),
23
+ specifics: `url: ${urlString}, activeGeo: ${activeGeo}`
24
+ })
25
+
14
26
  navigationHandler(urlString)
15
27
  }
16
28
  }