@cdc/core 4.26.1 → 4.26.3

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 (249) hide show
  1. package/.claude/agents/qa-test-developer.md +126 -0
  2. package/CLAUDE.local.md +67 -0
  3. package/LICENSE +201 -0
  4. package/_stories/Gallery.Charts.stories.tsx +35 -42
  5. package/_stories/Gallery.DataBite.stories.tsx +15 -8
  6. package/_stories/Gallery.Maps.stories.tsx +37 -28
  7. package/_stories/Gallery.WaffleChart.stories.tsx +1 -1
  8. package/_stories/PageART.stories.tsx +5 -4
  9. package/_stories/PageBRFSS.stories.tsx +21 -16
  10. package/_stories/PageCancerRegistries.stories.tsx +15 -15
  11. package/_stories/PageEasternEquineEncephalitis.stories.tsx +33 -19
  12. package/_stories/PageExcessiveAlcoholUse.stories.tsx +148 -143
  13. package/_stories/PageMaternalMortality.stories.tsx +5 -4
  14. package/_stories/PageOralHealth.stories.tsx +15 -10
  15. package/_stories/PageRespiratory.stories.tsx +4 -4
  16. package/_stories/PageSmokingTobacco.stories.tsx +15 -10
  17. package/_stories/PageStateDiabetesProfiles.stories.tsx +15 -10
  18. package/_stories/PageWastewater.stories.tsx +44 -30
  19. package/_stories/VegaImport.stories.tsx +401 -0
  20. package/_stories/vega-fixtures/bars-with-line.json +444 -0
  21. package/_stories/vega-fixtures/bars.json +58 -0
  22. package/_stories/vega-fixtures/combo-bar-rolling-mean.json +88 -0
  23. package/_stories/vega-fixtures/combo.json +68 -0
  24. package/_stories/vega-fixtures/grouped-horizontal-bars.json +83 -0
  25. package/_stories/vega-fixtures/grouped-horizontal-bars2.json +231 -0
  26. package/_stories/vega-fixtures/horizontal-bar.json +427 -0
  27. package/_stories/vega-fixtures/horizontal-bars-with-bad-colors.json +197 -0
  28. package/_stories/vega-fixtures/horizontal-bars2.json +58 -0
  29. package/_stories/vega-fixtures/lines.json +227 -0
  30. package/_stories/vega-fixtures/measles-bars.json +348 -0
  31. package/_stories/vega-fixtures/measles-map.json +11101 -0
  32. package/_stories/vega-fixtures/measles-stacked-bars.json +2147 -0
  33. package/_stories/vega-fixtures/multi-dataset.json +255 -0
  34. package/_stories/vega-fixtures/no-data.json +14 -0
  35. package/_stories/vega-fixtures/pie-chart.json +94 -0
  36. package/_stories/vega-fixtures/repeat-spec.json +47 -0
  37. package/_stories/vega-fixtures/stacked-area.json +222 -0
  38. package/_stories/vega-fixtures/stacked-bar-with-rect.json +3412 -0
  39. package/_stories/vega-fixtures/stacked-bars-with-line.json +364 -0
  40. package/_stories/vega-fixtures/stacked-bars.json +212 -0
  41. package/_stories/vega-fixtures/stacked-horizontal-bars.json +140 -0
  42. package/_stories/vega-fixtures/warning-combo.json +59 -0
  43. package/_stories/vega-fixtures/warning-scatter-and-line.json +1182 -0
  44. package/assets/callout-flag.svg +7 -0
  45. package/assets/icon-chart-area.svg +1 -0
  46. package/assets/icon-chart-radar.svg +23 -0
  47. package/assets/logo2.svg +31 -0
  48. package/components/AdvancedEditor/EmbedEditor.tsx +270 -38
  49. package/components/Alert/components/Alert.styles.css +2 -2
  50. package/components/ComboBox/combobox.styles.css +48 -48
  51. package/components/CustomColorsEditor/CustomColorsEditor.css +53 -53
  52. package/components/CustomColorsEditor/CustomColorsEditor.tsx +3 -10
  53. package/components/DataTable/DataTable.tsx +46 -18
  54. package/components/DataTable/DataTableStandAlone.tsx +1 -0
  55. package/components/DataTable/components/ChartHeader.tsx +21 -12
  56. package/components/DataTable/components/MapHeader.tsx +34 -28
  57. package/components/DataTable/components/SortIcon/sort-icon.css +5 -5
  58. package/components/DataTable/data-table.css +50 -52
  59. package/components/DataTable/helpers/applyCustomOrder.ts +17 -0
  60. package/components/DataTable/helpers/getChartCellValue.ts +10 -7
  61. package/components/DataTable/helpers/getMapDataTableColumnKeys.ts +22 -0
  62. package/components/DataTable/helpers/getSeriesName.ts +6 -0
  63. package/components/DataTable/helpers/mapCellMatrix.tsx +33 -23
  64. package/components/DataTable/helpers/tests/mapCellMatrix.test.ts +33 -0
  65. package/components/DownloadButton.tsx +14 -6
  66. package/components/EditorPanel/ColumnsEditor.tsx +38 -31
  67. package/components/EditorPanel/CustomSortOrder.tsx +94 -0
  68. package/components/EditorPanel/DataTableEditor.tsx +139 -23
  69. package/components/EditorPanel/EditorPanel.styles.css +71 -71
  70. package/components/EditorPanel/EditorPanel.tsx +3 -8
  71. package/components/EditorPanel/EditorPanelDispatch.tsx +4 -4
  72. package/components/EditorPanel/FootnotesEditor.tsx +2 -2
  73. package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +21 -12
  74. package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +16 -10
  75. package/components/EditorPanel/VizFilterEditor/components/FilterOrder.tsx +33 -29
  76. package/components/EditorPanel/components/MarkupVariablesEditor.tsx +160 -106
  77. package/components/EditorPanel/components/PanelMarkup.tsx +5 -1
  78. package/{styles/v2/components → components/EditorPanel}/editor.scss +76 -22
  79. package/components/EditorPanel/sections/StyleTreatmentSection.tsx +99 -0
  80. package/components/EditorPanel/sections/VisualSection.tsx +11 -0
  81. package/components/EditorWrapper/editor-wrapper.style.css +1 -1
  82. package/components/Filters/Filters.tsx +3 -5
  83. package/components/Filters/components/Tabs.tsx +19 -7
  84. package/{styles → components/Filters}/filters.scss +3 -3
  85. package/components/Footnotes/FootnotesStandAlone.tsx +4 -2
  86. package/components/HeaderThemeSelector/HeaderThemeSelector.css +61 -5
  87. package/components/Layout/components/Responsive.tsx +14 -6
  88. package/components/Layout/components/Sidebar/components/Sidebar.tsx +1 -1
  89. package/components/Layout/components/Sidebar/components/sidebar.styles.scss +14 -20
  90. package/components/Layout/components/Visualization/index.tsx +50 -38
  91. package/components/Layout/components/Visualization/visualizations.scss +232 -15
  92. package/components/Layout/components/VisualizationContainer.test.tsx +67 -0
  93. package/components/Layout/components/VisualizationContainer.tsx +37 -0
  94. package/components/Layout/components/VisualizationContent.test.tsx +182 -0
  95. package/components/Layout/components/VisualizationContent.tsx +75 -0
  96. package/components/Layout/index.tsx +5 -5
  97. package/components/Layout/styles/editor-utils.scss +3 -3
  98. package/components/Layout/styles/editor.scss +4 -4
  99. package/components/Legend/Legend.Gradient.tsx +7 -1
  100. package/components/Loader/loader.styles.css +2 -2
  101. package/components/Loading.jsx +1 -1
  102. package/components/MediaControls.tsx +10 -3
  103. package/components/MultiSelect/multiselect.styles.css +19 -19
  104. package/components/NestedDropdown/nesteddropdown.styles.css +15 -15
  105. package/components/PaletteSelector/PaletteSelector.css +15 -15
  106. package/components/RichTooltip/richTooltip.css +6 -6
  107. package/components/Table/table.styles.css +2 -2
  108. package/components/Waiting.tsx +1 -1
  109. package/components/_stories/CustomColorsEditor.stories.tsx +37 -0
  110. package/components/_stories/DataTable.stories.tsx +1 -0
  111. package/components/_stories/Filters.stories.tsx +1 -1
  112. package/components/_stories/styles.scss +0 -1
  113. package/components/elements/Button.jsx +1 -1
  114. package/components/elements/Card.jsx +1 -1
  115. package/{styles/v2/components → components/elements}/button.scss +9 -8
  116. package/components/inputs/InputCheckbox.jsx +1 -1
  117. package/components/inputs/InputSelect.tsx +1 -1
  118. package/components/inputs/InputText.jsx +1 -1
  119. package/components/inputs/InputToggle.tsx +1 -1
  120. package/{styles/v2/components/input → components/inputs}/_input-check-radio.scss +2 -2
  121. package/{styles/v2/components/input → components/inputs}/_input-group.scss +3 -3
  122. package/{styles/v2/components/input → components/inputs}/_input-slider.scss +2 -2
  123. package/{styles/v2/components/input → components/inputs}/_input.scss +5 -5
  124. package/{styles/v2/components/input → components/inputs}/index.scss +2 -2
  125. package/{styles → components}/loading.scss +1 -1
  126. package/components/managers/DataDesigner.tsx +1 -1
  127. package/{styles/v2/components → components/managers}/data-designer.scss +6 -7
  128. package/components/ui/Accordion.jsx +1 -1
  129. package/components/ui/Icon.tsx +1 -1
  130. package/components/ui/LoadSpin.jsx +1 -1
  131. package/components/ui/Modal.jsx +1 -1
  132. package/components/ui/Overlay.jsx +1 -1
  133. package/components/ui/Title/index.test.tsx +34 -0
  134. package/components/ui/Title/index.tsx +24 -7
  135. package/components/ui/Title/title.styles.css +119 -25
  136. package/components/ui/Tooltip.tsx +1 -1
  137. package/components/ui/_stories/Title.stories.tsx +1 -1
  138. package/{styles/v2/components → components/ui}/accordion.scss +3 -3
  139. package/components/ui/accordion.styles.css +11 -11
  140. package/{styles/v2/components → components/ui}/modal.scss +2 -2
  141. package/{styles/v2/components → components/ui}/overlay.scss +6 -6
  142. package/{styles/v2/components → components}/ui/tooltip.scss +1 -1
  143. package/{styles → components}/waiting.scss +9 -3
  144. package/data/colorPalettes.ts +18 -5
  145. package/data/mapColorPalettes.ts +10 -0
  146. package/devTemplate/dev.js +285 -0
  147. package/devTemplate/index.html +30 -0
  148. package/devTemplate/preview.html +1503 -0
  149. package/devTemplate/sidebar.css +151 -0
  150. package/dist/cove-main.css +2530 -3901
  151. package/dist/cove-main.css.map +1 -1
  152. package/generateViteConfig.js +111 -2
  153. package/helpers/DataTransform.ts +1 -5
  154. package/helpers/backfillDefaults.ts +35 -0
  155. package/helpers/constants.ts +12 -0
  156. package/helpers/cove/date.ts +64 -3
  157. package/helpers/cove/number.ts +29 -15
  158. package/helpers/cove/string.ts +29 -0
  159. package/helpers/coveUpdateWorker.ts +14 -8
  160. package/helpers/displayDataAsText.ts +1 -1
  161. package/helpers/embed/embedCodeGenerator.ts +80 -0
  162. package/helpers/embed/embedHelper.js +169 -0
  163. package/helpers/embed/filterUtils.ts +121 -0
  164. package/helpers/embed/index.ts +17 -0
  165. package/helpers/embed/urlValidation.ts +119 -0
  166. package/helpers/extractDataAndMetadata.ts +20 -0
  167. package/helpers/fetchRemoteData.ts +14 -8
  168. package/helpers/filterVizData.ts +6 -1
  169. package/helpers/getFileExtension.ts +0 -6
  170. package/helpers/labelHash.ts +9 -0
  171. package/helpers/markupProcessor.ts +56 -38
  172. package/helpers/metrics/types.ts +3 -0
  173. package/helpers/palettes/colorDistributions.ts +1 -1
  174. package/helpers/palettes/utils.ts +12 -12
  175. package/helpers/parseCsvWithQuotes.ts +15 -14
  176. package/helpers/prepareScreenshot.ts +33 -10
  177. package/helpers/testing.ts +44 -0
  178. package/helpers/tests/DataTransform.test.ts +125 -0
  179. package/helpers/tests/abbreviateNumber.test.ts +59 -0
  180. package/helpers/tests/backfillDefaults.test.ts +253 -0
  181. package/helpers/tests/date.test.ts +110 -0
  182. package/helpers/tests/extractDataAndMetadata.test.ts +93 -0
  183. package/helpers/tests/markupProcessor.test.ts +315 -124
  184. package/helpers/tests/number.test.ts +42 -0
  185. package/helpers/tests/prepareScreenshot.test.ts +28 -28
  186. package/helpers/tests/testStandaloneBuild.ts +36 -26
  187. package/helpers/tests/useDataVizClasses.test.ts +66 -0
  188. package/helpers/tests/visualizationWrapperUsage.test.ts +57 -0
  189. package/helpers/useDataVizClasses.ts +13 -7
  190. package/helpers/vegaConfig.ts +1 -1
  191. package/helpers/vegaConfigImport.ts +160 -0
  192. package/helpers/ver/4.24.4.ts +24 -0
  193. package/helpers/ver/4.26.1.ts +1 -1
  194. package/helpers/ver/4.26.2.ts +84 -0
  195. package/helpers/ver/4.26.3.ts +44 -0
  196. package/helpers/ver/4.26.4.ts +31 -0
  197. package/helpers/ver/tests/4.26.1.test.ts +105 -0
  198. package/helpers/ver/tests/4.26.2.test.ts +298 -0
  199. package/helpers/ver/tests/4.26.3.test.ts +168 -0
  200. package/helpers/ver/tests/4.26.4.test.ts +88 -0
  201. package/helpers/ver/tests/coveUpdateWorker.test.ts +57 -0
  202. package/helpers/viewports.ts +2 -0
  203. package/package.json +27 -32
  204. package/styles/_global.scss +7 -7
  205. package/styles/_reset.scss +2 -2
  206. package/styles/{v2/base → base}/_file-selector.scss +4 -4
  207. package/styles/{v2/base → base}/_general.scss +2 -4
  208. package/styles/{v2/base → base}/index.scss +1 -1
  209. package/styles/base.scss +107 -165
  210. package/styles/cove-main.scss +3 -6
  211. package/styles/layout/_component.scss +110 -0
  212. package/styles/{v2/layout → layout}/_data-table.scss +7 -7
  213. package/styles/layout/_wrapper-padding.scss +27 -0
  214. package/styles/{v2/main.scss → main.scss} +3 -1
  215. package/styles/{v2/themes → themes}/_color-definitions.scss +46 -41
  216. package/styles/{_accessibility.scss → utils/_accessibility.scss} +1 -1
  217. package/styles/{v2/utils → utils}/_grid.scss +8 -3
  218. package/styles/{_global-variables.scss → utils/_properties.scss} +133 -112
  219. package/styles/{v2/utils → utils}/index.scss +2 -1
  220. package/types/Annotation.ts +10 -11
  221. package/types/Axis.ts +2 -0
  222. package/types/ComponentStyles.ts +1 -0
  223. package/types/ConfigureData.ts +1 -0
  224. package/types/General.ts +2 -0
  225. package/types/MarkupInclude.ts +1 -0
  226. package/types/MarkupVariable.ts +2 -1
  227. package/types/Palette.ts +22 -0
  228. package/types/Table.ts +9 -0
  229. package/types/Visualization.ts +7 -0
  230. package/_stories/StoryRenderingTests.stories.tsx +0 -164
  231. package/helpers/embedCodeGenerator.ts +0 -109
  232. package/styles/_common-components.css +0 -73
  233. package/styles/_variables.scss +0 -63
  234. package/styles/v2/layout/_component.scss +0 -21
  235. package/styles/v2/utils/_variables.scss +0 -9
  236. package/{styles/v2/components/card.scss → components/elements/card.css} +2 -2
  237. /package/{styles/v2/components → components/ui}/icon.scss +0 -0
  238. /package/{styles/v2/components → components/ui}/loadspin.scss +0 -0
  239. /package/styles/{v2/base → base}/_heading.scss +0 -0
  240. /package/styles/{v2/base → base}/_reset.scss +0 -0
  241. /package/styles/{v2/layout → layout}/_alert.scss +0 -0
  242. /package/styles/{v2/layout → layout}/_progression.scss +0 -0
  243. /package/styles/{v2/layout → layout}/_tooltip.scss +0 -0
  244. /package/styles/{v2/layout → layout}/index.scss +0 -0
  245. /package/styles/{v2/themes → themes}/index.scss +0 -0
  246. /package/styles/{v2/utils → utils}/_align.scss +0 -0
  247. /package/styles/{v2/utils → utils}/_animations.scss +0 -0
  248. /package/styles/{v2/utils → utils}/_breakpoints.scss +0 -0
  249. /package/styles/{v2/utils → utils}/_mixins.scss +0 -0
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Generates iframe embed code for COVE visualizations
3
+ * Used by editor's "Share with Partners" feature
4
+ */
5
+
6
+ interface EmbedCodeOptions {
7
+ configUrl: string
8
+ /** Additional URL parameters (e.g., filter values, hide flags) */
9
+ urlParams?: Record<string, string>
10
+ }
11
+
12
+ /**
13
+ * Detect if we're in development mode
14
+ */
15
+ export function isDevMode(): boolean {
16
+ if (typeof window === 'undefined') return false
17
+ return window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'
18
+ }
19
+
20
+ /**
21
+ * Get the embed page URL for creating iframes.
22
+ * - In dev mode: returns localhost URL
23
+ * - On cdc.gov domains: returns relative path for same-origin iframe loading
24
+ * - On partner sites: returns full absolute URL to www.cdc.gov
25
+ */
26
+ export function getEmbedPageUrl(): string {
27
+ if (isDevMode()) {
28
+ return 'http://localhost:8080'
29
+ }
30
+
31
+ const embedPath = '/TemplatePackage/contrib/widgets/openVizWrapper/dist/embed/embed.html'
32
+
33
+ // On cdc.gov domains, use relative path for same-origin
34
+ if (typeof window !== 'undefined') {
35
+ const hostname = window.location.hostname
36
+ if (hostname === 'cdc.gov' || hostname.endsWith('.cdc.gov')) {
37
+ return embedPath
38
+ }
39
+ }
40
+
41
+ // For partner sites, use full absolute URL
42
+ return `https://www.cdc.gov${embedPath}`
43
+ }
44
+
45
+ /**
46
+ * Get default embed helper script URL based on environment
47
+ */
48
+ export function getHelperScriptUrl(): string {
49
+ if (isDevMode()) {
50
+ return 'http://localhost:8080/src/embed-helper/index.js'
51
+ }
52
+ return 'https://www.cdc.gov/TemplatePackage/contrib/widgets/openVizWrapper/dist/embed/embed-helper.js'
53
+ }
54
+
55
+ /**
56
+ * Generate embed code for partners (div-based)
57
+ *
58
+ * @param options.configUrl - URL to the published config JSON
59
+ * @param options.urlParams - Additional URL parameters (e.g., filter values, hide flags)
60
+ * @returns HTML string with div container and script tag (width/height are hardcoded in embed-helper)
61
+ */
62
+ export function generateEmbedCode(options: EmbedCodeOptions): string {
63
+ const { configUrl, urlParams = {} } = options
64
+
65
+ // Build full config URL with query parameters
66
+ const params = new URLSearchParams()
67
+ Object.entries(urlParams).forEach(([key, value]) => {
68
+ if (value) params.set(key, value)
69
+ })
70
+ const fullConfigUrl = params.toString() ? `${configUrl}?${params.toString()}` : configUrl
71
+
72
+ // Generate div-based embed code (width and height are hardcoded in embed-helper)
73
+ const embedCode = `<div
74
+ data-cove-embed
75
+ data-config-url="${fullConfigUrl}"
76
+ ></div>
77
+ <script type="module" src="${getHelperScriptUrl()}"></script>`
78
+
79
+ return embedCode
80
+ }
@@ -0,0 +1,169 @@
1
+ /**
2
+ * CDC COVE Embed Helper
3
+ *
4
+ * Standalone script that handles iframe creation and resizing for embedded COVE visualizations.
5
+ * Partners place a div container on their page with data-config-url attribute.
6
+ * This script finds the div, creates an iframe (width: 100%, initial height: 400px),
7
+ * and handles dynamic resizing.
8
+ *
9
+ * Usage:
10
+ * <div data-cove-embed data-config-url="/path/to/config.json"></div>
11
+ * <script src="https://www.cdc.gov/.../embed-helper.js"></script>
12
+ */
13
+
14
+ import { isValidMessageOrigin } from './urlValidation'
15
+ import { getEmbedPageUrl } from './embedCodeGenerator'
16
+
17
+ let iframeCounter = 0
18
+
19
+ // Initialize an iframe with unique ID and event listener
20
+ function initializeIframe(iframe) {
21
+ // Skip if already initialized
22
+ if (iframe.hasAttribute('data-cove-id')) {
23
+ return
24
+ }
25
+
26
+ const id = `cove-${iframeCounter++}`
27
+ iframe.setAttribute('data-cove-id', id)
28
+
29
+ // Send the ID and embed page URL to the iframe via postMessage
30
+ const sendId = () => {
31
+ if (iframe.contentWindow) {
32
+ iframe.contentWindow.postMessage(
33
+ {
34
+ type: 'cove:setId',
35
+ id: id,
36
+ embedPageUrl: window.location.origin + window.location.pathname
37
+ },
38
+ '*'
39
+ )
40
+ }
41
+ }
42
+
43
+ // If iframe is already loaded, send immediately
44
+ if (iframe.contentDocument && iframe.contentDocument.readyState === 'complete') {
45
+ sendId()
46
+ }
47
+
48
+ // Also listen for load event in case it hasn't loaded yet
49
+ iframe.addEventListener('load', sendId)
50
+ }
51
+
52
+ // Create iframe from div container
53
+ function createIframeFromContainer(container) {
54
+ // Skip if already processed
55
+ if (container.hasAttribute('data-cove-processed')) {
56
+ return
57
+ }
58
+ container.setAttribute('data-cove-processed', 'true')
59
+
60
+ // Read configuration from data attributes
61
+ const configUrl = container.dataset.configUrl
62
+
63
+ if (!configUrl) {
64
+ console.error('CDC COVE Embed: data-config-url attribute is required')
65
+ return
66
+ }
67
+
68
+ // Parse config URL to separate base path from query parameters
69
+ // data-config-url might be: "/path/config.json?state=CA&hidestate=true"
70
+ // We need to split into: configUrl + separate params for embed page
71
+ const [baseConfigUrl, queryString] = configUrl.split('?')
72
+
73
+ // Build clean URL manually (avoid encoding configUrl path)
74
+ let iframeSrc = `${getEmbedPageUrl()}?configUrl=${baseConfigUrl}`
75
+
76
+ // Add any additional query parameters from the config URL
77
+ if (queryString) {
78
+ iframeSrc += `&${queryString}`
79
+ }
80
+
81
+ // Create iframe element with hardcoded dimensions
82
+ const iframe = document.createElement('iframe')
83
+ iframe.src = iframeSrc
84
+
85
+ iframe.width = '100%'
86
+ iframe.height = '400'
87
+ iframe.frameBorder = '0'
88
+ iframe.title = 'CDC Data Visualization'
89
+ iframe.setAttribute('data-cove-embed', '')
90
+
91
+ // Apply min-height style to container
92
+ container.style.minHeight = '400px'
93
+
94
+ // Inject iframe into container
95
+ container.appendChild(iframe)
96
+
97
+ // Initialize the iframe for resizing
98
+ initializeIframe(iframe)
99
+ }
100
+
101
+ // Process existing div containers
102
+ const existingContainers = document.querySelectorAll('div[data-cove-embed]')
103
+ if (existingContainers.length > 0) {
104
+ existingContainers.forEach(createIframeFromContainer)
105
+ }
106
+
107
+ // Watch for dynamically added containers (for React/SPA apps)
108
+ const observer = new MutationObserver(mutations => {
109
+ mutations.forEach(mutation => {
110
+ mutation.addedNodes.forEach(node => {
111
+ if (node.nodeType === Node.ELEMENT_NODE) {
112
+ // Check for div containers
113
+ if (node.matches && node.matches('div[data-cove-embed]')) {
114
+ createIframeFromContainer(node)
115
+ }
116
+ // Also check children in case a container was added
117
+ const containers = node.querySelectorAll && node.querySelectorAll('div[data-cove-embed]')
118
+ if (containers && containers.length > 0) {
119
+ containers.forEach(createIframeFromContainer)
120
+ }
121
+ }
122
+ })
123
+ })
124
+ })
125
+
126
+ // Start observing the document for iframe additions
127
+ observer.observe(document.body, {
128
+ childList: true,
129
+ subtree: true
130
+ })
131
+
132
+ // Listen for resize messages from embedded visualizations
133
+ window.addEventListener('message', function (event) {
134
+ const type = event?.data?.type
135
+
136
+ // Ignore unrelated cross-window traffic (extensions, frameworks, etc.).
137
+ if (typeof type !== 'string' || !type.startsWith('cove:')) {
138
+ return
139
+ }
140
+
141
+ if (!isValidMessageOrigin(event.origin)) {
142
+ console.warn('CDC COVE Embed Helper: Rejected COVE message from invalid origin', {
143
+ origin: event.origin,
144
+ type,
145
+ id: event?.data?.id
146
+ })
147
+ return
148
+ }
149
+
150
+ // Handle resize events
151
+ if (type === 'cove:resize') {
152
+ const iframeId = event.data.id
153
+ const height = event.data.height
154
+
155
+ if (!height || typeof height !== 'number') {
156
+ console.warn('CDC COVE Embed Helper: Invalid height received:', height)
157
+ return
158
+ }
159
+
160
+ // Find the corresponding iframe
161
+ const iframe = document.querySelector(`iframe[data-cove-id="${iframeId}"]`)
162
+
163
+ if (iframe) {
164
+ iframe.style.height = height + 'px'
165
+ } else {
166
+ console.warn(`[Embed Helper] ✗ Could not find iframe with id "${iframeId}"`)
167
+ }
168
+ }
169
+ })
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Utilities for working with COVE filters in embed contexts
3
+ */
4
+
5
+ export type FilterMetadata = {
6
+ label: string
7
+ key: string
8
+ setByQueryParameter?: string
9
+ values?: any[]
10
+ defaultValue?: string
11
+ active?: string
12
+ }
13
+
14
+ export type FilterState = {
15
+ value: string
16
+ hide: boolean
17
+ }
18
+
19
+ /**
20
+ * Extract filter metadata from a COVE config
21
+ * Handles both regular viz filters and dashboard shared filters
22
+ */
23
+ export function extractFilters(config: any): FilterMetadata[] {
24
+ if (!config) return []
25
+
26
+ // Try regular filters first (charts, maps, etc.)
27
+ if (config.filters && Array.isArray(config.filters) && config.filters.length > 0) {
28
+ return config.filters.map(filter => normalizeFilter(filter))
29
+ }
30
+
31
+ // Try dashboard shared filters
32
+ if (config.dashboard?.sharedFilters && Array.isArray(config.dashboard.sharedFilters)) {
33
+ return config.dashboard.sharedFilters.map(filter => normalizeFilter(filter))
34
+ }
35
+
36
+ return []
37
+ }
38
+
39
+ /**
40
+ * Normalize a filter object to consistent metadata format
41
+ *
42
+ * Different filter types (chart filters vs dashboard filters) may use different field names.
43
+ * This function provides fallbacks to handle these variations:
44
+ *
45
+ * - label: Priority for readability: label > setByQueryParameter > columnName
46
+ * - key: May be called key, columnName, or id depending on the viz type
47
+ * - setByQueryParameter: MUST be used exactly as provided. COVE only recognizes this specific
48
+ * field for URL parameters - there are no fallbacks. Filters without this field cannot be
49
+ * controlled via URL parameters.
50
+ */
51
+ function normalizeFilter(filter: any): FilterMetadata {
52
+ const normalized = {
53
+ label: filter.label || filter.setByQueryParameter || filter.columnName || 'Unnamed Filter',
54
+ key: filter.key || filter.columnName || String(filter.id) || '',
55
+ setByQueryParameter: filter.setByQueryParameter,
56
+ values: filter.values || [],
57
+ defaultValue: filter.defaultValue,
58
+ active: filter.active
59
+ }
60
+ return normalized
61
+ }
62
+
63
+ /**
64
+ * Get initial/default value for a filter
65
+ */
66
+ export function getDefaultFilterValue(filter: FilterMetadata): string {
67
+ if (filter.defaultValue) return filter.defaultValue
68
+ if (filter.active) return filter.active
69
+ if (filter.values && filter.values.length > 0) return filter.values[0]
70
+ return ''
71
+ }
72
+
73
+ /**
74
+ * Initialize filter state from filter metadata
75
+ */
76
+ export function initializeFilterState(filters: FilterMetadata[]): Record<string, FilterState> {
77
+ const state: Record<string, FilterState> = {}
78
+
79
+ filters.forEach(filter => {
80
+ state[filter.key] = {
81
+ value: getDefaultFilterValue(filter),
82
+ hide: false
83
+ }
84
+ })
85
+
86
+ return state
87
+ }
88
+
89
+ /**
90
+ * Build URL parameters from filter state
91
+ * Returns an object with URL parameters for both filter values and hide states
92
+ */
93
+ export function buildFilterUrlParams(
94
+ filters: FilterMetadata[],
95
+ filterState: Record<string, FilterState>
96
+ ): Record<string, string> {
97
+ const urlParams: Record<string, string> = {}
98
+
99
+ filters.forEach(filter => {
100
+ if (!filter.setByQueryParameter) {
101
+ return
102
+ }
103
+
104
+ const state = filterState[filter.key]
105
+ if (!state) {
106
+ return
107
+ }
108
+
109
+ // Add filter value
110
+ if (state.value) {
111
+ urlParams[filter.setByQueryParameter] = state.value
112
+ }
113
+
114
+ // Add hide parameter
115
+ if (state.hide) {
116
+ urlParams[`hide${filter.setByQueryParameter}`] = 'true'
117
+ }
118
+ })
119
+
120
+ return urlParams
121
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Embed-related helpers for COVE visualizations
3
+ * Used for generating embed codes and managing filter customization
4
+ */
5
+
6
+ export { generateEmbedCode, getEmbedPageUrl, getHelperScriptUrl, isDevMode } from './embedCodeGenerator'
7
+
8
+ export {
9
+ extractFilters,
10
+ getDefaultFilterValue,
11
+ initializeFilterState,
12
+ buildFilterUrlParams,
13
+ type FilterMetadata,
14
+ type FilterState
15
+ } from './filterUtils'
16
+
17
+ export { getConfigUrlParam, isValidConfigUrl, isValidMessageOrigin } from './urlValidation'
@@ -0,0 +1,119 @@
1
+ /**
2
+ * URL validation utilities for embed functionality
3
+ */
4
+
5
+ /**
6
+ * Gets and validates the configUrl parameter from the current URL
7
+ * Returns the validated configUrl if it's a valid relative URL, null otherwise
8
+ *
9
+ * @returns The validated configUrl or null if missing/invalid
10
+ *
11
+ * @example
12
+ * const configUrl = getConfigUrlParam()
13
+ * if (!configUrl) {
14
+ * // Show error - missing or invalid
15
+ * }
16
+ */
17
+ export function getConfigUrlParam(): string | null {
18
+ const params = new URLSearchParams(window.location.search)
19
+ const configUrl = params.get('configUrl')
20
+
21
+ if (!configUrl) {
22
+ return null
23
+ }
24
+
25
+ // Validate that it's a relative URL
26
+ if (!isValidConfigUrl(configUrl)) {
27
+ return null
28
+ }
29
+
30
+ return configUrl
31
+ }
32
+
33
+ /**
34
+ * Validates that a configUrl is a relative URL (no protocol or host)
35
+ *
36
+ * Only relative URLs are allowed to ensure configs
37
+ * are loaded from the same origin as the embed page.
38
+ *
39
+ * @param configUrl - The URL to validate
40
+ * @returns true if the URL is valid (relative only), false otherwise
41
+ *
42
+ * @example
43
+ * isValidConfigUrl('/path/to/config.json') // true
44
+ * isValidConfigUrl('../config.json') // true
45
+ * isValidConfigUrl('config.json') // true
46
+ * isValidConfigUrl('https://evil.com/config.json') // false
47
+ * isValidConfigUrl('//evil.com/config.json') // false
48
+ * isValidConfigUrl('http://localhost/config.json') // false
49
+ */
50
+ export function isValidConfigUrl(configUrl: string | null): boolean {
51
+ if (!configUrl || typeof configUrl !== 'string') {
52
+ return false
53
+ }
54
+
55
+ const trimmed = configUrl.trim()
56
+
57
+ if (trimmed.length === 0) {
58
+ return false
59
+ }
60
+
61
+ // Reject any URL that contains a protocol (http://, https://, ftp://, etc.)
62
+ if (trimmed.match(/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//)) {
63
+ return false
64
+ }
65
+
66
+ // Reject protocol-relative URLs (//example.com/path)
67
+ if (trimmed.startsWith('//')) {
68
+ return false
69
+ }
70
+
71
+ // Reject URLs with protocols but no slashes (javascript:, data:, etc.)
72
+ if (trimmed.match(/^[a-zA-Z][a-zA-Z0-9+.-]*:/)) {
73
+ return false
74
+ }
75
+
76
+ // Additional validation: Try to parse it as a URL relative to current origin
77
+ try {
78
+ const parsed = new URL(trimmed, window.location.origin)
79
+
80
+ // Verify it's same origin
81
+ if (parsed.origin !== window.location.origin) {
82
+ return false
83
+ }
84
+
85
+ return true
86
+ } catch (error) {
87
+ return false
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Validates a postMessage origin against allowed CDC domains
93
+ *
94
+ * @param origin - The origin to validate (from MessageEvent.origin)
95
+ * @returns true if the origin is allowed, false otherwise
96
+ */
97
+ export function isValidMessageOrigin(origin: string): boolean {
98
+ if (!origin || typeof origin !== 'string') {
99
+ return false
100
+ }
101
+
102
+ try {
103
+ const url = new URL(origin)
104
+
105
+ // Allow localhost for development (HTTP only)
106
+ if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') {
107
+ return url.protocol === 'http:' || url.protocol === 'https:'
108
+ }
109
+
110
+ // Allow cdc.gov and any subdomain (HTTPS only)
111
+ if (url.hostname === 'cdc.gov' || url.hostname.endsWith('.cdc.gov')) {
112
+ return url.protocol === 'https:'
113
+ }
114
+
115
+ return false
116
+ } catch (error) {
117
+ return false
118
+ }
119
+ }
@@ -0,0 +1,20 @@
1
+ type DataWithMetadata = {
2
+ data: any[]
3
+ dataMetadata: Record<string, string>
4
+ }
5
+
6
+ /**
7
+ * Separates a data file response into the data array and any sibling metadata fields.
8
+ * Supports both plain arrays (current format) and the wrapper format:
9
+ * { "lastUpdated": "...", "source": "...", "data": [...] }
10
+ */
11
+ export function extractDataAndMetadata(response: any): DataWithMetadata {
12
+ if (Array.isArray(response)) {
13
+ return { data: response, dataMetadata: {} }
14
+ }
15
+ if (response && typeof response === 'object' && Array.isArray(response.data)) {
16
+ const { data, ...dataMetadata } = response
17
+ return { data, dataMetadata }
18
+ }
19
+ return { data: response, dataMetadata: {} }
20
+ }
@@ -1,7 +1,10 @@
1
1
  import { isSolrCsv } from '@cdc/core/helpers/isSolr'
2
2
  import { parseCsvWithQuotes } from '@cdc/core/helpers/parseCsvWithQuotes'
3
+ import { extractDataAndMetadata } from '@cdc/core/helpers/extractDataAndMetadata'
3
4
 
4
- export default function fetchRemoteData(_url) {
5
+ type FetchResult = { data: any[]; dataMetadata: Record<string, string> }
6
+
7
+ export default function fetchRemoteData(_url): Promise<FetchResult> {
5
8
  let url = new URL(_url, window.location.origin)
6
9
  const path = url.pathname
7
10
  const regex = /(?:\.([^.]+))?$/
@@ -11,17 +14,20 @@ export default function fetchRemoteData(_url) {
11
14
  return fetch(url.href)
12
15
  .then(response => response.text())
13
16
  .then(responseText => {
14
- return parseCsvWithQuotes(responseText, {
17
+ const data = parseCsvWithQuotes(responseText, {
15
18
  delimiter: '|',
16
19
  dynamicTyping: false
17
20
  })
21
+ return { data, dataMetadata: {} }
18
22
  })
19
23
  } else {
20
- return fetch(isSolrCsv(_url) ? _url : url.href).then(response => {
21
- if (!response.ok) {
22
- return Promise.reject(response)
23
- }
24
- return response.json()
25
- })
24
+ return fetch(isSolrCsv(_url) ? _url : url.href)
25
+ .then(response => {
26
+ if (!response.ok) {
27
+ return Promise.reject(response)
28
+ }
29
+ return response.json()
30
+ })
31
+ .then(json => extractDataAndMetadata(json))
26
32
  }
27
33
  }
@@ -16,10 +16,15 @@ export const filterVizData = (filters: Filter[], data) => {
16
16
  return []
17
17
  }
18
18
 
19
+ if (!Array.isArray(data)) {
20
+ console.warn('COVE: Data is not an array, received:', typeof data)
21
+ return []
22
+ }
23
+
19
24
  if (!filters) return data
20
25
  const filteredData: any[] = []
21
26
 
22
- data?.forEach(row => {
27
+ data.forEach(row => {
23
28
  let add = true
24
29
  filters
25
30
  .filter(filter => filter.type !== 'url')
@@ -1,9 +1,3 @@
1
- // export const getFileExtensionx = (path: string): string => {
2
- // const regex = /(?:\.([^.]+))?$/
3
- // const outCome: RegExpExecArray | null = regex.exec(path)
4
- // return outCome ? outCome[1] : ''
5
- // }
6
-
7
1
  export const getFileExtension = (url: string): string => {
8
2
  const regexForExtension = /(?:\.([^.]+))$/
9
3
  const regexForQueryParam = /[?&]wt=(csv|json)(?:&|$)/ // Regular expression for 'wt' query parameter
@@ -14,6 +14,15 @@ export const labelHash = {
14
14
  'filtered-text': 'Filtered Text',
15
15
  dashboardFilters: 'Filter Dropdowns',
16
16
  Sankey: 'Sankey Chart',
17
+ Combo: 'Combo',
18
+ 'Scatter Plot': 'Scatter Plot',
19
+ 'Area Chart': 'Area Chart',
20
+ 'Deviation Bar': 'Deviation Bar',
21
+ 'Paired Bar': 'Paired Bar',
22
+ 'Box Plot': 'Box Plot',
23
+ 'Forest Plot': 'Forest Plot',
24
+ Forecasting: 'Forecasting',
25
+ 'Warming Stripes': 'Warming Stripes',
17
26
  table: 'Table',
18
27
  'data-table': 'Data Table',
19
28
  chart: 'Chart',