「AntV」怎样用SVG & X6制作客户旅程时光轴

最近我在我们前端平台研发团队内部做了一次技术分享——主题是《怎样用SVG & X6制作客户旅程时光轴》——总结了我最近开发的一个客户旅程图项目的相关经验,现在写成这篇文章,希望能帮助到有同样需求的各位朋友。

大家好,我叫王士江,是前端平台研发团队的一名前端开发工程师,很高兴跟大家做这次技术分享,我这次分享的主题是——《怎样用SVG & X6制作客户旅程时光轴》。

这次分享的主要内容包含以下几个方面:

1、这里的“客户旅程时光轴”是什么?

2、实现“客户旅程时光轴”的技术选型

3、SVG基础与应用

4、AntV X6基础与应用

5、涉及到的布局算法

6、参考资料

以下是本次分享的正文——

1、这里的“客户旅程时光轴”是什么?

说到“客户旅程时光轴”,那它到底是什么?我这里给大家看几张示例图,大家先直观地感受一下:

「AntV」怎样用SVG & X6制作客户旅程时光轴

👆上面这是一张旅程图——从开始到结束,中间有若干个旅程节点,节点之间有连线相连。

「AntV」怎样用SVG & X6制作客户旅程时光轴

👆上面这是一张路径图——这里的节点是不同的步骤,节点之间由一条路径连线相连。

「AntV」怎样用SVG & X6制作客户旅程时光轴

👆上面这是一张时光轴图——每个节点是时间点,各个节点由一条时间轴相连。

从上面可以看出,这里的“时光轴图”“路径图”或者“旅程图”,整体结构是非常相似的——都是由或圆形/或方形的“节点”,以及将“节点”串在一起的“边”,构成的一张“图”形:

(1)节点可以是时间点、是里程碑,从而构成时光轴图;

(2)节点可以是步骤、是地点,构成路径图、旅程图。

实际上,这里的“时光轴图”“路径图”或者“旅程图”都算是信息图——它通过信息可视化的方式,让人更容易获取信息、发现规律、掌控全局。

我这里说的“客户旅程时光轴”,其实是一种以客户的视角进行客户旅程的信息化展示。这里为了解释方便,我没有区分是“时光轴图”“路径图”或者“旅程图”,而是直接称呼为“客户旅程时光轴”。

以下是我在项目中“客户旅程时光轴”的实现DEMO:

「AntV」怎样用SVG & X6制作客户旅程时光轴

项目中实现了3个客户旅程的功能原型,这里称呼为客户旅程A、客户旅程B、客户旅程C吧——

「AntV」怎样用SVG & X6制作客户旅程时光轴

👆上面这是客户旅程A原型

「AntV」怎样用SVG & X6制作客户旅程时光轴

👆上面这是客户旅程B原型

「AntV」怎样用SVG & X6制作客户旅程时光轴

👆上面这是客户旅程C原型

项目中的实际需求大概是这样的——

  1. 客户旅程是由旅程节点组成,一行展示固定个数(比如一行6个),超出则换行显示(比如这里的客户旅程A和C);
  2. 客户的某些旅程节点是可选的,对于没有走过的旅程节点和边,需要置灰显示(比如这里的客户旅程A和C);
  3. 客户旅程走过的节点,可以通过动画的方式进行展示。

「AntV」怎样用SVG & X6制作客户旅程时光轴

2、实现“客户旅程时光轴”的技术选型

“客户旅程时光轴”本质上是一个信息图,对于我们前端开发来说属于可视化领域,那么怎么实现“客户旅程时光轴”的技术选型?

说到前端可视化,我们第一个想到的工具、可能就会是Echarts —— 它是一个图表库,通过使用它提供的各个配置项、进行简单地组合使用,就能快速实现饼图、柱状图、折线图等等。

「AntV」怎样用SVG & X6制作客户旅程时光轴

但是像Echarts这样的图表库,适合处理的是表格型数据集,而我们这里要的“客户旅程时光轴”,我们需要处理的是一个个的“节点”、以及节点之间的“边”,很明显不符合我们的需求。

这里就引入可视化领域的另一个应用场景——图可视化——图可视化可以应用在包括流程图、组织架构图、思维导图等等场景。在计算机科学中的图,就是由“节点”和“边”组成——而我们在图可视化中需要关注的重点就是构成图的“节点”以及节点之间的“边”。

「AntV」怎样用SVG & X6制作客户旅程时光轴

我们平时前端开发用到的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标签语法来使用。

「AntV」怎样用SVG & X6制作客户旅程时光轴

<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标签:

「AntV」怎样用SVG & X6制作客户旅程时光轴

