diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/sharedControls.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/sharedControls.tsx index eaf0f172b45ce..1ec3735285484 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/sharedControls.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/sharedControls.tsx @@ -338,6 +338,20 @@ const color_scheme: SharedControlConfig<'ColorSchemeControl'> = { }), }; +const time_shift_color: SharedControlConfig<'CheckboxControl'> = { + type: 'CheckboxControl', + label: t('Match time shift color with original series'), + default: true, + renderTrigger: true, + description: t( + 'When unchecked, colors from the selected color scheme will be used for time shifted series', + ), + visibility: ({ controls }) => + Boolean( + controls?.time_compare?.value && !isEmpty(controls?.time_compare?.value), + ), +}; + const truncate_metric: SharedControlConfig<'CheckboxControl'> = { type: 'CheckboxControl', label: t('Truncate Metric'), @@ -399,6 +413,7 @@ export default { x_axis_time_format, adhoc_filters: dndAdhocFilterControl, color_scheme, + time_shift_color, series_columns: dndColumnsControl, series_limit, series_limit_metric: dndSortByControl, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/controlPanel.tsx index 201244f689412..29ce20e9d66a1 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/controlPanel.tsx @@ -299,6 +299,7 @@ const config: ControlPanelConfig = { expanded: true, controlSetRows: [ ['color_scheme'], + ['time_shift_color'], ...createCustomizeSection(t('Query A'), ''), ...createCustomizeSection(t('Query B'), 'B'), [ diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts index 29741f545c70d..d029201df4f1c 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts @@ -152,6 +152,7 @@ export default function transformProps( areaB, annotationLayers, colorScheme, + timeShiftColor, contributionMode, legendOrientation, legendType, @@ -408,6 +409,7 @@ export default function transformProps( showValueIndexes: showValueIndexesA, totalStackedValues, thresholdValues, + timeShiftColor, }, ); if (transformedSeries) series.push(transformedSeries); @@ -457,6 +459,7 @@ export default function transformProps( showValueIndexes: showValueIndexesB, totalStackedValues: totalStackedValuesB, thresholdValues: thresholdValuesB, + timeShiftColor, }, ); if (transformedSeries) series.push(transformedSeries); diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx index ae9dc0afaac39..c2b57ec72a3aa 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx @@ -68,6 +68,7 @@ const config: ControlPanelConfig = { controlSetRows: [ ...seriesOrderSection, ['color_scheme'], + ['time_shift_color'], [ { name: 'seriesType', diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx index 7482c7a16b01d..a679b0f7185dd 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx @@ -297,6 +297,7 @@ const config: ControlPanelConfig = { controlSetRows: [ ...seriesOrderSection, ['color_scheme'], + ['time_shift_color'], ...showValueSection, [minorTicks], [ diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/controlPanel.tsx index ca5d3377c4f8d..8bf40c06c82f0 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/controlPanel.tsx @@ -69,6 +69,7 @@ const config: ControlPanelConfig = { controlSetRows: [ ...seriesOrderSection, ['color_scheme'], + ['time_shift_color'], [ { name: 'seriesType', diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/controlPanel.tsx index 8ca10e02fdb06..c5bbe03ffb9c6 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/controlPanel.tsx @@ -65,6 +65,7 @@ const config: ControlPanelConfig = { controlSetRows: [ ...seriesOrderSection, ['color_scheme'], + ['time_shift_color'], ...showValueSection, [ { diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/SmoothLine/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/SmoothLine/controlPanel.tsx index 45681d7be7fb6..3275fad15877d 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/SmoothLine/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/SmoothLine/controlPanel.tsx @@ -65,6 +65,7 @@ const config: ControlPanelConfig = { controlSetRows: [ ...seriesOrderSection, ['color_scheme'], + ['time_shift_color'], ...showValueSectionWithoutStack, [ { diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/controlPanel.tsx index 274f11d629337..5956d2efe1970 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/controlPanel.tsx @@ -65,6 +65,7 @@ const config: ControlPanelConfig = { controlSetRows: [ ...seriesOrderSection, ['color_scheme'], + ['time_shift_color'], [ { name: 'seriesType', 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 9dae4385e973e..23187b9fdb85b 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts @@ -92,6 +92,7 @@ import { transformTimeseriesAnnotation, } from './transformers'; import { + OpacityEnum, StackControlsValue, TIMEGRAIN_TO_TIMESTAMP, TIMESERIES_CONSTANTS, @@ -164,6 +165,7 @@ export default function transformProps( sortSeriesAscending, timeGrainSqla, timeCompare, + timeShiftColor, stack, tooltipTimeFormat, tooltipSortByMetric, @@ -274,7 +276,7 @@ export default function transformProps( const array = ensureIsArray(chartProps.rawFormData?.time_compare); const inverted = invert(verboseMap); - const offsetLineWidths = {}; + const offsetLineWidths: { [key: string]: number } = {}; rawSeries.forEach(entry => { const derivedSeries = isDerivedSeries(entry, chartProps.rawFormData); @@ -289,6 +291,7 @@ export default function transformProps( } lineStyle.type = 'dashed'; lineStyle.width = offsetLineWidths[offset]; + lineStyle.opacity = OpacityEnum.DerivedSeries; } const entryName = String(entry.name || ''); @@ -327,6 +330,7 @@ export default function transformProps( isHorizontal, lineStyle, timeCompare: array, + timeShiftColor, }, ); if (transformedSeries) { diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts index 91649ecd55b79..e492581ec1780 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts @@ -167,6 +167,7 @@ export function transformSeries( lineStyle?: LineStyleOption; queryIndex?: number; timeCompare?: string[]; + timeShiftColor?: boolean; }, ): SeriesOption | undefined { const { name } = series; @@ -190,10 +191,12 @@ export function transformSeries( showValueIndexes = [], thresholdValues = [], richTooltip, + seriesKey, sliceId, isHorizontal = false, queryIndex = 0, timeCompare = [], + timeShiftColor, } = opts; const contexts = seriesContexts[name || ''] || []; const hasForecast = @@ -209,7 +212,7 @@ export function transformSeries( filterState?.selectedValues && !filterState?.selectedValues.includes(name); const opacity = isFiltered ? OpacityEnum.SemiTransparent - : OpacityEnum.NonTransparent; + : opts.lineStyle?.opacity || OpacityEnum.NonTransparent; // don't create a series if doing a stack or area chart and the result // is a confidence band @@ -241,11 +244,22 @@ export function transformSeries( } else { plotType = seriesType === 'bar' ? 'bar' : 'line'; } - // forcing the colorScale to return a different color for same metrics across different queries - const itemStyle = { - color: colorScale(colorScaleKey, sliceId), + /** + * if timeShiftColor is enabled the colorScaleKey forces the color to be the + * same as the original series, otherwise uses separate colors + * */ + const itemStyle: ItemStyleOption = { + color: timeShiftColor + ? colorScale(colorScaleKey, sliceId) + : colorScale(seriesKey || forecastSeries.name, sliceId), opacity, + borderWidth: 0, }; + if (seriesType === 'bar' && connectNulls) { + itemStyle.borderWidth = 1.5; + itemStyle.borderType = 'dotted'; + itemStyle.borderColor = itemStyle.color; + } let emphasis = {}; let showSymbol = false; if (!isConfidenceBand) { diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts index 6ca9650db62ef..5e3507dc6c5ae 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts @@ -55,6 +55,7 @@ export type EchartsTimeseriesFormData = QueryFormData & { annotationLayers: AnnotationLayer[]; area: boolean; colorScheme?: string; + timeShiftColor?: boolean; contributionMode?: ContributionType; forecastEnabled: boolean; forecastPeriods: number; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts b/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts index b0b87bd188e5c..5d33d0138d43e 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts @@ -66,6 +66,7 @@ export const LABEL_POSITION: [LabelPositionEnum, string][] = [ export enum OpacityEnum { Transparent = 0, SemiTransparent = 0.3, + DerivedSeries = 0.7, NonTransparent = 1, } diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/transformers.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/transformers.test.ts new file mode 100644 index 0000000000000..d138ed0c5dcb1 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/transformers.test.ts @@ -0,0 +1,84 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { CategoricalColorScale } from '@superset-ui/core'; +import { EchartsTimeseriesSeriesType } from '@superset-ui/plugin-chart-echarts'; +import { transformSeries } from '../../src/Timeseries/transformers'; + +// Mock the colorScale function +const mockColorScale = jest.fn( + (key: string, sliceId?: number) => `color-for-${key}-${sliceId}`, +) as unknown as CategoricalColorScale; + +describe('transformSeries', () => { + const series = { name: 'test-series' }; + + test('should use the colorScaleKey if timeShiftColor is enabled', () => { + const opts = { + timeShiftColor: true, + colorScaleKey: 'test-key', + sliceId: 1, + }; + + const result = transformSeries(series, mockColorScale, 'test-key', opts); + + expect((result as any)?.itemStyle.color).toBe('color-for-test-key-1'); + }); + + test('should use seriesKey if timeShiftColor is not enabled', () => { + const opts = { + timeShiftColor: false, + seriesKey: 'series-key', + sliceId: 2, + }; + + const result = transformSeries(series, mockColorScale, 'test-key', opts); + + expect((result as any)?.itemStyle.color).toBe('color-for-series-key-2'); + }); + + test('should apply border styles for bar series with connectNulls', () => { + const opts = { + seriesType: EchartsTimeseriesSeriesType.Bar, + connectNulls: true, + timeShiftColor: false, + }; + + const result = transformSeries(series, mockColorScale, 'test-key', opts); + + expect((result as any).itemStyle.borderWidth).toBe(1.5); + expect((result as any).itemStyle.borderType).toBe('dotted'); + expect((result as any).itemStyle.borderColor).toBe( + (result as any).itemStyle.color, + ); + }); + + test('should not apply border styles for non-bar series', () => { + const opts = { + seriesType: EchartsTimeseriesSeriesType.Line, + connectNulls: true, + timeShiftColor: false, + }; + + const result = transformSeries(series, mockColorScale, 'test-key', opts); + + expect((result as any).itemStyle.borderWidth).toBe(0); + expect((result as any).itemStyle.borderType).toBeUndefined(); + expect((result as any).itemStyle.borderColor).toBeUndefined(); + }); +});