@cdc/dashboard 4.24.5 → 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 (70) hide show
  1. package/dist/cdcdashboard.js +122872 -112065
  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 +10 -1
  8. package/package.json +12 -11
  9. package/src/CdcDashboard.tsx +5 -1
  10. package/src/CdcDashboardComponent.tsx +165 -306
  11. package/src/DashboardContext.tsx +9 -1
  12. package/src/_stories/Dashboard.stories.tsx +38 -34
  13. package/src/_stories/_mock/api-filter-chart.json +11 -35
  14. package/src/_stories/_mock/api-filter-map.json +17 -31
  15. package/src/_stories/_mock/multi-viz.json +2 -3
  16. package/src/_stories/_mock/pivot-filter.json +14 -12
  17. package/src/components/CollapsibleVisualizationRow.tsx +44 -0
  18. package/src/components/Column.tsx +1 -1
  19. package/src/components/DashboardFilters/DashboardFilters.tsx +80 -0
  20. package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +218 -0
  21. package/src/components/DashboardFilters/DashboardFiltersEditor/components/DeleteFilterModal.tsx +48 -0
  22. package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +367 -0
  23. package/src/components/DashboardFilters/DashboardFiltersEditor/index.ts +1 -0
  24. package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +143 -0
  25. package/src/components/DashboardFilters/index.ts +3 -0
  26. package/src/components/DataDesignerModal.tsx +9 -9
  27. package/src/components/ExpandCollapseButtons.tsx +20 -0
  28. package/src/components/Header/Header.tsx +1 -97
  29. package/src/components/MultiConfigTabs/MultiConfigTabs.tsx +4 -4
  30. package/src/components/Row.tsx +52 -19
  31. package/src/components/Toggle/Toggle.tsx +2 -4
  32. package/src/components/VisualizationRow.tsx +82 -24
  33. package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +116 -0
  34. package/src/components/VisualizationsPanel/index.ts +1 -0
  35. package/src/components/VisualizationsPanel/visualizations-panel-styles.css +12 -0
  36. package/src/components/Widget.tsx +26 -90
  37. package/src/helpers/apiFilterHelpers.ts +51 -0
  38. package/src/helpers/changeFilterActive.ts +30 -0
  39. package/src/helpers/filterData.ts +10 -48
  40. package/src/helpers/generateValuesForFilter.ts +1 -1
  41. package/src/helpers/getAutoLoadVisualization.ts +11 -0
  42. package/src/helpers/getFilteredData.ts +4 -2
  43. package/src/helpers/getVizConfig.ts +23 -2
  44. package/src/helpers/getVizRowColumnLocator.ts +2 -1
  45. package/src/helpers/hasDashboardApplyBehavior.ts +5 -0
  46. package/src/helpers/iconHash.tsx +3 -3
  47. package/src/helpers/mapDataToConfig.ts +29 -0
  48. package/src/helpers/processData.ts +2 -3
  49. package/src/helpers/reloadURLHelpers.ts +68 -0
  50. package/src/helpers/tests/filterData.test.ts +1 -93
  51. package/src/scss/editor-panel.scss +1 -1
  52. package/src/scss/grid.scss +34 -27
  53. package/src/scss/main.scss +41 -3
  54. package/src/scss/variables.scss +4 -0
  55. package/src/store/dashboard.actions.ts +12 -4
  56. package/src/store/dashboard.reducer.ts +30 -4
  57. package/src/types/APIFilter.ts +1 -5
  58. package/src/types/ConfigRow.ts +2 -0
  59. package/src/types/Dashboard.ts +1 -1
  60. package/src/types/DashboardConfig.ts +2 -4
  61. package/src/types/DashboardFilters.ts +7 -0
  62. package/src/types/InitialState.ts +1 -1
  63. package/src/types/MultiDashboard.ts +2 -2
  64. package/src/types/SharedFilter.ts +2 -5
  65. package/src/types/Tab.ts +1 -1
  66. package/LICENSE +0 -201
  67. package/src/components/Filters.tsx +0 -88
  68. package/src/components/Header/FilterModal.tsx +0 -510
  69. package/src/components/VisualizationsPanel.tsx +0 -95
  70. 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,14 +27,12 @@ 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'
