@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.
Files changed (99) hide show
  1. package/.claude/agents/qa-test-developer.md +126 -0
  2. package/CLAUDE.local.md +67 -0
  3. package/_stories/Gallery.Charts.stories.tsx +34 -41
  4. package/_stories/Gallery.DataBite.stories.tsx +14 -7
  5. package/_stories/Gallery.Maps.stories.tsx +36 -27
  6. package/_stories/Gallery.WaffleChart.stories.tsx +1 -1
  7. package/_stories/PageART.stories.tsx +4 -3
  8. package/_stories/PageBRFSS.stories.tsx +20 -15
  9. package/_stories/PageCancerRegistries.stories.tsx +14 -14
  10. package/_stories/PageEasternEquineEncephalitis.stories.tsx +30 -16
  11. package/_stories/PageExcessiveAlcoholUse.stories.tsx +148 -143
  12. package/_stories/PageMaternalMortality.stories.tsx +4 -3
  13. package/_stories/PageOralHealth.stories.tsx +14 -9
  14. package/_stories/PageSmokingTobacco.stories.tsx +14 -9
  15. package/_stories/PageStateDiabetesProfiles.stories.tsx +14 -9
  16. package/_stories/PageWastewater.stories.tsx +40 -26
  17. package/_stories/VegaImport.stories.tsx +401 -0
  18. package/_stories/vega-fixtures/bars-with-line.json +444 -0
  19. package/_stories/vega-fixtures/bars.json +58 -0
  20. package/_stories/vega-fixtures/combo-bar-rolling-mean.json +88 -0
  21. package/_stories/vega-fixtures/combo.json +68 -0
  22. package/_stories/vega-fixtures/grouped-horizontal-bars.json +83 -0
  23. package/_stories/vega-fixtures/grouped-horizontal-bars2.json +231 -0
  24. package/_stories/vega-fixtures/horizontal-bar.json +427 -0
  25. package/_stories/vega-fixtures/horizontal-bars-with-bad-colors.json +197 -0
  26. package/_stories/vega-fixtures/horizontal-bars2.json +58 -0
  27. package/_stories/vega-fixtures/lines.json +227 -0
  28. package/_stories/vega-fixtures/measles-bars.json +348 -0
  29. package/_stories/vega-fixtures/measles-map.json +11101 -0
  30. package/_stories/vega-fixtures/measles-stacked-bars.json +2147 -0
  31. package/_stories/vega-fixtures/multi-dataset.json +255 -0
  32. package/_stories/vega-fixtures/no-data.json +14 -0
  33. package/_stories/vega-fixtures/pie-chart.json +94 -0
  34. package/_stories/vega-fixtures/repeat-spec.json +47 -0
  35. package/_stories/vega-fixtures/stacked-area.json +222 -0
  36. package/_stories/vega-fixtures/stacked-bar-with-rect.json +3412 -0
  37. package/_stories/vega-fixtures/stacked-bars-with-line.json +364 -0
  38. package/_stories/vega-fixtures/stacked-bars.json +212 -0
  39. package/_stories/vega-fixtures/stacked-horizontal-bars.json +140 -0
  40. package/_stories/vega-fixtures/warning-combo.json +59 -0
  41. package/_stories/vega-fixtures/warning-scatter-and-line.json +1182 -0
  42. package/assets/icon-chart-area.svg +1 -0
  43. package/assets/icon-chart-radar.svg +23 -0
  44. package/assets/logo2.svg +31 -0
  45. package/components/AdvancedEditor/EmbedEditor.tsx +270 -38
  46. package/components/CustomColorsEditor/CustomColorsEditor.tsx +3 -10
  47. package/components/DataTable/helpers/getSeriesName.ts +6 -0
  48. package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +14 -6
  49. package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +4 -0
  50. package/components/EditorPanel/VizFilterEditor/components/FilterOrder.tsx +33 -29
  51. package/components/Layout/components/Sidebar/components/sidebar.styles.scss +2 -2
  52. package/components/Layout/components/Visualization/index.tsx +11 -0
  53. package/components/MediaControls.tsx +0 -1
  54. package/components/_stories/CustomColorsEditor.stories.tsx +37 -0
  55. package/components/_stories/DataTable.stories.tsx +1 -0
  56. package/data/colorPalettes.ts +18 -5
  57. package/data/mapColorPalettes.ts +10 -0
  58. package/devTemplate/dev.js +235 -0
  59. package/devTemplate/index.html +30 -0
  60. package/devTemplate/preview.html +1503 -0
  61. package/devTemplate/sidebar.css +151 -0
  62. package/dist/cove-main.css +2803 -4471
  63. package/dist/cove-main.css.map +1 -1
  64. package/generateViteConfig.js +111 -2
  65. package/helpers/DataTransform.ts +1 -5
  66. package/helpers/cove/date.ts +33 -1
  67. package/helpers/cove/string.ts +29 -0
  68. package/helpers/coveUpdateWorker.ts +3 -1
  69. package/helpers/embed/embedCodeGenerator.ts +80 -0
  70. package/helpers/embed/embedHelper.js +158 -0
  71. package/helpers/embed/filterUtils.ts +121 -0
  72. package/helpers/embed/index.ts +21 -0
  73. package/helpers/embed/urlValidation.ts +119 -0
  74. package/helpers/filterVizData.ts +6 -1
  75. package/helpers/getFileExtension.ts +0 -6
  76. package/helpers/metrics/types.ts +3 -0
  77. package/helpers/palettes/colorDistributions.ts +1 -1
  78. package/helpers/palettes/utils.ts +12 -12
  79. package/helpers/parseCsvWithQuotes.ts +15 -14
  80. package/helpers/prepareScreenshot.ts +27 -7
  81. package/helpers/testing.ts +44 -0
  82. package/helpers/tests/DataTransform.test.ts +125 -0
  83. package/helpers/tests/date.test.ts +64 -0
  84. package/helpers/vegaConfig.ts +1 -1
  85. package/helpers/vegaConfigImport.ts +160 -0
  86. package/helpers/ver/4.26.1.ts +1 -1
  87. package/helpers/ver/4.26.2.ts +84 -0
  88. package/helpers/ver/tests/4.26.1.test.ts +105 -0
  89. package/helpers/ver/tests/4.26.2.test.ts +298 -0
  90. package/helpers/viewports.ts +2 -0
  91. package/package.json +27 -32
  92. package/styles/v2/components/editor.scss +9 -9
  93. package/styles/v2/utils/_grid.scss +8 -3
  94. package/types/Annotation.ts +10 -11
  95. package/types/General.ts +2 -0
  96. package/types/Palette.ts +21 -0
  97. package/types/Visualization.ts +6 -0
  98. package/_stories/StoryRenderingTests.stories.tsx +0 -164
  99. 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>
