@cdc/dashboard 4.26.3 → 4.26.5

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 (151) hide show
  1. package/CONFIG.md +219 -0
  2. package/README.md +60 -20
  3. package/dist/cdcdashboard-CY9IcPSi.es.js +6 -0
  4. package/dist/cdcdashboard-DlpiY3fQ.es.js +4 -0
  5. package/dist/cdcdashboard.js +61559 -58048
  6. package/examples/__data__/data-2.json +6 -0
  7. package/examples/__data__/data.json +6 -0
  8. package/examples/dashboard-conditions-filters-incomplete.json +221 -0
  9. package/examples/dashboard-missing-datasets-multi.json +174 -0
  10. package/examples/dashboard-missing-datasets-single.json +121 -0
  11. package/examples/dashboard-multi-dashboard-version-regression.json +146 -0
  12. package/examples/dashboard-shared-filter-row-delete-cleanup.json +186 -0
  13. package/examples/dashboard-stale-dataset-keys.json +181 -0
  14. package/examples/dashboard-tiered-filter-regression.json +190 -0
  15. package/examples/legend-issue.json +1 -1
  16. package/examples/minimal-example.json +34 -0
  17. package/examples/private/cfa-dashboard.json +651 -0
  18. package/examples/private/data-bite-wrap.json +6936 -0
  19. package/examples/private/dengue.json +4640 -0
  20. package/examples/private/link_to_file.json +16662 -0
  21. package/examples/private/multi-dash-fix.json +16963 -0
  22. package/examples/private/versions.json +41612 -0
  23. package/examples/sankey.json +3 -3
  24. package/examples/test-api-filter-reset.json +4 -4
  25. package/examples/tp5-test.json +86 -4
  26. package/examples/us-map-filter-example.json +1074 -0
  27. package/package.json +9 -9
  28. package/src/CdcDashboard.tsx +6 -2
  29. package/src/CdcDashboardComponent.tsx +179 -88
  30. package/src/DashboardCopyPasteContext.test.tsx +33 -0
  31. package/src/DashboardCopyPasteContext.tsx +48 -0
  32. package/src/_stories/Dashboard.EditorRegression.stories.tsx +72 -0
  33. package/src/_stories/Dashboard.Regression.stories.tsx +196 -0
  34. package/src/_stories/Dashboard.Zoom.stories.tsx +88 -0
  35. package/src/_stories/Dashboard.smoke.stories.tsx +33 -0
  36. package/src/_stories/Dashboard.stories.tsx +337 -2
  37. package/src/_stories/FilteredTextMigrationComparison.stories.tsx +87 -0
  38. package/src/_stories/_mock/dashboard-data-driven-colors.json +171 -0
  39. package/src/_stories/_mock/tp5-test.json +86 -5
  40. package/src/components/Column.test.tsx +176 -0
  41. package/src/components/Column.tsx +214 -13
  42. package/src/components/DashboardConditionModal.test.tsx +420 -0
  43. package/src/components/DashboardConditionModal.tsx +367 -0
  44. package/src/components/DashboardConditionSummary.tsx +59 -0
  45. package/src/components/DashboardEditors.tsx +23 -0
  46. package/src/components/DashboardFilters/DashboardFilters.test.tsx +267 -0
  47. package/src/components/DashboardFilters/DashboardFilters.tsx +193 -172
  48. package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.test.tsx +164 -0
  49. package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +46 -6
  50. package/src/components/DashboardFilters/DashboardFiltersEditor/components/APIModal.tsx +5 -3
  51. package/src/components/DashboardFilters/DashboardFiltersEditor/components/DeleteFilterModal.tsx +59 -58
  52. package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.test.tsx +304 -0
  53. package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +43 -36
  54. package/src/components/DashboardFilters/DashboardFiltersEditor/components/NestedDropDownDashboard.tsx +2 -2
  55. package/src/components/DashboardFilters/DashboardFiltersWrapper.test.tsx +142 -0
  56. package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +32 -27
  57. package/src/components/DashboardFilters/dashboardfilter.styles.css +42 -27
  58. package/src/components/DataDesignerModal.tsx +2 -1
  59. package/src/components/ExpandCollapseButtons.tsx +6 -4
  60. package/src/components/Grid.tsx +12 -7
  61. package/src/components/Header/Header.tsx +36 -17
  62. package/src/components/MultiConfigTabs/MultiConfigTabs.tsx +141 -140
  63. package/src/components/Row.test.tsx +228 -0
  64. package/src/components/Row.tsx +104 -28
  65. package/src/components/VisualizationRow.test.tsx +396 -0
  66. package/src/components/VisualizationRow.tsx +177 -51
  67. package/src/components/VisualizationsPanel/VisualizationsPanel.test.tsx +49 -0
  68. package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +14 -13
  69. package/src/components/Widget/Widget.test.tsx +218 -0
  70. package/src/components/Widget/Widget.tsx +123 -20
  71. package/src/components/Widget/widget.styles.css +58 -14
  72. package/src/components/dashboard-condition-modal.css +76 -0
  73. package/src/components/dashboard-condition-summary.css +87 -0
  74. package/src/data/initial-state.js +1 -0
  75. package/src/helpers/addValuesToDashboardFilters.ts +3 -5
  76. package/src/helpers/addVisualization.ts +17 -4
  77. package/src/helpers/cloneDashboardWidget.ts +127 -0
  78. package/src/helpers/dashboardColumnWidgets.ts +99 -0
  79. package/src/helpers/dashboardConditionUi.ts +47 -0
  80. package/src/helpers/dashboardConditions.ts +200 -0
  81. package/src/helpers/dashboardFilterTargets.ts +156 -0
  82. package/src/helpers/filterData.ts +4 -9
  83. package/src/helpers/filterVisibility.ts +20 -0
  84. package/src/helpers/formatConfigBeforeSave.ts +2 -2
  85. package/src/helpers/getFilteredData.ts +18 -5
  86. package/src/helpers/getUpdateConfig.ts +43 -12
  87. package/src/helpers/getVizRowColumnLocator.ts +11 -1
  88. package/src/helpers/iconHash.tsx +9 -3
  89. package/src/helpers/mapDataToConfig.ts +31 -29
  90. package/src/helpers/reloadURLHelpers.ts +25 -5
  91. package/src/helpers/removeDashboardFilter.ts +33 -33
  92. package/src/helpers/tests/addVisualization.test.ts +53 -9
  93. package/src/helpers/tests/cloneDashboardWidget.test.ts +136 -0
  94. package/src/helpers/tests/dashboardColumnWidgets.test.ts +99 -0
  95. package/src/helpers/tests/dashboardConditionUi.test.ts +41 -0
  96. package/src/helpers/tests/dashboardConditions.test.ts +428 -0
  97. package/src/helpers/tests/formatConfigBeforeSave.test.ts +51 -0
  98. package/src/helpers/tests/getFilteredData.test.ts +265 -86
  99. package/src/helpers/tests/getUpdateConfig.test.ts +338 -0
  100. package/src/helpers/tests/reloadURLHelpers.test.ts +394 -238
  101. package/src/index.tsx +6 -3
  102. package/src/scss/grid.scss +281 -22
  103. package/src/scss/main.scss +215 -64
  104. package/src/store/dashboard.actions.ts +17 -4
  105. package/src/store/dashboard.reducer.test.ts +538 -0
  106. package/src/store/dashboard.reducer.ts +136 -22
  107. package/src/test/CdcDashboard.test.jsx +24 -0
  108. package/src/test/CdcDashboard.test.tsx +148 -0
  109. package/src/test/CdcDashboardComponent.test.tsx +935 -2
  110. package/src/types/ConfigRow.ts +15 -0
  111. package/src/types/DashboardFilters.ts +4 -0
  112. package/src/types/SharedFilter.ts +2 -0
  113. package/tests/fixtures/dashboard-config-with-metadata.json +1 -1
  114. package/dist/cdcdashboard-vr9HZwRt.es.js +0 -6
  115. package/examples/DEV-6574.json +0 -2224
  116. package/examples/api-dashboard-data.json +0 -272
  117. package/examples/api-dashboard-years.json +0 -11
  118. package/examples/api-geographies-data.json +0 -11
  119. package/examples/chart-data.json +0 -5409
  120. package/examples/custom/css/respiratory.css +0 -236
  121. package/examples/custom/js/respiratory.js +0 -242
  122. package/examples/default-data.json +0 -368
  123. package/examples/default-filter-control.json +0 -209
  124. package/examples/default-multi-dataset-shared-filter.json +0 -1729
  125. package/examples/default-multi-dataset.json +0 -506
  126. package/examples/ed-visits-county-file.json +0 -402
  127. package/examples/filters/Alabama.json +0 -72
  128. package/examples/filters/Alaska.json +0 -1737
  129. package/examples/filters/Arkansas.json +0 -4713
  130. package/examples/filters/California.json +0 -212
  131. package/examples/filters/Colorado.json +0 -1500
  132. package/examples/filters/Connecticut.json +0 -559
  133. package/examples/filters/Delaware.json +0 -63
  134. package/examples/filters/DistrictofColumbia.json +0 -63
  135. package/examples/filters/Florida.json +0 -4217
  136. package/examples/filters/States.json +0 -146
  137. package/examples/state-level.json +0 -90136
  138. package/examples/state-points.json +0 -10474
  139. package/examples/temp-example-data.json +0 -130
  140. package/examples/test-dashboard-simple.json +0 -503
  141. package/examples/test-example.json +0 -752
  142. package/examples/test-file.json +0 -147
  143. package/examples/test.json +0 -752
  144. package/examples/testing.json +0 -94456
  145. /package/examples/{data → __data__}/data-with-metadata.json +0 -0
  146. /package/examples/{legend-issue-data.json → __data__/legend-issue-data.json} +0 -0
  147. /package/examples/api-test/{categories.json → __data__/categories.json} +0 -0
  148. /package/examples/api-test/{chart-data.json → __data__/chart-data.json} +0 -0
  149. /package/examples/api-test/{topics.json → __data__/topics.json} +0 -0
  150. /package/examples/api-test/{years.json → __data__/years.json} +0 -0
  151. /package/src/_stories/{Dashboard.Pages.stories.tsx → Dashboard.Pages.smoke.stories.tsx} +0 -0
