This is an automated email from the ASF dual-hosted git repository.
enzomartellucci pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new 3fa5bb41385 fix(echarts): restore dashed line style for time
comparison series (#37135)
3fa5bb41385 is described below
commit 3fa5bb41385dd06f91bf0fba0f00968afb522d9a
Author: Enzo Martellucci <[email protected]>
AuthorDate: Thu Jan 22 00:44:57 2026 +0100
fix(echarts): restore dashed line style for time comparison series (#37135)
---
.../src/operators/utils/isDerivedSeries.ts | 7 +-
.../test/operators/utils/isDerivedSeries.test.ts | 21 +++
.../src/Timeseries/transformProps.ts | 52 +++++--
.../test/Timeseries/transformProps.test.ts | 165 +++++++++++++++++++++
4 files changed, 228 insertions(+), 17 deletions(-)
diff --git
a/superset-frontend/packages/superset-ui-chart-controls/src/operators/utils/isDerivedSeries.ts
b/superset-frontend/packages/superset-ui-chart-controls/src/operators/utils/isDerivedSeries.ts
index ddb3e425df0..50bb31503ad 100644
---
a/superset-frontend/packages/superset-ui-chart-controls/src/operators/utils/isDerivedSeries.ts
+++
b/superset-frontend/packages/superset-ui-chart-controls/src/operators/utils/isDerivedSeries.ts
@@ -28,11 +28,16 @@ import { hasTimeOffset } from './timeOffset';
export const isDerivedSeries = (
series: JsonObject,
formData: QueryFormData,
+ seriesName?: string,
): boolean => {
const comparisonType = formData.comparison_type;
if (comparisonType !== ComparisonType.Values) {
return false;
}
const timeCompare: string[] = ensureIsArray(formData?.time_compare);
- return hasTimeOffset(series, timeCompare);
+ // Check if series matches time offset patterns or exact match (single
metric case)
+ return (
+ hasTimeOffset(series, timeCompare) ||
+ (seriesName !== undefined && timeCompare.includes(seriesName))
+ );
};
diff --git
a/superset-frontend/packages/superset-ui-chart-controls/test/operators/utils/isDerivedSeries.test.ts
b/superset-frontend/packages/superset-ui-chart-controls/test/operators/utils/isDerivedSeries.test.ts
index 472b980be6f..f775dea2b80 100644
---
a/superset-frontend/packages/superset-ui-chart-controls/test/operators/utils/isDerivedSeries.test.ts
+++
b/superset-frontend/packages/superset-ui-chart-controls/test/operators/utils/isDerivedSeries.test.ts
@@ -97,3 +97,24 @@ test('should be false if series name invalid', () => {
};
expect(isDerivedSeries(series, formDataWithActualTypes)).toEqual(false);
});
+
+test('should be true for exact match when seriesName parameter is provided',
() => {
+ const exactMatchSeries = {
+ id: '1 week ago',
+ name: '1 week ago',
+ data: [100],
+ };
+ const formDataWithTimeCompare = {
+ ...formData,
+ comparison_type: ComparisonType.Values,
+ time_compare: ['1 week ago'],
+ };
+ // Without seriesName parameter, exact match is not detected via
hasTimeOffset
+ expect(isDerivedSeries(exactMatchSeries, formDataWithTimeCompare)).toEqual(
+ false,
+ );
+ // With seriesName parameter, exact match is detected
+ expect(
+ isDerivedSeries(exactMatchSeries, formDataWithTimeCompare, '1 week ago'),
+ ).toEqual(true);
+});
diff --git
a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
index cfb05d1602d..6ebc30fc226 100644
---
a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
+++
b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
@@ -45,6 +45,7 @@ import { GenericDataType } from
'@apache-superset/core/api/core';
import {
extractExtraMetrics,
getOriginalSeries,
+ getTimeOffset,
isDerivedSeries,
} from '@superset-ui/chart-controls';
import type { EChartsCoreOption } from 'echarts/core';
@@ -309,7 +310,7 @@ export default function transformProps(
const array = ensureIsArray(chartProps.rawFormData?.time_compare);
const inverted = invert(verboseMap);
- let patternIncrement = 0;
+ const offsetLineWidths: { [key: string]: number } = {};
// For horizontal bar charts, calculate min/max from data to avoid cutting
off labels
const shouldCalculateDataBounds =
@@ -320,18 +321,34 @@ export default function transformProps(
let dataMin: number | undefined;
rawSeries.forEach(entry => {
- const derivedSeries = isDerivedSeries(entry, chartProps.rawFormData);
+ const entryName = String(entry.name || '');
+ const seriesName = inverted[entryName] || entryName;
+ // isDerivedSeries checks for time comparison series patterns:
+ // - "metric__1 day ago" pattern (via hasTimeOffset)
+ // - "1 day ago, groupby" pattern (via hasTimeOffset)
+ // - exact match "1 day ago" (via seriesName parameter)
+ const derivedSeries = isDerivedSeries(
+ entry,
+ chartProps.rawFormData,
+ seriesName,
+ );
const lineStyle: LineStyleOption = {};
if (derivedSeries) {
- patternIncrement += 1;
- // use a combination of dash and dot for the line style
- lineStyle.type = [(patternIncrement % 5) + 1, (patternIncrement % 3) +
1];
+ // Get the time offset for this series to assign different dash patterns
+ const offset = getTimeOffset(entry, array) || seriesName;
+ if (!offsetLineWidths[offset]) {
+ offsetLineWidths[offset] = Object.keys(offsetLineWidths).length + 1;
+ }
+ // Use visible dash patterns that vary by offset index
+ // Pattern: [dash length, gap length] - scaled to be clearly visible
+ const patternIndex = offsetLineWidths[offset];
+ lineStyle.type = [
+ (patternIndex % 5) + 4, // dash: 4-8px (visible)
+ (patternIndex % 3) + 3, // gap: 3-5px (visible)
+ ];
lineStyle.opacity = OpacityEnum.DerivedSeries;
}
- const entryName = String(entry.name || '');
- const seriesName = inverted[entryName] || entryName;
-
// Calculate min/max from data for horizontal bar charts
if (shouldCalculateDataBounds && entry.data && Array.isArray(entry.data)) {
(entry.data as [number, any][]).forEach((datum: [number, any]) => {
@@ -349,14 +366,17 @@ export default function transformProps(
let colorScaleKey = getOriginalSeries(seriesName, array);
- // If this series name exactly matches a time compare value, it's a
time-shifted series
- // and we need to find the corresponding original series for color matching
- if (array && array.includes(seriesName)) {
- // Find the original series (first non-time-compare series)
- const originalSeries = rawSeries.find(s => {
- const sName = inverted[String(s.name || '')] || String(s.name || '');
- return !array.includes(sName);
- });
+ // If series name exactly matches a time offset (single metric case),
+ // find the original series for color matching
+ if (derivedSeries && array.includes(seriesName)) {
+ const originalSeries = rawSeries.find(
+ s =>
+ !isDerivedSeries(
+ s,
+ chartProps.rawFormData,
+ inverted[String(s.name || '')] || String(s.name || ''),
+ ),
+ );
if (originalSeries) {
const originalSeriesName =
inverted[String(originalSeries.name || '')] ||
diff --git
a/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/transformProps.test.ts
b/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/transformProps.test.ts
index 1bdaf9b5fa9..f6e7c3e770c 100644
---
a/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/transformProps.test.ts
+++
b/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/transformProps.test.ts
@@ -21,6 +21,7 @@ import {
AnnotationStyle,
AnnotationType,
ChartProps,
+ ComparisonType,
EventAnnotationLayer,
FormulaAnnotationLayer,
IntervalAnnotationLayer,
@@ -740,6 +741,170 @@ describe('legend sorting', () => {
});
});
+const timeCompareFormData: SqlaFormData = {
+ colorScheme: 'bnbColors',
+ datasource: '3__table',
+ granularity_sqla: 'ds',
+ metric: 'sum__num',
+ viz_type: 'my_viz',
+};
+
+const timeCompareChartPropsConfig = {
+ formData: timeCompareFormData,
+ width: 800,
+ height: 600,
+ theme: supersetTheme,
+};
+
+test('should apply dashed line style to time comparison series with single
metric', () => {
+ const queriesDataWithTimeCompare = [
+ {
+ data: [
+ { sum__num: 100, '1 week ago': 80, __timestamp: 599616000000 },
+ { sum__num: 150, '1 week ago': 120, __timestamp: 599916000000 },
+ ],
+ },
+ ];
+
+ const chartProps = new ChartProps({
+ ...timeCompareChartPropsConfig,
+ formData: {
+ ...timeCompareFormData,
+ time_compare: ['1 week ago'],
+ comparison_type: ComparisonType.Values,
+ },
+ queriesData: queriesDataWithTimeCompare,
+ });
+
+ const transformed = transformProps(
+ chartProps as unknown as EchartsTimeseriesChartProps,
+ );
+ const series = transformed.echartOptions.series as any[];
+
+ const mainSeries = series.find(s => s.name === 'sum__num');
+ const comparisonSeries = series.find(s => s.name === '1 week ago');
+
+ expect(mainSeries).toBeDefined();
+ expect(comparisonSeries).toBeDefined();
+ // Main series should not have a dash pattern array
+ expect(Array.isArray(mainSeries.lineStyle?.type)).toBe(false);
+ // Comparison series should have a visible dash pattern array [dash, gap]
+ expect(Array.isArray(comparisonSeries.lineStyle?.type)).toBe(true);
+ expect(comparisonSeries.lineStyle?.type[0]).toBeGreaterThanOrEqual(4);
+ expect(comparisonSeries.lineStyle?.type[1]).toBeGreaterThanOrEqual(3);
+});
+
+test('should apply dashed line style to time comparison series with
metric__offset pattern', () => {
+ const queriesDataWithTimeCompare = [
+ {
+ data: [
+ {
+ sum__num: 100,
+ 'sum__num__1 week ago': 80,
+ __timestamp: 599616000000,
+ },
+ {
+ sum__num: 150,
+ 'sum__num__1 week ago': 120,
+ __timestamp: 599916000000,
+ },
+ ],
+ },
+ ];
+
+ const chartProps = new ChartProps({
+ ...timeCompareChartPropsConfig,
+ formData: {
+ ...timeCompareFormData,
+ time_compare: ['1 week ago'],
+ comparison_type: ComparisonType.Values,
+ },
+ queriesData: queriesDataWithTimeCompare,
+ });
+
+ const transformed = transformProps(
+ chartProps as unknown as EchartsTimeseriesChartProps,
+ );
+ const series = transformed.echartOptions.series as any[];
+
+ const mainSeries = series.find(s => s.name === 'sum__num');
+ const comparisonSeries = series.find(s => s.name === 'sum__num__1 week ago');
+
+ expect(mainSeries).toBeDefined();
+ expect(comparisonSeries).toBeDefined();
+ // Main series should not have a dash pattern array
+ expect(Array.isArray(mainSeries.lineStyle?.type)).toBe(false);
+ // Comparison series should have a visible dash pattern array [dash, gap]
+ expect(Array.isArray(comparisonSeries.lineStyle?.type)).toBe(true);
+ expect(comparisonSeries.lineStyle?.type[0]).toBeGreaterThanOrEqual(4);
+ expect(comparisonSeries.lineStyle?.type[1]).toBeGreaterThanOrEqual(3);
+});
+
+test('should apply connectNulls to time comparison series', () => {
+ const queriesDataWithNulls = [
+ {
+ data: [
+ { sum__num: 100, '1 week ago': null, __timestamp: 599616000000 },
+ { sum__num: 150, '1 week ago': 120, __timestamp: 599916000000 },
+ { sum__num: 200, '1 week ago': null, __timestamp: 600216000000 },
+ ],
+ },
+ ];
+
+ const chartProps = new ChartProps({
+ ...timeCompareChartPropsConfig,
+ formData: {
+ ...timeCompareFormData,
+ time_compare: ['1 week ago'],
+ comparison_type: ComparisonType.Values,
+ },
+ queriesData: queriesDataWithNulls,
+ });
+
+ const transformed = transformProps(
+ chartProps as unknown as EchartsTimeseriesChartProps,
+ );
+ const series = transformed.echartOptions.series as any[];
+
+ const comparisonSeries = series.find(s => s.name === '1 week ago');
+
+ expect(comparisonSeries).toBeDefined();
+ expect(comparisonSeries.connectNulls).toBe(true);
+});
+
+test('should not apply dashed line style for non-Values comparison types', ()
=> {
+ const queriesDataWithTimeCompare = [
+ {
+ data: [
+ { sum__num: 100, '1 week ago': 80, __timestamp: 599616000000 },
+ { sum__num: 150, '1 week ago': 120, __timestamp: 599916000000 },
+ ],
+ },
+ ];
+
+ const chartProps = new ChartProps({
+ ...timeCompareChartPropsConfig,
+ formData: {
+ ...timeCompareFormData,
+ time_compare: ['1 week ago'],
+ comparison_type: ComparisonType.Difference,
+ },
+ queriesData: queriesDataWithTimeCompare,
+ });
+
+ const transformed = transformProps(
+ chartProps as unknown as EchartsTimeseriesChartProps,
+ );
+ const series = transformed.echartOptions.series as any[];
+
+ const comparisonSeries = series.find(s => s.name === '1 week ago');
+
+ expect(comparisonSeries).toBeDefined();
+ // Non-Values comparison types don't get dashed styling (isDerivedSeries
returns false)
+ expect(Array.isArray(comparisonSeries.lineStyle?.type)).toBe(false);
+ expect(comparisonSeries.connectNulls).toBeFalsy();
+});
+
test('EchartsTimeseries AUTO mode should detect single currency and format
with $ for USD', () => {
const chartProps = new ChartProps<SqlaFormData>({
...chartPropsConfig,