123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570 |
- /*
- * 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.
- */
- var zrUtil = require("zrender/lib/core/util");
- var BoundingRect = require("zrender/lib/core/BoundingRect");
- var _number = require("../../util/number");
- var parsePercent = _number.parsePercent;
- var MAX_SAFE_INTEGER = _number.MAX_SAFE_INTEGER;
- var layout = require("../../util/layout");
- var helper = require("../helper/treeHelper");
- /*
- * 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 embeded 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>).
- */
- var mathMax = Math.max;
- var mathMin = Math.min;
- var retrieveValue = zrUtil.retrieve;
- var each = zrUtil.each;
- var PATH_BORDER_WIDTH = ['itemStyle', 'borderWidth'];
- var PATH_GAP_WIDTH = ['itemStyle', 'gapWidth'];
- var PATH_UPPER_LABEL_SHOW = ['upperLabel', 'show'];
- var PATH_UPPER_LABEL_HEIGHT = ['upperLabel', 'height'];
- /**
- * @public
- */
- var _default = {
- seriesType: 'treemap',
- reset: function (seriesModel, ecModel, api, payload) {
- // Layout result in each node:
- // {x, y, width, height, area, borderWidth}
- var ecWidth = api.getWidth();
- var ecHeight = api.getHeight();
- var seriesOption = seriesModel.option;
- var layoutInfo = layout.getLayoutRect(seriesModel.getBoxLayoutParams(), {
- width: api.getWidth(),
- height: api.getHeight()
- });
- var size = seriesOption.size || []; // Compatible with ec2.
- var containerWidth = parsePercent(retrieveValue(layoutInfo.width, size[0]), ecWidth);
- var containerHeight = parsePercent(retrieveValue(layoutInfo.height, size[1]), ecHeight); // Fetch payload info.
- var payloadType = payload && payload.type;
- var types = ['treemapZoomToNode', 'treemapRootToNode'];
- var targetInfo = helper.retrieveTargetInfo(payload, types, seriesModel);
- var rootRect = payloadType === 'treemapRender' || payloadType === 'treemapMove' ? payload.rootRect : null;
- var viewRoot = seriesModel.getViewRoot();
- var viewAbovePath = helper.getPathToRoot(viewRoot);
- if (payloadType !== 'treemapMove') {
- var rootSize = payloadType === 'treemapZoomToNode' ? estimateRootSize(seriesModel, targetInfo, viewRoot, containerWidth, containerHeight) : rootRect ? [rootRect.width, rootRect.height] : [containerWidth, containerHeight];
- var sort = seriesOption.sort;
- if (sort && sort !== 'asc' && sort !== 'desc') {
- sort = 'desc';
- }
- var 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
- var 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.
- var viewRootLayout = viewRoot.getLayout();
- each(viewAbovePath, function (node, index) {
- var childValue = (viewAbovePath[index + 1] || viewRoot).getValue();
- node.setLayout(zrUtil.extend({
- dataExtent: [childValue, childValue],
- borderWidth: 0,
- upperHeight: 0
- }, viewRootLayout));
- });
- }
- var 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, options, hideChildren, depth) {
- var width;
- var height;
- if (node.isRemoved()) {
- return;
- }
- var thisLayout = node.getLayout();
- width = thisLayout.width;
- height = thisLayout.height; // Considering border and gap
- var nodeModel = node.getModel();
- var borderWidth = nodeModel.get(PATH_BORDER_WIDTH);
- var halfGapWidth = nodeModel.get(PATH_GAP_WIDTH) / 2;
- var upperLabelHeight = getUpperLabelHeight(nodeModel);
- var upperHeight = Math.max(borderWidth, upperLabelHeight);
- var layoutOffset = borderWidth - halfGapWidth;
- var layoutOffsetUpper = upperHeight - halfGapWidth;
- var nodeModel = node.getModel();
- node.setLayout({
- borderWidth: borderWidth,
- upperHeight: upperHeight,
- upperLabelHeight: upperLabelHeight
- }, true);
- width = mathMax(width - 2 * layoutOffset, 0);
- height = mathMax(height - layoutOffset - layoutOffsetUpper, 0);
- var totalArea = width * height;
- var viewChildren = initChildren(node, nodeModel, totalArea, options, hideChildren, depth);
- if (!viewChildren.length) {
- return;
- }
- var rect = {
- x: layoutOffset,
- y: layoutOffsetUpper,
- width: width,
- height: height
- };
- var rowFixedLength = mathMin(width, height);
- var best = Infinity; // the best row score so far
- var row = [];
- row.area = 0;
- for (var i = 0, len = viewChildren.length; i < len;) {
- var child = viewChildren[i];
- row.push(child);
- row.area += child.getLayout().area;
- var 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) {
- var childrenVisibleMin = nodeModel.get('childrenVisibleMin');
- if (childrenVisibleMin != null && totalArea < childrenVisibleMin) {
- hideChildren = true;
- }
- }
- for (var 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, nodeModel, totalArea, options, hideChildren, depth) {
- var viewChildren = node.children || [];
- var orderBy = options.sort;
- orderBy !== 'asc' && orderBy !== 'desc' && (orderBy = null);
- var 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);
- var 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 (var i = 0, len = viewChildren.length; i < len; i++) {
- var area = viewChildren[i].getValue() / 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, totalArea, sum, orderBy, orderedChildren) {
- // visibleMin is not supported yet when no option.sort.
- if (!orderBy) {
- return sum;
- }
- var visibleMin = nodeModel.get('visibleMin');
- var len = orderedChildren.length;
- var deletePoint = len; // Always travel from little value to big value.
- for (var i = len - 1; i >= 0; i--) {
- var value = orderedChildren[orderBy === 'asc' ? len - i - 1 : i].getValue();
- 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, orderBy) {
- if (orderBy) {
- viewChildren.sort(function (a, b) {
- var diff = orderBy === 'asc' ? a.getValue() - b.getValue() : b.getValue() - a.getValue();
- return diff === 0 ? orderBy === 'asc' ? a.dataIndex - b.dataIndex : b.dataIndex - a.dataIndex : diff;
- });
- }
- return viewChildren;
- }
- /**
- * Statistic
- */
- function statistic(nodeModel, children, orderBy) {
- // Calculate sum.
- var sum = 0;
- for (var i = 0, len = children.length; i < len; i++) {
- sum += children[i].getValue();
- } // 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).
- var dimension = nodeModel.get('visualDimension');
- var dataExtent; // The same as area dimension.
- if (!children || !children.length) {
- dataExtent = [NaN, NaN];
- } else if (dimension === 'value' && orderBy) {
- dataExtent = [children[children.length - 1].getValue(), children[0].getValue()];
- orderBy === 'asc' && dataExtent.reverse();
- } // Other dimension.
- else {
- var dataExtent = [Infinity, -Infinity];
- each(children, function (child) {
- var value = child.getValue(dimension);
- 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, rowFixedLength, ratio) {
- var areaMax = 0;
- var areaMin = Infinity;
- for (var i = 0, area, len = row.length; i < len; i++) {
- area = row[i].getLayout().area;
- if (area) {
- area < areaMin && (areaMin = area);
- area > areaMax && (areaMax = area);
- }
- }
- var squareArea = row.area * row.area;
- var 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, rowFixedLength, rect, halfGapWidth, flush) {
- // 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'.
- var idx0WhenH = rowFixedLength === rect.width ? 0 : 1;
- var idx1WhenH = 1 - idx0WhenH;
- var xy = ['x', 'y'];
- var wh = ['width', 'height'];
- var last = rect[xy[idx0WhenH]];
- var rowOtherLength = rowFixedLength ? row.area / rowFixedLength : 0;
- if (flush || rowOtherLength > rect[wh[idx1WhenH]]) {
- rowOtherLength = rect[wh[idx1WhenH]]; // over+underflow
- }
- for (var i = 0, rowLen = row.length; i < rowLen; i++) {
- var node = row[i];
- var nodeLayout = {};
- var step = rowOtherLength ? node.getLayout().area / rowOtherLength : 0;
- var wh1 = nodeLayout[wh[idx1WhenH]] = mathMax(rowOtherLength - 2 * halfGapWidth, 0); // We use Math.max/min to avoid negative width/height when considering gap width.
- var remain = rect[xy[idx0WhenH]] + rect[wh[idx0WhenH]] - last;
- var modWH = i === rowLen - 1 || remain < step ? remain : step;
- var 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, targetInfo, viewRoot, containerWidth, containerHeight) {
- // If targetInfo.node exists, we zoom to the node,
- // so estimate whold width and heigth by target node.
- var currNode = (targetInfo || {}).node;
- var defaultSize = [containerWidth, containerHeight];
- if (!currNode || currNode === viewRoot) {
- return defaultSize;
- }
- var parent;
- var viewArea = containerWidth * containerHeight;
- var area = viewArea * seriesModel.option.zoomToNodeRatio;
- while (parent = currNode.parentNode) {
- // jshint ignore:line
- var sum = 0;
- var siblings = parent.children;
- for (var i = 0, len = siblings.length; i < len; i++) {
- sum += siblings[i].getValue();
- }
- var currNodeValue = currNode.getValue();
- if (currNodeValue === 0) {
- return defaultSize;
- }
- area *= sum / currNodeValue; // Considering border, suppose aspect ratio is 1.
- var parentModel = parent.getModel();
- var borderWidth = parentModel.get(PATH_BORDER_WIDTH);
- var upperHeight = Math.max(borderWidth, getUpperLabelHeight(parentModel, borderWidth));
- 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);
- var scale = Math.pow(area / viewArea, 0.5);
- return [containerWidth * scale, containerHeight * scale];
- } // Root postion base on coord of containerGroup
- function calculateRootPosition(layoutInfo, rootRect, targetInfo) {
- if (rootRect) {
- return {
- x: rootRect.x,
- y: rootRect.y
- };
- }
- var 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.
- var targetNode = targetInfo.node;
- var layout = targetNode.getLayout();
- if (!layout) {
- return defaultPosition;
- } // Transform coord from local to container.
- var targetCenter = [layout.width / 2, layout.height / 2];
- var node = targetNode;
- while (node) {
- var 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, clipRect, viewAbovePath, viewRoot, depth) {
- var nodeLayout = node.getLayout();
- var nodeInViewAbovePath = viewAbovePath[depth];
- var 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: isAboveViewRoot
- }, true); // Transform to child coordinate.
- var 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) {
- return model.get(PATH_UPPER_LABEL_SHOW) ? model.get(PATH_UPPER_LABEL_HEIGHT) : 0;
- }
- module.exports = _default;
|