官方文档:x6.antv.vision/zh/docs/tut…
Ps:需求驱动式阅读文档
What —— antv-X6是什么
antv-x6是蚂蚁集团数据可视化团队开发维护的antv旗下的一款图编辑引擎,提供了一系列开箱即用的交互组件和简单易用的节点定制能力,方便我们快速搭建DAG 图、ER 图、流程图等应用。
其在画布中具有平移、缩放、对齐、居中、小地图等功能,基于 SVG SMIL Animation 实现了元素动画,可以精准定义元素的运动路径,X6 完全复用了 G6 的成熟布局能力,并做了数据格式的适配,在 X6 也可以很方便的使用布局功能。
X6 是图编辑引擎,特点是节点、边、等元素的定制能力非常强,经常用来构建流程图、ER图、DAG图、脑图等应用。G6 和 X6 是孪生兄弟,G6 更擅长于图可视化和图分析领域。
拓展:X6 与 G6 的区别
区别主要体现在以下几个方面
- 应用场景
- 数据量大小
- 定制能力和上手成本
- 是否需要支持移动端/小程序
- 是否需要统计图表节点
Why —— 为什么要使用antv-X6
功能需求:绘制一张展示产品版本分支之间关系的分支树图,可以灵活移动各部分,版本分支展示动画流向,可以保存移动后的布局。 示例如下图:

antv-X6可提供:画布平移、缩放;节点移动;Dagre布局;正交路由;动画 ...
How —— 如何在vue中使用antv-X6
安装
通过 npm 命令分别安装 X6和布局算法
$ npm install @antv/x6 --save
$ npm install @antv/layout --save
PS:使用该命令可能会报无法识别 @...的错误,此时需要给其添加引号'@...'
开始使用
1. 创建容器
在页面中创建一个用于容纳 X6 绘图的容器,可以是一个 div 标签
<div id="container"></div>
2. 准备节点数据
X6 支持 JSON 格式数据,该对象中需要有节点 nodes 和边 edges 字段,分别用数组表示:
nodeInfo:{
// 节点
nodes: [
{
id: 'node1', // String,可选,节点的唯一标识
label: 'hello', // String,节点标签
},
{ id: 'node2', // String,节点的唯一标识
label: 'world', // String,节点标签
},
],
// 边
edges: [
{ source: 'node1', // String,必须,起始节点 id
target: 'node2', // String,必须,目标节点 id
lineType: 'solid'//Sting,可选,用来定义线的样式
},
],
}
PS:在项目实际应用中是通过接口请求获取节点相关信息
3. 画布初始化及渲染
创建一个 Graph 对象,并为其指定一个页面上的绘图容器,并初始化画布
import { Graph,Vector } from "@antv/x6";
import { DagreLayout } from "@antv/layout";
//画布初始化
init() {
//创建一个 Graph 对象
this.graph = new Graph({
container: document.getElementById("container"),
width: 800, //画布容器宽度
height: 600, //画布容器高度
mousewheel: true, //滚轮缩放
panning: true //支持平移拖拽
});
//定义层次布局Dagre
const dagreLayout = new DagreLayout({
type: "dagre", //布局类型
rankdir: "LR", //布局的方向。T:top(上);B:bottom(下);L:left(左);R:right(右)
align: "UL", //节点对齐方式。U:upper(上);D:down(下);L:left(左);R:right(右);undefined (居中)
ranksep: 50, //层间距(px)。在 rankdir 为 TB 或 BT 时是竖直方向相邻层间距;在 rankdir 为 LR 或 RL 时代表水平方向相邻层间距
nodesep: 40, //节点间距(px)。在 rankdir 为 TB 或 BT 时是节点的水平间距;在 rankdir 为 LR 或 RL 时代表节点的竖直方向间距
controlPoints: true, //是否保留布局连线的控制点
});
//分别动态添加节点和边的样式
this.nodeInfo.nodes.map((item) => {
Object.assign(item, { width: 130, height: 40 });
});
this.nodeInfo.edges.map((item) => {
if (item.lineType === "solid") {
Object.assign(item, {
//路由将边的路径点 vertices 做进一步转换处理
router: {
name: "manhattan", //智能正交路由
args: {
startDirections: ["right"], // 支持从哪些方向开始路由
endDirections: ["left"], // 支持从哪些方向结束路由
},
},
//定义实线样式
attrs: {
line: { stroke: "#0e639c" }, // line 指代的元素代表了边的主体
},
});
} else if (item.lineType === "dotted") {
Object.assign(item, {
router: {
name: "manhattan", //智能正交路由,由水平或垂直的正交线段组成,并自动避开路径上的其他节点(障碍)
args: {
startDirections: ["bottom"], // 支持从哪些方向开始路由
endDirections: ["left"], // 支持从哪些方向结束路由
},
},
//定义虚线样式
attrs: {
line: { strokeDasharray: "5 5", stroke: "#51ff51" }, // line 指代的 元素代表了边的主体
},
});
}
});
//判断是否需要dagre布局(通过控制dagreLayout字段)
if (this.isDagreLayOut) {
//应用dagre布局
const model = dagreLayout.layout(this.nodeInfo);
//渲染画布
this.graph.fromJSON(model);
} else {
this.graph.fromJSON(this.nodeInfo);
}
}
其中布局使用的是Dagre布局,路由使用的是manhattan智能正交路由,由水平或垂直的正交线段组成,并自动避开路径上的其他节点(障碍)
渲染出来的效果:

