@cdc/dashboard 4.24.5 → 4.24.9

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 (87) hide show
  1. package/dist/cdcdashboard.js +144406 -127510
  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/examples/single-state-dashboard-filters.json +421 -0
  8. package/examples/state-level.json +90136 -0
  9. package/examples/state-points.json +10474 -0
  10. package/examples/test-file.json +147 -0
  11. package/examples/testing.json +94456 -0
  12. package/index.html +25 -4
  13. package/package.json +12 -11
  14. package/src/CdcDashboard.tsx +5 -1
  15. package/src/CdcDashboardComponent.tsx +250 -327
  16. package/src/DashboardContext.tsx +15 -1
  17. package/src/_stories/Dashboard.stories.tsx +158 -40
  18. package/src/_stories/_mock/api-filter-chart.json +11 -35
  19. package/src/_stories/_mock/api-filter-map.json +17 -31
  20. package/src/_stories/_mock/bump-chart.json +3554 -0
  21. package/src/_stories/_mock/methodology.json +412 -0
  22. package/src/_stories/_mock/methodologyAPI.ts +90 -0
  23. package/src/_stories/_mock/multi-viz.json +3 -4
  24. package/src/_stories/_mock/pivot-filter.json +14 -12
  25. package/src/_stories/_mock/single-state-dashboard-filters.json +390 -0
  26. package/src/components/CollapsibleVisualizationRow.tsx +44 -0
  27. package/src/components/Column.tsx +1 -1
  28. package/src/components/DashboardFilters/DashboardFilters.tsx +102 -0
  29. package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +218 -0
  30. package/src/components/DashboardFilters/DashboardFiltersEditor/components/DeleteFilterModal.tsx +48 -0
  31. package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +477 -0
  32. package/src/components/DashboardFilters/DashboardFiltersEditor/index.ts +1 -0
  33. package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +191 -0
  34. package/src/components/DashboardFilters/index.ts +3 -0
  35. package/src/components/DataDesignerModal.tsx +9 -9
  36. package/src/components/ExpandCollapseButtons.tsx +20 -0
  37. package/src/components/Header/Header.tsx +1 -102
  38. package/src/components/MultiConfigTabs/MultiConfigTabs.tsx +24 -12
  39. package/src/components/Row.tsx +52 -19
  40. package/src/components/Toggle/Toggle.tsx +2 -4
  41. package/src/components/VisualizationRow.tsx +169 -30
  42. package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +116 -0
  43. package/src/components/VisualizationsPanel/index.ts +1 -0
  44. package/src/components/VisualizationsPanel/visualizations-panel-styles.css +12 -0
  45. package/src/components/Widget.tsx +27 -90
  46. package/src/helpers/FilterBehavior.ts +4 -0
  47. package/src/helpers/addValuesToDashboardFilters.ts +49 -0
  48. package/src/helpers/apiFilterHelpers.ts +102 -0
  49. package/src/helpers/changeFilterActive.ts +39 -0
  50. package/src/helpers/filterData.ts +10 -48
  51. package/src/helpers/generateValuesForFilter.ts +1 -1
  52. package/src/helpers/getAutoLoadVisualization.ts +11 -0
  53. package/src/helpers/getFilteredData.ts +7 -5
  54. package/src/helpers/getVizConfig.ts +23 -2
  55. package/src/helpers/getVizRowColumnLocator.ts +2 -1
  56. package/src/helpers/hasDashboardApplyBehavior.ts +5 -0
  57. package/src/helpers/iconHash.tsx +5 -3
  58. package/src/helpers/loadAPIFilters.ts +74 -0
  59. package/src/helpers/mapDataToConfig.ts +29 -0
  60. package/src/helpers/processData.ts +2 -3
  61. package/src/helpers/reloadURLHelpers.ts +78 -0
  62. package/src/helpers/tests/addValuesToDashboardFilters.test.ts +44 -0
  63. package/src/helpers/tests/apiFilterHelpers.test.ts +156 -0
  64. package/src/helpers/tests/filterData.test.ts +1 -93
  65. package/src/helpers/tests/getFilteredData.test.ts +86 -0
  66. package/src/helpers/tests/loadAPIFiltersWrapper.test.ts +176 -0
  67. package/src/helpers/tests/reloadURLHelpers.test.ts +195 -0
  68. package/src/scss/editor-panel.scss +1 -1
  69. package/src/scss/grid.scss +34 -27
  70. package/src/scss/main.scss +41 -3
  71. package/src/scss/variables.scss +4 -0
  72. package/src/store/dashboard.actions.ts +12 -4
  73. package/src/store/dashboard.reducer.ts +30 -4
  74. package/src/types/APIFilter.ts +1 -5
  75. package/src/types/ConfigRow.ts +2 -0
  76. package/src/types/Dashboard.ts +1 -1
  77. package/src/types/DashboardConfig.ts +2 -4
  78. package/src/types/DashboardFilters.ts +7 -0
  79. package/src/types/InitialState.ts +1 -1
  80. package/src/types/MultiDashboard.ts +2 -2
  81. package/src/types/SharedFilter.ts +4 -6
  82. package/src/types/Tab.ts +1 -1
  83. package/LICENSE +0 -201
  84. package/src/components/Filters.tsx +0 -88
  85. package/src/components/Header/FilterModal.tsx +0 -510
  86. package/src/components/VisualizationsPanel.tsx +0 -95
  87. 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 reloadURLHelpers from './helpers/reloadURLHelpers'
