@zakkster/lite-charts 1.0.0 → 1.1.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.
Files changed (6) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/Charts.d.ts +249 -21
  3. package/Charts.js +1653 -482
  4. package/README.md +156 -102
  5. package/llms.txt +276 -126
  6. package/package.json +6 -4
package/README.md CHANGED
@@ -15,21 +15,29 @@
15
15
  > points at 60fps with sub-frame budget. Built on `@zakkster/lite-scene`
16
16
  > (Canvas2D scene graph), `@zakkster/lite-signal` (reactive core), and
17
17
  > `@zakkster/lite-axis` (tick generation). Three peer deps. ESM-only.
18
- > Single-file (~5k lines, three independent kernels). MIT.
19
-
20
- **Status:** v1.0.0 -- seven chart types on three independent kernels.
21
- Line, area, bar, bubble on the axis kernel; pie, donut on the polar
22
- slice kernel; radar on its own kernel. Kernel-side auto-resize via
23
- ResizeObserver -- omit `width` / `height` and the chart tracks its
24
- container. Tree-shake verified with esbuild: each chart's bundle
25
- contains only its kernel + renderer code. Zero runtime dependencies
26
- (three peers: `@zakkster/lite-signal`, `@zakkster/lite-scene`,
27
- `@zakkster/lite-axis`). 196/196 tests pass.
28
-
29
- See [ROADMAP.md](./ROADMAP.md) for the development history that led to
30
- this architecture and the forward plan (stacked bar, heatmap, scatter
31
- with `@zakkster/lite-delaunay` for O(log n) hit-test, log scale, pan +
32
- zoom).
18
+ > ~1100 lines single file. MIT.
19
+
20
+ **Status: v1.1.0** nine chart types across four independent kernels.
21
+ The headline addition is bar-chart layout polish (stacked bars, rounded
22
+ corners, per-bar hover tint, all opt-in). This release also lands several
23
+ features originally scoped for the v1.2.0 alpha train that proved stable
24
+ enough to ship together:
25
+
26
+ - **`createScatterChart`** bubble's simpler sibling on the same axis
27
+ kernel; constant marker, no third dimension.
28
+ - **`createHeatmap`** on a new `createBaseGridChart` kernel (10.5 KB
29
+ minified, the smallest of the nine bundles). Two band scales, flat
30
+ `Float32Array` cell storage, `Uint8Array` presentMask for sparse data,
31
+ per-cell color strings precomputed at extract for zero-allocation
32
+ draws. Default linear-RGB ramp; `colorFn` for custom mappings.
33
+ - **Multi-series bubble** with per-point color via `colorKey` and a
34
+ global size domain across visible series.
35
+ - **Pluggable spatial-index** (`SpatialIndex` / `SpatialIndexFactory`)
36
+ for O(log n) hit-test on dense point clouds.
37
+ `@zakkster/lite-delaunay` is the intended default but optional.
38
+
39
+ **231/231 tests pass.** See [CHANGELOG.md](./CHANGELOG.md) for the full
40
+ release contract and [ROADMAP.md](./ROADMAP.md) for the forward plan.
33
41
 
34
42
  ## Install
35
43
 
@@ -78,7 +86,7 @@ explicit re-render needed.
78
86
  | **Reactive data binding** | First-class signals | Imperative `.update()` | Imperative `.setData()` | Manual selection re-bind |
79
87
  | **100k points** | 1.4 ms / 4.7 ms p95 (CPU) | Drops frames | OK | Hand-rolled |
80
88
  | **Zero-GC steady state** | Yes (slab-based) | No | Mostly | No |
81
- | **Bundle (min, per-chart)** | 13-24 KB (see table) | 78 KB | 40 KB | 70+ KB |
89
+ | **Bundle (min+gz)** | ~6 KB (alpha est.) | 78 KB | 40 KB | 70+ KB |
82
90
  | **Render substrate** | Canvas2D via lite-scene | Canvas2D | Canvas2D | SVG / Canvas |
83
91
  | **API style** | Vega-Lite middle ground | Imperative config | Hand-tuned | Composable primitives |
84
92
  | **Twitch Extension fit** | Yes (1MB / 3s budget) | No | Yes | No |
@@ -93,30 +101,30 @@ in a 1MB bundle without GC pauses.
93
101
 
