@cdc/core 4.25.11 → 4.26.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/qa-test-developer.md +126 -0
- package/CLAUDE.local.md +67 -0
- package/_stories/Gallery.Charts.stories.tsx +300 -0
- package/_stories/Gallery.DataBite.stories.tsx +79 -0
- package/_stories/Gallery.Maps.stories.tsx +239 -0
- package/_stories/Gallery.WaffleChart.stories.tsx +187 -0
- package/_stories/PageART.stories.tsx +193 -0
- package/_stories/PageBRFSS.stories.tsx +294 -0
- package/_stories/PageCancerRegistries.stories.tsx +199 -0
- package/_stories/PageEasternEquineEncephalitis.stories.tsx +216 -0
- package/_stories/PageExcessiveAlcoholUse.stories.tsx +201 -0
- package/_stories/PageMaternalMortality.stories.tsx +193 -0
- package/_stories/PageOralHealth.stories.tsx +201 -0
- package/_stories/PageRespiratory.stories.tsx +332 -0
- package/_stories/PageSmokingTobacco.stories.tsx +200 -0
- package/_stories/PageStateDiabetesProfiles.stories.tsx +201 -0
- package/_stories/PageWastewater.stories.tsx +477 -0
- package/_stories/VegaImport.stories.tsx +401 -0
- package/_stories/vega-fixtures/bars-with-line.json +444 -0
- package/_stories/vega-fixtures/bars.json +58 -0
- package/_stories/vega-fixtures/combo-bar-rolling-mean.json +88 -0
- package/_stories/vega-fixtures/combo.json +68 -0
- package/_stories/vega-fixtures/grouped-horizontal-bars.json +83 -0
- package/_stories/vega-fixtures/grouped-horizontal-bars2.json +231 -0
- package/_stories/vega-fixtures/horizontal-bar.json +427 -0
- package/_stories/vega-fixtures/horizontal-bars-with-bad-colors.json +197 -0
- package/_stories/vega-fixtures/horizontal-bars2.json +58 -0
- package/_stories/vega-fixtures/lines.json +227 -0
- package/_stories/vega-fixtures/measles-bars.json +348 -0
- package/_stories/vega-fixtures/measles-map.json +11101 -0
- package/_stories/vega-fixtures/measles-stacked-bars.json +2147 -0
- package/_stories/vega-fixtures/multi-dataset.json +255 -0
- package/_stories/vega-fixtures/no-data.json +14 -0
- package/_stories/vega-fixtures/pie-chart.json +94 -0
- package/_stories/vega-fixtures/repeat-spec.json +47 -0
- package/_stories/vega-fixtures/stacked-area.json +222 -0
- package/_stories/vega-fixtures/stacked-bar-with-rect.json +3412 -0
- package/_stories/vega-fixtures/stacked-bars-with-line.json +364 -0
- package/_stories/vega-fixtures/stacked-bars.json +212 -0
- package/_stories/vega-fixtures/stacked-horizontal-bars.json +140 -0
- package/_stories/vega-fixtures/warning-combo.json +59 -0
- package/_stories/vega-fixtures/warning-scatter-and-line.json +1182 -0
- package/assets/icon-chart-area.svg +1 -0
- package/assets/icon-chart-radar.svg +23 -0
- package/assets/icon-magnifying-glass.svg +5 -0
- package/assets/icon-warming-stripes.svg +13 -0
- package/assets/logo2.svg +31 -0
- package/components/AdvancedEditor/AdvancedEditor.tsx +4 -0
- package/components/AdvancedEditor/EmbedEditor.tsx +513 -0
- package/components/ComboBox/ComboBox.tsx +345 -0
- package/components/ComboBox/combobox.styles.css +185 -0
- package/components/ComboBox/index.ts +1 -0
- package/components/CustomColorsEditor/CustomColorsEditor.tsx +3 -10
- package/components/DataTable/DataTable.tsx +132 -58
- package/components/DataTable/data-table.css +216 -215
- package/components/DataTable/helpers/getSeriesName.ts +6 -0
- package/components/DataTable/helpers/mapCellMatrix.tsx +14 -6
- package/components/EditorPanel/ColumnsEditor.tsx +37 -19
- package/components/EditorPanel/DataTableEditor.tsx +51 -25
- package/components/EditorPanel/EditorPanel.styles.css +16 -0
- package/components/EditorPanel/EditorPanel.tsx +144 -0
- package/components/EditorPanel/EditorPanelDispatch.tsx +75 -0
- package/components/EditorPanel/FieldSetWrapper.tsx +66 -23
- package/components/EditorPanel/Inputs.tsx +33 -7
- package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +14 -6
- package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +240 -175
- package/components/EditorPanel/VizFilterEditor/components/FilterOrder.tsx +33 -29
- package/components/EditorPanel/sections/VisualSection.tsx +169 -0
- package/components/Filters/Filters.tsx +31 -5
- package/components/Filters/helpers/getNestedOptions.ts +2 -1
- package/components/Filters/helpers/handleSorting.ts +1 -1
- package/components/Layout/components/Sidebar/components/sidebar.styles.scss +84 -2
- package/components/Layout/components/Visualization/index.tsx +27 -1
- package/components/Layout/components/Visualization/visualizations.scss +7 -0
- package/components/Legend/Legend.Gradient.tsx +1 -1
- package/components/MediaControls.tsx +53 -28
- package/components/_stories/CustomColorsEditor.stories.tsx +37 -0
- package/components/_stories/DataTable.stories.tsx +1 -0
- package/components/ui/Icon.tsx +3 -1
- package/components/ui/Title/index.tsx +30 -2
- package/components/ui/Title/title.styles.css +42 -0
- package/data/colorPalettes.ts +18 -5
- package/data/mapColorPalettes.ts +10 -0
- package/devTemplate/dev.js +235 -0
- package/devTemplate/index.html +30 -0
- package/devTemplate/preview.html +1503 -0
- package/devTemplate/sidebar.css +151 -0
- package/dist/cove-main.css +2803 -4448
- package/dist/cove-main.css.map +1 -1
- package/generateViteConfig.js +118 -2
- package/helpers/DataTransform.ts +1 -5
- package/helpers/addValuesToFilters.ts +6 -1
- package/helpers/cove/date.ts +33 -1
- package/helpers/cove/string.ts +29 -0
- package/helpers/coveUpdateWorker.ts +21 -12
- package/helpers/embed/embedCodeGenerator.ts +80 -0
- package/helpers/embed/embedHelper.js +158 -0
- package/helpers/embed/filterUtils.ts +121 -0
- package/helpers/embed/index.ts +21 -0
- package/helpers/embed/urlValidation.ts +119 -0
- package/helpers/filterVizData.ts +6 -1
- package/helpers/getFileExtension.ts +0 -6
- package/helpers/getUniqueValues.ts +19 -0
- package/helpers/hashObj.ts +25 -0
- package/helpers/isRightAlignedTableValue.js +5 -0
- package/helpers/metrics/helpers.ts +1 -0
- package/helpers/metrics/types.ts +3 -0
- package/helpers/palettes/colorDistributions.ts +1 -1
- package/helpers/palettes/utils.ts +12 -12
- package/helpers/parseCsvWithQuotes.ts +15 -14
- package/helpers/pivotData.ts +2 -2
- package/helpers/prepareScreenshot.ts +288 -0
- package/helpers/queryStringUtils.ts +29 -0
- package/helpers/testing.ts +44 -0
- package/helpers/tests/DataTransform.test.ts +125 -0
- package/helpers/tests/date.test.ts +64 -0
- package/helpers/tests/prepareScreenshot.test.ts +414 -0
- package/helpers/tests/queryStringUtils.test.ts +381 -0
- package/helpers/tests/testStandaloneBuild.ts +23 -5
- package/helpers/useDataVizClasses.ts +0 -1
- package/helpers/vegaConfig.ts +1 -1
- package/helpers/vegaConfigImport.ts +160 -0
- package/helpers/ver/4.26.1.ts +80 -0
- package/helpers/ver/4.26.2.ts +84 -0
- package/helpers/ver/tests/4.26.1.test.ts +105 -0
- package/helpers/ver/tests/4.26.2.test.ts +298 -0
- package/helpers/viewports.ts +2 -0
- package/hooks/useDataColumns.ts +63 -0
- package/hooks/useFilterManagement.ts +94 -0
- package/hooks/useLegendSeparators.ts +26 -0
- package/hooks/useListManagement.ts +192 -0
- package/package.json +29 -33
- package/styles/_button-section.scss +0 -3
- package/styles/v2/components/editor.scss +9 -9
- package/styles/v2/utils/_grid.scss +8 -3
- package/types/Annotation.ts +10 -11
- package/types/Axis.ts +1 -0
- package/types/ForecastingSeriesKey.ts +1 -0
- package/types/General.ts +2 -0
- package/types/MarkupInclude.ts +1 -0
- package/types/Palette.ts +21 -0
- package/types/Series.ts +3 -0
- package/types/Table.ts +1 -0
- package/types/Visualization.ts +7 -0
- package/types/VizFilter.ts +1 -0
- package/LICENSE +0 -201
- package/_stories/StoryRenderingTests.stories.tsx +0 -164
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for working with COVE filters in embed contexts
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type FilterMetadata = {
|
|
6
|
+
label: string
|
|
7
|
+
key: string
|
|
8
|
+
setByQueryParameter?: string
|
|
9
|
+
values?: any[]
|
|
10
|
+
defaultValue?: string
|
|
11
|
+
active?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type FilterState = {
|
|
15
|
+
value: string
|
|
16
|
+
hide: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Extract filter metadata from a COVE config
|
|
21
|
+
* Handles both regular viz filters and dashboard shared filters
|
|
22
|
+
*/
|
|
23
|
+
export function extractFilters(config: any): FilterMetadata[] {
|
|
24
|
+
if (!config) return []
|
|
25
|
+
|
|
26
|
+
// Try regular filters first (charts, maps, etc.)
|
|
27
|
+
if (config.filters && Array.isArray(config.filters) && config.filters.length > 0) {
|
|
28
|
+
return config.filters.map(filter => normalizeFilter(filter))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Try dashboard shared filters
|
|
32
|
+
if (config.dashboard?.sharedFilters && Array.isArray(config.dashboard.sharedFilters)) {
|
|
33
|
+
return config.dashboard.sharedFilters.map(filter => normalizeFilter(filter))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return []
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Normalize a filter object to consistent metadata format
|
|
41
|
+
*
|
|
42
|
+
* Different filter types (chart filters vs dashboard filters) may use different field names.
|
|
43
|
+
* This function provides fallbacks to handle these variations:
|
|
44
|
+
*
|
|
45
|
+
* - label: Priority for readability: label > setByQueryParameter > columnName
|
|
46
|
+
* - key: May be called key, columnName, or id depending on the viz type
|
|
47
|
+
* - setByQueryParameter: MUST be used exactly as provided. COVE only recognizes this specific
|
|
48
|
+
* field for URL parameters - there are no fallbacks. Filters without this field cannot be
|
|
49
|
+
* controlled via URL parameters.
|
|
50
|
+
*/
|
|
51
|
+
function normalizeFilter(filter: any): FilterMetadata {
|
|
52
|
+
const normalized = {
|
|
53
|
+
label: filter.label || filter.setByQueryParameter || filter.columnName || 'Unnamed Filter',
|
|
54
|
+
key: filter.key || filter.columnName || String(filter.id) || '',
|
|
55
|
+
setByQueryParameter: filter.setByQueryParameter,
|
|
56
|
+
values: filter.values || [],
|
|
57
|
+
defaultValue: filter.defaultValue,
|
|
58
|
+
active: filter.active
|
|
59
|
+
}
|
|
60
|
+
return normalized
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get initial/default value for a filter
|
|
65
|
+
*/
|
|
66
|
+
export function getDefaultFilterValue(filter: FilterMetadata): string {
|
|
67
|
+
if (filter.defaultValue) return filter.defaultValue
|
|
68
|
+
if (filter.active) return filter.active
|
|
69
|
+
if (filter.values && filter.values.length > 0) return filter.values[0]
|
|
70
|
+
return ''
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Initialize filter state from filter metadata
|
|
75
|
+
*/
|
|
76
|
+
export function initializeFilterState(filters: FilterMetadata[]): Record<string, FilterState> {
|
|
77
|
+
const state: Record<string, FilterState> = {}
|
|
78
|
+
|
|
79
|
+
filters.forEach(filter => {
|
|
80
|
+
state[filter.key] = {
|
|
81
|
+
value: getDefaultFilterValue(filter),
|
|
82
|
+
hide: false
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
return state
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Build URL parameters from filter state
|
|
91
|
+
* Returns an object with URL parameters for both filter values and hide states
|
|
92
|
+
*/
|
|
93
|
+
export function buildFilterUrlParams(
|
|
94
|
+
filters: FilterMetadata[],
|
|
95
|
+
filterState: Record<string, FilterState>
|
|
96
|
+
): Record<string, string> {
|
|
97
|
+
const urlParams: Record<string, string> = {}
|
|
98
|
+
|
|
99
|
+
filters.forEach(filter => {
|
|
100
|
+
if (!filter.setByQueryParameter) {
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const state = filterState[filter.key]
|
|
105
|
+
if (!state) {
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Add filter value
|
|
110
|
+
if (state.value) {
|
|
111
|
+
urlParams[filter.setByQueryParameter] = state.value
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Add hide parameter
|
|
115
|
+
if (state.hide) {
|
|
116
|
+
urlParams[`hide${filter.setByQueryParameter}`] = 'true'
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
return urlParams
|
|
121
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Embed-related helpers for COVE visualizations
|
|
3
|
+
* Used for generating embed codes and managing filter customization
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { generateEmbedCode, getEmbedPageUrl, getHelperScriptUrl, isDevMode } from './embedCodeGenerator'
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
extractFilters,
|
|
10
|
+
getDefaultFilterValue,
|
|
11
|
+
initializeFilterState,
|
|
12
|
+
buildFilterUrlParams,
|
|
13
|
+
type FilterMetadata,
|
|
14
|
+
type FilterState
|
|
15
|
+
} from './filterUtils'
|
|
16
|
+
|
|
17
|
+
export { getConfigUrlParam, isValidConfigUrl, isValidMessageOrigin } from './urlValidation'
|
|
18
|
+
|
|
19
|
+
// Import embed helper to initialize iframe resizing
|
|
20
|
+
// This runs immediately when imported
|
|
21
|
+
import './embedHelper.js'
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL validation utilities for embed functionality
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Gets and validates the configUrl parameter from the current URL
|
|
7
|
+
* Returns the validated configUrl if it's a valid relative URL, null otherwise
|
|
8
|
+
*
|
|
9
|
+
* @returns The validated configUrl or null if missing/invalid
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* const configUrl = getConfigUrlParam()
|
|
13
|
+
* if (!configUrl) {
|
|
14
|
+
* // Show error - missing or invalid
|
|
15
|
+
* }
|
|
16
|
+
*/
|
|
17
|
+
export function getConfigUrlParam(): string | null {
|
|
18
|
+
const params = new URLSearchParams(window.location.search)
|
|
19
|
+
const configUrl = params.get('configUrl')
|
|
20
|
+
|
|
21
|
+
if (!configUrl) {
|
|
22
|
+
return null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Validate that it's a relative URL
|
|
26
|
+
if (!isValidConfigUrl(configUrl)) {
|
|
27
|
+
return null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return configUrl
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Validates that a configUrl is a relative URL (no protocol or host)
|
|
35
|
+
*
|
|
36
|
+
* Only relative URLs are allowed to ensure configs
|
|
37
|
+
* are loaded from the same origin as the embed page.
|
|
38
|
+
*
|
|
39
|
+
* @param configUrl - The URL to validate
|
|
40
|
+
* @returns true if the URL is valid (relative only), false otherwise
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* isValidConfigUrl('/path/to/config.json') // true
|
|
44
|
+
* isValidConfigUrl('../config.json') // true
|
|
45
|
+
* isValidConfigUrl('config.json') // true
|
|
46
|
+
* isValidConfigUrl('https://evil.com/config.json') // false
|
|
47
|
+
* isValidConfigUrl('//evil.com/config.json') // false
|
|
48
|
+
* isValidConfigUrl('http://localhost/config.json') // false
|
|
49
|
+
*/
|
|
50
|
+
export function isValidConfigUrl(configUrl: string | null): boolean {
|
|
51
|
+
if (!configUrl || typeof configUrl !== 'string') {
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const trimmed = configUrl.trim()
|
|
56
|
+
|
|
57
|
+
if (trimmed.length === 0) {
|
|
58
|
+
return false
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Reject any URL that contains a protocol (http://, https://, ftp://, etc.)
|
|
62
|
+
if (trimmed.match(/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//)) {
|
|
63
|
+
return false
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Reject protocol-relative URLs (//example.com/path)
|
|
67
|
+
if (trimmed.startsWith('//')) {
|
|
68
|
+
return false
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Reject URLs with protocols but no slashes (javascript:, data:, etc.)
|
|
72
|
+
if (trimmed.match(/^[a-zA-Z][a-zA-Z0-9+.-]*:/)) {
|
|
73
|
+
return false
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Additional validation: Try to parse it as a URL relative to current origin
|
|
77
|
+
try {
|
|
78
|
+
const parsed = new URL(trimmed, window.location.origin)
|
|
79
|
+
|
|
80
|
+
// Verify it's same origin
|
|
81
|
+
if (parsed.origin !== window.location.origin) {
|
|
82
|
+
return false
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return true
|
|
86
|
+
} catch (error) {
|
|
87
|
+
return false
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Validates a postMessage origin against allowed CDC domains
|
|
93
|
+
*
|
|
94
|
+
* @param origin - The origin to validate (from MessageEvent.origin)
|
|
95
|
+
* @returns true if the origin is allowed, false otherwise
|
|
96
|
+
*/
|
|
97
|
+
export function isValidMessageOrigin(origin: string): boolean {
|
|
98
|
+
if (!origin || typeof origin !== 'string') {
|
|
99
|
+
return false
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const url = new URL(origin)
|
|
104
|
+
|
|
105
|
+
// Allow localhost for development (HTTP only)
|
|
106
|
+
if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') {
|
|
107
|
+
return url.protocol === 'http:' || url.protocol === 'https:'
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Allow cdc.gov and any subdomain (HTTPS only)
|
|
111
|
+
if (url.hostname === 'cdc.gov' || url.hostname.endsWith('.cdc.gov')) {
|
|
112
|
+
return url.protocol === 'https:'
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return false
|
|
116
|
+
} catch (error) {
|
|
117
|
+
return false
|
|
118
|
+
}
|
|
119
|
+
}
|
package/helpers/filterVizData.ts
CHANGED
|
@@ -16,10 +16,15 @@ export const filterVizData = (filters: Filter[], data) => {
|
|
|
16
16
|
return []
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
if (!Array.isArray(data)) {
|
|
20
|
+
console.warn('COVE: Data is not an array, received:', typeof data)
|
|
21
|
+
return []
|
|
22
|
+
}
|
|
23
|
+
|
|
19
24
|
if (!filters) return data
|
|
20
25
|
const filteredData: any[] = []
|
|
21
26
|
|
|
22
|
-
data
|
|
27
|
+
data.forEach(row => {
|
|
23
28
|
let add = true
|
|
24
29
|
filters
|
|
25
30
|
.filter(filter => filter.type !== 'url')
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
// export const getFileExtensionx = (path: string): string => {
|
|
2
|
-
// const regex = /(?:\.([^.]+))?$/
|
|
3
|
-
// const outCome: RegExpExecArray | null = regex.exec(path)
|
|
4
|
-
// return outCome ? outCome[1] : ''
|
|
5
|
-
// }
|
|
6
|
-
|
|
7
1
|
export const getFileExtension = (url: string): string => {
|
|
8
2
|
const regexForExtension = /(?:\.([^.]+))$/
|
|
9
3
|
const regexForQueryParam = /[?&]wt=(csv|json)(?:&|$)/ // Regular expression for 'wt' query parameter
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get unique values from a column in a dataset
|
|
3
|
+
* @returns {Array} - The unique values
|
|
4
|
+
*/
|
|
5
|
+
export const getUniqueValues = (data: Array<Record<string, any>>, columnName: string) => {
|
|
6
|
+
let result = {}
|
|
7
|
+
|
|
8
|
+
for (let i = 0; i < data.length; i++) {
|
|
9
|
+
let val = data[i][columnName]
|
|
10
|
+
|
|
11
|
+
if (undefined === val) continue
|
|
12
|
+
|
|
13
|
+
if (undefined === result[val]) {
|
|
14
|
+
result[val] = true
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return Object.keys(result)
|
|
19
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hashes an object
|
|
3
|
+
* @param {Object} row - The object to hash
|
|
4
|
+
* @returns {number} - The hash of the object
|
|
5
|
+
*/
|
|
6
|
+
export const hashObj = row => {
|
|
7
|
+
try {
|
|
8
|
+
if (!row || row === undefined) return null
|
|
9
|
+
|
|
10
|
+
let str = JSON.stringify(row)
|
|
11
|
+
let hash = 0
|
|
12
|
+
|
|
13
|
+
if (str.length === 0) return hash
|
|
14
|
+
|
|
15
|
+
for (let i = 0; i < str.length; i++) {
|
|
16
|
+
let char = str.charCodeAt(i)
|
|
17
|
+
hash = (hash << 5) - hash + char
|
|
18
|
+
hash = hash & hash
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return hash
|
|
22
|
+
} catch (e) {
|
|
23
|
+
console.error({ state: 'COVE: ' + e.message }) // eslint-disable-line
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -12,6 +12,11 @@ export default function isRightAlignedTableValue(value = '') {
|
|
|
12
12
|
if (/^\d{4}-\d{1,2}-\d{1,2}$/.test(value)) {
|
|
13
13
|
return false
|
|
14
14
|
}
|
|
15
|
+
// Years like 1995 and 2014 are not considered numbers
|
|
16
|
+
if (/^(19|20)\d{2}$/.test(value)) {
|
|
17
|
+
return false
|
|
18
|
+
}
|
|
19
|
+
|
|
15
20
|
return numericStrings.includes(value) || /^[\$\d\.\%\,\-\s\(\)CI<>]*$/.test(value)
|
|
16
21
|
}
|
|
17
22
|
return false
|
|
@@ -68,6 +68,7 @@ export const publishAnalyticsEvent = <T extends ANALYTICS_EVENT_TYPES>({
|
|
|
68
68
|
// Format: APP|VIZTYPE_VIZSUBTYPE|VIZ_TITLE|INTERACTION_EVENT_NAME|INTERACTION_TYPE|SPECIFICS
|
|
69
69
|
const vizTypeSubType = vizSubType ? `${vizType}_${vizSubType}` : vizType
|
|
70
70
|
const formattedEvent = `${app}|${vizTypeSubType}|${vizTitle || 'unknown'}|${eventType}|${eventAction}|${specifics || 'no details'}`
|
|
71
|
+
|
|
71
72
|
return publish('cove:analytics', {
|
|
72
73
|
formattedEvent,
|
|
73
74
|
eventLabel
|
package/helpers/metrics/types.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import { map } from 'lodash'
|
|
2
1
|
import { FALLBACK_COLOR_PALETTE_V1, FALLBACK_COLOR_PALETTE_V2, USE_V2_MIGRATION } from '../constants'
|
|
3
2
|
import { getColorPaletteVersion } from '../getColorPaletteVersion'
|
|
4
3
|
import { getPaletteAccessor } from '../getPaletteAccessor'
|
|
5
4
|
import { chartPaletteMigrationMap } from './migratePaletteName'
|
|
6
5
|
import { newMapPaletteNames } from './standardizePaletteNames'
|
|
6
|
+
import { Visualization } from '../../types/Visualization'
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Gets the current palette name from a visualization config
|
|
10
10
|
* @param config - The visualization config object
|
|
11
11
|
* @returns The current palette name or empty string if not found
|
|
12
12
|
*/
|
|
13
|
-
export const getCurrentPaletteName = (config:
|
|
13
|
+
export const getCurrentPaletteName = (config: Partial<Visualization>): string => {
|
|
14
14
|
// Check new v2 format first
|
|
15
15
|
if (config?.general?.palette?.name) {
|
|
16
16
|
return config.general.palette.name
|
|
@@ -35,7 +35,7 @@ export const getCurrentPaletteName = (config: any): string => {
|
|
|
35
35
|
* @param colorPalettes - The color palettes object (e.g., mapColorPalettes, chartColorPalettes)
|
|
36
36
|
* @returns The palette colors array or empty array if not found
|
|
37
37
|
*/
|
|
38
|
-
export const getPaletteColors = (config:
|
|
38
|
+
export const getPaletteColors = (config: Partial<Visualization>, colorPalettes: Record<string, Record<string, string[]>>): string[] => {
|
|
39
39
|
// First check for custom colors (v2 format)
|
|
40
40
|
if (config?.general?.palette?.customColors) {
|
|
41
41
|
return config.general.palette.customColors
|
|
@@ -65,7 +65,7 @@ export const getPaletteColors = (config: any, colorPalettes: any): string[] => {
|
|
|
65
65
|
* @param config - The visualization config object
|
|
66
66
|
* @returns True if the config is using v1 palette configuration (which would show conversion modal)
|
|
67
67
|
*/
|
|
68
|
-
export const isV1Palette = (config:
|
|
68
|
+
export const isV1Palette = (config: Partial<Visualization>): boolean => {
|
|
69
69
|
// If v2 migration is disabled globally, don't treat as v1 (no conversion modal)
|
|
70
70
|
if (!USE_V2_MIGRATION) {
|
|
71
71
|
return false
|
|
@@ -84,7 +84,7 @@ export const isV1Palette = (config: any): boolean => {
|
|
|
84
84
|
* @param config - The visualization config object
|
|
85
85
|
* @returns The fallback palette name for the detected version
|
|
86
86
|
*/
|
|
87
|
-
export const getFallbackColorPalette = (config:
|
|
87
|
+
export const getFallbackColorPalette = (config: Partial<Visualization>): string => {
|
|
88
88
|
const paletteVersion = getColorPaletteVersion(config)
|
|
89
89
|
return paletteVersion === 1 ? FALLBACK_COLOR_PALETTE_V1 : FALLBACK_COLOR_PALETTE_V2
|
|
90
90
|
}
|
|
@@ -161,7 +161,7 @@ export const migratePaletteWithMap = (
|
|
|
161
161
|
* @param config - The visualization config object
|
|
162
162
|
* @returns True if backup data exists
|
|
163
163
|
*/
|
|
164
|
-
export const hasPaletteBackup = (config:
|
|
164
|
+
export const hasPaletteBackup = (config: Partial<Visualization>): boolean => {
|
|
165
165
|
return !!(config?.general?.palette?.backups?.length > 0)
|
|
166
166
|
}
|
|
167
167
|
|
|
@@ -170,7 +170,7 @@ export const hasPaletteBackup = (config: any): boolean => {
|
|
|
170
170
|
* @param config - The visualization config object
|
|
171
171
|
* @returns The original palette name or null if no backup exists
|
|
172
172
|
*/
|
|
173
|
-
export const getOriginalPaletteName = (config:
|
|
173
|
+
export const getOriginalPaletteName = (config: Partial<Visualization>): string | null => {
|
|
174
174
|
const backups = config?.general?.palette?.backups
|
|
175
175
|
if (!backups || backups.length === 0) return null
|
|
176
176
|
|
|
@@ -184,7 +184,7 @@ export const getOriginalPaletteName = (config: any): string | null => {
|
|
|
184
184
|
* @param config - The visualization config object
|
|
185
185
|
* @returns The original two-color palette name or null if no backup exists
|
|
186
186
|
*/
|
|
187
|
-
export const getOriginalTwoColorPaletteName = (config:
|
|
187
|
+
export const getOriginalTwoColorPaletteName = (config: Partial<Visualization>): string | null => {
|
|
188
188
|
const backups = config?.general?.palette?.backups
|
|
189
189
|
if (!backups || backups.length === 0) return null
|
|
190
190
|
|
|
@@ -198,7 +198,7 @@ export const getOriginalTwoColorPaletteName = (config: any): string | null => {
|
|
|
198
198
|
* @param config - The visualization config object
|
|
199
199
|
* @returns True if two-color backup data exists
|
|
200
200
|
*/
|
|
201
|
-
export const hasTwoColorPaletteBackup = (config:
|
|
201
|
+
export const hasTwoColorPaletteBackup = (config: Partial<Visualization>): boolean => {
|
|
202
202
|
const backups = config?.general?.palette?.backups
|
|
203
203
|
if (!backups || backups.length === 0) return false
|
|
204
204
|
return backups.some((backup: any) => backup.type === 'twoColor')
|
|
@@ -209,7 +209,7 @@ export const hasTwoColorPaletteBackup = (config: any): boolean => {
|
|
|
209
209
|
* @param config - The visualization config object to modify
|
|
210
210
|
* @returns True if rollback was successful, false if no backup available
|
|
211
211
|
*/
|
|
212
|
-
export const rollbackPaletteToOriginal = (config:
|
|
212
|
+
export const rollbackPaletteToOriginal = (config: Partial<Visualization>): boolean => {
|
|
213
213
|
const backups = config?.general?.palette?.backups
|
|
214
214
|
if (!backups || backups.length === 0) {
|
|
215
215
|
return false
|
|
@@ -230,7 +230,7 @@ export const rollbackPaletteToOriginal = (config: any): boolean => {
|
|
|
230
230
|
config.general.palette.version = '1.0' // Reset to v1
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
-
return
|
|
233
|
+
return true
|
|
234
234
|
}
|
|
235
235
|
|
|
236
236
|
/**
|
|
@@ -238,7 +238,7 @@ export const rollbackPaletteToOriginal = (config: any): boolean => {
|
|
|
238
238
|
* @param config - The visualization config object to modify
|
|
239
239
|
* @returns True if rollback was successful, false if no backup available
|
|
240
240
|
*/
|
|
241
|
-
export const rollbackTwoColorPaletteToOriginal = (config:
|
|
241
|
+
export const rollbackTwoColorPaletteToOriginal = (config: Partial<Visualization>): boolean => {
|
|
242
242
|
const backups = config?.general?.palette?.backups
|
|
243
243
|
if (!backups || backups.length === 0) {
|
|
244
244
|
return false
|
|
@@ -2,7 +2,7 @@ import Papa from 'papaparse'
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Parses CSV text while preserving newlines and commas within quoted fields.
|
|
5
|
-
*
|
|
5
|
+
*
|
|
6
6
|
* @param responseText - The raw CSV text to parse
|
|
7
7
|
* @param options - Parsing options
|
|
8
8
|
* @param options.delimiter - The delimiter to use after processing (default: '|')
|
|
@@ -17,27 +17,28 @@ export function parseCsvWithQuotes(
|
|
|
17
17
|
} = {}
|
|
18
18
|
): any[] {
|
|
19
19
|
const { delimiter = '|', dynamicTyping = false } = options
|
|
20
|
-
|
|
20
|
+
|
|
21
21
|
const NEWLINE_PLACEHOLDER = '__COVE_NEWLINE__'
|
|
22
|
-
|
|
22
|
+
const newlinePlaceholderRegex = new RegExp(NEWLINE_PLACEHOLDER, 'g')
|
|
23
|
+
|
|
23
24
|
// Preserve newlines in quoted fields by replacing with placeholder
|
|
24
25
|
const quotedFields: string[] = []
|
|
25
26
|
let placeholderIndex = 0
|
|
26
|
-
let sanitizedText = responseText.replace(/("(?:[^"\\]|\\.|[\s\S])*?")/g,
|
|
27
|
+
let sanitizedText = responseText.replace(/("(?:[^"\\]|\\.|[\s\S])*?")/g, match => {
|
|
27
28
|
const preserved = match.replace(/\n/g, NEWLINE_PLACEHOLDER)
|
|
28
29
|
quotedFields.push(preserved)
|
|
29
30
|
return `__QUOTED_FIELD_${placeholderIndex++}__`
|
|
30
31
|
})
|
|
31
|
-
|
|
32
|
+
|
|
32
33
|
// Replace commas outside quoted fields with pipe delimiter
|
|
33
34
|
sanitizedText = sanitizedText.replace(/(__QUOTED_FIELD_\d+__)|,/g, (...m) => m[1] || delimiter)
|
|
34
|
-
|
|
35
|
-
// Restore quoted fields without outer quotes
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
35
|
+
|
|
36
|
+
// Restore quoted fields without outer quotes (single pass instead of N passes)
|
|
37
|
+
sanitizedText = sanitizedText.replace(/__QUOTED_FIELD_(\d+)__/g, (_, idx) => {
|
|
38
|
+
const field = quotedFields[parseInt(idx, 10)]
|
|
39
|
+
return field.slice(1, -1).replace(newlinePlaceholderRegex, '\n')
|
|
39
40
|
})
|
|
40
|
-
|
|
41
|
+
|
|
41
42
|
// Parse with Papa.parse
|
|
42
43
|
const parsedCsv = Papa.parse(sanitizedText, {
|
|
43
44
|
header: true,
|
|
@@ -45,21 +46,21 @@ export function parseCsvWithQuotes(
|
|
|
45
46
|
delimiter,
|
|
46
47
|
dynamicTyping
|
|
47
48
|
})
|
|
48
|
-
|
|
49
|
+
|
|
49
50
|
// Restore newlines in parsed data
|
|
50
51
|
const restoredData = parsedCsv.data.map((row: any) => {
|
|
51
52
|
const restoredRow: any = {}
|
|
52
53
|
Object.keys(row).forEach(key => {
|
|
53
54
|
const value = row[key]
|
|
54
55
|
if (typeof value === 'string') {
|
|
55
|
-
restoredRow[key] = value.replace(
|
|
56
|
+
restoredRow[key] = value.replace(newlinePlaceholderRegex, '\n')
|
|
56
57
|
} else {
|
|
57
58
|
restoredRow[key] = value
|
|
58
59
|
}
|
|
59
60
|
})
|
|
60
61
|
return restoredRow
|
|
61
62
|
})
|
|
62
|
-
|
|
63
|
+
|
|
63
64
|
return restoredData
|
|
64
65
|
}
|
|
65
66
|
|
package/helpers/pivotData.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import _ from 'lodash'
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const getNonPivotColumns = (data: Record<string, any>[], columnName: string, pivot: string[], excludeColumns: string[]) => {
|
|
4
4
|
const excludedColumns = [columnName, ...pivot, ...excludeColumns]
|
|
5
5
|
return _.uniq(data.flatMap(row => Object.keys(row))).filter(col => !excludedColumns.includes(col))
|
|
6
6
|
}
|
|
@@ -13,7 +13,7 @@ export const pivotData = (
|
|
|
13
13
|
pivot: string[],
|
|
14
14
|
excludeColumns: string[]
|
|
15
15
|
): Record<string, any>[] => {
|
|
16
|
-
const columns =
|
|
16
|
+
const columns = getNonPivotColumns(data, columnName, pivot, excludeColumns)
|
|
17
17
|
const newColumns = data.reduce((acc, row) => {
|
|
18
18
|
acc[row[columnName]] = ''
|
|
19
19
|
return acc
|