use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet, VecDeque};
#[cfg(feature = "wasm")]
use wasm_bindgen::prelude::*;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LayoutNode {
pub id: String,
pub width: f64,
pub height: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LayoutEdge {
pub source: String,
pub target: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LayoutOptions {
#[serde(default = "default_rankdir")]
pub rankdir: String,
#[serde(default = "default_nodesep")]
pub nodesep: f64,
#[serde(default = "default_ranksep")]
pub ranksep: f64,
}
fn default_rankdir() -> String { "TB".to_string() }
fn default_nodesep() -> f64 { 60.0 }
fn default_ranksep() -> f64 { 80.0 }
impl Default for LayoutOptions {
fn default() -> Self {
Self {
rankdir: default_rankdir(),
nodesep: default_nodesep(),
ranksep: default_ranksep(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LayoutResult {
pub positions: HashMap<String, Position>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct Position {
pub x: f64,
pub y: f64,
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn hierarchical_layout(
nodes_json: &str,
edges_json: &str,
options_json: &str,
) -> String {
let nodes: Vec<LayoutNode> = serde_json::from_str(nodes_json).unwrap_or_default();
let edges: Vec<LayoutEdge> = serde_json::from_str(edges_json).unwrap_or_default();
let options: LayoutOptions = serde_json::from_str(options_json).unwrap_or_default();
let result = layout_hierarchical(&nodes, &edges, &options);
serde_json::to_string(&result).unwrap_or_default()
}
pub fn layout_hierarchical(
nodes: &[LayoutNode],
edges: &[LayoutEdge],
options: &LayoutOptions,
) -> LayoutResult {
if nodes.is_empty() {
return LayoutResult { positions: HashMap::new() };
}
let node_ids: HashSet<&str> = nodes.iter().map(|n| n.id.as_str()).collect();
let dims: HashMap<&str, (f64, f64)> = nodes
.iter()
.map(|n| (n.id.as_str(), (n.width, n.height)))
.collect();
let valid_edges: Vec<(&str, &str)> = edges
.iter()
.filter(|e| node_ids.contains(e.source.as_str()) && node_ids.contains(e.target.as_str()))
.map(|e| (e.source.as_str(), e.target.as_str()))
.collect();
let mut adj: HashMap<&str, Vec<&str>> = HashMap::new();
let mut in_degree: HashMap<&str, usize> = HashMap::new();
for &id in &node_ids {
adj.entry(id).or_default();
in_degree.entry(id).or_insert(0);
}
for &(src, tgt) in &valid_edges {
adj.entry(src).or_default().push(tgt);
*in_degree.entry(tgt).or_insert(0) += 1;
}
let mut layers: HashMap<&str, usize> = HashMap::new();
let mut queue: VecDeque<&str> = VecDeque::new();
for (&id, °) in &in_degree {
if deg == 0 {
queue.push_back(id);
layers.insert(id, 0);
}
}
while let Some(node) = queue.pop_front() {
let layer = layers[node];
if let Some(children) = adj.get(node) {
for &child in children {
let child_layer = layers.entry(child).or_insert(0);
*child_layer = (*child_layer).max(layer + 1);
let deg = in_degree.get_mut(child).unwrap();
*deg -= 1;
if *deg == 0 {
queue.push_back(child);
}
}
}
}
for &id in &node_ids {
layers.entry(id).or_insert(0);
}
let max_layer = layers.values().copied().max().unwrap_or(0);
let mut layer_nodes: Vec<Vec<&str>> = vec![vec![]; max_layer + 1];
for (&id, &layer) in &layers {
layer_nodes[layer].push(id);
}
let rev_adj: HashMap<&str, Vec<&str>> = {
let mut r: HashMap<&str, Vec<&str>> = HashMap::new();
for &(src, tgt) in &valid_edges {
r.entry(tgt).or_default().push(src);
}
r
};
for _pass in 0..4 {
for layer_idx in 1..=max_layer {
let prev_positions: HashMap<&str, f64> = layer_nodes[layer_idx - 1]
.iter()
.enumerate()
.map(|(i, &id)| (id, i as f64))
.collect();
let mut barycenters: Vec<(&str, f64)> = layer_nodes[layer_idx]
.iter()
.map(|&id| {
let parents = rev_adj.get(id).map(|v| v.as_slice()).unwrap_or(&[]);
if parents.is_empty() {
(id, f64::MAX)
} else {
let sum: f64 = parents
.iter()
.filter_map(|p| prev_positions.get(p))
.sum();
let count = parents.iter().filter(|p| prev_positions.contains_key(*p)).count();
if count > 0 {
(id, sum / count as f64)
} else {
(id, f64::MAX)
}
}
})
.collect();
barycenters.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
layer_nodes[layer_idx] = barycenters.into_iter().map(|(id, _)| id).collect();
}
}
let is_horizontal = options.rankdir == "LR";
let mut positions = HashMap::new();
for (layer_idx, layer) in layer_nodes.iter().enumerate() {
let mut offset = 0.0;
for &id in layer {
let (w, h) = dims.get(id).copied().unwrap_or((160.0, 60.0));
let (node_main, node_cross) = if is_horizontal { (h, w) } else { (w, h) };
let main_pos = layer_idx as f64 * (if is_horizontal {
node_cross + options.ranksep
} else {
node_cross + options.ranksep
});
let cross_pos = offset + node_main / 2.0;
offset += node_main + options.nodesep;
let pos = if is_horizontal {
Position { x: main_pos, y: cross_pos }
} else {
Position { x: cross_pos, y: main_pos }
};
positions.insert(id.to_string(), pos);
}
if !layer.is_empty() {
let total_width = offset - options.nodesep;
let shift = -total_width / 2.0;
for &id in layer {
if let Some(pos) = positions.get_mut(id) {
if is_horizontal {
pos.y += shift;
} else {
pos.x += shift;
}
}
}
}
}
LayoutResult { positions }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_graph() {
let result = layout_hierarchical(&[], &[], &LayoutOptions::default());
assert!(result.positions.is_empty());
}
#[test]
fn single_node() {
let nodes = vec![LayoutNode { id: "a".into(), width: 160.0, height: 60.0 }];
let result = layout_hierarchical(&nodes, &[], &LayoutOptions::default());
assert_eq!(result.positions.len(), 1);
assert!(result.positions.contains_key("a"));
}
#[test]
fn linear_chain() {
let nodes = vec![
LayoutNode { id: "a".into(), width: 100.0, height: 50.0 },
LayoutNode { id: "b".into(), width: 100.0, height: 50.0 },
LayoutNode { id: "c".into(), width: 100.0, height: 50.0 },
];
let edges = vec![
LayoutEdge { source: "a".into(), target: "b".into() },
LayoutEdge { source: "b".into(), target: "c".into() },
];
let result = layout_hierarchical(&nodes, &edges, &LayoutOptions::default());
let a = result.positions["a"];
let b = result.positions["b"];
let c = result.positions["c"];
assert!(a.y < b.y, "a.y ({}) should be < b.y ({})", a.y, b.y);
assert!(b.y < c.y, "b.y ({}) should be < c.y ({})", b.y, c.y);
}
#[test]
fn diamond_graph() {
let nodes = vec![
LayoutNode { id: "top".into(), width: 100.0, height: 50.0 },
LayoutNode { id: "left".into(), width: 100.0, height: 50.0 },
LayoutNode { id: "right".into(), width: 100.0, height: 50.0 },
LayoutNode { id: "bottom".into(), width: 100.0, height: 50.0 },
];
let edges = vec![
LayoutEdge { source: "top".into(), target: "left".into() },
LayoutEdge { source: "top".into(), target: "right".into() },
LayoutEdge { source: "left".into(), target: "bottom".into() },
LayoutEdge { source: "right".into(), target: "bottom".into() },
];
let result = layout_hierarchical(&nodes, &edges, &LayoutOptions::default());
let top = result.positions["top"];
let left = result.positions["left"];
let right = result.positions["right"];
let bottom = result.positions["bottom"];
assert!(top.y < left.y);
assert!(top.y < right.y);
assert!(left.y < bottom.y);
assert!(right.y < bottom.y);
assert!((left.y - right.y).abs() < 1.0);
}
#[test]
fn handles_cycles() {
let nodes = vec![
LayoutNode { id: "a".into(), width: 100.0, height: 50.0 },
LayoutNode { id: "b".into(), width: 100.0, height: 50.0 },
];
let edges = vec![
LayoutEdge { source: "a".into(), target: "b".into() },
LayoutEdge { source: "b".into(), target: "a".into() },
];
let result = layout_hierarchical(&nodes, &edges, &LayoutOptions::default());
assert_eq!(result.positions.len(), 2);
}
#[test]
fn horizontal_layout() {
let nodes = vec![
LayoutNode { id: "a".into(), width: 100.0, height: 50.0 },
LayoutNode { id: "b".into(), width: 100.0, height: 50.0 },
];
let edges = vec![
LayoutEdge { source: "a".into(), target: "b".into() },
];
let opts = LayoutOptions { rankdir: "LR".into(), ..Default::default() };
let result = layout_hierarchical(&nodes, &edges, &opts);
let a = result.positions["a"];
let b = result.positions["b"];
assert!(a.x < b.x, "a.x ({}) should be < b.x ({})", a.x, b.x);
}
}