@pond-ts/charts 0.57.0 → 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.
- package/CHANGELOG.md +1070 -1
- package/dist/AreaChart.d.ts +12 -1
- package/dist/AreaChart.js +131 -13
- package/dist/BarChart.js +184 -30
- 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 +86 -12
- 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/area.d.ts +34 -1
- package/dist/area.js +88 -1
- package/dist/bars.d.ts +57 -3
- package/dist/bars.js +237 -26
- 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 +859 -33
- package/dist/cursors.d.ts +161 -0
- package/dist/cursors.js +503 -0
- package/dist/decimate.d.ts +78 -1
- package/dist/decimate.js +157 -0
- package/dist/heat.d.ts +163 -0
- package/dist/heat.js +659 -0
- package/dist/index.d.ts +11 -2
- package/dist/index.js +22 -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/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 +456 -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/package.json +3 -3
package/dist/ChartContainer.js
CHANGED
|
@@ -6,6 +6,9 @@ import { scaleBand } from './bandScale.js';
|
|
|
6
6
|
import { scaleElapsed } from './elapsed.js';
|
|
7
7
|
import { Sequence } from 'pond-ts';
|
|
8
8
|
import { ContainerContext, CursorContext, } from './context.js';
|
|
9
|
+
import { LegacyCursor, legacyCursorWarning, presetNameFor, warnOnDuplicateGestureOwners, } from './cursors.js';
|
|
10
|
+
import { effectiveSelectorEntries, resolveControlledHovered, resolveControlledSelected, selectorEntryEqual, warnInertClick, } from './selectors.js';
|
|
11
|
+
import { isDev } from './dev.js';
|
|
9
12
|
import { maxSlotWidths, sum } from './slots.js';
|
|
10
13
|
import { computeLabelLanes } from './annotations.js';
|
|
11
14
|
import { resolveCursorX, DEFAULT_CURSOR_MODE } from './tracker.js';
|
|
@@ -13,6 +16,11 @@ import { clampToBounds } from './viewport.js';
|
|
|
13
16
|
import { resolveAxisFormat, resolveTimeFormat, } from './format.js';
|
|
14
17
|
import { TimeAxis } from './TimeAxis.js';
|
|
15
18
|
import { defaultTheme } from './theme.js';
|
|
19
|
+
import { isSpanSelection, NO_SPANS } from './span.js';
|
|
20
|
+
/** Stable identity for "nothing selected" — see the normalization below. */
|
|
21
|
+
const EMPTY_SELECTION = [];
|
|
22
|
+
/** Stable "no sweep in flight" identity for the span preview channel. */
|
|
23
|
+
const EMPTY_PREVIEW_SPANS = [];
|
|
16
24
|
/** Tick count for a **continuous** (non-trading) x axis — the `ticks(count)`
|
|
17
25
|
* request `<TimeAxis>`, the x gridlines, and the cursor-time formatter share
|
|
18
26
|
* (as the frame's `xTickCount`). */
|
|
@@ -45,14 +53,117 @@ function normalizeRange(range) {
|
|
|
45
53
|
* {@link TimeAxis} at the bottom, aligned under the plots. Y axes are per-row
|
|
46
54
|
* (`<YAxis>`).
|
|
47
55
|
*/
|
|
48
|
-
export function ChartContainer({ range, maxBandWidth, bandAlign = 'start', width, rowGap = 0, showAxis = true, trackerPosition, onTrackerChanged, onDrawStats,
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
+
export function ChartContainer({ range, maxBandWidth, bandAlign = 'start', width, rowGap = 0, showAxis = true, trackerPosition, onTrackerChanged, onDrawStats, panZoom = false, bounds, onTimeRangeChange, minDuration = 1, cursor: cursorProp, cursorSequence: cursorSequenceProp, onRegionSelect, regionSelectModifier, cursorTime: cursorTimeProp, crosshairSnap: crosshairSnapProp, editAnnotations = false, creating = null, onCreate, onSelectAnnotation, onHoverAnnotation, onEditAnnotation, snap = true, timeFormat, cursorFormat: cursorFormatProp, origin, theme, discontinuities, calendar, spacing, grid = true, sessionDividers = 'none', children, }) {
|
|
57
|
+
// ── Legacy cursor props (deprecated) ───────────────────────────────────────
|
|
58
|
+
// The string surface keeps working for one minor: the resolved mode is
|
|
59
|
+
// synthesized into the equivalent mounted preset below (`<LegacyCursor>`),
|
|
60
|
+
// and a dev warning names the replacement whenever any of the props is
|
|
61
|
+
// *explicitly* set (never on the defaults). Mounted cursor components in the
|
|
62
|
+
// same scope override the shim. See docs/rfcs/interaction.md §9 / A4.4.
|
|
63
|
+
const cursor = cursorProp ?? DEFAULT_CURSOR_MODE;
|
|
64
|
+
const cursorTime = cursorTimeProp ?? false;
|
|
65
|
+
const crosshairSnap = crosshairSnapProp ?? true;
|
|
66
|
+
const warnedLegacyRef = useRef(false);
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (!isDev || warnedLegacyRef.current)
|
|
69
|
+
return;
|
|
70
|
+
const legacy = [];
|
|
71
|
+
if (cursorProp !== undefined)
|
|
72
|
+
legacy.push(`cursor="${cursorProp}" → mount ${presetNameFor(cursorProp)}`);
|
|
73
|
+
if (crosshairSnapProp !== undefined)
|
|
74
|
+
legacy.push('crosshairSnap → <CrosshairCursor snap>');
|
|
75
|
+
if (cursorTimeProp !== undefined)
|
|
76
|
+
legacy.push('cursorTime → showTime on the mounted cursor');
|
|
77
|
+
if (cursorFormatProp !== undefined)
|
|
78
|
+
legacy.push('cursorFormat → format on <CrosshairCursor>');
|
|
79
|
+
if (cursorSequenceProp !== undefined)
|
|
80
|
+
legacy.push('cursorSequence → <RangeCursor sequence>');
|
|
81
|
+
if (onRegionSelect !== undefined)
|
|
82
|
+
legacy.push('onRegionSelect → <RangeCursor onDragRelease> (the payload becomes ' +
|
|
83
|
+
'{ x: [lo, hi] })');
|
|
84
|
+
if (regionSelectModifier !== undefined)
|
|
85
|
+
legacy.push('regionSelectModifier → <RangeCursor dragModifier>');
|
|
86
|
+
if (legacy.length === 0)
|
|
87
|
+
return;
|
|
88
|
+
warnedLegacyRef.current = true;
|
|
89
|
+
console.warn(legacyCursorWarning(legacy));
|
|
90
|
+
}, [
|
|
91
|
+
cursorProp,
|
|
92
|
+
crosshairSnapProp,
|
|
93
|
+
cursorTimeProp,
|
|
94
|
+
cursorFormatProp,
|
|
95
|
+
cursorSequenceProp,
|
|
96
|
+
onRegionSelect,
|
|
97
|
+
regionSelectModifier,
|
|
98
|
+
]);
|
|
99
|
+
// Mounted-cursor registry ({@link ContainerFrame.registerCursor}): the
|
|
100
|
+
// presets (and the legacy shim) register their specs here; rows and
|
|
101
|
+
// `<XAxis>` render the effective set. Same per-instance-slot discipline as
|
|
102
|
+
// the tracker sources; register is idempotent under reference equality (the
|
|
103
|
+
// presets memoize their entries on props).
|
|
104
|
+
const [cursorMap, setCursorMap] = useState(() => new Map());
|
|
105
|
+
const registerCursor = useCallback((key, entry) => {
|
|
106
|
+
setCursorMap((m) => m.get(key) === entry ? m : new Map(m).set(key, entry));
|
|
107
|
+
}, []);
|
|
108
|
+
const unregisterCursor = useCallback((key) => {
|
|
109
|
+
setCursorMap((m) => {
|
|
110
|
+
if (!m.has(key))
|
|
111
|
+
return m;
|
|
112
|
+
const next = new Map(m);
|
|
113
|
+
next.delete(key);
|
|
114
|
+
return next;
|
|
115
|
+
});
|
|
116
|
+
}, []);
|
|
117
|
+
const cursors = useMemo(() => Array.from(cursorMap.values()), [cursorMap]);
|
|
118
|
+
// RFC A2.5: one snap/gesture owner per scope — warn (dev, once) on two.
|
|
119
|
+
const warnedGestureRef = useRef(false);
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
warnOnDuplicateGestureOwners(cursors, warnedGestureRef);
|
|
122
|
+
}, [cursors]);
|
|
123
|
+
// The registered cursors' resolution inputs, folded into the legacy
|
|
124
|
+
// channels: a mounted `<CrosshairCursor format>` feeds the shared readout
|
|
125
|
+
// channel exactly where `cursorFormat` fed it. First **component-mounted**
|
|
126
|
+
// entry wins (the shim registers before the children mount, so a bare
|
|
127
|
+
// first-wins would let the legacy synthesis shadow a real mount); the
|
|
128
|
+
// legacy prop is the fallback during the window. (`sequence` resolves the
|
|
129
|
+
// same way, below the selector registry — a `<MultiSelector sequence>`
|
|
130
|
+
// feeds the same channel.)
|
|
131
|
+
const cursorFormat = useMemo(() => cursors.find((e) => !e.legacy && e.format !== undefined)?.format ??
|
|
132
|
+
cursorFormatProp, [cursors, cursorFormatProp]);
|
|
133
|
+
// Which axes the gestures own. **Pan follows zoom's degrees of freedom**: an
|
|
134
|
+
// axis that can be zoomed can be panned, because a zoomed axis shows less than
|
|
135
|
+
// all of itself and the reader needs to reach the rest. `'pan'` is the one
|
|
136
|
+
// exception — pan with no zoom — and it stays x-only, which is what it has
|
|
137
|
+
// always meant. `'panZoom'` and `true` are `'panZoomX'`: the original
|
|
138
|
+
// behaviour, now named for the axis it acts on.
|
|
139
|
+
const zoomX = panZoom === true ||
|
|
140
|
+
panZoom === 'panZoom' ||
|
|
141
|
+
panZoom === 'panZoomX' ||
|
|
142
|
+
panZoom === 'panZoomXY';
|
|
143
|
+
const zoomY = panZoom === 'panZoomY' || panZoom === 'panZoomXY';
|
|
144
|
+
const panX = zoomX || panZoom === 'pan';
|
|
145
|
+
const panY = zoomY;
|
|
146
|
+
const panEnabled = panX || panY;
|
|
147
|
+
const zoomEnabled = zoomX || zoomY;
|
|
148
|
+
// **The aspect ratio is fixed only when BOTH axes zoom** — one factor about
|
|
149
|
+
// the cursor, so a feature that looked square stays square. A single-axis zoom
|
|
150
|
+
// necessarily changes the ratio, which is the whole point of asking for one.
|
|
151
|
+
//
|
|
152
|
+
// This is why the modes name their axes. An earlier cut had a single
|
|
153
|
+
// `panZoom2D` that claimed both and then silently fell back to y-only wherever
|
|
154
|
+
// x was a category axis: the ratio changed and nothing said so. Spelling out
|
|
155
|
+
// the axes makes that the caller's choice instead of a hidden one.
|
|
156
|
+
const aspectLocked = zoomX && zoomY;
|
|
157
|
+
const [yTransform, setYTransform] = useState({
|
|
158
|
+
k: 1,
|
|
159
|
+
ty: 0,
|
|
160
|
+
});
|
|
161
|
+
const applyYTransform = useCallback((next) => {
|
|
162
|
+
// `k < 1` would zoom out past the axis' natural fit, leaving blank bands the
|
|
163
|
+
// reader cannot interpret; clamping at 1 makes the un-zoomed view the floor.
|
|
164
|
+
const k = Math.max(1, next.k);
|
|
165
|
+
setYTransform((prev) => prev.k === k && prev.ty === next.ty ? prev : { k, ty: next.ty });
|
|
166
|
+
}, []);
|
|
56
167
|
const interactive = panEnabled || zoomEnabled;
|
|
57
168
|
// The explicit base domain from `range` (a tuple or a TimeRange). `undefined`
|
|
58
169
|
// ⇒ auto-fit (resolved from the layers below). Pan/zoom seeds from it; `seed`
|
|
@@ -109,6 +220,11 @@ export function ChartContainer({ range, maxBandWidth, bandAlign = 'start', width
|
|
|
109
220
|
const [hoverX, setHoverX] = useState(null);
|
|
110
221
|
// The region-cursor drag anchor (epoch ms) — set on press, cleared on release.
|
|
111
222
|
const [regionAnchor, setRegionAnchor] = useState(null);
|
|
223
|
+
// The live sweep preview for span-only layers ([PND-TRACESEL]) — a paint
|
|
224
|
+
// mirror beside `regionAnchor`, never part of the committed selection.
|
|
225
|
+
// `EMPTY_PREVIEW_SPANS` keeps the at-rest case reference-stable so a
|
|
226
|
+
// pointer move over an unswept plot re-identifies no layer entry.
|
|
227
|
+
const [previewSpans, setPreviewSpans] = useState(EMPTY_PREVIEW_SPANS);
|
|
112
228
|
// The free-form crosshair also needs the pointer's y + which row (row-specific,
|
|
113
229
|
// unlike the shared x). One state object so a move updates both atomically.
|
|
114
230
|
const [hoverPoint, setHoverPoint] = useState(null);
|
|
@@ -154,6 +270,48 @@ export function ChartContainer({ range, maxBandWidth, bandAlign = 'start', width
|
|
|
154
270
|
selectableRef.current = next;
|
|
155
271
|
setSelectableKeys(next);
|
|
156
272
|
}, []);
|
|
273
|
+
// Mounted-selector registry ({@link ContainerFrame.registerSelector}):
|
|
274
|
+
// `<Selector>` — and the legacy shim below — register here, and **the
|
|
275
|
+
// registration is what enables a plot click** (interaction RFC §7.1). Same
|
|
276
|
+
// per-instance-slot discipline as the cursors/tracker sources. Mirrored to a
|
|
277
|
+
// ref because `select` / `setHovered` are `[]`-stable callbacks that must not
|
|
278
|
+
// read a stale closure (the `onSelectRef` discipline, one level up).
|
|
279
|
+
const [selectorMap, setSelectorMap] = useState(() => new Map());
|
|
280
|
+
const registerSelector = useCallback((key, entry) => {
|
|
281
|
+
setSelectorMap((m) => {
|
|
282
|
+
// **Value-equal, not reference-equal.** Since A10.3 the entry carries the
|
|
283
|
+
// controlled `selected`/`hovered`, so a consumer's inline array mints a
|
|
284
|
+
// fresh entry every render; a reference-only guard then updates the
|
|
285
|
+
// registry every time, and any descendant that reads the container
|
|
286
|
+
// context (`useChartLegend()`) re-renders, rebuilds the array, and
|
|
287
|
+
// re-registers — an unbounded loop. Same guard, same reason, as
|
|
288
|
+
// `registerAxis`/`axisSpecEqual` in `ChartRow.tsx`.
|
|
289
|
+
const prev = m.get(key);
|
|
290
|
+
if (prev !== undefined && selectorEntryEqual(prev, entry))
|
|
291
|
+
return m;
|
|
292
|
+
return new Map(m).set(key, entry);
|
|
293
|
+
});
|
|
294
|
+
}, []);
|
|
295
|
+
const unregisterSelector = useCallback((key) => {
|
|
296
|
+
setSelectorMap((m) => {
|
|
297
|
+
if (!m.has(key))
|
|
298
|
+
return m;
|
|
299
|
+
const next = new Map(m);
|
|
300
|
+
next.delete(key);
|
|
301
|
+
return next;
|
|
302
|
+
});
|
|
303
|
+
}, []);
|
|
304
|
+
const selectors = useMemo(() => Array.from(selectorMap.values()), [selectorMap]);
|
|
305
|
+
const selectorsRef = useRef(selectors);
|
|
306
|
+
// The shared snap-bucket sequence, folded into the legacy channel exactly as
|
|
307
|
+
// `cursorFormat` is above: a component-mounted `<RangeCursor sequence>`
|
|
308
|
+
// wins, then a mounted `<MultiSelector sequence>` (its sweep extends bucket
|
|
309
|
+
// by bucket over the same realized buckets — one channel, so the band and
|
|
310
|
+
// the sweep can never snap differently), then the legacy shim / prop.
|
|
311
|
+
const cursorSequence = useMemo(() => cursors.find((e) => !e.legacy && e.sequence !== undefined)?.sequence ??
|
|
312
|
+
selectors.find((e) => e.sequence !== undefined)?.sequence ??
|
|
313
|
+
cursors.find((e) => e.sequence !== undefined)?.sequence ??
|
|
314
|
+
cursorSequenceProp, [cursors, selectors, cursorSequenceProp]);
|
|
157
315
|
// Annotations register here so the container can do what a mark can't in
|
|
158
316
|
// isolation: draw its guide line across other rows, order regions, serve snap
|
|
159
317
|
// targets. Keyed by per-instance slot key (same discipline as the sources).
|
|
@@ -252,43 +410,147 @@ export function ChartContainer({ range, maxBandWidth, bandAlign = 'start', width
|
|
|
252
410
|
const reportDrawStats = useMemo(() => hasDrawStats
|
|
253
411
|
? (frame) => onDrawStatsRef.current?.(frame)
|
|
254
412
|
: undefined, [hasDrawStats]);
|
|
255
|
-
// Selection: controlled (`selected`
|
|
256
|
-
// on a selectable layer calls `select()` after
|
|
257
|
-
//
|
|
413
|
+
// Selection: controlled (a mounted selector's `selected`) or uncontrolled
|
|
414
|
+
// (internal). A click on a selectable layer calls `select()` after
|
|
415
|
+
// hit-testing; the mounted `<Selector>`s in scope are notified in both
|
|
416
|
+
// modes, and the internal state is managed only when uncontrolled — whether
|
|
417
|
+
// *this* container's derived state is controlled now depends on the
|
|
418
|
+
// registry, not a container prop (interaction RFC A10.3). The full
|
|
258
419
|
// SelectInfo is the identity (key + series), so multi-series marks at one
|
|
259
420
|
// timestamp stay distinct. Refs written after commit (not in render) so the
|
|
260
421
|
// click handler never reads a callback / mode from a frame abandoned under
|
|
261
422
|
// concurrent rendering.
|
|
423
|
+
// Widened past a single mark for the sweep (RFC A5.2): an uncontrolled
|
|
424
|
+
// `<MultiSelector>` release commits its compact span descriptor here, so the
|
|
425
|
+
// swept bars stay lit with no controlled prop — the sweep analog of the
|
|
426
|
+
// uncontrolled click highlight. Clicks still store the single hit.
|
|
262
427
|
const [internalSelected, setInternalSelected] = useState(null);
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
428
|
+
// Controlled selection now comes from the registry (interaction RFC A10.3):
|
|
429
|
+
// whichever mounted `<Selector>`/`<MultiSelector>` declared `selected` owns
|
|
430
|
+
// it — chart-wide, not row-scoped, and independent of `gestureEnabled`
|
|
431
|
+
// (`<Selector enabled={false} selected={…}>` is a legitimate owner). Warned
|
|
432
|
+
// once if more than one mount declares it.
|
|
433
|
+
const warnedAmbiguousSelectedRef = useRef(false);
|
|
434
|
+
const controlledSelected = useMemo(() => resolveControlledSelected(selectors, warnedAmbiguousSelectedRef), [selectors]);
|
|
435
|
+
const controlledSelection = controlledSelected.present;
|
|
436
|
+
// Normalize the three accepted shapes — a single mark, a set, or nothing —
|
|
437
|
+
// into the shapes the frame carries ([PND-MULTISEL] / RFC A5.2). A mixed
|
|
438
|
+
// `SelectionEntry` array is split ONCE here into its mark entries
|
|
439
|
+
// (`selected`, the exact field every pre-span reader keeps consuming
|
|
440
|
+
// unchanged) and its span descriptors (`selectedSpans`, which only the
|
|
441
|
+
// span-aware layers read) — so a consumer who never passes a span pays
|
|
442
|
+
// nothing anywhere: the marks array is the prop's own array (no copy), the
|
|
443
|
+
// spans field is the module constant, and every downstream `length === 0`
|
|
444
|
+
// gate short-circuits as before. `EMPTY_SELECTION` / `NO_SPANS` are module
|
|
445
|
+
// constants so the common cases keep stable identities and don't
|
|
446
|
+
// re-identify the frame on every render.
|
|
447
|
+
const { selectedValue, selectedSpans } = useMemo(() => {
|
|
448
|
+
const raw = controlledSelection
|
|
449
|
+
? controlledSelected.value
|
|
450
|
+
: internalSelected;
|
|
451
|
+
if (raw === null || raw === undefined)
|
|
452
|
+
return { selectedValue: EMPTY_SELECTION, selectedSpans: NO_SPANS };
|
|
453
|
+
if (!Array.isArray(raw))
|
|
454
|
+
return {
|
|
455
|
+
selectedValue: [raw],
|
|
456
|
+
selectedSpans: NO_SPANS,
|
|
457
|
+
};
|
|
458
|
+
const entries = raw;
|
|
459
|
+
// The overwhelmingly common case — no span entries — returns the caller's
|
|
460
|
+
// array as-is rather than partitioning into fresh ones.
|
|
461
|
+
let spanCount = 0;
|
|
462
|
+
for (let i = 0; i < entries.length; i += 1) {
|
|
463
|
+
if (isSpanSelection(entries[i]))
|
|
464
|
+
spanCount += 1;
|
|
465
|
+
}
|
|
466
|
+
if (spanCount === 0)
|
|
467
|
+
return {
|
|
468
|
+
selectedValue: entries,
|
|
469
|
+
selectedSpans: NO_SPANS,
|
|
470
|
+
};
|
|
471
|
+
const marks = [];
|
|
472
|
+
const spans = [];
|
|
473
|
+
for (let i = 0; i < entries.length; i += 1) {
|
|
474
|
+
const e = entries[i];
|
|
475
|
+
if (isSpanSelection(e))
|
|
476
|
+
spans.push(e);
|
|
477
|
+
else
|
|
478
|
+
marks.push(e);
|
|
479
|
+
}
|
|
480
|
+
return {
|
|
481
|
+
selectedValue: marks.length === 0 ? EMPTY_SELECTION : marks,
|
|
482
|
+
selectedSpans: spans,
|
|
483
|
+
};
|
|
484
|
+
}, [controlledSelection, controlledSelected.value, internalSelected]);
|
|
268
485
|
const controlledSelectionRef = useRef(controlledSelection);
|
|
269
486
|
useLayoutEffect(() => {
|
|
270
|
-
|
|
487
|
+
selectorsRef.current = selectors;
|
|
271
488
|
controlledSelectionRef.current = controlledSelection;
|
|
272
489
|
});
|
|
273
|
-
|
|
274
|
-
|
|
490
|
+
// The library applies no set arithmetic: it reports the hit plus the
|
|
491
|
+
// modifiers and, when uncontrolled, keeps the single-mark behaviour it always
|
|
492
|
+
// had. A consumer wanting add/toggle reads `modifiers.additive` and drives
|
|
493
|
+
// the controlled `selected` set — see `SelectModifiers`.
|
|
494
|
+
//
|
|
495
|
+
// `rowKey` present ⇒ a **plot gesture**, and RFC §7.1's gate applies: with no
|
|
496
|
+
// `<Selector>` in scope the click does nothing at all — it does not even
|
|
497
|
+
// commit the uncontrolled selection, which is the whole point (a chart that
|
|
498
|
+
// silently highlighted on click now doesn't). Absent ⇒ a programmatic select
|
|
499
|
+
// (a `<Legend>` chip), which is intentional by construction and stays
|
|
500
|
+
// ungated.
|
|
501
|
+
const warnedInertClickRef = useRef(false);
|
|
502
|
+
const select = useCallback((hit, modifiers, rowKey) => {
|
|
503
|
+
const entries = effectiveSelectorEntries(selectorsRef.current, rowKey ?? null);
|
|
504
|
+
if (rowKey !== undefined && entries.length === 0) {
|
|
505
|
+
// A2.6: suppress when controlled `selected` is in effect — that is
|
|
506
|
+
// the signature of the *endorsed* controlled-highlight setup
|
|
507
|
+
// (`<Selector enabled={false} selected={…}>`), not of a consumer who
|
|
508
|
+
// lost their click.
|
|
509
|
+
if (isDev && hit !== null && !controlledSelectionRef.current)
|
|
510
|
+
warnInertClick(warnedInertClickRef);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
// Pass the second argument only when there is one. Calling
|
|
514
|
+
// `onSelect(hit, undefined)` unconditionally would change the observed
|
|
515
|
+
// arity for every existing consumer — enough to break a
|
|
516
|
+
// `toHaveBeenCalledWith(hit)` assertion, which is a silly thing to break
|
|
517
|
+
// for a purely additive feature.
|
|
518
|
+
for (const e of entries) {
|
|
519
|
+
if (modifiers === undefined)
|
|
520
|
+
e.onSelect?.(hit);
|
|
521
|
+
else
|
|
522
|
+
e.onSelect?.(hit, modifiers);
|
|
523
|
+
// A mounted <MultiSelector> hears the same click in its own currency
|
|
524
|
+
// (RFC §8: everything <Selector> does): 0/1 hits, no span — a click
|
|
525
|
+
// produces marks, only a sweep produces a span (A5.2).
|
|
526
|
+
e.onSelectMany?.(hit === null ? EMPTY_SELECTION : [hit], modifiers, EMPTY_PREVIEW_SPANS);
|
|
527
|
+
}
|
|
275
528
|
if (!controlledSelectionRef.current)
|
|
276
529
|
setInternalSelected(hit);
|
|
277
530
|
}, []);
|
|
278
|
-
// Dev-warn: selection is wired (`selected
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
|
|
531
|
+
// Dev-warn: selection is wired (a controlled `selected`, or a mounted
|
|
532
|
+
// `<Selector>`/`<MultiSelector>` at all — mounting is itself wiring, even
|
|
533
|
+
// with no callbacks) but no layer carries an `id`, so nothing is
|
|
534
|
+
// selectable — `id` gates interactivity, so a consumer who forgot it gets a
|
|
535
|
+
// silent no-op click without this nudge. Fires once per wired-but-empty
|
|
536
|
+
// transition (guarded by a ref); child layers register before this parent
|
|
537
|
+
// effect runs, so the set is settled here.
|
|
538
|
+
// "Wired" means a gesture is armed, or state is being driven. A selector
|
|
539
|
+
// whose gesture is off AND which declares no state is wired to nothing — it
|
|
540
|
+
// would be a strange thing to mount, but accusing it of a missing `id` is a
|
|
541
|
+
// false positive, so it doesn't count (reviewer finding on #638).
|
|
542
|
+
const selectionWired = controlledSelection ||
|
|
543
|
+
selectors.some((e) => e.gestureEnabled || e.declaresSelected || e.declaresHovered);
|
|
284
544
|
const warnedNoSelectableRef = useRef(false);
|
|
285
545
|
useEffect(() => {
|
|
286
546
|
if (selectionWired && selectableRef.current.size === 0) {
|
|
287
547
|
if (!warnedNoSelectableRef.current) {
|
|
288
548
|
warnedNoSelectableRef.current = true;
|
|
289
|
-
console.warn('[pond-charts]
|
|
290
|
-
'nothing is selectable. Give
|
|
291
|
-
'
|
|
549
|
+
console.warn('[pond-charts] a <Selector>/<MultiSelector> is mounted (or declares ' +
|
|
550
|
+
'`selected`) but no layer has an `id` — nothing is selectable. Give ' +
|
|
551
|
+
'a <BarChart>/<ScatterChart>/<BoxPlot>/<HeatMap>/<LineChart>/' +
|
|
552
|
+
'<AreaChart> an `id` to make it interactive (an `id` gates ' +
|
|
553
|
+
'selection + hover).');
|
|
292
554
|
}
|
|
293
555
|
}
|
|
294
556
|
else {
|
|
@@ -296,37 +558,140 @@ export function ChartContainer({ range, maxBandWidth, bandAlign = 'start', width
|
|
|
296
558
|
}
|
|
297
559
|
}, [selectionWired, selectableKeys]);
|
|
298
560
|
// Hover-highlight: the transient mark under the pointer (distinct from the
|
|
299
|
-
// committed selection). Controlled (`hovered`
|
|
300
|
-
// mirroring selection; `onHover` notifies in both
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
// cursor, which never touches the
|
|
561
|
+
// committed selection). Controlled (a mounted selector's `hovered`) or
|
|
562
|
+
// uncontrolled (internal), mirroring selection; `onHover` notifies in both
|
|
563
|
+
// modes. Deduped by the mark's full identity so it fires — and the data
|
|
564
|
+
// canvas repaints — only when the hovered mark changes, not on every pointer
|
|
565
|
+
// move (the move itself just slides the SVG cursor, which never touches the
|
|
566
|
+
// data canvas).
|
|
567
|
+
//
|
|
568
|
+
// "Full identity" means `label` and `mark` as well as `id` and `key`, and that
|
|
569
|
+
// is load-bearing rather than belt-and-braces. `key` is the mark's position on
|
|
570
|
+
// the **bin axis**, so any layer that stacks more than one mark in a bin — a
|
|
571
|
+
// stacked bar, a `<HeatMap>` column — has several marks sharing a key, and
|
|
572
|
+
// deduping on `id + key` alone silently swallows every move *within* a bin.
|
|
573
|
+
// On a heat map that reads as the hover being stuck: dragging straight down a
|
|
574
|
+
// column never changes the reported cell.
|
|
575
|
+
// Widened past a single mark for the sweep's live preview (RFC A1.4/A3.4):
|
|
576
|
+
// a drag under a mounted <MultiSelector> lights every covered mark at once
|
|
577
|
+
// through this same field — the reason `hovered` became a set.
|
|
304
578
|
const [internalHovered, setInternalHovered] = useState(null);
|
|
305
|
-
const
|
|
306
|
-
const
|
|
307
|
-
const
|
|
579
|
+
const warnedAmbiguousHoveredRef = useRef(false);
|
|
580
|
+
const controlledHovered = useMemo(() => resolveControlledHovered(selectors, warnedAmbiguousHoveredRef), [selectors]);
|
|
581
|
+
const controlledHover = controlledHovered.present;
|
|
582
|
+
// Same three-shape normalization `selected` does — a single mark, a set, or
|
|
583
|
+
// nothing — so a pointer-driven hover (always one mark) and a sweep-driven
|
|
584
|
+
// one (several) reach the draw paths in the same shape (RFC A4.2).
|
|
585
|
+
const hoveredValue = useMemo(() => {
|
|
586
|
+
const raw = controlledHover ? controlledHovered.value : internalHovered;
|
|
587
|
+
if (raw === null || raw === undefined)
|
|
588
|
+
return EMPTY_SELECTION;
|
|
589
|
+
return Array.isArray(raw) ? raw : [raw];
|
|
590
|
+
}, [controlledHover, controlledHovered.value, internalHovered]);
|
|
308
591
|
const controlledHoverRef = useRef(controlledHover);
|
|
309
592
|
// The last mark we reported — so the callback dedups across pointer moves even
|
|
310
593
|
// in controlled mode, where there's no internal state to compare against.
|
|
311
594
|
const lastHoverRef = useRef(null);
|
|
312
595
|
useLayoutEffect(() => {
|
|
313
|
-
onHoverRef.current = onHover;
|
|
314
596
|
controlledHoverRef.current = controlledHover;
|
|
315
597
|
});
|
|
316
|
-
|
|
598
|
+
// The last resting BLOCK we reported (identity — the row keeps the array
|
|
599
|
+
// reference-stable per block), so <MultiSelector onHover> hears one call per
|
|
600
|
+
// block transition and within-block mark transitions re-render nothing.
|
|
601
|
+
const lastHoverBlockRef = useRef(null);
|
|
602
|
+
// Unlike `select`, this is **not** gated on a mounted `<Selector>`: the
|
|
603
|
+
// hover state moves regardless, uncontrolled with none mounted; with none
|
|
604
|
+
// mounted there is simply no `onHover` to fire.
|
|
605
|
+
const setHovered = useCallback((hit, rowKey, block) => {
|
|
317
606
|
const prev = lastHoverRef.current;
|
|
318
|
-
const
|
|
607
|
+
const sameMark = prev === hit ||
|
|
319
608
|
(prev !== null &&
|
|
320
609
|
hit !== null &&
|
|
321
610
|
prev.id === hit.id &&
|
|
322
|
-
prev.key === hit.key
|
|
323
|
-
|
|
611
|
+
prev.key === hit.key &&
|
|
612
|
+
prev.label === hit.label &&
|
|
613
|
+
prev.mark === hit.mark);
|
|
614
|
+
const blockNow = block ?? null;
|
|
615
|
+
const sameBlock = lastHoverBlockRef.current === blockNow;
|
|
616
|
+
if (sameMark && sameBlock)
|
|
324
617
|
return;
|
|
325
618
|
lastHoverRef.current = hit;
|
|
326
|
-
|
|
619
|
+
lastHoverBlockRef.current = blockNow;
|
|
620
|
+
for (const e of effectiveSelectorEntries(selectorsRef.current, rowKey ?? null)) {
|
|
621
|
+
// <Selector onHover> keeps its per-mark currency regardless of any
|
|
622
|
+
// block — the mark under the pointer, per mark transition.
|
|
623
|
+
if (!sameMark)
|
|
624
|
+
e.onHover?.(hit);
|
|
625
|
+
// A mounted <MultiSelector> hears hover in its own plural currency:
|
|
626
|
+
// the resting BLOCK preview when there is one (per block transition —
|
|
627
|
+
// the marks a drag begun and released here would select), else 0/1
|
|
628
|
+
// hits per mark transition. A live sweep reports through
|
|
629
|
+
// `resolveSweep`'s preview instead, several at once.
|
|
630
|
+
if (blockNow !== null) {
|
|
631
|
+
if (!sameBlock)
|
|
632
|
+
e.onHoverMany?.(blockNow);
|
|
633
|
+
}
|
|
634
|
+
else if (!sameMark) {
|
|
635
|
+
e.onHoverMany?.(hit === null ? EMPTY_SELECTION : [hit]);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
// The block (reference-stable per block) is the hovered STATE when
|
|
639
|
+
// present, so every mark in it lights — and a within-block mark
|
|
640
|
+
// transition hands React the same value back (no re-render, no
|
|
641
|
+
// repaint), which is the block-level analog of the single-mark dedup.
|
|
327
642
|
if (!controlledHoverRef.current)
|
|
328
|
-
setInternalHovered(hit);
|
|
643
|
+
setInternalHovered(blockNow ?? hit);
|
|
644
|
+
}, []);
|
|
645
|
+
// The sweep gesture's container half (interaction RFC §8 / A5.2): resolve a
|
|
646
|
+
// row's press against the mounted <MultiSelector>s in scope and hand the
|
|
647
|
+
// gesture engine its two sinks. Presence is the arm switch — `null` means no
|
|
648
|
+
// sweep can claim the drag (§7.1's mounting-is-enablement, extended). The
|
|
649
|
+
// entries are captured at the press and live for that one drag, matching the
|
|
650
|
+
// ref-not-state discipline of the range drag (#508 item 7).
|
|
651
|
+
const resolveSweep = useCallback((rowKey) => {
|
|
652
|
+
const entries = effectiveSelectorEntries(selectorsRef.current, rowKey).filter((e) => e.multi);
|
|
653
|
+
if (entries.length === 0)
|
|
654
|
+
return null;
|
|
655
|
+
return {
|
|
656
|
+
// The *declaration*, not a measurement of the block — see the field doc.
|
|
657
|
+
snapped: entries.some((e) => e.sequence !== undefined),
|
|
658
|
+
preview: (hits, light = true) => {
|
|
659
|
+
// The single-hit / resting-block dedup state is meaningless mid-sweep;
|
|
660
|
+
// reset both so the first post-sweep pointer hover always reports.
|
|
661
|
+
lastHoverRef.current = null;
|
|
662
|
+
lastHoverBlockRef.current = null;
|
|
663
|
+
// Reporting and lighting are separate on purpose — see the field doc.
|
|
664
|
+
for (const e of entries)
|
|
665
|
+
e.onHoverMany?.(hits);
|
|
666
|
+
if (!controlledHoverRef.current)
|
|
667
|
+
setInternalHovered(light ? hits : null);
|
|
668
|
+
},
|
|
669
|
+
commit: (hits, modifiers, spans) => {
|
|
670
|
+
for (const e of entries)
|
|
671
|
+
e.onSelectMany?.(hits, modifiers, spans);
|
|
672
|
+
// Uncontrolled: the compact span descriptors ARE the selection (A5.2's
|
|
673
|
+
// second currency) — the swept marks stay lit via the same membership
|
|
674
|
+
// test a controlled span would use. The preview clears; the committed
|
|
675
|
+
// highlight takes over.
|
|
676
|
+
//
|
|
677
|
+
// **All of them**, not just the claimant's: a trace sweep produces one
|
|
678
|
+
// span per trace ([PND-TRACESEL]), and keeping only the topmost would
|
|
679
|
+
// leave the uncontrolled path showing less than the preview promised.
|
|
680
|
+
if (!controlledSelectionRef.current)
|
|
681
|
+
setInternalSelected(spans.length === 0 ? null : [...spans]);
|
|
682
|
+
lastHoverRef.current = null;
|
|
683
|
+
lastHoverBlockRef.current = null;
|
|
684
|
+
if (!controlledHoverRef.current)
|
|
685
|
+
setInternalHovered(null);
|
|
686
|
+
},
|
|
687
|
+
};
|
|
329
688
|
}, []);
|
|
689
|
+
// The render-time "is a <MultiSelector> in scope for this row" fact, for the
|
|
690
|
+
// resting block preview (frame doc in context.ts). Unlike `resolveSweep` —
|
|
691
|
+
// a []-stable callback over the selectors REF, correct at pointer-down — a
|
|
692
|
+
// row reads this during render to pick its resting cursor, so it derives
|
|
693
|
+
// from the selectors STATE and re-identifies when the registry changes.
|
|
694
|
+
const hasMultiSelector = useCallback((rowKey) => effectiveSelectorEntries(selectors, rowKey).some((e) => e.multi), [selectors]);
|
|
330
695
|
// Rows report their per-slot gutter widths; we reserve each slot's max.
|
|
331
696
|
const [gutters, setGutters] = useState([]);
|
|
332
697
|
const registerGutter = useCallback((req) => {
|
|
@@ -613,20 +978,23 @@ export function ChartContainer({ range, maxBandWidth, bandAlign = 'start', width
|
|
|
613
978
|
}
|
|
614
979
|
// No sequence → snap to a bar/histogram layer's bins, if any (a value axis,
|
|
615
980
|
// or a time-axis histogram with no explicit sequence). `binIntervals` is only
|
|
616
|
-
// published by a vertical bar layer
|
|
617
|
-
//
|
|
981
|
+
// published by a vertical bar layer, so this is a no-op for
|
|
982
|
+
// line/area/scatter rows. On a **category** axis the bins are the unit
|
|
983
|
+
// slots `[i, i+1)`: the region cursor never reads them (its band gates on
|
|
984
|
+
// a continuous axis), but the `<MultiSelector>` sweep's band snaps over
|
|
985
|
+
// them so it runs slot-edge to slot-edge — the band scale's `invert`
|
|
986
|
+
// returns slot *centres*, and a centre-to-centre band disagreed with the
|
|
987
|
+
// snapped-outward span the release commits (RFC A7.6's edge rule).
|
|
618
988
|
//
|
|
619
989
|
// **First bar layer wins** — deliberately non-fatal, unlike `xCategories`
|
|
620
990
|
// (which *throws* when category rows disagree, because a mismatched slot order
|
|
621
991
|
// corrupts the shared band scale). Two overlaid histograms with different bins
|
|
622
992
|
// is a degenerate layout the region cursor just snaps to whichever registered
|
|
623
993
|
// first; a wrong snap grid is harmless where a wrong axis is not.
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
return bins;
|
|
629
|
-
}
|
|
994
|
+
for (const s of sources.values()) {
|
|
995
|
+
const bins = s.binIntervals?.() ?? null;
|
|
996
|
+
if (bins && bins.length > 0)
|
|
997
|
+
return bins;
|
|
630
998
|
}
|
|
631
999
|
return undefined;
|
|
632
1000
|
}, [cursorSequence, d0, d1, resolvedKind, sources]);
|
|
@@ -689,6 +1057,8 @@ export function ChartContainer({ range, maxBandWidth, bandAlign = 'start', width
|
|
|
689
1057
|
crosshairSnap,
|
|
690
1058
|
cursorBuckets,
|
|
691
1059
|
regionAnchor,
|
|
1060
|
+
previewSpans,
|
|
1061
|
+
setPreviewSpans,
|
|
692
1062
|
setRegionAnchor,
|
|
693
1063
|
onRegionSelect,
|
|
694
1064
|
reportDrawStats,
|
|
@@ -696,6 +1066,7 @@ export function ChartContainer({ range, maxBandWidth, bandAlign = 'start', width
|
|
|
696
1066
|
draggingKey,
|
|
697
1067
|
setDragging,
|
|
698
1068
|
selected: selectedValue,
|
|
1069
|
+
selectedSpans,
|
|
699
1070
|
select,
|
|
700
1071
|
hovered: hoveredValue,
|
|
701
1072
|
setHovered,
|
|
@@ -717,6 +1088,13 @@ export function ChartContainer({ range, maxBandWidth, bandAlign = 'start', width
|
|
|
717
1088
|
unregisterTrackerSource,
|
|
718
1089
|
registerSelectable,
|
|
719
1090
|
unregisterSelectable,
|
|
1091
|
+
registerCursor,
|
|
1092
|
+
unregisterCursor,
|
|
1093
|
+
cursors,
|
|
1094
|
+
registerSelector,
|
|
1095
|
+
unregisterSelector,
|
|
1096
|
+
resolveSweep,
|
|
1097
|
+
hasMultiSelector,
|
|
720
1098
|
registerAnnotation,
|
|
721
1099
|
unregisterAnnotation,
|
|
722
1100
|
registerLegendItem,
|
|
@@ -734,6 +1112,13 @@ export function ChartContainer({ range, maxBandWidth, bandAlign = 'start', width
|
|
|
734
1112
|
zoomEnabled,
|
|
735
1113
|
minDuration,
|
|
736
1114
|
applyRange,
|
|
1115
|
+
zoomX,
|
|
1116
|
+
zoomY,
|
|
1117
|
+
panX,
|
|
1118
|
+
panY,
|
|
1119
|
+
aspectLocked,
|
|
1120
|
+
yTransform,
|
|
1121
|
+
applyYTransform,
|
|
737
1122
|
registerGutter,
|
|
738
1123
|
registerRow,
|
|
739
1124
|
firstRowKey,
|
|
@@ -751,6 +1136,8 @@ export function ChartContainer({ range, maxBandWidth, bandAlign = 'start', width
|
|
|
751
1136
|
crosshairSnap,
|
|
752
1137
|
cursorBuckets,
|
|
753
1138
|
regionAnchor,
|
|
1139
|
+
previewSpans,
|
|
1140
|
+
setPreviewSpans,
|
|
754
1141
|
setRegionAnchor,
|
|
755
1142
|
onRegionSelect,
|
|
756
1143
|
reportDrawStats,
|
|
@@ -758,6 +1145,7 @@ export function ChartContainer({ range, maxBandWidth, bandAlign = 'start', width
|
|
|
758
1145
|
draggingKey,
|
|
759
1146
|
setDragging,
|
|
760
1147
|
selectedValue,
|
|
1148
|
+
selectedSpans,
|
|
761
1149
|
select,
|
|
762
1150
|
hoveredValue,
|
|
763
1151
|
setHovered,
|
|
@@ -779,6 +1167,13 @@ export function ChartContainer({ range, maxBandWidth, bandAlign = 'start', width
|
|
|
779
1167
|
unregisterTrackerSource,
|
|
780
1168
|
registerSelectable,
|
|
781
1169
|
unregisterSelectable,
|
|
1170
|
+
registerCursor,
|
|
1171
|
+
unregisterCursor,
|
|
1172
|
+
cursors,
|
|
1173
|
+
registerSelector,
|
|
1174
|
+
unregisterSelector,
|
|
1175
|
+
resolveSweep,
|
|
1176
|
+
hasMultiSelector,
|
|
782
1177
|
registerAnnotation,
|
|
783
1178
|
unregisterAnnotation,
|
|
784
1179
|
registerLegendItem,
|
|
@@ -796,17 +1191,28 @@ export function ChartContainer({ range, maxBandWidth, bandAlign = 'start', width
|
|
|
796
1191
|
zoomEnabled,
|
|
797
1192
|
minDuration,
|
|
798
1193
|
applyRange,
|
|
1194
|
+
zoomX,
|
|
1195
|
+
zoomY,
|
|
1196
|
+
panX,
|
|
1197
|
+
panY,
|
|
1198
|
+
aspectLocked,
|
|
1199
|
+
yTransform,
|
|
1200
|
+
applyYTransform,
|
|
799
1201
|
registerGutter,
|
|
800
1202
|
registerRow,
|
|
801
1203
|
firstRowKey,
|
|
802
1204
|
]);
|
|
803
|
-
return (_jsx(ContainerContext.Provider, { value: frame, children:
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
1205
|
+
return (_jsx(ContainerContext.Provider, { value: frame, children: _jsxs(CursorContext.Provider, { value: cursorFrame, children: [_jsx(LegacyCursor, { mode: cursor, showTime: cursorTime, snap: crosshairSnap, sequence: cursorSequenceProp,
|
|
1206
|
+
// The 'line' default nobody asked for is IMPLICIT — the one cursor a
|
|
1207
|
+
// <MultiSelector>'s resting block preview may replace with the
|
|
1208
|
+
// brush band. An explicit `cursor` prop (any mode) still wins.
|
|
1209
|
+
implicit: cursorProp === undefined }), _jsxs("div", { style: { width: `${width}px` }, children: [_jsx("div", { style: {
|
|
1210
|
+
display: 'flex',
|
|
1211
|
+
flexDirection: 'column',
|
|
1212
|
+
gap: `${rowGap}px`,
|
|
1213
|
+
// The positioned ancestor for overlay chrome (`<Legend>`): the
|
|
1214
|
+
// card anchors to the rows block, never the axis strip below.
|
|
1215
|
+
position: 'relative',
|
|
1216
|
+
}, children: children }), showAxis && _jsx(TimeAxis, {})] })] }) }));
|
|
811
1217
|
}
|
|
812
1218
|
//# sourceMappingURL=ChartContainer.js.map
|