@cdc/map 4.24.12 → 4.25.2-25
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 +50119 -48822
- package/examples/annotation/index.json +1 -1
- package/examples/custom-map-layers.json +1 -1
- package/examples/default-geocode.json +2 -2
- package/examples/example-city-state.json +1 -1
- package/examples/private/DEV-9989.json +229 -0
- package/examples/private/ardi.json +180 -0
- package/examples/private/colors 2.json +416 -0
- package/examples/private/colors.json +416 -0
- package/examples/private/colors.json.zip +0 -0
- package/examples/private/customColors.json +45348 -0
- package/examples/private/mmr.json +246 -0
- package/examples/private/test.json +1632 -0
- package/index.html +12 -14
- package/package.json +8 -3
- package/src/CdcMap.tsx +126 -396
- package/src/_stories/CdcMap.Filters.stories.tsx +19 -0
- package/src/_stories/CdcMap.Legend.Gradient.stories.tsx +9 -0
- package/src/_stories/CdcMap.stories.tsx +1 -1
- package/src/_stories/GoogleMap.stories.tsx +19 -0
- package/src/_stories/_mock/DEV-10148.json +859 -0
- package/src/_stories/_mock/DEV-9989.json +229 -0
- package/src/_stories/_mock/example-city-state.json +1 -1
- package/src/_stories/_mock/google-map.json +819 -0
- package/src/_stories/_mock/wastewater-map.json +210 -206
- package/src/components/Annotation/Annotation.Draggable.tsx +34 -43
- package/src/components/Annotation/AnnotationDropdown.tsx +4 -4
- package/src/components/CityList.tsx +3 -9
- package/src/components/DataTable.tsx +8 -9
- package/src/components/EditorPanel/components/EditorPanel.tsx +255 -490
- package/src/components/GoogleMap/components/GoogleMap.tsx +67 -0
- package/src/components/GoogleMap/index.tsx +3 -0
- package/src/components/Legend/components/Legend.tsx +40 -30
- package/src/components/Legend/components/LegendItem.Hex.tsx +7 -3
- package/src/components/Legend/components/index.scss +22 -16
- package/src/components/Modal.tsx +6 -5
- package/src/components/NavigationMenu.tsx +4 -3
- package/src/components/UsaMap/components/TerritoriesSection.tsx +66 -0
- package/src/components/UsaMap/components/Territory/Territory.Hexagon.tsx +15 -16
- package/src/components/UsaMap/components/Territory/Territory.Rectangle.tsx +3 -3
- package/src/components/UsaMap/components/UsaMap.County.tsx +1 -1
- package/src/components/UsaMap/components/UsaMap.Region.tsx +12 -8
- package/src/components/UsaMap/components/UsaMap.SingleState.tsx +2 -2
- package/src/components/UsaMap/components/UsaMap.State.tsx +23 -29
- package/src/components/WorldMap/WorldMap.tsx +3 -5
- package/src/context.ts +0 -12
- package/src/data/initial-state.js +2 -2
- package/src/data/supported-geos.js +23 -3
- package/src/helpers/applyColorToLegend.ts +3 -3
- package/src/helpers/closeModal.ts +9 -0
- package/src/helpers/handleMapAriaLabels.ts +38 -0
- package/src/helpers/indexOfIgnoreType.ts +8 -0
- package/src/helpers/navigationHandler.ts +21 -0
- package/src/helpers/toTitleCase.ts +44 -0
- package/src/helpers/validateFipsCodeLength.ts +30 -0
- package/src/hooks/useResizeObserver.ts +42 -0
- package/src/hooks/useTooltip.ts +4 -2
- package/src/index.jsx +1 -0
- package/src/scss/editor-panel.scss +2 -1
- package/src/scss/filters.scss +0 -5
- package/src/scss/main.scss +57 -61
- package/src/scss/map.scss +1 -13
- package/src/types/MapConfig.ts +20 -11
- package/src/types/MapContext.ts +4 -12
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import React, { useContext, useEffect, useRef } from 'react'
|
|
2
|
+
import { Loader } from '@googlemaps/js-api-loader'
|
|
3
|
+
import { MarkerClusterer } from '@googlemaps/markerclusterer'
|
|
4
|
+
import ConfigContext from '../../../context'
|
|
5
|
+
|
|
6
|
+
// center on USA
|
|
7
|
+
const center = {
|
|
8
|
+
lat: 37.09024,
|
|
9
|
+
lng: -95.712891
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type GoogleMapComponentProps = {
|
|
13
|
+
apiKey?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const GoogleMapComponent: React.FC<GoogleMapComponentProps> = ({ apiKey = '' }) => {
|
|
17
|
+
const mapRef = useRef(null)
|
|
18
|
+
const { state } = useContext(ConfigContext)
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
const loader = new Loader({
|
|
22
|
+
apiKey: apiKey,
|
|
23
|
+
version: 'weekly'
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
loader
|
|
27
|
+
.load()
|
|
28
|
+
.then(() => {
|
|
29
|
+
if (mapRef.current) {
|
|
30
|
+
const map = new window.google.maps.Map(mapRef.current, {
|
|
31
|
+
center: center,
|
|
32
|
+
zoom: 4
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const markers = state.data.map(d => {
|
|
36
|
+
const markerContent = document.createElement('div')
|
|
37
|
+
markerContent.style.backgroundColor = 'orange' // Set the background color
|
|
38
|
+
markerContent.style.width = '25px'
|
|
39
|
+
markerContent.style.height = '25px'
|
|
40
|
+
markerContent.style.borderRadius = '50%'
|
|
41
|
+
markerContent.style.display = 'flex'
|
|
42
|
+
markerContent.style.alignItems = 'center'
|
|
43
|
+
markerContent.style.justifyContent = 'center'
|
|
44
|
+
markerContent.style.color = 'white'
|
|
45
|
+
markerContent.innerText = d[state.columns.geo.name]
|
|
46
|
+
|
|
47
|
+
const marker = new google.maps.Marker({
|
|
48
|
+
position: { lat: Number(d[state.columns.latitude.name]), lng: Number(d[state.columns.longitude.name]) },
|
|
49
|
+
title: d[state.columns.geo.name],
|
|
50
|
+
map: map
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
return marker
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
new MarkerClusterer({ markers, map })
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
.catch(e => {
|
|
60
|
+
console.error('Error loading Google Maps API:', e)
|
|
61
|
+
})
|
|
62
|
+
}, [apiKey, state])
|
|
63
|
+
|
|
64
|
+
return <div ref={mapRef} style={{ height: '500px', width: '100%' }} />
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export default GoogleMapComponent
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
//TODO: Move legends to core
|
|
2
|
-
import { forwardRef, useContext
|
|
2
|
+
import { forwardRef, useContext } from 'react'
|
|
3
3
|
import parse from 'html-react-parser'
|
|
4
4
|
|
|
5
5
|
//types
|
|
@@ -17,8 +17,9 @@ import { PatternLines, PatternCircles, PatternWaves } from '@visx/pattern'
|
|
|
17
17
|
import { GlyphStar, GlyphTriangle, GlyphDiamond, GlyphSquare, GlyphCircle } from '@visx/glyph'
|
|
18
18
|
import { Group } from '@visx/group'
|
|
19
19
|
import './index.scss'
|
|
20
|
-
import {
|
|
20
|
+
import { type ViewPort } from '@cdc/core/types/ViewPort'
|
|
21
21
|
import { isBelowBreakpoint, isMobileHeightViewport } from '@cdc/core/helpers/viewports'
|
|
22
|
+
import { displayDataAsText } from '@cdc/core/helpers/displayDataAsText'
|
|
22
23
|
|
|
23
24
|
const LEGEND_PADDING = 30
|
|
24
25
|
|
|
@@ -26,22 +27,22 @@ type LegendProps = {
|
|
|
26
27
|
skipId: string
|
|
27
28
|
dimensions: DimensionsType
|
|
28
29
|
containerWidthPadding: number
|
|
29
|
-
currentViewport:
|
|
30
|
+
currentViewport: ViewPort
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
|
|
33
|
-
const { skipId,
|
|
34
|
+
const { skipId, containerWidthPadding } = props
|
|
35
|
+
const { isEditor, dimensions, currentViewport } = useContext(ConfigContext)
|
|
34
36
|
|
|
35
37
|
const {
|
|
36
38
|
// prettier-ignore
|
|
37
|
-
displayDataAsText,
|
|
38
39
|
resetLegendToggles,
|
|
39
40
|
runtimeFilters,
|
|
40
41
|
runtimeLegend,
|
|
41
42
|
setAccessibleStatus,
|
|
42
43
|
setRuntimeLegend,
|
|
43
44
|
state,
|
|
44
|
-
viewport,
|
|
45
|
+
currentViewport: viewport,
|
|
45
46
|
mapId
|
|
46
47
|
} = useContext(ConfigContext)
|
|
47
48
|
|
|
@@ -75,9 +76,9 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
|
|
|
75
76
|
}
|
|
76
77
|
const getFormattedLegendItems = () => {
|
|
77
78
|
return runtimeLegend.map((entry, idx) => {
|
|
78
|
-
const entryMax = displayDataAsText(entry.max, 'primary')
|
|
79
|
+
const entryMax = displayDataAsText(entry.max, 'primary', state)
|
|
79
80
|
|
|
80
|
-
const entryMin = displayDataAsText(entry.min, 'primary')
|
|
81
|
+
const entryMin = displayDataAsText(entry.min, 'primary', state)
|
|
81
82
|
let formattedText = `${entryMin}${entryMax !== entryMin ? ` - ${entryMax}` : ''}`
|
|
82
83
|
|
|
83
84
|
// If interval, add some formatting
|
|
@@ -86,7 +87,7 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
|
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
if (legend.type === 'category') {
|
|
89
|
-
formattedText = displayDataAsText(entry.value, 'primary')
|
|
90
|
+
formattedText = displayDataAsText(entry.value, 'primary', state)
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
if (entry.max === 0 && entry.min === 0) {
|
|
@@ -116,12 +117,14 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
|
|
|
116
117
|
const legendList = (patternsOnly = false) => {
|
|
117
118
|
const formattedItems = patternsOnly ? [] : getFormattedLegendItems()
|
|
118
119
|
const patternsOnlyFont = isMobileHeightViewport(currentViewport) ? '12px' : '14px'
|
|
120
|
+
const hasDisabledItems = formattedItems.some(item => item.disabled)
|
|
119
121
|
let legendItems
|
|
120
122
|
|
|
121
123
|
legendItems = formattedItems.map((item, idx) => {
|
|
122
124
|
const handleListItemClass = () => {
|
|
123
125
|
let classes = ['legend-container__li', 'd-flex', 'align-items-center']
|
|
124
126
|
if (item.disabled) classes.push('legend-container__li--disabled')
|
|
127
|
+
else if (hasDisabledItems) classes.push('legend-container__li--not-disabled')
|
|
125
128
|
if (item.special) classes.push('legend-container__li--special-class')
|
|
126
129
|
return classes.join(' ')
|
|
127
130
|
}
|
|
@@ -264,26 +267,33 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
|
|
|
264
267
|
ref={ref}
|
|
265
268
|
>
|
|
266
269
|
<section className={legendClasses.section.join(' ') || ''} aria-label='Map Legend'>
|
|
267
|
-
{legend.title
|
|
268
|
-
|
|
269
|
-
|
|
270
|
+
{(legend.title || legend.description || legend.dynamicDescription) && (
|
|
271
|
+
<div className='mb-3'>
|
|
272
|
+
{legend.title && <h3 className={legendClasses.title.join(' ') || ''}>{parse(legend.title)}</h3>}
|
|
273
|
+
{legend.dynamicDescription === false && legend.description && (
|
|
274
|
+
<p className={legendClasses.description.join(' ') || ''}>{parse(legend.description)}</p>
|
|
275
|
+
)}
|
|
276
|
+
{legend.dynamicDescription === true &&
|
|
277
|
+
runtimeFilters.map((filter, idx) => {
|
|
278
|
+
const lookupStr = `${idx},${filter.values.indexOf(String(filter.active))}`
|
|
279
|
+
|
|
280
|
+
// Do we have a custom description for this?
|
|
281
|
+
const desc = legend.descriptions[lookupStr] || ''
|
|
282
|
+
|
|
283
|
+
if (desc.length > 0) {
|
|
284
|
+
return (
|
|
285
|
+
<p
|
|
286
|
+
key={`dynamic-description-${lookupStr}`}
|
|
287
|
+
className={`dynamic-legend-description-${lookupStr} mt-2`}
|
|
288
|
+
>
|
|
289
|
+
{desc}
|
|
290
|
+
</p>
|
|
291
|
+
)
|
|
292
|
+
}
|
|
293
|
+
return true
|
|
294
|
+
})}
|
|
295
|
+
</div>
|
|
270
296
|
)}
|
|
271
|
-
{legend.dynamicDescription === true &&
|
|
272
|
-
runtimeFilters.map((filter, idx) => {
|
|
273
|
-
const lookupStr = `${idx},${filter.values.indexOf(String(filter.active))}`
|
|
274
|
-
|
|
275
|
-
// Do we have a custom description for this?
|
|
276
|
-
const desc = legend.descriptions[lookupStr] || ''
|
|
277
|
-
|
|
278
|
-
if (desc.length > 0) {
|
|
279
|
-
return (
|
|
280
|
-
<p key={`dynamic-description-${lookupStr}`} className={`dynamic-legend-description-${lookupStr}`}>
|
|
281
|
-
{desc}
|
|
282
|
-
</p>
|
|
283
|
-
)
|
|
284
|
-
}
|
|
285
|
-
return true
|
|
286
|
-
})}
|
|
287
297
|
|
|
288
298
|
<LegendGradient
|
|
289
299
|
labels={getFormattedLegendItems().map(item => item?.label) ?? []}
|
|
@@ -334,8 +344,8 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
|
|
|
334
344
|
</>
|
|
335
345
|
)}
|
|
336
346
|
{runtimeLegend.disabledAmt > 0 && (
|
|
337
|
-
<Button className={legendClasses.
|
|
338
|
-
|
|
347
|
+
<Button className={legendClasses.showAllButton.join(' ')} onClick={handleReset}>
|
|
348
|
+
Show All
|
|
339
349
|
</Button>
|
|
340
350
|
)}
|
|
341
351
|
</section>
|
|
@@ -3,7 +3,7 @@ import { AiOutlineArrowUp, AiOutlineArrowDown, AiOutlineArrowRight } from 'react
|
|
|
3
3
|
import parse from 'html-react-parser'
|
|
4
4
|
|
|
5
5
|
const LegendItemHex = props => {
|
|
6
|
-
const { state, viewport } = props
|
|
6
|
+
const { state, currentViewport: viewport } = props
|
|
7
7
|
|
|
8
8
|
const getItemShape = shape => {
|
|
9
9
|
switch (shape) {
|
|
@@ -27,8 +27,12 @@ const LegendItemHex = props => {
|
|
|
27
27
|
return (
|
|
28
28
|
<aside id='legend' className={legendClasses.aside.join(' ')} role='region' aria-label='Legend' tabIndex={0}>
|
|
29
29
|
<section className={legendClasses.section.join(' ')} aria-label='Map Legend'>
|
|
30
|
-
{shapeGroup.legendTitle &&
|
|
31
|
-
|
|
30
|
+
{shapeGroup.legendTitle && (
|
|
31
|
+
<h3 className={legendClasses.title.join(' ')}>{parse(shapeGroup.legendTitle)}</h3>
|
|
32
|
+
)}
|
|
33
|
+
{shapeGroup.legendDescription && (
|
|
34
|
+
<p className={legendClasses.description.join(' ')}>{parse(shapeGroup.legendDescription)}</p>
|
|
35
|
+
)}
|
|
32
36
|
|
|
33
37
|
<ul className={legendClasses.ul.join(' ')} aria-label='Legend items' style={{ listStyle: 'none' }}>
|
|
34
38
|
{shapeGroup.items.map((item, itemIndex) => {
|
|
@@ -16,11 +16,9 @@
|
|
|
16
16
|
background-color: #fff;
|
|
17
17
|
z-index: 6;
|
|
18
18
|
border-top: var(--cool-gray-10) 1px solid;
|
|
19
|
+
border-radius: 6px;
|
|
19
20
|
|
|
20
21
|
@include breakpointClass(md) {
|
|
21
|
-
.legend-container.legend-padding {
|
|
22
|
-
padding: 15px;
|
|
23
|
-
}
|
|
24
22
|
&.bottom,
|
|
25
23
|
&.top {
|
|
26
24
|
border: var(--cool-gray-10) 1px solid;
|
|
@@ -70,21 +68,21 @@
|
|
|
70
68
|
}
|
|
71
69
|
|
|
72
70
|
.legend-container {
|
|
73
|
-
--space-between-legend-items: 0.6em;
|
|
74
71
|
position: relative;
|
|
75
|
-
|
|
76
|
-
|
|
72
|
+
|
|
73
|
+
.tspan {
|
|
74
|
+
font-size: var(--legend-item-font-size) !important;
|
|
77
75
|
}
|
|
76
|
+
|
|
78
77
|
.legend-container__title {
|
|
79
|
-
font-size:
|
|
78
|
+
font-size: var(--legend-title-font-size);
|
|
79
|
+
font-weight: var(--legend-title-font-weight);
|
|
80
80
|
padding-bottom: 0;
|
|
81
81
|
display: inline-block;
|
|
82
|
+
color: black;
|
|
82
83
|
}
|
|
83
|
-
.legend-
|
|
84
|
-
|
|
85
|
-
p + ul,
|
|
86
|
-
p + p {
|
|
87
|
-
padding-top: 1em;
|
|
84
|
+
.legend-container__description {
|
|
85
|
+
font-size: var(--legend-description-font-size);
|
|
88
86
|
}
|
|
89
87
|
.legend-container__reset-button {
|
|
90
88
|
margin-top: 1em;
|
|
@@ -92,11 +90,17 @@
|
|
|
92
90
|
p {
|
|
93
91
|
line-height: 1.4em;
|
|
94
92
|
}
|
|
93
|
+
|
|
94
|
+
.legend-container__ul {
|
|
95
|
+
line-height: 1;
|
|
96
|
+
row-gap: var(--space-between-legend-item-rows);
|
|
97
|
+
column-gap: var(--space-between-legend-item-columns);
|
|
98
|
+
}
|
|
95
99
|
.legend-container__ul:not(.single-row) {
|
|
96
100
|
list-style: none;
|
|
97
|
-
padding-top: 1em;
|
|
98
101
|
display: grid;
|
|
99
102
|
grid-template-columns: 1fr;
|
|
103
|
+
|
|
100
104
|
@include breakpoint(md) {
|
|
101
105
|
grid-template-columns: 1fr 1fr;
|
|
102
106
|
}
|
|
@@ -117,7 +121,6 @@
|
|
|
117
121
|
flex-shrink: 0;
|
|
118
122
|
display: inline-block;
|
|
119
123
|
padding-right: 1em;
|
|
120
|
-
margin-bottom: var(--space-between-legend-items);
|
|
121
124
|
vertical-align: middle;
|
|
122
125
|
transition: 0.1s opacity;
|
|
123
126
|
display: flex;
|
|
@@ -134,6 +137,11 @@
|
|
|
134
137
|
&.legend-container__li--disabled {
|
|
135
138
|
opacity: 0.4;
|
|
136
139
|
}
|
|
140
|
+
&.legend-container__li--not-disabled {
|
|
141
|
+
outline: 1px solid #005ea2;
|
|
142
|
+
outline-offset: 5px;
|
|
143
|
+
border-radius: 1px;
|
|
144
|
+
}
|
|
137
145
|
}
|
|
138
146
|
}
|
|
139
147
|
.legend-container__ul.single-row {
|
|
@@ -145,10 +153,8 @@
|
|
|
145
153
|
align-items: center;
|
|
146
154
|
justify-content: flex-start;
|
|
147
155
|
flex-wrap: wrap;
|
|
148
|
-
row-gap: var(--space-between-legend-items);
|
|
149
156
|
|
|
150
157
|
& > li {
|
|
151
|
-
margin-right: 1em;
|
|
152
158
|
white-space: wrap;
|
|
153
159
|
display: flex;
|
|
154
160
|
justify-content: center;
|
package/src/components/Modal.tsx
CHANGED
|
@@ -4,18 +4,19 @@ import ConfigContext from '../context'
|
|
|
4
4
|
import Icon from '@cdc/core/components/ui/Icon'
|
|
5
5
|
|
|
6
6
|
const Modal = () => {
|
|
7
|
-
const { applyTooltipsToGeo,
|
|
8
|
-
|
|
7
|
+
const { applyTooltipsToGeo, applyLegendToRow, content, state, currentViewport: viewport } = useContext(ConfigContext)
|
|
8
|
+
const { capitalizeLabels } = state.tooltips
|
|
9
9
|
const tooltip = applyTooltipsToGeo(content.geoName, content.keyedData, 'jsx')
|
|
10
|
-
|
|
10
|
+
const type = state.general.type
|
|
11
11
|
const legendColors = applyLegendToRow(content.keyedData)
|
|
12
12
|
|
|
13
13
|
return (
|
|
14
14
|
<section
|
|
15
|
-
className={
|
|
15
|
+
className={
|
|
16
|
+
capitalizeLabels ? 'modal-content tooltip capitalize ' + viewport : 'modal-content tooltip ' + viewport
|
|
17
|
+
}
|
|
16
18
|
aria-hidden='true'
|
|
17
19
|
>
|
|
18
|
-
{type === 'data' && <LegendShape fill={legendColors[0]} />}
|
|
19
20
|
<div className='content'>{tooltip}</div>
|
|
20
21
|
<Icon display='close' alt='Close Modal' size={20} color='#000' className='modal-close' />
|
|
21
22
|
</section>
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import React, { useEffect, useState } from 'react'
|
|
1
|
+
import React, { useContext, useEffect, useState } from 'react'
|
|
2
|
+
import ConfigContext from '../context'
|
|
2
3
|
|
|
3
4
|
const NavigationMenu = ({ data, navigationHandler, options, columns, displayGeoName, mapTabbingID }) => {
|
|
5
|
+
const { state } = useContext(ConfigContext)
|
|
4
6
|
const [activeGeo, setActiveGeo] = useState('')
|
|
5
|
-
|
|
6
7
|
const [dropdownItems, setDropdownItems] = useState({})
|
|
7
8
|
|
|
8
9
|
const handleSubmit = event => {
|
|
@@ -55,7 +56,7 @@ const NavigationMenu = ({ data, navigationHandler, options, columns, displayGeoN
|
|
|
55
56
|
<label htmlFor={mapTabbingID.replace('#', '')}>
|
|
56
57
|
<div className='select-heading'>{navSelect}</div>
|
|
57
58
|
<select value={activeGeo} id={mapTabbingID.replace('#', '')} onChange={e => setActiveGeo(e.target.value)}>
|
|
58
|
-
{Object.keys(dropdownItems).map(
|
|
59
|
+
{Object.keys(dropdownItems).map(key => (
|
|
59
60
|
<option key={key} value={key}>
|
|
60
61
|
{key}
|
|
61
62
|
</option>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
type TerritoriesSectionProps = {
|
|
4
|
+
territories: JSX.Element[]
|
|
5
|
+
// Keys of the territories to display
|
|
6
|
+
territoresData: string[]
|
|
7
|
+
logo: string
|
|
8
|
+
config: any
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const TerritoriesSection: React.FC<TerritoriesSectionProps> = ({ territories, logo, config, territoriesData }) => {
|
|
12
|
+
// filter territioriesData into the two groups below
|
|
13
|
+
const freelyAssociatedKeys = territoriesData.filter(territory => {
|
|
14
|
+
return ['US-FM', 'US-MH', 'US-PW'].includes(territory)
|
|
15
|
+
})
|
|
16
|
+
const usTerritoriesKeys = territoriesData.filter(territory => {
|
|
17
|
+
return ['US-AS', 'US-GU', 'US-MP', 'US-PR', 'US-VI'].includes(territory)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const usTerritories = territories
|
|
21
|
+
.filter(territory => {
|
|
22
|
+
return usTerritoriesKeys.includes(`US-${territory?.props?.label}`)
|
|
23
|
+
})
|
|
24
|
+
.sort((a, b) => {
|
|
25
|
+
return a.props.label.localeCompare(b.props.label)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const freelyAssociatedStates = territories
|
|
29
|
+
.filter(territory => {
|
|
30
|
+
return freelyAssociatedKeys.includes(`US-${territory?.props?.label}`)
|
|
31
|
+
})
|
|
32
|
+
.sort((a, b) => {
|
|
33
|
+
return a.props.label.localeCompare(b.props.label)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
territoriesData.length > 0 && (
|
|
38
|
+
<>
|
|
39
|
+
{/* Temporarily make the max width fit the image width */}
|
|
40
|
+
<div>
|
|
41
|
+
<div className='d-flex mt-4'>
|
|
42
|
+
{'data' === config.general.type && logo && (
|
|
43
|
+
<img src={logo} alt='' className='map-logo' style={{ maxWidth: '50px' }} />
|
|
44
|
+
)}
|
|
45
|
+
</div>
|
|
46
|
+
<div>
|
|
47
|
+
{(usTerritories.length > 0 || config.general.territoriesAlwaysShow) && (
|
|
48
|
+
<>
|
|
49
|
+
<h5 className='territories-label'>U.S. Territories</h5>
|
|
50
|
+
<span className='mt-1 mb-2 d-flex flex-wrap territories'>{usTerritories}</span>
|
|
51
|
+
</>
|
|
52
|
+
)}
|
|
53
|
+
{(freelyAssociatedStates.length > 0 || config.general.territoriesAlwaysShow) && (
|
|
54
|
+
<>
|
|
55
|
+
<h5 className='territories-label'>Freely Associated States</h5>
|
|
56
|
+
<span className='mt-1 mb-2 d-flex flex-wrap territories'>{freelyAssociatedStates}</span>
|
|
57
|
+
</>
|
|
58
|
+
)}
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</>
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export default TerritoriesSection
|
|
@@ -52,7 +52,7 @@ const TerritoryHexagon = ({
|
|
|
52
52
|
const hexagonLabel = (geo, bgColor = '#FFFFFF', projection) => {
|
|
53
53
|
let centroid = projection ? projection(geoCentroid(geo)) : [22, 17.5]
|
|
54
54
|
|
|
55
|
-
let abbr = geo?.properties?.iso ? geo.properties.iso : geo.uid
|
|
55
|
+
let abbr = geo?.properties?.iso ? geo.properties.iso : geo?.uid ? geo.uid : `US-${label}`
|
|
56
56
|
|
|
57
57
|
const getArrowDirection = (geoData, geo, isTerritory = false) => {
|
|
58
58
|
if (!isTerritory) {
|
|
@@ -63,19 +63,20 @@ const TerritoryHexagon = ({
|
|
|
63
63
|
<>
|
|
64
64
|
{state.hexMap.shapeGroups.map((group, groupIndex) => {
|
|
65
65
|
return group.items.map((item, itemIndex) => {
|
|
66
|
+
if (!geoData) return
|
|
66
67
|
switch (item.operator) {
|
|
67
68
|
case '=':
|
|
68
|
-
if (geoData[item.key] === item.value || Number(geoData[item.key]) === Number(item.value)) {
|
|
69
|
+
if (geoData?.[item.key] === item.value || Number(geoData[item.key]) === Number(item.value)) {
|
|
69
70
|
return <HexIcon item={item} index={itemIndex} centroid={centroid} isTerritory />
|
|
70
71
|
}
|
|
71
72
|
break
|
|
72
73
|
case '≠':
|
|
73
|
-
if (geoData[item.key] !== item.value && Number(geoData[item.key]) !== Number(item.value)) {
|
|
74
|
+
if (geoData?.[item.key] !== item.value && Number(geoData[item.key]) !== Number(item.value)) {
|
|
74
75
|
return <HexIcon item={item} index={itemIndex} centroid={centroid} isTerritory />
|
|
75
76
|
}
|
|
76
77
|
break
|
|
77
78
|
case '<':
|
|
78
|
-
if (Number(geoData[item.key]) < Number(item.value)) {
|
|
79
|
+
if (Number(geoData?.[item.key]) < Number(item.value)) {
|
|
79
80
|
return <HexIcon item={item} index={itemIndex} centroid={centroid} isTerritory />
|
|
80
81
|
}
|
|
81
82
|
break
|
|
@@ -175,18 +176,16 @@ const TerritoryHexagon = ({
|
|
|
175
176
|
}
|
|
176
177
|
|
|
177
178
|
return (
|
|
178
|
-
|
|
179
|
-
<
|
|
180
|
-
<
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
</svg>
|
|
189
|
-
)
|
|
179
|
+
<svg viewBox='-1 -1 46 53' className='territory-wrapper--hex'>
|
|
180
|
+
<g {...props} data-tooltip-html={dataTooltipHtml} data-tooltip-id={dataTooltipId} onClick={handleShapeClick}>
|
|
181
|
+
<polygon
|
|
182
|
+
stroke={stroke}
|
|
183
|
+
strokeWidth={strokeWidth}
|
|
184
|
+
points='22 0 44 12.702 44 38.105 22 50.807 0 38.105 0 12.702'
|
|
185
|
+
/>
|
|
186
|
+
{state.general.displayAsHex && hexagonLabel(territoryData, stroke, false)}
|
|
187
|
+
</g>
|
|
188
|
+
</svg>
|
|
190
189
|
)
|
|
191
190
|
}
|
|
192
191
|
|
|
@@ -54,7 +54,7 @@ const TerritoryRectangle: React.FC<TerritoryShape> = ({
|
|
|
54
54
|
|
|
55
55
|
{state.map.patterns.map((patternData, patternIndex) => {
|
|
56
56
|
const patternColor = patternData.color || getContrastColor('#FFF', backgroundColor)
|
|
57
|
-
const hasMatchingValues = patternData.dataValue === territoryData[patternData.dataKey]
|
|
57
|
+
const hasMatchingValues = patternData.dataValue === territoryData?.[patternData.dataKey]
|
|
58
58
|
|
|
59
59
|
if (!hasMatchingValues) return null
|
|
60
60
|
if (!patternData.pattern) return null
|
|
@@ -96,8 +96,8 @@ const TerritoryRectangle: React.FC<TerritoryShape> = ({
|
|
|
96
96
|
fill={`url(#territory-${territory}-${patternData?.dataKey}--${patternIndex})`}
|
|
97
97
|
color={patternData ? 'white' : textColor}
|
|
98
98
|
className={[
|
|
99
|
-
`territory-pattern-${patternData
|
|
100
|
-
`territory-pattern-${patternData
|
|
99
|
+
`territory-pattern-${patternData?.dataKey}`,
|
|
100
|
+
`territory-pattern-${patternData?.dataKey}--${patternData.dataValue}`
|
|
101
101
|
].join(' ')}
|
|
102
102
|
/>
|
|
103
103
|
<text
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
createShapeProperties
|
|
23
23
|
} from '../helpers/shapes'
|
|
24
24
|
import { getGeoStrokeColor } from '../../../helpers/colors'
|
|
25
|
+
import { handleMapAriaLabels } from '../../../helpers/handleMapAriaLabels'
|
|
25
26
|
|
|
26
27
|
const getCountyTopoURL = year => {
|
|
27
28
|
return `https://www.cdc.gov/TemplatePackage/contrib/data/county-topography/cb_${year}_us_county_20m.json`
|
|
@@ -137,7 +138,6 @@ const CountyMap = props => {
|
|
|
137
138
|
data,
|
|
138
139
|
displayGeoName,
|
|
139
140
|
geoClickHandler,
|
|
140
|
-
handleMapAriaLabels,
|
|
141
141
|
runtimeFilters,
|
|
142
142
|
runtimeLegend,
|
|
143
143
|
setState,
|
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
import { useState, useEffect, memo, useContext } from 'react'
|
|
2
|
-
import { getContrastColor } from '@cdc/core/helpers/cove/accessibility'
|
|
3
2
|
|
|
4
3
|
// 3rd party
|
|
5
4
|
import { geoCentroid } from 'd3-geo'
|
|
6
5
|
import { feature } from 'topojson-client'
|
|
7
6
|
import { Mercator } from '@visx/geo'
|
|
8
7
|
|
|
9
|
-
//
|
|
8
|
+
// Cdc Components
|
|
10
9
|
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
11
|
-
import topoJSON from '../data/us-regions-topo-2.json'
|
|
12
10
|
import ConfigContext from '../../../context'
|
|
13
11
|
import Annotation from '../../Annotation'
|
|
12
|
+
|
|
13
|
+
// Data
|
|
14
|
+
import topoJSON from '../data/us-regions-topo-2.json'
|
|
15
|
+
import { supportedTerritories } from '../../../data/supported-geos'
|
|
16
|
+
|
|
17
|
+
// Helpers
|
|
18
|
+
import { getContrastColor } from '@cdc/core/helpers/cove/accessibility'
|
|
14
19
|
import { getGeoFillColor, getGeoStrokeColor } from '../../../helpers/colors'
|
|
20
|
+
import { handleMapAriaLabels } from '../../../helpers/handleMapAriaLabels'
|
|
15
21
|
|
|
16
22
|
const { features: unitedStates } = feature(topoJSON, topoJSON.objects.regions)
|
|
17
23
|
|
|
@@ -40,9 +46,7 @@ const UsaRegionMap = props => {
|
|
|
40
46
|
data,
|
|
41
47
|
displayGeoName,
|
|
42
48
|
geoClickHandler,
|
|
43
|
-
handleMapAriaLabels,
|
|
44
49
|
state,
|
|
45
|
-
supportedTerritories,
|
|
46
50
|
tooltipId
|
|
47
51
|
} = useContext(ConfigContext)
|
|
48
52
|
|
|
@@ -150,8 +154,8 @@ const UsaRegionMap = props => {
|
|
|
150
154
|
<line
|
|
151
155
|
x1={centroid[0]}
|
|
152
156
|
y1={centroid[1]}
|
|
153
|
-
x2={centroid[0] +
|
|
154
|
-
y2={centroid[1] +
|
|
157
|
+
x2={centroid[0] + x}
|
|
158
|
+
y2={centroid[1] + y}
|
|
155
159
|
stroke='rgba(0,0,0,.5)'
|
|
156
160
|
strokeWidth={1}
|
|
157
161
|
/>
|
|
@@ -161,7 +165,7 @@ const UsaRegionMap = props => {
|
|
|
161
165
|
fontSize={13}
|
|
162
166
|
style={{ fill: '#202020' }}
|
|
163
167
|
alignmentBaseline='middle'
|
|
164
|
-
transform={`translate(${centroid[0] +
|
|
168
|
+
transform={`translate(${centroid[0] + x}, ${centroid[1] + y})`}
|
|
165
169
|
>
|
|
166
170
|
{abbr.substring(3)}
|
|
167
171
|
</text>
|
|
@@ -15,6 +15,8 @@ import { MapContext } from '../../../types/MapContext'
|
|
|
15
15
|
import useStateZoom from '../../../hooks/useStateZoom'
|
|
16
16
|
import { Text } from '@visx/text'
|
|
17
17
|
import { getGeoStrokeColor } from '../../../helpers/colors'
|
|
18
|
+
import { handleMapAriaLabels } from '../../../helpers/handleMapAriaLabels'
|
|
19
|
+
import { titleCase } from '../../../helpers/titleCase'
|
|
18
20
|
|
|
19
21
|
// SVG ITEMS
|
|
20
22
|
const WIDTH = 880
|
|
@@ -29,8 +31,6 @@ const SingleStateMap = props => {
|
|
|
29
31
|
geoClickHandler,
|
|
30
32
|
applyLegendToRow,
|
|
31
33
|
displayGeoName,
|
|
32
|
-
handleMapAriaLabels,
|
|
33
|
-
titleCase,
|
|
34
34
|
setSharedFilterValue,
|
|
35
35
|
isFilterValueSupported,
|
|
36
36
|
runtimeFilters,
|