use std::collections::HashMap;
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Position {
pub x: f32,
pub y: f32,
}
impl Position {
#[allow(dead_code)]
pub fn new(x: f32, y: f32) -> Self {
Self { x, y }
}
#[allow(dead_code)]
pub fn distance(&self, other: &Position) -> f32 {
let dx = self.x - other.x;
let dy = self.y - other.y;
(dx * dx + dy * dy).sqrt()
}
}
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct LayoutNodeId(pub usize);
#[allow(dead_code)]
#[derive(Debug, Clone, Copy)]
pub struct LayoutEdge {
pub from: LayoutNodeId,
pub to: LayoutNodeId,
}
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LayoutAlgorithm {
ForceDirected {
iterations: usize,
ideal_length: f32,
},
Hierarchical {
layer_spacing: f32,
node_spacing: f32,
},
Grid {
columns: usize,
cell_width: f32,
cell_height: f32,
},
}
impl Default for LayoutAlgorithm {
fn default() -> Self {
LayoutAlgorithm::Grid {
columns: 4,
cell_width: 150.0,
cell_height: 100.0,
}
}
}
#[allow(dead_code)]
pub struct GraphLayout {
algorithm: LayoutAlgorithm,
}
impl GraphLayout {
#[allow(dead_code)]
pub fn new(algorithm: LayoutAlgorithm) -> Self {
Self { algorithm }
}
#[allow(dead_code)]
pub fn compute(
&self,
node_ids: &[LayoutNodeId],
edges: &[LayoutEdge],
) -> HashMap<LayoutNodeId, Position> {
match self.algorithm {
LayoutAlgorithm::Grid {
columns,
cell_width,
cell_height,
} => self.grid_layout(node_ids, columns, cell_width, cell_height),
LayoutAlgorithm::Hierarchical {
layer_spacing,
node_spacing,
} => self.hierarchical_layout(node_ids, edges, layer_spacing, node_spacing),
LayoutAlgorithm::ForceDirected {
iterations,
ideal_length,
} => self.force_directed_layout(node_ids, edges, iterations, ideal_length),
}
}
#[allow(dead_code)]
fn grid_layout(
&self,
node_ids: &[LayoutNodeId],
columns: usize,
cell_width: f32,
cell_height: f32,
) -> HashMap<LayoutNodeId, Position> {
let cols = columns.max(1);
let mut positions = HashMap::new();
for (i, &node_id) in node_ids.iter().enumerate() {
let col = i % cols;
let row = i / cols;
positions.insert(
node_id,
Position::new(col as f32 * cell_width, row as f32 * cell_height),
);
}
positions
}
#[allow(dead_code)]
fn hierarchical_layout(
&self,
node_ids: &[LayoutNodeId],
edges: &[LayoutEdge],
layer_spacing: f32,
node_spacing: f32,
) -> HashMap<LayoutNodeId, Position> {
let mut in_degree: HashMap<LayoutNodeId, usize> = HashMap::new();
for &id in node_ids {
in_degree.insert(id, 0);
}
for edge in edges {
*in_degree.entry(edge.to).or_insert(0) += 1;
}
let mut layers: HashMap<usize, Vec<LayoutNodeId>> = HashMap::new();
for (&node, °) in &in_degree {
layers.entry(deg).or_default().push(node);
}
let mut positions = HashMap::new();
for (layer_idx, (_, layer_nodes)) in layers.iter().enumerate() {
for (node_idx, &node_id) in layer_nodes.iter().enumerate() {
positions.insert(
node_id,
Position::new(
node_idx as f32 * node_spacing,
layer_idx as f32 * layer_spacing,
),
);
}
}
positions
}
#[allow(dead_code)]
fn force_directed_layout(
&self,
node_ids: &[LayoutNodeId],
edges: &[LayoutEdge],
iterations: usize,
ideal_length: f32,
) -> HashMap<LayoutNodeId, Position> {
if node_ids.is_empty() {
return HashMap::new();
}
let n = node_ids.len();
let mut positions: HashMap<LayoutNodeId, Position> = HashMap::new();
for (i, &id) in node_ids.iter().enumerate() {
let angle = 2.0 * std::f32::consts::PI * i as f32 / n as f32;
positions.insert(
id,
Position::new(angle.cos() * ideal_length, angle.sin() * ideal_length),
);
}
let k = ideal_length;
let k2 = k * k;
for _ in 0..iterations {
let mut displacements: HashMap<LayoutNodeId, (f32, f32)> = HashMap::new();
for &id in node_ids {
displacements.insert(id, (0.0, 0.0));
}
for i in 0..n {
for j in (i + 1)..n {
let u = node_ids[i];
let v = node_ids[j];
let pu = match positions.get(&u) {
Some(p) => *p,
None => continue,
};
let pv = match positions.get(&v) {
Some(p) => *p,
None => continue,
};
let dx = pu.x - pv.x;
let dy = pu.y - pv.y;
let dist2 = (dx * dx + dy * dy).max(0.001);
let dist = dist2.sqrt();
let force = k2 / dist;
let fx = (dx / dist) * force;
let fy = (dy / dist) * force;
if let Some(d) = displacements.get_mut(&u) {
d.0 += fx;
d.1 += fy;
}
if let Some(d) = displacements.get_mut(&v) {
d.0 -= fx;
d.1 -= fy;
}
}
}
for edge in edges {
let pu = match positions.get(&edge.from) {
Some(p) => *p,
None => continue,
};
let pv = match positions.get(&edge.to) {
Some(p) => *p,
None => continue,
};
let dx = pu.x - pv.x;
let dy = pu.y - pv.y;
let dist = (dx * dx + dy * dy).sqrt().max(0.001);
let force = dist * dist / k;
let fx = (dx / dist) * force;
let fy = (dy / dist) * force;
if let Some(d) = displacements.get_mut(&edge.from) {
d.0 -= fx;
d.1 -= fy;
}
if let Some(d) = displacements.get_mut(&edge.to) {
d.0 += fx;
d.1 += fy;
}
}
let temp = k / 10.0;
for &id in node_ids {
let (dx, dy) = match displacements.get(&id) {
Some(&d) => d,
None => continue,
};
let disp_len = (dx * dx + dy * dy).sqrt().max(0.001);
let clamped = disp_len.min(temp);
if let Some(pos) = positions.get_mut(&id) {
pos.x += (dx / disp_len) * clamped;
pos.y += (dy / disp_len) * clamped;
}
}
}
positions
}
}
#[cfg(test)]
mod tests {
use super::*;
fn node_ids(n: usize) -> Vec<LayoutNodeId> {
(0..n).map(LayoutNodeId).collect()
}
#[test]
fn test_position_creation() {
let p = Position::new(1.0, 2.0);
assert_eq!(p.x, 1.0);
assert_eq!(p.y, 2.0);
}
#[test]
fn test_position_distance_zero() {
let p = Position::new(3.0, 4.0);
assert_eq!(p.distance(&p), 0.0);
}
#[test]
fn test_position_distance_nonzero() {
let a = Position::new(0.0, 0.0);
let b = Position::new(3.0, 4.0);
assert!((a.distance(&b) - 5.0).abs() < 1e-5);
}
#[test]
fn test_grid_layout_empty() {
let layout = GraphLayout::new(LayoutAlgorithm::Grid {
columns: 4,
cell_width: 100.0,
cell_height: 100.0,
});
let positions = layout.compute(&[], &[]);
assert!(positions.is_empty());
}
#[test]
fn test_grid_layout_count() {
let layout = GraphLayout::new(LayoutAlgorithm::Grid {
columns: 3,
cell_width: 100.0,
cell_height: 100.0,
});
let ids = node_ids(6);
let positions = layout.compute(&ids, &[]);
assert_eq!(positions.len(), 6);
}
#[test]
fn test_grid_layout_first_node_origin() {
let layout = GraphLayout::new(LayoutAlgorithm::Grid {
columns: 4,
cell_width: 100.0,
cell_height: 100.0,
});
let ids = node_ids(4);
let positions = layout.compute(&ids, &[]);
let p = positions[&LayoutNodeId(0)];
assert_eq!(p.x, 0.0);
assert_eq!(p.y, 0.0);
}
#[test]
fn test_grid_layout_second_row() {
let layout = GraphLayout::new(LayoutAlgorithm::Grid {
columns: 2,
cell_width: 100.0,
cell_height: 80.0,
});
let ids = node_ids(4);
let positions = layout.compute(&ids, &[]);
let p = positions[&LayoutNodeId(2)]; assert_eq!(p.x, 0.0);
assert_eq!(p.y, 80.0);
}
#[test]
fn test_hierarchical_layout_count() {
let layout = GraphLayout::new(LayoutAlgorithm::Hierarchical {
layer_spacing: 80.0,
node_spacing: 120.0,
});
let ids = node_ids(4);
let edges = vec![LayoutEdge {
from: LayoutNodeId(0),
to: LayoutNodeId(1),
}];
let positions = layout.compute(&ids, &edges);
assert_eq!(positions.len(), 4);
}
#[test]
fn test_hierarchical_positions_finite() {
let layout = GraphLayout::new(LayoutAlgorithm::Hierarchical {
layer_spacing: 80.0,
node_spacing: 120.0,
});
let ids = node_ids(5);
let edges = vec![
LayoutEdge {
from: LayoutNodeId(0),
to: LayoutNodeId(2),
},
LayoutEdge {
from: LayoutNodeId(1),
to: LayoutNodeId(3),
},
];
let positions = layout.compute(&ids, &edges);
for (_, pos) in &positions {
assert!(pos.x.is_finite());
assert!(pos.y.is_finite());
}
}
#[test]
fn test_force_directed_empty() {
let layout = GraphLayout::new(LayoutAlgorithm::ForceDirected {
iterations: 10,
ideal_length: 100.0,
});
let positions = layout.compute(&[], &[]);
assert!(positions.is_empty());
}
#[test]
fn test_force_directed_count() {
let layout = GraphLayout::new(LayoutAlgorithm::ForceDirected {
iterations: 5,
ideal_length: 100.0,
});
let ids = node_ids(4);
let edges = vec![LayoutEdge {
from: LayoutNodeId(0),
to: LayoutNodeId(1),
}];
let positions = layout.compute(&ids, &edges);
assert_eq!(positions.len(), 4);
}
#[test]
fn test_force_directed_positions_finite() {
let layout = GraphLayout::new(LayoutAlgorithm::ForceDirected {
iterations: 20,
ideal_length: 80.0,
});
let ids = node_ids(6);
let edges = vec![
LayoutEdge {
from: LayoutNodeId(0),
to: LayoutNodeId(1),
},
LayoutEdge {
from: LayoutNodeId(1),
to: LayoutNodeId(2),
},
LayoutEdge {
from: LayoutNodeId(2),
to: LayoutNodeId(3),
},
];
let positions = layout.compute(&ids, &edges);
for (_, pos) in &positions {
assert!(pos.x.is_finite());
assert!(pos.y.is_finite());
}
}
#[test]
fn test_default_algorithm_is_grid() {
let algo = LayoutAlgorithm::default();
assert!(matches!(algo, LayoutAlgorithm::Grid { .. }));
}
}