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.1.0.tar.gz (48.1 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.1.0-py3-none-any.whl (45.7 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: granim_viz-1.1.0.tar.gz
  • Upload date:
  • Size: 48.1 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.1.0.tar.gz
Algorithm Hash digest
SHA256 7b73e038093d762ebbcded26b587d4fff8a420d2336b09d54f957e7536c9ff47
MD5 49c85e75d8c0e4c7481815063cda480d
BLAKE2b-256 72daa1343190cec46c430575883a80e38a11c08e4366d07923313b8bd80312af

See more details on using hashes here.

Provenance

The following attestation bundles were made for granim_viz-1.1.0.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.1.0-py3-none-any.whl.

File metadata

  • Download URL: granim_viz-1.1.0-py3-none-any.whl
  • Upload date:
  • Size: 45.7 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.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 6cf2ba0940d6ca4e8fe969d0668a0bff251e90f7eb84d7fdafd2f0ee13355ea2
MD5 829dd9dc688d49852f269b6a597cf4aa
BLAKE2b-256 bb56fdcaaa1585541f7d877688aa7773ca1829f418cbe682111fc005829b15bb

See more details on using hashes here.

Provenance

The following attestation bundles were made for granim_viz-1.1.0-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