CocaColf

庭前桃李满,院外小径芳

实现类桑基图展示数据关系

2020-09-01


最近开发的页面中,有如下所示的板块,它展示了左右两侧信息的数据关系。

效果图


最初,交互设计师和我说这种设计图叫桑基图。我检索后,发现 ECharts 就有桑基图效果,于是我想可以基于其进行二次开发。在阅读文档后,我发现 ECharts 的桑基图可以自定义的部分不多,非常不灵活,我们的左侧和右侧板块的效果完全无法实现。好了,那就自己来了。

分析

将整个设计抽象为三个组件:

左侧和右侧很好实现,因此问题在于连线部分。连线部分主要有两点:

实现

实现连线我有两种方案可选: Canvas 或 Svg 实现。我发现这个场景特别适合 Svg,因为它和 HTML 可以结合得很好,而且连线的绘制非常简单,使用它提供的 path 标签即可连线。两点确定一条线,那么问题的关键就成了如何确定起始坐标。

结构设计

从 HTML 结构自上而下来说,整个视图分为两层:

  1. 第一层是组件层,即容纳左侧“表格”和右侧球信息的层
  2. 第二层是 Svg 连线层

这两层都容纳在同一个大容器内,每一层的宽度和高度都和最外层的容器一样大小。这里必须要将连线层放在组件层之下,因为如果不这样,那么组件层中像鼠标悬浮到某个元素显示信息气泡的效果就无法实现,因为鼠标悬浮到的实际上是连线层。

结构图

<div class="container">
	<line />
	<component-layer />
</div>

连线

开始绘制连线,以左侧某一行和右侧连接为例:

如何获取起点?起点是左侧柱子的位置坐标,我们可以通过获取柱子的 dom 元素,使用 getBoundingClientRect api 获取这个元素相对于容器的位置信息 leftSide,那么这个柱子的位置则是 (leftSide.right, leftSide.top)

同理获取终点坐标 (rightSide.right, rightSide.top)

这里有个要注意的事情,我们连线是使用 Svg path 连线,那么这个坐标信息应当是相对于 Svg 层的位置。即我们要从 Svg 的起点到我们算出来的某个点的位置需要平移多少,因此我们也需要用同样的方式得到 Svg 层的坐标信息 SvgPosition。

到这里,某一根线的点信息就很简单了,以柱子里的黄色层为例:

let startPoint = {
    X: leftSide.right - svgPosition.left,
    Y: leftSide.top + 左边表格的某一行的右边缘柱子的高度*黄色容量所占比例 - svgPosition.top

}

let endPosition = {
    X: rightSide.left - svgPosition.left
    Y: rightSide.top + 球的半径 - svgPosition.top
}

连线示意图

不过这样的线是直线,因此可以使用一些数学计算来美化这条连线,我采用的是一次贝塞尔曲线。

interface PositionInfo {
	x: number,
	y: number
}

/**
* 一次贝塞尔曲线
*/
private oneBezire (start: PositionInfo, end: PositionInfo) {
	const TWO = 2;

    let ret = `M ${start.x} ${start.y}`;
    let cpx1 = start.x + (end.x - start.x) * CURVATURE / TWO;
    let cpy1 = start.y;
    let cpx2 = start.x / TWO + end.x / TWO;
    let cpy2 = start.y / TWO + end.y / TWO;
    ret += ` Q ${cpx1} ${cpy1} ${cpx2} ${cpy2}`;
    ret += ` T ${end.x} ${end.y}`;
    
    return ret;
}

将计算出来的 Svg path 规则赋给 Svg path 属性即可绘制连线。

其余连线的绘制都是一模一样的,因此我们只需要获取到左侧的容器 dom,遍历其所有的每一行子元素去计算位置信息即可获得所有的连线信息。

由于位置信息都是动态计算的,因此在不同的分辨率下或缩放浏览器,连线的位置都不会错乱。


其他

流珠如何实现?

使用 Svg circle 和 animateMotion 属性配置即可完成动态流珠。

<circle :cx="0"
        :cy="0"
        :r="2"
        :fill="#ccc">

    <animateMotion :path="流珠的滑动路径,和线的路径一致"
                   begin="0s"
                   dur="5s"
                   repeatCount="indefinite"
                   rotate="auto"/> 
</circle>

Comments: