【问题标题】:Elliptical arc arrow edge d3 forced layout椭圆弧箭头边d3力布局
【发布时间】:2026-01-26 06:40:01
【问题描述】:

我正在使用强制布局来创建有向图。 它呈现在画布上。我的示例位于http://jsbin.com/vuyapibaqa/1/edit?html,output

现在我的灵感来自
https://bl.ocks.org/mattkohl/146d301c0fc20d89d85880df537de7b0#index.html

d3 svg 中的资源很少,我试图在画布中获得类似的东西。

http://jsfiddle.net/zhanghuancs/a2QpA/

http://bl.ocks.org/mbostock/1153292https://bl.ocks.org/ramtob/3658a11845a89c4742d62d32afce3160
http://bl.ocks.org/thomasdobber/9b78824119136778052f64a967c070e0 Drawing multiple edges between two nodes with d3.

想用箭头添加椭圆弧连接边。如何在画布中实现这一点。

我的代码:

<!DOCTYPE html>
<html>
<head>
        <title>Sample Graph Rendring Using Canvas</title>
        <script src="https://rawgit.com/gka/randomgraph.js/master/randomgraph.js"></script>
        <script src="https://d3js.org/d3.v4.min.js"></script>
</head>
<body>
    <script>
        var graph = {}//randomgraph.WattsStrogatz.beta(15, 4, 0.06);

    graph.nodes = [{"label":"x"} , {"label":"y"}];
    graph.edges = [{source:0,target:1},{source:0,target:1},
                   {source:1,target:0}]

        var canvas = null
        var width = window.innerWidth,
            height = window.innerHeight;
        canvas = d3.select("body").append("canvas").attr("width",width).attr("height",height);

        var context = canvas.node().getContext("2d");


        force = d3.forceSimulation()
                .force("link", d3.forceLink().id(function(d) { 
                     return d.index;
                })).force("charge", d3.forceManyBody())
                .force("center", d3.forceCenter(width / 2, height / 2));

        force.nodes(graph.nodes);
        force.force("link").links(graph.edges).distance(200);

        var detachedContainer = document.createElement("custom");
            dataContainer = d3.select(detachedContainer);

        link = dataContainer.selectAll(".link").data(graph.edges)
              .enter().append("line").attr("class", "link")
              .style("stroke-width", 2)

        node = dataContainer.selectAll(".node").data(graph.nodes)
              .enter().append("g");

          var circles = node.append("circle")
              .classed("circle-class", true)
              .attr("class", function (d){ return "node node_" + d.index;})
              .attr("r", 5)
              .attr("fill", 'red')
              .attr("strokeStyle", 'black');

        d3.timer(function(){
            context.clearRect(0, 0, width, height);

            // draw links
            link.each(function(d) {
              context.strokeStyle = "#ccc";
              /***** Elliptical arcs *****/
              context.stroke(new Path2D(linkArc(d)));
              /***** Elliptical arcs *****/
            });

            context.lineWidth = 2;
            node.each(function(d) {

              context.beginPath();
              context.moveTo(d.x, d.y);
              var r = d3.select(this).select("circle").node().getAttribute('r');   

              d.x = Math.max(30, Math.min(width - 30, d.x));
              d.y = Math.max(30, Math.min(height - 30, d.y));         
              context.closePath();
              context.arc(d.x, d.y, r, 0, 2 * Math.PI);

              context.fillStyle = d3.select(this).select("circle").node().getAttribute('fill');
              context.strokeStyle = d3.select(this).select("circle").node().getAttribute('strokeStyle');
              context.stroke();
              context.fill();

              context.beginPath();
              context.arc(d.x + 15, d.y-20, 5, 0, 2 * Math.PI);
              context.fillStyle = "orange";
              context.strokeStyle = "orange";
              var data = d3.select(this).data();
              context.stroke();
              context.fill();
              context.font = "10px Arial";
              context.fillStyle = "black";
              context.strokeStyle = "black";
              context.fillText(parseInt(data[0].index),d.x + 10, d.y-15);
            });

        });

        circles.transition().duration(5000).attr('r', 20).attr('fill', 'orange');

        canvas.node().addEventListener('click',function( event ){
           console.log(event)
            // Its COMING ANY TIME INSIDE ON CLICK OF CANVAS
        });

        /***** Elliptical arcs *****/
        function linkArc(d) {
          var dx = d.target.x - d.source.x,
              dy = d.target.y - d.source.y,
              dr = Math.sqrt(dx * dx + dy * dy);
          return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
        }
        /***** Elliptical arcs *****/
    </script>
</body>
</html>  

【问题讨论】:

  • 这可能会有所帮助:"Draw path in Canvas with SVG Path data (SVG paths to Canvas paths)"。这使得绘制线条非常容易。不过,您仍然需要弄清楚如何绘制标记。
  • 我试过了,没用,你能帮忙吗?
  • forked 你的 JSBin 并添加了椭圆弧,这非常简单(查找 /***** Elliptical arcs *****/ cmets)。当我第一次阅读您的问题时,我已经开始将其放入答案中,但是一旦我意识到箭头标记丢失,我就不再发布它。如果你也想要它们,这将是一个更加复杂的壮举。
  • 类似这样的东西,我们可以把它转换成画布吗?

标签: javascript jquery html d3.js canvas


【解决方案1】:

用箭头从圆画圆弧

基本问题

这两个点需要是随机的(从任何地方到任何地方)x1,y1 和 x2,y2。您将需要控制与点之间的距离不变的弯曲量(即,如果点之间的距离为 100 像素或 10 像素,则弯曲量相同)

因此输入是

x1,y1 // as start
x2,y2 // as end
bend  // as factor of distance between points 
      // negative bends up (to right) 
      // positive bends down (to left of line)
arrowLen  // in pixels
arrowWidth // in pixels,
arrowStart // boolean if arrow at start
arrowEnd   // boolean if arrow at end.

基本方法步骤

  1. 找到两个端点之间的中点。
  2. 获取点之间的距离
  3. 从头到尾获取归一化向量。
  4. 标准旋转90度
  5. 将距离乘以弯曲度乘以旋转的范数并添加到中点以找到弧上的中点
  6. 使用 3 个点找到适合所有 3 个点的圆的半径。
  7. 使用半径找到圆弧的中心
  8. 从中心找到开始和结束的方向
  9. 现在我们有了半径,使用箭头 len 求箭头的角长度
  10. 从内部箭头或开始/结束绘制弧线(取决于箭头是否显示)
  11. 从圆弧中心沿直线的平边点绘制箭头

其他问题。

我假设您希望线条从一个圆圈到下一个圆圈。因此,您要指定圆心和圆的半径。这将需要两个额外的参数,一个用于起始圆半径,一个用于结束。

还有一个问题是当两个点两个接近(即它们重叠)时该怎么办。除了不合适的线和箭头不画之外,没有真正的解决方案。

作为演示的解决方案

该演示必须是随着时间的推移而改变大小的圆,有 6 个圆弧具有不同的弯曲值,分别为 0.1、0.3、0.6 和 -0.1、-0.3、-0.6。移动鼠标改变结束圆的位置。

完成这一切的函数称为drawBend,我在那里放了很多cmets,还有一些注释行可以让你改变当起点和终点之间的距离发生变化时弧线的变化方式。如果你取消注释一个,设置变量b1 (你分配给x3,y3弧上的中点)你必须注释掉其他分配

求圆弧半径和圆心的解决方案很复杂,由于对称性,很可能有更好的解决方案。该部分将找到一个适合任何 3 个点的圆(如果不是全部在一条线上),因此可能对您有其他用途。

更新我找到了一个更好的方法来找到圆弧半径和中心点。对称性提供了一组非常方便的相似三角形,因此我可以将函数缩短 9 行。我已经更新了演示。

弧线绘制为笔划,箭头绘制为填充。

它相当快,但是如果您计划实时绘制许多 100,您可以通过让弧从然后回来共享一些计算来进行优化。如果你交换开始和结束,从开始到结束的弧会向另一个方向弯曲,并且有许多值保持不变,所以你可以获得两条弧,大约 75% 的绘图 2 的 CPU 负载

