feat(bar-chart): add option to color bars by primary axis when no dimensions are set (#37531)

Co-authored-by: madhushree agarwal <madhushree_agarwal@apple.com>
This commit is contained in:
madhushreeag
2026-03-03 16:11:04 -08:00
committed by GitHub
parent f10cb14d92
commit 983b633972
6 changed files with 470 additions and 18 deletions

View File

@@ -41,6 +41,7 @@ import {
xAxisLabelRotation,
xAxisLabelInterval,
forceMaxInterval,
colorByPrimaryAxisSection,
} from '../../../controls';
import { OrientationType } from '../../types';
@@ -328,6 +329,7 @@ const config: ControlPanelConfig = {
['color_scheme'],
['time_shift_color'],
...showValueSectionWithoutStream,
...colorByPrimaryAxisSection,
[
{
name: 'stackDimension',

View File

@@ -175,6 +175,7 @@ export default function transformProps(
seriesType,
showLegend,
showValue,
colorByPrimaryAxis,
sliceId,
sortSeriesType,
sortSeriesAscending,
@@ -421,6 +422,7 @@ export default function transformProps(
timeShiftColor,
theme,
hasDimensions: (groupBy?.length ?? 0) > 0,
colorByPrimaryAxis,
},
);
if (transformedSeries) {
@@ -438,6 +440,59 @@ export default function transformProps(
}
});
// Add x-axis color legend when colorByPrimaryAxis is enabled
if (colorByPrimaryAxis && groupBy.length === 0 && series.length > 0) {
// Hide original series from legend
series.forEach(s => {
s.legendHoverLink = false;
});
// Get x-axis values from the first series
const firstSeries = series[0];
if (firstSeries && Array.isArray(firstSeries.data)) {
const xAxisValues: (string | number)[] = [];
// Extract primary axis values (category axis)
// For horizontal charts the category is at index 1, for vertical at index 0
const primaryAxisIndex = isHorizontal ? 1 : 0;
(firstSeries.data as any[]).forEach(point => {
let xValue;
if (point && typeof point === 'object' && 'value' in point) {
const val = point.value;
xValue = Array.isArray(val) ? val[primaryAxisIndex] : val;
} else if (Array.isArray(point)) {
xValue = point[primaryAxisIndex];
} else {
xValue = point;
}
xAxisValues.push(xValue);
});
// Create hidden series for legend (using 'line' type to not affect bar width)
// Deduplicate x-axis values to avoid duplicate legend entries and unnecessary series
const uniqueXAxisValues = Array.from(
new Set(xAxisValues.map(v => String(v))),
);
uniqueXAxisValues.forEach(xValue => {
const colorKey = xValue;
series.push({
name: xValue,
type: 'line', // Use line type to not affect bar positioning
data: [], // Empty - doesn't render
itemStyle: {
color: colorScale(colorKey, sliceId),
},
lineStyle: {
color: colorScale(colorKey, sliceId),
},
silent: true,
legendHoverLink: false,
showSymbol: false,
});
});
}
}
if (stack === StackControlsValue.Stream) {
const baselineSeries = getBaselineSeriesForStream(
series.map(entry => entry.data) as [string | number, number][][],
@@ -592,14 +647,43 @@ export default function transformProps(
isHorizontal,
);
const legendData = rawSeries
.filter(
entry =>
extractForecastSeriesContext(entry.name || '').type ===
ForecastSeriesEnum.Observation,
)
.map(entry => entry.name || '')
.concat(extractAnnotationLabels(annotationLayers));
const legendData =
colorByPrimaryAxis && groupBy.length === 0 && series.length > 0
? // When colorByPrimaryAxis is enabled, show only primary axis values (deduped + filtered)
(() => {
const firstSeries = series[0];
// For horizontal charts the category is at index 1, for vertical at index 0
const primaryAxisIndex = isHorizontal ? 1 : 0;
if (firstSeries && Array.isArray(firstSeries.data)) {
const names = (firstSeries.data as any[])
.map(point => {
if (point && typeof point === 'object' && 'value' in point) {
const val = point.value;
return String(
Array.isArray(val) ? val[primaryAxisIndex] : val,
);
}
if (Array.isArray(point)) {
return String(point[primaryAxisIndex]);
}
return String(point);
})
.filter(
name => name !== '' && name !== 'undefined' && name !== 'null',
);
return Array.from(new Set(names));
}
return [];
})()
: // Otherwise show original series names
rawSeries
.filter(
entry =>
extractForecastSeriesContext(entry.name || '').type ===
ForecastSeriesEnum.Observation,
)
.map(entry => entry.name || '')
.concat(extractAnnotationLabels(annotationLayers));
let xAxis: any = {
type: xAxisType,
@@ -818,10 +902,27 @@ export default function transformProps(
padding,
),
scrollDataIndex: legendIndex || 0,
data: legendData.sort((a: string, b: string) => {
if (!legendSort) return 0;
return legendSort === 'asc' ? a.localeCompare(b) : b.localeCompare(a);
}) as string[],
data:
colorByPrimaryAxis && groupBy.length === 0
? // When colorByPrimaryAxis, configure legend items with roundRect icons
legendData.map(name => ({
name,
icon: 'roundRect',
}))
: // Otherwise use normal legend data
legendData.sort((a: string, b: string) => {
if (!legendSort) return 0;
return legendSort === 'asc'
? a.localeCompare(b)
: b.localeCompare(a);
}),
// Disable legend selection and buttons when colorByPrimaryAxis is enabled
...(colorByPrimaryAxis && groupBy.length === 0
? {
selectedMode: false, // Disable clicking legend items
selector: false, // Hide All/Invert buttons
}
: {}),
},
series: dedupSeries(reorderForecastSeries(series) as SeriesOption[]),
toolbox: {

View File

@@ -181,6 +181,31 @@ export function optimizeBarLabelPlacement(
return (series.data as TimeseriesDataRecord[]).map(transformValue);
}
export function applyColorByPrimaryAxis(
series: SeriesOption,
colorScale: CategoricalColorScale,
sliceId: number | undefined,
opacity: number,
isHorizontal = false,
): {
value: [string | number, number];
itemStyle: { color: string; opacity: number; borderWidth: number };
}[] {
return (series.data as [string | number, number][]).map(value => {
// For horizontal charts the primary axis is index 1 (category), not index 0 (numeric)
const colorKey = String(isHorizontal ? value[1] : value[0]);
return {
value,
itemStyle: {
color: colorScale(colorKey, sliceId),
opacity,
borderWidth: 0,
},
};
});
}
export function transformSeries(
series: SeriesOption,
colorScale: CategoricalColorScale,
@@ -214,6 +239,7 @@ export function transformSeries(
timeShiftColor?: boolean;
theme?: SupersetTheme;
hasDimensions?: boolean;
colorByPrimaryAxis?: boolean;
},
): SeriesOption | undefined {
const { name, data } = series;
@@ -244,6 +270,7 @@ export function transformSeries(
timeCompare = [],
timeShiftColor,
theme,
colorByPrimaryAxis = false,
} = opts;
const contexts = seriesContexts[name || ''] || [];
const hasForecast =
@@ -349,17 +376,27 @@ export function transformSeries(
return {
...series,
...(Array.isArray(data) && seriesType === 'bar'
? {
data: optimizeBarLabelPlacement(series, isHorizontal),
}
...(Array.isArray(data)
? colorByPrimaryAxis
? {
data: applyColorByPrimaryAxis(
series,
colorScale,
sliceId,
opacity,
isHorizontal,
),
}
: seriesType === 'bar' && !stack
? { data: optimizeBarLabelPlacement(series, isHorizontal) }
: null
: null),
connectNulls,
queryIndex,
yAxisIndex,
name: forecastSeries.name,
itemStyle,
// @ts-expect-error
...(colorByPrimaryAxis ? {} : { itemStyle }),
// @ts-ignore
type: plotType,
smooth: seriesType === 'smooth',
triggerLineEvent: true,

View File

@@ -97,6 +97,7 @@ export type EchartsTimeseriesFormData = QueryFormData & {
onlyTotal: boolean;
showExtraControls: boolean;
percentageThreshold: number;
colorByPrimaryAxis?: boolean;
orientation?: OrientationType;
} & LegendFormData &
TitleFormData;