@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.
- package/API.md +576 -0
- package/CHANGELOG.md +1213 -1
- package/dist/AreaChart.d.ts +12 -1
- package/dist/AreaChart.js +131 -13
- package/dist/BarChart.d.ts +56 -7
- package/dist/BarChart.js +263 -39
- package/dist/BarList.d.ts +85 -5
- package/dist/BarList.js +25 -4
- package/dist/BoxList.d.ts +70 -3
- package/dist/BoxList.js +21 -7
- package/dist/BoxPlot.d.ts +2 -1
- package/dist/BoxPlot.js +101 -9
- package/dist/Candlestick.d.ts +13 -1
- package/dist/Candlestick.js +89 -3
- package/dist/ChartContainer.d.ts +36 -48
- package/dist/ChartContainer.js +465 -59
- package/dist/ChartRow.d.ts +9 -2
- package/dist/ChartRow.js +176 -14
- package/dist/HeatMap.d.ts +176 -0
- package/dist/HeatMap.js +344 -0
- package/dist/Layers.d.ts +5 -1
- package/dist/Layers.js +1014 -253
- package/dist/Legend.js +8 -4
- package/dist/LineChart.d.ts +18 -1
- package/dist/LineChart.js +165 -4
- package/dist/ListTable.d.ts +30 -3
- package/dist/ListTable.js +381 -23
- package/dist/ScatterChart.d.ts +3 -2
- package/dist/ScatterChart.js +68 -4
- package/dist/XAxis.js +40 -22
- package/dist/YAxis.d.ts +58 -2
- package/dist/YAxis.js +3 -1
- package/dist/area.d.ts +34 -1
- package/dist/area.js +88 -1
- package/dist/bars.d.ts +67 -6
- package/dist/bars.js +250 -35
- package/dist/box.d.ts +2 -2
- package/dist/box.js +158 -40
- package/dist/brush.d.ts +142 -0
- package/dist/brush.js +179 -0
- package/dist/child-index.d.ts +27 -0
- package/dist/child-index.js +57 -0
- package/dist/context.d.ts +870 -39
- package/dist/cursors.d.ts +161 -0
- package/dist/cursors.js +503 -0
- package/dist/data.d.ts +38 -0
- package/dist/data.js +43 -0
- package/dist/decimate.d.ts +78 -1
- package/dist/decimate.js +157 -0
- package/dist/format.d.ts +15 -0
- package/dist/format.js +16 -1
- package/dist/heat.d.ts +163 -0
- package/dist/heat.js +659 -0
- package/dist/index.d.ts +13 -4
- package/dist/index.js +27 -0
- package/dist/line.d.ts +137 -0
- package/dist/line.js +328 -0
- package/dist/ohlc.d.ts +16 -1
- package/dist/ohlc.js +93 -4
- package/dist/range.d.ts +14 -1
- package/dist/range.js +24 -3
- package/dist/scatter.d.ts +17 -9
- package/dist/scatter.js +221 -33
- package/dist/select.d.ts +13 -5
- package/dist/select.js +14 -6
- package/dist/selection-fixtures.d.ts +174 -0
- package/dist/selection-fixtures.js +569 -0
- package/dist/selection-stories.d.ts +73 -0
- package/dist/selection-stories.js +301 -0
- package/dist/selectors.d.ts +316 -0
- package/dist/selectors.js +391 -0
- package/dist/span.d.ts +122 -0
- package/dist/span.js +203 -0
- package/dist/sweep.d.ts +154 -0
- package/dist/sweep.js +282 -0
- package/dist/theme.d.ts +510 -5
- package/dist/theme.js +217 -41
- package/dist/tracker.d.ts +6 -0
- package/dist/tracker.js +6 -0
- package/dist/tradingAxis.fixture.d.ts +78 -0
- package/dist/tradingAxis.fixture.js +215 -0
- package/dist/useChartLegend.js +18 -3
- package/dist/yticks.d.ts +3 -0
- package/dist/yticks.js +104 -0
- 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
|