@cdc/map 4.24.5 → 4.24.9-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 (66) hide show
  1. package/dist/cdcmap.js +71876 -64947
  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 +373 -202
  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/src/components/Modal.jsx +0 -22
  62. package/src/test/CdcMap.test.jsx +0 -19
  63. /package/src/components/EditorPanel/components/{Panel.PatternSettings-style.css → Panels/Panel.PatternSettings-style.css} +0 -0
  64. /package/src/components/{Geo.jsx → Geo.tsx} +0 -0
  65. /package/src/components/{NavigationMenu.jsx → NavigationMenu.tsx} +0 -0
  66. /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,16 @@ 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
- const tooltipId = useId()
140
+ // create random tooltipId
141
+ const tooltipId = `${Math.random().toString(16).slice(-4)}`
142
+ const mapId = useId()
147
143
 
148
144
  const { changeFilterActive, handleSorting } = useFilters({ config: state, setConfig: setState })
149
145
  let legendMemo = useRef(new Map())
@@ -152,6 +148,10 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
152
148
 
153
149
  if (isDebug) console.log('CdcMap state=', state) // <eslint-disable-line></eslint-disable-line>
154
150
 
151
+ const handleDragStateChange = isDragging => {
152
+ setIsDraggingAnnotation(isDragging)
153
+ }
154
+
155
155
  const columnsRequiredChecker = useCallback(() => {
156
156
  let columnList = []
157
157
 
@@ -166,15 +166,24 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
166
166
  }
167
167
 
168
168
  // Navigate is required for navigation maps
169
- if ('navigation' === state.general.type && ('' === state.columns.navigate.name || undefined === state.columns.navigate)) {
169
+ if (
170
+ 'navigation' === state.general.type &&
171
+ ('' === state.columns.navigate.name || undefined === state.columns.navigate)
172
+ ) {
170
173
  columnList.push('Navigation')
171
174
  }
172
175
 
173
- if (('us-geocode' === state.general.type || 'world-geocode' === state.general.type) && '' === state.columns.latitude.name) {
176
+ if (
177
+ ('us-geocode' === state.general.type || 'world-geocode' === state.general.type) &&
178
+ '' === state.columns.latitude.name
179
+ ) {
174
180
  columnList.push('Latitude')
175
181
  }
176
182
 
177
- if (('us-geocode' === state.general.type || 'world-geocode' === state.general.type) && '' === state.columns.longitude.name) {
183
+ if (
184
+ ('us-geocode' === state.general.type || 'world-geocode' === state.general.type) &&
185
+ '' === state.columns.longitude.name
186
+ ) {
178
187
  columnList.push('Longitude')
179
188
  }
180
189
 
@@ -219,32 +228,11 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
219
228
  }
220
229
  }, [state.mapPosition, setPosition])
221
230
 
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
231
  const resizeObserver = new ResizeObserver(entries => {
245
232
  for (let entry of entries) {
246
233
  let { width, height } = entry.contentRect
247
234
  let newViewport = getViewport(entry.contentRect.width)
235
+
248
236
  let editorWidth = 350
249
237
 
250
238
  setCurrentViewport(newViewport)
@@ -260,7 +248,7 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
260
248
  // We are mutating state in place here (depending on where called) - but it's okay, this isn't used for rerender
261
249
  // eslint-disable-next-line
262
250
  const addUIDs = useCallback((obj, fromColumn) => {
263
- obj.data.forEach(row => {
251
+ obj.data.forEach((row, index) => {
264
252
  let uid = null
265
253
 
266
254
  if (row.uid) row.uid = null // Wipe existing UIDs
@@ -320,7 +308,10 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
320
308
  }
321
309
 
322
310
  // County Check
323
- if (('us-county' === obj.general.geoType || 'single-state' === obj.general.geoType) && 'us-geocode' !== obj.general.type) {
311
+ if (
312
+ ('us-county' === obj.general.geoType || 'single-state' === obj.general.geoType) &&
313
+ 'us-geocode' !== obj.general.type
314
+ ) {
324
315
  const fips = row[obj.columns.geo.name]
325
316
  uid = countyKeys.find(key => key === fips)
326
317
  }
@@ -329,8 +320,14 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
329
320
  uid = row[state.columns.geo.name]
330
321
  }
331
322
 
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]
323
+ if (
324
+ !uid &&
325
+ state.columns.latitude?.name &&
326
+ state.columns.longitude?.name &&
327
+ row[state.columns.latitude?.name] &&
328
+ row[state.columns.longitude?.name]
329
+ ) {
330
+ uid = `${row[state.columns.geo.name]}`
334
331
  }
335
332
 
336
333
  if (uid) {
@@ -349,6 +346,7 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
349
346
  const newLegendMemo = new Map() // Reset memoization
350
347
  const newLegendSpecialClassLastMemo = new Map() // Reset bin memoization
351
348
  let primaryCol = obj.columns.primary.name,
349
+ isSingleState = obj.general.geoType === 'single-state',
352
350
  isBubble = obj.general.type === 'bubble',
353
351
  categoricalCol = obj.columns.categorical ? obj.columns.categorical.name : undefined,
354
352
  type = obj.legend.type,
@@ -680,14 +678,32 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
680
678
  let colors = colorPalettes[state.color]
681
679
  let colorRange = colors.slice(0, state.legend.numberOfItems)
682
680
 
681
+ const getDomain = () => {
682
+ // backwards compatibility
683
+ if (state?.columns?.primary?.roundToPlace !== undefined && state?.general?.equalNumberOptIn) {
684
+ return _.uniq(
685
+ dataSet.map(item =>
686
+ Number(item[state.columns.primary.name]).toFixed(Number(state?.columns?.primary?.roundToPlace))
687
+ )
688
+ )
689
+ }
690
+ return _.uniq(dataSet.map(item => Math.round(Number(item[state.columns.primary.name]))))
691
+ }
692
+
693
+ const getBreaks = scale => {
694
+ // backwards compatibility
695
+ if (state?.columns?.primary?.roundToPlace !== undefined && state?.general?.equalNumberOptIn) {
696
+ return scale.quantiles().map(b => Number(b)?.toFixed(Number(state?.columns?.primary?.roundToPlace)))
697
+ }
698
+ return scale.quantiles().map(item => Number(Math.round(item)))
699
+ }
700
+
683
701
  let scale = d3
684
702
  .scaleQuantile()
685
- .domain([...new Set(dataSet.map(item => Math.round(item[state.columns.primary.name])))]) // min/max values
703
+ .domain(getDomain()) // min/max values
686
704
  .range(colorRange) // set range to our colors array
687
705
 
688
- let breaks = scale.quantiles()
689
-
690
- breaks = breaks.map(item => Math.round(item))
706
+ const breaks = getBreaks(scale)
691
707
 
692
708
  // if seperating zero force it into breaks
693
709
  if (breaks[0] !== 0) {
@@ -716,8 +732,12 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
716
732
  return min
717
733
  }
718
734
 
735
+ const getDecimalPlace = n => {
736
+ return Math.pow(10, -n)
737
+ }
738
+
719
739
  const setMax = (index, min) => {
720
- let max = breaks[index + 1] - 1
740
+ let max = Number(breaks[index + 1]) - getDecimalPlace(Number(state?.columns?.primary?.roundToPlace))
721
741
 
722
742
  // check if min and max range are the same
723
743
  // if (min === max + 1) {
@@ -845,57 +865,73 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
845
865
 
846
866
  if (hash) filters.fromHash = hash
847
867
 
848
- obj?.filters.forEach(({ columnName, label, labels, queryParameter, orderedValues, active, values, type, showDropdown, setByQueryParameter }, idx) => {
849
- let newFilter = runtimeFilters[idx]
868
+ obj?.filters.forEach(
869
+ (
870
+ {
871
+ columnName,
872
+ label,
873
+ labels,
874
+ queryParameter,
875
+ orderedValues,
876
+ active,
877
+ values,
878
+ type,
879
+ showDropdown,
880
+ setByQueryParameter
881
+ },
882
+ idx
883
+ ) => {
884
+ let newFilter = runtimeFilters[idx]
885
+
886
+ const sortAsc = (a, b) => {
887
+ return a.toString().localeCompare(b.toString(), 'en', { numeric: true })
888
+ }
850
889
 
851
- const sortAsc = (a, b) => {
852
- return a.toString().localeCompare(b.toString(), 'en', { numeric: true })
853
- }
890
+ const sortDesc = (a, b) => {
891
+ return b.toString().localeCompare(a.toString(), 'en', { numeric: true })
892
+ }
854
893
 
855
- const sortDesc = (a, b) => {
856
- return b.toString().localeCompare(a.toString(), 'en', { numeric: true })
857
- }
894
+ if (type !== 'url') {
895
+ values = getUniqueValues(state.data, columnName)
858
896
 
859
- if (type !== 'url') {
860
- values = getUniqueValues(state.data, columnName)
897
+ if (obj.filters[idx].order === 'asc') {
898
+ values = values.sort(sortAsc)
899
+ }
861
900
 
862
- if (obj.filters[idx].order === 'asc') {
863
- values = values.sort(sortAsc)
864
- }
901
+ if (obj.filters[idx].order === 'desc') {
902
+ values = values.sort(sortDesc)
903
+ }
865
904
 
866
- if (obj.filters[idx].order === 'desc') {
867
- values = values.sort(sortDesc)
905
+ if (obj.filters[idx].order === 'cust') {
906
+ if (obj.filters[idx]?.values.length > 0) {
907
+ values = obj.filters[idx].values
908
+ }
909
+ }
910
+ } else {
911
+ values = values
868
912
  }
869
913
 
870
- if (obj.filters[idx].order === 'cust') {
871
- if (obj.filters[idx]?.values.length > 0) {
872
- values = obj.filters[idx].values
873
- }
914
+ if (undefined === newFilter) {
915
+ newFilter = {}
874
916
  }
875
- } else {
876
- values = values
877
- }
878
917
 
879
- if (undefined === newFilter) {
880
- newFilter = {}
918
+ newFilter.order = obj.filters[idx].order ? obj.filters[idx].order : 'asc'
919
+ newFilter.type = type
920
+ newFilter.label = label ?? ''
921
+ newFilter.columnName = columnName
922
+ newFilter.orderedValues = orderedValues
923
+ newFilter.queryParameter = queryParameter
924
+ newFilter.labels = labels
925
+ newFilter.values = values
926
+ newFilter.setByQueryParameter = setByQueryParameter
927
+ handleSorting(newFilter)
928
+ newFilter.active = active ?? values[0] // Default to first found value
929
+ newFilter.filterStyle = obj.filters[idx].filterStyle ? obj.filters[idx].filterStyle : 'dropdown'
930
+ newFilter.showDropdown = showDropdown
931
+
932
+ filters.push(newFilter)
881
933
  }
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
- })
934
+ )
899
935
 
900
936
  return filters
901
937
  })
@@ -920,11 +956,6 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
920
956
 
921
957
  if (undefined === row.uid) return false // No UID for this row, we can't use for mapping
922
958
 
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
959
  if (row[obj.columns.primary.name]) {
929
960
  row[obj.columns.primary.name] = numberFromString(row[obj.columns.primary.name], state)
930
961
  }
@@ -972,7 +1003,11 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
972
1003
  const mapSvg = useRef(null)
973
1004
 
974
1005
  const closeModal = ({ target }) => {
975
- if ('string' === typeof target.className && (target.className.includes('modal-close') || target.className.includes('modal-background')) && null !== modal) {
1006
+ if (
1007
+ 'string' === typeof target.className &&
1008
+ (target.className.includes('modal-close') || target.className.includes('modal-background')) &&
1009
+ null !== modal
1010
+ ) {
976
1011
  setModal(null)
977
1012
  }
978
1013
  }
@@ -983,7 +1018,12 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
983
1018
  }
984
1019
 
985
1020
  // 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') {
1021
+ if (
1022
+ typeof value === 'string' &&
1023
+ value.length > 0 &&
1024
+ /[a-zA-Z]/.test(value) &&
1025
+ state.legend.type === 'equalnumber'
1026
+ ) {
987
1027
  return value
988
1028
  }
989
1029
 
@@ -1129,7 +1169,11 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1129
1169
  const formatLegendLocation = key => {
1130
1170
  let value = key
1131
1171
  let formattedName = ''
1132
- let stateName = stateFipsToTwoDigit[key.substring(0, 2)]
1172
+ let stateName = stateFipsToTwoDigit[key?.substring(0, 2)]
1173
+ ? stateFipsToTwoDigit[key?.substring(0, 2)]
1174
+ : key
1175
+ ? runtimeData?.[key]?.[state.columns.geo.name]
1176
+ : ''
1133
1177
 
1134
1178
  if (stateName) {
1135
1179
  formattedName += stateName
@@ -1245,9 +1289,9 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1245
1289
  }
1246
1290
 
1247
1291
  // 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]
1292
+ if (['world-geocode'].includes(state.general.type)) {
1293
+ const lat = value[state.columns.latitude.name]
1294
+ const long = value[state.columns.longitude.name]
1251
1295
 
1252
1296
  setState({
1253
1297
  ...state,
@@ -1272,10 +1316,17 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1272
1316
  }
1273
1317
 
1274
1318
  const validateFipsCodeLength = newState => {
1275
- if (newState.general.geoType === 'us-county' || newState.general.geoType === 'single-state' || (newState.general.geoType === 'us' && newState?.data)) {
1319
+ if (
1320
+ newState.general.geoType === 'us-county' ||
1321
+ newState.general.geoType === 'single-state' ||
1322
+ (newState.general.geoType === 'us' && newState?.data)
1323
+ ) {
1276
1324
  newState?.data.forEach(dataPiece => {
1277
1325
  if (dataPiece[newState.columns.geo.name]) {
1278
- if (!isNaN(parseInt(dataPiece[newState.columns.geo.name])) && dataPiece[newState.columns.geo.name].length === 4) {
1326
+ if (
1327
+ !isNaN(parseInt(dataPiece[newState.columns.geo.name])) &&
1328
+ dataPiece[newState.columns.geo.name].length === 4
1329
+ ) {
1279
1330
  dataPiece[newState.columns.geo.name] = 0 + dataPiece[newState.columns.geo.name]
1280
1331
  }
1281
1332
  dataPiece[newState.columns.geo.name] = dataPiece[newState.columns.geo.name].toString()
@@ -1393,7 +1444,11 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1393
1444
  ...configObj
1394
1445
  }
1395
1446
 
1396
- const urlFilters = newState.filters ? (newState.filters.filter(filter => filter.type === 'url').length > 0 ? true : false) : false
1447
+ const urlFilters = newState.filters
1448
+ ? newState.filters.filter(filter => filter.type === 'url').length > 0
1449
+ ? true
1450
+ : false
1451
+ : false
1397
1452
 
1398
1453
  if (newState.dataUrl && !urlFilters) {
1399
1454
  // handle urls with spaces in the name.
@@ -1437,10 +1492,12 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1437
1492
  validateFipsCodeLength(newState)
1438
1493
 
1439
1494
  // add ability to rename state properties over time.
1440
- const processedConfig = { ...(await coveUpdateWorker(newState)) }
1495
+ const processedConfig = { ...coveUpdateWorker(newState) }
1441
1496
 
1442
- setState(processedConfig)
1443
- setLoading(false)
1497
+ setTimeout(() => {
1498
+ setState(processedConfig)
1499
+ setLoading(false)
1500
+ }, 10)
1444
1501
  }
1445
1502
 
1446
1503
  const init = async () => {
@@ -1474,13 +1531,6 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1474
1531
  }
1475
1532
  }, [state, container]) // eslint-disable-line
1476
1533
 
1477
- useEffect(() => {
1478
- if (state.data) {
1479
- let newData = generateRuntimeData(state)
1480
- setRuntimeData(newData)
1481
- }
1482
- }, [state.general.statePicked]) // eslint-disable-line
1483
-
1484
1534
  useEffect(() => {
1485
1535
  // When geotype changes - add UID
1486
1536
  if (state.data && state.columns.geo.name) {
@@ -1531,7 +1581,7 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1531
1581
  }
1532
1582
  }
1533
1583
 
1534
- const hashLegend = generateRuntimeLegendHash()
1584
+ const hashLegend = generateRuntimeLegendHash(state, runtimeFilters)
1535
1585
 
1536
1586
  const hashData = hashObj({
1537
1587
  data: state.data,
@@ -1541,6 +1591,7 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1541
1591
  geo: state.columns.geo.name,
1542
1592
  primary: state.columns.primary.name,
1543
1593
  mapPosition: state.mapPosition,
1594
+ map: state.map,
1544
1595
  ...runtimeFilters
1545
1596
  })
1546
1597
 
@@ -1558,12 +1609,21 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1558
1609
  }, [state]) // eslint-disable-line
1559
1610
 
1560
1611
  useEffect(() => {
1561
- const hashLegend = generateRuntimeLegendHash()
1612
+ const hashLegend = generateRuntimeLegendHash(state, runtimeFilters)
1562
1613
 
1563
1614
  // Legend - Update when runtimeData does
1564
1615
  const legend = generateRuntimeLegend(state, runtimeData, hashLegend)
1565
1616
  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
1617
+ }, [
1618
+ runtimeData,
1619
+ state.legend.unified,
1620
+ state.legend.showSpecialClassesLast,
1621
+ state.legend.separateZero,
1622
+ state.general.equalNumberOptIn,
1623
+ state.legend.numberOfItems,
1624
+ state.legend.specialClasses,
1625
+ state.legend.additionalCategories
1626
+ ]) // eslint-disable-line
1567
1627
 
1568
1628
  useEffect(() => {
1569
1629
  reloadURLData()
@@ -1587,7 +1647,13 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1587
1647
  if (!table.label || table.label === '') table.label = 'Data Table'
1588
1648
 
1589
1649
  // Map container classes
1590
- let mapContainerClasses = ['map-container', state.legend.position, state.general.type, state.general.geoType, 'outline-none']
1650
+ let mapContainerClasses = [
1651
+ 'map-container',
1652
+ state.legend?.position,
1653
+ state.general.type,
1654
+ state.general.geoType,
1655
+ 'outline-none'
1656
+ ]
1591
1657
 
1592
1658
  if (modal) {
1593
1659
  mapContainerClasses.push('modal-background')
@@ -1599,14 +1665,26 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1599
1665
 
1600
1666
  // Props passed to all map types
1601
1667
  const mapProps = {
1668
+ projection,
1669
+ setProjection,
1670
+ stateToShow,
1671
+ setStateToShow,
1672
+ setScale,
1673
+ setTranslate,
1674
+ scale,
1675
+ translate,
1676
+ isDraggingAnnotation,
1677
+ handleDragStateChange,
1602
1678
  applyLegendToRow,
1603
1679
  applyTooltipsToGeo,
1604
1680
  capitalize: state.tooltips?.capitalizeLabels,
1605
1681
  closeModal,
1606
1682
  columnsInData: state?.data?.[0] ? Object.keys(state.data[0]) : [],
1683
+ container,
1607
1684
  content: modal,
1608
1685
  currentViewport,
1609
1686
  data: runtimeData,
1687
+ dimensions,
1610
1688
  displayDataAsText,
1611
1689
  displayGeoName,
1612
1690
  filteredCountryCode,
@@ -1625,6 +1703,7 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1625
1703
  resetLegendToggles,
1626
1704
  runtimeFilters,
1627
1705
  runtimeLegend,
1706
+ runtimeData,
1628
1707
  setAccessibleStatus,
1629
1708
  setFilteredCountryCode,
1630
1709
  setParentConfig: setConfig,
@@ -1642,12 +1721,21 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1642
1721
  titleCase,
1643
1722
  type: general.type,
1644
1723
  viewport: currentViewport,
1645
- tooltipId
1724
+ tooltipId,
1725
+ tooltipRef,
1726
+ topoData,
1727
+ setTopoData,
1728
+ getTextWidth,
1729
+ mapId
1646
1730
  }
1647
1731
 
1648
1732
  if (!mapProps.data || !state.data) return <></>
1649
1733
 
1650
- const hasDataTable = state.runtime.editorErrorMessage.length === 0 && true === table.forceDisplay && general.type !== 'navigation' && false === loading
1734
+ const hasDataTable =
1735
+ state.runtime.editorErrorMessage.length === 0 &&
1736
+ true === table.forceDisplay &&
1737
+ general.type !== 'navigation' &&
1738
+ false === loading
1651
1739
 
1652
1740
  const handleMapTabbing = () => {
1653
1741
  let tabbingID
@@ -1680,18 +1768,29 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1680
1768
  </a>
1681
1769
  )
1682
1770
 
1771
+ const sectionClassNames = () => {
1772
+ const classes = ['cove-component__content', 'cdc-map-inner-container', `${currentViewport}`]
1773
+ if (config?.runtime?.editorErrorMessage.length > 0) classes.push('type-map--has-error')
1774
+ return classes.join(' ')
1775
+ }
1776
+
1683
1777
  return (
1684
1778
  <ConfigContext.Provider value={mapProps}>
1685
- <Layout.VisualizationWrapper config={state} isEditor={isEditor} ref={outerContainerRef} imageId={imageId} showEditorPanel={state.showEditorPanel}>
1779
+ <Layout.VisualizationWrapper
1780
+ config={state}
1781
+ isEditor={isEditor}
1782
+ ref={outerContainerRef}
1783
+ imageId={imageId}
1784
+ showEditorPanel={state.showEditorPanel}
1785
+ >
1686
1786
  {isEditor && <EditorPanel columnsRequiredChecker={columnsRequiredChecker} />}
1687
1787
  <Layout.Responsive isEditor={isEditor}>
1688
- {state?.runtime?.editorErrorMessage.length > 0 && <Error state={state} />}
1689
- {requiredColumns && <Waiting requiredColumns={requiredColumns} className={displayPanel ? `waiting` : `waiting collapsed`} />}
1788
+ {requiredColumns && (
1789
+ <Waiting requiredColumns={requiredColumns} className={displayPanel ? `waiting` : `waiting collapsed`} />
1790
+ )}
1690
1791
  {!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
- )}
1792
+ <section className={sectionClassNames()} aria-label={'Map: ' + title} ref={innerContainerRef}>
1793
+ {state?.runtime?.editorErrorMessage.length > 0 && <Error state={state} />}
1695
1794
  {/* prettier-ignore */}
1696
1795
  <Title
1697
1796
  title={title}
@@ -1700,11 +1799,26 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1700
1799
  classes={['map-title', general.showTitle === true ? 'visible' : 'hidden', `${general.headerColor}`]}
1701
1800
  />
1702
1801
  <SkipTo skipId={tabId} skipMessage='Skip Over Map Container' />
1802
+ {state?.annotations?.length > 0 && (
1803
+ <SkipTo skipId={tabId} skipMessage={`Skip over annotations`} key={`skip-annotations`} />
1804
+ )}
1703
1805
 
1704
- {general.introText && <section className='introText'>{parse(general.introText)}</section>}
1806
+ {general.introText && (
1807
+ <section className='introText' style={{ padding: '15px', margin: '0px' }}>
1808
+ {parse(general.introText)}
1809
+ </section>
1810
+ )}
1705
1811
 
1706
- {/* prettier-ignore */}
1707
- {state?.filters?.length > 0 && <Filters config={state} setConfig={setState} getUniqueValues={getUniqueValues} filteredData={runtimeFilters} setFilteredData={setRuntimeFilters} dimensions={dimensions} />}
1812
+ {state?.filters?.length > 0 && (
1813
+ <Filters
1814
+ config={state}
1815
+ setConfig={setState}
1816
+ getUniqueValues={getUniqueValues}
1817
+ filteredData={runtimeFilters}
1818
+ setFilteredData={setRuntimeFilters}
1819
+ dimensions={dimensions}
1820
+ />
1821
+ )}
1708
1822
 
1709
1823
  <div
1710
1824
  role='region'
@@ -1716,9 +1830,10 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1716
1830
  closeModal(e)
1717
1831
  }
1718
1832
  }}
1833
+ style={{ padding: '15px 25px', margin: '0px' }}
1719
1834
  >
1720
1835
  {/* eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex */}
1721
- <section className='outline-none geography-container' ref={mapSvg} tabIndex='0' style={{ width: '100%' }}>
1836
+ <section className='outline-none geography-container w-100' ref={mapSvg} tabIndex='0'>
1722
1837
  {currentViewport && (
1723
1838
  <>
1724
1839
  {modal && <Modal />}
@@ -1732,10 +1847,21 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1732
1847
  )}
1733
1848
  </section>
1734
1849
 
1735
- {general.showSidebar && 'navigation' !== general.type && <Legend ref={legendRef} skipId={tabId} />}
1850
+ {general.showSidebar && 'navigation' !== general.type && (
1851
+ <Legend dimensions={dimensions} currentViewport={currentViewport} ref={legendRef} skipId={tabId} />
1852
+ )}
1736
1853
  </div>
1737
1854
 
1738
- {'navigation' === general.type && <NavigationMenu mapTabbingID={tabId} displayGeoName={displayGeoName} data={runtimeData} options={general} columns={state.columns} navigationHandler={val => navigationHandler(val)} />}
1855
+ {'navigation' === general.type && (
1856
+ <NavigationMenu
1857
+ mapTabbingID={tabId}
1858
+ displayGeoName={displayGeoName}
1859
+ data={runtimeData}
1860
+ options={general}
1861
+ columns={state.columns}
1862
+ navigationHandler={val => navigationHandler(val)}
1863
+ />
1864
+ )}
1739
1865
 
1740
1866
  {/* Link */}
1741
1867
  {isDashboard && config.table?.forceDisplay && config.table.showDataTableLink ? tableLink : link && link}
@@ -1743,41 +1869,64 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1743
1869
  {subtext.length > 0 && <p className='subtext'>{parse(subtext)}</p>}
1744
1870
 
1745
1871
  <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} />}
1872
+ {state.general.showDownloadImgButton && (
1873
+ <MediaControls.Button
1874
+ text='Download Image'
1875
+ title='Download Chart as Image'
1876
+ type='image'
1877
+ state={state}
1878
+ elementToCapture={imageId}
1879
+ />
1880
+ )}
1881
+ {state.general.showDownloadPdfButton && (
1882
+ <MediaControls.Button
1883
+ text='Download PDF'
1884
+ title='Download Chart as PDF'
1885
+ type='pdf'
1886
+ state={state}
1887
+ elementToCapture={imageId}
1888
+ />
1889
+ )}
1748
1890
  </MediaControls.Section>
1749
1891
 
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
- )}
1892
+ {state.runtime.editorErrorMessage.length === 0 &&
1893
+ true === table.forceDisplay &&
1894
+ general.type !== 'navigation' &&
1895
+ false === loading && (
1896
+ <DataTable
1897
+ config={state}
1898
+ rawData={state.data}
1899
+ navigationHandler={navigationHandler}
1900
+ expandDataTable={table.expanded}
1901
+ headerColor={general.headerColor}
1902
+ columns={state.columns}
1903
+ showDownloadButton={general.showDownloadButton}
1904
+ showFullGeoNameInCSV={table.showFullGeoNameInCSV}
1905
+ runtimeLegend={runtimeLegend}
1906
+ runtimeData={runtimeData}
1907
+ displayDataAsText={displayDataAsText}
1908
+ displayGeoName={displayGeoName}
1909
+ applyLegendToRow={applyLegendToRow}
1910
+ tableTitle={table.label}
1911
+ indexTitle={table.indexLabel}
1912
+ vizTitle={general.title}
1913
+ viewport={currentViewport}
1914
+ formatLegendLocation={formatLegendLocation}
1915
+ setFilteredCountryCode={setFilteredCountryCode}
1916
+ tabbingId={tabId}
1917
+ showDownloadImgButton={state.general.showDownloadImgButton}
1918
+ showDownloadPdfButton={state.general.showDownloadPdfButton}
1919
+ innerContainerRef={innerContainerRef}
1920
+ outerContainerRef={outerContainerRef}
1921
+ imageRef={imageId}
1922
+ isDebug={isDebug}
1923
+ wrapColumns={table.wrapColumns}
1924
+ />
1925
+ )}
1926
+
1927
+ {state.annotations.length > 0 && <Annotation.Dropdown />}
1928
+
1929
+ {state.annotations.length > 0 && <Annotation.Dropdown />}
1781
1930
 
1782
1931
  {general.footnotes && <section className='footnotes'>{parse(general.footnotes)}</section>}
1783
1932
  </section>
@@ -1786,6 +1935,28 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1786
1935
  <div aria-live='assertive' className='cdcdataviz-sr-only'>
1787
1936
  {accessibleStatus}
1788
1937
  </div>
1938
+
1939
+ {!isDraggingAnnotation &&
1940
+ !window.matchMedia('(any-hover: none)').matches &&
1941
+ 'hover' === tooltips.appearanceType && (
1942
+ <ReactTooltip
1943
+ id={`tooltip__${tooltipId}`}
1944
+ float={true}
1945
+ className={`${tooltips.capitalizeLabels ? 'capitalize tooltip tooltip-test' : 'tooltip tooltip-test'}`}
1946
+ style={{ background: `rgba(255,255,255, ${state.tooltips.opacity / 100})`, color: 'black' }}
1947
+ />
1948
+ )}
1949
+ <div
1950
+ ref={tooltipRef}
1951
+ id={`tooltip__${tooltipId}-canvas`}
1952
+ className='tooltip'
1953
+ style={{
1954
+ background: `rgba(255,255,255,${state.tooltips.opacity / 100})`,
1955
+ position: 'absolute',
1956
+ whiteSpace: 'nowrap',
1957
+ display: 'none' // can't use d-none here
1958
+ }}
1959
+ ></div>
1789
1960
  </Layout.Responsive>
1790
1961
  </Layout.VisualizationWrapper>
1791
1962
  </ConfigContext.Provider>