Sumeet Kumar Yadav
Sumeet Kumar Yadav

Reputation: 12985

Elliptical arc arrow edge d3 forced layout

I am using forced layout to create directed graph . Its rendered on canvas . My sample example is at http://jsbin.com/vuyapibaqa/1/edit?html,output

Now I am inspired from
https://bl.ocks.org/mattkohl/146d301c0fc20d89d85880df537de7b0#index.html

Few Resources in d3 svg , something similar i am trying to get in canvas.

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.

Want to add elliptical arc connecting edge with arrow . How to achieve this in canvas.

enter image description here

My Code :

<!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>  

Upvotes: 3

Views: 1971

Answers (1)

Blindman67
Blindman67

Reputation: 54039

Draw arc from circle to circle with arrow heads

The basic problem

The two points need to be random(from anywhere to anywhere) x1,y1 and x2,y2. You will want to control the amount of bending that is invariant to the distance between the points (ie the same amount of bending if the distance between points is 100 pixels or 10 pixels)

Thus inputs are

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.

Basic method steps

  1. Find the mid-point between the two end points.
  2. Get distance between points
  3. Get the normalised vector from start to end.
  4. Rotate norm 90deg
  5. Multiply distance by bend by rotated norm and add to mid point to find mid point on arc
  6. With the 3 points find the radius of a circle that will fit all 3 points.
  7. Use the radius to find the center of the arc
  8. From the center find the direction to the start and end
  9. Use the arrow len to find angular length of arrows now we have the radius
  10. Draw the arc from inside arrows or start / end (depending if arrows shown)
  11. Draw arrow from point with flat side along line from arc center

Additional problems.

I assume you want the lines to be from one circle to the next. Thus you want to specify the circle centers and the radius of the circles. This will require two additional arguments one for the start circle radius and one for the end.

There is also the problem of what to do when the two points are two close (ie they overlap). There is not real solution apart from not to draw the lines and arrows if they don't fit.

The Solution as a Demo

The demo has to circles that change size over time, there are 6 arcs with different bend values of 0.1,0.3, 0.6 and -0.1, -0.3, -0.6. Move the mouse to change end circles position.

The function that does it all is called drawBend and I have put a lot of comments in there, There is also some commented lines that let you change how the arcs change when the distance between start and end changes. If you uncomment one, setting the variable b1 (where you assign to x3,y3 the mid point on the arc) you MUST comment out the other assignments

The solution to finding the arc radius and center is complicated and there is most likely a better solution due to the symmetry. That part will find a circle to fit any 3 points, (if not all on a line) so may have other uses for you.

Update I have found a much better method of finding the arc radius and thus center point. The symmetry provided a very convenient set of similar triangles and and thus I could shorten the function by 9 lines. I have updated the demo.

The arc is draw as a stroke, and the arrowheads as a fill.

Its reasonably quick, but if you plan to draw many 100's in real-time you can optimise by having the arc from and then back share some calcs. The arc from start to end will bend the other way if you swap the start and end, and there are many values that remain unchanged, so you can get two arcs for about a 75% CPU load of drawing 2

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; }
<canvas id="canvas"></canvas>

Upvotes: 7

Related Questions