@pond-ts/charts 0.48.1 → 0.49.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 +200 -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 +18 -1
- package/dist/BoxPlot.js +60 -3
- package/dist/Candlestick.d.ts +8 -1
- package/dist/Candlestick.js +40 -6
- 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 +14 -1
- package/dist/box.js +56 -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 +193 -0
- package/dist/decimate.js +359 -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.js +6 -1
- 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,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Viewport culling (charts decimator wave, Phase 2). Before a layer strokes its
|
|
3
|
+
* data it clips to the **visible** slice of its key column, so a pan/zoom repaint
|
|
4
|
+
* costs O(visible), not O(N): a 1M-point line panned to show 800px of data draws
|
|
5
|
+
* the ~1k points under the plot, not a million.
|
|
6
|
+
*
|
|
7
|
+
* This is the "slice plumbing" the RFC pipeline (store → viewport/decimator →
|
|
8
|
+
* renderer) puts *before* the M4 decimator (Phase 3): culling narrows the input
|
|
9
|
+
* to the visible window; the decimator later collapses that window to
|
|
10
|
+
* ~plot-width buckets. Culling alone is the win that hits the failing pan metric
|
|
11
|
+
* (#256: 100k line pan 120 → 8 fps), and it lands independently of any
|
|
12
|
+
* decimation semantics — it never changes *which* pixels are drawn, only how
|
|
13
|
+
* many points are walked to draw them.
|
|
14
|
+
*
|
|
15
|
+
* **The §2.3 invariant holds by construction:** culling lives on the *draw* path
|
|
16
|
+
* only. `sampleAt` / `hitTest` / `yExtent` read the full source series (they
|
|
17
|
+
* capture `cs` directly), so a hover readout or selection never shifts when the
|
|
18
|
+
* window resizes — nothing user-facing depends on the visible slice.
|
|
19
|
+
*/
|
|
20
|
+
import type { ChartSeries, BandSeries } from './data.js';
|
|
21
|
+
import type { Scale } from './line.js';
|
|
22
|
+
/**
|
|
23
|
+
* The visible x-domain of a chart scale as an ascending `[lo, hi]` pair (epoch
|
|
24
|
+
* ms on a time / trading axis, the axis value on a value axis), or `null` when
|
|
25
|
+
* the scale exposes no numeric domain.
|
|
26
|
+
*
|
|
27
|
+
* The draw contract types `xScale` as a bare `(value) => px` function, but the
|
|
28
|
+
* runtime object is always a real d3 `scaleTime` / `scaleLinear` or a
|
|
29
|
+
* `TradingTimeScale` — all three carry `.domain()`, and the domain **is** the
|
|
30
|
+
* visible range (the container sets it to the current view). Read it through a
|
|
31
|
+
* localized, documented cast rather than widening the draw signature — the same
|
|
32
|
+
* trick {@link baselinePxFromScale} uses for the y-axis floor.
|
|
33
|
+
*
|
|
34
|
+
* Returns `null` (⇒ callers skip culling, drawing the whole series) when:
|
|
35
|
+
* - the scale has no `.domain()` — a bare `(v) => v` test stub; or
|
|
36
|
+
* - the domain isn't a numeric pair — a category {@link ScaleBand}, whose domain
|
|
37
|
+
* is ordinal category strings (`+string` is `NaN`).
|
|
38
|
+
*
|
|
39
|
+
* A `scaleTime` domain is `[Date, Date]`; `+date` coerces to ms. The pair is
|
|
40
|
+
* returned ascending (sorted defensively) so the bisect bounds are well-ordered
|
|
41
|
+
* even under an unusual reversed domain.
|
|
42
|
+
*/
|
|
43
|
+
export declare function scaleDomain(xScale: Scale): [number, number] | null;
|
|
44
|
+
/**
|
|
45
|
+
* The index window `[start, end)` of a **monotonically ascending** key column
|
|
46
|
+
* `x` (logical length `length`) that covers the visible range `[lo, hi]` plus
|
|
47
|
+
* `margin` points on **each** side. Pure, O(log length) — two binary searches,
|
|
48
|
+
* no allocation.
|
|
49
|
+
*
|
|
50
|
+
* The margin points are the **entry / exit** samples: the last point left of the
|
|
51
|
+
* viewport and the first point right of it, so the line segment that *crosses*
|
|
52
|
+
* each plot edge is still drawn (drop them and the line would stop at the first
|
|
53
|
+
* in-view point, leaving a visible notch at each edge under a pan). `margin = 1`
|
|
54
|
+
* is exact for a straight (linear) segment — the crossing segment's two
|
|
55
|
+
* endpoints are both present. A smoothing `curve` (monotone) computes an
|
|
56
|
+
* interior point's tangent from a wider neighbourhood, so the *entry segment*
|
|
57
|
+
* itself can differ by a sub-pixel from the un-culled render at the very edge;
|
|
58
|
+
* the visible boundary point's own tangent stays exact (its neighbours are both
|
|
59
|
+
* in the slice). Pixel-identity across the whole edge is an M4 (Phase 3)
|
|
60
|
+
* concern, not culling's.
|
|
61
|
+
*
|
|
62
|
+
* Degenerate cases fall out of the two bounds:
|
|
63
|
+
* - **Whole series visible** — `[0, length]` (the caller then skips the slice).
|
|
64
|
+
* - **Series entirely left of the view** (`hi < x[0]`) — `[length-1, length]`,
|
|
65
|
+
* a one-point off-screen slice that strokes nothing.
|
|
66
|
+
* - **Series entirely right of the view** (`lo > x[last]`) — `[0, 1]`, likewise.
|
|
67
|
+
* - **Empty series** — `[0, 0]`.
|
|
68
|
+
*/
|
|
69
|
+
export declare function visiblePointWindow(x: Float64Array, length: number, lo: number, hi: number, margin?: number): [number, number];
|
|
70
|
+
/**
|
|
71
|
+
* A {@link ChartSeries} clipped to the visible window of `xScale` (+`margin`
|
|
72
|
+
* points each side). Returns the **same object** untouched when the whole series
|
|
73
|
+
* is in view or the scale exposes no domain (a test stub / category axis) — so
|
|
74
|
+
* the common "everything fits" frame allocates nothing and the draw stays
|
|
75
|
+
* byte-identical to the pre-culling pass. Otherwise the returned view is a
|
|
76
|
+
* zero-copy `subarray` of the source buffers (the source is immutable by
|
|
77
|
+
* contract, so aliasing is safe).
|
|
78
|
+
*
|
|
79
|
+
* **Gap-mode neutrality.** After the pixel bisect, each boundary is walked
|
|
80
|
+
* outward past any non-finite (`NaN` gap) run until the slice's first and last
|
|
81
|
+
* samples are **finite** (or the buffer end is hit). Without this, a gap wider
|
|
82
|
+
* than `margin` straddling a plot edge would drop the finite anchor sitting
|
|
83
|
+
* >`margin` points off-screen, turning an *interior* gap into a *leading /
|
|
84
|
+
* trailing* one inside the slice — which `bridgeGaps` and `collectGapEdges` both
|
|
85
|
+
* leave broken (they only bridge gaps with a finite sample on *both* sides). The
|
|
86
|
+
* `none` / `dashed` / `step` / `fade` connector that crossed the edge would then
|
|
87
|
+
* vanish (a notch under pan). Re-including the anchor keeps the boundary gap
|
|
88
|
+
* *interior*, so every mode draws exactly as it does un-culled. Cost is one
|
|
89
|
+
* `isFinite` check per side in the common (finite-boundary) case; the walk only
|
|
90
|
+
* runs for an edge-straddling gap and is bounded by that gap's width. (The
|
|
91
|
+
* default `empty` mode breaks at gaps regardless, so it is unaffected either
|
|
92
|
+
* way — this makes the guarantee hold for *all* modes.)
|
|
93
|
+
*/
|
|
94
|
+
export declare function cullChartSeries(cs: ChartSeries, xScale: Scale, margin?: number): ChartSeries;
|
|
95
|
+
/**
|
|
96
|
+
* A {@link BandSeries} clipped to the visible window of `xScale` — the paired
|
|
97
|
+
* `lower`/`upper` edges culled in lockstep with the shared `x` axis, so the
|
|
98
|
+
* envelope stays aligned. Same identity-preserving fast path and zero-copy
|
|
99
|
+
* `subarray` view as {@link cullChartSeries}.
|
|
100
|
+
*
|
|
101
|
+
* Unlike {@link cullChartSeries} this needs **no** finite-anchor boundary walk:
|
|
102
|
+
* a band has no gap-bridge mode (`drawBand` always breaks the fill at a gap, it
|
|
103
|
+
* never interpolates one), so a gap straddling a plot edge is a hole on both
|
|
104
|
+
* sides of the cut — there is no crossing fill to lose. The `margin` entry/exit
|
|
105
|
+
* sample is enough for a gap-free envelope that spans the edge.
|
|
106
|
+
*/
|
|
107
|
+
export declare function cullBandSeries(band: BandSeries, xScale: Scale, margin?: number): BandSeries;
|
|
108
|
+
/**
|
|
109
|
+
* The index range `[start, end)` of **interval marks** — each spanning
|
|
110
|
+
* `[begin[i], end[i]]` on a **monotonically ascending** `begin` axis — whose span
|
|
111
|
+
* overlaps the visible `[lo, hi]`, plus `margin` marks on each side. A mark is
|
|
112
|
+
* visible iff `end[i] >= lo && begin[i] <= hi`.
|
|
113
|
+
*
|
|
114
|
+
* - **Right:** `begin[i] <= hi` ⇒ everything below `upperBound(begin, hi)`; a
|
|
115
|
+
* mark starting past the right edge is off-screen. Exact — no bisect on `end`
|
|
116
|
+
* needed.
|
|
117
|
+
* - **Left:** a mark with `begin[i] < lo` is still visible if its span reaches
|
|
118
|
+
* `lo` (`end[i] >= lo`) — a wide bar crossing the left edge. `begin` bisects
|
|
119
|
+
* the first in-range mark; from there the scan walks back while the previous
|
|
120
|
+
* mark's `end` still reaches `lo`. For sorted non-overlapping marks (the bar /
|
|
121
|
+
* candle / box contract) `end` is ascending, so the walk stops at the first
|
|
122
|
+
* mark clear of the edge — typically one step.
|
|
123
|
+
*
|
|
124
|
+
* Pure, O(log length + crossing marks). `margin` (default 1) pads each side for
|
|
125
|
+
* a mark whose drawn rect is nudged by `gapPx` / `minWidth` / a pixel `offsetPx`
|
|
126
|
+
* the data-space window can't see.
|
|
127
|
+
*/
|
|
128
|
+
export declare function visibleSpanWindow(begin: Float64Array, end: Float64Array, length: number, lo: number, hi: number, margin?: number): [number, number];
|
|
129
|
+
/**
|
|
130
|
+
* The visible `[start, end)` index range of a **point** layer (scatter) against
|
|
131
|
+
* `xScale` — a thin wrapper over {@link visiblePointWindow} that reads the scale's
|
|
132
|
+
* domain. Returns the **full** range `[0, length]` when the scale exposes no
|
|
133
|
+
* numeric domain (a bare test stub / category axis) or the series is empty, so a
|
|
134
|
+
* caller loops over everything and the draw is unchanged there.
|
|
135
|
+
*
|
|
136
|
+
* **Radius-aware widening (`padPx`).** The `margin` is in *index* space, but a
|
|
137
|
+
* point mark's **disc** has a pixel radius independent of sample spacing — so a
|
|
138
|
+
* dense scatter of fat marks can put an edge bubble's *centre* several samples
|
|
139
|
+
* off-screen while its disc still overlaps the plot edge, which a bare index
|
|
140
|
+
* margin would drop (a subtle flicker under pan — the sharp edge #499 flagged as
|
|
141
|
+
* a follow-up). Passing `padPx` widens the data window by that many **pixels** on
|
|
142
|
+
* each side — converted px→data through `xScale.invert` — before the bisect, so
|
|
143
|
+
* every mark whose disc can paint into the plot is kept. Scatter passes its max
|
|
144
|
+
* drawn radius (plus any pixel offset); interval marks ({@link visibleSpanRange})
|
|
145
|
+
* don't need it — their width *is* their x-span.
|
|
146
|
+
*
|
|
147
|
+
* The pad is skipped (the plain domain window still applies) when `padPx <= 0` or
|
|
148
|
+
* the scale carries no `invert` (a real domain-bearing runtime scale always has
|
|
149
|
+
* one; only a partial stub lacks it, and it degrades to the index window — a
|
|
150
|
+
* slightly tighter cull, never a dropped mark, since over-padding only *adds*
|
|
151
|
+
* marks). `padPx` converts as `|invert(padPx) − invert(0)|`, the data span of
|
|
152
|
+
* `padPx` pixels: exact for the linear `scaleTime`/`scaleLinear` regardless of
|
|
153
|
+
* range offset, a local estimate for a non-linear axis, and the `Math.abs` keeps
|
|
154
|
+
* it a *widening* even under a reversed scale.
|
|
155
|
+
*/
|
|
156
|
+
export declare function visiblePointRange(x: Float64Array, length: number, xScale: Scale, padPx?: number, margin?: number): [number, number];
|
|
157
|
+
/**
|
|
158
|
+
* The visible `[start, end)` index range of an **interval** layer (bars,
|
|
159
|
+
* candles, boxes) against `xScale` — a thin wrapper over
|
|
160
|
+
* {@link visibleSpanWindow} that reads the scale's domain. Returns the **full**
|
|
161
|
+
* range `[0, length]` when the scale exposes no numeric domain or the series is
|
|
162
|
+
* empty (the draw is unchanged there — a bare stub / category axis draws all).
|
|
163
|
+
*/
|
|
164
|
+
export declare function visibleSpanRange(begin: Float64Array, end: Float64Array, length: number, xScale: Scale, margin?: number): [number, number];
|
|
165
|
+
//# sourceMappingURL=culling.d.ts.map
|
package/dist/culling.js
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Viewport culling (charts decimator wave, Phase 2). Before a layer strokes its
|
|
3
|
+
* data it clips to the **visible** slice of its key column, so a pan/zoom repaint
|
|
4
|
+
* costs O(visible), not O(N): a 1M-point line panned to show 800px of data draws
|
|
5
|
+
* the ~1k points under the plot, not a million.
|
|
6
|
+
*
|
|
7
|
+
* This is the "slice plumbing" the RFC pipeline (store → viewport/decimator →
|
|
8
|
+
* renderer) puts *before* the M4 decimator (Phase 3): culling narrows the input
|
|
9
|
+
* to the visible window; the decimator later collapses that window to
|
|
10
|
+
* ~plot-width buckets. Culling alone is the win that hits the failing pan metric
|
|
11
|
+
* (#256: 100k line pan 120 → 8 fps), and it lands independently of any
|
|
12
|
+
* decimation semantics — it never changes *which* pixels are drawn, only how
|
|
13
|
+
* many points are walked to draw them.
|
|
14
|
+
*
|
|
15
|
+
* **The §2.3 invariant holds by construction:** culling lives on the *draw* path
|
|
16
|
+
* only. `sampleAt` / `hitTest` / `yExtent` read the full source series (they
|
|
17
|
+
* capture `cs` directly), so a hover readout or selection never shifts when the
|
|
18
|
+
* window resizes — nothing user-facing depends on the visible slice.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* The visible x-domain of a chart scale as an ascending `[lo, hi]` pair (epoch
|
|
22
|
+
* ms on a time / trading axis, the axis value on a value axis), or `null` when
|
|
23
|
+
* the scale exposes no numeric domain.
|
|
24
|
+
*
|
|
25
|
+
* The draw contract types `xScale` as a bare `(value) => px` function, but the
|
|
26
|
+
* runtime object is always a real d3 `scaleTime` / `scaleLinear` or a
|
|
27
|
+
* `TradingTimeScale` — all three carry `.domain()`, and the domain **is** the
|
|
28
|
+
* visible range (the container sets it to the current view). Read it through a
|
|
29
|
+
* localized, documented cast rather than widening the draw signature — the same
|
|
30
|
+
* trick {@link baselinePxFromScale} uses for the y-axis floor.
|
|
31
|
+
*
|
|
32
|
+
* Returns `null` (⇒ callers skip culling, drawing the whole series) when:
|
|
33
|
+
* - the scale has no `.domain()` — a bare `(v) => v` test stub; or
|
|
34
|
+
* - the domain isn't a numeric pair — a category {@link ScaleBand}, whose domain
|
|
35
|
+
* is ordinal category strings (`+string` is `NaN`).
|
|
36
|
+
*
|
|
37
|
+
* A `scaleTime` domain is `[Date, Date]`; `+date` coerces to ms. The pair is
|
|
38
|
+
* returned ascending (sorted defensively) so the bisect bounds are well-ordered
|
|
39
|
+
* even under an unusual reversed domain.
|
|
40
|
+
*/
|
|
41
|
+
export function scaleDomain(xScale) {
|
|
42
|
+
const d = xScale.domain?.();
|
|
43
|
+
if (d === undefined || d.length < 2)
|
|
44
|
+
return null;
|
|
45
|
+
const lo = +d[0];
|
|
46
|
+
const hi = +d[d.length - 1];
|
|
47
|
+
if (!Number.isFinite(lo) || !Number.isFinite(hi))
|
|
48
|
+
return null;
|
|
49
|
+
return lo <= hi ? [lo, hi] : [hi, lo];
|
|
50
|
+
}
|
|
51
|
+
/** First index `i` in `x[0..n)` with `x[i] >= v` (`n` if none) — lower bound. */
|
|
52
|
+
function lowerBound(x, n, v) {
|
|
53
|
+
let lo = 0;
|
|
54
|
+
let hi = n;
|
|
55
|
+
while (lo < hi) {
|
|
56
|
+
const mid = (lo + hi) >>> 1;
|
|
57
|
+
if (x[mid] < v)
|
|
58
|
+
lo = mid + 1;
|
|
59
|
+
else
|
|
60
|
+
hi = mid;
|
|
61
|
+
}
|
|
62
|
+
return lo;
|
|
63
|
+
}
|
|
64
|
+
/** First index `i` in `x[0..n)` with `x[i] > v` (`n` if none) — upper bound. */
|
|
65
|
+
function upperBound(x, n, v) {
|
|
66
|
+
let lo = 0;
|
|
67
|
+
let hi = n;
|
|
68
|
+
while (lo < hi) {
|
|
69
|
+
const mid = (lo + hi) >>> 1;
|
|
70
|
+
if (x[mid] <= v)
|
|
71
|
+
lo = mid + 1;
|
|
72
|
+
else
|
|
73
|
+
hi = mid;
|
|
74
|
+
}
|
|
75
|
+
return lo;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The index window `[start, end)` of a **monotonically ascending** key column
|
|
79
|
+
* `x` (logical length `length`) that covers the visible range `[lo, hi]` plus
|
|
80
|
+
* `margin` points on **each** side. Pure, O(log length) — two binary searches,
|
|
81
|
+
* no allocation.
|
|
82
|
+
*
|
|
83
|
+
* The margin points are the **entry / exit** samples: the last point left of the
|
|
84
|
+
* viewport and the first point right of it, so the line segment that *crosses*
|
|
85
|
+
* each plot edge is still drawn (drop them and the line would stop at the first
|
|
86
|
+
* in-view point, leaving a visible notch at each edge under a pan). `margin = 1`
|
|
87
|
+
* is exact for a straight (linear) segment — the crossing segment's two
|
|
88
|
+
* endpoints are both present. A smoothing `curve` (monotone) computes an
|
|
89
|
+
* interior point's tangent from a wider neighbourhood, so the *entry segment*
|
|
90
|
+
* itself can differ by a sub-pixel from the un-culled render at the very edge;
|
|
91
|
+
* the visible boundary point's own tangent stays exact (its neighbours are both
|
|
92
|
+
* in the slice). Pixel-identity across the whole edge is an M4 (Phase 3)
|
|
93
|
+
* concern, not culling's.
|
|
94
|
+
*
|
|
95
|
+
* Degenerate cases fall out of the two bounds:
|
|
96
|
+
* - **Whole series visible** — `[0, length]` (the caller then skips the slice).
|
|
97
|
+
* - **Series entirely left of the view** (`hi < x[0]`) — `[length-1, length]`,
|
|
98
|
+
* a one-point off-screen slice that strokes nothing.
|
|
99
|
+
* - **Series entirely right of the view** (`lo > x[last]`) — `[0, 1]`, likewise.
|
|
100
|
+
* - **Empty series** — `[0, 0]`.
|
|
101
|
+
*/
|
|
102
|
+
export function visiblePointWindow(x, length, lo, hi, margin = 1) {
|
|
103
|
+
if (length === 0)
|
|
104
|
+
return [0, 0];
|
|
105
|
+
const left = lowerBound(x, length, lo); // first index with x[i] >= lo
|
|
106
|
+
const right = upperBound(x, length, hi); // first index with x[i] > hi
|
|
107
|
+
const start = Math.max(0, left - margin);
|
|
108
|
+
const end = Math.min(length, right + margin);
|
|
109
|
+
return [start, end];
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* A {@link ChartSeries} clipped to the visible window of `xScale` (+`margin`
|
|
113
|
+
* points each side). Returns the **same object** untouched when the whole series
|
|
114
|
+
* is in view or the scale exposes no domain (a test stub / category axis) — so
|
|
115
|
+
* the common "everything fits" frame allocates nothing and the draw stays
|
|
116
|
+
* byte-identical to the pre-culling pass. Otherwise the returned view is a
|
|
117
|
+
* zero-copy `subarray` of the source buffers (the source is immutable by
|
|
118
|
+
* contract, so aliasing is safe).
|
|
119
|
+
*
|
|
120
|
+
* **Gap-mode neutrality.** After the pixel bisect, each boundary is walked
|
|
121
|
+
* outward past any non-finite (`NaN` gap) run until the slice's first and last
|
|
122
|
+
* samples are **finite** (or the buffer end is hit). Without this, a gap wider
|
|
123
|
+
* than `margin` straddling a plot edge would drop the finite anchor sitting
|
|
124
|
+
* >`margin` points off-screen, turning an *interior* gap into a *leading /
|
|
125
|
+
* trailing* one inside the slice — which `bridgeGaps` and `collectGapEdges` both
|
|
126
|
+
* leave broken (they only bridge gaps with a finite sample on *both* sides). The
|
|
127
|
+
* `none` / `dashed` / `step` / `fade` connector that crossed the edge would then
|
|
128
|
+
* vanish (a notch under pan). Re-including the anchor keeps the boundary gap
|
|
129
|
+
* *interior*, so every mode draws exactly as it does un-culled. Cost is one
|
|
130
|
+
* `isFinite` check per side in the common (finite-boundary) case; the walk only
|
|
131
|
+
* runs for an edge-straddling gap and is bounded by that gap's width. (The
|
|
132
|
+
* default `empty` mode breaks at gaps regardless, so it is unaffected either
|
|
133
|
+
* way — this makes the guarantee hold for *all* modes.)
|
|
134
|
+
*/
|
|
135
|
+
export function cullChartSeries(cs, xScale, margin = 1) {
|
|
136
|
+
if (cs.length === 0)
|
|
137
|
+
return cs;
|
|
138
|
+
const dom = scaleDomain(xScale);
|
|
139
|
+
if (dom === null)
|
|
140
|
+
return cs;
|
|
141
|
+
let [start, end] = visiblePointWindow(cs.x, cs.length, dom[0], dom[1], margin);
|
|
142
|
+
// Extend each boundary to the nearest finite y-anchor so a gap straddling the
|
|
143
|
+
// edge stays interior (see "Gap-mode neutrality" above).
|
|
144
|
+
while (start > 0 && !Number.isFinite(cs.y[start]))
|
|
145
|
+
start -= 1;
|
|
146
|
+
while (end < cs.length && !Number.isFinite(cs.y[end - 1]))
|
|
147
|
+
end += 1;
|
|
148
|
+
if (start === 0 && end === cs.length)
|
|
149
|
+
return cs; // whole series in view
|
|
150
|
+
return {
|
|
151
|
+
x: cs.x.subarray(start, end),
|
|
152
|
+
y: cs.y.subarray(start, end),
|
|
153
|
+
length: end - start,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* A {@link BandSeries} clipped to the visible window of `xScale` — the paired
|
|
158
|
+
* `lower`/`upper` edges culled in lockstep with the shared `x` axis, so the
|
|
159
|
+
* envelope stays aligned. Same identity-preserving fast path and zero-copy
|
|
160
|
+
* `subarray` view as {@link cullChartSeries}.
|
|
161
|
+
*
|
|
162
|
+
* Unlike {@link cullChartSeries} this needs **no** finite-anchor boundary walk:
|
|
163
|
+
* a band has no gap-bridge mode (`drawBand` always breaks the fill at a gap, it
|
|
164
|
+
* never interpolates one), so a gap straddling a plot edge is a hole on both
|
|
165
|
+
* sides of the cut — there is no crossing fill to lose. The `margin` entry/exit
|
|
166
|
+
* sample is enough for a gap-free envelope that spans the edge.
|
|
167
|
+
*/
|
|
168
|
+
export function cullBandSeries(band, xScale, margin = 1) {
|
|
169
|
+
if (band.length === 0)
|
|
170
|
+
return band;
|
|
171
|
+
const dom = scaleDomain(xScale);
|
|
172
|
+
if (dom === null)
|
|
173
|
+
return band;
|
|
174
|
+
const [start, end] = visiblePointWindow(band.x, band.length, dom[0], dom[1], margin);
|
|
175
|
+
if (start === 0 && end === band.length)
|
|
176
|
+
return band; // whole band in view
|
|
177
|
+
return {
|
|
178
|
+
x: band.x.subarray(start, end),
|
|
179
|
+
lower: band.lower.subarray(start, end),
|
|
180
|
+
upper: band.upper.subarray(start, end),
|
|
181
|
+
length: end - start,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
185
|
+
// Index-range culling for **per-mark** layers (scatter, bars, candles, boxes).
|
|
186
|
+
//
|
|
187
|
+
// Unlike the line/area/band draws — which stroke one continuous path and take a
|
|
188
|
+
// zero-copy `subarray` view — these layers loop over *independent* marks with
|
|
189
|
+
// **index-keyed accessors** (a scatter's `colorAt(i)` / `keyAt(i)`, a bar's
|
|
190
|
+
// `begin[i]` selection match). A subarray would renumber `i` and break those, so
|
|
191
|
+
// the fit is instead a visible `[start, end)` **index range** the draw loop runs
|
|
192
|
+
// over (`for (i = start; i < end; …)`), leaving every accessor's `i` intact and
|
|
193
|
+
// the source arrays untouched (the §2.3 interaction-reads-source invariant holds
|
|
194
|
+
// the same way — hit-tests still scan the full arrays).
|
|
195
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
196
|
+
/**
|
|
197
|
+
* The index range `[start, end)` of **interval marks** — each spanning
|
|
198
|
+
* `[begin[i], end[i]]` on a **monotonically ascending** `begin` axis — whose span
|
|
199
|
+
* overlaps the visible `[lo, hi]`, plus `margin` marks on each side. A mark is
|
|
200
|
+
* visible iff `end[i] >= lo && begin[i] <= hi`.
|
|
201
|
+
*
|
|
202
|
+
* - **Right:** `begin[i] <= hi` ⇒ everything below `upperBound(begin, hi)`; a
|
|
203
|
+
* mark starting past the right edge is off-screen. Exact — no bisect on `end`
|
|
204
|
+
* needed.
|
|
205
|
+
* - **Left:** a mark with `begin[i] < lo` is still visible if its span reaches
|
|
206
|
+
* `lo` (`end[i] >= lo`) — a wide bar crossing the left edge. `begin` bisects
|
|
207
|
+
* the first in-range mark; from there the scan walks back while the previous
|
|
208
|
+
* mark's `end` still reaches `lo`. For sorted non-overlapping marks (the bar /
|
|
209
|
+
* candle / box contract) `end` is ascending, so the walk stops at the first
|
|
210
|
+
* mark clear of the edge — typically one step.
|
|
211
|
+
*
|
|
212
|
+
* Pure, O(log length + crossing marks). `margin` (default 1) pads each side for
|
|
213
|
+
* a mark whose drawn rect is nudged by `gapPx` / `minWidth` / a pixel `offsetPx`
|
|
214
|
+
* the data-space window can't see.
|
|
215
|
+
*/
|
|
216
|
+
export function visibleSpanWindow(begin, end, length, lo, hi, margin = 1) {
|
|
217
|
+
if (length === 0)
|
|
218
|
+
return [0, 0];
|
|
219
|
+
const right = upperBound(begin, length, hi); // first begin > hi
|
|
220
|
+
let start = lowerBound(begin, length, lo); // first begin >= lo
|
|
221
|
+
// Walk back to include earlier marks whose span still crosses into [lo, …].
|
|
222
|
+
while (start > 0 && end[start - 1] >= lo)
|
|
223
|
+
start -= 1;
|
|
224
|
+
return [Math.max(0, start - margin), Math.min(length, right + margin)];
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* The visible `[start, end)` index range of a **point** layer (scatter) against
|
|
228
|
+
* `xScale` — a thin wrapper over {@link visiblePointWindow} that reads the scale's
|
|
229
|
+
* domain. Returns the **full** range `[0, length]` when the scale exposes no
|
|
230
|
+
* numeric domain (a bare test stub / category axis) or the series is empty, so a
|
|
231
|
+
* caller loops over everything and the draw is unchanged there.
|
|
232
|
+
*
|
|
233
|
+
* **Radius-aware widening (`padPx`).** The `margin` is in *index* space, but a
|
|
234
|
+
* point mark's **disc** has a pixel radius independent of sample spacing — so a
|
|
235
|
+
* dense scatter of fat marks can put an edge bubble's *centre* several samples
|
|
236
|
+
* off-screen while its disc still overlaps the plot edge, which a bare index
|
|
237
|
+
* margin would drop (a subtle flicker under pan — the sharp edge #499 flagged as
|
|
238
|
+
* a follow-up). Passing `padPx` widens the data window by that many **pixels** on
|
|
239
|
+
* each side — converted px→data through `xScale.invert` — before the bisect, so
|
|
240
|
+
* every mark whose disc can paint into the plot is kept. Scatter passes its max
|
|
241
|
+
* drawn radius (plus any pixel offset); interval marks ({@link visibleSpanRange})
|
|
242
|
+
* don't need it — their width *is* their x-span.
|
|
243
|
+
*
|
|
244
|
+
* The pad is skipped (the plain domain window still applies) when `padPx <= 0` or
|
|
245
|
+
* the scale carries no `invert` (a real domain-bearing runtime scale always has
|
|
246
|
+
* one; only a partial stub lacks it, and it degrades to the index window — a
|
|
247
|
+
* slightly tighter cull, never a dropped mark, since over-padding only *adds*
|
|
248
|
+
* marks). `padPx` converts as `|invert(padPx) − invert(0)|`, the data span of
|
|
249
|
+
* `padPx` pixels: exact for the linear `scaleTime`/`scaleLinear` regardless of
|
|
250
|
+
* range offset, a local estimate for a non-linear axis, and the `Math.abs` keeps
|
|
251
|
+
* it a *widening* even under a reversed scale.
|
|
252
|
+
*/
|
|
253
|
+
export function visiblePointRange(x, length, xScale, padPx = 0, margin = 1) {
|
|
254
|
+
if (length === 0)
|
|
255
|
+
return [0, 0];
|
|
256
|
+
const dom = scaleDomain(xScale);
|
|
257
|
+
if (dom === null)
|
|
258
|
+
return [0, length];
|
|
259
|
+
let [lo, hi] = dom;
|
|
260
|
+
if (padPx > 0) {
|
|
261
|
+
const invert = xScale
|
|
262
|
+
.invert;
|
|
263
|
+
if (typeof invert === 'function') {
|
|
264
|
+
const dataPad = Math.abs(invert(padPx) - invert(0));
|
|
265
|
+
lo -= dataPad;
|
|
266
|
+
hi += dataPad;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return visiblePointWindow(x, length, lo, hi, margin);
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* The visible `[start, end)` index range of an **interval** layer (bars,
|
|
273
|
+
* candles, boxes) against `xScale` — a thin wrapper over
|
|
274
|
+
* {@link visibleSpanWindow} that reads the scale's domain. Returns the **full**
|
|
275
|
+
* range `[0, length]` when the scale exposes no numeric domain or the series is
|
|
276
|
+
* empty (the draw is unchanged there — a bare stub / category axis draws all).
|
|
277
|
+
*/
|
|
278
|
+
export function visibleSpanRange(begin, end, length, xScale, margin = 1) {
|
|
279
|
+
if (length === 0)
|
|
280
|
+
return [0, 0];
|
|
281
|
+
const dom = scaleDomain(xScale);
|
|
282
|
+
if (dom === null)
|
|
283
|
+
return [0, length];
|
|
284
|
+
return visibleSpanWindow(begin, end, length, dom[0], dom[1], margin);
|
|
285
|
+
}
|
|
286
|
+
//# sourceMappingURL=culling.js.map
|
package/dist/data.d.ts
CHANGED
|
@@ -10,7 +10,9 @@ import type { SeriesSchema, TimeSeries, ValueSeriesSchema } from 'pond-ts';
|
|
|
10
10
|
*
|
|
11
11
|
* Both arrays are length `length`. `x` is a zero-copy view of the key column's
|
|
12
12
|
* `begin` buffer (immutable by contract — do not mutate); `y` is the value
|
|
13
|
-
* column materialized to a `Float64Array`.
|
|
13
|
+
* column materialized to a `Float64Array`. `x` is **monotonically ascending**
|
|
14
|
+
* (a series' key column is sorted) — the draw layers and the viewport bisect
|
|
15
|
+
* (`culling.ts`) rely on it, as `sessionRuns` already does.
|
|
14
16
|
*/
|
|
15
17
|
export interface ChartSeries {
|
|
16
18
|
readonly x: Float64Array;
|
|
@@ -0,0 +1,193 @@
|
|
|
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 } 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
|
+
//# sourceMappingURL=decimate.d.ts.map
|