用箭头连接可变半径的节点

时间:2013-03-19 09:33:00

标签: javascript graph svg d3.js

我有一些不同半径的圆圈/节点,我必须将它们与带箭头的路径连接起来。

以下是标记的代码:

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");  

我已将圆的半径存储在数组中。 这是屏幕截图:

enter image description here

箭头实际上是“圈内”。如何让箭头位于圆圈表面?

4 个答案:

答案 0 :(得分:6)

这是一个老问题,但如果您希望箭头位于节点的边缘而不是在它们的顶部或下方,这是我的解决方案。我的方法也是绘制连接节点的路径,使得端点位于节点上。边缘而不是节点中心。从Mobile Patent Suits示例(http://bl.ocks.org/mbostock/1153292)开始,我将linkArc方法替换为:

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;
}

请注意,此代码需要" r,"或半径,属性在节点数据中。为了将箭头的点放在正确的位置,我更改了refX和refY属性,以便箭头的点位于节点的边缘:

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");

答案 1 :(得分:5)

这真的很有趣;我昨天刚刚解决了这个问题。

我所做的是结束节点边缘的路径,而不是中心的路径。 我的情况比较复杂,因为我使用Bezier曲线,而不是直线,但这可能对您有所帮助:

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
      });
顺便说一下;你必须为'源'箭头做同样的事情(我只在目标处拥有它)

答案 2 :(得分:2)

您可以订购svg元素,以便首先渲染圆圈,之后带箭头的线条(在d3中有一个.order方法,see here for details。对于记录,相应的部分raphael api是discussed here)。

答案 3 :(得分:0)

我在线搜索,答案都没有,所以我自己做了:

以下是代码:

   //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
}