dlin_core/render/
layout.rs1use petgraph::Direction;
2use petgraph::stable_graph::NodeIndex;
3use petgraph::visit::EdgeRef;
4use std::collections::HashMap;
5
6use crate::graph::types::LineageGraph;
7
8#[derive(Debug, Clone)]
10pub struct LayoutResult {
11 pub positions: HashMap<NodeIndex, (usize, usize)>,
13 pub num_layers: usize,
15 pub max_layer_width: usize,
17 pub layers: Vec<Vec<NodeIndex>>,
19}
20
21pub fn sugiyama_layout(graph: &LineageGraph) -> LayoutResult {
23 if graph.node_count() == 0 {
24 return LayoutResult {
25 positions: HashMap::new(),
26 num_layers: 0,
27 max_layer_width: 0,
28 layers: Vec::new(),
29 };
30 }
31
32 let layers = assign_layers(graph);
34
35 let ordered_layers = reduce_crossings(graph, &layers);
37
38 let mut positions = HashMap::new();
40 let mut max_width = 0;
41
42 for (layer_idx, layer) in ordered_layers.iter().enumerate() {
43 max_width = max_width.max(layer.len());
44 for (pos, &node) in layer.iter().enumerate() {
45 positions.insert(node, (layer_idx, pos));
46 }
47 }
48
49 LayoutResult {
50 positions,
51 num_layers: ordered_layers.len(),
52 max_layer_width: max_width,
53 layers: ordered_layers,
54 }
55}
56
57fn assign_layers(graph: &LineageGraph) -> Vec<Vec<NodeIndex>> {
59 let mut layer_of: HashMap<NodeIndex, usize> = HashMap::new();
60
61 if let Ok(topo) = petgraph::algo::toposort(graph, None) {
63 for node in &topo {
64 let predecessors: Vec<usize> = graph
65 .edges_directed(*node, Direction::Incoming)
66 .filter_map(|e| layer_of.get(&e.source()).copied())
67 .collect();
68
69 let layer = if predecessors.is_empty() {
70 0
71 } else {
72 predecessors.iter().max().unwrap() + 1
73 };
74 layer_of.insert(*node, layer);
75 }
76 } else {
77 for (i, node) in graph.node_indices().enumerate() {
79 layer_of.insert(node, i);
80 }
81 }
82
83 let max_layer = layer_of.values().copied().max().unwrap_or(0);
85 let mut layers: Vec<Vec<NodeIndex>> = vec![Vec::new(); max_layer + 1];
86 for (node, layer) in &layer_of {
87 layers[*layer].push(*node);
88 }
89
90 layers.retain(|l| !l.is_empty());
92
93 layers
94}
95
96fn reduce_crossings(
98 graph: &LineageGraph,
99 initial_layers: &[Vec<NodeIndex>],
100) -> Vec<Vec<NodeIndex>> {
101 let mut layers = initial_layers.to_vec();
102
103 for _ in 0..3 {
105 for i in 1..layers.len() {
107 let prev_layer = layers[i - 1].clone();
108 sort_by_barycenter(graph, &mut layers[i], &prev_layer, Direction::Incoming);
109 }
110
111 for i in (0..layers.len().saturating_sub(1)).rev() {
113 let next_layer = layers[i + 1].clone();
114 sort_by_barycenter(graph, &mut layers[i], &next_layer, Direction::Outgoing);
115 }
116 }
117
118 layers
119}
120
121fn sort_by_barycenter(
123 graph: &LineageGraph,
124 layer: &mut [NodeIndex],
125 adjacent: &[NodeIndex],
126 direction: Direction,
127) {
128 let adj_positions: HashMap<NodeIndex, usize> =
129 adjacent.iter().enumerate().map(|(i, &n)| (n, i)).collect();
130
131 let mut barycenters: HashMap<NodeIndex, f64> = HashMap::new();
132
133 for &node in layer.iter() {
134 let neighbors: Vec<usize> = graph
135 .edges_directed(node, direction)
136 .filter_map(|e| {
137 let other = match direction {
138 Direction::Incoming => e.source(),
139 Direction::Outgoing => e.target(),
140 };
141 adj_positions.get(&other).copied()
142 })
143 .collect();
144
145 let bc = if neighbors.is_empty() {
146 f64::MAX } else {
148 neighbors.iter().sum::<usize>() as f64 / neighbors.len() as f64
149 };
150 barycenters.insert(node, bc);
151 }
152
153 layer.sort_by(|a, b| {
154 let ba = barycenters.get(a).unwrap_or(&f64::MAX);
155 let bb = barycenters.get(b).unwrap_or(&f64::MAX);
156 ba.partial_cmp(bb).unwrap_or(std::cmp::Ordering::Equal)
157 });
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163 use crate::graph::types::*;
164
165 #[test]
166 fn test_empty_graph() {
167 let g = LineageGraph::new();
168 let layout = sugiyama_layout(&g);
169 assert_eq!(layout.num_layers, 0);
170 }
171
172 use crate::render::test_helpers::make_node;
173
174 #[test]
175 fn test_disconnected_node_in_layer() {
176 let mut g = LineageGraph::new();
179 let a = g.add_node(make_node("a", "a", NodeType::Source));
180 let b = g.add_node(make_node("b", "b", NodeType::Model));
181 let c = g.add_node(make_node("c", "c", NodeType::Model)); g.add_edge(a, b, EdgeData::direct(EdgeType::Source));
183 let _ = c; let layout = sugiyama_layout(&g);
187 assert!(layout.num_layers >= 1);
189 assert!(layout.positions.contains_key(&a));
190 assert!(layout.positions.contains_key(&b));
191 assert!(layout.positions.contains_key(&c));
192 }
193
194 #[test]
195 fn test_linear_graph() {
196 let mut g = LineageGraph::new();
197 let a = g.add_node(make_node("a", "a", NodeType::Source));
198 let b = g.add_node(make_node("b", "b", NodeType::Model));
199 let c = g.add_node(make_node("c", "c", NodeType::Model));
200 g.add_edge(a, b, EdgeData::direct(EdgeType::Source));
201 g.add_edge(b, c, EdgeData::direct(EdgeType::Ref));
202
203 let layout = sugiyama_layout(&g);
204 assert_eq!(layout.num_layers, 3);
205
206 let (la, _) = layout.positions[&a];
207 let (lb, _) = layout.positions[&b];
208 let (lc, _) = layout.positions[&c];
209 assert!(la < lb);
210 assert!(lb < lc);
211 }
212
213 #[test]
214 fn test_cyclic_graph_fallback() {
215 let mut g = LineageGraph::new();
217 let a = g.add_node(make_node("a", "a", NodeType::Model));
218 let b = g.add_node(make_node("b", "b", NodeType::Model));
219 g.add_edge(a, b, EdgeData::direct(EdgeType::Ref));
221 g.add_edge(b, a, EdgeData::direct(EdgeType::Ref));
222
223 let layout = sugiyama_layout(&g);
224 assert_eq!(layout.positions.len(), 2);
226 assert!(layout.positions.contains_key(&a));
227 assert!(layout.positions.contains_key(&b));
228 }
229}