@pond-ts/charts 0.62.0 → 0.64.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/API.md +1 -1
- package/CHANGELOG.md +223 -1
- package/dist/BarChart.js +6 -6
- 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 +25 -8
- package/dist/XAxis.d.ts +6 -0
- package/dist/XAxis.js +81 -3
- package/dist/YAxis.d.ts +92 -2
- package/dist/YAxis.js +268 -6
- package/dist/annotations.js +14 -7
- package/dist/bars.d.ts +21 -8
- package/dist/bars.js +38 -12
- package/dist/chip.d.ts +50 -7
- package/dist/chip.js +66 -9
- package/dist/context.d.ts +134 -7
- package/dist/cursors.js +33 -8
- package/dist/domain.d.ts +18 -0
- package/dist/domain.js +33 -0
- package/dist/select.d.ts +1 -1
- package/dist/select.js +7 -2
- package/dist/tradingTimeScale.d.ts +11 -1
- package/dist/tradingTimeScale.js +53 -7
- package/dist/use-axis-gestures.d.ts +79 -0
- package/dist/use-axis-gestures.js +257 -0
- package/package.json +3 -3
package/dist/ChartContainer.js
CHANGED
|
@@ -58,17 +58,24 @@ function normalizeRange(range) {
|
|
|
58
58
|
* ChartContainerProps.width} and {@link AutoWidthContainer}.
|
|
59
59
|
*/
|
|
60
60
|
export function ChartContainer(props) {
|
|
61
|
-
const { width } = props;
|
|
61
|
+
const { width, height } = props;
|
|
62
62
|
// The measure pass is a *different component* rather than a branch inside
|
|
63
63
|
// the resolved one, because the resolved container may not render at all
|
|
64
|
-
// until
|
|
65
|
-
// component by the
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
64
|
+
// until its dimensions exist — and ~60 hooks cannot be conditional. Choosing
|
|
65
|
+
// the component by the props' kinds (number vs auto) means flipping between
|
|
66
|
+
// fixed and auto remounts; that is a layout change, and a remount is the
|
|
67
|
+
// honest response to one.
|
|
68
|
+
//
|
|
69
|
+
// The dimensions default differently, deliberately: an omitted `width`
|
|
70
|
+
// means `'auto'` (a chart must have a width, and filling is the sensible
|
|
71
|
+
// way to get one), while an omitted `height` means *unmanaged* — the
|
|
72
|
+
// classic mode where rows declare pixel heights and the container is their
|
|
73
|
+
// sum. `'auto'` height is opt-in because it changes who answers "how tall
|
|
74
|
+
// is a row".
|
|
75
|
+
if (typeof width === 'number' && height !== 'auto') {
|
|
76
|
+
return _jsx(ResolvedChartContainer, { ...props, width: width, height: height });
|
|
70
77
|
}
|
|
71
|
-
return _jsx(
|
|
78
|
+
return _jsx(AutoSizeContainer, { ...props });
|
|
72
79
|
}
|
|
73
80
|
/**
|
|
74
81
|
* The `width="auto"` half: render a plain full-width box, measure it, and
|
|
@@ -88,24 +95,47 @@ export function ChartContainer(props) {
|
|
|
88
95
|
* never overflow its own measurement. A caller who wants a bordered frame
|
|
89
96
|
* puts it on a wrapper *outside* the container.
|
|
90
97
|
*/
|
|
91
|
-
function
|
|
98
|
+
function AutoSizeContainer(props) {
|
|
92
99
|
const boxRef = useRef(null);
|
|
93
|
-
const [measured, setMeasured] = useState(0);
|
|
100
|
+
const [measured, setMeasured] = useState({ width: 0, height: 0 });
|
|
101
|
+
// Which dimensions this instance is responsible for. A numeric width with
|
|
102
|
+
// height="auto" measures height only, and vice versa.
|
|
103
|
+
const needWidth = typeof props.width !== 'number';
|
|
104
|
+
const needHeight = props.height === 'auto';
|
|
105
|
+
// The needs, readable from the long-lived measure closure without going
|
|
106
|
+
// stale — `props.width` can legally flip number ↔ 'auto' without leaving
|
|
107
|
+
// this component (the dispatcher only remounts on the managed/unmanaged
|
|
108
|
+
// boundary).
|
|
109
|
+
const needsRef = useRef({ needWidth, needHeight });
|
|
110
|
+
needsRef.current = { needWidth, needHeight };
|
|
94
111
|
useLayoutEffect(() => {
|
|
95
112
|
const el = boxRef.current;
|
|
96
113
|
if (el === null)
|
|
97
114
|
return;
|
|
98
115
|
const measure = () => setMeasured((prev) => {
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
|
|
116
|
+
const need = needsRef.current;
|
|
117
|
+
const r = el.getBoundingClientRect();
|
|
118
|
+
// **Latch the last non-zero value, per dimension.** A box measures 0
|
|
119
|
+
// whenever it is not laid out — most often because an ancestor went
|
|
120
|
+
// `display: none` (a tab switch, a collapsed accordion), which is a
|
|
121
|
+
// *hidden* chart, not a resized one. Writing that 0 through would
|
|
122
|
+
// unmount the resolved container and discard everything it owns:
|
|
123
|
+
// pan/zoom position, selection, hover, and every layer's memoized
|
|
124
|
+
// draw state, all rebuilt on the way back. Keeping the stale value
|
|
125
|
+
// holds the chart mounted through the hide, and the next real
|
|
126
|
+
// measurement corrects it.
|
|
127
|
+
const w = Math.round(r.width);
|
|
128
|
+
const h = Math.round(r.height);
|
|
129
|
+
// Track only the dimensions this instance is responsible for
|
|
130
|
+
// (Layer-2 review find): a width-only container that also stored
|
|
131
|
+
// height would re-render its whole tree on every *content*-height
|
|
132
|
+
// change — the classic splitter drag, an axis strip growing a band
|
|
133
|
+
// row — where the pre-[PND-HEIGHT] width-only measure bailed.
|
|
134
|
+
const width = need.needWidth && w > 0 ? w : prev.width;
|
|
135
|
+
const height = need.needHeight && h > 0 ? h : prev.height;
|
|
136
|
+
return width === prev.width && height === prev.height
|
|
137
|
+
? prev
|
|
138
|
+
: { width, height };
|
|
109
139
|
});
|
|
110
140
|
measure();
|
|
111
141
|
// Guarded rather than assumed: a non-browser render target (SSR, an older
|
|
@@ -117,10 +147,58 @@ function AutoWidthContainer(props) {
|
|
|
117
147
|
ro.observe(el);
|
|
118
148
|
return () => ro.disconnect();
|
|
119
149
|
}, []);
|
|
120
|
-
|
|
150
|
+
const width = needWidth ? measured.width : props.width;
|
|
151
|
+
const height = needHeight
|
|
152
|
+
? measured.height
|
|
153
|
+
: props.height;
|
|
154
|
+
const ready = width > 0 && (!needHeight || measured.height > 0);
|
|
155
|
+
// **A measured dimension that stays 0 is a standing deadlock, not a slow
|
|
156
|
+
// start** — the parent's size is content-derived and the chart is the
|
|
157
|
+
// content that would have given it one, so nothing will ever paint and
|
|
158
|
+
// nothing errors. Worse for height than width: a flex-*column* child's
|
|
159
|
+
// height defaults to `auto`, so there the deadlock is the default, not an
|
|
160
|
+
// edge case. Say so once, in dev, after layout has had ample time.
|
|
161
|
+
const warnedZeroRef = useRef(false);
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
if (!isDev || ready || warnedZeroRef.current)
|
|
164
|
+
return;
|
|
165
|
+
const t = setTimeout(() => {
|
|
166
|
+
if (ready || warnedZeroRef.current)
|
|
167
|
+
return;
|
|
168
|
+
const el = boxRef.current;
|
|
169
|
+
if (el === null)
|
|
170
|
+
return;
|
|
171
|
+
const r = el.getBoundingClientRect();
|
|
172
|
+
const stuck = [
|
|
173
|
+
...(needWidth && Math.round(r.width) === 0 ? ['width'] : []),
|
|
174
|
+
...(needHeight && Math.round(r.height) === 0 ? ['height'] : []),
|
|
175
|
+
];
|
|
176
|
+
if (stuck.length === 0)
|
|
177
|
+
return;
|
|
178
|
+
warnedZeroRef.current = true;
|
|
179
|
+
console.warn(`[pond-charts] <ChartContainer> measured ${stuck.join(' and ')} of 0 ` +
|
|
180
|
+
`and it has not changed — the chart will stay blank. The measured ` +
|
|
181
|
+
`box fills its parent, so the parent needs a definite ` +
|
|
182
|
+
`${stuck.join('/')} (a sized ancestor, a flex basis, or ` +
|
|
183
|
+
`\`min-${stuck[0]}: 0\` on a flex child); a parent sized by its ` +
|
|
184
|
+
`own content deadlocks, because the chart is that content.`);
|
|
185
|
+
}, ZERO_SIZE_WARNING_MS);
|
|
186
|
+
return () => clearTimeout(t);
|
|
187
|
+
}, [ready, needWidth, needHeight]);
|
|
188
|
+
return (_jsx("div", { ref: boxRef, style: {
|
|
189
|
+
width: '100%',
|
|
190
|
+
// Only claim the parent's height when asked to measure it: a
|
|
191
|
+
// width-only auto container must keep its intrinsic height (the rows'
|
|
192
|
+
// sum), or every pre-[PND-HEIGHT] consumer's layout changes.
|
|
193
|
+
...(needHeight ? { height: '100%', minHeight: 0 } : {}),
|
|
194
|
+
}, children: ready && (_jsx(ResolvedChartContainer, { ...props, width: width, height: height })) }));
|
|
121
195
|
}
|
|
196
|
+
/** How long a measured dimension may stay 0 before the dev warning names the
|
|
197
|
+
* deadlock (see {@link AutoSizeContainer}). Long enough for any real layout
|
|
198
|
+
* pass; a chart legitimately gated this long is not painting anyway. */
|
|
199
|
+
const ZERO_SIZE_WARNING_MS = 600;
|
|
122
200
|
/** {@link ChartContainer} with its width resolved to a concrete pixel number. */
|
|
123
|
-
function ResolvedChartContainer({ range, categories: categoriesProp, maxBandWidth, bandAlign = 'start', width, rowGap = 0, showAxis = true, trackerPosition, onTrackerChanged, onDrawStats, panZoom = false, bounds, onTimeRangeChange, minDuration = 1, cursor: cursorProp, cursorSequence: cursorSequenceProp, onRegionSelect, regionSelectModifier, cursorTime: cursorTimeProp, crosshairSnap: crosshairSnapProp, editAnnotations = false, creating = null, onCreate, onSelectAnnotation, onHoverAnnotation, onEditAnnotation, snap = true, timeFormat, cursorFormat: cursorFormatProp, origin, theme, discontinuities, calendar, spacing, xScale: xScaleKind = 'linear', grid = true, sessionDividers = 'none', children, }) {
|
|
201
|
+
function ResolvedChartContainer({ range, categories: categoriesProp, maxBandWidth, bandAlign = 'start', width, height, rowGap = 0, showAxis = true, trackerPosition, onTrackerChanged, onDrawStats, panZoom = false, axisPanZoom = false, bounds, onTimeRangeChange, minDuration = 1, cursor: cursorProp, cursorSequence: cursorSequenceProp, onRegionSelect, regionSelectModifier, cursorTime: cursorTimeProp, crosshairSnap: crosshairSnapProp, editAnnotations = false, creating = null, onCreate, onSelectAnnotation, onHoverAnnotation, onEditAnnotation, snap = true, timeFormat, cursorFormat: cursorFormatProp, origin, theme, discontinuities, calendar, spacing, xScale: xScaleKind = 'linear', grid = true, sessionDividers = 'none', children, }) {
|
|
124
202
|
// ── Legacy cursor props (deprecated) ───────────────────────────────────────
|
|
125
203
|
// The string surface keeps working for one minor: the resolved mode is
|
|
126
204
|
// synthesized into the equivalent mounted preset below (`<LegacyCursor>`),
|
|
@@ -128,6 +206,10 @@ function ResolvedChartContainer({ range, categories: categoriesProp, maxBandWidt
|
|
|
128
206
|
// *explicitly* set (never on the defaults). Mounted cursor components in the
|
|
129
207
|
// same scope override the shim. See docs/rfcs/interaction.md §9 / A4.4.
|
|
130
208
|
const cursor = cursorProp ?? DEFAULT_CURSOR_MODE;
|
|
209
|
+
// [PND-HEIGHT] Whether this container owns vertical layout (see the
|
|
210
|
+
// `height` prop). Carried on the frame so a `<ChartRow flex>` can tell a
|
|
211
|
+
// home that can size it from one that never will.
|
|
212
|
+
const managesHeight = height !== undefined;
|
|
131
213
|
// [PND-IGNITECAT] The declared slot list, normalized to `null` when absent
|
|
132
214
|
// and held by **content** identity. An inline `categories={['a', 'b']}` is a
|
|
133
215
|
// fresh array every render; keying the kind/scale memos off the raw prop
|
|
@@ -221,6 +303,11 @@ function ResolvedChartContainer({ range, categories: categoriesProp, maxBandWidt
|
|
|
221
303
|
panZoom === 'panZoomX' ||
|
|
222
304
|
panZoom === 'panZoomXY';
|
|
223
305
|
const zoomY = panZoom === 'panZoomY' || panZoom === 'panZoomXY';
|
|
306
|
+
// Axis-strip gestures are their own opt-in (see `axisPanZoom`), so they are
|
|
307
|
+
// resolved from that prop alone — never from `panZoom`, which would make every
|
|
308
|
+
// already-interactive chart grow axis gestures on upgrade.
|
|
309
|
+
const axisPanZoomX = axisPanZoom === true || axisPanZoom === 'x' || axisPanZoom === 'xy';
|
|
310
|
+
const axisPanZoomY = axisPanZoom === true || axisPanZoom === 'y' || axisPanZoom === 'xy';
|
|
224
311
|
const panX = zoomX || panZoom === 'pan';
|
|
225
312
|
const panY = zoomY;
|
|
226
313
|
const panEnabled = panX || panY;
|
|
@@ -244,7 +331,14 @@ function ResolvedChartContainer({ range, categories: categoriesProp, maxBandWidt
|
|
|
244
331
|
const k = Math.max(1, next.k);
|
|
245
332
|
setYTransform((prev) => prev.k === k && prev.ty === next.ty ? prev : { k, ty: next.ty });
|
|
246
333
|
}, []);
|
|
247
|
-
|
|
334
|
+
// The x **strip**'s gestures move the same view the plot's do, so they must
|
|
335
|
+
// make the container own a view as well. Leaving `axisPanZoomX` out of this
|
|
336
|
+
// silently broke the headline combination — `axisPanZoom="x"` with the default
|
|
337
|
+
// `panZoom="none"`: `applyRange` wrote `internalRange` while `view` kept
|
|
338
|
+
// reading `seed`, so an uncontrolled strip captured the drag and drew nothing.
|
|
339
|
+
// (`axisPanZoomY` is absent on purpose: a gutter zoom is per-axis row state,
|
|
340
|
+
// not the shared x view.)
|
|
341
|
+
const interactive = panEnabled || zoomEnabled || axisPanZoomX;
|
|
248
342
|
// The explicit base domain from `range` (a tuple or a TimeRange). `undefined`
|
|
249
343
|
// ⇒ auto-fit (resolved from the layers below). Pan/zoom seeds from it; `seed`
|
|
250
344
|
// is the placeholder while auto-fitting.
|
|
@@ -1210,6 +1304,11 @@ function ResolvedChartContainer({ range, categories: categoriesProp, maxBandWidt
|
|
|
1210
1304
|
// it lives in `cursorFrame` below, [PND-HOVCTX] — but the tuple stays a memo
|
|
1211
1305
|
// to hold the line for every other rebuild path.)
|
|
1212
1306
|
const timeRangeTuple = useMemo(() => [d0, d1], [d0, d1]);
|
|
1307
|
+
// The declared view (`range`), as against the gestured one above — the x
|
|
1308
|
+
// strip's double-click reset target. Memoized on its endpoints for the same
|
|
1309
|
+
// reason `timeRangeTuple` is: it sits on the frame, and a fresh tuple each
|
|
1310
|
+
// render would re-identify it for every draw callback that reads the frame.
|
|
1311
|
+
const seedRangeTuple = useMemo(() => [seed[0], seed[1]], [seed[0], seed[1]]);
|
|
1213
1312
|
// The per-move cursor state, split into its own context so a mousemove
|
|
1214
1313
|
// re-identifies only this small object — not the ~50-field frame below, which
|
|
1215
1314
|
// stays stable across hovers so `YAxis` / `Bar` / `Box` don't re-render. See
|
|
@@ -1221,6 +1320,10 @@ function ResolvedChartContainer({ range, categories: categoriesProp, maxBandWidt
|
|
|
1221
1320
|
}), [cursorX, hoverPoint]);
|
|
1222
1321
|
const frame = useMemo(() => ({
|
|
1223
1322
|
timeRange: timeRangeTuple,
|
|
1323
|
+
seedRange: seedRangeTuple,
|
|
1324
|
+
axisPanZoomX,
|
|
1325
|
+
axisPanZoomY,
|
|
1326
|
+
managesHeight,
|
|
1224
1327
|
width,
|
|
1225
1328
|
theme: theme ?? defaultTheme,
|
|
1226
1329
|
plotWidth,
|
|
@@ -1302,6 +1405,10 @@ function ResolvedChartContainer({ range, categories: categoriesProp, maxBandWidt
|
|
|
1302
1405
|
firstRowKey,
|
|
1303
1406
|
}), [
|
|
1304
1407
|
timeRangeTuple,
|
|
1408
|
+
seedRangeTuple,
|
|
1409
|
+
axisPanZoomX,
|
|
1410
|
+
axisPanZoomY,
|
|
1411
|
+
managesHeight,
|
|
1305
1412
|
width,
|
|
1306
1413
|
theme,
|
|
1307
1414
|
plotWidth,
|
|
@@ -1385,13 +1492,33 @@ function ResolvedChartContainer({ range, categories: categoriesProp, maxBandWidt
|
|
|
1385
1492
|
// The 'line' default nobody asked for is IMPLICIT — the one cursor a
|
|
1386
1493
|
// <MultiSelector>'s resting block preview may replace with the
|
|
1387
1494
|
// brush band. An explicit `cursor` prop (any mode) still wins.
|
|
1388
|
-
implicit: cursorProp === undefined }), _jsxs("div", { style: {
|
|
1495
|
+
implicit: cursorProp === undefined }), _jsxs("div", { style: {
|
|
1496
|
+
width: `${width}px`,
|
|
1497
|
+
// [PND-HEIGHT] A managed height makes the outer box a flex
|
|
1498
|
+
// column: the rows block below flexes, the axis strip keeps its
|
|
1499
|
+
// natural height at the bottom, and CSS subtracts one from the
|
|
1500
|
+
// other. That subtraction being layout rather than arithmetic is
|
|
1501
|
+
// the feature — the strip's height varies with label, font size,
|
|
1502
|
+
// calendar bands and pill lanes, so no constant is correct.
|
|
1503
|
+
...(height !== undefined
|
|
1504
|
+
? {
|
|
1505
|
+
height: `${height}px`,
|
|
1506
|
+
display: 'flex',
|
|
1507
|
+
flexDirection: 'column',
|
|
1508
|
+
}
|
|
1509
|
+
: {}),
|
|
1510
|
+
}, children: [_jsx("div", { style: {
|
|
1389
1511
|
display: 'flex',
|
|
1390
1512
|
flexDirection: 'column',
|
|
1391
1513
|
gap: `${rowGap}px`,
|
|
1392
1514
|
// The positioned ancestor for overlay chrome (`<Legend>`): the
|
|
1393
1515
|
// card anchors to the rows block, never the axis strip below.
|
|
1394
1516
|
position: 'relative',
|
|
1517
|
+
// The rows block takes what the axis strip leaves. `minHeight:
|
|
1518
|
+
// 0` lets it shrink below its content — without it a flex
|
|
1519
|
+
// child's floor is its content and nothing can ever get
|
|
1520
|
+
// smaller.
|
|
1521
|
+
...(height !== undefined ? { flex: '1 1 0%', minHeight: 0 } : {}),
|
|
1395
1522
|
}, children: children }), showAxis && _jsx(TimeAxis, {})] })] }) }));
|
|
1396
1523
|
}
|
|
1397
1524
|
//# sourceMappingURL=ChartContainer.js.map
|
package/dist/ChartRow.d.ts
CHANGED
|
@@ -1,8 +1,44 @@
|
|
|
1
1
|
import { type ReactNode } from 'react';
|
|
2
2
|
import { type CursorMode } from './context.js';
|
|
3
3
|
export interface ChartRowProps {
|
|
4
|
-
/**
|
|
5
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Row height in CSS pixels — the **fixed** sizing mode. Omit it (or pass
|
|
6
|
+
* {@link flex}) to let the row share the container's remaining height
|
|
7
|
+
* instead; a bare `<ChartRow>` means `flex={1}`.
|
|
8
|
+
*/
|
|
9
|
+
height?: number;
|
|
10
|
+
/**
|
|
11
|
+
* Share of the container's **remaining** height ([PND-HEIGHT]) — the
|
|
12
|
+
* CSS-flex sizing mode, and what an omitted `height` defaults to (`1`).
|
|
13
|
+
*
|
|
14
|
+
* The remainder is what CSS flex layout says it is: the container's height
|
|
15
|
+
* minus its axis strip, minus every fixed-`height` row, minus any non-row
|
|
16
|
+
* children you placed between rows (a draggable splitter), minus `rowGap`s.
|
|
17
|
+
* That is deliberate — the row's box is `flex: <n> 1 0`, so **the browser
|
|
18
|
+
* does the subtraction** and there is no strip-height constant for a caller
|
|
19
|
+
* to know, guess, or drift on (the reporting consumer had `20` and `24` in
|
|
20
|
+
* one codebase for a strip that is actually 22 — *when it is not showing a
|
|
21
|
+
* calendar band row or marker pills, which change it*). The row then reads
|
|
22
|
+
* back the height the layout gave it and builds its y-scales from that.
|
|
23
|
+
*
|
|
24
|
+
* Mixing modes is the point, not an edge case: a price row over a volume
|
|
25
|
+
* row is `<ChartRow flex={3}>` over `<ChartRow flex={1}>`; the splitter
|
|
26
|
+
* shape is one `flex` row that absorbs slack over one fixed row the drag
|
|
27
|
+
* resizes.
|
|
28
|
+
*
|
|
29
|
+
* **Needs a container that manages height** — `<ChartContainer
|
|
30
|
+
* height={number | 'auto'}>`. Inside a container with no height, a flex
|
|
31
|
+
* row's box has nothing to flex into, collapses to zero, and stays gated
|
|
32
|
+
* out; dev builds warn.
|
|
33
|
+
*
|
|
34
|
+
* A flex row's first useful paint waits for its first measurement — by
|
|
35
|
+
* **timing**, not a gate: the first render does execute children at height
|
|
36
|
+
* 0 (a 0-height canvas draws nothing), and the layout effect's synchronous
|
|
37
|
+
* setState delivers the real height before the browser paints. Like the
|
|
38
|
+
* container's `width="auto"`, it keeps its last non-zero height while
|
|
39
|
+
* hidden, so a `display: none` tab switch does not discard its scales.
|
|
40
|
+
*/
|
|
41
|
+
flex?: number;
|
|
6
42
|
/**
|
|
7
43
|
* Cursor presentation for this row, overriding the container's default
|
|
8
44
|
* ({@link ChartContainerProps.cursor}). Omit to inherit. See {@link CursorMode}.
|
|
@@ -32,5 +68,5 @@ export interface ChartRowProps {
|
|
|
32
68
|
* Children lay out left-to-right in author order, so `<YAxis side="left"/>` goes
|
|
33
69
|
* before `<Layers/>` and `<YAxis side="right"/>` after.
|
|
34
70
|
*/
|
|
35
|
-
export declare function ChartRow({ height, cursor, children }: ChartRowProps): import("react/jsx-runtime").JSX.Element;
|
|
71
|
+
export declare function ChartRow({ height: heightProp, flex, cursor, children, }: ChartRowProps): import("react/jsx-runtime").JSX.Element;
|
|
36
72
|
//# sourceMappingURL=ChartRow.d.ts.map
|
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
|