@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/CHANGELOG.md
CHANGED
|
@@ -8,7 +8,9 @@ The `@pond-ts` packages — `pond-ts`, `@pond-ts/react`, `@pond-ts/charts`, and
|
|
|
8
8
|
them all. Pre-1.0: minor bumps may include new features and type-level changes;
|
|
9
9
|
patch bumps are strictly additive.
|
|
10
10
|
|
|
11
|
-
[Unreleased]: https://github.com/pjm17971/pond-ts/compare/v0.
|
|
11
|
+
[Unreleased]: https://github.com/pjm17971/pond-ts/compare/v0.33.0...HEAD
|
|
12
|
+
[0.33.0]: https://github.com/pjm17971/pond-ts/compare/v0.32.0...v0.33.0
|
|
13
|
+
[0.32.0]: https://github.com/pjm17971/pond-ts/compare/v0.31.2...v0.32.0
|
|
12
14
|
[0.31.2]: https://github.com/pjm17971/pond-ts/compare/v0.31.1...v0.31.2
|
|
13
15
|
[0.31.1]: https://github.com/pjm17971/pond-ts/compare/v0.30.0...v0.31.1
|
|
14
16
|
[0.31.0]: https://github.com/pjm17971/pond-ts/compare/v0.30.0...3c4e8bd
|
|
@@ -26,6 +28,78 @@ patch bumps are strictly additive.
|
|
|
26
28
|
[0.19.0]: https://github.com/pjm17971/pond-ts/compare/v0.18.0...v0.19.0
|
|
27
29
|
[0.18.0]: https://github.com/pjm17971/pond-ts/compare/v0.17.1...v0.18.0
|
|
28
30
|
|
|
31
|
+
## [0.33.0] — 2026-06-30
|
|
32
|
+
|
|
33
|
+
A `@pond-ts/charts` release: a label opt-out for annotations plus interaction
|
|
34
|
+
fixes from an adversarial review of the #306 annotation system. `pond-ts`,
|
|
35
|
+
`@pond-ts/react`, and `@pond-ts/fit` carry no code changes — republished in
|
|
36
|
+
lock-step (their `pond-ts` / `@pond-ts/react` peer ranges widen to `^0.33.0`).
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- **Charts — annotation label opt-out.** `<Region>`, `<Marker>`, and
|
|
41
|
+
`<Baseline>` accept `label={false}` (or `label=""`) to render **no label
|
|
42
|
+
chip** — for an inert background mark (e.g. a `selectable={false}` highlight
|
|
43
|
+
band) where the auto-label would only show a raw axis value. Omitting `label`
|
|
44
|
+
still auto-labels; a string still renders it. `label` widens to
|
|
45
|
+
`string | false`. New `Highlight` story + Linux e2e baselines for the
|
|
46
|
+
`Annotations` stories. (#308)
|
|
47
|
+
|
|
48
|
+
### Changed
|
|
49
|
+
|
|
50
|
+
- **Charts — `<Region>` / `<Marker>` / `<Baseline>` with `label=""` now render
|
|
51
|
+
no chip** (previously an empty, zero-width chip). The label-less path; pass a
|
|
52
|
+
non-empty string for a visible label. (#308)
|
|
53
|
+
|
|
54
|
+
### Fixed
|
|
55
|
+
|
|
56
|
+
- **Charts — annotation edge-resize no longer inverts.** Dragging a `<Region>`
|
|
57
|
+
edge past the opposite one previously reported `{ from > to }`; it now pivots
|
|
58
|
+
around the fixed opposite edge, so the reported span stays ordered and a drag
|
|
59
|
+
either way re-opens the region instead of dead-ending at zero width. (#308)
|
|
60
|
+
- **Charts — annotation drag releases on `pointercancel`.** A system gesture
|
|
61
|
+
takeover (which fires `pointercancel`, not `pointerup`) no longer leaves a mark
|
|
62
|
+
stuck mid-drag. (#308)
|
|
63
|
+
- **Charts — single-annotation edit exits on an empty-plot click** even when the
|
|
64
|
+
mark is `editing` but not `selected`. (#308)
|
|
65
|
+
|
|
66
|
+
## [0.32.0] — 2026-06-29
|
|
67
|
+
|
|
68
|
+
A `@pond-ts/charts` release: value-axis support across the fill/bar layers, plus
|
|
69
|
+
explicit y-axis ticks and the annotation primitives. `pond-ts`,
|
|
70
|
+
`@pond-ts/react`, and `@pond-ts/fit` are unchanged this cycle — republished in
|
|
71
|
+
lock-step at the same version.
|
|
72
|
+
|
|
73
|
+
### Added
|
|
74
|
+
|
|
75
|
+
- **Charts — explicit y-axis ticks.** `<YAxis ticks={[{ at, label }]}>` places
|
|
76
|
+
ticks (and their gridlines) at chosen values with custom labels, mirroring
|
|
77
|
+
`<XAxis ticks>`; `ticks={[]}` draws none. (#303)
|
|
78
|
+
- **Charts — value-axis (`ValueSeries`) support for `<AreaChart>`,
|
|
79
|
+
`<BandChart>`, and `<BarChart>`.** Each now accepts a `ValueSeries`
|
|
80
|
+
(`series.byValue('dist')`) and plots against its monotonic value axis
|
|
81
|
+
(distance, cumulative work, …), not just time — joining `<LineChart>`, which
|
|
82
|
+
already did. The container infers the x-axis kind from the data, so there is
|
|
83
|
+
no axis-type prop. `BarChart` derives each bar's span from neighbour spacing
|
|
84
|
+
on a point-keyed value axis (the splits/laps case). (#304, #307)
|
|
85
|
+
- **Charts — annotations: `<Region>`, `<Marker>`, `<Baseline>`.** User-authored
|
|
86
|
+
marks in a distinct register — a shaded x-span, a vertical x line, and a
|
|
87
|
+
horizontal value line — with flag labels, a three-level depth ramp, and opt-in
|
|
88
|
+
interaction modes (inspect-select, single-edit, drag-resize, create-tool
|
|
89
|
+
gestures) coordinated by the container (cross-row guide lines,
|
|
90
|
+
snap-to-guideline, z-order). Adds `ChartContainer` annotation props
|
|
91
|
+
(`creating` / `editAnnotations` / `onCreate` / `onSelectAnnotation` /
|
|
92
|
+
`onHoverAnnotation` / `onEditAnnotation` / `snap`) and a `ChartTheme.annotation`
|
|
93
|
+
depth theme. (#306)
|
|
94
|
+
|
|
95
|
+
### Changed
|
|
96
|
+
|
|
97
|
+
- **Charts — the fill/bar layer `series` prop widens (additive).**
|
|
98
|
+
`AreaChartProps` / `BandChartProps` / `BarChartProps` now accept
|
|
99
|
+
`TimeSeries | ValueSeries`; the new second generic defaults, so existing
|
|
100
|
+
one-type-argument uses (`AreaChartProps<S>`, `<AreaChart<S>>`) compile
|
|
101
|
+
unchanged. (#304, #307)
|
|
102
|
+
|
|
29
103
|
## [0.31.2] — 2026-06-29
|
|
30
104
|
|
|
31
105
|
### Fixed
|
package/dist/AreaChart.d.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { ValueSeries } from 'pond-ts';
|
|
2
|
+
import type { SeriesSchema, TimeSeries, ValueSeriesSchema } from 'pond-ts';
|
|
2
3
|
import { type Curve } from './curve.js';
|
|
3
4
|
import { type GapMode } from './gaps.js';
|
|
4
|
-
export interface AreaChartProps<S extends SeriesSchema> {
|
|
5
|
-
/**
|
|
6
|
-
|
|
5
|
+
export interface AreaChartProps<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> {
|
|
6
|
+
/**
|
|
7
|
+
* The source series. A `TimeSeries` fills against the time axis; a
|
|
8
|
+
* `ValueSeries` (`series.byValue('dist')`) against its value axis — the
|
|
9
|
+
* container infers which from the data, no axis-type prop (mirrors
|
|
10
|
+
* `<LineChart>`). Either way `column` names the numeric value to fill from.
|
|
11
|
+
*/
|
|
12
|
+
series: TimeSeries<S> | ValueSeries<VS>;
|
|
7
13
|
/** Name of the numeric value column to fill from. */
|
|
8
14
|
column: string;
|
|
9
15
|
/**
|
|
@@ -81,5 +87,5 @@ export interface AreaChartProps<S extends SeriesSchema> {
|
|
|
81
87
|
* </Layers>
|
|
82
88
|
* ```
|
|
83
89
|
*/
|
|
84
|
-
export declare function AreaChart<S extends SeriesSchema>({ series, column, as: semantic, axis, baseline, curve, gaps, index, }: AreaChartProps<S>): null;
|
|
90
|
+
export declare function AreaChart<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema>({ series, column, as: semantic, axis, baseline, curve, gaps, index, }: AreaChartProps<S, VS>): null;
|
|
85
91
|
//# sourceMappingURL=AreaChart.d.ts.map
|
package/dist/AreaChart.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useContext, useEffect, useMemo } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { ValueSeries } from 'pond-ts';
|
|
3
|
+
import { fromTimeSeries, fromValueSeries } from './data.js';
|
|
3
4
|
import { areaExtent, drawArea } from './area.js';
|
|
4
5
|
import { resolveCurve } from './curve.js';
|
|
5
6
|
import { DEFAULT_GAP_MODE, DEFAULT_GAP_CONNECTOR_OPACITY, } from './gaps.js';
|
|
@@ -43,7 +44,9 @@ export function AreaChart({ series, column, as: semantic, axis, baseline, curve,
|
|
|
43
44
|
if (layers === null) {
|
|
44
45
|
throw new Error('<AreaChart> must be rendered inside a <Layers>');
|
|
45
46
|
}
|
|
46
|
-
const cs = useMemo(() =>
|
|
47
|
+
const cs = useMemo(() => series instanceof ValueSeries
|
|
48
|
+
? fromValueSeries(series, column)
|
|
49
|
+
: fromTimeSeries(series, column), [series, column]);
|
|
47
50
|
// Styling: semantic identifier → theme area style. The single styling channel.
|
|
48
51
|
const { area } = container.theme;
|
|
49
52
|
const style = (semantic !== undefined ? area[semantic] : undefined) ?? area.default;
|
|
@@ -56,17 +59,27 @@ export function AreaChart({ series, column, as: semantic, axis, baseline, curve,
|
|
|
56
59
|
const entry = useMemo(() => ({
|
|
57
60
|
layer: {
|
|
58
61
|
yExtent: () => areaExtent(cs, baseline),
|
|
59
|
-
|
|
62
|
+
// The container infers the shared x scale's kind from its layers — a
|
|
63
|
+
// ValueSeries plots on a value axis, a TimeSeries on time.
|
|
64
|
+
xKind: series instanceof ValueSeries ? 'value' : 'time',
|
|
60
65
|
xExtent: () => cs.length === 0 ? null : [cs.x[0], cs.x[cs.length - 1]],
|
|
61
|
-
sampleAt: (
|
|
62
|
-
// No readout past the data (tracker policy —
|
|
63
|
-
//
|
|
64
|
-
if (cs.length === 0 ||
|
|
65
|
-
time < cs.x[0] ||
|
|
66
|
-
time > cs.x[cs.length - 1]) {
|
|
66
|
+
sampleAt: (x) => {
|
|
67
|
+
// No readout past the data (tracker policy — nearest clamps to an
|
|
68
|
+
// endpoint outside the span); bounds from the columnar x axis.
|
|
69
|
+
if (cs.length === 0 || x < cs.x[0] || x > cs.x[cs.length - 1]) {
|
|
67
70
|
return [];
|
|
68
71
|
}
|
|
69
|
-
|
|
72
|
+
if (series instanceof ValueSeries) {
|
|
73
|
+
// Value axis: bisect the axis for the nearest row, read y from `cs`.
|
|
74
|
+
const i = series.nearestIndex(x);
|
|
75
|
+
if (i < 0)
|
|
76
|
+
return [];
|
|
77
|
+
const v = cs.y[i];
|
|
78
|
+
return Number.isFinite(v)
|
|
79
|
+
? [{ x: cs.x[i], value: v, color: style.color, label }]
|
|
80
|
+
: [];
|
|
81
|
+
}
|
|
82
|
+
const e = series.nearest(x);
|
|
70
83
|
if (e === undefined)
|
|
71
84
|
return [];
|
|
72
85
|
// get() wants a literal key; column is a runtime string. Cast the
|
package/dist/BandChart.d.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { ValueSeries } from 'pond-ts';
|
|
2
|
+
import type { SeriesSchema, TimeSeries, ValueSeriesSchema } from 'pond-ts';
|
|
2
3
|
import { type Curve } from './curve.js';
|
|
3
|
-
export interface BandChartProps<S extends SeriesSchema> {
|
|
4
|
-
/**
|
|
5
|
-
|
|
4
|
+
export interface BandChartProps<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> {
|
|
5
|
+
/**
|
|
6
|
+
* The source series. A `TimeSeries` fills the envelope against the time axis;
|
|
7
|
+
* a `ValueSeries` (`series.byValue('dist')`) against its value axis — the
|
|
8
|
+
* container infers which from the data, no axis-type prop (mirrors
|
|
9
|
+
* `<LineChart>` / `<AreaChart>`). Either way `lower`/`upper` name the numeric
|
|
10
|
+
* edge columns.
|
|
11
|
+
*/
|
|
12
|
+
series: TimeSeries<S> | ValueSeries<VS>;
|
|
6
13
|
/** Name of the numeric column for the band's lower edge (e.g. `p25`). */
|
|
7
14
|
lower: string;
|
|
8
15
|
/** Name of the numeric column for the band's upper edge (e.g. `p75`). */
|
|
@@ -51,5 +58,5 @@ export interface BandChartProps<S extends SeriesSchema> {
|
|
|
51
58
|
* </Layers>
|
|
52
59
|
* ```
|
|
53
60
|
*/
|
|
54
|
-
export declare function BandChart<S extends SeriesSchema>({ series, lower, upper, as: semantic, axis, curve, index, }: BandChartProps<S>): null;
|
|
61
|
+
export declare function BandChart<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema>({ series, lower, upper, as: semantic, axis, curve, index, }: BandChartProps<S, VS>): null;
|
|
55
62
|
//# sourceMappingURL=BandChart.d.ts.map
|
package/dist/BandChart.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useContext, useEffect, useMemo } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { ValueSeries } from 'pond-ts';
|
|
3
|
+
import { bandFromTimeSeries, bandFromValueSeries } from './data.js';
|
|
3
4
|
import { bandExtent, drawBand } from './band.js';
|
|
4
5
|
import { resolveCurve } from './curve.js';
|
|
5
6
|
import { ContainerContext, LayersContext } from './context.js';
|
|
@@ -30,7 +31,9 @@ export function BandChart({ series, lower, upper, as: semantic, axis, curve, ind
|
|
|
30
31
|
if (layers === null) {
|
|
31
32
|
throw new Error('<BandChart> must be rendered inside a <Layers>');
|
|
32
33
|
}
|
|
33
|
-
const bs = useMemo(() =>
|
|
34
|
+
const bs = useMemo(() => series instanceof ValueSeries
|
|
35
|
+
? bandFromValueSeries(series, lower, upper)
|
|
36
|
+
: bandFromTimeSeries(series, lower, upper), [series, lower, upper]);
|
|
34
37
|
// Styling: semantic identifier → theme band style. The single styling channel.
|
|
35
38
|
const { band } = container.theme;
|
|
36
39
|
const style = (semantic !== undefined ? band[semantic] : undefined) ?? band.default;
|
|
@@ -38,17 +41,32 @@ export function BandChart({ series, lower, upper, as: semantic, axis, curve, ind
|
|
|
38
41
|
const entry = useMemo(() => ({
|
|
39
42
|
layer: {
|
|
40
43
|
yExtent: () => bandExtent(bs),
|
|
41
|
-
|
|
44
|
+
// The container infers the shared x scale's kind from its layers — a
|
|
45
|
+
// ValueSeries plots on a value axis, a TimeSeries on time.
|
|
46
|
+
xKind: series instanceof ValueSeries ? 'value' : 'time',
|
|
42
47
|
xExtent: () => bs.length === 0 ? null : [bs.x[0], bs.x[bs.length - 1]],
|
|
43
|
-
sampleAt: (
|
|
44
|
-
// No readout past the data (tracker policy — nearest
|
|
45
|
-
// from the columnar
|
|
46
|
-
if (bs.length === 0 ||
|
|
47
|
-
time < bs.x[0] ||
|
|
48
|
-
time > bs.x[bs.length - 1]) {
|
|
48
|
+
sampleAt: (x) => {
|
|
49
|
+
// No readout past the data (tracker policy — nearest clamps); bounds
|
|
50
|
+
// from the columnar x axis.
|
|
51
|
+
if (bs.length === 0 || x < bs.x[0] || x > bs.x[bs.length - 1]) {
|
|
49
52
|
return [];
|
|
50
53
|
}
|
|
51
|
-
|
|
54
|
+
if (series instanceof ValueSeries) {
|
|
55
|
+
// Value axis: bisect the axis for the nearest row, read both edges
|
|
56
|
+
// from `bs`. A gap on either edge yields no readout (like the fill).
|
|
57
|
+
const i = series.nearestIndex(x);
|
|
58
|
+
if (i < 0)
|
|
59
|
+
return [];
|
|
60
|
+
const lo = bs.lower[i];
|
|
61
|
+
const hi = bs.upper[i];
|
|
62
|
+
if (!Number.isFinite(lo) || !Number.isFinite(hi))
|
|
63
|
+
return [];
|
|
64
|
+
return [
|
|
65
|
+
{ x: bs.x[i], value: lo, color: style.fill, label: lower },
|
|
66
|
+
{ x: bs.x[i], value: hi, color: style.fill, label: upper },
|
|
67
|
+
];
|
|
68
|
+
}
|
|
69
|
+
const e = series.nearest(x);
|
|
52
70
|
if (e === undefined)
|
|
53
71
|
return [];
|
|
54
72
|
// get() wants a literal key; lower/upper are runtime strings. Cast the
|
package/dist/BarChart.d.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import { ValueSeries } from 'pond-ts';
|
|
2
|
+
import type { SeriesSchema, TimeSeries, ValueSeriesSchema } from 'pond-ts';
|
|
3
|
+
export interface BarChartProps<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> {
|
|
3
4
|
/**
|
|
4
|
-
* The source series. **Interval / timeRange-keyed** is the primary
|
|
5
|
-
* event's key `[begin, end]` is a bar's x-span. A **point-keyed**
|
|
6
|
-
* series is supported too: each bar's width is derived from neighbour
|
|
7
|
-
* (see {@link barsFromTimeSeries}).
|
|
5
|
+
* The source series. **Interval / timeRange-keyed** `TimeSeries` is the primary
|
|
6
|
+
* form — each event's key `[begin, end]` is a bar's x-span. A **point-keyed**
|
|
7
|
+
* (`time`) series is supported too: each bar's width is derived from neighbour
|
|
8
|
+
* spacing (see {@link barsFromTimeSeries}). A **`ValueSeries`**
|
|
9
|
+
* (`series.byValue('dist')`) bars against its value axis — also point-keyed, so
|
|
10
|
+
* the same neighbour-spacing span applies (see {@link barsFromValueSeries}); the
|
|
11
|
+
* container infers the x-kind from the data, no axis-type prop (mirrors the
|
|
12
|
+
* other layers).
|
|
8
13
|
*/
|
|
9
|
-
series: TimeSeries<S>;
|
|
14
|
+
series: TimeSeries<S> | ValueSeries<VS>;
|
|
10
15
|
/** Name of the numeric value column for the bar height. */
|
|
11
16
|
column: string;
|
|
12
17
|
/**
|
|
@@ -58,9 +63,10 @@ export interface BarChartProps<S extends SeriesSchema> {
|
|
|
58
63
|
* readout reads the same bar you click, even across a wide bucket (they differ
|
|
59
64
|
* only by the `gap` inset, where the pixel rect is narrower than the span).
|
|
60
65
|
*
|
|
61
|
-
* **
|
|
62
|
-
*
|
|
63
|
-
*
|
|
66
|
+
* **Value axis** — bars also scale on a value axis when fed a `ValueSeries`
|
|
67
|
+
* (`series.byValue('dist')`): estela's distance-domain splits/laps, one bar per
|
|
68
|
+
* segment over a monotonic axis. A `ValueSeries` is point-keyed, so the span is
|
|
69
|
+
* neighbour-derived like a point `TimeSeries` (see {@link barsFromValueSeries}).
|
|
64
70
|
*
|
|
65
71
|
* ```tsx
|
|
66
72
|
* <Layers>
|
|
@@ -68,5 +74,5 @@ export interface BarChartProps<S extends SeriesSchema> {
|
|
|
68
74
|
* </Layers>
|
|
69
75
|
* ```
|
|
70
76
|
*/
|
|
71
|
-
export declare function BarChart<S extends SeriesSchema>({ series, column, as: semantic, axis, gap, index, }: BarChartProps<S>): null;
|
|
77
|
+
export declare function BarChart<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema>({ series, column, as: semantic, axis, gap, index, }: BarChartProps<S, VS>): null;
|
|
72
78
|
//# sourceMappingURL=BarChart.d.ts.map
|
package/dist/BarChart.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useContext, useEffect, useMemo } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { ValueSeries } from 'pond-ts';
|
|
3
|
+
import { barsFromTimeSeries, barsFromValueSeries } from './data.js';
|
|
3
4
|
import { barAt, barExtent, barIndexAtTime, drawBars, resolveBarBaseline, } from './bars.js';
|
|
4
5
|
import { ContainerContext, LayersContext, } from './context.js';
|
|
5
6
|
import { useSlotKey } from './use-slot-key.js';
|
|
@@ -25,9 +26,10 @@ import { useSlotKey } from './use-slot-key.js';
|
|
|
25
26
|
* readout reads the same bar you click, even across a wide bucket (they differ
|
|
26
27
|
* only by the `gap` inset, where the pixel rect is narrower than the span).
|
|
27
28
|
*
|
|
28
|
-
* **
|
|
29
|
-
*
|
|
30
|
-
*
|
|
29
|
+
* **Value axis** — bars also scale on a value axis when fed a `ValueSeries`
|
|
30
|
+
* (`series.byValue('dist')`): estela's distance-domain splits/laps, one bar per
|
|
31
|
+
* segment over a monotonic axis. A `ValueSeries` is point-keyed, so the span is
|
|
32
|
+
* neighbour-derived like a point `TimeSeries` (see {@link barsFromValueSeries}).
|
|
31
33
|
*
|
|
32
34
|
* ```tsx
|
|
33
35
|
* <Layers>
|
|
@@ -44,7 +46,9 @@ export function BarChart({ series, column, as: semantic, axis, gap, index = 0, }
|
|
|
44
46
|
if (layers === null) {
|
|
45
47
|
throw new Error('<BarChart> must be rendered inside a <Layers>');
|
|
46
48
|
}
|
|
47
|
-
const bs = useMemo(() =>
|
|
49
|
+
const bs = useMemo(() => series instanceof ValueSeries
|
|
50
|
+
? barsFromValueSeries(series, column)
|
|
51
|
+
: barsFromTimeSeries(series, column), [series, column]);
|
|
48
52
|
// Styling: semantic identifier → theme bar style. The single styling channel.
|
|
49
53
|
const { bar } = container.theme;
|
|
50
54
|
const style = (semantic !== undefined ? bar[semantic] : undefined) ?? bar.default;
|
|
@@ -69,7 +73,9 @@ export function BarChart({ series, column, as: semantic, axis, gap, index = 0, }
|
|
|
69
73
|
const entry = useMemo(() => ({
|
|
70
74
|
layer: {
|
|
71
75
|
yExtent: () => barExtent(bs),
|
|
72
|
-
|
|
76
|
+
// The container infers the shared x scale's kind from its layers — a
|
|
77
|
+
// ValueSeries bars on a value axis, a TimeSeries on time.
|
|
78
|
+
xKind: series instanceof ValueSeries ? 'value' : 'time',
|
|
73
79
|
xExtent: () => bs.length === 0 ? null : [bs.begin[0], bs.end[bs.length - 1]],
|
|
74
80
|
sampleAt: (time) => {
|
|
75
81
|
// The flag belongs to the bar **under the cursor** — the bar whose
|
package/dist/ChartContainer.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ReactNode } from 'react';
|
|
2
2
|
import type { TimeRange } from 'pond-ts';
|
|
3
|
-
import { type CursorMode, type SelectInfo, type TrackerInfo } from './context.js';
|
|
3
|
+
import { type AnnotationKind, type CreateSpec, type CursorMode, type SelectInfo, type TrackerInfo } from './context.js';
|
|
4
4
|
import { type AxisFormat } from './format.js';
|
|
5
5
|
import { type ChartTheme } from './theme.js';
|
|
6
6
|
export interface ChartContainerProps {
|
|
@@ -82,6 +82,49 @@ export interface ChartContainerProps {
|
|
|
82
82
|
* axis.
|
|
83
83
|
*/
|
|
84
84
|
cursorTime?: boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Enter **annotation-edit mode**: suppresses the data cursor and makes editable
|
|
87
|
+
* annotations (those given an `onChange`) interactive — hovering one reveals its
|
|
88
|
+
* handles + highlights it, and dragging edits it. **Default `false`.** Pairs
|
|
89
|
+
* with each annotation's `onChange` (where the edit goes); this is the mode that
|
|
90
|
+
* turns the affordances on and gets the cursor out of the way.
|
|
91
|
+
*/
|
|
92
|
+
editAnnotations?: boolean;
|
|
93
|
+
/**
|
|
94
|
+
* The armed annotation **creation tool** (the consumer's toolbar sets it), or
|
|
95
|
+
* `null`/omitted for idle. When set, the plot captures a create gesture — a
|
|
96
|
+
* preview tracks the pointer, and on release {@link onCreate} fires. The consumer
|
|
97
|
+
* then adds the mark, disarms (back to `null`), and selects it (spring-loaded);
|
|
98
|
+
* keep it set to place several. Requires {@link editAnnotations}.
|
|
99
|
+
*/
|
|
100
|
+
creating?: AnnotationKind | null;
|
|
101
|
+
/** Fired when a create gesture completes (on release). See {@link CreateSpec}. */
|
|
102
|
+
onCreate?: (spec: CreateSpec) => void;
|
|
103
|
+
/**
|
|
104
|
+
* Fired when an annotation is clicked (its `id`), the plot is clicked empty
|
|
105
|
+
* (`null`), or a region is double-clicked (the shortcut into edit). The consumer
|
|
106
|
+
* holds the selected id and sets each mark's `selected={id === sel}`.
|
|
107
|
+
*/
|
|
108
|
+
onSelectAnnotation?: (id: string | null) => void;
|
|
109
|
+
/**
|
|
110
|
+
* Fired when the pointer enters an annotation (its `id`) or leaves it (`null`).
|
|
111
|
+
* Mirror it to a controlled `hovered` prop on each mark to sync hover both ways
|
|
112
|
+
* (e.g. a legend row ↔ the mark). Fires in any mode.
|
|
113
|
+
*/
|
|
114
|
+
onHoverAnnotation?: (id: string | null) => void;
|
|
115
|
+
/**
|
|
116
|
+
* Fired when a mark is **double-clicked** — the request to edit just that one
|
|
117
|
+
* (set its `editing` prop in response). Single click selects (inspect); double
|
|
118
|
+
* click edits. Works in any mode.
|
|
119
|
+
*/
|
|
120
|
+
onEditAnnotation?: (id: string) => void;
|
|
121
|
+
/**
|
|
122
|
+
* Snap mode (the toolbar's "Snap"). **Default `true`.** When on, a dragged
|
|
123
|
+
* mark snaps to other marks' **guidelines** (their x-positions, within a few
|
|
124
|
+
* px) so spans align; off = free placement. (Snapping to the nearest data
|
|
125
|
+
* sample is not implemented — guideline alignment only.)
|
|
126
|
+
*/
|
|
127
|
+
snap?: boolean;
|
|
85
128
|
/**
|
|
86
129
|
* Time-axis value formatting — a d3 time specifier string (e.g. `'%H:%M'`) or a
|
|
87
130
|
* `(epochMs) => string` function ({@link AxisFormat}); applies to both the time
|
|
@@ -102,5 +145,5 @@ export interface ChartContainerProps {
|
|
|
102
145
|
* {@link TimeAxis} at the bottom, aligned under the plots. Y axes are per-row
|
|
103
146
|
* (`<YAxis>`).
|
|
104
147
|
*/
|
|
105
|
-
export declare function ChartContainer({ range, width, rowGap, showAxis, trackerPosition, onTrackerChanged, selected, onSelect, panZoom, onTimeRangeChange, minDuration, cursor, cursorTime, timeFormat, theme, children, }: ChartContainerProps): import("react/jsx-runtime").JSX.Element;
|
|
148
|
+
export declare function ChartContainer({ range, width, rowGap, showAxis, trackerPosition, onTrackerChanged, selected, onSelect, panZoom, onTimeRangeChange, minDuration, cursor, cursorTime, editAnnotations, creating, onCreate, onSelectAnnotation, onHoverAnnotation, onEditAnnotation, snap, timeFormat, theme, children, }: ChartContainerProps): import("react/jsx-runtime").JSX.Element;
|
|
106
149
|
//# sourceMappingURL=ChartContainer.d.ts.map
|
package/dist/ChartContainer.js
CHANGED
|
@@ -3,6 +3,7 @@ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } f
|
|
|
3
3
|
import { scaleLinear, scaleTime } from 'd3-scale';
|
|
4
4
|
import { ContainerContext, } from './context.js';
|
|
5
5
|
import { maxSlotWidths, sum } from './slots.js';
|
|
6
|
+
import { computeLabelLanes } from './annotations.js';
|
|
6
7
|
import { resolveCursorX, DEFAULT_CURSOR_MODE } from './tracker.js';
|
|
7
8
|
import { resolveAxisFormat, resolveTimeFormat, } from './format.js';
|
|
8
9
|
import { TimeAxis } from './TimeAxis.js';
|
|
@@ -30,7 +31,7 @@ function normalizeRange(range) {
|
|
|
30
31
|
* {@link TimeAxis} at the bottom, aligned under the plots. Y axes are per-row
|
|
31
32
|
* (`<YAxis>`).
|
|
32
33
|
*/
|
|
33
|
-
export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trackerPosition, onTrackerChanged, selected, onSelect, panZoom = false, onTimeRangeChange, minDuration = 1, cursor = DEFAULT_CURSOR_MODE, cursorTime = false, timeFormat, theme, children, }) {
|
|
34
|
+
export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trackerPosition, onTrackerChanged, selected, onSelect, panZoom = false, onTimeRangeChange, minDuration = 1, cursor = DEFAULT_CURSOR_MODE, cursorTime = false, editAnnotations = false, creating = null, onCreate, onSelectAnnotation, onHoverAnnotation, onEditAnnotation, snap = true, timeFormat, theme, children, }) {
|
|
34
35
|
// The explicit base domain from `range` (a tuple or a TimeRange). `undefined`
|
|
35
36
|
// ⇒ auto-fit (resolved from the layers below). Pan/zoom seeds from it; `seed`
|
|
36
37
|
// is the placeholder while auto-fitting.
|
|
@@ -91,6 +92,21 @@ export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trac
|
|
|
91
92
|
return next;
|
|
92
93
|
});
|
|
93
94
|
}, []);
|
|
95
|
+
// Annotations register here so the container can do what a mark can't in
|
|
96
|
+
// isolation: draw its guide line across other rows, order regions, serve snap
|
|
97
|
+
// targets. Keyed by per-instance slot key (same discipline as the sources).
|
|
98
|
+
const [annotationMap, setAnnotationMap] = useState(() => new Map());
|
|
99
|
+
const registerAnnotation = useCallback((key, spec) => setAnnotationMap((m) => new Map(m).set(key, spec)), []);
|
|
100
|
+
const unregisterAnnotation = useCallback((key) => {
|
|
101
|
+
setAnnotationMap((m) => {
|
|
102
|
+
if (!m.has(key))
|
|
103
|
+
return m;
|
|
104
|
+
const next = new Map(m);
|
|
105
|
+
next.delete(key);
|
|
106
|
+
return next;
|
|
107
|
+
});
|
|
108
|
+
}, []);
|
|
109
|
+
const annotations = useMemo(() => Array.from(annotationMap.values()), [annotationMap]);
|
|
94
110
|
// The shared x scale's kind, **inferred from the registered layers**: a
|
|
95
111
|
// ValueSeries row plots on a value axis, a TimeSeries on time. A container
|
|
96
112
|
// has one shared x (the synced cursor's whole point), so the rows must agree
|
|
@@ -238,6 +254,9 @@ export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trac
|
|
|
238
254
|
const values = Array.from(sources.values()).flatMap((s) => s.sampleAt(time));
|
|
239
255
|
cb({ time, values });
|
|
240
256
|
}, [cursorX, xScale, sources, plotWidth]);
|
|
257
|
+
// Pack overlapping top-flag labels (markers + regions) into stacked lanes so
|
|
258
|
+
// close-in-x labels don't collide; chips read their lane back off the frame.
|
|
259
|
+
const labelLanes = useMemo(() => computeLabelLanes(annotations, (v) => xScale(v)), [annotations, xScale]);
|
|
241
260
|
const frame = useMemo(() => ({
|
|
242
261
|
timeRange: [d0, d1],
|
|
243
262
|
width,
|
|
@@ -256,9 +275,20 @@ export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trac
|
|
|
256
275
|
setHovered,
|
|
257
276
|
cursor,
|
|
258
277
|
cursorTime,
|
|
278
|
+
editAnnotations,
|
|
279
|
+
creating,
|
|
280
|
+
snap,
|
|
281
|
+
onCreate,
|
|
282
|
+
onSelectAnnotation,
|
|
283
|
+
onHoverAnnotation,
|
|
284
|
+
onEditAnnotation,
|
|
259
285
|
formatTime,
|
|
260
286
|
registerTrackerSource,
|
|
261
287
|
unregisterTrackerSource,
|
|
288
|
+
registerAnnotation,
|
|
289
|
+
unregisterAnnotation,
|
|
290
|
+
annotations,
|
|
291
|
+
labelLanes,
|
|
262
292
|
xScale,
|
|
263
293
|
xKind: resolvedKind,
|
|
264
294
|
panZoom,
|
|
@@ -285,9 +315,20 @@ export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trac
|
|
|
285
315
|
setHovered,
|
|
286
316
|
cursor,
|
|
287
317
|
cursorTime,
|
|
318
|
+
editAnnotations,
|
|
319
|
+
creating,
|
|
320
|
+
snap,
|
|
321
|
+
onCreate,
|
|
322
|
+
onSelectAnnotation,
|
|
323
|
+
onHoverAnnotation,
|
|
324
|
+
onEditAnnotation,
|
|
288
325
|
formatTime,
|
|
289
326
|
registerTrackerSource,
|
|
290
327
|
unregisterTrackerSource,
|
|
328
|
+
registerAnnotation,
|
|
329
|
+
unregisterAnnotation,
|
|
330
|
+
annotations,
|
|
331
|
+
labelLanes,
|
|
291
332
|
xScale,
|
|
292
333
|
resolvedKind,
|
|
293
334
|
panZoom,
|
package/dist/ChartRow.js
CHANGED
|
@@ -92,6 +92,7 @@ export function ChartRow({ height, cursor, children }) {
|
|
|
92
92
|
min: undefined,
|
|
93
93
|
max: undefined,
|
|
94
94
|
format: undefined,
|
|
95
|
+
tickValues: undefined,
|
|
95
96
|
index: 0,
|
|
96
97
|
},
|
|
97
98
|
], [realAxes]);
|
|
@@ -152,12 +153,25 @@ export function ChartRow({ height, cursor, children }) {
|
|
|
152
153
|
}
|
|
153
154
|
return map;
|
|
154
155
|
}, [effectiveAxes, yScales]);
|
|
156
|
+
// Explicit tick values per axis (axes that set `<YAxis ticks>`) — so Layers
|
|
157
|
+
// draws the row's gridlines at the same positions the axis labels, instead of
|
|
158
|
+
// d3's auto-picked ticks.
|
|
159
|
+
const tickValues = useMemo(() => {
|
|
160
|
+
const map = new Map();
|
|
161
|
+
for (const ax of effectiveAxes) {
|
|
162
|
+
if (ax.tickValues)
|
|
163
|
+
map.set(ax.id, ax.tickValues);
|
|
164
|
+
}
|
|
165
|
+
return map;
|
|
166
|
+
}, [effectiveAxes]);
|
|
155
167
|
const frame = useMemo(() => ({
|
|
156
168
|
height,
|
|
157
169
|
cursor,
|
|
158
170
|
isFirstRow,
|
|
171
|
+
rowKey,
|
|
159
172
|
yScales,
|
|
160
173
|
formats,
|
|
174
|
+
tickValues,
|
|
161
175
|
defaultAxisId,
|
|
162
176
|
axisSlots,
|
|
163
177
|
registerAxis,
|
|
@@ -169,8 +183,10 @@ export function ChartRow({ height, cursor, children }) {
|
|
|
169
183
|
height,
|
|
170
184
|
cursor,
|
|
171
185
|
isFirstRow,
|
|
186
|
+
rowKey,
|
|
172
187
|
yScales,
|
|
173
188
|
formats,
|
|
189
|
+
tickValues,
|
|
174
190
|
defaultAxisId,
|
|
175
191
|
axisSlots,
|
|
176
192
|
registerAxis,
|