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

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

整体演示代码如下:

<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>test4</title>
    <style>
        path.link {
          fill: none;
          stroke: #666;
          stroke-width: 1.5px;
        }

        marker#licensing {
          fill: green;
        }

        path.link.licensing {
          stroke: green;
        }

        path.link.resolved {
          stroke-dasharray: 0,2 1;
        }

        circle {
          fill: #ccc;
          stroke: #333;
          stroke-width: 1.5px;
        }

        text {
          font: 10px sans-serif;
          pointer-events: none;
        }

        text.shadow {
          stroke: #fff;
          stroke-width: 3px;
          stroke-opacity: .8;
        }
    </style>
</head>
<body>
<div id="chart"></div>
<script src="http://code.jquery.com/jquery-latest.js"></script>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script type="text/javascript">
    var index = 0;
    var width = 1140,
    	height = 1000;
    var links = [
            // {source: "Microsoft", target: "Amazon", type: "suit"},
             // {source: "Microsoft", target: "Amazon", type: "suit"},
             // {source: "Microsoft", target: "Amazon", type: "suit"},
             // {source: "Samsung", target: "Apple", type: "suit"},
             // {source: "Samsung", target: "Samsung", type: "suit"},
             {source: "a", target: "b", type: "suit"},
             {source: "a", target: "b", type: "suit"},
             {source: "a", target: "b", type: "suit"},
             {source: "a", target: "b", type: "suit"},
             {source: "a", target: "a", type: "suit"},
             {source: "a", target: "a", type: "suit"},
             {source: "b", target: "a", type: "suit"},
             {source: "b", target: "a", type: "suit"},
             // {source: "Amazon", target: "Microsoft", type: "suit"}
             ];
    
    //关系分组
    var linkGroup = {};
    //对连接线进行统计和分组,不区分连接线的方向,只要属于同两个实体,即认为是同一组
    var linkmap = {}
    for(var i=0; i<links.length; i++){
        var key = links[i].source<links[i].target?links[i].source+':'+links[i].target:links[i].target+':'+links[i].source;
        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<links[i].target?links[i].source+':'+links[i].target:links[i].target+':'+links[i].source;
        links[i].size = linkmap[key];
        //同一组的关系进行编号
        var group = linkGroup[key];
        var keyPair = key.split(':');
        var type = 'noself';//标示该组关系是指向两个不同实体还是同一个实体
        if(keyPair[0]==keyPair[1]){
            type = 'self';
        }
        //给节点分配编号
        setLinkNumber(group,type);
    }
    console.log(links);

    var nodes = {};

    // Compute the distinct nodes from the links.
    links.forEach(function(link) {
      link.source = nodes[link.source] || (nodes[link.source] = {name: link.source});
      link.target = nodes[link.target] || (nodes[link.target] = {name: link.target});
    });

    var force = d3.layout.force()
        .nodes(d3.values(nodes))
        .links(links)
        .size([width, height])
        .linkDistance(600)
        .charge(-300)
        .on("tick", tick)
        .start();

    var svg = d3.select("body").append("svg:svg")
        .attr("width", width)
        .attr("height", height);

    // Per-type markers, as they don't inherit styles.
    svg.append("svg:defs").selectAll("marker")
        .data(["suit", "licensing", "resolved"])
      .enter().append("svg:marker")
        .attr("id", String)
        .attr("viewBox", "0 -5 10 10")
        .attr("refX", 30)
        .attr("refY", 0)
        .attr("markerWidth", 6)
        .attr("markerHeight", 6)
        .attr("orient", "auto")
        .append("svg:path")
        .attr("d", "M0,-5L10,0L0,5");

    var path = svg.append("svg:g").selectAll("path")
        .data(force.links())
      .enter().append("svg:path")
        .attr("class", function(d) { return "link " + d.type; })
        .attr("marker-end", function(d) { return "url(#" + d.type + ")"; });

    var circle = svg.append("svg:g").selectAll("circle")
        .data(force.nodes())
      .enter().append("svg:circle")
        .attr("r", 20)
        .call(force.drag);

    var text = svg.append("svg:g").selectAll("g")
        .data(force.nodes())
      .enter().append("svg:g");

    // A copy of the text with a thick white stroke for legibility.
    text.append("svg:text")
        .attr("x", 8)
        .attr("y", ".31em")
        .attr("class", "shadow")
        .text(function(d) { return d.name; });

    text.append("svg:text")
        .attr("x", 8)
        .attr("y", ".31em")
        .text(function(d) { return d.name; });

    // Use elliptical arc path segments to doubly-encode directionality.
    function tick() {
      path.attr("d", function(d) {
        //如果连接线连接的是同一个实体,则对path属性进行调整,绘制的圆弧属于长圆弧,同时对终点坐标进行微调,避免因坐标一致导致弧无法绘制
        if(d.target==d.source){
            dr = 30/d.linknum;
            return"M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 1,1 " + d.target.x + "," + (d.target.y+1);
        }else if(d.size%2!=0 && d.linknum==1){//如果两个节点之间的连接线数量为奇数条,则设置编号为1的连接线为直线,其他连接线会均分在两边
            return 'M '+d.source.x+' '+d.source.y+' L '+ d.target.x +' '+d.target.y;
        }
        //根据连接线编号值来动态确定该条椭圆弧线的长半轴和短半轴,当两者一致时绘制的是圆弧
        //注意A属性后面的参数,前两个为长半轴和短半轴,第三个默认为0,第四个表示弧度大于180度则为1,小于则为0,这在绘制连接到相同节点的连接线时用到;第五个参数,0表示正角,1表示负角,即用来控制弧形凹凸的方向。本文正是结合编号的正负情况来控制该条连接线的凹凸方向,从而达到连接线对称的效果
        var curve=1.5;
        var homogeneous=1.2;
        var dx = d.target.x - d.source.x,
            dy = d.target.y - d.source.y,
            dr = Math.sqrt(dx*dx+dy*dy)*(d.linknum+homogeneous)/(curve*homogeneous);
        //当节点编号为负数时,对弧形进行反向凹凸,达到对称效果
        if(d.linknum<0){
            dr = Math.sqrt(dx*dx+dy*dy)*(-1*d.linknum+homogeneous)/(curve*homogeneous);
            return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,0 " + d.target.x + "," + d.target.y;
        }
        return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
      });

      circle.attr("transform", function(d) {
        return "translate(" + d.x + "," + d.y + ")";
      });

      text.attr("transform", function(d) {
        return "translate(" + d.x + "," + d.y + ")";
      });
    }

    function 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 < link.target){
                linksA.push(link);
            }else{
                linksB.push(link);
            }
        }
        //确定关系最大编号。为了使得连接两个实体的关系曲线呈现对称,根据关系数量奇偶性进行平分。
        //特殊情况:当关系都是连接到同一个实体时,不平分
        var maxLinkNumber = 0;
        if(type=='self'){
            maxLinkNumber = group.length;
        }else{
            maxLinkNumber = group.length%2==0?group.length/2:(group.length+1)/2;
        }
        //如果两个方向的关系数量一样多,直接分别设置编号即可
        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++;
            }
        } 
    }
</script>
</body>
</html>
最终效果如下(a、b节点之间6条连接线,四条a->b,两条b->a):


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


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

参考资源:

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

  • 5
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
以下是一个简单的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元素。 最后,我们更新了导向对象,并重新启动了导向的模拟,以确保新节点和关系连线被正确地添加导向中。 请注意,这只是一个简单的示例,实际应用中需要根据具体需求进行更复杂的处理。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值