@pond-ts/charts 0.49.0 → 0.51.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/CHANGELOG.md +138 -1
- package/dist/AreaChart.js +1 -0
- package/dist/BandChart.js +1 -0
- package/dist/BarChart.d.ts +14 -1
- package/dist/BarChart.js +5 -2
- package/dist/BoxPlot.d.ts +12 -1
- package/dist/BoxPlot.js +4 -2
- package/dist/Candlestick.d.ts +14 -1
- package/dist/Candlestick.js +4 -2
- package/dist/ChartContainer.d.ts +61 -11
- package/dist/ChartContainer.js +70 -23
- package/dist/Layers.js +53 -14
- package/dist/Legend.js +5 -2
- package/dist/LineChart.js +1 -0
- package/dist/ScatterChart.js +1 -0
- package/dist/XAxis.js +3 -2
- package/dist/affine.d.ts +41 -0
- package/dist/affine.js +77 -0
- package/dist/area.d.ts +20 -2
- package/dist/area.js +151 -45
- package/dist/band.d.ts +2 -1
- package/dist/band.js +5 -0
- package/dist/bars.d.ts +14 -1
- package/dist/bars.js +42 -1
- package/dist/box.d.ts +3 -1
- package/dist/box.js +26 -6
- package/dist/context.d.ts +121 -22
- package/dist/context.js +8 -0
- package/dist/data.d.ts +8 -0
- package/dist/decimate.d.ts +116 -1
- package/dist/decimate.js +251 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.js +5 -3
- package/dist/line.d.ts +15 -1
- package/dist/line.js +84 -30
- package/dist/ohlc.d.ts +3 -1
- package/dist/ohlc.js +26 -5
- package/dist/tracker.d.ts +17 -4
- package/dist/tracker.js +19 -6
- package/dist/useChartLegend.d.ts +2 -2
- package/dist/useChartLegend.js +8 -7
- package/dist/viewport.d.ts +19 -0
- package/dist/viewport.js +32 -0
- package/package.json +3 -3
package/dist/context.d.ts
CHANGED
|
@@ -41,25 +41,12 @@ export interface ContainerFrame {
|
|
|
41
41
|
readonly rightGutter: number;
|
|
42
42
|
/** Vertical space between rows in px (not under the time axis). */
|
|
43
43
|
readonly rowGap: number;
|
|
44
|
-
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* still cursor stays put while a live window slides under it (a stored
|
|
48
|
-
* timestamp would drift sideways as `xScale` changes). A controlled
|
|
49
|
-
* `trackerPosition` (a timestamp) resolves to a pixel here.
|
|
50
|
-
*/
|
|
51
|
-
readonly cursorX: number | null;
|
|
52
|
-
/** Set the hovered plot-pixel x; a row's event surface calls this on pointer move. */
|
|
44
|
+
/** Set the hovered plot-pixel x; a row's event surface calls this on pointer move.
|
|
45
|
+
* The value itself is on {@link CursorFrame.cursorX} ({@link CursorContext}) —
|
|
46
|
+
* split out so a hover doesn't re-identify this frame (see [PND-HOVCTX]). */
|
|
53
47
|
setHoverX(x: number | null): void;
|
|
54
|
-
/**
|
|
55
|
-
*
|
|
56
|
-
* crosshair's horizontal line + value readout (which are row-specific, unlike
|
|
57
|
-
* the shared vertical `cursorX`). `null` when not hovering a plot. Hover-driven
|
|
58
|
-
* only (no controlled equivalent).
|
|
59
|
-
*/
|
|
60
|
-
readonly cursorY: number | null;
|
|
61
|
-
readonly cursorRowKey: symbol | null;
|
|
62
|
-
/** Set the hovered plot-pixel y + its row; the event surface calls this on move. */
|
|
48
|
+
/** Set the hovered plot-pixel y + its row; the event surface calls this on move.
|
|
49
|
+
* The values are on {@link CursorFrame} ({@link CursorContext}). */
|
|
63
50
|
setHoverY(y: number | null, rowKey: symbol | null): void;
|
|
64
51
|
/**
|
|
65
52
|
* `cursor="crosshair"` **y** snapping. **Default `true`** — the reticle centres
|
|
@@ -98,6 +85,14 @@ export interface ContainerFrame {
|
|
|
98
85
|
* zoom the view, or map the span onto a subscription's range params.
|
|
99
86
|
*/
|
|
100
87
|
readonly onRegionSelect: ((range: readonly [number, number]) => void) | undefined;
|
|
88
|
+
/**
|
|
89
|
+
* A stable sink for per-repaint {@link DrawStatsFrame}s, or `undefined` when no
|
|
90
|
+
* `onDrawStats` consumer is subscribed — the `undefined` is the signal for
|
|
91
|
+
* `Layers` to skip per-layer timing entirely (zero overhead when unused). The
|
|
92
|
+
* identity is stable while subscribed (it reads a ref), so an inline
|
|
93
|
+
* `onDrawStats` arrow doesn't thrash the draw memo.
|
|
94
|
+
*/
|
|
95
|
+
readonly reportDrawStats: ((frame: DrawStatsFrame) => void) | undefined;
|
|
101
96
|
/**
|
|
102
97
|
* Require a modifier key held to start a region-drag — set to `'shift'` to make
|
|
103
98
|
* plain drag **pan** and **shift**-drag select, when `panZoom` is on. Only
|
|
@@ -250,8 +245,10 @@ export interface ContainerFrame {
|
|
|
250
245
|
* category label), and the cursor readout to format the x position.
|
|
251
246
|
*/
|
|
252
247
|
readonly xKind: 'time' | 'value' | 'category';
|
|
253
|
-
/**
|
|
254
|
-
readonly
|
|
248
|
+
/** Drag-pan enabled (the `'pan'` and `'panZoom'` container modes). */
|
|
249
|
+
readonly panEnabled: boolean;
|
|
250
|
+
/** Wheel-zoom enabled (the `'panZoom'` container mode only). */
|
|
251
|
+
readonly zoomEnabled: boolean;
|
|
255
252
|
/** Minimum visible duration (ms) — the zoom-in floor. */
|
|
256
253
|
readonly minDuration: number;
|
|
257
254
|
/**
|
|
@@ -398,6 +395,97 @@ export interface GutterReq {
|
|
|
398
395
|
readonly right: readonly number[];
|
|
399
396
|
}
|
|
400
397
|
export declare const ContainerContext: import("react").Context<ContainerFrame | null>;
|
|
398
|
+
/**
|
|
399
|
+
* The **per-move** cursor state — split out of {@link ContainerFrame} so a
|
|
400
|
+
* mousemove re-identifies only this (small) context, not the whole frame.
|
|
401
|
+
* `ContainerFrame` carries ~50 mostly-static fields; when the cursor lived
|
|
402
|
+
* there, every pointer move rebuilt it and re-rendered **all** its consumers
|
|
403
|
+
* (both `YAxis`, `Legend`, `Bar`/`Box`) even though only the SVG overlay moved.
|
|
404
|
+
* Config consumers now read the stable frame and skip hover re-renders; the
|
|
405
|
+
* genuine cursor consumers (`Layers` overlay, `XAxis` crosshair pill,
|
|
406
|
+
* `useChartLegend` values) read this. See [PND-HOVCTX] and the note it links.
|
|
407
|
+
*
|
|
408
|
+
* The cursor *time* is **not** here — each consumer derives it locally from
|
|
409
|
+
* `cursorX` + its own `xScale` (an in-bounds `xScale.invert`), as before.
|
|
410
|
+
* ({@link ContainerFrame} still carries a `cursorTime` **boolean** — the
|
|
411
|
+
* unrelated "show time in the readout" config flag.)
|
|
412
|
+
*/
|
|
413
|
+
export interface CursorFrame {
|
|
414
|
+
/**
|
|
415
|
+
* The crosshair's **plot-pixel x** (`0..plotWidth`), shared across rows so the
|
|
416
|
+
* tracker syncs, or `null` when not hovering. A *pixel*, not a timestamp — so a
|
|
417
|
+
* still cursor stays put while a live window slides under it (a stored
|
|
418
|
+
* timestamp would drift sideways as `xScale` changes). A controlled
|
|
419
|
+
* `trackerPosition` (a timestamp) resolves to a pixel here.
|
|
420
|
+
*/
|
|
421
|
+
readonly cursorX: number | null;
|
|
422
|
+
/**
|
|
423
|
+
* The hovered plot-pixel **y** and the row it's in — for the free-form
|
|
424
|
+
* crosshair's horizontal line + value readout (which are row-specific, unlike
|
|
425
|
+
* the shared vertical `cursorX`). `null` when not hovering a plot. Hover-driven
|
|
426
|
+
* only (no controlled equivalent).
|
|
427
|
+
*/
|
|
428
|
+
readonly cursorY: number | null;
|
|
429
|
+
readonly cursorRowKey: symbol | null;
|
|
430
|
+
}
|
|
431
|
+
export declare const CursorContext: import("react").Context<CursorFrame>;
|
|
432
|
+
/**
|
|
433
|
+
* What a {@link RowLayer.draw} may return so the container can report render
|
|
434
|
+
* cost + whether M4 decimation engaged this frame ({@link
|
|
435
|
+
* ContainerProps.onDrawStats}). Optional — a layer that returns `void` reports
|
|
436
|
+
* only its `drawMs` (the render loop times every layer regardless).
|
|
437
|
+
*/
|
|
438
|
+
export interface LayerDrawStats {
|
|
439
|
+
/** Source series length the draw received (pre-cull, pre-decimation). */
|
|
440
|
+
readonly sourceCount: number;
|
|
441
|
+
/** Points / marks actually drawn this frame — after viewport culling and, if
|
|
442
|
+
* it engaged, M4 decimation. `drawnCount < sourceCount` can come from **either**
|
|
443
|
+
* culling (a zoomed-in view drops off-screen points) **or** decimation — read
|
|
444
|
+
* `decimated` to tell which; `drawnCount === sourceCount` means everything in
|
|
445
|
+
* view was drawn full-resolution. */
|
|
446
|
+
readonly drawnCount: number;
|
|
447
|
+
/** Whether M4 decimation engaged (vs. drew the culled slice full-resolution).
|
|
448
|
+
* Distinguishes a decimated draw from a merely culled one — both shrink
|
|
449
|
+
* `drawnCount`. */
|
|
450
|
+
readonly decimated: boolean;
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* One layer's line in a {@link DrawStatsFrame}: its identity + measured draw
|
|
454
|
+
* time, plus the {@link LayerDrawStats} the layer reported (`undefined` counts
|
|
455
|
+
* for a layer that returns none — e.g. a non-decimating scatter/bar, which
|
|
456
|
+
* still contributes its `drawMs`).
|
|
457
|
+
*/
|
|
458
|
+
export interface LayerDrawInfo {
|
|
459
|
+
/** The layer's `as` role, or `undefined` if it set none. */
|
|
460
|
+
readonly as: string | undefined;
|
|
461
|
+
/** Z-order index within the row (the `<Layers>` declaration position). */
|
|
462
|
+
readonly index: number;
|
|
463
|
+
/** Wall-clock ms spent in this layer's `draw` this frame. */
|
|
464
|
+
readonly drawMs: number;
|
|
465
|
+
readonly sourceCount: number | undefined;
|
|
466
|
+
readonly drawnCount: number | undefined;
|
|
467
|
+
readonly decimated: boolean | undefined;
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* The per-repaint draw-stats frame handed to {@link ContainerProps.onDrawStats}.
|
|
471
|
+
* Fires **once per row-canvas repaint** (rows repaint independently, so a
|
|
472
|
+
* multi-row container fires one frame per row that painted), carrying that row's
|
|
473
|
+
* layers newest-drawn. The seam the dashboard A/B asked for (2026-07-21): read
|
|
474
|
+
* `drawnCount` vs `sourceCount` to see whether M4 engaged, and `drawMs` for the
|
|
475
|
+
* per-layer render cost the packaged layer otherwise hides.
|
|
476
|
+
*/
|
|
477
|
+
export interface DrawStatsFrame {
|
|
478
|
+
/**
|
|
479
|
+
* Opaque, stable identity of the **row** this frame is for — rows repaint
|
|
480
|
+
* independently, so a multi-row container fires one frame per row and this is
|
|
481
|
+
* how a consumer attributes each (group frames by `rowKey`, e.g. as a `Map`
|
|
482
|
+
* key). Not human-readable; `layers[].as` labels the series within a row.
|
|
483
|
+
*/
|
|
484
|
+
readonly rowKey: symbol;
|
|
485
|
+
readonly layers: readonly LayerDrawInfo[];
|
|
486
|
+
/** Total ms across this row's layer draws this frame. */
|
|
487
|
+
readonly totalDrawMs: number;
|
|
488
|
+
}
|
|
401
489
|
/**
|
|
402
490
|
* A draw layer ({@link LineChart}, …) registered into a {@link Layers}, paired
|
|
403
491
|
* with the id of the axis it scales against. The row computes a y-scale per
|
|
@@ -405,6 +493,12 @@ export declare const ContainerContext: import("react").Context<ContainerFrame |
|
|
|
405
493
|
* domain); each layer draws with its own axis's scale.
|
|
406
494
|
*/
|
|
407
495
|
export interface RowLayer {
|
|
496
|
+
/**
|
|
497
|
+
* The layer's `as` role (the series identity), surfaced in {@link
|
|
498
|
+
* DrawStatsFrame} so a draw-stats consumer can label each line. `undefined`
|
|
499
|
+
* when the layer was given no `as`.
|
|
500
|
+
*/
|
|
501
|
+
readonly as?: string | undefined;
|
|
408
502
|
/** This layer's finite-value `[min, max]`, or `null` if it has none. */
|
|
409
503
|
yExtent(): [number, number] | null;
|
|
410
504
|
/**
|
|
@@ -468,8 +562,13 @@ export interface RowLayer {
|
|
|
468
562
|
* resolves the layer's axis scale, as for `draw`).
|
|
469
563
|
*/
|
|
470
564
|
hitTest?(px: number, py: number, xScale: (value: number) => number, yScale: (value: number) => number): SelectInfo | null;
|
|
471
|
-
/**
|
|
472
|
-
|
|
565
|
+
/**
|
|
566
|
+
* Draw into the plot canvas. `xScale`/`yScale` map data→pixels. May return
|
|
567
|
+
* {@link LayerDrawStats} (source/drawn counts + whether decimation engaged) so
|
|
568
|
+
* the container can surface them via {@link ContainerProps.onDrawStats}; a
|
|
569
|
+
* layer that returns `void` still contributes its measured `drawMs`.
|
|
570
|
+
*/
|
|
571
|
+
draw(ctx: CanvasRenderingContext2D, xScale: (value: number) => number, yScale: (value: number) => number): LayerDrawStats | void;
|
|
473
572
|
}
|
|
474
573
|
/** One tracker readout point — a dot + value the overlay draws at the cursor. */
|
|
475
574
|
export interface TrackerSample {
|
package/dist/context.js
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { createContext } from 'react';
|
|
2
2
|
export const ContainerContext = createContext(null);
|
|
3
|
+
/** No-cursor default, so a consumer outside a provider reads "not hovering"
|
|
4
|
+
* rather than needing a null guard (the container always provides a real one). */
|
|
5
|
+
const NO_CURSOR = {
|
|
6
|
+
cursorX: null,
|
|
7
|
+
cursorY: null,
|
|
8
|
+
cursorRowKey: null,
|
|
9
|
+
};
|
|
10
|
+
export const CursorContext = createContext(NO_CURSOR);
|
|
3
11
|
export const RowContext = createContext(null);
|
|
4
12
|
export const LayersContext = createContext(null);
|
|
5
13
|
//# sourceMappingURL=context.js.map
|
package/dist/data.d.ts
CHANGED
|
@@ -13,6 +13,14 @@ import type { SeriesSchema, TimeSeries, ValueSeriesSchema } from 'pond-ts';
|
|
|
13
13
|
* column materialized to a `Float64Array`. `x` is **monotonically ascending**
|
|
14
14
|
* (a series' key column is sorted) — the draw layers and the viewport bisect
|
|
15
15
|
* (`culling.ts`) rely on it, as `sessionRuns` already does.
|
|
16
|
+
*
|
|
17
|
+
* **Neither buffer may be mutated in place** — not just `x`. The area fill
|
|
18
|
+
* gradient's value extent is memoized on the `y` buffer's identity
|
|
19
|
+
* (`columnFiniteExtent`, [PND-GRADX]), so a consumer that mutated a live `y`
|
|
20
|
+
* buffer's contents across frames instead of materializing a fresh column would
|
|
21
|
+
* read a stale gradient span (no crash, just a wrong shade). The built-in
|
|
22
|
+
* readers always allocate a fresh exactly-sized buffer per materialization, so
|
|
23
|
+
* buffer identity tracks data identity — hold to that.
|
|
16
24
|
*/
|
|
17
25
|
export interface ChartSeries {
|
|
18
26
|
readonly x: Float64Array;
|
package/dist/decimate.d.ts
CHANGED
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
* `xScale` and breaks its subpath on `NaN`) — decimation is a pre-pass that
|
|
46
46
|
* shrinks the point count, not a second renderer.
|
|
47
47
|
*/
|
|
48
|
-
import type { ChartSeries, BandSeries } from './data.js';
|
|
48
|
+
import type { ChartSeries, BandSeries, OhlcSeries, BoxSeries, BarSeries } from './data.js';
|
|
49
49
|
import type { Scale } from './line.js';
|
|
50
50
|
/**
|
|
51
51
|
* A line layer's M4-decimation control (`<LineChart decimate>`). **Default
|
|
@@ -152,6 +152,42 @@ export declare function mergeGapEdges(edges: Float64Array, gaps: number[], lo: n
|
|
|
152
152
|
* series into per-session subpaths at exactly those instants.
|
|
153
153
|
*/
|
|
154
154
|
export declare function decimateM4(cs: ChartSeries, xScale: Scale, ctx: CanvasRenderingContext2D, k?: number, boundaries?: readonly number[]): ChartSeries;
|
|
155
|
+
/**
|
|
156
|
+
* Cull `source` to the visible window and M4-decimate it, **memoized per source
|
|
157
|
+
* series** so a y-only repaint reuses the prior frame's polyline instead of
|
|
158
|
+
* re-binning O(N) points. The decimation output is a pure function of the source
|
|
159
|
+
* data, the x mapping, the device width, the threshold, and the session breaks —
|
|
160
|
+
* it **never reads the y-scale** — so it is byte-identical across every y-zoom /
|
|
161
|
+
* y-autorange frame (the ~19% mountain@1M recompute the 2026-07 bench profile
|
|
162
|
+
* flagged, finding 3: an x-only computation re-run under y-only invalidation).
|
|
163
|
+
*
|
|
164
|
+
* The cache holds **one** entry per source: a y-only frame matches the stored
|
|
165
|
+
* key → hit; a pan / x-zoom mints a fresh `xScale` (`ChartContainer` keys the
|
|
166
|
+
* scale on the x-domain, not the y-domain — so the scale object is stable under
|
|
167
|
+
* y-zoom and fresh under pan) → miss, and the single entry is overwritten. So it
|
|
168
|
+
* **wins on y-only frames and is a no-op under pan** — bounded, never growing,
|
|
169
|
+
* the same reasoning that made Path2D caching not help pan ([PND-DECIM]).
|
|
170
|
+
*
|
|
171
|
+
* Correctness rests on the {@link ChartSeries} immutability contract: a data
|
|
172
|
+
* change mints a **new** source object (the layer re-materializes its column),
|
|
173
|
+
* so a stale entry can't be read. `boundaries` is compared by **identity** — the
|
|
174
|
+
* `<LineChart>` session-break instants are `useMemo`-stable across frames; a
|
|
175
|
+
* caller passing a fresh array each frame simply misses (safe, no benefit). `W`
|
|
176
|
+
* is `deviceBucketCount(ctx)` so a DPR / resize change re-keys. Keying on the
|
|
177
|
+
* `xScale` object (not just its `[domain, W]`) is what keeps the hit correct on
|
|
178
|
+
* a **non-affine** trading-time scale too — same scale object ⇒ identical
|
|
179
|
+
* pixel-column edges.
|
|
180
|
+
*
|
|
181
|
+
* Returns `{ series, decimated }`: `series` is the M4 polyline, or the plain
|
|
182
|
+
* culled slice when the window is too sparse to decimate ({@link decimateM4}
|
|
183
|
+
* no-ops); `decimated` says which (for the caller's draw-stats + session-run
|
|
184
|
+
* split). Callers pass the **pre-cull** source and skip their own cull — this
|
|
185
|
+
* function does it, so a cache hit skips the cull too.
|
|
186
|
+
*/
|
|
187
|
+
export declare function decimateM4Cached(source: ChartSeries, xScale: Scale, ctx: CanvasRenderingContext2D, k?: number, boundaries?: readonly number[]): {
|
|
188
|
+
series: ChartSeries;
|
|
189
|
+
decimated: boolean;
|
|
190
|
+
};
|
|
155
191
|
/**
|
|
156
192
|
* Assemble the M4 polyline {@link ChartSeries} from the four binned channels.
|
|
157
193
|
* Split out (pure, no canvas / pond deps) so the point emission is unit-tested
|
|
@@ -190,4 +226,83 @@ export declare function m4Polyline(edges: Float64Array, mn: Float64Array, mx: Fl
|
|
|
190
226
|
* band segment that no single sample carried.
|
|
191
227
|
*/
|
|
192
228
|
export declare function decimateBand(band: BandSeries, xScale: Scale, ctx: CanvasRenderingContext2D, k?: number): BandSeries;
|
|
229
|
+
/**
|
|
230
|
+
* Decimate an {@link OhlcSeries} to one **aggregate candle per device-pixel
|
|
231
|
+
* column** — `open = first`, `high = max`, `low = min`, `close = last` over the
|
|
232
|
+
* candles that fall in the column. This is exactly a candle re-bucketed to a
|
|
233
|
+
* **coarser timeframe** (the pixel-column's time range): it is never *wrong* —
|
|
234
|
+
* it is the true OHLC of that span — so a dense chart that zooms out reads as
|
|
235
|
+
* fewer, wider aggregate candles, the trading-UI convention (decimator
|
|
236
|
+
* assessment §2.4). Auto-on with an opt-out; a consumer wanting fixed-timeframe
|
|
237
|
+
* candles pre-aggregates upstream and passes `decimate={false}`.
|
|
238
|
+
*
|
|
239
|
+
* Returns the **same object** when decimation doesn't apply (sparse series,
|
|
240
|
+
* domainless / non-invertible scale, no canvas width). The slot of each
|
|
241
|
+
* aggregate candle is its pixel column `[edges[b], edges[b+1]]`; an empty column
|
|
242
|
+
* (no candles) reduces to `NaN` on all channels — `drawCandles` skips it. No
|
|
243
|
+
* session-break union is needed: candles are independent marks (they never
|
|
244
|
+
* connect), and a trading-axis closed period is simply an empty column.
|
|
245
|
+
*/
|
|
246
|
+
export declare function decimateOhlc(ohlc: OhlcSeries, xScale: Scale, ctx: CanvasRenderingContext2D, k?: number, visibleCount?: number): OhlcSeries;
|
|
247
|
+
/**
|
|
248
|
+
* Decimate a {@link BoxSeries} to one **aggregate box per device-pixel column** —
|
|
249
|
+
* the interval-mark sibling of {@link decimateOhlc}. Each channel is binned over
|
|
250
|
+
* the column: the whiskers widen to the column's full reach (`lower = min(lower)`,
|
|
251
|
+
* `upper = max(upper)`, exactly {@link decimateBand}'s envelope), the body to the
|
|
252
|
+
* column's **IQR envelope** (`q1 = min(q1)`, `q3 = max(q3)`), and the centre line
|
|
253
|
+
* to the **first** box's `median` in the column (a real median value, not an
|
|
254
|
+
* average — `binBy` carries no mean; it stays within the aggregate body since the
|
|
255
|
+
* first box's `[q1, q3]` ⊆ the envelope). So a dense per-x distribution chart
|
|
256
|
+
* that zooms out reads as fewer, wider boxes summarising each column's spread.
|
|
257
|
+
*
|
|
258
|
+
* Gates on the **visible** box count (a box's width is its slot — decimating a
|
|
259
|
+
* handful of deep-zoomed boxes would render 1px slivers, the same trap the candle
|
|
260
|
+
* path has). Returns the **same object** when decimation doesn't apply (below the
|
|
261
|
+
* visible-density threshold, domainless / non-invertible scale, no canvas width).
|
|
262
|
+
* The `hasBox` / `hasMedian` flags carry through, so a **range-only** box (all-NaN
|
|
263
|
+
* `q1`/`q3`) stays range-only (its binned body is NaN throughout). An empty column
|
|
264
|
+
* reduces to `NaN` on every channel — `drawBox` skips it via `isFiniteBox`.
|
|
265
|
+
*/
|
|
266
|
+
export declare function decimateBox(box: BoxSeries, xScale: Scale, ctx: CanvasRenderingContext2D, k?: number, visibleCount?: number): BoxSeries;
|
|
267
|
+
/**
|
|
268
|
+
* One filled **envelope rect per device-pixel column** — the decimated form of a
|
|
269
|
+
* {@link BarSeries} ({@link decimateBars}). Each column `b` spans `[begin[b],
|
|
270
|
+
* end[b]]` in key space and fills the value range `[lo[b], hi[b]]`, i.e. the union
|
|
271
|
+
* of every bar in that column (their tops range over `[min, max]`, and each bar
|
|
272
|
+
* also reaches the baseline — so the union is `[min(minValue, baseline),
|
|
273
|
+
* max(maxValue, baseline)]`). An empty column carries `NaN` on `lo`/`hi` and draws
|
|
274
|
+
* nothing.
|
|
275
|
+
*/
|
|
276
|
+
export interface BarColumnEnvelope {
|
|
277
|
+
readonly begin: Float64Array;
|
|
278
|
+
readonly end: Float64Array;
|
|
279
|
+
readonly lo: Float64Array;
|
|
280
|
+
readonly hi: Float64Array;
|
|
281
|
+
readonly length: number;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Decimate a {@link BarSeries} to **one envelope rect per device-pixel column**
|
|
285
|
+
* ([PND-MARKDEC]) — the interval-mark analog of {@link decimateBand}, for the
|
|
286
|
+
* "column chart, dense in x" case (SciChart-suite finding 4: a bar column has no
|
|
287
|
+
* decimation path, so it falls off where line/area/candle don't). Once each bar's
|
|
288
|
+
* slot is narrower than ~1px (the visible bars exceed `k ×` the device-pixel
|
|
289
|
+
* column count), the individual rects overplot into a solid silhouette; this
|
|
290
|
+
* replaces them with the exact painted union: per column, `lo = min(minValue,
|
|
291
|
+
* baseline)` and `hi = max(maxValue, baseline)` — the bars' value range widened
|
|
292
|
+
* to include the baseline every bar reaches. Drawing one rect `[begin, end] ×
|
|
293
|
+
* [lo, hi]` per column reproduces that silhouette from O(W) rects instead of
|
|
294
|
+
* O(visible).
|
|
295
|
+
*
|
|
296
|
+
* Gates on the **visible** bar count (`visibleCount`, a bar's width is its slot —
|
|
297
|
+
* decimating a handful of deep-zoomed bars would render 1px slivers, the same
|
|
298
|
+
* trap the candle / box paths gate against). Returns `null` when decimation
|
|
299
|
+
* doesn't apply (below the visible-density threshold, domainless / non-invertible
|
|
300
|
+
* scale, no canvas width) — the caller then draws every visible bar. Bars are
|
|
301
|
+
* binned by their **`begin`** key (at this density `begin`/`end` sit in the same
|
|
302
|
+
* column); the envelope ignores per-bar `gapPx` and tiles the column (a few-px
|
|
303
|
+
* gap is invisible at <1px bars anyway — the standard decimation tradeoff).
|
|
304
|
+
* `baseline` is the resolved bar baseline in **value** units (from
|
|
305
|
+
* `resolveBarBaseline`), so the union is honest about the zero line.
|
|
306
|
+
*/
|
|
307
|
+
export declare function decimateBars(cs: BarSeries, xScale: Scale, ctx: CanvasRenderingContext2D, baseline: number, k?: number, visibleCount?: number): BarColumnEnvelope | null;
|
|
193
308
|
//# sourceMappingURL=decimate.d.ts.map
|
package/dist/decimate.js
CHANGED
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
* shrinks the point count, not a second renderer.
|
|
47
47
|
*/
|
|
48
48
|
import { Float64Column } from 'pond-ts';
|
|
49
|
-
import { scaleDomain } from './culling.js';
|
|
49
|
+
import { scaleDomain, cullChartSeries } from './culling.js';
|
|
50
50
|
/** The device-pixel bucket count for `ctx` — the backing buffer width, i.e.
|
|
51
51
|
* `plotWidthCss × DPR` (so buckets land at device-pixel resolution). Falls back
|
|
52
52
|
* to `0` when there is no sized canvas (a headless test ctx), which the caller
|
|
@@ -247,6 +247,74 @@ export function decimateM4(cs, xScale, ctx, k = 2, boundaries = []) {
|
|
|
247
247
|
const { lo: mn, hi: mx, first, last, } = col.binBy(cs.x, edges, 'minMaxFirstLast');
|
|
248
248
|
return m4Polyline(edges, mn, mx, first, last, buckets, breaks.length > 0 ? new Set(breaks) : undefined);
|
|
249
249
|
}
|
|
250
|
+
/**
|
|
251
|
+
* Stable empty boundary list for the (common) no-session-break decimation cache
|
|
252
|
+
* key — so an area / no-break line draw compares equal frame-to-frame instead of
|
|
253
|
+
* missing on a fresh default `[]`.
|
|
254
|
+
*/
|
|
255
|
+
const NO_BOUNDARIES = [];
|
|
256
|
+
/**
|
|
257
|
+
* One-entry-per-source cache of the cull+M4-decimate result ([PND-DECKEY]),
|
|
258
|
+
* keyed on the source series (`WeakMap`) → the last `(xScale, W, k, boundaries)`
|
|
259
|
+
* it was drawn for. Evicts with the series (no leak); one entry per source keeps
|
|
260
|
+
* it bounded under pan (see {@link decimateM4Cached}).
|
|
261
|
+
*/
|
|
262
|
+
const m4Cache = new WeakMap();
|
|
263
|
+
/**
|
|
264
|
+
* Cull `source` to the visible window and M4-decimate it, **memoized per source
|
|
265
|
+
* series** so a y-only repaint reuses the prior frame's polyline instead of
|
|
266
|
+
* re-binning O(N) points. The decimation output is a pure function of the source
|
|
267
|
+
* data, the x mapping, the device width, the threshold, and the session breaks —
|
|
268
|
+
* it **never reads the y-scale** — so it is byte-identical across every y-zoom /
|
|
269
|
+
* y-autorange frame (the ~19% mountain@1M recompute the 2026-07 bench profile
|
|
270
|
+
* flagged, finding 3: an x-only computation re-run under y-only invalidation).
|
|
271
|
+
*
|
|
272
|
+
* The cache holds **one** entry per source: a y-only frame matches the stored
|
|
273
|
+
* key → hit; a pan / x-zoom mints a fresh `xScale` (`ChartContainer` keys the
|
|
274
|
+
* scale on the x-domain, not the y-domain — so the scale object is stable under
|
|
275
|
+
* y-zoom and fresh under pan) → miss, and the single entry is overwritten. So it
|
|
276
|
+
* **wins on y-only frames and is a no-op under pan** — bounded, never growing,
|
|
277
|
+
* the same reasoning that made Path2D caching not help pan ([PND-DECIM]).
|
|
278
|
+
*
|
|
279
|
+
* Correctness rests on the {@link ChartSeries} immutability contract: a data
|
|
280
|
+
* change mints a **new** source object (the layer re-materializes its column),
|
|
281
|
+
* so a stale entry can't be read. `boundaries` is compared by **identity** — the
|
|
282
|
+
* `<LineChart>` session-break instants are `useMemo`-stable across frames; a
|
|
283
|
+
* caller passing a fresh array each frame simply misses (safe, no benefit). `W`
|
|
284
|
+
* is `deviceBucketCount(ctx)` so a DPR / resize change re-keys. Keying on the
|
|
285
|
+
* `xScale` object (not just its `[domain, W]`) is what keeps the hit correct on
|
|
286
|
+
* a **non-affine** trading-time scale too — same scale object ⇒ identical
|
|
287
|
+
* pixel-column edges.
|
|
288
|
+
*
|
|
289
|
+
* Returns `{ series, decimated }`: `series` is the M4 polyline, or the plain
|
|
290
|
+
* culled slice when the window is too sparse to decimate ({@link decimateM4}
|
|
291
|
+
* no-ops); `decimated` says which (for the caller's draw-stats + session-run
|
|
292
|
+
* split). Callers pass the **pre-cull** source and skip their own cull — this
|
|
293
|
+
* function does it, so a cache hit skips the cull too.
|
|
294
|
+
*/
|
|
295
|
+
export function decimateM4Cached(source, xScale, ctx, k, boundaries = NO_BOUNDARIES) {
|
|
296
|
+
const W = deviceBucketCount(ctx);
|
|
297
|
+
const cached = m4Cache.get(source);
|
|
298
|
+
if (cached !== undefined &&
|
|
299
|
+
cached.xScale === xScale &&
|
|
300
|
+
cached.W === W &&
|
|
301
|
+
cached.k === k &&
|
|
302
|
+
cached.boundaries === boundaries) {
|
|
303
|
+
return cached;
|
|
304
|
+
}
|
|
305
|
+
const culled = cullChartSeries(source, xScale);
|
|
306
|
+
const series = decimateM4(culled, xScale, ctx, k, boundaries);
|
|
307
|
+
const entry = {
|
|
308
|
+
xScale,
|
|
309
|
+
W,
|
|
310
|
+
k,
|
|
311
|
+
boundaries,
|
|
312
|
+
series,
|
|
313
|
+
decimated: series !== culled,
|
|
314
|
+
};
|
|
315
|
+
m4Cache.set(source, entry);
|
|
316
|
+
return entry;
|
|
317
|
+
}
|
|
250
318
|
/** Shared empty break-set for the common (no session-break) case. */
|
|
251
319
|
const NO_BREAKS = new Set();
|
|
252
320
|
/**
|
|
@@ -356,4 +424,186 @@ export function decimateBand(band, xScale, ctx, k = 2) {
|
|
|
356
424
|
}
|
|
357
425
|
return { x, lower, upper, length: W };
|
|
358
426
|
}
|
|
427
|
+
/**
|
|
428
|
+
* Decimate an {@link OhlcSeries} to one **aggregate candle per device-pixel
|
|
429
|
+
* column** — `open = first`, `high = max`, `low = min`, `close = last` over the
|
|
430
|
+
* candles that fall in the column. This is exactly a candle re-bucketed to a
|
|
431
|
+
* **coarser timeframe** (the pixel-column's time range): it is never *wrong* —
|
|
432
|
+
* it is the true OHLC of that span — so a dense chart that zooms out reads as
|
|
433
|
+
* fewer, wider aggregate candles, the trading-UI convention (decimator
|
|
434
|
+
* assessment §2.4). Auto-on with an opt-out; a consumer wanting fixed-timeframe
|
|
435
|
+
* candles pre-aggregates upstream and passes `decimate={false}`.
|
|
436
|
+
*
|
|
437
|
+
* Returns the **same object** when decimation doesn't apply (sparse series,
|
|
438
|
+
* domainless / non-invertible scale, no canvas width). The slot of each
|
|
439
|
+
* aggregate candle is its pixel column `[edges[b], edges[b+1]]`; an empty column
|
|
440
|
+
* (no candles) reduces to `NaN` on all channels — `drawCandles` skips it. No
|
|
441
|
+
* session-break union is needed: candles are independent marks (they never
|
|
442
|
+
* connect), and a trading-axis closed period is simply an empty column.
|
|
443
|
+
*/
|
|
444
|
+
export function decimateOhlc(ohlc, xScale, ctx, k = 2, visibleCount = ohlc.length) {
|
|
445
|
+
// Gate on the number of candles *in view*, not the whole series: a candle's
|
|
446
|
+
// width is its pixel-column slot, so re-slotting a handful of deep-zoomed
|
|
447
|
+
// candles to one column each would render them as 1px slivers. Below the
|
|
448
|
+
// visible-density threshold the loop-bound cull draws them at full width.
|
|
449
|
+
if (!shouldDecimateCount(visibleCount, ctx, k))
|
|
450
|
+
return ohlc;
|
|
451
|
+
const dom = scaleDomain(xScale);
|
|
452
|
+
if (dom === null || dom[1] <= dom[0])
|
|
453
|
+
return ohlc;
|
|
454
|
+
const invert = scaleInvert(xScale);
|
|
455
|
+
const plotWidthCss = scaleRangeWidth(xScale);
|
|
456
|
+
if (invert === null || plotWidthCss === null)
|
|
457
|
+
return ohlc;
|
|
458
|
+
const W = deviceBucketCount(ctx);
|
|
459
|
+
const edges = pixelEdges(invert, plotWidthCss, W);
|
|
460
|
+
// Bin each channel over the candles' (monotonic) left-edge key. open/close need
|
|
461
|
+
// the first/last channels (only `'minMaxFirstLast'` carries them); high/low are
|
|
462
|
+
// the scalar max/min. Four O(n) walks — candle counts are modest.
|
|
463
|
+
const key = ohlc.x;
|
|
464
|
+
const openCh = new Float64Column(ohlc.open, ohlc.length).binBy(key, edges, 'minMaxFirstLast').first;
|
|
465
|
+
const closeCh = new Float64Column(ohlc.close, ohlc.length).binBy(key, edges, 'minMaxFirstLast').last;
|
|
466
|
+
const highCh = new Float64Column(ohlc.high, ohlc.length).binBy(key, edges, 'max');
|
|
467
|
+
const lowCh = new Float64Column(ohlc.low, ohlc.length).binBy(key, edges, 'min');
|
|
468
|
+
const x = new Float64Array(W);
|
|
469
|
+
const xEnd = new Float64Array(W);
|
|
470
|
+
for (let b = 0; b < W; b += 1) {
|
|
471
|
+
x[b] = edges[b]; // the aggregate candle's slot IS its pixel column
|
|
472
|
+
xEnd[b] = edges[b + 1];
|
|
473
|
+
}
|
|
474
|
+
return {
|
|
475
|
+
x,
|
|
476
|
+
xEnd,
|
|
477
|
+
open: openCh,
|
|
478
|
+
high: highCh,
|
|
479
|
+
low: lowCh,
|
|
480
|
+
close: closeCh,
|
|
481
|
+
length: W,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Decimate a {@link BoxSeries} to one **aggregate box per device-pixel column** —
|
|
486
|
+
* the interval-mark sibling of {@link decimateOhlc}. Each channel is binned over
|
|
487
|
+
* the column: the whiskers widen to the column's full reach (`lower = min(lower)`,
|
|
488
|
+
* `upper = max(upper)`, exactly {@link decimateBand}'s envelope), the body to the
|
|
489
|
+
* column's **IQR envelope** (`q1 = min(q1)`, `q3 = max(q3)`), and the centre line
|
|
490
|
+
* to the **first** box's `median` in the column (a real median value, not an
|
|
491
|
+
* average — `binBy` carries no mean; it stays within the aggregate body since the
|
|
492
|
+
* first box's `[q1, q3]` ⊆ the envelope). So a dense per-x distribution chart
|
|
493
|
+
* that zooms out reads as fewer, wider boxes summarising each column's spread.
|
|
494
|
+
*
|
|
495
|
+
* Gates on the **visible** box count (a box's width is its slot — decimating a
|
|
496
|
+
* handful of deep-zoomed boxes would render 1px slivers, the same trap the candle
|
|
497
|
+
* path has). Returns the **same object** when decimation doesn't apply (below the
|
|
498
|
+
* visible-density threshold, domainless / non-invertible scale, no canvas width).
|
|
499
|
+
* The `hasBox` / `hasMedian` flags carry through, so a **range-only** box (all-NaN
|
|
500
|
+
* `q1`/`q3`) stays range-only (its binned body is NaN throughout). An empty column
|
|
501
|
+
* reduces to `NaN` on every channel — `drawBox` skips it via `isFiniteBox`.
|
|
502
|
+
*/
|
|
503
|
+
export function decimateBox(box, xScale, ctx, k = 2, visibleCount = box.length) {
|
|
504
|
+
if (!shouldDecimateCount(visibleCount, ctx, k))
|
|
505
|
+
return box;
|
|
506
|
+
const dom = scaleDomain(xScale);
|
|
507
|
+
if (dom === null || dom[1] <= dom[0])
|
|
508
|
+
return box;
|
|
509
|
+
const invert = scaleInvert(xScale);
|
|
510
|
+
const plotWidthCss = scaleRangeWidth(xScale);
|
|
511
|
+
if (invert === null || plotWidthCss === null)
|
|
512
|
+
return box;
|
|
513
|
+
const W = deviceBucketCount(ctx);
|
|
514
|
+
const edges = pixelEdges(invert, plotWidthCss, W);
|
|
515
|
+
const key = box.x;
|
|
516
|
+
const n = box.length;
|
|
517
|
+
// Envelope whiskers + IQR body (scalar min/max); centre line = the first box's
|
|
518
|
+
// median (only `'minMaxFirstLast'` carries `first`). Five O(n) walks — box
|
|
519
|
+
// counts are modest, like candles.
|
|
520
|
+
const lowerCh = new Float64Column(box.lower, n).binBy(key, edges, 'min');
|
|
521
|
+
const upperCh = new Float64Column(box.upper, n).binBy(key, edges, 'max');
|
|
522
|
+
const q1Ch = new Float64Column(box.q1, n).binBy(key, edges, 'min');
|
|
523
|
+
const q3Ch = new Float64Column(box.q3, n).binBy(key, edges, 'max');
|
|
524
|
+
const medianCh = new Float64Column(box.median, n).binBy(key, edges, 'minMaxFirstLast').first;
|
|
525
|
+
const x = new Float64Array(W);
|
|
526
|
+
const xEnd = new Float64Array(W);
|
|
527
|
+
for (let b = 0; b < W; b += 1) {
|
|
528
|
+
x[b] = edges[b]; // the aggregate box's slot IS its pixel column
|
|
529
|
+
xEnd[b] = edges[b + 1];
|
|
530
|
+
}
|
|
531
|
+
return {
|
|
532
|
+
x,
|
|
533
|
+
xEnd,
|
|
534
|
+
lower: lowerCh,
|
|
535
|
+
q1: q1Ch,
|
|
536
|
+
median: medianCh,
|
|
537
|
+
q3: q3Ch,
|
|
538
|
+
upper: upperCh,
|
|
539
|
+
length: W,
|
|
540
|
+
// Carry the flags through so a range-only / no-median box stays that way;
|
|
541
|
+
// omit (not `undefined`) when unset, per `exactOptionalPropertyTypes`.
|
|
542
|
+
...(box.hasBox !== undefined ? { hasBox: box.hasBox } : {}),
|
|
543
|
+
...(box.hasMedian !== undefined ? { hasMedian: box.hasMedian } : {}),
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Decimate a {@link BarSeries} to **one envelope rect per device-pixel column**
|
|
548
|
+
* ([PND-MARKDEC]) — the interval-mark analog of {@link decimateBand}, for the
|
|
549
|
+
* "column chart, dense in x" case (SciChart-suite finding 4: a bar column has no
|
|
550
|
+
* decimation path, so it falls off where line/area/candle don't). Once each bar's
|
|
551
|
+
* slot is narrower than ~1px (the visible bars exceed `k ×` the device-pixel
|
|
552
|
+
* column count), the individual rects overplot into a solid silhouette; this
|
|
553
|
+
* replaces them with the exact painted union: per column, `lo = min(minValue,
|
|
554
|
+
* baseline)` and `hi = max(maxValue, baseline)` — the bars' value range widened
|
|
555
|
+
* to include the baseline every bar reaches. Drawing one rect `[begin, end] ×
|
|
556
|
+
* [lo, hi]` per column reproduces that silhouette from O(W) rects instead of
|
|
557
|
+
* O(visible).
|
|
558
|
+
*
|
|
559
|
+
* Gates on the **visible** bar count (`visibleCount`, a bar's width is its slot —
|
|
560
|
+
* decimating a handful of deep-zoomed bars would render 1px slivers, the same
|
|
561
|
+
* trap the candle / box paths gate against). Returns `null` when decimation
|
|
562
|
+
* doesn't apply (below the visible-density threshold, domainless / non-invertible
|
|
563
|
+
* scale, no canvas width) — the caller then draws every visible bar. Bars are
|
|
564
|
+
* binned by their **`begin`** key (at this density `begin`/`end` sit in the same
|
|
565
|
+
* column); the envelope ignores per-bar `gapPx` and tiles the column (a few-px
|
|
566
|
+
* gap is invisible at <1px bars anyway — the standard decimation tradeoff).
|
|
567
|
+
* `baseline` is the resolved bar baseline in **value** units (from
|
|
568
|
+
* `resolveBarBaseline`), so the union is honest about the zero line.
|
|
569
|
+
*/
|
|
570
|
+
export function decimateBars(cs, xScale, ctx, baseline, k = 2, visibleCount = cs.length) {
|
|
571
|
+
if (!shouldDecimateCount(visibleCount, ctx, k))
|
|
572
|
+
return null;
|
|
573
|
+
const dom = scaleDomain(xScale);
|
|
574
|
+
if (dom === null || dom[1] <= dom[0])
|
|
575
|
+
return null;
|
|
576
|
+
const invert = scaleInvert(xScale);
|
|
577
|
+
const plotWidthCss = scaleRangeWidth(xScale);
|
|
578
|
+
if (invert === null || plotWidthCss === null)
|
|
579
|
+
return null;
|
|
580
|
+
const W = deviceBucketCount(ctx);
|
|
581
|
+
const edges = pixelEdges(invert, plotWidthCss, W);
|
|
582
|
+
// Per-column min/max of the bar values, binned by the bar's begin key — two O(n)
|
|
583
|
+
// walks, like decimateBand. An empty column reduces to NaN on both.
|
|
584
|
+
const col = new Float64Column(cs.y, cs.length);
|
|
585
|
+
const vMin = col.binBy(cs.begin, edges, 'min');
|
|
586
|
+
const vMax = col.binBy(cs.begin, edges, 'max');
|
|
587
|
+
const begin = new Float64Array(W);
|
|
588
|
+
const end = new Float64Array(W);
|
|
589
|
+
const lo = new Float64Array(W);
|
|
590
|
+
const hi = new Float64Array(W);
|
|
591
|
+
for (let b = 0; b < W; b += 1) {
|
|
592
|
+
begin[b] = edges[b];
|
|
593
|
+
end[b] = edges[b + 1];
|
|
594
|
+
const mn = vMin[b];
|
|
595
|
+
if (Number.isFinite(mn)) {
|
|
596
|
+
// Widen to the baseline so the rect spans exactly the painted union (each
|
|
597
|
+
// bar reaches the baseline). One-signed data ⇒ one edge is the baseline ⇒
|
|
598
|
+
// the rect is the tallest bar, unchanged.
|
|
599
|
+
lo[b] = Math.min(mn, baseline);
|
|
600
|
+
hi[b] = Math.max(vMax[b], baseline);
|
|
601
|
+
}
|
|
602
|
+
else {
|
|
603
|
+
lo[b] = NaN; // empty column — drawBars skips it
|
|
604
|
+
hi[b] = NaN;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return { begin, end, lo, hi, length: W };
|
|
608
|
+
}
|
|
359
609
|
//# sourceMappingURL=decimate.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Canvas-rendered, streaming-first time-series charts with a
|
|
5
5
|
* react-timeseries-charts-style declarative layout. The architecture (hard
|
|
6
|
-
* layers: adapter → typed-array store → decimator →
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* layers: adapter → typed-array store → decimator → canvas renderer → React
|
|
7
|
+
* shell) is documented in the charts RFC at `docs/rfcs/charts.md`; the
|
|
8
|
+
* milestone plan lives in `PLAN.md`. (A Path2D path cache was explored and
|
|
9
|
+
* deferred — it doesn't help the pan case, which re-decimates every frame; see
|
|
10
|
+
* the [PND-DECIM] floor decision.)
|
|
9
11
|
*
|
|
10
12
|
* **M1 — rendering spine.** The layout shell + the first draw layer:
|
|
11
13
|
* `<ChartContainer>` (time axis) → `<ChartRow>` (y-axis + canvas) →
|
|
@@ -75,4 +77,5 @@ export type { ChartThemeOverrides, VarReader } from './css-theme.js';
|
|
|
75
77
|
export { useChartTheme } from './useChartTheme.js';
|
|
76
78
|
export type { UseChartThemeOptions } from './useChartTheme.js';
|
|
77
79
|
export type { CursorMode, TrackerInfo, TrackerSample, SelectInfo, } from './context.js';
|
|
80
|
+
export type { DrawStatsFrame, LayerDrawInfo } from './context.js';
|
|
78
81
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Canvas-rendered, streaming-first time-series charts with a
|
|
5
5
|
* react-timeseries-charts-style declarative layout. The architecture (hard
|
|
6
|
-
* layers: adapter → typed-array store → decimator →
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* layers: adapter → typed-array store → decimator → canvas renderer → React
|
|
7
|
+
* shell) is documented in the charts RFC at `docs/rfcs/charts.md`; the
|
|
8
|
+
* milestone plan lives in `PLAN.md`. (A Path2D path cache was explored and
|
|
9
|
+
* deferred — it doesn't help the pan case, which re-decimates every frame; see
|
|
10
|
+
* the [PND-DECIM] floor decision.)
|
|
9
11
|
*
|
|
10
12
|
* **M1 — rendering spine.** The layout shell + the first draw layer:
|
|
11
13
|
* `<ChartContainer>` (time axis) → `<ChartRow>` (y-axis + canvas) →
|