可视化 |【d3】力导向关系图

📚目标效果

  • 力导向关系图
    在这里插入图片描述
  • 人物详情
    在这里插入图片描述
  • 子图高亮
    在这里插入图片描述

📚html和css

  • html放一个div框:<div class="network"></div>
  • css主要完整高亮后的透明度设置以及悬浮提示框的样式设置。
    .link.inactive,
    .linetext.inactive,
     .node.inactive image,
     .node.inactive text {
         opacity: .2;
     }
     .tooltip {
         font-family:  "KaiTi", "serif";
         font-size: 12px;
         width: 220px;
         height: auto;
         position: absolute;
         background: #fff;
         opacity: 0.5;
         border-radius: 5px;
         box-shadow: 0 5px 10px rgba(0, 0, 0, .2);
     }
     .tooltip .title {
         color: #fff;
         padding: 5px;
         font-size: 14px;
         background-color: #d2a36c;
         border-radius: 5px 5px 0 0;
     }
     .tooltip .detail-info {
         width: 100%;
         border-collapse: collapse;
         border: 1px solid #d2a36c;
     }
     .tooltip .detail-info td {
         padding: 3px 5px;
         color: #666;
         vertical-align: middle;
     }
     .tooltip .detail-info tr:nth-of-type(odd) {
         background: #f9f9f9;
     }
     .tooltip .detail-info td.td-label {
         color: #333;
         width:60px;
     }
     .tooltip .detail-info td a {
         color: #666
     }
    

📚js


  • 外部引入js
    <script type = "text/javascript" src = "./assects/js/d3/d3.v4.js"></script>
    <script type = "text/javascript" src = "./assects/js/jquery/jquery-1.9.1.js"></script>
    
  • 下述为自定义部分⭐️

🐇整体框架

  • 分为两部分:创建 + 应用

    d3.json("./data/people.json", function(json) {
      // 创建部分
      function GroupExplorer(wrapper,config){...}
      // 实例应用
      new GroupExplorer('.network',{
          data:json
      });
    });
    
  • 数据采用外部json导入,基本格式适合桑基图的数据格式一样的。nodes放诗人具体信息,links放诗人之间的联系。

    {
      "nodes": [
         {
           "name": "元稹",
           "zihao": "字微之,别字威明",
           "dynasty": "中唐",
           "age": "52岁(779年~831年)",
           "epithet": "与白居易并称“元白”",
           "deeds": "提倡“新乐府”,倡导“古文运动”",
           "famous": "《乐府古题序》、《莺莺传》",
           "image": "元稹.png",
           "link": "https://so.gushiwen.cn/authorv_201a0677dee4.aspx"
         },
         ...
       ],
       "links": [
         {
           "source": 14,
           "target": 29,
           "value": "好友"
         },
         ...
       ]
     }
    

