@pond-ts/charts 0.56.2 → 0.58.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.
Files changed (78) hide show
  1. package/CHANGELOG.md +1218 -1
  2. package/dist/AreaChart.d.ts +12 -1
  3. package/dist/AreaChart.js +131 -13
  4. package/dist/BarChart.d.ts +84 -9
  5. package/dist/BarChart.js +295 -40
  6. package/dist/BarList.d.ts +85 -5
  7. package/dist/BarList.js +25 -4
  8. package/dist/BoxList.d.ts +70 -3
  9. package/dist/BoxList.js +21 -7
  10. package/dist/BoxPlot.d.ts +2 -1
  11. package/dist/BoxPlot.js +101 -9
  12. package/dist/Candlestick.d.ts +13 -1
  13. package/dist/Candlestick.js +89 -3
  14. package/dist/ChartContainer.d.ts +79 -48
  15. package/dist/ChartContainer.js +482 -60
  16. package/dist/ChartRow.d.ts +9 -2
  17. package/dist/ChartRow.js +86 -12
  18. package/dist/HeatMap.d.ts +176 -0
  19. package/dist/HeatMap.js +344 -0
  20. package/dist/Layers.d.ts +5 -1
  21. package/dist/Layers.js +1014 -253
  22. package/dist/Legend.js +8 -4
  23. package/dist/LineChart.d.ts +18 -1
  24. package/dist/LineChart.js +165 -4
  25. package/dist/ListTable.d.ts +30 -3
  26. package/dist/ListTable.js +381 -23
  27. package/dist/ScatterChart.d.ts +3 -2
  28. package/dist/ScatterChart.js +68 -4
  29. package/dist/XAxis.js +40 -22
  30. package/dist/YAxis.d.ts +28 -1
  31. package/dist/YAxis.js +24 -2
  32. package/dist/annotations.d.ts +74 -0
  33. package/dist/annotations.js +97 -7
  34. package/dist/area.d.ts +34 -1
  35. package/dist/area.js +88 -1
  36. package/dist/bars.d.ts +178 -5
  37. package/dist/bars.js +504 -46
  38. package/dist/box.d.ts +2 -2
  39. package/dist/box.js +158 -40
  40. package/dist/brush.d.ts +142 -0
  41. package/dist/brush.js +179 -0
  42. package/dist/child-index.d.ts +27 -0
  43. package/dist/child-index.js +57 -0
  44. package/dist/context.d.ts +871 -36
  45. package/dist/cursors.d.ts +161 -0
  46. package/dist/cursors.js +503 -0
  47. package/dist/decimate.d.ts +78 -1
  48. package/dist/decimate.js +157 -0
  49. package/dist/heat.d.ts +163 -0
  50. package/dist/heat.js +659 -0
  51. package/dist/index.d.ts +13 -4
  52. package/dist/index.js +25 -2
  53. package/dist/line.d.ts +137 -0
  54. package/dist/line.js +328 -0
  55. package/dist/ohlc.d.ts +16 -1
  56. package/dist/ohlc.js +93 -4
  57. package/dist/scatter.d.ts +17 -9
  58. package/dist/scatter.js +221 -33
  59. package/dist/select.d.ts +13 -5
  60. package/dist/select.js +14 -6
  61. package/dist/selection-fixtures.d.ts +174 -0
  62. package/dist/selection-fixtures.js +569 -0
  63. package/dist/selection-stories.d.ts +73 -0
  64. package/dist/selection-stories.js +301 -0
  65. package/dist/selectors.d.ts +316 -0
  66. package/dist/selectors.js +391 -0
  67. package/dist/span.d.ts +122 -0
  68. package/dist/span.js +203 -0
  69. package/dist/sweep.d.ts +154 -0
  70. package/dist/sweep.js +282 -0
  71. package/dist/theme.d.ts +517 -11
  72. package/dist/theme.js +220 -39
  73. package/dist/tracker.d.ts +6 -0
  74. package/dist/tracker.js +6 -0
  75. package/dist/tradingAxis.fixture.d.ts +78 -0
  76. package/dist/tradingAxis.fixture.js +215 -0
  77. package/dist/useChartLegend.js +18 -3
  78. package/package.json +3 -3
package/dist/context.d.ts CHANGED
@@ -1,8 +1,9 @@
1
+ import { type ReactNode } from 'react';
1
2
  import type { ScaleContinuousNumeric, ScaleLinear, ScaleTime } from 'd3-scale';
2
3
  import type { ChartTheme } from './theme.js';
3
- import type { AxisFormat } from './format.js';
4
+ import type { AxisFormat, CursorFormat } from './format.js';
4
5
  import type { LegendItemSpec } from './swatch.js';
5
- import type { Interval } from 'pond-ts';
6
+ import type { Interval, Sequence, BoundedSequence } from 'pond-ts';
6
7
  import type { TradingTimeScale, DiscontinuityProvider } from './tradingTimeScale.js';
7
8
  import type { ScaleBand } from './bandScale.js';
8
9
  import type { ElapsedScale } from './elapsed.js';
