@cdc/map 4.23.2 → 4.23.3

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.
@@ -1,70 +1,67 @@
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
+
14
+ const sortById = (a, b) => {
15
+ if (a.id < b.id) return -1
16
+ if (b.id < a.id) return 1
17
+ return 0
18
+ }
36
19
 
37
20
  // DATA
38
- let { features: counties } = feature(testJSON, testJSON.objects.counties)
39
- let { features: states } = feature(testJSON, testJSON.objects.states)
21
+ const { features: counties } = feature(topoJSON, topoJSON.objects.counties)
22
+ const { features: states } = feature(topoJSON, topoJSON.objects.states)
23
+
24
+ // Sort states/counties data to ensure that countyIndecies logic below works
25
+ states.sort(sortById)
26
+ counties.sort(sortById)
27
+ 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
28
+
29
+ // For each state, find the beginning index and end index of the counties in that state in the mapData array
30
+ // For use in hover logic (only check counties for hover in the state that is being hovered, instead of checking all counties)
31
+ const countyIndecies = {}
32
+ states.forEach(state => {
33
+ let minIndex = mapData.length - 1
34
+ let maxIndex = 0
35
+
36
+ for (let i = 0; i < mapData.length; i++) {
37
+ if (mapData[i].id.length > 2 && mapData[i].id.indexOf(state.id) === 0) {
38
+ if (i < minIndex) minIndex = i
39
+ if (i > maxIndex) maxIndex = i
40
+ }
41
+ }
40
42
 
41
- // CONSTANTS
42
- const STATE_BORDER = '#c0cad4'
43
- const STATE_INACTIVE_FILL = '#F4F7FA'
43
+ countyIndecies[state.id] = [minIndex, maxIndex]
44
+ })
44
45
 
45
46
  // 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))
47
+ const projection = geoAlbersUsaTerritories()
50
48
 
49
+ // Ensures county map is only rerendered when it needs to (when one of the variables below is updated)
51
50
  function CountyMapChecks(prevState, nextState) {
52
51
  const equalNumberOptIn = prevState.state.general.equalNumberOptIn && nextState.state.general.equalNumberOptIn
53
52
  const equalColumnName = prevState.state.general.type && nextState.state.general.type
54
53
  const equalNavColumn = prevState.state.columns.navigate && nextState.state.columns.navigate
55
54
  const equalLegend = prevState.runtimeLegend === nextState.runtimeLegend
56
55
  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
56
  const equalData = prevState.data === nextState.data // update when data changes
59
- return equalMapColors && equalData && equalBorderColors && equalLegend && equalColumnName && equalNavColumn && equalNumberOptIn ? true : false
57
+ const equalTooltipBehavior = prevState.state.tooltips.appearanceType === nextState.state.tooltips.appearanceType
58
+ return equalData && equalBorderColors && equalLegend && equalColumnName && equalNavColumn && equalNumberOptIn && equalTooltipBehavior ? true : false
60
59
  }
61
60
 
62
61
  const CountyMap = props => {
63
- let mapData = states.concat(counties)
64
-
65
- const { state, applyTooltipsToGeo, data, geoClickHandler, applyLegendToRow, displayGeoName, containerEl, handleMapAriaLabels, titleCase, setSharedFilterValue, isFilterValueSupported } = props
62
+ const { state, runtimeLegend, applyTooltipsToGeo, data, geoClickHandler, applyLegendToRow, displayGeoName, containerEl, handleMapAriaLabels } = props
66
63
 
67
- console.log(data)
64
+ const [focus, setFocus] = useState({})
68
65
 
69
66
  useEffect(() => {
70
67
  if (containerEl) {
@@ -74,448 +71,289 @@ const CountyMap = props => {
74
71
  }
75
72
  })
76
73
 
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
74
  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
- }
115
-
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'
75
+ const canvasRef = useRef()
76
+ const tooltipRef = useRef()
157
77
 
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)
78
+ const runtimeKeys = Object.keys(data)
165
79
 
