I’m trying to create a simple network using D3 force and Svelte.
The network position should depend on the container dimensions computed using bind:clientWidth
and bind:clientHeight
.
I mean that if the network container has width=200, height=300, then the network center should be x=100, y=150.
The code that creates the graph is this:
export let width; export let height; let svg; let simulation; let zoomTransform = zoomIdentity; let links = $network.links.map(d => cloneDeep(d)); let nodes = $network.nodes.map(d => cloneDeep(d)); simulation = forceSimulation(nodes) .force( "link", forceLink( links.map(l => { return { ...l, source: nodes.find(n => n.id === l.source), target: nodes.find(n => n.id === l.source) }; }) ) ) .force( "collision", forceCollide() .strength(0.2) .radius(120) .iterations(1) ) .force("charge", forceManyBody().strength(0)) .force("center", forceCenter(width / 2, height / 2)) .on("tick", simulationUpdate); // .stop() // .tick(100) function simulationUpdate() { simulation.tick(); nodes = nodes.map(n => cloneDeep(n)); links = links.map(l => cloneDeep(l)); } $: { simulation .force("center") .x(width / 2) .y(height / 2); } </script> <svg bind:this={svg} viewBox={`${0} ${0} ${width} ${height}`} {width} {height}> {#if simulation} <g> {#each links as link} <Link {link} {nodes} /> {/each} </g> {:else} null {/if} {#if simulation} <g> {#each nodes as node} <Node {node} x={node.x} y={node.y} /> {/each} </g> {:else} null {/if} </svg>
It’s very simple: width
and height
are props.
It create some local stores and updated them with new data.
Since that width
and height
are dynamic, I compute the forceCenter
force in a reactive block.
Then, to draw the nodes, I use a Node
component with prop nodes
, x
, y
. I kwnow that I can use only nodes
or only x,y
but it was a test. The problem is that the nodes position never changes even if width
and height
change.
So if you change the window size, the graph is not recomputed but it should. Why?
HERE a complete working example
Thank you!
Advertisement
Answer
One of the issues is that you replace the nodes
/links
references. This entails that the simulation happens on objects that you no longer have any reference to, while you render a different set of objects, which after the first tick will never change again.
One approach would be to add a separate object/s that are used for updating the DOM generated by Svelte.
e.g.
let links = $network.links.map(d => cloneDeep(d)); let nodes = $network.nodes.map(d => cloneDeep(d)); // Initial render state let render = { links, nodes, } // ... function simulationUpdate() { // (No need to call tick, a tick has already happened here) render = { nodes: nodes.map(d => cloneDeep(d)), links: links.map(d => cloneDeep(d)), }; }
Adjust the each
loops. You also need to make the links loop keyed, or adjust the Link
component code to make sourceNode
/targetNode
reactive instead of const
:
{#each render.links as link (link)} ... {#each render.nodes as node}
(Using the link
itself as key causes a re-render of all elements because the links are cloned, so none of the objects are identical.)
Also, you might need to call restart
when the center changes to make sure it applies correctly:
$: { simulation .force("center") .x(width / 2) .y(height / 2); simulation.restart(); }
Alternatively to having separate objects for rendering, you can use the {#key}
feature to make the DOM re-render (for large graphs this may have a negative impact). You only need some variable to change and use that as trigger:
let renderKey = false; // ... function simulationUpdate() { renderKey = !renderKey; }
{#if simulation} {#key renderKey} <g> {#each links as link} <Link {link} {nodes} /> {/each} </g> <g> {#each nodes as node} <Node {node} x={node.x} y={node.y} /> {/each} </g> {/key} {/if}