diff --git a/src/chart/heatmap/HeatmapView.ts b/src/chart/heatmap/HeatmapView.ts index f8b912a3d7..bfa781a31f 100644 --- a/src/chart/heatmap/HeatmapView.ts +++ b/src/chart/heatmap/HeatmapView.ts @@ -121,6 +121,13 @@ class HeatmapView extends ChartView { this.group.removeAll(); const coordSys = seriesModel.coordinateSystem; + // The coordinate system may be unresolved, e.g. a heatmap that + // references a `calendar`/`geo`/`bmap` coordinate system whose component + // is missing. Skip rendering instead of throwing on `coordSys.type` + // (`incrementalRender` already guards this the same way). See #19060. + if (!coordSys) { + return; + } if (coordSys.type === 'cartesian2d' || coordSys.type === 'calendar' || coordSys.type === 'matrix' diff --git a/src/coord/Axis.ts b/src/coord/Axis.ts index 1bcb13a01b..e4f986ed25 100644 --- a/src/coord/Axis.ts +++ b/src/coord/Axis.ts @@ -336,8 +336,10 @@ function fixOnBandTicksCoords( return false; } + const axisExtent = axis.getExtent(); + const coordStep = axisExtent[1] >= axisExtent[0] ? bandWidth : -bandWidth; each(preTicksCoords, function (ticksItem) { - ticksItem.coord -= bandWidth / 2; + ticksItem.coord -= coordStep / 2; }); const dataExtent = axis.scale.getExtent(); @@ -346,7 +348,7 @@ function fixOnBandTicksCoords( preTicksCoords.pop(); } preTicksCoords.push({ - coord: oldLast.coord + bandWidth, + coord: oldLast.coord + coordStep, tick: {value: dataExtent[1] + 1}, }); diff --git a/src/coord/View.ts b/src/coord/View.ts index 0cba662625..ac2c75616e 100644 --- a/src/coord/View.ts +++ b/src/coord/View.ts @@ -559,12 +559,18 @@ function legacyCopyOverallTrans( target: Transformable | NullUndefined, overallTrans: Transformable, mtOverall: MatrixArray, - mtOverallInv: MatrixArray + mtOverallInv: MatrixArray | NullUndefined ): void { if (target) { copyTransform(target, overallTrans); matrixCopy(target.transform || (target.transform = []), mtOverall); - matrixCopy(target.invTransform || (target.invTransform = []), mtOverallInv); + const invTransform = target.invTransform || (target.invTransform = matrixCreate()); + if (mtOverallInv) { + // `matrixInvert` returns null if the overall transform is singular (e.g., a + // zero-sized view rect when the chart container is hidden or sized 0x0); + // keep the last inverse in that case rather than crash. + matrixCopy(invTransform, mtOverallInv); + } } } diff --git a/src/processor/dataStack.ts b/src/processor/dataStack.ts index ab41b864d8..eb167cb965 100644 --- a/src/processor/dataStack.ts +++ b/src/processor/dataStack.ts @@ -150,7 +150,11 @@ function calculateStack(stackInfoList: StackInfo[]) { // Considering positive stack, negative stack and empty data if ( - stackStrategy === 'all' // single stack group + // A null/NaN value in a lower series must be excluded from the + // stack, otherwise it corrupts the sum. The other strategies + // already reject it via their `val > 0`/`val < 0` checks, but + // `all` needs an explicit guard. See #21685. + (stackStrategy === 'all' && !isNaN(val)) // single stack group || (stackStrategy === 'positive' && val > 0) || (stackStrategy === 'negative' && val < 0) || (stackStrategy === 'samesign' && sum >= 0 && val > 0) // All positive stack diff --git a/test/bar-markArea.html b/test/bar-markArea.html index fc2eefe9f0..755fb4d8dc 100644 --- a/test/bar-markArea.html +++ b/test/bar-markArea.html @@ -43,6 +43,7 @@
+
+ + + diff --git a/test/ut/spec/component/geo/geo.test.ts b/test/ut/spec/component/geo/geo.test.ts new file mode 100644 index 0000000000..055351f2eb --- /dev/null +++ b/test/ut/spec/component/geo/geo.test.ts @@ -0,0 +1,75 @@ +/* +* 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, registerMap } from '../../../../../src/echarts'; +import { GeoJSON } from '../../../../../src/coord/geo/geoTypes'; +import { createChart } from '../../../core/utHelper'; + +describe('geo', function () { + + const testGeoJson: GeoJSON = { + 'type': 'FeatureCollection', + 'features': [ + { + 'type': 'Feature', + 'geometry': { + 'type': 'Polygon', + 'coordinates': [ + [ + [0, 0], + [10, 0], + [10, 10], + [0, 10] + ] + ] + }, + 'properties': { + 'name': 'A', + 'childNum': 1 + } + } + ] + }; + registerMap('geo_test_zero_size', testGeoJson); + + let chart: EChartsType; + afterEach(function () { + chart.dispose(); + }); + + it('should not throw when the chart size is zero', function () { + // A hidden (display:none) container makes the chart 0x0. The geo view + // transform is singular then and must not crash. See #21706. + chart = createChart({ opts: { width: 0, height: 0 } }); + expect(function () { + chart.setOption({ + geo: { + map: 'geo_test_zero_size' + }, + series: [ + { + type: 'scatter', + coordinateSystem: 'geo', + data: [{ name: 'x', value: [5, 5, 1] }] + } + ] + }); + }).not.toThrow(); + }); +}); diff --git a/test/ut/spec/data/dataStack.test.ts b/test/ut/spec/data/dataStack.test.ts new file mode 100644 index 0000000000..42e230c665 --- /dev/null +++ b/test/ut/spec/data/dataStack.test.ts @@ -0,0 +1,71 @@ +/* +* 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 { createChart, getECModel } from '../../core/utHelper'; +import { EChartsType } from '../../../../src/echarts'; + +describe('processor/dataStack', function () { + + let chart: EChartsType; + beforeEach(function () { + chart = createChart(); + }); + + afterEach(function () { + chart.dispose(); + }); + + function getStackResult(seriesIndex: number, dataIndex: number): number { + const data = getECModel(chart).getSeriesByIndex(seriesIndex).getData(); + const stackResultDim = data.getCalculationInfo('stackResultDimension'); + return data.get(stackResultDim, dataIndex) as number; + } + + // Regression test for #21685: with `stackStrategy: 'all'`, a null/'-' value + // in a lower series used to corrupt the stacked sum (NaN), making the upper + // series' bar disappear. The null should be excluded from the stack instead. + it('excludes null values from the stack when stackStrategy is "all" (#21685)', function () { + chart.setOption({ + xAxis: { type: 'category', data: ['A', 'B', 'C'] }, + yAxis: { type: 'value' }, + series: [ + { + type: 'bar', + stack: 'total', + data: [10, null, 30] + }, + { + type: 'bar', + stack: 'total', + stackStrategy: 'all', + data: [5, 5, 5] + } + ] + }); + + // Index 0 and 2: the lower series has valid values, so they stack normally. + expect(getStackResult(1, 0)).toBe(15); + expect(getStackResult(1, 2)).toBe(35); + + // Index 1: the lower series is null. It must be excluded from the stack, + // so the upper series keeps its own value instead of becoming NaN. + expect(getStackResult(1, 1)).toBe(5); + }); + +}); diff --git a/test/ut/spec/series/heatmap.test.ts b/test/ut/spec/series/heatmap.test.ts new file mode 100644 index 0000000000..df27f38690 --- /dev/null +++ b/test/ut/spec/series/heatmap.test.ts @@ -0,0 +1,57 @@ +/* +* 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 { createChart } from '../../core/utHelper'; +import { EChartsType } from '../../../../src/echarts'; + +describe('series/heatmap', function () { + + let chart: EChartsType; + beforeEach(function () { + chart = createChart(); + }); + + afterEach(function () { + chart.dispose(); + }); + + // Regression test for #19060: a heatmap whose coordinate system cannot be + // resolved (e.g. `coordinateSystem: 'calendar'` without a calendar + // component) used to throw "Cannot read properties of undefined (reading + // 'type')" in HeatmapView.render, aborting the whole chart render. + it('does not throw when the heatmap coordinate system is missing (#19060)', function () { + function setMisconfiguredHeatmap() { + chart.setOption({ + visualMap: { + min: 0, + max: 10, + calculable: true + }, + series: [{ + type: 'heatmap', + coordinateSystem: 'calendar', + data: [['2023-01-01', 5]] + }] + }); + } + + expect(setMisconfiguredHeatmap).not.toThrow(); + }); + +});