Estoy tratando de crear una red simple usando D3 force y Svelte.
La posición de la red debe depender de las dimensiones del contenedor calculadas mediante bind:clientWidth y bind:clientHeight . Quiero decir que si el contenedor de la red tiene ancho = 200, alto = 300, entonces el centro de la red debe ser x = 100, y = 150.
El código que crea el gráfico es este:
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> Es muy simple: el width y la height son accesorios. Creó algunas tiendas locales y las actualizó con nuevos datos. Dado que el width y la height son dinámicos, forceCenter la fuerza del centro de fuerza en un bloque reactivo.
Luego, para dibujar los nodos, uso un componente Node con prop nodes , x , y . Sé que puedo usar solo nodes o solo x,y pero fue una prueba. El problema es que la posición de los nodos nunca cambia, incluso si cambian el width y la height .
Entonces, si cambia el tamaño de la ventana, el gráfico no se vuelve a calcular, pero debería hacerlo. ¿Por qué?
AQUÍ un ejemplo de trabajo completo
¡Gracias!
Uno de los problemas es que reemplaza las referencias de nodes / links . Esto implica que la simulación ocurre en objetos a los que ya no tienes ninguna referencia, mientras renderizas un conjunto diferente de objetos, que después del primer tic nunca volverá a cambiar.
Un enfoque sería agregar objetos separados que se usan para actualizar el DOM generado por Svelte.
p.ej
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)), }; } Ajuste each de los bucles. También debe hacer que los enlaces tengan clave de bucle, o ajustar el código del componente de Link para hacer que sourceNode / targetNode reactivo en lugar de const :
{#each render.links as link (link)} ... {#each render.nodes as node} (Usar el link en sí mismo como clave provoca una nueva representación de todos los elementos porque los enlaces se clonan, por lo que ninguno de los objetos es idéntico).
Además, es posible que deba llamar al restart cuando cambie el centro para asegurarse de que se aplique correctamente:
$: { simulation .force("center") .x(width / 2) .y(height / 2); simulation.restart(); } Como alternativa a tener objetos separados para renderizar, puede usar la {#key} para hacer que el DOM se vuelva a renderizar (para gráficos grandes, esto puede tener un impacto negativo). Solo necesita alguna variable para cambiar y usar eso como disparador:
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}