use petgraph::stable_graph::NodeIndex;
use petgraph::visit::EdgeRef;
use petgraph::Direction;
use std::collections::HashMap;
use crate::graph::types::LineageGraph;
#[derive(Debug, Clone)]
pub struct LayoutResult {
pub positions: HashMap<NodeIndex, (usize, usize)>,
pub num_layers: usize,
pub max_layer_width: usize,
pub layers: Vec<Vec<NodeIndex>>,
}
pub fn sugiyama_layout(graph: &LineageGraph) -> LayoutResult {
if graph.node_count() == 0 {
return LayoutResult {
positions: HashMap::new(),
num_layers: 0,
max_layer_width: 0,
layers: Vec::new(),
};
}
let layers = assign_layers(graph);
let ordered_layers = reduce_crossings(graph, &layers);
let mut positions = HashMap::new();
let mut max_width = 0;
for (layer_idx, layer) in ordered_layers.iter().enumerate() {
max_width = max_width.max(layer.len());
for (pos, &node) in layer.iter().enumerate() {
positions.insert(node, (layer_idx, pos));
}
}
LayoutResult {
positions,
num_layers: ordered_layers.len(),
max_layer_width: max_width,
layers: ordered_layers,
}
}
fn assign_layers(graph: &LineageGraph) -> Vec<Vec<NodeIndex>> {
let mut layer_of: HashMap<NodeIndex, usize> = HashMap::new();
if let Ok(topo) = petgraph::algo::toposort(graph, None) {
for node in &topo {
let predecessors: Vec<usize> = graph
.edges_directed(*node, Direction::Incoming)
.filter_map(|e| layer_of.get(&e.source()).copied())
.collect();
let layer = if predecessors.is_empty() {
0
} else {
predecessors.iter().max().unwrap() + 1
};
layer_of.insert(*node, layer);
}
} else {
for (i, node) in graph.node_indices().enumerate() {
layer_of.insert(node, i);
}
}
let max_layer = layer_of.values().copied().max().unwrap_or(0);
let mut layers: Vec<Vec<NodeIndex>> = vec![Vec::new(); max_layer + 1];
for (node, layer) in &layer_of {
layers[*layer].push(*node);
}
layers.retain(|l| !l.is_empty());
layers
}
fn reduce_crossings(
graph: &LineageGraph,
initial_layers: &[Vec<NodeIndex>],
) -> Vec<Vec<NodeIndex>> {
let mut layers = initial_layers.to_vec();
for _ in 0..3 {
for i in 1..layers.len() {
let prev_layer = layers[i - 1].clone();
sort_by_barycenter(graph, &mut layers[i], &prev_layer, Direction::Incoming);
}
for i in (0..layers.len().saturating_sub(1)).rev() {
let next_layer = layers[i + 1].clone();
sort_by_barycenter(graph, &mut layers[i], &next_layer, Direction::Outgoing);
}
}
layers
}
fn sort_by_barycenter(
graph: &LineageGraph,
layer: &mut [NodeIndex],
adjacent: &[NodeIndex],
direction: Direction,
) {
let adj_positions: HashMap<NodeIndex, usize> =
adjacent.iter().enumerate().map(|(i, &n)| (n, i)).collect();
let mut barycenters: HashMap<NodeIndex, f64> = HashMap::new();
for &node in layer.iter() {
let neighbors: Vec<usize> = graph
.edges_directed(node, direction)
.filter_map(|e| {
let other = match direction {
Direction::Incoming => e.source(),
Direction::Outgoing => e.target(),
};
adj_positions.get(&other).copied()
})
.collect();
let bc = if neighbors.is_empty() {
f64::MAX } else {
neighbors.iter().sum::<usize>() as f64 / neighbors.len() as f64
};
barycenters.insert(node, bc);
}
layer.sort_by(|a, b| {
let ba = barycenters.get(a).unwrap_or(&f64::MAX);
let bb = barycenters.get(b).unwrap_or(&f64::MAX);
ba.partial_cmp(bb).unwrap_or(std::cmp::Ordering::Equal)
});
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::types::*;
#[test]
fn test_empty_graph() {
let g = LineageGraph::new();
let layout = sugiyama_layout(&g);
assert_eq!(layout.num_layers, 0);
}
#[test]
fn test_linear_graph() {
let mut g = LineageGraph::new();
let a = g.add_node(NodeData {
unique_id: "a".into(),
label: "a".into(),
node_type: NodeType::Source,
file_path: None,
description: None,
});
let b = g.add_node(NodeData {
unique_id: "b".into(),
label: "b".into(),
node_type: NodeType::Model,
file_path: None,
description: None,
});
let c = g.add_node(NodeData {
unique_id: "c".into(),
label: "c".into(),
node_type: NodeType::Model,
file_path: None,
description: None,
});
g.add_edge(
a,
b,
EdgeData {
edge_type: EdgeType::Source,
},
);
g.add_edge(
b,
c,
EdgeData {
edge_type: EdgeType::Ref,
},
);
let layout = sugiyama_layout(&g);
assert_eq!(layout.num_layers, 3);
let (la, _) = layout.positions[&a];
let (lb, _) = layout.positions[&b];
let (lc, _) = layout.positions[&c];
assert!(la < lb);
assert!(lb < lc);
}
}