背景
图可视化有时需要借助动画达到展示、分析的目的。合理的动画不仅可以为视觉带来更多炫酷的体验,也能够辅助图的分析。
图 1. 时序图的动画展示。
问题
有如下数据集,节点北京地铁线路的起点和终点;边代表一条地铁线路,其 controlPoints
字段是边的控制点集合:
{
"nodes": [{
"id": "0",
"x": 12955418.07889617,
"y": 4858516.455509626,
"class": "地铁二号线",
"name": "地铁二号线 0"
}, {
"id": "1",
"x": 12955418.07889617,
"y": 4858516.455509626,
"class": "地铁二号线",
"name": "地铁二号线 1"
},
...
],
"edges": [{
"id": "0",
"source": "0",
"target": "1",
"class": "地铁二号线",
"name": "地铁二号线 0",
"controlPoints": [{
"x": 12956158.028659772,
"y": 4858523.099945488
}, {
"x": 12956181.882943347,
"y": 4858523.313643505
}, {
"x": 12956333.569265882,
"y": 4858524.675979247
}, {
"x": 12956722.101097316,
"y": 4858528.426228019
}, {
"x": 12956892.09019568,
"y": 4858529.577087689
},
...
]},
...
]
}
如果使用 G6 简单地绘制默认样式,将会得到如下结果:
图 2. G6 渲染原始数据结果。
上图中的边被绘制成了直线,难以看出任何有用信息。
期待效果
我们希望可以利用 G6 绘制一个能够反映一定地理信息,且有交通动感的图。要实现下图效果,涉及到的 G6 主要功能是自定义节点、自定义边,在自定义中实现动画效果。
图 3. 期待效果图。
实现步骤
画布背景设置
为了实现荧光效果,并反映地理信息,我们给画布贴了一个暗色系的北京地图。
在 HTML 或 CSS 中设置 canvas
的 style
:
<style>
canvas{
background: rgb(0, 0, 0);
background-image: url("./assets/data/background-small.png");
background-size: 600px 600px;
}
</style>
自定义呼吸节点
节点的呼吸效果是通过为节点增加循环播放的渐大、渐淡的 Halo 实现的。
// 自定义呼吸节点名为 breath-node
G6.registerNode('breath-node', {
afterDraw(cfg, group) {
const r = cfg.size / 2;
const haloColor = cfg.color || (cfg.style && cfg.style.fill);
const back1 = group.addShape('circle', {
zIndex: -2,
attrs: {
x: 0,
y: 0,
r,
fill: haloColor,
opacity: 0.6
}
});
const back2 = group.addShape('circle', {
zIndex: -1,
attrs: {
x: 0,
y: 0,
r,
fill: haloColor,
opacity: 0.6
}
});
group.sort(); // 排序,根据zIndex 排序
const delayBase = Math.random() * 2000;
back1.animate({ // 逐渐放大,并消失
r: r + 10,
opacity: 0.0,
repeat: true // 循环
}, 3000, 'easeCubic', null, delayBase) // 无延迟
back2.animate({ // 逐渐放大,并消失
r: r + 10,
opacity: 0.0,
repeat: true // 循环
}, 3000, 'easeCubic', null, delayBase + 1000) // 1 秒延迟
}
}, 'circle');
自定义边
边的动画效果是通过在边上增加沿着边的路径移动的小圆点实现的。
G6.registerEdge('running-polyline', {
afterDraw(cfg, group) {
const shape = group.get('children')[0];
const length = shape.getTotalLength(); // 获取边的总长度
const startPoint = shape.getPoint(0);
let circleCount = Math.ceil(length / 20); // 根据边总长度计算该边上的小圆点数量
circleCount = circleCount === 0 ? 1 : circleCount;
// 生成小圆点
for (let i = 0; i < circleCount; i++) {
// 小圆点动画的随机延迟
const delay = Math.random() * 1000;
const start = shape.getPoint(i / circleCount);
// 加入小圆点
const circle = group.addShape('circle', {
attrs: {
x: start.x,
y: start.y,
r: 0.8,
fill: '#A0F3AF',
shadowColor: '#fff',
shadowBlur: 30,
}
});
// 小圆点的动画
circle.animate({
// 动画的每一帧,返回小圆点在该帧的位置,入参 ratio 是 [0, 1] 的比例值
onFrame(ratio) {
ratio += i / circleCount;
if (ratio > 1) {
ratio %= 1;
}
// 根据比例值获取在边上的位置
const tmpPoint = shape.getPoint(ratio);
return {
x: tmpPoint.x,
y: tmpPoint.y
};
},
repeat: true // 循环动画
}, 10 * length, 'easeCubic', null, delay);
}
}
}, 'polyline'); // 继承 polyline 折线
实例化图
在这一步中,我们在实例化图时,为之指定节点和边的类型(刚才自定义的 breath-node
和 running-polyline
)、节点样式、边样式。
const graphSize = [ 600, 600 ];
const graph = new G6.Graph({
container: 'mountNode',
width: graphSize[0],
height: graphSize[1],
defaultNode: {
shape: 'breath-node',
size: 3,
style: {
lineWidth: 0,
fill: 'rgb(240, 223, 83)'
}
},
defaultEdge: {
shape: 'running-polyline',
size: 1,
color: 'rgb(14,142,63)',
style: {
opacity: 0.4,
lineAppendWidth: 3
}
}
});
整理数据并渲染
这里使用了JQuery 读取文件中的数据。由于地理数据中的 y
坐标代表的是经度,其方向与画布的 y 轴方向相反,因此需要将其反向。下面代码还使用了 scaleNodesPoints
函数将节点和边归一化到之前定义的图大小 graphSize
。另外,我们根据边的聚类信息设置边的颜色。
// 边的颜色数组
const colors = [ 'rgb(64, 174, 247)', 'rgb(108, 207, 169)', 'rgb(157, 223, 125)',
'rgb(240, 198, 74)', 'rgb(221, 158, 97)', 'rgb(141, 163, 112)',
'rgb(115, 136, 220)', 'rgb(133, 88, 219)', 'rgb(203, 135, 226)',
'rgb(227, 137, 163)' ];
// 使用 JQuery 读取数据文件
$.getJSON('./assets/data/beijing-metro-lines.json', data => {
const nodes = data.nodes;
const edges = data.edges;
const classMap = new Map();
let classId = 0;
// 反向节点的 y 坐标
nodes.forEach(node => {
node.y = -node.y;
});
edges.forEach(edge => {
// 根据边的聚类信息设置边的颜色。同一条地铁线路使用同一种颜色
if (edge.class && classMap.get(edge.class) === undefined) {
classMap.set(edge.class, classId);
classId ++;
}
const cid = classMap.get(edge.class);
edge.color = colors[cid % colors.length];
const controlPoints = edge.controlPoints;
// 反向边控制点的 y 坐标
controlPoints.forEach(cp => {
cp.y = -cp.y;
});
})
scaleNodesPoints(nodes, edges, graphSize);
graph.data(data); // 为图实例配置数据源
graph.render(); // 渲染图
});
scaleNodesPoints
函数如下:
function scaleNodesPoints(nodes, edges, graphSize) {
const size = graphSize[0] < graphSize[1] ? graphSize[0] : graphSize[1];
let minX = 99999999999999999;
let maxX = -99999999999999999;
let minY = 99999999999999999;
let maxY = -99999999999999999;
nodes.forEach(node => {
if (node.x > maxX) maxX = node.x;
if (node.x < minX) minX = node.x;
if (node.y > maxY) maxY = node.y;
if (node.y < minY) minY = node.y;
});
edges.forEach(edge => {
const controlPoints = edge.controlPoints;
controlPoints.forEach(cp => {
if (cp.x > maxX) maxX = cp.x;
if (cp.x < minX) minX = cp.x;
if (cp.y > maxY) maxY = cp.y;
if (cp.y < minY) minY = cp.y;
});
});
const xScale = maxX - minX;
const yScale = maxY - minY;
nodes.forEach(node => {
node.orix = node.x;
node.oriy = node.y;
node.x = (node.x - minX) / xScale * size;
node.y = (node.y - minY) / yScale * size;
});
edges.forEach(edge => {
const controlPoints = edge.controlPoints;
controlPoints.forEach(cp => {
cp.x = (cp.x - minX) / xScale * size;
cp.y = (cp.y - minY) / yScale * size;
});
});
}
设置 edge-tooltip
使用 edge-tooltip,可以在鼠标 hover 到边上时展示该边的某些信息。首先在 HTML 中设定 tooltip 的样式:
<style>
.g6-tooltip {
border: 1px solid #e2e2e2;
border-radius: 4px;
font-size: 12px;
color: #545454;
background-color: rgba(255, 255, 255, 0.9);
padding: 10px 8px;
box-shadow: rgb(174, 174, 174) 0px 0px 10px;
}
</style>
然后,在上一步实例化 graph
时,增加一个名为 modes
的配置项到参数中,如下写法启动了 edge-tooltip
,在 formatText
函数中指定了 edge-tooltip
显示的文本内容:
modes: {
default: [{
type: 'edge-tooltip',
formatText(model) {
const text = model.class;
return text;
},
shouldUpdate: e => {
return true;
}
}]
}
这样,当鼠标移动到节点上时,带有该边所属地铁线路信息的 tooltip
将会出现:
edge-tooltip
最终效果
最终效果图。节点代表地铁线路的起点和终点。边的颜色代表不同的地铁线路。鼠标放置在边上出现有该边所属的地铁线路名的 edge-tooltip。
完整代码
自此,该案例完成。完整代码参见:Metro Animation Case。