or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

advanced-algorithms.mdanalysis.mdcentrality.mdcore-classes.mdgenerators.mdindex.mdlayouts.mdshortest-paths.mdtraversal.mdvisualization.md

layouts.mddocs/

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

```