@@ -47,14 +44,12 @@ import { type TableConfig } from '@cdc/core/components/DataTable/types/TableConf
47
44
  // types
48
45
  import { type SharedFilter } from './types/SharedFilter'
49
46
  import { type APIFilter } from './types/APIFilter'
50
- import { type Visualization } from '@cdc/core/types/Visualization'
51
47
  import { type WCMSProps } from '@cdc/core/types/WCMSProps'
52
48
  import { type InitialState } from './types/InitialState'
53
49
  import MultiTabs from './components/MultiConfigTabs'
54
50
  import _ from 'lodash'
55
51
  import EditorContext from '../../editor/src/ConfigContext'
56
- import { getApiFilterKey } from './helpers/getApiFilterKey'
57
- import Filters, { APIFilterDropdowns, DropdownOptions } from './components/Filters'
52
+ import { APIFilterDropdowns, DropdownOptions } from './components/DashboardFilters'
58
53
  import DataTableStandAlone from '@cdc/core/components/DataTable/DataTableStandAlone'
59
54
  import { ViewPort } from '@cdc/core/types/ViewPort'
60
55
  import VisualizationRow from './components/VisualizationRow'
@@ -62,6 +57,14 @@ import { getVizConfig } from './helpers/getVizConfig'
62
57
  import { getFilteredData } from './helpers/getFilteredData'
63
58
  import { getVizRowColumnLocator } from './helpers/getVizRowColumnLocator'
64
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'
65
68
 
66
69
  type DashboardProps = Omit<WCMSProps, 'configUrl'> & {
67
70
  initialState: InitialState
@@ -73,13 +76,9 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
73
76
  const [apiFilterDropdowns, setAPIFilterDropdowns] = useState<APIFilterDropdowns>({})
74
77
  const [currentViewport, setCurrentViewport] = useState<ViewPort>('lg')
75
78
  const [imageId] = useState(`cove-${Math.random().toString(16).slice(-4)}`)
79
+ const [allExpanded, setAllExpanded] = useState(true)
76
80
 
77
81
  const isPreview = state.tabSelected === 'Dashboard Preview'
78
- const replacements = {
79
- 'Remove Spaces': '',
80
- 'Keep Spaces': ' ',
81
- 'Replace With Underscore': '_'
82
- }
83
82
 
84
83
  const inNoDataState = useMemo(() => {
85
84
  const vals = Object.values(state.data)
@@ -89,121 +88,97 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
89
88
 
90
89
  const vizRowColumnLocator = getVizRowColumnLocator(state.config.rows)
91
90
 
92
- const getAutoLoadVisualization = (): Visualization | undefined => {
93
- const autoLoadViz = Object.values(state.config.visualizations).filter(vis => {
94
- return vis.autoLoad && vis.type === 'filter-dropdowns'
95
- })
96
- if (autoLoadViz.length === 0) return
97
- if (autoLoadViz.length > 1) throw new Error('Only one filter row can be autoloaded')
98
- return autoLoadViz[0]
99
- }
100
-
101
91
  const transform = new DataTransform()
102
92
 
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)
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)) {
103
+ const filterParents = sharedFilters.filter(f => sharedFilter.parents?.includes(f.key))
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
108
+ sharedFilter.active = defaultValue
119
109
  }
110
+ return sharedFilter
120
111
  }
121
112
 
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
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
129
146
  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 = (data: 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 ? data[selector] : data, { ...apiFilter, heirarchyLookup: heirarchy.join('.') })
144
- }
145
- 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')
146
- 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)
147
154
  }
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
- }
155
+ })
168
156
  }
169
157
 
