@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/llms.txt CHANGED
@@ -1,30 +1,49 @@
1
1
  # @zakkster/lite-charts
2
2
 
3
3
  > Reactive, zero-GC chart library built on @zakkster/lite-scene. Signals
4
- > for data, dimensions, theme, series visibility. v1.0.0 ships SEVEN
5
- > chart types on THREE independent kernels: line/area/bar/bubble on the
6
- > axis kernel, pie/donut on the polar slice kernel, radar on its own
7
- > kernel. Kernel boundaries are strict and verified with esbuild tree-
8
- > shaking: line bundle = 24 KB minified, bubble = 22 KB, pie = 14 KB,
9
- > radar = 13 KB. Importing only radar drops every axis-chart AND every
10
- > polar-slice helper. Importing only bubble drops every polar AND radar
11
- > helper. Etc. Kernel-side auto-resize: omit width / height from config
12
- > and the chart observes its mount container via ResizeObserver,
13
- > updating through the existing reactive graph (synchronous initial read
14
- > avoids size pop; rAF-throttled updates coalesce burst events). Falls
15
- > back gracefully (keeps default size) when ResizeObserver is absent.
16
- > Bubble uses sqrt size scale by default (area-proportional, Tukey
17
- > convention): r = sqrt(rMin^2 + t*(rMax^2 - rMin^2)). Radar precomputes
18
- > cos/sin per axis into Float64 tables -- polygons, grid rings, and
19
- > spokes share them, zero per-frame trig. Hit detection is nearest-vertex
20
- > within 12 px across visible series. Pie and donut share SLICE_RENDERER;
21
- > only innerRadius default differs (0 vs 0.5). Polar angles use Float64
22
- > (Float32(PI/2) widens enough to misclassify boundary hits). 196/196
23
- > tests pass. ESM-only. Single-file ~4.9k lines. Three peer deps
24
- > (lite-signal, lite-scene, lite-axis), zero runtime deps. MIT.
25
- > See ROADMAP.md for forward plan: v1.1 stacked bar + SVG export, v1.2
26
- > heatmap + scatter + @zakkster/lite-delaunay for dense hit-test, v1.3
27
- > log scale + pan/zoom, v1.4 time-series + annotations.
4
+ > for data, dimensions, theme, series visibility. v1.1.0 ships NINE
5
+ > chart types on FOUR independent kernels: line/area/bar/bubble/scatter
6
+ > on the axis kernel, pie/donut on the polar slice kernel, radar on its
7
+ > own kernel, and heatmap on its own grid kernel. Kernel boundaries are
8
+ > strict and verified with esbuild tree-shaking: line bundle = 23 KB
9
+ > minified, bar = 25 KB (stacked + rounded + hover), bubble = 25 KB
10
+ > (spatial-index + multi-series + per-point color), scatter = 22 KB,
11
+ > pie = 13 KB, radar = 13 KB, **heatmap = 10.5 KB (the smallest)**.
12
+ > all-nine total: ~70 KB. Kernel-side auto-resize: omit width / height
13
+ > from config and the chart observes its mount container via
14
+ > ResizeObserver.
15
+ > Heatmap: createHeatmap rides createBaseGridChart -- a kernel that
16
+ > knows nothing about axis / polar / radar code. Two band scales (x +
17
+ > y) reuse makeBandScale math; the y band scale uses the same +y-down
18
+ > convention so yBand.leftEdge(0) is the topmost cell. Cell storage:
19
+ > flat Float32Array indexed yIdx*nx + xIdx; Uint8Array presentMask
20
+ > for sparse data (missing cells render as empty space, hit-test
21
+ > returns null for them). Per-cell colors precomputed at extract into
22
+ > state.cellColors: string[] so the draw loop is just
23
+ > fillStyle = cellColors[i]; fillRect(...) -- zero alloc per cell.
24
+ > Default ramp is linear RGB interpolation between two endpoint hex
25
+ > colors via _parseHexColor + _lerpRGBString. The colorFn(v, vMin,
26
+ > vMax) -> css config overrides entirely for OKLCH ramps, quantile
27
+ > binning, diverging schemes. Hit-test is O(1) -- one xBand.invert +
28
+ > one yBand.invert + a presence-mask check. chart.hover is a
29
+ > signal-style reactive accessor with .peek() escape hatch. Mouse
30
+ > events on canvas drive moveHover / hideHover automatically.
31
+ > Bar polish (this release): postExtract hook for stacked layout,
32
+ > native ctx.roundRect with arcTo fallback for rounded corners,
33
+ > per-bar hover tint via reusable cached tinted-color strings.
34
+ > Multi-series bubble: per-point color via colorKey, global size
35
+ > domain across visible series so equal sizes render at equal radii
36
+ > regardless of which series owns the point.
37
+ > Pluggable spatial index: SpatialIndex / SpatialIndexFactory
38
+ > contract auto-engages at ~1000 points for bubble + scatter
39
+ > hit-test; @zakkster/lite-delaunay is the intended default but
40
+ > optional. 231/231 tests pass.
41
+ > ESM-only. Single-file ~6.2k lines. Four peer deps (lite-signal,
42
+ > lite-scene, lite-axis required; lite-delaunay optional), zero
43
+ > runtime deps. MIT.
44
+ > See ROADMAP.md for forward plan: v1.2 heatmap polish (per-row /
45
+ > per-column highlight, quantile binning), v1.3 SVG export, v1.4 log
46
+ > scale + pan/zoom, v1.5 time-series + annotations.
28
47
 
