@cdc/dashboard 4.25.10 → 4.25.11
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/dist/{cdcdashboard-fce76882.es.js → cdcdashboard-BnB1QM5d.es.js} +6 -13
- package/dist/{cdcdashboard-c55ac1ea.es.js → cdcdashboard-D6CG2-Hb.es.js} +5 -12
- package/dist/{cdcdashboard-31a33da1.es.js → cdcdashboard-MXgURbdZ.es.js} +6 -13
- package/dist/{cdcdashboard-1a1724a1.es.js → cdcdashboard-dgT_1dIT.es.js} +136 -151
- package/dist/cdcdashboard.js +48574 -46414
- package/examples/api-test/categories.json +18 -0
- package/examples/api-test/chart-data.json +602 -0
- package/examples/api-test/topics.json +47 -0
- package/examples/api-test/years.json +22 -0
- package/examples/markup-axis-label.json +4167 -0
- package/examples/private/DEV-10538.json +407 -0
- package/examples/private/DEV-11405.json +39112 -0
- package/examples/private/big-dashboard.json +39095 -39077
- package/examples/private/clade-2.json +430 -0
- package/examples/private/delete.json +32919 -0
- package/examples/private/diabetes.json +546 -196
- package/examples/private/markup-footer/mortality-deaths-footnotes-age.csv +3 -0
- package/examples/private/mpox.json +38128 -0
- package/examples/private/reset.json +32920 -0
- package/examples/test-api-filter-reset.json +132 -0
- package/index.html +2 -2
- package/package.json +9 -10
- package/src/CdcDashboardComponent.tsx +17 -8
- package/src/DashboardContext.tsx +3 -1
- package/src/_stories/Dashboard.stories.tsx +17 -0
- package/src/_stories/_mock/custom-order-new-values.json +116 -0
- package/src/components/DashboardFilters/DashboardFilters.tsx +34 -20
- package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +29 -12
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +77 -111
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/NestedDropDownDashboard.tsx +51 -51
- package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +120 -24
- package/src/components/DashboardFilters/_stories/DashboardFilters.stories.tsx +62 -3
- package/src/components/DataDesignerModal.tsx +12 -5
- package/src/components/Header/Header.tsx +10 -9
- package/src/components/Toggle/Toggle.tsx +48 -48
- package/src/components/VisualizationRow.tsx +4 -3
- package/src/helpers/addValuesToDashboardFilters.ts +29 -4
- package/src/helpers/apiFilterHelpers.ts +26 -2
- package/src/helpers/filterData.ts +52 -7
- package/src/helpers/filterResetHelpers.ts +102 -0
- package/src/helpers/getVizConfig.ts +2 -2
- package/src/helpers/loadAPIFilters.ts +109 -99
- package/src/helpers/tests/filterResetHelpers.test.ts +532 -0
- package/src/index.tsx +1 -0
- package/src/scss/editor-panel.scss +3 -431
- package/src/scss/main.scss +1 -24
- package/src/store/errorMessage/errorMessage.reducer.ts +1 -1
- package/src/types/DashboardFilters.ts +9 -8
- package/examples/private/burden_toolkit_mortality_diabetes_attributable_deaths_data.csv +0 -14041
- package/examples/private/burden_toolkit_mortality_diabetes_attributable_deaths_per_100000_data.csv +0 -14041
- package/examples/private/burden_toolkit_mortality_qaly_data.csv +0 -18721
- package/examples/private/burden_toolkit_mortality_yll_data.csv +0 -18721
- package/src/helpers/getAutoLoadVisualization.ts +0 -11
- package/src/scss/mixins.scss +0 -47
- package/src/scss/variables.scss +0 -5
- /package/dist/{cdcdashboard-548642e6.es.js → cdcdashboard-Ct2SB0vL.es.js} +0 -0
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
2
|
import DashboardFilters from '../DashboardFilters'
|
|
3
|
+
import '../../../scss/main.scss'
|
|
3
4
|
|
|
4
5
|
const meta: Meta<typeof DashboardFilters> = {
|
|
5
6
|
title: 'Components/Atoms/Inputs/DashboardFilters',
|
|
6
|
-
component: DashboardFilters
|
|
7
|
+
component: DashboardFilters,
|
|
8
|
+
decorators: [
|
|
9
|
+
Story => (
|
|
10
|
+
<div className='cdc-open-viz-module type-dashboard'>
|
|
11
|
+
<Story />
|
|
12
|
+
</div>
|
|
13
|
+
)
|
|
14
|
+
]
|
|
7
15
|
}
|
|
8
16
|
|
|
9
17
|
type Story = StoryObj<typeof DashboardFilters>
|
|
@@ -11,11 +19,62 @@ type Story = StoryObj<typeof DashboardFilters>
|
|
|
11
19
|
export const Example_1: Story = {
|
|
12
20
|
args: {
|
|
13
21
|
filters: [
|
|
14
|
-
{
|
|
15
|
-
|
|
22
|
+
{
|
|
23
|
+
type: 'datafilter',
|
|
24
|
+
key: 'label here',
|
|
25
|
+
values: ['1', '2', '3', '4'],
|
|
26
|
+
columnName: 'label',
|
|
27
|
+
showDropdown: true,
|
|
28
|
+
id: 0,
|
|
29
|
+
parents: []
|
|
30
|
+
} as any,
|
|
31
|
+
{
|
|
32
|
+
type: 'datafilter',
|
|
33
|
+
key: 'something',
|
|
34
|
+
values: ['A', 'B', 'C'],
|
|
35
|
+
columnName: 'something',
|
|
36
|
+
showDropdown: true,
|
|
37
|
+
id: 1,
|
|
38
|
+
parents: []
|
|
39
|
+
} as any
|
|
16
40
|
],
|
|
41
|
+
show: [0, 1],
|
|
42
|
+
apiFilterDropdowns: {},
|
|
17
43
|
handleOnChange: () => {}
|
|
18
44
|
}
|
|
19
45
|
}
|
|
20
46
|
|
|
47
|
+
export const WithClearButton: Story = {
|
|
48
|
+
args: {
|
|
49
|
+
filters: [
|
|
50
|
+
{
|
|
51
|
+
type: 'datafilter',
|
|
52
|
+
key: 'Category',
|
|
53
|
+
values: ['Option 1', 'Option 2', 'Option 3'],
|
|
54
|
+
active: 'Option 1',
|
|
55
|
+
columnName: 'category',
|
|
56
|
+
showDropdown: true,
|
|
57
|
+
id: 0,
|
|
58
|
+
parents: []
|
|
59
|
+
} as any,
|
|
60
|
+
{
|
|
61
|
+
type: 'datafilter',
|
|
62
|
+
key: 'Status',
|
|
63
|
+
values: ['Active', 'Inactive', 'Pending'],
|
|
64
|
+
active: 'Active',
|
|
65
|
+
columnName: 'status',
|
|
66
|
+
showDropdown: true,
|
|
67
|
+
id: 1,
|
|
68
|
+
parents: []
|
|
69
|
+
} as any
|
|
70
|
+
],
|
|
71
|
+
show: [0, 1],
|
|
72
|
+
apiFilterDropdowns: {},
|
|
73
|
+
handleOnChange: () => {},
|
|
74
|
+
showSubmit: true,
|
|
75
|
+
applyFilters: () => {},
|
|
76
|
+
handleReset: () => {}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
21
80
|
export default meta
|
|
@@ -4,14 +4,13 @@ import { useContext, useMemo, useState } from 'react'
|
|
|
4
4
|
import { DashboardContext, DashboardDispatchContext } from '../DashboardContext'
|
|
5
5
|
import Modal from '@cdc/core/components/ui/Modal'
|
|
6
6
|
import Loader from '@cdc/core/components/Loader'
|
|
7
|
-
import { CheckBox } from '@cdc/core/components/EditorPanel/Inputs'
|
|
7
|
+
import { CheckBox, Select } from '@cdc/core/components/EditorPanel/Inputs'
|
|
8
8
|
import Tooltip from '@cdc/core/components/ui/Tooltip'
|
|
9
9
|
import _ from 'lodash'
|
|
10
10
|
import fetchRemoteData from '@cdc/core/helpers/fetchRemoteData'
|
|
11
11
|
import DataTransform from '@cdc/core/helpers/DataTransform'
|
|
12
12
|
import { ConfigureData } from '@cdc/core/types/ConfigureData'
|
|
13
13
|
import Icon from '@cdc/core/components/ui/Icon'
|
|
14
|
-
import InputSelect from '@cdc/core/components/inputs/InputSelect'
|
|
15
14
|
|
|
16
15
|
type DataDesignerModalProps = {
|
|
17
16
|
rowIndex: number
|
|
@@ -134,10 +133,18 @@ export const DataDesignerModal: React.FC<DataDesignerModalProps> = ({ vizKey, ro
|
|
|
134
133
|
{loadingAPIData && <Loader fullScreen />}
|
|
135
134
|
<div className='dataset-selector-container'>
|
|
136
135
|
Select a dataset:
|
|
137
|
-
<select
|
|
136
|
+
<select
|
|
137
|
+
className='dataset-selector cove-form-select'
|
|
138
|
+
value={configureData.dataKey || ''}
|
|
139
|
+
onChange={changeDataset}
|
|
140
|
+
>
|
|
138
141
|
<option value=''>Select a dataset</option>
|
|
139
142
|
{config.datasets &&
|
|
140
|
-
Object.keys(config.datasets).map(datasetKey =>
|
|
143
|
+
Object.keys(config.datasets).map(datasetKey => (
|
|
144
|
+
<option key={datasetKey} value={datasetKey}>
|
|
145
|
+
{datasetKey}
|
|
146
|
+
</option>
|
|
147
|
+
))}
|
|
141
148
|
</select>
|
|
142
149
|
{vizKey && (
|
|
143
150
|
// only shows for visualizations
|
|
@@ -186,7 +193,7 @@ export const DataDesignerModal: React.FC<DataDesignerModalProps> = ({ vizKey, ro
|
|
|
186
193
|
/>
|
|
187
194
|
) : (
|
|
188
195
|
<>
|
|
189
|
-
<
|
|
196
|
+
<Select
|
|
190
197
|
options={Object.keys(config.datasets[configureData.dataKey]?.data[0] || {})}
|
|
191
198
|
value={config.rows[rowIndex].multiVizColumn}
|
|
192
199
|
label='Multi-Visualization Column'
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useContext, useRef } from 'react'
|
|
2
2
|
import cloneConfig from '@cdc/core/helpers/cloneConfig'
|
|
3
3
|
import { DashboardContext, DashboardDispatchContext } from '../../DashboardContext'
|
|
4
4
|
|
|
@@ -56,21 +56,22 @@ const Header = (props: HeaderProps) => {
|
|
|
56
56
|
return strippedState
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
const parsedData = convertStateToConfig()
|
|
59
|
+
const configStringRef = useRef<string>()
|
|
61
60
|
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
// Only update parent when config content actually changes (not just reference)
|
|
62
|
+
const configString = JSON.stringify(convertStateToConfig())
|
|
63
|
+
if (configStringRef.current !== configString) {
|
|
64
|
+
configStringRef.current = configString
|
|
64
65
|
|
|
66
|
+
// Emit the data in a regular JS event so it can be consumed by anything.
|
|
67
|
+
const event = new CustomEvent('updateVizConfig', { detail: configString })
|
|
65
68
|
window.dispatchEvent(event)
|
|
66
69
|
|
|
67
70
|
// Pass up to Editor if needed
|
|
68
71
|
if (setParentConfig) {
|
|
69
|
-
setParentConfig(
|
|
72
|
+
setParentConfig(JSON.parse(configString))
|
|
70
73
|
}
|
|
71
|
-
|
|
72
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
73
|
-
}, [config])
|
|
74
|
+
}
|
|
74
75
|
|
|
75
76
|
const handleCheck = e => {
|
|
76
77
|
const { checked } = e.currentTarget
|
|
@@ -1,48 +1,48 @@
|
|
|
1
|
-
import { ConfigRow } from '../../types/ConfigRow'
|
|
2
|
-
import { AnyVisualization } from '@cdc/core/types/Visualization'
|
|
3
|
-
import { getIcon } from '../../helpers/iconHash'
|
|
4
|
-
import { labelHash } from '@cdc/core/helpers/labelHash'
|
|
5
|
-
import './toggle-style.css'
|
|
6
|
-
import _ from 'lodash'
|
|
7
|
-
|
|
8
|
-
type ToggleProps = {
|
|
9
|
-
active: number
|
|
10
|
-
row: ConfigRow
|
|
11
|
-
visualizations: Record<string, AnyVisualization>
|
|
12
|
-
setToggled: (colIndex: number) => void
|
|
13
|
-
}
|
|
14
|
-
const Toggle: React.FC<ToggleProps> = ({ active, row, visualizations, setToggled, text }) => {
|
|
15
|
-
const selectItem = (colIndex, e = null) => {
|
|
16
|
-
if (e?.key && e.key !== 'Enter' && e.key !== ' ') return
|
|
17
|
-
if (e?.key === ' ') e.preventDefault() // Prevent page scroll
|
|
18
|
-
setToggled(colIndex)
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
return (
|
|
22
|
-
<div className='toggle-component' role='radiogroup' aria-label='Visualization options'>
|
|
23
|
-
{row.columns.map((col, colIndex) => {
|
|
24
|
-
if (!col.widget) return null
|
|
25
|
-
const type = visualizations[col.widget].type
|
|
26
|
-
// Get the column toggele Text or default to the type
|
|
27
|
-
const text = col.toggleName ? col.toggleName : labelHash[type]
|
|
28
|
-
const selected = colIndex === active
|
|
29
|
-
return (
|
|
30
|
-
<div
|
|
31
|
-
role='radio'
|
|
32
|
-
className={selected ? 'selected' : ''}
|
|
33
|
-
key={colIndex}
|
|
34
|
-
onClick={() => selectItem(colIndex)}
|
|
35
|
-
onKeyUp={e => selectItem(colIndex, e)}
|
|
36
|
-
aria-checked={selected}
|
|
37
|
-
tabIndex={0}
|
|
38
|
-
aria-label={`Toggle ${
|
|
39
|
-
>
|
|
40
|
-
<span aria-hidden='true'>{getIcon(visualizations[col.widget])}</span> <span>{text}</span>
|
|
41
|
-
</div>
|
|
42
|
-
)
|
|
43
|
-
})}
|
|
44
|
-
</div>
|
|
45
|
-
)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export default Toggle
|
|
1
|
+
import { ConfigRow } from '../../types/ConfigRow'
|
|
2
|
+
import { AnyVisualization } from '@cdc/core/types/Visualization'
|
|
3
|
+
import { getIcon } from '../../helpers/iconHash'
|
|
4
|
+
import { labelHash } from '@cdc/core/helpers/labelHash'
|
|
5
|
+
import './toggle-style.css'
|
|
6
|
+
import _ from 'lodash'
|
|
7
|
+
|
|
8
|
+
type ToggleProps = {
|
|
9
|
+
active: number
|
|
10
|
+
row: ConfigRow
|
|
11
|
+
visualizations: Record<string, AnyVisualization>
|
|
12
|
+
setToggled: (colIndex: number) => void
|
|
13
|
+
}
|
|
14
|
+
const Toggle: React.FC<ToggleProps> = ({ active, row, visualizations, setToggled, text }) => {
|
|
15
|
+
const selectItem = (colIndex, e = null) => {
|
|
16
|
+
if (e?.key && e.key !== 'Enter' && e.key !== ' ') return
|
|
17
|
+
if (e?.key === ' ') e.preventDefault() // Prevent page scroll
|
|
18
|
+
setToggled(colIndex)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div className='toggle-component' role='radiogroup' aria-label='Visualization options'>
|
|
23
|
+
{row.columns.map((col, colIndex) => {
|
|
24
|
+
if (!col.widget) return null
|
|
25
|
+
const type = visualizations[col.widget].type
|
|
26
|
+
// Get the column toggele Text or default to the type
|
|
27
|
+
const text = col.toggleName ? col.toggleName : labelHash[type]
|
|
28
|
+
const selected = colIndex === active
|
|
29
|
+
return (
|
|
30
|
+
<div
|
|
31
|
+
role='radio'
|
|
32
|
+
className={selected ? 'selected' : ''}
|
|
33
|
+
key={colIndex}
|
|
34
|
+
onClick={() => selectItem(colIndex)}
|
|
35
|
+
onKeyUp={e => selectItem(colIndex, e)}
|
|
36
|
+
aria-checked={selected}
|
|
37
|
+
tabIndex={0}
|
|
38
|
+
aria-label={`Toggle ${text}`}
|
|
39
|
+
>
|
|
40
|
+
<span aria-hidden='true'>{getIcon(visualizations[col.widget])}</span> <span>{text}</span>
|
|
41
|
+
</div>
|
|
42
|
+
)
|
|
43
|
+
})}
|
|
44
|
+
</div>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default Toggle
|
|
@@ -223,11 +223,12 @@ const VisualizationRow: React.FC<VizRowProps> = ({
|
|
|
223
223
|
type === 'dashboardFilters' &&
|
|
224
224
|
sharedFilterIndexes &&
|
|
225
225
|
sharedFilterIndexes.filter(idx => config.dashboard.sharedFilters?.[idx]?.showDropdown === false).length ===
|
|
226
|
-
|
|
226
|
+
sharedFilterIndexes.length
|
|
227
227
|
const hasMarginBottom = !isLastRow && !hiddenDashboardFilters
|
|
228
228
|
|
|
229
|
-
const vizWrapperClass = `col-12 col-md-${col.width}${!shouldShow ? ' d-none' : ''}${
|
|
230
|
-
|
|
229
|
+
const vizWrapperClass = `col-12 col-md-${col.width}${!shouldShow ? ' d-none' : ''}${
|
|
230
|
+
hideVisualization ? ' hide-parent-visualization' : hasMarginBottom ? ' mb-4' : ''
|
|
231
|
+
}`
|
|
231
232
|
const link =
|
|
232
233
|
config.table && config.table.show && config.datasets && table && table.showDataTableLink
|
|
233
234
|
? tableLink
|
|
@@ -2,6 +2,7 @@ import _ from 'lodash'
|
|
|
2
2
|
import { getQueryStringFilterValue } from '@cdc/core/helpers/queryStringUtils'
|
|
3
3
|
import { SharedFilter } from '../types/SharedFilter'
|
|
4
4
|
import { handleSorting } from '@cdc/core/components/Filters'
|
|
5
|
+
import { mergeCustomOrderValues } from '@cdc/core/helpers/mergeCustomOrderValues'
|
|
5
6
|
|
|
6
7
|
// Gets filter values from dataset
|
|
7
8
|
const generateValuesForFilter = (columnName: string, data: Record<string, any[]>) => {
|
|
@@ -34,13 +35,16 @@ export const addValuesToDashboardFilters = (
|
|
|
34
35
|
data: Record<string, any[]>,
|
|
35
36
|
filtersToSkip: number[] = []
|
|
36
37
|
): Array<SharedFilter> => {
|
|
37
|
-
|
|
38
|
+
const result = filters?.map((filter, index) => {
|
|
38
39
|
if (filtersToSkip.includes(index)) return filter
|
|
39
40
|
if (filter.type === 'urlfilter') return filter
|
|
40
41
|
const filterCopy = _.cloneDeep(filter)
|
|
41
42
|
const filterValues = generateValuesForFilter(getSelector(filter), data)
|
|
42
43
|
filterCopy.values = filterValues
|
|
43
44
|
|
|
45
|
+
// Merge new values with existing custom order (fixes DEV-11740 & DEV-11376)
|
|
46
|
+
filterCopy.orderedValues = mergeCustomOrderValues(filterValues, filterCopy.orderedValues, filterCopy.order)
|
|
47
|
+
|
|
44
48
|
if (filterValues.length > 0) {
|
|
45
49
|
const queryStringFilterValue = getQueryStringFilterValue(filterCopy)
|
|
46
50
|
if (queryStringFilterValue) {
|
|
@@ -50,9 +54,29 @@ export const addValuesToDashboardFilters = (
|
|
|
50
54
|
const active: string[] = Array.isArray(filterCopy.active) ? filterCopy.active : [filterCopy.active]
|
|
51
55
|
filterCopy.active = active.filter(val => defaultValues.includes(val))
|
|
52
56
|
} else {
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
57
|
+
// Preserve existing active value if it's valid in the new filter values
|
|
58
|
+
const currentActive = filterCopy.active as string
|
|
59
|
+
const isResetLabelValue = currentActive && currentActive === filterCopy.resetLabel
|
|
60
|
+
const isCurrentActiveValid = currentActive && (filterValues.includes(currentActive) || isResetLabelValue)
|
|
61
|
+
|
|
62
|
+
// Check if this is an intentional clear (empty string, but not undefined during initial load)
|
|
63
|
+
const isIntentionalClear = currentActive === ''
|
|
64
|
+
|
|
65
|
+
// Priority: defaultValue > valid current active > reset label > first value
|
|
66
|
+
if (filterCopy.defaultValue) {
|
|
67
|
+
// If defaultValue is explicitly set, always use it
|
|
68
|
+
filterCopy.active = filterCopy.defaultValue
|
|
69
|
+
} else if (isCurrentActiveValid) {
|
|
70
|
+
// Keep the current active value if valid
|
|
71
|
+
filterCopy.active = currentActive
|
|
72
|
+
} else if (isIntentionalClear) {
|
|
73
|
+
// Don't override intentional clears
|
|
74
|
+
filterCopy.active = currentActive
|
|
75
|
+
} else {
|
|
76
|
+
// Set to reset label or first value
|
|
77
|
+
const defaultValue = filterCopy.resetLabel || filterCopy.values[0]
|
|
78
|
+
filterCopy.active = defaultValue
|
|
79
|
+
}
|
|
56
80
|
}
|
|
57
81
|
}
|
|
58
82
|
|
|
@@ -83,4 +107,5 @@ export const addValuesToDashboardFilters = (
|
|
|
83
107
|
|
|
84
108
|
return handleSorting(filterCopy)
|
|
85
109
|
})
|
|
110
|
+
return result
|
|
86
111
|
}
|
|
@@ -53,7 +53,31 @@ export const getParentParams = (
|
|
|
53
53
|
})
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Checks if any parent filters are unselected or at their reset state.
|
|
58
|
+
* Returns true if at least one parent is not properly selected.
|
|
59
|
+
*/
|
|
60
|
+
export const hasUnselectedParents = (parentParams, sharedFilters?: SharedFilter[]): boolean => {
|
|
61
|
+
if (!parentParams) return false
|
|
62
|
+
|
|
63
|
+
return parentParams.some(({ key, value }) => {
|
|
64
|
+
// Check if value is empty
|
|
65
|
+
if (value === '') return true
|
|
66
|
+
|
|
67
|
+
// Check if value equals the parent filter's resetLabel
|
|
68
|
+
if (sharedFilters) {
|
|
69
|
+
const parentFilter = sharedFilters.find(f => f.queryParameter === key || f.apiFilter?.valueSelector === key)
|
|
70
|
+
if (parentFilter?.resetLabel && value === parentFilter.resetLabel) {
|
|
71
|
+
return true
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return false
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Keep old name for backward compatibility
|
|
80
|
+
export const notAllParentsSelected = hasUnselectedParents
|
|
57
81
|
|
|
58
82
|
export const getFilterValues = (data: Array<Object>, apiFilter: APIFilter): DropdownOptions => {
|
|
59
83
|
const { textSelector, valueSelector, subgroupTextSelector, subgroupValueSelector } = apiFilter
|
|
@@ -86,7 +110,7 @@ export const getToFetch = (
|
|
|
86
110
|
if (apiFilterDropdowns[_key]) return // don't reload cached filter
|
|
87
111
|
const parentParams = getParentParams(filter, sharedFilters)
|
|
88
112
|
|
|
89
|
-
if (notAllParentsSelected(parentParams)) return // don't send request for dependent children filter options
|
|
113
|
+
if (notAllParentsSelected(parentParams, sharedFilters)) return // don't send request for dependent children filter options
|
|
90
114
|
|
|
91
115
|
const endpoint = baseEndpoint + (parentParams ? gatherQueryParams(baseEndpoint, parentParams) : '')
|
|
92
116
|
toFetch[endpoint] = [_key, index]
|
|
@@ -2,7 +2,11 @@ import _ from 'lodash'
|
|
|
2
2
|
import { SharedFilter } from '../types/SharedFilter'
|
|
3
3
|
import { FILTER_STYLE } from '../types/FilterStyles'
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Recursively calculates the tier/depth of a filter based on its parent dependencies.
|
|
7
|
+
* Root filters (no parents) are tier 1, children of root filters are tier 2, etc.
|
|
8
|
+
*/
|
|
9
|
+
const findFilterTier = (filters: SharedFilter[], sharedFilter: SharedFilter): number => {
|
|
6
10
|
if (!sharedFilter.parents?.length) {
|
|
7
11
|
return 1
|
|
8
12
|
} else {
|
|
@@ -12,6 +16,10 @@ const findFilterTier = (filters: SharedFilter[], sharedFilter: SharedFilter) =>
|
|
|
12
16
|
}
|
|
13
17
|
}
|
|
14
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Calculates and assigns tier values to all filters, returns the maximum tier.
|
|
21
|
+
* Mutates the filter objects by setting their tier property.
|
|
22
|
+
*/
|
|
15
23
|
function getMaxTierAndSetFilterTiers(filters: SharedFilter[]): number {
|
|
16
24
|
let maxTier = 1
|
|
17
25
|
filters.forEach(sharedFilter => {
|
|
@@ -23,7 +31,30 @@ function getMaxTierAndSetFilterTiers(filters: SharedFilter[]): number {
|
|
|
23
31
|
return maxTier
|
|
24
32
|
}
|
|
25
33
|
|
|
26
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Checks if a filter is currently at its reset/incomplete state.
|
|
36
|
+
* A filter is incomplete if it's visible AND:
|
|
37
|
+
* - The active value is empty/null/undefined, OR
|
|
38
|
+
* - The active value equals the resetLabel (if one is defined)
|
|
39
|
+
*/
|
|
40
|
+
export const isFilterAtResetState = (filter: SharedFilter): boolean => {
|
|
41
|
+
// Only check filters that are visible to the user
|
|
42
|
+
if (!filter.showDropdown) return false
|
|
43
|
+
|
|
44
|
+
// Check if active value is empty/null/undefined
|
|
45
|
+
const isEmptyValue = filter.active === '' || filter.active === null || filter.active === undefined
|
|
46
|
+
|
|
47
|
+
// Check if active value equals the resetLabel
|
|
48
|
+
const equalsResetLabel = filter.resetLabel && filter.resetLabel === filter.active
|
|
49
|
+
|
|
50
|
+
return isEmptyValue || equalsResetLabel
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Filters data by applying filters of a specific tier.
|
|
55
|
+
* Filters are applied hierarchically by tier to handle parent-child dependencies.
|
|
56
|
+
*/
|
|
57
|
+
function filterDataByTier(data = [], filters: SharedFilter[], tier: number) {
|
|
27
58
|
const activeFilters = _.filter(filters, f => (f.resetLabel === f.active ? f.values?.includes(f.resetLabel) : true))
|
|
28
59
|
return data.filter(row => {
|
|
29
60
|
const foundMatchingFilter = activeFilters.find(filter => {
|
|
@@ -51,8 +82,9 @@ function filter(data = [], filters: SharedFilter[], condition) {
|
|
|
51
82
|
isNotTheSelectedValue = subGroupActive && selectedSubGroupValue !== subGroupActive
|
|
52
83
|
}
|
|
53
84
|
|
|
54
|
-
const
|
|
55
|
-
|
|
85
|
+
const isMatchingTier = filter.tier === tier
|
|
86
|
+
// Only apply client-side filtering for datafilter (urlfilters modify the API endpoint instead)
|
|
87
|
+
if (filter.type !== 'urlfilter' && isMatchingTier && isNotTheSelectedValue) {
|
|
56
88
|
return true
|
|
57
89
|
}
|
|
58
90
|
})
|
|
@@ -60,17 +92,30 @@ function filter(data = [], filters: SharedFilter[], condition) {
|
|
|
60
92
|
})
|
|
61
93
|
}
|
|
62
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Filters data based on shared filter configurations.
|
|
97
|
+
* Returns empty array if any filter is at its reset state (incomplete selection).
|
|
98
|
+
* Otherwise applies filters hierarchically by tier to handle parent-child dependencies.
|
|
99
|
+
*/
|
|
63
100
|
export const filterData = (filters: SharedFilter[], _data: Object[]): Object[] => {
|
|
64
101
|
const maxTier = getMaxTierAndSetFilterTiers(filters)
|
|
65
102
|
|
|
103
|
+
// Check if any filters are currently at their reset state
|
|
104
|
+
const hasResetFilters = filters.some(isFilterAtResetState)
|
|
105
|
+
|
|
106
|
+
// If any filter is at reset state, return empty data to show "no data" message
|
|
107
|
+
if (hasResetFilters) {
|
|
108
|
+
return []
|
|
109
|
+
}
|
|
110
|
+
|
|
66
111
|
for (let i = 0; i < maxTier; i++) {
|
|
67
112
|
const lastIteration = i === maxTier - 1
|
|
68
113
|
|
|
69
|
-
const filteredData =
|
|
114
|
+
const filteredData = filterDataByTier(_data, filters, i + 1)
|
|
70
115
|
|
|
71
116
|
if (lastIteration) {
|
|
72
|
-
// not sure if this last run of
|
|
73
|
-
return
|
|
117
|
+
// not sure if this last run of filterDataByTier() function is necessary.
|
|
118
|
+
return filterDataByTier(filteredData, filters, maxTier - 1)
|
|
74
119
|
}
|
|
75
120
|
}
|
|
76
121
|
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { SharedFilter } from '../types/SharedFilter'
|
|
2
|
+
import { APIFilterDropdowns } from '../components/DashboardFilters'
|
|
3
|
+
import { FILTER_STYLE } from '../types/FilterStyles'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Determines the reset value for a filter based on its configuration.
|
|
7
|
+
* When forceEmpty is true (like when clicking "Clear Filters"), always returns empty.
|
|
8
|
+
* Otherwise uses priority: defaultValue > empty string (for resetLabel) > first API option
|
|
9
|
+
* Note: resetLabel is for display purposes only. When present, we return empty string
|
|
10
|
+
* so the placeholder option renders correctly in the dropdown.
|
|
11
|
+
*/
|
|
12
|
+
export const getFilterResetValue = (
|
|
13
|
+
filter: SharedFilter,
|
|
14
|
+
apiFilterDropdowns: APIFilterDropdowns,
|
|
15
|
+
forceEmpty: boolean = false
|
|
16
|
+
): string | undefined => {
|
|
17
|
+
// When clearing filters, always reset to empty/resetLabel state
|
|
18
|
+
if (forceEmpty) {
|
|
19
|
+
// Return empty string to show reset label or placeholder, undefined falls back to first value
|
|
20
|
+
return ''
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// If filter has a defaultValue, use that (for initial load)
|
|
24
|
+
if (filter.defaultValue) {
|
|
25
|
+
return filter.defaultValue
|
|
26
|
+
}
|
|
27
|
+
// If filter has a resetLabel, return empty string so placeholder renders
|
|
28
|
+
if (typeof filter.resetLabel === 'string') {
|
|
29
|
+
return ''
|
|
30
|
+
}
|
|
31
|
+
// Otherwise, use first available value if API filter
|
|
32
|
+
if (filter.apiFilter) {
|
|
33
|
+
const _key = filter.apiFilter.apiEndpoint
|
|
34
|
+
const options = apiFilterDropdowns[_key]
|
|
35
|
+
if (options && options.length > 0) {
|
|
36
|
+
return options[0].value
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return undefined
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Resets a filter's active and queuedActive values based on its filter style.
|
|
44
|
+
* Handles multi-select, nested-dropdown, and standard dropdown styles.
|
|
45
|
+
*/
|
|
46
|
+
export const resetFilterToValue = (
|
|
47
|
+
filter: SharedFilter,
|
|
48
|
+
resetValue: string | undefined,
|
|
49
|
+
apiFilterDropdowns: APIFilterDropdowns
|
|
50
|
+
): void => {
|
|
51
|
+
// Handle multi-select filters
|
|
52
|
+
if (filter.filterStyle === FILTER_STYLE.multiSelect) {
|
|
53
|
+
filter.active = resetValue ? [resetValue] : []
|
|
54
|
+
filter.queuedActive = undefined
|
|
55
|
+
} else if (filter.filterStyle === FILTER_STYLE.nestedDropdown) {
|
|
56
|
+
// For nested dropdowns, reset both group and subgroup
|
|
57
|
+
const _key = filter.apiFilter?.apiEndpoint
|
|
58
|
+
const options = apiFilterDropdowns[_key]
|
|
59
|
+
|
|
60
|
+
// When resetValue is explicitly empty/undefined, clear both group and subgroup
|
|
61
|
+
if (resetValue === '' || resetValue === undefined) {
|
|
62
|
+
filter.active = resetValue || ''
|
|
63
|
+
filter.subGrouping.active = ''
|
|
64
|
+
} else if (options && options.length > 0) {
|
|
65
|
+
// Use specific resetValue or fall back to first option
|
|
66
|
+
const selectedOption = options.find(opt => opt.value === resetValue) || options[0]
|
|
67
|
+
filter.active = selectedOption.value
|
|
68
|
+
if (selectedOption.subOptions && selectedOption.subOptions.length > 0) {
|
|
69
|
+
filter.subGrouping.active = selectedOption.subOptions[0].value
|
|
70
|
+
} else {
|
|
71
|
+
filter.subGrouping.active = ''
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
// No options available, use resetValue or empty
|
|
75
|
+
filter.active = resetValue || ''
|
|
76
|
+
filter.subGrouping.active = ''
|
|
77
|
+
}
|
|
78
|
+
filter.queuedActive = undefined
|
|
79
|
+
} else {
|
|
80
|
+
// Standard dropdown
|
|
81
|
+
filter.active = resetValue
|
|
82
|
+
filter.queuedActive = undefined
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Clears dropdown cache for child filters that have parent dependencies.
|
|
88
|
+
* Sets them to empty arrays so they appear disabled without loading state.
|
|
89
|
+
*/
|
|
90
|
+
export const clearChildFilterDropdowns = (
|
|
91
|
+
sharedFilters: SharedFilter[],
|
|
92
|
+
apiFilterDropdowns: APIFilterDropdowns
|
|
93
|
+
): APIFilterDropdowns => {
|
|
94
|
+
const updatedDropdowns = { ...apiFilterDropdowns }
|
|
95
|
+
sharedFilters.forEach(filter => {
|
|
96
|
+
if (filter.apiFilter && filter.parents && filter.parents.length > 0) {
|
|
97
|
+
// Set to empty array so they show as disabled without loading state
|
|
98
|
+
updatedDropdowns[filter.apiFilter.apiEndpoint] = []
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
return updatedDropdowns
|
|
102
|
+
}
|
|
@@ -8,7 +8,7 @@ import { AnyVisualization } from '@cdc/core/types/Visualization'
|
|
|
8
8
|
|
|
9
9
|
const transform = new DataTransform()
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
const getFootnotesVizConfig = (
|
|
12
12
|
visualizationConfig: AnyVisualization,
|
|
13
13
|
rowNumber: number,
|
|
14
14
|
config: MultiDashboardConfig,
|
|
@@ -100,7 +100,7 @@ export const getVizConfig = (
|
|
|
100
100
|
const dataKey = visualizationConfig.dataKey || 'backwards-compatibility'
|
|
101
101
|
// Markup-includes need data even when shared filters exist (for markup variables)
|
|
102
102
|
const shouldClearData = sharedFilterColumns.length && visualizationConfig.type !== 'markup-include'
|
|
103
|
-
visualizationConfig.data = data[dataKey] || []
|
|
103
|
+
visualizationConfig.data = shouldClearData ? [] : data[dataKey] || []
|
|
104
104
|
if (visualizationConfig.formattedData) {
|
|
105
105
|
visualizationConfig.formattedData =
|
|
106
106
|
transform.developerStandardize(visualizationConfig.data, visualizationConfig.dataDescription) ||
|