@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.
Files changed (86) hide show
  1. package/Dynamic_Data.md +66 -0
  2. package/dist/{cdcdashboard-fce76882.es.js → cdcdashboard-BnB1QM5d.es.js} +6 -13
  3. package/dist/{cdcdashboard-c55ac1ea.es.js → cdcdashboard-D6CG2-Hb.es.js} +5 -12
  4. package/dist/{cdcdashboard-31a33da1.es.js → cdcdashboard-MXgURbdZ.es.js} +6 -13
  5. package/dist/{cdcdashboard-1a1724a1.es.js → cdcdashboard-dgT_1dIT.es.js} +136 -151
  6. package/dist/cdcdashboard.js +84214 -79641
  7. package/examples/api-dashboard-data.json +272 -0
  8. package/examples/api-dashboard-years.json +11 -0
  9. package/examples/api-geographies-data.json +11 -0
  10. package/examples/api-test/categories.json +18 -0
  11. package/examples/api-test/chart-data.json +602 -0
  12. package/examples/api-test/topics.json +47 -0
  13. package/examples/api-test/years.json +22 -0
  14. package/examples/markup-axis-label.json +4167 -0
  15. package/examples/private/big-dashboard.json +39095 -39077
  16. package/examples/private/cat-y.json +1235 -0
  17. package/examples/private/chronic-dash.json +1584 -0
  18. package/examples/private/clade-2.json +430 -0
  19. package/examples/private/diabetes.json +546 -196
  20. package/examples/private/map-issue.json +2260 -0
  21. package/examples/private/markup-footer/mortality-deaths-footnotes-age.csv +3 -0
  22. package/examples/private/mpinc-state-reports.json +2260 -0
  23. package/examples/private/mpox.json +38128 -0
  24. package/examples/private/nwss/rsv.json +1240 -0
  25. package/examples/private/reset.json +32920 -0
  26. package/examples/private/simple-dash.json +490 -0
  27. package/examples/private/test-dash.json +0 -0
  28. package/examples/private/test123.json +491 -0
  29. package/examples/test-api-filter-reset.json +132 -0
  30. package/examples/test-dashboard-simple.json +503 -0
  31. package/index.html +25 -26
  32. package/package.json +11 -11
  33. package/src/CdcDashboardComponent.tsx +35 -10
  34. package/src/DashboardContext.tsx +3 -1
  35. package/src/_stories/Dashboard.DataSetup.stories.tsx +203 -0
  36. package/src/_stories/Dashboard.stories.tsx +402 -1
  37. package/src/_stories/_mock/custom-order-new-values.json +116 -0
  38. package/src/_stories/_mock/filter-cascade.json +3350 -0
  39. package/src/_stories/_mock/gallery-data-bite-dashboard.json +3500 -0
  40. package/src/_stories/_mock/nested-parent-child-filters.json +392 -0
  41. package/src/_stories/_mock/parent-child-filters.json +233 -0
  42. package/src/components/DashboardFilters/DashboardFilters.tsx +54 -31
  43. package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +118 -50
  44. package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +96 -108
  45. package/src/components/DashboardFilters/DashboardFiltersEditor/components/NestedDropDownDashboard.tsx +196 -59
  46. package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +129 -29
  47. package/src/components/DashboardFilters/_stories/DashboardFilters.stories.tsx +62 -3
  48. package/src/components/DataDesignerModal.tsx +18 -6
  49. package/src/components/Header/Header.tsx +53 -21
  50. package/src/components/Toggle/Toggle.tsx +48 -48
  51. package/src/components/VisualizationRow.tsx +73 -6
  52. package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +2 -3
  53. package/src/components/Widget/Widget.tsx +1 -1
  54. package/src/data/initial-state.js +1 -0
  55. package/src/helpers/addValuesToDashboardFilters.ts +24 -6
  56. package/src/helpers/apiFilterHelpers.ts +26 -2
  57. package/src/helpers/changeFilterActive.ts +67 -65
  58. package/src/helpers/filterData.ts +52 -7
  59. package/src/helpers/filterResetHelpers.ts +102 -0
  60. package/src/helpers/formatConfigBeforeSave.ts +6 -5
  61. package/src/helpers/getUpdateConfig.ts +91 -91
  62. package/src/helpers/getVizConfig.ts +2 -2
  63. package/src/helpers/loadAPIFilters.ts +109 -99
  64. package/src/helpers/tests/filterResetHelpers.test.ts +532 -0
  65. package/src/helpers/tests/updatesChildFilters.test.ts +53 -22
  66. package/src/helpers/updateChildFilters.ts +50 -27
  67. package/src/index.tsx +1 -0
  68. package/src/scss/editor-panel.scss +3 -431
  69. package/src/scss/main.scss +142 -25
  70. package/src/store/errorMessage/errorMessage.reducer.ts +1 -1
  71. package/src/test/CdcDashboard.test.jsx +9 -4
  72. package/src/types/Dashboard.ts +1 -0
  73. package/src/types/DashboardFilters.ts +9 -8
  74. package/src/types/FilterStyles.ts +8 -7
  75. package/src/types/SharedFilter.ts +13 -0
  76. package/LICENSE +0 -201
  77. package/examples/private/DEV-11072.json +0 -7591
  78. package/examples/private/burden_toolkit_mortality_diabetes_attributable_deaths_data.csv +0 -14041
  79. package/examples/private/burden_toolkit_mortality_diabetes_attributable_deaths_per_100000_data.csv +0 -14041
  80. package/examples/private/burden_toolkit_mortality_qaly_data.csv +0 -18721
  81. package/examples/private/burden_toolkit_mortality_yll_data.csv +0 -18721
  82. package/examples/private/pedro.json +0 -1
  83. package/src/helpers/getAutoLoadVisualization.ts +0 -11
  84. package/src/scss/mixins.scss +0 -47
  85. package/src/scss/variables.scss +0 -5
  86. /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
