@@ -2,105 +2,41 @@ import dagre from '@dagrejs/dagre';
22
33// Node dimensions - must match actual rendered sizes
44const NODE_WIDTH = 200 ;
5- const NODE_HEIGHT = 100 ;
5+ const NODE_HEIGHT = 120 ;
66const RESUME_WIDTH = 140 ;
7- const RESUME_HEIGHT = 70 ;
7+ const RESUME_HEIGHT = 80 ;
8+
9+ // Spacing - generous to avoid overlap
10+ const HORIZONTAL_SPACING = 60 ; // Space between sibling nodes
11+ const VERTICAL_SPACING = 120 ; // Space between levels (ranks)
812
913/**
10- * Applies Dagre graph layout algorithm to React Flow nodes and edges
11- * Handles disconnected components by laying them out in a grid
14+ * Applies a clean top-down tree layout
15+ * Resume at top, primary jobs below, secondary jobs below those
1216 * @param {Array } nodes - React Flow nodes
1317 * @param {Array } edges - React Flow edges
14- * @param {string } direction - Layout direction ('TB', 'LR', etc. )
18+ * @param {string } direction - Layout direction ('TB' = top-bottom )
1519 * @returns {Object } Layouted nodes and edges
1620 */
1721export const getLayoutedElements = ( nodes , edges , direction = 'TB' ) => {
1822 if ( nodes . length === 0 ) return { nodes : [ ] , edges } ;
1923
20- // Find connected components
21- const components = findConnectedComponents ( nodes , edges ) ;
22-
23- // Layout each component separately
24- const layoutedComponents = components . map ( ( component ) =>
25- layoutComponent ( component . nodes , component . edges , direction )
26- ) ;
27-
28- // Arrange components in a compact grid layout
29- const finalNodes = arrangeComponents ( layoutedComponents ) ;
30-
31- return { nodes : finalNodes , edges } ;
32- } ;
33-
34- /**
35- * Find connected components in the graph
36- */
37- function findConnectedComponents ( nodes , edges ) {
38- const visited = new Set ( ) ;
39- const components = [ ] ;
40-
41- // Build adjacency list (undirected)
42- const adj = new Map ( ) ;
43- nodes . forEach ( ( n ) => adj . set ( n . id , new Set ( ) ) ) ;
44- edges . forEach ( ( e ) => {
45- if ( adj . has ( e . source ) && adj . has ( e . target ) ) {
46- adj . get ( e . source ) . add ( e . target ) ;
47- adj . get ( e . target ) . add ( e . source ) ;
48- }
49- } ) ;
50-
51- // DFS to find components
52- function dfs ( nodeId , component ) {
53- if ( visited . has ( nodeId ) ) return ;
54- visited . add ( nodeId ) ;
55- component . add ( nodeId ) ;
56- for ( const neighbor of adj . get ( nodeId ) || [ ] ) {
57- dfs ( neighbor , component ) ;
58- }
59- }
60-
61- for ( const node of nodes ) {
62- if ( ! visited . has ( node . id ) ) {
63- const component = new Set ( ) ;
64- dfs ( node . id , component ) ;
65- const componentNodes = nodes . filter ( ( n ) => component . has ( n . id ) ) ;
66- const componentEdges = edges . filter (
67- ( e ) => component . has ( e . source ) && component . has ( e . target )
68- ) ;
69- components . push ( { nodes : componentNodes , edges : componentEdges } ) ;
70- }
71- }
72-
73- // Sort components: main tree (with resume) first, then by size
74- components . sort ( ( a , b ) => {
75- const aHasResume = a . nodes . some ( ( n ) => n . data ?. isResume ) ;
76- const bHasResume = b . nodes . some ( ( n ) => n . data ?. isResume ) ;
77- if ( aHasResume && ! bHasResume ) return - 1 ;
78- if ( ! aHasResume && bHasResume ) return 1 ;
79- return b . nodes . length - a . nodes . length ;
80- } ) ;
81-
82- return components ;
83- }
84-
85- /**
86- * Layout a single connected component using dagre
87- */
88- function layoutComponent ( nodes , edges , direction ) {
8924 const dagreGraph = new dagre . graphlib . Graph ( ) ;
9025 dagreGraph . setDefaultEdgeLabel ( ( ) => ( { } ) ) ;
9126
27+ // Configure for a clean top-down tree
9228 dagreGraph . setGraph ( {
9329 rankdir : direction ,
9430 align : 'UL' ,
95- nodesep : 20 , // Horizontal spacing between nodes
96- ranksep : 60 , // Vertical spacing between ranks
97- edgesep : 10 ,
98- marginx : 20 ,
99- marginy : 20 ,
100- acyclicer : 'greedy' ,
101- ranker : 'tight-tree' , // More compact than network-simplex
31+ nodesep : HORIZONTAL_SPACING ,
32+ ranksep : VERTICAL_SPACING ,
33+ edgesep : 20 ,
34+ marginx : 50 ,
35+ marginy : 50 ,
36+ ranker : 'tight-tree' ,
10237 } ) ;
10338
39+ // Add nodes to dagre
10440 nodes . forEach ( ( node ) => {
10541 const isResume = node . data ?. isResume ;
10642 dagreGraph . setNode ( node . id , {
@@ -109,94 +45,29 @@ function layoutComponent(nodes, edges, direction) {
10945 } ) ;
11046 } ) ;
11147
48+ // Add edges to dagre
11249 edges . forEach ( ( edge ) => {
11350 dagreGraph . setEdge ( edge . source , edge . target ) ;
11451 } ) ;
11552
53+ // Run layout
11654 dagre . layout ( dagreGraph ) ;
11755
118- // Calculate bounding box
119- let minX = Infinity ,
120- minY = Infinity ,
121- maxX = - Infinity ,
122- maxY = - Infinity ;
123-
56+ // Apply positions
12457 const layoutedNodes = nodes . map ( ( node ) => {
12558 const pos = dagreGraph . node ( node . id ) ;
12659 const isResume = node . data ?. isResume ;
12760 const width = isResume ? RESUME_WIDTH : NODE_WIDTH ;
12861 const height = isResume ? RESUME_HEIGHT : NODE_HEIGHT ;
12962
130- // Dagre returns center position, convert to top-left
131- const x = pos . x - width / 2 ;
132- const y = pos . y - height / 2 ;
133-
134- minX = Math . min ( minX , x ) ;
135- minY = Math . min ( minY , y ) ;
136- maxX = Math . max ( maxX , x + width ) ;
137- maxY = Math . max ( maxY , y + height ) ;
138-
13963 return {
14064 ...node ,
141- position : { x, y } ,
142- } ;
143- } ) ;
144-
145- return {
146- nodes : layoutedNodes ,
147- width : maxX - minX ,
148- height : maxY - minY ,
149- minX,
150- minY,
151- } ;
152- }
153-
154- /**
155- * Arrange multiple components in a grid to avoid overlap
156- */
157- function arrangeComponents ( components ) {
158- if ( components . length === 0 ) return [ ] ;
159- if ( components . length === 1 ) {
160- // Single component - normalize to origin
161- const comp = components [ 0 ] ;
162- return comp . nodes . map ( ( n ) => ( {
163- ...n ,
16465 position : {
165- x : n . position . x - comp . minX ,
166- y : n . position . y - comp . minY ,
66+ x : pos . x - width / 2 ,
67+ y : pos . y - height / 2 ,
16768 } ,
168- } ) ) ;
169- }
170-
171- const allNodes = [ ] ;
172- let currentX = 0 ;
173- let currentY = 0 ;
174- let rowHeight = 0 ;
175- const maxRowWidth = 2000 ; // Max width before wrapping to next row
176- const gap = 80 ; // Gap between components
177-
178- for ( const comp of components ) {
179- // Check if we need to wrap to next row
180- if ( currentX + comp . width > maxRowWidth && currentX > 0 ) {
181- currentX = 0 ;
182- currentY += rowHeight + gap ;
183- rowHeight = 0 ;
184- }
185-
186- // Add nodes with offset
187- for ( const node of comp . nodes ) {
188- allNodes . push ( {
189- ...node ,
190- position : {
191- x : node . position . x - comp . minX + currentX ,
192- y : node . position . y - comp . minY + currentY ,
193- } ,
194- } ) ;
195- }
196-
197- currentX += comp . width + gap ;
198- rowHeight = Math . max ( rowHeight , comp . height ) ;
199- }
69+ } ;
70+ } ) ;
20071
201- return allNodes ;
202- }
72+ return { nodes : layoutedNodes , edges } ;
73+ } ;
0 commit comments