@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/Charts.d.ts +638 -0
- package/Charts.js +5123 -0
- package/LICENSE +21 -0
- package/README.md +711 -0
- package/llms.txt +472 -0
- package/package.json +66 -0
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
|
+
}
|