〇. 概述
antv-x6 是基于 HTML 和 SVG 的图编辑引擎,提供低成本的定制能力和开箱即用的内置扩展,方便我们快速搭建 DAG (有向无环图)图、ER (实体联系模式图) 图、流程图、血缘图等应用。
✨ 特性:
- 🌱 极易定制:支持使用
SVG/HTML/React/Vue定制节点样式和交互; - 🚀 开箱即用:内置 10+ 图编辑配套扩展,如框选、对齐线、小地图等;
- 🧲 数据驱动:基于
MVC架构,用户更加专注于数据逻辑和业务逻辑; - 💯 事件驱动:可以监听图表内发生的任何事件。
本文档旨在使读者了解 antv-x6 常见用途及用法, 详细内容请参考官方文档:
antv-x6 官方文档: x6.antv.antgroup.com/
SVG 图像入门教程: www.ruanyifeng.com/blog/2018/0…
SVG - MDN: developer.mozilla.org/zh-CN/docs/…
antv-x6 使用布局: x6.antv.antgroup.com/temp/layout
一. 快速上手
1. 安装
npm install @antv/x6 --save
2. 初始化画布
在页面中创建一个画布容器,然后初始化画布对象,可以通过配置设置画布的样式,比如背景颜色。
<div id="container"></div>
// ...
import { Graph } from "@antv/x6";
// ...
const graph = new Graph({
container: document.getElementById("container"),
width: 800,
height: 600,
background: {
color: "#F2F7FA", // 指定画布背景色
},
});
// ...
3. 注册VUE节点
X6 支持使用Vue 组件来渲染节点
// 子组件 VueNode.vue
<template>
<div class="node">
<div class="text">{{nodeData.text}}</div>
</div>
</template>
<script>
export default {
name: "vueNode",
inject: ["getNode"], // x6使用依赖注入向vue节点传递数据
data() {
return {
nodeData: {},
};
},
created() {
this.nodeData = this.getNode()?.store?.data;
},
};
</script>
<style lang="less" scoped>
div {
box-sizing: border-box;
}
.node {
display: flex;
justify-content: center;
align-items: center;
width: 200px;
height: 90px;
background: #ffffff;
box-shadow: 0px 3px 6px 0px rgba(213, 224, 243, 0.5);
border-radius: 5px;
overflow: hidden;
border: 2px solid #dcdfe6;
.text {
font-size: 20px;
color: rebeccapurple;
}
}
</style>
// 父组件
// ...
import { register } from "@antv/x6-vue-shape";
import VueNode from "../components/VueNode.vue";
// ...
register({
shape: "vueNode",
component: VueNode,
});
// ...
4. 渲染节点和边
X6支持使用fromJSON()渲染JSON格式数据,该对象中nodes代表节点数据,edges代表边数据。
// ...
data () {
return {
graphData: {
nodes: [
{
id: 'node1',
shape: 'rect',
x: 40,
y: 40,
width: 100,
height: 40,
label: 'hello',
attrs: {
// body 是选择器名称,选中的是 rect 元素
body: {
stroke: '#8f8f8f',
strokeWidth: 1,
fill: '#fff',
rx: 6,
ry: 6
}
}
},
{
id: 'node2',
shape: 'rect',
x: 160,
y: 180,
width: 100,
height: 40,
label: 'world',
attrs: {
body: {
stroke: '#8f8f8f',
strokeWidth: 1,
fill: '#fff',
rx: 6,
ry: 6
}
}
},
{
id: 'node3',
shape: 'vueNode', // 支持渲染vue节点
x: 160,
y: 280,
text: 'helloVue'
}
],
edges: [
{
shape: 'edge',
source: 'node1',
target: 'node2',
label: 'x6',
attrs: {
// line 是选择器名称,选中的边的 path 元素
line: {
stroke: '#8f8f8f',
strokeWidth: 1
}
}
},
{
shape: 'edge',
source: 'node2',
target: 'node3',
attrs: {
// line 是选择器名称,选中的边的 path 元素
line: {
stroke: '#8f8f8f',
strokeWidth: 1
}
}
}
]
}, // 画布数据
}
}
// ...
graph.fromJSON(this.graphData) // 渲染元素
二. SVG基础
antv-x6 是基于
HTML和SVG的图编辑引擎 参考文档:SVG 图像入门教程:www.ruanyifeng.com/blog/2018/0…
SVG - MDN: developer.mozilla.org/zh-CN/docs/…
1. 概述
SVG是一种基于XML语法的图像格式,全称是可缩放矢量图Scalable Vector Graphics。其他图像格式都是基于像素处理的,SVG 则是属于对图像的形状描述,所以它本质上是文本文件,体积较小,且不管放大多少倍都不会失真。
SVG文件可以直接插入网页,成为DOM的一部分,然后用JavaScript和 CSS进行操作。
<!DOCTYPE html>
<html>
<head></head>
<body>
<svg
id="mysvg"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 800 600"
preserveAspectRatio="xMidYMid meet"
>
<circle id="mycircle" cx="400" cy="300" r="50" />
<svg>
</body>
</html>
SVG 代码也可以写在一个独立文件中,然后用<img>、<object>、<embed>、<iframe>等标签插入网页。
<img src="circle.svg" />
<object id="object" data="circle.svg" type="image/svg+xml"></object>
<embed id="embed" src="icon.svg" type="image/svg+xml" />
<iframe id="iframe" src="icon.svg"></iframe>
CSS也可以使用SVG文件。
.logo {
background: url(icon.svg);
}
SVG文件还可以转为BASE64编码,然后作为Data URI写入网页。
<img src="data:image/svg+xml;base64,[data]" />
2. 语法
<svg>标签
<svg width="100%" height="100%">
<circle id="mycircle" cx="50" cy="50" r="50" />
</svg>
<svg>的width属性和height属性,指定了SVG图像在HTML元素中所占据的宽度和高度。除了相对单位,也可以采用绝对单位(单位:像素)。如果不指定这两个属性,SVG图像默认大小是 300 像素(宽) x 150 像素(高)。 如果只想展示SVG 图像的一部分,就要指定viewBox属性
<svg width="100" height="100" viewBox="50 50 50 50">
<circle id="mycircle" cx="50" cy="50" r="50" />
</svg>
<viewBox>属性的值有四个数字,分别是左上角的横坐标和纵坐标、视口的宽度和高度。上面代码中,SVG图像是 100 像素宽 x 100 像素高,viewBox属性指定视口从(50, 50)这个点开始。所以,实际看到的是右下角的四分之一圆
注意,视口必须适配所在的空间。上面代码中,视口的大小是 50 x 50,由于SVG图像的大小是 100 x 100,所以视口会放大去适配SVG图像的大小,即放大了四倍。
<circle>标签
<circle>标签代表圆形。
<svg width="300" height="180">
<circle cx="30" cy="50" r="25" />
<circle cx="90" cy="50" r="25" class="red" />
<circle cx="150" cy="50" r="25" class="fancy" />
</svg>
上面的代码定义了三个圆。<circle>标签的cx、cy、r属性分别为横坐标、纵坐标和半径,单位为像素。坐标都是相对于<svg>画布的左上角原点。
class属性用来指定对应的CSS类。
.red {
fill: red;
}
.fancy {
fill: none;
stroke: black;
stroke-width: 3pt;
}
SVG的CSS属性与网页元素有所不同。
- fill:填充色
- stroke:描边色
- stroke-width:边框宽度
<rect>标签
标签用于绘制矩形。
<svg width="300" height="180">
<rect
x="0"
y="0"
height="100"
width="200"
style="stroke: #70d5dd; fill: #dd524b"
/>
</svg>
<rect>的x属性和y属性,指定了矩形左上角端点的横坐标和纵坐标;width属性和height属性指定了矩形的宽度和高度(单位像素)。
<path>标签
<svg width="300" height="180">
<path
d="
M 18,3
L 46,3
L 46,40
L 61,40
L 32,68
L 3,40
L 18,40
Z
"
></path>
</svg>
<path>的d属性表示绘制顺序,它的值是一个长字符串,每个字母表示一个绘制动作,后面跟着坐标。
- M:移动到(moveto)
- L:画直线到(lineto)
- Z:闭合路径
其他常用标签
除了上诉的标签, SVG还支持以下常用标签
<line>标签用来绘制直线
<polyline>标签用于绘制一根折线
<ellipse>标签用于绘制椭圆
<polygon>标签用于绘制多边形
<text>标签用于绘制文本
<use>标签用于复制一个形状
<g>标签用于将多个形状组成一个组(group)
<defs>标签用于自定义形状,它内部的代码不会显示
<pattern>标签用于自定义一个形状,该形状可以被引用来平铺一个区域
<image>标签用于插入图片文件
<animate>标签用于产生动画效果
<animate>标签对 CSS 的 transform 属性不起作用,如果需要变形,就要使用<animateTransform>标签
三. x6 基础概念
-
安装及相关插件
$ npm install @antv/x6 --save
2.x版本将部分功能拆分为插件形式, 请按需安装:
@antv/x6-plugin-clipboard // 剪切板功能
@antv/x6-plugin-history // 撤销重做功能
@antv/x6-plugin-keyboard // 快捷键功能
@antv/x6-plugin-minimap // 小地图功能
@antv/x6-plugin-scroller // 滚动画布功能
@antv/x6-plugin-selection // 框选功能
@antv/x6-plugin-snapline // 对齐线功能
@antv/x6-plugin-dnd // dnd 功能
@antv/x6-plugin-stencil // stencil 功能
@antv/x6-plugin-transform // 图形变换功能
@antv/x6-plugin-export // 图片导出功能
@antv/x6-react-components // 配套 UI 组件
@antv/x6-react-shape // react 渲染功能
@antv/x6-vue-shape // vue 渲染功能 需与x6使用同一个大版本
@antv/layout // 布局功能 x6可以使用antv公共的布局库
-
画布
在 X6 中,画布Graph 是图的载体,它包含了图上的所有元素(节点、边等),同时挂载了图的相关操作(如交互监听、元素操作、渲染等)。
初始化画布:
在页面中创建一个用于容纳 X6 绘图的容器,可以是一个 div 标签
<div id="container"></div>
创建一个 Graph 对象,并为其指定一个页面上的绘图容器,通常也会指定画布的大小
import { Graph } from "@antv/x6";
const graph = new Graph({
container: document.getElementById("container"),
width: 800,
height: 600,
background: {
color: "#F2F7FA",
},
});
如果希望画布对象撑满父容器, 可以使用autoResize
<!-- 注意,使用 autoResize 配置时,需要在画布容器外面再套一层宽高都是 100% 的外层容器,在外层容器上监听尺寸改变,当外层容器大小改变时,画布自动重新计算宽高以及元素位置。 -->
<div style="width:100%; height:100%">
<div id="container"></div>
</div>
const graph = new Graph({
container: document.getElementById("container"),
autoResize: true,
});
创建 Graph 对象时可以通过 background 和 grid 两个配置来设置画布的背景以及网格
// ...
const graph = new Graph({
...,
background: {
color: '#F2F7FA',
},
grid: {
visible: true,
type: 'doubleMesh',
args: [
{
color: '#eee', // 主网格线颜色
thickness: 1, // 主网格线宽度
},
{
color: '#ddd', // 次网格线颜色
thickness: 1, // 次网格线宽度
factor: 4, // 主次网格线间隔
},
],
},
})
// ...
画布的拖拽、缩放也是常用操作,Graph 中通过 panning 和 mousewheel 配置来实现这两个功能,鼠标按下画布后移动时会拖拽画布,滚动鼠标滚轮会缩放画布
const graph = new Graph({
...,
// 属性值可以时布尔值表示开启功能
panning: true, // 开启画布拖拽
// 也可以是详细配置的对象
mousewheel: {
enabled: true, // 开启滚轮缩放交互
zoomAtMousePosition: false, // 是否将鼠标位置作为中心缩放
modifiers: ['ctrl', 'meta'] // 修饰键
},
})
X6最吸引开发者的地方是具备非常完整的交互定制能力, 可以通过这些属性选择开启或关闭它们
const graph = new Graph({
...,
interacting: {
nodeMovable: false, // 节点是否可以被移动
magnetConnectable: false, // 当在具有 magnet 属性的元素上按下鼠标开始拖动时,是否触发连线交互
edgeMovable: false, // 边是否可以被移动
edgeLabelMovable: false, // 边的标签是否可以被移动
arrowheadMovable: false, // 边的起始/终止箭头(在使用 arrowhead 工具后)是否可以被移动
vertexMovable: false, // 边的路径点是否可以被移动
vertexAddable: false, // 是否可以添加边的路径点
vertexDeletable: false // 边的路径点是否可以被删除
}
})
-
节点
X6 支持使用 SVG、HTML 来渲染节点内容,在此基础上,我们还可以使用 React、Vue 组件来渲染节点,这样在开发过程中会非常便捷。在拿到设计稿之后,你就需要权衡一下使用哪一种渲染方式,可以参考下面的一些建议:
- 如果节点内容比较简单,而且需求比较固定,使用
SVG节点 - 其他场景,都推荐使用当前项目所使用的框架来渲染节点
React/Vue/HTML渲染方式也存在一些限制,因为浏览器的兼容性问题,有时会出现一些异常的渲染行为。主要表现形式为节点内容展示不全或者节点内容闪烁。可以通过一些方法规避,比如在节点内部元素的 css 样式中不要使用position:absolute、position:relative、tranform、opacity。
向画布添加节点
graph.addNode({
shape: "rect",
x: 100,
y: 40,
width: 100,
height: 40,
});
节点支持以下常用属性:
| 属性名 | 类型 | 默认值 | 描述 |
|---|---|---|---|
| id | string | 节点的唯一标识,推荐使用具备业务意义的 ID, 默认使用自动生成的 UUID。 | |
| shape | string | 渲染节点/边的图形。节点对应的默认值为 rect。 | |
| markup | Markup | 节点/边的 SVG/HTML 片段。 | |
| attrs | Attr.CellAttrs | 节点/边属性样式。类比css。 | |
| x | number | 0 | 节点位置 x 坐标,单位为 px。 |
| y | number | 0 | 节点位置 y 坐标,单位为 px。 |
| width | number | 1 | 节点宽度,单位为 px。 |
| height | number | 1 | 节点高度,单位为 px。 |
| 自定义的属性 | any | 自定义业务数据 |
上面使用 shape 来指定了节点的图形,X6 内置节点与 shape 名称对应关系如下表
| 构造函数 | shape 名称 | 描述 |
|---|---|---|
| Shape.Rect | rect | 矩形。 |
| Shape.Circle | circle | 圆形。 |
| Shape.Ellipse | ellipse | 椭圆。 |
| Shape.Polygon | polygon | 多边形。 |
| Shape.Polyline | polyline | 折线。 |
| Shape.Path | path | 路径。 |
| Shape.Image | image | 图片。 |
| Shape.HTML | html | HTML 节点,使用 foreignObject 渲染 HTML 片段。 |
-
使用 vue 节点
使用 vue 节点时的数据及事件交互:
vue节点组件可以通过注入x6提供的getNode获得节点对象, 并通过调用父组件提供的方法传递数据
子组件:
<template>
<div class="node">
<div class="text">{{nodeData.text}}</div>
<input type="text" v-model="inputText" />
<button @click="btnClick">click me</button>
</div>
</template>
<script>
export default {
name: "x6vueNode",
inject: ["getNode"], // x6使用依赖注入向vue节点传递数据
data() {
return {
nodeData: {},
inputText: "",
};
},
methods: {
btnClick() {
this.nodeData.foo(this.inputText); // 调用父组件方法
},
},
created() {
this.nodeData = this.getNode()?.store?.data;
},
};
</script>
<style lang="less" scoped>
div {
box-sizing: border-box;
}
.node {
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
width: 200px;
height: 90px;
background: #ffffff;
box-shadow: 0px 3px 6px 0px rgba(213, 224, 243, 0.5);
border-radius: 5px;
overflow: hidden;
border: 2px solid #dcdfe6;
input {
width: 60%;
border: solid 2px #666;
border-radius: 5px;
}
.text {
font-size: 20px;
color: rebeccapurple;
}
}
</style>
父节点:
import { register } from "@antv/x6-vue-shape";
import VueNode from "../components/VueNode.vue";
// ...
// 注册vue节点
register({
shape: "vueNode",
component: VueNode,
});
// ...
// 添加vue节点:
this.graph.addNode({
id: "node3",
shape: "X6VueNode", // 支持渲染vue节点
x: 160,
y: 280,
text: "helloVue",
foo: this.foo, // 向节点暴露的方法
});
-
边
向画布添加边
graph.addEdge({
shape: "edge",
source: "node1",
target: "node2",
});
边支持以下常用属性:
| 属性名 | 类型 | 描述 |
|---|---|---|
| id | string | 边的唯一标识,推荐使用具备业务意义的 ID, 默认使用自动生成的 UUID。 |
| shape | string | 渲染节点/边的图形。节点对应的默认值为 rect。 |
| markup | Markup | 节点/边的 SVG/HTML 片段。 |
| attrs | Attr.CellAttrs | 节点/边属性样式。类比css。可用来定制箭头和边的样式。 |
| source | TerminalData | 源节点或起始点。 |
| target | TerminalData | 目标节点或目标点。 |
| vertices | Point.PointLike[] | 路径点。 |
| router | RouterData | 路由。 |
| connector | ConnectorData | 连接器。 |
| labels | Label[] | 标签。 |
| defaultLabel | Label | 默认标签。 |
边的源和目标节点支持以下属性
source: rect1, // 源节点对象
source: 'rect1', // 源节点 ID
source: { cell: rect1, port: 'out-port-1' }, // 源节点和连接桩 ID
source: { x: 100, y: 120 } // 坐标点
边支持添加路径点vertices。边从起始点开始,按顺序经过路径点,最后到达终止点
graph.addEdge({
source: rect1,
target: rect2,
vertices: [
{ x: 100, y: 200 },
{ x: 300, y: 120 },
],
});
路由 router 将对 vertices 进一步处理,并在必要时添加额外的点,然后返回处理后的点
例如,经过orth路由处理后,边的每一条链接线段都是水平或垂直的
graph.addEdge({
source: rect1,
target: rect2,
vertices: [
{ x: 100, y: 200 },
{ x: 300, y: 120 },
],
// 如果没有 args 参数,可以简写为 router: 'orth'
router: {
name: "orth",
args: {},
},
});
X6默认提供了以下几种路由:
| 路由名称 | 说明 | 图例 |
|---|---|---|
| normal | 默认路由,原样返回路径点。 | |
| orth | 正交路由,由水平或垂直的正交线段组成。 | |
| oneSide | 受限正交路由,由受限的三段水平或垂直的正交线段组成。 | |
| manhattan | 智能正交路由,由水平或垂直的正交线段组成,并自动避开路径上的其他节点(障碍)。 | |
| metro | 智能地铁线路由,由水平或垂直的正交线段和斜角线段组成,类似地铁轨道图,并自动避开路径上的其他节点(障碍)。 | |
| er | 实体关系路由,由 Z 字形的斜角线段组成。 |
连接器 connector 将路由 router 返回的点加工成渲染边所需要的pathData。例如,rounded 连接器将连线之间的倒角处理为圆弧倒角
graph.addEdge({
source: rect1,
target: rect2,
vertices: [
{ x: 100, y: 200 },
{ x: 300, y: 120 },
],
router: "orth",
// 如果没有 args 参数,可以简写写 connector: 'rounded'
connector: {
name: "rounded",
args: {},
},
});
X6默认提供了以下几种连接器
| 连接器 | 说明 | 图例 |
|---|---|---|
| normal | 简单连接器,用直线连接起点、路由点和终点。 | |
| smooth | 平滑连接器,用三次贝塞尔曲线线连接起点、路由点和终点。 | |
| rounded | 圆角连接器,用直线连接起点、路由点和终点,并在线段连接处用圆弧链接(倒圆角)。 | |
| jumpover | 跳线连接器,用直线连接起点、路由点和终点,并在边与边的交叉处用跳线符号链接。 |
labels 用于设置标签文本、位置、样式等。通过数组形式支持多标签
const edge = graph.addEdge({
source: rect1,
target: rect2,
labels: ["edge"], // 通过 labels 可以设置多个标签
});
-
连接桩
连接桩可以用于边的源节点和目标点, 可以以优雅的交互添加边链接节点
-
布局
x6 可以使用 antv 公共的布局库 @antv/layout
安装布局插件:
npm install @antv/layout --save
可以使用GridLayout对节点数据进行处理, GridLayout使用相应算法为节点添加坐标
// ...
import { DagreLayout } from "@antv/layout";
// ...
// 布局所需要的数据格式(需要确定节点的大小, 不需要节点xy坐标)
const model = {
nodes: [
{
id: "node1",
size: {
width: 30,
height: 40,
},
},
{
id: "node2",
size: {
width: 30,
height: 40,
},
},
],
edges: [
{
source: "node1",
target: "node2",
},
],
};
const gridLayout = new DagreLayout({
type: "dagre", // 布局类型
align: undefined, // 节点对齐方式
begin: [0, 0], // 布局左上角对齐位置
rankdir: "TB", // 布局的方向
nodesep: "180", // 节点间距
ranksep: "60", // 层间距
});
const newModel = gridLayout.layout(model);
graph.fromJSON(newModel);
常用的布局类型:
-
网格布局: grid
-
-
环形布局: circular
-
层次布局: dagre
每种布局创建时传入的配置不同, 具体请参考文档: x6.antv.antgroup.com/temp/layout
-
常用 API
可以直接用的:
graph.centerContent(); // 画布居中
graph.zoom(0.2); // 画布缩放
graph.zoomTo(1); // 画布缩放到
graph.zoomToFit({ maxScale: 2, padding: 20 }); // 撑满画布, maxScale 配置最大缩放级别, padding 配置内边距
graph.fromJSON(jsonData); // 使用json数据渲染画布数据
graph.toJSON(); // 将节点/边的结构化数据转换为 JSON 数据
撤销和重做:
graph.undo(); // 撤销
graph.redo(); // 重做
通过history:change事件监听画布操作, 控制是否可以撤销 | 重做
this.graph.on("history:change", () => {
this.state = {
canRedo: this.graph.canRedo(),
canUndo: this.graph.canUndo(),
};
});
导出 svg :
graph.exportSVG(fileName, options); // 导出svg
fileName 为文件名称,缺省为 chart,options 描述如下:
| 属性名 | 类型 | 默认值 | 描述 |
|---|---|---|---|
| preserveDimensions | boolean | Size | preserveDimensions 用来控制导出 svg 的尺寸, 如果不设置,width 和 height 默认为 100%;如果设置为 true, width 和 height 会自动计算为图形区域的实际大小 |
| viewBox | Rectangle.RectangleLike | - | 设置导出 svg 的 viewBox |
| copyStyles | boolean | true | 是否复制外部样式表中的样式,默认是 true。开启 copyStyles 后,在导出过程中因为需要禁用所有样式表,所以页面可能会出现短暂的样式丢失现象。如果效果特别差,可以将 copyStyles 设置为 false (亲测即使开启 copyStyles 也会丢失样式, 可配合下面的 stylesheet 选项解决) |
| stylesheet | string | - | 自定义样式表 (亲测支持 css 扩展语言, 譬如 less) |
| serializeImages | boolean | true | 是否将 image 元素的 xlink:href 链接转化为 dataUri 格式 |
| beforeSerialize | (this: Graph, svg: SVGSVGElement) => any | - | 可以在导出 svg 字符串之前调用 beforeSerialize 来修改它 |
经实测发现
exportSVG仅会导出可见部分画布, 所以导出前可能需要将画布缩放到全部可见:graph.zoomToFit()
导出图片:
graph.exportPNG(fileName, options); // 导出png
graph.exportJPEG(fileName, options); // 导出jpg
fileName 为文件名称, 缺省为 chart, options 除了继承上面exportSVG的options外还支持以下属性:
| 属性名 | 类型 | 默认值 | 描述 |
|---|---|---|---|
| width | number | - | 导出图片的宽度 |
| height | number | - | 导出图片的高度 |
| backgroundColor | string | - | 导出图片的背景色 |
| padding | NumberExt.SideOptions | - | 图片的 padding |
| quality | number | - | 图片质量,可以从 0 到 1 的区间内选择图片的质量。如果超出取值范围,将会使用默认值 0.92 |
经实测发现
exportPNG和exportJPEG会导出画布全部部分, 但清晰度和当前页面展示的一致, 所以导出前可能需要将画布缩放到清晰的倍数:graph.zoomTo(1)