@cdc/map 4.24.7 → 4.24.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cdcmap.js +40720 -38422
- package/examples/county-year.csv +10 -0
- package/examples/default-geocode.json +44 -10
- package/examples/default-patterns.json +0 -2
- package/examples/default-single-state.json +279 -108
- package/examples/map-issue-3.json +646 -0
- package/examples/single-state-filter.json +153 -0
- package/index.html +9 -6
- package/package.json +3 -3
- package/src/CdcMap.tsx +322 -126
- package/src/_stories/CdcMap.stories.tsx +7 -0
- package/src/_stories/_mock/DEV-8942.json +270 -0
- package/src/components/Annotation/AnnotationDropdown.tsx +1 -0
- package/src/components/{BubbleList.jsx → BubbleList.tsx} +1 -1
- package/src/components/{CityList.jsx → CityList.tsx} +28 -2
- package/src/components/{DataTable.jsx → DataTable.tsx} +2 -2
- package/src/components/EditorPanel/components/EditorPanel.tsx +647 -127
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +0 -22
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +61 -11
- package/src/components/Legend/components/Legend.tsx +125 -36
- package/src/components/Legend/components/index.scss +42 -42
- package/src/components/Modal.tsx +25 -0
- package/src/components/UsaMap/components/SingleState/SingleState.CountyOutput.tsx +74 -0
- package/src/components/UsaMap/components/SingleState/SingleState.StateOutput.tsx +29 -0
- package/src/components/UsaMap/components/SingleState/index.tsx +9 -0
- package/src/components/UsaMap/components/UsaMap.County.tsx +84 -33
- package/src/components/UsaMap/components/UsaMap.SingleState.tsx +173 -206
- package/src/components/UsaMap/components/UsaMap.State.tsx +161 -26
- package/src/components/UsaMap/data/us-extended-geography.json +1 -0
- package/src/components/UsaMap/helpers/map.ts +111 -0
- package/src/components/WorldMap/WorldMap.tsx +17 -32
- package/src/components/ZoomControls.tsx +41 -0
- package/src/data/initial-state.js +7 -1
- package/src/data/supported-geos.js +15 -4
- package/src/helpers/generateRuntimeLegendHash.ts +2 -2
- package/src/hooks/useStateZoom.tsx +157 -0
- package/src/hooks/{useZoomPan.js → useZoomPan.ts} +6 -5
- package/src/scss/editor-panel.scss +0 -4
- package/src/scss/main.scss +23 -1
- package/src/scss/map.scss +8 -0
- package/src/types/MapConfig.ts +9 -1
- package/src/types/MapContext.ts +14 -2
- package/src/components/Modal.jsx +0 -22
- /package/src/components/{Geo.jsx → Geo.tsx} +0 -0
- /package/src/components/{NavigationMenu.jsx → NavigationMenu.tsx} +0 -0
- /package/src/components/{ZoomableGroup.jsx → ZoomableGroup.tsx} +0 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import React, { useContext } from 'react'
|
|
2
|
+
import ConfigContext from '../../../../context'
|
|
3
|
+
import { MapContext } from '../../../../types/MapContext'
|
|
4
|
+
|
|
5
|
+
interface CountyOutputProps {
|
|
6
|
+
counties: any[]
|
|
7
|
+
scale: number
|
|
8
|
+
geoStrokeColor: string
|
|
9
|
+
tooltipId: string
|
|
10
|
+
path: any
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const CountyOutput: React.FC<CountyOutputProps> = ({ path, counties, scale, geoStrokeColor, tooltipId }) => {
|
|
14
|
+
const { applyTooltipsToGeo, applyLegendToRow, displayGeoName, state, data, geoClickHandler } = useContext<MapContext>(ConfigContext)
|
|
15
|
+
return (
|
|
16
|
+
<>
|
|
17
|
+
{counties.map(county => {
|
|
18
|
+
// Map the name from the geo data with the appropriate key for the processed data
|
|
19
|
+
const geoKey = county.id
|
|
20
|
+
|
|
21
|
+
if (!geoKey) return null
|
|
22
|
+
|
|
23
|
+
const countyPath = path(county)
|
|
24
|
+
|
|
25
|
+
const geoData = data[county.id]
|
|
26
|
+
let legendColors
|
|
27
|
+
|
|
28
|
+
// Once we receive data for this geographic item, setup variables.
|
|
29
|
+
if (geoData !== undefined) {
|
|
30
|
+
legendColors = applyLegendToRow(geoData)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const geoDisplayName = displayGeoName(geoKey)
|
|
34
|
+
|
|
35
|
+
// For some reason, these two geos are breaking the display.
|
|
36
|
+
if (geoDisplayName === 'Franklin City' || geoDisplayName === 'Waynesboro') return null
|
|
37
|
+
|
|
38
|
+
const toolTip = applyTooltipsToGeo(geoDisplayName, geoData)
|
|
39
|
+
|
|
40
|
+
if (legendColors && legendColors[0] !== '#000000') {
|
|
41
|
+
const styles = {
|
|
42
|
+
fill: legendColors[0],
|
|
43
|
+
cursor: 'default',
|
|
44
|
+
'&:hover': {
|
|
45
|
+
fill: legendColors[1]
|
|
46
|
+
},
|
|
47
|
+
'&:active': {
|
|
48
|
+
fill: legendColors[2]
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// When to add pointer cursor
|
|
53
|
+
if ((state.columns.navigate && geoData[state.columns.navigate.name]) || state.tooltips.appearanceType === 'hover') {
|
|
54
|
+
styles.cursor = 'pointer'
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<g key={`key--${county.id}`} className={`county county--${geoDisplayName.split(' ').join('')} county--${geoData[state.columns.geo.name]}`} style={styles} onClick={() => geoClickHandler(geoDisplayName, geoData)} data-tooltip-id={`tooltip__${tooltipId}`} data-tooltip-html={toolTip}>
|
|
59
|
+
<path tabIndex={-1} className={`county`} stroke={geoStrokeColor} d={countyPath} strokeWidth={0.75 / scale} />
|
|
60
|
+
</g>
|
|
61
|
+
)
|
|
62
|
+
} else {
|
|
63
|
+
return (
|
|
64
|
+
<g key={`key--${county.id}`} className={`county county--${geoDisplayName.split(' ').join('')}`} style={{ fill: '#e6e6e6' }} data-tooltip-id={`tooltip__${tooltipId}`} data-tooltip-html={toolTip}>
|
|
65
|
+
<path tabIndex={-1} className={`county`} stroke={geoStrokeColor} d={countyPath} strokeWidth={0.75 / scale} />
|
|
66
|
+
</g>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
})}
|
|
70
|
+
</>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export default CountyOutput
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { useContext } from 'react'
|
|
2
|
+
import { mesh, Topology } from 'topojson-client'
|
|
3
|
+
import ConfigContext from '../../../../context'
|
|
4
|
+
|
|
5
|
+
type StateOutputProps = {
|
|
6
|
+
topoData: Topology
|
|
7
|
+
path: any
|
|
8
|
+
scale: any
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const StateOutput: React.FC<StateOutputProps> = ({ topoData, path, scale, stateToShow }: StateOutputProps) => {
|
|
12
|
+
const { state } = useContext(ConfigContext)
|
|
13
|
+
if (!topoData?.objects?.states) return null
|
|
14
|
+
let geo = topoData.objects.states.geometries.filter(s => {
|
|
15
|
+
return s.properties.name === state.general.statePicked.stateName
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const geoStrokeColor = state.general.geoBorderColor === 'darkGray' ? 'rgba(0, 0, 0, 0.2)' : 'rgba(255,255,255,0.7)'
|
|
19
|
+
|
|
20
|
+
let stateLines = path(mesh(topoData, geo[0]))
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<g key={'single-state'} className='single-state' style={{ fill: '#E6E6E6' }} stroke={geoStrokeColor} strokeWidth={0.95 / scale}>
|
|
24
|
+
<path tabIndex={-1} className='state-path' d={stateLines} />
|
|
25
|
+
</g>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default StateOutput
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useEffect, useState, useRef, useContext } from 'react'
|
|
2
|
+
import * as d3 from 'd3-geo'
|
|
2
3
|
|
|
3
4
|
import { geoCentroid, geoPath, geoContains } from 'd3-geo'
|
|
4
5
|
import { feature } from 'topojson-client'
|
|
@@ -131,7 +132,8 @@ const CountyMap = props => {
|
|
|
131
132
|
runtimeFilters,
|
|
132
133
|
tooltipId,
|
|
133
134
|
tooltipRef,
|
|
134
|
-
container
|
|
135
|
+
container,
|
|
136
|
+
setState
|
|
135
137
|
} = useContext(ConfigContext)
|
|
136
138
|
|
|
137
139
|
// CREATE STATE LINES
|
|
@@ -204,6 +206,10 @@ const CountyMap = props => {
|
|
|
204
206
|
const lineWidth = 0.3
|
|
205
207
|
|
|
206
208
|
const onReset = () => {
|
|
209
|
+
setState({
|
|
210
|
+
...state,
|
|
211
|
+
mapPosition: { coordinates: [0, 30], zoom: 1 }
|
|
212
|
+
})
|
|
207
213
|
setFocus({})
|
|
208
214
|
}
|
|
209
215
|
|
|
@@ -225,10 +231,19 @@ const CountyMap = props => {
|
|
|
225
231
|
|
|
226
232
|
// If the user clicked outside of all states, no behavior
|
|
227
233
|
if (clickedState) {
|
|
234
|
+
setState({
|
|
235
|
+
...state,
|
|
236
|
+
mapPosition: { coordinates: [0, 30], zoom: 3 }
|
|
237
|
+
})
|
|
238
|
+
|
|
228
239
|
// If a county within the state was also clicked and has data, call parent click handler
|
|
229
240
|
if (topoData.countyIndecies[clickedState.id]) {
|
|
230
241
|
let county
|
|
231
|
-
for (
|
|
242
|
+
for (
|
|
243
|
+
let i = topoData.countyIndecies[clickedState.id][0];
|
|
244
|
+
i <= topoData.countyIndecies[clickedState.id][1];
|
|
245
|
+
i++
|
|
246
|
+
) {
|
|
232
247
|
if (geoContains(topoData.mapData[i], pointCoordinates)) {
|
|
233
248
|
county = topoData.mapData[i]
|
|
234
249
|
break
|
|
@@ -239,23 +254,26 @@ const CountyMap = props => {
|
|
|
239
254
|
}
|
|
240
255
|
}
|
|
241
256
|
|
|
242
|
-
let focusIndex = -1
|
|
243
|
-
for(let i = 0; i < topoData.mapData.length; i++){
|
|
244
|
-
if(topoData.mapData[i].id === clickedState.id){
|
|
245
|
-
focusIndex = i
|
|
246
|
-
break
|
|
257
|
+
let focusIndex = -1
|
|
258
|
+
for (let i = 0; i < topoData.mapData.length; i++) {
|
|
259
|
+
if (topoData.mapData[i].id === clickedState.id) {
|
|
260
|
+
focusIndex = i
|
|
261
|
+
break
|
|
247
262
|
}
|
|
248
263
|
}
|
|
249
264
|
|
|
250
265
|
// Redraw with focus on state
|
|
251
|
-
setFocus({ id: clickedState.id, index: focusIndex, center: geoCentroid(clickedState) })
|
|
266
|
+
setFocus({ id: clickedState.id, index: focusIndex, center: geoCentroid(clickedState), feature: clickedState })
|
|
252
267
|
}
|
|
253
268
|
|
|
254
269
|
if (state.general.type === 'us-geocode') {
|
|
255
270
|
const geoRadius = (state.visual.geoCodeCircleSize || 5) * (focus.id ? 2 : 1)
|
|
256
271
|
let clickedGeo
|
|
257
272
|
for (let i = 0; i < runtimeKeys.length; i++) {
|
|
258
|
-
const pixelCoords = topoData.projection([
|
|
273
|
+
const pixelCoords = topoData.projection([
|
|
274
|
+
data[runtimeKeys[i]][state.columns.longitude.name],
|
|
275
|
+
data[runtimeKeys[i]][state.columns.latitude.name]
|
|
276
|
+
])
|
|
259
277
|
if (pixelCoords && Math.sqrt(Math.pow(pixelCoords[0] - x, 2) + Math.pow(pixelCoords[1] - y, 2)) < geoRadius) {
|
|
260
278
|
clickedGeo = data[runtimeKeys[i]]
|
|
261
279
|
break
|
|
@@ -269,7 +287,12 @@ const CountyMap = props => {
|
|
|
269
287
|
}
|
|
270
288
|
|
|
271
289
|
const canvasHover = e => {
|
|
272
|
-
if (
|
|
290
|
+
if (
|
|
291
|
+
!tooltipRef.current ||
|
|
292
|
+
state.tooltips.appearanceType !== 'hover' ||
|
|
293
|
+
window.matchMedia('(any-hover: none)').matches
|
|
294
|
+
)
|
|
295
|
+
return
|
|
273
296
|
|
|
274
297
|
const canvas = e.target
|
|
275
298
|
const canvasBounds = canvas.getBoundingClientRect()
|
|
@@ -281,7 +304,7 @@ const CountyMap = props => {
|
|
|
281
304
|
let pointCoordinates = topoData.projection.invert([x, y])
|
|
282
305
|
|
|
283
306
|
const currentTooltipIndex = parseInt(tooltipRef.current.getAttribute('data-index'))
|
|
284
|
-
const geoRadius = (state.visual.geoCodeCircleSize || 5) *
|
|
307
|
+
const geoRadius = (state.visual.geoCodeCircleSize || 5) * 1
|
|
285
308
|
|
|
286
309
|
const context = canvas.getContext('2d')
|
|
287
310
|
const path = geoPath(topoData.projection, context)
|
|
@@ -337,13 +360,13 @@ const CountyMap = props => {
|
|
|
337
360
|
|
|
338
361
|
tooltipRef.current.style.display = 'block'
|
|
339
362
|
tooltipRef.current.style.top = tooltipY + 'px'
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
363
|
+
if (tooltipX > containerBounds.width / 2) {
|
|
364
|
+
tooltipRef.current.style.transform = 'translate(-100%, -50%)'
|
|
365
|
+
tooltipRef.current.style.left = tooltipX - 5 + 'px'
|
|
366
|
+
} else {
|
|
367
|
+
tooltipRef.current.style.transform = 'translate(0, -50%)'
|
|
368
|
+
tooltipRef.current.style.left = tooltipX + 5 + 'px'
|
|
369
|
+
}
|
|
347
370
|
tooltipRef.current.innerHTML = applyTooltipsToGeo(displayGeoName(county.id), data[county.id])
|
|
348
371
|
tooltipRef.current.setAttribute('data-index', countyIndex)
|
|
349
372
|
} else {
|
|
@@ -354,7 +377,10 @@ const CountyMap = props => {
|
|
|
354
377
|
} else {
|
|
355
378
|
// Handle geo map hover
|
|
356
379
|
if (!isNaN(currentTooltipIndex)) {
|
|
357
|
-
const pixelCoords = topoData.projection([
|
|
380
|
+
const pixelCoords = topoData.projection([
|
|
381
|
+
data[runtimeKeys[currentTooltipIndex]][state.columns.longitude.name],
|
|
382
|
+
data[runtimeKeys[currentTooltipIndex]][state.columns.latitude.name]
|
|
383
|
+
])
|
|
358
384
|
if (pixelCoords && Math.sqrt(Math.pow(pixelCoords[0] - x, 2) + Math.pow(pixelCoords[1] - y, 2)) < geoRadius) {
|
|
359
385
|
// Who knew pythagorean theorum was useful
|
|
360
386
|
return // The user is still hovering over the previous geo point, don't redraw tooltip
|
|
@@ -367,8 +393,16 @@ const CountyMap = props => {
|
|
|
367
393
|
let hoveredGeo
|
|
368
394
|
let hoveredGeoIndex
|
|
369
395
|
for (let i = 0; i < runtimeKeys.length; i++) {
|
|
370
|
-
const pixelCoords = topoData.projection([
|
|
371
|
-
|
|
396
|
+
const pixelCoords = topoData.projection([
|
|
397
|
+
data[runtimeKeys[i]][state.columns.longitude.name],
|
|
398
|
+
data[runtimeKeys[i]][state.columns.latitude.name]
|
|
399
|
+
])
|
|
400
|
+
if (
|
|
401
|
+
state.visual.cityStyle === 'circle' &&
|
|
402
|
+
pixelCoords &&
|
|
403
|
+
Math.sqrt(Math.pow(pixelCoords[0] - x, 2) + Math.pow(pixelCoords[1] - y, 2)) < geoRadius &&
|
|
404
|
+
applyLegendToRow(data[runtimeKeys[i]])
|
|
405
|
+
) {
|
|
372
406
|
hoveredGeo = data[runtimeKeys[i]]
|
|
373
407
|
hoveredGeoIndex = i
|
|
374
408
|
break
|
|
@@ -387,14 +421,17 @@ const CountyMap = props => {
|
|
|
387
421
|
if (hoveredGeo) {
|
|
388
422
|
tooltipRef.current.style.display = 'block'
|
|
389
423
|
tooltipRef.current.style.top = tooltipY + 'px'
|
|
390
|
-
if(tooltipX > containerBounds.width / 2) {
|
|
424
|
+
if (tooltipX > containerBounds.width / 2) {
|
|
391
425
|
tooltipRef.current.style.transform = 'translate(-100%, -50%)'
|
|
392
|
-
tooltipRef.current.style.left =
|
|
426
|
+
tooltipRef.current.style.left = tooltipX - 5 + 'px'
|
|
393
427
|
} else {
|
|
394
428
|
tooltipRef.current.style.transform = 'translate(0, -50%)'
|
|
395
|
-
tooltipRef.current.style.left =
|
|
429
|
+
tooltipRef.current.style.left = tooltipX + 5 + 'px'
|
|
396
430
|
}
|
|
397
|
-
tooltipRef.current.innerHTML = applyTooltipsToGeo(
|
|
431
|
+
tooltipRef.current.innerHTML = applyTooltipsToGeo(
|
|
432
|
+
displayGeoName(hoveredGeo[state.columns.geo.name]),
|
|
433
|
+
hoveredGeo
|
|
434
|
+
)
|
|
398
435
|
tooltipRef.current.setAttribute('data-index', hoveredGeoIndex)
|
|
399
436
|
} else {
|
|
400
437
|
tooltipRef.current.style.display = 'none'
|
|
@@ -431,10 +468,15 @@ const CountyMap = props => {
|
|
|
431
468
|
}
|
|
432
469
|
|
|
433
470
|
// Centers the projection on the paramter passed
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
471
|
+
// Centers the projection on the parameter passed
|
|
472
|
+
if (focus.feature) {
|
|
473
|
+
const PADDING = 10
|
|
474
|
+
// Fit the feature within the canvas dimensions with padding
|
|
475
|
+
const fitExtent = [
|
|
476
|
+
[PADDING, PADDING],
|
|
477
|
+
[canvas.width - 0, canvas.height - PADDING]
|
|
478
|
+
]
|
|
479
|
+
topoData.projection.fitExtent(fitExtent, focus.feature)
|
|
438
480
|
}
|
|
439
481
|
|
|
440
482
|
// Erases previous renderings before redrawing map
|
|
@@ -508,25 +550,34 @@ const CountyMap = props => {
|
|
|
508
550
|
}
|
|
509
551
|
|
|
510
552
|
const drawCircle = (circle, context) => {
|
|
553
|
+
const adjustedGeoRadius = Number(circle.geoRadius)
|
|
511
554
|
context.lineWidth = lineWidth
|
|
512
555
|
context.fillStyle = circle.color
|
|
513
556
|
context.beginPath()
|
|
514
|
-
context.arc(circle.x, circle.y,
|
|
557
|
+
context.arc(circle.x, circle.y, adjustedGeoRadius, 0, 2 * Math.PI)
|
|
515
558
|
context.fill()
|
|
516
559
|
context.stroke()
|
|
517
560
|
}
|
|
518
561
|
|
|
519
562
|
if (state.general.type === 'us-geocode') {
|
|
520
563
|
context.strokeStyle = 'black'
|
|
521
|
-
const geoRadius =
|
|
564
|
+
const geoRadius = state.visual.geoCodeCircleSize || 5
|
|
522
565
|
|
|
523
566
|
runtimeKeys.forEach(key => {
|
|
524
|
-
const pixelCoords = topoData.projection([
|
|
567
|
+
const pixelCoords = topoData.projection([
|
|
568
|
+
data[key][state.columns.longitude.name],
|
|
569
|
+
data[key][state.columns.latitude.name]
|
|
570
|
+
])
|
|
525
571
|
|
|
526
572
|
if (pixelCoords) {
|
|
527
573
|
const legendValues = data[key] !== undefined ? applyLegendToRow(data[key]) : false
|
|
528
574
|
if (legendValues && state.visual.cityStyle === 'circle') {
|
|
529
|
-
const circle = {
|
|
575
|
+
const circle = {
|
|
576
|
+
x: pixelCoords[0],
|
|
577
|
+
y: pixelCoords[1],
|
|
578
|
+
color: legendValues[0],
|
|
579
|
+
geoRadius: geoRadius
|
|
580
|
+
}
|
|
530
581
|
drawCircle(circle, context)
|
|
531
582
|
}
|
|
532
583
|
if (legendValues && state.visual.cityStyle === 'pin') {
|