@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,187 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { within, expect } from 'storybook/test'
3
+ import WaffleChart from '@cdc/waffle-chart'
4
+
5
+ // Fallback step function for test descriptions
6
+ const step = async (description: string, fn: () => Promise<void> | void) => {
7
+ console.log(`▶ ${description}`)
8
+ await fn()
9
+ console.log(`✓ ${description}`)
10
+ }
11
+
12
+ const meta: Meta = {
13
+ title: 'Regression Tests/Gallery/Waffle Charts',
14
+ parameters: {
15
+ layout: 'fullscreen',
16
+ docs: {
17
+ description: {
18
+ component: 'Waffle Chart and Gauge visualization examples from the CDC COVE Gallery'
19
+ }
20
+ }
21
+ },
22
+ tags: ['autodocs']
23
+ }
24
+
25
+ export default meta
26
+
27
+ type Story = StoryObj<typeof WaffleChart>
28
+
29
+ // Helper function to test waffle chart rendering
30
+ const testWaffleChartRendering = async (canvasElement: HTMLElement, storyName: string) => {
31
+ await step('Wait for waffle chart to render', async () => {
32
+ // Wait for SVG element to appear
33
+ await new Promise<void>((resolve, reject) => {
34
+ const startTime = Date.now()
35
+ const timeout = 10000
36
+
37
+ const checkSvg = () => {
38
+ const svgElement = canvasElement.querySelector('svg')
39
+ if (svgElement) {
40
+ resolve()
41
+ } else if (Date.now() - startTime > timeout) {
42
+ reject(new Error(`Timeout: SVG element not found after ${timeout}ms`))
43
+ } else {
44
+ setTimeout(checkSvg, 100)
45
+ }
46
+ }
47
+ checkSvg()
48
+ })
49
+ })
50
+
51
+ await step('Verify SVG element is present', async () => {
52
+ const svgElement = canvasElement.querySelector('svg')
53
+ expect(svgElement).toBeInTheDocument()
54
+ })
55
+
56
+ await step('Verify waffle chart container is present', async () => {
57
+ const waffleContainer = canvasElement.querySelector('.cove-waffle-chart')
58
+ expect(waffleContainer).toBeInTheDocument()
59
+ })
60
+
61
+ console.log(` ${storyName} waffle chart rendered successfully`)
62
+ }
63
+
64
+ // Base config for examples
65
+ const baseWaffleConfig = {
66
+ type: 'waffle-chart',
67
+ shape: 'person',
68
+ title: 'Overdose Mortality Rates',
69
+ content: 'of overdoses resulted in death.',
70
+ subtext: 'This data is an example and does not reflect actual averages',
71
+ orientation: 'horizontal',
72
+ data: [
73
+ {
74
+ 'Resulted in Death': 250,
75
+ 'Number of Overdoses': 600,
76
+ state: 'Alabama',
77
+ Year: '2010'
78
+ },
79
+ {
80
+ 'Resulted in Death': 16,
81
+ 'Number of Overdoses': 80,
82
+ state: 'Alaska',
83
+ Year: '2008'
84
+ },
85
+ {
86
+ 'Resulted in Death': 19,
87
+ 'Number of Overdoses': 100,
88
+ state: 'Alaska',
89
+ Year: '2010'
90
+ },
91
+ {
92
+ 'Resulted in Death': 93,
93
+ 'Number of Overdoses': 400,
94
+ state: 'Alaska',
95
+ Year: '2012'
96
+ },
97
+ {
98
+ 'Resulted in Death': 82,
99
+ 'Number of Overdoses': 400,
100
+ state: 'Arizona',
101
+ Year: '2010'
102
+ }
103
+ ],
104
+ filters: [],
105
+ fontSize: null,
106
+ overallFontSize: 'medium',
107
+ dataColumn: 'Resulted in Death',
108
+ dataFunction: 'Sum',
109
+ dataConditionalColumn: '',
110
+ dataConditionalOperator: null,
111
+ dataConditionalComparate: '',
112
+ customDenom: true,
113
+ dataDenom: null,
114
+ dataDenomColumn: 'Number of Overdoses',
115
+ dataDenomFunction: 'Sum',
116
+ prefix: '',
117
+ suffix: '%',
118
+ roundToPlace: 0,
119
+ nodeWidth: 10,
120
+ nodeSpacer: 2,
121
+ theme: 'theme-blue',
122
+ invalidComparate: false,
123
+ showDenominator: false,
124
+ showPercent: true,
125
+ valueDescription: 'testing',
126
+ visual: {
127
+ border: true,
128
+ accent: true,
129
+ background: true,
130
+ hideBackgroundColor: true,
131
+ borderColorTheme: true
132
+ }
133
+ }
134
+
135
+ export const Waffle_Chart_Person: Story = {
136
+ render: () => (
137
+ <WaffleChart
138
+ config={{
139
+ ...baseWaffleConfig,
140
+ visualizationType: 'Waffle',
141
+ visualizationSubType: '',
142
+ gauge: {
143
+ height: 35,
144
+ width: 400
145
+ },
146
+ visual: {
147
+ border: true,
148
+ accent: true,
149
+ background: true,
150
+ hideBackgroundColor: true,
151
+ borderColorTheme: true,
152
+ colors: {
153
+ 'theme-blue': '#005eaa',
154
+ 'theme-purple': '#712177',
155
+ 'theme-brown': '#705043',
156
+ 'theme-teal': '#00695c',
157
+ 'theme-pink': '#af4448',
158
+ 'theme-orange': '#bb4d00',
159
+ 'theme-slate': '#29434e',
160
+ 'theme-indigo': '#26418f',
161
+ 'theme-cyan': '#006778',
162
+ 'theme-green': '#4b830d',
163
+ 'theme-amber': '#fbab18'
164
+ }
165
+ }
166
+ }}
167
+ />
168
+ ),
169
+ play: async ({ canvasElement }) => {
170
+ await testWaffleChartRendering(canvasElement, 'Waffle Chart Person')
171
+ }
172
+ }
173
+
174
+ export const Gauge_Chart: Story = {
175
+ render: () => (
176
+ <WaffleChart
177
+ config={{
178
+ ...baseWaffleConfig,
179
+ visualizationType: 'Gauge',
180
+ visualizationSubType: ''
181
+ }}
182
+ />
183
+ ),
184
+ play: async ({ canvasElement }) => {
185
+ await testWaffleChartRendering(canvasElement, 'Gauge Chart')
186
+ }
187
+ }
@@ -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/ART',
15
+ parameters: {
16
+ layout: 'fullscreen',
17
+ docs: {
18
+ description: {
19
+ component: 'Stories for visualizations from the CDC ART (Assisted Reproductive Technology) Surveillance State-Specific page (https://www.cdc.gov/art/php/surveillance-state-specific/index.html)'
20
+ }
21
+ }
22
+ },
23
+ tags: ['autodocs']
24
+ }
25
+
26
+ export default meta
27
+
28
+ // Config URL from the ART surveillance state-specific page
29
+ const CONFIG_URLS = {
30
+ stateSpecificDashboard: 'https://www.cdc.gov/art/dfe-modules/state-specific-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
+ * ART State-Specific Surveillance Dashboard
178
+ *
179
+ * Interactive dashboard showing Assisted Reproductive Technology (ART) surveillance
180
+ * data for different states. This visualization allows users to explore state-specific
181
+ * ART outcomes and trends.
182
+ */
183
+ export const State_Specific_Dashboard: DashboardStory = {
184
+ render: () => {
185
+ const config = useConfigWithAbsoluteDataUrl(CONFIG_URLS.stateSpecificDashboard)
186
+ if (!config) return <div>Loading...</div>
187
+ return <Dashboard config={config} />
188
+ },
189
+ play: async ({ canvasElement }) => {
190
+ await testDashboardRendering(canvasElement, 'State Specific Dashboard')
191
+ }
192
+ }
@@ -0,0 +1,289 @@
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/BRFSS',
15
+ parameters: {
16
+ layout: 'fullscreen',
17
+ docs: {
18
+ description: {
19
+ component: 'Stories for visualizations from the CDC Behavioral Risk Factor Surveillance System (BRFSS) Prevalence Data & Data Analysis Tools page (https://www.cdc.gov/brfss/brfssprevalence/index.html)'
20
+ }
21
+ }
22
+ },
23
+ tags: ['autodocs']
24
+ }
25
+
26
+ export default meta
27
+
28
+ // Config URLs from the BRFSS prevalence page
29
+ const CONFIG_URLS = {
30
+ exploreByLocation: 'https://www.cdc.gov/brfss/brfssprevalence/modules/explore-by-location.json',
31
+ exploreByTopic: 'https://www.cdc.gov/brfss/brfssprevalence/modules/explore-by-topic.json'
32
+ }
33
+
34
+ // Helper to fetch config and update data URLs to use absolute cdc.gov paths
35
+ const useConfigWithAbsoluteDataUrl = (configUrl: string) => {
36
+ const [config, setConfig] = useState(null)
37
+
38
+ useEffect(() => {
39
+ fetch(configUrl)
40
+ .then(res => res.json())
41
+ .then(data => {
42
+ // Convert relative data URLs to absolute cdc.gov URLs
43
+ if (data.dataUrl) {
44
+ // Handle different relative path formats (../../path or /path)
45
+ const dataUrl = data.dataUrl.replace(/^(\.\.\/)+/, '').replace(/^\//, '')
46
+ data.dataUrl = `https://www.cdc.gov/${dataUrl}`
47
+ }
48
+ if (data.dataFileName) {
49
+ const dataFileName = data.dataFileName.replace(/^(\.\.\/)+/, '').replace(/^\//, '')
50
+ data.dataFileName = `https://www.cdc.gov/${dataFileName}`
51
+ }
52
+
53
+ // For dashboard configs with multiDashboards, convert dataKey references in visualizations
54
+ if (data.multiDashboards) {
55
+ data.multiDashboards.forEach((dashboard: any) => {
56
+ if (dashboard.visualizations) {
57
+ Object.values(dashboard.visualizations).forEach((viz: any) => {
58
+ // Only convert dataKey if it's a URL path (starts with / or ../)
59
+ if (viz.dataKey && (viz.dataKey.startsWith('/') || viz.dataKey.startsWith('../'))) {
60
+ const dataKey = viz.dataKey.replace(/^(\.\.\/)+/, '').replace(/^\//, '')
61
+ viz.dataKey = `https://www.cdc.gov/${dataKey}`
62
+ }
63
+ })
64
+ }
65
+ })
66
+ }
67
+
68
+ // For dashboard configs, convert dataKey references in visualizations
69
+ if (data.visualizations) {
70
+ Object.values(data.visualizations).forEach((viz: any) => {
71
+ // Only convert dataKey if it's a URL path (starts with / or ../)
72
+ if (viz.dataKey && (viz.dataKey.startsWith('/') || viz.dataKey.startsWith('../'))) {
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 only if they reference external files
80
+ if (data.datasets) {
81
+ const newDatasets = {}
82
+ Object.entries(data.datasets).forEach(([key, dataset]: [string, any]) => {
83
+ // Check if dataset has embedded data
84
+ const hasEmbeddedData = (dataset as any).data && Array.isArray((dataset as any).data)
85
+
86
+ // If data is embedded, keep the original key
87
+ if (hasEmbeddedData) {
88
+ newDatasets[key] = dataset
89
+ } else {
90
+ // Otherwise, convert paths to absolute URLs (but keep absolute URLs as-is)
91
+ const newKey = key.replace(/^(\.\.\/)+/, '').replace(/^\//, '')
92
+ const absoluteKey = key.startsWith('http') ? key : `https://www.cdc.gov/${newKey}`
93
+
94
+ newDatasets[absoluteKey] = {
95
+ ...dataset,
96
+ dataFileName: (dataset as any).dataFileName && !(dataset as any).dataFileName.startsWith('http')
97
+ ? `https://www.cdc.gov/${(dataset as any).dataFileName.replace(/^(\.\.\/)+/, '').replace(/^\//, '')}`
98
+ : (dataset as any).dataFileName,
99
+ dataUrl: (dataset as any).dataUrl && !(dataset as any).dataUrl.startsWith('http')
100
+ ? `https://www.cdc.gov/${(dataset as any).dataUrl.replace(/^(\.\.\/)+/, '').replace(/^\//, '')}`
101
+ : (dataset as any).dataUrl
102
+ }
103
+ }
104
+ })
105
+ data.datasets = newDatasets
106
+ }
107
+
108
+ // Set activeDashboard to 0 if it's null and multiDashboards exist
109
+ if (data.multiDashboards && data.multiDashboards.length > 0 && data.activeDashboard === null) {
110
+ data.activeDashboard = 0
111
+ }
112
+
113
+ // Log config info for debugging
114
+ console.log('✓ Config loaded:', {
115
+ type: data.type,
116
+ hasMultiDashboards: !!data.multiDashboards,
117
+ dashboardCount: data.multiDashboards?.length || 0,
118
+ activeDashboard: data.activeDashboard,
119
+ datasetCount: Object.keys(data.datasets || {}).length,
120
+ vizCount: data.visualizations ? Object.keys(data.visualizations).length : 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 = 30000 // Longer timeout for external data loading
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
+ * Explore by Location Dashboard
178
+ *
179
+ * Interactive dashboard showing BRFSS prevalence data organized by geographic location.
180
+ * Users can explore health indicators by state, territory, and metropolitan/micropolitan
181
+ * statistical areas (MMSA).
182
+ */
183
+ export const Explore_By_Location: DashboardStory = {
184
+ render: () => {
185
+ const config = useConfigWithAbsoluteDataUrl(CONFIG_URLS.exploreByLocation)
186
+ if (!config) return <div>Loading...</div>
187
+ return <Dashboard config={config} />
188
+ },
189
+ play: async ({ canvasElement }) => {
190
+ await testDashboardRendering(canvasElement, 'Explore By Location Dashboard')
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Explore by Topic Dashboard
196
+ *
197
+ * Interactive dashboard showing BRFSS prevalence data organized by health topics.
198
+ * Users can explore various health indicators and risk factors across different
199
+ * categories such as chronic diseases, health behaviors, and preventive practices.
200
+ */
201
+ export const Explore_By_Topic: DashboardStory = {
202
+ render: () => {
203
+ const config = useConfigWithAbsoluteDataUrl(CONFIG_URLS.exploreByTopic)
204
+ if (!config) return <div>Loading...</div>
205
+ return <Dashboard config={config} />
206
+ },
207
+ play: async ({ canvasElement }) => {
208
+ await testDashboardRendering(canvasElement, 'Explore By Topic Dashboard')
209
+ }
210
+ }
211
+
212
+ /**
213
+ * All BRFSS Dashboards - Combined Test
214
+ *
215
+ * Tests both BRFSS dashboards to ensure they all render correctly together.
216
+ */
217
+ export const All_BRFSS_Dashboards: StoryObj = {
218
+ render: () => {
219
+ const locationConfig = useConfigWithAbsoluteDataUrl(CONFIG_URLS.exploreByLocation)
220
+ const topicConfig = useConfigWithAbsoluteDataUrl(CONFIG_URLS.exploreByTopic)
221
+
222
+ if (!locationConfig || !topicConfig) {
223
+ return <div>Loading...</div>
224
+ }
225
+
226
+ return (
227
+ <div className="container-fluid p-4">
228
+ <h1 className="mb-4">BRFSS Prevalence Data - All Dashboards</h1>
229
+
230
+ <section className="mb-5">
231
+ <h2>Explore by Location</h2>
232
+ <Dashboard config={locationConfig} />
233
+ </section>
234
+
235
+ <section className="mb-5">
236
+ <h2>Explore by Topic</h2>
237
+ <Dashboard config={topicConfig} />
238
+ </section>
239
+ </div>
240
+ )
241
+ },
242
+ play: async ({ canvasElement }) => {
243
+ const canvas = within(canvasElement)
244
+
245
+ await step('Wait for all configs to load', async () => {
246
+ await new Promise<void>(resolve => {
247
+ const checkLoading = () => {
248
+ const loadingDiv = canvasElement.querySelector('div:not(.container-fluid)')
249
+ if (!loadingDiv || !loadingDiv.textContent?.includes('Loading')) {
250
+ resolve()
251
+ } else {
252
+ setTimeout(checkLoading, 100)
253
+ }
254
+ }
255
+ checkLoading()
256
+ })
257
+ })
258
+
259
+ await step('Wait for visualizations to start rendering', async () => {
260
+ await new Promise<void>(resolve => setTimeout(resolve, 2000))
261
+ })
262
+
263
+ await step('Wait for both dashboards to render', async () => {
264
+ await new Promise<void>((resolve, reject) => {
265
+ const startTime = Date.now()
266
+ const timeout = 40000
267
+
268
+ const checkDashboards = () => {
269
+ const dashboards = canvasElement.querySelectorAll('.cove-dashboard')
270
+ if (dashboards.length >= 2) {
271
+ resolve()
272
+ } else if (Date.now() - startTime > timeout) {
273
+ reject(new Error(`Timeout: Only ${dashboards.length}/2 dashboards found after ${timeout}ms`))
274
+ } else {
275
+ setTimeout(checkDashboards, 200)
276
+ }
277
+ }
278
+ checkDashboards()
279
+ })
280
+ })
281
+
282
+ await step('Verify both dashboards are present', async () => {
283
+ const dashboards = canvasElement.querySelectorAll('.cove-dashboard')
284
+ expect(dashboards.length).toBe(2)
285
+ })
286
+
287
+ console.log(` All 2 BRFSS dashboards rendered successfully`)
288
+ }
289
+ }