@cdc/map 2.6.3 → 9.22.9

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