@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
package/src/CdcMap.tsx CHANGED
@@ -2,7 +2,13 @@ import React, { useState, useEffect, useRef, useCallback, useId } from 'react'
2
2
  import * as d3 from 'd3'
3
3
  import Layout from '@cdc/core/components/Layout'
4
4
  import Waiting from '@cdc/core/components/Waiting'
5
+ import Annotation from './components/Annotation'
5
6
  import Error from './components/EditorPanel/components/Error'
7
+ import _ from 'lodash'
8
+
9
+ // types
10
+ import { type ViewportSize } from './types/MapConfig'
11
+ import { type DimensionsType } from '@cdc/core/types/Dimensions'
6
12
 
7
13
  // IE11
8
14
  import 'whatwg-fetch'
@@ -16,14 +22,28 @@ import parse from 'html-react-parser'
16
22
  import 'react-tooltip/dist/react-tooltip.css'
17
23
 
18
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'
19
29
  import { publish } from '@cdc/core/helpers/events'
20
30
  import coveUpdateWorker from '@cdc/core/helpers/coveUpdateWorker'
21
31
  import { getQueryStringFilterValue } from '@cdc/core/helpers/queryStringUtils'
22
32
  import Title from '@cdc/core/components/ui/Title'
33
+ import { getTextWidth } from '@cdc/core/helpers/getTextWidth'
23
34
 
24
35
  // Data
25
36
  import { countryCoordinates } from './data/country-coordinates'
26
- import { supportedStates, supportedTerritories, supportedCountries, supportedCounties, supportedCities, supportedStatesFipsCodes, stateFipsToTwoDigit, supportedRegions } from './data/supported-geos'
37
+ import {
38
+ supportedStates,
39
+ supportedTerritories,
40
+ supportedCountries,
41
+ supportedCounties,
42
+ supportedCities,
43
+ supportedStatesFipsCodes,
44
+ stateFipsToTwoDigit,
45
+ supportedRegions
46
+ } from './data/supported-geos'
27
47
  import colorPalettes from '@cdc/core/data/colorPalettes'
28
48
  import initialState from './data/initial-state'
29
49
 
@@ -42,7 +62,6 @@ import MediaControls from '@cdc/core/components/MediaControls'
42
62
  import fetchRemoteData from '@cdc/core/helpers/fetchRemoteData'
43
63
  import getViewport from '@cdc/core/helpers/getViewport'
44
64
  import isDomainExternal from '@cdc/core/helpers/isDomainExternal'
45
- import Loading from '@cdc/core/components/Loading'
46
65
  import numberFromString from '@cdc/core/helpers/numberFromString'
47
66
  import DataTable from '@cdc/core/components/DataTable' // Future: Lazy
48
67
 
@@ -68,34 +87,6 @@ const countryKeys = Object.keys(supportedCountries)
68
87
  const countyKeys = Object.keys(supportedCounties)
69
88
  const cityKeys = Object.keys(supportedCities)
70
89
 