94
102
  ```mermaid
95
103
  graph TD
96
- User["User config + data signal"] --> Constructor["createLineChart"]
104
+ User[User config + data signal] --> Constructor[createLineChart]
97
105
  Constructor --> Normalize["Normalize: data shorthand -> series[]"]
98
- Normalize --> Accessors["Build accessors x/y"]
99
- Accessors --> InferType["Infer x-scale type"]
100
- InferType --> StateAlloc["Allocate SeriesState slabs"]
101
-
102
- StateAlloc --> Mount["mount(container)"]
103
- Mount --> Scene["createScene from lite-scene"]
104
- Scene --> Effect1["Effect: width/height -> plotBounds"]
105
- Scene --> Effect2["Effect: data -> SoA extract -> scale -> pixels"]
106
- Scene --> Axes["buildAxis x2 / lite-axis ticks"]
107
- Scene --> SeriesNodes["path nodes / one per series"]
108
- SeriesNodes --> DrawFn["makeLineDrawFn closure"]
106
+ Normalize --> Accessors[Build accessors x/y]
107
+ Accessors --> InferType[Infer x-scale type]
108
+ InferType --> StateAlloc[Allocate SeriesState slabs]
109
+
110
+ StateAlloc --> Mount[mount(container)]
111
+ Mount --> Scene[createScene from lite-scene]
112
+ Scene --> Effect1[Effect: width/height -> plotBounds]
113
+ Scene --> Effect2[Effect: data -> SoA extract -> scale -> pixels]
114
+ Scene --> Axes[buildAxis x2 / lite-axis ticks]
115
+ Scene --> SeriesNodes[path nodes / one per series]
116
+ SeriesNodes --> DrawFn[makeLineDrawFn closure]
109
117
 
110
118
  DrawFn --> PathSelect{"n > 2*cols?"}
111
119
  PathSelect -->|yes| Decimate["decimateMinMax kernel<br/>lifted from lite-canvas-graph"]
112
- PathSelect -->|no| Polyline["Direct polyline / NaN-aware"]
113
- Decimate --> Stroke["ctx.stroke"]
120
+ PathSelect -->|no| Polyline[Direct polyline / NaN-aware]
121
+ Decimate --> Stroke[ctx.stroke]
114
122
  Polyline --> Stroke
115
123
 
116
- Signal["Any signal write"] --> LiteSignal["lite-signal sync flush"]
117
- LiteSignal --> EffectsRun["Effects re-run"]
118
- EffectsRun --> DirtyBridge["scaleVersion bump -> scene.markDirty"]
119
- DirtyBridge --> SceneDraw["lite-scene drawAll / coalesced via _queued"]
124
+ Signal[Any signal write] --> LiteSignal[lite-signal sync flush]
125
+ LiteSignal --> EffectsRun[Effects re-run]
126
+ EffectsRun --> DirtyBridge[scaleVersion bump -> scene.markDirty]
127
+ DirtyBridge --> SceneDraw[lite-scene drawAll / coalesced via _queued]
120
128
  SceneDraw --> DrawFn
121
129
  ```
122
130
 
@@ -217,7 +225,7 @@ const chart = createLineChart({ ..., schedule: (fn) => queueMicrotask(fn) });
217
225
 
218
226
  ## Tooltip + crosshair
219
227
 
220
- On by default. The crosshair vertical line snaps to the
228
+ On by default in v1.0.0-alpha.1. The crosshair vertical line snaps to the
221
229
  nearest sample on the primary series (binary search on sorted xs); markers
222
230
  on each additional series snap independently at the same domain x. The
223
231
  tooltip is canvas-drawn (no DOM overlay), so it remains headless-testable.
@@ -265,7 +273,7 @@ against the mock canvas (no event simulation needed). The mock canvas in
265
273
  `test/harness.js` doesn't implement `addEventListener`, so the DOM listener
266
274
  is skipped in headless contexts -- the programmatic API is the only way in.
267
275
 
268
- ## Area chart
276
+ ## Area chart (v1.0.0-alpha.2)
269
277
 
270
278
  `createAreaChart(config)` shares everything with `createLineChart` -- same
271
279
  data shape, same accessors, same scales, same reactivity, same crosshair
@@ -295,7 +303,7 @@ for sparse data, decimated per-column for dense. The decimated path fills
295
303
  to the column's upper envelope (max), matching d3-area's default behavior;
296
304
  ribbon-style min-max area is a separate primitive in v1.1+.
