@pond-ts/charts 0.69.0 → 0.71.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.
Files changed (46) hide show
  1. package/AGENTS.md +3 -1
  2. package/API.md +55 -56
  3. package/CHANGELOG.md +177 -1
  4. package/dist/AreaChart.d.ts +30 -22
  5. package/dist/AreaChart.js +37 -22
  6. package/dist/BandChart.d.ts +20 -1
  7. package/dist/BandChart.js +18 -2
  8. package/dist/BoxPlot.js +5 -3
  9. package/dist/Candlestick.d.ts +3 -3
  10. package/dist/Candlestick.js +5 -5
  11. package/dist/ChartContainer.d.ts +8 -149
  12. package/dist/ChartContainer.js +68 -111
  13. package/dist/ChartRow.d.ts +1 -12
  14. package/dist/ChartRow.js +15 -30
  15. package/dist/HeatMap.d.ts +3 -3
  16. package/dist/HeatMap.js +3 -3
  17. package/dist/Layers.js +84 -34
  18. package/dist/XAxis.js +1 -0
  19. package/dist/area.d.ts +6 -5
  20. package/dist/area.js +13 -6
  21. package/dist/axis-events.d.ts +4 -4
  22. package/dist/band.d.ts +10 -2
  23. package/dist/band.js +38 -10
  24. package/dist/brush.d.ts +13 -21
  25. package/dist/brush.js +22 -36
  26. package/dist/context.d.ts +81 -96
  27. package/dist/cursors.d.ts +81 -51
  28. package/dist/cursors.js +164 -127
  29. package/dist/decimate.d.ts +10 -1
  30. package/dist/decimate.js +51 -10
  31. package/dist/elapsed.d.ts +1 -1
  32. package/dist/elapsed.js +1 -1
  33. package/dist/format.d.ts +2 -1
  34. package/dist/index.d.ts +1 -1
  35. package/dist/selection-fixtures.d.ts +4 -7
  36. package/dist/selection-fixtures.js +0 -9
  37. package/dist/selection-stories.d.ts +4 -6
  38. package/dist/selection-stories.js +4 -4
  39. package/dist/selectors.d.ts +2 -3
  40. package/dist/selectors.js +2 -3
  41. package/dist/tracker.d.ts +10 -28
  42. package/dist/tracker.js +17 -40
  43. package/dist/tradingAxis.fixture.d.ts +19 -0
  44. package/dist/tradingAxis.fixture.js +24 -0
  45. package/dist/tradingTimeScale.d.ts +2 -2
  46. package/package.json +3 -3
@@ -48,8 +48,6 @@ export const categoricalBars = {
48
48
  picks: SERVICES.map((c) => ({ label: c.label, info: svc(c.label) })),
49
49
  describe: (hit) => hit.mark ?? hit.label,
50
50
  // No `sequence`: a time bucketing over ordinal slots is meaningless.
51
- // No `rangeCursor`: it gates on a continuous x.
52
- rangeCursor: false,
53
51
  sweep: true,
54
52
  };
55
53
  // ── Time axis ──────────────────────────────────────────────────────────────
@@ -108,7 +106,6 @@ export const timeBars = {
108
106
  * bar so the buckets line up with the data rather than with the epoch.
109
107
  */
110
108
  sequence: () => Sequence.every('7d', { anchor: D0 }),
111
- rangeCursor: true,
112
109
  sweep: true,
113
110
  };
114
111
  // ── Stacked bars (many marks per bin) ──────────────────────────────────────
@@ -177,7 +174,6 @@ export const stackedBars = {
177
174
  ? isoDay(hit.key)
178
175
  : `${isoDay(hit.key)}·${hit.label}`,
179
176
  sequence: () => Sequence.every('7d', { anchor: D0 }),
180
- rangeCursor: true,
181
177
  sweep: true,
182
178
  };
183
179
  // ── Box plots (a mark with internal structure) ─────────────────────────────
