Skip to main content

Self-rendering data structures for visualizing algorithms in the terminal

Project description

AlgoViz

Data structures that draw themselves as your algorithm runs.

CI PyPI Python Downloads License Ruff uv Checked with mypy

Quick start · Structures · Demos · Configuration · Upgrading


Wrap a data structure, run your algorithm unchanged, and watch every step render in your terminal with the cells you touched highlighted. Reads light up in one colour, writes in another, and each frame shows only what that step touched.

No decorators. No callbacks. No rewriting your solution to fit a framework.

from algoviz import VizList

dp = VizList([0] * 4, title_name='Coin Change')
dp[0] = 1
for coin in (1, 2):
    for value in range(coin, 4):
        dp[value] += dp[value - coin]
Coin Change Init
┏━━━┳━━━┳━━━┳━━━┓
┃ 0 ┃ 1 ┃ 2 ┃ 3 ┃
┡━━━╇━━━╇━━━╇━━━┩
│ 0 │ 0 │ 0 │ 0 │
└───┴───┴───┴───┘
   Coin Change
┏━━━┳━━━┳━━━┳━━━┓
┃ 0 ┃ 1 ┃ 2 ┃ 3 ┃
┡━━━╇━━━╇━━━╇━━━┩
│ 1 │ 0 │ 0 │ 0 │
└───┴───┴───┴───┘

Why

Reading a DP recurrence tells you what the code does. It does not tell you what the table looks like three iterations in, which is where the bug usually is. AlgoViz prints that table on every write, so the state is in front of you instead of in your head.

It is built for the moment you are stuck on a problem, teaching someone who is, or writing up a solution and want the intermediate states to show.

Installation

pip install algoviz
uv add algoviz

Requires Python 3.10 or newer. The only dependency is rich.

Quick start

Every structure takes the data you already have and hands back something that behaves the same way:

from algoviz import VizList, VizStack, VizHeap, VizGrid

VizList([0] * 5)              # 1D and 2D tables
VizStack()                    # LIFO, top always labelled
VizHeap([5, 3, 8], max_heap=True)
VizGrid(['110', '011'])       # terrain plus a visited/queued overlay

The structures

Class Backed by Reach for it when
VizList MutableSequence 1D and 2D DP tables, prefix sums, any array problem
VizStack list Valid parentheses, monotonic stack, next greater element
VizQueue deque BFS, level-order traversal, shortest path
VizDeque deque Sliding window maximum, monotonic deque
VizHeap heapq Top-K, merge K lists, task scheduler, running median
VizDict MutableMapping Index maps, memoisation, seen-value lookups
VizCounter MutableMapping Frequency maps, group anagrams, sliding windows
VizSet MutableSet Seen-sets, cycle detection, longest consecutive
VizGrid list[list] Islands, flood fill, word search, shortest path
VizLinkedList ListNode Reverse a list, cycle detection, two pointers
VizTree TreeNode Binary tree and BST traversals
VizTrie prefix tree Word search, autocomplete, word break
VizUnionFind rank + path compression Connected components, redundant connection

ListNode and TreeNode use the same .val / .next / .left / .right shape LeetCode uses, so existing solutions paste in unchanged.

Implementing it yourself

When the structure is the exercise, the library gets out of the way. Each of these hands you the real nodes so your own code does the walking, and a hook to light up the step you are on:

Structure Nodes Drive the animation with
VizTree root_node, TreeNode visit(node)
VizTrie root_node, TrieNode visit(node, writing=...)
VizLinkedList head, nodes(), ListNode set_pointer(name, node)
VizList positions set_pointer(name, index)
from algoviz import TrieNode, VizTrie

viz = VizTrie(title_name='Trie')
node = viz.root_node
for char in 'cat':               # your insert, not the library's
    node.children.setdefault(char, TrieNode())
    node = node.children[char]
    viz.visit(node, writing=True)
node.is_word = True

Sizing note for VizTrie. Keep it to roughly a dozen short words. The render is one line per node, and a trie's node count grows with the total number of characters, not the number of words, so 20 words of 5 to 8 letters is already about 62 nodes and fills a terminal in a single frame. The other structures are far less dense: 20 values in a VizHeap is 20 nodes, and in a VizList it is 3 lines. Nothing breaks past that point, it just stops being readable.

Demo gallery

Thirty runnable solutions live in demo/. Every one is executed and checked against the correct answer in CI, so none of them can rot.

Each demo writes its own algorithm. Where the library offers a shortcut that would be the answer, the demo deliberately does not call it: the level-order demo drives its own queue, the top-K demo maintains its own bounded heap, and the trie demo builds its own nodes.

python demo/num_islands.py

Arrays and dynamic programming

