D3.js

作为一个前端,说到可视化除了听过 D3.js 的大名,常见的可视化库还有 ECharts、Chart.js,这两个库功能也很强大,但是有一个共同特点是封装层次高,留给开发者可设计和控制的部分太少。和 EChart、Chart.js 等相比,D3.js** 的相对来说自由度会高很多,得益于 D3.js **中的 SVG 画图对事件处理器的支持,D3.js 可将任意数据绑定到文档对象模型(DOM)上,也可以直接操作对象模型(DOM)完成 W3C DOM API 相关操作,对于想要展示自己设计图形的开发者,D3.js 绝对是一个不错的选择。

d3-force 力导向图

以实现一个关系网来说,d3-force 力导向图是不二的选择。d3-force 是 D3.js 实现以模拟粒子物理运动的 velocity Verlet 数值积分器的模块,可用来控制粒子和边秩序。在力导向图中,d3-force 中的每个节点都可以看成是一个放电粒子,粒子间存在某种斥力(库仑斥力)。同时,这些粒子间被它们之间的“边”所牵连,从而产生牵引力。

而 d3-force 中的粒子在斥力和牵引力的作用下,从随机无序的初态不断发生位移,逐渐趋于平衡有序。整个图只有点 / 边,图形实现样例较少且自定义样式居多。

下图就是最简单的关系网图,想要实现自己想要的关系网图,还是动手自己实现一个 D3.js 力导向图最佳。

构建 D3.js 力导向图

在这里实践过程中,我们用 D3.js 力导向图来对图数据库的数据关系进行分析,其节点和关系线直观地体现出图数据库的数据关系,并且还可以关联相对应的图数据库语句完成拓展查询。进阶来说,可通过对文档对象模型(DOM)的直接操作同步到数据库进而更新数据,当然操作这个比较复杂, 不在本文中详细讲述。

下面,我们来实现一个简单的力导向图,初窥 D3.js 对数据分析的作用和显示优化的一些思路。首先我们创建一个力导向图:

this.force = d3        .forceSimulation()        // 为节点分配坐标        .nodes(data.vertexes)        // 连接线        .force('link', linkForce)        // 整个实例中心        .force('center', d3.forceCenter(width / 2, height / 2))        // 引力        .force('charge', d3.forceManyBody().strength(-20))        // 碰撞力 防止节点重叠        .force('collide',d3.forceCollide().radius(60).iterations(2));

通过上述代码,我们可以得到下面这样一个可视化的节点和关系图。

实现拓展查询显示优化

看到关系图(上图),我们会发现有一个新需求:选中节点继续往下拓展查询。为了实现拓展查询,在这里笔者要介绍下 D3.js 自带 API。

D3.js 的 enter() API 可对新增的节点作单独的逻辑处理,所以当拓展查询到新的节点 push 进节点数组时,不会去改变之前存在的节点信息(包括 x,y 坐标),而是按照 d3-force 实例分配的坐标进行渲染。从 API 上理解来说确实是这样,但是新增的节点对于 d3-force 这个已经存在的实例来说是一个不是简单的 push 就能处理的。因为 d3.forceSimulation() 这个模型给当前节点分配的位置坐标(x,y)是随机,目前看来没什么问题对不对?

但由于d3.forceSimulation().node() 的坐标随机分配导致了图形拓展出来位置的随机出现,加上之前 d3-force 实例中我们设定好的 collide(碰撞力)和 links (引力)参数,所以和新节点相关联的节点受到牵引力影响互相靠近。在靠近的过程中又会和其他节点发送碰撞力的作用,当力导图存在的节点的情况下,这些新增节点出现时会让整个力导向图在 collide 和 links 的作用下不停地碰撞,进行牵引,直到每个节点都找到自己合适的位置,即碰撞力和牵引力都满足要求时才停止移动,看看下图,像不像宇宙大爆炸 。

上述无序到有序熵减的过程,站在用户角度,每新增一个节点导致整个力导图都在一直在动,除了有一种抽搐的感,停止图形变化又需要长时间的等待,这是不能接受的。可 D3.js API enter() 又是这样定义规定好的,难道新增的节点和之前的节点的呈现处理需要开发者分开单独处理吗?如果是分开单独处理,每次节点渲染都要遍历判断是不是新增,在节点较多时反而更影响性能?那么如何优化这个新增节点呈现的问题呢?

网上解决新增节点呈现问题,大多采用减小 d3-force 实例 collide,增大 links 的 distance 参数值,这样会让节点很快地找到平衡点从而使整个力导图稳定下来,这确实是一个好办法。但是这样节点之间的连线长度相差明显,而且图形整体偏大,对于大数据量的 case 来说,这种显示方式并不太适合。

