@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.
- package/dist/cdcdashboard.js +122872 -112065
- package/examples/custom/css/respiratory.css +236 -0
- package/examples/custom/js/respiratory.js +242 -0
- package/examples/default-multi-dataset-shared-filter.json +1729 -0
- package/examples/ed-visits-county-file.json +618 -0
- package/examples/filtered-dash.json +6 -21
- package/index.html +10 -1
- package/package.json +12 -11
- package/src/CdcDashboard.tsx +5 -1
- package/src/CdcDashboardComponent.tsx +165 -306
- package/src/DashboardContext.tsx +9 -1
- package/src/_stories/Dashboard.stories.tsx +38 -34
- package/src/_stories/_mock/api-filter-chart.json +11 -35
- package/src/_stories/_mock/api-filter-map.json +17 -31
- package/src/_stories/_mock/multi-viz.json +2 -3
- package/src/_stories/_mock/pivot-filter.json +14 -12
- package/src/components/CollapsibleVisualizationRow.tsx +44 -0
- package/src/components/Column.tsx +1 -1
- package/src/components/DashboardFilters/DashboardFilters.tsx +80 -0
- package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +218 -0
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/DeleteFilterModal.tsx +48 -0
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +367 -0
- package/src/components/DashboardFilters/DashboardFiltersEditor/index.ts +1 -0
- package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +143 -0
- package/src/components/DashboardFilters/index.ts +3 -0
- package/src/components/DataDesignerModal.tsx +9 -9
- package/src/components/ExpandCollapseButtons.tsx +20 -0
- package/src/components/Header/Header.tsx +1 -97
- package/src/components/MultiConfigTabs/MultiConfigTabs.tsx +4 -4
- package/src/components/Row.tsx +52 -19
- package/src/components/Toggle/Toggle.tsx +2 -4
- package/src/components/VisualizationRow.tsx +82 -24
- package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +116 -0
- package/src/components/VisualizationsPanel/index.ts +1 -0
- package/src/components/VisualizationsPanel/visualizations-panel-styles.css +12 -0
- package/src/components/Widget.tsx +26 -90
- package/src/helpers/apiFilterHelpers.ts +51 -0
- package/src/helpers/changeFilterActive.ts +30 -0
- package/src/helpers/filterData.ts +10 -48
- package/src/helpers/generateValuesForFilter.ts +1 -1
- package/src/helpers/getAutoLoadVisualization.ts +11 -0
- package/src/helpers/getFilteredData.ts +4 -2
- package/src/helpers/getVizConfig.ts +23 -2
- package/src/helpers/getVizRowColumnLocator.ts +2 -1
- package/src/helpers/hasDashboardApplyBehavior.ts +5 -0
- package/src/helpers/iconHash.tsx +3 -3
- package/src/helpers/mapDataToConfig.ts +29 -0
- package/src/helpers/processData.ts +2 -3
- package/src/helpers/reloadURLHelpers.ts +68 -0
- package/src/helpers/tests/filterData.test.ts +1 -93
- package/src/scss/editor-panel.scss +1 -1
- package/src/scss/grid.scss +34 -27
- package/src/scss/main.scss +41 -3
- package/src/scss/variables.scss +4 -0
- package/src/store/dashboard.actions.ts +12 -4
- package/src/store/dashboard.reducer.ts +30 -4
- package/src/types/APIFilter.ts +1 -5
- package/src/types/ConfigRow.ts +2 -0
- package/src/types/Dashboard.ts +1 -1
- package/src/types/DashboardConfig.ts +2 -4
- package/src/types/DashboardFilters.ts +7 -0
- package/src/types/InitialState.ts +1 -1
- package/src/types/MultiDashboard.ts +2 -2
- package/src/types/SharedFilter.ts +2 -5
- package/src/types/Tab.ts +1 -1
- package/LICENSE +0 -201
- package/src/components/Filters.tsx +0 -88
- package/src/components/Header/FilterModal.tsx +0 -510
- package/src/components/VisualizationsPanel.tsx +0 -95
- 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
|
|
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 {
|
|
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
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const filterParents =
|
|
114
|
-
const notAllParentFiltersSelected = filterParents.some(p => !p.active)
|
|
115
|
-
if (filterParents && notAllParentFiltersSelected) return
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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 =
|
|
123
|
-
if (
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
return
|
|
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
|
-
|
|
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
|
|
158
|
+
const reloadURLData = async (newFilters?: SharedFilter[]) => {
|
|
159
|
+
const config = _.cloneDeep(state.config)
|
|
172
160
|
if (!config.datasets) return
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
172
|
+
|
|
183
173
|
if (dataset.dataUrl && filters) {
|
|
184
174
|
const dataUrl = new URL(dataset.runtimeDataUrl || dataset.dataUrl, window.location.origin)
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
192
|
+
if (filter.apiFilter) {
|
|
193
|
+
updatedQSParams[filter.apiFilter.valueSelector] = filter.active
|
|
194
|
+
}
|
|
222
195
|
}
|
|
223
196
|
})
|
|
224
197
|
|
|
225
|
-
if (isUpdateNeeded) {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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 (
|
|
220
|
+
if (dataWasFetched) {
|
|
259
221
|
dispatch({ type: 'SET_DATA', payload: newData })
|
|
260
|
-
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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,
|
|
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 (
|
|
262
|
+
if (isEditor && !isPreview) return
|
|
308
263
|
const { config } = state
|
|
309
|
-
if (config.
|
|
264
|
+
if (!hasDashboardApplyBehavior(config.visualizations)) {
|
|
310
265
|
reloadURLData()
|
|
311
266
|
}
|
|
312
|
-
|
|
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
|
|
317
|
-
|
|
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(
|
|
325
|
-
multiDashboards[activeDashboard] =
|
|
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
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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 '
|
|
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
|
-
<
|
|
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
|
|
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
|
-
|
|
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}
|