@cdc/map 4.25.3 → 4.25.6

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 (119) hide show
  1. package/.idea/map.iml +12 -0
  2. package/.idea/modules.xml +8 -0
  3. package/.idea/vcs.xml +6 -0
  4. package/dist/cdcmap.js +31254 -32242
  5. package/examples/hex-colors.json +3 -3
  6. package/examples/m2.json +32904 -0
  7. package/examples/private/test.json +470 -1457
  8. package/examples/private/{mmr.json → wastewatermap.json} +86 -115
  9. package/index.html +36 -63
  10. package/package.json +7 -19
  11. package/src/CdcMap.tsx +56 -1552
  12. package/src/CdcMapComponent.tsx +608 -0
  13. package/src/_stories/CdcMap.Legend.Gradient.stories.tsx +10 -0
  14. package/src/_stories/CdcMap.Legend.stories.tsx +67 -0
  15. package/src/_stories/CdcMap.Table.stories.tsx +19 -0
  16. package/src/_stories/CdcMap.stories.tsx +12 -1
  17. package/src/_stories/UsaMap.NoData.stories.tsx +4 -4
  18. package/src/_stories/_mock/default-patterns.json +8 -5
  19. package/src/_stories/_mock/legend-bins.json +428 -0
  20. package/{examples/private/default-patterns.json → src/_stories/_mock/legends/legend-tests.json} +36 -131
  21. package/src/cdcMapComponent.styles.css +9 -0
  22. package/src/components/Annotation/Annotation.Draggable.tsx +27 -26
  23. package/src/components/Annotation/AnnotationDropdown.tsx +5 -6
  24. package/src/components/BubbleList.tsx +135 -49
  25. package/src/components/CityList.tsx +89 -87
  26. package/src/components/DataTable.tsx +8 -8
  27. package/src/components/EditorPanel/components/EditorPanel.tsx +823 -885
  28. package/src/components/EditorPanel/components/Error.tsx +9 -2
  29. package/src/components/EditorPanel/components/HexShapeSettings.tsx +127 -141
  30. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +55 -86
  31. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +89 -75
  32. package/src/components/EditorPanel/components/editorPanel.styles.css +95 -0
  33. package/src/components/Geo.tsx +9 -1
  34. package/src/components/GoogleMap/components/GoogleMap.tsx +1 -1
  35. package/src/components/Legend/components/Legend.tsx +92 -87
  36. package/src/components/Legend/components/LegendGroup/Legend.Group.tsx +128 -0
  37. package/src/components/Legend/components/LegendGroup/legend.group.css +27 -0
  38. package/src/components/Legend/components/LegendItem.Hex.tsx +4 -1
  39. package/src/components/Legend/components/index.scss +74 -17
  40. package/src/components/Modal.tsx +17 -7
  41. package/src/components/NavigationMenu.tsx +11 -9
  42. package/src/components/UsaMap/components/SingleState/SingleState.CountyOutput.tsx +12 -8
  43. package/src/components/UsaMap/components/SingleState/SingleState.StateOutput.tsx +4 -4
  44. package/src/components/UsaMap/components/TerritoriesSection.tsx +33 -10
  45. package/src/components/UsaMap/components/Territory/Territory.Hexagon.tsx +12 -10
  46. package/src/components/UsaMap/components/Territory/Territory.Rectangle.tsx +12 -14
  47. package/src/components/UsaMap/components/Territory/TerritoryShape.ts +2 -1
  48. package/src/components/UsaMap/components/UsaMap.County.tsx +138 -96
  49. package/src/components/UsaMap/components/UsaMap.Region.styles.css +72 -0
  50. package/src/components/UsaMap/components/UsaMap.Region.tsx +56 -103
  51. package/src/components/UsaMap/components/UsaMap.SingleState.styles.css +10 -0
  52. package/src/components/UsaMap/components/UsaMap.SingleState.tsx +65 -74
  53. package/src/components/UsaMap/components/UsaMap.State.tsx +112 -91
  54. package/src/components/UsaMap/helpers/map.ts +1 -1
  55. package/src/components/UsaMap/helpers/shapes.ts +20 -7
  56. package/src/components/WorldMap/WorldMap.tsx +64 -118
  57. package/src/components/WorldMap/worldMap.styles.css +28 -0
  58. package/src/components/ZoomControls.tsx +15 -13
  59. package/src/components/zoomControls.styles.css +53 -0
  60. package/src/context.ts +17 -9
  61. package/src/data/initial-state.js +5 -2
  62. package/src/helpers/addUIDs.ts +150 -0
  63. package/src/helpers/applyColorToLegend.ts +39 -64
  64. package/src/helpers/applyLegendToRow.ts +51 -0
  65. package/src/helpers/colorDistributions.ts +12 -0
  66. package/src/helpers/constants.ts +44 -0
  67. package/src/helpers/displayGeoName.ts +9 -2
  68. package/src/helpers/formatLegendLocation.ts +3 -2
  69. package/src/helpers/generateColorsArray.ts +2 -1
  70. package/src/helpers/generateRuntimeData.ts +78 -0
  71. package/src/helpers/generateRuntimeFilters.ts +63 -0
  72. package/src/helpers/generateRuntimeLegend.ts +566 -0
  73. package/src/helpers/generateRuntimeLegendHash.ts +16 -15
  74. package/src/helpers/getColumnNames.ts +19 -0
  75. package/src/helpers/getMapContainerClasses.ts +23 -0
  76. package/src/helpers/getStatePicked.ts +8 -0
  77. package/src/helpers/handleMapTabbing.ts +31 -0
  78. package/src/helpers/hashObj.ts +1 -1
  79. package/src/helpers/index.ts +22 -0
  80. package/src/helpers/navigationHandler.ts +3 -3
  81. package/src/helpers/resetLegendToggles.ts +13 -0
  82. package/src/helpers/setBinNumbers.ts +5 -0
  83. package/src/helpers/sortSpecialClassesLast.ts +7 -0
  84. package/src/helpers/tests/getColumnNames.test.ts +52 -0
  85. package/src/helpers/titleCase.ts +1 -1
  86. package/src/helpers/toggleLegendActive.ts +25 -0
  87. package/src/hooks/useApplyTooltipsToGeo.tsx +51 -0
  88. package/src/hooks/useColumnsRequiredChecker.ts +51 -0
  89. package/src/hooks/useGeoClickHandler.ts +45 -0
  90. package/src/hooks/useLegendSeparators.ts +26 -0
  91. package/src/hooks/useMapLayers.tsx +34 -60
  92. package/src/hooks/useModal.ts +22 -0
  93. package/src/hooks/useResizeObserver.ts +4 -5
  94. package/src/hooks/useStateZoom.tsx +52 -75
  95. package/src/hooks/useTooltip.ts +2 -3
  96. package/src/index.jsx +3 -9
  97. package/src/scss/editor-panel.scss +3 -99
  98. package/src/scss/main.scss +1 -19
  99. package/src/scss/map.scss +15 -220
  100. package/src/store/map.actions.ts +46 -0
  101. package/src/store/map.reducer.ts +96 -0
  102. package/src/types/Annotations.ts +24 -0
  103. package/src/types/MapConfig.ts +23 -3
  104. package/src/types/MapContext.ts +36 -35
  105. package/src/types/Modal.ts +1 -0
  106. package/src/types/RuntimeData.ts +3 -0
  107. package/examples/private/DEV-9644.json +0 -184
  108. package/examples/private/DEV-9989.json +0 -229
  109. package/examples/private/ardi.json +0 -180
  110. package/examples/private/colors 2.json +0 -416
  111. package/examples/private/colors.json +0 -416
  112. package/examples/private/colors.json.zip +0 -0
  113. package/examples/private/customColors.json +0 -45348
  114. package/examples/test.json +0 -183
  115. package/src/helpers/closeModal.ts +0 -9
  116. package/src/scss/btn.scss +0 -69
  117. package/src/scss/filters.scss +0 -27
  118. package/src/scss/variables.scss +0 -1
  119. /package/src/hooks/{useActiveElement.js → useActiveElement.ts} +0 -0