Demo Problem Idea
climbing_stairs.py 70 Climbing Stairs The smallest interesting DP
coin_change.py 518 Coin Change II Count the ways to make an amount
house_robber.py 198 House Robber Skip-or-take DP
jump.py 45 Jump Game II Fewest jumps to the last index
count_palin.py 647 Palindromic Substrings Expand a 2D table
longest_common_subsequence.py 1143 LCS The canonical 2D table

Two pointers and prefix sums

Demo Problem Idea
dutch_national_flag.py 75 Sort Colors Three-pointer partition in one pass
container_with_most_water.py 11 Container With Most Water Converge from both ends
three_sum.py 15 3Sum Sort, then two-pointer the rest
subarray_sum_equals_k.py 560 Subarray Sum Equals K Prefix sums in a map

Stacks, queues, and deques

Demo Problem Idea
valid_parentheses.py 20 Valid Parentheses The classic stack warm-up
daily_temperatures.py 739 Daily Temperatures A monotonic stack of indices
min_stack.py 155 Min Stack Constant-time minimum via a paired stack
sliding_window_max.py 239 Sliding Window Maximum The monotonic deque
rotting_oranges.py 994 Rotting Oranges BFS measured in levels

Heaps, maps, and sets

Demo Problem Idea
kth_largest.py 215 Kth Largest A bounded min-heap
top_k_frequent.py 347 Top K Frequent A heap capped at k
two_sum.py 1 Two Sum The hash-map one-pass
group_anagrams.py 49 Group Anagrams Bucket by sorted letters
longest_consecutive.py 128 Longest Consecutive A set, not a sort

Grids, lists, trees, and graphs

Demo Problem Idea
num_islands.py 200 Number of Islands BFS over a grid overlay
flood_fill.py 733 Flood Fill Recolour a connected region
reverse_linked_list.py 206 Reverse Linked List Pointer surgery, one node at a time
linked_list_cycle.py 141 Linked List Cycle Floyd's tortoise and hare
binary_tree_level_order.py 102 Level Order Drive the BFS queue yourself
validate_bst.py 98 Validate BST Bounds, not just parents
implement_trie.py 208 Implement Trie Walk and build the nodes yourself
word_break.py 139 Word Break A trie plus a DP sweep
number_of_provinces.py 547 Number of Provinces Union-find over a matrix
redundant_connection.py 684 Redundant Connection The edge that closes a cycle

Highlights

Two pointers

VizList takes named pointers by index, so the labels track your loop variables and the invariant is visible instead of implied.

from algoviz import VizList

colors = VizList([2, 0, 2, 1, 1, 0], title_name='Sort Colors')
colors.set_pointer('low', 0)
colors.set_pointer('mid', 0)
colors.set_pointer('high', 5)
            Sort Colors
┏━━━━━━━━━━┳━━━┳━━━┳━━━┳━━━┳━━━━━━━┓
┃ 0        ┃ 1 ┃ 2 ┃ 3 ┃ 4 ┃ 5     ┃
┡━━━━━━━━━━╇━━━╇━━━╇━━━╇━━━╇━━━━━━━┩
│ 2        │ 0 │ 2 │ 1 │ 1 │ 0     │
│ ↑low,mid │   │   │   │   │ ↑high │
└──────────┴───┴───┴───┴───┴───────┘

A pointer that runs past the end is remembered but not drawn, so loops that terminate by walking off the array need no special-casing.

Stack

The top is always labelled, and a popped value stays visible for exactly one frame so you can see what left.

from algoviz import VizStack

stack = VizStack(title_name='Parens')
for char in '([])':
    stack.push(char) if char in '([' else stack.pop()
       Parens
