@pond-ts/charts 0.49.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 +138 -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.d.ts +12 -1
- package/dist/BoxPlot.js +4 -2
- package/dist/Candlestick.d.ts +14 -1
- package/dist/Candlestick.js +4 -2
- 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 +3 -1
- package/dist/box.js +26 -6
- 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 +116 -1
- package/dist/decimate.js +251 -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 +3 -1
- package/dist/ohlc.js +26 -5
- 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/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
|
package/dist/Layers.js
CHANGED
|
@@ -6,7 +6,7 @@ import { cursorParts, bandRect, regionSpan } from './tracker.js';
|
|
|
6
6
|
import { resolveSelection } from './select.js';
|
|
7
7
|
import { panRange, zoomRange, panRangeTrading, zoomRangeTrading, } from './viewport.js';
|
|
8
8
|
import { flagChipStyle, flagChipX, axisPillX, axisPillStyle } from './chip.js';
|
|
9
|
-
import { ContainerContext, LayersContext, RowContext, } from './context.js';
|
|
9
|
+
import { ContainerContext, CursorContext, LayersContext, RowContext, } from './context.js';
|
|
10
10
|
/** Fallback **y**-gridline tick count, used only before the row publishes its
|
|
11
11
|
* resolved `tickCounts` (pre-registration). Normally the gridlines read the
|
|
12
12
|
* default axis's resolved count from `row.tickCounts` — the same value the
|
|
@@ -69,6 +69,9 @@ export function Layers({ children }) {
|
|
|
69
69
|
if (container === null) {
|
|
70
70
|
throw new Error('<Layers> must be rendered inside a <ChartContainer>');
|
|
71
71
|
}
|
|
72
|
+
// Per-move cursor state (own context, so this overlay re-renders on hover
|
|
73
|
+
// without re-identifying the container frame — [PND-HOVCTX]).
|
|
74
|
+
const cursor = useContext(CursorContext);
|
|
72
75
|
const row = useContext(RowContext);
|
|
73
76
|
if (row === null) {
|
|
74
77
|
throw new Error('<Layers> must be rendered inside a <ChartRow>');
|
|
@@ -204,11 +207,40 @@ export function Layers({ children }) {
|
|
|
204
207
|
drawDividers(ctx, thinPixels(bx, MIN_DIVIDER_PX), h, dividerColor);
|
|
205
208
|
}
|
|
206
209
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
210
|
+
// Draw the layers. When a draw-stats consumer is subscribed
|
|
211
|
+
// (`reportDrawStats` defined), time each layer and collect its reported
|
|
212
|
+
// {@link LayerDrawStats}; otherwise the plain loop with zero timing
|
|
213
|
+
// overhead. Fires one {@link DrawStatsFrame} per repaint of THIS row.
|
|
214
|
+
const report = container.reportDrawStats;
|
|
215
|
+
if (report === undefined) {
|
|
216
|
+
for (const entry of layers) {
|
|
217
|
+
const yScale = yScales.get(entry.axisId ?? defaultAxisId);
|
|
218
|
+
if (yScale === undefined)
|
|
219
|
+
continue;
|
|
220
|
+
entry.layer.draw(ctx, xScale, yScale);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
const infos = [];
|
|
225
|
+
let totalDrawMs = 0;
|
|
226
|
+
for (const entry of layers) {
|
|
227
|
+
const yScale = yScales.get(entry.axisId ?? defaultAxisId);
|
|
228
|
+
if (yScale === undefined)
|
|
229
|
+
continue;
|
|
230
|
+
const t0 = performance.now();
|
|
231
|
+
const stats = entry.layer.draw(ctx, xScale, yScale);
|
|
232
|
+
const drawMs = performance.now() - t0;
|
|
233
|
+
totalDrawMs += drawMs;
|
|
234
|
+
infos.push({
|
|
235
|
+
as: entry.layer.as,
|
|
236
|
+
index: entry.index,
|
|
237
|
+
drawMs,
|
|
238
|
+
sourceCount: stats?.sourceCount,
|
|
239
|
+
drawnCount: stats?.drawnCount,
|
|
240
|
+
decimated: stats?.decimated,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
report({ rowKey: row.rowKey, layers: infos, totalDrawMs });
|
|
212
244
|
}
|
|
213
245
|
}, [
|
|
214
246
|
layers,
|
|
@@ -222,7 +254,13 @@ export function Layers({ children }) {
|
|
|
222
254
|
gridColor,
|
|
223
255
|
gridDash,
|
|
224
256
|
container.discontinuities,
|
|
257
|
+
// Identity-stable across cursor moves (memoized tuple in ChartContainer;
|
|
258
|
+
// e2e `hover sweep never repaints the data canvas` pins this). A fresh
|
|
259
|
+
// array per frame rebuild here would re-fire the Canvas draw effect —
|
|
260
|
+
// a full replot per mousemove.
|
|
225
261
|
container.timeRange,
|
|
262
|
+
container.reportDrawStats,
|
|
263
|
+
row.rowKey,
|
|
226
264
|
]);
|
|
227
265
|
// Interaction overlay: the cursor marks live on a DOM/SVG overlay above the
|
|
228
266
|
// data, so hovering never repaints the data canvas (whose `draw` doesn't depend
|
|
@@ -230,7 +268,8 @@ export function Layers({ children }) {
|
|
|
230
268
|
// pointer is over — syncs the cursor across every row for free. cursorX is a
|
|
231
269
|
// *pixel*, so it stays put while a live window slides; the time + values under
|
|
232
270
|
// it derive from the current xScale.
|
|
233
|
-
const {
|
|
271
|
+
const { cursorTime: showCursorTime, formatTime } = container;
|
|
272
|
+
const { cursorX } = cursor;
|
|
234
273
|
// Cursor mode: the row's override, else the container default. One mode per
|
|
235
274
|
// row (the synced vertical line is shared across rows); each layer renders the
|
|
236
275
|
// mode in its own way. `parts` decomposes it into {line, dots, chip}.
|
|
@@ -392,7 +431,7 @@ export function Layers({ children }) {
|
|
|
392
431
|
if (c.cursor === 'region' &&
|
|
393
432
|
c.onRegionSelect &&
|
|
394
433
|
(c.xKind === 'time' || c.xKind === 'value')) {
|
|
395
|
-
const needsShift = c.regionSelectModifier === 'shift' && c.
|
|
434
|
+
const needsShift = c.regionSelectModifier === 'shift' && c.panEnabled;
|
|
396
435
|
if (!needsShift || e.shiftKey) {
|
|
397
436
|
const px = Math.max(0, Math.min(c.plotWidth, e.clientX - e.currentTarget.getBoundingClientRect().left));
|
|
398
437
|
regionAnchorRef.current = +c.xScale.invert(px);
|
|
@@ -408,7 +447,7 @@ export function Layers({ children }) {
|
|
|
408
447
|
}
|
|
409
448
|
// Modifier required but not held → fall through to pan.
|
|
410
449
|
}
|
|
411
|
-
if (!c.
|
|
450
|
+
if (!c.panEnabled || c.xKind === 'category')
|
|
412
451
|
return;
|
|
413
452
|
const r = c.timeRange;
|
|
414
453
|
// Arm a potential pan: record the anchor, but DON'T capture the pointer or
|
|
@@ -663,7 +702,7 @@ export function Layers({ children }) {
|
|
|
663
702
|
return;
|
|
664
703
|
const onWheel = (e) => {
|
|
665
704
|
const c = containerRef.current;
|
|
666
|
-
if (!c.
|
|
705
|
+
if (!c.zoomEnabled || c.xKind === 'category')
|
|
667
706
|
return;
|
|
668
707
|
e.preventDefault();
|
|
669
708
|
const rect = el.getBoundingClientRect();
|
|
@@ -724,14 +763,14 @@ export function Layers({ children }) {
|
|
|
724
763
|
const reticle = (() => {
|
|
725
764
|
if (parts.chip !== 'axis' || !cursorInBounds)
|
|
726
765
|
return null;
|
|
727
|
-
const hoveredRow =
|
|
728
|
-
const cy =
|
|
766
|
+
const hoveredRow = cursor.cursorRowKey === row.rowKey;
|
|
767
|
+
const cy = cursor.cursorY;
|
|
729
768
|
if (container.crosshairSnap) {
|
|
730
769
|
if (trackerSamples.length === 0)
|
|
731
770
|
return null;
|
|
732
771
|
const pick = hoveredRow && cy !== null
|
|
733
772
|
? trackerSamples.reduce((a, b) => Math.abs(b.py - cy) < Math.abs(a.py - cy) ? b : a)
|
|
734
|
-
:
|
|
773
|
+
: cursor.cursorRowKey === null
|
|
735
774
|
? trackerSamples[0]
|
|
736
775
|
: null;
|
|
737
776
|
return pick
|
|
@@ -824,7 +863,7 @@ export function Layers({ children }) {
|
|
|
824
863
|
? `inset 0 0 0 1px ${guideColor}`
|
|
825
864
|
: undefined,
|
|
826
865
|
// Let pan/zoom own touch gestures (no native scroll) when enabled.
|
|
827
|
-
touchAction: container.
|
|
866
|
+
touchAction: container.panEnabled || container.zoomEnabled ? 'none' : 'auto',
|
|
828
867
|
}, onPointerMove: handlePointerMove, onPointerDown: handlePointerDown, onPointerUp: handlePointerUp, onPointerCancel: handlePointerUp, onPointerLeave: handlePointerLeave, onClick: handleClick, children: [_jsx(Canvas, { width: plotWidth, height: row.height, draw: draw }), guideXs.length > 0 && (_jsx("svg", { width: plotWidth, height: row.height, style: {
|
|
829
868
|
position: 'absolute',
|
|
830
869
|
top: 0,
|
package/dist/Legend.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useContext } from 'react';
|
|
3
|
-
import { ContainerContext, RowContext } from './context.js';
|
|
3
|
+
import { ContainerContext, CursorContext, RowContext } from './context.js';
|
|
4
4
|
import { buildChartLegend } from './useChartLegend.js';
|
|
5
5
|
import { defaultTheme } from './theme.js';
|
|
6
6
|
/** One 20×12 swatch glyph, drawn from the layer's resolved style. */
|
|
@@ -83,6 +83,7 @@ function placementStyle(placement, leftGutter, rightGutter) {
|
|
|
83
83
|
*/
|
|
84
84
|
export function Legend({ placement = 'top-right', items, onRowClick, onRowHover, theme: themeProp, }) {
|
|
85
85
|
const container = useContext(ContainerContext);
|
|
86
|
+
const cursor = useContext(CursorContext);
|
|
86
87
|
if (container === null && items === undefined) {
|
|
87
88
|
throw new Error('<Legend> must be inside a <ChartContainer> (or be given explicit `items`)');
|
|
88
89
|
}
|
|
@@ -99,7 +100,9 @@ export function Legend({ placement = 'top-right', items, onRowClick, onRowHover,
|
|
|
99
100
|
// The shared headless core (also `useChartLegend`'s) — rows + the id-gated
|
|
100
101
|
// hover/select verbs — so the built-in card and a custom-rendered legend
|
|
101
102
|
// can never disagree. `null` in standalone `items` mode (no chart to sync).
|
|
102
|
-
const legend = container !== null
|
|
103
|
+
const legend = container !== null
|
|
104
|
+
? buildChartLegend(container, cursor, row?.rowKey)
|
|
105
|
+
: null;
|
|
103
106
|
// The card renders a flat item list — `rows` is grouped by chart row, so
|
|
104
107
|
// flatten it (a scoped legend has one group anyway).
|
|
105
108
|
const entries = items ?? legend.rows.flatMap((r) => r.items);
|
package/dist/LineChart.js
CHANGED
|
@@ -50,6 +50,7 @@ export function LineChart({ series, column, as: semantic, axis, curve, gaps = DE
|
|
|
50
50
|
}, [sessionBreaks, container.discontinuities, cs]);
|
|
51
51
|
const entry = useMemo(() => ({
|
|
52
52
|
layer: {
|
|
53
|
+
as: semantic,
|
|
53
54
|
yExtent: () => yExtent(cs),
|
|
54
55
|
// The container infers the shared x scale's kind + auto-fit domain from
|
|
55
56
|
// its layers: a ValueSeries plots on a value axis, a TimeSeries on time.
|
package/dist/ScatterChart.js
CHANGED
|
@@ -95,6 +95,7 @@ export function ScatterChart({ series, column, as: semantic, id, axis, radius, c
|
|
|
95
95
|
const keyAt = useMemo(() => (i) => cs.x[i], [cs]);
|
|
96
96
|
const entry = useMemo(() => ({
|
|
97
97
|
layer: {
|
|
98
|
+
as: semantic,
|
|
98
99
|
yExtent: () => scatterExtent(cs),
|
|
99
100
|
// The container infers the shared x scale's kind + auto-fit domain from
|
|
100
101
|
// its layers: a ValueSeries scatters on a value axis, a TimeSeries on time.
|
package/dist/XAxis.js
CHANGED
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Fragment, useContext } from 'react';
|
|
3
3
|
import { scaleLinear } from 'd3-scale';
|
|
4
4
|
import { derivedTicks } from './derivedTicks.js';
|
|
5
|
-
import { ContainerContext } from './context.js';
|
|
5
|
+
import { ContainerContext, CursorContext } from './context.js';
|
|
6
6
|
import { axisPillStyle } from './chip.js';
|
|
7
7
|
import { resolveAxisFormat, resolveTimeFormat, } from './format.js';
|
|
8
8
|
/** Tick strip height (mark + value label) in CSS px. */
|
|
@@ -60,6 +60,7 @@ export function XAxis({ format, label, side = 'bottom', height, ticks: customTic
|
|
|
60
60
|
if (container === null) {
|
|
61
61
|
throw new Error('<XAxis> must be rendered inside a <ChartContainer>');
|
|
62
62
|
}
|
|
63
|
+
const cursor = useContext(CursorContext);
|
|
63
64
|
// `xTickCount` is the container's shared x-side count — the same value the x
|
|
64
65
|
// gridlines and `formatTime` use, so labels and grid stay on the same instants
|
|
65
66
|
// (width-derived on a trading-time axis).
|
|
@@ -68,7 +69,7 @@ export function XAxis({ format, label, side = 'bottom', height, ticks: customTic
|
|
|
68
69
|
// cursor is live in-bounds, pin the hovered time to this axis (covering the
|
|
69
70
|
// tick behind it), matching the on-axis y value pills the rows draw. Gated on
|
|
70
71
|
// the container default, so a per-row `cursor` override doesn't reach here.
|
|
71
|
-
const cursorX =
|
|
72
|
+
const cursorX = cursor.cursorX;
|
|
72
73
|
const showCursorTag = container.cursor === 'crosshair' &&
|
|
73
74
|
cursorX !== null &&
|
|
74
75
|
cursorX >= 0 &&
|
package/dist/affine.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Affine-scale fast path (charts perf, [PND-AFFINE]). A chart's continuous
|
|
3
|
+
* scales — `scaleLinear` (value axis, every y axis), `scaleTime`, and the
|
|
4
|
+
* **gap-free** `scaleTradingTime(identityProvider())` (the default continuous
|
|
5
|
+
* time axis) — map data→pixels by a single `px = k·v + b`. The per-point draw
|
|
6
|
+
* loops in `drawLine` / `drawArea` can then multiply-add inline over the typed
|
|
7
|
+
* arrays instead of paying a d3-scale closure call (deinterpolate → interpolate)
|
|
8
|
+
* per point — the ~37% of stroke-bound frame self-time the 2026-07 external
|
|
9
|
+
* bench profile attributed to `scale()` (see
|
|
10
|
+
* `docs/notes/charts-bench-vs-scichart-suite-2026-07.md`, finding 1).
|
|
11
|
+
*
|
|
12
|
+
* The affine coefficients are recovered from the scale's own domain/range
|
|
13
|
+
* endpoints, then **verified affine** by probing interior points: a scale that
|
|
14
|
+
* deviates (a `scaleTradingTime` with *collapsed* gaps, or a future
|
|
15
|
+
* log/pow/sqrt axis) is rejected — the caller falls back to the exact d3-scale
|
|
16
|
+
* path — while a genuinely affine scale (including a gap-free trading axis) is
|
|
17
|
+
* accepted and reproduced to floating-point precision. The verification is what
|
|
18
|
+
* keeps the fast path a pure optimization: it never draws a non-affine scale as
|
|
19
|
+
* a straight line.
|
|
20
|
+
*/
|
|
21
|
+
import type { Scale } from './line.js';
|
|
22
|
+
/** Coefficients of an affine pixel map `px = k·value + b`. */
|
|
23
|
+
export interface Affine {
|
|
24
|
+
readonly k: number;
|
|
25
|
+
readonly b: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The affine coefficients `{ k, b }` with `scale(v) === k·v + b` for all `v`, or
|
|
29
|
+
* `null` when the scale is not affine over its domain (a real-gap
|
|
30
|
+
* `scaleTradingTime`, a non-linear axis) or exposes no numeric domain/range (a
|
|
31
|
+
* bare `(v) => v` test stub, a `scaleBand` category axis). `null` ⇒ the caller
|
|
32
|
+
* keeps the d3-scale path.
|
|
33
|
+
*
|
|
34
|
+
* Recovered from the domain/range endpoints (`k` from the two extremes, `b`
|
|
35
|
+
* pinning the low end), then verified at {@link PROBE_FRACTIONS}. Every probe
|
|
36
|
+
* must map finite and within {@link PROBE_EPSILON} of the reconstruction — so a
|
|
37
|
+
* scale that returns non-numbers for an interior value (a `scaleBand`) or bends
|
|
38
|
+
* away from the endpoint line (trading gaps, log) is rejected.
|
|
39
|
+
*/
|
|
40
|
+
export declare function affineOf(scale: Scale): Affine | null;
|
|
41
|
+
//# sourceMappingURL=affine.d.ts.map
|
package/dist/affine.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Affine-scale fast path (charts perf, [PND-AFFINE]). A chart's continuous
|
|
3
|
+
* scales — `scaleLinear` (value axis, every y axis), `scaleTime`, and the
|
|
4
|
+
* **gap-free** `scaleTradingTime(identityProvider())` (the default continuous
|
|
5
|
+
* time axis) — map data→pixels by a single `px = k·v + b`. The per-point draw
|
|
6
|
+
* loops in `drawLine` / `drawArea` can then multiply-add inline over the typed
|
|
7
|
+
* arrays instead of paying a d3-scale closure call (deinterpolate → interpolate)
|
|
8
|
+
* per point — the ~37% of stroke-bound frame self-time the 2026-07 external
|
|
9
|
+
* bench profile attributed to `scale()` (see
|
|
10
|
+
* `docs/notes/charts-bench-vs-scichart-suite-2026-07.md`, finding 1).
|
|
11
|
+
*
|
|
12
|
+
* The affine coefficients are recovered from the scale's own domain/range
|
|
13
|
+
* endpoints, then **verified affine** by probing interior points: a scale that
|
|
14
|
+
* deviates (a `scaleTradingTime` with *collapsed* gaps, or a future
|
|
15
|
+
* log/pow/sqrt axis) is rejected — the caller falls back to the exact d3-scale
|
|
16
|
+
* path — while a genuinely affine scale (including a gap-free trading axis) is
|
|
17
|
+
* accepted and reproduced to floating-point precision. The verification is what
|
|
18
|
+
* keeps the fast path a pure optimization: it never draws a non-affine scale as
|
|
19
|
+
* a straight line.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Irregular interior sample fractions for the affinity probe. Deliberately not
|
|
23
|
+
* `[0.25, 0.5, 0.75]` — a piecewise-linear scale (trading time) can have
|
|
24
|
+
* breakpoints that a symmetric, round-fraction probe set slips between; the
|
|
25
|
+
* jittered spread makes a false "affine" verdict on a real-gap scale
|
|
26
|
+
* astronomically unlikely (and the e2e visual-regression layer is the backstop,
|
|
27
|
+
* the same net that guards M4).
|
|
28
|
+
*/
|
|
29
|
+
const PROBE_FRACTIONS = [0.1213, 0.2857, 0.4391, 0.6137, 0.7649, 0.8831];
|
|
30
|
+
/**
|
|
31
|
+
* Pixel tolerance for the affinity probe. Far below a sub-pixel (so a real
|
|
32
|
+
* non-affine deviation — a collapsed trading gap or a log curve is many pixels)
|
|
33
|
+
* yet far above the float-reconstruction noise of `k·v + b` on wide domains
|
|
34
|
+
* (~1e-9 px), so an exactly-affine scale is never rejected.
|
|
35
|
+
*/
|
|
36
|
+
const PROBE_EPSILON = 1e-3;
|
|
37
|
+
/**
|
|
38
|
+
* The affine coefficients `{ k, b }` with `scale(v) === k·v + b` for all `v`, or
|
|
39
|
+
* `null` when the scale is not affine over its domain (a real-gap
|
|
40
|
+
* `scaleTradingTime`, a non-linear axis) or exposes no numeric domain/range (a
|
|
41
|
+
* bare `(v) => v` test stub, a `scaleBand` category axis). `null` ⇒ the caller
|
|
42
|
+
* keeps the d3-scale path.
|
|
43
|
+
*
|
|
44
|
+
* Recovered from the domain/range endpoints (`k` from the two extremes, `b`
|
|
45
|
+
* pinning the low end), then verified at {@link PROBE_FRACTIONS}. Every probe
|
|
46
|
+
* must map finite and within {@link PROBE_EPSILON} of the reconstruction — so a
|
|
47
|
+
* scale that returns non-numbers for an interior value (a `scaleBand`) or bends
|
|
48
|
+
* away from the endpoint line (trading gaps, log) is rejected.
|
|
49
|
+
*/
|
|
50
|
+
export function affineOf(scale) {
|
|
51
|
+
const s = scale;
|
|
52
|
+
const d = s.domain?.();
|
|
53
|
+
const r = s.range?.();
|
|
54
|
+
if (d === undefined || r === undefined || d.length < 2 || r.length < 2) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const lo = +d[0];
|
|
58
|
+
const hi = +d[d.length - 1];
|
|
59
|
+
if (!Number.isFinite(lo) || !Number.isFinite(hi) || lo === hi)
|
|
60
|
+
return null;
|
|
61
|
+
const pLo = scale(lo);
|
|
62
|
+
const pHi = scale(hi);
|
|
63
|
+
if (!Number.isFinite(pLo) || !Number.isFinite(pHi))
|
|
64
|
+
return null;
|
|
65
|
+
const k = (pHi - pLo) / (hi - lo);
|
|
66
|
+
const b = pLo - k * lo;
|
|
67
|
+
const span = hi - lo;
|
|
68
|
+
for (const t of PROBE_FRACTIONS) {
|
|
69
|
+
const v = lo + t * span;
|
|
70
|
+
const p = scale(v);
|
|
71
|
+
if (!Number.isFinite(p) || Math.abs(p - (k * v + b)) > PROBE_EPSILON) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return { k, b };
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=affine.js.map
|
package/dist/area.d.ts
CHANGED
|
@@ -1,9 +1,27 @@
|
|
|
1
1
|
import { type CurveFactory } from 'd3-shape';
|
|
2
2
|
import type { ChartSeries } from './data.js';
|
|
3
|
-
import type
|
|
3
|
+
import { type Scale } from './line.js';
|
|
4
4
|
import type { AreaStyle } from './theme.js';
|
|
5
|
+
import type { LayerDrawStats } from './context.js';
|
|
5
6
|
import { type GapMode } from './gaps.js';
|
|
6
7
|
import { type DecimateOption } from './decimate.js';
|
|
8
|
+
import { type Affine } from './affine.js';
|
|
9
|
+
export declare function columnFiniteExtent(y: Float64Array, length: number): readonly [number, number] | null;
|
|
10
|
+
/**
|
|
11
|
+
* Fill the area between an affine-mapped value polyline and a constant baseline
|
|
12
|
+
* pixel — the [PND-AFFINE] fast path for {@link drawArea}'s fill, the counterpart
|
|
13
|
+
* to {@link strokeAffinePolyline} for its outline. Emits one **independent closed
|
|
14
|
+
* polygon per finite run** (matching `d3.area`'s `.defined(Number.isFinite)`
|
|
15
|
+
* segmentation for a linear curve + constant `y0`): per run `[a, b)`,
|
|
16
|
+
* `moveTo(top_a)` → `lineTo(top…)` along the value edge → `lineTo(x_{b-1}, base)`
|
|
17
|
+
* → `lineTo(x_a, base)` → `closePath`. That is the same filled region `d3.area`
|
|
18
|
+
* draws — its flat backward baseline edge only adds collinear interior vertices,
|
|
19
|
+
* which don't change the fill — without the per-point `scale()` / d3-shape
|
|
20
|
+
* closures. A signed value edge crossing the baseline stays one polygon (no NaN),
|
|
21
|
+
* filled correctly on both sides. The caller brackets `beginPath`/`fill`;
|
|
22
|
+
* `xs`/`ys` are aligned index-for-index.
|
|
23
|
+
*/
|
|
24
|
+
export declare function fillAffineArea(ctx: CanvasRenderingContext2D, xs: Float64Array, ys: Float64Array, baselinePx: number, ax: Affine, ay: Affine): void;
|
|
7
25
|
/**
|
|
8
26
|
* The `[min, max]` vertical extent an area occupies — the finite values of
|
|
9
27
|
* `cs.y` widened to include `baseline`, since the fill spans from each value to
|
|
@@ -51,5 +69,5 @@ export declare function areaExtent(cs: ChartSeries, baseline: number | undefined
|
|
|
51
69
|
* bracketed by `save`/`restore` so they don't leak into later layers. Gap edges
|
|
52
70
|
* are collected by one O(N) walk ({@link collectGapEdges}).
|
|
53
71
|
*/
|
|
54
|
-
export declare function drawArea(ctx: CanvasRenderingContext2D, cs: ChartSeries, xScale: Scale, yScale: Scale, style: AreaStyle, baselineValue: number, curve?: CurveFactory, gaps?: GapMode, gapConnectorOpacity?: number, decimate?: DecimateOption):
|
|
72
|
+
export declare function drawArea(ctx: CanvasRenderingContext2D, cs: ChartSeries, xScale: Scale, yScale: Scale, style: AreaStyle, baselineValue: number, curve?: CurveFactory, gaps?: GapMode, gapConnectorOpacity?: number, decimate?: DecimateOption): LayerDrawStats;
|
|
55
73
|
//# sourceMappingURL=area.d.ts.map
|