椭圆弧箭头边缘d3强制布局

时间:2017-10-05 19:15:15

标签: javascript jquery html d3.js canvas

我正在使用强制布局来创建有向图。 它在画布上呈现。我的示例示例位于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/1153292 https://bl.ocks.org/ramtob/3658a11845a89c4742d62d32afce3160
http://bl.ocks.org/thomasdobber/9b78824119136778052f64a967c070e0 Drawing multiple edges between two nodes with d3

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

enter image description here

我的代码:



<!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>  
&#13;
&#13;
&#13;

1 个答案:

答案 0 :(得分:6)

使用箭头

从圆弧绘制圆弧

基本问题

这两点需要是随机的(从任何地方到任何地方)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. 旋转标准90deg
  5. 通过旋转范数按弯曲乘以距离并添加到中点以找到弧上的中点
  6. 使用3个点找到适合所有3个点的圆的半径。
  7. 使用半径查找圆弧的中心
  8. 从中心找到开始和结束的方向
  9. 使用箭头len查找箭头的角度长度,现在我们有半径
  10. 从箭头内部绘制圆弧或开始/结束(取决于显示的箭头)
  11. 从弧形中心沿着直线的方向绘制箭头
  12. 其他问题。

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

    当两点接近两点(即它们重叠)时,还有一个问题。除了不绘制线条和箭头(如果它们不适合)之外,没有真正的解决方案。

    作为演示的解决方案

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

    执行此操作的函数称为drawBend,我在其中添加了很多注释。还有一些注释行可以让您在开始和结束之间的距离发生变化时更改弧的变化方式。如果你取消注释一个,设置变量b1 (你指定给x3,y3是弧上的中点)你必须注释掉其他作业

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

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

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

    它的速度相当快,但如果你打算实时绘制100多个,你可以通过使用弧然后再分享一些计算来优化。如果交换开始和结束,从开始到结束的弧将以另一种方式弯曲,并且有许多值保持不变,因此您可以获得两个弧,用于绘制2的大约75%的CPU负载

    &#13;
    &#13;
    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);
    &#13;
    canvas { position : absolute; top : 0px; left : 0px; }
    &#13;
    <canvas id="canvas"></canvas>
    &#13;
    &#13;
    &#13;