297
305
 
298
- ## Legend
306
+ ## Legend (v1.0.0-alpha.3)
299
307
 
300
308
  Rendered as a DOM element (sibling of the canvas, inside an auto-created
301
309
  flex wrapper), so it's keyboard-accessible (each row is a `<button>` with
@@ -354,10 +362,9 @@ const showOnly = (idx) => {
354
362
  };
355
363
  ```
356
364
 
357
- ## Path interpolation
365
+ ## Path interpolation (v1.0.0)
358
366
 
359
- Six modes (seven names -- `'step'` is an alias for `'step-after'`).
360
- Default is `'linear'` (the polyline). Three step variants for
367
+ Seven modes. Default is `'linear'` (the polyline). Three step variants for
361
368
  discrete data (telemetry, state machines, financial OHLC). Two smoothing
362
369
  modes for continuous data.
363
370
 
@@ -394,7 +401,7 @@ the direct path.
394
401
  renders independently). Smoothing modes assume contiguous data; if you need
395
402
  gaps, use linear or step.
396
403
 
397
- ## Markers
404
+ ## Markers (v1.0.0)
398
405
 
399
406
  Marker dots at each sample point. Distinct from crosshair markers (those
400
407
  appear only on hover).
@@ -425,7 +432,7 @@ createLineChart({ data: dense, markers: { everyN: 10 } });
425
432
  **Decimation interaction:** markers are suppressed when the decimated path
426
433
  runs (>2x plot width). They'd be unreadable.
427
434
 
428
- ## Theme reactivity
435
+ ## Theme reactivity (v1.0.0)
429
436
 
430
437
  Colors passed as `'--token-name'` get resolved against the container's
431
438
  computed style at mount. When you switch themes (dark mode, brand swap),
@@ -455,7 +462,7 @@ re-resolve. Legend swatches update too.
455
462
  > `chart.refreshTheme()`, or pair it with whatever theme-change event your
456
463
  > framework emits.
457
464
 
458
- ## Bar chart
465
+ ## Bar chart (v1.1.0-alpha.0)
459
466
 
460
467
  ```javascript
461
468
  import { createBarChart } from '@zakkster/lite-charts';
@@ -508,7 +515,7 @@ multi-series (which is the right default -- stacked introduces design
508
515
  choices around shared y-domain and tooltip ordering that are worth a
509
516
  dedicated session).
510
517
 
511
- ## Tree-shakeable architecture
518
+ ## Tree-shakeable architecture (v1.2.0)
512
519
 
513
520
  `lite-charts` is built on a tiny shared kernel that's parameterized by a
514
521
  **renderer object** per chart type:
@@ -535,17 +542,22 @@ rest, along with all their renderer-specific helpers.
535
542
  | Entry | Bundle size | What's included |
536
543
  |---|---|---|
537
544
  | `import { createLineChart }` | **24 KB** | Line renderer + interp helpers + decimation + shared axis kernel + auto-resize |
538
- | `import { createAreaChart }` | **24 KB** | Area renderer + interp helpers + decimation + shared axis kernel + auto-resize |
539
- | `import { createBarChart }` | **23 KB** | Bar renderer + bandScale + bar helpers + shared axis kernel + auto-resize |
540
- | `import { createBubbleChart }` | **22 KB** | Bubble renderer + sqrt size scale + distance hit-test + axis kernel + auto-resize |
541
- | `import { createPieChart }` | **14 KB** | Slice renderer + polar kernel (no axes / scales / interp / decimation) + auto-resize |
542
- | `import { createDonutChart }` | **14 KB** | Same as pie (shared renderer; only innerRadius default differs) |
545
+ | `import { createAreaChart }` | **25 KB** | Area renderer + interp helpers + decimation + shared axis kernel + auto-resize |
546
+ | `import { createBarChart }` | **25 KB** | Bar renderer + bandScale + bar helpers + shared axis kernel + auto-resize + **stack / rounded / hover (v1.1.0)** |
547
+ | `import { createBubbleChart }` | **25 KB** | Bubble renderer + sqrt size scale + distance hit-test + axis kernel + auto-resize + **spatial-index hook (v1.2.0-alpha.0)** + **multi-series + per-point color (v1.2.0-alpha.2)** |
548
+ | `import { createScatterChart }` | **22 KB** | Scatter renderer + axis kernel + spatial-index hook (v1.2.0-alpha.1) |
549
+ | `import { createPieChart }` | **13 KB** | Slice renderer + polar kernel (no axes / scales / interp / decimation) + auto-resize |
550
+ | `import { createDonutChart }` | **13 KB** | Same as pie (shared renderer; only innerRadius default differs) |
543
551
  | `import { createRadarChart }` | **13 KB** | Radar kernel (cos/sin tables, polygon draw, spokes, grid rings, vertex hit-test) -- zero axis/polar code |
