@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,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;
|