|
|
|
|
|
/*
|
|
|
* 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.
|
|
|
*/
|
|
|
|
|
|
|
|
|
/**
|
|
|
* AUTO-GENERATED FILE. DO NOT MODIFY.
|
|
|
*/
|
|
|
|
|
|
/*
|
|
|
* Licensed to the Apache Software Foundation (ASF) under one
|
|
|
* or more contributor license agreements. See the NOTICE file
|
|
|
* distributed with this work for additional information
|
|
|
* regarding copyright ownership. The ASF licenses this file
|
|
|
* to you under the Apache License, Version 2.0 (the
|
|
|
* "License"); you may not use this file except in compliance
|
|
|
* with the License. You may obtain a copy of the License at
|
|
|
*
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
*
|
|
|
* Unless required by applicable law or agreed to in writing,
|
|
|
* software distributed under the License is distributed on an
|
|
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
|
* KIND, either express or implied. See the License for the
|
|
|
* specific language governing permissions and limitations
|
|
|
* under the License.
|
|
|
*/
|
|
|
import { __extends } from "tslib";
|
|
|
import { bind, each, indexOf, curry, extend, normalizeCssArray, isFunction } from 'zrender/lib/core/util.js';
|
|
|
import * as graphic from '../../util/graphic.js';
|
|
|
import { getECData } from '../../util/innerStore.js';
|
|
|
import { isHighDownDispatcher, setAsHighDownDispatcher, setDefaultStateProxy, enableHoverFocus, Z2_EMPHASIS_LIFT } from '../../util/states.js';
|
|
|
import DataDiffer from '../../data/DataDiffer.js';
|
|
|
import * as helper from '../helper/treeHelper.js';
|
|
|
import Breadcrumb from './Breadcrumb.js';
|
|
|
import RoamController from '../../component/helper/RoamController.js';
|
|
|
import BoundingRect from 'zrender/lib/core/BoundingRect.js';
|
|
|
import * as matrix from 'zrender/lib/core/matrix.js';
|
|
|
import * as animationUtil from '../../util/animation.js';
|
|
|
import makeStyleMapper from '../../model/mixin/makeStyleMapper.js';
|
|
|
import ChartView from '../../view/Chart.js';
|
|
|
import Displayable from 'zrender/lib/graphic/Displayable.js';
|
|
|
import { makeInner, convertOptionIdName } from '../../util/model.js';
|
|
|
import { windowOpen } from '../../util/format.js';
|
|
|
import { setLabelStyle, getLabelStatesModels } from '../../label/labelStyle.js';
|
|
|
var Group = graphic.Group;
|
|
|
var Rect = graphic.Rect;
|
|
|
var DRAG_THRESHOLD = 3;
|
|
|
var PATH_LABEL_NOAMAL = 'label';
|
|
|
var PATH_UPPERLABEL_NORMAL = 'upperLabel';
|
|
|
// Should larger than emphasis states lift z
|
|
|
var Z2_BASE = Z2_EMPHASIS_LIFT * 10; // Should bigger than every z2.
|
|
|
var Z2_BG = Z2_EMPHASIS_LIFT * 2;
|
|
|
var Z2_CONTENT = Z2_EMPHASIS_LIFT * 3;
|
|
|
var getStateItemStyle = makeStyleMapper([['fill', 'color'],
|
|
|
// `borderColor` and `borderWidth` has been occupied,
|
|
|
// so use `stroke` to indicate the stroke of the rect.
|
|
|
['stroke', 'strokeColor'], ['lineWidth', 'strokeWidth'], ['shadowBlur'], ['shadowOffsetX'], ['shadowOffsetY'], ['shadowColor']
|
|
|
// Option decal is in `DecalObject` but style.decal is in `PatternObject`.
|
|
|
// So do not transfer decal directly.
|
|
|
]);
|
|
|
|
|
|
var getItemStyleNormal = function (model) {
|
|
|
// Normal style props should include emphasis style props.
|
|
|
var itemStyle = getStateItemStyle(model);
|
|
|
// Clear styles set by emphasis.
|
|
|
itemStyle.stroke = itemStyle.fill = itemStyle.lineWidth = null;
|
|
|
return itemStyle;
|
|
|
};
|
|
|
var inner = makeInner();
|
|
|
var TreemapView = /** @class */function (_super) {
|
|
|
__extends(TreemapView, _super);
|
|
|
function TreemapView() {
|
|
|
var _this = _super !== null && _super.apply(this, arguments) || this;
|
|
|
_this.type = TreemapView.type;
|
|
|
_this._state = 'ready';
|
|
|
_this._storage = createStorage();
|
|
|
return _this;
|
|
|
}
|
|
|
/**
|
|
|
* @override
|
|
|
*/
|
|
|
TreemapView.prototype.render = function (seriesModel, ecModel, api, payload) {
|
|
|
var models = ecModel.findComponents({
|
|
|
mainType: 'series',
|
|
|
subType: 'treemap',
|
|
|
query: payload
|
|
|
});
|
|
|
if (indexOf(models, seriesModel) < 0) {
|
|
|
return;
|
|
|
}
|
|
|
this.seriesModel = seriesModel;
|
|
|
this.api = api;
|
|
|
this.ecModel = ecModel;
|
|
|
var types = ['treemapZoomToNode', 'treemapRootToNode'];
|
|
|
var targetInfo = helper.retrieveTargetInfo(payload, types, 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 hasAnimation = seriesModel.get('animation');
|
|
|
var renderResult = this._doRender(containerGroup, seriesModel, reRoot);
|
|
|
hasAnimation && !isInit && (!payloadType || payloadType === 'treemapZoomToNode' || payloadType === 'treemapRootToNode') ? this._doAnimation(containerGroup, renderResult, seriesModel, reRoot) : renderResult.renderFinally();
|
|
|
this._resetController(api);
|
|
|
this._renderBreadcrumb(seriesModel, api, targetInfo);
|
|
|
};
|
|
|
TreemapView.prototype._giveContainerGroup = function (layoutInfo) {
|
|
|
var containerGroup = this._containerGroup;
|
|
|
if (!containerGroup) {
|
|
|
// FIXME
|
|
|
// 加一层containerGroup是为了clip,但是现在clip功能并没有实现。
|
|
|
containerGroup = this._containerGroup = new Group();
|
|
|
this._initEvents(containerGroup);
|
|
|
this.group.add(containerGroup);
|
|
|
}
|
|
|
containerGroup.x = layoutInfo.x;
|
|
|
containerGroup.y = layoutInfo.y;
|
|
|
return containerGroup;
|
|
|
};
|
|
|
TreemapView.prototype._doRender = function (containerGroup, seriesModel, reRoot) {
|
|
|
var thisTree = seriesModel.getData().tree;
|
|
|
var oldTree = this._oldTree;
|
|
|
// Clear last shape records.
|
|
|
var lastsForAnimation = createStorage();
|
|
|
var thisStorage = createStorage();
|
|
|
var oldStorage = this._storage;
|
|
|
var willInvisibleEls = [];
|
|
|
function doRenderNode(thisNode, oldNode, parentGroup, depth) {
|
|
|
return renderNode(seriesModel, thisStorage, oldStorage, reRoot, lastsForAnimation, willInvisibleEls, thisNode, oldNode, parentGroup, depth);
|
|
|
}
|
|
|
// Notice: When thisTree and oldTree are the same tree (see list.cloneShallow),
|
|
|
// the oldTree is actually losted, so we cannot find all of the old graphic
|
|
|
// elements from tree. So we use this strategy: make element storage, move
|
|
|
// from old storage to new storage, clear old storage.
|
|
|
dualTravel(thisTree.root ? [thisTree.root] : [], oldTree && oldTree.root ? [oldTree.root] : [], containerGroup, thisTree === oldTree || !oldTree, 0);
|
|
|
// Process all removing.
|
|
|
var willDeleteEls = clearStorage(oldStorage);
|
|
|
this._oldTree = thisTree;
|
|
|
this._storage = thisStorage;
|
|
|
if (this._controllerHost) {
|
|
|
var _oldRootLayout = this.seriesModel.layoutInfo;
|
|
|
var rootLayout = thisTree.root.getLayout();
|
|
|
if (rootLayout.width === _oldRootLayout.width && rootLayout.height === _oldRootLayout.height) {
|
|
|
this._controllerHost.zoom = 1;
|
|
|
}
|
|
|
}
|
|
|
return {
|
|
|
lastsForAnimation: lastsForAnimation,
|
|
|
willDeleteEls: willDeleteEls,
|
|
|
renderFinally: renderFinally
|
|
|
};
|
|
|
function dualTravel(thisViewChildren, oldViewChildren, parentGroup, sameTree, depth) {
|
|
|
// When 'render' is triggered by action,
|
|
|
// 'this' and 'old' may be the same tree,
|
|
|
// we use rawIndex in that case.
|
|
|
if (sameTree) {
|
|
|
oldViewChildren = thisViewChildren;
|
|
|
each(thisViewChildren, function (child, index) {
|
|
|
!child.isRemoved() && processNode(index, index);
|
|
|
});
|
|
|
}
|
|
|
// Diff hierarchically (diff only in each subtree, but not whole).
|
|
|
// because, consistency of view is important.
|
|
|
else {
|
|
|
new DataDiffer(oldViewChildren, thisViewChildren, getKey, getKey).add(processNode).update(processNode).remove(curry(processNode, null)).execute();
|
|
|
}
|
|
|
function getKey(node) {
|
|
|
// Identify by name or raw index.
|
|
|
return node.getId();
|
|
|
}
|
|
|
function processNode(newIndex, oldIndex) {
|
|
|
var thisNode = newIndex != null ? thisViewChildren[newIndex] : null;
|
|
|
var oldNode = oldIndex != null ? oldViewChildren[oldIndex] : null;
|
|
|
var group = doRenderNode(thisNode, oldNode, parentGroup, depth);
|
|
|
group && dualTravel(thisNode && thisNode.viewChildren || [], oldNode && oldNode.viewChildren || [], group, sameTree, depth + 1);
|
|
|
}
|
|
|
}
|
|
|
function clearStorage(storage) {
|
|
|
var willDeleteEls = createStorage();
|
|
|
storage && each(storage, function (store, storageName) {
|
|
|
var delEls = willDeleteEls[storageName];
|
|
|
each(store, function (el) {
|
|
|
el && (delEls.push(el), inner(el).willDelete = true);
|
|
|
});
|
|
|
});
|
|
|
return willDeleteEls;
|
|
|
}
|
|
|
function renderFinally() {
|
|
|
each(willDeleteEls, function (els) {
|
|
|
each(els, function (el) {
|
|
|
el.parent && el.parent.remove(el);
|
|
|
});
|
|
|
});
|
|
|
each(willInvisibleEls, function (el) {
|
|
|
el.invisible = true;
|
|
|
// Setting invisible is for optimizing, so no need to set dirty,
|
|
|
// just mark as invisible.
|
|
|
el.dirty();
|
|
|
});
|
|
|
}
|
|
|
};
|
|
|
TreemapView.prototype._doAnimation = function (containerGroup, renderResult, seriesModel, reRoot) {
|
|
|
var durationOption = seriesModel.get('animationDurationUpdate');
|
|
|
var easingOption = seriesModel.get('animationEasing');
|
|
|
// TODO: do not support function until necessary.
|
|
|
var duration = (isFunction(durationOption) ? 0 : durationOption) || 0;
|
|
|
var easing = (isFunction(easingOption) ? null : easingOption) || 'cubicOut';
|
|
|
var animationWrap = animationUtil.createWrap();
|
|
|
// Make delete animations.
|
|
|
each(renderResult.willDeleteEls, function (store, storageName) {
|
|
|
each(store, function (el, rawIndex) {
|
|
|
if (el.invisible) {
|
|
|
return;
|
|
|
}
|
|
|
var parent = el.parent; // Always has parent, and parent is nodeGroup.
|
|
|
var target;
|
|
|
var innerStore = inner(parent);
|
|
|
if (reRoot && reRoot.direction === 'drillDown') {
|
|
|
target = parent === reRoot.rootNodeGroup
|
|
|
// This is the content element of view root.
|
|
|
// Only `content` will enter this branch, because
|
|
|
// `background` and `nodeGroup` will not be deleted.
|
|
|
? {
|
|
|
shape: {
|
|
|
x: 0,
|
|
|
y: 0,
|
|
|
width: innerStore.nodeWidth,
|
|
|
height: innerStore.nodeHeight
|
|
|
},
|
|
|
style: {
|
|
|
opacity: 0
|
|
|
}
|
|
|
}
|
|
|
// Others.
|
|
|
: {
|
|
|
style: {
|
|
|
opacity: 0
|
|
|
}
|
|
|
};
|
|
|
} else {
|
|
|
var targetX = 0;
|
|
|
var targetY = 0;
|
|
|
if (!innerStore.willDelete) {
|
|
|
// Let node animate to right-bottom corner, cooperating with fadeout,
|
|
|
// which is appropriate for user understanding.
|
|
|
// Divided by 2 for reRoot rolling up effect.
|
|
|
targetX = innerStore.nodeWidth / 2;
|
|
|
targetY = innerStore.nodeHeight / 2;
|
|
|
}
|
|
|
target = storageName === 'nodeGroup' ? {
|
|
|
x: targetX,
|
|
|
y: targetY,
|
|
|
style: {
|
|
|
opacity: 0
|
|
|
}
|
|
|
} : {
|
|
|
shape: {
|
|
|
x: targetX,
|
|
|
y: targetY,
|
|
|
width: 0,
|
|
|
height: 0
|
|
|
},
|
|
|
style: {
|
|
|
opacity: 0
|
|
|
}
|
|
|
};
|
|
|
}
|
|
|
// TODO: do not support delay until necessary.
|
|
|
target && animationWrap.add(el, target, duration, 0, easing);
|
|
|
});
|
|
|
});
|
|
|
// Make other animations
|
|
|
each(this._storage, function (store, storageName) {
|
|
|
each(store, function (el, rawIndex) {
|
|
|
var last = renderResult.lastsForAnimation[storageName][rawIndex];
|
|
|
var target = {};
|
|
|
if (!last) {
|
|
|
return;
|
|
|
}
|
|
|
if (el instanceof graphic.Group) {
|
|
|
if (last.oldX != null) {
|
|
|
target.x = el.x;
|
|
|
target.y = el.y;
|
|
|
el.x = last.oldX;
|
|
|
el.y = last.oldY;
|
|
|
}
|
|
|
} else {
|
|
|
if (last.oldShape) {
|
|
|
target.shape = extend({}, el.shape);
|
|
|
el.setShape(last.oldShape);
|
|
|
}
|
|
|
if (last.fadein) {
|
|
|
el.setStyle('opacity', 0);
|
|
|
target.style = {
|
|
|
opacity: 1
|
|
|
};
|
|
|
}
|
|
|
// When animation is stopped for succedent animation starting,
|
|
|
// el.style.opacity might not be 1
|
|
|
else if (el.style.opacity !== 1) {
|
|
|
target.style = {
|
|
|
opacity: 1
|
|
|
};
|
|
|
}
|
|
|
}
|
|
|
animationWrap.add(el, target, duration, 0, easing);
|
|
|
});
|
|
|
}, this);
|
|
|
this._state = 'animating';
|
|
|
animationWrap.finished(bind(function () {
|
|
|
this._state = 'ready';
|
|
|
renderResult.renderFinally();
|
|
|
}, this)).start();
|
|
|
};
|
|
|
TreemapView.prototype._resetController = function (api) {
|
|
|
var controller = this._controller;
|
|
|
var controllerHost = this._controllerHost;
|
|
|
if (!controllerHost) {
|
|
|
this._controllerHost = {
|
|
|
target: this.group
|
|
|
};
|
|
|
controllerHost = this._controllerHost;
|
|
|
}
|
|
|
// Init controller.
|
|
|
if (!controller) {
|
|
|
controller = this._controller = new RoamController(api.getZr());
|
|
|
controller.enable(this.seriesModel.get('roam'));
|
|
|
controllerHost.zoomLimit = this.seriesModel.get('scaleLimit');
|
|
|
controllerHost.zoom = this.seriesModel.get('zoom');
|
|
|
controller.on('pan', bind(this._onPan, this));
|
|
|
controller.on('zoom', bind(this._onZoom, this));
|
|
|
}
|
|
|
var rect = new BoundingRect(0, 0, api.getWidth(), api.getHeight());
|
|
|
controller.setPointerChecker(function (e, x, y) {
|
|
|
return rect.contain(x, y);
|
|
|
});
|
|
|
};
|
|
|
TreemapView.prototype._clearController = function () {
|
|
|
var controller = this._controller;
|
|
|
this._controllerHost = null;
|
|
|
if (controller) {
|
|
|
controller.dispose();
|
|
|
controller = null;
|
|
|
}
|
|
|
};
|
|
|
TreemapView.prototype._onPan = function (e) {
|
|
|
if (this._state !== 'animating' && (Math.abs(e.dx) > DRAG_THRESHOLD || Math.abs(e.dy) > DRAG_THRESHOLD)) {
|
|
|
// These param must not be cached.
|
|
|
var root = this.seriesModel.getData().tree.root;
|
|
|
if (!root) {
|
|
|
return;
|
|
|
}
|
|
|
var rootLayout = root.getLayout();
|
|
|
if (!rootLayout) {
|
|
|
return;
|
|
|
}
|
|
|
this.api.dispatchAction({
|
|
|
type: 'treemapMove',
|
|
|
from: this.uid,
|
|
|
seriesId: this.seriesModel.id,
|
|
|
rootRect: {
|
|
|
x: rootLayout.x + e.dx,
|
|
|
y: rootLayout.y + e.dy,
|
|
|
width: rootLayout.width,
|
|
|
height: rootLayout.height
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
};
|
|
|
TreemapView.prototype._onZoom = function (e) {
|
|
|
var mouseX = e.originX;
|
|
|
var mouseY = e.originY;
|
|
|
var zoomDelta = e.scale;
|
|
|
if (this._state !== 'animating') {
|
|
|
// These param must not be cached.
|
|
|
var root = this.seriesModel.getData().tree.root;
|
|
|
if (!root) {
|
|
|
return;
|
|
|
}
|
|
|
var rootLayout = root.getLayout();
|
|
|
if (!rootLayout) {
|
|
|
return;
|
|
|
}
|
|
|
var rect = new BoundingRect(rootLayout.x, rootLayout.y, rootLayout.width, rootLayout.height);
|
|
|
// scaleLimit
|
|
|
var zoomLimit = null;
|
|
|
var _controllerHost = this._controllerHost;
|
|
|
zoomLimit = _controllerHost.zoomLimit;
|
|
|
var newZoom = _controllerHost.zoom = _controllerHost.zoom || 1;
|
|
|
newZoom *= zoomDelta;
|
|
|
if (zoomLimit) {
|
|
|
var zoomMin = zoomLimit.min || 0;
|
|
|
var zoomMax = zoomLimit.max || Infinity;
|
|
|
newZoom = Math.max(Math.min(zoomMax, newZoom), zoomMin);
|
|
|
}
|
|
|
var zoomScale = newZoom / _controllerHost.zoom;
|
|
|
_controllerHost.zoom = newZoom;
|
|
|
var layoutInfo = this.seriesModel.layoutInfo;
|
|
|
// Transform mouse coord from global to containerGroup.
|
|
|
mouseX -= layoutInfo.x;
|
|
|
mouseY -= layoutInfo.y;
|
|
|
// Scale root bounding rect.
|
|
|
var m = matrix.create();
|
|
|
matrix.translate(m, m, [-mouseX, -mouseY]);
|
|
|
matrix.scale(m, m, [zoomScale, zoomScale]);
|
|
|
matrix.translate(m, m, [mouseX, mouseY]);
|
|
|
rect.applyTransform(m);
|
|
|
this.api.dispatchAction({
|
|
|
type: 'treemapRender',
|
|
|
from: this.uid,
|
|
|
seriesId: this.seriesModel.id,
|
|
|
rootRect: {
|
|
|
x: rect.x,
|
|
|
y: rect.y,
|
|
|
width: rect.width,
|
|
|
height: rect.height
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
};
|
|
|
TreemapView.prototype._initEvents = function (containerGroup) {
|
|
|
var _this = this;
|
|
|
containerGroup.on('click', function (e) {
|
|
|
if (_this._state !== 'ready') {
|
|
|
return;
|
|
|
}
|
|
|
var nodeClick = _this.seriesModel.get('nodeClick', true);
|
|
|
if (!nodeClick) {
|
|
|
return;
|
|
|
}
|
|
|
var targetInfo = _this.findTarget(e.offsetX, e.offsetY);
|
|
|
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 itemModel = node.hostTree.data.getItemModel(node.dataIndex);
|
|
|
var link = itemModel.get('link', true);
|
|
|
var linkTarget = itemModel.get('target', true) || 'blank';
|
|
|
link && windowOpen(link, linkTarget);
|
|
|
}
|
|
|
}
|
|
|
}, this);
|
|
|
};
|
|
|
TreemapView.prototype._renderBreadcrumb = function (seriesModel, api, targetInfo) {
|
|
|
var _this = this;
|
|
|
if (!targetInfo) {
|
|
|
targetInfo = seriesModel.get('leafDepth', true) != null ? {
|
|
|
node: seriesModel.getViewRoot()
|
|
|
}
|
|
|
// FIXME
|
|
|
// better way?
|
|
|
// Find breadcrumb tail on center of containerGroup.
|
|
|
: this.findTarget(api.getWidth() / 2, api.getHeight() / 2);
|
|
|
if (!targetInfo) {
|
|
|
targetInfo = {
|
|
|
node: seriesModel.getData().tree.root
|
|
|
};
|
|
|
}
|
|
|
}
|
|
|
(this._breadcrumb || (this._breadcrumb = new Breadcrumb(this.group))).render(seriesModel, api, targetInfo.node, function (node) {
|
|
|
if (_this._state !== 'animating') {
|
|
|
helper.aboveViewRoot(seriesModel.getViewRoot(), node) ? _this._rootToNode({
|
|
|
node: node
|
|
|
}) : _this._zoomToNode({
|
|
|
node: node
|
|
|
});
|
|
|
}
|
|
|
});
|
|
|
};
|
|
|
/**
|
|
|
* @override
|
|
|
*/
|
|
|
TreemapView.prototype.remove = function () {
|
|
|
this._clearController();
|
|
|
this._containerGroup && this._containerGroup.removeAll();
|
|
|
this._storage = createStorage();
|
|
|
this._state = 'ready';
|
|
|
this._breadcrumb && this._breadcrumb.remove();
|
|
|
};
|
|
|
TreemapView.prototype.dispose = function () {
|
|
|
this._clearController();
|
|
|
};
|
|
|
TreemapView.prototype._zoomToNode = function (targetInfo) {
|
|
|
this.api.dispatchAction({
|
|
|
type: 'treemapZoomToNode',
|
|
|
from: this.uid,
|
|
|
seriesId: this.seriesModel.id,
|
|
|
targetNode: targetInfo.node
|
|
|
});
|
|
|
};
|
|
|
TreemapView.prototype._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.
|
|
|
* @param {number} y Global coord y.
|
|
|
* @return {Object} info If not found, return undefined;
|
|
|
* @return {number} info.node Target node.
|
|
|
* @return {number} info.offsetX x refer to target node.
|
|
|
* @return {number} info.offsetY y refer to target node.
|
|
|
*/
|
|
|
TreemapView.prototype.findTarget = function (x, y) {
|
|
|
var targetInfo;
|
|
|
var viewRoot = this.seriesModel.getViewRoot();
|
|
|
viewRoot.eachNode({
|
|
|
attr: 'viewChildren',
|
|
|
order: 'preorder'
|
|
|
}, function (node) {
|
|
|
var bgEl = this._storage.background[node.getRawIndex()];
|
|
|
// If invisible, there might be no element.
|
|
|
if (bgEl) {
|
|
|
var point = bgEl.transformCoordToLocal(x, y);
|
|
|
var shape = bgEl.shape;
|
|
|
// For performance consideration, don't use 'getBoundingRect'.
|
|
|
if (shape.x <= point[0] && point[0] <= shape.x + shape.width && shape.y <= point[1] && point[1] <= shape.y + shape.height) {
|
|
|
targetInfo = {
|
|
|
node: node,
|
|
|
offsetX: point[0],
|
|
|
offsetY: point[1]
|
|
|
};
|
|
|
} else {
|
|
|
return false; // Suppress visit subtree.
|
|
|
}
|
|
|
}
|
|
|
}, this);
|
|
|
return targetInfo;
|
|
|
};
|
|
|
TreemapView.type = 'treemap';
|
|
|
return TreemapView;
|
|
|
}(ChartView);
|
|
|
/**
|
|
|
* @inner
|
|
|
*/
|
|
|
function createStorage() {
|
|
|
return {
|
|
|
nodeGroup: [],
|
|
|
background: [],
|
|
|
content: []
|
|
|
};
|
|
|
}
|
|
|
/**
|
|
|
* @inner
|
|
|
* @return Return undefined means do not travel further.
|
|
|
*/
|
|
|
function renderNode(seriesModel, thisStorage, oldStorage, reRoot, lastsForAnimation, willInvisibleEls, thisNode, oldNode, parentGroup, depth) {
|
|
|
// Whether under viewRoot.
|
|
|
if (!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;
|
|
|
}
|
|
|
// -------------------------------------------------------------------
|
|
|
// Start of closure variables available in "Procedures in renderNode".
|
|
|
var thisLayout = thisNode.getLayout();
|
|
|
var data = seriesModel.getData();
|
|
|
var nodeModel = thisNode.getModel();
|
|
|
// Only for enabling highlight/downplay. Clear firstly.
|
|
|
// Because some node will not be rendered.
|
|
|
data.setItemGraphicEl(thisNode.dataIndex, null);
|
|
|
if (!thisLayout || !thisLayout.isInView) {
|
|
|
return;
|
|
|
}
|
|
|
var thisWidth = thisLayout.width;
|
|
|
var thisHeight = thisLayout.height;
|
|
|
var borderWidth = thisLayout.borderWidth;
|
|
|
var thisInvisible = thisLayout.invisible;
|
|
|
var thisRawIndex = thisNode.getRawIndex();
|
|
|
var oldRawIndex = oldNode && oldNode.getRawIndex();
|
|
|
var thisViewChildren = thisNode.viewChildren;
|
|
|
var upperHeight = thisLayout.upperHeight;
|
|
|
var isParent = thisViewChildren && thisViewChildren.length;
|
|
|
var itemStyleNormalModel = nodeModel.getModel('itemStyle');
|
|
|
var itemStyleEmphasisModel = nodeModel.getModel(['emphasis', 'itemStyle']);
|
|
|
var itemStyleBlurModel = nodeModel.getModel(['blur', 'itemStyle']);
|
|
|
var itemStyleSelectModel = nodeModel.getModel(['select', 'itemStyle']);
|
|
|
var borderRadius = itemStyleNormalModel.get('borderRadius') || 0;
|
|
|
// End of closure ariables available in "Procedures in renderNode".
|
|
|
// -----------------------------------------------------------------
|
|
|
// Node group
|
|
|
var group = giveGraphic('nodeGroup', Group);
|
|
|
if (!group) {
|
|
|
return;
|
|
|
}
|
|
|
parentGroup.add(group);
|
|
|
// x,y are not set when el is above view root.
|
|
|
group.x = thisLayout.x || 0;
|
|
|
group.y = thisLayout.y || 0;
|
|
|
group.markRedraw();
|
|
|
inner(group).nodeWidth = thisWidth;
|
|
|
inner(group).nodeHeight = thisHeight;
|
|
|
if (thisLayout.isAboveViewRoot) {
|
|
|
return group;
|
|
|
}
|
|
|
// Background
|
|
|
var bg = giveGraphic('background', Rect, depth, Z2_BG);
|
|
|
bg && renderBackground(group, bg, isParent && thisLayout.upperLabelHeight);
|
|
|
var emphasisModel = nodeModel.getModel('emphasis');
|
|
|
var focus = emphasisModel.get('focus');
|
|
|
var blurScope = emphasisModel.get('blurScope');
|
|
|
var isDisabled = emphasisModel.get('disabled');
|
|
|
var focusOrIndices = focus === 'ancestor' ? thisNode.getAncestorsIndices() : focus === 'descendant' ? thisNode.getDescendantIndices() : focus;
|
|
|
// No children, render content.
|
|
|
if (isParent) {
|
|
|
// Because of the implementation about "traverse" in graphic hover style, we
|
|
|
// can not set hover listener on the "group" of non-leaf node. Otherwise the
|
|
|
// hover event from the descendents will be listenered.
|
|
|
if (isHighDownDispatcher(group)) {
|
|
|
setAsHighDownDispatcher(group, false);
|
|
|
}
|
|
|
if (bg) {
|
|
|
setAsHighDownDispatcher(bg, !isDisabled);
|
|
|
// Only for enabling highlight/downplay.
|
|
|
data.setItemGraphicEl(thisNode.dataIndex, bg);
|
|
|
enableHoverFocus(bg, focusOrIndices, blurScope);
|
|
|
}
|
|
|
} else {
|
|
|
var content = giveGraphic('content', Rect, depth, Z2_CONTENT);
|
|
|
content && renderContent(group, content);
|
|
|
bg.disableMorphing = true;
|
|
|
if (bg && isHighDownDispatcher(bg)) {
|
|
|
setAsHighDownDispatcher(bg, false);
|
|
|
}
|
|
|
setAsHighDownDispatcher(group, !isDisabled);
|
|
|
// Only for enabling highlight/downplay.
|
|
|
data.setItemGraphicEl(thisNode.dataIndex, group);
|
|
|
enableHoverFocus(group, focusOrIndices, blurScope);
|
|
|
}
|
|
|
return group;
|
|
|
// ----------------------------
|
|
|
// | Procedures in renderNode |
|
|
|
// ----------------------------
|
|
|
function renderBackground(group, bg, useUpperLabel) {
|
|
|
var ecData = getECData(bg);
|
|
|
// For tooltip.
|
|
|
ecData.dataIndex = thisNode.dataIndex;
|
|
|
ecData.seriesIndex = seriesModel.seriesIndex;
|
|
|
bg.setShape({
|
|
|
x: 0,
|
|
|
y: 0,
|
|
|
width: thisWidth,
|
|
|
height: thisHeight,
|
|
|
r: borderRadius
|
|
|
});
|
|
|
if (thisInvisible) {
|
|
|
// If invisible, do not set visual, otherwise the element will
|
|
|
// change immediately before animation. We think it is OK to
|
|
|
// remain its origin color when moving out of the view window.
|
|
|
processInvisible(bg);
|
|
|
} else {
|
|
|
bg.invisible = false;
|
|
|
var style = thisNode.getVisual('style');
|
|
|
var visualBorderColor = style.stroke;
|
|
|
var normalStyle = getItemStyleNormal(itemStyleNormalModel);
|
|
|
normalStyle.fill = visualBorderColor;
|
|
|
var emphasisStyle = getStateItemStyle(itemStyleEmphasisModel);
|
|
|
emphasisStyle.fill = itemStyleEmphasisModel.get('borderColor');
|
|
|
var blurStyle = getStateItemStyle(itemStyleBlurModel);
|
|
|
blurStyle.fill = itemStyleBlurModel.get('borderColor');
|
|
|
var selectStyle = getStateItemStyle(itemStyleSelectModel);
|
|
|
selectStyle.fill = itemStyleSelectModel.get('borderColor');
|
|
|
if (useUpperLabel) {
|
|
|
var upperLabelWidth = thisWidth - 2 * borderWidth;
|
|
|
prepareText(
|
|
|
// PENDING: convert ZRColor to ColorString for text.
|
|
|
bg, visualBorderColor, style.opacity, {
|
|
|
x: borderWidth,
|
|
|
y: 0,
|
|
|
width: upperLabelWidth,
|
|
|
height: upperHeight
|
|
|
});
|
|
|
}
|
|
|
// For old bg.
|
|
|
else {
|
|
|
bg.removeTextContent();
|
|
|
}
|
|
|
bg.setStyle(normalStyle);
|
|
|
bg.ensureState('emphasis').style = emphasisStyle;
|
|
|
bg.ensureState('blur').style = blurStyle;
|
|
|
bg.ensureState('select').style = selectStyle;
|
|
|
setDefaultStateProxy(bg);
|
|
|
}
|
|
|
group.add(bg);
|
|
|
}
|
|
|
function renderContent(group, content) {
|
|
|
var ecData = getECData(content);
|
|
|
// For tooltip.
|
|
|
ecData.dataIndex = thisNode.dataIndex;
|
|
|
ecData.seriesIndex = seriesModel.seriesIndex;
|
|
|
var contentWidth = Math.max(thisWidth - 2 * borderWidth, 0);
|
|
|
var contentHeight = Math.max(thisHeight - 2 * borderWidth, 0);
|
|
|
content.culling = true;
|
|
|
content.setShape({
|
|
|
x: borderWidth,
|
|
|
y: borderWidth,
|
|
|
width: contentWidth,
|
|
|
height: contentHeight,
|
|
|
r: borderRadius
|
|
|
});
|
|
|
if (thisInvisible) {
|
|
|
// If invisible, do not set visual, otherwise the element will
|
|
|
// change immediately before animation. We think it is OK to
|
|
|
// remain its origin color when moving out of the view window.
|
|
|
processInvisible(content);
|
|
|
} else {
|
|
|
content.invisible = false;
|
|
|
var nodeStyle = thisNode.getVisual('style');
|
|
|
var visualColor = nodeStyle.fill;
|
|
|
var normalStyle = getItemStyleNormal(itemStyleNormalModel);
|
|
|
normalStyle.fill = visualColor;
|
|
|
normalStyle.decal = nodeStyle.decal;
|
|
|
var emphasisStyle = getStateItemStyle(itemStyleEmphasisModel);
|
|
|
var blurStyle = getStateItemStyle(itemStyleBlurModel);
|
|
|
var selectStyle = getStateItemStyle(itemStyleSelectModel);
|
|
|
// PENDING: convert ZRColor to ColorString for text.
|
|
|
prepareText(content, visualColor, nodeStyle.opacity, null);
|
|
|
content.setStyle(normalStyle);
|
|
|
content.ensureState('emphasis').style = emphasisStyle;
|
|
|
content.ensureState('blur').style = blurStyle;
|
|
|
content.ensureState('select').style = selectStyle;
|
|
|
setDefaultStateProxy(content);
|
|
|
}
|
|
|
group.add(content);
|
|
|
}
|
|
|
function processInvisible(element) {
|
|
|
// Delay invisible setting utill animation finished,
|
|
|
// avoid element vanish suddenly before animation.
|
|
|
!element.invisible && willInvisibleEls.push(element);
|
|
|
}
|
|
|
function prepareText(rectEl, visualColor, visualOpacity,
|
|
|
// Can be null/undefined
|
|
|
upperLabelRect) {
|
|
|
var normalLabelModel = nodeModel.getModel(upperLabelRect ? PATH_UPPERLABEL_NORMAL : PATH_LABEL_NOAMAL);
|
|
|
var defaultText = convertOptionIdName(nodeModel.get('name'), null);
|
|
|
var isShow = normalLabelModel.getShallow('show');
|
|
|
setLabelStyle(rectEl, getLabelStatesModels(nodeModel, upperLabelRect ? PATH_UPPERLABEL_NORMAL : PATH_LABEL_NOAMAL), {
|
|
|
defaultText: isShow ? defaultText : null,
|
|
|
inheritColor: visualColor,
|
|
|
defaultOpacity: visualOpacity,
|
|
|
labelFetcher: seriesModel,
|
|
|
labelDataIndex: thisNode.dataIndex
|
|
|
});
|
|
|
var textEl = rectEl.getTextContent();
|
|
|
if (!textEl) {
|
|
|
return;
|
|
|
}
|
|
|
var textStyle = textEl.style;
|
|
|
var textPadding = normalizeCssArray(textStyle.padding || 0);
|
|
|
if (upperLabelRect) {
|
|
|
rectEl.setTextConfig({
|
|
|
layoutRect: upperLabelRect
|
|
|
});
|
|
|
textEl.disableLabelLayout = true;
|
|
|
}
|
|
|
textEl.beforeUpdate = function () {
|
|
|
var width = Math.max((upperLabelRect ? upperLabelRect.width : rectEl.shape.width) - textPadding[1] - textPadding[3], 0);
|
|
|
var height = Math.max((upperLabelRect ? upperLabelRect.height : rectEl.shape.height) - textPadding[0] - textPadding[2], 0);
|
|
|
if (textStyle.width !== width || textStyle.height !== height) {
|
|
|
textEl.setStyle({
|
|
|
width: width,
|
|
|
height: height
|
|
|
});
|
|
|
}
|
|
|
};
|
|
|
textStyle.truncateMinChar = 2;
|
|
|
textStyle.lineOverflow = 'truncate';
|
|
|
addDrillDownIcon(textStyle, upperLabelRect, thisLayout);
|
|
|
var textEmphasisState = textEl.getState('emphasis');
|
|
|
addDrillDownIcon(textEmphasisState ? textEmphasisState.style : null, upperLabelRect, thisLayout);
|
|
|
}
|
|
|
function addDrillDownIcon(style, upperLabelRect, thisLayout) {
|
|
|
var text = style ? style.text : null;
|
|
|
if (!upperLabelRect && thisLayout.isLeafRoot && text != null) {
|
|
|
var iconChar = seriesModel.get('drillDownIcon', true);
|
|
|
style.text = iconChar ? iconChar + ' ' + text : text;
|
|
|
}
|
|
|
}
|
|
|
function giveGraphic(storageName, Ctor, depth, z) {
|
|
|
var element = oldRawIndex != null && oldStorage[storageName][oldRawIndex];
|
|
|
var lasts = lastsForAnimation[storageName];
|
|
|
if (element) {
|
|
|
// Remove from oldStorage
|
|
|
oldStorage[storageName][oldRawIndex] = null;
|
|
|
prepareAnimationWhenHasOld(lasts, element);
|
|
|
}
|
|
|
// If invisible and no old element, do not create new element (for optimizing).
|
|
|
else if (!thisInvisible) {
|
|
|
element = new Ctor();
|
|
|
if (element instanceof Displayable) {
|
|
|
element.z2 = calculateZ2(depth, z);
|
|
|
}
|
|
|
prepareAnimationWhenNoOld(lasts, element);
|
|
|
}
|
|
|
// Set to thisStorage
|
|
|
return thisStorage[storageName][thisRawIndex] = element;
|
|
|
}
|
|
|
function prepareAnimationWhenHasOld(lasts, element) {
|
|
|
var lastCfg = lasts[thisRawIndex] = {};
|
|
|
if (element instanceof Group) {
|
|
|
lastCfg.oldX = element.x;
|
|
|
lastCfg.oldY = element.y;
|
|
|
} else {
|
|
|
lastCfg.oldShape = extend({}, element.shape);
|
|
|
}
|
|
|
}
|
|
|
// If a element is new, we need to find the animation start point carefully,
|
|
|
// otherwise it will looks strange when 'zoomToNode'.
|
|
|
function prepareAnimationWhenNoOld(lasts, element) {
|
|
|
var lastCfg = lasts[thisRawIndex] = {};
|
|
|
var parentNode = thisNode.parentNode;
|
|
|
var isGroup = element instanceof graphic.Group;
|
|
|
if (parentNode && (!reRoot || reRoot.direction === 'drillDown')) {
|
|
|
var parentOldX = 0;
|
|
|
var parentOldY = 0;
|
|
|
// New nodes appear from right-bottom corner in 'zoomToNode' animation.
|
|
|
// For convenience, get old bounding rect from background.
|
|
|
var parentOldBg = lastsForAnimation.background[parentNode.getRawIndex()];
|
|
|
if (!reRoot && parentOldBg && parentOldBg.oldShape) {
|
|
|
parentOldX = parentOldBg.oldShape.width;
|
|
|
parentOldY = parentOldBg.oldShape.height;
|
|
|
}
|
|
|
// When no parent old shape found, its parent is new too,
|
|
|
// so we can just use {x:0, y:0}.
|
|
|
if (isGroup) {
|
|
|
lastCfg.oldX = 0;
|
|
|
lastCfg.oldY = parentOldY;
|
|
|
} else {
|
|
|
lastCfg.oldShape = {
|
|
|
x: parentOldX,
|
|
|
y: parentOldY,
|
|
|
width: 0,
|
|
|
height: 0
|
|
|
};
|
|
|
}
|
|
|
}
|
|
|
// Fade in, user can be aware that these nodes are new.
|
|
|
lastCfg.fadein = !isGroup;
|
|
|
}
|
|
|
}
|
|
|
// We cannot set all background with the same z, because the behaviour of
|
|
|
// drill down and roll up differ background creation sequence from tree
|
|
|
// hierarchy sequence, which cause lower background elements to overlap
|
|
|
// upper ones. So we calculate z based on depth.
|
|
|
// Moreover, we try to shrink down z interval to [0, 1] to avoid that
|
|
|
// treemap with large z overlaps other components.
|
|
|
function calculateZ2(depth, z2InLevel) {
|
|
|
return depth * Z2_BASE + z2InLevel;
|
|
|
}
|
|
|
export default TreemapView; |