「AntV」怎样用SVG & X6制作客户旅程时光轴
最近我在我们前端平台研发团队内部做了一次技术分享——主题是《怎样用SVG & X6制作客户旅程时光轴》——总结了我最近开发的一个客户旅程图项目的相关经验,现在写成这篇文章,希望能帮助到有同样需求的各位朋友。
大家好,我叫王士江,是前端平台研发团队的一名前端开发工程师,很高兴跟大家做这次技术分享,我这次分享的主题是——《怎样用SVG & X6制作客户旅程时光轴》。
这次分享的主要内容包含以下几个方面:
1、这里的“客户旅程时光轴”是什么?
2、实现“客户旅程时光轴”的技术选型
3、SVG基础与应用
4、AntV X6基础与应用
5、涉及到的布局算法
6、参考资料
以下是本次分享的正文——
1、这里的“客户旅程时光轴”是什么?
说到“客户旅程时光轴”,那它到底是什么?我这里给大家看几张示例图,大家先直观地感受一下:
👆上面这是一张旅程图——从开始到结束,中间有若干个旅程节点,节点之间有连线相连。
👆上面这是一张路径图——这里的节点是不同的步骤,节点之间由一条路径连线相连。
👆上面这是一张时光轴图——每个节点是时间点,各个节点由一条时间轴相连。
从上面可以看出,这里的“时光轴图”“路径图”或者“旅程图”,整体结构是非常相似的——都是由或圆形/或方形的“节点”,以及将“节点”串在一起的“边”,构成的一张“图”形:
(1)节点可以是时间点、是里程碑,从而构成时光轴图;
(2)节点可以是步骤、是地点,构成路径图、旅程图。
实际上,这里的“时光轴图”“路径图”或者“旅程图”都算是信息图——它通过信息可视化的方式,让人更容易获取信息、发现规律、掌控全局。
我这里说的“客户旅程时光轴”,其实是一种以客户的视角进行客户旅程的信息化展示。这里为了解释方便,我没有区分是“时光轴图”“路径图”或者“旅程图”,而是直接称呼为“客户旅程时光轴”。
以下是我在项目中“客户旅程时光轴”的实现DEMO:
项目中实现了3个客户旅程的功能原型,这里称呼为客户旅程A、客户旅程B、客户旅程C吧——
👆上面这是客户旅程A原型
👆上面这是客户旅程B原型
👆上面这是客户旅程C原型
项目中的实际需求大概是这样的——
- 客户旅程是由旅程节点组成,一行展示固定个数(比如一行6个),超出则换行显示(比如这里的客户旅程A和C);
- 客户的某些旅程节点是可选的,对于没有走过的旅程节点和边,需要置灰显示(比如这里的客户旅程A和C);
- 客户旅程走过的节点,可以通过动画的方式进行展示。
2、实现“客户旅程时光轴”的技术选型
“客户旅程时光轴”本质上是一个信息图,对于我们前端开发来说属于可视化领域,那么怎么实现“客户旅程时光轴”的技术选型?
说到前端可视化,我们第一个想到的工具、可能就会是Echarts —— 它是一个图表库,通过使用它提供的各个配置项、进行简单地组合使用,就能快速实现饼图、柱状图、折线图等等。
但是像Echarts这样的图表库,适合处理的是表格型数据集,而我们这里要的“客户旅程时光轴”,我们需要处理的是一个个的“节点”、以及节点之间的“边”,很明显不符合我们的需求。
这里就引入可视化领域的另一个应用场景——图可视化——图可视化可以应用在包括流程图、组织架构图、思维导图等等场景。在计算机科学中的图,就是由“节点”和“边”组成——而我们在图可视化中需要关注的重点就是构成图的“节点”以及节点之间的“边”。
我们平时前端开发用到的HTML & CSS,其实是可以实现简单的图形——比如用HTML & CSS可以实现画方块、画圆,如果用很小高度的DIV + CSS Transform甚至可以画折线——但是对于更复杂的图形,通过HTML & CSS就很难实现了。
而提到绘图,对于我们前端来说可以通过两种实现方式——Canvas 和 SVG:
-
Canvas是命令式图形系统,可以通过其API进行方形、圆形、线形等等图形的绘制,上手难度较高,定制图形比较复杂,在大数据量场景性能突出;
-
SVG是指令式图形系统,可以通过类似HTML的XML标签进行图形绘制,上手难度较低,体验跟HTML开发的体验非常一致,基于 DOM图形定制能力强,但是在大数据量场景性能较差。
由于我们的客户旅程图不需要渲染大量的节点,图形定制能力强、上手成本低是我们非常关注的一个重点,所以使用SVG更适合来完成我们的旅程图应用开发。
而AntV X6的底层绘制系统就是基于SVG,而且AntV X6在图可视化方面、尤其是图编辑方面,进行了图(Graph)、节点(Node)、边(Edge)等等模型抽象,提供了大量非常方便使用的API,以及在编辑场景上提供了包括历史(History)、对齐(Snapline)、小地图(Minimap)等等各种插件实现,所以在项目中我们最终选用了AntV X6来实现客户旅程时光轴图。
接下来,我们就客户旅程时光轴图的一些实现细节,了解和掌握SVG和AntV X6的基础与应用。
3、SVG基础与应用
对于SVG,我们需要重点关注的是以下几点——
(1)图形
SVG是声明式图形系统,通过XML标签语法来使用。
<svg width="450px" height="100px" viewBox="0 0 450 100">
<rect x="10" y="5" fill="white" stroke="black" width="90" height="90"/>
<circle fill="white" stroke="black" cx="170" cy="50" r="45"/>
<polygon fill="white" stroke="black" points="279,5 294,35 328,40 303,62 309,94
279,79 248,94 254,62 230,39 263,35 "/>
<line fill="none" stroke="black" x1="410" y1="95" x2="440" y2="6"/>
<line fill="none" stroke="black" x1="360" y1="6" x2="360" y2="95"/>
</svg>
形状元素包括:rect、circle、ellipse、line、polyline、polygon以及path——可以分别用来画方形、圆形、椭圆、线、折线、多边形以及更复杂的图形。
g标签常常用来作为分组,比如AntV X6就的“节点”和“边”的实现都使用了g标签:
这里需要重点强调的是path标签,它是SVG中最强大的标签,前面的rect、circle、ellipse、line、polyline以及polygon等等都可以通过path来实现,使用path可以实现更复杂的图形。
比如AntV X6中的边的实现,就使用到了path标签:
path标签中,我们是通过“d”属性进行图形定义,d属性中的内容是由字母和数字组成——字母代表指令,比如大写的M,是指“moveTo”(移动到)的,表示的开始移动到某个绝对坐标,小写的m表示移动到某个相对坐标;这里的数字是指令的参数,比如M后面的坐标,代表的是移动到的具体坐标。
除了M(moveTo)指令,其他可用的指令包括L(lineTo)、H、V、Z、C、S、Q、T、A等等:
下面在讲到AntV X6的“边的定制”(Edge)时,我们会使用到多个path标签。
(2)画家模型
SVG的渲染模型被称为画家模型——
- 使用stroke进行描边;
- 使用fill进行填充。
这里我们可以做个类比——不知道你有没有见过《秘密花园》涂鸦绘本?SVG的“画家模型”可以通过它来解释——它的黑白线稿就相当于stroke,表示描边;它的彩色涂鸦就相当于fill,表示填充。
stroke和fill的取值有以下选择——
- none——没有颜色;
- currentColor——继承父标签的color颜色值;
- ——常规的颜色值,RGBA, HSBA都支持。
(3)坐标
跟我们平时的开发认识一样,SVG的坐标也是以左上角作为原点,标签的width、height属性可以设置在浏览器中显示时的像素宽高,viewBox属性则设置了的可视区域坐标和宽高——
<svg width="450px" height="100px" viewBox="0 0 450 100">
<rect x="10" y="5" fill="white" stroke="black" width="90" height="90"/>
<circle fill="white" stroke="black" cx="170" cy="50" r="45"/>
<polygon fill="white" stroke="black" points="279,5 294,35 328,40 303,62 309,94
279,79 248,94 254,62 230,39 263,35 "/>
<line fill="none" stroke="black" x1="410" y1="95" x2="440" y2="6"/>
<line fill="none" stroke="black" x1="360" y1="6" x2="360" y2="95"/>
</svg>
- 通过x、y属性设置一个方形的左上角的坐标,通过width、height设置宽高;
- 通过cx、cy属性设置一个圆形的中心点,通过r属性设置圆的半径;
- 其他以此类推。
在SVG中各个图形元素的坐标位置,就是我们需要关注的第三个重点——目前SVG还没有像flex布局、grid布局等等内置布局供我们使用——各个图形元素的坐标位置都需要我们进行设置。
好在AntV X6提供了一个@antv/layout包——里面提供了包括grid布局、dagre布局、force布局等等布局算法供我们使用——我们往往会通过使用各个布局算法,将布局的结果作为我们节点的位置坐标。
(4)描边动画
描边动画不是使用SVG必须关注的内容——由于我当前这个客户旅程项目有动画方面的需求,所以我在这里简单说一下这个描边动画的实现:
上面在SVG的《画家模型》章节里面,我们讲到使用stroke进行描边,描边动画就是使用了stroke-dasharray和stroke-dashoffset这两个属性的组合进行实现的
- stroke-dasharray 表示虚线描边;
- stroke-dashoffset 表示虚线的起始偏移。
如果stroke-dasharray和stroke-dashoffset值都很大,超过了描边路径的总长度,然后给stroke-dashoffset添加一个animation设置,让它一点一点地恢复到0——就会看到一根黑线一点点地出现,就好像是正在绘制上去似的:
path {
stroke-dasharray: 1000;
stroke-dashoffset: 1000;
animation: dash 5s linear infinite;
}
@keyframes dash {
to {
stroke-dashoffset: 0;
}
}
张鑫旭写了一篇《纯CSS实现帅气的SVG路径描边动画效果》的文章,感兴趣的可以阅读一下。
4、AntV X6基础与应用
AntV X6的底层绘制系统是基于SVG的,而且在图可视化方面、尤其是图编辑方面,进行了图(Graph)、节点(Node)、边(Edge)等等模型抽象,提供了大量非常方便使用的API,所以我们直接使用了AntV X6作为我们客户旅程时光轴的技术选型。
使用AntV X6时,我们最关注的是以下几个重点——节点定制,边的定制,以及布局算法。
(1)节点定制
在 SVG 中有一个特殊的 元素,在该元素中可以内嵌任何 XHTML 元素,所以我们可以借助该元素来渲染 HTML 元素和 React/Vue/Angular组件到需要位置。
由于我们项目上主要是使用React,所以我们是用React组件进行节点定制,实现代码如下——
import { useEffect } from 'react';
import { ReactShape } from '@antv/x6-react-shape';
import { AppstoreFilled } from '@ant-design/icons';
import { Dom, Edge } from '@antv/x6';
import classNames from 'classnames';
import {
NODE_WIDTH,
LINE_COLOR,
LINE_LIGHT_COLOR,
COMPONENT_TEXT_HEIGHT,
} from '../../constants';
import './index.less';
export default class JourneyNode extends ReactShape {}
interface IProps extends React.HTMLProps<HTMLDivElement> {
node: ReactShape;
}
const NodeView: React.FC<IProps> = (props) => {
const { node } = props;
const info = node.getData<any>();
useEffect(() => {
if (info.status === 'disabled') {
const edges: Edge[] = [
...(node.model?.getOutgoingEdges(node) || []),
...(node.model?.getIncomingEdges(node) || []),
];
edges.forEach((edge: Edge) => {
edge.attr('line/stroke', `#${LINE_LIGHT_COLOR}`);
});
}
}, []);
return (
<div className={classNames('journey-node-wrapper', info.status)}>
<AppstoreFilled style={{ fontSize: '16px' }} />
<div className="warning-tips">!</div>
</div>
);
};
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,
},
},
},
});
Graph.registerNode('journey-node', JourneyNode);
AntV X6的节点有一个data
属性,可以添加自定义数据,我们在data里面添加了一个名称为status
的属性,如果status属性的值是disabled,则说明这个节点没有走过,这时这个节点、以及连接节点的两条边都给置灰,效果如客户旅程A所示:
(2)边定制
在AntV X6中,边的实现是通过SVG中的<path>
标签来实现的。我们项目中的客户旅程图,就对AntV X6进行了边的定制,实现代码如下——
import { Graph } from '@antv/x6';
Graph.registerEdge(
'journey-edge',
{
inherit: 'edge',
markup: [
{
tagName: 'path',
selector: 'wrap',
groupSelector: 'lines',
attrs: {
fill: 'none',
cursor: 'pointer',
stroke: 'transparent',
strokeLinecap: 'round',
},
},
{
tagName: 'path',
selector: 'background',
groupSelector: 'lines',
attrs: {
fill: 'none',
pointerEvents: 'none',
stroke: `#${LINE_LIGHT_COLOR}`,
targetMarker: '',
},
},
{
tagName: 'path',
selector: 'line',
groupSelector: 'lines',
attrs: {
fill: 'none',
pointerEvents: 'none',
class: 'journey-path-line',
targetMarker: '',
},
},
{
tagName: 'path',
selector: 'dash',
groupSelector: 'lines',
attrs: {
fill: 'none',
pointerEvents: 'none',
targetMarker: '',
stroke: `#FFFFFF`,
'stroke-dasharray': 5,
},
},
],
attrs: {
background: {
strokeWidth: 10,
},
line: {
strokeWidth: 6,
},
dash: {
strokeWidth: 1,
},
},
},
true,
);
在边的定制中,markup是指使用到的SVG元素,在客户旅程边的实现中,我们这里用到了4个path标签:
- 第一个选择器为wrap的path,是为了方便响应交互的占位元素;
- 第二个选择器为background的path,是置灰效果时显示的那个灰色底边;
- 第三个选择器为line的path,是实际展示的黑色的边;
- 第四个选择器为dash的path,则是为了模拟成柏油公路的白色虚线。
客户旅程图中边的最终实现效果如下:
5、布局算法
在一个客户旅程图中,有了旅程节点和边,我们接下来就需要按一定的布局进行旅程节点的展示。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(随机布局算法)。
在客户旅程图的项目开发过程中,我们使用到了如下两种布局算法——
(1)Dagre布局
Dagre布局算法是“层次布局”的一个成熟算法实现,X6的@antv/layout布局包中就有Dagre布局的实现。
在 Dagre算法中定义了几个基本概念:
(1)rankDir:图的延展方向,分为由上到下(tb)、由下到上(bt)、由左到右(lr)、由右到左(rl)四种。
图 图的延展方向rankdir
(2)rank:沿着图的延展方向划分的层级,每个顶点都存在于某个层级上,一个层级上可能有多个顶点。
图 图的层次划分rank
(3)level:在每个 rank 中针对每一个节点划分的级。不同 rank 中的 level 互不影响。
图 布局中的层级划分
Dagre布局的使用很简单,实现如下——
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);
经过Dagre布局之后,我们能得到从左往右的布局的一个旅程图,效果如下——
但是项目上,我们有一个需求是一行展示固定个数(比如一行6个),超出则换行显示(比如这里的客户旅程A和C)——
这时候我们需要在Dagre算法基础之上,再使用网格布局处理一次。
(2)网格布局
网格布局是一种几何布局,我们可以通过把旅程节点划分到各个行和列中,然后根据节点所在的行和列、计算出节点实际所处的坐标。
在客户旅程项目中,跟正常网格布局稍微不一样的地方是,这里的旅程节点需要折行显示——
在这里我们做了如下处理:
-
对于奇数行,节点y坐标相同,x坐标值是节点的列号(columnNumber)进行计算;
-
对于偶数行,节点y坐标相同,x坐标值需要在上述步骤1的基础之上进行位置兑换。
6、参考资料
在客户旅程项目开发过程中翻阅了大量资料,以下是推荐大家进行延伸阅读的参考资料:
转载自:https://juejin.cn/post/7247167843496722490