提交 9f0adf80 编写于 作者: P pah100

treemap zoom and drag

上级 7923d5a9
......@@ -3,17 +3,11 @@
*/
define(function(require) {
// var zrUtil = require('zrender/core/util');
var echarts = require('../../echarts');
// var modelUtil = require('../../util/model');
var actionInfo = {
type: 'zoomToNode',
update: 'updateView'
};
var noop = function () {}
echarts.registerAction(actionInfo, function (payload, ecModel) {
// do nothing
});
echarts.registerAction({type: 'treemapZoomToNode', update: 'updateView'}, noop);
echarts.registerAction({type: 'treemapRender', update: 'updateView'}, noop);
});
\ No newline at end of file
......@@ -32,27 +32,32 @@ define(function (require) {
return;
}
// Set container size
var size = seriesModel.get('size') || []; // Compatible with ec2.
var containerSize = seriesModel.setContainerSize([
parsePercent(
retrieveValue(seriesModel.get('width'), size[0]),
ecWidth
),
parsePercent(
retrieveValue(seriesModel.get('height'), size[1]),
ecHeight
)
]);
// FIXME
// 暂使用 ecWidth ecHeight 作为可视区大小
var estimatedSize = estimateRootSize(
helper.retrieveTargetInfo(payload, seriesModel),
seriesModel, ecWidth, ecHeight
);
// 暂使用 ecWidth ecHeight 作为可视区大小,不进行clip。
var payloadType = payload && payload.type;
var payloadSize = payloadType === 'treemapZoomToNode'
? estimateRootSize(payload, seriesModel, ecWidth, ecHeight)
: payloadType === 'treemapRender'
? [payload.viewRect.width, payload.viewRect.height]
: null;
var size = seriesModel.get('size') || []; // Compatible with ec2.
var options = {
squareRatio: seriesModel.get('squareRatio'),
sort: seriesModel.get('sort'),
rootSize: estimatedSize || [
parsePercent(
retrieveValue(seriesModel.get('width'), size[0]),
ecWidth
),
parsePercent(
retrieveValue(seriesModel.get('height'), size[1]),
ecHeight
)
]
rootSize: payloadSize || containerSize.slice()
};
this.squarify(seriesModel.getViewRoot(), options);
......@@ -282,7 +287,8 @@ define(function (require) {
rect[wh[idx1WhenH]] -= rowOtherLength;
}
function estimateRootSize(targetInfo, seriesModel, viewWidth, viewHeight) {
function estimateRootSize(payload, seriesModel, viewWidth, viewHeight) {
var targetInfo = helper.retrieveTargetNodeInfo(payload, seriesModel);
// If targetInfo.node exists, we zoom to the node,
// so estimate whold width and heigth by target node.
var currNode = (targetInfo || {}).node;
......
......@@ -26,12 +26,12 @@ define(function(require) {
squareRatio: 0.5 * (1 + Math.sqrt(5)), // golden ratio
root: '',
colorDimension: 'value', // 默认第一个维度。
zoomStep: 10, // 0表示不zoom。
zoomToNodeRatio: 0.2 * 0.2, // zoom to node时 node占可视区域的面积比例。
roam: true,
breadcrumb: {
show: true,
height: 22,
x: 10,
x: 'center',
y: 'bottom',
emptyItemWidth: 25, // 空节点宽度
itemStyle: {
......@@ -46,7 +46,7 @@ define(function(require) {
textStyle: {
color: '#fff',
fontFamily: 'Arial',
fontSize: 13,
fontSize: 12,
fontWeight: 'normal'
}
},
......@@ -62,7 +62,7 @@ define(function(require) {
y: 12,
textStyle: {
align: 'left',
color: '#000',
color: '#fff',
fontFamily: 'Arial',
fontSize: 13,
fontStyle: 'normal',
......@@ -119,6 +119,18 @@ define(function(require) {
var optionRoot = this.option.root;
var treeRoot = this.getData().tree.root;
return optionRoot && treeRoot.getNodeByName(optionRoot) || treeRoot;
},
/**
* @public
* @param {Array.<number>} size [width, height]
*/
setContainerSize: function (size) {
/**
* @readOnly
* @type {Array.<number>}
*/
return this.containerSize = size.slice();
}
});
......
......@@ -7,9 +7,17 @@
var modelUtil = require('../../util/model');
var helper = require('./helper');
var Breadcrumb = require('./Breadcrumb');
var RoamController = require('../../component/helper/RoamController');
var BoundingRect = require('zrender/core/BoundingRect');
var matrix = require('zrender/core/matrix');
var bind = zrUtil.bind;
var Group = graphic.Group;
var Rect = graphic.Rect;
var ANIMATION_DURATION = 700;
var EASING = 'cubicOut';
var DRAG_THRESHOLD = 3;
return require('../../echarts').extendChartView({
type: 'treemap',
......@@ -17,7 +25,7 @@
/**
* @override
*/
init: function () {
init: function (o, api) {
/**
* @private
......@@ -31,6 +39,12 @@
*/
this._storage;
/**
* @private
* @type {Object.<string, Array.<module:zrender/container/Group>>}
*/
this._lastShapes;
/**
* @private
* @type {module:echarts/data/Tree}
......@@ -42,6 +56,18 @@
* @type {module:echarts/chart/treemap/Breadcrumb}
*/
this._breadcrumb;
/**
* @private
* @type {module:echarts/component/helper/RoamController}
*/
this._controller;
/**
* 'ready', 'dragging', 'animating'
* @private
*/
this._state = 'ready';
},
/**
......@@ -54,36 +80,49 @@
this.seriesModel = seriesModel;
this.api = api;
this.ecModel = ecModel;
var thisTree = seriesModel.getData().tree;
var containerGroup = this._containerGroup;
var lastContainerPosition;
var containerSize = seriesModel.containerSize;
if (!containerGroup) {
containerGroup = this._containerGroup = new Group({
onclick: zrUtil.bind(this._onClick, this)
});
containerGroup = this._containerGroup = new Group();
this._initEvents(containerGroup);
this.group.add(containerGroup);
}
else {
// First rendering do not animate.
lastContainerPosition = containerGroup.position.slice();
}
this._doRender(thisTree, containerGroup, seriesModel);
layout.positionGroup(
containerGroup,
var positionInfo = layout.parsePositionInfo(
{
x: seriesModel.get('x'),
y: seriesModel.get('y'),
x2: seriesModel.get('x2'),
y2: seriesModel.get('y2')
y2: seriesModel.get('y2'),
width: containerSize[0],
height: containerSize[1]
},
{
width: api.getWidth(),
height: api.getHeight()
}
);
containerGroup.position = [positionInfo.x, positionInfo.y];
var targetInfo = helper.retrieveTargetInfo(payload, seriesModel);
this._doRender(thisTree, containerGroup, seriesModel);
var targetInfo = helper.retrieveTargetNodeInfo(payload, seriesModel);
var viewRect = payload && payload.type === 'treemapRender' && payload.viewRect;
this._positionRoot(containerGroup, positionInfo, thisTree.root, viewRect, targetInfo);
this._positionRoot(containerGroup, thisTree.root, targetInfo);
this._doAnimation(payload, containerGroup, lastContainerPosition);
this._initController(positionInfo, thisTree.root, seriesModel, api);
this._renderBreadcrumb(seriesModel, api, targetInfo);
},
......@@ -94,9 +133,11 @@
_doRender: function (thisTree, containerGroup, seriesModel) {
var oldTree = this._oldTree;
// Clear last shape records.
this._lastShapes = {nodeGroup: [], background: [], content: []};
var thisStorage = {nodeGroup: [], background: [], content: []};
var oldStorage = this._storage;
var renderNode = zrUtil.bind(this._renderNode, this);
var renderNode = bind(this._renderNode, this);
var viewRoot = seriesModel.getViewRoot();
dualTravel(
......@@ -113,6 +154,7 @@
this._oldTree = thisTree;
this._storage = thisStorage;
function dualTravel(thisViewChildren, oldViewChildren, parentGroup, sameTree, inView) {
// When 'render' is triggered by action,
// 'this' and 'old' may be the same tree,
......@@ -176,6 +218,7 @@
_renderNode: function (thisNode, oldNode, parentGroup, thisStorage, oldStorage) {
var thisRawIndex = thisNode && thisNode.getRawIndex();
var oldRawIndex = oldNode && oldNode.getRawIndex();
var lastShapes = this._lastShapes;
if (!thisNode) {
return;
......@@ -185,18 +228,19 @@
var thisWidth = layout.width;
var thisHeight = layout.height;
var group = giveGraphic('nodeGroup', Group);
group.position = [layout.x, layout.y];
// Node group
var group = giveGraphic('nodeGroup', Group, 'position');
parentGroup.add(group);
group.position = [layout.x, layout.y];
var itemStyleModel = thisNode.getModel('itemStyle.normal');
var borderColor = itemStyleModel.get('borderColor') || itemStyleModel.get('gapColor');
// Background
var background = giveGraphic('background', Rect);
background.setShape({x: 0, y: 0, width: thisWidth, height: thisHeight});
background.setStyle({fill: borderColor});
group.add(background);
var bg = giveGraphic('background', Rect, 'shape');
bg.setShape({x: 0, y: 0, width: thisWidth, height: thisHeight});
bg.setStyle({fill: borderColor});
group.add(bg);
var thisViewChildren = thisNode.viewChildren;
......@@ -204,30 +248,48 @@
if (!thisViewChildren || !thisViewChildren.length) {
var borderWidth = layout.borderWidth;
var content = giveGraphic('content', Rect);
var content = giveGraphic('content', Rect, 'shape');
var contentWidth = Math.max(thisWidth - 2 * borderWidth, 0);
var contentHeight = Math.max(thisHeight - 2 * borderWidth, 0);
var textStyleModel = thisNode.getModel('label.normal.textStyle');
var text = thisNode.getModel().get('name');
var textRect = textStyleModel.getTextRect(text);
if (textRect.width > contentWidth || textRect.height > contentHeight) {
text = '';
}
content.setShape({
x: borderWidth,
y: borderWidth,
width: Math.max(thisWidth - 2 * borderWidth, 0),
height: Math.max(thisHeight - 2 * borderWidth, 0)
width: contentWidth,
height: contentHeight
});
content.setStyle({
fill: thisNode.getVisual('color', true)
fill: thisNode.getVisual('color', true),
text: text,
textFill: textStyleModel.get('color'),
textAlign: textStyleModel.get('align'),
textFont: textStyleModel.getFont()
});
group.add(content);
}
return group;
function giveGraphic(storage, Ctor) {
function giveGraphic(storage, Ctor, type) {
var shape = oldRawIndex != null && oldStorage && oldStorage[storage][oldRawIndex];
if (shape) {
// Remove from oldStorage
oldStorage && (oldStorage[storage][oldRawIndex] = null);
var lastCfg = lastShapes[storage][thisRawIndex] = {};
lastCfg[type] = type === 'position'
? shape.position.slice() : zrUtil.extend({}, shape.shape);
}
else {
// FIXME
// 太多函数?
shape = new Ctor();
}
......@@ -236,6 +298,164 @@
}
},
/**
* @private
*/
_doAnimation: function (payload, containerGroup, lastContainerPosition) {
if (!this.ecModel.get('animation')
|| (payload && payload.type === 'treemapRender')
) {
return;
}
var lastShapes = this._lastShapes;
var animationCount = 0;
var containerGroup = this._containerGroup;
var that = this;
zrUtil.each(this._storage, function (shapes, key) {
zrUtil.each(shapes, function (shape, index) {
var last = lastShapes[key][index];
if (!last) {
return;
}
if (last.position) {
var target = shape.position.slice();
shape.position = last.position;
shape.animateTo({position: target}, ANIMATION_DURATION, EASING, done);
animationCount++;
}
else if (last.shape) {
var target = zrUtil.extend({}, shape.shape);
shape.setShape(last.shape);
shape.animateTo({shape: target}, ANIMATION_DURATION, EASING, done);
animationCount++;
}
});
});
if (lastContainerPosition) {
var target = containerGroup.position.slice();
containerGroup.position = lastContainerPosition;
containerGroup.animateTo({position: target}, ANIMATION_DURATION, EASING, done);
animationCount++;
}
if (animationCount) {
this._state = 'animating';
}
function done() {
animationCount--;
if (!animationCount) {
that._state = 'ready';
}
}
},
/**
* @private
*/
_initController: function (positionInfo, root, seriesModel, api) {
var controller = this._controller;
var nodeGroups = this._storage.nodeGroup;
var rootGroup = nodeGroups[root.getRawIndex()];
var containerGroup = this._containerGroup;
// Init controller.
if (!controller) {
controller = this._controller = new RoamController(api.getZr());
controller.on('pan', bind(onPan, this));
controller.on('zoom', bind(onZoom, this));
controller.rect = new BoundingRect(0, 0, api.getWidth(), api.getHeight());
}
if (!seriesModel.get('roam')) {
controller.off('pan').off('zoom');
this._controller = null;
return;
}
function onPan(dx, dy) {
if (this._state !== 'animating'
&& (Math.abs(dx) > DRAG_THRESHOLD || Math.abs(dy) > DRAG_THRESHOLD)
) {
// FIXME
// 找个好点的方法?
this._state = 'dragging';
var pos = rootGroup.position;
pos[0] += dx;
pos[1] += dy;
rootGroup.dirty();
// Update breadcrumb when drag move.
this._renderBreadcrumb(seriesModel, api);
}
}
function onZoom(scale, mouseX, mouseY) {
if (this._state !== 'animating' && this._state !== 'dragging') {
// var rect = this.group.getBoundingRect([containerGroup]);
var rect = containerGroup.getBoundingRect();
mouseX -= positionInfo.x;
mouseY -= positionInfo.y;
// Recalculate bounding rect.
var m = matrix.create();
matrix.translate(m, m, [-mouseX, -mouseY]);
matrix.scale(m, m, [scale, scale]);
matrix.translate(m, m, [mouseX, mouseY]);
rect.applyTransform(m);
this.api.dispatch({
type: 'treemapRender',
from: this.uid,
seriesId: this.seriesModel.uid,
viewRect: {
x: rect.x, y: rect.y, width: rect.width, height: rect.height
}
});
}
}
},
/**
* @private
*/
_initEvents: function (containerGroup, clickCallback) {
// FIXME
// 不用click以及silent的原因是,animate时视图设置silent true来避免click生效,
// 但是animate中,按下鼠标,animate结束后(silent设回为false)松开鼠标,
// 还是会触发click,期望是不触发。
var maybeClick = false;
var containerGroup = this._containerGroup;
var that = this;
containerGroup.on('mousedown', function (e) {
if (that._state === 'ready') {
maybeClick = true;
}
});
containerGroup.on('mouseup', function (e) {
if (that._state !== 'ready') {
that._state === 'dragging' && (that._state = 'ready');
return;
}
if (maybeClick) {
maybeClick = false;
onClick(e);
}
});
function onClick(e) {
var targetInfo = that.findTarget(e.offsetX, e.offsetY);
if (targetInfo) {
that._zoomToNode(targetInfo);
}
}
},
/**
* @private
*/
......@@ -270,23 +490,12 @@
this._breadcrumb && this._breadcrumb.remove();
},
/**
* @private
*/
_onClick: function (e) {
var targetInfo = this.findTarget(e.offsetX, e.offsetY);
if (targetInfo) {
this._zoomToNode(targetInfo);
}
},
/**
* @private
*/
_zoomToNode: function (targetInfo) {
this.api.dispatch({
type: 'zoomToNode',
type: 'treemapZoomToNode',
from: this.uid,
seriesId: this.seriesModel.uid,
targetInfo: targetInfo
......@@ -323,22 +532,25 @@
/**
* @private
*/
_positionRoot: function(containerGroup, root, targetInfo) {
_positionRoot: function(containerGroup, positionInfo, root, viewRect, targetInfo) {
var nodeGroups = this._storage.nodeGroup;
var rootGroup = nodeGroups[root.getRawIndex()];
if (viewRect) {
rootGroup.position = [viewRect.x, viewRect.y];
return;
}
if (!targetInfo) {
rootGroup.position = [0, 0];
return;
}
// If targetInfo is fetched by 'retrieveTargetInfo',
// If targetInfo is fetched by 'retrieveTargetNodeInfo',
// old tree and new tree are the same tree,
// so we can use raw index of targetInfo.node to find shape from storage.
var targetNode = targetInfo.node;
var containerRect = containerGroup.getBoundingRect();
var targetGroup = nodeGroups[targetNode.getRawIndex()];
......@@ -351,10 +563,9 @@
var targetCenter = modelUtil.transformCoordToAncestor(
[targetRect.width / 2, targetRect.height / 2], targetGroup, containerGroup
);
rootGroup.position = [
containerRect.width / 2 - targetCenter[0],
containerRect.height / 2 - targetCenter[1]
positionInfo.width / 2 - targetCenter[0],
positionInfo.height / 2 - targetCenter[1]
];
}
......
......@@ -6,11 +6,11 @@ define(function (require) {
return payload && payload.seriesId && seriesModel.uid !== payload.seriesId;
},
retrieveTargetInfo: function (payload, seriesModel) {
retrieveTargetNodeInfo: function (payload, seriesModel) {
if (payload && payload.seriesId && seriesModel.uid !== payload.seriesId) {
return;
}
if (!payload || payload.type !== 'zoomToNode') {
if (!payload || payload.type !== 'treemapZoomToNode') {
return;
}
......
define({
define(function (require) {
getFont: function () {
return [
this.get('fontStyle'),
this.get('fontWeight'),
(this.get('fontSize') || 12) + 'px',
this.get('fontFamily') || 'sans-serif'
].join(' ');
},
return {
getFont: function () {
return [
this.get('fontStyle'),
this.get('fontWeight'),
(this.get('fontSize') || 12) + 'px',
this.get('fontFamily') || 'sans-serif'
].join(' ');
},
getTextRect: function (text) {
var textStyle = this.get('textStyle') || {};
return require('zrender/contain/text').getBoundingRect(
text,
this.getFont(),
textStyle.align,
textStyle.baseline
);
}
getTextRect: function (text) {
var textStyle = this.get('textStyle') || {};
return require('zrender/contain/text').getBoundingRect(
text,
this.getFont(),
textStyle.align,
textStyle.baseline
);
}
};
});
\ No newline at end of file
......@@ -70,10 +70,10 @@ define(function(require) {
var x2 = parsePercent(positionInfo.x2, containerWidth);
var y2 = parsePercent(positionInfo.y2, containerHeight);
isNaN(x) && (x = 0);
isNaN(x2) && (x2 = containerWidth);
isNaN(y) && (y = 0);
isNaN(y2) && (y2 = containerHeight);
(isNaN(x) || isNaN(parseFloat(positionInfo.x))) && (x = 0);
(isNaN(x2) || isNaN(parseFloat(positionInfo.x2))) && (x2 = containerWidth);
(isNaN(y) || isNaN(parseFloat(positionInfo.y))) && (y = 0);
(isNaN(y2) || isNaN(parseFloat(positionInfo.y2))) && (y2 = containerHeight);
margin = formatUtil.normalizeCssArray(margin || 0);
......@@ -84,30 +84,29 @@ define(function(require) {
};
/**
* Position group of component in viewport
* Group position is specified by either
* Parse position info.
* position info is specified by either
* {x, y}, {x2, y2}
* If all properties exists, x2 and y2 will be igonred.
*
* @param {module:zrender/container/Group} group
* @param {Object} positionInfo
* @param {number|string} [positionInfo.x]
* @param {number|string} [positionInfo.y]
* @param {number|string} [positionInfo.x2]
* @param {number|string} [positionInfo.y2]
* @param {number|string} [positionInfo.width]
* @param {number|string} [positionInfo.height]
* @param {Object} containerRect
* @param {string|number} margin
* @param {boolean} [notAlignX=false]
* @param {boolean} [notAlignY=false]
*/
layout.positionGroup = function (
group, positionInfo, containerRect, margin,
layout.parsePositionInfo = function (
positionInfo, containerRect, margin,
notAlignX, notAlignY
) {
margin = formatUtil.normalizeCssArray(margin || 0);
var groupRect = group.getBoundingRect();
var containerWidth = containerRect.width;
var containerHeight = containerRect.height;
......@@ -115,9 +114,8 @@ define(function(require) {
var y = parsePercent(positionInfo.y, containerHeight);
var x2 = parsePercent(positionInfo.x2, containerWidth);
var y2 = parsePercent(positionInfo.y2, containerHeight);
var width = groupRect.width;
var height = groupRect.height;
var width = parsePercent(positionInfo.width, containerWidth);
var height = parsePercent(positionInfo.height, containerHeight);
height += margin[2] + margin[0];
width += margin[1] + margin[3];
......@@ -137,7 +135,6 @@ define(function(require) {
case 'right':
x = containerWidth - width;
break;
}
}
if (!notAlignY) {
......@@ -151,9 +148,51 @@ define(function(require) {
}
}
return {
x: x + margin[3],
y: y + margin[0],
width: width,
height: height,
margin: margin
};
};
/**
* Position group of component in viewport
* Group position is specified by either
* {x, y}, {x2, y2}
* If all properties exists, x2 and y2 will be igonred.
*
* @param {module:zrender/container/Group} group
* @param {Object} positionInfo
* @param {number|string} [positionInfo.x]
* @param {number|string} [positionInfo.y]
* @param {number|string} [positionInfo.x2]
* @param {number|string} [positionInfo.y2]
* @param {Object} containerRect
* @param {string|number} margin
* @param {boolean} [notAlignX=false]
* @param {boolean} [notAlignY=false]
*/
layout.positionGroup = function (
group, positionInfo, containerRect, margin,
notAlignX, notAlignY
) {
var groupRect = group.getBoundingRect();
positionInfo = zrUtil.extend({
width: groupRect.width,
height: groupRect.height
}, positionInfo);
positionInfo = layout.parsePositionInfo(
positionInfo, containerRect, margin,
notAlignX, notAlignY
);
group.position = [
x - groupRect.x + margin[3],
y - groupRect.y + margin[0]
positionInfo.x - groupRect.x,
positionInfo.y - groupRect.y
];
};
......
......@@ -45,60 +45,122 @@
{
name: '三星',
value: 6,
},
{
name: '小米',
value: 4,
// children: [
// {
// name: '三星1',
// value: 12
// },
// {
// name: '小米1',
// value: 4
// },
// {
// name: '苹果1',
// value: 4
// },
// {
// name: '华为1',
// value: 2
// },
// {
// name: '联想1',
// value: 2,
// children: [
// {
// name: '三星1',
// value: 12
// },
// {
// name: '联想1',
// value: 2
// },
// {
// name: '魅族1',
// value: .09
// },
// {
// name: '中兴1',
// value: .003
// }
// ]
// },
// {
// name: '魅族1',
// value: .09
// },
// {
// name: '中兴1',
// value: .003
// }
// ]
},
// {
// name: '苹果',
// value: 4
// },
// {
// name: '华为',
// value: 2
// },
// {
// name: '联想',
// value: 2
// },
// {
// name: '魅族',
// value: 1,
// children: [
// {
// name: '三星1',
// value: 6
// },
// {
// name: '小米1',
// value: 4
// },
// {
// name: '苹果1',
// value: 4
// },
// {
// name: '华为1',
// value: 2
// },
// {
// name: '联想1',
// value: 2
// },
// {
// name: '魅族1',
// value: .09
// },
// {
// name: '中兴1',
// value: .01
// }
// ]
// },
{
name: '中兴',
value: .1,
children: [
{
name: '三星1',
value: 6
},
{
name: '小米1',
value: 4
},
{
name: '苹果1',
value: 4
},
{
name: '华为1',
value: 2
},
{
name: '联想1',
value: 2
},
{
name: '魅族1',
value: .3
name: '中兴',
value: .1
},
{
name: '中兴1',
value: .1
name: '中兴',
value: .1,
}
]
},
{
name: '小米',
value: 4
},
{
name: '苹果',
value: 4
},
{
name: '华为',
value: 2
},
{
name: '联想',
value: 2
},
{
name: '魅族',
value: 1
},
{
name: '中兴',
value: 1
value: .1,
}
]
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册