29
48
  ## Why use this library
30
49
 
@@ -37,48 +56,39 @@
37
56
 
38
57
  ## When NOT to use
39
58
 
40
- - You need SVG output -- coming v1.1. Today only `exportPNG` (canvas
41
- `toDataURL`).
59
+ - You need SVG output -- coming v1.3. Today only `exportPNG`.
42
60
  - You need server-side rendering -- the renderer is Canvas2D-bound.
43
- - You need stacked bars today -- only grouped multi-series in v1.0.0;
44
- stacked layout lands v1.1.
45
- - You need heatmap or scatter today -- on the v1.2.0 roadmap with the
46
- new grid kernel and the `@zakkster/lite-delaunay` integration.
47
- - You need legend virtualization for hundreds of series -- v1.4.0 via
48
- `@zakkster/lite-virtual`.
49
- - You need `<2000` points and don't care about GC -- Chart.js is simpler.
61
+ - You need `<200` points across one chart and don't care about GC --
62
+ Chart.js is simpler and the bundle-size argument is weaker at that
63
+ scale.
64
+ - You need built-in heatmap, scatter, or polar-area charts today -- on
65
+ the roadmap (v1.2.0 +) but not yet shipped.
50
66
 
51
67
  ## Module shape
52
68
 
53
69
  ESM-only. Named exports from the single `Charts.js` entry:
54
70
 
