@cdc/map 4.24.12 → 4.25.1

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 (60) hide show
  1. package/dist/cdcmap.js +51010 -49337
  2. package/examples/annotation/index.json +1 -1
  3. package/examples/custom-map-layers.json +1 -1
  4. package/examples/default-geocode.json +2 -2
  5. package/examples/private/DEV-9989.json +229 -0
  6. package/examples/private/ardi.json +180 -0
  7. package/examples/private/colors 2.json +416 -0
  8. package/examples/private/colors.json +416 -0
  9. package/examples/private/colors.json.zip +0 -0
  10. package/examples/private/customColors.json +45348 -0
  11. package/examples/private/mmr.json +246 -0
  12. package/examples/private/test.json +1632 -0
  13. package/index.html +12 -14
  14. package/package.json +8 -3
  15. package/src/CdcMap.tsx +126 -394
  16. package/src/_stories/CdcMap.Legend.Gradient.stories.tsx +9 -0
  17. package/src/_stories/CdcMap.stories.tsx +1 -1
  18. package/src/_stories/GoogleMap.stories.tsx +19 -0
  19. package/src/_stories/_mock/DEV-10148.json +859 -0
  20. package/src/_stories/_mock/DEV-9989.json +229 -0
  21. package/src/_stories/_mock/example-city-state.json +1 -1
  22. package/src/_stories/_mock/google-map.json +819 -0
  23. package/src/components/Annotation/Annotation.Draggable.tsx +34 -43
  24. package/src/components/Annotation/AnnotationDropdown.tsx +4 -4
  25. package/src/components/CityList.tsx +2 -2
  26. package/src/components/DataTable.tsx +8 -9
  27. package/src/components/EditorPanel/components/EditorPanel.tsx +96 -270
  28. package/src/components/GoogleMap/components/GoogleMap.tsx +67 -0
  29. package/src/components/GoogleMap/index.tsx +3 -0
  30. package/src/components/Legend/components/Legend.tsx +40 -30
  31. package/src/components/Legend/components/LegendItem.Hex.tsx +7 -3
  32. package/src/components/Legend/components/index.scss +22 -16
  33. package/src/components/Modal.tsx +6 -5
  34. package/src/components/NavigationMenu.tsx +5 -4
  35. package/src/components/UsaMap/components/TerritoriesSection.tsx +56 -0
  36. package/src/components/UsaMap/components/Territory/Territory.Rectangle.tsx +3 -3
  37. package/src/components/UsaMap/components/UsaMap.County.tsx +1 -1
  38. package/src/components/UsaMap/components/UsaMap.Region.tsx +12 -8
  39. package/src/components/UsaMap/components/UsaMap.SingleState.tsx +2 -2
  40. package/src/components/UsaMap/components/UsaMap.State.tsx +23 -29
  41. package/src/components/WorldMap/WorldMap.tsx +3 -5
  42. package/src/context.ts +0 -12
  43. package/src/data/initial-state.js +2 -2
  44. package/src/data/supported-geos.js +23 -3
  45. package/src/helpers/applyColorToLegend.ts +3 -3
  46. package/src/helpers/closeModal.ts +9 -0
  47. package/src/helpers/handleMapAriaLabels.ts +38 -0
  48. package/src/helpers/indexOfIgnoreType.ts +8 -0
  49. package/src/helpers/navigationHandler.ts +21 -0
  50. package/src/helpers/toTitleCase.ts +44 -0
  51. package/src/helpers/validateFipsCodeLength.ts +30 -0
  52. package/src/hooks/useResizeObserver.ts +42 -0
  53. package/src/hooks/useTooltip.ts +4 -2
  54. package/src/index.jsx +1 -0
  55. package/src/scss/editor-panel.scss +2 -1
  56. package/src/scss/filters.scss +0 -5
  57. package/src/scss/main.scss +57 -61
  58. package/src/scss/map.scss +1 -13
  59. package/src/types/MapConfig.ts +20 -11
  60. package/src/types/MapContext.ts +4 -12
package/src/CdcMap.tsx CHANGED
@@ -1,47 +1,36 @@
1
+ // Vendor
1
2
  import React, { useState, useEffect, useRef, useCallback, useId } from 'react'
2
3
  import * as d3 from 'd3'
3
- import Layout from '@cdc/core/components/Layout'
4
- import Waiting from '@cdc/core/components/Waiting'
5
- import Annotation from './components/Annotation'
6
- import Error from './components/EditorPanel/components/Error'
7
4
  import _ from 'lodash'
8
- import { applyColorToLegend } from './helpers/applyColorToLegend'
9
-
10
- // types
11
- import { type ViewportSize } from './types/MapConfig'
12
- import { type DimensionsType } from '@cdc/core/types/Dimensions'
13
-
14
- // IE11
15
5
  import 'whatwg-fetch'
16
- import ResizeObserver from 'resize-observer-polyfill'
17
-
18
- // Third party
19
6
  import { Tooltip as ReactTooltip } from 'react-tooltip'
20
7
  import Papa from 'papaparse'
21
8
  import parse from 'html-react-parser'
22
9
  import 'react-tooltip/dist/react-tooltip.css'
23
10
 
24
- // Helpers
25
- import { hashObj } from './helpers/hashObj'
26
- import { generateRuntimeLegendHash } from './helpers/generateRuntimeLegendHash'
27
- import { generateColorsArray } from './helpers/generateColorsArray'
28
- import { getUniqueValues } from './helpers/getUniqueValues'
29
- import { publish } from '@cdc/core/helpers/events'
30
- import coveUpdateWorker from '@cdc/core/helpers/coveUpdateWorker'
31
- import { getQueryStringFilterValue } from '@cdc/core/helpers/queryStringUtils'
11
+ // Core Components
12
+ import DataTable from '@cdc/core/components/DataTable'
13
+ import Filters, { useFilters } from '@cdc/core/components/Filters'
14
+ import Layout from '@cdc/core/components/Layout'
15
+ import MediaControls from '@cdc/core/components/MediaControls'
16
+ import SkipTo from '@cdc/core/components/elements/SkipTo'
32
17
  import Title from '@cdc/core/components/ui/Title'
