Merge remote-tracking branch 'origin/master'

This commit is contained in:
elaine 2019-04-27 15:01:34 +08:00
commit 90a03d1d44
9 changed files with 352 additions and 743 deletions

18
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,18 @@
# 不要修改该文件, 会自动生成, 详见 http://gitlab.alibaba-inc.com/node/ci
# 不要使用 ci 生成,直接按照 gitlab 文档自己写吧!
before_script:
- export PATH=$PWD/node_modules/.bin:$PWD/.node/bin:$PATH
- time enclose install tnpm:tnpm
- tnpm -v
stages:
- test
# job1
test:
image: reg.docker.alibaba-inc.com/dockerlab/node-ci:3.2.0
stage: test
script:
- env -u CI tnpm i --silent --internal-oss-cache --install-node=10
- time tnpm run lint
tags:
- swarm

View File

@ -56,7 +56,7 @@ G6.registerNode('tree-node', {
}
});
const bbox = text.getBBox();
const hasChildren = cfg.data.children && cfg.data.children.length > 0;
const hasChildren = cfg.children && cfg.children.length > 0;
if (hasChildren) {
group.addShape('marker', {
attrs: {
@ -88,15 +88,12 @@ const graph = new G6.TreeGraph({
default: [{
type: 'collapse-expand',
onChange(item, collapsed) {
const data = item.get('model').data;
const icon = item.get('group').findByClassName('collapse-icon');
if (collapsed) {
icon.attr('symbol', EXPAND_ICON);
} else {
icon.attr('symbol', COLLAPSE_ICON);
}
data.collapsed = collapsed;
return true;
}
}, 'drag-canvas', 'zoom-canvas']
},

View File

@ -49,17 +49,17 @@
width: 500,
height: 500,
pixelRatio: 2,
renderer: 'svg',
modes: {
default: [{
type: 'collapse-expand',
onChange(item, collapsed) {
const data = item.get('model').data;
data.collapsed = collapsed;
return true;
}
}, 'drag-canvas']
default: ['collapse-expand', 'drag-canvas']
},
defaultNode: {
size: 16,
anchorPoints: [[0,0.5], [1,0.5]]
},
defaultEdge: {
shape: 'cubic-horizontal'
},
defaultNode: { size: 16 },
nodeStyle: {
default: {
fill: '#40a9ff',
@ -144,13 +144,6 @@
};
graph.data(data);
graph.render();
graph.getNodes().forEach(node => {
node.get('model').anchorPoints = [[0,0.5], [1,0.5]];
});
graph.getEdges().forEach((edge) => {
edge.get('model').shape = 'cubic-horizontal';
});
graph.refresh();
graph.fitView();
document.getElementById('layout').addEventListener('change', (e) => {
const layout = e.target.value;

View File

@ -115,6 +115,7 @@
},
"dependencies": {
"@antv/g": "~3.4.3",
"@antv/hierarchy": "~0.4.0",
"@antv/util": "~1.3.1"
},
"engines": {

View File

@ -1,20 +1,10 @@
const Util = require('../util');
const DEFAULT_ANIMATE = {
duration: 500,
delay: 0,
callback() {},
easing: 'easeLinear'
};
module.exports = {
getDefaultCfg() {
return {
/**
* 发生收缩/扩展变化时是否需要重新layout
* 发生收缩/扩展变化时的回调
*/
onChange() {},
animate: {}
onChange() {}
};
},
getEvents() {
@ -24,227 +14,22 @@ module.exports = {
},
onNodeClick(e) {
const item = e.item;
const children = item.get('model').data.children;
const model = item.getModel();
const children = model.children;
// 叶子节点的收缩和展开没有意义
if (!children || children.length === 0) {
return;
}
const collapsed = !item.hasState('collapsed');
const collapsed = !model.collapsed;
if (!this.shouldBegin(e, collapsed)) {
return;
}
item.set('collapsed', collapsed);
model.collapsed = collapsed;
this.graph.emit('itemcollapsed', { item: e.item, collapsed });
if (!this.shouldUpdate(e, collapsed)) {
return;
}
const autoPaint = this.graph.get('autoPaint');
this.graph.setAutoPaint(false);
this.updateLayout(item, collapsed);
this.setChildrenState(item, 'collapsed', collapsed);
this.graph.setAutoPaint(autoPaint);
},
updateLayout(item, isCollapsed) {
const graph = this.graph;
let data = this.onChange(item, isCollapsed);
if (data) {
data = typeof data === 'boolean' ? graph.get('data') : data;
if (graph.get('layout')) {
data = graph.get('layout')(data);
}
if (this.animate) {
// 有动画,且有重布局,先停掉原有动画
if (this.graph.isLayoutAnimating()) {
this.graph.stopLayoutAnimate();
}
// 计算每个节点移动的起始位置和最终位置
this.animateChild(data);
// 如果是展开,缓存一下需要 show因为要先出
if (!isCollapsed) {
Util.traverseTree(item.get('model').data, data => {
const node = graph.findById(data.id);
node.set('shouldShow', true);
if (data.collapsed) {
return false;
}
});
}
this.performAnimate();
} else {
Util.traverseTree(data, child => {
const node = graph.findById(child.id);
node.get('model').x = child.x;
node.get('model').y = child.y;
});
graph.refresh();
Util.traverseTree(item.get('model').data, child => {
const node = graph.findById(child.id);
if (node === item) { return; }
if (isCollapsed) {
graph.hideItem(node);
} else {
graph.showItem(node);
}
});
graph.paint();
}
} else {
if (this.animate) {
// 没有重布局,有动画的情况
const model = item.get('model');
if (isCollapsed) {
this.collapsePosition(item, {
x: model.x,
y: model.y
});
} else {
this.expandPosition(item, { x: model.x, y: model.y });
}
this.performAnimate();
} else {
// 没有重布局,没有动画
if (isCollapsed) {
this.collapse(item);
} else {
this.expand(item);
}
this.graph.paint();
}
}
},
// 执行位置动画
performAnimate() {
const self = this;
const animate = Util.mix({}, DEFAULT_ANIMATE, self.animate);
const graph = self.graph;
graph.layoutAnimate(graph.get('data'), (node, ratio, origin) => {
const to = node.get('toPosition');
if (to) {
return {
x: origin.x + (to.x - origin.x) * ratio,
y: origin.y + (to.y - origin.y) * ratio
};
}
}, animate.duration, animate.easing, () => {
Util.each(graph.get('nodes'), node => {
if (node.get('shouldHide')) {
node.set('shouldHide', false);
node.hide();
}
if (node.get('shouldShow')) {
node.set('shouldShow', false);
graph.showItem(node);
}
node.set('toPosition', null);
});
graph.paint();
animate.callback();
}, animate.delay);
},
// 根据最新layout整理节点位置
animateChild(data) {
const self = this;
const node = self.graph.findById(data.id);
const nodeModel = node.get('model');
const point = {
x: data.x,
y: data.y
};
if (Util.isNil(point.x) || Util.isNil(point.y)) {
const model = node.get('parent').get('model');
point.x = model.x;
point.y = model.y;
}
if (nodeModel.x !== point.x || nodeModel.y !== point.y) {
node.set('toPosition', {
x: point.x,
y: point.y
});
}
// 收缩状态的节点,都跑收缩动画
if (node.get('collapsed')) {
self.collapsePosition(node, point);
} else {
// 还是展开状态的子节点,也做位移动画
Util.each(data.children, child => {
self.animateChild(child, data);
});
}
node.show();
},
// 收缩的节点的动画终态是最顶收缩态节点的坐标
collapsePosition(node, toPoint) {
const self = this;
Util.each(node.get('model').children, child => {
child = self.graph.findById(child.id);
const model = child.get('model');
if (!child.get('originPosition')) {
child.set('originPosition', { x: model.x, y: model.y });
}
child.set('toPosition', {
x: toPoint.x,
y: toPoint.y
});
child.set('shouldHide', true);
if (!child.get('collapsed')) {
self.collapsePosition(child, toPoint);
}
});
node.getOutEdges().forEach(edge => {
edge.hide();
});
},
// 张开的节点从startPoint开始动画到节点原本的位置
expandPosition(item, startPoint) {
const self = this;
const children = item.get('model').children;
Util.each(children, child => {
child = self.graph.findById(child.id);
self.graph.showItem(child);
const origin = child.get('originPosition');
child.set('toPosition', { x: origin.x, y: origin.y });
if (!child.get('collapsed')) {
self.expandPosition(child, startPoint);
}
});
},
// 设置子节点的状态
setChildrenState(item, state, enabled) {
const self = this;
// 如果父节点展开了,子节点还有原本就收缩的,不设置这个子树的状态
if (!enabled && item.get('collapsed')) {
return;
}
self.graph.setItemState(item, state, enabled);
Util.each(item.get('model').children, child => {
const node = self.graph.findById(child.id);
self.setChildrenState(node, state, enabled);
});
},
// 不包含动画,仅展示展开节点
expand(item) {
const self = this;
const graph = self.graph;
if (item.get('collapsed')) {
graph.showItem(item);
return;
}
graph.showItem(item);
Util.each(item.get('model').children, child => {
const node = self.graph.findById(child.id);
self.expand(node);
});
},
// 不包含动画,仅隐藏收缩节点
collapse(item) {
const self = this;
const graph = self.graph;
graph.setItemState(item, 'collapsed', true);
Util.each(item.get('model').children, child => {
const node = graph.findById(child.id);
self.collapse(node);
self.graph.hideItem(node);
});
this.onChange(item, collapsed);
this.graph.refreshLayout();
}
};

View File

@ -1,3 +1,4 @@
const Hierarchy = require('@antv/hierarchy');
const Util = require('../util');
const Graph = require('./graph');
@ -13,6 +14,18 @@ function indexOfChild(children, child) {
}
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接口的数据渲染视图
*/
@ -24,17 +37,11 @@ class TreeGraph extends Graph {
}
self.clear();
self.emit('beforerender');
const autoPaint = self.get('autoPaint');
self.setAutoPaint(false);
const rootData = self._refreshLayout(data);
const root = self._addChild(rootData, null);
self.set('root', root);
self.paint();
self.setAutoPaint(autoPaint);
self.refreshLayout();
self.emit('afterrender');
}
/**
* 添加子树
* 添加子树到对应 id 的节点
* @param {object} data 子树数据模型
* @param {string} parent 子树的父节点id
*/
@ -44,23 +51,46 @@ class TreeGraph extends Graph {
if (!Util.isString(parent)) {
parent = parent.get('id');
}
const parentData = self.findDataById(parent);
const parentData = self.findById(parent).getModel();
if (!parentData.children) {
parentData.children = [];
}
parentData.children.push(data);
self.changeData(self.get('data'));
self.changeData();
}
// 计算好layout的数据添加到graph中
_addChild(data, parent) {
_addChild(data, parent, animate) {
const self = this;
const node = self.addItem('node', data);
const model = data.data;
// model 中应存储真实的数据,特别是真实的 children
model.x = data.x;
model.y = data.y;
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);
self._addChild(child, node, animate);
});
return node;
}
@ -70,69 +100,83 @@ class TreeGraph extends Graph {
*/
changeData(data) {
const self = this;
if (!self.get('data')) {
self.data(data);
self.render();
return;
}
const autoPaint = this.get('autoPaint');
self.setAutoPaint(false);
if (data) {
self.data(data);
self.render();
} else {
self.refreshLayout();
}
const root = self._refreshLayout();
self._updateChild(root, null);
self.paint();
self.setAutoPaint(autoPaint);
}
/**
* 差量更新子树
* 更新源数据差量更新子树
* @param {object} data 子树数据模型
* @param {string} parent 子树的父节点id
*/
updateChild(data, parent) {
const self = this;
// 如果有父节点,则更新父节点下的子树
if (parent) {
parent = self.findDataById(parent).get('model');
}
// 如果没有父节点是全量的更新直接重置data
if (!parent) {
self.changeData(data);
return;
}
const parentModel = self.findById(parent).getModel();
const current = self.findById(data.id);
// 如果不存在该节点,则添加
if (!current) {
if (!parent.children) {
parent.children = [];
if (!parentModel.children) {
parentModel.children = [ current ];
} else {
parentModel.children.push(data);
}
parent.children.push(data);
} else {
const index = indexOfChild(parent.children, data);
const index = indexOfChild(parentModel.children, data);
parent.children[index] = data;
}
self.changeData();
}
_updateChild(data, parent) {
// 将数据上的变更转换到视图上
_updateChild(data, parent, animate) {
const self = this;
const current = self.findById(data.id);
// 若子树不存在,整体添加即可
if (!current) {
self._addChild(data, parent);
self._addChild(data, parent, animate);
return;
}
// 更新新节点下所有子节点
Util.each(data.children, child => {
self._updateChild(child, current);
self._updateChild(child, current, animate);
});
// 用现在节点的children来删除移除的子节点
Util.each(current.get('model').children, child => {
if (indexOfChild(data.children, child) === -1) {
self._removeChild(child.id);
// 用现在节点的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);
}
}
}
});
// 最后更新节点本身
self.updateItem(current, data);
}
const model = current.getModel();
if (animate) {
// 如果有动画,先缓存节点运动再更新节点
current.set('origin', {
x: model.x,
y: model.y
});
}
model.x = data.x;
model.y = data.y;
}
/**
* 删除子树
@ -146,24 +190,30 @@ class TreeGraph extends Graph {
}
const parent = node.get('parent');
if (parent && !parent.destroyed) {
const siblings = self.findDataById(parent.get('id')).children;
const index = indexOfChild(siblings, node.get('model'));
const siblings = parent.getModel().children;
const index = indexOfChild(siblings, node.getModel());
siblings.splice(index, 1);
}
self._removeChild(id);
self.changeData();
}
// 删除子节点Item对象
_removeChild(id) {
_removeChild(id, to, animate) {
const self = this;
const node = self.findById(id);
if (!node) {
return;
}
Util.each(node.get('model').children, child => {
self._removeChild(child.id);
Util.each(node.get('children'), child => {
self._removeChild(child.getModel().id, to, animate);
});
self.removeItem(node);
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);
}
}
/**
* 导出图数据
@ -172,19 +222,6 @@ class TreeGraph extends Graph {
save() {
return this.get('data');
}
// 数据变更后,刷新图布局
_refreshLayout() {
let root = this.get('data');
const layout = this.get('layout');
if ((!layout) && (!(root.x && root.y))) {
console.warn('tree graph accepts either a layout method or calculated data');
return;
}
if (layout) {
root = layout(root);
}
return root;
}
/**
* 根据id获取对应的源数据
* @param {string|object} id 元素id
@ -218,16 +255,37 @@ class TreeGraph extends Graph {
*/
changeLayout(layout) {
const self = this;
self.set('layout', layout);
if (layout) {
const autoPaint = self.get('autoPaint');
self.setAutoPaint(false);
const root = self._refreshLayout();
self._updateChild(root, null);
self.fitView();
self.paint();
self.setAutoPaint(autoPaint);
if (!layout) {
console.warn('layout cannot be null');
return;
}
self.set('layout', self._getLayout());
if (layout) {
self.refreshLayout();
}
}
/**
* 根据目前的 data 刷新布局更新到画布上用于变更数据之后刷新视图
*/
refreshLayout() {
const self = this;
const data = self.get('data');
const layoutData = self.get('layoutMethod')(data);
const animate = self.get('animate');
const autoPaint = self.get('autoPaint');
self.emit('beforerefreshlayout', { data, layoutData });
self.setAutoPaint(false);
self._updateChild(layoutData, null, animate);
if (!animate) {
// 如果没有动画,目前仅更新了节点的位置,刷新一下边的样式
self.refreshPositions();
self.paint();
} else {
self.layoutAnimate(layoutData, null);
}
self.setAutoPaint(autoPaint);
self.emit('afterrefreshlayout', { data, layoutData });
}
/**
* 布局动画接口用于数据更新时做节点位置更新的动画
@ -238,11 +296,17 @@ class TreeGraph extends Graph {
* @param {function} callback 动画结束的回调
* @param {number} delay 动画延迟执行(ms)
*/
layoutAnimate(data, onFrame, duration = 500, ease = 'easeLinear', callback, delay = 0) {
layoutAnimate(data, onFrame) {
const self = this;
const animateCfg = this.get('animateCfg');
self.emit('layoutanimatestart', { data });
const autoPaint = self.get('autoPaint');
self.setAutoPaint(false);
// 如果边中没有指定锚点,但是本身有锚点控制,在动画过程中保持锚点不变
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 => {
@ -252,8 +316,7 @@ class TreeGraph extends Graph {
if (!origin) {
origin = {
x: model.x,
y: model.y,
style: model.style
y: model.y
};
node.set('origin', origin);
}
@ -261,22 +324,32 @@ class TreeGraph extends Graph {
const attrs = onFrame(node, ratio, origin, data);
node.set('model', Util.mix(model, attrs));
} else {
model.x = origin.x + (data.x - origin.x) * ratio;
model.y = origin.y + (data.y - origin.y) * ratio;
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();
}
}, duration, ease, () => {
self.setAutoPaint(autoPaint);
self.emit('layoutanimateend', { data });
if (callback) {
callback();
}
Util.each(self.get('nodes'), node => {
}, animateCfg.duration, animateCfg.ease, () => {
Util.each(self.getNodes(), node => {
node.set('origin', null);
});
}, delay);
Util.each(self.get('removeList'), node => {
self.removeItem(node);
});
self.set('removeList', []);
if (animateCfg.callback) {
animateCfg.callback();
}
self.emit('layoutanimateend', { data });
}, animateCfg.delay);
}
/**
* 立即停止布局动画
@ -294,6 +367,24 @@ class TreeGraph extends Graph {
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';
}
return function(data) {
return Hierarchy[layout.type](data, layout);
};
}
}
module.exports = TreeGraph;

View File

@ -78,6 +78,9 @@ class Edge extends Item {
}
// 如果锚点没有对应的点或者没有锚点,则直接计算连接点
point = point || item.getLinkPoint(prePoint);
if (!Util.isNil(point.index)) {
this.set(name + 'AnchorIndex', point.index);
}
}
return point;
}

View File

@ -8,6 +8,7 @@ const Item = require('./item');
const CACHE_ANCHOR_POINTS = 'anchorPointsCache';
function getNearestPoint(points, curPoint) {
let index = 0;
let nearestPoint = points[0];
let minDistance = pointDistance(points[0], curPoint);
for (let i = 0; i < points.length; i++) {
@ -16,8 +17,10 @@ function getNearestPoint(points, curPoint) {
if (distance < minDistance) {
nearestPoint = point;
minDistance = distance;
index = i;
}
}
nearestPoint.anchorIndex = index;
return nearestPoint;
}

View File

@ -6,301 +6,17 @@ const div = document.createElement('div');
div.id = 'tree-spec';
document.body.appendChild(div);
function isNumberEqual(a, b) {
return Math.abs(a - b) < 0.0001;
}
describe('tree graph without layout', () => {
const graph = new G6.TreeGraph({
container: div,
width: 500,
height: 500,
pixelRatio: 2,
modes: {
default: [ 'drag-canvas' ]
}
});
let root = {
isRoot: true,
id: 'Root',
children: [
{
id: 'SubTreeNode1',
children: [
{
id: 'SubTreeNode1.1'
},
{
id: 'SubTreeNode1.2'
}
]
},
{
id: 'SubTreeNode2'
}
]
};
const NODE_SIZE = 16;
const PEM = 5;
root = Hierarchy.dendrogram(root, {
direction: 'TB', // H / V / LR / RL / TB / BT
nodeSep: 200,
getId(d) {
return d.id;
},
getHeight() {
return NODE_SIZE;
},
getWidth() {
return NODE_SIZE;
},
getHGap() {
return 10;
},
getVGap() {
return 10;
},
getSubTreeSep(d) {
if (!d.children || !d.children.length) {
return 0;
}
return PEM;
}
});
it('render tree graph', () => {
graph.data(root);
graph.render();
const rootNode = graph.get('root');
expect(rootNode).not.to.be.undefined;
expect(rootNode.get('model').id).to.equal('Root');
expect(Object.keys(graph.get('itemMap')).length).to.equal(9);
const edge = graph.findById('Root:SubTreeNode1');
expect(edge).not.to.be.undefined;
expect(edge.get('source')).to.equal(graph.findById('Root'));
expect(edge.get('target')).to.equal(graph.findById('SubTreeNode1'));
});
it('change data', () => {
let data = {
isRoot: true,
id: 'Root',
children: [
{
id: 'SubTreeNode1',
children: [
{
id: 'SubTreeNode1.1'
},
{
id: 'SubTreeNode1.2'
}
]
},
{
id: 'SubTreeNode3'
}, {
id: 'SubTreeNode4',
children: [{ id: 'SubTreeNode4.1' }]
}
]
};
const root = graph.get('root');
data = Hierarchy.dendrogram(data, {
direction: 'LR', // H / V / LR / RL / TB / BT
nodeSep: 200,
getId(d) {
return d.id;
},
getHeight() {
return NODE_SIZE;
},
getWidth() {
return NODE_SIZE;
},
getHGap() {
return 10;
},
getVGap() {
return 10;
},
getSubTreeSep(d) {
if (!d.children || !d.children.length) {
return 0;
}
return PEM;
}
});
graph.changeData(data);
graph.fitView();
expect(graph.get('root')).to.equal(root);
expect(Object.keys(graph.get('itemMap')).length).to.equal(13);
expect(graph.findById('SubTreeNode2')).to.be.undefined;
expect(graph.findById('SubTreeNode3')).not.to.be.undefined;
expect(graph.findById('SubTreeNode4')).not.to.be.undefined;
const edge = graph.findById('SubTreeNode4:SubTreeNode4.1');
expect(edge).not.to.be.undefined;
expect(edge.get('source')).to.equal(graph.findById('SubTreeNode4'));
expect(edge.get('target')).to.equal(graph.findById('SubTreeNode4.1'));
});
it('add child', () => {
const parent = graph.findById('SubTreeNode3');
const child = { id: 'SubTreeNode3.1', x: 100, y: 100, shape: 'rect', children: [{ x: 150, y: 150, id: 'SubTreeNode3.1.1' }] };
graph.addChild(child, parent);
const children = parent.get('model').children;
expect(children).not.to.be.undefined;
expect(children.length).to.equal(1);
expect(children[0].id).to.equal('SubTreeNode3.1');
expect(graph.findById('SubTreeNode3.1')).not.to.be.undefined;
expect(graph.findById('SubTreeNode3:SubTreeNode3.1')).not.to.be.undefined;
expect(graph.findById('SubTreeNode3.1.1')).not.to.be.undefined;
expect(graph.findById('SubTreeNode3.1:SubTreeNode3.1.1')).not.to.be.undefined;
});
it('remove child', () => {
graph.removeChild('SubTreeNode3.1');
const parent = graph.findById('SubTreeNode3');
const children = parent.get('model').children;
expect(children.length).to.equal(0);
expect(graph.findById('SubTreeNode3.1')).to.be.undefined;
expect(graph.findById('SubTreeNode3:SubTreeNode3.1')).to.be.undefined;
expect(graph.findById('SubTreeNode3.1.1')).to.be.undefined;
expect(graph.findById('SubTreeNode3.1:SubTreeNode3.1.1')).to.be.undefined;
});
it('collapse & expand with default animate', done => {
const parent = graph.findById('SubTreeNode1');
const child = graph.findById('SubTreeNode1.1');
let collapsed = true;
graph.addBehaviors({
type: 'collapse-expand',
animate: {
duration: 500,
callback() {
if (collapsed) {
expect(parent.get('collapsed')).to.be.true;
expect(parent.hasState('collapsed')).to.be.true;
expect(isNumberEqual(child.get('model').x, parent.get('model').x)).to.be.true;
expect(!!child.get('collapsed')).to.be.false;
expect(child.hasState('collapsed')).to.be.true;
expect(isNumberEqual(child.get('model').y, parent.get('model').y)).to.be.true;
} else {
expect(parent.get('collapsed')).to.be.false;
expect(parent.hasState('collapsed')).to.be.false;
expect(child.get('model').x).not.to.equal(parent.get('model').x);
expect(!!child.get('collapsed')).to.be.false;
expect(child.hasState('collapsed')).to.be.false;
expect(child.get('model').y).not.to.equal(parent.get('model').y);
graph.removeBehaviors('collapse-expand', 'default');
done();
}
}
}
}, 'default');
graph.emit('node:click', { item: parent });
setTimeout(() => {
collapsed = false;
graph.emit('node:click', { item: parent });
}, 600);
});
it('collapse & expand without onChange', () => {
graph.addBehaviors({
type: 'collapse-expand',
animate: false
}, 'default');
const root = graph.get('root');
const child = graph.findById('SubTreeNode1');
const leave = graph.findById('SubTreeNode1.1');
graph.emit('node:click', { item: root });
expect(root.isVisible()).to.be.true;
expect(child.isVisible()).to.be.false;
expect(leave.isVisible()).to.be.false;
expect(child.hasState('collapsed')).to.be.true;
expect(leave.hasState('collapsed')).to.be.true;
graph.emit('node:click', { item: root });
expect(root.isVisible()).to.be.true;
expect(child.isVisible()).to.be.true;
expect(leave.isVisible()).to.be.true;
expect(child.hasState('collapsed')).to.be.false;
expect(leave.hasState('collapsed')).to.be.false;
graph.removeBehaviors('collapse-expand', 'default');
});
it('collapse & expand animate', done => {
G6.Global.defaultNode.style.fill = '#fff';
const parent = graph.findById('SubTreeNode1');
const child = graph.findById('SubTreeNode1.1');
let collapsed = true;
graph.addBehaviors({
type: 'collapse-expand',
animate: {
duration: 500,
callback() {
if (collapsed) {
expect(parent.get('collapsed')).to.be.true;
expect(parent.hasState('collapsed')).to.be.true;
expect(isNumberEqual(child.get('model').x, parent.get('model').x)).to.be.true;
expect(!!child.get('collapsed')).to.be.false;
expect(child.hasState('collapsed')).to.be.true;
expect(isNumberEqual(child.get('model').y, parent.get('model').y)).to.be.true;
} else {
expect(parent.get('collapsed')).to.be.false;
expect(parent.hasState('collapsed')).to.be.false;
expect(child.get('model').x).not.to.equal(parent.get('model').x);
expect(!!child.get('collapsed')).to.be.false;
expect(child.hasState('collapsed')).to.be.false;
expect(child.get('model').y).not.to.equal(parent.get('model').y);
graph.removeBehaviors('collapse-expand', 'default');
done();
}
}
},
onChange(item, collapsed) {
let data = graph.get('data');
item.get('model').data.collapsed = collapsed;
data = Hierarchy.dendrogram(data.data, {
direction: 'LR', // H / V / LR / RL / TB / BT
nodeSep: 200,
getId(d) {
return d.id;
},
getHeight() {
return NODE_SIZE;
},
getWidth() {
return NODE_SIZE;
},
getHGap() {
return 10;
},
getVGap() {
return 10;
},
getSubTreeSep(d) {
if (!d.children || !d.children.length) {
return 0;
}
return PEM;
}
});
return data;
}
}, 'default');
graph.emit('node:click', { item: parent });
setTimeout(() => {
collapsed = false;
graph.emit('node:click', { item: parent });
}, 600);
});
});
describe('tree graph with layout', () => {
let count = 0;
describe('tree graph without animate', () => {
const graph = new G6.TreeGraph({
container: div,
width: 500,
height: 500,
pixelRatio: 2,
animate: false,
modes: {
default: [ 'drag-canvas' ]
},
layout: data => {
count++;
return Hierarchy.dendrogram(data, {
direction: 'LR', // H / V / LR / RL / TB / BT
nodeSep: 50,
@ -332,16 +48,12 @@ describe('tree graph with layout', () => {
graph.data(data);
graph.render();
graph.fitView();
const rootNode = graph.get('root');
expect(rootNode).not.to.be.undefined;
expect(rootNode.get('model').id).to.equal('Root');
expect(Object.keys(graph.get('itemMap')).length).to.equal(9);
const edge = graph.findById('Root:SubTreeNode1');
expect(edge).not.to.be.undefined;
expect(edge.get('source')).to.equal(graph.findById('Root'));
expect(edge.get('target')).to.equal(graph.findById('SubTreeNode1'));
expect(graph.save()).to.equal(data);
expect(count).to.equal(1);
});
it('changeData', () => {
const data = {
@ -377,7 +89,6 @@ describe('tree graph with layout', () => {
expect(edge).not.to.be.undefined;
expect(edge.get('source')).to.equal(graph.findById('SubTreeNode4'));
expect(edge.get('target')).to.equal(graph.findById('SubTreeNode4.1'));
expect(count).to.equal(2);
});
it('add child', () => {
const parent = graph.findById('SubTreeNode3');
@ -391,7 +102,6 @@ describe('tree graph with layout', () => {
expect(graph.findById('SubTreeNode3:SubTreeNode3.1')).not.to.be.undefined;
expect(graph.findById('SubTreeNode3.1.1')).not.to.be.undefined;
expect(graph.findById('SubTreeNode3.1:SubTreeNode3.1.1')).not.to.be.undefined;
expect(count).to.equal(3);
});
it('remove child', () => {
graph.removeChild('SubTreeNode3.1');
@ -402,138 +112,146 @@ describe('tree graph with layout', () => {
expect(graph.findById('SubTreeNode3:SubTreeNode3.1')).to.be.undefined;
expect(graph.findById('SubTreeNode3.1.1')).to.be.undefined;
expect(graph.findById('SubTreeNode3.1:SubTreeNode3.1.1')).to.be.undefined;
expect(count).to.equal(4);
});
it('collapse & expand with layout', done => {
const parent = graph.findById('SubTreeNode1');
const child = graph.findById('SubTreeNode1.1');
let child = graph.findById('SubTreeNode1.1');
let collapsed = true;
graph.addBehaviors({
type: 'collapse-expand',
onChange: (item, collapsed) => {
const data = item.get('model').data;
data.collapsed = collapsed;
return false;
},
animate: {
callback() {
if (collapsed) {
expect(parent.get('collapsed')).to.be.true;
expect(parent.hasState('collapsed')).to.be.true;
expect(isNumberEqual(child.get('model').x, parent.get('model').x)).to.be.true;
expect(!!child.get('collapsed')).to.be.false;
expect(child.hasState('collapsed')).to.be.true;
expect(isNumberEqual(child.get('model').y, parent.get('model').y)).to.be.true;
} else {
expect(parent.get('collapsed')).to.be.false;
expect(parent.hasState('collapsed')).to.be.false;
expect(child.get('model').x).not.to.equal(parent.get('model').x);
expect(!!child.get('collapsed')).to.be.false;
expect(child.hasState('collapsed')).to.be.false;
expect(child.get('model').y).not.to.equal(parent.get('model').y);
done();
}
}
} }, 'default');
graph.on('afterrefreshlayout', () => {
if (collapsed) {
expect(parent.getModel().collapsed).to.be.true;
expect(child.destroyed).to.be.true;
} else {
child = graph.findById('SubTreeNode1.1');
expect(parent.getModel().collapsed).to.be.false;
expect(child.get('model').x).not.to.equal(parent.get('model').x);
expect(!!child.getModel().collapsed).to.be.false;
expect(child.get('model').y).not.to.equal(parent.get('model').y);
done();
}
});
graph.addBehaviors('collapse-expand', 'default');
graph.emit('node:click', { item: parent });
setTimeout(() => {
collapsed = false;
graph.emit('node:click', { item: parent });
}, 600);
});
/* const treeData = {
isRoot: true,
id: 'Root',
children: [
{
id: 'SubTreeNode1',
children: [
{
id: 'SubTreeNode1.1'
},
{
id: 'SubTreeNode1.2'
}
]
},
{
id: 'SubTreeNode2',
children: [
{
id: 'SubTreeNode2.1'
},
{
id: 'SubTreeNode2.2',
children: [
{
id: 'SubTreeNode1.2.1'
},
{
id: 'SubTreeNode1.2.2'
},
{
id: 'SubTreeNode1.2.3'
}
]
}
]
}, {
id: 'SubTreeNode3'
}, {
id: 'SubTreeNode4'
}, {
id: 'SubTreeNode5'
}, {
id: 'SubTreeNode6'
}, {
id: 'SubTreeNode7',
children: [
{
id: 'SubTreeNode3.1'
},
{
id: 'SubTreeNode3.2'
},
{
id: 'SubTreeNode3.3'
}
]
}, {
id: 'SubTreeNode8'
}, {
id: 'SubTreeNode9'
}, {
id: 'SubTreeNode10'
}, {
id: 'SubTreeNode11'
}
]
};
it('radial layout', () => {
graph.data(treeData);
graph.render();
G6.Util.radialLayout(graph, 'LR');
});
describe('tree graph with animate', () => {
const graph = new G6.TreeGraph({
container: div,
width: 500,
height: 500,
pixelRatio: 2,
modes: {
default: [ 'drag-canvas' ]
},
layout: {
type: 'dendrogram',
direction: 'LR',
nodeSep: 50,
rankSep: 100
}
});
it('vertical layout to radial layout', () => {
const graph = new G6.TreeGraph({
container: div,
width: 500,
height: 500,
pixelRatio: 2,
modes: {
default: [ 'drag-canvas' ]
},
layout: data => {
count++;
return Hierarchy.dendrogram(data, {
direction: 'TB', // H / V / LR / RL / TB / BT
nodeSep: 50,
rankSep: 100
});
it('layout init', done => {
const data = {
isRoot: true,
id: 'Root',
children: [
{
id: 'SubTreeNode1',
children: [
{
id: 'SubTreeNode1.1'
},
{
id: 'SubTreeNode1.2'
}
]
},
{
id: 'SubTreeNode2'
}
]
};
graph.data(data);
graph.render();
graph.fitView();
const layoutMethod = graph.get('layoutMethod');
expect(layoutMethod).not.to.be.undefined;
expect(typeof layoutMethod).to.equal('function');
expect(Object.keys(graph.get('itemMap')).length).to.equal(9);
const edge = graph.findById('Root:SubTreeNode1');
expect(edge).not.to.be.undefined;
expect(edge.get('source')).to.equal(graph.findById('Root'));
expect(edge.get('target')).to.equal(graph.findById('SubTreeNode1'));
expect(graph.save()).to.equal(data);
expect(JSON.stringify(data)).not.to.throw;
graph.on('layoutanimateend', () => { done(); });
});
it('changeData', done => {
graph.removeEvent();
const data = {
isRoot: true,
id: 'Root',
children: [
{
id: 'SubTreeNode1',
children: [
{
id: 'SubTreeNode1.1'
},
{
id: 'SubTreeNode1.2'
}
]
},
{
id: 'SubTreeNode3'
}, {
id: 'SubTreeNode4',
children: [{ id: 'SubTreeNode4.1' }]
}
]
};
graph.changeData(data);
expect(graph.save()).to.equal(data);
graph.on('layoutanimateend', () => {
expect(Object.keys(graph.get('itemMap')).length).to.equal(13);
expect(graph.findById('SubTreeNode2')).to.be.undefined;
expect(graph.findById('SubTreeNode3')).not.to.be.undefined;
expect(graph.findById('SubTreeNode4')).not.to.be.undefined;
const edge = graph.findById('SubTreeNode4:SubTreeNode4.1');
expect(edge).not.to.be.undefined;
expect(edge.get('source')).to.equal(graph.findById('SubTreeNode4'));
expect(edge.get('target')).to.equal(graph.findById('SubTreeNode4.1'));
done();
});
});
it('collapse & expand', done => {
graph.removeEvent();
const parent = graph.findById('SubTreeNode1');
let child = graph.findById('SubTreeNode1.1');
let collapsed = true;
graph.on('layoutanimateend', () => {
if (collapsed) {
expect(parent.getModel().collapsed).to.be.true;
expect(child.destroyed).to.be.true;
} else {
child = graph.findById('SubTreeNode1.1');
expect(parent.getModel().collapsed).to.be.false;
expect(child.get('model').x).not.to.equal(parent.get('model').x);
expect(!!child.getModel().collapsed).to.be.false;
expect(child.get('model').y).not.to.equal(parent.get('model').y);
done();
}
});
graph.data(treeData);
graph.render();
G6.Util.radialLayout(graph, 'TB');
});*/
graph.addBehaviors('collapse-expand', 'default');
graph.emit('node:click', { item: parent });
setTimeout(() => {
collapsed = false;
graph.emit('node:click', { item: parent });
}, 600);
});
});