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