170
- const reloadURLData = async () => {
171
- const { config } = state
158
+ const reloadURLData = async (newFilters?: SharedFilter[]) => {
159
+ const config = _.cloneDeep(state.config)
172
160
  if (!config.datasets) return
173
- let newData = { ...state.data }
174
- let newDatasets = { ...config.datasets }
175
- let datasetsNeedsUpdate = false
176
- 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
177
167
  let newFileName = ''
178
168
 
179
169
  for (let i = 0; i < datasetKeys.length; i++) {
180
170
  const datasetKey = datasetKeys[i]
181
171
  const dataset = config.datasets[datasetKey]
182
- const filters = config.dashboard?.sharedFilters
172
+
183
173
  if (dataset.dataUrl && filters) {
184
174
  const dataUrl = new URL(dataset.runtimeDataUrl || dataset.dataUrl, window.location.origin)
185
- let currentQSParams = Object.fromEntries(new URLSearchParams(dataUrl.search))
186
- let updatedQSParams = {}
187
- let isUpdateNeeded = false
188
-
175
+ const currentQSParams = Object.fromEntries(new URLSearchParams(dataUrl.search))
176
+ const updatedQSParams = {}
189
177
  filters.forEach(filter => {
190
178
  // filter.active is always a string when filter.type is 'urlfilter'
191
179
  if (filter.type === 'urlfilter' && !Array.isArray(filter.active)) {
192
180
  if (filter.filterBy === 'File Name') {
193
- isUpdateNeeded = true
194
- if (filter.datasetKey === datasetKey) {
195
- if (filter.fileName) {
196
- // if a file name is found, ie, state_${query}, use that, ie. state_activeFilter.json
197
- newFileName = capitalizeSplitAndJoin.call(String(filter.fileName), ' ', replacements[filter.whitespaceReplacement ?? 'Keep Spaces'])
198
- } else {
199
- // if no file name is entered use the default active filter. ie. /activeFilter.json
200
- newFileName = filter.active
201
- }
202
- }
203
-
204
- if (newFileName?.includes('${query}')) {
205
- newFileName = newFileName.replace('${query}', capitalizeSplitAndJoin.call(String(filter.active), ' ', replacements[filter.whitespaceReplacement ?? 'Keep Spaces']))
206
- }
181
+ newFileName = reloadURLHelpers.getNewFileName(newFileName, filter, datasetKey)
207
182
  }
208
183
 
209
184
  if (!!filter.queryParameter) {
@@ -213,63 +188,43 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
213
188
  updatedQSParams[filter.queryParameter] = filter.active
214
189
  }
215
190
  }
216
- }
217
- })
218
191
 
219
- Object.keys(updatedQSParams).forEach(updatedParam => {
220
- if (decodeURIComponent(updatedQSParams[updatedParam]) !== currentQSParams[updatedParam]) {
221
- isUpdateNeeded = true
192
+ if (filter.apiFilter) {
193
+ updatedQSParams[filter.apiFilter.valueSelector] = filter.active
194
+ }
222
195
  }
223
196
  })
224
197
 
225
- if (isUpdateNeeded) {
226
- datasetsNeedsUpdate = true
227
- Object.keys(currentQSParams).forEach(currentParam => {
228
- if (!updatedQSParams[currentParam]) {
229
- 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
+ }
230
211
  }
212
+ newDatasets[datasetKey].data = data
213
+ newDatasets[datasetKey].runtimeDataUrl = dataUrlFinal
214
+ newData[datasetKey] = data
231
215
  })
232
- const _params = Object.keys(updatedQSParams).map(key => ({ key, value: updatedQSParams[key] }))
233
- let dataUrlFinal = `${dataUrl.origin}${dataUrl.pathname}${gatherQueryParams(_params)}`
234
-
235
- if (newFileName !== '') {
236
- let fileExtension = dataUrl.pathname.split('.').pop()
237
- let pathWithoutFilename = dataUrl.pathname.substring(0, dataUrl.pathname.lastIndexOf('/'))
238
- dataUrlFinal = `${dataUrl.origin}${pathWithoutFilename}/${newFileName}.${fileExtension}${gatherQueryParams(_params)}`
239
- }
240
-
241
- let newDataset = await fetchRemoteData(`${dataUrlFinal}`)
242
-
243
- if (newDataset && dataset.dataDescription) {
244
- try {
245
- newDataset = transform.autoStandardize(newDataset)
246
- newDataset = transform.developerStandardize(newDataset, dataset.dataDescription)
247
- } catch (e) {
248
- //Data not able to be standardized, leave as is
249
- }
250
- }
251
- newDatasets[datasetKey].data = newDataset
252
- newDatasets[datasetKey].runtimeDataUrl = dataUrlFinal
253
- newData[datasetKey] = newDataset
254
216
  }
255
217
  }
256
218
  }