18
+ import Waiting from '@cdc/core/components/Waiting'
19
+
20
+ // types
21
+ import { type Coordinate, type MapConfig } from './types/MapConfig'
33
22
 
34
23
  // Data
35
24
  import { countryCoordinates } from './data/country-coordinates'
36
25
  import {
37
- supportedStates,
38
- supportedTerritories,
39
- supportedCountries,
40
- supportedCounties,
26
+ stateFipsToTwoDigit,
41
27
  supportedCities,
28
+ supportedCounties,
29
+ supportedCountries,
30
+ supportedRegions,
31
+ supportedStates,
42
32
  supportedStatesFipsCodes,
43
- stateFipsToTwoDigit,
44
- supportedRegions
33
+ supportedTerritories
45
34
  } from './data/supported-geos'
46
35
  import colorPalettes from '@cdc/core/data/colorPalettes'
47
36
  import initialState from './data/initial-state'
@@ -51,33 +40,48 @@ import ExternalIcon from './images/external-link.svg'
51
40
 
52
41
  // Sass
53
42
  import './scss/main.scss'
54
-
55
- // TODO: combine in scss.
56
43
  import './scss/btn.scss'
57
44
 
58
- // Core
59
- import { DataTransform } from '@cdc/core/helpers/DataTransform'
60
- import MediaControls from '@cdc/core/components/MediaControls'
45
+ // Core Helpers
46
+ import coveUpdateWorker from '@cdc/core/helpers/coveUpdateWorker'
61
47
  import fetchRemoteData from '@cdc/core/helpers/fetchRemoteData'
62
- import getViewport from '@cdc/core/helpers/getViewport'
63
48
  import isDomainExternal from '@cdc/core/helpers/isDomainExternal'
64
49
  import numberFromString from '@cdc/core/helpers/numberFromString'
65
- import DataTable from '@cdc/core/components/DataTable' // Future: Lazy
50
+ import { DataTransform } from '@cdc/core/helpers/DataTransform'
51
+ import { getQueryStringFilterValue } from '@cdc/core/helpers/queryStringUtils'
52
+ import { isSolrCsv, isSolrJson } from '@cdc/core/helpers/isSolr'
53
+ import { publish } from '@cdc/core/helpers/events'
54
+
55
+ // Map Helpers
56
+ import { applyColorToLegend } from './helpers/applyColorToLegend'
57
+ import { closeModal } from './helpers/closeModal'
58
+ import { generateColorsArray } from './helpers/generateColorsArray'
59
+ import { generateRuntimeLegendHash } from './helpers/generateRuntimeLegendHash'
60
+ import { getGeoFillColor } from './helpers/colors'
61
+ import { getUniqueValues } from './helpers/getUniqueValues'
62
+ import { hashObj } from './helpers/hashObj'
63
+ import { navigationHandler } from './helpers/navigationHandler'
64
+ import { validateFipsCodeLength } from './helpers/validateFipsCodeLength'
65
+ import { titleCase } from './helpers/titleCase'
66
+ import { indexOfIgnoreType } from './helpers/indexOfIgnoreType'
66
67
 
67
68
  // Child Components
69
+ import Annotation from './components/Annotation'
68
70
  import ConfigContext from './context'
69
- import Filters, { useFilters } from '@cdc/core/components/Filters'
70
- import Modal from './components/Modal'
71
+ import EditorPanel from './components/EditorPanel'
72
+ import Error from './components/EditorPanel/components/Error'
71
73
  import Legend from './components/Legend'
74
+ import Modal from './components/Modal'
75
+ import NavigationMenu from './components/NavigationMenu'
76
+ import UsaMap from './components/UsaMap'
77
+ import WorldMap from './components/WorldMap'
78
+ import GoogleMap from './components/GoogleMap'
72
79
 
73
- import EditorPanel from './components/EditorPanel' // Future: Lazy
74
- import NavigationMenu from './components/NavigationMenu' // Future: Lazy
75
- import UsaMap from './components/UsaMap' // Future: Lazy
76
- import WorldMap from './components/WorldMap' // Future: Lazy
80
+ // hooks
77
81
  import useTooltip from './hooks/useTooltip'
78
- import { isSolrCsv, isSolrJson } from '@cdc/core/helpers/isSolr'
79
- import SkipTo from '@cdc/core/components/elements/SkipTo'
80
- import { getGeoFillColor } from './helpers/colors'
82
+ import useResizeObserver from './hooks/useResizeObserver'
83
+ import { SubGrouping } from '@cdc/core/types/VizFilter'
84
+ import { ViewPort } from '@cdc/core/types/ViewPort'
81
85
 
82
86
  // Data props
83
87
  const stateKeys = Object.keys(supportedStates)
@@ -87,17 +91,7 @@ const countryKeys = Object.keys(supportedCountries)
87
91
  const countyKeys = Object.keys(supportedCounties)
88
92
  const cityKeys = Object.keys(supportedCities)
89
93
 
