@pond-ts/charts 0.71.0 → 0.73.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/API.md +5 -4
- package/CHANGELOG.md +334 -5
- package/README.md +11 -0
- package/dist/AreaChart.d.ts +35 -12
- package/dist/AreaChart.js +28 -13
- package/dist/BandChart.d.ts +9 -1
- package/dist/BandChart.js +5 -1
- package/dist/ChartRow.js +2 -2
- package/dist/LineChart.d.ts +25 -1
- package/dist/LineChart.js +5 -1
- package/dist/XAxis.d.ts +70 -3
- package/dist/XAxis.js +187 -35
- package/dist/area.d.ts +1 -13
- package/dist/area.js +65 -69
- package/dist/bars.d.ts +7 -6
- package/dist/bars.js +7 -6
- package/dist/context.d.ts +2 -7
- package/dist/line.d.ts +14 -0
- package/dist/line.js +47 -7
- package/dist/tradingTimeScale.d.ts +7 -1
- package/dist/tradingTimeScale.js +3 -2
- package/dist/x-offset.d.ts +20 -0
- package/dist/x-offset.js +23 -0
- package/package.json +3 -3
package/dist/area.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { area as d3area, curveLinear } from 'd3-shape';
|
|
2
|
-
import { baselinePxFromScale, plotExtentOf, strokeAffinePolyline, TRACE_HIT_PX, } from './line.js';
|
|
2
|
+
import { baselinePxFromScale, decimatedSessionRuns, plotExtentOf, sessionRuns, strokeAffinePolyline, TRACE_HIT_PX, } from './line.js';
|
|
3
3
|
import { bridgeGaps, collectGapEdges, drawGapBridges, drawGapFades, drawGapSteps, gapUnscalable, withAlpha, DEFAULT_GAP_MODE, DEFAULT_GAP_CONNECTOR_OPACITY, } from './gaps.js';
|
|
4
4
|
import { cullChartSeries } from './culling.js';
|
|
5
5
|
import { decimateM4Cached } from './decimate.js';
|
|
6
6
|
import { affineOf } from './affine.js';
|
|
7
|
+
/** Shared empty boundary list — the no-breaks default (a stable reference, so
|
|
8
|
+
* the decimation cache keyed on it keeps hitting). */
|
|
9
|
+
const EMPTY_BOUNDARIES = [];
|
|
7
10
|
/**
|
|
8
11
|
* Per-buffer cache of a column's finite `[min, max]` value extent ([PND-GRADX]).
|
|
9
12
|
* The area fill gradient spans the **full** series' vertical pixel extent (so a
|
|
@@ -21,7 +24,7 @@ import { affineOf } from './affine.js';
|
|
|
21
24
|
* length); a `subarray` view is never the cache key here (the gradient reads the
|
|
22
25
|
* pre-cull full series).
|
|
23
26
|
*
|
|
24
|
-
* NaN (the gap signal) is ignored — matching
|
|
27
|
+
* NaN (the gap signal) is ignored — matching the layer's `yExtent` — so
|
|
25
28
|
* a coast doesn't drag the span. `null` when nothing is finite (the caller then
|
|
26
29
|
* falls back to a flat fill).
|
|
27
30
|
*/
|
|
@@ -85,41 +88,6 @@ export function fillAffineArea(ctx, xs, ys, baselinePx, ax, ay) {
|
|
|
85
88
|
}
|
|
86
89
|
}
|
|
87
90
|
}
|
|
88
|
-
/**
|
|
89
|
-
* The `[min, max]` vertical extent an area occupies — the finite values of
|
|
90
|
-
* `cs.y` widened to include `baseline`, since the fill spans from each value to
|
|
91
|
-
* the baseline (so the baseline must be in-domain or the fill clips). `null` if
|
|
92
|
-
* no value is finite. When `baseline` is `undefined` (`<AreaChart
|
|
93
|
-
* baseline="floor">`) the area rests on the axis's own lower bound (resolved
|
|
94
|
-
* later), so only the values constrain the
|
|
95
|
-
* domain — matching {@link yExtent}.
|
|
96
|
-
*
|
|
97
|
-
* NaN values (the gap signal) are ignored, so a coast doesn't drag the domain.
|
|
98
|
-
*/
|
|
99
|
-
export function areaExtent(cs, baseline) {
|
|
100
|
-
let min = Infinity;
|
|
101
|
-
let max = -Infinity;
|
|
102
|
-
for (let i = 0; i < cs.length; i += 1) {
|
|
103
|
-
const v = cs.y[i];
|
|
104
|
-
if (Number.isFinite(v)) {
|
|
105
|
-
if (v < min)
|
|
106
|
-
min = v;
|
|
107
|
-
if (v > max)
|
|
108
|
-
max = v;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
if (min === Infinity)
|
|
112
|
-
return null;
|
|
113
|
-
// The fill reaches the baseline, so it must be inside the domain (an
|
|
114
|
-
// above/below-axis area with baseline 0 has to show the zero line).
|
|
115
|
-
if (baseline !== undefined) {
|
|
116
|
-
if (baseline < min)
|
|
117
|
-
min = baseline;
|
|
118
|
-
if (baseline > max)
|
|
119
|
-
max = baseline;
|
|
120
|
-
}
|
|
121
|
-
return [min, max];
|
|
122
|
-
}
|
|
123
91
|
/**
|
|
124
92
|
* Fill the area between `cs`'s value line and a horizontal `baseline`, with a
|
|
125
93
|
* vertical gradient (most opaque at the line, fading to transparent at the
|
|
@@ -156,7 +124,7 @@ export function areaExtent(cs, baseline) {
|
|
|
156
124
|
* bracketed by `save`/`restore` so they don't leak into later layers. Gap edges
|
|
157
125
|
* are collected by one O(N) walk ({@link collectGapEdges}).
|
|
158
126
|
*/
|
|
159
|
-
export function drawArea(ctx, cs, xScale, yScale, style, baselineValue, curve = curveLinear, gaps = DEFAULT_GAP_MODE, gapConnectorOpacity = DEFAULT_GAP_CONNECTOR_OPACITY, decimate = true, banding) {
|
|
127
|
+
export function drawArea(ctx, cs, xScale, yScale, style, baselineValue, curve = curveLinear, gaps = DEFAULT_GAP_MODE, gapConnectorOpacity = DEFAULT_GAP_CONNECTOR_OPACITY, decimate = true, banding, boundaries = EMPTY_BOUNDARIES) {
|
|
160
128
|
const sourceCount = cs.length; // pre-cull, pre-decimation (for draw stats)
|
|
161
129
|
const baselinePx = yScale(baselineValue);
|
|
162
130
|
// The fill gradient's vertical extent is computed from the **full** series (a
|
|
@@ -191,7 +159,7 @@ export function drawArea(ctx, cs, xScale, yScale, style, baselineValue, curve =
|
|
|
191
159
|
let decimated = false;
|
|
192
160
|
if (decimate !== false && curve === curveLinear) {
|
|
193
161
|
const k = typeof decimate === 'object' ? decimate.threshold : undefined;
|
|
194
|
-
const r = decimateM4Cached(source, xScale, ctx, k);
|
|
162
|
+
const r = decimateM4Cached(source, xScale, ctx, k, boundaries);
|
|
195
163
|
cs = r.series;
|
|
196
164
|
decimated = r.decimated;
|
|
197
165
|
}
|
|
@@ -207,16 +175,41 @@ export function drawArea(ctx, cs, xScale, yScale, style, baselineValue, curve =
|
|
|
207
175
|
const scaledY = gapUnscalable(cs.y, cs.length, yScale);
|
|
208
176
|
if (scaledY !== cs.y)
|
|
209
177
|
cs = { ...cs, y: scaledY };
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
|
|
178
|
+
// Split into independent index runs at each session break, exactly as
|
|
179
|
+
// `drawLine` does: no boundary inside the data ⇒ one run over the whole series
|
|
180
|
+
// (the hot path — no slicing, byte-identical to the pre-boundary draw). When
|
|
181
|
+
// decimated, the breaks are already baked in as `NaN` points at the break
|
|
182
|
+
// instants, and {@link decimatedSessionRuns} cuts there instead.
|
|
183
|
+
const runs = decimated
|
|
184
|
+
? decimatedSessionRuns(cs.x, cs.y, cs.length, boundaries)
|
|
185
|
+
: sessionRuns(cs.x, cs.length, boundaries);
|
|
186
|
+
const singleRun = runs.length === 1;
|
|
187
|
+
const xsOf = (s, e) => singleRun ? cs.x : cs.x.subarray(s, e);
|
|
188
|
+
// `none` interpolates interior gaps so the fill + outline bridge them — but
|
|
189
|
+
// only *within* a run (a session break is not a dropout to interpolate over);
|
|
190
|
+
// every other mode keeps NaN so d3 breaks both (the inferred line bridge, if
|
|
191
|
+
// any, is a separate overlay pass below).
|
|
192
|
+
const ysRuns = runs.map(([s, e]) => {
|
|
193
|
+
const seg = singleRun ? cs.y : cs.y.subarray(s, e);
|
|
194
|
+
return gaps === 'none' ? bridgeGaps(seg, e - s) : seg;
|
|
195
|
+
});
|
|
214
196
|
// [PND-AFFINE] fast path: with a linear curve and both scales affine, draw the
|
|
215
197
|
// fill polygon + outline with inline multiply-add over the typed arrays, past
|
|
216
198
|
// the per-point d3-scale + d3-shape closures (finding 1/2). A smoothing curve
|
|
217
199
|
// or a non-affine (real-gap trading) x scale keeps the exact d3-area path.
|
|
218
200
|
const ax = curve === curveLinear ? affineOf(xScale) : null;
|
|
219
201
|
const ay = ax !== null ? affineOf(yScale) : null;
|
|
202
|
+
// The d3-area generator for a run (slow path only) — also the source of that
|
|
203
|
+
// run's outline line. Each run's generator opens its own subpath and closes
|
|
204
|
+
// its own polygon, so a run boundary is a clean break in the fill: the shade
|
|
205
|
+
// ends at the close and re-starts at the open.
|
|
206
|
+
const areaGen = (s) => d3area()
|
|
207
|
+
.defined((v) => Number.isFinite(v))
|
|
208
|
+
.x((_, j) => xScale(cs.x[s + j]))
|
|
209
|
+
.y0(() => baselinePx)
|
|
210
|
+
.y1((v) => yScale(v))
|
|
211
|
+
.curve(curve)
|
|
212
|
+
.context(ctx);
|
|
220
213
|
ctx.save();
|
|
221
214
|
// The fill: a vertical gradient anchored at the baseline pixel, opaque at the
|
|
222
215
|
// line and transparent at the baseline (see buildGradient — handles both the
|
|
@@ -225,44 +218,47 @@ export function drawArea(ctx, cs, xScale, yScale, style, baselineValue, curve =
|
|
|
225
218
|
ctx.fillStyle = fill;
|
|
226
219
|
ctx.globalAlpha = style.fillOpacity;
|
|
227
220
|
ctx.beginPath();
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
.x((_, i) => xScale(cs.x[i]))
|
|
237
|
-
.y0(() => baselinePx)
|
|
238
|
-
.y1((v) => yScale(v))
|
|
239
|
-
.curve(curve)
|
|
240
|
-
.context(ctx);
|
|
241
|
-
gen(ys);
|
|
242
|
-
outline = gen.lineY1();
|
|
243
|
-
}
|
|
221
|
+
runs.forEach(([s, e], r) => {
|
|
222
|
+
if (ax !== null && ay !== null) {
|
|
223
|
+
fillAffineArea(ctx, xsOf(s, e), ysRuns[r], baselinePx, ax, ay);
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
areaGen(s)(ysRuns[r]);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
244
229
|
ctx.fill();
|
|
245
230
|
ctx.restore();
|
|
246
231
|
// The outline on top: the area's top edge as a line (breaks at the same gaps
|
|
247
|
-
// as the fill), at full opacity over the graded fill.
|
|
248
|
-
// the same hard-stop gradient the fill used, so the
|
|
249
|
-
// where it crosses a threshold — the whole point of
|
|
250
|
-
// reader sees *where* the value sits, and the edge is
|
|
232
|
+
// and session breaks as the fill), at full opacity over the graded fill.
|
|
233
|
+
// Banded, it strokes with the same hard-stop gradient the fill used, so the
|
|
234
|
+
// line switches hue exactly where it crosses a threshold — the whole point of
|
|
235
|
+
// the ladder is that the reader sees *where* the value sits, and the edge is
|
|
236
|
+
// the value.
|
|
251
237
|
ctx.save();
|
|
252
238
|
ctx.beginPath();
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
239
|
+
runs.forEach(([s, e], r) => {
|
|
240
|
+
if (ax !== null && ay !== null) {
|
|
241
|
+
strokeAffinePolyline(ctx, xsOf(s, e), ysRuns[r], ax, ay);
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
areaGen(s).lineY1()(ysRuns[r]);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
257
247
|
ctx.strokeStyle = banding !== undefined ? fill : style.color;
|
|
258
248
|
ctx.lineWidth = style.width;
|
|
259
249
|
ctx.stroke();
|
|
260
250
|
ctx.restore();
|
|
261
251
|
// Inferred bridges for the line edge (fill stays broken). `dashed` / `step`
|
|
262
252
|
// are faint dashed connectors (gapConnectorOpacity); only `fade` drops to the
|
|
263
|
-
// area's own baseline pixel (the fill floor).
|
|
253
|
+
// area's own baseline pixel (the fill floor). Edges are collected **per run**
|
|
254
|
+
// so no connector spans a session break.
|
|
264
255
|
if (gaps === 'dashed' || gaps === 'step' || gaps === 'fade') {
|
|
265
|
-
const edges =
|
|
256
|
+
const edges = [];
|
|
257
|
+
for (const [s, e] of runs) {
|
|
258
|
+
const runEdges = collectGapEdges(e - s, xsOf(s, e), (i) => cs.y[s + i], xScale, (i) => yScale(cs.y[s + i]));
|
|
259
|
+
for (const ed of runEdges)
|
|
260
|
+
edges.push(ed);
|
|
261
|
+
}
|
|
266
262
|
if (gaps === 'dashed') {
|
|
267
263
|
drawGapBridges(ctx, edges, style.color, style.width, gapConnectorOpacity);
|
|
268
264
|
}
|
package/dist/bars.d.ts
CHANGED
|
@@ -17,12 +17,13 @@ export type Orientation = 'vertical' | 'horizontal';
|
|
|
17
17
|
* and the baseline must be in-domain or the bar clips. `null` if no value is
|
|
18
18
|
* finite.
|
|
19
19
|
*
|
|
20
|
-
* Including `0` is
|
|
21
|
-
*
|
|
22
|
-
* rest on a visible floor (the zero line),
|
|
23
|
-
* shows the zero line both above and below
|
|
24
|
-
* wins — `resolveBarBaseline` rests the
|
|
25
|
-
* (the gap signal) are ignored, so a
|
|
20
|
+
* Including `0` is where a bar differs from an area, whose baseline never
|
|
21
|
+
* widens the axis: a bar's length is its value, so an all-positive series
|
|
22
|
+
* auto-fits to `[0, max]` and the bars rest on a visible floor (the zero line),
|
|
23
|
+
* and a series that straddles zero shows the zero line both above and below
|
|
24
|
+
* it. An explicit `<YAxis min>` still wins — `resolveBarBaseline` rests the
|
|
25
|
+
* bars on that floor instead. NaN values (the gap signal) are ignored, so a
|
|
26
|
+
* sparse bucket doesn't drag the domain.
|
|
26
27
|
*/
|
|
27
28
|
export declare function barExtent(cs: BarSeries): [number, number] | null;
|
|
28
29
|
/**
|
package/dist/bars.js
CHANGED
|
@@ -8,12 +8,13 @@ import { NO_SPANS, spanMatchesAny } from './span.js';
|
|
|
8
8
|
* and the baseline must be in-domain or the bar clips. `null` if no value is
|
|
9
9
|
* finite.
|
|
10
10
|
*
|
|
11
|
-
* Including `0` is
|
|
12
|
-
*
|
|
13
|
-
* rest on a visible floor (the zero line),
|
|
14
|
-
* shows the zero line both above and below
|
|
15
|
-
* wins — `resolveBarBaseline` rests the
|
|
16
|
-
* (the gap signal) are ignored, so a
|
|
11
|
+
* Including `0` is where a bar differs from an area, whose baseline never
|
|
12
|
+
* widens the axis: a bar's length is its value, so an all-positive series
|
|
13
|
+
* auto-fits to `[0, max]` and the bars rest on a visible floor (the zero line),
|
|
14
|
+
* and a series that straddles zero shows the zero line both above and below
|
|
15
|
+
* it. An explicit `<YAxis min>` still wins — `resolveBarBaseline` rests the
|
|
16
|
+
* bars on that floor instead. NaN values (the gap signal) are ignored, so a
|
|
17
|
+
* sparse bucket doesn't drag the domain.
|
|
17
18
|
*/
|
|
18
19
|
export function barExtent(cs) {
|
|
19
20
|
let min = Infinity;
|
package/dist/context.d.ts
CHANGED
|
@@ -716,13 +716,8 @@ export interface RowLayer {
|
|
|
716
716
|
* when the layer was given no `as`.
|
|
717
717
|
*/
|
|
718
718
|
readonly as?: string | undefined;
|
|
719
|
-
/**
|
|
720
|
-
|
|
721
|
-
* is the kind of the axis the extent is being fitted to, for a layer whose
|
|
722
|
-
* extent depends on it (an area's zero baseline has no position on a log
|
|
723
|
-
* axis, so it must not be pulled into one).
|
|
724
|
-
*/
|
|
725
|
-
yExtent(scale?: YScaleKind): [number, number] | null;
|
|
719
|
+
/** This layer's finite-value `[min, max]`, or `null` if it has none. */
|
|
720
|
+
yExtent(): [number, number] | null;
|
|
726
721
|
/**
|
|
727
722
|
* The **kind of x axis** this layer's data lives on — `'time'` for a
|
|
728
723
|
* `TimeSeries`, `'value'` for a `ValueSeries`, `'category'` for a categorical
|
package/dist/line.d.ts
CHANGED
|
@@ -82,6 +82,20 @@ export declare function drawLine(ctx: CanvasRenderingContext2D, cs: ChartSeries,
|
|
|
82
82
|
* negligible next to the row sweep.
|
|
83
83
|
*/
|
|
84
84
|
export declare function sessionRuns(x: Float64Array, length: number, boundaries: readonly number[]): Array<[number, number]>;
|
|
85
|
+
/**
|
|
86
|
+
* {@link sessionRuns} for a **decimated** series. `decimateM4` bakes each
|
|
87
|
+
* session break in as a `NaN` point at the break instant, with the closing
|
|
88
|
+
* column's last point and the opening column's first point **both** at that
|
|
89
|
+
* instant — so `sessionRuns`'s `(x[i-1], x[i]]` cut would hand the close's last
|
|
90
|
+
* point to the next session. Instead, cut at every non-finite run whose finite
|
|
91
|
+
* neighbours bracket a boundary (`lastX ≤ b ≤ nextX`), starting the new run at
|
|
92
|
+
* the run's first `NaN`. The break then sits as a **leading** gap of the next
|
|
93
|
+
* session: never interior, so `gaps="none"` does not interpolate across it and
|
|
94
|
+
* no inferred connector (dashed / step / fade) spans it — left in one run it
|
|
95
|
+
* read as an ordinary dropout and got both. A data gap that straddles a break
|
|
96
|
+
* is cut the same way, matching the full-resolution path. Pure + O(N).
|
|
97
|
+
*/
|
|
98
|
+
export declare function decimatedSessionRuns(x: Float64Array, y: Float64Array, length: number, boundaries: readonly number[]): Array<[number, number]>;
|
|
85
99
|
/** How near the pointer must come to a trace's path to count as a hit, in px.
|
|
86
100
|
* Generous on purpose: a 1.5px stroke is not a target anyone can hit, and a
|
|
87
101
|
* trace has no fat mark to aim at the way a bar does. */
|
package/dist/line.js
CHANGED
|
@@ -3,9 +3,6 @@ import { cullChartSeries } from './culling.js';
|
|
|
3
3
|
import { decimateM4Cached } from './decimate.js';
|
|
4
4
|
import { affineOf } from './affine.js';
|
|
5
5
|
import { bridgeGaps, collectGapEdges, drawGapBridges, drawGapFades, drawGapSteps, gapUnscalable, DEFAULT_GAP_MODE, DEFAULT_GAP_CONNECTOR_OPACITY, } from './gaps.js';
|
|
6
|
-
/** Shared empty boundary list — passed to `sessionRuns` when a decimated series
|
|
7
|
-
* already carries its session breaks as baked-in `NaN` points. */
|
|
8
|
-
const EMPTY_BOUNDARIES = [];
|
|
9
6
|
/**
|
|
10
7
|
* Stroke one run of `ys` (aligned index-for-index with `xs`) through the affine
|
|
11
8
|
* pixel maps `ax` / `ay` — the [PND-AFFINE] fast path. Replicates d3-shape's
|
|
@@ -144,10 +141,12 @@ export function drawLine(ctx, cs, xScale, yScale, style, curve = curveLinear, ga
|
|
|
144
141
|
// data ⇒ one run over the whole series (the hot path — no slicing, so the draw
|
|
145
142
|
// is byte-identical to the pre-boundary single pass). When the series was
|
|
146
143
|
// decimated, `decimateM4` already baked the session breaks in as `NaN` points
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
const runs =
|
|
144
|
+
// at the break instants, where `sessionRuns` would mis-attribute the boundary
|
|
145
|
+
// points — {@link decimatedSessionRuns} cuts at those baked gaps instead, so a
|
|
146
|
+
// break is never interior to a run (no `none` bridge or connector across it).
|
|
147
|
+
const runs = decimated
|
|
148
|
+
? decimatedSessionRuns(cs.x, cs.y, cs.length, boundaries)
|
|
149
|
+
: sessionRuns(cs.x, cs.length, boundaries);
|
|
151
150
|
const singleRun = runs.length === 1;
|
|
152
151
|
// [PND-AFFINE] fast path: when the curve is linear and **both** scales are
|
|
153
152
|
// affine (every y axis is `scaleLinear`; x is `scaleLinear` / `scaleTime` /
|
|
@@ -260,6 +259,47 @@ export function sessionRuns(x, length, boundaries) {
|
|
|
260
259
|
runs.push([start, length]);
|
|
261
260
|
return runs;
|
|
262
261
|
}
|
|
262
|
+
/**
|
|
263
|
+
* {@link sessionRuns} for a **decimated** series. `decimateM4` bakes each
|
|
264
|
+
* session break in as a `NaN` point at the break instant, with the closing
|
|
265
|
+
* column's last point and the opening column's first point **both** at that
|
|
266
|
+
* instant — so `sessionRuns`'s `(x[i-1], x[i]]` cut would hand the close's last
|
|
267
|
+
* point to the next session. Instead, cut at every non-finite run whose finite
|
|
268
|
+
* neighbours bracket a boundary (`lastX ≤ b ≤ nextX`), starting the new run at
|
|
269
|
+
* the run's first `NaN`. The break then sits as a **leading** gap of the next
|
|
270
|
+
* session: never interior, so `gaps="none"` does not interpolate across it and
|
|
271
|
+
* no inferred connector (dashed / step / fade) spans it — left in one run it
|
|
272
|
+
* read as an ordinary dropout and got both. A data gap that straddles a break
|
|
273
|
+
* is cut the same way, matching the full-resolution path. Pure + O(N).
|
|
274
|
+
*/
|
|
275
|
+
export function decimatedSessionRuns(x, y, length, boundaries) {
|
|
276
|
+
if (boundaries.length === 0 || length === 0)
|
|
277
|
+
return [[0, length]];
|
|
278
|
+
const bounds = boundaries.length > 1 ? [...boundaries].sort((a, b) => a - b) : boundaries;
|
|
279
|
+
const runs = [];
|
|
280
|
+
let start = 0;
|
|
281
|
+
let lastFinite = -1;
|
|
282
|
+
let bi = 0;
|
|
283
|
+
for (let i = 0; i < length; i += 1) {
|
|
284
|
+
if (!Number.isFinite(y[i]))
|
|
285
|
+
continue;
|
|
286
|
+
if (lastFinite >= 0 && i - lastFinite > 1) {
|
|
287
|
+
const a = x[lastFinite];
|
|
288
|
+
const b = x[i];
|
|
289
|
+
while (bi < bounds.length && bounds[bi] < a)
|
|
290
|
+
bi += 1;
|
|
291
|
+
if (bi < bounds.length && bounds[bi] <= b) {
|
|
292
|
+
runs.push([start, lastFinite + 1]);
|
|
293
|
+
start = lastFinite + 1;
|
|
294
|
+
while (bi < bounds.length && bounds[bi] <= b)
|
|
295
|
+
bi += 1;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
lastFinite = i;
|
|
299
|
+
}
|
|
300
|
+
runs.push([start, length]);
|
|
301
|
+
return runs;
|
|
302
|
+
}
|
|
263
303
|
/** How near the pointer must come to a trace's path to count as a hit, in px.
|
|
264
304
|
* Generous on purpose: a 1.5px stroke is not a target anyone can hit, and a
|
|
265
305
|
* trace has no fat mark to aim at the way a bar does. */
|
|
@@ -130,8 +130,14 @@ export interface TradingTimeScale {
|
|
|
130
130
|
* formats with the d3 multi-scale default, like {@link tickFormat}; the
|
|
131
131
|
* cursor readout itself uses {@link readoutFormat} (grain-aware), so ticks
|
|
132
132
|
* read terse while the crosshair reads an unambiguous date/clock.
|
|
133
|
+
*
|
|
134
|
+
* `drawn` — the subset of {@link ticks}`(count)` actually rendered, when an
|
|
135
|
+
* axis drops ticks whose labels would collide. Promotions then walk only the
|
|
136
|
+
* drawn ticks, so a dropped tick's period turn carries to the next drawn one
|
|
137
|
+
* (drop a stray `Sep 29` session open and the real open beside it reads
|
|
138
|
+
* `Sep 29`, not `15:30`). Defaults to every tick.
|
|
133
139
|
*/
|
|
134
|
-
flatFormat(count?: number): (value: number) => string;
|
|
140
|
+
flatFormat(count?: number, drawn?: readonly number[]): (value: number) => string;
|
|
135
141
|
/**
|
|
136
142
|
* The boundary-row label for the **domain start** — the reader's left-edge
|
|
137
143
|
* context (`Jan 01` over an intraday axis, the year over a month axis),
|
package/dist/tradingTimeScale.js
CHANGED
|
@@ -209,7 +209,7 @@ export function scaleTradingTime(provider, options = {}) {
|
|
|
209
209
|
}
|
|
210
210
|
return (value) => labelled.get(value);
|
|
211
211
|
};
|
|
212
|
-
scale.flatFormat = (count = 10) => {
|
|
212
|
+
scale.flatFormat = (count = 10, drawn) => {
|
|
213
213
|
// A non-tick instant always uses the d3 multi-scale default — same as
|
|
214
214
|
// tickFormat. (The cursor readout doesn't route through here; it uses
|
|
215
215
|
// readoutFormat, grain-aware.) Without a calendar there are no ladder
|
|
@@ -217,7 +217,8 @@ export function scaleTradingTime(provider, options = {}) {
|
|
|
217
217
|
const defFmt = fmt(count);
|
|
218
218
|
if (!hasCalendar())
|
|
219
219
|
return (value) => defFmt(new Date(value));
|
|
220
|
-
const { ticks, granularity } = resolved(count);
|
|
220
|
+
const { ticks: all, granularity } = resolved(count);
|
|
221
|
+
const ticks = drawn ?? all;
|
|
221
222
|
// Seed the promotion walk from the previous **live** instant before the
|
|
222
223
|
// domain start (clampDown; identity on a continuous axis): a domain that
|
|
223
224
|
// opens exactly on a month's first session then promotes that tick to the
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ValueSeries } from 'pond-ts';
|
|
2
|
+
import type { DurationInput, SeriesSchema, TimeSeries, ValueSeriesSchema } from 'pond-ts';
|
|
3
|
+
/**
|
|
4
|
+
* A layer's `xOffset`: how far after its key each row is **drawn** —
|
|
5
|
+
* milliseconds or a duration string (`'1m'`, `'-30s'`). See
|
|
6
|
+
* `LineChartCommon.xOffset`.
|
|
7
|
+
*/
|
|
8
|
+
export type XOffset = DurationInput;
|
|
9
|
+
/**
|
|
10
|
+
* The series a layer draws, moved by `xOffset`. Every downstream read — the
|
|
11
|
+
* drawn x, the auto-fit extent, session breaks, the tracker's nearest row and
|
|
12
|
+
* dot, the sweep bounds — then sees one consistent axis. The move is pond's
|
|
13
|
+
* `offsetTime`: one pass over the key, value columns shared.
|
|
14
|
+
*
|
|
15
|
+
* `undefined` / `0` return the source unchanged (same reference, so memo deps
|
|
16
|
+
* downstream stay stable). A value series has no time to move, so a non-zero
|
|
17
|
+
* offset on one throws.
|
|
18
|
+
*/
|
|
19
|
+
export declare function useXOffset<Sr extends TimeSeries<SeriesSchema> | ValueSeries<ValueSeriesSchema>>(series: Sr, xOffset: XOffset | undefined, layer: string): Sr;
|
|
20
|
+
//# sourceMappingURL=x-offset.d.ts.map
|
package/dist/x-offset.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { ValueSeries } from 'pond-ts';
|
|
3
|
+
/**
|
|
4
|
+
* The series a layer draws, moved by `xOffset`. Every downstream read — the
|
|
5
|
+
* drawn x, the auto-fit extent, session breaks, the tracker's nearest row and
|
|
6
|
+
* dot, the sweep bounds — then sees one consistent axis. The move is pond's
|
|
7
|
+
* `offsetTime`: one pass over the key, value columns shared.
|
|
8
|
+
*
|
|
9
|
+
* `undefined` / `0` return the source unchanged (same reference, so memo deps
|
|
10
|
+
* downstream stay stable). A value series has no time to move, so a non-zero
|
|
11
|
+
* offset on one throws.
|
|
12
|
+
*/
|
|
13
|
+
export function useXOffset(series, xOffset, layer) {
|
|
14
|
+
return useMemo(() => {
|
|
15
|
+
if (xOffset === undefined || xOffset === 0)
|
|
16
|
+
return series;
|
|
17
|
+
if (series instanceof ValueSeries) {
|
|
18
|
+
throw new TypeError(`<${layer}> xOffset moves a time axis; a ValueSeries has none`);
|
|
19
|
+
}
|
|
20
|
+
return series.offsetTime(xOffset);
|
|
21
|
+
}, [series, xOffset, layer]);
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=x-offset.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pond-ts/charts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.73.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Canvas-rendered, streaming-first React time-series charts for pond-ts: line, area, band, bar, scatter, box, candlestick, with cursors, selection and annotations",
|
|
6
6
|
"keywords": [
|
|
@@ -62,8 +62,8 @@
|
|
|
62
62
|
"perf": "PERF_BENCH=1 playwright test perf.spec.ts --workers=1"
|
|
63
63
|
},
|
|
64
64
|
"peerDependencies": {
|
|
65
|
-
"@pond-ts/react": "^0.
|
|
66
|
-
"pond-ts": "^0.
|
|
65
|
+
"@pond-ts/react": "^0.73.0",
|
|
66
|
+
"pond-ts": "^0.73.0",
|
|
67
67
|
"react": "^18.0.0 || ^19.0.0"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|