@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.
- package/dist/cdcmap.js +40720 -38422
- package/examples/county-year.csv +10 -0
- package/examples/default-geocode.json +44 -10
- package/examples/default-patterns.json +0 -2
- package/examples/default-single-state.json +279 -108
- package/examples/map-issue-3.json +646 -0
- package/examples/single-state-filter.json +153 -0
- package/index.html +9 -6
- package/package.json +3 -3
- package/src/CdcMap.tsx +322 -126
- package/src/_stories/CdcMap.stories.tsx +7 -0
- package/src/_stories/_mock/DEV-8942.json +270 -0
- package/src/components/Annotation/AnnotationDropdown.tsx +1 -0
- package/src/components/{BubbleList.jsx → BubbleList.tsx} +1 -1
- package/src/components/{CityList.jsx → CityList.tsx} +28 -2
- package/src/components/{DataTable.jsx → DataTable.tsx} +2 -2
- package/src/components/EditorPanel/components/EditorPanel.tsx +647 -127
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +0 -22
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +61 -11
- package/src/components/Legend/components/Legend.tsx +125 -36
- package/src/components/Legend/components/index.scss +42 -42
- package/src/components/Modal.tsx +25 -0
- package/src/components/UsaMap/components/SingleState/SingleState.CountyOutput.tsx +74 -0
- package/src/components/UsaMap/components/SingleState/SingleState.StateOutput.tsx +29 -0
- package/src/components/UsaMap/components/SingleState/index.tsx +9 -0
- package/src/components/UsaMap/components/UsaMap.County.tsx +84 -33
- package/src/components/UsaMap/components/UsaMap.SingleState.tsx +173 -206
- package/src/components/UsaMap/components/UsaMap.State.tsx +161 -26
- package/src/components/UsaMap/data/us-extended-geography.json +1 -0
- package/src/components/UsaMap/helpers/map.ts +111 -0
- package/src/components/WorldMap/WorldMap.tsx +17 -32
- package/src/components/ZoomControls.tsx +41 -0
- package/src/data/initial-state.js +7 -1
- package/src/data/supported-geos.js +15 -4
- package/src/helpers/generateRuntimeLegendHash.ts +2 -2
- package/src/hooks/useStateZoom.tsx +157 -0
- package/src/hooks/{useZoomPan.js → useZoomPan.ts} +6 -5
- package/src/scss/editor-panel.scss +0 -4
- package/src/scss/main.scss +23 -1
- package/src/scss/map.scss +8 -0
- package/src/types/MapConfig.ts +9 -1
- package/src/types/MapContext.ts +14 -2
- package/src/components/Modal.jsx +0 -22
- /package/src/components/{Geo.jsx → Geo.tsx} +0 -0
- /package/src/components/{NavigationMenu.jsx → NavigationMenu.tsx} +0 -0
- /package/src/components/{ZoomableGroup.jsx → ZoomableGroup.tsx} +0 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { useContext, useEffect, useState } from 'react'
|
|
2
|
+
import ConfigContext from '../context'
|
|
3
|
+
import { geoAlbersUsaTerritories, GeoProjection } from 'd3-composite-projections'
|
|
4
|
+
import { MapContext } from '../types/MapContext'
|
|
5
|
+
import { geoPath, GeoPath } from 'd3-geo'
|
|
6
|
+
import _ from 'lodash'
|
|
7
|
+
import { getFilterControllingStatePicked } from '../components/UsaMap/helpers/map'
|
|
8
|
+
import { supportedStatesFipsCodes } from '../data/supported-geos'
|
|
9
|
+
|
|
10
|
+
interface StateData {
|
|
11
|
+
geometry: { type: 'MultiPolygon'; coordinates: number[][][][] }
|
|
12
|
+
// FIPS ID of US state
|
|
13
|
+
id: string
|
|
14
|
+
// name of US state
|
|
15
|
+
properties: { name: string }
|
|
16
|
+
type: 'Feature'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface Position {
|
|
20
|
+
zoom: number
|
|
21
|
+
coordinates: [number, number]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const useSetScaleAndTranslate = (topoData: { states: StateData[] }) => {
|
|
25
|
+
const { setTranslate, setScale, setStateToShow, setPosition, state, setState, runtimeData } = useContext<MapContext>(ConfigContext)
|
|
26
|
+
const statePicked = getFilterControllingStatePicked(state, runtimeData)
|
|
27
|
+
const defaultStateToShow = 'Alabama'
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const fipsCode = Object.keys(supportedStatesFipsCodes).find(key => supportedStatesFipsCodes[key] === statePicked)
|
|
30
|
+
const stateName = statePicked
|
|
31
|
+
const stateData = { fipsCode, stateName }
|
|
32
|
+
setScale(1)
|
|
33
|
+
setTranslate([0, 0])
|
|
34
|
+
setState({
|
|
35
|
+
...state,
|
|
36
|
+
general: {
|
|
37
|
+
...state.general,
|
|
38
|
+
statePicked: stateData
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
setStateToShow(topoData?.states?.find(s => s.properties.name === statePicked))
|
|
42
|
+
}, [topoData])
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
const fipsCode = Object.keys(supportedStatesFipsCodes).find(key => supportedStatesFipsCodes[key] === statePicked)
|
|
46
|
+
const stateName = statePicked
|
|
47
|
+
const stateData = { fipsCode, stateName }
|
|
48
|
+
|
|
49
|
+
setState({
|
|
50
|
+
...state,
|
|
51
|
+
general: {
|
|
52
|
+
...state.general,
|
|
53
|
+
statePicked: stateData
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
setScaleAndTranslate('reset')
|
|
57
|
+
}, [statePicked])
|
|
58
|
+
|
|
59
|
+
// SVG ITEMS
|
|
60
|
+
const WIDTH = 880
|
|
61
|
+
const HEIGHT = 500
|
|
62
|
+
const PADDING = 50
|
|
63
|
+
|
|
64
|
+
// TODO: same as city list projection?
|
|
65
|
+
const [projection, setProjection] = useState(() =>
|
|
66
|
+
geoAlbersUsaTerritories()
|
|
67
|
+
.translate([WIDTH / 2, HEIGHT / 2])
|
|
68
|
+
.scale(1)
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
/*
|
|
72
|
+
NORMALIZATION_FACTOR NOTES:
|
|
73
|
+
This is used during state switching,
|
|
74
|
+
I'm not sure why the value is 1070 but it does appear to work during the switching.
|
|
75
|
+
During zoom it does not work.
|
|
76
|
+
*/
|
|
77
|
+
const NORMALIZATION_FACTOR = 1070
|
|
78
|
+
const _statePickedData = topoData?.states?.find(s => s.properties.name === statePicked)
|
|
79
|
+
const newProjection = projection.fitExtent(
|
|
80
|
+
[
|
|
81
|
+
[PADDING, PADDING],
|
|
82
|
+
[WIDTH - PADDING, HEIGHT - PADDING]
|
|
83
|
+
],
|
|
84
|
+
_statePickedData
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
// Work for centering the state.
|
|
88
|
+
const newScale = newProjection.scale()
|
|
89
|
+
const normalizedScale = newScale / NORMALIZATION_FACTOR
|
|
90
|
+
let [x, y] = newProjection.translate()
|
|
91
|
+
x = x - WIDTH / 2
|
|
92
|
+
y = y - HEIGHT / 2
|
|
93
|
+
|
|
94
|
+
const path: GeoPath = geoPath().projection(projection)
|
|
95
|
+
const featureCenter = path.centroid(_statePickedData)
|
|
96
|
+
const stateCenter = newProjection.invert(featureCenter)
|
|
97
|
+
|
|
98
|
+
const switchState = () => {
|
|
99
|
+
setStateToShow(_statePickedData)
|
|
100
|
+
setScaleAndTranslate('reset')
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const setScaleAndTranslate = (zoomFunction: string = '') => {
|
|
104
|
+
setPosition((pos: Position) => {
|
|
105
|
+
let newZoom = pos.zoom
|
|
106
|
+
let newCoordinates = pos.coordinates
|
|
107
|
+
if (zoomFunction === 'zoomIn' && pos.zoom < 4) {
|
|
108
|
+
newZoom = pos.zoom * 1.5
|
|
109
|
+
newCoordinates = pos.coordinates[0] !== 0 && pos.coordinates[1] !== 0 ? pos.coordinates : stateCenter
|
|
110
|
+
} else if (zoomFunction === 'zoomOut' && pos.zoom > 1) {
|
|
111
|
+
newZoom = pos.zoom / 1.5
|
|
112
|
+
newCoordinates = pos.coordinates[0] !== 0 && pos.coordinates[1] !== 0 ? pos.coordinates : stateCenter
|
|
113
|
+
} else if (zoomFunction === 'reset') {
|
|
114
|
+
newZoom = 1
|
|
115
|
+
newCoordinates = stateCenter
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
zoom: newZoom,
|
|
119
|
+
coordinates: newCoordinates
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
if (zoomFunction === 'reset') {
|
|
124
|
+
setTranslate([0, 0]) // needed for state switcher
|
|
125
|
+
setScale(1) // needed for state switcher
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const handleZoomIn = () => {
|
|
130
|
+
setScaleAndTranslate('zoomIn')
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const handleZoomOut = () => {
|
|
134
|
+
setScaleAndTranslate('zoomOut')
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const handleMoveEnd = position => {
|
|
138
|
+
setPosition(position)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const handleReset = () => {
|
|
142
|
+
setScaleAndTranslate('reset')
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
statePicked,
|
|
147
|
+
setScaleAndTranslate,
|
|
148
|
+
switchState,
|
|
149
|
+
handleZoomIn,
|
|
150
|
+
handleZoomOut,
|
|
151
|
+
handleMoveEnd,
|
|
152
|
+
handleReset,
|
|
153
|
+
projection
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export default useSetScaleAndTranslate
|
|
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react'
|
|
|
2
2
|
import { zoom as d3Zoom, zoomIdentity as d3ZoomIdentity } from 'd3-zoom'
|
|
3
3
|
import { select as d3Select } from 'd3-selection'
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
const getCoords = (w, h, t) => {
|
|
6
6
|
const xOffset = (w * t.k - w) / 2
|
|
7
7
|
const yOffset = (h * t.k - h) / 2
|
|
8
8
|
return [w / 2 - (xOffset + t.x) / t.k, h / 2 - (yOffset + t.y) / t.k]
|
|
@@ -39,12 +39,12 @@ export default function useZoomPan({
|
|
|
39
39
|
useEffect(() => {
|
|
40
40
|
const svg = d3Select(mapRef.current)
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
const handleZoomStart = d3Event => {
|
|
43
43
|
if (!onMoveStart || bypassEvents.current) return
|
|
44
44
|
onMoveStart({ coordinates: projection.invert(getCoords(width, height, d3Event.transform)), zoom: d3Event.transform.k }, d3Event)
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
const handleZoom = d3Event => {
|
|
48
48
|
if (bypassEvents.current) return
|
|
49
49
|
const { transform, sourceEvent } = d3Event
|
|
50
50
|
setPosition({ x: transform.x, y: transform.y, k: transform.k, dragging: sourceEvent })
|
|
@@ -52,7 +52,7 @@ export default function useZoomPan({
|
|
|
52
52
|
onMove({ x: transform.x, y: transform.y, k: transform.k, dragging: sourceEvent }, d3Event)
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
const handleZoomEnd = d3Event => {
|
|
56
56
|
if (bypassEvents.current) {
|
|
57
57
|
bypassEvents.current = false
|
|
58
58
|
return
|
|
@@ -63,7 +63,7 @@ export default function useZoomPan({
|
|
|
63
63
|
onMoveEnd({ coordinates: [x, y], zoom: d3Event.transform.k }, d3Event)
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
const filterFunc = d3Event => {
|
|
67
67
|
if (filterZoomEvent) {
|
|
68
68
|
return filterZoomEvent(d3Event)
|
|
69
69
|
}
|
|
@@ -89,6 +89,7 @@ export default function useZoomPan({
|
|
|
89
89
|
if (lon === lastPosition.current.x && lat === lastPosition.current.y && zoom === lastPosition.current.k) return
|
|
90
90
|
|
|
91
91
|
const coords = projection([lon, lat])
|
|
92
|
+
if (!coords) return
|
|
92
93
|
const x = coords[0] * zoom
|
|
93
94
|
const y = coords[1] * zoom
|
|
94
95
|
const svg = d3Select(mapRef.current)
|
package/src/scss/main.scss
CHANGED
|
@@ -5,6 +5,21 @@
|
|
|
5
5
|
@import 'filters';
|
|
6
6
|
@import '@cdc/core/styles/v2/components/ui/tooltip';
|
|
7
7
|
|
|
8
|
+
.type-map--has-error {
|
|
9
|
+
overflow: hidden !important;
|
|
10
|
+
height: 100vh;
|
|
11
|
+
|
|
12
|
+
.waiting {
|
|
13
|
+
display: flex;
|
|
14
|
+
align-content: center;
|
|
15
|
+
flex-wrap: wrap;
|
|
16
|
+
display: flex;
|
|
17
|
+
align-content: center;
|
|
18
|
+
flex-wrap: wrap;
|
|
19
|
+
height: 100vh;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
8
23
|
.cdc-map-outer-container {
|
|
9
24
|
position: relative;
|
|
10
25
|
display: flex; // Needed for the main content
|
|
@@ -58,6 +73,14 @@
|
|
|
58
73
|
display: flex;
|
|
59
74
|
position: relative;
|
|
60
75
|
flex-direction: column;
|
|
76
|
+
|
|
77
|
+
&.bottom {
|
|
78
|
+
flex-direction: column;
|
|
79
|
+
}
|
|
80
|
+
&.top {
|
|
81
|
+
flex-direction: column-reverse;
|
|
82
|
+
}
|
|
83
|
+
|
|
61
84
|
&.modal-background {
|
|
62
85
|
position: relative;
|
|
63
86
|
&::before {
|
|
@@ -142,7 +165,6 @@
|
|
|
142
165
|
|
|
143
166
|
p.subtext {
|
|
144
167
|
font-size: 0.9em;
|
|
145
|
-
padding: 0 0.8em 0.8em;
|
|
146
168
|
em {
|
|
147
169
|
font-style: italic;
|
|
148
170
|
}
|
package/src/scss/map.scss
CHANGED
|
@@ -258,6 +258,10 @@ $medium: 768px;
|
|
|
258
258
|
stroke: none !important;
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
+
.countyMapGroup--no-transition {
|
|
262
|
+
transition: none !important;
|
|
263
|
+
}
|
|
264
|
+
|
|
261
265
|
// .state {
|
|
262
266
|
// display: none;
|
|
263
267
|
// }
|
|
@@ -374,3 +378,7 @@ canvas {
|
|
|
374
378
|
pointer-events: none;
|
|
375
379
|
display: none;
|
|
376
380
|
}
|
|
381
|
+
|
|
382
|
+
.data-table-container {
|
|
383
|
+
margin: 5px 0px !important;
|
|
384
|
+
}
|
package/src/types/MapConfig.ts
CHANGED
|
@@ -33,6 +33,7 @@ type PatternSelection = {
|
|
|
33
33
|
label: string
|
|
34
34
|
// size of pattern
|
|
35
35
|
size: 'small' | 'medium' | 'large'
|
|
36
|
+
contrastCheck: boolean
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
export type GeoColumnProperties = Pick<EditorColumnProperties, 'name' | 'label' | 'tooltip' | 'dataTable'>
|
|
@@ -40,7 +41,7 @@ export type LatitudeColumnProperties = Pick<EditorColumnProperties, 'name'>
|
|
|
40
41
|
export type LongitudeColumnProperties = Pick<EditorColumnProperties, 'name'>
|
|
41
42
|
export type NavigateColumnProperties = Pick<EditorColumnProperties, 'name'>
|
|
42
43
|
export type PrimaryColumnProperties = Pick<EditorColumnProperties, 'dataTable' | 'label' | 'name' | 'prefix' | 'suffix' | 'tooltip'>
|
|
43
|
-
|
|
44
|
+
export type ViewportSize = 'xxs' | 'xs' | 'sm' | 'md' | 'lg'
|
|
44
45
|
export type LegendShapeItem = {
|
|
45
46
|
column: string
|
|
46
47
|
key: string
|
|
@@ -123,6 +124,11 @@ export type MapConfig = Visualization & {
|
|
|
123
124
|
numberOfItems: number
|
|
124
125
|
position: string
|
|
125
126
|
title: string
|
|
127
|
+
style: 'circles' | 'boxes' | 'gradient'
|
|
128
|
+
subStyle: 'linear blocks' | 'smooth'
|
|
129
|
+
tickRotation: string
|
|
130
|
+
hideBorder: false
|
|
131
|
+
singleColumnLegend: false
|
|
126
132
|
}
|
|
127
133
|
table: {
|
|
128
134
|
label: string
|
|
@@ -144,6 +150,8 @@ export type MapConfig = Visualization & {
|
|
|
144
150
|
}
|
|
145
151
|
runtime: {
|
|
146
152
|
editorErrorMessage: string[]
|
|
153
|
+
// when a single state map doesn't include a fips code show a message...
|
|
154
|
+
noStateFoundMessage: string
|
|
147
155
|
}
|
|
148
156
|
mapPosition: { coordinates: Coordinate; zoom: number }
|
|
149
157
|
map: {
|
package/src/types/MapContext.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { type MapConfig } from './MapConfig'
|
|
1
|
+
import { type MapConfig, type ViewportSize } from './MapConfig'
|
|
2
2
|
|
|
3
3
|
export type MapContext = {
|
|
4
4
|
applyLegendToRow
|
|
5
5
|
applyTooltipsToGeo
|
|
6
6
|
closeModal
|
|
7
7
|
columnsInData
|
|
8
|
-
currentViewport
|
|
8
|
+
currentViewport: ViewportSize
|
|
9
9
|
data
|
|
10
10
|
displayDataAsText
|
|
11
11
|
displayGeoName
|
|
@@ -22,6 +22,7 @@ export type MapContext = {
|
|
|
22
22
|
isDashboard
|
|
23
23
|
isDebug
|
|
24
24
|
isEditor
|
|
25
|
+
isFilterValueSupported: boolean
|
|
25
26
|
loadConfig
|
|
26
27
|
navigationHandler
|
|
27
28
|
position
|
|
@@ -44,4 +45,15 @@ export type MapContext = {
|
|
|
44
45
|
supportedTerritories
|
|
45
46
|
titleCase
|
|
46
47
|
viewport
|
|
48
|
+
setStateToShow: (string) => void
|
|
49
|
+
stateToShow: string
|
|
50
|
+
scale: number
|
|
51
|
+
translate: [number, number]
|
|
52
|
+
topoData
|
|
53
|
+
setScale: (number) => void
|
|
54
|
+
setTranslate: ([x, y]: [number, number]) => void
|
|
55
|
+
runtimeData: Object[]
|
|
56
|
+
tooltipId: string
|
|
57
|
+
setTopoData: Function
|
|
58
|
+
getTextWidth: (text: string, font: string) => string | undefined
|
|
47
59
|
}
|
package/src/components/Modal.jsx
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import React, { useContext } from 'react'
|
|
2
|
-
import closeIcon from '../images/close.svg?inline'
|
|
3
|
-
import LegendCircle from '@cdc/core/components/LegendCircle'
|
|
4
|
-
import ConfigContext from '../context'
|
|
5
|
-
|
|
6
|
-
const Modal = props => {
|
|
7
|
-
const { applyTooltipsToGeo, capitalize, applyLegendToRow, viewport, type, content } = useContext(ConfigContext)
|
|
8
|
-
|
|
9
|
-
const tooltip = applyTooltipsToGeo(content.geoName, content.keyedData, 'jsx')
|
|
10
|
-
|
|
11
|
-
const legendColors = applyLegendToRow(content.keyedData)
|
|
12
|
-
|
|
13
|
-
return (
|
|
14
|
-
<section className={capitalize ? 'modal-content tooltip capitalize ' + viewport : 'modal-content tooltip ' + viewport} aria-hidden='true'>
|
|
15
|
-
<img src={closeIcon} className='modal-close' alt='Close Modal' />
|
|
16
|
-
{type === 'data' && <LegendCircle fill={legendColors[0]} />}
|
|
17
|
-
<div className='content'>{tooltip}</div>
|
|
18
|
-
</section>
|
|
19
|
-
)
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export default Modal
|
|
File without changes
|
|
File without changes
|
|
File without changes
|