0
# Layout Algorithms
1
2
Graph layout algorithms for positioning nodes in 2D space for visualization and analysis. rustworkx provides comprehensive layout options including force-directed, circular, grid-based, and specialized layouts for different graph topologies.
3
4
## Capabilities
5
6
### Force-Directed Layouts
7
8
Physics-based layout algorithms that simulate forces between nodes to achieve aesthetically pleasing positioning.
9
10
```python { .api }
11
def spring_layout(graph, pos = None, fixed = None, k = None, repulsive_exponent: int = 2, adaptive_cooling: bool = True, num_iter: int = 50, tol: float = 1e-6, weight_fn = None, default_weight: float = 1, scale: float = 1, center = None, seed = None) -> dict:
12
"""
13
Position nodes using Fruchterman-Reingold force-directed algorithm.
14
15
Simulates attractive forces between connected nodes and repulsive
16
forces between all nodes to achieve balanced, aesthetically pleasing layout.
17
18
Parameters:
19
- graph: Input graph (PyGraph or PyDiGraph)
20
- pos (dict, optional): Initial positions {node_id: (x, y)}
21
- fixed (set, optional): Nodes to keep at initial positions
22
- k (float, optional): Optimal distance between nodes, default 1/sqrt(n)
23
- repulsive_exponent (int): Exponent for repulsive force calculation
24
- adaptive_cooling (bool): Use adaptive temperature cooling
25
- num_iter (int): Maximum number of iterations
26
- tol (float): Convergence tolerance for position changes
27
- weight_fn (callable, optional): Function to extract edge weights
28
- default_weight (float): Default edge weight for attraction
29
- scale (float): Scale factor for final positions
30
- center (tuple, optional): Center point (x, y) for layout
31
- seed (int, optional): Random seed for initial positions
32
33
Returns:
34
dict: Mapping of node indices to (x, y) coordinate tuples
35
"""
36
```
37
38
### Circular and Ring Layouts
39
40
Layouts that arrange nodes in circular patterns with customizable spacing and orientation.
41
42
```python { .api }
43
def circular_layout(graph, scale: float = 1, center = None):
44
"""
45
Position nodes in circle.
46
47
Arranges all nodes evenly spaced around a circle,
48
useful for displaying cyclic relationships.
49
50
Parameters:
51
- graph: Input graph (PyGraph or PyDiGraph)
52
- scale (float): Radius scaling factor
53
- center (tuple, optional): Center point (x, y), defaults to origin
54
55
Returns:
56
Pos2DMapping: Mapping of node indices to (x, y) coordinates
57
"""
58
59
def shell_layout(graph, nlist = None, rotate = None, scale: float = 1, center = None):
60
"""
61
Position nodes in concentric circles (shells).
62
63
Arranges nodes in multiple circular shells, useful for
64
hierarchical visualization and grouped node display.
65
66
Parameters:
67
- graph: Input graph (PyGraph or PyDiGraph)
68
- nlist (list, optional): List of node lists for each shell
69
- rotate (float, optional): Rotation angle between shells (radians)
70
- scale (float): Overall scaling factor
71
- center (tuple, optional): Center point (x, y)
72
73
Returns:
74
Pos2DMapping: Mapping of node indices to (x, y) coordinates
75
"""
76
77
def spiral_layout(graph, scale: float = 1, center = None, resolution: float = 0.35, equidistant: bool = False):
78
"""
79
Position nodes in spiral pattern.
80
81
Arranges nodes along an outward spiral, useful for
82
displaying sequential or temporal relationships.
83
84
Parameters:
85
- graph: Input graph (PyGraph or PyDiGraph)
86
- scale (float): Overall scaling factor
87
- center (tuple, optional): Center point (x, y)
88
- resolution (float): Spiral compactness (lower = more compressed)
89
- equidistant (bool): Maintain equal distance between adjacent nodes
90
91
Returns:
92
Pos2DMapping: Mapping of node indices to (x, y) coordinates
93
"""
94
```
95
96
### Specialized Layouts
97
98
Layout algorithms designed for specific graph types and topologies.
99
100
```python { .api }
101
def bipartite_layout(graph, first_nodes, horizontal: bool = False, scale: float = 1, center = None, aspect_ratio: float = 4/3):
102
"""
103
Position nodes for bipartite graph visualization.
104
105
Places nodes from each partition on opposite sides,
106
clearly showing bipartite structure.
107
108
Parameters:
109
- graph: Input graph (PyGraph or PyDiGraph)
110
- first_nodes (set): Node indices for first partition
111
- horizontal (bool): Horizontal layout if True, vertical if False
112
- scale (float): Overall scaling factor
113
- center (tuple, optional): Center point (x, y)
114
- aspect_ratio (float): Width to height ratio
115
116
Returns:
117
Pos2DMapping: Mapping of node indices to (x, y) coordinates
118
"""
119
120
def random_layout(graph, center = None, seed = None):
121
"""
122
Position nodes randomly.
123
124
Useful as starting point for other algorithms or
125
for testing layout-independent graph properties.
126
127
Parameters:
128
- graph: Input graph (PyGraph or PyDiGraph)
129
- center (tuple, optional): Center point (x, y)
130
- seed (int, optional): Random seed for reproducible layouts
131
132
Returns:
133
Pos2DMapping: Mapping of node indices to (x, y) coordinates
134
"""
135
```
136
137
## Usage Examples
138
139
### Basic Spring Layout
140
141
```python
142
import rustworkx as rx
143
import matplotlib.pyplot as plt
144
145
# Create sample graph
146
graph = rx.generators.erdos_renyi_gnp_random_graph(20, 0.3, seed=42)
147
148
# Apply spring layout with default parameters
149
pos = rx.spring_layout(graph, seed=42)
150
151
print(f"Positioned {len(pos)} nodes")
152
print(f"Sample positions: {dict(list(pos.items())[:3])}")
153
154
# Plot using matplotlib (if available)
155
if 'matplotlib' in globals():
156
node_x = [pos[node][0] for node in graph.node_indices()]
157
node_y = [pos[node][1] for node in graph.node_indices()]
158
plt.scatter(node_x, node_y)
159
plt.title("Spring Layout")
160
plt.axis('equal')
161
plt.show()
162
```
163
164
### Customized Force-Directed Layout
165
166
```python
167
# Create weighted graph
168
weighted_graph = rx.PyGraph()
169
nodes = weighted_graph.add_nodes_from(['A', 'B', 'C', 'D', 'E'])
170
weighted_graph.add_edges_from([
171
(nodes[0], nodes[1], 0.5), # Weak connection
172
(nodes[1], nodes[2], 2.0), # Strong connection
173
(nodes[2], nodes[3], 1.0), # Medium connection
174
(nodes[3], nodes[4], 0.3), # Very weak connection
175
(nodes[0], nodes[4], 1.5), # Strong connection
176
])
177
178
# Spring layout with edge weights and custom parameters
179
weighted_pos = rx.spring_layout(
180
weighted_graph,
181
k=2.0, # Larger optimal distance
182
weight_fn=lambda x: x, # Use edge weights
183
num_iter=100, # More iterations
184
adaptive_cooling=True, # Better convergence
185
seed=42
186
)
187
188
print("Weighted spring layout positions:")
189
for i, node_name in enumerate(['A', 'B', 'C', 'D', 'E']):
190
x, y = weighted_pos[i]
191
print(f" {node_name}: ({x:.2f}, {y:.2f})")
192
```
193
194
### Circular and Shell Layouts
195
196
```python
197
# Circular layout for cycle graph
198
cycle = rx.generators.cycle_graph(8)
199
circular_pos = rx.circular_layout(cycle, scale=2.0)
200
201
# Shell layout for hierarchical structure
202
hierarchical = rx.PyGraph()
203
# Level 0: root
204
root = [hierarchical.add_node("Root")]
205
# Level 1: children
206
level1 = hierarchical.add_nodes_from(["Child1", "Child2", "Child3"])
207
# Level 2: grandchildren
208
level2 = hierarchical.add_nodes_from(["GC1", "GC2", "GC3", "GC4"])
209
210
# Connect hierarchy
211
for child in level1:
212
hierarchical.add_edge(root[0], child, None)
213
for i, grandchild in enumerate(level2):
214
parent = level1[i % len(level1)] # Distribute grandchildren
215
hierarchical.add_edge(parent, grandchild, None)
216
217
# Shell layout with explicit shell assignments
218
shell_pos = rx.shell_layout(
219
hierarchical,
220
nlist=[root, level1, level2], # Define shells
221
scale=3.0
222
)
223
224
print(f"Circular layout range: x=[{min(pos[0] for pos in circular_pos.values()):.1f}, {max(pos[0] for pos in circular_pos.values()):.1f}]")
225
print(f"Shell layout has {len(shell_pos)} positioned nodes")
226
```
227
228
### Bipartite Layout
229
230
```python
231
# Create bipartite graph
232
bipartite = rx.PyGraph()
233
# Partition A: servers
234
servers = bipartite.add_nodes_from(["Server1", "Server2", "Server3"])
235
# Partition B: clients
236
clients = bipartite.add_nodes_from(["Client1", "Client2", "Client3", "Client4"])
237
238
# Add bipartite edges (only between partitions)
239
connections = [
240
(servers[0], clients[0]),
241
(servers[0], clients[2]),
242
(servers[1], clients[1]),
243
(servers[1], clients[3]),
244
(servers[2], clients[0]),
245
(servers[2], clients[1])
246
]
247
bipartite.add_edges_from([(s, c, None) for s, c in connections])
248
249
# Bipartite layout
250
bip_pos = rx.bipartite_layout(
251
bipartite,
252
first_nodes=set(servers), # Left side
253
horizontal=True, # Horizontal arrangement
254
scale=4.0
255
)
256
257
print("Bipartite layout:")
258
print("Servers (left side):")
259
for server in servers:
260
x, y = bip_pos[server]
261
print(f" Node {server}: ({x:.1f}, {y:.1f})")
262
print("Clients (right side):")
263
for client in clients:
264
x, y = bip_pos[client]
265
print(f" Node {client}: ({x:.1f}, {y:.1f})")
266
```
267
268
### Spiral Layout
269
270
```python
271
# Spiral layout for sequential data
272
sequence = rx.generators.path_graph(15)
273
spiral_pos = rx.spiral_layout(
274
sequence,
275
scale=2.0,
276
resolution=0.3, # Tighter spiral
277
equidistant=True # Equal spacing
278
)
279
280
# Analyze spiral properties
281
distances = []
282
positions = [spiral_pos[i] for i in range(len(spiral_pos))]
283
for i in range(len(positions) - 1):
284
x1, y1 = positions[i]
285
x2, y2 = positions[i + 1]
286
dist = ((x2 - x1)**2 + (y2 - y1)**2)**0.5
287
distances.append(dist)
288
289
print(f"Spiral layout: {len(positions)} nodes")
290
print(f"Average distance between consecutive nodes: {sum(distances)/len(distances):.3f}")
291
print(f"Distance variation: {max(distances) - min(distances):.3f}")
292
```
293
294
### Fixed Node Positioning
295
296
```python
297
# Layout with some nodes fixed in position
298
mixed_graph = rx.generators.complete_graph(6)
299
300
# Fix some nodes at specific positions
301
fixed_positions = {
302
0: (0.0, 0.0), # Center
303
1: (2.0, 0.0), # Right
304
2: (-2.0, 0.0), # Left
305
}
306
307
# Spring layout with fixed nodes
308
mixed_pos = rx.spring_layout(
309
mixed_graph,
310
pos=fixed_positions, # Initial positions
311
fixed=set(fixed_positions.keys()), # Keep these fixed
312
num_iter=50,
313
seed=42
314
)
315
316
print("Layout with fixed nodes:")
317
for node in mixed_graph.node_indices():
318
x, y = mixed_pos[node]
319
status = "FIXED" if node in fixed_positions else "free"
320
print(f" Node {node}: ({x:.2f}, {y:.2f}) [{status}]")
321
```
322
323
### Comparing Different Layouts
324
325
```python
326
# Compare layouts for the same graph
327
test_graph = rx.generators.karate_club_graph()
328
329
layouts = {
330
'spring': rx.spring_layout(test_graph, seed=42),
331
'circular': rx.circular_layout(test_graph, scale=2.0),
332
'random': rx.random_layout(test_graph, seed=42),
333
'spiral': rx.spiral_layout(test_graph, scale=2.0)
334
}
335
336
# Analyze layout properties
337
for name, pos in layouts.items():
338
# Calculate bounding box
339
x_coords = [p[0] for p in pos.values()]
340
y_coords = [p[1] for p in pos.values()]
341
342
width = max(x_coords) - min(x_coords)
343
height = max(y_coords) - min(y_coords)
344
345
print(f"{name.capitalize()} layout:")
346
print(f" Bounding box: {width:.2f} x {height:.2f}")
347
print(f" Aspect ratio: {width/height:.2f}")
348
```
349
350
### Custom Layout Post-Processing
351
352
```python
353
def normalize_layout(pos, target_size=1.0):
354
"""Normalize layout to fit within target size."""
355
if not pos:
356
return pos
357
358
# Find current bounds
359
x_coords = [p[0] for p in pos.values()]
360
y_coords = [p[1] for p in pos.values()]
361
362
min_x, max_x = min(x_coords), max(x_coords)
363
min_y, max_y = min(y_coords), max(y_coords)
364
365
# Calculate scaling factor
366
current_size = max(max_x - min_x, max_y - min_y)
367
if current_size == 0:
368
return pos
369
370
scale = target_size / current_size
371
372
# Center and scale
373
center_x = (min_x + max_x) / 2
374
center_y = (min_y + max_y) / 2
375
376
normalized = {}
377
for node, (x, y) in pos.items():
378
new_x = (x - center_x) * scale
379
new_y = (y - center_y) * scale
380
normalized[node] = (new_x, new_y)
381
382
return normalized
383
384
# Apply custom normalization
385
raw_layout = rx.spring_layout(test_graph, seed=42)
386
normalized_layout = normalize_layout(raw_layout, target_size=5.0)
387
388
print("Layout normalization:")
389
print(f"Raw layout range: {max(max(p) for p in raw_layout.values()):.2f}")
390
print(f"Normalized range: {max(max(abs(coord) for coord in p) for p in normalized_layout.values()):.2f}")
391
```
392
393
### Layout for Large Graphs
394
395
```python
396
# Efficient layout for larger graphs
397
large_graph = rx.generators.erdos_renyi_gnp_random_graph(200, 0.02, seed=42)
398
399
# Use lower tolerance and fewer iterations for speed
400
fast_layout = rx.spring_layout(
401
large_graph,
402
num_iter=30, # Fewer iterations
403
tol=1e-3, # Lower precision
404
adaptive_cooling=True, # Better convergence
405
seed=42
406
)
407
408
# Calculate layout quality metrics
409
def layout_quality(graph, pos):
410
"""Simple metric based on edge lengths."""
411
edge_lengths = []
412
for source, target in graph.edge_list():
413
x1, y1 = pos[source]
414
x2, y2 = pos[target]
415
length = ((x2 - x1)**2 + (y2 - y1)**2)**0.5
416
edge_lengths.append(length)
417
418
return {
419
'mean_edge_length': sum(edge_lengths) / len(edge_lengths),
420
'edge_length_std': (sum((l - sum(edge_lengths)/len(edge_lengths))**2 for l in edge_lengths) / len(edge_lengths))**0.5
421
}
422
423
quality = layout_quality(large_graph, fast_layout)
424
print(f"Large graph layout ({large_graph.num_nodes()} nodes):")
425
print(f" Mean edge length: {quality['mean_edge_length']:.3f}")
426
print(f" Edge length std: {quality['edge_length_std']:.3f}")
427
```