@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,391 @@
1
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useContext, useLayoutEffect, useMemo, useRef, } from 'react';
3
+ import { isDev } from './dev.js';
4
+ import { isSpanSelection } from './span.js';
5
+ import { ContainerContext, RowContext, } from './context.js';
6
+ import { useSlotKey } from './use-slot-key.js';
7
+ /**
8
+ * Register a selector with the container, scoped to the enclosing `<ChartRow>`
9
+ * when there is one (that row's clicks only) else the container (every row).
10
+ *
11
+ * The registered entry is memoized on which callbacks are *present* (not their
12
+ * identity — read through a ref at report time) and on the controlled state
13
+ * values — a consumer writing `<Selector onSelect={(hit) => …} />` passes a
14
+ * fresh function every render, and re-registering on each one would thrash the
15
+ * container's registry state (and, since the registry is `useState`, loop).
16
+ */
17
+ function useSelectorMount(opts) {
18
+ const { cb, gestureEnabled, multi = false, sequence, declaresSelected, selected, declaresHovered, hovered, } = opts;
19
+ const container = useContext(ContainerContext);
20
+ if (container === null) {
21
+ throw new Error('<Selector> must be mounted inside a <ChartContainer> (as a direct ' +
22
+ 'child, or inside a <ChartRow> to scope it to that row)');
23
+ }
24
+ const row = useContext(RowContext);
25
+ const rowKey = row?.rowKey ?? null;
26
+ const key = useSlotKey();
27
+ const cbRef = useRef(cb);
28
+ useLayoutEffect(() => {
29
+ cbRef.current = cb;
30
+ });
31
+ const hasHover = cb.onHover !== undefined;
32
+ const hasSelect = cb.onSelect !== undefined;
33
+ const hasHoverMany = cb.onHoverMany !== undefined;
34
+ const hasSelectMany = cb.onSelectMany !== undefined;
35
+ const entry = useMemo(() => ({
36
+ onHover: gestureEnabled && hasHover
37
+ ? (hit) => cbRef.current.onHover?.(hit)
38
+ : undefined,
39
+ // Forward the modifiers with the arity we were called with. `select()`
40
+ // omits them for a programmatic (legend) select, and passing an
41
+ // explicit `undefined` there would change the observed arity for every
42
+ // consumer asserting `toHaveBeenCalledWith(hit)`.
43
+ onSelect: gestureEnabled && hasSelect
44
+ ? (hit, modifiers) => {
45
+ if (modifiers === undefined)
46
+ cbRef.current.onSelect?.(hit);
47
+ else
48
+ cbRef.current.onSelect?.(hit, modifiers);
49
+ }
50
+ : undefined,
51
+ multi,
52
+ onHoverMany: gestureEnabled && hasHoverMany
53
+ ? (hits) => cbRef.current.onHoverMany?.(hits)
54
+ : undefined,
55
+ onSelectMany: gestureEnabled && hasSelectMany
56
+ ? (hits, modifiers, spans) => cbRef.current.onSelectMany?.(hits, modifiers, spans)
57
+ : undefined,
58
+ sequence: gestureEnabled ? sequence : undefined,
59
+ rowKey,
60
+ gestureEnabled,
61
+ declaresSelected,
62
+ selected,
63
+ declaresHovered,
64
+ hovered,
65
+ }), [
66
+ gestureEnabled,
67
+ hasHover,
68
+ hasSelect,
69
+ hasHoverMany,
70
+ hasSelectMany,
71
+ multi,
72
+ sequence,
73
+ rowKey,
74
+ declaresSelected,
75
+ selected,
76
+ declaresHovered,
77
+ hovered,
78
+ ]);
79
+ const { registerSelector, unregisterSelector } = container;
80
+ // **`useLayoutEffect`, not `useEffect`** — this registration is the path
81
+ // controlled `selected` / `hovered` now travel (A10.3), and a passive effect
82
+ // would make them a commit late: the first paint after a `selected` change
83
+ // would show the *previous* selection, and a mount with `selected` already
84
+ // set would flash unselected before lighting up. The old container props were
85
+ // render-synchronous, so anything slower here is a visible regression rather
86
+ // than a micro-optimisation. (Reviewer finding on #638.)
87
+ useLayoutEffect(() => {
88
+ registerSelector(key, entry);
89
+ }, [registerSelector, key, entry]);
90
+ // Unregistration is a LAYOUT cleanup too, and the symmetry is load-bearing.
91
+ // With a passive cleanup the two halves ran in different phases, so removing
92
+ // a selector left its entry in the registry for one commit — the container
93
+ // drew once from a dead owner — and a keyed remount registered the new owner
94
+ // (layout) *before* the old one was dropped (passive), which put both in the
95
+ // Map at once and let "first registered wins" hand the old value out until
96
+ // cleanup caught up. Fixing the read path (above) without fixing the teardown
97
+ // just moved the stale window. (Codex finding on #638.)
98
+ useLayoutEffect(() => () => unregisterSelector(key), [unregisterSelector, key]);
99
+ }
100
+ /**
101
+ * Mount it to make the plot **click-selectable** (RFC §7.1) and to hold the
102
+ * selection state that produces — wrap every `<ChartRow>` as a direct child of
103
+ * `<ChartContainer>`, or wrap one row's **`<Layers>`** to scope the *gesture*
104
+ * to that row (interaction RFC A10.1; see {@link SelectorProps.children} for
105
+ * why the axes stay outside).
106
+ *
107
+ * **Placement scopes the gesture, not the state.** A {@link SelectInfo} names a
108
+ * *layer*, not a row, so `selected` / `hovered` apply chart-wide wherever this
109
+ * sits — one selector should own each per chart (first registered wins, dev
110
+ * warns otherwise).
111
+ *
112
+ * ```tsx
113
+ * <ChartContainer>
114
+ * <Selector
115
+ * selected={sel}
116
+ * hovered={hov}
117
+ * onSelect={(hit, mods) => …}
118
+ * onHover={setHov}
119
+ * >
120
+ * <ChartRow>…</ChartRow>
121
+ * </Selector>
122
+ * </ChartContainer>
123
+ * ```
124
+ *
125
+ * `value`/`onChange`, one component: `selected` is what's lit, `onSelect`
126
+ * reports what changes it, and the mount itself is what makes a plot click do
127
+ * anything. Need controlled highlighting with **no** plot click at all?
128
+ * `enabled={false}`.
129
+ */
130
+ export function Selector({ enabled = true, selected, hovered, onHover, onSelect, children, } = {}) {
131
+ useSelectorMount({
132
+ cb: { onHover, onSelect },
133
+ gestureEnabled: enabled,
134
+ declaresSelected: selected !== undefined,
135
+ selected,
136
+ declaresHovered: hovered !== undefined,
137
+ hovered,
138
+ });
139
+ return _jsx(_Fragment, { children: children });
140
+ }
141
+ /**
142
+ * `<MultiSelector>` — **sweep-select as a mounted component** (interaction RFC
143
+ * §8 / A4.2 / A10), a superset of `<Selector>`: a click still selects one mark,
144
+ * and a drag past the slop **sweeps** — the band extends (bucket by bucket
145
+ * with a {@link MultiSelectorProps.sequence}, freeform without), every
146
+ * covered mark lights through the plural `hovered` as the drag moves, and
147
+ * release commits `(hits, modifiers, spans)` once. The gesture rides the
148
+ * shared brush recognizer (`brush.tsx`) and draws the same band `<RangeCursor>`
149
+ * does — identical pixels, different currency (§8.1): the range cursor
150
+ * releases an extent, this releases **marks** (which is what folds the
151
+ * category axis in — ordinal and continuous are the same gesture when nobody
152
+ * sees a numeric range).
153
+ *
154
+ * ```tsx
155
+ * <ChartContainer>
156
+ * <MultiSelector
157
+ * selected={sel}
158
+ * sequence={daily}
159
+ * onSelect={(hits, mods, spans) => …}
160
+ * >
161
+ * <ChartRow>…</ChartRow>
162
+ * </MultiSelector>
163
+ * </ChartContainer>
164
+ * ```
165
+ *
166
+ * Wraps what it applies to, same as `<Selector>`; `selected` / `hovered` live
167
+ * here too. The sweep captures marks from the row's **topmost** sweep-capable
168
+ * layer (the z-order rule a click already follows); a layer without an `id`
169
+ * is never swept (Q8).
170
+ *
171
+ * **Mounting also changes the row's RESTING state** — the grey band and the
172
+ * hover are a live preview of the block a drag would select:
173
+ *
174
+ * - The shared brush band becomes the **resting cursor**, spanning the snap
175
+ * block under the pointer (the `sequence` bucket, else the layer's own
176
+ * bin/slot), replacing the container's implicit `'line'` default. An
177
+ * explicitly chosen cursor — a mounted component, or a legacy `cursor`
178
+ * string the consumer actually set — still wins the surface.
179
+ * - Hover is **block-scoped**: pointing at any one mark of a block lights
180
+ * (and reports) every mark in it. Rest and drag share one code path — the
181
+ * same snap buckets, the same layer session — so what the rest previews and
182
+ * what a drag commits cannot disagree; the drag just grows the same band.
183
+ */
184
+ export function MultiSelector({ enabled = true, selected, hovered, sequence, onHover, onSelect, children, } = {}) {
185
+ useSelectorMount({
186
+ cb: {
187
+ onHover: undefined,
188
+ onSelect: undefined,
189
+ onHoverMany: onHover,
190
+ onSelectMany: onSelect,
191
+ },
192
+ gestureEnabled: enabled,
193
+ multi: true,
194
+ sequence,
195
+ declaresSelected: selected !== undefined,
196
+ selected,
197
+ declaresHovered: hovered !== undefined,
198
+ hovered,
199
+ });
200
+ return _jsx(_Fragment, { children: children });
201
+ }
202
+ /** One controlled entry — a mark or a span — compared by the fields that carry
203
+ * its identity, so a freshly-built object literal equals the one it replaces. */
204
+ function entryValueEqual(a, b) {
205
+ if (a === b)
206
+ return true;
207
+ const aSpan = isSpanSelection(a);
208
+ if (aSpan !== isSpanSelection(b))
209
+ return false;
210
+ if (aSpan) {
211
+ const x = a;
212
+ const y = b;
213
+ if (x.id !== y.id)
214
+ return false;
215
+ if (x.x[0] !== y.x[0] || x.x[1] !== y.x[1])
216
+ return false;
217
+ if ((x.y === undefined) !== (y.y === undefined))
218
+ return false;
219
+ if (x.y && y.y && (x.y[0] !== y.y[0] || x.y[1] !== y.y[1]))
220
+ return false;
221
+ if ((x.rows === undefined) !== (y.rows === undefined))
222
+ return false;
223
+ if (x.rows && y.rows) {
224
+ if (x.rows.length !== y.rows.length)
225
+ return false;
226
+ for (let i = 0; i < x.rows.length; i += 1)
227
+ if (x.rows[i] !== y.rows[i])
228
+ return false;
229
+ }
230
+ return true;
231
+ }
232
+ const m = a;
233
+ const n = b;
234
+ return (m.id === n.id &&
235
+ Object.is(m.key, n.key) &&
236
+ Object.is(m.value, n.value) &&
237
+ m.label === n.label &&
238
+ m.mark === n.mark &&
239
+ m.color === n.color);
240
+ }
241
+ /** Value-equality for a controlled `selected` / `hovered`, over all three of
242
+ * its accepted shapes (a single mark, a set, or nothing). */
243
+ function controlledValueEqual(a, b) {
244
+ if (a === b)
245
+ return true;
246
+ if (a === null || a === undefined || b === null || b === undefined)
247
+ return false;
248
+ const aArr = Array.isArray(a);
249
+ if (aArr !== Array.isArray(b))
250
+ return false;
251
+ if (!aArr)
252
+ return entryValueEqual(a, b);
253
+ const x = a;
254
+ const y = b;
255
+ if (x.length !== y.length)
256
+ return false;
257
+ for (let i = 0; i < x.length; i += 1)
258
+ if (!entryValueEqual(x[i], y[i]))
259
+ return false;
260
+ return true;
261
+ }
262
+ /**
263
+ * **Value-equality for a registered selector — the guard that stops a
264
+ * controlled selection from looping.** `registerSelector` must no-op when the
265
+ * incoming entry is value-equal to the stored one, exactly as `registerAxis`
266
+ * does via `axisSpecEqual` (`ChartRow.tsx`), and for the same reason spelled
267
+ * out there: register → `setState` → re-render → register is a
268
+ * "Maximum update depth exceeded" spin.
269
+ *
270
+ * A10.3 made this guard load-bearing rather than defensive. The entry now
271
+ * carries the controlled *values*, and a consumer writing
272
+ * `selected={[hit]}` — or `selected={[{ id, key, … }]}` — mints a fresh
273
+ * reference every render. On its own that is survivable, because a
274
+ * container-only state update does not re-run the consumer's JSX. **But a
275
+ * descendant that consumes the container context and renders that inline
276
+ * array does re-run** — `useChartLegend()` is a supported example — so the
277
+ * chain became: registry update → new frame → context change → descendant
278
+ * re-render → fresh array → register → registry update, without end. Compare
279
+ * by value and the cycle closes on the first iteration. (Codex finding on
280
+ * #638; the reference-only guard it replaces was mine.)
281
+ *
282
+ * Cost: the common case is a stable array from `useState`, which hits the
283
+ * reference fast path. A fresh array costs one element-wise pass with no
284
+ * allocation, which is the right trade against an unbounded render loop.
285
+ */
286
+ export function selectorEntryEqual(a, b) {
287
+ if (a === b)
288
+ return true;
289
+ return (a.rowKey === b.rowKey &&
290
+ a.multi === b.multi &&
291
+ a.gestureEnabled === b.gestureEnabled &&
292
+ a.sequence === b.sequence &&
293
+ a.declaresSelected === b.declaresSelected &&
294
+ a.declaresHovered === b.declaresHovered &&
295
+ // The callbacks are stable wrappers over a ref, so their *presence* is
296
+ // what can change, and presence is what the entry is memoized on.
297
+ (a.onSelect === undefined) === (b.onSelect === undefined) &&
298
+ (a.onHover === undefined) === (b.onHover === undefined) &&
299
+ (a.onSelectMany === undefined) === (b.onSelectMany === undefined) &&
300
+ (a.onHoverMany === undefined) === (b.onHoverMany === undefined) &&
301
+ controlledValueEqual(a.selected, b.selected) &&
302
+ controlledValueEqual(a.hovered, b.hovered));
303
+ }
304
+ /**
305
+ * The selectors in effect for a row's GESTURE: the row's own mounts when it has
306
+ * any (the per-row scope — nearest mount wins, mirroring
307
+ * `effectiveCursorEntries`), else the container-scoped mounts. A
308
+ * `gestureEnabled: false` entry (`<Selector enabled={false}>`) is filtered out
309
+ * entirely — it behaves as unmounted for click/hover/sweep purposes, exactly
310
+ * as `enabled` promises.
311
+ *
312
+ * `rowKey` of `null` asks for the **container** scope only — the programmatic
313
+ * (legend) path, which belongs to no row.
314
+ *
315
+ * Controlled-state resolution does **not** use this function — state is
316
+ * chart-wide, not row-scoped, and a disabled selector may still own it. See
317
+ * {@link resolveControlledSelected} / {@link resolveControlledHovered}.
318
+ */
319
+ export function effectiveSelectorEntries(all, rowKey) {
320
+ const active = all.filter((e) => e.gestureEnabled);
321
+ if (rowKey !== null) {
322
+ const rowEntries = active.filter((e) => e.rowKey === rowKey);
323
+ if (rowEntries.length > 0)
324
+ return rowEntries;
325
+ }
326
+ return active.filter((e) => e.rowKey === null);
327
+ }
328
+ /** Fires once per container: more than one registered selector declared the
329
+ * same controlled state, which is ambiguous — the first registered wins and
330
+ * every other declaration is silently ignored until this is resolved. */
331
+ function warnAmbiguousControlledState(prop) {
332
+ console.warn(`[pond-charts] more than one mounted <Selector>/<MultiSelector> declares ` +
333
+ `\`${prop}\` in the same chart — only the first registered is used, the ` +
334
+ `rest are ignored. One selector should own \`${prop}\` per chart.`);
335
+ }
336
+ /**
337
+ * Resolve the chart-wide controlled `selected`, from whichever registered
338
+ * selector declared it. Not row-scoped — selection identity spans every row
339
+ * (a `SelectInfo.id` names a layer, not a row) — and not filtered on
340
+ * `gestureEnabled`: `<Selector enabled={false} selected={…}>` is exactly the
341
+ * "state, no gesture" configuration `enabled` exists for.
342
+ */
343
+ export function resolveControlledSelected(all, warned) {
344
+ const owner = pickControlledOwner(all, (e) => e.declaresSelected, warned, 'selected');
345
+ return owner
346
+ ? { present: true, value: owner.selected ?? null }
347
+ : { present: false, value: null };
348
+ }
349
+ /** As {@link resolveControlledSelected}, for `hovered`. */
350
+ export function resolveControlledHovered(all, warned) {
351
+ const owner = pickControlledOwner(all, (e) => e.declaresHovered, warned, 'hovered');
352
+ return owner
353
+ ? { present: true, value: owner.hovered ?? null }
354
+ : { present: false, value: null };
355
+ }
356
+ function pickControlledOwner(all, has, warned, prop) {
357
+ const owners = all.filter(has);
358
+ if (owners.length === 0)
359
+ return null;
360
+ if (isDev && owners.length > 1 && !warned.current) {
361
+ warned.current = true;
362
+ warnAmbiguousControlledState(prop);
363
+ }
364
+ return owners[0];
365
+ }
366
+ /**
367
+ * RFC §7.1's softening: a plot click resolved to a real mark and there was no
368
+ * `<Selector>` to tell — the exact path that goes silently inert on upgrade.
369
+ *
370
+ * **Suppressed when controlled `selected` is in effect** (A2.6) — that is the
371
+ * runtime signature of the *endorsed* controlled-highlight setup
372
+ * (`<Selector enabled={false} selected={…}>`, plot deliberately inert), and
373
+ * the warning should not spend its loudness on people already doing that.
374
+ *
375
+ * Not permanent: once mounting is the established model, an `id` without a
376
+ * `<Selector>` is a legitimate configuration (Q8) and warning on it forever
377
+ * would flag a supported setup. Fires once per container.
378
+ */
379
+ export function warnInertClick(warned) {
380
+ if (warned.current)
381
+ return;
382
+ warned.current = true;
383
+ console.warn('[pond-charts] a click hit a mark but no <Selector> is mounted (or the ' +
384
+ 'one in scope has `enabled={false}`), so nothing happened. Mount ' +
385
+ '`<Selector onSelect={…}>` wrapping the chart (or one <ChartRow> to ' +
386
+ 'scope it to that row) — click-select is no longer implied by giving a ' +
387
+ 'layer an `id`. See docs/rfcs/interaction.md §7.1. (Silent if ' +
388
+ 'controlled `selected` is in effect: controlled highlighting with an ' +
389
+ 'inert plot is a supported setup.)');
390
+ }
391
+ //# sourceMappingURL=selectors.js.map
package/dist/span.d.ts ADDED
@@ -0,0 +1,122 @@
1
+ import type { SelectInfo, SelectionEntry, SpanSelection } from './context.js';
2
+ /**
3
+ * Span-selection membership — the one place the containment rule lives
4
+ * (interaction RFC A5.2/A5.3, edge rule A7.6).
5
+ *
6
+ * `selectionContains` is the **public** predicate, and it is deliberately the
7
+ * same code every layer's draw runs per mark ({@link spanContainsPoint} for
8
+ * span entries): a consumer asking "is this hit already selected?" must get
9
+ * exactly the answer the canvas paints, or the boundary marks disagree with
10
+ * the span that swept them — the inverse-band-scale friction the marks
11
+ * currency was built to kill (`selection.md` A4.2) re-imported through the
12
+ * back door.
13
+ *
14
+ * Pure, DOM-free, theme-free — unit-tests like `select.ts` does.
15
+ */
16
+ /** Stable identity for "no spans" — the resting case, shared by the container
17
+ * normalization and every layer's narrowing, so a spanless render never hands
18
+ * a draw (or a memo dep) a fresh empty array. */
19
+ export declare const NO_SPANS: readonly SpanSelection[];
20
+ /**
21
+ * Is this `selected` entry a span descriptor rather than a single mark? The
22
+ * discriminant is the `kind` field, which {@link SelectInfo} does not have —
23
+ * useful to a consumer editing a mixed selection (RFC A5.2's demote-on-edit:
24
+ * filter the span out, splice in the marks it stashed at commit time).
25
+ */
26
+ export declare function isSpanSelection(entry: SelectionEntry): entry is SpanSelection;
27
+ /**
28
+ * Does `span` contain the mark with these channels? The containment rule of
29
+ * {@link SpanSelection}, minus the layer-`id` gate (the caller has already
30
+ * matched it — a layer narrows the set to its own `id` once per render, not
31
+ * once per mark):
32
+ *
33
+ * - `key` in the **half-open** `x` interval — `x[0] <= key < x[1]`;
34
+ * - `value` in the half-open `y` interval, when the span has one;
35
+ * - `label` a member of `rows`, when the span has one.
36
+ *
37
+ * Each channel is the mark's own {@link SelectInfo} field, so the test is
38
+ * answerable from a hit alone *and* from a draw loop's per-mark scalars — the
39
+ * property RFC A5.3 requires (nothing here reads a slot index or a pixel).
40
+ * `NaN` in any tested channel fails its comparison, so a series-scoped entry
41
+ * (`key: NaN`) or a gap value is never inside any span. A span carrying `rows`
42
+ * tested against a caller with no label channel (`label === undefined`)
43
+ * matches nothing — a row-set has to be checked, not skipped.
44
+ *
45
+ * O(1) per mark (plus O(|rows|) for the label set, which is a handful of row
46
+ * names) — the whole point of the descriptor: a span covering ten thousand
47
+ * marks costs each of them one interval test, not a ten-thousand-entry scan.
48
+ */
49
+ export declare function spanContainsPoint(span: SpanSelection, key: number, value: number, label: string | undefined): boolean;
50
+ /**
51
+ * Does **any** of `spans` contain the mark with these channels? The set form of
52
+ * {@link spanContainsPoint}, for the draw loops — `spans` is the layer's
53
+ * already-`id`-narrowed list (see {@link spansForLayer}), so this is pure
54
+ * channel tests. Linear over the spans for the reason `barMatchesAny` records
55
+ * about mark sets: a selection is a handful of entries, and the common cases
56
+ * (0 or 1) short-circuit.
57
+ */
58
+ export declare function spanMatchesAny(spans: readonly SpanSelection[], key: number, value: number, label?: string): boolean;
59
+ /**
60
+ * Narrow the container's span set to one layer — the span analog of the
61
+ * per-layer mark narrowing every chart component already does (`keysOf`,
62
+ * `marksOf`): drop the spans naming other layers, and — when the layer's marks
63
+ * all share **one** label (`label` given: a single-series bar, scatter or box,
64
+ * whose `SelectInfo.label` is the series label) — resolve the `rows` channel
65
+ * here, once, instead of per mark: a row set that excludes the constant label
66
+ * can never match and is dropped; one that includes it always matches and is
67
+ * stripped. Layers whose label varies per mark (a stack's groups, a heat map's
68
+ * rows) pass no `label` and keep `rows` for the draw to test per mark.
69
+ *
70
+ * Returns {@link NO_SPANS} when nothing survives, so the resting case keeps a
71
+ * stable identity (no re-registered layer, no repaint, when some *other*
72
+ * layer's spans change).
73
+ */
74
+ export declare function spansForLayer(spans: readonly SpanSelection[], id: string | undefined, label?: string): readonly SpanSelection[];
75
+ /**
76
+ * **Are these the same mark?** The full mark identity, matching the
77
+ * container's own hover dedup: same layer `id`, same per-mark handle — the
78
+ * stable `mark` when **both** sides carry one, else the sample `key` (the
79
+ * `barMatches` fallback rule) — and same `label`, which on a grouped layer
80
+ * (stack segment, heat cell) is the half of the identity that separates two
81
+ * marks sharing a bin. `NaN` keys never match (`NaN !== NaN`), so a
82
+ * series-scoped legend entry names no mark, deliberately.
83
+ *
84
+ * Exported as the companion to {@link selectionContains}, whose doc has
85
+ * always told a consumer to write `remove(cur, hit)` without giving them
86
+ * anything to write it with. **`key` alone is not identity**, and reaching
87
+ * for it is the natural mistake: it is right for a bar, a box and a candle,
88
+ * and on a stack or a heat map it silently takes out every mark in the bin.
89
+ * `<MultiSelector>`'s own demote-on-edit stories made exactly that error, in
90
+ * three places, in the file that is supposed to be the worked example —
91
+ * which is why this is a library export and not a docs note.
92
+ */
93
+ export declare function sameMark(a: SelectInfo, b: SelectInfo): boolean;
94
+ /**
95
+ * Is `hit` inside the selection — named by a mark entry, or covered by a span
96
+ * (interaction RFC A5.2)? **The same predicate the layers run**, exported so a
97
+ * consumer implementing click-policy over a mixed selection (toggle a mark out,
98
+ * ⌘-click-add next to a swept span) never re-implements the interval test in
99
+ * axis units — the exact friction `selection.md` A4.2's marks currency exists
100
+ * to eliminate.
101
+ *
102
+ * ```tsx
103
+ * onSelect={(hit, mods) =>
104
+ * setSelected((cur) =>
105
+ * hit === null ? []
106
+ * : mods?.additive
107
+ * ? selectionContains(cur, hit)
108
+ * ? cur.filter((e) => isSpanSelection(e) || !sameMark(e, hit))
109
+ * : [...cur, hit]
110
+ * : [hit],
111
+ * )
112
+ * }
113
+ * ```
114
+ *
115
+ * Span entries use {@link SpanSelection}'s containment rule (half-open `x`/`y`
116
+ * intervals on the hit's `key`/`value`, `rows` membership on its `label`);
117
+ * mark entries use the full mark identity (`id`, `mark`-or-`key`, `label`).
118
+ * Entries naming another layer's `id` never match. O(|sel|) with O(1) per
119
+ * entry, spans included.
120
+ */
121
+ export declare function selectionContains(sel: readonly SelectionEntry[], hit: SelectInfo): boolean;
122
+ //# sourceMappingURL=span.d.ts.map