skip to Main Content

Indeed, this is very specific to a package, more specifically, @vasturiano‘s React Force Graph. But, since it makes heavy use of the HTML Canvas and D3.js, I thought maybe someone here could shed a light on how to solve it.

What I would like to do has already been reported on issue #433 of that project, but it hasn’t received any answers. Anyways, I would like to add text on top of nodes, with said text not overflowing out of the node circle boundary, something like this:

Not overflowing

I think the best you can do right now is something like this — I’ve only been able to do something like it through React Force 3D actually —:

Overflowing

By using the example of 2D Text Nodes provided by @vasturiano (ctx.fillText(...)), I’ve managed to add text to circular nodes, but it somehow ends up behind it, no matter where I put it:

<ForceGraph2D
  graphData={dataMemo}
  nodeCanvasObjectMode={() => "before"}
  nodeCanvasObject={(node, ctx) => {
    ctx.beginPath();
    ctx.arc(node.x!, node.y!, NODE_R * 1.4, 0, 2 * Math.PI, false);
    ctx.fill();
    ctx.fillText("hello", node.x!, node.y!);
  }
/>

Behind the nodes...

Does anyone know how to stack the text and delimit it properly? I expected the text to at least be on top of the node circles, since it’s supposedly only drawn later on, I believe (I don’t think there’s a z-index on <canvas> so I don’t think that’s a feasible direction).

@vasturiano provided me a link to how to do the bounded text: Mike Bostock – Fit Text to Circle, while also noting that this is something related to HTML Canvas, not his project itself.

2

Answers


  1. Your question has multiple questions hidden in one, you are dealing with a complex problem…
    My answer will not be a fix to your problem, but should send you in the right direction.

    Start by reading more about how is text aligned when you do draw it in a canvas:

    Now your problem, you have to calculate if the text fits in the given circle, for that you need to know or at least get approximations on the size of your text, more reading:

    With all that you should be able to calculate if the text fits, if not, split it or try something to "make it fit" and you loop until you get something that fits.


    Here is some code I had for some other project…
    In that code I change the font size to make the letter look nicer in the circle

    const canvas = document.getElementById('c');
    const ctx = canvas.getContext('2d');
    
    function textFits(text, max) {
      let m = ctx.measureText(text);
      let h = m.actualBoundingBoxAscent + m.actualBoundingBoxDescent;
      return Math.sqrt(m.width * m.width + h * h) < max;
    }
    
    function draw(x, y, r, text) {
      ctx.beginPath();
      ctx.fillStyle = "black";
      ctx.arc(x, y, r, 0, 2 * Math.PI, false);
      ctx.fill();
    
      let size = 300;
      do {
        ctx.font = size + "px monospace";
        size -= 2;
      } while (!textFits(text, r*1.8));
      ctx.textAlign = "center";
      ctx.textBaseline = "middle";
      ctx.fillStyle = "red";
      ctx.fillText(text, x, y);
    }
    
    draw(240, 110, 120, "m")
    draw(30, 140, 30, "o")
    draw(60, 60, 50, "w")
    <canvas id="c" width=600 height=200></canvas>
    Login or Signup to reply.
  2. Below are a simplified version of the code you provided in the github question:
    https://github.com/vasturiano/react-force-graph/issues/433#issuecomment-1807292462

    If we are going to troubleshoot drawing overlap, we don’t need a bunch of node, one will suffice and you don’t need all the other fancy hover functionality…

    simple drawing:

    <head>
      <style> body { margin: 0; } </style>
      <script src="https://unpkg.com/react/umd/react.production.min.js"></script>
      <script src="https://unpkg.com/react-dom/umd/react-dom.production.min.js"></script>
      <script src="https://unpkg.com/@babel/standalone"></script>
      <script src="https://unpkg.com/react-force-graph-2d"></script>
    </head>
    
    <body>
      <div id="graph"></div>
      <script type="text/jsx">
        const { useState, useCallback } = React;
    
        const HighlightGraph = () => {
          const data = genRandomTree(1);
          const paintRing = useCallback((node, ctx) => {
            ctx.beginPath();
            ctx.arc(node.x, node.y, 20, 0, 2 * Math.PI, false);
            ctx.lineTo(node.x, node.y);
            ctx.stroke();
            ctx.fillText("123", node.x, node.y);
          }, []);
    
          return <ForceGraph2D
            graphData={data}
            nodeRelSize={10}
            autoPauseRedraw={false}
            nodeCanvasObjectMode={node => 'before' }
            nodeCanvasObject={paintRing}
          />;
        };
        ReactDOM.render( <HighlightGraph />, document.getElementById('graph') );
        
        function genRandomTree(N) {
          return {
            nodes: [...Array(N).keys()].map((i) => ({ id: i })),
            links: [...Array(N).keys()].filter((id) => id)
              .map((id) => ({ "source": id, "target": id}))
          };
        }
      </script>
    </body>

    Now let’s change to draw after
    nodeCanvasObjectMode={node => 'after' }

    We can see the difference

    <head>
      <style> body { margin: 0; } </style>
      <script src="https://unpkg.com/react/umd/react.production.min.js"></script>
      <script src="https://unpkg.com/react-dom/umd/react-dom.production.min.js"></script>
      <script src="https://unpkg.com/@babel/standalone"></script>
      <script src="https://unpkg.com/react-force-graph-2d"></script>
    </head>
    
    <body>
      <div id="graph"></div>
      <script type="text/jsx">
        const { useState, useCallback } = React;
    
        const HighlightGraph = () => {
          const data = genRandomTree(1);
          const paintRing = useCallback((node, ctx) => {
            ctx.beginPath();
            ctx.arc(node.x, node.y, 20, 0, 2 * Math.PI, false);
            ctx.lineTo(node.x, node.y);
            ctx.stroke();
            ctx.fillText("123", node.x, node.y);
          }, []);
    
          return <ForceGraph2D
            graphData={data}
            nodeRelSize={10}
            autoPauseRedraw={false}
            nodeCanvasObjectMode={node => 'after' }
            nodeCanvasObject={paintRing}
          />;
        };
        ReactDOM.render( <HighlightGraph />, document.getElementById('graph') );
        
        function genRandomTree(N) {
          return {
            nodes: [...Array(N).keys()].map((i) => ({ id: i })),
            links: [...Array(N).keys()].filter((id) => id)
              .map((id) => ({ "source": id, "target": id}))
          };
        }
      </script>
    </body>

    That center light blue circle is not the code in the paintRing is something else so I will set the:
    nodeRelSize={0} and we can do all the drawing in the paintRing

    <head>
      <style> body { margin: 0; } </style>
      <script src="https://unpkg.com/react/umd/react.production.min.js"></script>
      <script src="https://unpkg.com/react-dom/umd/react-dom.production.min.js"></script>
      <script src="https://unpkg.com/@babel/standalone"></script>
      <script src="https://unpkg.com/react-force-graph-2d"></script>
    </head>
    
    <body>
      <div id="graph"></div>
      <script type="text/jsx">
        const { useState, useCallback } = React;
    
        const HighlightGraph = () => {
          const data = genRandomTree(1);
          const paintRing = useCallback((node, ctx) => {
            ctx.beginPath();
            ctx.arc(node.x, node.y, 20, 0, 2 * Math.PI, false);
            ctx.fillStyle = "blue";
            ctx.fill();
            ctx.textAlign = "center";
            ctx.textBaseline = "middle";
            ctx.fillStyle = "red";
            ctx.fillText("123", node.x, node.y);
          }, []);
    
          return <ForceGraph2D
            graphData={data}
            nodeRelSize={0}
            autoPauseRedraw={false}
            nodeCanvasObjectMode={node => 'before' }
            nodeCanvasObject={paintRing}
          />;
        };
        ReactDOM.render( <HighlightGraph />, document.getElementById('graph') );
        
        function genRandomTree(N) {
          return {
            nodes: [...Array(N).keys()].map((i) => ({ id: i })),
            links: [...Array(N).keys()].filter((id) => id)
              .map((id) => ({ "source": id, "target": id}))
          };
        }
      </script>
    </body>

    You can do a lot of things but you have to experiment with the tools you are using…
    I have never used this ForceGraph2D before, this is just me testing a few stuff

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search