@@ -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 { generateEmbedCode } from '../../helpers/embedCodeGenerator'
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 filters = config.filters || []
48
+ const regularFilters = config.filters || []
25
49
  // Check dashboard shared filters
26
50
  const sharedFilters = config.dashboard?.sharedFilters || []
27
51
 
28
- const allFilters = [...filters, ...sharedFilters]
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
- const code = generateEmbedCode({ configUrl })
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 websites. Your visualization will
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: 'center',
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: '600px',
257
+ maxWidth: '800px',
202
258
  width: '90%',
203
- margin: '20px'
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 }}>Embed Code</h3>
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
- <div className='modal-body' style={{ padding: '20px' }}>
238
- <p style={{ marginBottom: '10px', color: '#666' }}>Copy this code and paste it into your website:</p>
239
- <textarea
240
- readOnly
241
- value={embedCode}
242
- style={{
243
- width: '100%',
244
- height: '180px',
245
- fontFamily: 'monospace',
246
- fontSize: '0.85em',
247
- padding: '10px',
248
- border: '1px solid #ddd',
249
- borderRadius: '4px',
250
- resize: 'vertical',
251
- boxSizing: 'border-box'
252
- }}
253
- onFocus={e => e.target.select()}
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
- <button className='btn btn-primary' onClick={handleCopyFromModal} style={{ minWidth: '120px' }}>
271
- {embedCodeCopied ? ' Copied!' : 'Copy to Clipboard'}
272
- </button>
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
- if (colors.length < maxColors) {
30
- // Add a new color (default to the last color or a neutral color)
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={[{ value: '', label: '- Select Option -' }, ...columnNameOptions.map(opt => ({ value: opt, label: opt }))]}
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 orderedValues={filter.orderedValues} handleFilterOrder={handleGroupingCustomOrder} />
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 && (