0
# Diagram Generation
1
2
Graphical representation and visualization features including Graphviz diagram generation, state machine visualization, and diagram customization options.
3
4
## Capabilities
5
6
### DotGraphMachine Class
7
8
Main class for generating graphical representations of state machines using Graphviz DOT format.
9
10
```python { .api }
11
class DotGraphMachine:
12
"""
13
Generates graphical representations of state machines using Graphviz.
14
15
Provides customizable visualization with support for different layouts,
16
colors, fonts, and styling options.
17
18
Class Attributes:
19
- graph_rankdir: Direction of the graph layout ("LR" for left-right, "TB" for top-bottom)
20
- font_name: Font face name for graph text
21
- state_font_size: Font size for state labels in points
22
- state_active_penwidth: Line width for active state borders
23
- state_active_fillcolor: Fill color for active states
24
- transition_font_size: Font size for transition labels in points
25
"""
26
27
graph_rankdir: str = "LR"
28
"""Direction of the graph. Defaults to "LR" (option "TB" for top bottom)."""
29
30
font_name: str = "Arial"
31
"""Graph font face name."""
32
33
state_font_size: str = "10"
34
"""State font size in points."""
35
36
state_active_penwidth: int = 2
37
"""Active state external line width."""
38
39
state_active_fillcolor: str = "turquoise"
40
"""Active state fill color."""
41
42
transition_font_size: str = "9"
43
"""Transition font size in points."""
44
45
def __init__(self, machine: StateMachine):
46
"""
47
Initialize diagram generator for a state machine.
48
49
Parameters:
50
- machine: StateMachine instance to visualize
51
"""
52
53
def get_graph(self) -> pydot.Dot:
54
"""
55
Generate and return the Graphviz digraph object.
56
57
Returns:
58
pydot.Dot object that can be rendered to various formats
59
"""
60
61
def create_digraph(self) -> pydot.Dot:
62
"""
63
Create a Graphviz digraph object representing the state machine.
64
65
Returns:
66
pydot.Dot object that can be rendered to various formats
67
"""
68
69
def __call__(self) -> pydot.Dot:
70
"""Alias for get_graph() - allows calling instance as function."""
71
72
def write(self, filename: str, format: str = "png", prog: str = "dot"):
73
"""
74
Write diagram to file.
75
76
Parameters:
77
- filename: Output file path
78
- format: Output format (png, svg, pdf, etc.)
79
- prog: Graphviz layout program (dot, neato, fdp, etc.)
80
"""
81
82
# Built-in diagram generation method
83
class StateMachine:
84
def _graph(self) -> DotGraphMachine:
85
"""Get DotGraphMachine instance for this state machine."""
86
```
87
88
### Diagram Utility Functions
89
90
Standalone functions for diagram generation and export.
91
92
```python { .api }
93
def quickchart_write_svg(sm: StateMachine, path: str):
94
"""
95
Write state machine diagram as SVG using QuickChart service.
96
97
Generates diagram without requiring local Graphviz installation
98
by using the QuickChart.io online service.
99
100
Parameters:
101
- sm: StateMachine instance to visualize
102
- path: Output SVG file path
103
"""
104
105
def write_image(qualname: str, out: str):
106
"""
107
Write state machine diagram to file by class qualname.
108
109
Parameters:
110
- qualname: Fully qualified name of StateMachine class
111
- out: Output file path
112
"""
113
114
def import_sm(qualname: str):
115
"""
116
Import StateMachine class by fully qualified name.
117
118
Parameters:
119
- qualname: Fully qualified class name
120
121
Returns:
122
StateMachine class
123
"""
124
```
125
126
### Command Line Interface
127
128
Command line tools for diagram generation.
129
130
```python { .api }
131
def main(argv=None):
132
"""
133
Main entry point for command line diagram generation.
134
135
Supports generating diagrams from command line using:
136
python -m statemachine.contrib.diagram <qualname> <output_file>
137
"""
138
```
139
140
## Usage Examples
141
142
### Basic Diagram Generation
143
144
```python
145
from statemachine import StateMachine, State
146
from statemachine.contrib.diagram import DotGraphMachine
147
148
class TrafficLight(StateMachine):
149
green = State(initial=True)
150
yellow = State()
151
red = State()
152
153
cycle = (
154
green.to(yellow)
155
| yellow.to(red)
156
| red.to(green)
157
)
158
159
# Create state machine and generate diagram
160
traffic = TrafficLight()
161
162
# Method 1: Using built-in _graph() method
163
graph = traffic._graph()
164
graph.write("traffic_light.png")
165
166
# Method 2: Using DotGraphMachine directly
167
diagram = DotGraphMachine(traffic)
168
diagram.write("traffic_light_custom.png", format="png")
169
170
# Method 3: Generate SVG
171
diagram.write("traffic_light.svg", format="svg")
172
173
# Method 4: Generate PDF
174
diagram.write("traffic_light.pdf", format="pdf")
175
```
176
177
### Custom Diagram Styling
178
179
```python
180
class CustomStyledDiagram(DotGraphMachine):
181
# Customize appearance
182
graph_rankdir = "TB" # Top to bottom layout
183
font_name = "Helvetica"
184
state_font_size = "12"
185
state_active_penwidth = 3
186
state_active_fillcolor = "lightblue"
187
transition_font_size = "10"
188
189
class OrderWorkflow(StateMachine):
190
pending = State("Pending Order", initial=True)
191
paid = State("Payment Received")
192
shipped = State("Order Shipped")
193
delivered = State("Delivered", final=True)
194
cancelled = State("Cancelled", final=True)
195
196
pay = pending.to(paid)
197
ship = paid.to(shipped)
198
deliver = shipped.to(delivered)
199
cancel = (
200
pending.to(cancelled)
201
| paid.to(cancelled)
202
| shipped.to(cancelled)
203
)
204
205
# Create workflow and generate custom styled diagram
206
workflow = OrderWorkflow()
207
custom_diagram = CustomStyledDiagram(workflow)
208
custom_diagram.write("order_workflow_custom.png")
209
210
# Set current state and regenerate to show active state
211
workflow.send("pay")
212
workflow.send("ship")
213
active_diagram = CustomStyledDiagram(workflow)
214
active_diagram.write("order_workflow_active.png")
215
```
216
217
### Online Diagram Generation (No Local Graphviz)
218
219
```python
220
from statemachine.contrib.diagram import quickchart_write_svg
221
222
class SimpleSwitch(StateMachine):
223
off = State("Off", initial=True)
224
on = State("On")
225
226
turn_on = off.to(on)
227
turn_off = on.to(off)
228
229
# Generate diagram using online service (no local Graphviz required)
230
switch = SimpleSwitch()
231
quickchart_write_svg(switch, "switch_diagram.svg")
232
233
print("Diagram generated using QuickChart service")
234
```
235
236
### Complex State Machine Visualization
237
238
```python
239
class ComplexWorkflow(StateMachine):
240
# Define multiple states with descriptive names
241
draft = State("Draft Document", initial=True)
242
review = State("Under Review")
243
revision = State("Needs Revision")
244
approved = State("Approved")
245
published = State("Published", final=True)
246
rejected = State("Rejected", final=True)
247
archived = State("Archived", final=True)
248
249
# Define complex transition network
250
submit = draft.to(review)
251
approve = review.to(approved)
252
request_revision = review.to(revision)
253
resubmit = revision.to(review)
254
reject = review.to(rejected) | approved.to(rejected)
255
publish = approved.to(published)
256
archive = (
257
draft.to(archived)
258
| rejected.to(archived)
259
| published.to(archived)
260
)
261
262
# Add transition conditions for better diagram labeling
263
urgent_publish = approved.to(published).cond("is_urgent")
264
regular_publish = approved.to(published).unless("is_urgent")
265
266
def is_urgent(self, priority: str = "normal"):
267
return priority == "urgent"
268
269
# Generate comprehensive diagram
270
workflow = ComplexWorkflow()
271
272
# Create diagram with custom settings for complex visualization
273
class ComplexDiagram(DotGraphMachine):
274
graph_rankdir = "LR"
275
font_name = "Arial"
276
state_font_size = "11"
277
transition_font_size = "9"
278
279
def create_digraph(self):
280
"""Override to add custom graph attributes."""
281
dot = super().create_digraph()
282
283
# Add graph-level styling
284
dot.set_graph_defaults(
285
fontname=self.font_name,
286
fontsize="14",
287
labelloc="t",
288
label="Document Workflow State Machine"
289
)
290
291
# Add node-level styling
292
dot.set_node_defaults(
293
shape="box",
294
style="rounded,filled",
295
fillcolor="lightyellow",
296
fontname=self.font_name
297
)
298
299
# Add edge-level styling
300
dot.set_edge_defaults(
301
fontname=self.font_name,
302
fontsize=self.transition_font_size
303
)
304
305
return dot
306
307
complex_diagram = ComplexDiagram(workflow)
308
complex_diagram.write("complex_workflow.png", format="png")
309
complex_diagram.write("complex_workflow.svg", format="svg")
310
```
311
312
### Diagram Generation in Different Formats
313
314
```python
315
class MultiFormatExample(StateMachine):
316
start = State("Start", initial=True)
317
process = State("Processing")
318
complete = State("Complete", final=True)
319
error = State("Error", final=True)
320
321
begin = start.to(process)
322
finish = process.to(complete)
323
fail = process.to(error)
324
retry = error.to(process)
325
326
# Generate diagrams in multiple formats
327
example = MultiFormatExample()
328
diagram = DotGraphMachine(example)
329
330
# Common formats
331
formats = {
332
"png": "Portable Network Graphics",
333
"svg": "Scalable Vector Graphics",
334
"pdf": "Portable Document Format",
335
"jpg": "JPEG Image",
336
"gif": "Graphics Interchange Format",
337
"dot": "DOT Source Code"
338
}
339
340
for fmt, description in formats.items():
341
filename = f"multi_format_example.{fmt}"
342
try:
343
diagram.write(filename, format=fmt)
344
print(f"Generated {description}: {filename}")
345
except Exception as e:
346
print(f"Failed to generate {fmt}: {e}")
347
```
348
349
### Command Line Usage Examples
350
351
```python
352
# Command line diagram generation examples:
353
354
# Basic usage:
355
# python -m statemachine.contrib.diagram myapp.machines.OrderMachine order_diagram.png
356
357
# Generate SVG:
358
# python -m statemachine.contrib.diagram myapp.machines.OrderMachine order_diagram.svg
359
360
# Example of programmatic command line invocation
361
import subprocess
362
import sys
363
364
def generate_diagram_cli(machine_qualname: str, output_path: str):
365
"""Generate diagram using command line interface."""
366
try:
367
result = subprocess.run([
368
sys.executable, "-m", "statemachine.contrib.diagram",
369
machine_qualname, output_path
370
], capture_output=True, text=True, check=True)
371
372
print(f"Diagram generated successfully: {output_path}")
373
return True
374
except subprocess.CalledProcessError as e:
375
print(f"Error generating diagram: {e.stderr}")
376
return False
377
378
# Usage
379
if __name__ == "__main__":
380
# This would work if the machine class is importable
381
success = generate_diagram_cli(
382
"myproject.machines.WorkflowMachine",
383
"workflow_diagram.png"
384
)
385
```
386
387
### Integration with Jupyter Notebooks
388
389
```python
390
# Jupyter notebook integration example
391
try:
392
from IPython.display import Image, SVG, display
393
import tempfile
394
import os
395
396
def display_state_machine(machine: StateMachine, format: str = "svg"):
397
"""Display state machine diagram in Jupyter notebook."""
398
# Create temporary file
399
with tempfile.NamedTemporaryFile(suffix=f".{format}", delete=False) as tmp:
400
temp_path = tmp.name
401
402
try:
403
# Generate diagram
404
diagram = DotGraphMachine(machine)
405
diagram.write(temp_path, format=format)
406
407
# Display in notebook
408
if format.lower() == "svg":
409
with open(temp_path, 'r') as f:
410
display(SVG(f.read()))
411
else:
412
display(Image(temp_path))
413
414
finally:
415
# Clean up temporary file
416
if os.path.exists(temp_path):
417
os.unlink(temp_path)
418
419
# Usage in Jupyter notebook
420
class NotebookExample(StateMachine):
421
idle = State("Idle", initial=True)
422
working = State("Working")
423
done = State("Done", final=True)
424
425
start = idle.to(working)
426
complete = working.to(done)
427
428
notebook_machine = NotebookExample()
429
430
# Display the diagram in notebook
431
display_state_machine(notebook_machine, "svg")
432
433
# Show state after transition
434
notebook_machine.send("start")
435
display_state_machine(notebook_machine, "svg")
436
437
except ImportError:
438
print("IPython not available - skipping Jupyter notebook example")
439
```
440
441
### Animated State Diagrams
442
443
```python
444
import time
445
from pathlib import Path
446
447
def create_animated_sequence(machine: StateMachine, events: list, output_dir: str = "animation_frames"):
448
"""Create sequence of diagrams showing state transitions."""
449
Path(output_dir).mkdir(exist_ok=True)
450
451
frames = []
452
453
# Initial state
454
diagram = DotGraphMachine(machine)
455
frame_path = f"{output_dir}/frame_000_initial.png"
456
diagram.write(frame_path)
457
frames.append(frame_path)
458
459
# Process each event
460
for i, event in enumerate(events, 1):
461
try:
462
machine.send(event)
463
diagram = DotGraphMachine(machine)
464
frame_path = f"{output_dir}/frame_{i:03d}_{event}.png"
465
diagram.write(frame_path)
466
frames.append(frame_path)
467
print(f"Generated frame {i}: {event} -> {machine.current_state.id}")
468
except Exception as e:
469
print(f"Error processing event {event}: {e}")
470
break
471
472
return frames
473
474
# Create animated sequence
475
class AnimatedExample(StateMachine):
476
state1 = State("State 1", initial=True)
477
state2 = State("State 2")
478
state3 = State("State 3")
479
final = State("Final", final=True)
480
481
next1 = state1.to(state2)
482
next2 = state2.to(state3)
483
finish = state3.to(final)
484
reset = (state2.to(state1) | state3.to(state1))
485
486
animated_machine = AnimatedExample()
487
event_sequence = ["next1", "next2", "reset", "next1", "next2", "finish"]
488
489
frames = create_animated_sequence(animated_machine, event_sequence)
490
print(f"Generated {len(frames)} animation frames")
491
492
# Note: To create actual animated GIF, you would need additional tools like PIL or ImageIO
493
# Example with PIL (if available):
494
try:
495
from PIL import Image
496
497
def create_gif(frame_paths: list, output_path: str, duration: int = 1000):
498
"""Create animated GIF from frame images."""
499
images = []
500
for path in frame_paths:
501
images.append(Image.open(path))
502
503
images[0].save(
504
output_path,
505
save_all=True,
506
append_images=images[1:],
507
duration=duration,
508
loop=0
509
)
510
print(f"Animated GIF created: {output_path}")
511
512
# create_gif(frames, "state_machine_animation.gif", duration=1500)
513
514
except ImportError:
515
print("PIL not available - cannot create animated GIF")
516
```