goro-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.
@@ -0,0 +1,248 @@
1
+ /**
2
+ * @file Abstract chart orchestrator shared by every chart type.
3
+ *
4
+ * Owns cross-cutting concerns: the data stores (one per series), the canvas
5
+ * surface, the dirty flag and rAF coalescing, pointer interaction state, the
6
+ * ResizeObserver, and the draw sequence (static layer → offscreen, blit,
7
+ * crosshair overlay). All data lives in {@link SeriesStore} instances; all
8
+ * pixels are produced by the `render/` functions.
9
+ *
10
+ * Multi-series: a {@link SeriesStore} is created for each
11
+ * {@link SeriesConfig} entry. The series extent is computed as a union over
12
+ * all stores so grid ticks span every visible series. Each store is rendered
13
+ * independently by the subclass's {@link renderSeries}, receiving per-series
14
+ * colour/style overrides merged from its config entry.
15
+ *
16
+ * Dual-Y: when any series opts into `yAxis: 'right'` the chart maintains
17
+ * left and right grid domains independently. Left-axis series share the left
18
+ * domain; right-axis series share the right. Tick labels appear on both sides
19
+ * and the crosshair reads the correct axis per series.
20
+ */
21
+ import { SeriesStore } from '../data/series-store.js';
22
+ import { Surface } from '../render/surface.js';
23
+ import type { ChartOpts, ResolvedOpts, SeriesConfig, SeriesView, PlotRect } from '../types.js';
24
+ import type { SeriesHit } from '../render/crosshair.js';
25
+ /** Abstract base for all chart types. */
26
+ export declare abstract class ChartBase {
27
+ protected opts: ResolvedOpts;
28
+ protected surface: Surface;
29
+ protected stores: SeriesStore[];
30
+ protected seriesConfigs: SeriesConfig[];
31
+ private gridDomainLeft;
32
+ private gridDomainRight;
33
+ private gridPinned;
34
+ private hasRightAxis;
35
+ /**
36
+ * Cached stack-group detection. `seriesConfigs` is immutable after the
37
+ * constructor (validateOpts is the last writer), so groups are computed
38
+ * once and reused every draw instead of rebuilt each frame.
39
+ */
40
+ private stackGroupsAll;
41
+ private stackGroupsByAxis;
42
+ private dirty;
43
+ private cursorX;
44
+ private cursorY;
45
+ private showCrosshair;
46
+ private autoDraw;
47
+ private rafScheduled;
48
+ private suspendCount;
49
+ private syncTargets;
50
+ private resizeObserver;
51
+ protected destroyed: boolean;
52
+ private liveRegion;
53
+ private readonly handleResize;
54
+ private readonly handleMouseMove;
55
+ private readonly handleMouseLeave;
56
+ private readonly handleKeyDown;
57
+ constructor(canvas: HTMLCanvasElement, opts?: ChartOpts);
58
+ /**
59
+ * Validate and normalize constructor options.
60
+ * Warns on suspicious values and clamps/repairs where possible.
61
+ */
62
+ private validateOpts;
63
+ /**
64
+ * Detect system accessibility preferences and adjust visual styles.
65
+ * - prefers-reduced-motion → disable rAF coalescing (synchronous draws)
66
+ * - prefers-contrast: more → increase colour opacity
67
+ * - forced-colors: active → use system CSS system colours
68
+ */
69
+ private applySystemTheme;
70
+ /**
71
+ * Ensure an aria-live region exists for screen-reader announcements
72
+ * of crosshair position values.
73
+ */
74
+ private ensureLiveRegion;
75
+ /**
76
+ * Update the canvas aria-label with a summary of visible data.
77
+ * Called once per static redraw.
78
+ */
79
+ private updateAriaLabel;
80
+ /**
81
+ * Handle keyboard navigation for the crosshair.
82
+ * - ArrowLeft / ArrowRight: move crosshair by 1 point (Shift: 10 points)
83
+ * - Escape: hide crosshair
84
+ */
85
+ private onKeyDown;
86
+ /** Reusable crosshair-sync helpers used by both mouse and keyboard handlers. */
87
+ private notifySyncCrosshair;
88
+ private notifySyncCrosshairLeave;
89
+ /** Draw the series shape. Implemented by {@link LineChart} / {@link AreaChart}. */
90
+ protected abstract renderSeries(ctx: CanvasRenderingContext2D, view: SeriesView, plot: PlotRect, opts: ResolvedOpts): void;
91
+ /**
92
+ * Snapshot mode: replace the series at `index` (O(n) extent).
93
+ * @param index - Series index (0-based)
94
+ * @param x - X values, must be monotonically increasing
95
+ * @param y - Y values, must have the same length as x
96
+ * @throws {Error} if `index` is out of range, or x/y length mismatch
97
+ */
98
+ setData(index: number, x: Float64Array<ArrayBufferLike>, y: Float64Array<ArrayBufferLike>): void;
99
+ /**
100
+ * Ring mode: append one sample to series `index`.
101
+ * @param index - Series index (0-based)
102
+ * @param x - X value (must be monotonically increasing)
103
+ * @param y - Y value
104
+ * @throws {Error} if ring mode is not active (maxPoints not set)
105
+ * @throws {Error} if `index` is out of range
106
+ */
107
+ append(index: number, x: number, y: number): void;
108
+ /**
109
+ * Ring mode: append a batch of samples to series `index`.
110
+ * @param index - Series index (0-based)
111
+ * @param xs - X values (must be monotonically increasing)
112
+ * @param ys - Y values, must have the same length as xs
113
+ * @throws {Error} if ring mode is not active
114
+ * @throws {Error} if `index` is out of range or xs/ys length mismatch
115
+ */
116
+ appendBatch(index: number, xs: ArrayLike<number>, ys: ArrayLike<number>): void;
117
+ /**
118
+ * Resize the streaming window (applies to all series).
119
+ * Preserves the most recent samples in each series.
120
+ * @param maxPoints - New window size (must be >= 1)
121
+ */
122
+ setMaxPoints(maxPoints: number): void;
123
+ /** Empty all series and reset grid domain. */
124
+ clear(): void;
125
+ /** Number of series configured. */
126
+ get seriesCount(): number;
127
+ /**
128
+ * Total points rendered across all series in the last draw.
129
+ * Useful for debug/performance monitoring (e.g. to verify decimation is active).
130
+ */
131
+ get renderedPointCount(): number;
132
+ /**
133
+ * Number of points currently in the window for series `index`.
134
+ * @param index - Series index (0-based)
135
+ */
136
+ pointCount(index: number): number;
137
+ /**
138
+ * Current window y minimum for series `index` (O(1)).
139
+ * @param index - Series index (0-based)
140
+ */
141
+ extentMin(index: number): number;
142
+ /**
143
+ * Current window y maximum for series `index` (O(1)).
144
+ * @param index - Series index (0-based)
145
+ */
146
+ extentMax(index: number): number;
147
+ /**
148
+ * Most recent y value for series `index`, or NaN if empty.
149
+ * @param index - Series index (0-based)
150
+ */
151
+ lastValue(index: number): number;
152
+ /**
153
+ * Pause rAF-coalesced drawing. Nestable — call {@link resumeDraw} the
154
+ * same number of times to re-enable. Useful for bulk-loading data without
155
+ * intermediate paints.
156
+ */
157
+ suspendDraw(): void;
158
+ /** Resume drawing after a matching {@link suspendDraw}. Draws immediately if dirty. */
159
+ resumeDraw(): void;
160
+ /**
161
+ * Export the current canvas as a PNG data URL.
162
+ * @returns A `data:image/png` URL string
163
+ */
164
+ toImage(): string;
165
+ /**
166
+ * Bidirectionally sync crosshair position with `other`. When the mouse
167
+ * moves on one chart, crosshair overlay is injected on all synced charts
168
+ * at the matching x coordinate.
169
+ * @param other - Another chart instance to sync with
170
+ */
171
+ sync(other: ChartBase): void;
172
+ /**
173
+ * External callback for hover events. Called on `mousemove` with the
174
+ * interpolated data for every visible series. Use to build custom DOM
175
+ * tooltips or bind to framework state — the Canvas tooltip still draws
176
+ * unless suppressed externally.
177
+ */
178
+ onHover?: (hits: SeriesHit[]) => void;
179
+ /** Paint the chart. Cheap to call repeatedly: returns early when clean. */
180
+ draw(): void;
181
+ /** Detach observers/listeners and release buffers. Idempotent. */
182
+ destroy(): void;
183
+ /** Render the static layer (background, grid, axes, series, legend) to offscreen. */
184
+ private renderStatic;
185
+ /** Render a single series. */
186
+ private renderOne;
187
+ /** Cached per-axis stack groups (computed once in the constructor). */
188
+ private detectStackGroupsOnAxis;
189
+ /**
190
+ * Compute stack groups for a given axis. Returns the group map and a set
191
+ * of series indices that belong to groups with 2+ members.
192
+ */
193
+ private computeStackGroupsOnAxis;
194
+ /** Cached all-axis stack groups (computed once in the constructor). */
195
+ private detectAllStackGroups;
196
+ /**
197
+ * Compute all stack groups across all axes.
198
+ * Unlike {@link computeStackGroupsOnAxis}, this ignores the yAxis setting
199
+ * and returns every group that has 2+ members.
200
+ */
201
+ private computeAllStackGroups;
202
+ /**
203
+ * Compute accumulated Y values across a stack group.
204
+ * Returns the cumulative Y array (length = n), or null if all stores are empty.
205
+ */
206
+ private accumulateStackGroup;
207
+ /**
208
+ * Update the grid Y domain.
209
+ *
210
+ * Two regimes:
211
+ * - **Snapshot mode:** anchor the grid so it only expands, never shrinks.
212
+ * The domain snaps to the union extent on the first draw and stays frozen
213
+ * until a series exceeds it, then expands with a 10 % margin — a stable
214
+ * visual anchor for static data.
215
+ * - **Ring (streaming) mode:** the window slides, so the true extent both
216
+ * grows and shrinks as samples enter and leave. Here we recompute the
217
+ * domain from the current window every tick (via {@link initDomain}) so
218
+ * the grid tracks the visible data instead of drifting or letting stacked
219
+ * bands overflow the frame.
220
+ *
221
+ * If the user supplied `yMin` / `yMax` those are hard bounds that override
222
+ * the auto logic in either regime.
223
+ */
224
+ private updateGridDomain;
225
+ /**
226
+ * Compute the domain for one axis from the current window.
227
+ * @param margin optional fraction of the Y range to pad on each side
228
+ * (used in streaming mode so peaks/troughs don't touch the frame).
229
+ */
230
+ private initDomain;
231
+ private expandDomain;
232
+ /**
233
+ * Recompute the shared X domain from all non-empty series so grid ticks
234
+ * and renderers always map fresh X positions. Called every draw.
235
+ */
236
+ private refreshXDomain;
237
+ /** Compute the plot rectangle from canvas size minus padding. */
238
+ private plotRect;
239
+ /** Mark dirty and schedule/perform a draw per the autoDraw policy. */
240
+ private invalidate;
241
+ private storeAt;
242
+ private injectCursor;
243
+ private injectCursorLeave;
244
+ private attachEvents;
245
+ private onResize;
246
+ private onMouseMove;
247
+ private onMouseLeave;
248
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @file Line chart — the default series drawn as a single batched polyline.
3
+ *
4
+ * Supports the decimation auto-switch: dense data collapses to a per-pixel
5
+ * min/max envelope ribbon; sparse data draws the real polyline. For the
6
+ * full algorithm details see {@link renderLine}.
7
+ */
8
+ import { ChartBase } from './chart-base.js';
9
+ import type { SeriesView, PlotRect, ResolvedOpts } from '../types.js';
10
+ /** High-performance Canvas 2D line chart. */
11
+ export declare class LineChart extends ChartBase {
12
+ protected renderSeries(ctx: CanvasRenderingContext2D, view: SeriesView, plot: PlotRect, opts: ResolvedOpts): void;
13
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @file Scatter chart — one filled circle per (sampled) point.
3
+ *
4
+ * Uses stride thinning via {@link renderScatter}: when the dataset exceeds
5
+ * `maxDots` every Nth point is drawn so the chart stays responsive. The
6
+ * thinning step is `floor(n / maxDots)`.
7
+ */
8
+ import { ChartBase } from './chart-base.js';
9
+ import type { SeriesView, PlotRect, ResolvedOpts } from '../types.js';
10
+ /** High-performance Canvas 2D scatter chart. */
11
+ export declare class ScatterChart extends ChartBase {
12
+ protected renderSeries(ctx: CanvasRenderingContext2D, view: SeriesView, plot: PlotRect, opts: ResolvedOpts): void;
13
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * @file Sliding-window min & max in O(1) amortized per push.
3
+ *
4
+ * Maintains two monotonic deques backed by circular Float64Arrays (no object
5
+ * allocation): the min-deque is strictly increasing front→back, the max-deque
6
+ * strictly decreasing, so each window extreme sits at its deque's front.
7
+ *
8
+ * Each push runs three steps in the canonical order — evict expired from
9
+ * front, pop dominated from back, append — which is what guarantees a deque
10
+ * never exceeds capacity. `seq` is a globally increasing sample id and
11
+ * `windowStart` is the oldest seq still inside the window; an entry expires
12
+ * once its seq < windowStart.
13
+ */
14
+ /** Incremental sliding-window extent tracker. */
15
+ export declare class MonotonicExtent {
16
+ private cap;
17
+ private vMin;
18
+ private sMin;
19
+ private hMin;
20
+ private nMin;
21
+ private vMax;
22
+ private sMax;
23
+ private hMax;
24
+ private nMax;
25
+ constructor(cap: number);
26
+ /** Reset to empty (keeps allocated buffers). */
27
+ clear(): void;
28
+ /**
29
+ * Record a new sample and slide the window forward.
30
+ * @param value the sample value
31
+ * @param seq globally increasing sample id
32
+ * @param windowStart oldest seq still inside the window
33
+ */
34
+ push(value: number, seq: number, windowStart: number): void;
35
+ /** Current window minimum. */
36
+ get min(): number;
37
+ /** Current window maximum. */
38
+ get max(): number;
39
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * @file Fixed-capacity ring buffer of parallel (x, y) Float64Arrays.
3
+ *
4
+ * Append is O(1) with no memmove: once the window is full the oldest sample is
5
+ * overwritten in place and `head` advances. y-extent is maintained
6
+ * incrementally by a {@link MonotonicExtent}, so min/max are O(1) too.
7
+ *
8
+ * Logical order (oldest→newest) is `head, head+1, … head+count-1` (mod cap).
9
+ * x is assumed monotonically increasing in push order, so it stays sorted in
10
+ * logical order — which the crosshair's binary search relies on.
11
+ */
12
+ /** Sliding-window columnar store for streaming (ring) mode. */
13
+ export declare class RingBuffer {
14
+ cap: number;
15
+ x: Float64Array;
16
+ y: Float64Array;
17
+ head: number;
18
+ count: number;
19
+ private seq;
20
+ private ext;
21
+ constructor(cap: number);
22
+ /** Current window y minimum (O(1)). */
23
+ get yMin(): number;
24
+ /** Current window y maximum (O(1)). */
25
+ get yMax(): number;
26
+ /** x of the oldest sample. */
27
+ get xFirst(): number;
28
+ /** x of the newest sample. */
29
+ get xLast(): number;
30
+ /** y of the newest sample. */
31
+ get lastY(): number;
32
+ /** Map a logical index [0, count) to its physical slot. */
33
+ physOf(logical: number): number;
34
+ /** Append one sample. x must be ≥ the previous x (monotonic). */
35
+ push(xv: number, yv: number): void;
36
+ /** Reset to empty (keeps allocated buffers). */
37
+ clear(): void;
38
+ /**
39
+ * Resize the window, preserving the most recent samples. Rare and O(keep):
40
+ * the retained tail is copied out, fresh buffers allocated, then re-pushed so
41
+ * the extent deque rebuilds correctly.
42
+ */
43
+ resize(newCap: number): void;
44
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * @file Unified data model behind both chart modes, implementing SeriesView.
3
+ *
4
+ * Snapshot mode (setData) holds caller-owned arrays directly with head=0.
5
+ * Ring mode (append/appendBatch) delegates storage to a {@link RingBuffer} and
6
+ * mirrors its O(1) cached geometry into local fields each tick. Either way the
7
+ * store exposes the same logical window `[0, count)` plus `physOf` /
8
+ * `bracketLogical` addressing, so renderers consume one stable contract.
9
+ *
10
+ * The store is purely about data: it never touches the canvas or scheduling.
11
+ */
12
+ import type { SeriesView } from '../types.js';
13
+ /** Owns the series data and computes/caches its extents. */
14
+ export declare class SeriesStore implements SeriesView {
15
+ xArr: Float64Array<ArrayBufferLike>;
16
+ yArr: Float64Array<ArrayBufferLike>;
17
+ head: number;
18
+ count: number;
19
+ cap: number;
20
+ xMin: number;
21
+ xMax: number;
22
+ yMin: number;
23
+ yMax: number;
24
+ private ring;
25
+ /** Whether ring (streaming) mode is active. */
26
+ get isRing(): boolean;
27
+ /**
28
+ * Create the ring up front (used when constructed with maxPoints).
29
+ * @throws {Error} if maxPoints < 1
30
+ */
31
+ initRing(maxPoints: number): void;
32
+ /**
33
+ * Snapshot mode: replace the whole series. Extents computed once in O(n).
34
+ * Disables any active ring mode.
35
+ * @throws {Error} if x and y have different lengths
36
+ * @throws {Error} if x is empty
37
+ */
38
+ setData(x: Float64Array<ArrayBufferLike>, y: Float64Array<ArrayBufferLike>): void;
39
+ /**
40
+ * Ring mode: append one sample (x must be monotonically increasing).
41
+ * @throws {Error} if ring mode is not active (maxPoints not set)
42
+ */
43
+ append(x: number, y: number): void;
44
+ /**
45
+ * Ring mode: append a batch of parallel samples. O(k).
46
+ * @throws {Error} if ring mode is not active
47
+ * @throws {Error} if xs and ys have different lengths
48
+ */
49
+ appendBatch(xs: ArrayLike<number>, ys: ArrayLike<number>): void;
50
+ /** Resize the streaming window, keeping the most recent samples. */
51
+ setMaxPoints(maxPoints: number): void;
52
+ /** Empty the current data (works in both modes). */
53
+ clear(): void;
54
+ /** Most recent y value, or NaN if empty. */
55
+ get lastValue(): number;
56
+ physOf(logical: number): number;
57
+ bracketLogical(target: number): number;
58
+ private requireRing;
59
+ /** Point the store's logical window at the ring's physical storage. */
60
+ private bindRing;
61
+ /** Mirror the ring's O(1) cached geometry into the store's fields. */
62
+ private syncFromRing;
63
+ /** Store y-extent, expanding a degenerate (flat) range so the line shows. */
64
+ private applyExtent;
65
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @file Default options merged over any user-supplied {@link ChartOpts}.
3
+ *
4
+ * The top-level colour / width fields are per-series fallbacks — each series
5
+ * config entry provides its own colour and optional overrides. `maxPoints: 0`
6
+ * is a sentinel meaning "snapshot mode" (no ring); the constructor activates
7
+ * ring mode only when the user passes a positive value.
8
+ *
9
+ * `yMin: 0 / yMax: 0` are sentinels meaning "auto" — the grid domain
10
+ * expands from data unless the user sets non-zero values.
11
+ */
12
+ import type { ResolvedOpts } from './types.js';
13
+ export declare const CHART_DEFAULTS: ResolvedOpts;