166
- const state = testJSON.objects.states.geometries.filter((el, index) => {
167
- return el.id === myState.id
168
- })
80
+ const geoStrokeColor = state.general.geoBorderColor === 'darkGray' ? 'rgb(90, 90, 90)' : 'rgb(255, 255, 255)'
81
+ const lineWidth = 0.3
169
82
 
170
- const focusedStateLine = path(mesh(testJSON, state[0]))
171
-
172
- currentState.style.display = 'none'
173
-
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'))
177
-
178
- // State Line Updates
179
- stateLinesPath.current.setAttribute('stroke-width', 0.75 / newScaleWithHypot)
180
- stateLinesPath.current.setAttribute('stroke', geoStrokeColor)
181
-
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)
83
+ const onReset = () => {
84
+ setFocus({})
187
85
  }
188
86
 
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)
195
-
196
- const allStates = document.querySelectorAll('.state path')
197
- const allCounties = document.querySelectorAll('.county path')
198
-
199
- stateLinesPath.current.setAttribute('stroke', geoStrokeColor)
200
- stateLinesPath.current.setAttribute('stroke-width', startingLineWidth)
201
-
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))
206
-
207
- mapGroup.current.setAttribute('transform', `translate(${[ 0, 0 ]}) scale(${0.85})`)
208
-
209
- // reset button
210
- resetButton.current.style.display = 'none'
211
- } 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'
87
+ const canvasClick = e => {
88
+ const canvas = e.target
89
+ const canvasBounds = canvas.getBoundingClientRect()
90
+ const x = e.clientX - canvasBounds.left
91
+ const y = e.clientY - canvasBounds.top
92
+ const pointCoordinates = projection.invert([x, y])
93
+
94
+ // Use d3 geoContains method to find the state geo data that the user clicked inside
95
+ let clickedState
96
+ for (let i = 0; i < states.length; i++) {
97
+ if (geoContains(states[i], pointCoordinates)) {
98
+ clickedState = states[i]
99
+ break
100
+ }
223
101
  }
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
102
 
236
- let myState = id.substring(0, 2)
237
- const allStates = document.querySelectorAll('.state path')
103
+ // If the user clicked outside of all states, no behavior
104
+ if (clickedState) {
105
+ // If a county within the state was also clicked and has data, call parent click handler
106
+ if (countyIndecies[clickedState.id]) {
107
+ let county
108
+ for (let i = countyIndecies[clickedState.id][0]; i <= countyIndecies[clickedState.id][1]; i++) {
109
+ if (geoContains(mapData[i], pointCoordinates)) {
110
+ county = mapData[i]
111
+ break
112
+ }
113
+ }
114
+ if (county && data[county.id]) {
115
+ geoClickHandler(displayGeoName(county.id), data[county.id])
116
+ }
117
+ }
238
118
 
239
- let state = testJSON.objects.states.geometries.filter((el, index) => {
240
- return el.id === myState
241
- })
119
+ // Redraw with focus on state
120
+ setFocus({ id: clickedState.id, center: geoCentroid(clickedState) })
121
+ }
242
122
 
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')
123
+ if (state.general.type === 'us-geocode') {
124
+ const geoRadius = (state.visual.geoCodeCircleSize || 5) * (focus.id ? 2 : 1)
125
+ let clickedGeo
126
+ for (let i = 0; i < runtimeKeys.length; i++) {
127
+ const pixelCoords = projection([data[runtimeKeys[i]][state.columns.longitude.name], data[runtimeKeys[i]][state.columns.latitude.name]])
128
+ if (pixelCoords && Math.sqrt(Math.pow(pixelCoords[0] - x, 2) + Math.pow(pixelCoords[1] - y, 2)) < geoRadius) {
129
+ clickedGeo = data[runtimeKeys[i]]
130
+ break
131
+ }
132
+ }
247
133
 
248
- if (scale) {
249
- allStates.forEach(state => state.setAttribute('stroke-width', 0.75 / scale))
250
- focusedBorderPath.current.setAttribute('stroke-width', 0.75 / scale)
134
+ if (clickedGeo) {
135
+ geoClickHandler(displayGeoName(clickedGeo[state.columns.geo.name]), clickedGeo)
136
+ }
251
137
  }
252
138
  }
253
139
 
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
- })
140
+ const canvasHover = e => {
141
+ if (!tooltipRef.current || state.tooltips.appearanceType !== 'hover' || window.matchMedia('(any-hover: none)').matches) return
261
142
 
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
- })
143
+ const canvas = e.target
144
+ const canvasBounds = canvas.getBoundingClientRect()
145
+ const x = e.clientX - canvasBounds.left
146
+ const y = e.clientY - canvasBounds.top
147
+ let pointCoordinates = projection.invert([x, y])
148
+
149
+ const currentTooltipIndex = parseInt(tooltipRef.current.getAttribute('data-index'))
150
+ const geoRadius = (state.visual.geoCodeCircleSize || 5) * (focus.id ? 2 : 1)
269
151
 
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'
152
+ // Handle standard county map hover
153
+ if (state.general.type !== 'us-geocode') {
154
+ //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
155
+ if (isNaN(currentTooltipIndex) || !geoContains(mapData[currentTooltipIndex], pointCoordinates)) {
156
+ const context = canvas.getContext('2d')
157
+ const path = geoPath(projection, context)
158
+ if (!isNaN(currentTooltipIndex)) {
159
+ context.fillStyle = applyLegendToRow(data[mapData[currentTooltipIndex].id])[0]
160
+ context.strokeStyle = geoStrokeColor
161
+ context.lineWidth = lineWidth
162
+ context.beginPath()
163
+ path(mapData[currentTooltipIndex])
164
+ context.fill()
165
+ context.stroke()
281
166
  }
282
167
 
283
- // Map the name from the geo data with the appropriate key for the processed data
284
- let geoKey = geo.id
168
+ let hoveredState
169
+ let county
170
+ let countyIndex
171
+ // First find the state that is hovered
172
+ for (let i = 0; i < states.length; i++) {
173
+ if (geoContains(states[i], pointCoordinates)) {
174
+ hoveredState = states[i].id
175
+ break
176
+ }
177
+ }
178
+ // If a state is hovered and it is not an unfocused state, search only the counties within that state for the county hovered
179
+ if (hoveredState && (!focus.id || focus.id === hoveredState) && countyIndecies[hoveredState]) {
180
+ for (let i = countyIndecies[hoveredState][0]; i <= countyIndecies[hoveredState][1]; i++) {
181
+ if (geoContains(mapData[i], pointCoordinates)) {
182
+ county = mapData[i]
183
+ countyIndex = i
184
+ break
185
+ }
186
+ }
187
+ }
285
188
 
286
- if (!geoKey) return null
189
+ // If the hovered county is found, show the tooltip for that county, otherwise hide the tooltip
190
+ if (county && data[county.id]) {
191
+ context.fillStyle = applyLegendToRow(data[county.id])[1]
192
+ context.strokeStyle = geoStrokeColor
193
+ context.lineWidth = lineWidth
194
+ context.beginPath()
195
+ path(mapData[countyIndex])
196
+ context.fill()
197
+ context.stroke()
198
+
199
+ tooltipRef.current.style.display = 'block'
200
+ tooltipRef.current.style.top = e.clientY + 'px'
201
+ tooltipRef.current.style.left = e.clientX + 'px'
202
+ tooltipRef.current.innerHTML = applyTooltipsToGeo(displayGeoName(county.id), data[county.id])
203
+ tooltipRef.current.setAttribute('data-index', countyIndex)
204
+ } else {
205
+ tooltipRef.current.style.display = 'none'
206
+ tooltipRef.current.setAttribute('data-index', null)
207
+ }
208
+ }
209
+ } else {
210
+ // Handle geo map hover
211
+ if (!isNaN(currentTooltipIndex)) {
212
+ const pixelCoords = projection([data[runtimeKeys[currentTooltipIndex]][state.columns.longitude.name], data[runtimeKeys[currentTooltipIndex]][state.columns.latitude.name]])
213
+ if (pixelCoords && Math.sqrt(Math.pow(pixelCoords[0] - x, 2) + Math.pow(pixelCoords[1] - y, 2)) < geoRadius) {
214
+ // Who knew pythagorean theorum was useful
215
+ return // The user is still hovering over the previous geo point, don't redraw tooltip
216
+ }
217
+ }
287
218
 
288
- const geoData = data[geoKey]
219
+ let hoveredGeo
220
+ let hoveredGeoIndex
221
+ for (let i = 0; i < runtimeKeys.length; i++) {
222
+ const pixelCoords = projection([data[runtimeKeys[i]][state.columns.longitude.name], data[runtimeKeys[i]][state.columns.latitude.name]])
223
+ if (pixelCoords && Math.sqrt(Math.pow(pixelCoords[0] - x, 2) + Math.pow(pixelCoords[1] - y, 2)) < geoRadius) {
224
+ hoveredGeo = data[runtimeKeys[i]]
225
+ hoveredGeoIndex = i
226
+ break
227
+ }
228
+ }
289
229
 
290
- let legendColors
230
+ if (hoveredGeo) {
231
+ tooltipRef.current.style.display = 'block'
232
+ tooltipRef.current.style.top = e.clientY + 'px'
233
+ tooltipRef.current.style.left = e.clientX + 'px'
234
+ tooltipRef.current.innerHTML = applyTooltipsToGeo(displayGeoName(hoveredGeo[state.columns.geo.name]), hoveredGeo)
235
+ tooltipRef.current.setAttribute('data-index', hoveredGeoIndex)
236
+ } else {
237
+ tooltipRef.current.style.display = 'none'
238
+ tooltipRef.current.setAttribute('data-index', null)
239
+ }
240
+ }
241
+ }
291
242
 
