11import ELK from "elkjs/lib/elk.bundled.js" ;
22import type { Edge , Node } from "@xyflow/react" ;
33import type { GraphEdgeData , GraphPort , RuntimeGraphNodeData } from "./types" ;
4+ import { nodeWidth } from "./nodeSizing" ;
45
56const elk = new ELK ( ) ;
6- const NODE_WIDTH = 312 ;
7+ export type LayoutMode = "data_flow" | "compact" | "scale_grouped" | "call_stack" ;
78
8- export async function layoutGraph ( nodes : Node < RuntimeGraphNodeData > [ ] , edges : Edge < GraphEdgeData > [ ] ) {
9+ export async function layoutGraph ( nodes : Node < RuntimeGraphNodeData > [ ] , edges : Edge < GraphEdgeData > [ ] , mode : LayoutMode = "data_flow" ) {
10+ const layoutEdges = mode === "call_stack" ? edges . filter ( ( edge ) => isCallEdge ( edge . data ) ) : edges ;
911 const graph = {
1012 id : "root" ,
11- layoutOptions : {
12- "elk.algorithm" : "layered" ,
13- "elk.direction" : "RIGHT" ,
14- "elk.spacing.nodeNode" : "58" ,
15- "elk.layered.spacing.nodeNodeBetweenLayers" : "110" ,
16- "elk.layered.nodePlacement.strategy" : "BRANDES_KOEPF" ,
17- "elk.layered.crossingMinimization.semiInteractive" : "true" ,
18- "elk.edgeRouting" : "ORTHOGONAL" ,
19- } ,
13+ layoutOptions : layoutOptions ( mode ) ,
2014 children : nodes . map ( ( node ) => ( {
2115 id : node . id ,
22- width : NODE_WIDTH ,
16+ width : nodeWidth ( node . data ) ,
2317 height : nodeHeight ( node . data ) ,
2418 ports : [
2519 elkCallPort ( node . id , "target" ) ,
@@ -31,7 +25,7 @@ export async function layoutGraph(nodes: Node<RuntimeGraphNodeData>[], edges: Ed
3125 "org.eclipse.elk.portConstraints" : "FIXED_ORDER" ,
3226 } ,
3327 } ) ) ,
34- edges : edges . map ( ( edge ) => ( {
28+ edges : layoutEdges . map ( ( edge ) => ( {
3529 id : edge . id ,
3630 sources : [ edge . sourceHandle ?? edge . source ] ,
3731 targets : [ edge . targetHandle ?? edge . target ] ,
@@ -40,11 +34,62 @@ export async function layoutGraph(nodes: Node<RuntimeGraphNodeData>[], edges: Ed
4034
4135 const result = await elk . layout ( graph ) ;
4236 const positions = new Map ( ( result . children ?? [ ] ) . map ( ( child ) => [ child . id , { x : child . x ?? 0 , y : child . y ?? 0 } ] ) ) ;
37+ const scaleOffsets = mode === "scale_grouped" ? scaleBandOffsets ( nodes ) : new Map < string , number > ( ) ;
38+
39+ return nodes . map ( ( node ) => {
40+ const position = positions . get ( node . id ) ?? node . position ;
41+ return {
42+ ...node ,
43+ position : {
44+ x : position . x ,
45+ y : position . y + ( scaleOffsets . get ( node . data . scale ) ?? 0 ) ,
46+ } ,
47+ } ;
48+ } ) ;
49+ }
50+
51+ function layoutOptions ( mode : LayoutMode ) : Record < string , string > {
52+ if ( mode === "compact" ) {
53+ return {
54+ "elk.algorithm" : "layered" ,
55+ "elk.direction" : "RIGHT" ,
56+ "elk.spacing.nodeNode" : "28" ,
57+ "elk.layered.spacing.nodeNodeBetweenLayers" : "52" ,
58+ "elk.layered.nodePlacement.strategy" : "BRANDES_KOEPF" ,
59+ "elk.layered.crossingMinimization.semiInteractive" : "true" ,
60+ "elk.edgeRouting" : "ORTHOGONAL" ,
61+ } ;
62+ }
63+
64+ if ( mode === "call_stack" ) {
65+ return {
66+ "elk.algorithm" : "layered" ,
67+ "elk.direction" : "DOWN" ,
68+ "elk.spacing.nodeNode" : "46" ,
69+ "elk.layered.spacing.nodeNodeBetweenLayers" : "76" ,
70+ "elk.layered.nodePlacement.strategy" : "NETWORK_SIMPLEX" ,
71+ "elk.edgeRouting" : "ORTHOGONAL" ,
72+ } ;
73+ }
74+
75+ return {
76+ "elk.algorithm" : "layered" ,
77+ "elk.direction" : "RIGHT" ,
78+ "elk.spacing.nodeNode" : mode === "scale_grouped" ? "72" : "58" ,
79+ "elk.layered.spacing.nodeNodeBetweenLayers" : mode === "scale_grouped" ? "130" : "110" ,
80+ "elk.layered.nodePlacement.strategy" : "BRANDES_KOEPF" ,
81+ "elk.layered.crossingMinimization.semiInteractive" : "true" ,
82+ "elk.edgeRouting" : "ORTHOGONAL" ,
83+ } ;
84+ }
85+
86+ function scaleBandOffsets ( nodes : Node < RuntimeGraphNodeData > [ ] ) {
87+ const scales = [ ...new Set ( nodes . map ( ( node ) => node . data . scale ) ) ] . sort ( ) ;
88+ return new Map ( scales . map ( ( scale , index ) => [ scale , index * 260 ] ) ) ;
89+ }
4390
44- return nodes . map ( ( node ) => ( {
45- ...node ,
46- position : positions . get ( node . id ) ?? node . position ,
47- } ) ) ;
91+ function isCallEdge ( edge ?: GraphEdgeData ) {
92+ return edge ?. kind === "hard_dependency" && ! edge . sourcePort && ! edge . targetPort ;
4893}
4994
5095function nodeHeight ( node : RuntimeGraphNodeData ) {
0 commit comments