@cdc/dashboard 4.24.4 → 4.24.7

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 (75) hide show
  1. package/dist/cdcdashboard.js +179228 -141419
  2. package/examples/custom/css/respiratory.css +236 -0
  3. package/examples/custom/js/respiratory.js +242 -0
  4. package/examples/default-multi-dataset-shared-filter.json +1729 -0
  5. package/examples/ed-visits-county-file.json +618 -0
  6. package/examples/filtered-dash.json +6 -21
  7. package/index.html +12 -3
  8. package/package.json +12 -11
  9. package/src/CdcDashboard.tsx +5 -1
  10. package/src/CdcDashboardComponent.tsx +156 -334
  11. package/src/DashboardContext.tsx +9 -1
  12. package/src/_stories/Dashboard.stories.tsx +31 -3
  13. package/src/_stories/_mock/dashboard-gallery.json +534 -523
  14. package/src/_stories/_mock/markup-include.json +78 -0
  15. package/src/_stories/_mock/multi-dashboards.json +914 -0
  16. package/src/_stories/_mock/multi-viz.json +2 -3
  17. package/src/_stories/_mock/pivot-filter.json +15 -11
  18. package/src/_stories/_mock/standalone-table.json +2 -0
  19. package/src/components/CollapsibleVisualizationRow.tsx +44 -0
  20. package/src/components/Column.tsx +1 -1
  21. package/src/components/DashboardFilters/DashboardFilters.tsx +80 -0
  22. package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +218 -0
  23. package/src/components/DashboardFilters/DashboardFiltersEditor/components/DeleteFilterModal.tsx +48 -0
  24. package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +367 -0
  25. package/src/components/DashboardFilters/DashboardFiltersEditor/index.ts +1 -0
  26. package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +143 -0
  27. package/src/components/DashboardFilters/index.ts +3 -0
  28. package/src/components/DataDesignerModal.tsx +9 -9
  29. package/src/components/ExpandCollapseButtons.tsx +20 -0
  30. package/src/components/Header/Header.tsx +1 -97
  31. package/src/components/MultiConfigTabs/MultiConfigTabs.tsx +4 -4
  32. package/src/components/MultiConfigTabs/MultiTabs.tsx +3 -2
  33. package/src/components/Row.tsx +52 -19
  34. package/src/components/Toggle/Toggle.tsx +2 -4
  35. package/src/components/VisualizationRow.tsx +96 -29
  36. package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +116 -0
  37. package/src/components/VisualizationsPanel/index.ts +1 -0
  38. package/src/components/VisualizationsPanel/visualizations-panel-styles.css +12 -0
  39. package/src/components/Widget.tsx +26 -90
  40. package/src/helpers/apiFilterHelpers.ts +51 -0
  41. package/src/helpers/changeFilterActive.ts +30 -0
  42. package/src/helpers/filterData.ts +16 -56
  43. package/src/helpers/generateValuesForFilter.ts +1 -1
  44. package/src/helpers/getAutoLoadVisualization.ts +11 -0
  45. package/src/helpers/getFilteredData.ts +4 -2
  46. package/src/helpers/getVizConfig.ts +23 -2
  47. package/src/helpers/getVizRowColumnLocator.ts +2 -1
  48. package/src/helpers/hasDashboardApplyBehavior.ts +5 -0
  49. package/src/helpers/iconHash.tsx +3 -3
  50. package/src/helpers/mapDataToConfig.ts +29 -0
  51. package/src/helpers/processData.ts +2 -3
  52. package/src/helpers/reloadURLHelpers.ts +68 -0
  53. package/src/helpers/tests/filterData.test.ts +1 -93
  54. package/src/scss/editor-panel.scss +1 -1
  55. package/src/scss/grid.scss +34 -27
  56. package/src/scss/main.scss +41 -3
  57. package/src/scss/variables.scss +4 -0
  58. package/src/store/dashboard.actions.ts +9 -10
  59. package/src/store/dashboard.reducer.ts +41 -13
  60. package/src/types/APIFilter.ts +1 -4
  61. package/src/types/ConfigRow.ts +2 -0
  62. package/src/types/Dashboard.ts +1 -1
  63. package/src/types/DashboardConfig.ts +2 -4
  64. package/src/types/DashboardFilters.ts +7 -0
  65. package/src/types/InitialState.ts +1 -1
  66. package/src/types/MultiDashboard.ts +2 -2
  67. package/src/types/SharedFilter.ts +2 -5
  68. package/src/types/Tab.ts +1 -1
  69. package/LICENSE +0 -201
  70. package/src/components/EditorWrapper/EditorWrapper.tsx +0 -52
  71. package/src/components/EditorWrapper/editor-wrapper.style.css +0 -13
  72. package/src/components/Filters.tsx +0 -88
  73. package/src/components/Header/FilterModal.tsx +0 -506
  74. package/src/components/VisualizationsPanel.tsx +0 -72
  75. package/src/helpers/getApiFilterKey.ts +0 -5
