1、背景
刚刚开始接触AntV X6的前端小伙伴,应该能立即感受到------使用它展开前端开发工作、跟我们平时的开发经验不太一样,因为它属于有别于我们日常前端开发的另一个领域------前端可视化。
我个人非常喜欢AntV X6------不仅因为它很酷,而且很有用、很好用,我们可以用它开发一些很酷、很有用的应用------我在公司内部做了几场关于AntV X6的技术分享,也使用AntV X6在公司实际开发过3个项目,在项目中实现的功能如下:
(1)逻辑可视化编排
这是公司内部一个前端低代码项目------包括页面可视化搭建,逻辑可视化编排,以及数据源配置等等。
我在这个项目里,使用AntV X6实现了逻辑可视化编排------其中包括逻辑可视化编排的Debug功能,对逻辑可视化编排的Debug功能实现感兴趣的同学,可以阅读我的另一篇文章《怎样给iMove开发一个Debug插件------可视化逻辑编排的Debug实现》。
(2)流程可视化搭建
在这个项目里,我通过使用AntV X6实现了流程可视化搭建的功能原型。
同样是在这个项目里,因为需求方有一个自动布局的需求,我使用到了@antv/layout 这个关于图布局的包,同时对Dagre(图的层次布局算法)有了一些学习和研究,对这块感兴趣的同学,可以阅读我写的另一篇文章《Dagre算法简介以及在流程图自动布局中的应用》。
(3)客户旅程时光轴
这是我最近正在做的一个项目,项目中的实际需求大概是这样的------
- 客户旅程是由旅程节点组成,一行展示固定个数(比如一行6个),超出则换行显示;
- 客户的某些旅程节点是可选的,对于没有走过的旅程节点和边,需要置灰显示;
- 客户旅程走过的节点,可以通过动画的方式进行展示。
在这个项目里,我通过使用AntV X6实现了客户旅程时光轴,目前正在实现客户旅程时光轴的可视化编排,对这块感兴趣的同学,可以阅读我写的另一篇文章《「AntV」怎样用SVG & X6制作客户旅程时光轴》。
我个人非常喜欢AntV X6------它很酷,很有用,而且很好用,然而使用AntV X6也是有一些门槛------软件/工具是知识和经验凝结的产物,在开发上遇到的很多障碍、往往是相关知识、开发经验或者能力方面有欠缺,我希望在这篇文章中能够把我使用AntV X6的经验进行提炼、帮助大家跨过使用AntV X6的门槛。
2、数据:网状数据集
我们之前提到过,AntV X6属于有别于我们日常前端开发的另一个领域------前端可视化。而说到前端可视化,我们第一个想到的工具、可能就会是Echarts ------ 它是一个图表库,通过使用它提供的各个配置项、进行简单地组合使用,就能快速实现饼图、柱状图、折线图等等。
但是像Echarts这样的图表库,适合处理的是表格型数据集------而使用AntV X6,我们需要处理的是一个个的"节点"、以及节点之间的"边",这属于网状数据集,所以AntV X6属于有别于Echarts这种图表可视化的另一个可视化分类------"图可视化"。
举个例子,用Antv X6绘制一个Hello -> World,需要准备的数据如下:
typescript
const data = {
// 节点
nodes: [
{
id: 'node1', // String,可选,节点的唯一标识
x: 40, // Number,必选,节点位置的 x 值
y: 40, // Number,必选,节点位置的 y 值
width: 80, // Number,可选,节点大小的 width 值
height: 40, // Number,可选,节点大小的 height 值
label: 'Hello', // String,节点标签
},
{
id: 'node2', // String,节点的唯一标识
x: 200, // Number,必选,节点位置的 x 值
y: 40, // Number,必选,节点位置的 y 值
width: 80, // Number,可选,节点大小的 width 值
height: 40, // Number,可选,节点大小的 height 值
label: 'World', // String,节点标签
},
],
// 边
edges: [
{
source: 'node1', // String,必须,起始节点 id
target: 'node2', // String,必须,目标节点 id
},
],
};
在上面的"Hello -> World"示例中------我们提供两个节点Hello和World,还提供了一条连接两个节点的边------以下是Hello -> World 示例实际查看效果:
3、结构:5个核心要素
仔细观察上面的上面的"Hello -> World"示例,我们拆解来看、一个图本质上无外乎就是「两点一线」,以及在节点和连线上的文字标签。
根据我使用AntV X6进行实际项目的开发经验,一个图可视化编排应用大概是由以下5个核心要素组成的:
- 节点(Node):图中那些一个个的,或圆形、或方型、或图标等等图形所代表的元素,被称为"节点",节点是图里面的最基础的元素;
- 连线(Edge):连接两个节点的元素,被称为"连线",连线是 AntV X6 中非常重要的一部分,AntV X6 内置了很多实用的连线功能,也提供了优雅的扩展机制 ,这是相比于其他流程图框架占据绝对优势的地方;
- 标签(Label):节点或者连线上的说明文字,被称为"标签";
- 布局(Layout):让图中各个节点按照一定的规则进行排列,被称为"布局"。AntV X6提供了一个@antv/layout包------里面提供了包括grid布局、dagre布局、force布局等等布局算法供我们使用------我们往往会通过使用各个布局算法,将布局的结果作为我们节点的位置坐标;
- 事件(Events):通过AntV X6内置事件系统,我们可以监听图内发生的任何事件。
接下来我们结合下面的客户旅程时光的案例,从这5个核心要素出发、进行一一讲解:
(1)节点(Node)
节点是图里面的最基础的元素。打开Chrome开发者工具,当我们查看第一个节点的元素时,我们可以在元素面板看到如下DOM结构:
一个节点
是由一个<g>
标签组成,<g>
标签是SVG中的标签------AntV X6的底层绘制系统是基于SVG,SVG是指令式图形系统,可以通过类似HTML的XML标签进行图形绘制,上手难度较低,体验跟HTML开发的体验非常一致。
再往下看,我们可以看到,旅程图的圆形节点
是由<circle>
标签实现的------<circle>
是SVG中的形状元素,其他形状元素包括:<rect>
、<ellipse>
、<line>
、<polyline>
、<polygon>
以及<path>
,可以分别用来画方形、椭圆、线、折线、多边形以及更复杂的图形。
在 SVG 中有一个特殊的<foreignObject>
元素,在该元素中可以内嵌任何 XHTML 元素,所以我们可以借助该元素来渲染 HTML 元素和 React/Vue/Angular组件到需要位置------由于我们项目上主要是使用React,所以我们是用React组件进行节点定制------只要你会React/Vue/Angular组件开发,就能进行AntV X6的节点开发。
接下来我们从节点定义和节点样式两个方面讲述在AntV X6中节点的使用:
① 节点定义
首先来看下一个简单的矩形节点的基础配置:
typescript
// 一个简单的矩形节点的基础配置
graph.addNode({
shape: 'rect', // shape:定义节点的形状,X6 内置了 rect、circle、ellipse、polygon、polyline、image、html 等基础形状
x: 100, // x/y:定义节点的左上角坐标
y: 100,
width: 80, // width/height:定义节点的尺寸
height: 40,
attrs: { // attrs: 可以将 attrs 看做 css 样式集合
body: {
stroke: 'red'
}
}
});
- shape:定义节点的形状,X6 内置了 rect、circle、ellipse、polygon、polyline、image、html 等基础形状;
- x/y:定义节点的左上角坐标;
- width/height:定义节点的尺寸 ;
- attrs:看到 attrs 大家可能比较奇怪,这是什么东西?其实可以将 attrs 看做 css 样式集合,其中 body 类似于 css 选择器,body 的值是被选中元素的属性。那 body 又是哪来的呢?这里就要说的另一个重要的配置项markup,markup 表示的是节点的 DOM 结构,内置的 rect 的默认 markup 为:
typescript
[
{
tagName: 'rect',
selector: 'body',
},
{
tagName: 'text',
selector: 'label',
},
]
渲染完成后,实际生效的 DOM 为:
typescript
<g data-cell-id="ca715562-8faf-4c88-a242-2b18d4ce47a6" data-shape="rect" class="x6-cell x6-node" transform="translate(100,100)">
<rect fill="#ffffff" stroke="red" stroke-width="2" width="80" height="40"></rect>
<text font-size="14" fill="#000000" text-anchor="middle" text-vertical-anchor="middle" font-family="Arial, helvetica, sans-serif" transform="matrix(1,0,0,1,40,20)"></text>
</g>
② 节点样式
适用于表达节点的视觉样式的有大小、颜色、描边、形状、图标、角标 这么几个视觉参数:
- 大小:通过width/height来定义节点的尺寸;
- 颜色:通过attrs中的fill来定义填充颜色;
- 描边:通过attrs中的stroke来定义描边颜色;
- 形状:通过shape来定义节点形状,X6 内置了 rect、circle、ellipse、polygon、polyline、image、html等基础形状,另外也支持React、Vue组件;
- 图标:同上;
- 角标:节点右上角的圆形徽标。
(2)连线(Edge)
接下来我们查看一下连线的DOM元素,我们可以看到连线是由许多<path>
标签实现的------<path>
标签是SVG中最强大的标签,前面的<rect>
、<circle>
、<ellipse>
、<line>
、<polyline>
以及<polygon>
等等都可以通过<path>
来实现,使用<path>
可以实现更复杂的图形。
AntV X6 中连线分两种形式,代码生成
的和用户手动拖拽
而成。
① 代码生成
我们可以通过以下代码建立连线:
typescript
// source 或 target 是坐标点
graph.addEdge({
source: [0, 0],
target: [100, 100]
})
// source 或 target 是节点对象
graph.addEdge({
source: sourceNode,
target: targetNode,
})
// source 或 target 是节点 ID
graph.addEdge({
source: 'sourceId',
target: 'targetId',
})
// source 或 target 是连接桩
graph.addEdge({
source: { cell: 'cellId1', port: 'portId1' },
target: { cell: 'cellId2', port: 'portId2' }
})
② 手动拖拽
如果想通过手动操作来创建连线,需要有两个条件:
- 需要从具有
magnet: true
属性的元素上才能手动拖拽出连线; - 需要在全局
connecting
配置中自定义createEdge
方法。
typescript
import { Graph, Shape } from '@antv/x6'
const graph = new Graph({
connecting: {
createEdge() {
return new Shape.Edge()
},
},
})
graph.addNode({
shape: 'rect',
x: 100,
y: 100,
width: 80,
height: 40,
attrs: {
body: {
stroke: 'red',
magnet: true
}
}
})
graph.addNode({
shape: 'rect',
x: 400,
y: 100,
width: 80,
height: 40,
attrs: {
body: {
stroke: 'red',
magnet: true
}
}
})
我们经常需要自己来定义连线的样式,其实连线和图形都是由 markup 和 attrs 来定义的,默认连线的 markup 为:
typescript
[
{
tagName: 'path',
selector: 'wrap',
groupSelector: 'lines',
attrs: {
fill: 'none',
cursor: 'pointer',
stroke: 'transparent',
strokeLinecap: 'round',
},
},
{
tagName: 'path',
selector: 'line',
groupSelector: 'lines',
attrs: {
fill: 'none',
pointerEvents: 'none',
},
},
]
其中 line 为实际展示的连线,wrap 是为了方便响应交互的占位元素。
(3)标签(Label)
我们可以看到标签是由一个<text>
文本元素实现的。
这里需要注意的是,SVG中的<text>
标签文本内容超出设定的长度之后不会自动换行,不过AntV X6提供了textWrap这个扩展属性实现了文本内容自动换行,比如以下是我在客户旅程时光轴节点上的标签设置:
typescript
JourneyNode.config({
shape: 'journey-node',
component: (node: any) => {
return <NodeView node={node} />;
},
width: NODE_WIDTH,
height: NODE_WIDTH,
attrs: {
label: {
refX: 0.5,
refY: '100%',
refY2: 20,
fill: '#333',
fontSize: 13,
textAnchor: 'middle',
textVerticalAnchor: 'middle',
textWrap: {
width: 80,
height: 60,
ellipsis: false,
breakWord: true,
},
},
},
});
(4)布局(Layout)
AntV X6提供了一个@antv/layout包------里面提供了包括grid布局、dagre布局、force布局等等布局算法供我们使用------我们往往会通过使用各个布局算法,将布局的结果作为我们节点的位置坐标。
在一篇名称为《可视化图布局算法浅析》的文章中总结了图可视化场景下常用的布局算法------
- 几何布局:grid(网格布局算法),circle(环形布局算法),concentric(同心圆布局算法),radial(辐射状布局算法),avsdf(邻接点最小度优先算法,Adjacent Vertex with Smallest Degree First);
- 层级布局:dagre(有向无环图树布局算法,Directed Acyclic Graph and Trees),breadthfirst(广度优先布局算法),elk(Eclipse布局算法,Eclipse Layout Kernel),klay(K层布局算法,K Lay);
- 力导布局:fcose(最快复合弹簧内置布局算法,Fast Compound Spring Embedder),cola(约束布局,Constraint-based Layout),cise(环形弹簧内置布局算法,Circular Spring Embedder),elk2(Eclipse布局算法,Eclipse Layout Kernel),euler(欧拉布局算法),spread(扩展布局算法),fruchterman(Fruchterman-Reingold布局算法),combo(混合布局算法);
- 其他布局:mds(高维数据降维布局算法,Multi Dimensional Scaling),random(随机布局算法)。
流程图应用是一种层次布局,而Dagre布局算法是"层次布局"的一个成熟算法实现,X6的@antv/layout
布局包中就有Dagre布局的实现,接下来我们重点介绍一下:
在 Dagre算法中定义了几个基本概念:
(1)rankDir:图的延展方向,分为由上到下(tb)、由下到上(bt)、由左到右(lr)、由右到左(rl)四种。
(2)rank:沿着图的延展方向划分的层级,每个顶点都存在于某个层级上,一个层级上可能有多个顶点。
(3)level:在每个 rank 中针对每一个节点划分的级。不同 rank 中的 level 互不影响。
Dagre布局的使用很简单,实现如下------
typescript
const dagreLayout = new DagreLayout({
type: 'dagre',
rankdir: 'LR',
// align: 'UR', // 居中对齐
ranksep: 36,
nodesep: 20,
});
dagreLayout.updateCfg({
begin: begin,
ranker: 'longest-path', // 'tight-tree' 'longest-path' 'network-simplex'
});
let dagreModel = dagreLayout.layout(data as any);
graph.fromJSON(dagreModel);
(5)事件(Events)
通过AntV X6内置事件系统,我们可以监听图内发生的任何事件------包括鼠标交互事件,节点/连线的增/删/改事件,以及自定义事件等等。
比如上面的例子所示,在节点通过拖拽添加到画布中时,我们可以通过监听新增节点的添加事件(即node:added),实现流程图的自动连线和自动布局,代码大致如下所示:
typescript
flowGraph.on('node:added', (args) => {
// 1. 实现自动连线
const oldTarget = edge.getTargetNode() || undefined;
const oldPort = edge.getTargetPortId() || 'left';
edge.setTarget(args.node, { port: 'left' });
flowGraph.addEdge({
source: args.node,
sourcePort: 'right',
target: oldTarget,
targetPort: oldPort,
});
// 2. 实现自动布局
const dagreLayout: DagreLayout = new DagreLayout({
begin: [40, 40],
type: 'dagre',
rankdir,
align,
nodeSize,
ranksep,
nodesep,
controlPoints,
});
dagreLayout.updateCfg({
// ranker: 'tight-tree', // 'tight-tree' 'longest-path' 'network-simplex'
// nodeOrder,
// preset: {
// nodes: model.nodes.filter((node: any) => node._order !== undefined),
// },
...cfg,
});
const model = flowGraph.toJSON();
const { nodes: newNodes } = dagreLayout.layout({
// @ts-ignore
nodes: model.cells.filter((cell) => cell.shape !== 'edge'), // @ts-ignore
edges: model.cells.filter((cell) => cell.shape === 'edge'),
});
newNodes?.forEach((node: any) => {
const cell: Node | undefined = flowGraph.getCellById(node.id) as
| Node
| undefined;
if (cell) {
cell.position(node.x, node.y);
}
});
flowGraph.cleanSelection();
flowGraph.select(args.cell);
});
4、参考资料
我在实际使用AntV X6进行项目开发过程中翻阅了大量资料,以下是推荐大家进行延伸阅读的参考资料: