vasturiano / force-graph

Force-directed graph rendered on HTML5 canvas
https://vasturiano.github.io/force-graph/example/directional-links-particles/
MIT License
1.61k stars 248 forks source link

How to add link label curved linlks. #39

Open gdswapnil11 opened 5 years ago

gdswapnil11 commented 5 years ago

Am using force-graph in my project and i am using 'linkCurvature ' for curve link and ' .linkCanvasObjectMode' and '.linkCanvasObject' for link label but problem is that am not able to get link label on curved links, how can i do so.

Here i share my code.

`

    fetch('../assets/json/miserables.json').then(res => res.json()).then(data => {
      this.Graph = ForceGraph()
      (document.getElementById('graph'))
        .graphData(data)
        .nodeId('id')
        .nodeLabel('name')
        .linkCurvature('curvature')
        .nodeAutoColorBy('group')
        .linkDirectionalArrowLength(1) // 0 off 1 on for arrow
        .linkDirectionalArrowRelPos(function (d) {
          return d.markerPosition; // 0 start, 0.5 middle, 1 end
        })
        .linkDirectionalArrowLength(5)
        .linkDirectionalArrowLength(5)
        .linkCanvasObjectMode(() => 'after')
    .linkCanvasObject((link, ctx) => {
      const MAX_FONT_SIZE = 4;
      const LABEL_NODE_MARGIN = this.Graph.nodeRelSize() * 1.5;
      const start = link.source;
      const end = link.target;
      // ignore unbound links
      if (typeof start !== 'object' || typeof end !== 'object') return;
      // calculate label positioning
      const textPos = Object.assign({},...['x', 'y'].map(c => ({
        [c]: start[c] + (end[c] - start[c]) / 2 // calc middle point
      })));
      const relLink = { x: end.x - start.x, y: end.y - start.y };
      const maxTextLength = Math.sqrt(Math.pow(relLink.x, 2) + Math.pow(relLink.y, 2)) - LABEL_NODE_MARGIN * 2;
      let textAngle = Math.atan2(relLink.y, relLink.x);
      // maintain label vertical orientation for legibility
      if (textAngle > Math.PI / 2) textAngle = -(Math.PI - textAngle);
      if (textAngle < -Math.PI / 2) textAngle = -(-Math.PI - textAngle);
      const label = link.linkType;
      // estimate fontSize to fit in link length
      ctx.font = '1px Sans-Serif';
      const fontSize = Math.min(MAX_FONT_SIZE, maxTextLength / ctx.measureText(label).width);
      ctx.font = `${fontSize}px Sans-Serif`;
      const textWidth = ctx.measureText(label).width;
      const bckgDimensions = [textWidth, fontSize].map(n => n + fontSize * 0.2); // some padding
      // draw text label (with background rect)
      ctx.save();
      ctx.translate(textPos.x, textPos.y);
      ctx.rotate(textAngle);
      ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
      ctx.fillRect(- bckgDimensions[0] / 2, - bckgDimensions[1] / 2, ...bckgDimensions);
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      ctx.fillStyle = 'darkgrey';
      ctx.setLineDash([5, 5]);
      ctx.fillText(label, 0, 0);
      ctx.restore();
    })

    .nodeCanvasObjectMode(() => 'after')
    .nodeCanvasObject((node, ctx, globalScale) => {
      const label = node.Name;
      const fontSize = 18 / globalScale;
      ctx.font = `${fontSize}px Arial`;
      const textWidth = ctx.measureText(label).width;
      const bckgDimensions = [textWidth, fontSize].map(n => n + fontSize * 0.2); // some padding
      ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
      ctx.fillRect(node.x - bckgDimensions[0] / 2, node.y - bckgDimensions[1] / 2, ...bckgDimensions);
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      ctx.fillStyle = node.color;
      // ctx.fillStyle = '#7D8080';
      ctx.fillText(label, node.x, node.y);
    })

    .d3Force('collide', d3.forceCollide(30))

    .linkColor(function(d){
      return d.color;
    })

    .zoom(2)
    .nodeRelSize(10)
    .linkDirectionalParticles(1)
    .linkDirectionalParticleColor(function(){
      return "#6ECF9C";
    })
    .linkDirectionalArrowColor(function(){
      return "#757977";
    })
    .onNodeClick(node => {
      // Center/zoom on node
      this.Graph.centerAt(node.x, node.y, 1000);
      this.Graph.zoom(8, 1500);
    });
});`
gdswapnil11 commented 5 years ago

@vasturiano is there any solution.

vasturiano commented 5 years ago

