@pond-ts/charts 0.48.1 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +225 -1
  2. package/dist/AreaChart.d.ts +18 -1
  3. package/dist/AreaChart.js +23 -2
  4. package/dist/BandChart.d.ts +21 -2
  5. package/dist/BandChart.js +68 -9
  6. package/dist/BarChart.d.ts +12 -1
  7. package/dist/BarChart.js +34 -1
  8. package/dist/BoxPlot.d.ts +29 -1
  9. package/dist/BoxPlot.js +61 -3
  10. package/dist/Candlestick.d.ts +21 -1
  11. package/dist/Candlestick.js +42 -7
  12. package/dist/ChartContainer.d.ts +23 -13
  13. package/dist/ChartContainer.js +86 -32
  14. package/dist/ChartRow.js +22 -3
  15. package/dist/Layers.js +37 -14
  16. package/dist/Legend.d.ts +62 -0
  17. package/dist/Legend.js +169 -0
  18. package/dist/LineChart.d.ts +20 -1
  19. package/dist/LineChart.js +23 -2
  20. package/dist/ScatterChart.d.ts +8 -1
  21. package/dist/ScatterChart.js +24 -1
  22. package/dist/XAxis.js +9 -2
  23. package/dist/YAxis.d.ts +9 -1
  24. package/dist/YAxis.js +27 -6
  25. package/dist/annotations.d.ts +21 -3
  26. package/dist/annotations.js +36 -15
  27. package/dist/area.d.ts +2 -1
  28. package/dist/area.js +29 -4
  29. package/dist/band.d.ts +2 -1
  30. package/dist/band.js +18 -1
  31. package/dist/bars.js +8 -1
  32. package/dist/box.d.ts +15 -1
  33. package/dist/box.js +71 -2
  34. package/dist/context.d.ts +51 -4
  35. package/dist/culling.d.ts +165 -0
  36. package/dist/culling.js +286 -0
  37. package/dist/data.d.ts +3 -1
  38. package/dist/decimate.d.ts +231 -0
  39. package/dist/decimate.js +478 -0
  40. package/dist/format.d.ts +20 -11
  41. package/dist/index.d.ts +6 -0
  42. package/dist/index.js +6 -0
  43. package/dist/line.d.ts +2 -1
  44. package/dist/line.js +38 -3
  45. package/dist/ohlc.d.ts +2 -1
  46. package/dist/ohlc.js +23 -2
  47. package/dist/scatter.js +42 -7
  48. package/dist/swatch.d.ts +104 -0
  49. package/dist/swatch.js +96 -0
  50. package/dist/theme.d.ts +27 -0
  51. package/dist/theme.js +12 -0
  52. package/dist/useChartLegend.d.ts +106 -0
  53. package/dist/useChartLegend.js +122 -0
  54. package/dist/yticks.d.ts +20 -0
  55. package/dist/yticks.js +28 -0
  56. package/package.json +3 -3
