@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,121 @@
1
+ /**
2
+ * Utilities for working with COVE filters in embed contexts
3
+ */
4
+
5
+ export type FilterMetadata = {
6
+ label: string
7
+ key: string
8
+ setByQueryParameter?: string
9
+ values?: any[]
10
+ defaultValue?: string
11
+ active?: string
12
+ }
13
+
14
+ export type FilterState = {
15
+ value: string
16
+ hide: boolean
17
+ }
18
+
19
+ /**
20
+ * Extract filter metadata from a COVE config
21
+ * Handles both regular viz filters and dashboard shared filters
22
+ */
23
+ export function extractFilters(config: any): FilterMetadata[] {
24
+ if (!config) return []
25
+
26
+ // Try regular filters first (charts, maps, etc.)
27
+ if (config.filters && Array.isArray(config.filters) && config.filters.length > 0) {
28
+ return config.filters.map(filter => normalizeFilter(filter))
29
+ }
30
+
31
+ // Try dashboard shared filters
32
+ if (config.dashboard?.sharedFilters && Array.isArray(config.dashboard.sharedFilters)) {
33
+ return config.dashboard.sharedFilters.map(filter => normalizeFilter(filter))
34
+ }
35
+
36
+ return []
37
+ }
38
+
39
+ /**
40
+ * Normalize a filter object to consistent metadata format
41
+ *
42
+ * Different filter types (chart filters vs dashboard filters) may use different field names.
43
+ * This function provides fallbacks to handle these variations:
44
+ *
45
+ * - label: Priority for readability: label > setByQueryParameter > columnName
46
+ * - key: May be called key, columnName, or id depending on the viz type
47
+ * - setByQueryParameter: MUST be used exactly as provided. COVE only recognizes this specific
48
+ * field for URL parameters - there are no fallbacks. Filters without this field cannot be
49
+ * controlled via URL parameters.
50
+ */
51
+ function normalizeFilter(filter: any): FilterMetadata {
52
+ const normalized = {
53
+ label: filter.label || filter.setByQueryParameter || filter.columnName || 'Unnamed Filter',
54
+ key: filter.key || filter.columnName || String(filter.id) || '',
55
+ setByQueryParameter: filter.setByQueryParameter,
56
+ values: filter.values || [],
57
+ defaultValue: filter.defaultValue,
58
+ active: filter.active
59
+ }
60
+ return normalized
61
+ }
62
+
63
+ /**
64
+ * Get initial/default value for a filter
65
+ */
66
+ export function getDefaultFilterValue(filter: FilterMetadata): string {
67
+ if (filter.defaultValue) return filter.defaultValue
68
+ if (filter.active) return filter.active
69
+ if (filter.values && filter.values.length > 0) return filter.values[0]
70
+ return ''
71
+ }
72
+
73
+ /**
74
+ * Initialize filter state from filter metadata
75
+ */
76
+ export function initializeFilterState(filters: FilterMetadata[]): Record<string, FilterState> {
77
+ const state: Record<string, FilterState> = {}
78
+
79
+ filters.forEach(filter => {
80
+ state[filter.key] = {
81
+ value: getDefaultFilterValue(filter),
82
+ hide: false
83
+ }
84
+ })
85
+
86
+ return state
87
+ }
88
+
89
+ /**
90
+ * Build URL parameters from filter state
91
+ * Returns an object with URL parameters for both filter values and hide states
92
+ */
93
+ export function buildFilterUrlParams(
94
+ filters: FilterMetadata[],
95
+ filterState: Record<string, FilterState>
96
+ ): Record<string, string> {
97
+ const urlParams: Record<string, string> = {}
98
+
99
+ filters.forEach(filter => {
100
+ if (!filter.setByQueryParameter) {
101
+ return
102
+ }
103
+
104
+ const state = filterState[filter.key]
105
+ if (!state) {
106
+ return
107
+ }
108
+
109
+ // Add filter value
110
+ if (state.value) {
111
+ urlParams[filter.setByQueryParameter] = state.value
112
+ }
113
+
114
+ // Add hide parameter
115
+ if (state.hide) {
116
+ urlParams[`hide${filter.setByQueryParameter}`] = 'true'
117
+ }
118
+ })
119
+
120
+ return urlParams
121
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Embed-related helpers for COVE visualizations
3
+ * Used for generating embed codes and managing filter customization
4
+ */
5
+
6
+ export { generateEmbedCode, getEmbedPageUrl, getHelperScriptUrl, isDevMode } from './embedCodeGenerator'
7
+
8
+ export {
9
+ extractFilters,
10
+ getDefaultFilterValue,
11
+ initializeFilterState,
12
+ buildFilterUrlParams,
13
+ type FilterMetadata,
14
+ type FilterState
15
+ } from './filterUtils'
16
+
17
+ export { getConfigUrlParam, isValidConfigUrl, isValidMessageOrigin } from './urlValidation'
18
+
19
+ // Import embed helper to initialize iframe resizing
20
+ // This runs immediately when imported
21
+ import './embedHelper.js'
@@ -0,0 +1,119 @@
1
+ /**
2
+ * URL validation utilities for embed functionality
3
+ */
4
+
5
+ /**
6
+ * Gets and validates the configUrl parameter from the current URL
7
+ * Returns the validated configUrl if it's a valid relative URL, null otherwise
8
+ *
9
+ * @returns The validated configUrl or null if missing/invalid
10
+ *
11
+ * @example
12
+ * const configUrl = getConfigUrlParam()
13
+ * if (!configUrl) {
14
+ * // Show error - missing or invalid
15
+ * }
16
+ */
17
+ export function getConfigUrlParam(): string | null {
18
+ const params = new URLSearchParams(window.location.search)
19
+ const configUrl = params.get('configUrl')
20
+
21
+ if (!configUrl) {
22
+ return null
23
+ }
24
+
25
+ // Validate that it's a relative URL
26
+ if (!isValidConfigUrl(configUrl)) {
27
+ return null
28
+ }
29
+
30
+ return configUrl
31
+ }
32
+
33
+ /**
34
+ * Validates that a configUrl is a relative URL (no protocol or host)
35
+ *
36
+ * Only relative URLs are allowed to ensure configs
37
+ * are loaded from the same origin as the embed page.
38
+ *
39
+ * @param configUrl - The URL to validate
40
+ * @returns true if the URL is valid (relative only), false otherwise
41
+ *
42
+ * @example
43
+ * isValidConfigUrl('/path/to/config.json') // true
44
+ * isValidConfigUrl('../config.json') // true
45
+ * isValidConfigUrl('config.json') // true
46
+ * isValidConfigUrl('https://evil.com/config.json') // false
47
+ * isValidConfigUrl('//evil.com/config.json') // false
48
+ * isValidConfigUrl('http://localhost/config.json') // false
49
+ */
50
+ export function isValidConfigUrl(configUrl: string | null): boolean {
51
+ if (!configUrl || typeof configUrl !== 'string') {
52
+ return false
53
+ }
54
+
55
+ const trimmed = configUrl.trim()
56
+
57
+ if (trimmed.length === 0) {
58
+ return false
59
+ }
60
+
61
+ // Reject any URL that contains a protocol (http://, https://, ftp://, etc.)
62
+ if (trimmed.match(/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//)) {
63
+ return false
64
+ }
65
+
66
+ // Reject protocol-relative URLs (//example.com/path)
67
+ if (trimmed.startsWith('//')) {
68
+ return false
69
+ }
70
+
71
+ // Reject URLs with protocols but no slashes (javascript:, data:, etc.)
72
+ if (trimmed.match(/^[a-zA-Z][a-zA-Z0-9+.-]*:/)) {
73
+ return false
74
+ }
75
+
76
+ // Additional validation: Try to parse it as a URL relative to current origin
77
+ try {
78
+ const parsed = new URL(trimmed, window.location.origin)
79
+
80
+ // Verify it's same origin
81
+ if (parsed.origin !== window.location.origin) {
82
+ return false
83
+ }
84
+
85
+ return true
86
+ } catch (error) {
87
+ return false
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Validates a postMessage origin against allowed CDC domains
93
+ *
94
+ * @param origin - The origin to validate (from MessageEvent.origin)
95
+ * @returns true if the origin is allowed, false otherwise
96
+ */
97
+ export function isValidMessageOrigin(origin: string): boolean {
98
+ if (!origin || typeof origin !== 'string') {
99
+ return false
100
+ }
101
+
102
+ try {
103
+ const url = new URL(origin)
104
+
105
+ // Allow localhost for development (HTTP only)
106
+ if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') {
107
+ return url.protocol === 'http:' || url.protocol === 'https:'
108
+ }
109
+
110
+ // Allow cdc.gov and any subdomain (HTTPS only)
111
+ if (url.hostname === 'cdc.gov' || url.hostname.endsWith('.cdc.gov')) {
112
+ return url.protocol === 'https:'
113
+ }
114
+
115
+ return false
116
+ } catch (error) {
117
+ return false
118
+ }
119
+ }
@@ -16,10 +16,15 @@ export const filterVizData = (filters: Filter[], data) => {
16
16
  return []
17
17
  }
18
18
 
19
+ if (!Array.isArray(data)) {
20
+ console.warn('COVE: Data is not an array, received:', typeof data)
21
+ return []
22
+ }
23
+
19
24
  if (!filters) return data
20
25
  const filteredData: any[] = []
21
26
 
22
- data?.forEach(row => {
27
+ data.forEach(row => {
23
28
  let add = true
24
29
  filters
25
30
  .filter(filter => filter.type !== 'url')
@@ -1,9 +1,3 @@
1
- // export const getFileExtensionx = (path: string): string => {
2
- // const regex = /(?:\.([^.]+))?$/
3
- // const outCome: RegExpExecArray | null = regex.exec(path)
4
- // return outCome ? outCome[1] : ''
5
- // }
6
-
7
1
  export const getFileExtension = (url: string): string => {
8
2
  const regexForExtension = /(?:\.([^.]+))$/
9
3
  const regexForQueryParam = /[?&]wt=(csv|json)(?:&|$)/ // Regular expression for 'wt' query parameter
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Get unique values from a column in a dataset
3
+ * @returns {Array} - The unique values
4
+ */
5
+ export const getUniqueValues = (data: Array<Record<string, any>>, columnName: string) => {
6
+ let result = {}
7
+
8
+ for (let i = 0; i < data.length; i++) {
9
+ let val = data[i][columnName]
10
+
11
+ if (undefined === val) continue
12
+
13
+ if (undefined === result[val]) {
14
+ result[val] = true
15
+ }
16
+ }
17
+
18
+ return Object.keys(result)
19
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Hashes an object
3
+ * @param {Object} row - The object to hash
4
+ * @returns {number} - The hash of the object
5
+ */
6
+ export const hashObj = row => {
7
+ try {
8
+ if (!row || row === undefined) return null
9
+
10
+ let str = JSON.stringify(row)
11
+ let hash = 0
12
+
13
+ if (str.length === 0) return hash
14
+
15
+ for (let i = 0; i < str.length; i++) {
16
+ let char = str.charCodeAt(i)
17
+ hash = (hash << 5) - hash + char
18
+ hash = hash & hash
19
+ }
20
+
21
+ return hash
22
+ } catch (e) {
23
+ console.error({ state: 'COVE: ' + e.message }) // eslint-disable-line
24
+ }
25
+ }
@@ -12,6 +12,11 @@ export default function isRightAlignedTableValue(value = '') {
12
12
  if (/^\d{4}-\d{1,2}-\d{1,2}$/.test(value)) {
13
13
  return false
14
14
  }
15
+ // Years like 1995 and 2014 are not considered numbers
16
+ if (/^(19|20)\d{2}$/.test(value)) {
17
+ return false
18
+ }
19
+
15
20
  return numericStrings.includes(value) || /^[\$\d\.\%\,\-\s\(\)CI<>]*$/.test(value)
16
21
  }
17
22
  return false
@@ -68,6 +68,7 @@ export const publishAnalyticsEvent = <T extends ANALYTICS_EVENT_TYPES>({
68
68
  // Format: APP|VIZTYPE_VIZSUBTYPE|VIZ_TITLE|INTERACTION_EVENT_NAME|INTERACTION_TYPE|SPECIFICS
69
69
  const vizTypeSubType = vizSubType ? `${vizType}_${vizSubType}` : vizType
70
70
  const formattedEvent = `${app}|${vizTypeSubType}|${vizTitle || 'unknown'}|${eventType}|${eventAction}|${specifics || 'no details'}`
71
+
71
72
  return publish('cove:analytics', {
72
73
  formattedEvent,
73
74
  eventLabel
@@ -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
 
@@ -1,6 +1,6 @@
1
1
  import _ from 'lodash'
2
2
 
3
- const getColumns = (data: Record<string, any>[], columnName: string, pivot: string[], excludeColumns: string[]) => {
3
+ const getNonPivotColumns = (data: Record<string, any>[], columnName: string, pivot: string[], excludeColumns: string[]) => {
4
4
  const excludedColumns = [columnName, ...pivot, ...excludeColumns]
5
5
  return _.uniq(data.flatMap(row => Object.keys(row))).filter(col => !excludedColumns.includes(col))
6
6
  }
@@ -13,7 +13,7 @@ export const pivotData = (
13
13
  pivot: string[],
14
14
  excludeColumns: string[]
15
15
  ): Record<string, any>[] => {
16
- const columns = getColumns(data, columnName, pivot, excludeColumns)
16
+ const columns = getNonPivotColumns(data, columnName, pivot, excludeColumns)
17
17
  const newColumns = data.reduce((acc, row) => {
18
18
  acc[row[columnName]] = ''
19
19
  return acc