@cdc/map 4.24.2 → 4.24.3
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 +27001 -26354
- package/examples/508.json +548 -0
- package/examples/default-county.json +0 -28
- package/examples/default-hex.json +110 -13
- package/examples/default-usa.json +69 -28
- package/examples/usa-special-class-legend.json +501 -0
- package/index.html +4 -3
- package/package.json +3 -3
- package/src/CdcMap.tsx +60 -21
- package/src/components/BubbleList.jsx +9 -1
- package/src/components/CityList.jsx +60 -8
- package/src/components/DataTable.jsx +7 -7
- package/src/components/EditorPanel/components/EditorPanel.tsx +180 -44
- package/src/components/EditorPanel/components/HexShapeSettings.tsx +16 -1
- package/src/components/Legend/components/Legend.tsx +67 -13
- package/src/components/Legend/components/index.scss +31 -5
- package/src/components/UsaMap/components/HexIcon.tsx +41 -0
- package/src/components/UsaMap/components/Territory/Territory.Hexagon.tsx +38 -19
- package/src/components/UsaMap/components/Territory/Territory.Rectangle.tsx +10 -21
- package/src/components/UsaMap/components/UsaMap.Region.tsx +11 -37
- package/src/components/UsaMap/components/UsaMap.State.tsx +61 -59
- package/src/components/UsaMap/helpers/patternSizes.tsx +5 -0
- package/src/components/WorldMap/components/WorldMap.jsx +3 -1
- package/src/data/initial-state.js +3 -0
- package/src/data/supported-geos.js +1 -1
- package/src/scss/editor-panel.scss +3 -0
- package/src/scss/main.scss +2 -1
- package/src/types/MapConfig.ts +7 -0
|
@@ -117,6 +117,75 @@ const EditorPanel = props => {
|
|
|
117
117
|
specialClasses = legend.specialClasses || []
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
const getCityStyleOptions = target => {
|
|
121
|
+
switch (target) {
|
|
122
|
+
case 'value': {
|
|
123
|
+
const values = ['Circle', 'Square', 'Triangle', 'Diamond', 'Star', 'Pin']
|
|
124
|
+
const filteredValues = values.filter(val => String(state.visual.cityStyle).toLocaleLowerCase() !== val.toLocaleLowerCase())
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<>
|
|
128
|
+
<option value='' key={'Select Option'}>
|
|
129
|
+
- Select Option -
|
|
130
|
+
</option>
|
|
131
|
+
{filteredValues.map((val, i) => {
|
|
132
|
+
return (
|
|
133
|
+
<option key={i} value={val}>
|
|
134
|
+
{val}
|
|
135
|
+
</option>
|
|
136
|
+
)
|
|
137
|
+
})}
|
|
138
|
+
</>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const editCityStyles = (target, index, fieldName, value) => {
|
|
145
|
+
switch (target) {
|
|
146
|
+
case 'add': {
|
|
147
|
+
const additionalCityStyles = state.visual.additionalCityStyles ? [...state.visual.additionalCityStyles] : []
|
|
148
|
+
additionalCityStyles.push({ label: '', column: '', value: '', shape: '' })
|
|
149
|
+
setState({
|
|
150
|
+
...state,
|
|
151
|
+
visual: {
|
|
152
|
+
...state.visual,
|
|
153
|
+
additionalCityStyles: additionalCityStyles
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
break
|
|
157
|
+
}
|
|
158
|
+
case 'remove': {
|
|
159
|
+
let additionalCityStyles = []
|
|
160
|
+
if (state.visual.additionalCityStyles) {
|
|
161
|
+
additionalCityStyles = [...state.visual.additionalCityStyles]
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
additionalCityStyles.splice(index, 1)
|
|
165
|
+
setState({
|
|
166
|
+
...state,
|
|
167
|
+
visual: {
|
|
168
|
+
...state.visual,
|
|
169
|
+
additionalCityStyles: additionalCityStyles
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
break
|
|
173
|
+
}
|
|
174
|
+
case 'update': {
|
|
175
|
+
let additionalCityStyles = []
|
|
176
|
+
additionalCityStyles = [...state.visual.additionalCityStyles]
|
|
177
|
+
additionalCityStyles[index][fieldName] = value
|
|
178
|
+
setState({
|
|
179
|
+
...state,
|
|
180
|
+
visual: {
|
|
181
|
+
...state.visual,
|
|
182
|
+
additionalCityStyles: additionalCityStyles
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
120
189
|
const DynamicDesc = ({ label, fieldName, value: stateValue, type = 'input', ...attributes }) => {
|
|
121
190
|
const [value, setValue] = useState(stateValue)
|
|
122
191
|
|
|
@@ -379,20 +448,6 @@ const EditorPanel = props => {
|
|
|
379
448
|
}
|
|
380
449
|
})
|
|
381
450
|
break
|
|
382
|
-
case 'toggleDownloadButton':
|
|
383
|
-
setState({
|
|
384
|
-
...state,
|
|
385
|
-
general: {
|
|
386
|
-
...state.general,
|
|
387
|
-
showDownloadButton: !state.general.showDownloadButton
|
|
388
|
-
},
|
|
389
|
-
table: {
|
|
390
|
-
// setting both bc DataTable new core needs it here
|
|
391
|
-
...state.table,
|
|
392
|
-
download: !state.general.showDownloadButton
|
|
393
|
-
}
|
|
394
|
-
})
|
|
395
|
-
break
|
|
396
451
|
case 'toggleShowFullGeoNameInCSV':
|
|
397
452
|
setState({
|
|
398
453
|
...state,
|
|
@@ -1289,6 +1344,8 @@ const EditorPanel = props => {
|
|
|
1289
1344
|
</select>
|
|
1290
1345
|
</label>
|
|
1291
1346
|
|
|
1347
|
+
<TextField value={state.filters[index].setByQueryParameter} section='filters' subsection={index} fieldName='setByQueryParameter' label='Default Value Set By Query String Parameter' updateField={updateField} />
|
|
1348
|
+
|
|
1292
1349
|
{filter.order === 'cust' && (
|
|
1293
1350
|
<DragDropContext onDragEnd={({ source, destination }) => handleFilterOrder(source.index, destination.index, index, state.filters[index])}>
|
|
1294
1351
|
<Droppable droppableId='filter_order'>
|
|
@@ -1928,7 +1985,7 @@ const EditorPanel = props => {
|
|
|
1928
1985
|
</label>
|
|
1929
1986
|
</fieldset>
|
|
1930
1987
|
)}
|
|
1931
|
-
{
|
|
1988
|
+
{
|
|
1932
1989
|
<>
|
|
1933
1990
|
<label>Latitude Column</label>
|
|
1934
1991
|
<select
|
|
@@ -1949,7 +2006,7 @@ const EditorPanel = props => {
|
|
|
1949
2006
|
{columnsOptions}
|
|
1950
2007
|
</select>
|
|
1951
2008
|
</>
|
|
1952
|
-
|
|
2009
|
+
}
|
|
1953
2010
|
|
|
1954
2011
|
{'navigation' !== state.general.type && (
|
|
1955
2012
|
<fieldset className='primary-fieldset edit-block'>
|
|
@@ -2634,16 +2691,6 @@ const EditorPanel = props => {
|
|
|
2634
2691
|
<span className='edit-label'>Show URL to Automatically Updated Data</span>
|
|
2635
2692
|
</label>
|
|
2636
2693
|
)}
|
|
2637
|
-
<label className='checkbox'>
|
|
2638
|
-
<input
|
|
2639
|
-
type='checkbox'
|
|
2640
|
-
checked={state.general.showDownloadButton}
|
|
2641
|
-
onChange={event => {
|
|
2642
|
-
handleEditorChanges('toggleDownloadButton', event.target.checked)
|
|
2643
|
-
}}
|
|
2644
|
-
/>
|
|
2645
|
-
<span className='edit-label'>Show Download CSV Link</span>
|
|
2646
|
-
</label>
|
|
2647
2694
|
<label className='checkbox'>
|
|
2648
2695
|
<input
|
|
2649
2696
|
type='checkbox'
|
|
@@ -2849,12 +2896,10 @@ const EditorPanel = props => {
|
|
|
2849
2896
|
)
|
|
2850
2897
|
})}
|
|
2851
2898
|
</ul>
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
</label>
|
|
2857
|
-
)}
|
|
2899
|
+
<label>
|
|
2900
|
+
Geocode Settings
|
|
2901
|
+
<TextField type='number' value={state.visual.geoCodeCircleSize} section='visual' max='10' fieldName='geoCodeCircleSize' label='Geocode Circle Size' updateField={updateField} />
|
|
2902
|
+
</label>
|
|
2858
2903
|
|
|
2859
2904
|
{state.general.type === 'bubble' && (
|
|
2860
2905
|
<>
|
|
@@ -2899,19 +2944,109 @@ const EditorPanel = props => {
|
|
|
2899
2944
|
</label>
|
|
2900
2945
|
)}
|
|
2901
2946
|
{(state.general.geoType === 'us' || state.general.geoType === 'us-county' || state.general.geoType === 'world') && (
|
|
2902
|
-
|
|
2903
|
-
<
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2947
|
+
<>
|
|
2948
|
+
<label>
|
|
2949
|
+
<span className='edit-label'>Default City Style</span>
|
|
2950
|
+
<select
|
|
2951
|
+
value={state.visual.cityStyle || false}
|
|
2952
|
+
onChange={event => {
|
|
2953
|
+
handleEditorChanges('handleCityStyle', event.target.value)
|
|
2954
|
+
}}
|
|
2955
|
+
>
|
|
2956
|
+
<option value='circle'>Circle</option>
|
|
2957
|
+
<option value='pin'>Pin</option>
|
|
2958
|
+
<option value='square'>Square</option>
|
|
2959
|
+
<option value='triangle'>Triangle</option>
|
|
2960
|
+
<option value='diamond'>Diamond</option>
|
|
2961
|
+
<option value='star'>Star</option>
|
|
2962
|
+
</select>
|
|
2963
|
+
</label>
|
|
2964
|
+
<TextField
|
|
2965
|
+
value={state.visual.cityStyleLabel}
|
|
2966
|
+
section='visual'
|
|
2967
|
+
fieldName='cityStyleLabel'
|
|
2968
|
+
label='Label (Optional) '
|
|
2969
|
+
updateField={updateField}
|
|
2970
|
+
tooltip={
|
|
2971
|
+
<Tooltip style={{ textTransform: 'none' }}>
|
|
2972
|
+
<Tooltip.Target>
|
|
2973
|
+
<Icon display='question' style={{ marginLeft: '0.5rem' }} />
|
|
2974
|
+
</Tooltip.Target>
|
|
2975
|
+
<Tooltip.Content>
|
|
2976
|
+
<p>When a label is provided, the default city style will appear in the legend.</p>
|
|
2977
|
+
</Tooltip.Content>
|
|
2978
|
+
</Tooltip>
|
|
2979
|
+
}
|
|
2980
|
+
/>
|
|
2981
|
+
</>
|
|
2914
2982
|
)}
|
|
2983
|
+
{/* <AdditionalCityStyles /> */}
|
|
2984
|
+
<>
|
|
2985
|
+
{state.visual.additionalCityStyles.length > 0 &&
|
|
2986
|
+
state.visual.additionalCityStyles.map(({ label, column, value, shape }, i) => {
|
|
2987
|
+
return (
|
|
2988
|
+
<div className='edit-block' key={`additional-city-style-${i}`}>
|
|
2989
|
+
<button
|
|
2990
|
+
className='remove-column'
|
|
2991
|
+
onClick={e => {
|
|
2992
|
+
e.preventDefault()
|
|
2993
|
+
editCityStyles('remove', i, '', '')
|
|
2994
|
+
}}
|
|
2995
|
+
>
|
|
2996
|
+
Remove
|
|
2997
|
+
</button>
|
|
2998
|
+
<p>City Style {i + 1}</p>
|
|
2999
|
+
<label>
|
|
3000
|
+
<span className='edit-label column-heading'>Column with configuration value</span>
|
|
3001
|
+
<select
|
|
3002
|
+
value={column}
|
|
3003
|
+
onChange={e => {
|
|
3004
|
+
editCityStyles('update', i, 'column', e.target.value)
|
|
3005
|
+
}}
|
|
3006
|
+
>
|
|
3007
|
+
{columnsOptions}
|
|
3008
|
+
</select>
|
|
3009
|
+
</label>
|
|
3010
|
+
<label>
|
|
3011
|
+
<span className='edit-label column-heading'>Value to Trigger</span>
|
|
3012
|
+
<input
|
|
3013
|
+
type='text'
|
|
3014
|
+
value={value}
|
|
3015
|
+
onChange={e => {
|
|
3016
|
+
editCityStyles('update', i, 'value', e.target.value)
|
|
3017
|
+
}}
|
|
3018
|
+
></input>
|
|
3019
|
+
</label>
|
|
3020
|
+
<label>
|
|
3021
|
+
<span className='edit-label column-heading'>Shape</span>
|
|
3022
|
+
<select
|
|
3023
|
+
value={shape}
|
|
3024
|
+
onChange={e => {
|
|
3025
|
+
editCityStyles('update', i, 'shape', e.target.value)
|
|
3026
|
+
}}
|
|
3027
|
+
>
|
|
3028
|
+
{getCityStyleOptions('value')}
|
|
3029
|
+
</select>
|
|
3030
|
+
</label>
|
|
3031
|
+
<label>
|
|
3032
|
+
<span className='edit-label column-heading'>Label</span>
|
|
3033
|
+
<input
|
|
3034
|
+
key={i}
|
|
3035
|
+
type='text'
|
|
3036
|
+
value={label}
|
|
3037
|
+
onChange={e => {
|
|
3038
|
+
editCityStyles('update', i, 'label', e.target.value)
|
|
3039
|
+
}}
|
|
3040
|
+
/>
|
|
3041
|
+
</label>
|
|
3042
|
+
</div>
|
|
3043
|
+
)
|
|
3044
|
+
})}
|
|
3045
|
+
|
|
3046
|
+
<button type='button' onClick={() => editCityStyles('add', 0, '', '')} className='btn full-width'>
|
|
3047
|
+
Add city style
|
|
3048
|
+
</button>
|
|
3049
|
+
</>
|
|
2915
3050
|
<label htmlFor='opacity'>
|
|
2916
3051
|
<TextField type='number' min={0} max={100} value={state.tooltips.opacity ? state.tooltips.opacity : 100} section='tooltips' fieldName='opacity' label='Tooltip Opacity (%)' updateField={updateField} />
|
|
2917
3052
|
</label>
|
|
@@ -2961,6 +3096,7 @@ const EditorPanel = props => {
|
|
|
2961
3096
|
<button className={'btn full-width'} onClick={handleAddLayer}>
|
|
2962
3097
|
Add Map Layer
|
|
2963
3098
|
</button>
|
|
3099
|
+
<p className='layer-purpose-details'>Context should be added to your visualization or associated page to describe the significance of layers that are added to maps.</p>
|
|
2964
3100
|
</AccordionItemPanel>
|
|
2965
3101
|
</AccordionItem>
|
|
2966
3102
|
{state.general.geoType === 'us' && <Panels.PatternSettings name='Pattern Settings' />}
|
|
@@ -4,7 +4,7 @@ import parse from 'html-react-parser'
|
|
|
4
4
|
import { AiOutlineArrowUp, AiOutlineArrowDown, AiOutlineArrowRight } from 'react-icons/ai'
|
|
5
5
|
import useDataVizClasses from '@cdc/core/helpers/useDataVizClasses'
|
|
6
6
|
|
|
7
|
-
const shapeOptions = ['Arrow Up', 'Arrow Down', 'Arrow Right', 'None']
|
|
7
|
+
const shapeOptions = ['Arrow Up', 'Arrow Down', 'Arrow Right', 'Arrow Left', 'None']
|
|
8
8
|
|
|
9
9
|
// todo: Move duplicated operators to CORE
|
|
10
10
|
export const DATA_OPERATOR_LESS = '<'
|
|
@@ -221,6 +221,21 @@ const HexSettingShapeColumns = props => {
|
|
|
221
221
|
{[DATA_OPERATOR_EQUAL].map(option => {
|
|
222
222
|
return <option value={option}>{option}</option>
|
|
223
223
|
})}
|
|
224
|
+
{[DATA_OPERATOR_NOTEQUAL].map(option => {
|
|
225
|
+
return <option value={option}>{option}</option>
|
|
226
|
+
})}
|
|
227
|
+
{[DATA_OPERATOR_LESS].map(option => {
|
|
228
|
+
return <option value={option}>{option}</option>
|
|
229
|
+
})}
|
|
230
|
+
{[DATA_OPERATOR_GREATER].map(option => {
|
|
231
|
+
return <option value={option}>{option}</option>
|
|
232
|
+
})}
|
|
233
|
+
{[DATA_OPERATOR_LESSEQUAL].map(option => {
|
|
234
|
+
return <option value={option}>{option}</option>
|
|
235
|
+
})}
|
|
236
|
+
{[DATA_OPERATOR_GREATEREQUAL].map(option => {
|
|
237
|
+
return <option value={option}>{option}</option>
|
|
238
|
+
})}
|
|
224
239
|
</select>
|
|
225
240
|
</div>
|
|
226
241
|
<div className='cove-accordion__panel-col cove-input'>
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
//TODO: Move legends to core
|
|
2
|
-
import { useContext } from 'react'
|
|
2
|
+
import { forwardRef, useContext } from 'react'
|
|
3
3
|
import parse from 'html-react-parser'
|
|
4
|
-
import chroma from 'chroma-js'
|
|
5
4
|
|
|
6
5
|
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
7
6
|
import LegendCircle from '@cdc/core/components/LegendCircle'
|
|
8
7
|
import LegendItemHex from './LegendItem.Hex'
|
|
8
|
+
import Button from '@cdc/core/components/elements/Button'
|
|
9
9
|
|
|
10
10
|
import useDataVizClasses from '@cdc/core/helpers/useDataVizClasses'
|
|
11
11
|
import ConfigContext from '../../../context'
|
|
12
12
|
import { PatternLines, PatternCircles, PatternWaves } from '@visx/pattern'
|
|
13
|
+
import { GlyphStar, GlyphTriangle, GlyphDiamond, GlyphSquare, GlyphCircle } from '@visx/glyph'
|
|
14
|
+
import { Group } from '@visx/group'
|
|
13
15
|
import './index.scss'
|
|
14
16
|
|
|
15
|
-
const Legend = () => {
|
|
17
|
+
const Legend = forwardRef((props, ref) => {
|
|
16
18
|
// prettier-ignore
|
|
17
19
|
const {
|
|
18
20
|
displayDataAsText,
|
|
@@ -93,8 +95,16 @@ const Legend = () => {
|
|
|
93
95
|
onClick={() => {
|
|
94
96
|
toggleLegendActive(idx, legendLabel)
|
|
95
97
|
}}
|
|
98
|
+
onKeyDown={e => {
|
|
99
|
+
if (e.key === 'Enter') {
|
|
100
|
+
e.preventDefault()
|
|
101
|
+
toggleLegendActive(idx, legendLabel)
|
|
102
|
+
}
|
|
103
|
+
}}
|
|
104
|
+
role='button'
|
|
105
|
+
tabIndex={0}
|
|
96
106
|
>
|
|
97
|
-
<LegendCircle fill={entry.color} /> <span
|
|
107
|
+
<LegendCircle fill={entry.color} /> <span>{legendLabel}</span>
|
|
98
108
|
</li>
|
|
99
109
|
)
|
|
100
110
|
})
|
|
@@ -114,7 +124,7 @@ const Legend = () => {
|
|
|
114
124
|
|
|
115
125
|
legendItems.push(
|
|
116
126
|
<>
|
|
117
|
-
<li className={`legend-container__li legend-container__li--geo-pattern`}>
|
|
127
|
+
<li className={`legend-container__li legend-container__li--geo-pattern`} aria-label='You are on a pattern button. We dont support toggling patterns on this legend at the moment, but provide the area as being focusable for congruity.' tabIndex={0}>
|
|
118
128
|
<span className='legend-item' style={{ border: 'unset' }}>
|
|
119
129
|
<svg width={legendSize} height={legendSize}>
|
|
120
130
|
{pattern === 'waves' && <PatternWaves id={`${dataKey}--${patternDataIndex}`} height={sizes[size] ?? 10} width={sizes[size] ?? 10} fill={defaultPatternColor} />}
|
|
@@ -136,21 +146,33 @@ const Legend = () => {
|
|
|
136
146
|
const { legendClasses } = useDataVizClasses(state, viewport)
|
|
137
147
|
|
|
138
148
|
const handleReset = e => {
|
|
139
|
-
|
|
149
|
+
const legend = ref.current
|
|
150
|
+
if (e) {
|
|
151
|
+
e.preventDefault()
|
|
152
|
+
}
|
|
140
153
|
resetLegendToggles()
|
|
141
154
|
setAccessibleStatus('Legend has been reset, please reference the data table to see updated values.')
|
|
155
|
+
if (legend) {
|
|
156
|
+
legend.focus()
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const pin = <path className='marker' d='M0,0l-8.8-17.7C-12.1-24.3-7.4-32,0-32h0c7.4,0,12.1,7.7,8.8,14.3L0,0z' strokeWidth={2} stroke={'black'} transform={`scale(0.5)`} />
|
|
161
|
+
|
|
162
|
+
const cityStyleShapes = {
|
|
163
|
+
pin: pin,
|
|
164
|
+
circle: <GlyphCircle color='#000' size={150} />,
|
|
165
|
+
square: <GlyphSquare color='#000' size={150} />,
|
|
166
|
+
diamond: <GlyphDiamond color='#000' size={150} />,
|
|
167
|
+
star: <GlyphStar color='#000' size={150} />,
|
|
168
|
+
triangle: <GlyphTriangle color='#000' size={150} />
|
|
142
169
|
}
|
|
143
170
|
|
|
144
171
|
return (
|
|
145
172
|
<ErrorBoundary component='Sidebar'>
|
|
146
173
|
<div className='legends'>
|
|
147
|
-
<aside id='legend' className={legendClasses.aside.join(' ') || ''} role='region' aria-label='Legend'>
|
|
174
|
+
<aside id='legend' className={legendClasses.aside.join(' ') || ''} role='region' aria-label='Legend' tabIndex={0} ref={ref}>
|
|
148
175
|
<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
176
|
{legend.title && <span className={legendClasses.title.join(' ') || ''}>{parse(legend.title)}</span>}
|
|
155
177
|
{legend.dynamicDescription === false && legend.description && <p className={legendClasses.description.join(' ') || ''}>{parse(legend.description)}</p>}
|
|
156
178
|
{legend.dynamicDescription === true &&
|
|
@@ -172,12 +194,44 @@ const Legend = () => {
|
|
|
172
194
|
<ul className={legendClasses.ul.join(' ') || ''} aria-label='Legend items'>
|
|
173
195
|
{legendList()}
|
|
174
196
|
</ul>
|
|
197
|
+
{(state.visual.additionalCityStyles.some(c => c.label) || state.visual.cityStyleLabel) && (
|
|
198
|
+
<>
|
|
199
|
+
<hr />
|
|
200
|
+
<div className={legendClasses.div.join(' ') || ''}>
|
|
201
|
+
{state.visual.cityStyleLabel && (
|
|
202
|
+
<div>
|
|
203
|
+
<svg>
|
|
204
|
+
<Group top={state.visual.cityStyle === 'pin' ? 19 : state.visual.cityStyle === 'triangle' ? 13 : 11} left={10}>
|
|
205
|
+
{cityStyleShapes[state.visual.cityStyle.toLowerCase()]}
|
|
206
|
+
</Group>
|
|
207
|
+
</svg>
|
|
208
|
+
<p>{state.visual.cityStyleLabel}</p>
|
|
209
|
+
</div>
|
|
210
|
+
)}
|
|
211
|
+
|
|
212
|
+
{state.visual.additionalCityStyles.map(
|
|
213
|
+
({ shape, label }) =>
|
|
214
|
+
label && (
|
|
215
|
+
<div>
|
|
216
|
+
<svg>
|
|
217
|
+
<Group top={shape === 'Pin' ? 19 : shape === 'Triangle' ? 13 : 11} left={10}>
|
|
218
|
+
{cityStyleShapes[shape.toLowerCase()]}
|
|
219
|
+
</Group>
|
|
220
|
+
</svg>
|
|
221
|
+
<p>{label}</p>
|
|
222
|
+
</div>
|
|
223
|
+
)
|
|
224
|
+
)}
|
|
225
|
+
</div>
|
|
226
|
+
</>
|
|
227
|
+
)}
|
|
228
|
+
{runtimeLegend.disabledAmt > 0 && <Button onClick={handleReset}>Reset</Button>}
|
|
175
229
|
</section>
|
|
176
230
|
</aside>
|
|
177
231
|
{state.hexMap.shapeGroups?.length > 0 && state.hexMap.type === 'shapes' && state.general.displayAsHex && <LegendItemHex state={state} runtimeLegend={runtimeLegend} viewport={viewport} />}
|
|
178
232
|
</div>
|
|
179
233
|
</ErrorBoundary>
|
|
180
234
|
)
|
|
181
|
-
}
|
|
235
|
+
})
|
|
182
236
|
|
|
183
237
|
export default Legend
|
|
@@ -89,17 +89,14 @@
|
|
|
89
89
|
}
|
|
90
90
|
.legend-container__reset-button {
|
|
91
91
|
font-size: 0.75em;
|
|
92
|
-
color: rgba(0, 0, 0, 0.6);
|
|
93
|
-
position: absolute;
|
|
94
92
|
right: 1em;
|
|
95
|
-
background: rgba(0, 0, 0, 0.1);
|
|
96
93
|
text-transform: uppercase;
|
|
97
94
|
transition: 0.2s all;
|
|
98
95
|
padding: 0.2em 0.5em;
|
|
99
96
|
border: rgba(0, 0, 0, 0.2) 1px solid;
|
|
97
|
+
padding: 0.375rem;
|
|
100
98
|
&:hover {
|
|
101
99
|
text-decoration: none;
|
|
102
|
-
background: rgba(0, 0, 0, 0.15);
|
|
103
100
|
transition: 0.2s all;
|
|
104
101
|
}
|
|
105
102
|
}
|
|
@@ -118,7 +115,7 @@
|
|
|
118
115
|
flex-direction: column;
|
|
119
116
|
}
|
|
120
117
|
|
|
121
|
-
&:not(.vertical-sorted, .legend-container__ul--single-column) {
|
|
118
|
+
&:not(.vertical-sorted, .legend-container__ul--single-column, .single-row) {
|
|
122
119
|
width: 100%;
|
|
123
120
|
@include breakpoint(sm) {
|
|
124
121
|
.legend-container__li {
|
|
@@ -231,5 +228,34 @@
|
|
|
231
228
|
}
|
|
232
229
|
}
|
|
233
230
|
}
|
|
231
|
+
& .shape-container {
|
|
232
|
+
&.single-row {
|
|
233
|
+
display: flex;
|
|
234
|
+
flex-direction: row;
|
|
235
|
+
align-items: center;
|
|
236
|
+
justify-content: flex-start;
|
|
237
|
+
flex-wrap: wrap;
|
|
238
|
+
|
|
239
|
+
& > div {
|
|
240
|
+
margin-right: 2em;
|
|
241
|
+
}
|
|
242
|
+
& > div:last-child {
|
|
243
|
+
margin-right: 0;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
& > div {
|
|
248
|
+
display: flex;
|
|
249
|
+
flex-direction: row;
|
|
250
|
+
}
|
|
251
|
+
& div > svg {
|
|
252
|
+
width: 25px;
|
|
253
|
+
height: 32px;
|
|
254
|
+
}
|
|
255
|
+
& div > p {
|
|
256
|
+
white-space: nowrap;
|
|
257
|
+
margin-top: 1px;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
234
260
|
}
|
|
235
261
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { AiOutlineArrowUp, AiOutlineArrowDown, AiOutlineArrowRight, AiOutlineArrowLeft } from 'react-icons/ai'
|
|
2
|
+
import { Group } from '@visx/group'
|
|
3
|
+
|
|
4
|
+
type HexIconProps = {
|
|
5
|
+
item: {
|
|
6
|
+
operator: '=' | '≠' | '<' | '>' | '<=' | '>='
|
|
7
|
+
shape: 'Arrow Up' | 'Arrow Down' | 'Arrow Right' | 'Arrow Left'
|
|
8
|
+
key: string
|
|
9
|
+
value: string
|
|
10
|
+
}
|
|
11
|
+
index: number
|
|
12
|
+
centroid: [number, number]
|
|
13
|
+
iconSize: number
|
|
14
|
+
textColor: string
|
|
15
|
+
isTerritory: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const HexIcon: React.FC<HexIconProps> = props => {
|
|
19
|
+
const { item, index, centroid, iconSize, textColor, isTerritory } = props
|
|
20
|
+
if (!centroid) return
|
|
21
|
+
|
|
22
|
+
if (isTerritory) {
|
|
23
|
+
return (
|
|
24
|
+
<Group style={{ transform: `translate(36%, 50%)`, fill: 'currentColor' }} key={`territory-hex--${index}`}>
|
|
25
|
+
{item.shape === 'Arrow Down' && <AiOutlineArrowDown size={12} stroke='none' fontWeight={100} />}
|
|
26
|
+
{item.shape === 'Arrow Up' && <AiOutlineArrowUp size={12} stroke='none' fontWeight={100} />}
|
|
27
|
+
{item.shape === 'Arrow Right' && <AiOutlineArrowRight size={12} stroke='none' fontWeight={100} />}
|
|
28
|
+
{item.shape === 'Arrow Left' && <AiOutlineArrowLeft size={12} stroke='none' fontWeight={100} />}
|
|
29
|
+
</Group>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
return (
|
|
33
|
+
<Group top={centroid[1] - 5} left={centroid[0] - iconSize} color={textColor} textAnchor='start' key={`hex--${item.key}-${item.value}-${index}`}>
|
|
34
|
+
{item.shape === 'Arrow Down' && <AiOutlineArrowDown />}
|
|
35
|
+
{item.shape === 'Arrow Up' && <AiOutlineArrowUp />}
|
|
36
|
+
{item.shape === 'Arrow Right' && <AiOutlineArrowRight />}
|
|
37
|
+
{item.shape === 'Arrow Left' && <AiOutlineArrowLeft />}
|
|
38
|
+
</Group>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
export default HexIcon
|
|
@@ -2,10 +2,9 @@ import { useContext } from 'react'
|
|
|
2
2
|
import { geoCentroid, geoPath } from 'd3-geo'
|
|
3
3
|
import ConfigContext from './../../../../context'
|
|
4
4
|
import { MapContext } from './../../../../types/MapContext'
|
|
5
|
-
import
|
|
6
|
-
import { Group } from '@visx/group'
|
|
5
|
+
import HexIcon from '../HexIcon'
|
|
7
6
|
import { Text } from '@visx/text'
|
|
8
|
-
import
|
|
7
|
+
import { getContrastColor } from '@cdc/core/helpers/cove/accessibility'
|
|
9
8
|
|
|
10
9
|
const offsets = {
|
|
11
10
|
'US-VT': [50, -8],
|
|
@@ -52,16 +51,41 @@ const TerritoryHexagon = ({ label, text, stroke, strokeWidth, textColor, territo
|
|
|
52
51
|
<>
|
|
53
52
|
{state.hexMap.shapeGroups.map((group, groupIndex) => {
|
|
54
53
|
return group.items.map((item, itemIndex) => {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
<
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
54
|
+
switch (item.operator) {
|
|
55
|
+
case '=':
|
|
56
|
+
if (geoData[item.key] === item.value || Number(geoData[item.key]) === Number(item.value)) {
|
|
57
|
+
return <HexIcon item={item} index={itemIndex} centroid={centroid} isTerritory />
|
|
58
|
+
}
|
|
59
|
+
break
|
|
60
|
+
case '≠':
|
|
61
|
+
if (geoData[item.key] !== item.value && Number(geoData[item.key]) !== Number(item.value)) {
|
|
62
|
+
return <HexIcon item={item} index={itemIndex} centroid={centroid} isTerritory />
|
|
63
|
+
}
|
|
64
|
+
break
|
|
65
|
+
case '<':
|
|
66
|
+
if (Number(geoData[item.key]) < Number(item.value)) {
|
|
67
|
+
return <HexIcon item={item} index={itemIndex} centroid={centroid} isTerritory />
|
|
68
|
+
}
|
|
69
|
+
break
|
|
70
|
+
case '>':
|
|
71
|
+
if (Number(geoData[item.key]) > Number(item.value)) {
|
|
72
|
+
return <HexIcon item={item} index={itemIndex} centroid={centroid} isTerritory />
|
|
73
|
+
}
|
|
74
|
+
break
|
|
75
|
+
case '<=':
|
|
76
|
+
if (Number(geoData[item.key]) <= Number(item.value)) {
|
|
77
|
+
return <HexIcon item={item} index={itemIndex} centroid={centroid} isTerritory />
|
|
78
|
+
}
|
|
79
|
+
break
|
|
80
|
+
case '>=':
|
|
81
|
+
if (item.operator === '>=') {
|
|
82
|
+
if (Number(geoData[item.key]) >= Number(item.value)) {
|
|
83
|
+
return <HexIcon item={item} index={itemIndex} centroid={centroid} isTerritory />
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
break
|
|
87
|
+
default:
|
|
88
|
+
break
|
|
65
89
|
}
|
|
66
90
|
})
|
|
67
91
|
})}
|
|
@@ -71,12 +95,7 @@ const TerritoryHexagon = ({ label, text, stroke, strokeWidth, textColor, territo
|
|
|
71
95
|
|
|
72
96
|
if (undefined === abbr) return null
|
|
73
97
|
|
|
74
|
-
let textColor = '#FFF'
|
|
75
|
-
|
|
76
|
-
// Dynamic text color
|
|
77
|
-
if (chroma.contrast(textColor, bgColor) < 3.5) {
|
|
78
|
-
textColor = '#202020' // dark gray
|
|
79
|
-
}
|
|
98
|
+
let textColor = getContrastColor('#FFF', bgColor)
|
|
80
99
|
|
|
81
100
|
// always make HI black since it is off to the side
|
|
82
101
|
if (abbr === 'US-HI') {
|