257
219
 
258
- if (datasetsNeedsUpdate) {
220
+ if (dataWasFetched) {
259
221
  dispatch({ type: 'SET_DATA', payload: newData })
260
-
261
- const newFilteredData = getFilteredData(state, {}, newData)
262
-
263
- const visualizations = Object.keys(config.visualizations).reduce((acc, vizKey) => {
264
- const dataKey = config.visualizations[vizKey].dataKey
265
- if (newData[dataKey]) {
266
- acc[vizKey].formattedData = newData[dataKey]
267
- }
268
- return acc
269
- }, _.cloneDeep(config.visualizations))
270
-
271
- dispatch({ type: 'SET_FILTERED_DATA', payload: newFilteredData })
272
- dispatch({ type: 'SET_CONFIG', payload: { ...config, datasets: newDatasets, visualizations } })
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)
227
+ dispatch({ type: 'SET_CONFIG', payload: { dashboard: dashboardConfig, datasets: newDatasets, visualizations } })
273
228
  }
274
229
  }
275
230
 
@@ -284,8 +239,7 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
284
239
  }
285
240
 
286
241
  const setSharedFilter = (key, datum) => {
287
- const { config } = state
288
- let newConfig = { ...config }
242
+ const { config: newConfig, filteredData } = _.cloneDeep(state)
289
243
 
290
244
  for (let i = 0; i < newConfig.dashboard.sharedFilters.length; i++) {
291
245
  const filter = newConfig.dashboard.sharedFilters[i]
@@ -297,32 +251,36 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
297
251
  }
298
252
  }
299
253
 
300
- const newFilteredData = getFilteredData(state, _.cloneDeep(state.filteredData))
254
+ const newFilteredData = getFilteredData({ ...state, config: newConfig }, filteredData)
301
255
 
302
256
  dispatch({ type: 'SET_FILTERED_DATA', payload: newFilteredData })
303
257
  dispatch({ type: 'SET_CONFIG', payload: newConfig })
258
+ dispatch({ type: 'SET_SHARED_FILTERS', payload: newConfig.dashboard.sharedFilters })
304
259
  }
305
260
 
306
261
  useEffect(() => {
307
- if (state.tabSelected && state.tabSelected !== 'Dashboard Preview') return
262
+ if (isEditor && !isPreview) return
308
263
  const { config } = state
309
- if (config.filterBehavior !== FilterBehavior.Apply) {
264
+ if (!hasDashboardApplyBehavior(config.visualizations)) {
310
265
  reloadURLData()
311
266
  }
312
- loadAPIFilters()
313
- }, [])
267
+
268
+ const sharedFiltersWithValues = addValuesToFilters<SharedFilter>(config.dashboard.sharedFilters, state.data)
269
+ loadAPIFilters(sharedFiltersWithValues)
270
+ updateFilteredData()
271
+ }, [isEditor, isPreview, state.config?.activeDashboard])
314
272
 
315
273
  const updateChildConfig = (visualizationKey, newConfig) => {
316
- const { config } = state
317
- let updatedConfig = { ...config }
274
+ const config = _.cloneDeep(state.config)
275
+ const updatedConfig = _.pick(config, ['visualizations', 'multiDashboards'])
318
276
  updatedConfig.visualizations[visualizationKey] = newConfig
319
277
  updatedConfig.visualizations[visualizationKey].formattedData = config.visualizations[visualizationKey].formattedData
320
278
  if (config.multiDashboards) {
321
279
  const activeDashboard = config.activeDashboard
322
280
  const multiDashboards = [...config.multiDashboards]
323
281
  const label = multiDashboards[activeDashboard].label
324
- const toSave = _.pick(updatedConfig, ['dashboard', 'visualizations', 'rows'])
325
- multiDashboards[activeDashboard] = { ...toSave, label }
282
+ const toSave = { label, visualizations: updatedConfig.visualizations, ..._.pick(config, ['dashboard', 'rows']) }
283
+ multiDashboards[activeDashboard] = toSave
326
284
  updatedConfig.multiDashboards = multiDashboards
327
285
  }
328
286
 
@@ -333,103 +291,11 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
333
291
  }
