@cdc/core 4.25.8 → 4.25.11

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 (163) hide show
  1. package/_stories/StoryRenderingTests.stories.tsx +164 -0
  2. package/components/AdvancedEditor/AdvancedEditor.tsx +32 -9
  3. package/components/CustomColorsEditor/CustomColorsEditor.css +299 -0
  4. package/components/CustomColorsEditor/CustomColorsEditor.tsx +209 -0
  5. package/components/CustomColorsEditor/index.ts +1 -0
  6. package/components/DataTable/DataTable.tsx +56 -38
  7. package/components/DataTable/DataTableStandAlone.tsx +8 -3
  8. package/components/DataTable/components/ChartHeader.tsx +44 -14
  9. package/components/DataTable/components/DataTableEditorPanel.tsx +12 -2
  10. package/components/DataTable/components/ExpandCollapse.tsx +10 -1
  11. package/components/DataTable/components/MapHeader.tsx +24 -13
  12. package/components/DataTable/data-table.css +12 -0
  13. package/components/DataTable/helpers/chartCellMatrix.tsx +11 -8
  14. package/components/DataTable/helpers/mapCellMatrix.tsx +33 -4
  15. package/components/DataTable/helpers/standardizeState.js +2 -2
  16. package/components/DataTable/helpers/tests/standardizeState.test.js +54 -0
  17. package/components/DownloadButton.tsx +40 -14
  18. package/components/EditorPanel/DataTableEditor.tsx +3 -3
  19. package/components/EditorPanel/EditorPanel.styles.css +423 -0
  20. package/components/EditorPanel/FootnotesEditor.tsx +44 -37
  21. package/components/EditorPanel/Inputs.tsx +12 -2
  22. package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +35 -62
  23. package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +12 -2
  24. package/components/EditorPanel/components/MarkupHighlightedTextField.tsx +227 -0
  25. package/components/EditorPanel/components/MarkupVariablesEditor.tsx +450 -0
  26. package/components/EditorPanel/components/PanelMarkup.tsx +59 -0
  27. package/components/ErrorBoundary.jsx +3 -1
  28. package/components/Filters/Filters.tsx +52 -24
  29. package/components/Filters/components/Dropdown.tsx +6 -1
  30. package/components/Filters/components/Tabs.tsx +1 -0
  31. package/components/Footnotes/Footnotes.tsx +35 -25
  32. package/components/Footnotes/FootnotesStandAlone.tsx +42 -6
  33. package/components/HeaderThemeSelector/HeaderThemeSelector.css +43 -0
  34. package/components/HeaderThemeSelector/HeaderThemeSelector.stories.tsx +74 -0
  35. package/components/HeaderThemeSelector/HeaderThemeSelector.tsx +61 -0
  36. package/components/HeaderThemeSelector/index.ts +2 -0
  37. package/components/Layout/styles/editor.scss +2 -1
  38. package/components/Legend/Legend.Gradient.tsx +3 -6
  39. package/components/LegendShape.tsx +121 -3
  40. package/components/Loader/Loader.tsx +1 -1
  41. package/components/MediaControls.tsx +72 -21
  42. package/components/PaletteConversionModal.tsx +90 -0
  43. package/components/PaletteSelector/DeveloperPaletteRollback.tsx +114 -0
  44. package/components/PaletteSelector/PaletteSelector.css +94 -0
  45. package/components/PaletteSelector/PaletteSelector.tsx +112 -0
  46. package/components/PaletteSelector/index.ts +2 -0
  47. package/components/RichTooltip/RichTooltip.tsx +1 -0
  48. package/components/Table/Table.tsx +3 -1
  49. package/components/Table/components/Cell.tsx +23 -2
  50. package/components/Table/components/Row.tsx +5 -3
  51. package/components/_stories/BlurStrokeTest.stories.tsx +1 -1
  52. package/components/_stories/DataTable.stories.tsx +1 -1
  53. package/components/_stories/Filters.stories.tsx +21 -2
  54. package/components/_stories/Footnotes.CSV.stories.tsx +247 -0
  55. package/components/_stories/Footnotes.stories.tsx +769 -4
  56. package/components/_stories/Inputs.stories.tsx +3 -3
  57. package/components/_stories/MultiSelect.stories.tsx +3 -3
  58. package/components/_stories/NestedDropdown.stories.tsx +1 -1
  59. package/components/_stories/Table.stories.tsx +1 -1
  60. package/components/_stories/styles.scss +0 -1
  61. package/components/elements/_stories/Button.stories.tsx +1 -1
  62. package/components/elements/_stories/Card.stories.tsx +1 -1
  63. package/components/inputs/InputToggle.tsx +2 -0
  64. package/components/managers/DataDesigner.tsx +10 -9
  65. package/components/managers/_stories/DataDesigner.stories.tsx +1 -1
  66. package/components/ui/Accordion.jsx +1 -1
  67. package/components/ui/Tooltip.tsx +2 -1
  68. package/components/ui/_stories/Accordion.stories.tsx +1 -1
  69. package/components/ui/_stories/ColorPaletteMigration.stories.mdx +275 -0
  70. package/components/ui/_stories/Colors.stories.tsx +330 -0
  71. package/components/ui/_stories/IconGallery.stories.tsx +316 -0
  72. package/components/ui/_stories/Title.stories.tsx +1 -1
  73. package/components/ui/accordion.styles.css +57 -0
  74. package/contexts/EditorContext.ts +18 -0
  75. package/contexts/editor.actions.ts +28 -0
  76. package/contexts/editor.reducer.ts +94 -0
  77. package/data/chartColorPalettes.ts +118 -0
  78. package/data/colorPalettes.ts +9 -0
  79. package/data/mapColorPalettes.ts +45 -0
  80. package/data/sharedPalettes.ts +50 -0
  81. package/dist/cove-main.css +63 -14
  82. package/dist/cove-main.css.map +1 -1
  83. package/generateViteConfig.js +80 -0
  84. package/helpers/addValuesToFilters.ts +7 -3
  85. package/helpers/cloneConfig.ts +31 -0
  86. package/helpers/configDataHelpers.ts +128 -0
  87. package/helpers/configHelpers.ts +27 -0
  88. package/helpers/constants.ts +42 -2
  89. package/helpers/cove/number.ts +33 -12
  90. package/helpers/coveUpdateWorker.ts +15 -3
  91. package/helpers/fetchRemoteData.ts +3 -15
  92. package/helpers/filterColorPalettes.ts +152 -0
  93. package/helpers/generateColorsArray.ts +13 -0
  94. package/helpers/getColorPaletteVersion.ts +33 -0
  95. package/helpers/getPaletteAccessor.ts +18 -0
  96. package/helpers/markupProcessor.ts +220 -0
  97. package/helpers/mergeCustomOrderValues.ts +37 -0
  98. package/helpers/metrics/helpers.ts +42 -19
  99. package/helpers/metrics/types.ts +48 -9
  100. package/helpers/metrics/utils.ts +34 -0
  101. package/helpers/palettes/colorDistributions.ts +56 -0
  102. package/helpers/palettes/migratePaletteName.ts +150 -0
  103. package/helpers/palettes/standardizePaletteNames.ts +77 -0
  104. package/helpers/palettes/utils.ts +267 -0
  105. package/helpers/parseCsvWithQuotes.ts +65 -0
  106. package/helpers/queryStringUtils.ts +13 -0
  107. package/helpers/testing.ts +358 -0
  108. package/helpers/tests/addValuesToFilters.test.ts +1 -2
  109. package/helpers/tests/generateColorsArray.test.ts +24 -0
  110. package/helpers/tests/markupProcessor.test.ts +538 -0
  111. package/helpers/tests/testStandaloneBuild.ts +44 -0
  112. package/helpers/useMarkupVariables.ts +31 -0
  113. package/helpers/vegaConfig.ts +0 -1
  114. package/helpers/ver/4.24.10.ts +2 -1
  115. package/helpers/ver/4.24.11.ts +2 -1
  116. package/helpers/ver/4.24.3.ts +2 -1
  117. package/helpers/ver/4.24.4.ts +2 -1
  118. package/helpers/ver/4.24.5.ts +2 -1
  119. package/helpers/ver/4.24.7.ts +2 -1
  120. package/helpers/ver/4.24.9.ts +2 -1
  121. package/helpers/ver/4.25.1.ts +2 -1
  122. package/helpers/ver/4.25.10.ts +36 -0
  123. package/helpers/ver/4.25.11.ts +13 -0
  124. package/helpers/ver/4.25.3.ts +2 -1
  125. package/helpers/ver/4.25.4.ts +2 -1
  126. package/helpers/ver/4.25.6.ts +2 -1
  127. package/helpers/ver/4.25.7.ts +2 -1
  128. package/helpers/ver/4.25.8.ts +2 -1
  129. package/helpers/ver/4.25.9.ts +293 -0
  130. package/helpers/ver/tests/4.25.10.test.ts +204 -0
  131. package/helpers/ver/tests/4.25.8.test.ts +1 -1
  132. package/helpers/ver/tests/4.25.9.test.ts +51 -0
  133. package/helpers/viewports.ts +2 -0
  134. package/hooks/useColorPalette.ts +79 -0
  135. package/package.json +13 -4
  136. package/styles/_common-components.css +73 -0
  137. package/styles/_global.scss +32 -10
  138. package/styles/base.scss +8 -55
  139. package/styles/cove-main.scss +3 -1
  140. package/styles/filters.scss +10 -3
  141. package/styles/v2/base/index.scss +0 -1
  142. package/styles/v2/components/button.scss +4 -3
  143. package/styles/v2/components/editor.scss +16 -7
  144. package/styles/v2/layout/_data-table.scss +3 -2
  145. package/styles/v2/themes/_color-definitions.scss +18 -17
  146. package/styles/v2/utils/_breakpoints.scss +1 -1
  147. package/styles/v2/utils/index.scss +0 -1
  148. package/styles/waiting.scss +1 -1
  149. package/testing-setup.js +32 -0
  150. package/types/MarkupInclude.ts +8 -2
  151. package/types/MarkupVariable.ts +19 -0
  152. package/types/VizFilter.ts +2 -0
  153. package/vitest.config.ts +16 -0
  154. package/components/ui/_stories/Colors.stories.mdx +0 -220
  155. package/components/ui/_stories/IconGallery.stories.mdx +0 -14
  156. package/data/colorPalettes.js +0 -171
  157. package/helpers/formatConfigBeforeSave.ts +0 -135
  158. package/helpers/tests/formatConfigBeforeSave.test.ts +0 -68
  159. package/styles/_mixins.scss +0 -13
  160. package/styles/v2/base/_typography.scss +0 -0
  161. package/styles/v2/components/guidance-block.scss +0 -74
  162. package/styles/v2/utils/_functions.scss +0 -0
  163. /package/{styles/_typography.scss → testBuild.js} +0 -0
