@pond-ts/charts 0.57.0 → 0.59.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 (85) hide show
  1. package/API.md +576 -0
  2. package/CHANGELOG.md +1213 -1
  3. package/dist/AreaChart.d.ts +12 -1
  4. package/dist/AreaChart.js +131 -13
  5. package/dist/BarChart.d.ts +56 -7
  6. package/dist/BarChart.js +263 -39
  7. package/dist/BarList.d.ts +85 -5
  8. package/dist/BarList.js +25 -4
  9. package/dist/BoxList.d.ts +70 -3
  10. package/dist/BoxList.js +21 -7
  11. package/dist/BoxPlot.d.ts +2 -1
  12. package/dist/BoxPlot.js +101 -9
  13. package/dist/Candlestick.d.ts +13 -1
  14. package/dist/Candlestick.js +89 -3
  15. package/dist/ChartContainer.d.ts +36 -48
  16. package/dist/ChartContainer.js +465 -59
  17. package/dist/ChartRow.d.ts +9 -2
  18. package/dist/ChartRow.js +176 -14
  19. package/dist/HeatMap.d.ts +176 -0
  20. package/dist/HeatMap.js +344 -0
  21. package/dist/Layers.d.ts +5 -1
  22. package/dist/Layers.js +1014 -253
  23. package/dist/Legend.js +8 -4
  24. package/dist/LineChart.d.ts +18 -1
  25. package/dist/LineChart.js +165 -4
  26. package/dist/ListTable.d.ts +30 -3
  27. package/dist/ListTable.js +381 -23
  28. package/dist/ScatterChart.d.ts +3 -2
  29. package/dist/ScatterChart.js +68 -4
  30. package/dist/XAxis.js +40 -22
  31. package/dist/YAxis.d.ts +58 -2
  32. package/dist/YAxis.js +3 -1
  33. package/dist/area.d.ts +34 -1
  34. package/dist/area.js +88 -1
  35. package/dist/bars.d.ts +67 -6
  36. package/dist/bars.js +250 -35
  37. package/dist/box.d.ts +2 -2
  38. package/dist/box.js +158 -40
  39. package/dist/brush.d.ts +142 -0
  40. package/dist/brush.js +179 -0
  41. package/dist/child-index.d.ts +27 -0
  42. package/dist/child-index.js +57 -0
  43. package/dist/context.d.ts +870 -39
  44. package/dist/cursors.d.ts +161 -0
  45. package/dist/cursors.js +503 -0
  46. package/dist/data.d.ts +38 -0
  47. package/dist/data.js +43 -0
  48. package/dist/decimate.d.ts +78 -1
  49. package/dist/decimate.js +157 -0
  50. package/dist/format.d.ts +15 -0
  51. package/dist/format.js +16 -1
  52. package/dist/heat.d.ts +163 -0
  53. package/dist/heat.js +659 -0
  54. package/dist/index.d.ts +13 -4
  55. package/dist/index.js +27 -0
  56. package/dist/line.d.ts +137 -0
  57. package/dist/line.js +328 -0
  58. package/dist/ohlc.d.ts +16 -1
  59. package/dist/ohlc.js +93 -4
  60. package/dist/range.d.ts +14 -1
  61. package/dist/range.js +24 -3
  62. package/dist/scatter.d.ts +17 -9
  63. package/dist/scatter.js +221 -33
  64. package/dist/select.d.ts +13 -5
  65. package/dist/select.js +14 -6
  66. package/dist/selection-fixtures.d.ts +174 -0
  67. package/dist/selection-fixtures.js +569 -0
  68. package/dist/selection-stories.d.ts +73 -0
  69. package/dist/selection-stories.js +301 -0
  70. package/dist/selectors.d.ts +316 -0
  71. package/dist/selectors.js +391 -0
  72. package/dist/span.d.ts +122 -0
  73. package/dist/span.js +203 -0
  74. package/dist/sweep.d.ts +154 -0
  75. package/dist/sweep.js +282 -0
  76. package/dist/theme.d.ts +510 -5
  77. package/dist/theme.js +217 -41
  78. package/dist/tracker.d.ts +6 -0
  79. package/dist/tracker.js +6 -0
  80. package/dist/tradingAxis.fixture.d.ts +78 -0
  81. package/dist/tradingAxis.fixture.js +215 -0
  82. package/dist/useChartLegend.js +18 -3
  83. package/dist/yticks.d.ts +3 -0
  84. package/dist/yticks.js +104 -0
  85. package/package.json +6 -5