@@ -0,0 +1,156 @@
1
+ import { ConfigRow, DashboardCondition } from '../types/ConfigRow'
2
+ import { DashboardConfig } from '../types/DashboardConfig'
3
+ import { Dashboard } from '../types/Dashboard'
4
+ import { SharedFilter } from '../types/SharedFilter'
5
+ import { getConditionalWidgets, hasConditionalWidgets } from './dashboardColumnWidgets'
6
+ import { getVizRowColumnLocator } from './getVizRowColumnLocator'
7
+
8
+ export type SharedFilterTarget = string | number
9
+
10
+ export type SharedFilterTargetOptions = {
11
+ nameLookup: Record<string, string>
12
+ options: SharedFilterTarget[]
13
+ }
14
+
15
+ export type DashboardConditionTarget = {
16
+ id: string
17
+ dashboardCondition: DashboardCondition
18
+ rowIndex: number
19
+ filterTarget: SharedFilterTarget
20
+ columnIndex?: number
21
+ entryIndex?: number
22
+ }
23
+
24
+ export const dashboardConditionsSupportedForRow = (row?: ConfigRow) =>
25
+ !!row && !row.toggle && !row.multiVizColumn && !row.originalMultiVizColumn
26
+
27
+ const normalizeTarget = (target: SharedFilterTarget) => `${target}`
28
+
29
+ const dedupeSharedFilterTargets = (targets: SharedFilterTarget[]) =>
30
+ targets.filter(
31
+ (value, index, values) => values.findIndex(entry => normalizeTarget(entry) === normalizeTarget(value)) === index
32
+ )
33
+
34
+ export const matchesSharedFilterTarget = (usedBy: SharedFilter['usedBy'], target: SharedFilterTarget) =>
35
+ !!usedBy?.some(entry => normalizeTarget(entry) === normalizeTarget(target))
36
+
37
+ export const getDashboardConditionTargets = (rows: ConfigRow[]): DashboardConditionTarget[] =>
38
+ rows.reduce((targets, row, rowIndex) => {
39
+ if (!dashboardConditionsSupportedForRow(row)) return targets
40
+
41
+ if (row.dashboardCondition?.id) {
42
+ targets.push({
43
+ id: row.dashboardCondition.id,
44
+ dashboardCondition: row.dashboardCondition,
45
+ rowIndex,
46
+ filterTarget: rowIndex
47
+ })
48
+ }
49
+
50
+ row.columns?.forEach((column, columnIndex) => {
51
+ if (hasConditionalWidgets(column)) {
52
+ getConditionalWidgets(column).forEach((entry, entryIndex) => {
53
+ if (!entry.dashboardCondition?.id) return
54
+ targets.push({
55
+ id: entry.dashboardCondition.id,
56
+ dashboardCondition: entry.dashboardCondition,
57
+ rowIndex,
58
+ filterTarget: row.dataKey ? rowIndex : entry.widget,
59
+ columnIndex,
60
+ entryIndex
61
+ })
62
+ })
63
+ return
64
+ }
65
+ })
66
+
67
+ return targets
68
+ }, [] as DashboardConditionTarget[])
69
+
70
+ export const getSharedFilterTargetOptions = (
71
+ config: DashboardConfig,
72
+ filter: Pick<SharedFilter, 'usedBy' | 'setBy'>
73
+ ): SharedFilterTargetOptions => {
74
+ const nameLookup: Record<string, string> = {}
75
+ const options: SharedFilterTarget[] = []
76
+ const vizRowColumnLocator = getVizRowColumnLocator(config.rows)
77
+
78
+ Object.keys(config.visualizations).forEach(vizKey => {
79
+ const vizLookup = vizRowColumnLocator[vizKey]
80
+ if (!vizLookup) return
81
+
82
+ const viz = config.visualizations[vizKey] as any
83
+ if (viz.type === 'dashboardFilters') return
84
+
85
+ const vizName =
86
+ viz.visualizationType === 'markup-include'
87
+ ? viz.contentEditor?.title || vizKey
88
+ : viz.general?.title || viz.title || vizKey
89
+ const dataConfiguredOnRow = config.rows[vizLookup.row]?.dataKey
90
+
91
+ nameLookup[vizKey] = vizName
92
+ if (filter.setBy !== vizKey && !viz.usesSharedFilter && !dataConfiguredOnRow) {
93
+ options.push(vizKey)
94
+ }
95
+ })
96
+
97
+ config.rows.forEach((row, rowIndex) => {
98
+ if (row.dataKey || (dashboardConditionsSupportedForRow(row) && row.dashboardCondition)) {
99
+ nameLookup[`${rowIndex}`] = `Row ${rowIndex + 1}`
100
+ options.push(rowIndex)
101
+ }
102
+ })
103
+
104
+ // Preserve legacy row targets that are no longer offered as new selectable targets.
105
+ filter.usedBy?.forEach(target => {
106
+ const isNumericRowTarget = typeof target === 'number' || (typeof target === 'string' && /^-?\d+$/.test(target))
107
+ if (isNumericRowTarget) {
108
+ const rowIndex = Number(target)
109
+ if (!config.rows[rowIndex]) return
110
+ nameLookup[`${target}`] = `Row ${rowIndex + 1}`
111
+ options.push(target)
112
+ }
113
+ })
114
+
115
+ return {
116
+ nameLookup,
117
+ options: dedupeSharedFilterTargets(options)
118
+ }
119
+ }
120
+
121
+ export const remapRowTargetsInSharedFilters = (
122
+ sharedFilters: SharedFilter[],
123
+ remapRowIndex: (rowIndex: number) => number | null
124
+ ): SharedFilter[] =>
125
+ sharedFilters.map(sharedFilter => {
126
+ if (!sharedFilter.usedBy) return sharedFilter
127
+
128
+ const nextUsedBy = dedupeSharedFilterTargets(
129
+ sharedFilter.usedBy.flatMap(target => {
130
+ const isNumericRowTarget = typeof target === 'number' || (typeof target === 'string' && /^-?\d+$/.test(target))
131
+ if (!isNumericRowTarget) return [target]
132
+
133
+ const mappedRowIndex = remapRowIndex(Number(target))
134
+ return mappedRowIndex === null ? [] : [typeof target === 'number' ? mappedRowIndex : `${mappedRowIndex}`]
135
+ })
136
+ )
137
+
138
+ return nextUsedBy === sharedFilter.usedBy ? sharedFilter : { ...sharedFilter, usedBy: nextUsedBy }
139
+ })
140
+
141
+ export const getApplicableFiltersForTarget = (
142
+ dashboard: Dashboard,
143
+ target: SharedFilterTarget,
144
+ options?: { includeUnscoped?: boolean }
145
+ ): SharedFilter[] | false => {
146
+ const applicableFilters =
147
+ dashboard.sharedFilters?.filter(sharedFilter => {
148
+ if (options?.includeUnscoped && (!sharedFilter.usedBy || sharedFilter.usedBy.length === 0)) {
149
+ return true
150
+ }
151
+
152
+ return matchesSharedFilterTarget(sharedFilter.usedBy, target)
153
+ }) || []
154
+
155
+ return applicableFilters.length > 0 ? applicableFilters : false
156
+ }
@@ -108,14 +108,9 @@ export const filterData = (filters: SharedFilter[], _data: Object[]): Object[] =
108
108
  return []