- const findFilterTier = (filters: SharedFilter[], sharedFilter: SharedFilter) => {
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
- function filter(data = [], filters: SharedFilter[], condition) {
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 isFirstOccurrenceOfTier = filter.tier === condition
55
- if (filter.type !== 'urlfilter' && isFirstOccurrenceOfTier && isNotTheSelectedValue) {
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 = filter(_data, filters, i + 1)
114
+ const filteredData = filterDataByTier(_data, filters, i + 1)
70
115
 
71
116
  if (lastIteration) {
72
- // not sure if this last run of filter() function is necessary.
73
- return filter(filteredData, filters, maxTier - 1)
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
- if (config.datasets[datasetKey].dataUrl) {
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
- export const getFootnotesVizConfig = (
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
- ): Promise<SharedFilter[]> => {
20
- if (!sharedFilters) return
21
- const allIndexes = sharedFilters.map((_, index) => index)
22
- const _autoLoadFilterIndexes = loadAll ? allIndexes : autoLoadFilterIndexes
23
- sharedFilters = sharedFilters.map((filter, index) =>
24
- apiFilterHelpers.setAutoLoadDefaultValue(
25
- index,
26
- dropdowns[filter.apiFilter?.apiEndpoint],
27
- sharedFilters,
28
- _autoLoadFilterIndexes
29
- )
30
- )
31
- const sharedAPIFilters = sharedFilters.filter(f => f.apiFilter)
32
- const filterLookup = new Map(sharedAPIFilters.map(filter => [filter.apiFilter.apiEndpoint, filter.apiFilter]))
33
- const toFetch = apiFilterHelpers.getToFetch(sharedFilters, dropdowns)
34
- const loadingDropdowns = Object.values(toFetch).reduce(
35
- (acc, [dropdownsKey]) => ({ ...acc, [dropdownsKey]: null }),
36
- {}
37
- )
38
- setAPIFilterDropdowns(currentState => {
39
- return { ...currentState, ...loadingDropdowns }
40
- })
41
- const newDropdowns = _.cloneDeep(dropdowns)
42
- return Promise.all(
43
- Object.keys(toFetch).map(
44
- endpoint =>
45
- new Promise<{ error: boolean }>(resolve => {
46
- fetch(endpoint)
47
- .then(resp => resp.json())
48
- .then(data => {
49
- if (!Array.isArray(data)) {
50
- console.error('COVE only supports response data in the shape Array<Object>')
51
- }
52
- const [_key, index] = toFetch[endpoint]
53
- const apiFilter = filterLookup.get(_key) as APIFilter
54
- const _filterValues = apiFilterHelpers.getFilterValues(data, apiFilter)
55
-
56
- newDropdowns[_key] = _filterValues
57
- const newDefaultSelectedFilter = apiFilterHelpers.setAutoLoadDefaultValue(
58
- index,
59
- _filterValues,
60
- sharedFilters,
61
- _autoLoadFilterIndexes
62
- )
63
- sharedFilters[index] = newDefaultSelectedFilter
64
- })
65
- .catch(() => {
66
- dispatchErrorMessages({
67
- type: 'ADD_ERROR_MESSAGE',
68
- payload: 'There was a problem returning data. Please try again.'
69
- })
70
- resolve({ error: true })
71
- })
72
- .finally(() => {
73
- resolve({ error: false })
74
- })
75
- })
76
- )
77
- ).then(responses => {
78
- const hasError = responses.some(({ error }) => error)
79
- const toLoad = sharedFilters.reduce((acc, curr, index) => {
80
- // the filter is autoloading and it hasn't finished yet
81
- if (_autoLoadFilterIndexes.includes(index) && !curr.active) {
82
- if (notAllParentsSelected(getParentParams(curr, sharedFilters))) {
83
- return acc
84
- }
85
- return [...acc, index]
86
- }
87
- return acc
88
- }, [])
89
- if (hasError || !toLoad.length || recursiveLimit === 0) {
90
- setAPIFilterDropdowns(newDropdowns)
91
- dispatch({ type: 'SET_SHARED_FILTERS', payload: sharedFilters })
92
- return sharedFilters
93
- } else {
94
- return loadAPIFilters(sharedFilters, newDropdowns, loadAll, recursiveLimit - 1)
95
- }
96
- })
97
- }
98
- return loadAPIFilters
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
+ }