mirror of
https://github.com/apache/superset.git
synced 2026-05-12 19:35:17 +00:00
fix(echarts): fix stacked horizontal bar chart clipping and duplicate x-axis labels (#39012)
(cherry picked from commit 022342839a)
This commit is contained in:
committed by
Michael S. Molina
parent
b1eb6ac7c9
commit
bf399b9f97
@@ -659,7 +659,10 @@ export default function transformProps(
|
||||
for (const s of series) {
|
||||
if (s.id) {
|
||||
const columnsArr = labelMap[s.id];
|
||||
(s as any).stack = columnsArr[idxSelectedDimension];
|
||||
const dimensionValue = columnsArr?.[idxSelectedDimension];
|
||||
if (dimensionValue !== undefined) {
|
||||
(s as any).stack = dimensionValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -682,9 +685,24 @@ export default function transformProps(
|
||||
|
||||
// For horizontal bar charts, set max/min from calculated data bounds
|
||||
if (shouldCalculateDataBounds) {
|
||||
// Set max to actual data max to avoid gaps and ensure labels are visible
|
||||
if (dataMax !== undefined && yAxisMax === undefined) {
|
||||
yAxisMax = dataMax;
|
||||
// For stacked charts, clamp against the per-row stacked total to avoid
|
||||
// clipping bars. Also keep dataMax so that mixed-sign stacks (where
|
||||
// positive and negative values cancel in the algebraic row sum) cannot
|
||||
// produce an axis max smaller than the largest individual positive segment.
|
||||
const stackedTotalMax = Math.max(
|
||||
...sortedTotalValues.filter(
|
||||
(v): v is number => typeof v === 'number' && !Number.isNaN(v),
|
||||
),
|
||||
);
|
||||
const effectiveDataMax = stack
|
||||
? Math.max(dataMax ?? Number.NEGATIVE_INFINITY, stackedTotalMax)
|
||||
: dataMax;
|
||||
if (
|
||||
effectiveDataMax !== undefined &&
|
||||
Number.isFinite(effectiveDataMax) &&
|
||||
yAxisMax === undefined
|
||||
) {
|
||||
yAxisMax = effectiveDataMax;
|
||||
}
|
||||
// Set min to actual data min for diverging bars
|
||||
if (dataMin !== undefined && yAxisMin === undefined && dataMin < 0) {
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
} from '@superset-ui/core';
|
||||
import { GenericDataType } from '@apache-superset/core/common';
|
||||
import { supersetTheme } from '@apache-superset/core/theme';
|
||||
import { StackControlsValue } from '../../../src/constants';
|
||||
import type {
|
||||
GridComponentOption,
|
||||
LegendComponentOption,
|
||||
@@ -727,6 +728,97 @@ describe('Bar Chart X-axis Time Formatting', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Horizontal stacked bar chart axis bounds', () => {
|
||||
// Dataset where each series max = 4 but stacked total max = 8
|
||||
const stackedData: ChartDataResponseResult[] = [
|
||||
createTestQueryData(
|
||||
[
|
||||
{ team: 'Team A', High: 2, Low: 2, Medium: 4 },
|
||||
{ team: 'Team B', High: null, Low: null, Medium: 3 },
|
||||
{ team: 'Team C', High: null, Low: null, Medium: 1 },
|
||||
],
|
||||
{
|
||||
colnames: ['team', 'High', 'Low', 'Medium'],
|
||||
coltypes: [
|
||||
GenericDataType.String,
|
||||
GenericDataType.Numeric,
|
||||
GenericDataType.Numeric,
|
||||
GenericDataType.Numeric,
|
||||
],
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
const horizontalStackedFormData: EchartsTimeseriesFormData = {
|
||||
...(baseFormData as EchartsTimeseriesFormData),
|
||||
x_axis: 'team',
|
||||
metric: ['High', 'Low', 'Medium'],
|
||||
groupby: [],
|
||||
orientation: OrientationType.Horizontal,
|
||||
seriesType: EchartsTimeseriesSeriesType.Bar,
|
||||
stack: StackControlsValue.Stack,
|
||||
truncateYAxis: true,
|
||||
};
|
||||
|
||||
test('xAxis.max uses stacked total, not individual series max', () => {
|
||||
// Individual series max = 4 (Medium), stacked total for Team A = 8
|
||||
// Without the fix, xAxis.max would be 4, clipping bars and duplicating labels
|
||||
const chartProps = createEchartsTimeseriesTestChartProps<
|
||||
EchartsTimeseriesFormData,
|
||||
EchartsTimeseriesChartProps
|
||||
>({
|
||||
defaultFormData: horizontalStackedFormData,
|
||||
defaultVizType: 'echarts_timeseries_bar',
|
||||
defaultQueriesData: stackedData,
|
||||
});
|
||||
|
||||
const { echartOptions } = transformProps(chartProps);
|
||||
const xAxis = echartOptions.xAxis as any;
|
||||
|
||||
// xAxis.max must be >= stacked total (8), not capped at individual series max (4)
|
||||
expect(xAxis.max).toBeGreaterThanOrEqual(8);
|
||||
});
|
||||
|
||||
test('xAxis.max is not set to individual series max when stacking', () => {
|
||||
const chartProps = createEchartsTimeseriesTestChartProps<
|
||||
EchartsTimeseriesFormData,
|
||||
EchartsTimeseriesChartProps
|
||||
>({
|
||||
defaultFormData: horizontalStackedFormData,
|
||||
defaultVizType: 'echarts_timeseries_bar',
|
||||
defaultQueriesData: stackedData,
|
||||
});
|
||||
|
||||
const { echartOptions } = transformProps(chartProps);
|
||||
const xAxis = echartOptions.xAxis as any;
|
||||
|
||||
// 4 is the individual series max — the axis should not be clipped there
|
||||
expect(xAxis.max).not.toBe(4);
|
||||
});
|
||||
|
||||
test('non-stacked horizontal bar chart still uses individual series max', () => {
|
||||
const nonStackedFormData: EchartsTimeseriesFormData = {
|
||||
...horizontalStackedFormData,
|
||||
stack: null,
|
||||
};
|
||||
|
||||
const chartProps = createEchartsTimeseriesTestChartProps<
|
||||
EchartsTimeseriesFormData,
|
||||
EchartsTimeseriesChartProps
|
||||
>({
|
||||
defaultFormData: nonStackedFormData,
|
||||
defaultVizType: 'echarts_timeseries_bar',
|
||||
defaultQueriesData: stackedData,
|
||||
});
|
||||
|
||||
const { echartOptions } = transformProps(chartProps);
|
||||
const xAxis = echartOptions.xAxis as any;
|
||||
|
||||
// Without stacking, xAxis.max should be based on individual series values
|
||||
expect(xAxis.max).toBe(4);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Legend layout regressions', () => {
|
||||
const getBottomLegendLayout = (
|
||||
chartWidth: number,
|
||||
|
||||
Reference in New Issue
Block a user