544
- | All seven together | **56 KB** | Axis + polar + radar kernels deduplicated; all renderers; shared utilities (resolveColor, ensureFloat32, mount/DPR, legend, auto-resize) shared once |
552
+ | `import { createHeatmap }` | **10.5 KB** | **Grid kernel (v1.2.0-alpha.3)** -- two band scales, Float32 cells, Uint8 presentMask, precomputed cell colors. Zero axis / polar / radar code. |
553
+ | All nine together | **~70 KB** | Four kernels deduplicated; all renderers; shared utilities (resolveColor, ensureFloat32, mount/DPR, legend, auto-resize) shared once |
545
554
 
546
- The ~400 byte uptick from earlier alphas is the kernel-side
547
- `ResizeObserver` helper -- shared across all kernels, inlined into
548
- each `mount()`.
555
+ The v1.1.0 bar features (stacked layout, rounded corners, hover tint)
556
+ add ~1.6 KB to the bar bundle (`computeBarStacks`, `_roundRectPath`,
557
+ the per-bar tint overlay path). The kernel-level `postExtract` hook is
558
+ a single null-check that minifies to a few dozen bytes; line / area /
559
+ bubble bundles each pick up ~300 bytes for it. Pie / donut / radar are
560
+ on different kernels and unaffected.
549
561
 
550
562
  **Auto-resize:** omit `width` / `height` from the config and the chart
551
563
  observes its mount container, updating dimensions on container resize
@@ -586,21 +598,16 @@ nearest-vertex hit-test.
586
598
  The same architecture extends to upcoming chart families:
587
599
 
588
600
  ```javascript
589
- // Pie + donut already ship -- shared SLICE_RENDERER on the polar kernel
590
- // (only the innerRadius default differs between the two factories).
601
+ // v1.3.0 -- pie family (no axes, polar coordinates)
591
602
  const createBasePolarChart = (config, renderer) => { /* polar scaffold */ };
592
- export const createPieChart = (c) => createBasePolarChart(_pieCfg(c), SLICE_RENDERER);
593
- export const createDonutChart = (c) => createBasePolarChart(_donutCfg(c), SLICE_RENDERER);
603
+ export const createPieChart = (c) => createBasePolarChart(c, PIE_RENDERER);
604
+ export const createDonutChart = (c) => createBasePolarChart(c, DONUT_RENDERER);
605
+ export const createRadarChart = (c) => createBasePolarChart(c, RADAR_RENDERER);
594
606
 
595
- // Radar has its own kernel -- no renderer indirection in v1.0.0 because
596
- // there's only one radar variant. A second variant will trigger the
597
- // extraction into createBaseRadarChart(config, renderer).
598
- export const createRadarChart = (config) => { /* radar kernel inline */ };
599
-
600
- // Bubble ships too -- BUBBLE_RENDERER on the axis kernel.
607
+ // v1.3.0 -- scatter family (extends axis chart with size dimension)
601
608
  export const createBubbleChart = (c) => createBaseAxisChart(c, BUBBLE_RENDERER);
602
609
 
603
- // v1.2.0 -- heatmap (2D categorical grid) lands on a new kernel.
610
+ // v1.4.0 -- heatmap (2D categorical grid)
604
611
  const createBaseGridChart = (config, renderer) => { /* grid scaffold */ };
605
612
  export const createHeatmap = (c) => createBaseGridChart(c, HEATMAP_RENDERER);
606
613
  ```
@@ -639,37 +646,20 @@ v1.0.1) measures real paint.
639
646
 
640
647
  ### Zero-GC discipline
641
648
 
