@cdc/map 4.25.3 → 4.25.6

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 (119) hide show
  1. package/.idea/map.iml +12 -0
  2. package/.idea/modules.xml +8 -0
  3. package/.idea/vcs.xml +6 -0
  4. package/dist/cdcmap.js +31254 -32242
  5. package/examples/hex-colors.json +3 -3
  6. package/examples/m2.json +32904 -0
  7. package/examples/private/test.json +470 -1457
  8. package/examples/private/{mmr.json → wastewatermap.json} +86 -115
  9. package/index.html +36 -63
  10. package/package.json +7 -19
  11. package/src/CdcMap.tsx +56 -1552
  12. package/src/CdcMapComponent.tsx +608 -0
  13. package/src/_stories/CdcMap.Legend.Gradient.stories.tsx +10 -0
  14. package/src/_stories/CdcMap.Legend.stories.tsx +67 -0
  15. package/src/_stories/CdcMap.Table.stories.tsx +19 -0
  16. package/src/_stories/CdcMap.stories.tsx +12 -1
  17. package/src/_stories/UsaMap.NoData.stories.tsx +4 -4
  18. package/src/_stories/_mock/default-patterns.json +8 -5
  19. package/src/_stories/_mock/legend-bins.json +428 -0
  20. package/{examples/private/default-patterns.json → src/_stories/_mock/legends/legend-tests.json} +36 -131
  21. package/src/cdcMapComponent.styles.css +9 -0
  22. package/src/components/Annotation/Annotation.Draggable.tsx +27 -26
  23. package/src/components/Annotation/AnnotationDropdown.tsx +5 -6
  24. package/src/components/BubbleList.tsx +135 -49
  25. package/src/components/CityList.tsx +89 -87
  26. package/src/components/DataTable.tsx +8 -8
  27. package/src/components/EditorPanel/components/EditorPanel.tsx +823 -885
  28. package/src/components/EditorPanel/components/Error.tsx +9 -2
  29. package/src/components/EditorPanel/components/HexShapeSettings.tsx +127 -141
  30. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +55 -86
  31. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +89 -75
  32. package/src/components/EditorPanel/components/editorPanel.styles.css +95 -0
  33. package/src/components/Geo.tsx +9 -1
  34. package/src/components/GoogleMap/components/GoogleMap.tsx +1 -1
  35. package/src/components/Legend/components/Legend.tsx +92 -87
  36. package/src/components/Legend/components/LegendGroup/Legend.Group.tsx +128 -0
  37. package/src/components/Legend/components/LegendGroup/legend.group.css +27 -0
  38. package/src/components/Legend/components/LegendItem.Hex.tsx +4 -1
  39. package/src/components/Legend/components/index.scss +74 -17
  40. package/src/components/Modal.tsx +17 -7
  41. package/src/components/NavigationMenu.tsx +11 -9
  42. package/src/components/UsaMap/components/SingleState/SingleState.CountyOutput.tsx +12 -8
  43. package/src/components/UsaMap/components/SingleState/SingleState.StateOutput.tsx +4 -4
  44. package/src/components/UsaMap/components/TerritoriesSection.tsx +33 -10
  45. package/src/components/UsaMap/components/Territory/Territory.Hexagon.tsx +12 -10
  46. package/src/components/UsaMap/components/Territory/Territory.Rectangle.tsx +12 -14
  47. package/src/components/UsaMap/components/Territory/TerritoryShape.ts +2 -1
  48. package/src/components/UsaMap/components/UsaMap.County.tsx +138 -96
  49. package/src/components/UsaMap/components/UsaMap.Region.styles.css +72 -0
  50. package/src/components/UsaMap/components/UsaMap.Region.tsx +56 -103
  51. package/src/components/UsaMap/components/UsaMap.SingleState.styles.css +10 -0
  52. package/src/components/UsaMap/components/UsaMap.SingleState.tsx +65 -74
  53. package/src/components/UsaMap/components/UsaMap.State.tsx +112 -91
  54. package/src/components/UsaMap/helpers/map.ts +1 -1
  55. package/src/components/UsaMap/helpers/shapes.ts +20 -7
  56. package/src/components/WorldMap/WorldMap.tsx +64 -118
  57. package/src/components/WorldMap/worldMap.styles.css +28 -0
  58. package/src/components/ZoomControls.tsx +15 -13
  59. package/src/components/zoomControls.styles.css +53 -0
  60. package/src/context.ts +17 -9
  61. package/src/data/initial-state.js +5 -2
  62. package/src/helpers/addUIDs.ts +150 -0
  63. package/src/helpers/applyColorToLegend.ts +39 -64
  64. package/src/helpers/applyLegendToRow.ts +51 -0
  65. package/src/helpers/colorDistributions.ts +12 -0
  66. package/src/helpers/constants.ts +44 -0
  67. package/src/helpers/displayGeoName.ts +9 -2
  68. package/src/helpers/formatLegendLocation.ts +3 -2
  69. package/src/helpers/generateColorsArray.ts +2 -1
  70. package/src/helpers/generateRuntimeData.ts +78 -0
  71. package/src/helpers/generateRuntimeFilters.ts +63 -0
  72. package/src/helpers/generateRuntimeLegend.ts +566 -0
  73. package/src/helpers/generateRuntimeLegendHash.ts +16 -15
  74. package/src/helpers/getColumnNames.ts +19 -0
  75. package/src/helpers/getMapContainerClasses.ts +23 -0
  76. package/src/helpers/getStatePicked.ts +8 -0
  77. package/src/helpers/handleMapTabbing.ts +31 -0
  78. package/src/helpers/hashObj.ts +1 -1
  79. package/src/helpers/index.ts +22 -0
  80. package/src/helpers/navigationHandler.ts +3 -3
  81. package/src/helpers/resetLegendToggles.ts +13 -0
  82. package/src/helpers/setBinNumbers.ts +5 -0
  83. package/src/helpers/sortSpecialClassesLast.ts +7 -0
  84. package/src/helpers/tests/getColumnNames.test.ts +52 -0
  85. package/src/helpers/titleCase.ts +1 -1
  86. package/src/helpers/toggleLegendActive.ts +25 -0
  87. package/src/hooks/useApplyTooltipsToGeo.tsx +51 -0
  88. package/src/hooks/useColumnsRequiredChecker.ts +51 -0
  89. package/src/hooks/useGeoClickHandler.ts +45 -0
  90. package/src/hooks/useLegendSeparators.ts +26 -0
  91. package/src/hooks/useMapLayers.tsx +34 -60
  92. package/src/hooks/useModal.ts +22 -0
  93. package/src/hooks/useResizeObserver.ts +4 -5
  94. package/src/hooks/useStateZoom.tsx +52 -75
  95. package/src/hooks/useTooltip.ts +2 -3
  96. package/src/index.jsx +3 -9
  97. package/src/scss/editor-panel.scss +3 -99
  98. package/src/scss/main.scss +1 -19
  99. package/src/scss/map.scss +15 -220
  100. package/src/store/map.actions.ts +46 -0
  101. package/src/store/map.reducer.ts +96 -0
  102. package/src/types/Annotations.ts +24 -0
  103. package/src/types/MapConfig.ts +23 -3
  104. package/src/types/MapContext.ts +36 -35
  105. package/src/types/Modal.ts +1 -0
  106. package/src/types/RuntimeData.ts +3 -0
  107. package/examples/private/DEV-9644.json +0 -184
  108. package/examples/private/DEV-9989.json +0 -229
  109. package/examples/private/ardi.json +0 -180
  110. package/examples/private/colors 2.json +0 -416
  111. package/examples/private/colors.json +0 -416
  112. package/examples/private/colors.json.zip +0 -0
  113. package/examples/private/customColors.json +0 -45348
  114. package/examples/test.json +0 -183
  115. package/src/helpers/closeModal.ts +0 -9
  116. package/src/scss/btn.scss +0 -69
  117. package/src/scss/filters.scss +0 -27
  118. package/src/scss/variables.scss +0 -1
  119. /package/src/hooks/{useActiveElement.js → useActiveElement.ts} +0 -0
