@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.
Files changed (88) hide show
  1. package/dist/{cdcdashboard-fce76882.es.js → cdcdashboard-BnB1QM5d.es.js} +6 -13
  2. package/dist/{cdcdashboard-c55ac1ea.es.js → cdcdashboard-D6CG2-Hb.es.js} +5 -12
  3. package/dist/{cdcdashboard-31a33da1.es.js → cdcdashboard-MXgURbdZ.es.js} +6 -13
  4. package/dist/{cdcdashboard-1a1724a1.es.js → cdcdashboard-dgT_1dIT.es.js} +136 -151
  5. package/dist/cdcdashboard.js +80040 -75976
  6. package/examples/api-test/categories.json +18 -0
  7. package/examples/api-test/chart-data.json +602 -0
  8. package/examples/api-test/topics.json +47 -0
  9. package/examples/api-test/years.json +22 -0
  10. package/examples/markup-axis-label.json +4167 -0
  11. package/examples/private/DEV-10538.json +407 -0
  12. package/examples/private/DEV-11405.json +39112 -0
  13. package/examples/private/big-dashboard.json +39112 -0
  14. package/examples/private/brfs-2.json +1532 -0
  15. package/examples/private/brfs.json +2128 -2138
  16. package/examples/private/clade-2.json +430 -0
  17. package/examples/private/delete.json +32919 -0
  18. package/examples/private/diabetes.json +5582 -0
  19. package/examples/private/example-2.json +49796 -0
  20. package/examples/private/group-legend-test.json +328 -0
  21. package/examples/private/map.json +1211 -0
  22. package/examples/private/markup-footer/burden_toolkit_mortality_diabetes_attributable_deaths_data.csv +14041 -0
  23. package/examples/private/markup-footer/burden_toolkit_mortality_diabetes_attributable_deaths_per_100000_data.csv +14041 -0
  24. package/examples/private/markup-footer/burden_toolkit_mortality_qaly_data.csv +18721 -0
  25. package/examples/private/markup-footer/burden_toolkit_mortality_yll_data.csv +18721 -0
  26. package/examples/private/markup-footer/mortality-deaths-footnotes-age.csv +3 -0
  27. package/examples/private/markup-variables.json +1451 -0
  28. package/examples/private/markup.json +5471 -0
  29. package/examples/private/mpox.json +38128 -0
  30. package/examples/private/north-dakota.json +1132 -0
  31. package/examples/private/ophdst.json +38754 -0
  32. package/examples/private/pedro.json +1 -0
  33. package/examples/private/pivot.json +683 -0
  34. package/examples/private/reset.json +32920 -0
  35. package/examples/private/sewershed.json +435 -0
  36. package/examples/private/tobacco.json +1938 -0
  37. package/examples/test-api-filter-reset.json +132 -0
  38. package/index.html +2 -2
  39. package/package.json +16 -10
  40. package/src/CdcDashboard.tsx +1 -3
  41. package/src/CdcDashboardComponent.tsx +34 -16
  42. package/src/DashboardContext.tsx +5 -1
  43. package/src/_stories/Dashboard.API.stories.tsx +62 -0
  44. package/src/_stories/Dashboard.stories.tsx +492 -472
  45. package/src/_stories/_mock/api/cessation.json +1 -0
  46. package/src/_stories/_mock/api/data-explorer.json +1 -0
  47. package/src/_stories/_mock/api/explore-by-location.json +1 -0
  48. package/src/_stories/_mock/api/explore-by-topic.json +1 -0
  49. package/src/_stories/_mock/api/legislation.json +1 -0
  50. package/src/_stories/_mock/api/oral-health-data.json +1 -0
  51. package/src/_stories/_mock/custom-order-new-values.json +116 -0
  52. package/src/components/CollapsibleVisualizationRow.tsx +1 -1
  53. package/src/components/DashboardFilters/DashboardFilters.tsx +34 -23
  54. package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +29 -12
  55. package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +81 -112
  56. package/src/components/DashboardFilters/DashboardFiltersEditor/components/NestedDropDownDashboard.tsx +82 -52
  57. package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +130 -31
  58. package/src/components/DashboardFilters/_stories/DashboardFilters.stories.tsx +80 -21
  59. package/src/components/DataDesignerModal.tsx +227 -210
  60. package/src/components/Header/Header.tsx +13 -12
  61. package/src/components/Toggle/Toggle.tsx +48 -47
  62. package/src/components/VisualizationRow.tsx +13 -6
  63. package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +2 -0
  64. package/src/components/Widget/Widget.tsx +47 -18
  65. package/src/helpers/addValuesToDashboardFilters.ts +111 -60
  66. package/src/helpers/apiFilterHelpers.ts +190 -166
  67. package/src/helpers/filterData.ts +52 -7
  68. package/src/helpers/filterResetHelpers.ts +102 -0
  69. package/src/helpers/formatConfigBeforeSave.ts +137 -0
  70. package/src/helpers/getVizConfig.ts +36 -18
  71. package/src/helpers/loadAPIFilters.ts +109 -99
  72. package/src/helpers/reloadURLHelpers.ts +1 -1
  73. package/src/helpers/tests/filterResetHelpers.test.ts +532 -0
  74. package/src/helpers/tests/formatConfigBeforeSave.test.ts +69 -0
  75. package/src/index.tsx +1 -1
  76. package/src/scss/editor-panel.scss +3 -431
  77. package/src/scss/grid.scss +7 -5
  78. package/src/scss/main.scss +1 -24
  79. package/src/store/errorMessage/errorMessage.reducer.ts +1 -1
  80. package/src/types/DashboardFilters.ts +9 -8
  81. package/src/types/InitialState.ts +12 -12
  82. package/vite.config.js +1 -1
  83. package/vitest.config.ts +16 -0
  84. package/src/coreStyles_dashboard.scss +0 -3
  85. package/src/helpers/getAutoLoadVisualization.ts +0 -11
  86. package/src/scss/mixins.scss +0 -47
  87. package/src/scss/variables.scss +0 -5
  88. /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
