@pond-ts/charts 0.50.0 → 0.51.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 +113 -1
- package/dist/AreaChart.js +1 -0
- package/dist/BandChart.js +1 -0
- package/dist/BarChart.d.ts +14 -1
- package/dist/BarChart.js +5 -2
- package/dist/BoxPlot.js +1 -0
- package/dist/Candlestick.js +1 -0
- package/dist/ChartContainer.d.ts +61 -11
- package/dist/ChartContainer.js +70 -23
- package/dist/Layers.js +53 -14
- package/dist/Legend.js +5 -2
- package/dist/LineChart.js +1 -0
- package/dist/ScatterChart.js +1 -0
- package/dist/XAxis.js +3 -2
- package/dist/affine.d.ts +41 -0
- package/dist/affine.js +77 -0
- package/dist/area.d.ts +20 -2
- package/dist/area.js +151 -45
- package/dist/band.d.ts +2 -1
- package/dist/band.js +5 -0
- package/dist/bars.d.ts +14 -1
- package/dist/bars.js +42 -1
- package/dist/box.d.ts +2 -1
- package/dist/box.js +8 -3
- package/dist/context.d.ts +121 -22
- package/dist/context.js +8 -0
- package/dist/data.d.ts +8 -0
- package/dist/decimate.d.ts +78 -1
- package/dist/decimate.js +132 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.js +5 -3
- package/dist/line.d.ts +15 -1
- package/dist/line.js +84 -30
- package/dist/ohlc.d.ts +2 -1
- package/dist/ohlc.js +8 -3
- package/dist/tracker.d.ts +17 -4
- package/dist/tracker.js +19 -6
- package/dist/useChartLegend.d.ts +2 -2
- package/dist/useChartLegend.js +8 -7
- package/dist/viewport.d.ts +19 -0
- package/dist/viewport.js +32 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -8,7 +8,8 @@ The `@pond-ts` packages — `pond-ts`, `@pond-ts/react`, `@pond-ts/charts`,
|
|
|
8
8
|
tag, so this file covers them all. Pre-1.0: minor bumps may include new features
|
|
9
9
|
and type-level changes; patch bumps are strictly additive.
|
|
10
10
|
|
|
11
|
-
[Unreleased]: https://github.com/pond-ts/pond/compare/v0.
|
|
11
|
+
[Unreleased]: https://github.com/pond-ts/pond/compare/v0.51.0...HEAD
|
|
12
|
+
[0.51.0]: https://github.com/pond-ts/pond/compare/v0.50.0...v0.51.0
|
|
12
13
|
[0.50.0]: https://github.com/pond-ts/pond/compare/v0.49.0...v0.50.0
|
|
13
14
|
[0.49.0]: https://github.com/pond-ts/pond/compare/v0.48.1...v0.49.0
|
|
14
15
|
[0.48.1]: https://github.com/pond-ts/pond/compare/v0.48.0...v0.48.1
|
|
@@ -50,6 +51,117 @@ and type-level changes; patch bumps are strictly additive.
|
|
|
50
51
|
|
|
51
52
|
## [Unreleased]
|
|
52
53
|
|
|
54
|
+
## [0.51.0] — 2026-07-22
|
|
55
|
+
|
|
56
|
+
### Changed
|
|
57
|
+
|
|
58
|
+
- **charts:** **`trackerPosition` is now a _followed_ position, not a hard pin —
|
|
59
|
+
enabling cross-chart cursor sync.** A live local hover wins over
|
|
60
|
+
`trackerPosition`, so the chart under the pointer shows its own cursor while
|
|
61
|
+
any chart without a local pointer follows the controlled time (mapped through
|
|
62
|
+
its own `xScale`, so it's correct across different zooms). This makes
|
|
63
|
+
**multi-chart dashboard cursor sync** fall out of the plain props: give every
|
|
64
|
+
`<ChartContainer>` the same `trackerPosition={sharedTime}` and set `sharedTime`
|
|
65
|
+
from each one's `onTrackerChanged` (clear it to `null` on the group's
|
|
66
|
+
`onPointerLeave`) — no "which chart is active" bookkeeping. **Behaviour
|
|
67
|
+
change:** previously a numeric `trackerPosition` overrode local hover, and
|
|
68
|
+
`trackerPosition={null}` force-hid the cursor; now `null` and `undefined` are
|
|
69
|
+
equivalent ("no controlled position") and a hovered chart always tracks its
|
|
70
|
+
pointer. To force a chart to never show a cursor, use `cursor="none"`. See the
|
|
71
|
+
"Synced cursors across charts" story. No type change (`number | null`).
|
|
72
|
+
- **charts:** **Line / area draw is ~3× faster on stroke-bound frames**
|
|
73
|
+
(PND-AFFINE / PND-GRADX; 2026-07 external-bench profile). When the curve is
|
|
74
|
+
linear and both scales are affine (every y axis is `scaleLinear`; x is
|
|
75
|
+
`scaleLinear` / `scaleTime` / the gap-free default time axis), `drawLine` and
|
|
76
|
+
`drawArea` now map points with an inline `k·v + b` over the typed arrays
|
|
77
|
+
instead of a per-point d3-scale closure + d3-shape generator — a **visually
|
|
78
|
+
identical** draw (guarded by the decimation pixel-identity and per-layer
|
|
79
|
+
visual-regression e2e). A real-gap trading-time axis, or a non-linear curve,
|
|
80
|
+
transparently keeps the exact d3 path. Measured on a JS-only micro-bench
|
|
81
|
+
(`scripts/perf-affine.mjs`): line 1M 60.4 → 19.0 ms (3.2×), area 1M 132 →
|
|
82
|
+
38 ms (3.5×). Separately, the area fill gradient's full-series value extent is
|
|
83
|
+
now memoized per column buffer, so a y-zoom / pan repaint no longer re-walks
|
|
84
|
+
the whole series to find the gradient span. No API change.
|
|
85
|
+
- **charts:** **A y-zoom / y-autorange repaint no longer re-decimates line /
|
|
86
|
+
area layers** (PND-DECKEY; same 2026-07 profile, finding 3). The M4
|
|
87
|
+
decimation output is a pure function of the source data, x-domain, device
|
|
88
|
+
width, threshold, and session breaks — it never reads the y-scale — so it is
|
|
89
|
+
identical across every y-only frame. `drawLine` / `drawArea` now memoize the
|
|
90
|
+
cull+decimate result per source series (one entry, keyed on the x-scale
|
|
91
|
+
object + width + threshold + breaks), so a y-zoom / live y-autorange frame
|
|
92
|
+
reuses the prior polyline instead of re-binning O(N) points; a pan / x-zoom
|
|
93
|
+
mints a fresh x-scale and correctly recomputes. Measured
|
|
94
|
+
(`scripts/perf-deckey.mjs`): the ~5 ms/frame decimation walk at 1M points is
|
|
95
|
+
eliminated on every y-only frame. No API change.
|
|
96
|
+
|
|
97
|
+
### Added
|
|
98
|
+
|
|
99
|
+
- **charts:** **`<BarChart decimate>` — dense column charts now decimate**
|
|
100
|
+
(PND-MARKDEC; 2026-07 profile, finding 4 — "column dead by 5M"). **Default
|
|
101
|
+
`true`**: once the visible **single-series** bars are denser than ~2 per device
|
|
102
|
+
pixel (each slot < ~1px), they're drawn as one per-column **envelope** rect —
|
|
103
|
+
the exact painted union `[min(value, baseline), max(value, baseline)]` of each
|
|
104
|
+
pixel column — instead of every bar, so a 100k–5M-bar column chart stays
|
|
105
|
+
interactive. **Visually lossless** at that density (a perf knob, not a style);
|
|
106
|
+
interaction still reads the source bars (`barAt`), and the per-bar
|
|
107
|
+
selection/hover highlight is suppressed only when decimated (a <1px bar's ring
|
|
108
|
+
isn't visible anyway). `decimate={false}` draws every bar; `{ threshold }`
|
|
109
|
+
tunes the samples-per-pixel factor. No-op for a stacked / multi-group
|
|
110
|
+
histogram (the low-count categorical path). `drawBars` now returns
|
|
111
|
+
`LayerDrawStats` (visible via `onDrawStats`). Measured
|
|
112
|
+
(`scripts/perf-markdec.mjs`, JS-only): the bar draw at 5M points drops
|
|
113
|
+
485 → 26 ms (18.9×), 100k drops 9.7 → 0.9 ms (10.7×) — with the larger
|
|
114
|
+
rasterization win on top, browser-side.
|
|
115
|
+
- **charts:** **`panZoom` is now a three-way mode + a `bounds` extent.**
|
|
116
|
+
`<ChartContainer panZoom>` takes `'none'` / `'pan'` / `'panZoom'` (drag-only
|
|
117
|
+
vs. drag+wheel), with the old boolean kept as shorthand (`true` ⇒ `'panZoom'`,
|
|
118
|
+
`false` ⇒ `'none'`) — so existing charts are unchanged. A new `bounds`
|
|
119
|
+
(`[min, max]`) prop fences pan/zoom to an **outer** extent (panning into an
|
|
120
|
+
edge stops there keeping its span; zoom-out is capped at the whole span), the
|
|
121
|
+
companion
|
|
122
|
+
to the existing `minDuration` zoom-in floor — together they pin the reachable
|
|
123
|
+
window between an inner and outer bound. On a trading-time axis `bounds`
|
|
124
|
+
clamps in wall-clock ms. Purely additive; no type narrowing.
|
|
125
|
+
- **charts:** **Draw-cost + decimation observability** — `<ChartContainer
|
|
126
|
+
onDrawStats>` (PND-DECOBS; dashboard A/B friction, 2026-07-21). Fires a
|
|
127
|
+
`DrawStatsFrame` once per row-canvas repaint (keyed by an opaque `rowKey` for
|
|
128
|
+
multi-row attribution), one `LayerDrawInfo` per layer carrying its `as`,
|
|
129
|
+
measured `drawMs`, and — for a decimating layer (line / area / band / candle /
|
|
130
|
+
box) — `sourceCount` / `drawnCount` / `decimated`.
|
|
131
|
+
Compare `drawnCount` to `sourceCount` to see whether M4 engaged; read `drawMs`
|
|
132
|
+
for per-layer render cost. **Zero-overhead when unused** — the render loop
|
|
133
|
+
skips per-layer timing entirely unless a consumer subscribes. New exports:
|
|
134
|
+
`DrawStatsFrame`, `LayerDrawInfo`.
|
|
135
|
+
|
|
136
|
+
### Fixed
|
|
137
|
+
|
|
138
|
+
- **charts:** hovering no longer repaints the row data canvas on every cursor
|
|
139
|
+
mousemove. The container frame minted a fresh `timeRange` array identity per
|
|
140
|
+
rebuild (and the frame rebuilds per cursor move), which the Layers draw
|
|
141
|
+
callback — depending on `container.timeRange` — read as a domain change:
|
|
142
|
+
each hover frame re-fired the canvas draw effect, including per-layer M4
|
|
143
|
+
re-decimation (measured 105 repaints per 122 mousemove events; with
|
|
144
|
+
`decimate` off, hover fell to ~10 fps). The tuple is now identity-stable on
|
|
145
|
+
its endpoints, restoring the SVG-overlay cursor contract (0 repaints, full
|
|
146
|
+
frame rate while hovering). Found running uPlot's bench protocol against
|
|
147
|
+
pond-charts; guarded by a new hover-sweep perf invariant in
|
|
148
|
+
`e2e/perf-invariants.spec.ts`.
|
|
149
|
+
- **charts:** hovering no longer re-renders cursor-independent components
|
|
150
|
+
(both `YAxis`, `Bar`/`Box`). The cursor position was a `ContainerFrame`
|
|
151
|
+
field, so every mousemove re-identified the whole (~50-field) frame and
|
|
152
|
+
re-rendered **all** its context consumers — even ones that never read the
|
|
153
|
+
cursor. The per-move cursor state (`cursorX`/`cursorY`/`cursorRowKey`) now
|
|
154
|
+
lives in a dedicated `CursorContext`; the frame stays identity-stable across
|
|
155
|
+
a hover, so only the genuine cursor consumers (the `Layers` overlay,
|
|
156
|
+
`XAxis` crosshair pill, `Legend` values) re-render. Measured: 4 → 2 React
|
|
157
|
+
commits per mousemove, ~25% less hover script time on the uPlot-bench
|
|
158
|
+
workload (the win scales with axis/row count). No API change — the split
|
|
159
|
+
types are internal (`PND-HOVCTX`, follow-up to the repaint fix above).
|
|
160
|
+
- **charts:** corrected the `@pond-ts/charts` package-header doc comment, which
|
|
161
|
+
described a "chunked Path2D cache" render stage that was explored and
|
|
162
|
+
**deferred**, never built (it doesn't help the pan case, which re-decimates
|
|
163
|
+
every frame). The stale comment had misled a consumer's perf investigation.
|
|
164
|
+
|
|
53
165
|
## [0.50.0] — 2026-07-21
|
|
54
166
|
|
|
55
167
|
### Added
|
package/dist/AreaChart.js
CHANGED
|
@@ -59,6 +59,7 @@ export function AreaChart({ series, column, as: semantic, axis, baseline, curve,
|
|
|
59
59
|
const gapConnectorOpacity = container.theme.gap?.connectorOpacity ?? DEFAULT_GAP_CONNECTOR_OPACITY;
|
|
60
60
|
const entry = useMemo(() => ({
|
|
61
61
|
layer: {
|
|
62
|
+
as: semantic,
|
|
62
63
|
yExtent: () => areaExtent(cs, baseline),
|
|
63
64
|
// The container infers the shared x scale's kind from its layers — a
|
|
64
65
|
// ValueSeries plots on a value axis, a TimeSeries on time.
|
package/dist/BandChart.js
CHANGED
|
@@ -49,6 +49,7 @@ export function BandChart({ series, lower, upper, as: semantic, axis, curve, dec
|
|
|
49
49
|
}, [semantic]);
|
|
50
50
|
const entry = useMemo(() => ({
|
|
51
51
|
layer: {
|
|
52
|
+
as: semantic,
|
|
52
53
|
yExtent: () => bandExtent(bs),
|
|
53
54
|
// The container infers the shared x scale's kind from its layers — a
|
|
54
55
|
// ValueSeries plots on a value axis, a TimeSeries on time.
|
package/dist/BarChart.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { ValueSeries } from 'pond-ts';
|
|
|
2
2
|
import type { SeriesSchema, TimeSeries, ValueSeriesSchema } from 'pond-ts';
|
|
3
3
|
import { type BinRecord, type CategoryDatum } from './data.js';
|
|
4
4
|
import { type Orientation } from './bars.js';
|
|
5
|
+
import type { DecimateOption } from './decimate.js';
|
|
5
6
|
export interface BarChartProps<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> {
|
|
6
7
|
/**
|
|
7
8
|
* The source series. Provide **exactly one** of `series` or `bins`.
|
|
@@ -120,6 +121,18 @@ export interface BarChartProps<S extends SeriesSchema = SeriesSchema, VS extends
|
|
|
120
121
|
* would invert collapses to the style's `minWidth`.
|
|
121
122
|
*/
|
|
122
123
|
gap?: number;
|
|
124
|
+
/**
|
|
125
|
+
* **M4 column decimation** (charts decimator wave). **Omitted ⇒ `true`**: once
|
|
126
|
+
* the visible bars are denser than ~2 per device pixel (each slot < ~1px), the
|
|
127
|
+
* **single-series** bars are drawn as one per-column **envelope** rect — the
|
|
128
|
+
* exact painted union `[min, max]` of each pixel column's bars — instead of every
|
|
129
|
+
* bar, so a 100k-bar column chart stays interactive. It is **visually lossless**
|
|
130
|
+
* at that density (a perf knob, not a style); interaction still reads the source
|
|
131
|
+
* bars. Pass `false` to always draw every bar, or `{ threshold }` to tune the
|
|
132
|
+
* samples-per-pixel factor. **No-op for a stacked / multi-group histogram** (the
|
|
133
|
+
* categorical case is low-count; only the single-series path decimates).
|
|
134
|
+
*/
|
|
135
|
+
decimate?: DecimateOption;
|
|
123
136
|
/**
|
|
124
137
|
* This layer's `<Legend>` row(s): `false` ⇒ none (opt out), a string ⇒ the
|
|
125
138
|
* display name of a **one-row** layer. **Omitted ⇒** the single path (and a
|
|
@@ -180,5 +193,5 @@ export interface BarChartProps<S extends SeriesSchema = SeriesSchema, VS extends
|
|
|
180
193
|
* </Layers>
|
|
181
194
|
* ```
|
|
182
195
|
*/
|
|
183
|
-
export declare function BarChart<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema>({ series, bins, categories, column, columns, as: semantic, colors, binColors, orientation, ordinal, id, axis, gap, legend, index, }: BarChartProps<S, VS>): null;
|
|
196
|
+
export declare function BarChart<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema>({ series, bins, categories, column, columns, as: semantic, colors, binColors, orientation, ordinal, id, axis, gap, decimate, legend, index, }: BarChartProps<S, VS>): null;
|
|
184
197
|
//# sourceMappingURL=BarChart.d.ts.map
|
package/dist/BarChart.js
CHANGED
|
@@ -48,7 +48,7 @@ import { useSlotKey } from './use-slot-key.js';
|
|
|
48
48
|
* </Layers>
|
|
49
49
|
* ```
|
|
50
50
|
*/
|
|
51
|
-
export function BarChart({ series, bins, categories, column, columns, as: semantic, colors, binColors, orientation = 'vertical', ordinal = false, id, axis, gap, legend, index = 0, }) {
|
|
51
|
+
export function BarChart({ series, bins, categories, column, columns, as: semantic, colors, binColors, orientation = 'vertical', ordinal = false, id, axis, gap, decimate = true, legend, index = 0, }) {
|
|
52
52
|
const container = useContext(ContainerContext);
|
|
53
53
|
if (container === null) {
|
|
54
54
|
throw new Error('<BarChart> must be rendered inside a <ChartContainer>');
|
|
@@ -226,6 +226,7 @@ export function BarChart({ series, bins, categories, column, columns, as: semant
|
|
|
226
226
|
const bs = shape.bs;
|
|
227
227
|
return {
|
|
228
228
|
layer: {
|
|
229
|
+
as: semantic,
|
|
229
230
|
yExtent: () => barExtent(bs),
|
|
230
231
|
xKind: binAxisKind,
|
|
231
232
|
xExtent: () => bs.length === 0 ? null : [bs.begin[0], bs.end[bs.length - 1]],
|
|
@@ -266,7 +267,7 @@ export function BarChart({ series, bins, categories, column, columns, as: semant
|
|
|
266
267
|
};
|
|
267
268
|
},
|
|
268
269
|
}),
|
|
269
|
-
draw: (ctx, xScale, yScale) => drawBars(ctx, bs, xScale, yScale, singleStyle, resolveBarBaseline(yScale), gapPx, id, selection, hover),
|
|
270
|
+
draw: (ctx, xScale, yScale) => drawBars(ctx, bs, xScale, yScale, singleStyle, resolveBarBaseline(yScale), gapPx, id, selection, hover, decimate),
|
|
270
271
|
},
|
|
271
272
|
axisId: axis,
|
|
272
273
|
index,
|
|
@@ -279,6 +280,7 @@ export function BarChart({ series, bins, categories, column, columns, as: semant
|
|
|
279
280
|
const vertical = orientation === 'vertical';
|
|
280
281
|
return {
|
|
281
282
|
layer: {
|
|
283
|
+
as: semantic,
|
|
282
284
|
// Horizontal puts the value on the shared x (always 'value'); vertical
|
|
283
285
|
// keeps the bin axis on x. The bin axis on the *other* side is a linear
|
|
284
286
|
// numeric scale either way (time ms label via <YAxis ticks>).
|
|
@@ -336,6 +338,7 @@ export function BarChart({ series, bins, categories, column, columns, as: semant
|
|
|
336
338
|
label,
|
|
337
339
|
id,
|
|
338
340
|
gapPx,
|
|
341
|
+
decimate,
|
|
339
342
|
stackMinWidth,
|
|
340
343
|
selection,
|
|
341
344
|
hover,
|
package/dist/BoxPlot.js
CHANGED
|
@@ -73,6 +73,7 @@ export function BoxPlot({ series, lower, q1, median, q3, upper, as: semantic, ax
|
|
|
73
73
|
const hoveredKey = id !== undefined && hov !== null && hov.id === id ? hov.key : null;
|
|
74
74
|
const entry = useMemo(() => ({
|
|
75
75
|
layer: {
|
|
76
|
+
as: semantic,
|
|
76
77
|
yExtent: () => boxExtent(bx),
|
|
77
78
|
// A ValueSeries plots on a value axis, a TimeSeries on time; the container
|
|
78
79
|
// infers the shared x kind from its layers.
|
package/dist/Candlestick.js
CHANGED
|
@@ -49,6 +49,7 @@ export function Candlestick({ series, open = 'open', high = 'high', low = 'low',
|
|
|
49
49
|
const label = semantic ?? close;
|
|
50
50
|
const entry = useMemo(() => ({
|
|
51
51
|
layer: {
|
|
52
|
+
as: semantic,
|
|
52
53
|
yExtent: () => ohlcExtent(ohlc),
|
|
53
54
|
xKind: 'time',
|
|
54
55
|
xExtent: () => ohlc.length === 0 ? null : [ohlc.x[0], ohlc.xEnd[ohlc.length - 1]],
|
package/dist/ChartContainer.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { type ReactNode } from 'react';
|
|
|
2
2
|
import { type DiscontinuityProvider, type TradingCalendarLike } from './tradingTimeScale.js';
|
|
3
3
|
import { Sequence, BoundedSequence } from 'pond-ts';
|
|
4
4
|
import type { TimeRange } from 'pond-ts';
|
|
5
|
-
import { type AnnotationKind, type CreateSpec, type CursorMode, type SelectInfo, type TrackerInfo } from './context.js';
|
|
5
|
+
import { type AnnotationKind, type CreateSpec, type CursorMode, type SelectInfo, type TrackerInfo, type DrawStatsFrame } from './context.js';
|
|
6
6
|
import { type AxisFormat, type CursorFormat } from './format.js';
|
|
7
7
|
import { type ChartTheme } from './theme.js';
|
|
8
8
|
export interface ChartContainerProps {
|
|
@@ -94,9 +94,25 @@ export interface ChartContainerProps {
|
|
|
94
94
|
*/
|
|
95
95
|
showAxis?: boolean;
|
|
96
96
|
/**
|
|
97
|
-
* Controlled tracker position (epoch ms) —
|
|
98
|
-
*
|
|
99
|
-
*
|
|
97
|
+
* Controlled tracker position (epoch ms) — where to show the synced crosshair
|
|
98
|
+
* **when this chart isn't the one under the pointer**. A live local hover
|
|
99
|
+
* always wins over it, so this is a *followed* position, not a hard pin:
|
|
100
|
+
* supply it to drive the cursor from outside (a scrubber, a playback head, or
|
|
101
|
+
* — the main use — **cross-chart sync**). Maps through this chart's own
|
|
102
|
+
* `xScale`, so it lands at the right pixel even under a different zoom.
|
|
103
|
+
*
|
|
104
|
+
* **Multi-chart sync** falls out of this plus {@link onTrackerChanged}: give
|
|
105
|
+
* every `<ChartContainer>` the same `trackerPosition={sharedTime}` and set
|
|
106
|
+
* `sharedTime` from each one's `onTrackerChanged`. The hovered chart favors its
|
|
107
|
+
* own pointer (and reports the time out); the others follow. Clear `sharedTime`
|
|
108
|
+
* to `null` on the group's `onPointerLeave` so the crosshair lifts when the
|
|
109
|
+
* pointer leaves every chart. (See the "Synced cursors across charts" story /
|
|
110
|
+
* dashboard guide.)
|
|
111
|
+
*
|
|
112
|
+
* **Omit or pass `null`** (equivalent) for no controlled position — a hovered
|
|
113
|
+
* chart still tracks its pointer, a non-hovered one shows nothing. To force a
|
|
114
|
+
* chart to *never* show a cursor, use `cursor="none"`, not `trackerPosition`.
|
|
115
|
+
* See {@link onTrackerChanged}.
|
|
100
116
|
*/
|
|
101
117
|
trackerPosition?: number | null;
|
|
102
118
|
/**
|
|
@@ -154,9 +170,10 @@ export interface ChartContainerProps {
|
|
|
154
170
|
onRegionSelect?: (range: readonly [number, number]) => void;
|
|
155
171
|
/**
|
|
156
172
|
* Which modifier a region-drag needs — set `'shift'` when you also enable
|
|
157
|
-
* `panZoom` and want **plain drag to pan,
|
|
158
|
-
* enforced while
|
|
159
|
-
* shift is optional — either drag
|
|
173
|
+
* **pan** (`panZoom="pan"` or `"panZoom"`) and want **plain drag to pan,
|
|
174
|
+
* shift-drag to select**. It's only enforced while pan is enabled (with pan
|
|
175
|
+
* off there's no gesture conflict, so shift is optional — either drag
|
|
176
|
+
* selects). **Omitted** ⇒ a region-drag
|
|
160
177
|
* **preempts** pan (drag always selects; document that precedence for users).
|
|
161
178
|
* Wheel-zoom is unaffected in every case.
|
|
162
179
|
*/
|
|
@@ -166,6 +183,17 @@ export interface ChartContainerProps {
|
|
|
166
183
|
* you can render a readout outside the chart), and `null` on leave.
|
|
167
184
|
*/
|
|
168
185
|
onTrackerChanged?: (info: TrackerInfo | null) => void;
|
|
186
|
+
/**
|
|
187
|
+
* Draw-cost + decimation observability. Fires **once per row-canvas repaint**
|
|
188
|
+
* with a {@link DrawStatsFrame} — one {@link LayerDrawInfo} per layer in that
|
|
189
|
+
* row carrying its `as`, `drawMs`, and (for a decimating layer) `sourceCount`
|
|
190
|
+
* / `drawnCount` / `decimated`. Compare `drawnCount` to `sourceCount` to see
|
|
191
|
+
* whether M4 engaged; read `drawMs` for per-layer render cost. **Omitted ⇒ no
|
|
192
|
+
* measurement** — the render loop skips per-layer timing entirely, so this is
|
|
193
|
+
* zero-overhead when unused. Keep the callback cheap (it runs inside the draw
|
|
194
|
+
* frame); route it to a ref/store rather than doing React state work per frame.
|
|
195
|
+
*/
|
|
196
|
+
onDrawStats?: (frame: DrawStatsFrame) => void;
|
|
169
197
|
/**
|
|
170
198
|
* Controlled selection — the selected mark (echo the `onSelect` arg back), or
|
|
171
199
|
* `null`. **Omitted ⇒ uncontrolled** (a click on a selectable layer manages it
|
|
@@ -212,10 +240,32 @@ export interface ChartContainerProps {
|
|
|
212
240
|
*/
|
|
213
241
|
onHover?: (hit: SelectInfo | null) => void;
|
|
214
242
|
/**
|
|
215
|
-
*
|
|
216
|
-
*
|
|
243
|
+
* Which pan/zoom gestures the plot captures:
|
|
244
|
+
*
|
|
245
|
+
* - `'none'` (or `false`, the **default**) — neither; the plot doesn't capture
|
|
246
|
+
* drag or scroll.
|
|
247
|
+
* - `'pan'` — drag to pan the time range, **no** wheel-zoom (scroll still
|
|
248
|
+
* scrolls the page).
|
|
249
|
+
* - `'panZoom'` (or `true`) — drag to pan **and** wheel to zoom around the
|
|
250
|
+
* cursor.
|
|
251
|
+
*
|
|
252
|
+
* The boolean form is the back-compat shorthand (`true` ⇒ `'panZoom'`,
|
|
253
|
+
* `false` ⇒ `'none'`). Bound the reachable range with {@link bounds}
|
|
254
|
+
* (zoom-out / pan extent) and {@link minDuration} (zoom-in floor).
|
|
255
|
+
*/
|
|
256
|
+
panZoom?: boolean | 'none' | 'pan' | 'panZoom';
|
|
257
|
+
/**
|
|
258
|
+
* **Outer pan/zoom extent** — `[min, max]` (same units as {@link range}) the
|
|
259
|
+
* view can never move outside. Panning into an edge stops there (the window
|
|
260
|
+
* keeps its span); zooming out is capped at this width, so `bounds` is the
|
|
261
|
+
* zoom-**out** ceiling that pairs with the {@link minDuration} zoom-**in**
|
|
262
|
+
* floor. **Omit for no limit** (pan/zoom is unbounded). Constrains gestures
|
|
263
|
+
* (and any range routed through the container); seed {@link range} within it.
|
|
264
|
+
* On a trading-time axis the clamp is in wall-clock ms (a sensible outer
|
|
265
|
+
* limit; the per-session pan/zoom math already holds the trading span at each
|
|
266
|
+
* calendar edge).
|
|
217
267
|
*/
|
|
218
|
-
|
|
268
|
+
bounds?: readonly [number, number];
|
|
219
269
|
/**
|
|
220
270
|
* Controlled view range — fires on pan/zoom with the new `[start, end]`. Wire
|
|
221
271
|
* it back to `range` for a controlled chart; omit for uncontrolled (the
|
|
@@ -340,5 +390,5 @@ export interface ChartContainerProps {
|
|
|
340
390
|
* {@link TimeAxis} at the bottom, aligned under the plots. Y axes are per-row
|
|
341
391
|
* (`<YAxis>`).
|
|
342
392
|
*/
|
|
343
|
-
export declare function ChartContainer({ range, width, rowGap, showAxis, trackerPosition, onTrackerChanged, selected, onSelect, hovered, onHover, panZoom, onTimeRangeChange, minDuration, cursor, cursorSequence, onRegionSelect, regionSelectModifier, cursorTime, crosshairSnap, editAnnotations, creating, onCreate, onSelectAnnotation, onHoverAnnotation, onEditAnnotation, snap, timeFormat, cursorFormat, theme, discontinuities, calendar, spacing, grid, sessionDividers, children, }: ChartContainerProps): import("react/jsx-runtime").JSX.Element;
|
|
393
|
+
export declare function ChartContainer({ range, width, rowGap, showAxis, trackerPosition, onTrackerChanged, onDrawStats, selected, onSelect, hovered, onHover, panZoom, bounds, onTimeRangeChange, minDuration, cursor, cursorSequence, onRegionSelect, regionSelectModifier, cursorTime, crosshairSnap, editAnnotations, creating, onCreate, onSelectAnnotation, onHoverAnnotation, onEditAnnotation, snap, timeFormat, cursorFormat, theme, discontinuities, calendar, spacing, grid, sessionDividers, children, }: ChartContainerProps): import("react/jsx-runtime").JSX.Element;
|
|
344
394
|
//# sourceMappingURL=ChartContainer.d.ts.map
|
package/dist/ChartContainer.js
CHANGED
|
@@ -4,10 +4,11 @@ import { scaleLinear } from 'd3-scale';
|
|
|
4
4
|
import { identityProvider, scaleTradingTime, } from './tradingTimeScale.js';
|
|
5
5
|
import { scaleBand } from './bandScale.js';
|
|
6
6
|
import { Sequence } from 'pond-ts';
|
|
7
|
-
import { ContainerContext, } from './context.js';
|
|
7
|
+
import { ContainerContext, CursorContext, } from './context.js';
|
|
8
8
|
import { maxSlotWidths, sum } from './slots.js';
|
|
9
9
|
import { computeLabelLanes } from './annotations.js';
|
|
10
10
|
import { resolveCursorX, DEFAULT_CURSOR_MODE } from './tracker.js';
|
|
11
|
+
import { clampToBounds } from './viewport.js';
|
|
11
12
|
import { resolveAxisFormat, resolveTimeFormat, } from './format.js';
|
|
12
13
|
import { TimeAxis } from './TimeAxis.js';
|
|
13
14
|
import { defaultTheme } from './theme.js';
|
|
@@ -43,7 +44,15 @@ function normalizeRange(range) {
|
|
|
43
44
|
* {@link TimeAxis} at the bottom, aligned under the plots. Y axes are per-row
|
|
44
45
|
* (`<YAxis>`).
|
|
45
46
|
*/
|
|
46
|
-
export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trackerPosition, onTrackerChanged, selected, onSelect, hovered, onHover, panZoom = false, onTimeRangeChange, minDuration = 1, cursor = DEFAULT_CURSOR_MODE, cursorSequence, onRegionSelect, regionSelectModifier, cursorTime = false, crosshairSnap = true, editAnnotations = false, creating = null, onCreate, onSelectAnnotation, onHoverAnnotation, onEditAnnotation, snap = true, timeFormat, cursorFormat, theme, discontinuities, calendar, spacing, grid = true, sessionDividers = 'none', children, }) {
|
|
47
|
+
export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trackerPosition, onTrackerChanged, onDrawStats, selected, onSelect, hovered, onHover, panZoom = false, bounds, onTimeRangeChange, minDuration = 1, cursor = DEFAULT_CURSOR_MODE, cursorSequence, onRegionSelect, regionSelectModifier, cursorTime = false, crosshairSnap = true, editAnnotations = false, creating = null, onCreate, onSelectAnnotation, onHoverAnnotation, onEditAnnotation, snap = true, timeFormat, cursorFormat, theme, discontinuities, calendar, spacing, grid = true, sessionDividers = 'none', children, }) {
|
|
48
|
+
// Normalize the `panZoom` mode (boolean shorthand or the three-way string)
|
|
49
|
+
// into the two gesture flags the event surface reads. `true` ⇒ both; `'pan'`
|
|
50
|
+
// ⇒ drag only; `false`/`'none'` ⇒ neither. Zoom implies pan (there is no
|
|
51
|
+
// zoom-without-pan mode), so `interactive` (holds an internal view) tracks
|
|
52
|
+
// whichever is on.
|
|
53
|
+
const panEnabled = panZoom === true || panZoom === 'pan' || panZoom === 'panZoom';
|
|
54
|
+
const zoomEnabled = panZoom === true || panZoom === 'panZoom';
|
|
55
|
+
const interactive = panEnabled || zoomEnabled;
|
|
47
56
|
// The explicit base domain from `range` (a tuple or a TimeRange). `undefined`
|
|
48
57
|
// ⇒ auto-fit (resolved from the layers below). Pan/zoom seeds from it; `seed`
|
|
49
58
|
// is the placeholder while auto-fitting.
|
|
@@ -57,7 +66,7 @@ export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trac
|
|
|
57
66
|
seed[0],
|
|
58
67
|
seed[1],
|
|
59
68
|
]);
|
|
60
|
-
const uncontrolled =
|
|
69
|
+
const uncontrolled = interactive && onTimeRangeChange === undefined;
|
|
61
70
|
// While the internal view isn't in use (not uncontrolled), keep it synced to
|
|
62
71
|
// the prop — so *entering* uncontrolled pan/zoom (toggling panZoom on, or a
|
|
63
72
|
// controlled→uncontrolled switch) starts from the current range, not the
|
|
@@ -78,12 +87,20 @@ export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trac
|
|
|
78
87
|
useLayoutEffect(() => {
|
|
79
88
|
onRangeRef.current = onTimeRangeChange;
|
|
80
89
|
});
|
|
90
|
+
// Latest `bounds` in a ref too, so `applyRange` clamps to the current extent
|
|
91
|
+
// while staying identity-stable (it's a frame field + a gesture callback dep).
|
|
92
|
+
const boundsRef = useRef(bounds);
|
|
93
|
+
useLayoutEffect(() => {
|
|
94
|
+
boundsRef.current = bounds;
|
|
95
|
+
});
|
|
81
96
|
const applyRange = useCallback((range) => {
|
|
97
|
+
const b = boundsRef.current;
|
|
98
|
+
const next = b ? clampToBounds(range, b) : range;
|
|
82
99
|
const cb = onRangeRef.current;
|
|
83
100
|
if (cb)
|
|
84
|
-
cb(
|
|
101
|
+
cb(next);
|
|
85
102
|
else
|
|
86
|
-
setInternalRange(
|
|
103
|
+
setInternalRange(next);
|
|
87
104
|
}, []);
|
|
88
105
|
// Cross-row tracker. We store the cursor's plot-pixel x (not a timestamp), so a
|
|
89
106
|
// still cursor stays put while a live window slides under it; a controlled
|
|
@@ -222,6 +239,18 @@ export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trac
|
|
|
222
239
|
}, [sources]);
|
|
223
240
|
const onTrackerRef = useRef(onTrackerChanged);
|
|
224
241
|
onTrackerRef.current = onTrackerChanged;
|
|
242
|
+
// Draw-stats sink: hold the latest `onDrawStats` in a ref and expose a *stable*
|
|
243
|
+
// reporter that reads it, so an inline arrow doesn't re-identify the context
|
|
244
|
+
// (which would thrash every row's draw memo). The reporter is `undefined` when
|
|
245
|
+
// there's no subscriber — the signal for `Layers` to skip per-layer timing
|
|
246
|
+
// entirely (zero overhead when unused). Its identity flips only when the
|
|
247
|
+
// presence of `onDrawStats` toggles, not on every render.
|
|
248
|
+
const onDrawStatsRef = useRef(onDrawStats);
|
|
249
|
+
onDrawStatsRef.current = onDrawStats;
|
|
250
|
+
const hasDrawStats = onDrawStats !== undefined;
|
|
251
|
+
const reportDrawStats = useMemo(() => hasDrawStats
|
|
252
|
+
? (frame) => onDrawStatsRef.current?.(frame)
|
|
253
|
+
: undefined, [hasDrawStats]);
|
|
225
254
|
// Selection: controlled (`selected` prop) or uncontrolled (internal). A click
|
|
226
255
|
// on a selectable layer calls `select()` after hit-testing; `onSelect` notifies
|
|
227
256
|
// in both modes, the internal state is managed only when uncontrolled. The full
|
|
@@ -531,8 +560,28 @@ export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trac
|
|
|
531
560
|
// Pack overlapping top-flag labels (markers + regions) into stacked lanes so
|
|
532
561
|
// close-in-x labels don't collide; chips read their lane back off the frame.
|
|
533
562
|
const labelLanes = useMemo(() => computeLabelLanes(annotations, (v) => xScale(v), draggingKey, plotWidth), [annotations, xScale, draggingKey]);
|
|
563
|
+
// The frame's `[d0, d1]` tuple, identity-stable on the endpoints. The frame
|
|
564
|
+
// memo rebuilds whenever any of its (many) fields change — a `hovered`
|
|
565
|
+
// transition, a selection, an annotation edit, a range change — so an inline
|
|
566
|
+
// `[d0, d1]` literal there would mint a fresh array on any such rebuild, and
|
|
567
|
+
// every draw callback listing `container.timeRange` in its deps (Layers'
|
|
568
|
+
// data-canvas draw) would read that as a domain change and replot the row
|
|
569
|
+
// canvas. Memoizing on the endpoints keeps the draw stable across those
|
|
570
|
+
// unrelated rebuilds. (Cursor *position* no longer rebuilds the frame at all —
|
|
571
|
+
// it lives in `cursorFrame` below, [PND-HOVCTX] — but the tuple stays a memo
|
|
572
|
+
// to hold the line for every other rebuild path.)
|
|
573
|
+
const timeRangeTuple = useMemo(() => [d0, d1], [d0, d1]);
|
|
574
|
+
// The per-move cursor state, split into its own context so a mousemove
|
|
575
|
+
// re-identifies only this small object — not the ~50-field frame below, which
|
|
576
|
+
// stays stable across hovers so `YAxis` / `Bar` / `Box` don't re-render. See
|
|
577
|
+
// [PND-HOVCTX] / {@link CursorContext}.
|
|
578
|
+
const cursorFrame = useMemo(() => ({
|
|
579
|
+
cursorX,
|
|
580
|
+
cursorY: hoverPoint?.y ?? null,
|
|
581
|
+
cursorRowKey: hoverPoint?.rowKey ?? null,
|
|
582
|
+
}), [cursorX, hoverPoint]);
|
|
534
583
|
const frame = useMemo(() => ({
|
|
535
|
-
timeRange:
|
|
584
|
+
timeRange: timeRangeTuple,
|
|
536
585
|
width,
|
|
537
586
|
theme: theme ?? defaultTheme,
|
|
538
587
|
plotWidth,
|
|
@@ -541,16 +590,14 @@ export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trac
|
|
|
541
590
|
leftGutter,
|
|
542
591
|
rightGutter,
|
|
543
592
|
rowGap,
|
|
544
|
-
cursorX,
|
|
545
593
|
setHoverX,
|
|
546
|
-
cursorY: hoverPoint?.y ?? null,
|
|
547
|
-
cursorRowKey: hoverPoint?.rowKey ?? null,
|
|
548
594
|
setHoverY,
|
|
549
595
|
crosshairSnap,
|
|
550
596
|
cursorBuckets,
|
|
551
597
|
regionAnchor,
|
|
552
598
|
setRegionAnchor,
|
|
553
599
|
onRegionSelect,
|
|
600
|
+
reportDrawStats,
|
|
554
601
|
regionSelectModifier,
|
|
555
602
|
draggingKey,
|
|
556
603
|
setDragging,
|
|
@@ -588,15 +635,15 @@ export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trac
|
|
|
588
635
|
discontinuities: xDiscontinuities,
|
|
589
636
|
grid,
|
|
590
637
|
sessionDividers,
|
|
591
|
-
|
|
638
|
+
panEnabled,
|
|
639
|
+
zoomEnabled,
|
|
592
640
|
minDuration,
|
|
593
641
|
applyRange,
|
|
594
642
|
registerGutter,
|
|
595
643
|
registerRow,
|
|
596
644
|
firstRowKey,
|
|
597
645
|
}), [
|
|
598
|
-
|
|
599
|
-
d1,
|
|
646
|
+
timeRangeTuple,
|
|
600
647
|
width,
|
|
601
648
|
theme,
|
|
602
649
|
plotWidth,
|
|
@@ -605,14 +652,13 @@ export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trac
|
|
|
605
652
|
leftGutter,
|
|
606
653
|
rightGutter,
|
|
607
654
|
rowGap,
|
|
608
|
-
cursorX,
|
|
609
|
-
hoverPoint,
|
|
610
655
|
setHoverY,
|
|
611
656
|
crosshairSnap,
|
|
612
657
|
cursorBuckets,
|
|
613
658
|
regionAnchor,
|
|
614
659
|
setRegionAnchor,
|
|
615
660
|
onRegionSelect,
|
|
661
|
+
reportDrawStats,
|
|
616
662
|
regionSelectModifier,
|
|
617
663
|
draggingKey,
|
|
618
664
|
setDragging,
|
|
@@ -650,20 +696,21 @@ export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trac
|
|
|
650
696
|
xDiscontinuities,
|
|
651
697
|
grid,
|
|
652
698
|
sessionDividers,
|
|
653
|
-
|
|
699
|
+
panEnabled,
|
|
700
|
+
zoomEnabled,
|
|
654
701
|
minDuration,
|
|
655
702
|
applyRange,
|
|
656
703
|
registerGutter,
|
|
657
704
|
registerRow,
|
|
658
705
|
firstRowKey,
|
|
659
706
|
]);
|
|
660
|
-
return (_jsx(ContainerContext.Provider, { value: frame, children: _jsxs("div", { style: { width: `${width}px` }, children: [_jsx("div", { style: {
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
707
|
+
return (_jsx(ContainerContext.Provider, { value: frame, children: _jsx(CursorContext.Provider, { value: cursorFrame, children: _jsxs("div", { style: { width: `${width}px` }, children: [_jsx("div", { style: {
|
|
708
|
+
display: 'flex',
|
|
709
|
+
flexDirection: 'column',
|
|
710
|
+
gap: `${rowGap}px`,
|
|
711
|
+
// The positioned ancestor for overlay chrome (`<Legend>`): the
|
|
712
|
+
// card anchors to the rows block, never the axis strip below.
|
|
713
|
+
position: 'relative',
|
|
714
|
+
}, children: children }), showAxis && _jsx(TimeAxis, {})] }) }) }));
|
|
668
715
|
}
|
|
669
716
|
//# sourceMappingURL=ChartContainer.js.map
|