@cdc/map 2.6.3 → 4.22.10-alpha.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/LICENSE +201 -0
- package/dist/cdcmap.js +32 -18
- package/examples/bubble-us.json +363 -0
- package/examples/bubble-world.json +427 -0
- package/examples/default-county.json +64 -12
- package/examples/default-geocode.json +746 -0
- package/examples/default-hex.json +477 -0
- package/examples/default-usa-regions.json +118 -0
- package/examples/default-usa.json +1 -1
- package/examples/default-world-data.json +1450 -0
- package/examples/default-world.json +5 -3
- package/examples/example-city-state.json +46 -1
- package/examples/gallery/categorical-qualitative.json +797 -0
- package/examples/gallery/categorical-scale-based.json +739 -0
- package/examples/gallery/city-state.json +479 -0
- package/examples/gallery/county.json +22731 -0
- package/examples/gallery/equal-interval.json +1027 -0
- package/examples/gallery/equal-number.json +1027 -0
- package/examples/gallery/filterable.json +909 -0
- package/examples/gallery/hex-filtered.json +420 -0
- package/examples/gallery/hex.json +413 -0
- package/examples/gallery/single-state.json +21402 -0
- package/examples/gallery/world.json +1592 -0
- package/examples/private/atsdr.json +439 -0
- package/examples/private/atsdr_new.json +436 -0
- package/examples/private/bubble.json +285 -0
- package/examples/private/city-state.json +428 -0
- package/examples/private/city-state2.json +434 -0
- package/examples/private/cty-issue.json +42768 -0
- package/examples/private/default-usa.json +460 -0
- package/examples/private/default-world-data.json +1444 -0
- package/examples/private/default.json +968 -0
- package/examples/private/legend-issue.json +1 -0
- package/examples/private/map-rounding-error.json +42759 -0
- package/examples/private/map.csv +60 -0
- package/examples/private/mdx.json +210 -0
- package/examples/private/monkeypox.json +376 -0
- package/examples/private/regions.json +52 -0
- package/examples/private/valid-data-map.csv +59 -0
- package/examples/private/wcmsrd-13881-data.json +2858 -0
- package/examples/private/wcmsrd-13881.json +5823 -0
- package/examples/private/wcmsrd-14492-data.json +292 -0
- package/examples/private/wcmsrd-14492.json +114 -0
- package/examples/private/wcmsrd-test.json +268 -0
- package/examples/private/world.json +1580 -0
- package/examples/private/worldmap.json +1490 -0
- package/package.json +51 -50
- package/src/CdcMap.js +1384 -1075
- package/src/components/BubbleList.js +244 -0
- package/src/components/CityList.js +79 -17
- package/src/components/CountyMap.js +104 -44
- package/src/components/DataTable.js +32 -22
- package/src/components/EditorPanel.js +977 -414
- package/src/components/Geo.js +1 -1
- package/src/components/Modal.js +2 -1
- package/src/components/NavigationMenu.js +4 -3
- package/src/components/Sidebar.js +14 -19
- package/src/components/SingleStateMap.js +178 -249
- package/src/components/UsaMap.js +104 -36
- package/src/components/UsaRegionMap.js +320 -0
- package/src/components/WorldMap.js +117 -34
- package/src/data/country-coordinates.js +250 -0
- package/src/data/{dfc-map.json → county-map.json} +0 -0
- package/src/data/initial-state.js +23 -3
- package/src/data/state-coordinates.js +55 -0
- package/src/data/supported-geos.js +101 -15
- package/src/data/us-regions-topo-2.json +360525 -0
- package/src/data/us-regions-topo.json +37894 -0
- package/src/hooks/useColorPalette.ts +96 -0
- package/src/index.html +8 -4
- package/src/scss/editor-panel.scss +78 -57
- package/src/scss/main.scss +1 -6
- package/src/scss/map.scss +126 -2
- package/src/scss/sidebar.scss +2 -1
- package/src/data/color-palettes.js +0 -200
- package/src/images/map-folded.svg +0 -1
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import React, {memo, useState, useEffect} from 'react'
|
|
2
|
+
import { scaleLinear } from 'd3-scale';
|
|
3
|
+
import { countryCoordinates } from '../data/country-coordinates';
|
|
4
|
+
import stateCoordinates from '../data/state-coordinates';
|
|
5
|
+
import ReactTooltip from 'react-tooltip'
|
|
6
|
+
|
|
7
|
+
export const BubbleList = (
|
|
8
|
+
{
|
|
9
|
+
data: dataImport,
|
|
10
|
+
state,
|
|
11
|
+
projection,
|
|
12
|
+
applyLegendToRow,
|
|
13
|
+
applyTooltipsToGeo,
|
|
14
|
+
handleCircleClick,
|
|
15
|
+
runtimeData,
|
|
16
|
+
displayGeoName
|
|
17
|
+
}) => {
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
ReactTooltip.hide()
|
|
21
|
+
}, [runtimeData]);
|
|
22
|
+
|
|
23
|
+
const maxDataValue = Math.max(...dataImport.map(d => d[state.columns.primary.name]))
|
|
24
|
+
const hasBubblesWithZeroOnMap = state.visual.showBubbleZeros ? 0 : 1;
|
|
25
|
+
// sort runtime data. Smaller bubbles should appear on top.
|
|
26
|
+
const sortedRuntimeData = Object.values(runtimeData).sort((a, b) => a[state.columns.primary.name] < b[state.columns.primary.name] ? 1 : -1 )
|
|
27
|
+
if(!sortedRuntimeData) return;
|
|
28
|
+
|
|
29
|
+
const clickTolerance = 10;
|
|
30
|
+
|
|
31
|
+
// Set bubble sizes
|
|
32
|
+
var size = scaleLinear()
|
|
33
|
+
.domain([hasBubblesWithZeroOnMap, maxDataValue])
|
|
34
|
+
.range([state.visual.minBubbleSize, state.visual.maxBubbleSize])
|
|
35
|
+
|
|
36
|
+
// Start looping through the countries to create the bubbles.
|
|
37
|
+
if(state.general.geoType === 'world') {
|
|
38
|
+
const countries = sortedRuntimeData && sortedRuntimeData.map( (country, index) => {
|
|
39
|
+
|
|
40
|
+
let coordinates = countryCoordinates[country.uid]
|
|
41
|
+
|
|
42
|
+
if(!coordinates) return true;
|
|
43
|
+
|
|
44
|
+
const countryName = displayGeoName(country[state.columns.geo.name]);
|
|
45
|
+
const toolTip = applyTooltipsToGeo(countryName, country);
|
|
46
|
+
const legendColors = applyLegendToRow(country);
|
|
47
|
+
|
|
48
|
+
let primaryKey = state.columns.primary.name
|
|
49
|
+
if ((Math.floor(Number(size(country[primaryKey]))) === 0 || country[primaryKey] === "") && !state.visual.showBubbleZeros) return;
|
|
50
|
+
|
|
51
|
+
let transform = `translate(${projection([coordinates[1], coordinates[0]])})`
|
|
52
|
+
|
|
53
|
+
let pointerX, pointerY;
|
|
54
|
+
|
|
55
|
+
if( !projection(coordinates) ) return true;
|
|
56
|
+
|
|
57
|
+
const circle = (
|
|
58
|
+
<>
|
|
59
|
+
<circle
|
|
60
|
+
key={`circle-${countryName.replace(' ', '')}`}
|
|
61
|
+
data-tip={toolTip}
|
|
62
|
+
data-for="tooltip"
|
|
63
|
+
className={`bubble country--${countryName}`}
|
|
64
|
+
cx={ Number(projection(coordinates[1], coordinates[0])[0]) || 0 } // || 0 handles error on loads where the data isn't ready
|
|
65
|
+
cy={ Number(projection(coordinates[1], coordinates[0])[1]) || 0 }
|
|
66
|
+
r={ Number(size(country[primaryKey])) }
|
|
67
|
+
fill={legendColors[0] }
|
|
68
|
+
stroke={legendColors[0]}
|
|
69
|
+
strokeWidth={1.25}
|
|
70
|
+
fillOpacity={.4}
|
|
71
|
+
onPointerDown={(e) => {
|
|
72
|
+
pointerX = e.clientX;
|
|
73
|
+
pointerY = e.clientY;
|
|
74
|
+
}}
|
|
75
|
+
onPointerUp={(e) => {
|
|
76
|
+
if(pointerX && pointerY &&
|
|
77
|
+
e.clientX > (pointerX - clickTolerance) &&
|
|
78
|
+
e.clientX < (pointerX + clickTolerance) &&
|
|
79
|
+
e.clientY > (pointerY - clickTolerance) &&
|
|
80
|
+
e.clientY < (pointerY + clickTolerance)
|
|
81
|
+
){
|
|
82
|
+
handleCircleClick(country)
|
|
83
|
+
pointerX = undefined;
|
|
84
|
+
pointerY = undefined;
|
|
85
|
+
}
|
|
86
|
+
}}
|
|
87
|
+
transform={transform}
|
|
88
|
+
style={{ transition: 'all .25s ease-in-out', cursor: "pointer" }}
|
|
89
|
+
/>
|
|
90
|
+
|
|
91
|
+
{state.visual.extraBubbleBorder &&
|
|
92
|
+
<circle
|
|
93
|
+
key={`circle-${countryName.replace(' ', '')}`}
|
|
94
|
+
data-tip={toolTip}
|
|
95
|
+
data-for="tooltip"
|
|
96
|
+
className="bubble"
|
|
97
|
+
cx={ Number(projection(coordinates[1], coordinates[0])[0]) || 0 } // || 0 handles error on loads where the data isn't ready
|
|
98
|
+
cy={ Number(projection(coordinates[1], coordinates[0])[1]) || 0 }
|
|
99
|
+
r={ Number(size(country[primaryKey])) + 1 }
|
|
100
|
+
fill={ "transparent" }
|
|
101
|
+
stroke={ "white" }
|
|
102
|
+
strokeWidth={.5}
|
|
103
|
+
onPointerDown={(e) => {
|
|
104
|
+
pointerX = e.clientX;
|
|
105
|
+
pointerY = e.clientY;
|
|
106
|
+
}}
|
|
107
|
+
onPointerUp={(e) => {
|
|
108
|
+
if(pointerX && pointerY &&
|
|
109
|
+
e.clientX > (pointerX - clickTolerance) &&
|
|
110
|
+
e.clientX < (pointerX + clickTolerance) &&
|
|
111
|
+
e.clientY > (pointerY - clickTolerance) &&
|
|
112
|
+
e.clientY < (pointerY + clickTolerance)
|
|
113
|
+
){
|
|
114
|
+
handleCircleClick(country)
|
|
115
|
+
pointerX = undefined;
|
|
116
|
+
pointerY = undefined;
|
|
117
|
+
}
|
|
118
|
+
}}
|
|
119
|
+
transform={transform}
|
|
120
|
+
style={{ transition: 'all .25s ease-in-out', cursor: "pointer" }}
|
|
121
|
+
/>
|
|
122
|
+
}
|
|
123
|
+
</>
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<g key={`group-${countryName.replace(' ', '')}`}>
|
|
129
|
+
{circle}
|
|
130
|
+
</g>
|
|
131
|
+
)
|
|
132
|
+
})
|
|
133
|
+
return countries;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if(state.general.geoType === 'us') {
|
|
137
|
+
const bubbles = sortedRuntimeData && sortedRuntimeData.map( (item, index) => {
|
|
138
|
+
let stateData = stateCoordinates[item.uid]
|
|
139
|
+
let primaryKey = state?.columns?.primary?.name
|
|
140
|
+
if ( Number(size(item[primaryKey])) === 0) return;
|
|
141
|
+
|
|
142
|
+
if (item[primaryKey] === null) item[primaryKey] = ""
|
|
143
|
+
|
|
144
|
+
// Return if hiding zeros on the map
|
|
145
|
+
if( (Math.floor(Number(size(item[primaryKey]))) === 0 || item[primaryKey] === "" )&& !state.visual.showBubbleZeros ) return;
|
|
146
|
+
|
|
147
|
+
if(!stateData) return true;
|
|
148
|
+
let longitude = Number( stateData.Longitude);
|
|
149
|
+
let latitude = Number( stateData.Latitude);
|
|
150
|
+
let coordinates = [longitude, latitude]
|
|
151
|
+
//console.log('projection', projection([longitude, latitude]))
|
|
152
|
+
let stateName = stateData.Name;
|
|
153
|
+
if (!coordinates) return true;
|
|
154
|
+
|
|
155
|
+
stateName = displayGeoName(stateName);
|
|
156
|
+
const toolTip = applyTooltipsToGeo(stateName, item);
|
|
157
|
+
const legendColors = applyLegendToRow(item);
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
let transform = `translate(${projection([coordinates[1], coordinates[0]])})`
|
|
161
|
+
|
|
162
|
+
if ( !projection(coordinates) ) return true;
|
|
163
|
+
|
|
164
|
+
let pointerX, pointerY;
|
|
165
|
+
const circle = (
|
|
166
|
+
<>
|
|
167
|
+
<circle
|
|
168
|
+
key={`circle-${stateName.replace(' ', '')}`}
|
|
169
|
+
data-tip={toolTip}
|
|
170
|
+
data-for="tooltip"
|
|
171
|
+
className="bubble"
|
|
172
|
+
cx={projection(coordinates)[0] || 0} // || 0 handles error on loads where the data isn't ready
|
|
173
|
+
cy={projection(coordinates)[1] || 0}
|
|
174
|
+
r={Number(size(item[primaryKey]))}
|
|
175
|
+
fill={legendColors[0]}
|
|
176
|
+
stroke={legendColors[0]}
|
|
177
|
+
strokeWidth={1.25}
|
|
178
|
+
fillOpacity={.4}
|
|
179
|
+
onPointerDown={(e) => {
|
|
180
|
+
pointerX = e.clientX;
|
|
181
|
+
pointerY = e.clientY;
|
|
182
|
+
}}
|
|
183
|
+
onPointerUp={(e) => {
|
|
184
|
+
if (pointerX && pointerY &&
|
|
185
|
+
e.clientX > (pointerX - clickTolerance) &&
|
|
186
|
+
e.clientX < (pointerX + clickTolerance) &&
|
|
187
|
+
e.clientY > (pointerY - clickTolerance) &&
|
|
188
|
+
e.clientY < (pointerY + clickTolerance)
|
|
189
|
+
) {
|
|
190
|
+
handleCircleClick(state)
|
|
191
|
+
pointerX = undefined;
|
|
192
|
+
pointerY = undefined;
|
|
193
|
+
}
|
|
194
|
+
}}
|
|
195
|
+
transform={transform}
|
|
196
|
+
style={{ transition: 'all .25s ease-in-out', cursor: "pointer" }}
|
|
197
|
+
/>
|
|
198
|
+
{ state.visual.extraBubbleBorder &&
|
|
199
|
+
<circle
|
|
200
|
+
key={`circle-${stateName.replace(' ', '')}`}
|
|
201
|
+
data-tip={toolTip}
|
|
202
|
+
data-for="tooltip"
|
|
203
|
+
className="bubble"
|
|
204
|
+
cx={ projection(coordinates)[0] || 0 } // || 0 handles error on loads where the data isn't ready
|
|
205
|
+
cy={ projection(coordinates)[1] || 0 }
|
|
206
|
+
r={ Number(size(item[primaryKey])) + 1 }
|
|
207
|
+
fill={"transparent"}
|
|
208
|
+
stroke={"white"}
|
|
209
|
+
strokeWidth={.5}
|
|
210
|
+
fillOpacity={.4}
|
|
211
|
+
onPointerDown={(e) => {
|
|
212
|
+
pointerX = e.clientX;
|
|
213
|
+
pointerY = e.clientY;
|
|
214
|
+
}}
|
|
215
|
+
onPointerUp={(e) => {
|
|
216
|
+
if (pointerX && pointerY &&
|
|
217
|
+
e.clientX > (pointerX - clickTolerance) &&
|
|
218
|
+
e.clientX < (pointerX + clickTolerance) &&
|
|
219
|
+
e.clientY > (pointerY - clickTolerance) &&
|
|
220
|
+
e.clientY < (pointerY + clickTolerance)
|
|
221
|
+
) {
|
|
222
|
+
handleCircleClick(state)
|
|
223
|
+
pointerX = undefined;
|
|
224
|
+
pointerY = undefined;
|
|
225
|
+
}
|
|
226
|
+
}}
|
|
227
|
+
transform={transform}
|
|
228
|
+
style={{ transition: 'all .25s ease-in-out', cursor: "pointer" }}
|
|
229
|
+
/>
|
|
230
|
+
}
|
|
231
|
+
</>
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
return (
|
|
236
|
+
<g key={`group-${stateName.replace(' ', '')}`}>
|
|
237
|
+
{circle}
|
|
238
|
+
</g>
|
|
239
|
+
)
|
|
240
|
+
})
|
|
241
|
+
return bubbles;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
export default BubbleList
|
|
@@ -2,6 +2,8 @@ import React, { useState, useEffect, useContext } from 'react';
|
|
|
2
2
|
/** @jsx jsx */
|
|
3
3
|
import { jsx } from '@emotion/react'
|
|
4
4
|
import { supportedCities } from '../data/supported-geos';
|
|
5
|
+
import { scaleLinear } from 'd3-scale';
|
|
6
|
+
import ReactTooltip from 'react-tooltip';
|
|
5
7
|
|
|
6
8
|
const CityList = (({
|
|
7
9
|
data,
|
|
@@ -11,26 +13,56 @@ const CityList = (({
|
|
|
11
13
|
displayGeoName,
|
|
12
14
|
applyLegendToRow,
|
|
13
15
|
projection,
|
|
14
|
-
titleCase
|
|
16
|
+
titleCase,
|
|
17
|
+
setSharedFilterValue,
|
|
18
|
+
isFilterValueSupported,
|
|
19
|
+
isGeoCodeMap
|
|
15
20
|
}) => {
|
|
21
|
+
|
|
16
22
|
const [citiesData, setCitiesData] = useState({});
|
|
17
23
|
|
|
18
24
|
useEffect(() => {
|
|
19
|
-
|
|
25
|
+
ReactTooltip.rebuild()
|
|
26
|
+
});
|
|
20
27
|
|
|
21
|
-
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if(!isGeoCodeMap) {
|
|
30
|
+
const citiesList = Object.keys(data).filter((item) => Object.keys(supportedCities).includes(item));
|
|
31
|
+
|
|
32
|
+
const citiesDictionary = {};
|
|
33
|
+
|
|
34
|
+
citiesList.map((city) => citiesDictionary[city] = data[city]);
|
|
35
|
+
|
|
36
|
+
setCitiesData(citiesDictionary);
|
|
37
|
+
} else {
|
|
38
|
+
const citiesDictionary = {};
|
|
39
|
+
state.data.map(city => citiesDictionary[city[state.columns.geo.name]] = city)
|
|
40
|
+
setCitiesData(citiesDictionary);
|
|
41
|
+
}
|
|
42
|
+
}, [data, state.data]);
|
|
22
43
|
|
|
23
|
-
|
|
44
|
+
if (state.general.type === 'bubble') {
|
|
45
|
+
const maxDataValue = Math.max(...state.data.map(d => d[state.columns.primary.name]))
|
|
46
|
+
const sortedRuntimeData = Object.values(data).sort((a, b) => a[state.columns.primary.name] < b[state.columns.primary.name] ? 1 : -1)
|
|
47
|
+
if (!sortedRuntimeData) return;
|
|
24
48
|
|
|
25
|
-
|
|
26
|
-
|
|
49
|
+
// Set bubble sizes
|
|
50
|
+
var size = scaleLinear()
|
|
51
|
+
.domain([1, maxDataValue])
|
|
52
|
+
.range([state.visual.minBubbleSize, state.visual.maxBubbleSize])
|
|
27
53
|
|
|
28
|
-
|
|
54
|
+
}
|
|
55
|
+
let cityList = isGeoCodeMap ? Object.keys(citiesData).filter((c) => undefined !== c) : Object.keys(citiesData).filter((c) => undefined !== data[c]);
|
|
56
|
+
if(!cityList) return true;
|
|
29
57
|
|
|
58
|
+
// Cities output
|
|
30
59
|
const cities = cityList.map((city, i) => {
|
|
31
|
-
const cityDisplayName = titleCase( displayGeoName(city) );
|
|
32
60
|
|
|
33
|
-
const
|
|
61
|
+
const geoData = isGeoCodeMap ? state.data.filter(item => city === item[state.columns.geo.name])[0] : data[city];
|
|
62
|
+
|
|
63
|
+
const cityDisplayName = isGeoCodeMap ? city : titleCase( displayGeoName(city) );
|
|
64
|
+
|
|
65
|
+
const legendColors = (isGeoCodeMap && geoData) ? applyLegendToRow(geoData) : data[city] ? applyLegendToRow(data[city]) : false;
|
|
34
66
|
|
|
35
67
|
if (legendColors === false) {
|
|
36
68
|
return true;
|
|
@@ -38,8 +70,8 @@ const CityList = (({
|
|
|
38
70
|
|
|
39
71
|
const styles = {
|
|
40
72
|
fill: legendColors[0],
|
|
41
|
-
|
|
42
|
-
stroke: 'rgba(0, 0, 0, 0.4)',
|
|
73
|
+
opacity: setSharedFilterValue && isFilterValueSupported && data[city][state.columns.geo.name] !== setSharedFilterValue ? .5 : 1,
|
|
74
|
+
stroke: setSharedFilterValue && isFilterValueSupported && data[city][state.columns.geo.name] === setSharedFilterValue ? 'rgba(0, 0, 0, 1)' : 'rgba(0, 0, 0, 0.4)',
|
|
43
75
|
'&:hover': {
|
|
44
76
|
fill: legendColors[1],
|
|
45
77
|
outline: 0
|
|
@@ -50,16 +82,20 @@ const CityList = (({
|
|
|
50
82
|
}
|
|
51
83
|
};
|
|
52
84
|
|
|
53
|
-
|
|
85
|
+
|
|
54
86
|
|
|
55
87
|
const toolTip = applyTooltipsToGeo(cityDisplayName, data[city]);
|
|
56
88
|
|
|
57
89
|
// If we need to add a cursor pointer
|
|
58
|
-
if ((state.columns.navigate && geoData[state.columns.navigate.name]) || state.tooltips.appearanceType === 'click') {
|
|
90
|
+
if ((state.columns.navigate && (geoData?.[state.columns.navigate.name] && geoData[state.columns.navigate.name]) ) || state.tooltips.appearanceType === 'click') {
|
|
59
91
|
styles.cursor = 'pointer'
|
|
60
92
|
}
|
|
61
93
|
|
|
62
|
-
const radius = state.general.geoType === 'us' ? 8 : 4;
|
|
94
|
+
const radius = state.general.geoType === 'us' && !isGeoCodeMap ? 8 : isGeoCodeMap ? 2 : 4;
|
|
95
|
+
|
|
96
|
+
const additionalProps = {
|
|
97
|
+
fillOpacity: state.general.type === 'bubble' ? .4 : 1
|
|
98
|
+
}
|
|
63
99
|
|
|
64
100
|
const circle = (
|
|
65
101
|
<circle
|
|
@@ -67,13 +103,38 @@ const CityList = (({
|
|
|
67
103
|
data-for="tooltip"
|
|
68
104
|
cx={0}
|
|
69
105
|
cy={0}
|
|
70
|
-
r={radius}
|
|
106
|
+
r={ state.general.type === 'bubble' ? size(geoData[state.columns.primary.name]) : radius}
|
|
71
107
|
title="Click for more information"
|
|
72
108
|
onClick={() => geoClickHandler(cityDisplayName, geoData)}
|
|
109
|
+
{...additionalProps}
|
|
73
110
|
/>
|
|
74
111
|
);
|
|
75
112
|
|
|
76
|
-
|
|
113
|
+
const pin = (
|
|
114
|
+
<path
|
|
115
|
+
className="marker"
|
|
116
|
+
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"
|
|
117
|
+
title="Click for more information"
|
|
118
|
+
onClick={() => geoClickHandler(cityDisplayName, geoData)}
|
|
119
|
+
data-tip={toolTip}
|
|
120
|
+
data-for="tooltip"
|
|
121
|
+
strokeWidth={2}
|
|
122
|
+
stroke={'black'}
|
|
123
|
+
{...additionalProps}
|
|
124
|
+
>
|
|
125
|
+
</path>
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
let transform = '';
|
|
129
|
+
|
|
130
|
+
if (!isGeoCodeMap) {
|
|
131
|
+
transform = `translate(${projection(supportedCities[city])})`
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (isGeoCodeMap) {
|
|
135
|
+
let coords = [Number(geoData?.[state.columns.longitude.name]), Number(geoData?.[state.columns.latitude.name])]
|
|
136
|
+
transform = `translate(${projection(coords)})`
|
|
137
|
+
}
|
|
77
138
|
|
|
78
139
|
return (
|
|
79
140
|
<g
|
|
@@ -82,7 +143,8 @@ const CityList = (({
|
|
|
82
143
|
css={styles}
|
|
83
144
|
className="geo-point"
|
|
84
145
|
>
|
|
85
|
-
{circle}
|
|
146
|
+
{state.visual.cityStyle === 'circle' && circle }
|
|
147
|
+
{state.visual.cityStyle === 'pin' && pin }
|
|
86
148
|
</g>
|
|
87
149
|
);
|
|
88
150
|
});
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import React, { useState, useEffect, memo, useRef } from 'react';
|
|
2
|
+
import Loading from '@cdc/core/components/Loading';
|
|
2
3
|
/** @jsx jsx */
|
|
3
4
|
import { jsx } from '@emotion/react';
|
|
4
5
|
import ErrorBoundary from '@cdc/core/components/ErrorBoundary';
|
|
5
6
|
import { geoCentroid, geoPath } from 'd3-geo';
|
|
6
7
|
import { feature, mesh } from 'topojson-client';
|
|
7
8
|
import { CustomProjection } from '@visx/geo';
|
|
8
|
-
import colorPalettes from '
|
|
9
|
+
import colorPalettes from '../../../core/data/colorPalettes'
|
|
9
10
|
import { geoAlbersUsaTerritories } from 'd3-composite-projections';
|
|
10
|
-
import testJSON from '../data/
|
|
11
|
+
import testJSON from '../data/county-map.json';
|
|
11
12
|
import { abbrs } from '../data/abbreviations';
|
|
13
|
+
import CityList from './CityList';
|
|
12
14
|
|
|
13
15
|
const offsets = {
|
|
14
16
|
Vermont: [50, -8],
|
|
@@ -43,31 +45,21 @@ const STATE_INACTIVE_FILL = '#F4F7FA';
|
|
|
43
45
|
const projection = geoAlbersUsaTerritories().translate([WIDTH / 2, HEIGHT / 2]);
|
|
44
46
|
const path = geoPath().projection(projection);
|
|
45
47
|
const stateLines = path(mesh(testJSON, testJSON.objects.states));
|
|
48
|
+
const countyLines = path(mesh(testJSON, testJSON.objects.counties));
|
|
46
49
|
|
|
47
|
-
const nudges = {
|
|
48
|
-
'US-FL': [15, 3],
|
|
49
|
-
'US-AK': [0, -8],
|
|
50
|
-
'US-CA': [-10, 0],
|
|
51
|
-
'US-NY': [5, 0],
|
|
52
|
-
'US-MI': [13, 20],
|
|
53
|
-
'US-LA': [-10, -3],
|
|
54
|
-
'US-HI': [-10, 10],
|
|
55
|
-
'US-ID': [0, 10],
|
|
56
|
-
'US-WV': [-2, 2],
|
|
57
|
-
};
|
|
58
50
|
|
|
59
51
|
function CountyMapChecks(prevState, nextState) {
|
|
52
|
+
const equalNumberOptIn = prevState.state.general.equalNumberOptIn && nextState.state.general.equalNumberOptIn;
|
|
60
53
|
const equalColumnName = prevState.state.general.type && nextState.state.general.type;
|
|
61
54
|
const equalNavColumn = prevState.state.columns.navigate && nextState.state.columns.navigate;
|
|
62
55
|
const equalLegend = prevState.runtimeLegend === nextState.runtimeLegend;
|
|
63
56
|
const equalBorderColors = prevState.state.general.geoBorderColor === nextState.state.general.geoBorderColor; // update when geoborder color changes
|
|
64
57
|
const equalMapColors = prevState.state.color === nextState.state.color; // update when map colors change
|
|
65
58
|
const equalData = prevState.data === nextState.data; // update when data changes
|
|
66
|
-
return equalMapColors && equalData && equalBorderColors && equalLegend && equalColumnName && equalNavColumn ? true : false;
|
|
59
|
+
return equalMapColors && equalData && equalBorderColors && equalLegend && equalColumnName && equalNavColumn && equalNumberOptIn ? true : false;
|
|
67
60
|
}
|
|
68
61
|
|
|
69
62
|
const CountyMap = (props) => {
|
|
70
|
-
|
|
71
63
|
let mapData = states.concat(counties);
|
|
72
64
|
|
|
73
65
|
const {
|
|
@@ -79,6 +71,10 @@ const CountyMap = (props) => {
|
|
|
79
71
|
displayGeoName,
|
|
80
72
|
rebuildTooltips,
|
|
81
73
|
containerEl,
|
|
74
|
+
handleMapAriaLabels,
|
|
75
|
+
titleCase,
|
|
76
|
+
setSharedFilterValue,
|
|
77
|
+
isFilterValueSupported
|
|
82
78
|
} = props;
|
|
83
79
|
|
|
84
80
|
useEffect(() => {
|
|
@@ -213,27 +209,42 @@ const CountyMap = (props) => {
|
|
|
213
209
|
};
|
|
214
210
|
|
|
215
211
|
const onReset = (e) => {
|
|
216
|
-
e.preventDefault();
|
|
217
|
-
const svg = document.querySelector('.svg-container')
|
|
218
212
|
|
|
219
|
-
|
|
213
|
+
if (state.general.type !== 'us-geocode') {
|
|
214
|
+
e.preventDefault();
|
|
215
|
+
const svg = document.querySelector('.svg-container')
|
|
220
216
|
|
|
217
|
+
svg.setAttribute('data-scaleZoom', 0)
|
|
221
218
|
|
|
222
|
-
const allStates = document.querySelectorAll('.state path');
|
|
223
|
-
const allCounties = document.querySelectorAll('.county path');
|
|
224
219
|
|
|
225
|
-
|
|
226
|
-
|
|
220
|
+
const allStates = document.querySelectorAll('.state path');
|
|
221
|
+
const allCounties = document.querySelectorAll('.county path');
|
|
222
|
+
|
|
223
|
+
stateLinesPath.current.setAttribute('stroke', geoStrokeColor);
|
|
224
|
+
stateLinesPath.current.setAttribute('stroke-width', startingLineWidth);
|
|
227
225
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
226
|
+
let otherStates = document.querySelectorAll(`.state--inactive`);
|
|
227
|
+
otherStates.forEach((el) => (el.style.display = 'none'));
|
|
228
|
+
allCounties.forEach((el) => (el.style.strokeWidth = 0.85));
|
|
229
|
+
allStates.forEach((state) => state.setAttribute('stroke-width', .75 / .85));
|
|
232
230
|
|
|
233
|
-
|
|
231
|
+
mapGroup.current.setAttribute('transform', `translate(${[0, 0]}) scale(${0.85})`);
|
|
234
232
|
|
|
235
|
-
|
|
236
|
-
|
|
233
|
+
// reset button
|
|
234
|
+
resetButton.current.style.display = 'none';
|
|
235
|
+
} else {
|
|
236
|
+
const svg = document.querySelector('.svg-container')
|
|
237
|
+
const allStates = document.querySelectorAll('.state');
|
|
238
|
+
document.querySelector('#focusedBorder path').style.stroke = 'none';
|
|
239
|
+
allStates.forEach(item => item.classList.remove('state--inactive'))
|
|
240
|
+
//document.querySelectorAll('.state path').forEach(item => item.style.fill = 'rgb(244, 247, 250)')
|
|
241
|
+
document.querySelectorAll('.state').forEach(item => item.style.display = 'block')
|
|
242
|
+
stateLinesPath.current.setAttribute('stroke', geoStrokeColor);
|
|
243
|
+
stateLinesPath.current.setAttribute('stroke-width', startingLineWidth);
|
|
244
|
+
svg.setAttribute('data-scaleZoom', 0)
|
|
245
|
+
mapGroup.current.setAttribute('transform', `translate(${[0, 0]}) scale(${0.85})`);
|
|
246
|
+
resetButton.current.style.display = 'none';
|
|
247
|
+
}
|
|
237
248
|
};
|
|
238
249
|
|
|
239
250
|
function setStateLeave() {
|
|
@@ -259,11 +270,11 @@ const CountyMap = (props) => {
|
|
|
259
270
|
focusedBorderPath.current.setAttribute('d', focusedStateLine);
|
|
260
271
|
focusedBorderPath.current.setAttribute('stroke', '#000');
|
|
261
272
|
|
|
262
|
-
if(scale) {
|
|
263
|
-
allStates.forEach(
|
|
264
|
-
focusedBorderPath.current.setAttribute('stroke-width', 0.75 / scale
|
|
273
|
+
if (scale) {
|
|
274
|
+
allStates.forEach(state => state.setAttribute('stroke-width', 0.75 / scale))
|
|
275
|
+
focusedBorderPath.current.setAttribute('stroke-width', 0.75 / scale);
|
|
265
276
|
}
|
|
266
|
-
|
|
277
|
+
|
|
267
278
|
}
|
|
268
279
|
|
|
269
280
|
const StateLines = memo(({ stateLines, lineWidth, geoStrokeColor }) => {
|
|
@@ -422,6 +433,12 @@ const CountyMap = (props) => {
|
|
|
422
433
|
return output;
|
|
423
434
|
});
|
|
424
435
|
|
|
436
|
+
const GeoCodeCountyLines = memo(() => {
|
|
437
|
+
return (
|
|
438
|
+
<path d={countyLines} className="county-borders" style={{ stroke: geoStrokeColor}} />
|
|
439
|
+
)
|
|
440
|
+
})
|
|
441
|
+
|
|
425
442
|
const StateOutput = memo(({ geographies, states }) => {
|
|
426
443
|
let output = [];
|
|
427
444
|
output.push(
|
|
@@ -444,13 +461,25 @@ const CountyMap = (props) => {
|
|
|
444
461
|
|
|
445
462
|
const geoDisplayName = displayGeoName(geoKey);
|
|
446
463
|
|
|
447
|
-
let stateStyles = {
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
464
|
+
let stateStyles = {}
|
|
465
|
+
|
|
466
|
+
if (state.general.type !== 'us-geocode') {
|
|
467
|
+
stateStyles = {
|
|
468
|
+
cursor: 'default',
|
|
469
|
+
stroke: STATE_BORDER,
|
|
470
|
+
strokeWidth: 0.75 / scale,
|
|
471
|
+
display: !focusedState ? 'none' : focusedState && focusedState !== geo.id ? 'block' : 'none',
|
|
472
|
+
fill: focusedState && focusedState !== geo.id ? STATE_INACTIVE_FILL : 'none',
|
|
473
|
+
};
|
|
474
|
+
} else {
|
|
475
|
+
stateStyles = {
|
|
476
|
+
cursor: 'default',
|
|
477
|
+
stroke: STATE_BORDER,
|
|
478
|
+
strokeWidth: 0.75 / scale,
|
|
479
|
+
display: 'block',
|
|
480
|
+
fill: '#f4f7fa',
|
|
481
|
+
};
|
|
482
|
+
}
|
|
454
483
|
|
|
455
484
|
let stateSelectedStyles = {
|
|
456
485
|
fillOpacity: 1,
|
|
@@ -501,7 +530,14 @@ const CountyMap = (props) => {
|
|
|
501
530
|
const states = geographies.slice(0, 56);
|
|
502
531
|
const counties = geographies.slice(56);
|
|
503
532
|
let geosJsx = [];
|
|
504
|
-
|
|
533
|
+
{
|
|
534
|
+
'us-geocode' !== state.general.type &&
|
|
535
|
+
geosJsx.push(<CountyOutput geographies={geographies} counties={counties} key="county-key" />);
|
|
536
|
+
}
|
|
537
|
+
{
|
|
538
|
+
'us-geocode' === state.general.type &&
|
|
539
|
+
geosJsx.push(<GeoCodeCountyLines />);
|
|
540
|
+
}
|
|
505
541
|
geosJsx.push(<StateOutput geographies={geographies} states={states} key="state-key" />);
|
|
506
542
|
geosJsx.push(
|
|
507
543
|
<StateLines
|
|
@@ -512,12 +548,34 @@ const CountyMap = (props) => {
|
|
|
512
548
|
/>
|
|
513
549
|
);
|
|
514
550
|
geosJsx.push(<FocusedStateBorder key="focused-border-key" />);
|
|
551
|
+
geosJsx.push(<CityList
|
|
552
|
+
projection={projection}
|
|
553
|
+
key="cities"
|
|
554
|
+
data={data}
|
|
555
|
+
state={state}
|
|
556
|
+
geoClickHandler={geoClickHandler}
|
|
557
|
+
applyTooltipsToGeo={applyTooltipsToGeo}
|
|
558
|
+
displayGeoName={displayGeoName}
|
|
559
|
+
applyLegendToRow={applyLegendToRow}
|
|
560
|
+
titleCase={titleCase}
|
|
561
|
+
setSharedFilterValue={setSharedFilterValue}
|
|
562
|
+
isFilterValueSupported={isFilterValueSupported}
|
|
563
|
+
isGeoCodeMap={state.general.type === 'us-geocode'}
|
|
564
|
+
/>)
|
|
515
565
|
return geosJsx;
|
|
516
566
|
};
|
|
517
|
-
|
|
567
|
+
if(!data) <Loading />
|
|
518
568
|
return (
|
|
519
569
|
<ErrorBoundary component='CountyMap'>
|
|
520
|
-
<svg
|
|
570
|
+
<svg
|
|
571
|
+
viewBox={`0 0 ${WIDTH} ${HEIGHT}`}
|
|
572
|
+
preserveAspectRatio='xMinYMin'
|
|
573
|
+
className='svg-container'
|
|
574
|
+
data-scale={scale ? scale : ''}
|
|
575
|
+
data-translate={translate ? translate : ''}
|
|
576
|
+
role="img"
|
|
577
|
+
aria-label={handleMapAriaLabels(state)}
|
|
578
|
+
>
|
|
521
579
|
<rect
|
|
522
580
|
className='background center-container ocean'
|
|
523
581
|
width={WIDTH}
|
|
@@ -540,12 +598,14 @@ const CountyMap = (props) => {
|
|
|
540
598
|
transform={`translate(${translate}) scale(${scale})`}
|
|
541
599
|
key='countyMapGroup'
|
|
542
600
|
>
|
|
543
|
-
{constructGeoJsx(features,
|
|
601
|
+
{constructGeoJsx(features, projection)}
|
|
544
602
|
</g>
|
|
545
603
|
);
|
|
546
604
|
}}
|
|
547
605
|
</CustomProjection>
|
|
548
606
|
</svg>
|
|
607
|
+
|
|
608
|
+
{/* TODO: Refactor to COVE button */}
|
|
549
609
|
<button className={`btn btn--reset`} onClick={onReset} ref={resetButton} style={{ display: 'none' }} tabIndex="0">
|
|
550
610
|
Reset Zoom
|
|
551
611
|
</button>
|