@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.
Files changed (51) hide show
  1. package/dist/cdcmap.js +41327 -40764
  2. package/examples/default-patterns.json +581 -0
  3. package/examples/default-usa.json +159 -57
  4. package/examples/private/map-text-wrap.json +574 -0
  5. package/examples/private/map-world-data.json +1046 -0
  6. package/examples/private/new-world.json +38337 -0
  7. package/examples/private/zika-issue.json +1198 -0
  8. package/index.html +10 -4
  9. package/package.json +3 -3
  10. package/src/CdcMap.tsx +10 -15
  11. package/src/components/{EditorPanel.jsx → EditorPanel/components/EditorPanel.tsx} +30 -62
  12. package/src/components/{HexShapeSettings.jsx → EditorPanel/components/HexShapeSettings.tsx} +0 -49
  13. package/src/components/EditorPanel/components/Inputs.tsx +59 -0
  14. package/src/components/EditorPanel/components/Panel.PatternSettings.tsx +140 -0
  15. package/src/components/EditorPanel/components/Panels.tsx +7 -0
  16. package/src/components/EditorPanel/index.tsx +3 -0
  17. package/src/components/Legend/components/Legend.tsx +183 -0
  18. package/src/components/Legend/components/LegendItem.Hex.tsx +53 -0
  19. package/src/components/Legend/components/index.scss +235 -0
  20. package/src/components/Legend/index.tsx +3 -0
  21. package/src/components/UsaMap/components/Territory/Territory.Hexagon.tsx +129 -0
  22. package/src/components/UsaMap/components/Territory/Territory.Rectangle.tsx +66 -0
  23. package/src/components/UsaMap/components/Territory/index.tsx +9 -0
  24. package/src/components/{CountyMap.jsx → UsaMap/components/UsaMap.County.tsx} +9 -9
  25. package/src/components/{UsaRegionMap.jsx → UsaMap/components/UsaMap.Region.tsx} +1 -3
  26. package/src/components/{SingleStateMap.jsx → UsaMap/components/UsaMap.SingleState.tsx} +8 -9
  27. package/src/components/{UsaMap.jsx → UsaMap/components/UsaMap.State.tsx} +52 -123
  28. package/src/components/UsaMap/index.tsx +13 -0
  29. package/src/components/{WorldMap.jsx → WorldMap/components/WorldMap.jsx} +6 -6
  30. package/src/components/WorldMap/data/world-topo.json +1 -0
  31. package/src/components/WorldMap/index.tsx +3 -0
  32. package/src/context.ts +2 -1
  33. package/src/data/initial-state.js +3 -1
  34. package/src/scss/main.scss +11 -1
  35. package/src/types/MapConfig.ts +149 -0
  36. package/src/types/MapContext.ts +45 -0
  37. package/src/types/runtimeLegend.ts +1 -0
  38. package/src/components/Sidebar.tsx +0 -142
  39. package/src/data/abbreviations.js +0 -57
  40. package/src/data/feature-test.json +0 -73
  41. package/src/data/newtest.json +0 -1
  42. package/src/data/state-abbreviations.js +0 -60
  43. package/src/data/supported-cities.csv +0 -165
  44. package/src/data/test.json +0 -1
  45. package/src/data/world-topo.json +0 -1
  46. package/src/scss/sidebar.scss +0 -230
  47. /package/src/{data → components/UsaMap/data}/cb_2019_us_county_20m.json +0 -0
  48. /package/src/{data → components/UsaMap/data}/us-hex-topo.json +0 -0
  49. /package/src/{data → components/UsaMap/data}/us-regions-topo-2.json +0 -0
  50. /package/src/{data → components/UsaMap/data}/us-regions-topo.json +0 -0
  51. /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,3 @@
1
+ import Legend from './components/Legend'
2
+
3
+ export default Legend
@@ -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
@@ -0,0 +1,9 @@
1
+ import TerritoryHexagon from './Territory.Hexagon'
2
+ import TerritoryRectangle from './Territory.Rectangle'
3
+
4
+ const Territory = {
5
+ Rectangle: TerritoryRectangle,
6
+ Hexagon: TerritoryHexagon
7
+ }
8
+
9
+ export default Territory