- const url = new URL(dataUrl)
83
- url.searchParams.set('$limit', limit)
84
- // Replace encoded $ with actual $ for the URL
85
- return url.href.replace(/%24/g, '$')
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
- fetchRemoteData(url).then(responseData => {
95
- // this sample data is temporary.
96
- // the HEADER component removes the data when you toggle to the main viz panel.
97
- // data will be cached only when it's loaded via dashboard preview.
98
- responseData.sample = true
99
- dispatch({ type: 'SET_DATA', payload: { ...data, [dataKey]: responseData } })
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 && config.datasets[widgetConfig.dataKey]) {
121
- const formattedDataAttempt = transform.autoStandardize(config.datasets[widgetConfig.dataKey].data)
122
- const canFormatData = !!transform.developerStandardize(formattedDataAttempt, widgetConfig.dataDescription)
123
- if (canFormatData) {
124
- isConfigurationReady = true
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
- // Gets filter values from dataset
7
- const generateValuesForFilter = (columnName: string, data: Record<string, any[]>) => {
8
- const valuesSet = new Set<string>()
9
-
10
- // Iterate over all data sets
11
- const datasets = Object.values(data) || []
12
- datasets.forEach((rows: any[]) => {
13
- // Iterate over each row in the dataset
14
- rows?.forEach(row => {
15
- const value = row[columnName]
16
- if (value !== undefined) {
17
- // Normalize the value by trimming
18
- const normalizedValue = String(value).trim()
19
- valuesSet.add(normalizedValue)
20
- }
21
- })
22
- })
23
-
24
- // Convert Set back to array to return
25
- return Array.from(valuesSet)
26
- }
27
-
28
- const getSelector = (filter: SharedFilter) => {
29
- return filter.type === 'urlfilter' ? filter.apiFilter?.valueSelector : filter.columnName
30
- }
31
-
32
- export const addValuesToDashboardFilters = (
33
- filters: SharedFilter[],
34
- data: Record<string, any[]>,
35
- filtersToSkip: number[] = []
36
- ): Array<SharedFilter> => {
37
- return filters?.map((filter, index) => {
38
- if (filtersToSkip.includes(index)) return filter
39
- if (filter.type === 'urlfilter') return filter
40
- const filterCopy = _.cloneDeep(filter)
41
- const filterValues = generateValuesForFilter(getSelector(filter), data)
42
- filterCopy.values = filterValues
43
-
44
- if (filterValues.length > 0) {
45
- const queryStringFilterValue = getQueryStringFilterValue(filterCopy)
46
- if (queryStringFilterValue) {
47
- filterCopy.active = queryStringFilterValue
48
- } else if (filter.multiSelect) {
49
- const defaultValues = filterCopy.values
50
- const active: string[] = Array.isArray(filterCopy.active) ? filterCopy.active : [filterCopy.active]
51
- filterCopy.active = active.filter(val => defaultValues.includes(val))
52
- } else {
53
- const hasResetLabel = filters.find(filter => filter.resetLabel)
54
- const defaultValue = hasResetLabel ? hasResetLabel.resetLabel : filterCopy.active || filterCopy.values[0]
55
- filterCopy.active = filterCopy.defaultValue || defaultValue
56
- }
57
- }
58
- return handleSorting(filterCopy)
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
+ }