cytoscape.js(一)布局切换动画+两节点间多个关系展示

需求

仍然是关系图的展示,前面用到的vis.js在显示两个节点间多个关系时的表现有些鸡肋,同时布局切换时没有动画效果(节点逐渐移动边随之移动),这一周特尝试了sigma.js,它在布局切换时的动画很吸引人,但是同样的问题,不支持两节点多个关系的展示,所以也是从入门到放弃了,下面会分别就 sigma.js 来谈谈问题

  • 两节点间同方向多个关系的展示
  • 布局切换动画效果

问题分析

两节点间同方向多个关系的展示

生成曲线边的时候,发现使用的是贝塞尔曲线来画线,除了已知起始点坐标,我们需要知道一个控制点坐标,在 sigma.js 中,算法根据起始点坐标计算出一个控制点,所以只要坐标不变,无论几条边,控制点永远是一个,这样就导致边重合在一起

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
sigma.utils.getQuadraticControlPoint = function(x1, y1, x2, y2) {
return {
x: (x1 + x2) / 2 + (y2 - y1) / 4,
y: (y1 + y2) / 2 + (x1 - x2) / 4
};
};

sigma.utils.getPointOnQuadraticCurve = function(t, x1, y1, x2, y2, xi, yi) {
// http://stackoverflow.com/a/5634528
return {
x: Math.pow(1 - t, 2) * x1 + 2 * (1 - t) * t * xi + Math.pow(t, 2) * x2,
y: Math.pow(1 - t, 2) * y1 + 2 * (1 - t) * t * yi + Math.pow(t, 2) * y2
};
};

sigma.utils.getPointOnBezierCurve =
function(t, x1, y1, x2, y2, cx, cy, dx, dy) {
// http://stackoverflow.com/a/15397596
// Blending functions:
var B0_t = Math.pow(1 - t, 3),
B1_t = 3 * t * Math.pow(1 - t, 2),
B2_t = 3 * Math.pow(t, 2) * (1 - t),
B3_t = Math.pow(t, 3);

return {
x: (B0_t * x1) + (B1_t * cx) + (B2_t * dx) + (B3_t * x2),
y: (B0_t * y1) + (B1_t * cy) + (B2_t * dy) + (B3_t * y2)
};
};

布局切换动画

sigma.js 的切换动画真的很吸引人,我是通过 gephi 进行布局,生成节点的坐标,并生成为 json 导出,sigma.js 的动画需要将不同的坐标放在一个 json 中,使用不同的前缀区分。这样我在做的时候需要读每个布局文件然后取出节点坐标,根据id追加到对应的节点对象中,将不同布局的节点坐标存进去。个人认为,如果不需要对关系图进行很强的数据操作,只求显示效果好的话,sigma.js真的是一个不错的选择

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
for (i = 0; i < N; i++) {
o = {
id: 'n' + i,
label: 'Node ' + i,
circular_x: L * Math.cos(Math.PI * 2 * i / N - Math.PI / 2),
circular_y: L * Math.sin(Math.PI * 2 * i / N - Math.PI / 2),
circular_size: Math.random(),
circular_color: '#' + (
Math.floor(Math.random() * 16777215).toString(16) + '000000'
).substr(0, 6),
grid_x: i % L,
grid_y: Math.floor(i / L),
grid_size: 1,
grid_color: '#ccc'
};

['x', 'y', 'size', 'color'].forEach(function(val) {
o[val] = o['grid_' + val];
});

g.nodes.push(o);
}

for (i = 0; i < E; i++)
g.edges.push({
id: 'e' + i,
source: 'n' + (Math.random() * N | 0),
target: 'n' + (Math.random() * N | 0)
});

// Instantiate sigma:
s = new sigma({
graph: g,
container: 'graph-container',
settings: {
animationsTime: 1000
}
});

