@cdc/map 4.23.11 → 4.24.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.
- package/dist/cdcmap.js +41327 -40764
- package/examples/default-patterns.json +581 -0
- package/examples/default-usa.json +159 -57
- package/examples/private/map-text-wrap.json +574 -0
- package/examples/private/map-world-data.json +1046 -0
- package/examples/private/new-world.json +38337 -0
- package/examples/private/zika-issue.json +1198 -0
- package/index.html +10 -4
- package/package.json +3 -3
- package/src/CdcMap.tsx +10 -15
- package/src/components/{EditorPanel.jsx → EditorPanel/components/EditorPanel.tsx} +30 -62
- package/src/components/{HexShapeSettings.jsx → EditorPanel/components/HexShapeSettings.tsx} +0 -49
- 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/Legend/components/Legend.tsx +183 -0
- package/src/components/Legend/components/LegendItem.Hex.tsx +53 -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 -9
- package/src/components/{UsaMap.jsx → UsaMap/components/UsaMap.State.tsx} +52 -123
- package/src/components/UsaMap/index.tsx +13 -0
- package/src/components/{WorldMap.jsx → WorldMap/components/WorldMap.jsx} +6 -6
- 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 +3 -1
- package/src/scss/main.scss +11 -1
- package/src/types/MapConfig.ts +149 -0
- package/src/types/MapContext.ts +45 -0
- package/src/types/runtimeLegend.ts +1 -0
- 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,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,53 @@
|
|
|
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, runtimeLegend, viewport } = props
|
|
7
|
+
const { legend } = state
|
|
8
|
+
const { title } = state.general
|
|
9
|
+
|
|
10
|
+
const columnLogic = legend.position === 'side' && legend.singleColumn ? 'single-column' : legend.position === 'bottom' && legend.singleRow ? 'single-row' : ''
|
|
11
|
+
|
|
12
|
+
const getItemShape = shape => {
|
|
13
|
+
switch (shape) {
|
|
14
|
+
case 'Arrow Down':
|
|
15
|
+
return <AiOutlineArrowDown />
|
|
16
|
+
case 'Arrow Up':
|
|
17
|
+
return <AiOutlineArrowUp />
|
|
18
|
+
case 'Arrow Right':
|
|
19
|
+
return <AiOutlineArrowRight />
|
|
20
|
+
default:
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const { legendClasses } = useDataVizClasses(state, viewport)
|
|
26
|
+
|
|
27
|
+
// TODO: create core legend for reusability
|
|
28
|
+
return (
|
|
29
|
+
state.hexMap.type === 'shapes' &&
|
|
30
|
+
state.hexMap.shapeGroups.map((shapeGroup, shapeGroupIndex) => {
|
|
31
|
+
return (
|
|
32
|
+
<aside id='legend' className={legendClasses.aside.join(' ')} role='region' aria-label='Legend' tabIndex='0'>
|
|
33
|
+
<section className={legendClasses.section.join(' ')} aria-label='Map Legend'>
|
|
34
|
+
{legend.title && <span className={legendClasses.title.join(' ')}>{parse(shapeGroup.legendTitle)}</span>}
|
|
35
|
+
{legend.dynamicDescription === false && legend.description && <p className={legendClasses.description.join(' ')}>{parse(shapeGroup.legendDescription)}</p>}
|
|
36
|
+
|
|
37
|
+
<ul className={legendClasses.ul.join(' ')} aria-label='Legend items' style={{ listStyle: 'none' }}>
|
|
38
|
+
{shapeGroup.items.map((item, itemIndex) => {
|
|
39
|
+
return (
|
|
40
|
+
<li className={legendClasses.li.join(' ')}>
|
|
41
|
+
{getItemShape(item.shape)} {item.value}
|
|
42
|
+
</li>
|
|
43
|
+
)
|
|
44
|
+
})}
|
|
45
|
+
</ul>
|
|
46
|
+
</section>
|
|
47
|
+
</aside>
|
|
48
|
+
)
|
|
49
|
+
})
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
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
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { useContext } from 'react'
|
|
2
|
+
import { geoCentroid, geoPath } from 'd3-geo'
|
|
3
|
+
import ConfigContext from './../../../../context'
|
|
4
|
+
import { MapContext } from './../../../../types/MapContext'
|
|
5
|
+
import { AiOutlineArrowUp, AiOutlineArrowDown, AiOutlineArrowRight } from 'react-icons/ai'
|
|
6
|
+
import { Group } from '@visx/group'
|
|
7
|
+
import { Text } from '@visx/text'
|
|
8
|
+
import chroma from 'chroma-js'
|
|
9
|
+
|
|
10
|
+
const offsets = {
|
|
11
|
+
'US-VT': [50, -8],
|
|
12
|
+
'US-NH': [34, 2],
|
|
13
|
+
'US-MA': [30, -1],
|
|
14
|
+
'US-RI': [28, 2],
|
|
15
|
+
'US-CT': [35, 10],
|
|
16
|
+
'US-NJ': [42, 1],
|
|
17
|
+
'US-DE': [33, 0],
|
|
18
|
+
'US-MD': [47, 10]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const nudges = {
|
|
22
|
+
'US-FL': [15, 3],
|
|
23
|
+
'US-AK': [0, -8],
|
|
24
|
+
'US-CA': [-10, 0],
|
|
25
|
+
'US-NY': [5, 0],
|
|
26
|
+
'US-MI': [13, 20],
|
|
27
|
+
'US-LA': [-10, -3],
|
|
28
|
+
'US-HI': [-10, 10],
|
|
29
|
+
'US-ID': [0, 10],
|
|
30
|
+
'US-WV': [-2, 2]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// todo: combine hexagonLabel & geoLabel functions
|
|
34
|
+
// todo: move geoLabel functions outside of components for reusability
|
|
35
|
+
const TerritoryHexagon = ({ label, text, stroke, strokeWidth, textColor, territory, territoryData, ...props }) => {
|
|
36
|
+
const { state } = useContext<MapContext>(ConfigContext)
|
|
37
|
+
|
|
38
|
+
const isHex = state.general.displayAsHex
|
|
39
|
+
|
|
40
|
+
// Labels
|
|
41
|
+
const hexagonLabel = (geo, bgColor = '#FFFFFF', projection) => {
|
|
42
|
+
let centroid = projection ? projection(geoCentroid(geo)) : [22, 17.5]
|
|
43
|
+
|
|
44
|
+
let abbr = geo?.properties?.iso ? geo.properties.iso : geo.uid
|
|
45
|
+
|
|
46
|
+
const getArrowDirection = (geoData, geo, isTerritory = false) => {
|
|
47
|
+
if (!isTerritory) {
|
|
48
|
+
centroid = projection(geoCentroid(geo))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<>
|
|
53
|
+
{state.hexMap.shapeGroups.map((group, groupIndex) => {
|
|
54
|
+
return group.items.map((item, itemIndex) => {
|
|
55
|
+
if (item.operator === '=') {
|
|
56
|
+
if (geoData[item.key] === item.value) {
|
|
57
|
+
return (
|
|
58
|
+
<Group style={{ transform: `translate(36%, 50%)`, fill: 'currentColor' }}>
|
|
59
|
+
{item.shape === 'Arrow Down' && <AiOutlineArrowDown size={12} stroke='none' fontWeight={100} />}
|
|
60
|
+
{item.shape === 'Arrow Up' && <AiOutlineArrowUp size={12} stroke='none' fontWeight={100} />}
|
|
61
|
+
{item.shape === 'Arrow Right' && <AiOutlineArrowRight size={12} stroke='none' fontWeight={100} />}
|
|
62
|
+
</Group>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
})}
|
|
68
|
+
</>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (undefined === abbr) return null
|
|
73
|
+
|
|
74
|
+
let textColor = '#FFF'
|
|
75
|
+
|
|
76
|
+
// Dynamic text color
|
|
77
|
+
if (chroma.contrast(textColor, bgColor) < 3.5) {
|
|
78
|
+
textColor = '#202020' // dark gray
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// always make HI black since it is off to the side
|
|
82
|
+
if (abbr === 'US-HI') {
|
|
83
|
+
textColor = '#000'
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let x = 0,
|
|
87
|
+
y = state.hexMap.type === 'shapes' ? -10 : 5
|
|
88
|
+
|
|
89
|
+
// used to nudge/move some of the labels for better readability
|
|
90
|
+
if (nudges[abbr] && false === isHex) {
|
|
91
|
+
x += nudges[abbr][0]
|
|
92
|
+
y += nudges[abbr][1]
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (undefined === offsets[abbr] || isHex) {
|
|
96
|
+
let y = state.hexMap.type === 'shapes' ? '30%' : '50%'
|
|
97
|
+
return (
|
|
98
|
+
<>
|
|
99
|
+
<Text fontSize={14} x={'50%'} y={y} style={{ fill: 'currentColor', stroke: 'initial', fontWeight: 400, opacity: 1, fillOpacity: 1 }} textAnchor='middle' verticalAnchor='middle'>
|
|
100
|
+
{abbr.substring(3)}
|
|
101
|
+
</Text>
|
|
102
|
+
{state.general.displayAsHex && state.hexMap.type === 'shapes' && getArrowDirection(territoryData, geo, true)}
|
|
103
|
+
</>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let [dx, dy] = offsets[abbr]
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<g>
|
|
111
|
+
<line x1={centroid[0]} y1={centroid[1]} x2={centroid[0] + dx} y2={centroid[1] + dy} stroke='rgba(0,0,0,.5)' strokeWidth={1} />
|
|
112
|
+
<text x={4} strokeWidth='0' fontSize={13} style={{ fill: '#202020' }} alignmentBaseline='middle' transform={`translate(${centroid[0] + dx}, ${centroid[1] + dy})`}>
|
|
113
|
+
{abbr.substring(3)}
|
|
114
|
+
</text>
|
|
115
|
+
</g>
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<svg viewBox='0 0 45 51' className='territory-wrapper--hex'>
|
|
121
|
+
<g {...props}>
|
|
122
|
+
<polygon stroke={stroke} strokeWidth={strokeWidth} points='22 0 44 12.702 44 38.105 22 50.807 0 38.105 0 12.702' />
|
|
123
|
+
{state.general.displayAsHex && hexagonLabel(territoryData ? territoryData : geo, stroke, false)}
|
|
124
|
+
</g>
|
|
125
|
+
</svg>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export default TerritoryHexagon
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { useContext } from 'react'
|
|
2
|
+
import { PatternLines, PatternCircles, PatternWaves } from '@visx/pattern'
|
|
3
|
+
import chroma from 'chroma-js'
|
|
4
|
+
import ConfigContext from './../../../../context'
|
|
5
|
+
import { type MapContext } from '../../../../types/MapContext'
|
|
6
|
+
// todo: move this somewhere that makes better sense for pattern sizes.
|
|
7
|
+
const sizes = {
|
|
8
|
+
small: '8',
|
|
9
|
+
medium: '10',
|
|
10
|
+
large: '12'
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const TerritoryRectangle = ({ label, text, stroke, strokeWidth, textColor, hasPattern, territory, ...props }) => {
|
|
14
|
+
const { state, supportedTerritories } = useContext<MapContext>(ConfigContext)
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<svg viewBox='0 0 45 28'>
|
|
18
|
+
<g {...props} strokeLinejoin='round'>
|
|
19
|
+
<path
|
|
20
|
+
stroke={stroke}
|
|
21
|
+
strokeWidth={strokeWidth}
|
|
22
|
+
d='M40,0.5 C41.2426407,0.5 42.3676407,1.00367966 43.1819805,1.81801948 C43.9963203,2.63235931 44.5,3.75735931 44.5,5 L44.5,5 L44.5,23 C44.5,24.2426407 43.9963203,25.3676407 43.1819805,26.1819805 C42.3676407,26.9963203 41.2426407,27.5 40,27.5 L40,27.5 L5,27.5 C3.75735931,27.5 2.63235931,26.9963203 1.81801948,26.1819805 C1.00367966,25.3676407 0.5,24.2426407 0.5,23 L0.5,23 L0.5,5 C0.5,3.75735931 1.00367966,2.63235931 1.81801948,1.81801948 C2.63235931,1.00367966 3.75735931,0.5 5,0.5 L5,0.5 Z'
|
|
23
|
+
{...props}
|
|
24
|
+
/>
|
|
25
|
+
<text textAnchor='middle' dominantBaseline='middle' x='50%' y='54%' fill={text} style={{ stroke: textColor, strokeWidth: 1 }} className='territory-text' paint-order='stroke'>
|
|
26
|
+
{label}
|
|
27
|
+
</text>
|
|
28
|
+
|
|
29
|
+
{state.map.patterns.map((patternData, patternIndex) => {
|
|
30
|
+
let defaultPatternColor = 'black'
|
|
31
|
+
|
|
32
|
+
const hasMatchingValues = supportedTerritories[territory].includes(patternData?.dataValue)
|
|
33
|
+
console.log('has match', patternData)
|
|
34
|
+
|
|
35
|
+
if (chroma.contrast(defaultPatternColor, props.style.fill) < 3.5) {
|
|
36
|
+
defaultPatternColor = 'white'
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!hasMatchingValues) return null
|
|
40
|
+
if (!patternData.pattern) return null
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<>
|
|
44
|
+
{patternData?.pattern === 'waves' && <PatternWaves id={`territory-${patternData?.dataKey}--${patternIndex}`} height={sizes[patternData?.size] ?? 10} width={sizes[patternData?.size] ?? 10} fill={defaultPatternColor} complement />}
|
|
45
|
+
{patternData?.pattern === 'circles' && <PatternCircles id={`territory-${patternData?.dataKey}--${patternIndex}`} height={sizes[patternData?.size] ?? 10} width={sizes[patternData?.size] ?? 10} fill={defaultPatternColor} complement />}
|
|
46
|
+
{patternData?.pattern === 'lines' && <PatternLines id={`territory-${patternData?.dataKey}--${patternIndex}`} height={sizes[patternData?.size] ?? 6} width={sizes[patternData?.size] ?? 6} stroke={defaultPatternColor} strokeWidth={1} orientation={['diagonalRightToLeft']} />}
|
|
47
|
+
<path
|
|
48
|
+
stroke={stroke}
|
|
49
|
+
strokeWidth={strokeWidth}
|
|
50
|
+
d='M40,0.5 C41.2426407,0.5 42.3676407,1.00367966 43.1819805,1.81801948 C43.9963203,2.63235931 44.5,3.75735931 44.5,5 L44.5,5 L44.5,23 C44.5,24.2426407 43.9963203,25.3676407 43.1819805,26.1819805 C42.3676407,26.9963203 41.2426407,27.5 40,27.5 L40,27.5 L5,27.5 C3.75735931,27.5 2.63235931,26.9963203 1.81801948,26.1819805 C1.00367966,25.3676407 0.5,24.2426407 0.5,23 L0.5,23 L0.5,5 C0.5,3.75735931 1.00367966,2.63235931 1.81801948,1.81801948 C2.63235931,1.00367966 3.75735931,0.5 5,0.5 L5,0.5 Z'
|
|
51
|
+
fill={`url(#territory-${patternData?.dataKey}--${patternIndex})`}
|
|
52
|
+
color='white'
|
|
53
|
+
className={[`territory-pattern-${patternData.dataKey}`, `territory-pattern-${patternData.dataKey}--${patternData.dataValue}`].join(' ')}
|
|
54
|
+
/>
|
|
55
|
+
<text textAnchor='middle' dominantBaseline='middle' x='50%' y='54%' fill={text} style={{ stroke: patternData ? 'black' : textColor, strokeWidth: patternData ? 6 : 0 }} className='territory-text' paint-order='stroke'>
|
|
56
|
+
{label}
|
|
57
|
+
</text>
|
|
58
|
+
</>
|
|
59
|
+
)
|
|
60
|
+
})}
|
|
61
|
+
</g>
|
|
62
|
+
</svg>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export default TerritoryRectangle
|