334
292
  }
335
293
 
336
- const applyFilters = () => {
337
- let dashboardConfig = state.config.dashboard
338
- const allFiltersSelected = !state.config.dashboard.sharedFilters.some(filter => !filter.active && !filter.queuedActive)
339
- if (allFiltersSelected) {
340
- if (state.config.filterBehavior === FilterBehavior.Apply) {
341
- const queryParams = getQueryParams()
342
- let needsQueryUpdate = false
343
- state.config.dashboard.sharedFilters.forEach((sharedFilter, index) => {
344
- if (sharedFilter.queuedActive) {
345
- dashboardConfig.sharedFilters[index].active = sharedFilter.queuedActive
346
- delete dashboardConfig.sharedFilters[index].queuedActive
347
-
348
- if (sharedFilter.setByQueryParameter && queryParams[sharedFilter.setByQueryParameter] !== sharedFilter.active) {
349
- queryParams[sharedFilter.setByQueryParameter] = sharedFilter.active
350
- needsQueryUpdate = true
351
- }
352
- }
353
- })
354
-
355
- if (needsQueryUpdate) {
356
- updateQueryString(queryParams)
357
- }
358
- }
359
-
360
- dispatch({ type: 'SET_CONFIG', payload: { ...state.config, dashboard: dashboardConfig } })
361
- dispatch({ type: 'SET_FILTERED_DATA', payload: getFilteredData(state) })
362
- reloadURLData()
363
- } else {
364
- // TODO noftify of required fields
365
- }
366
- }
367
-
368
- const changeFilterActive = (index: number, value: string | string[]) => {
369
- const { config } = state
370
- let dashboardConfig = { ...config.dashboard }
371
- let filterActive = dashboardConfig.sharedFilters[index]
372
-
373
- if (config.filterBehavior !== FilterBehavior.Apply) {
374
- dashboardConfig.sharedFilters[index].active = value
375
-
376
- const queryParams = getQueryParams()
377
- if (filterActive.setByQueryParameter && queryParams[filterActive.setByQueryParameter] !== filterActive.active) {
378
- queryParams[filterActive.setByQueryParameter] = filterActive.active
379
- updateQueryString(queryParams)
380
- }
381
- } else {
382
- if (Array.isArray(value)) throw Error(`Cannot set active values on urlfilters. expected: ${JSON.stringify(value)} to be a single value.`)
383
- dashboardConfig.sharedFilters[index].queuedActive = value
384
- }
385
-
386
- dispatch({ type: 'SET_CONFIG', payload: { ...config, dashboard: dashboardConfig } })
387
- if (config.filterBehavior !== FilterBehavior.Apply) {
388
- dispatch({ type: 'SET_FILTERED_DATA', payload: getFilteredData(state) })
389
- reloadURLData()
390
- }
391
- }
392
-
393
- const handleOnChange = (index: number, value: string | string[]) => {
394
- const { config } = state
395
- changeFilterActive(index, value)
396
- if (config.filterBehavior === FilterBehavior.Apply) {
397
- const autoLoadViz = getAutoLoadVisualization()
398
- if (!autoLoadViz) return // nothing left to do for regular filter behavior.
399
- const isAutoSelectFilter = !autoLoadViz.hide.includes(index)
400
- const missingFilterSelections = config.dashboard.sharedFilters.some(f => !f.active)
401
- if (isAutoSelectFilter && !missingFilterSelections) {
402
- // a dropdown has been selected that doesn't
403
- // require the Go Button
404
- reloadURLData()
405
- } else {
406
- // A parent filter was selected, reset filters by:
407
- // set auto select filter dropdowns to null
408
- const autoSelectFilters = config.dashboard.sharedFilters.filter((_, _index) => !autoLoadViz?.hide.includes(_index))
409
- const dropdownFilterKeys = autoSelectFilters.map(filter => getApiFilterKey(filter.apiFilter!))
410
- const newApiDropdowns = { ...apiFilterDropdowns }
411
- dropdownFilterKeys.forEach(key => (newApiDropdowns[key] = null))
412
- setAPIFilterDropdowns(newApiDropdowns)
413
- // remove active from sharedFilters that are autoLoading
414
- const dashboardConfig = { ...config.dashboard }
415
- if (config.filterBehavior !== FilterBehavior.Apply) {
416
- dashboardConfig.sharedFilters[index].active = value
417
- } else {
418
- if (Array.isArray(value)) throw Error(`Cannot set active values on urlfilters. expected: ${JSON.stringify(value)} to be a single value.`)
419
- dashboardConfig.sharedFilters[index].queuedActive = value
420
- }
421
- const newSharedFilters = config.dashboard.sharedFilters.map((filter, _index) => {
422
- const _isAutoSelectFilter = !autoLoadViz?.hide.includes(_index)
423
- if (_isAutoSelectFilter) filter.active = ''
424
- return filter
425
- })
426
- const _newConfig = { ...config, dashboard: { ...config.dashboard, sharedFilters: newSharedFilters } }
427
- dispatch({ type: 'SET_CONFIG', payload: _newConfig })
428
- // setData to empty object because we no longer have a data state.
429
- dispatch({ type: 'SET_DATA', payload: {} })
430
- dispatch({ type: 'SET_FILTERED_DATA', payload: {} })
431
- }
432
- }
294
+ const updateFilteredData = (sharedFilters = undefined) => {
295
+ const clonedState = _.cloneDeep(state)
296
+ if (sharedFilters) clonedState.config.dashboard.sharedFilters = sharedFilters
297
+ const newFilteredData = getFilteredData(clonedState)
298
+ dispatch({ type: 'SET_FILTERED_DATA', payload: newFilteredData })
433
299
  }
434
300
 
435
301
  const resizeObserver = new ResizeObserver(entries => {
@@ -446,18 +312,9 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
446
312
  }
447
313
  }, [])
