@pond-ts/charts 0.70.0 → 0.72.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 +30 -31
- package/CHANGELOG.md +143 -1
- package/dist/AreaChart.d.ts +30 -22
- package/dist/AreaChart.js +37 -22
- 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/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/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/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/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
|
|
@@ -48,8 +48,6 @@ export const categoricalBars = {
|
|
|
48
48
|
picks: SERVICES.map((c) => ({ label: c.label, info: svc(c.label) })),
|
|
49
49
|
describe: (hit) => hit.mark ?? hit.label,
|
|
50
50
|
// No `sequence`: a time bucketing over ordinal slots is meaningless.
|
|
51
|
-
// No `rangeCursor`: it gates on a continuous x.
|
|
52
|
-
rangeCursor: false,
|
|
53
51
|
sweep: true,
|
|
54
52
|
};
|
|
55
53
|
// ── Time axis ──────────────────────────────────────────────────────────────
|
|
@@ -108,7 +106,6 @@ export const timeBars = {
|
|
|
108
106
|
* bar so the buckets line up with the data rather than with the epoch.
|
|
109
107
|
*/
|
|
110
108
|
sequence: () => Sequence.every('7d', { anchor: D0 }),
|
|
111
|
-
rangeCursor: true,
|
|
112
109
|
sweep: true,
|
|
113
110
|
};
|
|
114
111
|
// ── Stacked bars (many marks per bin) ──────────────────────────────────────
|
|
@@ -177,7 +174,6 @@ export const stackedBars = {
|
|
|
177
174
|
? isoDay(hit.key)
|
|
178
175
|
: `${isoDay(hit.key)}·${hit.label}`,
|
|
179
176
|
sequence: () => Sequence.every('7d', { anchor: D0 }),
|
|
180
|
-
rangeCursor: true,
|
|
181
177
|
sweep: true,
|
|
182
178
|
};
|
|
183
179
|
// ── Box plots (a mark with internal structure) ─────────────────────────────
|
|
@@ -258,7 +254,6 @@ const boxFixture = (name, shape, note) => ({
|
|
|
258
254
|
/** Three 10-minute boxes per bucket — coarser than the mark, so the snap has
|
|
259
255
|
* something to do (the lesson `timeBars` learned with its `'1d'`). */
|
|
260
256
|
sequence: () => Sequence.every('30m', { anchor: BOX_BASE }),
|
|
261
|
-
rangeCursor: true,
|
|
262
257
|
});
|
|
263
258
|
/** The default shape: a q1→q3 body with thin stems and end caps. The stems are
|
|
264
259
|
* the interesting part for selection — they are drawn ink far from the body. */
|
|
@@ -332,7 +327,6 @@ export const candles = {
|
|
|
332
327
|
describe: (hit) => isoDay(hit.key),
|
|
333
328
|
sweep: true,
|
|
334
329
|
sequence: () => Sequence.every('5d', { anchor: D0 }),
|
|
335
|
-
rangeCursor: true,
|
|
336
330
|
};
|
|
337
331
|
// ── The 2-D family: scatter and heat map ──────────────────────────────────
|
|
338
332
|
/** Forty points on a time axis — a scatter's marks live at `(x, y)`, not in a
|
|
@@ -402,7 +396,6 @@ export const scatterPoints = {
|
|
|
402
396
|
// A **2-D** sweep: the drag cuts a free rect, and the committed span carries
|
|
403
397
|
// a `y` window alongside `x` ([PND-INTERACT2D]).
|
|
404
398
|
sweep: true,
|
|
405
|
-
rangeCursor: true,
|
|
406
399
|
};
|
|
407
400
|
/**
|
|
408
401
|
* **A heat-map column** — a *grid* of cells, `(bin × row)`.
|
|
@@ -443,7 +436,6 @@ export const heatGrid = {
|
|
|
443
436
|
// A **2-D** sweep, snapped in both dimensions — bins on x, rows on y — so a
|
|
444
437
|
// capture is always a contiguous rectangle of cells ([PND-INTERACT2D]).
|
|
445
438
|
sweep: true,
|
|
446
|
-
rangeCursor: true,
|
|
447
439
|
};
|
|
448
440
|
// ── Trading sessions (a discontinuous time axis) ───────────────────────────
|
|
449
441
|
/** Six weekday sessions (09:30–16:00 UTC) from a Monday — five overnight seams
|
|
@@ -531,7 +523,6 @@ export const tradingSessions = {
|
|
|
531
523
|
// the same bucketing `sessions.conforming` names, so the generated story and
|
|
532
524
|
// the session pair below agree about what "conforming" means.
|
|
533
525
|
sequence: () => sessionSeq(SESSIONS),
|
|
534
|
-
rangeCursor: true,
|
|
535
526
|
sweep: true,
|
|
536
527
|
sessions: {
|
|
537
528
|
conforming: () => sessionSeq(SESSIONS),
|
|
@@ -9,15 +9,13 @@ import { type ChartFixture } from './selection-fixtures.js';
|
|
|
9
9
|
* to write the story. That is what makes walking the matrix a review technique
|
|
10
10
|
* rather than a gallery — see CLAUDE.md → "Storybook stories".
|
|
11
11
|
*
|
|
12
|
-
* Stories whose subject is *not* chart-type dependent
|
|
13
|
-
*
|
|
14
|
-
* every fixture would test nothing new.
|
|
12
|
+
* Stories whose subject is *not* chart-type dependent deliberately live in one
|
|
13
|
+
* column only; running them against every fixture would test nothing new.
|
|
15
14
|
*/
|
|
16
15
|
type Story = StoryObj;
|
|
17
16
|
/**
|
|
18
|
-
* The `<Selector>` feature set for one chart type
|
|
19
|
-
*
|
|
20
|
-
* and mounting one there costs the row its cursor entirely.
|
|
17
|
+
* The `<Selector>` feature set for one chart type, each with a `<RangeCursor>`
|
|
18
|
+
* mounted (on an ordinal axis it shades the slot under the pointer).
|
|
21
19
|
*/
|
|
22
20
|
export interface SelectorStories {
|
|
23
21
|
MountedAtContainer: Story;
|
|
@@ -6,7 +6,7 @@ import { Layers } from './Layers.js';
|
|
|
6
6
|
import { YAxis } from './YAxis.js';
|
|
7
7
|
import { Selector, MultiSelector } from './selectors.js';
|
|
8
8
|
import { isSpanSelection, sameMark, selectionContains } from './span.js';
|
|
9
|
-
import { RangeCursor } from './cursors.js';
|
|
9
|
+
import { LineCursor, RangeCursor } from './cursors.js';
|
|
10
10
|
import { caption } from './selection-fixtures.js';
|
|
11
11
|
const H = 240;
|
|
12
12
|
/**
|
|
@@ -23,7 +23,7 @@ function Chart({ fx, children, height = H, selector, ...container }) {
|
|
|
23
23
|
}
|
|
24
24
|
const list = (fx, sel) => sel.map((m) => fx.describe(m)).join(', ') || '—';
|
|
25
25
|
export function makeSelectorStories(fx) {
|
|
26
|
-
const Cursor = () =>
|
|
26
|
+
const Cursor = () => _jsx(RangeCursor, {});
|
|
27
27
|
return {
|
|
28
28
|
/** **Mounted at the container** — the ordinary case: one `<Selector>` as a
|
|
29
29
|
* child of `<ChartContainer>`, enabling the click for every row. It
|
|
@@ -40,7 +40,7 @@ export function makeSelectorStories(fx) {
|
|
|
40
40
|
MountedInRow: {
|
|
41
41
|
render: function Render() {
|
|
42
42
|
const [sel, setSel] = useState([]);
|
|
43
|
-
return (_jsxs("div", { children: [_jsxs(ChartContainer, { width: 640, ...fx.container, children: [_jsxs(ChartRow, { height: 150, children: [_jsx(YAxis, { id: fx.axis.id, label: fx.axis.label || 'primary', min: fx.axis.min, max: fx.axis.max }), _jsx(Cursor, {}), _jsx(Selector, { selected: sel, onSelect: (h) => setSel(h === null ? [] : [h]), children: _jsx(Layers, { children: fx.renderLayer('svc') }) })] }), _jsxs(ChartRow, { height: 110, children: [_jsx(YAxis, { id: fx.secondary.axis.id, label: fx.secondary.axis.label, min: fx.secondary.axis.min, max: fx.secondary.axis.max }), _jsx(Layers, { children: fx.secondary.renderLayer('err') })] })] }), _jsxs("p", { style: caption, children: ["The top row selects; the bottom row has no", ' ', _jsx("code", { children: "<Selector>" }), " in scope. ", _jsx("strong", { children: "selected:" }), ' ', list(fx, sel)] })] }));
|
|
43
|
+
return (_jsxs("div", { children: [_jsxs(ChartContainer, { width: 640, ...fx.container, children: [_jsx(LineCursor, {}), _jsxs(ChartRow, { height: 150, children: [_jsx(YAxis, { id: fx.axis.id, label: fx.axis.label || 'primary', min: fx.axis.min, max: fx.axis.max }), _jsx(Cursor, {}), _jsx(Selector, { selected: sel, onSelect: (h) => setSel(h === null ? [] : [h]), children: _jsx(Layers, { children: fx.renderLayer('svc') }) })] }), _jsxs(ChartRow, { height: 110, children: [_jsx(YAxis, { id: fx.secondary.axis.id, label: fx.secondary.axis.label, min: fx.secondary.axis.min, max: fx.secondary.axis.max }), _jsx(Layers, { children: fx.secondary.renderLayer('err') })] })] }), _jsxs("p", { style: caption, children: ["The top row selects; the bottom row has no", ' ', _jsx("code", { children: "<Selector>" }), " in scope. ", _jsx("strong", { children: "selected:" }), ' ', list(fx, sel)] })] }));
|
|
44
44
|
},
|
|
45
45
|
},
|
|
46
46
|
/** **No selector — the plot is inert.** An id-bearing, hit-testable layer
|
|
@@ -68,7 +68,7 @@ export function makeSelectorStories(fx) {
|
|
|
68
68
|
};
|
|
69
69
|
return (_jsxs("div", { children: [_jsx("div", { style: { marginBottom: 8 }, children: fx.picks.map((p) => (_jsx("button", { style: btn, onClick: () => setSel((cur) => cur.some((m) => sameMark(m, p.info))
|
|
70
70
|
? cur.filter((m) => !sameMark(m, p.info))
|
|
71
|
-
: [...cur, p.info]), children: p.label }, p.label))) }), _jsx(Chart, { fx: fx, selector: _jsx(Selector, { enabled: false, selected: sel }) }), _jsxs("p", { style: caption, children: ["The buttons drive the highlight; clicking the plot does nothing.", ' ', _jsx("strong", { children: "selected:" }), " ", list(fx, sel)] })] }));
|
|
71
|
+
: [...cur, p.info]), children: p.label }, p.label))) }), _jsx(Chart, { fx: fx, selector: _jsx(Selector, { enabled: false, selected: sel }), children: _jsx(LineCursor, {}) }), _jsxs("p", { style: caption, children: ["The buttons drive the highlight; clicking the plot does nothing.", ' ', _jsx("strong", { children: "selected:" }), " ", list(fx, sel)] })] }));
|
|
72
72
|
},
|
|
73
73
|
},
|
|
74
74
|
/** **The modifiers are reported; the policy is yours.** `onSelect`'s second
|