@cdc/map 4.23.11 → 4.24.2
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/dist/cdcmap.js +52413 -46767
- package/examples/default-patterns.json +581 -0
- package/examples/default-usa.json +159 -57
- package/examples/test.json +0 -9614
- package/examples/zika.json +1194 -0
- package/index.html +17 -6
- package/package.json +3 -3
- package/src/CdcMap.tsx +39 -31
- package/src/components/CityList.jsx +34 -23
- package/src/components/{EditorPanel.jsx → EditorPanel/components/EditorPanel.tsx} +31 -64
- package/src/components/{HexShapeSettings.jsx → EditorPanel/components/HexShapeSettings.tsx} +2 -51
- package/src/components/EditorPanel/components/Inputs.tsx +59 -0
- package/src/components/EditorPanel/components/Panel.PatternSettings.tsx +140 -0
- package/src/components/EditorPanel/components/Panels.tsx +7 -0
- package/src/components/EditorPanel/index.tsx +3 -0
- package/src/components/Geo.jsx +4 -2
- package/src/components/Legend/components/Legend.tsx +183 -0
- package/src/components/Legend/components/LegendItem.Hex.tsx +49 -0
- package/src/components/Legend/components/index.scss +235 -0
- package/src/components/Legend/index.tsx +3 -0
- package/src/components/UsaMap/components/Territory/Territory.Hexagon.tsx +129 -0
- package/src/components/UsaMap/components/Territory/Territory.Rectangle.tsx +66 -0
- package/src/components/UsaMap/components/Territory/index.tsx +9 -0
- package/src/components/{CountyMap.jsx → UsaMap/components/UsaMap.County.tsx} +9 -9
- package/src/components/{UsaRegionMap.jsx → UsaMap/components/UsaMap.Region.tsx} +1 -3
- package/src/components/{SingleStateMap.jsx → UsaMap/components/UsaMap.SingleState.tsx} +8 -10
- package/src/components/{UsaMap.jsx → UsaMap/components/UsaMap.State.tsx} +55 -127
- package/src/components/UsaMap/index.tsx +13 -0
- package/src/components/{WorldMap.jsx → WorldMap/components/WorldMap.jsx} +20 -14
- package/src/components/WorldMap/data/world-topo-guiana-update.json +1 -0
- package/src/components/WorldMap/data/world-topo-old.json +1 -0
- package/src/components/WorldMap/data/world-topo-recent.json +39194 -0
- package/src/components/WorldMap/data/world-topo.json +1 -0
- package/src/components/WorldMap/index.tsx +3 -0
- package/src/context.ts +2 -1
- package/src/data/initial-state.js +5 -3
- package/src/data/supported-geos.js +21 -1
- package/src/hooks/useTooltip.ts +4 -4
- package/src/scss/editor-panel.scss +2 -3
- package/src/scss/main.scss +11 -1
- package/src/scss/map.scss +22 -12
- package/src/types/MapConfig.ts +149 -0
- package/src/types/MapContext.ts +45 -0
- package/src/types/runtimeLegend.ts +1 -0
- package/examples/world-geocode-data.json +0 -18
- package/examples/world-geocode.json +0 -108
- package/src/components/Sidebar.tsx +0 -142
- package/src/data/abbreviations.js +0 -57
- package/src/data/feature-test.json +0 -73
- package/src/data/newtest.json +0 -1
- package/src/data/state-abbreviations.js +0 -60
- package/src/data/supported-cities.csv +0 -165
- package/src/data/test.json +0 -1
- package/src/data/world-topo.json +0 -1
- package/src/scss/sidebar.scss +0 -230
- /package/src/{data → components/UsaMap/data}/cb_2019_us_county_20m.json +0 -0
- /package/src/{data → components/UsaMap/data}/us-hex-topo.json +0 -0
- /package/src/{data → components/UsaMap/data}/us-regions-topo-2.json +0 -0
- /package/src/{data → components/UsaMap/data}/us-regions-topo.json +0 -0
- /package/src/{data → components/UsaMap/data}/us-topo.json +0 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { memo, useState, useEffect } from 'react'
|
|
2
|
+
import { useDebounce } from 'use-debounce'
|
|
3
|
+
|
|
4
|
+
// todo: look into combining these with core
|
|
5
|
+
const CheckBox = memo(({ label, value, fieldName, section = null, subsection = null, tooltip, updateField, ...attributes }) => (
|
|
6
|
+
<label className='checkbox column-heading'>
|
|
7
|
+
<input
|
|
8
|
+
type='checkbox'
|
|
9
|
+
name={fieldName}
|
|
10
|
+
checked={value}
|
|
11
|
+
onChange={e => {
|
|
12
|
+
updateField(section, subsection, fieldName, !value)
|
|
13
|
+
}}
|
|
14
|
+
{...attributes}
|
|
15
|
+
/>
|
|
16
|
+
<span className='edit-label'>
|
|
17
|
+
{label}
|
|
18
|
+
{tooltip}
|
|
19
|
+
</span>
|
|
20
|
+
</label>
|
|
21
|
+
))
|
|
22
|
+
|
|
23
|
+
const TextField = ({ label, section = null, subsection = null, fieldName, updateField, value: stateValue, type = 'input', tooltip, ...attributes }) => {
|
|
24
|
+
const [value, setValue] = useState(stateValue)
|
|
25
|
+
|
|
26
|
+
const [debouncedValue] = useDebounce(value, 500)
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if ('string' === typeof debouncedValue && stateValue !== debouncedValue) {
|
|
30
|
+
updateField(section, subsection, fieldName, debouncedValue)
|
|
31
|
+
}
|
|
32
|
+
}, [debouncedValue]) // eslint-disable-line
|
|
33
|
+
|
|
34
|
+
let name = subsection ? `${section}-${subsection}-${fieldName}` : `${section}-${subsection}-${fieldName}`
|
|
35
|
+
|
|
36
|
+
const onChange = e => setValue(e.target.value)
|
|
37
|
+
|
|
38
|
+
let formElement = <input type='text' name={name} onChange={onChange} {...attributes} value={value} />
|
|
39
|
+
|
|
40
|
+
if ('textarea' === type) {
|
|
41
|
+
formElement = <textarea name={name} onChange={onChange} {...attributes} value={value}></textarea>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if ('number' === type) {
|
|
45
|
+
formElement = <input type='number' name={name} onChange={onChange} {...attributes} value={value} />
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<label>
|
|
50
|
+
<span className='edit-label column-heading'>
|
|
51
|
+
{label}
|
|
52
|
+
{tooltip}
|
|
53
|
+
</span>
|
|
54
|
+
{formElement}
|
|
55
|
+
</label>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export { CheckBox, TextField }
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { useContext } from 'react'
|
|
2
|
+
import { Accordion, AccordionItem, AccordionItemHeading, AccordionItemPanel, AccordionItemButton } from 'react-accessible-accordion'
|
|
3
|
+
import ConfigContext from '../../../context'
|
|
4
|
+
import { type MapContext } from '../../../types/MapContext'
|
|
5
|
+
import Button from '@cdc/core/components/elements/Button'
|
|
6
|
+
|
|
7
|
+
type PanelProps = {
|
|
8
|
+
name: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const PatternSettings = ({ name }: PanelProps) => {
|
|
12
|
+
const { state, setState } = useContext<MapContext>(ConfigContext)
|
|
13
|
+
const defaultPattern = 'circles'
|
|
14
|
+
const patternTypes = ['circles', 'waves', 'lines']
|
|
15
|
+
|
|
16
|
+
const {
|
|
17
|
+
map: { patterns },
|
|
18
|
+
data
|
|
19
|
+
} = state
|
|
20
|
+
|
|
21
|
+
/** Updates the map config with a new pattern item */
|
|
22
|
+
const handleAddGeoPattern = () => {
|
|
23
|
+
let patterns = [...state.map.patterns]
|
|
24
|
+
patterns.push({ dataKey: '', pattern: defaultPattern })
|
|
25
|
+
setState({
|
|
26
|
+
...state,
|
|
27
|
+
map: {
|
|
28
|
+
...state.map,
|
|
29
|
+
patterns
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Updates the map pattern at a given index */
|
|
35
|
+
const handleUpdateGeoPattern = (value: string, index: number, keyToUpdate: 'dataKey' | 'pattern' | 'dataValue' | 'size' | 'label') => {
|
|
36
|
+
const updatedPatterns = [...state.map.patterns]
|
|
37
|
+
updatedPatterns[index] = { ...updatedPatterns[index], [keyToUpdate]: value }
|
|
38
|
+
|
|
39
|
+
setState({
|
|
40
|
+
...state,
|
|
41
|
+
map: {
|
|
42
|
+
...state.map,
|
|
43
|
+
patterns: updatedPatterns
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const handleRemovePattern = index => {
|
|
49
|
+
const updatedPatterns = state.map.patterns.filter((pattern, i) => i !== index)
|
|
50
|
+
|
|
51
|
+
setState({
|
|
52
|
+
...state,
|
|
53
|
+
map: {
|
|
54
|
+
...state.map,
|
|
55
|
+
patterns: updatedPatterns
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<AccordionItem>
|
|
62
|
+
<AccordionItemHeading>
|
|
63
|
+
<AccordionItemButton>{name}</AccordionItemButton>
|
|
64
|
+
</AccordionItemHeading>
|
|
65
|
+
<AccordionItemPanel>
|
|
66
|
+
{patterns &&
|
|
67
|
+
patterns.map((pattern, patternIndex) => {
|
|
68
|
+
const dataValueOptions = [...new Set(data?.map(d => d?.[pattern?.dataKey]))]
|
|
69
|
+
const dataKeyOptions = Object.keys(data[0])
|
|
70
|
+
dataValueOptions.unshift('Select')
|
|
71
|
+
dataKeyOptions.unshift('Select')
|
|
72
|
+
|
|
73
|
+
dataValueOptions.sort()
|
|
74
|
+
dataKeyOptions.sort()
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<Accordion allowZeroExpanded>
|
|
78
|
+
<AccordionItem>
|
|
79
|
+
<AccordionItemHeading>
|
|
80
|
+
<AccordionItemButton>{pattern.dataKey ? `${pattern.dataKey}: ${pattern.dataValue ?? 'No Value'}` : 'Select Column'}</AccordionItemButton>
|
|
81
|
+
</AccordionItemHeading>
|
|
82
|
+
<AccordionItemPanel>
|
|
83
|
+
<>
|
|
84
|
+
<label htmlFor={`pattern-dataKey--${patternIndex}`}>Data Key:</label>
|
|
85
|
+
<select id={`pattern-dataKey--${patternIndex}`} value={pattern.dataKey !== '' ? pattern.dataKey : 'Select'} onChange={e => handleUpdateGeoPattern(e.target.value, patternIndex, 'dataKey')}>
|
|
86
|
+
{/* TODO: sort these? */}
|
|
87
|
+
{dataKeyOptions.map((d, index) => {
|
|
88
|
+
return (
|
|
89
|
+
<option value={d} key={index}>
|
|
90
|
+
{d}
|
|
91
|
+
</option>
|
|
92
|
+
)
|
|
93
|
+
})}
|
|
94
|
+
</select>
|
|
95
|
+
|
|
96
|
+
<label htmlFor={`pattern-dataValue--${patternIndex}`}>
|
|
97
|
+
Data Value:
|
|
98
|
+
<input type='text' onChange={e => handleUpdateGeoPattern(e.target.value, patternIndex, 'dataValue')} id={`pattern-dataValue--${patternIndex}`} value={pattern.dataValue === '' ? '' : pattern.dataValue} />
|
|
99
|
+
</label>
|
|
100
|
+
|
|
101
|
+
<label htmlFor={`pattern-label--${patternIndex}`}>
|
|
102
|
+
Label (optional):
|
|
103
|
+
<input type='text' onChange={e => handleUpdateGeoPattern(e.target.value, patternIndex, 'label')} id={`pattern-dataValue--${patternIndex}`} value={pattern.label === '' ? '' : pattern.label} />
|
|
104
|
+
</label>
|
|
105
|
+
|
|
106
|
+
<label htmlFor={`pattern-type--${patternIndex}`}>Pattern Type:</label>
|
|
107
|
+
<select id={`pattern-type--${patternIndex}`} value={pattern?.pattern} onChange={e => handleUpdateGeoPattern(e.target.value, patternIndex, 'pattern')}>
|
|
108
|
+
{patternTypes.map((patternName, index) => (
|
|
109
|
+
<option value={patternName} key={index}>
|
|
110
|
+
{patternName}
|
|
111
|
+
</option>
|
|
112
|
+
))}
|
|
113
|
+
</select>
|
|
114
|
+
|
|
115
|
+
<label htmlFor={`pattern-size--${patternIndex}`}>Pattern Size:</label>
|
|
116
|
+
<select id={`pattern-size--${patternIndex}`} value={pattern?.size} onChange={e => handleUpdateGeoPattern(e.target.value, patternIndex, 'size')}>
|
|
117
|
+
{['small', 'medium', 'large'].map((size, index) => (
|
|
118
|
+
<option value={size} key={index}>
|
|
119
|
+
{size}
|
|
120
|
+
</option>
|
|
121
|
+
))}
|
|
122
|
+
</select>
|
|
123
|
+
<Button onClick={e => handleRemovePattern(patternIndex)} className='btn btn--remove warn'>
|
|
124
|
+
Remove Pattern
|
|
125
|
+
</Button>
|
|
126
|
+
</>
|
|
127
|
+
</AccordionItemPanel>
|
|
128
|
+
</AccordionItem>
|
|
129
|
+
</Accordion>
|
|
130
|
+
)
|
|
131
|
+
})}
|
|
132
|
+
<button className='btn full-width' onClick={handleAddGeoPattern}>
|
|
133
|
+
Add Geo Pattern
|
|
134
|
+
</button>
|
|
135
|
+
</AccordionItemPanel>
|
|
136
|
+
</AccordionItem>
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export default PatternSettings
|
package/src/components/Geo.jsx
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import React, { memo } from 'react'
|
|
2
2
|
|
|
3
3
|
const Geo = ({ path, styles, stroke, strokeWidth, ...props }) => {
|
|
4
|
+
const { className, ...restProps } = props
|
|
5
|
+
const geoClassName = String(props.additionalData?.name)?.toLowerCase()?.replaceAll(' ', '') || 'country'
|
|
4
6
|
return (
|
|
5
|
-
<g className=
|
|
6
|
-
<path tabIndex={-1} className=
|
|
7
|
+
<g className={`geo-group ${geoClassName}`} style={styles} {...restProps}>
|
|
8
|
+
<path tabIndex={-1} className={`single-geo ${geoClassName}`} stroke={stroke} strokeWidth={strokeWidth} d={path} />
|
|
7
9
|
</g>
|
|
8
10
|
)
|
|
9
11
|
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
//TODO: Move legends to core
|
|
2
|
+
import { useContext } from 'react'
|
|
3
|
+
import parse from 'html-react-parser'
|
|
4
|
+
import chroma from 'chroma-js'
|
|
5
|
+
|
|
6
|
+
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
7
|
+
import LegendCircle from '@cdc/core/components/LegendCircle'
|
|
8
|
+
import LegendItemHex from './LegendItem.Hex'
|
|
9
|
+
|
|
10
|
+
import useDataVizClasses from '@cdc/core/helpers/useDataVizClasses'
|
|
11
|
+
import ConfigContext from '../../../context'
|
|
12
|
+
import { PatternLines, PatternCircles, PatternWaves } from '@visx/pattern'
|
|
13
|
+
import './index.scss'
|
|
14
|
+
|
|
15
|
+
const Legend = () => {
|
|
16
|
+
// prettier-ignore
|
|
17
|
+
const {
|
|
18
|
+
displayDataAsText,
|
|
19
|
+
resetLegendToggles,
|
|
20
|
+
runtimeFilters,
|
|
21
|
+
runtimeLegend,
|
|
22
|
+
setAccessibleStatus,
|
|
23
|
+
setRuntimeLegend,
|
|
24
|
+
state,
|
|
25
|
+
viewport,
|
|
26
|
+
} = useContext(ConfigContext)
|
|
27
|
+
|
|
28
|
+
const { legend } = state
|
|
29
|
+
|
|
30
|
+
// Toggles if a legend is active and being applied to the map and data table.
|
|
31
|
+
const toggleLegendActive = (i, legendLabel) => {
|
|
32
|
+
const newValue = !runtimeLegend[i].disabled
|
|
33
|
+
|
|
34
|
+
runtimeLegend[i].disabled = newValue // Toggle!
|
|
35
|
+
|
|
36
|
+
let newLegend = [...runtimeLegend]
|
|
37
|
+
|
|
38
|
+
newLegend[i].disabled = newValue
|
|
39
|
+
|
|
40
|
+
const disabledAmt = runtimeLegend.disabledAmt ?? 0
|
|
41
|
+
|
|
42
|
+
newLegend['disabledAmt'] = newValue ? disabledAmt + 1 : disabledAmt - 1
|
|
43
|
+
|
|
44
|
+
setRuntimeLegend(newLegend)
|
|
45
|
+
|
|
46
|
+
setAccessibleStatus(`Disabled legend item ${legendLabel ?? ''}. Please reference the data table to see updated values.`)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const legendList = () => {
|
|
50
|
+
let legendItems
|
|
51
|
+
|
|
52
|
+
legendItems = runtimeLegend.map((entry, idx) => {
|
|
53
|
+
const entryMax = displayDataAsText(entry.max, 'primary')
|
|
54
|
+
|
|
55
|
+
const entryMin = displayDataAsText(entry.min, 'primary')
|
|
56
|
+
|
|
57
|
+
let formattedText = `${entryMin}${entryMax !== entryMin ? ` - ${entryMax}` : ''}`
|
|
58
|
+
|
|
59
|
+
// If interval, add some formatting
|
|
60
|
+
if (legend.type === 'equalinterval' && idx !== runtimeLegend.length - 1) {
|
|
61
|
+
formattedText = `${entryMin} - < ${entryMax}`
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const { disabled } = entry
|
|
65
|
+
|
|
66
|
+
if (legend.type === 'category') {
|
|
67
|
+
formattedText = displayDataAsText(entry.value, 'primary')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (entry.max === 0 && entry.min === 0) {
|
|
71
|
+
formattedText = '0'
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let legendLabel = formattedText
|
|
75
|
+
|
|
76
|
+
if (entry.hasOwnProperty('special')) {
|
|
77
|
+
legendLabel = entry.label || entry.value
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const handleListItemClass = () => {
|
|
81
|
+
let classes = ['legend-container__li']
|
|
82
|
+
if (disabled) classes.push('legend-container__li--disabled')
|
|
83
|
+
if (entry.hasOwnProperty('special')) classes.push('legend-container__li--special-class')
|
|
84
|
+
return classes
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
// eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-noninteractive-element-interactions
|
|
89
|
+
<li
|
|
90
|
+
className={handleListItemClass().join(' ')}
|
|
91
|
+
key={idx}
|
|
92
|
+
title={`Legend item ${legendLabel} - Click to disable`}
|
|
93
|
+
onClick={() => {
|
|
94
|
+
toggleLegendActive(idx, legendLabel)
|
|
95
|
+
}}
|
|
96
|
+
>
|
|
97
|
+
<LegendCircle fill={entry.color} /> <span className='label'>{legendLabel}</span>
|
|
98
|
+
</li>
|
|
99
|
+
)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
if (state.map.patterns) {
|
|
103
|
+
// loop over map patterns
|
|
104
|
+
state.map.patterns.map((patternData, patternDataIndex) => {
|
|
105
|
+
const { pattern, dataKey, size } = patternData
|
|
106
|
+
let defaultPatternColor = 'black'
|
|
107
|
+
const sizes = {
|
|
108
|
+
small: '8',
|
|
109
|
+
medium: '10',
|
|
110
|
+
large: '12'
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const legendSize = 16
|
|
114
|
+
|
|
115
|
+
legendItems.push(
|
|
116
|
+
<>
|
|
117
|
+
<li className={`legend-container__li legend-container__li--geo-pattern`}>
|
|
118
|
+
<span className='legend-item' style={{ border: 'unset' }}>
|
|
119
|
+
<svg width={legendSize} height={legendSize}>
|
|
120
|
+
{pattern === 'waves' && <PatternWaves id={`${dataKey}--${patternDataIndex}`} height={sizes[size] ?? 10} width={sizes[size] ?? 10} fill={defaultPatternColor} />}
|
|
121
|
+
{pattern === 'circles' && <PatternCircles id={`${dataKey}--${patternDataIndex}`} height={sizes[size] ?? 10} width={sizes[size] ?? 10} fill={defaultPatternColor} />}
|
|
122
|
+
{pattern === 'lines' && <PatternLines id={`${dataKey}--${patternDataIndex}`} height={sizes[size] ?? 6} width={sizes[size] ?? 10} stroke={defaultPatternColor} strokeWidth={2} orientation={['diagonalRightToLeft']} />}
|
|
123
|
+
<circle id={dataKey} fill={`url(#${dataKey}--${patternDataIndex})`} r={legendSize / 2} cx={legendSize / 2} cy={legendSize / 2} stroke='#0000004d' strokeWidth={1} />
|
|
124
|
+
</svg>
|
|
125
|
+
</span>
|
|
126
|
+
<p style={{ lineHeight: '22.4px' }}>{patternData.label || patternData.dataValue || ''}</p>
|
|
127
|
+
</li>
|
|
128
|
+
</>
|
|
129
|
+
)
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return legendItems
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const { legendClasses } = useDataVizClasses(state, viewport)
|
|
137
|
+
|
|
138
|
+
const handleReset = e => {
|
|
139
|
+
e.preventDefault()
|
|
140
|
+
resetLegendToggles()
|
|
141
|
+
setAccessibleStatus('Legend has been reset, please reference the data table to see updated values.')
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<ErrorBoundary component='Sidebar'>
|
|
146
|
+
<div className='legends'>
|
|
147
|
+
<aside id='legend' className={legendClasses.aside.join(' ') || ''} role='region' aria-label='Legend'>
|
|
148
|
+
<section className={legendClasses.section.join(' ') || ''} aria-label='Map Legend'>
|
|
149
|
+
{runtimeLegend.disabledAmt > 0 && (
|
|
150
|
+
<button onClick={handleReset} className={legendClasses.resetButton.join(' ') || ''}>
|
|
151
|
+
Clear
|
|
152
|
+
</button>
|
|
153
|
+
)}
|
|
154
|
+
{legend.title && <span className={legendClasses.title.join(' ') || ''}>{parse(legend.title)}</span>}
|
|
155
|
+
{legend.dynamicDescription === false && legend.description && <p className={legendClasses.description.join(' ') || ''}>{parse(legend.description)}</p>}
|
|
156
|
+
{legend.dynamicDescription === true &&
|
|
157
|
+
runtimeFilters.map((filter, idx) => {
|
|
158
|
+
const lookupStr = `${idx},${filter.values.indexOf(String(filter.active))}`
|
|
159
|
+
|
|
160
|
+
// Do we have a custom description for this?
|
|
161
|
+
const desc = legend.descriptions[lookupStr] || ''
|
|
162
|
+
|
|
163
|
+
if (desc.length > 0) {
|
|
164
|
+
return (
|
|
165
|
+
<p key={`dynamic-description-${lookupStr}`} className={`dynamic-legend-description-${lookupStr}`}>
|
|
166
|
+
{desc}
|
|
167
|
+
</p>
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
return true
|
|
171
|
+
})}
|
|
172
|
+
<ul className={legendClasses.ul.join(' ') || ''} aria-label='Legend items'>
|
|
173
|
+
{legendList()}
|
|
174
|
+
</ul>
|
|
175
|
+
</section>
|
|
176
|
+
</aside>
|
|
177
|
+
{state.hexMap.shapeGroups?.length > 0 && state.hexMap.type === 'shapes' && state.general.displayAsHex && <LegendItemHex state={state} runtimeLegend={runtimeLegend} viewport={viewport} />}
|
|
178
|
+
</div>
|
|
179
|
+
</ErrorBoundary>
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export default Legend
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import useDataVizClasses from '@cdc/core/helpers/useDataVizClasses'
|
|
2
|
+
import { AiOutlineArrowUp, AiOutlineArrowDown, AiOutlineArrowRight } from 'react-icons/ai'
|
|
3
|
+
import parse from 'html-react-parser'
|
|
4
|
+
|
|
5
|
+
const LegendItemHex = props => {
|
|
6
|
+
const { state, viewport } = props
|
|
7
|
+
|
|
8
|
+
const getItemShape = shape => {
|
|
9
|
+
switch (shape) {
|
|
10
|
+
case 'Arrow Down':
|
|
11
|
+
return <AiOutlineArrowDown />
|
|
12
|
+
case 'Arrow Up':
|
|
13
|
+
return <AiOutlineArrowUp />
|
|
14
|
+
case 'Arrow Right':
|
|
15
|
+
return <AiOutlineArrowRight />
|
|
16
|
+
default:
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { legendClasses } = useDataVizClasses(state, viewport)
|
|
22
|
+
|
|
23
|
+
// TODO: create core legend for reusability
|
|
24
|
+
return (
|
|
25
|
+
state.hexMap.type === 'shapes' &&
|
|
26
|
+
state.hexMap.shapeGroups.map((shapeGroup, shapeGroupIndex) => {
|
|
27
|
+
return (
|
|
28
|
+
<aside id='legend' className={legendClasses.aside.join(' ')} role='region' aria-label='Legend' tabIndex={0}>
|
|
29
|
+
<section className={legendClasses.section.join(' ')} aria-label='Map Legend'>
|
|
30
|
+
{shapeGroup.legendTitle && <span className={legendClasses.title.join(' ')}>{parse(shapeGroup.legendTitle)}</span>}
|
|
31
|
+
{shapeGroup.legendDescription && <p className={legendClasses.description.join(' ')}>{parse(shapeGroup.legendDescription)}</p>}
|
|
32
|
+
|
|
33
|
+
<ul className={legendClasses.ul.join(' ')} aria-label='Legend items' style={{ listStyle: 'none' }}>
|
|
34
|
+
{shapeGroup.items.map((item, itemIndex) => {
|
|
35
|
+
return (
|
|
36
|
+
<li className={legendClasses.li.join(' ')} key={`hex-legend-item-${itemIndex}`}>
|
|
37
|
+
{getItemShape(item.shape)} {item.value}
|
|
38
|
+
</li>
|
|
39
|
+
)
|
|
40
|
+
})}
|
|
41
|
+
</ul>
|
|
42
|
+
</section>
|
|
43
|
+
</aside>
|
|
44
|
+
)
|
|
45
|
+
})
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export default LegendItemHex
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
@import '@cdc/core/styles/base';
|
|
2
|
+
@import '@cdc/core/styles/heading-colors';
|
|
3
|
+
|
|
4
|
+
.cdc-map-inner-container {
|
|
5
|
+
.map-container.world aside.side {
|
|
6
|
+
border-top: 0;
|
|
7
|
+
}
|
|
8
|
+
@include breakpointClass(md) {
|
|
9
|
+
.map-container.world aside.side {
|
|
10
|
+
border-top: $lightGray 1px solid;
|
|
11
|
+
position: absolute;
|
|
12
|
+
box-shadow: rgba(0, 0, 0, 0.2) 0 10px 18px;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
aside {
|
|
17
|
+
background-color: #fff;
|
|
18
|
+
z-index: 6;
|
|
19
|
+
border-top: $lightGray 1px solid;
|
|
20
|
+
@include breakpointClass(md) {
|
|
21
|
+
&.bottom {
|
|
22
|
+
border: $lightGray 1px solid;
|
|
23
|
+
}
|
|
24
|
+
&.side {
|
|
25
|
+
z-index: 1;
|
|
26
|
+
box-sizing: content-box;
|
|
27
|
+
max-width: 450px;
|
|
28
|
+
margin-top: 2em;
|
|
29
|
+
margin-bottom: 2em;
|
|
30
|
+
align-self: flex-start;
|
|
31
|
+
z-index: 4;
|
|
32
|
+
right: 1em;
|
|
33
|
+
border: $lightGray 1px solid;
|
|
34
|
+
top: 2em;
|
|
35
|
+
right: 1em;
|
|
36
|
+
|
|
37
|
+
ul.vertical-sorted {
|
|
38
|
+
column-count: 2;
|
|
39
|
+
column-fill: balance;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
ul:not(.vertical-sorted) {
|
|
43
|
+
column-count: initial;
|
|
44
|
+
column-fill: initial;
|
|
45
|
+
display: flex;
|
|
46
|
+
flex-direction: row;
|
|
47
|
+
flex-wrap: wrap;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
&.bottom {
|
|
52
|
+
ul.legend-container__ul.vertical-sorted {
|
|
53
|
+
display: block;
|
|
54
|
+
column-count: 2;
|
|
55
|
+
column-fill: balance;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
ul.legend-container__ul {
|
|
59
|
+
display: flex;
|
|
60
|
+
flex-direction: row;
|
|
61
|
+
flex-wrap: wrap;
|
|
62
|
+
|
|
63
|
+
li {
|
|
64
|
+
width: 50%;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
ul.single-row {
|
|
69
|
+
display: block;
|
|
70
|
+
column-count: initial;
|
|
71
|
+
column-fill: auto;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.legend-container {
|
|
77
|
+
padding: 1em;
|
|
78
|
+
position: relative;
|
|
79
|
+
.legend-container__title {
|
|
80
|
+
font-size: 1.3em;
|
|
81
|
+
padding-bottom: 0;
|
|
82
|
+
display: inline-block;
|
|
83
|
+
}
|
|
84
|
+
.legend-container__title + p,
|
|
85
|
+
.legend-container__title + ul,
|
|
86
|
+
p + ul,
|
|
87
|
+
p + p {
|
|
88
|
+
padding-top: 1em;
|
|
89
|
+
}
|
|
90
|
+
.legend-container__reset-button {
|
|
91
|
+
font-size: 0.75em;
|
|
92
|
+
color: rgba(0, 0, 0, 0.6);
|
|
93
|
+
position: absolute;
|
|
94
|
+
right: 1em;
|
|
95
|
+
background: rgba(0, 0, 0, 0.1);
|
|
96
|
+
text-transform: uppercase;
|
|
97
|
+
transition: 0.2s all;
|
|
98
|
+
padding: 0.2em 0.5em;
|
|
99
|
+
border: rgba(0, 0, 0, 0.2) 1px solid;
|
|
100
|
+
&:hover {
|
|
101
|
+
text-decoration: none;
|
|
102
|
+
background: rgba(0, 0, 0, 0.15);
|
|
103
|
+
transition: 0.2s all;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
p {
|
|
107
|
+
line-height: 1.4em;
|
|
108
|
+
}
|
|
109
|
+
.legend-container__ul {
|
|
110
|
+
list-style: none;
|
|
111
|
+
padding-top: 1em;
|
|
112
|
+
button {
|
|
113
|
+
font-size: unset;
|
|
114
|
+
background: transparent;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
&.vertical-sorted {
|
|
118
|
+
flex-direction: column;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
&:not(.vertical-sorted, .legend-container__ul--single-column) {
|
|
122
|
+
width: 100%;
|
|
123
|
+
@include breakpoint(sm) {
|
|
124
|
+
.legend-container__li {
|
|
125
|
+
width: 50%;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
.legend-container__li {
|
|
130
|
+
flex-shrink: 0;
|
|
131
|
+
display: inline-block;
|
|
132
|
+
padding-right: 1em;
|
|
133
|
+
padding-bottom: 1em;
|
|
134
|
+
vertical-align: middle;
|
|
135
|
+
transition: 0.1s opacity;
|
|
136
|
+
display: flex;
|
|
137
|
+
cursor: pointer;
|
|
138
|
+
flex-grow: 1;
|
|
139
|
+
|
|
140
|
+
&.legend-container__li--disabled {
|
|
141
|
+
opacity: 0.4;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.bottom .legend-container__ul--single-column:not(.vertical-sorted) {
|
|
148
|
+
display: inline-block;
|
|
149
|
+
|
|
150
|
+
@include breakpoint(md) {
|
|
151
|
+
display: flex;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.legend-container__li {
|
|
155
|
+
width: 100%;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
&.side .legend-container .legend-container__ul--single-column {
|
|
160
|
+
@include breakpointClass(md) {
|
|
161
|
+
width: 25%;
|
|
162
|
+
min-width: 200px;
|
|
163
|
+
.legend-section ul {
|
|
164
|
+
flex-direction: column;
|
|
165
|
+
li {
|
|
166
|
+
width: 100%;
|
|
167
|
+
&:nth-last-of-type(-n + 2) {
|
|
168
|
+
padding-bottom: 1em;
|
|
169
|
+
}
|
|
170
|
+
&:last-child {
|
|
171
|
+
padding-bottom: 0;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
li {
|
|
177
|
+
width: 100%;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
&.bottom.single-row {
|
|
182
|
+
width: 100%;
|
|
183
|
+
.legend-container ul {
|
|
184
|
+
flex-direction: row;
|
|
185
|
+
align-items: baseline;
|
|
186
|
+
justify-content: flex-start;
|
|
187
|
+
flex-wrap: wrap;
|
|
188
|
+
li {
|
|
189
|
+
justify-items: center;
|
|
190
|
+
line-break: loose;
|
|
191
|
+
align-items: center;
|
|
192
|
+
width: auto;
|
|
193
|
+
padding-right: 1em;
|
|
194
|
+
padding-bottom: 1em;
|
|
195
|
+
display: inline-block;
|
|
196
|
+
& > span {
|
|
197
|
+
margin: 0 !important;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
@include breakpointClass(sm) {
|
|
204
|
+
.legend-container ul {
|
|
205
|
+
align-items: flex-start;
|
|
206
|
+
justify-content: space-between;
|
|
207
|
+
li {
|
|
208
|
+
flex-grow: 1;
|
|
209
|
+
padding-right: 0.5em;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.filters-section {
|
|
215
|
+
padding: 0 1em 1em;
|
|
216
|
+
.heading-3 {
|
|
217
|
+
font-weight: bold;
|
|
218
|
+
margin-bottom: 0.5em;
|
|
219
|
+
}
|
|
220
|
+
form {
|
|
221
|
+
margin-top: 0.5em;
|
|
222
|
+
line-height: 2em;
|
|
223
|
+
display: flex;
|
|
224
|
+
align-items: flex-end;
|
|
225
|
+
section + section {
|
|
226
|
+
margin-left: 0.75em;
|
|
227
|
+
}
|
|
228
|
+
select {
|
|
229
|
+
display: block;
|
|
230
|
+
font-size: 1em;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|