This is an automated email from the ASF dual-hosted git repository. susiwen8 pushed a commit to branch codex/tree-center-orient in repository https://gitbox.apache.org/repos/asf/echarts.git
commit 98d3fe21e6b4c67cad5197861522d4ee4728520a Author: susiwen8 <[email protected]> AuthorDate: Mon May 4 12:53:35 2026 +0800 Enable centered tree expansion for mind-map layouts Tree diagrams need a way to keep the root visually centered while first-level branches expand to both sides. The new center orient reuses the existing orthogonal layout pass per side, maps each side into the left or right half of the layout box, and keeps the existing LR/RL/TB/BT/radial behavior on the normal path. Constraint: Keep the feature inside the existing tree series layout and rendering model without adding dependencies. Rejected: Add a separate layout type | center expansion is still an orthogonal tree orientation, and a new layout would duplicate existing traversal behavior. Confidence: high Scope-risk: moderate Reversibility: clean Directive: Root-child side assignment is only honored for orient:center; do not apply side to nested nodes without adding nested-side tests. Tested: npx jest --config test/ut/jest.config.cjs --coverage=false test/ut/spec/series/tree.test.ts --runInBand Tested: npx eslint src/chart/tree/TreeSeries.ts src/chart/tree/treeLayout.ts src/chart/tree/TreeView.ts test/ut/spec/series/tree.test.ts Tested: npx tsc --noEmit Tested: git diff --check -- src/chart/tree/TreeSeries.ts src/chart/tree/TreeView.ts src/chart/tree/treeLayout.ts test/tree-center.html test/ut/spec/series/tree.test.ts Tested: Chrome headless screenshot of test/tree-center.html saved at .codex-artifacts/tree-center.png Not-tested: Full visual regression suite --- src/chart/tree/TreeSeries.ts | 11 +- src/chart/tree/TreeView.ts | 118 ++++++++++++++----- src/chart/tree/treeLayout.ts | 249 +++++++++++++++++++++++++++++---------- test/tree-center.html | 127 ++++++++++++++++++++ test/ut/spec/series/tree.test.ts | 151 ++++++++++++++++++++++++ 5 files changed, 562 insertions(+), 94 deletions(-) diff --git a/src/chart/tree/TreeSeries.ts b/src/chart/tree/TreeSeries.ts index 6c8826f74..aae9a5c53 100644 --- a/src/chart/tree/TreeSeries.ts +++ b/src/chart/tree/TreeSeries.ts @@ -70,6 +70,12 @@ export interface TreeSeriesNodeItemOption extends SymbolOptionMixin<CallbackData children?: TreeSeriesNodeItemOption[] + /** + * Only works on root children when `orient` is 'center'. + * If not specified, root children are split by their original order. + */ + side?: 'left' | 'right' + collapsed?: boolean link?: string @@ -103,8 +109,9 @@ export interface TreeSeriesOption extends /** * The orient of orthoginal layout, can be setted to 'LR', 'TB', 'RL', 'BT'. * and the backward compatibility configuration 'horizontal = LR', 'vertical = TB'. + * 'center' puts the root in the center and lays out root children to both left and right sides. */ - orient?: 'LR' | 'TB' | 'RL' | 'BT' | 'horizontal' | 'vertical' + orient?: 'LR' | 'TB' | 'RL' | 'BT' | 'center' | 'horizontal' | 'vertical' expandAndCollapse?: boolean @@ -318,4 +325,4 @@ class TreeSeriesModel extends SeriesModel<TreeSeriesOption> { }; } -export default TreeSeriesModel; \ No newline at end of file +export default TreeSeriesModel; diff --git a/src/chart/tree/TreeView.ts b/src/chart/tree/TreeView.ts index f7482687b..ed5edb61b 100644 --- a/src/chart/tree/TreeView.ts +++ b/src/chart/tree/TreeView.ts @@ -31,6 +31,7 @@ import ChartView from '../../view/Chart'; import TreeSeriesModel, { TreeSeriesOption, TreeSeriesNodeItemOption } from './TreeSeries'; import Path, { PathProps, PathStyleProps } from 'zrender/src/graphic/Path'; import GlobalModel from '../../model/Global'; +import Model from '../../model/Model'; import ExtensionAPI from '../../core/ExtensionAPI'; import { TreeNode } from '../../data/Tree'; import SeriesData from '../../data/SeriesData'; @@ -66,6 +67,7 @@ interface TreeNodeLayout { y: number rawX: number rawY: number + side?: 'left' | 'right' | 'center' } class TreePath extends Path<TreeEdgePathProps> { @@ -87,40 +89,66 @@ class TreePath extends Path<TreeEdgePathProps> { buildPath(ctx: CanvasRenderingContext2D, shape: TreeEdgeShape) { const childPoints = shape.childPoints; - const childLen = childPoints.length; - const parentPoint = shape.parentPoint; - const firstChildPos = childPoints[0]; - const lastChildPos = childPoints[childLen - 1]; - - if (childLen === 1) { - ctx.moveTo(parentPoint[0], parentPoint[1]); - ctx.lineTo(firstChildPos[0], firstChildPos[1]); + const orient = shape.orient; + + if (orient === 'center') { + const leftPoints: number[][] = []; + const rightPoints: number[][] = []; + for (let i = 0; i < childPoints.length; i++) { + const point = childPoints[i]; + (point[0] < shape.parentPoint[0] ? leftPoints : rightPoints).push(point); + } + drawOrthogonalPath(ctx, shape, leftPoints); + drawOrthogonalPath(ctx, shape, rightPoints); return; } - const orient = shape.orient; - const forkDim = (orient === 'TB' || orient === 'BT') ? 0 : 1; - const otherDim = 1 - forkDim; - const forkPosition = parsePercent(shape.forkPosition, 1); - const tmpPoint = []; - tmpPoint[forkDim] = parentPoint[forkDim]; - tmpPoint[otherDim] = parentPoint[otherDim] + (lastChildPos[otherDim] - parentPoint[otherDim]) * forkPosition; + drawOrthogonalPath(ctx, shape, childPoints); + } +} + +function drawOrthogonalPath(ctx: CanvasRenderingContext2D, shape: TreeEdgeShape, childPoints: number[][]) { + const childLen = childPoints.length; + const parentPoint = shape.parentPoint; + + if (!childLen) { + return; + } + + const orient = shape.orient; + const forkDim = (orient === 'TB' || orient === 'BT') ? 0 : 1; + const orderedChildPoints = childPoints.slice().sort(function (pointA, pointB) { + return pointA[forkDim] - pointB[forkDim]; + }); + const firstChildPos = orderedChildPoints[0]; + const lastChildPos = orderedChildPoints[childLen - 1]; + if (childLen === 1) { ctx.moveTo(parentPoint[0], parentPoint[1]); - ctx.lineTo(tmpPoint[0], tmpPoint[1]); - ctx.moveTo(firstChildPos[0], firstChildPos[1]); - tmpPoint[forkDim] = firstChildPos[forkDim]; - ctx.lineTo(tmpPoint[0], tmpPoint[1]); - tmpPoint[forkDim] = lastChildPos[forkDim]; - ctx.lineTo(tmpPoint[0], tmpPoint[1]); - ctx.lineTo(lastChildPos[0], lastChildPos[1]); + ctx.lineTo(firstChildPos[0], firstChildPos[1]); + return; + } - for (let i = 1; i < childLen - 1; i++) { - const point = childPoints[i]; - ctx.moveTo(point[0], point[1]); - tmpPoint[forkDim] = point[forkDim]; - ctx.lineTo(tmpPoint[0], tmpPoint[1]); - } + const otherDim = 1 - forkDim; + const forkPosition = parsePercent(shape.forkPosition, 1); + const tmpPoint = []; + tmpPoint[forkDim] = parentPoint[forkDim]; + tmpPoint[otherDim] = parentPoint[otherDim] + (lastChildPos[otherDim] - parentPoint[otherDim]) * forkPosition; + + ctx.moveTo(parentPoint[0], parentPoint[1]); + ctx.lineTo(tmpPoint[0], tmpPoint[1]); + ctx.moveTo(firstChildPos[0], firstChildPos[1]); + tmpPoint[forkDim] = firstChildPos[forkDim]; + ctx.lineTo(tmpPoint[0], tmpPoint[1]); + tmpPoint[forkDim] = lastChildPos[forkDim]; + ctx.lineTo(tmpPoint[0], tmpPoint[1]); + ctx.lineTo(lastChildPos[0], lastChildPos[1]); + + for (let i = 1; i < childLen - 1; i++) { + const point = orderedChildPoints[i]; + ctx.moveTo(point[0], point[1]); + tmpPoint[forkDim] = point[forkDim]; + ctx.lineTo(tmpPoint[0], tmpPoint[1]); } } @@ -458,6 +486,9 @@ function updateNode( } } + else if (seriesModel.getOrient() === 'center') { + adjustCenterLabel(symbolPath, itemModel, targetLayout); + } // Handle status const focus = itemModel.get(['emphasis', 'focus']); @@ -549,7 +580,9 @@ function drawEdge( graphic.updateProps(edge as Path, { shape: { parentPoint: [targetLayout.x, targetLayout.y], - childPoints: childPoints + childPoints: childPoints, + orient: orient, + forkPosition: edgeForkPosition } }, seriesModel); } @@ -574,6 +607,29 @@ function drawEdge( } } +function adjustCenterLabel( + symbolPath: ReturnType<TreeSymbol['getSymbolPath']>, + itemModel: Model<TreeSeriesNodeItemOption>, + targetLayout: TreeNodeLayout +) { + if (targetLayout.side !== 'left' && targetLayout.side !== 'right') { + return; + } + + const normalLabelModel = itemModel.getModel('label'); + if (normalLabelModel.getShallow('position') != null) { + return; + } + + const textContent = symbolPath.getTextContent(); + if (textContent) { + symbolPath.setTextConfig({ + position: targetLayout.side + }); + textContent.setStyle('verticalAlign', 'middle'); + } +} + function removeNodeEdge( node: TreeNode, data: SeriesData, @@ -742,7 +798,7 @@ function getEdgeShape( x2 = targetLayout.x; y2 = targetLayout.y; - if (orient === 'LR' || orient === 'RL') { + if (orient === 'LR' || orient === 'RL' || orient === 'center') { cpx1 = x1 + (x2 - x1) * curvature; cpy1 = y1; cpx2 = x2 + (x1 - x2) * curvature; @@ -768,4 +824,4 @@ function getEdgeShape( }; } -export default TreeView; \ No newline at end of file +export default TreeView; diff --git a/src/chart/tree/treeLayout.ts b/src/chart/tree/treeLayout.ts index aa7ffaba3..a62630912 100644 --- a/src/chart/tree/treeLayout.ts +++ b/src/chart/tree/treeLayout.ts @@ -31,9 +31,11 @@ import { } from './layoutHelper'; import GlobalModel from '../../model/Global'; import ExtensionAPI from '../../core/ExtensionAPI'; -import TreeSeriesModel from './TreeSeries'; +import TreeSeriesModel, { TreeSeriesNodeItemOption } from './TreeSeries'; import { createBoxLayoutReference, getLayoutRect } from '../../util/layout'; +type CenterTreeSide = 'left' | 'right'; + export default function treeLayout(ecModel: GlobalModel, api: ExtensionAPI) { ecModel.eachSeriesByType('tree', function (seriesModel: TreeSeriesModel) { commonLayout(seriesModel, api); @@ -66,68 +68,193 @@ function commonLayout(seriesModel: TreeSeriesModel, api: ExtensionAPI) { const realRoot = virtualRoot.children[0]; if (realRoot) { - init(virtualRoot); - eachAfter(realRoot, firstWalk, separation); - virtualRoot.hierNode.modifier = -realRoot.hierNode.prelim; - eachBefore(realRoot, secondWalk); - - let left = realRoot; - let right = realRoot; - let bottom = realRoot; - eachBefore(realRoot, function (node: TreeLayoutNode) { - const x = node.getLayout().x; - if (x < left.getLayout().x) { - left = node; - } - if (x > right.getLayout().x) { - right = node; - } - if (node.depth > bottom.depth) { - bottom = node; - } + const orient = seriesModel.getOrient(); + if (layout === 'orthogonal' && orient === 'center') { + centerLayout(virtualRoot, realRoot, width, height, separation); + } + else { + normalLayout(virtualRoot, realRoot, layout, orient, width, height, separation); + } + } +} + +function prepareTreeLayout( + virtualRoot: TreeLayoutNode, + realRoot: TreeLayoutNode, + separation: ReturnType<typeof sep> +) { + init(virtualRoot); + eachAfter(realRoot, firstWalk, separation); + virtualRoot.hierNode.modifier = -realRoot.hierNode.prelim; + eachBefore(realRoot, secondWalk); +} + +function normalLayout( + virtualRoot: TreeLayoutNode, + realRoot: TreeLayoutNode, + layout: 'orthogonal' | 'radial', + orient: ReturnType<TreeSeriesModel['getOrient']>, + width: number, + height: number, + separation: ReturnType<typeof sep> +) { + prepareTreeLayout(virtualRoot, realRoot, separation); + + let left = realRoot; + let right = realRoot; + let bottom = realRoot; + eachBefore(realRoot, function (node: TreeLayoutNode) { + const x = node.getLayout().x; + if (x < left.getLayout().x) { + left = node; + } + if (x > right.getLayout().x) { + right = node; + } + if (node.depth > bottom.depth) { + bottom = node; + } + }); + + const delta = left === right ? 1 : separation(left, right) / 2; + const tx = delta - left.getLayout().x; + let kx = 0; + let ky = 0; + let coorX = 0; + let coorY = 0; + if (layout === 'radial') { + kx = width / (right.getLayout().x + delta + tx); + // here we use (node.depth - 1), because the real root's depth is 1 + ky = height / ((bottom.depth - 1) || 1); + eachBefore(realRoot, function (node) { + coorX = (node.getLayout().x + tx) * kx; + coorY = (node.depth - 1) * ky; + const finalCoor = radialCoordinate(coorX, coorY); + node.setLayout({x: finalCoor.x, y: finalCoor.y, rawX: coorX, rawY: coorY}, true); + }); + } + else if (orient === 'RL' || orient === 'LR') { + ky = height / (right.getLayout().x + delta + tx); + kx = width / ((bottom.depth - 1) || 1); + eachBefore(realRoot, function (node) { + coorY = (node.getLayout().x + tx) * ky; + coorX = orient === 'LR' + ? (node.depth - 1) * kx + : width - (node.depth - 1) * kx; + node.setLayout({x: coorX, y: coorY}, true); }); + } + else if (orient === 'TB' || orient === 'BT') { + kx = width / (right.getLayout().x + delta + tx); + ky = height / ((bottom.depth - 1) || 1); + eachBefore(realRoot, function (node) { + coorX = (node.getLayout().x + tx) * kx; + coorY = orient === 'TB' + ? (node.depth - 1) * ky + : height - (node.depth - 1) * ky; + node.setLayout({x: coorX, y: coorY}, true); + }); + } +} + +function centerLayout( + virtualRoot: TreeLayoutNode, + realRoot: TreeLayoutNode, + width: number, + height: number, + separation: ReturnType<typeof sep> +) { + const originalChildren = realRoot.children.slice(); + const leftChildren: TreeLayoutNode[] = []; + const rightChildren: TreeLayoutNode[] = []; + const autoLeftCount = Math.floor(originalChildren.length / 2); - const delta = left === right ? 1 : separation(left, right) / 2; - const tx = delta - left.getLayout().x; - let kx = 0; - let ky = 0; - let coorX = 0; - let coorY = 0; - if (layout === 'radial') { - kx = width / (right.getLayout().x + delta + tx); - // here we use (node.depth - 1), bucause the real root's depth is 1 - ky = height / ((bottom.depth - 1) || 1); - eachBefore(realRoot, function (node) { - coorX = (node.getLayout().x + tx) * kx; - coorY = (node.depth - 1) * ky; - const finalCoor = radialCoordinate(coorX, coorY); - node.setLayout({x: finalCoor.x, y: finalCoor.y, rawX: coorX, rawY: coorY}, true); - }); + for (let i = 0; i < originalChildren.length; i++) { + const child = originalChildren[i]; + const side = getRootChildSide(child, i, autoLeftCount); + (side === 'left' ? leftChildren : rightChildren).push(child); + } + + let bottom = realRoot; + eachBefore(realRoot, function (node: TreeLayoutNode) { + if (node.depth > bottom.depth) { + bottom = node; } - else { - const orient = seriesModel.getOrient(); - if (orient === 'RL' || orient === 'LR') { - ky = height / (right.getLayout().x + delta + tx); - kx = width / ((bottom.depth - 1) || 1); - eachBefore(realRoot, function (node) { - coorY = (node.getLayout().x + tx) * ky; - coorX = orient === 'LR' - ? (node.depth - 1) * kx - : width - (node.depth - 1) * kx; - node.setLayout({x: coorX, y: coorY}, true); - }); - } - else if (orient === 'TB' || orient === 'BT') { - kx = width / (right.getLayout().x + delta + tx); - ky = height / ((bottom.depth - 1) || 1); - eachBefore(realRoot, function (node) { - coorX = (node.getLayout().x + tx) * kx; - coorY = orient === 'TB' - ? (node.depth - 1) * ky - : height - (node.depth - 1) * ky; - node.setLayout({x: coorX, y: coorY}, true); - }); - } + }); + + const centerX = width / 2; + const centerY = height / 2; + const kx = centerX / ((bottom.depth - realRoot.depth) || 1); + + try { + layoutCenterSide(virtualRoot, realRoot, leftChildren, 'left', centerX, centerY, kx, separation); + layoutCenterSide(virtualRoot, realRoot, rightChildren, 'right', centerX, centerY, kx, separation); + } + finally { + realRoot.children = originalChildren; + } + + realRoot.setLayout({x: centerX, y: centerY, side: 'center'}, true); +} + +function layoutCenterSide( + virtualRoot: TreeLayoutNode, + realRoot: TreeLayoutNode, + children: TreeLayoutNode[], + side: CenterTreeSide, + centerX: number, + centerY: number, + kx: number, + separation: ReturnType<typeof sep> +) { + if (!children.length) { + return; + } + + realRoot.children = children; + prepareTreeLayout(virtualRoot, realRoot, separation); + + let top = realRoot; + let bottom = realRoot; + eachBefore(realRoot, function (node: TreeLayoutNode) { + const x = node.getLayout().x; + if (x < top.getLayout().x) { + top = node; + } + if (x > bottom.getLayout().x) { + bottom = node; + } + }); + + const maxDistance = Math.max( + Math.abs(top.getLayout().x), + Math.abs(bottom.getLayout().x) + ); + const ky = centerY / (maxDistance || 1); + const direction = side === 'left' ? -1 : 1; + + eachBefore(realRoot, function (node: TreeLayoutNode) { + if (node === realRoot) { + return; } + + node.setLayout({ + x: centerX + direction * (node.depth - realRoot.depth) * kx, + y: centerY + node.getLayout().x * ky, + side: side + }, true); + }); +} + +function getRootChildSide( + node: TreeLayoutNode, + index: number, + autoLeftCount: number +): CenterTreeSide { + const item = node.hostTree.data.getRawDataItem(node.dataIndex) as TreeSeriesNodeItemOption; + const side = item && item.side; + if (side === 'left' || side === 'right') { + return side; } -} \ No newline at end of file + return index < autoLeftCount ? 'left' : 'right'; +} diff --git a/test/tree-center.html b/test/tree-center.html new file mode 100644 index 000000000..43bbda446 --- /dev/null +++ b/test/tree-center.html @@ -0,0 +1,127 @@ +<!-- +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. +--> + +<html> + <head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <script src="lib/simpleRequire.js"></script> + <script src="lib/config.js"></script> + </head> + <body> + <style> + html, body, #main { + width: 100%; + height: 100%; + padding: 0; + margin: 0; + } + </style> + <div id="main"></div> + <script> + require(['echarts'], function (echarts) { + var chart = echarts.init(document.getElementById('main')); + + window.onresize = function () { + chart.resize(); + }; + + chart.setOption({ + series: [{ + type: 'tree', + data: [{ + name: 'Modeling Methods', + children: [{ + name: 'Regression', + side: 'left', + children: [ + { name: 'Multiple linear regression' }, + { name: 'Partial least squares' }, + { name: 'Multi-layer feedforward neural network' }, + { name: 'General regression neural network' }, + { name: 'Support vector regression' } + ] + }, { + name: 'Classification', + children: [ + { name: 'Logistic regression' }, + { name: 'Linear discriminant analysis' }, + { name: 'Rules' }, + { name: 'Decision trees' }, + { name: 'Naive Bayes' }, + { name: 'K nearest neighbor' }, + { name: 'Probabilistic neural network' }, + { name: 'Support vector machine' } + ] + }, { + name: 'Consensus', + children: [{ + name: 'Models diversity', + children: [ + { name: 'Different initializations' }, + { name: 'Different parameter choices' }, + { name: 'Different architectures' }, + { name: 'Different modeling methods' }, + { name: 'Different training sets' }, + { name: 'Different feature sets' } + ] + }, { + name: 'Methods', + children: [ + { name: 'Classifier selection' }, + { name: 'Classifier fusion' } + ] + }, { + name: 'Common', + children: [ + { name: 'Bagging' }, + { name: 'Boosting' }, + { name: 'AdaBoost' } + ] + }] + }] + }], + orient: 'center', + expandAndCollapse: false, + initialTreeDepth: -1, + top: 40, + bottom: 40, + left: 280, + right: 280, + symbol: 'circle', + symbolSize: 10, + edgeShape: 'curve', + itemStyle: { + color: '#1b7ef2' + }, + lineStyle: { + color: '#6aa0e6', + width: 1.5, + curveness: 0.5 + }, + label: { + fontSize: 14, + color: '#222' + } + }] + }); + }); + </script> + </body> +</html> diff --git a/test/ut/spec/series/tree.test.ts b/test/ut/spec/series/tree.test.ts new file mode 100644 index 000000000..b0b489ed4 --- /dev/null +++ b/test/ut/spec/series/tree.test.ts @@ -0,0 +1,151 @@ +/* + * 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, getECModel } from '../../core/utHelper'; +import { TreeNode } from '@/src/data/Tree'; + +function findNode(root: TreeNode, name: string): TreeNode { + let found: TreeNode; + root.eachNode('preorder', function (node) { + if (node.name === name) { + found = node; + return false; + } + }); + return found; +} + +describe('series.tree', function () { + let chart: EChartsType; + + beforeEach(function () { + chart = createChart({ + width: 800, + height: 600 + }); + }); + + afterEach(function () { + chart.dispose(); + }); + + it('should support centered root with children expanding to both sides', function () { + chart.setOption({ + series: [{ + type: 'tree', + animation: false, + left: 0, + top: 0, + right: 0, + bottom: 0, + orient: 'center', + expandAndCollapse: false, + data: [{ + name: 'root', + children: [{ + name: 'left-child', + side: 'left', + children: [{ name: 'left-leaf' }] + }, { + name: 'right-child', + side: 'right', + children: [{ name: 'right-leaf' }] + }] + }] + }] + }); + + const tree = getECModel(chart).getSeries()[0].getData().tree; + const root = findNode(tree.root, 'root'); + const leftChild = findNode(tree.root, 'left-child'); + const leftLeaf = findNode(tree.root, 'left-leaf'); + const rightChild = findNode(tree.root, 'right-child'); + const rightLeaf = findNode(tree.root, 'right-leaf'); + + const rootLayout = root.getLayout(); + expect(rootLayout.x).toBeCloseTo(400); + expect(rootLayout.y).toBeCloseTo(300); + + expect(leftChild.getLayout().x).toBeLessThan(rootLayout.x); + expect(leftLeaf.getLayout().x).toBeLessThan(leftChild.getLayout().x); + expect(rightChild.getLayout().x).toBeGreaterThan(rootLayout.x); + expect(rightLeaf.getLayout().x).toBeGreaterThan(rightChild.getLayout().x); + }); + + it('should split root children automatically when side is not specified', function () { + chart.setOption({ + series: [{ + type: 'tree', + animation: false, + left: 0, + top: 0, + right: 0, + bottom: 0, + orient: 'center', + expandAndCollapse: false, + data: [{ + name: 'root', + children: [ + { name: 'first' }, + { name: 'second' }, + { name: 'third' } + ] + }] + }] + }); + + const tree = getECModel(chart).getSeries()[0].getData().tree; + const rootLayout = findNode(tree.root, 'root').getLayout(); + + expect(findNode(tree.root, 'first').getLayout().x).toBeLessThan(rootLayout.x); + expect(findNode(tree.root, 'second').getLayout().x).toBeGreaterThan(rootLayout.x); + expect(findNode(tree.root, 'third').getLayout().x).toBeGreaterThan(rootLayout.x); + }); + + it('should render center orient with polyline edges', function () { + chart.setOption({ + series: [{ + type: 'tree', + animation: false, + orient: 'center', + edgeShape: 'polyline', + expandAndCollapse: false, + data: [{ + name: 'root', + children: [{ + name: 'left', + side: 'left', + children: [{ name: 'left-leaf' }] + }, { + name: 'right', + side: 'right', + children: [{ name: 'right-leaf' }] + }] + }] + }] + }); + + const tree = getECModel(chart).getSeries()[0].getData().tree; + const rootLayout = findNode(tree.root, 'root').getLayout(); + + expect(findNode(tree.root, 'left').getLayout().x).toBeLessThan(rootLayout.x); + expect(findNode(tree.root, 'right').getLayout().x).toBeGreaterThan(rootLayout.x); + }); +}); --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