@@ -0,0 +1,231 @@
1
+ /**
2
+ * M4 line decimation (charts decimator wave, Phase 3). Reduces an
3
+ * already-viewport-culled visible slice to a **pixel-dense** polyline that is
4
+ * **visually lossless** vs the full line at the current plot width + DPR, from
5
+ * O(devicePlotWidth) points instead of O(visible) — the win that lifts the
6
+ * *fully-visible* draw ceiling Phase 2 culling deliberately left in place (a
7
+ * dense series that fills the plot still strokes every point).
8
+ *
9
+ * **Algorithm — M4** (Jugel et al., VLDB 2014). Split the visible key range into
10
+ * one bucket per device pixel column; per column keep the **min**, **max**,
11
+ * **first**, and **last** value (the four channels of `Float64Column.binBy(…,
12
+ * 'minMaxFirstLast')` — the pond-side reducer math from PR #362/#363). Drawing
13
+ * first → min → max → last per column reproduces what the full line rasterizes
14
+ * there: the vertical extent (min→max) is the exact band of pixels the dense
15
+ * samples cover, and first/last carry the slope to the neighbouring columns. It
16
+ * is lossless to within a **sub-pixel AA seam** along the envelope edges — the
17
+ * min/max are placed at the column *centre* (their true sub-pixel x isn't carried
18
+ * by the value-only reducer), so the edge antialiases a fraction of a pixel
19
+ * differently than the full line (the e2e bounds the whole-plot difference at a
20
+ * low single-digit %; a broken M4 diffs a large area). An empty column (a gap
21
+ * with no samples) reduces to `NaN` on all four channels — the canvas
22
+ * sub-path-break sentinel — so a gap becomes a break for free.
23
+ *
24
+ * **Gaps (§2.2 gap-edge union).** A `binBy` bucket straddling a gap *edge* is
25
+ * validity-blind (min/max/first/last see only the finite samples), so it would
26
+ * silently bridge a gap `'empty'` must break and rob the dashed/step/fade
27
+ * connectors of exact edge values. {@link gapKeyEdges} folds every ≥1-column
28
+ * interior gap's boundaries into the bucket-edge list, so each gap reduces to its
29
+ * own empty (NaN) bucket and the bordering buckets carry the exact pre/post-gap
30
+ * values — the decimated series then feeds the *unchanged* gap-mode machinery in
31
+ * `drawLine` (`'none'` bridges the breaks, dashed/step/fade draw their inferred
32
+ * connectors from `collectGapEdges`).
33
+ *
34
+ * **Reads the frame geometry off the canvas + scale**, not the layer signature:
35
+ * the bucket count `W` is the backing buffer width `ctx.canvas.width` (already
36
+ * `plotWidthCss × DPR` — see `Canvas`), so the grid is at **device-pixel**
37
+ * resolution — twice the columns at 2× DPR, which keeps extremes from
38
+ * flat-topping (decimator assessment §2.6). The bucket **edges** are the scale's
39
+ * CSS-pixel range (`xScale.range()`) inverted back to key space at those `W`
40
+ * positions (see {@link pixelEdges}) — so each bucket is exactly one column on
41
+ * **any** scale, including a non-affine `TradingTimeScale`.
42
+ *
43
+ * The output is a plain {@link ChartSeries} in **key space**, so it feeds
44
+ * straight back into the existing `drawLine` path (which maps x through the same
45
+ * `xScale` and breaks its subpath on `NaN`) — decimation is a pre-pass that
46
+ * shrinks the point count, not a second renderer.
47
+ */
48
+ import type { ChartSeries, BandSeries, OhlcSeries, BoxSeries } from './data.js';
49
+ import type { Scale } from './line.js';
50
+ /**
51
+ * A line layer's M4-decimation control (`<LineChart decimate>`). **Default
52
+ * `true`** — auto-decimate once the visible slice exceeds `2 ×` the device-pixel
53
+ * column count. `false` disables it (always draw every visible point).
54
+ * `{ threshold }` overrides the samples-per-pixel factor `k` (higher ⇒
55
+ * decimate later). Only the honest default draw path decimates (see
56
+ * `drawLine`); a decimated line is visually identical, so this is a perf knob,
57
+ * not a rendering-style one.
58
+ */
59
+ export type DecimateOption = boolean | {
60
+ readonly threshold?: number;
61
+ };
62
+ /** The device-pixel bucket count for `ctx` — the backing buffer width, i.e.
63
+ * `plotWidthCss × DPR` (so buckets land at device-pixel resolution). Falls back
64
+ * to `0` when there is no sized canvas (a headless test ctx), which the caller
65
+ * reads as "can't decimate". */
66
+ export declare function deviceBucketCount(ctx: CanvasRenderingContext2D): number;
67
+ /**
68
+ * Whether decimating a series of `length` samples would pay off at the current
69
+ * frame width: `true` once `length` exceeds `k ×` the device-pixel column count
70
+ * (default `k = 2` — below ~2 samples per pixel the min/max buckets barely shrink
71
+ * the point set, so plain drawing is cheaper than the bin walk). Returns `false`
72
+ * when the canvas has no measurable width (a test ctx) so those draws stay
73
+ * full-resolution and byte-identical. Shared by the line ({@link shouldDecimate})
74
+ * and band decimators.
75
+ */
76
+ export declare function shouldDecimateCount(length: number, ctx: CanvasRenderingContext2D, k?: number): boolean;
77
+ /** {@link shouldDecimateCount} for a {@link ChartSeries} (the line / area case). */
78
+ export declare function shouldDecimate(cs: ChartSeries, ctx: CanvasRenderingContext2D, k?: number): boolean;
79
+ /**
80
+ * The `W + 1` pixel-column **edges** in key space — built by **inverting uniform
81
+ * pixel positions** through the scale (`edges[b] = invert(b/W · plotWidthCss)`),
82
+ * NOT by partitioning the key domain uniformly. The distinction is load-bearing:
83
+ * "one bucket per pixel column" means uniform in *pixel* space, which equals a
84
+ * uniform *key* partition only when the scale is **affine** (`scaleLinear` /
85
+ * `scaleTime`). A `TradingTimeScale` compresses closed-market gaps — its key→px
86
+ * map is piecewise-linear — so inverting pixel positions is what keeps each
87
+ * bucket exactly one column wide there too (else the min/max envelope would thin
88
+ * within a session). `invert` is monotonic, so the edges ascend; the last is the
89
+ * domain max (`invert(plotWidthCss)`), inclusive in `binBy`.
90
+ *
91
+ * `plotWidthCss` is the scale's CSS-pixel range width (`xScale.range()` max);
92
+ * `W` counts *device* columns (`plotWidthCss × DPR`), so the `W` inverted
93
+ * positions land at device-pixel resolution across the CSS range.
94
+ */
95
+ export declare function pixelEdges(invert: (px: number) => number, plotWidthCss: number, W: number): Float64Array;
96
+ /**
97
+ * Key-space bucket boundaries that isolate each **interior gap** — a `NaN` run in
98
+ * `y` with a finite sample on both sides — that spans at least one pixel column
99
+ * (`minSpan`). This is the §2.2 gap-edge union: without it a `binBy` bucket
100
+ * straddling a gap edge is *validity-blind* (min/max/first/last see only the
101
+ * finite samples), so it silently bridges a gap `'empty'` mode must break and the
102
+ * `dashed`/`step`/`fade` connectors lose their exact edge values. For a gap
103
+ * bounded by finite `x[a]` (last before) and `x[c]` (first after), with the first
104
+ * `NaN` at `x[a+1]`, two edges are emitted:
105
+ *
106
+ * - `x[a+1]` — so `x[a]` stays the **last** finite sample of the prior bucket
107
+ * (its `last` channel = the exact pre-gap edge value); and
108
+ * - `x[c]` — so `x[c]` **starts** the next bucket (its `first` = the exact
109
+ * post-gap edge value).
110
+ *
111
+ * The `[x[a+1], x[c])` bucket between them is then all-`NaN` → an empty bucket →
112
+ * the `NaN` break. Only gaps at least one pixel column wide (`x[c] − x[a] ≥
113
+ * minSpan`) are emitted — a sub-pixel dropout is invisible and left to the
114
+ * plain empty-bucket convention, which also **bounds the edge count** (disjoint
115
+ * gaps each ≥ `minSpan` ⇒ ≤ `W` of them ⇒ ≤ `3W` total edges). Emitted ascending
116
+ * (`x` is). Leading / trailing `NaN` runs are skipped (no bridge to preserve —
117
+ * the first/last live bucket handles the end).
118
+ *
119
+ * `minSpan` is the caller's mean per-column key width (`domainSpan / W`) — exact
120
+ * on an affine scale, an **approximation** on a `TradingTimeScale` (where a
121
+ * column's key width varies across compressed gaps). A misfire there is benign:
122
+ * a real ≥1px gap it skips still breaks in its fully-empty interior columns; only
123
+ * the ~1px gap *edges* bridge (and session-break charts gate decimation off
124
+ * entirely). A per-gap pixel-width measure is the follow-up if a consumer hits it.
125
+ */
126
+ export declare function gapKeyEdges(cs: ChartSeries, minSpan: number): number[];
127
+ /**
128
+ * Merge the pixel-column `edges` with the interior-gap boundaries `gaps` (both
129
+ * ascending) into one ascending, duplicate-free edge list, keeping only gap
130
+ * boundaries strictly inside the domain `(lo, hi)` so the pixel span isn't
131
+ * extended. Returns the **same** `edges` array (identity — no allocation) when
132
+ * `gaps` is empty, so the gapless hot path is untouched.
133
+ */
134
+ export declare function mergeGapEdges(edges: Float64Array, gaps: number[], lo: number, hi: number): Float64Array;
135
+ /**
136
+ * Decimate `cs` (a viewport-culled visible slice, ascending `x`) to an M4
137
+ * polyline for `ctx`'s current width + DPR. Returns the **same object** when
138
+ * decimation doesn't apply — the scale has no domain (a test stub), the canvas
139
+ * has no width, or the series is already sparse enough ({@link shouldDecimate})
140
+ * — so those frames draw full-resolution unchanged.
141
+ *
142
+ * Otherwise returns a fresh {@link ChartSeries} of up to `4·W` points: per
143
+ * non-empty column, four points at `[first, min, max, last]` placed at the
144
+ * column's left / centre / centre / right key positions (sub-pixel within the
145
+ * 1px column), and a single `NaN` break per empty column. The classic M4 render
146
+ * — the min→max vertical is the exact pixel band the dense samples cover, and
147
+ * first/last carry the inter-column slope.
148
+ *
149
+ * `boundaries` are trading-axis session-break instants: their keys are unioned
150
+ * into the bucket edges so no bucket straddles a break (which would merge two
151
+ * sessions' extremes). The caller's `sessionRuns` then splits the returned
152
+ * series into per-session subpaths at exactly those instants.
153
+ */
154
+ export declare function decimateM4(cs: ChartSeries, xScale: Scale, ctx: CanvasRenderingContext2D, k?: number, boundaries?: readonly number[]): ChartSeries;
155
+ /**
156
+ * Assemble the M4 polyline {@link ChartSeries} from the four binned channels.
157
+ * Split out (pure, no canvas / pond deps) so the point emission is unit-tested
158
+ * directly. Per column `b`: an empty bucket (`first[b]` non-finite ⇒ all four
159
+ * are) emits one `NaN` break; a live bucket emits
160
+ * `(left, first) (mid, min) (mid, max) (right, last)`.
161
+ *
162
+ * `breakAt` holds bucket-edge keys (session-break instants, already unioned into
163
+ * `edges`) at which the line must **break** rather than connect: a bucket whose
164
+ * left edge is in `breakAt` emits a `NaN` **before** its points. This makes a
165
+ * session split explicit in the geometry — clean regardless of whether the break
166
+ * fell exactly on a pixel edge (where otherwise the closing bucket's `last` and
167
+ * the opening bucket's `first` would sit at the same x and connect with a
168
+ * spurious vertical stub).
169
+ */
170
+ export declare function m4Polyline(edges: Float64Array, mn: Float64Array, mx: Float64Array, first: Float64Array, last: Float64Array, W: number, breakAt?: ReadonlySet<number>): ChartSeries;
171
+ /**
172
+ * Decimate a {@link BandSeries} (a filled variance envelope) to one sample per
173
+ * device-pixel column: per column the **min of `lower`** and the **max of
174
+ * `upper`** — the *widest* envelope the dense samples span, so a decimated band
175
+ * covers exactly the pixels the full band's silhouette would (decimator
176
+ * assessment §2.5: paired min-lower / max-upper, so the envelope can never
177
+ * invert — `max(upper) ≥ min(lower)` for any valid band). Returns the **same
178
+ * object** when decimation doesn't apply (sparse band, domainless / non-invertible
179
+ * scale, no canvas width).
180
+ *
181
+ * Uses the same pixel-aligned edges as the line decimator ({@link pixelEdges} —
182
+ * correct on non-affine scales too), binning `lower` with `'min'` and `upper`
183
+ * with `'max'`. An empty column (no samples) reduces to `NaN` on both edges — the
184
+ * `drawBand` `.defined` break. Unlike the line path this needs **no gap-edge
185
+ * union**: a band has no inferred-connector modes (`drawBand` always breaks the
186
+ * fill at a gap, never bridges), so a sub-pixel gap edge folding into a boundary
187
+ * bucket is invisible — there is no connector to misplace. Assumes `lower` /
188
+ * `upper` are finite **together** per sample (the paired-percentile shape bands
189
+ * are built from); a column where only one edge has finite samples would bin a
190
+ * band segment that no single sample carried.
191
+ */
192
+ export declare function decimateBand(band: BandSeries, xScale: Scale, ctx: CanvasRenderingContext2D, k?: number): BandSeries;
193
+ /**
194
+ * Decimate an {@link OhlcSeries} to one **aggregate candle per device-pixel
195
+ * column** — `open = first`, `high = max`, `low = min`, `close = last` over the
196
+ * candles that fall in the column. This is exactly a candle re-bucketed to a
197
+ * **coarser timeframe** (the pixel-column's time range): it is never *wrong* —
198
+ * it is the true OHLC of that span — so a dense chart that zooms out reads as
199
+ * fewer, wider aggregate candles, the trading-UI convention (decimator
200
+ * assessment §2.4). Auto-on with an opt-out; a consumer wanting fixed-timeframe
201
+ * candles pre-aggregates upstream and passes `decimate={false}`.
202
+ *
203
+ * Returns the **same object** when decimation doesn't apply (sparse series,
204
+ * domainless / non-invertible scale, no canvas width). The slot of each
205
+ * aggregate candle is its pixel column `[edges[b], edges[b+1]]`; an empty column
206
+ * (no candles) reduces to `NaN` on all channels — `drawCandles` skips it. No
207
+ * session-break union is needed: candles are independent marks (they never
208
+ * connect), and a trading-axis closed period is simply an empty column.
209
+ */
210
+ export declare function decimateOhlc(ohlc: OhlcSeries, xScale: Scale, ctx: CanvasRenderingContext2D, k?: number, visibleCount?: number): OhlcSeries;
211
+ /**
212
+ * Decimate a {@link BoxSeries} to one **aggregate box per device-pixel column** —
213
+ * the interval-mark sibling of {@link decimateOhlc}. Each channel is binned over
214
+ * the column: the whiskers widen to the column's full reach (`lower = min(lower)`,
215
+ * `upper = max(upper)`, exactly {@link decimateBand}'s envelope), the body to the
216
+ * column's **IQR envelope** (`q1 = min(q1)`, `q3 = max(q3)`), and the centre line
217
+ * to the **first** box's `median` in the column (a real median value, not an
218
+ * average — `binBy` carries no mean; it stays within the aggregate body since the
219
+ * first box's `[q1, q3]` ⊆ the envelope). So a dense per-x distribution chart
220
+ * that zooms out reads as fewer, wider boxes summarising each column's spread.
221
+ *
222
+ * Gates on the **visible** box count (a box's width is its slot — decimating a
223
+ * handful of deep-zoomed boxes would render 1px slivers, the same trap the candle
224
+ * path has). Returns the **same object** when decimation doesn't apply (below the
225
+ * visible-density threshold, domainless / non-invertible scale, no canvas width).
226
+ * The `hasBox` / `hasMedian` flags carry through, so a **range-only** box (all-NaN
227
+ * `q1`/`q3`) stays range-only (its binned body is NaN throughout). An empty column
228
+ * reduces to `NaN` on every channel — `drawBox` skips it via `isFiniteBox`.
229
+ */
230
+ export declare function decimateBox(box: BoxSeries, xScale: Scale, ctx: CanvasRenderingContext2D, k?: number, visibleCount?: number): BoxSeries;
231
+ //# sourceMappingURL=decimate.d.ts.map