@grundyjs/algiviz 0.5.0 → 0.6.0
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.
- package/CHANGELOG.md +15 -0
- package/README.md +211 -304
- package/dist/playback/index.d.ts +1 -0
- package/dist/playback/index.js +1 -0
- package/dist/playback/step-emitter.d.ts +6 -0
- package/dist/playback/step-emitter.js +13 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.0 — 2026-10-14
|
|
4
|
+
|
|
5
|
+
- Add an application-owned graph BFS example with custom vertices, edges and queue
|
|
6
|
+
entries, shortest distances, disconnected components and both playback modes.
|
|
7
|
+
- Add createStepEmitter to /playback for per-iterator step numbering and frozen
|
|
8
|
+
wrappers; snapshot copying and state/event immutability remain author-owned.
|
|
9
|
+
- Use the helper in graph and merge examples, and document custom graph integration.
|
|
10
|
+
|
|
11
|
+
## 0.5.1 — 2026-10-14
|
|
12
|
+
|
|
13
|
+
- Update the README around the current public API and examples; remove migration
|
|
14
|
+
instructions and detailed legacy API documentation.
|
|
15
|
+
- Correct demo input limits and clarify playback, snapshot memory and rendering.
|
|
16
|
+
- No runtime API or behavior changes.
|
|
17
|
+
|
|
3
18
|
## 0.5.0 — 2026-10-14
|
|
4
19
|
|
|
5
20
|
- Cache immutable array and quick-sort scene projections and avoid repeated scale/layout
|
package/README.md
CHANGED
|
@@ -1,10 +1,28 @@
|
|
|
1
1
|
# AlgiViz
|
|
2
2
|
|
|
3
|
-
A TypeScript toolkit for user-defined algorithm visualizations. You write the
|
|
3
|
+
A TypeScript toolkit for user-defined algorithm visualizations. You write the
|
|
4
|
+
algorithm and define your objects; AlgiViz provides array operations, playback,
|
|
5
|
+
typed scene dispatch and a ready-made Canvas array renderer. No runtime dependencies.
|
|
6
|
+
Playback works without DOM, React or Next.js. ESM only, with TypeScript declarations.
|
|
7
|
+
|
|
8
|
+
This README describes version 0.6.0.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
npm install @grundyjs/algiviz
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
There is no root entry point or CommonJS build. Import the modules you need:
|
|
17
|
+
`/array`, `/playback`, `/scene` and `/canvas`.
|
|
18
|
+
|
|
19
|
+
The deprecated `/core` sorting API remains available for compatibility.
|
|
20
|
+
For new integrations, use the APIs below. Version 0.x may introduce breaking changes
|
|
21
|
+
in minor releases; consult [CHANGELOG.md](CHANGELOG.md) when updating.
|
|
4
22
|
|
|
5
23
|
## User-defined algorithms
|
|
6
24
|
|
|
7
|
-
|
|
25
|
+
|
|
8
26
|
New applications use these entry points, which do not import bundled algorithms:
|
|
9
27
|
|
|
10
28
|
- `@grundyjs/algiviz/array`: array operations and generator definitions.
|
|
@@ -75,12 +93,8 @@ order throw before mutating the workspace. Read snapshots rather than modifying
|
|
|
75
93
|
items yourself. `createArrayTrace(values)` exposes the same operations directly,
|
|
76
94
|
with immediate input validation, for authors writing their own generator wrapper.
|
|
77
95
|
|
|
78
|
-
|
|
79
|
-
The
|
|
80
|
-
remain available through isolated compatibility implementations using the same
|
|
81
|
-
operations; they are retained in the package until a breaking release removes them.
|
|
82
|
-
Existing insertion/bubble traces remain unchanged. `ArrayEvent` also includes the
|
|
83
|
-
new `highlight` event, so exhaustive event switches must handle it.
|
|
96
|
+
Insertion and bubble sort implementations live in [examples/algorithms.mjs](examples/algorithms.mjs).
|
|
97
|
+
The algorithms belong to the application; they require no changes to the library.
|
|
84
98
|
|
|
85
99
|
### Custom structures and renderers
|
|
86
100
|
|
|
@@ -109,9 +123,31 @@ Custom array events or tree/graph operations can use this contract and a custom
|
|
|
109
123
|
renderer. The scene API below dispatches individual objects; there is no built-in
|
|
110
124
|
graph/tree algorithm or operation set.
|
|
111
125
|
|
|
126
|
+
### Step emission helper (since 0.6.0)
|
|
127
|
+
|
|
128
|
+
Version 0.6.0 adds `createStepEmitter` from `/playback`. It removes manual step numbering and freezes the step wrapper:
|
|
129
|
+
|
|
130
|
+
```js
|
|
131
|
+
import { createStepEmitter } from '@grundyjs/algiviz/playback';
|
|
132
|
+
|
|
133
|
+
function* counter() {
|
|
134
|
+
let count = 0;
|
|
135
|
+
const emit = createStepEmitter(() => Object.freeze({ count }));
|
|
136
|
+
yield emit('start');
|
|
137
|
+
count++;
|
|
138
|
+
yield emit('done');
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Create the emitter inside each iterator. Its callback runs once per emission;
|
|
143
|
+
indices start at zero and failed captures do not consume an index. The helper
|
|
144
|
+
does not clone or freeze state/event data, infer terminal events or store history.
|
|
145
|
+
The callback must produce isolated immutable snapshots; immutable shared structures
|
|
146
|
+
can be reused. Returning a mutable workspace directly does not make it a snapshot.
|
|
147
|
+
|
|
112
148
|
### User-defined objects and visualizations
|
|
113
149
|
|
|
114
|
-
|
|
150
|
+
Define a type map with your own data, then
|
|
115
151
|
provide a visualization for every type. AlgiViz matches identities and dispatches
|
|
116
152
|
objects; your handlers decide how data becomes geometry and how it interpolates.
|
|
117
153
|
|
|
@@ -180,30 +216,9 @@ with `bar` objects whose data contains `item`, `index` and `held`. The built-in
|
|
|
180
216
|
array renderer uses this same dispatcher. You can provide a different `bar`
|
|
181
217
|
visualization using `createSceneRenderer<ArrayObjects, YourContext>`.
|
|
182
218
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
pointer movement and removal. No tree-specific code is added to AlgiViz. The Renderer selector switches between Canvas and SVG without restarting playback. The SVG example retains elements by ID, restores layer order and removes stale elements on every frame, including after a seek that skips exit transitions. See [examples/tree-svg.mjs](examples/tree-svg.mjs).
|
|
187
|
-
|
|
188
|
-
For a second custom scene, open `examples/merge.html`. Its application-owned
|
|
189
|
-
[merge algorithm](examples/merge-algorithm.mjs) and
|
|
190
|
-
[renderer](examples/merge-scene.mjs) use `/playback` and `/scene` to show the main
|
|
191
|
-
array and a reusable buffer. Equal values have letter suffixes to demonstrate
|
|
192
|
-
stability. Cells use row/position IDs because copy-back temporarily duplicates
|
|
193
|
-
item identities. Play/pause, step controls, speed and seeking support up to 64
|
|
194
|
-
input values; text status, reduced motion and hidden-tab pausing are included.
|
|
195
|
-
Select Generator for forward-only playback with O(n) retained snapshot memory;
|
|
196
|
-
steps are produced on demand, and restarting creates a fresh iterator. History
|
|
197
|
-
mode enables backward stepping and seeking with up to 64 values. Generator mode
|
|
198
|
-
accepts up to 2,000 values; dense charts hide labels and text status previews the
|
|
199
|
-
first 24 cells. Switching to history requires loading an array within its limit.
|
|
200
|
-
The example stores full history, costing O(n² log n) space in the worst case;
|
|
201
|
-
the underlying algorithm uses O(n) auxiliary memory. No merge-sort package
|
|
202
|
-
export or new library API is needed.
|
|
203
|
-
|
|
204
|
-
### State-to-scene projection (since 0.5.0)
|
|
205
|
-
|
|
206
|
-
`createSceneProjector` was added in 0.5.0. It adapts
|
|
219
|
+
### State-to-scene projection
|
|
220
|
+
|
|
221
|
+
`createSceneProjector` adapts
|
|
207
222
|
playback frames from your state model to scenes without changing the algorithm:
|
|
208
223
|
|
|
209
224
|
```js
|
|
@@ -227,309 +242,201 @@ or deep-frozen. Scene validation still happens in the scene renderer.
|
|
|
227
242
|
The merge example uses this adapter to avoid rebuilding two cell scenes on every
|
|
228
243
|
animation frame. The algorithm, object types and visual behavior remain external.
|
|
229
244
|
|
|
230
|
-
##
|
|
245
|
+
## Playback and rendering
|
|
231
246
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
247
|
+
Array players and timelines use the same frame shape as generic playback:
|
|
248
|
+
`previous`, `current`, `progress`, `stepIndex` and `event`.
|
|
249
|
+
A frame describes the transition into the current step; that step's state is the
|
|
250
|
+
state after its operation.
|
|
236
251
|
|
|
237
|
-
|
|
|
252
|
+
| API | Behavior |
|
|
238
253
|
| --- | --- |
|
|
239
|
-
| `
|
|
240
|
-
| `
|
|
241
|
-
| `
|
|
242
|
-
|
|
|
243
|
-
|
|
|
244
|
-
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
```sh
|
|
265
|
-
npm install @grundyjs/algiviz
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
Try the [interactive insertion sort demo](https://grundyjs.ru/algorithms/insertion-sort/). 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.
|
|
269
|
-
|
|
270
|
-
## Development
|
|
271
|
-
|
|
272
|
-
Requires Node.js 22 or later and npm.
|
|
273
|
-
|
|
274
|
-
GitHub Actions runs `npm test` and `npm run test:package` on pushes to `master`
|
|
275
|
-
and pull requests, using Node.js 22 and 24 on Linux and Windows. CI checks do not
|
|
276
|
-
publish packages; publishing remains tied to a GitHub Release.
|
|
277
|
-
|
|
278
|
-
```sh
|
|
279
|
-
npm install
|
|
280
|
-
npm test
|
|
281
|
-
npm pack
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
Build 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.
|
|
285
|
-
|
|
286
|
-
```js
|
|
287
|
-
import { insertionSortSteps } from "@grundyjs/algiviz/core";
|
|
288
|
-
|
|
289
|
-
const steps = insertionSortSteps([5, 2, 4, 2, 1]);
|
|
290
|
-
console.log(steps.at(-1).state.slots.map(item => item.value));
|
|
291
|
-
// [1, 2, 2, 4, 5]
|
|
292
|
-
```
|
|
293
|
-
|
|
294
|
-
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.
|
|
295
|
-
|
|
296
|
-
`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.
|
|
297
|
-
|
|
298
|
-
`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.
|
|
299
|
-
|
|
300
|
-
The result, steps, events, snapshots, slot arrays and items are frozen at runtime. Items may be shared between snapshots because they are immutable.
|
|
301
|
-
|
|
302
|
-
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.
|
|
303
|
-
|
|
304
|
-
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.
|
|
305
|
-
|
|
306
|
-
## Bubble sort
|
|
307
|
-
|
|
308
|
-
`bubbleSortSteps(values)` collects a frozen history; `iterateBubbleSortSteps(values)`
|
|
309
|
-
yields the same snapshots on demand. Both work with the existing timeline, player
|
|
310
|
-
and canvas renderer. Available since version 0.3.0.
|
|
311
|
-
|
|
312
|
-
```js
|
|
313
|
-
import { bubbleSortSteps, iterateBubbleSortSteps, createSortPlayer } from "@grundyjs/algiviz/core";
|
|
314
|
-
|
|
315
|
-
const steps = bubbleSortSteps([5, 2, 4, 2, 1]);
|
|
316
|
-
const player = createSortPlayer(iterateBubbleSortSteps([5, 2, 4, 2, 1]), {
|
|
317
|
-
stepDurationMs: 350, finalHoldMs: 1000
|
|
318
|
-
});
|
|
319
|
-
```
|
|
320
|
-
|
|
321
|
-
Each left-to-right pass compares adjacent items. `compare` identifies both items;
|
|
322
|
-
`swap` contains their pre-swap `leftId`, `rightId`, `left` and `right` positions,
|
|
323
|
-
and its snapshot is after the exchange. Each swap counts as two array writes.
|
|
324
|
-
Equal items are never swapped, so the sort is stable. `held` remains null and
|
|
325
|
-
`sortedPrefixLength` is zero.
|
|
326
|
-
|
|
327
|
-
`pass` marks a completed pass, with `end` identifying its last compared position.
|
|
328
|
-
The optional `sortedSuffixLength` snapshot field counts trailing items in their
|
|
329
|
-
final positions; renderers treat an omitted value as zero for older traces.
|
|
330
|
-
A pass with no swaps ends the sort early and marks the entire array sorted.
|
|
331
|
-
`done` always has `sortedSuffixLength` equal to the array length.
|
|
332
|
-
|
|
333
|
-
Validation, lazy input copying, freezing and memory costs match insertion sort.
|
|
334
|
-
The underlying algorithm performs O(n²) comparisons in the worst case and O(n)
|
|
335
|
-
on sorted input; full snapshots still make worst-case trace generation O(n³).
|
|
336
|
-
Consumers switching on `SortEvent.type` should handle the new `swap` and `pass` events.
|
|
337
|
-
|
|
338
|
-
## Lazy steps
|
|
339
|
-
|
|
340
|
-
Use `iterateInsertionSortSteps` to consume steps on demand without retaining the
|
|
341
|
-
entire history. It yields the same immutable `SortStep` snapshots as
|
|
342
|
-
`insertionSortSteps`, which collects this iterator into a frozen array.
|
|
343
|
-
|
|
344
|
-
```js
|
|
345
|
-
import { iterateInsertionSortSteps } from "@grundyjs/algiviz/core";
|
|
346
|
-
|
|
347
|
-
const iterator = iterateInsertionSortSteps([5, 2, 4, 2, 1]);
|
|
348
|
-
const start = iterator.next().value;
|
|
349
|
-
const selected = iterator.next().value;
|
|
350
|
-
iterator.return(); // Stop early when no more steps are needed.
|
|
351
|
-
```
|
|
352
|
-
|
|
353
|
-
Creating the generator does not read the input or run the algorithm. The first
|
|
354
|
-
`next()` copies and validates the input; invalid values throw at that point.
|
|
355
|
-
Changes to the input before the first `next()` are observed; later changes do not
|
|
356
|
-
affect the iterator. Each subsequent `next()` runs only to the next step. An
|
|
357
|
-
iterator is single-use; create a new one to restart. A `for...of` loop can also
|
|
358
|
-
consume it, and `break` closes it early.
|
|
359
|
-
|
|
360
|
-
When the consumer retains only a fixed number of snapshots, memory is O(n).
|
|
361
|
-
Every snapshot still copies n slots, so consuming the complete worst-case trace
|
|
362
|
-
still takes O(n³) time. Collecting the iterator into an array restores the full
|
|
363
|
-
history memory cost. Iteration is synchronous; a long loop can block the UI.
|
|
364
|
-
|
|
365
|
-
The existing `createSortTimeline` still requires an array of steps for seeking.
|
|
366
|
-
It does not accept this iterator directly. Use `createSortPlayer` for forward-only
|
|
367
|
-
playback; the generator itself provides no timing or backward seeking.
|
|
368
|
-
|
|
369
|
-
## Sequential playback
|
|
370
|
-
|
|
371
|
-
`createSortPlayer` consumes an iterable of steps and retains only the two adjacent
|
|
372
|
-
snapshots needed for rendering. With the lazy generator, playback uses O(n)
|
|
373
|
-
memory as long as the caller does not retain old frames.
|
|
254
|
+
| `createArrayPlayer(iterable, options)` | Array playback; recognizes the `done` event |
|
|
255
|
+
| `createPlayer(iterable, options)` | Generic playback; requires `isTerminal(step)` |
|
|
256
|
+
| `createArrayTimeline(steps, timing)` / `createTimeline(steps, timing)` | Retained history with `durationMs` and `sample(timeMs)` |
|
|
257
|
+
| `createArrayRenderer({ theme: "dark" \| "light" })` | Full-frame Canvas array rendering |
|
|
258
|
+
| `createSceneRenderer(handlers)` | Dispatches custom objects to your visualization handlers |
|
|
259
|
+
| `createSceneProjector(project)` | Adapts immutable state endpoints to scenes with a bounded cache |
|
|
260
|
+
|
|
261
|
+
Players read the first step at construction and expose `frame`, `advance(deltaMs)`,
|
|
262
|
+
`finished` and `dispose()`. The initial frame has progress 1.
|
|
263
|
+
`advance` accepts finite, non-negative elapsed playback time; zero consumes no steps.
|
|
264
|
+
Pause by not advancing. Apply speed by multiplying elapsed time.
|
|
265
|
+
The optional render callback runs initially and on every advance.
|
|
266
|
+
|
|
267
|
+
The source must reach a terminal step. The player closes the iterator after its
|
|
268
|
+
final transition and becomes finished after `finalHoldMs`. Premature exhaustion,
|
|
269
|
+
source errors and render errors propagate and close the iterator.
|
|
270
|
+
`dispose()` closes early and can be repeated; the frame remains readable, but
|
|
271
|
+
advancing a disposed player throws. Restart with a new iterator and player.
|
|
272
|
+
|
|
273
|
+
Timelines require an array of steps. Step i completes at `i * stepDurationMs`;
|
|
274
|
+
`finalHoldMs` extends the last frame. Sampling clamps finite times to the timeline
|
|
275
|
+
range and rejects non-finite times. There is no seeking or known total duration
|
|
276
|
+
in generator playback.
|
|
277
|
+
|
|
278
|
+
Animation scheduling belongs to your application. For example:
|
|
374
279
|
|
|
375
280
|
```js
|
|
376
|
-
|
|
377
|
-
import { createSortRenderer } from "@grundyjs/algiviz/canvas";
|
|
378
|
-
|
|
379
|
-
const player = createSortPlayer(iterateInsertionSortSteps([5, 2, 4, 1]), {
|
|
380
|
-
stepDurationMs: 650, finalHoldMs: 2000
|
|
381
|
-
});
|
|
382
|
-
const renderer = createSortRenderer({ theme: "dark" });
|
|
383
|
-
const ctx = canvas.getContext("2d");
|
|
384
|
-
let paused = false;
|
|
385
|
-
let speed = 1;
|
|
281
|
+
const player = createArrayPlayer(reverse.iterate([1, 2, 3, 4]), options);
|
|
386
282
|
let last;
|
|
387
283
|
let requestId;
|
|
284
|
+
let paused = false;
|
|
285
|
+
let speed = 1;
|
|
286
|
+
|
|
388
287
|
function tick(now) {
|
|
389
|
-
const delta = last === undefined ? 0 : now - last;
|
|
288
|
+
const delta = last === undefined ? 0 : Math.min(100, now - last);
|
|
390
289
|
last = now;
|
|
391
290
|
if (!paused) player.advance(delta * speed);
|
|
392
291
|
renderer.render(ctx, player.frame);
|
|
393
292
|
if (!player.finished) requestId = requestAnimationFrame(tick);
|
|
394
293
|
}
|
|
395
294
|
requestId = requestAnimationFrame(tick);
|
|
396
|
-
// Set paused = true/false or speed = 2 from your controls.
|
|
397
295
|
// On teardown: cancelAnimationFrame(requestId); player.dispose();
|
|
296
|
+
// To replay after finishing, create a new player and schedule a new loop.
|
|
398
297
|
```
|
|
399
298
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
returns a `SortFrame` compatible with the existing canvas renderer. Zero does
|
|
404
|
-
not consume steps. Pause by not advancing; multiply delta by a non-negative
|
|
405
|
-
speed factor to change speed.
|
|
406
|
-
- `finished` becomes true after the final transition and `finalHoldMs`. Further
|
|
407
|
-
advances return the final frame. There is no known total duration or seeking.
|
|
408
|
-
- The source must end with a `done` event. The player closes the iterator when
|
|
409
|
-
that transition completes; unexpected exhaustion or source errors throw and
|
|
410
|
-
close playback. `dispose()` closes early and is safe to repeat; afterward the
|
|
411
|
-
frame remains readable but `advance()` throws.
|
|
412
|
-
- Restart by disposing the old player and creating a new player and generator.
|
|
413
|
-
Large deltas synchronously consume all intervening steps, so limit the elapsed
|
|
414
|
-
delta in the UI if resuming from a background tab should not cause a long catch-up.
|
|
415
|
-
|
|
416
|
-
## Timeline and canvas
|
|
417
|
-
|
|
418
|
-
Import `createSortTimeline` from `@grundyjs/algiviz/core` and `createSortRenderer` from `@grundyjs/algiviz/canvas`.
|
|
419
|
-
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.
|
|
420
|
-
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.
|
|
299
|
+
This is an alternative to the player setup in the earlier array example, reusing
|
|
300
|
+
its `reverse`, `options`, `ctx` and `renderer`. Large deltas consume intervening steps
|
|
301
|
+
synchronously, so cap elapsed time or pause on hidden tabs to avoid a long catch-up.
|
|
421
302
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
303
|
+
The array renderer expects immutable snapshots. It uses Canvas bitmap dimensions
|
|
304
|
+
for layout, draws a full frame and restores context state. Negative values use
|
|
305
|
+
magnitude for bar height and retain their sign in labels; this is not a signed-axis
|
|
306
|
+
plot. Dense charts hide labels. For recording, use a canvas with fixed export
|
|
307
|
+
dimensions. Rendering does not start an animation loop or record video.
|
|
425
308
|
|
|
426
|
-
|
|
427
|
-
const renderer = createSortRenderer({ theme: "dark" });
|
|
428
|
-
renderer.render(canvas.getContext("2d"), timeline.sample(3250));
|
|
429
|
-
```
|
|
309
|
+
### Memory and generation cost
|
|
430
310
|
|
|
431
|
-
|
|
311
|
+
Array operations copy the input, validate finite numbers and reject sparse inputs.
|
|
312
|
+
`defineArrayAlgorithm` performs this work on the first iterator `next()`;
|
|
313
|
+
`createArrayTrace` validates immediately. Each iterator owns its workspace.
|
|
314
|
+
Array steps, events, snapshots and items are frozen; immutable items can be shared
|
|
315
|
+
between snapshots.
|
|
432
316
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
317
|
+
Generator playback retains adjacent snapshots, not the entire history. Array
|
|
318
|
+
playback therefore uses O(n) snapshot memory if the caller does not retain old
|
|
319
|
+
frames. Generic states and algorithm-specific storage may have different costs.
|
|
320
|
+
Each full array snapshot still copies n slots: laziness reduces retained history,
|
|
321
|
+
not the cost of producing every step. Collecting steps restores the history cost.
|
|
322
|
+
For example, a quadratic sorting trace with O(n)-sized snapshots can take O(n³)
|
|
323
|
+
time and history space. Iteration is synchronous and can block the UI.
|
|
440
324
|
|
|
325
|
+
## Browser examples
|
|
441
326
|
|
|
442
|
-
|
|
327
|
+
Build the project, serve the repository root with a static HTTP server and open
|
|
328
|
+
one of these pages. Examples are application code in the repository and are not
|
|
329
|
+
included in the npm archive.
|
|
443
330
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
331
|
+
| Example | What it demonstrates |
|
|
332
|
+
| --- | --- |
|
|
333
|
+
| [Sorting lab](examples/index.html) | Insertion and bubble sort using the array operations and renderer |
|
|
334
|
+
| [Merge sort](examples/merge.html) | Custom state and a two-row scene for the main array and buffer |
|
|
335
|
+
| [Quick sort partitions](examples/quick.html) | Lomuto and three-way partitioning with custom state and scene |
|
|
336
|
+
| [Graph BFS](examples/graph.html) | Cycles, disconnected components, shortest distances and an animated queue |
|
|
337
|
+
| [Tree traversal](examples/tree.html) | Custom nodes, edges and pointer with Canvas and retained SVG backends |
|
|
338
|
+
|
|
339
|
+
All examples reuse [demo-player.mjs](examples/demo-player.mjs) and
|
|
340
|
+
[player-settings.mjs](examples/player-settings.mjs): 650 ms per step, 1,000 ms final
|
|
341
|
+
hold, speeds 0.25× through 32×, play/pause, restart and step controls.
|
|
342
|
+
History enables backward stepping and seeking; generator mode disables both.
|
|
343
|
+
Reduced motion disables interpolation, and hidden tabs pause playback.
|
|
344
|
+
|
|
345
|
+
All sorting examples accept up to **64 finite values with history** or **2,000 in
|
|
346
|
+
generator mode**. Enter values or generate an array by count with Random, Sorted or
|
|
347
|
+
Reverse order. These are demo limits, not library restrictions. Switching a large
|
|
348
|
+
array to history requires first loading an array within the history limit.
|
|
349
|
+
Dense charts hide labels; custom sorting examples preview the first 24 cells in
|
|
350
|
+
their text status.
|
|
351
|
+
|
|
352
|
+
### Merge sort
|
|
353
|
+
|
|
354
|
+
The [algorithm](examples/merge-algorithm.mjs) and
|
|
355
|
+
[renderer](examples/merge-scene.mjs) use `/playback` and `/scene`.
|
|
356
|
+
Equal values have letter suffixes to demonstrate stability. Cells use row/position
|
|
357
|
+
IDs because copy-back temporarily duplicates item identities. Left wins ties.
|
|
358
|
+
The algorithm uses O(n) auxiliary memory; full snapshot history has an
|
|
359
|
+
O(n² log n) space upper bound.
|
|
360
|
+
|
|
361
|
+
### Quick sort
|
|
362
|
+
|
|
363
|
+
Choose Lomuto or three-way partitioning while preserving loaded values.
|
|
364
|
+
Both algorithms and their scene belong to the example. Three-way recursion skips
|
|
365
|
+
the equal band: n ≥ 2 equal items take 2n scalar checks and zero swaps, versus
|
|
366
|
+
n(n−1)/2 checks with this Lomuto variant. Neither variant guarantees stability.
|
|
367
|
+
Worst-case algorithm time is quadratic; recursion can use O(n) stack space.
|
|
368
|
+
Full snapshot history has a separate O(n³) space upper bound.
|
|
369
|
+
|
|
370
|
+
### Tree traversal
|
|
371
|
+
|
|
372
|
+
The example defines its own `node`, `edge` and `pointer` types. The pointer follows
|
|
373
|
+
tree edges through shared ancestors between visited nodes. The Renderer selector
|
|
374
|
+
switches between Canvas and SVG without restarting playback.
|
|
375
|
+
[tree-svg.mjs](examples/tree-svg.mjs) retains elements by ID, restores layer order
|
|
376
|
+
and removes stale elements even after seeking skips exit transitions.
|
|
377
|
+
No tree-specific algorithm or renderer is added to the package.
|
|
378
|
+
|
|
379
|
+
### Graph breadth-first search
|
|
380
|
+
|
|
381
|
+
Open [examples/graph.html](examples/graph.html) and choose a start vertex.
|
|
382
|
+
The example explores only its connected component, marks vertices when enqueued
|
|
383
|
+
and shows shortest unweighted distances. Its custom vertex, connection and queue
|
|
384
|
+
entry types are projected from immutable algorithm state. Both playback modes use
|
|
385
|
+
the same generator; changing the start creates a new traversal.
|
|
386
|
+
|
|
387
|
+
See [Building a custom graph example](docs/custom-graph-example.md) for the
|
|
388
|
+
algorithm, projection and renderer boundaries. This example uses the existing
|
|
389
|
+
public APIs and adds no graph-specific package exports.
|
|
450
390
|
|
|
451
|
-
|
|
391
|
+
## Development
|
|
452
392
|
|
|
453
|
-
|
|
393
|
+
Requires Node.js 22 or later and npm.
|
|
454
394
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
395
|
+
```sh
|
|
396
|
+
npm ci
|
|
397
|
+
npm test
|
|
398
|
+
npm run test:package
|
|
399
|
+
```
|
|
459
400
|
|
|
460
|
-
|
|
401
|
+
`npm test` builds the library, checks TypeScript fixtures and runs the tests.
|
|
402
|
+
`npm run test:package` builds a tarball, installs it offline in an isolated temporary
|
|
403
|
+
consumer, compiles against the installed declarations and exercises public APIs,
|
|
404
|
+
including compatibility exports. The temporary consumer is removed afterward.
|
|
461
405
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
the installed declarations and runs an external algorithm and custom scene with
|
|
465
|
-
both playback modes. It also checks the array adapter and legacy compatibility.
|
|
466
|
-
The temporary consumer is removed afterward. The release workflow runs this check
|
|
467
|
-
before publishing. Source and issues: [urffin/algiviz](https://github.com/urffin/algiviz).
|
|
468
|
-
Publishing is a separate maintainer action.
|
|
469
|
-
|
|
470
|
-
### Publishing from GitHub Releases
|
|
471
|
-
|
|
472
|
-
The `.github/workflows/npm-publish.yml` workflow publishes to npm when a stable
|
|
473
|
-
GitHub Release is published (`release: published`). Drafts, prereleases and tag
|
|
474
|
-
pushes alone do not publish a package. The release tag must be `v` followed by
|
|
475
|
-
the exact version in `package.json` and `package-lock.json` (for example `v0.4.0`).
|
|
476
|
-
The workflow uses Node.js 24 and npm 11. The publish lifecycle runs the tests and
|
|
477
|
-
build before publishing with provenance, using npm trusted publishing (OIDC).
|
|
478
|
-
|
|
479
|
-
One-time setup in the npm package settings, under **Trusted Publisher**:
|
|
480
|
-
|
|
481
|
-
- Provider: GitHub Actions.
|
|
482
|
-
- Organization or user: `urffin`.
|
|
483
|
-
- Repository: `algiviz`.
|
|
484
|
-
- Workflow filename: `npm-publish.yml` (without the directory).
|
|
485
|
-
- Environment: leave empty; this workflow does not use a GitHub environment.
|
|
486
|
-
- Allow direct publishing with `npm publish` if the settings show this option.
|
|
487
|
-
|
|
488
|
-
No `NPM_TOKEN` secret is required. Keep account 2FA enabled.
|
|
489
|
-
See [npm trusted publishing](https://docs.npmjs.com/trusted-publishers/).
|
|
406
|
+
GitHub Actions runs both checks on pushes to `master` and pull requests, using
|
|
407
|
+
Node.js 22 and 24 on Linux and Windows. CI checks do not publish packages.
|
|
490
408
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
reuse an already published npm version; if setup failed before publication,
|
|
495
|
-
correct the settings and rerun the failed workflow.
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
All browser examples share playback defaults from examples/player-settings.mjs:
|
|
499
|
-
The browser controller in `examples/demo-player.mjs` also owns animation scheduling,
|
|
500
|
-
mode changes, stepping, seeking, restart, reduced motion and generator cleanup.
|
|
501
|
-
650 ms per step, 1,000 ms final hold, speeds 0.25× through 32×, history/generator
|
|
502
|
-
modes and forward/backward step controls. Backward stepping and seeking are
|
|
503
|
-
disabled in generator mode. Array input limits are 64 for history and 2,000
|
|
504
|
-
for generators. Reduced motion disables interpolation; hidden tabs pause.
|
|
505
|
-
All sorting demos also share array generation: choose the element count and
|
|
506
|
-
Random, Sorted or Reverse order, then Generate & load. The generated values
|
|
507
|
-
appear in the input field and obey the selected playback mode's limit.
|
|
508
|
-
|
|
509
|
-
## Quick sort partitions example
|
|
510
|
-
|
|
511
|
-
Build and serve the repository root, then open [examples/quick.html](examples/quick.html).
|
|
512
|
-
Choose Lomuto or three-way partitioning, preserving loaded values when switching.
|
|
513
|
-
Both use the shared demo player: history supports seeking; generator mode retains
|
|
514
|
-
adjacent snapshots and supports forward stepping. Speed, restart, reduced motion
|
|
515
|
-
and hidden-tab pausing follow the other examples. Both modes are capped at 64
|
|
516
|
-
finite values here because direct recursion and worst-case traces are expensive.
|
|
517
|
-
This is a demo limit, not a library restriction.
|
|
518
|
-
|
|
519
|
-
The example owns its algorithm and scene and uses the public /scene and /playback
|
|
520
|
-
APIs. Three-way recursion skips the equal band: n >= 2 equal items take 2n scalar
|
|
521
|
-
checks and zero swaps, versus n(n-1)/2 checks with this Lomuto variant. Both may
|
|
522
|
-
be unstable and take quadratic worst-case time. The recursion stack can reach
|
|
523
|
-
O(n); stored history has a separate O(n³) memory upper bound. This adds no new
|
|
524
|
-
package exports and does not require a package release.
|
|
409
|
+
`npm pack` rebuilds automatically. The archive includes dist, README, LICENSE,
|
|
410
|
+
CHANGELOG and package metadata. Source and issues:
|
|
411
|
+
[urffin/algiviz](https://github.com/urffin/algiviz). Licensed under MIT.
|
|
525
412
|
|
|
526
413
|
### Performance checks
|
|
527
414
|
|
|
528
415
|
Run `npm run benchmark` for 2,000-element insertion, bubble, merge and both quick-sort
|
|
529
416
|
variants. It reports generation of 1,000 steps, median rendering CPU time over five
|
|
530
|
-
100-frame samples
|
|
531
|
-
25,000
|
|
532
|
-
GPU measurements. Heap samples use explicit GC and include the Node process;
|
|
533
|
-
are diagnostics, not portable pass/fail thresholds.
|
|
534
|
-
|
|
535
|
-
|
|
417
|
+
100-frame samples and retained Node heap after each 5,000 playback steps, up to
|
|
418
|
+
25,000. Drawing uses a mock Canvas: these are CPU comparisons, not browser FPS or
|
|
419
|
+
GPU measurements. Heap samples use explicit GC and include the Node process;
|
|
420
|
+
they are diagnostics, not portable pass/fail thresholds.
|
|
421
|
+
|
|
422
|
+
## Release
|
|
423
|
+
|
|
424
|
+
Publishing is a maintainer action. Update the package version, lockfile and changelog;
|
|
425
|
+
run `npm test` and `npm run test:package`, commit and push, and check CI on the
|
|
426
|
+
candidate commit.
|
|
427
|
+
|
|
428
|
+
The [publish workflow](.github/workflows/npm-publish.yml) runs when a stable GitHub
|
|
429
|
+
Release is published. Drafts, prereleases and tag pushes alone do not publish.
|
|
430
|
+
Use `v` followed by the exact package version, such as `v0.6.0`, at the tested commit.
|
|
431
|
+
|
|
432
|
+
The workflow uses Node.js 24 and npm 11, checks the version and packed consumer,
|
|
433
|
+
then publishes with provenance through npm trusted publishing (OIDC).
|
|
434
|
+
The publish lifecycle also runs tests and rebuilds. No `NPM_TOKEN` secret is required.
|
|
435
|
+
|
|
436
|
+
The npm trusted publisher must match GitHub owner `urffin`, repository `algiviz`
|
|
437
|
+
and workflow `npm-publish.yml`. The workflow does not use a GitHub environment.
|
|
438
|
+
See [npm trusted publishing](https://docs.npmjs.com/trusted-publishers/).
|
|
439
|
+
|
|
440
|
+
After publication, verify the Actions run and the exact version in npm.
|
|
441
|
+
Never reuse an already published npm version. If a workflow fails, check whether
|
|
442
|
+
publication occurred before deciding whether to rerun it.
|
package/dist/playback/index.d.ts
CHANGED
package/dist/playback/index.js
CHANGED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Step } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Capture state in a frozen, consecutively indexed step. Use one emitter per iterator.
|
|
4
|
+
* The callback owns snapshot isolation; state and event are not cloned or frozen.
|
|
5
|
+
*/
|
|
6
|
+
export declare function createStepEmitter<State>(snapshot: () => State): <Event>(event: Event) => Step<State, Event>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capture state in a frozen, consecutively indexed step. Use one emitter per iterator.
|
|
3
|
+
* The callback owns snapshot isolation; state and event are not cloned or frozen.
|
|
4
|
+
*/
|
|
5
|
+
export function createStepEmitter(snapshot) {
|
|
6
|
+
let index = 0;
|
|
7
|
+
return (event) => {
|
|
8
|
+
const state = snapshot();
|
|
9
|
+
const step = Object.freeze({ index, state, event });
|
|
10
|
+
index++;
|
|
11
|
+
return step;
|
|
12
|
+
};
|
|
13
|
+
}
|