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