const ctx = canvas.getContext("2d");

const mouse  = {x : 0, y : 0, button : false}
function mouseEvents(e){
	mouse.x = e.pageX;
	mouse.y = e.pageY;
	mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
}
["down","up","move"].forEach(name => document.addEventListener("mouse"+name,mouseEvents));




// x1,y1 location of a circle start
// x2,y2 location of the end circle
// bend factor. negative bends up for, positive bends down. If zero the world will end 
// aLen is Arrow head length in pixels
// aWidth is arrow head width in pixels
// sArrow boolean if true draw start arrow
// eArrow  boolean if true draw end  arrow
// startRadius = radius of a circle if start attached to circle
// endRadius = radius of a circle if end attached to circle
function drawBend(x1, y1, x2, y2, bend, aLen, aWidth, sArrow, eArrow, startRadius, endRadius){
    var mx, my, dist, nx, ny, x3, y3, cx, cy, radius, vx, vy, a1, a2;
    var arrowAng,aa1,aa2,b1;
    // find mid point
    mx = (x1 + x2) / 2;  
    my = (y1 + y2) / 2;
    
    // get vector from start to end
    nx = x2 - x1;
    ny = y2 - y1;
    
    // find dist
    dist = Math.sqrt(nx * nx + ny * ny);
    
    // normalise vector
    nx /= dist;
    ny /= dist;
    
    // The next section has some optional behaviours
    // that set the dist from the line mid point to the arc mid point
    // You should only use one of the following sets
    
    //-- Uncomment for behaviour of arcs
    // This make the lines flatten at distance
    //b1 =  (bend * 300) / Math.pow(dist,1/4);

    //-- Uncomment for behaviour of arcs
    // Arc bending amount close to constant
    // b1 =  bend * dist * 0.5

    b1 = bend * dist

    // Arc amount bend more at dist
    x3 = mx + ny * b1;
    y3 = my - nx * b1;
   
    // get the radius
    radius = (0.5 * ((x1-x3) * (x1-x3) + (y1-y3) * (y1-y3)) / (b1));

    // use radius to get arc center
    cx = x3 - ny * radius;
    cy = y3 + nx * radius;

    // radius needs to be positive for the rest of the code
    radius = Math.abs(radius);

    


    // find angle from center to start and end
    a1 = Math.atan2(y1 - cy, x1 - cx);
    a2 = Math.atan2(y2 - cy, x2 - cx);
    
    // normalise angles
    a1 = (a1 + Math.PI * 2) % (Math.PI * 2);
    a2 = (a2 + Math.PI * 2) % (Math.PI * 2);
    // ensure angles are in correct directions
    if (bend < 0) {
        if (a1 < a2) { a1 += Math.PI * 2 }
    } else {
        if (a2 < a1) { a2 += Math.PI * 2 }
    }
    
    // convert arrow length to angular len
    arrowAng = aLen / radius  * Math.sign(bend);
    // get angular length of start and end circles and move arc start and ends
    
    a1 += startRadius / radius * Math.sign(bend);
    a2 -= endRadius / radius * Math.sign(bend);
    aa1 = a1;
    aa2 = a2;
   
    // check for too close and no room for arc
    if ((bend < 0 && a1 < a2) || (bend > 0 && a2 < a1)) {
        return;
    }
    // is there a start arrow
    if (sArrow) { aa1 += arrowAng } // move arc start to inside arrow
    // is there an end arrow
    if (eArrow) { aa2 -= arrowAng } // move arc end to inside arrow
    
    // check for too close and remove arrows if so
    if ((bend < 0 && aa1 < aa2) || (bend > 0 && aa2 < aa1)) {
        sArrow = false;
        eArrow = false;
        aa1 = a1;
        aa2 = a2;
    }
    // draw arc
    ctx.beginPath();
    ctx.arc(cx, cy, radius, aa1, aa2, bend < 0);
    ctx.stroke();

    ctx.beginPath();

    // draw start arrow if needed
    if(sArrow){
        ctx.moveTo(
            Math.cos(a1) * radius + cx,
            Math.sin(a1) * radius + cy
        );
        ctx.lineTo(
            Math.cos(aa1) * (radius + aWidth / 2) + cx,
            Math.sin(aa1) * (radius + aWidth / 2) + cy
        );
        ctx.lineTo(
            Math.cos(aa1) * (radius - aWidth / 2) + cx,
            Math.sin(aa1) * (radius - aWidth / 2) + cy
        );
        ctx.closePath();
    }
    
    // draw end arrow if needed
    if(eArrow){
        ctx.moveTo(
            Math.cos(a2) * radius + cx,
            Math.sin(a2) * radius + cy
        );
        ctx.lineTo(
            Math.cos(aa2) * (radius - aWidth / 2) + cx,
            Math.sin(aa2) * (radius - aWidth / 2) + cy
        );
        ctx.lineTo(
            Math.cos(aa2) * (radius + aWidth / 2) + cx,
            Math.sin(aa2) * (radius + aWidth / 2) + cy
        );
        ctx.closePath();
    }
    ctx.fill();
}