62
+ import { addValuesToDashboardFilters } from './helpers/addValuesToDashboardFilters'
63
+ import { DashboardFilters } from './types/DashboardFilters'
64
+ import DashboardSharedFilters from './components/DashboardFilters'
65
+ import ExpandCollapseButtons from './components/ExpandCollapseButtons'
66
+ import { hasDashboardApplyBehavior } from './helpers/hasDashboardApplyBehavior'
67
+ import { loadAPIFiltersFactory } from './helpers/loadAPIFilters'
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,42 @@ 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)
119
- }
120
- }
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])
121
98
 
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 = (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
- }
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
- }
99
+ const loadAPIFilters = loadAPIFiltersFactory(dispatch, setAPIFilterDropdowns, autoLoadFilterIndexes)
169
100
 
170
- const reloadURLData = async () => {
171
- const { config } = state
101
+ const reloadURLData = async (newFilters?: SharedFilter[]) => {
102
+ const config = _.cloneDeep(state.config)
172
103
  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)
104
+ const filters = newFilters || config.dashboard.sharedFilters
105
+ const datasetKeys = Object.keys(config.datasets)
106
+
107
+ const newData = _.cloneDeep(state.data)
108
+ const newDatasets = _.cloneDeep(config.datasets)
109
+ let dataWasFetched = false
177
110
  let newFileName = ''
178
111
 
179
112
  for (let i = 0; i < datasetKeys.length; i++) {
180
113
  const datasetKey = datasetKeys[i]
181
114
  const dataset = config.datasets[datasetKey]
182
- const filters = config.dashboard?.sharedFilters
115
+
183
116
  if (dataset.dataUrl && filters) {
184
117
  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
-
118
+ const currentQSParams = Object.fromEntries(new URLSearchParams(dataUrl.search))
119
+ const updatedQSParams = {}
189
120
  filters.forEach(filter => {
190
- // filter.active is always a string when filter.type is 'urlfilter'
191
- if (filter.type === 'urlfilter' && !Array.isArray(filter.active)) {
121
+ if (
122
+ filter.type === 'urlfilter' &&
123
+ reloadURLHelpers.filterUsedByDataUrl(filter, datasetKey, config.visualizations)
124
+ ) {
192
125
  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
- }
126
+ newFileName = reloadURLHelpers.getNewFileName(newFileName, filter, datasetKey)
207
127
  }
208
128
 
209
129
  if (!!filter.queryParameter) {
@@ -213,63 +133,53 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
213
133
  updatedQSParams[filter.queryParameter] = filter.active
214
134
  }
215
135
  }
216
- }
217
- })
218
136
 
219
- Object.keys(updatedQSParams).forEach(updatedParam => {
220
- if (decodeURIComponent(updatedQSParams[updatedParam]) !== currentQSParams[updatedParam]) {
221
- isUpdateNeeded = true
137
+ if (filter.apiFilter) {
138
+ updatedQSParams[filter.apiFilter.valueSelector] = filter.active
139
+ }
222
140
  }
223
141
  })
224
142
 
225
- if (isUpdateNeeded) {
226
- datasetsNeedsUpdate = true
227
- Object.keys(currentQSParams).forEach(currentParam => {
228
- if (!updatedQSParams[currentParam]) {
229
- updatedQSParams[currentParam] = currentQSParams[currentParam]
143
+ if (!!newFilters || reloadURLHelpers.isUpdateNeeded(filters, currentQSParams, updatedQSParams)) {
144
+ dataWasFetched = true
145
+ const dataUrlFinal = reloadURLHelpers.getDataURL(
146
+ { ...currentQSParams, ...updatedQSParams },
147
+ dataUrl,
148
+ newFileName
149
+ )
150
+
151
+ await fetchRemoteData(dataUrlFinal).then(responseData => {
152
+ let data: any[] = responseData
153
+ if (responseData && dataset.dataDescription) {
154
+ try {
155
+ data = transform.autoStandardize(data)
156
+ data = transform.developerStandardize(data, dataset.dataDescription)
157
+ } catch (e) {
158
+ //Data not able to be standardized, leave as is
159
+ }
230
160
  }
161
+ newDatasets[datasetKey].data = data
162
+ newDatasets[datasetKey].runtimeDataUrl = dataUrlFinal
163
+ newData[datasetKey] = data
231
164
  })
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
165
  }
255
166
  }
256
167
  }
257
168
 
258
- if (datasetsNeedsUpdate) {
169
+ if (dataWasFetched) {
259
170
  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 } })
171
+ const filtersWithNewValues = addValuesToDashboardFilters(filters, newData)
172
+ const dashboardConfig = newFilters
173
+ ? { ...config.dashboard, sharedFilters: filtersWithNewValues }
174
+ : config.dashboard
175
+ const filteredData = getFilteredData(
176
+ { ...state, config: { ...state.config, dashboard: dashboardConfig } },
177
+ {},
178
+ newData
179
+ )
180
+ dispatch({ type: 'SET_FILTERED_DATA', payload: filteredData })
181
+ const visualizations = reloadURLHelpers.getVisualizationsWithFormattedData(config.visualizations, newData)
182
+ dispatch({ type: 'SET_CONFIG', payload: { dashboard: dashboardConfig, datasets: newDatasets, visualizations } })
273
183
  }
274
184
  }
