@cdc/map 2.6.3 → 2.6.4
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 +26 -18
- package/examples/bubble-us.json +363 -0
- package/examples/bubble-world.json +427 -0
- package/examples/default-hex.json +475 -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 +36 -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/default-world-data.json +1444 -0
- package/examples/private/default.json +968 -0
- package/examples/private/map.csv +60 -0
- package/examples/private/mdx.json +210 -0
- package/examples/private/regions.json +52 -0
- package/examples/private/wcmsrd-13881-data.json +2858 -0
- package/examples/private/wcmsrd-13881.json +5823 -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 +340 -79
- package/src/components/BubbleList.js +240 -0
- package/src/components/CityList.js +19 -1
- package/src/components/CountyMap.js +3 -2
- package/src/components/DataTable.js +17 -10
- package/src/components/EditorPanel.js +741 -348
- package/src/components/Geo.js +1 -1
- package/src/components/SingleStateMap.js +1 -1
- package/src/components/UsaMap.js +22 -7
- package/src/components/UsaRegionMap.js +319 -0
- package/src/components/WorldMap.js +112 -35
- package/src/data/country-coordinates.js +250 -0
- package/src/data/initial-state.js +19 -2
- package/src/data/state-coordinates.js +55 -0
- package/src/data/supported-geos.js +91 -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 +10 -2
- package/src/scss/editor-panel.scss +76 -55
- package/src/scss/map.scss +108 -2
- package/src/data/color-palettes.js +0 -200
- package/src/images/map-folded.svg +0 -1
package/src/components/Geo.js
CHANGED
|
@@ -5,7 +5,7 @@ import ErrorBoundary from '@cdc/core/components/ErrorBoundary';
|
|
|
5
5
|
import { geoCentroid, geoPath } from "d3-geo";
|
|
6
6
|
import { feature, mesh } from "topojson-client";
|
|
7
7
|
import { CustomProjection } from '@visx/geo';
|
|
8
|
-
import colorPalettes from '
|
|
8
|
+
import colorPalettes from '../../../core/data/colorPalettes';
|
|
9
9
|
import { geoAlbersUsaTerritories } from 'd3-composite-projections';
|
|
10
10
|
import testJSON from '../data/dfc-map.json';
|
|
11
11
|
|
package/src/components/UsaMap.js
CHANGED
|
@@ -9,6 +9,7 @@ import hexTopoJSON from '../data/us-hex-topo.json';
|
|
|
9
9
|
import { AlbersUsa, Mercator } from '@visx/geo';
|
|
10
10
|
import chroma from 'chroma-js';
|
|
11
11
|
import CityList from './CityList';
|
|
12
|
+
import BubbleList from './BubbleList';
|
|
12
13
|
|
|
13
14
|
const { features: unitedStates } = feature(topoJSON, topoJSON.objects.states)
|
|
14
15
|
const { features: unitedStatesHex } = feature(hexTopoJSON, hexTopoJSON.objects.states)
|
|
@@ -68,7 +69,8 @@ const UsaMap = (props) => {
|
|
|
68
69
|
displayGeoName,
|
|
69
70
|
supportedTerritories,
|
|
70
71
|
rebuildTooltips,
|
|
71
|
-
titleCase
|
|
72
|
+
titleCase,
|
|
73
|
+
handleCircleClick
|
|
72
74
|
} = props;
|
|
73
75
|
|
|
74
76
|
// "Choose State" options
|
|
@@ -218,9 +220,6 @@ const UsaMap = (props) => {
|
|
|
218
220
|
let geoKey = geo.properties.iso;
|
|
219
221
|
|
|
220
222
|
// Manually add Washington D.C. in for Hex maps
|
|
221
|
-
if(isHex && geoKey === 'US-DC') {
|
|
222
|
-
geoKey = 'District of Columbia'
|
|
223
|
-
}
|
|
224
223
|
|
|
225
224
|
if(!geoKey) return
|
|
226
225
|
|
|
@@ -240,13 +239,13 @@ const UsaMap = (props) => {
|
|
|
240
239
|
const tooltip = applyTooltipsToGeo(geoDisplayName, geoData);
|
|
241
240
|
|
|
242
241
|
styles = {
|
|
243
|
-
fill: legendColors[0],
|
|
242
|
+
fill: state.general.type !== 'bubble' ? legendColors[0] : '#E6E6E6',
|
|
244
243
|
cursor: 'default',
|
|
245
244
|
'&:hover': {
|
|
246
|
-
fill: legendColors[1],
|
|
245
|
+
fill: state.general.type !== 'bubble' ? legendColors[1] : '#e6e6e6',
|
|
247
246
|
},
|
|
248
247
|
'&:active': {
|
|
249
|
-
fill: legendColors[2],
|
|
248
|
+
fill: state.general.type !== 'bubble' ? legendColors[2] : '#e6e6e6',
|
|
250
249
|
},
|
|
251
250
|
};
|
|
252
251
|
|
|
@@ -310,6 +309,22 @@ const UsaMap = (props) => {
|
|
|
310
309
|
titleCase={titleCase}
|
|
311
310
|
/>)
|
|
312
311
|
|
|
312
|
+
// Bubbles
|
|
313
|
+
if (state.general.type === 'bubble') {
|
|
314
|
+
geosJsx.push(
|
|
315
|
+
<BubbleList
|
|
316
|
+
key="bubbles"
|
|
317
|
+
data={state.data}
|
|
318
|
+
runtimeData={data}
|
|
319
|
+
state={state}
|
|
320
|
+
projection={projection}
|
|
321
|
+
applyLegendToRow={applyLegendToRow}
|
|
322
|
+
applyTooltipsToGeo={applyTooltipsToGeo}
|
|
323
|
+
displayGeoName={displayGeoName}
|
|
324
|
+
/>
|
|
325
|
+
)
|
|
326
|
+
}
|
|
327
|
+
|
|
313
328
|
return geosJsx;
|
|
314
329
|
};
|
|
315
330
|
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import React, { useState, useEffect, memo } from 'react';
|
|
2
|
+
/** @jsx jsx */
|
|
3
|
+
import { jsx } from '@emotion/react'
|
|
4
|
+
import ErrorBoundary from '@cdc/core/components/ErrorBoundary';
|
|
5
|
+
import { geoCentroid } from "d3-geo";
|
|
6
|
+
import { feature } from "topojson-client";
|
|
7
|
+
import topoJSON from '../data/us-regions-topo-2.json';
|
|
8
|
+
import { Mercator } from '@visx/geo';
|
|
9
|
+
import chroma from 'chroma-js';
|
|
10
|
+
|
|
11
|
+
const { features: unitedStates } = feature(topoJSON, topoJSON.objects.regions)
|
|
12
|
+
|
|
13
|
+
const Rect = ({label, text, stroke, strokeWidth, ...props}) => {
|
|
14
|
+
return (
|
|
15
|
+
<svg viewBox="0 0 45 28">
|
|
16
|
+
<g {...props} strokeLinejoin="round">
|
|
17
|
+
<path stroke={stroke} strokeWidth={strokeWidth} 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" />
|
|
18
|
+
<text textAnchor="middle" dominantBaseline="middle" x="50%" y="54%" fill={text}>{label}</text>
|
|
19
|
+
</g>
|
|
20
|
+
</svg>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const UsaRegionMap = (props) => {
|
|
25
|
+
const {
|
|
26
|
+
state,
|
|
27
|
+
applyTooltipsToGeo,
|
|
28
|
+
data,
|
|
29
|
+
geoClickHandler,
|
|
30
|
+
applyLegendToRow,
|
|
31
|
+
displayGeoName,
|
|
32
|
+
supportedTerritories,
|
|
33
|
+
rebuildTooltips,
|
|
34
|
+
titleCase,
|
|
35
|
+
handleCircleClick
|
|
36
|
+
} = props;
|
|
37
|
+
|
|
38
|
+
// "Choose State" options
|
|
39
|
+
const [extent, setExtent] = useState(null)
|
|
40
|
+
const [focusedStates, setFocusedStates] = useState(unitedStates)
|
|
41
|
+
const [translate, setTranslate] = useState([455,200])
|
|
42
|
+
|
|
43
|
+
// When returning from another map we want to reset the state
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
setTranslate( [455,250] )
|
|
46
|
+
setExtent( null )
|
|
47
|
+
}, [state.general.geoType]);
|
|
48
|
+
|
|
49
|
+
const isHex = state.general.displayAsHex
|
|
50
|
+
|
|
51
|
+
const [territoriesData, setTerritoriesData] = useState([]);
|
|
52
|
+
|
|
53
|
+
const territoriesKeys = Object.keys(supportedTerritories); // data will have already mapped abbreviated territories to their full names
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
// Territories need to show up if they're in the data at all, not just if they're "active". That's why this is different from Cities
|
|
57
|
+
const territoriesList = territoriesKeys.filter(key => data[key]);
|
|
58
|
+
|
|
59
|
+
setTerritoriesData(territoriesList);
|
|
60
|
+
}, [data]);
|
|
61
|
+
|
|
62
|
+
useEffect(() => rebuildTooltips());
|
|
63
|
+
|
|
64
|
+
const geoStrokeColor = state.general.geoBorderColor === 'darkGray' ? 'rgba(0, 0, 0, 0.2)' : 'rgba(255,255,255,0.7)'
|
|
65
|
+
|
|
66
|
+
const territories = territoriesData.map(territory => {
|
|
67
|
+
const Shape = Rect
|
|
68
|
+
|
|
69
|
+
const territoryData = data[territory];
|
|
70
|
+
|
|
71
|
+
let toolTip;
|
|
72
|
+
|
|
73
|
+
let styles = {
|
|
74
|
+
fill: '#E6E6E6',
|
|
75
|
+
color: '#202020',
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const label = supportedTerritories[territory][1]
|
|
79
|
+
|
|
80
|
+
if(!territoryData) return <Shape key={label} label={label} css={styles} text={styles.color} />
|
|
81
|
+
|
|
82
|
+
toolTip = applyTooltipsToGeo(displayGeoName(territory), territoryData);
|
|
83
|
+
|
|
84
|
+
const legendColors = applyLegendToRow(territoryData);
|
|
85
|
+
|
|
86
|
+
let textColor = '#FFF';
|
|
87
|
+
|
|
88
|
+
if (legendColors) {
|
|
89
|
+
// Use white text if the background is dark, and dark grey if it's light
|
|
90
|
+
if (chroma.contrast(textColor, legendColors[0]) < 3.5) {
|
|
91
|
+
textColor = '#202020';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let needsPointer = false;
|
|
95
|
+
|
|
96
|
+
// If we need to add a pointer cursor
|
|
97
|
+
if ((state.columns.navigate && territoryData[state.columns.navigate.name]) || state.tooltips.appearanceType === 'click') {
|
|
98
|
+
needsPointer = true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
styles = {
|
|
102
|
+
color: textColor,
|
|
103
|
+
fill: legendColors[0],
|
|
104
|
+
cursor: needsPointer ? 'pointer' : 'default',
|
|
105
|
+
'&:hover': {
|
|
106
|
+
fill: legendColors[1],
|
|
107
|
+
},
|
|
108
|
+
'&:active': {
|
|
109
|
+
fill: legendColors[2],
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
return (<Shape
|
|
114
|
+
key={label}
|
|
115
|
+
label={label}
|
|
116
|
+
css={styles}
|
|
117
|
+
text={styles.color}
|
|
118
|
+
data-tip={toolTip}
|
|
119
|
+
data-for="tooltip"
|
|
120
|
+
stroke={geoStrokeColor}
|
|
121
|
+
strokeWidth={1.5}
|
|
122
|
+
onClick={() => geoClickHandler(territory, territoryData)}
|
|
123
|
+
/>)
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const geoLabel = (geo, bgColor = "#FFFFFF", projection) => {
|
|
128
|
+
let centroid = projection(geoCentroid(geo))
|
|
129
|
+
let abbr = geo.properties.iso
|
|
130
|
+
|
|
131
|
+
if(undefined === abbr) return null
|
|
132
|
+
|
|
133
|
+
let textColor = "#FFF"
|
|
134
|
+
|
|
135
|
+
// Dynamic text color
|
|
136
|
+
if (chroma.contrast(textColor, bgColor) < 3.5 ) {
|
|
137
|
+
textColor = '#202020';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
let x = 0, y = 5
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<g>
|
|
145
|
+
<line x1={centroid[0]} y1={centroid[1]} x2={centroid[0] + dx} y2={centroid[1] + dy} stroke="rgba(0,0,0,.5)" strokeWidth={1} />
|
|
146
|
+
<text x={4} strokeWidth="0" fontSize={13} style={{fill: "#202020"}} alignmentBaseline="middle" transform={`translate(${centroid[0] + dx}, ${centroid[1] + dy})`}>
|
|
147
|
+
{abbr.substring(3)}
|
|
148
|
+
</text>
|
|
149
|
+
</g>
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Constructs and displays markup for all geos on the map (except territories right now)
|
|
154
|
+
const constructGeoJsx = (geographies, projection) => {
|
|
155
|
+
let showLabel = state.general.displayStateLabels
|
|
156
|
+
|
|
157
|
+
const geosJsx = geographies.map(( {feature: geo, path = '', index}) => {
|
|
158
|
+
const key = isHex ? geo.properties.iso + '-hex-group' : geo.properties.iso + '-group'
|
|
159
|
+
|
|
160
|
+
let styles = {
|
|
161
|
+
fill: '#E6E6E6',
|
|
162
|
+
cursor: 'default'
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Map the name from the geo data with the appropriate key for the processed data
|
|
166
|
+
let geoKey = geo.properties.iso;
|
|
167
|
+
|
|
168
|
+
// Manually add Washington D.C. in for Hex maps
|
|
169
|
+
|
|
170
|
+
if(!geoKey) return
|
|
171
|
+
|
|
172
|
+
const geoData = data[geoKey];
|
|
173
|
+
|
|
174
|
+
let legendColors;
|
|
175
|
+
// Once we receive data for this geographic item, setup variables.
|
|
176
|
+
if (geoData !== undefined) {
|
|
177
|
+
legendColors = applyLegendToRow(geoData);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const geoDisplayName = displayGeoName(geoKey);
|
|
181
|
+
|
|
182
|
+
// If a legend applies, return it with appropriate information.
|
|
183
|
+
if (legendColors && legendColors[0] !== '#000000') {
|
|
184
|
+
const tooltip = applyTooltipsToGeo(geoDisplayName, geoData);
|
|
185
|
+
|
|
186
|
+
styles = {
|
|
187
|
+
fill: state.general.type !== 'bubble' ? legendColors[0] : '#E6E6E6',
|
|
188
|
+
cursor: 'default',
|
|
189
|
+
'&:hover': {
|
|
190
|
+
fill: state.general.type !== 'bubble' ? legendColors[1] : '#e6e6e6',
|
|
191
|
+
},
|
|
192
|
+
'&:active': {
|
|
193
|
+
fill: state.general.type !== 'bubble' ? legendColors[2] : '#e6e6e6',
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// When to add pointer cursor
|
|
198
|
+
if ((state.columns.navigate && geoData[state.columns.navigate.name]) || state.tooltips.appearanceType === 'click') {
|
|
199
|
+
styles.cursor = 'pointer'
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const TerratoryRect = (props) => {
|
|
203
|
+
const { posX = 0, tName } = props
|
|
204
|
+
let textColor = "#fff"
|
|
205
|
+
|
|
206
|
+
if (chroma.contrast(textColor, legendColors[0]) < 4.5) {
|
|
207
|
+
textColor = '#202020';
|
|
208
|
+
}
|
|
209
|
+
return (
|
|
210
|
+
<>
|
|
211
|
+
<rect x={posX} width="36" height="24" rx="6" stroke="#fff" strokeWidth="1" />
|
|
212
|
+
<text x={posX + 8} y="17" fill={textColor}>{tName}</text>
|
|
213
|
+
</>
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const circleRadius = 15;
|
|
218
|
+
|
|
219
|
+
// SIDE CHART EXPERIMENT
|
|
220
|
+
// const height = state.data[index].Change;
|
|
221
|
+
// const barHeight = Math.abs(height * 20 );
|
|
222
|
+
// const barPositive = height > 0;
|
|
223
|
+
// const barY = barPositive ? -barHeight + 15 : 15;
|
|
224
|
+
// const baseY = 14;
|
|
225
|
+
// const barFill = barPositive ? "#fff" : "#fff";
|
|
226
|
+
|
|
227
|
+
return (
|
|
228
|
+
<g
|
|
229
|
+
data-for="tooltip"
|
|
230
|
+
data-tip={tooltip}
|
|
231
|
+
key={key}
|
|
232
|
+
className="geo-group"
|
|
233
|
+
css={styles}
|
|
234
|
+
onClick={() => geoClickHandler(geoDisplayName, geoData)}
|
|
235
|
+
>
|
|
236
|
+
|
|
237
|
+
<path
|
|
238
|
+
tabIndex={-1}
|
|
239
|
+
className='single-geo'
|
|
240
|
+
stroke={geoStrokeColor}
|
|
241
|
+
strokeWidth={1.3}
|
|
242
|
+
d={path}
|
|
243
|
+
/>
|
|
244
|
+
<g id={`region-${index+1}-label`}>
|
|
245
|
+
<circle fill="#fff" stroke="#999" cx={circleRadius} cy={circleRadius} r={circleRadius}/>
|
|
246
|
+
<text fill="#333" x="15px" y="20px" textAnchor="middle">{index+1}</text>
|
|
247
|
+
{/* SIDE CHART EXPERIMENT */}
|
|
248
|
+
{/*<g y={barY*20}>*/}
|
|
249
|
+
{/* <rect x="-20" y={barY} width="10" height={barHeight} fill={barFill} stroke="#333"/>*/}
|
|
250
|
+
{/* <rect x="-23" y={baseY} width="16" height="2" fill="#000" />*/}
|
|
251
|
+
{/*</g>*/}
|
|
252
|
+
{/* / SIDE CHART EXPERIMENT */}
|
|
253
|
+
</g>
|
|
254
|
+
{geoKey === 'region 2' &&
|
|
255
|
+
<g id="region-2-territories">
|
|
256
|
+
<TerratoryRect tName="PR" />
|
|
257
|
+
<TerratoryRect posX={45} tName="VI" />
|
|
258
|
+
</g>
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
{ geoKey === 'region 9' &&
|
|
262
|
+
<g id="region-9-territories">
|
|
263
|
+
<g className="region-9-row1">
|
|
264
|
+
<TerratoryRect tName="AS" />
|
|
265
|
+
<TerratoryRect posX={45} tName="GU" />
|
|
266
|
+
<TerratoryRect posX={90} tName="MP" />
|
|
267
|
+
</g>
|
|
268
|
+
<g className="region-9-row2">
|
|
269
|
+
<TerratoryRect tName="FM" />
|
|
270
|
+
<TerratoryRect posX={45} tName="PW" />
|
|
271
|
+
<TerratoryRect posX={90} tName="MH" />
|
|
272
|
+
</g>
|
|
273
|
+
</g>
|
|
274
|
+
}
|
|
275
|
+
</g>
|
|
276
|
+
)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Default return state, just geo with no additional information
|
|
280
|
+
return (
|
|
281
|
+
<g
|
|
282
|
+
key={key}
|
|
283
|
+
className="geo-group"
|
|
284
|
+
css={styles}
|
|
285
|
+
>
|
|
286
|
+
<path
|
|
287
|
+
tabIndex={-1}
|
|
288
|
+
className='single-geo'
|
|
289
|
+
stroke={geoStrokeColor}
|
|
290
|
+
strokeWidth={1.3}
|
|
291
|
+
d={path}
|
|
292
|
+
/>
|
|
293
|
+
{(isHex || showLabel) && geoLabel(geo, styles.fill, projection)}
|
|
294
|
+
</g>
|
|
295
|
+
)
|
|
296
|
+
});
|
|
297
|
+
return geosJsx;
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
return (
|
|
301
|
+
<ErrorBoundary component="UsaRegionMap">
|
|
302
|
+
<svg viewBox="0 0 880 500">
|
|
303
|
+
|
|
304
|
+
<Mercator data={focusedStates} scale={620} translate={[1500, 735]}>
|
|
305
|
+
{({ features, projection }) => constructGeoJsx(features, projection)}
|
|
306
|
+
</Mercator>
|
|
307
|
+
|
|
308
|
+
</svg>
|
|
309
|
+
{territories.length > 0 && (
|
|
310
|
+
<section className="territories">
|
|
311
|
+
<span className="label">{state.general.territoriesLabel}</span>
|
|
312
|
+
{territories}
|
|
313
|
+
</section>
|
|
314
|
+
)}
|
|
315
|
+
</ErrorBoundary>
|
|
316
|
+
);
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
export default memo(UsaRegionMap)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, { useEffect, memo } from 'react';
|
|
2
2
|
/** @jsx jsx */
|
|
3
3
|
import { jsx } from '@emotion/react'
|
|
4
4
|
import ErrorBoundary from '@cdc/core/components/ErrorBoundary';
|
|
@@ -9,11 +9,43 @@ import topoJSON from '../data/world-topo.json';
|
|
|
9
9
|
import ZoomableGroup from './ZoomableGroup';
|
|
10
10
|
import Geo from './Geo'
|
|
11
11
|
import CityList from './CityList';
|
|
12
|
+
import BubbleList from './BubbleList';
|
|
12
13
|
|
|
13
14
|
const { features: world } = feature(topoJSON, topoJSON.objects.countries)
|
|
14
15
|
|
|
15
16
|
let projection = geoMercator()
|
|
16
17
|
|
|
18
|
+
const WorldMap = (props) => {
|
|
19
|
+
const {
|
|
20
|
+
state,
|
|
21
|
+
applyTooltipsToGeo,
|
|
22
|
+
data,
|
|
23
|
+
geoClickHandler,
|
|
24
|
+
applyLegendToRow,
|
|
25
|
+
displayGeoName,
|
|
26
|
+
supportedCountries,
|
|
27
|
+
rebuildTooltips,
|
|
28
|
+
setState,
|
|
29
|
+
setRuntimeData,
|
|
30
|
+
generateRuntimeData,
|
|
31
|
+
setFilteredCountryCode,
|
|
32
|
+
position,
|
|
33
|
+
setPosition,
|
|
34
|
+
hasZoom
|
|
35
|
+
} = props;
|
|
36
|
+
|
|
37
|
+
// TODO Refactor - state should be set together here to avoid rerenders
|
|
38
|
+
// Resets to original data & zooms out
|
|
39
|
+
const handleReset = (state, setState, setRuntimeData, generateRuntimeData) => {
|
|
40
|
+
let reRun = generateRuntimeData(state)
|
|
41
|
+
setRuntimeData(reRun)
|
|
42
|
+
setState({
|
|
43
|
+
...state,
|
|
44
|
+
focusedCountry: false,
|
|
45
|
+
mapPosition: { coordinates: [0, 30], zoom: 1 }
|
|
46
|
+
})
|
|
47
|
+
setFilteredCountryCode('')
|
|
48
|
+
}
|
|
17
49
|
const handleZoomIn = (position, setPosition) => {
|
|
18
50
|
if (position.zoom >= 4) return;
|
|
19
51
|
setPosition((pos) => ({ ...pos, zoom: pos.zoom * 1.5 }));
|
|
@@ -24,9 +56,9 @@ const handleZoomOut = (position, setPosition) => {
|
|
|
24
56
|
setPosition((pos) => ({ ...pos, zoom: pos.zoom / 1.5 }));
|
|
25
57
|
};
|
|
26
58
|
|
|
27
|
-
const ZoomControls = ({position, setPosition}) => (
|
|
59
|
+
const ZoomControls = ({position, setPosition, state, setState, setRuntimeData, generateRuntimeData}) => (
|
|
28
60
|
<div className="zoom-controls" data-html2canvas-ignore>
|
|
29
|
-
<button onClick={() => handleZoomIn(position, setPosition)}>
|
|
61
|
+
<button onClick={() => handleZoomIn(position, setPosition)} aria-label="Zoom In">
|
|
30
62
|
<svg
|
|
31
63
|
viewBox="0 0 24 24"
|
|
32
64
|
stroke="currentColor"
|
|
@@ -36,7 +68,7 @@ const ZoomControls = ({position, setPosition}) => (
|
|
|
36
68
|
<line x1="5" y1="12" x2="19" y2="12" />
|
|
37
69
|
</svg>
|
|
38
70
|
</button>
|
|
39
|
-
<button onClick={() => handleZoomOut(position, setPosition)}>
|
|
71
|
+
<button onClick={() => handleZoomOut(position, setPosition)} aria-label="Zoom Out">
|
|
40
72
|
<svg
|
|
41
73
|
viewBox="0 0 24 24"
|
|
42
74
|
stroke="currentColor"
|
|
@@ -45,22 +77,20 @@ const ZoomControls = ({position, setPosition}) => (
|
|
|
45
77
|
<line x1="5" y1="12" x2="19" y2="12" />
|
|
46
78
|
</svg>
|
|
47
79
|
</button>
|
|
80
|
+
{state.general.type === 'bubble' &&
|
|
81
|
+
<button onClick={() => handleReset(state, setState, setRuntimeData, generateRuntimeData)} className="reset" aria-label="Reset Zoom and Map Filters">
|
|
82
|
+
Reset Filters
|
|
83
|
+
</button>
|
|
84
|
+
}
|
|
48
85
|
</div>
|
|
49
86
|
);
|
|
50
87
|
|
|
51
|
-
|
|
52
|
-
const {
|
|
53
|
-
state
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
applyLegendToRow,
|
|
58
|
-
displayGeoName,
|
|
59
|
-
supportedCountries,
|
|
60
|
-
rebuildTooltips
|
|
61
|
-
} = props;
|
|
62
|
-
|
|
63
|
-
const [position, setPosition] = useState({ coordinates: [0, 30], zoom: 1 });
|
|
88
|
+
// TODO Refactor - state should be set together here to avoid rerenders
|
|
89
|
+
const handleCircleClick = (country, state, setState, setRuntimeData, generateRuntimeData) => {
|
|
90
|
+
if(!state.general.allowMapZoom) return;
|
|
91
|
+
let newRuntimeData = state.data.filter(item => item[state.columns.geo.name] === country[state.columns.geo.name])
|
|
92
|
+
setFilteredCountryCode(newRuntimeData[0].uid)
|
|
93
|
+
}
|
|
64
94
|
|
|
65
95
|
useEffect(() => rebuildTooltips());
|
|
66
96
|
|
|
@@ -95,7 +125,7 @@ const WorldMap = (props) => {
|
|
|
95
125
|
const strokeWidth = .9
|
|
96
126
|
|
|
97
127
|
// If a legend applies, return it with appropriate information.
|
|
98
|
-
if (legendColors && legendColors[0] !== '#000000') {
|
|
128
|
+
if (legendColors && legendColors[0] !== '#000000' && state.general.type !== 'bubble') {
|
|
99
129
|
const tooltip = applyTooltipsToGeo(geoDisplayName, geoData);
|
|
100
130
|
|
|
101
131
|
styles = {
|
|
@@ -145,29 +175,76 @@ const WorldMap = (props) => {
|
|
|
145
175
|
applyLegendToRow={applyLegendToRow}
|
|
146
176
|
/>)
|
|
147
177
|
|
|
178
|
+
// Bubbles
|
|
179
|
+
if(state.general.type === 'bubble') {
|
|
180
|
+
geosJsx.push(
|
|
181
|
+
<BubbleList
|
|
182
|
+
key="bubbles"
|
|
183
|
+
data={state.data}
|
|
184
|
+
runtimeData={data}
|
|
185
|
+
state={state}
|
|
186
|
+
projection={projection}
|
|
187
|
+
applyLegendToRow={applyLegendToRow}
|
|
188
|
+
applyTooltipsToGeo={applyTooltipsToGeo}
|
|
189
|
+
displayGeoName={displayGeoName}
|
|
190
|
+
handleCircleClick={(country) => handleCircleClick(country, state, setState, setRuntimeData, generateRuntimeData) }
|
|
191
|
+
/>
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
|
|
148
195
|
return geosJsx;
|
|
149
196
|
};
|
|
150
197
|
|
|
151
198
|
return (
|
|
152
199
|
<ErrorBoundary component="WorldMap">
|
|
153
|
-
|
|
154
|
-
<
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
data={world}
|
|
200
|
+
{hasZoom ? (
|
|
201
|
+
<svg viewBox="0 0 880 500">
|
|
202
|
+
<rect height={500} width={880} onClick={() => handleReset(state, setState, setRuntimeData, generateRuntimeData)} fill="white"/>
|
|
203
|
+
<ZoomableGroup
|
|
204
|
+
zoom={position.zoom}
|
|
205
|
+
center={position.coordinates}
|
|
206
|
+
onMoveEnd={handleMoveEnd}
|
|
207
|
+
maxZoom={4}
|
|
208
|
+
projection={projection}
|
|
209
|
+
width={880}
|
|
210
|
+
height={500}
|
|
165
211
|
>
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
212
|
+
<Mercator
|
|
213
|
+
data={world}
|
|
214
|
+
>
|
|
215
|
+
{({ features }) => constructGeoJsx(features)}
|
|
216
|
+
</Mercator>
|
|
217
|
+
</ZoomableGroup>
|
|
218
|
+
</svg>
|
|
219
|
+
) :
|
|
220
|
+
<svg viewBox="0 0 880 500">
|
|
221
|
+
<ZoomableGroup
|
|
222
|
+
zoom={1}
|
|
223
|
+
center={position.coordinates}
|
|
224
|
+
onMoveEnd={handleMoveEnd}
|
|
225
|
+
maxZoom={0}
|
|
226
|
+
projection={projection}
|
|
227
|
+
width={880}
|
|
228
|
+
height={500}
|
|
229
|
+
>
|
|
230
|
+
<Mercator
|
|
231
|
+
data={world}
|
|
232
|
+
>
|
|
233
|
+
{({ features }) => constructGeoJsx(features)}
|
|
234
|
+
</Mercator>
|
|
235
|
+
</ZoomableGroup>
|
|
236
|
+
</svg>
|
|
237
|
+
}
|
|
238
|
+
{(state.general.type === 'data' || state.general.type === 'bubble' && hasZoom) &&
|
|
239
|
+
<ZoomControls
|
|
240
|
+
position={position}
|
|
241
|
+
setPosition={setPosition}
|
|
242
|
+
setRuntimeData={setRuntimeData}
|
|
243
|
+
state={state}
|
|
244
|
+
setState={setState}
|
|
245
|
+
generateRuntimeData={generateRuntimeData} />
|
|
246
|
+
}
|
|
247
|
+
|
|
171
248
|
</ErrorBoundary>
|
|
172
249
|
);
|
|
173
250
|
};
|