@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.
Files changed (46) hide show
  1. package/dist/cdcmap.js +40720 -38422
  2. package/examples/county-year.csv +10 -0
  3. package/examples/default-geocode.json +44 -10
  4. package/examples/default-patterns.json +0 -2
  5. package/examples/default-single-state.json +279 -108
  6. package/examples/map-issue-3.json +646 -0
  7. package/examples/single-state-filter.json +153 -0
  8. package/index.html +9 -6
  9. package/package.json +3 -3
  10. package/src/CdcMap.tsx +322 -126
  11. package/src/_stories/CdcMap.stories.tsx +7 -0
  12. package/src/_stories/_mock/DEV-8942.json +270 -0
  13. package/src/components/Annotation/AnnotationDropdown.tsx +1 -0
  14. package/src/components/{BubbleList.jsx → BubbleList.tsx} +1 -1
  15. package/src/components/{CityList.jsx → CityList.tsx} +28 -2
  16. package/src/components/{DataTable.jsx → DataTable.tsx} +2 -2
  17. package/src/components/EditorPanel/components/EditorPanel.tsx +647 -127
  18. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +0 -22
  19. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +61 -11
  20. package/src/components/Legend/components/Legend.tsx +125 -36
  21. package/src/components/Legend/components/index.scss +42 -42
  22. package/src/components/Modal.tsx +25 -0
  23. package/src/components/UsaMap/components/SingleState/SingleState.CountyOutput.tsx +74 -0
  24. package/src/components/UsaMap/components/SingleState/SingleState.StateOutput.tsx +29 -0
  25. package/src/components/UsaMap/components/SingleState/index.tsx +9 -0
  26. package/src/components/UsaMap/components/UsaMap.County.tsx +84 -33
  27. package/src/components/UsaMap/components/UsaMap.SingleState.tsx +173 -206
  28. package/src/components/UsaMap/components/UsaMap.State.tsx +161 -26
  29. package/src/components/UsaMap/data/us-extended-geography.json +1 -0
  30. package/src/components/UsaMap/helpers/map.ts +111 -0
  31. package/src/components/WorldMap/WorldMap.tsx +17 -32
  32. package/src/components/ZoomControls.tsx +41 -0
  33. package/src/data/initial-state.js +7 -1
  34. package/src/data/supported-geos.js +15 -4
  35. package/src/helpers/generateRuntimeLegendHash.ts +2 -2
  36. package/src/hooks/useStateZoom.tsx +157 -0
  37. package/src/hooks/{useZoomPan.js → useZoomPan.ts} +6 -5
  38. package/src/scss/editor-panel.scss +0 -4
  39. package/src/scss/main.scss +23 -1
  40. package/src/scss/map.scss +8 -0
  41. package/src/types/MapConfig.ts +9 -1
  42. package/src/types/MapContext.ts +14 -2
  43. package/src/components/Modal.jsx +0 -22
  44. /package/src/components/{Geo.jsx → Geo.tsx} +0 -0
  45. /package/src/components/{NavigationMenu.jsx → NavigationMenu.tsx} +0 -0
  46. /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
@@ -0,0 +1,9 @@
1
+ import CountyOutput from './SingleState.CountyOutput'
2
+ import StateOutput from './SingleState.StateOutput'
3
+
4
+ const SingleState = {
5
+ StateOutput,
6
+ CountyOutput
7
+ }
8
+
9
+ export default SingleState
@@ -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 (let i = topoData.countyIndecies[clickedState.id][0]; i <= topoData.countyIndecies[clickedState.id][1]; i++) {
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([data[runtimeKeys[i]][state.columns.longitude.name], data[runtimeKeys[i]][state.columns.latitude.name]])
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 (!tooltipRef.current || state.tooltips.appearanceType !== 'hover' || window.matchMedia('(any-hover: none)').matches) return
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) * (focus.id ? 2 : 1)
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
- if(tooltipX > containerBounds.width / 2) {
341
- tooltipRef.current.style.transform = 'translate(-100%, -50%)'
342
- tooltipRef.current.style.left = (tooltipX - 5) + 'px'
343
- } else {
344
- tooltipRef.current.style.transform = 'translate(0, -50%)'
345
- tooltipRef.current.style.left = (tooltipX + 5) + 'px'
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([data[runtimeKeys[currentTooltipIndex]][state.columns.longitude.name], data[runtimeKeys[currentTooltipIndex]][state.columns.latitude.name]])
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([data[runtimeKeys[i]][state.columns.longitude.name], data[runtimeKeys[i]][state.columns.latitude.name]])
371
- if (state.visual.cityStyle === 'circle' && pixelCoords && Math.sqrt(Math.pow(pixelCoords[0] - x, 2) + Math.pow(pixelCoords[1] - y, 2)) < geoRadius && applyLegendToRow(data[runtimeKeys[i]])) {
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 = (tooltipX - 5) + 'px'
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 = (tooltipX + 5) + 'px'
429
+ tooltipRef.current.style.left = tooltipX + 5 + 'px'
396
430
  }
397
- tooltipRef.current.innerHTML = applyTooltipsToGeo(displayGeoName(hoveredGeo[state.columns.geo.name]), hoveredGeo)
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
- if (focus.center) {
435
- topoData.projection.scale(canvas.width * (focus.id === '72' ? 10 : 2.5))
436
- let offset = topoData.projection(focus.center)
437
- topoData.projection.translate([-offset[0] + canvas.width, -offset[1] + canvas.height])
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, circle.geoRadius, 0, 2 * Math.PI)
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 = (state.visual.geoCodeCircleSize || 5) * (focus.id ? 2 : 1)
564
+ const geoRadius = state.visual.geoCodeCircleSize || 5
522
565
 
523
566
  runtimeKeys.forEach(key => {
524
- const pixelCoords = topoData.projection([data[key][state.columns.longitude.name], data[key][state.columns.latitude.name]])
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 = { x: pixelCoords[0], y: pixelCoords[1], color: legendValues[0], geoRadius }
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') {