/** SimpleUpdate.js begin **/
// short cut vars 
var w = canvas.width;
var h = canvas.height;
var cw = w / 2;  // center 
var ch = h / 2;
var globalTime = new Date().valueOf();  // global to this 

// main update function
function update(timer){
    globalTime = timer;
    if(w !== innerWidth || h !== innerHeight){  // resize if needed
      cw = (w = canvas.width = innerWidth) / 2;
      ch = (h = canvas.height = innerHeight) / 2;
    }    
    ctx.setTransform(1,0,0,1,0,0); // reset transform
    ctx.globalAlpha = 1;           // reset alpha
    ctx.clearRect(0,0,w,h);

    var startRad = (Math.sin(timer / 2000) * 0.5 + 0.5) * 20 + 5;
    var endRad = (Math.sin(timer / 7000) * 0.5 + 0.5) * 20 + 5;
    ctx.lineWidth = 2;
    ctx.fillStyle = "white";
    ctx.strokeStyle = "black";
    ctx.beginPath();
    ctx.arc(cw,ch,startRad,0,Math.PI * 2);
    ctx.fill();
    ctx.stroke();
    ctx.beginPath();
    ctx.arc(mouse.x,mouse.y,endRad,0,Math.PI * 2);
    ctx.fill();
    ctx.stroke();

    ctx.lineWidth = 2;
    ctx.fillStyle = "black";
    ctx.strokeStyle = "black";
    
    
    
    drawBend(cw,ch,mouse.x,mouse.y,-0.1,10,10,true,true,startRad + 1,endRad + 1);
    drawBend(cw,ch,mouse.x,mouse.y,-0.3,10,10,true,true,startRad + 1,endRad + 1);
    drawBend(cw,ch,mouse.x,mouse.y,-0.6,10,10,true,true,startRad + 1,endRad + 1);
    drawBend(cw,ch,mouse.x,mouse.y,0.1,10,10,true,true,startRad + 1,endRad + 1);
    drawBend(cw,ch,mouse.x,mouse.y,0.3,10,10,true,true,startRad + 1,endRad + 1);
    drawBend(cw,ch,mouse.x,mouse.y,0.6,10,10,true,true,startRad + 1,endRad + 1);


    requestAnimationFrame(update);
}
requestAnimationFrame(update);
canvas { position : absolute; top : 0px; left : 0px; }
&lt;canvas id="canvas"&gt;&lt;/canvas&gt;

【讨论】:

  • 感谢您的回答!我要接受这个。我正在学习画布和 d3 现在才刚刚开始。因此,我现在不太了解您的代码。你能帮我在边缘添加标签并在强制布局中渲染吗
  • @Sumeet 如果您仍然有超出此问题范围的问题,您可以问另一个问题,或者您可以在这个问题中提供更多细节,因为您还有一些时间与赏金和其他人可能提供更适合您需求的答案。
  • 谢谢@Bilndman67 你的回答是正确的问题在于我,因为我不太了解 d3 和 canvas 。实际上我想在边缘显示文本标签并使用强制布局进行渲染。我还有另一个问题,就像我想达到的那样*.com/questions/46609700/…