react-force-graph icon indicating copy to clipboard operation
react-force-graph copied to clipboard

Initial zoomToFit problem

Open TylerShin opened this issue 5 years ago • 6 comments

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.

  1. 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.)
  2. 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 bug1 ^ when cooldownTicks are not enough

proper1 ^ 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,
        })
      }
    />

TylerShin avatar Oct 19 '20 08:10 TylerShin

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 avatar Jan 03 '21 13:01 pojntfx

@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 avatar Jan 03 '21 22:01 vasturiano

@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?

TylerShin avatar Jan 04 '21 04:01 TylerShin

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! ¯\(ツ)

00155837364765867 avatar Feb 27 '23 12:02 00155837364765867

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 avatar Apr 09 '23 21:04 00155837364765867

@00155837364765867 could you make a simple repro example of this on https://codesandbox.io/ ? I don't recall coming across this particular issue.

vasturiano avatar Apr 15 '23 22:04 vasturiano