Skip to content
Advertisement

Unable to update nodes position using d3 force and Svelte

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}
Advertisement