react-force-graph
react-force-graph copied to clipboard
Initial zoomToFit problem
Describe the bug I want to draw 2d graph without zooming animation at the initial rendering time. Also, don't want to fire 'zoomToFit' after initial rendering.
From the example, I tried to use 'onEngineStop' with the initial flag ref.
onEngineStop={() => isInitialRender.current && graphRef.current?.zoomToFit(50, 120)}
However, there are 2 problems.
- if there isn't enough cooldownTicks, 'zoomToFit' couldn't find the proper center position. (it seems like timing issue. sometimes it goes well, but sometime it is crushed.)
- if there is enough cooldownTicks, I have to wait the animation until cooldown is finished.
Currently I resolve this problem with 'setInterval' but it's really ugly and hacky.
useEffect(() => {
let count = 0;
const interval = setInterval(() => {
if (graphRef.current?.getGraphBbox()?.x) {
graphRef.current.zoomToFit(0, 100);
count += 1;
// HACK: make the initial zoom to fit work.
// because there isn't the proper event to handle engine stop
if (count > 100) {
clearInterval(interval);
}
}
}, 10);
return () => {
clearInterval(interval);
};
}, [graphData]);
any advice?
Screenshots
^ when cooldownTicks are not enough
^ when properly works
Additional context current <ForceGraph2D />
<ForceGraph2D
ref={graphRef}
graphData={graphData}
width={window.innerWidth - 260 - 30}
height={window.innerHeight - 50 - 100}
nodeId="author_id"
linkSource="author_id_from"
linkTarget="author_id_to"
nodeLabel="author_name"
nodeCanvasObject={(node, ctx) => {
drawNodes({ node, ctx, highlightNodes: highlightNodes.current, topAuthors: topAuthorIds.current });
}}
cooldownTicks={50}
linkWidth={1.3}
linkDirectionalParticleSpeed={0.002}
linkDirectionalParticles={5}
linkDirectionalParticleWidth={(link) => (highlightLinks.current.has(link as CitationGraphLink) ? 2 : 0)}
linkColor={(link) => getLinkLineColour(link as CitationGraphLink, highlightLinks.current)}
onEngineStop={() => graphRef.current?.zoomToFit(50, 120)}
onNodeHover={(node) =>
handleHoverNode({
node: node as AuthorNode | null,
highlightNodes: highlightNodes.current,
highlightLinks: highlightLinks.current,
})
}
/>
I also tried to use this, minimized it down to this:
const graphRef = useCallback((node) => {
if (node) {
node.zoomToFit(100, 20);
}
}, []);
<Graph
graphData={network}
ref={graphRef}
/>
node.zoomToFit has no effect. It works with setTimout.
@pojntfx I normally link element refs to useRef objects, but assuming your useCallback functionality behaves the same and gives you the dom node, I wouldn't invoke zoomToFit immediately on element mount, because the system hasn't had the opportunity to initialize and spread the nodes the final layout you'd want to fit to.
So, I'd recommend either set a timeout, or better yet handle it at onEngineStop.
@vasturiano I totally agree for your advice. however, sometimes onEngineStop callback makes zoom to wired position. (especially faster CPU system).
I set warmUpTicks={100} cooldownTicks={0}. should I set more warnUpTicks?
Try This:
This is an interesting topic and can be tricky given browsers can run on all sorts of hardware :-) I solved this by using a combination of zoomToFit() in useEffect(), onEngineStop() and some judicious setting of warmup and cooldown. Like this:
useEffect(() =>
{
const fg = fgRef.current;
fg.d3Force('charge').strength(-500);
fg.d3Force("link").distance(140)
fg.d3Force("center").strength(1)
fg.zoomToFit(10, 10);
}, []);
function onEngineStop()
{
const fg = fgRef.current;
fg.zoomToFit(1000, 10); //the '1000' presents the changed animation
}
<ForceGraph2D
ref={fgRef}
warmupTicks={250}
cooldownTime={0}
onEngineStop={onEngineStop}
. . .
/>
Note that warmupticks needs to be >0 (250 is an empirical value but seems to be a good balance for my use-case. If it's too large, it will pause your render, if it's too small, the render happens before the canvas is ready). N.B. my render is occurring in an animated expand row, so this is significant. If your canvas dimension is static you shouldn't encounter this problem.
This approach changes the animation somewhat -- i.e. it's only governed by the 1st arg in onEngineStop() zoomToFit, and cooldownTime is set to 0 to disable initial animation -- but it is consistent - i.e. I never get the 'bunched-up-in-the-corner' render (at least not on the browsers/hw I tested! :-) As this is the weird and wonderful world of javascript, your mileage may vary! ¯\(ツ)/¯
I got this to almost work using a ref then calling zoomToFit() in onEngineStop() as above...
But this causes a problem... The problem is that calling zoomToFit() (or anything that changes the layout points) after the engine stop messes up the tooltip hovers (the hover coords are no longer in the right place).
I believe this is because the mouseover events are bound during the underlying drawGraph and if that changes (which it does in this scenario), the mouseover coordinates are no longer in the right place.
The effect of this is that if I DON'T use zoomToFit() hovers work, but the graph zoom is completely wrong; if I DO use zoomToFit() the graph is correct but the hovers are hosed.
I wonder if there's a way to get the graph to recalculate mouseover coordinates after a zoom operation?
@00155837364765867 could you make a simple repro example of this on https://codesandbox.io/ ? I don't recall coming across this particular issue.