@cdc/map 4.25.8 → 4.25.11

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 (137) hide show
  1. package/.claude/agents/typescript-organizer.md +118 -0
  2. package/.claude/settings.local.json +30 -0
  3. package/dist/{cdcmap-fce76882.es.js → cdcmap-BnB1QM5d.es.js} +6 -13
  4. package/dist/{cdcmap-c55ac1ea.es.js → cdcmap-D6CG2-Hb.es.js} +5 -12
  5. package/dist/{cdcmap-31a33da1.es.js → cdcmap-MXgURbdZ.es.js} +6 -13
  6. package/dist/{cdcmap-1a1724a1.es.js → cdcmap-dgT_1dIT.es.js} +136 -151
  7. package/dist/cdcmap.js +56991 -53706
  8. package/examples/example-city-state.json +9 -1
  9. package/examples/multi-country-centering.json +45 -0
  10. package/examples/private/c.json +290 -0
  11. package/examples/private/canvas-city-hover.json +787 -0
  12. package/examples/private/colors-2.json +221 -0
  13. package/examples/private/colors.json +221 -0
  14. package/examples/private/d.json +345 -0
  15. package/examples/private/g.json +1 -0
  16. package/examples/private/h.json +105911 -0
  17. package/examples/private/measles-data.json +378 -0
  18. package/examples/private/measles.json +211 -0
  19. package/examples/private/north-dakota.json +1132 -0
  20. package/examples/private/state-with-pattern.json +883 -0
  21. package/index.html +36 -34
  22. package/package.json +26 -5
  23. package/src/CdcMap.tsx +23 -8
  24. package/src/CdcMapComponent.tsx +238 -308
  25. package/src/_stories/CdcMap.ColumnWrap.stories.tsx +31 -0
  26. package/src/_stories/CdcMap.DistrictOfColumbia.stories.tsx +320 -0
  27. package/src/_stories/CdcMap.Editor.stories.tsx +3371 -0
  28. package/src/_stories/CdcMap.Filters.stories.tsx +2 -2
  29. package/src/_stories/CdcMap.Legend.Gradient.stories.tsx +3 -3
  30. package/src/_stories/CdcMap.Legend.stories.tsx +7 -4
  31. package/src/_stories/CdcMap.Patterns.stories.tsx +2 -2
  32. package/src/_stories/CdcMap.SmallMultiples.stories.tsx +35 -0
  33. package/src/_stories/CdcMap.Table.stories.tsx +2 -2
  34. package/src/_stories/CdcMap.stories.tsx +37 -9
  35. package/src/_stories/GoogleMap.stories.tsx +2 -2
  36. package/src/_stories/UsaMap.NoData.stories.tsx +2 -2
  37. package/src/_stories/_mock/column-wrap-test.json +265 -0
  38. package/src/_stories/_mock/equal-number.json +1109 -0
  39. package/src/_stories/_mock/multi-country-hide.json +78 -0
  40. package/src/_stories/_mock/multi-country.json +95 -0
  41. package/src/_stories/_mock/multi-state.json +887 -20403
  42. package/src/_stories/_mock/small_multiples/multi-state-small-multiples.json +8399 -0
  43. package/src/_stories/_mock/small_multiples/region-small-multiples.json +657 -0
  44. package/src/_stories/_mock/small_multiples/wastewater-map-small-multiples.json +221 -0
  45. package/src/_stories/_mock/us-bubble-cities.json +306 -0
  46. package/src/_stories/_mock/usa-state-gradient.json +2 -4
  47. package/src/components/BubbleList.tsx +17 -13
  48. package/src/components/CityList.tsx +85 -107
  49. package/src/components/EditorPanel/components/EditorPanel.tsx +787 -709
  50. package/src/components/EditorPanel/components/HexShapeSettings.tsx +58 -95
  51. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +34 -42
  52. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +354 -0
  53. package/src/components/EditorPanel/components/Panels/index.tsx +3 -1
  54. package/src/components/Geo.tsx +22 -3
  55. package/src/components/Legend/components/Legend.tsx +76 -40
  56. package/src/components/Legend/components/LegendGroup/Legend.Group.tsx +10 -7
  57. package/src/components/Legend/components/index.scss +1 -1
  58. package/src/components/MapContainer.tsx +52 -0
  59. package/src/components/MapControls.tsx +44 -0
  60. package/src/components/NavigationMenu.tsx +27 -15
  61. package/src/components/SmallMultiples/SmallMultipleTile.tsx +163 -0
  62. package/src/components/SmallMultiples/SmallMultiples.css +32 -0
  63. package/src/components/SmallMultiples/SmallMultiples.tsx +150 -0
  64. package/src/components/SmallMultiples/SynchronizedTooltip.tsx +105 -0
  65. package/src/components/SmallMultiples/index.tsx +3 -0
  66. package/src/components/UsaMap/components/SingleState/SingleState.CountyOutput.tsx +36 -4
  67. package/src/components/UsaMap/components/TerritoriesSection.tsx +26 -12
  68. package/src/components/UsaMap/components/Territory/Territory.Hexagon.tsx +30 -4
  69. package/src/components/UsaMap/components/Territory/Territory.Rectangle.tsx +23 -4
  70. package/src/components/UsaMap/components/Territory/TerritoryShape.ts +6 -0
  71. package/src/components/UsaMap/components/UsaMap.County.tsx +123 -37
  72. package/src/components/UsaMap/components/UsaMap.Region.tsx +36 -5
  73. package/src/components/UsaMap/components/UsaMap.SingleState.tsx +30 -10
  74. package/src/components/UsaMap/components/UsaMap.State.tsx +53 -12
  75. package/src/components/UsaMap/helpers/map.ts +4 -4
  76. package/src/components/UsaMap/helpers/shapes.ts +9 -6
  77. package/src/components/WorldMap/WorldMap.tsx +193 -35
  78. package/src/components/ZoomControls.tsx +6 -9
  79. package/src/context/LegendMemoContext.tsx +30 -0
  80. package/src/context.ts +1 -40
  81. package/src/data/initial-state.js +153 -130
  82. package/src/data/supported-geos.js +25 -78
  83. package/src/helpers/addUIDs.ts +13 -2
  84. package/src/helpers/applyColorToLegend.ts +140 -20
  85. package/src/helpers/applyLegendToRow.ts +10 -6
  86. package/src/helpers/componentHelpers.ts +8 -0
  87. package/src/helpers/constants.ts +12 -14
  88. package/src/helpers/dataTableHelpers.ts +6 -0
  89. package/src/helpers/displayGeoName.ts +18 -3
  90. package/src/helpers/generateRuntimeLegend.ts +44 -10
  91. package/src/helpers/generateRuntimeLegendHash.ts +4 -2
  92. package/src/helpers/getColumnNames.ts +1 -1
  93. package/src/helpers/getCountriesPicked.ts +103 -0
  94. package/src/helpers/getMapContainerClasses.ts +7 -0
  95. package/src/helpers/getPatternForRow.ts +33 -0
  96. package/src/helpers/getStatesPicked.ts +8 -5
  97. package/src/helpers/index.ts +3 -3
  98. package/src/helpers/isLegendItemDisabled.ts +16 -0
  99. package/src/helpers/mapObserverHelpers.ts +40 -0
  100. package/src/helpers/resetLegendToggles.ts +3 -2
  101. package/src/helpers/smallMultiplesHelpers.ts +359 -0
  102. package/src/helpers/tests/titleCase.test.ts +76 -0
  103. package/src/helpers/titleCase.ts +13 -13
  104. package/src/helpers/toggleLegendActive.ts +6 -11
  105. package/src/helpers/urlDataHelpers.ts +70 -0
  106. package/src/hooks/useCountryZoom.tsx +241 -0
  107. package/src/hooks/useGeoClickHandler.ts +36 -2
  108. package/src/hooks/useLegendMemo.ts +17 -0
  109. package/src/hooks/useMapLayers.tsx +5 -4
  110. package/src/hooks/useProgrammaticMapTooltip.ts +110 -0
  111. package/src/hooks/useResizeObserver.ts +5 -2
  112. package/src/hooks/useStateZoom.tsx +30 -8
  113. package/src/hooks/useSynchronizedGeographies.ts +56 -0
  114. package/src/hooks/useTooltip.ts +1 -2
  115. package/src/index.jsx +1 -2
  116. package/src/scss/editor-panel.scss +4 -440
  117. package/src/scss/main.scss +1 -1
  118. package/src/scss/map.scss +12 -15
  119. package/src/store/map.actions.ts +7 -7
  120. package/src/store/map.reducer.ts +17 -6
  121. package/src/test/CdcMap.test.jsx +11 -0
  122. package/src/types/MapConfig.ts +46 -18
  123. package/src/types/MapContext.ts +6 -7
  124. package/src/types/runtimeLegend.ts +17 -1
  125. package/vite.config.js +2 -7
  126. package/vitest.config.ts +16 -0
  127. package/src/components/DataTable.tsx +0 -385
  128. package/src/components/EditorPanel/components/Inputs.tsx +0 -59
  129. package/src/coreStyles_map.scss +0 -3
  130. package/src/helpers/colorDistributions.ts +0 -12
  131. package/src/helpers/generateColorsArray.ts +0 -14
  132. package/src/helpers/tests/generateColorsArray.test.ts +0 -18
  133. package/src/helpers/tests/generateRuntimeLegendHash.test.ts +0 -11
  134. package/src/hooks/useActiveElement.ts +0 -19
  135. package/src/scss/mixins.scss +0 -47
  136. package/src/types/Annotations.ts +0 -24
  137. /package/dist/{cdcmap-548642e6.es.js → cdcmap-Ct2SB0vL.es.js} +0 -0
