diff --git a/src/views/GraphView.ts b/src/views/GraphView.ts index 610f4ef..f0b2a48 100644 --- a/src/views/GraphView.ts +++ b/src/views/GraphView.ts @@ -160,18 +160,7 @@ export class GraphView { }, }, ], - layout: { - name: "concentric", - concentric: (node) => -((node.data("depth") as number) ?? 0), - levelWidth: () => 1, - minNodeSpacing: 40, - // @ts-ignore — spacingFactor exists in concentric layout - spacingFactor: 0.9, - fit: true, - padding: 30, - startAngle: -Math.PI / 2, - clockwise: true, - }, + layout: { name: "preset", fit: true, padding: 30 }, }); // @ts-ignore — added by cytoscape-node-html-label @@ -191,7 +180,6 @@ export class GraphView { private buildElements(data: GraphData, collections: string[]): cytoscape.ElementDefinition[] { const els: cytoscape.ElementDefinition[] = []; - els.push({ data: { id: ROOT_ID, label: this.project, depth: 0 } }); const childrenMap = new Map(); for (const e of data.edges) { @@ -201,6 +189,13 @@ export class GraphView { } const known = new Set(collections); + const positions = this.computePositions(childrenMap, known); + + els.push({ + data: { id: ROOT_ID, label: this.project, depth: 0 }, + position: positions.get(ROOT_ID) ?? { x: 0, y: 0 }, + }); + const visited = new Set(); const walk = (parentName: string | null, parentId: string, depth: number) => { const children = (childrenMap.get(parentName) ?? []).filter((c) => known.has(c)); @@ -213,6 +208,7 @@ export class GraphView { const { w, h } = this.nodeSize(features.length); els.push({ data: { id, name: child, depth, features, w, h }, + position: positions.get(id) ?? { x: 0, y: 0 }, }); els.push({ data: { source: parentId, target: id } }); walk(child, id, depth + 1); @@ -222,6 +218,52 @@ export class GraphView { return els; } + private computePositions( + childrenMap: Map, + known: Set, + ): Map { + const R1 = 220; + const STEP = 200; + const positions = new Map(); + positions.set(ROOT_ID, { x: 0, y: 0 }); + + const direct = (childrenMap.get(null) ?? []).filter((c) => known.has(c)); + const N = Math.max(direct.length, 1); + const sectorPerDirect = (2 * Math.PI) / N; + + const layoutSubtree = ( + parentName: string, + parentAngle: number, + allowedSector: number, + parentRadius: number, + ) => { + const children = (childrenMap.get(parentName) ?? []).filter((c) => known.has(c)); + const k = children.length; + if (k === 0) return; + const newRadius = parentRadius + STEP; + const perChild = allowedSector / k; + for (let i = 0; i < k; i++) { + const angle = parentAngle - allowedSector / 2 + (i + 0.5) * perChild; + positions.set(`n_${children[i]}`, { + x: Math.cos(angle) * newRadius, + y: Math.sin(angle) * newRadius, + }); + layoutSubtree(children[i], angle, perChild, newRadius); + } + }; + + direct.forEach((child, i) => { + const angle = i * sectorPerDirect - Math.PI / 2; + positions.set(`n_${child}`, { + x: Math.cos(angle) * R1, + y: Math.sin(angle) * R1, + }); + layoutSubtree(child, angle, sectorPerDirect, R1); + }); + + return positions; + } + private nodeSize(featureCount: number): { w: number; h: number } { const w = 180; const rows = Math.max(1, Math.ceil(featureCount / 2));