Search code examples
javascriptreactjsd3.jsgraphhtml5-canvas

Adding Bounded Text to React Force 2D (HTML Canvas)


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.


Solution

  • Below are simplified versions 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