@pond-ts/charts 0.70.0 → 0.72.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,8 @@
1
1
  import { type ReactNode } from 'react';
2
2
  import { type DiscontinuityProvider, type TradingCalendarLike } from './tradingTimeScale.js';
3
- import { Sequence, BoundedSequence } from 'pond-ts';
4
3
  import type { TimeRange } from 'pond-ts';
5
- import { type AnnotationKind, type CreateSpec, type CursorMode, type TrackerInfo, type DrawStatsFrame } from './context.js';
6
- import { type AxisFormat, type CursorFormat } from './format.js';
4
+ import { type AnnotationKind, type CreateSpec, type TrackerInfo, type DrawStatsFrame } from './context.js';
5
+ import { type AxisFormat } from './format.js';
7
6
  import { type ChartTheme } from './theme.js';
8
7
  export interface ChartContainerProps {
9
8
  /**
@@ -210,7 +209,7 @@ export interface ChartContainerProps {
210
209
  * (`TimeZone`) places these ticks and cuts `Sequence.calendar` buckets, so
211
210
  * `Sequence.calendar('day', { timeZone })` and `<ChartContainer timeZone>`
212
211
  * given the same zone put a bucket edge and its tick on one instant.
213
- * Function formatters (`timeFormat`, `cursorFormat`) still receive epoch ms;
212
+ * Function formatters (`timeFormat`, a cursor's `format`) still receive epoch ms;
214
213
  * read the resolved zone from the chart context. An unknown id throws
215
214
  * `RangeError`. Only affects a **time** axis.
216
215
  */
@@ -387,93 +386,10 @@ export interface ChartContainerProps {
387
386
  *
388
387
  * **Omit or pass `null`** (equivalent) for no controlled position — a hovered
389
388
  * chart still tracks its pointer, a non-hovered one shows nothing. To force a
390
- * chart to *never* show a cursor, use `cursor="none"`, not `trackerPosition`.
391
- * See {@link onTrackerChanged}.
389
+ * chart to *never* show a cursor, mount no cursor component, rather than
390
+ * clearing `trackerPosition`. See {@link onTrackerChanged}.
392
391
  */
393
392
  trackerPosition?: number | null;
394
- /**
395
- * In-chart cursor presentation — the default for all rows (a row may override
396
- * via `<ChartRow cursor>`). **Default `'line'`** — the synced vertical line,
397
- * with values surfaced *outside* the chart via {@link onTrackerChanged}.
398
- * `'point'` / `'inline'` / `'flag'` add per-series marks; `'none'` hides it.
399
- * `'region'` shades the bucket under the pointer (needs {@link cursorSequence}).
400
- * See {@link CursorMode}.
401
- *
402
- * @deprecated Mount a **cursor component** instead — `<LineCursor>` /
403
- * `<PointCursor>` / `<InlineCursor>` / `<FlagCursor>` / `<CrosshairCursor>` /
404
- * `<RangeCursor>` as a child of the container (or inside a `<ChartRow>` for
405
- * the per-row override); mount nothing for `'none'`. This prop keeps working
406
- * for one more minor by synthesizing the equivalent preset internally; a
407
- * mounted cursor component overrides it. See `docs/rfcs/interaction.md` §9.
408
- */
409
- cursor?: CursorMode;
410
- /**
411
- * The bucketing for `cursor="region"` — the interval highlighted under the
412
- * pointer. A pond {@link Sequence} (duration or calendar-aware —
413
- * `Sequence.every('1d')`, `Sequence.calendar('month')`) is realized over the
414
- * current view; a {@link BoundedSequence} (e.g. a `TradingCalendar`'s
415
- * `sessionSequence()` / `barSequence()`) is used as-is, so the band can track
416
- * whole **sessions**. Either way the band maps through `xScale`, so on a
417
- * trading-time axis the closed part of the bucket collapses. Ignored unless
418
- * `cursor="region"`.
419
- *
420
- * **Time axis only.** A bucket is a *time* interval, so the region cursor is
421
- * gated to a **time** x-axis — on a **value** axis (a horizontal histogram, a
422
- * value-keyed chart) it's a no-op (highlighting a value *band* on a horizontal
423
- * histogram would be a different, y-oriented cursor).
424
- *
425
- * **Pass a stable reference.** The buckets are memoized on this value + the
426
- * view range; a `Sequence`/`BoundedSequence` rebuilt inline every render
427
- * re-realizes the buckets on each pointer move (harmless for a coarse
428
- * day/session sequence, wasteful for a fine one over a wide view) — hoist it or
429
- * `useMemo` it.
430
- *
431
- * @deprecated Use `<RangeCursor sequence={…}>` — the prop moved onto the
432
- * component that uses it, where it is no longer mode-conditional. Works for
433
- * one more minor; a mounted `<RangeCursor>`'s sequence wins over this.
434
- */
435
- cursorSequence?: Sequence | BoundedSequence;
436
- /**
437
- * Makes the `region` cursor **draggable**: drag across the plot and the band
438
- * extends **bucket by bucket** (snapping to `cursorSequence` points); on
439
- * release this fires **once** with the selected `[lo, hi]` span, and the cursor
440
- * reverts to the single-bucket highlight (it does not keep the range). Typical
441
- * use — zoom the view to the returned span (the container doesn't zoom itself;
442
- * that's the consumer's call), or map it onto a data subscription's range params.
443
- *
444
- * The span is a **neutral numeric pair in axis units** — epoch ms on a **time**
445
- * axis, the axis value (strike, distance, …) on a **value** axis — mirroring the
446
- * polymorphic `range` input. A time consumer that wants a `TimeRange` builds one
447
- * from the pair.
448
- *
449
- * With **no `cursorSequence`** the region cursor is the degenerate case — it
450
- * renders as a **line** on hover and the drag is **freeform** (raw `[lo, hi]`, no
451
- * bucket snapping); the same callback fires on release. Bucket snapping needs a
452
- * `cursorSequence`, which is **time-axis only** (a time interval over a value
453
- * domain is meaningless), so a **value** axis is always freeform. No-op unless
454
- * `cursor="region"` on a **time** or **value** x-axis (a **category** axis is
455
- * excluded — an ordinal-slot select is a different gesture).
456
- *
457
- * @deprecated Use `<RangeCursor onDragRelease>` — the drag moved onto the
458
- * component, and the payload becomes `{ x: [lo, hi] }` (a {@link RangeSpan},
459
- * forward-compatible with the 2-D drag's optional `y`) instead of the bare
460
- * pair. Works for one more minor; a mounted `<RangeCursor onDragRelease>`
461
- * takes over the gesture.
462
- */
463
- onRegionSelect?: (range: readonly [number, number]) => void;
464
- /**
465
- * Which modifier a region-drag needs — set `'shift'` when you also enable
466
- * **pan** (`panZoom="pan"` or `"panZoom"`) and want **plain drag to pan,
467
- * shift-drag to select**. It's only enforced while pan is enabled (with pan
468
- * off there's no gesture conflict, so shift is optional — either drag
469
- * selects). **Omitted** ⇒ a region-drag
470
- * **preempts** pan (drag always selects; document that precedence for users).
471
- * Wheel-zoom is unaffected in every case.
472
- *
473
- * @deprecated Use `<RangeCursor dragModifier>` — the prop moved onto the
474
- * component alongside `onDragRelease`. Works for one more minor.
475
- */
476
- regionSelectModifier?: 'shift';
477
393
  /**
478
394
  * Fires on pointer move with the hovered time + every series' value there (so
479
395
  * you can render a readout outside the chart), and `null` on leave.
@@ -561,30 +477,6 @@ export interface ChartContainerProps {
561
477
  onTimeRangeChange?: (range: [number, number]) => void;
562
478
  /** Zoom-in floor — the minimum visible duration in ms. Default `1`. */
563
479
  minDuration?: number;
564
- /**
565
- * Show the cursor's time atop the in-chart readout (when a row's `cursor` draws
566
- * one). **Default `false`.** Formatted by {@link timeFormat} to match the time
567
- * axis.
568
- *
569
- * @deprecated Use `showTime` on the mounted cursor component
570
- * (`<LineCursor showTime>` / `<FlagCursor showTime>` / …). Works for one
571
- * more minor via the shim.
572
- */
573
- cursorTime?: boolean;
574
- /**
575
- * `cursor="crosshair"` reticle **y** snapping. **Default `true`** — the
576
- * crosshair centres on the nearest **data point** (the horizontal line snaps to
577
- * that sample's value). `false` — the horizontal line + centre follow the
578
- * pointer **y** freely, the value read as `yScale.invert(pointerY)`. Either way
579
- * the vertical line snaps its **x** to the data grid (so the time readout is
580
- * clean), and both draw a full-height dashed vertical + full-width dashed
581
- * horizontal line.
582
- *
583
- * @deprecated Use `<CrosshairCursor snap={…}>` — the prop moved onto the
584
- * component, where it is no longer mode-conditional. Works for one more
585
- * minor via the shim.
586
- */
587
- crosshairSnap?: boolean;
588
480
  /**
589
481
  * Enter **annotation-edit mode**: suppresses the data cursor and makes editable
590
482
  * annotations (those given an `onChange`) interactive — hovering one reveals its
@@ -634,43 +526,10 @@ export interface ChartContainerProps {
634
526
  * **owns the labels**, so it opts the axis out of the `dateStyle` ladder
635
527
  * (flat / stacked) by design. **Omitted ⇒ the flat/stacked date style.** To
636
528
  * shape only the cursor readout while keeping a date style, use
637
- * {@link cursorFormat} instead. (For back-compat this also shapes the readout
638
- * when `cursorFormat` is absent.)
529
+ * a mounted cursor's `format` (e.g. `<CrosshairCursor format>`) instead. (For
530
+ * back-compat this also shapes the readout when no cursor `format` is set.)
639
531
  */
640
532
  timeFormat?: AxisFormat;
641
- /**
642
- * The **cursor / marker readout** format — the crosshair x pill, marker
643
- * axis indicators, and annotation auto-labels — **independent of the tick
644
- * labels** on both axis kinds: it does **not** disqualify the `dateStyle`
645
- * ladder (time), and it never moves the tick labels (value). It beats an
646
- * explicit `<XAxis format>` for the **readout only** — pill precedence is
647
- * `cursorFormat → axis format → container` — so terse ticks can pair with a
648
- * precise readout (`+2.0σ` labels, `+1.83σ` pill).
649
- *
650
- * **Omitted ⇒ the axis's own formatter.** On a time axis that default is
651
- * grain-aware: the readout formats at the axis's granularity, so a
652
- * day-or-coarser axis reads a **date** (never a time-of-day) and a sub-day
653
- * axis reads date + clock — a daily bar at a foreign-tz midnight no longer
654
- * renders as `02 AM`. On a value axis it is the tick formatter
655
- * ({@link timeFormat}-shaped, else the d3 default).
656
- *
657
- * A d3 specifier **string** formats uniformly (time specifier on a time
658
- * axis, number specifier on a value axis); a **function**
659
- * `(value, { grain, defaultText }) => string` receives the axis's resolved
660
- * coarse {@link TimeGrain} (`undefined` on a value axis) and the default
661
- * readout text, so it can branch on the zoom level and pass `defaultText`
662
- * through for grains it doesn't override (no re-deriving the grain from the
663
- * range). See {@link CursorFormat}. This is the independent readout channel;
664
- * {@link timeFormat} owns the labels. (A category axis reads names, and a
665
- * `transform`ed axis's pill speaks its derived unit — neither consults
666
- * `cursorFormat`.)
667
- *
668
- * @deprecated Use `format` on the mounted cursor (`<CrosshairCursor
669
- * format={…}>`) — it feeds the same shared readout channel (marker
670
- * indicators and annotation auto-labels included). Works for one more
671
- * minor; a mounted cursor's `format` wins over this.
672
- */
673
- cursorFormat?: CursorFormat;
674
533
  /**
675
534
  * Label the x axis as **offsets from a zero point** instead of absolute
676
535
  * values — the *duration* (elapsed-time) axis. A time axis reads
@@ -690,7 +549,7 @@ export interface ChartContainerProps {
690
549
  * and so does the cursor pill (one grain finer, as ever: `00:05:12`).
691
550
  *
692
551
  * This is a **labelling** mode, not a data transform: `range`, an annotation's
693
- * `at`, an `onRegionSelect` span, `trackerPosition` are all still absolute
552
+ * `at`, a `<RangeCursor onDragRelease>` span, `trackerPosition` are all still absolute
694
553
  * axis units. Ignored on a category axis. An explicit `timeFormat` /
695
554
  * `<XAxis format>` still wins — on a time axis a d3 *time* specifier can only
696
555
  * describe an instant, so it labels the underlying wall clock (the lever for
@@ -6,12 +6,12 @@ import { scaleBand } from './bandScale.js';
6
6
  import { scaleElapsed } from './elapsed.js';
7
7
  import { Sequence, TimeZone } from 'pond-ts';
8
8
  import { ContainerContext, CursorContext, } from './context.js';
9
- import { LegacyCursor, legacyCursorWarning, presetNameFor, warnOnDuplicateGestureOwners, } from './cursors.js';
9
+ import { warnOnDuplicateGestureOwners } from './cursors.js';
10
10
  import { effectiveSelectorEntries, resolveControlledHovered, resolveControlledSelected, selectorEntryEqual, warnInertClick, } from './selectors.js';
11
11
  import { isDev } from './dev.js';
12
12
  import { maxSlotWidths, sum } from './slots.js';
13
13
  import { computeLabelLanes } from './annotations.js';
14
- import { resolveCursorX, DEFAULT_CURSOR_MODE } from './tracker.js';
14
+ import { resolveCursorX } from './tracker.js';
15
15
  import { clampToBounds } from './viewport.js';
16
16
  import { resolveAxisFormat, resolveTimeFormat, } from './format.js';
17
17
  import { TimeAxis } from './TimeAxis.js';
@@ -198,14 +198,7 @@ function AutoSizeContainer(props) {
198
198
  * pass; a chart legitimately gated this long is not painting anyway. */
199
199
  const ZERO_SIZE_WARNING_MS = 600;
200
200
  /** {@link ChartContainer} with its width resolved to a concrete pixel number. */
201
- function ResolvedChartContainer({ range, categories: categoriesProp, maxBandWidth, bandAlign = 'start', width, height, rowGap = 0, showAxis = true, trackerPosition, onTrackerChanged, onDrawStats, panZoom = false, axisPanZoom = false, bounds, onTimeRangeChange, minDuration = 1, cursor: cursorProp, cursorSequence: cursorSequenceProp, onRegionSelect, regionSelectModifier, cursorTime: cursorTimeProp, crosshairSnap: crosshairSnapProp, editAnnotations = false, creating = null, onCreate, onSelectAnnotation, onHoverAnnotation, onEditAnnotation, snap = true, timeFormat, cursorFormat: cursorFormatProp, origin, theme, discontinuities, calendar, timeZone: timeZoneProp, spacing, xScale: xScaleKind = 'linear', grid = true, sessionDividers = 'none', children, }) {
202
- // ── Legacy cursor props (deprecated) ───────────────────────────────────────
203
- // The string surface keeps working for one minor: the resolved mode is
204
- // synthesized into the equivalent mounted preset below (`<LegacyCursor>`),
205
- // and a dev warning names the replacement whenever any of the props is
206
- // *explicitly* set (never on the defaults). Mounted cursor components in the
207
- // same scope override the shim. See docs/rfcs/interaction.md §9 / A4.4.
208
- const cursor = cursorProp ?? DEFAULT_CURSOR_MODE;
201
+ function ResolvedChartContainer({ range, categories: categoriesProp, maxBandWidth, bandAlign = 'start', width, height, rowGap = 0, showAxis = true, trackerPosition, onTrackerChanged, onDrawStats, panZoom = false, axisPanZoom = false, bounds, onTimeRangeChange, minDuration = 1, editAnnotations = false, creating = null, onCreate, onSelectAnnotation, onHoverAnnotation, onEditAnnotation, snap = true, timeFormat, origin, theme, discontinuities, calendar, timeZone: timeZoneProp, spacing, xScale: xScaleKind = 'linear', grid = true, sessionDividers = 'none', children, }) {
209
202
  // [PND-HEIGHT] Whether this container owns vertical layout (see the
210
203
  // `height` prop). Carried on the frame so a `<ChartRow flex>` can tell a
211
204
  // home that can size it from one that never will.
@@ -223,43 +216,8 @@ function ResolvedChartContainer({ range, categories: categoriesProp, maxBandWidt
223
216
  const declaredCategories = useMemo(() => categoriesProp === undefined ? null : [...categoriesProp],
224
217
  // eslint-disable-next-line react-hooks/exhaustive-deps -- content identity
225
218
  [categoriesKey]);
226
- const cursorTime = cursorTimeProp ?? false;
227
- const crosshairSnap = crosshairSnapProp ?? true;
228
- const warnedLegacyRef = useRef(false);
229
- useEffect(() => {
230
- if (!isDev || warnedLegacyRef.current)
231
- return;
232
- const legacy = [];
233
- if (cursorProp !== undefined)
234
- legacy.push(`cursor="${cursorProp}" → mount ${presetNameFor(cursorProp)}`);
235
- if (crosshairSnapProp !== undefined)
236
- legacy.push('crosshairSnap → <CrosshairCursor snap>');
237
- if (cursorTimeProp !== undefined)
238
- legacy.push('cursorTime → showTime on the mounted cursor');
239
- if (cursorFormatProp !== undefined)
240
- legacy.push('cursorFormat → format on <CrosshairCursor>');
241
- if (cursorSequenceProp !== undefined)
242
- legacy.push('cursorSequence → <RangeCursor sequence>');
243
- if (onRegionSelect !== undefined)
244
- legacy.push('onRegionSelect → <RangeCursor onDragRelease> (the payload becomes ' +
245
- '{ x: [lo, hi] })');
246
- if (regionSelectModifier !== undefined)
247
- legacy.push('regionSelectModifier → <RangeCursor dragModifier>');
248
- if (legacy.length === 0)
249
- return;
250
- warnedLegacyRef.current = true;
251
- console.warn(legacyCursorWarning(legacy));
252
- }, [
253
- cursorProp,
254
- crosshairSnapProp,
255
- cursorTimeProp,
256
- cursorFormatProp,
257
- cursorSequenceProp,
258
- onRegionSelect,
259
- regionSelectModifier,
260
- ]);
261
219
  // Mounted-cursor registry ({@link ContainerFrame.registerCursor}): the
262
- // presets (and the legacy shim) register their specs here; rows and
220
+ // presets register their specs here; rows and
263
221
  // `<XAxis>` render the effective set. Same per-instance-slot discipline as
264
222
  // the tracker sources; register is idempotent under reference equality (the
265
223
  // presets memoize their entries on props).
@@ -282,16 +240,12 @@ function ResolvedChartContainer({ range, categories: categoriesProp, maxBandWidt
282
240
  useEffect(() => {
283
241
  warnOnDuplicateGestureOwners(cursors, warnedGestureRef);
284
242
  }, [cursors]);
285
- // The registered cursors' resolution inputs, folded into the legacy
286
- // channels: a mounted `<CrosshairCursor format>` feeds the shared readout
287
- // channel exactly where `cursorFormat` fed it. First **component-mounted**
288
- // entry wins (the shim registers before the children mount, so a bare
289
- // first-wins would let the legacy synthesis shadow a real mount); the
290
- // legacy prop is the fallback during the window. (`sequence` resolves the
243
+ // The registered cursors' resolution inputs: a mounted cursor's `format`
244
+ // feeds the shared readout channel (the x pill, marker indicators,
245
+ // annotation auto-labels). First mounted wins. (`sequence` resolves the
291
246
  // same way, below the selector registry — a `<MultiSelector sequence>`
292
247
  // feeds the same channel.)
293
- const cursorFormat = useMemo(() => cursors.find((e) => !e.legacy && e.format !== undefined)?.format ??
294
- cursorFormatProp, [cursors, cursorFormatProp]);
248
+ const cursorFormat = useMemo(() => cursors.find((e) => e.format !== undefined)?.format, [cursors]);
295
249
  // Which axes the gestures own. **Pan follows zoom's degrees of freedom**: an
296
250
  // axis that can be zoomed can be panned, because a zoomed axis shows less than
297
251
  // all of itself and the reader needs to reach the rest. `'pan'` is the one
@@ -445,7 +399,7 @@ function ResolvedChartContainer({ range, categories: categoriesProp, maxBandWidt
445
399
  setSelectableKeys(next);
446
400
  }, []);
447
401
  // Mounted-selector registry ({@link ContainerFrame.registerSelector}):
448
- // `<Selector>` — and the legacy shim below — register here, and **the
402
+ // `<Selector>` / `<MultiSelector>` register here, and **the
449
403
  // registration is what enables a plot click** (interaction RFC §7.1). Same
450
404
  // per-instance-slot discipline as the cursors/tracker sources. Mirrored to a
451
405
  // ref because `select` / `setHovered` are `[]`-stable callbacks that must not
@@ -477,15 +431,13 @@ function ResolvedChartContainer({ range, categories: categoriesProp, maxBandWidt
477
431
  }, []);
478
432
  const selectors = useMemo(() => Array.from(selectorMap.values()), [selectorMap]);
479
433
  const selectorsRef = useRef(selectors);
480
- // The shared snap-bucket sequence, folded into the legacy channel exactly as
481
- // `cursorFormat` is above: a component-mounted `<RangeCursor sequence>`
482
- // wins, then a mounted `<MultiSelector sequence>` (its sweep extends bucket
483
- // by bucket over the same realized buckets — one channel, so the band and
484
- // the sweep can never snap differently), then the legacy shim / prop.
485
- const cursorSequence = useMemo(() => cursors.find((e) => !e.legacy && e.sequence !== undefined)?.sequence ??
486
- selectors.find((e) => e.sequence !== undefined)?.sequence ??
487
- cursors.find((e) => e.sequence !== undefined)?.sequence ??
488
- cursorSequenceProp, [cursors, selectors, cursorSequenceProp]);
434
+ // The shared snap-bucket sequence, resolved like `cursorFormat` above: a
435
+ // mounted `<RangeCursor sequence>` wins, then a mounted `<MultiSelector
436
+ // sequence>` (its sweep extends bucket by bucket over the same realized
437
+ // buckets — one channel, so the band and the sweep can never snap
438
+ // differently).
439
+ const cursorSequence = useMemo(() => cursors.find((e) => e.sequence !== undefined)?.sequence ??
440
+ selectors.find((e) => e.sequence !== undefined)?.sequence, [cursors, selectors]);
489
441
  // Annotations register here so the container can do what a mark can't in
490
442
  // isolation: draw its guide line across other rows, order regions, serve snap
491
443
  // targets. Keyed by per-instance slot key (same discipline as the sources).
@@ -1201,10 +1153,10 @@ function ResolvedChartContainer({ range, categories: categoriesProp, maxBandWidt
1201
1153
  // briefly stale until the next pointer move — rare, and the bounds check below
1202
1154
  // hides an out-of-plot crosshair meanwhile.
1203
1155
  const cursorX = resolveCursorX(trackerPosition, hoverX, xScale);
1204
- // `cursor="region"` snap buckets — the intervals the band snaps to (and a drag
1156
+ // `<RangeCursor>` snap buckets — the intervals the band snaps to (and a drag
1205
1157
  // extends bucket by bucket over). Two sources, in precedence order:
1206
1158
  //
1207
- // 1. **An explicit `cursorSequence`** (time axis only): realized over the view
1159
+ // 1. **An explicit `sequence`** (time axis only): realized over the view
1208
1160
  // (a `Sequence` → `.bounded`; a `BoundedSequence` used as-is). A `Sequence`
1209
1161
  // bucket is a *time* interval, so it's gated to a time axis — realizing time
1210
1162
  // buckets over a value domain is meaningless (it would shade the whole plot).
@@ -1231,9 +1183,9 @@ function ResolvedChartContainer({ range, categories: categoriesProp, maxBandWidt
1231
1183
  // or a time-axis histogram with no explicit sequence). `binIntervals` is only
1232
1184
  // published by a vertical bar layer, so this is a no-op for
1233
1185
  // line/area/scatter rows. On a **category** axis the bins are the unit
1234
- // slots `[i, i+1)`: the region cursor never reads them (its band gates on
1235
- // a continuous axis), but the `<MultiSelector>` sweep's band snaps over
1236
- // them so it runs slot-edge to slot-edge — the band scale's `invert`
1186
+ // slots `[i, i+1)`: the `<RangeCursor>` band shades the slot under the
1187
+ // pointer ([PND-ORDCURSOR]), and the `<MultiSelector>` sweep's band snaps
1188
+ // over them so it runs slot-edge to slot-edge — the band scale's `invert`
1237
1189
  // returns slot *centres*, and a centre-to-centre band disagreed with the
1238
1190
  // snapped-outward span the release commits (RFC A7.6's edge rule).
1239
1191
  //
@@ -1294,6 +1246,25 @@ function ResolvedChartContainer({ range, categories: categoriesProp, maxBandWidt
1294
1246
  'axis; key the data on the quantity itself (a `ValueSeries`) to get a ' +
1295
1247
  'value axis you can scale.');
1296
1248
  }
1249
+ // [PND-ORDCURSOR] A `<RangeCursor onDragRelease>` on a category axis draws
1250
+ // its slot band but never starts a drag (`resolveRangeDrag`): the callback
1251
+ // would silently never fire, so say so once. Same `sources.size` guard as
1252
+ // above — the kind is 'time' until the layers register.
1253
+ const rangeDragOnCategory = resolvedKind === 'category' &&
1254
+ sources.size > 0 &&
1255
+ cursors.some((e) => e.onDragRelease !== undefined && e.enableDrag !== false);
1256
+ const warnedRangeDragOnCategoryRef = useRef(false);
1257
+ useEffect(() => {
1258
+ if (!isDev || !rangeDragOnCategory)
1259
+ return;
1260
+ if (warnedRangeDragOnCategoryRef.current)
1261
+ return;
1262
+ warnedRangeDragOnCategoryRef.current = true;
1263
+ console.warn('[pond-charts] <RangeCursor onDragRelease> is mounted on a category x ' +
1264
+ 'axis, where the range drag is off: the band shades the slot under the ' +
1265
+ 'pointer, but `onDragRelease` never fires. To drag across bars and get ' +
1266
+ 'them back, mount a <MultiSelector> instead.');
1267
+ }, [rangeDragOnCategory]);
1297
1268
  const xIsLog = ((s) => {
1298
1269
  const probe = s;
1299
1270
  return (typeof probe.base === 'function' || typeof probe.constant === 'function');
@@ -1342,15 +1313,12 @@ function ResolvedChartContainer({ range, categories: categoriesProp, maxBandWidt
1342
1313
  rowGap,
1343
1314
  setHoverX,
1344
1315
  setHoverY,
1345
- crosshairSnap,
1346
1316
  cursorBuckets,
1347
1317
  regionAnchor,
1348
1318
  previewSpans,
1349
1319
  setPreviewSpans,
1350
1320
  setRegionAnchor,
1351
- onRegionSelect,
1352
1321
  reportDrawStats,
1353
- regionSelectModifier,
1354
1322
  draggingKey,
1355
1323
  setDragging,
1356
1324
  selected: selectedValue,
@@ -1358,8 +1326,6 @@ function ResolvedChartContainer({ range, categories: categoriesProp, maxBandWidt
1358
1326
  select,
1359
1327
  hovered: hoveredValue,
1360
1328
  setHovered,
1361
- cursor,
1362
- cursorTime,
1363
1329
  editAnnotations,
1364
1330
  creating,
1365
1331
  snap,
@@ -1428,15 +1394,12 @@ function ResolvedChartContainer({ range, categories: categoriesProp, maxBandWidt
1428
1394
  rightGutter,
1429
1395
  rowGap,
1430
1396
  setHoverY,
1431
- crosshairSnap,
1432
1397
  cursorBuckets,
1433
1398
  regionAnchor,
1434
1399
  previewSpans,
1435
1400
  setPreviewSpans,
1436
1401
  setRegionAnchor,
1437
- onRegionSelect,
1438
1402
  reportDrawStats,
1439
- regionSelectModifier,
1440
1403
  draggingKey,
1441
1404
  setDragging,
1442
1405
  selectedValue,
@@ -1444,8 +1407,6 @@ function ResolvedChartContainer({ range, categories: categoriesProp, maxBandWidt
1444
1407
  select,
1445
1408
  hoveredValue,
1446
1409
  setHovered,
1447
- cursor,
1448
- cursorTime,
1449
1410
  editAnnotations,
1450
1411
  creating,
1451
1412
  snap,
@@ -1499,37 +1460,33 @@ function ResolvedChartContainer({ range, categories: categoriesProp, maxBandWidt
1499
1460
  registerRow,
1500
1461
  firstRowKey,
1501
1462
  ]);
1502
- return (_jsx(ContainerContext.Provider, { value: frame, children: _jsxs(CursorContext.Provider, { value: cursorFrame, children: [_jsx(LegacyCursor, { mode: cursor, showTime: cursorTime, snap: crosshairSnap, sequence: cursorSequenceProp,
1503
- // The 'line' default nobody asked for is IMPLICIT — the one cursor a
1504
- // <MultiSelector>'s resting block preview may replace with the
1505
- // brush band. An explicit `cursor` prop (any mode) still wins.
1506
- implicit: cursorProp === undefined }), _jsxs("div", { style: {
1507
- width: `${width}px`,
1508
- // [PND-HEIGHT] A managed height makes the outer box a flex
1509
- // column: the rows block below flexes, the axis strip keeps its
1510
- // natural height at the bottom, and CSS subtracts one from the
1511
- // other. That subtraction being layout rather than arithmetic is
1512
- // the feature — the strip's height varies with label, font size,
1513
- // calendar bands and pill lanes, so no constant is correct.
1514
- ...(height !== undefined
1515
- ? {
1516
- height: `${height}px`,
1517
- display: 'flex',
1518
- flexDirection: 'column',
1519
- }
1520
- : {}),
1521
- }, children: [_jsx("div", { style: {
1522
- display: 'flex',
1523
- flexDirection: 'column',
1524
- gap: `${rowGap}px`,
1525
- // The positioned ancestor for overlay chrome (`<Legend>`): the
1526
- // card anchors to the rows block, never the axis strip below.
1527
- position: 'relative',
1528
- // The rows block takes what the axis strip leaves. `minHeight:
1529
- // 0` lets it shrink below its content — without it a flex
1530
- // child's floor is its content and nothing can ever get
1531
- // smaller.
1532
- ...(height !== undefined ? { flex: '1 1 0%', minHeight: 0 } : {}),
1533
- }, children: children }), showAxis && _jsx(TimeAxis, {})] })] }) }));
1463
+ return (_jsx(ContainerContext.Provider, { value: frame, children: _jsx(CursorContext.Provider, { value: cursorFrame, children: _jsxs("div", { style: {
1464
+ width: `${width}px`,
1465
+ // [PND-HEIGHT] A managed height makes the outer box a flex
1466
+ // column: the rows block below flexes, the axis strip keeps its
1467
+ // natural height at the bottom, and CSS subtracts one from the
1468
+ // other. That subtraction being layout rather than arithmetic is
1469
+ // the feature — the strip's height varies with label, font size,
1470
+ // calendar bands and pill lanes, so no constant is correct.
1471
+ ...(height !== undefined
1472
+ ? {
1473
+ height: `${height}px`,
1474
+ display: 'flex',
1475
+ flexDirection: 'column',
1476
+ }
1477
+ : {}),
1478
+ }, children: [_jsx("div", { style: {
1479
+ display: 'flex',
1480
+ flexDirection: 'column',
1481
+ gap: `${rowGap}px`,
1482
+ // The positioned ancestor for overlay chrome (`<Legend>`): the
1483
+ // card anchors to the rows block, never the axis strip below.
1484
+ position: 'relative',
1485
+ // The rows block takes what the axis strip leaves. `minHeight:
1486
+ // 0` lets it shrink below its content — without it a flex
1487
+ // child's floor is its content and nothing can ever get
1488
+ // smaller.
1489
+ ...(height !== undefined ? { flex: '1 1 0%', minHeight: 0 } : {}),
1490
+ }, children: children }), showAxis && _jsx(TimeAxis, {})] }) }) }));
1534
1491
  }
1535
1492
  //# sourceMappingURL=ChartContainer.js.map
@@ -1,5 +1,4 @@
1
1
  import { type ReactNode } from 'react';
2
- import { type CursorMode } from './context.js';
3
2
  export interface ChartRowProps {
4
3
  /**
5
4
  * Row height in CSS pixels — the **fixed** sizing mode. Omit it (or pass
@@ -39,16 +38,6 @@ export interface ChartRowProps {
39
38
  * hidden, so a `display: none` tab switch does not discard its scales.
40
39
  */
41
40
  flex?: number;
42
- /**
43
- * Cursor presentation for this row, overriding the container's default
44
- * ({@link ChartContainerProps.cursor}). Omit to inherit. See {@link CursorMode}.
45
- *
46
- * @deprecated Mount a cursor component **inside the row** instead
47
- * (`<ChartRow><CrosshairCursor /> …</ChartRow>`) — the per-row override with
48
- * the same nearest-mount-wins semantics. Works for one more minor; a mounted
49
- * cursor in the row overrides this prop.
50
- */
51
- cursor?: CursorMode;
52
41
  children?: ReactNode;
53
42
  }
54
43
  /**
@@ -68,5 +57,5 @@ export interface ChartRowProps {
68
57
  * Children lay out left-to-right in author order, so `<YAxis side="left"/>` goes
69
58
  * before `<Layers/>` and `<YAxis side="right"/>` after.
70
59
  */
71
- export declare function ChartRow({ height: heightProp, flex, cursor, children, }: ChartRowProps): import("react/jsx-runtime").JSX.Element;
60
+ export declare function ChartRow({ height: heightProp, flex, children, }: ChartRowProps): import("react/jsx-runtime").JSX.Element;
72
61
  //# sourceMappingURL=ChartRow.d.ts.map
package/dist/ChartRow.js CHANGED
@@ -8,7 +8,6 @@ import { resolveAxisFormat } from './format.js';
8
8
  import { resolveYTickCount } from './yticks.js';
9
9
  import { placeAxisSlots } from './slots.js';
10
10
  import { useSlotKey } from './use-slot-key.js';
11
- import { LegacyCursor } from './cursors.js';
12
11
  import { YAxis } from './YAxis.js';
13
12
  import { ContainerContext, RowContext, } from './context.js';
14
13
  /**
@@ -139,7 +138,7 @@ const TOP_LABEL_HEADER = 16;
139
138
  * Children lay out left-to-right in author order, so `<YAxis side="left"/>` goes
140
139
  * before `<Layers/>` and `<YAxis side="right"/>` after.
141
140
  */
142
- export function ChartRow({ height: heightProp, flex, cursor, children, }) {
141
+ export function ChartRow({ height: heightProp, flex, children, }) {
143
142
  const container = useContext(ContainerContext);
144
143
  if (container === null) {
145
144
  throw new Error('<ChartRow> must be rendered inside a <ChartContainer>');
@@ -212,18 +211,6 @@ export function ChartRow({ height: heightProp, flex, cursor, children, }) {
212
211
  const { registerRow } = container;
213
212
  useEffect(() => registerRow(rowKey), [registerRow, rowKey]);
214
213
  const isFirstRow = container.firstRowKey === rowKey;
215
- // Deprecation notice for the legacy `cursor` prop (dev, once per row): the
216
- // per-row override is now a cursor component mounted inside the row. The
217
- // prop keeps working via the shim rendered below.
218
- const warnedCursorRef = useRef(false);
219
- useEffect(() => {
220
- if (!isDev || cursor === undefined || warnedCursorRef.current)
221
- return;
222
- warnedCursorRef.current = true;
223
- console.warn(`[pond-charts] <ChartRow cursor="${cursor}"> is deprecated (it keeps ` +
224
- 'working this minor, removed next) — mount the cursor component ' +
225
- 'inside the row instead (docs/rfcs/interaction.md §9).');
226
- }, [cursor]);
227
214
  // Keyed by a stable per-instance id (Map preserves insertion order; setting an
228
215
  // existing key updates in place). So a re-register on a prop change keeps the
229
216
  // entry's slot — the axis-default (first axis) and layer z-order stay stable
@@ -378,7 +365,7 @@ export function ChartRow({ height: heightProp, flex, cursor, children, }) {
378
365
  const extents = needsExtents(ax)
379
366
  ? layerList
380
367
  .filter((entry) => (entry.axisId ?? defaultAxisId) === ax.id)
381
- .map((entry) => entry.layer.yExtent())
368
+ .map((entry) => entry.layer.yExtent(ax.scale))
382
369
  : [];
383
370
  const [lo, hi] = resolveYDomain(ax.min, ax.max, extents, ax.pad, ax.scale);
384
371
  // Reserve a header band at the top when any axis draws a `'top'` title,
@@ -483,7 +470,7 @@ export function ChartRow({ height: heightProp, flex, cursor, children, }) {
483
470
  // about.
484
471
  const message = logAxisWarning(ax, layerList
485
472
  .filter((entry) => (entry.axisId ?? defaultAxisId) === ax.id)
486
- .map((entry) => entry.layer.yExtent()));
473
+ .map((entry) => entry.layer.yExtent(ax.scale)));
487
474
  if (message === null)
488
475
  warned.delete(ax.id);
489
476
  else if (warned.get(ax.id) !== message) {
@@ -626,7 +613,6 @@ export function ChartRow({ height: heightProp, flex, cursor, children, }) {
626
613
  const frame = useMemo(() => ({
627
614
  height,
628
615
  topInset: topHeader,
629
- cursor,
630
616
  isFirstRow,
631
617
  rowKey,
632
618
  yScales,
@@ -649,7 +635,6 @@ export function ChartRow({ height: heightProp, flex, cursor, children, }) {
649
635
  }), [
650
636
  height,
651
637
  topHeader,
652
- cursor,
653
638
  isFirstRow,
654
639
  rowKey,
655
640
  yScales,
@@ -737,17 +722,17 @@ export function ChartRow({ height: heightProp, flex, cursor, children, }) {
737
722
  "wrap the row's <Layers>, leaving each <YAxis> a direct child of the " +
738
723
  '<ChartRow>.');
739
724
  }, [axisInsideWrapper]);
740
- return (_jsxs(RowContext.Provider, { value: frame, children: [cursor !== undefined && (_jsx(LegacyCursor, { mode: cursor, showTime: container.cursorTime, snap: container.crosshairSnap })), _jsxs("div", { ref: boxRef, style: {
741
- display: 'flex',
742
- flexDirection: 'row',
743
- width: `${container.width}px`,
744
- // Fixed rows keep their pixels; a flex row is sized by the
745
- // container's column layout and reads the result back
746
- // ([PND-HEIGHT]). `minHeight: 0` is what lets it actually shrink —
747
- // a flex child's default min-height is its content.
748
- ...(isFlex
749
- ? { flex: `${flexGrow} 1 0%`, minHeight: 0 }
750
- : { height: `${height}px` }),
751
- }, children: [leftPad > 0 && _jsx("div", { style: { flex: `0 0 ${leftPad}px` } }), leftAxisEls, plotEls, rightAxisEls, rightPad > 0 && _jsx("div", { style: { flex: `0 0 ${rightPad}px` } })] })] }));
725
+ return (_jsx(RowContext.Provider, { value: frame, children: _jsxs("div", { ref: boxRef, style: {
726
+ display: 'flex',
727
+ flexDirection: 'row',
728
+ width: `${container.width}px`,
729
+ // Fixed rows keep their pixels; a flex row is sized by the
730
+ // container's column layout and reads the result back
731
+ // ([PND-HEIGHT]). `minHeight: 0` is what lets it actually shrink —
732
+ // a flex child's default min-height is its content.
733
+ ...(isFlex
734
+ ? { flex: `${flexGrow} 1 0%`, minHeight: 0 }
735
+ : { height: `${height}px` }),
736
+ }, children: [leftPad > 0 && _jsx("div", { style: { flex: `0 0 ${leftPad}px` } }), leftAxisEls, plotEls, rightAxisEls, rightPad > 0 && _jsx("div", { style: { flex: `0 0 ${rightPad}px` } })] }) }));
752
737
  }
753
738
  //# sourceMappingURL=ChartRow.js.map