292
- // Once we receive data for this geographic item, setup variables.
293
- if (geoData !== undefined) {
294
- legendColors = applyLegendToRow(geoData)
295
- }
243
+ // Redraws canvas. Takes as parameters the fips id of a state to center on and the [lat,long] center of that state
244
+ const drawCanvas = () => {
245
+ if (canvasRef.current && runtimeLegend.length > 0) {
246
+ const canvas = canvasRef.current
247
+ const context = canvas.getContext('2d')
248
+ const path = geoPath(projection, context)
296
249
 
297
- const geoDisplayName = displayGeoName(geoKey)
250
+ canvas.width = canvas.clientWidth
251
+ canvas.height = canvas.width * 0.6
298
252
 
299
- // For some reason, these two geos are breaking the display.
300
- if (geoDisplayName === 'Franklin City' || geoDisplayName === 'Waynesboro') return null
253
+ projection.scale(canvas.width * 1.25).translate([canvas.width / 2, canvas.height / 2])
301
254
 
302
- // If a legend applies, return it with appropriate information.
303
- if (legendColors && legendColors[0] !== '#000000') {
304
- const toolTip = applyTooltipsToGeo(geoDisplayName, geoData)
255
+ // If we are rendering the map without a zoom on a state, hide the reset button
256
+ if (!focus.id) {
257
+ if (resetButton.current) resetButton.current.style.display = 'none'
258
+ } else {
259
+ if (resetButton.current) resetButton.current.style.display = 'block'
260
+ }
305
261
 
306
- styles = {
307
- fill: legendColors[0],
308
- cursor: 'default',
309
- '&:hover': {
310
- fill: legendColors[1]
311
- },
312
- '&:active': {
313
- fill: legendColors[2]
314
- }
315
- }
262
+ // Centers the projection on the paramter passed
263
+ if (focus.center) {
264
+ projection.scale(canvas.width * 2.5)
265
+ let offset = projection(focus.center)
266
+ projection.translate([-offset[0] + canvas.width, -offset[1] + canvas.height])
267
+ }
316
268
 
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
- )
269
+ // Erases previous renderings before redrawing map
270
+ context.clearRect(0, 0, canvas.width, canvas.height)
271
+
272
+ // Enforces stroke style of the county lines
273
+ context.strokeStyle = geoStrokeColor
274
+ context.lineWidth = lineWidth
275
+
276
+ let focusIndex = -1
277
+ // Iterates through each state/county topo and renders it
278
+ mapData.forEach((geo, i) => {
279
+ // If invalid geo item, don't render
280
+ if (!geo.id) return
281
+ // If the map is focused on one state, don't render counties that are not in that state
282
+ if (focus.id && geo.id.length > 2 && geo.id.indexOf(focus.id) !== 0) return
283
+ // If rendering a geocode map without a focus, don't render counties
284
+ if (!focus.id && state.general.type === 'us-geocode' && geo.id.length > 2) return
285
+
286
+ // Gets numeric data associated with the topo data for this state/county
287
+ const geoData = data[geo.id]
288
+
289
+ // Marks that the focused state was found for the logic below
290
+ if (geo.id === focus.id) {
291
+ focusIndex = i
350
292
  }
351
293
 
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
- )
294
+ // Renders state/county
295
+ const legendValues = geoData !== undefined ? applyLegendToRow(geoData) : false
296
+ context.fillStyle = legendValues ? legendValues[0] : '#EEE'
297
+ context.beginPath()
298
+ path(geo)
299
+ context.fill()
300
+ context.stroke()
379
301
  })
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
302
 
397
- if (!geoKey) return
303
+ // If the focused state is found in the geo data, render it with a thicker outline
304
+ if (focusIndex !== -1) {
305
+ context.strokeStyle = 'black'
306
+ context.lineWidth = 2
307
+ context.beginPath()
308
+ path(mapData[focusIndex])
309
+ context.stroke()
310
+ }
398
311
 
399
- const geoData = data[geoKey]
312
+ if (state.general.type === 'us-geocode') {
313
+ context.strokeStyle = 'black'
314
+ const geoRadius = (state.visual.geoCodeCircleSize || 5) * (focus.id ? 2 : 1)
400
315
 
401
- let legendColors
316
+ runtimeKeys.forEach(key => {
317
+ const pixelCoords = projection([data[key][state.columns.longitude.name], data[key][state.columns.latitude.name]])
402
318
 
403
- // Once we receive data for this geographic item, setup variables.
404
- if (geoData !== undefined) {
405
- legendColors = applyLegendToRow(geoData)
406
- }
319
+ if (pixelCoords) {
320
+ context.fillStyle = data[key] !== undefined ? applyLegendToRow(data[key])[0] : '#EEE'
321
+ context.beginPath()
322
+ context.arc(pixelCoords[0], pixelCoords[1], geoRadius, 0, 2 * Math.PI)
323
+ context.fill()
324
+ context.stroke()
325
+ }
326
+ })
327
+ }
328
+ }
329
+ }
407
330
 
