@cdc/map 4.24.7 → 4.24.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 (46) hide show
  1. package/dist/cdcmap.js +40720 -38422
  2. package/examples/county-year.csv +10 -0
  3. package/examples/default-geocode.json +44 -10
  4. package/examples/default-patterns.json +0 -2
  5. package/examples/default-single-state.json +279 -108
  6. package/examples/map-issue-3.json +646 -0
  7. package/examples/single-state-filter.json +153 -0
  8. package/index.html +9 -6
  9. package/package.json +3 -3
  10. package/src/CdcMap.tsx +322 -126
  11. package/src/_stories/CdcMap.stories.tsx +7 -0
  12. package/src/_stories/_mock/DEV-8942.json +270 -0
  13. package/src/components/Annotation/AnnotationDropdown.tsx +1 -0
  14. package/src/components/{BubbleList.jsx → BubbleList.tsx} +1 -1
  15. package/src/components/{CityList.jsx → CityList.tsx} +28 -2
  16. package/src/components/{DataTable.jsx → DataTable.tsx} +2 -2
  17. package/src/components/EditorPanel/components/EditorPanel.tsx +647 -127
  18. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +0 -22
  19. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +61 -11
  20. package/src/components/Legend/components/Legend.tsx +125 -36
  21. package/src/components/Legend/components/index.scss +42 -42
  22. package/src/components/Modal.tsx +25 -0
  23. package/src/components/UsaMap/components/SingleState/SingleState.CountyOutput.tsx +74 -0
  24. package/src/components/UsaMap/components/SingleState/SingleState.StateOutput.tsx +29 -0
  25. package/src/components/UsaMap/components/SingleState/index.tsx +9 -0
  26. package/src/components/UsaMap/components/UsaMap.County.tsx +84 -33
  27. package/src/components/UsaMap/components/UsaMap.SingleState.tsx +173 -206
  28. package/src/components/UsaMap/components/UsaMap.State.tsx +161 -26
  29. package/src/components/UsaMap/data/us-extended-geography.json +1 -0
  30. package/src/components/UsaMap/helpers/map.ts +111 -0
  31. package/src/components/WorldMap/WorldMap.tsx +17 -32
  32. package/src/components/ZoomControls.tsx +41 -0
  33. package/src/data/initial-state.js +7 -1
  34. package/src/data/supported-geos.js +15 -4
  35. package/src/helpers/generateRuntimeLegendHash.ts +2 -2
  36. package/src/hooks/useStateZoom.tsx +157 -0
  37. package/src/hooks/{useZoomPan.js → useZoomPan.ts} +6 -5
  38. package/src/scss/editor-panel.scss +0 -4
  39. package/src/scss/main.scss +23 -1
  40. package/src/scss/map.scss +8 -0
  41. package/src/types/MapConfig.ts +9 -1
  42. package/src/types/MapContext.ts +14 -2
  43. package/src/components/Modal.jsx +0 -22
  44. /package/src/components/{Geo.jsx → Geo.tsx} +0 -0
  45. /package/src/components/{NavigationMenu.jsx → NavigationMenu.tsx} +0 -0
  46. /package/src/components/{ZoomableGroup.jsx → ZoomableGroup.tsx} +0 -0
@@ -1,104 +1,26 @@
1
- import { useState, useEffect, memo, useContext } from 'react'
2
-
3
- import { jsx } from '@emotion/react'
1
+ import { useEffect, memo, useContext, useRef, useState } from 'react'
4
2
  import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
5
3
  import { geoPath } from 'd3-geo'
6
- import { feature, mesh } from 'topojson-client'
7
4
  import { CustomProjection } from '@visx/geo'
8
5
  import Loading from '@cdc/core/components/Loading'
9
- import colorPalettes from '@cdc/core/data/colorPalettes'
10
6
  import { geoAlbersUsaTerritories } from 'd3-composite-projections'
11
7
  import CityList from '../../CityList'
12
8
  import ConfigContext from '../../../context'
13
9
  import Annotation from '../../Annotation'
10
+ import SingleState from './SingleState'
11
+ import { getTopoData, getCurrentTopoYear, isTopoReady } from './../helpers/map'
12
+ import ZoomableGroup from '../../ZoomableGroup'
13
+ import ZoomControls from '../../ZoomControls'
14
+ import { MapContext } from '../../../types/MapContext'
15
+ import useStateZoom from '../../../hooks/useStateZoom'
16
+ import { Text } from '@visx/text'
14
17
 
