@zakkster/lite-charts 1.0.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/llms.txt ADDED
@@ -0,0 +1,472 @@
1
+ # @zakkster/lite-charts
2
+
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.
28
+
29
+ ## Why use this library
30
+
31
+ - You need a chart that re-renders automatically when a signal changes
32
+ (data, width, theme, etc.) without manual `.update()` calls.
33
+ - You're streaming live data (100-100k points/sec) and Chart.js drops frames.
34
+ - You're in a Twitch Extension or game HUD with a 1 MB / 60 fps budget that
35
+ rules out Chart.js (~78 KB) or D3 (~70+ KB plus selection allocation).
36
+ - You want a Vega-Lite middle ground: sensible defaults + escape hatches.
37
+
38
+ ## When NOT to use
39
+
40
+ - You need SVG output -- coming v1.1. Today only `exportPNG` (canvas
41
+ `toDataURL`).
42
+ - 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.
50
+
51
+ ## Module shape
52
+
53
+ ESM-only. Named exports from the single `Charts.js` entry:
54
+
55
+ ```ts
56
+ // Axis kernel
57
+ function createLineChart(config: LineChartConfig): Chart;
58
+ function createAreaChart(config: AreaChartConfig): Chart;
59
+ function createBarChart(config: BarChartConfig): Chart;
60
+ 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, ... */ };
80
+ ```
81
+
82
+ `AreaChartConfig extends LineChartConfig` with three additional fields:
83
+
84
+ ```ts
85
+ interface AreaChartConfig extends LineChartConfig {
86
+ baseline?: number | 'bottom'; // default 0; 'bottom' = bottom of plot rect
87
+ stroke?: boolean; // default true; whether to stroke upper boundary
88
+ fillOpacity?: number; // default 0.3; multiplied into globalAlpha for fill
89
+ }
90
+ ```
91
+
92
+ `BarChartConfig` is line config minus interpolation/markers (don't apply
93
+ to bars), plus bar-specific layout:
94
+
95
+ ```ts
96
+ interface BarChartConfig extends Omit<LineChartConfig, 'interpolation' | 'markers' | 'xScale'> {
97
+ baseline?: number; // default 0; where bars anchor on the y-axis
98
+ paddingInner?: number; // default 0.15; gap between bands as fraction of step
99
+ paddingOuter?: number; // default 0.1; padding at each end of the range
100
+ groupInnerPad?: number; // default 0.08; gap between bars within a grouped slot
101
+ xScale?: { domain?: string[] }; // explicit categories if needed
102
+ }
103
+ ```
104
+
105
+ Bar charts treat x as a categorical key. Data shape: `[{x: 'Q1', y: 42}, ...]`.
106
+ String, number, or any value that survives `String(x)` round-trip works.
107
+ Multi-series renders **grouped** side-by-side; each bar occupies a slice of
108
+ the band centered on its series index. The y-domain always includes the
109
+ `baseline` (default 0) so bars don't visually float.
110
+
111
+ Hit detection is discrete and O(1): `Math.floor((px - origin) / step)`.
112
+ No bisection -- the discriminator is categorical. The crosshair snaps to
113
+ the nearest band; per-series markers are skipped (bars highlight
114
+ themselves).
115
+
116
+ ## Core types
117
+
118
+ ```ts
119
+ interface LineChartConfig {
120
+ // Data shape: either `data` (single series shorthand) or `series`.
121
+ data?: Row[] | (() => Row[]) | { xs: Float32Array; ys: Float32Array };
122
+ series?: SeriesConfig[];
123
+
124
+ // Accessors. String key, integer index, or function. Default 'x' / 'y'.
125
+ x?: string | number | ((row: any, i: number) => number);
126
+ y?: string | number | ((row: any, i: number) => number);
127
+
128
+ // Static-or-signal dimensions.
129
+ width?: number | (() => number);
130
+ height?: number | (() => number);
131
+
132
+ margin?: { top?: number; right?: number; bottom?: number; left?: number };
133
+
134
+ // Style. CSS color strings (hex/rgb/oklch/named) or "--css-var" tokens
135
+ // resolved against the container via getComputedStyle at mount time.
136
+ color?: string;
137
+ lineWidth?: number;
138
+ background?: string | null;
139
+ axisColor?: string;
140
+ labelColor?: string;
141
+ font?: string;
142
+
143
+ // Device pixel ratio override. Default: globalThis.devicePixelRatio.
144
+ dpr?: number;
145
+
146
+ // Scale overrides. Without these, x-type is inferred from the first
147
+ // data row (Date probe -> time; numeric -> linear) and y-domain is
148
+ // computed from union of all series with 5% nice padding.
149
+ xScale?: { type?: 'linear' | 'time'; domain?: [number, number] };
150
+ yScale?: { domain?: [number, number]; zero?: boolean; nice?: boolean };
151
+
152
+ // Crosshair: vertical line + per-series marker dots at the snapped x.
153
+ // Default true. `crosshair: false` disables the line + markers but keeps
154
+ // the tooltip if it's also enabled.
155
+ crosshair?: boolean | { color?: string; dash?: number[] };
156
+
157
+ // Tooltip: canvas-drawn box at the snapped x. Default true. `format`
158
+ // receives `{snapIdx, snapDomainX, xScaleType, rows}` and may return a
159
+ // string (header-only, suppresses rows) or `{header?, rows?}`.
160
+ tooltip?: boolean | {
161
+ background?: string;
162
+ border?: string;
163
+ format?: (ctx: {
164
+ snapIdx: number;
165
+ snapDomainX: number;
166
+ xScaleType: 'linear' | 'time';
167
+ rows: Array<{ color: string; label: string; value: string }>;
168
+ }) => string | { header?: string; rows?: any[] };
169
+ };
170
+
171
+ // Legend: DOM-rendered with click-to-toggle visibility. Default 'bottom'.
172
+ // Auto-wraps the canvas in a flex container; pass `legend.container` to
173
+ // append into an existing element instead.
174
+ legend?: boolean | 'top' | 'bottom' | 'left' | 'right' | {
175
+ position?: 'top' | 'bottom' | 'left' | 'right';
176
+ container?: HTMLElement;
177
+ };
178
+
179
+ // Path interpolation (v1.0.0). Default 'linear'. Per-series override
180
+ // via SeriesConfig.interpolation. Decimated branch ignores this.
181
+ interpolation?: 'linear' | 'step' | 'step-after' | 'step-before'
182
+ | 'step-mid' | 'monotone' | 'catmull-rom';
183
+
184
+ // Marker dots at each sample point (v1.0.0). `true` for circle defaults.
185
+ // `everyN` thins markers for dense series. Suppressed in decimated mode.
186
+ markers?: boolean | {
187
+ shape?: 'circle' | 'square' | 'triangle' | 'diamond';
188
+ size?: number; // default 5
189
+ fill?: string; // defaults to series color
190
+ stroke?: string; // default '#ffffff'
191
+ strokeWidth?: number; // default 1
192
+ everyN?: number; // default 1
193
+ };
194
+
195
+ // Frame scheduler. Default rAF in browser, synchronous in Node (for
196
+ // headless tests). Pass `queueMicrotask` for coalesced headless benches.
197
+ schedule?: (fn: () => void) => void;
198
+ }
199
+
200
+ interface SeriesConfig {
201
+ name?: string;
202
+ data: Row[] | (() => Row[]) | { xs: Float32Array; ys: Float32Array };
203
+ color?: string;
204
+ lineWidth?: number;
205
+ interpolation?: InterpolationMode; // per-series override
206
+ markers?: boolean | MarkerConfig; // per-series override
207
+ }
208
+
209
+ interface Chart {
210
+ mount(target: HTMLElement | HTMLCanvasElement): Chart;
211
+ unmount(): void;
212
+ exportPNG(opts?: { mimeType?: string; quality?: number }): string;
213
+ redraw(): void;
214
+
215
+ // v1.0.0-alpha.1: crosshair / tooltip control.
216
+ moveCrosshair(canvasX: number, canvasY: number): void;
217
+ hideCrosshair(): void;
218
+
219
+ // v1.0.0-alpha.3: series visibility control.
220
+ setSeriesVisible(idx: number, visible: boolean): void;
221
+
222
+ // v1.0.0-beta.0: theme reactivity.
223
+ refreshTheme(): void; // re-resolve CSS-var colors + redraw
224
+
225
+ readonly scene: Scene | null; // lite-scene instance
226
+ readonly canvas: HTMLCanvasElement | null;
227
+ readonly xScale: Scale;
228
+ readonly yScale: Scale;
229
+ readonly xScaleType: 'linear' | 'time';
230
+ plotBounds: Signal<number>; // version counter; read to subscribe
231
+ crosshair: Signal<{ // live crosshair state; subscribe for sync small-multiples
232
+ visible: boolean;
233
+ snapIdx: number; // -1 if hidden
234
+ snapDomainX: number;
235
+ snapPixelX: number;
236
+ mousePixelY: number;
237
+ }>;
238
+ seriesVisibility: Signal<boolean>[]; // one per series; write to toggle
239
+ legend: HTMLElement | null; // null if legend disabled or bare-canvas mount
240
+ }
241
+
242
+ interface Scale {
243
+ type: 'linear' | 'time';
244
+ dMin: number; dMax: number;
245
+ rMin: number; rMax: number;
246
+ map(value: number): number; // domain -> pixel
247
+ invert(pixel: number): number; // pixel -> domain
248
+ }
249
+ ```
250
+
251
+ ## Static-or-signal acceptance
252
+
253
+ Every config value that could plausibly be reactive accepts both forms.
254
+ Internally lite-charts wraps statics in constant accessors via a `asAccessor`
255
+ helper, so the engine only ever calls functions. Zero overhead for static
256
+ config; full reactivity for signal config:
257
+
258
+ ```js
259
+ const w = signal(800);
260
+ const chart = createLineChart({ data, width: w, height: 400 }); // mixed
261
+ w.set(1200); // triggers resize + rescale + redraw automatically
262
+ ```
263
+
264
+ ## Data shape conventions
265
+
266
+ Two accepted forms:
267
+
268
+ 1. **AoS (most ergonomic)**: array of row objects.
269
+ ```js
270
+ data: [{ x: 0, y: 1 }, { x: 1, y: 4 }, ...]
271
+ ```
272
+ Internally extracted to SoA `Float32Array` slabs once per data update.
273
+
274
+ 2. **SoA (zero-copy fast path)**: `{ xs: Float32Array, ys: Float32Array }`.
275
+ ```js
276
+ const xs = new Float32Array(N), ys = new Float32Array(N);
277
+ // ... fill ...
278
+ data: { xs, ys }
279
+ ```
280
+ The chart references the buffers directly. Use this when you're already
281
+ producing typed-array data (audio, telemetry, sensor streams, WebGL
282
+ pipelines, etc.).
283
+
284
+ Multiple series can mix forms.
285
+
286
+ ## Architecture invariants
287
+
288
+ - **One scene per chart.** `mount` creates a `lite-scene` over the canvas.
289
+ All axes, lines, ticks, labels are scene nodes.
290
+ - **Series state is pre-allocated and grown on demand.** Each series has a
291
+ pair of `Float32Array` slabs (`xs`, `ys`, `pxs`, `pys`) plus decimation
292
+ working buffers. Slabs grow by power-of-two doubling; never shrink.
293
+ - **The decimation kernel was lifted from `@zakkster/lite-canvas-graph`.**
294
+ Per-column min/max envelope for `n > 2 * plotWidth`. Preserves spike
295
+ visibility. Allocation-free; writes into caller-owned buffers.
296
+ - **Two render paths, selected per-draw**: direct polyline for sparse data
297
+ (`n <= 2 * plotWidth`), decimated envelope for dense data. NaN samples
298
+ break the polyline in direct mode and are skipped in decimated mode.
299
+ - **Three effects per chart, set up at `mount` time**:
300
+ 1. Size/plot-bounds effect (tracks `widthAcc`, `heightAcc`)
301
+ 2. Data/scale effect (tracks each series' `dataAccessor` + `plotBoundsSignal`)
302
+ 3. Dirty-bridge effect (tracks `scaleVersion` + `plotBoundsSignal`, calls
303
+ `scene.markDirty`)
304
+ - **Plus two axis effects** (X and Y), tracking `scaleVersion` and rebuilding
305
+ tick positions from the live scale via `lite-axis.linearTicks` /
306
+ `timeTicks` and `thinLabels`.
307
+ - **The line series itself is a `path` node** with a raw draw function. The
308
+ draw closure reads `state.pxs/pys` and color/lineWidth refs directly;
309
+ zero allocation per frame.
310
+
311
+ ## Performance characteristics
312
+
313
+ Measured at N=100,000 points, canvas 1600x800, Node 22 (CPU only -- mock
314
+ canvas):
315
+
316
+ - Full update cycle (data.set -> draw): **p50 1.39 ms, p95 4.66 ms**
317
+ - Decimation kernel alone: **p50 0.52 ms, p95 0.56 ms**
318
+ - Draw alone (cached): **p50 0.48 ms, p95 0.58 ms**
319
+ - Steady-state heap delta: **~270 bytes/cycle** (target <100; gap is
320
+ niceYDomain tuple + axis label strings + queueMicrotask Promise; v1.0.1
321
+ closes)
322
+
323
+ Both 60fps (16.67 ms) and 120fps (8.33 ms) budgets fit on the CPU side.
324
+ Real GPU paint is additional; browser bench comes in v1.0.1.
325
+
326
+ ## Testing
327
+
328
+ ```
329
+ npm test # 43 deterministic tests via node:test, --expose-gc
330
+ npm run bench # 100k-point latency + allocation report
331
+ ```
332
+
333
+ Tests use a recording mock canvas context (`test/harness.js`) that captures
334
+ every method call and property assignment into a flat `calls` array. Tests
335
+ assert on the call sequence (what was drawn, with what state) so no real
336
+ canvas is needed. Bench mode supports `ctx.recordingEnabled = false` to
337
+ avoid heap blowup over many frames.
338
+
339
+ ## Common patterns
340
+
341
+ ### Live streaming with a ring buffer
342
+
343
+ ```js
344
+ const xs = new Float32Array(1024), ys = new Float32Array(1024);
345
+ const data = signal({ xs, ys });
346
+ const chart = createLineChart({ data, width: 800, height: 200 });
347
+ chart.mount(el);
348
+
349
+ // In the data source -- writes mutate xs/ys in place; signal.set on the
350
+ // same object reference re-extracts and re-projects.
351
+ const newDataTick = (newY) => {
352
+ // ... shift the ring, write newY at the head ...
353
+ data.set({ xs, ys }); // forces effect re-fire
354
+ };
355
+ ```
356
+
357
+ ### Multi-series with mixed forms
358
+
359
+ ```js
360
+ createLineChart({
361
+ series: [
362
+ { name: 'cpu', data: cpuRows, x: 't', y: 'pct', color: 'red' },
363
+ { name: 'mem', data: { xs: memXs, ys: memYs }, color: 'blue' },
364
+ ],
365
+ });
366
+ ```
367
+
368
+ ### Locked x-domain (for synchronized small multiples)
369
+
370
+ ```js
371
+ const sharedX = signal([Date.now() - 3600_000, Date.now()]);
372
+ createLineChart({
373
+ data: series1,
374
+ xScale: { type: 'time', domain: untrack(sharedX) },
375
+ // Pass a computed for live sync:
376
+ // xScale: { type: 'time', domain: sharedX() },
377
+ });
378
+ ```
379
+
380
+ ## Gotchas
381
+
382
+ - **`data: { xs, ys }` is reference-tracked.** The signal compares by
383
+ identity. Mutating the buffers in place and calling `signal.set(sameRef)`
384
+ works -- lite-signal's default `equals` is `Object.is`, which is false
385
+ for `set(x)` vs prior `x` only if you... wait, `Object.is(x, x)` is
386
+ true. So passing the same reference will NOT trigger. Either swap to a
387
+ new wrapper object `signal.set({xs, ys})` (one alloc) or use
388
+ `signal.update(() => ({xs, ys}))`. Future v1.1: add an opt-in
389
+ "rebroadcast" flag to skip the equality check.
390
+ - **`exportPNG` requires a real HTMLCanvasElement.** Throws on mock canvases.
391
+ - **Mount target may be either an element (canvas created inside) or a
392
+ canvas itself.** Test mocks duck-type via `getContext`.
393
+ - **In Node, the default schedule is synchronous.** This is correct for
394
+ tests but causes lite-scene to skip draw coalescing -- if you're driving
395
+ many `node.set()` calls per signal write, use
396
+ `schedule: (fn) => queueMicrotask(fn)`. The bench documents this clearly.
397
+
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
429
+ 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
469
+
470
+ ## License
471
+
472
+ MIT (c) Zahary Shinikchiev
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@zakkster/lite-charts",
3
+ "version": "1.0.0",
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
+ "type": "module",
6
+ "main": "./Charts.js",
7
+ "module": "./Charts.js",
8
+ "types": "./Charts.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "node": "./Charts.js",
12
+ "types": "./Charts.d.ts",
13
+ "import": "./Charts.js",
14
+ "default": "./Charts.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "Charts.js",
19
+ "Charts.d.ts",
20
+ "README.md",
21
+ "llms.txt",
22
+ "LICENSE"
23
+ ],
24
+ "sideEffects": false,
25
+ "peerDependencies": {
26
+ "@zakkster/lite-axis": "^1.0.1",
27
+ "@zakkster/lite-scene": "^1.0.0",
28
+ "@zakkster/lite-signal": "^1.1.5"
29
+ },
30
+ "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"
34
+ },
35
+ "keywords": [
36
+ "chart",
37
+ "charts",
38
+ "visualization",
39
+ "canvas",
40
+ "reactive",
41
+ "signal",
42
+ "zero-gc",
43
+ "performance",
44
+ "line-chart",
45
+ "telemetry",
46
+ "dashboard"
47
+ ],
48
+ "author": "Zahary Shinikchiev <shinikchiev@yahoo.com>",
49
+ "license": "MIT",
50
+ "homepage": "https://github.com/PeshoVurtoleta/lite-charts#readme",
51
+ "repository": {
52
+ "type": "git",
53
+ "url": "git+https://github.com/PeshoVurtoleta/lite-charts.git"
54
+ },
55
+ "bugs": {
56
+ "url": "https://github.com/PeshoVurtoleta/lite-charts/issues",
57
+ "email": "shinikchiev@yahoo.com"
58
+ },
59
+ "engines": {
60
+ "node": ">=18"
61
+ },
62
+ "funding": {
63
+ "type": "github",
64
+ "url": "https://github.com/sponsors/PeshoVurtoleta"
65
+ }
66
+ }