@@ -74,6 +75,26 @@ export interface ContainerFrame {
74
75
  readonly regionAnchor: number | null;
75
76
  /** Set / clear the region-drag anchor (see {@link regionAnchor}). */
76
77
  setRegionAnchor(value: number | null): void;
78
+ /**
79
+ * The **live** spans of a sweep in flight — the preview a span-only layer
80
+ * paints ([PND-TRACESEL]), and empty at rest.
81
+ *
82
+ * A mark layer previews through plural `hovered`: the covered marks light.
83
+ * A **trace has no marks**, so there is nothing for that channel to carry —
84
+ * what wants lighting is the *portion of the trace inside the window*, which
85
+ * is a span, not a set of marks. Hence a second preview channel rather than
86
+ * an abuse of the first.
87
+ *
88
+ * A paint mirror in the same sense as {@link regionAnchor}: `Layers` writes
89
+ * it from the live session and clears it on release, and it never enters the
90
+ * committed selection — {@link selectedSpans} is the consumer's, this is the
91
+ * gesture's. A layer that finds itself named here should draw as though the
92
+ * span were committed, so releasing changes nothing visually and the preview
93
+ * cannot promise a different picture than the commit delivers.
94
+ */
95
+ readonly previewSpans: readonly SpanSelection[];
96
+ /** Set / clear the live sweep preview (see {@link previewSpans}). */
97
+ setPreviewSpans(spans: readonly SpanSelection[]): void;
77
98
  /**
78
99
  * One-shot callback fired when a `region`-cursor **drag** is released, with the
79
100
  * selected `[lo, hi]` span in **axis units** — epoch ms on a time axis, the axis
@@ -102,36 +123,93 @@ export interface ContainerFrame {
102
123
  */
103
124
  readonly regionSelectModifier: 'shift' | undefined;
104
125
  /**
105
- * The selected mark, or `null`. Shared across rows (single selection). A layer
106
- * highlights the mark matching the selection's series **`id`** and the clicked
107
- * sample `key` (epoch ms) — the `id` picks the series (so two series sharing a
108
- * timestamp don't both light up), the `key` picks the mark within it. A
109
- * controlled `selected` prop pins it; otherwise a click on a selectable layer
110
- * (one with an `id`) sets it.
126
+ * The selected marks **empty when nothing is selected**, never `null`.
127
+ * Shared across rows, insertion-ordered. A layer highlights every mark
128
+ * matching a member's series **`id`** and clicked sample `key` (epoch ms) —
129
+ * the `id` picks the series (so two series sharing a timestamp don't both
130
+ * light up), the `key` picks the mark within it. A controlled `selected` prop
131
+ * pins the set; otherwise a click on a selectable layer (one with an `id`)
132
+ * sets it.
133
+ *
134
+ * **A set, not a single mark, since [PND-MULTISEL].** The container prop
135
+ * accepts either shape and normalizes here, so a single-selection consumer is
136
+ * unaffected; this frame field is internal (not exported from `index.ts`), so
137
+ * widening it breaks nobody. A layer that only ever wants one mark can read
138
+ * `selected[0]`, but membership is the honest test — see `isSelected`.
111
139
  */
112
- readonly selected: SelectInfo | null;
140
+ readonly selected: readonly SelectInfo[];
113
141
  /**
114
- * Select a mark, or `null` to clear a row's click surface calls this after
115
- * hit-testing its layers. Always fires `onSelect`; manages the internal
116
- * selection only when uncontrolled (no `selectedKey` prop). The split mirrors
117
- * the tracker's `trackerPosition` (controlled by a *value* prop) + its
118
- * `onTrackerChanged` notification not `applyRange`, which is controlled by
119
- * the presence of a *callback*.
142
+ * The **span** entries of the controlled `selected` prop**empty when it
143
+ * carries none**, never `null`. The container splits the prop's mixed
144
+ * `SelectionEntry` array into per-mark entries ({@link selected}) and range
145
+ * descriptors (this field) once at the boundary, so every existing reader of
146
+ * {@link selected} keeps its exact shape (and cost) while span-aware layers
147
+ * read this **additionally** — the union of the two fields is the selection
148
+ * (interaction RFC A5.2). Like `selected`, this frame field is internal (not
149
+ * exported from `index.ts`).
150
+ *
151
+ * Only the controlled prop can populate it: clicks produce marks, so the
152
+ * uncontrolled path never holds a span.
120
153
  */
121
- select(hit: SelectInfo | null): void;
154
+ readonly selectedSpans: readonly SpanSelection[];
122
155
  /**
123
- * The **hovered** mark, or `null` the transient hover-highlight, distinct
124
- * from the committed `selected`. A row's pointer-move surface hit-tests its
125
- * selectable layers and sets it; a layer that supports hover-highlight (Bar)
126
- * draws the matching mark lit (a lighter treatment than `selected`'s outline).
156
+ * Select a mark, or `null` to clear. Reports the hit to the `<Selector>`s in
157
+ * scope and manages the internal selection only when uncontrolled (no
158
+ * `selected` prop). The split mirrors the tracker's `trackerPosition`
159
+ * (controlled by a *value* prop) + its `onTrackerChanged` notification — not
160
+ * `applyRange`, which is controlled by the presence of a *callback*.
161
+ *
162
+ * **`rowKey` distinguishes the two callers, and it is load-bearing**
163
+ * (interaction RFC §7.1). Pass a row's key for a **plot gesture** — the row's
164
+ * click surface, after hit-testing — and the call is *gated on a mounted
165
+ * `<Selector>`*: with none in scope it does nothing at all (the deliberate
166
+ * break), and dev-warns on the hit that went nowhere. Omit it for a
167
+ * **programmatic** select (a `<Legend>` chip, a consumer's own control),
168
+ * which reports to the container-scoped selectors and commits as it always
169
+ * has — mounting gates the *plot*, not an explicit call.
170
+ */
171
+ select(hit: SelectInfo | null, modifiers?: SelectModifiers, rowKey?: symbol): void;
172
+ /**
173
+ * The **hovered** marks — **empty when nothing is hovered, never `null`** —
174
+ * the transient hover-highlight, distinct from the committed `selected`. A
175
+ * row's pointer-move surface hit-tests its selectable layers and sets it; a
176
+ * layer that supports hover-highlight (Bar) draws every matching mark lit (a
177
+ * lighter treatment than `selected`'s outline).
178
+ *
179
+ * **A set, not one mark, since RFC `selection.md` A4.2.** A1.4 argued hover
180
+ * "is inherently one mark under the pointer" — true while hover *means*
181
+ * pointer position, and false under a drag sweep, where it means "would be
182
+ * selected if you released now" and several marks are lit at once. Plain
183
+ * pointer-over therefore carries 0 or 1 members; that slightly odd type for
184
+ * the common case is the accepted cost of not minting a third `preview`
185
+ * state a theme would style identically anyway.
127
186
  * Set-on-change (deduped by the series `id` + sample `key`) so the data canvas
128
187
  * repaints only on a mark transition, not every pointer move.
129
188
  */
130
- readonly hovered: SelectInfo | null;
131
- /** Set the hovered mark (or `null` to clear) from a pointer-move hit-test;
132
- * deduped by series `id` + sample `key`, so an unchanged mark is a no-op
133
- * (no repaint). */
134
- setHovered(hit: SelectInfo | null): void;
189
+ readonly hovered: readonly SelectInfo[];
190
+ /**
191
+ * Set the hovered mark (or `null` to clear) from a pointer-move hit-test;
192
+ * deduped by series `id` + sample `key`, so an unchanged mark is a no-op (no
193
+ * repaint). `rowKey` scopes which `<Selector>`s hear about it (a row's own
194
+ * mounts, else the container's) exactly as {@link select} does; omit it for a
195
+ * programmatic hover (a `<Legend>` chip).
196
+ *
197
+ * **Not gated on a mounted `<Selector>`, unlike {@link select}.** With no
198
+ * selector mounted there is simply nobody registered to hear about it (RFC
199
+ * A10.3) — the hover state still moves, it just has nothing to report to,
200
+ * which needs no gate to arrange.
201
+ *
202
+ * `block` — the **resting block preview** under a mounted `<MultiSelector>`
203
+ * (the marks of the snap block the pointer is in — exactly the set a drag
204
+ * begun and released there would select). When present it becomes the
205
+ * hovered *state* (every mark in the block lights) and what
206
+ * `<MultiSelector onHover>` hears, deduped by the block array's identity
207
+ * (the caller keeps it reference-stable per block, so within-block mark
208
+ * transitions re-render nothing). `<Selector onHover>` keeps its per-mark
209
+ * currency — `hit` still reports per mark transition. Omitted ⇒ the
210
+ * plain single-mark hover, exactly as before.
211
+ */
212
+ setHovered(hit: SelectInfo | null, rowKey?: symbol, block?: readonly SelectInfo[]): void;
135
213
  /** The default in-chart cursor presentation for all rows ({@link CursorMode});
136
214
  * a row may override it via its own `cursor`. */
137
215
  readonly cursor: CursorMode;
@@ -200,6 +278,45 @@ export interface ContainerFrame {
200
278
  */
201
279
  registerSelectable(key: symbol): void;
202
280
  unregisterSelectable(key: symbol): void;
281
+ /**
282
+ * Register a mounted **selector** ({@link SelectorEntry}) — `<Selector>` /
283
+ * `<MultiSelector>` call this, keyed by the component's per-instance slot
284
+ * key. The same idiom as `registerCursor` / `registerAxis` / `registerLayer`:
285
+ * update in place, unregister on unmount.
286
+ *
287
+ * **The registration is the enablement** (interaction RFC §7.1): a plot click
288
+ * resolves the selectors in scope (`effectiveSelectorEntries`) and does
289
+ * nothing when there are none.
290
+ *
291
+ * The registry itself is deliberately **not** exposed on the frame (unlike
292
+ * `cursors`, which the rows and `<XAxis>` render): nothing outside the
293
+ * container reads it — `select` / `setHovered` resolve the scope internally —
294
+ * and publishing it would re-identify the whole frame on every selector
295
+ * mount. Same shape as `registerSelectable`, which also keeps its set private.
296
+ */
297
+ registerSelector(key: symbol, entry: SelectorEntry): void;
298
+ unregisterSelector(key: symbol): void;
299
+ /**
300
+ * Resolve a row's press against the mounted `<MultiSelector>`s in scope
301
+ * (interaction RFC §8): the {@link SweepGesture} sinks when at least one is
302
+ * in scope, else `null` — which is how the brush recognizer learns whether a
303
+ * sweep can claim the drag at all (mounting arms the gesture, §7.1's rule
304
+ * extended to the sweep). Scope resolution mirrors {@link select}'s: the
305
+ * row's own mounts when it has any, else the container's. The selector
306
+ * registry itself stays private, for the reason documented on
307
+ * {@link registerSelector}.
308
+ */
309
+ resolveSweep(rowKey: symbol): SweepGesture | null;
310
+ /**
311
+ * Whether a `<MultiSelector>` is in scope for this row — the *render-time*
312
+ * fact behind {@link resolveSweep} (which builds the per-drag gesture and is
313
+ * for the pointer-down path). Rows read this to decide the **resting block
314
+ * preview**: with one in scope over a sweep-capable row, the shared brush
315
+ * band becomes the resting cursor (replacing the implicit line shim) and
316
+ * hover lights the whole snap block a drag would select. Identity changes
317
+ * when the selector registry does, so it is safe in render memos.
318
+ */
319
+ hasMultiSelector(rowKey: symbol): boolean;
203
320
  /**
204
321
  * Register this layer's **legend row** — its display label + resolved
205
322
  * {@link SwatchSpec} (and selection `id` when it has one) — keyed by the
@@ -275,6 +392,34 @@ export interface ContainerFrame {
275
392
  * called while `panZoom` is on.
276
393
  */
277
394
  applyRange(range: readonly [number, number]): void;
395
+ /**
396
+ * The **y view transform** for 2-D pan/zoom (`panZoom="panZoom2D"`), in
397
+ * *pixel* space: a y scale's range becomes `ty + k · basePixel`. Identity is
398
+ * `{ k: 1, ty: 0 }`.
399
+ *
400
+ * Pixel space rather than domain space is what makes this axis-independent.
401
+ * A row may carry several y axes with unrelated domains (left price, right
402
+ * volume), and the question "which one does a vertical gesture own?" has no
403
+ * good answer — but a uniform pixel transform sidesteps it: every axis zooms
404
+ * by the same factor about its own pivot, which is also exactly what keeps
405
+ * the **aspect ratio** fixed. The x half stays in domain space, where
406
+ * `bounds`, `minDuration` and the trading-calendar zoom maths live.
407
+ */
408
+ /** Which axes the gestures own; pan follows zoom's degrees of freedom. */
409
+ readonly zoomX: boolean;
410
+ readonly zoomY: boolean;
411
+ readonly panX: boolean;
412
+ readonly panY: boolean;
413
+ /** True only when both axes zoom — one factor, so the ratio is fixed. */
414
+ readonly aspectLocked: boolean;
415
+ readonly yTransform: {
416
+ readonly k: number;
417
+ readonly ty: number;
418
+ };
419
+ applyYTransform(next: {
420
+ k: number;
421
+ ty: number;
422
+ }): void;
278
423
  /**
279
424
  * A row reports its per-slot gutter widths each side; the container reserves
280
425
  * each slot's max so every row's plot left-aligns. Returns an unregister fn.
@@ -289,6 +434,19 @@ export interface ContainerFrame {
289
434
  registerRow(key: symbol): () => void;
290
435
  /** The first (topmost) row's key, or `null` before any row has registered. */
291
436
  readonly firstRowKey: symbol | null;
437
+ /**
438
+ * Register a mounted **cursor** ({@link CursorEntry}) — the `<LineCursor>` /
439
+ * `<CrosshairCursor>` / … presets (and the deprecation shim synthesizing them
440
+ * from the legacy string props) call this, keyed by the component's
441
+ * per-instance slot key. The container resolves the per-row effective set
442
+ * (row mounts override container mounts — see `effectiveCursorEntries`) and
443
+ * the rows/`<XAxis>` render the registered slots. Update is in place;
444
+ * unregister on unmount.
445
+ */
446
+ registerCursor(key: symbol, entry: CursorEntry): void;
447
+ unregisterCursor(key: symbol): void;
448
+ /** Every registered cursor, in mount order (see {@link registerCursor}). */
449
+ readonly cursors: readonly CursorEntry[];
292
450
  /**
293
451
  * Register an annotation (`<Region>`/`<Marker>`/`<Baseline>`) so the container
294
452
  * can coordinate what a mark can't do in isolation: draw each mark's **guide
@@ -349,6 +507,15 @@ export interface ContainerFrame {
349
507
  }
350
508
  /** The kind of an annotation, and of a creation tool. */
351
509
  export type AnnotationKind = 'region' | 'marker' | 'baseline';
510
+ /**
511
+ * The kind of a **registered** mark — wider than {@link AnnotationKind}.
512
+ *
513
+ * A `<Zone>` is a mark you place in JSX but **not** a create *tool*: there is no
514
+ * draw gesture for it and no {@link CreateSpec} variant, so it registers under
515
+ * its own kind without widening the toolbar vocabulary (which would let
516
+ * `creating="zone"` type-check and then silently never fire `onCreate`).
517
+ */
518
+ export type AnnotationSpecKind = AnnotationKind | 'zone';
352
519
  /** What a completed create gesture reports to {@link ContainerFrame.onCreate} —
353
520
  * the new mark's kind + position in axis units (+ the y-axis id for a baseline).
354
521
  * (Which row a mark lands on is the consumer's call for now; multi-row routing is
@@ -376,14 +543,14 @@ export interface AnnotationSpec {
376
543
  * double-click reports via {@link ContainerFrame.onSelectAnnotation}, so the
377
544
  * consumer knows which mark to select. */
378
545
  readonly id: string | undefined;
379
- readonly kind: AnnotationKind;
546
+ readonly kind: AnnotationSpecKind;
380
547
  /** The row it lives on (its `<ChartRow>`'s key), so a row skips its own marks
381
548
  * when drawing guides. */
382
549
  readonly rowKey: symbol;
383
550
  /**
384
551
  * Its vertical-guide x-position(s) in **axis units** (the shared x): a marker's
385
- * `[at]`, a region's `[from, to]`. Empty for a baseline a horizontal line
386
- * casts no vertical guide.
552
+ * `[at]`, a region's `[from, to]`. Empty for a baseline or a zone — a
553
+ * horizontal line (or band) casts no vertical guide.
387
554
  */
388
555
  readonly xs: readonly number[];
389
556
  /** Whether it's currently selected (controlled by the consumer). */
@@ -554,13 +721,15 @@ export interface RowLayer {
554
721
  binCategories?(): readonly string[] | null;
555
722
  /**
556
723
  * A bar/histogram layer's bar `[begin, end)` spans, as pond `Interval`s — the
557
- * **region cursor's snap buckets**. When present (and no `cursorSequence` is
558
- * set), a region drag snaps bar by bar and a hover highlights the bar under the
559
- * pointer, so a histogram gets bin-aligned selection for free. Only a
560
- * **vertical** bar layer on a **continuous** (time / value) x axis publishes
561
- * them — a horizontal chart puts the value on x (snapping counts is meaningless)
562
- * and a **category** (ordinal-slot) axis is excluded from the region cursor.
563
- * `null` / absent otherwise.
724
+ * **shared snap buckets**. When present (and no `cursorSequence` is set), a
725
+ * region drag snaps bar by bar and a `<MultiSelector>` sweep's band extends
726
+ * bar by bar, so a histogram gets bin-aligned selection for free. Only a
727
+ * **vertical** bar layer publishes them a horizontal chart puts the value
728
+ * on x (snapping counts is meaningless). On a **category** axis they are
729
+ * the unit slots `[i, i+1)`: the region cursor still ignores them (its band
730
+ * gates on a continuous axis), but the sweep band snaps over them to the
731
+ * slots' outer edges (RFC A7.6's edge rule — the band must agree with the
732
+ * span the release commits). `null` / absent otherwise.
564
733
  */
565
734
  binIntervals?(): readonly Interval[] | null;
566
735
  /**
@@ -589,8 +758,87 @@ export interface RowLayer {
589
758
  * so they render + read out but never select/hover (a click on them resolves
590
759
  * to empty space ⇒ deselect). `xScale`/`yScale` map data→pixels (the row
591
760
  * resolves the layer's axis scale, as for `draw`).
761
+ *
762
+ * `mode` says which gesture is asking, and a layer whose hover target is
763
+ * generous may narrow the *select* one. The single-series bar path does:
764
+ * **hover** attributes the bar's whole slot (full interval width, full plot
765
+ * height — the continuous model #582 asked for, so the highlight tracks
766
+ * like the readout), while **select** additionally requires the pointer to
767
+ * be within the bar's drawn ink vertically. Without that narrowing, slots
768
+ * tile the entire plot and a click can never resolve to `null` — RFC §7's
769
+ * empty-click deselect path becomes unreachable on any full-range bar
770
+ * chart. Layers whose hit target is already the drawn mark (stacks,
771
+ * scatter, boxes, heat cells) ignore `mode`.
592
772
  */
593
- hitTest?(px: number, py: number, xScale: (value: number) => number, yScale: (value: number) => number): SelectInfo | null;
773
+ hitTest?(px: number, py: number, xScale: (value: number) => number, yScale: (value: number) => number, mode?: 'hover' | 'select'): SelectInfo | null;
774
+ /**
775
+ * Begin a **sweep session** over this layer's marks — `<MultiSelector>`'s
776
+ * range query (interaction RFC A7.6/A7.7), the range analog of
777
+ * {@link hitTest} and gated exactly like it: only a layer with an `id` (and
778
+ * discrete selectable marks) wires it, so an untagged layer is never swept.
779
+ * Called on the pointer-down that survives `DRAG_SLOP` under a mounted
780
+ * `<MultiSelector>`; the session lives for that one drag and nothing
781
+ * persists outside it. Returns `null` when there is nothing sweepable.
782
+ *
783
+ * `xScale`/`yScale` are the resolved scales (as for `draw`/`hitTest`) —
784
+ * unused by the 1-D layers, but part of the seam so the 2-D sessions
785
+ * (scatter's y-window, the heat map's row cut — [PND-INTERACT2D]) can be
786
+ * added without redesigning the shape (RFC A7.6).
787
+ */
788
+ beginSweep?(xScale: (value: number) => number, yScale: (value: number) => number): SweepSession | null;
789
+ /**
790
+ * Whether a sweep over this layer cuts a **rect** rather than a band — the
791
+ * same fact {@link SweepSession.twoD} reports, declared on the layer because
792
+ * the RESTING state has to know it and there is no session at rest.
793
+ *
794
+ * It decides what the row's resting cursor is: a band over the snap block
795
+ * for a 1-D layer, a small crosshair at the pointer for a 2-D one (a rect
796
+ * gesture has no resting block to preview — see `Layers`). Building a
797
+ * per-drag session just to ask would be the wrong shape and would snapshot
798
+ * the layer's arrays for nothing.
799
+ *
800
+ * Must agree with the session the layer's own `beginSweep` returns;
801
+ * `sweep-capabilities.test.ts` pins that across every layer.
802
+ */
803
+ readonly sweepsRect?: boolean;
804
+ /**
805
+ * **Which screen axis a sweep over this layer cuts** — `'x'` (the default,
806
+ * and every layer that shipped before [PND-HSWEEP]) or `'y'` for a
807
+ * transposed layer whose bins run down the screen: a horizontal
808
+ * `<BarChart>` puts the value on the shared x, so a window there says
809
+ * nothing about which bins are covered and the cut has to come from the
810
+ * pointer's y.
811
+ *
812
+ * **Orthogonal to {@link sweepsRect}, and all four pairs are real** — (x,
813
+ * band) a vertical bar, (y, band) a horizontal bar, (x, rect) a scatter or a
814
+ * vertical heat map, (y, rect) a horizontal heat map. That is why this is a
815
+ * second field rather than a third value on `sweepsRect`.
816
+ *
817
+ * The session itself is unaffected: {@link SweepSession.update} takes
818
+ * **key-axis units** and does not care which screen axis produced them, so a
819
+ * transposed layer builds the same `sweep1D` a vertical one does. What the
820
+ * axis changes is the *gesture* — which pointer coordinate is inverted (and
821
+ * through which scale), where the drag slop lives (`|dy|` rather than
822
+ * `|dx|`), and which way the brush band is drawn.
823
+ *
824
+ * A `'y'` cut is measured against **one row's** axis, so its band is
825
+ * row-local exactly as a rect is, and it takes its geometry from
826
+ * {@link SweepSession.extent} rather than from the shared bin channel —
827
+ * `binIntervals` carries the *value* axis on a transposed layer.
828
+ */
829
+ readonly sweepAxis?: 'x' | 'y';
830
+ /**
831
+ * **Whether a sweep over this layer has a range but no marks** — the same
832
+ * fact {@link SweepSession.spanOnly} reports, declared on the layer for
833
+ * exactly the reason {@link sweepsRect} is: the RESTING state has to know it
834
+ * and there is no session at rest.
835
+ *
836
+ * It suppresses the resting **block** band. That band previews "a drag begun
837
+ * here selects this block", and a trace has no blocks — a drag takes a
838
+ * freeform window — so the band would advertise a set the gesture never
839
+ * selects. Precisely the reason a `twoD` layer gets no resting band either.
840
+ */
841
+ readonly sweepSpanOnly?: boolean;
594
842
  /**
595
843
  * Draw into the plot canvas. `xScale`/`yScale` map data→pixels. May return
596
844
  * {@link LayerDrawStats} (source/drawn counts + whether decimation engaged) so
@@ -599,6 +847,142 @@ export interface RowLayer {
599
847
  */
600
848
  draw(ctx: CanvasRenderingContext2D, xScale: (value: number) => number, yScale: (value: number) => number): LayerDrawStats | void;
601
849
  }
850
+ /**
851
+ * A layer's per-drag **sweep session** ({@link RowLayer.beginSweep} — RFC
852
+ * A7.7's `beginSweep(scales) → session` shape, 1-D form). The gesture engine
853
+ * drives it: `update` per coalesced frame with the swept key-window,
854
+ * {@link hits} for the frame-gated live preview, and — at release — the same
855
+ * `hits()` again as the commit payload plus {@link extent} for the span, so
856
+ * the committed hits ARE the materialised preview rather than a fresh range
857
+ * query (RFC A5.2's "the hits are free"). Internal, like `RowLayer` itself.
858
+ */
859
+ export interface SweepSession {
860
+ /** The layer's `id` — what the committed {@link SpanSelection} carries. */
861
+ readonly id: string;
862
+ /**
863
+ * Re-cut the covered set to the marks intersecting the half-open window
864
+ * `[x0, x1)` (key-axis units, `x0 <= x1`). Returns whether the covered set
865
+ * changed — the delta gate: an unchanged frame re-materialises nothing.
866
+ *
867
+ * `y0`/`y1` are the rect's second dimension in the layer's **y-axis data
868
+ * units** (not pixels — the gesture inverts through the same `yScale` the
869
+ * session was built with), and only a {@link twoD} session reads them; the
870
+ * gesture omits them entirely for a 1-D layer. They are unordered, because
871
+ * a drag upward hands them over inverted and the session is the one place
872
+ * that knows whether its axis descends.
873
+ *
874
+ * **A `twoD` session must include y in its own delta gate.** A purely
875
+ * vertical drag leaves the x run untouched, so a gate that watched x alone
876
+ * would answer `false` and freeze the preview under a moving pointer.
877
+ */
878
+ update(x0: number, x1: number, y0?: number, y1?: number): boolean;
879
+ /**
880
+ * The covered marks, materialised (and cached until the next change) — the
881
+ * live preview `hovered` lights, and verbatim the release payload.
882
+ */
883
+ hits(): readonly SelectInfo[];
884
+ /**
885
+ * The covered marks' snapped-outward key extent `[begin(first), end(last))`
886
+ * — {@link SpanSelection.x} per RFC A7.6's edge rule, so the span's
887
+ * half-open containment test reproduces exactly the captured set. `null`
888
+ * when nothing is covered.
889
+ */
890
+ extent(): readonly [number, number] | null;
891
+ /**
892
+ * **Set by a layer whose marks live in two dimensions** — a scatter (a point
893
+ * is a position, not a column) or a heat map (a grid of cells). The gesture
894
+ * reads this to decide whether to track a y window alongside x and to draw a
895
+ * rect rather than a band: dimensionality is a property of the *layer*, so a
896
+ * consumer mounts the same `<MultiSelector>` either way (RFC Q14).
897
+ */
898
+ readonly twoD?: boolean;
899
+ /**
900
+ * **Set by a layer that has a range but no marks** — a continuous trace
901
+ * ([PND-TRACESEL]). `hits()` is always empty and the span is the whole
902
+ * selection.
903
+ *
904
+ * The gesture reads this to answer "who else did I sweep?". Topmost-wins
905
+ * exists because a release carried **one** span, and on mark layers it is
906
+ * defensible — you were pointing at marks, so the topmost is the one you
907
+ * meant. A trace sweep points at *nothing*: every trace in the row shares
908
+ * the same x window, so singling one out by z-order is arbitrary to the
909
+ * reader. Every span-only layer in the row is therefore swept together,
910
+ * while mark layers keep topmost-wins unchanged.
911
+ */
912
+ readonly spanOnly?: boolean;
913
+ /**
914
+ * The captured set's **second-dimension** channels, for a `twoD` session —
915
+ * whichever of {@link SpanSelection.y} (a scatter's continuous window) and
916
+ * {@link SpanSelection.rows} (a heat map's ordinal row set) the layer uses.
917
+ * `null` when nothing is covered. Absent on a 1-D session.
918
+ */
919
+ extent2D?(): {
920
+ readonly y?: readonly [number, number];
921
+ readonly rows?: readonly string[];
922
+ } | null;
923
+ /**
924
+ * The cut's **snapped rect in axis units** (x in key units, y in the
925
+ * sweeping layer's y-axis units), for the brush to draw — `null` when the
926
+ * layer's cut is free, or when nothing is covered.
927
+ *
928
+ * A snapping layer must draw the rect it is going to *take*, not the one
929
+ * the pointer traced: a heat map snaps to whole cells, so a raw pointer
930
+ * rect promises a different set than the release delivers, and the two
931
+ * disagree most visibly at exactly the moment the user is deciding where
932
+ * to let go.
933
+ */
934
+ snappedRect?(): {
935
+ readonly x: readonly [number, number];
936
+ readonly y: readonly [number, number];
937
+ } | null;
938
+ }
939
+ /**
940
+ * What the container resolves for a row's press under a mounted
941
+ * `<MultiSelector>` ({@link ContainerFrame.resolveSweep}): the two sinks of
942
+ * the sweep gesture, with the selector registry kept private (the same reason
943
+ * `select` resolves scope internally). Both operate on the entries that were
944
+ * in scope at the press, for the lifetime of that one drag.
945
+ */
946
+ export interface SweepGesture {
947
+ /**
948
+ * Whether an in-scope `<MultiSelector>` **declared a `sequence`** — i.e. the
949
+ * consumer said selection happens in bucket units rather than in marks.
950
+ *
951
+ * This is what widens a *click* from the mark under the pointer to the whole
952
+ * snap block (RFC §8). It has to be the declaration and not a property of
953
+ * what the block happened to contain: a stack's bin holds one mark per
954
+ * group, so "the block covers more than one mark" is true of an ordinary
955
+ * single-bin click on a stacked chart, and using that as the test made a
956
+ * click there select the whole bin instead of the clicked segment.
957
+ */
958
+ readonly snapped: boolean;
959
+ /**
960
+ * The frame-coalesced **live preview**: light `hits` through the plural
961
+ * `hovered` (RFC A3.4 — the library owns the state, each layer renders its
962
+ * own hover treatment) and report them to the `<MultiSelector>`s in scope.
963
+ * Nothing else crosses the public boundary until release (RFC A1.4).
964
+ *
965
+ * `light: false` **reports without lighting** — for a layer whose brush
966
+ * already outlines exactly the covered region (a heat map's snapped rect).
967
+ * Its hover treatment is a ring around *the cell under the pointer*, and
968
+ * during a sweep there is no such cell: a ring on every covered cell turns
969
+ * the region into the mostly-border grid the selection outline exists to
970
+ * avoid, inside a rect that is already saying the same thing. The consumer
971
+ * still hears the full set through `onHoverMany`, so a controlled `hovered`
972
+ * can render it however it likes.
973
+ */
974
+ preview(hits: readonly SelectInfo[], light?: boolean): void;
975
+ /**
976
+ * The release: report `(hits, modifiers, span)` to the `<MultiSelector>`s in
977
+ * scope (RFC A5.2), clear the preview, and — when `selected` is
978
+ * uncontrolled — commit the compact span descriptor as the selection.
979
+ */
980
+ commit(hits: readonly SelectInfo[], modifiers: SelectModifiers,
981
+ /** Every span the gesture produced, **topmost first** — see
982
+ * `<MultiSelector onSelect>`. Empty for a click, or for a sweep that
983
+ * covered nothing. */
984
+ spans: readonly SpanSelection[]): void;
985
+ }
602
986
  /** One tracker readout point — a dot + value the overlay draws at the cursor. */
603
987
  export interface TrackerSample {
604
988
  /** The sample's time (epoch ms); the dot sits at `xScale(x)`. */
@@ -676,6 +1060,125 @@ export interface TrackerSource {
676
1060
  * a selection survives a streaming data update where a sample `key` would go
677
1061
  * stale. Only layers that carry an `id` are selectable (see {@link RowLayer.hitTest}).
678
1062
  */
1063
+ /**
1064
+ * The keyboard modifiers held during the click that produced a selection —
1065
+ * handed to `<Selector onSelect>` alongside the hit ([PND-MULTISEL]).
1066
+ *
1067
+ * **Why the library reports these instead of acting on them.** A consumer with
1068
+ * a multi-valued filter needs ⌘/Ctrl-click to mean "add to the selection", and
1069
+ * without the modifier state on the callback it simply cannot: the click has
1070
+ * already been reduced to a hit by the time it arrives, so every consumer is
1071
+ * forced to treat every click as a replace. Reporting the modifiers keeps the
1072
+ * *policy* with the consumer while removing the thing that made the policy
1073
+ * unexpressible.
1074
+ *
1075
+ * The library itself still applies no modifier semantics — a chart click sets
1076
+ * the single hit as it always has. A consumer that wants add/toggle reads
1077
+ * {@link additive} and drives the controlled `selected` set itself.
1078
+ */
1079
+ export interface SelectModifiers {
1080
+ /**
1081
+ * The platform-idiomatic **"add to selection"** chord — `metaKey` on macOS,
1082
+ * `ctrlKey` elsewhere, surfaced as one boolean so every consumer doesn't
1083
+ * re-derive the same platform rule (and get it wrong on one of the two).
1084
+ * Prefer this over the raw keys unless you specifically want one of them.
1085
+ */
1086
+ readonly additive: boolean;
1087
+ readonly ctrlKey: boolean;
1088
+ readonly metaKey: boolean;
1089
+ /**
1090
+ * **Note the conflict:** `shift` is already the drag chord for
1091
+ * `<ChartContainer regionSelectModifier="shift">` on a continuous axis, so a
1092
+ * shift-click there may also be the start of a region drag. Reported for
1093
+ * completeness; think before you give it a second meaning. (There is
1094
+ * deliberately no derived `range` flag for this reason — an ordinal range
1095
+ * gesture is [PND-CATRANGE], not a modifier.)
1096
+ */
1097
+ readonly shiftKey: boolean;
1098
+ readonly altKey: boolean;
1099
+ }
1100
+ /**
1101
+ * A mounted `<Selector>` as the container holds it — the reporting callbacks
1102
+ * plus the mount scope (interaction RFC §7 / A4.2). **A selector both reports
1103
+ * and, optionally, owns controlled state** (`selected` / `hovered` — A10.3):
1104
+ * the gesture and the state it drives are declared on the same mount.
1105
+ *
1106
+ * Registered via {@link ContainerFrame.registerSelector}, the same idiom as
1107
+ * `registerCursor`. Internal (not exported from `index.ts`) — `<Selector>`'s
1108
+ * props are the public surface.
1109
+ */
1110
+ export interface SelectorEntry {
1111
+ /** Report the mark under the pointer (`null` on leaving every mark). */
1112
+ readonly onHover: ((hit: SelectInfo | null) => void) | undefined;
1113
+ /** Report the clicked mark plus the modifiers held. The library applies no
1114
+ * policy to them and holds no set — see {@link SelectModifiers}. */
1115
+ readonly onSelect: ((hit: SelectInfo | null, modifiers?: SelectModifiers) => void) | undefined;
1116
+ /**
1117
+ * Whether this entry is a mounted `<MultiSelector>` (RFC §8) — the flag the
1118
+ * sweep gesture resolves on: **mounting one is what arms the sweep drag** on
1119
+ * the plot, exactly as mounting any selector is what arms the click (§7.1).
1120
+ * `<Selector>` registers `false`.
1121
+ */
1122
+ readonly multi: boolean;
1123
+ /** `<MultiSelector onHover>` — the marks a live sweep covers (0/1 outside a
1124
+ * drag, mirroring the single-hit channel). Plural entries only. */
1125
+ readonly onHoverMany?: ((hits: readonly SelectInfo[]) => void) | undefined;
1126
+ /** `<MultiSelector onSelect>` — RFC A5.2's `(hits, modifiers, span)`. A
1127
+ * plain click reports `([hit] | [], modifiers, null)`; a sweep release
1128
+ * reports the covered marks plus the span they demote to. Plural entries
1129
+ * only. */
1130
+ readonly onSelectMany?: ((hits: readonly SelectInfo[], modifiers: SelectModifiers | undefined, spans: readonly SpanSelection[]) => void) | undefined;
1131
+ /** `<MultiSelector sequence>` — the sweep's bucket snap, folded into the
1132
+ * container's shared snap-bucket channel (as `<RangeCursor sequence>` is). */
1133
+ readonly sequence?: Sequence | BoundedSequence | undefined;
1134
+ /** Mount scope: a row's key when mounted inside a `<ChartRow>` (that row's
1135
+ * clicks only), `null` when mounted at the container (every row). */
1136
+ readonly rowKey: symbol | null;
1137
+ /**
1138
+ * Whether the **gesture** is active (`<Selector enabled>`, A10.2). `false`
1139
+ * means the plot behaves as if this entry were unmounted — no hit-testing,
1140
+ * no callbacks fire, no sweep arms — while the entry may still be the
1141
+ * chart's controlled-state owner below. This is what makes
1142
+ * `<Selector enabled={false} selected={sel} />` the "highlight only, no
1143
+ * plot gesture" configuration: one component, gesture off, state on.
1144
+ */
1145
+ readonly gestureEnabled: boolean;
1146
+ /** Whether `selected` was explicitly passed to this mount — distinguishes
1147
+ * "controlled with nothing selected" (`null`) from "not the state owner"
1148
+ * (`undefined`), the same three-state shape `selected` itself needs. */
1149
+ readonly declaresSelected: boolean;
1150
+ /** `<Selector selected>` / `<MultiSelector selected>` (A10.3) — meaningful
1151
+ * only when {@link declaresSelected} is true. */
1152
+ readonly selected?: SelectInfo | readonly SelectionEntry[] | null | undefined;
1153
+ /** As {@link declaresSelected}, for `hovered`. */
1154
+ readonly declaresHovered: boolean;
1155
+ /** `<Selector hovered>` / `<MultiSelector hovered>` (A10.3) — meaningful
1156
+ * only when {@link declaresHovered} is true. */
1157
+ readonly hovered?: SelectInfo | readonly SelectInfo[] | null | undefined;
1158
+ }
1159
+ /**
1160
+ * The span a completed `<RangeCursor>` drag reports to `onDragRelease` —
1161
+ * **one uniform shape with an optional y** (interaction RFC A3.3), not the
1162
+ * bare pair the legacy `onRegionSelect` used, and not a polymorphic union a
1163
+ * consumer must narrow.
1164
+ *
1165
+ * `x` is `[lo, hi]` in **axis units** — epoch ms on a time axis, the axis
1166
+ * value (strike, distance, …) on a value axis — snapped to the cursor's
1167
+ * buckets when it has a `sequence` (or a histogram's bins), else the raw drag
1168
+ * span. `y` is **absent on today's 1-D layers**; the 2-D drag (scatter / heat
1169
+ * map — [PND-INTERACT2D]) will populate it *without a breaking change*, which
1170
+ * is the whole reason the 1-D payload is already an object.
1171
+ *
1172
+ * The x pair feeds `ChartContainer.range` directly (it accepts a
1173
+ * `readonly [number, number]`), so drag-to-zoom is
1174
+ * `onDragRelease={(s) => setRange(s.x)}`; a time-axis consumer who wants a
1175
+ * `TimeRange` constructs one from the pair.
1176
+ */
1177
+ export interface RangeSpan {
1178
+ readonly x: readonly [number, number];
1179
+ /** Present only on a 2-D drag (scatter / heat map) — not yet emitted. */
1180
+ readonly y?: readonly [number, number];
1181
+ }
679
1182
  export interface SelectInfo {
680
1183
  /**
681
1184
  * The **series identity** — the layer's `id` prop. The selection / dedup /
@@ -720,6 +1223,77 @@ export interface SelectInfo {
720
1223
  */
721
1224
  readonly mark?: string;
722
1225
  }
1226
+ /**
1227
+ * A **span selection** — a compact range descriptor over one layer's marks, the
1228
+ * second currency `selected` accepts beside per-mark {@link SelectInfo} entries
1229
+ * (interaction RFC A5.2). Where a mark entry names one mark, a span names
1230
+ * *every* mark of layer {@link id} inside its extent — the shape a sweep
1231
+ * gesture commits, and the shape a consumer with a range-valued filter already
1232
+ * has. Membership is evaluated per draw against the mark's own channels, so a
1233
+ * span costs O(1) per mark however many marks it covers.
1234
+ *
1235
+ * **The containment rule, stated once** (evaluated by `selectionContains` and,
1236
+ * identically, by every layer's draw): a mark is inside a span when the span's
1237
+ * `id` is the mark's layer, the mark's **key** (its `SelectInfo.key` — the bin
1238
+ * axis position in axis units) lies in the **half-open** interval
1239
+ * `x[0] <= key < x[1]`, the mark's **value** (its `SelectInfo.value`) lies in
1240
+ * `y[0] <= value < y[1]` when {@link y} is present, and the mark's **label**
1241
+ * (its `SelectInfo.label` — the row/group name on a 2-D-ordinal layer) is a
1242
+ * member of {@link rows} when present.
1243
+ *
1244
+ * **Half-open, deliberately** — the pond bucket convention (`[begin, end)`),
1245
+ * and the half the edge rule needs: contiguous interval marks share edges
1246
+ * (`end[i] === begin[i+1]`), so a sweep that captures marks `i..j` by
1247
+ * *intersection* stores the snapped-outward edges `[begin(i), end(j)]`, and the
1248
+ * half-open key test then reproduces exactly the captured set — `begin(j+1) ===
1249
+ * end(j)` falls out on the open side (RFC A7.6). A closed test would light the
1250
+ * first mark *past* the sweep on every shared edge.
1251
+ *
1252
+ * **Two optional second dimensions, not one** (RFC A5.3): {@link y} is a
1253
+ * numeric interval for a continuous × continuous layer (scatter), where the
1254
+ * second coordinate is the mark's plotted value. {@link rows} is a label set
1255
+ * for a continuous × ordinal layer (heat map), whose second coordinate is an
1256
+ * ordinal row **name** — a numeric y-interval there would be untestable from a
1257
+ * hit (the hit carries no slot number) and unstable under a row reorder. Do not
1258
+ * conflate them.
1259
+ */
1260
+ export interface SpanSelection {
1261
+ /** Discriminant against {@link SelectInfo} (which has no `kind`). */
1262
+ readonly kind: 'span';
1263
+ /** The **layer** whose marks the span covers — a layer `id`, exactly as
1264
+ * {@link SelectInfo.id}. A span never matches marks of another layer. */
1265
+ readonly id: string;
1266
+ /**
1267
+ * The key-axis extent in **axis units** (epoch ms on a time axis, the axis
1268
+ * value on a value axis), half-open `[lo, hi)` against each mark's
1269
+ * {@link SelectInfo.key}. This is the layer's **bin/key axis** whatever the
1270
+ * orientation — a horizontal heat map's bins run down the screen, but their
1271
+ * keys (and so this interval) stay in bin-axis units. Must be ordered
1272
+ * `lo <= hi`; a reversed or empty pair matches nothing.
1273
+ */
1274
+ readonly x: readonly [number, number];
1275
+ /**
1276
+ * Optional **value-axis** interval, half-open against each mark's
1277
+ * {@link SelectInfo.value} — the continuous second dimension of a 2-D sweep
1278
+ * on a scatter (RFC A3.3/A5.3). Omit for 1-D layers.
1279
+ */
1280
+ readonly y?: readonly [number, number];
1281
+ /**
1282
+ * Optional **row label set**, matched against each mark's
1283
+ * {@link SelectInfo.label} — the ordinal second dimension of a 2-D sweep on
1284
+ * a heat map (RFC A5.3), stable under a row reorder because it names rows
1285
+ * rather than numbering slots. Omit for 1-D layers.
1286
+ */
1287
+ readonly rows?: readonly string[];
1288
+ }
1289
+ /**
1290
+ * One entry of a plural `selected` — a single mark ({@link SelectInfo}) or a
1291
+ * whole range of one layer's marks ({@link SpanSelection}). The two currencies
1292
+ * of interaction RFC A5.2: clicks produce marks, sweeps produce a span (plus
1293
+ * the marks it covered, for demote-on-edit). Discriminate with
1294
+ * `isSpanSelection` / the `kind` field.
1295
+ */
1296
+ export type SelectionEntry = SelectInfo | SpanSelection;
723
1297
  /** The hover snapshot handed to `onTrackerChanged` — the cursor time + every
724
1298
  * series' value there, so a consumer can render the readout outside the chart. */
725
1299
  export interface TrackerInfo {
@@ -743,6 +1317,267 @@ export interface TrackerInfo {
743
1317
  * snap to the series (the axis pills read like ticks), not the raw mouse Y.
744
1318
  */
745
1319
  export type CursorMode = 'none' | 'line' | 'point' | 'inline' | 'flag' | 'crosshair' | 'region';
1320
+ /**
1321
+ * How a cursor wants the shared `cursorX` snapped, **declared, resolved by the
1322
+ * container** (interaction RFC A2.3). The x-snap consults each layer's
1323
+ * `sampleAt` in the hovered row and writes the result into the shared
1324
+ * {@link CursorFrame.cursorX} every other row reads — a cursor component has
1325
+ * neither the layers nor the right to write that value, so it declares the
1326
+ * policy and the container resolves it. Container resolves; slots draw.
1327
+ *
1328
+ * - `'none'` — the raw pointer x.
1329
+ * - `'sample'` — snap to the nearest data sample's x (the crosshair).
1330
+ * - `'sequence'` — bucket-shaped: the *rendering* snaps to the realized
1331
+ * sequence buckets (the range cursor's band); `cursorX` itself stays raw.
1332
+ */
1333
+ export type CursorSnapX = 'none' | 'sample' | 'sequence';
1334
+ /**
1335
+ * One resolved per-series measurement at the cursor — **finished numbers, not
1336
+ * raw materials** (interaction RFC A2.3): the sample's plot pixels, the axis it
1337
+ * scales against (id + side, so a pill can hug the right gutter), and its value
1338
+ * already formatted by that axis's formatter. A cursor slot draws these; it
1339
+ * never sees a scale, a format map, or an axis-side map.
1340
+ */
1341
+ export interface ResolvedCursorSample {
1342
+ readonly px: number;
1343
+ readonly py: number;
1344
+ readonly axisId: string;
1345
+ readonly side: 'left' | 'right';
1346
+ readonly formatted: string;
1347
+ readonly color: string;
1348
+ readonly label: string;
1349
+ }
1350
+ /** A resolved consolidated multi-value flag (a {@link RowLayer.cursorFlag}
1351
+ * layer — the BoxPlot): its anchor pixels + the formatted, coloured lines. */
1352
+ export interface ResolvedCursorFlag {
1353
+ readonly px: number;
1354
+ readonly topPy: number;
1355
+ readonly lines: readonly {
1356
+ readonly text: string;
1357
+ readonly color: string;
1358
+ }[];
1359
+ }
1360
+ /**
1361
+ * The frame handed to a {@link CursorSpec}'s render slots — resolved geometry
1362
+ * and finished measurements (RFC A2.3), never scales or format maps.
1363
+ *
1364
+ * **Internal for now** (deliberately not exported from `index.ts`): RFC Q3
1365
+ * publishes the cursor contract only after every built-in preset — and the SR
1366
+ * gapped crosshair — is written against it. The presets in `cursors.tsx` are
1367
+ * that litmus; until it passes, this shape may still move.
1368
+ */
1369
+ export interface ResolvedCursorFrame {
1370
+ /** The shared plot-pixel x ({@link CursorFrame.cursorX}) — may be out of
1371
+ * `[0, plotWidth]` (a controlled tracker extrapolated); slots gate. */
1372
+ readonly cursorX: number | null;
1373
+ /** The hovered plot-pixel y (row-local; see {@link CursorFrame.cursorY}). */
1374
+ readonly cursorY: number | null;
1375
+ /** The **renderer's own** row, `null` in the x-axis slot… */
1376
+ readonly rowKey: symbol | null;
1377
+ /** …alongside the **hovered** row (A1.3: a slot needs both to know whether
1378
+ * it is drawing in the row the pointer is in). */
1379
+ readonly hoveredRowKey: symbol | null;
1380
+ /** Per-series resolved measurements at the cursor time (empty when the
1381
+ * effective cursors declared no need for them, or nothing is hovered). */
1382
+ readonly samples: readonly ResolvedCursorSample[];
1383
+ /** Resolved consolidated flags (BoxPlot) — the flag cursor's one-chip form. */
1384
+ readonly flags: readonly ResolvedCursorFlag[];
1385
+ /**
1386
+ * The **raw pointer**'s y resolved against the row's default axis — position,
1387
+ * formatted value, and axis side — or `null` when this row isn't hovered.
1388
+ * The free (non-snapping) crosshair reads this; it is resolved here because a
1389
+ * slot has no `yScale.invert` to do it itself.
1390
+ */
1391
+ readonly pointer: {
1392
+ readonly py: number;
1393
+ readonly formatted: string;
1394
+ readonly side: 'left' | 'right';
1395
+ } | null;
1396
+ /** The range cursor's **band** under the pointer (bucket-snapped via the
1397
+ * declared sequence, else the drag span), as clamped plot pixels; `null`
1398
+ * when nothing to shade. Resolved by the container from `regionSpan`. */
1399
+ readonly band: {
1400
+ readonly x0: number;
1401
+ readonly x1: number;
1402
+ } | null;
1403
+ /**
1404
+ * The **transposed band** — the same brush over a layer that declares
1405
+ * `sweepAxis: 'y'` (a horizontal `<BarChart>`, whose bins run down the
1406
+ * screen), as plot pixels on the row's own y axis. `null` in every other
1407
+ * state, including an x sweep, which paints {@link band}.
1408
+ *
1409
+ * Row-local for {@link rect}'s reason: a y interval only means anything
1410
+ * against the axis that measured it, so the row owning the drag resolves it
1411
+ * and the others stay `null`. It is drawn by the same `renderBrushBand` with
1412
+ * the geometry transposed — one renderer, so the two orientations of one
1413
+ * gesture cannot drift (§8.1).
1414
+ */
1415
+ readonly bandY: {
1416
+ readonly y0: number;
1417
+ readonly y1: number;
1418
+ } | null;
1419
+ /** Degenerate range cursor (no buckets, not mid-drag): draw a plain line. */
1420
+ readonly bandLine: boolean;
1421
+ /**
1422
+ * Whether the band's extent is being **dragged** right now — a range drag
1423
+ * anchored, or a `<MultiSelector>` sweep past its slop — rather than merely
1424
+ * previewing the block under the pointer.
1425
+ *
1426
+ * The band renders in both states; this is what separates them. The edges
1427
+ * are the gesture's grabbed boundary, so they draw only while there is a
1428
+ * gesture: at rest the band is a *preview*, and edging a preview reads as a
1429
+ * committed range the user hasn't made yet.
1430
+ */
1431
+ readonly bandDragging: boolean;
1432
+ /**
1433
+ * The **2-D brush rect** — a live `<MultiSelector>` sweep over a `twoD`
1434
+ * layer ({@link SweepSession.twoD}: a scatter, a heat map), as clamped plot
1435
+ * pixels. `null` in every other state, including a 1-D sweep, which paints
1436
+ * {@link band} instead.
1437
+ *
1438
+ * It is deliberately **not** a band with a y range bolted on. The band is
1439
+ * container state, so every row paints it from one anchor — right for an
1440
+ * x-range, which means the same thing in every row. A rect's y is only
1441
+ * meaningful in the row whose layer's axis it was measured against, so this
1442
+ * is resolved per row by the row that owns the drag, and stays `null` in
1443
+ * the others.
1444
+ *
1445
+ * **Unsorted, on purpose:** `(x0, y0)` is the corner the drag was anchored
1446
+ * at and `(x1, y1)` the corner under the pointer, so either may be the
1447
+ * larger. The renderer sorts for the box, but it needs the diagonal intact
1448
+ * to put the two crosshairs on the corners the user is actually holding.
1449
+ * (`x` is still the *snapped* band edge where the layer snaps — assigned to
1450
+ * whichever end the anchor is on.)
1451
+ */
1452
+ readonly rect: {
1453
+ readonly x0: number;
1454
+ readonly x1: number;
1455
+ readonly y0: number;
1456
+ readonly y1: number;
1457
+ } | null;
1458
+ /**
1459
+ * Draw the **resting** 2-D brush — a small grey `+` at the pointer, the
1460
+ * rect gesture's answer to {@link band}'s resting block preview.
1461
+ *
1462
+ * Small on purpose, and not a crosshair in the usual sense. A full-plot
1463
+ * crosshair is a value-reading instrument: it exists to project the pointer
1464
+ * onto both axes. This one marks *a corner a rect would start from*, and
1465
+ * the rect draws its own edges out to those axes the moment a drag begins —
1466
+ * so plot-spanning rules would add two more lines to a picture that is
1467
+ * about to have them anyway. The compact `+` says "here", which is all a
1468
+ * corner needs to say.
1469
+ */
1470
+ readonly restingCross: boolean;
1471
+ /** The cursor time, formatted by the container's readout channel
1472
+ * (`formatReadout ?? formatTime` in a row; the axis's own resolved readout
1473
+ * formatter in the x-axis slot). `null` when out of bounds / not wanted. */
1474
+ readonly formattedTime: string | null;
1475
+ readonly plotWidth: number;
1476
+ /** The renderer's row height (`0` in the x-axis slot). */
1477
+ readonly rowHeight: number;
1478
+ /** Whether this row is the topmost — the shared time chip shows once, here. */
1479
+ readonly isFirstRow: boolean;
1480
+ readonly theme: ChartTheme;
1481
+ /** X-axis slot placement (set only when invoking {@link CursorSpec.renderXAxis}):
1482
+ * which side the axis strip is on and the pill's tick-label offset. */
1483
+ readonly xAxis: {
1484
+ readonly onTop: boolean;
1485
+ readonly pillOffset: number;
1486
+ } | null;
1487
+ }
1488
+ /**
1489
+ * A mounted cursor's contract with the container (interaction RFC A2.3):
1490
+ * **declared** snap plus up to three render slots taking resolved geometry.
1491
+ * The container resolves (`cursorX` snapping, per-sample measurements, the
1492
+ * band); the slots draw. Registered via {@link ContainerFrame.registerCursor}
1493
+ * — the same idiom as `registerAxis` / `registerLayer`.
1494
+ *
1495
+ * Internal for now, like {@link ResolvedCursorFrame} (RFC Q3).
1496
+ */
1497
+ export interface CursorSpec {
1498
+ /** How the shared `cursorX` snaps while this cursor owns the hovered row. */
1499
+ readonly snapX?: CursorSnapX;
1500
+ /** SVG into the row's cursor overlay (above the data canvas — hovering never
1501
+ * repaints the canvas). */
1502
+ renderPlot?(f: ResolvedCursorFrame): ReactNode;
1503
+ /**
1504
+ * DOM into the row's overlay, above the SVG — the value chips (inline /
1505
+ * flag) and the in-plot time readout. NOT in RFC A2.3's three-slot shape:
1506
+ * the production flag/inline cursors are DOM chips positioned in plot space,
1507
+ * which "SVG into the overlay" cannot express — the same gap A1.3 called on
1508
+ * §5. Kept internal; the published contract must resolve this (see the
1509
+ * step-2 notes in the charts plan).
1510
+ */
1511
+ renderPlotHtml?(f: ResolvedCursorFrame): ReactNode;
1512
+ /** DOM, per row: the axis-edge **value pill** (positioned via `axisPillX`,
1513
+ * overflowing the plot into its axis gutter). */
1514
+ renderYGutter?(f: ResolvedCursorFrame): ReactNode;
1515
+ /** DOM, on the shared x axis: the **time pill** (+ its connector). `<XAxis>`
1516
+ * shows it whenever the hovered row's effective cursor registers this slot
1517
+ * — the mount is the gate, not a mode string. */
1518
+ renderXAxis?(f: ResolvedCursorFrame): ReactNode;
1519
+ }
1520
+ /** What a cursor needs the container to resolve per pointer move — declared at
1521
+ * registration so a line-only cursor never pays for per-sample measurement. */
1522
+ export interface CursorWants {
1523
+ /** Per-series {@link ResolvedCursorSample}s (dots, chips, the reticle pick). */
1524
+ readonly samples: boolean;
1525
+ /** Consolidated {@link ResolvedCursorFlag}s (the flag cursor only). */
1526
+ readonly flags: boolean;
1527
+ /** The range band (+ the degenerate band line). */
1528
+ readonly band: boolean;
1529
+ /** The raw-pointer readout (the free crosshair). */
1530
+ readonly pointer: boolean;
1531
+ /** The formatted in-plot cursor time (`showTime` presets). */
1532
+ readonly time: boolean;
1533
+ }
1534
+ /**
1535
+ * A registered cursor as the container holds it: the {@link CursorSpec} plus
1536
+ * the resolution inputs that must live on the registration rather than the
1537
+ * spec — scope, gesture ownership, and the declared needs.
1538
+ */
1539
+ export interface CursorEntry {
1540
+ readonly spec: CursorSpec;
1541
+ /** Mount scope: a row's key when mounted inside a `<ChartRow>` (the per-row
1542
+ * override), `null` when mounted at the container (the default for all rows). */
1543
+ readonly rowKey: symbol | null;
1544
+ /** Synthesized by the deprecation shim from the legacy string props. A scope
1545
+ * with any non-legacy (component-mounted) cursor drops its legacy entries —
1546
+ * mounting a component overrides the string prop during the window. */
1547
+ readonly legacy: boolean;
1548
+ /**
1549
+ * The shim entry nobody asked for: the container's `'line'` **default**,
1550
+ * synthesized with no `cursor` prop set. A mounted `<MultiSelector>`'s
1551
+ * resting block preview (the brush band as the resting cursor) replaces
1552
+ * only these — an explicitly chosen cursor, component-mounted or via the
1553
+ * legacy string prop, still wins.
1554
+ */
1555
+ readonly implicit?: boolean;
1556
+ /**
1557
+ * Whether this cursor owns **snap and gesture** (RFC A2.5): at most one per
1558
+ * scope (dev-warned otherwise), resolved to the hovered row's innermost
1559
+ * mount. Render-only presets (line/point/inline/flag) stack freely.
1560
+ */
1561
+ readonly ownsGesture: boolean;
1562
+ readonly wants: CursorWants;
1563
+ /** The range cursor's bucket sequence (realized by the container into the
1564
+ * shared snap buckets — the `cursorSequence` successor). */
1565
+ readonly sequence?: Sequence | BoundedSequence | undefined;
1566
+ /** The range cursor's drag-release callback (the `onRegionSelect`
1567
+ * successor) — read by the brush recognizer (`resolveRangeDrag`), which
1568
+ * wraps it into the range-drag session. Only a `<RangeCursor>` sets it. */
1569
+ readonly onDragRelease?: ((span: RangeSpan) => void) | undefined;
1570
+ /** Whether the range drag is live — **resolved** at build time
1571
+ * (`enableDrag ?? !!onDragRelease`), so `false` here means frozen: the
1572
+ * gesture is off even though the callback is wired (§6's OFF switch). */
1573
+ readonly enableDrag?: boolean | undefined;
1574
+ /** The modifier the range drag needs (the `regionSelectModifier`
1575
+ * successor) — only enforced while pan is enabled. */
1576
+ readonly dragModifier?: 'shift' | undefined;
1577
+ /** The cursor's readout format (the `cursorFormat` successor) — resolved by
1578
+ * the container into the shared readout channel. */
1579
+ readonly format?: CursorFormat | undefined;
1580
+ }
746
1581
  /** A registered layer plus the axis id it draws against. */
747
1582
  export interface LayerEntry {
748
1583
  readonly layer: RowLayer;