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