448
314
 
449
- const setPreview = shouldPreview => dispatch({ type: 'SET_PREVIEW', payload: shouldPreview })
450
-
451
315
  // Prevent render if loading
452
316
  if (state.loading) return <Loading />
453
317
 
454
- const GoButton = ({ autoLoad }: { autoLoad?: boolean }) => {
455
- if (state.config.filterBehavior === FilterBehavior.Apply && !autoLoad) {
456
- return <button onClick={applyFilters}>GO!</button>
457
- }
458
- return null
459
- }
460
-
461
318
  let body: JSX.Element | null = null
462
319
  // Editor mode
463
320
  if (isEditor && !isPreview) {
@@ -466,7 +323,8 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
466
323
  getVizKeys(state.config).forEach(visualizationKey => {
467
324
  const rowNumber = vizRowColumnLocator[visualizationKey]?.row
468
325
  const visualizationConfig = getVizConfig(visualizationKey, rowNumber, state.config, state.data, state.filteredData)
469
-
326
+ visualizationConfig.uid = visualizationKey
327
+ if (visualizationConfig.type === 'footnotes') visualizationConfig.formattedData = undefined
470
328
  const setsSharedFilter = state.config.dashboard.sharedFilters && state.config.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.setBy === visualizationKey).length > 0
471
329
  const setSharedFilterValue = setsSharedFilter ? state.config.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.setBy === visualizationKey)[0].active : undefined
472
330
 
@@ -552,13 +410,12 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
552
410
  </>
553
411
  )
554
412
  break
555
- case 'filter-dropdowns':
413
+ case 'dashboardFilters':
556
414
  const hideFilter = visualizationConfig.autoLoad && inNoDataState
557
415
  body = !hideFilter ? (
558
416
  <>
559
417
  <Header visualizationKey={visualizationKey} subEditor='Filter Dropdowns' />
560
- <Filters hide={visualizationConfig.hide} filters={state.config.dashboard.sharedFilters} apiFilterDropdowns={apiFilterDropdowns} handleOnChange={handleOnChange} />
561
- <GoButton autoLoad={visualizationConfig.autoLoad} />
418
+ <DashboardSharedFilters isEditor={true} visualizationConfig={visualizationConfig} apiFilterDropdowns={apiFilterDropdowns} setConfig={_updateConfig} />
562
419
  </>
563
420
  ) : (
564
421
  <></>
@@ -572,6 +429,14 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
572
429
  </>
573
430
  )