642
- Hot-path allocation target: <100 bytes/cycle. Current measurement on
643
- the `bench/line-100k.mjs` full-update cycle (200-sample mean after
644
- `global.gc()`) sits at roughly 270-330 bytes/cycle in Node 22, which is
645
- within `process.memoryUsage().heapUsed` noise band but still above the
646
- target. The library-side accounting:
647
-
648
- - **Per-frame line draw closure: 0 bytes.** Verified by the
649
- `decimateMinMax` zero-alloc test in `test/Charts.test.js` (asserts
650
- <100 B/call across 200 iterations under `--expose-gc`).
651
- - **Per-mousemove tooltip path: 0 row objects, 0 ctx objects.** The
652
- `_tooltipRowPool` and `_tooltipFormatterCtx` are chart-instance
653
- scratch; only `formatTooltipValue` allocates the value string itself
654
- (already minimised via the shared char buffer + `String.fromCharCode`).
655
- Identity-stability is enforced by the
656
- `tooltip pool zero-alloc contract` test suite.
657
- - **Per-data-update y-domain: 0 bytes.** `_yDomScratch` is a 2-element
658
- array filled by `_niceYDomainInto`; the legacy `niceYDomain` that
659
- returned a fresh `[lo, hi]` is still exported on `_testHelpers` for
660
- the test API but not used by the kernel.
661
- - **Axis label string concatenation via `String.fromCharCode`** still
662
- allocates ~5-10 B per tick label rebuild (~120 B for 20 labels), but
663
- only fires on size or domain change, not per frame. Switching to a
664
- shared `Uint8Array` and stringifying only when text changes is a
665
- v1.1 nicety.
666
- - **Bench harness `{xs, ys}` wrapper** is allocated by the test rotor
667
- itself (~32 B per cycle), not the library. The honest framing: most
668
- of the remaining heap delta is V8 measurement noise plus the bench
669
- harness's own object literal.
670
-
671
- None of these touch the per-frame draw -- they're in the data / axis
672
- update path that fires only on actual changes, or in the bench harness.
649
+ Hot-path allocations target: <100 bytes/cycle. Currently measured at ~270
650
+ bytes/cycle, attributable to:
651
+
652
+ - `{xs, ys}` object literal allocated by the test rotor (~16 B)
653
+ - `niceYDomain` returning a fresh `[lo, hi]` tuple (~40 B; fix in v1.0.1)
654
+ - Axis label string concatenation via `String.fromCharCode` (~120 B for 20
655
+ labels; fix by switching to a shared `Uint8Array` and only stringifying
656
+ when text actually changes)
657
+ - Promise allocations from `queueMicrotask`-based draining
658
+
659
+ None of these touch the per-frame line render -- they're in the data/axis
660
+ update path that fires only on actual changes. The line draw closure
661
+ itself is fully allocation-free in steady state (verified by the
662
+ `decimateMinMax` zero-GC test in `test/charts.test.js`).
673
663
 
674
664
  ### What's measured, what isn't
675
665
 
@@ -679,21 +669,85 @@ update path that fires only on actual changes, or in the bench harness.
679
669
  | Decimation kernel zero-alloc | YES | Test asserts <100 bytes/call |
680
670
  | GPU paint cost | NO | Browser bench coming v1.0.1 |
681
671
  | Cold-start overhead | NO | Single-figure ms; not yet measured |