@gdswapnil11 you should calculate where you wish to position your link labels in this part:

      const textPos = Object.assign({},...['x', 'y'].map(c => ({
        [c]: start[c] + (end[c] - start[c]) / 2 // calc middle point
      })));

This uses the halfway point in between the two nodes. If you wish it to be positioned elsewhere, simply change that calculation.

gdswapnil11 commented 5 years ago

hey @vasturiano , i tried this, but can not find right solution ,can u provide me sample code for this. i use curvature for curve links.

vasturiano commented 5 years ago

@gdswapnil11 which part are you not able to get to work properly?

gdswapnil11 commented 5 years ago

Capture

@vasturiano my output look like this, label on curved line are over lapping on one position so how do i manage as per link curvature.

vasturiano commented 5 years ago

@gdswapnil11 have you tried adjusting the textPos method as mentioned above?

gdswapnil11 commented 5 years ago

hey, @vasturiano yes i have tried adjusting the textPos, here is my sample code

ctx.fillText(label,0, curvature*50);

image

Still my output look like, i have another question that can we use ctx.bezierCurveTo() for curve text.

aoloo commented 3 years ago

@gdswapnil11 do you by chance have a repo of your implementation? I have a similar use case.

Bomfim commented 3 years ago

Hey Guys, a little bit of patience and math then I solved the problem this way: force-graph uses a third-party lib to render these curved links called bezier-js ,so i made a function that calculates a point along the curve and if you pass 0.5 as parameter you will reach the middle point of the curve. Thanks @vasturiano for the help.

function getQuadraticXY(t, sx, sy, cp1x, cp1y, ex, ey) {
  return {
    x: (1 - t) * (1 - t) * sx + 2 * (1 - t) * t * cp1x + t * t * ex,
    y: (1 - t) * (1 - t) * sy + 2 * (1 - t) * t * cp1y + t * t * ey,
  };
}

if (+link.curvature > 0) {
          textPos = getQuadraticXY(
            0.5,
            start.x,
            start.y,
            link.__controlPoints[0],
            link.__controlPoints[1],
            end.x,
            end.y
          );
        }

If anyone wants to see here is my code

aoloo commented 3 years ago

@Bomfim thank you

federico-acn commented 2 years ago

Hi :)

Did anyone get anything better than this? image

I also tried with this library https://github.com/Viglino/Canvas-TextPath but I find difficult to find the coordinates of the path

thanks

moda20 commented 1 year ago

I tried this function and it worked correctly for curved links between 2 nodes, but not correctly for self referencing links (from A to A) @vasturiano can you help with this ? it's probably an issue that the start and end coordinates are the same image

moda20 commented 1 year ago

Ok, i was able to get this to work correctly, by updating the formula above to use 4 control pints instead of 2 which is the case for self referencing links : i got the formula from here (which is from wikipedia) : https://stackoverflow.com/a/54216695 this is the full function usage :

CanvasRenderingContext2D.prototype.getQuadraticXY = function (t, sx, sy, cp1x, cp1y, ex, ey) {
    return {
      x: (1 - t) * (1 - t) * sx + 2 * (1 - t) * t * cp1x + t * t * ex,
      y: (1 - t) * (1 - t) * sy + 2 * (1 - t) * t * cp1y + t * t * ey,
    };
  };
 CanvasRenderingContext2D.prototype.getQuadraticXYFourWays = function (
    t,
    sx,
    sy,
    cp1x,
    cp1y,
    cp2x,
    cp2y,
    ex,
    ey,
  ) {
    return {
      x:
        (1 - t) * (1 - t) * (1 - t) * sx +
        3 * (1 - t) * (1 - t) * t * cp1x +
        3 * (1 - t) * t * t * cp2x +
        t * t * t * ex,
      y:
        (1 - t) * (1 - t) * (1 - t) * sy +
        3 * (1 - t) * (1 - t) * t * cp1y +
        3 * (1 - t) * t * t * cp2y +
        t * t * t * ey,
    };
  };

  (.....)

  if (+link.curvature > 0) {
      if (start.id === end.id) {
        textPos = this.getQuadraticXYFourWays(
          0.5,
          start.x,
          start.y,
          link.__controlPoints[0],
          link.__controlPoints[1],
          link.__controlPoints[2],
          link.__controlPoints[3],
          end.x,
          end.y,
        );
      } else {
        textPos = this.getQuadraticXY(
          0.5,
          start.x,
          start.y,
          link.__controlPoints[0],
          link.__controlPoints[1],
          end.x,
          end.y,
        );
      }
    }
Bomfim commented 1 year ago

Awesome @moda20, you should be very proud dude