@cdc/dashboard 4.26.1 → 4.26.3

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 (76) hide show
  1. package/LICENSE +201 -0
  2. package/dist/cdcdashboard-8NmHlKRI.es.js +15 -0
  3. package/dist/cdcdashboard-BPoPzKPz.es.js +6 -0
  4. package/dist/{cdcdashboard-dgT_1dIT.es.js → cdcdashboard-DQ00cQCm.es.js} +1 -20
  5. package/dist/cdcdashboard-jiQQPkty.es.js +6 -0
  6. package/dist/cdcdashboard-vr9HZwRt.es.js +6 -0
  7. package/dist/cdcdashboard.js +80971 -83096
  8. package/examples/custom/css/respiratory.css +1 -1
  9. package/examples/data/data-with-metadata.json +18 -0
  10. package/examples/default.json +492 -132
  11. package/examples/nested-dropdown.json +6985 -0
  12. package/examples/private/abc.json +467 -0
  13. package/examples/private/dash.json +12696 -0
  14. package/examples/private/inline-markup.json +775 -0
  15. package/examples/private/npcr.json +1 -0
  16. package/examples/private/recent-update.json +1456 -0
  17. package/examples/private/test.json +125407 -0
  18. package/examples/private/timeline-data.json +4994 -0
  19. package/examples/private/timeline.json +1708 -0
  20. package/examples/private/toggle.json +10137 -0
  21. package/examples/test-api-filter-reset.json +8 -4
  22. package/examples/tp5-gauges.json +196 -0
  23. package/examples/tp5-test.json +266 -0
  24. package/index.html +1 -29
  25. package/package.json +38 -40
  26. package/src/CdcDashboard.tsx +2 -1
  27. package/src/CdcDashboardComponent.tsx +47 -30
  28. package/src/_stories/Dashboard.DataSetup.stories.tsx +8 -2
  29. package/src/_stories/Dashboard.Pages.stories.tsx +22 -0
  30. package/src/_stories/Dashboard.stories.tsx +4501 -80
  31. package/src/_stories/_mock/dashboard-line-chart-angles.json +1030 -0
  32. package/src/_stories/_mock/tab-simple-filter.json +153 -0
  33. package/src/_stories/_mock/tp5-test.json +267 -0
  34. package/src/components/DashboardFilters/DashboardFilters.tsx +19 -3
  35. package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +10 -4
  36. package/src/components/DashboardFilters/DashboardFiltersEditor/components/APIModal.tsx +1 -1
  37. package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +6 -3
  38. package/src/components/DashboardFilters/DashboardFiltersEditor/components/NestedDropDownDashboard.tsx +13 -8
  39. package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +8 -8
  40. package/src/components/DashboardFilters/_stories/DashboardFilters.stories.tsx +1 -1
  41. package/src/components/DashboardFilters/dashboardfilter.styles.css +3 -3
  42. package/src/components/DataDesignerModal.tsx +2 -2
  43. package/src/components/Header/Header.tsx +27 -5
  44. package/src/components/Header/index.scss +1 -1
  45. package/src/components/MultiConfigTabs/multiconfigtabs.styles.css +6 -6
  46. package/src/components/Row.tsx +21 -0
  47. package/src/components/Toggle/toggle-style.css +7 -7
  48. package/src/components/VisualizationRow.tsx +42 -29
  49. package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +1 -71
  50. package/src/components/VisualizationsPanel/visualizations-panel-styles.css +2 -2
  51. package/src/components/Widget/Widget.tsx +2 -2
  52. package/src/components/Widget/widget.styles.css +12 -12
  53. package/src/data/initial-state.js +1 -1
  54. package/src/helpers/addValuesToDashboardFilters.ts +17 -11
  55. package/src/helpers/addVisualization.ts +71 -0
  56. package/src/helpers/apiFilterHelpers.ts +28 -32
  57. package/src/helpers/formatConfigBeforeSave.ts +1 -1
  58. package/src/helpers/getVizConfig.ts +13 -3
  59. package/src/helpers/iconHash.tsx +45 -36
  60. package/src/helpers/processDataLegacy.ts +19 -14
  61. package/src/helpers/tests/addValuesToDashboardFilters.test.ts +141 -44
  62. package/src/helpers/tests/addVisualization.test.ts +52 -0
  63. package/src/helpers/tests/apiFilterHelpers.test.ts +523 -420
  64. package/src/helpers/tests/formatConfigBeforeSave.test.ts +81 -1
  65. package/src/scss/editor-panel.scss +1 -1
  66. package/src/scss/main.scss +169 -41
  67. package/src/store/dashboard.reducer.ts +1 -1
  68. package/src/test/CdcDashboard.test.jsx +2 -2
  69. package/src/test/CdcDashboardComponent.test.tsx +74 -0
  70. package/src/types/FilterStyles.ts +2 -1
  71. package/tests/fixtures/dashboard-config-with-metadata.json +89 -0
  72. package/vite.config.js +7 -1
  73. package/dist/cdcdashboard-BnB1QM5d.es.js +0 -361528
  74. package/dist/cdcdashboard-Ct2SB0vL.es.js +0 -231049
  75. package/dist/cdcdashboard-D6CG2-Hb.es.js +0 -39377
  76. package/dist/cdcdashboard-MXgURbdZ.es.js +0 -39194
