Skip to main content

Write the algorithm, get the animation. Instrumented data structures that compile your real Python code into interactive HTML visualizations. Built for educational purposes

Project description

granim — User Guide

granim turns the algorithm you already wrote into an interactive animation. You write normal Python against granim's data structures (or your own classes, decorated); running the function produces a single self-contained HTML file with a player: nodes appear, arrows flip, cells change color, variables hop between nodes, and you can scrub through every step.

pip install granim-viz
import granim as ga

The PyPI name is granim-viz; the import name is granim. Zero dependencies, Python ≥ 3.10. Every snippet in this guide is runnable as-is.


1. The one decorator: @ga.animate

import granim as ga

@ga.animate(debug=True)
def reverse(head):
    prev, cur = None, head
    while cur is not None:
        nxt = cur.next
        cur.next = prev
        prev, cur = cur, nxt
    return prev

reverse(ga.linked_list([1, 2, 3, 4, 5]).head)

Run it. A reverse.html appears next to your script (named after the function) and opens in your browser. Each loop iteration is one animation beat; cur.next = prev renders as the arrow visibly reversing; prev/cur/ nxt badges hop from node to node.

All parameters (every one optional):

parameter default meaning
debug False show the side panel (variables, call stack) and on-canvas badges
theme "dark" "dark" (manim-style black), "light", "contrast", or a dict overlay like {"base": "dark", "--node-stroke": "#ff5577"}
out <funcname>.html output path
show auto open in browser (scripts) / render inline (Jupyter); False to just write the file
speed 1.0 initial playback speed
title function name header title

Useful facts: the decorated function's return value is passed through unchanged; recursion through the decorated name records as one animation; if the function raises, the partial animation is still saved with the exception as the final step label — animating a crash is a feature. In Jupyter, the animation renders inline automatically.

2. Built-in structures

All five work as plain Python with no recording active — your algorithm stays unit-testable. They only emit animation events inside a recorded call.

ga.array(values)

arr = ga.array([2, 5, 8, 12, 16, 23, 38, 56, 72, 91])

@ga.animate(debug=True)
def binary_search(a, x):
    lo, hi = 0, len(a) - 1
    while lo <= hi:
        mid = (lo + hi) // 2
        if a[mid] == x:
            return mid
        if a[mid] < x:
            lo = mid + 1
        else:
            hi = mid - 1
    return -1

binary_search(arr, 23)

a[i] pulses the cell; comparisons (a[mid] < x) animate with a floating 8 < 23 ✓ glyph; a[i] = v animates the value swap; a.swap(i, j) swaps two cells in one beat; a[i].state = "done" colors a cell. Index badges are automatic: any int local in range (here lo, hi, mid) renders as a pointer under the cells — no annotation needed. Elements must be scalars (int/float/str/bool/None).

ga.linked_list(values, doubly=False)

ll = ga.linked_list([1, 2, 3], doubly=True)
node = ll.head          # tracked: a "head" badge floats over this node
node.next               # read -> pulse
node.next = other       # edge_set -> arrow redraws (or flips, see below)
node.value, node.value = ...  # reads/writes animate
list(ll), ll.to_list()  # plain iteration helpers

Notes for doubly lists: prev edges render as arcs above the chain; node.next = x does not auto-set x.prev — set both explicitly, exactly like LeetCode code does. The flip animation is automatic: when you point an edge back along a channel that just carried the reverse edge (cur.next = prev, or recursive node.next.next = node), granim classifies it as a flip and animates the arrow turning around instead of delete+add.

Unlinked nodes dim out automatically once nothing points to them and no variable holds them (try the dedup example) — see §6.

ga.tree(adj=None) and ga.TreeNode

t = ga.tree()                       # empty; or ga.tree({"A": ["B","C"], "B": ["D"]})

@ga.animate(debug=True)
def insert(t, values):
    for v in values:
        node = ga.TreeNode(v)
        if t.root is None:
            t.root = node
            continue
        cur = t.root
        while True:
            if v < cur.value:
                if cur.left is None:
                    cur.left = node; break
                cur = cur.left
            else:
                if cur.right is None:
                    cur.right = node; break
                cur = cur.right

insert(t, [50, 30, 70, 20, 40, 60, 80])

Tidy-tree layout (Buchheim) re-flows as nodes attach. left/right are sugar over a children list; node.add_child(c) for n-ary trees.

ga.graph(directed=True)

g = ga.graph(directed=False)
a, b = g.add_node("A"), g.add_node("B")
g.add_edge(a, b, weight=4)     # weights render as edge labels
g.neighbors(a); g.weight(a, b)
a.state = "frontier"           # color: default/active/visited/frontier/done

DAGs get layered layout; cyclic graphs get force-directed (deterministic — same input, same picture). State changes on the same line of a loop body merge into one parallel beat: a BFS frontier lights up all at once, no annotations needed.

ga.matrix(rows)

m = ga.matrix([[1, 1, 0], [0, 1, 0], [0, 1, 1]])
x = m[0][1]                # read (row-proxy indexing, like normal DP code)
y = m[0, 1]                # tuple indexing also works
m[1][2] = 7                # value animates
m[1][2].state = "visited"  # cell color animates
rows, cols = len(m), m.cols

See examples/flood_fill.py (colors + values spreading recursively) and examples/edit_distance.py (DP table filling, dependency cells pulsing).

3. Your own classes: @ga.node and @ga.container

You don't have to use granim's structures at all.

@ga.node(value="val", shape="pill")     # shape: "pill" | "circle" | "cell"
class Node:                              # the exact class LeetCode gives you
    def __init__(self, val):
        self.val = val
        self.next = None
        self.random = None