@@ -0,0 +1,354 @@
1
+ import { useContext, FC } from 'react'
2
+ import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd'
3
+ import {
4
+ AccordionItem,
5
+ AccordionItemHeading,
6
+ AccordionItemPanel,
7
+ AccordionItemButton
8
+ } from 'react-accessible-accordion'
9
+
10
+ // core
11
+ import { TextField, Select, CheckBox } from '@cdc/core/components/EditorPanel/Inputs'
12
+ import Tooltip from '@cdc/core/components/ui/Tooltip'
13
+ import Icon from '@cdc/core/components/ui/Icon'
14
+
15
+ // contexts
16
+ import ConfigContext from '../../../../context'
17
+ import { MapContext } from '../../../../types/MapContext'
18
+ import { getTileKeys } from '../../../../helpers/smallMultiplesHelpers'
19
+
20
+ interface PanelSmallMultiplesProps {
21
+ name?: string
22
+ }
23
+
24
+ const PanelSmallMultiples: FC<PanelSmallMultiplesProps> = props => {
25
+ const { config, setConfig } = useContext<MapContext>(ConfigContext)
26
+ const { general } = config
27
+
28
+ const getColumns = () => {
29
+ let columns = {}
30
+ config.data?.forEach(row => {
31
+ Object.keys(row).forEach(columnName => (columns[columnName] = true))
32
+ })
33
+
34
+ // Filter out geo and primary columns
35
+ if (config.columns?.geo?.name) {
36
+ delete columns[config.columns.geo.name]
37
+ }
38
+ if (config.columns?.primary?.name) {
39
+ delete columns[config.columns.primary.name]
40
+ }
41
+
42
+ return Object.keys(columns)
43
+ }
44
+
45
+ const updateField = (section, subsection, fieldName, value) => {
46
+ const newConfig = { ...config }
47
+
48
+ if (subsection) {
49
+ newConfig[section] = {
50
+ ...newConfig[section],
51
+ [subsection]: {
52
+ ...newConfig[section]?.[subsection],
53
+ [fieldName]: value
54
+ }
55
+ }
56
+ } else {
57
+ newConfig[section] = {
58
+ ...newConfig[section],
59
+ [fieldName]: value
60
+ }
61
+ }
62
+
63
+ setConfig(newConfig)
64
+ }
65
+
66
+ const handleColumnChange = (section, subsection, fieldName, value) => {
67
+ const newConfig = { ...config }
68
+
69
+ // Set the column value
70
+ newConfig.smallMultiples = {
71
+ ...newConfig.smallMultiples,
72
+ tileColumn: value
73
+ }
74
+
75
+ // Automatically set mode based on whether a column is selected
76
+ if (value) {
77
+ newConfig.smallMultiples.mode = 'by-column'
78
+ } else {
79
+ newConfig.smallMultiples.mode = undefined
80
+ }
81
+
82
+ setConfig(newConfig)
83
+ }
84
+
85
+ return (
86
+ <AccordionItem>
87
+ <AccordionItemHeading>
88
+ <AccordionItemButton>Small Multiples</AccordionItemButton>
89
+ </AccordionItemHeading>
90
+ <AccordionItemPanel>
91
+ <Select
92
+ value={config.smallMultiples?.tileColumn || ''}
93
+ fieldName='tileColumn'
94
+ section='smallMultiples'
95
+ label='Tile By Column'
96
+ initial='Select Column'
97
+ updateField={handleColumnChange}
98
+ options={getColumns()}
99
+ tooltip={
100
+ <Tooltip style={{ textTransform: 'none' }}>
101
+ <Tooltip.Target>
102
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
103
+ </Tooltip.Target>
104
+ <Tooltip.Content>
105
+ <p>
106
+ Select the column whose unique values will create separate map tiles. Choosing a column will enable
107
+ small multiples mode.
108
+ </p>
109
+ </Tooltip.Content>
110
+ </Tooltip>
111
+ }
112
+ />
113
+
114
+ {config.smallMultiples?.tileColumn && (
115
+ <>
116
+ <TextField
117
+ type='number'
118
+ value={config.smallMultiples?.tilesPerRowDesktop}
119
+ section='smallMultiples'
120
+ fieldName='tilesPerRowDesktop'
121
+ label='Tiles Per Row (Desktop)'
122
+ updateField={updateField}
123
+ min={1}
124
+ max={3}
125
+ tooltip={
126
+ <Tooltip style={{ textTransform: 'none' }}>
127
+ <Tooltip.Target>
128
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
129
+ </Tooltip.Target>
130
+ <Tooltip.Content>
131
+ <p>
132
+ Number of map tiles to display per row on desktop screens. Mobile will always show 1 tile per row.
133
+ </p>
134
+ </Tooltip.Content>
135
+ </Tooltip>
136
+ }
137
+ />
138
+
139
+ {/* Tile Ordering */}
140
+ {(() => {
141
+ const availableTiles = getTileKeys(config, config.data).map(String)
142
+ if (availableTiles.length === 0) return null
143
+
144
+ const tileOrderOptions = [
145
+ {
146
+ label: 'Ascending By Title',
147
+ value: 'asc'
148
+ },
149
+ {
150
+ label: 'Descending By Title',
151
+ value: 'desc'
152
+ },
153
+ {
154
+ label: 'Custom',
155
+ value: 'custom'
156
+ }
157
+ ]
158
+
159
+ const currentOrderType = config.smallMultiples?.tileOrderType || 'asc'
160
+
161
+ const handleOrderTypeChange = orderType => {
162
+ const newConfig = {
163
+ ...config,
164
+ smallMultiples: {
165
+ ...config.smallMultiples,
166
+ tileOrderType: orderType
167
+ }
168
+ }
169
+
170
+ // If switching to custom, initialize with current tile order
171
+ if (orderType === 'custom' && !config.smallMultiples?.tileOrder?.length) {
172
+ newConfig.smallMultiples.tileOrder = [...availableTiles]
173
+ }
174
+
175
+ setConfig(newConfig)
176
+ }
177
+
178
+ const handleCustomTileOrderChange = (sourceIndex, destinationIndex) => {
179
+ if (destinationIndex === null) return
180
+
181
+ const currentOrder = config.smallMultiples?.tileOrder || [...availableTiles]
182
+ const newOrder = [...currentOrder]
183
+ const [removed] = newOrder.splice(sourceIndex, 1)
184
+ newOrder.splice(destinationIndex, 0, removed)
185
+
186
+ setConfig({
187
+ ...config,
188
+ smallMultiples: {
189
+ ...config.smallMultiples,
190
+ tileOrder: newOrder,
191
+ tileOrderType: 'custom'
192
+ }
193
+ })
194
+ }
195
+
196
+ return (
197
+ <>
198
+ <Select
199
+ value={currentOrderType}
200
+ options={tileOrderOptions}
201
+ label='Tile Order'
202
+ updateField={(_section, _subsection, _fieldName, value) => {
203
+ handleOrderTypeChange(value)
204
+ }}
205
+ />
206
+
207
+ {currentOrderType === 'custom' && (
208
+ <DragDropContext
209
+ onDragEnd={({ source, destination }) =>
210
+ handleCustomTileOrderChange(source.index, destination?.index)
211
+ }
212
+ >
213
+ <Droppable droppableId='tile_order'>
214
+ {provided => (
215
+ <ul
216
+ {...provided.droppableProps}
217
+ className='sort-list'
218
+ ref={provided.innerRef}
219
+ style={{ marginTop: '1em' }}
220
+ >
221
+ {(config.smallMultiples?.tileOrder || availableTiles).map((tileKey, index) => (
222
+ <Draggable key={tileKey} draggableId={`tile-${tileKey}`} index={index}>
223
+ {(provided, snapshot) => (
224
+ <li>
225
+ <div
226
+ className={snapshot.isDragging ? 'currently-dragging' : ''}
227
+ style={provided.draggableProps.style}
228
+ ref={provided.innerRef}
229
+ {...provided.draggableProps}
230
+ {...provided.dragHandleProps}
231
+ >
232
+ {tileKey}
233
+ </div>
234
+ </li>
235
+ )}
236
+ </Draggable>
237
+ ))}
238
+ {provided.placeholder}
239
+ </ul>
240
+ )}
241
+ </Droppable>
242
+ </DragDropContext>
243
+ )}
244
+ </>
245
+ )
246
+ })()}
247
+
248
+ {/* Custom Tile Titles */}
249
+ <div>
250
+ <label style={{ marginTop: '1.5rem', marginBottom: '0.5rem' }}>Custom Tile Titles</label>
251
+
252
+ {(() => {
253
+ const availableTiles = getTileKeys(config, config.data).map(String)
254
+ if (availableTiles.length === 0) return null
255
+
256
+ const handleTitleChange = (tileKey, customTitle) => {
257
+ const newTitles = { ...config.smallMultiples?.tileTitles }
258
+ if (customTitle.trim() === '' || customTitle === tileKey) {
259
+ delete newTitles[tileKey] // Remove entry if empty or same as key
260
+ } else {
261
+ newTitles[tileKey] = customTitle
262
+ }
263
+
264
+ setConfig({
265
+ ...config,
266
+ smallMultiples: {
267
+ ...config.smallMultiples,
268
+ tileTitles: newTitles
269
+ }
270
+ })
271
+ }
272
+
273
+ return (
274
+ <div className='tile-titles-editor' style={{ maxWidth: '100%', overflow: 'hidden' }}>
275
+ {availableTiles.map(tileKey => {
276
+ const customTitle = config.smallMultiples?.tileTitles?.[tileKey] || ''
277
+ return (
278
+ <div
279
+ key={tileKey}
280
+ className='tile-title-row'
281
+ style={{
282
+ display: 'flex',
283
+ alignItems: 'center',
284
+ marginBottom: '0.75rem',
285
+ maxWidth: '100%'
286
+ }}
287
+ >
288
+ <label
289
+ style={{
290
+ minWidth: '80px',
291
+ maxWidth: '120px',
292
+ marginRight: '0.75rem',
293
+ fontWeight: 'normal',
294
+ fontSize: '13px',
295
+ overflow: 'hidden',
296
+ textOverflow: 'ellipsis',
297
+ whiteSpace: 'nowrap',
298
+ flexShrink: 0
299
+ }}
300
+ >
301
+ {tileKey}:
302
+ </label>
303
+ <input
304
+ type='text'
305
+ value={customTitle}
306
+ placeholder={tileKey}
307
+ onChange={event => handleTitleChange(tileKey, event.target.value)}
308
+ style={{
309
+ flex: 1,
310
+ minWidth: 0,
311
+ maxWidth: '200px',
312
+ fontSize: '13px',
313
+ padding: '4px 8px',
314
+ height: '30px',
315
+ border: '1px solid #ccc',
316
+ borderRadius: '3px'
317
+ }}
318
+ />
319
+ </div>
320
+ )
321
+ })}
322
+ </div>
323
+ )
324
+ })()}
325
+ </div>
326
+
327
+ <CheckBox
328
+ value={config.smallMultiples?.synchronizedTooltips}
329
+ fieldName='synchronizedTooltips'
330
+ section='smallMultiples'
331
+ label='Synchronized Tooltips'
332
+ updateField={updateField}
333
+ tooltip={
334
+ <Tooltip style={{ textTransform: 'none' }}>
335
+ <Tooltip.Target>
336
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
337
+ </Tooltip.Target>
338
+ <Tooltip.Content>
339
+ <p>
340
+ When checked, hovering over a geography in one map will show synchronized tooltips for that same
341
+ geography on all other maps at the same position.
342
+ </p>
343
+ </Tooltip.Content>
344
+ </Tooltip>
345
+ }
346
+ />
347
+ </>
348
+ )}
349
+ </AccordionItemPanel>
350
+ </AccordionItem>
351
+ )
352
+ }
353
+
354
+ export default PanelSmallMultiples
@@ -1,10 +1,12 @@
1
1
  import React from 'react'