109
109
  }
110
110
 
111
- for (let i = 0; i < maxTier; i++) {
112
- const lastIteration = i === maxTier - 1
113
-
114
- const filteredData = filterDataByTier(_data, filters, i + 1)
115
-
116
- if (lastIteration) {
117
- // not sure if this last run of filterDataByTier() function is necessary.
118
- return filterDataByTier(filteredData, filters, maxTier - 1)
119
- }
111
+ let filteredData = _data
112
+ for (let tier = 1; tier <= maxTier; tier++) {
113
+ filteredData = filterDataByTier(filteredData, filters, tier)
120
114
  }
115
+ return filteredData
121
116
  }
@@ -0,0 +1,20 @@
1
+ import { FILTER_STYLE } from '../types/FilterStyles'
2
+ import { SharedFilter } from '../types/SharedFilter'
3
+
4
+ export const isVisibleDashboardFilter = (filter?: SharedFilter | null): boolean =>
5
+ Boolean(
6
+ filter &&
7
+ (filter.type === 'urlfilter' ||
8
+ filter.showDropdown ||
9
+ filter.filterStyle === FILTER_STYLE.nestedDropdown ||
10
+ filter.filterStyle === FILTER_STYLE.tabSimple)
11
+ )
12
+
13
+ export const hasVisibleDashboardFiltersForIndexes = (
14
+ sharedFilters?: SharedFilter[] | null,
15
+ sharedFilterIndexes?: (number | string)[] | null
16
+ ): boolean =>
17
+ Boolean(
18
+ sharedFilterIndexes?.length &&
19
+ sharedFilterIndexes.map(Number).some(filterIndex => isVisibleDashboardFilter(sharedFilters?.[filterIndex]))
20
+ )
@@ -51,9 +51,9 @@ export const cleanSharedFilters = (config: DashboardConfig) => {
51
51
  if (config.dashboard?.sharedFilters) {
52
52
  const recursiveRemoveFilters = (sharedFilters, visualizations: Record<string, AnyVisualization>) => {
53
53
  const usedFilters = _.uniq(
54
- Object.values(visualizations).reduce((acc, viz) => {
54
+ Object.values(visualizations).reduce((acc: number[], viz) => {
55
55
  if (viz.type === 'dashboardFilters') {
56
- acc = acc.concat(viz.sharedFilterIndexes)
56
+ acc = acc.concat((viz.sharedFilterIndexes ?? []).map(Number))
57
57
  }
58
58
  return acc
59
59
  }, [])
@@ -1,16 +1,14 @@
1
1
  import { DashboardState } from '../store/dashboard.reducer'
2
2
  import { Dashboard } from '../types/Dashboard'
3
3
  import { SharedFilter } from '../types/SharedFilter'
4
+ import { getDashboardConditionFilteredData } from './dashboardConditions'
5
+ import { getApplicableFiltersForTarget, getDashboardConditionTargets } from './dashboardFilterTargets'
4
6
  import { filterData } from './filterData'
5
7
  import { getFormattedData } from './getFormattedData'
6
8
  import { getVizKeys } from './getVizKeys'
7
9
 
8
10
  export const getApplicableFilters = (dashboard: Dashboard, key: string | number): false | SharedFilter[] => {
9
- const c = dashboard.sharedFilters?.filter(
10
- sharedFilter =>
11
- (sharedFilter.usedBy && sharedFilter.usedBy.indexOf(`${key}`) !== -1) || sharedFilter.usedBy?.indexOf(key) !== -1
12
- )
13
- return c?.length > 0 ? c : false
11
+ return getApplicableFiltersForTarget(dashboard, key, { includeUnscoped: true })
14
12
  }
15
13
  export const getFilteredData = (
16
14
  state: DashboardState,
@@ -45,5 +43,20 @@ export const getFilteredData = (
45
43
  }
46
44
  }
47
45
  })
46
+ const dashboardConditionTargets = getDashboardConditionTargets(config.rows)
47
+ dashboardConditionTargets.forEach(conditionTarget => {
48
+ delete newFilteredData[conditionTarget.id]
49
+
50
+ const filteredData = getDashboardConditionFilteredData(
51
+ conditionTarget.dashboardCondition,
52
+ config.dashboard,
53
+ (dataOverride || state.data) as Record<string, any[]>,
54
+ conditionTarget.filterTarget
55
+ )
56
+
57
+ if (filteredData !== undefined) {
58
+ newFilteredData[conditionTarget.id] = filteredData
59
+ }
60
+ })
48
61
  return newFilteredData
49
62
  }
@@ -1,5 +1,7 @@
1
1
  import { DashboardState } from '../store/dashboard.reducer'
2
2
  import { DashboardConfig as Config, DashboardConfig } from '../types/DashboardConfig'
3
+ import { ensureRowConditionIds, getDashboardConditionFilteredData } from './dashboardConditions'
4
+ import { getApplicableFiltersForTarget, getDashboardConditionTargets } from './dashboardFilterTargets'
3
5
  import { filterData } from './filterData'
4
6
  import { generateValuesForFilter } from './generateValuesForFilter'
5
7
  import { getFormattedData } from './getFormattedData'
@@ -16,6 +18,7 @@ export const getUpdateConfig =
16
18
  (state: UpdateState) =>
17
19
  (newConfig, dataOverride?: Object): [Config, Object] => {
18
20
  let newFilteredData = {}
21
+ newConfig.rows = ensureRowConditionIds(newConfig.rows)
19
22
  let visualizationKeys = getVizKeys(newConfig)
20
23
 
21
24
  const vizRowColumnLocator = getVizRowColumnLocator(newConfig.rows)
@@ -55,13 +58,13 @@ export const getUpdateConfig =
55
58
  })
56
59
 
57
60
  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
- )
61
+ const locator = vizRowColumnLocator[visualizationKey]
62
+ if (newConfig.rows[locator?.row]?.dataKey) return // data configured on the row level
63
+ const applicableFilters = getApplicableFiltersForTarget(newConfig.dashboard, visualizationKey, {
64
+ includeUnscoped: true
65
+ })
63
66
 
64
- if (applicableFilters.length > 0) {
67
+ if (applicableFilters) {
65
68
  const visualization = newConfig.visualizations[visualizationKey]
66
69
  const _newConfigDataSet = newConfig.datasets[visualization.dataKey]
67
70
  const formattedData = getFormattedData(
@@ -74,16 +77,44 @@ export const getUpdateConfig =
74
77
  })
75
78
 
76
79
  newConfig.rows.forEach((row, rowIndex) => {
77
- const applicableFilters = newConfig.dashboard.sharedFilters.filter(
78
- sharedFilter => sharedFilter.usedBy && sharedFilter.usedBy.indexOf(rowIndex) !== -1
79
- )
80
+ if (!row.dataKey) return
81
+
82
+ const applicableFilters = getApplicableFiltersForTarget(newConfig.dashboard, rowIndex, {
83
+ includeUnscoped: true
84
+ })
80
85
 
81
- if (applicableFilters.length > 0) {
82
- const formattedData = getFormattedData(row.data, row.dataDescription)
83
- const _data = formattedData || (dataOverride || state.data)[rowIndex]
86
+ if (applicableFilters) {
87
+ const datasetData = newConfig.datasets?.[row.dataKey]?.data
88
+ const formattedData = getFormattedData(row.data || datasetData, row.dataDescription)
89
+ const _data = formattedData || (dataOverride || state.data)[row.dataKey]
84
90
  newFilteredData[rowIndex] = filterData(applicableFilters, _data)
85
91
  }
86
92
  })
93
+
94
+ const conditionDataSource = {
95
+ ...(state.data || {}),
96
+ ...(dataOverride || {}),
97
+ ...Object.keys(newConfig.datasets || {}).reduce((acc, datasetKey) => {
98
+ const datasetData = newConfig.datasets[datasetKey]?.data
99
+ if (datasetData) {
100
+ acc[datasetKey] = datasetData
101
+ }
102
+ return acc
103
+ }, {} as Record<string, any[]>)
104
+ }
105
+
106
+ getDashboardConditionTargets(newConfig.rows).forEach(conditionTarget => {
107
+ const filteredData = getDashboardConditionFilteredData(
108
+ conditionTarget.dashboardCondition,
109
+ newConfig.dashboard,
110
+ conditionDataSource,
111
+ conditionTarget.filterTarget
112
+ )
113
+
114
+ if (filteredData !== undefined) {
115
+ newFilteredData[conditionTarget.id] = filteredData
116
+ }
117
+ })
87
118
  }
88
119
  //Enforce default values that need to be calculated at runtime
89
120
  newConfig.runtime = {}
@@ -1,9 +1,19 @@
1
1
  import { ConfigRow } from '../types/ConfigRow'
2
+ import { getConditionalWidgets, hasConditionalWidgets } from './dashboardColumnWidgets'
2
3
 
3
4
  // returns a dictionary of widget names and their corresponding row and column index
4
- export const getVizRowColumnLocator = (rows: ConfigRow[]): Record<string, { row: number; column: number }> =>
5
+ export const getVizRowColumnLocator = (
6
+ rows: ConfigRow[]
7
+ ): Record<string, { row: number; column: number; entry?: number }> =>
5
8
  rows.reduce((acc, curr, index) => {
6
9
  curr.columns?.forEach((column, columnIndex) => {
10
+ if (hasConditionalWidgets(column)) {
11
+ getConditionalWidgets(column).forEach((entry, entryIndex) => {
12
+ acc[entry.widget] = { row: index, column: columnIndex, entry: entryIndex }
13
+ })
14
+ return
15
+ }
16
+
7
17
  if (column.widget !== undefined) acc[column.widget] = { row: index, column: columnIndex }
8
18
  })
9
19
  return acc
@@ -1,13 +1,18 @@
1
+ import React from 'react'
1
2
  import Icon from '@cdc/core/components/ui/Icon'
2
3
  import { AnyVisualization } from '@cdc/core/types/Visualization'
3
4
 
4
- export const iconHash = {
5
+ const waffleAliases = ['TP5 Waffle', 'Waffle', 'TP5 Gauge', 'Gauge']
6
+ const waffleIcon = <Icon display='grid' base />
7
+
8
+ export const iconHash: Record<string, React.ReactNode> = {
5
9
  'data-bite': <Icon display='databite' base />,
6
10
  Bar: <Icon display='chartBar' base />,
7
11
  'Spark Line': <Icon display='chartLine' />,
8
12
  'Bump Chart': <Icon display='chartLine' />,
9
- 'waffle-chart': <Icon display='grid' base />,
13
+ 'waffle-chart': waffleIcon,
10
14
  'markup-include': <Icon display='code' base />,
15
+ condition: <Icon display='condition' base />,
11
16
  Line: <Icon display='chartLine' base />,
12
17
  Pie: <Icon display='chartPie' base />,
13
18
  us: <Icon display='mapUsa' base />,
@@ -29,7 +34,8 @@ export const iconHash = {
29
34
  'Box Plot': <Icon display='chartBar' base />,
30
35
  'Forest Plot': <Icon display='chartBar' base />,
31
36
  Forecasting: <Icon display='chartLine' base />,
32
- 'Warming Stripes': <Icon display='chartBar' base />
37
+ 'Warming Stripes': <Icon display='chartBar' base />,
38
+ ...Object.fromEntries(waffleAliases.map(alias => [alias, waffleIcon]))
33
39
  }
34
40
 
35
41
  export const getIcon = (visualization: AnyVisualization) => {
@@ -1,29 +1,31 @@
1
- import { getFormattedData } from './getFormattedData'
2
- import { DashboardConfig } from '../types/DashboardConfig'
3
-
4
- const mapDataToVisualizations = (config: DashboardConfig) => {
5
- Object.keys(config.visualizations).forEach((vizKey, i) => {
6
- const viz = config.visualizations[vizKey]
7
- if (viz.dataKey && !viz.data) {
8
- const data = config.datasets[viz.dataKey].data
9
- config.visualizations[vizKey].data = data
10
- config.visualizations[vizKey].formattedData = getFormattedData(data, viz.dataDescription)
11
- }
12
- })
13
- }
14
-
15
- const mapDataToRows = (config: DashboardConfig) => {
16
- config.rows.forEach((row, i) => {
17
- if (row.dataKey && !row.data) {
18
- const data = config.datasets[row.dataKey].data
19
- config.rows[i].data = data
20
- config.rows[i].formattedData = getFormattedData(data, row.dataDescription)
21
- }
22
- })
23
- }
24
-
25
- export const mapDataToConfig = (config: DashboardConfig) => {
26
- mapDataToVisualizations(config)
27
- mapDataToRows(config)
28
- return config
29
- }
1
+ import { getFormattedData } from './getFormattedData'
2
+ import { DashboardConfig } from '../types/DashboardConfig'
3
+
4
+ const mapDataToVisualizations = (config: DashboardConfig) => {
5
+ Object.keys(config.visualizations).forEach(vizKey => {
6
+ const viz = config.visualizations[vizKey]
7
+ if (viz.dataKey && !viz.data) {
8
+ const dataset = config.datasets[viz.dataKey]
9
+ if (!dataset) return
10
+ config.visualizations[vizKey].data = dataset.data
11
+ config.visualizations[vizKey].formattedData = getFormattedData(dataset.data, viz.dataDescription)
12
+ }
13
+ })
14
+ }
15
+
16
+ const mapDataToRows = (config: DashboardConfig) => {
17
+ config.rows.forEach((row, i) => {
18
+ if (row.dataKey && !row.data) {
19
+ const dataset = config.datasets[row.dataKey]
20
+ if (!dataset) return
21
+ config.rows[i].data = dataset.data
22
+ config.rows[i].formattedData = getFormattedData(dataset.data, row.dataDescription)
23
+ }
24
+ })
25
+ }
26
+
27
+ export const mapDataToConfig = (config: DashboardConfig) => {
28
+ mapDataToVisualizations(config)
29
+ mapDataToRows(config)
30
+ return config
31
+ }
@@ -6,6 +6,8 @@ import _ from 'lodash'
6
6
  import { DashboardConfig } from '../types/DashboardConfig'
7
7
  import { ConfigRow } from '../types/ConfigRow'
8
8
  import { getVizRowColumnLocator } from './getVizRowColumnLocator'
9
+ import { getDashboardConditionDatasetKeys } from './dashboardConditions'
10
+ import { getDashboardConditionTargets } from './dashboardFilterTargets'
9
11
 
10
12
  export const isUpdateNeeded = (
11
13
  filters: SharedFilter[],
@@ -28,14 +30,26 @@ export const isUpdateNeeded = (
28
30
  }
29
31
 
30
32
  type GetDatasetKeysParams = Pick<DashboardConfig, 'visualizations' | 'datasets' | 'rows'>
31
- export const getDatasetKeys = ({ visualizations, datasets, rows }: GetDatasetKeysParams): string[] => {
33
+ type GetDatasetKeysOptions = {
34
+ includeDashboardConditionDatasetKeys?: boolean
35
+ }
36
+ export const getDatasetKeys = (
37
+ { visualizations, datasets, rows }: GetDatasetKeysParams,
38
+ { includeDashboardConditionDatasetKeys = true }: GetDatasetKeysOptions = {}
39
+ ): string[] => {
32
40
  const vizDataKeys = Object.values(visualizations).map(viz => viz.dataKey)
33
41
  const rowDataKeys = rows.map(row => row.dataKey)
42
+ const dashboardConditionDataKeys = includeDashboardConditionDatasetKeys ? getDashboardConditionDatasetKeys(rows) : []
34
43
  const footnoteDataKeys = Object.values(visualizations)
35
44
  .map(viz => viz.footnotes?.dataKey)
36
45
  .filter(Boolean)
37
46
  // ensure to only load datasets for the specific dashboard tab.
38
- const datasetsUsedByDashboard = _.uniq([...vizDataKeys, ...rowDataKeys, ...footnoteDataKeys])
47
+ const datasetsUsedByDashboard = _.uniq([
48
+ ...vizDataKeys,
49
+ ...rowDataKeys,
50
+ ...dashboardConditionDataKeys,
51
+ ...footnoteDataKeys
52
+ ])
39
53
  return Object.keys(datasets).filter(datasetKey => datasetsUsedByDashboard.includes(datasetKey))
40
54
  }
41
55
 
@@ -113,12 +127,18 @@ export const filterUsedByDataUrl = (
113
127
  rows: ConfigRow[]
114
128
  ) => {
115
129
  if (!filter.usedBy || !filter.usedBy.length) return true
116
- const vizUsingFilters = filter.usedBy?.map(vizOrRowKey => visualizations[vizOrRowKey] || rows[vizOrRowKey])
130
+ const dashboardConditionTargets = getDashboardConditionTargets(rows)
117
131
 
118
- return vizUsingFilters?.some(viz => {
132
+ return filter.usedBy.some(vizOrRowKey => {
133
+ const viz = visualizations[vizOrRowKey] || rows[vizOrRowKey]
119
134
  const usedByViz = viz?.dataKey === datasetKey
120
135
  // datasetKey might be a key to a dynamic footnotes URL
121
136
  const usedByVizFootnote = viz?.footnotes?.dataKey === datasetKey
122
- return usedByViz || usedByVizFootnote
137
+ const usedByDashboardCondition = dashboardConditionTargets.some(
138
+ conditionTarget =>
139
+ conditionTarget.dashboardCondition.datasetKey === datasetKey &&
140
+ `${conditionTarget.filterTarget}` === `${vizOrRowKey}`
141
+ )
142
+ return usedByViz || usedByVizFootnote || usedByDashboardCondition
123
143
  })
124
144
  }
@@ -1,33 +1,33 @@
1
- import { AnyVisualization } from '@cdc/core/types/Visualization'
2
- import _ from 'lodash'
3
- import { SharedFilter } from '../types/SharedFilter'
4
-
5
- type Viz = Record<string, AnyVisualization>
6
-
7
- export const removeDashboardFilter = (
8
- index,
9
- sharedFilters: SharedFilter[],
10
- visualizations: Viz
11
- ): [SharedFilter[], Viz] => {
12
- const newSharedFilters = _.cloneDeep(sharedFilters)
13
-
14
- newSharedFilters.splice(index, 1)
15
- const shiftDownIndexes = Object.keys(sharedFilters).slice(index + 1)
16
- const newVisualizations: Viz = _.cloneDeep(visualizations)
17
- Object.keys(newVisualizations).forEach(vizKey => {
18
- const viz = newVisualizations[vizKey]
19
- if (viz.type === 'dashboardFilters') {
20
- // shift the indexes down
21
- const sharedFilterIndexes = viz.sharedFilterIndexes
22
- .filter(filterIndex => filterIndex != index)
23
- .map(filterIndex => {
24
- if (shiftDownIndexes.includes(filterIndex.toString())) {
25
- return filterIndex - 1
26
- }
27
- return filterIndex
28
- })
29
- newVisualizations[vizKey].sharedFilterIndexes = sharedFilterIndexes
30
- }
31
- })
32
- return [newSharedFilters, newVisualizations]
33
- }
1
+ import { AnyVisualization } from '@cdc/core/types/Visualization'
2
+ import _ from 'lodash'
3
+ import { SharedFilter } from '../types/SharedFilter'
4
+
5
+ type Viz = Record<string, AnyVisualization>
6
+
7
+ export const removeDashboardFilter = (
8
+ index,
9
+ sharedFilters: SharedFilter[],
10
+ visualizations: Viz
11
+ ): [SharedFilter[], Viz] => {
12
+ const newSharedFilters = _.cloneDeep(sharedFilters)
13
+
14
+ newSharedFilters.splice(index, 1)
15
+ const shiftDownIndexes = Object.keys(sharedFilters).slice(index + 1)
16
+ const newVisualizations: Viz = _.cloneDeep(visualizations)
17
+ Object.keys(newVisualizations).forEach(vizKey => {
18
+ const viz = newVisualizations[vizKey]
19
+ if (viz.type === 'dashboardFilters') {
20
+ // shift the indexes down
21
+ const sharedFilterIndexes = (viz.sharedFilterIndexes ?? [])
22
+ .filter(filterIndex => filterIndex != index)
23
+ .map(filterIndex => {
24
+ if (shiftDownIndexes.includes(filterIndex.toString())) {
25
+ return filterIndex - 1
26
+ }
27
+ return filterIndex
28
+ })
29
+ newVisualizations[vizKey].sharedFilterIndexes = sharedFilterIndexes
30
+ }
31
+ })
32
+ return [newSharedFilters, newVisualizations]
33
+ }