@pond-ts/charts 0.31.2 → 0.33.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 +75 -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 +238 -96
- package/dist/YAxis.d.ts +16 -1
- package/dist/YAxis.js +22 -6
- package/dist/annotations.d.ts +129 -0
- package/dist/annotations.js +499 -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,38 @@ 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: selected, OR the
|
|
358
|
+
// single-edit target (`editing`). Checking `editing` too means a consumer that
|
|
359
|
+
// sets `editing` without also setting `selected` still gets the exit signal.
|
|
360
|
+
// Marks stop their own clicks in DragArea, so this only fires on true empty space.
|
|
361
|
+
if (c.editAnnotations ||
|
|
362
|
+
c.annotations.some((a) => a.selected || a.editing)) {
|
|
363
|
+
c.onSelectAnnotation?.(null);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
253
366
|
const r = rowRef.current;
|
|
254
367
|
const rect = e.currentTarget.getBoundingClientRect();
|
|
255
368
|
const hit = resolveSelection(r.layers, e.clientX - rect.left, e.clientY - rect.top, c.xScale, (axisId) => r.yScales.get(axisId ?? r.defaultAxisId));
|
|
@@ -281,19 +394,9 @@ export function Layers({ children }) {
|
|
|
281
394
|
// dot ('inline', clamped within the row) or stack at the top of the flag staff
|
|
282
395
|
// ('flag'). line / point / none draw no chips — surface values off-chart.
|
|
283
396
|
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
|
-
};
|
|
397
|
+
// Cursor chips share the annotation label look (filled, no outline) — one
|
|
398
|
+
// source of truth so a flag and a placed label read as the same object.
|
|
399
|
+
const chipStyle = flagChipStyle(container.theme);
|
|
297
400
|
// Show the cursor's time atop the readout (opt-in via `cursorTime`), whenever
|
|
298
401
|
// the cursor is active (any mode that draws marks). A single chip at the cursor
|
|
299
402
|
// x, top of the row; for `flag` it sits above the value chips (which shift down).
|
|
@@ -304,96 +407,135 @@ export function Layers({ children }) {
|
|
|
304
407
|
cursorTime !== null &&
|
|
305
408
|
(parts.line || parts.dots) &&
|
|
306
409
|
row.isFirstRow;
|
|
307
|
-
// Flag
|
|
308
|
-
//
|
|
410
|
+
// Flag geometry: each value flies as a flag from the top of its own staff — the
|
|
411
|
+
// chip's top sits at `flagBase` (just below the time chip when shown) and the
|
|
412
|
+
// staff drops from there to the dot. (Chips share that top and spread by x, so
|
|
413
|
+
// near-coincident flags can overlap — a de-overlap heuristic is a follow-up.)
|
|
309
414
|
const flagTop = 2;
|
|
310
415
|
const flagBase = flagTop + (showTime ? flagLineHeight : 0);
|
|
311
|
-
const stackBottom = flagBase + trackerSamples.length * flagLineHeight;
|
|
312
416
|
// The cursor-time chip caps the readout. In `flag` mode it tops the flag stack,
|
|
313
417
|
// so anchor it to the stack's x (the nearest sample's point) so time + flag +
|
|
314
418
|
// staff + dot read as one column; otherwise it labels the cursor line at cursorX.
|
|
315
419
|
const timeX = parts.chip === 'flag' && trackerSamples.length > 0
|
|
316
420
|
? trackerSamples[0].px
|
|
317
421
|
: cursorX;
|
|
422
|
+
// Cross-row guide lines: the x-positions of annotations on the OTHER rows
|
|
423
|
+
// (markers + region edges), so a mark on one row reads against this row's data +
|
|
424
|
+
// the shared x axis. A mark's own row skips itself; baselines cast no vertical
|
|
425
|
+
// guide (empty `xs`). Faint + dashed so they read as reference, not data.
|
|
426
|
+
const guideXs = container.annotations
|
|
427
|
+
.filter((a) => a.rowKey !== row.rowKey)
|
|
428
|
+
.flatMap((a) => a.xs)
|
|
429
|
+
.map((xv) => xScale(xv));
|
|
430
|
+
const guideColor = container.theme.annotation?.color ?? gridColor;
|
|
431
|
+
// Create preview: while a tool is armed, the hovered row (the one with
|
|
432
|
+
// `createPt`) shows a cursor-style line tracking the pointer — vertical for
|
|
433
|
+
// marker/region, horizontal for baseline, a span once a region is being dragged.
|
|
434
|
+
// The OTHER rows show the faint guide at the shared preview x (markers/regions).
|
|
435
|
+
const creating = container.creating;
|
|
436
|
+
let createPreview = null;
|
|
437
|
+
if (creating !== null && createPt !== null) {
|
|
438
|
+
if (creating === 'baseline') {
|
|
439
|
+
createPreview = (_jsx("line", { x1: 0, y1: createPt.y, x2: plotWidth, y2: createPt.y, stroke: guideColor, strokeWidth: 1, opacity: 0.85, shapeRendering: "crispEdges" }));
|
|
440
|
+
}
|
|
441
|
+
else if (drawFrom !== null) {
|
|
442
|
+
const l = Math.min(drawFrom, createPt.x);
|
|
443
|
+
const w = Math.abs(createPt.x - drawFrom);
|
|
444
|
+
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" })] }));
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
createPreview = (_jsx("line", { x1: createPt.x, y1: 0, x2: createPt.x, y2: row.height, stroke: guideColor, strokeWidth: 1, opacity: 0.85, shapeRendering: "crispEdges" }));
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
else if (creating !== null && creating !== 'baseline' && cursorX !== null) {
|
|
451
|
+
// Another row — the faint preview guide at the shared pointer x.
|
|
452
|
+
createPreview = (_jsx("line", { x1: cursorX, y1: 0, x2: cursorX, y2: row.height, stroke: guideColor, strokeWidth: 1, opacity: 0.22, strokeDasharray: "2 3", shapeRendering: "crispEdges" }));
|
|
453
|
+
}
|
|
318
454
|
// Inject each draw layer's JSX position so it registers its declaration order
|
|
319
455
|
// (z-stack: lower index at the back), independent of mount timing.
|
|
320
456
|
const indexedChildren = Children.map(children, (child, index) => isValidElement(child)
|
|
321
457
|
? cloneElement(child, { index })
|
|
322
458
|
: 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
|
-
|
|
459
|
+
return (_jsx(LayersContext.Provider, { value: registry, children: _jsxs("div", { ref: plotRef, style: {
|
|
460
|
+
position: 'relative',
|
|
461
|
+
width: `${plotWidth}px`,
|
|
462
|
+
height: `${row.height}px`,
|
|
463
|
+
// Edit mode: a plain cursor on the plot (the annotations supply their
|
|
464
|
+
// own grab/resize cursors); crosshair only when the data cursor is live
|
|
465
|
+
// (suppressed in single-annotation edit too, not just global edit).
|
|
466
|
+
cursor: editingActive ? 'default' : 'crosshair',
|
|
467
|
+
// The turquoise edit border — the "you're in *global* Edit" signal (not
|
|
468
|
+
// single-annotation edit). Inset shadow so it doesn't shift layout.
|
|
469
|
+
boxShadow: container.editAnnotations
|
|
470
|
+
? `inset 0 0 0 1px ${guideColor}`
|
|
471
|
+
: undefined,
|
|
472
|
+
// Let pan/zoom own touch gestures (no native scroll) when enabled.
|
|
473
|
+
touchAction: container.panZoom ? 'none' : 'auto',
|
|
474
|
+
}, 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: {
|
|
475
|
+
position: 'absolute',
|
|
476
|
+
top: 0,
|
|
477
|
+
left: 0,
|
|
478
|
+
pointerEvents: 'none',
|
|
479
|
+
}, 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: {
|
|
480
|
+
position: 'absolute',
|
|
481
|
+
top: 0,
|
|
482
|
+
left: 0,
|
|
483
|
+
pointerEvents: 'none',
|
|
484
|
+
}, children: createPreview })), indexedChildren, _jsxs("svg", { width: plotWidth, height: row.height, style: {
|
|
485
|
+
position: 'absolute',
|
|
486
|
+
top: 0,
|
|
487
|
+
left: 0,
|
|
488
|
+
pointerEvents: 'none',
|
|
489
|
+
}, children: [parts.line &&
|
|
490
|
+
cursorX !== null &&
|
|
491
|
+
cursorX >= 0 &&
|
|
492
|
+
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' &&
|
|
493
|
+
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' &&
|
|
494
|
+
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 &&
|
|
495
|
+
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: {
|
|
496
|
+
...chipStyle,
|
|
497
|
+
top: `${flagTop}px`,
|
|
498
|
+
left: timeX > plotWidth * LABEL_FLIP_FRACTION
|
|
499
|
+
? undefined
|
|
500
|
+
: `${timeX + 4}px`,
|
|
501
|
+
right: timeX > plotWidth * LABEL_FLIP_FRACTION
|
|
502
|
+
? `${plotWidth - timeX + 4}px`
|
|
503
|
+
: undefined,
|
|
504
|
+
color: cursorColor,
|
|
505
|
+
}, children: formatTime(cursorTime) })), parts.chip === 'inline' &&
|
|
506
|
+
trackerSamples.map((s, i) => {
|
|
507
|
+
// Flip the chip left of its dot near the right edge so it stays in-plot.
|
|
508
|
+
const flip = s.px > plotWidth * LABEL_FLIP_FRACTION;
|
|
509
|
+
// Clamp within the row so a chip near the top/bottom isn't clipped by
|
|
510
|
+
// (or spilling into) the neighbouring row. Chip-vs-chip de-overlap is
|
|
511
|
+
// a later refinement; this keeps each chip inside its own row.
|
|
512
|
+
const top = Math.max(flagLineHeight / 2, Math.min(row.height - flagLineHeight / 2, s.py));
|
|
513
|
+
return (_jsx("div", { style: {
|
|
514
|
+
...chipStyle,
|
|
515
|
+
top: `${top}px`,
|
|
516
|
+
transform: 'translateY(-50%)',
|
|
517
|
+
left: flip ? undefined : `${s.px + 8}px`,
|
|
518
|
+
right: flip ? `${plotWidth - s.px + 8}px` : undefined,
|
|
519
|
+
color: s.color,
|
|
520
|
+
}, children: s.format(s.value) }, i));
|
|
521
|
+
}), parts.chip === 'flag' &&
|
|
522
|
+
cursorX !== null &&
|
|
523
|
+
trackerSamples.map((s, i) => (
|
|
524
|
+
// The flag flies from the top of its staff — chip top at the staff top
|
|
525
|
+
// (`flagBase`), beside the pole at the point's x (shared `flagChipX`).
|
|
526
|
+
_jsx("div", { style: {
|
|
527
|
+
...chipStyle,
|
|
528
|
+
top: `${flagBase}px`,
|
|
529
|
+
...flagChipX(s.px, plotWidth),
|
|
530
|
+
color: s.color,
|
|
531
|
+
}, children: s.format(s.value) }, i))), parts.chip === 'flag' &&
|
|
532
|
+
trackerFlags.map((f, i) => (_jsx("div", { style: {
|
|
346
533
|
...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] }));
|
|
534
|
+
top: `${flagBase}px`,
|
|
535
|
+
...flagChipX(f.px, plotWidth),
|
|
536
|
+
display: 'flex',
|
|
537
|
+
flexDirection: 'row',
|
|
538
|
+
gap: '6px',
|
|
539
|
+
}, children: f.lines.map((l, j) => (_jsx("span", { style: { color: l.color }, children: l.text }, j))) }, `boxflag-${i}`)))] }) }));
|
|
398
540
|
}
|
|
399
541
|
//# 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,
|