echarts拓扑图_使用 d3.js 绘制资源拓扑图

eb7a8712c688b346f4bc937e6b7b0d5c.png

596f14160e42452f6a159e11d3d5c847.png

wzb

网易游戏高级开发工程师,现主要负责 CMDB 的前端开发工作

背景

随着业务的发展,项目下的各种资源会越来越多,越来越复杂。如何提供一种让用户快捷查看全局资源与模型关联关系的能力呢?资源拓扑图便是一种很好的方式。

本文将尽量简化业务上的内容,重点介绍如何使用 d3.js 来进行前端拓扑图的绘制。

为什么选择 d3?

d3.js (data driven ducument) 是一个实现数据可视化的前端 JavaScript 库。那么说到数据可视化,大家可能很快想到诸如 highchartsecharts 之类的库。而 highcharrsecharts 比较常用于柱状图,折线图,饼图等统计类相关的图表展示,对于拓扑图可能不太适合。这里想要拿出来与 d3 进行对比的是以下几个库:go.jsAntV G6 。这几个库都能较好的满足业务的需求,这里直接放出这些库的一些优缺点:

ee7496c5727cf0ad9af50821b51686b5.png

通过以上的对比,最终我们还是选择了拓展性高,稳定且 免费 的 d3.js。

PS:由于 d3 版本之间差距较大,且不是向后兼容的,本文所用的为最新的 d3 v5

svg 简介

前端可视化的库千千万,但归根结底,底层所用的技术无非就是 canvas 和 svg。d3 主要使用的是 svg。

SVG 是一种用于描述二维矢量图形的,基于 XML 的标记语言。它能和 HTML 及 CSS 一样被浏览器识别,我们可以简单的将其看作一类特殊的 HTML 元素。

这里要注意,在 HTML 中,所有的 SVG 类元素都必须嵌套在一个 中,否则浏览器不会进行渲染,这个 svg 元素相当于一个画布,有自己的尺寸,而其内部的元素默认都是基于其左上角进行定位的。

由于篇幅关系,详细的 SVG 内容这里就不再赘述,只简单介绍一些常用的 svg 元素,待会会用到。

circle

circle 用来绘制一个圆,有三个主要属性:cxcyr,分别代表圆心的 x,y 坐标及圆的半径,当然这里的圆心坐标是相对于外层 svg 画布的左上角进行定位的。三个参数都是数字类型,虽然同样是以像素作为单位,但不需要加上 px。如下所示:

<svg width="500px" height="500px">

line

line 用来绘制一条直线。两点确定一条直线,因此通过四个属性可以定位一条 linex1, y1, x2, y2,分别表示两个点的横纵坐标。

<svg width="500px" height="500px">

text

text 用来表示文字。它可以设置 xy 属性来进行定位,同时还能设置样式:

<svg width="500px" height="500px">

svg 中用 stroke 来表示线条颜色,相当于 css 中的 colorfill 表示填充色,相当于 background-color

path

path 是 svg 中的万金油元素,用它可以模拟任意形状,这主要是通过它的 d 属性来进行。d 属性实际上是一个字符串,包含了一系列路径指令。指令大小写敏感,大写的命令指明它的参数是绝对位置,而小写的命令指明是相对于当前位置:

5f120e2cfa14d554bb4963483abfb323.png

我们可以用 path 来代替 line 绘制直线:

<svg width="500px" height="500px">

textPath

textPath 可以通过其 xlink:href 属性值引用 元素实现将文字沿路径排列的效果。

<svg width="1000px" height="300px">

以上代码效果如下:

2cec8d41ffc3a195c9b9566604f5b9fc.png

d3 使用初探

介绍完 svg 的一些基本元素,那么接下来就要使用 d3 将这些元素组合起来,进行资源拓扑图的绘制。

d3 的使用很像 jQuery,需要将数据和 DOM 节点进行绑定,数据变化后,需要手动处理来绘制新的视图。这对用惯了现代前端框架的双向绑定,自动更新视图的开发者来说可能有些不适应。

操作 DOM

d3 提供了 d3.selectd3.selectAll 两个 API,根据 CSS 选择器来选取 DOM 节点。但是它们返回的并不是真正的 DOM 节点,而是会对 DOM 做一层封装,我们姑且称之为 selection。可以通过 selection.nodes() 来获取真正的 DOM 节点。