55
71
  ```ts
56
- // Axis kernel
57
- function createLineChart(config: LineChartConfig): Chart;
58
- function createAreaChart(config: AreaChartConfig): Chart;
59
- function createBarChart(config: BarChartConfig): Chart;
72
+ function createLineChart(config: LineChartConfig): Chart;
73
+ function createAreaChart(config: AreaChartConfig): Chart;
74
+ function createBarChart(config: BarChartConfig): Chart;
60
75
  function createBubbleChart(config: BubbleChartConfig): Chart;
61
-
62
- // Polar slice kernel (pie + donut share SLICE_RENDERER; only the
63
- // innerRadius default differs).
64
- function createPieChart(config: PieChartConfig): PolarChart;
65
- function createDonutChart(config: DonutChartConfig): PolarChart;
66
-
67
- // Radar kernel (third independent kernel)
68
- function createRadarChart(config: RadarChartConfig): RadarChart;
69
-
70
- // Stubs that throw at runtime so version mismatches surface immediately.
71
- // These ship in v1.2.0 on a new createBaseGridChart kernel.
72
- function createScatterChart(): never;
73
- function createHeatmap(): never;
74
-
75
- // Test-only export -- NOT part of the stable public API. Pure helpers
76
- // for white-box unit testing; the leading underscore signals private.
77
- // Production code that imports only chart factories never references
78
- // _testHelpers, so the bundler drops it and everything it pins.
79
- const _testHelpers: { /* decimateMinMax, makeBandScale, ... */ };
76
+ function createScatterChart(config: ScatterChartConfig): Chart;
77
+ function createPieChart(config: PieChartConfig): Chart;
78
+ function createDonutChart(config: DonutChartConfig): Chart;
79
+ function createRadarChart(config: RadarChartConfig): Chart;
80
+ function createHeatmap(config: HeatmapConfig): Chart;
81
+
82
+ // Public interface for the spatial-index integration (v1.2.0-alpha.0+).
83
+ interface SpatialIndex {
84
+ findNearest(qx, qy, k, maxDistSq, outIndices, outDistSq): number;
85
+ dispose(): void;
86
+ }
87
+ type SpatialIndexFactory = (pxs, pys, n) => SpatialIndex;
80
88
  ```
81
89
 
90
+ Nine chart types across four strictly-independent kernels.
91
+
82
92
  `AreaChartConfig extends LineChartConfig` with three additional fields:
83
93
 