2
2
  import Annotate from './Panel.Annotate'
3
3
  import PatternSettings from './Panel.PatternSettings'
4
+ import SmallMultiples from './Panel.SmallMultiples'
4
5
 
5
6
  const Panels = {
6
7
  Annotate,
7
- PatternSettings
8
+ PatternSettings,
9
+ SmallMultiples
8
10
  }
9
11
 
10
12
  export default Panels
@@ -6,14 +6,33 @@ type GeoProps = {
6
6
  strokeWidth?: number
7
7
  path?: string
8
8
  className?: string
9
+ onMouseEnter?: () => void
10
+ onClick?: () => void
11
+ 'data-country-code'?: string
12
+ 'data-tooltip-id'?: string
13
+ 'data-tooltip-html'?: string
14
+ additionalData?: any
15
+ geoData?: any
16
+ additionaldata?: string
17
+ geodata?: string
18
+ tabIndex?: number
9
19
  }
10
20
 
11
21
  const Geo: React.FC<GeoProps> = ({ path, styles, stroke, strokeWidth, ...props }) => {
12
- const { className, ...restProps } = props
13
- const geoClassName = String(props.additionalData?.name)?.toLowerCase()?.replaceAll(' ', '') || 'country'
22
+ const { className, 'data-country-code': dataCountryCode, ...restProps } = props
23
+ const geoClassName = String(props.additionalData?.name)?.toLowerCase()?.replace(/\s+/g, '') || 'country'
14
24
  return (
15
25
  <g className={`geo-group ${geoClassName}`} style={styles} {...restProps}>
16
- <path tabIndex={-1} className={`single-geo ${geoClassName}`} stroke={stroke} strokeWidth={strokeWidth} d={path} />
26
+ <path
27
+ tabIndex={-1}
28
+ className={`single-geo ${geoClassName} ${className || ''}`}
29
+ stroke={stroke}
30
+ strokeWidth={strokeWidth}
31
+ strokeLinejoin='round'
32
+ strokeLinecap='round'
33
+ d={path}
34
+ data-country-code={dataCountryCode}
35
+ />
17
36
  </g>
18
37
  )
19
38
  }
