@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 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.31.2...HEAD
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
@@ -1,9 +1,15 @@
1
- import type { SeriesSchema, TimeSeries } from 'pond-ts';
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
- /** The source series. Its key column supplies the time axis. */
6
- series: TimeSeries<S>;
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 { fromTimeSeries } from './data.js';
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(() => fromTimeSeries(series, column), [series, column]);
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
- xKind: 'time',
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: (time) => {
62
- // No readout past the data (tracker policy — core's nearest() clamps
63
- // to an endpoint outside the span); bounds from the columnar time axis.
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
- const e = series.nearest(time);
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
@@ -1,8 +1,15 @@
1
- import type { SeriesSchema, TimeSeries } from 'pond-ts';
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
- /** The source series. Its key column supplies the time axis. */
5
- series: TimeSeries<S>;
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 { bandFromTimeSeries } from './data.js';
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(() => bandFromTimeSeries(series, lower, upper), [series, lower, upper]);
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
- xKind: 'time',
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: (time) => {
44
- // No readout past the data (tracker policy — nearest() clamps); bounds
45
- // from the columnar time axis.
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
- const e = series.nearest(time);
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
@@ -1,12 +1,17 @@
1
- import type { SeriesSchema, TimeSeries } from 'pond-ts';
2
- export interface BarChartProps<S extends SeriesSchema> {
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 form — each
5
- * event's key `[begin, end]` is a bar's x-span. A **point-keyed** (`time`)
6
- * series is supported too: each bar's width is derived from neighbour spacing
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
- * **Distance domain is deferred** — v1 bars scale on the shared **time** xScale
62
- * only. estela's distance-domain (records over a monotonic value axis) needs
63
- * value-axis support that isn't built yet (charts RFC perf section).
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 { barsFromTimeSeries } from './data.js';
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
- * **Distance domain is deferred** — v1 bars scale on the shared **time** xScale
29
- * only. estela's distance-domain (records over a monotonic value axis) needs
30
- * value-axis support that isn't built yet (charts RFC perf section).
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(() => barsFromTimeSeries(series, column), [series, column]);
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
- xKind: 'time',
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
@@ -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
@@ -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,