275
185
 
@@ -284,8 +194,7 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
284
194
  }
285
195
 
286
196
  const setSharedFilter = (key, datum) => {
287
- const { config } = state
288
- let newConfig = { ...config }
197
+ const { config: newConfig, filteredData } = _.cloneDeep(state)
289
198
 
290
199
  for (let i = 0; i < newConfig.dashboard.sharedFilters.length; i++) {
291
200
  const filter = newConfig.dashboard.sharedFilters[i]
@@ -297,32 +206,36 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
297
206
  }
298
207
  }
299
208
 
300
- const newFilteredData = getFilteredData(state, _.cloneDeep(state.filteredData))
209
+ const newFilteredData = getFilteredData({ ...state, config: newConfig }, filteredData)
301
210
 
302
211
  dispatch({ type: 'SET_FILTERED_DATA', payload: newFilteredData })
303
212
  dispatch({ type: 'SET_CONFIG', payload: newConfig })
213
+ dispatch({ type: 'SET_SHARED_FILTERS', payload: newConfig.dashboard.sharedFilters })
304
214
  }
305
215
 
306
216
  useEffect(() => {
307
- if (state.tabSelected && state.tabSelected !== 'Dashboard Preview') return
217
+ if (isEditor && !isPreview) return
308
218
  const { config } = state
309
- if (config.filterBehavior !== FilterBehavior.Apply) {
219
+ if (!hasDashboardApplyBehavior(config.visualizations)) {
310
220
  reloadURLData()
311
221
  }
312
- loadAPIFilters()
313
- }, [])
222
+
223
+ const sharedFiltersWithValues = addValuesToDashboardFilters(config.dashboard.sharedFilters, state.data)
224
+ loadAPIFilters(sharedFiltersWithValues, apiFilterDropdowns)
225
+ updateFilteredData(sharedFiltersWithValues)
226
+ }, [isEditor, isPreview, state.config?.activeDashboard])
314
227
 
