@cdc/dashboard 4.24.3 → 4.24.4

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 (34) hide show
  1. package/dist/cdcdashboard.js +119414 -103311
  2. package/examples/chart-data.json +5409 -0
  3. package/examples/full-dash-test.json +14643 -0
  4. package/examples/full-dashboard.json +10036 -0
  5. package/index.html +2 -2
  6. package/package.json +9 -9
  7. package/src/CdcDashboard.tsx +8 -3
  8. package/src/CdcDashboardComponent.tsx +232 -344
  9. package/src/_stories/Dashboard.stories.tsx +59 -38
  10. package/src/_stories/_mock/api-filter-chart.json +11 -35
  11. package/src/_stories/_mock/api-filter-map.json +17 -31
  12. package/src/_stories/_mock/dashboard-gallery.json +523 -534
  13. package/src/_stories/_mock/multi-viz.json +378 -0
  14. package/src/_stories/_mock/pivot-filter.json +0 -2
  15. package/src/components/DataDesignerModal.tsx +145 -0
  16. package/src/components/Grid.tsx +3 -1
  17. package/src/components/Header/FilterModal.tsx +49 -23
  18. package/src/components/Row.tsx +50 -25
  19. package/src/components/Toggle/Toggle.tsx +6 -7
  20. package/src/components/VisualizationRow.tsx +174 -0
  21. package/src/components/Widget.tsx +21 -103
  22. package/src/helpers/filterData.ts +16 -14
  23. package/src/helpers/getFilteredData.ts +39 -0
  24. package/src/helpers/getUpdateConfig.ts +15 -0
  25. package/src/helpers/getVizConfig.ts +31 -0
  26. package/src/helpers/getVizRowColumnLocator.ts +9 -0
  27. package/src/scss/grid.scss +9 -2
  28. package/src/scss/main.scss +5 -0
  29. package/src/store/dashboard.actions.ts +16 -1
  30. package/src/store/dashboard.reducer.ts +25 -2
  31. package/src/types/APIFilter.ts +4 -5
  32. package/src/types/ConfigRow.ts +12 -3
  33. package/src/types/DataSet.ts +11 -8
  34. package/src/types/SharedFilter.ts +1 -1
@@ -60,8 +60,11 @@ import EditorWrapper from './components/EditorWrapper/EditorWrapper'
60
60
  import DataTableEditorPanel from '@cdc/core/components/DataTable/components/DataTableEditorPanel'
61
61
  import DataTableStandAlone from '@cdc/core/components/DataTable/DataTableStandAlone'
62
62
  import { ViewPort } from '@cdc/core/types/ViewPort'
63
- import Toggle from './components/Toggle'
64
- import { Dashboard } from './types/Dashboard'
63
+ import VisualizationRow from './components/VisualizationRow'
64
+ import { getVizConfig } from './helpers/getVizConfig'
65
+ import { getApplicableFilters, getFilteredData } from './helpers/getFilteredData'
66
+ import { getVizRowColumnLocator } from './helpers/getVizRowColumnLocator'
67
+ import Layout from '@cdc/core/components/Layout'
65
68
 
66
69
  type DashboardProps = Omit<WCMSProps, 'configUrl'> & {
67
70
  initialState: InitialState
@@ -87,6 +90,8 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
87
90
  return vals.some(val => val === undefined)
88
91
  }, [state.data])
89
92
 
