reactjsd3.jsreact-flow

Update d3's force-directed graph in a React app while preserving old nodes positions


I am using d3's forceSimulation to calculate (x,y) coordinates for each node given the forces (starting without nodes or links):

const simulation = d3.forceSimulation()
      .force('charge', d3.forceManyBody().strength(-1000))
      .force('link', d3.forceLink().id(d => d?.id).distance(200))
      .force('collide', d3.forceCollide().radius(150))
      .stop()
      .tick(300)

and then listen to simulation's 'tick' event to update graph (utilising react-flow-renderer setNodes and setSetEdges see the docs):

simulation.on('tick', () => {
    setEdges([...simulation.force('link').links()].map(setEdge));
    setNodes([...simulation.nodes()].map(setNode));
})

React part is supposed to restart the simulation while applying actual nodes and edges to the simulation:

useEffect(() => {
    simulation.nodes(nodes);
    simulation.force('link').links(edges);
    simulation.alpha(0.1).restart();

    return () => simulation.stop();
}, [nodes, edges])

Now, the nodes and edges might get updated as there is a possibility to extend node's relations. The thing is - every time we extend a node relationships, we get a new array of nodes and edges, each containing old values and the new ones:

Dummy example:

old nodes : [{ id: 1 }]

new nodes: [{ id: 1 }, { id: 2}, { id: 3}]

The 'simulation' restarts with new values and recalculates the graph starting from scratch so the new graph is nothing like old one. Is there an option so the graph would keep old nodes' (x,y) coordinates and simply 'add' new ones to existing graph?


Solution

  • In case anyone faces this kind of problem:

    Keep a record of previous nodes before you update the nodes' array:

    const prev = usePrev(new Map(nodes.map((node) => [node.id, node])));
    

    and then, right before passing the new nodes to simulation, make a new copy, merging previously existing elements like so:

    useEffect(() => {
        nodes = nodes.map((node) => Object.assign(node, prev?.get(node.id)));
    
        simulation.nodes(nodes);
        (...) // rest of the code
    

    This way when the simulation starts with new nodes, some of them might already have (x,y) coordinates set.