@@ -258,7 +254,6 @@ const boxFixture = (name, shape, note) => ({
258
254
  /** Three 10-minute boxes per bucket — coarser than the mark, so the snap has
259
255
  * something to do (the lesson `timeBars` learned with its `'1d'`). */
260
256
  sequence: () => Sequence.every('30m', { anchor: BOX_BASE }),
261
- rangeCursor: true,
262
257
  });
263
258
  /** The default shape: a q1→q3 body with thin stems and end caps. The stems are
264
259
  * the interesting part for selection — they are drawn ink far from the body. */
@@ -332,7 +327,6 @@ export const candles = {
332
327
  describe: (hit) => isoDay(hit.key),
333
328
  sweep: true,
334
329
  sequence: () => Sequence.every('5d', { anchor: D0 }),
335
- rangeCursor: true,
336
330
  };
337
331
  // ── The 2-D family: scatter and heat map ──────────────────────────────────
338
332
  /** Forty points on a time axis — a scatter's marks live at `(x, y)`, not in a
@@ -402,7 +396,6 @@ export const scatterPoints = {
402
396
  // A **2-D** sweep: the drag cuts a free rect, and the committed span carries
403
397
  // a `y` window alongside `x` ([PND-INTERACT2D]).
404
398
  sweep: true,
405
- rangeCursor: true,
406
399
  };
407
400
  /**
408
401
  * **A heat-map column** — a *grid* of cells, `(bin × row)`.
@@ -443,7 +436,6 @@ export const heatGrid = {
443
436
  // A **2-D** sweep, snapped in both dimensions — bins on x, rows on y — so a
444
437
  // capture is always a contiguous rectangle of cells ([PND-INTERACT2D]).
445
438
  sweep: true,
446
- rangeCursor: true,
447
439
  };
448
440
  // ── Trading sessions (a discontinuous time axis) ───────────────────────────
449
441
  /** Six weekday sessions (09:30–16:00 UTC) from a Monday — five overnight seams
@@ -531,7 +523,6 @@ export const tradingSessions = {
531
523
  // the same bucketing `sessions.conforming` names, so the generated story and
532
524
  // the session pair below agree about what "conforming" means.
533
525
  sequence: () => sessionSeq(SESSIONS),
534
- rangeCursor: true,
535
526
  sweep: true,
536
527
  sessions: {
537
528
  conforming: () => sessionSeq(SESSIONS),
@@ -9,15 +9,13 @@ import { type ChartFixture } from './selection-fixtures.js';
9
9
  * to write the story. That is what makes walking the matrix a review technique
10
10
  * rather than a gallery — see CLAUDE.md → "Storybook stories".
11
11
  *
12
- * Stories whose subject is *not* chart-type dependent (the deprecation shim,
13
- * for instance) deliberately live in one column only; running them against
14
- * every fixture would test nothing new.
12
+ * Stories whose subject is *not* chart-type dependent deliberately live in one
13
+ * column only; running them against every fixture would test nothing new.
15
14
  */
16
15
  type Story = StoryObj;
17
16
  /**
18
- * The `<Selector>` feature set for one chart type. `<RangeCursor>` is mounted
19
- * only where the fixture declares it draws — on an ordinal axis it does not,
20
- * and mounting one there costs the row its cursor entirely.
17
+ * The `<Selector>` feature set for one chart type, each with a `<RangeCursor>`
18
+ * mounted (on an ordinal axis it shades the slot under the pointer).
21
19
  */
