@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.
- package/_stories/StoryRenderingTests.stories.tsx +164 -0
- package/components/AdvancedEditor/AdvancedEditor.tsx +32 -9
- package/components/CustomColorsEditor/CustomColorsEditor.css +299 -0
- package/components/CustomColorsEditor/CustomColorsEditor.tsx +209 -0
- package/components/CustomColorsEditor/index.ts +1 -0
- package/components/DataTable/DataTable.tsx +56 -38
- package/components/DataTable/DataTableStandAlone.tsx +8 -3
- package/components/DataTable/components/ChartHeader.tsx +44 -14
- package/components/DataTable/components/DataTableEditorPanel.tsx +12 -2
- package/components/DataTable/components/ExpandCollapse.tsx +10 -1
- package/components/DataTable/components/MapHeader.tsx +24 -13
- package/components/DataTable/data-table.css +12 -0
- package/components/DataTable/helpers/chartCellMatrix.tsx +11 -8
- package/components/DataTable/helpers/mapCellMatrix.tsx +33 -4
- package/components/DataTable/helpers/standardizeState.js +2 -2
- package/components/DataTable/helpers/tests/standardizeState.test.js +54 -0
- package/components/DownloadButton.tsx +40 -14
- package/components/EditorPanel/DataTableEditor.tsx +3 -3
- package/components/EditorPanel/EditorPanel.styles.css +423 -0
- package/components/EditorPanel/FootnotesEditor.tsx +44 -37
- package/components/EditorPanel/Inputs.tsx +12 -2
- package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +35 -62
- package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +12 -2
- package/components/EditorPanel/components/MarkupHighlightedTextField.tsx +227 -0
- package/components/EditorPanel/components/MarkupVariablesEditor.tsx +450 -0
- package/components/EditorPanel/components/PanelMarkup.tsx +59 -0
- package/components/ErrorBoundary.jsx +3 -1
- package/components/Filters/Filters.tsx +52 -24
- package/components/Filters/components/Dropdown.tsx +6 -1
- package/components/Filters/components/Tabs.tsx +1 -0
- package/components/Footnotes/Footnotes.tsx +35 -25
- package/components/Footnotes/FootnotesStandAlone.tsx +42 -6
- package/components/HeaderThemeSelector/HeaderThemeSelector.css +43 -0
- package/components/HeaderThemeSelector/HeaderThemeSelector.stories.tsx +74 -0
- package/components/HeaderThemeSelector/HeaderThemeSelector.tsx +61 -0
- package/components/HeaderThemeSelector/index.ts +2 -0
- package/components/Layout/styles/editor.scss +2 -1
- package/components/Legend/Legend.Gradient.tsx +3 -6
- package/components/LegendShape.tsx +121 -3
- package/components/Loader/Loader.tsx +1 -1
- package/components/MediaControls.tsx +72 -21
- package/components/PaletteConversionModal.tsx +90 -0
- package/components/PaletteSelector/DeveloperPaletteRollback.tsx +114 -0
- package/components/PaletteSelector/PaletteSelector.css +94 -0
- package/components/PaletteSelector/PaletteSelector.tsx +112 -0
- package/components/PaletteSelector/index.ts +2 -0
- package/components/RichTooltip/RichTooltip.tsx +1 -0
- package/components/Table/Table.tsx +3 -1
- package/components/Table/components/Cell.tsx +23 -2
- package/components/Table/components/Row.tsx +5 -3
- package/components/_stories/BlurStrokeTest.stories.tsx +1 -1
- package/components/_stories/DataTable.stories.tsx +1 -1
- package/components/_stories/Filters.stories.tsx +21 -2
- package/components/_stories/Footnotes.CSV.stories.tsx +247 -0
- package/components/_stories/Footnotes.stories.tsx +769 -4
- package/components/_stories/Inputs.stories.tsx +3 -3
- package/components/_stories/MultiSelect.stories.tsx +3 -3
- package/components/_stories/NestedDropdown.stories.tsx +1 -1
- package/components/_stories/Table.stories.tsx +1 -1
- package/components/_stories/styles.scss +0 -1
- package/components/elements/_stories/Button.stories.tsx +1 -1
- package/components/elements/_stories/Card.stories.tsx +1 -1
- package/components/inputs/InputToggle.tsx +2 -0
- package/components/managers/DataDesigner.tsx +10 -9
- package/components/managers/_stories/DataDesigner.stories.tsx +1 -1
- package/components/ui/Accordion.jsx +1 -1
- package/components/ui/Tooltip.tsx +2 -1
- package/components/ui/_stories/Accordion.stories.tsx +1 -1
- package/components/ui/_stories/ColorPaletteMigration.stories.mdx +275 -0
- package/components/ui/_stories/Colors.stories.tsx +330 -0
- package/components/ui/_stories/IconGallery.stories.tsx +316 -0
- package/components/ui/_stories/Title.stories.tsx +1 -1
- package/components/ui/accordion.styles.css +57 -0
- package/contexts/EditorContext.ts +18 -0
- package/contexts/editor.actions.ts +28 -0
- package/contexts/editor.reducer.ts +94 -0
- package/data/chartColorPalettes.ts +118 -0
- package/data/colorPalettes.ts +9 -0
- package/data/mapColorPalettes.ts +45 -0
- package/data/sharedPalettes.ts +50 -0
- package/dist/cove-main.css +63 -14
- package/dist/cove-main.css.map +1 -1
- package/generateViteConfig.js +80 -0
- package/helpers/addValuesToFilters.ts +7 -3
- package/helpers/cloneConfig.ts +31 -0
- package/helpers/configDataHelpers.ts +128 -0
- package/helpers/configHelpers.ts +27 -0
- package/helpers/constants.ts +42 -2
- package/helpers/cove/number.ts +33 -12
- package/helpers/coveUpdateWorker.ts +15 -3
- package/helpers/fetchRemoteData.ts +3 -15
- package/helpers/filterColorPalettes.ts +152 -0
- package/helpers/generateColorsArray.ts +13 -0
- package/helpers/getColorPaletteVersion.ts +33 -0
- package/helpers/getPaletteAccessor.ts +18 -0
- package/helpers/markupProcessor.ts +220 -0
- package/helpers/mergeCustomOrderValues.ts +37 -0
- package/helpers/metrics/helpers.ts +42 -19
- package/helpers/metrics/types.ts +48 -9
- package/helpers/metrics/utils.ts +34 -0
- package/helpers/palettes/colorDistributions.ts +56 -0
- package/helpers/palettes/migratePaletteName.ts +150 -0
- package/helpers/palettes/standardizePaletteNames.ts +77 -0
- package/helpers/palettes/utils.ts +267 -0
- package/helpers/parseCsvWithQuotes.ts +65 -0
- package/helpers/queryStringUtils.ts +13 -0
- package/helpers/testing.ts +358 -0
- package/helpers/tests/addValuesToFilters.test.ts +1 -2
- package/helpers/tests/generateColorsArray.test.ts +24 -0
- package/helpers/tests/markupProcessor.test.ts +538 -0
- package/helpers/tests/testStandaloneBuild.ts +44 -0
- package/helpers/useMarkupVariables.ts +31 -0
- package/helpers/vegaConfig.ts +0 -1
- package/helpers/ver/4.24.10.ts +2 -1
- package/helpers/ver/4.24.11.ts +2 -1
- package/helpers/ver/4.24.3.ts +2 -1
- package/helpers/ver/4.24.4.ts +2 -1
- package/helpers/ver/4.24.5.ts +2 -1
- package/helpers/ver/4.24.7.ts +2 -1
- package/helpers/ver/4.24.9.ts +2 -1
- package/helpers/ver/4.25.1.ts +2 -1
- package/helpers/ver/4.25.10.ts +36 -0
- package/helpers/ver/4.25.11.ts +13 -0
- package/helpers/ver/4.25.3.ts +2 -1
- package/helpers/ver/4.25.4.ts +2 -1
- package/helpers/ver/4.25.6.ts +2 -1
- package/helpers/ver/4.25.7.ts +2 -1
- package/helpers/ver/4.25.8.ts +2 -1
- package/helpers/ver/4.25.9.ts +293 -0
- package/helpers/ver/tests/4.25.10.test.ts +204 -0
- package/helpers/ver/tests/4.25.8.test.ts +1 -1
- package/helpers/ver/tests/4.25.9.test.ts +51 -0
- package/helpers/viewports.ts +2 -0
- package/hooks/useColorPalette.ts +79 -0
- package/package.json +13 -4
- package/styles/_common-components.css +73 -0
- package/styles/_global.scss +32 -10
- package/styles/base.scss +8 -55
- package/styles/cove-main.scss +3 -1
- package/styles/filters.scss +10 -3
- package/styles/v2/base/index.scss +0 -1
- package/styles/v2/components/button.scss +4 -3
- package/styles/v2/components/editor.scss +16 -7
- package/styles/v2/layout/_data-table.scss +3 -2
- package/styles/v2/themes/_color-definitions.scss +18 -17
- package/styles/v2/utils/_breakpoints.scss +1 -1
- package/styles/v2/utils/index.scss +0 -1
- package/styles/waiting.scss +1 -1
- package/testing-setup.js +32 -0
- package/types/MarkupInclude.ts +8 -2
- package/types/MarkupVariable.ts +19 -0
- package/types/VizFilter.ts +2 -0
- package/vitest.config.ts +16 -0
- package/components/ui/_stories/Colors.stories.mdx +0 -220
- package/components/ui/_stories/IconGallery.stories.mdx +0 -14
- package/data/colorPalettes.js +0 -171
- package/helpers/formatConfigBeforeSave.ts +0 -135
- package/helpers/tests/formatConfigBeforeSave.test.ts +0 -68
- package/styles/_mixins.scss +0 -13
- package/styles/v2/base/_typography.scss +0 -0
- package/styles/v2/components/guidance-block.scss +0 -74
- package/styles/v2/utils/_functions.scss +0 -0
- /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
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* @param {
|
|
28
|
-
* @param {
|
|
29
|
-
* @param {
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
eventLabel,
|
|
48
|
-
component: visualizationType || 'unknown',
|
|
49
|
-
version: getPackageVersion() || 'unknown',
|
|
50
|
-
...(additionalDetails || {})
|
|
72
|
+
formattedEvent,
|
|
73
|
+
eventLabel
|
|
51
74
|
})
|
|
52
75
|
}
|
package/helpers/metrics/types.ts
CHANGED
|
@@ -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
|
-
| '
|
|
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
|
-
|
|
34
|
-
|
|
68
|
+
|
|
69
|
+
// Legend events
|
|
70
|
+
| `${COVE_VISUALIZATION_TYPES}_legend_item_toggled` // simplified with specifics for mode
|
|
35
71
|
| `${COVE_VISUALIZATION_TYPES}_legend_reset`
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
|
41
|
-
|
|
|
42
|
-
|
|
|
43
|
-
|
|
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 }
|