d3.js力导向图节点间多连接线对称绘制

本文基于d3.js中的力导向图对关系网络进行可视化。针对实体之间多关系亦即节点之间多条连接线的问题,采用弧形连接线,同时对节点间的多条连接线进行动态编号,并根据编号绘制不同半径的弧线,从而解决多条弧形连接线相互遮挡的问题。同时基于svg中的path标签属性,对弧形方向进行调整,保证多条连接线在节点之间的分布具有对称效果。

整体演示代码如下:

[html]  view plain  copy
  1. <!DOCTYPE html>  
  2. <html>  
  3. <head>  
  4.     <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">  
  5.     <title>test4</title>  
  6.     <style>  
  7.         path.link {  
  8.           fill: none;  
  9.           stroke: #666;  
  10.           stroke-width: 1.5px;  
  11.         }  
  12.   
  13.         marker#licensing {  
  14.           fill: green;  
  15.         }  
  16.   
  17.         path.link.licensing {  
  18.           stroke: green;  
  19.         }  
  20.   
  21.         path.link.resolved {  
  22.           stroke-dasharray: 0,2 1;  
  23.         }  
  24.   
  25.         circle {  
  26.           fill: #ccc;  
  27.           stroke: #333;  
  28.           stroke-width: 1.5px;  
  29.         }  
  30.   
  31.         text {  
  32.           font: 10px sans-serif;  
  33.           pointer-events: none;  
  34.         }  
  35.   
  36.         text.shadow {  
  37.           stroke: #fff;  
  38.           stroke-width: 3px;  
  39.           stroke-opacity: .8;  
  40.         }  
  41.     </style>  
  42. </head>  
  43. <body>  
  44. <div id="chart"></div>  
  45. <script src="http://code.jquery.com/jquery-latest.js"></script>  
  46. <script src="http://d3js.org/d3.v3.min.js"></script>  
  47. <script type="text/javascript">  
  48.     var index = 0;  
  49.     var width = 1140,  
  50.         height = 1000;  
  51.     var links = [  
  52.             // {source: "Microsoft", target: "Amazon", type: "suit"},  
  53.              // {source: "Microsoft", target: "Amazon", type: "suit"},  
  54.              // {source: "Microsoft", target: "Amazon", type: "suit"},  
  55.              // {source: "Samsung", target: "Apple", type: "suit"},  
  56.              // {source: "Samsung", target: "Samsung", type: "suit"},  
  57.              {source: "a", target: "b", type: "suit"},  
  58.              {source: "a", target: "b", type: "suit"},  
  59.              {source: "a", target: "b", type: "suit"},  
  60.              {source: "a", target: "b", type: "suit"},  
  61.              {source: "a", target: "a", type: "suit"},  
  62.              {source: "a", target: "a", type: "suit"},  
  63.              {source: "b", target: "a", type: "suit"},  
  64.              {source: "b", target: "a", type: "suit"},  
  65.              // {source: "Amazon", target: "Microsoft", type: "suit"}  
  66.              ];  
  67.       
  68.     //关系分组  
  69.     var linkGroup = {};  
  70.     //对连接线进行统计和分组,不区分连接线的方向,只要属于同两个实体,即认为是同一组  
  71.     var linkmap = {}  
  72.     for(var i=0; i<links.length; i++){  
  73.         var key = links[i].source<links[i].target?links[i].source+':'+links[i].target:links[i].target+':'+links[i].source;  
  74.         if(!linkmap.hasOwnProperty(key)){  
  75.             linkmap[key] = 0;  
  76.         }  
  77.         linkmap[key]+=1;  
  78.         if(!linkGroup.hasOwnProperty(key)){  
  79.             linkGroup[key]=[];  
  80.         }  
  81.         linkGroup[key].push(links[i]);  
  82.     }  
  83.     //为每一条连接线分配size属性,同时对每一组连接线进行编号  
  84.     for(var i=0; i<links.length; i++){  
  85.         var key = links[i].source<links[i].target?links[i].source+':'+links[i].target:links[i].target+':'+links[i].source;  
  86.         links[i].size = linkmap[key];  
  87.         //同一组的关系进行编号  
  88.         var group = linkGroup[key];  
  89.         var keyPair = key.split(':');  
  90.         var type = 'noself';//标示该组关系是指向两个不同实体还是同一个实体  
  91.         if(keyPair[0]==keyPair[1]){  
  92.             type = 'self';  
  93.         }  
  94.         //给节点分配编号  
  95.         setLinkNumber(group,type);  
  96.     }  
  97.     console.log(links);  
  98.   
  99.     var nodes = {};  
  100.   
  101.     // Compute the distinct nodes from the links.  
  102.     links.forEach(function(link) {  
  103.       link.source = nodes[link.source] || (nodes[link.source] = {name: link.source});  
  104.       link.target = nodes[link.target] || (nodes[link.target] = {name: link.target});  
  105.     });  
  106.   
  107.     var force = d3.layout.force()  
  108.         .nodes(d3.values(nodes))  
  109.         .links(links)  
  110.         .size([width, height])  
  111.         .linkDistance(600)  
  112.         .charge(-300)  
  113.         .on("tick", tick)  
  114.         .start();  
  115.   
  116.     var svg = d3.select("body").append("svg:svg")  
  117.         .attr("width", width)  
  118.         .attr("height", height);  
  119.   
  120.     // Per-type markers, as they don't inherit styles.  
  121.     svg.append("svg:defs").selectAll("marker")  
  122.         .data(["suit", "licensing", "resolved"])  
  123.       .enter().append("svg:marker")  
  124.         .attr("id", String)  
  125.         .attr("viewBox", "0 -5 10 10")  
  126.         .attr("refX", 30)  
  127.         .attr("refY", 0)  
  128.         .attr("markerWidth", 6)  
  129.         .attr("markerHeight", 6)  
  130.         .attr("orient", "auto")  
  131.         .append("svg:path")  
  132.         .attr("d", "M0,-5L10,0L0,5");  
  133.   
  134.     var path = svg.append("svg:g").selectAll("path")  
  135.         .data(force.links())  
  136.       .enter().append("svg:path")  
  137.         .attr("class", function(d) { return "link " + d.type; })  
  138.         .attr("marker-end", function(d) { return "url(#" + d.type + ")"; });  
  139.   
  140.     var circle = svg.append("svg:g").selectAll("circle")  
  141.         .data(force.nodes())  
  142.       .enter().append("svg:circle")  
  143.         .attr("r", 20)  
  144.         .call(force.drag);  
  145.   
  146.     var text = svg.append("svg:g").selectAll("g")  
  147.         .data(force.nodes())  
  148.       .enter().append("svg:g");  
  149.   
  150.     // A copy of the text with a thick white stroke for legibility.  
  151.     text.append("svg:text")  
  152.         .attr("x", 8)  
  153.         .attr("y", ".31em")  
  154.         .attr("class", "shadow")  
  155.         .text(function(d) { return d.name; });  
  156.   
  157.     text.append("svg:text")  
  158.         .attr("x", 8)  
  159.         .attr("y", ".31em")  
  160.         .text(function(d) { return d.name; });  
  161.   
  162.     // Use elliptical arc path segments to doubly-encode directionality.  
  163.     function tick() {  
  164.       path.attr("d", function(d) {  
  165.         //如果连接线连接的是同一个实体,则对path属性进行调整,绘制的圆弧属于长圆弧,同时对终点坐标进行微调,避免因坐标一致导致弧无法绘制  
  166.         if(d.target==d.source){  
  167.             dr = 30/d.linknum;  
  168.             return"M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 1,1 " + d.target.x + "," + (d.target.y+1);  
  169.         }else if(d.size%2!=0 && d.linknum==1){//如果两个节点之间的连接线数量为奇数条,则设置编号为1的连接线为直线,其他连接线会均分在两边  
  170.             return 'M '+d.source.x+' '+d.source.y+' L '+ d.target.x +' '+d.target.y;  
  171.         }  
  172.         //根据连接线编号值来动态确定该条椭圆弧线的长半轴和短半轴,当两者一致时绘制的是圆弧  
  173.         //注意A属性后面的参数,前两个为长半轴和短半轴,第三个默认为0,第四个表示弧度大于180度则为1,小于则为0,这在绘制连接到相同节点的连接线时用到;第五个参数,0表示正角,1表示负角,即用来控制弧形凹凸的方向。本文正是结合编号的正负情况来控制该条连接线的凹凸方向,从而达到连接线对称的效果  
  174.         var curve=1.5;  
  175.         var homogeneous=1.2;  
  176.         var dx = d.target.x - d.source.x,  
  177.             dy = d.target.y - d.source.y,  
  178.             dr = Math.sqrt(dx*dx+dy*dy)*(d.linknum+homogeneous)/(curve*homogeneous);  
  179.         //当节点编号为负数时,对弧形进行反向凹凸,达到对称效果  
  180.         if(d.linknum<0){  
  181.             dr = Math.sqrt(dx*dx+dy*dy)*(-1*d.linknum+homogeneous)/(curve*homogeneous);  
  182.             return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,0 " + d.target.x + "," + d.target.y;  
  183.         }  
  184.         return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;  
  185.       });  
  186.   
  187.       circle.attr("transform", function(d) {  
  188.         return "translate(" + d.x + "," + d.y + ")";  
  189.       });  
  190.   
  191.       text.attr("transform", function(d) {  
  192.         return "translate(" + d.x + "," + d.y + ")";  
  193.       });  
  194.     }  
  195.   
  196.     function setLinkNumber(group,type){  
  197.         if(group.length==0) return;  
  198.         //对该分组内的关系按照方向进行分类,此处根据连接的实体ASCII值大小分成两部分  
  199.         var linksA = [], linksB = [];  
  200.         for(var i = 0;i<group.length;i++){  
  201.             var link = group[i];  
  202.             if(link.source < link.target){  
  203.                 linksA.push(link);  
  204.             }else{  
  205.                 linksB.push(link);  
  206.             }  
  207.         }  
  208.         //确定关系最大编号。为了使得连接两个实体的关系曲线呈现对称,根据关系数量奇偶性进行平分。  
  209.         //特殊情况:当关系都是连接到同一个实体时,不平分  
  210.         var maxLinkNumber = 0;  
  211.         if(type=='self'){  
  212.             maxLinkNumber = group.length;  
  213.         }else{  
  214.             maxLinkNumber = group.length%2==0?group.length/2:(group.length+1)/2;  
  215.         }  
  216.         //如果两个方向的关系数量一样多,直接分别设置编号即可  
  217.         if(linksA.length==linksB.length){  
  218.             var startLinkNumber = 1;  
  219.             for(var i=0;i<linksA.length;i++){  
  220.                 linksA[i].linknum = startLinkNumber++;  
  221.             }  
  222.             startLinkNumber = 1;  
  223.             for(var i=0;i<linksB.length;i++){  
  224.                 linksB[i].linknum = startLinkNumber++;  
  225.             }  
  226.         }else{//当两个方向的关系数量不对等时,先对数量少的那组关系从最大编号值进行逆序编号,然后在对另一组数量多的关系从编号1一直编号到最大编号,再对剩余关系进行负编号  
  227.             //如果抛开负号,可以发现,最终所有关系的编号序列一定是对称的(对称是为了保证后续绘图时曲线的弯曲程度也是对称的)  
  228.             var biggerLinks,smallerLinks;  
  229.             if(linksA.length>linksB.length){  
  230.                 biggerLinks = linksA;  
  231.                 smallerLinks = linksB;  
  232.             }else{  
  233.                 biggerLinks = linksB;  
  234.                 smallerLinks = linksA;  
  235.             }  
  236.   
  237.             var startLinkNumber = maxLinkNumber;  
  238.             for(var i=0;i<smallerLinks.length;i++){  
  239.                 smallerLinks[i].linknum = startLinkNumber--;  
  240.             }  
  241.             var tmpNumber = startLinkNumber;  
  242.   
  243.             startLinkNumber = 1;  
  244.             var p = 0;  
  245.             while(startLinkNumber<=maxLinkNumber){  
  246.                 biggerLinks[p++].linknum = startLinkNumber++;  
  247.             }  
  248.             //开始负编号  
  249.             startLinkNumber = 0-tmpNumber;  
  250.             for(var i=p;i<biggerLinks.length;i++){  
  251.                 biggerLinks[i].linknum = startLinkNumber++;  
  252.             }  
  253.         }   
  254.     }  
  255. </script>  
  256. </body>  
  257. </html>  