setInterval(function() {
var prefix = ['grid_', 'circular_'][step = +!step];
sigma.plugins.animate(
s,
{
x: prefix + 'x',
y: prefix + 'y',
size: prefix + 'size',
color: prefix + 'color'
}
);
}, 2000);

cytoscape.js

完美支持两节点间多个关系的显示,这点 vis.js 和 sigma.js 都没有很好的支持.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
var cy = cytoscape({
container: document.getElementById('cy'),

boxSelectionEnabled: false,
autounselectify: true,

style: cytoscape.stylesheet()
.selector('node')
.style({
'content': 'data(id)'
})
.selector('edge')
.style({
'curve-style': 'bezier',
'target-arrow-shape': 'triangle',
'width': 4,
'line-color': '#ddd',
'target-arrow-color': '#ddd'
})
.selector('.highlighted')
.style({
'background-color': '#61bffc',
'line-color': '#61bffc',
'target-arrow-color': '#61bffc',
'transition-property': 'background-color, line-color, target-arrow-color',
'transition-duration': '0.5s'
}),
elements: {
nodes: [
{ data: { id: 'a' } },
{ data: { id: 'b' } },
{ data: { id: 'c' } },
{ data: { id: 'd' } },
{ data: { id: 'e' } }
],

edges: [
{ data: { id: 'ae1', weight: 1, source: 'a', target: 'e' } },
{ data: { id: 'ab', weight: 3, source: 'a', target: 'b' } },
{ data: { id: 'be', weight: 4, source: 'b', target: 'e' } },
{ data: { id: 'bc', weight: 5, source: 'b', target: 'c' } },
{ data: { id: 'ce', weight: 6, source: 'c', target: 'e' } },
{ data: { id: 'cd', weight: 2, source: 'c', target: 'd' } },
{ data: { id: 'de', weight: 7, source: 'd', target: 'e' } },
{ data: { id: 'ae2', weight: 1, source: 'a', target: 'e' } }
]
},
layout: {
name: 'circle'
}
});
var id = {};
var data = fetch('data.json', {mode: 'no-cors'})
.then(function(res) {
return res.json()
})
.then(function(data) {
data.forEach(function(e,i){
cy.elements().nodes().forEach(function( node ){
if(e.data.id == node.id()){
node.animate({
position: { x: e.position.x, y: e.position.y }
},{
duration: 1000
})
}
});
})
});

这是切换布局的动画,读json并获取对应id的坐标,使用动画移动到响应的位置,后面需要生成历史的话,可以根据sigma.js的思路来做.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
fetch('data.json', {mode: 'no-cors'})
.then(function(res) {
return res.json()
})
.then(function(data) {

console.log(data);
var toJson = function(res){ return res.json(); };
var cy = window.cy = cytoscape({
container: document.getElementById('cy'),

layout: {
name: 'circle'
},

style: fetch('cy-style.json').then(toJson),

elements: data
});
cy.nodes().animate({
style: { 'background-color': 'blue' }
}, {
duration: 1000
});
});

//以下为cy_style.json
[{
"selector": "node",
"style": {
"text-valign": "center",
"text-halign": "left",
"width": 16,
"height": 16,
"background-color": "red"
}
}, {
"selector": "node[type]",
"style": {
"label": "data(type)"
}
}, {
"selector": "edge",
"style": {
"width": 1,
"curve-style": "bezier"
}
}, {
"selector": "edge[arrow]",
"style": {
"target-arrow-shape": "data(arrow)"
}
}, {
"selector": "edge.hollow",
"style": {
"target-arrow-fill": "hollow"
}
}]

data.json我就不贴了,按照格式自己写几条测试两节点间多条边的数据即可

总结

布局的动画切换这个部分其实还是手动了,如果只需要更改layout对应参数就能使点移动会更加完美,不过手动也好,学习深度会增加,有时间一定要看看如何实现的两节点间多条变不重合,看看贝塞尔的控制点是如何变的。