| /* | |
| * 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. | |
| */ | |
| /* | |
| * A third-party license is embedded for some of the code in this file: | |
| * The treemap layout implementation was originally copied from | |
| * "d3.js" with some modifications made for this project. | |
| * (See more details in the comment of the method "squarify" below.) | |
| * The use of the source code of this file is also subject to the terms | |
| * and consitions of the license of "d3.js" (BSD-3Clause, see | |
| * </licenses/LICENSE-d3>). | |
| */ | |
| import * as zrUtil from 'zrender/src/core/util'; | |
| import BoundingRect, { RectLike } from 'zrender/src/core/BoundingRect'; | |
| import {parsePercent, MAX_SAFE_INTEGER} from '../../util/number'; | |
| import * as layout from '../../util/layout'; | |
| import * as helper from '../helper/treeHelper'; | |
| import TreemapSeriesModel, { TreemapSeriesNodeItemOption } from './TreemapSeries'; | |
| import GlobalModel from '../../model/Global'; | |
| import ExtensionAPI from '../../core/ExtensionAPI'; | |
| import { TreeNode } from '../../data/Tree'; | |
| import Model from '../../model/Model'; | |
| import { TreemapRenderPayload, TreemapMovePayload, TreemapZoomToNodePayload } from './treemapAction'; | |
| const mathMax = Math.max; | |
| const mathMin = Math.min; | |
| const retrieveValue = zrUtil.retrieve; | |
| const each = zrUtil.each; | |
| const PATH_BORDER_WIDTH = ['itemStyle', 'borderWidth'] as const; | |
| const PATH_GAP_WIDTH = ['itemStyle', 'gapWidth'] as const; | |
| const PATH_UPPER_LABEL_SHOW = ['upperLabel', 'show'] as const; | |
| const PATH_UPPER_LABEL_HEIGHT = ['upperLabel', 'height'] as const; | |
| export interface TreemapLayoutNode extends TreeNode { | |
| parentNode: TreemapLayoutNode | |
| children: TreemapLayoutNode[] | |
| viewChildren: TreemapLayoutNode[] | |
| } | |
| export interface TreemapItemLayout extends RectLike { | |
| area: number | |
| isLeafRoot: boolean | |
| dataExtent: [number, number] | |
| borderWidth: number | |
| upperHeight: number | |
| upperLabelHeight: number | |
| isInView: boolean | |
| invisible: boolean | |
| isAboveViewRoot: boolean | |
| }; | |
| type NodeModel = Model<TreemapSeriesNodeItemOption>; | |
| type OrderBy = 'asc' | 'desc' | boolean; | |
| type LayoutRow = TreemapLayoutNode[] & { | |
| area: number | |
| }; | |
| /** | |
| * @public | |
| */ | |
| export default { | |
| seriesType: 'treemap', | |
| reset: function ( | |
| seriesModel: TreemapSeriesModel, | |
| ecModel: GlobalModel, | |
| api: ExtensionAPI, | |
| payload?: TreemapZoomToNodePayload | TreemapRenderPayload | TreemapMovePayload | |
| ) { | |
| // Layout result in each node: | |
| // {x, y, width, height, area, borderWidth} | |
| const ecWidth = api.getWidth(); | |
| const ecHeight = api.getHeight(); | |
| const seriesOption = seriesModel.option; | |
| const layoutInfo = layout.getLayoutRect( | |
| seriesModel.getBoxLayoutParams(), | |
| { | |
| width: api.getWidth(), | |
| height: api.getHeight() | |
| } | |
| ); | |
| const size = seriesOption.size || []; // Compatible with ec2. | |
| const containerWidth = parsePercent( | |
| retrieveValue(layoutInfo.width, size[0]), | |
| ecWidth | |
| ); | |
| const containerHeight = parsePercent( | |
| retrieveValue(layoutInfo.height, size[1]), | |
| ecHeight | |
| ); | |
| // Fetch payload info. | |
| const payloadType = payload && payload.type; | |
| const types = ['treemapZoomToNode', 'treemapRootToNode']; | |
| const targetInfo = helper | |
| .retrieveTargetInfo(payload, types, seriesModel); | |
| const rootRect = (payloadType === 'treemapRender' || payloadType === 'treemapMove') | |
| ? payload.rootRect : null; | |
| const viewRoot = seriesModel.getViewRoot(); | |
| const viewAbovePath = helper.getPathToRoot(viewRoot) as TreemapLayoutNode[]; | |
| if (payloadType !== 'treemapMove') { | |
| const rootSize = payloadType === 'treemapZoomToNode' | |
| ? estimateRootSize( | |
| seriesModel, targetInfo, viewRoot, containerWidth, containerHeight | |
| ) | |
| : rootRect | |
| ? [rootRect.width, rootRect.height] | |
| : [containerWidth, containerHeight]; | |
| let sort = seriesOption.sort; | |
| if (sort && sort !== 'asc' && sort !== 'desc') { | |
| // Default to be desc order. | |
| sort = 'desc'; | |
| } | |
| const options = { | |
| squareRatio: seriesOption.squareRatio, | |
| sort: sort, | |
| leafDepth: seriesOption.leafDepth | |
| }; | |
| // layout should be cleared because using updateView but not update. | |
| viewRoot.hostTree.clearLayouts(); | |
| // TODO | |
| // optimize: if out of view clip, do not layout. | |
| // But take care that if do not render node out of view clip, | |
| // how to calculate start po | |
| let viewRootLayout = { | |
| x: 0, | |
| y: 0, | |
| width: rootSize[0], | |
| height: rootSize[1], | |
| area: rootSize[0] * rootSize[1] | |
| }; | |
| viewRoot.setLayout(viewRootLayout); | |
| squarify(viewRoot, options, false, 0); | |
| // Supplement layout. | |
| viewRootLayout = viewRoot.getLayout(); | |
| each(viewAbovePath, function (node, index) { | |
| const childValue = (viewAbovePath[index + 1] || viewRoot).getValue(); | |
| node.setLayout(zrUtil.extend( | |
| { | |
| dataExtent: [childValue, childValue], | |
| borderWidth: 0, | |
| upperHeight: 0 | |
| }, | |
| viewRootLayout | |
| )); | |
| }); | |
| } | |
| const treeRoot = seriesModel.getData().tree.root; | |
| treeRoot.setLayout( | |
| calculateRootPosition(layoutInfo, rootRect, targetInfo), | |
| true | |
| ); | |
| seriesModel.setLayoutInfo(layoutInfo); | |
| // FIXME | |
| // 现在没有clip功能,暂时取ec高宽。 | |
| prunning( | |
| treeRoot, | |
| // Transform to base element coordinate system. | |
| new BoundingRect(-layoutInfo.x, -layoutInfo.y, ecWidth, ecHeight), | |
| viewAbovePath, | |
| viewRoot, | |
| 0 | |
| ); | |
| } | |
| }; | |
| /** | |
| * Layout treemap with squarify algorithm. | |
| * The original presentation of this algorithm | |
| * was made by Mark Bruls, Kees Huizing, and Jarke J. van Wijk | |
| * <https://graphics.ethz.ch/teaching/scivis_common/Literature/squarifiedTreeMaps.pdf>. | |
| * The implementation of this algorithm was originally copied from "d3.js" | |
| * <https://github.com/d3/d3/blob/9cc9a875e636a1dcf36cc1e07bdf77e1ad6e2c74/src/layout/treemap.js> | |
| * with some modifications made for this program. | |
| * See the license statement at the head of this file. | |
| * | |
| * @protected | |
| * @param {module:echarts/data/Tree~TreeNode} node | |
| * @param {Object} options | |
| * @param {string} options.sort 'asc' or 'desc' | |
| * @param {number} options.squareRatio | |
| * @param {boolean} hideChildren | |
| * @param {number} depth | |
| */ | |
| function squarify( | |
| node: TreemapLayoutNode, | |
| options: { | |
| sort?: OrderBy | |
| squareRatio?: number | |
| leafDepth?: number | |
| }, | |
| hideChildren: boolean, | |
| depth: number | |
| ) { | |
| let width; | |
| let height; | |
| if (node.isRemoved()) { | |
| return; | |
| } | |
| const thisLayout = node.getLayout(); | |
| width = thisLayout.width; | |
| height = thisLayout.height; | |
| // Considering border and gap | |
| const nodeModel = node.getModel<TreemapSeriesNodeItemOption>(); | |
| const borderWidth = nodeModel.get(PATH_BORDER_WIDTH); | |
| const halfGapWidth = nodeModel.get(PATH_GAP_WIDTH) / 2; | |
| const upperLabelHeight = getUpperLabelHeight(nodeModel); | |
| const upperHeight = Math.max(borderWidth, upperLabelHeight); | |
| const layoutOffset = borderWidth - halfGapWidth; | |
| const layoutOffsetUpper = upperHeight - halfGapWidth; | |
| node.setLayout({ | |
| borderWidth: borderWidth, | |
| upperHeight: upperHeight, | |
| upperLabelHeight: upperLabelHeight | |
| }, true); | |
| width = mathMax(width - 2 * layoutOffset, 0); | |
| height = mathMax(height - layoutOffset - layoutOffsetUpper, 0); | |
| const totalArea = width * height; | |
| const viewChildren = initChildren( | |
| node, nodeModel, totalArea, options, hideChildren, depth | |
| ); | |
| if (!viewChildren.length) { | |
| return; | |
| } | |
| const rect = {x: layoutOffset, y: layoutOffsetUpper, width: width, height: height}; | |
| let rowFixedLength = mathMin(width, height); | |
| let best = Infinity; // the best row score so far | |
| const row = [] as LayoutRow; | |
| row.area = 0; | |
| for (let i = 0, len = viewChildren.length; i < len;) { | |
| const child = viewChildren[i]; | |
| row.push(child); | |
| row.area += child.getLayout().area; | |
| const score = worst(row, rowFixedLength, options.squareRatio); | |
| // continue with this orientation | |
| if (score <= best) { | |
| i++; | |
| best = score; | |
| } | |
| // abort, and try a different orientation | |
| else { | |
| row.area -= row.pop().getLayout().area; | |
| position(row, rowFixedLength, rect, halfGapWidth, false); | |
| rowFixedLength = mathMin(rect.width, rect.height); | |
| row.length = row.area = 0; | |
| best = Infinity; | |
| } | |
| } | |
| if (row.length) { | |
| position(row, rowFixedLength, rect, halfGapWidth, true); | |
| } | |
| if (!hideChildren) { | |
| const childrenVisibleMin = nodeModel.get('childrenVisibleMin'); | |
| if (childrenVisibleMin != null && totalArea < childrenVisibleMin) { | |
| hideChildren = true; | |
| } | |
| } | |
| for (let i = 0, len = viewChildren.length; i < len; i++) { | |
| squarify(viewChildren[i], options, hideChildren, depth + 1); | |
| } | |
| } | |
| /** | |
| * Set area to each child, and calculate data extent for visual coding. | |
| */ | |
| function initChildren( | |
| node: TreemapLayoutNode, | |
| nodeModel: NodeModel, | |
| totalArea: number, | |
| options: { | |
| sort?: OrderBy | |
| leafDepth?: number | |
| }, | |
| hideChildren: boolean, | |
| depth: number | |
| ) { | |
| let viewChildren = node.children || []; | |
| let orderBy = options.sort; | |
| orderBy !== 'asc' && orderBy !== 'desc' && (orderBy = null); | |
| const overLeafDepth = options.leafDepth != null && options.leafDepth <= depth; | |
| // leafDepth has higher priority. | |
| if (hideChildren && !overLeafDepth) { | |
| return (node.viewChildren = []); | |
| } | |
| // Sort children, order by desc. | |
| viewChildren = zrUtil.filter(viewChildren, function (child) { | |
| return !child.isRemoved(); | |
| }); | |
| sort(viewChildren, orderBy); | |
| const info = statistic(nodeModel, viewChildren, orderBy); | |
| if (info.sum === 0) { | |
| return (node.viewChildren = []); | |
| } | |
| info.sum = filterByThreshold(nodeModel, totalArea, info.sum, orderBy, viewChildren); | |
| if (info.sum === 0) { | |
| return (node.viewChildren = []); | |
| } | |
| // Set area to each child. | |
| for (let i = 0, len = viewChildren.length; i < len; i++) { | |
| const area = viewChildren[i].getValue() as number / info.sum * totalArea; | |
| // Do not use setLayout({...}, true), because it is needed to clear last layout. | |
| viewChildren[i].setLayout({ | |
| area: area | |
| }); | |
| } | |
| if (overLeafDepth) { | |
| viewChildren.length && node.setLayout({ | |
| isLeafRoot: true | |
| }, true); | |
| viewChildren.length = 0; | |
| } | |
| node.viewChildren = viewChildren; | |
| node.setLayout({ | |
| dataExtent: info.dataExtent | |
| }, true); | |
| return viewChildren; | |
| } | |
| /** | |
| * Consider 'visibleMin'. Modify viewChildren and get new sum. | |
| */ | |
| function filterByThreshold( | |
| nodeModel: NodeModel, | |
| totalArea: number, | |
| sum: number, | |
| orderBy: OrderBy, | |
| orderedChildren: TreemapLayoutNode[] | |
| ) { | |
| // visibleMin is not supported yet when no option.sort. | |
| if (!orderBy) { | |
| return sum; | |
| } | |
| const visibleMin = nodeModel.get('visibleMin'); | |
| const len = orderedChildren.length; | |
| let deletePoint = len; | |
| // Always travel from little value to big value. | |
| for (let i = len - 1; i >= 0; i--) { | |
| const value = orderedChildren[ | |
| orderBy === 'asc' ? len - i - 1 : i | |
| ].getValue() as number; | |
| if (value / sum * totalArea < visibleMin) { | |
| deletePoint = i; | |
| sum -= value; | |
| } | |
| } | |
| orderBy === 'asc' | |
| ? orderedChildren.splice(0, len - deletePoint) | |
| : orderedChildren.splice(deletePoint, len - deletePoint); | |
| return sum; | |
| } | |
| /** | |
| * Sort | |
| */ | |
| function sort( | |
| viewChildren: TreemapLayoutNode[], | |
| orderBy: OrderBy | |
| ) { | |
| if (orderBy) { | |
| viewChildren.sort(function (a, b) { | |
| const diff = orderBy === 'asc' | |
| ? a.getValue() as number - (b.getValue() as number) | |
| : b.getValue() as number - (a.getValue() as number); | |
| return diff === 0 | |
| ? (orderBy === 'asc' | |
| ? a.dataIndex - b.dataIndex : b.dataIndex - a.dataIndex | |
| ) | |
| : diff; | |
| }); | |
| } | |
| return viewChildren; | |
| } | |
| /** | |
| * Statistic | |
| */ | |
| function statistic( | |
| nodeModel: NodeModel, | |
| children: TreemapLayoutNode[], | |
| orderBy: OrderBy | |
| ) { | |
| // Calculate sum. | |
| let sum = 0; | |
| for (let i = 0, len = children.length; i < len; i++) { | |
| sum += children[i].getValue() as number; | |
| } | |
| // Statistic data extent for latter visual coding. | |
| // Notice: data extent should be calculate based on raw children | |
| // but not filtered view children, otherwise visual mapping will not | |
| // be stable when zoom (where children is filtered by visibleMin). | |
| const dimension = nodeModel.get('visualDimension'); | |
| let dataExtent: number[]; | |
| // The same as area dimension. | |
| if (!children || !children.length) { | |
| dataExtent = [NaN, NaN]; | |
| } | |
| else if (dimension === 'value' && orderBy) { | |
| dataExtent = [ | |
| children[children.length - 1].getValue() as number, | |
| children[0].getValue() as number | |
| ]; | |
| orderBy === 'asc' && dataExtent.reverse(); | |
| } | |
| // Other dimension. | |
| else { | |
| dataExtent = [Infinity, -Infinity]; | |
| each(children, function (child) { | |
| const value = child.getValue(dimension) as number; | |
| value < dataExtent[0] && (dataExtent[0] = value); | |
| value > dataExtent[1] && (dataExtent[1] = value); | |
| }); | |
| } | |
| return {sum: sum, dataExtent: dataExtent}; | |
| } | |
| /** | |
| * Computes the score for the specified row, | |
| * as the worst aspect ratio. | |
| */ | |
| function worst(row: LayoutRow, rowFixedLength: number, ratio: number) { | |
| let areaMax = 0; | |
| let areaMin = Infinity; | |
| for (let i = 0, area, len = row.length; i < len; i++) { | |
| area = row[i].getLayout().area; | |
| if (area) { | |
| area < areaMin && (areaMin = area); | |
| area > areaMax && (areaMax = area); | |
| } | |
| } | |
| const squareArea = row.area * row.area; | |
| const f = rowFixedLength * rowFixedLength * ratio; | |
| return squareArea | |
| ? mathMax( | |
| (f * areaMax) / squareArea, | |
| squareArea / (f * areaMin) | |
| ) | |
| : Infinity; | |
| } | |
| /** | |
| * Positions the specified row of nodes. Modifies `rect`. | |
| */ | |
| function position( | |
| row: LayoutRow, | |
| rowFixedLength: number, | |
| rect: RectLike, | |
| halfGapWidth: number, | |
| flush?: boolean | |
| ) { | |
| // When rowFixedLength === rect.width, | |
| // it is horizontal subdivision, | |
| // rowFixedLength is the width of the subdivision, | |
| // rowOtherLength is the height of the subdivision, | |
| // and nodes will be positioned from left to right. | |
| // wh[idx0WhenH] means: when horizontal, | |
| // wh[idx0WhenH] => wh[0] => 'width'. | |
| // xy[idx1WhenH] => xy[1] => 'y'. | |
| const idx0WhenH = rowFixedLength === rect.width ? 0 : 1; | |
| const idx1WhenH = 1 - idx0WhenH; | |
| const xy = ['x', 'y'] as const; | |
| const wh = ['width', 'height'] as const; | |
| let last = rect[xy[idx0WhenH]]; | |
| let rowOtherLength = rowFixedLength | |
| ? row.area / rowFixedLength : 0; | |
| if (flush || rowOtherLength > rect[wh[idx1WhenH]]) { | |
| rowOtherLength = rect[wh[idx1WhenH]]; // over+underflow | |
| } | |
| for (let i = 0, rowLen = row.length; i < rowLen; i++) { | |
| const node = row[i]; | |
| const nodeLayout = {} as TreemapItemLayout; | |
| const step = rowOtherLength | |
| ? node.getLayout().area / rowOtherLength : 0; | |
| const wh1 = nodeLayout[wh[idx1WhenH]] = mathMax(rowOtherLength - 2 * halfGapWidth, 0); | |
| // We use Math.max/min to avoid negative width/height when considering gap width. | |
| const remain = rect[xy[idx0WhenH]] + rect[wh[idx0WhenH]] - last; | |
| const modWH = (i === rowLen - 1 || remain < step) ? remain : step; | |
| const wh0 = nodeLayout[wh[idx0WhenH]] = mathMax(modWH - 2 * halfGapWidth, 0); | |
| nodeLayout[xy[idx1WhenH]] = rect[xy[idx1WhenH]] + mathMin(halfGapWidth, wh1 / 2); | |
| nodeLayout[xy[idx0WhenH]] = last + mathMin(halfGapWidth, wh0 / 2); | |
| last += modWH; | |
| node.setLayout(nodeLayout, true); | |
| } | |
| rect[xy[idx1WhenH]] += rowOtherLength; | |
| rect[wh[idx1WhenH]] -= rowOtherLength; | |
| } | |
| // Return [containerWidth, containerHeight] as default. | |
| function estimateRootSize( | |
| seriesModel: TreemapSeriesModel, | |
| targetInfo: { node: TreemapLayoutNode }, | |
| viewRoot: TreemapLayoutNode, | |
| containerWidth: number, | |
| containerHeight: number | |
| ) { | |
| // If targetInfo.node exists, we zoom to the node, | |
| // so estimate whole width and height by target node. | |
| let currNode = (targetInfo || {}).node; | |
| const defaultSize = [containerWidth, containerHeight]; | |
| if (!currNode || currNode === viewRoot) { | |
| return defaultSize; | |
| } | |
| let parent; | |
| const viewArea = containerWidth * containerHeight; | |
| let area = viewArea * seriesModel.option.zoomToNodeRatio; | |
| while (parent = currNode.parentNode) { // jshint ignore:line | |
| let sum = 0; | |
| const siblings = parent.children; | |
| for (let i = 0, len = siblings.length; i < len; i++) { | |
| sum += siblings[i].getValue() as number; | |
| } | |
| const currNodeValue = currNode.getValue() as number; | |
| if (currNodeValue === 0) { | |
| return defaultSize; | |
| } | |
| area *= sum / currNodeValue; | |
| // Considering border, suppose aspect ratio is 1. | |
| const parentModel = parent.getModel<TreemapSeriesNodeItemOption>(); | |
| const borderWidth = parentModel.get(PATH_BORDER_WIDTH); | |
| const upperHeight = Math.max(borderWidth, getUpperLabelHeight(parentModel)); | |
| area += 4 * borderWidth * borderWidth | |
| + (3 * borderWidth + upperHeight) * Math.pow(area, 0.5); | |
| area > MAX_SAFE_INTEGER && (area = MAX_SAFE_INTEGER); | |
| currNode = parent; | |
| } | |
| area < viewArea && (area = viewArea); | |
| const scale = Math.pow(area / viewArea, 0.5); | |
| return [containerWidth * scale, containerHeight * scale]; | |
| } | |
| // Root position based on coord of containerGroup | |
| function calculateRootPosition( | |
| layoutInfo: layout.LayoutRect, | |
| rootRect: RectLike, | |
| targetInfo: { node: TreemapLayoutNode } | |
| ) { | |
| if (rootRect) { | |
| return {x: rootRect.x, y: rootRect.y}; | |
| } | |
| const defaultPosition = {x: 0, y: 0}; | |
| if (!targetInfo) { | |
| return defaultPosition; | |
| } | |
| // If targetInfo is fetched by 'retrieveTargetInfo', | |
| // old tree and new tree are the same tree, | |
| // so the node still exists and we can visit it. | |
| const targetNode = targetInfo.node; | |
| const layout = targetNode.getLayout(); | |
| if (!layout) { | |
| return defaultPosition; | |
| } | |
| // Transform coord from local to container. | |
| const targetCenter = [layout.width / 2, layout.height / 2]; | |
| let node = targetNode; | |
| while (node) { | |
| const nodeLayout = node.getLayout(); | |
| targetCenter[0] += nodeLayout.x; | |
| targetCenter[1] += nodeLayout.y; | |
| node = node.parentNode; | |
| } | |
| return { | |
| x: layoutInfo.width / 2 - targetCenter[0], | |
| y: layoutInfo.height / 2 - targetCenter[1] | |
| }; | |
| } | |
| // Mark nodes visible for prunning when visual coding and rendering. | |
| // Prunning depends on layout and root position, so we have to do it after layout. | |
| function prunning( | |
| node: TreemapLayoutNode, | |
| clipRect: BoundingRect, | |
| viewAbovePath: TreemapLayoutNode[], | |
| viewRoot: TreemapLayoutNode, | |
| depth: number | |
| ) { | |
| const nodeLayout = node.getLayout(); | |
| const nodeInViewAbovePath = viewAbovePath[depth]; | |
| const isAboveViewRoot = nodeInViewAbovePath && nodeInViewAbovePath === node; | |
| if ( | |
| (nodeInViewAbovePath && !isAboveViewRoot) | |
| || (depth === viewAbovePath.length && node !== viewRoot) | |
| ) { | |
| return; | |
| } | |
| node.setLayout({ | |
| // isInView means: viewRoot sub tree + viewAbovePath | |
| isInView: true, | |
| // invisible only means: outside view clip so that the node can not | |
| // see but still layout for animation preparation but not render. | |
| invisible: !isAboveViewRoot && !clipRect.intersect(nodeLayout), | |
| isAboveViewRoot | |
| }, true); | |
| // Transform to child coordinate. | |
| const childClipRect = new BoundingRect( | |
| clipRect.x - nodeLayout.x, | |
| clipRect.y - nodeLayout.y, | |
| clipRect.width, | |
| clipRect.height | |
| ); | |
| each(node.viewChildren || [], function (child) { | |
| prunning(child, childClipRect, viewAbovePath, viewRoot, depth + 1); | |
| }); | |
| } | |
| function getUpperLabelHeight(model: NodeModel): number { | |
| return model.get(PATH_UPPER_LABEL_SHOW) ? model.get(PATH_UPPER_LABEL_HEIGHT) : 0; | |
| } |