┏━━━━━━━━━━┳━━━━━━━┓
┃          ┃ value ┃
┡━━━━━━━━━━╇━━━━━━━┩
│ ✗ popped │ [     │
│ ← top    │ (     │
└──────────┴───────┘

Heap

Renders the backing array and the tree it represents, which is the part a flat list never shows you.

from algoviz import VizHeap

heap = VizHeap([5, 3, 8, 1], title_name='Min Heap')
heap.push(2)
      Min Heap
┏━━━┳━━━┳━━━┳━━━┳━━━┓
┃ 0 ┃ 1 ┃ 2 ┃ 3 ┃ 4 ┃
┡━━━╇━━━╇━━━╇━━━╇━━━┩
│ 1 │ 2 │ 8 │ 5 │ 3 │
└───┴───┴───┴───┴───┘
Min Heap (tree)
1
├── 2
│   ├── 5
│   └── 3
└── 8

Ordering comes straight from the standard library's heapq, so what you see is the real thing rather than a reimplementation. Pass max_heap=True for a max-heap.

Grid

Grid cells usually hold terrain. What matters is the overlay: which cells are visited, queued, or on the current path.

from algoviz import VizGrid

grid = VizGrid(['11000', '11000', '00100'], title_name='Islands')
grid.mark_start(0, 0)
for row, col in grid.neighbors(0, 0):
    grid.mark_queued(row, col)

neighbors() handles bounds for you, diagonal=True gives all eight, and grid.batch() suspends redrawing so you get one frame per BFS level instead of one per cell.

Linked list

Named pointers make two-pointer and cycle problems legible, and a cyclic list renders its back-edge instead of hanging.

from algoviz import VizLinkedList

linked = VizLinkedList([1, 2, 3, 4, 5])
slow = fast = linked.head
while fast and fast.next:
    slow, fast = slow.next, fast.next.next
    linked.set_pointer('slow', slow)
    linked.set_pointer('fast', fast)

Union-find

Path compression and union by rank are both implemented, and the render shows each element's parent alongside the current components, so you can watch compression flatten the tree.

from algoviz import VizUnionFind

uf = VizUnionFind(5)
uf.union(0, 1)
uf.union(3, 4)
uf.component_count  # 3

Configuration

Every structure accepts the same settings, as keyword overrides or a shared VizConfig:

from algoviz import VizConfig, VizList

VizList([1, 2, 3], get_color='green', set_color='magenta', sleep_time=0.3)

quiet = VizConfig(auto_print=False, show_init=False)
dp = VizList([0] * 5, config=quiet)
dp[2] = 7
dp.show()  # draw only when you ask
Setting Default Meaning
get_color blue Colour for cells that were read
set_color red Colour for cells that were written
sleep_time 0 Seconds to pause after each frame
auto_print True Redraw automatically after every write
show_init True Draw the initial state on construction
show_header True Show the index header row

Set sleep_time and let it run to get an animation. Set auto_print=False and call .show() yourself to control exactly which frames appear.

Development

This project uses uv.

uv sync             # create the environment
uv run pytest       # 584 tests
uv run pytest --cov # with coverage
uv run ruff check   # lint
uv run ruff format  # format
uv run mypy         # type check
uv build            # build sdist and wheel

CI runs the suite on Python 3.10 through 3.14 across Linux, macOS, and Windows, plus lint, format, types, and a packaging check.

Upgrading from 0.2.x

Version 0.3.0 rewrote the core. Three changes can affect existing code:

  1. VizList is a MutableSequence, not a list subclass. It behaves like a list everywhere that matters, but isinstance(dp, list) is now False. The old inheritance was broken regardless: the real list storage was always empty, so anything reading it saw the wrong data. Use dp.data when you need a plain list.
  2. a + b no longer mutates a. The old __add__ called extend, so concatenation modified the left operand in place. It now returns a new list, as + should.
  3. print(expr) is sandboxed. Expressions still use # for the underlying data, but they evaluate with no builtins and no access to internals.

Python 3.9 users can stay on 0.2.3.

Contributing

Issues and pull requests are welcome. Please run uv run pytest, uv run ruff check, and uv run mypy before opening one. New demos should include their expected output in tests/test_demos.py, which is what keeps the gallery honest.

License

MIT. See LICENSE.txt.

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

algoviz-0.4.0.tar.gz (74.7 kB view details)

Uploaded Source

Built Distribution

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

algoviz-0.4.0-py3-none-any.whl (50.6 kB view details)

Uploaded Python 3

File details

Details for the file algoviz-0.4.0.tar.gz.

File metadata

  • Download URL: algoviz-0.4.0.tar.gz
  • Upload date:
  • Size: 74.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for algoviz-0.4.0.tar.gz
Algorithm Hash digest
SHA256 227a8c5f38177df883ed4583d36a570520804f9454762f38a92f71d57de816bf
MD5 ef41ffb8d76aa025596577ec4caa695f
BLAKE2b-256 58b739b96f7457989bc8875178d2e4900f014431ed80a464f93dcdaafa037e2d

See more details on using hashes here.

Provenance

The following attestation bundles were made for algoviz-0.4.0.tar.gz:

Publisher: release.yml on algometrix/algoviz

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

File details

Details for the file algoviz-0.4.0-py3-none-any.whl.

File metadata

  • Download URL: algoviz-0.4.0-py3-none-any.whl
  • Upload date:
  • Size: 50.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for algoviz-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7eb9a5e7300edf2bfead0c6f0c7fedc0a5896aca8c50186a5437cc00eeb792f3
MD5 b2eb445b6489319e1f9af42c88418a06
BLAKE2b-256 09e9c95822f1b2d218fdc9b494613bf525f8d93c2ac35ec5daaab7e60c2a997f

See more details on using hashes here.

Provenance

The following attestation bundles were made for algoviz-0.4.0-py3-none-any.whl:

Publisher: release.yml on algometrix/algoviz

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