【问题标题】:Drawing multiple, bezier-connected edges in a force-directed graph in D3在 D3 中的力有向图中绘制多个贝塞尔连接边
【发布时间】:2017-02-12 00:35:25
【问题描述】:

我正在尝试绘制一个力导向图,每个节点有多个边,其中边看起来彼此不同,无论目标节点和源节点之间的距离如何。

tl;博士

当前图表:

  • 我有一个力有向图
  • 每个节点可以有多条边指向另一个节点
  • 为了说明从一个节点发出的多条边,我使用弧而不是线来分隔每条(通过给出递增的linknum 并使用它来计算弧半径)

问题:

如果 2 个连接节点之间的距离变得太大,则弧会相互合并 - 因为弧不能“扩展”超过与 2 重叠的假想圆的半径。

因此,看起来应该彼此不同的边缘合并成一个大弧。

可能的解决方案:

我可以将 Arc 半径固定为对应于有效/合适的链接距离,因此这永远不会发生,但不幸的是,节点可以由用户拖动/锚定(为简洁起见,我在下面的代码示例中省略了用户拖动/锚定代码)

我认为使用贝塞尔曲线而不是没有扩展限制的弧线会更有意义。但是,我不确定在这种情况下我将如何着手计算他们的控制点

代码

拖动滑块会随着链接/边缘距离的增加重新绘制图表。高于 75 的值会创建我正在谈论的弧/链接/边缘合并。

function draw(linkDistance) {
  
  var links = [{
    source: "Microsoft",
    target: "Amazon",
    type: "licensing"
  }, {
    source: "Microsoft",
    target: "Amazon",
    type: "suit"
  }, {
    source: "Microsoft",
    target: "Amazon",
    type: "resolved"
  }];

  //sort links by source, then target
  links.sort(function(a, b) {
    if (a.source > b.source) {
      return 1;
    }
    else if (a.source < b.source) {
      return -1;
    }
    else {
      if (a.target > b.target) {
        return 1;
      }
      if (a.target < b.target) {
        return -1;
      }
      else {
        return 0;
      }
    }
  });

  //any links with duplicate source and target get an incremented 'linknum'
  for (var i = 0; i < links.length; i++) {
    if (i != 0 &&
      links[i].source == links[i - 1].source &&
      links[i].target == links[i - 1].target) {
      links[i].linknum = links[i - 1].linknum + 1;
    }
    else {
      links[i].linknum = 1;
    };
  };

  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 w = 300,
    h = 200;

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

  var svg = d3.select("#chart").append("svg:svg")
    .attr("width", w)
    .attr("height", h);

  // 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", 15)
    .attr("refY", -1.5)
    .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", 6)
    .call(force.drag);

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

  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) {
      var dx = d.target.x - d.source.x,
        dy = d.target.y - d.source.y,
        dr = 75 / d.linknum; //linknum is defined above
      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 + ")";
    });
  }
}



window.onload = function() {
  document.getElementById("range").addEventListener("input", function(e) {
    var value = this.value;
    
    document.getElementById("chart").innerHTML = "";
    document.getElementById("label").innerHTML = value;
    draw(value);
  })

  draw(70);
}
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;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.9/d3.min.js"></script>
<input type="range" id="range" step="2" value="70" min="50" max="150">
<span id="label"></span>
<div id="chart"></div>

【问题讨论】:

    标签: javascript d3.js


    【解决方案1】:

    你的意思是这样的吗? -->

    path.attr("d", function(d) {
          var dx = d.target.x - d.source.x,
            dy = d.target.y - d.source.y;
            var qx = dy /  1 * d.linknum, //linknum is defined above
            qy = -dx / 1 * d.linknum;
            var qx1 = (d.source.x + (dx / 2)) + qx,
            qy1 = (d.source.y + (dy / 2)) + qy;
          return "M"+d.source.x+" "+d.source.y+" C" + d.source.x + " " + d.source.y + " " + qx1 + " " + qy1 + " " + d.target.x + " " + d.target.y;
        });
    

    这会将它们从弧(路径语法中的 A)变成贝塞尔曲线(路径语法中的 C)。控制点正好从两个节点之间的线的中心以直角伸出,“伸出”距离缩放到 linknum 变量。

    http://jsfiddle.net/a5ua66zy/2/

    附言。可以增加 qx/qy 变量中的“1”以将曲线收紧

    ps2。如果您不希望弧线在拖动时摇摆不定(即取决于节点之间的距离),您可以这样做:

    var ds = Math.sqrt ((dx * dx) + (dy * dy));
    var qx = (dy / ds) * 20 * d.linknum, //linknum is defined above
        qy = -(dx / ds) * 20 * d.linknum;
    
    // 20 is the separation between adjacent curves
    

    【讨论】:

    • 一模一样
    • 表达得这么清楚,连我这样的无数人都能跟得上!非常感谢@mgraham。我很快发现,更高的乘法因子可以使曲线不那么球状:var qx = dy / 6 * d.linknum, qy = -dx / 6 * d.linknum;
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-08-19
    • 2013-11-29
    • 1970-01-01
    • 2013-08-21
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多