@@ -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'
@@ -20,11 +21,12 @@ import './index.scss'
20
21
  import { type ViewPort } from '@cdc/core/types/ViewPort'
21
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'
27
28
  import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
29
+ import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
28
30
 
29
31
  const LEGEND_PADDING = 30
30
32
 
@@ -45,8 +47,7 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
45
47
  dimensions,
46
48
  mapId,
47
49
  runtimeFilters,
48
- runtimeLegend,
49
- setRuntimeLegend
50
+ runtimeLegend
50
51
  } = useContext<MapContext>(ConfigContext)
51
52
 
52
53
  const dispatch = useContext(MapDispatchContext)
@@ -61,7 +62,10 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
61
62
 
62
63
  const getFormattedLegendItems = () => {
63
64
  try {
64
- if (!runtimeLegend.items) Error('No runtime legend data')
65
+ if (!runtimeLegend.items) {
66
+ console.warn('No runtime legend data available')
67
+ return []
68
+ }
65
69
  return runtimeLegend.items.map((entry, idx) => {
66
70
  const entryMax = displayDataAsText(entry.max, 'primary', config)
67
71
 
@@ -120,34 +124,36 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
120
124
  return classes.join(' ')
121
125
  }
122
126
 
123
- const setAccessibleStatus = (message: string) => {
124
- dispatch({ type: 'SET_ACCESSIBLE_STATUS', payload: message })
125
- }
126
-
127
127
  return (
128
128
  <li
129
129
  className={handleListItemClass()}
130
130
  key={idx}
131
131
  title={`Legend item ${item.label} - Click to disable`}
132
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
- )
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
+ })
140
143
  }}
