@cdc/map 4.24.5 → 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 (67) hide show
  1. package/dist/cdcmap.js +71853 -64936
  2. package/examples/annotation/index.json +552 -0
  3. package/examples/annotation/usa-map.json +900 -0
  4. package/examples/county-year.csv +10 -0
  5. package/examples/default-geocode.json +44 -10
  6. package/examples/default-patterns.json +0 -2
  7. package/examples/default-single-state.json +279 -108
  8. package/examples/map-issue-3.json +646 -0
  9. package/examples/single-state-filter.json +153 -0
  10. package/index.html +10 -6
  11. package/package.json +6 -5
  12. package/src/CdcMap.tsx +367 -199
  13. package/src/_stories/CdcMap.stories.tsx +14 -0
  14. package/src/_stories/_mock/DEV-7286.json +165 -0
  15. package/src/_stories/_mock/DEV-8942.json +270 -0
  16. package/src/components/Annotation/Annotation.Draggable.styles.css +18 -0
  17. package/src/components/Annotation/Annotation.Draggable.tsx +152 -0
  18. package/src/components/Annotation/AnnotationDropdown.styles.css +14 -0
  19. package/src/components/Annotation/AnnotationDropdown.tsx +70 -0
  20. package/src/components/Annotation/AnnotationList.styles.css +45 -0
  21. package/src/components/Annotation/AnnotationList.tsx +42 -0
  22. package/src/components/Annotation/index.tsx +11 -0
  23. package/src/components/{BubbleList.jsx → BubbleList.tsx} +1 -1
  24. package/src/components/{CityList.jsx → CityList.tsx} +28 -2
  25. package/src/components/{DataTable.jsx → DataTable.tsx} +2 -2
  26. package/src/components/EditorPanel/components/EditorPanel.tsx +650 -129
  27. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +336 -0
  28. package/src/components/EditorPanel/components/{Panel.PatternSettings.tsx → Panels/Panel.PatternSettings.tsx} +63 -13
  29. package/src/components/EditorPanel/components/{Panels.tsx → Panels/index.tsx} +3 -0
  30. package/src/components/Legend/components/Legend.tsx +125 -42
  31. package/src/components/Legend/components/index.scss +42 -42
  32. package/src/components/Modal.tsx +25 -0
  33. package/src/components/UsaMap/components/SingleState/SingleState.CountyOutput.tsx +74 -0
  34. package/src/components/UsaMap/components/SingleState/SingleState.StateOutput.tsx +29 -0
  35. package/src/components/UsaMap/components/SingleState/index.tsx +9 -0
  36. package/src/components/UsaMap/components/Territory/Territory.Rectangle.tsx +4 -3
  37. package/src/components/UsaMap/components/UsaMap.County.tsx +114 -36
  38. package/src/components/UsaMap/components/UsaMap.Region.tsx +2 -0
  39. package/src/components/UsaMap/components/UsaMap.SingleState.tsx +175 -206
  40. package/src/components/UsaMap/components/UsaMap.State.tsx +188 -44
  41. package/src/components/UsaMap/data/us-extended-geography.json +1 -0
  42. package/src/components/UsaMap/helpers/map.ts +111 -0
  43. package/src/components/WorldMap/WorldMap.tsx +17 -32
  44. package/src/components/ZoomControls.tsx +41 -0
  45. package/src/data/initial-state.js +11 -2
  46. package/src/data/supported-geos.js +15 -4
  47. package/src/helpers/generateColorsArray.ts +13 -0
  48. package/src/helpers/generateRuntimeLegendHash.ts +23 -0
  49. package/src/helpers/getUniqueValues.ts +19 -0
  50. package/src/helpers/hashObj.ts +25 -0
  51. package/src/helpers/tests/generateColorsArray.test.ts +18 -0
  52. package/src/helpers/tests/generateRuntimeLegendHash.test.ts +11 -0
  53. package/src/helpers/tests/hashObj.test.ts +10 -0
  54. package/src/hooks/useStateZoom.tsx +157 -0
  55. package/src/hooks/{useZoomPan.js → useZoomPan.ts} +6 -5
  56. package/src/scss/editor-panel.scss +0 -4
  57. package/src/scss/main.scss +23 -1
  58. package/src/scss/map.scss +14 -3
  59. package/src/types/MapConfig.ts +9 -1
  60. package/src/types/MapContext.ts +16 -2
  61. package/LICENSE +0 -201
  62. package/src/components/Modal.jsx +0 -22
  63. package/src/test/CdcMap.test.jsx +0 -19
  64. /package/src/components/EditorPanel/components/{Panel.PatternSettings-style.css → Panels/Panel.PatternSettings-style.css} +0 -0
  65. /package/src/components/{Geo.jsx → Geo.tsx} +0 -0
  66. /package/src/components/{NavigationMenu.jsx → NavigationMenu.tsx} +0 -0
  67. /package/src/components/{ZoomableGroup.jsx → ZoomableGroup.tsx} +0 -0
