granim-viz 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. granim_viz-0.1.0/LICENSE +21 -0
  2. granim_viz-0.1.0/PKG-INFO +120 -0
  3. granim_viz-0.1.0/README.md +89 -0
  4. granim_viz-0.1.0/pyproject.toml +48 -0
  5. granim_viz-0.1.0/setup.cfg +4 -0
  6. granim_viz-0.1.0/src/granim/__init__.py +107 -0
  7. granim_viz-0.1.0/src/granim/core/__init__.py +0 -0
  8. granim_viz-0.1.0/src/granim/core/events.py +44 -0
  9. granim_viz-0.1.0/src/granim/core/recorder.py +260 -0
  10. granim_viz-0.1.0/src/granim/core/steps.py +107 -0
  11. granim_viz-0.1.0/src/granim/core/trace.py +38 -0
  12. granim_viz-0.1.0/src/granim/layout/__init__.py +78 -0
  13. granim_viz-0.1.0/src/granim/layout/force.py +69 -0
  14. granim_viz-0.1.0/src/granim/layout/layered.py +66 -0
  15. granim_viz-0.1.0/src/granim/layout/linear.py +28 -0
  16. granim_viz-0.1.0/src/granim/layout/tidy.py +141 -0
  17. granim_viz-0.1.0/src/granim/render/__init__.py +0 -0
  18. granim_viz-0.1.0/src/granim/render/compiler.py +260 -0
  19. granim_viz-0.1.0/src/granim/render/html.py +33 -0
  20. granim_viz-0.1.0/src/granim/render/player/player.css +151 -0
  21. granim_viz-0.1.0/src/granim/render/player/player.js +442 -0
  22. granim_viz-0.1.0/src/granim/render/player/template.html +37 -0
  23. granim_viz-0.1.0/src/granim/structures/__init__.py +0 -0
  24. granim_viz-0.1.0/src/granim/structures/array.py +70 -0
  25. granim_viz-0.1.0/src/granim/structures/base.py +135 -0
  26. granim_viz-0.1.0/src/granim/structures/custom.py +119 -0
  27. granim_viz-0.1.0/src/granim/structures/graph.py +85 -0
  28. granim_viz-0.1.0/src/granim/structures/linked_list.py +115 -0
  29. granim_viz-0.1.0/src/granim/structures/matrix.py +99 -0
  30. granim_viz-0.1.0/src/granim/structures/tracked.py +68 -0
  31. granim_viz-0.1.0/src/granim/structures/tree.py +114 -0
  32. granim_viz-0.1.0/src/granim/themes.py +64 -0
  33. granim_viz-0.1.0/src/granim_viz.egg-info/PKG-INFO +120 -0
  34. granim_viz-0.1.0/src/granim_viz.egg-info/SOURCES.txt +36 -0
  35. granim_viz-0.1.0/src/granim_viz.egg-info/dependency_links.txt +1 -0
  36. granim_viz-0.1.0/src/granim_viz.egg-info/requires.txt +4 -0
  37. granim_viz-0.1.0/src/granim_viz.egg-info/top_level.txt +1 -0
  38. granim_viz-0.1.0/tests/test_granim.py +523 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Musaib Bashir
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,120 @@
1
+ Metadata-Version: 2.4
2
+ Name: granim-viz
3
+ Version: 0.1.0
4
+ Summary: Write the algorithm, get the animation - instrumented data structures that compile your real Python code into interactive HTML visualizations.
5
+ Author-email: Musaib Bashir <musaibbashir.24@kgpian.iitkgp.ac.in>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/musaibbashir/granim
8
+ Project-URL: Repository, https://github.com/musaibbashir/granim
9
+ Project-URL: Issues, https://github.com/musaibbashir/granim/issues
10
+ Keywords: visualization,animation,algorithms,data-structures,education,linked-list,graph,tree,manim,teaching
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Education
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Education
22
+ Classifier: Topic :: Scientific/Engineering :: Visualization
23
+ Classifier: Topic :: Software Development :: Debuggers
24
+ Requires-Python: >=3.10
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest; extra == "dev"
29
+ Requires-Dist: ruff; extra == "dev"
30
+ Dynamic: license-file
31
+
32
+ # granim
33
+
34
+ Write the algorithm. Get the animation.
35
+
36
+ granim instruments its data structures so that running your *normal* Python code
37
+ produces an interactive, self-contained HTML animation — nodes appear as you
38
+ build, arrows flip as you reverse, frontiers light up in parallel, and debug mode
39
+ shows every variable hopping between nodes.
40
+
41
+ ```python
42
+ import granim as ga
43
+
44
+ @ga.animate(debug=True)
45
+ def reverse(node):
46
+ if node is None or node.next is None:
47
+ return node
48
+ new_head = reverse(node.next)
49
+ node.next.next = node
50
+ node.next = None
51
+ return new_head
52
+
53
+ reverse(ga.linked_list([1, 2, 3, 4, 5]).head) # -> reverse.html, opens in browser
54
+ ```
55
+
56
+ That's the entire integration: one decorator, one constructor. The recursion walks
57
+ to null while the call stack panel fills; on the way back, each arrow visibly
58
+ reverses (granim classifies `node.next.next = node` as an *edge flip*, not a
59
+ delete+add).
60
+
61
+ ## What you get
62
+
63
+ - **Automatic steps** — each loop iteration is one animation beat; same-line
64
+ mutations (a BFS frontier) merge into one *parallel* beat. No annotations.
65
+ - **Debug mode** (`debug=True`) — all locals in a side panel, plus on-canvas
66
+ badges: node-valued variables dock to their node, ints near an array become
67
+ index pointers (`lo`/`hi`/`mid` under the cells).
68
+ - **Five structures** — `ga.array`, `ga.matrix`, `ga.linked_list`, `ga.tree`,
69
+ `ga.graph`, all usable (and unit-testable) without recording. Matrix cells
70
+ animate values (`m[i][j] = v`) and colors (`m[i][j].state = "visited"`).
71
+ - **Your own classes** — `@ga.node(value="val")` instruments any class without
72
+ inheritance: node-valued fields become labeled edges, the value field animates,
73
+ everything else is plain storage. Ad-hoc fields on granim nodes work too
74
+ (`node.random`, `node.child` arc underneath with their name on them).
75
+ `@ga.container` does the same for wrappers (queues, custom lists): `head`/
76
+ `tail`-style fields render as floating badges and root the garbage pass, so
77
+ dropped nodes dim out.
78
+ - **Auto layout** — tidy trees (Buchheim), layered DAGs, force-directed graphs,
79
+ snake-wrapped lists; positions stay stable between steps instead of jumping.
80
+ - **One file out** — interactive player (play/pause/step/scrub/speed) in a single
81
+ offline HTML file. Jupyter renders inline automatically.
82
+ - **Zero dependencies.**
83
+
84
+ ## Examples
85
+
86
+ `examples/` doubles as the acceptance suite:
87
+
88
+ | file | shows off |
89
+ |---|---|
90
+ | `binary_search.py` | auto index badges, comparison pulses |
91
+ | `reverse_recursive.py` | call stack + arrows flipping on unwind |
92
+ | `reverse_iterative.py` | `prev`/`cur`/`nxt` badges hopping |
93
+ | `bst.py` | tidy tree re-flowing as nodes attach |
94
+ | `bfs.py` | parallel frontier steps, state coloring |
95
+ | `dedup.py` | unlinked nodes dim out (reachability pass) |
96
+ | `floyd.py` | tortoise/hare badges meeting; cycle arc |
97
+ | `flood_fill.py` | matrix: values + colors spreading, deep recursion |
98
+ | `dijkstra.py` | weighted edges, frontier/visited/done coloring |
99
+ | `quicksort.py` | pivot glow, swaps, sorted cells locking in |
100
+ | `edit_distance.py` | DP table filling, dependency-cell pulses |
101
+ | `custom_node.py` | `@ga.node` on a user-owned class (LeetCode 138) |
102
+ | `queue.py` | `@ga.container`: head/tail badges, dequeued nodes dim |
103
+
104
+ Run any of them: `PYTHONPATH=src python examples/bfs.py` → open the HTML next to it.
105
+
106
+ ## Tests
107
+
108
+ ```
109
+ python tests/run_tests.py # pure-python pipeline tests (pytest also works)
110
+ node tests/verify_html.js # replays each example's timeline, checks end state
111
+ ```
112
+
113
+ ## Design
114
+
115
+ `docs/PLAN.md` holds the rationale, `docs/SPEC.md` the contracts. The short version:
116
+ structures emit a small vocabulary of semantic events; a step builder groups them
117
+ into beats (line-scoped via a trace limited to your function); a pure compiler
118
+ classifies edge flips, computes layout keyframes, and serializes a timeline that
119
+ a dependency-free JS player animates. The trace can only ever coarsen steps —
120
+ animation truth comes from the structures.
@@ -0,0 +1,89 @@
1
+ # granim
2
+
3
+ Write the algorithm. Get the animation.
4
+
5
+ granim instruments its data structures so that running your *normal* Python code
6
+ produces an interactive, self-contained HTML animation — nodes appear as you
7
+ build, arrows flip as you reverse, frontiers light up in parallel, and debug mode
8
+ shows every variable hopping between nodes.
9
+
10
+ ```python
11
+ import granim as ga
12
+
13
+ @ga.animate(debug=True)
14
+ def reverse(node):
15
+ if node is None or node.next is None:
16
+ return node
17
+ new_head = reverse(node.next)
18
+ node.next.next = node
19
+ node.next = None
20
+ return new_head
21
+
22
+ reverse(ga.linked_list([1, 2, 3, 4, 5]).head) # -> reverse.html, opens in browser
23
+ ```
24
+
25
+ That's the entire integration: one decorator, one constructor. The recursion walks
26
+ to null while the call stack panel fills; on the way back, each arrow visibly
27
+ reverses (granim classifies `node.next.next = node` as an *edge flip*, not a
28
+ delete+add).
29
+
30
+ ## What you get
31
+
32
+ - **Automatic steps** — each loop iteration is one animation beat; same-line
33
+ mutations (a BFS frontier) merge into one *parallel* beat. No annotations.
34
+ - **Debug mode** (`debug=True`) — all locals in a side panel, plus on-canvas
35
+ badges: node-valued variables dock to their node, ints near an array become
36
+ index pointers (`lo`/`hi`/`mid` under the cells).
37
+ - **Five structures** — `ga.array`, `ga.matrix`, `ga.linked_list`, `ga.tree`,
38
+ `ga.graph`, all usable (and unit-testable) without recording. Matrix cells
39
+ animate values (`m[i][j] = v`) and colors (`m[i][j].state = "visited"`).
40
+ - **Your own classes** — `@ga.node(value="val")` instruments any class without
41
+ inheritance: node-valued fields become labeled edges, the value field animates,
42
+ everything else is plain storage. Ad-hoc fields on granim nodes work too
43
+ (`node.random`, `node.child` arc underneath with their name on them).
44
+ `@ga.container` does the same for wrappers (queues, custom lists): `head`/
45
+ `tail`-style fields render as floating badges and root the garbage pass, so
46
+ dropped nodes dim out.
47
+ - **Auto layout** — tidy trees (Buchheim), layered DAGs, force-directed graphs,
48
+ snake-wrapped lists; positions stay stable between steps instead of jumping.
49
+ - **One file out** — interactive player (play/pause/step/scrub/speed) in a single
50
+ offline HTML file. Jupyter renders inline automatically.
51
+ - **Zero dependencies.**
52
+
53
+ ## Examples
54
+
55
+ `examples/` doubles as the acceptance suite:
56
+
57
+ | file | shows off |
58
+ |---|---|
59
+ | `binary_search.py` | auto index badges, comparison pulses |
60
+ | `reverse_recursive.py` | call stack + arrows flipping on unwind |
61
+ | `reverse_iterative.py` | `prev`/`cur`/`nxt` badges hopping |
62
+ | `bst.py` | tidy tree re-flowing as nodes attach |
63
+ | `bfs.py` | parallel frontier steps, state coloring |
64
+ | `dedup.py` | unlinked nodes dim out (reachability pass) |
65
+ | `floyd.py` | tortoise/hare badges meeting; cycle arc |
66
+ | `flood_fill.py` | matrix: values + colors spreading, deep recursion |
67
+ | `dijkstra.py` | weighted edges, frontier/visited/done coloring |
68
+ | `quicksort.py` | pivot glow, swaps, sorted cells locking in |
69
+ | `edit_distance.py` | DP table filling, dependency-cell pulses |
70
+ | `custom_node.py` | `@ga.node` on a user-owned class (LeetCode 138) |
71
+ | `queue.py` | `@ga.container`: head/tail badges, dequeued nodes dim |
72
+
73
+ Run any of them: `PYTHONPATH=src python examples/bfs.py` → open the HTML next to it.
74
+
75
+ ## Tests
76
+
77
+ ```
78
+ python tests/run_tests.py # pure-python pipeline tests (pytest also works)
79
+ node tests/verify_html.js # replays each example's timeline, checks end state
80
+ ```
81
+
82
+ ## Design
83
+
84
+ `docs/PLAN.md` holds the rationale, `docs/SPEC.md` the contracts. The short version:
85
+ structures emit a small vocabulary of semantic events; a step builder groups them
86
+ into beats (line-scoped via a trace limited to your function); a pure compiler
87
+ classifies edge flips, computes layout keyframes, and serializes a timeline that
88
+ a dependency-free JS player animates. The trace can only ever coarsen steps —
89
+ animation truth comes from the structures.
@@ -0,0 +1,48 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "granim-viz"
7
+ version = "0.1.0"
8
+ description = "Write the algorithm, get the animation - instrumented data structures that compile your real Python code into interactive HTML visualizations."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Musaib Bashir", email = "musaibbashir.24@kgpian.iitkgp.ac.in" }]
13
+ keywords = [
14
+ "visualization", "animation", "algorithms", "data-structures",
15
+ "education", "linked-list", "graph", "tree", "manim", "teaching",
16
+ ]
17
+ classifiers = [
18
+ "Development Status :: 4 - Beta",
19
+ "Intended Audience :: Developers",
20
+ "Intended Audience :: Education",
21
+ "License :: OSI Approved :: MIT License",
22
+ "Operating System :: OS Independent",
23
+ "Programming Language :: Python :: 3",
24
+ "Programming Language :: Python :: 3.10",
25
+ "Programming Language :: Python :: 3.11",
26
+ "Programming Language :: Python :: 3.12",
27
+ "Programming Language :: Python :: 3.13",
28
+ "Topic :: Education",
29
+ "Topic :: Scientific/Engineering :: Visualization",
30
+ "Topic :: Software Development :: Debuggers",
31
+ ]
32
+
33
+ [project.urls]
34
+ Homepage = "https://github.com/musaibbashir/granim"
35
+ Repository = "https://github.com/musaibbashir/granim"
36
+ Issues = "https://github.com/musaibbashir/granim/issues"
37
+
38
+ [project.optional-dependencies]
39
+ dev = ["pytest", "ruff"]
40
+
41
+ [tool.setuptools.packages.find]
42
+ where = ["src"]
43
+
44
+ [tool.setuptools.package-data]
45
+ "granim.render" = ["player/*"]
46
+
47
+ [tool.ruff]
48
+ line-length = 100
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,107 @@
1
+ """granim — write the algorithm, get the animation.
2
+
3
+ import granim as ga
4
+
5
+ @ga.animate(debug=True)
6
+ def rev(node, prev=None):
7
+ if node is None:
8
+ return prev
9
+ nxt = node.next
10
+ node.next = prev
11
+ return rev(nxt, node)
12
+
13
+ rev(ga.linked_list([1, 2, 3, 4, 5]).head) # -> rev.html
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import functools
18
+ import sys
19
+ import webbrowser
20
+ from pathlib import Path
21
+
22
+ from .core.recorder import GranimError, Index, Recorder, active
23
+ from .core.trace import Trace
24
+ from .structures.array import Array
25
+ from .structures.graph import Graph, GraphNode
26
+ from .structures.linked_list import LinkedList, ListNode
27
+ from .structures.custom import container, node
28
+ from .structures.matrix import Matrix
29
+ from .structures.tracked import Tracked
30
+ from .structures.tree import Tree, TreeNode
31
+
32
+ __version__ = "0.1.0"
33
+ __all__ = [
34
+ "animate", "record", "array", "matrix", "linked_list", "tree", "graph",
35
+ "node", "container", "index", "GranimError", "ListNode", "TreeNode",
36
+ "GraphNode",
37
+ ]
38
+
39
+ array = Array
40
+ matrix = Matrix
41
+ linked_list = LinkedList
42
+ tree = Tree
43
+ graph = Graph
44
+ index = Index
45
+ record = Recorder
46
+
47
+
48
+ def animate(fn=None, *, debug=False, theme="dark", out=None, show=None,
49
+ speed=1.0, title=None):
50
+ """SPEC §2.1. Decorate a function; calling it records, compiles, saves, and
51
+ opens the animation. Returns the function's result unchanged."""
52
+ if fn is None:
53
+ return functools.partial(animate, debug=debug, theme=theme, out=out,
54
+ show=show, speed=speed, title=title)
55
+
56
+ @functools.wraps(fn)
57
+ def wrapper(*args, **kwargs):
58
+ rec = active()
59
+ if rec is not None:
60
+ if rec._owner is wrapper: # recursion through the decorated name
61
+ return fn(*args, **kwargs)
62
+ raise GranimError("nested recording: another @animate function is running")
63
+
64
+ rec = Recorder(debug=debug, theme=theme, title=title or fn.__name__,
65
+ speed=speed, owner=wrapper)
66
+ from .structures.base import is_node
67
+ rec.ext_roots = [a._id for a in (*args, *kwargs.values()) if is_node(a)]
68
+ result, err = None, None
69
+ with rec:
70
+ try:
71
+ with Trace(rec, fn.__code__):
72
+ result = fn(*args, **kwargs)
73
+ except Exception as e: # finalize + save, then re-raise (SPEC §2.1)
74
+ err = e
75
+ rec.error = f"{type(e).__name__}: {e}"
76
+ if is_node(result):
77
+ rec.ext_roots.append(result._id)
78
+
79
+ path = _out_path(out, fn)
80
+ rec.save(path)
81
+ if err is not None:
82
+ sys.stderr.write(f"granim: saved partial animation to {path}\n")
83
+ raise err
84
+ _present(rec, path, show, out)
85
+ return result._v if isinstance(result, Tracked) else result
86
+
87
+ return wrapper
88
+
89
+
90
+ def _out_path(out, fn) -> Path:
91
+ if out is not None:
92
+ return Path(out)
93
+ src = Path(fn.__code__.co_filename)
94
+ base = src.parent if src.is_file() else Path.cwd()
95
+ return base / f"{fn.__name__}.html"
96
+
97
+
98
+ def _present(rec, path, show, out) -> None:
99
+ in_jupyter = "ipykernel" in sys.modules
100
+ if show is None:
101
+ show = out is None
102
+ if not show:
103
+ return
104
+ if in_jupyter:
105
+ rec.show()
106
+ else:
107
+ webbrowser.open(path.resolve().as_uri())
File without changes
@@ -0,0 +1,44 @@
1
+ """Event model. SPEC §3: a small, closed vocabulary; everything else is derived."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+
6
+ KINDS = frozenset({
7
+ "node_add", "node_remove", "edge_set", "read", "compare",
8
+ "value_set", "state_set", "var_set",
9
+ })
10
+
11
+ # Kinds that change structure (drive layout keyframes). edge_flip is assigned by
12
+ # the compiler, never emitted.
13
+ STRUCTURAL = frozenset({"node_add", "node_remove", "edge_set"})
14
+ # Kinds eligible for parallel batch-merging (SPEC §5 rule 2).
15
+ BATCHABLE = frozenset({"state_set", "node_add"})
16
+ PASSIVE = frozenset({"read", "compare"})
17
+
18
+ REPR_MAX = 60
19
+
20
+
21
+ @dataclass(frozen=True, slots=True)
22
+ class Event:
23
+ seq: int
24
+ kind: str
25
+ payload: dict
26
+ depth: int
27
+ line: int | None
28
+ frame: int
29
+
30
+
31
+ def safe_repr(value) -> str:
32
+ from ..structures.base import NodeBase
33
+ from ..structures.tracked import Tracked
34
+
35
+ if isinstance(value, Tracked):
36
+ value = value._v
37
+ if isinstance(value, NodeBase):
38
+ r = f"Node({value._value!r})"
39
+ else:
40
+ try:
41
+ r = repr(value)
42
+ except Exception:
43
+ r = f"<{type(value).__name__}>"
44
+ return r if len(r) <= REPR_MAX else r[: REPR_MAX - 1] + "…"