最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - linking nodes of variable radius with arrows - Stack Overflow

programmeradmin3浏览0评论

I have some circles/nodes of different radius and I have to connect them with paths having arrow ends.

Here's the code for the marker:

svg.append("svg:defs").selectAll("marker")
    .data(["default"])
  .enter().append("svg:marker")
    .attr("id", String)
    .attr("viewBox", "0 -5 10 10")
    .attr("refX", 5)
    .attr("refY", -1.5)
    .attr("markerWidth", 10)
    .attr("markerHeight", 10)
    .attr("orient", "auto")
    .append("svg:path")
    .attr("d", "M1,-5L10,0L0,5");  

I have stored the radius of circles in an array. Here's the screen shot:

The arrow is actually "inside" the circles. How do I get the arrows to be at the surface of the circles?

I have some circles/nodes of different radius and I have to connect them with paths having arrow ends.

Here's the code for the marker:

svg.append("svg:defs").selectAll("marker")
    .data(["default"])
  .enter().append("svg:marker")
    .attr("id", String)
    .attr("viewBox", "0 -5 10 10")
    .attr("refX", 5)
    .attr("refY", -1.5)
    .attr("markerWidth", 10)
    .attr("markerHeight", 10)
    .attr("orient", "auto")
    .append("svg:path")
    .attr("d", "M1,-5L10,0L0,5");  

I have stored the radius of circles in an array. Here's the screen shot:

The arrow is actually "inside" the circles. How do I get the arrows to be at the surface of the circles?

Share Improve this question edited Mar 19, 2013 at 9:39 sudeepdino008 asked Mar 19, 2013 at 9:32 sudeepdino008sudeepdino008 3,3645 gold badges43 silver badges73 bronze badges 3
  • Actually, I cannot see, the arrow is actually "inside" the circles. – Oswald Commented Mar 19, 2013 at 9:37
  • Make the paths start and end at the circles radius rather than centre. Some trigonometry will be required. – Robert Longson Commented Mar 19, 2013 at 9:39
  • I may be wrong, but the paths for the links are separate from the markers that make up the arrows. The .attr("refX", 5) sets the offset of the arrow from the centre of the circle. While it is designated as the X offset, because the object is rotating, it doesn't correspond to the x (left and right) axis of the screen. The same is true of the .attr("refY", -1.5) line. When I tried to apply a function to work it out I failed miserably. I could offset the arrows, but not consistently to the correct distance. – d3noob Commented Mar 19, 2013 at 9:51
Add a ment  | 

4 Answers 4

Reset to default 7

