@cdc/map 4.23.3 → 4.23.5
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 +25301 -29100
- package/examples/custom-map-layers.json +764 -0
- package/examples/default-county.json +169 -155
- package/examples/example-city-state.json +34 -12
- package/examples/testing-layer-2.json +1 -0
- package/examples/testing-layer.json +96 -0
- package/index.html +6 -5
- package/package.json +3 -3
- package/src/CdcMap.jsx +201 -105
- package/src/components/CountyMap.jsx +31 -6
- package/src/components/DataTable.jsx +185 -218
- package/src/components/EditorPanel.jsx +293 -162
- package/src/components/UsaMap.jsx +17 -11
- package/src/data/initial-state.js +16 -8
- 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/components/Filters.jsx +0 -113
|
@@ -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)
|
|
@@ -118,7 +121,7 @@ const UsaMap = props => {
|
|
|
118
121
|
const territoriesList = territoriesKeys.filter(key => data[key])
|
|
119
122
|
setTerritoriesData(territoriesList)
|
|
120
123
|
}
|
|
121
|
-
}, [data, state.general.territoriesAlwaysShow])
|
|
124
|
+
}, [data, state.general.territoriesAlwaysShow])
|
|
122
125
|
|
|
123
126
|
const geoStrokeColor = state.general.geoBorderColor === 'darkGray' ? 'rgba(0, 0, 0, 0.2)' : 'rgba(255,255,255,0.7)'
|
|
124
127
|
|
|
@@ -171,10 +174,7 @@ const UsaMap = props => {
|
|
|
171
174
|
}
|
|
172
175
|
}
|
|
173
176
|
|
|
174
|
-
return <Shape key={label} label={label} css={styles} text={styles.color} strokeWidth={1.5} textColor={textColor} onClick={() => geoClickHandler(territory, territoryData)}
|
|
175
|
-
data-tooltip-id="tooltip"
|
|
176
|
-
data-tooltip-html={toolTip}
|
|
177
|
-
/>
|
|
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} />
|
|
178
178
|
}
|
|
179
179
|
})
|
|
180
180
|
|
|
@@ -221,6 +221,9 @@ const UsaMap = props => {
|
|
|
221
221
|
)
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
+
let pathGenerator = geoPath().projection(geoAlbersUsa().translate(translate))
|
|
225
|
+
const { pathArray } = useMapLayers(state, '', pathGenerator)
|
|
226
|
+
|
|
224
227
|
// Constructs and displays markup for all geos on the map (except territories right now)
|
|
225
228
|
const constructGeoJsx = (geographies, projection) => {
|
|
226
229
|
let showLabel = state.general.displayStateLabels
|
|
@@ -296,11 +299,7 @@ const UsaMap = props => {
|
|
|
296
299
|
|
|
297
300
|
return (
|
|
298
301
|
<g data-name={geoName} key={key}>
|
|
299
|
-
<g className='geo-group' css={styles} onClick={() => geoClickHandler(geoDisplayName, geoData)}
|
|
300
|
-
id={geoName}
|
|
301
|
-
data-tooltip-id="tooltip"
|
|
302
|
-
data-tooltip-html={tooltip}
|
|
303
|
-
>
|
|
302
|
+
<g className='geo-group' css={styles} onClick={() => geoClickHandler(geoDisplayName, geoData)} id={geoName} data-tooltip-id='tooltip' data-tooltip-html={tooltip}>
|
|
304
303
|
<path tabIndex={-1} className='single-geo' strokeWidth={1.3} d={path} />
|
|
305
304
|
{(isHex || showLabel) && geoLabel(geo, legendColors[0], projection)}
|
|
306
305
|
</g>
|
|
@@ -344,6 +343,13 @@ const UsaMap = props => {
|
|
|
344
343
|
geosJsx.push(<BubbleList key='bubbles' data={state.data} runtimeData={data} state={state} projection={projection} applyLegendToRow={applyLegendToRow} applyTooltipsToGeo={applyTooltipsToGeo} displayGeoName={displayGeoName} />)
|
|
345
344
|
}
|
|
346
345
|
|
|
346
|
+
// })
|
|
347
|
+
|
|
348
|
+
if (pathArray.length > 0) {
|
|
349
|
+
pathArray.map(layer => {
|
|
350
|
+
return geosJsx.push(layer)
|
|
351
|
+
})
|
|
352
|
+
}
|
|
347
353
|
return geosJsx
|
|
348
354
|
}
|
|
349
355
|
|
|
@@ -5,7 +5,7 @@ export default {
|
|
|
5
5
|
title: '',
|
|
6
6
|
showTitle: true,
|
|
7
7
|
showSidebar: true,
|
|
8
|
-
showDownloadButton:
|
|
8
|
+
showDownloadButton: false,
|
|
9
9
|
showDownloadMediaButton: false,
|
|
10
10
|
displayAsHex: false,
|
|
11
11
|
displayStateLabels: false,
|
|
@@ -15,7 +15,6 @@ export default {
|
|
|
15
15
|
geoType: 'single-state',
|
|
16
16
|
geoLabelOverride: '',
|
|
17
17
|
hasRegions: false,
|
|
18
|
-
expandDataTable: true,
|
|
19
18
|
fullBorder: false,
|
|
20
19
|
type: 'data',
|
|
21
20
|
palette: {
|
|
@@ -25,7 +24,6 @@ export default {
|
|
|
25
24
|
hideGeoColumnInTooltip: false,
|
|
26
25
|
hidePrimaryColumnInTooltip: false
|
|
27
26
|
},
|
|
28
|
-
|
|
29
27
|
type: 'map',
|
|
30
28
|
color: 'pinkpurple',
|
|
31
29
|
columns: {
|
|
@@ -63,11 +61,17 @@ export default {
|
|
|
63
61
|
title: 'Legend'
|
|
64
62
|
},
|
|
65
63
|
filters: [],
|
|
66
|
-
dataTable: {
|
|
67
|
-
title: 'Data Table'
|
|
68
|
-
},
|
|
69
64
|
table: {
|
|
70
|
-
|
|
65
|
+
label: 'Data Table',
|
|
66
|
+
expanded: false,
|
|
67
|
+
limitHeight: false,
|
|
68
|
+
height: '',
|
|
69
|
+
caption: '',
|
|
70
|
+
showDownloadUrl: false,
|
|
71
|
+
showDataTableLink: true,
|
|
72
|
+
forceDisplay: true,
|
|
73
|
+
download: false,
|
|
74
|
+
indexLabel: ''
|
|
71
75
|
},
|
|
72
76
|
tooltips: {
|
|
73
77
|
appearanceType: 'hover',
|
|
@@ -85,5 +89,9 @@ export default {
|
|
|
85
89
|
geoCodeCircleSize: 2,
|
|
86
90
|
showBubbleZeros: false
|
|
87
91
|
},
|
|
88
|
-
mapPosition: { coordinates: [0, 30], zoom: 1 }
|
|
92
|
+
mapPosition: { coordinates: [0, 30], zoom: 1 },
|
|
93
|
+
map: {
|
|
94
|
+
layers: []
|
|
95
|
+
},
|
|
96
|
+
filterBehavior: 'Filter Change'
|
|
89
97
|
}
|
|
@@ -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
|
)
|