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);
+  });
 });

Reply via email to