This is an old question, but here is my solution if you want your arrowheads to be at the edge of your nodes instead of on top of or beneath them. My approach was also to draw the path connecting the nodes such that the end points were on the nodes' edges rather than at the nodes' centers. Starting from the Mobile Patent Suits example (http://bl.ocks/mbostock/1153292), I replaced the linkArc method with:

function linkArc(d) {
    var sourceX = d.source.x;
    var sourceY = d.source.y;
    var targetX = d.target.x;
    var targetY = d.target.y;

    var theta = Math.atan((targetX - sourceX) / (targetY - sourceY));
    var phi = Math.atan((targetY - sourceY) / (targetX - sourceX));

    var sinTheta = d.source.r * Math.sin(theta);
    var cosTheta = d.source.r * Math.cos(theta);
    var sinPhi = d.target.r * Math.sin(phi);
    var cosPhi = d.target.r * Math.cos(phi);

    // Set the position of the link's end point at the source node
    // such that it is on the edge closest to the target node
    if (d.target.y > d.source.y) {
        sourceX = sourceX + sinTheta;
        sourceY = sourceY + cosTheta;
    }
    else {
        sourceX = sourceX - sinTheta;
        sourceY = sourceY - cosTheta;
    }

    // Set the position of the link's end point at the target node
    // such that it is on the edge closest to the source node
    if (d.source.x > d.target.x) {
        targetX = targetX + cosPhi;
        targetY = targetY + sinPhi;    
    }
    else {
        targetX = targetX - cosPhi;
        targetY = targetY - sinPhi;   
    }

    // Draw an arc between the two calculated points
    var dx = targetX - sourceX,
        dy = targetY - sourceY,
        dr = Math.sqrt(dx * dx + dy * dy);
    return "M" + sourceX + "," + sourceY + "A" + dr + "," + dr + " 0 0,1 " + targetX + "," + targetY;
}

Note that this code expects an "r," or radius, attribute to be in the node data. To place the points of the arrows at the correct positions, I changed the refX and refY attributes so that the point of the arrow was at the edge of the node:

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", 10)
    .attr("refY", 0)
    .attr("markerWidth", 6)
    .attr("markerHeight", 6)
    .attr("orient", "auto")
  .append("path")
    .attr("d", "M0,-5L10,0L0,5");

This is really funny; I just solved this problem yesterday.

What I did is to end the path at the edge of the node, not at the centre. My case is more plicated because I use Bezier curves, not straight lines, but this might help you:

svg.append("svg:defs").selectAll("marker")
    .data(["default"])
  .enter().append("svg:marker")
    .attr("id", String)
    .attr("viewBox", "0 -3 6 6")
    .attr("refX", 5.0)
    .attr("refY", 0.0)
    .attr("markerWidth", 6)
    .attr("markerHeight", 6)
    .attr("orient", "auto")
  .append("svg:path")
    .attr("d", "M0,-2.0L5,0L0,2.0"); 


    links
      .attr("fill", "none")
      .attr("d", function(d) {
        var tightness = -3.0;
        if(d.type == "straight")
            tightness = 1000;

        // Places the control point for the Bezier on the bisection of the
        // segment between the source and target points, at a distance
        // equal to half the distance between the points.
        var dx = d.target.x - d.source.x;
        var dy = d.target.y - d.source.y;
        var dr = Math.sqrt(dx * dx + dy * dy);
        var qx = d.source.x + dx/2.0 - dy/tightness;
        var qy = d.source.y + dy/2.0 + dx/tightness;

        // Calculates the segment from the control point Q to the target
        // to use it as a direction to wich it will move "node_size" back
        // from the end point, to finish the edge aprox at the edge of the
        // node. Note there will be an angular error due to the segment not
        // having the same direction as the curve at that point.
        var dqx = d.target.x - qx;
        var dqy = d.target.y - qy;
        var qr = Math.sqrt(dqx * dqx + dqy * dqy);

        var offset = 1.1 * node_size(d.target);
        var tx = d.target.x - dqx/qr* offset;
        var ty = d.target.y - dqy/qr* offset;

        return "M" + d.source.x + "," + d.source.y + "Q"+ qx + "," + qy 
                + " " + tx + "," + ty;  // to "node_size" pixels before
                //+ " " + d.target.x + "," + d.target.y; // til target
      });

By the way; you'll have to do the same for the 'source' arrow head (I only have it at the target)

you may order the svg elements such that the circles will be rendered first, the lines with arrows thereafter (in d3 there is a .ordermethod, see here for details. for the record, the corrsponding part of the raphael api is discussed here).

I search online, none of the answer worked, so I made my own:

Here is the code:

   //arrows
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", 9)
    .attr("refY", 0)
    .attr("markerWidth", 10)
    .attr("markerHeight", 10)
    .attr("orient", "auto")
    .append("path")
    .attr("d", "M0,-5L10,0L0,5 L10,0 L0, -5")
    .style("stroke", "#4679BD")
    .style("opacity", "0.6"); 

  //Create all the line svgs but without locations yet
var link = svg.selectAll(".link")
   .data(forceData.links)
   .enter().append("line")
   .attr("class", "link")
   .style("marker-end", "url(#suit)");

//Set up the force layout
var force = d3.layout.force()
    .nodes(forceData.nodes)
    .links(forceData.links)
    .charge(-120)
    .linkDistance(200)
    .size([width, height])
    .on("tick", tick)
    .start();

function tick(){
    link.attr("x1", function (d) { return d.source.x; })
        .attr("y1", function (d) { return d.source.y; })
        .attr("x2", function (d) { 
            return calculateX(d.target.x, d.target.y, d.source.x, d.source.y, d.target.radius); 
        })
        .attr("y2", function (d) { 
            return calculateY(d.target.x, d.target.y, d.source.x, d.source.y, d.target.radius);
        });

    d3.selectAll("circle")
        .attr("cx", function (d) { return d.x; })
        .attr("cy", function (d) { return d.y; });

    d3.select("#forcelayoutGraph").selectAll("text")
        .attr("x", function (d) { return d.x; })
        .attr("y", function (d) { return d.y; });
}
function calculateX(tx, ty, sx, sy, radius){
    if(tx == sx) return tx;                 //if the target x == source x, no need to change the target x.
    var xLength = Math.abs(tx - sx);    //calculate the difference of x
    var yLength = Math.abs(ty - sy);    //calculate the difference of y
    //calculate the ratio using the trigonometric function
    var ratio = radius / Math.sqrt(xLength * xLength + yLength * yLength);
    if(tx > sx)  return tx - xLength * ratio;    //if target x > source x return target x - radius
    if(tx < sx) return  tx + xLength * ratio;    //if target x < source x return target x + radius
}
function calculateY(tx, ty, sx, sy, radius){
    if(ty == sy) return ty;                 //if the target y == source y, no need to change the target y.
    var xLength = Math.abs(tx - sx);    //calculate the difference of x
    var yLength = Math.abs(ty - sy);    //calculate the difference of y
    //calculate the ratio using the trigonometric function
    var ratio = radius / Math.sqrt(xLength * xLength + yLength * yLength);
    if(ty > sy) return ty - yLength * ratio;   //if target y > source y return target x - radius
    if(ty < sy) return ty + yLength * ratio;   //if target y > source y return target x - radius
}
发布评论

评论列表(0)

  1. 暂无评论