@pond-ts/charts 0.62.0 → 0.63.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 +101 -1
- package/dist/ChartContainer.d.ts +72 -0
- package/dist/ChartContainer.js +151 -24
- package/dist/ChartRow.d.ts +39 -3
- package/dist/ChartRow.js +194 -7
- package/dist/Layers.js +16 -2
- package/dist/XAxis.d.ts +6 -0
- package/dist/XAxis.js +80 -2
- package/dist/YAxis.d.ts +66 -2
- package/dist/YAxis.js +141 -4
- package/dist/annotations.js +14 -7
- package/dist/chip.d.ts +50 -7
- package/dist/chip.js +66 -9
- package/dist/context.d.ts +121 -4
- package/dist/cursors.js +33 -8
- package/dist/domain.d.ts +18 -0
- package/dist/domain.js +33 -0
- package/dist/use-axis-gestures.d.ts +79 -0
- package/dist/use-axis-gestures.js +259 -0
- package/package.json +3 -3
package/dist/ChartRow.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Children, Fragment, isValidElement, useCallback, useContext, useEffect, useMemo, useRef, useState, } from 'react';
|
|
2
|
+
import { Children, Fragment, isValidElement, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
|
|
3
3
|
import { scaleLinear, scaleLog, scaleSymlog } from 'd3-scale';
|
|
4
4
|
import { isDev } from './dev.js';
|
|
5
5
|
import { useIndexedChildren } from './child-index.js';
|
|
@@ -90,6 +90,11 @@ function axisSpecEqual(a, b) {
|
|
|
90
90
|
a.labelPlacement === b.labelPlacement &&
|
|
91
91
|
a.index === b.index &&
|
|
92
92
|
a.tickCount === b.tickCount &&
|
|
93
|
+
// The axis-edge chrome drawn *by the row* wears this (the crosshair's value
|
|
94
|
+
// pill), so a swallowed colour change would leave the pill on the old ink
|
|
95
|
+
// while the axis's own labels repainted — the same silent-staleness the
|
|
96
|
+
// `linearWindow` note above warns about.
|
|
97
|
+
a.color === b.color &&
|
|
93
98
|
Object.is(a.format, b.format) &&
|
|
94
99
|
numberArraysEqual(a.tickValues, b.tickValues));
|
|
95
100
|
}
|
|
@@ -134,11 +139,73 @@ const TOP_LABEL_HEADER = 16;
|
|
|
134
139
|
* Children lay out left-to-right in author order, so `<YAxis side="left"/>` goes
|
|
135
140
|
* before `<Layers/>` and `<YAxis side="right"/>` after.
|
|
136
141
|
*/
|
|
137
|
-
export function ChartRow({ height, cursor, children }) {
|
|
142
|
+
export function ChartRow({ height: heightProp, flex, cursor, children, }) {
|
|
138
143
|
const container = useContext(ContainerContext);
|
|
139
144
|
if (container === null) {
|
|
140
145
|
throw new Error('<ChartRow> must be rendered inside a <ChartContainer>');
|
|
141
146
|
}
|
|
147
|
+
// ── Sizing mode ([PND-HEIGHT]) ─────────────────────────────────────────────
|
|
148
|
+
// Fixed (`height` in px) or flex (`flex`, the default when neither is
|
|
149
|
+
// given). A flex row's box is sized by CSS (`flex: <n> 1 0` inside the
|
|
150
|
+
// container's column), and the row reads back what layout gave it — the
|
|
151
|
+
// browser subtracts the axis strip, fixed siblings, splitters and row gaps,
|
|
152
|
+
// so no caller (and no code here) ever knows what those cost.
|
|
153
|
+
const isFlex = heightProp === undefined;
|
|
154
|
+
const flexGrow = flex ?? 1;
|
|
155
|
+
const warnedSizingRef = useRef(false);
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
if (!isDev || warnedSizingRef.current)
|
|
158
|
+
return;
|
|
159
|
+
if (heightProp !== undefined && flex !== undefined) {
|
|
160
|
+
// Both given is a contradiction, not a tiebreak: warn and honour
|
|
161
|
+
// `height` (the long-standing prop) so the chart renders
|
|
162
|
+
// deterministically.
|
|
163
|
+
warnedSizingRef.current = true;
|
|
164
|
+
console.warn(`[pond-charts] <ChartRow> got both height={${heightProp}} and ` +
|
|
165
|
+
`flex={${flex}} — they are alternative sizing modes. Using ` +
|
|
166
|
+
`height; drop one.`);
|
|
167
|
+
}
|
|
168
|
+
}, [heightProp, flex]);
|
|
169
|
+
const boxRef = useRef(null);
|
|
170
|
+
const [measured, setMeasured] = useState(0);
|
|
171
|
+
useLayoutEffect(() => {
|
|
172
|
+
if (!isFlex)
|
|
173
|
+
return;
|
|
174
|
+
const el = boxRef.current;
|
|
175
|
+
if (el === null)
|
|
176
|
+
return;
|
|
177
|
+
const measure = () => setMeasured((prev) => {
|
|
178
|
+
const next = Math.round(el.getBoundingClientRect().height);
|
|
179
|
+
// Latch the last non-zero height — a hidden row (display: none
|
|
180
|
+
// ancestor) is not a resized one, and writing 0 through would tear
|
|
181
|
+
// down its scales. Same rule as the container's width="auto".
|
|
182
|
+
return next > 0 ? next : prev;
|
|
183
|
+
});
|
|
184
|
+
measure();
|
|
185
|
+
if (typeof ResizeObserver === 'undefined')
|
|
186
|
+
return;
|
|
187
|
+
const ro = new ResizeObserver(measure);
|
|
188
|
+
ro.observe(el);
|
|
189
|
+
return () => ro.disconnect();
|
|
190
|
+
}, [isFlex]);
|
|
191
|
+
// A flex row inside a container that doesn't manage height has nothing to
|
|
192
|
+
// flex into: its box is 0 and stays 0, silently. That is a wiring error the
|
|
193
|
+
// consumer should hear about now, not a slow start to wait through.
|
|
194
|
+
const warnedNoHeightRef = useRef(false);
|
|
195
|
+
useEffect(() => {
|
|
196
|
+
if (!isDev ||
|
|
197
|
+
!isFlex ||
|
|
198
|
+
container.managesHeight ||
|
|
199
|
+
warnedNoHeightRef.current) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
warnedNoHeightRef.current = true;
|
|
203
|
+
console.warn(`[pond-charts] <ChartRow flex> needs a container that manages height ` +
|
|
204
|
+
`— give <ChartContainer> a height ({number | 'auto'}). Inside a ` +
|
|
205
|
+
`container with no height this row's box collapses to 0 and never ` +
|
|
206
|
+
`paints.`);
|
|
207
|
+
}, [isFlex, container.managesHeight]);
|
|
208
|
+
const height = isFlex ? measured : heightProp;
|
|
142
209
|
// Register on mount so the container can mark the first (topmost) row by
|
|
143
210
|
// mount order — the shared cursor-time chip renders there only.
|
|
144
211
|
const rowKey = useSlotKey();
|
|
@@ -165,6 +232,33 @@ export function ChartRow({ height, cursor, children }) {
|
|
|
165
232
|
// min/max or series change silently rebind axes / reorder the z-stack.)
|
|
166
233
|
const [axes, setAxes] = useState(() => new Map());
|
|
167
234
|
const [layers, setLayers] = useState(() => new Map());
|
|
235
|
+
// Per-axis pixel zoom — a drag on one gutter (see `RowFrame.axisTransforms`).
|
|
236
|
+
// Empty until an axis is actually grabbed, so a chart with no axis gestures
|
|
237
|
+
// carries no extra state and the identity branch below skips the work.
|
|
238
|
+
// Filled by the scale memo below (see `baseYScales` on the frame).
|
|
239
|
+
const baseRef = useRef(new Map());
|
|
240
|
+
const [axisTransforms, setAxisTransforms] = useState(() => new Map());
|
|
241
|
+
const applyAxisTransform = useCallback((id, next) => {
|
|
242
|
+
setAxisTransforms((prev) => {
|
|
243
|
+
const cur = prev.get(id);
|
|
244
|
+
if (cur !== undefined && cur.k === next.k && cur.ty === next.ty) {
|
|
245
|
+
return prev; // no-op: don't re-render (a wheel notch at a clamp)
|
|
246
|
+
}
|
|
247
|
+
const map = new Map(prev);
|
|
248
|
+
// Identity is the absence of a transform, not an entry recording one —
|
|
249
|
+
// so the reset genuinely returns the axis to the un-grabbed state and
|
|
250
|
+
// the scale memo's fast path applies again.
|
|
251
|
+
if (next.k === 1 && next.ty === 0) {
|
|
252
|
+
if (cur === undefined)
|
|
253
|
+
return prev;
|
|
254
|
+
map.delete(id);
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
map.set(id, next);
|
|
258
|
+
}
|
|
259
|
+
return map;
|
|
260
|
+
});
|
|
261
|
+
}, []);
|
|
168
262
|
// Registration is idempotent under value-equality: a `<YAxis>` re-fires its
|
|
169
263
|
// register effect whenever its `spec` memo yields a fresh object — which an
|
|
170
264
|
// inline `ticks={[]}` / `format` or a re-rendered parent does every render. If
|
|
@@ -234,6 +328,7 @@ export function ChartRow({ height, cursor, children }) {
|
|
|
234
328
|
format: undefined,
|
|
235
329
|
tickValues: undefined,
|
|
236
330
|
tickCount: undefined,
|
|
331
|
+
color: undefined,
|
|
237
332
|
index: 0,
|
|
238
333
|
},
|
|
239
334
|
], [realAxes]);
|
|
@@ -278,6 +373,7 @@ export function ChartRow({ height, cursor, children }) {
|
|
|
278
373
|
const { k: yk, ty: yty } = container.yTransform;
|
|
279
374
|
const yScales = useMemo(() => {
|
|
280
375
|
const map = new Map();
|
|
376
|
+
const bases = new Map();
|
|
281
377
|
for (const ax of effectiveAxes) {
|
|
282
378
|
const extents = needsExtents(ax)
|
|
283
379
|
? layerList
|
|
@@ -315,14 +411,43 @@ export function ChartRow({ height, cursor, children }) {
|
|
|
315
411
|
// each other in the first cut. Narrowing the domain means ticks, padding
|
|
316
412
|
// and every downstream reader see an ordinary axis over the visible
|
|
317
413
|
// window, and none of them need to know a transform exists.
|
|
318
|
-
|
|
319
|
-
|
|
414
|
+
//
|
|
415
|
+
// The **per-axis** transform (an axis-gutter drag) is applied the same
|
|
416
|
+
// way, immediately after — so a doubly-transformed axis is still just an
|
|
417
|
+
// ordinary axis over the doubly-narrowed window. Sequential rather than
|
|
418
|
+
// pre-composed on purpose: each step inverts through the scale it is
|
|
419
|
+
// actually narrowing, which is what keeps it correct on a log / symlog
|
|
420
|
+
// axis, where pixel→value is not affine and two composed `k`s would not
|
|
421
|
+
// land where two applications do.
|
|
422
|
+
const narrow = (k, ty) => {
|
|
423
|
+
const at = (px) => +s.invert((px - ty) / k);
|
|
320
424
|
s.domain([at(height), at(topHeader)]);
|
|
425
|
+
};
|
|
426
|
+
// Snapshot before either transform: `baseYScales` is the domain the axis
|
|
427
|
+
// RESOLVED to (bounds + pad + nice), which is the space a controlled
|
|
428
|
+
// consumer's `min`/`max` live in. Reading the transformed scale instead and
|
|
429
|
+
// handing those values back re-applies the transform on top of them.
|
|
430
|
+
bases.set(ax.id, s.copy());
|
|
431
|
+
if (yk !== 1 || yty !== 0)
|
|
432
|
+
narrow(yk, yty);
|
|
433
|
+
const own = axisTransforms.get(ax.id);
|
|
434
|
+
if (own !== undefined && (own.k !== 1 || own.ty !== 0)) {
|
|
435
|
+
narrow(own.k, own.ty);
|
|
321
436
|
}
|
|
322
437
|
map.set(ax.id, s);
|
|
323
438
|
}
|
|
439
|
+
baseRef.current = bases;
|
|
324
440
|
return map;
|
|
325
|
-
}, [
|
|
441
|
+
}, [
|
|
442
|
+
effectiveAxes,
|
|
443
|
+
layerList,
|
|
444
|
+
height,
|
|
445
|
+
defaultAxisId,
|
|
446
|
+
topHeader,
|
|
447
|
+
yk,
|
|
448
|
+
yty,
|
|
449
|
+
axisTransforms,
|
|
450
|
+
]);
|
|
326
451
|
// Dev-mode diagnostics for a `scale="log"` axis (see `logAxisWarning`). Three
|
|
327
452
|
// things about *where* this sits are load-bearing, each of them a bug the
|
|
328
453
|
// first version shipped:
|
|
@@ -451,6 +576,53 @@ export function ChartRow({ height, cursor, children }) {
|
|
|
451
576
|
map.set(ax.id, ax.side);
|
|
452
577
|
return map;
|
|
453
578
|
}, [effectiveAxes]);
|
|
579
|
+
// How far out in its gutter each axis sits: the px from the plot's edge to the
|
|
580
|
+
// axis's inner edge, walking each side plot-outward and accumulating the
|
|
581
|
+
// *reserved* slot widths (what the axis boxes actually render at, so the
|
|
582
|
+
// offset lands on the axis and not between two of them). Side alone would put
|
|
583
|
+
// every pill on the innermost axis — the wrong scale as soon as a side carries
|
|
584
|
+
// two (see RowFrame.axisOffsets). Right axes are authored inner→outer, left
|
|
585
|
+
// axes outer→inner, so the left list walks in reverse.
|
|
586
|
+
//
|
|
587
|
+
// Resolved per **instance** first, then collapsed to ids by the same
|
|
588
|
+
// last-declared-wins rule `axisSides` uses — so for a mirrored id (one scale
|
|
589
|
+
// registered on both sides, or a duplicate) the side and the offset always
|
|
590
|
+
// come from the *same* axis. A different rule per map (say, keeping the
|
|
591
|
+
// smallest offset) can pair one instance's side with another's offset, which
|
|
592
|
+
// is the very "pill on an axis that didn't measure it" this fixes.
|
|
593
|
+
const axisOffsets = useMemo(() => {
|
|
594
|
+
const byInstance = new Map();
|
|
595
|
+
const walk = (side) => {
|
|
596
|
+
const inward = realEntries.filter(([, spec]) => spec.side === side);
|
|
597
|
+
if (side === 'left')
|
|
598
|
+
inward.reverse();
|
|
599
|
+
let offset = 0;
|
|
600
|
+
for (const [key, spec] of inward) {
|
|
601
|
+
byInstance.set(key, offset);
|
|
602
|
+
offset += axisSlots.get(key) ?? spec.width;
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
walk('right');
|
|
606
|
+
walk('left');
|
|
607
|
+
// realEntries is index-sorted, the order effectiveAxes (and so axisSides)
|
|
608
|
+
// walks — hence the same winner on a repeated id.
|
|
609
|
+
const map = new Map();
|
|
610
|
+
for (const [key, spec] of realEntries) {
|
|
611
|
+
map.set(spec.id, byInstance.get(key) ?? 0);
|
|
612
|
+
}
|
|
613
|
+
return map;
|
|
614
|
+
}, [realEntries, axisSlots]);
|
|
615
|
+
// Each axis's own ink (`<YAxis color>`) — the axis-edge pill that lands on an
|
|
616
|
+
// axis wears its colour, so with several axes the number says which scale it
|
|
617
|
+
// is on. Axes that set no colour are absent (the pill falls back to theme).
|
|
618
|
+
const axisColors = useMemo(() => {
|
|
619
|
+
const map = new Map();
|
|
620
|
+
for (const ax of effectiveAxes) {
|
|
621
|
+
if (ax.color !== undefined)
|
|
622
|
+
map.set(ax.id, ax.color);
|
|
623
|
+
}
|
|
624
|
+
return map;
|
|
625
|
+
}, [effectiveAxes]);
|
|
454
626
|
const frame = useMemo(() => ({
|
|
455
627
|
height,
|
|
456
628
|
topInset: topHeader,
|
|
@@ -458,10 +630,15 @@ export function ChartRow({ height, cursor, children }) {
|
|
|
458
630
|
isFirstRow,
|
|
459
631
|
rowKey,
|
|
460
632
|
yScales,
|
|
633
|
+
baseYScales: baseRef.current,
|
|
634
|
+
axisTransforms,
|
|
635
|
+
applyAxisTransform,
|
|
461
636
|
formats,
|
|
462
637
|
tickValues,
|
|
463
638
|
tickCounts,
|
|
464
639
|
axisSides,
|
|
640
|
+
axisOffsets,
|
|
641
|
+
axisColors,
|
|
465
642
|
defaultAxisId,
|
|
466
643
|
axisSlots,
|
|
467
644
|
registerAxis,
|
|
@@ -476,10 +653,14 @@ export function ChartRow({ height, cursor, children }) {
|
|
|
476
653
|
isFirstRow,
|
|
477
654
|
rowKey,
|
|
478
655
|
yScales,
|
|
656
|
+
axisTransforms,
|
|
657
|
+
applyAxisTransform,
|
|
479
658
|
formats,
|
|
480
659
|
tickValues,
|
|
481
660
|
tickCounts,
|
|
482
661
|
axisSides,
|
|
662
|
+
axisOffsets,
|
|
663
|
+
axisColors,
|
|
483
664
|
defaultAxisId,
|
|
484
665
|
axisSlots,
|
|
485
666
|
registerAxis,
|
|
@@ -556,11 +737,17 @@ export function ChartRow({ height, cursor, children }) {
|
|
|
556
737
|
"wrap the row's <Layers>, leaving each <YAxis> a direct child of the " +
|
|
557
738
|
'<ChartRow>.');
|
|
558
739
|
}, [axisInsideWrapper]);
|
|
559
|
-
return (_jsxs(RowContext.Provider, { value: frame, children: [cursor !== undefined && (_jsx(LegacyCursor, { mode: cursor, showTime: container.cursorTime, snap: container.crosshairSnap })), _jsxs("div", { style: {
|
|
740
|
+
return (_jsxs(RowContext.Provider, { value: frame, children: [cursor !== undefined && (_jsx(LegacyCursor, { mode: cursor, showTime: container.cursorTime, snap: container.crosshairSnap })), _jsxs("div", { ref: boxRef, style: {
|
|
560
741
|
display: 'flex',
|
|
561
742
|
flexDirection: 'row',
|
|
562
743
|
width: `${container.width}px`,
|
|
563
|
-
|
|
744
|
+
// Fixed rows keep their pixels; a flex row is sized by the
|
|
745
|
+
// container's column layout and reads the result back
|
|
746
|
+
// ([PND-HEIGHT]). `minHeight: 0` is what lets it actually shrink —
|
|
747
|
+
// a flex child's default min-height is its content.
|
|
748
|
+
...(isFlex
|
|
749
|
+
? { flex: `${flexGrow} 1 0%`, minHeight: 0 }
|
|
750
|
+
: { height: `${height}px` }),
|
|
564
751
|
}, children: [leftPad > 0 && _jsx("div", { style: { flex: `0 0 ${leftPad}px` } }), leftAxisEls, plotEls, rightAxisEls, rightPad > 0 && _jsx("div", { style: { flex: `0 0 ${rightPad}px` } })] })] }));
|
|
565
752
|
}
|
|
566
753
|
//# sourceMappingURL=ChartRow.js.map
|
package/dist/Layers.js
CHANGED
|
@@ -207,7 +207,7 @@ export function Layers({ children }) {
|
|
|
207
207
|
}), [row.registerLayer, row.unregisterLayer]);
|
|
208
208
|
const background = container.theme.background;
|
|
209
209
|
const { grid: gridColor, gridDash } = container.theme.axis;
|
|
210
|
-
const { layers, yScales, formats, defaultAxisId, tickValues, tickCounts, axisSides, } = row;
|
|
210
|
+
const { layers, yScales, formats, defaultAxisId, tickValues, tickCounts, axisSides, axisOffsets, axisColors, } = row;
|
|
211
211
|
// x geometry is shared and lives on the container (uniform across rows), and
|
|
212
212
|
// so is the x tick count — vertical gridlines must sit under the `<XAxis>`
|
|
213
213
|
// labels, which pass the same `xTickCount` to the same scale.
|
|
@@ -516,14 +516,20 @@ export function Layers({ children }) {
|
|
|
516
516
|
// The chip uses this layer's axis formatter, so a readout value reads
|
|
517
517
|
// exactly as the axis labels it.
|
|
518
518
|
const fmt = formats.get(axisId) ?? String;
|
|
519
|
-
//
|
|
519
|
+
// Where the crosshair value pill lands, and in what ink: this axis's own
|
|
520
|
+
// side, its offset out into that gutter (so the pill sits on the axis that
|
|
521
|
+
// measured the value, not the innermost one), and its `<YAxis color>`.
|
|
520
522
|
const side = axisSides.get(axisId) ?? 'left';
|
|
523
|
+
const axisOffset = axisOffsets.get(axisId) ?? 0;
|
|
524
|
+
const axisColor = axisColors.get(axisId);
|
|
521
525
|
for (const s of entry.layer.sampleAt(cursorTime)) {
|
|
522
526
|
out.push({
|
|
523
527
|
px: xScale(s.x),
|
|
524
528
|
py: yScale(s.value),
|
|
525
529
|
axisId,
|
|
526
530
|
side,
|
|
531
|
+
axisOffset,
|
|
532
|
+
axisColor,
|
|
527
533
|
formatted: fmt(s.value),
|
|
528
534
|
color: s.color,
|
|
529
535
|
label: s.label,
|
|
@@ -538,6 +544,8 @@ export function Layers({ children }) {
|
|
|
538
544
|
yScales,
|
|
539
545
|
formats,
|
|
540
546
|
axisSides,
|
|
547
|
+
axisOffsets,
|
|
548
|
+
axisColors,
|
|
541
549
|
xScale,
|
|
542
550
|
defaultAxisId,
|
|
543
551
|
]);
|
|
@@ -1539,6 +1547,10 @@ export function Layers({ children }) {
|
|
|
1539
1547
|
py: cursor.cursorY,
|
|
1540
1548
|
formatted: fmt(ys.invert(cursor.cursorY)),
|
|
1541
1549
|
side: axisSides.get(defaultAxisId) ?? 'left',
|
|
1550
|
+
// The free reticle reads the row's *default* axis, so its pill belongs on
|
|
1551
|
+
// that axis — at its offset, in its ink — like a snapped sample's.
|
|
1552
|
+
axisOffset: axisOffsets.get(defaultAxisId) ?? 0,
|
|
1553
|
+
axisColor: axisColors.get(defaultAxisId),
|
|
1542
1554
|
};
|
|
1543
1555
|
}, [
|
|
1544
1556
|
wantsPointer,
|
|
@@ -1548,6 +1560,8 @@ export function Layers({ children }) {
|
|
|
1548
1560
|
yScales,
|
|
1549
1561
|
formats,
|
|
1550
1562
|
axisSides,
|
|
1563
|
+
axisOffsets,
|
|
1564
|
+
axisColors,
|
|
1551
1565
|
defaultAxisId,
|
|
1552
1566
|
]);
|
|
1553
1567
|
// The in-plot cursor time, readout-formatted — the `showTime` presets' chip
|
package/dist/XAxis.d.ts
CHANGED
|
@@ -133,6 +133,12 @@ export interface XAxisProps {
|
|
|
133
133
|
* numbers, with no axis-type prop here; the kind follows the data.
|
|
134
134
|
*
|
|
135
135
|
* `<TimeAxis>` is the time-flavoured preset (`<XAxis />`).
|
|
136
|
+
*
|
|
137
|
+
* **Gestures.** With `<ChartContainer axisPanZoom="x">` (or `"xy"`) the strip is a
|
|
138
|
+
* second handle on the canvas gesture: it **pans on drag** and **zooms on
|
|
139
|
+
* wheel**, with double-click returning to the declared `range`. Same maths as the
|
|
140
|
+
* plot's own drag, including `bounds` / `minDuration` and the trading calendar.
|
|
141
|
+
* A category axis has no continuous domain and stays inert.
|
|
136
142
|
*/
|
|
137
143
|
export declare function XAxis({ format, label, side, height, ticks: customTicks, transform, color, align, dateStyle, onMouseEvent, }?: XAxisProps): import("react/jsx-runtime").JSX.Element;
|
|
138
144
|
export {};
|
package/dist/XAxis.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Fragment, useContext } from 'react';
|
|
2
|
+
import { Fragment, useContext, useRef } from 'react';
|
|
3
3
|
import { scaleLinear } from 'd3-scale';
|
|
4
4
|
import { derivedTicks } from './derivedTicks.js';
|
|
5
5
|
import { ContainerContext, CursorContext, } from './context.js';
|
|
@@ -8,6 +8,8 @@ import { xAxisCursorEntries } from './cursors.js';
|
|
|
8
8
|
import { axisPillStyle } from './chip.js';
|
|
9
9
|
import { resolveAxisFormat, resolveTimeFormat, } from './format.js';
|
|
10
10
|
import { axisMouseProps, axisPointerPx, } from './axis-events.js';
|
|
11
|
+
import { useAxisGestures } from './use-axis-gestures.js';
|
|
12
|
+
import { panRange, panRangeTrading, zoomRange, zoomRangeTrading, } from './viewport.js';
|
|
11
13
|
/** Tick strip height (mark + value label) in CSS px. */
|
|
12
14
|
const TICK_STRIP = 22;
|
|
13
15
|
/** Extra height reserved for an axis `label` line. */
|
|
@@ -157,6 +159,12 @@ export function thinCategoryLabels(ticks, slot, plotWidth, fontSize, fontFamily)
|
|
|
157
159
|
* numbers, with no axis-type prop here; the kind follows the data.
|
|
158
160
|
*
|
|
159
161
|
* `<TimeAxis>` is the time-flavoured preset (`<XAxis />`).
|
|
162
|
+
*
|
|
163
|
+
* **Gestures.** With `<ChartContainer axisPanZoom="x">` (or `"xy"`) the strip is a
|
|
164
|
+
* second handle on the canvas gesture: it **pans on drag** and **zooms on
|
|
165
|
+
* wheel**, with double-click returning to the declared `range`. Same maths as the
|
|
166
|
+
* plot's own drag, including `bounds` / `minDuration` and the trading calendar.
|
|
167
|
+
* A category axis has no continuous domain and stays inert.
|
|
160
168
|
*/
|
|
161
169
|
export function XAxis({ format, label, side = 'bottom', height, ticks: customTicks, transform, color, align = 'center', dateStyle = 'flat', onMouseEvent, } = {}) {
|
|
162
170
|
const container = useContext(ContainerContext);
|
|
@@ -417,14 +425,84 @@ export function XAxis({ format, label, side = 'bottom', height, ticks: customTic
|
|
|
417
425
|
// scale — no gutter arithmetic. The label reads the same channel a cursor
|
|
418
426
|
// pill does: the band scale's category name on a category axis (a d3 number
|
|
419
427
|
// format can't name one), this axis's readout format everywhere else.
|
|
428
|
+
// The view the current pan started from — see `onDragStart`.
|
|
429
|
+
const panStartRef = useRef(null);
|
|
430
|
+
// Drag pans and wheel zooms the shared x view, double-click returns to the
|
|
431
|
+
// declared range. Enabled by the container's own `panZoom` zoom-x degree of
|
|
432
|
+
// freedom — a chart that never opted in captures nothing here.
|
|
433
|
+
//
|
|
434
|
+
// A **category** axis has no continuous domain to zoom, exactly as for the
|
|
435
|
+
// plot's gesture (`Layers`' wheel makes the same exclusion), so the strip
|
|
436
|
+
// stays inert there rather than snapping between slots.
|
|
437
|
+
const gestures = useAxisGestures({
|
|
438
|
+
axis: 'x',
|
|
439
|
+
// Gated on `<ChartContainer axisPanZoom>` — the axis opt-in — and NOT on the
|
|
440
|
+
// plot's `panZoom`: inheriting that would hand every already-interactive
|
|
441
|
+
// chart gestures its author never asked for. Once opted in the strip is the
|
|
442
|
+
// canvas gesture (drag pans, wheel zooms). A category axis has no continuous
|
|
443
|
+
// domain for either.
|
|
444
|
+
drag: container.axisPanZoomX && xKind !== 'category' ? 'pan' : 'none',
|
|
445
|
+
wheel: container.axisPanZoomX && xKind !== 'category',
|
|
446
|
+
// Snapshot the view at press: the pan is re-derived from it on every move
|
|
447
|
+
// (the plot's own approach), so a long drag can't accumulate rounding, and
|
|
448
|
+
// `roundRange`'s ms snap can't ratchet the span.
|
|
449
|
+
onDragStart: () => {
|
|
450
|
+
panStartRef.current = [container.timeRange[0], container.timeRange[1]];
|
|
451
|
+
},
|
|
452
|
+
onPan: (totalDeltaPx) => {
|
|
453
|
+
const start = panStartRef.current;
|
|
454
|
+
if (start === null)
|
|
455
|
+
return;
|
|
456
|
+
// Dragging right moves the view EARLIER — the content follows the pointer,
|
|
457
|
+
// the sign the plot's drag uses.
|
|
458
|
+
if (container.discontinuities) {
|
|
459
|
+
// Trading-time axis: pan by an equal amount of *trading* time so the
|
|
460
|
+
// drag feels uniform across collapsed gaps (a raw-ms shift jumps).
|
|
461
|
+
const fraction = plotWidth > 0 ? -totalDeltaPx / plotWidth : 0;
|
|
462
|
+
container.applyRange(panRangeTrading(start, fraction, container.discontinuities));
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
const span = start[1] - start[0];
|
|
466
|
+
const dt = plotWidth > 0 ? -totalDeltaPx * (span / plotWidth) : 0;
|
|
467
|
+
container.applyRange(panRange(start, dt, { log: container.xIsLog, snap: xKind === 'time' }));
|
|
468
|
+
},
|
|
469
|
+
onZoom: (factor, pivotPx) => {
|
|
470
|
+
const pivot = +xScale.invert(pivotPx);
|
|
471
|
+
// The same two-branch zoom the plot uses, so an axis drag and a plot
|
|
472
|
+
// wheel move the view by identical maths — including `minDuration` as the
|
|
473
|
+
// zoom-in floor and, on a trading axis, a floor in *trading* ms.
|
|
474
|
+
// `applyRange` then applies `bounds` for both.
|
|
475
|
+
container.applyRange(container.discontinuities
|
|
476
|
+
? zoomRangeTrading(container.timeRange, pivot, factor, container.discontinuities, container.minDuration)
|
|
477
|
+
: zoomRange(container.timeRange, pivot, factor, container.minDuration, {
|
|
478
|
+
log: container.xIsLog,
|
|
479
|
+
snap: xKind === 'time',
|
|
480
|
+
}));
|
|
481
|
+
},
|
|
482
|
+
onReset: () => container.applyRange(container.seedRange),
|
|
483
|
+
});
|
|
420
484
|
const mouse = axisMouseProps(onMouseEvent, 'x', undefined, (event) => {
|
|
485
|
+
// A zoom drag ends with a trailing `click` on the strip, which would report
|
|
486
|
+
// as "clicked the axis at the value I released on" — a value the user never
|
|
487
|
+
// aimed at. Swallow exactly that one report; every other event still flows.
|
|
488
|
+
if (event.type === 'click' && gestures.consumeDrag())
|
|
489
|
+
return null;
|
|
421
490
|
const value = +xScale.invert(axisPointerPx(event, 'x', [0, plotWidth]));
|
|
422
491
|
return {
|
|
423
492
|
value,
|
|
424
493
|
label: xKind === 'category' ? fmt(value) : readoutFmt(value),
|
|
425
494
|
};
|
|
426
495
|
});
|
|
427
|
-
return (_jsxs("div", { "data-axis": "x", ...mouse,
|
|
496
|
+
return (_jsxs("div", { "data-axis": "x", ref: gestures.ref, ...gestures.props, ...mouse,
|
|
497
|
+
// Both spreads carry an `onDoubleClick` — the gesture's reset and the
|
|
498
|
+
// consumer's report — and a spread silently keeps the last one. Compose
|
|
499
|
+
// them, reporting *before* the reset so the payload describes the view the
|
|
500
|
+
// user actually double-clicked in.
|
|
501
|
+
onDoubleClick: (e) => {
|
|
502
|
+
mouse.onDoubleClick?.(e);
|
|
503
|
+
gestures.props.onDoubleClick?.();
|
|
504
|
+
}, style: {
|
|
505
|
+
...gestures.style,
|
|
428
506
|
position: 'relative',
|
|
429
507
|
marginLeft: `${leftGutter}px`,
|
|
430
508
|
width: `${plotWidth}px`,
|
package/dist/YAxis.d.ts
CHANGED
|
@@ -195,7 +195,14 @@ export interface YAxisProps {
|
|
|
195
195
|
* overriding the theme's `axis.label` / `axis.title.color`. The multi-axis
|
|
196
196
|
* convention of colouring each y axis to match its series (`color`
|
|
197
197
|
* matching the layer's) — busy, but standard. Omit for the theme's axis
|
|
198
|
-
* colours.
|
|
198
|
+
* colours.
|
|
199
|
+
*
|
|
200
|
+
* **Also worn by the axis-edge chrome that lands on this axis** — a
|
|
201
|
+
* `<CrosshairCursor>`'s value pill takes it when the reticle reads a series
|
|
202
|
+
* scaled here, so with several axes the pill says *which* scale the number is
|
|
203
|
+
* on (the ChartIQ price-tag convention). That is why it rides on the
|
|
204
|
+
* registered spec: the pill is drawn by the row's cursor overlay, not by this
|
|
205
|
+
* component, so a colour it never registered could not reach it.
|
|
199
206
|
*/
|
|
200
207
|
color?: string;
|
|
201
208
|
/**
|
|
@@ -209,8 +216,57 @@ export interface YAxisProps {
|
|
|
209
216
|
* menu, down/up, move, enter, leave — so switch on `event.type`. Nothing is
|
|
210
217
|
* attached when the prop is omitted, so the move events cost nothing unless
|
|
211
218
|
* you ask for them. A `hide`den axis draws no gutter and so fires nothing.
|
|
219
|
+
*
|
|
220
|
+
* A gutter that also zooms (see the component docs) still reports every event
|
|
221
|
+
* here, minus the trailing `click` a zoom drag would otherwise synthesize.
|
|
212
222
|
*/
|
|
213
223
|
onMouseEvent?: AxisMouseHandler;
|
|
224
|
+
/**
|
|
225
|
+
* A gutter gesture scaled this axis — **the "auto vs manual" hand-off.**
|
|
226
|
+
* Fires with the `[min, max]` **bounds** the gesture arrived at, and with
|
|
227
|
+
* `null` when the axis is released back to auto-fit (double-click).
|
|
228
|
+
*
|
|
229
|
+
* Named for bounds rather than the domain because that is what it reports: with
|
|
230
|
+
* a `pad` set, the visible domain is these bounds *plus* the padding, and it is
|
|
231
|
+
* the bounds you hand back as `min`/`max`.
|
|
232
|
+
*
|
|
233
|
+
* The common shape this exists for: an auto-fitting y axis on a chart whose x
|
|
234
|
+
* is panned and zoomed. The moment the user scrolls or drags the y gutter they
|
|
235
|
+
* have overridden the fit, and a UI usually wants to *say* so — show the
|
|
236
|
+
* resulting min/max, mark the scale "manual", and offer a toggle back to auto
|
|
237
|
+
* (which is the same thing double-clicking the gutter does).
|
|
238
|
+
*
|
|
239
|
+
* ```tsx
|
|
240
|
+
* const [scale, setScale] = useState<[number, number] | null>(null); // null = auto
|
|
241
|
+
* <YAxis
|
|
242
|
+
* id="price"
|
|
243
|
+
* {...(scale ? { min: scale[0], max: scale[1] } : {})}
|
|
244
|
+
* onBoundsChange={setScale}
|
|
245
|
+
* />
|
|
246
|
+
* ```
|
|
247
|
+
*
|
|
248
|
+
* **Providing it makes the axis controlled**, exactly as `onTimeRangeChange`
|
|
249
|
+
* does for the x view: the gesture then only *reports*, and what the axis draws
|
|
250
|
+
* is whatever `min`/`max` you feed back. Omit it and the axis holds the zoom
|
|
251
|
+
* itself (an internal per-axis transform) — which is the standalone behaviour,
|
|
252
|
+
* and why a chart with no scale UI needs no wiring at all.
|
|
253
|
+
*
|
|
254
|
+
* The reported pair is in data units, ready to hand straight back as
|
|
255
|
+
* `min`/`max`.
|
|
256
|
+
*
|
|
257
|
+
* **`scale="symlog"` is approximate on this path, by construction.**
|
|
258
|
+
* {@link linearWindow} is a fraction of the *domain*, so bounds fed back
|
|
259
|
+
* re-derive the knee and reshape the curve — the grabbed pixel cannot be held
|
|
260
|
+
* on a curve that moves with the bounds. (It is the same fact that makes
|
|
261
|
+
* `linearWindow` deliberately *not* recompute under a 2-D gesture.) The zoom is
|
|
262
|
+
* still monotone and well-behaved; if you need the pixel held exactly on a
|
|
263
|
+
* symlog axis, leave this callback off and let the axis hold the zoom itself,
|
|
264
|
+
* where the knee stays anchored to the resolved domain. With an active plot-level y zoom (`panZoom="panZoomY"`/`"panZoomXY"`)
|
|
265
|
+
* the two **compose**: the bounds are the axis's own, and the plot transform
|
|
266
|
+
* still narrows what is drawn on top of them. On a `log` axis it stays positive (the zoom is done in log
|
|
267
|
+
* space), so it is always a domain the axis can actually draw.
|
|
268
|
+
*/
|
|
269
|
+
onBoundsChange?: (bounds: readonly [number, number] | null) => void;
|
|
214
270
|
/**
|
|
215
271
|
* @internal Declaration position among the row's children, injected by
|
|
216
272
|
* `ChartRow` so the first-declared axis stays the default. Do not set.
|
|
@@ -224,6 +280,14 @@ export interface YAxisProps {
|
|
|
224
280
|
* computes this axis's scale from the charts linked to it; the gutter then draws
|
|
225
281
|
* tick marks + labels from that scale. Charts attach via `<LineChart axis="id">`
|
|
226
282
|
* (default: the first axis).
|
|
283
|
+
*
|
|
284
|
+
* **Gestures.** With `<ChartContainer axisPanZoom="y">` (or `"xy"`) the gutter is
|
|
285
|
+
* grabbable: drag or wheel it to scale **this axis only**
|
|
286
|
+
* — a sibling axis on the other side, and every other row, hold still — and
|
|
287
|
+
* double-click to release it back to its fit. That per-axis scaling is what the
|
|
288
|
+
* plot's vertical gesture deliberately cannot do; see
|
|
289
|
+
* {@link RowFrame.axisTransforms}. Report it to a scale UI with
|
|
290
|
+
* {@link YAxisProps.onBoundsChange}.
|
|
227
291
|
*/
|
|
228
|
-
export declare function YAxis({ id, side, label, scale, linearWindow, min, max, format, ticks, tickCount, pad, boundaryLabels, width, hide, labelPlacement, color, onMouseEvent, index, }: YAxisProps): import("react/jsx-runtime").JSX.Element | null;
|
|
292
|
+
export declare function YAxis({ id, side, label, scale, linearWindow, min, max, format, ticks, tickCount, pad, boundaryLabels, width, hide, labelPlacement, color, onMouseEvent, onBoundsChange, index, }: YAxisProps): import("react/jsx-runtime").JSX.Element | null;
|
|
229
293
|
//# sourceMappingURL=YAxis.d.ts.map
|