@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.
- package/.claude/agents/qa-test-developer.md +126 -0
- package/CLAUDE.local.md +67 -0
- package/LICENSE +201 -0
- package/_stories/Gallery.Charts.stories.tsx +35 -42
- package/_stories/Gallery.DataBite.stories.tsx +15 -8
- package/_stories/Gallery.Maps.stories.tsx +37 -28
- package/_stories/Gallery.WaffleChart.stories.tsx +1 -1
- package/_stories/PageART.stories.tsx +5 -4
- package/_stories/PageBRFSS.stories.tsx +21 -16
- package/_stories/PageCancerRegistries.stories.tsx +15 -15
- package/_stories/PageEasternEquineEncephalitis.stories.tsx +33 -19
- package/_stories/PageExcessiveAlcoholUse.stories.tsx +148 -143
- package/_stories/PageMaternalMortality.stories.tsx +5 -4
- package/_stories/PageOralHealth.stories.tsx +15 -10
- package/_stories/PageRespiratory.stories.tsx +4 -4
- package/_stories/PageSmokingTobacco.stories.tsx +15 -10
- package/_stories/PageStateDiabetesProfiles.stories.tsx +15 -10
- package/_stories/PageWastewater.stories.tsx +44 -30
- package/_stories/VegaImport.stories.tsx +401 -0
- package/_stories/vega-fixtures/bars-with-line.json +444 -0
- package/_stories/vega-fixtures/bars.json +58 -0
- package/_stories/vega-fixtures/combo-bar-rolling-mean.json +88 -0
- package/_stories/vega-fixtures/combo.json +68 -0
- package/_stories/vega-fixtures/grouped-horizontal-bars.json +83 -0
- package/_stories/vega-fixtures/grouped-horizontal-bars2.json +231 -0
- package/_stories/vega-fixtures/horizontal-bar.json +427 -0
- package/_stories/vega-fixtures/horizontal-bars-with-bad-colors.json +197 -0
- package/_stories/vega-fixtures/horizontal-bars2.json +58 -0
- package/_stories/vega-fixtures/lines.json +227 -0
- package/_stories/vega-fixtures/measles-bars.json +348 -0
- package/_stories/vega-fixtures/measles-map.json +11101 -0
- package/_stories/vega-fixtures/measles-stacked-bars.json +2147 -0
- package/_stories/vega-fixtures/multi-dataset.json +255 -0
- package/_stories/vega-fixtures/no-data.json +14 -0
- package/_stories/vega-fixtures/pie-chart.json +94 -0
- package/_stories/vega-fixtures/repeat-spec.json +47 -0
- package/_stories/vega-fixtures/stacked-area.json +222 -0
- package/_stories/vega-fixtures/stacked-bar-with-rect.json +3412 -0
- package/_stories/vega-fixtures/stacked-bars-with-line.json +364 -0
- package/_stories/vega-fixtures/stacked-bars.json +212 -0
- package/_stories/vega-fixtures/stacked-horizontal-bars.json +140 -0
- package/_stories/vega-fixtures/warning-combo.json +59 -0
- package/_stories/vega-fixtures/warning-scatter-and-line.json +1182 -0
- package/assets/callout-flag.svg +7 -0
- package/assets/icon-chart-area.svg +1 -0
- package/assets/icon-chart-radar.svg +23 -0
- package/assets/logo2.svg +31 -0
- package/components/AdvancedEditor/EmbedEditor.tsx +270 -38
- package/components/Alert/components/Alert.styles.css +2 -2
- package/components/ComboBox/combobox.styles.css +48 -48
- package/components/CustomColorsEditor/CustomColorsEditor.css +53 -53
- package/components/CustomColorsEditor/CustomColorsEditor.tsx +3 -10
- package/components/DataTable/DataTable.tsx +46 -18
- package/components/DataTable/DataTableStandAlone.tsx +1 -0
- package/components/DataTable/components/ChartHeader.tsx +21 -12
- package/components/DataTable/components/MapHeader.tsx +34 -28
- package/components/DataTable/components/SortIcon/sort-icon.css +5 -5
- package/components/DataTable/data-table.css +50 -52
- package/components/DataTable/helpers/applyCustomOrder.ts +17 -0
- package/components/DataTable/helpers/getChartCellValue.ts +10 -7
- package/components/DataTable/helpers/getMapDataTableColumnKeys.ts +22 -0
- package/components/DataTable/helpers/getSeriesName.ts +6 -0
- package/components/DataTable/helpers/mapCellMatrix.tsx +33 -23
- package/components/DataTable/helpers/tests/mapCellMatrix.test.ts +33 -0
- package/components/DownloadButton.tsx +14 -6
- package/components/EditorPanel/ColumnsEditor.tsx +38 -31
- package/components/EditorPanel/CustomSortOrder.tsx +94 -0
- package/components/EditorPanel/DataTableEditor.tsx +139 -23
- package/components/EditorPanel/EditorPanel.styles.css +71 -71
- package/components/EditorPanel/EditorPanel.tsx +3 -8
- package/components/EditorPanel/EditorPanelDispatch.tsx +4 -4
- package/components/EditorPanel/FootnotesEditor.tsx +2 -2
- package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +21 -12
- package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +16 -10
- package/components/EditorPanel/VizFilterEditor/components/FilterOrder.tsx +33 -29
- package/components/EditorPanel/components/MarkupVariablesEditor.tsx +160 -106
- package/components/EditorPanel/components/PanelMarkup.tsx +5 -1
- package/{styles/v2/components → components/EditorPanel}/editor.scss +76 -22
- package/components/EditorPanel/sections/StyleTreatmentSection.tsx +99 -0
- package/components/EditorPanel/sections/VisualSection.tsx +11 -0
- package/components/EditorWrapper/editor-wrapper.style.css +1 -1
- package/components/Filters/Filters.tsx +3 -5
- package/components/Filters/components/Tabs.tsx +19 -7
- package/{styles → components/Filters}/filters.scss +3 -3
- package/components/Footnotes/FootnotesStandAlone.tsx +4 -2
- package/components/HeaderThemeSelector/HeaderThemeSelector.css +61 -5
- package/components/Layout/components/Responsive.tsx +14 -6
- package/components/Layout/components/Sidebar/components/Sidebar.tsx +1 -1
- package/components/Layout/components/Sidebar/components/sidebar.styles.scss +14 -20
- package/components/Layout/components/Visualization/index.tsx +50 -38
- package/components/Layout/components/Visualization/visualizations.scss +232 -15
- package/components/Layout/components/VisualizationContainer.test.tsx +67 -0
- package/components/Layout/components/VisualizationContainer.tsx +37 -0
- package/components/Layout/components/VisualizationContent.test.tsx +182 -0
- package/components/Layout/components/VisualizationContent.tsx +75 -0
- package/components/Layout/index.tsx +5 -5
- package/components/Layout/styles/editor-utils.scss +3 -3
- package/components/Layout/styles/editor.scss +4 -4
- package/components/Legend/Legend.Gradient.tsx +7 -1
- package/components/Loader/loader.styles.css +2 -2
- package/components/Loading.jsx +1 -1
- package/components/MediaControls.tsx +10 -3
- package/components/MultiSelect/multiselect.styles.css +19 -19
- package/components/NestedDropdown/nesteddropdown.styles.css +15 -15
- package/components/PaletteSelector/PaletteSelector.css +15 -15
- package/components/RichTooltip/richTooltip.css +6 -6
- package/components/Table/table.styles.css +2 -2
- package/components/Waiting.tsx +1 -1
- package/components/_stories/CustomColorsEditor.stories.tsx +37 -0
- package/components/_stories/DataTable.stories.tsx +1 -0
- package/components/_stories/Filters.stories.tsx +1 -1
- package/components/_stories/styles.scss +0 -1
- package/components/elements/Button.jsx +1 -1
- package/components/elements/Card.jsx +1 -1
- package/{styles/v2/components → components/elements}/button.scss +9 -8
- package/components/inputs/InputCheckbox.jsx +1 -1
- package/components/inputs/InputSelect.tsx +1 -1
- package/components/inputs/InputText.jsx +1 -1
- package/components/inputs/InputToggle.tsx +1 -1
- package/{styles/v2/components/input → components/inputs}/_input-check-radio.scss +2 -2
- package/{styles/v2/components/input → components/inputs}/_input-group.scss +3 -3
- package/{styles/v2/components/input → components/inputs}/_input-slider.scss +2 -2
- package/{styles/v2/components/input → components/inputs}/_input.scss +5 -5
- package/{styles/v2/components/input → components/inputs}/index.scss +2 -2
- package/{styles → components}/loading.scss +1 -1
- package/components/managers/DataDesigner.tsx +1 -1
- package/{styles/v2/components → components/managers}/data-designer.scss +6 -7
- package/components/ui/Accordion.jsx +1 -1
- package/components/ui/Icon.tsx +1 -1
- package/components/ui/LoadSpin.jsx +1 -1
- package/components/ui/Modal.jsx +1 -1
- package/components/ui/Overlay.jsx +1 -1
- package/components/ui/Title/index.test.tsx +34 -0
- package/components/ui/Title/index.tsx +24 -7
- package/components/ui/Title/title.styles.css +119 -25
- package/components/ui/Tooltip.tsx +1 -1
- package/components/ui/_stories/Title.stories.tsx +1 -1
- package/{styles/v2/components → components/ui}/accordion.scss +3 -3
- package/components/ui/accordion.styles.css +11 -11
- package/{styles/v2/components → components/ui}/modal.scss +2 -2
- package/{styles/v2/components → components/ui}/overlay.scss +6 -6
- package/{styles/v2/components → components}/ui/tooltip.scss +1 -1
- package/{styles → components}/waiting.scss +9 -3
- package/data/colorPalettes.ts +18 -5
- package/data/mapColorPalettes.ts +10 -0
- package/devTemplate/dev.js +285 -0
- package/devTemplate/index.html +30 -0
- package/devTemplate/preview.html +1503 -0
- package/devTemplate/sidebar.css +151 -0
- package/dist/cove-main.css +2530 -3901
- package/dist/cove-main.css.map +1 -1
- package/generateViteConfig.js +111 -2
- package/helpers/DataTransform.ts +1 -5
- package/helpers/backfillDefaults.ts +35 -0
- package/helpers/constants.ts +12 -0
- package/helpers/cove/date.ts +64 -3
- package/helpers/cove/number.ts +29 -15
- package/helpers/cove/string.ts +29 -0
- package/helpers/coveUpdateWorker.ts +14 -8
- package/helpers/displayDataAsText.ts +1 -1
- package/helpers/embed/embedCodeGenerator.ts +80 -0
- package/helpers/embed/embedHelper.js +169 -0
- package/helpers/embed/filterUtils.ts +121 -0
- package/helpers/embed/index.ts +17 -0
- package/helpers/embed/urlValidation.ts +119 -0
- package/helpers/extractDataAndMetadata.ts +20 -0
- package/helpers/fetchRemoteData.ts +14 -8
- package/helpers/filterVizData.ts +6 -1
- package/helpers/getFileExtension.ts +0 -6
- package/helpers/labelHash.ts +9 -0
- package/helpers/markupProcessor.ts +56 -38
- package/helpers/metrics/types.ts +3 -0
- package/helpers/palettes/colorDistributions.ts +1 -1
- package/helpers/palettes/utils.ts +12 -12
- package/helpers/parseCsvWithQuotes.ts +15 -14
- package/helpers/prepareScreenshot.ts +33 -10
- package/helpers/testing.ts +44 -0
- package/helpers/tests/DataTransform.test.ts +125 -0
- package/helpers/tests/abbreviateNumber.test.ts +59 -0
- package/helpers/tests/backfillDefaults.test.ts +253 -0
- package/helpers/tests/date.test.ts +110 -0
- package/helpers/tests/extractDataAndMetadata.test.ts +93 -0
- package/helpers/tests/markupProcessor.test.ts +315 -124
- package/helpers/tests/number.test.ts +42 -0
- package/helpers/tests/prepareScreenshot.test.ts +28 -28
- package/helpers/tests/testStandaloneBuild.ts +36 -26
- package/helpers/tests/useDataVizClasses.test.ts +66 -0
- package/helpers/tests/visualizationWrapperUsage.test.ts +57 -0
- package/helpers/useDataVizClasses.ts +13 -7
- package/helpers/vegaConfig.ts +1 -1
- package/helpers/vegaConfigImport.ts +160 -0
- package/helpers/ver/4.24.4.ts +24 -0
- package/helpers/ver/4.26.1.ts +1 -1
- package/helpers/ver/4.26.2.ts +84 -0
- package/helpers/ver/4.26.3.ts +44 -0
- package/helpers/ver/4.26.4.ts +31 -0
- package/helpers/ver/tests/4.26.1.test.ts +105 -0
- package/helpers/ver/tests/4.26.2.test.ts +298 -0
- package/helpers/ver/tests/4.26.3.test.ts +168 -0
- package/helpers/ver/tests/4.26.4.test.ts +88 -0
- package/helpers/ver/tests/coveUpdateWorker.test.ts +57 -0
- package/helpers/viewports.ts +2 -0
- package/package.json +27 -32
- package/styles/_global.scss +7 -7
- package/styles/_reset.scss +2 -2
- package/styles/{v2/base → base}/_file-selector.scss +4 -4
- package/styles/{v2/base → base}/_general.scss +2 -4
- package/styles/{v2/base → base}/index.scss +1 -1
- package/styles/base.scss +107 -165
- package/styles/cove-main.scss +3 -6
- package/styles/layout/_component.scss +110 -0
- package/styles/{v2/layout → layout}/_data-table.scss +7 -7
- package/styles/layout/_wrapper-padding.scss +27 -0
- package/styles/{v2/main.scss → main.scss} +3 -1
- package/styles/{v2/themes → themes}/_color-definitions.scss +46 -41
- package/styles/{_accessibility.scss → utils/_accessibility.scss} +1 -1
- package/styles/{v2/utils → utils}/_grid.scss +8 -3
- package/styles/{_global-variables.scss → utils/_properties.scss} +133 -112
- package/styles/{v2/utils → utils}/index.scss +2 -1
- package/types/Annotation.ts +10 -11
- package/types/Axis.ts +2 -0
- package/types/ComponentStyles.ts +1 -0
- package/types/ConfigureData.ts +1 -0
- package/types/General.ts +2 -0
- package/types/MarkupInclude.ts +1 -0
- package/types/MarkupVariable.ts +2 -1
- package/types/Palette.ts +22 -0
- package/types/Table.ts +9 -0
- package/types/Visualization.ts +7 -0
- package/_stories/StoryRenderingTests.stories.tsx +0 -164
- package/helpers/embedCodeGenerator.ts +0 -109
- package/styles/_common-components.css +0 -73
- package/styles/_variables.scss +0 -63
- package/styles/v2/layout/_component.scss +0 -21
- package/styles/v2/utils/_variables.scss +0 -9
- package/{styles/v2/components/card.scss → components/elements/card.css} +2 -2
- /package/{styles/v2/components → components/ui}/icon.scss +0 -0
- /package/{styles/v2/components → components/ui}/loadspin.scss +0 -0
- /package/styles/{v2/base → base}/_heading.scss +0 -0
- /package/styles/{v2/base → base}/_reset.scss +0 -0
- /package/styles/{v2/layout → layout}/_alert.scss +0 -0
- /package/styles/{v2/layout → layout}/_progression.scss +0 -0
- /package/styles/{v2/layout → layout}/_tooltip.scss +0 -0
- /package/styles/{v2/layout → layout}/index.scss +0 -0
- /package/styles/{v2/themes → themes}/index.scss +0 -0
- /package/styles/{v2/utils → utils}/_align.scss +0 -0
- /package/styles/{v2/utils → utils}/_animations.scss +0 -0
- /package/styles/{v2/utils → utils}/_breakpoints.scss +0 -0
- /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
|
-
|
|
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
|
-
|
|
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)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
}
|
package/helpers/filterVizData.ts
CHANGED
|
@@ -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
|
|
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
|
package/helpers/labelHash.ts
CHANGED
|
@@ -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',
|