@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.
@@ -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]) // eslint-disable-line
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: true,
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
- showDownloadUrl: false
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
- isEditor={isEditor}
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
  )