@pond-ts/charts 0.70.0 → 0.71.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/HeatMap.d.ts CHANGED
@@ -161,9 +161,9 @@ export interface HeatMapProps<S extends SeriesSchema = SeriesSchema, VS extends
161
161
  * theme, and the M5 "theme tokens optional-with-default" gate has to land
162
162
  * first. Borrowing defers that decision instead of pre-empting it.
163
163
  *
164
- * **Pair it with `<ChartContainer cursor="none">`.** The container's default is
165
- * the shared vertical line, and on a grid that is a *second, weaker* cursor
166
- * competing with the one that already works: the cell under the pointer takes an
164
+ * **Mount no cursor component with it.** A `<LineCursor>` draws the shared
165
+ * vertical line, and on a grid that is a *second, weaker* cursor competing
166
+ * with the one that already works: the cell under the pointer takes an
167
167
  * outline, which says both axes at once. The line says only x, and a heat map's
168
168
  * x position is rarely the question. The pointer's own crosshair shape plus the
169
169
  * cell outline is the whole affordance.
package/dist/HeatMap.js CHANGED
@@ -61,9 +61,9 @@ function marksOf(set) {
61
61
  * theme, and the M5 "theme tokens optional-with-default" gate has to land
62
62
  * first. Borrowing defers that decision instead of pre-empting it.
63
63
  *
64
- * **Pair it with `<ChartContainer cursor="none">`.** The container's default is
65
- * the shared vertical line, and on a grid that is a *second, weaker* cursor
66
- * competing with the one that already works: the cell under the pointer takes an
64
+ * **Mount no cursor component with it.** A `<LineCursor>` draws the shared
65
+ * vertical line, and on a grid that is a *second, weaker* cursor competing
66
+ * with the one that already works: the cell under the pointer takes an
67
67
  * outline, which says both axes at once. The line says only x, and a heat map's
68
68
  * x position is rarely the question. The pointer's own crosshair shape plus the
69
69
  * cell outline is the whole affordance.
package/dist/Layers.js CHANGED
@@ -2,7 +2,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
2
2
  import { Children, Fragment, isValidElement, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
3
3
  import { Canvas } from './Canvas.js';
4
4
  import { drawGrid, drawDividers, dividerAlphas, thinPixels } from './grid.js';
5
- import { bandRect, regionSpan } from './tracker.js';
5
+ import { bandRect, categorySlots, regionSpan } from './tracker.js';
6
6
  import { effectiveCursorEntries, gestureOwner } from './cursors.js';
7
7
  import { renderBrushBand, renderBrushRect, resolveBrushClaim, resolveRangeDrag, warnSweepShadowsRangeDrag, } from './brush.js';
8
8
  import { resolveSelection } from './select.js';
@@ -443,12 +443,10 @@ export function Layers({ children }) {
443
443
  //
444
444
  // - `blockPreview` — the fact ("this row previews blocks"). It also scopes
445
445
  // the resting hover to the snap block (handlePointerMove).
446
- // - `restingBand` — the brush band is this row's resting CURSOR, replacing
447
- // the shim's un-asked-for `'line'` default. Any *explicitly chosen*
448
- // cursor still wins: a mounted component, or a legacy `cursor` string the
449
- // consumer actually set — both register non-`implicit` entries and keep
450
- // their own slots (a mounted `<RangeCursor>` already draws this same
451
- // band; a `<CrosshairCursor>` keeps its crosshair).
446
+ // - `restingBand` — the brush band is this row's resting CURSOR when no
447
+ // cursor component is mounted for the row. A mounted cursor still wins
448
+ // and keeps its own slots (a mounted `<RangeCursor>` already draws this
449
+ // same band; a `<CrosshairCursor>` keeps its crosshair).
452
450
  //
453
451
  // Which of the two the row gets is the TOPMOST sweep-capable layer's
454
452
  // business (§8's z-order rule again, and the same rule `beginTopmostSweep`
@@ -466,13 +464,14 @@ export function Layers({ children }) {
466
464
  !rectPreview &&
467
465
  !topmostSweepSpanOnly(layers) &&
468
466
  layers.some((e) => e.layer.beginSweep !== undefined);
469
- // The resting brush replaces the implicit cursor either way; only its SHAPE
470
- // differs. A 2-D row gets no band: its snap block is a whole x column while
467
+ // The resting brush is the row's cursor when no cursor component is
468
+ // mounted for it (a mounted cursor was asked for, so it wins); only its
469
+ // SHAPE differs. A 2-D row gets no band: its snap block is a whole x column while
471
470
  // a drag there captures a rect, so a band would advertise a set the gesture
472
471
  // never selects (the same reason the block hover opts out).
473
472
  const restingBrush = (blockPreview || rectPreview) &&
474
473
  !editingActive &&
475
- effectiveCursorEntries(container.cursors, row.rowKey).every((e) => e.implicit === true);
474
+ effectiveCursorEntries(container.cursors, row.rowKey).length === 0;
476
475
  // …and a **transposed** row gets none yet: the resting block preview is
477
476
  // resolved from the shared x buckets, so a y-cutting row would draw a band
478
477
  // over a column its drag can never select. Suppressed rather than
@@ -484,6 +483,7 @@ export function Layers({ children }) {
484
483
  ? []
485
484
  : effectiveCursorEntries(container.cursors, row.rowKey), [editingActive, sweeping, restingBrush, container.cursors, row.rowKey]);
486
485
  const wantsSamples = cursorEntries.some((e) => e.wants.samples);
486
+ const wantsReticle = cursorEntries.some((e) => e.wants.reticle);
487
487
  const wantsFlags = cursorEntries.some((e) => e.wants.flags);
488
488
  const wantsBand = cursorEntries.some((e) => e.wants.band);
489
489
  const wantsPointer = cursorEntries.some((e) => e.wants.pointer);
@@ -502,15 +502,21 @@ export function Layers({ children }) {
502
502
  // slides under it. Empty when not hovering — or when no effective cursor
503
503
  // declared a need — so the data canvas is never touched and a line-only
504
504
  // cursor never pays the per-layer walk.
505
- const trackerSamples = useMemo(() => {
506
- if (cursorTime === null || !wantsSamples)
507
- return [];
508
- const out = [];
505
+ //
506
+ // Two lists from one walk. `samples` feeds the per-series marks (dots,
507
+ // chips), so a layer with a consolidated flag (BoxPlot) is left out — it
508
+ // renders that flag instead (its values still fan to the off-chart readout
509
+ // via sampleAt on the container). `reticle` feeds the crosshair's pick,
510
+ // which lands on ONE value, so a box's quantiles are candidates there
511
+ // ([PND-BOXPLT]).
512
+ const { samples: trackerSamples, reticle: reticleSamples } = useMemo(() => {
513
+ if (cursorTime === null || (!wantsSamples && !wantsReticle))
514
+ return { samples: [], reticle: [] };
515
+ const samples = [];
516
+ const reticle = [];
509
517
  for (const entry of layers) {
510
- // A layer with a consolidated flag (BoxPlot) renders that, not per-sample
511
- // dots/chips — skip it here (its values still fan to the off-chart readout
512
- // via sampleAt on the container).
513
- if (entry.layer.cursorFlag)
518
+ const toSamples = wantsSamples && !entry.layer.cursorFlag;
519
+ if (!toSamples && !wantsReticle)
514
520
  continue;
515
521
  const axisId = entry.axisId ?? defaultAxisId;
516
522
  const yScale = yScales.get(axisId);
@@ -526,9 +532,12 @@ export function Layers({ children }) {
526
532
  const axisOffset = axisOffsets.get(axisId) ?? 0;
527
533
  const axisColor = axisColors.get(axisId);
528
534
  for (const s of entry.layer.sampleAt(cursorTime)) {
529
- out.push({
535
+ const r = {
530
536
  px: xScale(s.x),
531
537
  py: yScale(s.value),
538
+ x: s.x,
539
+ value: s.value,
540
+ ...(s.readout !== undefined ? { readout: s.readout } : {}),
532
541
  axisId,
533
542
  side,
534
543
  axisOffset,
@@ -536,13 +545,18 @@ export function Layers({ children }) {
536
545
  formatted: fmt(s.value),
537
546
  color: s.color,
538
547
  label: s.label,
539
- });
548
+ };
549
+ if (toSamples)
550
+ samples.push(r);
551
+ if (wantsReticle)
552
+ reticle.push(r);
540
553
  }
541
554
  }
542
- return out;
555
+ return { samples, reticle };
543
556
  }, [
544
557
  cursorTime,
545
558
  wantsSamples,
559
+ wantsReticle,
546
560
  layers,
547
561
  yScales,
548
562
  formats,
@@ -631,7 +645,7 @@ export function Layers({ children }) {
631
645
  // (#508 item 7). Trusted human-paced input hides this (React flushes
632
646
  // trusted discrete events synchronously); the ref is correct under both.
633
647
  // The release sink rides the ref too, so what fires is what the press
634
- // resolved — a `<RangeCursor onDragRelease>` or the legacy `onRegionSelect`.
648
+ // resolved — the `<RangeCursor onDragRelease>`.
635
649
  const rangeDragRef = useRef(null);
636
650
  // The live sweep session (a mounted <MultiSelector> — RFC §8 / A7.7): the
637
651
  // anchor + the topmost capable layer's per-drag session + the container's
@@ -841,7 +855,7 @@ export function Layers({ children }) {
841
855
  const swept = sweepGesture !== null ? beginTopmostSweep(c, r) : null;
842
856
  const drag = resolveRangeDrag(c, gestureOwner(effectiveCursorEntries(c.cursors, r.rowKey)));
843
857
  // ONE brush recognizer arbitrates every drag claim — annotation-create,
844
- // the sweep, the range drag (component or legacy), pan — in a
858
+ // the sweep, the range drag, pan — in a
845
859
  // documented order (RFC A1.5 / A2.7; see brush.tsx). This handler only
846
860
  // routes.
847
861
  const claim = resolveBrushClaim({
@@ -895,8 +909,7 @@ export function Layers({ children }) {
895
909
  }
896
910
  return;
897
911
  }
898
- // Range drag (a drag-enabled <RangeCursor>, or the legacy
899
- // `cursor="region"` + `onRegionSelect`): anchor the selection at the
912
+ // Range drag (a drag-enabled <RangeCursor>): anchor the selection at the
900
913
  // press; the band then extends as the pointer moves (bucket by bucket
901
914
  // with a sequence, freeform without), and release commits the span to
902
915
  // whichever sink the claim resolved. Continuous x only, and gated
@@ -1119,8 +1132,8 @@ export function Layers({ children }) {
1119
1132
  const t = +c.xScale.invert(rawX);
1120
1133
  for (let i = r.layers.length - 1; i >= 0; i -= 1) {
1121
1134
  const entry = r.layers[i];
1122
- if (entry.layer.cursorFlag)
1123
- continue;
1135
+ // Box plots included ([PND-BOXPLT]): a box's samples sit at its
1136
+ // centre, so the reticle lands mid-box.
1124
1137
  const s = entry.layer.sampleAt(t)[0];
1125
1138
  if (s !== undefined) {
1126
1139
  px = c.xScale(s.x);
@@ -1269,7 +1282,7 @@ export function Layers({ children }) {
1269
1282
  }
1270
1283
  // End a range drag: commit the anchor→pointer span as a one-shot range —
1271
1284
  // to the sink the press resolved (`<RangeCursor onDragRelease>`'s
1272
- // `{ x: [lo, hi] }`, or the legacy `onRegionSelect` bare pair) — then
1285
+ // `{ x: [lo, hi] }`) — then
1273
1286
  // clear the anchor: the cursor **reverts** to the single-bucket
1274
1287
  // highlight (it does not keep the range). The anchor is read from the
1275
1288
  // ref, never the state mirror — under a batched pointer stream the
@@ -1574,10 +1587,9 @@ export function Layers({ children }) {
1574
1587
  const formattedTime = wantsTime && cursorTime !== null
1575
1588
  ? (container.formatReadout ?? formatTime)(cursorTime)
1576
1589
  : null;
1577
- // The range cursor's band (continuous x axis — time or value): shade the
1578
- // span under the pointer. With snap buckets (a sequence / a histogram's
1590
+ // The range cursor's band: shade the span under the pointer. With snap buckets (a sequence / a histogram's
1579
1591
  // bins) the band snaps to the bucket (and extends bucket by bucket under a
1580
- // legacy drag); with none it's the **freeform** case — a bare hover draws a
1592
+ // drag); with none it's the **freeform** case — a bare hover draws a
1581
1593
  // plain line (`bandLine`), a drag shades the raw `[anchor, pointer]`. Edges
1582
1594
  // map through `xScale`, so on a trading-time axis the band crops to live time.
1583
1595
  // A live <MultiSelector> sweep shades the same band — and so does its
@@ -1594,12 +1606,27 @@ export function Layers({ children }) {
1594
1606
  // not ALSO shade an x band: `sweeping` alone would resolve one from the
1595
1607
  // pointer's bucket and lay a full-height column across the horizontal band
1596
1608
  // the drag is actually drawing.
1597
- const bandActive = (wantsBand &&
1598
- (container.xKind === 'time' || container.xKind === 'value')) ||
1609
+ //
1610
+ // A `<RangeCursor>` on a **category** axis shades the slot under the
1611
+ // pointer ([PND-ORDCURSOR]). Its buckets are the unit slots `[i, i+1)` —
1612
+ // a vertical bar layer already publishes exactly those as `cursorBuckets`;
1613
+ // a category row with no bar layer (a heat map) gets them from the band
1614
+ // scale's domain (`categoryBandSlots`). The range *drag* stays off there
1615
+ // (`resolveRangeDrag`): it reports a numeric span, and a category chart
1616
+ // selects slots, which is `<MultiSelector>`'s gesture.
1617
+ // The slot fallback is for the cursor's band only: a sweep or the resting
1618
+ // brush keeps reading `cursorBuckets` alone, exactly as before. Memoized on
1619
+ // the scale, so a pointer move doesn't rebuild the slots.
1620
+ const categoryBandSlots = useMemo(() => wantsBand &&
1621
+ container.xKind === 'category' &&
1622
+ container.cursorBuckets === undefined
1623
+ ? categorySlots(xScale.domain())
1624
+ : undefined, [wantsBand, container.xKind, container.cursorBuckets, xScale]);
1625
+ const bandActive = wantsBand ||
1599
1626
  (sweeping && sweepRect === null && sweepBandY === null) ||
1600
1627
  restingBand;
1601
1628
  const band = bandActive && cursorTime !== null
1602
- ? bandRect(container.cursorBuckets ?? [], cursorTime, (v) => xScale(v), plotWidth, container.regionAnchor ?? undefined)
1629
+ ? bandRect(container.cursorBuckets ?? categoryBandSlots ?? [], cursorTime, (v) => xScale(v), plotWidth, container.regionAnchor ?? undefined)
1603
1630
  : null;
1604
1631
  // Degenerate range cursor (no buckets, not mid-drag): a plain vertical
1605
1632
  // line. Deliberately NOT extended to `restingBand` — the resting preview is
@@ -1627,6 +1654,7 @@ export function Layers({ children }) {
1627
1654
  rowKey: row.rowKey,
1628
1655
  hoveredRowKey: cursor.cursorRowKey,
1629
1656
  samples: trackerSamples,
1657
+ reticleSamples,
1630
1658
  flags: trackerFlags,
1631
1659
  pointer,
1632
1660
  band,
@@ -1642,6 +1670,28 @@ export function Layers({ children }) {
1642
1670
  theme: container.theme,
1643
1671
  xAxis: null,
1644
1672
  };
1673
+ // The crosshair's snap report (`<CrosshairCursor onSnap>`). The snap owner is
1674
+ // read from the unfiltered entries, so a row whose cursor is hidden (editing,
1675
+ // a sweep, the resting brush) still reports "nothing snapped" rather than
1676
+ // leaving the consumer on a stale point. Only the hovered row reports a
1677
+ // point; the other rows report `null` — unless the hovered row has the same
1678
+ // owner, in which case it speaks for all of them (two rows calling at once
1679
+ // would flap). The cursor dedupes, so repeated calls are free.
1680
+ const snapOwner = gestureOwner(effectiveCursorEntries(container.cursors, row.rowKey));
1681
+ const hoveredRowKey = cursor.cursorRowKey;
1682
+ useEffect(() => {
1683
+ const report = snapOwner?.reportSnap;
1684
+ if (report === undefined)
1685
+ return;
1686
+ if (hoveredRowKey === row.rowKey) {
1687
+ report(cursorEntries.includes(snapOwner) ? cursorRenderFrame : null);
1688
+ }
1689
+ else if (hoveredRowKey === null ||
1690
+ gestureOwner(effectiveCursorEntries(container.cursors, hoveredRowKey)) !==
1691
+ snapOwner) {
1692
+ report(null);
1693
+ }
1694
+ });
1645
1695
  // Cross-row guide lines: the x-positions of annotations on the OTHER rows
1646
1696
  // (markers + region edges), so a mark on one row reads against this row's data +
1647
1697
  // the shared x axis. A mark's own row skips itself; baselines cast no vertical
package/dist/XAxis.js CHANGED
@@ -667,6 +667,7 @@ export function XAxis({ format, label, side = 'bottom', height, ticks: customTic
667
667
  rowKey: null,
668
668
  hoveredRowKey: cursor.cursorRowKey,
669
669
  samples: [],
670
+ reticleSamples: [],
670
671
  flags: [],
671
672
  pointer: null,
672
673
  band: null,
package/dist/area.d.ts CHANGED
@@ -27,8 +27,9 @@ export declare function fillAffineArea(ctx: CanvasRenderingContext2D, xs: Float6
27
27
  * The `[min, max]` vertical extent an area occupies — the finite values of
28
28
  * `cs.y` widened to include `baseline`, since the fill spans from each value to
29
29
  * the baseline (so the baseline must be in-domain or the fill clips). `null` if
30
- * no value is finite. When `baseline` is `undefined` the area rests on the
31
- * axis's own lower bound (resolved later), so only the values constrain the
30
+ * no value is finite. When `baseline` is `undefined` (`<AreaChart
31
+ * baseline="floor">`) the area rests on the axis's own lower bound (resolved
32
+ * later), so only the values constrain the
32
33
  * domain — matching {@link yExtent}.
33
34
  *
34
35
  * NaN values (the gap signal) are ignored, so a coast doesn't drag the domain.
@@ -41,10 +42,10 @@ export declare function areaExtent(cs: ChartSeries, baseline: number | undefined
41
42
  *
42
43
  * Two forms, selected by `baseline`:
43
44
  *
44
- * - **Elevation** (`baseline` = the axis lower bound, supplied as the resolved
45
- * `baselineValue`): the line sits above the baseline, the shade grades down
45
+ * - **Floor** (`<AreaChart baseline="floor">` — the axis lower bound,
46
+ * supplied as the resolved `baselineValue`): the line sits above the baseline, the shade grades down
46
47
  * from it — the estela elevation look.
47
- * - **Above/below axis** (`baseline` = `0`): positive values fill up, negative
48
+ * - **Above/below a level** (`baseline` = `0`, the default, or any number): positive values fill up, negative
48
49
  * fill down (d3's `area` handles the zero crossing in one path). The gradient
49
50
  * is anchored at the baseline pixel so each side grades *away* from the axis —
50
51
  * opaque at the line, transparent at the axis — in both directions. Compose
package/dist/area.js CHANGED
@@ -89,8 +89,9 @@ export function fillAffineArea(ctx, xs, ys, baselinePx, ax, ay) {
89
89
  * The `[min, max]` vertical extent an area occupies — the finite values of
90
90
  * `cs.y` widened to include `baseline`, since the fill spans from each value to
91
91
  * the baseline (so the baseline must be in-domain or the fill clips). `null` if
92
- * no value is finite. When `baseline` is `undefined` the area rests on the
93
- * axis's own lower bound (resolved later), so only the values constrain the
92
+ * no value is finite. When `baseline` is `undefined` (`<AreaChart
93
+ * baseline="floor">`) the area rests on the axis's own lower bound (resolved
94
+ * later), so only the values constrain the
94
95
  * domain — matching {@link yExtent}.
95
96
  *
96
97
  * NaN values (the gap signal) are ignored, so a coast doesn't drag the domain.
@@ -126,10 +127,10 @@ export function areaExtent(cs, baseline) {
126
127
  *
127
128
  * Two forms, selected by `baseline`:
128
129
  *
129
- * - **Elevation** (`baseline` = the axis lower bound, supplied as the resolved
130
- * `baselineValue`): the line sits above the baseline, the shade grades down
130
+ * - **Floor** (`<AreaChart baseline="floor">` — the axis lower bound,
131
+ * supplied as the resolved `baselineValue`): the line sits above the baseline, the shade grades down
131
132
  * from it — the estela elevation look.
132
- * - **Above/below axis** (`baseline` = `0`): positive values fill up, negative
133
+ * - **Above/below a level** (`baseline` = `0`, the default, or any number): positive values fill up, negative
133
134
  * fill down (d3's `area` handles the zero crossing in one path). The gradient
134
135
  * is anchored at the baseline pixel so each side grades *away* from the axis —
135
136
  * opaque at the line, transparent at the axis — in both directions. Compose
@@ -509,7 +510,13 @@ export function areaHitIndex(cs, baseline, px, py, xScale, yScale, tolerance = T
509
510
  // slope and not a staircase. Guard the degenerate same-pixel pair.
510
511
  const t = xi === xj ? 0 : (px - xj) / (xi - xj);
511
512
  const edgePx = yScale(yj + (yi - yj) * Math.max(0, Math.min(1, t)));
512
- const basePx = baseline === undefined ? baselinePxFromScale(yScale) : yScale(baseline);
513
+ // A baseline with no position on this axis (zero on a log axis) falls back
514
+ // to the axis floor — the same resolution the draw uses. Without it `basePx`
515
+ // is NaN, both range checks below are false, and every point hits.
516
+ const fixedPx = baseline === undefined ? NaN : yScale(baseline);
517
+ const basePx = Number.isFinite(fixedPx)
518
+ ? fixedPx
519
+ : baselinePxFromScale(yScale);
513
520
  const top = Math.min(edgePx, basePx) - tolerance;
514
521
  const bottom = Math.max(edgePx, basePx) + tolerance;
515
522
  if (py < top || py > bottom)
@@ -44,11 +44,11 @@ export interface AxisMouseEvent {
44
44
  * elsewhere.
45
45
  *
46
46
  * Precisely: it is the axis's **readout** channel, the one the cursor pill
47
- * uses — so it always agrees with the pill at that pixel, and a container
48
- * `cursorFormat` shapes it exactly as it shapes the pill. That is the
49
- * documented precedence (`cursorFormat` → axis `format` → container), and it
47
+ * uses — so it always agrees with the pill at that pixel, and a cursor
48
+ * `format` shapes it exactly as it shapes the pill. That is the
49
+ * documented precedence (cursor `format` → axis `format` → container), and it
50
50
  * is the one case where `label` can read differently from the tick text: a
51
- * chart with a precise `cursorFormat` over terse ticks gets the precise form
51
+ * chart with a precise cursor `format` over terse ticks gets the precise form
52
52
  * here, which is the readout it asked for.
53
53
  */
54
54
  label: string;
package/dist/brush.d.ts CHANGED
@@ -28,8 +28,7 @@ import type { ContainerFrame, CursorEntry, ResolvedCursorFrame } from './context
28
28
  * `DRAG_SLOP` — a plain click stays a click and selects one mark (§8.1:
29
29
  * the two are separated by movement, not modifier).
30
30
  * 3. **The range drag** ({@link resolveRangeDrag}) — a drag-enabled
31
- * `<RangeCursor>` in the hovered row's effective cursor set, else the
32
- * legacy `cursor="region"` + `onRegionSelect` container props. Preempts
31
+ * `<RangeCursor>` in the hovered row's effective cursor set. Preempts
33
32
  * pan — unless a `dragModifier` is declared **and pan is enabled**, in
34
33
  * which case a plain drag falls through to pan and only a modifier-held
35
34
  * drag brushes. (With pan off there is no gesture to share, so the
@@ -45,9 +44,8 @@ import type { ContainerFrame, CursorEntry, ResolvedCursorFrame } from './context
45
44
  * marks), which is A1.5's "one brush engine, two components".
46
45
  */
47
46
  /** What a completed range drag calls with the released `[start, end]`
48
- * (axis units, `start ≤ end`). Resolved once at pointer-down — the component
49
- * path wraps `onDragRelease` (the `{ x }` payload), the legacy path wraps
50
- * `onRegionSelect` (the bare pair). */
47
+ * (axis units, `start ≤ end`). Resolved once at pointer-down, wrapping the
48
+ * `<RangeCursor>`'s `onDragRelease` (the `{ x }` payload). */
51
49
  export interface RangeDrag {
52
50
  readonly release: (start: number, end: number) => void;
53
51
  /** The modifier the drag needs — only enforced while pan is enabled. */
@@ -55,23 +53,17 @@ export interface RangeDrag {
55
53
  }
56
54
  /**
57
55
  * Resolve whether a press could start a **range drag**, and who gets the
58
- * released span. Two sources, component first:
59
- *
60
- * 1. **A mounted `<RangeCursor>`** — the hovered row's effective
61
- * gesture-owning cursor (`owner`), when it carries `onDragRelease` and is
62
- * not frozen (`enableDrag={false}` — the OFF switch, which also suppresses
63
- * the legacy fallback: the consumer wired the new API and asked for no
64
- * gesture). A `<RangeCursor>` without `onDragRelease` has nothing to fire,
65
- * so it does not claim — the legacy props keep working underneath it
66
- * during the deprecation window (exactly the step-2 behaviour).
67
- * 2. **The legacy container props** — `cursor="region"` + `onRegionSelect`
68
- * (+ `regionSelectModifier`), byte-for-byte today's semantics, including
69
- * the bare-pair payload.
56
+ * released span: the hovered row's effective gesture-owning cursor (`owner`),
57
+ * when it is a `<RangeCursor>` carrying `onDragRelease` and is not frozen
58
+ * (`enableDrag={false}` — the OFF switch). A `<RangeCursor>` without
59
+ * `onDragRelease` has nothing to fire, so it does not claim.
70
60
  *
71
61
  * Continuous x only (time or value): a category axis has no span to drag
72
- * (an ordinal-slot select is a different gesture), so both paths gate on it.
62
+ * (an ordinal-slot select is `<MultiSelector>`'s gesture). The range cursor's
63
+ * hover band still draws there — over the slot — and the container dev-warns
64
+ * that the drag is off ([PND-ORDCURSOR]).
73
65
  */
74
- export declare function resolveRangeDrag(c: Pick<ContainerFrame, 'cursor' | 'onRegionSelect' | 'regionSelectModifier' | 'xKind'>, owner: CursorEntry | undefined): RangeDrag | null;
66
+ export declare function resolveRangeDrag(c: Pick<ContainerFrame, 'xKind'>, owner: CursorEntry | undefined): RangeDrag | null;
75
67
  /** Who owns the drag a pointer-down might start (see the module doc's
76
68
  * precedence order). `'none'` = the press is a potential click only. */
77
69
  export type BrushClaim = {
@@ -110,8 +102,8 @@ export declare function resolveBrushClaim(opts: {
110
102
  }): BrushClaim;
111
103
  /**
112
104
  * Dev-warn (once per plot surface) when a press found BOTH a mounted
113
- * `<MultiSelector>` and a live range drag (a drag-enabled `<RangeCursor>`, or
114
- * the legacy region props) competing for it — the sweep wins (see the module
105
+ * `<MultiSelector>` and a live range drag (a drag-enabled `<RangeCursor>`)
106
+ * competing for it — the sweep wins (see the module
115
107
  * doc's precedence), and a silent shadow would hide the loser exactly the way
116
108
  * A1.5 said docs alone couldn't.
117
109
  */
package/dist/brush.js CHANGED
@@ -1,44 +1,30 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /**
3
3
  * Resolve whether a press could start a **range drag**, and who gets the
4
- * released span. Two sources, component first:
5
- *
6
- * 1. **A mounted `<RangeCursor>`** — the hovered row's effective
7
- * gesture-owning cursor (`owner`), when it carries `onDragRelease` and is
8
- * not frozen (`enableDrag={false}` — the OFF switch, which also suppresses
9
- * the legacy fallback: the consumer wired the new API and asked for no
10
- * gesture). A `<RangeCursor>` without `onDragRelease` has nothing to fire,
11
- * so it does not claim — the legacy props keep working underneath it
12
- * during the deprecation window (exactly the step-2 behaviour).
13
- * 2. **The legacy container props** — `cursor="region"` + `onRegionSelect`
14
- * (+ `regionSelectModifier`), byte-for-byte today's semantics, including
15
- * the bare-pair payload.
4
+ * released span: the hovered row's effective gesture-owning cursor (`owner`),
5
+ * when it is a `<RangeCursor>` carrying `onDragRelease` and is not frozen
6
+ * (`enableDrag={false}` — the OFF switch). A `<RangeCursor>` without
7
+ * `onDragRelease` has nothing to fire, so it does not claim.
16
8
  *
17
9
  * Continuous x only (time or value): a category axis has no span to drag
18
- * (an ordinal-slot select is a different gesture), so both paths gate on it.
10
+ * (an ordinal-slot select is `<MultiSelector>`'s gesture). The range cursor's
11
+ * hover band still draws there — over the slot — and the container dev-warns
12
+ * that the drag is off ([PND-ORDCURSOR]).
19
13
  */
20
14
  export function resolveRangeDrag(c, owner) {
21
15
  if (c.xKind !== 'time' && c.xKind !== 'value')
22
16
  return null;
23
- if (owner !== undefined && !owner.legacy && owner.onDragRelease) {
24
- // Frozen: the gesture is off without unwiring the callback (§6's
25
- // `enableDrag`-as-disabler) — and the legacy fallback stays off too.
26
- if (owner.enableDrag === false)
27
- return null;
28
- const cb = owner.onDragRelease;
29
- return {
30
- release: (start, end) => cb({ x: [start, end] }),
31
- modifier: owner.dragModifier,
32
- };
33
- }
34
- if (c.cursor === 'region' && c.onRegionSelect !== undefined) {
35
- const cb = c.onRegionSelect;
36
- return {
37
- release: (start, end) => cb([start, end]),
38
- modifier: c.regionSelectModifier,
39
- };
40
- }
41
- return null;
17
+ if (owner === undefined || !owner.onDragRelease)
18
+ return null;
19
+ // Frozen: the gesture is off without unwiring the callback (§6's
20
+ // `enableDrag`-as-disabler).
21
+ if (owner.enableDrag === false)
22
+ return null;
23
+ const cb = owner.onDragRelease;
24
+ return {
25
+ release: (start, end) => cb({ x: [start, end] }),
26
+ modifier: owner.dragModifier,
27
+ };
42
28
  }
43
29
  /**
44
30
  * The claim decision at pointer-down — pure, so the precedence order is
@@ -71,8 +57,8 @@ export function resolveBrushClaim(opts) {
71
57
  }
72
58
  /**
73
59
  * Dev-warn (once per plot surface) when a press found BOTH a mounted
74
- * `<MultiSelector>` and a live range drag (a drag-enabled `<RangeCursor>`, or
75
- * the legacy region props) competing for it — the sweep wins (see the module
60
+ * `<MultiSelector>` and a live range drag (a drag-enabled `<RangeCursor>`)
61
+ * competing for it — the sweep wins (see the module
76
62
  * doc's precedence), and a silent shadow would hide the loser exactly the way
77
63
  * A1.5 said docs alone couldn't.
78
64
  */
@@ -80,8 +66,8 @@ export function warnSweepShadowsRangeDrag(warned) {
80
66
  if (warned.current)
81
67
  return;
82
68
  warned.current = true;
83
- console.warn('[pond-charts] a <MultiSelector> and a drag-enabled <RangeCursor> (or ' +
84
- 'the legacy onRegionSelect props) are both in scope for this plot — ' +
69
+ console.warn('[pond-charts] a <MultiSelector> and a drag-enabled <RangeCursor> are ' +
70
+ 'both in scope for this plot — ' +
85
71
  'the sweep claims the drag and the range drag never fires. Mount one ' +
86
72
  'drag owner per scope, or freeze the cursor with enableDrag={false}. ' +
87
73
  'See docs/rfcs/interaction.md A1.5 / §8.1.');