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 0abefc95e3d..01acf1ff7af 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts @@ -171,7 +171,7 @@ export function transformSeries( timeShiftColor?: boolean; }, ): SeriesOption | undefined { - const { name } = series; + const { name, data } = series; const { area, connectNulls, @@ -290,6 +290,9 @@ export function transformSeries( : { ...opts.lineStyle, opacity }; return { ...series, + ...(Array.isArray(data) && seriesType === 'bar' && !stack + ? { data: transformNegativeLabelsPosition(series, isHorizontal) } + : null), connectNulls, queryIndex, yAxisIndex, @@ -627,3 +630,30 @@ export function getPadding( isHorizontal, ); } + +export function transformNegativeLabelsPosition( + series: SeriesOption, + isHorizontal: boolean, +): TimeseriesDataRecord[] { + /* + * Adjusts label position for negative values in bar series + * @param series - Array of series options + * @param isHorizontal - Whether chart is horizontal + * @returns data with adjusted label positions for negative values + */ + const transformValue = (value: any) => { + const [xValue, yValue] = Array.isArray(value) ? value : [null, null]; + const axisValue = isHorizontal ? xValue : yValue; + + return axisValue < 0 + ? { + value, + label: { + position: 'outside', + }, + } + : value; + }; + + return (series.data as TimeseriesDataRecord[]).map(transformValue); +} 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 index a0b3d5ad7d2..8f04e1c4031 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/transformers.test.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/transformers.test.ts @@ -17,8 +17,12 @@ * under the License. */ import { CategoricalColorScale } from '@superset-ui/core'; +import type { SeriesOption } from 'echarts'; import { EchartsTimeseriesSeriesType } from '../../src'; -import { transformSeries } from '../../src/Timeseries/transformers'; +import { + transformSeries, + transformNegativeLabelsPosition, +} from '../../src/Timeseries/transformers'; // Mock the colorScale function const mockColorScale = jest.fn( @@ -82,3 +86,107 @@ describe('transformSeries', () => { expect((result as any).itemStyle.borderColor).toBeUndefined(); }); }); + +describe('transformNegativeLabelsPosition', () => { + test('label position bottom of negative value no Horizontal', () => { + const isHorizontal = false; + const series: SeriesOption = { + data: [ + [2020, 1], + [2021, 3], + [2022, -2], + [2023, -5], + [2024, 4], + ], + type: EchartsTimeseriesSeriesType.Bar, + stack: undefined, + }; + const result = + Array.isArray(series.data) && series.type === 'bar' && !series.stack + ? transformNegativeLabelsPosition(series, isHorizontal) + : series.data; + expect((result as any)[0].label).toBe(undefined); + expect((result as any)[1].label).toBe(undefined); + expect((result as any)[2].label.position).toBe('outside'); + expect((result as any)[3].label.position).toBe('outside'); + expect((result as any)[4].label).toBe(undefined); + }); + + test('label position left of negative value is Horizontal', () => { + const isHorizontal = true; + const series: SeriesOption = { + data: [ + [1, 2020], + [-3, 2021], + [2, 2022], + [-4, 2023], + [-6, 2024], + ], + type: EchartsTimeseriesSeriesType.Bar, + stack: undefined, + }; + + const result = + Array.isArray(series.data) && series.type === 'bar' && !series.stack + ? transformNegativeLabelsPosition(series, isHorizontal) + : series.data; + expect((result as any)[0].label).toBe(undefined); + expect((result as any)[1].label.position).toBe('outside'); + expect((result as any)[2].label).toBe(undefined); + expect((result as any)[3].label.position).toBe('outside'); + expect((result as any)[4].label.position).toBe('outside'); + }); + + test('label position to line type', () => { + const isHorizontal = false; + const series: SeriesOption = { + data: [ + [2020, 1], + [2021, 3], + [2022, -2], + [2023, -5], + [2024, 4], + ], + type: EchartsTimeseriesSeriesType.Line, + stack: undefined, + }; + + const result = + Array.isArray(series.data) && + !series.stack && + series.type !== 'line' && + series.type === 'bar' + ? transformNegativeLabelsPosition(series, isHorizontal) + : series.data; + expect((result as any)[0].label).toBe(undefined); + expect((result as any)[1].label).toBe(undefined); + expect((result as any)[2].label).toBe(undefined); + expect((result as any)[3].label).toBe(undefined); + expect((result as any)[4].label).toBe(undefined); + }); + + test('label position to bar type and stack', () => { + const isHorizontal = false; + const series: SeriesOption = { + data: [ + [2020, 1], + [2021, 3], + [2022, -2], + [2023, -5], + [2024, 4], + ], + type: EchartsTimeseriesSeriesType.Bar, + stack: 'obs', + }; + + const result = + Array.isArray(series.data) && series.type === 'bar' && !series.stack + ? transformNegativeLabelsPosition(series, isHorizontal) + : series.data; + expect((result as any)[0].label).toBe(undefined); + expect((result as any)[1].label).toBe(undefined); + expect((result as any)[2].label).toBe(undefined); + expect((result as any)[3].label).toBe(undefined); + expect((result as any)[4].label).toBe(undefined); + }); +});