@@ -12,7 +12,7 @@ import LegendItemHex from './LegendItem.Hex'
12
12
  import Button from '@cdc/core/components/elements/Button'
13
13
 
14
14
  import useDataVizClasses from '@cdc/core/helpers/useDataVizClasses'
15
- import ConfigContext from '../../../context'
15
+ import ConfigContext, { MapDispatchContext } from '../../../context'
16
16
  import { PatternLines, PatternCircles, PatternWaves } from '@visx/pattern'
17
17
  import { GlyphStar, GlyphTriangle, GlyphDiamond, GlyphSquare, GlyphCircle } from '@visx/glyph'
18
18
  import { Group } from '@visx/group'
@@ -20,6 +20,10 @@ import './index.scss'
20
20
  import { type ViewPort } from '@cdc/core/types/ViewPort'
21
21
  import { isBelowBreakpoint, isMobileHeightViewport } from '@cdc/core/helpers/viewports'
22
22
  import { displayDataAsText } from '@cdc/core/helpers/displayDataAsText'
23
+ import { toggleLegendActive } from '@cdc/map/src/helpers/toggleLegendActive'
24
+ import { resetLegendToggles } from '../../../helpers'
25
+ import { MapContext } from '../../../types/MapContext'
26
+ import LegendGroup from './LegendGroup/Legend.Group'
23
27
 
