我前一段时间写过一篇文章《「AntV」怎样用SVG & X6制作客户旅程时光轴》,分享了怎样通过使用SVG和X6制作客户旅程时光轴,得到了很多伙伴们的喜欢。
一些小伙伴找我要到了演示Demo的源代码,可是即使在有演示Demo源代码,在这基础之上再进行二次开发时却遇到了很多障碍。
软件/工具是知识和经验凝结的产物,在开发上遇到的很多障碍、往往是相关知识、开发经验或者能力方面有欠缺,所以我打算在演示Demo源代码基础之上,通过接下来的《「AntV X6」从0到1实现客户旅程时光轴组件》一系列文章,将我使用SVG和X6的相关知识和经验对大家进行分享,希望能帮到有同样需求的朋友。
《「AntV X6」从0到1实现客户旅程时光轴组件》会以系列文章的形式,有步骤、有重点地分享我使用AntV X6进行客户旅程时光轴组件实现相关的知识和经验,目前该系列文章会有如下6篇:
- 「AntV X6」从0到1实现客户旅程时光轴组件01-先导篇
- 「AntV X6」从0到1实现客户旅程时光轴组件02-画布篇
- 「AntV X6」从0到1实现客户旅程时光轴组件03-节点篇
- 「AntV X6」从0到1实现客户旅程时光轴组件04-连线篇
- 「AntV X6」从0到1实现客户旅程时光轴组件05-布局篇
- 「AntV X6」从0到1实现客户旅程时光轴组件06-动画篇
系列文章将会以“项目驱动”的方式进行技术知识和开发经验的分享,贯穿整个系列文章的案例如下:
以下是《「AntV X6」从0到1实现客户旅程时光轴组件01-先导篇》的文章正文。
刚刚开始接触AntV X6的前端小伙伴,应该能立即感受到——使用它展开前端开发工作、跟我们平时的开发经验不太一样,因为它属于有别于我们日常前端开发的另一个领域——前端可视化。作为前端开发,我们经常谈的就是视图(View)
、数据(Data)
和逻辑(Logic)
,那么接下来我将从这三个方面来分别进行阐述有哪些不一样:
1、视图:View
仔细观察上面的Demo,我们分析一下一张客户旅程时光轴图
是由什么要素构成的呢?其实把上面这张 DEMO 图拆解来看,再复杂的图,本质上无外乎就是「两点一线」,以及在节点和边上的文字标签。
我们可以发现一个“客户旅程时光轴组件”是由以下5个核心要素组成的——
点(节点,Node)
:旅程图Demo中那些一个个的圆圈,被称为“节点
”;边(连线,Edge)
:两个节点中的连线,被称为“边
”;标签(Label)
:节点下的说明文字,被称为“标签
”;布局(Layout)
:各个节点位置坐标的排列,被称为“布局
”;动画(Animation)
:在旅程时光轴图中添加了一个从第一个节点一直到最后一个节点的描边动画
。
本系列的文章就是围绕着客户旅程时光轴图的这5个要素进行展开的——而且由于“节点
”、“连线
”、“布局
”、“动画
”都是定制化开发时的重中之重,接下来将会分别以单独的文章进行更详细地阐述——在这里我先就这5个要素进行一下简要概述:
(1)节点(Node) & 标签(Label)
你可以通过点击这个链接看到客户旅程时光轴的示例。
打开Chrome开发者工具,当我们查看第一个节点的元素时,我们可以在元素面板看到如下DOM结构:
一个节点
是由一个<g>
标签组成,<g>
标签是SVG中的标签——AntV X6的底层绘制系统是基于SVG,SVG是指令式图形系统,可以通过类似HTML的XML标签进行图形绘制,上手难度较低,体验跟HTML开发的体验非常一致。
再往下看,我们可以看到,旅程图的圆形节点
是由<circle>
标签实现的——<circle>
是SVG中的形状元素,其他形状元素包括:<rect>
、<ellipse>
、<line>
、<polyline>
、<polygon>
以及<path>
,可以分别用来画方形、椭圆、线、折线、多边形以及更复杂的图形。
再往下看,我们可以看到标签
是由一个<text>
文本元素实现的。
另外,在 SVG 中有一个特殊的 <foreignObject>
标签,在该标签中可以内嵌任何 XHTML 元素,所以我们可以借助该标签来渲染 HTML
元素和 React
/Vue
/Angular
组件到需要位置——因此我们也可以使用React
/Vue
/Angular
等等的组件进行旅程图的节点
定制。
(2)连线(Edge)
接下来我们查看一下连线
的DOM元素,我们可以看到连线
是由许多<path>
标签实现的——<path>
标签是SVG中最强大的标签,前面的<rect>
、<circle>
、<ellipse>
、<line>
、<polyline>
以及<polygon>
等等都可以通过<path>
来实现,使用<path>
可以实现更复杂的图形。
(3)布局(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(随机布局算法)。
在当前的客户旅程时光轴Demo中,我们是结合了Dagre算法
和网格布局
进行布局实现的——先使用Dagre算法进行一次布局计算,再使用网格布局处理一次。
(4)动画(Animation)
在SVG中,我们是通过使用stroke
属性进行描边,描边动画
就是使用了stroke-dasharray
和stroke-dashoffset
这两个属性的组合进行实现:
如果stroke-dasharray
和stroke-dashoffset
的值都很大,超过了描边路径的总长度,然后给stroke-dashoffset
添加一个animation
设置,让它一点一点地恢复到0——就会看到一根实线一点点地出现,就好像使用画笔正在绘制上去似的。
张鑫旭写了一篇《纯CSS实现帅气的SVG路径描边动画效果》的文章,感兴趣的可以阅读一下,在本系列文章的动画篇
中我们会详细讲述怎样在Antv X6中实现描边动画
。
2、数据:Data
从数据方面来看,我们作为前端开发日常处理的数据类型可以归类为“表格数据”——就像Excel表格一样,数据集是由一条条记录(行)
组成,每一条记录由不同的属性(列)
组成。
而在客户旅程时光轴的数据集,不仅仅有一个个的节点
(每一个节点都相当于一条记录),而且节点之间还有由连线
进行连接的关系。
举个例子,用Antv X6绘制一个Hello -> World
,需要准备的数据如下:
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
,以下是本示例在码上掘金
上的查看效果:
3、逻辑:Logic
在计算机科学中的图,就是由“节点”和“边”组成——而我们在图可视化中需要关注的重点就是构成图的“节点”以及节点之间的“边”。
在进行图应用的开发工作中,对于图的遍历是一个常用操作,常用的图遍历算法有深度优先
算法和广度优先
算法。
以下是广度优先
算法的一个实现:
breadthFirstSearch(
cell: Cell,
iterator: Model.SearchIterator,
options: Model.GetNeighborsOptions = {},
) {
const queue: Cell[] = []
const visited: KeyValue<boolean> = {}
const distance: KeyValue<number> = {}
queue.push(cell)
distance[cell.id] = 0
while (queue.length > 0) {
const next = queue.shift()
if (next == null || visited[next.id]) {
continue
}
visited[next.id] = true
if (FunctionExt.call(iterator, this, next, distance[next.id]) === false) {
continue
}
const neighbors = this.getNeighbors(next, options)
neighbors.forEach((neighbor) => {
distance[neighbor.id] = distance[next.id] + 1
queue.push(neighbor)
})
}
}
以下是深度优先
算法的一个实现:
depthFirstSearch(
cell: Cell,
iterator: Model.SearchIterator,
options: Model.GetNeighborsOptions = {},
) {
const queue: Cell[] = []
const visited: KeyValue<boolean> = {}
const distance: KeyValue<number> = {}
queue.push(cell)
distance[cell.id] = 0
while (queue.length > 0) {
const next = queue.pop()
if (next == null || visited[next.id]) {
continue
}
visited[next.id] = true
if (FunctionExt.call(iterator, this, next, distance[next.id]) === false) {
continue
}
const neighbors = this.getNeighbors(next, options)
const lastIndex = queue.length
neighbors.forEach((neighbor) => {
distance[neighbor.id] = distance[next.id] + 1
queue.splice(lastIndex, 0, neighbor)
})
}
}
在进行客户旅程时光轴图相关实现时,尤其是进行自定义图的布局算法时,深度优先
算法和广度优先
算法都会有他们的应用场景。
在接下来的系列文章中,我们将会从“节点
”、“连线
”、“标签
”、“布局
”、“动画
”这5个要素出发,通过结合上面的客户旅程时光轴图Demo案例,进行详细的分解和讲述,希望能帮助到有需要的朋友: