@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.
Files changed (36) hide show
  1. package/dist/cdcmap.js +24661 -24191
  2. package/examples/custom-map-layers.json +764 -0
  3. package/examples/default-county.json +169 -155
  4. package/examples/default-geocode.json +744 -744
  5. package/examples/default-hex.json +3 -5
  6. package/examples/example-city-state-no-territories.json +703 -0
  7. package/examples/example-city-state.json +26 -7
  8. package/examples/example-city-stateBAD.json +744 -0
  9. package/examples/gallery/city-state.json +478 -478
  10. package/examples/testing-layer-2.json +1 -0
  11. package/examples/testing-layer.json +96 -0
  12. package/examples/world-geocode-data.json +18 -0
  13. package/examples/world-geocode.json +108 -0
  14. package/index.html +35 -29
  15. package/package.json +6 -3
  16. package/src/CdcMap.jsx +179 -111
  17. package/src/components/CityList.jsx +35 -35
  18. package/src/components/CountyMap.jsx +309 -446
  19. package/src/components/DataTable.jsx +7 -31
  20. package/src/components/EditorPanel.jsx +468 -217
  21. package/src/components/Sidebar.jsx +2 -0
  22. package/src/components/UsaMap.jsx +34 -23
  23. package/src/components/WorldMap.jsx +40 -8
  24. package/src/data/feature-test.json +73 -0
  25. package/src/data/initial-state.js +10 -3
  26. package/src/data/supported-geos.js +7 -7
  27. package/src/hooks/useMapLayers.jsx +243 -0
  28. package/src/index.jsx +4 -8
  29. package/src/scss/editor-panel.scss +97 -97
  30. package/src/scss/filters.scss +0 -2
  31. package/src/scss/main.scss +25 -26
  32. package/src/scss/map.scss +12 -0
  33. package/src/test/CdcMap.test.jsx +19 -0
  34. package/vite.config.js +3 -3
  35. package/src/components/Filters.jsx +0 -113
  36. package/src/hooks/useColorPalette.ts +0 -88
@@ -1,70 +1,72 @@
1
- import React, { useState, useEffect, memo, useRef } from 'react'
2
- import Loading from '@cdc/core/components/Loading'
1
+ import React, { useEffect, useState, memo, useRef } from 'react'
3
2
 
4
- import { jsx } from '@emotion/react'
5
- import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
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 testJSON from '../data/county-map.json'
12
- import { abbrs } from '../data/abbreviations'
13
- import CityList from './CityList'
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
- // SVG ITEMS
33
- const WIDTH = 880
34
- const HEIGHT = 500
35
- const PADDING = 25
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
- let { features: counties } = feature(testJSON, testJSON.objects.counties)
39
- let { features: states } = feature(testJSON, testJSON.objects.states)
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
- // CONSTANTS
42
- const STATE_BORDER = '#c0cad4'
43
- const STATE_INACTIVE_FILL = '#F4F7FA'
44
+ countyIndecies[state.id] = [minIndex, maxIndex]
45
+ })
44
46
 
45
47
  // CREATE STATE LINES
46
- const projection = geoAlbersUsaTerritories().translate([ WIDTH / 2, HEIGHT / 2 ])
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
- return equalMapColors && equalData && equalBorderColors && equalLegend && equalColumnName && equalNavColumn && equalNumberOptIn ? true : false
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
- let mapData = states.concat(counties)
63
+ const { state, runtimeLegend, applyTooltipsToGeo, data, geoClickHandler, applyLegendToRow, displayGeoName, containerEl, handleMapAriaLabels } = props
64
+
65
+ const [focus, setFocus] = useState({})
64
66
 
65
- const { state, applyTooltipsToGeo, data, geoClickHandler, applyLegendToRow, displayGeoName, containerEl, handleMapAriaLabels, titleCase, setSharedFilterValue, isFilterValueSupported } = props
67
+ const pathGenerator = geoPath().projection(geoAlbersUsaTerritories())
66
68
 
67
- console.log(data)
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 focusedBorderPath = useRef()
87
- const stateLinesPath = useRef()
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
- // 1) Get the state the county is in.
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
- // set the states border
159
- let allStates = document.querySelectorAll('.state path')
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
- const state = testJSON.objects.states.geometries.filter((el, index) => {
167
- return el.id === myState.id
168
- })
88
+ const onReset = () => {
89
+ setFocus({})
90
+ }
169
91
 
170
- const focusedStateLine = path(mesh(testJSON, state[0]))
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
- currentState.style.display = 'none'
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
- allStates.forEach(state => (state.style.strokeWidth = 0.75 / newScaleWithHypot))
175
- allCounties.forEach(county => (county.style.strokeWidth = 0.75 / newScaleWithHypot))
176
- otherStates.forEach(el => (el.style.display = 'block'))
124
+ // Redraw with focus on state
125
+ setFocus({ id: clickedState.id, center: geoCentroid(clickedState) })
126
+ }
177
127
 