package/src/CdcMap.tsx CHANGED
@@ -1,1143 +1,56 @@
1
- // Vendor
2
- import React, { useState, useEffect, useRef, useCallback, useId } from 'react'
3
- import * as d3 from 'd3'
4
- import _ from 'lodash'
5
- import 'whatwg-fetch'
6
- import { Tooltip as ReactTooltip } from 'react-tooltip'
7
- import Papa from 'papaparse'
8
- import parse from 'html-react-parser'
9
- import 'react-tooltip/dist/react-tooltip.css'
10
-
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'
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'
22
- import { displayGeoName } from './helpers/displayGeoName'
23
-
24
- // Data
25
- import { countryCoordinates } from './data/country-coordinates'
26
- import {
27
- supportedCities,
28
- supportedCounties,
29
- supportedCountries,
30
- supportedRegions,
31
- supportedStates,
32
- supportedStatesFipsCodes,
33
- supportedTerritories
34
- } from './data/supported-geos'
35
- import colorPalettes from '@cdc/core/data/colorPalettes'
36
- import initialState from './data/initial-state'
37
-
38
- // Assets
39
- import ExternalIcon from './images/external-link.svg'
40
-
41
- // Sass
42
- import './scss/main.scss'
43
- import './scss/btn.scss'
44
-
45
- // Core Helpers
46
- import coveUpdateWorker from '@cdc/core/helpers/coveUpdateWorker'
1
+ import React, { useState, useEffect, useContext } from 'react'
2
+ import CdcMapComponent from './CdcMapComponent'
47
3
  import fetchRemoteData from '@cdc/core/helpers/fetchRemoteData'
48
- import isDomainExternal from '@cdc/core/helpers/isDomainExternal'
49
- import numberFromString from '@cdc/core/helpers/numberFromString'
50
4
  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 { indexOfIgnoreType } from './helpers/indexOfIgnoreType'
66
-
67
- // Child Components
68
- import Annotation from './components/Annotation'
69
- import ConfigContext from './context'
70
- import EditorPanel from './components/EditorPanel'
71
- import Error from './components/EditorPanel/components/Error'
72
- import Legend from './components/Legend'
73
- import Modal from './components/Modal'
74
- import NavigationMenu from './components/NavigationMenu'
75
- import UsaMap from './components/UsaMap'
76
- import WorldMap from './components/WorldMap'
77
- import GoogleMap from './components/GoogleMap'
78
-
79
- // hooks
80
- import useTooltip from './hooks/useTooltip'
81
- import useResizeObserver from './hooks/useResizeObserver'
82
- import { formatLegendLocation } from './helpers/formatLegendLocation'
83
-
84
- // Data props
85
- const stateKeys = Object.keys(supportedStates)
86
- const territoryKeys = Object.keys(supportedTerritories)
87
- const regionKeys = Object.keys(supportedRegions)
88
- const countryKeys = Object.keys(supportedCountries)
89
- const countyKeys = Object.keys(supportedCounties)
90
- const cityKeys = Object.keys(supportedCities)
5
+ import initialState from './data/initial-state'
6
+ import coveUpdateWorker from '@cdc/core/helpers/coveUpdateWorker'
7
+ import { addUIDs, validateFipsCodeLength } from './helpers'
8
+ import EditorContext from '@cdc/editor/src/ConfigContext'
9
+ import { MapConfig } from './types/MapConfig'
10
+ import _, { set } from 'lodash'
11
+
12
+ type CdcMapProps = {
13
+ config: MapConfig
14
+ configUrl?: string
15
+ isEditor?: boolean
16
+ isDashboard?: boolean
17
+ link?: string
18
+ logo?: string
19
+ navigationHandler: Function
20
+ setConfig: Function
21
+ }
91
22
 