@@ -18,7 +18,6 @@ import OverlayFrame from '@cdc/core/components/ui/OverlayFrame'
18
18
  import Loading from '@cdc/core/components/Loading'
19
19
  import { DataTransform } from '@cdc/core/helpers/DataTransform'
20
20
  import getViewport from '@cdc/core/helpers/getViewport'
21
- import { getQueryParams, updateQueryString } from '@cdc/core/helpers/queryStringUtils'
22
21
 
23
22
  import CdcMap from '@cdc/map'
24
23
  import CdcChart from '@cdc/chart'
@@ -28,19 +27,16 @@ import CdcMarkupInclude from '@cdc/markup-include'
28
27
  import CdcFilteredText from '@cdc/filtered-text'
29
28
 
30
29
  import Grid from './components/Grid'
31
- import Header, { FilterBehavior } from './components/Header/Header'
30
+ import Header from './components/Header/Header'
32
31
  import DataTable from '@cdc/core/components/DataTable'
33
32
  import MediaControls from '@cdc/core/components/MediaControls'
34
33
 
35
34
  import './scss/main.scss'
36
35
  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
36
 
40
37
  import VisualizationsPanel from './components/VisualizationsPanel'
41
38
  import dashboardReducer from './store/dashboard.reducer'
42
39
  import { filterData } from './helpers/filterData'
43
- import { getFormattedData } from './helpers/getFormattedData'
44
40
  import { getVizKeys } from './helpers/getVizKeys'
45
41
  import Title from '@cdc/core/components/ui/Title'
46
42
  import { type TableConfig } from '@cdc/core/components/DataTable/types/TableConfig'
@@ -48,23 +44,27 @@ import { type TableConfig } from '@cdc/core/components/DataTable/types/TableConf
48
44
  // types
49
45
  import { type SharedFilter } from './types/SharedFilter'
50
46
  import { type APIFilter } from './types/APIFilter'
51
- import { type Visualization } from '@cdc/core/types/Visualization'
52
47
  import { type WCMSProps } from '@cdc/core/types/WCMSProps'
53
48
  import { type InitialState } from './types/InitialState'
54
49
  import MultiTabs from './components/MultiConfigTabs'
55
50
  import _ from 'lodash'
56
51
  import EditorContext from '../../editor/src/ConfigContext'
57
- import { getApiFilterKey } from './helpers/getApiFilterKey'
58
- import Filters, { APIFilterDropdowns, DropdownOptions } from './components/Filters'
59
- import EditorWrapper from './components/EditorWrapper/EditorWrapper'
60
- import DataTableEditorPanel from '@cdc/core/components/DataTable/components/DataTableEditorPanel'
52
+ import { APIFilterDropdowns, DropdownOptions } from './components/DashboardFilters'
61
53
  import DataTableStandAlone from '@cdc/core/components/DataTable/DataTableStandAlone'
62
54
  import { ViewPort } from '@cdc/core/types/ViewPort'
63
55
  import VisualizationRow from './components/VisualizationRow'
64
56
  import { getVizConfig } from './helpers/getVizConfig'
65
- import { getApplicableFilters, getFilteredData } from './helpers/getFilteredData'
57
+ import { getFilteredData } from './helpers/getFilteredData'
66
58
  import { getVizRowColumnLocator } from './helpers/getVizRowColumnLocator'
67
59
  import Layout from '@cdc/core/components/Layout'
60
+ import FootnotesStandAlone from '@cdc/core/components/Footnotes/FootnotesStandAlone'
61
+ import * as apiFilterHelpers from './helpers/apiFilterHelpers'
62
+ import * as reloadURLHelpers from './helpers/reloadURLHelpers'
63
+ import { addValuesToFilters } from '@cdc/core/helpers/addValuesToFilters'
64
+ import { DashboardFilters } from './types/DashboardFilters'
65
+ import DashboardSharedFilters from './components/DashboardFilters'
66
+ import ExpandCollapseButtons from './components/ExpandCollapseButtons'
67
+ import { hasDashboardApplyBehavior } from './helpers/hasDashboardApplyBehavior'
68
68
 
