test(plugin-chart-echarts): regression guards for temporal x-axis labels on timeseries charts (#39208)

This commit is contained in:
jesperct
2026-05-05 12:37:35 -03:00
committed by GitHub
parent 8173cfe9e3
commit 5b5f23d127
4 changed files with 313 additions and 0 deletions

View File

@@ -20,12 +20,14 @@ import {
AnnotationStyle,
AnnotationType,
AnnotationSourceType,
AxisType,
DataRecord,
FormulaAnnotationLayer,
IntervalAnnotationLayer,
VizType,
ChartDataResponseResult,
} from '@superset-ui/core';
import { GenericDataType } from '@apache-superset/core/common';
import {
LegendOrientation,
LegendType,
@@ -496,3 +498,133 @@ test('should add a formula annotation when X-axis column has dataset-level label
expect(Array.isArray(formulaSeries?.data)).toBe(true);
expect((formulaSeries!.data as unknown[]).length).toBeGreaterThan(0);
});
test('numeric x coltype never gets silently coerced to the Time axis', () => {
// Regression guard for echarts-timeseries-epoch-x-axis-labels investigation.
// Mixed Timeseries must follow the reported coltype: Numeric values stay
// off the Time axis and are not silently reinterpreted as Date instances.
// A future change that coerces Numeric → Time would bring back the "NaN"
// label symptom we were investigating. We also assert that whichever
// formatter is picked, it produces a string and does not emit "NaN".
const ts1 = 1745784000000;
const ts2 = 1745870400000;
const epochRows = [
{ __timestamp: ts1, metric: 10 },
{ __timestamp: ts2, metric: 20 },
];
const epochQueryData = createTestQueryData(epochRows, {
colnames: ['__timestamp', 'metric'],
coltypes: [GenericDataType.Numeric, GenericDataType.Numeric],
label_map: { __timestamp: ['__timestamp'], metric: ['metric'] },
});
const chartProps = createEchartsTimeseriesTestChartProps<
EchartsMixedTimeseriesFormData,
EchartsMixedTimeseriesProps
>({
...MIXED_TIMESERIES_CHART_PROPS_DEFAULTS,
defaultQueriesData: [epochQueryData, epochQueryData],
formData: {
...formData,
x_axis: '__timestamp',
metrics: ['metric'],
metricsB: ['metric'],
groupby: [],
groupbyB: [],
},
queriesData: [epochQueryData, epochQueryData],
});
const { echartOptions } = transformProps(chartProps);
const xAxis = echartOptions.xAxis as {
type: string;
axisLabel: { formatter: (v: number) => string };
};
expect(xAxis.type).not.toBe(AxisType.Time);
const label = xAxis.axisLabel.formatter(ts1);
expect(typeof label).toBe('string');
expect(label).not.toMatch(/NaN/);
});
test('xAxisForceCategorical forces Category axis regardless of Numeric coltype', () => {
const ts1 = 1745784000000;
const ts2 = 1745870400000;
const epochRows = [
{ __timestamp: ts1, metric: 10 },
{ __timestamp: ts2, metric: 20 },
];
const epochQueryData = createTestQueryData(epochRows, {
colnames: ['__timestamp', 'metric'],
coltypes: [GenericDataType.Numeric, GenericDataType.Numeric],
label_map: { __timestamp: ['__timestamp'], metric: ['metric'] },
});
const chartProps = createEchartsTimeseriesTestChartProps<
EchartsMixedTimeseriesFormData,
EchartsMixedTimeseriesProps
>({
...MIXED_TIMESERIES_CHART_PROPS_DEFAULTS,
defaultQueriesData: [epochQueryData, epochQueryData],
formData: {
...formData,
x_axis: '__timestamp',
metrics: ['metric'],
metricsB: ['metric'],
groupby: [],
groupbyB: [],
xAxisForceCategorical: true,
},
queriesData: [epochQueryData, epochQueryData],
});
const { echartOptions } = transformProps(chartProps);
const xAxis = echartOptions.xAxis as { type: string };
expect(xAxis.type).toBe(AxisType.Category);
});
test('temporal x coltype wires the time formatter and Time axis', () => {
// Regression guard: the happy path for mixed-timeseries charts. Ensures
// Temporal coltype still routes through the TimeFormatter so the time axis
// rendering path is exercised by the test suite.
const ts1 = 1745784000000;
const ts2 = 1745870400000;
const temporalRows = [
{ __timestamp: ts1, metric: 10 },
{ __timestamp: ts2, metric: 20 },
];
const temporalQueryData = createTestQueryData(temporalRows, {
colnames: ['__timestamp', 'metric'],
coltypes: [GenericDataType.Temporal, GenericDataType.Numeric],
label_map: { __timestamp: ['__timestamp'], metric: ['metric'] },
});
const chartProps = createEchartsTimeseriesTestChartProps<
EchartsMixedTimeseriesFormData,
EchartsMixedTimeseriesProps
>({
...MIXED_TIMESERIES_CHART_PROPS_DEFAULTS,
defaultQueriesData: [temporalQueryData, temporalQueryData],
formData: {
...formData,
x_axis: '__timestamp',
metrics: ['metric'],
metricsB: ['metric'],
groupby: [],
groupbyB: [],
},
queriesData: [temporalQueryData, temporalQueryData],
});
const { echartOptions } = transformProps(chartProps);
const xAxis = echartOptions.xAxis as {
type: string;
axisLabel: { formatter: (v: Date) => string };
};
expect(xAxis.type).toBe(AxisType.Time);
const label = xAxis.axisLabel.formatter(new Date(ts1));
expect(typeof label).toBe('string');
expect(label).not.toMatch(/NaN/);
});