Documentation from the repository README. View source
A TypeScript toolkit for user-defined algorithm visualizations. You write the algorithm; AlgiViz provides array operations, immutable steps, playback and canvas rendering. No runtime dependencies. Playback works without DOM, React or Next.js. ESM only, with TypeScript declarations.
The following API is introduced in 0.4.0. New applications use these entry points, which do not import bundled algorithms:
@grundyjs/algiviz/array: array operations and generator definitions.@grundyjs/algiviz/playback: generic playback of any immutable states and events.@grundyjs/algiviz/scene: typed objects and user-provided visualizations.@grundyjs/algiviz/canvas: the ready-made array renderer.You own the loops, conditions and operation order. No library registration or changes to AlgiViz are required. For example, reversing an array:
import { defineArrayAlgorithm, createArrayPlayer, createArrayTimeline } from "@grundyjs/algiviz/array";
import { createArrayRenderer } from "@grundyjs/algiviz/canvas";
const reverse = defineArrayAlgorithm(function* (array) {
yield array.start();
for (let i = 0; i < Math.floor(array.length / 2); i++) {
yield array.highlight([i, array.length - 1 - i]);
yield array.swap(i, array.length - 1 - i);
}
yield array.done();
});
const options = { stepDurationMs: 350, finalHoldMs: 1000 };
const ctx = canvas.getContext("2d");
const renderer = createArrayRenderer({ theme: "dark" });
const player = createArrayPlayer(reverse.iterate([1, 2, 3, 4]), {
...options,
render: frame => renderer.render(ctx, frame)
});
// Your animation loop calls player.advance(deltaMs * speed).
// No calls while paused; advance(0) redraws after resizing.
// On teardown: cancel your animation loop and call player.dispose().
// Alternatively, retain history and seek:
const timeline = createArrayTimeline(reverse.steps([1, 2, 3, 4]), options);
renderer.render(ctx, timeline.sample(500));defineArrayAlgorithm returns iterate(values) and steps(values). Each
iterator owns a separate workspace and copies/validates input on the first
next(). steps collects the iterator into a frozen array. Yield every
operation immediately, begin with start, and end with done. Each operation
updates the workspace and returns an immutable snapshot; it does not draw.
The player invokes the supplied render initially and on every advance.
Rendering errors close the iterator and propagate to the caller.
| Operation | Effect |
|---|---|
at(index), held, length |
Read a frozen item, the held item or array length |
start(regions?) |
Emit initial state; call once |
compare(left, right) |
Highlight two positions and count one comparison; either position can be "held" |
highlight(positions) |
Highlight items for this step without changing counters |
swap(left, right, regions?) |
Exchange occupied positions; count two writes (non-adjacent swaps are supported) |
select(from, regions?) |
Hold an item and leave a hole; no counted writes |
shift(from, to, regions?) |
Move an item into the hole; count one write |
insert(to, regions?) |
Fill the hole with the held item; count one write |
pass(end, regions?) |
Describe a completed pass without changing items or counters |
done(regions?) |
Finish; requires no held item; does not sort or infer sorted regions |
regions can contain sortedPrefixLength and sortedSuffixLength.
Omitted markers retain their previous values. The author decides when regions
are sorted; AlgiViz validates their bounds but does not prove that claim. Positions
are zero-based; invalid positions, occupied destinations and invalid operation
order throw before mutating the workspace. Read snapshots rather than modifying
items yourself. createArrayTrace(values) exposes the same operations directly,
with immediate input validation, for authors writing their own generator wrapper.
Full insertion/bubble implementations live in examples/algorithms.mjs.
The browser demo imports those external algorithms. Deprecated /core exports
remain available through isolated compatibility implementations using the same
operations; they are retained in the package until a breaking release removes them.
Existing insertion/bubble traces remain unchanged. ArrayEvent also includes the
new highlight event, so exhaustive event switches must handle it.
/playback does not know arrays, sorting, Canvas, or event names. A source yields
Step objects with contiguous zero-based index, immutable
state and event. Supply isTerminal to identify the final step and
render to draw each interpolated frame using any rendering technology:
import { createPlayer, createTimeline } from "@grundyjs/algiviz/playback";
const player = createPlayer(myTraversal(), {
stepDurationMs: 300,
finalHoldMs: 1000,
isTerminal: step => step.event.kind === "finished",
render: frame => drawGraph(frame.previous, frame.current, frame.progress, frame.event)
});The render callback is optional. Without it, read player.frame and render
manually. createTimeline(steps, timing) supports seeking with the same generic
frame shape; call your renderer with its sample(timeMs) result. The generic
engine does not clone or freeze user states; that is the source's responsibility.
It retains adjacent snapshots only; the timeline retains the supplied history.
Custom array events or tree/graph operations can use this contract and a custom
renderer. The scene API below dispatches individual objects; there is no built-in
graph/tree algorithm or operation set.
The /scene API is introduced in 0.4.0. Define a type map with your own data, then
provide a visualization for every type. AlgiViz matches identities and dispatches
objects; your handlers decide how data becomes geometry and how it interpolates.
import { createSceneRenderer, type Scene } from "@grundyjs/algiviz/scene";
interface MyObjects {
badge: { x: number; y: number; label: string };
}
const renderer = createSceneRenderer<MyObjects, CanvasRenderingContext2D>({
badge(ctx, object) {
const from = (object.previous ?? object.current)!.data;
const to = (object.current ?? object.previous)!.data;
const x = from.x + (to.x - from.x) * object.progress;
const y = from.y + (to.y - from.y) * object.progress;
ctx.save();
try {
ctx.globalAlpha = object.presence;
ctx.fillText(to.label, x, y);
} finally { ctx.restore(); }
}
});
const before: Scene<MyObjects> = {
objects: [{ id: "first", type: "badge", data: { x: 20, y: 30, label: "A" } }]
};
const after: Scene<MyObjects> = {
objects: [{ id: "first", type: "badge", data: { x: 120, y: 30, label: "A" } }]
};
// Keep these states immutable. A generic timeline/player can provide this frame.
renderer.render(ctx, {
previous: before, current: after, progress: 0.5, stepIndex: 1, event: "move"
});id, a type, user-defined data and an
optional finite zIndex (default zero). IDs are unique within a snapshot. A
retained ID cannot change type; use a new ID to replace an object of another type.previous and current objects, with null for a missing
endpoint. phase is enter, update or exit. progress is the raw 0–1 frame
progress. presence is progress on entry, 1 on update and 1−progress on exit;
handlers may use it for opacity or scale. It is not applied automatically.zIndex draws first. Ties follow the current snapshot's object order;
removed objects follow in their previous order. The current layer wins when it
changes. There is no implicit layout or coordinate system.scene.get(id) for links, pointers and other
references. It returns the referenced object's full transition, including both
endpoints; missing references return undefined. The application decides how to
handle them and how to interpolate positions or changing targets.createSceneFrame(frame) exposes the same resolved transitions without drawing.
The registry is captured at renderer construction. Missing handlers, duplicate
IDs, invalid progress/layers and changed types fail before any handler is called.arrayScene(snapshot) from /canvas adapts an array to a Scene
with bar objects whose data contains item, index and held. The built-in
array renderer uses this same dispatcher. You can provide a different bar
visualization using createSceneRenderer.
See examples/tree-scene.mjs and open
examples/tree.html after building and serving the repository. The application
defines its own tree traversal, node, edge and pointer types, appearance,
pointer movement and removal. No tree-specific code is added to AlgiViz.
For a second custom scene, open examples/merge.html. Its application-owned
merge algorithm and
renderer use /playback and /scene to show the main
array and a reusable buffer. Equal values have letter suffixes to demonstrate
stability. Cells use row/position IDs because copy-back temporarily duplicates
item identities. Play/pause, step controls, speed and seeking support up to 64
input values; text status, reduced motion and hidden-tab pausing are included.
Select Generator for forward-only playback with O(n) retained snapshot memory;
steps are produced on demand, and restarting creates a fresh iterator. History
mode enables backward stepping and seeking with up to 64 values. Generator mode
accepts up to 2,000 values; dense charts hide labels and text status previews the
first 24 cells. Switching to history requires loading an array within its limit.
The example stores full history, costing O(n² log n) space in the worst case;
the underlying algorithm uses O(n) auxiliary memory. No merge-sort package
export or new library API is needed.
Existing /core imports continue to work in 0.4.0. Built-in insertion and bubble
sort exports are deprecated, but their traces remain unchanged. Migrate each
algorithm into your application using defineArrayAlgorithm; complete examples
are in examples/algorithms.mjs.
| Previous API | Application-owned API |
|---|---|
insertionSortSteps(values) |
insertion.steps(values) on your algorithm definition |
iterateInsertionSortSteps(values) |
insertion.iterate(values) |
bubbleSortSteps(values) / iterateBubbleSortSteps(values) |
bubble.steps(values) / bubble.iterate(values) |
/core createSortTimeline / createSortPlayer |
/array createArrayTimeline / createArrayPlayer |
/canvas createSortRenderer |
/canvas createArrayRenderer |
/core SortEvent, SortSnapshot, SortStep, SortFrame |
/array ArrayEvent, ArraySnapshot, ArrayStep, ArrayFrame |
Timing options and array frames remain compatible. Exhaustive event switches
must handle the new highlight event, even though the legacy algorithms do not
emit it. An incomplete player source now reports a missing terminal step; avoid
depending on the old error text. Generic /playback requires an explicit
isTerminal predicate; /array players still recognize the done event.
Use /scene when you need your own object types and visualization handlers.
This is optional for array users: createArrayRenderer already uses scenes.
Generic states and custom scene data must remain immutable; unlike array
operations, the generic engine does not clone or deep-freeze them.
The following /core examples remain supported for compatibility. Prefer the
user-defined algorithm API above for new integrations.
npm install @grundyjs/algivizTry the interactive insertion sort demo. Import the core from @grundyjs/algiviz/core and the browser renderer from @grundyjs/algiviz/canvas; there is no root entry point or CommonJS build. Version 0.x is an initial API and may change in later minor releases.
Requires Node.js 22 or later and npm.
GitHub Actions runs npm test and npm run test:package on pushes to master
and pull requests, using Node.js 22 and 24 on Linux and Windows. CI checks do not
publish packages; publishing remains tied to a GitHub Release.
npm install
npm test
npm packBuild output includes ESM and TypeScript declarations. npm pack rebuilds automatically; npm publish runs the tests before packing. The archive includes only dist, README, LICENSE, CHANGELOG and package metadata. Licensed under MIT.
import { insertionSortSteps } from "@grundyjs/algiviz/core";
const steps = insertionSortSteps([5, 2, 4, 2, 1]);
console.log(steps.at(-1).state.slots.map(item => item.value));
// [1, 2, 2, 4, 5]Each step contains a typed event and the complete state after it. start preserves the input; done has no held item and a fully sorted array. Original-index ids (item-0, etc.) distinguish equal values and are deterministic within one trace. The input is never modified. Invalid values, including NaN, Infinity and sparse holes, throw TypeError.
select moves the key into held and leaves one null slot. compare compares the neighbor with the key. shift moves the neighbor into the hole. insert puts the key back. All original items occur exactly once across slots and held in every snapshot. Equal values are never shifted past each other.
sortedPrefixLength counts the leading occupied slots known to be sorted: initially min(1, n), during insertion it ends at the hole, after insertion it extends through the processed region. comparisons counts compare events. writes counts shift and insert events, including reinserting an unchanged key; extracting a key and clearing the hole are visualization bookkeeping, not counted writes.
The result, steps, events, snapshots, slot arrays and items are frozen at runtime. Items may be shared between snapshots because they are immutable.
The underlying sort uses O(n²) comparisons in the worst case and O(n) on already sorted input. This API stores full history, so its worst-case time and memory are O(n³), unlike ordinary in-place insertion sort. Intended for small educational inputs; the demo site limits inputs to 24 items. This core imposes no UI limit.
Tests replay every event independently, check item conservation, stability, prefix ordering, counters, frozen snapshots and 1,093 exhaustive inputs with values -1, 0 and 1 of lengths 0–6.
bubbleSortSteps(values) collects a frozen history; iterateBubbleSortSteps(values)
yields the same snapshots on demand. Both work with the existing timeline, player
and canvas renderer. Available since version 0.3.0.
import { bubbleSortSteps, iterateBubbleSortSteps, createSortPlayer } from "@grundyjs/algiviz/core";
const steps = bubbleSortSteps([5, 2, 4, 2, 1]);
const player = createSortPlayer(iterateBubbleSortSteps([5, 2, 4, 2, 1]), {
stepDurationMs: 350, finalHoldMs: 1000
});Each left-to-right pass compares adjacent items. compare identifies both items;
swap contains their pre-swap leftId, rightId, left and right positions,
and its snapshot is after the exchange. Each swap counts as two array writes.
Equal items are never swapped, so the sort is stable. held remains null and
sortedPrefixLength is zero.
pass marks a completed pass, with end identifying its last compared position.
The optional sortedSuffixLength snapshot field counts trailing items in their
final positions; renderers treat an omitted value as zero for older traces.
A pass with no swaps ends the sort early and marks the entire array sorted.
done always has sortedSuffixLength equal to the array length.
Validation, lazy input copying, freezing and memory costs match insertion sort.
The underlying algorithm performs O(n²) comparisons in the worst case and O(n)
on sorted input; full snapshots still make worst-case trace generation O(n³).
Consumers switching on SortEvent.type should handle the new swap and pass events.
Use iterateInsertionSortSteps to consume steps on demand without retaining the
entire history. It yields the same immutable SortStep snapshots as
insertionSortSteps, which collects this iterator into a frozen array.
import { iterateInsertionSortSteps } from "@grundyjs/algiviz/core";
const iterator = iterateInsertionSortSteps([5, 2, 4, 2, 1]);
const start = iterator.next().value;
const selected = iterator.next().value;
iterator.return(); // Stop early when no more steps are needed.Creating the generator does not read the input or run the algorithm. The first
next() copies and validates the input; invalid values throw at that point.
Changes to the input before the first next() are observed; later changes do not
affect the iterator. Each subsequent next() runs only to the next step. An
iterator is single-use; create a new one to restart. A for...of loop can also
consume it, and break closes it early.
When the consumer retains only a fixed number of snapshots, memory is O(n). Every snapshot still copies n slots, so consuming the complete worst-case trace still takes O(n³) time. Collecting the iterator into an array restores the full history memory cost. Iteration is synchronous; a long loop can block the UI.
The existing createSortTimeline still requires an array of steps for seeking.
It does not accept this iterator directly. Use createSortPlayer for forward-only
playback; the generator itself provides no timing or backward seeking.
createSortPlayer consumes an iterable of steps and retains only the two adjacent
snapshots needed for rendering. With the lazy generator, playback uses O(n)
memory as long as the caller does not retain old frames.
import { createSortPlayer, iterateInsertionSortSteps } from "@grundyjs/algiviz/core";
import { createSortRenderer } from "@grundyjs/algiviz/canvas";
const player = createSortPlayer(iterateInsertionSortSteps([5, 2, 4, 1]), {
stepDurationMs: 650, finalHoldMs: 2000
});
const renderer = createSortRenderer({ theme: "dark" });
const ctx = canvas.getContext("2d");
let paused = false;
let speed = 1;
let last;
let requestId;
function tick(now) {
const delta = last === undefined ? 0 : now - last;
last = now;
if (!paused) player.advance(delta * speed);
renderer.render(ctx, player.frame);
if (!player.finished) requestId = requestAnimationFrame(tick);
}
requestId = requestAnimationFrame(tick);
// Set paused = true/false or speed = 2 from your controls.
// On teardown: cancelAnimationFrame(requestId); player.dispose();frame is fully rendered at progress 1.advance(deltaMs) accepts finite, non-negative elapsed playback time and
returns a SortFrame compatible with the existing canvas renderer. Zero does
not consume steps. Pause by not advancing; multiply delta by a non-negative
speed factor to change speed.finished becomes true after the final transition and finalHoldMs. Further
advances return the final frame. There is no known total duration or seeking.done event. The player closes the iterator when
that transition completes; unexpected exhaustion or source errors throw and
close playback. dispose() closes early and is safe to repeat; afterward the
frame remains readable but advance() throws.Import createSortTimeline from @grundyjs/algiviz/core and createSortRenderer from @grundyjs/algiviz/canvas.
Step i completes at i * stepDurationMs; finalHoldMs extends the final frame. sample clamps finite times to the timeline range and rejects non-finite times. Frames include the current event for explanations and highlights.
The renderer draws a full frame using the canvas bitmap dimensions and restores context state. Signed values use magnitude for bar height and retain their sign in labels. Use small inputs for readable labels.
import { createSortTimeline } from "@grundyjs/algiviz/core";
import { createSortRenderer } from "@grundyjs/algiviz/canvas";
const timeline = createSortTimeline(steps, { stepDurationMs: 650, finalHoldMs: 2000 });
const renderer = createSortRenderer({ theme: "dark" });
renderer.render(canvas.getContext("2d"), timeline.sample(3250));Build, serve this project root using any static HTTP server, and open examples/index.html for play, pause, restart and seeking. The example changes bitmap size to fit its container; for video use a separate canvas with fixed export dimensions. Canvas rendering does not start any animation loop. There are no video recording dependencies.
The demo supports history (up to 64 items) and generator playback (up to 2,000). Enter values or generate random, sorted or reversed arrays. Both modes support pause, restart and speed changes; seeking is available only with full history. Algorithm and mode changes restart the loaded array. Choose insertion or bubble sort; both support both playback modes. These limits apply only to the demo. Dense charts hide bar labels. Hidden tabs pause playback, and delayed frames cap catch-up work to keep controls responsive.
insertionSortSteps(values) returns immutable SortStep[] snapshots. Empty arrays are valid.bubbleSortSteps(values) returns the same snapshot format for stable bubble sort.iterateInsertionSortSteps(values) and iterateBubbleSortSteps(values) yield snapshots lazily.createSortPlayer(iterable, { stepDurationMs, finalHoldMs }) provides forward-only playback with frame, advance(deltaMs), finished and dispose().createSortTimeline(steps, { stepDurationMs, finalHoldMs }) returns durationMs and sample(timeMs). Frames expose previous, current, progress, stepIndex and event.createSortRenderer({ theme: "dark" | "light" }).render(ctx, frame) draws into a browser 2D canvas context. Drawing and animation scheduling remain separate.For playback, call timeline.sample(elapsedMs) in your own requestAnimationFrame loop. For recording, use a fixed-size export canvas. Negative values are shown by magnitude with signed labels; the chart is not a signed-axis plot.
Create an application-owned generator with defineArrayAlgorithm, as shown
above. Add it to your application's controls or the demo registry. AlgiViz needs
no change when the existing operations are sufficient. For a new kind of state
or event, implement the generic playback contract and its renderer.
Run npm test and npm run test:package. The package check builds a real tarball,
installs it offline in a separate temporary consumer, compiles TypeScript against
the installed declarations and runs an external algorithm and custom scene with
both playback modes. It also checks the array adapter and legacy compatibility.
The temporary consumer is removed afterward. The release workflow runs this check
before publishing. Source and issues: urffin/algiviz.
Publishing is a separate maintainer action.
The .github/workflows/npm-publish.yml workflow publishes to npm when a stable
GitHub Release is published (release: published). Drafts, prereleases and tag
pushes alone do not publish a package. The release tag must be v followed by
the exact version in package.json and package-lock.json (for example v0.4.0).
The workflow uses Node.js 24 and npm 11. The publish lifecycle runs the tests and
build before publishing with provenance, using npm trusted publishing (OIDC).
One-time setup in the npm package settings, under Trusted Publisher:
urffin.algiviz.npm-publish.yml (without the directory).npm publish if the settings show this option.No NPM_TOKEN secret is required. Keep account 2FA enabled.
See npm trusted publishing.
For each release, update the version and changelog, commit and push the changes (including the workflow), then publish a GitHub Release for the matching tag at that commit. Check the Actions run and the npm package version afterward. Do not reuse an already published npm version; if setup failed before publication, correct the settings and rerun the failed workflow.
All browser examples share playback defaults from examples/player-settings.mjs:
The browser controller in examples/demo-player.mjs also owns animation scheduling,
mode changes, stepping, seeking, restart, reduced motion and generator cleanup.
650 ms per step, 1,000 ms final hold, speeds 0.25× through 32×, history/generator
modes and forward/backward step controls. Backward stepping and seeking are
disabled in generator mode. Array input limits are 64 for history and 2,000
for generators. Reduced motion disables interpolation; hidden tabs pause.
All sorting demos also share array generation: choose the element count and
Random, Sorted or Reverse order, then Generate & load. The generated values
appear in the input field and obey the selected playback mode's limit.