@cdc/core 4.25.10 → 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 (134) 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/_stories/StoryRenderingTests.stories.tsx +164 -0
  17. package/assets/icon-magnifying-glass.svg +5 -0
  18. package/assets/icon-warming-stripes.svg +13 -0
  19. package/components/AdvancedEditor/AdvancedEditor.tsx +7 -1
  20. package/components/AdvancedEditor/EmbedEditor.tsx +281 -0
  21. package/components/ComboBox/ComboBox.tsx +345 -0
  22. package/components/ComboBox/combobox.styles.css +185 -0
  23. package/components/ComboBox/index.ts +1 -0
  24. package/components/CustomColorsEditor/CustomColorsEditor.css +299 -0
  25. package/components/CustomColorsEditor/CustomColorsEditor.tsx +209 -0
  26. package/components/CustomColorsEditor/index.ts +1 -0
  27. package/components/DataTable/DataTable.tsx +132 -58
  28. package/components/DataTable/DataTableStandAlone.tsx +8 -3
  29. package/components/DataTable/components/DataTableEditorPanel.tsx +12 -2
  30. package/components/DataTable/data-table.css +217 -210
  31. package/components/DataTable/helpers/mapCellMatrix.tsx +28 -9
  32. package/components/DataTable/helpers/standardizeState.js +2 -2
  33. package/components/DataTable/helpers/tests/standardizeState.test.js +54 -0
  34. package/components/EditorPanel/ColumnsEditor.tsx +37 -19
  35. package/components/EditorPanel/DataTableEditor.tsx +54 -28
  36. package/components/EditorPanel/EditorPanel.styles.css +439 -0
  37. package/components/EditorPanel/EditorPanel.tsx +144 -0
  38. package/components/EditorPanel/EditorPanelDispatch.tsx +75 -0
  39. package/components/EditorPanel/FieldSetWrapper.tsx +66 -23
  40. package/components/EditorPanel/FootnotesEditor.tsx +44 -37
  41. package/components/EditorPanel/Inputs.tsx +44 -8
  42. package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +35 -62
  43. package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +246 -175
  44. package/components/EditorPanel/components/MarkupVariablesEditor.tsx +61 -22
  45. package/components/EditorPanel/sections/VisualSection.tsx +169 -0
  46. package/components/Filters/Filters.tsx +57 -10
  47. package/components/Filters/components/Dropdown.tsx +6 -1
  48. package/components/Filters/helpers/getNestedOptions.ts +2 -1
  49. package/components/Filters/helpers/handleSorting.ts +1 -1
  50. package/components/Footnotes/Footnotes.tsx +35 -25
  51. package/components/Footnotes/FootnotesStandAlone.tsx +42 -6
  52. package/components/HeaderThemeSelector/HeaderThemeSelector.css +43 -0
  53. package/components/HeaderThemeSelector/HeaderThemeSelector.stories.tsx +74 -0
  54. package/components/HeaderThemeSelector/HeaderThemeSelector.tsx +61 -0
  55. package/components/HeaderThemeSelector/index.ts +2 -0
  56. package/components/Layout/components/Sidebar/components/sidebar.styles.scss +82 -0
  57. package/components/Layout/components/Visualization/index.tsx +16 -1
  58. package/components/Layout/components/Visualization/visualizations.scss +7 -0
  59. package/components/Layout/styles/editor.scss +2 -1
  60. package/components/Legend/Legend.Gradient.tsx +1 -1
  61. package/components/Loader/Loader.tsx +1 -1
  62. package/components/MediaControls.tsx +63 -34
  63. package/components/PaletteConversionModal.tsx +7 -4
  64. package/components/PaletteSelector/PaletteSelector.css +49 -6
  65. package/components/Table/components/Cell.tsx +23 -2
  66. package/components/Table/components/Row.tsx +5 -3
  67. package/components/_stories/Filters.stories.tsx +20 -1
  68. package/components/_stories/Footnotes.CSV.stories.tsx +247 -0
  69. package/components/_stories/Footnotes.stories.tsx +768 -3
  70. package/components/_stories/Inputs.stories.tsx +2 -2
  71. package/components/_stories/styles.scss +0 -1
  72. package/components/ui/Accordion.jsx +1 -1
  73. package/components/ui/Icon.tsx +3 -1
  74. package/components/ui/Title/index.tsx +30 -2
  75. package/components/ui/Title/title.styles.css +42 -0
  76. package/components/ui/accordion.styles.css +57 -0
  77. package/data/chartColorPalettes.ts +1 -1
  78. package/dist/cove-main.css +75 -6
  79. package/dist/cove-main.css.map +1 -1
  80. package/generateViteConfig.js +8 -1
  81. package/helpers/addValuesToFilters.ts +11 -1
  82. package/helpers/constants.ts +37 -0
  83. package/helpers/cove/number.ts +33 -12
  84. package/helpers/coveUpdateWorker.ts +20 -11
  85. package/helpers/embedCodeGenerator.ts +109 -0
  86. package/helpers/fetchRemoteData.ts +3 -15
  87. package/helpers/getUniqueValues.ts +19 -0
  88. package/helpers/hashObj.ts +25 -0
  89. package/helpers/isRightAlignedTableValue.js +5 -0
  90. package/helpers/markupProcessor.ts +27 -12
  91. package/helpers/mergeCustomOrderValues.ts +37 -0
  92. package/helpers/metrics/helpers.ts +1 -0
  93. package/helpers/parseCsvWithQuotes.ts +65 -0
  94. package/helpers/pivotData.ts +2 -2
  95. package/helpers/prepareScreenshot.ts +268 -0
  96. package/helpers/queryStringUtils.ts +29 -0
  97. package/helpers/testing.ts +17 -4
  98. package/helpers/tests/prepareScreenshot.test.ts +414 -0
  99. package/helpers/tests/queryStringUtils.test.ts +381 -0
  100. package/helpers/tests/testStandaloneBuild.ts +23 -5
  101. package/helpers/useDataVizClasses.ts +0 -1
  102. package/helpers/ver/4.25.11.ts +13 -0
  103. package/helpers/ver/4.26.1.ts +80 -0
  104. package/helpers/viewports.ts +2 -0
  105. package/hooks/useDataColumns.ts +63 -0
  106. package/hooks/useFilterManagement.ts +94 -0
  107. package/hooks/useLegendSeparators.ts +26 -0
  108. package/hooks/useListManagement.ts +192 -0
  109. package/package.json +6 -4
  110. package/styles/_button-section.scss +0 -3
  111. package/styles/_common-components.css +73 -0
  112. package/styles/_global.scss +25 -5
  113. package/styles/base.scss +0 -50
  114. package/styles/cove-main.scss +3 -1
  115. package/styles/filters.scss +10 -3
  116. package/styles/v2/base/index.scss +0 -1
  117. package/styles/v2/components/editor.scss +14 -6
  118. package/styles/v2/utils/_breakpoints.scss +1 -1
  119. package/styles/v2/utils/index.scss +0 -1
  120. package/styles/waiting.scss +1 -1
  121. package/types/Axis.ts +1 -0
  122. package/types/ForecastingSeriesKey.ts +1 -0
  123. package/types/MarkupInclude.ts +5 -3
  124. package/types/MarkupVariable.ts +1 -1
  125. package/types/Series.ts +3 -0
  126. package/types/Table.ts +1 -0
  127. package/types/Visualization.ts +1 -0
  128. package/types/VizFilter.ts +2 -0
  129. package/LICENSE +0 -201
  130. package/styles/_mixins.scss +0 -13
  131. package/styles/_typography.scss +0 -0
  132. package/styles/v2/base/_typography.scss +0 -0
  133. package/styles/v2/components/guidance-block.scss +0 -74
  134. package/styles/v2/utils/_functions.scss +0 -0