最终效果如下(a、b节点之间6条连接线,四条a->b,两条b->a):


奇数条连接线效果(带有直线):


说明:本文专注于实现多连接线和对称效果,其他可视化效果可自行修改设置。

参考资源:

1、https://stackoverflow.com/questions/37417459/drawing-multiple-links-between-fixed-nodes

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
以下是一个简单的D3.js导向示例,演示如何添加新节点和关系连线: ```html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>D3.js导向 - 新增节点和关系连线示例</title> <script src="https://d3js.org/d3.v6.min.js"></script> </head> <body> <svg width="800" height="600"></svg> <script> // 定义节点和关系数据 var nodes = [ { id: "node1", name: "节点1" }, { id: "node2", name: "节点2" }, { id: "node3", name: "节点3" }, { id: "node4", name: "节点4" } ]; var links = [ { source: "node1", target: "node2" }, { source: "node1", target: "node3" }, { source: "node2", target: "node4" } ]; // 创建导向对象 var simulation = d3.forceSimulation(nodes) .force("link", d3.forceLink(links).id(d => d.id)) .force("charge", d3.forceManyBody()) .force("center", d3.forceCenter(400, 300)); // 创建节点和关系连线 var svg = d3.select("svg"); var link = svg.selectAll("line") .data(links) .enter().append("line") .attr("stroke", "#999") .attr("stroke-opacity", 0.6) .attr("stroke-width", d => Math.sqrt(d.value)); var node = svg.selectAll("circle") .data(nodes) .enter().append("circle") .attr("r", 10) .attr("fill", "#ccc") .call(drag(simulation)); var label = svg.selectAll("text") .data(nodes) .enter().append("text") .text(d => d.name) .attr("font-size", "12px") .attr("dx", 15) .attr("dy", 4); // 定义拖拽行为 function drag(simulation) { function dragstarted(event, d) { if (!event.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; } function dragged(event, d) { d.fx = event.x; d.fy = event.y; } function dragended(event, d) { if (!event.active) simulation.alphaTarget(0); d.fx = null; d.fy = null; } return d3.drag() .on("start", dragstarted) .on("drag", dragged) .on("end", dragended); } // 新增节点和关系连线 var newNode = { id: "node5", name: "节点5" }; var newLink = { source: "node4", target: "node5" }; nodes.push(newNode); links.push(newLink); node = node.data(nodes, d => d.id); node.exit().remove(); node = node.enter().append("circle") .attr("r", 10) .attr("fill", "#ccc") .call(drag(simulation)) .merge(node); label = label.data(nodes, d => d.id); label.exit().remove(); label = label.enter().append("text") .text(d => d.name) .attr("font-size", "12px") .attr("dx", 15) .attr("dy", 4) .merge(label); link = link.data(links); link.exit().remove(); link = link.enter().append("line") .attr("stroke", "#999") .attr("stroke-opacity", 0.6) .attr("stroke-width", d => Math.sqrt(d.value)) .merge(link); // 更新导向 simulation.nodes(nodes); simulation.force("link").links(links); simulation.alpha(1).restart(); </script> </body> </html> ``` 在上面的示例中,我们首先定义了一个简单的节点和关系数据,并创建了一个导向对象。然后,我们使用D3.js创建了节点和关系连线的SVG元素,并绑定了数据。 接下来,我们定义了一个拖拽行为,以便用户可以拖动节点。然后,我们添加了一个新节点和一个新的关系连线,并使用D3.js更新了节点和关系连线的SVG元素。 最后,我们更新了导向对象,并重新启动了导向的模拟,以确保新节点和关系连线被正确地添加到导向中。 请注意,这只是一个简单的示例,实际应用中需要根据具体需求进行更复杂的处理。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值