无法使用 d3 force 和 Svelte 更新节点位置
Unable to update nodes position using d3 force and Svelte
我正在尝试使用 D3 force 和 Svelte 创建一个简单的网络。
网络位置应取决于使用 bind:clientWidth
和 bind: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>
很简单:width
和height
是道具。
它创建了一些本地商店并用新数据更新了它们。
由于 width
和 height
是动态的,我计算了反应块中的 forceCenter
力。
然后,为了绘制节点,我使用 Node
组件和道具 nodes
、x
、y
。我知道我只能使用 nodes
或只能使用 x,y
但这是一个测试。问题是即使 width
和 height
发生变化,节点位置也永远不会改变。
因此,如果您更改 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}
我正在尝试使用 D3 force 和 Svelte 创建一个简单的网络。
网络位置应取决于使用 bind:clientWidth
和 bind: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>
很简单:width
和height
是道具。
它创建了一些本地商店并用新数据更新了它们。
由于 width
和 height
是动态的,我计算了反应块中的 forceCenter
力。
然后,为了绘制节点,我使用 Node
组件和道具 nodes
、x
、y
。我知道我只能使用 nodes
或只能使用 x,y
但这是一个测试。问题是即使 width
和 height
发生变化,节点位置也永远不会改变。
因此,如果您更改 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}