@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.
- package/CHANGELOG.md +225 -1
- package/dist/AreaChart.d.ts +18 -1
- package/dist/AreaChart.js +23 -2
- package/dist/BandChart.d.ts +21 -2
- package/dist/BandChart.js +68 -9
- package/dist/BarChart.d.ts +12 -1
- package/dist/BarChart.js +34 -1
- package/dist/BoxPlot.d.ts +29 -1
- package/dist/BoxPlot.js +61 -3
- package/dist/Candlestick.d.ts +21 -1
- package/dist/Candlestick.js +42 -7
- package/dist/ChartContainer.d.ts +23 -13
- package/dist/ChartContainer.js +86 -32
- package/dist/ChartRow.js +22 -3
- package/dist/Layers.js +37 -14
- package/dist/Legend.d.ts +62 -0
- package/dist/Legend.js +169 -0
- package/dist/LineChart.d.ts +20 -1
- package/dist/LineChart.js +23 -2
- package/dist/ScatterChart.d.ts +8 -1
- package/dist/ScatterChart.js +24 -1
- package/dist/XAxis.js +9 -2
- package/dist/YAxis.d.ts +9 -1
- package/dist/YAxis.js +27 -6
- package/dist/annotations.d.ts +21 -3
- package/dist/annotations.js +36 -15
- package/dist/area.d.ts +2 -1
- package/dist/area.js +29 -4
- package/dist/band.d.ts +2 -1
- package/dist/band.js +18 -1
- package/dist/bars.js +8 -1
- package/dist/box.d.ts +15 -1
- package/dist/box.js +71 -2
- package/dist/context.d.ts +51 -4
- package/dist/culling.d.ts +165 -0
- package/dist/culling.js +286 -0
- package/dist/data.d.ts +3 -1
- package/dist/decimate.d.ts +231 -0
- package/dist/decimate.js +478 -0
- package/dist/format.d.ts +20 -11
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/line.d.ts +2 -1
- package/dist/line.js +38 -3
- package/dist/ohlc.d.ts +2 -1
- package/dist/ohlc.js +23 -2
- package/dist/scatter.js +42 -7
- package/dist/swatch.d.ts +104 -0
- package/dist/swatch.js +96 -0
- package/dist/theme.d.ts +27 -0
- package/dist/theme.js +12 -0
- package/dist/useChartLegend.d.ts +106 -0
- package/dist/useChartLegend.js +122 -0
- package/dist/yticks.d.ts +20 -0
- package/dist/yticks.js +28 -0
- 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
|