@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.
- package/LICENSE +201 -0
- package/dist/cdcdashboard-8NmHlKRI.es.js +15 -0
- package/dist/cdcdashboard-BPoPzKPz.es.js +6 -0
- package/dist/{cdcdashboard-dgT_1dIT.es.js → cdcdashboard-DQ00cQCm.es.js} +1 -20
- package/dist/cdcdashboard-jiQQPkty.es.js +6 -0
- package/dist/cdcdashboard-vr9HZwRt.es.js +6 -0
- package/dist/cdcdashboard.js +80971 -83096
- package/examples/custom/css/respiratory.css +1 -1
- package/examples/data/data-with-metadata.json +18 -0
- package/examples/default.json +492 -132
- package/examples/nested-dropdown.json +6985 -0
- package/examples/private/abc.json +467 -0
- package/examples/private/dash.json +12696 -0
- package/examples/private/inline-markup.json +775 -0
- package/examples/private/npcr.json +1 -0
- package/examples/private/recent-update.json +1456 -0
- package/examples/private/test.json +125407 -0
- package/examples/private/timeline-data.json +4994 -0
- package/examples/private/timeline.json +1708 -0
- package/examples/private/toggle.json +10137 -0
- package/examples/test-api-filter-reset.json +8 -4
- package/examples/tp5-gauges.json +196 -0
- package/examples/tp5-test.json +266 -0
- package/index.html +1 -29
- package/package.json +38 -40
- package/src/CdcDashboard.tsx +2 -1
- package/src/CdcDashboardComponent.tsx +47 -30
- package/src/_stories/Dashboard.DataSetup.stories.tsx +8 -2
- package/src/_stories/Dashboard.Pages.stories.tsx +22 -0
- package/src/_stories/Dashboard.stories.tsx +4501 -80
- package/src/_stories/_mock/dashboard-line-chart-angles.json +1030 -0
- package/src/_stories/_mock/tab-simple-filter.json +153 -0
- package/src/_stories/_mock/tp5-test.json +267 -0
- package/src/components/DashboardFilters/DashboardFilters.tsx +19 -3
- package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +10 -4
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/APIModal.tsx +1 -1
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +6 -3
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/NestedDropDownDashboard.tsx +13 -8
- package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +8 -8
- package/src/components/DashboardFilters/_stories/DashboardFilters.stories.tsx +1 -1
- package/src/components/DashboardFilters/dashboardfilter.styles.css +3 -3
- package/src/components/DataDesignerModal.tsx +2 -2
- package/src/components/Header/Header.tsx +27 -5
- package/src/components/Header/index.scss +1 -1
- package/src/components/MultiConfigTabs/multiconfigtabs.styles.css +6 -6
- package/src/components/Row.tsx +21 -0
- package/src/components/Toggle/toggle-style.css +7 -7
- package/src/components/VisualizationRow.tsx +42 -29
- package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +1 -71
- package/src/components/VisualizationsPanel/visualizations-panel-styles.css +2 -2
- package/src/components/Widget/Widget.tsx +2 -2
- package/src/components/Widget/widget.styles.css +12 -12
- package/src/data/initial-state.js +1 -1
- package/src/helpers/addValuesToDashboardFilters.ts +17 -11
- package/src/helpers/addVisualization.ts +71 -0
- package/src/helpers/apiFilterHelpers.ts +28 -32
- package/src/helpers/formatConfigBeforeSave.ts +1 -1
- package/src/helpers/getVizConfig.ts +13 -3
- package/src/helpers/iconHash.tsx +45 -36
- package/src/helpers/processDataLegacy.ts +19 -14
- package/src/helpers/tests/addValuesToDashboardFilters.test.ts +141 -44
- package/src/helpers/tests/addVisualization.test.ts +52 -0
- package/src/helpers/tests/apiFilterHelpers.test.ts +523 -420
- package/src/helpers/tests/formatConfigBeforeSave.test.ts +81 -1
- package/src/scss/editor-panel.scss +1 -1
- package/src/scss/main.scss +169 -41
- package/src/store/dashboard.reducer.ts +1 -1
- package/src/test/CdcDashboard.test.jsx +2 -2
- package/src/test/CdcDashboardComponent.test.tsx +74 -0
- package/src/types/FilterStyles.ts +2 -1
- package/tests/fixtures/dashboard-config-with-metadata.json +89 -0
- package/vite.config.js +7 -1
- package/dist/cdcdashboard-BnB1QM5d.es.js +0 -361528
- package/dist/cdcdashboard-Ct2SB0vL.es.js +0 -231049
- package/dist/cdcdashboard-D6CG2-Hb.es.js +0 -39377
- 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
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
|
143
|
-
const
|
|
144
|
-
|
|
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 :
|
|
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
|
|
162
|
-
const
|
|
157
|
+
const queryValue = getQueryParam(sharedFilter?.setByQueryParameter)
|
|
158
|
+
const hasQuery = sharedFilter.setByQueryParameter ? queryValue !== undefined : false
|
|
163
159
|
if (!autoLoadFilterIndexes.length || !dropdownOptions?.length) {
|
|
164
|
-
if (
|
|
160
|
+
if (hasQuery && sharedFilter.apiFilter) {
|
|
165
161
|
const subQueryValue = getQueryParam(sharedFilter.subGrouping?.setByQueryParameter)
|
|
166
|
-
const
|
|
167
|
-
sharedFilter.queuedActive =
|
|
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) ||
|
|
167
|
+
if (autoLoadFilterIndexes.includes(sharedFilterIndex) || hasQuery) {
|
|
172
168
|
const filterParents = sharedFiltersCopy.filter(f => sharedFilter.parents?.includes(f.key))
|
|
173
|
-
const
|
|
174
|
-
if (
|
|
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 =
|
|
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
|
-
|
|
101
|
-
//
|
|
102
|
-
|
|
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 =
|
package/src/helpers/iconHash.tsx
CHANGED
|
@@ -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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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 (
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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 = {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
it('
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
+
})
|