@@ -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,220 @@
1
+ import _ from 'lodash'
2
+ import { MarkupVariable, MarkupCondition } from '../types/MarkupVariable'
3
+ import { VizFilter } from '../types/VizFilter'
4
+ import { Datasets } from '../types/DataSet'
5
+ import { filterVizData } from './filterVizData'
6
+
7
+ /**
8
+ * Replaces {{variable}} tags in content with actual data values.
9
+ *
10
+ * @param content - Content string with markup variables
11
+ * @param data - Dataset to extract values from (for backward compatibility)
12
+ * @param markupVariables - Variable configurations
13
+ * @param options - isEditor, showNoDataMessage, allowHideSection, filters, datasets
14
+ * @returns Processed content and state flags
15
+ *
16
+ * @security Returns plain text - must be parsed with html-react-parser before rendering
17
+ */
18
+ export const processMarkupVariables = (
19
+ content: string,
20
+ data: any[] = [],
21
+ markupVariables: MarkupVariable[] = [],
22
+ options: {
23
+ isEditor?: boolean
24
+ showNoDataMessage?: boolean
25
+ allowHideSection?: boolean
26
+ filters?: VizFilter[]
27
+ datasets?: Datasets
28
+ configDataKey?: string // Add support for widget's assigned dataset
29
+ } = {}
30
+ ): {
31
+ processedContent: string
32
+ shouldHideSection: boolean
33
+ shouldShowNoDataMessage: boolean
34
+ } => {
35
+ const { isEditor = false, showNoDataMessage = false, allowHideSection = false, filters = [], datasets, configDataKey } = options
36
+
37
+ // Helper function to get data for a specific variable
38
+ const getDataForVariable = (variable: MarkupVariable): any[] => {
39
+ // If data prop is empty, try to use the widget's assigned dataset
40
+ if ((!data || data.length === 0) && configDataKey && datasets && datasets[configDataKey]) {
41
+ return datasets[configDataKey].data || []
42
+ }
43
+
44
+ return data || []
45
+ }
46
+
47
+ // Early return for invalid inputs
48
+ if (_.isEmpty(markupVariables) || !content) {
49
+ return {
50
+ processedContent: content || '',
51
+ shouldHideSection: false,
52
+ shouldShowNoDataMessage: false
53
+ }
54
+ }
55
+
56
+ try {
57
+ const emptyVariableChecker: boolean[] = []
58
+ const noDataMessageChecker: boolean[] = []
59
+
60
+ const variableRegexPattern = /{{(.*?)}}/g
61
+ const processedContent = content.replace(variableRegexPattern, variableTag => {
62
+ try {
63
+ if (emptyVariableChecker.length > 0) return variableTag
64
+
65
+ const workingVariable = markupVariables.find(variable => variable.tag === variableTag)
66
+ if (!workingVariable) return variableTag
67
+
68
+ // Validate that columnName exists
69
+ if (!workingVariable.columnName) {
70
+ console.warn(`Markup variable ${variableTag} has no columnName specified`)
71
+ return variableTag
72
+ }
73
+
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
+ // Extract values with error handling
89
+ const variableValues: string[] = _.uniq(
90
+ (conditionFilteredData || []).map(dataObject => {
91
+ try {
92
+ const dataObjectValue = dataObject[workingVariable.columnName]
93
+
94
+ // Handle undefined column
95
+ if (dataObjectValue === undefined && isEditor) {
96
+ console.warn(
97
+ `Column "${workingVariable.columnName}" not found in data for variable ${variableTag}`
98
+ )
99
+ }
100
+
101
+ return workingVariable.addCommas && !isNaN(parseFloat(dataObjectValue))
102
+ ? parseFloat(dataObjectValue).toLocaleString('en-US', { useGrouping: true })
103
+ : String(dataObjectValue || '')
104
+ } catch (error) {
105
+ console.error(`Error processing data value for ${variableTag}:`, error)
106
+ return ''
107
+ }
108
+ })
109
+ ).filter(value => value !== '') // Filter out empty values
110
+
111
+ const listConjunction = !isEditor ? 'and' : 'or'
112
+ const formattedValues = formatValuesList(variableValues, listConjunction)
113
+
114
+ const finalDisplay = formattedValues.join(', ')
115
+
116
+ if (showNoDataMessage && finalDisplay === '') {
117
+ noDataMessageChecker.push(true)
118
+ }
119
+
120
+ if (finalDisplay === '' && allowHideSection) {
121
+ emptyVariableChecker.push(true)
122
+ }
123
+
124
+ return finalDisplay
125
+ } catch (error) {
126
+ console.error(`Error processing markup variable ${variableTag}:`, error)
127
+ return variableTag // Return original tag on error
128
+ }
129
+ })
130
+
131
+ return {
132
+ processedContent,
133
+ shouldHideSection: allowHideSection && emptyVariableChecker.length > 0 && !isEditor,
134
+ shouldShowNoDataMessage: showNoDataMessage && noDataMessageChecker.length > 0 && !isEditor
135
+ }
136
+ } catch (error) {
137
+ console.error('Error in processMarkupVariables:', error)
138
+ // Return original content on error
139
+ return {
140
+ processedContent: content,
141
+ shouldHideSection: false,
142
+ shouldShowNoDataMessage: false
143
+ }
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Filters data based on multiple conditions
149
+ */
150
+ const filterDataByConditions = (data: any[], conditions: MarkupCondition[]): any[] => {
151
+ if (!conditions.length) return data
152
+
153
+ const [currentCondition, ...remainingConditions] = conditions
154
+ const { columnName, isOrIsNotEqualTo, value } = currentCondition
155
+
156
+ const filteredData = isOrIsNotEqualTo === 'is'
157
+ ? data.filter(dataObject => String(dataObject[columnName]) === value)
158
+ : data.filter(dataObject => String(dataObject[columnName]) !== value)
159
+
160
+ return remainingConditions.length === 0
161
+ ? filteredData
162
+ : filterDataByConditions(filteredData, remainingConditions)
163
+ }
164
+
165
+ /**
166
+ * Formats a list of values with proper conjunction
167
+ */
168
+ const formatValuesList = (values: string[], conjunction: string): string[] => {
169
+ if (values.length === 0) return values
170
+ if (values.length === 1) return values
171
+ if (values.length === 2) {
172
+ return [`${values[0]} ${conjunction} ${values[1]}`]
173
+ }
174
+
175
+ const formatted = [...values]
176
+ formatted[formatted.length - 1] = `${conjunction} ${formatted[formatted.length - 1]}`
177
+ return [formatted.join(', ')]
178
+ }
179
+
180
+ /**
181
+ * Validates markup variables configuration
182
+ */
183
+ export const validateMarkupVariables = (
184
+ markupVariables: MarkupVariable[],
185
+ data: any[]
186
+ ): string[] => {
187
+ const errors: string[] = []
188
+
189
+ if (!markupVariables || !Array.isArray(markupVariables)) {
190
+ return errors
191
+ }
192
+
193
+ const availableColumns = data.length > 0 ? Object.keys(data[0]) : []
194
+
195
+ markupVariables.forEach((variable, index) => {
196
+ if (!variable.tag || !variable.tag.match(/^{{.+}}$/)) {
197
+ errors.push(`Variable ${index + 1}: Tag must be in format {{tagName}}`)
198
+ }
199
+
200
+ if (!variable.columnName) {
201
+ errors.push(`Variable ${index + 1}: Column name is required`)
202
+ } else if (availableColumns.length > 0 && !availableColumns.includes(variable.columnName)) {
203
+ errors.push(`Variable ${index + 1}: Column "${variable.columnName}" not found in data`)
204
+ }
205
+
206
+ variable.conditions.forEach((condition, condIndex) => {
207
+ if (!condition.columnName) {
208
+ errors.push(`Variable ${index + 1}, Condition ${condIndex + 1}: Column name is required`)
209
+ } 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`)
211
+ }
212
+
213
+ if (!condition.value) {
214
+ errors.push(`Variable ${index + 1}, Condition ${condIndex + 1}: Value is required`)
215
+ }
216
+ })
217
+ })
218
+
219
+ return errors
220
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Merges new filter values with existing custom ordered values
3
+ *
4
+ * When order === 'cust', this function ensures that:
5
+ * 1. New values from the data are appended to the end of orderedValues
6
+ * 2. If orderedValues is missing/empty, it initializes with current values
7
+ *
8
+ * @param currentValues - Array of all unique values extracted from current data
9
+ * @param existingOrderedValues - Existing custom ordered values array (may be undefined/empty)
10
+ * @param order - The filter's order setting
11
+ * @returns Updated orderedValues array with new values appended, or undefined if not custom order
12
+ */
13
+ export const mergeCustomOrderValues = (
14
+ currentValues: (string | number)[],
15
+ existingOrderedValues: string[] | undefined,
16
+ order: string | undefined
17
+ ): string[] | undefined => {
18
+ // Only process for custom order
19
+ if (order !== 'cust') {
20
+ return existingOrderedValues
21
+ }
22
+
23
+ // Normalize current values to strings (filter values are always displayed as strings)
24
+ const normalizedCurrentValues = currentValues.map(v => String(v))
25
+
26
+ // If orderedValues doesn't exist or is empty, initialize with current values
27
+ if (!existingOrderedValues || existingOrderedValues.length === 0) {
28
+ return [...normalizedCurrentValues]
29
+ }
30
+
31
+ // Find new values that aren't in orderedValues yet
32
+ const orderedValuesSet = new Set(existingOrderedValues)
33
+ const newValues = normalizedCurrentValues.filter(value => !orderedValuesSet.has(value))
34
+
35
+ // Return merged array: existing order + new values appended
36
+ return [...existingOrderedValues, ...newValues]
37
+ }
@@ -3,7 +3,8 @@ import packageJson from '../../package.json'
3
3
  import {
4
4
  COVE_VISUALIZATION_TYPES,
5
5
  ANALYTICS_EVENT_ACTIONS,
6
- ANALYTICS_EVENT_TYPES
6
+ ANALYTICS_EVENT_TYPES,
7
+ EventSpecifics
7
8
  } from './types'
8
9
  import { GetLabelForEvent } from './getLabelForEvent'
9
10
 
@@ -22,31 +23,53 @@ export const getPackageVersion = () => {
22
23
 
23
24
  /**
24
25
  * Publishes an analytics event with the specified parameters.
25
- * @param {ANALYTICS_EVENT_TYPES} eventType - The type of the event
26
- * @param {ANALYTICS_EVENT_ACTIONS} [eventAction='unknown'] - The action associated with the event
27
- * @param {ANALYTICS_EVENT_LABEL} [eventLabel='unknown'] - The label for the event
28
- * @param {COVE_VISUALIZATION_TYPES} [visualizationType] - The type of visualization associated with the event
29
- * @param {Object} [additionalDetails] - Additional details to include in the event
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)
30
38
  * @returns {void}
31
39
  * @description This function is used to publish analytics events for various user interactions and system states.
32
40
  */
33
- export const publishAnalyticsEvent = <T extends ANALYTICS_EVENT_TYPES>(
34
- eventType: T,
35
- eventAction: ANALYTICS_EVENT_ACTIONS = 'unknown',
36
- eventLabel: GetLabelForEvent<T>,
37
- visualizationType?: COVE_VISUALIZATION_TYPES,
38
- additionalDetails?: Object
39
- ) => {
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
+ }) => {
40
64
  // Added check if we ever need to disable analytics
41
65
  const ANALYTICS_POWERED_ON = true
42
66
  if (!ANALYTICS_POWERED_ON) return
43
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'}`
44
71
  return publish('cove:analytics', {
45
- eventType,
46
- eventAction,
47
- eventLabel,
48
- component: visualizationType || 'unknown',
49
- version: getPackageVersion() || 'unknown',
50
- ...(additionalDetails || {})
72
+ formattedEvent,
73
+ eventLabel
51
74
  })
52
75
  }
@@ -2,12 +2,14 @@ export type COVE_VISUALIZATION_TYPES =
2
2
  | 'map'
3
3
  | 'chart'
4
4
  | 'data-table'
5
+ | 'table'
5
6
  | 'markup-include'
6
7
  | 'waffle-chart'
7
8
  | 'dashboard'
8
9
  | 'filtered-text'
9
10
  | 'table-filter'
10
11
  | 'data-bite'
12
+ | 'navigation'
11
13
  | 'unknown'
12
14
 
13
15
  export type ANALYTICS_EVENT_ACTIONS =
@@ -16,28 +18,65 @@ export type ANALYTICS_EVENT_ACTIONS =
16
18
  | 'toggle'
17
19
  | 'none'
18
20
  | 'keydown'
21
+ | 'keyboard'
19
22
  | 'load'
20
23
  | 'submit'
21
24
  | 'change'
25
+ | 'hover'
22
26
  | 'unknown'
23
27
 
24
28
  export type LEGEND_TOGGLE_MODES = 'highlight' | 'isolate'
25
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
+
26
55
  export type ANALYTICS_EVENT_TYPES =
56
+ // Data actions
27
57
  | 'data_downloaded'
28
- | 'data_table_toggled' // expand/collapse
58
+ | 'expand_collapse_toggled' // alternative name for data_table_toggled
59
+ | 'data_table_sort'
29
60
  | 'data_viewed'
61
+ | 'clicked_data_link_to_view'
62
+ | 'link_to_data_table_click'
63
+
64
+ // Filter events
30
65
  | `${COVE_VISUALIZATION_TYPES}_filter_reset`
31
66
  | `${COVE_VISUALIZATION_TYPES}_filter_applied`
32
67
  | `${COVE_VISUALIZATION_TYPES}_filter_changed`
33
- | `${COVE_VISUALIZATION_TYPES}_image_downloaded`
34
- | `${COVE_VISUALIZATION_TYPES}_legend_item_toggled--${LEGEND_TOGGLE_MODES}-mode`
68
+
69
+ // Legend events
70
+ | `${COVE_VISUALIZATION_TYPES}_legend_item_toggled` // simplified with specifics for mode
35
71
  | `${COVE_VISUALIZATION_TYPES}_legend_reset`
36
- | `${COVE_VISUALIZATION_TYPES}_loaded`
37
- | `${COVE_VISUALIZATION_TYPES}_navigation_menu`
72
+
73
+ // Map-specific events
74
+ | `${COVE_VISUALIZATION_TYPES}_hover` // simplified with specifics for location
38
75
  | `${COVE_VISUALIZATION_TYPES}_panned`
39
76
  | `${COVE_VISUALIZATION_TYPES}_reset_zoom_level`
40
- | `${COVE_VISUALIZATION_TYPES}_zoomed_in`
41
- | `${COVE_VISUALIZATION_TYPES}_zoomed_out`
42
- | `data_table_sort_by|${string}|${'asc' | 'desc' | 'undefined'}`
43
- | 'link_to_data_table_click'
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 }