@@ -0,0 +1,71 @@
1
+ import type { AnyVisualization } from '@cdc/core/types/Visualization'
2
+ import type { Table } from '@cdc/core/types/Table'
3
+
4
+ export const addVisualization = (type, subType) => {
5
+ const modalWillOpen = type !== 'markup-include'
6
+ const newVisualizationConfig: Partial<AnyVisualization> = {
7
+ filters: [],
8
+ filterBehavior: 'Filter Change',
9
+ newViz: type !== 'table',
10
+ openModal: modalWillOpen,
11
+ uid: type + Date.now(),
12
+ type
13
+ }
14
+
15
+ switch (type) {
16
+ case 'chart':
17
+ newVisualizationConfig.visual = {
18
+ border: false,
19
+ borderColorTheme: false,
20
+ accent: false,
21
+ background: false,
22
+ hideBackgroundColor: false
23
+ }
24
+ newVisualizationConfig.visualizationType = subType
25
+ break
26
+ case 'map':
27
+ newVisualizationConfig.general = {}
28
+ newVisualizationConfig.general.geoType = subType
29
+ newVisualizationConfig.visual = {
30
+ border: false,
31
+ borderColorTheme: false,
32
+ accent: false,
33
+ background: false,
34
+ hideBackgroundColor: false
35
+ }
36
+ break
37
+ case 'data-bite':
38
+ case 'waffle-chart':
39
+ case 'filtered-text':
40
+ newVisualizationConfig.visualizationType = type
41
+ break
42
+ case 'table': {
43
+ const tableConfig: Table = {
44
+ label: 'Data Table',
45
+ show: true,
46
+ showDownloadUrl: false,
47
+ showVertical: true,
48
+ expanded: true,
49
+ collapsible: true
50
+ }
51
+ newVisualizationConfig.table = tableConfig
52
+ newVisualizationConfig.columns = {}
53
+ newVisualizationConfig.dataFormat = {}
54
+ newVisualizationConfig.visualizationType = type
55
+ break
56
+ }
57
+ case 'markup-include':
58
+ newVisualizationConfig.visualizationType = type
59
+ break
60
+ case 'dashboardFilters': {
61
+ newVisualizationConfig.sharedFilterIndexes = []
62
+ newVisualizationConfig.visualizationType = type
63
+ break
64
+ }
65
+ default:
66
+ newVisualizationConfig.visualizationType = type
67
+ break
68
+ }
69
+
70
+ return newVisualizationConfig
71
+ }
@@ -119,35 +119,31 @@ export const getToFetch = (
119
119
  }
120
120
 
