@pond-ts/charts 0.56.2 → 0.58.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CHANGELOG.md +1218 -1
  2. package/dist/AreaChart.d.ts +12 -1
  3. package/dist/AreaChart.js +131 -13
  4. package/dist/BarChart.d.ts +84 -9
  5. package/dist/BarChart.js +295 -40
  6. package/dist/BarList.d.ts +85 -5
  7. package/dist/BarList.js +25 -4
  8. package/dist/BoxList.d.ts +70 -3
  9. package/dist/BoxList.js +21 -7
  10. package/dist/BoxPlot.d.ts +2 -1
  11. package/dist/BoxPlot.js +101 -9
  12. package/dist/Candlestick.d.ts +13 -1
  13. package/dist/Candlestick.js +89 -3
  14. package/dist/ChartContainer.d.ts +79 -48
  15. package/dist/ChartContainer.js +482 -60
  16. package/dist/ChartRow.d.ts +9 -2
  17. package/dist/ChartRow.js +86 -12
  18. package/dist/HeatMap.d.ts +176 -0
  19. package/dist/HeatMap.js +344 -0
  20. package/dist/Layers.d.ts +5 -1
  21. package/dist/Layers.js +1014 -253
  22. package/dist/Legend.js +8 -4
  23. package/dist/LineChart.d.ts +18 -1
  24. package/dist/LineChart.js +165 -4
  25. package/dist/ListTable.d.ts +30 -3
  26. package/dist/ListTable.js +381 -23
  27. package/dist/ScatterChart.d.ts +3 -2
  28. package/dist/ScatterChart.js +68 -4
  29. package/dist/XAxis.js +40 -22
  30. package/dist/YAxis.d.ts +28 -1
  31. package/dist/YAxis.js +24 -2
  32. package/dist/annotations.d.ts +74 -0
  33. package/dist/annotations.js +97 -7
  34. package/dist/area.d.ts +34 -1
  35. package/dist/area.js +88 -1
  36. package/dist/bars.d.ts +178 -5
  37. package/dist/bars.js +504 -46
  38. package/dist/box.d.ts +2 -2
  39. package/dist/box.js +158 -40
  40. package/dist/brush.d.ts +142 -0
  41. package/dist/brush.js +179 -0
  42. package/dist/child-index.d.ts +27 -0
  43. package/dist/child-index.js +57 -0
  44. package/dist/context.d.ts +871 -36
  45. package/dist/cursors.d.ts +161 -0
  46. package/dist/cursors.js +503 -0
  47. package/dist/decimate.d.ts +78 -1
  48. package/dist/decimate.js +157 -0
  49. package/dist/heat.d.ts +163 -0
  50. package/dist/heat.js +659 -0
  51. package/dist/index.d.ts +13 -4
  52. package/dist/index.js +25 -2
  53. package/dist/line.d.ts +137 -0
  54. package/dist/line.js +328 -0
  55. package/dist/ohlc.d.ts +16 -1
  56. package/dist/ohlc.js +93 -4
  57. package/dist/scatter.d.ts +17 -9
  58. package/dist/scatter.js +221 -33
  59. package/dist/select.d.ts +13 -5
  60. package/dist/select.js +14 -6
  61. package/dist/selection-fixtures.d.ts +174 -0
  62. package/dist/selection-fixtures.js +569 -0
  63. package/dist/selection-stories.d.ts +73 -0
  64. package/dist/selection-stories.js +301 -0
  65. package/dist/selectors.d.ts +316 -0
  66. package/dist/selectors.js +391 -0
  67. package/dist/span.d.ts +122 -0
  68. package/dist/span.js +203 -0
  69. package/dist/sweep.d.ts +154 -0
  70. package/dist/sweep.js +282 -0
  71. package/dist/theme.d.ts +517 -11
  72. package/dist/theme.js +220 -39
  73. package/dist/tracker.d.ts +6 -0
  74. package/dist/tracker.js +6 -0
  75. package/dist/tradingAxis.fixture.d.ts +78 -0
  76. package/dist/tradingAxis.fixture.js +215 -0
  77. package/dist/useChartLegend.js +18 -3
  78. package/package.json +3 -3