24
28
  const LEGEND_PADDING = 30
25
29
 
@@ -32,91 +36,76 @@ type LegendProps = {
32
36
 
33
37
  const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
34
38
  const { skipId, containerWidthPadding } = props
35
- const { isEditor, dimensions, currentViewport } = useContext(ConfigContext)
36
39
 
37
40
  const {
38
- // prettier-ignore
39
- resetLegendToggles,
41
+ config,
42
+ currentViewport: viewport,
43
+ dimensions,
44
+ mapId,
40
45
  runtimeFilters,
41
46
  runtimeLegend,
42
- setAccessibleStatus,
43
- setRuntimeLegend,
44
- state,
45
- currentViewport: viewport,
46
- mapId
47
- } = useContext(ConfigContext)
47
+ setRuntimeLegend
48
+ } = useContext<MapContext>(ConfigContext)
48
49
 
49
- const { legend } = state
50
+ const dispatch = useContext(MapDispatchContext)
51
+
52
+ const { legend } = config
50
53
  const isLegendGradient = legend.style === 'gradient'
51
- const boxDynamicallyHidden = isBelowBreakpoint('md', currentViewport)
54
+ const boxDynamicallyHidden = isBelowBreakpoint('md', viewport)
52
55
  const legendWrapping =
53
- (legend.position === 'left' || legend.position === 'right') && isBelowBreakpoint('md', currentViewport)
56
+ (legend.position === 'left' || legend.position === 'right') && isBelowBreakpoint('md', viewport)
54
57
  const legendOnBottom = legend.position === 'bottom' || legendWrapping
55
58
  const needsTopMargin = legend.hideBorder && legendOnBottom
56
59
 
