|  | /* | 
|  | * 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; | 
|  | } |