oak_visualize/graph/
mod.rs

1#![doc = "Graph layout algorithms for dependency and relationship visualization"]
2
3use crate::{
4    geometry::{Point, Rect, Size},
5    layout::{Edge, Layout},
6};
7use serde::{Deserialize, Serialize};
8use std::collections::{HashMap, HashSet};
9
10/// Graph node for visualization
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct GraphNode {
13    pub id: String,
14    pub label: String,
15    pub node_type: String,
16    pub size: Option<Size>,
17    pub attributes: HashMap<String, String>,
18    pub weight: f64,
19}
20
21impl GraphNode {
22    pub fn new(id: String, label: String) -> Self {
23        Self { id, label, node_type: "default".to_string(), size: None, attributes: HashMap::new(), weight: 1.0 }
24    }
25
26    pub fn with_type(mut self, node_type: String) -> Self {
27        self.node_type = node_type;
28        self
29    }
30
31    pub fn with_size(mut self, size: Size) -> Self {
32        self.size = Some(size);
33        self
34    }
35
36    pub fn with_attribute(mut self, key: String, value: String) -> Self {
37        self.attributes.insert(key, value);
38        self
39    }
40
41    pub fn with_weight(mut self, weight: f64) -> Self {
42        self.weight = weight;
43        self
44    }
45}
46
47/// Graph edge for visualization
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct GraphEdge {
50    pub from: String,
51    pub to: String,
52    pub label: Option<String>,
53    pub edge_type: String,
54    pub weight: f64,
55    pub directed: bool,
56    pub attributes: HashMap<String, String>,
57}
58
59impl GraphEdge {
60    pub fn new(from: String, to: String) -> Self {
61        Self {
62            from,
63            to,
64            label: None,
65            edge_type: "default".to_string(),
66            weight: 1.0,
67            directed: true,
68            attributes: HashMap::new(),
69        }
70    }
71
72    pub fn with_label(mut self, label: String) -> Self {
73        self.label = Some(label);
74        self
75    }
76
77    pub fn with_type(mut self, edge_type: String) -> Self {
78        self.edge_type = edge_type;
79        self
80    }
81
82    pub fn with_weight(mut self, weight: f64) -> Self {
83        self.weight = weight;
84        self
85    }
86
87    pub fn undirected(mut self) -> Self {
88        self.directed = false;
89        self
90    }
91
92    pub fn with_attribute(mut self, key: String, value: String) -> Self {
93        self.attributes.insert(key, value);
94        self
95    }
96}
97
98/// Graph representation
99#[derive(Debug, Clone)]
100pub struct Graph {
101    pub nodes: HashMap<String, GraphNode>,
102    pub edges: Vec<GraphEdge>,
103    pub directed: bool,
104}
105
106impl Graph {
107    pub fn new(directed: bool) -> Self {
108        Self { nodes: HashMap::new(), edges: Vec::new(), directed }
109    }
110
111    pub fn add_node(&mut self, node: GraphNode) {
112        self.nodes.insert(node.id.clone(), node);
113    }
114
115    pub fn add_edge(&mut self, edge: GraphEdge) {
116        self.edges.push(edge);
117    }
118
119    pub fn get_neighbors(&self, node_id: &str) -> Vec<&str> {
120        let mut neighbors = Vec::new();
121
122        for edge in &self.edges {
123            if edge.from == node_id {
124                neighbors.push(edge.to.as_str());
125            }
126            if !self.directed && edge.to == node_id {
127                neighbors.push(edge.from.as_str());
128            }
129        }
130
131        neighbors
132    }
133
134    pub fn get_degree(&self, node_id: &str) -> usize {
135        self.get_neighbors(node_id).len()
136    }
137
138    pub fn is_connected(&self) -> bool {
139        if self.nodes.is_empty() {
140            return true;
141        }
142
143        let start_node = self.nodes.keys().next().unwrap();
144        let mut visited = HashSet::new();
145        let mut stack = vec![start_node.as_str()];
146
147        while let Some(node) = stack.pop() {
148            if visited.contains(node) {
149                continue;
150            }
151
152            visited.insert(node);
153
154            for neighbor in self.get_neighbors(node) {
155                if !visited.contains(neighbor) {
156                    stack.push(neighbor);
157                }
158            }
159        }
160
161        visited.len() == self.nodes.len()
162    }
163
164    pub fn find_cycles(&self) -> Vec<Vec<String>> {
165        let mut cycles = Vec::new();
166        let mut visited = HashSet::new();
167        let mut rec_stack = HashSet::new();
168        let mut path = Vec::new();
169
170        for node_id in self.nodes.keys() {
171            if !visited.contains(node_id) {
172                self.dfs_cycles(node_id, &mut visited, &mut rec_stack, &mut path, &mut cycles);
173            }
174        }
175
176        cycles
177    }
178
179    fn dfs_cycles(
180        &self,
181        node: &str,
182        visited: &mut HashSet<String>,
183        rec_stack: &mut HashSet<String>,
184        path: &mut Vec<String>,
185        cycles: &mut Vec<Vec<String>>,
186    ) {
187        visited.insert(node.to_string());
188        rec_stack.insert(node.to_string());
189        path.push(node.to_string());
190
191        for neighbor in self.get_neighbors(node) {
192            if !visited.contains(neighbor) {
193                self.dfs_cycles(neighbor, visited, rec_stack, path, cycles);
194            }
195            else if rec_stack.contains(neighbor) {
196                // Found a cycle
197                if let Some(cycle_start) = path.iter().position(|n| n == neighbor) {
198                    cycles.push(path[cycle_start..].to_vec());
199                }
200            }
201        }
202
203        rec_stack.remove(node);
204        path.pop();
205    }
206}
207
208/// Graph layout engine
209pub struct GraphLayout {
210    algorithm: GraphLayoutAlgorithm,
211    config: GraphLayoutConfig,
212}
213
214impl GraphLayout {
215    pub fn new(algorithm: GraphLayoutAlgorithm) -> Self {
216        Self { algorithm, config: GraphLayoutConfig::default() }
217    }
218
219    pub fn with_config(mut self, config: GraphLayoutConfig) -> Self {
220        self.config = config;
221        self
222    }
223
224    pub fn layout_graph(&self, graph: &Graph) -> crate::Result<Layout> {
225        match self.algorithm {
226            GraphLayoutAlgorithm::ForceDirected => self.force_directed_layout(graph),
227            GraphLayoutAlgorithm::Circular => self.circular_layout(graph),
228            GraphLayoutAlgorithm::Hierarchical => self.hierarchical_layout(graph),
229            GraphLayoutAlgorithm::Grid => self.grid_layout(graph),
230            GraphLayoutAlgorithm::Organic => self.organic_layout(graph),
231        }
232    }
233
234    /// Force-directed layout using spring-mass model
235    fn force_directed_layout(&self, graph: &Graph) -> crate::Result<Layout> {
236        let mut layout = Layout::new();
237
238        if graph.nodes.is_empty() {
239            return Ok(layout);
240        }
241
242        let mut positions: HashMap<String, Point> = HashMap::new();
243        let mut velocities: HashMap<String, Point> = HashMap::new();
244
245        // Initialize random positions
246        for (i, node_id) in graph.nodes.keys().enumerate() {
247            let angle = 2.0 * std::f64::consts::PI * i as f64 / graph.nodes.len() as f64;
248            let radius = 100.0;
249            positions.insert(node_id.clone(), Point::new(radius * angle.cos(), radius * angle.sin()));
250            velocities.insert(node_id.clone(), Point::origin());
251        }
252
253        // Run simulation
254        for _ in 0..self.config.iterations {
255            let mut forces: HashMap<String, Point> = HashMap::new();
256
257            // Initialize forces
258            for node_id in graph.nodes.keys() {
259                forces.insert(node_id.clone(), Point::origin());
260            }
261
262            // Repulsion forces between all nodes
263            let node_ids: Vec<_> = graph.nodes.keys().collect();
264            for i in 0..node_ids.len() {
265                for j in (i + 1)..node_ids.len() {
266                    let node1_id = node_ids[i];
267                    let node2_id = node_ids[j];
268
269                    if let (Some(pos1), Some(pos2)) = (positions.get(node1_id), positions.get(node2_id)) {
270                        let diff = *pos1 - *pos2;
271                        let distance = pos1.distance_to(pos2).max(1.0);
272                        let force_magnitude = self.config.repulsion_strength / (distance * distance);
273                        let force_direction = Point::new(diff.x / distance, diff.y / distance);
274                        let force = Point::new(force_direction.x * force_magnitude, force_direction.y * force_magnitude);
275
276                        *forces.get_mut(node1_id).unwrap() = *forces.get(node1_id).unwrap() + force;
277                        *forces.get_mut(node2_id).unwrap() = *forces.get(node2_id).unwrap() - force;
278                    }
279                }
280            }
281
282            // Attraction forces along edges
283            for edge in &graph.edges {
284                if let (Some(pos1), Some(pos2)) = (positions.get(&edge.from), positions.get(&edge.to)) {
285                    let diff = *pos2 - *pos1;
286                    let distance = pos1.distance_to(pos2);
287                    let ideal_length = self.config.ideal_edge_length;
288                    let force_magnitude = self.config.spring_strength * (distance - ideal_length) * edge.weight;
289
290                    if distance > 0.0 {
291                        let force_direction = Point::new(diff.x / distance, diff.y / distance);
292                        let force = Point::new(force_direction.x * force_magnitude, force_direction.y * force_magnitude);
293
294                        *forces.get_mut(&edge.from).unwrap() = *forces.get(&edge.from).unwrap() + force;
295                        *forces.get_mut(&edge.to).unwrap() = *forces.get(&edge.to).unwrap() - force;
296                    }
297                }
298            }
299
300            // Update positions
301            for node_id in graph.nodes.keys() {
302                if let (Some(force), Some(velocity), Some(position)) =
303                    (forces.get(node_id), velocities.get_mut(node_id), positions.get_mut(node_id))
304                {
305                    *velocity =
306                        Point::new(velocity.x * self.config.damping + force.x, velocity.y * self.config.damping + force.y);
307
308                    // Limit velocity
309                    let speed = (velocity.x * velocity.x + velocity.y * velocity.y).sqrt();
310                    if speed > self.config.max_velocity {
311                        let scale = self.config.max_velocity / speed;
312                        velocity.x *= scale;
313                        velocity.y *= scale;
314                    }
315
316                    *position = *position + *velocity;
317                }
318            }
319        }
320
321        // Convert positions to layout
322        for (node_id, node) in &graph.nodes {
323            if let Some(position) = positions.get(node_id) {
324                let size = node.size.unwrap_or(Size::new(self.config.node_width, self.config.node_height));
325                let rect = Rect::new(Point::new(position.x - size.width / 2.0, position.y - size.height / 2.0), size);
326                layout.add_node((*node_id).clone(), rect);
327            }
328        }
329
330        // Add edges
331        for edge in &graph.edges {
332            if let (Some(from_rect), Some(to_rect)) = (layout.nodes.get(&edge.from), layout.nodes.get(&edge.to)) {
333                let layout_edge =
334                    Edge::new(edge.from.clone(), edge.to.clone()).with_points(vec![from_rect.center(), to_rect.center()]);
335                layout.add_edge(layout_edge);
336            }
337        }
338
339        Ok(layout)
340    }
341
342    /// Circular layout - arrange nodes in a circle
343    fn circular_layout(&self, graph: &Graph) -> crate::Result<Layout> {
344        let mut layout = Layout::new();
345
346        if graph.nodes.is_empty() {
347            return Ok(layout);
348        }
349
350        let node_count = graph.nodes.len();
351        let radius = self.config.circle_radius;
352        let angle_step = 2.0 * std::f64::consts::PI / node_count as f64;
353
354        for (i, (node_id, node)) in graph.nodes.iter().enumerate() {
355            let angle = i as f64 * angle_step;
356            let position = Point::new(radius * angle.cos(), radius * angle.sin());
357            let size = node.size.unwrap_or(Size::new(self.config.node_width, self.config.node_height));
358            let rect = Rect::new(Point::new(position.x - size.width / 2.0, position.y - size.height / 2.0), size);
359            layout.add_node((*node_id).clone(), rect);
360        }
361
362        // Add edges
363        for edge in &graph.edges {
364            if let (Some(from_rect), Some(to_rect)) = (layout.nodes.get(&edge.from), layout.nodes.get(&edge.to)) {
365                let layout_edge =
366                    Edge::new(edge.from.clone(), edge.to.clone()).with_points(vec![from_rect.center(), to_rect.center()]);
367                layout.add_edge(layout_edge);
368            }
369        }
370
371        Ok(layout)
372    }
373
374    /// Hierarchical layout - arrange nodes in layers
375    fn hierarchical_layout(&self, graph: &Graph) -> crate::Result<Layout> {
376        let mut layout = Layout::new();
377
378        if graph.nodes.is_empty() {
379            return Ok(layout);
380        }
381
382        // Perform topological sort to determine layers
383        let layers = self.topological_layers(graph)?;
384
385        // Position nodes layer by layer
386        for (layer_index, layer) in layers.iter().enumerate() {
387            let y = layer_index as f64 * self.config.layer_distance;
388            let layer_width = layer.len() as f64 * (self.config.node_width + self.config.node_spacing);
389            let start_x = -layer_width / 2.0;
390
391            for (node_index, node_id) in layer.iter().enumerate() {
392                let x = start_x + node_index as f64 * (self.config.node_width + self.config.node_spacing);
393                let node = &graph.nodes[node_id];
394                let size = node.size.unwrap_or(Size::new(self.config.node_width, self.config.node_height));
395                let rect = Rect::new(Point::new(x, y), size);
396                layout.add_node((*node_id).clone(), rect);
397            }
398        }
399
400        // Add edges
401        for edge in &graph.edges {
402            if let (Some(from_rect), Some(to_rect)) = (layout.nodes.get(&edge.from), layout.nodes.get(&edge.to)) {
403                let layout_edge =
404                    Edge::new(edge.from.clone(), edge.to.clone()).with_points(vec![from_rect.center(), to_rect.center()]);
405                layout.add_edge(layout_edge);
406            }
407        }
408
409        Ok(layout)
410    }
411
412    /// Grid layout - arrange nodes in a regular grid
413    fn grid_layout(&self, graph: &Graph) -> crate::Result<Layout> {
414        let mut layout = Layout::new();
415
416        if graph.nodes.is_empty() {
417            return Ok(layout);
418        }
419
420        let node_count = graph.nodes.len();
421        let cols = (node_count as f64).sqrt().ceil() as usize;
422        let _rows = (node_count + cols - 1) / cols;
423
424        for (i, (node_id, node)) in graph.nodes.iter().enumerate() {
425            let row = i / cols;
426            let col = i % cols;
427            let x = col as f64 * (self.config.node_width + self.config.node_spacing);
428            let y = row as f64 * (self.config.node_height + self.config.node_spacing);
429            let size = node.size.unwrap_or(Size::new(self.config.node_width, self.config.node_height));
430            let rect = Rect::new(Point::new(x, y), size);
431            layout.add_node((*node_id).clone(), rect);
432        }
433
434        // Add edges
435        for edge in &graph.edges {
436            if let (Some(from_rect), Some(to_rect)) = (layout.nodes.get(&edge.from), layout.nodes.get(&edge.to)) {
437                let layout_edge =
438                    Edge::new(edge.from.clone(), edge.to.clone()).with_points(vec![from_rect.center(), to_rect.center()]);
439                layout.add_edge(layout_edge);
440            }
441        }
442
443        Ok(layout)
444    }
445
446    /// Organic layout - natural-looking arrangement
447    fn organic_layout(&self, graph: &Graph) -> crate::Result<Layout> {
448        // For now, use force-directed with different parameters
449        let mut organic_config = self.config.clone();
450        organic_config.spring_strength *= 0.5;
451        organic_config.repulsion_strength *= 1.5;
452        organic_config.damping = 0.95;
453
454        let organic_layout = GraphLayout::new(GraphLayoutAlgorithm::ForceDirected).with_config(organic_config);
455
456        organic_layout.force_directed_layout(graph)
457    }
458
459    /// Perform topological sort to determine node layers
460    fn topological_layers(&self, graph: &Graph) -> crate::Result<Vec<Vec<String>>> {
461        let mut in_degree: HashMap<String, usize> = HashMap::new();
462        let mut layers = Vec::new();
463
464        // Initialize in-degrees
465        for node_id in graph.nodes.keys() {
466            in_degree.insert(node_id.clone(), 0);
467        }
468
469        // Calculate in-degrees
470        for edge in &graph.edges {
471            *in_degree.get_mut(&edge.to).unwrap() += 1;
472        }
473
474        // Process layers
475        while !in_degree.is_empty() {
476            let mut current_layer = Vec::new();
477
478            // Find nodes with in-degree 0
479            let zero_in_degree: Vec<_> =
480                in_degree.iter().filter(|&(_, &degree)| degree == 0).map(|(id, _)| id.clone()).collect();
481
482            if zero_in_degree.is_empty() {
483                // Cycle detected - break it by selecting node with minimum in-degree
484                if let Some((min_node, _)) = in_degree.iter().min_by_key(|&(_, &degree)| degree) {
485                    current_layer.push(min_node.clone());
486                }
487            }
488            else {
489                current_layer = zero_in_degree;
490            }
491
492            // Remove processed nodes and update in-degrees
493            for node_id in &current_layer {
494                in_degree.remove(node_id);
495
496                // Decrease in-degree of neighbors
497                for edge in &graph.edges {
498                    if edge.from == *node_id {
499                        if let Some(degree) = in_degree.get_mut(&edge.to) {
500                            *degree = degree.saturating_sub(1);
501                        }
502                    }
503                }
504            }
505
506            layers.push(current_layer);
507        }
508
509        Ok(layers)
510    }
511}
512
513/// Graph layout configuration
514#[derive(Debug, Clone)]
515pub struct GraphLayoutConfig {
516    pub node_width: f64,
517    pub node_height: f64,
518    pub node_spacing: f64,
519    pub layer_distance: f64,
520    pub circle_radius: f64,
521    pub iterations: usize,
522    pub spring_strength: f64,
523    pub repulsion_strength: f64,
524    pub damping: f64,
525    pub max_velocity: f64,
526    pub ideal_edge_length: f64,
527}
528
529impl Default for GraphLayoutConfig {
530    fn default() -> Self {
531        Self {
532            node_width: 80.0,
533            node_height: 40.0,
534            node_spacing: 20.0,
535            layer_distance: 100.0,
536            circle_radius: 200.0,
537            iterations: 100,
538            spring_strength: 0.1,
539            repulsion_strength: 1000.0,
540            damping: 0.9,
541            max_velocity: 10.0,
542            ideal_edge_length: 100.0,
543        }
544    }
545}
546
547/// Graph layout algorithms
548#[derive(Debug, Clone, Copy, PartialEq, Eq)]
549pub enum GraphLayoutAlgorithm {
550    ForceDirected, // Spring-mass model
551    Circular,      // Circular arrangement
552    Hierarchical,  // Layered arrangement
553    Grid,          // Regular grid
554    Organic,       // Natural-looking
555}