@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.
- package/.idea/map.iml +12 -0
- package/.idea/modules.xml +8 -0
- package/.idea/vcs.xml +6 -0
- package/dist/cdcmap.js +31254 -32242
- package/examples/hex-colors.json +3 -3
- package/examples/m2.json +32904 -0
- package/examples/private/test.json +470 -1457
- package/examples/private/{mmr.json → wastewatermap.json} +86 -115
- package/index.html +36 -63
- package/package.json +7 -19
- package/src/CdcMap.tsx +56 -1552
- package/src/CdcMapComponent.tsx +608 -0
- package/src/_stories/CdcMap.Legend.Gradient.stories.tsx +10 -0
- package/src/_stories/CdcMap.Legend.stories.tsx +67 -0
- package/src/_stories/CdcMap.Table.stories.tsx +19 -0
- package/src/_stories/CdcMap.stories.tsx +12 -1
- package/src/_stories/UsaMap.NoData.stories.tsx +4 -4
- package/src/_stories/_mock/default-patterns.json +8 -5
- package/src/_stories/_mock/legend-bins.json +428 -0
- package/{examples/private/default-patterns.json → src/_stories/_mock/legends/legend-tests.json} +36 -131
- package/src/cdcMapComponent.styles.css +9 -0
- package/src/components/Annotation/Annotation.Draggable.tsx +27 -26
- package/src/components/Annotation/AnnotationDropdown.tsx +5 -6
- package/src/components/BubbleList.tsx +135 -49
- package/src/components/CityList.tsx +89 -87
- package/src/components/DataTable.tsx +8 -8
- package/src/components/EditorPanel/components/EditorPanel.tsx +823 -885
- package/src/components/EditorPanel/components/Error.tsx +9 -2
- package/src/components/EditorPanel/components/HexShapeSettings.tsx +127 -141
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +55 -86
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +89 -75
- package/src/components/EditorPanel/components/editorPanel.styles.css +95 -0
- package/src/components/Geo.tsx +9 -1
- package/src/components/GoogleMap/components/GoogleMap.tsx +1 -1
- package/src/components/Legend/components/Legend.tsx +92 -87
- package/src/components/Legend/components/LegendGroup/Legend.Group.tsx +128 -0
- package/src/components/Legend/components/LegendGroup/legend.group.css +27 -0
- package/src/components/Legend/components/LegendItem.Hex.tsx +4 -1
- package/src/components/Legend/components/index.scss +74 -17
- package/src/components/Modal.tsx +17 -7
- package/src/components/NavigationMenu.tsx +11 -9
- package/src/components/UsaMap/components/SingleState/SingleState.CountyOutput.tsx +12 -8
- package/src/components/UsaMap/components/SingleState/SingleState.StateOutput.tsx +4 -4
- package/src/components/UsaMap/components/TerritoriesSection.tsx +33 -10
- package/src/components/UsaMap/components/Territory/Territory.Hexagon.tsx +12 -10
- package/src/components/UsaMap/components/Territory/Territory.Rectangle.tsx +12 -14
- package/src/components/UsaMap/components/Territory/TerritoryShape.ts +2 -1
- package/src/components/UsaMap/components/UsaMap.County.tsx +138 -96
- package/src/components/UsaMap/components/UsaMap.Region.styles.css +72 -0
- package/src/components/UsaMap/components/UsaMap.Region.tsx +56 -103
- package/src/components/UsaMap/components/UsaMap.SingleState.styles.css +10 -0
- package/src/components/UsaMap/components/UsaMap.SingleState.tsx +65 -74
- package/src/components/UsaMap/components/UsaMap.State.tsx +112 -91
- package/src/components/UsaMap/helpers/map.ts +1 -1
- package/src/components/UsaMap/helpers/shapes.ts +20 -7
- package/src/components/WorldMap/WorldMap.tsx +64 -118
- package/src/components/WorldMap/worldMap.styles.css +28 -0
- package/src/components/ZoomControls.tsx +15 -13
- package/src/components/zoomControls.styles.css +53 -0
- package/src/context.ts +17 -9
- package/src/data/initial-state.js +5 -2
- package/src/helpers/addUIDs.ts +150 -0
- package/src/helpers/applyColorToLegend.ts +39 -64
- package/src/helpers/applyLegendToRow.ts +51 -0
- package/src/helpers/colorDistributions.ts +12 -0
- package/src/helpers/constants.ts +44 -0
- package/src/helpers/displayGeoName.ts +9 -2
- package/src/helpers/formatLegendLocation.ts +3 -2
- package/src/helpers/generateColorsArray.ts +2 -1
- package/src/helpers/generateRuntimeData.ts +78 -0
- package/src/helpers/generateRuntimeFilters.ts +63 -0
- package/src/helpers/generateRuntimeLegend.ts +566 -0
- package/src/helpers/generateRuntimeLegendHash.ts +16 -15
- package/src/helpers/getColumnNames.ts +19 -0
- package/src/helpers/getMapContainerClasses.ts +23 -0
- package/src/helpers/getStatePicked.ts +8 -0
- package/src/helpers/handleMapTabbing.ts +31 -0
- package/src/helpers/hashObj.ts +1 -1
- package/src/helpers/index.ts +22 -0
- package/src/helpers/navigationHandler.ts +3 -3
- package/src/helpers/resetLegendToggles.ts +13 -0
- package/src/helpers/setBinNumbers.ts +5 -0
- package/src/helpers/sortSpecialClassesLast.ts +7 -0
- package/src/helpers/tests/getColumnNames.test.ts +52 -0
- package/src/helpers/titleCase.ts +1 -1
- package/src/helpers/toggleLegendActive.ts +25 -0
- package/src/hooks/useApplyTooltipsToGeo.tsx +51 -0
- package/src/hooks/useColumnsRequiredChecker.ts +51 -0
- package/src/hooks/useGeoClickHandler.ts +45 -0
- package/src/hooks/useLegendSeparators.ts +26 -0
- package/src/hooks/useMapLayers.tsx +34 -60
- package/src/hooks/useModal.ts +22 -0
- package/src/hooks/useResizeObserver.ts +4 -5
- package/src/hooks/useStateZoom.tsx +52 -75
- package/src/hooks/useTooltip.ts +2 -3
- package/src/index.jsx +3 -9
- package/src/scss/editor-panel.scss +3 -99
- package/src/scss/main.scss +1 -19
- package/src/scss/map.scss +15 -220
- package/src/store/map.actions.ts +46 -0
- package/src/store/map.reducer.ts +96 -0
- package/src/types/Annotations.ts +24 -0
- package/src/types/MapConfig.ts +23 -3
- package/src/types/MapContext.ts +36 -35
- package/src/types/Modal.ts +1 -0
- package/src/types/RuntimeData.ts +3 -0
- package/examples/private/DEV-9644.json +0 -184
- package/examples/private/DEV-9989.json +0 -229
- package/examples/private/ardi.json +0 -180
- package/examples/private/colors 2.json +0 -416
- package/examples/private/colors.json +0 -416
- package/examples/private/colors.json.zip +0 -0
- package/examples/private/customColors.json +0 -45348
- package/examples/test.json +0 -183
- package/src/helpers/closeModal.ts +0 -9
- package/src/scss/btn.scss +0 -69
- package/src/scss/filters.scss +0 -27
- package/src/scss/variables.scss +0 -1
- /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
|
-
|
|
39
|
-
|
|
41
|
+
config,
|
|
42
|
+
currentViewport: viewport,
|
|
43
|
+
dimensions,
|
|
44
|
+
mapId,
|
|
40
45
|
runtimeFilters,
|
|
41
46
|
runtimeLegend,
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
state,
|
|
45
|
-
currentViewport: viewport,
|
|
46
|
-
mapId
|
|
47
|
-
} = useContext(ConfigContext)
|
|
47
|
+
setRuntimeLegend
|
|
48
|
+
} = useContext<MapContext>(ConfigContext)
|
|
48
49
|
|
|
49
|
-
const
|
|
50
|
+
const dispatch = useContext(MapDispatchContext)
|
|
51
|
+
|
|
52
|
+
const { legend } = config
|
|
50
53
|
const isLegendGradient = legend.style === 'gradient'
|
|
51
|
-
const boxDynamicallyHidden = isBelowBreakpoint('md',
|
|
54
|
+
const boxDynamicallyHidden = isBelowBreakpoint('md', viewport)
|
|
52
55
|
const legendWrapping =
|
|
53
|
-
(legend.position === 'left' || legend.position === 'right') && isBelowBreakpoint('md',
|
|
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
|
-
|
|
79
|
-
|
|
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
|
-
|
|
82
|
-
|
|
66
|
+
const entryMin = displayDataAsText(entry.min, 'primary', config)
|
|
67
|
+
let formattedText = `${entryMin}${entryMax !== entryMin ? ` - ${entryMax}` : ''}`
|
|
83
68
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
69
|
+
// If interval, add some formatting
|
|
70
|
+
if (legend.type === 'equalinterval' && idx !== runtimeLegend.length - 1) {
|
|
71
|
+
formattedText = `${entryMin} - < ${entryMax}`
|
|
72
|
+
}
|
|
88
73
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
74
|
+
if (legend.type === 'category') {
|
|
75
|
+
formattedText = displayDataAsText(entry.value, 'primary', config)
|
|
76
|
+
}
|
|
92
77
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
78
|
+
if (entry.max === 0 && entry.min === 0) {
|
|
79
|
+
formattedText = '0'
|
|
80
|
+
}
|
|
96
81
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
82
|
+
if (entry.max === null && entry.min === null) {
|
|
83
|
+
formattedText = 'No data'
|
|
84
|
+
}
|
|
100
85
|
|
|
101
|
-
|
|
86
|
+
let legendLabel = formattedText
|
|
102
87
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
88
|
+
if (entry.hasOwnProperty('special')) {
|
|
89
|
+
legendLabel = entry.label || entry.value
|
|
90
|
+
}
|
|
106
91
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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(
|
|
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={
|
|
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 (
|
|
145
|
+
if (config.map.patterns) {
|
|
154
146
|
// loop over map patterns
|
|
155
|
-
|
|
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={
|
|
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(
|
|
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
|
-
|
|
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-
|
|
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()
|
|
300
|
-
colors={getFormattedLegendItems()
|
|
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={
|
|
304
|
+
config={config}
|
|
306
305
|
/>
|
|
307
|
-
{
|
|
308
|
-
|
|
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
|
-
|
|
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
|
-
{
|
|
319
|
+
{config.visual.cityStyleLabel && (
|
|
317
320
|
<div>
|
|
318
321
|
<svg>
|
|
319
322
|
<Group
|
|
320
|
-
top={
|
|
323
|
+
top={
|
|
324
|
+
config.visual.cityStyle === 'pin' ? 19 : config.visual.cityStyle === 'triangle' ? 13 : 11
|
|
325
|
+
}
|
|
321
326
|
left={10}
|
|
322
327
|
>
|
|
323
|
-
{cityStyleShapes[
|
|
328
|
+
{cityStyleShapes[config.visual.cityStyle.toLowerCase()]}
|
|
324
329
|
</Group>
|
|
325
330
|
</svg>
|
|
326
|
-
<p>{
|
|
331
|
+
<p>{config.visual.cityStyleLabel}</p>
|
|
327
332
|
</div>
|
|
328
333
|
)}
|
|
329
334
|
|
|
330
|
-
{
|
|
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
|
-
{
|
|
354
|
-
<LegendItemHex
|
|
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 {
|
|
8
|
+
const { currentViewport: viewport } = props
|
|
9
|
+
const { config: state } = useContext(ConfigContext)
|
|
7
10
|
|
|
8
11
|
const getItemShape = shape => {
|
|
9
12
|
switch (shape) {
|