diff --git a/src/chart/tree/TreeView.ts b/src/chart/tree/TreeView.ts index c819e19716..446a48f2a1 100644 --- a/src/chart/tree/TreeView.ts +++ b/src/chart/tree/TreeView.ts @@ -267,6 +267,18 @@ class TreeView extends ChartView { const oldMin = this._min; const oldMax = this._max; + // `bbox.fromPoints` leaves `min`/`max` untouched when there is no valid point + // (e.g. `series.data` is empty, or no node has a layout yet). They would stay + // empty arrays, making `max[0] - min[0]` NaN, which the zero-size checks below + // do not correct, and the resulting dataRect yields a non-invertible view + // transform. Seed them so the checks below can expand a degenerate rect. + if (!points.length) { + min[0] = oldMin ? oldMin[0] : 0; + min[1] = oldMin ? oldMin[1] : 0; + max[0] = oldMax ? oldMax[0] : 0; + max[1] = oldMax ? oldMax[1] : 0; + } + // If width or height is 0 if (max[0] - min[0] === 0) { min[0] = oldMin ? oldMin[0] : min[0] - 1; @@ -575,7 +587,9 @@ function removeNodeEdge( } const sourceSymbolEl = data.getItemGraphicEl(source.dataIndex) as TreeSymbol; - const sourceEdge = sourceSymbolEl.__edge; + // The source node may already have been removed in the same pass, which resets its + // graphic element to null (see `removeNode`). Guard it like `symbolEl` above. + const sourceEdge = sourceSymbolEl && sourceSymbolEl.__edge; // 1. when expand the sub tree, delete the children node should delete the edge of // the source at the same time. because the polyline edge shape is only owned by the source. diff --git a/test/ut/spec/series/treeUpdate.test.ts b/test/ut/spec/series/treeUpdate.test.ts new file mode 100644 index 0000000000..2b759e9b1a --- /dev/null +++ b/test/ut/spec/series/treeUpdate.test.ts @@ -0,0 +1,89 @@ +/* +* 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 { each } from 'zrender/src/core/util'; +import { createChart } from '../../core/utHelper'; +import { EChartsType } from '../../../../src/echarts'; + +const FULL_TREE = [{ + name: 'root', + children: [ + {name: 'c1', children: [{name: 'g1'}, {name: 'g2'}]}, + {name: 'c2', children: [{name: 'g3'}]} + ] +}]; + +describe('tree_update', function () { + + let chart: EChartsType; + beforeEach(function () { + chart = createChart({width: 400, height: 300}); + }); + afterEach(function () { + chart.dispose(); + }); + + function setTree(data: unknown, animation: boolean, notMerge?: boolean) { + chart.setOption({ + animation: animation, + series: [{type: 'tree', data: data}] + }, notMerge); + } + + // `bbox.fromPoints` writes nothing when there is no point, so `min`/`max` stayed + // empty and the derived dataRect was NaN, making the view transform non-invertible. + it('should render a tree whose data is empty', function () { + expect(function () { + setTree([], false); + }).not.toThrow(); + }); + + each([false, true], function (animation) { + it(`should render a tree emptied after having data (animation: ${animation})`, function () { + setTree(FULL_TREE, animation); + expect(function () { + setTree([], animation, true); + }).not.toThrow(); + }); + }); + + // Removing several nodes in one pass resets their graphic elements to null as it + // goes, so a node's source may already be gone when its edge is removed. + each([false, true], function (animation) { + each([ + {name: 'a whole subtree', data: [{name: 'root', children: [{name: 'c2', children: [{name: 'g3'}]}]}]}, + {name: 'all but the root', data: [{name: 'root'}]} + ], function (removeCase) { + it(`should remove ${removeCase.name} (animation: ${animation})`, function () { + setTree(FULL_TREE, animation); + expect(function () { + setTree(removeCase.data, animation, true); + }).not.toThrow(); + }); + }); + }); + + it('should render a tree that gets data after being empty', function () { + setTree([], false); + expect(function () { + setTree(FULL_TREE, false, true); + }).not.toThrow(); + }); + +});