@cdc/chart 4.26.5 → 4.26.6
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/CONFIG.md +12 -8
- package/dist/{cdcchart-DQ00cQCm.es.js → cdcchart-BWVIUcAF.es.js} +5 -55
- package/dist/cdcchart.js +34093 -32767
- package/examples/feature/heatmap/average-age-categorical.json +1 -1
- package/examples/feature/heatmap/calendar-heatmap.json +1 -1
- package/examples/feature/heatmap/sparse-aggregation.json +1 -1
- package/examples/private/bad-y-axis.json +543 -0
- package/examples/private/bar-chart-negative-value.json +455 -0
- package/package.json +3 -3
- package/src/CdcChart.tsx +5 -4
- package/src/CdcChartComponent.tsx +32 -9
- package/src/_stories/Chart.SeriesColorAssignments.stories.tsx +183 -0
- package/src/_stories/Chart.YAxisAutoMaxStrategy.stories.tsx +52 -0
- package/src/_stories/Chart.YAxisFilterDomain.stories.tsx +90 -0
- package/src/_stories/ChartAnnotation.smoke.stories.tsx +10 -0
- package/src/_stories/_mock/annotation_event-line_mock.json +1064 -0
- package/src/_stories/_mock/heatmap-average-age-categorical.json +1 -1
- package/src/_stories/_mock/heatmap.json +1 -1
- package/src/components/Annotations/components/AnnotationDraggable.styles.css +22 -1
- package/src/components/Annotations/components/AnnotationDraggable.tsx +219 -119
- package/src/components/Annotations/tests/AnnotationDraggable.test.tsx +198 -0
- package/src/components/Axis/LeftAxis.tsx +3 -0
- package/src/components/Axis/LeftAxisGridlines.tsx +8 -1
- package/src/components/Axis/RightAxis.tsx +3 -0
- package/src/components/Axis/tests/AxisTickValues.test.tsx +86 -0
- package/src/components/EditorPanel/EditorPanel.tsx +367 -136
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +101 -52
- package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.test.tsx +98 -0
- package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +10 -0
- package/src/components/EditorPanel/components/Panels/Panel.Visual.test.tsx +484 -0
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +3 -0
- package/src/components/EditorPanel/components/Panels/SeriesColorAssignments.tsx +262 -0
- package/src/components/EditorPanel/components/Panels/panelVisual.styles.css +32 -0
- package/src/components/EditorPanel/components/panels.scss +10 -0
- package/src/components/EditorPanel/editor-panel.scss +47 -0
- package/src/components/EditorPanel/helpers/getVisualizationTypeConfigUpdate.ts +6 -2
- package/src/components/EditorPanel/helpers/tests/getVisualizationTypeConfigUpdate.test.ts +20 -2
- package/src/components/EditorPanel/useEditorPermissions.test.tsx +256 -11
- package/src/components/EditorPanel/useEditorPermissions.ts +41 -5
- package/src/components/HeatMap/components/HeatMap.tsx +22 -17
- package/src/components/HeatMap/components/HeatMapGradientLegend.tsx +29 -15
- package/src/components/HeatMap/heatmap.css +15 -1
- package/src/components/HeatMap/tests/HeatMap.test.tsx +39 -7
- package/src/components/Legend/Legend.Component.tsx +2 -2
- package/src/components/Legend/helpers/createFormatLabels.test.ts +88 -0
- package/src/components/Legend/helpers/createFormatLabels.tsx +6 -1
- package/src/components/LineChart/helpers.test.ts +230 -1
- package/src/components/LineChart/helpers.ts +26 -13
- package/src/components/LineChart/index.tsx +21 -4
- package/src/components/LinearChart/tests/LinearChart.test.tsx +30 -0
- package/src/components/LinearChart.tsx +50 -15
- package/src/components/ScatterPlot/ScatterPlot.jsx +3 -3
- package/src/components/SmallMultiples/SmallMultipleTile.tsx +3 -0
- package/src/components/SmallMultiples/SmallMultiples.tsx +38 -9
- package/src/components/SmallMultiples/tests/SmallMultiples.test.tsx +170 -0
- package/src/data/initial-state.js +1 -0
- package/src/helpers/calculateLeftYAxisWidth.ts +9 -3
- package/src/helpers/colorAssignmentHelpers.ts +102 -0
- package/src/helpers/dataHelpers.ts +11 -1
- package/src/helpers/getAxisMaxOverride.ts +16 -0
- package/src/helpers/getCleanTopTickMax.ts +64 -0
- package/src/helpers/getColorScale.ts +18 -9
- package/src/helpers/getHasBoundarySuppression.ts +26 -0
- package/src/helpers/getMinMax.ts +24 -22
- package/src/helpers/getYAxisDomainData.ts +38 -0
- package/src/helpers/getYAxisFinalizationEligibility.ts +40 -0
- package/src/helpers/hasSpacedInlineLabel.ts +10 -0
- package/src/helpers/seriesColumnSettings.ts +5 -4
- package/src/helpers/smallMultiplesHelpers.ts +5 -0
- package/src/helpers/tests/calculateLeftYAxisWidth.test.ts +38 -0
- package/src/helpers/tests/dataHelpers.test.ts +135 -0
- package/src/helpers/tests/getAxisMaxOverride.test.ts +36 -0
- package/src/helpers/tests/getCleanTopTickMax.test.ts +165 -0
- package/src/helpers/tests/getColorScale.test.ts +197 -0
- package/src/helpers/tests/getMinMax.test.ts +211 -0
- package/src/helpers/tests/getYAxisDomainData.test.ts +119 -0
- package/src/helpers/tests/getYAxisFinalizationEligibility.test.ts +185 -0
- package/src/helpers/tests/{getYAxisAutoPaddingMode.test.ts → hasSpacedInlineLabel.test.ts} +17 -32
- package/src/helpers/tests/seriesColumnSettings.test.ts +16 -2
- package/src/helpers/tests/smallMultiplesHelpers.test.ts +99 -0
- package/src/hooks/tests/useRightAxis.test.tsx +127 -0
- package/src/hooks/tests/useScales.test.tsx +307 -29
- package/src/hooks/useRightAxis.ts +30 -13
- package/src/hooks/useScales.ts +45 -28
- package/src/hooks/useTooltip.tsx +3 -1
- package/src/test/CdcChart.dataTable.test.tsx +122 -0
- package/src/types/ChartConfig.ts +6 -1
- package/src/types/ChartContext.ts +3 -0
- package/src/helpers/getYAxisAutoPadding.ts +0 -55
- package/src/helpers/getYAxisAutoPaddingMode.ts +0 -17
- package/src/helpers/tests/getYAxisAutoPadding.test.ts +0 -37
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { getSeriesWithData } from '../dataHelpers'
|
|
3
|
+
import { ChartConfig } from '../../types/ChartConfig'
|
|
4
|
+
|
|
5
|
+
type SeriesInput = {
|
|
6
|
+
dataKey: string
|
|
7
|
+
name?: string
|
|
8
|
+
type?: string
|
|
9
|
+
dynamicCategory?: string
|
|
10
|
+
originalDataKey?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const buildConfig = (
|
|
14
|
+
data: Record<string, unknown>[],
|
|
15
|
+
series: SeriesInput[],
|
|
16
|
+
overrides: Partial<ChartConfig> = {}
|
|
17
|
+
): ChartConfig =>
|
|
18
|
+
({
|
|
19
|
+
data,
|
|
20
|
+
filters: [],
|
|
21
|
+
legend: { colorCode: '' },
|
|
22
|
+
runtime: { series },
|
|
23
|
+
...overrides
|
|
24
|
+
} as unknown as ChartConfig)
|
|
25
|
+
|
|
26
|
+
describe('getSeriesWithData', () => {
|
|
27
|
+
it('includes a series that has at least one numeric non-zero value', () => {
|
|
28
|
+
const config = buildConfig(
|
|
29
|
+
[{ percent_mild: '3.8' }, { percent_mild: '0.0' }],
|
|
30
|
+
[{ dataKey: 'percent_mild', name: 'Mild', type: 'Bar' }]
|
|
31
|
+
)
|
|
32
|
+
expect(getSeriesWithData(config)).toEqual(['Mild'])
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('excludes a series whose values are all zero (0 and "0.0")', () => {
|
|
36
|
+
const config = buildConfig(
|
|
37
|
+
[{ percent_fever_notemp: '0.0' }, { percent_fever_notemp: 0 }],
|
|
38
|
+
[{ dataKey: 'percent_fever_notemp', name: 'Reported fever', type: 'Bar' }]
|
|
39
|
+
)
|
|
40
|
+
expect(getSeriesWithData(config)).toEqual([])
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('keeps a series that has a single non-zero value among zeros', () => {
|
|
44
|
+
const config = buildConfig(
|
|
45
|
+
[{ percent_mild: '0.0' }, { percent_mild: '0.0' }, { percent_mild: '1.2' }],
|
|
46
|
+
[{ dataKey: 'percent_mild', name: 'Mild', type: 'Bar' }]
|
|
47
|
+
)
|
|
48
|
+
expect(getSeriesWithData(config)).toEqual(['Mild'])
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('treats non-numeric placeholder strings as no data', () => {
|
|
52
|
+
const config = buildConfig(
|
|
53
|
+
[{ percent_fever_notemp: 'Not applicable' }, { percent_fever_notemp: '<1.0' }, { percent_fever_notemp: '' }],
|
|
54
|
+
[{ dataKey: 'percent_fever_notemp', name: 'Reported fever', type: 'Bar' }]
|
|
55
|
+
)
|
|
56
|
+
expect(getSeriesWithData(config)).toEqual([])
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('treats null and undefined values as no data', () => {
|
|
60
|
+
const config = buildConfig(
|
|
61
|
+
[{ percent_fever_notemp: null }, { percent_fever_notemp: undefined }],
|
|
62
|
+
[{ dataKey: 'percent_fever_notemp', name: 'Reported fever', type: 'Bar' }]
|
|
63
|
+
)
|
|
64
|
+
expect(getSeriesWithData(config)).toEqual([])
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('counts comma- and dollar-formatted numeric strings as data', () => {
|
|
68
|
+
const config = buildConfig(
|
|
69
|
+
[{ revenue: '$1,000' }, { revenue: '0.0' }],
|
|
70
|
+
[{ dataKey: 'revenue', name: 'Revenue', type: 'Bar' }]
|
|
71
|
+
)
|
|
72
|
+
expect(getSeriesWithData(config)).toEqual(['Revenue'])
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('counts negative non-zero values as data', () => {
|
|
76
|
+
const config = buildConfig([{ delta: '-5' }, { delta: '0' }], [{ dataKey: 'delta', name: 'Delta', type: 'Bar' }])
|
|
77
|
+
expect(getSeriesWithData(config)).toEqual(['Delta'])
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('applies active filters before evaluating series data', () => {
|
|
81
|
+
const config = buildConfig(
|
|
82
|
+
[
|
|
83
|
+
{ reaction: 'Headache', percent_fever_notemp: '0.0', percent_mild: '3.8' },
|
|
84
|
+
{ reaction: 'Fever or feverish', percent_fever_notemp: '0.9', percent_mild: '0.0' }
|
|
85
|
+
],
|
|
86
|
+
[
|
|
87
|
+
{ dataKey: 'percent_mild', name: 'Mild', type: 'Bar' },
|
|
88
|
+
{ dataKey: 'percent_fever_notemp', name: 'Reported fever', type: 'Bar' }
|
|
89
|
+
],
|
|
90
|
+
{ filters: [{ columnName: 'reaction', active: 'Headache' }] as ChartConfig['filters'] }
|
|
91
|
+
)
|
|
92
|
+
// Under "Headache" the fever column is all zeros, so it drops out of the legend.
|
|
93
|
+
expect(getSeriesWithData(config)).toEqual(['Mild'])
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('uses originalDataKey for dynamicCategory series', () => {
|
|
97
|
+
const config = buildConfig(
|
|
98
|
+
[
|
|
99
|
+
{ category: 'a', value: '4.2', label: 'a-value' },
|
|
100
|
+
{ category: 'b', value: '0.0', label: 'b-value' }
|
|
101
|
+
],
|
|
102
|
+
[
|
|
103
|
+
{
|
|
104
|
+
dataKey: 'a',
|
|
105
|
+
name: 'A',
|
|
106
|
+
type: 'Bar',
|
|
107
|
+
dynamicCategory: 'category',
|
|
108
|
+
originalDataKey: 'value'
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
dataKey: 'b',
|
|
112
|
+
name: 'B',
|
|
113
|
+
type: 'Bar',
|
|
114
|
+
dynamicCategory: 'category',
|
|
115
|
+
originalDataKey: 'value'
|
|
116
|
+
}
|
|
117
|
+
]
|
|
118
|
+
)
|
|
119
|
+
// Series "a" has a non-zero value; series "b" is zero only.
|
|
120
|
+
expect(getSeriesWithData(config)).toEqual(['A'])
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('splits color-coded Bar series by colorCode values', () => {
|
|
124
|
+
const config = buildConfig(
|
|
125
|
+
[
|
|
126
|
+
{ group: 'North', amount: '5' },
|
|
127
|
+
{ group: 'South', amount: '0.0' }
|
|
128
|
+
],
|
|
129
|
+
[{ dataKey: 'amount', name: 'Amount', type: 'Bar' }],
|
|
130
|
+
{ legend: { colorCode: 'group' } as ChartConfig['legend'] }
|
|
131
|
+
)
|
|
132
|
+
// Only the "North" rows carry a non-zero amount.
|
|
133
|
+
expect(getSeriesWithData(config)).toEqual(['North'])
|
|
134
|
+
})
|
|
135
|
+
})
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { getAxisMaxOverride } from '../getAxisMaxOverride'
|
|
3
|
+
|
|
4
|
+
describe('getAxisMaxOverride', () => {
|
|
5
|
+
it('accepts finite entered values that meet the required max', () => {
|
|
6
|
+
expect(getAxisMaxOverride({ value: '25', minimumValidMax: 20 })).toEqual({
|
|
7
|
+
hasEnteredMax: true,
|
|
8
|
+
hasValidMax: true,
|
|
9
|
+
maxNumber: 25
|
|
10
|
+
})
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('rejects missing or empty entered values', () => {
|
|
14
|
+
expect(getAxisMaxOverride({ value: undefined, minimumValidMax: 0 }).hasValidMax).toBe(false)
|
|
15
|
+
expect(getAxisMaxOverride({ value: null, minimumValidMax: 0 }).hasValidMax).toBe(false)
|
|
16
|
+
expect(getAxisMaxOverride({ value: '', minimumValidMax: 0 }).hasValidMax).toBe(false)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('rejects non-finite entered values', () => {
|
|
20
|
+
expect(getAxisMaxOverride({ value: 'not-a-number', minimumValidMax: 0 }).hasValidMax).toBe(false)
|
|
21
|
+
expect(getAxisMaxOverride({ value: Infinity, minimumValidMax: 0 }).hasValidMax).toBe(false)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('rejects entered values below the required max', () => {
|
|
25
|
+
expect(getAxisMaxOverride({ value: '19', minimumValidMax: 20 })).toMatchObject({
|
|
26
|
+
hasEnteredMax: true,
|
|
27
|
+
hasValidMax: false,
|
|
28
|
+
maxNumber: 19
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('supports the non-positive primary-axis rule by passing zero as the required max', () => {
|
|
33
|
+
expect(getAxisMaxOverride({ value: '0', minimumValidMax: 0 }).hasValidMax).toBe(true)
|
|
34
|
+
expect(getAxisMaxOverride({ value: '-1', minimumValidMax: 0 }).hasValidMax).toBe(false)
|
|
35
|
+
})
|
|
36
|
+
})
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { getCleanTopTickMax, getCleanTopTickMaxCandidates, getFinalTopTickMax } from '../getCleanTopTickMax'
|
|
3
|
+
|
|
4
|
+
describe('getCleanTopTickMax', () => {
|
|
5
|
+
it('rounds automatic max values with the clean-top-tick mantissa ladder', () => {
|
|
6
|
+
expect(getCleanTopTickMaxCandidates()).toEqual([1, 1.2, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10])
|
|
7
|
+
|
|
8
|
+
expect([
|
|
9
|
+
[0.0049, getCleanTopTickMax(0.0049)],
|
|
10
|
+
[0.011, getCleanTopTickMax(0.011)],
|
|
11
|
+
[0.012, getCleanTopTickMax(0.012)],
|
|
12
|
+
[1.1, getCleanTopTickMax(1.1)],
|
|
13
|
+
[1.2, getCleanTopTickMax(1.2)],
|
|
14
|
+
[3, getCleanTopTickMax(3)],
|
|
15
|
+
[5, getCleanTopTickMax(5)],
|
|
16
|
+
[5.1, getCleanTopTickMax(5.1)],
|
|
17
|
+
[7, getCleanTopTickMax(7)],
|
|
18
|
+
[7.2, getCleanTopTickMax(7.2)],
|
|
19
|
+
[8, getCleanTopTickMax(8)],
|
|
20
|
+
[9, getCleanTopTickMax(9)],
|
|
21
|
+
[25, getCleanTopTickMax(25)],
|
|
22
|
+
[89, getCleanTopTickMax(89)],
|
|
23
|
+
[101, getCleanTopTickMax(101)],
|
|
24
|
+
[1434, getCleanTopTickMax(1434)],
|
|
25
|
+
[1470, getCleanTopTickMax(1470)],
|
|
26
|
+
[1490, getCleanTopTickMax(1490)],
|
|
27
|
+
[1675, getCleanTopTickMax(1675)],
|
|
28
|
+
[2340, getCleanTopTickMax(2340)],
|
|
29
|
+
[5678, getCleanTopTickMax(5678)],
|
|
30
|
+
[8999, getCleanTopTickMax(8999)]
|
|
31
|
+
]).toEqual([
|
|
32
|
+
[0.0049, 0.005],
|
|
33
|
+
[0.011, 0.012],
|
|
34
|
+
[0.012, 0.012],
|
|
35
|
+
[1.1, 1.2],
|
|
36
|
+
[1.2, 1.2],
|
|
37
|
+
[3, 3],
|
|
38
|
+
[5, 5],
|
|
39
|
+
[5.1, 6],
|
|
40
|
+
[7, 8],
|
|
41
|
+
[7.2, 8],
|
|
42
|
+
[8, 8],
|
|
43
|
+
[9, 10],
|
|
44
|
+
[25, 25],
|
|
45
|
+
[89, 100],
|
|
46
|
+
[101, 120],
|
|
47
|
+
[1434, 1500],
|
|
48
|
+
[1470, 1500],
|
|
49
|
+
[1490, 1500],
|
|
50
|
+
[1675, 2000],
|
|
51
|
+
[2340, 2500],
|
|
52
|
+
[5678, 6000],
|
|
53
|
+
[8999, 10000]
|
|
54
|
+
])
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('leaves non-finite values unchanged', () => {
|
|
58
|
+
expect(getCleanTopTickMax(Infinity)).toBe(Infinity)
|
|
59
|
+
expect(Number.isNaN(getCleanTopTickMax(NaN))).toBe(true)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('leaves non-positive values unchanged', () => {
|
|
63
|
+
expect(getCleanTopTickMax(0)).toBe(0)
|
|
64
|
+
expect(getCleanTopTickMax(-1)).toBe(-1)
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe('getFinalTopTickMax', () => {
|
|
69
|
+
it('advances clean max values to the next generated tick interval when the top tick is below the max', () => {
|
|
70
|
+
expect(
|
|
71
|
+
getFinalTopTickMax({
|
|
72
|
+
currentMax: 12,
|
|
73
|
+
rawMax: 12,
|
|
74
|
+
ticks: [0, 5, 10],
|
|
75
|
+
shouldUseCleanTopTick: true
|
|
76
|
+
})
|
|
77
|
+
).toEqual({ max: 15, tickValues: [0, 5, 10, 15] })
|
|
78
|
+
|
|
79
|
+
expect(
|
|
80
|
+
getFinalTopTickMax({
|
|
81
|
+
currentMax: 25,
|
|
82
|
+
rawMax: 25,
|
|
83
|
+
ticks: [0, 10, 20],
|
|
84
|
+
shouldUseCleanTopTick: true
|
|
85
|
+
})
|
|
86
|
+
).toEqual({ max: 30, tickValues: [0, 10, 20, 30] })
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('keeps a clean max when the generated top tick already reaches it', () => {
|
|
90
|
+
expect(
|
|
91
|
+
getFinalTopTickMax({
|
|
92
|
+
currentMax: 12,
|
|
93
|
+
rawMax: 12,
|
|
94
|
+
ticks: [0, 2, 4, 6, 8, 10, 12],
|
|
95
|
+
shouldUseCleanTopTick: true
|
|
96
|
+
})
|
|
97
|
+
).toEqual({ max: 12, tickValues: [0, 2, 4, 6, 8, 10, 12] })
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('does not advance for floating-point dust above the generated top tick', () => {
|
|
101
|
+
expect(
|
|
102
|
+
getFinalTopTickMax({
|
|
103
|
+
currentMax: 0.6000000000000001,
|
|
104
|
+
rawMax: 0.6,
|
|
105
|
+
ticks: [0, 0.2, 0.4, 0.6],
|
|
106
|
+
shouldUseCleanTopTick: true
|
|
107
|
+
})
|
|
108
|
+
).toEqual({ max: 0.6, tickValues: [0, 0.2, 0.4, 0.6] })
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('adds one more interval for spaced inline labels when the raw max is too close to the top tick', () => {
|
|
112
|
+
expect(
|
|
113
|
+
getFinalTopTickMax({
|
|
114
|
+
currentMax: 14.4,
|
|
115
|
+
rawMax: 14.4,
|
|
116
|
+
ticks: [0, 5, 10, 15],
|
|
117
|
+
shouldUseCleanTopTick: true,
|
|
118
|
+
headroomMode: 'inline-label'
|
|
119
|
+
})
|
|
120
|
+
).toEqual({ max: 20, tickValues: [0, 5, 10, 15, 20] })
|
|
121
|
+
|
|
122
|
+
expect(
|
|
123
|
+
getFinalTopTickMax({
|
|
124
|
+
currentMax: 11.25,
|
|
125
|
+
rawMax: 11.25,
|
|
126
|
+
ticks: [0, 5, 10, 15],
|
|
127
|
+
shouldUseCleanTopTick: true,
|
|
128
|
+
headroomMode: 'inline-label'
|
|
129
|
+
})
|
|
130
|
+
).toEqual({ max: 15, tickValues: [0, 5, 10, 15] })
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('finalizes tiny spaced inline-label domains and includes the top tick', () => {
|
|
134
|
+
expect(
|
|
135
|
+
getFinalTopTickMax({
|
|
136
|
+
currentMax: 0.015,
|
|
137
|
+
rawMax: 0.015,
|
|
138
|
+
ticks: [0, 0.005, 0.01, 0.015],
|
|
139
|
+
shouldUseCleanTopTick: true,
|
|
140
|
+
headroomMode: 'inline-label'
|
|
141
|
+
})
|
|
142
|
+
).toEqual({ max: 0.02, tickValues: [0, 0.005, 0.01, 0.015, 0.02] })
|
|
143
|
+
|
|
144
|
+
expect(
|
|
145
|
+
getFinalTopTickMax({
|
|
146
|
+
currentMax: 0.019,
|
|
147
|
+
rawMax: 0.019,
|
|
148
|
+
ticks: [0, 0.005, 0.01, 0.015],
|
|
149
|
+
shouldUseCleanTopTick: true,
|
|
150
|
+
headroomMode: 'inline-label'
|
|
151
|
+
})
|
|
152
|
+
).toEqual({ max: 0.025, tickValues: [0, 0.005, 0.01, 0.015, 0.02, 0.025] })
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('does not change the max when effective clean-top-tick is disabled', () => {
|
|
156
|
+
expect(
|
|
157
|
+
getFinalTopTickMax({
|
|
158
|
+
currentMax: 12,
|
|
159
|
+
rawMax: 12,
|
|
160
|
+
ticks: [0, 5, 10],
|
|
161
|
+
shouldUseCleanTopTick: false
|
|
162
|
+
})
|
|
163
|
+
).toEqual({ max: 12 })
|
|
164
|
+
})
|
|
165
|
+
})
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { getColorScale } from '../getColorScale'
|
|
3
|
+
import { ChartConfig } from '../../types/ChartConfig'
|
|
4
|
+
|
|
5
|
+
const buildConfig = (overrides: Partial<ChartConfig> = {}): ChartConfig =>
|
|
6
|
+
({
|
|
7
|
+
visualizationType: 'Line',
|
|
8
|
+
visualizationSubType: 'regular',
|
|
9
|
+
general: {
|
|
10
|
+
palette: {
|
|
11
|
+
customColors: ['#111111', '#222222', '#333333'],
|
|
12
|
+
colorAssignmentMode: 'by-value',
|
|
13
|
+
colorAssignments: [
|
|
14
|
+
{ key: 'series_a', color: '#aa0000' },
|
|
15
|
+
{ key: 'series_c', color: '#00cc00' }
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
legend: {},
|
|
20
|
+
series: [
|
|
21
|
+
{ dataKey: 'series_a', name: 'Series A', type: 'Line' },
|
|
22
|
+
{ dataKey: 'series_b', name: 'Series B', type: 'Line' },
|
|
23
|
+
{ dataKey: 'series_c', name: 'Series C', type: 'Line' }
|
|
24
|
+
],
|
|
25
|
+
runtime: {
|
|
26
|
+
seriesKeys: ['series_a', 'series_b', 'series_c'],
|
|
27
|
+
seriesLabels: {
|
|
28
|
+
series_a: 'Series A',
|
|
29
|
+
series_b: 'Series B',
|
|
30
|
+
series_c: 'Series C'
|
|
31
|
+
},
|
|
32
|
+
seriesLabelsAll: ['Series A', 'Series B', 'Series C']
|
|
33
|
+
},
|
|
34
|
+
...overrides
|
|
35
|
+
} as ChartConfig)
|
|
36
|
+
|
|
37
|
+
describe('getColorScale series color assignments', () => {
|
|
38
|
+
it('keeps assigned series colors stable when runtime series order changes', () => {
|
|
39
|
+
const colorScale = getColorScale(
|
|
40
|
+
buildConfig({
|
|
41
|
+
runtime: {
|
|
42
|
+
seriesKeys: ['series_c', 'series_a'],
|
|
43
|
+
seriesLabels: {
|
|
44
|
+
series_a: 'Series A',
|
|
45
|
+
series_c: 'Series C'
|
|
46
|
+
},
|
|
47
|
+
seriesLabelsAll: ['Series C', 'Series A']
|
|
48
|
+
} as any
|
|
49
|
+
})
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
expect(colorScale('Series A')).toBe('#aa0000')
|
|
53
|
+
expect(colorScale('Series C')).toBe('#00cc00')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('keeps assigned colors when missing series later reappear', () => {
|
|
57
|
+
const filteredScale = getColorScale(
|
|
58
|
+
buildConfig({
|
|
59
|
+
runtime: {
|
|
60
|
+
seriesKeys: ['series_a'],
|
|
61
|
+
seriesLabels: { series_a: 'Series A' },
|
|
62
|
+
seriesLabelsAll: ['Series A']
|
|
63
|
+
} as any
|
|
64
|
+
})
|
|
65
|
+
)
|
|
66
|
+
const restoredScale = getColorScale(buildConfig())
|
|
67
|
+
|
|
68
|
+
expect(filteredScale('Series A')).toBe('#aa0000')
|
|
69
|
+
expect(restoredScale('Series C')).toBe('#00cc00')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('uses ordered palette/custom fallback for unassigned series', () => {
|
|
73
|
+
const colorScale = getColorScale(buildConfig())
|
|
74
|
+
|
|
75
|
+
expect(colorScale('Series A')).toBe('#aa0000')
|
|
76
|
+
expect(colorScale('Series B')).toBe('#222222')
|
|
77
|
+
expect(colorScale('Series C')).toBe('#00cc00')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('resolves assignments by dataKey when display labels differ', () => {
|
|
81
|
+
const colorScale = getColorScale(
|
|
82
|
+
buildConfig({
|
|
83
|
+
series: [{ dataKey: 'raw_value', name: 'Display Value', type: 'Line' }] as any,
|
|
84
|
+
runtime: {
|
|
85
|
+
seriesKeys: ['raw_value'],
|
|
86
|
+
seriesLabels: { raw_value: 'Display Value' },
|
|
87
|
+
seriesLabelsAll: ['Display Value']
|
|
88
|
+
} as any,
|
|
89
|
+
general: {
|
|
90
|
+
palette: {
|
|
91
|
+
customColors: ['#111111'],
|
|
92
|
+
colorAssignmentMode: 'by-value',
|
|
93
|
+
colorAssignments: [{ key: 'raw_value', color: '#123456' }]
|
|
94
|
+
}
|
|
95
|
+
} as any
|
|
96
|
+
})
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
expect(colorScale('Display Value')).toBe('#123456')
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('preserves ordered mode behavior when assignments are present', () => {
|
|
103
|
+
const colorScale = getColorScale(
|
|
104
|
+
buildConfig({
|
|
105
|
+
general: {
|
|
106
|
+
palette: {
|
|
107
|
+
customColors: ['#111111', '#222222', '#333333'],
|
|
108
|
+
colorAssignmentMode: 'ordered',
|
|
109
|
+
colorAssignments: [{ key: 'series_a', color: '#aa0000' }]
|
|
110
|
+
}
|
|
111
|
+
} as any
|
|
112
|
+
})
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
expect(colorScale('Series A')).toBe('#111111')
|
|
116
|
+
expect(colorScale('Series B')).toBe('#222222')
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('preserves scatter plot customColors through the shared scale', () => {
|
|
120
|
+
const colorScale = getColorScale(
|
|
121
|
+
buildConfig({
|
|
122
|
+
visualizationType: 'Scatter Plot',
|
|
123
|
+
general: {
|
|
124
|
+
palette: {
|
|
125
|
+
customColors: ['#101010', '#202020', '#303030']
|
|
126
|
+
}
|
|
127
|
+
} as any
|
|
128
|
+
})
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
expect(colorScale('Series A')).toBe('#101010')
|
|
132
|
+
expect(colorScale('Series B')).toBe('#202020')
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('uses v2 customColors in authored order when customColorsOrdered is absent', () => {
|
|
136
|
+
const colorScale = getColorScale(
|
|
137
|
+
buildConfig({
|
|
138
|
+
general: {
|
|
139
|
+
palette: {
|
|
140
|
+
name: 'qualitative_standard',
|
|
141
|
+
version: '2.0',
|
|
142
|
+
customColors: [
|
|
143
|
+
'#111111',
|
|
144
|
+
'#222222',
|
|
145
|
+
'#333333',
|
|
146
|
+
'#444444',
|
|
147
|
+
'#555555',
|
|
148
|
+
'#666666',
|
|
149
|
+
'#777777',
|
|
150
|
+
'#888888',
|
|
151
|
+
'#999999'
|
|
152
|
+
]
|
|
153
|
+
}
|
|
154
|
+
} as any
|
|
155
|
+
})
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
expect(colorScale('Series A')).toBe('#111111')
|
|
159
|
+
expect(colorScale('Series B')).toBe('#222222')
|
|
160
|
+
expect(colorScale('Series C')).toBe('#333333')
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('falls back to customColors when customColorsOrdered is empty', () => {
|
|
164
|
+
const colorScale = getColorScale(
|
|
165
|
+
buildConfig({
|
|
166
|
+
general: {
|
|
167
|
+
palette: {
|
|
168
|
+
customColorsOrdered: [],
|
|
169
|
+
customColors: ['#101010', '#202020', '#303030']
|
|
170
|
+
}
|
|
171
|
+
} as any
|
|
172
|
+
})
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
expect(colorScale('Series A')).toBe('#101010')
|
|
176
|
+
expect(colorScale('Series B')).toBe('#202020')
|
|
177
|
+
expect(colorScale('Series C')).toBe('#303030')
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('falls back to the named palette when customColors is empty', () => {
|
|
181
|
+
const colorScale = getColorScale(
|
|
182
|
+
buildConfig({
|
|
183
|
+
general: {
|
|
184
|
+
palette: {
|
|
185
|
+
name: 'qualitative_standard',
|
|
186
|
+
version: '2.0',
|
|
187
|
+
customColors: []
|
|
188
|
+
}
|
|
189
|
+
} as any
|
|
190
|
+
})
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
expect(colorScale('Series A')).toBe('#0057B7')
|
|
194
|
+
expect(colorScale('Series B')).toBe('#00B1CE')
|
|
195
|
+
expect(colorScale('Series C')).toBe('#5A8E3F')
|
|
196
|
+
})
|
|
197
|
+
})
|