From 2e2c81c8d06718556bbb1eaea908a2392201599e Mon Sep 17 00:00:00 2001 From: susiwen8 Date: Thu, 9 Apr 2026 00:02:22 +0800 Subject: [PATCH 1/2] fix(heatmap): render empty cells consistently --- src/chart/heatmap/HeatmapView.ts | 32 ++++++++--- test/ut/spec/series/heatmap.test.ts | 82 +++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 8 deletions(-) create mode 100644 test/ut/spec/series/heatmap.test.ts diff --git a/src/chart/heatmap/HeatmapView.ts b/src/chart/heatmap/HeatmapView.ts index 4e21c179e6..78a5b943f1 100644 --- a/src/chart/heatmap/HeatmapView.ts +++ b/src/chart/heatmap/HeatmapView.ts @@ -21,6 +21,7 @@ import * as graphic from '../../util/graphic'; import { toggleHoverEmphasis } from '../../util/states'; import HeatmapLayer from './HeatmapLayer'; import * as zrUtil from 'zrender/src/core/util'; +import tokens from '../../visual/tokens'; import ChartView from '../../view/Chart'; import HeatmapSeriesModel, { HeatmapDataItemOption } from './HeatmapSeries'; import type GlobalModel from '../../model/Global'; @@ -29,7 +30,7 @@ import type VisualMapModel from '../../component/visualMap/VisualMapModel'; import type PiecewiseModel from '../../component/visualMap/PiecewiseModel'; import type ContinuousModel from '../../component/visualMap/ContinuousModel'; import { CoordinateSystem, isCoordinateSystemType } from '../../coord/CoordinateSystem'; -import { StageHandlerProgressParams, Dictionary, OptionDataValue } from '../../util/types'; +import { StageHandlerProgressParams, Dictionary, OptionDataValue, ZRColor } from '../../util/types'; import type Cartesian2D from '../../coord/cartesian/Cartesian2D'; import type Calendar from '../../coord/calendar/Calendar'; import { setLabelStyle, getLabelStatesModels } from '../../label/labelStyle'; @@ -130,11 +131,12 @@ class HeatmapView extends ChartView { this.group.removeAll(); const coordSys = seriesModel.coordinateSystem; + const emptyCellFill: ZRColor = ecModel.get('backgroundColor') || tokens.color.neutral00; if (coordSys.type === 'cartesian2d' || coordSys.type === 'calendar' || coordSys.type === 'matrix' ) { - this._renderOnGridLike(seriesModel, api, 0, seriesModel.getData().count()); + this._renderOnGridLike(seriesModel, api, emptyCellFill, 0, seriesModel.getData().count()); } else if (isGeoCoordSys(coordSys)) { this._renderOnGeo( @@ -154,6 +156,7 @@ class HeatmapView extends ChartView { api: ExtensionAPI ) { const coordSys = seriesModel.coordinateSystem; + const emptyCellFill: ZRColor = ecModel.get('backgroundColor') || tokens.color.neutral00; if (coordSys) { // geo does not support incremental rendering? if (isGeoCoordSys(coordSys)) { @@ -161,7 +164,7 @@ class HeatmapView extends ChartView { } else { this._progressiveEls = []; - this._renderOnGridLike(seriesModel, api, params.start, params.end, true); + this._renderOnGridLike(seriesModel, api, emptyCellFill, params.start, params.end, true); } } } @@ -173,6 +176,7 @@ class HeatmapView extends ChartView { _renderOnGridLike( seriesModel: HeatmapSeriesModel, api: ExtensionAPI, + emptyCellFill: ZRColor, start: number, end: number, incremental?: boolean @@ -236,10 +240,11 @@ class HeatmapView extends ChartView { if (isCartesian2d) { const dataDimX = data.get(dataDims[0], idx); const dataDimY = data.get(dataDims[1], idx); + const value = data.get(dataDims[2], idx) as number; - // Ignore empty data and out of extent data - if (isNaN(data.get(dataDims[2], idx) as number) - || isNaN(dataDimX as number) + // Ignore out of extent data. Preserve empty cells so splitArea + // does not show through inconsistently behind them. + if (isNaN(dataDimX as number) || isNaN(dataDimY as number) || dataDimX < xAxisExtent[0] || dataDimX > xAxisExtent[1] @@ -253,6 +258,7 @@ class HeatmapView extends ChartView { dataDimX, dataDimY ]); + const emptyCell = isNaN(value); rect = new graphic.Rect({ shape: { @@ -261,10 +267,15 @@ class HeatmapView extends ChartView { width, height }, - style + style: emptyCell + ? zrUtil.extend(zrUtil.extend({}, style), { + fill: emptyCellFill + }) + : style }); } else if (isMatrix) { + const value = data.get(dataDims[2], idx) as number; const shape = coordSys.dataToLayout([ data.get(dataDims[0], idx), data.get(dataDims[1], idx) @@ -272,10 +283,15 @@ class HeatmapView extends ChartView { if (zrUtil.eqNaN(shape.x)) { continue; } + const emptyCell = isNaN(value); rect = new graphic.Rect({ z2: 1, shape, - style, + style: emptyCell + ? zrUtil.extend(zrUtil.extend({}, style), { + fill: emptyCellFill + }) + : style, }); } else { // Calendar diff --git a/test/ut/spec/series/heatmap.test.ts b/test/ut/spec/series/heatmap.test.ts new file mode 100644 index 0000000000..2c87802ec0 --- /dev/null +++ b/test/ut/spec/series/heatmap.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 { EChartsType } from '@/src/echarts'; +import { createChart, getGraphicElements } from '../../core/utHelper'; + +describe('heatmap', function () { + let chart: EChartsType; + + beforeEach(function () { + chart = createChart({ + width: 240, + height: 160 + }); + }); + + afterEach(function () { + chart.dispose(); + }); + + it('should render empty cells so splitArea does not leak through', function () { + chart.setOption({ + backgroundColor: '#fff', + animation: false, + visualMap: { + min: 0, + max: 10, + show: false + }, + grid: { + left: 20, + right: 20, + top: 20, + bottom: 20 + }, + xAxis: { + type: 'category', + data: ['a', 'b', 'c'], + splitArea: { + show: true + } + }, + yAxis: { + type: 'category', + data: ['row'], + splitArea: { + show: true + } + }, + series: [{ + type: 'heatmap', + data: [ + [0, 0, 8], + [1, 0, ''], + [2, 0, ''] + ] + }] + }, true); + + const rects = getGraphicElements(chart, 'series') + .filter(el => el.type === 'rect'); + + expect(rects).toHaveLength(3); + expect(rects.filter(rect => (rect as any).style.fill === '#fff')).toHaveLength(2); + }); +}); From adb71c1e6ac7ae23650179b2308ec80bc5bd6d49 Mon Sep 17 00:00:00 2001 From: susiwen8 Date: Fri, 10 Apr 2026 09:48:20 +0800 Subject: [PATCH 2/2] chore(heatmap): address review feedback --- src/chart/heatmap/HeatmapView.ts | 4 +-- test/ut/spec/series/heatmap.test.ts | 45 +++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/chart/heatmap/HeatmapView.ts b/src/chart/heatmap/HeatmapView.ts index 78a5b943f1..5a39a47743 100644 --- a/src/chart/heatmap/HeatmapView.ts +++ b/src/chart/heatmap/HeatmapView.ts @@ -131,7 +131,7 @@ class HeatmapView extends ChartView { this.group.removeAll(); const coordSys = seriesModel.coordinateSystem; - const emptyCellFill: ZRColor = ecModel.get('backgroundColor') || tokens.color.neutral00; + const emptyCellFill: ZRColor = ecModel.get('backgroundColor') || tokens.color.background; if (coordSys.type === 'cartesian2d' || coordSys.type === 'calendar' || coordSys.type === 'matrix' @@ -156,7 +156,7 @@ class HeatmapView extends ChartView { api: ExtensionAPI ) { const coordSys = seriesModel.coordinateSystem; - const emptyCellFill: ZRColor = ecModel.get('backgroundColor') || tokens.color.neutral00; + const emptyCellFill: ZRColor = ecModel.get('backgroundColor') || tokens.color.background; if (coordSys) { // geo does not support incremental rendering? if (isGeoCoordSys(coordSys)) { diff --git a/test/ut/spec/series/heatmap.test.ts b/test/ut/spec/series/heatmap.test.ts index 2c87802ec0..27a251e4b5 100644 --- a/test/ut/spec/series/heatmap.test.ts +++ b/test/ut/spec/series/heatmap.test.ts @@ -19,6 +19,7 @@ import { EChartsType } from '@/src/echarts'; import { createChart, getGraphicElements } from '../../core/utHelper'; +import tokens from '@/src/visual/tokens'; describe('heatmap', function () { let chart: EChartsType; @@ -79,4 +80,48 @@ describe('heatmap', function () { expect(rects).toHaveLength(3); expect(rects.filter(rect => (rect as any).style.fill === '#fff')).toHaveLength(2); }); + + it('should use token background for empty cells by default', function () { + chart.setOption({ + animation: false, + visualMap: { + min: 0, + max: 10, + show: false + }, + grid: { + left: 20, + right: 20, + top: 20, + bottom: 20 + }, + xAxis: { + type: 'category', + data: ['a', 'b'], + splitArea: { + show: true + } + }, + yAxis: { + type: 'category', + data: ['row'], + splitArea: { + show: true + } + }, + series: [{ + type: 'heatmap', + data: [ + [0, 0, 8], + [1, 0, ''] + ] + }] + }, true); + + const rects = getGraphicElements(chart, 'series') + .filter(el => el.type === 'rect'); + + expect(rects).toHaveLength(2); + expect(rects.filter(rect => (rect as any).style.fill === tokens.color.background)).toHaveLength(1); + }); });