forked from habedi/graphina
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlayout.rs
More file actions
344 lines (293 loc) · 10.8 KB
/
layout.rs
File metadata and controls
344 lines (293 loc) · 10.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
/*!
Graph layout algorithms and position computation
*/
use crate::core::types::{BaseGraph, GraphConstructor, NodeId};
use petgraph::EdgeType;
use std::collections::HashMap;
/// Node position in 2D space
#[derive(Debug, Clone, Copy)]
pub struct NodePosition {
/// X coordinate
pub x: f64,
/// Y coordinate
pub y: f64,
}
/// Layout algorithms for graph visualization
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LayoutAlgorithm {
/// Force-directed layout (default)
#[default]
ForceDirected,
/// Circular layout
Circular,
/// Hierarchical/tree layout
Hierarchical,
/// Grid layout
Grid,
/// Random layout
Random,
}
/// Layout engine for computing node positions
pub struct LayoutEngine;
impl LayoutEngine {
/// Compute node positions using the specified layout algorithm
pub fn compute_layout<A, W, Ty: GraphConstructor<A, W> + EdgeType>(
graph: &BaseGraph<A, W, Ty>,
algorithm: LayoutAlgorithm,
width: f64,
height: f64,
) -> HashMap<NodeId, NodePosition> {
match algorithm {
LayoutAlgorithm::ForceDirected => Self::force_directed_layout(graph, width, height),
LayoutAlgorithm::Circular => Self::circular_layout(graph, width, height),
LayoutAlgorithm::Hierarchical => Self::hierarchical_layout(graph, width, height),
LayoutAlgorithm::Grid => Self::grid_layout(graph, width, height),
LayoutAlgorithm::Random => Self::random_layout(graph, width, height),
}
}
/// Force-directed layout using Fruchterman-Reingold algorithm
fn force_directed_layout<A, W, Ty: GraphConstructor<A, W> + EdgeType>(
graph: &BaseGraph<A, W, Ty>,
width: f64,
height: f64,
) -> HashMap<NodeId, NodePosition> {
let mut positions = HashMap::new();
let nodes: Vec<_> = graph.nodes().map(|(id, _)| id).collect();
if nodes.is_empty() {
return positions;
}
// Initialize with random positions
use rand::Rng;
let mut rng = rand::rng();
for node in &nodes {
positions.insert(
*node,
NodePosition {
x: rng.random_range(0.0..width),
y: rng.random_range(0.0..height),
},
);
}
// Fruchterman-Reingold parameters
let area = width * height;
let k = (area / nodes.len() as f64).sqrt();
let iterations = 50;
let mut temperature = width.max(height) / 10.0;
let cooling_factor = 0.95;
for _ in 0..iterations {
let mut displacements = HashMap::new();
// Initialize displacements for all nodes to avoid panics
for &node in &nodes {
displacements.insert(node, (0.0, 0.0));
}
// Repulsive forces between all pairs
for i in 0..nodes.len() {
let mut dx = 0.0;
let mut dy = 0.0;
for j in 0..nodes.len() {
if i != j {
let pos_i = positions[&nodes[i]];
let pos_j = positions[&nodes[j]];
let delta_x = pos_i.x - pos_j.x;
let delta_y = pos_i.y - pos_j.y;
let distance = (delta_x * delta_x + delta_y * delta_y).sqrt().max(0.01);
let force = k * k / distance;
dx += (delta_x / distance) * force;
dy += (delta_y / distance) * force;
}
}
if let Some((dx_curr, dy_curr)) = displacements.get_mut(&nodes[i]) {
*dx_curr += dx;
*dy_curr += dy;
}
}
// Attractive forces for edges
for (src, tgt, _) in graph.edges() {
if let (Some(&pos_src), Some(&pos_tgt)) = (positions.get(&src), positions.get(&tgt))
{
let delta_x = pos_tgt.x - pos_src.x;
let delta_y = pos_tgt.y - pos_src.y;
let distance = (delta_x * delta_x + delta_y * delta_y).sqrt().max(0.01);
let force = distance * distance / k;
if let Some((dx_src, dy_src)) = displacements.get_mut(&src) {
*dx_src += (delta_x / distance) * force;
*dy_src += (delta_y / distance) * force;
}
if let Some((dx_tgt, dy_tgt)) = displacements.get_mut(&tgt) {
*dx_tgt -= (delta_x / distance) * force;
*dy_tgt -= (delta_y / distance) * force;
}
}
}
// Apply displacements
for node in &nodes {
let (dx, dy) = displacements.get(node).copied().unwrap_or((0.0, 0.0));
if let Some(pos) = positions.get_mut(node) {
let displacement = (dx * dx + dy * dy).sqrt();
if displacement > 0.0 {
let limited = displacement.min(temperature);
pos.x += (dx / displacement) * limited;
pos.y += (dy / displacement) * limited;
// Keep within bounds
pos.x = pos.x.max(0.0).min(width);
pos.y = pos.y.max(0.0).min(height);
}
}
}
temperature *= cooling_factor;
}
positions
}
/// Circular layout
fn circular_layout<A, W, Ty: GraphConstructor<A, W> + EdgeType>(
graph: &BaseGraph<A, W, Ty>,
width: f64,
height: f64,
) -> HashMap<NodeId, NodePosition> {
let mut positions = HashMap::new();
let nodes: Vec<_> = graph.nodes().map(|(id, _)| id).collect();
if nodes.is_empty() {
return positions;
}
let center_x = width / 2.0;
let center_y = height / 2.0;
let radius = width.min(height) / 2.5;
for (i, node) in nodes.iter().enumerate() {
let angle = 2.0 * std::f64::consts::PI * i as f64 / nodes.len() as f64;
positions.insert(
*node,
NodePosition {
x: center_x + radius * angle.cos(),
y: center_y + radius * angle.sin(),
},
);
}
positions
}
/// Hierarchical/tree layout
fn hierarchical_layout<A, W, Ty: GraphConstructor<A, W> + EdgeType>(
graph: &BaseGraph<A, W, Ty>,
width: f64,
height: f64,
) -> HashMap<NodeId, NodePosition> {
let mut positions = HashMap::new();
let nodes: Vec<_> = graph.nodes().map(|(id, _)| id).collect();
if nodes.is_empty() {
return positions;
}
// Simple BFS-based layering
let mut layers: Vec<Vec<NodeId>> = Vec::new();
let mut visited = std::collections::HashSet::new();
let mut queue = std::collections::VecDeque::new();
// Start from nodes with no incoming edges, or just the first node
let start_nodes: Vec<_> = nodes
.iter()
.filter(|&&n| graph.in_degree(n).unwrap_or(0) == 0)
.copied()
.collect();
if start_nodes.is_empty() {
queue.push_back(nodes[0]);
} else {
for node in start_nodes {
queue.push_back(node);
}
}
while !queue.is_empty() {
let layer_size = queue.len();
let mut current_layer = Vec::new();
for _ in 0..layer_size {
if let Some(node) = queue.pop_front() {
if visited.insert(node) {
current_layer.push(node);
// Add neighbors to queue using direct neighbor iteration
for neighbor in graph.neighbors(node) {
if !visited.contains(&neighbor) {
queue.push_back(neighbor);
}
}
}
}
}
if !current_layer.is_empty() {
layers.push(current_layer);
}
}
// Add any remaining unvisited nodes
for node in nodes {
if !visited.contains(&node) {
layers.push(vec![node]);
}
}
// Position nodes
let layer_height = if layers.len() > 1 {
height / (layers.len() - 1) as f64
} else {
height / 2.0
};
for (layer_idx, layer) in layers.iter().enumerate() {
let y = layer_idx as f64 * layer_height;
let layer_width = if layer.len() > 1 {
width / (layer.len() - 1) as f64
} else {
width / 2.0
};
for (node_idx, &node) in layer.iter().enumerate() {
let x = if layer.len() > 1 {
node_idx as f64 * layer_width
} else {
width / 2.0
};
positions.insert(node, NodePosition { x, y });
}
}
positions
}
/// Grid layout
fn grid_layout<A, W, Ty: GraphConstructor<A, W> + EdgeType>(
graph: &BaseGraph<A, W, Ty>,
width: f64,
height: f64,
) -> HashMap<NodeId, NodePosition> {
let mut positions = HashMap::new();
let nodes: Vec<_> = graph.nodes().map(|(id, _)| id).collect();
if nodes.is_empty() {
return positions;
}
let cols = (nodes.len() as f64).sqrt().ceil() as usize;
let rows = (nodes.len() as f64 / cols as f64).ceil() as usize;
let cell_width = width / cols as f64;
let cell_height = height / rows as f64;
for (i, node) in nodes.iter().enumerate() {
let row = i / cols;
let col = i % cols;
positions.insert(
*node,
NodePosition {
x: (col as f64 + 0.5) * cell_width,
y: (row as f64 + 0.5) * cell_height,
},
);
}
positions
}
/// Random layout
fn random_layout<A, W, Ty: GraphConstructor<A, W> + EdgeType>(
graph: &BaseGraph<A, W, Ty>,
width: f64,
height: f64,
) -> HashMap<NodeId, NodePosition> {
use rand::Rng;
let mut rng = rand::rng();
let mut positions = HashMap::new();
for (node, _) in graph.nodes() {
positions.insert(
node,
NodePosition {
x: rng.random_range(0.0..width),
y: rng.random_range(0.0..height),
},
);
}
positions
}
}