@cdc/core 4.25.11 → 4.26.1

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 (77) hide show
  1. package/_stories/Gallery.Charts.stories.tsx +307 -0
  2. package/_stories/Gallery.DataBite.stories.tsx +72 -0
  3. package/_stories/Gallery.Maps.stories.tsx +230 -0
  4. package/_stories/Gallery.WaffleChart.stories.tsx +187 -0
  5. package/_stories/PageART.stories.tsx +192 -0
  6. package/_stories/PageBRFSS.stories.tsx +289 -0
  7. package/_stories/PageCancerRegistries.stories.tsx +199 -0
  8. package/_stories/PageEasternEquineEncephalitis.stories.tsx +202 -0
  9. package/_stories/PageExcessiveAlcoholUse.stories.tsx +196 -0
  10. package/_stories/PageMaternalMortality.stories.tsx +192 -0
  11. package/_stories/PageOralHealth.stories.tsx +196 -0
  12. package/_stories/PageRespiratory.stories.tsx +332 -0
  13. package/_stories/PageSmokingTobacco.stories.tsx +195 -0
  14. package/_stories/PageStateDiabetesProfiles.stories.tsx +196 -0
  15. package/_stories/PageWastewater.stories.tsx +463 -0
  16. package/assets/icon-magnifying-glass.svg +5 -0
  17. package/assets/icon-warming-stripes.svg +13 -0
  18. package/components/AdvancedEditor/AdvancedEditor.tsx +4 -0
  19. package/components/AdvancedEditor/EmbedEditor.tsx +281 -0
  20. package/components/ComboBox/ComboBox.tsx +345 -0
  21. package/components/ComboBox/combobox.styles.css +185 -0
  22. package/components/ComboBox/index.ts +1 -0
  23. package/components/DataTable/DataTable.tsx +132 -58
  24. package/components/DataTable/data-table.css +216 -215
  25. package/components/DataTable/helpers/mapCellMatrix.tsx +14 -6
  26. package/components/EditorPanel/ColumnsEditor.tsx +37 -19
  27. package/components/EditorPanel/DataTableEditor.tsx +51 -25
  28. package/components/EditorPanel/EditorPanel.styles.css +16 -0
  29. package/components/EditorPanel/EditorPanel.tsx +144 -0
  30. package/components/EditorPanel/EditorPanelDispatch.tsx +75 -0
  31. package/components/EditorPanel/FieldSetWrapper.tsx +66 -23
  32. package/components/EditorPanel/Inputs.tsx +33 -7
  33. package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +236 -175
  34. package/components/EditorPanel/sections/VisualSection.tsx +169 -0
  35. package/components/Filters/Filters.tsx +31 -5
  36. package/components/Filters/helpers/getNestedOptions.ts +2 -1
  37. package/components/Filters/helpers/handleSorting.ts +1 -1
  38. package/components/Layout/components/Sidebar/components/sidebar.styles.scss +82 -0
  39. package/components/Layout/components/Visualization/index.tsx +16 -1
  40. package/components/Layout/components/Visualization/visualizations.scss +7 -0
  41. package/components/Legend/Legend.Gradient.tsx +1 -1
  42. package/components/MediaControls.tsx +53 -27
  43. package/components/ui/Icon.tsx +3 -1
  44. package/components/ui/Title/index.tsx +30 -2
  45. package/components/ui/Title/title.styles.css +42 -0
  46. package/dist/cove-main.css +26 -3
  47. package/dist/cove-main.css.map +1 -1
  48. package/generateViteConfig.js +8 -1
  49. package/helpers/addValuesToFilters.ts +6 -1
  50. package/helpers/coveUpdateWorker.ts +19 -12
  51. package/helpers/embedCodeGenerator.ts +109 -0
  52. package/helpers/getUniqueValues.ts +19 -0
  53. package/helpers/hashObj.ts +25 -0
  54. package/helpers/isRightAlignedTableValue.js +5 -0
  55. package/helpers/metrics/helpers.ts +1 -0
  56. package/helpers/pivotData.ts +2 -2
  57. package/helpers/prepareScreenshot.ts +268 -0
  58. package/helpers/queryStringUtils.ts +29 -0
  59. package/helpers/tests/prepareScreenshot.test.ts +414 -0
  60. package/helpers/tests/queryStringUtils.test.ts +381 -0
  61. package/helpers/tests/testStandaloneBuild.ts +23 -5
  62. package/helpers/useDataVizClasses.ts +0 -1
  63. package/helpers/ver/4.26.1.ts +80 -0
  64. package/hooks/useDataColumns.ts +63 -0
  65. package/hooks/useFilterManagement.ts +94 -0
  66. package/hooks/useLegendSeparators.ts +26 -0
  67. package/hooks/useListManagement.ts +192 -0
  68. package/package.json +4 -3
  69. package/styles/_button-section.scss +0 -3
  70. package/types/Axis.ts +1 -0
  71. package/types/ForecastingSeriesKey.ts +1 -0
  72. package/types/MarkupInclude.ts +1 -0
  73. package/types/Series.ts +3 -0
  74. package/types/Table.ts +1 -0
  75. package/types/Visualization.ts +1 -0
  76. package/types/VizFilter.ts +1 -0
  77. package/LICENSE +0 -201