基于上述的方法,笔者有了另一种解决思路——保证新增节点是在该选中拓展的节点周围,也就是说直接把新增节点的坐标设置为对应选择拓展节点一样的 x,y 坐标而不用 D3 .forceSimulation().node() 分配,这样利用 d3-force 这个实例的节点碰撞力,保证新增节点的出现都不会覆盖,最终会在选中拓展节点周围出现。 这样处理虽然还是对新增节点小的范围内的节点有影响,但相对来说,不会大幅度地影响整个关系图形走势。关键代码如下:

# 给新增的坐标设置为拓展起点中心或整个图中心addVertexes.map(d => {  d.x = _.meanBy(selectVertexes, 'x') || svg.style('width') / 2;  d.y = _.meanBy(selectVertexes, 'y') || svg.style('heigth') / 2;});

如果没有选中节点(既添加起点)则该起点坐标位置就在图中心位置,对已存在的节点来说,影响程度会小很多,这还是一个很不错的思路,这个解决办法可以推荐一下。

除了新增节点的呈现问题,整个图形的呈现还有另外一个问题:两点之间多边优化显示处理。

两点之间多边优化显示处理

当两个节点之间存在多条边关系时,默认连接线是直线的情况下肯定会出现多线覆盖。因此曲线连接便成了我们的另外需要解决的问题。 曲线如何定义弯曲度保证两点之间的多条线不会交互覆盖呢?在多条线弯曲下,如何平均半圆弧弯曲避免全跑到某半圆弧上?定义曲线弧方向?

上述问题都是下一步需要解决的问题,其实问题的解决方法也不少。目前笔者采用了先统计下两点之间的线条数,再将这些连接线分配到一个 map 里,两个节点的 name 字段进行拼接做成 key,这样计算得到两点之间的连接线总数。

然后在遍历时同 map 的线根据方向分成正向、反向两组,正向组遍历给每条线追加设置一个 linknum 编号,同理,反向组遍历追加一个 -linknum 编号值。这个正向、反向判断方法很多,笔者是根据节点 source.name、target.name 进行比较,btw,这里其实是比较 ASCII 码。定义连接线的正反方向办法太多了,用两点之间的任意固定字段比较即可,在这里不做赘述。而我们设定的 linknum 值就是来确定该条弧线的弯曲度和弯曲方向的,这里搭配下面代码讲解比较好理解:

  const linkGroup = {};  // 两点之间的线根据两点的 name 属性设置为同一个 key,加入到 linkGroup 中,给两点之间的所有边分成一个组  edges.forEach((link: any) => {    const key =      link.source.name < link.target.name        ? link.source.name + ':' + link.target.name        : link.target.name + ':' + link.source.name;    if (!linkGroup.hasOwnProperty(key)) {      linkGroup[key] = [];    }    linkGroup[key].push(link);  });  // 遍历给每组去调用 setLinkNumbers 来分配 linkum  edges.forEach((link: any) => {    const key = setLinkName(link);    link.size = linkGroup[key].length;    const group = linkGroup[key];    const keyPair = key.split(':');    let type = 'noself';    if (keyPair[0] === keyPair[1]) {      type = 'self';    }    setLinkNumbers(group, type);  });
#根据不同方向分为 linkA,linkB 两个数组,分别分配两种 linknum,从而控制上下椭圆弧export function setLinkNumbers(group) {  const len = group.length;  const linksA: any = [];  const linksB: any = [];  for (let i = 0; i < len; i++) {    const link = group[i];    if (link.source.name < link.target.name) {      linksA.push(link);    } else {      linksB.push(link);    }  }  let startLinkANumber = 1;  linksA.forEach(linkA=> {    linkA.linknum = startLinkANumber++;  }  let startLinkBNumber = -1;  linksB.forEach(linkB=> {    linkB.linknum = startLinkBNumber--;  }}

按照我们上面描述的思路,给每条连接线分配 linknum 值后,接着在实现监听连接线的的 tick 事件函数里面判断 linknum 正负数判断设置 path 路径的弯曲度和方向 就行了,最终效果如下图

结语

好了,以上便是笔者使用 D3.js 力导向图实现关系网的优化思路和方法。其实要构建一个复杂的关系网,需要考虑的问题很多,需要优化的地方也很多,今天给大家分享两个最容易遇到的新节点呈现、多边处理问题,后续我们会继续产出 D3.js 优化系列文,欢迎订阅 Nebula Graph 头条。

作者有话说:Hi,我是 Nico,是 Nebula Graph 的前端工程师,对数据可视化比较感兴趣,分享一些自己的实践心得,希望这次分享能给大家带来帮助,如有不当之处,欢迎帮忙纠正,谢谢~