57
- // Toggles if a legend is active and being applied to the map and data table.
58
- const toggleLegendActive = (i, legendLabel) => {
59
- const newValue = !runtimeLegend[i].disabled
60
-
61
- runtimeLegend[i].disabled = newValue // Toggle!
62
-
63
- let newLegend = [...runtimeLegend]
64
-
65
- newLegend[i].disabled = newValue
66
-
67
- const disabledAmt = runtimeLegend.disabledAmt ?? 0
68
-
69
- newLegend['disabledAmt'] = newValue ? disabledAmt + 1 : disabledAmt - 1
70
-
71
- setRuntimeLegend(newLegend)
72
-
73
- setAccessibleStatus(
74
- `Disabled legend item ${legendLabel ?? ''}. Please reference the data table to see updated values.`
75
- )
76
- }
77
60
  const getFormattedLegendItems = () => {
78
- return runtimeLegend.map((entry, idx) => {
79
- const entryMax = displayDataAsText(entry.max, 'primary', state)
61
+ try {
62
+ if (!runtimeLegend.items) Error('No runtime legend data')
63
+ return runtimeLegend.items.map((entry, idx) => {
64
+ const entryMax = displayDataAsText(entry.max, 'primary', config)
80
65
 
81
- const entryMin = displayDataAsText(entry.min, 'primary', state)
82
- let formattedText = `${entryMin}${entryMax !== entryMin ? ` - ${entryMax}` : ''}`
66
+ const entryMin = displayDataAsText(entry.min, 'primary', config)
67
+ let formattedText = `${entryMin}${entryMax !== entryMin ? ` - ${entryMax}` : ''}`
83
68
 
84
- // If interval, add some formatting
85
- if (legend.type === 'equalinterval' && idx !== runtimeLegend.length - 1) {
86
- formattedText = `${entryMin} - < ${entryMax}`
87
- }
69
+ // If interval, add some formatting
70
+ if (legend.type === 'equalinterval' && idx !== runtimeLegend.length - 1) {
71
+ formattedText = `${entryMin} - < ${entryMax}`
72
+ }
88
73
 
89
- if (legend.type === 'category') {
90
- formattedText = displayDataAsText(entry.value, 'primary', state)
91
- }
74
+ if (legend.type === 'category') {
75
+ formattedText = displayDataAsText(entry.value, 'primary', config)
76
+ }
92
77
 
93
- if (entry.max === 0 && entry.min === 0) {
94
- formattedText = '0'
95
- }
78
+ if (entry.max === 0 && entry.min === 0) {
79
+ formattedText = '0'
80
+ }
96
81
 
97
- if (entry.max === null && entry.min === null) {
98
- formattedText = 'No data'
99
- }
82
+ if (entry.max === null && entry.min === null) {
83
+ formattedText = 'No data'
84
+ }
100
85
 
101
- let legendLabel = formattedText
86
+ let legendLabel = formattedText
102
87
 
103
- if (entry.hasOwnProperty('special')) {
104
- legendLabel = entry.label || entry.value
105
- }
88
+ if (entry.hasOwnProperty('special')) {
89
+ legendLabel = entry.label || entry.value
90
+ }
106
91
 
107
- return {
108
- color: entry.color,
109
- label: parse(legendLabel),
110
- disabled: entry.disabled,
111
- special: entry.hasOwnProperty('special'),
112
- value: [entry.min, entry.max]
113
- }
114
- })
92
+ return {
93
+ color: entry.color,
94
+ label: parse(legendLabel),
95
+ disabled: entry.disabled,
96
+ special: entry.hasOwnProperty('special'),
97
+ value: [entry.min, entry.max]
98
+ }
99
+ })
100
+ } catch (e) {
101
+ console.error('Error in getFormattedLegendItems', e) // eslint-disable-line
102
+ return []
103
+ }
115
104
  }
116
105
 
117
106
  const legendList = (patternsOnly = false) => {
118
107
  const formattedItems = patternsOnly ? [] : getFormattedLegendItems()
119
- const patternsOnlyFont = isMobileHeightViewport(currentViewport) ? '12px' : '14px'
108
+ const patternsOnlyFont = isMobileHeightViewport(viewport) ? '12px' : '14px'
120
109
  const hasDisabledItems = formattedItems.some(item => item.disabled)
121
110
  let legendItems
122
111
 
@@ -129,30 +118,33 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
129
118
  return classes.join(' ')
130
119
  }
131
120
 
121
+ const setAccessibleStatus = (message: string) => {
122
+ dispatch({ type: 'SET_ACCESSIBLE_STATUS', payload: message })
123
+ }
124
+
132
125
  return (
133
- // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-noninteractive-element-interactions
134
126
  <li
135
127
  className={handleListItemClass()}
136
128
  key={idx}
137
129
  title={`Legend item ${item.label} - Click to disable`}
138
- onClick={() => toggleLegendActive(idx, item.label)}
130
+ onClick={() => toggleLegendActive(idx, item.label, runtimeLegend, setRuntimeLegend, setAccessibleStatus)}
139
131
  onKeyDown={e => {
140
132
  if (e.key === 'Enter') {
141
133
  e.preventDefault()
142
- toggleLegendActive(idx, item.label)
134
+ toggleLegendActive(idx, item.label, runtimeLegend, setRuntimeLegend, setAccessibleStatus)
143
135
  }
144
136
  }}
145
137
  tabIndex={0}
146
138
  >
147
- <LegendShape shape={state.legend.style === 'boxes' ? 'square' : 'circle'} fill={item.color} />
139
+ <LegendShape shape={config.legend.style === 'boxes' ? 'square' : 'circle'} fill={item.color} />
148
140
  <span>{item.label}</span>
149
141
  </li>
150
142
  )
151
143
  })
152
144
 