315
228
  const updateChildConfig = (visualizationKey, newConfig) => {
316
- const { config } = state
317
- let updatedConfig = { ...config }
229
+ const config = _.cloneDeep(state.config)
230
+ const updatedConfig = _.pick(config, ['visualizations', 'multiDashboards'])
318
231
  updatedConfig.visualizations[visualizationKey] = newConfig
319
232
  updatedConfig.visualizations[visualizationKey].formattedData = config.visualizations[visualizationKey].formattedData
320
233
  if (config.multiDashboards) {
321
234
  const activeDashboard = config.activeDashboard
322
235
  const multiDashboards = [...config.multiDashboards]
323
236
  const label = multiDashboards[activeDashboard].label
324
- const toSave = _.pick(updatedConfig, ['dashboard', 'visualizations', 'rows'])
325
- multiDashboards[activeDashboard] = { ...toSave, label }
237
+ const toSave = { label, visualizations: updatedConfig.visualizations, ..._.pick(config, ['dashboard', 'rows']) }
238
+ multiDashboards[activeDashboard] = toSave
326
239
  updatedConfig.multiDashboards = multiDashboards
327
240
  }
328
241
 
@@ -333,103 +246,11 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
333
246
  }
334
247
  }
335
248
 
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
- }
249
+ const updateFilteredData = (sharedFilters = undefined) => {
250
+ const clonedState = _.cloneDeep(state)
251
+ if (sharedFilters) clonedState.config.dashboard.sharedFilters = sharedFilters
252
+ const newFilteredData = getFilteredData(clonedState)
253
+ dispatch({ type: 'SET_FILTERED_DATA', payload: newFilteredData })
433
254
  }
434
255
 
435
256
  const resizeObserver = new ResizeObserver(entries => {
@@ -446,18 +267,9 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
446
267
  }
447
268
  }, [])
448
269
 
449
- const setPreview = shouldPreview => dispatch({ type: 'SET_PREVIEW', payload: shouldPreview })
450
-
451
270
  // Prevent render if loading
452
271
  if (state.loading) return <Loading />
453
272
 
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
273
  let body: JSX.Element | null = null
462
274
  // Editor mode
463
275
  if (isEditor && !isPreview) {
@@ -465,16 +277,29 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
465
277
 
466
278
  getVizKeys(state.config).forEach(visualizationKey => {
467
279
  const rowNumber = vizRowColumnLocator[visualizationKey]?.row
468
- const visualizationConfig = getVizConfig(visualizationKey, rowNumber, state.config, state.data, state.filteredData)
469
-
470
- const setsSharedFilter = state.config.dashboard.sharedFilters && state.config.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.setBy === visualizationKey).length > 0
471
- const setSharedFilterValue = setsSharedFilter ? state.config.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.setBy === visualizationKey)[0].active : undefined
280
+ const visualizationConfig = getVizConfig(
281
+ visualizationKey,
282
+ rowNumber,
283
+ state.config,
284
+ state.data,
285
+ state.filteredData
286
+ )
287
+ visualizationConfig.uid = visualizationKey
288
+ if (visualizationConfig.type === 'footnotes') visualizationConfig.formattedData = undefined
289
+ const setsSharedFilter =
290
+ state.config.dashboard.sharedFilters &&
291
+ state.config.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.setBy === visualizationKey).length > 0
292
+ const setSharedFilterValue = setsSharedFilter
293
+ ? state.config.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.setBy === visualizationKey)[0].active
294
+ : undefined
472
295
 
