@cdc/dashboard 4.25.10 → 4.26.1
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/Dynamic_Data.md +66 -0
- package/dist/{cdcdashboard-fce76882.es.js → cdcdashboard-BnB1QM5d.es.js} +6 -13
- package/dist/{cdcdashboard-c55ac1ea.es.js → cdcdashboard-D6CG2-Hb.es.js} +5 -12
- package/dist/{cdcdashboard-31a33da1.es.js → cdcdashboard-MXgURbdZ.es.js} +6 -13
- package/dist/{cdcdashboard-1a1724a1.es.js → cdcdashboard-dgT_1dIT.es.js} +136 -151
- package/dist/cdcdashboard.js +84214 -79641
- package/examples/api-dashboard-data.json +272 -0
- package/examples/api-dashboard-years.json +11 -0
- package/examples/api-geographies-data.json +11 -0
- package/examples/api-test/categories.json +18 -0
- package/examples/api-test/chart-data.json +602 -0
- package/examples/api-test/topics.json +47 -0
- package/examples/api-test/years.json +22 -0
- package/examples/markup-axis-label.json +4167 -0
- package/examples/private/big-dashboard.json +39095 -39077
- package/examples/private/cat-y.json +1235 -0
- package/examples/private/chronic-dash.json +1584 -0
- package/examples/private/clade-2.json +430 -0
- package/examples/private/diabetes.json +546 -196
- package/examples/private/map-issue.json +2260 -0
- package/examples/private/markup-footer/mortality-deaths-footnotes-age.csv +3 -0
- package/examples/private/mpinc-state-reports.json +2260 -0
- package/examples/private/mpox.json +38128 -0
- package/examples/private/nwss/rsv.json +1240 -0
- package/examples/private/reset.json +32920 -0
- package/examples/private/simple-dash.json +490 -0
- package/examples/private/test-dash.json +0 -0
- package/examples/private/test123.json +491 -0
- package/examples/test-api-filter-reset.json +132 -0
- package/examples/test-dashboard-simple.json +503 -0
- package/index.html +25 -26
- package/package.json +11 -11
- package/src/CdcDashboardComponent.tsx +35 -10
- package/src/DashboardContext.tsx +3 -1
- package/src/_stories/Dashboard.DataSetup.stories.tsx +203 -0
- package/src/_stories/Dashboard.stories.tsx +402 -1
- package/src/_stories/_mock/custom-order-new-values.json +116 -0
- package/src/_stories/_mock/filter-cascade.json +3350 -0
- package/src/_stories/_mock/gallery-data-bite-dashboard.json +3500 -0
- package/src/_stories/_mock/nested-parent-child-filters.json +392 -0
- package/src/_stories/_mock/parent-child-filters.json +233 -0
- package/src/components/DashboardFilters/DashboardFilters.tsx +54 -31
- package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +118 -50
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +96 -108
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/NestedDropDownDashboard.tsx +196 -59
- package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +129 -29
- package/src/components/DashboardFilters/_stories/DashboardFilters.stories.tsx +62 -3
- package/src/components/DataDesignerModal.tsx +18 -6
- package/src/components/Header/Header.tsx +53 -21
- package/src/components/Toggle/Toggle.tsx +48 -48
- package/src/components/VisualizationRow.tsx +73 -6
- package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +2 -3
- package/src/components/Widget/Widget.tsx +1 -1
- package/src/data/initial-state.js +1 -0
- package/src/helpers/addValuesToDashboardFilters.ts +24 -6
- package/src/helpers/apiFilterHelpers.ts +26 -2
- package/src/helpers/changeFilterActive.ts +67 -65
- package/src/helpers/filterData.ts +52 -7
- package/src/helpers/filterResetHelpers.ts +102 -0
- package/src/helpers/formatConfigBeforeSave.ts +6 -5
- package/src/helpers/getUpdateConfig.ts +91 -91
- package/src/helpers/getVizConfig.ts +2 -2
- package/src/helpers/loadAPIFilters.ts +109 -99
- package/src/helpers/tests/filterResetHelpers.test.ts +532 -0
- package/src/helpers/tests/updatesChildFilters.test.ts +53 -22
- package/src/helpers/updateChildFilters.ts +50 -27
- package/src/index.tsx +1 -0
- package/src/scss/editor-panel.scss +3 -431
- package/src/scss/main.scss +142 -25
- package/src/store/errorMessage/errorMessage.reducer.ts +1 -1
- package/src/test/CdcDashboard.test.jsx +9 -4
- package/src/types/Dashboard.ts +1 -0
- package/src/types/DashboardFilters.ts +9 -8
- package/src/types/FilterStyles.ts +8 -7
- package/src/types/SharedFilter.ts +13 -0
- package/LICENSE +0 -201
- package/examples/private/DEV-11072.json +0 -7591
- package/examples/private/burden_toolkit_mortality_diabetes_attributable_deaths_data.csv +0 -14041
- package/examples/private/burden_toolkit_mortality_diabetes_attributable_deaths_per_100000_data.csv +0 -14041
- package/examples/private/burden_toolkit_mortality_qaly_data.csv +0 -18721
- package/examples/private/burden_toolkit_mortality_yll_data.csv +0 -18721
- package/examples/private/pedro.json +0 -1
- package/src/helpers/getAutoLoadVisualization.ts +0 -11
- package/src/scss/mixins.scss +0 -47
- package/src/scss/variables.scss +0 -5
- /package/dist/{cdcdashboard-548642e6.es.js → cdcdashboard-Ct2SB0vL.es.js} +0 -0
|
@@ -2,7 +2,11 @@ import _ from 'lodash'
|
|
|
2
2
|
import { SharedFilter } from '../types/SharedFilter'
|
|
3
3
|
import { FILTER_STYLE } from '../types/FilterStyles'
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Recursively calculates the tier/depth of a filter based on its parent dependencies.
|
|
7
|
+
* Root filters (no parents) are tier 1, children of root filters are tier 2, etc.
|
|
8
|
+
*/
|
|
9
|
+
const findFilterTier = (filters: SharedFilter[], sharedFilter: SharedFilter): number => {
|
|
6
10
|
if (!sharedFilter.parents?.length) {
|
|
7
11
|
return 1
|
|
8
12
|
} else {
|
|
@@ -12,6 +16,10 @@ const findFilterTier = (filters: SharedFilter[], sharedFilter: SharedFilter) =>
|
|
|
12
16
|
}
|
|
13
17
|
}
|
|
14
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Calculates and assigns tier values to all filters, returns the maximum tier.
|
|
21
|
+
* Mutates the filter objects by setting their tier property.
|
|
22
|
+
*/
|
|
15
23
|
function getMaxTierAndSetFilterTiers(filters: SharedFilter[]): number {
|
|
16
24
|
let maxTier = 1
|
|
17
25
|
filters.forEach(sharedFilter => {
|
|
@@ -23,7 +31,30 @@ function getMaxTierAndSetFilterTiers(filters: SharedFilter[]): number {
|
|
|
23
31
|
return maxTier
|
|
24
32
|
}
|
|
25
33
|
|
|
26
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Checks if a filter is currently at its reset/incomplete state.
|
|
36
|
+
* A filter is incomplete if it's visible AND:
|
|
37
|
+
* - The active value is empty/null/undefined, OR
|
|
38
|
+
* - The active value equals the resetLabel (if one is defined)
|
|
39
|
+
*/
|
|
40
|
+
export const isFilterAtResetState = (filter: SharedFilter): boolean => {
|
|
41
|
+
// Only check filters that are visible to the user
|
|
42
|
+
if (!filter.showDropdown) return false
|
|
43
|
+
|
|
44
|
+
// Check if active value is empty/null/undefined
|
|
45
|
+
const isEmptyValue = filter.active === '' || filter.active === null || filter.active === undefined
|
|
46
|
+
|
|
47
|
+
// Check if active value equals the resetLabel
|
|
48
|
+
const equalsResetLabel = filter.resetLabel && filter.resetLabel === filter.active
|
|
49
|
+
|
|
50
|
+
return isEmptyValue || equalsResetLabel
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Filters data by applying filters of a specific tier.
|
|
55
|
+
* Filters are applied hierarchically by tier to handle parent-child dependencies.
|
|
56
|
+
*/
|
|
57
|
+
function filterDataByTier(data = [], filters: SharedFilter[], tier: number) {
|
|
27
58
|
const activeFilters = _.filter(filters, f => (f.resetLabel === f.active ? f.values?.includes(f.resetLabel) : true))
|
|
28
59
|
return data.filter(row => {
|
|
29
60
|
const foundMatchingFilter = activeFilters.find(filter => {
|
|
@@ -51,8 +82,9 @@ function filter(data = [], filters: SharedFilter[], condition) {
|
|
|
51
82
|
isNotTheSelectedValue = subGroupActive && selectedSubGroupValue !== subGroupActive
|
|
52
83
|
}
|
|
53
84
|
|
|
54
|
-
const
|
|
55
|
-
|
|
85
|
+
const isMatchingTier = filter.tier === tier
|
|
86
|
+
// Only apply client-side filtering for datafilter (urlfilters modify the API endpoint instead)
|
|
87
|
+
if (filter.type !== 'urlfilter' && isMatchingTier && isNotTheSelectedValue) {
|
|
56
88
|
return true
|
|
57
89
|
}
|
|
58
90
|
})
|
|
@@ -60,17 +92,30 @@ function filter(data = [], filters: SharedFilter[], condition) {
|
|
|
60
92
|
})
|
|
61
93
|
}
|
|
62
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Filters data based on shared filter configurations.
|
|
97
|
+
* Returns empty array if any filter is at its reset state (incomplete selection).
|
|
98
|
+
* Otherwise applies filters hierarchically by tier to handle parent-child dependencies.
|
|
99
|
+
*/
|
|
63
100
|
export const filterData = (filters: SharedFilter[], _data: Object[]): Object[] => {
|
|
64
101
|
const maxTier = getMaxTierAndSetFilterTiers(filters)
|
|
65
102
|
|
|
103
|
+
// Check if any filters are currently at their reset state
|
|
104
|
+
const hasResetFilters = filters.some(isFilterAtResetState)
|
|
105
|
+
|
|
106
|
+
// If any filter is at reset state, return empty data to show "no data" message
|
|
107
|
+
if (hasResetFilters) {
|
|
108
|
+
return []
|
|
109
|
+
}
|
|
110
|
+
|
|
66
111
|
for (let i = 0; i < maxTier; i++) {
|
|
67
112
|
const lastIteration = i === maxTier - 1
|
|
68
113
|
|
|
69
|
-
const filteredData =
|
|
114
|
+
const filteredData = filterDataByTier(_data, filters, i + 1)
|
|
70
115
|
|
|
71
116
|
if (lastIteration) {
|
|
72
|
-
// not sure if this last run of
|
|
73
|
-
return
|
|
117
|
+
// not sure if this last run of filterDataByTier() function is necessary.
|
|
118
|
+
return filterDataByTier(filteredData, filters, maxTier - 1)
|
|
74
119
|
}
|
|
75
120
|
}
|
|
76
121
|
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { SharedFilter } from '../types/SharedFilter'
|
|
2
|
+
import { APIFilterDropdowns } from '../components/DashboardFilters'
|
|
3
|
+
import { FILTER_STYLE } from '../types/FilterStyles'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Determines the reset value for a filter based on its configuration.
|
|
7
|
+
* When forceEmpty is true (like when clicking "Clear Filters"), always returns empty.
|
|
8
|
+
* Otherwise uses priority: defaultValue > empty string (for resetLabel) > first API option
|
|
9
|
+
* Note: resetLabel is for display purposes only. When present, we return empty string
|
|
10
|
+
* so the placeholder option renders correctly in the dropdown.
|
|
11
|
+
*/
|
|
12
|
+
export const getFilterResetValue = (
|
|
13
|
+
filter: SharedFilter,
|
|
14
|
+
apiFilterDropdowns: APIFilterDropdowns,
|
|
15
|
+
forceEmpty: boolean = false
|
|
16
|
+
): string | undefined => {
|
|
17
|
+
// When clearing filters, always reset to empty/resetLabel state
|
|
18
|
+
if (forceEmpty) {
|
|
19
|
+
// Return empty string to show reset label or placeholder, undefined falls back to first value
|
|
20
|
+
return ''
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// If filter has a defaultValue, use that (for initial load)
|
|
24
|
+
if (filter.defaultValue) {
|
|
25
|
+
return filter.defaultValue
|
|
26
|
+
}
|
|
27
|
+
// If filter has a resetLabel, return empty string so placeholder renders
|
|
28
|
+
if (typeof filter.resetLabel === 'string') {
|
|
29
|
+
return ''
|
|
30
|
+
}
|
|
31
|
+
// Otherwise, use first available value if API filter
|
|
32
|
+
if (filter.apiFilter) {
|
|
33
|
+
const _key = filter.apiFilter.apiEndpoint
|
|
34
|
+
const options = apiFilterDropdowns[_key]
|
|
35
|
+
if (options && options.length > 0) {
|
|
36
|
+
return options[0].value
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return undefined
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Resets a filter's active and queuedActive values based on its filter style.
|
|
44
|
+
* Handles multi-select, nested-dropdown, and standard dropdown styles.
|
|
45
|
+
*/
|
|
46
|
+
export const resetFilterToValue = (
|
|
47
|
+
filter: SharedFilter,
|
|
48
|
+
resetValue: string | undefined,
|
|
49
|
+
apiFilterDropdowns: APIFilterDropdowns
|
|
50
|
+
): void => {
|
|
51
|
+
// Handle multi-select filters
|
|
52
|
+
if (filter.filterStyle === FILTER_STYLE.multiSelect) {
|
|
53
|
+
filter.active = resetValue ? [resetValue] : []
|
|
54
|
+
filter.queuedActive = undefined
|
|
55
|
+
} else if (filter.filterStyle === FILTER_STYLE.nestedDropdown) {
|
|
56
|
+
// For nested dropdowns, reset both group and subgroup
|
|
57
|
+
const _key = filter.apiFilter?.apiEndpoint
|
|
58
|
+
const options = apiFilterDropdowns[_key]
|
|
59
|
+
|
|
60
|
+
// When resetValue is explicitly empty/undefined, clear both group and subgroup
|
|
61
|
+
if (resetValue === '' || resetValue === undefined) {
|
|
62
|
+
filter.active = resetValue || ''
|
|
63
|
+
filter.subGrouping.active = ''
|
|
64
|
+
} else if (options && options.length > 0) {
|
|
65
|
+
// Use specific resetValue or fall back to first option
|
|
66
|
+
const selectedOption = options.find(opt => opt.value === resetValue) || options[0]
|
|
67
|
+
filter.active = selectedOption.value
|
|
68
|
+
if (selectedOption.subOptions && selectedOption.subOptions.length > 0) {
|
|
69
|
+
filter.subGrouping.active = selectedOption.subOptions[0].value
|
|
70
|
+
} else {
|
|
71
|
+
filter.subGrouping.active = ''
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
// No options available, use resetValue or empty
|
|
75
|
+
filter.active = resetValue || ''
|
|
76
|
+
filter.subGrouping.active = ''
|
|
77
|
+
}
|
|
78
|
+
filter.queuedActive = undefined
|
|
79
|
+
} else {
|
|
80
|
+
// Standard dropdown
|
|
81
|
+
filter.active = resetValue
|
|
82
|
+
filter.queuedActive = undefined
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Clears dropdown cache for child filters that have parent dependencies.
|
|
88
|
+
* Sets them to empty arrays so they appear disabled without loading state.
|
|
89
|
+
*/
|
|
90
|
+
export const clearChildFilterDropdowns = (
|
|
91
|
+
sharedFilters: SharedFilter[],
|
|
92
|
+
apiFilterDropdowns: APIFilterDropdowns
|
|
93
|
+
): APIFilterDropdowns => {
|
|
94
|
+
const updatedDropdowns = { ...apiFilterDropdowns }
|
|
95
|
+
sharedFilters.forEach(filter => {
|
|
96
|
+
if (filter.apiFilter && filter.parents && filter.parents.length > 0) {
|
|
97
|
+
// Set to empty array so they show as disabled without loading state
|
|
98
|
+
updatedDropdowns[filter.apiFilter.apiEndpoint] = []
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
return updatedDropdowns
|
|
102
|
+
}
|
|
@@ -17,11 +17,12 @@ const cleanDashboardFootnotes = (config: DashboardConfig) => {
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
const cleanDashboardData = (config: DashboardConfig) => {
|
|
20
|
+
const cleanDashboardData = (config: DashboardConfig, isEditor = false) => {
|
|
21
21
|
if (config.datasets) {
|
|
22
22
|
Object.keys(config.datasets).forEach(datasetKey => {
|
|
23
23
|
delete config.datasets[datasetKey].formattedData
|
|
24
|
-
|
|
24
|
+
// Only delete data when not in editor mode
|
|
25
|
+
if (config.datasets[datasetKey].dataUrl && !isEditor) {
|
|
25
26
|
delete config.datasets[datasetKey].data
|
|
26
27
|
}
|
|
27
28
|
})
|
|
@@ -104,12 +105,12 @@ const removeRuntimeDataURLs = (config: DashboardConfig) => {
|
|
|
104
105
|
}
|
|
105
106
|
}
|
|
106
107
|
|
|
107
|
-
export const stripConfig = configToStrip => {
|
|
108
|
+
export const stripConfig = (configToStrip, isEditor = false) => {
|
|
108
109
|
const strippedConfig = cloneConfig(configToStrip)
|
|
109
110
|
if (strippedConfig.type === 'dashboard') {
|
|
110
111
|
if (strippedConfig.multiDashboards) {
|
|
111
112
|
strippedConfig.multiDashboards.forEach((multiDashboard, i) => {
|
|
112
|
-
cleanDashboardData(strippedConfig.multiDashboards[i])
|
|
113
|
+
cleanDashboardData(strippedConfig.multiDashboards[i], isEditor)
|
|
113
114
|
cleanSharedFilters(strippedConfig.multiDashboards[i])
|
|
114
115
|
cleanDashboardFootnotes(strippedConfig.multiDashboards[i])
|
|
115
116
|
cleanVisualizationFilters(strippedConfig.multiDashboards[i])
|
|
@@ -120,7 +121,7 @@ export const stripConfig = configToStrip => {
|
|
|
120
121
|
delete strippedConfig.label
|
|
121
122
|
}
|
|
122
123
|
delete strippedConfig.activeDashboard
|
|
123
|
-
cleanDashboardData(strippedConfig)
|
|
124
|
+
cleanDashboardData(strippedConfig, isEditor)
|
|
124
125
|
cleanSharedFilters(strippedConfig)
|
|
125
126
|
cleanDashboardFootnotes(strippedConfig)
|
|
126
127
|
cleanVisualizationFilters(strippedConfig)
|
|
@@ -1,91 +1,91 @@
|
|
|
1
|
-
import { DashboardState } from '../store/dashboard.reducer'
|
|
2
|
-
import { DashboardConfig as Config, DashboardConfig } from '../types/DashboardConfig'
|
|
3
|
-
import { filterData } from './filterData'
|
|
4
|
-
import { generateValuesForFilter } from './generateValuesForFilter'
|
|
5
|
-
import { getFormattedData } from './getFormattedData'
|
|
6
|
-
import { getVizKeys } from './getVizKeys'
|
|
7
|
-
import { getVizRowColumnLocator } from './getVizRowColumnLocator'
|
|
8
|
-
|
|
9
|
-
import { getQueryStringFilterValue } from '@cdc/core/helpers/queryStringUtils'
|
|
10
|
-
|
|
11
|
-
type UpdateState = Omit<DashboardState, 'config'> & {
|
|
12
|
-
config?: DashboardConfig
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export const getUpdateConfig =
|
|
16
|
-
(state: UpdateState) =>
|
|
17
|
-
(newConfig, dataOverride?: Object): [Config, Object] => {
|
|
18
|
-
let newFilteredData = {}
|
|
19
|
-
let visualizationKeys = getVizKeys(newConfig)
|
|
20
|
-
|
|
21
|
-
const vizRowColumnLocator = getVizRowColumnLocator(newConfig.rows)
|
|
22
|
-
|
|
23
|
-
if (newConfig.dashboard.sharedFilters) {
|
|
24
|
-
newConfig.dashboard.sharedFilters.forEach((filter, i) => {
|
|
25
|
-
const filterIsSetByVizData = !!visualizationKeys.find(key => key === filter.setBy)
|
|
26
|
-
const _filter = newConfig.dashboard.sharedFilters[i]
|
|
27
|
-
|
|
28
|
-
const setValuesAndActive = filterValues => {
|
|
29
|
-
_filter.values = filterValues
|
|
30
|
-
if (filterValues.length > 0) {
|
|
31
|
-
const defaultValues = _filter.pivot ? _filter.values : _filter.values[0]
|
|
32
|
-
|
|
33
|
-
const queryStringFilterValue = getQueryStringFilterValue(_filter)
|
|
34
|
-
if (queryStringFilterValue) {
|
|
35
|
-
_filter.active = queryStringFilterValue
|
|
36
|
-
} else {
|
|
37
|
-
_filter.active = _filter.active || defaultValues
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const filterValues = generateValuesForFilter(filter.columnName, dataOverride || state.data)
|
|
43
|
-
if (filterIsSetByVizData) {
|
|
44
|
-
if (_filter.order === 'asc') {
|
|
45
|
-
filterValues.sort()
|
|
46
|
-
}
|
|
47
|
-
if (_filter.order === 'desc') {
|
|
48
|
-
filterValues.sort().reverse()
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
setValuesAndActive(filterValues)
|
|
52
|
-
} else if ((!filter.values || filter.values.length === 0) && filter.showDropdown) {
|
|
53
|
-
setValuesAndActive(filterValues)
|
|
54
|
-
}
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
visualizationKeys.forEach(visualizationKey => {
|
|
58
|
-
const row = vizRowColumnLocator[visualizationKey]
|
|
59
|
-
if (newConfig.rows[row]?.datakey) return // data configured on the row level
|
|
60
|
-
const applicableFilters = newConfig.dashboard.sharedFilters.filter(
|
|
61
|
-
sharedFilter => sharedFilter.usedBy && sharedFilter.usedBy.indexOf(visualizationKey) !== -1
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
if (applicableFilters.length > 0) {
|
|
65
|
-
const visualization = newConfig.visualizations[visualizationKey]
|
|
66
|
-
const _newConfigDataSet = newConfig.datasets[visualization.dataKey]
|
|
67
|
-
const formattedData = getFormattedData(
|
|
68
|
-
_newConfigDataSet?.data || visualization.data,
|
|
69
|
-
visualization.dataDescription
|
|
70
|
-
)
|
|
71
|
-
const _data = formattedData || (dataOverride || state.data)[visualization.dataKey]
|
|
72
|
-
newFilteredData[visualizationKey] = filterData(applicableFilters, _data)
|
|
73
|
-
}
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
newConfig.rows.forEach((row, rowIndex) => {
|
|
77
|
-
const applicableFilters = newConfig.dashboard.sharedFilters.filter(
|
|
78
|
-
sharedFilter => sharedFilter.usedBy && sharedFilter.usedBy.indexOf(rowIndex) !== -1
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
if (applicableFilters.length > 0) {
|
|
82
|
-
const formattedData = getFormattedData(row.data, row.dataDescription)
|
|
83
|
-
const _data = formattedData || (dataOverride || state.data)[rowIndex]
|
|
84
|
-
newFilteredData[rowIndex] = filterData(applicableFilters, _data)
|
|
85
|
-
}
|
|
86
|
-
})
|
|
87
|
-
}
|
|
88
|
-
//Enforce default values that need to be calculated at runtime
|
|
89
|
-
newConfig.runtime = {}
|
|
90
|
-
return [newConfig, newFilteredData]
|
|
91
|
-
}
|
|
1
|
+
import { DashboardState } from '../store/dashboard.reducer'
|
|
2
|
+
import { DashboardConfig as Config, DashboardConfig } from '../types/DashboardConfig'
|
|
3
|
+
import { filterData } from './filterData'
|
|
4
|
+
import { generateValuesForFilter } from './generateValuesForFilter'
|
|
5
|
+
import { getFormattedData } from './getFormattedData'
|
|
6
|
+
import { getVizKeys } from './getVizKeys'
|
|
7
|
+
import { getVizRowColumnLocator } from './getVizRowColumnLocator'
|
|
8
|
+
|
|
9
|
+
import { getQueryStringFilterValue } from '@cdc/core/helpers/queryStringUtils'
|
|
10
|
+
|
|
11
|
+
type UpdateState = Omit<DashboardState, 'config'> & {
|
|
12
|
+
config?: DashboardConfig
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const getUpdateConfig =
|
|
16
|
+
(state: UpdateState) =>
|
|
17
|
+
(newConfig, dataOverride?: Object): [Config, Object] => {
|
|
18
|
+
let newFilteredData = {}
|
|
19
|
+
let visualizationKeys = getVizKeys(newConfig)
|
|
20
|
+
|
|
21
|
+
const vizRowColumnLocator = getVizRowColumnLocator(newConfig.rows)
|
|
22
|
+
|
|
23
|
+
if (newConfig.dashboard.sharedFilters) {
|
|
24
|
+
newConfig.dashboard.sharedFilters.forEach((filter, i) => {
|
|
25
|
+
const filterIsSetByVizData = !!visualizationKeys.find(key => key === filter.setBy)
|
|
26
|
+
const _filter = newConfig.dashboard.sharedFilters[i]
|
|
27
|
+
|
|
28
|
+
const setValuesAndActive = filterValues => {
|
|
29
|
+
_filter.values = filterValues
|
|
30
|
+
if (filterValues.length > 0) {
|
|
31
|
+
const defaultValues = _filter.pivot ? _filter.values : _filter.values[0]
|
|
32
|
+
|
|
33
|
+
const queryStringFilterValue = getQueryStringFilterValue(_filter)
|
|
34
|
+
if (queryStringFilterValue) {
|
|
35
|
+
_filter.active = queryStringFilterValue
|
|
36
|
+
} else {
|
|
37
|
+
_filter.active = _filter.active || defaultValues
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const filterValues = generateValuesForFilter(filter.columnName, dataOverride || state.data)
|
|
43
|
+
if (filterIsSetByVizData) {
|
|
44
|
+
if (_filter.order === 'asc') {
|
|
45
|
+
filterValues.sort()
|
|
46
|
+
}
|
|
47
|
+
if (_filter.order === 'desc') {
|
|
48
|
+
filterValues.sort().reverse()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
setValuesAndActive(filterValues)
|
|
52
|
+
} else if ((!filter.values || filter.values.length === 0) && filter.showDropdown) {
|
|
53
|
+
setValuesAndActive(filterValues)
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
visualizationKeys.forEach(visualizationKey => {
|
|
58
|
+
const row = vizRowColumnLocator[visualizationKey]
|
|
59
|
+
if (newConfig.rows[row]?.datakey) return // data configured on the row level
|
|
60
|
+
const applicableFilters = newConfig.dashboard.sharedFilters.filter(
|
|
61
|
+
sharedFilter => sharedFilter.usedBy && sharedFilter.usedBy.indexOf(visualizationKey) !== -1
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
if (applicableFilters.length > 0) {
|
|
65
|
+
const visualization = newConfig.visualizations[visualizationKey]
|
|
66
|
+
const _newConfigDataSet = newConfig.datasets[visualization.dataKey]
|
|
67
|
+
const formattedData = getFormattedData(
|
|
68
|
+
_newConfigDataSet?.data || visualization.data,
|
|
69
|
+
visualization.dataDescription
|
|
70
|
+
)
|
|
71
|
+
const _data = formattedData || (dataOverride || state.data)[visualization.dataKey]
|
|
72
|
+
newFilteredData[visualizationKey] = filterData(applicableFilters, _data)
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
newConfig.rows.forEach((row, rowIndex) => {
|
|
77
|
+
const applicableFilters = newConfig.dashboard.sharedFilters.filter(
|
|
78
|
+
sharedFilter => sharedFilter.usedBy && sharedFilter.usedBy.indexOf(rowIndex) !== -1
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
if (applicableFilters.length > 0) {
|
|
82
|
+
const formattedData = getFormattedData(row.data, row.dataDescription)
|
|
83
|
+
const _data = formattedData || (dataOverride || state.data)[rowIndex]
|
|
84
|
+
newFilteredData[rowIndex] = filterData(applicableFilters, _data)
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
//Enforce default values that need to be calculated at runtime
|
|
89
|
+
newConfig.runtime = {}
|
|
90
|
+
return [newConfig, newFilteredData]
|
|
91
|
+
}
|
|
@@ -8,7 +8,7 @@ import { AnyVisualization } from '@cdc/core/types/Visualization'
|
|
|
8
8
|
|
|
9
9
|
const transform = new DataTransform()
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
const getFootnotesVizConfig = (
|
|
12
12
|
visualizationConfig: AnyVisualization,
|
|
13
13
|
rowNumber: number,
|
|
14
14
|
config: MultiDashboardConfig,
|
|
@@ -100,7 +100,7 @@ export const getVizConfig = (
|
|
|
100
100
|
const dataKey = visualizationConfig.dataKey || 'backwards-compatibility'
|
|
101
101
|
// Markup-includes need data even when shared filters exist (for markup variables)
|
|
102
102
|
const shouldClearData = sharedFilterColumns.length && visualizationConfig.type !== 'markup-include'
|
|
103
|
-
visualizationConfig.data = data[dataKey] || []
|
|
103
|
+
visualizationConfig.data = shouldClearData ? [] : data[dataKey] || []
|
|
104
104
|
if (visualizationConfig.formattedData) {
|
|
105
105
|
visualizationConfig.formattedData =
|
|
106
106
|
transform.developerStandardize(visualizationConfig.data, visualizationConfig.dataDescription) ||
|
|
@@ -1,99 +1,109 @@
|
|
|
1
|
-
import _ from 'lodash'
|
|
2
|
-
import { APIFilterDropdowns } from '../components/DashboardFilters'
|
|
3
|
-
import { SharedFilter } from '../types/SharedFilter'
|
|
4
|
-
import * as apiFilterHelpers from './apiFilterHelpers'
|
|
5
|
-
import { APIFilter } from '../types/APIFilter'
|
|
6
|
-
import { getParentParams, notAllParentsSelected } from './apiFilterHelpers'
|
|
7
|
-
|
|
8
|
-
export const loadAPIFiltersFactory = (
|
|
9
|
-
dispatch: Function,
|
|
10
|
-
dispatchErrorMessages: Function,
|
|
11
|
-
setAPIFilterDropdowns: Function,
|
|
12
|
-
autoLoadFilterIndexes: number[]
|
|
13
|
-
) => {
|
|
14
|
-
const loadAPIFilters = (
|
|
15
|
-
sharedFilters: SharedFilter[],
|
|
16
|
-
dropdowns: APIFilterDropdowns,
|
|
17
|
-
loadAll?: boolean,
|
|
18
|
-
recursiveLimit = 50
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
return
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
1
|
+
import _ from 'lodash'
|
|
2
|
+
import { APIFilterDropdowns } from '../components/DashboardFilters'
|
|
3
|
+
import { SharedFilter } from '../types/SharedFilter'
|
|
4
|
+
import * as apiFilterHelpers from './apiFilterHelpers'
|
|
5
|
+
import { APIFilter } from '../types/APIFilter'
|
|
6
|
+
import { getParentParams, notAllParentsSelected } from './apiFilterHelpers'
|
|
7
|
+
|
|
8
|
+
export const loadAPIFiltersFactory = (
|
|
9
|
+
dispatch: Function,
|
|
10
|
+
dispatchErrorMessages: Function,
|
|
11
|
+
setAPIFilterDropdowns: Function,
|
|
12
|
+
autoLoadFilterIndexes: number[]
|
|
13
|
+
) => {
|
|
14
|
+
const loadAPIFilters = (
|
|
15
|
+
sharedFilters: SharedFilter[],
|
|
16
|
+
dropdowns: APIFilterDropdowns,
|
|
17
|
+
loadAll?: boolean,
|
|
18
|
+
recursiveLimit = 50,
|
|
19
|
+
isStale?: () => boolean
|
|
20
|
+
): Promise<SharedFilter[]> => {
|
|
21
|
+
if (!sharedFilters) return
|
|
22
|
+
const allIndexes = sharedFilters.map((_, index) => index)
|
|
23
|
+
const _autoLoadFilterIndexes = loadAll ? allIndexes : autoLoadFilterIndexes
|
|
24
|
+
sharedFilters = sharedFilters.map((filter, index) => {
|
|
25
|
+
// For data filters (no API endpoint), return unchanged to preserve user selections
|
|
26
|
+
if (!filter.apiFilter?.apiEndpoint) {
|
|
27
|
+
return filter
|
|
28
|
+
}
|
|
29
|
+
return apiFilterHelpers.setAutoLoadDefaultValue(
|
|
30
|
+
index,
|
|
31
|
+
dropdowns[filter.apiFilter.apiEndpoint],
|
|
32
|
+
sharedFilters,
|
|
33
|
+
_autoLoadFilterIndexes
|
|
34
|
+
)
|
|
35
|
+
})
|
|
36
|
+
const sharedAPIFilters = sharedFilters.filter(f => f.apiFilter)
|
|
37
|
+
const filterLookup = new Map(sharedAPIFilters.map(filter => [filter.apiFilter.apiEndpoint, filter.apiFilter]))
|
|
38
|
+
const toFetch = apiFilterHelpers.getToFetch(sharedFilters, dropdowns)
|
|
39
|
+
const loadingDropdowns = Object.values(toFetch).reduce(
|
|
40
|
+
(acc, [dropdownsKey]) => ({ ...acc, [dropdownsKey]: null }),
|
|
41
|
+
{}
|
|
42
|
+
)
|
|
43
|
+
setAPIFilterDropdowns(currentState => {
|
|
44
|
+
return { ...currentState, ...loadingDropdowns }
|
|
45
|
+
})
|
|
46
|
+
const newDropdowns = _.cloneDeep(dropdowns)
|
|
47
|
+
return Promise.all(
|
|
48
|
+
Object.keys(toFetch).map(
|
|
49
|
+
endpoint =>
|
|
50
|
+
new Promise<{ error: boolean }>(resolve => {
|
|
51
|
+
fetch(endpoint)
|
|
52
|
+
.then(resp => resp.json())
|
|
53
|
+
.then(data => {
|
|
54
|
+
if (!Array.isArray(data)) {
|
|
55
|
+
console.error('COVE only supports response data in the shape Array<Object>')
|
|
56
|
+
}
|
|
57
|
+
const [_key, index] = toFetch[endpoint]
|
|
58
|
+
const apiFilter = filterLookup.get(_key) as APIFilter
|
|
59
|
+
const _filterValues = apiFilterHelpers.getFilterValues(data, apiFilter)
|
|
60
|
+
|
|
61
|
+
newDropdowns[_key] = _filterValues
|
|
62
|
+
const newDefaultSelectedFilter = apiFilterHelpers.setAutoLoadDefaultValue(
|
|
63
|
+
index,
|
|
64
|
+
_filterValues,
|
|
65
|
+
sharedFilters,
|
|
66
|
+
_autoLoadFilterIndexes
|
|
67
|
+
)
|
|
68
|
+
sharedFilters[index] = newDefaultSelectedFilter
|
|
69
|
+
})
|
|
70
|
+
.catch(() => {
|
|
71
|
+
dispatchErrorMessages({
|
|
72
|
+
type: 'ADD_ERROR_MESSAGE',
|
|
73
|
+
payload: 'There was a problem returning data. Please try again.'
|
|
74
|
+
})
|
|
75
|
+
resolve({ error: true })
|
|
76
|
+
})
|
|
77
|
+
.finally(() => {
|
|
78
|
+
resolve({ error: false })
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
)
|
|
82
|
+
).then(responses => {
|
|
83
|
+
const hasError = responses.some(({ error }) => error)
|
|
84
|
+
const toLoad = sharedFilters.reduce((acc, curr, index) => {
|
|
85
|
+
// the filter is autoloading and it hasn't finished yet
|
|
86
|
+
if (_autoLoadFilterIndexes.includes(index) && !curr.active) {
|
|
87
|
+
if (notAllParentsSelected(getParentParams(curr, sharedFilters))) {
|
|
88
|
+
return acc
|
|
89
|
+
}
|
|
90
|
+
return [...acc, index]
|
|
91
|
+
}
|
|
92
|
+
return acc
|
|
93
|
+
}, [])
|
|
94
|
+
if (hasError || !toLoad.length || recursiveLimit === 0) {
|
|
95
|
+
// Check if this operation is stale before dispatching
|
|
96
|
+
if (isStale && isStale()) {
|
|
97
|
+
// Operation is stale (filters were cleared), skip dispatch
|
|
98
|
+
return sharedFilters
|
|
99
|
+
}
|
|
100
|
+
setAPIFilterDropdowns(newDropdowns)
|
|
101
|
+
dispatch({ type: 'SET_SHARED_FILTERS', payload: sharedFilters })
|
|
102
|
+
return sharedFilters
|
|
103
|
+
} else {
|
|
104
|
+
return loadAPIFilters(sharedFilters, newDropdowns, loadAll, recursiveLimit - 1, isStale)
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
return loadAPIFilters
|
|
109
|
+
}
|