@pond-ts/charts 0.31.1 → 0.32.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 +55 -1
- package/dist/AreaChart.d.ts +11 -5
- package/dist/AreaChart.js +23 -10
- package/dist/BandChart.d.ts +12 -5
- package/dist/BandChart.js +28 -10
- package/dist/BarChart.d.ts +17 -11
- package/dist/BarChart.js +12 -6
- package/dist/ChartContainer.d.ts +45 -2
- package/dist/ChartContainer.js +42 -1
- package/dist/ChartRow.js +16 -0
- package/dist/Layers.js +236 -96
- package/dist/YAxis.d.ts +16 -1
- package/dist/YAxis.js +22 -6
- package/dist/annotations.d.ts +110 -0
- package/dist/annotations.js +463 -0
- package/dist/chip.d.ts +23 -0
- package/dist/chip.js +43 -0
- package/dist/context.d.ts +116 -0
- package/dist/data.d.ts +34 -0
- package/dist/data.js +89 -14
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/theme.d.ts +18 -0
- package/dist/theme.js +13 -0
- package/package.json +3 -3
package/dist/Layers.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Children, cloneElement, isValidElement, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, } from 'react';
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Children, cloneElement, isValidElement, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
|
|
3
3
|
import { Canvas } from './Canvas.js';
|
|
4
4
|
import { drawGrid } from './grid.js';
|
|
5
5
|
import { cursorParts } from './tracker.js';
|
|
6
6
|
import { resolveSelection } from './select.js';
|
|
7
7
|
import { panRange, zoomRange } from './viewport.js';
|
|
8
|
+
import { flagChipStyle, flagChipX } from './chip.js';
|
|
8
9
|
import { ContainerContext, LayersContext, RowContext, } from './context.js';
|
|
9
10
|
/** Gridline tick count — matches the axes (`YAxis`/`TimeAxis`) so they align. */
|
|
10
11
|
const GRID_TICKS = 5;
|
|
@@ -48,7 +49,7 @@ export function Layers({ children }) {
|
|
|
48
49
|
}), [row.registerLayer, row.unregisterLayer]);
|
|
49
50
|
const background = container.theme.background;
|
|
50
51
|
const { grid: gridColor, gridDash } = container.theme.axis;
|
|
51
|
-
const { layers, yScales, formats, defaultAxisId } = row;
|
|
52
|
+
const { layers, yScales, formats, defaultAxisId, tickValues } = row;
|
|
52
53
|
// x geometry is shared and lives on the container (uniform across rows).
|
|
53
54
|
const { xScale, plotWidth } = container;
|
|
54
55
|
const draw = useCallback((ctx, w, h) => {
|
|
@@ -59,8 +60,13 @@ export function Layers({ children }) {
|
|
|
59
60
|
// Gridlines behind the data, from the same ticks the axes label: vertical
|
|
60
61
|
// from the shared time scale, horizontal from the row's default y-axis.
|
|
61
62
|
const gridY = yScales.get(defaultAxisId);
|
|
63
|
+
// Explicit `<YAxis ticks>` drive the gridlines too, so they align with the
|
|
64
|
+
// axis labels; otherwise d3 auto-picks (the default).
|
|
65
|
+
const explicitY = tickValues.get(defaultAxisId);
|
|
62
66
|
const xTicks = xScale.ticks(GRID_TICKS).map((d) => xScale(d));
|
|
63
|
-
const yTicks = gridY
|
|
67
|
+
const yTicks = gridY
|
|
68
|
+
? (explicitY ?? gridY.ticks(GRID_TICKS)).map((t) => gridY(t))
|
|
69
|
+
: [];
|
|
64
70
|
drawGrid(ctx, xTicks, yTicks, w, h, gridColor, gridDash);
|
|
65
71
|
for (const entry of layers) {
|
|
66
72
|
const yScale = yScales.get(entry.axisId ?? defaultAxisId);
|
|
@@ -68,7 +74,16 @@ export function Layers({ children }) {
|
|
|
68
74
|
continue;
|
|
69
75
|
entry.layer.draw(ctx, xScale, yScale);
|
|
70
76
|
}
|
|
71
|
-
}, [
|
|
77
|
+
}, [
|
|
78
|
+
layers,
|
|
79
|
+
yScales,
|
|
80
|
+
xScale,
|
|
81
|
+
defaultAxisId,
|
|
82
|
+
tickValues,
|
|
83
|
+
background,
|
|
84
|
+
gridColor,
|
|
85
|
+
gridDash,
|
|
86
|
+
]);
|
|
72
87
|
// Interaction overlay: the cursor marks live on a DOM/SVG overlay above the
|
|
73
88
|
// data, so hovering never repaints the data canvas (whose `draw` doesn't depend
|
|
74
89
|
// on the cursor). Reading the container's cursorX — set by whichever row the
|
|
@@ -79,7 +94,13 @@ export function Layers({ children }) {
|
|
|
79
94
|
// Cursor mode: the row's override, else the container default. One mode per
|
|
80
95
|
// row (the synced vertical line is shared across rows); each layer renders the
|
|
81
96
|
// mode in its own way. `parts` decomposes it into {line, dots, chip}.
|
|
82
|
-
|
|
97
|
+
// Editing suppresses the data cursor — the marks get the surface (hover/drag),
|
|
98
|
+
// and a crosshair would just be noise. True in global edit mode *and* while a
|
|
99
|
+
// single annotation is being edited (the double-click target).
|
|
100
|
+
const editingActive = container.editAnnotations || container.annotations.some((a) => a.editing);
|
|
101
|
+
const parts = editingActive
|
|
102
|
+
? cursorParts('none')
|
|
103
|
+
: cursorParts(row.cursor ?? container.cursor);
|
|
83
104
|
const cursorColor = container.theme.cursor ?? container.theme.axis.label;
|
|
84
105
|
// Only read a time when the cursor is within the plot. An out-of-bounds
|
|
85
106
|
// controlled trackerPosition hides the cursor, so the dots + chips hide too —
|
|
@@ -181,9 +202,31 @@ export function Layers({ children }) {
|
|
|
181
202
|
});
|
|
182
203
|
// Pointer-down position, to tell a click (select) from the tail of a drag/pan.
|
|
183
204
|
const clickStartRef = useRef(null);
|
|
205
|
+
// Create gesture (when a tool is armed): `createPt` is the live pointer driving
|
|
206
|
+
// the preview on the hovered row; `drawFrom` is a region's fixed start edge (px)
|
|
207
|
+
// once pressed. `drawFromRef` mirrors it for the stable up-handler to read.
|
|
208
|
+
const [createPt, setCreatePt] = useState(null);
|
|
209
|
+
const [drawFrom, setDrawFrom] = useState(null);
|
|
210
|
+
const drawFromRef = useRef(null);
|
|
184
211
|
const handlePointerDown = useCallback((e) => {
|
|
185
212
|
clickStartRef.current = { x: e.clientX, y: e.clientY };
|
|
186
213
|
const c = containerRef.current;
|
|
214
|
+
if (c.creating !== null) {
|
|
215
|
+
// Armed: a region presses to fix its start edge; a line just tracks until
|
|
216
|
+
// release. Capture so the draw can continue outside the plot.
|
|
217
|
+
if (c.creating === 'region') {
|
|
218
|
+
const px = e.clientX - e.currentTarget.getBoundingClientRect().left;
|
|
219
|
+
drawFromRef.current = px;
|
|
220
|
+
setDrawFrom(px);
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
/* ignore */
|
|
227
|
+
}
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
187
230
|
if (!c.panZoom)
|
|
188
231
|
return;
|
|
189
232
|
const r = c.timeRange;
|
|
@@ -201,6 +244,13 @@ export function Layers({ children }) {
|
|
|
201
244
|
}, []);
|
|
202
245
|
const handlePointerMove = useCallback((e) => {
|
|
203
246
|
const c = containerRef.current;
|
|
247
|
+
if (c.creating !== null) {
|
|
248
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
249
|
+
const px = Math.max(0, Math.min(c.plotWidth, e.clientX - rect.left));
|
|
250
|
+
setCreatePt({ x: px, y: e.clientY - rect.top });
|
|
251
|
+
c.setHoverX(px); // share the preview x so other rows draw a guide there
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
204
254
|
const drag = dragRef.current;
|
|
205
255
|
if (drag) {
|
|
206
256
|
// Pan from the start range by the total drag — right → earlier (−dt).
|
|
@@ -227,6 +277,48 @@ export function Layers({ children }) {
|
|
|
227
277
|
c.setHovered(hit);
|
|
228
278
|
}, []);
|
|
229
279
|
const handlePointerUp = useCallback((e) => {
|
|
280
|
+
const c = containerRef.current;
|
|
281
|
+
if (c.creating !== null) {
|
|
282
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
283
|
+
const px = Math.max(0, Math.min(c.plotWidth, e.clientX - rect.left));
|
|
284
|
+
const py = e.clientY - rect.top;
|
|
285
|
+
if (c.creating === 'marker') {
|
|
286
|
+
c.onCreate?.({ kind: 'marker', at: +c.xScale.invert(px) });
|
|
287
|
+
}
|
|
288
|
+
else if (c.creating === 'baseline') {
|
|
289
|
+
const r = rowRef.current;
|
|
290
|
+
const ys = r.yScales.get(r.defaultAxisId);
|
|
291
|
+
if (ys) {
|
|
292
|
+
c.onCreate?.({
|
|
293
|
+
kind: 'baseline',
|
|
294
|
+
value: ys.invert(py),
|
|
295
|
+
axis: r.defaultAxisId,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
else if (c.creating === 'region') {
|
|
300
|
+
const fromPx = drawFromRef.current;
|
|
301
|
+
// Need a real drag — a click (no span) creates nothing.
|
|
302
|
+
if (fromPx !== null && Math.abs(px - fromPx) > DRAG_SLOP) {
|
|
303
|
+
const a = +c.xScale.invert(fromPx);
|
|
304
|
+
const b = +c.xScale.invert(px);
|
|
305
|
+
c.onCreate?.({
|
|
306
|
+
kind: 'region',
|
|
307
|
+
from: Math.min(a, b),
|
|
308
|
+
to: Math.max(a, b),
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
drawFromRef.current = null;
|
|
313
|
+
setDrawFrom(null);
|
|
314
|
+
try {
|
|
315
|
+
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
/* ignore */
|
|
319
|
+
}
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
230
322
|
if (dragRef.current) {
|
|
231
323
|
dragRef.current = null;
|
|
232
324
|
try {
|
|
@@ -239,17 +331,36 @@ export function Layers({ children }) {
|
|
|
239
331
|
}, []);
|
|
240
332
|
const handlePointerLeave = useCallback(() => {
|
|
241
333
|
const c = containerRef.current;
|
|
334
|
+
if (c.creating !== null) {
|
|
335
|
+
// Leaving mid-arm cancels the preview (and an in-progress region draw).
|
|
336
|
+
setCreatePt(null);
|
|
337
|
+
drawFromRef.current = null;
|
|
338
|
+
setDrawFrom(null);
|
|
339
|
+
c.setHoverX(null);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
242
342
|
c.setHoverX(null);
|
|
243
343
|
c.setHovered(null);
|
|
244
344
|
}, []);
|
|
245
345
|
// Click selection: ignore the click that ends a drag/pan (moved past a few px),
|
|
246
346
|
// else hit-test the row's layers top-down and select — or clear on a miss.
|
|
247
347
|
const handleClick = useCallback((e) => {
|
|
348
|
+
if (containerRef.current.creating !== null)
|
|
349
|
+
return; // the draw owns the click
|
|
248
350
|
const start = clickStartRef.current;
|
|
249
351
|
if (start &&
|
|
250
352
|
Math.hypot(e.clientX - start.x, e.clientY - start.y) > DRAG_SLOP)
|
|
251
353
|
return;
|
|
252
354
|
const c = containerRef.current;
|
|
355
|
+
// A click that reached the plot (no mark's DragArea claimed it) is an empty
|
|
356
|
+
// click. Deselect / exit edit when the consumer is tracking annotations — in
|
|
357
|
+
// global edit mode, or whenever a mark is currently active (a selected mark, or
|
|
358
|
+
// the single-edit target, which also reads as selected). Marks stop their own
|
|
359
|
+
// clicks in DragArea, so this only fires on true empty space.
|
|
360
|
+
if (c.editAnnotations || c.annotations.some((a) => a.selected)) {
|
|
361
|
+
c.onSelectAnnotation?.(null);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
253
364
|
const r = rowRef.current;
|
|
254
365
|
const rect = e.currentTarget.getBoundingClientRect();
|
|
255
366
|
const hit = resolveSelection(r.layers, e.clientX - rect.left, e.clientY - rect.top, c.xScale, (axisId) => r.yScales.get(axisId ?? r.defaultAxisId));
|
|
@@ -281,19 +392,9 @@ export function Layers({ children }) {
|
|
|
281
392
|
// dot ('inline', clamped within the row) or stack at the top of the flag staff
|
|
282
393
|
// ('flag'). line / point / none draw no chips — surface values off-chart.
|
|
283
394
|
const flagLineHeight = container.theme.font.size + 5;
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
border: `1px solid ${gridColor}`,
|
|
288
|
-
borderRadius: '3px',
|
|
289
|
-
padding: '0 4px',
|
|
290
|
-
fontFamily: container.theme.font.family,
|
|
291
|
-
fontSize: `${container.theme.font.size}px`,
|
|
292
|
-
fontVariantNumeric: 'tabular-nums',
|
|
293
|
-
whiteSpace: 'nowrap',
|
|
294
|
-
pointerEvents: 'none',
|
|
295
|
-
lineHeight: 1.5,
|
|
296
|
-
};
|
|
395
|
+
// Cursor chips share the annotation label look (filled, no outline) — one
|
|
396
|
+
// source of truth so a flag and a placed label read as the same object.
|
|
397
|
+
const chipStyle = flagChipStyle(container.theme);
|
|
297
398
|
// Show the cursor's time atop the readout (opt-in via `cursorTime`), whenever
|
|
298
399
|
// the cursor is active (any mode that draws marks). A single chip at the cursor
|
|
299
400
|
// x, top of the row; for `flag` it sits above the value chips (which shift down).
|
|
@@ -304,96 +405,135 @@ export function Layers({ children }) {
|
|
|
304
405
|
cursorTime !== null &&
|
|
305
406
|
(parts.line || parts.dots) &&
|
|
306
407
|
row.isFirstRow;
|
|
307
|
-
// Flag
|
|
308
|
-
//
|
|
408
|
+
// Flag geometry: each value flies as a flag from the top of its own staff — the
|
|
409
|
+
// chip's top sits at `flagBase` (just below the time chip when shown) and the
|
|
410
|
+
// staff drops from there to the dot. (Chips share that top and spread by x, so
|
|
411
|
+
// near-coincident flags can overlap — a de-overlap heuristic is a follow-up.)
|
|
309
412
|
const flagTop = 2;
|
|
310
413
|
const flagBase = flagTop + (showTime ? flagLineHeight : 0);
|
|
311
|
-
const stackBottom = flagBase + trackerSamples.length * flagLineHeight;
|
|
312
414
|
// The cursor-time chip caps the readout. In `flag` mode it tops the flag stack,
|
|
313
415
|
// so anchor it to the stack's x (the nearest sample's point) so time + flag +
|
|
314
416
|
// staff + dot read as one column; otherwise it labels the cursor line at cursorX.
|
|
315
417
|
const timeX = parts.chip === 'flag' && trackerSamples.length > 0
|
|
316
418
|
? trackerSamples[0].px
|
|
317
419
|
: cursorX;
|
|
420
|
+
// Cross-row guide lines: the x-positions of annotations on the OTHER rows
|
|
421
|
+
// (markers + region edges), so a mark on one row reads against this row's data +
|
|
422
|
+
// the shared x axis. A mark's own row skips itself; baselines cast no vertical
|
|
423
|
+
// guide (empty `xs`). Faint + dashed so they read as reference, not data.
|
|
424
|
+
const guideXs = container.annotations
|
|
425
|
+
.filter((a) => a.rowKey !== row.rowKey)
|
|
426
|
+
.flatMap((a) => a.xs)
|
|
427
|
+
.map((xv) => xScale(xv));
|
|
428
|
+
const guideColor = container.theme.annotation?.color ?? gridColor;
|
|
429
|
+
// Create preview: while a tool is armed, the hovered row (the one with
|
|
430
|
+
// `createPt`) shows a cursor-style line tracking the pointer — vertical for
|
|
431
|
+
// marker/region, horizontal for baseline, a span once a region is being dragged.
|
|
432
|
+
// The OTHER rows show the faint guide at the shared preview x (markers/regions).
|
|
433
|
+
const creating = container.creating;
|
|
434
|
+
let createPreview = null;
|
|
435
|
+
if (creating !== null && createPt !== null) {
|
|
436
|
+
if (creating === 'baseline') {
|
|
437
|
+
createPreview = (_jsx("line", { x1: 0, y1: createPt.y, x2: plotWidth, y2: createPt.y, stroke: guideColor, strokeWidth: 1, opacity: 0.85, shapeRendering: "crispEdges" }));
|
|
438
|
+
}
|
|
439
|
+
else if (drawFrom !== null) {
|
|
440
|
+
const l = Math.min(drawFrom, createPt.x);
|
|
441
|
+
const w = Math.abs(createPt.x - drawFrom);
|
|
442
|
+
createPreview = (_jsxs(_Fragment, { children: [_jsx("rect", { x: l, y: 0, width: w, height: row.height, fill: guideColor, opacity: 0.12 }), _jsx("line", { x1: drawFrom, y1: 0, x2: drawFrom, y2: row.height, stroke: guideColor, strokeWidth: 1, opacity: 0.85, strokeDasharray: "3 2", shapeRendering: "crispEdges" }), _jsx("line", { x1: createPt.x, y1: 0, x2: createPt.x, y2: row.height, stroke: guideColor, strokeWidth: 1, opacity: 0.85, shapeRendering: "crispEdges" })] }));
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
createPreview = (_jsx("line", { x1: createPt.x, y1: 0, x2: createPt.x, y2: row.height, stroke: guideColor, strokeWidth: 1, opacity: 0.85, shapeRendering: "crispEdges" }));
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
else if (creating !== null && creating !== 'baseline' && cursorX !== null) {
|
|
449
|
+
// Another row — the faint preview guide at the shared pointer x.
|
|
450
|
+
createPreview = (_jsx("line", { x1: cursorX, y1: 0, x2: cursorX, y2: row.height, stroke: guideColor, strokeWidth: 1, opacity: 0.22, strokeDasharray: "2 3", shapeRendering: "crispEdges" }));
|
|
451
|
+
}
|
|
318
452
|
// Inject each draw layer's JSX position so it registers its declaration order
|
|
319
453
|
// (z-stack: lower index at the back), independent of mount timing.
|
|
320
454
|
const indexedChildren = Children.map(children, (child, index) => isValidElement(child)
|
|
321
455
|
? cloneElement(child, { index })
|
|
322
456
|
: child);
|
|
323
|
-
return (
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
457
|
+
return (_jsx(LayersContext.Provider, { value: registry, children: _jsxs("div", { ref: plotRef, style: {
|
|
458
|
+
position: 'relative',
|
|
459
|
+
width: `${plotWidth}px`,
|
|
460
|
+
height: `${row.height}px`,
|
|
461
|
+
// Edit mode: a plain cursor on the plot (the annotations supply their
|
|
462
|
+
// own grab/resize cursors); crosshair only when the data cursor is live
|
|
463
|
+
// (suppressed in single-annotation edit too, not just global edit).
|
|
464
|
+
cursor: editingActive ? 'default' : 'crosshair',
|
|
465
|
+
// The turquoise edit border — the "you're in *global* Edit" signal (not
|
|
466
|
+
// single-annotation edit). Inset shadow so it doesn't shift layout.
|
|
467
|
+
boxShadow: container.editAnnotations
|
|
468
|
+
? `inset 0 0 0 1px ${guideColor}`
|
|
469
|
+
: undefined,
|
|
470
|
+
// Let pan/zoom own touch gestures (no native scroll) when enabled.
|
|
471
|
+
touchAction: container.panZoom ? 'none' : 'auto',
|
|
472
|
+
}, 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: {
|
|
473
|
+
position: 'absolute',
|
|
474
|
+
top: 0,
|
|
475
|
+
left: 0,
|
|
476
|
+
pointerEvents: 'none',
|
|
477
|
+
}, children: guideXs.map((gx, i) => (_jsx("line", { x1: gx, y1: 0, x2: gx, y2: row.height, stroke: guideColor, strokeWidth: 1, opacity: 0.22, strokeDasharray: "2 3", shapeRendering: "crispEdges" }, i))) })), createPreview !== null && (_jsx("svg", { width: plotWidth, height: row.height, style: {
|
|
478
|
+
position: 'absolute',
|
|
479
|
+
top: 0,
|
|
480
|
+
left: 0,
|
|
481
|
+
pointerEvents: 'none',
|
|
482
|
+
}, children: createPreview })), indexedChildren, _jsxs("svg", { width: plotWidth, height: row.height, style: {
|
|
483
|
+
position: 'absolute',
|
|
484
|
+
top: 0,
|
|
485
|
+
left: 0,
|
|
486
|
+
pointerEvents: 'none',
|
|
487
|
+
}, children: [parts.line &&
|
|
488
|
+
cursorX !== null &&
|
|
489
|
+
cursorX >= 0 &&
|
|
490
|
+
cursorX <= plotWidth && (_jsx("line", { x1: Math.round(cursorX), y1: 0, x2: Math.round(cursorX), y2: row.height, stroke: cursorColor, strokeWidth: 1, shapeRendering: "crispEdges" })), parts.chip === 'flag' &&
|
|
491
|
+
trackerSamples.map((s, i) => s.py > flagBase ? (_jsx("line", { x1: s.px, y1: flagBase, x2: s.px, y2: s.py, stroke: cursorColor, strokeWidth: 1, opacity: 0.5 }, `staff-${i}`)) : null), parts.chip === 'flag' &&
|
|
492
|
+
trackerFlags.map((f, i) => f.topPy > flagBase ? (_jsx("line", { x1: f.px, y1: flagBase, x2: f.px, y2: f.topPy, stroke: cursorColor, strokeWidth: 1, opacity: 0.5 }, `boxstaff-${i}`)) : null), parts.dots &&
|
|
493
|
+
trackerSamples.map((s, i) => (_jsx("circle", { cx: s.px, cy: s.py, r: 3, fill: s.color, stroke: background, strokeWidth: background ? 1 : 0 }, `dot-${i}`)))] }), showTime && timeX !== null && cursorTime !== null && (_jsx("div", { style: {
|
|
494
|
+
...chipStyle,
|
|
495
|
+
top: `${flagTop}px`,
|
|
496
|
+
left: timeX > plotWidth * LABEL_FLIP_FRACTION
|
|
497
|
+
? undefined
|
|
498
|
+
: `${timeX + 4}px`,
|
|
499
|
+
right: timeX > plotWidth * LABEL_FLIP_FRACTION
|
|
500
|
+
? `${plotWidth - timeX + 4}px`
|
|
501
|
+
: undefined,
|
|
502
|
+
color: cursorColor,
|
|
503
|
+
}, children: formatTime(cursorTime) })), parts.chip === 'inline' &&
|
|
504
|
+
trackerSamples.map((s, i) => {
|
|
505
|
+
// Flip the chip left of its dot near the right edge so it stays in-plot.
|
|
506
|
+
const flip = s.px > plotWidth * LABEL_FLIP_FRACTION;
|
|
507
|
+
// Clamp within the row so a chip near the top/bottom isn't clipped by
|
|
508
|
+
// (or spilling into) the neighbouring row. Chip-vs-chip de-overlap is
|
|
509
|
+
// a later refinement; this keeps each chip inside its own row.
|
|
510
|
+
const top = Math.max(flagLineHeight / 2, Math.min(row.height - flagLineHeight / 2, s.py));
|
|
511
|
+
return (_jsx("div", { style: {
|
|
512
|
+
...chipStyle,
|
|
513
|
+
top: `${top}px`,
|
|
514
|
+
transform: 'translateY(-50%)',
|
|
515
|
+
left: flip ? undefined : `${s.px + 8}px`,
|
|
516
|
+
right: flip ? `${plotWidth - s.px + 8}px` : undefined,
|
|
517
|
+
color: s.color,
|
|
518
|
+
}, children: s.format(s.value) }, i));
|
|
519
|
+
}), parts.chip === 'flag' &&
|
|
520
|
+
cursorX !== null &&
|
|
521
|
+
trackerSamples.map((s, i) => (
|
|
522
|
+
// The flag flies from the top of its staff — chip top at the staff top
|
|
523
|
+
// (`flagBase`), beside the pole at the point's x (shared `flagChipX`).
|
|
524
|
+
_jsx("div", { style: {
|
|
525
|
+
...chipStyle,
|
|
526
|
+
top: `${flagBase}px`,
|
|
527
|
+
...flagChipX(s.px, plotWidth),
|
|
528
|
+
color: s.color,
|
|
529
|
+
}, children: s.format(s.value) }, i))), parts.chip === 'flag' &&
|
|
530
|
+
trackerFlags.map((f, i) => (_jsx("div", { style: {
|
|
346
531
|
...chipStyle,
|
|
347
|
-
top: `${
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
: undefined,
|
|
354
|
-
color: cursorColor,
|
|
355
|
-
}, children: formatTime(cursorTime) })), parts.chip === 'inline' &&
|
|
356
|
-
trackerSamples.map((s, i) => {
|
|
357
|
-
// Flip the chip left of its dot near the right edge so it stays in-plot.
|
|
358
|
-
const flip = s.px > plotWidth * LABEL_FLIP_FRACTION;
|
|
359
|
-
// Clamp within the row so a chip near the top/bottom isn't clipped by
|
|
360
|
-
// (or spilling into) the neighbouring row. Chip-vs-chip de-overlap is
|
|
361
|
-
// a later refinement; this keeps each chip inside its own row.
|
|
362
|
-
const top = Math.max(flagLineHeight / 2, Math.min(row.height - flagLineHeight / 2, s.py));
|
|
363
|
-
return (_jsx("div", { style: {
|
|
364
|
-
...chipStyle,
|
|
365
|
-
top: `${top}px`,
|
|
366
|
-
transform: 'translateY(-50%)',
|
|
367
|
-
left: flip ? undefined : `${s.px + 8}px`,
|
|
368
|
-
right: flip ? `${plotWidth - s.px + 8}px` : undefined,
|
|
369
|
-
color: s.color,
|
|
370
|
-
}, children: s.format(s.value) }, i));
|
|
371
|
-
}), parts.chip === 'flag' &&
|
|
372
|
-
cursorX !== null &&
|
|
373
|
-
trackerSamples.map((s, i) => {
|
|
374
|
-
// Each flag caps its own staff — anchored to the data point's x
|
|
375
|
-
// (`s.px`), riding the point with the dot + staff, not the cursor.
|
|
376
|
-
// Flip left near the right edge so it stays in-plot.
|
|
377
|
-
const flip = s.px > plotWidth * LABEL_FLIP_FRACTION;
|
|
378
|
-
return (_jsx("div", { style: {
|
|
379
|
-
...chipStyle,
|
|
380
|
-
top: `${flagBase + i * flagLineHeight}px`,
|
|
381
|
-
left: flip ? undefined : `${s.px + 4}px`,
|
|
382
|
-
right: flip ? `${plotWidth - s.px + 4}px` : undefined,
|
|
383
|
-
color: s.color,
|
|
384
|
-
}, children: s.format(s.value) }, i));
|
|
385
|
-
}), parts.chip === 'flag' &&
|
|
386
|
-
trackerFlags.map((f, i) => {
|
|
387
|
-
const flip = f.px > plotWidth * LABEL_FLIP_FRACTION;
|
|
388
|
-
return (_jsx("div", { style: {
|
|
389
|
-
...chipStyle,
|
|
390
|
-
top: `${flagBase}px`,
|
|
391
|
-
left: flip ? undefined : `${f.px + 4}px`,
|
|
392
|
-
right: flip ? `${plotWidth - f.px + 4}px` : undefined,
|
|
393
|
-
display: 'flex',
|
|
394
|
-
flexDirection: 'row',
|
|
395
|
-
gap: '6px',
|
|
396
|
-
}, children: f.lines.map((l, j) => (_jsx("span", { style: { color: l.color }, children: l.text }, j))) }, `boxflag-${i}`));
|
|
397
|
-
})] }), indexedChildren] }));
|
|
532
|
+
top: `${flagBase}px`,
|
|
533
|
+
...flagChipX(f.px, plotWidth),
|
|
534
|
+
display: 'flex',
|
|
535
|
+
flexDirection: 'row',
|
|
536
|
+
gap: '6px',
|
|
537
|
+
}, children: f.lines.map((l, j) => (_jsx("span", { style: { color: l.color }, children: l.text }, j))) }, `boxflag-${i}`)))] }) }));
|
|
398
538
|
}
|
|
399
539
|
//# sourceMappingURL=Layers.js.map
|
package/dist/YAxis.d.ts
CHANGED
|
@@ -22,6 +22,21 @@ export interface YAxisProps {
|
|
|
22
22
|
* (e.g. `',.2f'`) when you want finer readout precision. See {@link AxisFormat}.
|
|
23
23
|
*/
|
|
24
24
|
format?: AxisFormat;
|
|
25
|
+
/**
|
|
26
|
+
* Explicit ticks — `{ at, label }` in axis-value units — instead of the
|
|
27
|
+
* scale's automatic ticks, driving BOTH the labels and the row's gridlines so
|
|
28
|
+
* the two align. The y-axis counterpart of `<XAxis ticks>` (same shape): the
|
|
29
|
+
* lever for a non-uniform axis like pace, where the caller chooses round-pace
|
|
30
|
+
* positions and their own `m:ss` labels (`{ at: -300, label: '5:00' }`). `at`
|
|
31
|
+
* values outside `[min, max]` extrapolate off-plot (the scale does not clamp).
|
|
32
|
+
* Pass `[]` to draw none. For a live / animating chart, **memoize the array** —
|
|
33
|
+
* an inline `ticks={[…]}` is a fresh reference each render and re-registers the
|
|
34
|
+
* axis (like `format`; harmless for a static chart).
|
|
35
|
+
*/
|
|
36
|
+
ticks?: ReadonlyArray<{
|
|
37
|
+
readonly at: number;
|
|
38
|
+
readonly label: string;
|
|
39
|
+
}>;
|
|
25
40
|
/** Gutter width in CSS pixels (default 50). */
|
|
26
41
|
width?: number;
|
|
27
42
|
/**
|
|
@@ -38,5 +53,5 @@ export interface YAxisProps {
|
|
|
38
53
|
* tick marks + labels from that scale. Charts attach via `<LineChart axis="id">`
|
|
39
54
|
* (default: the first axis).
|
|
40
55
|
*/
|
|
41
|
-
export declare function YAxis({ id, side, label, min, max, format, width, index, }: YAxisProps): import("react/jsx-runtime").JSX.Element;
|
|
56
|
+
export declare function YAxis({ id, side, label, min, max, format, ticks, width, index, }: YAxisProps): import("react/jsx-runtime").JSX.Element;
|
|
42
57
|
//# sourceMappingURL=YAxis.d.ts.map
|
package/dist/YAxis.js
CHANGED
|
@@ -13,7 +13,7 @@ const TICK_COUNT = 5;
|
|
|
13
13
|
* tick marks + labels from that scale. Charts attach via `<LineChart axis="id">`
|
|
14
14
|
* (default: the first axis).
|
|
15
15
|
*/
|
|
16
|
-
export function YAxis({ id, side = 'left', label, min, max, format, width = DEFAULT_WIDTH, index = 0, }) {
|
|
16
|
+
export function YAxis({ id, side = 'left', label, min, max, format, ticks, width = DEFAULT_WIDTH, index = 0, }) {
|
|
17
17
|
const container = useContext(ContainerContext);
|
|
18
18
|
if (container === null) {
|
|
19
19
|
throw new Error('<YAxis> must be rendered inside a <ChartContainer>');
|
|
@@ -22,7 +22,16 @@ export function YAxis({ id, side = 'left', label, min, max, format, width = DEFA
|
|
|
22
22
|
if (row === null) {
|
|
23
23
|
throw new Error('<YAxis> must be rendered inside a <ChartRow>');
|
|
24
24
|
}
|
|
25
|
-
const spec = useMemo(() => ({
|
|
25
|
+
const spec = useMemo(() => ({
|
|
26
|
+
id,
|
|
27
|
+
side,
|
|
28
|
+
width,
|
|
29
|
+
min,
|
|
30
|
+
max,
|
|
31
|
+
format,
|
|
32
|
+
tickValues: ticks?.map((t) => t.at),
|
|
33
|
+
index,
|
|
34
|
+
}), [id, side, width, min, max, format, ticks, index]);
|
|
26
35
|
// A stable per-instance slot (see useSlotKey) keeps this axis in a fixed
|
|
27
36
|
// registry position, so a min/max/side change updates in place rather than
|
|
28
37
|
// re-appending (which would move the first axis behind a later one and
|
|
@@ -37,10 +46,17 @@ export function YAxis({ id, side = 'left', label, min, max, format, width = DEFA
|
|
|
37
46
|
}, [registerAxis, slot, spec]);
|
|
38
47
|
const { theme } = container;
|
|
39
48
|
const yScale = row.yScales.get(id);
|
|
40
|
-
const ticks = yScale ? yScale.ticks(TICK_COUNT) : [];
|
|
41
49
|
// Same formatter the readout uses (resolved per axis on the row), so a tick and
|
|
42
50
|
// a cursor value read identically.
|
|
43
51
|
const fmt = yScale ? resolveAxisFormat(yScale, TICK_COUNT, format) : String;
|
|
52
|
+
// Explicit `{ at, label }` ticks render verbatim (each label at its `at`),
|
|
53
|
+
// overriding the auto-picked d3 ticks; otherwise label the scale's ticks via `fmt`.
|
|
54
|
+
const tickList = ticks
|
|
55
|
+
? ticks.map((t) => ({ value: t.at, label: t.label }))
|
|
56
|
+
: (yScale ? yScale.ticks(TICK_COUNT) : []).map((t) => ({
|
|
57
|
+
value: t,
|
|
58
|
+
label: fmt(t),
|
|
59
|
+
}));
|
|
44
60
|
// The row reserves a slot per axis column (the widest in that column across
|
|
45
61
|
// rows). Size the box to the slot and align this axis's own (narrower)
|
|
46
62
|
// content toward the plot — left axes flush right, right axes flush left — so
|
|
@@ -60,13 +76,13 @@ export function YAxis({ id, side = 'left', label, min, max, format, width = DEFA
|
|
|
60
76
|
fontSize: `${theme.font.size}px`,
|
|
61
77
|
color: theme.axis.label,
|
|
62
78
|
}, children: [yScale &&
|
|
63
|
-
|
|
79
|
+
tickList.map(({ value, label }) => (_jsx("div", { style: {
|
|
64
80
|
position: 'absolute',
|
|
65
|
-
top: `${yScale(
|
|
81
|
+
top: `${yScale(value)}px`,
|
|
66
82
|
[side === 'left' ? 'right' : 'left']: '4px',
|
|
67
83
|
transform: 'translateY(-50%)',
|
|
68
84
|
whiteSpace: 'nowrap',
|
|
69
|
-
}, children:
|
|
85
|
+
}, children: label }, value))), _jsx("div", { style: {
|
|
70
86
|
position: 'absolute',
|
|
71
87
|
[side === 'left' ? 'left' : 'right']: '1px',
|
|
72
88
|
top: 0,
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { type AnnotationSpec } from './context.js';
|
|
2
|
+
/**
|
|
3
|
+
* Greedy left→right lane packing for the **top-flag** labels (markers + regions): a
|
|
4
|
+
* label that would overlap the one to its left drops to the next free lane below,
|
|
5
|
+
* so close-in-x labels stack instead of colliding (and a dragged label slides under
|
|
6
|
+
* its neighbour). Returns slot-key → lane (0 = top). Baselines, whose labels anchor
|
|
7
|
+
* at the left at their own y, don't participate.
|
|
8
|
+
*/
|
|
9
|
+
export declare function computeLabelLanes(annotations: readonly AnnotationSpec[], toPixel: (axisX: number) => number): Map<symbol, number>;
|
|
10
|
+
export interface MarkerProps {
|
|
11
|
+
/** x position in axis units — epoch ms on a time axis, the value on a value
|
|
12
|
+
* axis. (The generalisation of the mockup's "time line": a mark at an x, time
|
|
13
|
+
* or value.) */
|
|
14
|
+
at: number;
|
|
15
|
+
/** Chip label; omit to auto-label with the shared x formatter (the axis's). */
|
|
16
|
+
label?: string;
|
|
17
|
+
/** Stable consumer id — a click reports it via the container's
|
|
18
|
+
* `onSelectAnnotation`, so the consumer can track which mark is selected. */
|
|
19
|
+
id?: string;
|
|
20
|
+
/** Controlled selection — brightens to the front (level 1). Handles are an
|
|
21
|
+
* edit-mode hover affordance, not a selection cue. Ignored if not `selectable`. */
|
|
22
|
+
selected?: boolean;
|
|
23
|
+
/** Whether the mark responds to hover + selection (default `true`). When
|
|
24
|
+
* `false` it's inert background context — drawn at the back (level 3) always,
|
|
25
|
+
* no hover, no select, no edit. */
|
|
26
|
+
selectable?: boolean;
|
|
27
|
+
/** Controlled hover (OR'd with pointer hover) — lets a legend row light the mark
|
|
28
|
+
* remotely. Pair with the container's `onHoverAnnotation` to sync both ways. */
|
|
29
|
+
hovered?: boolean;
|
|
30
|
+
/** When `true`, this mark is in **single-annotation edit** (the double-click
|
|
31
|
+
* target): handles stay out, it's draggable, and it reads as level 1 — while
|
|
32
|
+
* other marks stay static. Independent of the container's global
|
|
33
|
+
* `editAnnotations`. Pair with `onEditAnnotation` (the consumer holds an
|
|
34
|
+
* `editingId` and sets `editing={editingId === id}`). */
|
|
35
|
+
editing?: boolean;
|
|
36
|
+
/** Make the marker **editable** (in edit mode): dragging its line reports the
|
|
37
|
+
* new `at` (controlled — wire it back to `at`). The whole line moves. */
|
|
38
|
+
onChange?: (at: number) => void;
|
|
39
|
+
}
|
|
40
|
+
/** A vertical line at an x position (a time, a distance, a lap boundary). */
|
|
41
|
+
export declare function Marker({ at, label, id, selected, selectable, hovered, editing, onChange, }: MarkerProps): import("react/jsx-runtime").JSX.Element;
|
|
42
|
+
export interface BaselineProps {
|
|
43
|
+
/** y value in the linked axis's units. */
|
|
44
|
+
value: number;
|
|
45
|
+
/** Which `<YAxis>` (by id) to measure against; omit for the row's default axis. */
|
|
46
|
+
axis?: string;
|
|
47
|
+
/** Chip label; omit to format `value` with that axis's formatter. */
|
|
48
|
+
label?: string;
|
|
49
|
+
/** Stable consumer id — a click reports it via `onSelectAnnotation`. */
|
|
50
|
+
id?: string;
|
|
51
|
+
/** Controlled selection — brightens to the front (level 1). Handles are an
|
|
52
|
+
* edit-mode hover affordance, not a selection cue. Ignored if not `selectable`. */
|
|
53
|
+
selected?: boolean;
|
|
54
|
+
/** Whether the baseline responds to hover + selection (default `true`). When
|
|
55
|
+
* `false` it's inert background context — drawn at the back (level 3) always. */
|
|
56
|
+
selectable?: boolean;
|
|
57
|
+
/** Controlled hover (OR'd with pointer hover) — lets a legend row light the mark
|
|
58
|
+
* remotely. Pair with the container's `onHoverAnnotation` to sync both ways. */
|
|
59
|
+
hovered?: boolean;
|
|
60
|
+
/** When `true`, this mark is in **single-annotation edit** (the double-click
|
|
61
|
+
* target): handles stay out, it's draggable, and it reads as level 1 — while
|
|
62
|
+
* other marks stay static. Independent of the container's global
|
|
63
|
+
* `editAnnotations`. Pair with `onEditAnnotation` (the consumer holds an
|
|
64
|
+
* `editingId` and sets `editing={editingId === id}`). */
|
|
65
|
+
editing?: boolean;
|
|
66
|
+
/** Make the baseline **editable** (in edit mode): dragging it vertically reports
|
|
67
|
+
* the new `value` (controlled — wire it back to `value`). */
|
|
68
|
+
onChange?: (value: number) => void;
|
|
69
|
+
}
|
|
70
|
+
/** A horizontal line at a y value, scaled against one row axis (RTC's `Baseline`).
|
|
71
|
+
* Its label anchors at the left, at the line's height. */
|
|
72
|
+
export declare function Baseline({ value, axis, label, id, selected, selectable, hovered, editing, onChange, }: BaselineProps): import("react/jsx-runtime").JSX.Element | null;
|
|
73
|
+
export interface RegionProps {
|
|
74
|
+
/** Start x in axis units (time or value). */
|
|
75
|
+
from: number;
|
|
76
|
+
/** End x in axis units. */
|
|
77
|
+
to: number;
|
|
78
|
+
/** Chip label; omit to auto-label `from–to` with the shared x formatter. */
|
|
79
|
+
label?: string;
|
|
80
|
+
/** Stable consumer id — a click (or double-click outside edit) reports it via
|
|
81
|
+
* `onSelectAnnotation`. */
|
|
82
|
+
id?: string;
|
|
83
|
+
/** Controlled selection — brightens to the front (level 1; the body too). Edge
|
|
84
|
+
* handles are an edit-mode hover affordance, not a selection cue. Ignored if not
|
|
85
|
+
* `selectable`. */
|
|
86
|
+
selected?: boolean;
|
|
87
|
+
/** Whether the region responds to hover + selection (default `true`). When
|
|
88
|
+
* `false` it's inert background context — drawn at the back (level 3) always,
|
|
89
|
+
* and the double-click hit-test skips it. */
|
|
90
|
+
selectable?: boolean;
|
|
91
|
+
/** Controlled hover (OR'd with pointer hover) — lets a legend row light the mark
|
|
92
|
+
* remotely. Pair with the container's `onHoverAnnotation` to sync both ways. */
|
|
93
|
+
hovered?: boolean;
|
|
94
|
+
/** When `true`, this mark is in **single-annotation edit** (the double-click
|
|
95
|
+
* target): handles stay out, it's draggable, and it reads as level 1 — while
|
|
96
|
+
* other marks stay static. Independent of the container's global
|
|
97
|
+
* `editAnnotations`. Pair with `onEditAnnotation` (the consumer holds an
|
|
98
|
+
* `editingId` and sets `editing={editingId === id}`). */
|
|
99
|
+
editing?: boolean;
|
|
100
|
+
/** Make the region **editable** (in edit mode): drag the body to move it (both
|
|
101
|
+
* edges shift), drag an edge to resize. Reports the new `{ from, to }`. */
|
|
102
|
+
onChange?: (next: {
|
|
103
|
+
from: number;
|
|
104
|
+
to: number;
|
|
105
|
+
}) => void;
|
|
106
|
+
}
|
|
107
|
+
/** A shaded span over an x range — a lap, a zone, a selected interval. Its label
|
|
108
|
+
* flies as a flag off the left edge. */
|
|
109
|
+
export declare function Region({ from, to, label, id, selected, selectable, hovered, editing, onChange, }: RegionProps): import("react/jsx-runtime").JSX.Element;
|
|
110
|
+
//# sourceMappingURL=annotations.d.ts.map
|