473
296
  if (visualizationConfig.editing) {
474
297
  subVisualizationEditing = true
475
298
 
476
299
  const _updateConfig = newConfig => {
477
- let dataCorrectedConfig = visualizationConfig.originalFormattedData ? { ...newConfig, formattedData: visualizationConfig.originalFormattedData } : newConfig
300
+ let dataCorrectedConfig = visualizationConfig.originalFormattedData
301
+ ? { ...newConfig, formattedData: visualizationConfig.originalFormattedData }
302
+ : newConfig
478
303
  updateChildConfig(visualizationKey, dataCorrectedConfig)
479
304
  }
480
305
 
@@ -524,7 +349,13 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
524
349
  body = (
525
350
  <>
526
351
  <Header visualizationKey={visualizationKey} subEditor='Data Bite' />
527
- <CdcDataBite key={visualizationKey} config={{ ...visualizationConfig, newViz: true }} isEditor={true} setConfig={_updateConfig} isDashboard={true} />
352
+ <CdcDataBite
353
+ key={visualizationKey}
354
+ config={{ ...visualizationConfig, newViz: true }}
355
+ isEditor={true}
356
+ setConfig={_updateConfig}
357
+ isDashboard={true}
358
+ />
528
359
  </>
529
360
  )
530
361
  break
@@ -532,7 +363,14 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
532
363
  body = (
533
364
  <>
534
365
  <Header visualizationKey={visualizationKey} subEditor='Waffle Chart' />
535
- <CdcWaffleChart key={visualizationKey} config={visualizationConfig} isEditor={true} setConfig={_updateConfig} isDashboard={true} configUrl={undefined} />
366
+ <CdcWaffleChart
367
+ key={visualizationKey}
368
+ config={visualizationConfig}
369
+ isEditor={true}
370
+ setConfig={_updateConfig}
371
+ isDashboard={true}
372
+ configUrl={undefined}
373
+ />
536
374
  </>
537
375
  )
538
376
  break
@@ -540,7 +378,14 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
540
378
  body = (
541
379
  <>
542
380
  <Header visualizationKey={visualizationKey} subEditor='Markup Include' />
543
- <CdcMarkupInclude key={visualizationKey} config={visualizationConfig} isEditor={true} setConfig={_updateConfig} isDashboard={true} configUrl={undefined} />
381
+ <CdcMarkupInclude
382
+ key={visualizationKey}
383
+ config={visualizationConfig}
384
+ isEditor={true}
385
+ setConfig={_updateConfig}
386
+ isDashboard={true}
387
+ configUrl={undefined}
388
+ />
544
389
  </>
545
390
  )
546
391
  break
@@ -548,17 +393,28 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
548
393
  body = (
549
394
  <>
550
395
  <Header visualizationKey={visualizationKey} subEditor='Filtered Text' />
551
- <CdcFilteredText key={visualizationKey} config={visualizationConfig} isEditor={true} setConfig={_updateConfig} isDashboard={true} configUrl={undefined} />
396
+ <CdcFilteredText
397
+ key={visualizationKey}
398
+ config={visualizationConfig}
399
+ isEditor={true}
400
+ setConfig={_updateConfig}
401
+ isDashboard={true}
402
+ configUrl={undefined}
403
+ />
552
404
  </>
553
405
  )
554
406
  break
555
- case 'filter-dropdowns':
407
+ case 'dashboardFilters':
556
408
  const hideFilter = visualizationConfig.autoLoad && inNoDataState
557
409
  body = !hideFilter ? (
558
410
  <>
559
411
  <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} />
412
+ <DashboardSharedFilters
413
+ isEditor={true}
414
+ visualizationConfig={visualizationConfig}
415
+ apiFilterDropdowns={apiFilterDropdowns}
416
+ setConfig={_updateConfig}
417
+ />
562
418
  </>
563
419
  ) : (
564
420
  <></>
@@ -568,10 +424,27 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
568
424
  body = (
569
425
  <>
570
426
  <Header visualizationKey={visualizationKey} subEditor='Table' />
571
- <DataTableStandAlone visualizationKey={visualizationKey} config={visualizationConfig} isEditor={true} updateConfig={_updateConfig} />
427
+ <DataTableStandAlone
428
+ visualizationKey={visualizationKey}
429
+ config={visualizationConfig}
430
+ isEditor={true}
431
+ updateConfig={_updateConfig}
432
+ />
572
433
  </>
573
434
  )
574
435
  break
436
+ case 'footnotes':
437
+ body = (
438
+ <>
439
+ <Header visualizationKey={visualizationKey} subEditor='Footnotes' />
440
+ <FootnotesStandAlone
441
+ visualizationKey={visualizationKey}
442
+ config={{ ...visualizationConfig, datasets: state.config.datasets }}
443
+ isEditor={true}
444
+ updateConfig={_updateConfig}
445
+ />
446
+ </>
447
+ )
575
448
  default:
576
449
  body = <></>
577
450
  break
@@ -584,7 +457,7 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
584
457
  <DndProvider backend={HTML5Backend}>
585
458
  <div className='header-container'>
586
459
  <Header />
587
- <VisualizationsPanel loadConfig={newConfig => dispatch({ type: 'UPDATE_CONFIG', payload: [newConfig] })} config={state.config} />
460
+ <VisualizationsPanel />
588
461
  </div>
589
462
 
590
463
  <div className='layout-container'>
@@ -596,31 +469,27 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
596
469
  } else {
597
470
  const { config } = state
598
471
  const { title, description } = config.dashboard || {}
472
+
599
473
  body = (
600
474
  <>
601
475
  {isEditor && <Header />}
602
476
  <MultiTabs isEditor={isEditor && !isPreview} />
603
477
  <Layout.Responsive isEditor={isEditor}>
604
478
  <div className={`cdc-dashboard-inner-container${isEditor ? ' is-editor' : ''}`}>
605
- <Title title={title} isDashboard={true} classes={[`dashboard-title`, `${config.dashboard.theme ?? 'theme-blue'}`]} />
479
+ <Title
480
+ title={title}
481
+ isDashboard={true}
482
+ classes={[`dashboard-title`, `${config.dashboard.theme ?? 'theme-blue'}`]}
483
+ />
606
484
  {/* Description */}
607
485
  {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
486
  {/* Visualizations */}
618
487
  {config.rows &&
619
488
  config.rows
620
489
  .filter(row => row.columns.filter(col => col.widget).length !== 0)
621
490
  .map((row, index) => {
622
491
  if (row.multiVizColumn && (isPreview || !isEditor)) {
623
- const filteredData = getFilteredData(state)
492
+ const filteredData = getFilteredData(state, _.cloneDeep(state.data))
624
493
  const data = filteredData[index] ?? row.formattedData
625
494
  const dataGroups = {}
626
495
  data.forEach(d => {
@@ -628,36 +497,68 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
628
497
  if (!dataGroups[groupKey]) dataGroups[groupKey] = []
629
498
  dataGroups[groupKey].push(d)
630
499
  })
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
- })
500
+ return (
501
+ <>
502
+ {/* Expand/Collapse All */}
503
+ {row.expandCollapseAllButtons === true && (
504
+ <ExpandCollapseButtons setAllExpanded={setAllExpanded} />
505
+ )}
506
+ {Object.keys(dataGroups).map(groupName => {
507
+ const dataValue = dataGroups[groupName]
508
+ return (
509
+ <VisualizationRow
510
+ key={`row__${index}__${groupName}`}
511
+ allExpanded={allExpanded}
512
+ filteredDataOverride={dataValue}
513
+ groupName={groupName}
514
+ row={row}
515
+ rowIndex={index}
516
+ setSharedFilter={setSharedFilter}
517
+ updateChildConfig={updateChildConfig}
518
+ apiFilterDropdowns={apiFilterDropdowns}
519
+ currentViewport={currentViewport}
520
+ />
521
+ )
522
+ })}
523
+ </>
524
+ )
650
525
  } else {
651
526
  return (
652
- <VisualizationRow key={`row__${index}`} row={row} rowIndex={index} setSharedFilter={setSharedFilter} updateChildConfig={updateChildConfig} applyFilters={applyFilters} apiFilterDropdowns={apiFilterDropdowns} handleOnChange={handleOnChange} currentViewport={currentViewport} />
527
+ <VisualizationRow
528
+ key={`row__${index}`}
529
+ allExpanded={false}
530
+ groupName={''}
531
+ row={row}
532
+ rowIndex={index}
533
+ setSharedFilter={setSharedFilter}
534
+ updateChildConfig={updateChildConfig}
535
+ apiFilterDropdowns={apiFilterDropdowns}
536
+ currentViewport={currentViewport}
537
+ />
653
538
  )
654
539
  }
655
540
  })}
656
541
 
657
542
  {/* Image or PDF Inserts */}
658
543
  <section className='download-buttons'>
659
- {config.table?.downloadImageButton && <MediaControls.Button title='Download Dashboard as Image' type='image' state={config} text='Download Dashboard Image' elementToCapture={imageId} />}
660
- {config.table?.downloadPdfButton && <MediaControls.Button title='Download Dashboard as PDF' type='pdf' state={config} text='Download Dashboard PDF' elementToCapture={imageId} />}
544
+ {config.table?.downloadImageButton && (
545
+ <MediaControls.Button
546
+ title='Download Dashboard as Image'
547
+ type='image'
548
+ state={config}
549
+ text='Download Dashboard Image'
550
+ elementToCapture={imageId}
551
+ />
552
+ )}
553
+ {config.table?.downloadPdfButton && (
554
+ <MediaControls.Button
555
+ title='Download Dashboard as PDF'
556
+ type='pdf'
557
+ state={config}
558
+ text='Download Dashboard PDF'
559
+ elementToCapture={imageId}
560
+ />
561
+ )}
661
562
  </section>
662
563
 
663
564
  {/* Data Table */}
@@ -702,14 +603,26 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
702
603
  })