178
- // State Line Updates
179
- stateLinesPath.current.setAttribute('stroke-width', 0.75 / newScaleWithHypot)
180
- stateLinesPath.current.setAttribute('stroke', geoStrokeColor)
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
- // Set Focus Border
183
- focusedBorderPath.current.style.display = 'block'
184
- focusedBorderPath.current.setAttribute('d', focusedStateLine)
185
- //focusedBorderPath.current.setAttribute('stroke-width', 0.75 / newScaleWithHypot);
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 onReset = e => {
190
- if (state.general.type !== 'us-geocode') {
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
- const allStates = document.querySelectorAll('.state path')
197
- const allCounties = document.querySelectorAll('.county path')
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
- stateLinesPath.current.setAttribute('stroke', geoStrokeColor)
200
- stateLinesPath.current.setAttribute('stroke-width', startingLineWidth)
154
+ const currentTooltipIndex = parseInt(tooltipRef.current.getAttribute('data-index'))
155
+ const geoRadius = (state.visual.geoCodeCircleSize || 5) * (focus.id ? 2 : 1)
201
156
 
202
- let otherStates = document.querySelectorAll(`.state--inactive`)
203
- otherStates.forEach(el => (el.style.display = 'none'))
204
- allCounties.forEach(el => (el.style.strokeWidth = 0.85))
205
- allStates.forEach(state => state.setAttribute('stroke-width', 0.75 / 0.85))
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
- mapGroup.current.setAttribute('transform', `translate(${[ 0, 0 ]}) scale(${0.85})`)
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
- // reset button
210
- resetButton.current.style.display = 'none'
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
- const svg = document.querySelector('.svg-container')
213
- const allStates = document.querySelectorAll('.state')
214
- document.querySelector('#focusedBorder path').style.stroke = 'none'
215
- allStates.forEach(item => item.classList.remove('state--inactive'))
216
- //document.querySelectorAll('.state path').forEach(item => item.style.fill = 'rgb(244, 247, 250)')
217
- document.querySelectorAll('.state').forEach(item => (item.style.display = 'block'))
218
- stateLinesPath.current.setAttribute('stroke', geoStrokeColor)
219
- stateLinesPath.current.setAttribute('stroke-width', startingLineWidth)
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
- let focusedStateLine = path(mesh(testJSON, state[0]))
244
- focusedBorderPath.current.style.display = 'block'
245
- focusedBorderPath.current.setAttribute('d', focusedStateLine)
246
- focusedBorderPath.current.setAttribute('stroke', '#000')
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
- if (scale) {
249
- allStates.forEach(state => state.setAttribute('stroke-width', 0.75 / scale))
250
- focusedBorderPath.current.setAttribute('stroke-width', 0.75 / scale)
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
- const StateLines = memo(({ stateLines, lineWidth, geoStrokeColor }) => {
255
- return (
256
- <g className="stateLines" key="state-line">
257
- <path id="stateLinesPath" ref={stateLinesPath} d={stateLines} strokeWidth={lineWidth} stroke={geoStrokeColor} fill="none" fillOpacity="1"/>
258
- </g>
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
- const CountyOutput = memo(({ geographies, counties }) => {
271
- let output = []
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
- if (!geoKey) return null
261
+ projection.scale(canvas.width * 1.25).translate([canvas.width / 2, canvas.height / 2])
287
262
 
288
- const geoData = data[geoKey]
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
- let legendColors
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
- // Once we receive data for this geographic item, setup variables.
293
- if (geoData !== undefined) {
294
- legendColors = applyLegendToRow(geoData)
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
- const geoDisplayName = displayGeoName(geoKey)
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
- // For some reason, these two geos are breaking the display.
300
- if (geoDisplayName === 'Franklin City' || geoDisplayName === 'Waynesboro') return null
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
- // If a legend applies, return it with appropriate information.
303
- if (legendColors && legendColors[0] !== '#000000') {
304
- const toolTip = applyTooltipsToGeo(geoDisplayName, geoData)
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
- styles = {
307
- fill: legendColors[0],
308
- cursor: 'default',
309
- '&:hover': {
310
- fill: legendColors[1]
311
- },
312
- '&:active': {
313
- fill: legendColors[2]
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
- // When to add pointer cursor
318
- if ((state.columns.navigate && geoData[state.columns.navigate.name]) || state.tooltips.appearanceType === 'hover') {
319
- styles.cursor = 'pointer'
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
- let stateStyles = {}
361
+ const onResize = () => {
362
+ if (canvasRef.current) {
363
+ drawCanvas()
364
+ }
365
+ }
411
366
 
412
- if (state.general.type !== 'us-geocode') {
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
- let stateSelectedStyles = {
431
- fillOpacity: 1,
432
- cursor: 'default'
433
- }
369
+ window.addEventListener('resize', debounceOnResize)
434
370
 
435
- let stateClasses = [ 'state', `state--${geo.properties.name}`, `state--${geo.id}` ]
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
- // Constructs and displays markup for all geos on the map (except territories right now)
472
- const constructGeoJsx = (geographies, projection) => {
473
- const states = geographies.slice(0, 56)
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="CountyMap">
504
- <svg viewBox={`0 0 ${WIDTH} ${HEIGHT}`} preserveAspectRatio="xMinYMin" className="svg-container" data-scale={scale ? scale : ''} data-translate={translate ? translate : ''} role="img" aria-label={handleMapAriaLabels(state)}>
505
- <rect className="background center-container ocean" width={WIDTH} height={HEIGHT} fillOpacity={1} fill="white" onClick={e => onReset(e)} tabIndex="0"></rect>
506
- <CustomProjection data={mapData} translate={[ WIDTH / 2, HEIGHT / 2 ]} projection={geoAlbersUsaTerritories}>
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>