@@ -0,0 +1,301 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { cloneElement, useMemo, useState, } from 'react';
3
+ import { ChartContainer } from './ChartContainer.js';
4
+ import { ChartRow } from './ChartRow.js';
5
+ import { Layers } from './Layers.js';
6
+ import { YAxis } from './YAxis.js';
7
+ import { Selector, MultiSelector } from './selectors.js';
8
+ import { isSpanSelection, sameMark, selectionContains } from './span.js';
9
+ import { RangeCursor } from './cursors.js';
10
+ import { caption } from './selection-fixtures.js';
11
+ const H = 240;
12
+ /**
13
+ * The chart under test, wired identically in every cell. `children` are
14
+ * non-wrapping registrants rendered before the row (e.g. `<Cursor/>`);
15
+ * `selector`, when given, is a `<Selector>`/`<MultiSelector>` element that
16
+ * WRAPS the row instead (A10.1) — `Chart` clones it with the row as its
17
+ * children, so a story only ever writes the selector element itself and never
18
+ * has to hand-nest `<ChartRow>` inside it.
19
+ */
20
+ function Chart({ fx, children, height = H, selector, ...container }) {
21
+ const row = (_jsxs(ChartRow, { height: height, children: [_jsx(YAxis, { id: fx.axis.id, label: fx.axis.label, min: fx.axis.min, max: fx.axis.max }), _jsx(Layers, { children: fx.renderLayer('svc') })] }));
22
+ return (_jsxs(ChartContainer, { width: 640, ...fx.container, ...container, children: [children, selector ? cloneElement(selector, undefined, row) : row] }));
23
+ }
24
+ const list = (fx, sel) => sel.map((m) => fx.describe(m)).join(', ') || '—';
25
+ export function makeSelectorStories(fx) {
26
+ const Cursor = () => (fx.rangeCursor ? _jsx(RangeCursor, {}) : null);
27
+ return {
28
+ /** **Mounted at the container** — the ordinary case: one `<Selector>` as a
29
+ * child of `<ChartContainer>`, enabling the click for every row. It
30
+ * reports; this story's `useState` is the selection. */
31
+ MountedAtContainer: {
32
+ render: function Render() {
33
+ const [sel, setSel] = useState([]);
34
+ return (_jsxs("div", { children: [_jsx(Chart, { fx: fx, selector: _jsx(Selector, { selected: sel, onSelect: (h) => setSel(h === null ? [] : [h]) }), children: _jsx(Cursor, {}) }), _jsxs("p", { style: caption, children: ["Click a mark to select it, empty space to clear.", ' ', _jsx("strong", { children: "selected:" }), " ", list(fx, sel)] })] }));
35
+ },
36
+ },
37
+ /** **Mounted inside a row** — nearest mount wins, mirroring the cursor
38
+ * components. The top row selects; the bottom row's clicks are inert even
39
+ * though its layer carries an `id`. */
40
+ MountedInRow: {
41
+ render: function Render() {
42
+ const [sel, setSel] = useState([]);
43
+ return (_jsxs("div", { children: [_jsxs(ChartContainer, { width: 640, ...fx.container, children: [_jsxs(ChartRow, { height: 150, children: [_jsx(YAxis, { id: fx.axis.id, label: fx.axis.label || 'primary', min: fx.axis.min, max: fx.axis.max }), _jsx(Cursor, {}), _jsx(Selector, { selected: sel, onSelect: (h) => setSel(h === null ? [] : [h]), children: _jsx(Layers, { children: fx.renderLayer('svc') }) })] }), _jsxs(ChartRow, { height: 110, children: [_jsx(YAxis, { id: fx.secondary.axis.id, label: fx.secondary.axis.label, min: fx.secondary.axis.min, max: fx.secondary.axis.max }), _jsx(Layers, { children: fx.secondary.renderLayer('err') })] })] }), _jsxs("p", { style: caption, children: ["The top row selects; the bottom row has no", ' ', _jsx("code", { children: "<Selector>" }), " in scope. ", _jsx("strong", { children: "selected:" }), ' ', list(fx, sel)] })] }));
44
+ },
45
+ },
46
+ /** **No selector — the plot is inert.** An id-bearing, hit-testable layer
47
+ * whose clicks go nowhere: what a chart that selected on click *before*
48
+ * §7.1 now looks like until one is mounted. In dev the first such click
49
+ * logs a one-time migration warning. */
50
+ NoSelector: {
51
+ render: () => (_jsxs("div", { children: [_jsx(Chart, { fx: fx, children: _jsx(Cursor, {}) }), _jsx("p", { style: caption, children: "Click anywhere \u2014 nothing selects. Check the console for the one-time migration warning." })] })),
52
+ },
53
+ /** **Controlled selection, gesture disabled** — `enabled={false}` exists
54
+ * to protect. The buttons stand in for the legend chip / filter list that
55
+ * really drives this: the chart *displays* a selection owned elsewhere,
56
+ * and the plot is deliberately inert. This is also why §7.1's warning
57
+ * suppresses whenever controlled `selected` is in effect (A2.6). */
58
+ ControlledNoSelector: {
59
+ render: function Render() {
60
+ const [sel, setSel] = useState([
61
+ fx.picks[0].info,
62
+ ]);
63
+ const btn = {
64
+ font: '13px system-ui',
65
+ padding: '4px 10px',
66
+ marginRight: 6,
67
+ cursor: 'pointer',
68
+ };
69
+ return (_jsxs("div", { children: [_jsx("div", { style: { marginBottom: 8 }, children: fx.picks.map((p) => (_jsx("button", { style: btn, onClick: () => setSel((cur) => cur.some((m) => sameMark(m, p.info))
70
+ ? cur.filter((m) => !sameMark(m, p.info))
71
+ : [...cur, p.info]), children: p.label }, p.label))) }), _jsx(Chart, { fx: fx, selector: _jsx(Selector, { enabled: false, selected: sel }) }), _jsxs("p", { style: caption, children: ["The buttons drive the highlight; clicking the plot does nothing.", ' ', _jsx("strong", { children: "selected:" }), " ", list(fx, sel)] })] }));
72
+ },
73
+ },
74
+ /** **The modifiers are reported; the policy is yours.** `onSelect`'s second
75
+ * argument carries `additive` (⌘ on macOS, Ctrl elsewhere) plus the raw
76
+ * keys. pond applies no policy and holds no set — the toggle below is this
77
+ * story's few lines, not the library's. */
78
+ ModifiersReported: {
79
+ render: function Render() {
80
+ const [sel, setSel] = useState([]);
81
+ const [last, setLast] = useState('—');
82
+ return (_jsxs("div", { children: [_jsx(Chart, { fx: fx, selector: _jsx(Selector, { selected: sel, onSelect: (hit, mods) => {
83
+ setLast(hit === null
84
+ ? 'null hit → clear'
85
+ : `${fx.describe(hit)} · additive=${mods?.additive ?? false}` +
86
+ ` shift=${mods?.shiftKey ?? false}` +
87
+ ` alt=${mods?.altKey ?? false}`);
88
+ setSel((cur) => {
89
+ if (hit === null)
90
+ return [];
91
+ if (!(mods?.additive ?? false))
92
+ return [hit];
93
+ return cur.some((m) => sameMark(m, hit))
94
+ ? cur.filter((m) => !sameMark(m, hit))
95
+ : [...cur, hit];
96
+ });
97
+ } }), children: _jsx(Cursor, {}) }), _jsxs("p", { style: caption, children: ["\u2318/Ctrl-click to add or remove; try shift and alt too.", ' ', _jsx("strong", { children: "reported:" }), " ", last, _jsx("br", {}), _jsx("strong", { children: "selected:" }), " ", list(fx, sel)] })] }));
98
+ },
99
+ },
100
+ /** **`onHover` only** — the readout-driving case. The click still selects
101
+ * (the *mount* is the enablement, not the callback), driving the
102
+ * container's own uncontrolled highlight since no `selected` is supplied. */
103
+ HoverOnly: {
104
+ render: function Render() {
105
+ const [hov, setHov] = useState(null);
106
+ return (_jsxs("div", { children: [_jsx(Chart, { fx: fx, selector: _jsx(Selector, { hovered: hov, onHover: setHov }), children: _jsx(Cursor, {}) }), _jsxs("p", { style: caption, children: [_jsx("strong", { children: "hovered:" }), ' ', hov === null ? '—' : `${fx.describe(hov)} (${hov.value})`] })] }));
107
+ },
108
+ },
109
+ /** **A bare `<Selector />`** — no callbacks at all; the mount is the whole
110
+ * statement. With no `selected` prop the container keeps the selection
111
+ * itself: the smallest working chart under the new model. */
112
+ BareSelector: {
113
+ render: () => (_jsxs("div", { children: [_jsx(Chart, { fx: fx, selector: _jsx(Selector, {}), children: _jsx(Cursor, {}) }), _jsxs("p", { style: caption, children: ["Uncontrolled: ", _jsx("code", { children: "<Selector />" }), " with no props, no", ' ', _jsx("code", { children: "selected" }), "."] })] })),
114
+ },
115
+ };
116
+ }
117
+ // ── <MultiSelector> ────────────────────────────────────────────────────────
118
+ const describeEntries = (fx, sel) => {
119
+ if (sel.length === 0)
120
+ return '—';
121
+ return sel
122
+ .map((e) => 'kind' in e && e.kind === 'span'
123
+ ? `span [${fx.describe({ key: e.x[0] })} → ${fx.describe({ key: e.x[1] })})`
124
+ : fx.describe(e))
125
+ .join(', ');
126
+ };
127
+ export function makeMultiSelectorStories(fx) {
128
+ if (!fx.sweep)
129
+ return null;
130
+ const stories = {
131
+ /** **Sweep, freeform.** Drag across the marks: the shared brush band
132
+ * tracks the drag (bin-snapped — a bar layer's own bins feed the snap
133
+ * channel), release reports the covered marks plus the span, and the
134
+ * consumer feeds the **span** back as `selected` — one compact entry
135
+ * however many marks it covers. A click still selects one. */
136
+ SweepMarks: {
137
+ render: function Render() {
138
+ const [sel, setSel] = useState([]);
139
+ const [count, setCount] = useState(0);
140
+ return (_jsxs("div", { children: [_jsx(Chart, { fx: fx, height: 220, selector: _jsx(MultiSelector, { selected: sel, onSelect: (hits, _mods, spans) => {
141
+ setCount(hits.length);
142
+ setSel(spans.length > 0 ? [...spans] : hits.slice(0, 1));
143
+ } }) }), _jsxs("p", { style: caption, children: ["Drag to sweep \u00B7 click one mark to select just it \u00B7 click away to clear.", _jsx("br", {}), _jsx("strong", { children: "selected:" }), " ", describeEntries(fx, sel), " (", count, ' ', "marks)"] })] }));
144
+ },
145
+ },
146
+ /** **A click still selects one.** `<MultiSelector>` is a *superset* of
147
+ * `<Selector>`: below `DRAG_SLOP` the gesture is a click and reports
148
+ * `([hit], modifiers, null)` — a null span, since there is no range. */
149
+ ClickStillSelectsOne: {
150
+ render: function Render() {
151
+ const [sel, setSel] = useState([]);
152
+ const [shape, setShape] = useState('—');
153
+ return (_jsxs("div", { children: [_jsx(Chart, { fx: fx, height: 220, selector: _jsx(MultiSelector, { selected: sel, onSelect: (hits, _mods, spans) => {
154
+ setShape(spans.length === 0
155
+ ? `click → ${hits.length} hit, no span`
156
+ : `sweep → ${hits.length} hits, ${spans.length} span(s)`);
157
+ setSel(spans.length > 0 ? [...spans] : hits);
158
+ } }) }), _jsxs("p", { style: caption, children: ["Click, then drag, and watch the shape of the report change.", ' ', _jsx("strong", { children: "last:" }), " ", shape] })] }));
159
+ },
160
+ },
161
+ /** **Live preview.** Every covered mark lights through the plural
162
+ * `hovered` while the drag is in flight — and at rest the block a drag
163
+ * *would* select is already previewed. The count tracks the band before
164
+ * release; on release the commit equals what was lit. */
165
+ LivePreviewDuringDrag: {
166
+ render: function Render() {
167
+ const [sel, setSel] = useState([]);
168
+ const [preview, setPreview] = useState(0);
169
+ const [committed, setCommitted] = useState(0);
170
+ return (_jsxs("div", { children: [_jsx(Chart, { fx: fx, height: 220, selector: _jsx(MultiSelector, { selected: sel, onHover: (hits) => setPreview(hits.length), onSelect: (hits, _mods, spans) => {
171
+ setCommitted(hits.length);
172
+ setSel(spans.length > 0 ? [...spans] : hits);
173
+ } }) }), _jsxs("p", { style: caption, children: ["Hover, then drag, and watch the count track the band before you release.", _jsx("br", {}), _jsx("strong", { children: "previewing:" }), " ", preview, " marks \u00B7", ' ', _jsx("strong", { children: "last commit:" }), " ", committed, " marks"] })] }));
174
+ },
175
+ },
176
+ /** **Additive sweep, then edit.** ⌘/Ctrl-drag adds a span to the
177
+ * selection. The policy is the consumer's — and note it must handle the
178
+ * **click** case too: click and sweep arrive through one callback, so a
179
+ * policy that only handles spans deletes the selection on the very
180
+ * gesture meant to extend it. */
181
+ SweepAdditive: {
182
+ render: function Render() {
183
+ const [sel, setSel] = useState([]);
184
+ return (_jsxs("div", { children: [_jsx(Chart, { fx: fx, height: 220, selector: _jsx(MultiSelector, { selected: sel, onSelect: (hits, mods, spans) => {
185
+ const add = mods?.additive ?? false;
186
+ if (spans.length > 0) {
187
+ setSel((cur) => (add ? [...cur, ...spans] : [...spans]));
188
+ return;
189
+ }
190
+ if (hits.length === 0)
191
+ return setSel([]);
192
+ setSel((cur) => (add ? [...cur, hits[0]] : [hits[0]]));
193
+ } }) }), _jsxs("p", { style: caption, children: ["Sweep a range, then \u2318/Ctrl-click a mark to add it \u2014 and plain-click to replace. ", _jsx("strong", { children: "selected:" }), ' ', describeEntries(fx, sel)] })] }));
194
+ },
195
+ },
196
+ /** **Demote on edit (RFC A5.2).** A span is editable only *as a whole*; to
197
+ * edit inside one, swap the span entry for the marks stashed at commit
198
+ * time and filter. Plain array arithmetic — pond computes no policy, which
199
+ * is exactly why the descriptor has this shape. */
200
+ DemoteOnEdit: {
201
+ render: function Render() {
202
+ const [sel, setSel] = useState([]);
203
+ const [stash, setStash] = useState([]);
204
+ return (_jsxs("div", { children: [_jsx(Chart, { fx: fx, height: 220, selector: _jsx(MultiSelector, { selected: sel, onSelect: (hits, mods, spans) => {
205
+ if (spans.length > 0) {
206
+ setStash(hits);
207
+ setSel([...spans]);
208
+ return;
209
+ }
210
+ const hit = hits[0];
211
+ if (hit === undefined)
212
+ return setSel([]);
213
+ if (!(mods?.additive ?? false))
214
+ return setSel([hit]);
215
+ // The ⌘-click policy `selectionContains`' doc describes,
216
+ // written out in full: a mark already in the selection comes
217
+ // OUT, one that isn't goes in. The demote is the extra step
218
+ // a span needs before it can lose a member — it becomes the
219
+ // marks it covered, minus this one.
220
+ //
221
+ // Two mistakes this has already made, both worth the words:
222
+ //
223
+ // - **`sameMark`, not `m.key !== hit.key`.** A key IS a
224
+ // bar's identity, so the shorter test looks right — and on
225
+ // a stack or a heat map it knocks out every mark in the
226
+ // bin, which showed up as a column-shaped hole.
227
+ // - **Handle the MARK arm too.** Demoting only rewrote the
228
+ // span entries, so once the span was gone every later
229
+ // ⌘-click fell through the `flatMap` unchanged and the
230
+ // second knock-out silently did nothing.
231
+ setSel((cur) => selectionContains(cur, hit)
232
+ ? cur.flatMap((e) => isSpanSelection(e)
233
+ ? stash.filter((m) => !sameMark(m, hit))
234
+ : sameMark(e, hit)
235
+ ? []
236
+ : [e])
237
+ : [...cur, hit]);
238
+ } }) }), _jsxs("p", { style: caption, children: ["Sweep a run, then \u2318/Ctrl-click marks inside it to knock them out one by one \u2014 the span demotes to its marks on the first, and every \u2318-click after that toggles. ", _jsx("strong", { children: "selected:" }), ' ', describeEntries(fx, sel)] })] }));
239
+ },
240
+ },
241
+ };
242
+ if (fx.sequence !== undefined) {
243
+ const seq = fx.sequence;
244
+ /** **Sweep, snapped to a sequence.** The band, the preview and the
245
+ * committed span all snap to whole buckets — at rest the block under the
246
+ * pointer is already previewed, so what will be selected is visible before
247
+ * the drag starts. Only fixtures with a time bucketing generate this. */
248
+ stories.SweepWithSequence = {
249
+ render: function Render() {
250
+ const [sel, setSel] = useState([]);
251
+ const [count, setCount] = useState(0);
252
+ return (_jsxs("div", { children: [_jsx(Chart, { fx: fx, height: 220, selector: _jsx(MultiSelector, { selected: sel, sequence: seq(), onSelect: (hits, _mods, spans) => {
253
+ setCount(hits.length);
254
+ setSel(spans.length > 0 ? [...spans] : hits);
255
+ } }) }), _jsxs("p", { style: caption, children: ["The sweep snaps to whole buckets \u2014 hover first and the whole block previews. ", _jsx("strong", { children: "selected:" }), " ", describeEntries(fx, sel), " (", count, " marks)"] })] }));
256
+ },
257
+ };
258
+ }
259
+ return stories;
260
+ }
261
+ export function makeSessionStories(fx) {
262
+ const sessions = fx.sessions;
263
+ if (sessions === undefined)
264
+ return null;
265
+ /** Both cells are the same chart under a different bucketing — the only
266
+ * variable is the sequence, which is the whole comparison. */
267
+ const cell = (sequence, note) => ({
268
+ render: function Render() {
269
+ const [sel, setSel] = useState([]);
270
+ const [count, setCount] = useState(0);
271
+ const [preview, setPreview] = useState(0);
272
+ const seq = useMemo(sequence, []);
273
+ return (_jsxs("div", { children: [_jsx(Chart, { fx: fx, height: 220, selector: _jsx(MultiSelector, { selected: sel, sequence: seq, onHover: (hits) => setPreview(hits.length), onSelect: (hits, _mods, spans) => {
274
+ setCount(hits.length);
275
+ setSel(spans.length > 0 ? [...spans] : hits);
276
+ } }) }), _jsxs("p", { style: caption, children: [note, _jsx("br", {}), _jsx("strong", { children: "previewing:" }), " ", preview, " bars \u00B7", ' ', _jsx("strong", { children: "selected:" }), " ", describeEntries(fx, sel), " (", count, ' ', "bars)"] })] }));
277
+ },
278
+ });
279
+ return {
280
+ /** **The bucketing agrees with the sessions.** One block per session, so
281
+ * every block edge lands exactly on a divider and no block spans a
282
+ * collapsed gap. Hover a bar and its whole session lights; a click
283
+ * commits that session. */
284
+ SequenceConformsToSessions: cell(sessions.conforming, _jsxs(_Fragment, { children: ["One bucket ", _jsx("em", { children: "per session" }), ": the band's edges land on the dividers, and no block ever spans a break."] })),
285
+ /**
286
+ * **The bucketing doesn't.** A wall-clock day anchored mid-session, so a
287
+ * block is the afternoon of one session plus the morning of the next, and
288
+ * the band crosses a divider. The collapsed gap has zero width, so the
289
+ * block still draws as one rectangle — what it *contains* is two runs of
290
+ * bars from different days.
291
+ *
292
+ * The weekend is where that stops being cosmetic. Wall-clock buckets keep
293
+ * marching through Saturday and Sunday while the axis has no sessions to
294
+ * give them, so the uniform run of 7-bar blocks breaks into **4, then an
295
+ * empty bucket with no trading time in it at all, then 3** — a bucketing
296
+ * that ignores the session grid cannot keep its blocks the same size.
297
+ */
298
+ SequenceCrossesSessions: cell(sessions.crossing, _jsxs(_Fragment, { children: ["A wall-clock day anchored ", _jsx("em", { children: "mid-session" }), ": each block takes one afternoon plus the next morning, so the band crosses a divider. Hover either side of the ", _jsx("strong", { children: "weekend" }), " divider \u2014 the blocks there are 4 bars and 3, not 7, and the bucket between them holds no trading time at all."] })),
299
+ };
300
+ }
301
+ //# sourceMappingURL=selection-stories.js.map
@@ -0,0 +1,316 @@
1
+ import { type ReactNode } from 'react';
2
+ import type { Sequence, BoundedSequence } from 'pond-ts';
3
+ import { type SelectInfo, type SelectModifiers, type SelectionEntry, type SelectorEntry, type SpanSelection } from './context.js';
4
+ export interface SelectorProps {
5
+ /**
6
+ * `false` disables the **gesture** — no hit-testing, `onHover`/`onSelect`
7
+ * never fire, a plot click behaves as if `<Selector>` weren't mounted at
8
+ * all. **Omitted ⇒ `true`.**
9
+ *
10
+ * `<Selector enabled={false} selected={sel} />` is controlled highlighting
11
+ * with no plot gesture — a legend chip or an external filter list driving
12
+ * the chart, deliberately inert on click (interaction RFC A10.2).
13
+ */
14
+ enabled?: boolean;
15
+ /**
16
+ * Controlled selection — the selected mark(s) (echo `onSelect`'s hit back),
17
+ * or `null`. **Omitted ⇒ uncontrolled** (a click on a selectable layer
18
+ * manages it internally; pass `null` to force nothing selected). A layer is
19
+ * **selectable only when it carries an `id`** — the stable series identity.
20
+ *
21
+ * **Accepts a set**: an array lights several marks at once, and array
22
+ * entries may be {@link SpanSelection}s (a swept range, demoted to one
23
+ * entry instead of enumerating every covered mark). A single `SelectInfo`
24
+ * still works and means exactly what it did.
25
+ */
26
+ selected?: SelectInfo | readonly SelectionEntry[] | null;
27
+ /**
28
+ * Controlled hover-highlight — the transiently lit mark(s), or `null`.
29
+ * **Omitted ⇒ uncontrolled.** The hover analog of {@link selected}: pass it
30
+ * to pin lit marks from outside the chart (e.g. hovering a legend / list row
31
+ * lights the matching bar). Accepts a single mark or a set, the same union
32
+ * `selected` takes.
33
+ */
34
+ hovered?: SelectInfo | readonly SelectInfo[] | null;
35
+ /**
36
+ * What is under the pointer — one {@link SelectInfo}, or `null` on leaving
37
+ * every mark. Deduped by the mark's full identity, so it fires on a mark
38
+ * transition rather than on every pointer move.
39
+ */
40
+ onHover?: (hit: SelectInfo | null) => void;
41
+ /**
42
+ * What was clicked — one {@link SelectInfo}, or `null` for a click that hit
43
+ * no mark (the deselect path) — plus the modifiers held.
44
+ *
45
+ * **The library reports; you decide.** `modifiers.additive` is the
46
+ * platform-idiomatic add chord (⌘ on macOS, Ctrl elsewhere); pond applies no
47
+ * policy to it and holds no set. Compute the next selection yourself and
48
+ * feed it back as this same component's `selected`:
49
+ *
50
+ * ```tsx
51
+ * <Selector
52
+ * selected={sel}
53
+ * onSelect={(hit, mods) =>
54
+ * setSel(
55
+ * hit === null ? [] : mods?.additive ? toggle(sel, hit) : [hit],
56
+ * )
57
+ * }
58
+ * />
59
+ * ```
60
+ *
61
+ * `modifiers` is absent for a **programmatic** select (a `<Legend>` chip),
62
+ * which carries no keyboard state.
63
+ */
64
+ onSelect?: (hit: SelectInfo | null, modifiers?: SelectModifiers) => void;
65
+ /**
66
+ * What it applies to (interaction RFC A10.1): every `<ChartRow>` when
67
+ * mounted as a direct child of `<ChartContainer>`, or just one row when
68
+ * mounted inside that `<ChartRow>`. Optional — `<Selector />` with no
69
+ * children keeps working exactly as it always has.
70
+ *
71
+ * **Row-scoped, wrap the row's `<Layers>` — not its axes.** `<ChartRow>`
72
+ * places axes into gutters by matching its *own* children against `<YAxis>`,
73
+ * so an axis nested inside this component is invisible to that sort and
74
+ * renders in the plot column instead. Dev warns if you do.
75
+ *
76
+ * ```tsx
77
+ * <ChartRow height={180}>
78
+ * <YAxis id="v" /> // stays a direct child of the row
79
+ * <Selector selected={sel} onSelect={setSel}>
80
+ * <Layers>…</Layers>
81
+ * </Selector>
82
+ * </ChartRow>
83
+ * ```
84
+ */
85
+ children?: ReactNode;
86
+ }
87
+ /**
88
+ * Mount it to make the plot **click-selectable** (RFC §7.1) and to hold the
89
+ * selection state that produces — wrap every `<ChartRow>` as a direct child of
90
+ * `<ChartContainer>`, or wrap one row's **`<Layers>`** to scope the *gesture*
91
+ * to that row (interaction RFC A10.1; see {@link SelectorProps.children} for
92
+ * why the axes stay outside).
93
+ *
94
+ * **Placement scopes the gesture, not the state.** A {@link SelectInfo} names a
95
+ * *layer*, not a row, so `selected` / `hovered` apply chart-wide wherever this
96
+ * sits — one selector should own each per chart (first registered wins, dev
97
+ * warns otherwise).
98
+ *
99
+ * ```tsx
100
+ * <ChartContainer>
101
+ * <Selector
102
+ * selected={sel}
103
+ * hovered={hov}
104
+ * onSelect={(hit, mods) => …}
105
+ * onHover={setHov}
106
+ * >
107
+ * <ChartRow>…</ChartRow>
108
+ * </Selector>
109
+ * </ChartContainer>
110
+ * ```
111
+ *
112
+ * `value`/`onChange`, one component: `selected` is what's lit, `onSelect`
113
+ * reports what changes it, and the mount itself is what makes a plot click do
114
+ * anything. Need controlled highlighting with **no** plot click at all?
115
+ * `enabled={false}`.
116
+ */
117
+ export declare function Selector({ enabled, selected, hovered, onHover, onSelect, children, }?: SelectorProps): import("react/jsx-runtime").JSX.Element;
118
+ export interface MultiSelectorProps {
119
+ /**
120
+ * `false` disables the gesture — no hit-testing, no armed sweep, callbacks
121
+ * never fire. **Omitted ⇒ `true`.** See {@link SelectorProps.enabled};
122
+ * applies identically here.
123
+ */
124
+ enabled?: boolean;
125
+ /**
126
+ * Controlled selection — the state half of what `onSelect` reports. See
127
+ * {@link SelectorProps.selected}; `<MultiSelector>` additionally accepts
128
+ * {@link SpanSelection} entries directly, which is exactly the shape a
129
+ * sweep's `onSelect` hands back.
130
+ */
131
+ selected?: SelectInfo | readonly SelectionEntry[] | null;
132
+ /** Controlled hover-highlight — see {@link SelectorProps.hovered}. Accepts
133
+ * a set, since a sweep's live preview lights several marks at once. */
134
+ hovered?: SelectInfo | readonly SelectInfo[] | null;
135
+ /**
136
+ * Snap the sweep to buckets — a pond `Sequence` (realized over the view) or
137
+ * `BoundedSequence` (used as-is; a trading calendar's sessions). A drag
138
+ * extends **bucket by bucket** over these, capturing every mark the snapped
139
+ * window covers. **Omit ⇒ freeform**: the sweep covers the raw drag span (a
140
+ * bar/histogram layer's bins still snap it when present — the same shared
141
+ * snap-bucket channel `<RangeCursor sequence>` feeds). Pass a stable
142
+ * reference (the realized buckets memoize on it).
143
+ */
144
+ sequence?: Sequence | BoundedSequence;
145
+ /**
146
+ * The marks the gesture currently covers — or **would** cover:
147
+ *
148
+ * - **At rest**, the marks of the **snap block under the pointer** (the
149
+ * `sequence` bucket, else the layer's own bin/slot), reported once per
150
+ * block transition. This is the resting preview: hovering ANY mark of a
151
+ * block reports the whole block, because that is exactly the set a drag
152
+ * begun and released there would select.
153
+ * - **During a sweep**, every covered mark, updated as the drag crosses
154
+ * marks (coalesced to animation frames past the first cut).
155
+ *
156
+ * Echo it back as this component's `hovered` only when you control hover —
157
+ * uncontrolled, the covered marks already light through the container's
158
+ * own hover state (RFC A3.4: the library owns the state, each layer draws
159
+ * its own hover treatment).
160
+ */
161
+ onHover?: (hits: readonly SelectInfo[]) => void;
162
+ /**
163
+ * The committed selection, on release (RFC A5.2's signature):
164
+ *
165
+ * - **A sweep** reports every covered mark, the modifiers held, and the
166
+ * {@link SpanSelection}s the coverage demotes to — `hits` are the
167
+ * materialised live preview (no fresh range query), `spans` are the
168
+ * snapped-outward extents whose `selectionContains` test reproduces
169
+ * exactly `hits`. Feed `[...others, ...spans]` back as this component's
170
+ * `selected` and stash `hits` for A5.2's demote-on-edit: to edit *inside*
171
+ * a span later, swap that span entry for the stashed hits and filter —
172
+ * plain array arithmetic, no interval math.
173
+ * - **A click** (no movement past the drag slop) is `<Selector>`'s gesture
174
+ * in this currency: one hit (or none — the deselect path), the modifiers,
175
+ * and an **empty** `spans`. Clicks produce marks; only sweeps produce
176
+ * spans.
177
+ *
178
+ * **`spans` is plural because one sweep can commit several.** A trace sweep
179
+ * produces one span per trace ([PND-TRACESEL]): every trace shares the swept
180
+ * x window, so singling one out by z-order would be arbitrary to the reader.
181
+ * Mark layers keep topmost-wins, so there it holds exactly one. **Topmost
182
+ * layer first**, and compare spans by `id` rather than identity — each
183
+ * span-only layer clamps the window to *its own* key range, so two traces of
184
+ * different extents report different `x` for one drag.
185
+ *
186
+ * `modifiers` is absent for a **programmatic** select (a `<Legend>` chip),
187
+ * as on `<Selector onSelect>`. **The library reports; you decide** — pond
188
+ * applies no policy to the modifiers and holds no set.
189
+ */
190
+ onSelect?: (hits: readonly SelectInfo[], modifiers: SelectModifiers | undefined, spans: readonly SpanSelection[]) => void;
191
+ /** What it applies to — see {@link SelectorProps.children} (interaction RFC
192
+ * A10.1); identical scoping rule. */
193
+ children?: ReactNode;
194
+ }
195
+ /**
196
+ * `<MultiSelector>` — **sweep-select as a mounted component** (interaction RFC
197
+ * §8 / A4.2 / A10), a superset of `<Selector>`: a click still selects one mark,
198
+ * and a drag past the slop **sweeps** — the band extends (bucket by bucket
199
+ * with a {@link MultiSelectorProps.sequence}, freeform without), every
200
+ * covered mark lights through the plural `hovered` as the drag moves, and
201
+ * release commits `(hits, modifiers, spans)` once. The gesture rides the
202
+ * shared brush recognizer (`brush.tsx`) and draws the same band `<RangeCursor>`
203
+ * does — identical pixels, different currency (§8.1): the range cursor
204
+ * releases an extent, this releases **marks** (which is what folds the
205
+ * category axis in — ordinal and continuous are the same gesture when nobody
206
+ * sees a numeric range).
207
+ *
208
+ * ```tsx
209
+ * <ChartContainer>
210
+ * <MultiSelector
211
+ * selected={sel}
212
+ * sequence={daily}
213
+ * onSelect={(hits, mods, spans) => …}
214
+ * >
215
+ * <ChartRow>…</ChartRow>
216
+ * </MultiSelector>
217
+ * </ChartContainer>
218
+ * ```
219
+ *
220
+ * Wraps what it applies to, same as `<Selector>`; `selected` / `hovered` live
221
+ * here too. The sweep captures marks from the row's **topmost** sweep-capable
222
+ * layer (the z-order rule a click already follows); a layer without an `id`
223
+ * is never swept (Q8).
224
+ *
225
+ * **Mounting also changes the row's RESTING state** — the grey band and the
226
+ * hover are a live preview of the block a drag would select:
227
+ *
228
+ * - The shared brush band becomes the **resting cursor**, spanning the snap
229
+ * block under the pointer (the `sequence` bucket, else the layer's own
230
+ * bin/slot), replacing the container's implicit `'line'` default. An
231
+ * explicitly chosen cursor — a mounted component, or a legacy `cursor`
232
+ * string the consumer actually set — still wins the surface.
233
+ * - Hover is **block-scoped**: pointing at any one mark of a block lights
234
+ * (and reports) every mark in it. Rest and drag share one code path — the
235
+ * same snap buckets, the same layer session — so what the rest previews and
236
+ * what a drag commits cannot disagree; the drag just grows the same band.
237
+ */
238
+ export declare function MultiSelector({ enabled, selected, hovered, sequence, onHover, onSelect, children, }?: MultiSelectorProps): import("react/jsx-runtime").JSX.Element;
239
+ /**
240
+ * **Value-equality for a registered selector — the guard that stops a
241
+ * controlled selection from looping.** `registerSelector` must no-op when the
242
+ * incoming entry is value-equal to the stored one, exactly as `registerAxis`
243
+ * does via `axisSpecEqual` (`ChartRow.tsx`), and for the same reason spelled
244
+ * out there: register → `setState` → re-render → register is a
245
+ * "Maximum update depth exceeded" spin.
246
+ *
247
+ * A10.3 made this guard load-bearing rather than defensive. The entry now
248
+ * carries the controlled *values*, and a consumer writing
249
+ * `selected={[hit]}` — or `selected={[{ id, key, … }]}` — mints a fresh
250
+ * reference every render. On its own that is survivable, because a
251
+ * container-only state update does not re-run the consumer's JSX. **But a
252
+ * descendant that consumes the container context and renders that inline
253
+ * array does re-run** — `useChartLegend()` is a supported example — so the
254
+ * chain became: registry update → new frame → context change → descendant
255
+ * re-render → fresh array → register → registry update, without end. Compare
256
+ * by value and the cycle closes on the first iteration. (Codex finding on
257
+ * #638; the reference-only guard it replaces was mine.)
258
+ *
259
+ * Cost: the common case is a stable array from `useState`, which hits the
260
+ * reference fast path. A fresh array costs one element-wise pass with no
261
+ * allocation, which is the right trade against an unbounded render loop.
262
+ */
263
+ export declare function selectorEntryEqual(a: SelectorEntry, b: SelectorEntry): boolean;
264
+ /**
265
+ * The selectors in effect for a row's GESTURE: the row's own mounts when it has
266
+ * any (the per-row scope — nearest mount wins, mirroring
267
+ * `effectiveCursorEntries`), else the container-scoped mounts. A
268
+ * `gestureEnabled: false` entry (`<Selector enabled={false}>`) is filtered out
269
+ * entirely — it behaves as unmounted for click/hover/sweep purposes, exactly
270
+ * as `enabled` promises.
271
+ *
272
+ * `rowKey` of `null` asks for the **container** scope only — the programmatic
273
+ * (legend) path, which belongs to no row.
274
+ *
275
+ * Controlled-state resolution does **not** use this function — state is
276
+ * chart-wide, not row-scoped, and a disabled selector may still own it. See
277
+ * {@link resolveControlledSelected} / {@link resolveControlledHovered}.
278
+ */
279
+ export declare function effectiveSelectorEntries(all: readonly SelectorEntry[], rowKey: symbol | null): readonly SelectorEntry[];
280
+ /**
281
+ * Resolve the chart-wide controlled `selected`, from whichever registered
282
+ * selector declared it. Not row-scoped — selection identity spans every row
283
+ * (a `SelectInfo.id` names a layer, not a row) — and not filtered on
284
+ * `gestureEnabled`: `<Selector enabled={false} selected={…}>` is exactly the
285
+ * "state, no gesture" configuration `enabled` exists for.
286
+ */
287
+ export declare function resolveControlledSelected(all: readonly SelectorEntry[], warned: {
288
+ current: boolean;
289
+ }): {
290
+ readonly present: boolean;
291
+ readonly value: SelectInfo | readonly SelectionEntry[] | null;
292
+ };
293
+ /** As {@link resolveControlledSelected}, for `hovered`. */
294
+ export declare function resolveControlledHovered(all: readonly SelectorEntry[], warned: {
295
+ current: boolean;
296
+ }): {
297
+ readonly present: boolean;
298
+ readonly value: SelectInfo | readonly SelectInfo[] | null;
299
+ };
300
+ /**
301
+ * RFC §7.1's softening: a plot click resolved to a real mark and there was no
302
+ * `<Selector>` to tell — the exact path that goes silently inert on upgrade.
303
+ *
304
+ * **Suppressed when controlled `selected` is in effect** (A2.6) — that is the
305
+ * runtime signature of the *endorsed* controlled-highlight setup
306
+ * (`<Selector enabled={false} selected={…}>`, plot deliberately inert), and
307
+ * the warning should not spend its loudness on people already doing that.
308
+ *
309
+ * Not permanent: once mounting is the established model, an `id` without a
310
+ * `<Selector>` is a legitimate configuration (Q8) and warning on it forever
311
+ * would flag a supported setup. Fires once per container.
312
+ */
313
+ export declare function warnInertClick(warned: {
314
+ current: boolean;
315
+ }): void;
316
+ //# sourceMappingURL=selectors.d.ts.map