703
604
 
704
605
  //Applys any applicable filters to the Table
705
- const filteredTableData = applicableFilters.length > 0 ? filterData(applicableFilters, config.datasets[datasetKey].data) : undefined
606
+ const filteredTableData =
607
+ applicableFilters.length > 0
608
+ ? filterData(applicableFilters, config.datasets[datasetKey].data)
609
+ : undefined
706
610
  return (
707
- <div className='multi-table-container' id={`data-table-${datasetKey}`} key={`data-table-${datasetKey}`}>
611
+ <div
612
+ className='multi-table-container'
613
+ id={`data-table-${datasetKey}`}
614
+ key={`data-table-${datasetKey}`}
615
+ >
708
616
  <DataTable
709
617
  config={config as TableConfig}
710
618
  dataConfig={config.datasets[datasetKey]}
711
619
  rawData={config.datasets[datasetKey].data?.[0]?.tableData || config.datasets[datasetKey].data}
712
- runtimeData={config.datasets[datasetKey].data?.[0]?.tableData || filteredTableData || config.datasets[datasetKey].data || []}
620
+ runtimeData={
621
+ config.datasets[datasetKey].data?.[0]?.tableData ||
622
+ filteredTableData ||
623
+ config.datasets[datasetKey].data ||
624
+ []
625
+ }
713
626
  expandDataTable={config.table.expanded}
714
627
  tableTitle={datasetKey}
715
628
  viewport={currentViewport}
@@ -728,7 +641,17 @@ export default function CdcDashboard({ initialState, isEditor = false, isDebug =
728
641
 
729
642
  return (
730
643
  <GlobalContextProvider>
731
- <DashboardContext.Provider value={{ ...state, setParentConfig: editorContext.setTempConfig, outerContainerRef, isDebug }}>
644
+ <DashboardContext.Provider
645
+ value={{
646
+ ...state,
647
+ setParentConfig: editorContext.setTempConfig,
648
+ outerContainerRef,
649
+ isDebug,
650
+ loadAPIFilters,
651
+ setAPIFilterDropdowns,
652
+ reloadURLData
653
+ }}
654
+ >
732
655
  <DashboardDispatchContext.Provider value={dispatch}>
733
656
  <div className={dashboardContainerClasses.join(' ')} ref={outerContainerRef} data-download-id={imageId}>
734
657
  {body}