在D3中的力导向图中绘制多个贝塞尔连接的边

时间:2016-10-03 23:56:51

标签: javascript d3.js

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

TL;博士

当前图表:

  • 我有一张力导向图
  • 每个节点可以有多个边指向另一个节点
  • 为了说明从节点发出的多个边缘,我使用Arcs而不是Lines来分隔每个边缘(通过给出递增的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>

1 个答案:

答案 0 :(得分:1)

你是说这样的意思吗? - &GT;

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)转换为beziers(路径语法中的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