71
- const generateColorsArray = (color = '#000000', special = false) => {
72
- let colorObj = chroma(color)
73
- let hoverColor = special ? colorObj.brighten(0.5).hex() : colorObj.saturate(1.3).hex()
74
-
75
- return [color, hoverColor, colorObj.darken(0.3).hex()]
76
- }
77
-
78
- const hashObj = row => {
79
- try {
80
- if (!row) throw new Error('No row supplied to hashObj')
81
-
82
- let str = JSON.stringify(row)
83
- let hash = 0
84
-
85
- if (str.length === 0) return hash
86
-
87
- for (let i = 0; i < str.length; i++) {
88
- let char = str.charCodeAt(i)
89
- hash = (hash << 5) - hash + char
90
- hash = hash & hash
91
- }
92
-
93
- return hash
94
- } catch (e) {
95
- console.error('COVE: ', e) // eslint-disable-line
96
- }
97
- }
98
-
99
90
  const indexOfIgnoreType = (arr, item) => {
100
91
  for (let i = 0; i < arr.length; i++) {
101
92
  if (item === arr[i]) {
@@ -105,32 +96,33 @@ const indexOfIgnoreType = (arr, item) => {
105
96
  return -1
106
97
  }
107
98
 
108
- // returns string[]
109
- const getUniqueValues = (data, columnName) => {
110
- let result = {}
111
-
112
- for (let i = 0; i < data.length; i++) {
113
- let val = data[i][columnName]
114
-
115
- if (undefined === val) continue
116
-
117
- if (undefined === result[val]) {
118
- result[val] = true
119
- }
120
- }
121
-
122
- return Object.keys(result)
123
- }
124
-
125
- const CdcMap = ({ className, config, navigationHandler: customNavigationHandler, isDashboard = false, isEditor = false, isDebug = false, configUrl, logo = '', setConfig, setSharedFilter, setSharedFilterValue, link }) => {
99
+ const CdcMap = ({
100
+ className,
101
+ config,
102
+ navigationHandler: customNavigationHandler,
103
+ isDashboard = false,
104
+ isEditor = false,
105
+ isDebug = false,
106
+ configUrl,
107
+ logo = '',
108
+ setConfig,
109
+ setSharedFilter,
110
+ setSharedFilterValue,
111
+ link
112
+ }) => {
126
113
  const transform = new DataTransform()
114
+ const [translate, setTranslate] = useState([0, 0])
115
+ const [scale, setScale] = useState(1)
127
116
  const [state, setState] = useState({ ...initialState })
117
+ const [isDraggingAnnotation, setIsDraggingAnnotation] = useState(false)
128
118
  const [loading, setLoading] = useState(true)
129
119
  const [displayPanel, setDisplayPanel] = useState(true)
130
- const [currentViewport, setCurrentViewport] = useState()
120
+ const [currentViewport, setCurrentViewport] = useState<ViewportSize>('lg')
121
+ const [topoData, setTopoData] = useState<Topology | {}>({})
131
122
  const [runtimeFilters, setRuntimeFilters] = useState([])
132
123
  const [runtimeLegend, setRuntimeLegend] = useState([])
133
124
  const [runtimeData, setRuntimeData] = useState({ init: true })
125
+ const [stateToShow, setStateToShow] = useState(null)
134
126
  const [modal, setModal] = useState(null)
135
127
  const [accessibleStatus, setAccessibleStatus] = useState('')
136
128
  const [filteredCountryCode, setFilteredCountryCode] = useState()
@@ -138,12 +130,15 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
138
130
  const [coveLoadedHasRan, setCoveLoadedHasRan] = useState(false)
139
131
  const [container, setContainer] = useState()
140
132
  const [imageId, setImageId] = useState(`cove-${Math.random().toString(16).slice(-4)}`) // eslint-disable-line
141
- const [dimensions, setDimensions] = useState()
133
+ const [dimensions, setDimensions] = useState<DimensionsType>([0, 0])
142
134
  const [requiredColumns, setRequiredColumns] = useState(null) // Simple state so we know if we need more information before parsing the map
135
+ const [projection, setProjection] = useState(null)
143
136
 
144
137
  const legendRef = useRef(null)
138
+ const tooltipRef = useRef(null)
145
139
  const legendId = useId()
146
140
  const tooltipId = useId()
141
+ const mapId = useId()
147
142
 
148
143
  const { changeFilterActive, handleSorting } = useFilters({ config: state, setConfig: setState })
149
144
  let legendMemo = useRef(new Map())
@@ -152,6 +147,10 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
152
147
 
153
148
  if (isDebug) console.log('CdcMap state=', state) // <eslint-disable-line></eslint-disable-line>
154
149
 
150
+ const handleDragStateChange = isDragging => {
151
+ setIsDraggingAnnotation(isDragging)
152
+ }
153
+
155
154
  const columnsRequiredChecker = useCallback(() => {
156
155
  let columnList = []
157
156
 
@@ -166,15 +165,24 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
166
165
  }
167
166
 
168
167
  // Navigate is required for navigation maps
169
- if ('navigation' === state.general.type && ('' === state.columns.navigate.name || undefined === state.columns.navigate)) {
168
+ if (
169
+ 'navigation' === state.general.type &&
170
+ ('' === state.columns.navigate.name || undefined === state.columns.navigate)
171
+ ) {
170
172
  columnList.push('Navigation')
171
173
  }
172
174
 
173
- if (('us-geocode' === state.general.type || 'world-geocode' === state.general.type) && '' === state.columns.latitude.name) {
175
+ if (
176
+ ('us-geocode' === state.general.type || 'world-geocode' === state.general.type) &&
177
+ '' === state.columns.latitude.name
178
+ ) {
174
179
  columnList.push('Latitude')
175
180
  }
176
181
 
177
- if (('us-geocode' === state.general.type || 'world-geocode' === state.general.type) && '' === state.columns.longitude.name) {
182
+ if (
183
+ ('us-geocode' === state.general.type || 'world-geocode' === state.general.type) &&
184
+ '' === state.columns.longitude.name
185
+ ) {
178
186
  columnList.push('Longitude')
179
187
  }
180
188
 
@@ -219,32 +227,11 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
219
227
  }
220
228
  }, [state.mapPosition, setPosition])
221
229
 
222
- const generateRuntimeLegendHash = () => {
223
- return hashObj({
224
- unified: state.legend.unified ?? false,
225
- equalNumberOptIn: state.general.equalNumberOptIn ?? false,
226
- specialClassesLast: state.legend.showSpecialClassesLast ?? false,
227
- color: state.color,
228
- customColors: state.customColors,
229
- numberOfItems: state.legend.numberOfItems,
230
- type: state.legend.type,
231
- separateZero: state.legend.separateZero ?? false,
232
- primary: state.columns.primary.name,
233
- categoryValuesOrder: state.legend.categoryValuesOrder,
234
- specialClasses: state.legend.specialClasses,
235
- geoType: state.general.geoType,
236
- data: state.data,
237
- ...runtimeFilters,
238
- filters: {
239
- ...state.filters
240
- }
241
- })
242
- }
243
-
244
230
  const resizeObserver = new ResizeObserver(entries => {
245
231
  for (let entry of entries) {
246
232
  let { width, height } = entry.contentRect
247
233
  let newViewport = getViewport(entry.contentRect.width)
234
+
248
235
  let editorWidth = 350
249
236
 
250
237
  setCurrentViewport(newViewport)
@@ -260,7 +247,7 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
260
247
  // We are mutating state in place here (depending on where called) - but it's okay, this isn't used for rerender
261
248
  // eslint-disable-next-line
262
249
  const addUIDs = useCallback((obj, fromColumn) => {
263
- obj.data.forEach(row => {
250
+ obj.data.forEach((row, index) => {
264
251
  let uid = null
265
252
 
266
253
  if (row.uid) row.uid = null // Wipe existing UIDs
@@ -320,7 +307,10 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
320
307
  }
321
308
 
322
309
  // County Check
323
- if (('us-county' === obj.general.geoType || 'single-state' === obj.general.geoType) && 'us-geocode' !== obj.general.type) {
310
+ if (
311
+ ('us-county' === obj.general.geoType || 'single-state' === obj.general.geoType) &&
312
+ 'us-geocode' !== obj.general.type
313
+ ) {
324
314
  const fips = row[obj.columns.geo.name]
325
315
  uid = countyKeys.find(key => key === fips)
326
316
  }
@@ -329,8 +319,14 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
329
319
  uid = row[state.columns.geo.name]
330
320
  }
331
321
 
332
- if (!uid && state.columns.latitude?.name && state.columns.longitude?.name && row[state.columns.latitude?.name] && row[state.columns.longitude?.name]) {
333
- uid = row[state.columns.geo.name]
322
+ if (
323
+ !uid &&
324
+ state.columns.latitude?.name &&
325
+ state.columns.longitude?.name &&
326
+ row[state.columns.latitude?.name] &&
327
+ row[state.columns.longitude?.name]
328
+ ) {
329
+ uid = `${row[state.columns.geo.name]}`
334
330
  }
335
331
 
336
332
  if (uid) {
@@ -349,6 +345,7 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
349
345
  const newLegendMemo = new Map() // Reset memoization
350
346
  const newLegendSpecialClassLastMemo = new Map() // Reset bin memoization
351
347
  let primaryCol = obj.columns.primary.name,
348
+ isSingleState = obj.general.geoType === 'single-state',
352
349
  isBubble = obj.general.type === 'bubble',
353
350
  categoricalCol = obj.columns.categorical ? obj.columns.categorical.name : undefined,
354
351
  type = obj.legend.type,
@@ -680,14 +677,32 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
680
677
  let colors = colorPalettes[state.color]
681
678
  let colorRange = colors.slice(0, state.legend.numberOfItems)
682
679
 
680
+ const getDomain = () => {
681
+ // backwards compatibility
682
+ if (state?.columns?.primary?.roundToPlace !== undefined && state?.general?.equalNumberOptIn) {
683
+ return _.uniq(
684
+ dataSet.map(item =>
685
+ Number(item[state.columns.primary.name]).toFixed(Number(state?.columns?.primary?.roundToPlace))
686
+ )
687
+ )
688
+ }
689
+ return _.uniq(dataSet.map(item => Math.round(Number(item[state.columns.primary.name]))))
690
+ }
691
+
692
+ const getBreaks = scale => {
693
+ // backwards compatibility
694
+ if (state?.columns?.primary?.roundToPlace !== undefined && state?.general?.equalNumberOptIn) {
695
+ return scale.quantiles().map(b => Number(b)?.toFixed(Number(state?.columns?.primary?.roundToPlace)))
696
+ }
697
+ return scale.quantiles().map(item => Number(Math.round(item)))
698
+ }
699
+
683
700
  let scale = d3
684
701
  .scaleQuantile()
685
- .domain([...new Set(dataSet.map(item => Math.round(item[state.columns.primary.name])))]) // min/max values
702
+ .domain(getDomain()) // min/max values
686
703
  .range(colorRange) // set range to our colors array
687
704
 
688
- let breaks = scale.quantiles()
689
-
690
- breaks = breaks.map(item => Math.round(item))
705
+ const breaks = getBreaks(scale)
691
706
 
692
707
  // if seperating zero force it into breaks
693
708
  if (breaks[0] !== 0) {
@@ -716,8 +731,12 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
716
731
  return min
717
732
  }
718
733
 
734
+ const getDecimalPlace = n => {
735
+ return Math.pow(10, -n)
736
+ }
737
+
719
738
  const setMax = (index, min) => {
720
- let max = breaks[index + 1] - 1
739
+ let max = Number(breaks[index + 1]) - getDecimalPlace(Number(state?.columns?.primary?.roundToPlace))
721
740
 
722
741
  // check if min and max range are the same
723
742
  // if (min === max + 1) {
@@ -845,57 +864,73 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
845
864
 
846
865
  if (hash) filters.fromHash = hash
847
866
 
848
- obj?.filters.forEach(({ columnName, label, labels, queryParameter, orderedValues, active, values, type, showDropdown, setByQueryParameter }, idx) => {
849
- let newFilter = runtimeFilters[idx]
867
+ obj?.filters.forEach(
868
+ (
869
+ {
870
+ columnName,
871
+ label,
872
+ labels,
873
+ queryParameter,
874
+ orderedValues,
875
+ active,
876
+ values,
877
+ type,
878
+ showDropdown,
879
+ setByQueryParameter
880
+ },
881
+ idx
882
+ ) => {
883
+ let newFilter = runtimeFilters[idx]
884
+
885
+ const sortAsc = (a, b) => {
886
+ return a.toString().localeCompare(b.toString(), 'en', { numeric: true })
887
+ }
850
888
 
851
- const sortAsc = (a, b) => {
852
- return a.toString().localeCompare(b.toString(), 'en', { numeric: true })
853
- }
889
+ const sortDesc = (a, b) => {
890
+ return b.toString().localeCompare(a.toString(), 'en', { numeric: true })
891
+ }
854
892
 
855
- const sortDesc = (a, b) => {
856
- return b.toString().localeCompare(a.toString(), 'en', { numeric: true })
857
- }
893
+ if (type !== 'url') {
894
+ values = getUniqueValues(state.data, columnName)
858
895
 
859
- if (type !== 'url') {
860
- values = getUniqueValues(state.data, columnName)
896
+ if (obj.filters[idx].order === 'asc') {
897
+ values = values.sort(sortAsc)
898
+ }
861
899
 
862
- if (obj.filters[idx].order === 'asc') {
863
- values = values.sort(sortAsc)
864
- }
900
+ if (obj.filters[idx].order === 'desc') {
901
+ values = values.sort(sortDesc)
902
+ }
865
903
 
866
- if (obj.filters[idx].order === 'desc') {
867
- values = values.sort(sortDesc)
904
+ if (obj.filters[idx].order === 'cust') {
905
+ if (obj.filters[idx]?.values.length > 0) {
906
+ values = obj.filters[idx].values
907
+ }
908
+ }
909
+ } else {
910
+ values = values
868
911
  }
869
912
 
870
- if (obj.filters[idx].order === 'cust') {
871
- if (obj.filters[idx]?.values.length > 0) {
872
- values = obj.filters[idx].values
873
- }
913
+ if (undefined === newFilter) {
914
+ newFilter = {}
874
915
  }
875
- } else {
876
- values = values
877
- }
878
916
 
879
- if (undefined === newFilter) {
880
- newFilter = {}
917
+ newFilter.order = obj.filters[idx].order ? obj.filters[idx].order : 'asc'
918
+ newFilter.type = type
919
+ newFilter.label = label ?? ''
920
+ newFilter.columnName = columnName
921
+ newFilter.orderedValues = orderedValues
922
+ newFilter.queryParameter = queryParameter
923
+ newFilter.labels = labels
924
+ newFilter.values = values
925
+ newFilter.setByQueryParameter = setByQueryParameter
926
+ handleSorting(newFilter)
927
+ newFilter.active = active ?? values[0] // Default to first found value
928
+ newFilter.filterStyle = obj.filters[idx].filterStyle ? obj.filters[idx].filterStyle : 'dropdown'
929
+ newFilter.showDropdown = showDropdown
930
+
931
+ filters.push(newFilter)
881
932
  }
882
-
883
- newFilter.order = obj.filters[idx].order ? obj.filters[idx].order : 'asc'
884
- newFilter.type = type
885
- newFilter.label = label ?? ''
886
- newFilter.columnName = columnName
887
- newFilter.orderedValues = orderedValues
888
- newFilter.queryParameter = queryParameter
889
- newFilter.labels = labels
890
- newFilter.values = values
891
- newFilter.setByQueryParameter = setByQueryParameter
892
- handleSorting(newFilter)
893
- newFilter.active = active ?? values[0] // Default to first found value
894
- newFilter.filterStyle = obj.filters[idx].filterStyle ? obj.filters[idx].filterStyle : 'dropdown'
895
- newFilter.showDropdown = showDropdown
896
-
897
- filters.push(newFilter)
898
- })
933
+ )
899
934
 
900
935
  return filters
901
936
  })
@@ -920,11 +955,6 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
920
955
 
921
956
  if (undefined === row.uid) return false // No UID for this row, we can't use for mapping
922
957
 
923
- // When on a single state map filter runtime data by state
924
- if (!(String(row[obj.columns.geo.name]).substring(0, 2) === obj.general?.statePicked?.fipsCode) && obj.general.geoType === 'single-state' && obj.general.type !== 'us-geocode') {
925
- return false
926
- }
927
-
928
958
  if (row[obj.columns.primary.name]) {
929
959
  row[obj.columns.primary.name] = numberFromString(row[obj.columns.primary.name], state)
930
960
  }
@@ -972,7 +1002,11 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
972
1002
  const mapSvg = useRef(null)
973
1003
 
974
1004
  const closeModal = ({ target }) => {
975
- if ('string' === typeof target.className && (target.className.includes('modal-close') || target.className.includes('modal-background')) && null !== modal) {
1005
+ if (
1006
+ 'string' === typeof target.className &&
1007
+ (target.className.includes('modal-close') || target.className.includes('modal-background')) &&
1008
+ null !== modal
1009
+ ) {
976
1010
  setModal(null)
977
1011
  }
978
1012
  }
@@ -983,7 +1017,12 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
983
1017
  }
984
1018
 
985
1019
  // if string of letters like 'Home' then dont need to format as a number
986
- if (typeof value === 'string' && value.length > 0 && state.legend.type === 'equalnumber') {
1020
+ if (
1021
+ typeof value === 'string' &&
1022
+ value.length > 0 &&
1023
+ /[a-zA-Z]/.test(value) &&
1024
+ state.legend.type === 'equalnumber'
1025
+ ) {
987
1026
  return value
988
1027
  }
989
1028
 
@@ -1129,7 +1168,11 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1129
1168
  const formatLegendLocation = key => {
1130
1169
  let value = key
1131
1170
  let formattedName = ''
1132
- let stateName = stateFipsToTwoDigit[key.substring(0, 2)]
1171
+ let stateName = stateFipsToTwoDigit[key?.substring(0, 2)]
1172
+ ? stateFipsToTwoDigit[key?.substring(0, 2)]
1173
+ : key
1174
+ ? runtimeData?.[key]?.[state.columns.geo.name]
1175
+ : ''
1133
1176
 
1134
1177
  if (stateName) {
1135
1178
  formattedName += stateName
@@ -1245,9 +1288,9 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1245
1288
  }
1246
1289
 
1247
1290
  // If world-geocode map zoom to geo point
1248
- if ('world-geocode' === state.general.type) {
1249
- let lat = value[state.columns.latitude.name]
1250
- let long = value[state.columns.longitude.name]
1291
+ if (['world-geocode'].includes(state.general.type)) {
1292
+ const lat = value[state.columns.latitude.name]
1293
+ const long = value[state.columns.longitude.name]
1251
1294
 
1252
1295
  setState({
1253
1296
  ...state,
@@ -1272,10 +1315,17 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1272
1315
  }
1273
1316
 
1274
1317
  const validateFipsCodeLength = newState => {
1275
- if (newState.general.geoType === 'us-county' || newState.general.geoType === 'single-state' || (newState.general.geoType === 'us' && newState?.data)) {
1318
+ if (
1319
+ newState.general.geoType === 'us-county' ||
1320
+ newState.general.geoType === 'single-state' ||
1321
+ (newState.general.geoType === 'us' && newState?.data)
1322
+ ) {
1276
1323
  newState?.data.forEach(dataPiece => {
1277
1324
  if (dataPiece[newState.columns.geo.name]) {
1278
- if (!isNaN(parseInt(dataPiece[newState.columns.geo.name])) && dataPiece[newState.columns.geo.name].length === 4) {
1325
+ if (
1326
+ !isNaN(parseInt(dataPiece[newState.columns.geo.name])) &&
1327
+ dataPiece[newState.columns.geo.name].length === 4
1328
+ ) {
1279
1329
  dataPiece[newState.columns.geo.name] = 0 + dataPiece[newState.columns.geo.name]
1280
1330
  }
1281
1331
  dataPiece[newState.columns.geo.name] = dataPiece[newState.columns.geo.name].toString()
@@ -1393,7 +1443,11 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1393
1443
  ...configObj
1394
1444
  }
1395
1445
 
1396
- const urlFilters = newState.filters ? (newState.filters.filter(filter => filter.type === 'url').length > 0 ? true : false) : false
1446
+ const urlFilters = newState.filters
1447
+ ? newState.filters.filter(filter => filter.type === 'url').length > 0
1448
+ ? true
1449
+ : false
1450
+ : false
1397
1451
 
1398
1452
  if (newState.dataUrl && !urlFilters) {
1399
1453
  // handle urls with spaces in the name.
@@ -1437,7 +1491,7 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1437
1491
  validateFipsCodeLength(newState)
1438
1492
 
1439
1493
  // add ability to rename state properties over time.
1440
- const processedConfig = { ...(await coveUpdateWorker(newState)) }
1494
+ const processedConfig = { ...coveUpdateWorker(newState) }
1441
1495
 
1442
1496
  setState(processedConfig)
1443
1497
  setLoading(false)
@@ -1474,13 +1528,6 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1474
1528
  }
1475
1529
  }, [state, container]) // eslint-disable-line
1476
1530
 
1477
- useEffect(() => {
1478
- if (state.data) {
1479
- let newData = generateRuntimeData(state)
1480
- setRuntimeData(newData)
1481
- }
1482
- }, [state.general.statePicked]) // eslint-disable-line
1483
-
1484
1531
  useEffect(() => {
1485
1532
  // When geotype changes - add UID
1486
1533
  if (state.data && state.columns.geo.name) {
@@ -1531,7 +1578,7 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1531
1578
  }
1532
1579
  }
1533
1580
 
1534
- const hashLegend = generateRuntimeLegendHash()
1581
+ const hashLegend = generateRuntimeLegendHash(state, runtimeFilters)
1535
1582
 
1536
1583
  const hashData = hashObj({
1537
1584
  data: state.data,
@@ -1541,6 +1588,7 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1541
1588
  geo: state.columns.geo.name,
1542
1589
  primary: state.columns.primary.name,
1543
1590
  mapPosition: state.mapPosition,
1591
+ map: state.map,
1544
1592
  ...runtimeFilters
1545
1593
  })
1546
1594
 
@@ -1558,12 +1606,21 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1558
1606
  }, [state]) // eslint-disable-line
1559
1607
 
1560
1608
  useEffect(() => {
1561
- const hashLegend = generateRuntimeLegendHash()
1609
+ const hashLegend = generateRuntimeLegendHash(state, runtimeFilters)
1562
1610
 
1563
1611
  // Legend - Update when runtimeData does
1564
1612
  const legend = generateRuntimeLegend(state, runtimeData, hashLegend)
1565
1613
  setRuntimeLegend(legend)
1566
- }, [runtimeData, state.legend.unified, state.legend.showSpecialClassesLast, state.legend.separateZero, state.general.equalNumberOptIn, state.legend.numberOfItems, state.legend.specialClasses, state.legend.additionalCategories]) // eslint-disable-line
1614
+ }, [
1615
+ runtimeData,
1616
+ state.legend.unified,
1617
+ state.legend.showSpecialClassesLast,
1618
+ state.legend.separateZero,
1619
+ state.general.equalNumberOptIn,
1620
+ state.legend.numberOfItems,
1621
+ state.legend.specialClasses,
1622
+ state.legend.additionalCategories
1623
+ ]) // eslint-disable-line
1567
1624
 
1568
1625
  useEffect(() => {
1569
1626
  reloadURLData()
@@ -1587,7 +1644,13 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1587
1644
  if (!table.label || table.label === '') table.label = 'Data Table'
1588
1645
 
1589
1646
  // Map container classes
1590
- let mapContainerClasses = ['map-container', state.legend.position, state.general.type, state.general.geoType, 'outline-none']
1647
+ let mapContainerClasses = [
1648
+ 'map-container',
1649
+ state.legend?.position,
1650
+ state.general.type,
1651
+ state.general.geoType,
1652
+ 'outline-none'
1653
+ ]
1591
1654
 
1592
1655
  if (modal) {
1593
1656
  mapContainerClasses.push('modal-background')
@@ -1599,14 +1662,26 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1599
1662
 
1600
1663
  // Props passed to all map types
1601
1664
  const mapProps = {
1665
+ projection,
1666
+ setProjection,
1667
+ stateToShow,
1668
+ setStateToShow,
1669
+ setScale,
1670
+ setTranslate,
1671
+ scale,
1672
+ translate,
1673
+ isDraggingAnnotation,
1674
+ handleDragStateChange,
1602
1675
  applyLegendToRow,
1603
1676
  applyTooltipsToGeo,
1604
1677
  capitalize: state.tooltips?.capitalizeLabels,
1605
1678
  closeModal,
1606
1679
  columnsInData: state?.data?.[0] ? Object.keys(state.data[0]) : [],
1680
+ container,
1607
1681
  content: modal,
1608
1682
  currentViewport,
1609
1683
  data: runtimeData,
1684
+ dimensions,
1610
1685
  displayDataAsText,
1611
1686
  displayGeoName,
1612
1687
  filteredCountryCode,
@@ -1625,6 +1700,7 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1625
1700
  resetLegendToggles,
1626
1701
  runtimeFilters,
1627
1702
  runtimeLegend,
1703
+ runtimeData,
1628
1704
  setAccessibleStatus,
1629
1705
  setFilteredCountryCode,
1630
1706
  setParentConfig: setConfig,
@@ -1642,12 +1718,21 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1642
1718
  titleCase,
1643
1719
  type: general.type,
1644
1720
  viewport: currentViewport,
1645
- tooltipId
1721
+ tooltipId,
1722
+ tooltipRef,
1723
+ topoData,
1724
+ setTopoData,
1725
+ getTextWidth,
1726
+ mapId
1646
1727
  }
1647
1728
 
1648
1729
  if (!mapProps.data || !state.data) return <></>
1649
1730
 
1650
- const hasDataTable = state.runtime.editorErrorMessage.length === 0 && true === table.forceDisplay && general.type !== 'navigation' && false === loading
1731
+ const hasDataTable =
1732
+ state.runtime.editorErrorMessage.length === 0 &&
1733
+ true === table.forceDisplay &&
1734
+ general.type !== 'navigation' &&
1735
+ false === loading
1651
1736
 
1652
1737
  const handleMapTabbing = () => {
1653
1738
  let tabbingID
@@ -1680,18 +1765,29 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1680
1765
  </a>
1681
1766
  )
1682
1767
 
1768
+ const sectionClassNames = () => {
1769
+ const classes = ['cove-component__content', 'cdc-map-inner-container', `${currentViewport}`]
1770
+ if (config?.runtime?.editorErrorMessage.length > 0) classes.push('type-map--has-error')
1771
+ return classes.join(' ')
1772
+ }
1773
+
1683
1774
  return (
1684
1775
  <ConfigContext.Provider value={mapProps}>
1685
- <Layout.VisualizationWrapper config={state} isEditor={isEditor} ref={outerContainerRef} imageId={imageId} showEditorPanel={state.showEditorPanel}>
1776
+ <Layout.VisualizationWrapper
1777
+ config={state}
1778
+ isEditor={isEditor}
1779
+ ref={outerContainerRef}
1780
+ imageId={imageId}
1781
+ showEditorPanel={state.showEditorPanel}
1782
+ >
1686
1783
  {isEditor && <EditorPanel columnsRequiredChecker={columnsRequiredChecker} />}
1687
1784
  <Layout.Responsive isEditor={isEditor}>
1688
- {state?.runtime?.editorErrorMessage.length > 0 && <Error state={state} />}
1689
- {requiredColumns && <Waiting requiredColumns={requiredColumns} className={displayPanel ? `waiting` : `waiting collapsed`} />}
1785
+ {requiredColumns && (
1786
+ <Waiting requiredColumns={requiredColumns} className={displayPanel ? `waiting` : `waiting collapsed`} />
1787
+ )}
1690
1788
  {!runtimeData.init && (general.type === 'navigation' || runtimeLegend) && (
1691
- <section className={`cove-component__content cdc-map-inner-container ${currentViewport}`} aria-label={'Map: ' + title} ref={innerContainerRef}>
1692
- {!window.matchMedia('(any-hover: none)').matches && 'hover' === tooltips.appearanceType && (
1693
- <ReactTooltip id={`tooltip__${tooltipId}`} float={true} className={`${tooltips.capitalizeLabels ? 'capitalize tooltip tooltip-test' : 'tooltip tooltip-test'}`} style={{ background: `rgba(255,255,255, ${state.tooltips.opacity / 100})`, color: 'black' }} />
1694
- )}
1789
+ <section className={sectionClassNames()} aria-label={'Map: ' + title} ref={innerContainerRef}>
1790
+ {state?.runtime?.editorErrorMessage.length > 0 && <Error state={state} />}
1695
1791
  {/* prettier-ignore */}
1696
1792
  <Title
1697
1793
  title={title}
@@ -1700,11 +1796,26 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1700
1796
  classes={['map-title', general.showTitle === true ? 'visible' : 'hidden', `${general.headerColor}`]}
1701
1797
  />
1702
1798
  <SkipTo skipId={tabId} skipMessage='Skip Over Map Container' />
1799
+ {state?.annotations?.length > 0 && (
1800
+ <SkipTo skipId={tabId} skipMessage={`Skip over annotations`} key={`skip-annotations`} />
1801
+ )}
1703
1802
 
1704
- {general.introText && <section className='introText'>{parse(general.introText)}</section>}
1803
+ {general.introText && (
1804
+ <section className='introText' style={{ padding: '15px', margin: '0px' }}>
1805
+ {parse(general.introText)}
1806
+ </section>
1807
+ )}
1705
1808
 
1706
- {/* prettier-ignore */}
1707
- {state?.filters?.length > 0 && <Filters config={state} setConfig={setState} getUniqueValues={getUniqueValues} filteredData={runtimeFilters} setFilteredData={setRuntimeFilters} dimensions={dimensions} />}
1809
+ {state?.filters?.length > 0 && (
1810
+ <Filters
1811
+ config={state}
1812
+ setConfig={setState}
1813
+ getUniqueValues={getUniqueValues}
1814
+ filteredData={runtimeFilters}
1815
+ setFilteredData={setRuntimeFilters}
1816
+ dimensions={dimensions}
1817
+ />
1818
+ )}
1708
1819
 
1709
1820
  <div
1710
1821
  role='region'
@@ -1716,9 +1827,10 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1716
1827
  closeModal(e)
1717
1828
  }
1718
1829
  }}
1830
+ style={{ padding: '15px 25px', margin: '0px' }}
1719
1831
  >
1720
1832
  {/* eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex */}
1721
- <section className='outline-none geography-container' ref={mapSvg} tabIndex='0' style={{ width: '100%' }}>
1833
+ <section className='outline-none geography-container w-100' ref={mapSvg} tabIndex='0'>
1722
1834
  {currentViewport && (
1723
1835
  <>
1724
1836
  {modal && <Modal />}
@@ -1732,10 +1844,21 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1732
1844
  )}
1733
1845
  </section>
1734
1846
 
1735
- {general.showSidebar && 'navigation' !== general.type && <Legend ref={legendRef} skipId={tabId} />}
1847
+ {general.showSidebar && 'navigation' !== general.type && (
1848
+ <Legend dimensions={dimensions} currentViewport={currentViewport} ref={legendRef} skipId={tabId} />
1849
+ )}
1736
1850
  </div>
1737
1851
 
1738
- {'navigation' === general.type && <NavigationMenu mapTabbingID={tabId} displayGeoName={displayGeoName} data={runtimeData} options={general} columns={state.columns} navigationHandler={val => navigationHandler(val)} />}
1852
+ {'navigation' === general.type && (
1853
+ <NavigationMenu
1854
+ mapTabbingID={tabId}
1855
+ displayGeoName={displayGeoName}
1856
+ data={runtimeData}
1857
+ options={general}
1858
+ columns={state.columns}
1859
+ navigationHandler={val => navigationHandler(val)}
1860
+ />
1861
+ )}
1739
1862
 
1740
1863
  {/* Link */}
1741
1864
  {isDashboard && config.table?.forceDisplay && config.table.showDataTableLink ? tableLink : link && link}
@@ -1743,41 +1866,64 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1743
1866
  {subtext.length > 0 && <p className='subtext'>{parse(subtext)}</p>}
1744
1867
 
1745
1868
  <MediaControls.Section classes={['download-buttons']}>
1746
- {state.general.showDownloadImgButton && <MediaControls.Button text='Download Image' title='Download Chart as Image' type='image' state={state} elementToCapture={imageId} />}
1747
- {state.general.showDownloadPdfButton && <MediaControls.Button text='Download PDF' title='Download Chart as PDF' type='pdf' state={state} elementToCapture={imageId} />}
1869
+ {state.general.showDownloadImgButton && (
1870
+ <MediaControls.Button
1871
+ text='Download Image'
1872
+ title='Download Chart as Image'
1873
+ type='image'
1874
+ state={state}
1875
+ elementToCapture={imageId}
1876
+ />
1877
+ )}
1878
+ {state.general.showDownloadPdfButton && (
1879
+ <MediaControls.Button
1880
+ text='Download PDF'
1881
+ title='Download Chart as PDF'
1882
+ type='pdf'
1883
+ state={state}
1884
+ elementToCapture={imageId}
1885
+ />
1886
+ )}
1748
1887
  </MediaControls.Section>
1749
1888
 
1750
- {state.runtime.editorErrorMessage.length === 0 && true === table.forceDisplay && general.type !== 'navigation' && false === loading && (
1751
- <DataTable
1752
- config={state}
1753
- rawData={state.data}
1754
- navigationHandler={navigationHandler}
1755
- expandDataTable={table.expanded}
1756
- headerColor={general.headerColor}
1757
- columns={state.columns}
1758
- showDownloadButton={general.showDownloadButton}
1759
- showFullGeoNameInCSV={table.showFullGeoNameInCSV}
1760
- runtimeLegend={runtimeLegend}
1761
- runtimeData={runtimeData}
1762
- displayDataAsText={displayDataAsText}
1763
- displayGeoName={displayGeoName}
1764
- applyLegendToRow={applyLegendToRow}
1765
- tableTitle={table.label}
1766
- indexTitle={table.indexLabel}
1767
- vizTitle={general.title}
1768
- viewport={currentViewport}
1769
- formatLegendLocation={formatLegendLocation}
1770
- setFilteredCountryCode={setFilteredCountryCode}
1771
- tabbingId={tabId}
1772
- showDownloadImgButton={state.general.showDownloadImgButton}
1773
- showDownloadPdfButton={state.general.showDownloadPdfButton}
1774
- innerContainerRef={innerContainerRef}
1775
- outerContainerRef={outerContainerRef}
1776
- imageRef={imageId}
1777
- isDebug={isDebug}
1778
- wrapColumns={table.wrapColumns}
1779
- />
1780
- )}
1889
+ {state.runtime.editorErrorMessage.length === 0 &&
1890
+ true === table.forceDisplay &&
1891
+ general.type !== 'navigation' &&
1892
+ false === loading && (
1893
+ <DataTable
1894
+ config={state}
1895
+ rawData={state.data}
1896
+ navigationHandler={navigationHandler}
1897
+ expandDataTable={table.expanded}
1898
+ headerColor={general.headerColor}
1899
+ columns={state.columns}
1900
+ showDownloadButton={general.showDownloadButton}
1901
+ showFullGeoNameInCSV={table.showFullGeoNameInCSV}
1902
+ runtimeLegend={runtimeLegend}
1903
+ runtimeData={runtimeData}
1904
+ displayDataAsText={displayDataAsText}
1905
+ displayGeoName={displayGeoName}
1906
+ applyLegendToRow={applyLegendToRow}
1907
+ tableTitle={table.label}
1908
+ indexTitle={table.indexLabel}
1909
+ vizTitle={general.title}
1910
+ viewport={currentViewport}
1911
+ formatLegendLocation={formatLegendLocation}
1912
+ setFilteredCountryCode={setFilteredCountryCode}
1913
+ tabbingId={tabId}
1914
+ showDownloadImgButton={state.general.showDownloadImgButton}
1915
+ showDownloadPdfButton={state.general.showDownloadPdfButton}
1916
+ innerContainerRef={innerContainerRef}
1917
+ outerContainerRef={outerContainerRef}
1918
+ imageRef={imageId}
1919
+ isDebug={isDebug}
1920
+ wrapColumns={table.wrapColumns}
1921
+ />
1922
+ )}
1923
+
1924
+ {state.annotations.length > 0 && <Annotation.Dropdown />}
1925
+
1926
+ {state.annotations.length > 0 && <Annotation.Dropdown />}
1781
1927
 
1782
1928
  {general.footnotes && <section className='footnotes'>{parse(general.footnotes)}</section>}
1783
1929
  </section>
@@ -1786,6 +1932,28 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1786
1932
  <div aria-live='assertive' className='cdcdataviz-sr-only'>
1787
1933
  {accessibleStatus}
1788
1934
  </div>
1935
+
1936
+ {!isDraggingAnnotation &&
1937
+ !window.matchMedia('(any-hover: none)').matches &&
1938
+ 'hover' === tooltips.appearanceType && (
1939
+ <ReactTooltip
1940
+ id={`tooltip__${tooltipId}`}
1941
+ float={true}
1942
+ className={`${tooltips.capitalizeLabels ? 'capitalize tooltip tooltip-test' : 'tooltip tooltip-test'}`}
1943
+ style={{ background: `rgba(255,255,255, ${state.tooltips.opacity / 100})`, color: 'black' }}
1944
+ />
1945
+ )}
1946
+ <div
1947
+ ref={tooltipRef}
1948
+ id={`tooltip__${tooltipId}-canvas`}
1949
+ className='tooltip'
1950
+ style={{
1951
+ background: `rgba(255,255,255,${state.tooltips.opacity / 100})`,
1952
+ position: 'absolute',
1953
+ whiteSpace: 'nowrap',
1954
+ display: 'none' // can't use d-none here
1955
+ }}
1956
+ ></div>
1789
1957
  </Layout.Responsive>
1790
1958
  </Layout.VisualizationWrapper>
1791
1959
  </ConfigContext.Provider>