ascii_dag/
layout.rs

1//! Graph layout algorithms for hierarchical rendering.
2//!
3//! This module implements the Sugiyama layered graph layout algorithm
4//! for positioning nodes in a hierarchical DAG visualization.
5//!
6//! ## Submodules
7//!
8//! - [`generic`] - Generic topological sorting for any data structure (requires `generic` feature)
9
10#[cfg(feature = "generic")]
11pub mod generic;
12
13use crate::graph::DAG;
14use alloc::{vec, vec::Vec};
15
16impl<'a> DAG<'a> {
17    /// Calculate hierarchical levels for all nodes in the graph.
18    ///
19    /// Uses a fixed-point algorithm to assign each node to a level,
20    /// where a node's level is one more than the maximum level of its parents.
21    pub(crate) fn calculate_levels(&self) -> Vec<(usize, usize)> {
22        let mut levels = vec![0usize; self.nodes.len()];
23        let mut changed = true;
24
25        while changed {
26            changed = false;
27            for &(from, to) in &self.edges {
28                // Guard against missing nodes - O(1) HashMap lookups
29                if let Some(from_idx) = self.node_index(from) {
30                    if let Some(to_idx) = self.node_index(to) {
31                        let new_level = levels[from_idx] + 1;
32                        if new_level > levels[to_idx] {
33                            levels[to_idx] = new_level;
34                            changed = true;
35                        }
36                    }
37                }
38            }
39        }
40
41        levels.into_iter().enumerate().collect()
42    }
43
44    /// Calculate levels for a specific subgraph.
45    pub(crate) fn calculate_levels_for_subgraph(
46        &self,
47        subgraph_indices: &[usize],
48    ) -> Vec<(usize, usize)> {
49        let subgraph_node_ids: Vec<usize> = subgraph_indices
50            .iter()
51            .map(|&idx| self.nodes[idx].0)
52            .collect();
53
54        let mut levels = vec![0usize; self.nodes.len()];
55        let mut changed = true;
56
57        while changed {
58            changed = false;
59            for &(from, to) in &self.edges {
60                // Only process edges within this subgraph
61                if !subgraph_node_ids.contains(&from) || !subgraph_node_ids.contains(&to) {
62                    continue;
63                }
64
65                // Guard against missing nodes - O(1) HashMap lookups
66                if let Some(from_idx) = self.node_index(from) {
67                    if let Some(to_idx) = self.node_index(to) {
68                        let new_level = levels[from_idx] + 1;
69                        if new_level > levels[to_idx] {
70                            levels[to_idx] = new_level;
71                            changed = true;
72                        }
73                    }
74                }
75            }
76        }
77
78        subgraph_indices
79            .iter()
80            .map(|&idx| (idx, levels[idx]))
81            .collect()
82    }
83
84    /// PASS 1: Reduce edge crossings using median heuristic.
85    ///
86    /// Applies the Sugiyama crossing reduction algorithm by iteratively
87    /// reordering nodes within levels to minimize edge crossings.
88    pub(crate) fn reduce_crossings(&self, levels: &mut [Vec<usize>], max_level: usize) {
89        // Iterate a few times for better results (diminishing returns after 4-5 iterations)
90        for _ in 0..4 {
91            // Top-down pass: order nodes by median of parents
92            for level_idx in 1..=max_level {
93                // Split borrows to avoid clone
94                let (prev_levels, rest) = levels.split_at_mut(level_idx);
95                let parent_level = &prev_levels[level_idx - 1];
96                self.order_by_median_parents(&mut rest[0], parent_level);
97            }
98
99            // Bottom-up pass: order nodes by median of children
100            for level_idx in (0..max_level).rev() {
101                // Split borrows to avoid clone
102                let (left, right) = levels.split_at_mut(level_idx + 1);
103                let child_level = &right[0];
104                self.order_by_median_children(&mut left[level_idx], child_level);
105            }
106        }
107    }
108
109    /// Order nodes by median position of their parents.
110    fn order_by_median_parents(&self, level_nodes: &mut Vec<usize>, parent_level: &[usize]) {
111        let mut node_medians: Vec<(usize, f32)> = Vec::new();
112
113        for (pos, &idx) in level_nodes.iter().enumerate() {
114            let node_id = self.nodes[idx].0;
115            let parents = self.get_parents(node_id);
116
117            if parents.is_empty() {
118                node_medians.push((idx, pos as f32));
119            } else {
120                // Find positions of parents in the parent level
121                let mut parent_positions: Vec<usize> = parents
122                    .iter()
123                    .filter_map(|&p_id| parent_level.iter().position(|&i| self.nodes[i].0 == p_id))
124                    .collect();
125                parent_positions.sort_unstable();
126
127                let median = if parent_positions.is_empty() {
128                    pos as f32
129                } else if parent_positions.len() % 2 == 1 {
130                    parent_positions[parent_positions.len() / 2] as f32
131                } else {
132                    let mid = parent_positions.len() / 2;
133                    (parent_positions[mid - 1] + parent_positions[mid]) as f32 / 2.0
134                };
135
136                node_medians.push((idx, median));
137            }
138        }
139
140        // Sort by median
141        node_medians.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
142        *level_nodes = node_medians.iter().map(|(idx, _)| *idx).collect();
143    }
144
145    /// Order nodes by median position of their children.
146    fn order_by_median_children(&self, level_nodes: &mut Vec<usize>, child_level: &[usize]) {
147        let mut node_medians: Vec<(usize, f32)> = Vec::new();
148
149        for (pos, &idx) in level_nodes.iter().enumerate() {
150            let node_id = self.nodes[idx].0;
151            let children = self.get_children(node_id);
152
153            if children.is_empty() {
154                node_medians.push((idx, pos as f32));
155            } else {
156                // Find positions of children in the child level
157                let mut child_positions: Vec<usize> = children
158                    .iter()
159                    .filter_map(|&c_id| child_level.iter().position(|&i| self.nodes[i].0 == c_id))
160                    .collect();
161                child_positions.sort_unstable();
162
163                let median = if child_positions.is_empty() {
164                    pos as f32
165                } else if child_positions.len() % 2 == 1 {
166                    child_positions[child_positions.len() / 2] as f32
167                } else {
168                    let mid = child_positions.len() / 2;
169                    (child_positions[mid - 1] + child_positions[mid]) as f32 / 2.0
170                };
171
172                node_medians.push((idx, median));
173            }
174        }
175
176        node_medians.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
177        *level_nodes = node_medians.iter().map(|(idx, _)| *idx).collect();
178    }
179
180    /// PASS 2: Assign x-coordinates to each node (character-level positioning).
181    ///
182    /// Positions nodes horizontally to minimize edge length while
183    /// maintaining the ordering from crossing reduction.
184    pub(crate) fn assign_x_coordinates(
185        &self,
186        levels: &mut [Vec<usize>],
187        max_level: usize,
188    ) -> Vec<usize> {
189        let mut x_coords = vec![0usize; self.nodes.len()];
190
191        // Start with left-to-right layout within each level, preserving crossing reduction order
192        for level_nodes in levels.iter() {
193            let mut x = 0;
194            for &idx in level_nodes.iter() {
195                x_coords[idx] = x;
196                let width = self.get_node_width(idx);
197                x += width + 3;
198            }
199        }
200
201        // Refine positions using median of connected nodes to center under parents/over children
202        // But maintain relative order within levels
203        for _ in 0..2 {
204            // Top-down: center under parents where possible
205            for level_idx in 1..=max_level {
206                for &idx in &levels[level_idx] {
207                    let node_id = self.nodes[idx].0;
208                    let parents = self.get_parents(node_id);
209
210                    if !parents.is_empty() {
211                        let mut parent_centers: Vec<usize> = Vec::new();
212                        for &p_id in &parents {
213                            // O(1) HashMap lookup instead of O(n) scan
214                            if let Some(p_idx) = self.node_index(p_id) {
215                                let width = self.get_node_width(p_idx); // Use cached width
216                                parent_centers.push(x_coords[p_idx] + width / 2);
217                            }
218                        }
219
220                        if !parent_centers.is_empty() {
221                            parent_centers.sort_unstable();
222                            let median = parent_centers[parent_centers.len() / 2];
223                            let width = self.get_node_width(idx);
224                            // Shift toward median but don't reorder
225                            let target = median.saturating_sub(width / 2);
226                            x_coords[idx] = (x_coords[idx] + target) / 2;
227                        }
228                    }
229                }
230
231                // Re-compact this level to remove overlaps and reorder to match x-coords
232                self.compact_level(&mut x_coords, &mut levels[level_idx]);
233            }
234        }
235
236        x_coords
237    }
238
239    /// Compact a level to remove overlaps and reorder nodes left-to-right by x-coordinate.
240    pub(crate) fn compact_level(&self, x_coords: &mut [usize], level_nodes: &mut Vec<usize>) {
241        if level_nodes.is_empty() {
242            return;
243        }
244
245        // Sort nodes by their current x position
246        let mut sorted: Vec<_> = level_nodes
247            .iter()
248            .map(|&idx| (x_coords[idx], idx))
249            .collect();
250        sorted.sort_by_key(|(x, _)| *x);
251
252        // Reassign x-coords to remove overlaps and update level_nodes order
253        level_nodes.clear();
254        let mut x = 0;
255        for (_, idx) in sorted {
256            level_nodes.push(idx);
257            x_coords[idx] = x;
258            let width = self.get_node_width(idx);
259            x += width + 3;
260        }
261    }
262
263    /// PASS 3: Calculate canvas dimensions.
264    ///
265    /// Determines the width needed for each level and the overall canvas.
266    pub(crate) fn calculate_canvas_dimensions(
267        &self,
268        levels: &[Vec<usize>],
269        x_coords: &[usize],
270    ) -> (Vec<usize>, usize) {
271        let mut level_widths = Vec::new();
272        let mut max_width = 0;
273
274        for level_nodes in levels {
275            if level_nodes.is_empty() {
276                level_widths.push(0);
277                continue;
278            }
279
280            let min_x = level_nodes
281                .iter()
282                .map(|&idx| x_coords[idx])
283                .min()
284                .unwrap_or(0);
285            let max_node_idx = level_nodes
286                .iter()
287                .max_by_key(|&&idx| x_coords[idx])
288                .unwrap();
289            let width = self.get_node_width(*max_node_idx);
290            let level_width = (x_coords[*max_node_idx] - min_x) + width;
291
292            level_widths.push(level_width);
293            max_width = max_width.max(level_width);
294        }
295
296        (level_widths, max_width)
297    }
298
299    /// Find disconnected subgraphs in the DAG.
300    pub(crate) fn find_subgraphs(&self) -> Vec<Vec<usize>> {
301        let mut visited = vec![false; self.nodes.len()];
302        let mut subgraphs = Vec::new();
303
304        for i in 0..self.nodes.len() {
305            if !visited[i] {
306                let mut subgraph = Vec::new();
307                self.collect_connected(i, &mut visited, &mut subgraph);
308                subgraphs.push(subgraph);
309            }
310        }
311
312        subgraphs
313    }
314
315    /// Collect all nodes connected to the given node (helper for find_subgraphs).
316    fn collect_connected(&self, start_idx: usize, visited: &mut [bool], subgraph: &mut Vec<usize>) {
317        if visited[start_idx] {
318            return;
319        }
320
321        visited[start_idx] = true;
322        subgraph.push(start_idx);
323
324        let node_id = self.nodes[start_idx].0;
325
326        // Follow edges in both directions
327        for &(from, to) in &self.edges {
328            if from == node_id {
329                // O(1) HashMap lookup instead of O(n) scan
330                if let Some(child_idx) = self.node_index(to) {
331                    self.collect_connected(child_idx, visited, subgraph);
332                }
333            }
334            if to == node_id {
335                // O(1) HashMap lookup instead of O(n) scan
336                if let Some(parent_idx) = self.node_index(from) {
337                    self.collect_connected(parent_idx, visited, subgraph);
338                }
339            }
340        }
341    }
342
343    /// Check if a subgraph is a simple chain (no branching).
344    pub(crate) fn is_subgraph_simple_chain(&self, subgraph_indices: &[usize]) -> bool {
345        for &idx in subgraph_indices {
346            let node_id = self.nodes[idx].0;
347            let parents = self.get_parents(node_id);
348            let children = self.get_children(node_id);
349
350            if parents.len() > 1 || children.len() > 1 {
351                return false;
352            }
353        }
354        true
355    }
356}
357
358#[cfg(test)]
359mod tests {
360    use crate::graph::DAG;
361
362    #[test]
363    fn test_calculate_levels() {
364        let dag = DAG::from_edges(&[(1, "A"), (2, "B"), (3, "C")], &[(1, 2), (2, 3)]);
365
366        let levels = dag.calculate_levels();
367
368        // Find levels for each node
369        let level_map: std::collections::HashMap<_, _> = levels
370            .into_iter()
371            .map(|(idx, level)| (dag.nodes[idx].0, level))
372            .collect();
373
374        assert_eq!(level_map[&1], 0); // Root
375        assert_eq!(level_map[&2], 1);
376        assert_eq!(level_map[&3], 2);
377    }
378
379    #[test]
380    fn test_diamond_layout() {
381        let dag = DAG::from_edges(
382            &[(1, "Top"), (2, "Left"), (3, "Right"), (4, "Bottom")],
383            &[(1, 2), (1, 3), (2, 4), (3, 4)],
384        );
385
386        let levels = dag.calculate_levels();
387        let level_map: std::collections::HashMap<_, _> = levels
388            .into_iter()
389            .map(|(idx, level)| (dag.nodes[idx].0, level))
390            .collect();
391
392        assert_eq!(level_map[&1], 0); // Top
393        assert_eq!(level_map[&2], 1); // Left and Right
394        assert_eq!(level_map[&3], 1);
395        assert_eq!(level_map[&4], 2); // Bottom
396    }
397}