682
- | Bundle size minified | YES | Per-chart 13-24 KB, all seven ~56 KB (esbuild --bundle --minify) |
672
+ | Bundle size min+gz | NO | Single-file ESM, ~6 KB estimated; not yet minified |
673
+
674
+ ## Capacity considerations
675
+
676
+ lite-charts builds on `@zakkster/lite-signal`, which pre-allocates a
677
+ fixed-size arena for its reactive nodes (signals + effects). The default
678
+ capacity is **1024 nodes**, which fits a typical app with a few charts
679
+ but can be exhausted on dashboards or demos with many simultaneous
680
+ charts. If you see a `CapacityError: nodes capacity (1024) exceeded`,
681
+ this is the cause.
682
+
683
+ **Per-chart active node footprint** (measured against the v1.1.0
684
+ implementation, on a chart with default options at typical sizes):
685
+
686
+ | Chart | Active nodes |
687
+ |---|---|
688
+ | `createLineChart` | ~43 |
689
+ | `createAreaChart` | ~43 |
690
+ | `createBarChart` | ~60 (3 series x 10 cats) |
691
+ | `createBubbleChart` | ~46 |
692
+ | `createScatterChart` | ~46 |
693
+ | `createPieChart` | ~25 |
694
+ | `createDonutChart` | ~25 |
695
+ | `createRadarChart` | ~50 |
696
+ | `createHeatmap` | ~5 |
697
+
698
+ The dominant cost on axis-kernel charts is the per-axis tick pool:
699
+ each tick allocates a `lineNode` and a `textNode` (the label), each of
700
+ which creates one lite-scene effect. At max tick count (12 per axis)
701
+ that's ~24 effect nodes per axis x 2 axes = ~48 per chart. Heatmap is
702
+ unusually cheap because the grid kernel renders cells through a single
703
+ `pathNode`-per-layer rather than per-cell scene nodes.
704
+
705
+ **Rule of thumb**: the default 1024-node arena fits ~15-20 axis-kernel
706
+ charts on a single page. Multiply by the headroom you want for safety.
707
+
708
+ **Bumping the arena** -- call `setDefaultRegistry` BEFORE constructing
709
+ any chart:
710
+
711
+ ```js
712
+ import { createRegistry, setDefaultRegistry } from '@zakkster/lite-signal';
713
+
714
+ // 32k nodes -- comfortable headroom for dashboards or demos. The arena
715
+ // is a few tens of KB of memory, so this is cheap.
716
+ setDefaultRegistry(createRegistry({ maxNodes: 32768 }));
717
+
718
+ // ... THEN construct your charts:
719
+ import { createLineChart } from '@zakkster/lite-charts';
720
+ const chart = createLineChart({ /* ... */ });
721
+ ```
722
+
723
+ Order matters: charts read the *current* default registry at
724
+ construction time. Bumping after charts are already created doesn't
725
+ help those charts. The lite-charts demo (`demo/index.html`) bumps to
726
+ 32768 at the very top for this reason.
727
+
728
+ **Mount/unmount note**: each mount/unmount cycle leaves a small
729
+ residue (~4 reactive nodes per chart, from construction-time signals
730
+ that aren't disposed in `unmount` so the chart can be remounted).
731
+ For apps that create and destroy many charts dynamically over a long
732
+ session, that residue accumulates until the chart reference is dropped
733
+ and the lite-signal arena slots become eligible for reclamation.
734
+ A dedicated terminal-teardown `chart.destroy()` is on the roadmap for
735
+ v1.3.
683
736
 
684
737
  ## Roadmap
685
738
 
686
- v1.0.0 ships seven chart types on three independent kernels with
739
+ v1.1.0 ships nine chart types on four independent kernels with
687
740
  kernel-side auto-resize. See [ROADMAP.md](./ROADMAP.md) for the full
688
741
  forward plan and the development history that led here. Headlines:
689
742
 
690
743
  | Version | Scope |
691
744
  |---|---|
692
- | **v1.0.0** (this) | Seven chart types, three kernels, auto-resize, 196 tests, full tree-shake verification |
693
- | v1.1.0 | Stacked + grouped bar; per-bar hover tint; rounded corners; SVG export across all charts |
694
- | v1.2.0 | `createHeatmap` on new grid kernel; multi-series bubble + per-point colour; `createScatterChart` with `@zakkster/lite-delaunay` for O(log n) nearest-point hit-test |
695
- | v1.3.0 | Log scale; pan + zoom; brushing primitives |
696
- | v1.4.0 | Time-series specialized variants; legend virtualization via `lite-virtual`; annotation layer |
745
+ | **v1.0.0** | Seven chart types, three kernels, auto-resize, 182 tests, full tree-shake verification. |
746
+ | **v1.1.0** (this release) | Bar polish (stacked, rounded corners, hover tint) **plus** the features prototyped over four internal alphas now landing in one go: pluggable spatial index for bubble hit-test (auto-engages ≥1000 points), `createScatterChart` (eighth type), multi-series bubble + per-point color + global size domain, and `createHeatmap` on a new `createBaseGridChart` kernel (10.5 KB minified, the smallest of the nine). 231 tests. |
747
+ | v1.2.0 | Heatmap polish (per-row / per-column highlight on hover, quantile binning); doc + release notes. |
748
+ | v1.3.0 | SVG export across all nine charts (mirrors every draw fn through SVG path commands; pixel-identical output). |
749
+ | v1.4.0 | Log scale; pan + zoom; brushing primitives. |
750
+ | v1.5.0 | Time-series specialized variants; legend virtualization via `lite-virtual`; annotation layer. |
697
751
 
698
752
  ## Ecosystem
699
753