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:
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 —:
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!);
}
/>
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.
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...
<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