84
94
  ```ts
@@ -395,77 +405,217 @@ createLineChart({
395
405
  many `node.set()` calls per signal write, use
396
406
  `schedule: (fn) => queueMicrotask(fn)`. The bench documents this clearly.
397
407
 
398
- ## Roadmap
399
-
400
- - v1.0.0-beta.0 - beta.3: line + area polish (interp, markers, theme, gridlines,
401
- zero-alloc crosshair, DPR fix)
402
- - v1.0.0: line + area API lock
403
- - v1.1.0-alpha.0: createBarChart (band scale, grouped, O(1) hit detection)
404
- - **v1.2.0-alpha.0: architectural refactor.** _createChartImpl(config,
405
- renderKind) extracted into createBaseAxisChart(config, renderer) + per-
406
- chart renderer objects (LINE_RENDERER, AREA_RENDERER, BAR_RENDERER). 22
407
- renderKind branches eliminated. Polymorphic dispatch via renderer
408
- interface: buildXAccessor, createXScale, extractData, updateXScale,
409
- buildXAxis, makeDrawFn, hitTest, lookupRow, formatTooltipHeader.
410
- rendererCtx singleton (xScale, yScale, opts, categoriesRef) mutated in
411
- place -- preserves zero-alloc on hot path. _testHelpers moved to separate
412
- top-level export so chart._internal doesn't pin pure helpers.
413
- - **v1.2.0-alpha.1: pie + donut chart family.** New
414
- createBasePolarChart kernel -- completely independent from
415
- createBaseAxisChart. Polar state struct: parallel arrays for values
416
- (Float32), labels (string[]), colors, visibility (Uint8), startAngles
417
- (Float64 -- Float32 widens PI/2 enough that exact-boundary hits land in
418
- wrong slice), arcAngles (Float64). extractSliceData normalizes array-of-
419
- objects, parallel-arrays, or plain number arrays. computeSliceGeometry
420
- centers in plot rect with configurable inner radius. sliceHitTest is
421
- O(n) atan2 + linear scan inside ring (n typically 3-12; binary search
422
- overkill). makeSliceDrawFn renders wedge (pie) or arc-ring (donut) per
423
- slice; hovered slice expands 4px. Per-slice legend with click-to-toggle
424
- visibility -- hidden slices give up their wedge, others grow to fill.
425
- Pie and donut share SLICE_RENDERER; only innerRadius default differs
426
- (0 vs 0.5; overridable). Pie bundle 13 KB minified (no axis kernel),
427
- line bundle 23 KB (no polar kernel). 29 new tests, 148 total.
428
- - v1.2.0-alpha.2: slice colour resolution bug fix (raw CSS-var leaked
408
+ ## Release history (shipped)
409
+
410
+ - v1.0.0-beta.0 - beta.3: line + area polish (interp, markers, theme,
411
+ gridlines, zero-alloc crosshair, DPR fix)
412
+ - v1.0.0 alpha.0: architectural refactor.
413
+ `_createChartImpl(config, renderKind)` extracted into
414
+ `createBaseAxisChart(config, renderer)` + per-chart renderer objects
415
+ (LINE_RENDERER, AREA_RENDERER, BAR_RENDERER). 22 renderKind branches
416
+ eliminated. Polymorphic dispatch via renderer interface:
417
+ buildXAccessor, createXScale, extractData, updateXScale, buildXAxis,
418
+ makeDrawFn, hitTest, lookupRow, formatTooltipHeader. `rendererCtx`
419
+ singleton (xScale, yScale, opts, categoriesRef) mutated in place --
420
+ preserves zero-alloc on hot path.
421
+ - v1.0.0 alpha.1: pie + donut chart family. New `createBasePolarChart`
422
+ kernel -- completely independent from `createBaseAxisChart`. Polar
423
+ state struct: parallel arrays for values (Float32), labels (string[]),
424
+ colors, visibility (Uint8), startAngles (Float64 -- Float32 widens
425
+ PI/2 enough that exact-boundary hits land in wrong slice), arcAngles
426
+ (Float64). `extractSliceData` normalizes array-of-objects, parallel-
427
+ arrays, or plain number arrays. `computeSliceGeometry` centers in
428
+ plot rect with configurable inner radius. `sliceHitTest` is O(n)
429
+ atan2 + linear scan inside ring (n typically 3-12). `makeSliceDrawFn`
430
+ renders wedge (pie) or arc-ring (donut) per slice; hovered slice
431
+ expands 4 px. Pie and donut share SLICE_RENDERER; only innerRadius
432
+ default differs (0 vs 0.5).
433
+ - v1.0.0 alpha.2: slice colour resolution bug fix (raw CSS-var leaked
429
434
  into canvas fillStyle); demo gets ResizeObserver-backed responsive
430
- sizing via a `responsiveWidth(containerId, fallback)` helper.
431
- - **v1.2.0-alpha.3: createBubbleChart on the axis kernel.**
432
- New BUBBLE_RENDERER. Each point becomes a circle with AREA
433
- proportional to a third dimension via sqrt scale (default; linear
434
- available). Pixel radii computed at extract time:
435
- r = sqrt(rMin^2 + t*(rMax^2 - rMin^2)). New seriesState fields rs
436
- (raw sizes) and prs (pixel radii) -- both null on non-bubble series,
437
- zero extra memory. extractBubbleData wraps extractSeriesData, adds
438
- size extraction + computeBubbleRadii in one pass. Hit-test signature
439
- on the axis kernel extended from (canvasX, primary, xScale, ctx) to
440
- (canvasX, canvasY, primary, xScale, ctx) -- line/area/bar tests ignore
441
- canvasY; bubble uses both for circle-containment with smallest-on-top
442
- tie-breaking on overlap. Bubble bundle 22 KB minified; tree-shake
443
- verified to drop all polar + bar + interp helpers. 10 new tests, 158
444
- total. Single-series only -- multi-series + per-point colour encoding
445
- land in v1.3.0.
446
- - **v1.2.0-alpha.4 (current): createRadarChart on a third independent
447
- kernel.** Multi-axis polygon layout: N axes (min 3, typical 5-8)
448
- spoked from a shared center; each series is a polygon connecting one
449
- value-per-axis vertex. computeRadarGeometry precomputes cos/sin per
450
- axis into Float64 tables -- polygons, grid rings, and spokes all
451
- share them, zero per-frame trig. radarHitTest is nearest-vertex
452
- within 12 px across visible series (O(series * axes), trivial).
453
- Spoke labels auto-align based on angular position (cosA > 0.2 ->
454
- left-align, < -0.2 -> right-align, near-vertical -> center). Three
455
- drawing layers (grid -> polygons -> spokes+labels) as separate scene
456
- nodes for natural z-ordering. Auto-domain anchors at 0 when min/max
457
- ratio < 0.5 (scored-radar convention); explicit domain: [vMin, vMax]
458
- overrides. 18 new tests, 176 total. Radar bundle 13 KB minified --
459
- drops every axis-chart helper AND every polar-slice helper. The
460
- three kernels (axis, polar-slice, radar) are now strictly independent.
461
- - v1.2.0: lock seven-chart API (line, area, bar, bubble, pie, donut, radar)
462
- - v1.3.0: createBaseGridChart + createHeatmap (2D categorical,
463
- color-mapped); multi-series bubble + per-point colour encoding;
464
- @zakkster/lite-delaunay for O(log n) nearest-point hit-test on dense
465
- scatter/bubble clouds (> ~1000 points; sweepline Delaunay -> Voronoi
466
- dual extraction). Scatter chart rides on the same spatial index.
467
- - v1.4.0: stacked bar, per-bar hover, rounded corners, SVG export
468
- - v1.5.0: log scale, pan + zoom, legend virtualization
435
+ sizing.
436
+ - v1.0.0 alpha.3: createBubbleChart on the axis kernel. New
437
+ BUBBLE_RENDERER. Each point becomes a circle with AREA proportional
438
+ to a third dimension via sqrt scale (default; linear available).
439
+ Pixel radii computed at extract time:
440
+ r = sqrt(rMin^2 + t*(rMax^2 - rMin^2)). New seriesState fields `rs`
441
+ (raw sizes) and `prs` (pixel radii) -- both null on non-bubble
442
+ series, zero extra memory. Hit-test signature on the axis kernel
443
+ extended from (canvasX, primary, xScale, ctx) to (canvasX, canvasY,
444
+ primary, xScale, ctx) -- line/area/bar tests ignore canvasY; bubble
445
+ uses both for circle-containment with smallest-on-top tie-breaking
446
+ on overlap.
447
+ - v1.0.0 alpha.4: createRadarChart on a third independent kernel.
448
+ Multi-axis polygon layout: N axes (min 3, typical 5-8) spoked from a
449
+ shared center; each series is a polygon connecting one value-per-axis
450
+ vertex. `computeRadarGeometry` precomputes cos/sin per axis into
451
+ Float64 tables -- polygons, grid rings, and spokes all share them,
452
+ zero per-frame trig. `radarHitTest` is nearest-vertex within 12 px
453
+ across visible series. Spoke labels auto-align based on angular
454
+ position. Three drawing layers (grid -> polygons -> spokes+labels) as
455
+ separate scene nodes for natural z-ordering.
456
+ - v1.0.0: kernel-side auto-resize. `_wireAutoSize` shared helper; omit
457
+ width / height from config and the chart observes its container via
458
+ ResizeObserver. Three modes per dimension: explicit static, explicit
459
+ reactive (signal/fn), or implicit (auto-observe). Synchronous
460
+ initial read avoids size pop; rAF-throttled updates coalesce burst
461
+ events. Falls back gracefully (keeps default size) when
462
+ ResizeObserver is absent. 182 tests, full tree-shake verification.
463
+ - **v1.1.0: bar layout polish.** Stacked bars via new
464
+ `postExtract(states, ctx)` renderer hook -- the kernel calls it
465
+ after the per-series extract loop, bar's hook fills per-series
466
+ `state.stackBottoms` / `state.stackTops` (Float32Arrays, lazy-
467
+ allocated via ensureFloat32) with a visibility-aware cumulative
468
+ accumulator in declaration order; the kernel then re-aggregates
469
+ y-domain to pick up the total stack height. Hidden series excluded
470
+ from the stack; surviving segments snap down on legend toggle.
471
+ Negative values clamp to 0 (diverging stacks deferred to a future
472
+ cut). Rounded corners on the end opposite the baseline (top for
473
+ positive bars, bottom for negative) via native `ctx.roundRect`
474
+ where available + hand-traced `arcTo` fallback for older Safari;
475
+ radii capped at min(w,h)/2. Per-bar hover tint as a CSS-color
476
+ overlay keyed on the chart's `crosshairDataRef.snapIdx`; default
477
+ low-alpha white works against any series color without color
478
+ parsing, opt out with `hoverTint: false`. All three opt-in;
479
+ defaults match v1.0.0 exactly. Bar bundle 25 KB (+1.6 KB);
480
+ line/area/bubble bundles +~300 bytes for the kernel's null-check
481
+ on postExtract; pie/donut/radar unchanged. Three kernels still
482
+ strictly independent -- verified. 196 tests.
483
+ - **v1.2.0-alpha.0: spatial-index foundation.** Pluggable
484
+ nearest-neighbor index for O(log n) hit-test on dense point clouds
485
+ (bubble first; scatter + heatmap will ride the same interface in
486
+ alpha.1+). Public contract on `Charts.d.ts`:
487
+ `SpatialIndexFactory = (pxs, pys, n) -> SpatialIndex` where the
488
+ index exposes `findNearest(qx, qy, k, maxDistSq, outIndices,
489
+ outDistSq) -> count` (caller-owned output buffers; zero alloc per
490
+ query) and `dispose()`. `@zakkster/lite-delaunay` is the intended
491
+ default (sweepline Delaunay -> half-edge mesh -> Voronoi-dual walk
492
+ for the cursor's cell) but optional -- k-d tree, uniform-grid, or
493
+ any custom impl works equally well. lite-charts adds it as an
494
+ optional peer dep via `peerDependenciesMeta.optional`. New
495
+ `BUBBLE_RENDERER` config: `spatialIndex: factory` and
496
+ `spatialIndexThreshold` (default 1000). Below threshold the v1.0.0
497
+ linear scan stays the path. Index is lazy-built on the first hit-
498
+ test after extract, cached across queries, disposed on data /
499
+ scale change (extract calls `_disposeSpatialIndex`) AND on chart
500
+ unmount (via new `renderer.cleanup(states)` hook on the axis
501
+ kernel, symmetric with v1.1.0's `postExtract`). `k > 1` matters
502
+ for bubble: overlapping discs mean the nearest CENTER may not be
503
+ the one whose disc contains the cursor; the renderer asks for
504
+ `K = 8` candidates and post-filters by disc containment +
505
+ smallest-r tie-break, preserving v1.0.0 "visually-topmost wins on
506
+ overlap" semantics. Cached `state.prMaxSq` (max pixel-radius
507
+ squared) gives the index a tight maxDistSq upper bound.
508
+ `state._hitIndices` (Int32Array(8)) and `state._hitDistSq`
509
+ (Float32Array(8)) are stable refs that the index writes into.
510
+ Bubble bundle ~22 KB -> ~23 KB. Line / area / bar / pie / donut /
511
+ radar unchanged. 8 new tests. 204 total.
512
+ - **v1.2.0-alpha.1: createScatterChart.** New `SCATTER_RENDERER` on
513
+ the axis kernel. Eighth chart type ships. Bubble's simpler sibling:
514
+ no size dimension, every marker the same pixel radius
515
+ (`markerSize`, default 4), hit-test snaps to nearest point within
516
+ `hitTolerance` disc (default `markerSize + 4`). Reuses the
517
+ spatial-index foundation from alpha.0 with `k = 1` (no overlap
518
+ concerns). `_extractScatterData` disposes any existing spatial
519
+ index per-extract so the index rebuilds lazily on data / scale
520
+ change. `_scatterCleanup` symmetric with bubble's: disposes
521
+ indices on unmount. Scatter bundle ~24 KB. Adds nothing to other
522
+ bundles (verified). 6 new tests. 210 total.
523
+ - **v1.2.0-alpha.2: multi-series bubble + per-point color +
524
+ global size domain.** `BUBBLE_RENDERER` gains a `postExtract` hook
525
+ (`_bubblePostExtract`) that computes a GLOBAL size domain across
526
+ visible series and re-runs `computeBubbleRadii` on every state.
527
+ Single-series charts short-circuit -- the v1.0.0 fast path stays
528
+ in effect when only one series is visible. Per-series state gets
529
+ `state._stateIdx = i` at construction so the bubble's `lookupRow`
530
+ can tell which series got hit and scope the tooltip to that
531
+ series. Crosshair state gains `snapSeriesIdx` (default -1 --
532
+ non-bubble charts never set it). `_bubbleHitTest` iterates
533
+ `ctx.seriesStates` rather than checking only `primary`. Each
534
+ visible series' bubbles get tested (linear scan or spatial index,
535
+ per its threshold). Returns `snapSeriesIdx` along with `snapIdx`.
536
+ `_initBubbleOpts` gains `colorKey` (string / number / fn). The
537
+ accessor uses `buildRawAccessor` (not `buildAccessor`) so color
538
+ strings like `'#ff0000'` are not `+v`-coerced to NaN.
539
+ `extractBubbleData` extracts per-point colors into `state.cs`
540
+ (plain string Array, lazy-allocated). Colors resolve once via
541
+ `resolveColor` at extract time -- CSS-var values like
542
+ `'--c-emerald'` become concrete strings the draw fn writes to
543
+ `ctx.fillStyle` directly. Bubble draw fn picks `state.cs[i]` per
544
+ row when set, falling back to the series fill otherwise. Zero
545
+ hot-path cost when `colorKey` is unset. Bubble bundle ~23 KB ->
546
+ ~24 KB. Test infra bumped lite-signal default registry to
547
+ `maxNodes: 32768` for 200+ chart-creations across the suite. 9
548
+ new tests. 219 total.
549
+ - **v1.2.0-alpha.3 (current): createHeatmap + fourth kernel.**
550
+ New `createBaseGridChart(config, renderer)` kernel. Strictly
551
+ independent from axis / polar / radar kernels -- no cross-kernel
552
+ imports either direction. Heatmap bundle is 10.5 KB minified --
553
+ the smallest of the nine -- because the grid kernel has none of
554
+ the axes / interpolation / decimation / markers / scale-math /
555
+ multi-series weight of the axis kernel. Two band scales (x + y)
556
+ reuse `makeBandScale` math; the y band scale uses the same
557
+ +y-down convention so `yBand.leftEdge(0)` is the topmost cell.
558
+ Cell storage: flat `Float32Array` indexed `yIdx * nx + xIdx`;
559
+ `Uint8Array` `presentMask` for sparse data (missing cells render
560
+ empty, hit-test returns null for them). Per-cell colors
561
+ precomputed at extract into `state.cellColors: string[]` so the
562
+ draw loop is `fillStyle = cellColors[i]; fillRect(...)` --
563
+ zero alloc per cell. Default ramp is linear RGB interpolation
564
+ between two endpoint hex colors via `_parseHexColor` +
565
+ `_lerpRGBString`. The `colorFn(v, vMin, vMax) -> css` config
566
+ overrides entirely for OKLCH ramps, quantile binning, diverging
567
+ schemes. Hit-test is O(1) -- one `xBand.invert` + one
568
+ `yBand.invert` + a presence-mask check. `chart.hover` is a
569
+ signal-style reactive accessor with `.peek()` escape hatch.
570
+ Mouse events on canvas drive `moveHover` / `hideHover`
571
+ automatically; tests drive them manually too. Axis labels
572
+ render inline (no lite-axis dep for the grid kernel since
573
+ categories are arbitrary strings). 12 new tests covering
574
+ category extraction, vMin/vMax, cell render counts (full +
575
+ sparse), default ramp endpoint correctness, custom colorFn,
576
+ hit-test (correct cell + missing + outside plot), data signal
577
+ reactivity, value labels, lifecycle. 231 total.
578
+
579
+ ## Capacity (lite-signal arena)
580
+
581
+ lite-signal's default node-arena capacity is **1024 nodes**. Each
582
+ axis-kernel chart uses ~45-60 active nodes (the dominant cost is
583
+ the per-axis tick pool: each tick = `lineNode` + `textNode` = 2
584
+ scene-node effects, at max 12 ticks per axis = ~24 effects per axis
585
+ x 2 axes = ~48). Heatmap uses ~5 nodes (grid kernel renders cells
586
+ through a single pathNode per layer). Pie / donut use ~25.
587
+
588
+ **Default 1024 fits ~15-20 axis-kernel charts.** Dashboards or demos
589
+ with more must bump the registry BEFORE constructing any chart:
590
+
591
+ ```js
592
+ import { createRegistry, setDefaultRegistry } from '@zakkster/lite-signal';
593
+ setDefaultRegistry(createRegistry({ maxNodes: 32768 }));
594
+ ```
595
+
596
+ The lite-charts demo (`demo/index.html`) and the test suite both
597
+ bump to 32768 for this reason. Bumping after charts are already
598
+ constructed doesn't help those charts -- they read the current
599
+ default registry at construction time.
600
+
601
+ Each mount/unmount cycle leaves ~4 nodes of residue from
602
+ construction-time signals (`widthAutoSig`, `plotBoundsSignal`,
603
+ `scaleVersion`, etc.) that survive unmount so the chart can be
604
+ remounted. For long-running apps that create and destroy many charts
605
+ dynamically, this accumulates until the chart reference is dropped.
606
+ A terminal-teardown `chart.destroy()` is on the roadmap for v1.3.
607
+
608
+ ## Forward plan
609
+
610
+ - v1.2.0: heatmap polish -- per-row / per-column highlight on hover,
611
+ quantile binning, value-label auto-color (white-on-dark, dark-on-
612
+ light). Final v1.2.0 cut after the polish lands.
613
+ - v1.3.0: SVG export across all nine charts. Mirrors every draw fn
614
+ through SVG path commands; reuses every projection function so
615
+ output is pixel-identical to canvas.
616
+ - v1.4.0: log scale; pan + zoom; brushing primitives.
617
+ - v1.5.0: time-series specialized variants; legend virtualization via
618
+ `@zakkster/lite-virtual`; annotation layer.
469
619
 
470
620
  ## License
471
621
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zakkster/lite-charts",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Reactive, zero-GC chart library. Signal-native data, scales, and dimensions; 60fps at 100k points; zero allocations in steady-state render. Built on @zakkster/lite-scene.",
5
5
  "type": "module",
6
6
  "main": "./Charts.js",
@@ -18,6 +18,7 @@
18
18
  "Charts.js",
19
19
  "Charts.d.ts",
20
20
  "README.md",
21
+ "CHANGELOG.md",
21
22
  "llms.txt",
22
23
  "LICENSE"
23
24
  ],
@@ -28,9 +29,10 @@
28
29
  "@zakkster/lite-signal": "^1.1.5"
29
30
  },
30
31
  "scripts": {
31
- "test": "node --test --test-reporter=spec",
32
- "test:gc": "node --expose-gc --test --test-reporter=spec",
33
- "bench": "node --expose-gc bench/line-100k.mjs"
32
+ "test": "node --test --test-reporter=spec test/*.test.js",
33
+ "test:gc": "node --expose-gc --test --test-reporter=spec test/*.test.js",
34
+ "bench": "node --expose-gc bench/line-100k.mjs",
35
+ "demo": "node demo/serve.js"
34
36
  },
35
37
  "keywords": [
36
38
  "chart",