92
- const CdcMap = ({
93
- config,
23
+ const CdcMap: React.FC<CdcMapProps> = ({
94
24
  navigationHandler: customNavigationHandler,
95
- isDashboard = false,
96
- isEditor = false,
97
- isDebug = false,
25
+ isEditor,
26
+ isDashboard,
98
27
  configUrl,
99
28
  logo = '',
100
- setConfig,
101
- setSharedFilter,
102
- setSharedFilterValue,
103
- link
29
+ link,
30
+ config: editorsConfig
104
31
  }) => {
105
- const transform = new DataTransform()
106
- const [translate, setTranslate] = useState([0, 0])
107
- const [scale, setScale] = useState(1)
108
- const [state, setState] = useState<MapConfig>({ ...initialState })
109
- const [isDraggingAnnotation, setIsDraggingAnnotation] = useState(false)
110
- const [loading, setLoading] = useState(true)
111
- const [displayPanel, setDisplayPanel] = useState(true)
112
- const [topoData, setTopoData] = useState<{}>({})
113
- const [runtimeFilters, setRuntimeFilters] = useState([])
114
- const [runtimeData, setRuntimeData] = useState({ init: true })
115
- const _setRuntimeData = (data: any) => {
116
- if (config) {
117
- setRuntimeData(data)
118
- } else {
119
- setRuntimeFilters(data)
120
- }
121
- }
122
- const [runtimeLegend, setRuntimeLegend] = useState([])
123
- const [stateToShow, setStateToShow] = useState(null)
124
- const [modal, setModal] = useState(null)
125
- const [accessibleStatus, setAccessibleStatus] = useState('')
126
- const [filteredCountryCode, setFilteredCountryCode] = useState()
127
- const [position, setPosition] = useState(state.mapPosition)
128
- const [coveLoadedHasRan, setCoveLoadedHasRan] = useState(false)
129
- const [imageId, setImageId] = useState(`cove-${Math.random().toString(16).slice(-4)}`) // eslint-disable-line
130
- const [requiredColumns, setRequiredColumns] = useState(null) // Simple state so we know if we need more information before parsing the map
131
- const [projection, setProjection] = useState(null)
132
- const { currentViewport, dimensions, container, outerContainerRef } = useResizeObserver(isEditor)
133
-
134
- const legendRef = useRef(null)
135
- const tooltipRef = useRef(null)
136
- const legendId = useId()
137
- // create random tooltipId
138
- const tooltipId = `${Math.random().toString(16).slice(-4)}`
139
- const mapId = useId()
140
-
141
- const { handleSorting } = useFilters({ config: state, setConfig: setState })
142
- let legendMemo = useRef(new Map())
143
- let legendSpecialClassLastMemo = useRef(new Map())
144
- let innerContainerRef = useRef()
145
-
146
- if (isDebug) console.log('CdcMap state=', state) // <eslint-disable-line></eslint-disable-line>
147
-
148
- const handleDragStateChange = isDragging => {
149
- setIsDraggingAnnotation(isDragging)
150
- }
151
-
152
- const columnsRequiredChecker = useCallback(() => {
153
- let columnList = []
154
-
155
- // Geo is always required
156
- if ('' === state.columns.geo.name) {
157
- columnList.push('Geography')
158
- }
159
-
160
- // Primary is required if we're on a data map or a point map
161
- if ('navigation' !== state.general.type && '' === state.columns.primary.name) {
162
- columnList.push('Primary')
163
- }
164
-
165
- // Navigate is required for navigation maps
166
- if ('navigation' === state.general.type && '' === state.columns.navigate.name) {
167
- columnList.push('Navigation')
168
- }
169
-
170
- if (
171
- ('us-geocode' === state.general.type || 'world-geocode' === state.general.type) &&
172
- '' === state.columns.latitude.name
173
- ) {
174
- columnList.push('Latitude')
175
- }
176
-
177
- if (
178
- ('us-geocode' === state.general.type || 'world-geocode' === state.general.type) &&
179
- '' === state.columns.longitude.name
180
- ) {
181
- columnList.push('Longitude')
182
- }
183
-
184
- if (columnList.length === 0) columnList = null
185
-
186
- setRequiredColumns(columnList)
187
- }, [state.columns, state.general.type])
188
-
189
- useEffect(() => {
190
- try {
191
- if (filteredCountryCode) {
192
- const coordinates = countryCoordinates[filteredCountryCode]
193
- const long = coordinates[1]
194
- const lat = coordinates[0]
195
- const reversedCoordinates: Coordinate = [long, lat]
196
-
197
- setState({
198
- ...state,
199
- mapPosition: { coordinates: reversedCoordinates, zoom: 3 }
200
- })
201
- }
202
- } catch (e) {
203
- console.error('COVE: Failed to set world map zoom.') // eslint-disable-line
204
- }
205
- }, [filteredCountryCode]) // eslint-disable-line
206
-
207
- useEffect(() => {
208
- setTimeout(() => {
209
- if (filteredCountryCode) {
210
- const filteredCountryObj = runtimeData[filteredCountryCode]
211
- const tmpData = {
212
- [filteredCountryCode]: filteredCountryObj
213
- }
214
- setRuntimeData(tmpData)
215
- }
216
- }, 100)
217
- }, [filteredCountryCode]) // eslint-disable-line
218
-
219
- useEffect(() => {
220
- if (state.mapPosition) {
221
- setPosition(state.mapPosition)
222
- }
223
- }, [state.mapPosition, setPosition])
224
-
225
- // 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
226
- // We are mutating state in place here (depending on where called) - but it's okay, this isn't used for rerender
227
- // eslint-disable-next-line
228
- const addUIDs = useCallback((configObj, fromColumn) => {
229
- configObj.data.forEach(row => {
230
- let uid = null
231
-
232
- if (row.uid) row.uid = null // Wipe existing UIDs
233
-
234
- // United States check
235
- if ('us' === configObj.general.geoType && configObj.columns.geo.name) {
236
- // const geoName = row[configObj.columns.geo.name] && typeof row[configObj.columns.geo.name] === "string" ? row[configObj.columns.geo.name].toUpperCase() : '';
237
- let geoName = ''
238
-
239
- if (row[configObj.columns.geo.name] !== undefined && row[configObj.columns.geo.name] !== null) {
240
- geoName = String(row[configObj.columns.geo.name])
241
- geoName = geoName.toUpperCase()
242
- }
243
-
244
- // States
245
- uid = stateKeys.find(key => supportedStates[key].includes(geoName))
246
-
247
- // Territories
248
- if (!uid) {
249
- uid = territoryKeys.find(key => supportedTerritories[key].includes(geoName))
250
- }
251
-
252
- // Cities
253
- if (!uid && geoName) {
254
- uid = cityKeys.find(key => key === geoName.toUpperCase())
255
- }
256
-
257
- if (state.general.displayAsHex) {
258
- const upperCaseKey = geoName.toUpperCase()
259
- const supportedDc = [
260
- 'WASHINGTON D.C.',
261
- 'DISTRICT OF COLUMBIA',
262
- 'WASHINGTON DC',
263
- 'DC',
264
- 'WASHINGTON DC.',
265
- 'D.C.',
266
- 'D.C'
267
- ]
268
- if (supportedDc.includes(upperCaseKey)) {
269
- uid = 'US-DC'
270
- }
271
- }
272
- }
273
-
274
- if ('us-region' === configObj.general.geoType && configObj.columns.geo.name) {
275
- // const geoName = row[configObj.columns.geo.name] && typeof row[configObj.columns.geo.name] === "string" ? row[configObj.columns.geo.name].toUpperCase() : '';
276
- let geoName = ''
277
-
278
- if (row[configObj.columns.geo.name] !== undefined && row[configObj.columns.geo.name] !== null) {
279
- geoName = String(row[configObj.columns.geo.name])
280
- geoName = geoName.toUpperCase()
281
- }
282
-
283
- // Regions
284
- uid = regionKeys.find(key => supportedRegions[key].includes(geoName))
285
- }
286
-
287
- // World Check
288
- if ('world' === configObj.general.geoType) {
289
- const geoName = row[configObj.columns.geo.name]
290
-
291
- uid = countryKeys.find(key => supportedCountries[key].includes(geoName))
292
-
293
- // Cities
294
- if (!uid && 'world-geocode' === state.general.type) {
295
- uid = cityKeys.find(key => key === geoName?.toUpperCase())
296
- }
297
-
298
- // Cities
299
- if (!uid && geoName) {
300
- uid = cityKeys.find(key => key === geoName.toUpperCase())
301
- }
302
- }
303
-
304
- // County Check
305
- if (
306
- ('us-county' === configObj.general.geoType || 'single-state' === configObj.general.geoType) &&
307
- 'us-geocode' !== configObj.general.type
308
- ) {
309
- const fips = row[configObj.columns.geo.name]
310
- uid = countyKeys.find(key => key === fips)
311
- }
312
-
313
- if ('us-geocode' === state.general.type) {
314
- uid = row[state.columns.geo.name]
315
- }
316
-
317
- if (
318
- !uid &&
319
- state.columns.latitude?.name &&
320
- state.columns.longitude?.name &&
321
- row[state.columns.latitude?.name] &&
322
- row[state.columns.longitude?.name]
323
- ) {
324
- uid = `${row[state.columns.geo.name]}`
325
- }
326
-
327
- if (uid) {
328
- Object.defineProperty(row, 'uid', {
329
- value: uid,
330
- writable: true
331
- })
332
- }
333
- })
334
-
335
- configObj.data.fromColumn = fromColumn
336
- })
337
-
338
- // eslint-disable-next-line
339
- const generateRuntimeLegend = useCallback((configObj, runtimeFilters, hash) => {
340
- const newLegendMemo = new Map() // Reset memoization
341
- const newLegendSpecialClassLastMemo = new Map() // Reset bin memoization
342
- let primaryCol = configObj.columns.primary.name,
343
- isBubble = configObj.general.type === 'bubble',
344
- categoricalCol = configObj.columns.categorical ? configObj.columns.categorical.name : undefined,
345
- type = configObj.legend.type,
346
- number = configObj.legend.numberOfItems,
347
- result = []
348
-
349
- // Add a hash for what we're working from if passed
350
- if (hash) {
351
- result.fromHash = hash
352
- }
353
-
354
- result.runtimeDataHash = runtimeFilters?.fromHash
355
-
356
- // Unified will base the legend off ALL of the data maps received. Otherwise, it will use
357
- let dataSet = configObj.legend.unified ? configObj.data : Object.values(runtimeData)
358
- let specialClasses = 0
359
- let specialClassesHash = {}
360
-
361
- // Special classes
362
- if (configObj.legend.specialClasses.length) {
363
- if (typeof configObj.legend.specialClasses[0] === 'object') {
364
- configObj.legend.specialClasses.forEach(specialClass => {
365
- dataSet = dataSet.filter(row => {
366
- const val = String(row[specialClass.key])
367
-
368
- if (specialClass.value === val) {
369
- if (undefined === specialClassesHash[val]) {
370
- specialClassesHash[val] = true
371
-
372
- result.push({
373
- special: true,
374
- value: val,
375
- label: specialClass.label
376
- })
377
-
378
- result[result.length - 1].color = applyColorToLegend(result.length - 1, state, result)
379
-
380
- specialClasses += 1
381
- }
382
-
383
- let specialColor: number
32
+ const editorContext = useContext(EditorContext)
33
+ const [config, _setConfig] = useState(editorsConfig ?? null)
384
34
 
385
- // color the state if val is in row
386
- specialColor = result.findIndex(p => p.value === val)
387
-
388
- newLegendMemo.set(hashObj(row), specialColor)
389
-
390
- return false
391
- }
392
-
393
- return true
394
- })
395
- })
396
- } else {
397
- dataSet = dataSet.filter(row => {
398
- const val = row[primaryCol]
399
-
400
- if (configObj.legend.specialClasses.includes(val)) {
401
- // apply the special color to the legend
402
- if (undefined === specialClassesHash[val]) {
403
- specialClassesHash[val] = true
404
-
405
- result.push({
406
- special: true,
407
- value: val
408
- })
409
-
410
- result[result.length - 1].color = applyColorToLegend(result.length - 1, state, result)
411
-
412
- specialClasses += 1
413
- }
414
-
415
- let specialColor = ''
416
-
417
- // color the state if val is in row
418
- if (Object.values(row).includes(val)) {
419
- specialColor = result.findIndex(p => p.value === val)
420
- }
421
-
422
- newLegendMemo.set(hashObj(row), specialColor)
423
-
424
- return false
425
- }
426
-
427
- return true
428
- })
429
- }
430
- }
431
-
432
- // Category
433
- if ('category' === type) {
434
- let uniqueValues = new Map()
435
- let count = 0
436
-
437
- for (let i = 0; i < dataSet.length; i++) {
438
- let row = dataSet[i]
439
- let value = isBubble && categoricalCol && row[categoricalCol] ? row[categoricalCol] : row[primaryCol]
440
- if (undefined === value) continue
441
-
442
- if (false === uniqueValues.has(value)) {
443
- uniqueValues.set(value, [hashObj(row)])
444
- count++
445
- } else {
446
- uniqueValues.get(value).push(hashObj(row))
447
- }
448
- }
449
-
450
- let sorted = [...uniqueValues.keys()]
451
-
452
- if (configObj.legend.additionalCategories) {
453
- configObj.legend.additionalCategories.forEach(additionalCategory => {
454
- if (additionalCategory && indexOfIgnoreType(sorted, additionalCategory) === -1) {
455
- sorted.push(additionalCategory)
456
- }
457
- })
458
- }
459
-
460
- // Apply custom sorting or regular sorting
461
- let configuredOrder = configObj.legend.categoryValuesOrder ?? []
462
-
463
- if (configuredOrder.length) {
464
- sorted.sort((a, b) => {
465
- let aVal = configuredOrder.indexOf(a)
466
- let bVal = configuredOrder.indexOf(b)
467
- if (aVal === bVal) return 0
468
- if (aVal === -1) return 1
469
- if (bVal === -1) return -1
470
- return aVal - bVal
471
- })
472
- } else {
473
- sorted.sort((a, b) => a - b)
474
- }
475
-
476
- // Add legend item for each
477
- sorted.forEach(val => {
478
- result.push({
479
- value: val
480
- })
481
-
482
- let lastIdx = result.length - 1
483
- let arr = uniqueValues.get(val)
484
-
485
- if (arr) {
486
- arr.forEach(hashedRow => newLegendMemo.set(hashedRow, lastIdx))
487
- }
488
- })
489
-
490
- // Add color to new legend item
491
- for (let i = 0; i < result.length; i++) {
492
- result[i].color = applyColorToLegend(i, state, result)
493
- }
494
-
495
- legendMemo.current = newLegendMemo
496
-
497
- // before returning the legend result
498
- // add property for bin number and set to index location
499
- result.forEach((row, i) => {
500
- row.bin = i // set bin number to index
501
- })
502
-
503
- // Move all special legend items from "Special Classes" to the end of the legend
504
- if (state.legend.showSpecialClassesLast) {
505
- let specialRows = result.filter(d => d.special === true)
506
- let otherRows = result.filter(d => !d.special)
507
- result = [...otherRows, ...specialRows]
508
- }
509
-
510
- const assignSpecialClassLastIndex = (value, key) => {
511
- const newIndex = result.findIndex(d => d.bin === value)
512
- newLegendSpecialClassLastMemo.set(key, newIndex)
513
- }
514
- newLegendMemo.forEach(assignSpecialClassLastIndex)
515
- legendSpecialClassLastMemo.current = newLegendSpecialClassLastMemo
516
-
517
- // filter special classes from results
518
- const specialValues = result.filter(d => d.special).map(d => d.value)
519
- return result.filter(d => d.special || !specialValues.includes(d.value))
520
- }
521
-
522
- let uniqueValues = {}
523
- dataSet.forEach(datum => {
524
- uniqueValues[datum[primaryCol]] = true
525
- })
526
-
527
- let legendNumber = Math.min(number, Object.keys(uniqueValues).length)
528
-
529
- // Separate zero
530
- if (true === configObj.legend.separateZero && !state.general.equalNumberOptIn) {
531
- let addLegendItem = false
532
-
533
- for (let i = 0; i < dataSet.length; i++) {
534
- if (dataSet[i][primaryCol] === 0) {
535
- addLegendItem = true
536
-
537
- let row = dataSet.splice(i, 1)[0]
538
-
539
- newLegendMemo.set(hashObj(row), result.length)
540
- i--
541
- }
542
- }
543
-
544
- if (addLegendItem) {
545
- legendNumber -= 1 // This zero takes up one legend item
546
-
547
- // Add new legend item
548
- result.push({
549
- min: 0,
550
- max: 0
551
- })
552
-
553
- let lastIdx = result.length - 1
554
-
555
- // Add color to new legend item
556
- result[lastIdx].color = applyColorToLegend(lastIdx, state, result)
557
- }
558
- }
559
-
560
- // Sort data for use in equalnumber or equalinterval
561
- if (state.general.type !== 'us-geocode') {
562
- dataSet = dataSet
563
- .filter(row => typeof row[primaryCol] === 'number')
564
- .sort((a, b) => {
565
- let aNum = a[primaryCol]
566
- let bNum = b[primaryCol]
567
-
568
- return aNum - bNum
569
- })
35
+ const setConfig = newConfig => {
36
+ _setConfig(newConfig)
37
+ if (isEditor && !isDashboard) {
38
+ editorContext.setTempConfig(newConfig)
570
39
  }
571
-
572
- // Equal Number
573
- if (type === 'equalnumber') {
574
- // start work on changing legend functionality
575
- // FALSE === ignore old version for now.
576
- if (!state.general.equalNumberOptIn) {
577
- let numberOfRows = dataSet.length
578
-
579
- let remainder
580
- let changingNumber = legendNumber
581
-
582
- let chunkAmt
583
-
584
- // Loop through the array until it has been split into equal subarrays
585
- while (numberOfRows > 0) {
586
- remainder = numberOfRows % changingNumber
587
- chunkAmt = Math.floor(numberOfRows / changingNumber)
588
-
589
- if (remainder > 0) {
590
- chunkAmt += 1
591
- }
592
-
593
- let removedRows = dataSet.splice(0, chunkAmt)
594
-
595
- let min = removedRows[0][primaryCol],
596
- max = removedRows[removedRows.length - 1][primaryCol]
597
-
598
- // eslint-disable-next-line
599
- removedRows.forEach(row => {
600
- newLegendMemo.set(hashObj(row), result.length)
601
- })
602
-
603
- result.push({
604
- min,
605
- max
606
- })
607
-
608
- result[result.length - 1].color = applyColorToLegend(result.length - 1, state, result)
609
-
610
- changingNumber -= 1
611
- numberOfRows -= chunkAmt
612
- }
613
- } else {
614
- // get nums
615
- let domainNums = new Set(dataSet.map(item => item[state.columns.primary.name]))
616
-
617
- domainNums = d3.extent(domainNums)
618
-
619
- let colors = colorPalettes[state.color]
620
- let colorRange = colors.slice(0, state.legend.numberOfItems)
621
-
622
- const getDomain = () => {
623
- // backwards compatibility
624
- if (state?.columns?.primary?.roundToPlace !== undefined && state?.general?.equalNumberOptIn) {
625
- return _.uniq(
626
- dataSet.map(item =>
627
- Number(item[state.columns.primary.name]).toFixed(Number(state?.columns?.primary?.roundToPlace))
628
- )
629
- )
630
- }
631
- return _.uniq(dataSet.map(item => Math.round(Number(item[state.columns.primary.name]))))
632
- }
633
-
634
- const getBreaks = scale => {
635
- // backwards compatibility
636
- if (state?.columns?.primary?.roundToPlace !== undefined && state?.general?.equalNumberOptIn) {
637
- return scale.quantiles().map(b => Number(b)?.toFixed(Number(state?.columns?.primary?.roundToPlace)))
638
- }
639
- return scale.quantiles().map(item => Number(Math.round(item)))
640
- }
641
-
642
- let scale = d3
643
- .scaleQuantile()
644
- .domain(getDomain()) // min/max values
645
- .range(colorRange) // set range to our colors array
646
-
647
- const breaks = getBreaks(scale)
648
-
649
- // if separating zero force it into breaks
650
- if (breaks[0] !== 0) {
651
- breaks.unshift(0)
652
- }
653
-
654
- // eslint-disable-next-line array-callback-return
655
- breaks.map((item, index) => {
656
- const setMin = index => {
657
- let min = breaks[index]
658
-
659
- // if first break is a seperated zero, min is zero
660
- if (index === 0 && state.legend.separateZero) {
661
- min = 0
662
- }
663
-
664
- // if we're on the second break, and separating out zero, increment min to 1.
665
- if (index === 1 && state.legend.separateZero) {
666
- min = 1
667
- }
668
-
669
- // // in starting position and zero in the data
670
- // if((index === state.legend.specialClasses?.length ) && (state.legend.specialClasses.length !== 0)) {
671
- // min = breaks[index]
672
- // }
673
- return min
674
- }
675
-
676
- const getDecimalPlace = n => {
677
- return Math.pow(10, -n)
678
- }
679
-
680
- const setMax = index => {
681
- let max = Number(breaks[index + 1]) - getDecimalPlace(Number(state?.columns?.primary?.roundToPlace))
682
-
683
- if (index === 0 && state.legend.separateZero) {
684
- max = 0
685
- }
686
-
687
- if (index + 1 === breaks.length) {
688
- max = domainNums[1]
689
- }
690
-
691
- return max
692
- }
693
-
694
- let min = setMin(index)
695
- let max = setMax(index, min)
696
-
697
- result.push({
698
- min,
699
- max,
700
- color: scale(item)
701
- })
702
-
703
- dataSet.forEach(row => {
704
- let number = row[state.columns.primary.name]
705
- let updated = result.length - 1
706
-
707
- if (result[updated]?.min === (null || undefined) || result[updated]?.max === (null || undefined)) return
708
-
709
- if (number >= result[updated].min && number <= result[updated].max) {
710
- newLegendMemo.set(hashObj(row), updated)
711
- }
712
- })
713
- })
714
- }
715
- }
716
-
717
- // Equal Interval
718
- if (type === 'equalinterval' && dataSet?.length !== 0) {
719
- if (!dataSet || dataSet.length === 0) {
720
- setState({
721
- ...state,
722
- runtime: {
723
- ...state.runtime,
724
- editorErrorMessage: 'Error setting equal interval legend type'
725
- }
726
- })
727
- return
728
- }
729
- dataSet = dataSet.filter(row => row[primaryCol] !== undefined)
730
- let dataMin = dataSet[0][primaryCol]
731
- let dataMax = dataSet[dataSet.length - 1][primaryCol]
732
-
733
- let pointer = 0 // Start at beginning of dataSet
734
-
735
- for (let i = 0; i < legendNumber; i++) {
736
- let interval = Math.abs(dataMax - dataMin) / legendNumber
737
-
738
- let min = dataMin + interval * i
739
- let max = min + interval
740
-
741
- // If this is the last loop, assign actual max of data as the end point
742
- if (i === legendNumber - 1) max = dataMax
743
-
744
- // Add rows in dataSet that belong to this new legend item since we've got the data sorted
745
- while (pointer < dataSet.length && dataSet[pointer][primaryCol] <= max) {
746
- newLegendMemo.set(hashObj(dataSet[pointer]), result.length)
747
- pointer += 1
748
- }
749
-
750
- let range = {
751
- min: Math.round(min * 100) / 100,
752
- max: Math.round(max * 100) / 100
753
- }
754
-
755
- result.push(range)
756
-
757
- result[result.length - 1].color = applyColorToLegend(result.length - 1, state, result)
758
- }
759
- }
760
-
761
- result.forEach((legendItem, idx) => {
762
- legendItem.color = applyColorToLegend(idx, state, result)
763
- })
764
-
765
- legendMemo.current = newLegendMemo
766
-
767
- if (state.general.geoType === 'world') {
768
- const runtimeDataKeys = Object.keys(runtimeFilters)
769
- const isCountriesWithNoDataState =
770
- configObj.data === undefined ? false : !countryKeys.every(countryKey => runtimeDataKeys.includes(countryKey))
771
-
772
- if (result.length > 0 && isCountriesWithNoDataState) {
773
- result.push({
774
- min: null,
775
- max: null,
776
- color: getGeoFillColor(state)
777
- })
778
- }
779
- }
780
-
781
- //----------
782
- // DEV-784
783
- // before returning the legend result
784
- // add property for bin number and set to index location
785
- result.forEach((row, i) => {
786
- row.bin = i // set bin number to index
787
- })
788
-
789
- // Move all special legend items from "Special Classes" to the end of the legend
790
- if (state.legend.showSpecialClassesLast) {
791
- let specialRows = result.filter(d => d.special === true)
792
- let otherRows = result.filter(d => !d.special)
793
- result = [...otherRows, ...specialRows]
794
- }
795
- //-----------
796
-
797
- const assignSpecialClassLastIndex = (value, key) => {
798
- const newIndex = result.findIndex(d => d.bin === value)
799
- newLegendSpecialClassLastMemo.set(key, newIndex)
800
- }
801
- newLegendMemo.forEach(assignSpecialClassLastIndex)
802
- legendSpecialClassLastMemo.current = newLegendSpecialClassLastMemo
803
-
804
- return result
805
- })
806
-
807
- // eslint-disable-next-line
808
- const generateRuntimeFilters = useCallback((configObj, hash, runtimeFilters) => {
809
- if (typeof configObj === 'undefined' || undefined === configObj.filters || configObj.filters.length === 0) return []
810
-
811
- let filters = []
812
-
813
- if (hash) filters.fromHash = hash
814
-
815
- configObj?.filters.forEach(
816
- (
817
- {
818
- columnName,
819
- label,
820
- labels,
821
- queryParameter,
822
- orderedValues,
823
- active,
824
- values,
825
- type,
826
- showDropdown,
827
- setByQueryParameter
828
- },
829
- idx
830
- ) => {
831
- let newFilter = runtimeFilters[idx]
832
-
833
- const sort = (a, b) => {
834
- const asc = configObj.filters[idx].order !== 'desc'
835
- return String(asc ? a : b).localeCompare(String(asc ? b : a), 'en', { numeric: true })
836
- }
837
-
838
- if (type !== 'url') {
839
- values = getUniqueValues(state.data, columnName)
840
-
841
- if (configObj.filters[idx].order === 'cust') {
842
- if (configObj.filters[idx]?.values.length > 0) {
843
- values = configObj.filters[idx].values
844
- }
845
- } else {
846
- values = values.sort(sort)
847
- }
848
- }
849
-
850
- if (undefined === newFilter) {
851
- newFilter = {}
852
- }
853
-
854
- newFilter.order = configObj.filters[idx].order ? configObj.filters[idx].order : 'asc'
855
- newFilter.type = type
856
- newFilter.label = label ?? ''
857
- newFilter.columnName = columnName
858
- newFilter.orderedValues = orderedValues
859
- newFilter.queryParameter = queryParameter
860
- newFilter.labels = labels
861
- newFilter.values = values
862
- newFilter.setByQueryParameter = setByQueryParameter
863
- handleSorting(newFilter)
864
- newFilter.active = active || configObj.filters[idx].defaultValue || values[0] // Default to first found value
865
- newFilter.defaultValue = configObj.filters[idx].defaultValue || ''
866
- newFilter.filterStyle = configObj.filters[idx].filterStyle ? configObj.filters[idx].filterStyle : 'dropdown'
867
- newFilter.showDropdown = showDropdown
868
- newFilter.subGrouping = configObj.filters[idx].subGrouping
869
-
870
- filters.push(newFilter)
871
- }
872
- )
873
-
874
- return filters
875
- })
876
-
877
- // Calculates what's going to be displayed on the map and data table at render.
878
- // eslint-disable-next-line
879
- const generateRuntimeData = useCallback((configObj, filters, hash, test) => {
880
- try {
881
- const result = {}
882
-
883
- // Adding property this way prevents it from being enumerated
884
- Object.defineProperty(result, 'fromHash', {
885
- value: hash
886
- })
887
-
888
- addUIDs(configObj, configObj.columns.geo.name)
889
- configObj.data.forEach(row => {
890
- if (test) {
891
- console.log('object', configObj) // eslint-disable-line
892
- console.log('row', row) // eslint-disable-line
893
- }
894
-
895
- if (undefined === row.uid) return false // No UID for this row, we can't use for mapping
896
- const configPrimaryName = configObj.columns.primary.name
897
- if (row[configPrimaryName]) {
898
- row[configPrimaryName] = numberFromString(row[configPrimaryName], state)
899
- }
900
-
901
- // If this is a navigation only map, skip if it doesn't have a URL
902
-
903
- if ('navigation' === configObj.general.type) {
904
- let navigateUrl = row[configObj.columns.navigate.name] || ''
905
-
906
- if (undefined !== navigateUrl && typeof navigateUrl === 'string') {
907
- // Strip hidden characters before we check length
908
- navigateUrl = navigateUrl.replace(/(\r\n|\n|\r)/gm, '')
909
- }
910
- if (0 === navigateUrl?.length) {
911
- return false
912
- }
913
- }
914
-
915
- // Filters
916
- if (filters?.length) {
917
- for (let i = 0; i < filters.length; i++) {
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
- }
928
- }
929
- }
930
- // Don't add additional rows with same UID
931
- if (result[row.uid] === undefined) {
932
- result[row.uid] = row
933
- }
934
- })
935
- return result
936
- } catch (e) {
937
- console.error('COVE: ', e) // eslint-disable-line
938
- }
939
- })
940
-
941
- const mapSvg = useRef(null)
942
-
943
- // this is passed DOWN into the various components
944
- // then they do a lookup based on the bin number as index into here
945
- const applyLegendToRow = rowObj => {
946
- try {
947
- if (!rowObj) throw new Error('COVE: No rowObj in applyLegendToRow')
948
- // Navigation mapchanged
949
- if ('navigation' === state.general.type) {
950
- let mapColorPalette = colorPalettes[state.color] || colorPalettes['bluegreenreverse']
951
- return generateColorsArray(mapColorPalette[3])
952
- }
953
-
954
- let hash = hashObj(rowObj)
955
-
956
- if (legendMemo.current.has(hash)) {
957
- let idx = legendMemo.current.get(hash)
958
- let disabledIdx = idx
959
-
960
- if (state.legend.showSpecialClassesLast) {
961
- disabledIdx = legendSpecialClassLastMemo.current.get(hash)
962
- }
963
-
964
- if (runtimeLegend[disabledIdx]?.disabled) return false
965
-
966
- // changed to use bin prop to get color instead of idx
967
- // bc we re-order legend when showSpecialClassesLast is checked
968
- let legendBinColor = runtimeLegend.find(o => o.bin === idx)?.color
969
- return generateColorsArray(legendBinColor, runtimeLegend[idx]?.special)
970
- }
971
-
972
- // Fail state
973
- return generateColorsArray()
974
- } catch (e) {
975
- console.error('COVE: ', e) // eslint-disable-line
976
- }
977
- }
978
-
979
- // This resets all active legend toggles.
980
- const resetLegendToggles = async () => {
981
- let newLegend = [...runtimeLegend]
982
-
983
- newLegend.forEach(legendItem => {
984
- delete legendItem.disabled
985
- })
986
-
987
- newLegend.runtimeDataHash = runtimeLegend.runtimeDataHash
988
-
989
- setRuntimeLegend(newLegend)
990
- }
991
-
992
- // todo: convert to store or context eventually.
993
- const { buildTooltip } = useTooltip({ state, displayGeoName, supportedStatesFipsCodes })
994
-
995
- const applyTooltipsToGeo = (geoName, row, returnType = 'string') => {
996
- let toolTipText = buildTooltip(row, geoName, '')
997
-
998
- // We convert the markup into JSX and add a navigation link if it's going into a modal.
999
- if ('jsx' === returnType) {
1000
- toolTipText = [<div key='modal-content'>{parse(toolTipText)}</div>]
1001
-
1002
- if (state.columns.hasOwnProperty('navigate') && row[state.columns.navigate.name]) {
1003
- toolTipText.push(
1004
- // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions,jsx-a11y/anchor-is-valid
1005
- <a
1006
- href='#'
1007
- className='navigation-link'
1008
- key='modal-navigation-link'
1009
- onClick={e => {
1010
- e.preventDefault()
1011
- navigationHandler(
1012
- state.general.navigationTarget,
1013
- row[state.columns.navigate.name],
1014
- customNavigationHandler
1015
- )
1016
- }}
1017
- >
1018
- {state.tooltips.linkLabel}
1019
- {isDomainExternal(row[state.columns.navigate.name]) && <ExternalIcon className='inline-icon ms-1' />}
1020
- </a>
1021
- )
1022
- }
1023
- }
1024
-
1025
- return toolTipText
1026
40
  }
1027
41
 
1028
- const geoClickHandler = (key, value) => {
1029
- if (setSharedFilter) {
1030
- setSharedFilter(state.uid, value)
1031
- }
1032
-
1033
- // If world-geocode map zoom to geo point
1034
- if (['world-geocode'].includes(state.general.type)) {
1035
- const lat = value[state.columns.latitude.name]
1036
- const long = value[state.columns.longitude.name]
1037
-
1038
- setState({
1039
- ...state,
1040
- mapPosition: { coordinates: [long, lat], zoom: 3 }
1041
- })
1042
- }
1043
-
1044
- // If modals are set, or we are on a mobile viewport, display modal
1045
- if (window.matchMedia('(any-hover: none)').matches || 'click' === state.tooltips.appearanceType) {
1046
- setModal({
1047
- geoName: key,
1048
- keyedData: value
1049
- })
1050
-
1051
- return
1052
- }
1053
-
1054
- // Otherwise if this item has a link specified for it, do regular navigation.
1055
- if (state.columns.navigate && value[state.columns.navigate.name]) {
1056
- navigationHandler(state.general.navigationTarget, value[state.columns.navigate.name], customNavigationHandler)
1057
- }
1058
- }
1059
-
1060
- const reloadURLData = async () => {
1061
- if (state.dataUrl) {
1062
- const dataUrl = new URL(state.runtimeDataUrl || state.dataUrl, window.location.origin)
1063
- let qsParams = Object.fromEntries(new URLSearchParams(dataUrl.search))
1064
-
1065
- let isUpdateNeeded = false
1066
- state.filters.forEach(filter => {
1067
- if (filter.type === 'url' && qsParams[filter.queryParameter] !== decodeURIComponent(filter.active)) {
1068
- qsParams[filter.queryParameter] = filter.active
1069
- isUpdateNeeded = true
1070
- }
1071
- })
1072
-
1073
- if (!isUpdateNeeded) return
1074
-
1075
- let dataUrlFinal = `${dataUrl.origin}${dataUrl.pathname}${Object.keys(qsParams)
1076
- .map((param, i) => {
1077
- let qs = i === 0 ? '?' : '&'
1078
- qs += param + '='
1079
- qs += qsParams[param]
1080
- return qs
1081
- })
1082
- .join('')}`
1083
-
1084
- let data
1085
-
1086
- try {
1087
- const regex = /(?:\.([^.]+))?$/
1088
-
1089
- const ext = regex.exec(dataUrl.pathname)[1]
1090
- if ('csv' === ext || isSolrCsv(dataUrlFinal)) {
1091
- data = await fetch(dataUrlFinal)
1092
- .then(response => response.text())
1093
- .then(responseText => {
1094
- const parsedCsv = Papa.parse(responseText, {
1095
- header: true,
1096
- dynamicTyping: true,
1097
- skipEmptyLines: true,
1098
- encoding: 'utf-8'
1099
- })
1100
- return parsedCsv.data
1101
- })
1102
- } else if ('json' === ext || isSolrJson(dataUrlFinal)) {
1103
- data = await fetch(dataUrlFinal).then(response => response.json())
1104
- } else {
1105
- data = []
1106
- }
1107
- } catch (e) {
1108
- console.error(`Cannot parse URL: ${dataUrlFinal}`) // eslint-disable-line
1109
- console.log(e) // eslint-disable-line
1110
- data = []
1111
- }
1112
-
1113
- if (state.dataDescription) {
1114
- data = transform.autoStandardize(data)
1115
- data = transform.developerStandardize(data, state.dataDescription)
1116
- }
1117
-
1118
- setState({ ...state, runtimeDataUrl: dataUrlFinal, data })
1119
- }
1120
- }
42
+ const [loading, setLoading] = useState(true)
43
+ const transform = new DataTransform()
1121
44
 
1122
45
  const loadConfig = async configObj => {
1123
- // Set loading flag
1124
46
  if (!loading) setLoading(true)
47
+ const configToLoad = editorsConfig ?? configObj
1125
48
 
1126
- // Create new config object the same way each time no matter when this method is called.
1127
49
  let newState = {
1128
50
  ...initialState,
1129
- ...configObj
51
+ ...configToLoad
1130
52
  }
1131
-
1132
- const urlFilters = newState.filters
1133
- ? newState.filters.filter(filter => filter.type === 'url').length > 0
1134
- ? true
1135
- : false
1136
- : false
1137
-
1138
- if (newState.dataUrl && !urlFilters) {
1139
- // handle urls with spaces in the name.
1140
- if (newState.dataUrl) newState.dataUrl = `${newState.dataUrl}`
53
+ if (newState.dataUrl) {
1141
54
  let newData = await fetchRemoteData(newState.dataUrl, 'map')
1142
55
 
1143
56
  if (newData && newState.dataDescription) {
@@ -1155,9 +68,6 @@ const CdcMap = ({
1155
68
  newState.data = transform.developerStandardize(newState.data, newState.dataDescription)
1156
69
  }
1157
70
 
1158
- // This code goes through and adds the defaults for every property declaring in the initial state at the top.
1159
- // This allows you to easily add new properties to the config without having to worry about accounting for backwards compatibility.
1160
- // Right now this does not work recursively -- only on first and second level properties. So state -> prop1 -> childPropOne
1161
71
  Object.keys(newState).forEach(key => {
1162
72
  if ('object' === typeof newState[key] && false === Array.isArray(newState[key])) {
1163
73
  if (initialState[key]) {
@@ -1170,465 +80,59 @@ const CdcMap = ({
1170
80
  }
1171
81
  })
1172
82
 
1173
- // If there's a name for the geo, add UIDs
1174
83
  if (newState.columns.geo.name || newState.columns.geo.fips) {
1175
84
  addUIDs(newState, newState.columns.geo.name || newState.columns.geo.fips)
1176
85
  }
1177
86
 
1178
87
  if (newState.table.forceDisplay === undefined) {
1179
- newState.table.forceDisplay = !isDashboard
88
+ newState.table.forceDisplay = true
1180
89
  }
1181
90
 
1182
91
  validateFipsCodeLength(newState)
1183
92
 
1184
- // add ability to rename state properties over time.
1185
93
  const processedConfig = { ...coveUpdateWorker(newState) }
1186
94
 
1187
95
  setTimeout(() => {
1188
- setState(processedConfig)
96
+ setConfig(processedConfig)
1189
97
  setLoading(false)
1190
98
  }, 10)
1191
99
  }
1192
100
 
1193
101
  const init = async () => {
1194
- let configData = null
1195
-
1196
- // Load the configuration data passed to this component if it exists
1197
- if (config) {
1198
- configData = config
1199
- }
102
+ let _newConfig = _.cloneDeep(config ?? initialState)
1200
103
 
1201
- // If the config passed is a string, try to load it as an ajax
1202
104
  if (configUrl) {
1203
- configData = await fetchRemoteData(configUrl)
105
+ _newConfig = await fetchRemoteData(configUrl)
1204
106
  }
1205
-
1206
- // Once we have a config verify that it is an object and load it
1207
- if ('object' === typeof configData) {
1208
- loadConfig(configData)
107
+ if ('object' === typeof _newConfig) {
108
+ loadConfig(_newConfig)
1209
109
  }
1210
110
  }
1211
111
 
1212
- // Initial load
1213
112
  useEffect(() => {
1214
113
  init()
1215
- }, []) // eslint-disable-line
1216
-
1217
- useEffect(() => {
1218
- if (state && !runtimeData.init && !coveLoadedHasRan && container) {
1219
- publish('cove_loaded', { config: state })
1220
- setCoveLoadedHasRan(true)
1221
- }
1222
- }, [state, container, runtimeData.init]) // eslint-disable-line
1223
-
1224
- useEffect(() => {
1225
- // When geotype changes - add UID
1226
- if (state.data && state.columns.geo.name) {
1227
- addUIDs(state, state.columns.geo.name)
1228
- }
1229
- }, [state]) // eslint-disable-line
114
+ }, [])
1230
115
 
1231
116
  useEffect(() => {
1232
- // UID
1233
- if (state.data && state.columns.geo.name && state.columns.geo.name !== state.data.fromColumn) {
1234
- addUIDs(state, state.columns.geo.name)
1235
- }
1236
-
1237
- // Filters
1238
- const hashFilters = hashObj(state.filters)
1239
- let filters
1240
-
1241
- if (state.filters && (config || hashFilters !== runtimeFilters.fromHash)) {
1242
- filters = generateRuntimeFilters(state, hashFilters, runtimeFilters)
1243
-
1244
- if (filters) {
1245
- filters.forEach((filter, index) => {
1246
- const queryStringFilterValue = getQueryStringFilterValue(filter)
1247
- if (queryStringFilterValue) {
1248
- filters[index].active = queryStringFilterValue
1249
- }
1250
- })
1251
- setRuntimeFilters(filters)
1252
- }
1253
- }
1254
-
1255
- const hashLegend = generateRuntimeLegendHash(state, runtimeFilters)
1256
-
1257
- const hashData = hashObj({
1258
- data: state.data,
1259
- columns: state.columns,
1260
- geoType: state.general.geoType,
1261
- type: state.general.type,
1262
- geo: state.columns.geo.name,
1263
- primary: state.columns.primary.name,
1264
- mapPosition: state.mapPosition,
1265
- map: state.map,
1266
- ...runtimeFilters
1267
- })
1268
-
1269
- // Data
1270
- if (hashData !== runtimeData?.fromHash && state.data?.fromColumn) {
1271
- const newRuntimeData = generateRuntimeData(state, filters || runtimeFilters, hashData)
1272
-
1273
- setRuntimeData(newRuntimeData)
1274
- } else {
1275
- if (hashLegend !== runtimeLegend?.fromHash && undefined === runtimeData.init) {
1276
- const legend = generateRuntimeLegend(state, runtimeData, hashLegend)
1277
- setRuntimeLegend(legend)
1278
- }
1279
- }
1280
- }, [state]) // eslint-disable-line
1281
-
1282
- useEffect(() => {
1283
- const hashLegend = generateRuntimeLegendHash(state, runtimeFilters)
1284
-
1285
- // Legend - Update when runtimeData does
1286
- const legend = generateRuntimeLegend(state, runtimeData, hashLegend)
1287
- setRuntimeLegend(legend)
1288
- }, [
1289
- runtimeData,
1290
- state.legend.unified,
1291
- state.legend.showSpecialClassesLast,
1292
- state.legend.separateZero,
1293
- state.general.equalNumberOptIn,
1294
- state.legend.numberOfItems,
1295
- state.legend.specialClasses,
1296
- state.legend.additionalCategories
1297
- ]) // eslint-disable-line
117
+ init()
118
+ }, [configUrl])
1298
119
 
1299
120
  useEffect(() => {
1300
- reloadURLData()
1301
- }, [JSON.stringify(state.filters)])
1302
-
1303
- if (config) {
1304
- // eslint-disable-next-line react-hooks/rules-of-hooks
1305
- useEffect(() => {
1306
- loadConfig(config)
1307
- }, [config.data]) // eslint-disable-line
1308
- }
1309
-
1310
- // Destructuring for more readable JSX
1311
- const { general, tooltips, table } = state
1312
- let { title, subtext = '', geoType } = general
1313
-
1314
- // if no title AND in editor then set a default
1315
- if (isEditor) {
1316
- if (!title || title === '') title = 'Map Title'
1317
- }
1318
- if (!table.label || table.label === '') table.label = 'Data Table'
121
+ setConfig(editorsConfig)
122
+ }, [editorsConfig])
1319
123
 
1320
- // Map container classes
1321
- let mapContainerClasses = [
1322
- 'map-container',
1323
- state.legend?.position,
1324
- state.general.type,
1325
- state.general.geoType,
1326
- 'outline-none'
1327
- ]
1328
-
1329
- if (modal) {
1330
- mapContainerClasses.push('modal-background')
1331
- }
1332
-
1333
- if (general.type === 'navigation' && true === general.fullBorder) {
1334
- mapContainerClasses.push('full-border')
1335
- }
1336
-
1337
- // Props passed to all map types
1338
- const mapProps = {
1339
- projection,
1340
- setProjection,
1341
- stateToShow,
1342
- setStateToShow,
1343
- setScale,
1344
- setTranslate,
1345
- scale,
1346
- translate,
1347
- isDraggingAnnotation,
1348
- handleDragStateChange,
1349
- applyLegendToRow,
1350
- applyTooltipsToGeo,
1351
- container,
1352
- content: modal,
1353
- data: runtimeData,
1354
- displayGeoName,
1355
- filteredCountryCode,
1356
- generateColorsArray,
1357
- generateRuntimeData,
1358
- geoClickHandler,
1359
- hasZoom: state.general.allowMapZoom,
1360
- innerContainerRef,
1361
- isDashboard,
1362
- isDebug,
1363
- isEditor,
1364
- loadConfig,
1365
- logo,
1366
- position,
1367
- resetLegendToggles,
1368
- runtimeFilters,
1369
- runtimeLegend,
1370
- runtimeData,
1371
- setAccessibleStatus,
1372
- setFilteredCountryCode,
1373
- setParentConfig: setConfig,
1374
- setPosition,
1375
- setRuntimeData,
1376
- setRuntimeFilters,
1377
- setRuntimeLegend,
1378
- setSharedFilterValue,
1379
- setState,
1380
- state,
1381
- tooltipId,
1382
- tooltipRef,
1383
- topoData,
1384
- setTopoData,
1385
- mapId,
1386
- outerContainerRef,
1387
- dimensions,
1388
- currentViewport
1389
- }
1390
-
1391
- if (!mapProps.data || !state.data) return <></>
1392
-
1393
- const hasDataTable =
1394
- state.runtime.editorErrorMessage.length === 0 &&
1395
- true === table.forceDisplay &&
1396
- general.type !== 'navigation' &&
1397
- false === loading
1398
-
1399
- const handleMapTabbing = () => {
1400
- let tabbingID
1401
-
1402
- // 1) skip to legend
1403
- if (general.showSidebar) {
1404
- tabbingID = legendId
1405
- }
1406
-
1407
- // 2) skip to data table if it exists and not a navigation map
1408
- if (hasDataTable && !general.showSidebar) {
1409
- tabbingID = `dataTableSection__${Date.now()}`
1410
- }
1411
-
1412
- // 3) if it's a navigation map skip to the dropdown.
1413
- if (state.general.type === 'navigation') {
1414
- tabbingID = `dropdown-${Date.now()}`
1415
- }
1416
-
1417
- // 4) handle other options
1418
- return tabbingID || '!'
1419
- }
1420
-
1421
- const tabId = handleMapTabbing()
1422
-
1423
- // this only shows in Dashboard config mode and only if Show Table is also set
1424
- const tableLink = (
1425
- <a href={`#data-table-${state.dataKey}`} className='margin-left-href'>
1426
- {state.dataKey} (Go to Table)
1427
- </a>
1428
- )
1429
-
1430
- const sectionClassNames = () => {
1431
- const classes = [
1432
- 'cove-component__content',
1433
- 'cdc-map-inner-container',
1434
- `${currentViewport}`,
1435
- `${state?.general?.headerColor}`
1436
- ]
1437
- if (config?.runtime?.editorErrorMessage.length > 0) classes.push('type-map--has-error')
1438
- return classes.join(' ')
1439
- }
124
+ if (loading) return null
1440
125
 
1441
126
  return (
1442
- <ConfigContext.Provider value={mapProps}>
1443
- <Layout.VisualizationWrapper
1444
- config={state}
1445
- isEditor={isEditor}
1446
- ref={outerContainerRef}
1447
- currentViewport={currentViewport}
1448
- imageId={imageId}
1449
- showEditorPanel={state.showEditorPanel}
1450
- >
1451
- {isEditor && <EditorPanel columnsRequiredChecker={columnsRequiredChecker} />}
1452
- <Layout.Responsive isEditor={isEditor}>
1453
- {requiredColumns && (
1454
- <Waiting requiredColumns={requiredColumns} className={displayPanel ? `waiting` : `waiting collapsed`} />
1455
- )}
1456
- {!runtimeData.init && (general.type === 'navigation' || runtimeLegend) && (
1457
- <section className={sectionClassNames()} aria-label={'Map: ' + title} ref={innerContainerRef}>
1458
- {state?.runtime?.editorErrorMessage.length > 0 && <Error state={state} />}
1459
- {/* prettier-ignore */}
1460
- <Title
1461
- title={title}
1462
- superTitle={general.superTitle}
1463
- config={config}
1464
- classes={['map-title', general.showTitle === true ? 'visible' : 'hidden', `${general.headerColor}`]}
1465
- />
1466
- <SkipTo skipId={tabId} skipMessage='Skip Over Map Container' />
1467
- {state?.annotations?.length > 0 && (
1468
- <SkipTo skipId={tabId} skipMessage={`Skip over annotations`} key={`skip-annotations`} />
1469
- )}
1470
-
1471
- {general.introText && <section className='introText mb-4'>{parse(general.introText)}</section>}
1472
-
1473
- {state?.filters?.length > 0 && (
1474
- <Filters
1475
- config={state}
1476
- setConfig={setState}
1477
- filteredData={runtimeFilters}
1478
- setFilteredData={_setRuntimeData}
1479
- dimensions={dimensions}
1480
- standaloneMap={!config}
1481
- />
1482
- )}
1483
-
1484
- <div
1485
- role='region'
1486
- tabIndex='0'
1487
- className={mapContainerClasses.join(' ')}
1488
- onClick={e => closeModal(e, modal, setModal)}
1489
- onKeyDown={e => {
1490
- if (e.key === 'Enter') {
1491
- closeModal(e, modal, setModal)
1492
- }
1493
- }}
1494
- style={{ padding: '15px 0px', margin: '0px' }}
1495
- >
1496
- {/* eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex */}
1497
- <section className='outline-none geography-container w-100' ref={mapSvg} tabIndex='0'>
1498
- {currentViewport && (
1499
- <>
1500
- {modal && <Modal />}
1501
- {'single-state' === geoType && <UsaMap.SingleState />}
1502
- {'us' === geoType && 'us-geocode' !== state.general.type && <UsaMap.State />}
1503
- {'us-region' === geoType && <UsaMap.Region />}
1504
- {'us-county' === geoType && <UsaMap.County />}
1505
- {'world' === geoType && <WorldMap />}
1506
- {/* logo is handled in UsaMap.State when applicable */}
1507
- {'google-map' === geoType && <GoogleMap />}
1508
- {'data' === general.type && logo && ('us' !== geoType || 'us-geocode' === state.general.type) && (
1509
- <img src={logo} alt='' className='map-logo' style={{ maxWidth: '50px' }} />
1510
- )}
1511
- </>
1512
- )}
1513
- </section>
1514
-
1515
- {general.showSidebar && 'navigation' !== general.type && (
1516
- <Legend
1517
- dimensions={dimensions}
1518
- ref={legendRef}
1519
- skipId={tabId}
1520
- containerWidthPadding={0}
1521
- currentViewport={currentViewport}
1522
- />
1523
- )}
1524
- </div>
1525
-
1526
- {'navigation' === general.type && (
1527
- <NavigationMenu
1528
- mapTabbingID={tabId}
1529
- displayGeoName={displayGeoName}
1530
- data={runtimeData}
1531
- options={general}
1532
- columns={state.columns}
1533
- navigationHandler={val => navigationHandler('_blank', val, customNavigationHandler)}
1534
- />
1535
- )}
1536
-
1537
- {/* Link */}
1538
- {isDashboard && config.table?.forceDisplay && config.table.showDataTableLink ? tableLink : link && link}
1539
-
1540
- {subtext.length > 0 && <p className='subtext mt-4'>{parse(subtext)}</p>}
1541
-
1542
- <MediaControls.Section classes={['download-buttons']}>
1543
- {state.general.showDownloadImgButton && (
1544
- <MediaControls.Button
1545
- text='Download Image'
1546
- title='Download Chart as Image'
1547
- type='image'
1548
- state={state}
1549
- elementToCapture={imageId}
1550
- />
1551
- )}
1552
- {state.general.showDownloadPdfButton && (
1553
- <MediaControls.Button
1554
- text='Download PDF'
1555
- title='Download Chart as PDF'
1556
- type='pdf'
1557
- state={state}
1558
- elementToCapture={imageId}
1559
- />
1560
- )}
1561
- </MediaControls.Section>
1562
-
1563
- {state.runtime.editorErrorMessage.length === 0 &&
1564
- true === table.forceDisplay &&
1565
- general.type !== 'navigation' &&
1566
- false === loading && (
1567
- <DataTable
1568
- config={state}
1569
- rawData={state.data}
1570
- navigationHandler={navigationHandler}
1571
- expandDataTable={table.expanded}
1572
- headerColor={general.headerColor}
1573
- columns={state.columns}
1574
- showFullGeoNameInCSV={table.showFullGeoNameInCSV}
1575
- runtimeLegend={runtimeLegend}
1576
- runtimeData={runtimeData}
1577
- displayGeoName={displayGeoName}
1578
- applyLegendToRow={applyLegendToRow}
1579
- tableTitle={table.label}
1580
- indexTitle={table.indexLabel}
1581
- vizTitle={general.title}
1582
- viewport={currentViewport}
1583
- formatLegendLocation={key =>
1584
- formatLegendLocation(key, runtimeData?.[key]?.[state.columns.geo.name])
1585
- }
1586
- setFilteredCountryCode={setFilteredCountryCode}
1587
- tabbingId={tabId}
1588
- showDownloadImgButton={state.general.showDownloadImgButton}
1589
- showDownloadPdfButton={state.general.showDownloadPdfButton}
1590
- innerContainerRef={innerContainerRef}
1591
- outerContainerRef={outerContainerRef}
1592
- imageRef={imageId}
1593
- isDebug={isDebug}
1594
- wrapColumns={table.wrapColumns}
1595
- />
1596
- )}
1597
-
1598
- {state.annotations.length > 0 && <Annotation.Dropdown />}
1599
-
1600
- {general.footnotes && <section className='footnotes pt-2 mt-4'>{parse(general.footnotes)}</section>}
1601
- </section>
1602
- )}
1603
-
1604
- <div aria-live='assertive' className='cdcdataviz-sr-only'>
1605
- {accessibleStatus}
1606
- </div>
1607
-
1608
- {!isDraggingAnnotation &&
1609
- !window.matchMedia('(any-hover: none)').matches &&
1610
- 'hover' === tooltips.appearanceType && (
1611
- <ReactTooltip
1612
- id={`tooltip__${tooltipId}`}
1613
- float={true}
1614
- className={`${tooltips.capitalizeLabels ? 'capitalize tooltip tooltip-test' : 'tooltip tooltip-test'}`}
1615
- style={{ background: `rgba(255,255,255, ${state.tooltips.opacity / 100})`, color: 'black' }}
1616
- />
1617
- )}
1618
- <div
1619
- ref={tooltipRef}
1620
- id={`tooltip__${tooltipId}-canvas`}
1621
- className='tooltip'
1622
- style={{
1623
- background: `rgba(255,255,255,${state.tooltips.opacity / 100})`,
1624
- position: 'absolute',
1625
- whiteSpace: 'nowrap',
1626
- display: 'none' // can't use d-none here
1627
- }}
1628
- ></div>
1629
- </Layout.Responsive>
1630
- </Layout.VisualizationWrapper>
1631
- </ConfigContext.Provider>
127
+ <CdcMapComponent
128
+ config={config}
129
+ navigationHandler={customNavigationHandler}
130
+ isEditor={isEditor}
131
+ isDashboard={isDashboard}
132
+ logo={logo}
133
+ link={link}
134
+ loadConfig={loadConfig}
135
+ />
1632
136
  )
1633
137
  }
1634
138