D3 Force Layout:如何动态定位泪珠形节点?

D3 Force Layout: How to dynamically orient teardrop-shaped nodes?

我想要具有动态方向的泪珠形节点,这样节点的方向性就可以单独显示其源节点和目标节点。 到目前为止,这是我的代码(泪珠的代码取自此示例:http://bl.ocks.org/mbostock/849853)及其生成的图形:

http://jsbin.com/kigiwinida/edit?html,output

但我最终想要的是这张图:

我认为使泪珠的方向动态化的关键是为 rotate() 函数提供正确的参数;使泪珠相应地旋转,以便与它们作为目标的边缘对齐。

像这样...

var w = window.innerWidth*2,
    h = window.innerHeight*3,
    r = 10,
    charge = 300,
    def_color = "#bdbdbd"; //default node color
    



    var dataset =   {"nodes": 
    [
    {"x": 469, "y": 410},
    {"x": 493, "y": 364},
    {"x": 442, "y": 365},
    {"x": 467, "y": 314},
    {"x": 477, "y": 248},
    {"x": 425, "y": 207},
    {"x": 402, "y": 155},
    {"x": 369, "y": 196},
    {"x": 350, "y": 148},
    {"x": 539, "y": 222},
    {"x": 594, "y": 235},
    {"x": 582, "y": 185},
    {"x": 633, "y": 200}
     ],
  "edges": [
    {"source":  0, "target":  1},
    {"source":  1, "target":  2},
    {"source":  2, "target":  0},
    {"source":  1, "target":  3},
    {"source":  3, "target":  2},
    {"source":  3, "target":  4},
    {"source":  4, "target":  5},
    {"source":  5, "target":  6},
    {"source":  5, "target":  7},
    {"source":  6, "target":  7},
    {"source":  6, "target":  8},
    {"source":  7, "target":  8},
    {"source":  9, "target":  4},
    {"source":  9, "target": 11},
    {"source":  9, "target": 10},
    {"source": 10, "target": 11},
    {"source": 11, "target": 12},
    {"source": 12, "target": 10}
  ]
};


    //Initialize a default force layout, using the nodes and edges in dataset
    var force = d3.layout.force()
         .nodes(dataset.nodes)
         .links(dataset.edges)
         .size([w, h])
         .linkDistance([70]);

    //Create SVG container element
    var svg = d3.select("body")
        .append("svg")
        .attr("width", w)
        .attr("height", h)
        .attr("class", "svg-container")

    //Build the arrow
    svg.append("defs").selectAll("marker")
      .data(["suit", "licensing", "resolved"])
      .enter().append("marker")
      .attr("id", function (d) { return d; })
      .attr("viewBox", "0 -5 10 10")
      .attr("refX", 50)
      //.attr("refY", 0)
      .attr("markerWidth", 6)
      .attr("markerHeight", 6)
      .attr("orient", "auto")
      .append("path")
      .attr("d", "M0,-5L10,0L0,5 L10,0 L0, -5")
      .style("stroke", "#ccc");


    //Create edges as lines
    var edges = svg.selectAll("line")
      .data(dataset.edges)
      .enter()
      .append("line")
      .attr("marker-end", "url(#suit)") //Add the arrow
      .style("stroke", "#ccc")
      .style("stroke-width", 1);



            // Create the groups under svg
    var gnodes = svg.selectAll('g.gnode')
      .data(dataset.nodes)
      .enter()
      .append('g')
      .classed('gnode', true);

    // Add one circle in each group
    var node = gnodes.append("path")
        .attr("class", "node")
        .attr("d", raindrop(r))
        .style("fill", def_color)
        .style("stroke", "black")
        .call(force.drag);

    function raindrop(size) {
      var r = size;
      return "M" + r + ",0"
        + "A" + r + "," + r + " 0 1,1 " + -r + ",0"
        + "C" + -r + "," + -r + " 0," + -r + " 0," + -3 * r
        + "C0," + -r + " " + r + "," + -r + " " + r + ",0"
        + "Z";
    }

    //add an orientation member to each node
    dataset.nodes.forEach(function (n, i, a) {
      n.orientation = {
        sinkCount: 0,
        totalX: 0,
        totalY: 0,
        reset: function () {
          this.sinkCount = this.totalX = this.totalY = 0;
        }
      };
      Object.defineProperty(n, "angle", {
        get: function () {
          var o = this.orientation;
          //angle of average vector
          return o.sinkCount ? Math.atan2(o.totalY / o.sinkCount, o.totalX / o.sinkCount) * 180 / Math.PI : 0;
        }
      });
    });
    function limitAngle(x) {
      var pi = Math.PI;
      return (x  > pi) ? 2 * pi - x : (x < -pi) ? 2 * pi + x : x;
    }
    //add link behaviour to tweek source node angle
    dataset.edges.forEach(function (l, i, a) {
      Object.defineProperties(l, {
        "angle": {
          get: function () {
            var s = this.source, t = this.target;
            return Math.atan2(-(t.y - s.y), t.x - s.x);
          }
        },
        "visitTarget": {
          value: function () {
            var n1 = this.target, n2 = this.source, t = n1.orientation;
            t.totalX += (n2.x - n1.x);
            t.totalY += -(n2.y - n1.y);
            t.sinkCount += 1;
          }
        }
      })
    });

    force.charge([-300])
        .start();

    //Every time the simulation "ticks", this will be called
    force.on("tick", function () {

      edges.attr("x1", function (d) { return d.x1 = d.source.x; })
         .attr("y1", function (d) { return d.y1 = d.source.y; })
         .attr("x2", function (d) { return d.x2 = d.target.x; })
         .attr("y2", function (d) { return d.y2 = d.target.y; });

      dataset.nodes.forEach(function (n, i, a) {
        n.orientation.reset();
      });
      dataset.edges.forEach(function (e, i, a) {
        e.visitTarget();
      });

      node.attr("transform", function (d) {
        return 'translate(' + [d.x, d.y] + ') rotate(' + -(d.angle - 90) + ")"
      });
    });
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>