121
121
  export const setActiveNestedDropdown = (dropdownOptions, sharedFilter) => {
122
- const defaultQueryParamValue = getQueryParam(sharedFilter?.setByQueryParameter)
123
- const defaultValue = dropdownOptions[0]?.value
124
- const subDefaultValue = dropdownOptions[0]?.subOptions[0].value
125
- const subDefaultQueryParamValue = getQueryParam(sharedFilter?.subGrouping.setByQueryParameter)
126
- if (!sharedFilter.active) {
127
- sharedFilter.active = defaultQueryParamValue || defaultValue
128
- sharedFilter.subGrouping.active = subDefaultQueryParamValue || subDefaultValue
129
- } else {
130
- const currentOption = dropdownOptions.find(option => option.value === sharedFilter.active)
131
- sharedFilter.active = currentOption ? currentOption.value : defaultValue
132
- if (currentOption) {
133
- const currentSubOption = currentOption.subOptions.find(option => option.value === sharedFilter.subGrouping.active)
134
- sharedFilter.subGrouping.active = currentSubOption?.value || subDefaultValue
135
- } else {
136
- sharedFilter.subGrouping.active = subDefaultValue
137
- }
138
- }
122
+ const queryValue = getQueryParam(sharedFilter?.setByQueryParameter)
123
+ const subQueryValue = getQueryParam(sharedFilter?.subGrouping?.setByQueryParameter)
124
+
125
+ // Priority: query string > configured defaultValue > existing active (if valid) > first option
126
+ // Note: use loose equality here to match values across possible string/number differences
127
+ const validActive = dropdownOptions.find(option => option.value == sharedFilter.active)
128
+ sharedFilter.active =
129
+ queryValue || sharedFilter.defaultValue || (validActive ? sharedFilter.active : dropdownOptions[0]?.value)
130
+
131
+ const options = dropdownOptions.find(option => option.value == sharedFilter.active)?.subOptions || []
132
+ const validSubActive = options.find(o => o.value == sharedFilter.subGrouping?.active)
133
+ sharedFilter.subGrouping.active =
134
+ subQueryValue ||
135
+ sharedFilter.subGrouping?.defaultValue ||
136
+ (validSubActive ? sharedFilter.subGrouping.active : options[0]?.value)
139
137
  }
140
138
 
141
139
  export const setActiveMultiDropdown = (dropdownOptions, sharedFilter) => {
142
- const defaultQueryParamValue = getQueryParam(sharedFilter?.setByQueryParameter)
143
- const multiDefaultQueryParamValue = Array.isArray(defaultQueryParamValue)
144
- ? defaultQueryParamValue
145
- : defaultQueryParamValue?.split(',')
146
- const multiDefaultValue = defaultQueryParamValue ? multiDefaultQueryParamValue : [dropdownOptions[0]?.value]
140
+ const queryValue = getQueryParam(sharedFilter?.setByQueryParameter)
141
+ const queryValues = Array.isArray(queryValue) ? queryValue : queryValue?.split(',')
142
+ const defaultValues = queryValue ? queryValues : [dropdownOptions[0]?.value]
147
143
  const currentOption = (Array.isArray(sharedFilter.active) ? sharedFilter.active : []).filter(activeVal =>
148
144
  dropdownOptions.find(option => option.value === activeVal)
149
145
  )
150
- sharedFilter.active = currentOption.length ? currentOption : multiDefaultValue
146
+ sharedFilter.active = currentOption.length ? currentOption : defaultValues
151
147
  }
152
148
 