574
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
+ </>
438
+ )
439
+ break
575
440
  default:
576
441
  body = <></>
577
442
  break
@@ -584,7 +449,7 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
584
449
  <DndProvider backend={HTML5Backend}>
585
450
  <div className='header-container'>
586
451
  <Header />
587
- <VisualizationsPanel loadConfig={newConfig => dispatch({ type: 'UPDATE_CONFIG', payload: [newConfig] })} config={state.config} />
452
+ <VisualizationsPanel />
588
453
  </div>
589
454
 
590
455
  <div className='layout-container'>
@@ -596,6 +461,7 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
596
461
  } else {
597
462
  const { config } = state
598
463
  const { title, description } = config.dashboard || {}
464
+
599
465
  body = (
600
466
  <>
601
467
  {isEditor && <Header />}
@@ -605,22 +471,13 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
605
471
  <Title title={title} isDashboard={true} classes={[`dashboard-title`, `${config.dashboard.theme ?? 'theme-blue'}`]} />
606
472
  {/* Description */}
607
473
  {description && <div className='subtext'>{parse(description)}</div>}
608
-
609
- {/* Filters */}
610
- {config.dashboard.sharedFilters && Object.values(config.visualizations || {}).filter(viz => viz.visualizationType === 'filter-dropdowns').length === 0 && (
611
- <>
612
- <Filters filters={state.config.dashboard.sharedFilters} apiFilterDropdowns={apiFilterDropdowns} handleOnChange={handleOnChange} />
613
- <GoButton />
614
- </>
615
- )}
616
-
617
474
  {/* Visualizations */}
618
475
  {config.rows &&
619
476
  config.rows
620
477
  .filter(row => row.columns.filter(col => col.widget).length !== 0)
621
478
  .map((row, index) => {
622
479
  if (row.multiVizColumn && (isPreview || !isEditor)) {
623
- const filteredData = getFilteredData(state)
480
+ const filteredData = getFilteredData(state, _.cloneDeep(state.data))
624
481
  const data = filteredData[index] ?? row.formattedData
625
482
  const dataGroups = {}
626
483
  data.forEach(d => {
@@ -628,29 +485,31 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
628
485
  if (!dataGroups[groupKey]) dataGroups[groupKey] = []
629
486
  dataGroups[groupKey].push(d)
630
487
  })
631
- return Object.keys(dataGroups).map(groupName => {
632
- const dataValue = dataGroups[groupName]
633
- return (
634
- <React.Fragment key={`row__${index}__${groupName}`}>
635
- <h1 className='h4'>{groupName}</h1>
636
- <VisualizationRow
637
- filteredDataOverride={dataValue}
638
- row={row}
639
- rowIndex={index}
640
- setSharedFilter={setSharedFilter}
641
- updateChildConfig={updateChildConfig}
642
- applyFilters={applyFilters}
643
- apiFilterDropdowns={apiFilterDropdowns}
644
- handleOnChange={handleOnChange}
645
- currentViewport={currentViewport}
646
- />
647
- </React.Fragment>
648
- )
649
- })
650
- } else {
651
488
  return (
652
- <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
+ </>
653
510
  )
511
+ } else {
512
+ return <VisualizationRow key={`row__${index}`} allExpanded={false} groupName={''} row={row} rowIndex={index} setSharedFilter={setSharedFilter} updateChildConfig={updateChildConfig} apiFilterDropdowns={apiFilterDropdowns} currentViewport={currentViewport} />
654
513
  }
655
514
  })}
656
515
 
@@ -728,7 +587,7 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
728
587
 
729
588
  return (
730
589
  <GlobalContextProvider>
731
- <DashboardContext.Provider value={{ ...state, setParentConfig: editorContext.setTempConfig, outerContainerRef, isDebug }}>
590
+ <DashboardContext.Provider value={{ ...state, setParentConfig: editorContext.setTempConfig, outerContainerRef, isDebug, loadAPIFilters, reloadURLData }}>
732
591
  <DashboardDispatchContext.Provider value={dispatch}>
733
592
  <div className={dashboardContainerClasses.join(' ')} ref={outerContainerRef} data-download-id={imageId}>
734
593
  {body}