d3.js 重力节点布局_节点间多关系显示处理

本文介绍了如何在d3.js的重力布局中处理节点间多关系,确保多条线不叠加并保持美观。通过统计两点间的线条数并分配到map中,根据节点名设置方向和编号,利用tick方法配置线条样式,实现曲线连接。同时讨论了linePadding参数对箭头布局的影响,提供了解决箭头拥挤的方法。

d3重力布局——节点间多关系处理

相关的知识:

path相关的知识

<path>标签是d3里面功能最为丰富的标签,很多图形用该标签可以制作出来。它通过一系列的坐标点来绘制。在d3的图形绘制中。经常会用到。

用法:给出一个坐标点,在坐标点前面添加一个英文字母,表示如何运动到此坐标点。

英文字母的按照功能可分为5类:

  • 移动类
    • M = moveto:将画笔移动到指定坐标
  • 直线类
    • L = lineto:画直线到指定坐标。
    • H = horizontal lineto:画水平线到指定坐标。
    • V = vertical lineto:画垂直线到指定坐标。
  • 曲线类
    • C = curveto:画三次贝塞尔曲线经两个指定控制点到达最终坐标。
    • S = shorthand/smooth curveto:与前一条三次贝塞尔曲线,第一个控制点为前一条曲线第二个控制点的对称点,只需要输入第二个控制点和终点,即可绘制一个三次贝塞尔曲线
    • Q = quadratic Bezier curveto:画一个二次贝塞尔曲线经过一个指定控制点到达终点坐标
    • T = Shorthand/smooth quadratic Bezier curveto:与前一条贝塞尔曲线相连,控制点为前一条二次贝塞尔曲线控制点的对称点。只需输入终点,即可绘制一个二次贝塞尔曲线。
  • 弧线类
    • A = elliptical arc:画椭圆曲线到指定坐标
  • 闭合类
    • Z = closepath:绘制一条直线,连接终点和起点,用来封闭图形。

到这里,我们知道了绘制一个<path>所要知道的基本的英文字母的基本意思

问题解析

然后就开始处理我们的核心问题:节点间的多关系

在实际应用场景中,人与人之间的关系不仅仅是单向,更多的是多项的,例如:小明 — 朋友 — 小红 ,小明 — 同学 — 小红 ;或者 小红— 朋友 — 小明 ,小红 — 同学 — 小明。

如果在d3的重力导向布局中如果不对这种节点间的关系做处理,很多关系会叠加载一块,影响美观与观感。
在这里插入图片描述
在现有的数据可视化框架中也有相关案例,例如echarts。你只需要设置lineStyle的属性curveness的值就可以使得线条呈现一个弧度。但是由于项目的特殊性,很多情况下需要我们用d3来完成自由度更高,并且一些个定制化的服务。
在这里插入图片描述
因此,在d3关于节点间的多关系处理中,曲线连接是一种比较好的方案。因此解决问题的核心在于如何绘制曲线,并且保证两点之间的多条线不会覆盖?在多条线弯曲下,如何平均半圆弧弯曲避免全跑到某半圆弧上?定义曲线弧方向?

首先统计下两点之间的线条数,再将这些连接线分配到一个 map 里,两个节点的 name 字段进行拼接做成 key,这样计算得到两点之间的连接线总数。

然后在遍历时同 map 的线根据方向分成正向、反向两组,正向组遍历给每条线追加设置一个 linknum 编号,同理,反向组遍历追加一个 -linknum 编号值。这个正向、反向判断方法很多,根据节点 source.name、target.name 进行比较,这里其实是比较 ASCII 码。而我们设定的 linknum 值就是来确定该条弧线的弯曲度和弯曲方向的,这里搭配下面代码讲解比较好理解:

关系处理

//在vue的methods的方法中
dealRealtions(links) {    
      var linkGroup = {};  //用来分组,将两点之间的连线进行归类
      var linkMap = {};  //对连接线的计数
      for (var i = 0; i < links.length; i++) {
        var key = links[i].source.name < links[i].target.name ? links[i].source.name + ':' + links[i].target.name : links[i].target.name + ':' + links[i].source.name;
        if (!linkMap.hasOwnProperty(key)) {
          linkMap[key] = 0 ;
        }
        linkMap[key] += 1;
        if (!linkGroup.hasOwnProperty(key)) {
          linkGroup[key] = [];
        }
        linkGroup[key].push(links[i]);
      }
      //为每一条连接线分配size属性,同时对每一组连接线进行编号  
      for (var i = 0; i < links.length; i++) {
        var key = links[i].source.name < links[i].target.name ? links[i].source.name + ':' + links[i].target.name : links[i].target.name + ':' + links[i].source.name;
        links[i].size = linkMap[key];
        //同一组的关系进行编号  
        var group = linkGroup[key];
        var keyPair = key.split(':');
        var type = 'noself';          
        if (keyPair[0] == keyPair[1]) {  //指向两个不同实体还是同一个实体
          type = 'self';
        }
        this.setLinkNumber(group, type); //给关系编号
      }
    },