141
144
  onKeyDown={e => {
142
145
  if (e.key === 'Enter') {
143
146
  e.preventDefault()
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
- )
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
+ })
151
157
  }
152
158
  }}
153
159
  tabIndex={0}
@@ -165,9 +171,9 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
165
171
  const { pattern, dataKey, size } = patternData
166
172
  let defaultPatternColor = 'black'
167
173
  const sizes = {
168
- small: '8',
169
- medium: '10',
170
- large: '12'
174
+ small: 8,
175
+ medium: 10,
176
+ large: 12
171
177
  }
172
178
 
173
179
  const legendSize = 16
@@ -240,8 +246,15 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
240
246
  if (e) {
241
247
  e.preventDefault()
242
248
  }
243
- publishAnalyticsEvent('map_legend_reset', 'click', interactionLabel, 'map')
244
- 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)
245
258
  dispatch({
246
259
  type: 'SET_ACCESSIBLE_STATUS',
247
260
  payload: 'Legend has been reset, please reference the data table to see updated values.'
@@ -261,14 +274,17 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
261
274
  />
262
275
  )
263
276
 
264
- const cityStyleShapes = {
265
- pin: pin,
266
- circle: <GlyphCircle color='#000' size={150} />,
267
- square: <GlyphSquare color='#000' size={150} />,
268
- diamond: <GlyphDiamond color='#000' size={150} />,
269
- star: <GlyphStar color='#000' size={150} />,
270
- triangle: <GlyphTriangle color='#000' size={150} />
271
- }
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
+ )
272
288
 