153
- if (state.map.patterns) {
145
+ if (config.map.patterns) {
154
146
  // loop over map patterns
155
- state.map.patterns.map((patternData, patternDataIndex) => {
147
+ config.map.patterns.map((patternData, patternDataIndex) => {
156
148
  const { pattern, dataKey, size } = patternData
157
149
  let defaultPatternColor = 'black'
158
150
  const sizes = {
@@ -178,6 +170,7 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
178
170
  height={sizes[size] ?? 10}
179
171
  width={sizes[size] ?? 10}
180
172
  fill={defaultPatternColor}
173
+ strokeWidth={0.25}
181
174
  />
182
175
  )}
183
176
  {pattern === 'circles' && (
@@ -186,6 +179,7 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
186
179
  height={sizes[size] ?? 10}
187
180
  width={sizes[size] ?? 10}
188
181
  fill={defaultPatternColor}
182
+ radius={1.25}
189
183
  />
190
184
  )}
191
185
  {pattern === 'lines' && (
@@ -194,7 +188,7 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
194
188
  height={sizes[size] ?? 6}
195
189
  width={sizes[size] ?? 10}
196
190
  stroke={defaultPatternColor}
197
- strokeWidth={2}
191
+ strokeWidth={0.75}
198
192
  orientation={['diagonalRightToLeft']}
199
193
  />
200
194
  )}
@@ -222,15 +216,18 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
222
216
  }
223
217
  const legendListItems = legendList(isLegendGradient)
224
218
 
225
- const { legendClasses } = useDataVizClasses(state, viewport)
219
+ const { legendClasses } = useDataVizClasses(config, viewport)
226
220
 
227
221
  const handleReset = e => {
228
222
  const legend = ref.current
229
223
  if (e) {
230
224
  e.preventDefault()
231
225
  }
232
- resetLegendToggles()
233
- setAccessibleStatus('Legend has been reset, please reference the data table to see updated values.')
226
+ resetLegendToggles(runtimeLegend, setRuntimeLegend)
227
+ dispatch({
228
+ type: 'SET_ACCESSIBLE_STATUS',
229
+ payload: 'Legend has been reset, please reference the data table to see updated values.'
230
+ })
234
231
  if (legend) {
235
232
  legend.focus()
236
233
  }
@@ -255,9 +252,11 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
255
252
  triangle: <GlyphTriangle color='#000' size={150} />
256
253
  }
257
254
 
255
+ const shouldRenderLegendList = legendListItems.length > 0 && ['Select Option', ''].includes(config.legend.groupBy)
256
+
258
257
  return (
259
258
  <ErrorBoundary component='Sidebar'>
260
- <div className={`legends ${needsTopMargin ? 'mt-1' : ''}`}>
259
+ <div className={`legends ${needsTopMargin ? 'mt-4' : ''}`}>
261
260
  <aside
262
261
  id={skipId || 'legend'}
263
262
  className={legendClasses.aside.join(' ') || ''}
@@ -296,38 +295,44 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
296
295
  )}
297
296
 
298
297
  <LegendGradient
299
- labels={getFormattedLegendItems().map(item => item?.label) ?? []}
300
- colors={getFormattedLegendItems().map(item => item?.color) ?? []}
298
+ labels={getFormattedLegendItems()?.map(item => item?.label) ?? []}
299
+ colors={getFormattedLegendItems()?.map(item => item?.color) ?? []}
301
300
  dimensions={dimensions}
302
301
  parentPaddingToSubtract={
303
302
  containerWidthPadding + (legend.hideBorder || boxDynamicallyHidden ? 0 : LEGEND_PADDING)
304
303
  }
305
- config={state}
304
+ config={config}
306
305
  />
307
- {!!legendListItems.length && (
308
- <ul className={legendClasses.ul.join(' ') || ''} aria-label='Legend items'>
306
+ <LegendGroup legendItems={getFormattedLegendItems()} />
307
+
308
+ {shouldRenderLegendList && (
309
+ <ul className={legendClasses.ul.join(' ')} aria-label='Legend items'>
309
310
  {legendListItems}
310
311
  </ul>
311
312
  )}
312
- {(state.visual.additionalCityStyles.some(c => c.label) || state.visual.cityStyleLabel) && (
313
+
314
+ {((config.visual.additionalCityStyles && config.visual.additionalCityStyles.some(c => c.label)) ||
315
+ config.visual.cityStyleLabel) && (
313
316
  <>
314
317
  <hr />
315
318
  <div className={legendClasses.div.join(' ') || ''}>
316
- {state.visual.cityStyleLabel && (
319
+ {config.visual.cityStyleLabel && (
317
320
  <div>
318
321
  <svg>
319
322
  <Group
320
- top={state.visual.cityStyle === 'pin' ? 19 : state.visual.cityStyle === 'triangle' ? 13 : 11}
323
+ top={
324
+ config.visual.cityStyle === 'pin' ? 19 : config.visual.cityStyle === 'triangle' ? 13 : 11
325
+ }
321
326
  left={10}
322
327
  >
323
- {cityStyleShapes[state.visual.cityStyle.toLowerCase()]}
328
+ {cityStyleShapes[config.visual.cityStyle.toLowerCase()]}
324
329
  </Group>
325
330
  </svg>
326
- <p>{state.visual.cityStyleLabel}</p>
331
+ <p>{config.visual.cityStyleLabel}</p>
327
332
  </div>
328
333
  )}
329
334
 
330
- {state.visual.additionalCityStyles.map(
335
+ {config.visual.additionalCityStyles.map(
331
336
  ({ shape, label }) =>
332
337
  label && (
333
338
  <div>
@@ -350,8 +355,8 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
350
355
  )}
351
356
  </section>
352
357
  </aside>
353
- {state.hexMap.shapeGroups?.length > 0 && state.hexMap.type === 'shapes' && state.general.displayAsHex && (
354
- <LegendItemHex state={state} runtimeLegend={runtimeLegend} viewport={viewport} />
358
+ {config.hexMap?.shapeGroups?.length > 0 && config.hexMap.type === 'shapes' && config.general.displayAsHex && (
359
+ <LegendItemHex runtimeLegend={runtimeLegend} viewport={viewport} />
355
360
  )}
356
361
  </div>
357
362
  </ErrorBoundary>
@@ -0,0 +1,128 @@
1
+ import { useContext, useMemo } from 'react'
2
+ import './legend.group.css'
3
+ import LegendShape from '@cdc/core/components/LegendShape'
4
+ import { toggleLegendActive } from '@cdc/map/src/helpers/toggleLegendActive'
5
+ import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
6
+ import ConfigContext, { MapDispatchContext } from '../../../../context'
7
+
8
+ interface LegendItem {
9
+ color: string
10
+ label: string
11
+ disabled?: boolean
12
+ special: boolean
13
+ }
14
+
15
+ interface GroupedData {
16
+ [key: string]: LegendItem[]
17
+ }
18
+
19
+ const LegendGroup = ({ legendItems }) => {
20
+ const { runtimeLegend, setRuntimeLegend, config } = useContext(ConfigContext)
21
+ const dispatch = useContext(MapDispatchContext)
22
+ const groupLegendItems = (items: LegendItem[], data: object[], groupByKey: string): GroupedData => {
23
+ if (!groupByKey || !data || !items) return {}
24
+
25
+ const columnKey = config.columns.primary.name || ''
26
+ const result: GroupedData = {}
27
+
28
+ for (const row of data) {
29
+ const groupValue = row[groupByKey]
30
+ if (!groupValue) continue
31
+
32
+ const label = row[columnKey]
33
+ const match = items.find(i => i.label === label)
34
+ if (!match) continue
35
+
36
+ result[groupValue] ||= []
37
+ if (!result[groupValue].some(i => i.label === label)) {
38
+ result[groupValue].push(match)
39
+ }
40
+ }
41
+
42
+ // Sort items in each group
43
+ Object.entries(result).forEach(([group, items]) => {
44
+ result[group] = [...items].sort(
45
+ (a, b) =>
46
+ (config.legend.categoryValuesOrder ?? []).indexOf(a.label) -
47
+ (config.legend.categoryValuesOrder ?? []).indexOf(b.label)
48
+ )
49
+ })
50
+
51
+ return result
52
+ }
53
+
54
+ const handleToggleItem = (item: LegendItem) => {
55
+ const newItems = runtimeLegend.items.map(legend =>
56
+ legend.value === item.label ? { ...legend, disabled: !legend.disabled } : legend
57
+ )
58
+
59
+ const wasDisabled = runtimeLegend.items.find(i => i.value === item.label)?.disabled
60
+ const delta = wasDisabled ? -1 : 1
61
+
62
+ setRuntimeLegend({
63
+ ...runtimeLegend,
64
+ items: newItems,
65
+ disabledAmt: (runtimeLegend.disabledAmt ?? 0) + delta
66
+ })
67
+ const message = `${wasDisabled ? 'Enabled' : 'Disabled'} legend item ${
68
+ item.label
69
+ }. Please reference the data table.`
70
+ dispatch({ type: 'SET_ACCESSIBLE_STATUS', payload: message })
71
+ }
72
+
73
+ const getLegendItemClasses = (item: LegendItem, hasDisabledItems: boolean) => {
74
+ return [
75
+ 'group-list-item',
76
+ item.disabled ? 'legend-group-item-disable' : hasDisabledItems ? 'legend-group-item-not-disable' : ''
77
+ ]
78
+ .filter(Boolean)
79
+ .join(' ')
80
+ }
81
+
82
+ const gridClass =
83
+ config.legend.position === 'bottom' || config.legend.position === 'top'
84
+ ? 'col-12 col-sm-6 col-md-4 col-lg-3'
85
+ : 'col-12'
86
+
87
+ const groupedData = useMemo(
88
+ () => groupLegendItems(legendItems, config.data, config.legend.groupBy),
89
+ [legendItems, config.data, config.legend.groupBy]
90
+ )
91
+
92
+ const hasDisabledItems = runtimeLegend.items.some(item => item.disabled)
93
+
94
+ return (
95
+ <ErrorBoundary component='Grouped Legend'>
96
+ <div className='row'>
97
+ {Object.entries(groupedData).map(([groupName, items]) => (
98
+ <div className={`${gridClass} group-container`} key={groupName}>
99
+ <p className='group-label'>{groupName}</p>
100
+ <ul className='row'>
101
+ {items.map((item, index) => (
102
+ <li
103
+ key={`${item.label}-${index}`}
104
+ role='button'
105
+ tabIndex={0}
106
+ title={`Legend item ${item.label} - Click to disable`}
107
+ className={getLegendItemClasses(item, hasDisabledItems)}
108
+ onClick={() => handleToggleItem(item)}
109
+ onKeyDown={e => {
110
+ if (e.key === 'Enter' || e.key === ' ') {
111
+ e.preventDefault()
112
+ toggleLegendActive(index, item.label, runtimeLegend, setRuntimeLegend, setAccessibleStatus)
113
+ }
114
+ }}
115
+ >
116
+ <LegendShape shape={config.legend.style === 'boxes' ? 'square' : 'circle'} fill={item.color} />
117
+ <span>{item.label}</span>
118
+ </li>
119
+ ))}
120
+ </ul>
121
+ </div>
122
+ ))}
123
+ </div>
124
+ </ErrorBoundary>
125
+ )
126
+ }
127
+
128
+ export default LegendGroup
@@ -0,0 +1,27 @@
1
+ .group-label {
2
+ font-weight: 500;
3
+ font-family: Nunito, sans-serif;
4
+ font-size: 1rem;
5
+ margin-bottom: 0.5rem;
6
+ }
7
+ .group-list-item {
8
+ list-style: none;
9
+ font-weight: 400;
10
+ font-size: 0.889rem;
11
+ margin-top: 0.5rem !important;
12
+ cursor: pointer;
13
+ }
14
+
15
+ .group-container {
16
+ margin-bottom: 2rem;
17
+ }
18
+
19
+ .legend-group-item-disable {
20
+ opacity: 0.4;
21
+ }
22
+
23
+ .legend-group-item-not-disable {
24
+ outline: 1px solid #005ea2;
25
+ outline-offset: 5px;
26
+ border-radius: 1px;
27
+ }
@@ -1,9 +1,12 @@
1
1
  import useDataVizClasses from '@cdc/core/helpers/useDataVizClasses'
2
2
  import { AiOutlineArrowUp, AiOutlineArrowDown, AiOutlineArrowRight } from 'react-icons/ai'
3
3
  import parse from 'html-react-parser'
4
+ import ConfigContext from '../../../context'
5
+ import { useContext } from 'react'
4
6
 
5
7
  const LegendItemHex = props => {
6
- const { state, currentViewport: viewport } = props
8
+ const { currentViewport: viewport } = props
9
+ const { config: state } = useContext(ConfigContext)
7
10
 
8
11
  const getItemShape = shape => {
9
12
  switch (shape) {