setLinkNumber(group,type){
      if(group.length==0) return;
      //对该分组内的关系按照方向进行分类,此处根据连接的实体ASCII值大小分成两部分
      var linksA = [], linksB = [];
      for(var i = 0 ;i < group.length; i++){
        var link = group[i];
        if(link.source.name < link.target.name){
          linksA.push(link);
        }else{
          linksB.push(link);
        }
      }
      console.log(linksA,linksB)
      //确定关系最大编号。为了使得连接两个实体的关系曲线呈现对称,根据关系数量奇偶性进行平分。
      //特殊情况:当关系都是连接到同一个实体时,不平分
      var maxLinkNumber = 1;
      if(type=='self'){
        maxLinkNumber = group.length;
      }else{
        maxLinkNumber = group.length%2==0?group.length/2:(group.length+1)/2;
      }
      console.log(maxLinkNumber)
      //如果两个方向的关系数量一样多,直接分别设置编号即可
      if(linksA.length==linksB.length){
        var startLinkNumber = 1;
        for(var i=0;i<linksA.length;i++){
          linksA[i].linknum = startLinkNumber++;
        }
        startLinkNumber = 1;
        for(var i=0;i<linksB.length;i++){
          linksB[i].linknum = startLinkNumber++;
        }
      }else{//当两个方向的关系数量不对等时,先对数量少的那组关系从最大编号值进行逆序编号,然后在对另一组数量多的关系从编号1一直编号到最大编号,再对剩余关系进行负编号
        //如果抛开负号,可以发现,最终所有关系的编号序列一定是对称的(对称是为了保证后续绘图时曲线的弯曲程度也是对称的)
        var biggerLinks,smallerLinks;
        if(linksA.length>linksB.length){
          biggerLinks = linksA;
          smallerLinks = linksB;
        }else{
          biggerLinks = linksB;
          smallerLinks = linksA;
        }

        var startLinkNumber = maxLinkNumber;
        for(var i=0;i<smallerLinks.length;i++){
          smallerLinks[i].linknum = startLinkNumber--;
        }
        var tmpNumber = startLinkNumber;

        startLinkNumber = 1;
        var p = 0;
        while(startLinkNumber<=maxLinkNumber){
          biggerLinks[p++].linknum = startLinkNumber++;
        }
        //开始负编号
        startLinkNumber = 0-tmpNumber;
        for(var i=p;i<biggerLinks.length;i++){
          biggerLinks[i].linknum = startLinkNumber++;
        }
      }
    },

然后在d3的tick方法中进行样式配置

function tick () {
        path.attr('d', function(d) { //连接线
          var linePadding = 0;  //给连线到节点间的距离
          var deltaX = d.target.x - d.source.x,
              deltaY = d.target.y - d.source.y,
              dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY),
              normX = deltaX / dist,
              normY = deltaY / dist;
          var sourceX = d.source.x + (linePadding * normX),
              sourceY = d.source.y + (linePadding * normY),
              targetX = d.target.x - (linePadding * normX),
              targetY = d.target.y - (linePadding * normY);
          if (d.target == d.source) {
            dr = 38/d.linknum;
            return"M" + sourceX + "," + sourceY + "A" + dr + "," + dr + " 0 1,1 " + targetX + "," + (targetY+1);
          }else if (d.size%2!=0&&d.linknum==1) {
            return 'M'+ sourceX +' '+sourceY+' L '+ targetX +' '+targetY;
          }
          var curve =1.5;
          var homogeneous=1.2;
          var dr = Math.sqrt(deltaX * deltaX + deltaY * deltaY) * (d.linknum + homogeneous) / (curve * homogeneous);
          //当节点编号为负数时,对弧形进行反向凹凸,达到对称效果
          if(d.linknum<0){
            dr = Math.sqrt(deltaX*deltaX+deltaY*deltaY)*(-1*d.linknum+homogeneous)/(curve*homogeneous);
            return "M" + sourceX + "," + sourceY + "A" + dr + "," + dr + " 0 0,0 " + targetX + "," + targetY;
          }
          return "M" + sourceX + "," + sourceY + "A" + dr + "," + dr + " 0 0,1 " + targetX + "," + targetY;
        });
        ....
 }

实现效果如下
在这里插入图片描述

附加

在tick方法中我们设置linePadding,用来配置path到节点的距离,设置的参数大于节点半径的时候,连线的末端会显示在节点外边,但是会出现path的箭头拥堵在一块。如果只是展示单一关系的图,则不需要调整。

否则:需要将参数设置为0,显示在节点底下,通过设置箭头的refX和refY的参数来调整箭头在path的显示位置,一般>=节点的半径。这样就可以简单的将箭头不至于挤在一块,但是这也仅仅只是在节点间的关系比较少的时候能够显得好看点,两个节点关系如果很多,则还是会出现拥挤。

注:参考如下
D3.js 力导向图的显示优化
【D3.js数据可视化系列教程】(三十二)-- 力导向图之弧形箭头连线
d3.js多重力导向图多条关系线,mouseover只显示相关节点

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值