From 604847d5632195444a9eedcf7469db846935b32f Mon Sep 17 00:00:00 2001 From: Evan Rusackas Date: Tue, 19 Aug 2025 11:50:09 -0700 Subject: [PATCH] fix(echarts): Display NULL values in categorical x-axis for bar charts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #31481 When using a categorical x-axis in ECharts bar charts, NULL values were being filtered out and not displayed. This was inconsistent with other chart types like pie charts which correctly show NULL values. The issue was in the extractSeries function which was not handling NULL values properly for categorical axes. The fix converts NULL values to the NULL_STRING constant ('') when the x-axis type is categorical, allowing them to be displayed as a valid category in the chart. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../src/MixedTimeseries/transformProps.ts | 10 +++-- .../src/Timeseries/transformProps.ts | 6 +-- .../plugin-chart-echarts/src/utils/series.ts | 7 +++- .../test/utils/series.test.ts | 37 +++++++++++++++++++ 4 files changed, 52 insertions(+), 8 deletions(-) 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 b8fbe1adf58f..add3cf372439 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts @@ -243,6 +243,10 @@ export default function transformProps( const MetricDisplayNameA = getMetricDisplayName(metrics[0], verboseMap); const MetricDisplayNameB = getMetricDisplayName(metricsB[0], verboseMap); + const dataTypes = getColtypesMapping(queriesData[0]); + const xAxisDataType = dataTypes?.[xAxisLabel] ?? dataTypes?.[xAxisOrig]; + const xAxisType = getAxisType(stack, xAxisForceCategorical, xAxisDataType); + const [rawSeriesA, sortedTotalValuesA] = extractSeries(rebasedDataA, { fillNeighborValue: stack ? 0 : undefined, xAxis: xAxisLabel, @@ -250,6 +254,7 @@ export default function transformProps( sortSeriesAscending, stack, totalStackedValues, + xAxisType, }); const rebasedDataB = rebaseForecastDatum(data2, verboseMap); const { @@ -267,11 +272,8 @@ export default function transformProps( sortSeriesAscending: sortSeriesAscendingB, stack: Boolean(stackB), totalStackedValues: totalStackedValuesB, + xAxisType, }); - - const dataTypes = getColtypesMapping(queriesData[0]); - const xAxisDataType = dataTypes?.[xAxisLabel] ?? dataTypes?.[xAxisOrig]; - const xAxisType = getAxisType(stack, xAxisForceCategorical, xAxisDataType); const series: SeriesOption[] = []; const formatter = contributionMode ? getNumberFormatter(',.0%') 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 8aa294106fbf..94cb666261b3 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts @@ -233,6 +233,8 @@ export default function transformProps( ); const isMultiSeries = groupBy.length || metrics?.length > 1; + const xAxisDataType = dataTypes?.[xAxisLabel] ?? dataTypes?.[xAxisOrig]; + const xAxisType = getAxisType(stack, xAxisForceCategorical, xAxisDataType); const [rawSeries, sortedTotalValues, minPositiveValue] = extractSeries( rebasedData, @@ -247,6 +249,7 @@ export default function transformProps( sortSeriesAscending, xAxisSortSeries: isMultiSeries ? xAxisSort : undefined, xAxisSortSeriesAscending: isMultiSeries ? xAxisSortAsc : undefined, + xAxisType, }, ); const showValueIndexes = extractShowValueIndexes(rawSeries, { @@ -259,9 +262,6 @@ export default function transformProps( rawSeries.map(series => series.name as string), ); const isAreaExpand = stack === StackControlsValue.Expand; - const xAxisDataType = dataTypes?.[xAxisLabel] ?? dataTypes?.[xAxisOrig]; - - const xAxisType = getAxisType(stack, xAxisForceCategorical, xAxisDataType); const series: SeriesOption[] = []; const forcePercentFormatter = Boolean(contributionMode || isAreaExpand); 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 4e3a1bd74e2c..e2fb9c2722a3 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts @@ -272,6 +272,7 @@ export function extractSeries( sortSeriesAscending?: boolean; xAxisSortSeries?: SortSeriesType; xAxisSortSeriesAscending?: boolean; + xAxisType?: AxisType; } = {}, ): [SeriesOption[], number[], number | undefined] { const { @@ -286,11 +287,15 @@ export function extractSeries( sortSeriesAscending, xAxisSortSeries, xAxisSortSeriesAscending, + xAxisType, } = opts; if (data.length === 0) return [[], [], undefined]; const rows: DataRecord[] = data.map(datum => ({ ...datum, - [xAxis]: datum[xAxis], + [xAxis]: + datum[xAxis] === null && xAxisType === AxisType.Category + ? NULL_STRING + : datum[xAxis], })); const sortedSeries = sortAndFilterSeries( rows, 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 5f54c63f94d3..9f8b07911939 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 @@ -493,6 +493,43 @@ describe('extractSeries', () => { ]); }); + it('should convert NULL x-values to NULL_STRING for categorical axis', () => { + const data = [ + { + browser: 'Firefox', + count: 5, + }, + { + browser: null, + count: 10, + }, + { + browser: 'Chrome', + count: 8, + }, + ]; + expect( + extractSeries(data, { + xAxis: 'browser', + xAxisType: AxisType.Category, + }), + ).toEqual([ + [ + { + id: 'count', + name: 'count', + data: [ + ['Firefox', 5], + [NULL_STRING, 10], + ['Chrome', 8], + ], + }, + ], + [], + 5, + ]); + }); + it('should do missing value imputation', () => { const data = [ {