4.给节点设置定时高亮
flash(cell){
//根据节点/边ID或实例查找对应的视图
const cellView = this.graph.findViewByCell(cell)
if(cellView){
//高亮滤镜,高亮指定的元素
cellView.highlight()
//取消高亮指定的元素
window.setTimeout(function () {
cellView.unhighlight();
}, 300);
}
}
5. 实现动画效果(点击节点后从父节点流动到该节点)
animation(){
//监听节点的点击事件
this.graph.on('node:mousedown',({cell}) => {
//trigger触发signer事件
this.graph.trigger('signer',cell)
})
//监听signer事件
this.graph.on("signal", (cell) => {
if (cell.isEdge()) {
//根据节点/边ID或实例查找对应的视图
const view = this.graph.findViewByCell(cell);
if (view) {
// Vector.create创建一个Vector对象
const token = Vector.create("circle", { r: 6, fill: "#feb662" }); // 获取边的起始节点/边 ⭐
const source= cell.getSourceCell(); //根据提供的选择器,获取指定根元素的第一个匹配的后代元素
const path = view.findOne('path')
if(path){
setTimeout(() => {
if(source){
//触发一个沿 SVGPathElement 路径元素运动的动画
token.animateAlongPath(
{
dur: '4s',
repeatCount: 'indefinite'
},
path
)
token.appendTo(path.parentNode)
this.graph.trigger('signal',source)
}
}, 300);
}
}else {
//给被点击的节点设置定时高亮
this.flash(cell); //获取与节点/边相连接的边
const edges = graph.model.getConnectedEdges(cell, {
// outgoing: true
incoming: true //返回输入边
});
edges.forEach((edge) => graph.trigger("signal", edge));
}
});
}
PS:使用animateAlongPath的时候需要注意antv/x6的版本不能是2.x以上的
"@antv/x6": "^1.34.3",
动画实现效果:

该动画效果使用的animateAlongPath沿路径运动的动画 | X6 (antv.vision),还可以考虑通过sendToken实现
沿边运动的动画 | X6 (antv.vision)
在2.x版本中,动画相关的一些api被删除了,升级后的文档中还没有提供
但是由于一些原因,项目中的antv-X6版本需要升级到2.x,本来是想创建一个html元素来实现圆球的动画,考虑到图形的缩放和平移后发现使用svg元素更合适,
因此动画的实现方式需要采用其他方式(创建svg元素 +CSS属性 offset-path)
PS:参考新 CSS 属性 offset-path 使元素沿着不规则路径运动
将路由调整为地铁路由metro (曼哈顿路由 manhattan 的一个变种),去掉了args配置,让箭头指向可以随节点位置变化而变化,还去掉了消除的高亮的定时器
更新后的 animation()方法:
animation() {
//监听节点的点击事件
this.graph.on("node:dblclick", ({ node }) => {
console.log("点击节点");
//trigger触发signer事件
this.graph.trigger("signal", node);
});
//监听signer事件
this.graph.on("signal", (cell) => {
console.log("cell", cell);
if (cell.isEdge()) {
//根据节点/边ID或实例查找对应的视图
const view = this.graph.findViewByCell(cell);
console.log("view", view);
if (view) {
//创建g元素
let g = document.getElementsByClassName("x6-graph-svg-viewport")[0];
let circle = document.createElementNS(
"http://www.w3.org/2000/svg",
"circle"
);
circle.setAttribute("fill", "#feb662");
circle.setAttribute("id", "circle");
g.appendChild(circle);
const source = cell.getSourceCell(); //根据提供的选择器,获取指定根元素的第一个匹配的后代元素
const path = view.findOne("path");
if (path) {
setTimeout(() => {
if (source) {
console.log("path", path);
let s = new XMLSerializer();
let str = s.serializeToString(path);
let d = str.split('d="')[1].split('"')[0];
circle.setAttribute("r", "5");
circle.style.offsetPath = `path("${d}")`;
circle.style.animation = "move 1s linear infinite";
this.graph.trigger("signal", source);
}
}, 1000);
}
}
} else {
//给被点击的节点设置定时高亮
this.flash(cell); //获取与节点/边相连接的边
const edges = this.graph.model.getConnectedEdges(cell, {
// outgoing: true
incoming: true, //返回输入边
});
edges.forEach((edge) => this.graph.trigger("signal", edge));
}
});
},
替换后的效果:

结束了吗?其实还没有
还需要考虑到动画的暂停等问题 需求也发生了变更并且产生了一些优化需求
【续集】:在vue项目里用antv-X6来生成一张版本分支图(二)
【项目地址】:antv-X6实现版本分支图 (gitee.com)