Sequences of Diagrams
Sequence functionality is very much experimental.
spytial.sequence() records a series of snapshots of a data structure. It plays the snapshots back as an interactive step-by-step visualization. The function is designed for two use cases:
- Algorithm tracing. Call
.record()inside an algorithm as each step completes. - Temporal snapshots. Record a structure at multiple points in time.
Basic usage
import spytial
with spytial.sequence(sequence_policy="stability") as seq:
seq.record({"count": 0, "items": ["a"]})
seq.record({"count": 1, "items": ["a", "b"]})
seq.record({"count": 2, "items": ["b"]})
seq.diagram()
sequence() returns a SequenceRecorder. Each .record(obj) call captures the current state of obj as the next frame. .diagram() renders all frames into an interactive viewer with Previous / Next navigation.
Labelling steps
record() accepts two optional keyword arguments that give frames semantic meaning in the viewer:
label. A short single line shown in the status bar (Step 3 / 24, rotate left at node 5) and as the scrubber tooltip. Whitespace is stripped. The label is truncated to 200 characters.note. A longer description (multiple lines permitted) shown in a collapsible panel below the controls. The note is hidden on frames where no note was recorded.
with spytial.sequence() as seq:
seq.record(tree, label="initial state")
rotate_left(tree, node)
seq.record(tree, label="left-rotate at root",
note="Promotes the right child so the new tree is height-balanced.")
seq.record(tree) # no label and no note; viewer falls back to "Step 3 / 3"
seq.diagram()
Both arguments are optional and independent. A frame can have a label, a note, both, or neither. The form with no keyword arguments (seq.record(obj)) continues to work unchanged.
This method is recommended to narrate algorithms. A typical pattern wraps record() in a helper that takes the label as its first argument:
class RBTree:
def __init__(self, seq):
self._seq = seq
self.root = NIL
def _snap(self, label):
self._seq.record(self.root, label=label)
def insert(self, key):
...
self._snap(f"BST insert: {key} (color=RED)")
self._insert_fixup(z)
See demos/balancing.ipynb for a full Red-Black tree example with labelled fixup cases.
Sequence policies
The sequence_policy controls how the frontend positions atoms across frames:
| Policy | Behaviour |
|---|---|
stability |
Atoms stay anchored to their previous position (default for sequences) |
change_emphasis |
Changed atoms are visually highlighted |
ignore_history |
Each frame is laid out independently |
random_positioning |
Random placement each frame |
The sequence_policy= argument sets the initial policy. Viewers can switch policy with a dropdown in the header. This switch helps to compare how each policy presents the same algorithm. A switch mid-sequence re-applies the previous to current transition with the new policy. On step 0, which has no transition, the change takes effect on the next navigation.
In-place mutation (most common)
When the same Python objects are mutated between frames, atom IDs are stable automatically. No configuration is needed. The recorder uses a single shared builder with a persistent ID table. The table survives across .record() calls.
class Node:
def __init__(self, val, left=None, right=None):
self.val = val
self.left = left
self.right = right
root = Node(5, Node(3), Node(7))
with spytial.sequence(sequence_policy="stability") as seq:
seq.record(root)
root.val = 99 # mutate in place
seq.record(root)
root.left.val = 11
seq.record(root)
seq.diagram()
This pattern works well for BST insertion, sorting algorithms, and graph BFS/DFS. It works for any algorithm that operates on a shared mutable structure.
Snapshot / deepcopy workflow
If each frame is a fresh object (e.g. copy.deepcopy), pass identity= so the recorder can match conceptually equivalent nodes across frames. The callable receives each object and should return a stable string key or None.
import copy, spytial
snapshots = [copy.deepcopy(root) for _ in range(3)]
with spytial.sequence(
identity=lambda obj: str(obj.node_id) if hasattr(obj, "node_id") else None,
sequence_policy="stability",
) as seq:
for snap in snapshots:
seq.record(snap)
seq.diagram()
identity shall return a str or None. Objects that share the same key across frames are rendered as the same atom and animated smoothly between positions.
Passing as_type
Class-level spatial annotations can be applied to every recorded object via as_type:
seq = spytial.sequence(as_type=MyAnnotatedType, sequence_policy="stability")
Display options
seq.diagram() # auto-detect (inline in Jupyter, browser otherwise)
seq.diagram(method="file") # save as spytial_sequence_visualization.html
seq.diagram(method="browser") # open in a new browser tab
seq.diagram(method="inline") # force Jupyter inline output
seq.diagram(width=1200, height=800, title="My algorithm")
Defaults can also be set at construction time and overridden at render time:
seq = spytial.sequence(method="file", auto_open=False)
# ... record frames ...
seq.diagram(title="Final render")
Using without a context manager
SequenceRecorder does not require with. The context manager is only a readability convention. __exit__ does nothing special.
seq = spytial.sequence(sequence_policy="stability")
tree.insert(seq, 42) # tree passes seq around internally and calls seq.record()
seq.diagram()