@cdc/core 4.25.8 → 4.25.10

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 (117) hide show
  1. package/components/AdvancedEditor/AdvancedEditor.tsx +29 -8
  2. package/components/DataTable/DataTable.tsx +56 -38
  3. package/components/DataTable/components/ChartHeader.tsx +44 -14
  4. package/components/DataTable/components/ExpandCollapse.tsx +10 -1
  5. package/components/DataTable/components/MapHeader.tsx +24 -13
  6. package/components/DataTable/data-table.css +6 -0
  7. package/components/DataTable/helpers/chartCellMatrix.tsx +11 -8
  8. package/components/DataTable/helpers/mapCellMatrix.tsx +19 -1
  9. package/components/DownloadButton.tsx +40 -14
  10. package/components/EditorPanel/components/MarkupHighlightedTextField.tsx +227 -0
  11. package/components/EditorPanel/components/MarkupVariablesEditor.tsx +411 -0
  12. package/components/EditorPanel/components/PanelMarkup.tsx +59 -0
  13. package/components/ErrorBoundary.jsx +3 -1
  14. package/components/Filters/Filters.tsx +27 -20
  15. package/components/Filters/components/Tabs.tsx +1 -0
  16. package/components/Legend/Legend.Gradient.tsx +3 -6
  17. package/components/LegendShape.tsx +121 -3
  18. package/components/MediaControls.tsx +51 -3
  19. package/components/PaletteConversionModal.tsx +87 -0
  20. package/components/PaletteSelector/DeveloperPaletteRollback.tsx +114 -0
  21. package/components/PaletteSelector/PaletteSelector.css +51 -0
  22. package/components/PaletteSelector/PaletteSelector.tsx +112 -0
  23. package/components/PaletteSelector/index.ts +2 -0
  24. package/components/RichTooltip/RichTooltip.tsx +1 -0
  25. package/components/Table/Table.tsx +3 -1
  26. package/components/_stories/BlurStrokeTest.stories.tsx +1 -1
  27. package/components/_stories/DataTable.stories.tsx +1 -1
  28. package/components/_stories/Filters.stories.tsx +1 -1
  29. package/components/_stories/Footnotes.stories.tsx +1 -1
  30. package/components/_stories/Inputs.stories.tsx +1 -1
  31. package/components/_stories/MultiSelect.stories.tsx +3 -3
  32. package/components/_stories/NestedDropdown.stories.tsx +1 -1
  33. package/components/_stories/Table.stories.tsx +1 -1
  34. package/components/elements/_stories/Button.stories.tsx +1 -1
  35. package/components/elements/_stories/Card.stories.tsx +1 -1
  36. package/components/inputs/InputToggle.tsx +2 -0
  37. package/components/managers/DataDesigner.tsx +10 -9
  38. package/components/managers/_stories/DataDesigner.stories.tsx +1 -1
  39. package/components/ui/Tooltip.tsx +2 -1
  40. package/components/ui/_stories/Accordion.stories.tsx +1 -1
  41. package/components/ui/_stories/ColorPaletteMigration.stories.mdx +275 -0
  42. package/components/ui/_stories/Colors.stories.tsx +330 -0
  43. package/components/ui/_stories/IconGallery.stories.tsx +316 -0
  44. package/components/ui/_stories/Title.stories.tsx +1 -1
  45. package/contexts/EditorContext.ts +18 -0
  46. package/contexts/editor.actions.ts +28 -0
  47. package/contexts/editor.reducer.ts +94 -0
  48. package/data/chartColorPalettes.ts +118 -0
  49. package/data/colorPalettes.ts +9 -0
  50. package/data/mapColorPalettes.ts +45 -0
  51. package/data/sharedPalettes.ts +50 -0
  52. package/dist/cove-main.css +14 -11
  53. package/dist/cove-main.css.map +1 -1
  54. package/generateViteConfig.js +80 -0
  55. package/helpers/addValuesToFilters.ts +2 -3
  56. package/helpers/cloneConfig.ts +31 -0
  57. package/helpers/configDataHelpers.ts +128 -0
  58. package/helpers/configHelpers.ts +27 -0
  59. package/helpers/constants.ts +5 -2
  60. package/helpers/coveUpdateWorker.ts +13 -3
  61. package/helpers/filterColorPalettes.ts +152 -0
  62. package/helpers/generateColorsArray.ts +13 -0
  63. package/helpers/getColorPaletteVersion.ts +33 -0
  64. package/helpers/getPaletteAccessor.ts +18 -0
  65. package/helpers/markupProcessor.ts +205 -0
  66. package/helpers/metrics/helpers.ts +42 -19
  67. package/helpers/metrics/types.ts +48 -9
  68. package/helpers/metrics/utils.ts +34 -0
  69. package/helpers/palettes/colorDistributions.ts +56 -0
  70. package/helpers/palettes/migratePaletteName.ts +150 -0
  71. package/helpers/palettes/standardizePaletteNames.ts +77 -0
  72. package/helpers/palettes/utils.ts +267 -0
  73. package/helpers/queryStringUtils.ts +13 -0
  74. package/helpers/testing.ts +345 -0
  75. package/helpers/tests/addValuesToFilters.test.ts +1 -2
  76. package/helpers/tests/generateColorsArray.test.ts +24 -0
  77. package/helpers/tests/markupProcessor.test.ts +538 -0
  78. package/helpers/tests/testStandaloneBuild.ts +44 -0
  79. package/helpers/useMarkupVariables.ts +31 -0
  80. package/helpers/vegaConfig.ts +0 -1
  81. package/helpers/ver/4.24.10.ts +2 -1
  82. package/helpers/ver/4.24.11.ts +2 -1
  83. package/helpers/ver/4.24.3.ts +2 -1
  84. package/helpers/ver/4.24.4.ts +2 -1
  85. package/helpers/ver/4.24.5.ts +2 -1
  86. package/helpers/ver/4.24.7.ts +2 -1
  87. package/helpers/ver/4.24.9.ts +2 -1
  88. package/helpers/ver/4.25.1.ts +2 -1
  89. package/helpers/ver/4.25.10.ts +36 -0
  90. package/helpers/ver/4.25.3.ts +2 -1
  91. package/helpers/ver/4.25.4.ts +2 -1
  92. package/helpers/ver/4.25.6.ts +2 -1
  93. package/helpers/ver/4.25.7.ts +2 -1
  94. package/helpers/ver/4.25.8.ts +2 -1
  95. package/helpers/ver/4.25.9.ts +293 -0
  96. package/helpers/ver/tests/4.25.10.test.ts +204 -0
  97. package/helpers/ver/tests/4.25.8.test.ts +1 -1
  98. package/helpers/ver/tests/4.25.9.test.ts +51 -0
  99. package/hooks/useColorPalette.ts +79 -0
  100. package/package.json +12 -4
  101. package/styles/_global.scss +7 -5
  102. package/styles/base.scss +8 -5
  103. package/styles/v2/components/button.scss +4 -3
  104. package/styles/v2/components/editor.scss +2 -1
  105. package/styles/v2/layout/_data-table.scss +3 -2
  106. package/styles/v2/themes/_color-definitions.scss +18 -17
  107. package/testBuild.js +0 -0
  108. package/testing-setup.js +32 -0
  109. package/types/MarkupInclude.ts +6 -1
  110. package/types/MarkupVariable.ts +19 -0
  111. package/types/VizFilter.ts +1 -0
  112. package/vitest.config.ts +16 -0
  113. package/components/ui/_stories/Colors.stories.mdx +0 -220
  114. package/components/ui/_stories/IconGallery.stories.mdx +0 -14
  115. package/data/colorPalettes.js +0 -171
  116. package/helpers/formatConfigBeforeSave.ts +0 -135
  117. package/helpers/tests/formatConfigBeforeSave.test.ts +0 -68
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Helper functions for stripping and restoring data arrays from config objects
3
+ * to improve performance during cloning operations.
4
+ */
5
+
6
+ /**
7
+ * Strips data arrays from config to improve cloning performance
8
+ */
9
+ export const stripDataFromConfig = (config: any): { strippedConfig: any; extractedData: any } => {
10
+ const extractedData: any = {}
11
+ const strippedConfig = { ...config }
12
+
13
+ // Extract root-level data arrays
14
+ if (strippedConfig.data) {
15
+ extractedData.data = strippedConfig.data
16
+ delete strippedConfig.data
17
+ }
18
+ if (strippedConfig.formattedData) {
19
+ extractedData.formattedData = strippedConfig.formattedData
20
+ delete strippedConfig.formattedData
21
+ }
22
+ if (strippedConfig.originalFormattedData) {
23
+ extractedData.originalFormattedData = strippedConfig.originalFormattedData
24
+ delete strippedConfig.originalFormattedData
25
+ }
26
+ if (strippedConfig.datasets) {
27
+ extractedData.datasets = strippedConfig.datasets
28
+ delete strippedConfig.datasets
29
+ }
30
+
31
+ // Handle dashboard visualizations
32
+ if (strippedConfig.visualizations) {
33
+ extractedData.visualizations = {}
34
+ Object.keys(strippedConfig.visualizations).forEach(vizKey => {
35
+ const viz = strippedConfig.visualizations[vizKey]
36
+ const vizData: any = {}
37
+
38
+ if (!viz.type) return
39
+
40
+ if (viz.data) {
41
+ vizData.data = viz.data
42
+ delete strippedConfig.visualizations[vizKey].data
43
+ }
44
+ if (viz.formattedData) {
45
+ vizData.formattedData = viz.formattedData
46
+ delete strippedConfig.visualizations[vizKey].formattedData
47
+ }
48
+ if (viz.originalFormattedData) {
49
+ vizData.originalFormattedData = viz.originalFormattedData
50
+ delete strippedConfig.visualizations[vizKey].originalFormattedData
51
+ }
52
+ if (viz.datasets) {
53
+ vizData.datasets = viz.datasets
54
+ delete strippedConfig.visualizations[vizKey].datasets
55
+ }
56
+
57
+ if (Object.keys(vizData).length > 0) {
58
+ extractedData.visualizations[vizKey] = vizData
59
+ }
60
+ })
61
+ }
62
+
63
+ // Handle multiDashboards
64
+ if (strippedConfig.multiDashboards) {
65
+ extractedData.multiDashboards = []
66
+ strippedConfig.multiDashboards.forEach((dashboard, index) => {
67
+ const { strippedConfig: strippedDashboard, extractedData: dashboardData } = stripDataFromConfig(dashboard)
68
+ strippedConfig.multiDashboards[index] = strippedDashboard
69
+ extractedData.multiDashboards[index] = dashboardData
70
+ })
71
+ }
72
+
73
+ return { strippedConfig, extractedData }
74
+ }
75
+
76
+ /**
77
+ * Restores data arrays back to config after updates are complete
78
+ */
79
+ export const restoreDataToConfig = (config: any, extractedData: any): any => {
80
+ const restoredConfig = { ...config }
81
+
82
+ // Restore root-level data arrays
83
+ if (extractedData.data) {
84
+ restoredConfig.data = extractedData.data
85
+ }
86
+ if (extractedData.formattedData) {
87
+ restoredConfig.formattedData = extractedData.formattedData
88
+ }
89
+ if (extractedData.originalFormattedData) {
90
+ restoredConfig.originalFormattedData = extractedData.originalFormattedData
91
+ }
92
+ if (extractedData.datasets) {
93
+ restoredConfig.datasets = extractedData.datasets
94
+ }
95
+
96
+ // Restore dashboard visualizations data
97
+ if (extractedData.visualizations && restoredConfig.visualizations) {
98
+ Object.keys(extractedData.visualizations).forEach(vizKey => {
99
+ const vizData = extractedData.visualizations[vizKey]
100
+ if (vizData.data) {
101
+ restoredConfig.visualizations[vizKey].data = vizData.data
102
+ }
103
+ if (vizData.formattedData) {
104
+ restoredConfig.visualizations[vizKey].formattedData = vizData.formattedData
105
+ }
106
+ if (vizData.originalFormattedData) {
107
+ restoredConfig.visualizations[vizKey].originalFormattedData = vizData.originalFormattedData
108
+ }
109
+ if (vizData.datasets) {
110
+ restoredConfig.visualizations[vizKey].datasets = vizData.datasets
111
+ }
112
+ })
113
+ }
114
+
115
+ // Restore multiDashboards data
116
+ if (extractedData.multiDashboards && restoredConfig.multiDashboards) {
117
+ extractedData.multiDashboards.forEach((dashboardData, index) => {
118
+ if (dashboardData && Object.keys(dashboardData).length > 0) {
119
+ restoredConfig.multiDashboards[index] = restoreDataToConfig(
120
+ restoredConfig.multiDashboards[index],
121
+ dashboardData
122
+ )
123
+ }
124
+ })
125
+ }
126
+
127
+ return restoredConfig
128
+ }
@@ -0,0 +1,27 @@
1
+ import cloneConfig from './cloneConfig'
2
+
3
+ /* editConfigKeys
4
+ * Add edit or update config keys
5
+ * keyUpdates: { path: string[], value: any }[]
6
+ * path is the array of keys needed to reach the value to be updated
7
+ * value is the new value to be set
8
+ * if the key does not exist, it will be created
9
+ */
10
+ export function editConfigKeys<T = any>(config: T, keyUpdates: { path: string[]; value: any }[]): T {
11
+ const configDeepCopy = cloneConfig(config)
12
+
13
+ const newConfig = keyUpdates.reduce((acc, { path, value }) => {
14
+ const pathCopy = [...path]
15
+ const lastKey = pathCopy.pop()
16
+ const target = pathCopy.reduce((target, key) => {
17
+ if (!target[key]) {
18
+ target[key] = {}
19
+ }
20
+ return target[key]
21
+ }, acc)
22
+ target[lastKey] = value
23
+ return acc
24
+ }, configDeepCopy)
25
+
26
+ return newConfig
27
+ }
@@ -1,6 +1,9 @@
1
1
  export const APP_FONT_SIZE = 18
