@cdc/core 4.25.7 → 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 (128) hide show
  1. package/components/AdvancedEditor/AdvancedEditor.tsx +29 -8
  2. package/components/DataTable/DataTable.tsx +63 -11
  3. package/components/DataTable/DataTableStandAlone.tsx +4 -1
  4. package/components/DataTable/components/ChartHeader.tsx +58 -9
  5. package/components/DataTable/components/ExpandCollapse.tsx +21 -1
  6. package/components/DataTable/components/MapHeader.tsx +35 -7
  7. package/components/DataTable/data-table.css +6 -0
  8. package/components/DataTable/helpers/chartCellMatrix.tsx +11 -8
  9. package/components/DataTable/helpers/mapCellMatrix.tsx +19 -1
  10. package/components/DownloadButton.tsx +42 -13
  11. package/components/EditorPanel/DataTableEditor.tsx +10 -1
  12. package/components/EditorPanel/components/MarkupHighlightedTextField.tsx +227 -0
  13. package/components/EditorPanel/components/MarkupVariablesEditor.tsx +411 -0
  14. package/components/EditorPanel/components/PanelMarkup.tsx +59 -0
  15. package/components/ErrorBoundary.jsx +3 -1
  16. package/components/Filters/Filters.tsx +35 -11
  17. package/components/Filters/components/Tabs.tsx +1 -0
  18. package/components/Footnotes/FootnotesStandAlone.tsx +2 -1
  19. package/components/Legend/Legend.Gradient.tsx +3 -6
  20. package/components/LegendShape.tsx +121 -3
  21. package/components/{MediaControls.jsx → MediaControls.tsx} +80 -16
  22. package/components/PaletteConversionModal.tsx +87 -0
  23. package/components/PaletteSelector/DeveloperPaletteRollback.tsx +114 -0
  24. package/components/PaletteSelector/PaletteSelector.css +51 -0
  25. package/components/PaletteSelector/PaletteSelector.tsx +112 -0
  26. package/components/PaletteSelector/index.ts +2 -0
  27. package/components/RichTooltip/RichTooltip.tsx +1 -0
  28. package/components/Table/Table.tsx +3 -1
  29. package/components/_stories/BlurStrokeTest.stories.tsx +1 -1
  30. package/components/_stories/DataTable.stories.tsx +1 -1
  31. package/components/_stories/Filters.stories.tsx +1 -1
  32. package/components/_stories/Footnotes.stories.tsx +1 -1
  33. package/components/_stories/Inputs.stories.tsx +1 -1
  34. package/components/_stories/MultiSelect.stories.tsx +3 -3
  35. package/components/_stories/NestedDropdown.stories.tsx +1 -1
  36. package/components/_stories/Table.stories.tsx +1 -1
  37. package/components/elements/_stories/Button.stories.tsx +1 -1
  38. package/components/elements/_stories/Card.stories.tsx +1 -1
  39. package/components/inputs/InputToggle.tsx +2 -0
  40. package/components/managers/DataDesigner.tsx +10 -9
  41. package/components/managers/_stories/DataDesigner.stories.tsx +1 -1
  42. package/components/ui/Tooltip.tsx +2 -1
  43. package/components/ui/_stories/Accordion.stories.tsx +1 -1
  44. package/components/ui/_stories/ColorPaletteMigration.stories.mdx +275 -0
  45. package/components/ui/_stories/Colors.stories.tsx +330 -0
  46. package/components/ui/_stories/IconGallery.stories.tsx +316 -0
  47. package/components/ui/_stories/Title.stories.tsx +1 -1
  48. package/contexts/EditorContext.ts +18 -0
  49. package/contexts/editor.actions.ts +28 -0
  50. package/contexts/editor.reducer.ts +94 -0
  51. package/data/chartColorPalettes.ts +118 -0
  52. package/data/colorPalettes.ts +9 -0
  53. package/data/mapColorPalettes.ts +45 -0
  54. package/data/sharedPalettes.ts +50 -0
  55. package/dist/cove-main.css +14 -13
  56. package/dist/cove-main.css.map +1 -1
  57. package/generateViteConfig.js +80 -0
  58. package/helpers/addValuesToFilters.ts +2 -3
  59. package/helpers/cloneConfig.ts +31 -0
  60. package/helpers/configDataHelpers.ts +128 -0
  61. package/helpers/configHelpers.ts +27 -0
  62. package/helpers/constants.ts +5 -2
  63. package/helpers/cove/number.ts +6 -2
  64. package/helpers/coveUpdateWorker.ts +15 -3
  65. package/helpers/events.ts +32 -0
  66. package/helpers/filterColorPalettes.ts +152 -0
  67. package/helpers/generateColorsArray.ts +13 -0
  68. package/helpers/getColorPaletteVersion.ts +33 -0
  69. package/helpers/getPaletteAccessor.ts +18 -0
  70. package/helpers/markupProcessor.ts +205 -0
  71. package/helpers/metrics/helpers.ts +75 -0
  72. package/helpers/metrics/types.ts +82 -0
  73. package/helpers/metrics/utils.ts +34 -0
  74. package/helpers/palettes/colorDistributions.ts +56 -0
  75. package/helpers/palettes/migratePaletteName.ts +150 -0
  76. package/helpers/palettes/standardizePaletteNames.ts +77 -0
  77. package/helpers/palettes/utils.ts +267 -0
  78. package/helpers/queryStringUtils.ts +13 -0
  79. package/helpers/testing.ts +345 -0
  80. package/helpers/tests/addValuesToFilters.test.ts +1 -2
  81. package/helpers/tests/generateColorsArray.test.ts +24 -0
  82. package/helpers/tests/markupProcessor.test.ts +538 -0
  83. package/helpers/tests/testStandaloneBuild.ts +44 -0
  84. package/helpers/useMarkupVariables.ts +31 -0
  85. package/helpers/vegaConfig.ts +0 -1
  86. package/helpers/ver/4.24.10.ts +2 -1
  87. package/helpers/ver/4.24.11.ts +2 -1
  88. package/helpers/ver/4.24.3.ts +2 -1
  89. package/helpers/ver/4.24.4.ts +2 -1
  90. package/helpers/ver/4.24.5.ts +2 -1
  91. package/helpers/ver/4.24.7.ts +2 -1
  92. package/helpers/ver/4.24.9.ts +2 -1
  93. package/helpers/ver/4.25.1.ts +2 -1
  94. package/helpers/ver/4.25.10.ts +36 -0
  95. package/helpers/ver/4.25.3.ts +2 -1
  96. package/helpers/ver/4.25.4.ts +2 -1
  97. package/helpers/ver/4.25.6.ts +2 -1
  98. package/helpers/ver/4.25.7.ts +2 -1
  99. package/helpers/ver/4.25.8.ts +62 -0
  100. package/helpers/ver/4.25.9.ts +293 -0
  101. package/helpers/ver/tests/4.25.10.test.ts +204 -0
  102. package/helpers/ver/tests/4.25.8.test.ts +86 -0
  103. package/helpers/ver/tests/4.25.9.test.ts +51 -0
  104. package/helpers/viewports.ts +2 -0
  105. package/hooks/useColorPalette.ts +79 -0
  106. package/package.json +12 -4
  107. package/styles/_button-section.scss +0 -2
  108. package/styles/_global.scss +7 -5
  109. package/styles/base.scss +8 -5
  110. package/styles/v2/components/button.scss +4 -3
  111. package/styles/v2/components/editor.scss +2 -1
  112. package/styles/v2/layout/_data-table.scss +3 -2
  113. package/styles/v2/themes/_color-definitions.scss +18 -17
  114. package/testBuild.js +0 -0
  115. package/testing-setup.js +32 -0
  116. package/types/ForecastingSeriesKey.ts +0 -1
  117. package/types/MarkupInclude.ts +6 -1
  118. package/types/MarkupVariable.ts +19 -0
  119. package/types/Series.ts +4 -0
  120. package/types/Table.ts +1 -0
  121. package/types/VizFilter.ts +1 -0
  122. package/vitest.config.ts +16 -0
  123. package/components/ui/_stories/Colors.stories.mdx +0 -220
  124. package/components/ui/_stories/IconGallery.stories.mdx +0 -14
  125. package/data/colorPalettes.js +0 -171
  126. package/helpers/events.js +0 -14
  127. package/helpers/formatConfigBeforeSave.ts +0 -135
  128. package/helpers/tests/formatConfigBeforeSave.test.ts +0 -68
