X6 是 AntV 旗下的图编辑引擎,提供了一系列开箱即用的交互组件和简单易用的节点定制能力,方便我们快速搭建流程图、DAG 图、ER 图等图应用。
X6 图表的构成要素
graph TD
容器Graph --> 管理承载
管理承载 --> 节点Node
管理承载 --> 边Edge
节点Node --> 基础属性[x,y,width,height,angle]
节点Node --> 样式定制[attrs,markup,选择器]
节点Node --> 连接桩[Ports]
边Edge --> 连接目标[source,target]
边Edge --> 路径控制[vertices,router,connector]
边Edge --> 装饰[labels,marker]
增强插件 --> 常用插件[Snapline,History,Selection]
增强插件 --> 节点工具[Tools,删除按钮,编辑按钮]
增强插件 --> 增强交互
增强交互 --> 容器Graph
增强交互 --> 节点Node
增强交互 --> 边Edge
画布Graph
AntV X6 的画布(Graph)是构建图编辑应用的起点和核心容器。它负责管理、渲染画布上的所有元素(如节点和边),并提供了丰富的配置选项和 API 来控制画布本身的行为与外观
创建与基础配置
首先需要在页面中准备一个容器元素(如 div),然后通过实例化 Graph 类来初始化画布
<div id="container"></div>
配置挂载点,背景,画布尺寸,网格
import { Graph } from '@antv/x6';
const graph = new Graph({
// 挂载点:必须指定
container: document.getElementById('container'),
// 画布尺寸:可以固定,也可以不设置以自适应容器
width: 800,
height: 600,
// 背景配置
background: {
color: '#F2F7FA', // 设置背景颜色
// 也可以配置为图片
},
// 网格配置
grid: {
visible: true, // 显示网格
size: 10, // 网格大小,也是移动的最小单位
type: 'doubleMesh', // 网格类型:dot, fixedDot, mesh, doubleMesh
args: [
{ color: '#eee', thickness: 1 }, // 主网格线
{ color: '#ddd', thickness: 1, factor: 4 } // 次网格线
],
},
});
画布尺寸控制方式
- 固定尺寸:在
Graph配置中显式设置width和height属性。 - 容器尺寸:不设置
width和height,画布将自动撑满container元素的大小。 - 自适应容器:设置
autoResize: true。当容器大小发生变化时(例如浏览器窗口改变或侧边栏折叠),画布会自动调整大小。注意:使用此功能时,建议在container外层套一个宽高为 100% 的父容器以便监听尺寸变化。
交互与行为配置
画布平移
允许用户通过鼠标拖拽来移动视口。
const graph = new Graph({
panning: {
enabled: true,
eventTypes: ['leftMouseDown', 'rightMouseDown', 'mouseWheel'], // 触发方式
},
});
滚轮缩放
允许用户通过滚动滚轮来缩放画布
const graph = new Graph({
mousewheel: {
enabled: true,
zoomAtMousePosition: true, // 是否以鼠标位置为中心进行缩放
factor: 1.1, // 缩放因子
},
});
节点对齐
通过插件实现,在拖动节点时显示对齐线,辅助排版
import { Snapline } from '@antv/x6';
graph.use(new Snapline({ enabled: true }));
数据渲染与导出
渲染数据
使用 graph.fromJSON(data),其中 data 对象包含 nodes 和 edges 数组。
const data = {
nodes: [ /* 节点定义 */ ],
edges: [ /* 边定义 */ ],
};
graph.fromJSON(data);
graph.centerContent(); // 将画布内容居中显示
导出数据
使用 graph.toJSON() 可以将当前画布上的所有元素序列化为 JSON 对象,便于存储或传输。
常用API
graph.resize(width, height):手动调整画布尺寸。graph.translate(tx, ty):平移画布视口。graph.zoom(factor)/graph.zoomTo(level):缩放画布。graph.zoomToFit(options?):自动缩放并平移,使所有图元适配视口大小。graph.centerContent():将画布内容的中心点对齐到视口的中心。graph.addNode(nodeMetadata)/graph.addEdge(edgeMetadata):动态添加节点或边。
图形元素
基类Cell
抽象基类,定义了所有图元素(节点和边)共有的属性和行为。可以把它想象成图元素的“基因”,无论最终是节点还是边,都继承了这些核心能力。
核心作用
- 统一数据模型:无论是节点还是边,在内部都被视为
Cell的实例,方便画布(Graph)进行统一管理和操作。 - 定义通用接口:提供通用方法
关键属性
| 属性 | 描述 | 作用 |
|---|---|---|
id | 节点的唯一标识 | 推荐使用有业务意义的 ID,便于数据追踪。 |
markup | SVG/HTML 结构的 JSON 描述 | 定义了元素由哪些基本图形(如 <rect>, <circle>, <text>)组成。这是自定义形状的基础。 |
attrs | 样式属性对象 | 通过 CSS 选择器或预定义的“选择器(selector)”来精细化控制 markup 中每个部分的样式(如颜色、边框、文字内容)。 |
visible | 是否可见 | 控制元素的显示与隐藏。 |
data | 自定义业务数据 | 用于存储与该元素相关的任何业务数据,而不影响渲染逻辑。 |
zIndex | 层级 | 控制节点在画布上的层叠顺序。 |
节点Node
“有形状、有位置、可以附着连接点”的视觉实体。它不仅仅是画布上的一个方块,更是一个高度可配置的数据模型。
核心属性
基础属性
interface NodeMetadata {
id: string; // 唯一标识
markup?: Markup; // SVG/HTML 结构定义
attrs?: Attrs; // 样式属性(支持 CSS 选择器)
data?: any; // 自定义业务数据
visible?: boolean; // 可见性
zIndex?: number; // 层级
}
位置属性(特有)
position: { x: number, y: number }
中心点对齐:默认情况下,节点的 position 是指节点的左上角。如果希望基于中心点定位,需要手动计算:
```ts
// 让节点的中心点在 (200, 150)
node.position(
node.getPosition().x - node.getSize().width / 2,
node.getPosition().y - node.getSize().height / 2
);
```
尺寸属性(特有)
size: { width: number, height: number }
定义节点的最小包围盒。
对于特殊形状(如圆形),width === height 通常表示直径。
```js
// 矩形
node.size(100, 50);
// 圆形
node.size(60, 60); // 直径 60px
```
连接桩
ports 是 Node 上专门用于连接边的锚点集合。通过 Ports,你可以精确控制边的连接位置(如“右侧中间”、“左上角”等)。
Port 的结构
interface PortMetadata {
id?: string; // 连接桩唯一标识
group?: string; // 所属组(用于批量设置样式)
args?: Partial<PortArgs>; // 连接点计算参数
markup?: Markup; // 连接桩的独立 SVG 结构
attrs?: Attrs; // 连接桩的独立样式
position?: Position; // 定位策略(内置或自定义)
}
定位策略(position)
ports: {
groups: {
// 定义一组连接桩
'top': {
position: 'top', // 顶部均匀分布
attrs: { circle: { fill: '#87e8de' } }
},
'bottom': {
position: 'bottom', // 底部均匀分布
attrs: { circle: { fill: '#ffa39e' } }
}
},
items: [
{ id: 'port1', group: 'top' }, // 顶部连接桩
{ id: 'port2', group: 'bottom' }, // 底部连接桩
{
id: 'port3',
position: { name: 'absolute', args: { x: 10, y: 20 } } // 绝对定位
}
]
}
核心方法与操作
位置与变换
// 获取位置
const pos = node.getPosition(); // { x, y }
const bbox = node.getBBox(); // 边界框 { x, y, width, height }
// 设置位置
node.setPosition(150, 200);
node.translate(10, -5); // 相对移动
// 旋转
node.rotate(45); // 旋转 45 度
const angle = node.getAngle();
样式更新
// 直接修改 attrs
node.attr('body/fill', '#ff4d4f'); // X6 路径语法
node.attr('title/text', '新标题');
// 批量修改
node.attr({
body: { stroke: '#faad14' },
status: { fill: '#52c41a' }
});
连接桩操作
// 添加连接桩
node.addPort({
id: 'new-port',
group: 'bottom',
attrs: { circle: { r: 6 } }
});
// 获取连接桩
const ports = node.getPorts();
const port = node.getPort('port-id');
// 删除连接桩
node.removePort('port-id');
高级特性
组合节点(Node Composition)
X6 支持将多个节点组合成一个逻辑整体:
const group = graph.addNode({
shape: 'rect',
x: 100,
y: 100,
width: 300,
height: 200,
isGroup: true, // 标记为组合节点
});
// 将子节点放入组合
const child = graph.addNode({
shape: 'rect',
x: 150,
y: 150,
width: 80,
height: 40,
parent: group, // 指定父节点
});
节点动画
通过 transition 让节点动起来:
node.transition('position', { x: 300, y: 300 }, {
delay: 1000,
duration: 2000,
timing: 'ease-in-out',
});
node.transition('attrs/body/fill', '#ff4d4f', {
duration: 500,
});
工具(Tools)
为节点添加交互工具:
// 添加删除按钮
node.addTools({
name: 'button-remove',
args: { x: '100%', y: 0, offset: { x: -20, y: 20 } }
});
// 添加边界编辑器
node.addTools('boundary');
边Edge
核心属性
连接属性
source 和 target:定义边的两端
// 最简单的形式:直接连接节点
{
source: 'node1', // 节点的 ID
target: 'node2'
}
// 指定连接桩(Port)
{
source: { cell: 'node1', port: 'out-port1' }, // 从 node1 的 out-port1 出发
target: { cell: 'node2', port: 'in-port2' } // 连接到 node2 的 in-port2
}
// 指定连接点(坐标)
{
source: { x: 100, y: 100 }, // 从画布上的点出发
target: { cell: 'node2' } // 连接到节点
}
// 指定连接位置(百分比)
{
source: {
cell: 'node1',
anchor: { name: 'center' } // 锚点策略:中心点
},
target: {
cell: 'node2',
anchor: { name: 'right' } // 锚点策略:右侧中间
}
}
路径控制属性
vertices: 中间路径点,定义边必须经过的中间点,实现绕行。
{
source: 'node1',
target: 'node2',
vertices: [
{ x: 200, y: 150 }, // 经过 (200,150)
{ x: 300, y: 200 } // 然后经过 (300,200)
]
}
router: 路由策略, 自动计算路径,避开节点或沿特定方向。
{
router: {
name: 'manhattan', // 曼哈顿路由(避开障碍物,只走水平/垂直线)
args: {
padding: 20, // 与节点的间距
step: 10 // 网格大小
}
}
}
// 其他内置 router
// - 'orth' : 正交路由(水平/垂直,不一定避开障碍)
// - 'metro' : 地铁线路风格
// - 'normal' : 普通直线
// - 'oneside' : 从节点一侧引出
connector: 连接器,定义路径点之间的连线方式。
{
connector: {
name: 'smooth', // 平滑曲线
args: {
direction: 'H' // 水平方向优先
}
}
}
// 其他内置 connector
// - 'straight' : 直线连接(默认)
// - 'rounded' : 圆角折线
// - 'jumpover' : 跨线(用于电路图)
装饰属性
箭头(Arrowhead)
{
attrs: {
line: {
stroke: '#1890ff',
strokeWidth: 2,
targetMarker: { // 终点箭头
name: 'block', // 箭头类型:block, classic, diamond, circle 等
size: 8,
fill: '#1890ff'
},
sourceMarker: { // 起点箭头
name: 'circle',
r: 4
}
}
}
}
标签(Label)
边上的文字标签:
{
labels: [
{
attrs: {
text: { text: '关系名称' },
label: { fill: '#333' }
},
position: 0.5 // 位于边的 50% 位置
}
]
}
// 或者通过 markup/attrs 方式
{
attrs: {
label: {
text: '关系名称',
fill: '#333',
fontSize: 12,
refX: 0.5, // 相对于边的位置
refY: -10 // 偏移量
}
}
}
核心方法与操作
连接操作
// 获取连接的节点
const sourceNode = edge.getSourceNode();
const targetNode = edge.getTargetNode();
// 修改连接
edge.setSource('node3');
edge.setTarget({ cell: 'node4', port: 'port2' });
// 获取连接点坐标
const sourcePoint = edge.getSourcePoint(); // { x, y }
const targetPoint = edge.getTargetPoint();
路径操作
// 获取/设置路径点
const vertices = edge.getVertices();
edge.setVertices([
{ x: 150, y: 150 },
{ x: 250, y: 150 }
]);
// 获取完整路径(所有点,包括起点终点)
const points = edge.getPoints();
// 获取路径的 SVG 字符串
const pathData = edge.getPathData();
标签操作
// 添加标签
edge.appendLabel({
attrs: {
text: { text: '新标签' },
label: { fill: 'red' }
},
position: 0.3 // 30% 位置
});
// 获取/设置标签
const labels = edge.getLabels();
edge.setLabelAt(0, { text: '修改标签' });
// 删除标签
edge.removeLabelAt(1);
样式更新
// 修改边样式
edge.attr('line/stroke', '#ff4d4f');
edge.attr('line/strokeDash', [5, 3]); // 虚线
// 修改箭头
edge.attr('line/targetMarker', {
name: 'diamond',
size: 10,
fill: '#faad14'
});
高级特性
边动画
让边“动起来”,表达数据流或处理过程:
// 添加流动的蚂蚁线
edge.attr('line/strokeDash', [10, 10]);
edge.attr('line/style', {
animation: 'ant-line 30s infinite linear'
});
// 使用 X6 内置动画
edge.animate('attrs/line/stroke', '#52c41a', {
delay: 1000,
duration: 500,
direction: 'alternate',
iterations: 3
});
// 移动的小圆点
const circle = edge.getView().find('animateDot');
circle.animate(
{ startOffset: '100%' },
{ duration: 3000, repeat: true }
);
边与交互
// 边点击事件
edge.on('click', () => {
console.log('边被点击');
edge.attr('line/stroke', '#ff4d4f');
});
// 高亮经过的边
edge.on('mouseenter', () => {
edge.attr('line/strokeWidth', 4);
});
edge.on('mouseleave', () => {
edge.attr('line/strokeWidth', 2);
});
// 添加工具按钮
edge.addTools({
name: 'button',
args: {
markup: [
{
tagName: 'circle',
selector: 'button',
attrs: {
r: 12,
fill: '#fff',
stroke: '#1890ff',
cursor: 'pointer'
}
},
{
tagName: 'text',
selector: 'icon',
attrs: {
text: '×',
fill: '#1890ff',
fontSize: 14,
textAnchor: 'middle',
textVerticalAnchor: 'middle',
cursor: 'pointer'
}
}
],
onClick({ edge }) {
edge.remove(); // 点击删除边
}
}
});
// 定义状态样式
edge.setStates({
highlight: { attrs: { line: { strokeWidth: 4 } } },
error: { attrs: { line: { stroke: '#ff4d4f' } } }
});
```
vue3+AntV X6简易实现可拖拽流程图
1. 模板部分 (Template)
<template>
<div class="x6-container">
<!-- 左侧节点库:存放可拖拽的节点模板 -->
<div class="node-palette">
<h3>节点库</h3>
<div
v-for="node in nodeTypes"
:key="node.type"
class="palette-node"
draggable="true"
@dragstart="handleDragStart($event, node)"
>
{{ node.label }}
</div>
</div>
<!-- 右侧画布区域:包含画布和工具栏 -->
<div class="canvas-wrapper">
<!-- 画布容器:X6实际渲染的地方 -->
<div ref="containerRef" class="canvas-container"></div>
<!-- 工具栏:控制画布的按钮组 -->
<div class="toolbar">
<button @click="zoomOut">缩小</button>
<button @click="zoomIn">放大</button>
<button @click="resetZoom">重置</button>
<button @click="undo">撤销</button>
<button @click="redo">重做</button>
<button @click="download">下载图片</button>
</div>
</div>
</div>
</template>
2. 数据定义部分
containerRef:获取DOM元素的引用,用于挂载画布
graph:存储X6画布实例,后续所有操作都通过它进行
nodeTypes:定义可用的节点类型,包括形状、大小和显示名称
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { Graph, Shape } from '@antv/x6'
// 画布容器引用
const containerRef = ref<HTMLElement>()
// 画布实例
let graph: Graph
// 节点类型定义:供左侧节点库使用
const nodeTypes = [
{ type: 'rect', label: '矩形节点', width: 100, height: 60 },
{ type: 'circle', label: '圆形节点', width: 60, height: 60 },
{ type: 'ellipse', label: '椭圆节点', width: 80, height: 50 },
{ type: 'diamond', label: '菱形节点', width: 80, height: 80 }
]
3. 画布初始化部分
创建X6画布实例,配置各项功能
// 初始化画布
onMounted(() => {
if (!containerRef.value) return
graph = new Graph({
container: containerRef.value,
width: 800,
height: 600,
// 背景设置
background: {
color: '#f5f5f5' // 背景颜色
},
// 网格设置
grid: {
visible: true, // 显示网格
type: 'doubleMesh', // 双网格效果
args: [
{ color: '#e9e9e9', thickness: 1 }, // 主网格
{ color: '#c9c9c9', thickness: 1, factor: 5 } // 次网格
]
},
// 画布拖拽设置
panning: {
enabled: true, // 启用画布拖拽
modifiers: 'ctrl' // 按住ctrl键才能拖拽画布
},
// 滚轮缩放设置
mousewheel: {
enabled: true,
modifiers: 'ctrl', // 按住ctrl键才能缩放
minScale: 0.5, // 最小缩放比例
maxScale: 2 // 最大缩放比例
},
// 连线设置
connecting: {
router: 'manhattan', // 连线路径算法
connector: { // 连线样式
name: 'rounded',
args: { radius: 8 }
},
snap: { radius: 20 }, // 吸附距离
createEdge() { // 创建连线的默认样式
return new Shape.Edge({
attrs: {
line: {
stroke: '#1890ff',
strokeWidth: 2,
targetMarker: { name: 'classic', size: 8 }
}
}
})
}
}
})
// 注册自定义节点
registerCustomNodes()
// 绑定事件
bindEvents()
})
4. 注册自定义节点
// 注册自定义节点
const registerCustomNodes = () => {
// 矩形节点
Graph.registerNode('custom-rect', {
inherit: 'rect', // 继承自基础矩形
width: 100,
height: 60,
attrs: {
body: { // 节点主体样式
stroke: '#1890ff', // 边框颜色
strokeWidth: 2, // 边框宽度
fill: '#ffffff', // 填充色
rx: 6, ry: 6 // 圆角半径
},
label: { // 节点标签样式
text: '节点',
fill: '#333',
fontSize: 14,
fontWeight: 'bold'
}
}
})
// 圆形节点
Graph.registerNode('custom-circle', {
inherit: 'circle',
width: 60,
height: 60,
attrs: {
body: {
stroke: '#52c41a',
strokeWidth: 2,
fill: '#ffffff'
},
label: {
text: '节点',
fill: '#333',
fontSize: 14
}
}
})
}
5. 事件绑定部分
// 绑定事件
const bindEvents = () => {
// 节点点击事件
graph.on('node:click', ({ node }) => {
console.log('节点被点击:', node)
})
// 节点双击事件 - 修改节点名称
graph.on('node:dblclick', ({ node }) => {
const newLabel = prompt('修改节点名称', node.attr('label/text'))
if (newLabel) {
node.attr('label/text', newLabel)
}
})
// 空白区域点击
graph.on('blank:click', () => {
console.log('空白区域被点击')
})
// 连线创建成功
graph.on('edge:connected', ({ edge }) => {
console.log('连线创建成功:', edge)
})
}
6. 拖拽功能实现
handleDragStart:当从左侧节点库拖拽时,保存节点信息
drop事件:当释放鼠标时,在对应位置创建新节点
坐标转换:将鼠标屏幕坐标转换为画布坐标
// 拖拽开始
const handleDragStart = (e: DragEvent, node: any) => {
// 存储节点数据到拖拽事件
e.dataTransfer?.setData('text/plain', JSON.stringify(node))
e.dataTransfer?.setData('nodeType', node.type)
e.dataTransfer!.effectAllowed = 'move'
}
// 监听放置事件
onMounted(() => {
if (!containerRef.value) return
// 允许拖拽元素放置
containerRef.value.addEventListener('dragover', (e) => {
e.preventDefault()
})
// 处理放置事件
containerRef.value.addEventListener('drop', (e) => {
e.preventDefault()
const data = e.dataTransfer?.getData('text/plain')
if (!data) return
const nodeData = JSON.parse(data)
// 将鼠标坐标转换为画布坐标
const point = graph.clientToLocal(e.clientX, e.clientY)
// 在鼠标位置创建新节点
const node = graph.addNode({
shape: nodeData.type === 'rect' ? 'custom-rect' :
nodeData.type === 'circle' ? 'custom-circle' : 'rect',
x: point.x - nodeData.width / 2, // 居中放置
y: point.y - nodeData.height / 2,
width: nodeData.width,
height: nodeData.height,
label: nodeData.label,
attrs: {
label: { text: nodeData.label }
}
})
})
})
7. 工具栏功能
// 缩放功能
const zoomIn = () => {
const scale = graph.zoom()
graph.zoom(scale + 0.1, { absolute: true })
}
const zoomOut = () => {
const scale = graph.zoom()
graph.zoom(scale - 0.1, { absolute: true })
}
const resetZoom = () => {
graph.zoom(1) // 重置到原始大小
}
// 历史操作
const undo = () => {
graph.undo() // 撤销
}
const redo = () => {
graph.redo() // 重做
}
// 导出图片
const download = () => {
graph.toPNG((dataUrl) => {
const link = document.createElement('a')
link.download = 'graph.png'
link.href = dataUrl
link.click()
})
}
// 清理画布
onUnmounted(() => {
if (graph) {
graph.dispose() // 释放资源
}
})