@@ -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/State Diabetes Profiles',
15
+ parameters: {
16
+ layout: 'fullscreen',
17
+ docs: {
18
+ description: {
19
+ component: 'Stories for visualizations from the CDC State Diabetes Profiles page (https://www.cdc.gov/diabetes-state-local/php/state-profiles/index.html)'
20
+ }
21
+ }
22
+ },
23
+ tags: ['autodocs']
24
+ }
25
+
26
+ export default meta
27
+
28
+ // Config URL from the state diabetes profiles page
29
+ const CONFIG_URLS = {
30
+ stateDiabetesProfiles: 'https://www.cdc.gov/diabetes-state-local/php/state-profiles/Diabetes-State-Profiles.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
+ * State Diabetes Profiles Dashboard
177
+ *
178
+ * Interactive dashboard showing the economic impact and burden of diabetes, key program outcomes,
179
+ * and the Division of Diabetes Translation's (DDT) direct funding in all 50 states and Washington, DC.
180
+ *
181
+ * The dashboard displays state-by-state diabetes data including:
182
+ * - DDT Direct Investments by State
183
+ * - Diabetes Burden and Economic Impact
184
+ * - Program Outcomes and Prevention Initiatives
185
+ * - State-specific diabetes statistics and trends
186
+ */
187
+ export const State_Diabetes_Profiles_Dashboard: DashboardStory = {
188
+ render: () => {
189
+ const config = useConfigWithAbsoluteDataUrl(CONFIG_URLS.stateDiabetesProfiles)
190
+ if (!config) return <div>Loading...</div>
191
+ return <Dashboard config={config} />
192
+ },
193
+ play: async ({ canvasElement }) => {
194
+ await testDashboardRendering(canvasElement, 'State Diabetes Profiles Dashboard')
195
+ }
196
+ }
@@ -0,0 +1,463 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { within, expect } from 'storybook/test'
3
+ import Chart from '@cdc/chart'
4
+ import CdcMap from '@cdc/map'
5
+ import Dashboard from '@cdc/dashboard'
6
+ import { useEffect, useState } from 'react'
7
+
8
+ // Fallback step function for test descriptions
9
+ const step = async (description: string, fn: () => Promise<void> | void) => {
10
+ console.log(`▶ ${description}`)
11
+ await fn()
12
+ console.log(`✓ ${description}`)
13
+ }
14
+
15
+ const meta: Meta = {
16
+ title: 'Regression Tests/Pages/Wastewater',
17
+ parameters: {
18
+ layout: 'fullscreen',
19
+ docs: {
20
+ description: {
21
+ component: 'Stories for all visualizations from the CDC National Wastewater Surveillance System (NWSS) pages'
22
+ }
23
+ }
24
+ },
25
+ tags: ['autodocs']
26
+ }
27
+
28
+ export default meta
29
+
30
+ // Config URLs from the NWSS pages
31
+ const CONFIG_URLS = {
32
+ // Main NWSS page (https://www.cdc.gov/nwss/index.html)
33
+ homePageModules: 'https://www.cdc.gov/nwss/rv/modules/home-page-modules.json',
34
+
35
+ // Measles page (https://www.cdc.gov/nwss/rv/measles.html)
36
+ measlesTopModules: 'https://www.cdc.gov/nwss/rv/modules/measles/top-three-modules.json',
37
+ measlesMap: 'https://www.cdc.gov/nwss/rv/modules/measles/measles-us-map.json',
38
+ measlesTimePeriod: 'https://www.cdc.gov/nwss/rv/modules/measles/time-period.json',
39
+
40
+ // COVID-19 National Data page (https://www.cdc.gov/nwss/rv/COVID19-national-data.html)
41
+ covidTopModules: 'https://www.cdc.gov/nwss/rv/modules/sc2/covid-top-modules.json',
42
+ covidTimePeriodMap: 'https://www.cdc.gov/nwss/rv/modules/sc2/covid-time-period-state-map.json',
43
+ covidStateLevel: 'https://www.cdc.gov/nwss/rv/modules/sc2/covid-19-state-level.json',
44
+ covidNationalRegionalTrends: 'https://www.cdc.gov/nwss/rv/modules/sc2/covid-19-national-and-regional-trends.json',
45
+
46
+ // COVID-19 State Trend page (https://www.cdc.gov/nwss/rv/COVID19-statetrend.html)
47
+ covidStateLevelRest: 'https://www.cdc.gov/nwss/rv/modules/sc2/State-Level-covid-rest.json'
48
+ }
49
+
50
+ // Helper to fetch config and update data URLs to use absolute cdc.gov paths
51
+ const useConfigWithAbsoluteDataUrl = (configUrl: string) => {
52
+ const [config, setConfig] = useState(null)
53
+
54
+ useEffect(() => {
55
+ fetch(configUrl)
56
+ .then(res => res.json())
57
+ .then(data => {
58
+ // Convert relative data URLs to absolute cdc.gov URLs
59
+ if (data.dataUrl) {
60
+ // Handle different relative path formats (../../path or /path)
61
+ const dataUrl = data.dataUrl.replace(/^(\.\.\/)+/, '').replace(/^\//, '')
62
+ data.dataUrl = `https://www.cdc.gov/${dataUrl}`
63
+ }
64
+ if (data.dataFileName) {
65
+ const dataFileName = data.dataFileName.replace(/^(\.\.\/)+/, '').replace(/^\//, '')
66
+ data.dataFileName = `https://www.cdc.gov/${dataFileName}`
67
+ }
68
+
69
+ // For dashboard configs, convert dataKey references in visualizations
70
+ if (data.visualizations) {
71
+ Object.values(data.visualizations).forEach((viz: any) => {
72
+ if (viz.dataKey) {
73
+ const dataKey = viz.dataKey.replace(/^(\.\.\/)+/, '').replace(/^\//, '')
74
+ viz.dataKey = `https://www.cdc.gov/${dataKey}`
75
+ }
76
+ })
77
+ }
78
+
79
+ // For dashboard configs, convert datasets
80
+ if (data.datasets) {
81
+ const newDatasets = {}
82
+ Object.entries(data.datasets).forEach(([key, dataset]: [string, any]) => {
83
+ const newKey = key.replace(/^(\.\.\/)+/, '').replace(/^\//, '')
84
+ const absoluteKey = `https://www.cdc.gov/${newKey}`
85
+
86
+ newDatasets[absoluteKey] = {
87
+ ...dataset,
88
+ dataFileName: dataset.dataFileName
89
+ ? `https://www.cdc.gov/${dataset.dataFileName.replace(/^(\.\.\/)+/, '').replace(/^\//, '')}`
90
+ : dataset.dataFileName,
91
+ dataUrl: dataset.dataUrl
92
+ ? `https://www.cdc.gov/${dataset.dataUrl.replace(/^(\.\.\/)+/, '').replace(/^\//, '')}`
93
+ : dataset.dataUrl
94
+ }
95
+ })
96
+ data.datasets = newDatasets
97
+ }
98
+
99
+ // Set activeDashboard to 0 if it's null and multiDashboards exist
100
+ if (data.multiDashboards && data.multiDashboards.length > 0 && data.activeDashboard === null) {
101
+ data.activeDashboard = 0
102
+ }
103
+
104
+ setConfig(data)
105
+ })
106
+ .catch(err => {
107
+ console.error('Failed to fetch config:', configUrl, err)
108
+ })
109
+ }, [configUrl])
110
+
111
+ return config
112
+ }
113
+
114
+ type MapStory = StoryObj<typeof CdcMap>
115
+ type ChartStory = StoryObj<typeof Chart>
116
+ type DashboardStory = StoryObj<typeof Dashboard>
117
+
118
+ // Helper function to test map rendering
119
+ const testMapRendering = async (canvasElement: HTMLElement, storyName: string) => {
120
+ const canvas = within(canvasElement)
121
+
122
+ await step('Wait for map to render', async () => {
123
+ const mapElement = await canvas.findByRole('img', { hidden: true }, { timeout: 10000 })
124
+ expect(mapElement).toBeInTheDocument()
125
+ })
126
+
127
+ await step('Verify SVG element is present', async () => {
128
+ const svgElement = canvasElement.querySelector('svg')
129
+ expect(svgElement).toBeInTheDocument()
130
+ })
131
+
132
+ await step('Verify COVE module wrapper is present', async () => {
133
+ const coveModule = canvasElement.querySelector('.cdc-open-viz-module')
134
+ expect(coveModule).toBeInTheDocument()
135
+ })
136
+
137
+ console.log(` ${storyName} map rendered successfully`)
138
+ }
139
+
140
+ // Helper function to test chart rendering
141
+ const testChartRendering = async (canvasElement: HTMLElement, storyName: string) => {
142
+ const canvas = within(canvasElement)
143
+
144
+ await step('Wait for chart to render', async () => {
145
+ const svgElement = await canvas.findByRole('img', { hidden: true }, { timeout: 10000 })
146
+ expect(svgElement).toBeInTheDocument()
147
+ })
148
+
149
+ await step('Verify chart SVG is present', async () => {
150
+ const chartSvg = canvasElement.querySelector('svg')
151
+ expect(chartSvg).toBeInTheDocument()
152
+ })
153
+
154
+ await step('Verify COVE module wrapper is present', async () => {
155
+ const coveModule = canvasElement.querySelector('.cdc-open-viz-module')
156
+ expect(coveModule).toBeInTheDocument()
157
+ })
158
+
159
+ console.log(` ${storyName} chart rendered successfully`)
160
+ }
161
+
162
+ // Helper function to test dashboard rendering
163
+ const testDashboardRendering = async (canvasElement: HTMLElement, storyName: string) => {
164
+ await step('Wait for dashboard to render', async () => {
165
+ await new Promise<void>((resolve, reject) => {
166
+ const startTime = Date.now()
167
+ const timeout = 15000
168
+
169
+ const checkDashboard = () => {
170
+ const dashboardElement = canvasElement.querySelector('.cove-dashboard')
171
+ if (dashboardElement) {
172
+ resolve()
173
+ } else if (Date.now() - startTime > timeout) {
174
+ reject(new Error(`Timeout: Dashboard element not found after ${timeout}ms`))
175
+ } else {
176
+ setTimeout(checkDashboard, 100)
177
+ }
178
+ }
179
+ checkDashboard()
180
+ })
181
+ })
182
+
183
+ await step('Verify dashboard wrapper is present', async () => {
184
+ const dashboard = canvasElement.querySelector('.cove-dashboard')
185
+ expect(dashboard).toBeInTheDocument()
186
+ })
187
+
188
+ console.log(` ${storyName} dashboard rendered successfully`)
189
+ }
190
+
191
+ /**
192
+ * NWSS Home Page - Summary Modules
193
+ *
194
+ * Multi-virus wastewater surveillance summary from the main NWSS landing page.
195
+ */
196
+ export const Home_Page_Modules: DashboardStory = {
197
+ render: () => {
198
+ const config = useConfigWithAbsoluteDataUrl(CONFIG_URLS.homePageModules)
199
+ if (!config) return <div>Loading...</div>
200
+ return <Dashboard config={config} />
201
+ },
202
+ play: async ({ canvasElement }) => {
203
+ await testDashboardRendering(canvasElement, 'Home Page Modules')
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Measles - Top Summary Modules
209
+ *
210
+ * Key metrics for measles wastewater detections nationwide.
211
+ */
212
+ export const Measles_Top_Modules: DashboardStory = {
213
+ render: () => {
214
+ const config = useConfigWithAbsoluteDataUrl(CONFIG_URLS.measlesTopModules)
215
+ if (!config) return <div>Loading...</div>
216
+ return <Dashboard config={config} />
217
+ },
218
+ play: async ({ canvasElement }) => {
219
+ await testDashboardRendering(canvasElement, 'Measles Top Modules')
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Measles - US Map
225
+ *
226
+ * Geographic distribution of measles wastewater detections across the United States.
227
+ */
228
+ export const Measles_Map: MapStory = {
229
+ render: () => {
230
+ const config = useConfigWithAbsoluteDataUrl(CONFIG_URLS.measlesMap)
231
+ if (!config) return <div>Loading...</div>
232
+ return <CdcMap config={config} />
233
+ },
234
+ play: async ({ canvasElement }) => {
235
+ await testMapRendering(canvasElement, 'Measles Map')
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Measles - Time Period
241
+ *
242
+ * Timeline information for measles wastewater surveillance data.
243
+ */
244
+ export const Measles_Time_Period: DashboardStory = {
245
+ render: () => {
246
+ const config = useConfigWithAbsoluteDataUrl(CONFIG_URLS.measlesTimePeriod)
247
+ if (!config) return <div>Loading...</div>
248
+ return <Dashboard config={config} />
249
+ },
250
+ play: async ({ canvasElement }) => {
251
+ await testDashboardRendering(canvasElement, 'Measles Time Period')
252
+ }
253
+ }
254
+
255
+ /**
256
+ * COVID-19 - Top Summary Modules
257
+ *
258
+ * Key metrics for COVID-19 wastewater surveillance at the national level.
259
+ */
260
+ export const COVID_Top_Modules: DashboardStory = {
261
+ render: () => {
262
+ const config = useConfigWithAbsoluteDataUrl(CONFIG_URLS.covidTopModules)
263
+ if (!config) return <div>Loading...</div>
264
+ return <Dashboard config={config} />
265
+ },
266
+ play: async ({ canvasElement }) => {
267
+ await testDashboardRendering(canvasElement, 'COVID Top Modules')
268
+ }
269
+ }
270
+
271
+ /**
272
+ * COVID-19 - State Map with Time Period
273
+ *
274
+ * State-level COVID-19 wastewater activity levels across the US.
275
+ */
276
+ export const COVID_Time_Period_Map: MapStory = {
277
+ render: () => {
278
+ const config = useConfigWithAbsoluteDataUrl(CONFIG_URLS.covidTimePeriodMap)
279
+ if (!config) return <div>Loading...</div>
280
+ return <CdcMap config={config} />
281
+ },
282
+ play: async ({ canvasElement }) => {
283
+ await testMapRendering(canvasElement, 'COVID Time Period Map')
284
+ }
285
+ }
286
+
287
+ /**
288
+ * COVID-19 - State Level Data
289
+ *
290
+ * COVID-19 wastewater data visualization by state.
291
+ */
292
+ export const COVID_State_Level: ChartStory = {
293
+ render: () => {
294
+ const config = useConfigWithAbsoluteDataUrl(CONFIG_URLS.covidStateLevel)
295
+ if (!config) return <div>Loading...</div>
296
+ return <Chart config={config} />
297
+ },
298
+ play: async ({ canvasElement }) => {
299
+ await testChartRendering(canvasElement, 'COVID State Level')
300
+ }
301
+ }
302
+
303
+ /**
304
+ * COVID-19 - National and Regional Trends
305
+ *
306
+ * Trends in COVID-19 wastewater viral activity at national and HHS regional levels.
307
+ */
308
+ export const COVID_National_Regional_Trends: ChartStory = {
309
+ render: () => {
310
+ const config = useConfigWithAbsoluteDataUrl(CONFIG_URLS.covidNationalRegionalTrends)
311
+ if (!config) return <div>Loading...</div>
312
+ return <Chart config={config} />
313
+ },
314
+ play: async ({ canvasElement }) => {
315
+ await testChartRendering(canvasElement, 'COVID National Regional Trends')
316
+ }
317
+ }
318
+
319
+ /**
320
+ * COVID-19 - State Trend Data (Alternative View)
321
+ *
322
+ * State-level COVID-19 wastewater trend visualization from the state trend page.
323
+ */
324
+ export const COVID_State_Level_Rest: ChartStory = {
325
+ render: () => {
326
+ const config = useConfigWithAbsoluteDataUrl(CONFIG_URLS.covidStateLevelRest)
327
+ if (!config) return <div>Loading...</div>
328
+ return <Chart config={config} />
329
+ },
330
+ play: async ({ canvasElement }) => {
331
+ await testChartRendering(canvasElement, 'COVID State Level Rest')
332
+ }
333
+ }
334
+
335
+ /**
336
+ * All Wastewater Visualizations - Combined Test
337
+ *
338
+ * Tests all visualizations from the NWSS pages to ensure they all render correctly together.
339
+ */
340
+ export const All_Wastewater_Visualizations: StoryObj = {
341
+ render: () => {
342
+ const homePageConfig = useConfigWithAbsoluteDataUrl(CONFIG_URLS.homePageModules)
343
+ const measlesTopConfig = useConfigWithAbsoluteDataUrl(CONFIG_URLS.measlesTopModules)
344
+ const measlesMapConfig = useConfigWithAbsoluteDataUrl(CONFIG_URLS.measlesMap)
345
+ const measlesTimePeriodConfig = useConfigWithAbsoluteDataUrl(CONFIG_URLS.measlesTimePeriod)
346
+ const covidTopConfig = useConfigWithAbsoluteDataUrl(CONFIG_URLS.covidTopModules)
347
+ const covidMapConfig = useConfigWithAbsoluteDataUrl(CONFIG_URLS.covidTimePeriodMap)
348
+ const covidStateLevelConfig = useConfigWithAbsoluteDataUrl(CONFIG_URLS.covidStateLevel)
349
+ const covidNationalRegionalConfig = useConfigWithAbsoluteDataUrl(CONFIG_URLS.covidNationalRegionalTrends)
350
+ const covidStateRestConfig = useConfigWithAbsoluteDataUrl(CONFIG_URLS.covidStateLevelRest)
351
+
352
+ const allLoaded =
353
+ homePageConfig &&
354
+ measlesTopConfig &&
355
+ measlesMapConfig &&
356
+ measlesTimePeriodConfig &&
357
+ covidTopConfig &&
358
+ covidMapConfig &&
359
+ covidStateLevelConfig &&
360
+ covidNationalRegionalConfig &&
361
+ covidStateRestConfig
362
+
363
+ if (!allLoaded) {
364
+ return <div>Loading...</div>
365
+ }
366
+
367
+ return (
368
+ <div className="container-fluid p-4">
369
+ <h1 className="mb-4">NWSS - All Wastewater Visualizations</h1>
370
+
371
+ <section className="mb-5">
372
+ <h2>NWSS Home Page</h2>
373
+ <Dashboard config={homePageConfig} />
374
+ </section>
375
+
376
+ <section className="mb-5">
377
+ <h2>Measles - Summary Modules</h2>
378
+ <Dashboard config={measlesTopConfig} />
379
+ </section>
380
+
381
+ <section className="mb-5">
382
+ <h2>Measles - US Map</h2>
383
+ <CdcMap config={measlesMapConfig} />
384
+ </section>
385
+
386
+ <section className="mb-5">
387
+ <h2>Measles - Time Period</h2>
388
+ <Dashboard config={measlesTimePeriodConfig} />
389
+ </section>
390
+
391
+ <section className="mb-5">
392
+ <h2>COVID-19 - Summary Modules</h2>
393
+ <Dashboard config={covidTopConfig} />
394
+ </section>
395
+
396
+ <section className="mb-5">
397
+ <h2>COVID-19 - State Map</h2>
398
+ <CdcMap config={covidMapConfig} />
399
+ </section>
400
+
401
+ <section className="mb-5">
402
+ <h2>COVID-19 - State Level Data</h2>
403
+ <Chart config={covidStateLevelConfig} />
404
+ </section>
405
+
406
+ <section className="mb-5">
407
+ <h2>COVID-19 - National and Regional Trends</h2>
408
+ <Chart config={covidNationalRegionalConfig} />
409
+ </section>
410
+
411
+ <section className="mb-5">
412
+ <h2>COVID-19 - State Trends</h2>
413
+ <Chart config={covidStateRestConfig} />
414
+ </section>
415
+ </div>
416
+ )
417
+ },
418
+ play: async ({ canvasElement }) => {
419
+ await step('Wait for all configs to load', async () => {
420
+ await new Promise<void>(resolve => {
421
+ const checkLoading = () => {
422
+ const loadingDiv = canvasElement.querySelector('div:not(.container-fluid)')
423
+ if (!loadingDiv || !loadingDiv.textContent?.includes('Loading')) {
424
+ resolve()
425
+ } else {
426
+ setTimeout(checkLoading, 100)
427
+ }
428
+ }
429
+ checkLoading()
430
+ })
431
+ })
432
+
433
+ await step('Wait for visualizations to start rendering', async () => {
434
+ await new Promise<void>(resolve => setTimeout(resolve, 2000))
435
+ })
436
+
437
+ await step('Wait for all 9 COVE modules to render', async () => {
438
+ await new Promise<void>((resolve, reject) => {
439
+ const startTime = Date.now()
440
+ const timeout = 30000
441
+
442
+ const checkModules = () => {
443
+ const coveModules = canvasElement.querySelectorAll('.cdc-open-viz-module')
444
+ if (coveModules.length >= 9) {
445
+ resolve()
446
+ } else if (Date.now() - startTime > timeout) {
447
+ reject(new Error(`Timeout: Only ${coveModules.length}/9 COVE modules found after ${timeout}ms`))
448
+ } else {
449
+ setTimeout(checkModules, 200)
450
+ }
451
+ }
452
+ checkModules()
453
+ })
454
+ })
455
+
456
+ await step('Verify at least 9 visualizations are present', async () => {
457
+ const coveModules = canvasElement.querySelectorAll('.cdc-open-viz-module')
458
+ expect(coveModules.length).toBeGreaterThanOrEqual(9)
459
+ })
460
+
461
+ console.log(` All 9+ wastewater visualizations rendered successfully`)
462
+ }
463
+ }