@@ -0,0 +1,192 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { within, expect } from 'storybook/test'
3
+ import Dashboard from '@cdc/dashboard'
4
+ import { useEffect, useState } from 'react'
5
+
6
+ // Fallback step function for test descriptions
7
+ const step = async (description: string, fn: () => Promise<void> | void) => {
8
+ console.log(`▶ ${description}`)
9
+ await fn()
10
+ console.log(`✓ ${description}`)
11
+ }
12
+
13
+ const meta: Meta = {
14
+ title: 'Regression Tests/Pages/Maternal Mortality',
15
+ parameters: {
16
+ layout: 'fullscreen',
17
+ docs: {
18
+ description: {
19
+ component: 'Stories for visualizations from the CDC Pregnancy Mortality Surveillance System (PMSS) page (https://www.cdc.gov/maternal-mortality/php/pregnancy-mortality-surveillance-data/index.html)'
20
+ }
21
+ }
22
+ },
23
+ tags: ['autodocs']
24
+ }
25
+
26
+ export default meta
27
+
28
+ // Config URL from the maternal mortality surveillance data page
29
+ const CONFIG_URLS = {
30
+ pmssDashboard: 'https://www.cdc.gov/maternal-mortality/dfe-module/data-mmrc/pmss-data-dashboard.json'
31
+ }
32
+
33
+ // Helper to fetch config and update data URLs to use absolute cdc.gov paths
34
+ const useConfigWithAbsoluteDataUrl = (configUrl: string) => {
35
+ const [config, setConfig] = useState(null)
36
+
37
+ useEffect(() => {
38
+ fetch(configUrl)
39
+ .then(res => res.json())
40
+ .then(data => {
41
+ // Convert relative data URLs to absolute cdc.gov URLs
42
+ if (data.dataUrl) {
43
+ // Handle different relative path formats (../../path or /path)
44
+ const dataUrl = data.dataUrl.replace(/^(\.\.\/)+/, '').replace(/^\//, '')
45
+ data.dataUrl = `https://www.cdc.gov/${dataUrl}`
46
+ }
47
+ if (data.dataFileName) {
48
+ const dataFileName = data.dataFileName.replace(/^(\.\.\/)+/, '').replace(/^\//, '')
49
+ data.dataFileName = `https://www.cdc.gov/${dataFileName}`
50
+ }
51
+
52
+ // For dashboard configs with multiDashboards, convert dataKey references in visualizations
53
+ if (data.multiDashboards) {
54
+ data.multiDashboards.forEach((dashboard: any) => {
55
+ if (dashboard.visualizations) {
56
+ Object.values(dashboard.visualizations).forEach((viz: any) => {
57
+ // Only convert dataKey if it's a URL path (starts with / or ../)
58
+ if (viz.dataKey && (viz.dataKey.startsWith('/') || viz.dataKey.startsWith('../'))) {
59
+ const dataKey = viz.dataKey.replace(/^(\.\.\/)+/, '').replace(/^\//, '')
60
+ viz.dataKey = `https://www.cdc.gov/${dataKey}`
61
+ }
62
+ })
63
+ }
64
+ })
65
+ }
66
+
67
+ // For dashboard configs, convert dataKey references in visualizations
68
+ if (data.visualizations) {
69
+ Object.values(data.visualizations).forEach((viz: any) => {
70
+ // Only convert dataKey if it's a URL path (starts with / or ../)
71
+ if (viz.dataKey && (viz.dataKey.startsWith('/') || viz.dataKey.startsWith('../'))) {
72
+ const dataKey = viz.dataKey.replace(/^(\.\.\/)+/, '').replace(/^\//, '')
73
+ viz.dataKey = `https://www.cdc.gov/${dataKey}`
74
+ }
75
+ })
76
+ }
77
+
78
+ // For dashboard configs, convert datasets only if they reference external files
79
+ if (data.datasets) {
80
+ const newDatasets = {}
81
+ Object.entries(data.datasets).forEach(([key, dataset]: [string, any]) => {
82
+ // Check if dataset has embedded data
83
+ const hasEmbeddedData = (dataset as any).data && Array.isArray((dataset as any).data)
84
+
85
+ // If data is embedded, keep the original key
86
+ if (hasEmbeddedData) {
87
+ newDatasets[key] = dataset
88
+ } else {
89
+ // Otherwise, convert paths to absolute URLs
90
+ const newKey = key.replace(/^(\.\.\/)+/, '').replace(/^\//, '')
91
+ const absoluteKey = `https://www.cdc.gov/${newKey}`
92
+
93
+ newDatasets[absoluteKey] = {
94
+ ...dataset,
95
+ dataFileName: (dataset as any).dataFileName
96
+ ? `https://www.cdc.gov/${(dataset as any).dataFileName.replace(/^(\.\.\/)+/, '').replace(/^\//, '')}`
97
+ : (dataset as any).dataFileName,
98
+ dataUrl: (dataset as any).dataUrl
99
+ ? `https://www.cdc.gov/${(dataset as any).dataUrl.replace(/^(\.\.\/)+/, '').replace(/^\//, '')}`
100
+ : (dataset as any).dataUrl
101
+ }
102
+ }
103
+ })
104
+ data.datasets = newDatasets
105
+ }
106
+
107
+ // Set activeDashboard to 0 if it's null and multiDashboards exist
108
+ if (data.multiDashboards && data.multiDashboards.length > 0 && data.activeDashboard === null) {
109
+ data.activeDashboard = 0
110
+ }
111
+
112
+ // Log config info for debugging
113
+ console.log('✓ Config loaded:', {
114
+ hasMultiDashboards: !!data.multiDashboards,
115
+ dashboardCount: data.multiDashboards?.length || 0,
116
+ activeDashboard: data.activeDashboard,
117
+ datasetCount: Object.keys(data.datasets || {}).length,
118
+ firstDashboardVizCount: data.multiDashboards?.[0]?.visualizations
119
+ ? Object.keys(data.multiDashboards[0].visualizations).length
120
+ : 0
121
+ })
122
+
123
+ setConfig(data)
124
+ })
125
+ .catch(err => {
126
+ console.error('Failed to fetch config:', configUrl, err)
127
+ })
128
+ }, [configUrl])
129
+
130
+ return config
131
+ }
132
+
133
+ type DashboardStory = StoryObj<typeof Dashboard>
134
+
135
+ // Helper function to test dashboard rendering
136
+ const testDashboardRendering = async (canvasElement: HTMLElement, storyName: string) => {
137
+ await step('Wait for dashboard to render', async () => {
138
+ await new Promise<void>((resolve, reject) => {
139
+ const startTime = Date.now()
140
+ const timeout = 20000
141
+
142
+ const checkDashboard = () => {
143
+ const dashboardElement = canvasElement.querySelector('.cove-dashboard')
144
+ const loadingDiv = canvasElement.querySelector('div')
145
+
146
+ // Log current state for debugging
147
+ if (!dashboardElement && loadingDiv?.textContent?.includes('Loading')) {
148
+ console.log('Still loading config...')
149
+ }
150
+
151
+ if (dashboardElement) {
152
+ resolve()
153
+ } else if (Date.now() - startTime > timeout) {
154
+ reject(new Error(`Timeout: Dashboard element not found after ${timeout}ms`))
155
+ } else {
156
+ setTimeout(checkDashboard, 100)
157
+ }
158
+ }
159
+ checkDashboard()
160
+ })
161
+ })
162
+
163
+ await step('Verify dashboard wrapper is present', async () => {
164
+ const dashboard = canvasElement.querySelector('.cove-dashboard')
165
+ expect(dashboard).toBeInTheDocument()
166
+ })
167
+
168
+ await step('Verify at least one visualization rendered', async () => {
169
+ const coveModules = canvasElement.querySelectorAll('.cdc-open-viz-module')
170
+ expect(coveModules.length).toBeGreaterThan(0)
171
+ })
172
+
173
+ console.log(` ${storyName} dashboard rendered successfully`)
174
+ }
175
+
176
+ /**
177
+ * PMSS Data Dashboard
178
+ *
179
+ * Interactive dashboard showing Pregnancy Mortality Surveillance System (PMSS) data,
180
+ * including pregnancy-related mortality ratios, trends over time, and demographic breakdowns.
181
+ * This surveillance system monitors pregnancy-related deaths in the United States.
182
+ */
183
+ export const PMSS_Dashboard: DashboardStory = {
184
+ render: () => {
185
+ const config = useConfigWithAbsoluteDataUrl(CONFIG_URLS.pmssDashboard)
186
+ if (!config) return <div>Loading...</div>
187
+ return <Dashboard config={config} />
188
+ },
189
+ play: async ({ canvasElement }) => {
190
+ await testDashboardRendering(canvasElement, 'PMSS Dashboard')
191
+ }
192
+ }
@@ -0,0 +1,196 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { within, expect } from 'storybook/test'
3
+ import Dashboard from '@cdc/dashboard'
4
+ import { useEffect, useState } from 'react'
5
+
6
+ // Fallback step function for test descriptions
7
+ const step = async (description: string, fn: () => Promise<void> | void) => {
8
+ console.log(`▶ ${description}`)
9
+ await fn()
10
+ console.log(`✓ ${description}`)
11
+ }
12
+
13
+ const meta: Meta = {
14
+ title: 'Regression Tests/Pages/Oral Health',
15
+ parameters: {
16
+ layout: 'fullscreen',
17
+ docs: {
18
+ description: {
19
+ component: 'Stories for visualizations from the CDC Oral Health Data page (https://www.cdc.gov/oral-health-data-systems/oral_health_data/oral-health-dashboard.html)'
20
+ }
21
+ }
22
+ },
23
+ tags: ['autodocs']
24
+ }
25
+
26
+ export default meta
27
+
28
+ // Config URL from the oral health data page
29
+ const CONFIG_URLS = {
30
+ oralHealthData: 'https://www.cdc.gov/oral-health-data-systems/oral_health_data/modules/oral-health-data.json'
31
+ }
32
+
33
+ // Helper to fetch config and update data URLs to use absolute cdc.gov paths
34
+ const useConfigWithAbsoluteDataUrl = (configUrl: string) => {
35
+ const [config, setConfig] = useState(null)
36
+
37
+ useEffect(() => {
38
+ fetch(configUrl)
39
+ .then(res => res.json())
40
+ .then(data => {
41
+ // Convert relative data URLs to absolute cdc.gov URLs
42
+ if (data.dataUrl) {
43
+ // Handle different relative path formats (../../path or /path)
44
+ const dataUrl = data.dataUrl.replace(/^(\.\.\/)+/, '').replace(/^\//, '')
45
+ data.dataUrl = `https://www.cdc.gov/${dataUrl}`
46
+ }
47
+ if (data.dataFileName) {
48
+ const dataFileName = data.dataFileName.replace(/^(\.\.\/)+/, '').replace(/^\//, '')
49
+ data.dataFileName = `https://www.cdc.gov/${dataFileName}`
50
+ }
51
+
52
+ // For dashboard configs with multiDashboards, convert dataKey references in visualizations
53
+ if (data.multiDashboards) {
54
+ data.multiDashboards.forEach((dashboard: any) => {
55
+ if (dashboard.visualizations) {
56
+ Object.values(dashboard.visualizations).forEach((viz: any) => {
57
+ // Only convert dataKey if it's a URL path (starts with / or ../)
58
+ if (viz.dataKey && (viz.dataKey.startsWith('/') || viz.dataKey.startsWith('../'))) {
59
+ const dataKey = viz.dataKey.replace(/^(\.\.\/)+/, '').replace(/^\//, '')
60
+ viz.dataKey = `https://www.cdc.gov/${dataKey}`
61
+ }
62
+ })
63
+ }
64
+ })
65
+ }
66
+
67
+ // For dashboard configs, convert dataKey references in visualizations
68
+ if (data.visualizations) {
69
+ Object.values(data.visualizations).forEach((viz: any) => {
70
+ // Only convert dataKey if it's a URL path (starts with / or ../)
71
+ if (viz.dataKey && (viz.dataKey.startsWith('/') || viz.dataKey.startsWith('../'))) {
72
+ const dataKey = viz.dataKey.replace(/^(\.\.\/)+/, '').replace(/^\//, '')
73
+ viz.dataKey = `https://www.cdc.gov/${dataKey}`
74
+ }
75
+ })
76
+ }
77
+
78
+ // For dashboard configs, convert datasets only if they reference external files
79
+ if (data.datasets) {
80
+ const newDatasets = {}
81
+ Object.entries(data.datasets).forEach(([key, dataset]: [string, any]) => {
82
+ // Check if dataset has embedded data
83
+ const hasEmbeddedData = (dataset as any).data && Array.isArray((dataset as any).data)
84
+
85
+ // If data is embedded, keep the original key
86
+ if (hasEmbeddedData) {
87
+ newDatasets[key] = dataset
88
+ } else {
89
+ // Otherwise, convert paths to absolute URLs (but keep absolute URLs as-is)
90
+ const newKey = key.replace(/^(\.\.\/)+/, '').replace(/^\//, '')
91
+ const absoluteKey = key.startsWith('http') ? key : `https://www.cdc.gov/${newKey}`
92
+
93
+ newDatasets[absoluteKey] = {
94
+ ...dataset,
95
+ dataFileName: (dataset as any).dataFileName && !(dataset as any).dataFileName.startsWith('http')
96
+ ? `https://www.cdc.gov/${(dataset as any).dataFileName.replace(/^(\.\.\/)+/, '').replace(/^\//, '')}`
97
+ : (dataset as any).dataFileName,
98
+ dataUrl: (dataset as any).dataUrl && !(dataset as any).dataUrl.startsWith('http')
99
+ ? `https://www.cdc.gov/${(dataset as any).dataUrl.replace(/^(\.\.\/)+/, '').replace(/^\//, '')}`
100
+ : (dataset as any).dataUrl
101
+ }
102
+ }
103
+ })
104
+ data.datasets = newDatasets
105
+ }
106
+
107
+ // Set activeDashboard to 0 if it's null and multiDashboards exist
108
+ if (data.multiDashboards && data.multiDashboards.length > 0 && data.activeDashboard === null) {
109
+ data.activeDashboard = 0
110
+ }
111
+
112
+ // Log config info for debugging
113
+ console.log('✓ Config loaded:', {
114
+ type: data.type,
115
+ hasMultiDashboards: !!data.multiDashboards,
116
+ dashboardCount: data.multiDashboards?.length || 0,
117
+ activeDashboard: data.activeDashboard,
118
+ datasetCount: Object.keys(data.datasets || {}).length,
119
+ dashboardLabels: data.multiDashboards?.map((d: any) => d.label)
120
+ })
121
+
122
+ setConfig(data)
123
+ })
124
+ .catch(err => {
125
+ console.error('Failed to fetch config:', configUrl, err)
126
+ })
127
+ }, [configUrl])
128
+
129
+ return config
130
+ }
131
+
132
+ type DashboardStory = StoryObj<typeof Dashboard>
133
+
134
+ // Helper function to test dashboard rendering
135
+ const testDashboardRendering = async (canvasElement: HTMLElement, storyName: string) => {
136
+ await step('Wait for dashboard to render', async () => {
137
+ await new Promise<void>((resolve, reject) => {
138
+ const startTime = Date.now()
139
+ const timeout = 30000 // Longer timeout for external data loading
140
+
141
+ const checkDashboard = () => {
142
+ const dashboardElement = canvasElement.querySelector('.cove-dashboard')
143
+ const loadingDiv = canvasElement.querySelector('div')
144
+
145
+ // Log current state for debugging
146
+ if (!dashboardElement && loadingDiv?.textContent?.includes('Loading')) {
147
+ console.log('Still loading config...')
148
+ }
149
+
150
+ if (dashboardElement) {
151
+ resolve()
152
+ } else if (Date.now() - startTime > timeout) {
153
+ reject(new Error(`Timeout: Dashboard element not found after ${timeout}ms`))
154
+ } else {
155
+ setTimeout(checkDashboard, 100)
156
+ }
157
+ }
158
+ checkDashboard()
159
+ })
160
+ })
161
+
162
+ await step('Verify dashboard wrapper is present', async () => {
163
+ const dashboard = canvasElement.querySelector('.cove-dashboard')
164
+ expect(dashboard).toBeInTheDocument()
165
+ })
166
+
167
+ await step('Verify at least one visualization rendered', async () => {
168
+ const coveModules = canvasElement.querySelectorAll('.cdc-open-viz-module')
169
+ expect(coveModules.length).toBeGreaterThan(0)
170
+ })
171
+
172
+ console.log(` ${storyName} dashboard rendered successfully`)
173
+ }
174
+
175
+ /**
176
+ * Oral Health Data Dashboard
177
+ *
178
+ * Interactive dashboard showing oral health surveillance data from the National Oral Health
179
+ * Surveillance System (NOHSS). The dashboard includes 4 views:
180
+ * - All States: State-level oral health data across all U.S. states
181
+ * - Single State: Detailed data for a selected state
182
+ * - All Counties: County-level oral health data
183
+ * - Single County: Detailed data for a selected county
184
+ *
185
+ * Data includes oral health indicators, intervention strategies, and public health programs.
186
+ */
187
+ export const Oral_Health_Dashboard: DashboardStory = {
188
+ render: () => {
189
+ const config = useConfigWithAbsoluteDataUrl(CONFIG_URLS.oralHealthData)
190
+ if (!config) return <div>Loading...</div>
191
+ return <Dashboard config={config} />
192
+ },
193
+ play: async ({ canvasElement }) => {
194
+ await testDashboardRendering(canvasElement, 'Oral Health Dashboard')
195
+ }
196
+ }