69
69
  type DashboardProps = Omit<WCMSProps, 'configUrl'> & {
70
70
  initialState: InitialState
@@ -76,13 +76,9 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
76
76
  const [apiFilterDropdowns, setAPIFilterDropdowns] = useState<APIFilterDropdowns>({})
77
77
  const [currentViewport, setCurrentViewport] = useState<ViewPort>('lg')
78
78
  const [imageId] = useState(`cove-${Math.random().toString(16).slice(-4)}`)
79
+ const [allExpanded, setAllExpanded] = useState(true)
79
80
 
80
81
  const isPreview = state.tabSelected === 'Dashboard Preview'
81
- const replacements = {
82
- 'Remove Spaces': '',
83
- 'Keep Spaces': ' ',
84
- 'Replace With Underscore': '_'
85
- }
86
82
 
87
83
  const inNoDataState = useMemo(() => {
88
84
  const vals = Object.values(state.data)
@@ -92,134 +88,97 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
92
88
 
93
89
  const vizRowColumnLocator = getVizRowColumnLocator(state.config.rows)
94
90
 
95
- const getAutoLoadVisualization = (): Visualization | undefined => {
96
- const autoLoadViz = Object.values(state.config.visualizations).filter(vis => {
97
- return vis.autoLoad && vis.type === 'filter-dropdowns'
98
- })
99
- if (autoLoadViz.length === 0) return
100
- if (autoLoadViz.length > 1) throw new Error('Only one filter row can be autoloaded')
101
- return autoLoadViz[0]
102
- }
103
-
104
91
  const transform = new DataTransform()
105
92
 
106
- const setAutoLoadDefaultValue = (sharedFilterIndex: number, filterDropdowns: DropdownOptions, dashboardConfigOverride) => {
107
- const autoLoadViz = getAutoLoadVisualization()
108
- if (!autoLoadViz) return // no autoLoading happening
109
- const notIncludedInAutoLoad = autoLoadViz.hide
110
- if (notIncludedInAutoLoad.includes(sharedFilterIndex)) {
111
- // we don't want to auto load it
112
- return
113
- } else {
114
- const sharedFilters = dashboardConfigOverride.sharedFilters
115
- const sharedFilter = sharedFilters[sharedFilterIndex]
116
- if (sharedFilter.active) return // a value has already been selected.
93
+ const autoLoadFilterIndexes = useMemo(() => {
94
+ return Object.values(state.config.visualizations)
95
+ .filter(v => v.type === 'dashboardFilters')
96
+ .reduce((acc, viz: DashboardFilters) => (viz.autoLoad ? [...acc, ...viz.sharedFilterIndexes] : acc), [])
97
+ }, [state.config.visualizations])
98
+
99
+ const setAutoLoadDefaultValue = (sharedFilterIndex: number, dropdownOptions: DropdownOptions, sharedFilters): SharedFilter => {
100
+ const sharedFilter = _.cloneDeep(sharedFilters[sharedFilterIndex])
101
+ if (!autoLoadFilterIndexes.length || !dropdownOptions) return sharedFilter // no autoLoading happening
102
+ if (!sharedFilter.active && autoLoadFilterIndexes.includes(sharedFilterIndex)) {
117
103
  const filterParents = sharedFilters.filter(f => sharedFilter.parents?.includes(f.key))
118
- const notAllParentFiltersSelected = filterParents.some(p => !p.active && !p.queuedActive)
119
- if (filterParents && notAllParentFiltersSelected) return
120
- const defaultFilterDropdown = filterDropdowns.find(({ value }) => value === sharedFilter.apiFilter!.defaultValue)
121
- const defaultValue = defaultFilterDropdown?.value || filterDropdowns[0].value
104
+ const notAllParentFiltersSelected = filterParents.some(p => !(p.active || p.queuedActive))
105
+ if (filterParents && notAllParentFiltersSelected) return sharedFilter
106
+ // TODO get default value from query parameter
107
+ const defaultValue = dropdownOptions[0].value
122
108
  sharedFilter.active = defaultValue
123
109
  }
110
+ return sharedFilter
124
111
  }
125
112
 
126
- const loadAPIFilters = (dashboardConfigOverride = undefined) => {
127
- const sharedFilters = (dashboardConfigOverride || state.config.dashboard).sharedFilters
128
- if (sharedFilters) {
129
- const sharedAPIFilters = sharedFilters.filter(f => f.apiFilter)
130
- const loadingFilterMemo: APIFilterDropdowns = sharedAPIFilters.reduce((acc, curr) => {
131
- const _key = getApiFilterKey(curr.apiFilter!)
132
- if (apiFilterDropdowns[_key] != null) return acc // don't overwrite fetched data.
133
- acc[_key] = null
113
+ const loadAPIFilters = (sharedFilters: SharedFilter[], dropdowns = apiFilterDropdowns, recursiveLimit = 3): Promise<SharedFilter[]> => {
114
+ if (!sharedFilters) return
115
+ sharedFilters = sharedFilters.map((filter, index) => setAutoLoadDefaultValue(index, dropdowns[filter.apiFilter?.apiEndpoint], sharedFilters))
116
+ const sharedAPIFilters = sharedFilters.filter(f => f.apiFilter)
117
+ const loadingFilterMemo = apiFilterHelpers.getLoadingFilterMemo(sharedAPIFilters, dropdowns)
118
+ setAPIFilterDropdowns({ ...dropdowns, ...loadingFilterMemo })
119
+ const filterLookup = new Map(sharedAPIFilters.map(filter => [filter.apiFilter.apiEndpoint, filter.apiFilter]))
120
+ const toFetch = apiFilterHelpers.getToFetch(sharedAPIFilters, dropdowns)
121
+ const newDropdowns = _.cloneDeep(dropdowns)
122
+ return Promise.all(
123
+ Object.keys(toFetch).map(
124
+ endpoint =>
125
+ new Promise<void>(resolve => {
126
+ fetch(endpoint)
127
+ .then(resp => resp.json())
128
+ .then(data => {
129
+ const [_key, index] = toFetch[endpoint]
130
+ if (!Array.isArray(data)) throw new Error('COVE only supports response data in the shape Array<Object>')
131
+ const apiFilter = filterLookup.get(_key) as APIFilter
132
+ const _filterValues = apiFilterHelpers.getFilterValues(data, apiFilter)
133
+ newDropdowns[_key] = _filterValues
134
+ const newDefaultSelectedFilter = setAutoLoadDefaultValue(index, _filterValues, sharedFilters)
135
+ sharedFilters[index] = newDefaultSelectedFilter
136
+ })
137
+ .catch(console.error)
138
+ .finally(() => {
139
+ resolve()
140
+ })
141
+ })
142
+ )
143
+ ).then(() => {
144
+ const finishedLoading = sharedFilters.reduce((acc, curr, index) => {
145
+ if (autoLoadFilterIndexes.includes(index) && !curr.active) return false
134
146
  return acc
135
- }, {})
136
- setAPIFilterDropdowns({ ...apiFilterDropdowns, ...loadingFilterMemo })
137
- const filterLookup = new Map(sharedAPIFilters.map(filter => [getApiFilterKey(filter.apiFilter!), filter.apiFilter!]))
138
- const getParentParams = (childFilter: SharedFilter): Record<'key' | 'value', string>[] | null => {
139
- const _parents = sharedAPIFilters.filter(parentFilter => childFilter.parents?.includes(parentFilter.key))
140
- if (!_parents.length) return null
141
- return _parents.map(({ queryParameter, active, queuedActive }) => ({ key: queryParameter || '', value: active || queuedActive || '' }))
142
- }
143
- const getFilterValues = (data: Array<Object>, apiFilter: APIFilter): DropdownOptions => {
144
- const { textSelector, valueSelector } = apiFilter
145
- return data.map(v => ({ text: v[textSelector], value: v[valueSelector] }))
147
+ }, true)
148
+ if (finishedLoading || recursiveLimit === 0) {
149
+ setAPIFilterDropdowns(dropdowns => ({ ...dropdowns, ...newDropdowns }))
150
+ dispatch({ type: 'SET_SHARED_FILTERS', payload: sharedFilters })
151
+ return sharedFilters
152
+ } else {
153
+ return loadAPIFilters(sharedFilters, newDropdowns, recursiveLimit - 1)
146
154
  }
147
- const toFetch = {}
148
- sharedAPIFilters.forEach((filter, index) => {
149
- const baseEndpoint = filter.apiFilter.apiEndpoint
150
- const _key = getApiFilterKey(filter.apiFilter)
151
- const params = getParentParams(filter)
152
- const notAllParentsSelected = params?.some(({ value }) => value === '')
153
-
154
- if (notAllParentsSelected) return // don't send request for dependent children filter options
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
158
-
159
- const endpoint = baseEndpoint + (params ? gatherQueryParams(params) : '')
160
- toFetch[endpoint] = [_key, index]
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
- )
183
- }
155
+ })
184
156
  }
185
157
 
186
- const reloadURLData = async (dashboardConfigOverride = undefined) => {
158
+ const reloadURLData = async (newFilters?: SharedFilter[]) => {
187
159
  const config = _.cloneDeep(state.config)
188
160
  if (!config.datasets) return
189
- let newData = { ...state.data }
190
- let newDatasets = { ...config.datasets }
191
- let datasetsNeedsUpdate = false
192
- let datasetKeys = Object.keys(config.datasets)
161
+ const filters = newFilters || config.dashboard.sharedFilters
162
+ const datasetKeys = Object.keys(config.datasets)
163
+
164
+ const newData = _.cloneDeep(state.data)
165
+ const newDatasets = _.cloneDeep(config.datasets)
166
+ let dataWasFetched = false
193
167
  let newFileName = ''
194
- const filters = (dashboardConfigOverride || config.dashboard)?.sharedFilters
168
+
195
169
  for (let i = 0; i < datasetKeys.length; i++) {
196
170
  const datasetKey = datasetKeys[i]
197
171
  const dataset = config.datasets[datasetKey]
198
172
 
199
173
  if (dataset.dataUrl && filters) {
200
174
  const dataUrl = new URL(dataset.runtimeDataUrl || dataset.dataUrl, window.location.origin)
201
- let currentQSParams = Object.fromEntries(new URLSearchParams(dataUrl.search))
202
- let updatedQSParams = {}
203
- let isUpdateNeeded = !!dashboardConfigOverride
204
-
175
+ const currentQSParams = Object.fromEntries(new URLSearchParams(dataUrl.search))
176
+ const updatedQSParams = {}
205
177
  filters.forEach(filter => {
206
178
  // filter.active is always a string when filter.type is 'urlfilter'
207
179
  if (filter.type === 'urlfilter' && !Array.isArray(filter.active)) {
208
180
  if (filter.filterBy === 'File Name') {
209
- isUpdateNeeded = true
210
- if (filter.datasetKey === datasetKey) {
211
- if (filter.fileName) {
212
- // if a file name is found, ie, state_${query}, use that, ie. state_activeFilter.json
213
- newFileName = capitalizeSplitAndJoin.call(String(filter.fileName), ' ', replacements[filter.whitespaceReplacement ?? 'Keep Spaces'])
214
- } else {
215
- // if no file name is entered use the default active filter. ie. /activeFilter.json
216
- newFileName = filter.active
217
- }
218
- }
219
-
220
- if (newFileName?.includes('${query}')) {
221
- newFileName = newFileName.replace('${query}', capitalizeSplitAndJoin.call(String(filter.active), ' ', replacements[filter.whitespaceReplacement ?? 'Keep Spaces']))
222
- }
181
+ newFileName = reloadURLHelpers.getNewFileName(newFileName, filter, datasetKey)
223
182
  }
224
183
 
225
184
  if (!!filter.queryParameter) {
@@ -229,62 +188,42 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
229
188
  updatedQSParams[filter.queryParameter] = filter.active
230
189
  }
231
190
  }
232
- }
233
- })
234
191
 
235
- Object.keys(updatedQSParams).forEach(updatedParam => {
236
- if (decodeURIComponent(updatedQSParams[updatedParam]) !== currentQSParams[updatedParam]) {
237
- isUpdateNeeded = true
192
+ if (filter.apiFilter) {
193
+ updatedQSParams[filter.apiFilter.valueSelector] = filter.active
194
+ }
238
195
  }
239
196
  })
240
197
 
241
- if (isUpdateNeeded) {
242
- datasetsNeedsUpdate = true
243
- Object.keys(currentQSParams).forEach(currentParam => {
244
- if (!updatedQSParams[currentParam]) {
245
- updatedQSParams[currentParam] = currentQSParams[currentParam]
198
+ if (!!newFilters || reloadURLHelpers.isUpdateNeeded(filters, currentQSParams, updatedQSParams)) {
199
+ dataWasFetched = true
200
+ const dataUrlFinal = reloadURLHelpers.getDataURL({ ...currentQSParams, ...updatedQSParams }, dataUrl, newFileName)
201
+
202
+ await fetchRemoteData(dataUrlFinal).then(responseData => {
203
+ let data: any[] = responseData
204
+ if (responseData && dataset.dataDescription) {
205
+ try {
206
+ data = transform.autoStandardize(data)
207
+ data = transform.developerStandardize(data, dataset.dataDescription)
208
+ } catch (e) {
209
+ //Data not able to be standardized, leave as is
210
+ }
246
211
  }
212
+ newDatasets[datasetKey].data = data
213
+ newDatasets[datasetKey].runtimeDataUrl = dataUrlFinal
214
+ newData[datasetKey] = data
247
215
  })
248
- const _params = Object.keys(updatedQSParams).map(key => ({ key, value: updatedQSParams[key] }))
249
- let dataUrlFinal = `${dataUrl.origin}${dataUrl.pathname}${gatherQueryParams(_params)}`
250
-
251
- if (newFileName !== '') {
252
- let fileExtension = dataUrl.pathname.split('.').pop()
253
- let pathWithoutFilename = dataUrl.pathname.substring(0, dataUrl.pathname.lastIndexOf('/'))
254
- dataUrlFinal = `${dataUrl.origin}${pathWithoutFilename}/${newFileName}.${fileExtension}${gatherQueryParams(_params)}`
255
- }
256
-
257
- let newDataset = await fetchRemoteData(`${dataUrlFinal}`)
258
-
259
- if (newDataset && dataset.dataDescription) {
260
- try {
261
- newDataset = transform.autoStandardize(newDataset)
262
- newDataset = transform.developerStandardize(newDataset, dataset.dataDescription)
263
- } catch (e) {
264
- //Data not able to be standardized, leave as is
265
- }
266
- }
267
- newDatasets[datasetKey].data = newDataset
268
- newDatasets[datasetKey].runtimeDataUrl = dataUrlFinal
269
- newData[datasetKey] = newDataset
270
216
  }
271
217
  }
272
218
  }
273
219
 
274
- if (datasetsNeedsUpdate) {
275
- const dashboardConfig = dashboardConfigOverride || config.dashboard
220
+ if (dataWasFetched) {
276
221
  dispatch({ type: 'SET_DATA', payload: newData })
277
-
278
- const newFilteredData = getFilteredData(state, {}, newData)
279
- const visualizations = Object.keys(config.visualizations).reduce((acc, vizKey) => {
280
- const dataKey = config.visualizations[vizKey].dataKey
281
- if (newData[dataKey]) {
282
- acc[vizKey].formattedData = newData[dataKey]
283
- }
284
- return acc
285
- }, _.cloneDeep(config.visualizations))
286
-
287
- dispatch({ type: 'SET_FILTERED_DATA', payload: newFilteredData })
222
+ const filtersWithNewValues = addValuesToFilters<SharedFilter>(filters, newData)
223
+ const dashboardConfig = newFilters ? { ...config.dashboard, sharedFilters: filtersWithNewValues } : config.dashboard
224
+ const filteredData = getFilteredData({ ...state, config: { ...state.config, dashboard: dashboardConfig } }, {}, newData)
225
+ dispatch({ type: 'SET_FILTERED_DATA', payload: filteredData })
226
+ const visualizations = reloadURLHelpers.getVisualizationsWithFormattedData(config.visualizations, newData)
288
227
  dispatch({ type: 'SET_CONFIG', payload: { dashboard: dashboardConfig, datasets: newDatasets, visualizations } })
289
228
  }
290
229
  }
@@ -315,16 +254,21 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
315
254
  const newFilteredData = getFilteredData({ ...state, config: newConfig }, filteredData)
316
255
 
317
256
  dispatch({ type: 'SET_FILTERED_DATA', payload: newFilteredData })
257
+ dispatch({ type: 'SET_CONFIG', payload: newConfig })
318
258
  dispatch({ type: 'SET_SHARED_FILTERS', payload: newConfig.dashboard.sharedFilters })
319
259
  }
320
260
 
321
261
  useEffect(() => {
262
+ if (isEditor && !isPreview) return
322
263
  const { config } = state
323
- if (config.filterBehavior !== FilterBehavior.Apply) {
264
+ if (!hasDashboardApplyBehavior(config.visualizations)) {
324
265
  reloadURLData()
325
266
  }
326
- loadAPIFilters()
327
- }, [])
267
+
268
+ const sharedFiltersWithValues = addValuesToFilters<SharedFilter>(config.dashboard.sharedFilters, state.data)
269
+ loadAPIFilters(sharedFiltersWithValues)
270
+ updateFilteredData()
271
+ }, [isEditor, isPreview, state.config?.activeDashboard])
328
272
 
329
273
  const updateChildConfig = (visualizationKey, newConfig) => {
330
274
  const config = _.cloneDeep(state.config)
@@ -347,129 +291,13 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
347
291
  }
348
292
  }
349
293
 
350
- const applyFilters = () => {
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) {
363
- if (state.config.filterBehavior === FilterBehavior.Apply) {
364
- const queryParams = getQueryParams()
365
- let needsQueryUpdate = false
366
- dashboardConfig.sharedFilters.forEach((sharedFilter, index) => {
367
- if (sharedFilter.queuedActive) {
368
- dashboardConfig.sharedFilters[index].active = sharedFilter.queuedActive
369
- delete dashboardConfig.sharedFilters[index].queuedActive
370
-
371
- if (sharedFilter.setByQueryParameter && queryParams[sharedFilter.setByQueryParameter] !== sharedFilter.active) {
372
- queryParams[sharedFilter.setByQueryParameter] = sharedFilter.active
373
- needsQueryUpdate = true
374
- }
375
- }
376
- })
377
-
378
- if (needsQueryUpdate) {
379
- updateQueryString(queryParams)
380
- }
381
- }
382
-
383
- dispatch({ type: 'SET_SHARED_FILTERS', payload: dashboardConfig.sharedFilters })
384
- updateDataFilters()
385
- loadAPIFilters(dashboardConfig)
386
- .then(() => {
387
- reloadURLData(dashboardConfig)
388
- })
389
- .catch(e => {
390
- console.error(e)
391
- })
392
- } else {
393
- // TODO noftify of required fields
394
- }
395
- }
396
-
397
- const changeFilterActive = (index: number, value: string | string[]) => {
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)
402
-
403
- if (state.config.filterBehavior !== FilterBehavior.Apply || isAutoLoad) {
404
- sharedFilters[index].active = value
405
-
406
- const queryParams = getQueryParams()
407
- if (filterActive.setByQueryParameter && queryParams[filterActive.setByQueryParameter] !== filterActive.active) {
408
- queryParams[filterActive.setByQueryParameter] = filterActive.active
409
- updateQueryString(queryParams)
410
- }
411
- } else {
412
- if (Array.isArray(value)) throw Error(`Cannot set active values on urlfilters. expected: ${JSON.stringify(value)} to be a single value.`)
413
- sharedFilters[index].queuedActive = value
414
- }
415
-
416
- dispatch({ type: 'SET_SHARED_FILTERS', payload: sharedFilters })
417
- if (state.config.filterBehavior !== FilterBehavior.Apply) {
418
- updateDataFilters(sharedFilters)
419
- reloadURLData()
420
- }
421
- return sharedFilters
422
- }
423
-
424
- const updateDataFilters = (sharedFilters = undefined) => {
294
+ const updateFilteredData = (sharedFilters = undefined) => {
425
295
  const clonedState = _.cloneDeep(state)
426
296
  if (sharedFilters) clonedState.config.dashboard.sharedFilters = sharedFilters
427
297
  const newFilteredData = getFilteredData(clonedState)
428
298
  dispatch({ type: 'SET_FILTERED_DATA', payload: newFilteredData })
429
299
  }
430
300
 
431
- const handleOnChange = (index: number, value: string | string[]) => {
432
- const config = _.cloneDeep(state.config)
433
- const newSharedFilters = changeFilterActive(index, value)
434
- if (config.filterBehavior === FilterBehavior.Apply) {
435
- const autoLoadViz = getAutoLoadVisualization()
436
- const isAutoSelectFilter = !autoLoadViz?.hide.includes(index)
437
- const missingFilterSelections = config.dashboard.sharedFilters.some(f => !f.active)
438
- if (isAutoSelectFilter && !missingFilterSelections) {
439
- // a dropdown has been selected that doesn't
440
- // require the Go Button
441
- reloadURLData({ sharedFilters: newSharedFilters })
442
- } else {
443
- // A parent filter was selected, reset filters by:
444
- // set auto select filter dropdowns to null
445
- const autoSelectFilters = config.dashboard.sharedFilters.filter((_, _index) => !autoLoadViz?.hide.includes(_index))
446
- const dropdownFilterKeys = autoSelectFilters.map(filter => getApiFilterKey(filter.apiFilter!))
447
- const newApiDropdowns = { ...apiFilterDropdowns }
448
- dropdownFilterKeys.forEach(key => (newApiDropdowns[key] = null))
449
- setAPIFilterDropdowns(newApiDropdowns)
450
- // remove active from sharedFilters that are autoLoading
451
- const dashboardConfig = { ...config.dashboard }
452
- if (config.filterBehavior !== FilterBehavior.Apply) {
453
- dashboardConfig.sharedFilters[index].active = value
454
- } else {
455
- if (Array.isArray(value)) throw Error(`Cannot set active values on urlfilters. expected: ${JSON.stringify(value)} to be a single value.`)
456
- dashboardConfig.sharedFilters[index].queuedActive = value
457
- }
458
- const newSharedFilters = config.dashboard.sharedFilters.map((filter, _index) => {
459
- const _isAutoSelectFilter = !autoLoadViz?.hide.includes(_index)
460
- if (_isAutoSelectFilter) filter.active = ''
461
- return filter
462
- })
463
- const _newConfig = { dashboard: { ...config.dashboard, sharedFilters: newSharedFilters } }
464
- dispatch({ type: 'SET_CONFIG', payload: _newConfig })
465
- // setData to empty object because we no longer have a data state.
466
- dispatch({ type: 'SET_DATA', payload: {} })
467
- dispatch({ type: 'SET_FILTERED_DATA', payload: {} })
468
- loadAPIFilters(_newConfig.dashboard)
469
- }
470
- }
471
- }
472
-
473
301
  const resizeObserver = new ResizeObserver(entries => {
474
302
  for (let entry of entries) {
475
303
  let newViewport = getViewport(entry.contentRect.width)
@@ -484,18 +312,9 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
484
312
  }
485
313
  }, [])
486
314
 
487
- const setPreview = shouldPreview => dispatch({ type: 'SET_PREVIEW', payload: shouldPreview })
488
-
489
315
  // Prevent render if loading
490
316
  if (state.loading) return <Loading />
491
317
 
492
- const GoButton = ({ autoLoad }: { autoLoad?: boolean }) => {
493
- if (state.config.filterBehavior === FilterBehavior.Apply && !autoLoad) {
494
- return <button onClick={applyFilters}>GO!</button>
495
- }
496
- return null
497
- }
498
-
499
318
  let body: JSX.Element | null = null
500
319
  // Editor mode
501
320
  if (isEditor && !isPreview) {
@@ -504,7 +323,8 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
504
323
  getVizKeys(state.config).forEach(visualizationKey => {
505
324
  const rowNumber = vizRowColumnLocator[visualizationKey]?.row
506
325
  const visualizationConfig = getVizConfig(visualizationKey, rowNumber, state.config, state.data, state.filteredData)
507
-
326
+ visualizationConfig.uid = visualizationKey
327
+ if (visualizationConfig.type === 'footnotes') visualizationConfig.formattedData = undefined
508
328
  const setsSharedFilter = state.config.dashboard.sharedFilters && state.config.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.setBy === visualizationKey).length > 0
509
329
  const setSharedFilterValue = setsSharedFilter ? state.config.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.setBy === visualizationKey)[0].active : undefined
510
330
 
@@ -590,13 +410,12 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
590
410
  </>
591
411
  )
592
412
  break
593
- case 'filter-dropdowns':
413
+ case 'dashboardFilters':
594
414
  const hideFilter = visualizationConfig.autoLoad && inNoDataState
595
415
  body = !hideFilter ? (
596
416
  <>
597
417
  <Header visualizationKey={visualizationKey} subEditor='Filter Dropdowns' />
598
- <Filters hide={visualizationConfig.hide} filters={state.config.dashboard.sharedFilters} apiFilterDropdowns={apiFilterDropdowns} handleOnChange={handleOnChange} />
599
- <GoButton autoLoad={visualizationConfig.autoLoad} />
418
+ <DashboardSharedFilters isEditor={true} visualizationConfig={visualizationConfig} apiFilterDropdowns={apiFilterDropdowns} setConfig={_updateConfig} />
600
419
  </>
601
420
  ) : (
602
421
  <></>
@@ -604,9 +423,18 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
604
423
  break
605
424
  case 'table':
606
425
  body = (
607
- <EditorWrapper component={DataTableStandAlone} visualizationKey={visualizationKey} visualizationConfig={visualizationConfig} updateConfig={_updateConfig} type={'Table'} viewport={currentViewport}>
608
- <DataTableEditorPanel key={visualizationKey} config={visualizationConfig} updateConfig={_updateConfig} />
609
- </EditorWrapper>
426
+ <>
427
+ <Header visualizationKey={visualizationKey} subEditor='Table' />
428
+ <DataTableStandAlone visualizationKey={visualizationKey} config={visualizationConfig} isEditor={true} updateConfig={_updateConfig} />
429
+ </>
430
+ )
431
+ break
432
+ case 'footnotes':
433
+ body = (
434
+ <>
435
+ <Header visualizationKey={visualizationKey} subEditor='Footnotes' />
436
+ <FootnotesStandAlone visualizationKey={visualizationKey} config={{ ...visualizationConfig, datasets: state.config.datasets }} isEditor={true} updateConfig={_updateConfig} />
437
+ </>
610
438
  )
611
439
  break
612
440
  default:
@@ -621,7 +449,7 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
621
449
  <DndProvider backend={HTML5Backend}>
622
450
  <div className='header-container'>
623
451
  <Header />
624
- <VisualizationsPanel loadConfig={newConfig => dispatch({ type: 'UPDATE_CONFIG', payload: [newConfig] })} config={state.config} />
452
+ <VisualizationsPanel />
625
453
  </div>
626
454
 
627
455
  <div className='layout-container'>
@@ -633,6 +461,7 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
633
461
  } else {
634
462
  const { config } = state
635
463
  const { title, description } = config.dashboard || {}
464
+
636
465
  body = (
637
466
  <>
638
467
  {isEditor && <Header />}
@@ -642,22 +471,13 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
642
471
  <Title title={title} isDashboard={true} classes={[`dashboard-title`, `${config.dashboard.theme ?? 'theme-blue'}`]} />
643
472
  {/* Description */}
644
473
  {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
474
  {/* Visualizations */}
655
475
  {config.rows &&
656
476
  config.rows
657
477
  .filter(row => row.columns.filter(col => col.widget).length !== 0)
658
478
  .map((row, index) => {
659
479
  if (row.multiVizColumn && (isPreview || !isEditor)) {
660
- const filteredData = getFilteredData(state)
480
+ const filteredData = getFilteredData(state, _.cloneDeep(state.data))
661
481
  const data = filteredData[index] ?? row.formattedData
662
482
  const dataGroups = {}
663
483
  data.forEach(d => {
@@ -665,29 +485,31 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
665
485
  if (!dataGroups[groupKey]) dataGroups[groupKey] = []
666
486
  dataGroups[groupKey].push(d)
667
487
  })
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
488
  return (
689
- <VisualizationRow key={`row__${index}`} row={row} rowIndex={index} setSharedFilter={setSharedFilter} updateChildConfig={updateChildConfig} applyFilters={applyFilters} apiFilterDropdowns={apiFilterDropdowns} handleOnChange={handleOnChange} currentViewport={currentViewport} />
489
+ <>
490
+ {/* Expand/Collapse All */}
491
+ {row.expandCollapseAllButtons === true && <ExpandCollapseButtons setAllExpanded={setAllExpanded} />}
492
+ {Object.keys(dataGroups).map(groupName => {
493
+ const dataValue = dataGroups[groupName]
494
+ return (
495
+ <VisualizationRow
496
+ key={`row__${index}__${groupName}`}
497
+ allExpanded={allExpanded}
498
+ filteredDataOverride={dataValue}
499
+ groupName={groupName}
500
+ row={row}
501
+ rowIndex={index}
502
+ setSharedFilter={setSharedFilter}
503
+ updateChildConfig={updateChildConfig}
504
+ apiFilterDropdowns={apiFilterDropdowns}
505
+ currentViewport={currentViewport}
506
+ />
507
+ )
508
+ })}
509
+ </>
690
510
  )
511
+ } else {
512
+ return <VisualizationRow key={`row__${index}`} allExpanded={false} groupName={''} row={row} rowIndex={index} setSharedFilter={setSharedFilter} updateChildConfig={updateChildConfig} apiFilterDropdowns={apiFilterDropdowns} currentViewport={currentViewport} />
691
513
  }
692
514
  })}
693
515
 
@@ -765,7 +587,7 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
765
587
 
766
588
  return (
767
589
  <GlobalContextProvider>
768
- <DashboardContext.Provider value={{ ...state, setParentConfig: editorContext.setTempConfig, outerContainerRef, isDebug }}>
590
+ <DashboardContext.Provider value={{ ...state, setParentConfig: editorContext.setTempConfig, outerContainerRef, isDebug, loadAPIFilters, reloadURLData }}>
769
591
  <DashboardDispatchContext.Provider value={dispatch}>
770
592
  <div className={dashboardContainerClasses.join(' ')} ref={outerContainerRef} data-download-id={imageId}>
771
593
  {body}