d3 svg path添加文本_D3.js 力导向图的显示优化相关推荐

  1. echarts力导向图节点连线动画_D3.js 力导向图的显示优化(二)- 自定义功能

    摘要: 在本文中,我们将借助 D3.js 的灵活性这一优势,去新增一些 D3.js 本身并不支持但我们想要的一些常见的功能:Nebula Graph 图探索的删除节点和缩放功能 前言 在上篇文章中(D ...

  2. D3.js 力导向图的显示优化

    D3.js 作为一个前端,说到可视化除了听过 D3.js 的大名,常见的可视化库还有 ECharts.Chart.js,这两个库功能也很强大,但是有一个共同特点是封装层次高,留给开发者可设计和控制的部 ...

  3. d3力导向图增加节点_d3.js力导向图节点如何都显示在边框内

    最近用到d3.js中的force力导向图,想实现效果如下,所有城市节点都在可视范围内,如果超出有滚动条也可以. 遇到的问题是,当节点一多,有的节点就会跑到外面去,这边是通过加大charge相互作用力, ...

  4. d3 svg path添加文本_数据可视化——D3展现数据最炫丽的一面

    热情的或--有温度的"1" 大家好,大家肯定很好奇,数据能是什么样子嘛,不就是干巴巴的1.2.3-!哟,这个火热的"1"好像是挺绚丽的啊,但对不起,这只是数字, ...

  5. d3 svg path添加文本_10 倍高清不花!大麦端选座 SVG 渲染

    作者 | 阿里文娱无线开发专家 波涛 责编 | 夕颜 出品 | CSDN(ID:CSDNnews) 背景介绍 用户在大麦上购票,需要自行选座.在大型场馆下,如何让 10 万+座位绘制达到闪开?这需要技 ...

  6. d3.js 力导向图 关系连接线 使用 path 标签时不显示的问题

    由于d3.js版本差异,再用d3开发力导向图时,使用path标签时可能会出现不显示的问题,特记录该问题. // links 连接线设置,使用path标签 _this.links = g.append( ...

  7. D3.js 力导向图来处理拓扑图

    记录一点碰到的问题和解决方案.感觉国内关于D3.js 4.0版本的相关资料还是少. 力导向图布局 D3一种布局的方式,可以将你nodes links的节点数据转换成可以绘制的坐标点数据,然后通过svg ...

  8. d3.js力导向图使用详解

    创建一个力导向图需要三个东西: 仿真模拟系统 节点 力 当然,一般我们也会创建links(边)来连接两个节点,例如上图 仿真模拟系统中存在多个节点和多种类型的力,通过力控制节点的运动,每个节点都在多个 ...

  9. vis.js力导向图第三弹——双击扩展节点

    正经学徒,佛系记录,不搞事情 基于上文:https://blog.csdn.net/qq_31748587/article/details/84143153 的项目 不能扩展节点的图是没有灵魂的,vi ...

最新文章

  1. OpenCV源代码编译
  2. 离群?异常?新类?开集?分布外检测?一文搞懂其间异同!
  3. CXF+Spring+Tomcat简明示例
  4. CVPR 2020 运行SGMN遇到的问题及解决办法
  5. jsp界面自动生成文件注释_实施注释界面
  6. python+[:]+切片_我从C ++到Python的方式:概念上的改变
  7. SQL Server开发接口生成方法
  8. oracle光标位置无效,解决在Form表单中光标移动不了问题
  9. java.io.FileNotFoundException:/mnt/sdcard/......(Permission denied)
  10. 【实习之T100开发】Linux 学习笔记
  11. C++ IO库:cmd读写,字符串读写,文件读写,<<重载,标准输出
  12. linux编译android源码,ubuntu16.04 Android源码下载编译
  13. 王者荣耀scratch版
  14. python组态开发_开发监控云组态软件的组成
  15. UCHome源码阅读
  16. 微信小程序富文本解析点击图片放大_小程序富文本提取图片可放大缩小
  17. OSPF虚链路与认证
  18. 人家出轨你为什么那么嗨
  19. 【C++】通信录管理系统
  20. 微信jssdk录音功能开发记录

热门文章

  1. 【春秋云境】 CVE-2022-24663复现
  2. 多线程在PyQt5中的应用记录
  3. linux sed f,Linux Sed 命令详解
  4. 12 系统数据库和数据库工具
  5. MPHY协议解读三:8b10b编码
  6. 一个电脑板绘初学者的自述,设计基础
  7. 计算广告的历史、现状及未来
  8. 基于微服务的司机注册与实名认证(活体检测)
  9. java软件工程师就业招聘信息_Java软件工程师就业前景为什么这么好呢?
  10. TabControl 选项卡控件