diff --git a/web/src/lib/components/EntityGraph.svelte b/web/src/lib/components/EntityGraph.svelte index 028987e..831bad0 100644 --- a/web/src/lib/components/EntityGraph.svelte +++ b/web/src/lib/components/EntityGraph.svelte @@ -120,7 +120,37 @@ return typeCategory.get(type) === category } - async function load() { + // Brand-new nodes (no `prev`) get x/y left undefined, and d3-force's + // default init spreads those via a spiral centered on the ORIGIN — not + // (width/2, height/2) — while the x/y centering forces below are + // deliberately weak (0.04, so they don't fight the link/collide layout). + // Together that meant the cluster could settle noticeably off-origin + // instead of centered. Fixed by explicitly fitting the viewport to the + // node bounding box once the simulation settles, rather than relying on + // the force balance to land on center by itself. + function fitToView() { + const placed = nodes.filter((n) => n.x != null && n.y != null) + if (!placed.length) return + const xs = placed.map((n) => n.x as number) + const ys = placed.map((n) => n.y as number) + const minX = Math.min(...xs) + const maxX = Math.max(...xs) + const minY = Math.min(...ys) + const maxY = Math.max(...ys) + const pad = 70 + const bw = Math.max(maxX - minX, 1) + const bh = Math.max(maxY - minY, 1) + const k = Math.min((width - pad * 2) / bw, (height - pad * 2) / bh, 2.5) + const cx = (minX + maxX) / 2 + const cy = (minY + maxY) / 2 + view = { k, x: width / 2 - cx * k, y: height / 2 - cy * k } + } + + // fit=false for passive background reloads (live entity/relationship + // events) — those shouldn't yank the view out from under someone + // actively panning/zooming. Fresh loads (mount, root/depth change, + // reset, re-root) default to fit=true. + async function load(fit = true) { loading = true graph = await fetchGraph({ root: root || undefined, depth, includeStatus: true }) loading = false @@ -163,6 +193,9 @@ .on('tick', () => { nodes = [...nodes] }) + .on('end', () => { + if (fit) fitToView() + }) } onMount(() => { @@ -191,7 +224,7 @@ const ev = $liveEvents[0] if (!ev) return if (ev.type.startsWith('entity.') || ev.type.startsWith('relationship.') || ev.type === 'health.changed') { - load() + load(false) } })