From abdb25cf5344a78fc7d8b2bb4429160b261e2033 Mon Sep 17 00:00:00 2001 From: pah100 Date: Mon, 14 Mar 2016 20:15:25 +0800 Subject: [PATCH] treemap drill down (part I) --- src/chart/treemap/Breadcrumb.js | 1 + src/chart/treemap/TreemapSeries.js | 105 +++++++++----- src/chart/treemap/TreemapView.js | 224 +++++++++++++++++++---------- src/chart/treemap/helper.js | 46 ++++-- src/chart/treemap/treemapAction.js | 34 ++++- src/chart/treemap/treemapLayout.js | 74 ++++++---- test/treemap-disk.html | 17 +++ test/treemap-option.html | 104 ++++++++++++++ 8 files changed, 452 insertions(+), 153 deletions(-) create mode 100644 test/treemap-option.html diff --git a/src/chart/treemap/Breadcrumb.js b/src/chart/treemap/Breadcrumb.js index b9d8d6bde..a8c1db806 100644 --- a/src/chart/treemap/Breadcrumb.js +++ b/src/chart/treemap/Breadcrumb.js @@ -127,6 +127,7 @@ textFont: textStyleModel.getFont() } ), + z: 10, onclick: zrUtil.bind(this._onSelect, this, item.node) })); diff --git a/src/chart/treemap/TreemapSeries.js b/src/chart/treemap/TreemapSeries.js index ff56592c2..9f7d5dec8 100644 --- a/src/chart/treemap/TreemapSeries.js +++ b/src/chart/treemap/TreemapSeries.js @@ -5,6 +5,7 @@ define(function(require) { var zrUtil = require('zrender/core/util'); var Model = require('../../model/Model'); var formatUtil = require('../../util/format'); + var helper = require('./helper'); var encodeHTML = formatUtil.encodeHTML; var addCommas = formatUtil.addCommas; @@ -15,9 +16,14 @@ define(function(require) { dependencies: ['grid', 'polar'], + /** + * @type {module:echarts/data/Tree~Node} + */ + _viewRoot: null, + defaultOption: { - // center: ['50%', '50%'], // not supported in ec3. - // size: ['80%', '80%'], // deprecated, compatible with ec2. + // center: ['50%', '50%'], // not supported in ec3. + // size: ['80%', '80%'], // deprecated, compatible with ec2. left: 'center', top: 'middle', right: null, @@ -26,15 +32,20 @@ define(function(require) { height: '80%', sort: true, // Can be null or false or true // (order by desc default, asc not supported yet (strange effect)) - clipWindow: 'origin', // 缩放时窗口大小。'origin' or 'fullscreen' + clipWindow: 'origin', // Size of clipped window when zooming. 'origin' or 'fullscreen' squareRatio: 0.5 * (1 + Math.sqrt(5)), // golden ratio - root: null, // default: tree root. This feature doesnt work unless node have id. + leafDepth: null, // Nodes on depth from root are regarded as leaves. + // Count from zero (zero represents only view root). visualDimension: 0, // Can be 0, 1, 2, 3. - zoomToNodeRatio: 0.32 * 0.32, // zoom to node时 node占可视区域的面积比例。 - roam: true, // true, false, 'scale' or 'zoom', 'move' - nodeClick: 'zoomToNode', // 'zoomToNode', 'link', false + zoomToNodeRatio: 0.32 * 0.32, // Be effective when using zoomToNode. Specify the proportion of the + // target node area in the view area. + roam: true, // true, false, 'scale' or 'zoom', 'move'. + nodeClick: 'zoomToNode', // Leaf node click behaviour: 'zoomToNode', 'link', false. + // If leafDepth is set and clicking a node which has children but + // be on left depth, the behaviour would be changing root. Otherwise + // use behavious defined above. animation: true, - animationDurationUpdate: 1500, + animationDurationUpdate: 900, animationEasing: 'quinticInOut', breadcrumb: { show: true, @@ -43,7 +54,7 @@ define(function(require) { top: 'bottom', // right // bottom - emptyItemWidth: 25, // 空节点宽度 + emptyItemWidth: 25, // Width of empty node. itemStyle: { normal: { color: 'rgba(0,0,0,0.7)', //'#5793f3', @@ -65,7 +76,7 @@ define(function(require) { label: { normal: { show: true, - position: ['50%', '50%'], // 可以是 5 '5%' 'insideTopLeft', ... + position: ['50%', '50%'], // Can be 5, '5%' or position stirng like 'insideTopLeft', ... textStyle: { align: 'center', baseline: 'middle', @@ -76,30 +87,35 @@ define(function(require) { }, itemStyle: { normal: { - color: null, // 各异 如不需,可设为'none' - colorAlpha: null, // 默认不设置 如不需,可设为'none' - colorSaturation: null, // 默认不设置 如不需,可设为'none' + color: null, // Can be 'none' if not necessary. + colorAlpha: null, // Can be 'none' if not necessary. + colorSaturation: null, // Can be 'none' if not necessary. borderWidth: 0, gapWidth: 0, borderColor: '#fff', - borderColorSaturation: null // 如果设置,则borderColor的设置无效,而是取当前节点计算出的颜色,再经由borderColorSaturation处理。 + borderColorSaturation: null // If specified, borderColor will be ineffective, and the + // border color is evaluated by color of current node and + // borderColorSaturation. }, - emphasis: {} + emphasis: { + + } }, - color: 'none', // 为数组,表示同一level的color 选取列表。默认空,在level[0].color中取系统color列表。 - colorAlpha: null, // 为数组,表示同一level的color alpha 选取范围。 - colorSaturation: null, // 为数组,表示同一level的color alpha 选取范围。 - colorMappingBy: 'index', // 'value' or 'index' or 'id'. - visibleMin: 10, // If area less than this threshold (unit: pixel^2), node will not be rendered. - // Only works when sort is 'asc' or 'desc'. - childrenVisibleMin: null, // If area of a node less than this threshold (unit: pixel^2), - // grandchildren will not show. - // Why grandchildren? If not grandchildren but children, - // some siblings show children and some not, - // the appearance may be mess and not consistent, - levels: [] // Each item: { - // visibleMin, itemStyle, visualDimension, label - // } + color: 'none', // Array. Specify color list of each level. + // level[0].color would be global color list. + colorAlpha: null, // Array. Specify color alpha range of each level, like [0.2, 0.8] + colorSaturation: null, // Array. Specify color saturation of each level, like [0.2, 0.5] + colorMappingBy: 'index', // 'value' or 'index' or 'id'. + visibleMin: 10, // If area less than this threshold (unit: pixel^2), node will not + // be rendered. Only works when sort is 'asc' or 'desc'. + childrenVisibleMin: null, // If area of a node less than this threshold (unit: pixel^2), + // grandchildren will not show. + // Why grandchildren? If not grandchildren but children, + // some siblings show children and some not, + // the appearance may be mess and not consistent, + levels: [] // Each item: { + // visibleMin, itemStyle, visualDimension, label + // } // data: { // value: [], // children: [], @@ -134,13 +150,8 @@ define(function(require) { return Tree.createTree(root, this, levels).data; }, - /** - * @public - */ - getViewRoot: function () { - var optionRoot = this.option.root; - var treeRoot = this.getData().tree.root; - return optionRoot && treeRoot.getNodeById(optionRoot) || treeRoot; + optionUpdated: function () { + this.resetViewRoot(); }, /** @@ -239,6 +250,28 @@ define(function(require) { } return index; + }, + + getViewRoot: function () { + return this._viewRoot; + }, + + /** + * @param {module:echarts/data/Tree~Node} [viewRoot] + * @return {string} direction 'drilldown' or 'rollup' + */ + resetViewRoot: function (viewRoot) { + viewRoot + ? (this._viewRoot = viewRoot) + : (viewRoot = this._viewRoot); + + var root = this.getData().tree.root; + + if (!viewRoot + || (viewRoot !== root && !root.contains(viewRoot)) + ) { + this._viewRoot = root; + } } }); diff --git a/src/chart/treemap/TreemapView.js b/src/chart/treemap/TreemapView.js index 86ab1a017..5a81b53a8 100644 --- a/src/chart/treemap/TreemapView.js +++ b/src/chart/treemap/TreemapView.js @@ -84,21 +84,36 @@ this.api = api; this.ecModel = ecModel; + var targetInfo = helper.retrieveTargetInfo(payload, seriesModel); var payloadType = payload && payload.type; var layoutInfo = seriesModel.layoutInfo; var isInit = !this._oldTree; + var thisStorage = this._storage; + + // Mark new root when action is treemapRootToNode. + var reRoot = (payloadType === 'treemapRootToNode' && targetInfo && thisStorage) + ? { + rootNodeGroup: thisStorage.nodeGroup[targetInfo.node.getRawIndex()], + direction: payload.direction + } + : null; var containerGroup = this._giveContainerGroup(layoutInfo); - var renderResult = this._doRender(containerGroup, seriesModel); + var renderResult = this._doRender(containerGroup, seriesModel, reRoot); - (!isInit && (!payloadType || payloadType === 'treemapZoomToNode')) - ? this._doAnimation(containerGroup, renderResult, seriesModel) + ( + !isInit && ( + !payloadType + || payloadType === 'treemapZoomToNode' + || payloadType === 'treemapRootToNode' + ) + ) + ? this._doAnimation(containerGroup, renderResult, seriesModel, reRoot) : renderResult.renderFinally(); this._resetController(api); - var targetInfo = helper.retrieveTargetInfo(payload, seriesModel); this._renderBreadcrumb(seriesModel, api, targetInfo); }, @@ -122,7 +137,7 @@ /** * @private */ - _doRender: function (containerGroup, seriesModel) { + _doRender: function (containerGroup, seriesModel, reRoot) { var thisTree = seriesModel.getData().tree; var oldTree = this._oldTree; @@ -135,9 +150,11 @@ var willDeleteEls = []; var renderNode = bind( this._renderNode, this, - thisStorage, oldStorage, lastsForAnimation, willInvisibleEls, willVisibleEls + thisStorage, oldStorage, reRoot, + lastsForAnimation, willInvisibleEls, willVisibleEls ); var viewRoot = seriesModel.getViewRoot(); + var viewPath = helper.getPathToRoot(viewRoot); // Notice: when thisTree and oldTree are the same tree (see list.cloneShadow), // the oldTree is actually losted, so we can not find all of the old graphic @@ -149,7 +166,7 @@ (oldTree && oldTree.root) ? [oldTree.root] : [], containerGroup, thisTree === oldTree || !oldTree, - viewRoot === thisTree.root + 0 ); // Process all removing. @@ -164,7 +181,7 @@ renderFinally: renderFinally }; - function dualTravel(thisViewChildren, oldViewChildren, parentGroup, sameTree, inView) { + function dualTravel(thisViewChildren, oldViewChildren, parentGroup, sameTree, viewPathIndex) { // When 'render' is triggered by action, // 'this' and 'old' may be the same tree, // we use rawIndex in that case. @@ -194,10 +211,14 @@ var oldNode = oldIndex != null ? oldViewChildren[oldIndex] : null; // Whether under viewRoot. - var subInView = inView || thisNode === viewRoot; - // If not under viewRoot, only remove. - if (!subInView) { - thisNode = null; + if (!thisNode + || isNaN(viewPathIndex) + || (viewPathIndex < viewPath.length && viewPath[viewPathIndex] !== thisNode) + ) { + // Deleting nodes will be performed finally. This method just find + // element from old storage, or create new element, set them to new + // storage, and set styles. + return; } var group = renderNode(thisNode, oldNode, parentGroup); @@ -207,7 +228,7 @@ oldNode && oldNode.viewChildren || [], group, sameTree, - subInView + viewPathIndex + 1 ); } } @@ -235,6 +256,7 @@ el.invisible = true; // Setting invisible is for optimizing, so no need to set dirty, // just mark as invisible. + el.dirty(); }); each(willVisibleEls, function (el) { el.invisible = false; @@ -248,19 +270,13 @@ * @private */ _renderNode: function ( - thisStorage, oldStorage, lastsForAnimation, - willInvisibleEls, willVisibleEls, + thisStorage, oldStorage, reRoot, + lastsForAnimation, willInvisibleEls, willVisibleEls, thisNode, oldNode, parentGroup ) { var thisRawIndex = thisNode && thisNode.getRawIndex(); var oldRawIndex = oldNode && oldNode.getRawIndex(); - // Deleting things will performed finally. This method just find element from - // old storage, or create new element, set them to new storage, and set styles. - if (!thisNode) { - return; - } - var layout = thisNode.getLayout(); var thisWidth = layout.width; var thisHeight = layout.height; @@ -277,7 +293,7 @@ group.__tmNodeHeight = thisHeight; // Background - var bg = giveGraphic('background', Rect); + var bg = giveGraphic('background', Rect, 0); if (bg) { bg.setShape({x: 0, y: 0, width: thisWidth, height: thisHeight}); updateStyle(bg, {fill: thisNode.getVisual('borderColor', true)}); @@ -289,13 +305,13 @@ // No children, render content. if (!thisViewChildren || !thisViewChildren.length) { var borderWidth = layout.borderWidth; - var content = giveGraphic('content', Rect); - + var content = giveGraphic('content', Rect, 3); if (content) { var contentWidth = Math.max(thisWidth - 2 * borderWidth, 0); var contentHeight = Math.max(thisHeight - 2 * borderWidth, 0); var labelModel = thisNode.getModel('label.normal'); var textStyleModel = thisNode.getModel('label.normal.textStyle'); + var hoverStyle = thisNode.getModel('itemStyle.emphasis').getItemStyle(); var text = thisNode.getModel().get('name'); var textRect = textStyleModel.getTextRect(text); var showLabel = labelModel.get('show'); @@ -308,6 +324,8 @@ ? textStyleModel.ellipsis(text, contentWidth) : ''; } + graphic.setHoverStyle(content, hoverStyle); + // For tooltip. content.dataIndex = thisNode.dataIndex; content.seriesIndex = this.seriesModel.seriesIndex; @@ -319,6 +337,7 @@ width: contentWidth, height: contentHeight }); + updateStyle(content, { fill: thisNode.getVisual('color', true), text: text, @@ -334,7 +353,7 @@ return group; - function giveGraphic(storageName, Ctor) { + function giveGraphic(storageName, Ctor, z) { var element = oldRawIndex != null && oldStorage[storageName][oldRawIndex]; var lasts = lastsForAnimation[storageName]; @@ -345,7 +364,7 @@ } // If invisible and no old element, do not create new element (for optimizing). else if (!invisible) { - element = new Ctor(); + element = new Ctor({z: z}); prepareAnimationWhenNoOld(lasts, element, storageName); } @@ -356,9 +375,9 @@ function prepareAnimationWhenHasOld(lasts, element, storageName) { var lastCfg = lasts[thisRawIndex] = {}; lastCfg.old = storageName === 'nodeGroup' - ? element.position.slice() - : zrUtil.extend({}, element.shape); - } + ? element.position.slice() + : zrUtil.extend({}, element.shape); + } // If a element is new, we need to find the animation start point carefully, // otherwise it will looks strange when 'zoomToNode'. @@ -370,23 +389,25 @@ willVisibleEls.push(element); } else { + var lastCfg = lasts[thisRawIndex] = {}; var parentNode = thisNode.parentNode; - var parentOldBg; - var parentOldX = 0; - var parentOldY = 0; - // For convenient, get old bounding rect from background. - if (parentNode && ( - parentOldBg = lastsForAnimation.background[parentNode.getRawIndex()] - )) { - parentOldX = parentOldBg.old.width; - parentOldY = parentOldBg.old.height; + + if (parentNode && (!reRoot || reRoot.direction === 'drilldown')) { + var parentOldX = 0; + var parentOldY = 0; + // For convenience, get old bounding rect from background. + var parentOldBg = lastsForAnimation.background[parentNode.getRawIndex()]; + + if (parentOldBg && parentOldBg.old) { + parentOldX = parentOldBg.old.width / 2; // Devided by 2 for reRoot effect. + parentOldY = parentOldBg.old.height / 2; + } + // When no parent old shape found, its parent is new too, + // so we can just use {x:0, y:0}. + lastCfg.old = storageName === 'nodeGroup' + ? [parentOldX, parentOldY] + : {x: parentOldX, y: parentOldY, width: 0, height: 0}; } - // When no parent old shape found, its parent is new too, - // so we can just use {x:0, y:0}. - var lastCfg = lasts[thisRawIndex] = {}; - lastCfg.old = storageName === 'nodeGroup' - ? [parentOldX, parentOldY] - : {x: parentOldX, y: parentOldY, width: 0, height: 0}; // Fade in, user can be aware that these nodes are new. lastCfg.fadein = storageName !== 'nodeGroup'; @@ -414,56 +435,87 @@ /** * @private */ - _doAnimation: function (containerGroup, renderResult, seriesModel) { + _doAnimation: function (containerGroup, renderResult, seriesModel, reRoot) { if (!seriesModel.get('animation')) { return; } var duration = seriesModel.get('animationDurationUpdate'); var easing = seriesModel.get('animationEasing'); - var animationWrap = animationUtil.createWrap(); // Make delete animations. - var viewRoot = this.seriesModel.getViewRoot(); - var rootGroup = this._storage.nodeGroup[viewRoot.getRawIndex()]; - rootGroup && rootGroup.traverse(function (el) { - var storageName; - if (el.invisible || !(storageName = el.__tmWillDelete)) { - return; - } - var targetX = 0; - var targetY = 0; - var parent = el.parent; // Always has parent, and parent is nodeGroup. - if (!parent.__tmWillDelete) { - // Let node animate to right-bottom corner, cooperating with fadeout, - // which is perfect for user understanding. - targetX = parent.__tmNodeWidth; - targetY = parent.__tmNodeHeight; - } - var target = storageName === 'nodeGroup' - ? {position: [targetX, targetY], style: {opacity: 0}} - : {shape: {x: targetX, y: targetY, width: 0, height: 0}, style: {opacity: 0}}; - animationWrap.add(el, target, duration, easing); + each(renderResult.willDeleteEls, function (store, storageName) { + each(store, function (el, rawIndex) { + var storageName; + + if (el.invisible || !(storageName = el.__tmWillDelete)) { + return; + } + + var parent = el.parent; // Always has parent, and parent is nodeGroup. + var target; + + if (reRoot && reRoot.direction === 'drilldown') { + if (parent === reRoot.rootNodeGroup) { + // Only 'content' will enter this branch, but not nodeGroup. + target = { + shape: { + x: 0, y: 0, + width: parent.__tmNodeWidth, height: parent.__tmNodeHeight + } + }; + el.z = 2; + } + else { + target = {style: {opacity: 0}}; + el.z = 1; + } + } + else { + var targetX = 0; + var targetY = 0; + + if (!parent.__tmWillDelete) { + // Let node animate to right-bottom corner, cooperating with fadeout, + // which is appropriate for user understanding. + // Divided by 2 for reRoot rollup effect. + targetX = parent.__tmNodeWidth / 2; + targetY = parent.__tmNodeHeight / 2; + } + target = storageName === 'nodeGroup' + ? {position: [targetX, targetY], style: {opacity: 0}} + : { + shape: {x: targetX, y: targetY, width: 0, height: 0}, + style: {opacity: 0} + }; + } + + target && animationWrap.add(el, target, duration, easing); + }); }); // Make other animations each(this._storage, function (store, storageName) { each(store, function (el, rawIndex) { var last = renderResult.lastsForAnimation[storageName][rawIndex]; - var target; + var target = {}; if (!last) { return; } if (storageName === 'nodeGroup') { - target = {position: el.position.slice()}; - el.position = last.old; + if (last.old) { + target.position = el.position.slice(); + el.position = last.old; + } } else { - target = {shape: zrUtil.extend({}, el.shape)}; - el.setShape(last.old); + if (last.old) { + target.shape = zrUtil.extend({}, el.shape); + el.setShape(last.old); + } if (last.fadein) { el.setStyle('opacity', 0); @@ -631,12 +683,19 @@ var targetInfo = this.findTarget(e.offsetX, e.offsetY); - if (targetInfo) { + if (!targetInfo) { + return; + } + + var node = targetInfo.node; + if (node.getLayout().isLeafRoot) { + this._rootToNode(targetInfo); + } + else { if (nodeClick === 'zoomToNode') { this._zoomToNode(targetInfo); } else if (nodeClick === 'link') { - var node = targetInfo.node; var itemModel = node.hostTree.data.getItemModel(node.dataIndex); var link = itemModel.get('link', true); var linkTarget = itemModel.get('target', true) || 'blank'; @@ -663,7 +722,11 @@ .render(seriesModel, api, targetInfo.node); function onSelect(node) { - this._zoomToNode({node: node}); + if (this._state !== 'animating') { + helper.aboveViewRoot(seriesModel.getViewRoot(), node) + ? this._rootToNode({node: node}) + : this._zoomToNode({node: node}); + } } }, @@ -694,6 +757,18 @@ }); }, + /** + * @private + */ + _rootToNode: function (targetInfo) { + this.api.dispatchAction({ + type: 'treemapRootToNode', + from: this.uid, + seriesId: this.seriesModel.id, + targetNode: targetInfo.node + }); + }, + /** * @public * @param {number} x Global coord x. @@ -736,4 +811,5 @@ function createStorage() { return {nodeGroup: [], background: [], content: []}; } + }); \ No newline at end of file diff --git a/src/chart/treemap/helper.js b/src/chart/treemap/helper.js index 7c61bc412..16ec41032 100644 --- a/src/chart/treemap/helper.js +++ b/src/chart/treemap/helper.js @@ -1,24 +1,48 @@ define(function (require) { + var zrUtil = require('zrender/core/util'); + var helper = { retrieveTargetInfo: function (payload, seriesModel) { - if (!payload || payload.type !== 'treemapZoomToNode') { - return; - } + if (payload + && ( + payload.type === 'treemapZoomToNode' + || payload.type === 'treemapRootToNode' + ) + ) { + var root = seriesModel.getData().tree.root; + var targetNode = payload.targetNode; + if (targetNode && root.contains(targetNode)) { + return {node: targetNode}; + } - var root = seriesModel.getData().tree.root; - var targetNode = payload.targetNode; - if (targetNode && root.contains(targetNode)) { - return {node: targetNode}; + var targetNodeId = payload.targetNodeId; + if (targetNodeId != null && (targetNode = root.getNodeById(targetNodeId))) { + return {node: targetNode}; + } } + }, - var targetNodeId = payload.targetNodeId; - if (targetNodeId != null && (targetNode = root.getNodeById(targetNodeId))) { - return {node: targetNode}; + getPathToRoot: function (node) { + var path = []; + while (node) { + path.push(node); + node = node.parentNode; } + return path.reverse(); + }, + + aboveViewRoot: function (viewRoot, node) { + var viewPath = helper.getPathToRoot(viewRoot); + return helper.aboveViewRootByViewPath(viewPath, node); + }, - return null; + // viewPath should obtained from getPathToRoot(viewRoot) + aboveViewRootByViewPath: function (viewPath, node) { + var index = zrUtil.indexOf(viewPath, node); + // The last one is viewRoot + return index >= 0 && index !== viewPath.length - 1; } }; diff --git a/src/chart/treemap/treemapAction.js b/src/chart/treemap/treemapAction.js index 1eca636e5..25e5745d0 100644 --- a/src/chart/treemap/treemapAction.js +++ b/src/chart/treemap/treemapAction.js @@ -4,11 +4,39 @@ define(function(require) { var echarts = require('../../echarts'); + var helper = require('./helper'); var noop = function () {}; - echarts.registerAction({type: 'treemapZoomToNode', update: 'updateView'}, noop); - echarts.registerAction({type: 'treemapRender', update: 'updateView'}, noop); - echarts.registerAction({type: 'treemapMove', update: 'updateView'}, noop); + var actionTypes = [ + 'treemapZoomToNode', + 'treemapRender', + 'treemapMove' + ]; + + for (var i = 0; i < actionTypes.length; i++) { + echarts.registerAction({type: actionTypes[i], update: 'updateView'}, noop); + } + + echarts.registerAction( + {type: 'treemapRootToNode', update: 'updateView'}, + function (payload, ecModel) { + ecModel.eachComponent( + {mainType: 'series', subType: 'treemap', query: payload}, + function (model, index) { + var targetInfo = helper.retrieveTargetInfo(payload, model); + + if (targetInfo) { + var originViewRoot = model.getViewRoot(); + if (originViewRoot) { + payload.direction = helper.aboveViewRoot(originViewRoot, targetInfo.node) + ? 'rollup' : 'drilldown'; + } + model.resetViewRoot(targetInfo.node); + } + } + ); + } + ); }); \ No newline at end of file diff --git a/src/chart/treemap/treemapLayout.js b/src/chart/treemap/treemapLayout.js index 6b5983d58..d6fd7bafd 100644 --- a/src/chart/treemap/treemapLayout.js +++ b/src/chart/treemap/treemapLayout.js @@ -5,6 +5,7 @@ define(function (require) { var zrUtil = require('zrender/core/util'); var numberUtil = require('../../util/number'); var layout = require('../../util/layout'); + var helper = require('./helper'); var parsePercent = numberUtil.parsePercent; var retrieveValue = zrUtil.retrieve; var BoundingRect = require('zrender/core/BoundingRect'); @@ -21,14 +22,15 @@ define(function (require) { var ecWidth = api.getWidth(); var ecHeight = api.getHeight(); + var seriesOption = seriesModel.option; - var size = seriesModel.get('size') || []; // Compatible with ec2. + var size = seriesOption.size || []; // Compatible with ec2. var containerWidth = parsePercent( - retrieveValue(seriesModel.get('width'), size[0]), + retrieveValue(seriesOption.width, size[0]), ecWidth ); var containerHeight = parsePercent( - retrieveValue(seriesModel.get('height'), size[1]), + retrieveValue(seriesOption.height, size[1]), ecHeight ); @@ -49,18 +51,21 @@ define(function (require) { if (payloadType !== 'treemapMove') { var rootSize = payloadType === 'treemapZoomToNode' - ? estimateRootSize(seriesModel, targetInfo, containerWidth, containerHeight) + ? estimateRootSize( + seriesModel, targetInfo, viewRoot, containerWidth, containerHeight + ) : rootRect ? [rootRect.width, rootRect.height] : [containerWidth, containerHeight]; - var sort = seriesModel.get('sort'); + var sort = seriesOption.sort; if (sort && sort !== 'asc' && sort !== 'desc') { sort = 'desc'; } var options = { - squareRatio: seriesModel.get('squareRatio'), - sort: sort + squareRatio: seriesOption.squareRatio, + sort: sort, + leafDepth: seriesOption.leafDepth }; viewRoot.setLayout({ @@ -69,7 +74,7 @@ define(function (require) { area: rootSize[0] * rootSize[1] }); - squarify(viewRoot, options); + squarify(viewRoot, options, false, 0); } // Set root position @@ -84,8 +89,10 @@ define(function (require) { // FIXME // 现在没有clip功能,暂时取ec高宽。 prunning( - viewRoot, - new BoundingRect(-layoutInfo.x, -layoutInfo.y, ecWidth, ecHeight) + seriesModel.getData().tree.root, + // Transform to base element coordinate system. + new BoundingRect(-layoutInfo.x, -layoutInfo.y, ecWidth, ecHeight), + helper.getPathToRoot(viewRoot) ); }); @@ -100,10 +107,11 @@ define(function (require) { * @param {module:echarts/data/Tree~TreeNode} node * @param {Object} options * @param {string} options.sort 'asc' or 'desc' - * @param {boolean} options.hideChildren * @param {number} options.squareRatio + * @param {boolean} hideChildren + * @param {number} depth */ - function squarify(node, options) { + function squarify(node, options, hideChildren, depth) { var width; var height; @@ -128,7 +136,9 @@ define(function (require) { height = mathMax(height - 2 * layoutOffset, 0); var totalArea = width * height; - var viewChildren = initChildren(node, nodeModel, totalArea, options); + var viewChildren = initChildren( + node, nodeModel, totalArea, options, hideChildren, depth + ); if (!viewChildren.length) { return; @@ -166,9 +176,7 @@ define(function (require) { position(row, rowFixedLength, rect, halfGapWidth, true); } - // Update option carefully. - var hideChildren; - if (!options.hideChildren) { + if (!hideChildren) { var childrenVisibleMin = nodeModel.get('childrenVisibleMin'); if (childrenVisibleMin != null && totalArea < childrenVisibleMin) { hideChildren = true; @@ -176,23 +184,22 @@ define(function (require) { } for (var i = 0, len = viewChildren.length; i < len; i++) { - var childOption = zrUtil.extend({ - hideChildren: hideChildren - }, options); - - squarify(viewChildren[i], childOption); + 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) { + function initChildren(node, nodeModel, totalArea, options, hideChildren, depth) { var viewChildren = node.children || []; var orderBy = options.sort; orderBy !== 'asc' && orderBy !== 'desc' && (orderBy = null); - if (options.hideChildren) { + var overLeafDepth = options.leafDepth != null && options.leafDepth <= depth; + + // leafDepth has higher priority. + if (hideChildren && !overLeafDepth) { return (node.viewChildren = []); } @@ -222,6 +229,11 @@ define(function (require) { 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); @@ -393,19 +405,19 @@ define(function (require) { } // Return [containerWidth, containerHeight] as defualt. - function estimateRootSize(seriesModel, targetInfo, containerWidth, containerHeight) { + 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 === seriesModel.getViewRoot()) { + if (!currNode || currNode === viewRoot) { return defaultSize; } var parent; var viewArea = containerWidth * containerHeight; - var area = viewArea * seriesModel.get('zoomToNodeRatio'); + var area = viewArea * seriesModel.option.zoomToNodeRatio; while (parent = currNode.parentNode) { // jshint ignore:line var sum = 0; @@ -478,10 +490,14 @@ define(function (require) { // Mark invisible nodes for prunning when visual coding and rendering. // Prunning depends on layout and root position, so we have to do it after them. - function prunning(node, clipRect) { + function prunning(node, clipRect, viewPath) { var nodeLayout = node.getLayout(); - node.setLayout({invisible: !clipRect.intersect(nodeLayout)}, true); + node.setLayout({ + invisible: nodeLayout + ? !clipRect.intersect(nodeLayout) + : !helper.aboveViewRootByViewPath(viewPath, node) + }, true); var viewChildren = node.viewChildren || []; for (var i = 0, len = viewChildren.length; i < len; i++) { @@ -492,7 +508,7 @@ define(function (require) { clipRect.width, clipRect.height ); - prunning(viewChildren[i], childClipRect); + prunning(viewChildren[i], childClipRect, viewPath); } } diff --git a/test/treemap-disk.html b/test/treemap-disk.html index 61bd31373..d94f9de9d 100644 --- a/test/treemap-disk.html +++ b/test/treemap-disk.html @@ -55,6 +55,15 @@ + + leafDepth: + + + + + + +
@@ -94,6 +103,14 @@ }); } + function leafDepthChange(value) { + chart.setOption({ + series: [{ + leafDepth: value + }] + }); + } + function getLevelOption(colorMapping) { return [ { diff --git a/test/treemap-option.html b/test/treemap-option.html new file mode 100644 index 000000000..7fd7cadbf --- /dev/null +++ b/test/treemap-option.html @@ -0,0 +1,104 @@ + + + + + Option View + + + + + + +
+ + + + + \ No newline at end of file -- GitLab