@@ -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
+ }
@@ -0,0 +1,75 @@
1
+ import { publish } from '../events'
2
+ import packageJson from '../../package.json'
3
+ import {
4
+ COVE_VISUALIZATION_TYPES,
5
+ ANALYTICS_EVENT_ACTIONS,
6
+ ANALYTICS_EVENT_TYPES,
7
+ EventSpecifics
8
+ } from './types'
9
+ import { GetLabelForEvent } from './getLabelForEvent'
10
+
11
+ /**
12
+ * Retrieves the version of the package from its package.json file.
13
+ * @returns {string} The version of the package.
14
+ */
15
+ export const getPackageVersion = () => {
16
+ try {
17
+ return packageJson.version
18
+ } catch (e) {
19
+ console.error('Error loading package version:', e)
20
+ return 'unknown'
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Publishes an analytics event with the specified parameters.
26
+ * Format: APP|VIZTYPE_VIZSUBTYPE|VIZ_TITLE|INTERACTION_EVENT_NAME|INTERACTION_TYPE|SPECIFICS
27
+ *
28
+ * @param {Object} params - The parameters for the analytics event
29
+ * @param {string} params.app - The application name (e.g., 'COVE')
30
+ * @param {COVE_VISUALIZATION_TYPES} params.vizType - The visualization type (e.g., 'map', 'chart')
31
+ * @param {string} [params.vizSubType] - The visualization subtype (e.g., 'county', 'state', 'bar', 'line')
32
+ * @param {string} [params.vizTitle] - The title of the visualization
33
+ * @param {ANALYTICS_EVENT_TYPES} params.eventType - The interaction event name
34
+ * @param {ANALYTICS_EVENT_ACTIONS} [params.eventAction='unknown'] - The interaction type (e.g., 'click', 'hover')
35
+ * @param {string} [params.eventLabel] - The event label (typically config URL or interaction label)
36
+ * @param {string} [params.specifics] - Additional specifics about the event (structured as "key: value, key2: value2")
37
+ * @param {string} [params.version] - The version of the package (defaults to package.json version)
38
+ * @returns {void}
39
+ * @description This function is used to publish analytics events for various user interactions and system states.
40
+ */
41
+ export const publishAnalyticsEvent = <T extends ANALYTICS_EVENT_TYPES>({
42
+ app = 'cove',
43
+ vizType,
44
+ vizSubType,
45
+ vizTitle,
46
+ eventType,
47
+ eventAction = 'unknown',
48
+ eventLabel,
49
+ specifics,
50
+ version,
51
+ ...additionalDetails
52
+ }: {
53
+ app?: string
54
+ vizType: COVE_VISUALIZATION_TYPES
55
+ vizSubType?: string
56
+ vizTitle?: string
57
+ eventType: T
58
+ eventAction?: ANALYTICS_EVENT_ACTIONS
59
+ eventLabel?: GetLabelForEvent<T>
60
+ specifics?: T extends keyof EventSpecifics ? EventSpecifics[T] : string
61
+ version?: string
62
+ [key: string]: any
63
+ }) => {
64
+ // Added check if we ever need to disable analytics
65
+ const ANALYTICS_POWERED_ON = true
66
+ if (!ANALYTICS_POWERED_ON) return
67
+
68
+ // Format: APP|VIZTYPE_VIZSUBTYPE|VIZ_TITLE|INTERACTION_EVENT_NAME|INTERACTION_TYPE|SPECIFICS
69
+ const vizTypeSubType = vizSubType ? `${vizType}_${vizSubType}` : vizType
70
+ const formattedEvent = `${app}|${vizTypeSubType}|${vizTitle || 'unknown'}|${eventType}|${eventAction}|${specifics || 'no details'}`
71
+ return publish('cove:analytics', {
72
+ formattedEvent,
73
+ eventLabel
74
+ })
75
+ }
@@ -0,0 +1,82 @@
1
+ export type COVE_VISUALIZATION_TYPES =
2
+ | 'map'
3
+ | 'chart'
4
+ | 'data-table'
5
+ | 'table'
6
+ | 'markup-include'
7
+ | 'waffle-chart'
8
+ | 'dashboard'
9
+ | 'filtered-text'
10
+ | 'table-filter'
11
+ | 'data-bite'
12
+ | 'navigation'
13
+ | 'unknown'
14
+
15
+ export type ANALYTICS_EVENT_ACTIONS =
16
+ | 'click'
17
+ | 'drag'
18
+ | 'toggle'
19
+ | 'none'
20
+ | 'keydown'
21
+ | 'keyboard'
22
+ | 'load'
23
+ | 'submit'
24
+ | 'change'
25
+ | 'hover'
26
+ | 'unknown'
27
+
28
+ export type LEGEND_TOGGLE_MODES = 'highlight' | 'isolate'
29
+
30
+ /**
31
+ * Type-safe specifics for different event types
32
+ * The specifics field should contain structured key-value pairs formatted as strings
33
+ * Format: "key1: value1, key2: value2"
34
+ */
35
+ export type EventSpecifics = {
36
+ // Map events
37
+ map_hover: `location: ${string}`
38
+ zoom_in: `zoom_level: ${number}` | `location: ${string}` | `zoom_level: ${number}, location: ${string}`
39
+ zoom_out: `zoom_level: ${number}` | `location: ${string}` | `zoom_level: ${number}, location: ${string}`
40
+
41
+ // Legend events
42
+ map_legend_item_toggled: `mode: ${LEGEND_TOGGLE_MODES}` | `mode: ${LEGEND_TOGGLE_MODES}, item: ${string}`
43
+ chart_legend_item_toggled: `mode: ${LEGEND_TOGGLE_MODES}` | `mode: ${LEGEND_TOGGLE_MODES}, item: ${string}`
44
+
45
+ // Table events
46
+ data_table_sort: `column: ${string}, order: ${'asc' | 'desc' | 'none'}`
47
+
48
+ // Filter events
49
+ dashboard_filter_changed: `key: ${string}, value: ${string}`
50
+
51
+ // Generic fallback for any event
52
+ [key: string]: string
53
+ }
54
+
55
+ export type ANALYTICS_EVENT_TYPES =
56
+ // Data actions
57
+ | 'data_downloaded'
58
+ | 'expand_collapse_toggled' // alternative name for data_table_toggled
59
+ | 'data_table_sort'
60
+ | 'data_viewed'
61
+ | 'clicked_data_link_to_view'
62
+ | 'link_to_data_table_click'
63
+
64
+ // Filter events
65
+ | `${COVE_VISUALIZATION_TYPES}_filter_reset`
66
+ | `${COVE_VISUALIZATION_TYPES}_filter_applied`
67
+ | `${COVE_VISUALIZATION_TYPES}_filter_changed`
68
+
69
+ // Legend events
70
+ | `${COVE_VISUALIZATION_TYPES}_legend_item_toggled` // simplified with specifics for mode
71
+ | `${COVE_VISUALIZATION_TYPES}_legend_reset`
72
+
73
+ // Map-specific events
74
+ | `${COVE_VISUALIZATION_TYPES}_hover` // simplified with specifics for location
75
+ | `${COVE_VISUALIZATION_TYPES}_panned`
76
+ | `${COVE_VISUALIZATION_TYPES}_reset_zoom_level`
77
+ | `zoom_in` // simplified with specifics for zoom level and location
78
+ | `zoom_out` // simplified with specifics for zoom level and location
79
+ | `${COVE_VISUALIZATION_TYPES}_navigation_menu`
80
+
81
+ // Image/export events
82
+ | 'image_download' // generic image download event
@@ -0,0 +1,34 @@
1
+ const getVizTitle = (config) => {
2
+ if (config?.type === 'dashboard') {
3
+ return String(config?.dashboard?.title).toLowerCase()
4
+ }
5
+ if (config?.title) {
6
+ return String(config.title).toLowerCase()
7
+ } else if (config?.general?.title) {
8
+ return String(config.general.title).toLowerCase()
9
+ } else {
10
+ return 'no title'
11
+ }
12
+ }
13
+
14
+ const getVizSubType = config => {
15
+ if (config?.type === 'markup-include') {
16
+ return `${config?.contentEditor?.title}`
17
+ }
18
+ if (config?.general?.geoType) {
19
+ return `${config.general.geoType}`
20
+ }
21
+
22
+ if (config?.type === 'chart' && config?.visualizationType) {
23
+ // Convert chart visualization type to format: chart_subtype
24
+ // e.g., "Bar" -> "chart_bar", "Line" -> "chart_line"
25
+ const subtype = String(config.visualizationType).toLowerCase().replace(/\s+/g, '_')
26
+ return `${subtype}`
27
+ }
28
+
29
+ if (config?.type === 'chart') {
30
+ return 'chart'
31
+ }
32
+ }
33
+
34
+ export { getVizTitle, getVizSubType }
@@ -0,0 +1,56 @@
1
+ // Chart color distributions for v2 9-color sequential palettes to improve contrast
2
+ // Similar to map distributions but optimized for chart legends
3
+ export const v2ColorDistribution = {
4
+ 1: [4], // Middle color for single item
5
+ 2: [0, 8], // First and middle
6
+ 3: [0, 4, 8], // First, middle, last
7
+ 4: [0, 2, 6, 8], // Evenly spaced
8
+ 5: [0, 2, 4, 6, 8], // Well-distributed
9
+ 6: [0, 1, 3, 5, 7, 8], // Good spacing
10
+ 7: [0, 1, 2, 4, 6, 7, 8], // Skip middle-adjacent
11
+ 8: [0, 1, 2, 3, 5, 6, 7, 8], // Skip one in middle
12
+ 9: [0, 1, 2, 3, 4, 5, 6, 7, 8] // Use all colors
13
+ }
14
+
15
+ // Enhanced color distributions for divergent palettes to maximize contrast
16
+ // Prioritizes endpoints and avoids the neutral middle region
17
+ export const divergentColorDistribution = {
18
+ 1: [4], // Neutral middle for single item
19
+ 2: [1, 7], // Strong contrast from each end
20
+ 3: [0, 4, 8], // Maximum endpoints plus neutral
21
+ 4: [0, 2, 6, 8], // Well-spaced with emphasis on extremes
22
+ 5: [0, 1, 4, 7, 8], // Skip adjacent to middle, emphasize ends
23
+ 6: [0, 1, 3, 5, 7, 8], // Avoid immediate middle area
24
+ 7: [0, 1, 2, 4, 6, 7, 8], // Skip one middle-adjacent
25
+ 8: [0, 1, 2, 3, 5, 6, 7, 8], // Skip one in middle
26
+ 9: [0, 1, 2, 3, 4, 5, 6, 7, 8] // Use all colors
27
+ }
28
+
29
+ // Color distribution optimized for colorblind-safe palette accessibility
30
+ // Maximizes perceptual differences for color vision deficiency
31
+ export const colorblindColorDistribution = {
32
+ 1: [0], // Start with strongest color
33
+ 2: [0, 4], // High contrast pair (orange, blue)
34
+ 3: [0, 2, 4], // Orange, green, blue - maximum differentiation
35
+ 4: [0, 1, 2, 4], // Add cyan for more separation
36
+ 5: [0, 1, 2, 4, 5], // Add dark blue for depth
37
+ 6: [0, 1, 2, 4, 5, 6], // Add pink for additional distinction
38
+ 7: [0, 1, 2, 4, 5, 6, 8], // Add brown, skip black temporarily
39
+ 8: [0, 1, 2, 4, 5, 6, 7, 8], // Add black for maximum contrast
40
+ 9: [0, 1, 2, 3, 4, 5, 6, 7, 8] // Use all colors
41
+ }
42
+
43
+ // Basic color distribution for map v1 compatibility
44
+ // Simple distribution pattern for any palette length up to 10 items
45
+ export const mapV1ColorDistribution = {
46
+ 1: [1],
47
+ 2: [1, 3],
48
+ 3: [1, 3, 5],
49
+ 4: [0, 2, 4, 6],
50
+ 5: [0, 2, 4, 6, 7],
51
+ 6: [0, 2, 3, 4, 5, 7],
52
+ 7: [0, 2, 3, 4, 5, 6, 7],
53
+ 8: [0, 2, 3, 4, 5, 6, 7, 8],
54
+ 9: [0, 1, 2, 3, 4, 5, 6, 7, 8],
55
+ 10: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
56
+ }