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