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