@cdc/core 4.25.11 → 4.26.2
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/_stories/Gallery.Charts.stories.tsx +300 -0
- package/_stories/Gallery.DataBite.stories.tsx +79 -0
- package/_stories/Gallery.Maps.stories.tsx +239 -0
- package/_stories/Gallery.WaffleChart.stories.tsx +187 -0
- package/_stories/PageART.stories.tsx +193 -0
- package/_stories/PageBRFSS.stories.tsx +294 -0
- package/_stories/PageCancerRegistries.stories.tsx +199 -0
- package/_stories/PageEasternEquineEncephalitis.stories.tsx +216 -0
- package/_stories/PageExcessiveAlcoholUse.stories.tsx +201 -0
- package/_stories/PageMaternalMortality.stories.tsx +193 -0
- package/_stories/PageOralHealth.stories.tsx +201 -0
- package/_stories/PageRespiratory.stories.tsx +332 -0
- package/_stories/PageSmokingTobacco.stories.tsx +200 -0
- package/_stories/PageStateDiabetesProfiles.stories.tsx +201 -0
- package/_stories/PageWastewater.stories.tsx +477 -0
- 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/icon-chart-area.svg +1 -0
- package/assets/icon-chart-radar.svg +23 -0
- package/assets/icon-magnifying-glass.svg +5 -0
- package/assets/icon-warming-stripes.svg +13 -0
- package/assets/logo2.svg +31 -0
- package/components/AdvancedEditor/AdvancedEditor.tsx +4 -0
- package/components/AdvancedEditor/EmbedEditor.tsx +513 -0
- package/components/ComboBox/ComboBox.tsx +345 -0
- package/components/ComboBox/combobox.styles.css +185 -0
- package/components/ComboBox/index.ts +1 -0
- package/components/CustomColorsEditor/CustomColorsEditor.tsx +3 -10
- package/components/DataTable/DataTable.tsx +132 -58
- package/components/DataTable/data-table.css +216 -215
- package/components/DataTable/helpers/getSeriesName.ts +6 -0
- package/components/DataTable/helpers/mapCellMatrix.tsx +14 -6
- package/components/EditorPanel/ColumnsEditor.tsx +37 -19
- package/components/EditorPanel/DataTableEditor.tsx +51 -25
- package/components/EditorPanel/EditorPanel.styles.css +16 -0
- package/components/EditorPanel/EditorPanel.tsx +144 -0
- package/components/EditorPanel/EditorPanelDispatch.tsx +75 -0
- package/components/EditorPanel/FieldSetWrapper.tsx +66 -23
- package/components/EditorPanel/Inputs.tsx +33 -7
- package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +14 -6
- package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +240 -175
- package/components/EditorPanel/VizFilterEditor/components/FilterOrder.tsx +33 -29
- package/components/EditorPanel/sections/VisualSection.tsx +169 -0
- package/components/Filters/Filters.tsx +31 -5
- package/components/Filters/helpers/getNestedOptions.ts +2 -1
- package/components/Filters/helpers/handleSorting.ts +1 -1
- package/components/Layout/components/Sidebar/components/sidebar.styles.scss +84 -2
- package/components/Layout/components/Visualization/index.tsx +27 -1
- package/components/Layout/components/Visualization/visualizations.scss +7 -0
- package/components/Legend/Legend.Gradient.tsx +1 -1
- package/components/MediaControls.tsx +53 -28
- package/components/_stories/CustomColorsEditor.stories.tsx +37 -0
- package/components/_stories/DataTable.stories.tsx +1 -0
- package/components/ui/Icon.tsx +3 -1
- package/components/ui/Title/index.tsx +30 -2
- package/components/ui/Title/title.styles.css +42 -0
- package/data/colorPalettes.ts +18 -5
- package/data/mapColorPalettes.ts +10 -0
- package/devTemplate/dev.js +235 -0
- package/devTemplate/index.html +30 -0
- package/devTemplate/preview.html +1503 -0
- package/devTemplate/sidebar.css +151 -0
- package/dist/cove-main.css +2803 -4448
- package/dist/cove-main.css.map +1 -1
- package/generateViteConfig.js +118 -2
- package/helpers/DataTransform.ts +1 -5
- package/helpers/addValuesToFilters.ts +6 -1
- package/helpers/cove/date.ts +33 -1
- package/helpers/cove/string.ts +29 -0
- package/helpers/coveUpdateWorker.ts +21 -12
- package/helpers/embed/embedCodeGenerator.ts +80 -0
- package/helpers/embed/embedHelper.js +158 -0
- package/helpers/embed/filterUtils.ts +121 -0
- package/helpers/embed/index.ts +21 -0
- package/helpers/embed/urlValidation.ts +119 -0
- package/helpers/filterVizData.ts +6 -1
- package/helpers/getFileExtension.ts +0 -6
- package/helpers/getUniqueValues.ts +19 -0
- package/helpers/hashObj.ts +25 -0
- package/helpers/isRightAlignedTableValue.js +5 -0
- package/helpers/metrics/helpers.ts +1 -0
- 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/pivotData.ts +2 -2
- package/helpers/prepareScreenshot.ts +288 -0
- package/helpers/queryStringUtils.ts +29 -0
- package/helpers/testing.ts +44 -0
- package/helpers/tests/DataTransform.test.ts +125 -0
- package/helpers/tests/date.test.ts +64 -0
- package/helpers/tests/prepareScreenshot.test.ts +414 -0
- package/helpers/tests/queryStringUtils.test.ts +381 -0
- package/helpers/tests/testStandaloneBuild.ts +23 -5
- package/helpers/useDataVizClasses.ts +0 -1
- package/helpers/vegaConfig.ts +1 -1
- package/helpers/vegaConfigImport.ts +160 -0
- package/helpers/ver/4.26.1.ts +80 -0
- package/helpers/ver/4.26.2.ts +84 -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/viewports.ts +2 -0
- package/hooks/useDataColumns.ts +63 -0
- package/hooks/useFilterManagement.ts +94 -0
- package/hooks/useLegendSeparators.ts +26 -0
- package/hooks/useListManagement.ts +192 -0
- package/package.json +29 -33
- package/styles/_button-section.scss +0 -3
- package/styles/v2/components/editor.scss +9 -9
- package/styles/v2/utils/_grid.scss +8 -3
- package/types/Annotation.ts +10 -11
- package/types/Axis.ts +1 -0
- package/types/ForecastingSeriesKey.ts +1 -0
- package/types/General.ts +2 -0
- package/types/MarkupInclude.ts +1 -0
- package/types/Palette.ts +21 -0
- package/types/Series.ts +3 -0
- package/types/Table.ts +1 -0
- package/types/Visualization.ts +7 -0
- package/types/VizFilter.ts +1 -0
- package/LICENSE +0 -201
- package/_stories/StoryRenderingTests.stories.tsx +0 -164
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free v6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M64 64c0-17.7-14.3-32-32-32S0 46.3 0 64L0 400c0 44.2 35.8 80 80 80l400 0c17.7 0 32-14.3 32-32s-14.3-32-32-32L80 416c-8.8 0-16-7.2-16-16L64 64zm96 288l288 0c17.7 0 32-14.3 32-32l0-68.2c0-7.6-2.7-15-7.7-20.8l-65.8-76.8c-12.1-14.2-33.7-15-46.9-1.8l-21 21c-10 10-26.4 9.2-35.4-1.6l-39.2-47c-12.6-15.1-35.7-15.4-48.7-.6L135.9 215c-5.1 5.8-7.9 13.3-7.9 21.1l0 84c0 17.7 14.3 32 32 32z"/></svg>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="currentColor">
|
|
2
|
+
<!-- Outer pentagon (grid) -->
|
|
3
|
+
<path d="M256 32l211.25 153.5L175 478.5H337l81.25-293L256 32zm0 0L44.75 185.5L175 478.5h162L175 478.5 44.75 185.5 256 32z" fill="none" stroke="currentColor" stroke-width="20" stroke-linejoin="round"/>
|
|
4
|
+
<!-- Middle pentagon (grid ring) -->
|
|
5
|
+
<path d="M256 112l126.75 92.1L207.5 375.3h97l48.75-171.2L256 112z" fill="none" stroke="currentColor" stroke-width="12" stroke-opacity="0.4" stroke-linejoin="round"/>
|
|
6
|
+
<path d="M256 112l-126.75 92.1L207.5 375.3h-97l-48.75-171.2L256 112z" fill="none" stroke="currentColor" stroke-width="12" stroke-opacity="0.4" stroke-linejoin="round"/>
|
|
7
|
+
<!-- Inner pentagon (grid ring) -->
|
|
8
|
+
<path d="M256 192l63.4 46L289 332.7h-66l-30-94.7L256 192z" fill="none" stroke="currentColor" stroke-width="8" stroke-opacity="0.3" stroke-linejoin="round"/>
|
|
9
|
+
<!-- Axis lines -->
|
|
10
|
+
<line x1="256" y1="256" x2="256" y2="32" stroke="currentColor" stroke-width="8" stroke-opacity="0.5"/>
|
|
11
|
+
<line x1="256" y1="256" x2="467.25" y2="185.5" stroke="currentColor" stroke-width="8" stroke-opacity="0.5"/>
|
|
12
|
+
<line x1="256" y1="256" x2="337" y2="478.5" stroke="currentColor" stroke-width="8" stroke-opacity="0.5"/>
|
|
13
|
+
<line x1="256" y1="256" x2="175" y2="478.5" stroke="currentColor" stroke-width="8" stroke-opacity="0.5"/>
|
|
14
|
+
<line x1="256" y1="256" x2="44.75" y2="185.5" stroke="currentColor" stroke-width="8" stroke-opacity="0.5"/>
|
|
15
|
+
<!-- Data polygon (filled) -->
|
|
16
|
+
<polygon points="256,80 400,200 350,420 162,420 112,200" fill="currentColor" fill-opacity="0.3" stroke="currentColor" stroke-width="16" stroke-linejoin="round"/>
|
|
17
|
+
<!-- Data points -->
|
|
18
|
+
<circle cx="256" cy="80" r="12" fill="currentColor"/>
|
|
19
|
+
<circle cx="400" cy="200" r="12" fill="currentColor"/>
|
|
20
|
+
<circle cx="350" cy="420" r="12" fill="currentColor"/>
|
|
21
|
+
<circle cx="162" cy="420" r="12" fill="currentColor"/>
|
|
22
|
+
<circle cx="112" cy="200" r="12" fill="currentColor"/>
|
|
23
|
+
</svg>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" viewBox="0 0 512 512" fill="currentColor">
|
|
2
|
+
<!-- Outlined/Regular style magnifying glass -->
|
|
3
|
+
<path d="M208 48a160 160 0 1 1 0 320 160 160 0 1 1 0-320zm0 368c48.8 0 93.7-16.8 129.1-44.9L471 505c9.4 9.4 24.6 9.4 33.9 0s9.4-24.6 0-33.9L371.1 337.1C399.2 301.7 416 256.8 416 208C416 93.1 322.9 0 208 0S0 93.1 0 208s93.1 208 208 208z"/>
|
|
4
|
+
</svg>
|
|
5
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
|
2
|
+
<rect x="32" y="64" width="32" height="384" fill="#000000"/>
|
|
3
|
+
<rect x="72" y="64" width="32" height="384" fill="#1a1a1a"/>
|
|
4
|
+
<rect x="112" y="64" width="32" height="384" fill="#333333"/>
|
|
5
|
+
<rect x="152" y="64" width="32" height="384" fill="#666666"/>
|
|
6
|
+
<rect x="192" y="64" width="32" height="384" fill="#999999"/>
|
|
7
|
+
<rect x="232" y="64" width="32" height="384" fill="#b3b3b3"/>
|
|
8
|
+
<rect x="272" y="64" width="32" height="384" fill="#cccccc"/>
|
|
9
|
+
<rect x="312" y="64" width="32" height="384" fill="#e6e6e6"/>
|
|
10
|
+
<rect x="352" y="64" width="32" height="384" fill="#f2f2f2"/>
|
|
11
|
+
<rect x="392" y="64" width="32" height="384" fill="#fafafa"/>
|
|
12
|
+
<rect x="432" y="64" width="32" height="384" fill="#ffffff"/>
|
|
13
|
+
</svg>
|
package/assets/logo2.svg
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<!-- Generator: Adobe Illustrator 27.9.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
|
3
|
+
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
|
4
|
+
viewBox="135 160 228 147" xml:space="preserve">
|
|
5
|
+
<style type="text/css">
|
|
6
|
+
.st0{fill:#0055B8;}
|
|
7
|
+
.st1{fill:#FFFFFF;}
|
|
8
|
+
</style>
|
|
9
|
+
<g>
|
|
10
|
+
<path class="st0" d="M141.22,300.14H186h151.44c10.88,0,19.84-8.27,20.91-18.87c0.07-0.71,0.11-1.42,0.11-2.15V165.04H188.93
|
|
11
|
+
h-26.68c-7.26,0-13.65,3.68-17.43,9.27c-0.76,1.12-1.41,2.31-1.94,3.57c-1.06,2.52-1.65,5.28-1.65,8.18V300.14z"/>
|
|
12
|
+
<path class="st1" d="M162.25,160.83c-13.91,0-25.23,11.32-25.23,25.23v118.28h31.62h6.95h161.85c13.91,0,25.23-11.32,25.23-25.23
|
|
13
|
+
V160.83H162.25z M326.53,239.21c-0.2-0.23-0.56-0.25-0.78-0.05c-1.32,1.19-5.84,4.76-12.61,4.88c-8.53,0.14-17.17-6.93-17.17-19.2
|
|
14
|
+
c0-12.27,8.92-19.21,17.27-19.21c6.2,0,10.2,2.92,11.41,3.95c0.23,0.19,0.57,0.17,0.77-0.06l7.57-8.41c0.18-0.2,0.2-0.51,0.02-0.72
|
|
15
|
+
c-1.24-1.49-6.57-6.89-18.51-6.89c-1.11,0-2.25,0.06-3.41,0.17l45.61-28.63h1.76v62.45l-31.15,12.59L326.53,239.21z M243.33,242.49
|
|
16
|
+
h-6.37c-0.31,0-0.56-0.25-0.56-0.56v-34.78c0-0.31,0.25-0.56,0.56-0.56h7.31c11.28,0,18.77,5.56,18.77,17.43
|
|
17
|
+
C263.04,237.79,255.72,242.49,243.33,242.49z M240.95,194.29h-17.32c-0.31,0-0.56,0.25-0.56,0.56v59.4c0,0.08,0.02,0.15,0.04,0.22
|
|
18
|
+
l-65.24,45.68h-6.1l40.05-44.48c2.03,0.34,4.03,0.51,5.93,0.51c11.85,0,18.13-6.24,19.55-7.87c0.19-0.21,0.19-0.53,0-0.74
|
|
19
|
+
l-7.52-8.36c-0.2-0.23-0.56-0.25-0.78-0.05c-1.32,1.19-5.84,4.76-12.61,4.88c-8.53,0.14-17.17-6.93-17.17-19.2
|
|
20
|
+
c0-12.27,8.92-19.21,17.27-19.21c6.2,0,10.2,2.92,11.41,3.95c0.23,0.19,0.57,0.17,0.77-0.06l7.57-8.41c0.18-0.2,0.2-0.51,0.02-0.72
|
|
21
|
+
c-1.24-1.49-6.57-6.89-18.51-6.89c-5.92,0-12.69,1.53-18.48,5.1l12.88-33.56h76.66L240.95,194.29z M142.88,177.88
|
|
22
|
+
c0.53-1.26,1.18-2.45,1.94-3.57c3.78-5.59,10.18-9.27,17.43-9.27h26.35l-14.39,37.49c-5.11,4.98-8.62,12.23-8.62,22.3
|
|
23
|
+
c0,0.05,0,0.1,0,0.15l-24.37,63.49V186.06C141.22,183.16,141.81,180.39,142.88,177.88z M141.22,297.77l25.1-65.41
|
|
24
|
+
c2.66,12.93,12.13,19.81,21.77,22.47l-40.8,45.31h-6.07V297.77z M163.69,300.14l64.76-45.34h16.43c18.03,0,32.01-10.51,32.01-30.08
|
|
25
|
+
c0-21.2-13.14-29.7-31.38-30.37l27.92-29.3h77.01l-49.53,31.09c-9.9,4-18.58,12.87-18.58,28.7c0,14.08,6.93,22.65,15.44,27.19
|
|
26
|
+
l-119.11,48.12H163.69z M358.35,281.27c-1.08,10.6-10.03,18.87-20.91,18.87h-149.9l114.43-46.23c4.21,1.55,8.58,2.26,12.53,2.26
|
|
27
|
+
c11.85,0,18.13-6.24,19.55-7.87c0.19-0.21,0.19-0.53,0-0.74l-4.37-4.85l28.78-11.63v48.03
|
|
28
|
+
C358.46,279.84,358.43,280.56,358.35,281.27z"/>
|
|
29
|
+
</g>
|
|
30
|
+
|
|
31
|
+
</svg>
|
|
@@ -6,6 +6,7 @@ import { FilterFunction, JsonEditor, UpdateFunction } from 'json-edit-react'
|
|
|
6
6
|
import './advanced-editor-styles.css'
|
|
7
7
|
import _ from 'lodash'
|
|
8
8
|
import Tooltip from '../ui/Tooltip'
|
|
9
|
+
import EmbedEditor from './EmbedEditor'
|
|
9
10
|
|
|
10
11
|
export const AdvancedEditor = ({
|
|
11
12
|
loadConfig,
|
|
@@ -115,6 +116,9 @@ export const AdvancedEditor = ({
|
|
|
115
116
|
</React.Fragment>
|
|
116
117
|
)}
|
|
117
118
|
</div>
|
|
119
|
+
|
|
120
|
+
{/* Share with Partners Section */}
|
|
121
|
+
<EmbedEditor config={config} />
|
|
118
122
|
</>
|
|
119
123
|
)
|
|
120
124
|
}
|
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
import React, { useState, useEffect, useMemo } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
generateEmbedCode,
|
|
4
|
+
extractFilters,
|
|
5
|
+
initializeFilterState,
|
|
6
|
+
buildFilterUrlParams,
|
|
7
|
+
type FilterMetadata,
|
|
8
|
+
type FilterState
|
|
9
|
+
} from '../../helpers/embed'
|
|
10
|
+
import '../../helpers/embed' // Initialize embed helper for iframe resizing
|
|
11
|
+
|
|
12
|
+
type EmbedEditorProps = {
|
|
13
|
+
config?: any // Current visualization config
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type TabId = 'preview' | 'code'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* EmbedEditor - Provides "Share with Partners" functionality
|
|
20
|
+
* Generates embed codes for iframe embedding of visualizations
|
|
21
|
+
* Now includes filter customization, preview, and embed code generation
|
|
22
|
+
*/
|
|
23
|
+
export const EmbedEditor: React.FC<EmbedEditorProps> = ({ config }) => {
|
|
24
|
+
const [configUrl, setConfigUrl] = useState<string | null>(null)
|
|
25
|
+
const [showEmbedModal, setShowEmbedModal] = useState(false)
|
|
26
|
+
const [isExpanded, setIsExpanded] = useState(false)
|
|
27
|
+
const [activeTab, setActiveTab] = useState<TabId>('preview')
|
|
28
|
+
const [embedCodeCopied, setEmbedCodeCopied] = useState(false)
|
|
29
|
+
|
|
30
|
+
// Extract filters from config
|
|
31
|
+
const filters = useMemo(() => extractFilters(config), [config])
|
|
32
|
+
|
|
33
|
+
// Initialize filter state
|
|
34
|
+
const [filterState, setFilterState] = useState<Record<string, FilterState>>({})
|
|
35
|
+
|
|
36
|
+
// Update filter state when filters change
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (filters.length > 0) {
|
|
39
|
+
setFilterState(initializeFilterState(filters))
|
|
40
|
+
}
|
|
41
|
+
}, [filters])
|
|
42
|
+
|
|
43
|
+
// Check if all filters have setByQueryParameter
|
|
44
|
+
const filtersAreValid = useMemo(() => {
|
|
45
|
+
if (!config) return true
|
|
46
|
+
|
|
47
|
+
// Check regular filters
|
|
48
|
+
const regularFilters = config.filters || []
|
|
49
|
+
// Check dashboard shared filters
|
|
50
|
+
const sharedFilters = config.dashboard?.sharedFilters || []
|
|
51
|
+
|
|
52
|
+
const allFilters = [...regularFilters, ...sharedFilters]
|
|
53
|
+
|
|
54
|
+
// If no filters, valid
|
|
55
|
+
if (allFilters.length === 0) return true
|
|
56
|
+
|
|
57
|
+
// All filters must have setByQueryParameter
|
|
58
|
+
return allFilters.every((filter: any) => !!filter.setByQueryParameter)
|
|
59
|
+
}, [config])
|
|
60
|
+
|
|
61
|
+
// Determine if we have valid filters to show
|
|
62
|
+
const hasFilters = filters.length > 0 && filtersAreValid
|
|
63
|
+
|
|
64
|
+
// Generate embed code with current filter settings
|
|
65
|
+
const embedCode = useMemo(() => {
|
|
66
|
+
if (!configUrl) return ''
|
|
67
|
+
|
|
68
|
+
const urlParams = buildFilterUrlParams(filters, filterState)
|
|
69
|
+
return generateEmbedCode({
|
|
70
|
+
configUrl,
|
|
71
|
+
urlParams
|
|
72
|
+
})
|
|
73
|
+
}, [configUrl, filters, filterState])
|
|
74
|
+
|
|
75
|
+
// Detect configUrl from WCMS permalink or use dev fallback
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
// Try to get config URL from WCMS permalink element
|
|
78
|
+
const permalinkElement = document.querySelector('#sample-permalink') as HTMLAnchorElement
|
|
79
|
+
|
|
80
|
+
if (permalinkElement?.href) {
|
|
81
|
+
try {
|
|
82
|
+
// Parse the URL and extract just the pathname (strip host)
|
|
83
|
+
const url = new URL(permalinkElement.href)
|
|
84
|
+
const pathname = url.pathname
|
|
85
|
+
setConfigUrl(pathname)
|
|
86
|
+
} catch (err) {
|
|
87
|
+
console.warn('[EmbedEditor] Failed to parse permalink URL:', err)
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
// Check if we're in development mode
|
|
91
|
+
const isDevelopment =
|
|
92
|
+
process.env.NODE_ENV === 'development' ||
|
|
93
|
+
window.location.hostname === 'localhost' ||
|
|
94
|
+
window.location.hostname === '127.0.0.1'
|
|
95
|
+
|
|
96
|
+
if (isDevelopment) {
|
|
97
|
+
// Use fallback only in development
|
|
98
|
+
const fallbackUrl = '/examples/line-chart-states.json'
|
|
99
|
+
setConfigUrl(fallbackUrl)
|
|
100
|
+
} else {
|
|
101
|
+
// In production without permalink, don't show embed section
|
|
102
|
+
console.warn('[EmbedEditor] No permalink found and not in development mode')
|
|
103
|
+
setConfigUrl(null)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}, [])
|
|
107
|
+
|
|
108
|
+
// Handle showing embed code modal
|
|
109
|
+
const handleShowEmbedCode = () => {
|
|
110
|
+
if (!configUrl) {
|
|
111
|
+
alert('This visualization must be published before generating embed code.')
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
setActiveTab('preview')
|
|
116
|
+
setShowEmbedModal(true)
|
|
117
|
+
setEmbedCodeCopied(false)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Handle filter value change
|
|
121
|
+
const handleFilterChange = (filterKey: string, value: string) => {
|
|
122
|
+
setFilterState(prev => ({
|
|
123
|
+
...prev,
|
|
124
|
+
[filterKey]: {
|
|
125
|
+
...prev[filterKey],
|
|
126
|
+
value
|
|
127
|
+
}
|
|
128
|
+
}))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Handle filter hide toggle
|
|
132
|
+
const handleHideToggle = (filterKey: string, hide: boolean) => {
|
|
133
|
+
setFilterState(prev => ({
|
|
134
|
+
...prev,
|
|
135
|
+
[filterKey]: {
|
|
136
|
+
...prev[filterKey],
|
|
137
|
+
hide
|
|
138
|
+
}
|
|
139
|
+
}))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Handle copying embed code from modal
|
|
143
|
+
const handleCopyFromModal = async () => {
|
|
144
|
+
try {
|
|
145
|
+
await navigator.clipboard.writeText(embedCode)
|
|
146
|
+
setEmbedCodeCopied(true)
|
|
147
|
+
setTimeout(() => setEmbedCodeCopied(false), 3000)
|
|
148
|
+
} catch (err) {
|
|
149
|
+
console.error('Failed to copy embed code:', err)
|
|
150
|
+
alert('Failed to copy to clipboard. Please copy manually.')
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Handle closing modal
|
|
155
|
+
const handleCloseModal = () => {
|
|
156
|
+
setShowEmbedModal(false)
|
|
157
|
+
setEmbedCodeCopied(false)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<>
|
|
162
|
+
{/* Collapsible Share with Partners Section */}
|
|
163
|
+
<div className='share-partners' style={{ padding: '0 1em 1em', textAlign: 'left' }}>
|
|
164
|
+
<span
|
|
165
|
+
className='advanced-toggle-link'
|
|
166
|
+
onClick={() => setIsExpanded(!isExpanded)}
|
|
167
|
+
style={{ paddingTop: '1em', display: 'block', cursor: 'pointer', textDecoration: 'underline' }}
|
|
168
|
+
>
|
|
169
|
+
<span
|
|
170
|
+
style={{ textDecoration: 'none', display: 'inline-block', fontFamily: 'monospace', paddingRight: '5px' }}
|
|
171
|
+
>
|
|
172
|
+
{isExpanded ? `— ` : `+ `}
|
|
173
|
+
</span>
|
|
174
|
+
Share with Partners
|
|
175
|
+
</span>
|
|
176
|
+
|
|
177
|
+
{isExpanded && (
|
|
178
|
+
<div style={{ paddingTop: '1em' }}>
|
|
179
|
+
{!configUrl ? (
|
|
180
|
+
<div
|
|
181
|
+
style={{
|
|
182
|
+
padding: '0.75em',
|
|
183
|
+
background: '#fff3cd',
|
|
184
|
+
border: '1px solid #ffc107',
|
|
185
|
+
borderRadius: '4px',
|
|
186
|
+
marginBottom: '0.5em'
|
|
187
|
+
}}
|
|
188
|
+
>
|
|
189
|
+
<p style={{ fontSize: '0.85em', margin: 0, color: '#856404' }}>
|
|
190
|
+
⚠️ An embed code cannot be generated until this visualization has been saved.
|
|
191
|
+
</p>
|
|
192
|
+
</div>
|
|
193
|
+
) : !filtersAreValid ? (
|
|
194
|
+
<div
|
|
195
|
+
style={{
|
|
196
|
+
padding: '0.75em',
|
|
197
|
+
background: '#fff3cd',
|
|
198
|
+
border: '1px solid #ffc107',
|
|
199
|
+
borderRadius: '4px',
|
|
200
|
+
marginBottom: '0.5em'
|
|
201
|
+
}}
|
|
202
|
+
>
|
|
203
|
+
<p style={{ fontSize: '0.85em', margin: '0 0 0.5em 0', fontWeight: 'bold', color: '#856404' }}>
|
|
204
|
+
⚠️ Embed Code Not Available
|
|
205
|
+
</p>
|
|
206
|
+
<p style={{ fontSize: '0.85em', margin: 0, color: '#856404' }}>
|
|
207
|
+
To enable embedding, all filters must have the "Query String Parameter" field set. Some filters are
|
|
208
|
+
missing this field. After setting the field, make sure to save your visualization.
|
|
209
|
+
</p>
|
|
210
|
+
</div>
|
|
211
|
+
) : (
|
|
212
|
+
<>
|
|
213
|
+
<p style={{ fontSize: '0.85em', marginBottom: '1em', color: '#666' }}>
|
|
214
|
+
Generate embed codes for partners to add this visualization to their website.
|
|
215
|
+
</p>
|
|
216
|
+
|
|
217
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5em' }}>
|
|
218
|
+
<button
|
|
219
|
+
className='btn btn-primary'
|
|
220
|
+
onClick={handleShowEmbedCode}
|
|
221
|
+
style={{ width: '100%', textAlign: 'left' }}
|
|
222
|
+
>
|
|
223
|
+
Get Embed Code
|
|
224
|
+
</button>
|
|
225
|
+
</div>
|
|
226
|
+
</>
|
|
227
|
+
)}
|
|
228
|
+
</div>
|
|
229
|
+
)}
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
{/* Embed Code Modal with Tabs */}
|
|
233
|
+
{showEmbedModal && (
|
|
234
|
+
<div
|
|
235
|
+
className='modal-overlay'
|
|
236
|
+
style={{
|
|
237
|
+
position: 'fixed',
|
|
238
|
+
top: 0,
|
|
239
|
+
left: 0,
|
|
240
|
+
right: 0,
|
|
241
|
+
bottom: 0,
|
|
242
|
+
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
243
|
+
display: 'flex',
|
|
244
|
+
alignItems: 'flex-start',
|
|
245
|
+
justifyContent: 'center',
|
|
246
|
+
paddingTop: '5vh',
|
|
247
|
+
zIndex: 9999
|
|
248
|
+
}}
|
|
249
|
+
onClick={handleCloseModal}
|
|
250
|
+
>
|
|
251
|
+
<div
|
|
252
|
+
className='modal-content'
|
|
253
|
+
style={{
|
|
254
|
+
backgroundColor: 'white',
|
|
255
|
+
borderRadius: '8px',
|
|
256
|
+
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
|
257
|
+
maxWidth: '800px',
|
|
258
|
+
width: '90%',
|
|
259
|
+
maxHeight: '90vh',
|
|
260
|
+
margin: '20px',
|
|
261
|
+
display: 'flex',
|
|
262
|
+
flexDirection: 'column'
|
|
263
|
+
}}
|
|
264
|
+
onClick={e => e.stopPropagation()}
|
|
265
|
+
>
|
|
266
|
+
{/* Modal Header */}
|
|
267
|
+
<div
|
|
268
|
+
className='modal-header'
|
|
269
|
+
style={{
|
|
270
|
+
padding: '15px 20px',
|
|
271
|
+
borderBottom: '1px solid #e0e0e0',
|
|
272
|
+
backgroundColor: '#005eaa',
|
|
273
|
+
display: 'flex',
|
|
274
|
+
justifyContent: 'space-between',
|
|
275
|
+
alignItems: 'center',
|
|
276
|
+
borderRadius: '8px 8px 0 0'
|
|
277
|
+
}}
|
|
278
|
+
>
|
|
279
|
+
<h3 style={{ color: 'white', margin: 0 }}>Share with Partners</h3>
|
|
280
|
+
<button
|
|
281
|
+
onClick={handleCloseModal}
|
|
282
|
+
style={{
|
|
283
|
+
background: 'transparent',
|
|
284
|
+
border: 'none',
|
|
285
|
+
color: 'white',
|
|
286
|
+
fontSize: '1.5em',
|
|
287
|
+
cursor: 'pointer',
|
|
288
|
+
padding: '0 5px',
|
|
289
|
+
lineHeight: 1
|
|
290
|
+
}}
|
|
291
|
+
aria-label='Close'
|
|
292
|
+
>
|
|
293
|
+
×
|
|
294
|
+
</button>
|
|
295
|
+
</div>
|
|
296
|
+
|
|
297
|
+
{/* Tab Navigation */}
|
|
298
|
+
<div
|
|
299
|
+
style={{
|
|
300
|
+
display: 'flex',
|
|
301
|
+
borderBottom: '1px solid #e0e0e0',
|
|
302
|
+
backgroundColor: '#f5f5f5'
|
|
303
|
+
}}
|
|
304
|
+
>
|
|
305
|
+
{(['preview', 'code'] as TabId[]).map(tab => {
|
|
306
|
+
const tabLabels: Record<TabId, string> = {
|
|
307
|
+
preview: 'Preview Visualization',
|
|
308
|
+
code: 'Get Embed Code'
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return (
|
|
312
|
+
<button
|
|
313
|
+
key={tab}
|
|
314
|
+
onClick={() => setActiveTab(tab)}
|
|
315
|
+
style={{
|
|
316
|
+
flex: 1,
|
|
317
|
+
padding: '12px 16px',
|
|
318
|
+
border: 'none',
|
|
319
|
+
backgroundColor: activeTab === tab ? 'white' : 'transparent',
|
|
320
|
+
borderBottom: activeTab === tab ? '2px solid #005eaa' : '2px solid transparent',
|
|
321
|
+
color: activeTab === tab ? '#005eaa' : '#666',
|
|
322
|
+
fontWeight: activeTab === tab ? 'bold' : 'normal',
|
|
323
|
+
cursor: 'pointer',
|
|
324
|
+
transition: 'all 0.2s'
|
|
325
|
+
}}
|
|
326
|
+
>
|
|
327
|
+
{tabLabels[tab]}
|
|
328
|
+
</button>
|
|
329
|
+
)
|
|
330
|
+
})}
|
|
331
|
+
</div>
|
|
332
|
+
|
|
333
|
+
{/* Tab Content */}
|
|
334
|
+
<div
|
|
335
|
+
className='modal-body'
|
|
336
|
+
style={{
|
|
337
|
+
padding: '20px',
|
|
338
|
+
flex: 1,
|
|
339
|
+
overflow: 'auto'
|
|
340
|
+
}}
|
|
341
|
+
>
|
|
342
|
+
{/* Preview Tab - Contains filter controls (if filters exist) and preview */}
|
|
343
|
+
{activeTab === 'preview' && (
|
|
344
|
+
<div>
|
|
345
|
+
{/* Filter Settings - only shown if there are valid filters */}
|
|
346
|
+
{hasFilters && (
|
|
347
|
+
<>
|
|
348
|
+
<h4 style={{ marginTop: 0, marginBottom: '1rem' }}>Filter Settings</h4>
|
|
349
|
+
<p style={{ marginBottom: '1rem', color: '#666' }}>
|
|
350
|
+
Set default values and visibility for filters in the partner's embedded visualization.
|
|
351
|
+
</p>
|
|
352
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem', marginBottom: '2rem' }}>
|
|
353
|
+
{filters.map((filter, index) => {
|
|
354
|
+
const state = filterState[filter.key] || { value: '', hide: false }
|
|
355
|
+
const hasValues = filter.values && filter.values.length > 0
|
|
356
|
+
|
|
357
|
+
return (
|
|
358
|
+
<div
|
|
359
|
+
key={filter.key || index}
|
|
360
|
+
style={{
|
|
361
|
+
padding: '1rem',
|
|
362
|
+
background: 'white',
|
|
363
|
+
border: '1px solid #ddd',
|
|
364
|
+
borderRadius: '4px'
|
|
365
|
+
}}
|
|
366
|
+
>
|
|
367
|
+
<label
|
|
368
|
+
htmlFor={`filter-${index}`}
|
|
369
|
+
style={{
|
|
370
|
+
display: 'block',
|
|
371
|
+
marginBottom: '0.5rem',
|
|
372
|
+
fontWeight: 'bold'
|
|
373
|
+
}}
|
|
374
|
+
>
|
|
375
|
+
{filter.label}
|
|
376
|
+
</label>
|
|
377
|
+
|
|
378
|
+
{hasValues ? (
|
|
379
|
+
<select
|
|
380
|
+
id={`filter-${index}`}
|
|
381
|
+
value={state.value}
|
|
382
|
+
onChange={e => handleFilterChange(filter.key, e.target.value)}
|
|
383
|
+
style={{
|
|
384
|
+
width: '100%',
|
|
385
|
+
padding: '0.5rem',
|
|
386
|
+
fontSize: '0.9rem',
|
|
387
|
+
border: '2px solid #d1d5db',
|
|
388
|
+
borderRadius: '6px',
|
|
389
|
+
backgroundColor: '#f9fafb',
|
|
390
|
+
cursor: 'pointer'
|
|
391
|
+
}}
|
|
392
|
+
>
|
|
393
|
+
{filter.values?.map((value, valueIndex) => (
|
|
394
|
+
<option key={valueIndex} value={value}>
|
|
395
|
+
{value}
|
|
396
|
+
</option>
|
|
397
|
+
))}
|
|
398
|
+
</select>
|
|
399
|
+
) : (
|
|
400
|
+
<div style={{ color: '#999', fontStyle: 'italic' }}>No values available</div>
|
|
401
|
+
)}
|
|
402
|
+
|
|
403
|
+
<div style={{ marginTop: '0.75rem' }}>
|
|
404
|
+
<label
|
|
405
|
+
style={{
|
|
406
|
+
display: 'flex',
|
|
407
|
+
alignItems: 'center',
|
|
408
|
+
cursor: 'pointer',
|
|
409
|
+
fontWeight: 'normal'
|
|
410
|
+
}}
|
|
411
|
+
>
|
|
412
|
+
<input
|
|
413
|
+
type='checkbox'
|
|
414
|
+
checked={state.hide}
|
|
415
|
+
onChange={e => handleHideToggle(filter.key, e.target.checked)}
|
|
416
|
+
style={{ marginRight: '0.5rem' }}
|
|
417
|
+
/>
|
|
418
|
+
<span style={{ color: '#666' }}>Hide filter in embed</span>
|
|
419
|
+
</label>
|
|
420
|
+
</div>
|
|
421
|
+
</div>
|
|
422
|
+
)
|
|
423
|
+
})}
|
|
424
|
+
</div>
|
|
425
|
+
</>
|
|
426
|
+
)}
|
|
427
|
+
|
|
428
|
+
{/* Preview Section - title only shown if there are filters */}
|
|
429
|
+
{hasFilters && <h4 style={{ marginBottom: '1rem' }}>Preview</h4>}
|
|
430
|
+
<p style={{ marginBottom: '1rem', color: '#666' }}>
|
|
431
|
+
This shows how the visualization will appear on the partner website
|
|
432
|
+
{hasFilters ? ' with your selected settings' : ''}. The partner will have control over the width of
|
|
433
|
+
the embedded visualization. If you do not see the latest version of the visualization, save it and
|
|
434
|
+
reopen this popup.
|
|
435
|
+
</p>
|
|
436
|
+
<div
|
|
437
|
+
style={{
|
|
438
|
+
border: '2px dashed #999',
|
|
439
|
+
borderRadius: '4px',
|
|
440
|
+
padding: '1rem'
|
|
441
|
+
}}
|
|
442
|
+
>
|
|
443
|
+
<div
|
|
444
|
+
key={`${configUrl}-${JSON.stringify(filterState)}`}
|
|
445
|
+
data-cove-embed
|
|
446
|
+
data-config-url={(() => {
|
|
447
|
+
const urlParams = buildFilterUrlParams(filters, filterState)
|
|
448
|
+
const params = new URLSearchParams()
|
|
449
|
+
Object.entries(urlParams).forEach(([key, value]) => {
|
|
450
|
+
if (value) params.set(key, value)
|
|
451
|
+
})
|
|
452
|
+
return params.toString() ? `${configUrl}?${params.toString()}` : configUrl || ''
|
|
453
|
+
})()}
|
|
454
|
+
/>
|
|
455
|
+
</div>
|
|
456
|
+
</div>
|
|
457
|
+
)}
|
|
458
|
+
|
|
459
|
+
{/* Embed Code Tab */}
|
|
460
|
+
{activeTab === 'code' && (
|
|
461
|
+
<div>
|
|
462
|
+
<p style={{ marginBottom: '10px', color: '#666' }}>
|
|
463
|
+
Copy this code and send it to a partner so they can add it to their site. Your visualization will
|
|
464
|
+
need to be published to Link (www.cdc.gov) before it can be embedded by a partner.
|
|
465
|
+
</p>
|
|
466
|
+
<textarea
|
|
467
|
+
readOnly
|
|
468
|
+
value={embedCode}
|
|
469
|
+
style={{
|
|
470
|
+
width: '100%',
|
|
471
|
+
height: '200px',
|
|
472
|
+
fontFamily: 'monospace',
|
|
473
|
+
fontSize: '0.85em',
|
|
474
|
+
padding: '10px',
|
|
475
|
+
border: '1px solid #ddd',
|
|
476
|
+
borderRadius: '4px',
|
|
477
|
+
resize: 'vertical',
|
|
478
|
+
boxSizing: 'border-box'
|
|
479
|
+
}}
|
|
480
|
+
onFocus={e => e.target.select()}
|
|
481
|
+
/>
|
|
482
|
+
</div>
|
|
483
|
+
)}
|
|
484
|
+
</div>
|
|
485
|
+
|
|
486
|
+
{/* Modal Footer */}
|
|
487
|
+
<div
|
|
488
|
+
className='modal-footer'
|
|
489
|
+
style={{
|
|
490
|
+
padding: '15px 20px',
|
|
491
|
+
borderTop: '1px solid #e0e0e0',
|
|
492
|
+
display: 'flex',
|
|
493
|
+
justifyContent: 'flex-end',
|
|
494
|
+
gap: '10px'
|
|
495
|
+
}}
|
|
496
|
+
>
|
|
497
|
+
<button className='btn btn-secondary' onClick={handleCloseModal}>
|
|
498
|
+
Close
|
|
499
|
+
</button>
|
|
500
|
+
{activeTab === 'code' && (
|
|
501
|
+
<button className='btn btn-primary' onClick={handleCopyFromModal} style={{ minWidth: '120px' }}>
|
|
502
|
+
{embedCodeCopied ? '✓ Copied!' : 'Copy to Clipboard'}
|
|
503
|
+
</button>
|
|
504
|
+
)}
|
|
505
|
+
</div>
|
|
506
|
+
</div>
|
|
507
|
+
</div>
|
|
508
|
+
)}
|
|
509
|
+
</>
|
|
510
|
+
)
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
export default EmbedEditor
|