无法使用 d3 force 和 Svelte 更新节点位置

Unable to update nodes position using d3 force and Svelte

我正在尝试使用 D3 force 和 Svelte 创建一个简单的网络。

网络位置应取决于使用 bind:clientWidthbind:clientHeight 计算的容器尺寸。 我的意思是如果网络容器有width=200,height=300,那么网络中心应该是x=100,y=150。

创建图形的代码是这样的:

  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={`[=10=] [=10=] ${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>

很简单:widthheight是道具。 它创建了一些本地商店并用新数据更新了它们。 由于 widthheight 是动态的,我计算了反应块中的 forceCenter 力。

然后,为了绘制节点,我使用 Node 组件和道具 nodesxy。我知道我只能使用 nodes 或只能使用 x,y 但这是一个测试。问题是即使 widthheight 发生变化,节点位置也永远不会改变。

因此,如果您更改 window 大小,图表不会重新计算,但应该重新计算。为什么?

HERE a complete working example

谢谢!

其中一个问题是您替换了 nodes/links 引用。这意味着模拟发生在您不再有任何参考的对象上,而您渲染一组不同的对象,在第一次滴答之后永远不会再改变。

一种方法是添加一个单独的 object/s,用于更新 Svelte 生成的 DOM。

例如

  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)),
    };
  }

调整 each 循环。您还需要使链接循环键控,或调整 Link 组件代码以使 sourceNode/targetNode 响应而不是 const:

{#each render.links as link (link)}
...
{#each render.nodes as node}

(使用 link 本身作为键会导致所有元素的 re-render 因为链接是克隆的,所以 none 的对象是相同的。)

此外,您可能需要在中心更改时调用 restart 以确保它正确应用:

  $: {
    simulation
      .force("center")
      .x(width / 2)
      .y(height / 2);
    simulation.restart();
  }

除了使用单独的对象进行渲染,您还可以使用 {#key} 功能来制作 DOM re-render(对于大图,这可能会产生负面影响)。您只需要一些变量来更改并将其用作触发器:

  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}