@pond-ts/charts 0.31.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 +3254 -0
- package/LICENSE +21 -0
- package/README.md +229 -0
- package/dist/AreaChart.d.ts +85 -0
- package/dist/AreaChart.js +119 -0
- package/dist/BandChart.d.ts +55 -0
- package/dist/BandChart.js +93 -0
- package/dist/BarChart.d.ts +72 -0
- package/dist/BarChart.js +137 -0
- package/dist/BoxPlot.d.ts +77 -0
- package/dist/BoxPlot.js +137 -0
- package/dist/Canvas.d.ts +37 -0
- package/dist/Canvas.js +39 -0
- package/dist/ChartContainer.d.ts +106 -0
- package/dist/ChartContainer.js +306 -0
- package/dist/ChartRow.d.ts +29 -0
- package/dist/ChartRow.js +215 -0
- package/dist/Layers.d.ts +22 -0
- package/dist/Layers.js +399 -0
- package/dist/LineChart.d.ts +60 -0
- package/dist/LineChart.js +105 -0
- package/dist/ScatterChart.d.ts +84 -0
- package/dist/ScatterChart.js +139 -0
- package/dist/TimeAxis.d.ts +9 -0
- package/dist/TimeAxis.js +12 -0
- package/dist/XAxis.d.ts +39 -0
- package/dist/XAxis.js +84 -0
- package/dist/YAxis.d.ts +42 -0
- package/dist/YAxis.js +86 -0
- package/dist/annotations.d.ts +110 -0
- package/dist/annotations.js +459 -0
- package/dist/area.d.ts +54 -0
- package/dist/area.js +186 -0
- package/dist/band.d.ts +31 -0
- package/dist/band.js +57 -0
- package/dist/bars.d.ts +96 -0
- package/dist/bars.js +171 -0
- package/dist/box.d.ts +59 -0
- package/dist/box.js +140 -0
- package/dist/chip.d.ts +23 -0
- package/dist/chip.js +43 -0
- package/dist/cjs-fallback.cjs +16 -0
- package/dist/context.d.ts +362 -0
- package/dist/context.js +5 -0
- package/dist/curve.d.ts +22 -0
- package/dist/curve.js +13 -0
- package/dist/data.d.ts +154 -0
- package/dist/data.js +197 -0
- package/dist/domain.d.ts +19 -0
- package/dist/domain.js +61 -0
- package/dist/encoding.d.ts +89 -0
- package/dist/encoding.js +144 -0
- package/dist/format.d.ts +53 -0
- package/dist/format.js +47 -0
- package/dist/gaps.d.ts +146 -0
- package/dist/gaps.js +209 -0
- package/dist/grid.d.ts +11 -0
- package/dist/grid.js +29 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.js +34 -0
- package/dist/line.d.ts +46 -0
- package/dist/line.js +88 -0
- package/dist/range.d.ts +15 -0
- package/dist/range.js +27 -0
- package/dist/scatter.d.ts +70 -0
- package/dist/scatter.js +213 -0
- package/dist/select.d.ts +13 -0
- package/dist/select.js +23 -0
- package/dist/slots.d.ts +48 -0
- package/dist/slots.js +64 -0
- package/dist/theme.d.ts +224 -0
- package/dist/theme.js +232 -0
- package/dist/tracker.d.ts +30 -0
- package/dist/tracker.js +47 -0
- package/dist/use-slot-key.d.ts +21 -0
- package/dist/use-slot-key.js +25 -0
- package/dist/viewport.d.ts +20 -0
- package/dist/viewport.js +30 -0
- package/package.json +67 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { useContext, useEffect, useMemo } from 'react';
|
|
2
|
+
import { fromTimeSeries } from './data.js';
|
|
3
|
+
import { drawScatter, hitTestScatter, nearestIndex, scatterExtent, } from './scatter.js';
|
|
4
|
+
import { resolveEncoding, } from './encoding.js';
|
|
5
|
+
import { ContainerContext, LayersContext } from './context.js';
|
|
6
|
+
import { useSlotKey } from './use-slot-key.js';
|
|
7
|
+
/**
|
|
8
|
+
* A scatter draw layer: one mark per finite point at `(time, column-value)`,
|
|
9
|
+
* with **data-driven radius + colour** (the signed-off exception — encode from
|
|
10
|
+
* columns via scales, not a per-event style callback). Reads `column` into a
|
|
11
|
+
* {@link ChartSeries} (gaps as NaN → no mark), registers into the enclosing
|
|
12
|
+
* {@link Layers} (scaling against its `axis`), and renders nothing to the DOM —
|
|
13
|
+
* the row draws it.
|
|
14
|
+
*
|
|
15
|
+
* **Interactions.** Hover snaps the tracker dot to the nearest point
|
|
16
|
+
* (`sampleAt`), and that sample flows to the container's `onTrackerChanged` —
|
|
17
|
+
* the nearest-point readout. Scatter reuses the shared tracker rather than
|
|
18
|
+
* adding a separate `onNearest` channel, so a scatter reads out exactly like a
|
|
19
|
+
* line. Click selection hit-tests each point's disc (`hitTest`); the selected
|
|
20
|
+
* point (matching both its key and this series' label) gets a highlight ring.
|
|
21
|
+
*
|
|
22
|
+
* ```tsx
|
|
23
|
+
* <Layers>
|
|
24
|
+
* <ScatterChart
|
|
25
|
+
* series={s}
|
|
26
|
+
* column="price"
|
|
27
|
+
* radius={{ column: 'volume', range: [3, 14] }}
|
|
28
|
+
* color={{ column: 'change', range: ['#e8836b', '#15B3A6'] }}
|
|
29
|
+
* />
|
|
30
|
+
* </Layers>
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export function ScatterChart({ series, column, as: semantic, axis, radius, color, label, index = 0, }) {
|
|
34
|
+
const container = useContext(ContainerContext);
|
|
35
|
+
if (container === null) {
|
|
36
|
+
throw new Error('<ScatterChart> must be rendered inside a <ChartContainer>');
|
|
37
|
+
}
|
|
38
|
+
const layers = useContext(LayersContext);
|
|
39
|
+
if (layers === null) {
|
|
40
|
+
throw new Error('<ScatterChart> must be rendered inside a <Layers>');
|
|
41
|
+
}
|
|
42
|
+
const cs = useMemo(() => fromTimeSeries(series, column), [series, column]);
|
|
43
|
+
// Styling: semantic identifier → theme scatter style. The single styling
|
|
44
|
+
// channel for the base mark.
|
|
45
|
+
const { scatter } = container.theme;
|
|
46
|
+
const style = (semantic !== undefined ? scatter[semantic] : undefined) ?? scatter.default;
|
|
47
|
+
// Series identity for the readout + selection match (the `as` role, else the
|
|
48
|
+
// column name).
|
|
49
|
+
const seriesLabel = semantic ?? column;
|
|
50
|
+
const { font } = container.theme;
|
|
51
|
+
// Resolve the data-driven encoding once per data/encoding change. The reader
|
|
52
|
+
// pulls a named numeric column to a Float64Array (gaps NaN) — the same path
|
|
53
|
+
// fromTimeSeries uses; an unknown / non-numeric column throws there (eager,
|
|
54
|
+
// so a typo surfaces at render, not silently as base-styled points).
|
|
55
|
+
const encoding = useMemo(() => resolveEncoding(cs, style.radius, style.color, radius, color, (col) => fromTimeSeries(series, col).y), [cs, style.radius, style.color, radius, color, series]);
|
|
56
|
+
// Per-point label accessor: a column name reads that field, `true` reads the
|
|
57
|
+
// plotted column, anything else (false / omitted) ⇒ no labels.
|
|
58
|
+
const labelAt = useMemo(() => {
|
|
59
|
+
if (label === undefined || label === false)
|
|
60
|
+
return undefined;
|
|
61
|
+
const field = label === true ? column : label;
|
|
62
|
+
return (i) => {
|
|
63
|
+
// series.at(i) is O(1) per row (columnar eventAt cache), so a label per
|
|
64
|
+
// point stays cheap. The field is a runtime string → cast off the literal-
|
|
65
|
+
// keyed get (mirrors the value reads).
|
|
66
|
+
const e = series.at(i);
|
|
67
|
+
if (e === undefined)
|
|
68
|
+
return undefined;
|
|
69
|
+
const v = e.get(field);
|
|
70
|
+
return v === undefined || v === null ? undefined : String(v);
|
|
71
|
+
};
|
|
72
|
+
}, [label, column, series]);
|
|
73
|
+
// The point's stable key is its event begin (epoch ms) — the same as cs.x[i],
|
|
74
|
+
// which is the key column's begin buffer. Used for selection identity.
|
|
75
|
+
const keyAt = useMemo(() => (i) => cs.x[i], [cs]);
|
|
76
|
+
const entry = useMemo(() => ({
|
|
77
|
+
layer: {
|
|
78
|
+
yExtent: () => scatterExtent(cs),
|
|
79
|
+
xKind: 'time',
|
|
80
|
+
xExtent: () => cs.length === 0 ? null : [cs.x[0], cs.x[cs.length - 1]],
|
|
81
|
+
sampleAt: (time) => {
|
|
82
|
+
// No readout past the data (tracker policy — the dot snaps to a drawn
|
|
83
|
+
// mark, never extrapolates past the span); bounds from the time axis.
|
|
84
|
+
if (cs.length === 0 ||
|
|
85
|
+
time < cs.x[0] ||
|
|
86
|
+
time > cs.x[cs.length - 1]) {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
// Nearest *drawn* point by index (skips gaps) — O(log N). Reading by
|
|
90
|
+
// index gives the value, the snap-to x, and the encoded colour in one
|
|
91
|
+
// shot, so the readout swatch matches the mark the user sees.
|
|
92
|
+
const i = nearestIndex(cs, time);
|
|
93
|
+
if (i < 0)
|
|
94
|
+
return [];
|
|
95
|
+
return [
|
|
96
|
+
{
|
|
97
|
+
x: cs.x[i],
|
|
98
|
+
value: cs.y[i],
|
|
99
|
+
color: encoding.colorAt(i),
|
|
100
|
+
label: seriesLabel,
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
},
|
|
104
|
+
hitTest: (px, py, xScale, yScale) => hitTestScatter(cs, px, py, xScale, yScale, encoding, keyAt, seriesLabel),
|
|
105
|
+
draw: (ctx, xScale, yScale) => drawScatter(ctx, cs, xScale, yScale, style, encoding, keyAt, labelAt, font, container.selected, seriesLabel),
|
|
106
|
+
},
|
|
107
|
+
axisId: axis,
|
|
108
|
+
index,
|
|
109
|
+
}), [
|
|
110
|
+
cs,
|
|
111
|
+
series,
|
|
112
|
+
column,
|
|
113
|
+
style,
|
|
114
|
+
seriesLabel,
|
|
115
|
+
encoding,
|
|
116
|
+
keyAt,
|
|
117
|
+
labelAt,
|
|
118
|
+
font,
|
|
119
|
+
container.selected,
|
|
120
|
+
axis,
|
|
121
|
+
index,
|
|
122
|
+
]);
|
|
123
|
+
// A stable per-instance slot (see useSlotKey) keeps this layer's z-position
|
|
124
|
+
// fixed across data/style/selection updates (no jump to the front on update).
|
|
125
|
+
const slot = useSlotKey();
|
|
126
|
+
useEffect(() => () => layers.unregisterLayer(slot), [layers, slot]);
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
layers.registerLayer(slot, entry);
|
|
129
|
+
}, [layers, slot, entry]);
|
|
130
|
+
// Also a tracker source: the container fans in this series' nearest-point
|
|
131
|
+
// value at the cursor for the (outside-the-chart) readout.
|
|
132
|
+
const { registerTrackerSource, unregisterTrackerSource } = container;
|
|
133
|
+
useEffect(() => () => unregisterTrackerSource(slot), [unregisterTrackerSource, slot]);
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
registerTrackerSource(slot, entry.layer);
|
|
136
|
+
}, [registerTrackerSource, slot, entry.layer]);
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=ScatterChart.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type XAxisProps } from './XAxis.js';
|
|
2
|
+
/**
|
|
3
|
+
* The time-flavoured preset of {@link XAxis} — `<TimeAxis />` is `<XAxis />`.
|
|
4
|
+
* Kept as the familiar name for time charts (and what the container auto-renders);
|
|
5
|
+
* the axis kind still follows the data, so on a value container it ticks as a
|
|
6
|
+
* value axis. Forwards every {@link XAxisProps} (`format`, `label`, `side`, …).
|
|
7
|
+
*/
|
|
8
|
+
export declare function TimeAxis(props?: XAxisProps): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
//# sourceMappingURL=TimeAxis.d.ts.map
|
package/dist/TimeAxis.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { XAxis } from './XAxis.js';
|
|
3
|
+
/**
|
|
4
|
+
* The time-flavoured preset of {@link XAxis} — `<TimeAxis />` is `<XAxis />`.
|
|
5
|
+
* Kept as the familiar name for time charts (and what the container auto-renders);
|
|
6
|
+
* the axis kind still follows the data, so on a value container it ticks as a
|
|
7
|
+
* value axis. Forwards every {@link XAxisProps} (`format`, `label`, `side`, …).
|
|
8
|
+
*/
|
|
9
|
+
export function TimeAxis(props = {}) {
|
|
10
|
+
return _jsx(XAxis, { ...props });
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=TimeAxis.js.map
|
package/dist/XAxis.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type AxisFormat } from './format.js';
|
|
2
|
+
export interface XAxisProps {
|
|
3
|
+
/**
|
|
4
|
+
* Tick / cursor value formatting — a d3 format/time specifier string or a
|
|
5
|
+
* `(value) => string`. **Omitted ⇒ the container's shared formatter** (so the
|
|
6
|
+
* axis and the cursor readout agree), which is the d3 multi-scale time format
|
|
7
|
+
* for a time axis or the number default for a value axis. The specifier is
|
|
8
|
+
* resolved against the axis's kind (time vs value).
|
|
9
|
+
*/
|
|
10
|
+
format?: AxisFormat;
|
|
11
|
+
/** A label drawn centred below (or above) the ticks — e.g. `Distance (m)`. */
|
|
12
|
+
label?: string;
|
|
13
|
+
/** Which edge the axis sits on. **Default `'bottom'`.** Declaration order in
|
|
14
|
+
* the `<ChartContainer>` places it; `side` orients the ticks + label. */
|
|
15
|
+
side?: 'top' | 'bottom';
|
|
16
|
+
/** Strip height in px. Defaults to fit the ticks (+ the label line if any). */
|
|
17
|
+
height?: number;
|
|
18
|
+
/**
|
|
19
|
+
* Explicit ticks — `{ at, label }` in axis-value units — instead of the
|
|
20
|
+
* scale's automatic ticks. The value-axis lever for e.g. lap markers placed at
|
|
21
|
+
* their cumulative-distance positions (`{ at: lap.endMeters, label: 'Lap 3' }`).
|
|
22
|
+
*/
|
|
23
|
+
ticks?: ReadonlyArray<{
|
|
24
|
+
readonly at: number;
|
|
25
|
+
readonly label: string;
|
|
26
|
+
}>;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The shared **x axis**, a sibling of {@link YAxis} for the horizontal axis. A
|
|
30
|
+
* child of {@link ChartContainer}, rendered as DOM chrome (crisp text,
|
|
31
|
+
* themeable) under (or over) the rows, aligned to the plot. It reads the
|
|
32
|
+
* container's resolved `xScale` + `xKind` — so a **time** container ticks on
|
|
33
|
+
* wall-clock boundaries and a **value** container (a `ValueSeries` row) ticks as
|
|
34
|
+
* numbers, with no axis-type prop here; the kind follows the data.
|
|
35
|
+
*
|
|
36
|
+
* `<TimeAxis>` is the time-flavoured preset (`<XAxis />`).
|
|
37
|
+
*/
|
|
38
|
+
export declare function XAxis({ format, label, side, height, ticks: customTicks, }?: XAxisProps): import("react/jsx-runtime").JSX.Element;
|
|
39
|
+
//# sourceMappingURL=XAxis.d.ts.map
|
package/dist/XAxis.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment, useContext } from 'react';
|
|
3
|
+
import { ContainerContext } from './context.js';
|
|
4
|
+
import { resolveAxisFormat, resolveTimeFormat, } from './format.js';
|
|
5
|
+
/** Tick strip height (mark + value label) in CSS px. */
|
|
6
|
+
const TICK_STRIP = 22;
|
|
7
|
+
/** Extra height reserved for an axis `label` line. */
|
|
8
|
+
const LABEL_STRIP = 16;
|
|
9
|
+
const TICK_COUNT = 5;
|
|
10
|
+
/**
|
|
11
|
+
* The shared **x axis**, a sibling of {@link YAxis} for the horizontal axis. A
|
|
12
|
+
* child of {@link ChartContainer}, rendered as DOM chrome (crisp text,
|
|
13
|
+
* themeable) under (or over) the rows, aligned to the plot. It reads the
|
|
14
|
+
* container's resolved `xScale` + `xKind` — so a **time** container ticks on
|
|
15
|
+
* wall-clock boundaries and a **value** container (a `ValueSeries` row) ticks as
|
|
16
|
+
* numbers, with no axis-type prop here; the kind follows the data.
|
|
17
|
+
*
|
|
18
|
+
* `<TimeAxis>` is the time-flavoured preset (`<XAxis />`).
|
|
19
|
+
*/
|
|
20
|
+
export function XAxis({ format, label, side = 'bottom', height, ticks: customTicks, } = {}) {
|
|
21
|
+
const container = useContext(ContainerContext);
|
|
22
|
+
if (container === null) {
|
|
23
|
+
throw new Error('<XAxis> must be rendered inside a <ChartContainer>');
|
|
24
|
+
}
|
|
25
|
+
const { xScale, plotWidth, leftGutter, theme, formatTime, xKind } = container;
|
|
26
|
+
// Tick formatter: an explicit `format` is resolved against the axis kind
|
|
27
|
+
// (a time specifier through the time scale, a number specifier through the
|
|
28
|
+
// value scale); otherwise the container's shared formatter — the one the
|
|
29
|
+
// cursor readout uses, so a tick and the cursor read identically.
|
|
30
|
+
const fmt = format === undefined
|
|
31
|
+
? formatTime
|
|
32
|
+
: xKind === 'time'
|
|
33
|
+
? resolveTimeFormat(xScale, TICK_COUNT, format)
|
|
34
|
+
: resolveAxisFormat(xScale, TICK_COUNT, format);
|
|
35
|
+
const placed = customTicks
|
|
36
|
+
? customTicks.map((t) => ({ x: xScale(t.at), label: t.label }))
|
|
37
|
+
: xScale.ticks(TICK_COUNT).map((d) => ({
|
|
38
|
+
x: xScale(d),
|
|
39
|
+
label: fmt(+d),
|
|
40
|
+
}));
|
|
41
|
+
const stripHeight = height ?? TICK_STRIP + (label ? LABEL_STRIP : 0);
|
|
42
|
+
const onTop = side === 'top';
|
|
43
|
+
return (_jsxs("div", { style: {
|
|
44
|
+
position: 'relative',
|
|
45
|
+
marginLeft: `${leftGutter}px`,
|
|
46
|
+
width: `${plotWidth}px`,
|
|
47
|
+
height: `${stripHeight}px`,
|
|
48
|
+
// The plot-facing edge carries the rule; a top axis rules its bottom.
|
|
49
|
+
[onTop ? 'borderBottom' : 'borderTop']: `1px solid ${theme.axis.grid}`,
|
|
50
|
+
fontFamily: theme.font.family,
|
|
51
|
+
fontSize: `${theme.font.size}px`,
|
|
52
|
+
color: theme.axis.label,
|
|
53
|
+
}, children: [placed.map((t, i) => {
|
|
54
|
+
// End-align the edge labels so they stay within [0, plotWidth].
|
|
55
|
+
const labelTransform = i === 0
|
|
56
|
+
? 'none'
|
|
57
|
+
: i === placed.length - 1
|
|
58
|
+
? 'translateX(-100%)'
|
|
59
|
+
: 'translateX(-50%)';
|
|
60
|
+
return (_jsxs(Fragment, { children: [_jsx("div", { style: {
|
|
61
|
+
position: 'absolute',
|
|
62
|
+
left: `${t.x}px`,
|
|
63
|
+
[onTop ? 'bottom' : 'top']: 0,
|
|
64
|
+
width: '1px',
|
|
65
|
+
height: '4px',
|
|
66
|
+
background: theme.axis.grid,
|
|
67
|
+
} }), _jsx("div", { style: {
|
|
68
|
+
position: 'absolute',
|
|
69
|
+
left: `${t.x}px`,
|
|
70
|
+
[onTop ? 'bottom' : 'top']: '6px',
|
|
71
|
+
transform: labelTransform,
|
|
72
|
+
whiteSpace: 'nowrap',
|
|
73
|
+
}, children: t.label })] }, `${t.x}-${i}`));
|
|
74
|
+
}), label !== undefined && (_jsx("div", { style: {
|
|
75
|
+
position: 'absolute',
|
|
76
|
+
left: 0,
|
|
77
|
+
width: '100%',
|
|
78
|
+
textAlign: 'center',
|
|
79
|
+
[onTop ? 'top' : 'bottom']: 0,
|
|
80
|
+
opacity: 0.7,
|
|
81
|
+
whiteSpace: 'nowrap',
|
|
82
|
+
}, children: label }))] }));
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=XAxis.js.map
|
package/dist/YAxis.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type AxisFormat } from './format.js';
|
|
2
|
+
export interface YAxisProps {
|
|
3
|
+
/** Identifier a chart links to via its `axis` prop (and the first declared is
|
|
4
|
+
* the row's default). */
|
|
5
|
+
id: string;
|
|
6
|
+
/**
|
|
7
|
+
* Which side of the plot the gutter sits on. Author left axes *before*
|
|
8
|
+
* `<Layers>` in JSX and right axes *after* — the row lays children out in
|
|
9
|
+
* order. Default `left`.
|
|
10
|
+
*/
|
|
11
|
+
side?: 'left' | 'right';
|
|
12
|
+
/** Display label / unit (e.g. `bpm`); defaults to `id`. */
|
|
13
|
+
label?: string;
|
|
14
|
+
/** Explicit domain bounds; omit to auto-fit the charts linked to this axis. */
|
|
15
|
+
min?: number;
|
|
16
|
+
max?: number;
|
|
17
|
+
/**
|
|
18
|
+
* Value formatting for the tick labels (and the cursor readout, which matches):
|
|
19
|
+
* a d3 format specifier string (e.g. `'.0%'`, `',.2f'`) or a `(value) => string`
|
|
20
|
+
* function. Omit for the scale's d3 default — which is calibrated to the tick
|
|
21
|
+
* step, so a between-ticks readout rounds to tick precision; pass a specifier
|
|
22
|
+
* (e.g. `',.2f'`) when you want finer readout precision. See {@link AxisFormat}.
|
|
23
|
+
*/
|
|
24
|
+
format?: AxisFormat;
|
|
25
|
+
/** Gutter width in CSS pixels (default 50). */
|
|
26
|
+
width?: number;
|
|
27
|
+
/**
|
|
28
|
+
* @internal Declaration position among the row's children, injected by
|
|
29
|
+
* `ChartRow` so the first-declared axis stays the default. Do not set.
|
|
30
|
+
*/
|
|
31
|
+
index?: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A y-axis for a {@link ChartRow}, rendered as DOM chrome (not canvas) so the
|
|
35
|
+
* text is crisp, themeable, and accessible. Registers its id / side / width /
|
|
36
|
+
* domain with the row, which reserves the gutter (shrinking `plotWidth`) and
|
|
37
|
+
* computes this axis's scale from the charts linked to it; the gutter then draws
|
|
38
|
+
* tick marks + labels from that scale. Charts attach via `<LineChart axis="id">`
|
|
39
|
+
* (default: the first axis).
|
|
40
|
+
*/
|
|
41
|
+
export declare function YAxis({ id, side, label, min, max, format, width, index, }: YAxisProps): import("react/jsx-runtime").JSX.Element;
|
|
42
|
+
//# sourceMappingURL=YAxis.d.ts.map
|
package/dist/YAxis.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useContext, useEffect, useMemo } from 'react';
|
|
3
|
+
import { ContainerContext, RowContext } from './context.js';
|
|
4
|
+
import { resolveAxisFormat } from './format.js';
|
|
5
|
+
import { useSlotKey } from './use-slot-key.js';
|
|
6
|
+
const DEFAULT_WIDTH = 50;
|
|
7
|
+
const TICK_COUNT = 5;
|
|
8
|
+
/**
|
|
9
|
+
* A y-axis for a {@link ChartRow}, rendered as DOM chrome (not canvas) so the
|
|
10
|
+
* text is crisp, themeable, and accessible. Registers its id / side / width /
|
|
11
|
+
* domain with the row, which reserves the gutter (shrinking `plotWidth`) and
|
|
12
|
+
* computes this axis's scale from the charts linked to it; the gutter then draws
|
|
13
|
+
* tick marks + labels from that scale. Charts attach via `<LineChart axis="id">`
|
|
14
|
+
* (default: the first axis).
|
|
15
|
+
*/
|
|
16
|
+
export function YAxis({ id, side = 'left', label, min, max, format, width = DEFAULT_WIDTH, index = 0, }) {
|
|
17
|
+
const container = useContext(ContainerContext);
|
|
18
|
+
if (container === null) {
|
|
19
|
+
throw new Error('<YAxis> must be rendered inside a <ChartContainer>');
|
|
20
|
+
}
|
|
21
|
+
const row = useContext(RowContext);
|
|
22
|
+
if (row === null) {
|
|
23
|
+
throw new Error('<YAxis> must be rendered inside a <ChartRow>');
|
|
24
|
+
}
|
|
25
|
+
const spec = useMemo(() => ({ id, side, width, min, max, format, index }), [id, side, width, min, max, format, index]);
|
|
26
|
+
// A stable per-instance slot (see useSlotKey) keeps this axis in a fixed
|
|
27
|
+
// registry position, so a min/max/side change updates in place rather than
|
|
28
|
+
// re-appending (which would move the first axis behind a later one and
|
|
29
|
+
// silently rebind the row's default-axis charts).
|
|
30
|
+
const slot = useSlotKey();
|
|
31
|
+
const { registerAxis, unregisterAxis } = row;
|
|
32
|
+
// Unregister on unmount only (deps are stable, so cleanup never runs early).
|
|
33
|
+
useEffect(() => () => unregisterAxis(slot), [unregisterAxis, slot]);
|
|
34
|
+
// Register on mount + update in place on every spec change — no reorder.
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
registerAxis(slot, spec);
|
|
37
|
+
}, [registerAxis, slot, spec]);
|
|
38
|
+
const { theme } = container;
|
|
39
|
+
const yScale = row.yScales.get(id);
|
|
40
|
+
const ticks = yScale ? yScale.ticks(TICK_COUNT) : [];
|
|
41
|
+
// Same formatter the readout uses (resolved per axis on the row), so a tick and
|
|
42
|
+
// a cursor value read identically.
|
|
43
|
+
const fmt = yScale ? resolveAxisFormat(yScale, TICK_COUNT, format) : String;
|
|
44
|
+
// The row reserves a slot per axis column (the widest in that column across
|
|
45
|
+
// rows). Size the box to the slot and align this axis's own (narrower)
|
|
46
|
+
// content toward the plot — left axes flush right, right axes flush left — so
|
|
47
|
+
// axes line up column-by-column. Keyed by this instance's slot key (not `id`,
|
|
48
|
+
// which may repeat across a mirror). Falls back to own width until reserved.
|
|
49
|
+
const slotWidth = row.axisSlots.get(slot) ?? width;
|
|
50
|
+
return (_jsx("div", { style: {
|
|
51
|
+
flex: `0 0 ${slotWidth}px`,
|
|
52
|
+
display: 'flex',
|
|
53
|
+
justifyContent: side === 'left' ? 'flex-end' : 'flex-start',
|
|
54
|
+
height: `${row.height}px`,
|
|
55
|
+
}, children: _jsxs("div", { style: {
|
|
56
|
+
position: 'relative',
|
|
57
|
+
width: `${width}px`,
|
|
58
|
+
height: `${row.height}px`,
|
|
59
|
+
fontFamily: theme.font.family,
|
|
60
|
+
fontSize: `${theme.font.size}px`,
|
|
61
|
+
color: theme.axis.label,
|
|
62
|
+
}, children: [yScale &&
|
|
63
|
+
ticks.map((t) => (_jsx("div", { style: {
|
|
64
|
+
position: 'absolute',
|
|
65
|
+
top: `${yScale(t)}px`,
|
|
66
|
+
[side === 'left' ? 'right' : 'left']: '4px',
|
|
67
|
+
transform: 'translateY(-50%)',
|
|
68
|
+
whiteSpace: 'nowrap',
|
|
69
|
+
}, children: fmt(t) }, t))), _jsx("div", { style: {
|
|
70
|
+
position: 'absolute',
|
|
71
|
+
[side === 'left' ? 'left' : 'right']: '1px',
|
|
72
|
+
top: 0,
|
|
73
|
+
bottom: 0,
|
|
74
|
+
width: `${theme.font.size + 2}px`,
|
|
75
|
+
display: 'flex',
|
|
76
|
+
alignItems: 'center',
|
|
77
|
+
justifyContent: 'center',
|
|
78
|
+
fontSize: `${theme.font.size - 1}px`,
|
|
79
|
+
opacity: 0.7,
|
|
80
|
+
pointerEvents: 'none',
|
|
81
|
+
}, children: _jsx("span", { style: {
|
|
82
|
+
whiteSpace: 'nowrap',
|
|
83
|
+
transform: `rotate(${side === 'left' ? -90 : 90}deg)`,
|
|
84
|
+
}, children: label ?? id }) })] }) }));
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=YAxis.js.map
|
|
@@ -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
|