@cdc/map 4.23.2 → 4.23.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 +24661 -24191
- package/examples/custom-map-layers.json +764 -0
- package/examples/default-county.json +169 -155
- package/examples/default-geocode.json +744 -744
- package/examples/default-hex.json +3 -5
- package/examples/example-city-state-no-territories.json +703 -0
- package/examples/example-city-state.json +26 -7
- package/examples/example-city-stateBAD.json +744 -0
- package/examples/gallery/city-state.json +478 -478
- package/examples/testing-layer-2.json +1 -0
- package/examples/testing-layer.json +96 -0
- package/examples/world-geocode-data.json +18 -0
- package/examples/world-geocode.json +108 -0
- package/index.html +35 -29
- package/package.json +6 -3
- package/src/CdcMap.jsx +179 -111
- package/src/components/CityList.jsx +35 -35
- package/src/components/CountyMap.jsx +309 -446
- package/src/components/DataTable.jsx +7 -31
- package/src/components/EditorPanel.jsx +468 -217
- package/src/components/Sidebar.jsx +2 -0
- package/src/components/UsaMap.jsx +34 -23
- package/src/components/WorldMap.jsx +40 -8
- package/src/data/feature-test.json +73 -0
- package/src/data/initial-state.js +10 -3
- package/src/data/supported-geos.js +7 -7
- package/src/hooks/useMapLayers.jsx +243 -0
- package/src/index.jsx +4 -8
- package/src/scss/editor-panel.scss +97 -97
- package/src/scss/filters.scss +0 -2
- package/src/scss/main.scss +25 -26
- package/src/scss/map.scss +12 -0
- package/src/test/CdcMap.test.jsx +19 -0
- package/vite.config.js +3 -3
- package/src/components/Filters.jsx +0 -113
- package/src/hooks/useColorPalette.ts +0 -88
|
@@ -22,6 +22,8 @@ const Sidebar = props => {
|
|
|
22
22
|
|
|
23
23
|
newLegend['disabledAmt'] = newValue ? disabledAmt + 1 : disabledAmt - 1
|
|
24
24
|
|
|
25
|
+
newLegend.runtimeDataHash = runtimeLegend.runtimeDataHash
|
|
26
|
+
|
|
25
27
|
setRuntimeLegend(newLegend)
|
|
26
28
|
|
|
27
29
|
setAccessibleStatus(`Disabled legend item ${legendLabel ?? ''}. Please reference the data table to see updated values.`)
|
|
@@ -2,7 +2,7 @@ import React, { useState, useEffect, memo } from 'react'
|
|
|
2
2
|
|
|
3
3
|
import { jsx } from '@emotion/react'
|
|
4
4
|
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
5
|
-
import { geoCentroid } from 'd3-geo'
|
|
5
|
+
import { geoCentroid, geoPath } from 'd3-geo'
|
|
6
6
|
import { feature } from 'topojson-client'
|
|
7
7
|
import topoJSON from '../data/us-topo.json'
|
|
8
8
|
import hexTopoJSON from '../data/us-hex-topo.json'
|
|
@@ -11,6 +11,9 @@ import chroma from 'chroma-js'
|
|
|
11
11
|
import CityList from './CityList'
|
|
12
12
|
import BubbleList from './BubbleList'
|
|
13
13
|
import { supportedCities, supportedStates } from '../data/supported-geos'
|
|
14
|
+
import { geoAlbersUsa } from 'd3-composite-projections'
|
|
15
|
+
|
|
16
|
+
import useMapLayers from '../hooks/useMapLayers'
|
|
14
17
|
|
|
15
18
|
const { features: unitedStates } = feature(topoJSON, topoJSON.objects.states)
|
|
16
19
|
const { features: unitedStatesHex } = feature(hexTopoJSON, hexTopoJSON.objects.states)
|
|
@@ -79,6 +82,7 @@ const UsaMap = props => {
|
|
|
79
82
|
isFilterValueSupported = true
|
|
80
83
|
}
|
|
81
84
|
})
|
|
85
|
+
|
|
82
86
|
Object.keys(supportedTerritories).forEach(supportedTerritory => {
|
|
83
87
|
if (supportedTerritories[supportedTerritory].indexOf(setSharedFilterValue.toUpperCase()) !== -1) {
|
|
84
88
|
isFilterValueSupported = true
|
|
@@ -109,11 +113,15 @@ const UsaMap = props => {
|
|
|
109
113
|
const territoriesKeys = Object.keys(supportedTerritories) // data will have already mapped abbreviated territories to their full names
|
|
110
114
|
|
|
111
115
|
useEffect(() => {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
116
|
+
if (state.general.territoriesAlwaysShow) {
|
|
117
|
+
// show all Territories whether in the data or not
|
|
118
|
+
setTerritoriesData(territoriesKeys)
|
|
119
|
+
} else {
|
|
120
|
+
// 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
|
|
121
|
+
const territoriesList = territoriesKeys.filter(key => data[key])
|
|
122
|
+
setTerritoriesData(territoriesList)
|
|
123
|
+
}
|
|
124
|
+
}, [data, state.general.territoriesAlwaysShow])
|
|
117
125
|
|
|
118
126
|
const geoStrokeColor = state.general.geoBorderColor === 'darkGray' ? 'rgba(0, 0, 0, 0.2)' : 'rgba(255,255,255,0.7)'
|
|
119
127
|
|
|
@@ -166,10 +174,7 @@ const UsaMap = props => {
|
|
|
166
174
|
}
|
|
167
175
|
}
|
|
168
176
|
|
|
169
|
-
return <Shape key={label} label={label} css={styles} text={styles.color} strokeWidth={1.5} textColor={textColor} onClick={() => geoClickHandler(territory, territoryData)}
|
|
170
|
-
data-tooltip-id="tooltip"
|
|
171
|
-
data-tooltip-html={toolTip}
|
|
172
|
-
/>
|
|
177
|
+
return <Shape key={label} label={label} css={styles} text={styles.color} strokeWidth={1.5} textColor={textColor} onClick={() => geoClickHandler(territory, territoryData)} data-tooltip-id='tooltip' data-tooltip-html={toolTip} />
|
|
173
178
|
}
|
|
174
179
|
})
|
|
175
180
|
|
|
@@ -216,6 +221,9 @@ const UsaMap = props => {
|
|
|
216
221
|
)
|
|
217
222
|
}
|
|
218
223
|
|
|
224
|
+
let pathGenerator = geoPath().projection(geoAlbersUsa().translate(translate))
|
|
225
|
+
const { pathArray } = useMapLayers(state, '', pathGenerator)
|
|
226
|
+
|
|
219
227
|
// Constructs and displays markup for all geos on the map (except territories right now)
|
|
220
228
|
const constructGeoJsx = (geographies, projection) => {
|
|
221
229
|
let showLabel = state.general.displayStateLabels
|
|
@@ -291,11 +299,7 @@ const UsaMap = props => {
|
|
|
291
299
|
|
|
292
300
|
return (
|
|
293
301
|
<g data-name={geoName} key={key}>
|
|
294
|
-
<g className='geo-group' css={styles} onClick={() => geoClickHandler(geoDisplayName, geoData)}
|
|
295
|
-
id={geoName}
|
|
296
|
-
data-tooltip-id="tooltip"
|
|
297
|
-
data-tooltip-html={tooltip}
|
|
298
|
-
>
|
|
302
|
+
<g className='geo-group' css={styles} onClick={() => geoClickHandler(geoDisplayName, geoData)} id={geoName} data-tooltip-id='tooltip' data-tooltip-html={tooltip}>
|
|
299
303
|
<path tabIndex={-1} className='single-geo' strokeWidth={1.3} d={path} />
|
|
300
304
|
{(isHex || showLabel) && geoLabel(geo, legendColors[0], projection)}
|
|
301
305
|
</g>
|
|
@@ -319,18 +323,18 @@ const UsaMap = props => {
|
|
|
319
323
|
// Cities
|
|
320
324
|
geosJsx.push(
|
|
321
325
|
<CityList
|
|
322
|
-
|
|
323
|
-
key='cities'
|
|
324
|
-
data={data}
|
|
325
|
-
state={state}
|
|
326
|
-
geoClickHandler={geoClickHandler}
|
|
326
|
+
applyLegendToRow={applyLegendToRow}
|
|
327
327
|
applyTooltipsToGeo={applyTooltipsToGeo}
|
|
328
|
+
data={data}
|
|
328
329
|
displayGeoName={displayGeoName}
|
|
329
|
-
|
|
330
|
-
titleCase={titleCase}
|
|
331
|
-
setSharedFilterValue={setSharedFilterValue}
|
|
330
|
+
geoClickHandler={geoClickHandler}
|
|
332
331
|
isFilterValueSupported={isFilterValueSupported}
|
|
333
332
|
isGeoCodeMap={state.general.type === 'us-geocode'}
|
|
333
|
+
key='cities'
|
|
334
|
+
projection={projection}
|
|
335
|
+
setSharedFilterValue={setSharedFilterValue}
|
|
336
|
+
state={state}
|
|
337
|
+
titleCase={titleCase}
|
|
334
338
|
/>
|
|
335
339
|
)
|
|
336
340
|
|
|
@@ -339,6 +343,13 @@ const UsaMap = props => {
|
|
|
339
343
|
geosJsx.push(<BubbleList key='bubbles' data={state.data} runtimeData={data} state={state} projection={projection} applyLegendToRow={applyLegendToRow} applyTooltipsToGeo={applyTooltipsToGeo} displayGeoName={displayGeoName} />)
|
|
340
344
|
}
|
|
341
345
|
|
|
346
|
+
// })
|
|
347
|
+
|
|
348
|
+
if (pathArray.length > 0) {
|
|
349
|
+
pathArray.map(layer => {
|
|
350
|
+
return geosJsx.push(layer)
|
|
351
|
+
})
|
|
352
|
+
}
|
|
342
353
|
return geosJsx
|
|
343
354
|
}
|
|
344
355
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { memo } from 'react'
|
|
2
2
|
|
|
3
3
|
import { jsx } from '@emotion/react'
|
|
4
4
|
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
@@ -16,7 +16,21 @@ const { features: world } = feature(topoJSON, topoJSON.objects.countries)
|
|
|
16
16
|
let projection = geoMercator()
|
|
17
17
|
|
|
18
18
|
const WorldMap = props => {
|
|
19
|
-
const {
|
|
19
|
+
const {
|
|
20
|
+
state,
|
|
21
|
+
applyTooltipsToGeo,
|
|
22
|
+
data,
|
|
23
|
+
geoClickHandler,
|
|
24
|
+
applyLegendToRow,
|
|
25
|
+
displayGeoName,
|
|
26
|
+
supportedCountries,
|
|
27
|
+
setState, setRuntimeData,
|
|
28
|
+
generateRuntimeData,
|
|
29
|
+
setFilteredCountryCode,
|
|
30
|
+
position, setPosition,
|
|
31
|
+
hasZoom,
|
|
32
|
+
handleMapAriaLabels,
|
|
33
|
+
titleCase } = props
|
|
20
34
|
|
|
21
35
|
// TODO Refactor - state should be set together here to avoid rerenders
|
|
22
36
|
// Resets to original data & zooms out
|
|
@@ -58,6 +72,11 @@ const WorldMap = props => {
|
|
|
58
72
|
Reset Filters
|
|
59
73
|
</button>
|
|
60
74
|
)}
|
|
75
|
+
{state.general.type === 'world-geocode' && (
|
|
76
|
+
<button onClick={() => handleReset(state, setState, setRuntimeData, generateRuntimeData)} className='reset' aria-label='Reset Zoom'>
|
|
77
|
+
Reset Zoom
|
|
78
|
+
</button>
|
|
79
|
+
)}
|
|
61
80
|
</div>
|
|
62
81
|
)
|
|
63
82
|
|
|
@@ -76,7 +95,7 @@ const WorldMap = props => {
|
|
|
76
95
|
const geosJsx = geographies.map(({ feature: geo, path }, i) => {
|
|
77
96
|
const geoKey = geo.properties.iso
|
|
78
97
|
|
|
79
|
-
if (!geoKey) return
|
|
98
|
+
if (!geoKey) return null;
|
|
80
99
|
|
|
81
100
|
const geoData = data[geoKey]
|
|
82
101
|
|
|
@@ -104,13 +123,13 @@ const WorldMap = props => {
|
|
|
104
123
|
|
|
105
124
|
styles = {
|
|
106
125
|
...styles,
|
|
107
|
-
fill: legendColors[0],
|
|
126
|
+
fill: state.general.type !== 'world-geocode' ? legendColors[0] : '#E6E6E6',
|
|
108
127
|
cursor: 'default',
|
|
109
128
|
'&:hover': {
|
|
110
|
-
fill: legendColors[1]
|
|
129
|
+
fill: state.general.type !== 'world-geocode' ? legendColors[1] : '#E6E6E6'
|
|
111
130
|
},
|
|
112
131
|
'&:active': {
|
|
113
|
-
fill: legendColors[2]
|
|
132
|
+
fill: state.general.type !== 'world-geocode' ? legendColors[2] : '#E6E6E6'
|
|
114
133
|
}
|
|
115
134
|
}
|
|
116
135
|
|
|
@@ -136,7 +155,20 @@ const WorldMap = props => {
|
|
|
136
155
|
})
|
|
137
156
|
|
|
138
157
|
// Cities
|
|
139
|
-
geosJsx.push(
|
|
158
|
+
geosJsx.push(
|
|
159
|
+
<CityList
|
|
160
|
+
applyLegendToRow={applyLegendToRow}
|
|
161
|
+
applyTooltipsToGeo={applyTooltipsToGeo}
|
|
162
|
+
data={data}
|
|
163
|
+
displayGeoName={displayGeoName}
|
|
164
|
+
geoClickHandler={geoClickHandler}
|
|
165
|
+
isGeoCodeMap={state.general.type === 'world-geocode'}
|
|
166
|
+
key='cities'
|
|
167
|
+
projection={projection}
|
|
168
|
+
state={state}
|
|
169
|
+
titleCase={titleCase}
|
|
170
|
+
/>
|
|
171
|
+
)
|
|
140
172
|
|
|
141
173
|
// Bubbles
|
|
142
174
|
if (state.general.type === 'bubble') {
|
|
@@ -174,7 +206,7 @@ const WorldMap = props => {
|
|
|
174
206
|
</ZoomableGroup>
|
|
175
207
|
</svg>
|
|
176
208
|
)}
|
|
177
|
-
{(state.general.type === 'data' || (state.general.type === 'bubble' && hasZoom)) && <ZoomControls position={position} setPosition={setPosition} setRuntimeData={setRuntimeData} state={state} setState={setState} generateRuntimeData={generateRuntimeData} />}
|
|
209
|
+
{(state.general.type === 'data' || (state.general.type === 'world-geocode' && hasZoom) || (state.general.type === 'bubble' && hasZoom)) && <ZoomControls position={position} setPosition={setPosition} setRuntimeData={setRuntimeData} state={state} setState={setState} generateRuntimeData={generateRuntimeData} />}
|
|
178
210
|
</ErrorBoundary>
|
|
179
211
|
)
|
|
180
212
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "Topology",
|
|
3
|
+
"arcs": [
|
|
4
|
+
[
|
|
5
|
+
[
|
|
6
|
+
33,
|
|
7
|
+
100
|
|
8
|
+
],
|
|
9
|
+
[
|
|
10
|
+
-6,
|
|
11
|
+
9
|
|
12
|
+
],
|
|
13
|
+
[
|
|
14
|
+
30,
|
|
15
|
+
66
|
|
16
|
+
],
|
|
17
|
+
[
|
|
18
|
+
102,
|
|
19
|
+
-42
|
|
20
|
+
],
|
|
21
|
+
[
|
|
22
|
+
-118,
|
|
23
|
+
-133
|
|
24
|
+
],
|
|
25
|
+
[
|
|
26
|
+
11,
|
|
27
|
+
39
|
|
28
|
+
],
|
|
29
|
+
[
|
|
30
|
+
-52,
|
|
31
|
+
39
|
|
32
|
+
],
|
|
33
|
+
[
|
|
34
|
+
33,
|
|
35
|
+
22
|
|
36
|
+
]
|
|
37
|
+
]
|
|
38
|
+
],
|
|
39
|
+
"transform": {
|
|
40
|
+
"scale": [
|
|
41
|
+
0.04505085471698112,
|
|
42
|
+
0.04496205714285712
|
|
43
|
+
],
|
|
44
|
+
"translate": [
|
|
45
|
+
-116.2012448,
|
|
46
|
+
33.3764121
|
|
47
|
+
]
|
|
48
|
+
},
|
|
49
|
+
"objects": {
|
|
50
|
+
"Untitled layer": {
|
|
51
|
+
"type": "GeometryCollection",
|
|
52
|
+
"geometries": [
|
|
53
|
+
{
|
|
54
|
+
"arcs": [
|
|
55
|
+
[
|
|
56
|
+
0
|
|
57
|
+
]
|
|
58
|
+
],
|
|
59
|
+
"type": "Polygon",
|
|
60
|
+
"properties": {
|
|
61
|
+
"name": "new",
|
|
62
|
+
"styleUrl": "#poly-000000-1200-77-nodesc",
|
|
63
|
+
"fill-opacity": 0.30196078431372547,
|
|
64
|
+
"fill": "#000000",
|
|
65
|
+
"stroke-opacity": 1,
|
|
66
|
+
"stroke": "#000000",
|
|
67
|
+
"stroke-width": 1.2
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -2,6 +2,7 @@ export default {
|
|
|
2
2
|
general: {
|
|
3
3
|
geoBorderColor: 'darkGray',
|
|
4
4
|
headerColor: 'theme-blue',
|
|
5
|
+
title: '',
|
|
5
6
|
showTitle: true,
|
|
6
7
|
showSidebar: true,
|
|
7
8
|
showDownloadButton: true,
|
|
@@ -9,6 +10,7 @@ export default {
|
|
|
9
10
|
displayAsHex: false,
|
|
10
11
|
displayStateLabels: false,
|
|
11
12
|
territoriesLabel: 'Territories',
|
|
13
|
+
territoriesAlwaysShow: false,
|
|
12
14
|
language: 'en',
|
|
13
15
|
geoType: 'single-state',
|
|
14
16
|
geoLabelOverride: '',
|
|
@@ -23,7 +25,6 @@ export default {
|
|
|
23
25
|
hideGeoColumnInTooltip: false,
|
|
24
26
|
hidePrimaryColumnInTooltip: false
|
|
25
27
|
},
|
|
26
|
-
|
|
27
28
|
type: 'map',
|
|
28
29
|
color: 'pinkpurple',
|
|
29
30
|
columns: {
|
|
@@ -53,6 +54,7 @@ export default {
|
|
|
53
54
|
unified: false,
|
|
54
55
|
singleColumn: false,
|
|
55
56
|
singleRow: false,
|
|
57
|
+
showSpecialClassesLast: false,
|
|
56
58
|
dynamicDescription: false,
|
|
57
59
|
type: 'equalnumber',
|
|
58
60
|
numberOfItems: 3,
|
|
@@ -64,7 +66,8 @@ export default {
|
|
|
64
66
|
title: 'Data Table'
|
|
65
67
|
},
|
|
66
68
|
table: {
|
|
67
|
-
showDownloadUrl: false
|
|
69
|
+
showDownloadUrl: false,
|
|
70
|
+
showDataTableLink: true
|
|
68
71
|
},
|
|
69
72
|
tooltips: {
|
|
70
73
|
appearanceType: 'hover',
|
|
@@ -82,5 +85,9 @@ export default {
|
|
|
82
85
|
geoCodeCircleSize: 2,
|
|
83
86
|
showBubbleZeros: false
|
|
84
87
|
},
|
|
85
|
-
mapPosition: { coordinates: [0, 30], zoom: 1 }
|
|
88
|
+
mapPosition: { coordinates: [0, 30], zoom: 1 },
|
|
89
|
+
map: {
|
|
90
|
+
layers: []
|
|
91
|
+
},
|
|
92
|
+
filterBehavior: 'Filter Change'
|
|
86
93
|
}
|
|
@@ -121,13 +121,13 @@ export const stateToIso = {
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
export const stateFipsToTwoDigit = {
|
|
124
|
-
['01']: 'AL',
|
|
125
|
-
['02']: 'AK',
|
|
126
|
-
['04']: 'AZ',
|
|
127
|
-
['05']: 'AR',
|
|
128
|
-
['06']: 'CA',
|
|
129
|
-
['08']: 'CO',
|
|
130
|
-
['09']: 'CT',
|
|
124
|
+
['01']: 'AL', // eslint-disable-line
|
|
125
|
+
['02']: 'AK', // eslint-disable-line
|
|
126
|
+
['04']: 'AZ', // eslint-disable-line
|
|
127
|
+
['05']: 'AR', // eslint-disable-line
|
|
128
|
+
['06']: 'CA', // eslint-disable-line
|
|
129
|
+
['08']: 'CO', // eslint-disable-line
|
|
130
|
+
['09']: 'CT', // eslint-disable-line
|
|
131
131
|
10: 'DE',
|
|
132
132
|
11: 'DC',
|
|
133
133
|
12: 'FL',
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { useEffect, useId, useState } from 'react'
|
|
2
|
+
import { feature } from 'topojson-client'
|
|
3
|
+
import { Group } from '@visx/group'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* This is the starting structure for adding custom geoJSON shape layers to a projection.
|
|
7
|
+
* The expectation should be that geoJSON is saved somewhere externally.
|
|
8
|
+
*
|
|
9
|
+
* todo: save map layers to local state and add debounce fn to improve performance
|
|
10
|
+
* todo: usaMap is using objects.cove which needs to be converted to a dynamic value
|
|
11
|
+
*
|
|
12
|
+
* User Interface Expectations:
|
|
13
|
+
* 1) Direct users to https://www.google.com/maps/about/mymaps to create a map
|
|
14
|
+
* 2) Export the shape layer as a kml file and import into mapshaper.org
|
|
15
|
+
* 3) Clean (ie. mapshaper -clean) and edit the shape as needed and export the new layer as geoJSON
|
|
16
|
+
* 4) Save the geoJSON somewhere external.
|
|
17
|
+
*/
|
|
18
|
+
export default function useMapLayers(config, setConfig, pathGenerator) {
|
|
19
|
+
const [fetchedTopoJSON, setFetchedTopoJSON] = useState([])
|
|
20
|
+
const geoId = useId()
|
|
21
|
+
|
|
22
|
+
// small reminder that we export the feature and the path as options
|
|
23
|
+
const [pathArray, setPathArray] = useState([])
|
|
24
|
+
const [featureArray, setFeatureArray] = useState([])
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
fetchGeoJSONLayers()
|
|
28
|
+
}, []) //eslint-disable-line
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
fetchGeoJSONLayers()
|
|
32
|
+
}, [config.map.layers]) //eslint-disable-line
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (pathGenerator) {
|
|
36
|
+
generateCustomLayers()
|
|
37
|
+
}
|
|
38
|
+
}, [fetchedTopoJSON]) //eslint-disable-line
|
|
39
|
+
|
|
40
|
+
const fetchGeoJSONLayers = async () => {
|
|
41
|
+
let geos = await getMapTopoJSONLayers()
|
|
42
|
+
setFetchedTopoJSON(geos)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Removes a custom map layer from the config.
|
|
47
|
+
* @param { Event } e Remove onclick event
|
|
48
|
+
* @param { Integer } index index of layer to remove
|
|
49
|
+
*/
|
|
50
|
+
const handleRemoveLayer = (e, index) => {
|
|
51
|
+
e.preventDefault()
|
|
52
|
+
|
|
53
|
+
const updatedState = {
|
|
54
|
+
...config,
|
|
55
|
+
map: {
|
|
56
|
+
...config.map,
|
|
57
|
+
layers: config.map.layers.filter((layer, i) => i !== index)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
setConfig(updatedState)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Adds a new custom map layer to the config
|
|
66
|
+
* @param { Event } e Add onclick event
|
|
67
|
+
*/
|
|
68
|
+
const handleAddLayer = e => {
|
|
69
|
+
e.preventDefault()
|
|
70
|
+
const updatedState = {
|
|
71
|
+
...config,
|
|
72
|
+
map: {
|
|
73
|
+
...config.map,
|
|
74
|
+
layers: [
|
|
75
|
+
...config.map.layers,
|
|
76
|
+
{
|
|
77
|
+
name: 'New Custom Layer',
|
|
78
|
+
url: ''
|
|
79
|
+
}
|
|
80
|
+
]
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
setConfig(updatedState)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Updates the index of the layer tooltip
|
|
88
|
+
* @param {Event} e
|
|
89
|
+
* @param {Integer} index
|
|
90
|
+
*/
|
|
91
|
+
const handleMapLayerTooltip = (e, index) => {
|
|
92
|
+
e.preventDefault()
|
|
93
|
+
let newLayers = [...config.map.layers]
|
|
94
|
+
|
|
95
|
+
newLayers[index].tooltip = e.target.value
|
|
96
|
+
|
|
97
|
+
setConfig({
|
|
98
|
+
...config,
|
|
99
|
+
map: {
|
|
100
|
+
...config.map,
|
|
101
|
+
layers: newLayers
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Changes the map layer url for a given index
|
|
108
|
+
* @param {Event} e - on add custom layer click
|
|
109
|
+
* @param {Integer} index - index of layer to update
|
|
110
|
+
*/
|
|
111
|
+
const handleMapLayerUrl = (e, index) => {
|
|
112
|
+
e.preventDefault()
|
|
113
|
+
let newLayers = [...config.map.layers]
|
|
114
|
+
|
|
115
|
+
newLayers[index].url = e.target.value
|
|
116
|
+
|
|
117
|
+
setConfig({
|
|
118
|
+
...config,
|
|
119
|
+
map: {
|
|
120
|
+
...config.map,
|
|
121
|
+
layers: newLayers
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Changes the map layer name for a given index
|
|
128
|
+
* @param {Event} e - on add custom layer click
|
|
129
|
+
* @param {Integer} index - index of layer to update
|
|
130
|
+
*/
|
|
131
|
+
const handleMapLayerName = (e, index) => {
|
|
132
|
+
e.preventDefault()
|
|
133
|
+
|
|
134
|
+
let newLayers = [...config.map.layers]
|
|
135
|
+
|
|
136
|
+
newLayers[index].name = e.target.value
|
|
137
|
+
|
|
138
|
+
setConfig({
|
|
139
|
+
...config,
|
|
140
|
+
map: {
|
|
141
|
+
...config.map,
|
|
142
|
+
layers: newLayers
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Changes the map layer namespace for a given index
|
|
149
|
+
* @param {Event} e - on add custom layer click
|
|
150
|
+
* @param {Integer} index - index of layer to update
|
|
151
|
+
*/
|
|
152
|
+
const handleMapLayerNamespace = (e, index) => {
|
|
153
|
+
e.preventDefault()
|
|
154
|
+
|
|
155
|
+
let newLayers = [...config.map.layers]
|
|
156
|
+
|
|
157
|
+
newLayers[index].namespace = e.target.value
|
|
158
|
+
|
|
159
|
+
setConfig({
|
|
160
|
+
...config,
|
|
161
|
+
map: {
|
|
162
|
+
...config.map,
|
|
163
|
+
layers: newLayers
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Fetches TopoJSON urls found in config.map.layers and stores it locally.
|
|
170
|
+
* @returns
|
|
171
|
+
*/
|
|
172
|
+
const getMapTopoJSONLayers = async () => {
|
|
173
|
+
let TopoJSONObjects = []
|
|
174
|
+
if (!config.map.layers) return
|
|
175
|
+
|
|
176
|
+
for (const mapLayer of config.map.layers) {
|
|
177
|
+
let newLayerItem = await fetch(mapLayer.url)
|
|
178
|
+
.then(res => res.json())
|
|
179
|
+
.catch(e => console.warn('error with newLayer item'))
|
|
180
|
+
if (!newLayerItem) newLayerItem = []
|
|
181
|
+
TopoJSONObjects.push(newLayerItem)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return TopoJSONObjects
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Updates the custom map layers based on the topojson data
|
|
189
|
+
* @returns {void} new map layers to the config
|
|
190
|
+
*/
|
|
191
|
+
const generateCustomLayers = () => {
|
|
192
|
+
if (fetchedTopoJSON.length === 0 || !fetchedTopoJSON) return false
|
|
193
|
+
let tempArr = []
|
|
194
|
+
let tempFeatureArray = []
|
|
195
|
+
|
|
196
|
+
// loop on each file.
|
|
197
|
+
fetchedTopoJSON?.map((layer, index) => {
|
|
198
|
+
if (layer.length === 0) return null
|
|
199
|
+
let layerObjects = layer.objects[config.map.layers[index].namespace]
|
|
200
|
+
if (!layerObjects) return null
|
|
201
|
+
|
|
202
|
+
let layerData = feature(layer, layerObjects).features
|
|
203
|
+
|
|
204
|
+
// now loop on each feature
|
|
205
|
+
layerData.forEach(item => {
|
|
206
|
+
let layerClasses = [`custom-map-layer`, `custom-map-layer--${item.properties.name.replace(' ', '-')}`]
|
|
207
|
+
|
|
208
|
+
// feature array for county maps
|
|
209
|
+
tempFeatureArray.push(item)
|
|
210
|
+
|
|
211
|
+
tempArr.push(
|
|
212
|
+
<Group className={layerClasses.join(' ')} key={`customMapLayer-${item.properties.name.replace(' ', '-')}-${index}`}>
|
|
213
|
+
{/* prettier-ignore */}
|
|
214
|
+
<path
|
|
215
|
+
d={pathGenerator(item)}
|
|
216
|
+
fill={item.properties.fill}
|
|
217
|
+
fillOpacity={item.properties['fill-opacity']}
|
|
218
|
+
key={geoId} data-id={geoId}
|
|
219
|
+
stroke={item.properties.stroke}
|
|
220
|
+
strokeWidth={item.properties['stroke-width']}
|
|
221
|
+
data-tooltip-id='tooltip'
|
|
222
|
+
data-tooltip-html={config.map.layers[index].tooltip ? config.map.layers[index].tooltip : ''}
|
|
223
|
+
/>
|
|
224
|
+
</Group>
|
|
225
|
+
)
|
|
226
|
+
})
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
// export options for either the feature or the path
|
|
230
|
+
setPathArray(tempArr)
|
|
231
|
+
setFeatureArray(tempFeatureArray)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const MapLayerHandlers = () => null
|
|
235
|
+
MapLayerHandlers.handleRemoveLayer = handleRemoveLayer
|
|
236
|
+
MapLayerHandlers.handleAddLayer = handleAddLayer
|
|
237
|
+
MapLayerHandlers.handleMapLayerUrl = handleMapLayerUrl
|
|
238
|
+
MapLayerHandlers.handleMapLayerName = handleMapLayerName
|
|
239
|
+
MapLayerHandlers.handleMapLayerNamespace = handleMapLayerNamespace
|
|
240
|
+
MapLayerHandlers.handleMapLayerTooltip = handleMapLayerTooltip
|
|
241
|
+
|
|
242
|
+
return { pathArray, featureArray, MapLayerHandlers }
|
|
243
|
+
}
|
package/src/index.jsx
CHANGED
|
@@ -1,20 +1,16 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import ReactDOM from 'react-dom/client'
|
|
3
3
|
|
|
4
|
-
import CdcMap from './CdcMap'
|
|
4
|
+
import CdcMap from './CdcMap'
|
|
5
5
|
|
|
6
6
|
import 'react-tooltip/dist/react-tooltip.css'
|
|
7
7
|
|
|
8
8
|
let isEditor = window.location.href.includes('editor=true')
|
|
9
|
-
|
|
9
|
+
let isDebug = window.location.href.includes('debug=true')
|
|
10
10
|
let domContainer = document.getElementsByClassName('react-container')[0]
|
|
11
11
|
|
|
12
12
|
ReactDOM.createRoot(domContainer).render(
|
|
13
13
|
<React.StrictMode>
|
|
14
|
-
<CdcMap
|
|
15
|
-
|
|
16
|
-
configUrl={domContainer.attributes['data-config'].value}
|
|
17
|
-
containerEl={domContainer}
|
|
18
|
-
/>
|
|
19
|
-
</React.StrictMode>,
|
|
14
|
+
<CdcMap isEditor={isEditor} isDebug={isDebug} configUrl={domContainer.attributes['data-config'].value} containerEl={domContainer} />
|
|
15
|
+
</React.StrictMode>
|
|
20
16
|
)
|