scirs2_graph/
layout.rs

1//! Graph layout algorithms for visualization
2//!
3//! This module provides algorithms for computing node positions
4//! for graph visualization.
5
6use std::collections::HashMap;
7use std::f64::consts::PI;
8
9use rand::Rng;
10
11use crate::base::{EdgeWeight, Graph, Node};
12use crate::error::Result;
13
14/// 2D position for graph layout
15#[derive(Debug, Clone, Copy, PartialEq)]
16pub struct Position {
17    /// X coordinate
18    pub x: f64,
19    /// Y coordinate  
20    pub y: f64,
21}
22
23impl Position {
24    /// Create a new position
25    pub fn new(x: f64, y: f64) -> Self {
26        Position { x, y }
27    }
28
29    /// Calculate Euclidean distance to another position
30    pub fn distance(&self, other: &Position) -> f64 {
31        let dx = self.x - other.x;
32        let dy = self.y - other.y;
33        (dx * dx + dy * dy).sqrt()
34    }
35}
36
37/// Compute a circular layout for the graph
38///
39/// Nodes are placed evenly around a circle.
40#[allow(dead_code)]
41pub fn circular_layout<N, E, Ix>(graph: &Graph<N, E, Ix>, radius: f64) -> HashMap<N, Position>
42where
43    N: Node + Clone + std::fmt::Debug,
44    E: EdgeWeight,
45    Ix: petgraph::graph::IndexType,
46{
47    let nodes: Vec<N> = graph.nodes().into_iter().cloned().collect();
48    let n = nodes.len();
49    let mut layout = HashMap::new();
50
51    if n == 0 {
52        return layout;
53    }
54
55    let angle_step = 2.0 * PI / n as f64;
56
57    for (i, node) in nodes.into_iter().enumerate() {
58        let angle = i as f64 * angle_step;
59        let x = radius * angle.cos();
60        let y = radius * angle.sin();
61        layout.insert(node, Position::new(x, y));
62    }
63
64    layout
65}
66
67/// Compute a spring layout using force-directed placement
68///
69/// This is a simplified version of the Fruchterman-Reingold algorithm.
70#[allow(dead_code)]
71pub fn spring_layout<N, E, Ix>(
72    graph: &Graph<N, E, Ix>,
73    iterations: usize,
74    area_width: f64,
75    area_height: f64,
76) -> HashMap<N, Position>
77where
78    N: Node + Clone + std::fmt::Debug,
79    E: EdgeWeight + Into<f64>,
80    Ix: petgraph::graph::IndexType,
81{
82    let nodes: Vec<N> = graph.nodes().into_iter().cloned().collect();
83    let n = nodes.len();
84
85    if n == 0 {
86        return HashMap::new();
87    }
88
89    // Initialize with random positions
90    let mut positions = vec![Position::new(0.0, 0.0); n];
91    let mut rng = rand::rng();
92
93    for pos in &mut positions {
94        pos.x = rng.random::<f64>() * area_width - area_width / 2.0;
95        pos.y = rng.random::<f64>() * area_height - area_height / 2.0;
96    }
97
98    // Ideal spring length
99    let area = area_width * area_height;
100    let k = (area / n as f64).sqrt();
101
102    // Temperature for simulated annealing
103    let mut temperature = area_width.min(area_height) / 10.0;
104    let cooling_rate = temperature / iterations as f64;
105
106    // Create node index mapping
107    let node_to_idx: HashMap<N, usize> = nodes
108        .iter()
109        .enumerate()
110        .map(|(i, n)| (n.clone(), i))
111        .collect();
112
113    // Force-directed iterations
114    for _ in 0..iterations {
115        // Calculate repulsive forces between all pairs
116        let mut forces = vec![Position::new(0.0, 0.0); n];
117
118        for i in 0..n {
119            for j in 0..n {
120                if i != j {
121                    let dx = positions[i].x - positions[j].x;
122                    let dy = positions[i].y - positions[j].y;
123                    let dist = (dx * dx + dy * dy).sqrt().max(0.1);
124
125                    // Repulsive force
126                    let force = k * k / dist;
127                    forces[i].x += (dx / dist) * force;
128                    forces[i].y += (dy / dist) * force;
129                }
130            }
131        }
132
133        // Calculate attractive forces along edges
134        for i in 0..n {
135            if let Ok(neighbors) = graph.neighbors(&nodes[i]) {
136                for neighbor in neighbors {
137                    if let Some(&j) = node_to_idx.get(&neighbor) {
138                        let dx = positions[i].x - positions[j].x;
139                        let dy = positions[i].y - positions[j].y;
140                        let dist = (dx * dx + dy * dy).sqrt().max(0.1);
141
142                        // Attractive force
143                        let force = dist * dist / k;
144                        forces[i].x -= (dx / dist) * force;
145                        forces[i].y -= (dy / dist) * force;
146                    }
147                }
148            }
149        }
150
151        // Update positions
152        for i in 0..n {
153            let disp = (forces[i].x * forces[i].x + forces[i].y * forces[i].y).sqrt();
154            if disp > 0.0 {
155                let limited_disp = disp.min(temperature);
156                positions[i].x += (forces[i].x / disp) * limited_disp;
157                positions[i].y += (forces[i].y / disp) * limited_disp;
158
159                // Keep within bounds
160                positions[i].x = positions[i].x.max(-area_width / 2.0).min(area_width / 2.0);
161                positions[i].y = positions[i]
162                    .y
163                    .max(-area_height / 2.0)
164                    .min(area_height / 2.0);
165            }
166        }
167
168        // Cool down
169        temperature -= cooling_rate;
170    }
171
172    // Convert to HashMap
173    nodes
174        .into_iter()
175        .enumerate()
176        .map(|(i, node)| (node, positions[i]))
177        .collect()
178}
179
180/// Compute a hierarchical layout for a directed acyclic graph
181///
182/// Nodes are arranged in layers based on their topological ordering.
183#[allow(dead_code)]
184pub fn hierarchical_layout<N, E, Ix>(
185    graph: &crate::base::DiGraph<N, E, Ix>,
186    layer_height: f64,
187    node_spacing: f64,
188) -> Result<HashMap<N, Position>>
189where
190    N: Node + Clone + std::hash::Hash + Eq + std::fmt::Debug,
191    E: EdgeWeight,
192    Ix: petgraph::graph::IndexType,
193{
194    use crate::algorithms::topological_sort;
195
196    // For hierarchical layout, we need a DAG
197    // Try to get a topological ordering
198    let topo_order = topological_sort(graph)?;
199
200    let mut layout = HashMap::new();
201    let mut node_to_layer: HashMap<N, usize> = HashMap::new();
202    let mut layer_counts: HashMap<usize, usize> = HashMap::new();
203
204    // Assign layers based on longest path from sources
205    for node in &topo_order {
206        let mut max_pred_layer = 0;
207
208        // Find maximum layer of predecessors
209        if let Ok(predecessors) = graph.predecessors(node) {
210            for predecessor in predecessors {
211                if let Some(&pred_layer) = node_to_layer.get(&predecessor) {
212                    max_pred_layer = max_pred_layer.max(pred_layer + 1);
213                }
214            }
215        }
216
217        node_to_layer.insert(node.clone(), max_pred_layer);
218        *layer_counts.entry(max_pred_layer).or_insert(0) += 1;
219    }
220
221    // Calculate positions
222    let mut layer_offsets: HashMap<usize, f64> = HashMap::new();
223
224    for (node, &layer) in &node_to_layer {
225        let layer_size = layer_counts[&layer];
226        let layer_width = (layer_size - 1) as f64 * node_spacing;
227
228        let offset = layer_offsets.entry(layer).or_insert(-layer_width / 2.0);
229
230        let x = *offset;
231        let y = -(layer as f64 * layer_height);
232
233        layout.insert(node.clone(), Position::new(x, y));
234
235        *offset += node_spacing;
236    }
237
238    Ok(layout)
239}
240
241/// Compute a spectral layout based on eigenvectors of the Laplacian
242///
243/// Uses the second and third smallest eigenvectors of the Laplacian matrix.
244#[allow(dead_code)]
245pub fn spectral_layout<N, E, Ix>(graph: &Graph<N, E, Ix>) -> Result<HashMap<N, Position>>
246where
247    N: Node + Clone + std::fmt::Debug,
248    E: EdgeWeight + Into<f64> + num_traits::Zero + num_traits::One + PartialOrd + Copy,
249    Ix: petgraph::graph::IndexType,
250{
251    use crate::spectral::{laplacian, LaplacianType};
252
253    let nodes: Vec<N> = graph.nodes().into_iter().cloned().collect();
254    let n = nodes.len();
255
256    if n < 2 {
257        let mut layout = HashMap::new();
258        if n == 1 {
259            layout.insert(nodes[0].clone(), Position::new(0.0, 0.0));
260        }
261        return Ok(layout);
262    }
263
264    // Get the Laplacian matrix
265    let _lap = laplacian(graph, LaplacianType::Normalized)?;
266
267    // For a proper implementation, we would compute eigenvectors here
268    // For now, use a simple approximation based on node degrees
269    let mut positions = vec![Position::new(0.0, 0.0); n];
270
271    // Use node degrees to spread out nodes
272    let degrees: Vec<usize> = (0..n)
273        .map(|i| graph.neighbors(&nodes[i]).unwrap_or_default().len())
274        .collect();
275
276    let max_degree = *degrees.iter().max().unwrap_or(&1) as f64;
277
278    for (i, pos) in positions.iter_mut().enumerate() {
279        let angle = 2.0 * PI * i as f64 / n as f64;
280        let radius = 1.0 - (degrees[i] as f64 / max_degree) * 0.5;
281        pos.x = radius * angle.cos();
282        pos.y = radius * angle.sin();
283    }
284
285    // Convert to HashMap
286    Ok(nodes
287        .into_iter()
288        .enumerate()
289        .map(|(i, node)| (node, positions[i]))
290        .collect())
291}
292
293#[cfg(test)]
294mod tests {
295    use super::*;
296
297    #[test]
298    fn test_circular_layout() {
299        let mut graph: Graph<char, f64> = Graph::new();
300        graph.add_edge('A', 'B', 1.0).unwrap();
301        graph.add_edge('B', 'C', 1.0).unwrap();
302        graph.add_edge('C', 'A', 1.0).unwrap();
303
304        let layout = circular_layout(&graph, 10.0);
305
306        assert_eq!(layout.len(), 3);
307
308        // All nodes should be at distance 10 from origin
309        for pos in layout.values() {
310            let dist = (pos.x * pos.x + pos.y * pos.y).sqrt();
311            assert!((dist - 10.0).abs() < 1e-10);
312        }
313    }
314
315    #[test]
316    fn test_spring_layout() {
317        let mut graph: Graph<char, f64> = Graph::new();
318        graph.add_edge('A', 'B', 1.0).unwrap();
319        graph.add_edge('B', 'C', 1.0).unwrap();
320        graph.add_edge('C', 'D', 1.0).unwrap();
321        graph.add_edge('D', 'A', 1.0).unwrap();
322
323        let layout = spring_layout(&graph, 50, 100.0, 100.0);
324
325        assert_eq!(layout.len(), 4);
326
327        // All positions should be within bounds
328        for pos in layout.values() {
329            assert!(pos.x >= -50.0 && pos.x <= 50.0);
330            assert!(pos.y >= -50.0 && pos.y <= 50.0);
331        }
332    }
333
334    #[test]
335    fn test_hierarchical_layout() {
336        let mut graph: crate::base::DiGraph<char, f64> = crate::base::DiGraph::new();
337        // Create a DAG
338        graph.add_edge('A', 'B', 1.0).unwrap();
339        graph.add_edge('A', 'C', 1.0).unwrap();
340        graph.add_edge('B', 'D', 1.0).unwrap();
341        graph.add_edge('C', 'D', 1.0).unwrap();
342
343        let layout = hierarchical_layout(&graph, 50.0, 30.0).unwrap();
344
345        assert_eq!(layout.len(), 4);
346
347        // Check that nodes are arranged in layers
348        // A should be at the top (y = 0)
349        assert!((layout[&'A'].y - 0.0).abs() < 1e-10);
350
351        // B and C should be in the middle layer
352        assert!((layout[&'B'].y - layout[&'C'].y).abs() < 1e-10);
353
354        // D should be at the bottom
355        assert!(layout[&'D'].y < layout[&'B'].y);
356    }
357}