@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,332 @@
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 { useEffect, useState } from 'react'
6
+
7
+ // Fallback step function for test descriptions
8
+ const step = async (description: string, fn: () => Promise<void> | void) => {
9
+ console.log(`▶ ${description}`)
10
+ await fn()
11
+ console.log(`✓ ${description}`)
12
+ }
13
+
14
+ const meta: Meta = {
15
+ title: 'Regression Tests/Pages/Respiratory',
16
+ parameters: {
17
+ layout: 'fullscreen',
18
+ docs: {
19
+ description: {
20
+ component: 'Stories for all visualizations from the CDC Respiratory Viruses Activity Levels page (https://www.cdc.gov/respiratory-viruses/data/activity-levels.html)'
21
+ }
22
+ }
23
+ },
24
+ tags: ['autodocs']
25
+ }
26
+
27
+ export default meta
28
+
29
+ // Config URLs from the respiratory viruses activity levels page
30
+ const CONFIG_URLS = {
31
+ ariMap: 'https://www.cdc.gov/respiratory-viruses/modules/respiratory-virus-activity/ARI_Map_Viz.json',
32
+ cfaMap: 'https://www.cdc.gov/respiratory-viruses/modules/respiratory-virus-activity/CFA_Map_Viz.json',
33
+ wastewaterMap: 'https://www.cdc.gov/respiratory-viruses/modules/respiratory-virus-activity/wastewatermap.json',
34
+ testPositivity: 'https://www.cdc.gov/respiratory-viruses/modules/test-in-percent-test-positivity-in-usa.json'
35
+ }
36
+
37
+ // Helper to fetch config and update data URLs to use absolute cdc.gov paths
38
+ const useConfigWithAbsoluteDataUrl = (configUrl: string) => {
39
+ const [config, setConfig] = useState(null)
40
+
41
+ useEffect(() => {
42
+ fetch(configUrl)
43
+ .then(res => res.json())
44
+ .then(data => {
45
+ // Convert relative data URLs to absolute cdc.gov URLs
46
+ if (data.dataUrl) {
47
+ // Handle different relative path formats (../../path or /path)
48
+ const dataUrl = data.dataUrl.replace(/^(\.\.\/)+/, '').replace(/^\//, '')
49
+ data.dataUrl = `https://www.cdc.gov/${dataUrl}`
50
+ }
51
+ if (data.dataFileName) {
52
+ const dataFileName = data.dataFileName.replace(/^(\.\.\/)+/, '').replace(/^\//, '')
53
+ data.dataFileName = `https://www.cdc.gov/${dataFileName}`
54
+ }
55
+
56
+ // Validate that color configuration exists
57
+ if (!data.customColors || data.customColors.length === 0) {
58
+ console.warn('⚠️ No customColors found in config:', configUrl)
59
+ } else {
60
+ console.log(`✓ Config has ${data.customColors.length} custom colors`, data.customColors)
61
+ }
62
+
63
+ // Validate legend configuration
64
+ if (data.legend) {
65
+ console.log('✓ Legend config:', {
66
+ type: data.legend.type,
67
+ categories: data.legend.categoryValuesOrder || data.legend.additionalCategories,
68
+ style: data.legend.style
69
+ })
70
+ }
71
+
72
+ setConfig(data)
73
+ })
74
+ .catch(err => {
75
+ console.error('Failed to fetch config:', configUrl, err)
76
+ })
77
+ }, [configUrl])
78
+
79
+ return config
80
+ }
81
+
82
+ type MapStory = StoryObj<typeof CdcMap>
83
+ type ChartStory = StoryObj<typeof Chart>
84
+
85
+ // Helper to verify colors in visualizations (Playwright assertions)
86
+ const verifyColors = (canvasElement: HTMLElement, storyName: string) => {
87
+ // Check for colored paths (maps)
88
+ const mapPaths = canvasElement.querySelectorAll('svg path[fill]')
89
+ let coloredPaths = 0
90
+ mapPaths.forEach(path => {
91
+ const fill = path.getAttribute('fill')
92
+ if (fill && fill !== 'none' && fill !== '#cccccc' && fill !== '#e0e0e0' && !fill.toLowerCase().includes('gray')) {
93
+ coloredPaths++
94
+ }
95
+ })
96
+
97
+ // Check for colored chart elements (lines, strokes)
98
+ const chartElements = canvasElement.querySelectorAll('svg path[stroke], svg line[stroke]')
99
+ let coloredElements = 0
100
+ chartElements.forEach(element => {
101
+ const stroke = element.getAttribute('stroke')
102
+ if (stroke && stroke !== 'none' && !stroke.toLowerCase().includes('gray')) {
103
+ coloredElements++
104
+ }
105
+ })
106
+
107
+ const totalColored = coloredPaths + coloredElements
108
+
109
+ // Assert that colored elements exist (will fail Playwright test if not)
110
+ expect(totalColored).toBeGreaterThan(0)
111
+
112
+ console.log(`✓ ${storyName}: ${totalColored} colored elements verified (${coloredPaths} paths, ${coloredElements} strokes)`)
113
+ }
114
+
115
+ // Helper function to test map rendering
116
+ const testMapRendering = async (canvasElement: HTMLElement, storyName: string) => {
117
+ const canvas = within(canvasElement)
118
+
119
+ await step('Wait for map to render', async () => {
120
+ const mapElement = await canvas.findByRole('img', { hidden: true }, { timeout: 10000 })
121
+ expect(mapElement).toBeInTheDocument()
122
+ })
123
+
124
+ await step('Verify SVG element is present', async () => {
125
+ const svgElement = canvasElement.querySelector('svg')
126
+ expect(svgElement).toBeInTheDocument()
127
+ })
128
+
129
+ await step('Verify COVE module wrapper is present', async () => {
130
+ const coveModule = canvasElement.querySelector('.cdc-open-viz-module')
131
+ expect(coveModule).toBeInTheDocument()
132
+ })
133
+
134
+ await step('Verify colors are applied to map regions', async () => {
135
+ verifyColors(canvasElement, storyName)
136
+ })
137
+
138
+ console.log(` ${storyName} map rendered successfully`)
139
+ }
140
+
141
+ // Helper function to test chart rendering
142
+ // Helper function to test chart rendering
143
+ const testChartRendering = async (canvasElement: HTMLElement, storyName: string) => {
144
+ const canvas = within(canvasElement)
145
+
146
+ await step('Wait for chart to render', async () => {
147
+ const svgElement = await canvas.findByRole('img', { hidden: true }, { timeout: 10000 })
148
+ expect(svgElement).toBeInTheDocument()
149
+ })
150
+
151
+ await step('Verify chart SVG is present', async () => {
152
+ const chartSvg = canvasElement.querySelector('svg')
153
+ expect(chartSvg).toBeInTheDocument()
154
+ })
155
+
156
+ await step('Verify COVE module wrapper is present', async () => {
157
+ const coveModule = canvasElement.querySelector('.cdc-open-viz-module')
158
+ expect(coveModule).toBeInTheDocument()
159
+ })
160
+
161
+ await step('Verify colors are applied to chart elements', async () => {
162
+ verifyColors(canvasElement, storyName)
163
+ })
164
+
165
+ console.log(` ${storyName} chart rendered successfully`)
166
+ }
167
+ /**
168
+ * Level of Respiratory Illness Activity Map
169
+ *
170
+ * Displays respiratory illness activity monitored using the acute respiratory
171
+ * illness (ARI) metric. ARI captures a broad range of diagnoses from emergency
172
+ * department visits for respiratory illnesses.
173
+ */
174
+ export const ARI_Activity_Map: MapStory = {
175
+ render: () => {
176
+ const config = useConfigWithAbsoluteDataUrl(CONFIG_URLS.ariMap)
177
+ if (!config) return <div>Loading...</div>
178
+ return <CdcMap config={config} />
179
+ },
180
+ play: async ({ canvasElement }) => {
181
+ await testMapRendering(canvasElement, 'ARI Activity Map')
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Epidemic Trends Map
187
+ *
188
+ * CDC uses data from emergency department visits to model epidemic trends.
189
+ * This model helps tell whether the number of new respiratory infections
190
+ * is growing or declining in your state.
191
+ */
192
+ export const Epidemic_Trends_Map: MapStory = {
193
+ render: () => {
194
+ const config = useConfigWithAbsoluteDataUrl(CONFIG_URLS.cfaMap)
195
+ if (!config) return <div>Loading...</div>
196
+ return <CdcMap config={config} />
197
+ },
198
+ play: async ({ canvasElement }) => {
199
+ await testMapRendering(canvasElement, 'Epidemic Trends Map')
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Wastewater Surveillance Map
205
+ *
206
+ * Wastewater surveillance for COVID-19, influenza, and RSV by state/territory.
207
+ * Wastewater data can detect infections before clinical symptoms appear.
208
+ */
209
+ export const Wastewater_Surveillance_Map: MapStory = {
210
+ render: () => {
211
+ const config = useConfigWithAbsoluteDataUrl(CONFIG_URLS.wastewaterMap)
212
+ if (!config) return <div>Loading...</div>
213
+ return <CdcMap config={config} />
214
+ },
215
+ play: async ({ canvasElement }) => {
216
+ await testMapRendering(canvasElement, 'Wastewater Surveillance Map')
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Percent of Tests Positive for Respiratory Viruses
222
+ *
223
+ * Weekly percent of tests positive for the viruses that cause COVID-19,
224
+ * influenza, and RSV at the national level.
225
+ */
226
+ export const Test_Positivity_Chart: ChartStory = {
227
+ render: () => {
228
+ const config = useConfigWithAbsoluteDataUrl(CONFIG_URLS.testPositivity)
229
+ if (!config) return <div>Loading...</div>
230
+ return <Chart config={config} />
231
+ },
232
+ play: async ({ canvasElement }) => {
233
+ await testChartRendering(canvasElement, 'Test Positivity Chart')
234
+ }
235
+ }
236
+
237
+ /**
238
+ * All Visualizations - Combined Test
239
+ *
240
+ * Tests all four visualizations from the respiratory viruses page to ensure
241
+ * they all render correctly together.
242
+ */
243
+ export const All_Visualizations: StoryObj = {
244
+ render: () => {
245
+ const ariConfig = useConfigWithAbsoluteDataUrl(CONFIG_URLS.ariMap)
246
+ const cfaConfig = useConfigWithAbsoluteDataUrl(CONFIG_URLS.cfaMap)
247
+ const wastewaterConfig = useConfigWithAbsoluteDataUrl(CONFIG_URLS.wastewaterMap)
248
+ const testPositivityConfig = useConfigWithAbsoluteDataUrl(CONFIG_URLS.testPositivity)
249
+
250
+ if (!ariConfig || !cfaConfig || !wastewaterConfig || !testPositivityConfig) {
251
+ return <div>Loading...</div>
252
+ }
253
+
254
+ return (
255
+ <div className="container-fluid p-4">
256
+ <h1 className="mb-4">Respiratory Viruses Activity Levels - All Visualizations</h1>
257
+
258
+ <section className="mb-5">
259
+ <h2>Level of Respiratory Illness Activity</h2>
260
+ <CdcMap config={ariConfig} />
261
+ </section>
262
+
263
+ <section className="mb-5">
264
+ <h2>Epidemic Trends</h2>
265
+ <CdcMap config={cfaConfig} />
266
+ </section>
267
+
268
+ <section className="mb-5">
269
+ <h2>Wastewater Surveillance</h2>
270
+ <CdcMap config={wastewaterConfig} />
271
+ </section>
272
+
273
+ <section className="mb-5">
274
+ <h2>Percent of Tests Positive for Respiratory Viruses</h2>
275
+ <Chart config={testPositivityConfig} />
276
+ </section>
277
+ </div>
278
+ )
279
+ },
280
+ play: async ({ canvasElement }) => {
281
+ const canvas = within(canvasElement)
282
+
283
+ await step('Wait for all configs to load', async () => {
284
+ await new Promise<void>(resolve => {
285
+ const checkLoading = () => {
286
+ const loadingDiv = canvasElement.querySelector('div:not(.container-fluid)')
287
+ if (!loadingDiv || !loadingDiv.textContent?.includes('Loading')) {
288
+ resolve()
289
+ } else {
290
+ setTimeout(checkLoading, 100)
291
+ }
292
+ }
293
+ checkLoading()
294
+ })
295
+ })
296
+
297
+ await step('Wait for visualizations to start rendering', async () => {
298
+ await new Promise<void>(resolve => setTimeout(resolve, 2000))
299
+ })
300
+
301
+ await step('Wait for all 4 COVE modules to render', async () => {
302
+ await new Promise<void>((resolve, reject) => {
303
+ const startTime = Date.now()
304
+ const timeout = 20000
305
+
306
+ const checkModules = () => {
307
+ const coveModules = canvasElement.querySelectorAll('.cdc-open-viz-module')
308
+ if (coveModules.length >= 4) {
309
+ resolve()
310
+ } else if (Date.now() - startTime > timeout) {
311
+ reject(new Error(`Timeout: Only ${coveModules.length}/4 COVE modules found after ${timeout}ms`))
312
+ } else {
313
+ setTimeout(checkModules, 200)
314
+ }
315
+ }
316
+ checkModules()
317
+ })
318
+ })
319
+
320
+ await step('Verify all 4 SVG visualizations are present', async () => {
321
+ const allSvgs = await canvas.findAllByRole('img', { hidden: true }, { timeout: 5000 })
322
+ expect(allSvgs.length).toBeGreaterThanOrEqual(4)
323
+ })
324
+
325
+ await step('Verify exactly 4 COVE modules are present', async () => {
326
+ const coveModules = canvasElement.querySelectorAll('.cdc-open-viz-module')
327
+ expect(coveModules.length).toBe(4)
328
+ })
329
+
330
+ console.log(` All 4 visualizations rendered successfully`)
331
+ }
332
+ }
@@ -0,0 +1,195 @@
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/Smoking and Tobacco',
15
+ parameters: {
16
+ layout: 'fullscreen',
17
+ docs: {
18
+ description: {
19
+ component: 'Stories for visualizations from the CDC Global Tobacco Surveillance System (GTSS) Data Explorer page (https://www.cdc.gov/tobacco/global/gtss/data-explorer/index.html)'
20
+ }
21
+ }
22
+ },
23
+ tags: ['autodocs']
24
+ }
25
+
26
+ export default meta
27
+
28
+ // Config URL from the tobacco data explorer page
29
+ const CONFIG_URLS = {
30
+ dataExplorer: 'https://www.cdc.gov/tobacco/global/gtss/data-explorer/data-explorer.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
+ * GTSS Data Explorer Dashboard
177
+ *
178
+ * Interactive dashboard for exploring Global Tobacco Surveillance System (GTSS) data.
179
+ * The dashboard includes 2 views:
180
+ * - Explore by Location: Browse tobacco use data by geographic location
181
+ * - Explore by Indicator: Browse tobacco use data by specific health indicators
182
+ *
183
+ * GTSS monitors tobacco use among youth and adults globally, tracking prevalence,
184
+ * attitudes, and exposure to tobacco products and smoke.
185
+ */
186
+ export const GTSS_Data_Explorer: DashboardStory = {
187
+ render: () => {
188
+ const config = useConfigWithAbsoluteDataUrl(CONFIG_URLS.dataExplorer)
189
+ if (!config) return <div>Loading...</div>
190
+ return <Dashboard config={config} />
191
+ },
192
+ play: async ({ canvasElement }) => {
193
+ await testDashboardRendering(canvasElement, 'GTSS Data Explorer Dashboard')
194
+ }
195
+ }