@cdc/dashboard 4.25.8 → 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 +80040 -75976
- 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 +39112 -0
- package/examples/private/brfs-2.json +1532 -0
- package/examples/private/brfs.json +2128 -2138
- package/examples/private/clade-2.json +430 -0
- package/examples/private/delete.json +32919 -0
- package/examples/private/diabetes.json +5582 -0
- package/examples/private/example-2.json +49796 -0
- package/examples/private/group-legend-test.json +328 -0
- package/examples/private/map.json +1211 -0
- package/examples/private/markup-footer/burden_toolkit_mortality_diabetes_attributable_deaths_data.csv +14041 -0
- package/examples/private/markup-footer/burden_toolkit_mortality_diabetes_attributable_deaths_per_100000_data.csv +14041 -0
- package/examples/private/markup-footer/burden_toolkit_mortality_qaly_data.csv +18721 -0
- package/examples/private/markup-footer/burden_toolkit_mortality_yll_data.csv +18721 -0
- package/examples/private/markup-footer/mortality-deaths-footnotes-age.csv +3 -0
- package/examples/private/markup-variables.json +1451 -0
- package/examples/private/markup.json +5471 -0
- package/examples/private/mpox.json +38128 -0
- package/examples/private/north-dakota.json +1132 -0
- package/examples/private/ophdst.json +38754 -0
- package/examples/private/pedro.json +1 -0
- package/examples/private/pivot.json +683 -0
- package/examples/private/reset.json +32920 -0
- package/examples/private/sewershed.json +435 -0
- package/examples/private/tobacco.json +1938 -0
- package/examples/test-api-filter-reset.json +132 -0
- package/index.html +2 -2
- package/package.json +16 -10
- package/src/CdcDashboard.tsx +1 -3
- package/src/CdcDashboardComponent.tsx +34 -16
- package/src/DashboardContext.tsx +5 -1
- package/src/_stories/Dashboard.API.stories.tsx +62 -0
- package/src/_stories/Dashboard.stories.tsx +492 -472
- package/src/_stories/_mock/api/cessation.json +1 -0
- package/src/_stories/_mock/api/data-explorer.json +1 -0
- package/src/_stories/_mock/api/explore-by-location.json +1 -0
- package/src/_stories/_mock/api/explore-by-topic.json +1 -0
- package/src/_stories/_mock/api/legislation.json +1 -0
- package/src/_stories/_mock/api/oral-health-data.json +1 -0
- package/src/_stories/_mock/custom-order-new-values.json +116 -0
- package/src/components/CollapsibleVisualizationRow.tsx +1 -1
- package/src/components/DashboardFilters/DashboardFilters.tsx +34 -23
- package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +29 -12
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +81 -112
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/NestedDropDownDashboard.tsx +82 -52
- package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +130 -31
- package/src/components/DashboardFilters/_stories/DashboardFilters.stories.tsx +80 -21
- package/src/components/DataDesignerModal.tsx +227 -210
- package/src/components/Header/Header.tsx +13 -12
- package/src/components/Toggle/Toggle.tsx +48 -47
- package/src/components/VisualizationRow.tsx +13 -6
- package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +2 -0
- package/src/components/Widget/Widget.tsx +47 -18
- package/src/helpers/addValuesToDashboardFilters.ts +111 -60
- package/src/helpers/apiFilterHelpers.ts +190 -166
- package/src/helpers/filterData.ts +52 -7
- package/src/helpers/filterResetHelpers.ts +102 -0
- package/src/helpers/formatConfigBeforeSave.ts +137 -0
- package/src/helpers/getVizConfig.ts +36 -18
- package/src/helpers/loadAPIFilters.ts +109 -99
- package/src/helpers/reloadURLHelpers.ts +1 -1
- package/src/helpers/tests/filterResetHelpers.test.ts +532 -0
- package/src/helpers/tests/formatConfigBeforeSave.test.ts +69 -0
- package/src/index.tsx +1 -1
- package/src/scss/editor-panel.scss +3 -431
- package/src/scss/grid.scss +7 -5
- package/src/scss/main.scss +1 -24
- package/src/store/errorMessage/errorMessage.reducer.ts +1 -1
- package/src/types/DashboardFilters.ts +9 -8
- package/src/types/InitialState.ts +12 -12
- package/vite.config.js +1 -1
- package/vitest.config.ts +16 -0
- package/src/coreStyles_dashboard.scss +0 -3
- 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
|
@@ -7,6 +7,7 @@ import { DashboardContext, DashboardDispatchContext } from '../../DashboardConte
|
|
|
7
7
|
import { mapDataToConfig } from '../../helpers/mapDataToConfig'
|
|
8
8
|
import './visualizations-panel-styles.css'
|
|
9
9
|
import { MultiDashboardConfig } from '../../types/MultiDashboard'
|
|
10
|
+
import { stripConfig } from '../../helpers/formatConfigBeforeSave'
|
|
10
11
|
|
|
11
12
|
const addVisualization = (type, subType) => {
|
|
12
13
|
const modalWillOpen = type !== 'markup-include'
|
|
@@ -123,6 +124,7 @@ const VisualizationsPanel = () => {
|
|
|
123
124
|
loadConfig={loadConfig}
|
|
124
125
|
config={config}
|
|
125
126
|
convertStateToConfig={() => undefined}
|
|
127
|
+
stripConfig={stripConfig}
|
|
126
128
|
onExpandCollapse={() => {
|
|
127
129
|
setAdvancedEditing(!advancedEditing)
|
|
128
130
|
}}
|
|
@@ -15,6 +15,7 @@ import './widget.styles.css'
|
|
|
15
15
|
type WidgetConfig = AnyVisualization & { rowIdx: number; colIdx: number }
|
|
16
16
|
type WidgetProps = {
|
|
17
17
|
title: string
|
|
18
|
+
columnData?: any
|
|
18
19
|
widgetConfig?: WidgetConfig
|
|
19
20
|
addVisualization?: Function
|
|
20
21
|
type: string
|
|
@@ -32,7 +33,7 @@ const Widget = ({
|
|
|
32
33
|
toggleRow = false
|
|
33
34
|
}: WidgetProps) => {
|
|
34
35
|
const { overlay } = useGlobalContext()
|
|
35
|
-
const { config, data } = useContext(DashboardContext)
|
|
36
|
+
const { config, data, isEditor } = useContext(DashboardContext)
|
|
36
37
|
const dispatch = useContext(DashboardDispatchContext)
|
|
37
38
|
|
|
38
39
|
const [isEditing, setIsEditing] = useState(false)
|
|
@@ -79,25 +80,42 @@ const Widget = ({
|
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
const changeDataLimit = (dataUrl, limit) => {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
if (!dataUrl || typeof dataUrl !== 'string') {
|
|
84
|
+
console.error('Invalid dataUrl provided to changeDataLimit:', dataUrl)
|
|
85
|
+
return null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
// Handle relative URLs by resolving them against the current base URL
|
|
90
|
+
const url = new URL(dataUrl, window.location.origin)
|
|
91
|
+
url.searchParams.set('$limit', limit)
|
|
92
|
+
// Replace encoded $ with actual $ for the URL
|
|
93
|
+
return url.href.replace(/%24/g, '$')
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error('Failed to construct URL from dataUrl:', dataUrl, error)
|
|
96
|
+
return null
|
|
97
|
+
}
|
|
86
98
|
}
|
|
87
99
|
|
|
88
100
|
const loadSampleData = () => {
|
|
89
101
|
const dataKey = config.rows[widgetConfig.rowIdx]?.dataKey || widgetConfig?.dataKey
|
|
90
102
|
const dataset = config.datasets[dataKey]
|
|
91
103
|
const _data = data[dataKey]
|
|
92
|
-
if (dataKey && !_data?.length) {
|
|
104
|
+
if (dataKey && !_data?.length && dataset?.dataUrl) {
|
|
93
105
|
const url = changeDataLimit(dataset.dataUrl, 100)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
106
|
+
if (url) {
|
|
107
|
+
fetchRemoteData(url)
|
|
108
|
+
.then(responseData => {
|
|
109
|
+
// this sample data is temporary.
|
|
110
|
+
// the HEADER component removes the data when you toggle to the main viz panel.
|
|
111
|
+
// data will be cached only when it's loaded via dashboard preview.
|
|
112
|
+
responseData.sample = true
|
|
113
|
+
dispatch({ type: 'SET_DATA', payload: { ...data, [dataKey]: responseData } })
|
|
114
|
+
})
|
|
115
|
+
.catch(error => {
|
|
116
|
+
console.error('Failed to fetch sample data:', error)
|
|
117
|
+
})
|
|
118
|
+
}
|
|
101
119
|
}
|
|
102
120
|
}
|
|
103
121
|
|
|
@@ -117,11 +135,22 @@ const Widget = ({
|
|
|
117
135
|
} else {
|
|
118
136
|
if (widgetConfig?.formattedData) {
|
|
119
137
|
isConfigurationReady = true
|
|
120
|
-
} else if (widgetConfig?.dataKey && widgetConfig?.dataDescription
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (
|
|
124
|
-
|
|
138
|
+
} else if (widgetConfig?.dataKey && widgetConfig?.dataDescription) {
|
|
139
|
+
// In editor mode, having a dataKey and dataDescription is sufficient
|
|
140
|
+
// In non-editor mode, also check if data is actually loaded
|
|
141
|
+
if (isEditor || config.datasets[widgetConfig.dataKey]?.data) {
|
|
142
|
+
const dataToCheck = config.datasets[widgetConfig.dataKey]?.data
|
|
143
|
+
if (dataToCheck) {
|
|
144
|
+
const formattedDataAttempt = transform.autoStandardize(dataToCheck)
|
|
145
|
+
const canFormatData = !!transform.developerStandardize(formattedDataAttempt, widgetConfig.dataDescription)
|
|
146
|
+
if (canFormatData) {
|
|
147
|
+
isConfigurationReady = true
|
|
148
|
+
}
|
|
149
|
+
} else {
|
|
150
|
+
// In editor mode, if dataKey and dataDescription are configured but data isn't loaded yet,
|
|
151
|
+
// still mark as ready so the tools icon shows
|
|
152
|
+
isConfigurationReady = true
|
|
153
|
+
}
|
|
125
154
|
}
|
|
126
155
|
}
|
|
127
156
|
}
|
|
@@ -1,60 +1,111 @@
|
|
|
1
|
-
import _ from 'lodash'
|
|
2
|
-
import { getQueryStringFilterValue } from '@cdc/core/helpers/queryStringUtils'
|
|
3
|
-
import { SharedFilter } from '../types/SharedFilter'
|
|
4
|
-
import { handleSorting } from '@cdc/core/components/Filters'
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
datasets.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
return
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
filterCopy.active =
|
|
52
|
-
} else {
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
filterCopy.active =
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
1
|
+
import _ from 'lodash'
|
|
2
|
+
import { getQueryStringFilterValue } from '@cdc/core/helpers/queryStringUtils'
|
|
3
|
+
import { SharedFilter } from '../types/SharedFilter'
|
|
4
|
+
import { handleSorting } from '@cdc/core/components/Filters'
|
|
5
|
+
import { mergeCustomOrderValues } from '@cdc/core/helpers/mergeCustomOrderValues'
|
|
6
|
+
|
|
7
|
+
// Gets filter values from dataset
|
|
8
|
+
const generateValuesForFilter = (columnName: string, data: Record<string, any[]>) => {
|
|
9
|
+
const valuesSet = new Set<string>()
|
|
10
|
+
|
|
11
|
+
// Iterate over all data sets
|
|
12
|
+
const datasets = Object.values(data) || []
|
|
13
|
+
datasets.forEach((rows: any[]) => {
|
|
14
|
+
// Iterate over each row in the dataset
|
|
15
|
+
rows?.forEach(row => {
|
|
16
|
+
const value = row[columnName]
|
|
17
|
+
if (value !== undefined) {
|
|
18
|
+
// Normalize the value by trimming
|
|
19
|
+
const normalizedValue = String(value).trim()
|
|
20
|
+
valuesSet.add(normalizedValue)
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
// Convert Set back to array to return
|
|
26
|
+
return Array.from(valuesSet)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const getSelector = (filter: SharedFilter) => {
|
|
30
|
+
return filter.type === 'urlfilter' ? filter.apiFilter?.valueSelector : filter.columnName
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const addValuesToDashboardFilters = (
|
|
34
|
+
filters: SharedFilter[],
|
|
35
|
+
data: Record<string, any[]>,
|
|
36
|
+
filtersToSkip: number[] = []
|
|
37
|
+
): Array<SharedFilter> => {
|
|
38
|
+
const result = filters?.map((filter, index) => {
|
|
39
|
+
if (filtersToSkip.includes(index)) return filter
|
|
40
|
+
if (filter.type === 'urlfilter') return filter
|
|
41
|
+
const filterCopy = _.cloneDeep(filter)
|
|
42
|
+
const filterValues = generateValuesForFilter(getSelector(filter), data)
|
|
43
|
+
filterCopy.values = filterValues
|
|
44
|
+
|
|
45
|
+
// Merge new values with existing custom order (fixes DEV-11740 & DEV-11376)
|
|
46
|
+
filterCopy.orderedValues = mergeCustomOrderValues(filterValues, filterCopy.orderedValues, filterCopy.order)
|
|
47
|
+
|
|
48
|
+
if (filterValues.length > 0) {
|
|
49
|
+
const queryStringFilterValue = getQueryStringFilterValue(filterCopy)
|
|
50
|
+
if (queryStringFilterValue) {
|
|
51
|
+
filterCopy.active = queryStringFilterValue
|
|
52
|
+
} else if (filter.multiSelect) {
|
|
53
|
+
const defaultValues = filterCopy.values
|
|
54
|
+
const active: string[] = Array.isArray(filterCopy.active) ? filterCopy.active : [filterCopy.active]
|
|
55
|
+
filterCopy.active = active.filter(val => defaultValues.includes(val))
|
|
56
|
+
} else {
|
|
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
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Handle nested dropdown subGrouping.active property
|
|
84
|
+
if (filterCopy.subGrouping && filterCopy.subGrouping.valuesLookup) {
|
|
85
|
+
const groupName = filterCopy.active as string
|
|
86
|
+
const subGroupingFilter = {
|
|
87
|
+
...filterCopy.subGrouping,
|
|
88
|
+
values: filterCopy.subGrouping.valuesLookup[groupName]?.values || []
|
|
89
|
+
}
|
|
90
|
+
const queryStringFilterValue = getQueryStringFilterValue(subGroupingFilter)
|
|
91
|
+
const groupActive = groupName || filterCopy.values[0]
|
|
92
|
+
const defaultSubValue = filterCopy.subGrouping.valuesLookup[groupActive as string]?.values[0]
|
|
93
|
+
|
|
94
|
+
// Priority order: query string > existing active > configured default > first available value
|
|
95
|
+
let activeValue = queryStringFilterValue || filterCopy.subGrouping.active
|
|
96
|
+
|
|
97
|
+
// If we have a configured default value and it exists in the current group's options, use it
|
|
98
|
+
if (!activeValue && filterCopy.subGrouping.defaultValue) {
|
|
99
|
+
const currentGroupValues = filterCopy.subGrouping.valuesLookup[groupActive as string]?.values || []
|
|
100
|
+
if (currentGroupValues.includes(filterCopy.subGrouping.defaultValue)) {
|
|
101
|
+
activeValue = filterCopy.subGrouping.defaultValue
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
filterCopy.subGrouping.active = activeValue || defaultSubValue
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return handleSorting(filterCopy)
|
|
109
|
+
})
|
|
110
|
+
return result
|
|
111
|
+
}
|