@antv/g6
Version:
graph visualization frame work
408 lines (401 loc) • 11.6 kB
JavaScript
const Hierarchy = require('@antv/hierarchy');
const Util = require('../util');
const Graph = require('./graph');
function indexOfChild(children, child) {
let index = -1;
Util.each(children, (former, i) => {
if (child.id === former.id) {
index = i;
return false;
}
});
return index;
}
class TreeGraph extends Graph {
constructor(cfg) {
super(cfg);
// 用于缓存动画结束后需要删除的节点
this.set('removeList', []);
this.set('layoutMethod', this._getLayout());
}
getDefaultCfg() {
const cfg = super.getDefaultCfg();
// 树图默认打开动画
cfg.animate = true;
return cfg;
}
/**
* 根据data接口的数据渲染视图
*/
render() {
const self = this;
const data = self.get('data');
if (!data) {
throw new Error('data must be defined first');
}
self.clear();
self.emit('beforerender');
self.refreshLayout(this.get('fitView'));
self.emit('afterrender');
}
/**
* 添加子树到对应 id 的节点
* @param {object} data 子树数据模型
* @param {string} parent 子树的父节点id
*/
addChild(data, parent) {
const self = this;
// 将数据添加到源数据中,走changeData方法
if (!Util.isString(parent)) {
parent = parent.get('id');
}
const parentData = self.findDataById(parent);
if (!parentData.children) {
parentData.children = [];
}
parentData.children.push(data);
self.changeData();
}
// 计算好layout的数据添加到graph中
_addChild(data, parent, animate) {
const self = this;
const model = data.data;
// model 中应存储真实的数据,特别是真实的 children
model.x = data.x;
model.y = data.y;
model.depth = data.depth;
const node = self.addItem('node', model);
if (parent) {
node.set('parent', parent);
if (animate) {
const origin = parent.get('origin');
if (origin) {
node.set('origin', origin);
} else {
const parentModel = parent.getModel();
node.set('origin', {
x: parentModel.x,
y: parentModel.y
});
}
}
const childrenList = parent.get('children');
if (!childrenList) {
parent.set('children', [ node ]);
} else {
childrenList.push(node);
}
self.addItem('edge', { source: parent, target: node, id: parent.get('id') + ':' + node.get('id') });
}
// 渲染到视图上应参考布局的children, 避免多绘制了收起的节点
Util.each(data.children, child => {
self._addChild(child, node, animate);
});
return node;
}
/**
* 更新数据模型,差量更新并重新渲染
* @param {object} data 数据模型
*/
changeData(data) {
const self = this;
if (data) {
self.data(data);
self.render();
} else {
self.refreshLayout(this.get('fitView'));
}
}
/**
* 更新源数据,差量更新子树
* @param {object} data 子树数据模型
* @param {string} parent 子树的父节点id
*/
updateChild(data, parent) {
const self = this;
// 如果没有父节点或找不到该节点,是全量的更新,直接重置data
if (!parent || !self.findById(parent)) {
self.changeData(data);
return;
}
const parentModel = self.findById(parent).getModel();
const current = self.findById(data.id);
// 如果不存在该节点,则添加
if (!current) {
if (!parentModel.children) {
parentModel.children = [ current ];
} else {
parentModel.children.push(data);
}
} else {
const index = indexOfChild(parentModel.children, data);
parentModel.children[index] = data;
}
self.changeData();
}
// 将数据上的变更转换到视图上
_updateChild(data, parent, animate) {
const self = this;
const current = self.findById(data.id);
// 若子树不存在,整体添加即可
if (!current) {
self._addChild(data, parent, animate);
return;
}
// 更新新节点下所有子节点
Util.each(data.children, child => {
self._updateChild(child, current, animate);
});
// 用现在节点的children实例来删除移除的子节点
const children = current.get('children');
if (children) {
const len = children.length;
if (len > 0) {
let child;
for (let i = children.length - 1; i >= 0; i--) {
child = children[i].getModel();
if (indexOfChild(data.children, child) === -1) {
self._removeChild(child.id, {
x: data.x,
y: data.y
}, animate);
// 更新父节点下缓存的子节点 item 实例列表
children.splice(i, 1);
}
}
}
}
const model = current.getModel();
if (animate) {
// 如果有动画,先缓存节点运动再更新节点
current.set('origin', {
x: model.x,
y: model.y
});
}
current.set('model', data.data);
current.updatePosition({ x: data.x, y: data.y });
}
/**
* 删除子树
* @param {string} id 子树根节点id
*/
removeChild(id) {
const self = this;
const node = self.findById(id);
if (!node) {
return;
}
const parent = node.get('parent');
if (parent && !parent.destroyed) {
const siblings = self.findDataById(parent.get('id')).children;
const index = indexOfChild(siblings, node.getModel());
siblings.splice(index, 1);
}
self.changeData();
}
// 删除子节点Item对象
_removeChild(id, to, animate) {
const self = this;
const node = self.findById(id);
if (!node) {
return;
}
Util.each(node.get('children'), child => {
self._removeChild(child.getModel().id, to, animate);
});
if (animate) {
const model = node.getModel();
node.set('to', to);
node.set('origin', { x: model.x, y: model.y });
self.get('removeList').push(node);
} else {
self.removeItem(node);
}
}
/**
* 导出图数据
* @return {object} data
*/
save() {
return this.get('data');
}
/**
* 根据id获取对应的源数据
* @param {string|object} id 元素id
* @param {object} parent 从哪个节点开始寻找,为空时从根节点开始查找
* @return {object} 对应源数据
*/
findDataById(id, parent) {
const self = this;
if (!parent) {
parent = self.get('data');
}
if (id === parent.id) {
return parent;
}
let result = null;
Util.each(parent.children, child => {
if (child.id === id) {
result = child;
return false;
}
result = self.findDataById(id, child);
if (result) {
return false;
}
});
return result;
}
/**
* 更改并应用树布局算法
* @param {object} layout 布局算法
*/
changeLayout(layout) {
const self = this;
if (!layout) {
console.warn('layout cannot be null');
return;
}
self.set('layout', layout);
self.set('layoutMethod', self._getLayout());
self.refreshLayout();
}
/**
* 根据目前的 data 刷新布局,更新到画布上。用于变更数据之后刷新视图。
* @param {boolean} fitView 更新布局时是否需要适应窗口
*/
refreshLayout(fitView) {
const self = this;
const data = self.get('data');
const layoutData = self.get('layoutMethod')(data, self.get('layout'));
const animate = self.get('animate');
const autoPaint = self.get('autoPaint');
self.emit('beforerefreshlayout', { data, layoutData });
self.setAutoPaint(false);
self._updateChild(layoutData, null, animate);
if (fitView) {
self.get('viewController')._fitView();
}
if (!animate) {
// 如果没有动画,目前仅更新了节点的位置,刷新一下边的样式
self.refresh();
self.paint();
} else {
self.layoutAnimate(layoutData, null);
}
self.setAutoPaint(autoPaint);
self.emit('afterrefreshlayout', { data, layoutData });
}
/**
* 布局动画接口,用于数据更新时做节点位置更新的动画
* @param {object} data 更新的数据
* @param {function} onFrame 定义节点位置更新时如何移动
* @param {number} duration 动画时间
* @param {string} ease 指定动效
* @param {function} callback 动画结束的回调
* @param {number} delay 动画延迟执行(ms)
*/
layoutAnimate(data, onFrame) {
const self = this;
this.setAutoPaint(false);
const animateCfg = this.get('animateCfg');
self.emit('beforeanimate', { data });
// 如果边中没有指定锚点,但是本身有锚点控制,在动画过程中保持锚点不变
self.getEdges().forEach(edge => {
const model = edge.get('model');
if (!model.sourceAnchor) {
model.sourceAnchor = edge.get('sourceAnchorIndex');
}
});
this.get('canvas').animate({
onFrame(ratio) {
Util.traverseTree(data, child => {
const node = self.findById(child.id);
// 只有当存在node的时候才执行
if (node) {
let origin = node.get('origin');
const model = node.get('model');
if (!origin) {
origin = {
x: model.x,
y: model.y
};
node.set('origin', origin);
}
if (onFrame) {
const attrs = onFrame(node, ratio, origin, data);
node.set('model', Util.mix(model, attrs));
} else {
model.x = origin.x + (child.x - origin.x) * ratio;
model.y = origin.y + (child.y - origin.y) * ratio;
}
}
});
Util.each(self.get('removeList'), node => {
const model = node.getModel();
const from = node.get('origin');
const to = node.get('to');
model.x = from.x + (to.x - from.x) * ratio;
model.y = from.y + (to.y - from.y) * ratio;
});
self.refreshPositions();
}
}, animateCfg.duration, animateCfg.ease, () => {
Util.each(self.getNodes(), node => {
node.set('origin', null);
});
Util.each(self.get('removeList'), node => {
self.removeItem(node);
});
self.set('removeList', []);
if (animateCfg.callback) {
animateCfg.callback();
}
self.paint();
this.setAutoPaint(true);
self.emit('afteranimate', { data });
}, animateCfg.delay);
}
/**
* 立即停止布局动画
*/
stopLayoutAnimate() {
this.get('canvas').stopAnimate();
this.emit('layoutanimateend', { data: this.get('data') });
this.layoutAnimating = false;
}
/**
* 是否在布局动画
* @return {boolean} 是否有布局动画
*/
isLayoutAnimating() {
return this.layoutAnimating;
}
_getLayout() {
const layout = this.get('layout');
if (!layout) {
return null;
}
if (typeof layout === 'function') {
return layout;
}
if (!layout.type) {
layout.type = 'dendrogram';
}
if (!layout.direction) {
layout.direction = 'TB';
}
if (layout.radial) {
return function(data) {
const layoutData = Hierarchy[layout.type](data, layout);
Util.radialLayout(layoutData);
return layoutData;
};
}
return function(data) {
return Hierarchy[layout.type](data, layout);
};
}
}
module.exports = TreeGraph;