| /* | |
| * 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 * as zrUtil from 'zrender/src/core/util'; | |
| import * as graphic from '../../util/graphic'; | |
| import {getECData} from '../../util/innerStore'; | |
| import SymbolClz from '../helper/Symbol'; | |
| import {radialCoordinate} from './layoutHelper'; | |
| import * as bbox from 'zrender/src/core/bbox'; | |
| import View from '../../coord/View'; | |
| import * as roamHelper from '../../component/helper/roamHelper'; | |
| import RoamController, { RoamControllerHost } from '../../component/helper/RoamController'; | |
| import {onIrrelevantElement} from '../../component/helper/cursorHelper'; | |
| import {parsePercent} from '../../util/number'; | |
| 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 ExtensionAPI from '../../core/ExtensionAPI'; | |
| import { TreeNode } from '../../data/Tree'; | |
| import SeriesData from '../../data/SeriesData'; | |
| import { setStatesStylesFromModel, setStatesFlag, setDefaultStateProxy, HOVER_STATE_BLUR } from '../../util/states'; | |
| import { AnimationOption, ECElement } from '../../util/types'; | |
| type TreeSymbol = SymbolClz & { | |
| __edge: graphic.BezierCurve | TreePath | |
| __radialOldRawX: number | |
| __radialOldRawY: number | |
| __radialRawX: number | |
| __radialRawY: number | |
| __oldX: number | |
| __oldY: number | |
| }; | |
| class TreeEdgeShape { | |
| parentPoint: number[] = []; | |
| childPoints: number[][] = []; | |
| orient: TreeSeriesOption['orient']; | |
| forkPosition: TreeSeriesOption['edgeForkPosition']; | |
| } | |
| interface TreeEdgePathProps extends PathProps { | |
| shape?: Partial<TreeEdgeShape> | |
| } | |
| interface TreeNodeLayout { | |
| x: number | |
| y: number | |
| rawX: number | |
| rawY: number | |
| } | |
| class TreePath extends Path<TreeEdgePathProps> { | |
| shape: TreeEdgeShape; | |
| constructor(opts?: TreeEdgePathProps) { | |
| super(opts); | |
| } | |
| getDefaultStyle() { | |
| return { | |
| stroke: '#000', | |
| fill: null as string | |
| }; | |
| } | |
| getDefaultShape() { | |
| return new TreeEdgeShape(); | |
| } | |
| 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]); | |
| 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; | |
| 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 = childPoints[i]; | |
| ctx.moveTo(point[0], point[1]); | |
| tmpPoint[forkDim] = point[forkDim]; | |
| ctx.lineTo(tmpPoint[0], tmpPoint[1]); | |
| } | |
| } | |
| } | |
| class TreeView extends ChartView { | |
| static readonly type = 'tree'; | |
| readonly type = TreeView.type; | |
| private _mainGroup = new graphic.Group(); | |
| private _controller: RoamController; | |
| private _controllerHost: RoamControllerHost; | |
| private _data: SeriesData<TreeSeriesModel>; | |
| private _nodeScaleRatio: number; | |
| private _min: number[]; | |
| private _max: number[]; | |
| init(ecModel: GlobalModel, api: ExtensionAPI) { | |
| this._controller = new RoamController(api.getZr()); | |
| this._controllerHost = { | |
| target: this.group | |
| } as RoamControllerHost; | |
| this.group.add(this._mainGroup); | |
| } | |
| render( | |
| seriesModel: TreeSeriesModel, | |
| ecModel: GlobalModel, | |
| api: ExtensionAPI | |
| ) { | |
| const data = seriesModel.getData(); | |
| const layoutInfo = seriesModel.layoutInfo; | |
| const group = this._mainGroup; | |
| const layout = seriesModel.get('layout'); | |
| if (layout === 'radial') { | |
| group.x = layoutInfo.x + layoutInfo.width / 2; | |
| group.y = layoutInfo.y + layoutInfo.height / 2; | |
| } | |
| else { | |
| group.x = layoutInfo.x; | |
| group.y = layoutInfo.y; | |
| } | |
| this._updateViewCoordSys(seriesModel, api); | |
| this._updateController(seriesModel, ecModel, api); | |
| const oldData = this._data; | |
| data.diff(oldData) | |
| .add(function (newIdx) { | |
| if (symbolNeedsDraw(data, newIdx)) { | |
| // Create node and edge | |
| updateNode(data, newIdx, null, group, seriesModel); | |
| } | |
| }) | |
| .update(function (newIdx, oldIdx) { | |
| const symbolEl = oldData.getItemGraphicEl(oldIdx) as TreeSymbol; | |
| if (!symbolNeedsDraw(data, newIdx)) { | |
| symbolEl && removeNode(oldData, oldIdx, symbolEl, group, seriesModel); | |
| return; | |
| } | |
| // Update node and edge | |
| updateNode(data, newIdx, symbolEl, group, seriesModel); | |
| }) | |
| .remove(function (oldIdx) { | |
| const symbolEl = oldData.getItemGraphicEl(oldIdx) as TreeSymbol; | |
| // When remove a collapsed node of subtree, since the collapsed | |
| // node haven't been initialized with a symbol element, | |
| // you can't found it's symbol element through index. | |
| // so if we want to remove the symbol element we should insure | |
| // that the symbol element is not null. | |
| if (symbolEl) { | |
| removeNode(oldData, oldIdx, symbolEl, group, seriesModel); | |
| } | |
| }) | |
| .execute(); | |
| this._nodeScaleRatio = seriesModel.get('nodeScaleRatio'); | |
| this._updateNodeAndLinkScale(seriesModel); | |
| if (seriesModel.get('expandAndCollapse') === true) { | |
| data.eachItemGraphicEl(function (el, dataIndex) { | |
| el.off('click').on('click', function () { | |
| api.dispatchAction({ | |
| type: 'treeExpandAndCollapse', | |
| seriesId: seriesModel.id, | |
| dataIndex: dataIndex | |
| }); | |
| }); | |
| }); | |
| } | |
| this._data = data; | |
| } | |
| _updateViewCoordSys(seriesModel: TreeSeriesModel, api: ExtensionAPI) { | |
| const data = seriesModel.getData(); | |
| const points: number[][] = []; | |
| data.each(function (idx) { | |
| const layout = data.getItemLayout(idx); | |
| if (layout && !isNaN(layout.x) && !isNaN(layout.y)) { | |
| points.push([+layout.x, +layout.y]); | |
| } | |
| }); | |
| const min: number[] = []; | |
| const max: number[] = []; | |
| bbox.fromPoints(points, min, max); | |
| // If don't Store min max when collapse the root node after roam, | |
| // the root node will disappear. | |
| const oldMin = this._min; | |
| const oldMax = this._max; | |
| // If width or height is 0 | |
| if (max[0] - min[0] === 0) { | |
| min[0] = oldMin ? oldMin[0] : min[0] - 1; | |
| max[0] = oldMax ? oldMax[0] : max[0] + 1; | |
| } | |
| if (max[1] - min[1] === 0) { | |
| min[1] = oldMin ? oldMin[1] : min[1] - 1; | |
| max[1] = oldMax ? oldMax[1] : max[1] + 1; | |
| } | |
| const viewCoordSys = seriesModel.coordinateSystem = new View(); | |
| viewCoordSys.zoomLimit = seriesModel.get('scaleLimit'); | |
| viewCoordSys.setBoundingRect(min[0], min[1], max[0] - min[0], max[1] - min[1]); | |
| viewCoordSys.setCenter(seriesModel.get('center'), api); | |
| viewCoordSys.setZoom(seriesModel.get('zoom')); | |
| // Here we use viewCoordSys just for computing the 'position' and 'scale' of the group | |
| this.group.attr({ | |
| x: viewCoordSys.x, | |
| y: viewCoordSys.y, | |
| scaleX: viewCoordSys.scaleX, | |
| scaleY: viewCoordSys.scaleY | |
| }); | |
| this._min = min; | |
| this._max = max; | |
| } | |
| _updateController( | |
| seriesModel: TreeSeriesModel, | |
| ecModel: GlobalModel, | |
| api: ExtensionAPI | |
| ) { | |
| const controller = this._controller; | |
| const controllerHost = this._controllerHost; | |
| const group = this.group; | |
| controller.setPointerChecker(function (e, x, y) { | |
| const rect = group.getBoundingRect(); | |
| rect.applyTransform(group.transform); | |
| return rect.contain(x, y) | |
| && !onIrrelevantElement(e, api, seriesModel); | |
| }); | |
| controller.enable(seriesModel.get('roam')); | |
| controllerHost.zoomLimit = seriesModel.get('scaleLimit'); | |
| controllerHost.zoom = seriesModel.coordinateSystem.getZoom(); | |
| controller | |
| .off('pan') | |
| .off('zoom') | |
| .on('pan', (e) => { | |
| roamHelper.updateViewOnPan(controllerHost, e.dx, e.dy); | |
| api.dispatchAction({ | |
| seriesId: seriesModel.id, | |
| type: 'treeRoam', | |
| dx: e.dx, | |
| dy: e.dy | |
| }); | |
| }) | |
| .on('zoom', (e) => { | |
| roamHelper.updateViewOnZoom(controllerHost, e.scale, e.originX, e.originY); | |
| api.dispatchAction({ | |
| seriesId: seriesModel.id, | |
| type: 'treeRoam', | |
| zoom: e.scale, | |
| originX: e.originX, | |
| originY: e.originY | |
| }); | |
| this._updateNodeAndLinkScale(seriesModel); | |
| // Only update label layout on zoom | |
| api.updateLabelLayout(); | |
| }); | |
| } | |
| _updateNodeAndLinkScale(seriesModel: TreeSeriesModel) { | |
| const data = seriesModel.getData(); | |
| const nodeScale = this._getNodeGlobalScale(seriesModel); | |
| data.eachItemGraphicEl(function (el: SymbolClz, idx) { | |
| el.setSymbolScale(nodeScale); | |
| }); | |
| } | |
| _getNodeGlobalScale(seriesModel: TreeSeriesModel) { | |
| const coordSys = seriesModel.coordinateSystem; | |
| if (coordSys.type !== 'view') { | |
| return 1; | |
| } | |
| const nodeScaleRatio = this._nodeScaleRatio; | |
| const groupZoom = coordSys.scaleX || 1; | |
| // Scale node when zoom changes | |
| const roamZoom = coordSys.getZoom(); | |
| const nodeScale = (roamZoom - 1) * nodeScaleRatio + 1; | |
| return nodeScale / groupZoom; | |
| } | |
| dispose() { | |
| this._controller && this._controller.dispose(); | |
| this._controllerHost = null; | |
| } | |
| remove() { | |
| this._mainGroup.removeAll(); | |
| this._data = null; | |
| } | |
| } | |
| function symbolNeedsDraw(data: SeriesData, dataIndex: number) { | |
| const layout = data.getItemLayout(dataIndex); | |
| return layout | |
| && !isNaN(layout.x) && !isNaN(layout.y); | |
| } | |
| function updateNode( | |
| data: SeriesData, | |
| dataIndex: number, | |
| symbolEl: TreeSymbol, | |
| group: graphic.Group, | |
| seriesModel: TreeSeriesModel | |
| ) { | |
| const isInit = !symbolEl; | |
| const node = data.tree.getNodeByDataIndex(dataIndex); | |
| const itemModel = node.getModel<TreeSeriesNodeItemOption>(); | |
| const visualColor = (node.getVisual('style') as PathStyleProps).fill; | |
| const symbolInnerColor = node.isExpand === false && node.children.length !== 0 | |
| ? visualColor : '#fff'; | |
| const virtualRoot = data.tree.root; | |
| const source = node.parentNode === virtualRoot ? node : node.parentNode || node; | |
| const sourceSymbolEl = data.getItemGraphicEl(source.dataIndex) as TreeSymbol; | |
| const sourceLayout = source.getLayout() as TreeNodeLayout; | |
| const sourceOldLayout = sourceSymbolEl | |
| ? { | |
| x: sourceSymbolEl.__oldX, | |
| y: sourceSymbolEl.__oldY, | |
| rawX: sourceSymbolEl.__radialOldRawX, | |
| rawY: sourceSymbolEl.__radialOldRawY | |
| } | |
| : sourceLayout; | |
| const targetLayout = node.getLayout(); | |
| if (isInit) { | |
| symbolEl = new SymbolClz(data, dataIndex, null, { | |
| symbolInnerColor, | |
| useNameLabel: true | |
| }) as TreeSymbol; | |
| symbolEl.x = sourceOldLayout.x; | |
| symbolEl.y = sourceOldLayout.y; | |
| } | |
| else { | |
| symbolEl.updateData(data, dataIndex, null, { | |
| symbolInnerColor, | |
| useNameLabel: true | |
| }); | |
| } | |
| symbolEl.__radialOldRawX = symbolEl.__radialRawX; | |
| symbolEl.__radialOldRawY = symbolEl.__radialRawY; | |
| symbolEl.__radialRawX = targetLayout.rawX; | |
| symbolEl.__radialRawY = targetLayout.rawY; | |
| group.add(symbolEl); | |
| data.setItemGraphicEl(dataIndex, symbolEl); | |
| symbolEl.__oldX = symbolEl.x; | |
| symbolEl.__oldY = symbolEl.y; | |
| graphic.updateProps(symbolEl, { | |
| x: targetLayout.x, | |
| y: targetLayout.y | |
| }, seriesModel); | |
| const symbolPath = symbolEl.getSymbolPath(); | |
| if (seriesModel.get('layout') === 'radial') { | |
| const realRoot = virtualRoot.children[0]; | |
| const rootLayout = realRoot.getLayout(); | |
| const length = realRoot.children.length; | |
| let rad; | |
| let isLeft; | |
| if (targetLayout.x === rootLayout.x && node.isExpand === true && realRoot.children.length) { | |
| const center = { | |
| x: (realRoot.children[0].getLayout().x + realRoot.children[length - 1].getLayout().x) / 2, | |
| y: (realRoot.children[0].getLayout().y + realRoot.children[length - 1].getLayout().y) / 2 | |
| }; | |
| rad = Math.atan2(center.y - rootLayout.y, center.x - rootLayout.x); | |
| if (rad < 0) { | |
| rad = Math.PI * 2 + rad; | |
| } | |
| isLeft = center.x < rootLayout.x; | |
| if (isLeft) { | |
| rad = rad - Math.PI; | |
| } | |
| } | |
| else { | |
| rad = Math.atan2(targetLayout.y - rootLayout.y, targetLayout.x - rootLayout.x); | |
| if (rad < 0) { | |
| rad = Math.PI * 2 + rad; | |
| } | |
| if (node.children.length === 0 || (node.children.length !== 0 && node.isExpand === false)) { | |
| isLeft = targetLayout.x < rootLayout.x; | |
| if (isLeft) { | |
| rad = rad - Math.PI; | |
| } | |
| } | |
| else { | |
| isLeft = targetLayout.x > rootLayout.x; | |
| if (!isLeft) { | |
| rad = rad - Math.PI; | |
| } | |
| } | |
| } | |
| const textPosition = isLeft ? 'left' as const : 'right' as const; | |
| const normalLabelModel = itemModel.getModel('label'); | |
| const rotate = normalLabelModel.get('rotate'); | |
| const labelRotateRadian = rotate * (Math.PI / 180); | |
| const textContent = symbolPath.getTextContent(); | |
| if (textContent) { | |
| symbolPath.setTextConfig({ | |
| position: normalLabelModel.get('position') || textPosition, | |
| rotation: rotate == null ? -rad : labelRotateRadian, | |
| origin: 'center' | |
| }); | |
| textContent.setStyle('verticalAlign', 'middle'); | |
| } | |
| } | |
| // Handle status | |
| const focus = itemModel.get(['emphasis', 'focus']); | |
| const focusDataIndices: number[] = focus === 'relative' | |
| ? zrUtil.concatArray(node.getAncestorsIndices(), node.getDescendantIndices()) as number[] | |
| : focus === 'ancestor' | |
| ? node.getAncestorsIndices() | |
| : focus === 'descendant' ? node.getDescendantIndices() : null; | |
| if (focusDataIndices) { | |
| // Modify the focus to data indices. | |
| getECData(symbolEl).focus = focusDataIndices; | |
| } | |
| drawEdge( | |
| seriesModel, node, virtualRoot, symbolEl, sourceOldLayout, | |
| sourceLayout, targetLayout, group | |
| ); | |
| if (symbolEl.__edge) { | |
| (symbolEl as ECElement).onHoverStateChange = function (toState) { | |
| if (toState !== 'blur') { | |
| // NOTE: Ensure the parent elements will been blurred firstly. | |
| // According to the return of getAncestorsIndices and getDescendantIndices | |
| // TODO: A bit tricky. | |
| const parentEl = node.parentNode | |
| && data.getItemGraphicEl(node.parentNode.dataIndex); | |
| if (!(parentEl && (parentEl as ECElement).hoverState === HOVER_STATE_BLUR)) { | |
| setStatesFlag(symbolEl.__edge, toState); | |
| } | |
| } | |
| }; | |
| } | |
| } | |
| function drawEdge( | |
| seriesModel: TreeSeriesModel, | |
| node: TreeNode, | |
| virtualRoot: TreeNode, | |
| symbolEl: TreeSymbol, | |
| sourceOldLayout: TreeNodeLayout, | |
| sourceLayout: TreeNodeLayout, | |
| targetLayout: TreeNodeLayout, | |
| group: graphic.Group | |
| ) { | |
| const itemModel = node.getModel<TreeSeriesNodeItemOption>(); | |
| const edgeShape = seriesModel.get('edgeShape'); | |
| const layout = seriesModel.get('layout'); | |
| const orient = seriesModel.getOrient(); | |
| const curvature = seriesModel.get(['lineStyle', 'curveness']); | |
| const edgeForkPosition = seriesModel.get('edgeForkPosition'); | |
| const lineStyle = itemModel.getModel('lineStyle').getLineStyle(); | |
| let edge = symbolEl.__edge; | |
| // curve edge from node -> parent | |
| // polyline edge from node -> children | |
| if (edgeShape === 'curve') { | |
| if (node.parentNode && node.parentNode !== virtualRoot) { | |
| if (!edge) { | |
| edge = symbolEl.__edge = new graphic.BezierCurve({ | |
| shape: getEdgeShape(layout, orient, curvature, sourceOldLayout, sourceOldLayout) | |
| }); | |
| } | |
| graphic.updateProps(edge as Path, { | |
| shape: getEdgeShape(layout, orient, curvature, sourceLayout, targetLayout) | |
| }, seriesModel); | |
| } | |
| } | |
| else if (edgeShape === 'polyline') { | |
| if (layout === 'orthogonal') { | |
| if (node !== virtualRoot && node.children && (node.children.length !== 0) && (node.isExpand === true)) { | |
| const children = node.children; | |
| const childPoints = []; | |
| for (let i = 0; i < children.length; i++) { | |
| const childLayout = children[i].getLayout(); | |
| childPoints.push([childLayout.x, childLayout.y]); | |
| } | |
| if (!edge) { | |
| edge = symbolEl.__edge = new TreePath({ | |
| shape: { | |
| parentPoint: [targetLayout.x, targetLayout.y], | |
| childPoints: [[targetLayout.x, targetLayout.y]], | |
| orient: orient, | |
| forkPosition: edgeForkPosition | |
| } | |
| }); | |
| } | |
| graphic.updateProps(edge as Path, { | |
| shape: { | |
| parentPoint: [targetLayout.x, targetLayout.y], | |
| childPoints: childPoints | |
| } | |
| }, seriesModel); | |
| } | |
| } | |
| else { | |
| if (__DEV__) { | |
| throw new Error('The polyline edgeShape can only be used in orthogonal layout'); | |
| } | |
| } | |
| } | |
| // show all edge when edgeShape is 'curve', filter node `isExpand` is false when edgeShape is 'polyline' | |
| if (edge && !(edgeShape === 'polyline' && !node.isExpand)) { | |
| edge.useStyle(zrUtil.defaults({ | |
| strokeNoScale: true, fill: null | |
| }, lineStyle)); | |
| setStatesStylesFromModel(edge, itemModel, 'lineStyle'); | |
| setDefaultStateProxy(edge); | |
| group.add(edge); | |
| } | |
| } | |
| function removeNodeEdge( | |
| node: TreeNode, | |
| data: SeriesData, | |
| group: graphic.Group, | |
| seriesModel: TreeSeriesModel, | |
| removeAnimationOpt: AnimationOption | |
| ) { | |
| const virtualRoot = data.tree.root; | |
| const { source, sourceLayout } = getSourceNode(virtualRoot, node); | |
| const symbolEl: TreeSymbol = data.getItemGraphicEl(node.dataIndex) as TreeSymbol; | |
| if (!symbolEl) { | |
| return; | |
| } | |
| const sourceSymbolEl = data.getItemGraphicEl(source.dataIndex) as TreeSymbol; | |
| const sourceEdge = 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. | |
| // 2.when the node is the only children of the source, delete the node should delete the edge of | |
| // the source at the same time. the same reason as above. | |
| const edge = symbolEl.__edge | |
| || ((source.isExpand === false || source.children.length === 1) ? sourceEdge : undefined); | |
| const edgeShape = seriesModel.get('edgeShape'); | |
| const layoutOpt = seriesModel.get('layout'); | |
| const orient = seriesModel.get('orient'); | |
| const curvature = seriesModel.get(['lineStyle', 'curveness']); | |
| if (edge) { | |
| if (edgeShape === 'curve') { | |
| graphic.removeElement(edge as Path, { | |
| shape: getEdgeShape( | |
| layoutOpt, | |
| orient, | |
| curvature, | |
| sourceLayout, | |
| sourceLayout | |
| ), | |
| style: { | |
| opacity: 0 | |
| } | |
| }, seriesModel, { | |
| cb() { | |
| group.remove(edge); | |
| }, | |
| removeOpt: removeAnimationOpt | |
| }); | |
| } | |
| else if (edgeShape === 'polyline' && seriesModel.get('layout') === 'orthogonal') { | |
| graphic.removeElement(edge as Path, { | |
| shape: { | |
| parentPoint: [sourceLayout.x, sourceLayout.y], | |
| childPoints: [[sourceLayout.x, sourceLayout.y]] | |
| }, | |
| style: { | |
| opacity: 0 | |
| } | |
| }, seriesModel, { | |
| cb() { | |
| group.remove(edge); | |
| }, | |
| removeOpt: removeAnimationOpt | |
| }); | |
| } | |
| } | |
| } | |
| function getSourceNode(virtualRoot: TreeNode, node: TreeNode): { source: TreeNode, sourceLayout: TreeNodeLayout } { | |
| let source = node.parentNode === virtualRoot ? node : node.parentNode || node; | |
| let sourceLayout; | |
| while (sourceLayout = source.getLayout(), sourceLayout == null) { | |
| source = source.parentNode === virtualRoot ? source : source.parentNode || source; | |
| } | |
| return { | |
| source, | |
| sourceLayout | |
| }; | |
| } | |
| function removeNode( | |
| data: SeriesData, | |
| dataIndex: number, | |
| symbolEl: TreeSymbol, | |
| group: graphic.Group, | |
| seriesModel: TreeSeriesModel | |
| ) { | |
| const node = data.tree.getNodeByDataIndex(dataIndex); | |
| const virtualRoot = data.tree.root; | |
| const { sourceLayout } = getSourceNode(virtualRoot, node); | |
| // Use same duration and easing with update to have more consistent animation. | |
| const removeAnimationOpt = { | |
| duration: seriesModel.get('animationDurationUpdate') as number, | |
| easing: seriesModel.get('animationEasingUpdate') | |
| }; | |
| graphic.removeElement(symbolEl, { | |
| x: sourceLayout.x + 1, | |
| y: sourceLayout.y + 1 | |
| }, seriesModel, { | |
| cb() { | |
| group.remove(symbolEl); | |
| data.setItemGraphicEl(dataIndex, null); | |
| }, | |
| removeOpt: removeAnimationOpt | |
| }); | |
| symbolEl.fadeOut(null, data.hostModel as TreeSeriesModel, { | |
| fadeLabel: true, | |
| animation: removeAnimationOpt | |
| }); | |
| // remove edge as parent node | |
| node.children.forEach(childNode => { | |
| removeNodeEdge(childNode, data, group, seriesModel, removeAnimationOpt); | |
| }); | |
| // remove edge as child node | |
| removeNodeEdge(node, data, group, seriesModel, removeAnimationOpt); | |
| } | |
| function getEdgeShape( | |
| layoutOpt: TreeSeriesOption['layout'], | |
| orient: TreeSeriesOption['orient'], | |
| curvature: number, | |
| sourceLayout: TreeNodeLayout, | |
| targetLayout: TreeNodeLayout | |
| ) { | |
| let cpx1: number; | |
| let cpy1: number; | |
| let cpx2: number; | |
| let cpy2: number; | |
| let x1: number; | |
| let x2: number; | |
| let y1: number; | |
| let y2: number; | |
| if (layoutOpt === 'radial') { | |
| x1 = sourceLayout.rawX; | |
| y1 = sourceLayout.rawY; | |
| x2 = targetLayout.rawX; | |
| y2 = targetLayout.rawY; | |
| const radialCoor1 = radialCoordinate(x1, y1); | |
| const radialCoor2 = radialCoordinate(x1, y1 + (y2 - y1) * curvature); | |
| const radialCoor3 = radialCoordinate(x2, y2 + (y1 - y2) * curvature); | |
| const radialCoor4 = radialCoordinate(x2, y2); | |
| return { | |
| x1: radialCoor1.x || 0, | |
| y1: radialCoor1.y || 0, | |
| x2: radialCoor4.x || 0, | |
| y2: radialCoor4.y || 0, | |
| cpx1: radialCoor2.x || 0, | |
| cpy1: radialCoor2.y || 0, | |
| cpx2: radialCoor3.x || 0, | |
| cpy2: radialCoor3.y || 0 | |
| }; | |
| } | |
| else { | |
| x1 = sourceLayout.x; | |
| y1 = sourceLayout.y; | |
| x2 = targetLayout.x; | |
| y2 = targetLayout.y; | |
| if (orient === 'LR' || orient === 'RL') { | |
| cpx1 = x1 + (x2 - x1) * curvature; | |
| cpy1 = y1; | |
| cpx2 = x2 + (x1 - x2) * curvature; | |
| cpy2 = y2; | |
| } | |
| if (orient === 'TB' || orient === 'BT') { | |
| cpx1 = x1; | |
| cpy1 = y1 + (y2 - y1) * curvature; | |
| cpx2 = x2; | |
| cpy2 = y2 + (y1 - y2) * curvature; | |
| } | |
| } | |
| return { | |
| x1: x1, | |
| y1: y1, | |
| x2: x2, | |
| y2: y2, | |
| cpx1: cpx1, | |
| cpy1: cpy1, | |
| cpx2: cpx2, | |
| cpy2: cpy2 | |
| }; | |
| } | |
| export default TreeView; |