2
-
3
2
  export const COOL_GRAY_90 = getComputedStyle(document.documentElement).getPropertyValue('--cool-gray-90').trim()
4
3
  export const APP_FONT_COLOR = COOL_GRAY_90
5
-
4
+ export const FALLBACK_COLOR_PALETTE_V1 = 'qualitative-bold'
5
+ export const FALLBACK_COLOR_PALETTE_V2 = 'sequential_blue'
6
6
  export const EDITOR_WIDTH = 350
7
+
8
+ // Palette migration behavior flag
9
+ export const USE_V2_MIGRATION = true // Set to true to enable v2 migration and conversion modal
@@ -17,9 +17,15 @@ import update_4_25_4 from './ver/4.25.4'
17
17
  import update_4_25_6 from './ver/4.25.6'
18
18
  import update_4_25_7 from './ver/4.25.7'
19
19
  import update_4_25_8 from './ver/4.25.8'
20
+ import update_4_25_9 from './ver/4.25.9'
21
+ import update_4_25_10 from './ver/4.25.10'
22
+
23
+ import { stripDataFromConfig, restoreDataToConfig } from './configDataHelpers'
20
24
 
21
25
  export const coveUpdateWorker = (config, multiDashboardVersion?) => {
22
- let genConfig = config
26
+ // Strip data from config for performance
27
+ const { strippedConfig, extractedData } = stripDataFromConfig(config)
28
+ let genConfig = strippedConfig
23
29
 
24
30
  if (multiDashboardVersion) genConfig.version = multiDashboardVersion
25
31
 
@@ -36,7 +42,9 @@ export const coveUpdateWorker = (config, multiDashboardVersion?) => {
36
42
  ['4.25.4', update_4_25_4],
37
43
  ['4.25.6', update_4_25_6],
38
44
  ['4.25.7', update_4_25_7],
39
- ['4.25.8', update_4_25_8]
45
+ ['4.25.8', update_4_25_8],
46
+ ['4.25.9', update_4_25_9],
47
+ ['4.25.10', update_4_25_10]
40
48
  ]
41
49
 
42
50
  versions.forEach(([version, updateFunction, alwaysRun]: [string, UpdateFunction, boolean?]) => {
@@ -54,7 +62,9 @@ export const coveUpdateWorker = (config, multiDashboardVersion?) => {
54
62
 
55
63
  // config version is stored at the root level of the config.
56
64
  if (multiDashboardVersion) delete genConfig.version
57
- return genConfig
65
+
66
+ // Restore data arrays after all updates are complete
67
+ return restoreDataToConfig(genConfig, extractedData)
58
68
  }
59
69
 
60
70
  export default coveUpdateWorker
@@ -0,0 +1,152 @@
1
+ import { getColorPaletteVersion } from './getColorPaletteVersion'
2
+ import { chartColorPalettes, twoColorPalette } from '../data/colorPalettes'
3
+
4
+ export interface FilterColorPalettesOptions {
5
+ config: any
6
+ isReversed?: boolean
7
+ colorPalettes?: any
8
+ visualizationType?: string
9
+ useV2Migration?: boolean
10
+ }
11
+
12
+ export interface FilteredPalettes {
13
+ sequential: string[]
14
+ nonSequential: string[]
15
+ accessibleColors: string[]
16
+ twoColorPalettes?: string[]
17
+ }
18
+
19
+ /**
20
+ * Universal color palette filtering function that works across all visualization types
21
+ * Combines chart and map palette filtering logic with backwards compatibility
22
+ */
23
+ export const filterColorPalettes = ({
24
+ config,
25
+ isReversed,
26
+ colorPalettes,
27
+ visualizationType,
28
+ useV2Migration
29
+ }: FilterColorPalettesOptions): FilteredPalettes => {
30
+ // Use provided colorPalettes or fall back to chart palettes
31
+ const palettes = colorPalettes || chartColorPalettes
32
+ const version = getColorPaletteVersion(config, useV2Migration)
33
+ const versionKey = `v${version}`
34
+ const currentPalettes = palettes[versionKey] || palettes.v2
35
+
36
+ // Handle two-color palettes for specific chart types
37
+ if (visualizationType === 'Paired Bar' || visualizationType === 'Deviation Bar') {
38
+ const twoColorPalettes = filterTwoColorPalettes(version, isReversed)
39
+ return {
40
+ sequential: [],
41
+ nonSequential: [],
42
+ accessibleColors: [],
43
+ twoColorPalettes
44
+ }
45
+ }
46
+
47
+ // Handle regular palette filtering
48
+ const isReversedFromConfig = isReversed !== undefined
49
+ ? isReversed
50
+ : config.general?.palette?.isReversed
51
+
52
+ return filterRegularPalettes(currentPalettes, version, isReversedFromConfig)
53
+ }
54
+
55
+ /**
56
+ * Filter two-color palettes (for Paired Bar and Deviation Bar charts)
57
+ */
58
+ function filterTwoColorPalettes(version: number, isReversed?: boolean): string[] {
59
+ // Use the version to get the correct two-color palettes
60
+ const versionKey = `v${version}`
61
+ const versionedTwoColorPalettes = twoColorPalette[versionKey] || twoColorPalette.v2
62
+
63
+ return Object.keys(versionedTwoColorPalettes).filter(name =>
64
+ isReversed ? name.endsWith('reverse') : !name.endsWith('reverse')
65
+ )
66
+ }
67
+
68
+ /**
69
+ * Filter regular palettes (sequential, non-sequential, accessible)
70
+ */
71
+ function filterRegularPalettes(palettes: any, version: number, isReversed?: boolean): FilteredPalettes {
72
+ const sequential: string[] = []
73
+ const nonSequential: string[] = []
74
+ const accessibleColors: string[] = []
75
+
76
+ for (const paletteName in palettes) {
77
+ const isPaletteReversed = paletteName.endsWith('reverse')
78
+ const matchesReversed = (!isReversed && !isPaletteReversed) || (isReversed && isPaletteReversed)
79
+
80
+ if (version === 1) {
81
+ filterV1Palette(paletteName, matchesReversed, sequential, nonSequential, accessibleColors)
82
+ } else {
83
+ filterV2Palette(paletteName, matchesReversed, sequential, nonSequential, accessibleColors)
84
+ }
85
+ }
86
+
87
+ return { sequential, nonSequential, accessibleColors }
88
+ }
89
+
90
+ /**
91
+ * Filter V1 palettes using original chart logic
92
+ */
93
+ function filterV1Palette(
94
+ paletteName: string,
95
+ matchesReversed: boolean,
96
+ sequential: string[],
97
+ nonSequential: string[],
98
+ accessibleColors: string[]
99
+ ): void {
100
+ if (!matchesReversed) return
101
+
102
+ if (paletteName.startsWith('sequential')) {
103
+ sequential.push(paletteName)
104
+ } else if (paletteName.startsWith('qualitative') && !paletteName.startsWith('colorblindsafe')) {
105
+ nonSequential.push(paletteName)
106
+ } else if (paletteName.startsWith('colorblindsafe') || paletteName.includes('colorblindsafe')) {
107
+ accessibleColors.push(paletteName)
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Filter V2 palettes using updated logic for new palette structure
113
+ */
114
+ function filterV2Palette(
115
+ paletteName: string,
116
+ matchesReversed: boolean,
117
+ sequential: string[],
118
+ nonSequential: string[],
119
+ accessibleColors: string[]
120
+ ): void {
121
+ if (!matchesReversed) return
122
+
123
+ if (paletteName.startsWith('sequential')) {
124
+ sequential.push(paletteName)
125
+ } else if (paletteName.startsWith('divergent')) {
126
+ nonSequential.push(paletteName)
127
+ } else if (paletteName.includes('colorblindsafe') || paletteName.startsWith('qualitative-standard')) {
128
+ accessibleColors.push(paletteName)
129
+ } else if (paletteName.startsWith('qualitative') && !paletteName.includes('colorblindsafe')) {
130
+ // V2 qualitative palettes go to accessible colors if they're standard
131
+ if (paletteName.includes('standard')) {
132
+ accessibleColors.push(paletteName)
133
+ }
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Legacy function for backwards compatibility with chart package
139
+ */
140
+ export const filterChartColorPalettes = (config: any) => {
141
+ const version = getColorPaletteVersion(config)
142
+
143
+ if (version === 1) {
144
+ return chartColorPalettes.v1
145
+ }
146
+
147
+ if (version === 2) {
148
+ return chartColorPalettes.v2
149
+ }
150
+
151
+ return chartColorPalettes.v2
152
+ }
@@ -0,0 +1,13 @@
1
+ import chroma from 'chroma-js'
2
+
3
+ /**
4
+ * Generate an array of colors based on a given color [color, hoverColor, darkColor]
5
+ * @param {string} color - The base color to generate the array from (defaults to black)
6
+ * @param {boolean} special - A flag to determine if the hover color should be brighter or saturated
7
+ * @returns {string[]} - An array of colors [baseColor, hoverColor, darkerColor]
8
+ */
9
+ export const generateColorsArray = (color: string = '#000000', special: boolean = false): string[] => {
10
+ const colorObj = chroma(color)
11
+ const hoverColor = special ? colorObj.brighten(0.5).hex() : colorObj.saturate(1.3).hex()
12
+ return [color, hoverColor, colorObj.darken(0.3).hex()]
13
+ }
@@ -0,0 +1,33 @@
1
+ import { USE_V2_MIGRATION } from './constants'
2
+
3
+ /**
4
+ * Gets the color palette version from a visualization config
5
+ * @param config - The visualization config object
6
+ * @param useV2Migration - If provided, overrides the global flag
7
+ * @returns The major version number
8
+ */
9
+ export const getColorPaletteVersion = (config: any, useV2Migration?: boolean): number => {
10
+ // Use passed parameter or fall back to global flag
11
+ const shouldUseV2 = useV2Migration !== undefined ? useV2Migration : USE_V2_MIGRATION
12
+
13
+ // If not using v2 migration, force v1 unless explicitly set to v2
14
+ if (!shouldUseV2) {
15
+ if (config.general?.palette?.version) {
16
+ return parseInt(config.general.palette.version.split('.')[0])
17
+ }
18
+ return 1
19
+ }
20
+
21
+ // V2 migration logic - If general.palette exists, it's either migrated or new
22
+ if (config.general?.palette) {
23
+ // If version is explicitly set, use it
24
+ if (config.general.palette.version) {
25
+ return parseInt(config.general.palette.version.split('.')[0])
26
+ }
27
+ // If no version but palette exists, it's likely migrated → use v2
28
+ return 2
29
+ }
30
+
31
+ // If no general.palette at all, it's legacy → use v1
32
+ return 1
33
+ }
@@ -0,0 +1,18 @@
1
+ import { getColorPaletteVersion } from './getColorPaletteVersion'
2
+
3
+ /**
4
+ * Gets the appropriate palette accessor based on config version
5
+ * @param colorPalettes - The color palettes object (e.g., mapColorPalettes, chartColorPalettes)
6
+ * @param config - The visualization config object
7
+ * @param paletteName - Optional palette name to get specific palette
8
+ * @returns The versioned palette accessor or fallback to main palettes, optionally filtered to specific palette
9
+ */
10
+ export const getPaletteAccessor = (colorPalettes: any, config: any, paletteName?: string) => {
11
+ const paletteAccessor = colorPalettes?.[`v${getColorPaletteVersion(config)}`] || colorPalettes
12
+
13
+ if (paletteName && paletteAccessor) {
14
+ return paletteAccessor[paletteName]
15
+ }
16
+
17
+ return paletteAccessor
18
+ }
@@ -0,0 +1,205 @@
1
+ import _ from 'lodash'
2
+ import { MarkupVariable, MarkupCondition } from '../types/MarkupVariable'
3
+ import { VizFilter } from '../types/VizFilter'
4
+ import { filterVizData } from './filterVizData'
5
+
6
+ /**
7
+ * Replaces {{variable}} tags in content with actual data values.
8
+ *
9
+ * @param content - Content string with markup variables
10
+ * @param data - Dataset to extract values from
11
+ * @param markupVariables - Variable configurations
12
+ * @param options - isEditor, showNoDataMessage, allowHideSection, filters
13
+ * @returns Processed content and state flags
14
+ *
15
+ * @security Returns plain text - must be parsed with html-react-parser before rendering
16
+ */
17
+ export const processMarkupVariables = (
18
+ content: string,
19
+ data: any[] = [],
20
+ markupVariables: MarkupVariable[] = [],
21
+ options: {
22
+ isEditor?: boolean
23
+ showNoDataMessage?: boolean
24
+ allowHideSection?: boolean
25
+ filters?: VizFilter[]
26
+ } = {}
27
+ ): {
28
+ processedContent: string
29
+ shouldHideSection: boolean
30
+ shouldShowNoDataMessage: boolean
31
+ } => {
32
+ const { isEditor = false, showNoDataMessage = false, allowHideSection = false, filters = [] } = options
33
+
34
+ // Early return for invalid inputs
35
+ if (_.isEmpty(markupVariables) || !content) {
36
+ return {
37
+ processedContent: content || '',
38
+ shouldHideSection: false,
39
+ shouldShowNoDataMessage: false
40
+ }
41
+ }
42
+
43
+ // Apply filters to data if filters are present
44
+ let workingData = data
45
+ if (filters && filters.length > 0) {
46
+ workingData = filterVizData(filters, data)
47
+ }
48
+
49
+ try {
50
+ const emptyVariableChecker: boolean[] = []
51
+ const noDataMessageChecker: boolean[] = []
52
+
53
+ const variableRegexPattern = /{{(.*?)}}/g
54
+ const processedContent = content.replace(variableRegexPattern, variableTag => {
55
+ try {
56
+ if (emptyVariableChecker.length > 0) return variableTag
57
+
58
+ const workingVariable = markupVariables.find(variable => variable.tag === variableTag)
59
+ if (!workingVariable) return variableTag
60
+
61
+ // Validate that columnName exists
62
+ if (!workingVariable.columnName) {
63
+ console.warn(`Markup variable ${variableTag} has no columnName specified`)
64
+ return variableTag
65
+ }
66
+
67
+ // Filter data with error handling (apply conditions on top of already filtered data)
68
+ const conditionFilteredData =
69
+ workingVariable.conditions.length === 0
70
+ ? workingData
71
+ : filterDataByConditions(workingData, [...workingVariable.conditions])
72
+
73
+ // Extract values with error handling
74
+ const variableValues: string[] = _.uniq(
75
+ (conditionFilteredData || []).map(dataObject => {
76
+ try {
77
+ const dataObjectValue = dataObject[workingVariable.columnName]
78
+
79
+ // Handle undefined column
80
+ if (dataObjectValue === undefined && isEditor) {
81
+ console.warn(
82
+ `Column "${workingVariable.columnName}" not found in data for variable ${variableTag}`
83
+ )
84
+ }
85
+
86
+ return workingVariable.addCommas && !isNaN(parseFloat(dataObjectValue))
87
+ ? parseFloat(dataObjectValue).toLocaleString('en-US', { useGrouping: true })
88
+ : String(dataObjectValue || '')
89
+ } catch (error) {
90
+ console.error(`Error processing data value for ${variableTag}:`, error)
91
+ return ''
92
+ }
93
+ })
94
+ ).filter(value => value !== '') // Filter out empty values
95
+
96
+ const listConjunction = !isEditor ? 'and' : 'or'
97
+ const formattedValues = formatValuesList(variableValues, listConjunction)
98
+
99
+ const finalDisplay = formattedValues.join(', ')
100
+
101
+ if (showNoDataMessage && finalDisplay === '') {
102
+ noDataMessageChecker.push(true)
103
+ }
104
+
105
+ if (finalDisplay === '' && allowHideSection) {
106
+ emptyVariableChecker.push(true)
107
+ }
108
+
109
+ return finalDisplay
110
+ } catch (error) {
111
+ console.error(`Error processing markup variable ${variableTag}:`, error)
112
+ return variableTag // Return original tag on error
113
+ }
114
+ })
115
+
116
+ return {
117
+ processedContent,
118
+ shouldHideSection: allowHideSection && emptyVariableChecker.length > 0 && !isEditor,
119
+ shouldShowNoDataMessage: showNoDataMessage && noDataMessageChecker.length > 0 && !isEditor
120
+ }
121
+ } catch (error) {
122
+ console.error('Error in processMarkupVariables:', error)
123
+ // Return original content on error
124
+ return {
125
+ processedContent: content,
126
+ shouldHideSection: false,
127
+ shouldShowNoDataMessage: false
128
+ }
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Filters data based on multiple conditions
134
+ */
135
+ const filterDataByConditions = (data: any[], conditions: MarkupCondition[]): any[] => {
136
+ if (!conditions.length) return data
137
+
138
+ const [currentCondition, ...remainingConditions] = conditions
139
+ const { columnName, isOrIsNotEqualTo, value } = currentCondition
140
+
141
+ const filteredData = isOrIsNotEqualTo === 'is'
142
+ ? data.filter(dataObject => String(dataObject[columnName]) === value)
143
+ : data.filter(dataObject => String(dataObject[columnName]) !== value)
144
+
145
+ return remainingConditions.length === 0
146
+ ? filteredData
147
+ : filterDataByConditions(filteredData, remainingConditions)
148
+ }
149
+
150
+ /**
151
+ * Formats a list of values with proper conjunction
152
+ */
153
+ const formatValuesList = (values: string[], conjunction: string): string[] => {
154
+ if (values.length === 0) return values
155
+ if (values.length === 1) return values
156
+ if (values.length === 2) {
157
+ return [`${values[0]} ${conjunction} ${values[1]}`]
158
+ }
159
+
160
+ const formatted = [...values]
161
+ formatted[formatted.length - 1] = `${conjunction} ${formatted[formatted.length - 1]}`
162
+ return [formatted.join(', ')]
163
+ }
164
+
165
+ /**
166
+ * Validates markup variables configuration
167
+ */
168
+ export const validateMarkupVariables = (
169
+ markupVariables: MarkupVariable[],
170
+ data: any[]
171
+ ): string[] => {
172
+ const errors: string[] = []
173
+
174
+ if (!markupVariables || !Array.isArray(markupVariables)) {
175
+ return errors
176
+ }
177
+
178
+ const availableColumns = data.length > 0 ? Object.keys(data[0]) : []
179
+
180
+ markupVariables.forEach((variable, index) => {
181
+ if (!variable.tag || !variable.tag.match(/^{{.+}}$/)) {
182
+ errors.push(`Variable ${index + 1}: Tag must be in format {{tagName}}`)
183
+ }
184
+
185
+ if (!variable.columnName) {
186
+ errors.push(`Variable ${index + 1}: Column name is required`)
187
+ } else if (availableColumns.length > 0 && !availableColumns.includes(variable.columnName)) {
188
+ errors.push(`Variable ${index + 1}: Column "${variable.columnName}" not found in data`)
189
+ }
190
+
191
+ variable.conditions.forEach((condition, condIndex) => {
192
+ if (!condition.columnName) {
193
+ errors.push(`Variable ${index + 1}, Condition ${condIndex + 1}: Column name is required`)
194
+ } else if (availableColumns.length > 0 && !availableColumns.includes(condition.columnName)) {
195
+ errors.push(`Variable ${index + 1}, Condition ${condIndex + 1}: Column "${condition.columnName}" not found in data`)
196
+ }
197
+
198
+ if (!condition.value) {
199
+ errors.push(`Variable ${index + 1}, Condition ${condIndex + 1}: Value is required`)
200
+ }
201
+ })
202
+ })
203
+
204
+ return errors
205
+ }