@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,84 @@
|
|
|
1
|
+
import cloneConfig from '../cloneConfig'
|
|
2
|
+
import { DashboardConfig } from '@cdc/dashboard/src/types/DashboardConfig'
|
|
3
|
+
|
|
4
|
+
const migrateAnnotationDimensions = config => {
|
|
5
|
+
if (config.annotations && Array.isArray(config.annotations)) {
|
|
6
|
+
// Calculate chart area height for Y conversion (matches calcInitialHeight)
|
|
7
|
+
const isHorizontal = config.orientation === 'horizontal'
|
|
8
|
+
const chartAreaHeight = isHorizontal
|
|
9
|
+
? Number(config.heights?.horizontal) || 750 // default horizontal height
|
|
10
|
+
: Number(config.heights?.vertical) || 300 // default vertical height
|
|
11
|
+
|
|
12
|
+
config.annotations = config.annotations.map(annotation => {
|
|
13
|
+
if (!annotation) return annotation
|
|
14
|
+
if (annotation.y !== undefined && chartAreaHeight > 0) {
|
|
15
|
+
// Convert Y from pixels to percentage using the chart area height
|
|
16
|
+
annotation.y = (annotation.y / chartAreaHeight) * 100
|
|
17
|
+
} else {
|
|
18
|
+
annotation.y = 50
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Delete savedDimensions to preserve old dx/dy behavior (fixed pixel offsets).
|
|
22
|
+
// The scaling function falls back to raw dx/dy when savedDimensions is missing.
|
|
23
|
+
// Once user drags the annotation, savedDimensions will be set with current chart
|
|
24
|
+
// dimensions, enabling responsive scaling from that point forward.
|
|
25
|
+
delete annotation.savedDimensions
|
|
26
|
+
|
|
27
|
+
return annotation
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (config.type === 'dashboard' && config.visualizations) {
|
|
32
|
+
Object.values((config as DashboardConfig).visualizations).forEach(visualization => {
|
|
33
|
+
migrateAnnotationDimensions(visualization)
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const migrateAnnotationDataModel = config => {
|
|
39
|
+
if (config.annotations && Array.isArray(config.annotations)) {
|
|
40
|
+
config.annotations = config.annotations.map(annotation => {
|
|
41
|
+
if (!annotation) return annotation
|
|
42
|
+
// Set all existing annotations to fixed mode
|
|
43
|
+
annotation.anchorMode = 'fixed'
|
|
44
|
+
|
|
45
|
+
// Delete xKey entirely - old format stored timestamps for dates,
|
|
46
|
+
// but new dataX expects raw data values. Format is incompatible.
|
|
47
|
+
// User can re-enable data mode which will set fresh dataX value.
|
|
48
|
+
delete annotation.xKey
|
|
49
|
+
|
|
50
|
+
// Delete yKey entirely (Y will be calculated dynamically in data mode)
|
|
51
|
+
delete annotation.yKey
|
|
52
|
+
|
|
53
|
+
// Delete empty seriesKey - it would cause yScale(undefined) errors in data mode
|
|
54
|
+
if (!annotation.seriesKey) {
|
|
55
|
+
delete annotation.seriesKey
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Delete deprecated properties
|
|
59
|
+
delete annotation.snapToNearestPoint
|
|
60
|
+
delete annotation.originalX
|
|
61
|
+
delete annotation.originalDX
|
|
62
|
+
delete annotation.originalY
|
|
63
|
+
|
|
64
|
+
return annotation
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (config.type === 'dashboard' && config.visualizations) {
|
|
69
|
+
Object.values((config as DashboardConfig).visualizations).forEach(visualization => {
|
|
70
|
+
migrateAnnotationDataModel(visualization)
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const update_4_26_2 = config => {
|
|
76
|
+
const ver = '4.26.2'
|
|
77
|
+
const newConfig = cloneConfig(config)
|
|
78
|
+
migrateAnnotationDimensions(newConfig)
|
|
79
|
+
migrateAnnotationDataModel(newConfig)
|
|
80
|
+
newConfig.version = ver
|
|
81
|
+
return newConfig
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export default update_4_26_2
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import update_4_26_1 from '../4.26.1'
|
|
2
|
+
import { expect, describe, it } from 'vitest'
|
|
3
|
+
|
|
4
|
+
describe('update_4_26_1', () => {
|
|
5
|
+
describe('normalizeFilterParents', () => {
|
|
6
|
+
it('should convert string parents to array in shared filters', () => {
|
|
7
|
+
const config: any = {
|
|
8
|
+
type: 'dashboard',
|
|
9
|
+
version: '4.26.0',
|
|
10
|
+
dashboard: {
|
|
11
|
+
sharedFilters: [
|
|
12
|
+
{
|
|
13
|
+
type: 'datafilter',
|
|
14
|
+
parents: 'parent-filter-id'
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const result = update_4_26_1(config)
|
|
21
|
+
|
|
22
|
+
expect(result.dashboard.sharedFilters[0].parents).toEqual(['parent-filter-id'])
|
|
23
|
+
expect(result.version).toBe('4.26.1')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('should leave array parents unchanged', () => {
|
|
27
|
+
const config: any = {
|
|
28
|
+
type: 'dashboard',
|
|
29
|
+
version: '4.26.0',
|
|
30
|
+
dashboard: {
|
|
31
|
+
sharedFilters: [
|
|
32
|
+
{
|
|
33
|
+
type: 'datafilter',
|
|
34
|
+
parents: ['parent1', 'parent2']
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const result = update_4_26_1(config)
|
|
41
|
+
|
|
42
|
+
expect(result.dashboard.sharedFilters[0].parents).toEqual(['parent1', 'parent2'])
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
describe('removeOldBrushKeys', () => {
|
|
47
|
+
it('should remove brush config from chart', () => {
|
|
48
|
+
const config: any = {
|
|
49
|
+
type: 'chart',
|
|
50
|
+
version: '4.26.0',
|
|
51
|
+
brush: { enabled: true }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const result = update_4_26_1(config)
|
|
55
|
+
|
|
56
|
+
expect(result.brush).toBeUndefined()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('should remove brush config from dashboard visualizations', () => {
|
|
60
|
+
const config: any = {
|
|
61
|
+
type: 'dashboard',
|
|
62
|
+
version: '4.26.0',
|
|
63
|
+
visualizations: {
|
|
64
|
+
chart1: {
|
|
65
|
+
type: 'chart',
|
|
66
|
+
brush: { enabled: true }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const result = update_4_26_1(config)
|
|
72
|
+
|
|
73
|
+
expect(result.visualizations.chart1.brush).toBeUndefined()
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
describe('combined migrations', () => {
|
|
78
|
+
it('should run all migrations together', () => {
|
|
79
|
+
const config: any = {
|
|
80
|
+
type: 'dashboard',
|
|
81
|
+
version: '4.26.0',
|
|
82
|
+
dashboard: {
|
|
83
|
+
sharedFilters: [
|
|
84
|
+
{
|
|
85
|
+
type: 'datafilter',
|
|
86
|
+
parents: 'parent-id'
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
},
|
|
90
|
+
visualizations: {
|
|
91
|
+
chart1: {
|
|
92
|
+
type: 'chart',
|
|
93
|
+
brush: { enabled: true }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const result = update_4_26_1(config)
|
|
99
|
+
|
|
100
|
+
expect(result.dashboard.sharedFilters[0].parents).toEqual(['parent-id'])
|
|
101
|
+
expect(result.visualizations.chart1.brush).toBeUndefined()
|
|
102
|
+
expect(result.version).toBe('4.26.1')
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
})
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import update_4_26_2 from '../4.26.2'
|
|
2
|
+
import { expect, describe, it } from 'vitest'
|
|
3
|
+
|
|
4
|
+
describe('update_4_26_2', () => {
|
|
5
|
+
describe('migrateAnnotationDimensions', () => {
|
|
6
|
+
it('should convert Y position from absolute pixels to percentage and delete savedDimensions', () => {
|
|
7
|
+
const config: any = {
|
|
8
|
+
type: 'chart',
|
|
9
|
+
version: '4.26.1',
|
|
10
|
+
heights: { vertical: 400 },
|
|
11
|
+
annotations: [
|
|
12
|
+
{
|
|
13
|
+
text: 'Test Annotation',
|
|
14
|
+
x: 50,
|
|
15
|
+
y: 200,
|
|
16
|
+
savedDimensions: [800, 560], // old format: will be deleted
|
|
17
|
+
dx: 10,
|
|
18
|
+
dy: -10
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const result = update_4_26_2(config)
|
|
24
|
+
|
|
25
|
+
// Y is now calculated as percentage of heights.vertical (400)
|
|
26
|
+
expect(result.annotations[0].y).toBe(50) // 200 / 400 * 100 = 50
|
|
27
|
+
expect(result.annotations[0].x).toBe(50)
|
|
28
|
+
// savedDimensions should be deleted to preserve old dx/dy behavior
|
|
29
|
+
expect(result.annotations[0].savedDimensions).toBeUndefined()
|
|
30
|
+
// dx/dy should be preserved unchanged
|
|
31
|
+
expect(result.annotations[0].dx).toBe(10)
|
|
32
|
+
expect(result.annotations[0].dy).toBe(-10)
|
|
33
|
+
expect(result.version).toBe('4.26.2')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('should handle multiple annotations with different Y positions', () => {
|
|
37
|
+
const config: any = {
|
|
38
|
+
type: 'chart',
|
|
39
|
+
version: '4.26.1',
|
|
40
|
+
heights: { vertical: 500 },
|
|
41
|
+
annotations: [
|
|
42
|
+
{
|
|
43
|
+
text: 'Top',
|
|
44
|
+
x: 25,
|
|
45
|
+
y: 100,
|
|
46
|
+
savedDimensions: [1000, 700]
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
text: 'Middle',
|
|
50
|
+
x: 75,
|
|
51
|
+
y: 250,
|
|
52
|
+
savedDimensions: [1000, 700]
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const result = update_4_26_2(config)
|
|
58
|
+
|
|
59
|
+
// Y calculated as percentage of heights.vertical (500)
|
|
60
|
+
expect(result.annotations[0].y).toBe(20) // 100 / 500 * 100 = 20
|
|
61
|
+
expect(result.annotations[1].y).toBe(50) // 250 / 500 * 100 = 50
|
|
62
|
+
// savedDimensions should be deleted
|
|
63
|
+
expect(result.annotations[0].savedDimensions).toBeUndefined()
|
|
64
|
+
expect(result.annotations[1].savedDimensions).toBeUndefined()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('should use default vertical height (300) when heights not specified', () => {
|
|
68
|
+
const config: any = {
|
|
69
|
+
type: 'chart',
|
|
70
|
+
version: '4.26.1',
|
|
71
|
+
annotations: [
|
|
72
|
+
{
|
|
73
|
+
text: 'No heights config',
|
|
74
|
+
x: 50,
|
|
75
|
+
y: 150,
|
|
76
|
+
savedDimensions: [800, 560]
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const result = update_4_26_2(config)
|
|
82
|
+
|
|
83
|
+
// Uses default vertical height of 300
|
|
84
|
+
expect(result.annotations[0].y).toBe(50) // 150 / 300 * 100 = 50
|
|
85
|
+
expect(result.annotations[0].savedDimensions).toBeUndefined()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('should use horizontal height for horizontal orientation', () => {
|
|
89
|
+
const config: any = {
|
|
90
|
+
type: 'chart',
|
|
91
|
+
version: '4.26.1',
|
|
92
|
+
orientation: 'horizontal',
|
|
93
|
+
heights: { horizontal: 750 },
|
|
94
|
+
annotations: [
|
|
95
|
+
{
|
|
96
|
+
text: 'Horizontal chart annotation',
|
|
97
|
+
x: 50,
|
|
98
|
+
y: 375,
|
|
99
|
+
savedDimensions: [800, 900]
|
|
100
|
+
}
|
|
101
|
+
]
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const result = update_4_26_2(config)
|
|
105
|
+
|
|
106
|
+
// Uses heights.horizontal for horizontal charts
|
|
107
|
+
expect(result.annotations[0].y).toBe(50) // 375 / 750 * 100 = 50
|
|
108
|
+
expect(result.annotations[0].savedDimensions).toBeUndefined()
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('should handle dashboard configs with nested visualizations', () => {
|
|
112
|
+
const config: any = {
|
|
113
|
+
type: 'dashboard',
|
|
114
|
+
version: '4.26.1',
|
|
115
|
+
visualizations: {
|
|
116
|
+
chart1: {
|
|
117
|
+
type: 'chart',
|
|
118
|
+
heights: { vertical: 300 },
|
|
119
|
+
annotations: [
|
|
120
|
+
{
|
|
121
|
+
text: 'Chart 1 Annotation',
|
|
122
|
+
x: 30,
|
|
123
|
+
y: 150,
|
|
124
|
+
savedDimensions: [600, 450]
|
|
125
|
+
}
|
|
126
|
+
]
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const result = update_4_26_2(config)
|
|
132
|
+
|
|
133
|
+
expect(result.visualizations.chart1.annotations[0].y).toBe(50) // 150 / 300 * 100 = 50
|
|
134
|
+
expect(result.visualizations.chart1.annotations[0].savedDimensions).toBeUndefined()
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('should preserve all other annotation properties and delete xKey entirely', () => {
|
|
138
|
+
const config: any = {
|
|
139
|
+
type: 'chart',
|
|
140
|
+
version: '4.26.1',
|
|
141
|
+
heights: { vertical: 500 },
|
|
142
|
+
annotations: [
|
|
143
|
+
{
|
|
144
|
+
text: 'Complex Annotation',
|
|
145
|
+
x: 50,
|
|
146
|
+
y: 250,
|
|
147
|
+
savedDimensions: [1000, 700],
|
|
148
|
+
dx: 20,
|
|
149
|
+
dy: -30,
|
|
150
|
+
xKey: 1577836800000,
|
|
151
|
+
yKey: '42',
|
|
152
|
+
seriesKey: 'series1',
|
|
153
|
+
marker: 'arrow'
|
|
154
|
+
}
|
|
155
|
+
]
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const result = update_4_26_2(config)
|
|
159
|
+
|
|
160
|
+
const annotation = result.annotations[0]
|
|
161
|
+
expect(annotation.y).toBe(50) // 250 / 500 * 100 = 50
|
|
162
|
+
expect(annotation.text).toBe('Complex Annotation')
|
|
163
|
+
expect(annotation.dx).toBe(20)
|
|
164
|
+
expect(annotation.dy).toBe(-30)
|
|
165
|
+
// xKey is deleted entirely (not renamed to dataX) because old format
|
|
166
|
+
// stored timestamps but new dataX expects raw data values
|
|
167
|
+
expect(annotation.xKey).toBeUndefined()
|
|
168
|
+
expect(annotation.dataX).toBeUndefined()
|
|
169
|
+
expect(annotation.yKey).toBeUndefined()
|
|
170
|
+
// Non-empty seriesKey is preserved
|
|
171
|
+
expect(annotation.seriesKey).toBe('series1')
|
|
172
|
+
expect(annotation.savedDimensions).toBeUndefined()
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('should delete empty seriesKey', () => {
|
|
176
|
+
const config: any = {
|
|
177
|
+
type: 'chart',
|
|
178
|
+
version: '4.26.1',
|
|
179
|
+
heights: { vertical: 300 },
|
|
180
|
+
annotations: [
|
|
181
|
+
{
|
|
182
|
+
text: 'Annotation with empty seriesKey',
|
|
183
|
+
x: 50,
|
|
184
|
+
y: 150,
|
|
185
|
+
seriesKey: ''
|
|
186
|
+
}
|
|
187
|
+
]
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const result = update_4_26_2(config)
|
|
191
|
+
|
|
192
|
+
// Empty seriesKey should be deleted to prevent yScale(undefined) errors
|
|
193
|
+
expect(result.annotations[0].seriesKey).toBeUndefined()
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('should handle config with no annotations', () => {
|
|
197
|
+
const config: any = {
|
|
198
|
+
type: 'chart',
|
|
199
|
+
version: '4.26.1',
|
|
200
|
+
title: 'Chart without annotations'
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const result = update_4_26_2(config)
|
|
204
|
+
|
|
205
|
+
expect(result.annotations).toBeUndefined()
|
|
206
|
+
expect(result.title).toBe('Chart without annotations')
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('should handle annotation without savedDimensions', () => {
|
|
210
|
+
const config: any = {
|
|
211
|
+
type: 'chart',
|
|
212
|
+
version: '4.26.1',
|
|
213
|
+
heights: { vertical: 300 },
|
|
214
|
+
annotations: [
|
|
215
|
+
{
|
|
216
|
+
text: 'No savedDimensions',
|
|
217
|
+
x: 50,
|
|
218
|
+
y: 150
|
|
219
|
+
}
|
|
220
|
+
]
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const result = update_4_26_2(config)
|
|
224
|
+
|
|
225
|
+
expect(result.annotations[0].y).toBe(50) // 150 / 300 * 100 = 50
|
|
226
|
+
expect(result.annotations[0].savedDimensions).toBeUndefined()
|
|
227
|
+
})
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
describe('combined migrations', () => {
|
|
231
|
+
it('should run all migrations together', () => {
|
|
232
|
+
const config: any = {
|
|
233
|
+
type: 'dashboard',
|
|
234
|
+
version: '4.26.1',
|
|
235
|
+
dashboard: {
|
|
236
|
+
sharedFilters: [
|
|
237
|
+
{
|
|
238
|
+
type: 'datafilter',
|
|
239
|
+
parents: 'parent-id'
|
|
240
|
+
}
|
|
241
|
+
]
|
|
242
|
+
},
|
|
243
|
+
visualizations: {
|
|
244
|
+
chart1: {
|
|
245
|
+
type: 'chart',
|
|
246
|
+
heights: { vertical: 400 },
|
|
247
|
+
brush: { enabled: true },
|
|
248
|
+
annotations: [
|
|
249
|
+
{
|
|
250
|
+
x: 50,
|
|
251
|
+
y: 200,
|
|
252
|
+
savedDimensions: [800, 560]
|
|
253
|
+
}
|
|
254
|
+
]
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const result = update_4_26_2(config)
|
|
260
|
+
|
|
261
|
+
expect(result.visualizations.chart1.annotations[0].y).toBe(50) // 200 / 400 * 100 = 50
|
|
262
|
+
expect(result.visualizations.chart1.annotations[0].anchorMode).toBe('fixed')
|
|
263
|
+
expect(result.visualizations.chart1.annotations[0].savedDimensions).toBeUndefined()
|
|
264
|
+
expect(result.visualizations.chart1.annotations[0].dataX).toBeUndefined()
|
|
265
|
+
expect(result.version).toBe('4.26.2')
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
it('should migrate data model along with Y position', () => {
|
|
269
|
+
const config: any = {
|
|
270
|
+
type: 'chart',
|
|
271
|
+
version: '4.26.1',
|
|
272
|
+
heights: { vertical: 400 },
|
|
273
|
+
annotations: [
|
|
274
|
+
{
|
|
275
|
+
x: 50,
|
|
276
|
+
y: 200,
|
|
277
|
+
savedDimensions: [800, 560],
|
|
278
|
+
xKey: 1577836800000,
|
|
279
|
+
yKey: '42',
|
|
280
|
+
snapToNearestPoint: true
|
|
281
|
+
}
|
|
282
|
+
]
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const result = update_4_26_2(config)
|
|
286
|
+
|
|
287
|
+
const annotation = result.annotations[0]
|
|
288
|
+
expect(annotation.y).toBe(50) // 200 / 400 * 100 = 50
|
|
289
|
+
expect(annotation.anchorMode).toBe('fixed')
|
|
290
|
+
// xKey is deleted entirely (not renamed to dataX) - format incompatible
|
|
291
|
+
expect(annotation.dataX).toBeUndefined()
|
|
292
|
+
expect(annotation.xKey).toBeUndefined()
|
|
293
|
+
expect(annotation.yKey).toBeUndefined()
|
|
294
|
+
expect(annotation.snapToNearestPoint).toBeUndefined()
|
|
295
|
+
expect(annotation.savedDimensions).toBeUndefined()
|
|
296
|
+
})
|
|
297
|
+
})
|
|
298
|
+
})
|
package/helpers/viewports.ts
CHANGED
|
@@ -15,4 +15,6 @@ export const isMobileTerritoryViewport = currentViewport => isBelowBreakpoint('s
|
|
|
15
15
|
|
|
16
16
|
export const isMobileFontViewport = currentViewport => isBelowBreakpoint('sm', currentViewport)
|
|
17
17
|
|
|
18
|
+
export const isMobileAnnotationViewport = currentViewport => isBelowBreakpoint('sm', currentViewport)
|
|
19
|
+
|
|
18
20
|
export const isMobileSmallMultiplesViewport = currentViewport => isBelowBreakpoint('md', currentViewport)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
|
|
3
|
+
export interface UseDataColumnsOptions {
|
|
4
|
+
/** Columns to exclude from the result */
|
|
5
|
+
excludeColumns?: string[]
|
|
6
|
+
/** Include only columns with specific data types */
|
|
7
|
+
dataTypes?: ('string' | 'number' | 'boolean' | 'date')[]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Extracts unique column names from data with memoization
|
|
12
|
+
*
|
|
13
|
+
* Performance optimization: Replaces the common getColumns() pattern
|
|
14
|
+
* that was duplicated across multiple packages and called multiple times per render.
|
|
15
|
+
*
|
|
16
|
+
* @param data - Array of data objects
|
|
17
|
+
* @param options - Optional configuration for filtering columns
|
|
18
|
+
* @returns Sorted array of unique column names
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* // Basic usage
|
|
22
|
+
* const columns = useDataColumns(data)
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* // With exclusions
|
|
26
|
+
* const columns = useDataColumns(data, { excludeColumns: [config.groupBy] })
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* // Filter by data type
|
|
30
|
+
* const numericColumns = useDataColumns(data, { dataTypes: ['number'] })
|
|
31
|
+
*/
|
|
32
|
+
export const useDataColumns = (data: any[], options?: UseDataColumnsOptions): string[] => {
|
|
33
|
+
const { excludeColumns = [], dataTypes } = options || {}
|
|
34
|
+
|
|
35
|
+
return useMemo(() => {
|
|
36
|
+
if (!data?.length) return []
|
|
37
|
+
|
|
38
|
+
const columnsSet = new Set<string>()
|
|
39
|
+
|
|
40
|
+
// Single iteration through all rows (optimized from previous pattern)
|
|
41
|
+
data.forEach(row => {
|
|
42
|
+
Object.keys(row).forEach(columnName => {
|
|
43
|
+
if (excludeColumns.includes(columnName)) return
|
|
44
|
+
|
|
45
|
+
// Optional: filter by data type
|
|
46
|
+
if (dataTypes && dataTypes.length > 0) {
|
|
47
|
+
const value = row[columnName]
|
|
48
|
+
const valueType = typeof value
|
|
49
|
+
if (!dataTypes.includes(valueType as any)) return
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
columnsSet.add(columnName)
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
return Array.from(columnsSet).sort()
|
|
57
|
+
}, [
|
|
58
|
+
data,
|
|
59
|
+
// Stringify arrays for stable dependency tracking
|
|
60
|
+
excludeColumns.join(','),
|
|
61
|
+
dataTypes?.join(',')
|
|
62
|
+
])
|
|
63
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom hook for managing filter operations in visualizations
|
|
3
|
+
*
|
|
4
|
+
* Provides common filter management functionality including:
|
|
5
|
+
* - Adding new filters
|
|
6
|
+
* - Removing filters
|
|
7
|
+
* - Updating filter properties
|
|
8
|
+
* - Getting unique column values for filter options
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```tsx
|
|
12
|
+
* const { addNewFilter, removeFilter, updateFilterProp, getFilterColumnValues } =
|
|
13
|
+
* useFilterManagement(config, updateConfig, data)
|
|
14
|
+
*
|
|
15
|
+
* // Add a new filter
|
|
16
|
+
* <Button onClick={addNewFilter}>Add Filter</Button>
|
|
17
|
+
*
|
|
18
|
+
* // Remove a filter
|
|
19
|
+
* <button onClick={() => removeFilter(index)}>Remove</button>
|
|
20
|
+
*
|
|
21
|
+
* // Update filter property
|
|
22
|
+
* <Select onChange={e => updateFilterProp('columnName', index, e.target.value)} />
|
|
23
|
+
*
|
|
24
|
+
* // Get column values for filter dropdown
|
|
25
|
+
* <Select options={getFilterColumnValues(index)} />
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export const useFilterManagement = <TConfig extends { filters?: any[] }>(
|
|
29
|
+
config: TConfig,
|
|
30
|
+
updateConfig: (config: TConfig) => void,
|
|
31
|
+
data: any[]
|
|
32
|
+
) => {
|
|
33
|
+
/**
|
|
34
|
+
* Adds a new empty filter to the config
|
|
35
|
+
*/
|
|
36
|
+
const addNewFilter = () => {
|
|
37
|
+
const filters = config.filters ? [...config.filters] : []
|
|
38
|
+
filters.push({ values: [] })
|
|
39
|
+
updateConfig({ ...config, filters })
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Removes a filter at the specified index
|
|
44
|
+
* @param index - The index of the filter to remove
|
|
45
|
+
*/
|
|
46
|
+
const removeFilter = (index: number) => {
|
|
47
|
+
const filters = [...(config.filters || [])]
|
|
48
|
+
filters.splice(index, 1)
|
|
49
|
+
updateConfig({ ...config, filters })
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Updates a specific property of a filter
|
|
54
|
+
* @param name - The property name to update
|
|
55
|
+
* @param index - The index of the filter
|
|
56
|
+
* @param value - The new value for the property
|
|
57
|
+
*/
|
|
58
|
+
const updateFilterProp = (name: string, index: number, value: any) => {
|
|
59
|
+
const filters = [...(config.filters || [])]
|
|
60
|
+
filters[index][name] = value
|
|
61
|
+
updateConfig({ ...config, filters })
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Gets unique values from a data column for filter options
|
|
66
|
+
* @param index - The index of the filter (to get its columnName)
|
|
67
|
+
* @returns Array of unique, sorted values from the specified column
|
|
68
|
+
*/
|
|
69
|
+
const getFilterColumnValues = (index: number): any[] => {
|
|
70
|
+
const filterDataOptions: any[] = []
|
|
71
|
+
const filterColumnName = config.filters?.[index]?.columnName
|
|
72
|
+
|
|
73
|
+
// Return empty array if no column name or no data
|
|
74
|
+
if (!filterColumnName || !data || !Array.isArray(data) || data.length === 0) {
|
|
75
|
+
return filterDataOptions
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
data.forEach(function (row: any) {
|
|
79
|
+
if (undefined !== row[filterColumnName] && -1 === filterDataOptions.indexOf(row[filterColumnName])) {
|
|
80
|
+
filterDataOptions.push(row[filterColumnName])
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
filterDataOptions.sort()
|
|
84
|
+
|
|
85
|
+
return filterDataOptions
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
addNewFilter,
|
|
90
|
+
removeFilter,
|
|
91
|
+
updateFilterProp,
|
|
92
|
+
getFilterColumnValues
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { clamp } from 'lodash'
|
|
2
|
+
|
|
3
|
+
// TODO: generalize this to be used in legends other than linear block gradient
|
|
4
|
+
|
|
5
|
+
const LEGEND_SEPARATOR_SIZE = 0.02
|
|
6
|
+
const LEGEND_SEPARATOR_SIZE_MAX = 20
|
|
7
|
+
const LEGEND_SEPARATOR_SIZE_MIN = 8
|
|
8
|
+
|
|
9
|
+
const useLegendSeparators = (separators, legendWidth, allowsLegendSeparators) => {
|
|
10
|
+
const legendSeparators = allowsLegendSeparators
|
|
11
|
+
? separators?.replace(' ', '').split(',').map(Number).filter(Boolean) || []
|
|
12
|
+
: []
|
|
13
|
+
const separatorSize = clamp(legendWidth * LEGEND_SEPARATOR_SIZE, LEGEND_SEPARATOR_SIZE_MIN, LEGEND_SEPARATOR_SIZE_MAX)
|
|
14
|
+
const legendSeparatorsToSubtract = legendSeparators.length * separatorSize
|
|
15
|
+
const getTickSeparatorsAdjustment = (index: number) =>
|
|
16
|
+
legendSeparators.reduce((acc, separators) => (index >= separators ? acc + separatorSize : acc), 0)
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
legendSeparators,
|
|
20
|
+
separatorSize,
|
|
21
|
+
legendSeparatorsToSubtract,
|
|
22
|
+
getTickSeparatorsAdjustment
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default useLegendSeparators
|