@cdc/core 4.26.1 → 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 +34 -41
- package/_stories/Gallery.DataBite.stories.tsx +14 -7
- package/_stories/Gallery.Maps.stories.tsx +36 -27
- package/_stories/Gallery.WaffleChart.stories.tsx +1 -1
- package/_stories/PageART.stories.tsx +4 -3
- package/_stories/PageBRFSS.stories.tsx +20 -15
- package/_stories/PageCancerRegistries.stories.tsx +14 -14
- package/_stories/PageEasternEquineEncephalitis.stories.tsx +30 -16
- package/_stories/PageExcessiveAlcoholUse.stories.tsx +148 -143
- package/_stories/PageMaternalMortality.stories.tsx +4 -3
- package/_stories/PageOralHealth.stories.tsx +14 -9
- package/_stories/PageSmokingTobacco.stories.tsx +14 -9
- package/_stories/PageStateDiabetesProfiles.stories.tsx +14 -9
- package/_stories/PageWastewater.stories.tsx +40 -26
- 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/logo2.svg +31 -0
- package/components/AdvancedEditor/EmbedEditor.tsx +270 -38
- package/components/CustomColorsEditor/CustomColorsEditor.tsx +3 -10
- package/components/DataTable/helpers/getSeriesName.ts +6 -0
- package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +14 -6
- package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +4 -0
- package/components/EditorPanel/VizFilterEditor/components/FilterOrder.tsx +33 -29
- package/components/Layout/components/Sidebar/components/sidebar.styles.scss +2 -2
- package/components/Layout/components/Visualization/index.tsx +11 -0
- package/components/MediaControls.tsx +0 -1
- package/components/_stories/CustomColorsEditor.stories.tsx +37 -0
- package/components/_stories/DataTable.stories.tsx +1 -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 -4471
- package/dist/cove-main.css.map +1 -1
- package/generateViteConfig.js +111 -2
- package/helpers/DataTransform.ts +1 -5
- package/helpers/cove/date.ts +33 -1
- package/helpers/cove/string.ts +29 -0
- package/helpers/coveUpdateWorker.ts +3 -1
- 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/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 +27 -7
- package/helpers/testing.ts +44 -0
- package/helpers/tests/DataTransform.test.ts +125 -0
- package/helpers/tests/date.test.ts +64 -0
- package/helpers/vegaConfig.ts +1 -1
- package/helpers/vegaConfigImport.ts +160 -0
- package/helpers/ver/4.26.1.ts +1 -1
- 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/package.json +27 -32
- package/styles/v2/components/editor.scss +9 -9
- package/styles/v2/utils/_grid.scss +8 -3
- package/types/Annotation.ts +10 -11
- package/types/General.ts +2 -0
- package/types/Palette.ts +21 -0
- package/types/Visualization.ts +6 -0
- package/_stories/StoryRenderingTests.stories.tsx +0 -164
- package/helpers/embedCodeGenerator.ts +0 -109
|
@@ -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>
|
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>
|
|
@@ -1,31 +1,55 @@
|
|
|
1
1
|
import React, { useState, useEffect, useMemo } from 'react'
|
|
2
|
-
import {
|
|
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
|
|
3
11
|
|
|
4
12
|
type EmbedEditorProps = {
|
|
5
13
|
config?: any // Current visualization config
|
|
6
14
|
}
|
|
7
15
|
|
|
16
|
+
type TabId = 'preview' | 'code'
|
|
17
|
+
|
|
8
18
|
/**
|
|
9
19
|
* EmbedEditor - Provides "Share with Partners" functionality
|
|
10
20
|
* Generates embed codes for iframe embedding of visualizations
|
|
21
|
+
* Now includes filter customization, preview, and embed code generation
|
|
11
22
|
*/
|
|
12
23
|
export const EmbedEditor: React.FC<EmbedEditorProps> = ({ config }) => {
|
|
13
24
|
const [configUrl, setConfigUrl] = useState<string | null>(null)
|
|
14
25
|
const [showEmbedModal, setShowEmbedModal] = useState(false)
|
|
15
|
-
const [embedCode, setEmbedCode] = useState('')
|
|
16
|
-
const [embedCodeCopied, setEmbedCodeCopied] = useState(false)
|
|
17
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])
|
|
18
42
|
|
|
19
43
|
// Check if all filters have setByQueryParameter
|
|
20
44
|
const filtersAreValid = useMemo(() => {
|
|
21
45
|
if (!config) return true
|
|
22
46
|
|
|
23
47
|
// Check regular filters
|
|
24
|
-
const
|
|
48
|
+
const regularFilters = config.filters || []
|
|
25
49
|
// Check dashboard shared filters
|
|
26
50
|
const sharedFilters = config.dashboard?.sharedFilters || []
|
|
27
51
|
|
|
28
|
-
const allFilters = [...
|
|
52
|
+
const allFilters = [...regularFilters, ...sharedFilters]
|
|
29
53
|
|
|
30
54
|
// If no filters, valid
|
|
31
55
|
if (allFilters.length === 0) return true
|
|
@@ -34,6 +58,20 @@ export const EmbedEditor: React.FC<EmbedEditorProps> = ({ config }) => {
|
|
|
34
58
|
return allFilters.every((filter: any) => !!filter.setByQueryParameter)
|
|
35
59
|
}, [config])
|
|
36
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
|
+
|
|
37
75
|
// Detect configUrl from WCMS permalink or use dev fallback
|
|
38
76
|
useEffect(() => {
|
|
39
77
|
// Try to get config URL from WCMS permalink element
|
|
@@ -74,12 +112,33 @@ export const EmbedEditor: React.FC<EmbedEditorProps> = ({ config }) => {
|
|
|
74
112
|
return
|
|
75
113
|
}
|
|
76
114
|
|
|
77
|
-
|
|
78
|
-
setEmbedCode(code)
|
|
115
|
+
setActiveTab('preview')
|
|
79
116
|
setShowEmbedModal(true)
|
|
80
117
|
setEmbedCodeCopied(false)
|
|
81
118
|
}
|
|
82
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
|
+
|
|
83
142
|
// Handle copying embed code from modal
|
|
84
143
|
const handleCopyFromModal = async () => {
|
|
85
144
|
try {
|
|
@@ -98,9 +157,6 @@ export const EmbedEditor: React.FC<EmbedEditorProps> = ({ config }) => {
|
|
|
98
157
|
setEmbedCodeCopied(false)
|
|
99
158
|
}
|
|
100
159
|
|
|
101
|
-
// Hide embed section until released
|
|
102
|
-
return null
|
|
103
|
-
|
|
104
160
|
return (
|
|
105
161
|
<>
|
|
106
162
|
{/* Collapsible Share with Partners Section */}
|
|
@@ -155,8 +211,7 @@ export const EmbedEditor: React.FC<EmbedEditorProps> = ({ config }) => {
|
|
|
155
211
|
) : (
|
|
156
212
|
<>
|
|
157
213
|
<p style={{ fontSize: '0.85em', marginBottom: '1em', color: '#666' }}>
|
|
158
|
-
Generate embed codes for partners to add this visualization to their
|
|
159
|
-
need to be published to Link (www.cdc.gov) before it can be embedded by a partner.
|
|
214
|
+
Generate embed codes for partners to add this visualization to their website.
|
|
160
215
|
</p>
|
|
161
216
|
|
|
162
217
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5em' }}>
|
|
@@ -174,7 +229,7 @@ export const EmbedEditor: React.FC<EmbedEditorProps> = ({ config }) => {
|
|
|
174
229
|
)}
|
|
175
230
|
</div>
|
|
176
231
|
|
|
177
|
-
{/* Embed Code Modal */}
|
|
232
|
+
{/* Embed Code Modal with Tabs */}
|
|
178
233
|
{showEmbedModal && (
|
|
179
234
|
<div
|
|
180
235
|
className='modal-overlay'
|
|
@@ -186,8 +241,9 @@ export const EmbedEditor: React.FC<EmbedEditorProps> = ({ config }) => {
|
|
|
186
241
|
bottom: 0,
|
|
187
242
|
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
188
243
|
display: 'flex',
|
|
189
|
-
alignItems: '
|
|
244
|
+
alignItems: 'flex-start',
|
|
190
245
|
justifyContent: 'center',
|
|
246
|
+
paddingTop: '5vh',
|
|
191
247
|
zIndex: 9999
|
|
192
248
|
}}
|
|
193
249
|
onClick={handleCloseModal}
|
|
@@ -198,12 +254,16 @@ export const EmbedEditor: React.FC<EmbedEditorProps> = ({ config }) => {
|
|
|
198
254
|
backgroundColor: 'white',
|
|
199
255
|
borderRadius: '8px',
|
|
200
256
|
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
|
201
|
-
maxWidth: '
|
|
257
|
+
maxWidth: '800px',
|
|
202
258
|
width: '90%',
|
|
203
|
-
|
|
259
|
+
maxHeight: '90vh',
|
|
260
|
+
margin: '20px',
|
|
261
|
+
display: 'flex',
|
|
262
|
+
flexDirection: 'column'
|
|
204
263
|
}}
|
|
205
264
|
onClick={e => e.stopPropagation()}
|
|
206
265
|
>
|
|
266
|
+
{/* Modal Header */}
|
|
207
267
|
<div
|
|
208
268
|
className='modal-header'
|
|
209
269
|
style={{
|
|
@@ -216,7 +276,7 @@ export const EmbedEditor: React.FC<EmbedEditorProps> = ({ config }) => {
|
|
|
216
276
|
borderRadius: '8px 8px 0 0'
|
|
217
277
|
}}
|
|
218
278
|
>
|
|
219
|
-
<h3 style={{ color: 'white', margin: 0 }}>
|
|
279
|
+
<h3 style={{ color: 'white', margin: 0 }}>Share with Partners</h3>
|
|
220
280
|
<button
|
|
221
281
|
onClick={handleCloseModal}
|
|
222
282
|
style={{
|
|
@@ -234,26 +294,196 @@ export const EmbedEditor: React.FC<EmbedEditorProps> = ({ config }) => {
|
|
|
234
294
|
</button>
|
|
235
295
|
</div>
|
|
236
296
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
+
})}
|
|
255
331
|
</div>
|
|
256
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 */}
|
|
257
487
|
<div
|
|
258
488
|
className='modal-footer'
|
|
259
489
|
style={{
|
|
@@ -267,9 +497,11 @@ export const EmbedEditor: React.FC<EmbedEditorProps> = ({ config }) => {
|
|
|
267
497
|
<button className='btn btn-secondary' onClick={handleCloseModal}>
|
|
268
498
|
Close
|
|
269
499
|
</button>
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
500
|
+
{activeTab === 'code' && (
|
|
501
|
+
<button className='btn btn-primary' onClick={handleCopyFromModal} style={{ minWidth: '120px' }}>
|
|
502
|
+
{embedCodeCopied ? '✓ Copied!' : 'Copy to Clipboard'}
|
|
503
|
+
</button>
|
|
504
|
+
)}
|
|
273
505
|
</div>
|
|
274
506
|
</div>
|
|
275
507
|
</div>
|
|
@@ -7,15 +7,13 @@ interface CustomColorsEditorProps {
|
|
|
7
7
|
onChange: (colors: string[]) => void
|
|
8
8
|
label?: string
|
|
9
9
|
minColors?: number
|
|
10
|
-
maxColors?: number
|
|
11
10
|
}
|
|
12
11
|
|
|
13
12
|
const CustomColorsEditor: React.FC<CustomColorsEditorProps> = ({
|
|
14
13
|
colors = [],
|
|
15
14
|
onChange,
|
|
16
15
|
label = 'Custom Colors',
|
|
17
|
-
minColors = 1
|
|
18
|
-
maxColors = 20
|
|
16
|
+
minColors = 1
|
|
19
17
|
}) => {
|
|
20
18
|
const [draggedIndex, setDraggedIndex] = useState<number | null>(null)
|
|
21
19
|
|
|
@@ -26,11 +24,8 @@ const CustomColorsEditor: React.FC<CustomColorsEditorProps> = ({
|
|
|
26
24
|
}
|
|
27
25
|
|
|
28
26
|
const handleAddColor = () => {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const defaultColor = colors.length > 0 ? colors[colors.length - 1] : '#3366cc'
|
|
32
|
-
onChange([...colors, defaultColor])
|
|
33
|
-
}
|
|
27
|
+
const defaultColor = colors.length > 0 ? colors[colors.length - 1] : '#3366cc'
|
|
28
|
+
onChange([...colors, defaultColor])
|
|
34
29
|
}
|
|
35
30
|
|
|
36
31
|
const handleRemoveColor = (index: number) => {
|
|
@@ -191,7 +186,6 @@ const CustomColorsEditor: React.FC<CustomColorsEditorProps> = ({
|
|
|
191
186
|
<button
|
|
192
187
|
type="button"
|
|
193
188
|
onClick={handleAddColor}
|
|
194
|
-
disabled={colors.length >= maxColors}
|
|
195
189
|
className="btn-add-color"
|
|
196
190
|
>
|
|
197
191
|
+ Add Color
|
|
@@ -200,7 +194,6 @@ const CustomColorsEditor: React.FC<CustomColorsEditorProps> = ({
|
|
|
200
194
|
<div className="custom-colors-info">
|
|
201
195
|
{colors.length} color{colors.length !== 1 ? 's' : ''}
|
|
202
196
|
{colors.length < minColors && ` (minimum ${minColors} required)`}
|
|
203
|
-
{colors.length >= maxColors && ` (maximum reached)`}
|
|
204
197
|
</div>
|
|
205
198
|
</div>
|
|
206
199
|
)
|
|
@@ -16,6 +16,12 @@ export const getSeriesName = (column: string, config: TableConfig) => {
|
|
|
16
16
|
return userDefinedSeries.name
|
|
17
17
|
}
|
|
18
18
|
if (config.runtimeSeriesLabels && config.runtimeSeriesLabels[column]) return config.runtimeSeriesLabels[column]
|
|
19
|
+
|
|
20
|
+
// For pie charts, use yAxis.label if the column is the yAxis data key
|
|
21
|
+
if (config.visualizationType === 'Pie' && column === config.yAxis?.dataKey && config.yAxis?.label) {
|
|
22
|
+
return config.yAxis.label
|
|
23
|
+
}
|
|
24
|
+
|
|
19
25
|
const columnIsDataKey = column === config.xAxis?.dataKey
|
|
20
26
|
const indexLabel = config.table?.indexLabel
|
|
21
27
|
return columnIsDataKey && indexLabel ? indexLabel : getLabel(column, config)
|
|
@@ -16,6 +16,7 @@ type NestedDropdownEditorProps = {
|
|
|
16
16
|
updateField: Function
|
|
17
17
|
updateFilterStyle: Function
|
|
18
18
|
handleGroupingCustomOrder: (index1: number, index2: number) => void
|
|
19
|
+
onNestedDragAreaHover?: (isHovering: boolean) => void
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
const NestedDropdownEditor: React.FC<NestedDropdownEditorProps> = ({
|
|
@@ -25,7 +26,8 @@ const NestedDropdownEditor: React.FC<NestedDropdownEditorProps> = ({
|
|
|
25
26
|
handleNameChange: handleGroupColumnNameChange,
|
|
26
27
|
filterIndex,
|
|
27
28
|
rawData,
|
|
28
|
-
updateField
|
|
29
|
+
updateField,
|
|
30
|
+
onNestedDragAreaHover
|
|
29
31
|
}) => {
|
|
30
32
|
const filter = config.filters[filterIndex]
|
|
31
33
|
const subGrouping = filter?.subGrouping
|
|
@@ -159,7 +161,10 @@ const NestedDropdownEditor: React.FC<NestedDropdownEditorProps> = ({
|
|
|
159
161
|
<Select
|
|
160
162
|
label='Filter Grouping'
|
|
161
163
|
value={filter.columnName}
|
|
162
|
-
options={[
|
|
164
|
+
options={[
|
|
165
|
+
{ value: '', label: '- Select Option -' },
|
|
166
|
+
...columnNameOptions.map(opt => ({ value: opt, label: opt }))
|
|
167
|
+
]}
|
|
163
168
|
onChange={e => handleGroupColumnNameChange(e.target.value)}
|
|
164
169
|
/>
|
|
165
170
|
|
|
@@ -168,9 +173,7 @@ const NestedDropdownEditor: React.FC<NestedDropdownEditorProps> = ({
|
|
|
168
173
|
value={subGrouping?.columnName ?? ''}
|
|
169
174
|
options={[
|
|
170
175
|
{ value: '', label: '- Select Option -' },
|
|
171
|
-
...columnNameOptions
|
|
172
|
-
.filter(option => option !== filter.columnName)
|
|
173
|
-
.map(opt => ({ value: opt, label: opt }))
|
|
176
|
+
...columnNameOptions.filter(option => option !== filter.columnName).map(opt => ({ value: opt, label: opt }))
|
|
174
177
|
]}
|
|
175
178
|
onChange={e => {
|
|
176
179
|
handleSubGroupColumnNameChange(e.target.value)
|
|
@@ -222,7 +225,11 @@ const NestedDropdownEditor: React.FC<NestedDropdownEditorProps> = ({
|
|
|
222
225
|
onChange={e => handleGroupingOrderBy(e.target.value as OrderBy)}
|
|
223
226
|
/>
|
|
224
227
|
{filter.order === 'cust' && (
|
|
225
|
-
<FilterOrder
|
|
228
|
+
<FilterOrder
|
|
229
|
+
orderedValues={filter.orderedValues}
|
|
230
|
+
handleFilterOrder={handleGroupingCustomOrder}
|
|
231
|
+
onNestedDragAreaHover={onNestedDragAreaHover}
|
|
232
|
+
/>
|
|
226
233
|
)}
|
|
227
234
|
</div>
|
|
228
235
|
|
|
@@ -247,6 +254,7 @@ const NestedDropdownEditor: React.FC<NestedDropdownEditorProps> = ({
|
|
|
247
254
|
handleFilterOrder={(sourceIndex, destinationIndex) => {
|
|
248
255
|
handleSubGroupingCustomOrder(sourceIndex, destinationIndex, orderedSubGroupValues, groupName)
|
|
249
256
|
}}
|
|
257
|
+
onNestedDragAreaHover={onNestedDragAreaHover}
|
|
250
258
|
/>
|
|
251
259
|
</div>
|
|
252
260
|
)
|
|
@@ -25,6 +25,7 @@ type VizFilterProps = {
|
|
|
25
25
|
|
|
26
26
|
const VizFilterEditor: React.FC<VizFilterProps> = ({ config, updateField, rawData, hasFootnotes }) => {
|
|
27
27
|
const openControls = useState({})
|
|
28
|
+
const [isNestedDragHovered, setIsNestedDragHovered] = useState(false)
|
|
28
29
|
const dataColumns = useMemo(() => {
|
|
29
30
|
return _.uniq(_.flatten(rawData?.map(row => Object.keys(row))))
|
|
30
31
|
}, [rawData])
|
|
@@ -196,6 +197,7 @@ const VizFilterEditor: React.FC<VizFilterProps> = ({ config, updateField, rawDat
|
|
|
196
197
|
key={filter.id || `filter-${filterIndex}`}
|
|
197
198
|
draggableId={`filter-${filter.id || filterIndex}`}
|
|
198
199
|
index={filterIndex}
|
|
200
|
+
isDragDisabled={isNestedDragHovered}
|
|
199
201
|
>
|
|
200
202
|
{(provided, snapshot) => (
|
|
201
203
|
<div
|
|
@@ -305,6 +307,7 @@ const VizFilterEditor: React.FC<VizFilterProps> = ({ config, updateField, rawDat
|
|
|
305
307
|
handleFilterOrder={(index1, index2) =>
|
|
306
308
|
handleFilterOrder(index1, index2, filterIndex)
|
|
307
309
|
}
|
|
310
|
+
onNestedDragAreaHover={setIsNestedDragHovered}
|
|
308
311
|
/>
|
|
309
312
|
)}
|
|
310
313
|
{filter.order === 'column' && (
|
|
@@ -354,6 +357,7 @@ const VizFilterEditor: React.FC<VizFilterProps> = ({ config, updateField, rawDat
|
|
|
354
357
|
handleNameChange={value => handleNameChange(filterIndex, value)}
|
|
355
358
|
updateField={updateField}
|
|
356
359
|
updateFilterStyle={updateFilterStyle}
|
|
360
|
+
onNestedDragAreaHover={setIsNestedDragHovered}
|
|
357
361
|
/>
|
|
358
362
|
)}
|
|
359
363
|
{hasFootnotes && (
|