以下使用 selection 来指代通过 d3.selectd3.selectAll 选中的内容

相对应的,对于 DOM 节点上的一些 API,d3 也提供了对应的镜像版本:

d0dff73501fcf473c22ecfefdc7d0370.png

同时,d3 也能像 jQuery 一样链式的调用这些方法,从而更快捷的操作。

下面的代码会在 标签中绘制一个圆:

select(

如果使用的是 d3.selectAll,则链式调用会作用于每一个选中的元素。

数据驱动

d3 的全称是 Data-Driven Documents,而 d3 实现数据驱动主要靠以下几个 API:

selection.data

通过 selection.data 可以将数据和元素进行绑定。这里的 data 是一个数组。那么绑定了数据后,该怎么使用呢?

回到上面的绘制资源节点的例子:

select(

我们只绘制了一个圆,而且使用了魔法数字(https://g.126.fm/03DTeJa)

我们需要对数据(圆心坐标,半径)进行一个集中定义,或者从后端获取这些数据并一次性绘制出来:

const circleData = [

对比一下可以看到有以下几个改动:

  1. 我们使用了 d3.selectAll 代替了 d3.select 来选中所有 circle 元素;

  2. 使用了 selection.data 来为元素绑定数据,相当于将 selections 做了一次遍历,给每个 selection 增加一个 data 属性;

  3. selection.enter 我们暂且不管,后面再说;

  4. selection.attrselection.text 都使用了函数形式来指定设置的值。函数的入参就是单个 selection 元素所绑定的数据。

那么问题来了,如果数据中的数组长度是 3,是否意味着需要在 html 中写 3 个 circle 元素呢?也就是说,d3 是如何保证数据和元素是同步的,当数据和元素个数不匹配时,如何处理?

selection.enter

这个 API 实际上是一个过滤器,它会过滤出数据相对于元素多出来的部分。继续看上面的例子,如果 svg   元素中没有任何的 circle 元素,那么第一次调用上面的代码时,selectAll('circle') 选中的 selection 个数为 0。而此时 data(circleData) 中数组的长度为 2。因此调用 enter() 后返回的内容是一个长度为 2 的空 selection

19661d2598df70d0c02447de85a6b1ab.png

随后我们往这个空的 selection 里添加了 circle 元素,并设置它的属性。

所以,enter() 的用途是:有数据,而没有足够元素的时候,使用此方法可以添加足够的元素。

selection.exit

enter() 相反,exit() 会过滤出元素相对于数据多出来的部分,常用于数据减少后,将多余元素进行删除。

560ae14819f066278e3288d7124d464f.png

比如我们将上述生成的两个圆都删掉:

const circleData = [];
update

既然新增和删除都有对应的 API,那么元素的更新呢?只要没有调用 enter() 或者 exit(),默认选中的都是 update 的部分,如下图:

3f550b20ad7f3a3ef523de714e2fa704.png

比如,我们需要将之前例子的两个圆的半径缩小到 20:

(item) => {
selection.join

前面讲的几种情况,都只单一的处理了一种情况:

  • enter 处理数据多于元素的情况;

  • update 处理数据个数没有变化的情况;

  • exit 处理数据少于元素的情况。

而数据个数发生变化的同时,原有数据也有可能发生了变化,那么按照之前的介绍,我们需要这样写:

// update 部分

这时候就非常需要 selection.join 了,它能将几种操作进行合并,减少重复代码:

'svg')

可以看到,enter 和 update 部分还有一些相同的代码,可以进一步简化:

'svg')

d3.js 实战 —— 绘制资源拓扑图

上面简单介绍了一下 svg 基础和 d3.js 的一些使用方法,接下来我们进入实战阶段。回到我们开篇的主题,如何使用 d3.js 来绘制资源拓扑图。

业务抽象

资源拓扑图一般是由一些节点和连线组成,表示各个节点之间的关系。以下是在实际业务中的一张效果展示图:

d47e01e2699d8061fba39525245bb24c.png

我们可以把上图中的内容简单抽象成 svg 能表现的元素:

  • 节点定义成圆形 circle。当然,如果你的节点想表现的更加丰富多彩一些,比如加入一些图片或者 css 3 动画等,可以用 html 来进行绘制

  • 节点之间的连线用 path 表示,如果只有直线也可以用 line

  • 节点和连线上的文字 text 。当然,连线上的文字也可以搭配 textPath 来实现一些酷炫效果

数据组装

根据抽象出的元素类型,我们需要组装出各个元素所需要的数据内容:

  • 绘制 circle 必须传入圆心坐标和半径,同时可以定制圆形的填充色,边框色等

  • 绘制 path 必须传入起点坐标和终点坐标,同时可以定制连线的颜色,粗细,样式等

  • 绘制 text 必须传入文字坐标及文字内容,同时可以定制文字的颜色,粗细,字体等

可以发现,其中最核心的数据就是节点和连线,大致数据结构如下:

// Node

对应到具体的业务中,各个字段可能都有不同的含义和组装规则,这里就不展开了。

那么,问题来了:节点的 x, y 这两个参数从何而来?
这其实是一个纯前端使用的参数,后端开发肯定不关心你前端把这个节点摆在哪里,这意味着我们需要自己去计算每个节点的位置,即需要一个布局算法。

2f41e823e28c205b224af2d7136e0697.png

对于一些简单的多行多列布局场景,我们可以逐个计算每个节点的位置,比如下面这个布局,三层排列,包含一个中心节点,上下两层节点列宽平分:

dddde56c2876060004bb01fb54204683.png

  1. 方法一:直接绘制 html,通过 flex 实现自动布局,然后通过 DOM 操作获取各节点坐标;

  2. 方法二:通过获取容器宽高,算出每一列宽度然后计算出各节点圆心的位置。

力导向图

对于一些复杂场景,我们逐一去计算节点位置似乎不太可行。这时候不要惊慌,d3.js 为大家提供了一种强大的布局算法:力导向图(Force-Directed Graph),它可以模拟物理界的各种作用力,使节点间相互碰撞和运动并最终达到一种静止状态。它会将静止状态时的节点位置作为节点的 x 和 y 坐标。

d3.js 力导向图中提供了 5 种作用力:

  • 中心力(Centering)
    中心力作用于所有的节点而不是某些单独节点,可以将所有节点的中心一致的向指定的位置移动,而且这种移动不会修改速度也不会影响节点间的相对位置。

  • 碰撞力(Collision)
    碰撞力将每个节点视为一个具有一定半径的圆,这个力会阻止代表节点的圆相互重叠,即两个节点间会相互碰撞,可以通过 strength 来设置力的强度。

  • 弹簧力(Links)
    当两个节点通过设置 link 连接到一起后,可以设置弹簧力,这个力将根据两个节点间的距离将两个节点拉近或推远,力的强度和这个距离成比例,就和弹簧一样。

  • 电荷力(Many-Body)
    模拟所有节点间的相互作用力,如果值为正则节点间就会相互吸引,可以用来模拟电荷吸引力,如果值为负则节点间就会相互排斥。这个力的大小也和节点间的距离有关。

  • 定位力(Positioning)
    这个力可以将节点沿着指定的维度推向一个指定位置,比如通过设置 forceX 和 forceY 就可以在 X 轴 和 Y 轴方向推或者拉所有的节点,forceRadial 则可以形成一个圆环把所有的节点都往这个圆环上相应的位置推。

回到我们的场景中:

  • 节点之间通过连线表示节点之间的关系,类似于弹簧力,通过连线互相牵引;

  • 节点是有半径的,需要碰撞力来防止节点之间重合;

  • 节点布局的容器大小是固定的,为了防止节点跑出边界,需要增加一个中心力来将所有节点往容器中心推

对应的代码如下:

const simulation = d3.forceSimulation(nodes)

力导向图形成静止状态有一个计算过程,默认是自动计算的。这个计算过程的长短受两个参数影响,计算公式为:log(alphaMin) / log(1 - alphaDecay),感兴趣的同学可以参考官方文档(https://g.126.fm/00gYsZG) 。其中每一次计算(称作一个 tick)都有一个对应的布局快照,可以通过设置事件监听来对每一个快照进行操作,更新 DOM。但当数据量大时,这样做会有非常大的性能开销。

这里我们只需要使用最终的静止状态来进行绘制即可,所以上述代码使用了 .stop() 停掉布局自动计算的默认行为。然后我们采用手动触发的方式来让布局达到静止状态,此时 nodes 中每个节点都会自动带上 x 和 y 属性了:

// 手动调用 tick 使布局达到稳定状态

绘制拓扑

利用力导向图解决了节点的坐标之后,我们拿到了完整的数据,现在可以利用这些数据来进行拓扑图的绘制了:

const svg = d3.select(

这里简单用 4 个节点的数据演示一下,效果如下:

8ea5962eb6d8d28953464633047e019e.png

优化完善

上面的效果图存在很多“扎眼”的地方,我们来一一优化一下。

字体居中

svg 中,text 元素的 x 和 y 是基于字体的 baseLine 进行设置的,可以使用 dx 和 dy 来设置偏移量。但我们需要根据文字的长度来动态计算其偏移量,操作起来较为麻烦。因此我们可以换一个思路,把 text 全部换成 html 来做。对于 html 来说,字体居中就非常简单了。

.text {
    position: 'absolute';
    display: 'flex';
    align-items: 'center';
    justify-content: 'center';
}
// 节点文字
textSelection = d3.select('#wrapper')
    .selectAll('.text')
    .data(nodes)
    .join('div')
    .attr('class', 'text')
    .text(d => d.text)
    .style('left', d => `${d.x - d.r}px`)
    .style('top', d => `${d.y - d.r}px`)
    .style('width', d => `${d.r * 2}px`)
    .style('height', d => `${d.r * 2}px`);

这里要注意,数据中的 x 和 y 表示的是 circle 的圆心坐标,使用 html 时定位用的 left 和 top 是以左上角为起始的,所以需要用圆心坐标减去对应的半径

效果如下:

96c71639f09307f2c004d50cbfb3fa73.png

连线增加方向

为了表示连线的方向,我们可以给终点加上一个箭头。

我们可以利用 path 元素上的 marker-end 属性来实现这个效果。

// 绘制一个箭头图形

效果如下:

f290aaffbb226310b6ecd61512f51a38.png

看起来好像没什么区别?

那是因为我们连线的起点和终点都是圆的圆心,导致箭头被文字挡住了。

连线被文字遮挡

我们只需要保留连线与两个圆的两个交点之间的那一段就可以了,如下图红色线条:

b1636ff68b4da02639b2d660c7c2e8f6.png

那么问题来了,如何求线与圆的交点呢?

利用高中知识,我们可以通过圆和直线的方程,代入圆心坐标求得表达式,然后通过解二元方程组得出交点。

得,想想就头疼,我不想努力了。

为了节省大家解方程的时间,还是直接上法宝吧 —— 有大佬已经实现了这种算法(https://g.126.fm/019trY4) 。

我们利用这个算法求出原来的 path 路径和起点终点两个圆的两个交点,并把交点作为新的起点和终点重新绘制 path 即可。

import { Intersection, ShapeInfo } 

效果如下:

e95a776b9f3677979855dfe73d545e95.png

其他

当然,在真实的业务实现中,可能还会遇到很多其他的问题和需求:

  1. 节点太多,布局算法不是很理想。
    其实力导向图是一个极其复杂和灵活的算法,真正吃透可能需要了解很多物理学知识,就留给大家自己去消化吧。

  2. 给节点增加拖拽。
    d3.js 其实支持拖拽和缩放,限于篇幅,这里就不展开讲了。

  3. 性能
    当数据量太大时,手动触发力导向图进入静止状态也是非常耗时的,此时可能需要用到 web worker 来处理。(https://g.126.fm/04vfPbi) 

结语

d3.js 是一个非常强大的可视化库,他能实现很多复杂的场景和需求。而其本质还是数据驱动,最大的难点在于数据的组装和维护。本文只是起到一个抛砖引玉的作用,剩下的还是要靠大家自己去慢慢尝试。希望可以和大家一起学习交流。

参考资料

  • SVG 元素参考
    (https://g.126.fm/0440kH6)

  • d3.js API
    (https://g.126.fm/0309ow8)

  • d3.js 力导向图
    (https://g.126.fm/01xjKTu)

  • kld-intersections
    (https://g.126.fm/019trY4)

  • web worker 处理力导向图布局示例
    (https://g.126.fm/02cbZCM)

  • 使用 d3.js 力导布局绘制资源拓扑图

    (https://g.126.fm/00SFXeo)

eb7a8712c688b346f4bc937e6b7b0d5c.png

往期精彩

运维里的人工智能

CI构建环境下的docker build最佳实践

浏览器中执行 C 语言?WebAssembly 实践

校招面试问到Linux CPU不用怕,来看看这份宝典

终于不用为大表添加列而烦恼了!

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值