No inheritance. Rules: a field assigned a node becomes an animated edge (next/prev are drawn as list edges and participate in flips; any other name — random, child, down — arcs underneath with its name as a label). The field named by value= animates label changes. Anything else (self.weight = 42) is plain storage. Reads of node-valued fields pulse. Iterators work: a generator __iter__ defined in your file animates step by step, appears in the call stack panel, and its locals get badges.

@ga.container
class Queue:
    def __init__(self):
        self.head = None
        self.tail = None
    def enqueue(self, v): ...
    def dequeue(self): ...

@ga.container is for the wrapper object: its node-valued fields (head, tail, top, any name) render as floating badges that follow their node, and they anchor garbage detection — dequeued nodes dim out. Without the decorator the container is simply invisible (the chain still animates; nothing dims, because granim refuses to guess about objects it can't see).

Ad-hoc fields work on built-in nodes too: node.random = other on a ga.ListNode animates the same way. Once a field name has been used on a class, reading it on other instances returns None (LeetCode convention); truly unknown names still raise AttributeError, so typos stay loud.

4. The HTML player

One file, fully offline, shareable. Controls: play/pause (Space), step forward/back ( / ), Home/End, a scrubber you can drag to any step, and a speed menu (0.25×–4×, remembered between sessions). The header chip shows the current source line. With debug=True the right panel shows live variables — → name (amber) are node references, ▸ name (blue) are array indices — and the call stack, with the active frame highlighted. Variables disappear when their function returns. Every recording ends on one extra beat showing the final state.

5. Tier 2: manual control

The decorator covers ~95% of use. For the rest:

with ga.record(debug=True) as rec:
    ll = ga.linked_list([1, 2, 3])
    rec.step("about to break the chain")   # explicit labeled beat
    with rec.batch():                       # everything inside = one beat
        ...
    with rec.quiet():                       # suppress read/compare pulses
        ...
    rec.watch(i=ga.index(arr, i))           # override badge classification
rec.save("out.html")                        # or rec.show() in Jupyter

Tier 2 has no tracing: there is no automatic stepping, so use rec.step() / rec.batch() to shape beats.

6. How granim decides things (so the output never surprises you)

Steps. One executed source line = one beat. Repeated executions of the same line that only color/add nodes merge into a parallel beat. Pointer-walk reads stay one beat per hop, so traversals are visible.

Dimming. A node dims only when (a) it explicitly lost an incoming edge whose source granim could see was rooted, and (b) nothing reachable holds it — where "held" means: a struct/container field, a live local variable, a node-valued argument, or the function's return value. Plain invisible containers never cause dimming.

Layout. Arrays/matrices are grids; lists are chains in creation order (stable: nodes hold still while arrows flip; long lists snake-wrap); trees are tidy; graphs are layered (DAG) or force-directed (cyclic, seeded). Positions never lurch between steps.

Limits. 20,000 events / 500 nodes / 3,000 steps — beyond that an animation is unwatchable, so granim raises a GranimError naming the hot source line instead of producing one.

7. Gotchas

  • is comparisons (slow is fast) are invisible to instrumentation — Python gives no hook. The meeting still shows: both badges land on the same node.
  • Builtins like deque/dict/list are untracked; use them freely for bookkeeping, but only granim structures and decorated classes animate.
  • Functions must live in the same file as the @ga.animate function to get per-line steps and stack frames; imported helpers still animate correctly but coarser (their events merge into the calling line's beat).
  • List layout follows node creation order — insert-at-head reads as a backward arc rather than a reshuffle.
  • Keep inputs teaching-sized (≲ 30 nodes); that's the point of the tool.

8. Gallery

The examples/ folder is the feature tour — each file is a dozen lines: binary_search, reverse_recursive (stack panel + flips on unwind), reverse_iterative, bst, bfs (parallel frontier), dedup (orphans dim), floyd (cycle + badges meeting), flood_fill (matrix), dijkstra (weights), quicksort, edit_distance (DP), custom_node (@ga.node, LeetCode 138), queue (@ga.container).

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

granim_viz-1.0.1.tar.gz (44.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

granim_viz-1.0.1-py3-none-any.whl (43.2 kB view details)

Uploaded Python 3

File details

Details for the file granim_viz-1.0.1.tar.gz.

File metadata

  • Download URL: granim_viz-1.0.1.tar.gz
  • Upload date:
  • Size: 44.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for granim_viz-1.0.1.tar.gz
Algorithm Hash digest
SHA256 aabb9b7c7508e65c06478bc97a9c4fe962ad3d8b35aa9dc9430c9275e8e50de9
MD5 404bcf8365ab7af2454342ba38118603
BLAKE2b-256 cf7a1a2b10fb62effc12aea102241a17b3f4c8d6e5ec074d10c951f7205e7f4e

See more details on using hashes here.

Provenance

The following attestation bundles were made for granim_viz-1.0.1.tar.gz:

Publisher: publish.yml on MusaibBashir/granim-viz

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file granim_viz-1.0.1-py3-none-any.whl.

File metadata

  • Download URL: granim_viz-1.0.1-py3-none-any.whl
  • Upload date:
  • Size: 43.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for granim_viz-1.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 138a1b3f3fc504aaacad14aae31d0daa758c929435dc3e95c6c65181e416f9df
MD5 a9e954d64c8e893efffd222ed4f0f874
BLAKE2b-256 9207b1d4b58c6cb245c997b9f3ed62b43da77680218c6738ddd3899b6ada226b

See more details on using hashes here.

Provenance

The following attestation bundles were made for granim_viz-1.0.1-py3-none-any.whl:

Publisher: publish.yml on MusaibBashir/granim-viz

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page