90
- const indexOfIgnoreType = (arr, item) => {
91
- for (let i = 0; i < arr.length; i++) {
92
- if (item === arr[i]) {
93
- return i
94
- }
95
- }
96
- return -1
97
- }
98
-
99
94
  const CdcMap = ({
100
- className,
101
95
  config,
102
96
  navigationHandler: customNavigationHandler,
103
97
  isDashboard = false,
@@ -113,26 +107,31 @@ const CdcMap = ({
113
107
  const transform = new DataTransform()
114
108
  const [translate, setTranslate] = useState([0, 0])
115
109
  const [scale, setScale] = useState(1)
116
- const [state, setState] = useState({ ...initialState })
110
+ const [state, setState] = useState<MapConfig>({ ...initialState })
117
111
  const [isDraggingAnnotation, setIsDraggingAnnotation] = useState(false)
118
112
  const [loading, setLoading] = useState(true)
119
113
  const [displayPanel, setDisplayPanel] = useState(true)
120
- const [currentViewport, setCurrentViewport] = useState<ViewportSize>('lg')
121
- const [topoData, setTopoData] = useState<Topology | {}>({})
114
+ const [topoData, setTopoData] = useState<{}>({})
122
115
  const [runtimeFilters, setRuntimeFilters] = useState([])
123
- const [runtimeLegend, setRuntimeLegend] = useState([])
124
116
  const [runtimeData, setRuntimeData] = useState({ init: true })
117
+ const _setRuntimeData = (data: any) => {
118
+ if (config) {
119
+ setRuntimeData(data)
120
+ } else {
121
+ setRuntimeFilters(data)
122
+ }
123
+ }
124
+ const [runtimeLegend, setRuntimeLegend] = useState([])
125
125
  const [stateToShow, setStateToShow] = useState(null)
126
126
  const [modal, setModal] = useState(null)
127
127
  const [accessibleStatus, setAccessibleStatus] = useState('')
128
128
  const [filteredCountryCode, setFilteredCountryCode] = useState()
129
129
  const [position, setPosition] = useState(state.mapPosition)
130
130
  const [coveLoadedHasRan, setCoveLoadedHasRan] = useState(false)
131
- const [container, setContainer] = useState()
132
131
  const [imageId, setImageId] = useState(`cove-${Math.random().toString(16).slice(-4)}`) // eslint-disable-line
133
- const [dimensions, setDimensions] = useState<DimensionsType>([0, 0])
134
132
  const [requiredColumns, setRequiredColumns] = useState(null) // Simple state so we know if we need more information before parsing the map
135
133
  const [projection, setProjection] = useState(null)
134
+ const { currentViewport, dimensions, container, outerContainerRef } = useResizeObserver(isEditor)
136
135
 
137
136
  const legendRef = useRef(null)
138
137
  const tooltipRef = useRef(null)
@@ -141,7 +140,7 @@ const CdcMap = ({
141
140
  const tooltipId = `${Math.random().toString(16).slice(-4)}`
142
141
  const mapId = useId()
143
142
 
144
- const { changeFilterActive, handleSorting } = useFilters({ config: state, setConfig: setState })
143
+ const { handleSorting } = useFilters({ config: state, setConfig: setState })
145
144
  let legendMemo = useRef(new Map())
146
145
  let legendSpecialClassLastMemo = useRef(new Map())
147
146
  let innerContainerRef = useRef()
@@ -166,10 +165,7 @@ const CdcMap = ({
166
165
  }
167
166
 
168
167
  // Navigate is required for navigation maps
169
- if (
170
- 'navigation' === state.general.type &&
171
- ('' === state.columns.navigate.name || undefined === state.columns.navigate)
172
- ) {
168
+ if ('navigation' === state.general.type && '' === state.columns.navigate.name) {
173
169
  columnList.push('Navigation')
174
170
  }
175
171
 
@@ -198,7 +194,7 @@ const CdcMap = ({
198
194
  const coordinates = countryCoordinates[filteredCountryCode]
199
195
  const long = coordinates[1]
200
196
  const lat = coordinates[0]
201
- const reversedCoordinates = [long, lat]
197
+ const reversedCoordinates: Coordinate = [long, lat]
202
198
 
203
199
  setState({
204
200
  ...state,
@@ -228,27 +224,11 @@ const CdcMap = ({
228
224
  }
229
225
  }, [state.mapPosition, setPosition])
230
226
 
231
- const resizeObserver = new ResizeObserver(entries => {
232
- for (let entry of entries) {
233
- let { width, height } = entry.contentRect
234
- let newViewport = getViewport(entry.contentRect.width)
235
-
236
- let editorWidth = 350
237
-
238
- setCurrentViewport(newViewport)
239
-
240
- if (isEditor) {
241
- width = width - editorWidth
242
- }
243
- setDimensions([width, height])
244
- }
245
- })
246
-
247
227
  // Tag each row with a UID. Helps with filtering/placing geos. Not enumerable so doesn't show up in loops/console logs except when directly addressed ex row.uid
248
228
  // We are mutating state in place here (depending on where called) - but it's okay, this isn't used for rerender
249
229
  // eslint-disable-next-line
250
230
  const addUIDs = useCallback((obj, fromColumn) => {
251
- obj.data.forEach((row, index) => {
231
+ obj.data.forEach(row => {
252
232
  let uid = null
253
233
 
254
234
  if (row.uid) row.uid = null // Wipe existing UIDs
@@ -358,7 +338,7 @@ const CdcMap = ({
358
338
  })
359
339
 
360
340
  // eslint-disable-next-line
361
- const generateRuntimeLegend = useCallback((obj, runtimeData, hash) => {
341
+ const generateRuntimeLegend = useCallback((obj, runtimeFilters, hash) => {
362
342
  const newLegendMemo = new Map() // Reset memoization
363
343
  const newLegendSpecialClassLastMemo = new Map() // Reset bin memoization
364
344
  let primaryCol = obj.columns.primary.name,
@@ -373,24 +353,10 @@ const CdcMap = ({
373
353
  result.fromHash = hash
374
354
  }
375
355
 
376
- result.runtimeDataHash = runtimeData.fromHash
356
+ result.runtimeDataHash = runtimeFilters?.fromHash
377
357
 
378
- // Unified will based the legend off ALL of the data maps received. Otherwise, it will use
358
+ // Unified will base the legend off ALL of the data maps received. Otherwise, it will use
379
359
  let dataSet = obj.legend.unified ? obj.data : Object.values(runtimeData)
380
-
381
- const colorDistributions = {
382
- 1: [1],
383
- 2: [1, 3],
384
- 3: [1, 3, 5],
385
- 4: [0, 2, 4, 6],
386
- 5: [0, 2, 4, 6, 7],
387
- 6: [0, 2, 3, 4, 5, 7],
388
- 7: [0, 2, 3, 4, 5, 6, 7],
389
- 8: [0, 2, 3, 4, 5, 6, 7, 8],
390
- 9: [0, 1, 2, 3, 4, 5, 6, 7, 8],
391
- 10: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
392
- }
393
-
394
360
  let specialClasses = 0
395
361
  let specialClassesHash = {}
396
362
 
@@ -416,7 +382,7 @@ const CdcMap = ({
416
382
  specialClasses += 1
417
383
  }
418
384
 
419
- let specialColor = ''
385
+ let specialColor: number
420
386
 
421
387
  // color the state if val is in row
422
388
  specialColor = result.findIndex(p => p.value === val)
@@ -648,7 +614,6 @@ const CdcMap = ({
648
614
  }
649
615
  } else {
650
616
  // get nums
651
- let hasZeroInData = dataSet.filter(obj => obj[state.columns.primary.name] === 0).length > 0
652
617
  let domainNums = new Set(dataSet.map(item => item[state.columns.primary.name]))
653
618
 
654
619
  domainNums = d3.extent(domainNums)
@@ -683,7 +648,7 @@ const CdcMap = ({
683
648
 
684
649
  const breaks = getBreaks(scale)
685
650
 
686
- // if seperating zero force it into breaks
651
+ // if separating zero force it into breaks
687
652
  if (breaks[0] !== 0) {
688
653
  breaks.unshift(0)
689
654
  }
@@ -698,7 +663,7 @@ const CdcMap = ({
698
663
  min = 0
699
664
  }
700
665
 
701
- // if we're on the second break, and seperating out zero, increment min to 1.
666
+ // if we're on the second break, and separating out zero, increment min to 1.
702
667
  if (index === 1 && state.legend.separateZero) {
703
668
  min = 1
704
669
  }
@@ -714,20 +679,12 @@ const CdcMap = ({
714
679
  return Math.pow(10, -n)
715
680
  }
716
681
 
717
- const setMax = (index, min) => {
682
+ const setMax = index => {
718
683
  let max = Number(breaks[index + 1]) - getDecimalPlace(Number(state?.columns?.primary?.roundToPlace))
719
684
 
720
- // check if min and max range are the same
721
- // if (min === max + 1) {
722
- // max = breaks[index + 1]
723
- // }
724
-
725
685
  if (index === 0 && state.legend.separateZero) {
726
686
  max = 0
727
687
  }
728
- // if ((index === state.legend.specialClasses.length && state.legend.specialClasses.length !== 0) && !state.legend.separateZero && hasZeroInData) {
729
- // max = 0;
730
- // }
731
688
 
732
689
  if (index + 1 === breaks.length) {
733
690
  max = domainNums[1]
@@ -745,7 +702,7 @@ const CdcMap = ({
745
702
  color: scale(item)
746
703
  })
747
704
 
748
- dataSet.forEach((row, dataIndex) => {
705
+ dataSet.forEach(row => {
749
706
  let number = row[state.columns.primary.name]
750
707
  let updated = result.length - 1
751
708
 
@@ -810,7 +767,7 @@ const CdcMap = ({
810
767
  legendMemo.current = newLegendMemo
811
768
 
812
769
  if (state.general.geoType === 'world') {
813
- const runtimeDataKeys = Object.keys(runtimeData)
770
+ const runtimeDataKeys = Object.keys(runtimeFilters)
814
771
  const isCountriesWithNoDataState =
815
772
  obj.data === undefined ? false : !countryKeys.every(countryKey => runtimeDataKeys.includes(countryKey))
816
773
 
@@ -875,32 +832,21 @@ const CdcMap = ({
875
832
  ) => {
876
833
  let newFilter = runtimeFilters[idx]
877
834
 
878
- const sortAsc = (a, b) => {
879
- return a.toString().localeCompare(b.toString(), 'en', { numeric: true })
880
- }
881
-
882
- const sortDesc = (a, b) => {
883
- return b.toString().localeCompare(a.toString(), 'en', { numeric: true })
835
+ const sort = (a, b) => {
836
+ const asc = obj.filters[idx].order !== 'desc'
837
+ return String(asc ? a : b).localeCompare(String(asc ? b : a), 'en', { numeric: true })
884
838
  }
885
839
 
886
840
  if (type !== 'url') {
887
841
  values = getUniqueValues(state.data, columnName)
888
842
 
889
- if (obj.filters[idx].order === 'asc') {
890
- values = values.sort(sortAsc)
891
- }
892
-
893
- if (obj.filters[idx].order === 'desc') {
894
- values = values.sort(sortDesc)
895
- }
896
-
897
843
  if (obj.filters[idx].order === 'cust') {
898
844
  if (obj.filters[idx]?.values.length > 0) {
899
845
  values = obj.filters[idx].values
900
846
  }
847
+ } else {
848
+ values = values.sort(sort)
901
849
  }
902
- } else {
903
- values = values
904
850
  }
905
851
 
906
852
  if (undefined === newFilter) {
@@ -920,6 +866,7 @@ const CdcMap = ({
920
866
  newFilter.active = active ?? values[0] // Default to first found value
921
867
  newFilter.filterStyle = obj.filters[idx].filterStyle ? obj.filters[idx].filterStyle : 'dropdown'
922
868
  newFilter.showDropdown = showDropdown
869
+ newFilter.subGrouping = obj.filters[idx].subGrouping
923
870
 
924
871
  filters.push(newFilter)
925
872
  }
@@ -960,7 +907,7 @@ const CdcMap = ({
960
907
  // Strip hidden characters before we check length
961
908
  navigateUrl = navigateUrl.replace(/(\r\n|\n|\r)/gm, '')
962
909
  }
963
- if (0 === navigateUrl.length) {
910
+ if (0 === navigateUrl?.length) {
964
911
  return false
965
912
  }
966
913
  }
@@ -968,103 +915,31 @@ const CdcMap = ({
968
915
  // Filters
969
916
  if (filters?.length) {
970
917
  for (let i = 0; i < filters.length; i++) {
971
- const { columnName, active, type } = filters[i]
972
- if (type !== 'url' && String(row[columnName]) !== String(active)) return false // Bail out, not part of filter
918
+ const { columnName, active, type, filterStyle, subGrouping } = filters[i]
919
+ const isDataFilter = type !== 'url'
920
+ const matchingValue = String(active) === String(row[columnName]) // Group
921
+ if (isDataFilter && !matchingValue) return false // Bail out, data doesn't match the filter selection
922
+ if (filterStyle == 'nested-dropdown') {
923
+ const matchingSubValue = String(row[subGrouping?.columnName]) === String(subGrouping?.active)
924
+ if (subGrouping?.active && !matchingSubValue) {
925
+ return false // Bail out, data doesn't match the subgroup selection
926
+ }
927
+ }
973
928
  }
974
929
  }
975
-
976
930
  // Don't add additional rows with same UID
977
- if (undefined === result[row.uid]) {
931
+ if (result[row.uid] === undefined) {
978
932
  result[row.uid] = row
979
933
  }
980
934
  })
981
-
982
935
  return result
983
936
  } catch (e) {
984
937
  console.error('COVE: ', e) // eslint-disable-line
985
938
  }
986
939
  })
987
940
 
988
- const outerContainerRef = useCallback(node => {
989
- if (node !== null) {
990
- resizeObserver.observe(node)
991
- }
992
- setContainer(node)
993
- }, []) // eslint-disable-line
994
-
995
941
  const mapSvg = useRef(null)
996
942
 
997
- const closeModal = ({ target }) => {
998
- if (
999
- 'string' === typeof target.className &&
1000
- (target.className.includes('modal-close') || target.className.includes('modal-background')) &&
1001
- null !== modal
1002
- ) {
1003
- setModal(null)
1004
- }
1005
- }
1006
-
1007
- const displayDataAsText = (value, columnName) => {
1008
- if (value === null || value === '' || value === undefined) {
1009
- return ''
1010
- }
1011
-
1012
- // if string of letters like 'Home' then dont need to format as a number
1013
- if (
1014
- typeof value === 'string' &&
1015
- value.length > 0 &&
1016
- /[a-zA-Z]/.test(value) &&
1017
- state.legend.type === 'equalnumber'
1018
- ) {
1019
- return value
1020
- }
1021
-
1022
- let formattedValue = value
1023
-
1024
- let columnObj = state.columns[columnName]
1025
-
1026
- if (columnObj === undefined) {
1027
- // then use left axis config
1028
- columnObj = state.columns.primary
1029
- // NOTE: Left Value Axis uses different names
1030
- // so map them below so the code below works
1031
- // - copy commas to useCommas to work below
1032
- columnObj['useCommas'] = columnObj.commas
1033
- // - copy roundTo to roundToPlace to work below
1034
- columnObj['roundToPlace'] = columnObj.roundTo ? columnObj.roundTo : ''
1035
- }
1036
-
1037
- if (columnObj) {
1038
- // If value is a number, apply specific formattings
1039
- if (Number(value)) {
1040
- const hasDecimal = columnObj.roundToPlace && (columnObj.roundToPlace !== '' || columnObj.roundToPlace !== null)
1041
- const decimalPoint = columnObj.roundToPlace ? Number(columnObj.roundToPlace) : 0
1042
-
1043
- // Rounding
1044
- if (columnObj.hasOwnProperty('roundToPlace') && hasDecimal) {
1045
- formattedValue = Number(value).toFixed(decimalPoint)
1046
- }
1047
-
1048
- if (columnObj.hasOwnProperty('useCommas') && columnObj.useCommas === true) {
1049
- // Formats number to string with commas - allows up to 5 decimal places, if rounding is not defined.
1050
- // Otherwise, uses the rounding value set at 'columnObj.roundToPlace'.
1051
- formattedValue = Number(value).toLocaleString('en-US', {
1052
- style: 'decimal',
1053
- minimumFractionDigits: hasDecimal ? decimalPoint : 0,
1054
- maximumFractionDigits: hasDecimal ? decimalPoint : 5
1055
- })
1056
- }
1057
- }
1058
-
1059
- // Check if it's a special value. If it is not, apply the designated prefix and suffix
1060
- if (false === state.legend.specialClasses.includes(String(value))) {
1061
- formattedValue = (columnObj.prefix || '') + formattedValue + (columnObj.suffix || '')
1062
- }
1063
- }
1064
-
1065
- return formattedValue
1066
- }
1067
-
1068
943
  // this is passed DOWN into the various components
1069
944
  // then they do a lookup based on the bin number as index into here
1070
945
  const applyLegendToRow = rowObj => {
@@ -1101,50 +976,6 @@ const CdcMap = ({
1101
976
  }
1102
977
  }
1103
978
 
1104
- // if city has a hyphen then in tooltip it ends up UPPER CASE instead of just regular Upper Case
1105
- // - this function is used to prevent that and instead give the formatting that is wanted
1106
- // Example: Desired city display in tooltip on map: "Inter-Tribal Indian Reservation"
1107
- const titleCase = string => {
1108
- // guard clause else error in editor
1109
- if (!string) return
1110
- if (string !== undefined) {
1111
- const toTitleCase = word => word.charAt(0).toUpperCase() + word.substring(1).toLowerCase()
1112
-
1113
- if (string.toUpperCase().includes('U.S.') || string.toUpperCase().includes('US')) {
1114
- return string
1115
- .split(' ')
1116
- .map(word => {
1117
- if (word.toUpperCase() === 'U.S.' || word.toUpperCase() === 'US') {
1118
- return word.toUpperCase()
1119
- } else {
1120
- return toTitleCase(word)
1121
- }
1122
- })
1123
- .join(' ')
1124
- }
1125
- // if hyphen found, then split, uppercase each word, and put back together
1126
- if (string.includes('–') || string.includes('-')) {
1127
- let dashSplit = string.includes('–') ? string.split('–') : string.split('-') // determine hyphen or en dash to split on
1128
- let splitCharacter = string.includes('–') ? '–' : '-' // print hyphen or en dash later on.
1129
- let frontSplit = dashSplit[0]
1130
- .split(' ')
1131
- .map(word => word.charAt(0).toUpperCase() + word.substring(1).toLowerCase())
1132
- .join(' ')
1133
- let backSplit = dashSplit[1]
1134
- .split(' ')
1135
- .map(word => word.charAt(0).toUpperCase() + word.substring(1).toLowerCase())
1136
- .join(' ')
1137
- return frontSplit + splitCharacter + backSplit
1138
- } else {
1139
- // just return with each word uppercase
1140
- return string
1141
- .split(' ')
1142
- .map(word => word.charAt(0).toUpperCase() + word.substring(1).toLowerCase())
1143
- .join(' ')
1144
- }
1145
- }
1146
- }
1147
-
1148
979
  // This resets all active legend toggles.
1149
980
  const resetLegendToggles = async () => {
1150
981
  let newLegend = [...runtimeLegend]
@@ -1217,7 +1048,7 @@ const CdcMap = ({
1217
1048
  value = dict[value]
1218
1049
  }
1219
1050
 
1220
- // if you get here and it's 2 letters then DONT titleCase state abbreviations like "AL"
1051
+ // if you get here and it's 2 letters then dont titleCase state abbreviations like "AL"
1221
1052
  if (value.length === 2) {
1222
1053
  return value
1223
1054
  } else {
@@ -1226,7 +1057,7 @@ const CdcMap = ({
1226
1057
  }
1227
1058
 
1228
1059
  // todo: convert to store or context eventually.
1229
- const { buildTooltip } = useTooltip({ state, displayGeoName, displayDataAsText, supportedStatesFipsCodes })
1060
+ const { buildTooltip } = useTooltip({ state, displayGeoName, supportedStatesFipsCodes })
1230
1061
 
1231
1062
  const applyTooltipsToGeo = (geoName, row, returnType = 'string') => {
1232
1063
  let toolTipText = buildTooltip(row, geoName, '')
@@ -1244,11 +1075,15 @@ const CdcMap = ({
1244
1075
  key='modal-navigation-link'
1245
1076
  onClick={e => {
1246
1077
  e.preventDefault()
1247
- navigationHandler(row[state.columns.navigate.name])
1078
+ navigationHandler(
1079
+ state.general.navigationTarget,
1080
+ row[state.columns.navigate.name],
1081
+ customNavigationHandler
1082
+ )
1248
1083
  }}
1249
1084
  >
1250
1085
  {state.tooltips.linkLabel}
1251
- {isDomainExternal(row[state.columns.navigate.name]) && <ExternalIcon className='inline-icon ml-1' />}
1086
+ {isDomainExternal(row[state.columns.navigate.name]) && <ExternalIcon className='inline-icon ms-1' />}
1252
1087
  </a>
1253
1088
  )
1254
1089
  }
@@ -1257,24 +1092,6 @@ const CdcMap = ({
1257
1092
  return toolTipText
1258
1093
  }
1259
1094
 
1260
- const navigationHandler = urlString => {
1261
- // Call custom navigation method if passed
1262
- if (customNavigationHandler) {
1263
- customNavigationHandler(urlString)
1264
- return
1265
- }
1266
-
1267
- // Abort if value is blank
1268
- if (0 === urlString.length) {
1269
- throw Error('Blank string passed as URL. Navigation aborted.')
1270
- }
1271
-
1272
- const urlObj = new URL(urlString, window.location.origin)
1273
-
1274
- // Open constructed link in new tab/window
1275
- window.open(urlObj.toString(), '_blank')
1276
- }
1277
-
1278
1095
  const geoClickHandler = (key, value) => {
1279
1096
  if (setSharedFilter) {
1280
1097
  setSharedFilter(state.uid, value)
@@ -1291,7 +1108,7 @@ const CdcMap = ({
1291
1108
  })
1292
1109
  }
1293
1110
 
1294
- // If modals are set or we are on a mobile viewport, display modal
1111
+ // If modals are set, or we are on a mobile viewport, display modal
1295
1112
  if (window.matchMedia('(any-hover: none)').matches || 'click' === state.tooltips.appearanceType) {
1296
1113
  setModal({
1297
1114
  geoName: key,
@@ -1303,64 +1120,7 @@ const CdcMap = ({
1303
1120
 
1304
1121
  // Otherwise if this item has a link specified for it, do regular navigation.
1305
1122
  if (state.columns.navigate && value[state.columns.navigate.name]) {
1306
- navigationHandler(value[state.columns.navigate.name])
1307
- }
1308
- }
1309
-
1310
- const validateFipsCodeLength = newState => {
1311
- if (
1312
- newState.general.geoType === 'us-county' ||
1313
- newState.general.geoType === 'single-state' ||
1314
- (newState.general.geoType === 'us' && newState?.data)
1315
- ) {
1316
- newState?.data.forEach(dataPiece => {
1317
- if (dataPiece[newState.columns.geo.name]) {
1318
- if (
1319
- !isNaN(parseInt(dataPiece[newState.columns.geo.name])) &&
1320
- dataPiece[newState.columns.geo.name].length === 4
1321
- ) {
1322
- dataPiece[newState.columns.geo.name] = 0 + dataPiece[newState.columns.geo.name]
1323
- }
1324
- dataPiece[newState.columns.geo.name] = dataPiece[newState.columns.geo.name].toString()
1325
- }
1326
- })
1327
- }
1328
- return newState
1329
- }
1330
-
1331
- const handleMapAriaLabels = (state = '', testing = false) => {
1332
- if (testing) console.log(`handleMapAriaLabels Testing On: ${state}`) // eslint-disable-line
1333
- try {
1334
- if (!state.general.geoType) throw Error('handleMapAriaLabels: no geoType found in state')
1335
- let ariaLabel = ''
1336
- switch (state.general.geoType) {
1337
- case 'world':
1338
- ariaLabel += 'World map'
1339
- break
1340
- case 'us':
1341
- ariaLabel += 'United States map'
1342
- break
1343
- case 'us-county':
1344
- ariaLabel += `United States county map`
1345
- break
1346
- case 'single-state':
1347
- ariaLabel += `${state.general.statePicked.stateName} county map`
1348
- break
1349
- case 'us-region':
1350
- ariaLabel += `United States HHS Region map`
1351
- break
1352
- default:
1353
- ariaLabel = 'Data visualization container'
1354
- break
1355
- }
1356
-
1357
- if (state.general.title) {
1358
- ariaLabel += ` with the title: ${state.general.title}`
1359
- }
1360
-
1361
- return ariaLabel
1362
- } catch (e) {
1363
- console.error('COVE: ', e.message) // eslint-disable-line
1123
+ navigationHandler(state.general.navigationTarget, value[state.columns.navigate.name], customNavigationHandler)
1364
1124
  }
1365
1125
  }
1366
1126
 
@@ -1464,7 +1224,7 @@ const CdcMap = ({
1464
1224
 
1465
1225
  // This code goes through and adds the defaults for every property declaring in the initial state at the top.
1466
1226
  // This allows you to easily add new properties to the config without having to worry about accounting for backwards compatibility.
1467
- // Right now this does not work recursively -- only on first and second level properties. So state -> prop1 -> childprop1
1227
+ // Right now this does not work recursively -- only on first and second level properties. So state -> prop1 -> childPropOne
1468
1228
  Object.keys(newState).forEach(key => {
1469
1229
  if ('object' === typeof newState[key] && false === Array.isArray(newState[key])) {
1470
1230
  if (initialState[key]) {
@@ -1535,25 +1295,6 @@ const CdcMap = ({
1535
1295
  }
1536
1296
  }, [state]) // eslint-disable-line
1537
1297
 
1538
- // DEV-769 make "Data Table" both a required field and default value
1539
- useEffect(() => {
1540
- if (state.table?.label === '' || state.table?.label === undefined) {
1541
- setState({
1542
- ...state,
1543
- table: {
1544
- ...state.table,
1545
- title: 'Data Table'
1546
- }
1547
- })
1548
- }
1549
- }, [state.table]) // eslint-disable-line
1550
-
1551
- // When geo label override changes
1552
- // - redo the tooltips
1553
- useEffect(() => {
1554
- applyTooltipsToGeo()
1555
- }, [state.general.geoLabelOverride]) // eslint-disable-line
1556
-
1557
1298
  useEffect(() => {
1558
1299
  // UID
1559
1300
  if (state.data && state.columns.geo.name && state.columns.geo.name !== state.data.fromColumn) {
@@ -1564,7 +1305,7 @@ const CdcMap = ({
1564
1305
  const hashFilters = hashObj(state.filters)
1565
1306
  let filters
1566
1307
 
1567
- if (state.filters && hashFilters !== runtimeFilters.fromHash) {
1308
+ if (state.filters && (config || hashFilters !== runtimeFilters.fromHash)) {
1568
1309
  filters = generateRuntimeFilters(state, hashFilters, runtimeFilters)
1569
1310
 
1570
1311
  if (filters) {
@@ -1593,12 +1334,12 @@ const CdcMap = ({
1593
1334
  })
1594
1335
 
1595
1336
  // Data
1596
- if (hashData !== runtimeData.fromHash && state.data?.fromColumn) {
1337
+ if (hashData !== runtimeData?.fromHash && state.data?.fromColumn) {
1597
1338
  const newRuntimeData = generateRuntimeData(state, filters || runtimeFilters, hashData)
1598
1339
 
1599
1340
  setRuntimeData(newRuntimeData)
1600
1341
  } else {
1601
- if (hashLegend !== runtimeLegend.fromHash && undefined === runtimeData.init) {
1342
+ if (hashLegend !== runtimeLegend?.fromHash && undefined === runtimeData.init) {
1602
1343
  const legend = generateRuntimeLegend(state, runtimeData, hashLegend)
1603
1344
  setRuntimeLegend(legend)
1604
1345
  }
@@ -1674,21 +1415,14 @@ const CdcMap = ({
1674
1415
  handleDragStateChange,
1675
1416
  applyLegendToRow,
1676
1417
  applyTooltipsToGeo,
1677
- capitalize: state.tooltips?.capitalizeLabels,
1678
- closeModal,
1679
- columnsInData: state?.data?.[0] ? Object.keys(state.data[0]) : [],
1680
1418
  container,
1681
1419
  content: modal,
1682
- currentViewport,
1683
1420
  data: runtimeData,
1684
- dimensions,
1685
- displayDataAsText,
1686
1421
  displayGeoName,
1687
1422
  filteredCountryCode,
1688
1423
  generateColorsArray,
1689
1424
  generateRuntimeData,
1690
1425
  geoClickHandler,
1691
- handleMapAriaLabels,
1692
1426
  hasZoom: state.general.allowMapZoom,
1693
1427
  innerContainerRef,
1694
1428
  isDashboard,
@@ -1696,7 +1430,6 @@ const CdcMap = ({
1696
1430
  isEditor,
1697
1431
  loadConfig,
1698
1432
  logo,
1699
- navigationHandler,
1700
1433
  position,
1701
1434
  resetLegendToggles,
1702
1435
  runtimeFilters,
@@ -1712,18 +1445,14 @@ const CdcMap = ({
1712
1445
  setSharedFilterValue,
1713
1446
  setState,
1714
1447
  state,
1715
- supportedCities,
1716
- supportedCounties,
1717
- supportedCountries,
1718
- supportedTerritories,
1719
- titleCase,
1720
- type: general.type,
1721
- viewport: currentViewport,
1722
1448
  tooltipId,
1723
1449
  tooltipRef,
1724
1450
  topoData,
1725
1451
  setTopoData,
1726
- mapId
1452
+ mapId,
1453
+ outerContainerRef,
1454
+ dimensions,
1455
+ currentViewport
1727
1456
  }
1728
1457
 
1729
1458
  if (!mapProps.data || !state.data) return <></>
@@ -1766,7 +1495,12 @@ const CdcMap = ({
1766
1495
  )
1767
1496
 
1768
1497
  const sectionClassNames = () => {
1769
- const classes = ['cove-component__content', 'cdc-map-inner-container', `${currentViewport}`]
1498
+ const classes = [
1499
+ 'cove-component__content',
1500
+ 'cdc-map-inner-container',
1501
+ `${currentViewport}`,
1502
+ `${state?.general?.headerColor}`
1503
+ ]
1770
1504
  if (config?.runtime?.editorErrorMessage.length > 0) classes.push('type-map--has-error')
1771
1505
  return classes.join(' ')
1772
1506
  }
@@ -1800,20 +1534,16 @@ const CdcMap = ({
1800
1534
  <SkipTo skipId={tabId} skipMessage={`Skip over annotations`} key={`skip-annotations`} />
1801
1535
  )}
1802
1536
 
1803
- {general.introText && (
1804
- <section className='introText' style={{ padding: '15px', margin: '0px' }}>
1805
- {parse(general.introText)}
1806
- </section>
1807
- )}
1537
+ {general.introText && <section className='introText mb-4'>{parse(general.introText)}</section>}
1808
1538
 
1809
1539
  {state?.filters?.length > 0 && (
1810
1540
  <Filters
1811
1541
  config={state}
1812
1542
  setConfig={setState}
1813
- getUniqueValues={getUniqueValues}
1814
1543
  filteredData={runtimeFilters}
1815
- setFilteredData={setRuntimeFilters}
1544
+ setFilteredData={_setRuntimeData}
1816
1545
  dimensions={dimensions}
1546
+ standaloneMap={!config}
1817
1547
  />
1818
1548
  )}
1819
1549
 
@@ -1821,10 +1551,10 @@ const CdcMap = ({
1821
1551
  role='region'
1822
1552
  tabIndex='0'
1823
1553
  className={mapContainerClasses.join(' ')}
1824
- onClick={e => closeModal(e)}
1554
+ onClick={e => closeModal(e, modal, setModal)}
1825
1555
  onKeyDown={e => {
1826
- if (e.keyCode === 13) {
1827
- closeModal(e)
1556
+ if (e.key === 'Enter') {
1557
+ closeModal(e, modal, setModal)
1828
1558
  }
1829
1559
  }}
1830
1560
  style={{ padding: '15px 0px', margin: '0px' }}
@@ -1840,6 +1570,7 @@ const CdcMap = ({
1840
1570
  {'us-county' === geoType && <UsaMap.County />}
1841
1571
  {'world' === geoType && <WorldMap />}
1842
1572
  {/* logo is handled in UsaMap.State when applicable */}
1573
+ {'google-map' === geoType && <GoogleMap />}
1843
1574
  {'data' === general.type && logo && ('us' !== geoType || 'us-geocode' === state.general.type) && (
1844
1575
  <img src={logo} alt='' className='map-logo' style={{ maxWidth: '50px' }} />
1845
1576
  )}
@@ -1865,14 +1596,16 @@ const CdcMap = ({
1865
1596
  data={runtimeData}
1866
1597
  options={general}
1867
1598
  columns={state.columns}
1868
- navigationHandler={val => navigationHandler(val)}
1599
+ navigationHandler={val =>
1600
+ navigationHandler(state.general.navigationBehavior, val, customNavigationHandler)
1601
+ }
1869
1602
  />
1870
1603
  )}
1871
1604
 
1872
1605
  {/* Link */}
1873
1606
  {isDashboard && config.table?.forceDisplay && config.table.showDataTableLink ? tableLink : link && link}
1874
1607
 
1875
- {subtext.length > 0 && <p className='subtext'>{parse(subtext)}</p>}
1608
+ {subtext.length > 0 && <p className='subtext mt-4'>{parse(subtext)}</p>}
1876
1609
 
1877
1610
  <MediaControls.Section classes={['download-buttons']}>
1878
1611
  {state.general.showDownloadImgButton && (
@@ -1910,7 +1643,6 @@ const CdcMap = ({
1910
1643
  showFullGeoNameInCSV={table.showFullGeoNameInCSV}
1911
1644
  runtimeLegend={runtimeLegend}
1912
1645
  runtimeData={runtimeData}
1913
- displayDataAsText={displayDataAsText}
1914
1646
  displayGeoName={displayGeoName}
1915
1647
  applyLegendToRow={applyLegendToRow}
1916
1648
  tableTitle={table.label}
@@ -1932,7 +1664,7 @@ const CdcMap = ({
1932
1664
 
1933
1665
  {state.annotations.length > 0 && <Annotation.Dropdown />}
1934
1666
 
1935
- {general.footnotes && <section className='footnotes'>{parse(general.footnotes)}</section>}
1667
+ {general.footnotes && <section className='footnotes pt-2 mt-4'>{parse(general.footnotes)}</section>}
1936
1668
  </section>
1937
1669
  )}
1938
1670