@pond-ts/charts 0.69.0 → 0.71.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/AGENTS.md +3 -1
- package/API.md +55 -56
- package/CHANGELOG.md +177 -1
- package/dist/AreaChart.d.ts +30 -22
- package/dist/AreaChart.js +37 -22
- package/dist/BandChart.d.ts +20 -1
- package/dist/BandChart.js +18 -2
- package/dist/BoxPlot.js +5 -3
- package/dist/Candlestick.d.ts +3 -3
- package/dist/Candlestick.js +5 -5
- package/dist/ChartContainer.d.ts +8 -149
- package/dist/ChartContainer.js +68 -111
- package/dist/ChartRow.d.ts +1 -12
- package/dist/ChartRow.js +15 -30
- package/dist/HeatMap.d.ts +3 -3
- package/dist/HeatMap.js +3 -3
- package/dist/Layers.js +84 -34
- package/dist/XAxis.js +1 -0
- package/dist/area.d.ts +6 -5
- package/dist/area.js +13 -6
- package/dist/axis-events.d.ts +4 -4
- package/dist/band.d.ts +10 -2
- package/dist/band.js +38 -10
- package/dist/brush.d.ts +13 -21
- package/dist/brush.js +22 -36
- package/dist/context.d.ts +81 -96
- package/dist/cursors.d.ts +81 -51
- package/dist/cursors.js +164 -127
- package/dist/decimate.d.ts +10 -1
- package/dist/decimate.js +51 -10
- package/dist/elapsed.d.ts +1 -1
- package/dist/elapsed.js +1 -1
- package/dist/format.d.ts +2 -1
- package/dist/index.d.ts +1 -1
- package/dist/selection-fixtures.d.ts +4 -7
- package/dist/selection-fixtures.js +0 -9
- package/dist/selection-stories.d.ts +4 -6
- package/dist/selection-stories.js +4 -4
- package/dist/selectors.d.ts +2 -3
- package/dist/selectors.js +2 -3
- package/dist/tracker.d.ts +10 -28
- package/dist/tracker.js +17 -40
- package/dist/tradingAxis.fixture.d.ts +19 -0
- package/dist/tradingAxis.fixture.js +24 -0
- package/dist/tradingTimeScale.d.ts +2 -2
- package/package.json +3 -3
package/dist/cursors.js
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useContext, useEffect, useMemo } from 'react';
|
|
2
|
+
import { useContext, useEffect, useLayoutEffect, useMemo, useRef, } from 'react';
|
|
3
3
|
import { ContainerContext, RowContext, } from './context.js';
|
|
4
4
|
import { renderBrushBand } from './brush.js';
|
|
5
5
|
import { flagChipStyle, flagChipX, axisPillStyle, axisPillX, axisPillConnector, } from './chip.js';
|
|
6
6
|
import { useSlotKey } from './use-slot-key.js';
|
|
7
7
|
import { isDev } from './dev.js';
|
|
8
8
|
/**
|
|
9
|
-
* Cursor **presets**
|
|
10
|
-
*
|
|
11
|
-
* `<
|
|
9
|
+
* Cursor **presets** (interaction RFC §4 / A4.1): `<LineCursor>`,
|
|
10
|
+
* `<PointCursor>`, `<InlineCursor>`, `<FlagCursor>`, `<CrosshairCursor>`,
|
|
11
|
+
* `<RangeCursor>`. **A chart shows a cursor only when one is mounted** —
|
|
12
|
+
* mounting none means no in-chart cursor (hover still reports through
|
|
13
|
+
* `onTrackerChanged`).
|
|
12
14
|
*
|
|
13
15
|
* Each preset registers a `CursorSpec` with the container (the
|
|
14
16
|
* `registerAxis` / `registerLayer` idiom): **declared** snap plus render slots
|
|
15
17
|
* taking resolved geometry. The container resolves — the x-snap, the
|
|
16
18
|
* per-sample measurements, the band — and the slots draw (RFC A2.3). Mount a
|
|
17
19
|
* preset as a child of `<ChartContainer>` (the default for every row) or
|
|
18
|
-
* inside a `<ChartRow>` (the per-row override
|
|
20
|
+
* inside a `<ChartRow>` (the per-row override: a row with its own mounts
|
|
21
|
+
* ignores the container's).
|
|
19
22
|
*
|
|
20
23
|
* Render-only presets may stack; **one cursor owns snap and gesture per
|
|
21
24
|
* scope**, resolved to the hovered row's innermost mount (RFC A2.5) — the
|
|
@@ -78,12 +81,13 @@ function cursorLine(f) {
|
|
|
78
81
|
}
|
|
79
82
|
const NO_WANTS = {
|
|
80
83
|
samples: false,
|
|
84
|
+
reticle: false,
|
|
81
85
|
flags: false,
|
|
82
86
|
band: false,
|
|
83
87
|
pointer: false,
|
|
84
88
|
time: false,
|
|
85
89
|
};
|
|
86
|
-
/**
|
|
90
|
+
/** `<LineCursor>` as a spec: the synced vertical line only (+ optional time). */
|
|
87
91
|
function buildLineCursor(o) {
|
|
88
92
|
return {
|
|
89
93
|
spec: {
|
|
@@ -95,9 +99,10 @@ function buildLineCursor(o) {
|
|
|
95
99
|
},
|
|
96
100
|
wants: { ...NO_WANTS, time: o.showTime },
|
|
97
101
|
ownsGesture: false,
|
|
102
|
+
format: o.format,
|
|
98
103
|
};
|
|
99
104
|
}
|
|
100
|
-
/**
|
|
105
|
+
/** `<PointCursor>` as a spec: a dot on each series, no line. */
|
|
101
106
|
function buildPointCursor(o) {
|
|
102
107
|
return {
|
|
103
108
|
spec: {
|
|
@@ -109,9 +114,10 @@ function buildPointCursor(o) {
|
|
|
109
114
|
},
|
|
110
115
|
wants: { ...NO_WANTS, samples: true, time: o.showTime },
|
|
111
116
|
ownsGesture: false,
|
|
117
|
+
format: o.format,
|
|
112
118
|
};
|
|
113
119
|
}
|
|
114
|
-
/**
|
|
120
|
+
/** `<InlineCursor>` as a spec: dots + a value chip beside each, clamped
|
|
115
121
|
* within the row and flipped left near the right edge. */
|
|
116
122
|
function buildInlineCursor(o) {
|
|
117
123
|
return {
|
|
@@ -137,9 +143,10 @@ function buildInlineCursor(o) {
|
|
|
137
143
|
},
|
|
138
144
|
wants: { ...NO_WANTS, samples: true, time: o.showTime },
|
|
139
145
|
ownsGesture: false,
|
|
146
|
+
format: o.format,
|
|
140
147
|
};
|
|
141
148
|
}
|
|
142
|
-
/**
|
|
149
|
+
/** `<FlagCursor>` as a spec: dots + staffed value flags stacked near the top
|
|
143
150
|
* of the row, plus the consolidated one-chip flag for `cursorFlag` layers
|
|
144
151
|
* (BoxPlot). The time readout (when shown, first row) tops the stack and the
|
|
145
152
|
* staffs start just below it. */
|
|
@@ -181,13 +188,29 @@ function buildFlagCursor(o) {
|
|
|
181
188
|
},
|
|
182
189
|
wants: { ...NO_WANTS, samples: true, flags: true, time: o.showTime },
|
|
183
190
|
ownsGesture: false,
|
|
191
|
+
format: o.format,
|
|
184
192
|
};
|
|
185
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* The sample the snapping crosshair centres on in a row: the one nearest the
|
|
196
|
+
* pointer y in the hovered row — or the first sample when nothing is hovered
|
|
197
|
+
* (a pinned tracker shows a reticle in every row). `null` in a row that isn't
|
|
198
|
+
* hovered while another is, or when there is nothing to snap to.
|
|
199
|
+
*/
|
|
200
|
+
function snappedSample(f) {
|
|
201
|
+
const samples = f.reticleSamples;
|
|
202
|
+
if (inBoundsX(f) === null || samples.length === 0)
|
|
203
|
+
return null;
|
|
204
|
+
const cy = f.cursorY;
|
|
205
|
+
if (f.hoveredRowKey === f.rowKey && cy !== null) {
|
|
206
|
+
return samples.reduce((a, b) => Math.abs(b.py - cy) < Math.abs(a.py - cy) ? b : a);
|
|
207
|
+
}
|
|
208
|
+
return f.hoveredRowKey === null ? samples[0] : null;
|
|
209
|
+
}
|
|
186
210
|
/**
|
|
187
211
|
* The crosshair's single reticle centre for a row: with `snap` (default) the
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
* mode reads the container-resolved raw-pointer measurement.
|
|
212
|
+
* {@link snappedSample}; free mode reads the container-resolved raw-pointer
|
|
213
|
+
* measurement.
|
|
191
214
|
*
|
|
192
215
|
* It carries the picked sample's **axis placement** (side + gutter offset +
|
|
193
216
|
* axis ink) as well as its value, because the reticle reads one series and its
|
|
@@ -199,16 +222,8 @@ function crosshairPick(f, snap) {
|
|
|
199
222
|
if (inBoundsX(f) === null)
|
|
200
223
|
return null;
|
|
201
224
|
if (!snap)
|
|
202
|
-
return f.pointer;
|
|
203
|
-
|
|
204
|
-
return null;
|
|
205
|
-
const hoveredRow = f.hoveredRowKey === f.rowKey;
|
|
206
|
-
const cy = f.cursorY;
|
|
207
|
-
const pick = hoveredRow && cy !== null
|
|
208
|
-
? f.samples.reduce((a, b) => Math.abs(b.py - cy) < Math.abs(a.py - cy) ? b : a)
|
|
209
|
-
: f.hoveredRowKey === null
|
|
210
|
-
? f.samples[0]
|
|
211
|
-
: null;
|
|
225
|
+
return f.pointer && { ...f.pointer, color: undefined };
|
|
226
|
+
const pick = snappedSample(f);
|
|
212
227
|
return pick
|
|
213
228
|
? {
|
|
214
229
|
py: pick.py,
|
|
@@ -216,10 +231,60 @@ function crosshairPick(f, snap) {
|
|
|
216
231
|
side: pick.side,
|
|
217
232
|
axisOffset: pick.axisOffset,
|
|
218
233
|
axisColor: pick.axisColor,
|
|
234
|
+
color: pick.color,
|
|
219
235
|
}
|
|
220
236
|
: null;
|
|
221
237
|
}
|
|
222
|
-
/**
|
|
238
|
+
/** The consumer-facing {@link CursorSnap} for a resolved sample. */
|
|
239
|
+
function toCursorSnap(s) {
|
|
240
|
+
return {
|
|
241
|
+
x: s.x,
|
|
242
|
+
value: s.value,
|
|
243
|
+
color: s.color,
|
|
244
|
+
label: s.label,
|
|
245
|
+
...(s.readout !== undefined ? { readout: s.readout } : {}),
|
|
246
|
+
axisId: s.axisId,
|
|
247
|
+
formatted: s.formatted,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
/** A {@link CursorSnap}'s identity for change detection — every field, so a
|
|
251
|
+
* new axis format under a still pointer (same point, new `formatted`) is a
|
|
252
|
+
* change the consumer hears about too. */
|
|
253
|
+
function snapKey(s) {
|
|
254
|
+
return s === null
|
|
255
|
+
? null
|
|
256
|
+
: [s.axisId, s.label, s.x, s.value, s.readout, s.color, s.formatted].join('\u0000');
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* The crosshair's snap reporter: picks the snapped sample from a hovered row's
|
|
260
|
+
* frame (`null` = nothing snapped) and calls the consumer only when the pick
|
|
261
|
+
* **changes** — rows re-render on every pointer move, and a callback that fired
|
|
262
|
+
* per move while the reticle sat still would make every consumer dedupe.
|
|
263
|
+
*
|
|
264
|
+
* Its state belongs to the mounted `<CrosshairCursor>`, not to this reporter:
|
|
265
|
+
* the reporter is rebuilt whenever `snap` / `showTime` / `format` change, and
|
|
266
|
+
* a fresh "last reported" would swallow the `null` that toggling to free mode
|
|
267
|
+
* owes a consumer still holding a point. `live` goes false on unmount, so a
|
|
268
|
+
* row still holding the old registration for one more commit can't report a
|
|
269
|
+
* point after the unmount's final `null`. The callback is read through a ref
|
|
270
|
+
* so an inline `onSnap` doesn't rebuild the cursor spec on every parent render.
|
|
271
|
+
*/
|
|
272
|
+
function snapReporter(onSnapRef, state, snap) {
|
|
273
|
+
return (f) => {
|
|
274
|
+
const st = state.current;
|
|
275
|
+
if (!st.live)
|
|
276
|
+
return;
|
|
277
|
+
// Free mode follows the pointer, not a series: there is nothing to report.
|
|
278
|
+
const pick = f !== null && snap ? snappedSample(f) : null;
|
|
279
|
+
const info = pick === null ? null : toCursorSnap(pick);
|
|
280
|
+
const key = snapKey(info);
|
|
281
|
+
if (key === st.last)
|
|
282
|
+
return;
|
|
283
|
+
st.last = key;
|
|
284
|
+
onSnapRef.current?.(info);
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
/** `<CrosshairCursor>` as a spec: the dashed reticle (renderPlot), the axis
|
|
223
288
|
* value pill (renderYGutter), and the x-axis time pill (renderXAxis). Declares
|
|
224
289
|
* `snapX: 'sample'` — the container snaps the shared cursorX to the data grid. */
|
|
225
290
|
function buildCrosshairCursor(o) {
|
|
@@ -233,7 +298,7 @@ function buildCrosshairCursor(o) {
|
|
|
233
298
|
const ink = cursorInk(f.theme);
|
|
234
299
|
const background = f.theme.background;
|
|
235
300
|
const reticle = crosshairPick(f, o.snap);
|
|
236
|
-
return (_jsxs(_Fragment, { children: [_jsx("line", { x1: Math.round(x), y1: 0, x2: Math.round(x), y2: f.rowHeight, stroke: ink, strokeWidth: 1, strokeDasharray: "3 3", shapeRendering: "crispEdges" }), reticle && (_jsxs(_Fragment, { children: [_jsx("line", { x1: 0, y1: Math.round(reticle.py), x2: f.plotWidth, y2: Math.round(reticle.py), stroke: ink, strokeWidth: 1, strokeDasharray: "3 3", shapeRendering: "crispEdges" }), _jsx("circle", { cx: x, cy: reticle.py, r: 3, fill: ink, stroke: background, strokeWidth: background ? 1 : 0 })] }))] }));
|
|
301
|
+
return (_jsxs(_Fragment, { children: [_jsx("line", { x1: Math.round(x), y1: 0, x2: Math.round(x), y2: f.rowHeight, stroke: ink, strokeWidth: 1, strokeDasharray: "3 3", shapeRendering: "crispEdges" }), reticle && (_jsxs(_Fragment, { children: [_jsx("line", { x1: 0, y1: Math.round(reticle.py), x2: f.plotWidth, y2: Math.round(reticle.py), stroke: ink, strokeWidth: 1, strokeDasharray: "3 3", shapeRendering: "crispEdges" }), _jsx("circle", { cx: x, cy: reticle.py, r: 3, fill: reticle.color ?? ink, stroke: background, strokeWidth: background ? 1 : 0 })] }))] }));
|
|
237
302
|
},
|
|
238
303
|
// The value pill goes **on the reticle's own axis**: its side, its offset
|
|
239
304
|
// out into that gutter (so a second axis on a side gets its own pill
|
|
@@ -287,12 +352,13 @@ function buildCrosshairCursor(o) {
|
|
|
287
352
|
}
|
|
288
353
|
: {}),
|
|
289
354
|
},
|
|
290
|
-
wants: { ...NO_WANTS,
|
|
355
|
+
wants: { ...NO_WANTS, reticle: true, pointer: !o.snap },
|
|
291
356
|
ownsGesture: true,
|
|
292
357
|
format: o.format,
|
|
358
|
+
reportSnap: o.reportSnap,
|
|
293
359
|
};
|
|
294
360
|
}
|
|
295
|
-
/**
|
|
361
|
+
/** `<RangeCursor>` as a spec: the hover-time **band** — the bucket under the
|
|
296
362
|
* pointer (sequence-snapped; freeform = a plain line until a drag shades the
|
|
297
363
|
* raw span) — plus the drag registration the brush recognizer reads
|
|
298
364
|
* (`resolveRangeDrag`). The container resolves the band; the shared
|
|
@@ -320,10 +386,7 @@ function buildRangeCursor(o) {
|
|
|
320
386
|
* Update-in-place on a prop change (the entry memo), unregister on unmount —
|
|
321
387
|
* the `registerAxis` discipline.
|
|
322
388
|
*/
|
|
323
|
-
function useCursorMount(built
|
|
324
|
-
/** The container shim's un-asked-for `'line'` default (see
|
|
325
|
-
* {@link CursorEntry.implicit}) — never set by component mounts. */
|
|
326
|
-
implicit = false) {
|
|
389
|
+
function useCursorMount(built) {
|
|
327
390
|
const container = useContext(ContainerContext);
|
|
328
391
|
if (container === null) {
|
|
329
392
|
throw new Error('cursor components must be mounted inside a <ChartContainer> (as a ' +
|
|
@@ -343,10 +406,9 @@ implicit = false) {
|
|
|
343
406
|
onDragRelease: built.onDragRelease,
|
|
344
407
|
enableDrag: built.enableDrag,
|
|
345
408
|
dragModifier: built.dragModifier,
|
|
409
|
+
reportSnap: built.reportSnap,
|
|
346
410
|
rowKey,
|
|
347
|
-
|
|
348
|
-
...(implicit ? { implicit } : {}),
|
|
349
|
-
}, [built, rowKey, legacy, implicit]);
|
|
411
|
+
}, [built, rowKey]);
|
|
350
412
|
const { registerCursor, unregisterCursor } = container;
|
|
351
413
|
useEffect(() => {
|
|
352
414
|
if (entry === null) {
|
|
@@ -357,98 +419,100 @@ implicit = false) {
|
|
|
357
419
|
}, [registerCursor, unregisterCursor, key, entry]);
|
|
358
420
|
useEffect(() => () => unregisterCursor(key), [unregisterCursor, key]);
|
|
359
421
|
}
|
|
360
|
-
/** The synced vertical cursor **line
|
|
361
|
-
*
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
useCursorMount(useMemo(() => buildLineCursor({ showTime }), [showTime]), false);
|
|
422
|
+
/** The synced vertical cursor **line**. Pair with an off-chart readout via
|
|
423
|
+
* `onTrackerChanged`. */
|
|
424
|
+
export function LineCursor({ showTime = false, format } = {}) {
|
|
425
|
+
useCursorMount(useMemo(() => buildLineCursor({ showTime, format }), [showTime, format]));
|
|
365
426
|
return null;
|
|
366
427
|
}
|
|
367
|
-
/** A **dot on each series** at the cursor, no line
|
|
368
|
-
export function PointCursor({ showTime = false } = {}) {
|
|
369
|
-
useCursorMount(useMemo(() => buildPointCursor({ showTime }), [showTime
|
|
428
|
+
/** A **dot on each series** at the cursor, no line. */
|
|
429
|
+
export function PointCursor({ showTime = false, format, } = {}) {
|
|
430
|
+
useCursorMount(useMemo(() => buildPointCursor({ showTime, format }), [showTime, format]));
|
|
370
431
|
return null;
|
|
371
432
|
}
|
|
372
|
-
/** Dots **plus a value chip beside each
|
|
373
|
-
export function InlineCursor({ showTime = false } = {}) {
|
|
374
|
-
useCursorMount(useMemo(() => buildInlineCursor({ showTime }), [showTime
|
|
433
|
+
/** Dots **plus a value chip beside each**. */
|
|
434
|
+
export function InlineCursor({ showTime = false, format, } = {}) {
|
|
435
|
+
useCursorMount(useMemo(() => buildInlineCursor({ showTime, format }), [showTime, format]));
|
|
375
436
|
return null;
|
|
376
437
|
}
|
|
377
|
-
/** Dots + **staffed value flags** stacked near the top of the row —
|
|
378
|
-
*
|
|
379
|
-
export function FlagCursor({ showTime = false } = {}) {
|
|
380
|
-
useCursorMount(useMemo(() => buildFlagCursor({ showTime }), [showTime
|
|
438
|
+
/** Dots + **staffed value flags** stacked near the top of the row — a `cursorFlag` layer (BoxPlot)
|
|
439
|
+
* consolidates onto one flag. */
|
|
440
|
+
export function FlagCursor({ showTime = false, format } = {}) {
|
|
441
|
+
useCursorMount(useMemo(() => buildFlagCursor({ showTime, format }), [showTime, format]));
|
|
381
442
|
return null;
|
|
382
443
|
}
|
|
383
|
-
/** The inspection **reticle
|
|
384
|
-
* centre dot
|
|
385
|
-
|
|
386
|
-
|
|
444
|
+
/** The inspection **reticle**: dashed cross lines, a
|
|
445
|
+
* centre dot in the snapped series' colour, the value pinned to its y axis,
|
|
446
|
+
* the time pinned to the x axis. */
|
|
447
|
+
export function CrosshairCursor({ snap = true, showTime = true, format, onSnap, } = {}) {
|
|
448
|
+
// Latest callback in a ref, so an inline `onSnap` doesn't re-register the
|
|
449
|
+
// cursor on every parent render; the spec is rebuilt only when a callback
|
|
450
|
+
// appears or goes away. Written in a *layout* effect: the rows report from
|
|
451
|
+
// passive effects, and every layout effect in a commit runs before any
|
|
452
|
+
// passive one — so a row always calls this commit's callback, wherever the
|
|
453
|
+
// cursor sits in the tree relative to the rows.
|
|
454
|
+
const onSnapRef = useRef(onSnap);
|
|
455
|
+
useLayoutEffect(() => {
|
|
456
|
+
onSnapRef.current = onSnap;
|
|
457
|
+
});
|
|
458
|
+
// What the consumer was last told, shared across reporter rebuilds (see
|
|
459
|
+
// `snapReporter`).
|
|
460
|
+
const reportState = useRef({ last: null, live: true });
|
|
461
|
+
const listening = onSnap !== undefined;
|
|
462
|
+
// A consumer who stops listening is owed nothing more; a fresh listener
|
|
463
|
+
// starts from "nothing reported yet".
|
|
464
|
+
if (!listening)
|
|
465
|
+
reportState.current.last = null;
|
|
466
|
+
// Unmounting while snapped lets go of the point, the same as the pointer
|
|
467
|
+
// leaving would. (`live` is re-set on mount for StrictMode's
|
|
468
|
+
// unmount-remount.)
|
|
469
|
+
useEffect(() => {
|
|
470
|
+
const st = reportState.current;
|
|
471
|
+
st.live = true;
|
|
472
|
+
return () => {
|
|
473
|
+
st.live = false;
|
|
474
|
+
if (st.last === null)
|
|
475
|
+
return;
|
|
476
|
+
st.last = null;
|
|
477
|
+
onSnapRef.current?.(null);
|
|
478
|
+
};
|
|
479
|
+
}, []);
|
|
480
|
+
useCursorMount(useMemo(() => buildCrosshairCursor({
|
|
481
|
+
snap,
|
|
482
|
+
showTime,
|
|
483
|
+
format,
|
|
484
|
+
reportSnap: listening
|
|
485
|
+
? snapReporter(onSnapRef, reportState, snap)
|
|
486
|
+
: undefined,
|
|
487
|
+
}), [snap, showTime, format, listening]));
|
|
387
488
|
return null;
|
|
388
489
|
}
|
|
389
490
|
/**
|
|
390
|
-
* The **range** cursor
|
|
491
|
+
* The **range** cursor (RFC A4.1 names
|
|
391
492
|
* it for what it emits: a live extent — and, dragged, exactly what
|
|
392
493
|
* `ChartContainer.range` accepts — against the annotation `<Region>`'s fixed
|
|
393
|
-
* mark). Hover shades the bucket under the pointer
|
|
494
|
+
* mark). Hover shades the bucket under the pointer (on a category axis, the
|
|
495
|
+
* slot); wiring
|
|
394
496
|
* {@link RangeCursorProps.onDragRelease} adds the drag, which fires once on
|
|
395
497
|
* release and reverts (RFC §6: a region is deliberately a cursor **and** a
|
|
396
498
|
* drag that fires and resets). The gesture rides the shared brush recognizer
|
|
397
499
|
* (`brush.tsx`) — one engine arbitrating every drag claim on the plot.
|
|
398
500
|
*/
|
|
399
501
|
export function RangeCursor({ sequence, onDragRelease, enableDrag, dragModifier, } = {}) {
|
|
400
|
-
useCursorMount(useMemo(() => buildRangeCursor({ sequence, onDragRelease, enableDrag, dragModifier }), [sequence, onDragRelease, enableDrag, dragModifier])
|
|
502
|
+
useCursorMount(useMemo(() => buildRangeCursor({ sequence, onDragRelease, enableDrag, dragModifier }), [sequence, onDragRelease, enableDrag, dragModifier]));
|
|
401
503
|
return null;
|
|
402
504
|
}
|
|
403
|
-
/**
|
|
404
|
-
* The deprecation shim (internal): synthesizes the preset equivalent of a
|
|
405
|
-
* legacy `cursor` string — the container's `cursor` prop (or its `'line'`
|
|
406
|
-
* default), and `<ChartRow cursor>` inside a row. Registers as `legacy`, so a
|
|
407
|
-
* component-mounted cursor in the same scope overrides it.
|
|
408
|
-
*/
|
|
409
|
-
export function LegacyCursor({ mode, showTime, snap, sequence, implicit = false, }) {
|
|
410
|
-
const built = useMemo(() => {
|
|
411
|
-
switch (mode) {
|
|
412
|
-
case 'line':
|
|
413
|
-
return buildLineCursor({ showTime });
|
|
414
|
-
case 'point':
|
|
415
|
-
return buildPointCursor({ showTime });
|
|
416
|
-
case 'inline':
|
|
417
|
-
return buildInlineCursor({ showTime });
|
|
418
|
-
case 'flag':
|
|
419
|
-
return buildFlagCursor({ showTime });
|
|
420
|
-
case 'crosshair':
|
|
421
|
-
// The legacy crosshair always pins the time to the x axis; its y-snap
|
|
422
|
-
// is the container's `crosshairSnap`. (`cursorTime` is deliberately
|
|
423
|
-
// NOT forwarded — crosshair has no per-row time chip.)
|
|
424
|
-
return buildCrosshairCursor({ snap, showTime: true });
|
|
425
|
-
case 'region':
|
|
426
|
-
return buildRangeCursor({ sequence });
|
|
427
|
-
case 'none':
|
|
428
|
-
return null;
|
|
429
|
-
}
|
|
430
|
-
}, [mode, showTime, snap, sequence]);
|
|
431
|
-
useCursorMount(built, true, implicit);
|
|
432
|
-
return null;
|
|
433
|
-
}
|
|
434
|
-
/** Drop a scope's legacy (shim-synthesized) entries when the scope also has a
|
|
435
|
-
* component-mounted cursor — mounting a component overrides the string prop. */
|
|
436
|
-
function dropShadowedLegacy(entries) {
|
|
437
|
-
return entries.some((e) => !e.legacy)
|
|
438
|
-
? entries.filter((e) => !e.legacy)
|
|
439
|
-
: entries;
|
|
440
|
-
}
|
|
441
505
|
/**
|
|
442
506
|
* The cursors in effect for a row: the row's own mounts when it has any (the
|
|
443
|
-
* per-row override — nearest mount wins,
|
|
444
|
-
*
|
|
445
|
-
*
|
|
507
|
+
* per-row override — nearest mount wins), else the container-scoped mounts.
|
|
508
|
+
* A row that wants no cursor while its siblings have one mounts the cursors
|
|
509
|
+
* per row rather than at the container.
|
|
446
510
|
*/
|
|
447
511
|
export function effectiveCursorEntries(all, rowKey) {
|
|
448
512
|
const rowEntries = all.filter((e) => e.rowKey === rowKey);
|
|
449
513
|
if (rowEntries.length > 0)
|
|
450
|
-
return
|
|
451
|
-
return
|
|
514
|
+
return rowEntries;
|
|
515
|
+
return all.filter((e) => e.rowKey === null);
|
|
452
516
|
}
|
|
453
517
|
/** The scope's single snap/gesture owner (RFC A2.5) — first mount wins; the
|
|
454
518
|
* container dev-warns when a scope has two. */
|
|
@@ -467,7 +531,7 @@ export function xAxisCursorEntries(all, hoveredRowKey) {
|
|
|
467
531
|
return effectiveCursorEntries(all, hoveredRowKey);
|
|
468
532
|
const out = [];
|
|
469
533
|
const seenRows = new Set();
|
|
470
|
-
out.push(...
|
|
534
|
+
out.push(...all.filter((e) => e.rowKey === null));
|
|
471
535
|
for (const e of all) {
|
|
472
536
|
if (e.rowKey === null || seenRows.has(e.rowKey))
|
|
473
537
|
continue;
|
|
@@ -486,7 +550,7 @@ export function warnOnDuplicateGestureOwners(all, warned) {
|
|
|
486
550
|
return;
|
|
487
551
|
const scopes = new Set(all.map((e) => e.rowKey));
|
|
488
552
|
for (const scope of scopes) {
|
|
489
|
-
const entries =
|
|
553
|
+
const entries = all.filter((e) => e.rowKey === scope);
|
|
490
554
|
if (entries.filter((e) => e.ownsGesture).length > 1) {
|
|
491
555
|
warned.current = true;
|
|
492
556
|
console.warn('[pond-charts] two gesture-owning cursors (<CrosshairCursor> / ' +
|
|
@@ -498,31 +562,4 @@ export function warnOnDuplicateGestureOwners(all, warned) {
|
|
|
498
562
|
}
|
|
499
563
|
}
|
|
500
564
|
}
|
|
501
|
-
/** @internal The dev deprecation notice for a legacy cursor prop — one line
|
|
502
|
-
* naming the replacement, shared by the container and row shims. */
|
|
503
|
-
export function legacyCursorWarning(lines) {
|
|
504
|
-
return ('[pond-charts] deprecated cursor props (they keep working this minor, ' +
|
|
505
|
-
'removed next): ' +
|
|
506
|
-
lines.join('; ') +
|
|
507
|
-
'. Mount a cursor component instead (docs/rfcs/interaction.md §9).');
|
|
508
|
-
}
|
|
509
|
-
/** @internal The preset name a legacy `cursor` mode maps to (for warnings). */
|
|
510
|
-
export function presetNameFor(mode) {
|
|
511
|
-
switch (mode) {
|
|
512
|
-
case 'line':
|
|
513
|
-
return '<LineCursor>';
|
|
514
|
-
case 'point':
|
|
515
|
-
return '<PointCursor>';
|
|
516
|
-
case 'inline':
|
|
517
|
-
return '<InlineCursor>';
|
|
518
|
-
case 'flag':
|
|
519
|
-
return '<FlagCursor>';
|
|
520
|
-
case 'crosshair':
|
|
521
|
-
return '<CrosshairCursor>';
|
|
522
|
-
case 'region':
|
|
523
|
-
return '<RangeCursor>';
|
|
524
|
-
case 'none':
|
|
525
|
-
return 'nothing (mount no cursor)';
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
565
|
//# sourceMappingURL=cursors.js.map
|
package/dist/decimate.d.ts
CHANGED
|
@@ -224,8 +224,17 @@ export declare function m4Polyline(edges: Float64Array, mn: Float64Array, mx: Fl
|
|
|
224
224
|
* `upper` are finite **together** per sample (the paired-percentile shape bands
|
|
225
225
|
* are built from); a column where only one edge has finite samples would bin a
|
|
226
226
|
* band segment that no single sample carried.
|
|
227
|
+
*
|
|
228
|
+
* `boundaries` are trading-axis session-break instants (`<BandChart
|
|
229
|
+
* sessionBreaks>`), handled exactly as {@link decimateM4} does for a line: each
|
|
230
|
+
* in-domain instant is unioned into the bucket edges so no column merges two
|
|
231
|
+
* sessions' envelopes across the discontinuity, **and** a `NaN` sample is
|
|
232
|
+
* emitted at the instant so the fill ends at the close and re-starts at the
|
|
233
|
+
* open — otherwise the closing and opening columns would sit as adjacent finite
|
|
234
|
+
* samples and the envelope would flow straight across the collapsed gap. The
|
|
235
|
+
* caller's `sessionRuns` then sees the break baked in and passes no boundaries.
|
|
227
236
|
*/
|
|
228
|
-
export declare function decimateBand(band: BandSeries, xScale: Scale, ctx: CanvasRenderingContext2D, k?: number): BandSeries;
|
|
237
|
+
export declare function decimateBand(band: BandSeries, xScale: Scale, ctx: CanvasRenderingContext2D, k?: number, boundaries?: readonly number[]): BandSeries;
|
|
229
238
|
/**
|
|
230
239
|
* Decimate an {@link OhlcSeries} to one **aggregate candle per device-pixel
|
|
231
240
|
* column** — `open = first`, `high = max`, `low = min`, `close = last` over the
|
package/dist/decimate.js
CHANGED
|
@@ -400,8 +400,17 @@ export function m4Polyline(edges, mn, mx, first, last, W, breakAt = NO_BREAKS) {
|
|
|
400
400
|
* `upper` are finite **together** per sample (the paired-percentile shape bands
|
|
401
401
|
* are built from); a column where only one edge has finite samples would bin a
|
|
402
402
|
* band segment that no single sample carried.
|
|
403
|
+
*
|
|
404
|
+
* `boundaries` are trading-axis session-break instants (`<BandChart
|
|
405
|
+
* sessionBreaks>`), handled exactly as {@link decimateM4} does for a line: each
|
|
406
|
+
* in-domain instant is unioned into the bucket edges so no column merges two
|
|
407
|
+
* sessions' envelopes across the discontinuity, **and** a `NaN` sample is
|
|
408
|
+
* emitted at the instant so the fill ends at the close and re-starts at the
|
|
409
|
+
* open — otherwise the closing and opening columns would sit as adjacent finite
|
|
410
|
+
* samples and the envelope would flow straight across the collapsed gap. The
|
|
411
|
+
* caller's `sessionRuns` then sees the break baked in and passes no boundaries.
|
|
403
412
|
*/
|
|
404
|
-
export function decimateBand(band, xScale, ctx, k = 2) {
|
|
413
|
+
export function decimateBand(band, xScale, ctx, k = 2, boundaries = []) {
|
|
405
414
|
if (!shouldDecimateCount(band.length, ctx, k))
|
|
406
415
|
return band;
|
|
407
416
|
const dom = scaleDomain(xScale);
|
|
@@ -412,18 +421,50 @@ export function decimateBand(band, xScale, ctx, k = 2) {
|
|
|
412
421
|
if (invert === null || plotWidthCss === null)
|
|
413
422
|
return band;
|
|
414
423
|
const W = deviceBucketCount(ctx);
|
|
415
|
-
const
|
|
424
|
+
const pixels = pixelEdges(invert, plotWidthCss, W);
|
|
425
|
+
// Session-break instants inside the visible domain — unioned into the edges
|
|
426
|
+
// (so a column never straddles a break) AND marked as explicit break samples.
|
|
427
|
+
// `mergeGapEdges` keeps their exact values, so the set matches the edges.
|
|
428
|
+
const breaks = boundaries.length > 0
|
|
429
|
+
? boundaries.filter((b) => b > dom[0] && b < dom[1])
|
|
430
|
+
: [];
|
|
431
|
+
const edges = breaks.length > 0 ? mergeGapEdges(pixels, breaks, dom[0], dom[1]) : pixels;
|
|
432
|
+
const buckets = edges.length - 1;
|
|
416
433
|
const lowerMin = new Float64Column(band.lower, band.length).binBy(band.x, edges, 'min');
|
|
417
434
|
const upperMax = new Float64Column(band.upper, band.length).binBy(band.x, edges, 'max');
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
435
|
+
const breakAt = breaks.length > 0 ? new Set(breaks) : null;
|
|
436
|
+
// One sample per column + one NaN break slot per session break. `breaks` are
|
|
437
|
+
// strictly inside the domain and `mergeGapEdges` keeps every one, so each
|
|
438
|
+
// lands on some `edges[b]` with `b > 0` and the arrays fill exactly; the
|
|
439
|
+
// subarray trim below is a guard against a provider instant that sorts ahead
|
|
440
|
+
// of the first pixel edge under float rounding, not an expected path.
|
|
441
|
+
const cap = buckets + (breakAt === null ? 0 : breakAt.size);
|
|
442
|
+
const x = new Float64Array(cap);
|
|
443
|
+
const lower = new Float64Array(cap);
|
|
444
|
+
const upper = new Float64Array(cap);
|
|
445
|
+
let n = 0;
|
|
446
|
+
for (let b = 0; b < buckets; b += 1) {
|
|
447
|
+
// Explicit session break: this column opens a new session → end the fill
|
|
448
|
+
// first. A NaN on both edges is the band's own gap signal (`.defined`).
|
|
449
|
+
if (breakAt !== null && b > 0 && breakAt.has(edges[b])) {
|
|
450
|
+
x[n] = edges[b];
|
|
451
|
+
lower[n] = NaN;
|
|
452
|
+
upper[n] = NaN;
|
|
453
|
+
n += 1;
|
|
454
|
+
}
|
|
455
|
+
x[n] = (edges[b] + edges[b + 1]) / 2; // column centre
|
|
456
|
+
lower[n] = lowerMin[b]; // NaN on an empty column → the fill break
|
|
457
|
+
upper[n] = upperMax[b];
|
|
458
|
+
n += 1;
|
|
425
459
|
}
|
|
426
|
-
return
|
|
460
|
+
return n === cap
|
|
461
|
+
? { x, lower, upper, length: n }
|
|
462
|
+
: {
|
|
463
|
+
x: x.subarray(0, n),
|
|
464
|
+
lower: lower.subarray(0, n),
|
|
465
|
+
upper: upper.subarray(0, n),
|
|
466
|
+
length: n,
|
|
467
|
+
};
|
|
427
468
|
}
|
|
428
469
|
/**
|
|
429
470
|
* Decimate an {@link OhlcSeries} to one **aggregate candle per device-pixel
|
package/dist/elapsed.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Two things change, and only these two: **where the ticks sit** and **what they
|
|
8
8
|
* say**. The pixel mapping is untouched, and so are the data coordinates — a
|
|
9
|
-
* mark's `at`, the container's `range`,
|
|
9
|
+
* mark's `at`, the container's `range`, a `<RangeCursor onDragRelease>` span are all still
|
|
10
10
|
* absolute axis units. Relabeling only.
|
|
11
11
|
*
|
|
12
12
|
* Where the ticks sit is the part that can't be done with `<XAxis transform>`:
|
package/dist/elapsed.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Two things change, and only these two: **where the ticks sit** and **what they
|
|
8
8
|
* say**. The pixel mapping is untouched, and so are the data coordinates — a
|
|
9
|
-
* mark's `at`, the container's `range`,
|
|
9
|
+
* mark's `at`, the container's `range`, a `<RangeCursor onDragRelease>` span are all still
|
|
10
10
|
* absolute axis units. Relabeling only.
|
|
11
11
|
*
|
|
12
12
|
* Where the ticks sit is the part that can't be done with `<XAxis transform>`:
|
package/dist/format.d.ts
CHANGED
|
@@ -15,7 +15,8 @@ import type { TimeGrain } from './tickLadder.js';
|
|
|
15
15
|
export type AxisFormat = string | ((value: number) => string);
|
|
16
16
|
/**
|
|
17
17
|
* How to format the **cursor / marker readout** on the x axis
|
|
18
|
-
* (
|
|
18
|
+
* (the `format` prop on a cursor such as `<CrosshairCursor>`) — time or
|
|
19
|
+
* value kind. Either:
|
|
19
20
|
*
|
|
20
21
|
* - a d3 specifier **string** applied uniformly: a [time specifier]
|
|
21
22
|
* (https://github.com/d3/d3-time-format#locale_format) on a time axis
|
package/dist/index.d.ts
CHANGED
|
@@ -94,7 +94,7 @@ export { LineCursor, PointCursor, InlineCursor, FlagCursor, CrosshairCursor, Ran
|
|
|
94
94
|
export type { LineCursorProps, PointCursorProps, InlineCursorProps, FlagCursorProps, CrosshairCursorProps, RangeCursorProps, } from './cursors.js';
|
|
95
95
|
export { Selector, MultiSelector } from './selectors.js';
|
|
96
96
|
export type { SelectorProps, MultiSelectorProps } from './selectors.js';
|
|
97
|
-
export type {
|
|
97
|
+
export type { CursorSnap, TrackerInfo, TrackerSample, SelectInfo, SelectModifiers, RangeSpan, SpanSelection, SelectionEntry, } from './context.js';
|
|
98
98
|
export { selectionContains, isSpanSelection, sameMark } from './span.js';
|
|
99
99
|
export type { DrawStatsFrame, LayerDrawInfo } from './context.js';
|
|
100
100
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -19,10 +19,11 @@ import type { SelectInfo } from './context.js';
|
|
|
19
19
|
*
|
|
20
20
|
* That is not hypothetical: half a built matrix already surfaced two defects —
|
|
21
21
|
* categorical hover being ink-only where the time axis claims the whole slot,
|
|
22
|
-
* and `<RangeCursor>` on an ordinal axis suppressing the row's cursor entirely
|
|
22
|
+
* and `<RangeCursor>` on an ordinal axis suppressing the row's cursor entirely
|
|
23
|
+
* (since fixed: it shades the slot there, [PND-ORDCURSOR]).
|
|
23
24
|
*
|
|
24
|
-
* **Capabilities are declared, not assumed.** `sequence`
|
|
25
|
-
*
|
|
25
|
+
* **Capabilities are declared, not assumed.** `sequence` is absent on
|
|
26
|
+
* fixtures that cannot support it, so the factory omits those
|
|
26
27
|
* stories rather than generating one that silently does nothing. A gap in the
|
|
27
28
|
* matrix is information; a story that renders nothing is a trap.
|
|
28
29
|
*/
|
|
@@ -72,10 +73,6 @@ export interface ChartFixture {
|
|
|
72
73
|
readonly sweep: boolean;
|
|
73
74
|
/** A snapping sequence, where the axis has one. Ordinal axes do not. */
|
|
74
75
|
readonly sequence?: () => Sequence | BoundedSequence;
|
|
75
|
-
/** Whether `<RangeCursor>` draws here — it gates on a continuous x
|
|
76
|
-
* (`brush.tsx`), so mounting one on a category axis is not just inert, it
|
|
77
|
-
* takes the row's cursor with it. */
|
|
78
|
-
readonly rangeCursor: boolean;
|
|
79
76
|
/**
|
|
80
77
|
* Present only where the x axis **collapses closed-market time**, which is
|
|
81
78
|
* what makes a snap block's relationship to the session grid a question at
|