use petgraph::graph::{DiGraph, NodeIndex};
use petgraph::visit::EdgeRef;
use std::collections::HashMap;
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct JobNode {
pub id: i64,
pub name: String,
pub status: Option<String>,
}
pub struct DagLayout {
pub graph: DiGraph<JobNode, ()>,
pub positions: HashMap<NodeIndex, (f64, f64)>,
pub width: f64,
pub height: f64,
}
impl DagLayout {
pub fn new() -> Self {
Self {
graph: DiGraph::new(),
positions: HashMap::new(),
width: 0.0,
height: 0.0,
}
}
pub fn add_node(&mut self, job: JobNode) -> NodeIndex {
self.graph.add_node(job)
}
pub fn add_edge(&mut self, from: NodeIndex, to: NodeIndex) {
self.graph.add_edge(from, to, ());
}
pub fn compute_layout(&mut self) {
let layers = self.compute_layers();
self.assign_positions(&layers);
}
fn compute_layers(&self) -> Vec<Vec<NodeIndex>> {
use petgraph::visit::Topo;
let mut layers: Vec<Vec<NodeIndex>> = Vec::new();
let mut node_layer: HashMap<NodeIndex, usize> = HashMap::new();
let mut topo = Topo::new(&self.graph);
while let Some(node) = topo.next(&self.graph) {
let mut max_predecessor_layer = 0;
for edge in self
.graph
.edges_directed(node, petgraph::Direction::Incoming)
{
if let Some(&layer) = node_layer.get(&edge.source()) {
max_predecessor_layer = max_predecessor_layer.max(layer + 1);
}
}
node_layer.insert(node, max_predecessor_layer);
while layers.len() <= max_predecessor_layer {
layers.push(Vec::new());
}
layers[max_predecessor_layer].push(node);
}
for layer in &mut layers {
layer.sort_by(|a, b| {
let a_preds: Vec<usize> = self
.graph
.edges_directed(*a, petgraph::Direction::Incoming)
.map(|e| e.source().index())
.collect();
let b_preds: Vec<usize> = self
.graph
.edges_directed(*b, petgraph::Direction::Incoming)
.map(|e| e.source().index())
.collect();
match (a_preds.first(), b_preds.first()) {
(Some(a_pred), Some(b_pred)) => a_pred.cmp(b_pred).then_with(|| {
let a_name = &self.graph[*a].name;
let b_name = &self.graph[*b].name;
a_name.cmp(b_name)
}),
(Some(_), None) => std::cmp::Ordering::Less,
(None, Some(_)) => std::cmp::Ordering::Greater,
(None, None) => {
let a_name = &self.graph[*a].name;
let b_name = &self.graph[*b].name;
a_name.cmp(b_name)
}
}
});
}
layers
}
fn assign_positions(&mut self, layers: &[Vec<NodeIndex>]) {
let layer_height = 8.0; let node_width = 3.0;
self.height = layers.len() as f64 * layer_height;
for (layer_idx, layer_nodes) in layers.iter().enumerate() {
let layer_width = layer_nodes.len() as f64 * node_width;
self.width = self.width.max(layer_width);
let y = layer_idx as f64 * layer_height;
for (node_idx, &node) in layer_nodes.iter().enumerate() {
let x = node_idx as f64 * node_width;
self.positions.insert(node, (x, y));
}
}
}
#[allow(dead_code)]
pub fn get_normalized_position(&self, node: NodeIndex) -> Option<(f64, f64)> {
self.positions.get(&node).map(|&(x, y)| {
let norm_x = if self.width > 0.0 {
x / self.width
} else {
0.5
};
let norm_y = if self.height > 0.0 {
y / self.height
} else {
0.5
};
(norm_x, norm_y)
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_simple_dag() {
let mut dag = DagLayout::new();
let n1 = dag.add_node(JobNode {
id: 1,
name: "Job 1".to_string(),
status: Some("completed".to_string()),
});
let n2 = dag.add_node(JobNode {
id: 2,
name: "Job 2".to_string(),
status: Some("running".to_string()),
});
let n3 = dag.add_node(JobNode {
id: 3,
name: "Job 3".to_string(),
status: Some("pending".to_string()),
});
dag.add_edge(n1, n2);
dag.add_edge(n2, n3);
dag.compute_layout();
assert!(dag.positions.contains_key(&n1));
assert!(dag.positions.contains_key(&n2));
assert!(dag.positions.contains_key(&n3));
}
}