【问题标题】:linking nodes of variable radius with arrows用箭头连接可变半径节点
【发布时间】:2013-03-19 09:32:59
【问题描述】:

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

这是标记的代码:

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

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

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

【问题讨论】:

  • 实际上,我看不到,箭头实际上是在圆圈“内部”。
  • 使路径在圆的半径而不是中心开始和结束。需要一些三角函数。
  • 我可能错了,但链接的路径与构成箭头的标记是分开的。 .attr("refX", 5) 设置箭头相对于圆心的偏移量。虽然它被指定为X 偏移量,但由于对象在旋转,它不对应于屏幕的 x(左右)轴。 .attr("refY", -1.5) 行也是如此。当我试图应用一个函数来解决它时,我失败了。我可以偏移箭头,但不能始终保持正确的距离。

标签: javascript graph svg d3.js


【解决方案1】:

这是一个老问题,但如果您希望箭头位于节点的边缘而不是在它们的顶部或下方,这是我的解决方案。我的方法也是绘制连接节点的路径,使端点位于节点的边缘而不是节点的中心。从 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");

【讨论】:

    【解决方案2】:

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

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

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

    顺便说一句;你必须对“源”箭头做同样的事情(我只有在目标处)

    【讨论】:

      【解决方案3】:

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

      【讨论】:

        【解决方案4】:

        我在网上搜索,没有一个答案,所以我自己做了:

        代码如下:

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

        【讨论】:

          猜你喜欢
          • 2015-03-22
          • 2020-01-04
          • 2021-01-07
          • 1970-01-01
          • 2017-02-22
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多