@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.
Files changed (147) 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 +300 -0
  4. package/_stories/Gallery.DataBite.stories.tsx +79 -0
  5. package/_stories/Gallery.Maps.stories.tsx +239 -0
  6. package/_stories/Gallery.WaffleChart.stories.tsx +187 -0
  7. package/_stories/PageART.stories.tsx +193 -0
  8. package/_stories/PageBRFSS.stories.tsx +294 -0
  9. package/_stories/PageCancerRegistries.stories.tsx +199 -0
  10. package/_stories/PageEasternEquineEncephalitis.stories.tsx +216 -0
  11. package/_stories/PageExcessiveAlcoholUse.stories.tsx +201 -0
  12. package/_stories/PageMaternalMortality.stories.tsx +193 -0
  13. package/_stories/PageOralHealth.stories.tsx +201 -0
  14. package/_stories/PageRespiratory.stories.tsx +332 -0
  15. package/_stories/PageSmokingTobacco.stories.tsx +200 -0
  16. package/_stories/PageStateDiabetesProfiles.stories.tsx +201 -0
  17. package/_stories/PageWastewater.stories.tsx +477 -0
  18. package/_stories/VegaImport.stories.tsx +401 -0
  19. package/_stories/vega-fixtures/bars-with-line.json +444 -0
  20. package/_stories/vega-fixtures/bars.json +58 -0
  21. package/_stories/vega-fixtures/combo-bar-rolling-mean.json +88 -0
  22. package/_stories/vega-fixtures/combo.json +68 -0
  23. package/_stories/vega-fixtures/grouped-horizontal-bars.json +83 -0
  24. package/_stories/vega-fixtures/grouped-horizontal-bars2.json +231 -0
  25. package/_stories/vega-fixtures/horizontal-bar.json +427 -0
  26. package/_stories/vega-fixtures/horizontal-bars-with-bad-colors.json +197 -0
  27. package/_stories/vega-fixtures/horizontal-bars2.json +58 -0
  28. package/_stories/vega-fixtures/lines.json +227 -0
  29. package/_stories/vega-fixtures/measles-bars.json +348 -0
  30. package/_stories/vega-fixtures/measles-map.json +11101 -0
  31. package/_stories/vega-fixtures/measles-stacked-bars.json +2147 -0
  32. package/_stories/vega-fixtures/multi-dataset.json +255 -0
  33. package/_stories/vega-fixtures/no-data.json +14 -0
  34. package/_stories/vega-fixtures/pie-chart.json +94 -0
  35. package/_stories/vega-fixtures/repeat-spec.json +47 -0
  36. package/_stories/vega-fixtures/stacked-area.json +222 -0
  37. package/_stories/vega-fixtures/stacked-bar-with-rect.json +3412 -0
  38. package/_stories/vega-fixtures/stacked-bars-with-line.json +364 -0
  39. package/_stories/vega-fixtures/stacked-bars.json +212 -0
  40. package/_stories/vega-fixtures/stacked-horizontal-bars.json +140 -0
  41. package/_stories/vega-fixtures/warning-combo.json +59 -0
  42. package/_stories/vega-fixtures/warning-scatter-and-line.json +1182 -0
  43. package/assets/icon-chart-area.svg +1 -0
  44. package/assets/icon-chart-radar.svg +23 -0
  45. package/assets/icon-magnifying-glass.svg +5 -0
  46. package/assets/icon-warming-stripes.svg +13 -0
  47. package/assets/logo2.svg +31 -0
  48. package/components/AdvancedEditor/AdvancedEditor.tsx +4 -0
  49. package/components/AdvancedEditor/EmbedEditor.tsx +513 -0
  50. package/components/ComboBox/ComboBox.tsx +345 -0
  51. package/components/ComboBox/combobox.styles.css +185 -0
  52. package/components/ComboBox/index.ts +1 -0
  53. package/components/CustomColorsEditor/CustomColorsEditor.tsx +3 -10
  54. package/components/DataTable/DataTable.tsx +132 -58
  55. package/components/DataTable/data-table.css +216 -215
  56. package/components/DataTable/helpers/getSeriesName.ts +6 -0
  57. package/components/DataTable/helpers/mapCellMatrix.tsx +14 -6
  58. package/components/EditorPanel/ColumnsEditor.tsx +37 -19
  59. package/components/EditorPanel/DataTableEditor.tsx +51 -25
  60. package/components/EditorPanel/EditorPanel.styles.css +16 -0
  61. package/components/EditorPanel/EditorPanel.tsx +144 -0
  62. package/components/EditorPanel/EditorPanelDispatch.tsx +75 -0
  63. package/components/EditorPanel/FieldSetWrapper.tsx +66 -23
  64. package/components/EditorPanel/Inputs.tsx +33 -7
  65. package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +14 -6
  66. package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +240 -175
  67. package/components/EditorPanel/VizFilterEditor/components/FilterOrder.tsx +33 -29
  68. package/components/EditorPanel/sections/VisualSection.tsx +169 -0
  69. package/components/Filters/Filters.tsx +31 -5
  70. package/components/Filters/helpers/getNestedOptions.ts +2 -1
  71. package/components/Filters/helpers/handleSorting.ts +1 -1
  72. package/components/Layout/components/Sidebar/components/sidebar.styles.scss +84 -2
  73. package/components/Layout/components/Visualization/index.tsx +27 -1
  74. package/components/Layout/components/Visualization/visualizations.scss +7 -0
  75. package/components/Legend/Legend.Gradient.tsx +1 -1
  76. package/components/MediaControls.tsx +53 -28
  77. package/components/_stories/CustomColorsEditor.stories.tsx +37 -0
  78. package/components/_stories/DataTable.stories.tsx +1 -0
  79. package/components/ui/Icon.tsx +3 -1
  80. package/components/ui/Title/index.tsx +30 -2
  81. package/components/ui/Title/title.styles.css +42 -0
  82. package/data/colorPalettes.ts +18 -5
  83. package/data/mapColorPalettes.ts +10 -0
  84. package/devTemplate/dev.js +235 -0
  85. package/devTemplate/index.html +30 -0
  86. package/devTemplate/preview.html +1503 -0
  87. package/devTemplate/sidebar.css +151 -0
  88. package/dist/cove-main.css +2803 -4448
  89. package/dist/cove-main.css.map +1 -1
  90. package/generateViteConfig.js +118 -2
  91. package/helpers/DataTransform.ts +1 -5
  92. package/helpers/addValuesToFilters.ts +6 -1
  93. package/helpers/cove/date.ts +33 -1
  94. package/helpers/cove/string.ts +29 -0
  95. package/helpers/coveUpdateWorker.ts +21 -12
  96. package/helpers/embed/embedCodeGenerator.ts +80 -0
  97. package/helpers/embed/embedHelper.js +158 -0
  98. package/helpers/embed/filterUtils.ts +121 -0
  99. package/helpers/embed/index.ts +21 -0
  100. package/helpers/embed/urlValidation.ts +119 -0
  101. package/helpers/filterVizData.ts +6 -1
  102. package/helpers/getFileExtension.ts +0 -6
  103. package/helpers/getUniqueValues.ts +19 -0
  104. package/helpers/hashObj.ts +25 -0
  105. package/helpers/isRightAlignedTableValue.js +5 -0
  106. package/helpers/metrics/helpers.ts +1 -0
  107. package/helpers/metrics/types.ts +3 -0
  108. package/helpers/palettes/colorDistributions.ts +1 -1
  109. package/helpers/palettes/utils.ts +12 -12
  110. package/helpers/parseCsvWithQuotes.ts +15 -14
  111. package/helpers/pivotData.ts +2 -2
  112. package/helpers/prepareScreenshot.ts +288 -0
  113. package/helpers/queryStringUtils.ts +29 -0
  114. package/helpers/testing.ts +44 -0
  115. package/helpers/tests/DataTransform.test.ts +125 -0
  116. package/helpers/tests/date.test.ts +64 -0
  117. package/helpers/tests/prepareScreenshot.test.ts +414 -0
  118. package/helpers/tests/queryStringUtils.test.ts +381 -0
  119. package/helpers/tests/testStandaloneBuild.ts +23 -5
  120. package/helpers/useDataVizClasses.ts +0 -1
  121. package/helpers/vegaConfig.ts +1 -1
  122. package/helpers/vegaConfigImport.ts +160 -0
  123. package/helpers/ver/4.26.1.ts +80 -0
  124. package/helpers/ver/4.26.2.ts +84 -0
  125. package/helpers/ver/tests/4.26.1.test.ts +105 -0
  126. package/helpers/ver/tests/4.26.2.test.ts +298 -0
  127. package/helpers/viewports.ts +2 -0
  128. package/hooks/useDataColumns.ts +63 -0
  129. package/hooks/useFilterManagement.ts +94 -0
  130. package/hooks/useLegendSeparators.ts +26 -0
  131. package/hooks/useListManagement.ts +192 -0
  132. package/package.json +29 -33
  133. package/styles/_button-section.scss +0 -3
  134. package/styles/v2/components/editor.scss +9 -9
  135. package/styles/v2/utils/_grid.scss +8 -3
  136. package/types/Annotation.ts +10 -11
  137. package/types/Axis.ts +1 -0
  138. package/types/ForecastingSeriesKey.ts +1 -0
  139. package/types/General.ts +2 -0
  140. package/types/MarkupInclude.ts +1 -0
  141. package/types/Palette.ts +21 -0
  142. package/types/Series.ts +3 -0
  143. package/types/Table.ts +1 -0
  144. package/types/Visualization.ts +7 -0
  145. package/types/VizFilter.ts +1 -0
  146. package/LICENSE +0 -201
  147. 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>
@@ -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