@cdc/dashboard 4.24.1 → 4.24.2

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 (53) hide show
  1. package/dist/cdcdashboard.js +117195 -108041
  2. package/examples/{private/DEV-6574.json → DEV-6574.json} +8 -8
  3. package/examples/filters/Alabama.json +72 -0
  4. package/examples/zika.json +2274 -0
  5. package/index.html +5 -3
  6. package/package.json +9 -9
  7. package/src/CdcDashboard.tsx +124 -991
  8. package/src/CdcDashboardComponent.tsx +903 -0
  9. package/src/_stories/Dashboard.stories.tsx +2 -2
  10. package/src/components/Column.tsx +15 -12
  11. package/src/components/Header/Header.tsx +18 -4
  12. package/src/components/MultiConfigTabs/MultiConfigTabs.tsx +106 -0
  13. package/src/components/MultiConfigTabs/MultiTabs.tsx +30 -0
  14. package/src/components/MultiConfigTabs/index.tsx +8 -0
  15. package/src/components/MultiConfigTabs/multiconfigtabs.styles.css +32 -0
  16. package/src/components/Widget.tsx +25 -9
  17. package/src/helpers/generateValuesForFilter.ts +1 -1
  18. package/src/helpers/getUpdateConfig.ts +6 -2
  19. package/src/helpers/processData.ts +13 -0
  20. package/src/helpers/processDataLegacy.ts +14 -0
  21. package/src/{index.jsx → index.tsx} +2 -2
  22. package/src/scss/editor-panel.scss +14 -11
  23. package/src/scss/grid.scss +4 -6
  24. package/src/scss/main.scss +2 -8
  25. package/src/store/dashboard.actions.ts +10 -4
  26. package/src/store/dashboard.reducer.ts +74 -3
  27. package/src/types/ConfigRow.ts +6 -0
  28. package/src/types/Dashboard.ts +11 -0
  29. package/src/types/{Config.ts → DashboardConfig.ts} +8 -14
  30. package/src/types/InitialState.ts +10 -0
  31. package/src/types/MultiDashboard.ts +11 -0
  32. package/examples/private/DEV-5189-2.json +0 -935
  33. package/examples/private/DEV-5189.json +0 -1266
  34. package/examples/private/dash-scaling.json +0 -45325
  35. package/examples/private/epi-chart.json +0 -813
  36. package/examples/private/epi-dash.json +0 -457
  37. package/examples/private/epi-new.json +0 -994
  38. package/examples/private/filters/Alabama.json +0 -4217
  39. package/examples/private/new-epi-csv.json +0 -358
  40. package/examples/private/new-epi.json +0 -358
  41. package/examples/private/no-lines.json +0 -8432
  42. package/examples/private/tick-bold-data.json +0 -82325
  43. package/examples/private/tick-bold.json +0 -164678
  44. package/examples/private/visits.json +0 -9985
  45. /package/examples/{private/filters → filters}/Alaska.json +0 -0
  46. /package/examples/{private/filters → filters}/Arkansas.json +0 -0
  47. /package/examples/{private/filters → filters}/California.json +0 -0
  48. /package/examples/{private/filters → filters}/Colorado.json +0 -0
  49. /package/examples/{private/filters → filters}/Connecticut.json +0 -0
  50. /package/examples/{private/filters → filters}/Delaware.json +0 -0
  51. /package/examples/{private/filters → filters}/DistrictofColumbia.json +0 -0
  52. /package/examples/{private/filters → filters}/Florida.json +0 -0
  53. /package/examples/{private/filters → filters}/States.json +0 -0
