1use serde::{Deserialize, Serialize};
9use std::collections::{HashMap, HashSet, VecDeque};
10
11#[cfg(feature = "wasm")]
12use wasm_bindgen::prelude::*;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct LayoutNode {
18 pub id: String,
19 pub width: f64,
20 pub height: f64,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct LayoutEdge {
25 pub source: String,
26 pub target: String,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct LayoutOptions {
31 #[serde(default = "default_rankdir")]
33 pub rankdir: String,
34 #[serde(default = "default_nodesep")]
36 pub nodesep: f64,
37 #[serde(default = "default_ranksep")]
39 pub ranksep: f64,
40}
41
42fn default_rankdir() -> String { "TB".to_string() }
43fn default_nodesep() -> f64 { 60.0 }
44fn default_ranksep() -> f64 { 80.0 }
45
46impl Default for LayoutOptions {
47 fn default() -> Self {
48 Self {
49 rankdir: default_rankdir(),
50 nodesep: default_nodesep(),
51 ranksep: default_ranksep(),
52 }
53 }
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct LayoutResult {
58 pub positions: HashMap<String, Position>,
59}
60
61#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
62pub struct Position {
63 pub x: f64,
64 pub y: f64,
65}
66
67#[cfg(feature = "wasm")]
70#[wasm_bindgen]
71pub fn hierarchical_layout(
72 nodes_json: &str,
73 edges_json: &str,
74 options_json: &str,
75) -> String {
76 let nodes: Vec<LayoutNode> = serde_json::from_str(nodes_json).unwrap_or_default();
77 let edges: Vec<LayoutEdge> = serde_json::from_str(edges_json).unwrap_or_default();
78 let options: LayoutOptions = serde_json::from_str(options_json).unwrap_or_default();
79 let result = layout_hierarchical(&nodes, &edges, &options);
80 serde_json::to_string(&result).unwrap_or_default()
81}
82
83pub fn layout_hierarchical(
93 nodes: &[LayoutNode],
94 edges: &[LayoutEdge],
95 options: &LayoutOptions,
96) -> LayoutResult {
97 if nodes.is_empty() {
98 return LayoutResult { positions: HashMap::new() };
99 }
100
101 let node_ids: HashSet<&str> = nodes.iter().map(|n| n.id.as_str()).collect();
102 let dims: HashMap<&str, (f64, f64)> = nodes
103 .iter()
104 .map(|n| (n.id.as_str(), (n.width, n.height)))
105 .collect();
106
107 let valid_edges: Vec<(&str, &str)> = edges
109 .iter()
110 .filter(|e| node_ids.contains(e.source.as_str()) && node_ids.contains(e.target.as_str()))
111 .map(|e| (e.source.as_str(), e.target.as_str()))
112 .collect();
113
114 let mut adj: HashMap<&str, Vec<&str>> = HashMap::new();
116 let mut in_degree: HashMap<&str, usize> = HashMap::new();
117 for &id in &node_ids {
118 adj.entry(id).or_default();
119 in_degree.entry(id).or_insert(0);
120 }
121 for &(src, tgt) in &valid_edges {
122 adj.entry(src).or_default().push(tgt);
123 *in_degree.entry(tgt).or_insert(0) += 1;
124 }
125
126 let mut layers: HashMap<&str, usize> = HashMap::new();
129 let mut queue: VecDeque<&str> = VecDeque::new();
130
131 for (&id, °) in &in_degree {
132 if deg == 0 {
133 queue.push_back(id);
134 layers.insert(id, 0);
135 }
136 }
137
138 while let Some(node) = queue.pop_front() {
139 let layer = layers[node];
140 if let Some(children) = adj.get(node) {
141 for &child in children {
142 let child_layer = layers.entry(child).or_insert(0);
143 *child_layer = (*child_layer).max(layer + 1);
144 let deg = in_degree.get_mut(child).unwrap();
145 *deg -= 1;
146 if *deg == 0 {
147 queue.push_back(child);
148 }
149 }
150 }
151 }
152
153 for &id in &node_ids {
155 layers.entry(id).or_insert(0);
156 }
157
158 let max_layer = layers.values().copied().max().unwrap_or(0);
160 let mut layer_nodes: Vec<Vec<&str>> = vec![vec![]; max_layer + 1];
161 for (&id, &layer) in &layers {
162 layer_nodes[layer].push(id);
163 }
164
165 let rev_adj: HashMap<&str, Vec<&str>> = {
167 let mut r: HashMap<&str, Vec<&str>> = HashMap::new();
168 for &(src, tgt) in &valid_edges {
169 r.entry(tgt).or_default().push(src);
170 }
171 r
172 };
173
174 for _pass in 0..4 {
175 for layer_idx in 1..=max_layer {
176 let prev_positions: HashMap<&str, f64> = layer_nodes[layer_idx - 1]
177 .iter()
178 .enumerate()
179 .map(|(i, &id)| (id, i as f64))
180 .collect();
181
182 let mut barycenters: Vec<(&str, f64)> = layer_nodes[layer_idx]
183 .iter()
184 .map(|&id| {
185 let parents = rev_adj.get(id).map(|v| v.as_slice()).unwrap_or(&[]);
186 if parents.is_empty() {
187 (id, f64::MAX)
188 } else {
189 let sum: f64 = parents
190 .iter()
191 .filter_map(|p| prev_positions.get(p))
192 .sum();
193 let count = parents.iter().filter(|p| prev_positions.contains_key(*p)).count();
194 if count > 0 {
195 (id, sum / count as f64)
196 } else {
197 (id, f64::MAX)
198 }
199 }
200 })
201 .collect();
202
203 barycenters.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
204 layer_nodes[layer_idx] = barycenters.into_iter().map(|(id, _)| id).collect();
205 }
206 }
207
208 let is_horizontal = options.rankdir == "LR";
210 let mut positions = HashMap::new();
211
212 for (layer_idx, layer) in layer_nodes.iter().enumerate() {
213 let mut offset = 0.0;
214
215 for &id in layer {
216 let (w, h) = dims.get(id).copied().unwrap_or((160.0, 60.0));
217 let (node_main, node_cross) = if is_horizontal { (h, w) } else { (w, h) };
218
219 let main_pos = layer_idx as f64 * (if is_horizontal {
220 node_cross + options.ranksep
221 } else {
222 node_cross + options.ranksep
223 });
224
225 let cross_pos = offset + node_main / 2.0;
226 offset += node_main + options.nodesep;
227
228 let pos = if is_horizontal {
229 Position { x: main_pos, y: cross_pos }
230 } else {
231 Position { x: cross_pos, y: main_pos }
232 };
233
234 positions.insert(id.to_string(), pos);
235 }
236
237 if !layer.is_empty() {
239 let total_width = offset - options.nodesep;
240 let shift = -total_width / 2.0;
241 for &id in layer {
242 if let Some(pos) = positions.get_mut(id) {
243 if is_horizontal {
244 pos.y += shift;
245 } else {
246 pos.x += shift;
247 }
248 }
249 }
250 }
251 }
252
253 LayoutResult { positions }
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259
260 #[test]
261 fn empty_graph() {
262 let result = layout_hierarchical(&[], &[], &LayoutOptions::default());
263 assert!(result.positions.is_empty());
264 }
265
266 #[test]
267 fn single_node() {
268 let nodes = vec![LayoutNode { id: "a".into(), width: 160.0, height: 60.0 }];
269 let result = layout_hierarchical(&nodes, &[], &LayoutOptions::default());
270 assert_eq!(result.positions.len(), 1);
271 assert!(result.positions.contains_key("a"));
272 }
273
274 #[test]
275 fn linear_chain() {
276 let nodes = vec![
277 LayoutNode { id: "a".into(), width: 100.0, height: 50.0 },
278 LayoutNode { id: "b".into(), width: 100.0, height: 50.0 },
279 LayoutNode { id: "c".into(), width: 100.0, height: 50.0 },
280 ];
281 let edges = vec![
282 LayoutEdge { source: "a".into(), target: "b".into() },
283 LayoutEdge { source: "b".into(), target: "c".into() },
284 ];
285 let result = layout_hierarchical(&nodes, &edges, &LayoutOptions::default());
286
287 let a = result.positions["a"];
288 let b = result.positions["b"];
289 let c = result.positions["c"];
290
291 assert!(a.y < b.y, "a.y ({}) should be < b.y ({})", a.y, b.y);
293 assert!(b.y < c.y, "b.y ({}) should be < c.y ({})", b.y, c.y);
294 }
295
296 #[test]
297 fn diamond_graph() {
298 let nodes = vec![
299 LayoutNode { id: "top".into(), width: 100.0, height: 50.0 },
300 LayoutNode { id: "left".into(), width: 100.0, height: 50.0 },
301 LayoutNode { id: "right".into(), width: 100.0, height: 50.0 },
302 LayoutNode { id: "bottom".into(), width: 100.0, height: 50.0 },
303 ];
304 let edges = vec![
305 LayoutEdge { source: "top".into(), target: "left".into() },
306 LayoutEdge { source: "top".into(), target: "right".into() },
307 LayoutEdge { source: "left".into(), target: "bottom".into() },
308 LayoutEdge { source: "right".into(), target: "bottom".into() },
309 ];
310 let result = layout_hierarchical(&nodes, &edges, &LayoutOptions::default());
311
312 let top = result.positions["top"];
314 let left = result.positions["left"];
315 let right = result.positions["right"];
316 let bottom = result.positions["bottom"];
317
318 assert!(top.y < left.y);
319 assert!(top.y < right.y);
320 assert!(left.y < bottom.y);
321 assert!(right.y < bottom.y);
322 assert!((left.y - right.y).abs() < 1.0);
324 }
325
326 #[test]
327 fn handles_cycles() {
328 let nodes = vec![
329 LayoutNode { id: "a".into(), width: 100.0, height: 50.0 },
330 LayoutNode { id: "b".into(), width: 100.0, height: 50.0 },
331 ];
332 let edges = vec![
333 LayoutEdge { source: "a".into(), target: "b".into() },
334 LayoutEdge { source: "b".into(), target: "a".into() },
335 ];
336 let result = layout_hierarchical(&nodes, &edges, &LayoutOptions::default());
338 assert_eq!(result.positions.len(), 2);
339 }
340
341 #[test]
342 fn horizontal_layout() {
343 let nodes = vec![
344 LayoutNode { id: "a".into(), width: 100.0, height: 50.0 },
345 LayoutNode { id: "b".into(), width: 100.0, height: 50.0 },
346 ];
347 let edges = vec![
348 LayoutEdge { source: "a".into(), target: "b".into() },
349 ];
350 let opts = LayoutOptions { rankdir: "LR".into(), ..Default::default() };
351 let result = layout_hierarchical(&nodes, &edges, &opts);
352
353 let a = result.positions["a"];
354 let b = result.positions["b"];
355 assert!(a.x < b.x, "a.x ({}) should be < b.x ({})", a.x, b.x);
357 }
358}