@@ -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'
@@ -10,6 +11,7 @@ import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
10
11
 
11
12
  import useMapLayers from '../../../hooks/useMapLayers'
12
13
  import ConfigContext from '../../../context'
14
+ import Annotation from '../../Annotation'
13
15
 
14
16
  const getCountyTopoURL = year => {
15
17
  return `https://www.cdc.gov/TemplatePackage/contrib/data/county-topography/cb_${year}_us_county_20m.json`
@@ -129,7 +131,9 @@ const CountyMap = props => {
129
131
  state,
130
132
  runtimeFilters,
131
133
  tooltipId,
132
- isEditor
134
+ tooltipRef,
135
+ container,
136
+ setState
133
137
  } = useContext(ConfigContext)
134
138
 
135
139
  // CREATE STATE LINES
@@ -186,7 +190,6 @@ const CountyMap = props => {
186
190
 
187
191
  const resetButton = useRef()
188
192
  const canvasRef = useRef()
189
- const tooltipRef = useRef()
190
193
 
191
194
  // If runtimeData is not defined, show loader
192
195
  if (!data || !isTopoReady(topoData, state, runtimeFilters)) {
@@ -203,6 +206,10 @@ const CountyMap = props => {
203
206
  const lineWidth = 0.3
204
207
 
205
208
  const onReset = () => {
209
+ setState({
210
+ ...state,
211
+ mapPosition: { coordinates: [0, 30], zoom: 1 }
212
+ })
206
213
  setFocus({})
207
214
  }
208
215
 
@@ -224,10 +231,19 @@ const CountyMap = props => {
224
231
 
225
232
  // If the user clicked outside of all states, no behavior
226
233
  if (clickedState) {
234
+ setState({
235
+ ...state,
236
+ mapPosition: { coordinates: [0, 30], zoom: 3 }
237
+ })
238
+
227
239
  // If a county within the state was also clicked and has data, call parent click handler
228
240
  if (topoData.countyIndecies[clickedState.id]) {
229
241
  let county
230
- 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
+ ) {
231
247
  if (geoContains(topoData.mapData[i], pointCoordinates)) {
232
248
  county = topoData.mapData[i]
233
249
  break
@@ -238,15 +254,26 @@ const CountyMap = props => {
238
254
  }
239
255
  }
240
256
 
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
262
+ }
263
+ }
264
+
241
265
  // Redraw with focus on state
242
- setFocus({ id: clickedState.id, center: geoCentroid(clickedState) })
266
+ setFocus({ id: clickedState.id, index: focusIndex, center: geoCentroid(clickedState), feature: clickedState })
243
267
  }
244
268
 
245
269
  if (state.general.type === 'us-geocode') {
246
270
  const geoRadius = (state.visual.geoCodeCircleSize || 5) * (focus.id ? 2 : 1)
247
271
  let clickedGeo
248
272
  for (let i = 0; i < runtimeKeys.length; i++) {
249
- 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
+ ])
250
277
  if (pixelCoords && Math.sqrt(Math.pow(pixelCoords[0] - x, 2) + Math.pow(pixelCoords[1] - y, 2)) < geoRadius) {
251
278
  clickedGeo = data[runtimeKeys[i]]
252
279
  break
@@ -260,23 +287,32 @@ const CountyMap = props => {
260
287
  }
261
288
 
262
289
  const canvasHover = e => {
263
- 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
264
296
 
265
297
  const canvas = e.target
266
298
  const canvasBounds = canvas.getBoundingClientRect()
267
299
  const x = e.clientX - canvasBounds.left
268
300
  const y = e.clientY - canvasBounds.top
301
+ const containerBounds = container?.getBoundingClientRect()
302
+ const tooltipX = e.clientX - (containerBounds?.left || 0)
303
+ const tooltipY = e.clientY - (containerBounds?.top || 0)
269
304
  let pointCoordinates = topoData.projection.invert([x, y])
270
305
 
271
306
  const currentTooltipIndex = parseInt(tooltipRef.current.getAttribute('data-index'))
272
- const geoRadius = (state.visual.geoCodeCircleSize || 5) * (focus.id ? 2 : 1)
307
+ const geoRadius = (state.visual.geoCodeCircleSize || 5) * 1
308
+
309
+ const context = canvas.getContext('2d')
310
+ const path = geoPath(topoData.projection, context)
273
311
 
274
312
  // Handle standard county map hover
275
313
  if (state.general.type !== 'us-geocode') {
276
314
  //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
277
315
  if (isNaN(currentTooltipIndex) || !geoContains(topoData.mapData[currentTooltipIndex], pointCoordinates)) {
278
- const context = canvas.getContext('2d')
279
- const path = geoPath(topoData.projection, context)
280
316
  if (!isNaN(currentTooltipIndex) && applyLegendToRow(data[topoData.mapData[currentTooltipIndex].id])) {
281
317
  context.fillStyle = applyLegendToRow(data[topoData.mapData[currentTooltipIndex].id])[0]
282
318
  context.strokeStyle = geoStrokeColor
@@ -323,8 +359,14 @@ const CountyMap = props => {
323
359
  }
324
360
 
325
361
  tooltipRef.current.style.display = 'block'
326
- tooltipRef.current.style.top = e.clientY + 'px'
327
- tooltipRef.current.style.left = isEditor ? Number(e.clientX - 350) + 'px' : e.clientX + 'px'
362
+ tooltipRef.current.style.top = tooltipY + 'px'
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
+ }
328
370
  tooltipRef.current.innerHTML = applyTooltipsToGeo(displayGeoName(county.id), data[county.id])
329
371
  tooltipRef.current.setAttribute('data-index', countyIndex)
330
372
  } else {
@@ -335,7 +377,10 @@ const CountyMap = props => {
335
377
  } else {
336
378
  // Handle geo map hover
337
379
  if (!isNaN(currentTooltipIndex)) {
338
- 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
+ ])
339
384
  if (pixelCoords && Math.sqrt(Math.pow(pixelCoords[0] - x, 2) + Math.pow(pixelCoords[1] - y, 2)) < geoRadius) {
340
385
  // Who knew pythagorean theorum was useful
341
386
  return // The user is still hovering over the previous geo point, don't redraw tooltip
@@ -348,8 +393,16 @@ const CountyMap = props => {
348
393
  let hoveredGeo
349
394
  let hoveredGeoIndex
350
395
  for (let i = 0; i < runtimeKeys.length; i++) {
351
- const pixelCoords = topoData.projection([data[runtimeKeys[i]][state.columns.longitude.name], data[runtimeKeys[i]][state.columns.latitude.name]])
352
- if (state.visual.cityStyle === 'circle' && pixelCoords && Math.sqrt(Math.pow(pixelCoords[0] - x, 2) + Math.pow(pixelCoords[1] - y, 2)) < geoRadius) {
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
+ ) {
353
406
  hoveredGeo = data[runtimeKeys[i]]
354
407
  hoveredGeoIndex = i
355
408
  break
@@ -357,7 +410,7 @@ const CountyMap = props => {
357
410
 
358
411
  if (state.visual.cityStyle === 'pin' && pixelCoords) {
359
412
  const distance = Math.hypot(pixelCoords[0] - x, pixelCoords[1] - y)
360
- if (distance < 15) {
413
+ if (distance < 15 && applyLegendToRow(data[runtimeKeys[i]])) {
361
414
  hoveredGeo = data[runtimeKeys[i]]
362
415
  hoveredGeoIndex = i
363
416
  break
@@ -365,17 +418,34 @@ const CountyMap = props => {
365
418
  }
366
419
  }
367
420
 
368
- if (hoveredGeo && applyLegendToRow(hoveredGeo)) {
421
+ if (hoveredGeo) {
369
422
  tooltipRef.current.style.display = 'block'
370
- tooltipRef.current.style.top = e.clientY + 'px'
371
- tooltipRef.current.style.left = e.clientX + 'px'
372
- tooltipRef.current.innerHTML = applyTooltipsToGeo(displayGeoName(hoveredGeo[state.columns.geo.name]), hoveredGeo)
423
+ tooltipRef.current.style.top = tooltipY + 'px'
424
+ if (tooltipX > containerBounds.width / 2) {
425
+ tooltipRef.current.style.transform = 'translate(-100%, -50%)'
426
+ tooltipRef.current.style.left = tooltipX - 5 + 'px'
427
+ } else {
428
+ tooltipRef.current.style.transform = 'translate(0, -50%)'
429
+ tooltipRef.current.style.left = tooltipX + 5 + 'px'
430
+ }
431
+ tooltipRef.current.innerHTML = applyTooltipsToGeo(
432
+ displayGeoName(hoveredGeo[state.columns.geo.name]),
433
+ hoveredGeo
434
+ )
373
435
  tooltipRef.current.setAttribute('data-index', hoveredGeoIndex)
374
436
  } else {
375
437
  tooltipRef.current.style.display = 'none'
376
438
  tooltipRef.current.setAttribute('data-index', null)
377
439
  }
378
440
  }
441
+
442
+ if (focus.index !== -1) {
443
+ context.strokeStyle = 'black'
444
+ context.lineWidth = 1
445
+ context.beginPath()
446
+ path(topoData.mapData[focus.index])
447
+ context.stroke()
448
+ }
379
449
  }
380
450
 
381
451
  // Redraws canvas. Takes as parameters the fips id of a state to center on and the [lat,long] center of that state
@@ -398,10 +468,15 @@ const CountyMap = props => {
398
468
  }
399
469
 
400
470
  // Centers the projection on the paramter passed
401
- if (focus.center) {
402
- topoData.projection.scale(canvas.width * (focus.id === '72' ? 10 : 2.5))
403
- let offset = topoData.projection(focus.center)
404
- 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)
405
480
  }
406
481
 
407
482
  // Erases previous renderings before redrawing map
@@ -411,7 +486,6 @@ const CountyMap = props => {
411
486
  context.strokeStyle = geoStrokeColor
412
487
  context.lineWidth = lineWidth
413
488
 
414
- let focusIndex = -1
415
489
  // Iterates through each state/county topo and renders it
416
490
  topoData.mapData.forEach((geo, i) => {
417
491
  // If invalid geo item, don't render
@@ -424,11 +498,6 @@ const CountyMap = props => {
424
498
  // Gets numeric data associated with the topo data for this state/county
425
499
  const geoData = data[geo.id]
426
500
 
427
- // Marks that the focused state was found for the logic below
428
- if (geo.id === focus.id) {
429
- focusIndex = i
430
- }
431
-
432
501
  // Renders state/county
433
502
  const legendValues = geoData !== undefined ? applyLegendToRow(geoData) : false
434
503
  context.fillStyle = legendValues && state.general.type !== 'us-geocode' ? legendValues[0] : '#EEE'
@@ -439,11 +508,11 @@ const CountyMap = props => {
439
508
  })
440
509
 
441
510
  // If the focused state is found in the geo data, render it with a thicker outline
442
- if (focusIndex !== -1) {
511
+ if (focus.index !== -1) {
443
512
  context.strokeStyle = 'black'
444
513
  context.lineWidth = 2
445
514
  context.beginPath()
446
- path(topoData.mapData[focusIndex])
515
+ path(topoData.mapData[focus.index])
447
516
  context.stroke()
448
517
  }
449
518
 
@@ -481,24 +550,34 @@ const CountyMap = props => {
481
550
  }
482
551
 
483
552
  const drawCircle = (circle, context) => {
553
+ const adjustedGeoRadius = Number(circle.geoRadius)
554
+ context.lineWidth = lineWidth
484
555
  context.fillStyle = circle.color
485
556
  context.beginPath()
486
- context.arc(circle.x, circle.y, circle.geoRadius, 0, 2 * Math.PI)
557
+ context.arc(circle.x, circle.y, adjustedGeoRadius, 0, 2 * Math.PI)
487
558
  context.fill()
488
559
  context.stroke()
489
560
  }
490
561
 
491
562
  if (state.general.type === 'us-geocode') {
492
563
  context.strokeStyle = 'black'
493
- const geoRadius = (state.visual.geoCodeCircleSize || 5) * (focus.id ? 2 : 1)
564
+ const geoRadius = state.visual.geoCodeCircleSize || 5
494
565
 
495
566
  runtimeKeys.forEach(key => {
496
- 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
+ ])
497
571
 
498
572
  if (pixelCoords) {
499
573
  const legendValues = data[key] !== undefined ? applyLegendToRow(data[key]) : false
500
574
  if (legendValues && state.visual.cityStyle === 'circle') {
501
- 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
+ }
502
581
  drawCircle(circle, context)
503
582
  }
504
583
  if (legendValues && state.visual.cityStyle === 'pin') {
@@ -524,7 +603,6 @@ const CountyMap = props => {
524
603
  onClick={canvasClick}
525
604
  className='county-map-canvas'
526
605
  ></canvas>
527
- <div ref={tooltipRef} id={`tooltip__${tooltipId}`} className='tooltip' style={{ background: `rgba(255,255,255,${state.tooltips.opacity / 100})` }}></div>
528
606
  <button className={`btn btn--reset`} onClick={onReset} ref={resetButton} tabIndex='0'>
529
607
  Reset Zoom
530
608
  </button>
@@ -10,6 +10,7 @@ import { Mercator } from '@visx/geo'
10
10
  import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
11
11
  import topoJSON from '../data/us-regions-topo-2.json'
12
12
  import ConfigContext from '../../../context'
13
+ import Annotation from '../../Annotation'
13
14
 
14
15
  const { features: unitedStates } = feature(topoJSON, topoJSON.objects.regions)
15
16
 
@@ -252,6 +253,7 @@ const UsaRegionMap = props => {
252
253
  <Mercator data={focusedStates} scale={620} translate={[1500, 735]}>
253
254
  {({ features, projection }) => constructGeoJsx(features, projection)}
254
255
  </Mercator>
256
+ {state.annotations.length > 0 && <Annotation.Draggable />}
255
257
  </svg>
256
258
  {territories.length > 0 && (
257
259
  <section className='territories'>