93
+ const vizRowColumnLocator = getVizRowColumnLocator(state.config.rows)
94
+
90
95
  const getAutoLoadVisualization = (): Visualization | undefined => {
91
96
  const autoLoadViz = Object.values(state.config.visualizations).filter(vis => {
92
97
  return vis.autoLoad && vis.type === 'filter-dropdowns'
@@ -98,7 +103,7 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
98
103
 
99
104
  const transform = new DataTransform()
100
105
 
101
- const setAutoLoadDefaultValue = (sharedFilterIndex: number, filterDropdowns: DropdownOptions) => {
106
+ const setAutoLoadDefaultValue = (sharedFilterIndex: number, filterDropdowns: DropdownOptions, dashboardConfigOverride) => {
102
107
  const autoLoadViz = getAutoLoadVisualization()
103
108
  if (!autoLoadViz) return // no autoLoading happening
104
109
  const notIncludedInAutoLoad = autoLoadViz.hide
@@ -106,20 +111,22 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
106
111
  // we don't want to auto load it
107
112
  return
108
113
  } else {
109
- const sharedFilter = state.config.dashboard.sharedFilters[sharedFilterIndex]
114
+ const sharedFilters = dashboardConfigOverride.sharedFilters
115
+ const sharedFilter = sharedFilters[sharedFilterIndex]
110
116
  if (sharedFilter.active) return // a value has already been selected.
111
- const filterParents = state.config.dashboard.sharedFilters.filter(f => sharedFilter.parents?.includes(f.key))
112
- const notAllParentFiltersSelected = filterParents.some(p => !p.active)
117
+ const filterParents = sharedFilters.filter(f => sharedFilter.parents?.includes(f.key))
118
+ const notAllParentFiltersSelected = filterParents.some(p => !p.active && !p.queuedActive)
113
119
  if (filterParents && notAllParentFiltersSelected) return
114
120
  const defaultFilterDropdown = filterDropdowns.find(({ value }) => value === sharedFilter.apiFilter!.defaultValue)
115
- let defaultValue = defaultFilterDropdown?.value || filterDropdowns[0].value
116
- changeFilterActive(sharedFilterIndex, defaultValue)
121
+ const defaultValue = defaultFilterDropdown?.value || filterDropdowns[0].value
122
+ sharedFilter.active = defaultValue
117
123
  }
118
124
  }
119
125
 
120
- const loadAPIFilters = async () => {
121
- if (state.config.dashboard.sharedFilters) {
122
- const sharedAPIFilters = state.config.dashboard.sharedFilters.filter(f => f.apiFilter)
126
+ const loadAPIFilters = (dashboardConfigOverride = undefined) => {
127
+ const sharedFilters = (dashboardConfigOverride || state.config.dashboard).sharedFilters
128
+ if (sharedFilters) {
129
+ const sharedAPIFilters = sharedFilters.filter(f => f.apiFilter)
123
130
  const loadingFilterMemo: APIFilterDropdowns = sharedAPIFilters.reduce((acc, curr) => {
124
131
  const _key = getApiFilterKey(curr.apiFilter!)
125
132
  if (apiFilterDropdowns[_key] != null) return acc // don't overwrite fetched data.
@@ -131,63 +138,69 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
131
138
  const getParentParams = (childFilter: SharedFilter): Record<'key' | 'value', string>[] | null => {
132
139
  const _parents = sharedAPIFilters.filter(parentFilter => childFilter.parents?.includes(parentFilter.key))
133
140
  if (!_parents.length) return null
134
- return _parents.map(({ queryParameter, queuedActive }) => ({ key: queryParameter || '', value: queuedActive || '' }))
141
+ return _parents.map(({ queryParameter, active, queuedActive }) => ({ key: queryParameter || '', value: active || queuedActive || '' }))
135
142
  }
136
- const getFilterValues = (data: Object | Array<Object>, apiFilter: APIFilter): DropdownOptions => {
137
- const { textSelector, valueSelector, heirarchyLookup } = apiFilter
138
- if (heirarchyLookup) {
139
- const heirarchy = heirarchyLookup!.split('.')
140
- const selector = heirarchy.shift() // pop first element
141
- return getFilterValues(selector ? data[selector] : data, { ...apiFilter, heirarchyLookup: heirarchy.join('.') })
142
- }
143
- if (!Array.isArray(data)) 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')
143
+ const getFilterValues = (data: Array<Object>, apiFilter: APIFilter): DropdownOptions => {
144
+ const { textSelector, valueSelector } = apiFilter
144
145
  return data.map(v => ({ text: v[textSelector], value: v[valueSelector] }))
145
146
  }
146
- state.config.dashboard.sharedFilters.forEach(async (filter, index) => {
147
- if (!filter.apiFilter) return
147
+ const toFetch = {}
148
+ sharedAPIFilters.forEach((filter, index) => {
148
149
  const baseEndpoint = filter.apiFilter.apiEndpoint
149
150
  const _key = getApiFilterKey(filter.apiFilter)
150
151
  const params = getParentParams(filter)
151
152
  const notAllParentsSelected = params?.some(({ value }) => value === '')
153
+
152
154
  if (notAllParentsSelected) return // don't send request for dependent children filter options
153
- if (apiFilterDropdowns[_key] && !params && filter.filterBy === 'Query String') return // don't reload filter unless it's a child
154
- const endpoint = baseEndpoint + (params ? gatherQueryParams(params) : '')
155
+ if (apiFilterDropdowns[_key] && !params) return // don't reload filter unless it's a child
156
+ const topLevelDataAlreadyLoaded = apiFilterDropdowns[_key] && !filter.parents
157
+ if (topLevelDataAlreadyLoaded) return // don't reload top level filters
155
158
 
156
- fetch(endpoint)
157
- .then(resp => resp.json())
158
- .then(data => {
159
- const apiFilter = filterLookup.get(_key) as APIFilter
160
- const _filterValues = getFilterValues(data, apiFilter)
161
- setAPIFilterDropdowns(dropdowns => ({ ...dropdowns, [_key]: _filterValues }))
162
- setAutoLoadDefaultValue(index, _filterValues)
163
- })
159
+ const endpoint = baseEndpoint + (params ? gatherQueryParams(params) : '')
160
+ toFetch[endpoint] = [_key, index]
164
161
  })
162
+ return Promise.all(
163
+ Object.keys(toFetch).map(
164
+ endpoint =>
165
+ new Promise<void>(resolve => {
166
+ fetch(endpoint)
167
+ .then(resp => resp.json())
168
+ .then(data => {
169
+ const [_key, index] = toFetch[endpoint]
170
+ if (!Array.isArray(data)) throw new Error('COVE only supports response data in the shape Array<Object>')
171
+ const apiFilter = filterLookup.get(_key) as APIFilter
172
+ const _filterValues = getFilterValues(data, apiFilter)
173
+ setAPIFilterDropdowns(dropdowns => ({ ...dropdowns, [_key]: _filterValues }))
174
+ setAutoLoadDefaultValue(index, _filterValues, dashboardConfigOverride)
175
+ })
176
+ .catch(console.error)
177
+ .finally(() => {
178
+ resolve()
179
+ })
180
+ })
181
+ )
182
+ )
165
183
  }
166
184
  }
167
185
 
168
- const getApplicableFilters = (dashboard: Dashboard, key: string): false | SharedFilter[] => {
169
- const c = dashboard.sharedFilters?.filter(sharedFilter => sharedFilter.usedBy && sharedFilter.usedBy.indexOf(key) !== -1)
170
- return c?.length > 0 ? c : false
171
- }
172
-
173
- const reloadURLData = async () => {
174
- const { config } = state
186
+ const reloadURLData = async (dashboardConfigOverride = undefined) => {
187
+ const config = _.cloneDeep(state.config)
175
188
  if (!config.datasets) return
176
189
  let newData = { ...state.data }
177
190
  let newDatasets = { ...config.datasets }
178
191
  let datasetsNeedsUpdate = false
179
192
  let datasetKeys = Object.keys(config.datasets)
180
193
  let newFileName = ''
181
-
194
+ const filters = (dashboardConfigOverride || config.dashboard)?.sharedFilters
182
195
  for (let i = 0; i < datasetKeys.length; i++) {
183
196
  const datasetKey = datasetKeys[i]
184
197
  const dataset = config.datasets[datasetKey]
185
- const filters = config.dashboard?.sharedFilters
198
+
186
199
  if (dataset.dataUrl && filters) {
187
200
  const dataUrl = new URL(dataset.runtimeDataUrl || dataset.dataUrl, window.location.origin)
188
201
  let currentQSParams = Object.fromEntries(new URLSearchParams(dataUrl.search))
189
202
  let updatedQSParams = {}
190
- let isUpdateNeeded = false
203
+ let isUpdateNeeded = !!dashboardConfigOverride
191
204
 
192
205
  filters.forEach(filter => {
193
206
  // filter.active is always a string when filter.type is 'urlfilter'
@@ -259,25 +272,20 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
259
272
  }
260
273
 
261
274
  if (datasetsNeedsUpdate) {
275
+ const dashboardConfig = dashboardConfigOverride || config.dashboard
262
276
  dispatch({ type: 'SET_DATA', payload: newData })
263
277
 
264
- let newFilteredData = {}
265
- let visualizations = { ...config.visualizations }
266
- getVizKeys(config).forEach(key => {
267
- let dataKey = config.visualizations[key].dataKey
268
-
269
- const applicableFilters = getApplicableFilters(config.dashboard, key)
270
- if (applicableFilters) {
271
- newFilteredData[key] = filterData(applicableFilters, newData[dataKey])
272
- }
273
-
278
+ const newFilteredData = getFilteredData(state, {}, newData)
279
+ const visualizations = Object.keys(config.visualizations).reduce((acc, vizKey) => {
280
+ const dataKey = config.visualizations[vizKey].dataKey
274
281
  if (newData[dataKey]) {
275
- visualizations[key].formattedData = newData[dataKey]
282
+ acc[vizKey].formattedData = newData[dataKey]
276
283
  }
277
- })
284
+ return acc
285
+ }, _.cloneDeep(config.visualizations))
278
286
 
279
287
  dispatch({ type: 'SET_FILTERED_DATA', payload: newFilteredData })
280
- dispatch({ type: 'SET_CONFIG', payload: { ...config, datasets: newDatasets, visualizations } })
288
+ dispatch({ type: 'SET_CONFIG', payload: { dashboard: dashboardConfig, datasets: newDatasets, visualizations } })
281
289
  }
282
290
  }
283
291
 
@@ -292,9 +300,8 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
292
300
  }
293
301
 
294
302
  const setSharedFilter = (key, datum) => {
295
- const { config } = state
296
- let newConfig = { ...config }
297
- let newFilteredData = { ...state.filteredData }
303
+ const { config: newConfig, filteredData } = _.cloneDeep(state)
304
+
298
305
  for (let i = 0; i < newConfig.dashboard.sharedFilters.length; i++) {
299
306
  const filter = newConfig.dashboard.sharedFilters[i]
300
307
  if (filter.setBy === key) {
@@ -305,19 +312,10 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
305
312
  }
306
313
  }
307
314
 
308
- getVizKeys(newConfig).forEach(visualizationKey => {
309
- const applicableFilters = getApplicableFilters(newConfig.dashboard, visualizationKey)
310
- if (applicableFilters) {
311
- const visualization = newConfig.visualizations[visualizationKey]
312
-
313
- const formattedData = visualization.dataDescription ? getFormattedData(state.data[visualization.dataKey] || visualization.data, visualization.dataDescription) : undefined
314
-
315
- newFilteredData[visualizationKey] = filterData(applicableFilters, formattedData || state.data[visualization.dataKey])
316
- }
317
- })
315
+ const newFilteredData = getFilteredData({ ...state, config: newConfig }, filteredData)
318
316
 
319
317
  dispatch({ type: 'SET_FILTERED_DATA', payload: newFilteredData })
320
- dispatch({ type: 'SET_CONFIG', payload: newConfig })
318
+ dispatch({ type: 'SET_SHARED_FILTERS', payload: newConfig.dashboard.sharedFilters })
321
319
  }
322
320
 
323
321
  useEffect(() => {
@@ -326,19 +324,19 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
326
324
  reloadURLData()
327
325
  }
328
326
  loadAPIFilters()
329
- }, [JSON.stringify(state.config.dashboard ? state.config.dashboard.sharedFilters : undefined)])
327
+ }, [])
330
328
 
331
329
  const updateChildConfig = (visualizationKey, newConfig) => {
332
- const { config } = state
333
- let updatedConfig = { ...config }
330
+ const config = _.cloneDeep(state.config)
331
+ const updatedConfig = _.pick(config, ['visualizations', 'multiDashboards'])
334
332
  updatedConfig.visualizations[visualizationKey] = newConfig
335
333
  updatedConfig.visualizations[visualizationKey].formattedData = config.visualizations[visualizationKey].formattedData
336
334
  if (config.multiDashboards) {
337
335
  const activeDashboard = config.activeDashboard
338
336
  const multiDashboards = [...config.multiDashboards]
339
337
  const label = multiDashboards[activeDashboard].label
340
- const toSave = _.pick(updatedConfig, ['dashboard', 'visualizations', 'rows'])
341
- multiDashboards[activeDashboard] = { ...toSave, label }
338
+ const toSave = { label, visualizations: updatedConfig.visualizations, ..._.pick(config, ['dashboard', 'rows']) }
339
+ multiDashboards[activeDashboard] = toSave
342
340
  updatedConfig.multiDashboards = multiDashboards
343
341
  }
344
342
 
@@ -350,13 +348,22 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
350
348
  }
351
349
 
352
350
  const applyFilters = () => {
353
- let dashboardConfig = state.config.dashboard
354
- const allFiltersSelected = !state.config.dashboard.sharedFilters.some(filter => !filter.active && !filter.queuedActive)
355
- if (allFiltersSelected) {
351
+ const dashboardConfig = _.cloneDeep(state.config.dashboard)
352
+ const autoLoadViz = getAutoLoadVisualization()
353
+ const nonAutoLoadFilterIndexes = autoLoadViz?.hide || []
354
+ const allRequiredFiltersSelected = !dashboardConfig.sharedFilters.some((filter, filterIndex) => {
355
+ if (nonAutoLoadFilterIndexes.includes(filterIndex)) {
356
+ !filter.active && !filter.queuedActive
357
+ } else {
358
+ // autoload filters don't need to be selected to apply filters
359
+ return false
360
+ }
361
+ })
362
+ if (allRequiredFiltersSelected) {
356
363
  if (state.config.filterBehavior === FilterBehavior.Apply) {
357
364
  const queryParams = getQueryParams()
358
365
  let needsQueryUpdate = false
359
- state.config.dashboard.sharedFilters.forEach((sharedFilter, index) => {
366
+ dashboardConfig.sharedFilters.forEach((sharedFilter, index) => {
360
367
  if (sharedFilter.queuedActive) {
361
368
  dashboardConfig.sharedFilters[index].active = sharedFilter.queuedActive
362
369
  delete dashboardConfig.sharedFilters[index].queuedActive
@@ -373,21 +380,28 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
373
380
  }
374
381
  }
375
382
 
376
- dispatch({ type: 'SET_CONFIG', payload: { ...state.config, dashboard: dashboardConfig } })
383
+ dispatch({ type: 'SET_SHARED_FILTERS', payload: dashboardConfig.sharedFilters })
377
384
  updateDataFilters()
378
- reloadURLData()
385
+ loadAPIFilters(dashboardConfig)
386
+ .then(() => {
387
+ reloadURLData(dashboardConfig)
388
+ })
389
+ .catch(e => {
390
+ console.error(e)
391
+ })
379
392
  } else {
380
393
  // TODO noftify of required fields
381
394
  }
382
395
  }
383
396
 
384
397
  const changeFilterActive = (index: number, value: string | string[]) => {
385
- const { config } = state
386
- let dashboardConfig = { ...config.dashboard }
387
- let filterActive = dashboardConfig.sharedFilters[index]
398
+ const sharedFilters = _.cloneDeep(state.config.dashboard.sharedFilters)
399
+ const filterActive = sharedFilters[index]
400
+ const nonAutoLoadFilterIndexes = getAutoLoadVisualization()?.hide
401
+ const isAutoLoad = nonAutoLoadFilterIndexes && !nonAutoLoadFilterIndexes.includes(index)
388
402
 
389
- if (config.filterBehavior !== FilterBehavior.Apply) {
390
- dashboardConfig.sharedFilters[index].active = value
403
+ if (state.config.filterBehavior !== FilterBehavior.Apply || isAutoLoad) {
404
+ sharedFilters[index].active = value
391
405
 
392
406
  const queryParams = getQueryParams()
393
407
  if (filterActive.setByQueryParameter && queryParams[filterActive.setByQueryParameter] !== filterActive.active) {
@@ -396,47 +410,35 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
396
410
  }
397
411
  } else {
398
412
  if (Array.isArray(value)) throw Error(`Cannot set active values on urlfilters. expected: ${JSON.stringify(value)} to be a single value.`)
399
- dashboardConfig.sharedFilters[index].queuedActive = value
413
+ sharedFilters[index].queuedActive = value
400
414
  }
401
415
 
402
- dispatch({ type: 'SET_CONFIG', payload: { ...config, dashboard: dashboardConfig } })
403
- if (config.filterBehavior !== FilterBehavior.Apply) {
404
- updateDataFilters()
416
+ dispatch({ type: 'SET_SHARED_FILTERS', payload: sharedFilters })
417
+ if (state.config.filterBehavior !== FilterBehavior.Apply) {
418
+ updateDataFilters(sharedFilters)
405
419
  reloadURLData()
406
420
  }
421
+ return sharedFilters
407
422
  }
408
423
 
409
- const updateDataFilters = () => {
410
- const { config } = state
411
- const dashboardConfig = { ...config.dashboard }
412
-
413
- const newFilteredData = {}
414
- getVizKeys(config).forEach(key => {
415
- const applicableFilters = getApplicableFilters(dashboardConfig, key)
416
- if (applicableFilters) {
417
- const visualization = config.visualizations[key]
418
- const _data = state.data[visualization.dataKey] || visualization.data
419
- const formattedData = visualization.dataDescription ? getFormattedData(_data, visualization.dataDescription) : _data
420
-
421
- newFilteredData[key] = filterData(applicableFilters, formattedData)
422
- }
423
- })
424
-
424
+ const updateDataFilters = (sharedFilters = undefined) => {
425
+ const clonedState = _.cloneDeep(state)
426
+ if (sharedFilters) clonedState.config.dashboard.sharedFilters = sharedFilters
427
+ const newFilteredData = getFilteredData(clonedState)
425
428
  dispatch({ type: 'SET_FILTERED_DATA', payload: newFilteredData })
426
429
  }
427
430
 
428
431
  const handleOnChange = (index: number, value: string | string[]) => {
429
- const { config } = state
430
- changeFilterActive(index, value)
432
+ const config = _.cloneDeep(state.config)
433
+ const newSharedFilters = changeFilterActive(index, value)
431
434
  if (config.filterBehavior === FilterBehavior.Apply) {
432
435
  const autoLoadViz = getAutoLoadVisualization()
433
- if (!autoLoadViz) return // nothing left to do for regular filter behavior.
434
- const isAutoSelectFilter = !autoLoadViz.hide.includes(index)
436
+ const isAutoSelectFilter = !autoLoadViz?.hide.includes(index)
435
437
  const missingFilterSelections = config.dashboard.sharedFilters.some(f => !f.active)
436
438
  if (isAutoSelectFilter && !missingFilterSelections) {
437
439
  // a dropdown has been selected that doesn't
438
440
  // require the Go Button
439
- reloadURLData()
441
+ reloadURLData({ sharedFilters: newSharedFilters })
440
442
  } else {
441
443
  // A parent filter was selected, reset filters by:
442
444
  // set auto select filter dropdowns to null
@@ -458,11 +460,12 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
458
460
  if (_isAutoSelectFilter) filter.active = ''
459
461
  return filter
460
462
  })
461
- const _newConfig = { ...config, dashboard: { ...config.dashboard, sharedFilters: newSharedFilters } }
463
+ const _newConfig = { dashboard: { ...config.dashboard, sharedFilters: newSharedFilters } }
462
464
  dispatch({ type: 'SET_CONFIG', payload: _newConfig })
463
465
  // setData to empty object because we no longer have a data state.
464
466
  dispatch({ type: 'SET_DATA', payload: {} })
465
467
  dispatch({ type: 'SET_FILTERED_DATA', payload: {} })
468
+ loadAPIFilters(_newConfig.dashboard)
466
469
  }
467
470
  }
468
471
  }
@@ -499,23 +502,8 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
499
502
  let subVisualizationEditing = false
500
503
 
501
504
  getVizKeys(state.config).forEach(visualizationKey => {
502
- const visualizationConfig = _.cloneDeep(state.config.visualizations[visualizationKey])
503
-
504
- const dataKey = visualizationConfig.dataKey || 'backwards-compatibility'
505
-
506
- if (state.filteredData && state.filteredData[visualizationKey]) {
507
- visualizationConfig.data = state.filteredData[visualizationKey]
508
- if (visualizationConfig.formattedData) {
509
- visualizationConfig.originalFormattedData = visualizationConfig.formattedData
510
- visualizationConfig.formattedData = visualizationConfig.data
511
- }
512
- } else {
513
- visualizationConfig.data = state.data[dataKey]
514
- if (visualizationConfig.formattedData) {
515
- visualizationConfig.originalFormattedData = visualizationConfig.formattedData
516
- visualizationConfig.formattedData = transform.developerStandardize(visualizationConfig.data, visualizationConfig.dataDescription) || visualizationConfig.data
517
- }
518
- }
505
+ const rowNumber = vizRowColumnLocator[visualizationKey]?.row
506
+ const visualizationConfig = getVizConfig(visualizationKey, rowNumber, state.config, state.data, state.filteredData)
519
507
 
520
508
  const setsSharedFilter = state.config.dashboard.sharedFilters && state.config.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.setBy === visualizationKey).length > 0
521
509
  const setSharedFilterValue = setsSharedFilter ? state.config.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.setBy === visualizationKey)[0].active : undefined
@@ -649,226 +637,126 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
649
637
  <>
650
638
  {isEditor && <Header />}
651
639
  <MultiTabs isEditor={isEditor && !isPreview} />
652
- <div className={`cdc-dashboard-inner-container${isEditor ? ' is-editor' : ''}`}>
653
- <Title title={title} isDashboard={true} classes={[`dashboard-title`, `${config.dashboard.theme ?? 'theme-blue'}`]} />
654
- {/* Description */}
655
- {description && <div className='subtext'>{parse(description)}</div>}
656
-
657
- {/* Filters */}
658
- {config.dashboard.sharedFilters && Object.values(config.visualizations || {}).filter(viz => viz.visualizationType === 'filter-dropdowns').length === 0 && (
659
- <>
660
- <Filters filters={state.config.dashboard.sharedFilters} apiFilterDropdowns={apiFilterDropdowns} handleOnChange={handleOnChange} />
661
- <GoButton />
662
- </>
663
- )}
664
-
665
- {/* Visualizations */}
666
- {config.rows &&
667
- config.rows
668
- .filter(row => row.filter(col => col.widget).length !== 0)
669
- .map((row, index) => {
670
- const isToggleRow = row[0].toggle
640
+ <Layout.Responsive isEditor={isEditor}>
641
+ <div className={`cdc-dashboard-inner-container${isEditor ? ' is-editor' : ''}`}>
642
+ <Title title={title} isDashboard={true} classes={[`dashboard-title`, `${config.dashboard.theme ?? 'theme-blue'}`]} />
643
+ {/* Description */}
644
+ {description && <div className='subtext'>{parse(description)}</div>}
645
+
646
+ {/* Filters */}
647
+ {config.dashboard.sharedFilters && Object.values(config.visualizations || {}).filter(viz => viz.visualizationType === 'filter-dropdowns').length === 0 && (
648
+ <>
649
+ <Filters filters={state.config.dashboard.sharedFilters} apiFilterDropdowns={apiFilterDropdowns} handleOnChange={handleOnChange} />
650
+ <GoButton />
651
+ </>
652
+ )}
653
+
654
+ {/* Visualizations */}
655
+ {config.rows &&
656
+ config.rows
657
+ .filter(row => row.columns.filter(col => col.widget).length !== 0)
658
+ .map((row, index) => {
659
+ if (row.multiVizColumn && (isPreview || !isEditor)) {
660
+ const filteredData = getFilteredData(state)
661
+ const data = filteredData[index] ?? row.formattedData
662
+ const dataGroups = {}
663
+ data.forEach(d => {
664
+ const groupKey = d[row.multiVizColumn]
665
+ if (!dataGroups[groupKey]) dataGroups[groupKey] = []
666
+ dataGroups[groupKey].push(d)
667
+ })
668
+ return Object.keys(dataGroups).map(groupName => {
669
+ const dataValue = dataGroups[groupName]
670
+ return (
671
+ <React.Fragment key={`row__${index}__${groupName}`}>
672
+ <h1 className='h4'>{groupName}</h1>
673
+ <VisualizationRow
674
+ filteredDataOverride={dataValue}
675
+ row={row}
676
+ rowIndex={index}
677
+ setSharedFilter={setSharedFilter}
678
+ updateChildConfig={updateChildConfig}
679
+ applyFilters={applyFilters}
680
+ apiFilterDropdowns={apiFilterDropdowns}
681
+ handleOnChange={handleOnChange}
682
+ currentViewport={currentViewport}
683
+ />
684
+ </React.Fragment>
685
+ )
686
+ })
687
+ } else {
688
+ return (
689
+ <VisualizationRow key={`row__${index}`} row={row} rowIndex={index} setSharedFilter={setSharedFilter} updateChildConfig={updateChildConfig} applyFilters={applyFilters} apiFilterDropdowns={apiFilterDropdowns} handleOnChange={handleOnChange} currentViewport={currentViewport} />
690
+ )
691
+ }
692
+ })}
693
+
694
+ {/* Image or PDF Inserts */}
695
+ <section className='download-buttons'>
696
+ {config.table?.downloadImageButton && <MediaControls.Button title='Download Dashboard as Image' type='image' state={config} text='Download Dashboard Image' elementToCapture={imageId} />}
697
+ {config.table?.downloadPdfButton && <MediaControls.Button title='Download Dashboard as PDF' type='pdf' state={config} text='Download Dashboard PDF' elementToCapture={imageId} />}
698
+ </section>
699
+
700
+ {/* Data Table */}
701
+ {config.table?.show && config.data && (
702
+ <DataTable
703
+ config={config}
704
+ rawData={config.data?.[0]?.tableData ? config.data?.[0]?.tableData : config.data}
705
+ runtimeData={config.data?.[0]?.tableData ? config.data?.[0]?.tableData : config.data || []}
706
+ expandDataTable={config.table.expanded}
707
+ showDownloadButton={config.table.download}
708
+ tableTitle={config.dashboard.title || ''}
709
+ viewport={currentViewport}
710
+ tabbingId={config.dashboard.title || ''}
711
+ outerContainerRef={outerContainerRef}
712
+ imageRef={imageId}
713
+ isDebug={isDebug}
714
+ isEditor={isEditor}
715
+ />
716
+ )}
717
+ {config.table?.show &&
718
+ config.datasets &&
719
+ Object.keys(config.datasets).map(datasetKey => {
720
+ //For each dataset, find any shared filters that apply to all visualizations using the dataset
721
+
722
+ //Gets list of visuailzations using the dataset
723
+ const vizKeysUsingDataset: string[] = getVizKeys(config).filter(visualizationKey => {
724
+ return config.visualizations[visualizationKey].dataKey === datasetKey
725
+ })
726
+
727
+ //Checks shared filters against list to see if all visualizations are represented
728
+ let applicableFilters: SharedFilter[] = []
729
+ config.dashboard.sharedFilters?.forEach(sharedFilter => {
730
+ let allMatch = true
731
+ vizKeysUsingDataset.forEach(visualizationKey => {
732
+ if (sharedFilter.usedBy && sharedFilter.usedBy.indexOf(visualizationKey) === -1) {
733
+ allMatch = false
734
+ }
735
+ })
736
+ if (allMatch) {
737
+ applicableFilters.push(sharedFilter)
738
+ }
739
+ })
740
+
741
+ //Applys any applicable filters to the Table
742
+ const filteredTableData = applicableFilters.length > 0 ? filterData(applicableFilters, config.datasets[datasetKey].data) : undefined
671
743
  return (
672
- <div className={`dashboard-row ${row.equalHeight ? 'equal-height' : ''} ${isToggleRow ? 'toggle' : ''}`} key={`row__${index}`}>
673
- {isToggleRow && <Toggle row={row} rowIndex={index} visualizations={config.visualizations} />}
674
- {row.map((col, colIndex) => {
675
- if (col.width) {
676
- if (!col.widget) return <div key={`row__${index}__col__${colIndex}`} className={`dashboard-col dashboard-col-${col.width}`}></div>
677
-
678
- const visualizationConfig = _.cloneDeep(config.visualizations[col.widget])
679
-
680
- const dataKey = visualizationConfig.dataKey || 'backwards-compatibility'
681
-
682
- if (state.filteredData && state.filteredData[col.widget]) {
683
- visualizationConfig.data = state.filteredData[col.widget]
684
- if (visualizationConfig.formattedData) {
685
- visualizationConfig.originalFormattedData = visualizationConfig.formattedData
686
- visualizationConfig.formattedData = visualizationConfig.data
687
- }
688
- } else {
689
- visualizationConfig.data = state.data[dataKey]
690
- if (visualizationConfig.formattedData) {
691
- visualizationConfig.originalFormattedData = visualizationConfig.formattedData
692
- visualizationConfig.formattedData = transform.developerStandardize(visualizationConfig.data, visualizationConfig.dataDescription) || visualizationConfig.data
693
- }
694
- }
695
-
696
- const setsSharedFilter = config.dashboard.sharedFilters && config.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.setBy === col.widget).length > 0
697
- const setSharedFilterValue = setsSharedFilter ? config.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.setBy === col.widget)[0].active : undefined
698
- const tableLink = (
699
- <a href={`#data-table-${visualizationConfig.dataKey}`} className='margin-left-href'>
700
- {visualizationConfig.dataKey} (Go to Table)
701
- </a>
702
- )
703
- const hideFilter = visualizationConfig.autoLoad && inNoDataState
704
-
705
- const hiddenToggle = col.hide !== undefined ? col.hide : colIndex !== 0
706
- const hidden = col.toggle ? hiddenToggle : false
707
- return (
708
- <React.Fragment key={`vis__${index}__${colIndex}`}>
709
- <div className={`dashboard-col dashboard-col-${col.width} ${hidden ? 'hidden-toggle' : ''}`}>
710
- {visualizationConfig.type === 'chart' && (
711
- <CdcChart
712
- key={col.widget}
713
- config={visualizationConfig}
714
- dashboardConfig={config}
715
- isEditor={false}
716
- setConfig={newConfig => {
717
- updateChildConfig(col.widget, newConfig)
718
- }}
719
- setSharedFilter={setsSharedFilter ? setSharedFilter : undefined}
720
- isDashboard={true}
721
- link={config.table && config.table.show && config.datasets && visualizationConfig.table && visualizationConfig.table.showDataTableLink ? tableLink : undefined}
722
- configUrl={undefined}
723
- setEditing={undefined}
724
- hostname={undefined}
725
- setSharedFilterValue={undefined}
726
- />
727
- )}
728
- {visualizationConfig.type === 'map' && (
729
- <CdcMap
730
- key={col.widget}
731
- config={visualizationConfig}
732
- isEditor={false}
733
- setConfig={newConfig => {
734
- updateChildConfig(col.widget, newConfig)
735
- }}
736
- showLoader={false}
737
- setSharedFilter={setsSharedFilter ? setSharedFilter : undefined}
738
- setSharedFilterValue={setSharedFilterValue}
739
- isDashboard={true}
740
- link={config.table && config.table.show && config.datasets && visualizationConfig.table && visualizationConfig.table.showDataTableLink ? tableLink : undefined}
741
- />
742
- )}
743
- {visualizationConfig.type === 'data-bite' && (
744
- <CdcDataBite
745
- key={col.widget}
746
- config={visualizationConfig}
747
- isEditor={false}
748
- setConfig={newConfig => {
749
- updateChildConfig(col.widget, newConfig)
750
- }}
751
- isDashboard={true}
752
- />
753
- )}
754
- {visualizationConfig.type === 'waffle-chart' && (
755
- <CdcWaffleChart
756
- key={col.widget}
757
- config={visualizationConfig}
758
- isEditor={false}
759
- setConfig={newConfig => {
760
- updateChildConfig(col.widget, newConfig)
761
- }}
762
- isDashboard={true}
763
- configUrl={config.table && config.table.show && config.datasets && visualizationConfig.table && visualizationConfig.table.showDataTableLink ? tableLink : undefined}
764
- />
765
- )}
766
- {visualizationConfig.type === 'markup-include' && (
767
- <CdcMarkupInclude
768
- key={col.widget}
769
- config={visualizationConfig}
770
- isEditor={false}
771
- setConfig={newConfig => {
772
- updateChildConfig(col.widget, newConfig)
773
- }}
774
- isDashboard={true}
775
- configUrl={undefined}
776
- />
777
- )}
778
- {visualizationConfig.type === 'filtered-text' && (
779
- <CdcFilteredText
780
- key={col.widget}
781
- config={visualizationConfig}
782
- isEditor={false}
783
- setConfig={newConfig => {
784
- updateChildConfig(col.widget, newConfig)
785
- }}
786
- isDashboard={true}
787
- configUrl={undefined}
788
- />
789
- )}
790
- {visualizationConfig.type === 'filter-dropdowns' && !hideFilter && (
791
- <>
792
- <Filters hide={visualizationConfig.hide} filters={state.config.dashboard.sharedFilters} apiFilterDropdowns={apiFilterDropdowns} handleOnChange={handleOnChange} />
793
- <GoButton autoLoad={visualizationConfig.autoLoad} />
794
- </>
795
- )}
796
- {visualizationConfig.type === 'table' && <DataTableStandAlone key={col.widget} visualizationKey={col.widget} config={visualizationConfig} viewport={currentViewport} />}
797
- </div>
798
- </React.Fragment>
799
- )
800
- }
801
- return <React.Fragment key={`vis__${index}__${colIndex}`}></React.Fragment>
802
- })}
744
+ <div className='multi-table-container' id={`data-table-${datasetKey}`} key={`data-table-${datasetKey}`}>
745
+ <DataTable
746
+ config={config as TableConfig}
747
+ dataConfig={config.datasets[datasetKey]}
748
+ rawData={config.datasets[datasetKey].data?.[0]?.tableData || config.datasets[datasetKey].data}
749
+ runtimeData={config.datasets[datasetKey].data?.[0]?.tableData || filteredTableData || config.datasets[datasetKey].data || []}
750
+ expandDataTable={config.table.expanded}
751
+ tableTitle={datasetKey}
752
+ viewport={currentViewport}
753
+ tabbingId={datasetKey}
754
+ />
803
755
  </div>
804
756
  )
805
757
  })}
806
-
807
- {/* Image or PDF Inserts */}
808
- <section className='download-buttons'>
809
- {config.table?.downloadImageButton && <MediaControls.Button title='Download Dashboard as Image' type='image' state={config} text='Download Dashboard Image' elementToCapture={imageId} />}
810
- {config.table?.downloadPdfButton && <MediaControls.Button title='Download Dashboard as PDF' type='pdf' state={config} text='Download Dashboard PDF' elementToCapture={imageId} />}
811
- </section>
812
-
813
- {/* Data Table */}
814
- {config.table?.show && config.data && (
815
- <DataTable
816
- config={config}
817
- rawData={config.data?.[0]?.tableData ? config.data?.[0]?.tableData : config.data}
818
- runtimeData={config.data?.[0]?.tableData ? config.data?.[0]?.tableData : config.data || []}
819
- expandDataTable={config.table.expanded}
820
- showDownloadButton={config.table.download}
821
- tableTitle={config.dashboard.title || ''}
822
- viewport={currentViewport}
823
- tabbingId={config.dashboard.title || ''}
824
- outerContainerRef={outerContainerRef}
825
- imageRef={imageId}
826
- isDebug={isDebug}
827
- isEditor={isEditor}
828
- />
829
- )}
830
- {config.table?.show &&
831
- config.datasets &&
832
- Object.keys(config.datasets).map(datasetKey => {
833
- //For each dataset, find any shared filters that apply to all visualizations using the dataset
834
-
835
- //Gets list of visuailzations using the dataset
836
- const vizKeysUsingDataset: string[] = getVizKeys(config).filter(visualizationKey => {
837
- return config.visualizations[visualizationKey].dataKey === datasetKey
838
- })
839
-
840
- //Checks shared filters against list to see if all visualizations are represented
841
- let applicableFilters: SharedFilter[] = []
842
- config.dashboard.sharedFilters.forEach(sharedFilter => {
843
- let allMatch = true
844
- vizKeysUsingDataset.forEach(visualizationKey => {
845
- if (sharedFilter.usedBy && sharedFilter.usedBy.indexOf(visualizationKey) === -1) {
846
- allMatch = false
847
- }
848
- })
849
- if (allMatch) {
850
- applicableFilters.push(sharedFilter)
851
- }
852
- })
853
-
854
- //Applys any applicable filters to the Table
855
- const filteredTableData = applicableFilters.length > 0 ? filterData(applicableFilters, config.datasets[datasetKey].data) : undefined
856
- return (
857
- <div className='multi-table-container' id={`data-table-${datasetKey}`} key={`data-table-${datasetKey}`}>
858
- <DataTable
859
- config={config as TableConfig}
860
- dataConfig={config.datasets[datasetKey]}
861
- rawData={config.datasets[datasetKey].data?.[0]?.tableData || config.datasets[datasetKey].data}
862
- runtimeData={config.datasets[datasetKey].data?.[0]?.tableData || filteredTableData || config.datasets[datasetKey].data || []}
863
- expandDataTable={config.table.expanded}
864
- tableTitle={datasetKey}
865
- viewport={currentViewport}
866
- tabbingId={datasetKey}
867
- />
868
- </div>
869
- )
870
- })}
871
- </div>
758
+ </div>
759
+ </Layout.Responsive>
872
760
  </>
873
761
  )
874
762
  }