408
- const geoDisplayName = displayGeoName(geoKey)
331
+ // Whenever the memo at the top is triggered and the map is called to re-render, call drawCanvas and update
332
+ // The resize function so it includes the latest state variables
333
+ useEffect(() => {
334
+ drawCanvas()
409
335
 
410
- let stateStyles = {}
336
+ const onResize = () => {
337
+ if (canvasRef.current) {
338
+ drawCanvas()
339
+ }
340
+ }
411
341
 
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
- }
342
+ const debounceOnResize = debounce(onResize, 300)
429
343
 
430
- let stateSelectedStyles = {
431
- fillOpacity: 1,
432
- cursor: 'default'
433
- }
344
+ window.addEventListener('resize', debounceOnResize)
434
345
 
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
346
+ return () => window.removeEventListener('resize', debounceOnResize)
469
347
  })
470
348
 
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/>
349
+ // If runtimeData is not defined, show loader
350
+ if (!data) <Loading />
351
+
502
352
  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">
353
+ <ErrorBoundary component='CountyMap'>
354
+ <canvas ref={canvasRef} aria-label={handleMapAriaLabels(state)} onMouseMove={canvasHover} onClick={canvasClick}></canvas>
355
+ <div ref={tooltipRef} id='canvas-tooltip' className='tooltip'></div>
356
+ <button className={`btn btn--reset`} onClick={onReset} ref={resetButton} tabIndex='0'>
519
357
  Reset Zoom
520
358
  </button>
521
359
  </ErrorBoundary>