这里需要重点强调的是path标签,它是SVG中最强大的标签,前面的rect、circle、ellipse、line、polyline以及polygon等等都可以通过path来实现,使用path可以实现更复杂的图形。

比如AntV X6中的边的实现,就使用到了path标签:

「AntV」怎样用SVG & X6制作客户旅程时光轴

path标签中,我们是通过“d”属性进行图形定义,d属性中的内容是由字母和数字组成——字母代表指令,比如大写的M,是指“moveTo”(移动到)的,表示的开始移动到某个绝对坐标,小写的m表示移动到某个相对坐标;这里的数字是指令的参数,比如M后面的坐标,代表的是移动到的具体坐标。

除了M(moveTo)指令,其他可用的指令包括L(lineTo)、H、V、Z、C、S、Q、T、A等等:

「AntV」怎样用SVG & X6制作客户旅程时光轴

「AntV」怎样用SVG & X6制作客户旅程时光轴

下面在讲到AntV X6的“边的定制”(Edge)时,我们会使用到多个path标签。

(2)画家模型

SVG的渲染模型被称为画家模型——

  • 使用stroke进行描边
  • 使用fill进行填充

这里我们可以做个类比——不知道你有没有见过《秘密花园》涂鸦绘本?SVG的“画家模型”可以通过它来解释——它的黑白线稿就相当于stroke,表示描边;它的彩色涂鸦就相当于fill,表示填充。

「AntV」怎样用SVG & X6制作客户旅程时光轴

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必须关注的内容——由于我当前这个客户旅程项目有动画方面的需求,所以我在这里简单说一下这个描边动画的实现:

「AntV」怎样用SVG & X6制作客户旅程时光轴

上面在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所示:

「AntV」怎样用SVG & X6制作客户旅程时光轴

(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标签:

  1. 第一个选择器为wrap的path,是为了方便响应交互的占位元素;
  2. 第二个选择器为background的path,是置灰效果时显示的那个灰色底边;
  3. 第三个选择器为line的path,是实际展示的黑色的边;
  4. 第四个选择器为dash的path,则是为了模拟成柏油公路的白色虚线。

客户旅程图中边的最终实现效果如下:

「AntV」怎样用SVG & X6制作客户旅程时光轴

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布局的实现。

「AntV」怎样用SVG & X6制作客户旅程时光轴

在 Dagre算法中定义了几个基本概念:

(1)rankDir:图的延展方向,分为由上到下(tb)、由下到上(bt)、由左到右(lr)、由右到左(rl)四种。

「AntV」怎样用SVG & X6制作客户旅程时光轴

图 图的延展方向rankdir

(2)rank:沿着图的延展方向划分的层级,每个顶点都存在于某个层级上,一个层级上可能有多个顶点。

「AntV」怎样用SVG & X6制作客户旅程时光轴

图 图的层次划分rank

(3)level:在每个 rank 中针对每一个节点划分的级。不同 rank 中的 level 互不影响。

「AntV」怎样用SVG & X6制作客户旅程时光轴

图 布局中的层级划分

关于Dagre布局更详细的内容,我写过一篇文章《Dagre算法简介以及在流程图自动布局中的应用》,感兴趣的可以去看看。

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布局之后,我们能得到从左往右的布局的一个旅程图,效果如下——

「AntV」怎样用SVG & X6制作客户旅程时光轴

但是项目上,我们有一个需求是一行展示固定个数(比如一行6个),超出则换行显示(比如这里的客户旅程A和C)——

「AntV」怎样用SVG & X6制作客户旅程时光轴

这时候我们需要在Dagre算法基础之上,再使用网格布局处理一次。

(2)网格布局

网格布局是一种几何布局,我们可以通过把旅程节点划分到各个行和列中,然后根据节点所在的行和列、计算出节点实际所处的坐标。

「AntV」怎样用SVG & X6制作客户旅程时光轴

在客户旅程项目中,跟正常网格布局稍微不一样的地方是,这里的旅程节点需要折行显示——

「AntV」怎样用SVG & X6制作客户旅程时光轴

在这里我们做了如下处理:

  1. 对于奇数行,节点y坐标相同,x坐标值是节点的列号(columnNumber)进行计算;

  2. 对于偶数行,节点y坐标相同,x坐标值需要在上述步骤1的基础之上进行位置兑换。「AntV」怎样用SVG & X6制作客户旅程时光轴

6、参考资料

在客户旅程项目开发过程中翻阅了大量资料,以下是推荐大家进行延伸阅读的参考资料:

原文链接:https://juejin.cn/post/7247167843496722490 作者:王士江WangJohn

(0)
上一篇 2023年6月23日 上午11:08
下一篇 2023年6月24日 上午10:00

相关推荐

发表回复

登录后才能评论