@cdc/core 4.26.1 → 4.26.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (249) hide show
  1. package/.claude/agents/qa-test-developer.md +126 -0
  2. package/CLAUDE.local.md +67 -0
  3. package/LICENSE +201 -0
  4. package/_stories/Gallery.Charts.stories.tsx +35 -42
  5. package/_stories/Gallery.DataBite.stories.tsx +15 -8
  6. package/_stories/Gallery.Maps.stories.tsx +37 -28
  7. package/_stories/Gallery.WaffleChart.stories.tsx +1 -1
  8. package/_stories/PageART.stories.tsx +5 -4
  9. package/_stories/PageBRFSS.stories.tsx +21 -16
  10. package/_stories/PageCancerRegistries.stories.tsx +15 -15
  11. package/_stories/PageEasternEquineEncephalitis.stories.tsx +33 -19
  12. package/_stories/PageExcessiveAlcoholUse.stories.tsx +148 -143
  13. package/_stories/PageMaternalMortality.stories.tsx +5 -4
  14. package/_stories/PageOralHealth.stories.tsx +15 -10
  15. package/_stories/PageRespiratory.stories.tsx +4 -4
  16. package/_stories/PageSmokingTobacco.stories.tsx +15 -10
  17. package/_stories/PageStateDiabetesProfiles.stories.tsx +15 -10
  18. package/_stories/PageWastewater.stories.tsx +44 -30
  19. package/_stories/VegaImport.stories.tsx +401 -0
  20. package/_stories/vega-fixtures/bars-with-line.json +444 -0
  21. package/_stories/vega-fixtures/bars.json +58 -0
  22. package/_stories/vega-fixtures/combo-bar-rolling-mean.json +88 -0
  23. package/_stories/vega-fixtures/combo.json +68 -0
  24. package/_stories/vega-fixtures/grouped-horizontal-bars.json +83 -0
  25. package/_stories/vega-fixtures/grouped-horizontal-bars2.json +231 -0
  26. package/_stories/vega-fixtures/horizontal-bar.json +427 -0
  27. package/_stories/vega-fixtures/horizontal-bars-with-bad-colors.json +197 -0
  28. package/_stories/vega-fixtures/horizontal-bars2.json +58 -0
  29. package/_stories/vega-fixtures/lines.json +227 -0
  30. package/_stories/vega-fixtures/measles-bars.json +348 -0
  31. package/_stories/vega-fixtures/measles-map.json +11101 -0
  32. package/_stories/vega-fixtures/measles-stacked-bars.json +2147 -0
  33. package/_stories/vega-fixtures/multi-dataset.json +255 -0
  34. package/_stories/vega-fixtures/no-data.json +14 -0
  35. package/_stories/vega-fixtures/pie-chart.json +94 -0
  36. package/_stories/vega-fixtures/repeat-spec.json +47 -0
  37. package/_stories/vega-fixtures/stacked-area.json +222 -0
  38. package/_stories/vega-fixtures/stacked-bar-with-rect.json +3412 -0
  39. package/_stories/vega-fixtures/stacked-bars-with-line.json +364 -0
  40. package/_stories/vega-fixtures/stacked-bars.json +212 -0
  41. package/_stories/vega-fixtures/stacked-horizontal-bars.json +140 -0
  42. package/_stories/vega-fixtures/warning-combo.json +59 -0
  43. package/_stories/vega-fixtures/warning-scatter-and-line.json +1182 -0
  44. package/assets/callout-flag.svg +7 -0
  45. package/assets/icon-chart-area.svg +1 -0
  46. package/assets/icon-chart-radar.svg +23 -0
  47. package/assets/logo2.svg +31 -0
  48. package/components/AdvancedEditor/EmbedEditor.tsx +270 -38
  49. package/components/Alert/components/Alert.styles.css +2 -2
  50. package/components/ComboBox/combobox.styles.css +48 -48
  51. package/components/CustomColorsEditor/CustomColorsEditor.css +53 -53
  52. package/components/CustomColorsEditor/CustomColorsEditor.tsx +3 -10
  53. package/components/DataTable/DataTable.tsx +46 -18
  54. package/components/DataTable/DataTableStandAlone.tsx +1 -0
  55. package/components/DataTable/components/ChartHeader.tsx +21 -12
  56. package/components/DataTable/components/MapHeader.tsx +34 -28
  57. package/components/DataTable/components/SortIcon/sort-icon.css +5 -5
  58. package/components/DataTable/data-table.css +50 -52
  59. package/components/DataTable/helpers/applyCustomOrder.ts +17 -0
  60. package/components/DataTable/helpers/getChartCellValue.ts +10 -7
  61. package/components/DataTable/helpers/getMapDataTableColumnKeys.ts +22 -0
  62. package/components/DataTable/helpers/getSeriesName.ts +6 -0
  63. package/components/DataTable/helpers/mapCellMatrix.tsx +33 -23
  64. package/components/DataTable/helpers/tests/mapCellMatrix.test.ts +33 -0
  65. package/components/DownloadButton.tsx +14 -6
  66. package/components/EditorPanel/ColumnsEditor.tsx +38 -31
  67. package/components/EditorPanel/CustomSortOrder.tsx +94 -0
  68. package/components/EditorPanel/DataTableEditor.tsx +139 -23
  69. package/components/EditorPanel/EditorPanel.styles.css +71 -71
  70. package/components/EditorPanel/EditorPanel.tsx +3 -8
  71. package/components/EditorPanel/EditorPanelDispatch.tsx +4 -4
  72. package/components/EditorPanel/FootnotesEditor.tsx +2 -2
  73. package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +21 -12
  74. package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +16 -10
  75. package/components/EditorPanel/VizFilterEditor/components/FilterOrder.tsx +33 -29
  76. package/components/EditorPanel/components/MarkupVariablesEditor.tsx +160 -106
  77. package/components/EditorPanel/components/PanelMarkup.tsx +5 -1
  78. package/{styles/v2/components → components/EditorPanel}/editor.scss +76 -22
  79. package/components/EditorPanel/sections/StyleTreatmentSection.tsx +99 -0
  80. package/components/EditorPanel/sections/VisualSection.tsx +11 -0
  81. package/components/EditorWrapper/editor-wrapper.style.css +1 -1
  82. package/components/Filters/Filters.tsx +3 -5
  83. package/components/Filters/components/Tabs.tsx +19 -7
  84. package/{styles → components/Filters}/filters.scss +3 -3
  85. package/components/Footnotes/FootnotesStandAlone.tsx +4 -2
  86. package/components/HeaderThemeSelector/HeaderThemeSelector.css +61 -5
  87. package/components/Layout/components/Responsive.tsx +14 -6
  88. package/components/Layout/components/Sidebar/components/Sidebar.tsx +1 -1
  89. package/components/Layout/components/Sidebar/components/sidebar.styles.scss +14 -20
  90. package/components/Layout/components/Visualization/index.tsx +50 -38
  91. package/components/Layout/components/Visualization/visualizations.scss +232 -15
  92. package/components/Layout/components/VisualizationContainer.test.tsx +67 -0
  93. package/components/Layout/components/VisualizationContainer.tsx +37 -0
  94. package/components/Layout/components/VisualizationContent.test.tsx +182 -0
  95. package/components/Layout/components/VisualizationContent.tsx +75 -0
  96. package/components/Layout/index.tsx +5 -5
  97. package/components/Layout/styles/editor-utils.scss +3 -3
  98. package/components/Layout/styles/editor.scss +4 -4
  99. package/components/Legend/Legend.Gradient.tsx +7 -1
  100. package/components/Loader/loader.styles.css +2 -2
  101. package/components/Loading.jsx +1 -1
  102. package/components/MediaControls.tsx +10 -3
  103. package/components/MultiSelect/multiselect.styles.css +19 -19
  104. package/components/NestedDropdown/nesteddropdown.styles.css +15 -15
  105. package/components/PaletteSelector/PaletteSelector.css +15 -15
  106. package/components/RichTooltip/richTooltip.css +6 -6
  107. package/components/Table/table.styles.css +2 -2
  108. package/components/Waiting.tsx +1 -1
  109. package/components/_stories/CustomColorsEditor.stories.tsx +37 -0
  110. package/components/_stories/DataTable.stories.tsx +1 -0
  111. package/components/_stories/Filters.stories.tsx +1 -1
  112. package/components/_stories/styles.scss +0 -1
  113. package/components/elements/Button.jsx +1 -1
  114. package/components/elements/Card.jsx +1 -1
  115. package/{styles/v2/components → components/elements}/button.scss +9 -8
  116. package/components/inputs/InputCheckbox.jsx +1 -1
  117. package/components/inputs/InputSelect.tsx +1 -1
  118. package/components/inputs/InputText.jsx +1 -1
  119. package/components/inputs/InputToggle.tsx +1 -1
  120. package/{styles/v2/components/input → components/inputs}/_input-check-radio.scss +2 -2
  121. package/{styles/v2/components/input → components/inputs}/_input-group.scss +3 -3
  122. package/{styles/v2/components/input → components/inputs}/_input-slider.scss +2 -2
  123. package/{styles/v2/components/input → components/inputs}/_input.scss +5 -5
  124. package/{styles/v2/components/input → components/inputs}/index.scss +2 -2
  125. package/{styles → components}/loading.scss +1 -1
  126. package/components/managers/DataDesigner.tsx +1 -1
  127. package/{styles/v2/components → components/managers}/data-designer.scss +6 -7
  128. package/components/ui/Accordion.jsx +1 -1
  129. package/components/ui/Icon.tsx +1 -1
  130. package/components/ui/LoadSpin.jsx +1 -1
  131. package/components/ui/Modal.jsx +1 -1
  132. package/components/ui/Overlay.jsx +1 -1
  133. package/components/ui/Title/index.test.tsx +34 -0
  134. package/components/ui/Title/index.tsx +24 -7
  135. package/components/ui/Title/title.styles.css +119 -25
  136. package/components/ui/Tooltip.tsx +1 -1
  137. package/components/ui/_stories/Title.stories.tsx +1 -1
  138. package/{styles/v2/components → components/ui}/accordion.scss +3 -3
  139. package/components/ui/accordion.styles.css +11 -11
  140. package/{styles/v2/components → components/ui}/modal.scss +2 -2
  141. package/{styles/v2/components → components/ui}/overlay.scss +6 -6
  142. package/{styles/v2/components → components}/ui/tooltip.scss +1 -1
  143. package/{styles → components}/waiting.scss +9 -3
  144. package/data/colorPalettes.ts +18 -5
  145. package/data/mapColorPalettes.ts +10 -0
  146. package/devTemplate/dev.js +285 -0
  147. package/devTemplate/index.html +30 -0
  148. package/devTemplate/preview.html +1503 -0
  149. package/devTemplate/sidebar.css +151 -0
  150. package/dist/cove-main.css +2530 -3901
  151. package/dist/cove-main.css.map +1 -1
  152. package/generateViteConfig.js +111 -2
  153. package/helpers/DataTransform.ts +1 -5
  154. package/helpers/backfillDefaults.ts +35 -0
  155. package/helpers/constants.ts +12 -0
  156. package/helpers/cove/date.ts +64 -3
  157. package/helpers/cove/number.ts +29 -15
  158. package/helpers/cove/string.ts +29 -0
  159. package/helpers/coveUpdateWorker.ts +14 -8
  160. package/helpers/displayDataAsText.ts +1 -1
  161. package/helpers/embed/embedCodeGenerator.ts +80 -0
  162. package/helpers/embed/embedHelper.js +169 -0
  163. package/helpers/embed/filterUtils.ts +121 -0
  164. package/helpers/embed/index.ts +17 -0
  165. package/helpers/embed/urlValidation.ts +119 -0
  166. package/helpers/extractDataAndMetadata.ts +20 -0
  167. package/helpers/fetchRemoteData.ts +14 -8
  168. package/helpers/filterVizData.ts +6 -1
  169. package/helpers/getFileExtension.ts +0 -6
  170. package/helpers/labelHash.ts +9 -0
  171. package/helpers/markupProcessor.ts +56 -38
  172. package/helpers/metrics/types.ts +3 -0
  173. package/helpers/palettes/colorDistributions.ts +1 -1
  174. package/helpers/palettes/utils.ts +12 -12
  175. package/helpers/parseCsvWithQuotes.ts +15 -14
  176. package/helpers/prepareScreenshot.ts +33 -10
  177. package/helpers/testing.ts +44 -0
  178. package/helpers/tests/DataTransform.test.ts +125 -0
  179. package/helpers/tests/abbreviateNumber.test.ts +59 -0
  180. package/helpers/tests/backfillDefaults.test.ts +253 -0
  181. package/helpers/tests/date.test.ts +110 -0
  182. package/helpers/tests/extractDataAndMetadata.test.ts +93 -0
  183. package/helpers/tests/markupProcessor.test.ts +315 -124
  184. package/helpers/tests/number.test.ts +42 -0
  185. package/helpers/tests/prepareScreenshot.test.ts +28 -28
  186. package/helpers/tests/testStandaloneBuild.ts +36 -26
  187. package/helpers/tests/useDataVizClasses.test.ts +66 -0
  188. package/helpers/tests/visualizationWrapperUsage.test.ts +57 -0
  189. package/helpers/useDataVizClasses.ts +13 -7
  190. package/helpers/vegaConfig.ts +1 -1
  191. package/helpers/vegaConfigImport.ts +160 -0
  192. package/helpers/ver/4.24.4.ts +24 -0
  193. package/helpers/ver/4.26.1.ts +1 -1
  194. package/helpers/ver/4.26.2.ts +84 -0
  195. package/helpers/ver/4.26.3.ts +44 -0
  196. package/helpers/ver/4.26.4.ts +31 -0
  197. package/helpers/ver/tests/4.26.1.test.ts +105 -0
  198. package/helpers/ver/tests/4.26.2.test.ts +298 -0
  199. package/helpers/ver/tests/4.26.3.test.ts +168 -0
  200. package/helpers/ver/tests/4.26.4.test.ts +88 -0
  201. package/helpers/ver/tests/coveUpdateWorker.test.ts +57 -0
  202. package/helpers/viewports.ts +2 -0
  203. package/package.json +27 -32
  204. package/styles/_global.scss +7 -7
  205. package/styles/_reset.scss +2 -2
  206. package/styles/{v2/base → base}/_file-selector.scss +4 -4
  207. package/styles/{v2/base → base}/_general.scss +2 -4
  208. package/styles/{v2/base → base}/index.scss +1 -1
  209. package/styles/base.scss +107 -165
  210. package/styles/cove-main.scss +3 -6
  211. package/styles/layout/_component.scss +110 -0
  212. package/styles/{v2/layout → layout}/_data-table.scss +7 -7
  213. package/styles/layout/_wrapper-padding.scss +27 -0
  214. package/styles/{v2/main.scss → main.scss} +3 -1
  215. package/styles/{v2/themes → themes}/_color-definitions.scss +46 -41
  216. package/styles/{_accessibility.scss → utils/_accessibility.scss} +1 -1
  217. package/styles/{v2/utils → utils}/_grid.scss +8 -3
  218. package/styles/{_global-variables.scss → utils/_properties.scss} +133 -112
  219. package/styles/{v2/utils → utils}/index.scss +2 -1
  220. package/types/Annotation.ts +10 -11
  221. package/types/Axis.ts +2 -0
  222. package/types/ComponentStyles.ts +1 -0
  223. package/types/ConfigureData.ts +1 -0
  224. package/types/General.ts +2 -0
  225. package/types/MarkupInclude.ts +1 -0
  226. package/types/MarkupVariable.ts +2 -1
  227. package/types/Palette.ts +22 -0
  228. package/types/Table.ts +9 -0
  229. package/types/Visualization.ts +7 -0
  230. package/_stories/StoryRenderingTests.stories.tsx +0 -164
  231. package/helpers/embedCodeGenerator.ts +0 -109
  232. package/styles/_common-components.css +0 -73
  233. package/styles/_variables.scss +0 -63
  234. package/styles/v2/layout/_component.scss +0 -21
  235. package/styles/v2/utils/_variables.scss +0 -9
  236. package/{styles/v2/components/card.scss → components/elements/card.css} +2 -2
  237. /package/{styles/v2/components → components/ui}/icon.scss +0 -0
  238. /package/{styles/v2/components → components/ui}/loadspin.scss +0 -0
  239. /package/styles/{v2/base → base}/_heading.scss +0 -0
  240. /package/styles/{v2/base → base}/_reset.scss +0 -0
  241. /package/styles/{v2/layout → layout}/_alert.scss +0 -0
  242. /package/styles/{v2/layout → layout}/_progression.scss +0 -0
  243. /package/styles/{v2/layout → layout}/_tooltip.scss +0 -0
  244. /package/styles/{v2/layout → layout}/index.scss +0 -0
  245. /package/styles/{v2/themes → themes}/index.scss +0 -0
  246. /package/styles/{v2/utils → utils}/_align.scss +0 -0
  247. /package/styles/{v2/utils → utils}/_animations.scss +0 -0
  248. /package/styles/{v2/utils → utils}/_breakpoints.scss +0 -0
  249. /package/styles/{v2/utils → utils}/_mixins.scss +0 -0
@@ -26,13 +26,23 @@ export const processMarkupVariables = (
26
26
  filters?: VizFilter[]
27
27
  datasets?: Datasets
28
28
  configDataKey?: string // Add support for widget's assigned dataset
29
+ locale?: string
30
+ dataMetadata?: Record<string, string>
29
31
  } = {}
30
32
  ): {
31
33
  processedContent: string
32
34
  shouldHideSection: boolean
33
35
  shouldShowNoDataMessage: boolean
34
36
  } => {
35
- const { isEditor = false, showNoDataMessage = false, allowHideSection = false, filters = [], datasets, configDataKey } = options
37
+ const {
38
+ isEditor = false,
39
+ showNoDataMessage = false,
40
+ allowHideSection = false,
41
+ filters = [],
42
+ datasets,
43
+ configDataKey,
44
+ locale = 'en-US'
45
+ } = options
36
46
 
37
47
  // Helper function to get data for a specific variable
38
48
  const getDataForVariable = (variable: MarkupVariable): any[] => {
@@ -65,41 +75,51 @@ export const processMarkupVariables = (
65
75
  const workingVariable = markupVariables.find(variable => variable.tag === variableTag)
66
76
  if (!workingVariable) return variableTag
67
77
 
68
- // Validate that columnName exists
69
- if (!workingVariable.columnName) {
70
- console.warn(`Markup variable ${variableTag} has no columnName specified`)
71
- return variableTag
78
+ // Resolve the data source for this variable. Metadata-sourced variables
79
+ // (metadataKey) pull a single value from the data file's top-level fields,
80
+ // while column-sourced variables (columnName) pull from dataset rows.
81
+ // Both paths produce a `conditionFilteredData` array so the downstream
82
+ // extraction, formatting (addCommas), dedup, and hide-check logic is shared.
83
+ let effectiveColumnName: string
84
+ let conditionFilteredData: any[]
85
+
86
+ if (workingVariable.metadataKey) {
87
+ // Metadata path: synthesize a single-row array from the file-level metadata
88
+ // so it flows through the same formatting pipeline as column values.
89
+ effectiveColumnName = workingVariable.metadataKey
90
+ const metaValue = options.dataMetadata?.[effectiveColumnName] ?? ''
91
+ conditionFilteredData = metaValue ? [{ [effectiveColumnName]: metaValue }] : []
92
+ } else {
93
+ // Column path: pull values from the dataset, applying filters and conditions.
94
+ if (!workingVariable.columnName) {
95
+ console.warn(`Markup variable ${variableTag} has no columnName specified`)
96
+ return variableTag
97
+ }
98
+ effectiveColumnName = workingVariable.columnName
99
+
100
+ let variableData = getDataForVariable(workingVariable)
101
+ if (filters && filters.length > 0) {
102
+ variableData = filterVizData(filters, variableData)
103
+ }
104
+
105
+ conditionFilteredData =
106
+ workingVariable.conditions.length === 0
107
+ ? variableData
108
+ : filterDataByConditions(variableData, [...workingVariable.conditions])
72
109
  }
73
110
 
74
- // Get the appropriate dataset for this variable
75
- let variableData = getDataForVariable(workingVariable)
76
-
77
- // Apply global filters if present
78
- if (filters && filters.length > 0) {
79
- variableData = filterVizData(filters, variableData)
80
- }
81
-
82
- // Filter data with error handling (apply conditions on top of already filtered data)
83
- const conditionFilteredData =
84
- workingVariable.conditions.length === 0
85
- ? variableData
86
- : filterDataByConditions(variableData, [...workingVariable.conditions])
87
-
88
111
  // Extract values with error handling
89
112
  const variableValues: string[] = _.uniq(
90
113
  (conditionFilteredData || []).map(dataObject => {
91
114
  try {
92
- const dataObjectValue = dataObject[workingVariable.columnName]
115
+ const dataObjectValue = dataObject[effectiveColumnName]
93
116
 
94
- // Handle undefined column
95
117
  if (dataObjectValue === undefined && isEditor) {
96
- console.warn(
97
- `Column "${workingVariable.columnName}" not found in data for variable ${variableTag}`
98
- )
118
+ console.warn(`Column "${effectiveColumnName}" not found in data for variable ${variableTag}`)
99
119
  }
100
120
 
101
121
  return workingVariable.addCommas && !isNaN(parseFloat(dataObjectValue))
102
- ? parseFloat(dataObjectValue).toLocaleString('en-US', { useGrouping: true })
122
+ ? parseFloat(dataObjectValue).toLocaleString(locale, { useGrouping: true })
103
123
  : String(dataObjectValue || '')
104
124
  } catch (error) {
105
125
  console.error(`Error processing data value for ${variableTag}:`, error)
@@ -153,13 +173,12 @@ const filterDataByConditions = (data: any[], conditions: MarkupCondition[]): any
153
173
  const [currentCondition, ...remainingConditions] = conditions
154
174
  const { columnName, isOrIsNotEqualTo, value } = currentCondition
155
175
 
156
- const filteredData = isOrIsNotEqualTo === 'is'
157
- ? data.filter(dataObject => String(dataObject[columnName]) === value)
158
- : data.filter(dataObject => String(dataObject[columnName]) !== value)
176
+ const filteredData =
177
+ isOrIsNotEqualTo === 'is'
178
+ ? data.filter(dataObject => String(dataObject[columnName]) === value)
179
+ : data.filter(dataObject => String(dataObject[columnName]) !== value)
159
180
 
160
- return remainingConditions.length === 0
161
- ? filteredData
162
- : filterDataByConditions(filteredData, remainingConditions)
181
+ return remainingConditions.length === 0 ? filteredData : filterDataByConditions(filteredData, remainingConditions)
163
182
  }
164
183
 
165
184
  /**
@@ -180,10 +199,7 @@ const formatValuesList = (values: string[], conjunction: string): string[] => {
180
199
  /**
181
200
  * Validates markup variables configuration
182
201
  */
183
- export const validateMarkupVariables = (
184
- markupVariables: MarkupVariable[],
185
- data: any[]
186
- ): string[] => {
202
+ export const validateMarkupVariables = (markupVariables: MarkupVariable[], data: any[]): string[] => {
187
203
  const errors: string[] = []
188
204
 
189
205
  if (!markupVariables || !Array.isArray(markupVariables)) {
@@ -197,9 +213,9 @@ export const validateMarkupVariables = (
197
213
  errors.push(`Variable ${index + 1}: Tag must be in format {{tagName}}`)
198
214
  }
199
215
 
200
- if (!variable.columnName) {
216
+ if (!variable.metadataKey && !variable.columnName) {
201
217
  errors.push(`Variable ${index + 1}: Column name is required`)
202
- } else if (availableColumns.length > 0 && !availableColumns.includes(variable.columnName)) {
218
+ } else if (variable.columnName && availableColumns.length > 0 && !availableColumns.includes(variable.columnName)) {
203
219
  errors.push(`Variable ${index + 1}: Column "${variable.columnName}" not found in data`)
204
220
  }
205
221
 
@@ -207,7 +223,9 @@ export const validateMarkupVariables = (
207
223
  if (!condition.columnName) {
208
224
  errors.push(`Variable ${index + 1}, Condition ${condIndex + 1}: Column name is required`)
209
225
  } else if (availableColumns.length > 0 && !availableColumns.includes(condition.columnName)) {
210
- errors.push(`Variable ${index + 1}, Condition ${condIndex + 1}: Column "${condition.columnName}" not found in data`)
226
+ errors.push(
227
+ `Variable ${index + 1}, Condition ${condIndex + 1}: Column "${condition.columnName}" not found in data`
228
+ )
211
229
  }
212
230
 
213
231
  if (!condition.value) {
@@ -80,3 +80,6 @@ export type ANALYTICS_EVENT_TYPES =
80
80
 
81
81
  // Image/export events
82
82
  | 'image_download' // generic image download event
83
+
84
+ // Embed events
85
+ | 'embed_loaded' // fired when a visualization is loaded via the embed system
@@ -53,4 +53,4 @@ export const mapV1ColorDistribution = {
53
53
  8: [0, 2, 3, 4, 5, 6, 7, 8],
54
54
  9: [0, 1, 2, 3, 4, 5, 6, 7, 8],
55
55
  10: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
56
- }
56
+ }
@@ -1,16 +1,16 @@
1
- import { map } from 'lodash'
2
1
  import { FALLBACK_COLOR_PALETTE_V1, FALLBACK_COLOR_PALETTE_V2, USE_V2_MIGRATION } from '../constants'
3
2
  import { getColorPaletteVersion } from '../getColorPaletteVersion'
4
3
  import { getPaletteAccessor } from '../getPaletteAccessor'
5
4
  import { chartPaletteMigrationMap } from './migratePaletteName'
6
5
  import { newMapPaletteNames } from './standardizePaletteNames'
6
+ import { Visualization } from '../../types/Visualization'
7
7
 
8
8
  /**
9
9
  * Gets the current palette name from a visualization config
10
10
  * @param config - The visualization config object
11
11
  * @returns The current palette name or empty string if not found
12
12
  */
13
- export const getCurrentPaletteName = (config: any): string => {
13
+ export const getCurrentPaletteName = (config: Partial<Visualization>): string => {
14
14
  // Check new v2 format first
15
15
  if (config?.general?.palette?.name) {
16
16
  return config.general.palette.name
@@ -35,7 +35,7 @@ export const getCurrentPaletteName = (config: any): string => {
35
35
  * @param colorPalettes - The color palettes object (e.g., mapColorPalettes, chartColorPalettes)
36
36
  * @returns The palette colors array or empty array if not found
37
37
  */
38
- export const getPaletteColors = (config: any, colorPalettes: any): string[] => {
38
+ export const getPaletteColors = (config: Partial<Visualization>, colorPalettes: Record<string, Record<string, string[]>>): string[] => {
39
39
  // First check for custom colors (v2 format)
40
40
  if (config?.general?.palette?.customColors) {
41
41
  return config.general.palette.customColors
@@ -65,7 +65,7 @@ export const getPaletteColors = (config: any, colorPalettes: any): string[] => {
65
65
  * @param config - The visualization config object
66
66
  * @returns True if the config is using v1 palette configuration (which would show conversion modal)
67
67
  */
68
- export const isV1Palette = (config: any): boolean => {
68
+ export const isV1Palette = (config: Partial<Visualization>): boolean => {
69
69
  // If v2 migration is disabled globally, don't treat as v1 (no conversion modal)
70
70
  if (!USE_V2_MIGRATION) {
71
71
  return false
@@ -84,7 +84,7 @@ export const isV1Palette = (config: any): boolean => {
84
84
  * @param config - The visualization config object
85
85
  * @returns The fallback palette name for the detected version
86
86
  */
87
- export const getFallbackColorPalette = (config: any): string => {
87
+ export const getFallbackColorPalette = (config: Partial<Visualization>): string => {
88
88
  const paletteVersion = getColorPaletteVersion(config)
89
89
  return paletteVersion === 1 ? FALLBACK_COLOR_PALETTE_V1 : FALLBACK_COLOR_PALETTE_V2
90
90
  }
@@ -161,7 +161,7 @@ export const migratePaletteWithMap = (
161
161
  * @param config - The visualization config object
162
162
  * @returns True if backup data exists
163
163
  */
164
- export const hasPaletteBackup = (config: any): boolean => {
164
+ export const hasPaletteBackup = (config: Partial<Visualization>): boolean => {
165
165
  return !!(config?.general?.palette?.backups?.length > 0)
166
166
  }
167
167
 
@@ -170,7 +170,7 @@ export const hasPaletteBackup = (config: any): boolean => {
170
170
  * @param config - The visualization config object
171
171
  * @returns The original palette name or null if no backup exists
172
172
  */
173
- export const getOriginalPaletteName = (config: any): string | null => {
173
+ export const getOriginalPaletteName = (config: Partial<Visualization>): string | null => {
174
174
  const backups = config?.general?.palette?.backups
175
175
  if (!backups || backups.length === 0) return null
176
176
 
@@ -184,7 +184,7 @@ export const getOriginalPaletteName = (config: any): string | null => {
184
184
  * @param config - The visualization config object
185
185
  * @returns The original two-color palette name or null if no backup exists
186
186
  */
187
- export const getOriginalTwoColorPaletteName = (config: any): string | null => {
187
+ export const getOriginalTwoColorPaletteName = (config: Partial<Visualization>): string | null => {
188
188
  const backups = config?.general?.palette?.backups
189
189
  if (!backups || backups.length === 0) return null
190
190
 
@@ -198,7 +198,7 @@ export const getOriginalTwoColorPaletteName = (config: any): string | null => {
198
198
  * @param config - The visualization config object
199
199
  * @returns True if two-color backup data exists
200
200
  */
201
- export const hasTwoColorPaletteBackup = (config: any): boolean => {
201
+ export const hasTwoColorPaletteBackup = (config: Partial<Visualization>): boolean => {
202
202
  const backups = config?.general?.palette?.backups
203
203
  if (!backups || backups.length === 0) return false
204
204
  return backups.some((backup: any) => backup.type === 'twoColor')
@@ -209,7 +209,7 @@ export const hasTwoColorPaletteBackup = (config: any): boolean => {
209
209
  * @param config - The visualization config object to modify
210
210
  * @returns True if rollback was successful, false if no backup available
211
211
  */
212
- export const rollbackPaletteToOriginal = (config: any): boolean => {
212
+ export const rollbackPaletteToOriginal = (config: Partial<Visualization>): boolean => {
213
213
  const backups = config?.general?.palette?.backups
214
214
  if (!backups || backups.length === 0) {
215
215
  return false
@@ -230,7 +230,7 @@ export const rollbackPaletteToOriginal = (config: any): boolean => {
230
230
  config.general.palette.version = '1.0' // Reset to v1
231
231
  }
232
232
 
233
- return config
233
+ return true
234
234
  }
235
235
 
236
236
  /**
@@ -238,7 +238,7 @@ export const rollbackPaletteToOriginal = (config: any): boolean => {
238
238
  * @param config - The visualization config object to modify
239
239
  * @returns True if rollback was successful, false if no backup available
240
240
  */
241
- export const rollbackTwoColorPaletteToOriginal = (config: any): boolean => {
241
+ export const rollbackTwoColorPaletteToOriginal = (config: Partial<Visualization>): boolean => {
242
242
  const backups = config?.general?.palette?.backups
243
243
  if (!backups || backups.length === 0) {
244
244
  return false
@@ -2,7 +2,7 @@ import Papa from 'papaparse'
2
2
 
3
3
  /**
4
4
  * Parses CSV text while preserving newlines and commas within quoted fields.
5
- *
5
+ *
6
6
  * @param responseText - The raw CSV text to parse
7
7
  * @param options - Parsing options
8
8
  * @param options.delimiter - The delimiter to use after processing (default: '|')
@@ -17,27 +17,28 @@ export function parseCsvWithQuotes(
17
17
  } = {}
18
18
  ): any[] {
19
19
  const { delimiter = '|', dynamicTyping = false } = options
20
-
20
+
21
21
  const NEWLINE_PLACEHOLDER = '__COVE_NEWLINE__'
22
-
22
+ const newlinePlaceholderRegex = new RegExp(NEWLINE_PLACEHOLDER, 'g')
23
+
23
24
  // Preserve newlines in quoted fields by replacing with placeholder
24
25
  const quotedFields: string[] = []
25
26
  let placeholderIndex = 0
26
- let sanitizedText = responseText.replace(/("(?:[^"\\]|\\.|[\s\S])*?")/g, (match) => {
27
+ let sanitizedText = responseText.replace(/("(?:[^"\\]|\\.|[\s\S])*?")/g, match => {
27
28
  const preserved = match.replace(/\n/g, NEWLINE_PLACEHOLDER)
28
29
  quotedFields.push(preserved)
29
30
  return `__QUOTED_FIELD_${placeholderIndex++}__`
30
31
  })
31
-
32
+
32
33
  // Replace commas outside quoted fields with pipe delimiter
33
34
  sanitizedText = sanitizedText.replace(/(__QUOTED_FIELD_\d+__)|,/g, (...m) => m[1] || delimiter)
34
-
35
- // Restore quoted fields without outer quotes
36
- quotedFields.forEach((field, index) => {
37
- const unquoted = field.slice(1, -1).replace(new RegExp(NEWLINE_PLACEHOLDER, 'g'), '\n')
38
- sanitizedText = sanitizedText.replace(`__QUOTED_FIELD_${index}__`, unquoted)
35
+
36
+ // Restore quoted fields without outer quotes (single pass instead of N passes)
37
+ sanitizedText = sanitizedText.replace(/__QUOTED_FIELD_(\d+)__/g, (_, idx) => {
38
+ const field = quotedFields[parseInt(idx, 10)]
39
+ return field.slice(1, -1).replace(newlinePlaceholderRegex, '\n')
39
40
  })
40
-
41
+
41
42
  // Parse with Papa.parse
42
43
  const parsedCsv = Papa.parse(sanitizedText, {
43
44
  header: true,
@@ -45,21 +46,21 @@ export function parseCsvWithQuotes(
45
46
  delimiter,
46
47
  dynamicTyping
47
48
  })
48
-
49
+
49
50
  // Restore newlines in parsed data
50
51
  const restoredData = parsedCsv.data.map((row: any) => {
51
52
  const restoredRow: any = {}
52
53
  Object.keys(row).forEach(key => {
53
54
  const value = row[key]
54
55
  if (typeof value === 'string') {
55
- restoredRow[key] = value.replace(new RegExp(NEWLINE_PLACEHOLDER, 'g'), '\n')
56
+ restoredRow[key] = value.replace(newlinePlaceholderRegex, '\n')
56
57
  } else {
57
58
  restoredRow[key] = value
58
59
  }
59
60
  })
60
61
  return restoredRow
61
62
  })
62
-
63
+
63
64
  return restoredData
64
65
  }
65
66
 
@@ -80,7 +80,7 @@ function findNearestHeadingIndex(parentChildren: Element[], vizWrapperIndex: num
80
80
  const child = parentChildren[i] as HTMLElement
81
81
 
82
82
  // STOP: Another visualization found - don't include its content
83
- if (child.classList.contains('cdc-open-viz-module') || child.querySelector('.cdc-open-viz-module')) {
83
+ if (child.classList.contains('cove-visualization') || child.querySelector('.cove-visualization')) {
84
84
  return -1
85
85
  }
86
86
 
@@ -135,7 +135,7 @@ function buildContextClone(
135
135
  }
136
136
 
137
137
  function isInEditorMode(element: HTMLElement): boolean {
138
- return element.closest('.cdc-open-viz-module.isEditor') !== null
138
+ return element.closest('.cove-visualization.is-editor') !== null
139
139
  }
140
140
 
141
141
  /**
@@ -208,6 +208,23 @@ function convertCanvasToImages(baseSvg: HTMLElement, clonedViz: HTMLElement): vo
208
208
  })
209
209
  }
210
210
 
211
+ /**
212
+ * Remove width classes that interfere with screenshot sizing
213
+ */
214
+ function removeWidthClasses(clonedTree: HTMLElement): void {
215
+ const classesToRemove = ['dfe-block--width-wide', 'dfe-block--width-full_width']
216
+
217
+ classesToRemove.forEach(className => {
218
+ const elements = clonedTree.querySelectorAll(`.${className}`)
219
+ elements.forEach(el => el.classList.remove(className))
220
+ })
221
+
222
+ // Also check the root element itself
223
+ classesToRemove.forEach(className => {
224
+ clonedTree.classList.remove(className)
225
+ })
226
+ }
227
+
211
228
  /**
212
229
  * Expand SVG widths and remove animation classes
213
230
  */
@@ -222,7 +239,10 @@ function expandSvgWidths(clonedViz: HTMLElement): void {
222
239
  }
223
240
 
224
241
  // Remove animation classes to show final state immediately
225
- svg.classList.remove('animated', 'animate')
242
+ const isAnimatedPie = svg.classList.contains('animated-pie')
243
+ if (!isAnimatedPie) {
244
+ svg.classList.remove('animated', 'animate')
245
+ }
226
246
  })
227
247
  }
228
248
 
@@ -237,31 +257,34 @@ export function prepareScreenshotContainer(
237
257
  // 1. Clone elements (with or without context)
238
258
  const { clonedTree, clonedViz } = prepareClonedElements(baseSvg, includeContextInDownload, elementToCapture)
239
259
 
240
- // 2. Strip all links (not clickable in static image)
260
+ // 2. Remove width classes that interfere with screenshot sizing
261
+ removeWidthClasses(clonedTree)
262
+
263
+ // 3. Strip all links (not clickable in static image)
241
264
  stripLinks(clonedTree)
242
265
 
243
- // 3. Convert canvas elements to images
266
+ // 4. Convert canvas elements to images
244
267
  convertCanvasToImages(baseSvg, clonedViz)
245
268
 
246
- // 4. Expand SVG widths to prevent clipping
269
+ // 5. Expand SVG widths to prevent clipping
247
270
  expandSvgWidths(clonedViz)
248
271
 
249
- // 5. Calculate viz dimensions
272
+ // 6. Calculate viz dimensions
250
273
  const computedStyle = getComputedStyle(baseSvg)
251
274
  const vizWidth =
252
275
  parseFloat(computedStyle.width) -
253
276
  (parseFloat(computedStyle.paddingLeft) || 0) -
254
277
  (parseFloat(computedStyle.paddingRight) || 0)
255
278
 
256
- // 6. Create and style container
279
+ // 7. Create and style container
257
280
  const container = document.createElement('div')
258
281
  container.style.width = `${vizWidth + 36}px`
259
282
  container.style.padding = '18px'
260
283
 
261
- // 7. Reset viz padding
284
+ // 8. Reset viz padding
262
285
  clonedViz.style.padding = '0'
263
286
 
264
- // 8. Append cloned tree to container
287
+ // 9. Append cloned tree to container
265
288
  container.appendChild(clonedTree)
266
289
 
267
290
  return container
@@ -313,6 +313,50 @@ export const testBooleanControl = async (checkbox: HTMLInputElement, getVisualSt
313
313
  expect(secondToggleVisualState).toEqual(initialVisualState)
314
314
  }
315
315
 
316
+ // ============================================================================
317
+ // VISUALIZATION RENDERING ASSERTIONS
318
+ // ============================================================================
319
+
320
+ /**
321
+ * Assert that a visualization has rendered successfully.
322
+ * Uses performAndAssert to poll until one of these conditions is met:
323
+ * ((svgCount > 0 || canvasCount > 0) && hasCoveModule) || isDataBite || isDataTable
324
+ *
325
+ * Handles all visualization types:
326
+ * - Charts (SVG-based)
327
+ * - Maps with SVG rendering (US state, world, hex, etc.)
328
+ * - Maps with canvas rendering (county maps via UsaMap.County)
329
+ * - Data bites (.bite-content)
330
+ * - Data tables (.data-table)
331
+ * - Waffle charts (SVG-based)
332
+ *
333
+ * Use as a play function in any Components/Templates/ story:
334
+ * play: async ({ canvasElement }) => {
335
+ * await assertVisualizationRendered(canvasElement)
336
+ * }
337
+ *
338
+ * @param vizElement The story's canvas element (Storybook's canvasElement)
339
+ */
340
+ export const assertVisualizationRendered = async (vizElement: HTMLElement) => {
341
+ await performAndAssert(
342
+ 'Wait for visualization to render',
343
+ () => {
344
+ const svgCount = vizElement.querySelectorAll('svg').length
345
+ const canvasCount = vizElement.querySelectorAll('canvas').length
346
+ const hasCoveModule = !!vizElement.querySelector('.cove-visualization')
347
+ const isDataBite = !!vizElement.querySelector('.bite-content')
348
+ const isDataTable = !!vizElement.querySelector('.type-data-table')
349
+ return { svgCount, canvasCount, hasCoveModule, isDataBite, isDataTable }
350
+ },
351
+ async () => {},
352
+ (_before, after) => {
353
+ return (
354
+ ((after.svgCount > 0 || after.canvasCount > 0) && after.hasCoveModule) || after.isDataBite || after.isDataTable
355
+ )
356
+ }
357
+ )
358
+ }
359
+
316
360
  // ============================================================================
317
361
  // DATA EXTRACTION HELPERS
318
362
  // ============================================================================
@@ -0,0 +1,125 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import DataTransform from '../DataTransform'
3
+
4
+ describe('DataTransform', () => {
5
+ let transformer: DataTransform
6
+
7
+ beforeEach(() => {
8
+ transformer = new DataTransform()
9
+ })
10
+
11
+ describe('autoStandardize', () => {
12
+ it('returns undefined for empty data', () => {
13
+ expect(transformer.autoStandardize([])).toBeUndefined()
14
+ expect(transformer.autoStandardize(null)).toBeUndefined()
15
+ expect(transformer.autoStandardize(undefined)).toBeUndefined()
16
+ })
17
+
18
+ it('returns undefined for invalid data format', () => {
19
+ expect(transformer.autoStandardize(['string', 'array'])).toBeUndefined()
20
+ expect(transformer.autoStandardize([1, 2, 3])).toBeUndefined()
21
+ })
22
+
23
+ it('returns data unchanged when already in object format', () => {
24
+ const data = [
25
+ { name: 'Alice', value: 10 },
26
+ { name: 'Bob', value: 20 }
27
+ ]
28
+ const result = transformer.autoStandardize(data)
29
+ expect(result).toEqual(data)
30
+ })
31
+
32
+ it('converts array of arrays to array of objects', () => {
33
+ const data = [
34
+ ['name', 'value'],
35
+ ['Alice', 10],
36
+ ['Bob', 20]
37
+ ]
38
+ const result = transformer.autoStandardize(data)
39
+ expect(result).toEqual([
40
+ { name: 'Alice', value: 10 },
41
+ { name: 'Bob', value: 20 }
42
+ ])
43
+ })
44
+
45
+ it('handles array of arrays with multiple columns', () => {
46
+ const data = [
47
+ ['state', 'year', 'population'],
48
+ ['CA', 2020, 39538223],
49
+ ['TX', 2020, 29145505]
50
+ ]
51
+ const result = transformer.autoStandardize(data)
52
+ expect(result).toEqual([
53
+ { state: 'CA', year: 2020, population: 39538223 },
54
+ { state: 'TX', year: 2020, population: 29145505 }
55
+ ])
56
+ })
57
+ })
58
+
59
+ describe('cleanDataPoint', () => {
60
+ it('removes commas from numbers', () => {
61
+ expect(transformer.cleanDataPoint('1,234')).toBe('1234')
62
+ expect(transformer.cleanDataPoint('1,234,567')).toBe('1234567')
63
+ })
64
+
65
+ it('removes dollar signs', () => {
66
+ expect(transformer.cleanDataPoint('$100')).toBe('100')
67
+ expect(transformer.cleanDataPoint('$1,234')).toBe('1234')
68
+ })
69
+
70
+ it('removes percent signs', () => {
71
+ expect(transformer.cleanDataPoint('50%')).toBe('50')
72
+ expect(transformer.cleanDataPoint('99.9%')).toBe('99.9')
73
+ })
74
+
75
+ it('handles combined formatting', () => {
76
+ expect(transformer.cleanDataPoint('$1,234,567')).toBe('1234567')
77
+ })
78
+
79
+ it('handles null and empty values', () => {
80
+ expect(transformer.cleanDataPoint(null)).toBe('')
81
+ expect(transformer.cleanDataPoint('')).toBe('')
82
+ })
83
+
84
+ it('handles non-string values', () => {
85
+ expect(transformer.cleanDataPoint(123)).toBe(123)
86
+ expect(transformer.cleanDataPoint(0)).toBe(0)
87
+ })
88
+
89
+ it('returns plain numbers unchanged', () => {
90
+ expect(transformer.cleanDataPoint('123')).toBe('123')
91
+ expect(transformer.cleanDataPoint('45.67')).toBe('45.67')
92
+ })
93
+ })
94
+
95
+ describe('developerStandardize', () => {
96
+ it('returns empty array when data is falsy', () => {
97
+ expect(transformer.developerStandardize(null, {})).toEqual([])
98
+ expect(transformer.developerStandardize(undefined, {})).toEqual([])
99
+ })
100
+
101
+ it('returns undefined when description is missing', () => {
102
+ const data = [{ a: 1 }]
103
+ expect(transformer.developerStandardize(data, null)).toBeUndefined()
104
+ expect(transformer.developerStandardize(data, undefined)).toBeUndefined()
105
+ })
106
+
107
+ it('returns undefined when description is incomplete', () => {
108
+ const data = [{ a: 1 }]
109
+ expect(transformer.developerStandardize(data, {})).toBeUndefined()
110
+ expect(transformer.developerStandardize(data, { horizontal: true })).toBeUndefined()
111
+ expect(transformer.developerStandardize(data, { series: true })).toBeUndefined()
112
+ })
113
+
114
+ it('handles horizontal data without series', () => {
115
+ const data = [
116
+ { category: 'A', val1: 10, val2: 20 },
117
+ { category: 'B', val1: 30, val2: 40 }
118
+ ]
119
+ const description = { horizontal: true, series: false }
120
+ const result = transformer.developerStandardize(data, description)
121
+ expect(result).toBeDefined()
122
+ expect(Array.isArray(result)).toBe(true)
123
+ })
124
+ })
125
+ })