This is an automated email from the ASF dual-hosted git repository.
msyavuz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new 4a7cdccdad5 fix: Heatmap does not render correctly on normalization
(#37208)
4a7cdccdad5 is described below
commit 4a7cdccdad50d211deeadace0033ca2d25a999dd
Author: Jonathan Alberth Quispe Fuentes <[email protected]>
AuthorDate: Mon Feb 2 04:34:46 2026 -0500
fix: Heatmap does not render correctly on normalization (#37208)
---
.../src/Heatmap/transformProps.ts | 29 +++++++-
.../test/Heatmap/buildQuery.test.ts | 82 ++++++++++++++++++++++
.../test/Heatmap/transformProps.test.ts | 68 ++++++++++++++++++
3 files changed, 176 insertions(+), 3 deletions(-)
diff --git
a/superset-frontend/plugins/plugin-chart-echarts/src/Heatmap/transformProps.ts
b/superset-frontend/plugins/plugin-chart-echarts/src/Heatmap/transformProps.ts
index 118082015cd..60c6aceaf29 100644
---
a/superset-frontend/plugins/plugin-chart-echarts/src/Heatmap/transformProps.ts
+++
b/superset-frontend/plugins/plugin-chart-echarts/src/Heatmap/transformProps.ts
@@ -46,6 +46,12 @@ type EChartsOption = ComposeOption<HeatmapSeriesOption>;
const DEFAULT_ECHARTS_BOUNDS = [0, 200];
+/**
+ * Column name for the rank values added by the backend's rank post-processing
operation.
+ * This is used when the heatmap is in normalized mode to color cells by
percentile rank.
+ */
+const RANK_COLUMN_NAME = 'rank';
+
/**
* Extract unique values for an axis from the data.
* Filters out null and undefined values.
@@ -212,7 +218,7 @@ export default function transformProps(
currencyFormats = {},
currencyCodeColumn,
} = datasource;
- const colorColumn = normalized ? 'rank' : metricLabel;
+ const colorColumn = normalized ? RANK_COLUMN_NAME : metricLabel;
const colors = getSequentialSchemeRegistry().get(linearColorScheme)?.colors;
const getAxisFormatter =
(colType: GenericDataType) => (value: number | string) => {
@@ -291,6 +297,7 @@ export default function transformProps(
const xValue = row[xAxisColumnName];
const yValue = row[yAxisColumnName];
const metricValue = row[metricLabel];
+ const rankValue = row[RANK_COLUMN_NAME];
// Convert to axis indices for ECharts when explicit axis data is
provided
const xIndex = xAxisIndexMap.get(xValue);
@@ -304,8 +311,21 @@ export default function transformProps(
);
return [];
}
- return [[xIndex, yIndex, metricValue] as [number, number, any]];
- }),
+ if (normalized && rankValue === undefined) {
+ logging.error(
+ `Heatmap: Skipping row due to missing rank value. xValue:
${xValue}, yValue: ${yValue}, metricValue: ${metricValue}`,
+ row,
+ );
+ return [];
+ }
+
+ // Include rank as 4th dimension when normalized is enabled
+ // This allows visualMap to use dimension: 3 to color by rank
percentile
+ if (normalized) {
+ return [[xIndex, yIndex, metricValue, rankValue]];
+ }
+ return [[xIndex, yIndex, metricValue]];
+ }) as any,
label: {
show: showValues,
formatter: (params: CallbackDataParams) => {
@@ -336,6 +356,9 @@ export default function transformProps(
bottom: bottomMargin,
left: leftMargin,
},
+ legend: {
+ show: false,
+ },
series,
tooltip: {
...getDefaultTooltip(refs),
diff --git
a/superset-frontend/plugins/plugin-chart-echarts/test/Heatmap/buildQuery.test.ts
b/superset-frontend/plugins/plugin-chart-echarts/test/Heatmap/buildQuery.test.ts
new file mode 100644
index 00000000000..d4d64f37419
--- /dev/null
+++
b/superset-frontend/plugins/plugin-chart-echarts/test/Heatmap/buildQuery.test.ts
@@ -0,0 +1,82 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { QueryFormData } from '@superset-ui/core';
+import buildQuery from '../../src/Heatmap/buildQuery';
+
+describe('Heatmap buildQuery - Rank Operation for Normalized Field', () => {
+ const baseFormData = {
+ datasource: '5__table',
+ granularity_sqla: 'ds',
+ metric: 'count',
+ x_axis: 'category',
+ groupby: ['region'],
+ viz_type: 'heatmap',
+ } as QueryFormData;
+
+ test('should ALWAYS include rank operation when normalized=true', () => {
+ const formData = {
+ ...baseFormData,
+ normalized: true,
+ };
+
+ const queryContext = buildQuery(formData);
+ const [query] = queryContext.queries;
+
+ const rankOperation = query.post_processing?.find(
+ op => op?.operation === 'rank',
+ );
+
+ expect(rankOperation).toBeDefined();
+ expect(rankOperation?.operation).toBe('rank');
+ });
+
+ test('should ALWAYS include rank operation when normalized=false', () => {
+ const formData = {
+ ...baseFormData,
+ normalized: false,
+ };
+
+ const queryContext = buildQuery(formData);
+ const [query] = queryContext.queries;
+
+ const rankOperation = query.post_processing?.find(
+ op => op?.operation === 'rank',
+ );
+
+ expect(rankOperation).toBeDefined();
+ expect(rankOperation?.operation).toBe('rank');
+ });
+
+ test('should ALWAYS include rank operation when normalized is undefined', ()
=> {
+ const formData = {
+ ...baseFormData,
+ // normalized not set
+ };
+
+ const queryContext = buildQuery(formData);
+ const [query] = queryContext.queries;
+
+ const rankOperation = query.post_processing?.find(
+ op => op?.operation === 'rank',
+ );
+
+ expect(rankOperation).toBeDefined();
+ expect(rankOperation?.operation).toBe('rank');
+ });
+});
diff --git
a/superset-frontend/plugins/plugin-chart-echarts/test/Heatmap/transformProps.test.ts
b/superset-frontend/plugins/plugin-chart-echarts/test/Heatmap/transformProps.test.ts
index 23912ea8aaa..2fa5775ade1 100644
---
a/superset-frontend/plugins/plugin-chart-echarts/test/Heatmap/transformProps.test.ts
+++
b/superset-frontend/plugins/plugin-chart-echarts/test/Heatmap/transformProps.test.ts
@@ -291,4 +291,72 @@ describe('Heatmap transformProps', () => {
// Y-axis: numbers sorted numerically (1, 2, 10 NOT 1, 10, 2)
expect(yAxisData).toEqual([1, 2, 10]);
});
+
+ test('should include rank as 4th dimension when normalized is true', () => {
+ const dataWithRank = [
+ { day_of_week: 'Monday', hour: 9, count: 10, rank: 0.33 },
+ { day_of_week: 'Monday', hour: 14, count: 15, rank: 0.67 },
+ { day_of_week: 'Wednesday', hour: 11, count: 8, rank: 0.17 },
+ { day_of_week: 'Friday', hour: 16, count: 20, rank: 1.0 },
+ ];
+
+ const chartProps = createChartProps({ normalized: true }, dataWithRank);
+
+ const result = transformProps(chartProps as HeatmapChartProps);
+
+ const seriesData = (result.echartOptions.series as any)[0].data;
+
+ // Each data point should be [xIndex, yIndex, metricValue, rankValue]
+ expect(Array.isArray(seriesData)).toBe(true);
+ expect(seriesData.length).toBe(4);
+
+ // Check that data points have 4 dimensions when normalized
+ seriesData.forEach((point: any) => {
+ expect(Array.isArray(point)).toBe(true);
+ expect(point.length).toBe(4);
+ // First two should be indices (numbers)
+ expect(typeof point[0]).toBe('number');
+ expect(typeof point[1]).toBe('number');
+ // Third should be the metric value
+ expect(typeof point[2]).toBe('number');
+ // Fourth should be the rank value
+ expect(typeof point[3]).toBe('number');
+ expect(point[3]).toBeGreaterThanOrEqual(0);
+ expect(point[3]).toBeLessThanOrEqual(1);
+ });
+
+ // visualMap should use dimension 3 (4th element) for coloring
+ expect((result.echartOptions.visualMap as any).dimension).toBe(3);
+ });
+
+ test('should use 3 dimensions when normalized is false', () => {
+ const chartProps = createChartProps({ normalized: false });
+ const result = transformProps(chartProps as HeatmapChartProps);
+
+ const seriesData = (result.echartOptions.series as any)[0].data;
+
+ // Each data point should be [xIndex, yIndex, metricValue]
+ seriesData.forEach((point: any) => {
+ expect(point.length).toBe(3);
+ });
+
+ // visualMap should use dimension 2 (3rd element) for coloring
+ expect((result.echartOptions.visualMap as any).dimension).toBe(2);
+ });
+
+ test('should always hide legend regardless of showLegend setting', () => {
+ // Test with showLegend: true
+ const chartPropsWithLegend = createChartProps({ showLegend: true });
+ const resultWithLegend = transformProps(
+ chartPropsWithLegend as HeatmapChartProps,
+ );
+ expect((resultWithLegend.echartOptions.legend as any).show).toBe(false);
+
+ // Test with showLegend: false
+ const chartPropsWithoutLegend = createChartProps({ showLegend: false });
+ const resultWithoutLegend = transformProps(
+ chartPropsWithoutLegend as HeatmapChartProps,
+ );
+ expect((resultWithoutLegend.echartOptions.legend as any).show).toBe(false);
+ });
});