22
20
  export interface SelectorStories {
23
21
  MountedAtContainer: Story;
@@ -6,7 +6,7 @@ import { Layers } from './Layers.js';
6
6
  import { YAxis } from './YAxis.js';
7
7
  import { Selector, MultiSelector } from './selectors.js';
8
8
  import { isSpanSelection, sameMark, selectionContains } from './span.js';
9
- import { RangeCursor } from './cursors.js';
9
+ import { LineCursor, RangeCursor } from './cursors.js';
10
10
  import { caption } from './selection-fixtures.js';
11
11
  const H = 240;
12
12
  /**
@@ -23,7 +23,7 @@ function Chart({ fx, children, height = H, selector, ...container }) {
23
23
  }
24
24
  const list = (fx, sel) => sel.map((m) => fx.describe(m)).join(', ') || '—';
25
25
  export function makeSelectorStories(fx) {
26
- const Cursor = () => (fx.rangeCursor ? _jsx(RangeCursor, {}) : null);
26
+ const Cursor = () => _jsx(RangeCursor, {});
27
27
  return {
28
28
  /** **Mounted at the container** — the ordinary case: one `<Selector>` as a
29
29
  * child of `<ChartContainer>`, enabling the click for every row. It
@@ -40,7 +40,7 @@ export function makeSelectorStories(fx) {
40
40
  MountedInRow: {
41
41
  render: function Render() {
42
42
  const [sel, setSel] = useState([]);
43
- return (_jsxs("div", { children: [_jsxs(ChartContainer, { width: 640, ...fx.container, children: [_jsxs(ChartRow, { height: 150, children: [_jsx(YAxis, { id: fx.axis.id, label: fx.axis.label || 'primary', min: fx.axis.min, max: fx.axis.max }), _jsx(Cursor, {}), _jsx(Selector, { selected: sel, onSelect: (h) => setSel(h === null ? [] : [h]), children: _jsx(Layers, { children: fx.renderLayer('svc') }) })] }), _jsxs(ChartRow, { height: 110, children: [_jsx(YAxis, { id: fx.secondary.axis.id, label: fx.secondary.axis.label, min: fx.secondary.axis.min, max: fx.secondary.axis.max }), _jsx(Layers, { children: fx.secondary.renderLayer('err') })] })] }), _jsxs("p", { style: caption, children: ["The top row selects; the bottom row has no", ' ', _jsx("code", { children: "<Selector>" }), " in scope. ", _jsx("strong", { children: "selected:" }), ' ', list(fx, sel)] })] }));
43
+ return (_jsxs("div", { children: [_jsxs(ChartContainer, { width: 640, ...fx.container, children: [_jsx(LineCursor, {}), _jsxs(ChartRow, { height: 150, children: [_jsx(YAxis, { id: fx.axis.id, label: fx.axis.label || 'primary', min: fx.axis.min, max: fx.axis.max }), _jsx(Cursor, {}), _jsx(Selector, { selected: sel, onSelect: (h) => setSel(h === null ? [] : [h]), children: _jsx(Layers, { children: fx.renderLayer('svc') }) })] }), _jsxs(ChartRow, { height: 110, children: [_jsx(YAxis, { id: fx.secondary.axis.id, label: fx.secondary.axis.label, min: fx.secondary.axis.min, max: fx.secondary.axis.max }), _jsx(Layers, { children: fx.secondary.renderLayer('err') })] })] }), _jsxs("p", { style: caption, children: ["The top row selects; the bottom row has no", ' ', _jsx("code", { children: "<Selector>" }), " in scope. ", _jsx("strong", { children: "selected:" }), ' ', list(fx, sel)] })] }));
44
44
  },
45
45
  },
46
46
  /** **No selector — the plot is inert.** An id-bearing, hit-testable layer
@@ -68,7 +68,7 @@ export function makeSelectorStories(fx) {
68
68
  };
69
69
  return (_jsxs("div", { children: [_jsx("div", { style: { marginBottom: 8 }, children: fx.picks.map((p) => (_jsx("button", { style: btn, onClick: () => setSel((cur) => cur.some((m) => sameMark(m, p.info))
70
70
  ? cur.filter((m) => !sameMark(m, p.info))
71
- : [...cur, p.info]), children: p.label }, p.label))) }), _jsx(Chart, { fx: fx, selector: _jsx(Selector, { enabled: false, selected: sel }) }), _jsxs("p", { style: caption, children: ["The buttons drive the highlight; clicking the plot does nothing.", ' ', _jsx("strong", { children: "selected:" }), " ", list(fx, sel)] })] }));
71
+ : [...cur, p.info]), children: p.label }, p.label))) }), _jsx(Chart, { fx: fx, selector: _jsx(Selector, { enabled: false, selected: sel }), children: _jsx(LineCursor, {}) }), _jsxs("p", { style: caption, children: ["The buttons drive the highlight; clicking the plot does nothing.", ' ', _jsx("strong", { children: "selected:" }), " ", list(fx, sel)] })] }));
72
72
  },
73
73
  },
74
74
  /** **The modifiers are reported; the policy is yours.** `onSelect`'s second
@@ -227,9 +227,8 @@ export interface MultiSelectorProps {
227
227
  *
228
228
  * - The shared brush band becomes the **resting cursor**, spanning the snap
229
229
  * block under the pointer (the `sequence` bucket, else the layer's own
230
- * bin/slot), replacing the container's implicit `'line'` default. An
231
- * explicitly chosen cursor — a mounted component, or a legacy `cursor`
232
- * string the consumer actually set — still wins the surface.
230
+ * bin/slot), when no cursor component is mounted for the row. A mounted
231
+ * cursor still wins the surface.
233
232
  * - Hover is **block-scoped**: pointing at any one mark of a block lights
234
233
  * (and reports) every mark in it. Rest and drag share one code path — the
235
234
  * same snap buckets, the same layer session — so what the rest previews and
package/dist/selectors.js CHANGED
@@ -173,9 +173,8 @@ export function Selector({ enabled = true, selected, hovered, onHover, onSelect,
173
173
  *
174
174
  * - The shared brush band becomes the **resting cursor**, spanning the snap
175
175
  * block under the pointer (the `sequence` bucket, else the layer's own
176
- * bin/slot), replacing the container's implicit `'line'` default. An
177
- * explicitly chosen cursor — a mounted component, or a legacy `cursor`
178
- * string the consumer actually set — still wins the surface.
176
+ * bin/slot), when no cursor component is mounted for the row. A mounted
177
+ * cursor still wins the surface.
179
178
  * - Hover is **block-scoped**: pointing at any one mark of a block lights
180
179
  * (and reports) every mark in it. Rest and drag share one code path — the
181
180
  * same snap buckets, the same layer session — so what the rest previews and
package/dist/tracker.d.ts CHANGED
@@ -1,11 +1,10 @@
1
1
  /**
2
- * Cursor geometry helpers — pure functions deciding *what* the cursor draws
3
- * ({@link cursorParts}) and *where* it sits ({@link resolveCursorX}). The marks
2
+ * Cursor geometry helpers — pure functions deciding *where* the cursor sits
3
+ * ({@link resolveCursorX}) and which bucket it shades. The marks
4
4
  * themselves render as an SVG overlay in `Layers` (no cursor canvas); these
5
5
  * helpers stay pure, so they're unit-tested directly.
6
6
  */
7
- import type { Interval } from 'pond-ts';
8
- import type { CursorMode } from './context.js';
7
+ import { Interval } from 'pond-ts';
9
8
  /**
10
9
  * The interval in the sorted, non-overlapping `buckets` that contains `t`
11
10
  * (`begin ≤ t < end`), or `undefined` if `t` falls in no bucket. Binary search —
@@ -39,30 +38,13 @@ export declare function bandRect(buckets: readonly Interval[], t1: number, xScal
39
38
  x0: number;
40
39
  x1: number;
41
40
  } | null;
42
- /** Default cursor mode — the synced vertical line (cursor enabled on the
43
- * container by default; pair with an off-chart readout via `onTrackerChanged`). */
44
- export declare const DEFAULT_CURSOR_MODE: CursorMode;
45
41
  /**
46
- * Decompose a {@link CursorMode} into what it draws: the shared vertical line,
47
- * the per-series dots, and which value chip (if any). The modes are exclusive
48
- * presets — `line` is line-only, `point` / `inline` / `flag` are dot-based with
49
- * no line, `none` draws nothing. `flag` raises a staff from each point to a
50
- * value flag stacked near the top of the row (drawn in `Layers`).
51
- *
52
- * **Superseded by the mounted cursor presets** (`cursors.tsx` — each mode's
53
- * drawing now lives in its component's registered spec): `Layers` no longer
54
- * reads this. Kept, with its tests, for the life of the `cursor` string-prop
55
- * deprecation window as the pinned record of what each mode drew; delete both
56
- * when the modes go.
42
+ * The unit slots `[i, i+1)` of a category axis, from its band scale's domain
43
+ * (`[0, n]`) — the buckets a `<RangeCursor>` snaps to there when no bar layer
44
+ * has published its own (a bar layer's are the same slots). Empty for an
45
+ * empty axis.
57
46
  */
58
- export declare function cursorParts(mode: CursorMode): {
59
- readonly line: boolean;
60
- readonly dots: boolean;
61
- readonly chip: 'none' | 'inline' | 'flag' | 'axis';
62
- /** `region` mode: a shaded **band** over the bucket under the pointer (from
63
- * `cursorSequence`), drawn by `Layers`; no line/dots/chip of its own. */
64
- readonly band: boolean;
65
- };
47
+ export declare function categorySlots(domain: readonly unknown[]): Interval[];
66
48
  /**
67
49
  * The crosshair's plot-pixel x from the tracker inputs. **A live local pointer
68
50
  * always wins:** a chart the user is actively hovering (`hoverX` non-null) shows
@@ -79,8 +61,8 @@ export declare function cursorParts(mode: CursorMode): {
79
61
  *
80
62
  * `null` and `undefined` are **equivalent** — both mean "no controlled position"
81
63
  * (a hovered chart still tracks its pointer; a non-hovered one shows nothing). To
82
- * force a chart to never show a cursor at all, use `cursor="none"`, not
83
- * `trackerPosition={null}`.
64
+ * force a chart to never show a cursor at all, mount no cursor component, rather
65
+ * than relying on `trackerPosition={null}`.
84
66
  */
85
67
  export declare function resolveCursorX(trackerPosition: number | null | undefined, hoverX: number | null, xScale: (time: number) => number): number | null;
86
68
  //# sourceMappingURL=tracker.d.ts.map
package/dist/tracker.js CHANGED
@@ -1,9 +1,10 @@
1
1
  /**
2
- * Cursor geometry helpers — pure functions deciding *what* the cursor draws
3
- * ({@link cursorParts}) and *where* it sits ({@link resolveCursorX}). The marks
2
+ * Cursor geometry helpers — pure functions deciding *where* the cursor sits
3
+ * ({@link resolveCursorX}) and which bucket it shades. The marks
4
4
  * themselves render as an SVG overlay in `Layers` (no cursor canvas); these
5
5
  * helpers stay pure, so they're unit-tested directly.
6
6
  */
7
+ import { Interval } from 'pond-ts';
7
8
  /**
8
9
  * The interval in the sorted, non-overlapping `buckets` that contains `t`
9
10
  * (`begin ≤ t < end`), or `undefined` if `t` falls in no bucket. Binary search —
@@ -70,45 +71,21 @@ export function bandRect(buckets, t1, xScale, plotWidth, t2) {
70
71
  const x1 = Math.min(plotWidth, xScale(span.end));
71
72
  return x1 > x0 ? { x0, x1 } : null;
72
73
  }
73
- /** Default cursor mode — the synced vertical line (cursor enabled on the
74
- * container by default; pair with an off-chart readout via `onTrackerChanged`). */
75
- export const DEFAULT_CURSOR_MODE = 'line';
76
74
  /**
77
- * Decompose a {@link CursorMode} into what it draws: the shared vertical line,
78
- * the per-series dots, and which value chip (if any). The modes are exclusive
79
- * presets — `line` is line-only, `point` / `inline` / `flag` are dot-based with
80
- * no line, `none` draws nothing. `flag` raises a staff from each point to a
81
- * value flag stacked near the top of the row (drawn in `Layers`).
82
- *
83
- * **Superseded by the mounted cursor presets** (`cursors.tsx` — each mode's
84
- * drawing now lives in its component's registered spec): `Layers` no longer
85
- * reads this. Kept, with its tests, for the life of the `cursor` string-prop
86
- * deprecation window as the pinned record of what each mode drew; delete both
87
- * when the modes go.
75
+ * The unit slots `[i, i+1)` of a category axis, from its band scale's domain
76
+ * (`[0, n]`) — the buckets a `<RangeCursor>` snaps to there when no bar layer
77
+ * has published its own (a bar layer's are the same slots). Empty for an
78
+ * empty axis.
88
79
  */
89
- export function cursorParts(mode) {
90
- const base = { line: false, dots: false, chip: 'none', band: false };
91
- switch (mode) {
92
- case 'line':
93
- return { ...base, line: true };
94
- case 'point':
95
- return { ...base, dots: true };
96
- case 'inline':
97
- return { ...base, dots: true, chip: 'inline' };
98
- case 'flag':
99
- return { ...base, dots: true, chip: 'flag' };
100
- case 'crosshair':
101
- // A single reticle (not per-series): `Layers` draws the dashed vertical +
102
- // full-width horizontal lines, the centre dot, and one value pill itself
103
- // (so no generic line/dots here); the x-time pill is on `<XAxis>`.
104
- return { ...base, chip: 'axis' };
105
- case 'region':
106
- // A shaded band over the bucket under the pointer — `Layers` resolves the
107
- // bucket from `cursorBuckets` and draws the rect (cropped through xScale).
108
- return { ...base, band: true };
109
- case 'none':
110
- return { ...base };
80
+ export function categorySlots(domain) {
81
+ const lo = Number(domain[0]);
82
+ const n = Math.max(0, Math.round(Number(domain[1]) - lo));
83
+ const out = [];
84
+ for (let i = 0; i < n; i += 1) {
85
+ const b = lo + i;
86
+ out.push(new Interval({ value: b, start: b, end: b + 1 }));
111
87
  }
88
+ return out;
112
89
  }
113
90
  /**
114
91
  * The crosshair's plot-pixel x from the tracker inputs. **A live local pointer
@@ -126,8 +103,8 @@ export function cursorParts(mode) {
126
103
  *
127
104
  * `null` and `undefined` are **equivalent** — both mean "no controlled position"
128
105
  * (a hovered chart still tracks its pointer; a non-hovered one shows nothing). To
129
- * force a chart to never show a cursor at all, use `cursor="none"`, not
130
- * `trackerPosition={null}`.
106
+ * force a chart to never show a cursor at all, mount no cursor component, rather
107
+ * than relying on `trackerPosition={null}`.
131
108
  */
132
109
  export function resolveCursorX(trackerPosition, hoverX, xScale) {
133
110
  if (hoverX !== null)
@@ -50,6 +50,25 @@ export declare function ticks(sessions: Session[], stepMs: number): TimeSeries<t
50
50
  * pen-up. (Plain {@link ticks} walks continuously across sessions, so close ≈
51
51
  * next open and the break is invisible — this is the fixture that shows it.) */
52
52
  export declare function gappingTicks(sessions: Session[], stepMs: number): TimeSeries<typeof tickSchema>;
53
+ export declare const envelopeSchema: readonly [{
54
+ readonly name: "time";
55
+ readonly kind: "time";
56
+ }, {
57
+ readonly name: "price";
58
+ readonly kind: "number";
59
+ }, {
60
+ readonly name: "lo";
61
+ readonly kind: "number";
62
+ }, {
63
+ readonly name: "hi";
64
+ readonly kind: "number";
65
+ }];
66
+ /** {@link gappingTicks} plus a `lo` / `hi` envelope around the price (a slowly
67
+ * breathing spread), so a `<BandChart>` on the trading axis shows the same
68
+ * overnight jump the line does: connected, the fill runs a near-vertical sliver
69
+ * from one session's close to the next open; with `sessionBreaks` it ends at
70
+ * the close and re-starts at the open. */
71
+ export declare function gappingEnvelope(sessions: Session[], stepMs: number): TimeSeries<typeof envelopeSchema>;
53
72
  export declare const OHLC: {
54
73
  readonly open: {
55
74
  readonly from: "price";
@@ -195,6 +195,30 @@ export function gappingTicks(sessions, stepMs) {
195
195
  });
196
196
  return new TimeSeries({ name: 'ticks', schema: tickSchema, rows });
197
197
  }
198
+ export const envelopeSchema = [
199
+ { name: 'time', kind: 'time' },
200
+ { name: 'price', kind: 'number' },
201
+ { name: 'lo', kind: 'number' },
202
+ { name: 'hi', kind: 'number' },
203
+ ];
204
+ /** {@link gappingTicks} plus a `lo` / `hi` envelope around the price (a slowly
205
+ * breathing spread), so a `<BandChart>` on the trading axis shows the same
206
+ * overnight jump the line does: connected, the fill runs a near-vertical sliver
207
+ * from one session's close to the next open; with `sessionBreaks` it ends at
208
+ * the close and re-starts at the open. */
209
+ export function gappingEnvelope(sessions, stepMs) {
210
+ const rows = [];
211
+ let i = 0;
212
+ sessions.forEach((s, si) => {
213
+ const base = 100 + si * 6; // each session gaps ~6 above the last
214
+ for (let t = s.open; t < s.close; t += stepMs, i++) {
215
+ const price = base + 4 * Math.sin(i / 18) + 1.5 * Math.sin(i / 3.5);
216
+ const spread = 1.5 + 0.8 * Math.sin(i / 40);
217
+ rows.push([t, price, price - spread, price + spread]);
218
+ }
219
+ });
220
+ return new TimeSeries({ name: 'envelope', schema: envelopeSchema, rows });
221
+ }
198
222
  export const OHLC = {
199
223
  open: { from: 'price', using: 'first' },
200
224
  high: { from: 'price', using: 'max' },
@@ -183,13 +183,13 @@ export interface TradingTimeScale {
183
183
  * `02 AM`; a sub-day axis reads date **+** clock). This is the grain-aware
184
184
  * default the crosshair pill, marker indicators, and annotation auto-labels
185
185
  * use — replacing d3's multi-scale default, which showed local time-of-day
186
- * for any off-local-midnight instant. A container `cursorFormat` overrides it.
186
+ * for any off-local-midnight instant. A cursor's `format` overrides it.
187
187
  */
188
188
  readoutFormat(count?: number): (value: number) => string;
189
189
  /**
190
190
  * The axis's resolved **coarse grain** ({@link TimeGrain}) at `count` — the
191
191
  * unit the ticks currently sit on (`year` … `second`), strides collapsed. A
192
- * `cursorFormat` callback receives this so it can branch on the zoom level
192
+ * cursor `format` callback receives this so it can branch on the zoom level
193
193
  * (return a year when zoomed out, a clock when zoomed in) without
194
194
  * re-deriving the ladder the axis already ran.
195
195
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pond-ts/charts",
3
- "version": "0.69.0",
3
+ "version": "0.71.0",
4
4
  "private": false,
5
5
  "description": "Canvas-rendered, streaming-first React time-series charts for pond-ts: line, area, band, bar, scatter, box, candlestick, with cursors, selection and annotations",
6
6
  "keywords": [
@@ -62,8 +62,8 @@
62
62
  "perf": "PERF_BENCH=1 playwright test perf.spec.ts --workers=1"
63
63
  },
64
64
  "peerDependencies": {
65
- "@pond-ts/react": "^0.69.0",
66
- "pond-ts": "^0.69.0",
65
+ "@pond-ts/react": "^0.71.0",
66
+ "pond-ts": "^0.71.0",
67
67
  "react": "^18.0.0 || ^19.0.0"
68
68
  },
69
69
  "devDependencies": {