15
18
  // SVG ITEMS
16
19
  const WIDTH = 880
17
20
  const HEIGHT = 500
18
21
  const PADDING = 25
19
22
 
20
- const getCountyTopoURL = year => {
21
- return `https://www.cdc.gov/TemplatePackage/contrib/data/county-topography/cb_${year}_us_county_20m.json`
22
- }
23
-
24
- const getTopoData = year => {
25
- return new Promise((resolve, reject) => {
26
- const resolveWithTopo = async response => {
27
- if (response.status !== 200) {
28
- response = await import('./../data/cb_2019_us_county_20m.json')
29
- } else {
30
- response = await response.json()
31
- }
32
- let topoData = {}
33
-
34
- topoData.year = year || 'default'
35
- topoData.fulljson = response
36
- topoData.counties = feature(response, response.objects.counties).features
37
- topoData.states = feature(response, response.objects.states).features
38
-
39
- resolve(topoData)
40
- }
41
-
42
- const numericYear = parseInt(year)
43
-
44
- if (isNaN(numericYear)) {
45
- fetch(getCountyTopoURL(2019)).then(resolveWithTopo)
46
- } else if (numericYear > 2022) {
47
- fetch(getCountyTopoURL(2022)).then(resolveWithTopo)
48
- } else if (numericYear < 2013) {
49
- fetch(getCountyTopoURL(2013)).then(resolveWithTopo)
50
- } else {
51
- switch (numericYear) {
52
- case 2022:
53
- fetch(getCountyTopoURL(2022)).then(resolveWithTopo)
54
- break
55
- case 2021:
56
- fetch(getCountyTopoURL(2021)).then(resolveWithTopo)
57
- break
58
- case 2020:
59
- fetch(getCountyTopoURL(2020)).then(resolveWithTopo)
60
- break
61
- case 2018:
62
- case 2017:
63
- case 2016:
64
- case 2015:
65
- fetch(getCountyTopoURL(2015)).then(resolveWithTopo)
66
- break
67
- case 2014:
68
- fetch(getCountyTopoURL(2014)).then(resolveWithTopo)
69
- break
70
- case 2013:
71
- fetch(getCountyTopoURL(2013)).then(resolveWithTopo)
72
- break
73
- default:
74
- fetch(getCountyTopoURL(2019)).then(resolveWithTopo)
75
- break
76
- }
77
- }
78
- })
79
- }
80
-
81
- const getCurrentTopoYear = (state, runtimeFilters) => {
82
- let currentYear = state.general.countyCensusYear
83
-
84
- if (state.general.filterControlsCountyYear && runtimeFilters && runtimeFilters.length > 0) {
85
- let yearFilter = runtimeFilters.filter(filter => filter.columnName === state.general.filterControlsCountyYear)
86
- if (yearFilter.length > 0 && yearFilter[0].active) {
87
- currentYear = yearFilter[0].active
88
- }
89
- }
90
-
91
- return currentYear || 'default'
92
- }
93
-
94
- const isTopoReady = (topoData, state, runtimeFilters) => {
95
- let currentYear = getCurrentTopoYear(state, runtimeFilters)
96
-
97
- return topoData.year && (!currentYear || currentYear === topoData.year)
98
- }
99
-
100
23
  const SingleStateMap = props => {
101
- // prettier-ignore
102
24
  const {
103
25
  state,
104
26
  applyTooltipsToGeo,
@@ -106,30 +28,34 @@ const SingleStateMap = props => {
106
28
  geoClickHandler,
107
29
  applyLegendToRow,
108
30
  displayGeoName,
109
- supportedTerritories,
110
- runtimeLegend,
111
- generateColorsArray,
112
31
  handleMapAriaLabels,
113
32
  titleCase,
114
33
  setSharedFilterValue,
115
34
  isFilterValueSupported,
116
35
  runtimeFilters,
117
- tooltipId
118
- } = useContext(ConfigContext)
119
-
120
- const projection = geoAlbersUsaTerritories().translate([WIDTH / 2, HEIGHT / 2])
121
- const cityListProjection = geoAlbersUsaTerritories().translate([WIDTH / 2, HEIGHT / 2])
36
+ tooltipId,
37
+ position,
38
+ setPosition,
39
+ stateToShow,
40
+ topoData,
41
+ setTopoData,
42
+ scale,
43
+ translate,
44
+ setStateToShow
45
+ } = useContext<MapContext>(ConfigContext)
46
+
47
+ const { handleMoveEnd, handleZoomIn, handleZoomOut, handleReset, projection, statePicked } = useStateZoom(topoData)
48
+
49
+ const cityListProjection = geoAlbersUsaTerritories()
50
+ .translate([WIDTH / 2, HEIGHT / 2])
51
+ .scale(1)
122
52
  const geoStrokeColor = state.general.geoBorderColor === 'darkGray' ? 'rgba(0, 0, 0, 0.2)' : 'rgba(255,255,255,0.7)'
123
- const [stateToShow, setStateToShow] = useState(null)
124
- const [translate, setTranslate] = useState()
125
- const [scale, setScale] = useState()
126
- const [strokeWidth, setStrokeWidth] = useState(0.75)
127
- const [topoData, setTopoData] = useState({})
128
- let mapColorPalette = colorPalettes[state.color] || '#fff'
129
- let focusedBorderColor = mapColorPalette[3]
130
-
131
53
  const path = geoPath().projection(projection)
132
54
 
55
+ useEffect(() => {
56
+ setStateToShow(topoData?.states?.find(s => s.properties.name === state.general.statePicked.stateName))
57
+ }, [statePicked])
58
+
133
59
  useEffect(() => {
134
60
  let currentYear = getCurrentTopoYear(state, runtimeFilters)
135
61
 
@@ -140,34 +66,6 @@ const SingleStateMap = props => {
140
66
  }
141
67
  }, [state.general.countyCensusYear, state.general.filterControlsCountyYear, JSON.stringify(runtimeFilters)])
142
68
 
143
- // When choosing a state changes...
144
- useEffect(() => {
145
- if (!isTopoReady(topoData, state, runtimeFilters)) return
146
- if (state.general.hasOwnProperty('statePicked')) {
147
- let statePicked = state.general.statePicked.stateName
148
- let statePickedData = topoData.states.find(s => s.properties.name === statePicked)
149
- setStateToShow(statePickedData)
150
-
151
- const projection = geoAlbersUsaTerritories().translate([WIDTH / 2, HEIGHT / 2])
152
- const newProjection = projection.fitExtent(
153
- [
154
- [PADDING, PADDING],
155
- [WIDTH - PADDING, HEIGHT - PADDING]
156
- ],
157
- statePickedData
158
- )
159
- const newScale = newProjection.scale()
160
- const newScaleWithHypot = newScale / 1070
161
-
162
- let [x, y] = newProjection.translate()
163
- x = x - WIDTH / 2
164
- y = y - HEIGHT / 2
165
-
166
- setTranslate([x, y])
167
- setScale(newScaleWithHypot)
168
- }
169
- }, [state.general.statePicked, topoData.year])
170
-
171
69
  if (!isTopoReady(topoData, state, runtimeFilters)) {
172
70
  return (
173
71
  <div style={{ height: `${HEIGHT}px` }}>
@@ -176,83 +74,40 @@ const SingleStateMap = props => {
176
74
  )
177
75
  }
178
76
 
77
+ const checkForNoData = () => {
78
+ // If no statePicked, return true
79
+ if (!state.general.statePicked.fipsCode) return true
80
+ }
81
+
179
82
  // Constructs and displays markup for all geos on the map (except territories right now)
180
83
  const constructGeoJsx = (geographies, projection) => {
181
- const statePassed = geographies[0].feature.states
182
84
  const counties = geographies[0].feature.counties
183
85
 
184
86
  let geosJsx = []
185
87
 
186
- const StateOutput = () => {
187
- let geo = topoData.fulljson.objects.states.geometries.filter(s => {
188
- return s.id === statePassed.id
189
- })
190
-
191
- // const stateLine = path(mesh(testJSON, lines ))
192
- let stateLines = path(mesh(topoData.fulljson, geo[0]))
193
- return (
194
- <g key={'single-state'} className='single-state' style={{ fill: '#E6E6E6' }} stroke={geoStrokeColor} strokeWidth={0.95 / scale}>
195
- <path tabIndex={-1} className='state-path' d={stateLines} />
196
- </g>
197
- )
198
- }
199
-
200
- const countyOutput = counties.map(county => {
201
- // Map the name from the geo data with the appropriate key for the processed data
202
- let geoKey = county.id
203
-
204
- if (!geoKey) return
205
-
206
- let countyPath = path(county)
207
-
208
- let geoData = data[county.id]
209
- let legendColors
210
-
211
- // Once we receive data for this geographic item, setup variables.
212
- if (geoData !== undefined) {
213
- legendColors = applyLegendToRow(geoData)
214
- }
215
-
216
- const geoDisplayName = displayGeoName(geoKey)
217
-
218
- // For some reason, these two geos are breaking the display.
219
- if (geoDisplayName === 'Franklin City' || geoDisplayName === 'Waynesboro') return null
220
-
221
- const toolTip = applyTooltipsToGeo(geoDisplayName, geoData)
222
-
223
- if (legendColors && legendColors[0] !== '#000000') {
224
- let styles = {
225
- fill: legendColors[0],
226
- cursor: 'default',
227
- '&:hover': {
228
- fill: legendColors[1]
229
- },
230
- '&:active': {
231
- fill: legendColors[2]
232
- }
233
- }
234
-
235
- // When to add pointer cursor
236
- if ((state.columns.navigate && geoData[state.columns.navigate.name]) || state.tooltips.appearanceType === 'hover') {
237
- styles.cursor = 'pointer'
238
- }
88
+ // Push state lines
89
+ geosJsx.push(
90
+ // prettier-ignore
91
+ <SingleState.StateOutput
92
+ topoData={topoData}
93
+ path={path}
94
+ scale={scale}
95
+ />
96
+ )
239
97
 
240
- return (
241
- <g key={`key--${county.id}`} className={`county county--${geoDisplayName.split(' ').join('')} county--${geoData[state.columns.geo.name]}`} style={styles} onClick={() => geoClickHandler(geoDisplayName, geoData)} data-tooltip-id={`tooltip__${tooltipId}`} data-tooltip-html={toolTip}>
242
- <path tabIndex={-1} className={`county`} stroke={geoStrokeColor} d={countyPath} strokeWidth={0.75 / scale} />
243
- </g>
244
- )
245
- } else {
246
- return (
247
- <g key={`key--${county.id}`} className={`county county--${geoDisplayName.split(' ').join('')}`} style={{ fill: '#e6e6e6' }} data-tooltip-id={`tooltip__${tooltipId}`} data-tooltip-html={toolTip}>
248
- <path tabIndex={-1} className={`county`} stroke={geoStrokeColor} d={countyPath} strokeWidth={0.75 / scale} />
249
- </g>
250
- )
251
- }
252
- })
98
+ // Push county lines
99
+ geosJsx.push(
100
+ // prettier-ignore
101
+ <SingleState.CountyOutput
102
+ counties={counties}
103
+ scale={scale}
104
+ geoStrokeColor={geoStrokeColor}
105
+ tooltipId={tooltipId}
106
+ path={path}
107
+ />
108
+ )
253
109
 
254
- geosJsx.push(<StateOutput />)
255
- geosJsx.push(countyOutput)
110
+ // Push city list
256
111
  geosJsx.push(
257
112
  <CityList
258
113
  projection={cityListProjection}
@@ -272,14 +127,91 @@ const SingleStateMap = props => {
272
127
 
273
128
  return geosJsx
274
129
  }
275
-
276
130
  return (
277
131
  <ErrorBoundary component='SingleStateMap'>
278
- {stateToShow && (
279
- <svg viewBox={`0 0 ${WIDTH} ${HEIGHT}`} preserveAspectRatio='xMinYMin' className='svg-container' role='img' aria-label={handleMapAriaLabels(state)}>
280
- <rect className='background center-container ocean' width={WIDTH} height={HEIGHT} fillOpacity={1} fill='white'></rect>
132
+ {statePicked && state.general.allowMapZoom && state.general.statePicked.fipsCode && (
133
+ <svg
134
+ viewBox={`0 0 ${WIDTH} ${HEIGHT}`}
135
+ preserveAspectRatio='xMinYMin'
136
+ className='svg-container'
137
+ role='img'
138
+ aria-label={handleMapAriaLabels(state)}
139
+ >
140
+ <ZoomableGroup
141
+ center={position.coordinates}
142
+ zoom={position.zoom}
143
+ minZoom={1} // Adjust this value if needed
144
+ maxZoom={4} // Adjust this value to limit the maximum zoom level
145
+ onMoveEnd={handleMoveEnd}
146
+ projection={projection}
147
+ width={880}
148
+ height={500}
149
+ >
150
+ <rect
151
+ className='background center-container ocean'
152
+ width={WIDTH}
153
+ height={HEIGHT}
154
+ fillOpacity={1}
155
+ fill='white'
156
+ ></rect>
157
+ <CustomProjection
158
+ data={[
159
+ {
160
+ states: topoData?.states,
161
+ counties: topoData.counties.filter(c => c.id.substring(0, 2) === state.general.statePicked.fipsCode)
162
+ }
163
+ ]}
164
+ projection={geoAlbersUsaTerritories}
165
+ fitExtent={[
166
+ [
167
+ [PADDING, PADDING],
168
+ [WIDTH - PADDING, HEIGHT - PADDING]
169
+ ],
170
+ stateToShow
171
+ ]}
172
+ >
173
+ {({ features, projection }) => {
174
+ return (
175
+ <g
176
+ id='mapGroup'
177
+ className={`countyMapGroup ${
178
+ state.general.geoType === 'single-state' ? `countyMapGroup--no-transition` : ''
179
+ }`}
180
+ transform={`translate(${translate}) scale(${scale})`}
181
+ data-scale=''
182
+ key='countyMapGroup'
183
+ >
184
+ {constructGeoJsx(features, projection)}
185
+ </g>
186
+ )
187
+ }}
188
+ </CustomProjection>
189
+ {state.annotations.length > 0 && <Annotation.Draggable />}
190
+ </ZoomableGroup>
191
+ </svg>
192
+ )}
193
+ {statePicked && !state.general.allowMapZoom && state.general.statePicked.fipsCode && (
194
+ <svg
195
+ viewBox={`0 0 ${WIDTH} ${HEIGHT}`}
196
+ preserveAspectRatio='xMinYMin'
197
+ className='svg-container'
198
+ role='img'
199
+ aria-label={handleMapAriaLabels(state)}
200
+ >
201
+ <rect
202
+ className='background center-container ocean'
203
+ width={WIDTH}
204
+ height={HEIGHT}
205
+ fillOpacity={1}
206
+ fill='white'
207
+ ></rect>
281
208
  <CustomProjection
282
- data={[{ states: stateToShow, counties: topoData.counties.filter(c => c.id.substring(0, 2) === state.general.statePicked.fipsCode) }]}
209
+ data={[
210
+ {
211
+ states: topoData?.states,
212
+ counties: topoData.counties.filter(c => c.id.substring(0, 2) === state.general.statePicked.fipsCode)
213
+ }
214
+ ]}
283
215
  projection={geoAlbersUsaTerritories}
284
216
  fitExtent={[
285
217
  [
@@ -291,7 +223,15 @@ const SingleStateMap = props => {
291
223
  >
292
224
  {({ features, projection }) => {
293
225
  return (
294
- <g id='mapGroup' className='countyMapGroup' transform={`translate(${translate}) scale(${scale})`} data-scale='' key='countyMapGroup'>
226
+ <g
227
+ id='mapGroup'
228
+ className={`countyMapGroup ${
229
+ state.general.geoType === 'single-state' ? `countyMapGroup--no-transition` : ''
230
+ }`}
231
+ transform={`translate(${translate}) scale(${scale})`}
232
+ data-scale=''
233
+ key='countyMapGroup'
234
+ >
295
235
  {constructGeoJsx(features, projection)}
296
236
  </g>
297
237
  )
@@ -300,7 +240,34 @@ const SingleStateMap = props => {
300
240
  {state.annotations.length > 0 && <Annotation.Draggable />}
301
241
  </svg>
302
242
  )}
303
- {!state.general.statePicked && 'No State Picked'}
243
+
244
+ {checkForNoData() && (
245
+ <svg
246
+ viewBox={`0 0 ${WIDTH} ${HEIGHT}`}
247
+ preserveAspectRatio='xMinYMin'
248
+ className='svg-container'
249
+ role='img'
250
+ aria-label={handleMapAriaLabels(state)}
251
+ >
252
+ <Text
253
+ verticalAnchor='start'
254
+ textAnchor='middle'
255
+ x={WIDTH / 2}
256
+ width={WIDTH}
257
+ y={HEIGHT / 2}
258
+ fontSize={18}
259
+ style={{ fontSize: '28px', height: '18px' }}
260
+ >
261
+ {state.general.noStateFoundMessage}
262
+ </Text>
263
+ </svg>
264
+ )}
265
+ <ZoomControls
266
+ // prettier-ignore
267
+ handleZoomIn={handleZoomIn}
268
+ handleZoomOut={handleZoomOut}
269
+ handleReset={handleReset}
270
+ />
304
271
  </ErrorBoundary>
305
272
  )
306
273
  }