273
289
  const shouldRenderLegendList = legendListItems.length > 0 && ['Select Option', ''].includes(config.legend.groupBy)
274
290
 
@@ -286,9 +302,29 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
286
302
  <section className={legendClasses.section.join(' ') || ''} aria-label='Map Legend'>
287
303
  {(legend.title || legend.description || legend.dynamicDescription) && (
288
304
  <div className='mb-3'>
289
- {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
+ )}
290
317
  {legend.dynamicDescription === false && legend.description && (
291
- <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>
292
328
  )}
293
329
  {legend.dynamicDescription === true &&
294
330
  runtimeFilters.map((filter, idx) => {
@@ -351,9 +387,9 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
351
387
  )}
352
388
 
353
389
  {config.visual.additionalCityStyles.map(
354
- ({ shape, label }) =>
390
+ ({ shape, label }, index) =>
355
391
  label && (
356
- <div>
392
+ <div key={`additional-city-style-${index}-${shape}`}>
357
393
  <svg>
358
394
  <Group top={shape === 'Pin' ? 19 : shape === 'Triangle' ? 13 : 11} left={10}>
359
395
  {cityStyleShapes[shape.toLowerCase()]}
@@ -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
  >
@@ -1,4 +1,4 @@
1
- @import '../../../scss/mixins';
1
+ @import '@cdc/core/styles/v2/utils/breakpoints';
2
2
 
3
3
  .cdc-map-inner-container {
4
4
  .map-container.world aside.side {