@cdc/map 4.25.3 → 4.25.5-1

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 (111) hide show
  1. package/dist/cdcmap.js +38945 -41511
  2. package/examples/hex-colors.json +3 -3
  3. package/examples/private/test.json +470 -1457
  4. package/examples/private/{mmr.json → wastewatermap.json} +86 -115
  5. package/index.html +13 -41
  6. package/package.json +4 -10
  7. package/src/CdcMap.tsx +51 -1555
  8. package/src/CdcMapComponent.tsx +594 -0
  9. package/src/_stories/CdcMap.Legend.Gradient.stories.tsx +10 -0
  10. package/src/_stories/CdcMap.Legend.stories.tsx +67 -0
  11. package/src/_stories/CdcMap.stories.tsx +4 -1
  12. package/src/_stories/UsaMap.NoData.stories.tsx +4 -4
  13. package/{examples/private/default-patterns.json → src/_stories/_mock/legends/legend-tests.json} +36 -131
  14. package/src/cdcMapComponent.styles.css +9 -0
  15. package/src/components/Annotation/Annotation.Draggable.tsx +27 -26
  16. package/src/components/Annotation/AnnotationDropdown.tsx +5 -6
  17. package/src/components/BubbleList.tsx +135 -49
  18. package/src/components/CityList.tsx +89 -87
  19. package/src/components/DataTable.tsx +8 -8
  20. package/src/components/EditorPanel/components/EditorPanel.tsx +714 -820
  21. package/src/components/EditorPanel/components/Error.tsx +9 -2
  22. package/src/components/EditorPanel/components/HexShapeSettings.tsx +127 -141
  23. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +55 -86
  24. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +89 -75
  25. package/src/components/EditorPanel/components/editorPanel.styles.css +95 -0
  26. package/src/components/Geo.tsx +9 -1
  27. package/src/components/GoogleMap/components/GoogleMap.tsx +1 -1
  28. package/src/components/Legend/components/Legend.tsx +92 -87
  29. package/src/components/Legend/components/LegendGroup/Legend.Group.tsx +128 -0
  30. package/src/components/Legend/components/LegendGroup/legend.group.css +27 -0
  31. package/src/components/Legend/components/LegendItem.Hex.tsx +4 -1
  32. package/src/components/Legend/components/index.scss +18 -6
  33. package/src/components/Modal.tsx +17 -7
  34. package/src/components/NavigationMenu.tsx +11 -9
  35. package/src/components/UsaMap/components/SingleState/SingleState.CountyOutput.tsx +12 -8
  36. package/src/components/UsaMap/components/SingleState/SingleState.StateOutput.tsx +4 -4
  37. package/src/components/UsaMap/components/TerritoriesSection.tsx +33 -10
  38. package/src/components/UsaMap/components/Territory/Territory.Hexagon.tsx +12 -10
  39. package/src/components/UsaMap/components/Territory/Territory.Rectangle.tsx +12 -14
  40. package/src/components/UsaMap/components/Territory/TerritoryShape.ts +2 -1
  41. package/src/components/UsaMap/components/UsaMap.County.tsx +138 -96
  42. package/src/components/UsaMap/components/UsaMap.Region.styles.css +72 -0
  43. package/src/components/UsaMap/components/UsaMap.Region.tsx +56 -103
  44. package/src/components/UsaMap/components/UsaMap.SingleState.styles.css +10 -0
  45. package/src/components/UsaMap/components/UsaMap.SingleState.tsx +59 -66
  46. package/src/components/UsaMap/components/UsaMap.State.tsx +112 -91
  47. package/src/components/UsaMap/helpers/map.ts +1 -1
  48. package/src/components/UsaMap/helpers/shapes.ts +20 -7
  49. package/src/components/WorldMap/WorldMap.tsx +64 -118
  50. package/src/components/WorldMap/worldMap.styles.css +28 -0
  51. package/src/components/ZoomControls.tsx +15 -13
  52. package/src/components/zoomControls.styles.css +53 -0
  53. package/src/context.ts +17 -9
  54. package/src/data/initial-state.js +5 -2
  55. package/src/helpers/addUIDs.ts +151 -0
  56. package/src/helpers/applyColorToLegend.ts +39 -64
  57. package/src/helpers/applyLegendToRow.ts +51 -0
  58. package/src/helpers/colorDistributions.ts +12 -0
  59. package/src/helpers/constants.ts +44 -0
  60. package/src/helpers/displayGeoName.ts +9 -2
  61. package/src/helpers/generateColorsArray.ts +2 -1
  62. package/src/helpers/generateRuntimeData.ts +74 -0
  63. package/src/helpers/generateRuntimeFilters.ts +63 -0
  64. package/src/helpers/generateRuntimeLegend.ts +537 -0
  65. package/src/helpers/generateRuntimeLegendHash.ts +16 -15
  66. package/src/helpers/getColumnNames.ts +19 -0
  67. package/src/helpers/getMapContainerClasses.ts +23 -0
  68. package/src/helpers/handleMapTabbing.ts +31 -0
  69. package/src/helpers/hashObj.ts +1 -1
  70. package/src/helpers/index.ts +22 -0
  71. package/src/helpers/navigationHandler.ts +3 -3
  72. package/src/helpers/resetLegendToggles.ts +13 -0
  73. package/src/helpers/setBinNumbers.ts +5 -0
  74. package/src/helpers/sortSpecialClassesLast.ts +7 -0
  75. package/src/helpers/tests/getColumnNames.test.ts +52 -0
  76. package/src/helpers/titleCase.ts +1 -1
  77. package/src/helpers/toggleLegendActive.ts +25 -0
  78. package/src/hooks/useApplyTooltipsToGeo.tsx +51 -0
  79. package/src/hooks/useColumnsRequiredChecker.ts +51 -0
  80. package/src/hooks/useGeoClickHandler.ts +45 -0
  81. package/src/hooks/useLegendSeparators.ts +26 -0
  82. package/src/hooks/useMapLayers.tsx +34 -60
  83. package/src/hooks/useModal.ts +22 -0
  84. package/src/hooks/useResizeObserver.ts +4 -5
  85. package/src/hooks/useStateZoom.tsx +52 -75
  86. package/src/hooks/useTooltip.ts +2 -3
  87. package/src/index.jsx +3 -9
  88. package/src/scss/editor-panel.scss +3 -99
  89. package/src/scss/main.scss +1 -19
  90. package/src/scss/map.scss +15 -220
  91. package/src/store/map.actions.ts +46 -0
  92. package/src/store/map.reducer.ts +96 -0
  93. package/src/types/Annotations.ts +24 -0
  94. package/src/types/MapConfig.ts +23 -3
  95. package/src/types/MapContext.ts +36 -35
  96. package/src/types/Modal.ts +1 -0
  97. package/src/types/RuntimeData.ts +3 -0
  98. package/LICENSE +0 -201
  99. package/examples/private/DEV-9644.json +0 -184
  100. package/examples/private/DEV-9989.json +0 -229
  101. package/examples/private/ardi.json +0 -180
  102. package/examples/private/colors 2.json +0 -416
  103. package/examples/private/colors.json +0 -416
  104. package/examples/private/colors.json.zip +0 -0
  105. package/examples/private/customColors.json +0 -45348
  106. package/examples/test.json +0 -183
  107. package/src/helpers/closeModal.ts +0 -9
  108. package/src/scss/btn.scss +0 -69
  109. package/src/scss/filters.scss +0 -27
  110. package/src/scss/variables.scss +0 -1
  111. /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) {
@@ -70,6 +70,10 @@
70
70
  .legend-container {
71
71
  position: relative;
72
72
 
73
+ .legend-container__li--disabled {
74
+ opacity: 0.4;
75
+ }
76
+
73
77
  .tspan {
74
78
  font-size: var(--legend-item-font-size) !important;
75
79
  }
@@ -79,7 +83,6 @@
79
83
  font-weight: var(--legend-title-font-weight);
80
84
  padding-bottom: 0;
81
85
  display: inline-block;
82
- color: black;
83
86
  }
84
87
  .legend-container__description {
85
88
  font-size: var(--legend-description-font-size);
@@ -98,7 +101,7 @@
98
101
  }
99
102
  .legend-container__ul:not(.single-row) {
100
103
  list-style: none;
101
- display: grid;
104
+ display: grid !important;
102
105
  grid-template-columns: 1fr;
103
106
 
104
107
  @include breakpoint(md) {
@@ -118,15 +121,24 @@
118
121
  width: 100%;
119
122
  }
120
123
  .legend-container__li {
121
- flex-shrink: 0;
122
- display: inline-block;
124
+ display: flex;
125
+ align-items: center;
126
+ gap: 0.5em;
127
+ width: 100%;
123
128
  padding-right: 1em;
124
129
  vertical-align: middle;
125
130
  transition: 0.1s opacity;
126
- display: flex;
127
131
  cursor: pointer;
128
- white-space: wrap;
129
132
  flex-grow: 1;
133
+ white-space: nowrap; // Prevents label wrapping
134
+
135
+ .legend-item {
136
+ display: flex;
137
+ justify-content: center;
138
+ align-items: center;
139
+ flex-shrink: 0;
140
+ }
141
+
130
142
  &:last-child {
131
143
  margin-bottom: 0;
132
144
  }
@@ -1,14 +1,15 @@
1
1
  import { useContext } from 'react'
2
- import LegendShape from '@cdc/core/components/LegendShape'
3
- import ConfigContext from '../context'
2
+ import ConfigContext, { MapDispatchContext } from '../context'
4
3
  import Icon from '@cdc/core/components/ui/Icon'
4
+ import useApplyTooltipsToGeo from '../hooks/useApplyTooltipsToGeo'
5
+ import { MapContext } from '../types/MapContext'
5
6
 
6
7
  const Modal = () => {
7
- const { applyTooltipsToGeo, applyLegendToRow, content, state, currentViewport: viewport } = useContext(ConfigContext)
8
- const { capitalizeLabels } = state.tooltips
8
+ const { content, config, currentViewport: viewport } = useContext<MapContext>(ConfigContext)
9
+ const { capitalizeLabels } = config.tooltips
10
+ const { applyTooltipsToGeo } = useApplyTooltipsToGeo()
9
11
  const tooltip = applyTooltipsToGeo(content.geoName, content.keyedData, 'jsx')
10
- const type = state.general.type
11
- const legendColors = applyLegendToRow(content.keyedData)
12
+ const dispatch = useContext(MapDispatchContext)
12
13
 
13
14
  return (
14
15
  <section
@@ -18,7 +19,16 @@ const Modal = () => {
18
19
  aria-hidden='true'
19
20
  >
20
21
  <div className='content'>{tooltip}</div>
21
- <Icon display='close' alt='Close Modal' size={20} color='#000' className='modal-close' />
22
+ <Icon
23
+ display='close'
24
+ alt='Close Modal'
25
+ size={20}
26
+ color='#000'
27
+ className='modal-close'
28
+ onClick={() => {
29
+ dispatch({ type: 'SET_MODAL', payload: null })
30
+ }}
31
+ />
22
32
  </section>
23
33
  )
24
34
  }
@@ -24,7 +24,7 @@ const NavigationMenu = ({ data, navigationHandler, options, columns, displayGeoN
24
24
  navGo = 'Ir'
25
25
  break
26
26
  default:
27
- navSelect = 'Select an Item'
27
+ navSelect = 'Select a Location'
28
28
  navGo = 'Go'
29
29
  }
30
30
 
@@ -55,15 +55,17 @@ const NavigationMenu = ({ data, navigationHandler, options, columns, displayGeoN
55
55
  <form onSubmit={handleSubmit} type='get'>
56
56
  <label htmlFor={mapTabbingID.replace('#', '')}>
57
57
  <div className='select-heading'>{navSelect}</div>
58
- <select value={activeGeo} id={mapTabbingID.replace('#', '')} onChange={e => setActiveGeo(e.target.value)}>
59
- {Object.keys(dropdownItems).map(key => (
60
- <option key={key} value={key}>
61
- {key}
62
- </option>
63
- ))}
64
- </select>
58
+ <div className='d-flex'>
59
+ <select value={activeGeo} id={mapTabbingID.replace('#', '')} onChange={e => setActiveGeo(e.target.value)}>
60
+ {Object.keys(dropdownItems).map(key => (
61
+ <option key={key} value={key}>
62
+ {key}
63
+ </option>
64
+ ))}
65
+ </select>
66
+ <input type='submit' value={navGo} className={`${options.headerColor} btn`} id='cdcnavmap-dropdown-go' />
67
+ </div>
65
68
  </label>
66
- <input type='submit' value={navGo} className={`${options.headerColor} btn`} id='cdcnavmap-dropdown-go' />
67
69
  </form>
68
70
  </section>
69
71
  )