From 144524f996a93fef6939e812f195f9918c09da42 Mon Sep 17 00:00:00 2001 From: Antonio Rivero <38889534+Antonio-RiveroMartnez@users.noreply.github.com> Date: Fri, 22 Mar 2024 08:24:38 +0100 Subject: [PATCH] feat(bar_chart): Stacked Bar chart with Time comparison in separated stacks (#27589) --- .../src/Timeseries/transformProps.ts | 1 + .../src/Timeseries/transformers.ts | 8 ++-- .../plugin-chart-echarts/src/utils/series.ts | 38 ++++++++++++++++++- .../test/utils/series.test.ts | 31 +++++++++++++++ 4 files changed, 74 insertions(+), 4 deletions(-) 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 8b4998ded0e35..0ee54f4577968 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts @@ -308,6 +308,7 @@ export default function transformProps( sliceId, isHorizontal, lineStyle, + timeCompare: array, }, ); 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 be89fdfc74c96..b5ff791fbefd2 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts @@ -62,7 +62,7 @@ import { formatAnnotationLabel, parseAnnotationOpacity, } from '../utils/annotation'; -import { getChartPadding } from '../utils/series'; +import { getChartPadding, getTimeCompareStackId } from '../utils/series'; import { OpacityEnum, StackControlsValue, @@ -164,6 +164,7 @@ export function transformSeries( isHorizontal?: boolean; lineStyle?: LineStyleOption; queryIndex?: number; + timeCompare?: string[]; }, ): SeriesOption | undefined { const { name } = series; @@ -188,6 +189,7 @@ export function transformSeries( sliceId, isHorizontal = false, queryIndex = 0, + timeCompare = [], } = opts; const contexts = seriesContexts[name || ''] || []; const hasForecast = @@ -217,9 +219,9 @@ export function transformSeries( } else if (stack && isObservation) { // the suffix of the observation series is '' (falsy), which disables // stacking. Therefore we need to set something that is truthy. - stackId = 'obs'; + stackId = getTimeCompareStackId('obs', timeCompare, name); } else if (stack && isTrend) { - stackId = forecastSeries.type; + stackId = getTimeCompareStackId(forecastSeries.type, timeCompare, name); } let plotType; if ( diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts b/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts index cebfe374d9cbd..cb97dff93a395 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts @@ -35,7 +35,7 @@ import { } from '@superset-ui/core'; import { SortSeriesType } from '@superset-ui/chart-controls'; import { format, LegendComponentOption, SeriesOption } from 'echarts'; -import { maxBy, meanBy, minBy, orderBy, sumBy } from 'lodash'; +import { isEmpty, maxBy, meanBy, minBy, orderBy, sumBy } from 'lodash'; import { NULL_STRING, StackControlsValue, @@ -604,3 +604,39 @@ export function getMinAndMaxFromBounds( } return {}; } + +/** + * Returns the stackId used in stacked series. + * It will return the defaultId if the chart is not using time comparison. + * If time comparison is used, it will return the time comparison value as the stackId + * if the name includes the time comparison value. + * + * @param {string} defaultId The default stackId. + * @param {string[]} timeCompare The time comparison values. + * @param {string | number} name The name of the serie. + * + * @returns {string} The stackId. + */ +export function getTimeCompareStackId( + defaultId: string, + timeCompare: string[], + name?: string | number, +): string { + if (isEmpty(timeCompare)) { + return defaultId; + } + // Each timeCompare is its own stack so it doesn't stack on top of original ones + return ( + timeCompare.find(value => { + if (typeof name === 'string') { + // offset is represented as , group by list + return ( + name.includes(`${value},`) || + // offset is represented as __ + name.includes(`__${value}`) + ); + } + return name?.toString().includes(value); + }) || defaultId + ); +} diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/utils/series.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/utils/series.test.ts index 6e4848643eba9..efc0ac745aedf 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/test/utils/series.test.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/test/utils/series.test.ts @@ -40,6 +40,7 @@ import { sanitizeHtml, sortAndFilterSeries, sortRows, + getTimeCompareStackId, } from '../../src/utils/series'; import { EchartsTimeseriesSeriesType, @@ -1041,3 +1042,33 @@ test('getMinAndMaxFromBounds returns automatic lower bound when truncating', () scale: true, }); }); + +describe('getTimeCompareStackId', () => { + it('returns the defaultId when timeCompare is empty', () => { + const result = getTimeCompareStackId('default', []); + expect(result).toEqual('default'); + }); + + it('returns the defaultId when no value in timeCompare is included in name', () => { + const result = getTimeCompareStackId( + 'default', + ['compare1', 'compare2'], + 'test__name', + ); + expect(result).toEqual('default'); + }); + + it('returns the first value in timeCompare that is included in name', () => { + const result = getTimeCompareStackId( + 'default', + ['compare1', 'compare2'], + 'test__compare1', + ); + expect(result).toEqual('compare1'); + }); + + it('handles name being a number', () => { + const result = getTimeCompareStackId('default', ['123', '456'], 123); + expect(result).toEqual('123'); + }); +});