From 447e2d918340b80263c4e248c7a997d10d6bceea Mon Sep 17 00:00:00 2001 From: Bernhard Jackiewicz Date: Fri, 2 Oct 2026 18:48:34 +0200 Subject: [PATCH] fix(ui): take scoped hierarchy hops from the discovered levels scopedHierarchy derived each node's hop column from its z coordinate, which only the arranged hop rings encode. While a scope loads, its preview is built from the overall layout and keeps the server's global coordinates, so the columns were wrong until the complete answer arrived. The hop now comes from the levels the traversal records (roots at 0); z is only a fallback for callers without levels. Signed-off-by: Bernhard Jackiewicz --- graph-ui/src/galaxy/graph-scope.test.ts | 8 ++++++++ graph-ui/src/galaxy/graph-scope.ts | 7 +++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/graph-ui/src/galaxy/graph-scope.test.ts b/graph-ui/src/galaxy/graph-scope.test.ts index 9780e772c..6fe3448a0 100644 --- a/graph-ui/src/galaxy/graph-scope.test.ts +++ b/graph-ui/src/galaxy/graph-scope.test.ts @@ -215,6 +215,14 @@ it('wraps large hierarchy levels into readable blocks without losing or overlapp } expect(result.data.nodes.every(node => node.color === '#abcabc')).toBe(true); }); +it('takes hop columns from discovered levels, not from the global layout depth of a preview', () => { + // A preview from the overall layout keeps server coordinates; z says nothing about hops. + const nodes = [{ ...node(1), z: -250 }, { ...node(2), z: 40 }, { ...node(3), z: -90 }]; + const levels = new Map([[1, 0], [2, 1], [3, 2]]); + const result = scopedHierarchy({ data: { nodes, edges: [], total_nodes: 3 }, roots: new Set([1]), depth: 2, exhausted: false, levels }, 'preview'); + expect(Object.fromEntries(result.placements.map(placement => [placement.name, placement.hop]))).toEqual({ n1: 0, n2: 1, n3: 2 }); + expect(result.depth).toBe(3); +}); it('retains simple columns for small hierarchy levels', () => { const nodes = [{ ...node(1), z: 0 }, { ...node(2), z: -18 }, { ...node(3), z: -18 }]; const result = scopedHierarchy({ data: { nodes, edges: [], total_nodes: 3 }, roots: new Set([1]), depth: 1, exhausted: false }, 'small'); diff --git a/graph-ui/src/galaxy/graph-scope.ts b/graph-ui/src/galaxy/graph-scope.ts index b4f823eaf..902c3c648 100644 --- a/graph-ui/src/galaxy/graph-scope.ts +++ b/graph-ui/src/galaxy/graph-scope.ts @@ -208,11 +208,14 @@ export function limitGraphRender(data: GraphData, nodeLimit: number, edgeLimit: /** The same evidence in successive hop blocks. Small hops keep one column; * large hops wrap into compact grids so fitting thousands of neighbors does - * not turn the entire scene into a single vertical line. */ + * not turn the entire scene into a single vertical line. + * + * Hops come from the discovered levels. Only arranged rings encode them in z; + * a preview built from the overall layout keeps its global coordinates. */ export function scopedHierarchy(scope: ScopedGraph, name: string): import('./hierarchy-layout').HierarchyProjection { const columns = new Map(); for (const node of scope.data.nodes) { - const hop = Math.max(0, Math.round(-node.z / 18)); + const hop = scope.roots.has(node.id) ? 0 : scope.levels?.get(node.id) ?? Math.max(0, Math.round(-node.z / 18)); const entries = columns.get(hop) ?? []; entries.push(node); columns.set(hop, entries); } const nodes: GraphNode[] = [], remap = new Map();