@cdc/map 4.23.2 → 4.23.4
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 +24661 -24191
- package/examples/custom-map-layers.json +764 -0
- package/examples/default-county.json +169 -155
- package/examples/default-geocode.json +744 -744
- package/examples/default-hex.json +3 -5
- package/examples/example-city-state-no-territories.json +703 -0
- package/examples/example-city-state.json +26 -7
- package/examples/example-city-stateBAD.json +744 -0
- package/examples/gallery/city-state.json +478 -478
- package/examples/testing-layer-2.json +1 -0
- package/examples/testing-layer.json +96 -0
- package/examples/world-geocode-data.json +18 -0
- package/examples/world-geocode.json +108 -0
- package/index.html +35 -29
- package/package.json +6 -3
- package/src/CdcMap.jsx +179 -111
- package/src/components/CityList.jsx +35 -35
- package/src/components/CountyMap.jsx +309 -446
- package/src/components/DataTable.jsx +7 -31
- package/src/components/EditorPanel.jsx +468 -217
- package/src/components/Sidebar.jsx +2 -0
- package/src/components/UsaMap.jsx +34 -23
- package/src/components/WorldMap.jsx +40 -8
- package/src/data/feature-test.json +73 -0
- package/src/data/initial-state.js +10 -3
- package/src/data/supported-geos.js +7 -7
- 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/scss/map.scss +12 -0
- package/src/test/CdcMap.test.jsx +19 -0
- package/vite.config.js +3 -3
- package/src/components/Filters.jsx +0 -113
- package/src/hooks/useColorPalette.ts +0 -88
|
@@ -1,70 +1,72 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
import Loading from '@cdc/core/components/Loading'
|
|
1
|
+
import React, { useEffect, useState, memo, useRef } from 'react'
|
|
3
2
|
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import { geoCentroid, geoPath } from 'd3-geo'
|
|
7
|
-
import { feature, mesh } from 'topojson-client'
|
|
8
|
-
import { CustomProjection } from '@visx/geo'
|
|
9
|
-
import colorPalettes from '../../../core/data/colorPalettes'
|
|
3
|
+
import { geoCentroid, geoPath, geoContains } from 'd3-geo'
|
|
4
|
+
import { feature } from 'topojson-client'
|
|
10
5
|
import { geoAlbersUsaTerritories } from 'd3-composite-projections'
|
|
11
|
-
import
|
|
12
|
-
|
|
13
|
-
import
|
|
14
|
-
|
|
15
|
-
// Label lines for smaller states/geo labels
|
|
16
|
-
const offsets = {
|
|
17
|
-
Vermont: [ 50, -8 ],
|
|
18
|
-
'New Hampshire': [ 34, 5 ],
|
|
19
|
-
Massachusetts: [ 30, -5 ],
|
|
20
|
-
'Rhode Island': [ 28, 4 ],
|
|
21
|
-
Connecticut: [ 35, 16 ],
|
|
22
|
-
'New Jersey': [ 42, 0 ],
|
|
23
|
-
Delaware: [ 33, 0 ],
|
|
24
|
-
Maryland: [ 47, 10 ],
|
|
25
|
-
'District of Columbia': [ 30, 20 ],
|
|
26
|
-
'Puerto Rico': [ 10, -20 ],
|
|
27
|
-
'Virgin Islands': [ 10, -10 ],
|
|
28
|
-
Guam: [ 10, -5 ],
|
|
29
|
-
'American Samoa': [ 10, 0 ]
|
|
30
|
-
}
|
|
6
|
+
import debounce from 'lodash.debounce'
|
|
7
|
+
|
|
8
|
+
import Loading from '@cdc/core/components/Loading'
|
|
9
|
+
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
31
10
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
11
|
+
import topoJSON from '../data/county-map.json'
|
|
12
|
+
import { formatPrefix } from 'd3'
|
|
13
|
+
import useMapLayers from '../hooks/useMapLayers'
|
|
14
|
+
|
|
15
|
+
const sortById = (a, b) => {
|
|
16
|
+
if (a.id < b.id) return -1
|
|
17
|
+
if (b.id < a.id) return 1
|
|
18
|
+
return 0
|
|
19
|
+
}
|
|
36
20
|
|
|
37
21
|
// DATA
|
|
38
|
-
|
|
39
|
-
|
|
22
|
+
const { features: counties } = feature(topoJSON, topoJSON.objects.counties)
|
|
23
|
+
const { features: states } = feature(topoJSON, topoJSON.objects.states)
|
|
24
|
+
|
|
25
|
+
// Sort states/counties data to ensure that countyIndecies logic below works
|
|
26
|
+
states.sort(sortById)
|
|
27
|
+
counties.sort(sortById)
|
|
28
|
+
const mapData = states.concat(counties).filter(geo => geo.id !== '51620') //Not sure why, but Franklin City, VA is very broken and messes up the rendering
|
|
29
|
+
|
|
30
|
+
// For each state, find the beginning index and end index of the counties in that state in the mapData array
|
|
31
|
+
// For use in hover logic (only check counties for hover in the state that is being hovered, instead of checking all counties)
|
|
32
|
+
const countyIndecies = {}
|
|
33
|
+
states.forEach(state => {
|
|
34
|
+
let minIndex = mapData.length - 1
|
|
35
|
+
let maxIndex = 0
|
|
36
|
+
|
|
37
|
+
for (let i = 0; i < mapData.length; i++) {
|
|
38
|
+
if (mapData[i].id.length > 2 && mapData[i].id.indexOf(state.id) === 0) {
|
|
39
|
+
if (i < minIndex) minIndex = i
|
|
40
|
+
if (i > maxIndex) maxIndex = i
|
|
41
|
+
}
|
|
42
|
+
}
|
|
40
43
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const STATE_INACTIVE_FILL = '#F4F7FA'
|
|
44
|
+
countyIndecies[state.id] = [minIndex, maxIndex]
|
|
45
|
+
})
|
|
44
46
|
|
|
45
47
|
// CREATE STATE LINES
|
|
46
|
-
const projection = geoAlbersUsaTerritories()
|
|
47
|
-
const path = geoPath().projection(projection)
|
|
48
|
-
const stateLines = path(mesh(testJSON, testJSON.objects.states))
|
|
49
|
-
const countyLines = path(mesh(testJSON, testJSON.objects.counties))
|
|
48
|
+
const projection = geoAlbersUsaTerritories()
|
|
50
49
|
|
|
50
|
+
// Ensures county map is only rerendered when it needs to (when one of the variables below is updated)
|
|
51
51
|
function CountyMapChecks(prevState, nextState) {
|
|
52
52
|
const equalNumberOptIn = prevState.state.general.equalNumberOptIn && nextState.state.general.equalNumberOptIn
|
|
53
53
|
const equalColumnName = prevState.state.general.type && nextState.state.general.type
|
|
54
54
|
const equalNavColumn = prevState.state.columns.navigate && nextState.state.columns.navigate
|
|
55
55
|
const equalLegend = prevState.runtimeLegend === nextState.runtimeLegend
|
|
56
56
|
const equalBorderColors = prevState.state.general.geoBorderColor === nextState.state.general.geoBorderColor // update when geoborder color changes
|
|
57
|
-
const equalMapColors = prevState.state.color === nextState.state.color // update when map colors change
|
|
58
57
|
const equalData = prevState.data === nextState.data // update when data changes
|
|
59
|
-
|
|
58
|
+
const equalTooltipBehavior = prevState.state.tooltips.appearanceType === nextState.state.tooltips.appearanceType
|
|
59
|
+
return equalData && equalBorderColors && equalLegend && equalColumnName && equalNavColumn && equalNumberOptIn && equalTooltipBehavior ? true : false
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
const CountyMap = props => {
|
|
63
|
-
|
|
63
|
+
const { state, runtimeLegend, applyTooltipsToGeo, data, geoClickHandler, applyLegendToRow, displayGeoName, containerEl, handleMapAriaLabels } = props
|
|
64
|
+
|
|
65
|
+
const [focus, setFocus] = useState({})
|
|
64
66
|
|
|
65
|
-
const
|
|
67
|
+
const pathGenerator = geoPath().projection(geoAlbersUsaTerritories())
|
|
66
68
|
|
|
67
|
-
|
|
69
|
+
const { featureArray } = useMapLayers(state, '', pathGenerator, false)
|
|
68
70
|
|
|
69
71
|
useEffect(() => {
|
|
70
72
|
if (containerEl) {
|
|
@@ -74,448 +76,309 @@ const CountyMap = props => {
|
|
|
74
76
|
}
|
|
75
77
|
})
|
|
76
78
|
|
|
77
|
-
// Use State
|
|
78
|
-
const [ scale, setScale ] = useState(0.85)
|
|
79
|
-
const [ startingLineWidth, setStartingLineWidth ] = useState(1.3)
|
|
80
|
-
const [ translate, setTranslate ] = useState([ 0, 0 ])
|
|
81
|
-
const [ mapColorPalette, setMapColorPalette ] = useState(colorPalettes[state.color] || '#fff')
|
|
82
|
-
const [ focusedState, setFocusedState ] = useState(null)
|
|
83
|
-
const [ showLabel, setShowLabels ] = useState(true)
|
|
84
|
-
|
|
85
79
|
const resetButton = useRef()
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
const mapGroup = useRef()
|
|
89
|
-
|
|
90
|
-
let focusedBorderColor = mapColorPalette[3]
|
|
91
|
-
let geoStrokeColor = state.general.geoBorderColor === 'darkGray' ? 'rgba(0, 0, 0, 0.2)' : 'rgba(255,255,255,0.7)'
|
|
92
|
-
|
|
93
|
-
const geoLabel = (geo, projection) => {
|
|
94
|
-
let [ x, y ] = projection(geoCentroid(geo))
|
|
95
|
-
let abbr = abbrs[geo.properties.name]
|
|
96
|
-
if (abbr === 'NJ') x += 3
|
|
97
|
-
if (undefined === abbr) return null
|
|
98
|
-
let [ dx, dy ] = offsets[geo.properties.name]
|
|
99
|
-
|
|
100
|
-
return (
|
|
101
|
-
<>
|
|
102
|
-
<line className="abbrLine" x1={x} y1={y} x2={x + dx} y2={y + dy} stroke="black" strokeWidth={0.85}/>
|
|
103
|
-
<text className="abbrText" x={4} strokeWidth="0" fontSize={13} style={{ fill: '#202020' }} alignmentBaseline="middle" transform={`translate(${x + dx}, ${y + dy})`}>
|
|
104
|
-
{abbr}
|
|
105
|
-
</text>
|
|
106
|
-
</>
|
|
107
|
-
)
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const focusGeo = (geoKey, geo) => {
|
|
111
|
-
if (!geoKey) {
|
|
112
|
-
console.log('County Map: no geoKey provided to focusGeo')
|
|
113
|
-
return
|
|
114
|
-
}
|
|
80
|
+
const canvasRef = useRef()
|
|
81
|
+
const tooltipRef = useRef()
|
|
115
82
|
|
|
116
|
-
|
|
117
|
-
let myState = states.find(s => s.id === geoKey)
|
|
118
|
-
|
|
119
|
-
// 2) Set projections translation & scale to the geographic center of the passed geo.
|
|
120
|
-
const projection = geoAlbersUsaTerritories().translate([ WIDTH / 2, HEIGHT / 2 ])
|
|
121
|
-
const newProjection = projection.fitExtent(
|
|
122
|
-
[
|
|
123
|
-
[ PADDING, PADDING ],
|
|
124
|
-
[ WIDTH - PADDING, HEIGHT - PADDING ]
|
|
125
|
-
],
|
|
126
|
-
myState
|
|
127
|
-
)
|
|
128
|
-
|
|
129
|
-
// 3) Gets the new scale
|
|
130
|
-
const newScale = newProjection.scale()
|
|
131
|
-
const hypot = Math.hypot(880, 500)
|
|
132
|
-
const newScaleWithHypot = newScale / 1070
|
|
133
|
-
|
|
134
|
-
// 4) Pull the x & y out, divide by half the viewport for some reason
|
|
135
|
-
let [ x, y ] = newProjection.translate()
|
|
136
|
-
x = x - WIDTH / 2
|
|
137
|
-
y = y - HEIGHT / 2
|
|
138
|
-
|
|
139
|
-
// 5) Debug if needed
|
|
140
|
-
const debug = {
|
|
141
|
-
width: WIDTH,
|
|
142
|
-
height: HEIGHT,
|
|
143
|
-
beginX: 0,
|
|
144
|
-
beginY: 0,
|
|
145
|
-
hypot: hypot,
|
|
146
|
-
x: x,
|
|
147
|
-
y: y,
|
|
148
|
-
newScale: newScale,
|
|
149
|
-
newScaleWithHypot: newScaleWithHypot,
|
|
150
|
-
geoKey: geoKey,
|
|
151
|
-
geo: geo
|
|
152
|
-
}
|
|
153
|
-
//console.table(debug)
|
|
154
|
-
|
|
155
|
-
mapGroup.current.setAttribute('transform', `translate(${[ x, y ]}) scale(${newScaleWithHypot})`)
|
|
156
|
-
resetButton.current.style.display = 'block'
|
|
83
|
+
const runtimeKeys = Object.keys(data)
|
|
157
84
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
let allCounties = document.querySelectorAll('.county path')
|
|
161
|
-
let currentState = document.querySelector(`.state--${myState.id}`)
|
|
162
|
-
let otherStates = document.querySelectorAll(`.state:not(.state--${myState.id})`)
|
|
163
|
-
let svgContainer = document.querySelector('.svg-container')
|
|
164
|
-
svgContainer.setAttribute('data-scaleZoom', newScaleWithHypot)
|
|
85
|
+
const geoStrokeColor = state.general.geoBorderColor === 'darkGray' ? 'rgb(90, 90, 90)' : 'rgb(255, 255, 255)'
|
|
86
|
+
const lineWidth = 0.3
|
|
165
87
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
88
|
+
const onReset = () => {
|
|
89
|
+
setFocus({})
|
|
90
|
+
}
|
|
169
91
|
|
|
170
|
-
|
|
92
|
+
const canvasClick = e => {
|
|
93
|
+
const canvas = e.target
|
|
94
|
+
const canvasBounds = canvas.getBoundingClientRect()
|
|
95
|
+
const x = e.clientX - canvasBounds.left
|
|
96
|
+
const y = e.clientY - canvasBounds.top
|
|
97
|
+
const pointCoordinates = projection.invert([x, y])
|
|
98
|
+
|
|
99
|
+
// Use d3 geoContains method to find the state geo data that the user clicked inside
|
|
100
|
+
let clickedState
|
|
101
|
+
for (let i = 0; i < states.length; i++) {
|
|
102
|
+
if (geoContains(states[i], pointCoordinates)) {
|
|
103
|
+
clickedState = states[i]
|
|
104
|
+
break
|
|
105
|
+
}
|
|
106
|
+
}
|
|
171
107
|
|
|
172
|
-
|
|
108
|
+
// If the user clicked outside of all states, no behavior
|
|
109
|
+
if (clickedState) {
|
|
110
|
+
// If a county within the state was also clicked and has data, call parent click handler
|
|
111
|
+
if (countyIndecies[clickedState.id]) {
|
|
112
|
+
let county
|
|
113
|
+
for (let i = countyIndecies[clickedState.id][0]; i <= countyIndecies[clickedState.id][1]; i++) {
|
|
114
|
+
if (geoContains(mapData[i], pointCoordinates)) {
|
|
115
|
+
county = mapData[i]
|
|
116
|
+
break
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (county && data[county.id]) {
|
|
120
|
+
geoClickHandler(displayGeoName(county.id), data[county.id])
|
|
121
|
+
}
|
|
122
|
+
}
|
|
173
123
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
124
|
+
// Redraw with focus on state
|
|
125
|
+
setFocus({ id: clickedState.id, center: geoCentroid(clickedState) })
|
|
126
|
+
}
|
|
177
127
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
128
|
+
if (state.general.type === 'us-geocode') {
|
|
129
|
+
const geoRadius = (state.visual.geoCodeCircleSize || 5) * (focus.id ? 2 : 1)
|
|
130
|
+
let clickedGeo
|
|
131
|
+
for (let i = 0; i < runtimeKeys.length; i++) {
|
|
132
|
+
const pixelCoords = projection([data[runtimeKeys[i]][state.columns.longitude.name], data[runtimeKeys[i]][state.columns.latitude.name]])
|
|
133
|
+
if (pixelCoords && Math.sqrt(Math.pow(pixelCoords[0] - x, 2) + Math.pow(pixelCoords[1] - y, 2)) < geoRadius) {
|
|
134
|
+
clickedGeo = data[runtimeKeys[i]]
|
|
135
|
+
break
|
|
136
|
+
}
|
|
137
|
+
}
|
|
181
138
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
//focusedBorderPath.current.setAttribute('stroke', focusedBorderColor)
|
|
139
|
+
if (clickedGeo) {
|
|
140
|
+
geoClickHandler(displayGeoName(clickedGeo[state.columns.geo.name]), clickedGeo)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
187
143
|
}
|
|
188
144
|
|
|
189
|
-
const
|
|
190
|
-
if (state.
|
|
191
|
-
e.preventDefault()
|
|
192
|
-
const svg = document.querySelector('.svg-container')
|
|
193
|
-
|
|
194
|
-
svg.setAttribute('data-scaleZoom', 0)
|
|
145
|
+
const canvasHover = e => {
|
|
146
|
+
if (!tooltipRef.current || state.tooltips.appearanceType !== 'hover' || window.matchMedia('(any-hover: none)').matches) return
|
|
195
147
|
|
|
196
|
-
|
|
197
|
-
|
|
148
|
+
const canvas = e.target
|
|
149
|
+
const canvasBounds = canvas.getBoundingClientRect()
|
|
150
|
+
const x = e.clientX - canvasBounds.left
|
|
151
|
+
const y = e.clientY - canvasBounds.top
|
|
152
|
+
let pointCoordinates = projection.invert([x, y])
|
|
198
153
|
|
|
199
|
-
|
|
200
|
-
|
|
154
|
+
const currentTooltipIndex = parseInt(tooltipRef.current.getAttribute('data-index'))
|
|
155
|
+
const geoRadius = (state.visual.geoCodeCircleSize || 5) * (focus.id ? 2 : 1)
|
|
201
156
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
157
|
+
// Handle standard county map hover
|
|
158
|
+
if (state.general.type !== 'us-geocode') {
|
|
159
|
+
//If no tooltip is shown, or if the current geo associated with the tooltip shown is no longer containing the mouse, then rerender the tooltip
|
|
160
|
+
if (isNaN(currentTooltipIndex) || !geoContains(mapData[currentTooltipIndex], pointCoordinates)) {
|
|
161
|
+
const context = canvas.getContext('2d')
|
|
162
|
+
const path = geoPath(projection, context)
|
|
163
|
+
if (!isNaN(currentTooltipIndex)) {
|
|
164
|
+
context.fillStyle = applyLegendToRow(data[mapData[currentTooltipIndex].id])[0]
|
|
165
|
+
context.strokeStyle = geoStrokeColor
|
|
166
|
+
context.lineWidth = lineWidth
|
|
167
|
+
context.beginPath()
|
|
168
|
+
path(mapData[currentTooltipIndex])
|
|
169
|
+
context.fill()
|
|
170
|
+
context.stroke()
|
|
171
|
+
}
|
|
206
172
|
|
|
207
|
-
|
|
173
|
+
let hoveredState
|
|
174
|
+
let county
|
|
175
|
+
let countyIndex
|
|
176
|
+
// First find the state that is hovered
|
|
177
|
+
for (let i = 0; i < states.length; i++) {
|
|
178
|
+
if (geoContains(states[i], pointCoordinates)) {
|
|
179
|
+
hoveredState = states[i].id
|
|
180
|
+
break
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// If a state is hovered and it is not an unfocused state, search only the counties within that state for the county hovered
|
|
184
|
+
if (hoveredState && (!focus.id || focus.id === hoveredState) && countyIndecies[hoveredState]) {
|
|
185
|
+
for (let i = countyIndecies[hoveredState][0]; i <= countyIndecies[hoveredState][1]; i++) {
|
|
186
|
+
if (geoContains(mapData[i], pointCoordinates)) {
|
|
187
|
+
county = mapData[i]
|
|
188
|
+
countyIndex = i
|
|
189
|
+
break
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
208
193
|
|
|
209
|
-
|
|
210
|
-
|
|
194
|
+
// If the hovered county is found, show the tooltip for that county, otherwise hide the tooltip
|
|
195
|
+
if (county && data[county.id]) {
|
|
196
|
+
context.fillStyle = applyLegendToRow(data[county.id])[1]
|
|
197
|
+
context.strokeStyle = geoStrokeColor
|
|
198
|
+
context.lineWidth = lineWidth
|
|
199
|
+
context.beginPath()
|
|
200
|
+
path(mapData[countyIndex])
|
|
201
|
+
context.fill()
|
|
202
|
+
context.stroke()
|
|
203
|
+
|
|
204
|
+
tooltipRef.current.style.display = 'block'
|
|
205
|
+
tooltipRef.current.style.top = e.clientY + 'px'
|
|
206
|
+
tooltipRef.current.style.left = e.clientX + 'px'
|
|
207
|
+
tooltipRef.current.innerHTML = applyTooltipsToGeo(displayGeoName(county.id), data[county.id])
|
|
208
|
+
tooltipRef.current.setAttribute('data-index', countyIndex)
|
|
209
|
+
} else {
|
|
210
|
+
tooltipRef.current.style.display = 'none'
|
|
211
|
+
tooltipRef.current.setAttribute('data-index', null)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
211
214
|
} else {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
svg.setAttribute('data-scaleZoom', 0)
|
|
221
|
-
mapGroup.current.setAttribute('transform', `translate(${[ 0, 0 ]}) scale(${0.85})`)
|
|
222
|
-
resetButton.current.style.display = 'none'
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function setStateLeave() {
|
|
227
|
-
focusedBorderPath.current.setAttribute('d', '')
|
|
228
|
-
focusedBorderPath.current.setAttribute('stroke', '')
|
|
229
|
-
focusedBorderPath.current.setAttribute('stroke-width', 0.75 / scale)
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function setStateEnter(id) {
|
|
233
|
-
const svg = document.querySelector('.svg-container')
|
|
234
|
-
const scale = svg.getAttribute('data-scaleZoom')
|
|
235
|
-
|
|
236
|
-
let myState = id.substring(0, 2)
|
|
237
|
-
const allStates = document.querySelectorAll('.state path')
|
|
238
|
-
|
|
239
|
-
let state = testJSON.objects.states.geometries.filter((el, index) => {
|
|
240
|
-
return el.id === myState
|
|
241
|
-
})
|
|
215
|
+
// Handle geo map hover
|
|
216
|
+
if (!isNaN(currentTooltipIndex)) {
|
|
217
|
+
const pixelCoords = projection([data[runtimeKeys[currentTooltipIndex]][state.columns.longitude.name], data[runtimeKeys[currentTooltipIndex]][state.columns.latitude.name]])
|
|
218
|
+
if (pixelCoords && Math.sqrt(Math.pow(pixelCoords[0] - x, 2) + Math.pow(pixelCoords[1] - y, 2)) < geoRadius) {
|
|
219
|
+
// Who knew pythagorean theorum was useful
|
|
220
|
+
return // The user is still hovering over the previous geo point, don't redraw tooltip
|
|
221
|
+
}
|
|
222
|
+
}
|
|
242
223
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
224
|
+
// todo: current item is a custom map layer
|
|
225
|
+
// if(currentItem === customMapLayer) show layer.tooltip
|
|
226
|
+
|
|
227
|
+
let hoveredGeo
|
|
228
|
+
let hoveredGeoIndex
|
|
229
|
+
for (let i = 0; i < runtimeKeys.length; i++) {
|
|
230
|
+
const pixelCoords = projection([data[runtimeKeys[i]][state.columns.longitude.name], data[runtimeKeys[i]][state.columns.latitude.name]])
|
|
231
|
+
if (pixelCoords && Math.sqrt(Math.pow(pixelCoords[0] - x, 2) + Math.pow(pixelCoords[1] - y, 2)) < geoRadius) {
|
|
232
|
+
hoveredGeo = data[runtimeKeys[i]]
|
|
233
|
+
hoveredGeoIndex = i
|
|
234
|
+
break
|
|
235
|
+
}
|
|
236
|
+
}
|
|
247
237
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
238
|
+
if (hoveredGeo) {
|
|
239
|
+
tooltipRef.current.style.display = 'block'
|
|
240
|
+
tooltipRef.current.style.top = e.clientY + 'px'
|
|
241
|
+
tooltipRef.current.style.left = e.clientX + 'px'
|
|
242
|
+
tooltipRef.current.innerHTML = applyTooltipsToGeo(displayGeoName(hoveredGeo[state.columns.geo.name]), hoveredGeo)
|
|
243
|
+
tooltipRef.current.setAttribute('data-index', hoveredGeoIndex)
|
|
244
|
+
} else {
|
|
245
|
+
tooltipRef.current.style.display = 'none'
|
|
246
|
+
tooltipRef.current.setAttribute('data-index', null)
|
|
247
|
+
}
|
|
251
248
|
}
|
|
252
249
|
}
|
|
253
250
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
})
|
|
261
|
-
|
|
262
|
-
const FocusedStateBorder = memo(() => {
|
|
263
|
-
return (
|
|
264
|
-
<g id="focusedBorder" key="focusedStateBorder">
|
|
265
|
-
<path ref={focusedBorderPath} d="" strokeWidth="" stroke={focusedBorderColor} fill="none" fillOpacity="1"/>
|
|
266
|
-
</g>
|
|
267
|
-
)
|
|
268
|
-
})
|
|
251
|
+
// Redraws canvas. Takes as parameters the fips id of a state to center on and the [lat,long] center of that state
|
|
252
|
+
const drawCanvas = () => {
|
|
253
|
+
if (canvasRef.current && runtimeLegend.length > 0) {
|
|
254
|
+
const canvas = canvasRef.current
|
|
255
|
+
const context = canvas.getContext('2d')
|
|
256
|
+
const path = geoPath(projection, context)
|
|
269
257
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
output.push(
|
|
273
|
-
counties.map(({ feature: geo, path = '' }) => {
|
|
274
|
-
const key = geo.id + '-group'
|
|
275
|
-
|
|
276
|
-
// COUNTY GROUPS
|
|
277
|
-
let styles = {
|
|
278
|
-
fillOpacity: '1',
|
|
279
|
-
fill: '#E6E6E6',
|
|
280
|
-
cursor: 'default'
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Map the name from the geo data with the appropriate key for the processed data
|
|
284
|
-
let geoKey = geo.id
|
|
258
|
+
canvas.width = canvas.clientWidth
|
|
259
|
+
canvas.height = canvas.width * 0.6
|
|
285
260
|
|
|
286
|
-
|
|
261
|
+
projection.scale(canvas.width * 1.25).translate([canvas.width / 2, canvas.height / 2])
|
|
287
262
|
|
|
288
|
-
|
|
263
|
+
// If we are rendering the map without a zoom on a state, hide the reset button
|
|
264
|
+
if (!focus.id) {
|
|
265
|
+
if (resetButton.current) resetButton.current.style.display = 'none'
|
|
266
|
+
} else {
|
|
267
|
+
if (resetButton.current) resetButton.current.style.display = 'block'
|
|
268
|
+
}
|
|
289
269
|
|
|
290
|
-
|
|
270
|
+
// Centers the projection on the paramter passed
|
|
271
|
+
if (focus.center) {
|
|
272
|
+
projection.scale(canvas.width * 2.5)
|
|
273
|
+
let offset = projection(focus.center)
|
|
274
|
+
projection.translate([-offset[0] + canvas.width, -offset[1] + canvas.height])
|
|
275
|
+
}
|
|
291
276
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
277
|
+
// Erases previous renderings before redrawing map
|
|
278
|
+
context.clearRect(0, 0, canvas.width, canvas.height)
|
|
279
|
+
|
|
280
|
+
// Enforces stroke style of the county lines
|
|
281
|
+
context.strokeStyle = geoStrokeColor
|
|
282
|
+
context.lineWidth = lineWidth
|
|
283
|
+
|
|
284
|
+
let focusIndex = -1
|
|
285
|
+
// Iterates through each state/county topo and renders it
|
|
286
|
+
mapData.forEach((geo, i) => {
|
|
287
|
+
// If invalid geo item, don't render
|
|
288
|
+
if (!geo.id) return
|
|
289
|
+
// If the map is focused on one state, don't render counties that are not in that state
|
|
290
|
+
if (focus.id && geo.id.length > 2 && geo.id.indexOf(focus.id) !== 0) return
|
|
291
|
+
// If rendering a geocode map without a focus, don't render counties
|
|
292
|
+
if (!focus.id && state.general.type === 'us-geocode' && geo.id.length > 2) return
|
|
293
|
+
|
|
294
|
+
// Gets numeric data associated with the topo data for this state/county
|
|
295
|
+
const geoData = data[geo.id]
|
|
296
|
+
|
|
297
|
+
// Marks that the focused state was found for the logic below
|
|
298
|
+
if (geo.id === focus.id) {
|
|
299
|
+
focusIndex = i
|
|
295
300
|
}
|
|
296
301
|
|
|
297
|
-
|
|
302
|
+
// Renders state/county
|
|
303
|
+
const legendValues = geoData !== undefined ? applyLegendToRow(geoData) : false
|
|
304
|
+
context.fillStyle = legendValues ? legendValues[0] : '#EEE'
|
|
305
|
+
context.beginPath()
|
|
306
|
+
path(geo)
|
|
307
|
+
context.fill()
|
|
308
|
+
context.stroke()
|
|
309
|
+
})
|
|
298
310
|
|
|
299
|
-
|
|
300
|
-
|
|
311
|
+
// If the focused state is found in the geo data, render it with a thicker outline
|
|
312
|
+
if (focusIndex !== -1) {
|
|
313
|
+
context.strokeStyle = 'black'
|
|
314
|
+
context.lineWidth = 2
|
|
315
|
+
context.beginPath()
|
|
316
|
+
path(mapData[focusIndex])
|
|
317
|
+
context.stroke()
|
|
318
|
+
}
|
|
301
319
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
320
|
+
// add in custom map layers
|
|
321
|
+
if (featureArray.length > 0) {
|
|
322
|
+
featureArray.map(layer => {
|
|
323
|
+
context.beginPath()
|
|
324
|
+
path(layer)
|
|
325
|
+
context.fillStyle = layer.properties.fill
|
|
326
|
+
context.globalAlpha = layer.properties['fill-opacity']
|
|
327
|
+
context.strokeStyle = layer.properties['stroke']
|
|
328
|
+
context.lineWidth = layer.properties['stroke-width']
|
|
329
|
+
context.fill()
|
|
330
|
+
context.stroke()
|
|
331
|
+
})
|
|
332
|
+
}
|
|
305
333
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
334
|
+
if (state.general.type === 'us-geocode') {
|
|
335
|
+
context.strokeStyle = 'black'
|
|
336
|
+
const geoRadius = (state.visual.geoCodeCircleSize || 5) * (focus.id ? 2 : 1)
|
|
337
|
+
|
|
338
|
+
runtimeKeys.forEach(key => {
|
|
339
|
+
const pixelCoords = projection([data[key][state.columns.longitude.name], data[key][state.columns.latitude.name]])
|
|
340
|
+
|
|
341
|
+
if (pixelCoords) {
|
|
342
|
+
const legendValues = data[key] !== undefined ? applyLegendToRow(data[key]) : false
|
|
343
|
+
if (legendValues) {
|
|
344
|
+
context.fillStyle = legendValues[0]
|
|
345
|
+
context.beginPath()
|
|
346
|
+
context.arc(pixelCoords[0], pixelCoords[1], geoRadius, 0, 2 * Math.PI)
|
|
347
|
+
context.fill()
|
|
348
|
+
context.stroke()
|
|
314
349
|
}
|
|
315
350
|
}
|
|
351
|
+
})
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
316
355
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
let stateFipsCode = geoData[state.columns.geo.name].substring(0, 2)
|
|
322
|
-
|
|
323
|
-
return (
|
|
324
|
-
<g
|
|
325
|
-
tabIndex="-1"
|
|
326
|
-
key={`county--${key}`}
|
|
327
|
-
className={`county county--${geoDisplayName.split(' ').join('')} county--${geoData[state.columns.geo.name]}`}
|
|
328
|
-
css={styles}
|
|
329
|
-
onMouseEnter={() => {
|
|
330
|
-
setStateEnter(geo.id)
|
|
331
|
-
}}
|
|
332
|
-
onMouseLeave={() => {
|
|
333
|
-
setStateLeave()
|
|
334
|
-
}}
|
|
335
|
-
onClick={
|
|
336
|
-
// default
|
|
337
|
-
e => {
|
|
338
|
-
e.stopPropagation()
|
|
339
|
-
e.nativeEvent.stopImmediatePropagation()
|
|
340
|
-
geoClickHandler(geoDisplayName, geoData)
|
|
341
|
-
focusGeo(stateFipsCode, geo)
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
data-tooltip-id="tooltip"
|
|
345
|
-
data-tooltip-html={toolTip}
|
|
346
|
-
>
|
|
347
|
-
<path tabIndex={-1} className={`county county--${geoDisplayName}`} stroke={geoStrokeColor} d={path} strokeWidth=".5"/>
|
|
348
|
-
</g>
|
|
349
|
-
)
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// default county
|
|
353
|
-
return (
|
|
354
|
-
<g
|
|
355
|
-
key={`county--default-${key}`}
|
|
356
|
-
className={`county county--${geoDisplayName}`}
|
|
357
|
-
css={styles}
|
|
358
|
-
strokeWidth=""
|
|
359
|
-
onMouseEnter={() => {
|
|
360
|
-
setStateEnter(geo.id)
|
|
361
|
-
}}
|
|
362
|
-
onMouseLeave={() => {
|
|
363
|
-
setStateLeave()
|
|
364
|
-
}}
|
|
365
|
-
onClick={
|
|
366
|
-
// default
|
|
367
|
-
e => {
|
|
368
|
-
e.stopPropagation()
|
|
369
|
-
e.nativeEvent.stopImmediatePropagation()
|
|
370
|
-
let countyFipsCode = geo.id
|
|
371
|
-
let stateFipsCode = countyFipsCode.substring(0, 2)
|
|
372
|
-
focusGeo(stateFipsCode, geo)
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
>
|
|
376
|
-
<path tabIndex={-1} className="single-geo" stroke={geoStrokeColor} d={path} strokeWidth=".85"/>
|
|
377
|
-
</g>
|
|
378
|
-
)
|
|
379
|
-
})
|
|
380
|
-
)
|
|
381
|
-
return output
|
|
382
|
-
})
|
|
383
|
-
|
|
384
|
-
const GeoCodeCountyLines = memo(() => {
|
|
385
|
-
return <path d={countyLines} className="county-borders" style={{ stroke: geoStrokeColor }}/>
|
|
386
|
-
})
|
|
387
|
-
|
|
388
|
-
const StateOutput = memo(({ geographies, states }) => {
|
|
389
|
-
let output = []
|
|
390
|
-
output.push(
|
|
391
|
-
states.map(({ feature: geo, path = '' }) => {
|
|
392
|
-
const key = geo.id + '-group'
|
|
393
|
-
|
|
394
|
-
// Map the name from the geo data with the appropriate key for the processed data
|
|
395
|
-
let geoKey = geo.id
|
|
396
|
-
|
|
397
|
-
if (!geoKey) return
|
|
398
|
-
|
|
399
|
-
const geoData = data[geoKey]
|
|
400
|
-
|
|
401
|
-
let legendColors
|
|
402
|
-
|
|
403
|
-
// Once we receive data for this geographic item, setup variables.
|
|
404
|
-
if (geoData !== undefined) {
|
|
405
|
-
legendColors = applyLegendToRow(geoData)
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
const geoDisplayName = displayGeoName(geoKey)
|
|
356
|
+
// Whenever the memo at the top is triggered and the map is called to re-render, call drawCanvas and update
|
|
357
|
+
// The resize function so it includes the latest state variables
|
|
358
|
+
useEffect(() => {
|
|
359
|
+
drawCanvas()
|
|
409
360
|
|
|
410
|
-
|
|
361
|
+
const onResize = () => {
|
|
362
|
+
if (canvasRef.current) {
|
|
363
|
+
drawCanvas()
|
|
364
|
+
}
|
|
365
|
+
}
|
|
411
366
|
|
|
412
|
-
|
|
413
|
-
stateStyles = {
|
|
414
|
-
cursor: 'default',
|
|
415
|
-
stroke: STATE_BORDER,
|
|
416
|
-
strokeWidth: 0.75 / scale,
|
|
417
|
-
display: !focusedState ? 'none' : focusedState && focusedState !== geo.id ? 'block' : 'none',
|
|
418
|
-
fill: focusedState && focusedState !== geo.id ? STATE_INACTIVE_FILL : 'none'
|
|
419
|
-
}
|
|
420
|
-
} else {
|
|
421
|
-
stateStyles = {
|
|
422
|
-
cursor: 'default',
|
|
423
|
-
stroke: STATE_BORDER,
|
|
424
|
-
strokeWidth: 0.75 / scale,
|
|
425
|
-
display: 'block',
|
|
426
|
-
fill: '#f4f7fa'
|
|
427
|
-
}
|
|
428
|
-
}
|
|
367
|
+
const debounceOnResize = debounce(onResize, 300)
|
|
429
368
|
|
|
430
|
-
|
|
431
|
-
fillOpacity: 1,
|
|
432
|
-
cursor: 'default'
|
|
433
|
-
}
|
|
369
|
+
window.addEventListener('resize', debounceOnResize)
|
|
434
370
|
|
|
435
|
-
|
|
436
|
-
focusedState === geo.id ? stateClasses.push('state--focused') : stateClasses.push('state--inactive')
|
|
437
|
-
|
|
438
|
-
return (
|
|
439
|
-
<React.Fragment key={`state--${key}`}>
|
|
440
|
-
<g key={`state--${key}`} className={stateClasses.join(' ')} style={stateStyles} tabIndex="-1">
|
|
441
|
-
<>
|
|
442
|
-
<path
|
|
443
|
-
tabIndex={-1}
|
|
444
|
-
className="state-path"
|
|
445
|
-
d={path}
|
|
446
|
-
fillOpacity={`${focusedState !== geo.id ? '1' : '0'}`}
|
|
447
|
-
fill={STATE_INACTIVE_FILL}
|
|
448
|
-
css={stateSelectedStyles}
|
|
449
|
-
onClick={e => {
|
|
450
|
-
e.stopPropagation()
|
|
451
|
-
e.nativeEvent.stopImmediatePropagation()
|
|
452
|
-
focusGeo(geo.id, geo)
|
|
453
|
-
}}
|
|
454
|
-
onMouseEnter={e => {
|
|
455
|
-
e.target.attributes.fill.value = colorPalettes[state.color][3]
|
|
456
|
-
}}
|
|
457
|
-
onMouseLeave={e => {
|
|
458
|
-
e.target.attributes.fill.value = STATE_INACTIVE_FILL
|
|
459
|
-
}}
|
|
460
|
-
/>
|
|
461
|
-
</>
|
|
462
|
-
</g>
|
|
463
|
-
<g key={`label--${key}`}>{offsets[geo.properties.name] && geoLabel(geo, geoAlbersUsaTerritories().translate([ WIDTH / 2, HEIGHT / 2 ]))}</g>
|
|
464
|
-
</React.Fragment>
|
|
465
|
-
)
|
|
466
|
-
})
|
|
467
|
-
)
|
|
468
|
-
return output
|
|
371
|
+
return () => window.removeEventListener('resize', debounceOnResize)
|
|
469
372
|
})
|
|
470
373
|
|
|
471
|
-
//
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
const counties = geographies.slice(56)
|
|
475
|
-
let geosJsx = []
|
|
476
|
-
|
|
477
|
-
'us-geocode' !== state.general.type && geosJsx.push(<CountyOutput geographies={geographies} counties={counties} key="county-key"/>)
|
|
478
|
-
'us-geocode' === state.general.type && geosJsx.push(<GeoCodeCountyLines/>)
|
|
479
|
-
|
|
480
|
-
geosJsx.push(<StateOutput geographies={geographies} states={states} key="state-key"/>)
|
|
481
|
-
geosJsx.push(<StateLines key="stateLines" lineWidth={startingLineWidth} geoStrokeColor={geoStrokeColor} stateLines={stateLines}/>)
|
|
482
|
-
geosJsx.push(<FocusedStateBorder key="focused-border-key"/>)
|
|
483
|
-
geosJsx.push(
|
|
484
|
-
<CityList
|
|
485
|
-
projection={projection}
|
|
486
|
-
key="cities"
|
|
487
|
-
data={data}
|
|
488
|
-
state={state}
|
|
489
|
-
geoClickHandler={geoClickHandler}
|
|
490
|
-
applyTooltipsToGeo={applyTooltipsToGeo}
|
|
491
|
-
displayGeoName={displayGeoName}
|
|
492
|
-
applyLegendToRow={applyLegendToRow}
|
|
493
|
-
titleCase={titleCase}
|
|
494
|
-
setSharedFilterValue={setSharedFilterValue}
|
|
495
|
-
isFilterValueSupported={isFilterValueSupported}
|
|
496
|
-
isGeoCodeMap={state.general.type === 'us-geocode'}
|
|
497
|
-
/>
|
|
498
|
-
)
|
|
499
|
-
return geosJsx
|
|
500
|
-
}
|
|
501
|
-
if (!data) <Loading/>
|
|
374
|
+
// If runtimeData is not defined, show loader
|
|
375
|
+
if (!data) <Loading />
|
|
376
|
+
|
|
502
377
|
return (
|
|
503
|
-
<ErrorBoundary component=
|
|
504
|
-
<
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
{({ features, projection }) => {
|
|
508
|
-
return (
|
|
509
|
-
<g ref={mapGroup} className="countyMapGroup" transform={`translate(${translate}) scale(${scale})`} key="countyMapGroup">
|
|
510
|
-
{constructGeoJsx(features, projection)}
|
|
511
|
-
</g>
|
|
512
|
-
)
|
|
513
|
-
}}
|
|
514
|
-
</CustomProjection>
|
|
515
|
-
</svg>
|
|
516
|
-
|
|
517
|
-
{/* TODO: Refactor to COVE button */}
|
|
518
|
-
<button className={`btn btn--reset`} onClick={onReset} ref={resetButton} style={{ display: 'none' }} tabIndex="0">
|
|
378
|
+
<ErrorBoundary component='CountyMap'>
|
|
379
|
+
<canvas ref={canvasRef} aria-label={handleMapAriaLabels(state)} onMouseMove={canvasHover} onClick={canvasClick}></canvas>
|
|
380
|
+
<div ref={tooltipRef} id='canvas-tooltip' className='tooltip'></div>
|
|
381
|
+
<button className={`btn btn--reset`} onClick={onReset} ref={resetButton} tabIndex='0'>
|
|
519
382
|
Reset Zoom
|
|
520
383
|
</button>
|
|
521
384
|
</ErrorBoundary>
|