🐇细说创建部分

  • 准备工作:设置了初始配置,为可视化准备数据,并使用D3.js为绘制网络图设置了带有缩放功能的SVG画布。

    • 默认配置:定义了网络图的默认配置,包括初始数据、宽度、高度和节点之间的距离。
    • 配置扩展:使用jQuery的 $.extend() 方法获取数据值。
    • 数据转换: 遍历连接并检查源和目标是否不是数字(假设它们是节点的名称)。如果是,则根据名称找到相应的节点,并将它们分配为连接的源和目标。
    • 初始化: 初始化主要函数的变量,并设置初始高亮节点以及用于存储依赖节点、连接和文本的数组。
    • 缩放功能: 定义了一个缩放处理函数(zoomed)来根据缩放事件对可视化进行转换。还设置了一个具有特定缩放范围和缩放事件处理程序的d3缩放行为。
    • SVG绘制: 选择.network元素,并在其上附加一个SVG画布,设置其宽度和高度,附加缩放行为,并禁用双击缩放。然后附加一个组元素来存储所有的节点、连接和文本。
    // 默认配置对象
    var defaultConfig = {
        data:{"nodes":[],"links":[]},
        // 初始宽高为视口宽高
        width:window.innerWidth,
        height:window.innerHeight,
        distance: 96
    };
    // jQuery库中的 ​$.extend()​方法,获取数据值
    $.extend(true,defaultConfig,config);
    
    // link里不是用名字匹配而是用数字(也就是对应对象的索引,即节点数组的顺序)
    // 检查是否存在名字,进行索引转换
    defaultConfig.data.links.forEach(function (e) {
        if(typeof e.source != "number" && typeof e.target != "number"){
            var sourceNode = defaultConfig.data.nodes.filter(function (n) {
                    return n.name === e.source;
                })[0],
                targetNode = defaultConfig.data.nodes.filter(function (n) {
                    return n.name === e.target;
                })[0];
            e.source = sourceNode;
            e.target = targetNode;
        }
    });
    
    //d3画图start,首先初始化
    var _this = this,highlighted = null,dependsNode = [],dependsLinkAndText = [];
    
    // 将缩放变换应用到容器元素
    this.zoomed = function(){
        _this.vis.attr("transform", d3.event.transform);
    };
    // 设置缩放范围和缩放响应函数。
    var zoom = d3.zoom()
            .scaleExtent([0.2,10])
            .on("zoom",function(){
                _this.zoomed();
            });
    
    // 选择body元素,并创建一个SVG画布,并应用缩放功能
    this.vis = d3.select(".network").append("svg:svg")
            .attr("width", defaultConfig.width)
            .attr("height", defaultConfig.height)
            // 实现双击还原
            .call(zoom).on("dblclick.zoom", null);
    // 分组元素,用于放置所有的节点、连接线和文字
    this.vis = this.vis.append('g').attr('class','all')
            .attr("width", defaultConfig.width)
            .attr("height", defaultConfig.height)
    
  • 创建力导向布局和力模型

    this.force = d3.forceSimulation()
    	// 指定节点
    	.nodes(defaultConfig.data.nodes)
    	// 引力
    	.force("link", d3.forceLink(defaultConfig.data.links).distance(defaultConfig.distance))
    	.force("linkForce", d3.forceLink().id(function(d) { return d.id; }).distance(50))
    	// 斥力
    	.force("charge", d3.forceManyBody())
    	// 设置可视化中心。
    	.force("center", d3.forceCenter(defaultConfig.width / 2, defaultConfig.height / 2))
    	// 将碰撞力指定为 ​"collide"​ 类型的力,使节点保持至少 ​60​ 个单位的距离,碰撞的强度为 0.2,使节点之间有一定的距离。​
    	// .iterations(5)​ 设置迭代次数为 5,提高碰撞的精确度。
    	.force("collide",d3.forceCollide(61).strength(0.2).iterations(5))
    	.alphaTarget(1)
    
  • 绘制连接线并添加箭头标记及文本标记:这里对“好友”关系作特殊处理(因为data里“好友”关系占大多数,对关系图的呈现有一定影响,故作特殊处理:连接线为绿色且后续不标注value值关系描述)。

    // 创建箭头​<marker>​元素,之后可以使用该箭头标记在连接线的末端添加箭头效果。
    this.vis.append("svg:defs").selectAll("marker")   
            .data(["end"])
            // 创建一个 ​<marker>​ 元素
            .enter().append("svg:marker")
            .attr("id","arrow")
            .attr('class','arrow')
            // 定义了在通过视图呈现时,元素起始点和围绕元素的盒子的属性
            .attr("viewBox", "0 -5 10 10")
            // 在X轴、Y轴方向上的参考点
            .attr("refX", 42)
            .attr("refY", 0)
            // 宽高
            .attr("markerWidth", 7)
            .attr("markerHeight", 12)
            .attr("markerUnits","userSpaceOnUse")
            .attr("orient", "auto")
            // 添加一个 ​<path>​ 元素,用于绘制箭头的路径
            .append("svg:path")
            // 设置路径命令
            .attr("d", "M0,-5L10,0L0,5")
            .attr('fill','#666');
    
    // 绘制连接线(link)并添加箭头标记
    this.link = this.vis.selectAll("line.link")
            .data(defaultConfig.data.links)
            .enter().append("svg:line")
            .attr("class", "link")
            .attr('stroke-width',1.8)
            // 获取起始点、目标点x,y
            .attr("x1", function(d) { return d.source.x; })
            .attr("y1", function(d) { return d.source.y; })
            .attr("x2", function(d) { return d.target.x; })
            .attr("y2", function(d) { return d.target.y; })
            .attr("stroke", function(d) {
                if (d.value === "好友") {
                return "#007175";  
                } else {
                return "gray";   // 其他关系为灰色或默认颜色
                }
            })
            .attr("marker-end", "url(#arrow)");
    
    // 为每条连线添加文本元素表示连线的关系描述
    this.linetext = this.vis.selectAll('.linetext')
      .data(defaultConfig.data.links)
      .enter()
      .append("text")
      .attr("class", "linetext")
      .attr("x", function(d){ return (d.source.x + d.target.x) / 2}) // 文本的水平位置为连线起点和终点x坐标的中间值
      .attr("y", function(d){ return (d.source.y + d.target.y) / 2}) // 文本的垂直位置为连线起点和终点y坐标的中间值
      .text(function (d) { 
          if (d.value !== "好友") {
              return d.value; 
          } else {
              return ""; 
          }
          // return d.value;
      })
      .call(d3.drag()) // 添加拖拽行为,使文本可以被拖动
      .style("font-family", "KaiTi, serif") 
      .style("font-size", "14px")
    
  • 拖拽功能的实现:核心是拖动后引斥力变化后的位置更新。

    // 重新计算力导向布局,并刷新可视化效果
    this.tick = function() {
      // 更新连接线的起始点 (x1, y1) 和终点 (x2, y2) 的位置
      _this.link.attr("x1", function(d) { return d.source.x; })
              .attr("y1", function(d) { return d.source.y; })
              .attr("x2", function(d) { return d.target.x})
              .attr("y2", function(d) { return d.target.y;});
      _this.linetext.attr("x",function(d){ return (d.source.x + d.target.x) / 2})
              .attr("y",function(d){ return (d.source.y + d.target.y) / 2});
      // 更新节点的位置
      _this.node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
    };
    
    _this.force.on("tick", this.tick);
    
    // 处理节点的拖拽事件,并创建了一个用于节点拖拽的drag行为
    var dragstart = function(d, i) {
      console.info(d3.event.subject)
      _this.force.stop();
      // 阻止事件传播,以防止与其他事件冲突
      d3.event.sourceEvent.stopPropagation();
    };
    var dragmove = function(d, i) {
      d.px += d3.event.dx;
      d.py += d3.event.dy;
      d.x += d3.event.dx;
      d.y += d3.event.dy;
      // 重新计算力导向布局,并刷新可视化效果
      _this.tick();
    };
    var dragend = function(d, i) {
      d3.event.subject.fx = null;
      d3.event.subject.fy = null;
      // 重新启动力导向布局的运行
      _this.force.restart();
      _this.tick();
    };
    this.nodeDrag = d3.drag()
          .on("start", dragstart)
          .on("drag", dragmove)
          .on("end", dragend);
    
  • 悬浮框内容设置:这里的悬浮框相当于一个小html,相关样式在css中设置,这里涉及到的鼠标动作目的是确保悬浮框在使用过程中能够正确地显示、隐藏,并且在鼠标交互中具有合适的响应和延迟效果。

    // 悬停框显示
    this.highlightToolTip = function(obj){
        if(obj){
            _this.tooltip.html("<div class='title'>" + obj.name + "的详情</div>" +
                                "<table class='detail-info'><tr><td class='td-label'>字号:</td><td>" + obj.zihao + "</td></tr>" +
                                "<tr><td class='td-label'>朝代:</td><td>" + obj.dynasty + "</td></tr>" +
                                "<tr><td class='td-label'>年龄:</td><td>" + obj.age + "</td></tr>" +
                                "<tr><td class='td-label'>称号:</td><td>" + obj.epithet + "</td></tr>" + 
                                "<tr><td class='td-label'>事迹:</td><td>" + obj.deeds + "</td></tr>" +
                                "<tr><td class='td-label'>代表作:</td><td>" + obj.famous + "</td></tr>" + 
                                "<tr><td class='td-label'>链接:</td><td><a href='" + obj.link + "'>" + obj.name + "的主页</a></td></tr></table>")
                    .style("left",(d3.event.pageX+20)+"px")
                    .style("top",(d3.event.pageY-20)+"px")
                    .style("opacity",1.0);
        }else{
            _this.tooltip.style("opacity",0.0);
        }
    };
    this.tooltip = d3.select(".network").append("div")
            .attr("class","tooltip")
            .attr("opacity",0.0)
            .on('dblclick',function(){
                // 被双击时,阻止提示框冒泡,避免影响其他事件。
                d3.event.stopPropagation();
            })
            .on('mouseover',function(){
                // 悬停时,如果之前设置了鼠标移出的定时器,则清除该定时器,以防止工具提示在短时间内被误隐藏
                if (_this.node.mouseoutTimeout) {
                    clearTimeout(_this.node.mouseoutTimeout);
                    _this.node.mouseoutTimeout = null;
                }
            })
            .on('mouseout',function(){
                // 当鼠标移出工具提示时,如果之前设置了鼠标移出的定时器,则先清除该定时器。
                // 然后,设置一个新的定时器
                // 在快速移动鼠标时工具提示不会立即消失,只有在鼠标离开一段时间后才会隐藏。
                if (_this.node.mouseoutTimeout) {
                    clearTimeout(_this.node.mouseoutTimeout);
                    _this.node.mouseoutTimeout = null;
                }
                _this.node.mouseoutTimeout=setTimeout(function() {
                    _this.highlightToolTip(null);
                }, 300);
            });
    
  • 高亮功能实现:核心是找到相关节点,将不相关的节点、连接线、连接文本设置为inactive类,搭配css里的透明度设置,实现先关节点的高亮。

    • 如果传入的对象 obj 存在,该函数将根据该对象相关的节点、连接线和文本进行高亮显示,同时取消其他元素的高亮显示。
      • 首先,将传入对象 obj 的索引添加到依赖节点数组 dependsNode 和依赖连接线和文本数组 dependsLinkAndText 中。
      • 然后遍历连接线数据,确定与指定对象相关的节点索引,并将这些节点索引添加到依赖节点数组中。
      • 最后使用classed()方法来根据依赖节点数组和依赖连接线和文本数组的内容,给节点、连接线和文本添加或移除 inactive 类,从而控制它们的显示状态。
    • 如果传入的对象 obj 不存在,那么该函数将取消所有元素的高亮显示,即移除所有节点、连接线和文本的 inactive 类。(这里针对搜索框应用(挖个坑,后续补充),如果搜索对象不存在则移除所有inactive类,使图表归位)
    // 高亮显示与指定对象相关的节点、连接线和文本,并取消其他元素的高亮显示
    this.highlightObject = function(obj) {
      if (obj) {
          // 获取要高亮显示的对象的索引
          var objIndex = obj.index;
          // 添加到依赖节点数组
          dependsNode = dependsNode.concat([objIndex]);
          // 添加到依赖连接线和文本数组
          dependsLinkAndText = dependsLinkAndText.concat([objIndex]);
          
          // 遍历连接线数据,确定与指定对象相关的节点索引,并添加到依赖节点数组中
          defaultConfig.data.links.forEach(function(lkItem) {
              if (objIndex == lkItem['source']['index']) {
                  dependsNode = dependsNode.concat([lkItem.target.index])
              } else if (objIndex == lkItem['target']['index']) {
                  dependsNode = dependsNode.concat([lkItem.source.index])
              }
          });
          
          _this.node.classed('inactive', function(d) {
              return dependsNode.indexOf(d.index) == -1;
          });
          
          _this.link.classed('inactive', function(d) {
              return dependsLinkAndText.indexOf(d.source.index) == -1 && dependsLinkAndText.indexOf(d.target.index) == -1;
          });
          
          _this.linetext.classed('inactive', function(d) {
              return dependsLinkAndText.indexOf(d.source.index) == -1 && dependsLinkAndText.indexOf(d.target.index) == -1;
          });
      } else {
          _this.node.classed('inactive', false);
          _this.link.classed('inactive', false);
          _this.linetext.classed('inactive', false);
      }
    };
    
  • 创建节点元素,绑定数据,绑定鼠标动作对应的悬浮框效果高亮效果,添加图片图标

    // 创建节点元素,并绑定相关的数据和事件处理函数
    this.node = this.vis.selectAll("g.node")
        .data(defaultConfig.data.nodes)
        .enter().append("svg:g")
        .attr("class", "node")
        .call(_this.nodeDrag)
        .on('mouseover', function(d) {
            // 鼠标悬停在节点上时,悬浮框来
            if (_this.node.mouseoutTimeout) {
                clearTimeout(_this.node.mouseoutTimeout);
                _this.node.mouseoutTimeout = null;
            }
            _this.highlightToolTip(d);
        })
        .on('mouseout', function() {
            // 鼠标移出节点时,悬浮框走
            if (_this.node.mouseoutTimeout) {
                clearTimeout(_this.node.mouseoutTimeout);
                _this.node.mouseoutTimeout = null;
            }
            _this.node.mouseoutTimeout=setTimeout(function() {
                _this.highlightToolTip(null);
            }, 300);
        })
        .on('dblclick',function(d){
            // 双击节点时,高亮显示与当前节点相关的节点、连接线和连线上的文本
            _this.highlightObject(d);
            //(阻止事件冒泡)
            d3.event.stopPropagation();
        });
    
    // 为每个节点添加图片元素表示节点图标
    this.node.append("svg:image")
        .data(defaultConfig.data.nodes)
        .attr("class", "circle")
        .attr("xlink:href", function(d){ return("./assects/images/" + d.image)}) 
        //以下设置直接绝对图标和连接线箭头的“和平共处”程度
        .attr("x", "-25px") 
        .attr("y", "-25px") 
        .attr("width", "50px")
        .attr("height", "50px");
    
  • 双击空白处复原实现

    // 在整个页面上绑定双击事件处理函数
    d3.select(".network").on('dblclick',function(){
       // 当双击页面其他区域时,取消所有节点、连接线和连线上的文本的高亮显示,并重置依赖节点和连接线数组
       dependsNode = dependsLinkAndText = [];
       _this.highlightObject(null);
    }); 
    
  • 41
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

啦啦右一

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值