153
149
  export const setAutoLoadDefaultValue = (
@@ -158,20 +154,20 @@ export const setAutoLoadDefaultValue = (
158
154
  ): SharedFilter => {
159
155
  const sharedFiltersCopy = _.cloneDeep(sharedFilters)
160
156
  const sharedFilter = _.cloneDeep(sharedFiltersCopy[sharedFilterIndex])
161
- const defaultQueryParamValue = getQueryParam(sharedFilter?.setByQueryParameter)
162
- const hasQueryParameter = sharedFilter.setByQueryParameter ? defaultQueryParamValue !== undefined : false
157
+ const queryValue = getQueryParam(sharedFilter?.setByQueryParameter)
158
+ const hasQuery = sharedFilter.setByQueryParameter ? queryValue !== undefined : false
163
159
  if (!autoLoadFilterIndexes.length || !dropdownOptions?.length) {
164
- if (hasQueryParameter && sharedFilter.apiFilter) {
160
+ if (hasQuery && sharedFilter.apiFilter) {
165
161
  const subQueryValue = getQueryParam(sharedFilter.subGrouping?.setByQueryParameter)
166
- const isNestedDropdown = subQueryValue !== undefined
167
- sharedFilter.queuedActive = isNestedDropdown ? [defaultQueryParamValue, subQueryValue] : defaultQueryParamValue
162
+ const isNested = subQueryValue !== undefined
163
+ sharedFilter.queuedActive = isNested ? [queryValue, subQueryValue] : queryValue
168
164
  }
169
165
  return sharedFilter // no autoLoading happening
170
166
  }
171
- if (autoLoadFilterIndexes.includes(sharedFilterIndex) || hasQueryParameter) {
167
+ if (autoLoadFilterIndexes.includes(sharedFilterIndex) || hasQuery) {
172
168
  const filterParents = sharedFiltersCopy.filter(f => sharedFilter.parents?.includes(f.key))
173
- const notAllParentFiltersSelected = filterParents.some(p => !(p.active || p.queuedActive))
174
- if (filterParents && notAllParentFiltersSelected) return sharedFilter
169
+ const missingParents = filterParents.some(p => !(p.active || p.queuedActive))
170
+ if (missingParents) return sharedFilter
175
171
  if (sharedFilter.filterStyle === FILTER_STYLE.multiSelect) {
176
172
  setActiveMultiDropdown(dropdownOptions, sharedFilter)
177
173
  } else if (sharedFilter.filterStyle === FILTER_STYLE.nestedDropdown) {
@@ -179,7 +175,7 @@ export const setAutoLoadDefaultValue = (
179
175
  } else {
180
176
  const defaultValue = dropdownOptions[0]?.value
181
177
  if (!sharedFilter.active) {
182
- sharedFilter.active = defaultQueryParamValue || defaultValue
178
+ sharedFilter.active = queryValue ?? defaultValue
183
179
  } else {
184
180
  const currentOption = dropdownOptions.find(option => option.value == sharedFilter.active) // loose equality required: 2017 should equal '2017'
185
181
  sharedFilter.active = currentOption ? currentOption.value : defaultValue
@@ -129,7 +129,7 @@ export const stripConfig = (configToStrip, isEditor = false) => {
129
129
  } else {
130
130
  delete strippedConfig.runtime
131
131
  delete strippedConfig.formattedData
132
- if (strippedConfig.dataUrl) {
132
+ if (strippedConfig.dataUrl && !isEditor) {
133
133
  delete strippedConfig.data
134
134
  }
135
135
  }
@@ -90,6 +90,9 @@ export const getVizConfig = (
90
90
 
91
91
  if (visualizationConfig.formattedData) visualizationConfig.originalFormattedData = visualizationConfig.formattedData
92
92
  const filteredVizData = filteredData?.[rowNumber] ?? filteredData?.[visualizationKey]
93
+ const dataKey = visualizationConfig.dataKey || 'backwards-compatibility'
94
+
95
+ visualizationConfig.dataMetadata = config.datasets[dataKey]?.dataMetadata || {}
93
96
 
94
97
  if (filteredVizData) {
95
98
  visualizationConfig.data = filteredVizData || []
@@ -97,9 +100,16 @@ export const getVizConfig = (
97
100
  visualizationConfig.formattedData = visualizationConfig.data
98
101
  }
99
102
  } else {
100
- const dataKey = visualizationConfig.dataKey || 'backwards-compatibility'
101
- // Markup-includes need data even when shared filters exist (for markup variables)
102
- const shouldClearData = sharedFilterColumns.length && visualizationConfig.type !== 'markup-include'
103
+ // Clear data for charts/maps when shared filters exist but filtered data
104
+ // hasn't arrived yet prevents rendering the full unfiltered dataset as DOM.
105
+ // Lighter types (data-bite, waffle-chart, filtered-text, markup-include) are
106
+ // excluded: they only compute scalars or single elements, and their editor
107
+ // panels need data to populate column dropdowns. Ideally data filters would
108
+ // apply synchronously before render, but they currently go through the same
109
+ // async loadAPIFilters pipeline as API filters, so filtered data isn't
110
+ // available on first render.
111
+ const heavyVizTypes = ['chart', 'map']
112
+ const shouldClearData = sharedFilterColumns.length && heavyVizTypes.includes(visualizationConfig.type)
103
113
  visualizationConfig.data = shouldClearData ? [] : data[dataKey] || []
104
114
  if (visualizationConfig.formattedData) {
105
115
  visualizationConfig.formattedData =
@@ -1,36 +1,45 @@
1
- import Icon from '@cdc/core/components/ui/Icon'
2
- import { AnyVisualization } from '@cdc/core/types/Visualization'
3
-
4
- export const iconHash = {
5
- 'data-bite': <Icon display='databite' base />,
6
- Bar: <Icon display='chartBar' base />,
7
- 'Spark Line': <Icon display='chartLine' />,
8
- 'Bump Chart': <Icon display='chartLine' />,
9
- 'waffle-chart': <Icon display='grid' base />,
10
- 'markup-include': <Icon display='code' base />,
11
- Line: <Icon display='chartLine' base />,
12
- Pie: <Icon display='chartPie' base />,
13
- us: <Icon display='mapUsa' base />,
14
- 'us-county': <Icon display='mapUsa' base />,
15
- world: <Icon display='mapWorld' base />,
16
- 'single-state': <Icon display='mapAl' base />,
17
- gear: <Icon display='gear' base />,
18
- gearMulti: <Icon display='gearMulti' base />,
19
- tools: <Icon display='tools' base />,
20
- 'filtered-text': <Icon display='filtered-text' base />,
21
- dashboardFilters: <Icon display='dashboardFilters' base />,
22
- table: <Icon display='table' base />,
23
- Sankey: <Icon display='sankey' base />
24
- }
25
-
26
- export const getIcon = (visualization: AnyVisualization) => {
27
- const { type, visualizationType, general } = visualization
28
- if (visualizationType) return iconHash[visualizationType]
29
- if (general?.geoType) {
30
- // for visualizations, mismatching state and state icon is not desired
31
- // so instead of showing alabama as the default state icon we show the US icon.
32
- if (general.geoType === 'single-state') return iconHash['us']
33
- return iconHash[general.geoType]
34
- }
35
- return iconHash[type]
36
- }
1
+ import Icon from '@cdc/core/components/ui/Icon'
2
+ import { AnyVisualization } from '@cdc/core/types/Visualization'
3
+
4
+ export const iconHash = {
5
+ 'data-bite': <Icon display='databite' base />,
6
+ Bar: <Icon display='chartBar' base />,
7
+ 'Spark Line': <Icon display='chartLine' />,
8
+ 'Bump Chart': <Icon display='chartLine' />,
9
+ 'waffle-chart': <Icon display='grid' base />,
10
+ 'markup-include': <Icon display='code' base />,
11
+ Line: <Icon display='chartLine' base />,
12
+ Pie: <Icon display='chartPie' base />,
13
+ us: <Icon display='mapUsa' base />,
14
+ 'us-county': <Icon display='mapUsa' base />,
15
+ world: <Icon display='mapWorld' base />,
16
+ 'single-state': <Icon display='mapAl' base />,
17
+ gear: <Icon display='gear' base />,
18
+ gearMulti: <Icon display='gearMulti' base />,
19
+ tools: <Icon display='tools' base />,
20
+ 'filtered-text': <Icon display='filtered-text' base />,
21
+ dashboardFilters: <Icon display='dashboardFilters' base />,
22
+ table: <Icon display='table' base />,
23
+ Sankey: <Icon display='sankey' base />,
24
+ Combo: <Icon display='chartBar' base />,
25
+ 'Scatter Plot': <Icon display='chartBar' base />,
26
+ 'Area Chart': <Icon display='chartLine' base />,
27
+ 'Deviation Bar': <Icon display='chartBar' base />,
28
+ 'Paired Bar': <Icon display='chartBar' base />,
29
+ 'Box Plot': <Icon display='chartBar' base />,
30
+ 'Forest Plot': <Icon display='chartBar' base />,
31
+ Forecasting: <Icon display='chartLine' base />,
32
+ 'Warming Stripes': <Icon display='chartBar' base />
33
+ }
34
+
35
+ export const getIcon = (visualization: AnyVisualization) => {
36
+ const { type, visualizationType, general } = visualization
37
+ if (visualizationType) return iconHash[visualizationType]
38
+ if (general?.geoType) {
39
+ // for visualizations, mismatching state and state icon is not desired
40
+ // so instead of showing alabama as the default state icon we show the US icon.
41
+ if (general.geoType === 'single-state') return iconHash['us']
42
+ return iconHash[general.geoType]
43
+ }
44
+ return iconHash[type]
45
+ }
@@ -1,14 +1,19 @@
1
- import fetchRemoteData from '@cdc/core/helpers/fetchRemoteData'
2
- import { getFormattedData } from './getFormattedData'
3
-
4
- export const processDataLegacy = async (response: any) => {
5
- let dataset = response.formattedData || response.data
6
-
7
- if (response.dataUrl) {
8
- dataset = await fetchRemoteData(`${response.dataUrl}`)
9
-
10
- dataset = getFormattedData(dataset, response.dataDescription)
11
- }
12
-
13
- return dataset
14
- }
1
+ import fetchRemoteData from '@cdc/core/helpers/fetchRemoteData'
2
+ import { getFormattedData } from './getFormattedData'
3
+
4
+ export const processDataLegacy = async (
5
+ response: any
6
+ ): Promise<{ data: any[]; dataMetadata: Record<string, string> }> => {
7
+ let dataset = response.formattedData || response.data
8
+ let dataMetadata: Record<string, string> = {}
9
+
10
+ if (response.dataUrl) {
11
+ const result = await fetchRemoteData(`${response.dataUrl}`)
12
+ dataset = result.data
13
+ dataMetadata = result.dataMetadata
14
+
15
+ dataset = getFormattedData(dataset, response.dataDescription)
16
+ }
17
+
18
+ return { data: dataset, dataMetadata }
19
+ }
@@ -1,44 +1,141 @@
1
- import { SharedFilter } from '../../types/SharedFilter'
2
- import { addValuesToDashboardFilters } from '../addValuesToDashboardFilters'
3
-
4
- describe('addValuesToDashboardFilters', () => {
5
- const colA = { columnName: 'colA', id: 11, active: 'apple', values: [], type: 'datafilter' } as SharedFilter
6
- const colB = { columnName: 'colB', id: 22, active: '1', values: [], type: 'datafilter' } as SharedFilter
7
- const colC = { columnName: 'colC', id: 33, values: [], setByQueryParameter: 'colC', type: 'datafilter' } as SharedFilter
8
-
9
- const data = {
10
- key: [
11
- { colA: 'apple', colB: 3, colC: 'abc' },
12
- { colA: 'apple', colB: 1, colC: 'bcd' },
13
- { colA: 'pear', colB: 4, colC: 'test' }
14
- ]
15
- }
16
- const filters = [colA, colC, colB]
17
- it('adds filter values', () => {
18
- const newFilters = addValuesToDashboardFilters(filters, data)
19
- expect(newFilters[0].values).toEqual(['apple', 'pear'])
20
- })
21
- it('converts to multiselect', () => {
22
- colA.multiSelect = true
23
- const newFilters = addValuesToDashboardFilters(filters, data)
24
- expect(newFilters[0].active).toEqual(['apple'])
25
- })
26
-
27
- it('sets active value by query string', () => {
28
- delete window.location
29
- window.location = new URL('https://www.example.com?colC=test')
30
- const newFilters = addValuesToDashboardFilters(filters, data)
31
- expect(newFilters[1].active).toEqual('test')
32
- })
33
- const colA2 = { apiFilter: { valueSelector: 'colA' }, id: 11, active: 'apple', values: [], type: 'urlfilter' } as SharedFilter
34
- const colB2 = { apiFilter: { valueSelector: 'colB' }, id: 22, active: '1', values: [], type: 'urlfilter' } as SharedFilter
35
- const colC2 = { apiFilter: { valueSelector: 'colC' }, id: 33, values: [], setByQueryParameter: 'colC', type: 'urlfilter' } as SharedFilter
36
- const filters2 = [colA2, colC2, colB2]
37
- it('skips urlfilters', () => {
38
- // urlfilter reloading happens in the dashboard in the loadAPIFilters function
39
- delete window.location
40
- window.location = new URL('https://www.example.com?colC=test')
41
- const newFilters = addValuesToDashboardFilters(filters2, data)
42
- expect(newFilters[1].active).toEqual(undefined)
43
- })
44
- })
1
+ import { SharedFilter } from '../../types/SharedFilter'
2
+ import { addValuesToDashboardFilters } from '../addValuesToDashboardFilters'
3
+
4
+ describe('addValuesToDashboardFilters', () => {
5
+ const colA = { columnName: 'colA', id: 11, active: 'apple', values: [], type: 'datafilter' } as SharedFilter
6
+ const colB = { columnName: 'colB', id: 22, active: '1', values: [], type: 'datafilter' } as SharedFilter
7
+ const colC = {
8
+ columnName: 'colC',
9
+ id: 33,
10
+ values: [],
11
+ setByQueryParameter: 'colC',
12
+ type: 'datafilter'
13
+ } as SharedFilter
14
+
15
+ const data = {
16
+ key: [
17
+ { colA: 'apple', colB: 3, colC: 'abc' },
18
+ { colA: 'apple', colB: 1, colC: 'bcd' },
19
+ { colA: 'pear', colB: 4, colC: 'test' }
20
+ ]
21
+ }
22
+ const filters = [colA, colC, colB]
23
+ it('adds filter values', () => {
24
+ const newFilters = addValuesToDashboardFilters(filters, data)
25
+ expect(newFilters[0].values).toEqual(['apple', 'pear'])
26
+ })
27
+ it('converts to multiselect', () => {
28
+ colA.multiSelect = true
29
+ const newFilters = addValuesToDashboardFilters(filters, data)
30
+ expect(newFilters[0].active).toEqual(['apple'])
31
+ })
32
+
33
+ it('sets active value by query string', () => {
34
+ delete window.location
35
+ window.location = new URL('https://www.example.com?colC=test')
36
+ const newFilters = addValuesToDashboardFilters(filters, data)
37
+ expect(newFilters[1].active).toEqual('test')
38
+ })
39
+ const colA2 = {
40
+ apiFilter: { valueSelector: 'colA' },
41
+ id: 11,
42
+ active: 'apple',
43
+ values: [],
44
+ type: 'urlfilter'
45
+ } as SharedFilter
46
+ const colB2 = {
47
+ apiFilter: { valueSelector: 'colB' },
48
+ id: 22,
49
+ active: '1',
50
+ values: [],
51
+ type: 'urlfilter'
52
+ } as SharedFilter
53
+ const colC2 = {
54
+ apiFilter: { valueSelector: 'colC' },
55
+ id: 33,
56
+ values: [],
57
+ setByQueryParameter: 'colC',
58
+ type: 'urlfilter'
59
+ } as SharedFilter
60
+ const filters2 = [colA2, colC2, colB2]
61
+ it('skips urlfilters', () => {
62
+ // urlfilter reloading happens in the dashboard in the loadAPIFilters function
63
+ delete window.location
64
+ window.location = new URL('https://www.example.com?colC=test')
65
+ const newFilters = addValuesToDashboardFilters(filters2, data)
66
+ expect(newFilters[1].active).toEqual(undefined)
67
+ })
68
+
69
+ it('respects nested dropdown default values for both group and subgroup', () => {
70
+ const nestedData = {
71
+ key: [
72
+ { year: '2022', quarter: 'Q1', region: 'North' },
73
+ { year: '2022', quarter: 'Q2', region: 'North' },
74
+ { year: '2023', quarter: 'Q1', region: 'North' },
75
+ { year: '2023', quarter: 'Q2', region: 'North' },
76
+ { year: '2023', quarter: 'Q3', region: 'North' },
77
+ { year: '2024', quarter: 'Q1', region: 'North' }
78
+ ]
79
+ }
80
+
81
+ const nestedFilter = {
82
+ columnName: 'year',
83
+ id: 1,
84
+ values: ['2022', '2023', '2024'],
85
+ type: 'datafilter',
86
+ filterStyle: 'nested-dropdown',
87
+ defaultValue: '2023',
88
+ subGrouping: {
89
+ columnName: 'quarter',
90
+ defaultValue: 'Q2',
91
+ valuesLookup: {
92
+ '2022': { values: ['Q1', 'Q2'] },
93
+ '2023': { values: ['Q1', 'Q2', 'Q3'] },
94
+ '2024': { values: ['Q1'] }
95
+ }
96
+ }
97
+ } as SharedFilter
98
+
99
+ const result = addValuesToDashboardFilters([nestedFilter], nestedData)
100
+
101
+ // Should use configured defaultValue for main group
102
+ expect(result[0].active).toBe('2023')
103
+
104
+ // Should use configured defaultValue for subgroup
105
+ expect(result[0].subGrouping.active).toBe('Q2')
106
+ })
107
+
108
+ it('uses first available subgroup value when defaultValue is not in current group', () => {
109
+ const nestedData = {
110
+ key: [
111
+ { year: '2022', quarter: 'Q1', region: 'North' },
112
+ { year: '2024', quarter: 'Q1', region: 'North' }
113
+ ]
114
+ }
115
+
116
+ const nestedFilter = {
117
+ columnName: 'year',
118
+ id: 1,
119
+ values: ['2022', '2024'],
120
+ type: 'datafilter',
121
+ filterStyle: 'nested-dropdown',
122
+ defaultValue: '2024',
123
+ subGrouping: {
124
+ columnName: 'quarter',
125
+ defaultValue: 'Q2', // Q2 doesn't exist for 2024
126
+ valuesLookup: {
127
+ '2022': { values: ['Q1'] },
128
+ '2024': { values: ['Q1'] }
129
+ }
130
+ }
131
+ } as SharedFilter
132
+
133
+ const result = addValuesToDashboardFilters([nestedFilter], nestedData)
134
+
135
+ // Should use configured defaultValue for main group
136
+ expect(result[0].active).toBe('2024')
137
+
138
+ // Should fall back to first available value since Q2 doesn't exist for 2024
139
+ expect(result[0].subGrouping.active).toBe('Q1')
140
+ })
141
+ })
@@ -0,0 +1,52 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+ import { addVisualization } from '../addVisualization'
3
+
4
+ describe('addVisualization', () => {
5
+ it('creates chart visual settings with extra theme toggles disabled by default', () => {
6
+ vi.spyOn(Date, 'now').mockReturnValue(12345)
7
+
8
+ const visualization = addVisualization('chart', 'Bar')
9
+
10
+ expect(visualization).toMatchObject({
11
+ uid: 'chart12345',
12
+ type: 'chart',
13
+ visualizationType: 'Bar',
14
+ visual: {
15
+ border: false,
16
+ borderColorTheme: false,
17
+ accent: false,
18
+ background: false,
19
+ hideBackgroundColor: false
20
+ }
21
+ })
22
+ })
23
+
24
+ it('creates map visual settings with extra theme toggles disabled by default', () => {
25
+ vi.spyOn(Date, 'now').mockReturnValue(12345)
26
+
27
+ const visualization = addVisualization('map', 'single-state')
28
+
29
+ expect(visualization).toMatchObject({
30
+ uid: 'map12345',
31
+ type: 'map',
32
+ general: {
33
+ geoType: 'single-state'
34
+ },
35
+ visual: {
36
+ border: false,
37
+ borderColorTheme: false,
38
+ accent: false,
39
+ background: false,
40
+ hideBackgroundColor: false
41
+ }
42
+ })
43
+ })
44
+
45
+ it('preserves visualizationType for data-bite family visualizations', () => {
46
+ vi.spyOn(Date, 'now').mockReturnValue(12345)
47
+
48
+ expect(addVisualization('data-bite')).toMatchObject({ visualizationType: 'data-bite' })
49
+ expect(addVisualization('waffle-chart')).toMatchObject({ visualizationType: 'waffle-chart' })
50
+ expect(addVisualization('filtered-text')).toMatchObject({ visualizationType: 'filtered-text' })
51
+ })
52
+ })