Step-by-step visual animations of classic sorting, searching, tree and data-structure algorithms, built with vanilla JavaScript (ES modules) and served by Node.js + Express.
Every algorithm is a class. Registering it makes its category tab, its button, its info badges, its legend and its animation appear automatically — the UI adapts to the registry.
npm install
npm startOpens at http://localhost:4000/algorithms.html. Override the port with PORT=xxxx npm start.
It must be served over HTTP (the code uses ES modules, which browsers refuse to load from a
file:// path), so open it through the server rather than double-clicking the HTML.
The category tabs and algorithm buttons are generated from the registry, so this list is exactly what ships today:
| Algorithm | Time | Space | Idea |
|---|---|---|---|
| Bubble Sort | O(n²) | O(1) | Swaps adjacent out-of-order pairs; the largest value bubbles to the end each pass. |
| Selection Sort | O(n²) | O(1) | Selects the minimum of the unsorted part and grows a sorted prefix one slot at a time. |
| Insertion Sort | O(n²) | O(1) | Inserts each element into its correct place in the already-sorted prefix. |
| Merge Sort | O(n log n) | O(n) | Recursively splits the array in half, then merges the sorted halves. |
| Quick Sort | O(n log n) | O(log n) | Partitions around a pivot (≤ left, > right), then recurses on each side. |
| Algorithm | Time | Space | Idea |
|---|---|---|---|
| Linear Search | O(n) | O(1) | Scans left to right until the target is found. Works on any array. |
| Binary Search | O(log n) | O(1) | On a sorted array, halves the remaining range each step. |
| Algorithm | Time | Space | Idea |
|---|---|---|---|
| Red-Black Tree | O(log n) | O(n) | Self-balancing BST kept balanced by node colors and rotations (cases 1/2/3). |
| AVL Tree | O(log n) | O(n) | Height-balanced BST kept balanced by the balance factor and LL/RR/LR/RL rotations. |
| Structure | Time | Space | Idea |
|---|---|---|---|
| Queue (FIFO) | O(1) per op | O(n) | First-In First-Out: enqueue at the rear, dequeue from the front. |
| Linked List | O(n) | O(n) | A chain of nodes pointing to the next; demonstrates append and a head-to-tail search. |
algorithms.html — page markup only (links the CSS and src/main.js)
server.js — Express static file server
css/
base.css — shell, controls, code block, layout
array.css — array stage and cell states
tree.css — tree stage, nodes and edges
linear.css — linear stage: cells, pointers, arrows
src/
main.js — app shell: playback, controls, and wiring the registry into tabs/buttons
utils.js — shared helpers (buildClasses, sortedRange, shuffle)
algorithms/
Algorithm.js — abstract base: render + generateSteps + IO contract (sampleInput, canShuffle, param)
ArrayAlgorithm.js — base for array-shaped algorithms (implements render)
TreeAlgorithm.js — base for tree-shaped algorithms (SVG render)
LinearAlgorithm.js — base for linear structures: queue, list (cells + pointers)
BubbleSort.js SelectionSort.js InsertionSort.js MergeSort.js QuickSort.js
LinearSearch.js BinarySearch.js
RedBlackTree.js AVLTree.js
Queue.js LinkedList.js
index.js — registry (add new algorithms here)
This is the core idea of the whole project, so it's worth understanding before adding anything.
There is no live animation loop running the algorithm. Instead, when an algorithm is
loaded, main.js calls algorithm.generateSteps(input) once. The algorithm runs to
completion and, at every interesting moment — a comparison, a swap, a rotation, an
insertion — it pushes a snapshot onto a list. A snapshot describes the full state at
that moment, never a delta.
So generateSteps returns a flat array like steps = [step0, step1, …, stepN], where each
step is a plain, immutable object.
Playback never touches the algorithm again. main.js keeps a single index idx into the
steps array and the controls only move it:
- Step →
idx++· ← Backidx--· ↺ Resetidx = 0 - ▶ Play a timer that increments
idxuntil the last step (speed controls the delay) - every move calls
render(steps[idx])
Because each step is a complete, self-contained snapshot, navigation is instant and fully
reversible: Back is as cheap as Step and the algorithm is never re-run. render is
stateless — given a step it draws exactly that step.
Whatever the visual form, each step includes:
| Field | Meaning |
|---|---|
status |
the message shown under the stage |
line |
the 1-based pseudocode line to highlight in the code block (-1 = none) |
done |
true on the final step |
main.js reads these to update the status line, highlight the active line of code, and
update the progress counter — identically for every algorithm.
main.js stays generic: it owns playback, the legend, the badges, the code block and the
status, and delegates only the stage (the drawing area) to algorithm.render(stage, step).
Algorithms that look the same share one renderer through a base class, so a renderer is written once per form, not once per algorithm:
| Base class | Visual form | Stage drawn as | Used by |
|---|---|---|---|
ArrayAlgorithm |
a row of cells | <div> cells |
all sorts + searches |
TreeAlgorithm |
a tree of nodes | inline SVG (circles + lines) | Red-Black, AVL |
LinearAlgorithm |
a row of cells with pointers/arrows | <div> cells + labels |
Queue, Linked List |
Each base sets stage.className and reads its own step fields, described next.
Array form — ArrayAlgorithm — { array, classes, status, line, done }
| Field | Type | Meaning |
|---|---|---|
array |
number[] |
the values shown in the cells for this step |
classes |
string[] |
one CSS state per cell: '', key, cmp, swap, sorted, pivot, min, found, out, dim, done |
Tree form — TreeAlgorithm — { nodes, edges, status, line, done }
| Field | Type | Meaning |
|---|---|---|
nodes |
[{ id, value, color, column, depth, state }] |
color is 'red'/'black'; column is the in-order horizontal slot and depth the level (the renderer turns them into x/y); state: '', cmp, new, fix |
edges |
[{ from, to }] |
parent → child node ids, drawn as lines |
Linear form — LinearAlgorithm — { cells, pointers, arrows, tail?, status, line, done }
| Field | Type | Meaning |
|---|---|---|
cells |
[{ value, state }] |
the boxes in the row; state: '', new, cmp, remove, active, sorted |
pointers |
[{ label, index }] |
labels drawn above a cell, e.g. FRONT, REAR, HEAD |
arrows |
boolean |
draw → between consecutive cells (linked list) |
tail |
string (optional) |
trailing marker after the last cell, e.g. null |
Legend colors map a cls name to a CSS variable (--cmp, --swap, …) defined in the CSS
files, so a legend entry just needs a matching variable.
The shell never branches on category to decide what data to run or which controls to show.
Each algorithm declares that itself, on the Algorithm base:
| Property | Default | Meaning |
|---|---|---|
sampleInput |
[] |
the data this algorithm animates. The shell copies it and passes it to generateSteps; it is opaque to the shell (only the Shuffle button assumes it's an array). The form bases set sensible defaults (ArrayAlgorithm an unsorted array, TreeAlgorithm an ascending one, …). |
canShuffle |
true |
whether the shell shows a Shuffle button that reshuffles sampleInput and regenerates. Searches set it to false (their array must stay sorted). |
param |
null |
an optional interactive scalar { label, value, min, max }. If present, the shell shows a labelled number box and passes its value to generateSteps(input, value). Searches use it for the target. |
So category is used only to group algorithms under a tab (categoryLabel is the tab
text). It carries no behavior. A brand-new category — say cryptography — works with no
change to the shell: give the class its category/categoryLabel, a sampleInput of
whatever shape your generateSteps reads (a number to factor, a short string to encode…), a
renderer (extend Algorithm or a form base), optionally a param (e.g. a key) and
canShuffle = false, then register it. The tab, button, badges, legend, controls and
animation all appear on their own.
Create src/algorithms/YourAlgorithm.js extending the base for its visual form. For an
array-shaped algorithm that's ArrayAlgorithm (rendering is inherited):
import { ArrayAlgorithm } from './ArrayAlgorithm.js';
import { buildClasses, sortedRange } from '../utils.js';
export class HeapSort extends ArrayAlgorithm {
id = 'heap';
name = 'Heap Sort';
category = 'sort'; // groups this under the Sorting tab
categoryLabel = 'Sorting'; // tab label (only used when the category is new)
time = 'O(n log n)';
space = 'O(1)';
desc = 'Builds a max-heap then repeatedly extracts the maximum.';
// sampleInput / canShuffle are inherited from ArrayAlgorithm; override only if needed.
legend = [
{ cls: 'key', label: 'root' },
{ cls: 'swap', label: 'swapping' },
{ cls: 'sorted', label: 'sorted' },
];
code = [
'def heap_sort(a):',
' build_max_heap(a)',
' for i in range(len(a) - 1, 0, -1):',
' a[0], a[i] = a[i], a[0]',
' heapify(a, 0, i)',
];
generateSteps(input) {
const values = [...input], size = values.length, steps = [];
// run the algorithm, pushing one snapshot per interesting moment
// (see "Step shapes per form" above for the array shape)
return steps;
}
}For building the classes array, utils.js provides:
buildClasses(size, { [i]: 'key', [j]: 'cmp' }) // array of `size` classes, rest are ''
sortedRange(low, high) // { low: 'sorted', ..., high: 'sorted' }In src/algorithms/index.js, add one import and one entry to REGISTRY:
import { HeapSort } from './HeapSort.js';
export const REGISTRY = [
// ... existing algorithms
new HeapSort(),
];The category tab and algorithm button appear automatically. No other changes needed.
Set a category key that doesn't exist yet (e.g. 'graph' or 'crypto') and set
categoryLabel to the tab label (e.g. 'Graphs', 'Cryptography'). The tab is created
automatically, in the order algorithms are registered. Category is just a grouping key — the
shell has no per-category logic, so the new category needs nothing else (see Input and
controls are declared, not hard-coded above for how a non-array category supplies its own
data and controls).
When the new algorithm cannot be drawn by an existing form, create a new base class beside
ArrayAlgorithm.js — say GraphAlgorithm.js — that extends Algorithm, implements its own
render(stage, step), sets stage.className, and reads its own step fields. Add a matching
css/graph.css and link it from algorithms.html. Algorithms of that form then extend
GraphAlgorithm. The shell (main.js) and the registry are untouched.
TreeAlgorithm + RedBlackTree (rendered as SVG, category tree) are a worked example of
exactly this: they added a whole new visual form without changing the shell.