@@ -132,14 +132,47 @@ pub fn sugiyama_layout(
132132
133133 // Collect results from all subgraphs, translating indices back to node IDs.
134134 // For horizontal layout, swap x/y so layers run left-to-right.
135+ // Each subgraph is offset along the perpendicular axis so disconnected
136+ // components (and isolated nodes) don't overlap.
137+ let spacing = if config. vertex_spacing > 0.0 {
138+ config. vertex_spacing
139+ } else {
140+ 10.0
141+ } ;
135142 let mut results = Vec :: with_capacity ( idx_to_id. len ( ) ) ;
143+ let mut perpendicular_offset = 0.0_f64 ;
144+
136145 for ( layout, _width, _height) in & subgraphs {
146+ // Compute bounding box of this subgraph in output coordinates
147+ let mut min_perp = f64:: INFINITY ;
148+ let mut max_perp = f64:: NEG_INFINITY ;
149+
150+ let start = results. len ( ) ;
137151 for & ( idx, ( x, y) ) in layout {
138152 if let Some ( & node_id) = idx_to_id. get ( idx) {
139153 let ( px, py) = if horizontal { ( y, x) } else { ( x, y) } ;
140154 results. push ( NodePosition { id : node_id, x : px, y : py } ) ;
155+
156+ // Perpendicular axis is y for both directions
157+ // Look up node height to get the full extent
158+ let node_h = id_to_idx
159+ . get ( & node_id)
160+ . and_then ( |& i| vertices. get ( i as usize ) )
161+ . map ( |& ( _, ( w, h) ) | if horizontal { w } else { h } )
162+ . unwrap_or ( 0.0 ) ;
163+ min_perp = min_perp. min ( py) ;
164+ max_perp = max_perp. max ( py + node_h) ;
141165 }
142166 }
167+
168+ if start < results. len ( ) {
169+ // Shift this subgraph so its top edge sits at the running offset
170+ let shift = perpendicular_offset - min_perp;
171+ for pos in & mut results[ start..] {
172+ pos. y += shift;
173+ }
174+ perpendicular_offset += max_perp - min_perp + spacing;
175+ }
143176 }
144177
145178 results
@@ -363,6 +396,31 @@ mod tests {
363396 assert ! ( pos[ & 3 ] . 1 < pos[ & 4 ] . 1 ) ;
364397 }
365398
399+ #[ test]
400+ fn test_isolated_nodes_do_not_overlap ( ) {
401+ // Three isolated nodes (no edges) — should not all land at the same position
402+ let sizes = vec ! [
403+ ( 1 , ( 100.0 , 50.0 ) ) ,
404+ ( 2 , ( 100.0 , 50.0 ) ) ,
405+ ( 3 , ( 100.0 , 50.0 ) ) ,
406+ ] ;
407+ let result = sugiyama_layout ( & [ ] , & sizes, & SugiyamaConfig :: default ( ) ) ;
408+ assert_eq ! ( result. len( ) , 3 ) ;
409+
410+ let pos = pos_map ( result) ;
411+ // All three should have distinct y values (stacked along perpendicular axis)
412+ let ys: Vec < f64 > = pos. values ( ) . map ( |p| p. 1 ) . collect ( ) ;
413+ for i in 0 ..ys. len ( ) {
414+ for j in ( i + 1 ) ..ys. len ( ) {
415+ assert ! (
416+ ( ys[ i] - ys[ j] ) . abs( ) > 1.0 ,
417+ "isolated nodes should not overlap: y[{}]={}, y[{}]={}" ,
418+ i, ys[ i] , j, ys[ j]
419+ ) ;
420+ }
421+ }
422+ }
423+
366424 #[ test]
367425 fn test_cycle_does_not_panic ( ) {
368426 // rust-sugiyama handles cycles internally; verify we don't break
0 commit comments