@@ -1,991 +1,124 @@
1
- import React, { useState, useEffect, useCallback, useMemo, useReducer } from 'react'
2
-
3
- // IE11
4
- // import 'core-js/stable'
5
- import 'whatwg-fetch'
6
- import ResizeObserver from 'resize-observer-polyfill'
7
-
8
- import { DndProvider } from 'react-dnd'
9
- import { HTML5Backend } from 'react-dnd-html5-backend'
10
-
11
- import parse from 'html-react-parser'
12
-
13
- import fetchRemoteData from '@cdc/core/helpers/fetchRemoteData'
14
- import { GlobalContextProvider } from '@cdc/core/components/GlobalContext'
15
- import { DashboardContext, DashboardDispatchContext, initialState } from './DashboardContext'
16
-
17
- import OverlayFrame from '@cdc/core/components/ui/OverlayFrame'
18
- import Loading from '@cdc/core/components/Loading'
19
- import { DataTransform } from '@cdc/core/helpers/DataTransform'
20
- import getViewport from '@cdc/core/helpers/getViewport'
21
-
22
- import CdcMap from '@cdc/map'
23
- import CdcChart from '@cdc/chart'
24
- import CdcDataBite from '@cdc/data-bite'
25
- import CdcWaffleChart from '@cdc/waffle-chart'
26
- import CdcMarkupInclude from '@cdc/markup-include'
27
- import CdcFilteredText from '@cdc/filtered-text'
28
-
29
- import Grid from './components/Grid'
30
- import Header, { FilterBehavior } from './components/Header/Header'
31
- import defaults from './data/initial-state'
32
- import DataTable from '@cdc/core/components/DataTable'
33
- import MediaControls from '@cdc/core/components/MediaControls'
34
-
35
- import './scss/main.scss'
36
- import '@cdc/core/styles/v2/main.scss'
37
- import { gatherQueryParams } from '@cdc/core/helpers/gatherQueryParams'
38
- import { capitalizeSplitAndJoin } from '@cdc/core/helpers/cove/string'
39
-
40
- import VisualizationsPanel from './components/VisualizationsPanel'
41
- import dashboardReducer from './store/dashboard.reducer'
42
- import { filterData } from './helpers/filterData'
43
- import { getFormattedData } from './helpers/getFormattedData'
44
- import { generateValuesForFilter } from './helpers/generateValuesForFilter'
45
- import { getVizKeys } from './helpers/getVizKeys'
46
- import Title from '@cdc/core/components/ui/Title'
47
- import { TableConfig } from '@cdc/core/components/DataTable/types/TableConfig'
48
-
49
- // types
50
- import { type SharedFilter } from './types/SharedFilter'
51
- import { type APIFilter } from './types/APIFilter'
52
- import { type DataSet } from './types/DataSet'
53
- import { type Config } from './types/Config'
54
- import { type Visualization } from '@cdc/core/types/Visualization'
55
-
56
- type DropdownOptions = Record<'value' | 'text', string>[]
57
-
58
- type APIFilterDropdowns = {
59
- // null means still loading
60
- [filtername: string]: null | DropdownOptions
61
- }
62
-
63
- type CdcDashboardTypes = {
64
- configUrl: string
65
- config: Config
66
- isEditor: boolean
67
- isDebug: boolean
68
- setConfig: Function
69
- }
70
-
71
- export default function CdcDashboard({ configUrl = '', config: configObj, isEditor = false, isDebug = false, setConfig: setParentConfig }: CdcDashboardTypes) {
72
- const [state, dispatch] = useReducer(dashboardReducer, { config: configObj, ...initialState })
73
- const [apiFilterDropdowns, setAPIFilterDropdowns] = useState<APIFilterDropdowns>({})
74
- const [currentViewport, setCurrentViewport] = useState('lg')
75
- const [imageId] = useState(`cove-${Math.random().toString(16).slice(-4)}`)
76
-
77
- const replacements = {
78
- 'Remove Spaces': '',
79
- 'Keep Spaces': ' ',
80
- 'Replace With Underscore': '_'
81
- }
82
-
83
- const inNoDataState = useMemo(() => {
84
- const vals = Object.values(state.data)
85
- if (!vals.length) return true
86
- return vals.some(val => val === undefined)
87
- }, [state.data])
88
-
89
- const getAutoLoadVisualization = (): Visualization | undefined => {
90
- if (!state.config) return
91
- const autoLoadViz = Object.values(state.config.visualizations).filter(vis => {
92
- return vis.autoLoad && vis.type === 'filter-dropdowns'
93
- })
94
- if (autoLoadViz.length === 0) return
95
- if (autoLoadViz.length > 1) throw new Error('Only one filter row can be autoloaded')
96
- return autoLoadViz[0]
97
- }
98
-
99
- const transform = new DataTransform()
100
-
101
- const processData = async (dataSet: DataSet, filterBehavior) => {
102
- let dataset = dataSet.formattedData || dataSet.data
103
-
104
- if (dataSet && dataSet.dataUrl && filterBehavior !== FilterBehavior.Apply) {
105
- dataset = await fetchRemoteData(`${dataSet.dataUrl}`)
106
-
107
- dataset = getFormattedData(dataset, dataSet.dataDescription)
108
- }
109
-
110
- return dataset
111
- }
112
-
113
- const processDataLegacy = async (response: any) => {
114
- let dataset = response.formattedData || response.data
115
-
116
- if (response.dataUrl) {
117
- dataset = await fetchRemoteData(`${response.dataUrl}`)
118
-
119
- dataset = getFormattedData(dataset, response.dataDescription)
120
- }
121
-
122
- return dataset
123
- }
124
- const getApiFilterKey = ({ apiEndpoint, heirarchyLookup }: APIFilter) => {
125
- return apiEndpoint + (heirarchyLookup || '')
126
- }
127
-
128
- const setAutoLoadDefaultValue = (sharedFilterIndex: number, filterDropdowns: DropdownOptions) => {
129
- if (!state.config) return
130
- const autoLoadViz = getAutoLoadVisualization()
131
- if (!autoLoadViz) return // no autoLoading happening
132
- const notIncludedInAutoLoad = autoLoadViz.hide
133
- if (notIncludedInAutoLoad.includes(sharedFilterIndex)) {
134
- // we don't want to auto load it
135
- return
136
- } else {
137
- const sharedFilter = state.config.dashboard.sharedFilters[sharedFilterIndex]
138
- if (sharedFilter.active) return // a value has already been selected.
139
- const filterParents = state.config.dashboard.sharedFilters.filter(f => sharedFilter.parents?.includes(f.key))
140
- const notAllParentFiltersSelected = filterParents.some(p => !p.active)
141
- if (filterParents && notAllParentFiltersSelected) return
142
- const defaultFilterDropdown = filterDropdowns.find(({ value }) => value === sharedFilter.apiFilter!.defaultValue)
143
- let defaultValue = defaultFilterDropdown?.value || filterDropdowns[0].value
144
- changeFilterActive(sharedFilterIndex, defaultValue)
145
- }
146
- }
147
-
148
- const loadAPIFilters = async () => {
149
- if (state.config?.dashboard?.sharedFilters) {
150
- const sharedAPIFilters = state.config.dashboard.sharedFilters.filter(f => f.apiFilter)
151
- const loadingFilterMemo: APIFilterDropdowns = sharedAPIFilters.reduce((acc, curr) => {
152
- const _key = getApiFilterKey(curr.apiFilter!)
153
- if (apiFilterDropdowns[_key] != null) return acc // don't overwrite fetched data.
154
- acc[_key] = null
155
- return acc
156
- }, {})
157
- setAPIFilterDropdowns({ ...apiFilterDropdowns, ...loadingFilterMemo })
158
- const filterLookup = new Map(sharedAPIFilters.map(filter => [getApiFilterKey(filter.apiFilter!), filter.apiFilter!]))
159
- const getParentParams = (childFilter: SharedFilter): Record<'key' | 'value', string>[] | null => {
160
- const _parents = sharedAPIFilters.filter(parentFilter => childFilter.parents?.includes(parentFilter.key))
161
- if (!_parents.length) return null
162
- return _parents.map(({ queryParameter, active }) => ({ key: queryParameter || '', value: active || '' }))
163
- }
164
- const getFilterValues = (filterData: Object | Array<Object>, apiFilter: APIFilter): DropdownOptions => {
165
- const { textSelector, valueSelector, heirarchyLookup } = apiFilter
166
- if (heirarchyLookup) {
167
- const heirarchy = heirarchyLookup!.split('.')
168
- const selector = heirarchy.shift() // pop first element
169
- return getFilterValues(selector ? filterData[selector] : filterData, { ...apiFilter, heirarchyLookup: heirarchy.join('.') })
170
- }
171
- if (!Array.isArray(filterData)) throw new Error('the filter data has requires a heirarchy path to access the filter values, This should be in the format key.subkey.subsubkey')
172
- return filterData.map(v => ({ text: v[textSelector], value: v[valueSelector] }))
173
- }
174
- state.config.dashboard.sharedFilters.forEach(async (filter, index) => {
175
- if (!filter.apiFilter) return
176
- const baseEndpoint = filter.apiFilter.apiEndpoint
177
- const _key = getApiFilterKey(filter.apiFilter)
178
- const params = getParentParams(filter)
179
- const notAllParentsSelected = params?.some(({ value }) => value === '')
180
- if (notAllParentsSelected) return // don't send request for dependent children filter options
181
- if (apiFilterDropdowns[_key] && !params && filter.filterBy === 'Query String') return // don't reload filter unless it's a child
182
- const endpoint = baseEndpoint + (params ? gatherQueryParams(params) : '')
183
-
184
- fetch(endpoint)
185
- .then(resp => resp.json())
186
- .then(data => {
187
- const apiFilter = filterLookup.get(_key) as APIFilter
188
- const _filterValues = getFilterValues(data, apiFilter)
189
- setAPIFilterDropdowns(dropdowns => ({ ...dropdowns, [_key]: _filterValues }))
190
- setAutoLoadDefaultValue(index, _filterValues)
191
- })
192
- })
193
- }
194
- }
195
-
196
- const reloadURLData = async () => {
197
- const { config } = state
198
- if (config && config.datasets) {
199
- let newData = { ...state.data }
200
- let newDatasets = { ...config.datasets }
201
- let datasetsNeedsUpdate = false
202
- let datasetKeys = Object.keys(config.datasets)
203
- let newFileName = ''
204
-
205
- for (let i = 0; i < datasetKeys.length; i++) {
206
- const datasetKey = datasetKeys[i]
207
- const dataset = config.datasets[datasetKey]
208
- if (dataset.dataUrl && config.dashboard && config.dashboard.sharedFilters) {
209
- const dataUrl = new URL(dataset.runtimeDataUrl || dataset.dataUrl, window.location.origin)
210
- let currentQSParams = Object.fromEntries(new URLSearchParams(dataUrl.search))
211
- let updatedQSParams = {}
212
-
213
- let isUpdateNeeded = false
214
-
215
- config.dashboard.sharedFilters.forEach(filter => {
216
- if (filter.filterBy === 'File Name') {
217
- // if no file name is entered use the default active filter. ie. /activeFilter.json
218
- if (!filter.fileName && filter.datasetKey === datasetKey) newFileName = filter.active
219
- // if a file name is found, ie, state_${query}, use that, ie. state_activeFilter.json
220
- if (filter.datasetKey === datasetKey && filter.fileName) newFileName = capitalizeSplitAndJoin.call(String(filter.fileName), ' ', replacements[filter.whitespaceReplacement ?? 'Keep Spaces'])
221
- if (newFileName && newFileName.includes('${query}')) {
222
- newFileName = newFileName.replace('${query}', capitalizeSplitAndJoin.call(String(filter.active), ' ', replacements[filter.whitespaceReplacement ?? 'Keep Spaces']))
223
- }
224
- }
225
-
226
- if (filter.type === 'urlfilter' && !!filter.queryParameter) {
227
- if (updatedQSParams[filter.queryParameter]) {
228
- updatedQSParams[filter.queryParameter] = updatedQSParams[filter.queryParameter] + filter.active
229
- } else {
230
- updatedQSParams[filter.queryParameter] = filter.active
231
- }
232
- }
233
- if (filter.filterBy === 'File Name') {
234
- isUpdateNeeded = true
235
- }
236
- })
237
-
238
- Object.keys(updatedQSParams).forEach(updatedParam => {
239
- if (decodeURIComponent(updatedQSParams[updatedParam]) !== currentQSParams[updatedParam]) {
240
- isUpdateNeeded = true
241
- }
242
- })
243
-
244
- if (!isUpdateNeeded) return
245
-
246
- Object.keys(currentQSParams).forEach(currentParam => {
247
- if (!updatedQSParams[currentParam]) {
248
- updatedQSParams[currentParam] = currentQSParams[currentParam]
249
- }
250
- })
251
- const _params = Object.keys(updatedQSParams).map(key => ({ key, value: updatedQSParams[key] }))
252
- let dataUrlFinal = `${dataUrl.origin}${dataUrl.pathname}${gatherQueryParams(_params)}`
253
-
254
- if (newFileName !== '') {
255
- let fileExtension = dataUrl.pathname.split('.').pop()
256
- let pathWithoutFilename = dataUrl.pathname.substring(0, dataUrl.pathname.lastIndexOf('/'))
257
- dataUrlFinal = `${dataUrl.origin}${pathWithoutFilename}/${newFileName}.${fileExtension}${gatherQueryParams(_params)}`
258
- }
259
-
260
- let newDataset = await fetchRemoteData(`${dataUrlFinal}`)
261
-
262
- if (newDataset && dataset.dataDescription) {
263
- try {
264
- newDataset = transform.autoStandardize(newDataset)
265
- newDataset = transform.developerStandardize(newDataset, dataset.dataDescription)
266
- } catch (e) {
267
- //Data not able to be standardized, leave as is
268
- }
269
- }
270
-
271
- newDatasets[datasetKey].runtimeDataUrl = dataUrlFinal
272
- newData[datasetKey] = newDataset
273
- datasetsNeedsUpdate = true
274
- }
275
- }
276
-
277
- if (datasetsNeedsUpdate) {
278
- dispatch({ type: 'SET_DATA', payload: newData })
279
-
280
- let newFilteredData = {}
281
- let newConfig = { ...config }
282
- getVizKeys(config).forEach(key => {
283
- let dataKey = config.visualizations[key].dataKey
284
-
285
- let applicableFilters = config.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.usedBy && sharedFilter.usedBy.indexOf(key) !== -1)
286
- if (applicableFilters.length > 0) {
287
- newFilteredData[key] = filterData(applicableFilters, newData[dataKey], state.config?.filterBehavior)
288
- }
289
-
290
- if (newData[dataKey]) {
291
- newConfig.visualizations[key].formattedData = newData[dataKey]
292
- }
293
- })
294
- dispatch({ type: 'SET_FILTERED_DATA', payload: newFilteredData })
295
- newConfig.datasets = newDatasets
296
- dispatch({ type: 'SET_CONFIG', payload: newConfig })
297
- }
298
- }
299
- }
300
-
301
- const loadConfig = async () => {
302
- dispatch({ type: 'SET_LOADING', payload: true })
303
- let response: Config = configObj || (await (await fetch(configUrl)).json())
304
- let newConfig = { ...defaults, ...response }
305
- let datasets = {}
306
-
307
- if (response.datasets) {
308
- await Promise.all(
309
- Object.keys(response.datasets).map(async key => {
310
- datasets[key] = await processData(response.datasets[key], response.filterBehavior)
311
- })
312
- )
313
-
314
- getVizKeys(newConfig).forEach(vizKey => {
315
- newConfig.visualizations[vizKey].formattedData = datasets[newConfig.visualizations[vizKey].dataKey]
316
- })
317
-
318
- Object.keys(datasets).forEach(key => {
319
- newConfig.datasets[key].data = datasets[key]
320
- })
321
- } else {
322
- let dataKey = newConfig.dataFileName || 'backwards-compatibility'
323
- datasets[dataKey] = await processDataLegacy(response)
324
-
325
- let datasetsFull = {}
326
- datasetsFull[dataKey] = {
327
- data: datasets[dataKey],
328
- dataDescription: newConfig.dataDescription
329
- }
330
- newConfig.datasets = datasetsFull
331
-
332
- getVizKeys(newConfig).forEach(vizKey => {
333
- newConfig.visualizations[vizKey].dataKey = dataKey
334
- newConfig.visualizations[vizKey].dataDescription = newConfig.dataDescription
335
- newConfig.visualizations[vizKey].formattedData = newConfig.formattedData
336
- })
337
-
338
- newConfig.data = []
339
- newConfig.dataUrl = ''
340
- newConfig.dataFileName = ''
341
- newConfig.dataFileSourceType = ''
342
- newConfig.dataDescription = {}
343
- newConfig.formattedData = []
344
-
345
- if (newConfig.dashboard && newConfig.dashboard.filters) {
346
- newConfig.dashboard.sharedFilters = newConfig.dashboard.sharedFilters || []
347
- newConfig.dashboard.filters.forEach(filter => {
348
- newConfig.dashboard.sharedFilters.push({ ...filter, key: filter.label, showDropdown: true, usedBy: getVizKeys(newConfig) })
349
- })
350
-
351
- newConfig.dashboard.filters = undefined
352
- }
353
- }
354
-
355
- dispatch({ type: 'SET_DATA', payload: datasets })
356
- dispatch({ type: 'UPDATE_CONFIG', payload: [newConfig, datasets] })
357
- dispatch({ type: 'SET_LOADING', payload: false })
358
- }
359
-
360
- const findFilterTier = (filters: SharedFilter[], sharedFilter: SharedFilter) => {
361
- if (!sharedFilter.parents?.length) {
362
- return 1
363
- } else {
364
- let parent = filters.find(filter => sharedFilter.parents!.includes(filter.key))
365
- if (!parent) return 1
366
- return 1 + findFilterTier(filters, parent)
367
- }
368
- }
369
-
370
- const setSharedFilter = (key, datum) => {
371
- const { config } = state
372
- if (!config) return
373
- let newConfig = { ...config }
374
- let newFilteredData = { ...state.filteredData }
375
- for (let i = 0; i < newConfig.dashboard.sharedFilters.length; i++) {
376
- const filter = newConfig.dashboard.sharedFilters[i]
377
- if (filter.setBy === key) {
378
- if (!!filter.columnName) {
379
- filter.active = datum[filter.columnName]
380
- }
381
- break
382
- }
383
- }
384
-
385
- getVizKeys(newConfig).forEach(visualizationKey => {
386
- let applicableFilters = newConfig.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.usedBy && sharedFilter.usedBy.indexOf(visualizationKey) !== -1)
387
-
388
- if (applicableFilters.length > 0) {
389
- const visualization = newConfig.visualizations[visualizationKey]
390
-
391
- const formattedData = visualization.dataDescription ? getFormattedData(state.data[visualization.dataKey] || visualization.data, visualization.dataDescription) : undefined
392
-
393
- newFilteredData[visualizationKey] = filterData(applicableFilters, formattedData || state.data[visualization.dataKey], state.config?.filterBehavior)
394
- }
395
- })
396
-
397
- dispatch({ type: 'SET_FILTERED_DATA', payload: newFilteredData })
398
- dispatch({ type: 'SET_CONFIG', payload: newConfig })
399
- }
400
-
401
- // Load data when component first mounts
402
- useEffect(() => {
403
- loadConfig()
404
- }, [])
405
-
406
- // Pass up to <CdcEditor /> if it exists when config state changes
407
- useEffect(() => {
408
- if (setParentConfig && isEditor) {
409
- setParentConfig(state.config)
410
- }
411
- }, [JSON.stringify(state.config)])
412
-
413
- useEffect(() => {
414
- const { config } = state
415
- if (config && config.filterBehavior !== FilterBehavior.Apply) {
416
- reloadURLData()
417
- }
418
- loadAPIFilters()
419
- }, [JSON.stringify(state.config?.dashboard ? state.config.dashboard.sharedFilters : undefined)])
420
-
421
- const updateChildConfig = (visualizationKey, newConfig) => {
422
- const { config } = state
423
- if (!config) return
424
- let updatedConfig = { ...config }
425
-
426
- updatedConfig.visualizations[visualizationKey] = newConfig
427
- updatedConfig.visualizations[visualizationKey].formattedData = config.visualizations[visualizationKey].formattedData
428
-
429
- dispatch({ type: 'SET_CONFIG', payload: config })
430
- }
431
-
432
- const applyFilters = () => {
433
- if (!state.config) return
434
- let dashboardConfig = state.config.dashboard
435
- const allFiltersSelected = !state.config.dashboard.sharedFilters.some(filter => !filter.active && !filter.queuedActive)
436
- if (allFiltersSelected) {
437
- if (state.config.filterBehavior === FilterBehavior.Apply) {
438
- state.config.dashboard.sharedFilters.forEach((sharedFilter, index) => {
439
- if (sharedFilter.queuedActive) {
440
- dashboardConfig.sharedFilters[index].active = sharedFilter.queuedActive
441
- delete dashboardConfig.sharedFilters[index].queuedActive
442
- }
443
- })
444
- }
445
-
446
- dispatch({ type: 'SET_CONFIG', payload: { ...state.config, dashboard: dashboardConfig } })
447
- updateDataFilters()
448
- reloadURLData()
449
- } else {
450
- // TODO noftify of required fields
451
- }
452
- }
453
-
454
- const changeFilterActive = (index: number, value: string) => {
455
- const { config } = state
456
- if (!config) return
457
- let dashboardConfig = { ...config.dashboard }
458
-
459
- if (config.filterBehavior !== FilterBehavior.Apply) {
460
- dashboardConfig.sharedFilters[index].active = value
461
- } else {
462
- dashboardConfig.sharedFilters[index].queuedActive = value
463
- }
464
-
465
- dispatch({ type: 'SET_CONFIG', payload: { ...config, dashboard: dashboardConfig } })
466
- if (config.filterBehavior !== FilterBehavior.Apply) {
467
- updateDataFilters()
468
- reloadURLData()
469
- }
470
- }
471
-
472
- const updateDataFilters = () => {
473
- const { config } = state
474
- if (!config) return
475
- let dashboardConfig = { ...config.dashboard }
476
-
477
- let newFilteredData = {}
478
- getVizKeys(config).forEach(key => {
479
- let applicableFilters = dashboardConfig.sharedFilters.filter(sharedFilter => sharedFilter.usedBy && sharedFilter.usedBy.indexOf(key) !== -1)
480
- if (applicableFilters.length > 0) {
481
- const visualization = config.visualizations[key]
482
- const _data = state.data[visualization.dataKey] || visualization.data
483
- const formattedData = visualization.dataDescription ? getFormattedData(_data, visualization.dataDescription) : _data
484
-
485
- newFilteredData[key] = filterData(applicableFilters, formattedData, config.filterBehavior)
486
- }
487
- })
488
-
489
- dispatch({ type: 'SET_FILTERED_DATA', payload: newFilteredData })
490
- }
491
-
492
- const handleOnChange = (index: number, value: string) => {
493
- const { config } = state
494
- if (!config) return
495
- changeFilterActive(index, value)
496
- if (config.filterBehavior === FilterBehavior.Apply) {
497
- const autoLoadViz = getAutoLoadVisualization()
498
- if (!autoLoadViz) return // nothing left to do for regular filter behavior.
499
- const isAutoSelectFilter = !autoLoadViz.hide.includes(index)
500
- const missingFilterSelections = config.dashboard.sharedFilters.some(f => !f.active)
501
- if (isAutoSelectFilter && !missingFilterSelections) {
502
- // a dropdown has been selected that doesn't
503
- // require the Go Button
504
- reloadURLData()
505
- } else {
506
- // A parent filter was selected, reset filters by:
507
- // set auto select filter dropdowns to null
508
- const autoSelectFilters = config.dashboard.sharedFilters.filter((_, _index) => !autoLoadViz?.hide.includes(_index))
509
- const dropdownFilterKeys = autoSelectFilters.map(filter => getApiFilterKey(filter.apiFilter!))
510
- const newApiDropdowns = { ...apiFilterDropdowns }
511
- dropdownFilterKeys.forEach(key => (newApiDropdowns[key] = null))
512
- setAPIFilterDropdowns(newApiDropdowns)
513
- // remove active from sharedFilters that are autoLoading
514
- const dashboardConfig = { ...config.dashboard }
515
- if (config.filterBehavior !== FilterBehavior.Apply) {
516
- dashboardConfig.sharedFilters[index].active = value
517
- } else {
518
- dashboardConfig.sharedFilters[index].queuedActive = value
519
- }
520
- const newSharedFilters = config.dashboard.sharedFilters.map((filter, _index) => {
521
- const _isAutoSelectFilter = !autoLoadViz?.hide.includes(_index)
522
- if (_isAutoSelectFilter) filter.active = ''
523
- return filter
524
- })
525
- const _newConfig = { ...config, dashboard: { ...config.dashboard, sharedFilters: newSharedFilters } }
526
- dispatch({ type: 'SET_CONFIG', payload: _newConfig })
527
- // setData to empty object because we no longer have a data state.
528
- dispatch({ type: 'SET_DATA', payload: {} })
529
- dispatch({ type: 'SET_FILTERED_DATA', payload: {} })
530
- }
531
- }
532
- }
533
-
534
- const Filters = ({ hide, autoLoad }: { hide?: number[]; autoLoad?: boolean }) => {
535
- const { config } = state
536
- if (!config) return <></>
537
- const isLegacyFilter = !config.filterBehavior
538
- return (
539
- <>
540
- {config.dashboard.sharedFilters.map((singleFilter, filterIndex) => {
541
- if ((singleFilter.type !== 'urlfilter' && !singleFilter.showDropdown) || (hide && hide.indexOf(filterIndex) !== -1)) return <></>
542
- const values: JSX.Element[] = []
543
- if (singleFilter.resetLabel) {
544
- values.push(
545
- <option key={`${singleFilter.resetLabel}-option`} value={singleFilter.resetLabel}>
546
- {singleFilter.resetLabel}
547
- </option>
548
- )
549
- }
550
- const _key = singleFilter.apiFilter ? getApiFilterKey(singleFilter.apiFilter) : undefined
551
- if (_key && apiFilterDropdowns[_key]) {
552
- // URL Filter
553
- apiFilterDropdowns[_key]!.forEach(({ text, value }, index) => {
554
- values.push(
555
- <option key={`${value}-option-${index}`} value={value}>
556
- {text}
557
- </option>
558
- )
559
- })
560
- } else {
561
- // Data Filter
562
- singleFilter.values?.forEach((filterOption, index) => {
563
- const labeledOpt = singleFilter.labels && singleFilter.labels[filterOption]
564
- values.push(
565
- <option key={`${singleFilter.key}-option-${index}`} value={filterOption}>
566
- {labeledOpt || filterOption}
567
- </option>
568
- )
569
- })
570
- }
571
-
572
- return (
573
- <div className='cove-dashboard-filters' key={`${singleFilter.key}-filtersection-${filterIndex}`}>
574
- <section className='dashboard-filters-section'>
575
- <label htmlFor={`filter-${filterIndex}`}>{singleFilter.key}</label>
576
- <select
577
- id={`filter-${filterIndex}`}
578
- className='filter-select'
579
- data-index='0'
580
- value={singleFilter.queuedActive || singleFilter.active}
581
- onChange={val => {
582
- handleOnChange(filterIndex, val.target.value)
583
- }}
584
- >
585
- {values}
586
- </select>
587
- </section>
588
- </div>
589
- )
590
- })}
591
-
592
- {!isLegacyFilter && config.filterBehavior === FilterBehavior.Apply && <button onClick={applyFilters}>GO!</button>}
593
- </>
594
- )
595
- }
596
-
597
- const resizeObserver = new ResizeObserver(entries => {
598
- for (let entry of entries) {
599
- let newViewport = getViewport(entry.contentRect.width)
600
-
601
- setCurrentViewport(newViewport)
602
- }
603
- })
604
-
605
- const outerContainerRef = useCallback(node => {
606
- if (node !== null) {
607
- resizeObserver.observe(node)
608
- }
609
- }, [])
610
-
611
- const setPreview = shouldPreview => dispatch({ type: 'SET_PREVIEW', payload: shouldPreview })
612
-
613
- // Prevent render if loading
614
- if (state.loading || !state.config) return <Loading />
615
-
616
- let body: JSX.Element | null = null
617
- // Editor mode
618
- if (isEditor && !state.preview) {
619
- let subVisualizationEditing = false
620
-
621
- getVizKeys(state.config).forEach(visualizationKey => {
622
- let visualizationConfig = { ...state.config?.visualizations[visualizationKey] }
623
-
624
- const dataKey = visualizationConfig.dataKey || 'backwards-compatibility'
625
-
626
- if (state.filteredData && state.filteredData[visualizationKey]) {
627
- visualizationConfig.data = state.filteredData[visualizationKey]
628
- if (visualizationConfig.formattedData) {
629
- visualizationConfig.originalFormattedData = visualizationConfig.formattedData
630
- visualizationConfig.formattedData = visualizationConfig.data
631
- }
632
- } else {
633
- visualizationConfig.data = state.data[dataKey]
634
- if (visualizationConfig.formattedData) {
635
- visualizationConfig.originalFormattedData = visualizationConfig.formattedData
636
- visualizationConfig.formattedData = transform.developerStandardize(visualizationConfig.data, visualizationConfig.dataDescription) || visualizationConfig.data
637
- }
638
- }
639
-
640
- const setsSharedFilter = state.config?.dashboard.sharedFilters && state.config.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.setBy === visualizationKey).length > 0
641
- const setSharedFilterValue = setsSharedFilter ? state.config?.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.setBy === visualizationKey)[0].active : undefined
642
-
643
- if (visualizationConfig.editing) {
644
- subVisualizationEditing = true
645
-
646
- const _updateConfig = newConfig => {
647
- let dataCorrectedConfig = visualizationConfig.originalFormattedData ? { ...newConfig, formattedData: visualizationConfig.originalFormattedData } : newConfig
648
- updateChildConfig(visualizationKey, dataCorrectedConfig)
649
- }
650
-
651
- switch (visualizationConfig.type) {
652
- case 'chart':
653
- body = (
654
- <>
655
- <Header visualizationKey={visualizationKey} subEditor='Chart' />
656
- <CdcChart
657
- key={visualizationKey}
658
- config={visualizationConfig}
659
- isEditor={true}
660
- isDebug={isDebug}
661
- setConfig={_updateConfig}
662
- setSharedFilter={setsSharedFilter ? setSharedFilter : undefined}
663
- setSharedFilterValue={setSharedFilterValue}
664
- dashboardConfig={state.config}
665
- isDashboard={true}
666
- configUrl={undefined}
667
- setEditing={undefined}
668
- hostname={undefined}
669
- link={undefined}
670
- />
671
- </>
672
- )
673
- break
674
- case 'map':
675
- body = (
676
- <>
677
- <Header visualizationKey={visualizationKey} subEditor='Map' />
678
- <CdcMap
679
- key={visualizationKey}
680
- config={visualizationConfig}
681
- isEditor={true}
682
- isDebug={isDebug}
683
- setConfig={_updateConfig}
684
- setSharedFilter={setsSharedFilter ? setSharedFilter : undefined}
685
- setSharedFilterValue={setSharedFilterValue}
686
- isDashboard={true}
687
- showLoader={false}
688
- dashboardConfig={state.config}
689
- />
690
- </>
691
- )
692
- break
693
- case 'data-bite':
694
- visualizationConfig = { ...visualizationConfig, newViz: true }
695
- body = (
696
- <>
697
- <Header visualizationKey={visualizationKey} subEditor='Data Bite' />
698
- <CdcDataBite key={visualizationKey} config={visualizationConfig} isEditor={true} setConfig={_updateConfig} isDashboard={true} />
699
- </>
700
- )
701
- break
702
- case 'waffle-chart':
703
- body = (
704
- <>
705
- <Header visualizationKey={visualizationKey} subEditor='Waffle Chart' />
706
- <CdcWaffleChart key={visualizationKey} config={visualizationConfig} isEditor={true} setConfig={_updateConfig} isDashboard={true} configUrl={undefined} />
707
- </>
708
- )
709
- break
710
- case 'markup-include':
711
- body = (
712
- <>
713
- <Header visualizationKey={visualizationKey} subEditor='Markup Include' />
714
- <CdcMarkupInclude key={visualizationKey} config={visualizationConfig} isEditor={true} setConfig={_updateConfig} isDashboard={true} configUrl={undefined} />
715
- </>
716
- )
717
- break
718
- case 'filtered-text':
719
- body = (
720
- <>
721
- <Header visualizationKey={visualizationKey} subEditor='Filtered Text' />
722
- <CdcFilteredText key={visualizationKey} config={visualizationConfig} isEditor={true} setConfig={_updateConfig} isDashboard={true} configUrl={undefined} />
723
- </>
724
- )
725
- break
726
- case 'filter-dropdowns':
727
- const hideFilter = visualizationConfig.autoLoad && inNoDataState
728
- body = !hideFilter ? (
729
- <>
730
- <Header visualizationKey={visualizationKey} subEditor='Filter Dropdowns' />
731
- <Filters hide={visualizationConfig.hide} autoLoad={visualizationConfig.autoLoad} />
732
- </>
733
- ) : (
734
- <></>
735
- )
736
- break
737
- default:
738
- body = <></>
739
- break
740
- }
741
- }
742
- })
743
-
744
- if (!subVisualizationEditing) {
745
- body = (
746
- <DndProvider backend={HTML5Backend}>
747
- <Header setPreview={setPreview} />
748
- <div className='layout-container'>
749
- <VisualizationsPanel loadConfig={loadConfig} config={state.config} />
750
- <Grid />
751
- </div>
752
- </DndProvider>
753
- )
754
- }
755
- } else {
756
- const { config } = state
757
- const { title, description } = config?.dashboard || {}
758
- body = (
759
- <>
760
- {isEditor && <Header setPreview={setPreview} />}
761
- <div className={`cdc-dashboard-inner-container${isEditor ? ' is-editor' : ''}`}>
762
- <Title title={title} isDashboard={true} classes={[`dashboard-title`, `${config.dashboard.theme ?? 'theme-blue'}`]} />
763
- {/* Description */}
764
- {description && <div className='subtext'>{parse(description)}</div>}
765
- {/* Filters */}
766
- {config.dashboard.sharedFilters && Object.values(config?.visualizations || {}).filter(viz => viz.visualizationType === 'filter-dropdowns').length === 0 && <Filters hide={undefined} autoLoad={undefined} />}
767
-
768
- {/* Visualizations */}
769
- {config.rows &&
770
- config.rows
771
- .filter(row => row.filter(col => col.widget).length !== 0)
772
- .map((row, index) => {
773
- return (
774
- <div className={`dashboard-row ${row.equalHeight ? 'equal-height' : ''}`} key={`row__${index}`}>
775
- {row.map((col, colIndex) => {
776
- if (col.width) {
777
- if (!col.widget) return <div key={`row__${index}__col__${colIndex}`} className={`dashboard-col dashboard-col-${col.width}`}></div>
778
-
779
- let visualizationConfig = { ...config.visualizations[col.widget] }
780
-
781
- const dataKey = visualizationConfig.dataKey || 'backwards-compatibility'
782
-
783
- if (state.filteredData && state.filteredData[col.widget]) {
784
- visualizationConfig.data = state.filteredData[col.widget]
785
- if (visualizationConfig.formattedData) {
786
- visualizationConfig.originalFormattedData = visualizationConfig.formattedData
787
- visualizationConfig.formattedData = visualizationConfig.data
788
- }
789
- } else {
790
- visualizationConfig.data = state.data[dataKey]
791
- if (visualizationConfig.formattedData) {
792
- visualizationConfig.originalFormattedData = visualizationConfig.formattedData
793
- visualizationConfig.formattedData = transform.developerStandardize(visualizationConfig.data, visualizationConfig.dataDescription) || visualizationConfig.data
794
- }
795
- }
796
-
797
- const setsSharedFilter = config.dashboard.sharedFilters && config.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.setBy === col.widget).length > 0
798
- const setSharedFilterValue = setsSharedFilter ? config.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.setBy === col.widget)[0].active : undefined
799
- const tableLink = (
800
- <a href={`#data-table-${visualizationConfig.dataKey}`} className='margin-left-href'>
801
- {visualizationConfig.dataKey} (Go to Table)
802
- </a>
803
- )
804
- const hideFilter = visualizationConfig.autoLoad && inNoDataState
805
- return (
806
- <React.Fragment key={`vis__${index}__${colIndex}`}>
807
- <div className={`dashboard-col dashboard-col-${col.width}`}>
808
- {visualizationConfig.type === 'chart' && (
809
- <CdcChart
810
- key={col.widget}
811
- config={visualizationConfig}
812
- dashboardConfig={config}
813
- isEditor={false}
814
- setConfig={newConfig => {
815
- updateChildConfig(col.widget, newConfig)
816
- }}
817
- setSharedFilter={setsSharedFilter ? setSharedFilter : undefined}
818
- isDashboard={true}
819
- link={config.table && config.table.show && config.datasets && visualizationConfig.table && visualizationConfig.table.showDataTableLink ? tableLink : undefined}
820
- configUrl={undefined}
821
- setEditing={undefined}
822
- hostname={undefined}
823
- setSharedFilterValue={undefined}
824
- />
825
- )}
826
- {visualizationConfig.type === 'map' && (
827
- <CdcMap
828
- key={col.widget}
829
- config={visualizationConfig}
830
- isEditor={false}
831
- setConfig={newConfig => {
832
- updateChildConfig(col.widget, newConfig)
833
- }}
834
- showLoader={false}
835
- setSharedFilter={setsSharedFilter ? setSharedFilter : undefined}
836
- setSharedFilterValue={setSharedFilterValue}
837
- isDashboard={true}
838
- link={config.table && config.table.show && config.datasets && visualizationConfig.table && visualizationConfig.table.showDataTableLink ? tableLink : undefined}
839
- />
840
- )}
841
- {visualizationConfig.type === 'data-bite' && (
842
- <CdcDataBite
843
- key={col.widget}
844
- config={visualizationConfig}
845
- isEditor={false}
846
- setConfig={newConfig => {
847
- updateChildConfig(col.widget, newConfig)
848
- }}
849
- isDashboard={true}
850
- />
851
- )}
852
- {visualizationConfig.type === 'waffle-chart' && (
853
- <CdcWaffleChart
854
- key={col.widget}
855
- config={visualizationConfig}
856
- isEditor={false}
857
- setConfig={newConfig => {
858
- updateChildConfig(col.widget, newConfig)
859
- }}
860
- isDashboard={true}
861
- configUrl={config.table && config.table.show && config.datasets && visualizationConfig.table && visualizationConfig.table.showDataTableLink ? tableLink : undefined}
862
- />
863
- )}
864
- {visualizationConfig.type === 'markup-include' && (
865
- <CdcMarkupInclude
866
- key={col.widget}
867
- config={visualizationConfig}
868
- isEditor={false}
869
- setConfig={newConfig => {
870
- updateChildConfig(col.widget, newConfig)
871
- }}
872
- isDashboard={true}
873
- configUrl={undefined}
874
- />
875
- )}
876
- {visualizationConfig.type === 'filtered-text' && (
877
- <CdcFilteredText
878
- key={col.widget}
879
- config={visualizationConfig}
880
- isEditor={false}
881
- setConfig={newConfig => {
882
- updateChildConfig(col.widget, newConfig)
883
- }}
884
- isDashboard={true}
885
- configUrl={undefined}
886
- />
887
- )}
888
- {visualizationConfig.type === 'filter-dropdowns' && !hideFilter && <Filters hide={visualizationConfig.hide} autoLoad={visualizationConfig.autoLoad} />}
889
- </div>
890
- </React.Fragment>
891
- )
892
- }
893
- return <React.Fragment key={`vis__${index}__${colIndex}`}></React.Fragment>
894
- })}
895
- </div>
896
- )
897
- })}
898
-
899
- {/* Image or PDF Inserts */}
900
- <section className='download-buttons'>
901
- {config.table?.downloadImageButton && <MediaControls.Button title='Download Dashboard as Image' type='image' state={config} text='Download Dashboard Image' elementToCapture={imageId} />}
902
- {config.table?.downloadPdfButton && <MediaControls.Button title='Download Dashboard as PDF' type='pdf' state={config} text='Download Dashboard PDF' elementToCapture={imageId} />}
903
- </section>
904
-
905
- {/* Data Table */}
906
- {config?.table?.show && config?.data && (
907
- <DataTable
908
- config={config}
909
- rawData={config.data}
910
- runtimeData={config.data || []}
911
- expandDataTable={config.table.expanded}
912
- showDownloadButton={config.table.download}
913
- tableTitle={config.dashboard.title || ''}
914
- viewport={currentViewport}
915
- tabbingId={config.dashboard.title || ''}
916
- outerContainerRef={outerContainerRef}
917
- imageRef={imageId}
918
- isDebug={isDebug}
919
- isEditor={isEditor}
920
- />
921
- )}
922
- {config.table?.show &&
923
- config.datasets &&
924
- Object.keys(config.datasets).map(datasetKey => {
925
- //For each dataset, find any shared filters that apply to all visualizations using the dataset
926
- //Apply these filters to the table
927
- let filteredTableData
928
- if (config.dashboard.sharedFilters && config.dashboard.sharedFilters.length > 0) {
929
- //Gets list of visuailzations using the dataset
930
- let vizKeysUsingDataset: string[] = []
931
- getVizKeys(config).forEach(visualizationKey => {
932
- if (config.visualizations[visualizationKey].dataKey === datasetKey) {
933
- vizKeysUsingDataset.push(visualizationKey)
934
- }
935
- })
936
-
937
- //Checks shared filters against list to see if all visualizations are represented
938
- let applicableFilters: SharedFilter[] = []
939
- config.dashboard.sharedFilters.forEach(sharedFilter => {
940
- let allMatch = true
941
- vizKeysUsingDataset.forEach(visualizationKey => {
942
- if (sharedFilter.usedBy && sharedFilter.usedBy.indexOf(visualizationKey) === -1) {
943
- allMatch = false
944
- }
945
- })
946
- if (allMatch) {
947
- applicableFilters.push(sharedFilter)
948
- }
949
- })
950
-
951
- //Applys any applicable filters
952
- if (applicableFilters.length > 0) {
953
- filteredTableData = filterData(applicableFilters, config.datasets[datasetKey].data, state.config?.filterBehavior)
954
- }
955
- }
956
-
957
- return (
958
- <div className='multi-table-container' id={`data-table-${datasetKey}`} key={`data-table-${datasetKey}`}>
959
- <DataTable
960
- config={config as TableConfig}
961
- dataConfig={config.datasets[datasetKey]}
962
- rawData={config.datasets[datasetKey].data}
963
- runtimeData={filteredTableData || config.datasets[datasetKey].data || []}
964
- expandDataTable={config.table.expanded}
965
- tableTitle={datasetKey}
966
- viewport={currentViewport}
967
- tabbingId={datasetKey}
968
- />
969
- </div>
970
- )
971
- })}
972
- </div>
973
- </>
974
- )
975
- }
976
-
977
- const dashboardContainerClasses = ['cdc-open-viz-module', 'type-dashboard', `${currentViewport}`]
978
-
979
- return (
980
- <GlobalContextProvider>
981
- <DashboardContext.Provider value={{ ...state, setParentConfig, outerContainerRef, isDebug }}>
982
- <DashboardDispatchContext.Provider value={dispatch}>
983
- <div className={dashboardContainerClasses.join(' ')} ref={outerContainerRef} data-download-id={imageId}>
984
- {body}
985
- </div>
986
- <OverlayFrame />
987
- </DashboardDispatchContext.Provider>
988
- </DashboardContext.Provider>
989
- </GlobalContextProvider>
990
- )
991
- }
1
+ import { useEffect, useState } from 'react'
2
+ import CdcDashboard from './CdcDashboardComponent'
3
+ import { MultiDashboardConfig } from './types/MultiDashboard'
4
+ import Loading from '@cdc/core/components/Loading'
5
+ import defaults from './data/initial-state'
6
+ import { processData } from './helpers/processData'
7
+ import { getVizKeys } from './helpers/getVizKeys'
8
+ import { processDataLegacy } from './helpers/processDataLegacy'
9
+ import { WCMSProps } from '@cdc/core/types/WCMSProps'
10
+ import { initialState } from './DashboardContext'
11
+ import { getUpdateConfig } from './helpers/getUpdateConfig'
12
+ import { InitialState } from './types/InitialState'
13
+ import { DashboardConfig } from './types/DashboardConfig'
14
+ import _ from 'lodash'
15
+
16
+ type MultiDashboardProps = Omit<WCMSProps, 'configUrl'> & {
17
+ configUrl?: string
18
+ config?: MultiDashboardConfig
19
+ }
20
+
21
+ const MultiDashboardWrapper: React.FC<MultiDashboardProps> = ({ configUrl, config: editorConfig, isEditor, isDebug }) => {
22
+ const [initial, setInitial] = useState<InitialState>(undefined)
23
+ console.log('multi dashboard wrapper')
24
+
25
+ const getSelectedConfig = (config: MultiDashboardConfig, selectedConfig?: string): number | null => {
26
+ if (!config.multiDashboards) return null
27
+ // TODO: if query parameter select based on query parameter
28
+ if (selectedConfig) {
29
+ const foundConfig = Object.values(config.multiDashboards).findIndex(({ label }) => {
30
+ return label === selectedConfig
31
+ })
32
+ if (foundConfig > -1) return foundConfig
33
+ }
34
+ // else select the first available
35
+ return 0
36
+ }
37
+
38
+ const formatInitialState = (newConfig: MultiDashboardConfig | DashboardConfig, datasets: Record<string, Object[]>) => {
39
+ const [config, filteredData] = getUpdateConfig(initialState)(newConfig, datasets)
40
+ return { ...initialState, config, filteredData, data: datasets }
41
+ }
42
+
43
+ const loadConfig = async (selectedConfig?: string) => {
44
+ const _config: MultiDashboardConfig = editorConfig || (await (await fetch(configUrl)).json())
45
+ const selected = getSelectedConfig(_config, selectedConfig)
46
+
47
+ const { newConfig, datasets } = selected !== null ? await loadMultiDashboard(_config, selected) : await loadSingleDashboard(_config)
48
+ setInitial(formatInitialState(newConfig, datasets))
49
+ }
50
+
51
+ useEffect(() => {
52
+ loadConfig()
53
+ }, [])
54
+
55
+ const loadData = async (initialConfig: DashboardConfig | MultiDashboardConfig) => {
56
+ let newConfig = { ...initialConfig }
57
+ let datasets: Record<string, Object[]> = {}
58
+ await Promise.all(
59
+ Object.keys(initialConfig.datasets).map(async key => {
60
+ datasets[key] = await processData(initialConfig.datasets[key], initialConfig.filterBehavior)
61
+ })
62
+ )
63
+
64
+ getVizKeys(newConfig).forEach(vizKey => {
65
+ newConfig.visualizations[vizKey].formattedData = datasets[newConfig.visualizations[vizKey].dataKey]
66
+ })
67
+
68
+ Object.keys(datasets).forEach(key => {
69
+ newConfig.datasets[key].data = datasets[key]
70
+ })
71
+ return { newConfig, datasets }
72
+ }
73
+
74
+ const loadSingleDashboard = async config => {
75
+ let newConfig = { ...defaults, ...config } as DashboardConfig
76
+
77
+ if (config.datasets) {
78
+ return await loadData(newConfig)
79
+ } else {
80
+ const dataKey = newConfig.dataFileName || 'backwards-compatibility'
81
+ const data = await processDataLegacy(config)
82
+
83
+ const datasetsFull = {}
84
+ datasetsFull[dataKey] = {
85
+ data,
86
+ dataDescription: newConfig.dataDescription
87
+ }
88
+ newConfig.datasets = datasetsFull
89
+
90
+ getVizKeys(newConfig).forEach(vizKey => {
91
+ const newData = { dataKey, ..._.pick(newConfig, 'dataDescription', 'formattedData') }
92
+ newConfig.visualizations[vizKey] = { ...newConfig.visualizations[vizKey], ...newData }
93
+ })
94
+
95
+ const blankFields = { data: [], dataUrl: '', dataFileName: '', dataFileSourceType: '', dataDescription: [], formattedData: [] }
96
+ newConfig = { ...newConfig, ...blankFields }
97
+
98
+ if (newConfig.dashboard.filters) {
99
+ const dashboard = { ...newConfig.dashboard }
100
+ // replace filters with sharedFilters
101
+ if (!dashboard.sharedFilters) dashboard.sharedFilters = []
102
+ const filters = dashboard.filters.map(filter => {
103
+ return { ...filter, key: filter.label, showDropdown: true, usedBy: getVizKeys(newConfig) }
104
+ })
105
+ dashboard.sharedFilters = [...dashboard.sharedFilters, ...filters]
106
+ newConfig.dashboard = { ...dashboard, filters: undefined }
107
+ }
108
+
109
+ const datasets: Record<string, Object[]> = { [dataKey]: data }
110
+ return { newConfig, datasets }
111
+ }
112
+ }
113
+
114
+ const loadMultiDashboard = async (multiConfig: MultiDashboardConfig, selectedConfig: number) => {
115
+ const selectedDashboard = multiConfig.multiDashboards[selectedConfig]
116
+ let newConfig = { ...defaults, ...multiConfig, ...selectedDashboard, multiDashboards: multiConfig.multiDashboards, activeDashboard: selectedConfig } as MultiDashboardConfig
117
+ return await loadData(newConfig)
118
+ }
119
+
120
+ if (!initial) return <Loading />
121
+ return <CdcDashboard isEditor={isEditor} isDebug={isDebug} initialState={initial} />
122
+ }
123
+
124
+ export default MultiDashboardWrapper