@@ -0,0 +1,161 @@
1
+ import type { Sequence, BoundedSequence } from 'pond-ts';
2
+ import { type CursorEntry, type CursorMode, type RangeSpan } from './context.js';
3
+ import type { CursorFormat } from './format.js';
4
+ export interface LineCursorProps {
5
+ /** Show the cursor's time atop the readout (once, on the first row),
6
+ * formatted by the container's readout channel. Default `false`. */
7
+ showTime?: boolean;
8
+ }
9
+ /** The synced vertical cursor **line** — `cursor="line"` as a component (the
10
+ * container default during the deprecation window). Pair with an off-chart
11
+ * readout via `onTrackerChanged`. */
12
+ export declare function LineCursor({ showTime }?: LineCursorProps): null;
13
+ export interface PointCursorProps {
14
+ /** Show the cursor's time atop the readout (first row). Default `false`. */
15
+ showTime?: boolean;
16
+ }
17
+ /** A **dot on each series** at the cursor, no line — `cursor="point"`. */
18
+ export declare function PointCursor({ showTime }?: PointCursorProps): null;
19
+ export interface InlineCursorProps {
20
+ /** Show the cursor's time atop the readout (first row). Default `false`. */
21
+ showTime?: boolean;
22
+ }
23
+ /** Dots **plus a value chip beside each** — `cursor="inline"`. */
24
+ export declare function InlineCursor({ showTime }?: InlineCursorProps): null;
25
+ export interface FlagCursorProps {
26
+ /** Show the cursor's time atop the flag stack (first row). Default `false`. */
27
+ showTime?: boolean;
28
+ }
29
+ /** Dots + **staffed value flags** stacked near the top of the row —
30
+ * `cursor="flag"`. A `cursorFlag` layer (BoxPlot) consolidates onto one flag. */
31
+ export declare function FlagCursor({ showTime }?: FlagCursorProps): null;
32
+ export interface CrosshairCursorProps {
33
+ /**
34
+ * Reticle **y** snapping. **Default `true`** — the reticle centres on the
35
+ * nearest data point. `false` — the horizontal line + value follow the
36
+ * pointer y freely. The **x** always snaps to the data grid either way
37
+ * (declared `snapX: 'sample'`; the container resolves it).
38
+ */
39
+ snap?: boolean;
40
+ /** Pin the cursor's **time to the x axis** (the trading-terminal pill).
41
+ * **Default `true`** — the time pill is the crosshair's readout; there is
42
+ * no per-row time chip to opt into. */
43
+ showTime?: boolean;
44
+ /** Readout format for the x-axis time pill — the `cursorFormat` successor,
45
+ * resolved by the container into the shared readout channel (it also
46
+ * shapes marker indicators + annotation auto-labels, as `cursorFormat`
47
+ * did). */
48
+ format?: CursorFormat;
49
+ }
50
+ /** The inspection **reticle** — `cursor="crosshair"`: dashed cross lines, a
51
+ * centre dot, the value pinned to its y axis, the time pinned to the x axis. */
52
+ export declare function CrosshairCursor({ snap, showTime, format, }?: CrosshairCursorProps): null;
53
+ export interface RangeCursorProps {
54
+ /**
55
+ * The bucketing for the hover band **and the drag's snap** — a pond
56
+ * `Sequence` (realized over the view) or `BoundedSequence` (used as-is; a
57
+ * trading calendar's sessions). A drag extends **bucket by bucket** over
58
+ * these. **Omit ⇒ freeform**: the cursor renders as a plain line and a drag
59
+ * spans the raw `[lo, hi]` (a bar/histogram layer's bins still snap both
60
+ * when present). Time axis only, like `cursorSequence`. Pass a stable
61
+ * reference (the buckets memoize on it).
62
+ */
63
+ sequence?: Sequence | BoundedSequence;
64
+ /**
65
+ * Makes the cursor **draggable**: drag across the plot and the band extends
66
+ * (bucket by bucket with a {@link sequence}, freeform without); on release
67
+ * this fires **once** with the selected {@link RangeSpan}, and the cursor
68
+ * **reverts** to the single-bucket highlight — it does not keep the range.
69
+ *
70
+ * The payload is `{ x: [lo, hi], y? }` in axis units — epoch ms on a time
71
+ * axis, the axis value on a value axis. `y` is absent on today's 1-D
72
+ * layers; the 2-D drag (scatter / heat map) will populate it additively
73
+ * (RFC A3.3). `span.x` feeds `ChartContainer.range` directly — the
74
+ * name-level coherence: a **Range**Cursor emits what `range` accepts —
75
+ * so drag-to-zoom is `onDragRelease={(s) => setRange(s.x)}`.
76
+ *
77
+ * The drag **preempts pan** unless {@link dragModifier} shares the gesture.
78
+ * Continuous x only (a category axis is excluded, as for the legacy
79
+ * `onRegionSelect`).
80
+ */
81
+ onDragRelease?: (span: RangeSpan) => void;
82
+ /**
83
+ * **The OFF switch, not the on switch** (RFC §6, resolved). The drag is
84
+ * already enabled by wiring {@link onDragRelease} — this defaults to
85
+ * `!!onDragRelease`, so you never need to set it to turn the drag on. Set
86
+ * it to `false` to **freeze the gesture without unwiring the callback**
87
+ * (otherwise a `useCallback` dance): the band stays hover-only and the
88
+ * plot's drag goes back to pan (or nothing). Without `onDragRelease` there
89
+ * is nothing to fire, so `enableDrag` alone never starts a gesture.
90
+ */
91
+ enableDrag?: boolean;
92
+ /**
93
+ * Which modifier the drag needs — set `'shift'` when pan is also enabled
94
+ * and you want **plain drag to pan, shift-drag to select**. **Only
95
+ * enforced while pan is enabled** (with pan off there is no gesture
96
+ * conflict, so either drag selects). Omitted ⇒ the drag preempts pan.
97
+ * The `regionSelectModifier` successor.
98
+ */
99
+ dragModifier?: 'shift';
100
+ }
101
+ /**
102
+ * The **range** cursor — `cursor="region"` as a component (RFC A4.1 renames
103
+ * it for what it emits: a live extent — and, dragged, exactly what
104
+ * `ChartContainer.range` accepts — against the annotation `<Region>`'s fixed
105
+ * mark). Hover shades the bucket under the pointer; wiring
106
+ * {@link RangeCursorProps.onDragRelease} adds the drag, which fires once on
107
+ * release and reverts (RFC §6: a region is deliberately a cursor **and** a
108
+ * drag that fires and resets). The gesture rides the shared brush recognizer
109
+ * (`brush.tsx`) — one engine arbitrating every drag claim on the plot.
110
+ */
111
+ export declare function RangeCursor({ sequence, onDragRelease, enableDrag, dragModifier, }?: RangeCursorProps): null;
112
+ /**
113
+ * The deprecation shim (internal): synthesizes the preset equivalent of a
114
+ * legacy `cursor` string — the container's `cursor` prop (or its `'line'`
115
+ * default), and `<ChartRow cursor>` inside a row. Registers as `legacy`, so a
116
+ * component-mounted cursor in the same scope overrides it.
117
+ */
118
+ export declare function LegacyCursor({ mode, showTime, snap, sequence, implicit, }: {
119
+ mode: CursorMode;
120
+ /** The container's `cursorTime` (the in-plot time readout opt-in). */
121
+ showTime: boolean;
122
+ /** The container's `crosshairSnap` (the reticle y-snap). */
123
+ snap: boolean;
124
+ sequence?: Sequence | BoundedSequence | undefined;
125
+ /** This shim carries the container's un-asked-for `'line'` DEFAULT (no
126
+ * `cursor` prop set) — the only cursor a mounted `<MultiSelector>`'s
127
+ * resting block preview replaces (see {@link CursorEntry.implicit}). */
128
+ implicit?: boolean;
129
+ }): null;
130
+ /**
131
+ * The cursors in effect for a row: the row's own mounts when it has any (the
132
+ * per-row override — nearest mount wins, exactly `row.cursor ?? container
133
+ * .cursor`'s semantics), else the container-scoped mounts. Within a scope,
134
+ * component mounts shadow the legacy shim.
135
+ */
136
+ export declare function effectiveCursorEntries(all: readonly CursorEntry[], rowKey: symbol): readonly CursorEntry[];
137
+ /** The scope's single snap/gesture owner (RFC A2.5) — first mount wins; the
138
+ * container dev-warns when a scope has two. */
139
+ export declare function gestureOwner(entries: readonly CursorEntry[]): CursorEntry | undefined;
140
+ /**
141
+ * The cursors whose x-axis slot `<XAxis>` should render: the **hovered row's**
142
+ * effective set while hovering (so a per-row override reaches the axis — the
143
+ * seam the string gate never let it through), else — a controlled
144
+ * `trackerPosition` with no live pointer — every scope's effective set, so a
145
+ * pinned crosshair keeps its pill wherever it is mounted.
146
+ */
147
+ export declare function xAxisCursorEntries(all: readonly CursorEntry[], hoveredRowKey: symbol | null): readonly CursorEntry[];
148
+ /**
149
+ * Dev-warn (once per container) when any scope mounts two gesture-owning
150
+ * cursors — RFC A2.5: stack render-only presets freely, but snap and gesture
151
+ * have one owner per scope, and a silent first-wins would hide the loser.
152
+ */
153
+ export declare function warnOnDuplicateGestureOwners(all: readonly CursorEntry[], warned: {
154
+ current: boolean;
155
+ }): void;
156
+ /** @internal The dev deprecation notice for a legacy cursor prop — one line
157
+ * naming the replacement, shared by the container and row shims. */
158
+ export declare function legacyCursorWarning(lines: readonly string[]): string;
159
+ /** @internal The preset name a legacy `cursor` mode maps to (for warnings). */
160
+ export declare function presetNameFor(mode: CursorMode): string;
161
+ //# sourceMappingURL=cursors.d.ts.map
@@ -0,0 +1,503 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useContext, useEffect, useMemo } from 'react';
3
+ import { ContainerContext, RowContext, } from './context.js';
4
+ import { renderBrushBand } from './brush.js';
5
+ import { flagChipStyle, flagChipX, axisPillStyle, axisPillX } from './chip.js';
6
+ import { useSlotKey } from './use-slot-key.js';
7
+ import { isDev } from './dev.js';
8
+ /**
9
+ * Cursor **presets** — the mounted-component successors of the `cursor` string
10
+ * modes (interaction RFC §4 / A4.1): `<LineCursor>`, `<PointCursor>`,
11
+ * `<InlineCursor>`, `<FlagCursor>`, `<CrosshairCursor>`, `<RangeCursor>`.
12
+ *
13
+ * Each preset registers a `CursorSpec` with the container (the
14
+ * `registerAxis` / `registerLayer` idiom): **declared** snap plus render slots
15
+ * taking resolved geometry. The container resolves — the x-snap, the
16
+ * per-sample measurements, the band — and the slots draw (RFC A2.3). Mount a
17
+ * preset as a child of `<ChartContainer>` (the default for every row) or
18
+ * inside a `<ChartRow>` (the per-row override, replacing `<ChartRow cursor>`).
19
+ *
20
+ * Render-only presets may stack; **one cursor owns snap and gesture per
21
+ * scope**, resolved to the hovered row's innermost mount (RFC A2.5) — the
22
+ * container dev-warns on two gesture owners in one scope.
23
+ *
24
+ * The specs themselves stay unpublished (RFC Q3): these presets are the litmus
25
+ * the contract must pass before a user-authored cursor is supported.
26
+ */
27
+ /** Past this fraction of the plot, an in-plot chip flips to the left of its
28
+ * anchor so it doesn't overflow the right edge (mirrors `Layers`). */
29
+ const LABEL_FLIP_FRACTION = 0.85;
30
+ /** Top inset (px) of the in-plot time readout / the flag stack. */
31
+ const FLAG_TOP = 2;
32
+ /** The cursor ink — the theme's cursor colour, else the axis label colour. */
33
+ function cursorInk(theme) {
34
+ return theme.cursor ?? theme.axis.label;
35
+ }
36
+ /** One chip line's height (px) — the font size plus the chip's leading. */
37
+ function chipLineHeight(theme) {
38
+ return theme.font.size + 5;
39
+ }
40
+ /** The shared cursorX, or `null` when it's outside the plot. */
41
+ function inBoundsX(f) {
42
+ return f.cursorX !== null && f.cursorX >= 0 && f.cursorX <= f.plotWidth
43
+ ? f.cursorX
44
+ : null;
45
+ }
46
+ /**
47
+ * The in-plot cursor-time readout (`showTime`) — plain text, no chip fill,
48
+ * once atop the **first** row (the time is shared; repeating it per row would
49
+ * stutter). `timeX` anchors it: the cursor line for line/point/inline, the
50
+ * flag stack's x for the flag cursor.
51
+ */
52
+ function timeReadout(f, timeX) {
53
+ if (f.formattedTime === null || !f.isFirstRow || timeX === null)
54
+ return null;
55
+ const flip = timeX > f.plotWidth * LABEL_FLIP_FRACTION;
56
+ return (_jsx("div", { style: {
57
+ ...flagChipStyle(f.theme),
58
+ background: 'transparent',
59
+ padding: 0,
60
+ top: `${FLAG_TOP}px`,
61
+ left: flip ? undefined : `${timeX + 4}px`,
62
+ right: flip ? `${f.plotWidth - timeX + 4}px` : undefined,
63
+ color: cursorInk(f.theme),
64
+ }, children: f.formattedTime }));
65
+ }
66
+ /** A dot on each series at the cursor (haloed by the plot background). */
67
+ function sampleDots(f) {
68
+ const background = f.theme.background;
69
+ return f.samples.map((s, i) => (_jsx("circle", { cx: s.px, cy: s.py, r: 3, fill: s.color, stroke: background, strokeWidth: background ? 1 : 0 }, `dot-${i}`)));
70
+ }
71
+ /** The synced vertical cursor line (solid; the crosshair draws its own dashed
72
+ * variant). */
73
+ function cursorLine(f) {
74
+ const x = inBoundsX(f);
75
+ if (x === null)
76
+ return null;
77
+ return (_jsx("line", { x1: Math.round(x), y1: 0, x2: Math.round(x), y2: f.rowHeight, stroke: cursorInk(f.theme), strokeWidth: 1, shapeRendering: "crispEdges" }));
78
+ }
79
+ const NO_WANTS = {
80
+ samples: false,
81
+ flags: false,
82
+ band: false,
83
+ pointer: false,
84
+ time: false,
85
+ };
86
+ /** `cursor="line"` as a spec: the synced vertical line only (+ optional time). */
87
+ function buildLineCursor(o) {
88
+ return {
89
+ spec: {
90
+ snapX: 'none',
91
+ renderPlot: cursorLine,
92
+ ...(o.showTime
93
+ ? { renderPlotHtml: (f) => timeReadout(f, inBoundsX(f)) }
94
+ : {}),
95
+ },
96
+ wants: { ...NO_WANTS, time: o.showTime },
97
+ ownsGesture: false,
98
+ };
99
+ }
100
+ /** `cursor="point"` as a spec: a dot on each series, no line. */
101
+ function buildPointCursor(o) {
102
+ return {
103
+ spec: {
104
+ snapX: 'none',
105
+ renderPlot: sampleDots,
106
+ ...(o.showTime
107
+ ? { renderPlotHtml: (f) => timeReadout(f, inBoundsX(f)) }
108
+ : {}),
109
+ },
110
+ wants: { ...NO_WANTS, samples: true, time: o.showTime },
111
+ ownsGesture: false,
112
+ };
113
+ }
114
+ /** `cursor="inline"` as a spec: dots + a value chip beside each, clamped
115
+ * within the row and flipped left near the right edge. */
116
+ function buildInlineCursor(o) {
117
+ return {
118
+ spec: {
119
+ snapX: 'none',
120
+ renderPlot: sampleDots,
121
+ renderPlotHtml: (f) => {
122
+ const chipStyle = flagChipStyle(f.theme);
123
+ const lh = chipLineHeight(f.theme);
124
+ return (_jsxs(_Fragment, { children: [o.showTime ? timeReadout(f, inBoundsX(f)) : null, f.samples.map((s, i) => {
125
+ const flip = s.px > f.plotWidth * LABEL_FLIP_FRACTION;
126
+ const top = Math.max(lh / 2, Math.min(f.rowHeight - lh / 2, s.py));
127
+ return (_jsx("div", { style: {
128
+ ...chipStyle,
129
+ top: `${top}px`,
130
+ transform: 'translateY(-50%)',
131
+ left: flip ? undefined : `${s.px + 8}px`,
132
+ right: flip ? `${f.plotWidth - s.px + 8}px` : undefined,
133
+ color: s.color,
134
+ }, children: s.formatted }, i));
135
+ })] }));
136
+ },
137
+ },
138
+ wants: { ...NO_WANTS, samples: true, time: o.showTime },
139
+ ownsGesture: false,
140
+ };
141
+ }
142
+ /** `cursor="flag"` as a spec: dots + staffed value flags stacked near the top
143
+ * of the row, plus the consolidated one-chip flag for `cursorFlag` layers
144
+ * (BoxPlot). The time readout (when shown, first row) tops the stack and the
145
+ * staffs start just below it. */
146
+ function buildFlagCursor(o) {
147
+ // The flag stack's top: below the time readout when this row shows it.
148
+ const flagBase = (f) => FLAG_TOP +
149
+ (o.showTime && f.formattedTime !== null && f.isFirstRow
150
+ ? chipLineHeight(f.theme)
151
+ : 0);
152
+ return {
153
+ spec: {
154
+ snapX: 'none',
155
+ renderPlot: (f) => {
156
+ const ink = cursorInk(f.theme);
157
+ const base = flagBase(f);
158
+ return (_jsxs(_Fragment, { children: [f.samples.map((s, i) => s.py > base ? (_jsx("line", { x1: s.px, y1: base, x2: s.px, y2: s.py, stroke: ink, strokeWidth: 1, opacity: 0.5 }, `staff-${i}`)) : null), f.flags.map((fl, i) => fl.topPy > base ? (_jsx("line", { x1: fl.px, y1: base, x2: fl.px, y2: fl.topPy, stroke: ink, strokeWidth: 1, opacity: 0.5 }, `boxstaff-${i}`)) : null), sampleDots(f)] }));
159
+ },
160
+ renderPlotHtml: (f) => {
161
+ const chipStyle = flagChipStyle(f.theme);
162
+ const base = flagBase(f);
163
+ // The time chip tops the flag stack, so it anchors to the stack's x
164
+ // (the nearest sample) rather than the cursor line.
165
+ const timeX = f.samples.length > 0 ? f.samples[0].px : inBoundsX(f);
166
+ return (_jsxs(_Fragment, { children: [o.showTime ? timeReadout(f, timeX) : null, f.cursorX !== null &&
167
+ f.samples.map((s, i) => (_jsx("div", { style: {
168
+ ...chipStyle,
169
+ top: `${base}px`,
170
+ ...flagChipX(s.px, f.plotWidth),
171
+ color: s.color,
172
+ }, children: s.formatted }, i))), f.flags.map((fl, i) => (_jsx("div", { style: {
173
+ ...chipStyle,
174
+ top: `${base}px`,
175
+ ...flagChipX(fl.px, f.plotWidth),
176
+ display: 'flex',
177
+ flexDirection: 'row',
178
+ gap: '6px',
179
+ }, children: fl.lines.map((l, j) => (_jsx("span", { style: { color: l.color }, children: l.text }, j))) }, `boxflag-${i}`)))] }));
180
+ },
181
+ },
182
+ wants: { ...NO_WANTS, samples: true, flags: true, time: o.showTime },
183
+ ownsGesture: false,
184
+ };
185
+ }
186
+ /**
187
+ * The crosshair's single reticle centre for a row: with `snap` (default) the
188
+ * sample nearest the pointer y in the hovered row — or the first sample when
189
+ * nothing is hovered (a pinned tracker shows a reticle in every row); free
190
+ * mode reads the container-resolved raw-pointer measurement.
191
+ */
192
+ function crosshairPick(f, snap) {
193
+ if (inBoundsX(f) === null)
194
+ return null;
195
+ if (!snap)
196
+ return f.pointer;
197
+ if (f.samples.length === 0)
198
+ return null;
199
+ const hoveredRow = f.hoveredRowKey === f.rowKey;
200
+ const cy = f.cursorY;
201
+ const pick = hoveredRow && cy !== null
202
+ ? f.samples.reduce((a, b) => Math.abs(b.py - cy) < Math.abs(a.py - cy) ? b : a)
203
+ : f.hoveredRowKey === null
204
+ ? f.samples[0]
205
+ : null;
206
+ return pick
207
+ ? { py: pick.py, formatted: pick.formatted, side: pick.side }
208
+ : null;
209
+ }
210
+ /** `cursor="crosshair"` as a spec: the dashed reticle (renderPlot), the axis
211
+ * value pill (renderYGutter), and the x-axis time pill (renderXAxis). Declares
212
+ * `snapX: 'sample'` — the container snaps the shared cursorX to the data grid. */
213
+ function buildCrosshairCursor(o) {
214
+ return {
215
+ spec: {
216
+ snapX: 'sample',
217
+ renderPlot: (f) => {
218
+ const x = inBoundsX(f);
219
+ if (x === null)
220
+ return null;
221
+ const ink = cursorInk(f.theme);
222
+ const background = f.theme.background;
223
+ const reticle = crosshairPick(f, o.snap);
224
+ return (_jsxs(_Fragment, { children: [_jsx("line", { x1: Math.round(x), y1: 0, x2: Math.round(x), y2: f.rowHeight, stroke: ink, strokeWidth: 1, strokeDasharray: "3 3", shapeRendering: "crispEdges" }), reticle && (_jsxs(_Fragment, { children: [_jsx("line", { x1: 0, y1: Math.round(reticle.py), x2: f.plotWidth, y2: Math.round(reticle.py), stroke: ink, strokeWidth: 1, strokeDasharray: "3 3", shapeRendering: "crispEdges" }), _jsx("circle", { cx: x, cy: reticle.py, r: 3, fill: ink, stroke: background, strokeWidth: background ? 1 : 0 })] }))] }));
225
+ },
226
+ renderYGutter: (f) => {
227
+ const reticle = crosshairPick(f, o.snap);
228
+ if (reticle === null)
229
+ return null;
230
+ const lh = chipLineHeight(f.theme);
231
+ return (_jsx("div", { style: {
232
+ ...axisPillStyle(f.theme, cursorInk(f.theme)),
233
+ top: `${Math.max(lh / 2, Math.min(f.rowHeight - lh / 2, reticle.py))}px`,
234
+ transform: 'translateY(-50%)',
235
+ ...axisPillX(reticle.side, f.plotWidth),
236
+ }, children: reticle.formatted }));
237
+ },
238
+ ...(o.showTime
239
+ ? {
240
+ renderXAxis: (f) => {
241
+ const x = inBoundsX(f);
242
+ if (x === null || f.xAxis === null)
243
+ return null;
244
+ const ink = cursorInk(f.theme);
245
+ const { onTop, pillOffset } = f.xAxis;
246
+ return (_jsxs(_Fragment, { children: [_jsx("div", { style: {
247
+ position: 'absolute',
248
+ left: `${x}px`,
249
+ [onTop ? 'bottom' : 'top']: 0,
250
+ width: '1px',
251
+ height: `${pillOffset}px`,
252
+ background: ink,
253
+ zIndex: 3,
254
+ } }), _jsx("div", { style: {
255
+ ...axisPillStyle(f.theme, ink),
256
+ left: `${x}px`,
257
+ transform: 'translateX(-50%)',
258
+ [onTop ? 'bottom' : 'top']: `${pillOffset}px`,
259
+ zIndex: 3,
260
+ }, children: f.formattedTime })] }));
261
+ },
262
+ }
263
+ : {}),
264
+ },
265
+ wants: { ...NO_WANTS, samples: true, pointer: !o.snap },
266
+ ownsGesture: true,
267
+ format: o.format,
268
+ };
269
+ }
270
+ /** `cursor="region"` as a spec: the hover-time **band** — the bucket under the
271
+ * pointer (sequence-snapped; freeform = a plain line until a drag shades the
272
+ * raw span) — plus the drag registration the brush recognizer reads
273
+ * (`resolveRangeDrag`). The container resolves the band; the shared
274
+ * `renderBrushBand` slot only draws it (one renderer for every brush-driven
275
+ * component, RFC A1.5 — `<MultiSelector>` plugs into the same one).
276
+ * `enableDrag` is resolved here (`?? !!onDragRelease`) so the registered
277
+ * entry carries the effective switch, not the raw prop. */
278
+ function buildRangeCursor(o) {
279
+ return {
280
+ spec: {
281
+ snapX: o.sequence !== undefined ? 'sequence' : 'none',
282
+ renderPlot: renderBrushBand,
283
+ },
284
+ wants: { ...NO_WANTS, band: true },
285
+ ownsGesture: true,
286
+ sequence: o.sequence,
287
+ onDragRelease: o.onDragRelease,
288
+ enableDrag: o.enableDrag ?? o.onDragRelease !== undefined,
289
+ dragModifier: o.dragModifier,
290
+ };
291
+ }
292
+ /**
293
+ * Register a built cursor with the container, scoped to the enclosing
294
+ * `<ChartRow>` when there is one (the per-row override) else the container.
295
+ * Update-in-place on a prop change (the entry memo), unregister on unmount —
296
+ * the `registerAxis` discipline.
297
+ */
298
+ function useCursorMount(built, legacy,
299
+ /** The container shim's un-asked-for `'line'` default (see
300
+ * {@link CursorEntry.implicit}) — never set by component mounts. */
301
+ implicit = false) {
302
+ const container = useContext(ContainerContext);
303
+ if (container === null) {
304
+ throw new Error('cursor components must be mounted inside a <ChartContainer> (as a ' +
305
+ 'direct child, or inside a <ChartRow> for a per-row override)');
306
+ }
307
+ const row = useContext(RowContext);
308
+ const rowKey = row?.rowKey ?? null;
309
+ const key = useSlotKey();
310
+ const entry = useMemo(() => built === null
311
+ ? null
312
+ : {
313
+ spec: built.spec,
314
+ wants: built.wants,
315
+ ownsGesture: built.ownsGesture,
316
+ sequence: built.sequence,
317
+ format: built.format,
318
+ onDragRelease: built.onDragRelease,
319
+ enableDrag: built.enableDrag,
320
+ dragModifier: built.dragModifier,
321
+ rowKey,
322
+ legacy,
323
+ ...(implicit ? { implicit } : {}),
324
+ }, [built, rowKey, legacy, implicit]);
325
+ const { registerCursor, unregisterCursor } = container;
326
+ useEffect(() => {
327
+ if (entry === null) {
328
+ unregisterCursor(key);
329
+ return;
330
+ }
331
+ registerCursor(key, entry);
332
+ }, [registerCursor, unregisterCursor, key, entry]);
333
+ useEffect(() => () => unregisterCursor(key), [unregisterCursor, key]);
334
+ }
335
+ /** The synced vertical cursor **line** — `cursor="line"` as a component (the
336
+ * container default during the deprecation window). Pair with an off-chart
337
+ * readout via `onTrackerChanged`. */
338
+ export function LineCursor({ showTime = false } = {}) {
339
+ useCursorMount(useMemo(() => buildLineCursor({ showTime }), [showTime]), false);
340
+ return null;
341
+ }
342
+ /** A **dot on each series** at the cursor, no line — `cursor="point"`. */
343
+ export function PointCursor({ showTime = false } = {}) {
344
+ useCursorMount(useMemo(() => buildPointCursor({ showTime }), [showTime]), false);
345
+ return null;
346
+ }
347
+ /** Dots **plus a value chip beside each** — `cursor="inline"`. */
348
+ export function InlineCursor({ showTime = false } = {}) {
349
+ useCursorMount(useMemo(() => buildInlineCursor({ showTime }), [showTime]), false);
350
+ return null;
351
+ }
352
+ /** Dots + **staffed value flags** stacked near the top of the row —
353
+ * `cursor="flag"`. A `cursorFlag` layer (BoxPlot) consolidates onto one flag. */
354
+ export function FlagCursor({ showTime = false } = {}) {
355
+ useCursorMount(useMemo(() => buildFlagCursor({ showTime }), [showTime]), false);
356
+ return null;
357
+ }
358
+ /** The inspection **reticle** — `cursor="crosshair"`: dashed cross lines, a
359
+ * centre dot, the value pinned to its y axis, the time pinned to the x axis. */
360
+ export function CrosshairCursor({ snap = true, showTime = true, format, } = {}) {
361
+ useCursorMount(useMemo(() => buildCrosshairCursor({ snap, showTime, format }), [snap, showTime, format]), false);
362
+ return null;
363
+ }
364
+ /**
365
+ * The **range** cursor — `cursor="region"` as a component (RFC A4.1 renames
366
+ * it for what it emits: a live extent — and, dragged, exactly what
367
+ * `ChartContainer.range` accepts — against the annotation `<Region>`'s fixed
368
+ * mark). Hover shades the bucket under the pointer; wiring
369
+ * {@link RangeCursorProps.onDragRelease} adds the drag, which fires once on
370
+ * release and reverts (RFC §6: a region is deliberately a cursor **and** a
371
+ * drag that fires and resets). The gesture rides the shared brush recognizer
372
+ * (`brush.tsx`) — one engine arbitrating every drag claim on the plot.
373
+ */
374
+ export function RangeCursor({ sequence, onDragRelease, enableDrag, dragModifier, } = {}) {
375
+ useCursorMount(useMemo(() => buildRangeCursor({ sequence, onDragRelease, enableDrag, dragModifier }), [sequence, onDragRelease, enableDrag, dragModifier]), false);
376
+ return null;
377
+ }
378
+ /**
379
+ * The deprecation shim (internal): synthesizes the preset equivalent of a
380
+ * legacy `cursor` string — the container's `cursor` prop (or its `'line'`
381
+ * default), and `<ChartRow cursor>` inside a row. Registers as `legacy`, so a
382
+ * component-mounted cursor in the same scope overrides it.
383
+ */
384
+ export function LegacyCursor({ mode, showTime, snap, sequence, implicit = false, }) {
385
+ const built = useMemo(() => {
386
+ switch (mode) {
387
+ case 'line':
388
+ return buildLineCursor({ showTime });
389
+ case 'point':
390
+ return buildPointCursor({ showTime });
391
+ case 'inline':
392
+ return buildInlineCursor({ showTime });
393
+ case 'flag':
394
+ return buildFlagCursor({ showTime });
395
+ case 'crosshair':
396
+ // The legacy crosshair always pins the time to the x axis; its y-snap
397
+ // is the container's `crosshairSnap`. (`cursorTime` is deliberately
398
+ // NOT forwarded — crosshair has no per-row time chip.)
399
+ return buildCrosshairCursor({ snap, showTime: true });
400
+ case 'region':
401
+ return buildRangeCursor({ sequence });
402
+ case 'none':
403
+ return null;
404
+ }
405
+ }, [mode, showTime, snap, sequence]);
406
+ useCursorMount(built, true, implicit);
407
+ return null;
408
+ }
409
+ /** Drop a scope's legacy (shim-synthesized) entries when the scope also has a
410
+ * component-mounted cursor — mounting a component overrides the string prop. */
411
+ function dropShadowedLegacy(entries) {
412
+ return entries.some((e) => !e.legacy)
413
+ ? entries.filter((e) => !e.legacy)
414
+ : entries;
415
+ }
416
+ /**
417
+ * The cursors in effect for a row: the row's own mounts when it has any (the
418
+ * per-row override — nearest mount wins, exactly `row.cursor ?? container
419
+ * .cursor`'s semantics), else the container-scoped mounts. Within a scope,
420
+ * component mounts shadow the legacy shim.
421
+ */
422
+ export function effectiveCursorEntries(all, rowKey) {
423
+ const rowEntries = all.filter((e) => e.rowKey === rowKey);
424
+ if (rowEntries.length > 0)
425
+ return dropShadowedLegacy(rowEntries);
426
+ return dropShadowedLegacy(all.filter((e) => e.rowKey === null));
427
+ }
428
+ /** The scope's single snap/gesture owner (RFC A2.5) — first mount wins; the
429
+ * container dev-warns when a scope has two. */
430
+ export function gestureOwner(entries) {
431
+ return entries.find((e) => e.ownsGesture);
432
+ }
433
+ /**
434
+ * The cursors whose x-axis slot `<XAxis>` should render: the **hovered row's**
435
+ * effective set while hovering (so a per-row override reaches the axis — the
436
+ * seam the string gate never let it through), else — a controlled
437
+ * `trackerPosition` with no live pointer — every scope's effective set, so a
438
+ * pinned crosshair keeps its pill wherever it is mounted.
439
+ */
440
+ export function xAxisCursorEntries(all, hoveredRowKey) {
441
+ if (hoveredRowKey !== null)
442
+ return effectiveCursorEntries(all, hoveredRowKey);
443
+ const out = [];
444
+ const seenRows = new Set();
445
+ out.push(...dropShadowedLegacy(all.filter((e) => e.rowKey === null)));
446
+ for (const e of all) {
447
+ if (e.rowKey === null || seenRows.has(e.rowKey))
448
+ continue;
449
+ seenRows.add(e.rowKey);
450
+ out.push(...effectiveCursorEntries(all, e.rowKey));
451
+ }
452
+ return out;
453
+ }
454
+ /**
455
+ * Dev-warn (once per container) when any scope mounts two gesture-owning
456
+ * cursors — RFC A2.5: stack render-only presets freely, but snap and gesture
457
+ * have one owner per scope, and a silent first-wins would hide the loser.
458
+ */
459
+ export function warnOnDuplicateGestureOwners(all, warned) {
460
+ if (!isDev || warned.current)
461
+ return;
462
+ const scopes = new Set(all.map((e) => e.rowKey));
463
+ for (const scope of scopes) {
464
+ const entries = dropShadowedLegacy(all.filter((e) => e.rowKey === scope));
465
+ if (entries.filter((e) => e.ownsGesture).length > 1) {
466
+ warned.current = true;
467
+ console.warn('[pond-charts] two gesture-owning cursors (<CrosshairCursor> / ' +
468
+ '<RangeCursor>) are mounted in the same scope — one cursor owns ' +
469
+ 'snap and gesture per scope (the first mounted wins). Render-only ' +
470
+ 'presets (<LineCursor>, <PointCursor>, <InlineCursor>, ' +
471
+ '<FlagCursor>) may stack; pick one gesture owner.');
472
+ return;
473
+ }
474
+ }
475
+ }
476
+ /** @internal The dev deprecation notice for a legacy cursor prop — one line
477
+ * naming the replacement, shared by the container and row shims. */
478
+ export function legacyCursorWarning(lines) {
479
+ return ('[pond-charts] deprecated cursor props (they keep working this minor, ' +
480
+ 'removed next): ' +
481
+ lines.join('; ') +
482
+ '. Mount a cursor component instead (docs/rfcs/interaction.md §9).');
483
+ }
484
+ /** @internal The preset name a legacy `cursor` mode maps to (for warnings). */
485
+ export function presetNameFor(mode) {
486
+ switch (mode) {
487
+ case 'line':
488
+ return '<LineCursor>';
489
+ case 'point':
490
+ return '<PointCursor>';
491
+ case 'inline':
492
+ return '<InlineCursor>';
493
+ case 'flag':
494
+ return '<FlagCursor>';
495
+ case 'crosshair':
496
+ return '<CrosshairCursor>';
497
+ case 'region':
498
+ return '<RangeCursor>';
499
+ case 'none':
500
+ return 'nothing (mount no cursor)';
501
+ }
502
+ }
503
+ //# sourceMappingURL=cursors.js.map