@pond-ts/charts 0.68.0 → 0.70.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.
@@ -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' },
@@ -1,4 +1,15 @@
1
1
  import { type TickGranularity, type TimeGrain } from './tickLadder.js';
2
+ /** Zone options shared by {@link scaleTradingTime} and {@link identityProvider}. */
3
+ export interface ScaleTimeZoneOptions {
4
+ /**
5
+ * The IANA zone the axis's calendar runs in — ticks on that zone's
6
+ * midnights / Mondays / month starts, labels and readouts reading in it.
7
+ * **Omitted ⇒ the runtime's local zone** (the browser's), exactly as before
8
+ * the option existed. Any id `Intl` knows (`'UTC'`, `'Europe/Berlin'`, …);
9
+ * an unknown id throws `RangeError`.
10
+ */
11
+ timeZone?: string | undefined;
12
+ }
2
13
  /**
3
14
  * The structural discontinuity-provider surface `scaleTradingTime` consumes to
4
15
  * collapse closed-market time. Charts declares this **shape** itself and never
@@ -29,6 +40,15 @@ export interface DiscontinuityProvider {
29
40
  * `TradingCalendar.discontinuities()` provider supplies it.)
30
41
  */
31
42
  boundaries?(from: number, to: number): number[];
43
+ /**
44
+ * Optional: the same provider with its calendar in another zone. Only a
45
+ * provider whose gap topology *depends* on a zone needs it — the identity
46
+ * provider, whose "sessions" are calendar days and therefore move with the
47
+ * zone; a trading calendar's session opens are instants and do not. Used by
48
+ * {@link TradingTimeScale.withTimeZone} so a second `<XAxis timeZone>` can
49
+ * re-derive its day anchors in its own zone.
50
+ */
51
+ withTimeZone?(timeZone: string | undefined): DiscontinuityProvider;
32
52
  }
33
53
  /**
34
54
  * The high-level counterpart to a bare {@link DiscontinuityProvider}: anything
@@ -45,6 +65,14 @@ export interface TradingCalendarLike {
45
65
  discontinuities(options?: {
46
66
  spacing?: 'proportional' | 'uniform';
47
67
  }): DiscontinuityProvider;
68
+ /**
69
+ * Optional: the exchange's IANA zone. When present and the container has no
70
+ * explicit `timeZone`, the axis renders in it — ticks on exchange-local day
71
+ * starts, labels and readouts in exchange time — instead of the viewer's
72
+ * zone. A `@pond-ts/financial` `TradingCalendar` built `fromRules` carries
73
+ * its rules' zone here.
74
+ */
75
+ readonly timeZone?: string | undefined;
48
76
  }
49
77
  /**
50
78
  * A d3-scale-shaped time scale whose pixel mapping runs through **trading time**
@@ -198,22 +226,35 @@ export interface TradingTimeScale {
198
226
  range(): [number, number];
199
227
  range(next: readonly [number, number]): TradingTimeScale;
200
228
  copy(): TradingTimeScale;
229
+ /**
230
+ * The same pixel mapping (provider, domain, range) with its **calendar in
231
+ * another zone** — `undefined` for runtime-local. The scale's own zone is
232
+ * unchanged; this is how a second `<XAxis timeZone>` strip ticks and labels
233
+ * in its own zone over the container's shared x mapping. A provider that
234
+ * exposes {@link DiscontinuityProvider.withTimeZone} re-derives its day
235
+ * anchors; any other keeps its instants (a trading calendar's session opens
236
+ * are zone-independent).
237
+ */
238
+ withTimeZone(timeZone: string | undefined): TradingTimeScale;
239
+ /** The IANA zone this scale's calendar runs in; `undefined` = runtime-local. */
240
+ timeZone(): string | undefined;
201
241
  }
202
242
  export { coarsenCalendar } from './tickLadder.js';
203
243
  export type { TickGranularity, TimeGrain } from './tickLadder.js';
204
244
  /**
205
245
  * The trivial gap-free {@link DiscontinuityProvider}: live time **is** wall
206
- * time, and every local midnight is a "session open". Backing a plain
246
+ * time, and every midnight (in `timeZone`, default runtime-local) is a
247
+ * "session open". Backing a plain
207
248
  * continuous time axis with `scaleTradingTime(identityProvider())` runs it
208
249
  * through the same logical tick ladder as a trading-calendar axis — calendar
209
250
  * days are the day anchors, so a year of data ticks on month starts and an
210
251
  * afternoon ticks on clock-aligned hours, instead of d3's mixed multi-scale
211
252
  * default.
212
253
  */
213
- export declare function identityProvider(): DiscontinuityProvider;
254
+ export declare function identityProvider(options?: ScaleTimeZoneOptions): DiscontinuityProvider;
214
255
  /**
215
256
  * Build a {@link TradingTimeScale} over the given discontinuity `provider`.
216
257
  * Configure like a d3 scale: `scaleTradingTime(provider).domain([t0, t1]).range([0, width])`.
217
258
  */
218
- export declare function scaleTradingTime(provider: DiscontinuityProvider): TradingTimeScale;
259
+ export declare function scaleTradingTime(provider: DiscontinuityProvider, options?: ScaleTimeZoneOptions): TradingTimeScale;
219
260
  //# sourceMappingURL=tradingTimeScale.d.ts.map
@@ -1,34 +1,84 @@
1
- import { scaleTime } from 'd3-scale';
2
- import { bandFormatFor, bandGrainFor, bandNext, bandShaded, bandStartOf, boundaryFormatFor, boundaryGrainFor, boundaryTicks, buildGridLevels, buildTicks, coarseUnitOf, flatBaseFormatFor, flatFormats, majorFormatFor, nominalStepMs, readoutFormatFor, } from './tickLadder.js';
1
+ import { scaleTime, scaleUtc } from 'd3-scale';
2
+ import { utcFormat } from 'd3-time-format';
3
+ import { TimeZone } from 'pond-ts';
4
+ import { bandFormatFor, bandGrainFor, bandNext, bandShaded, bandStartOf, boundaryFormatFor, boundaryGrainFor, boundaryTicks, buildGridLevels, buildTicks, coarseUnitOf, flatBaseFormatFor, flatFormats, majorFormatFor, nominalStepMs, readoutFormatFor, tickCalendarFor, } from './tickLadder.js';
5
+ /**
6
+ * The d3 time-specifier formatter for a zone. Local (no zone) is d3's own
7
+ * `timeFormat` through the scale, untouched. A named zone formats a
8
+ * **civil-shifted** date — the instant moved by the zone's offset and then
9
+ * read in UTC — so every `%Y %m %d %H %M %S %a %b %p …` directive reads in the
10
+ * zone for free; `%Z` / `%z`, which the shift would render `UTC` / `+0000`,
11
+ * are substituted per instant from the zone itself. The two directives that
12
+ * print the *instant* rather than a wall-clock field — `%s` (epoch seconds)
13
+ * and `%Q` (epoch ms) — read the shifted instant, i.e. offset by the zone;
14
+ * they have no meaning on a zoned axis.
15
+ */
16
+ function zonedFormatter(zone, specifier) {
17
+ // Tokenise so an escaped percent (`%%`) is never read as the start of a
18
+ // directive: `'%%Z'` is a literal `%Z`, not the zone name.
19
+ const tokens = specifier.match(/%%|%[Zz]|[^%]+|%/g) ?? [];
20
+ const hasZoneName = tokens.some((tok) => tok === '%Z' || tok === '%z');
21
+ if (!hasZoneName) {
22
+ const f = utcFormat(specifier);
23
+ return (d) => {
24
+ const t = +d;
25
+ return f(new Date(t + zone.offsetAt(t)));
26
+ };
27
+ }
28
+ const cache = new Map();
29
+ return (d) => {
30
+ const t = +d;
31
+ const offset = zone.offsetAt(t);
32
+ const sign = offset < 0 ? '-' : '+';
33
+ const abs = Math.abs(offset) / 60_000;
34
+ const hh = String(Math.floor(abs / 60)).padStart(2, '0');
35
+ const mm = String(abs % 60).padStart(2, '0');
36
+ const resolved = tokens
37
+ .map((tok) => tok === '%Z'
38
+ ? zone.abbreviation(t).replace(/%/g, '%%')
39
+ : tok === '%z'
40
+ ? `${sign}${hh}${mm}`
41
+ : tok)
42
+ .join('');
43
+ let f = cache.get(resolved);
44
+ if (f === undefined) {
45
+ f = utcFormat(resolved);
46
+ cache.set(resolved, f);
47
+ }
48
+ return f(new Date(t + offset));
49
+ };
50
+ }
3
51
  // Grain selection lives in `tickLadder.ts` (the full hour1…year ladder plus
4
52
  // the boundary-row helpers); re-exported here so existing imports keep working.
5
53
  export { coarsenCalendar } from './tickLadder.js';
6
54
  /**
7
55
  * The trivial gap-free {@link DiscontinuityProvider}: live time **is** wall
8
- * time, and every local midnight is a "session open". Backing a plain
56
+ * time, and every midnight (in `timeZone`, default runtime-local) is a
57
+ * "session open". Backing a plain
9
58
  * continuous time axis with `scaleTradingTime(identityProvider())` runs it
10
59
  * through the same logical tick ladder as a trading-calendar axis — calendar
11
60
  * days are the day anchors, so a year of data ticks on month starts and an
12
61
  * afternoon ticks on clock-aligned hours, instead of d3's mixed multi-scale
13
62
  * default.
14
63
  */
15
- export function identityProvider() {
64
+ export function identityProvider(options = {}) {
65
+ const cal = tickCalendarFor(options.timeZone);
16
66
  const self = {
17
67
  clampUp: (t) => t,
18
68
  clampDown: (t) => t,
19
69
  distance: (from, to) => to - from,
20
70
  offset: (v, amount) => v + amount,
21
71
  copy: () => self,
72
+ withTimeZone: (timeZone) => identityProvider({ timeZone }),
22
73
  boundaries: (from, to) => {
23
74
  const out = [];
24
- const d = new Date(from);
25
- // First local midnight strictly after `from`; step by calendar day (not
26
- // 24h) so DST transitions stay on midnight.
27
- let cur = new Date(d.getFullYear(), d.getMonth(), d.getDate() + 1);
28
- while (cur.getTime() < to) {
29
- if (cur.getTime() > from)
30
- out.push(cur.getTime());
31
- cur = new Date(cur.getFullYear(), cur.getMonth(), cur.getDate() + 1);
75
+ // First midnight (in the zone) strictly after `from`; step by calendar
76
+ // day (not 24h) so DST transitions stay on midnight.
77
+ let cur = cal.nextDay(from);
78
+ while (cur < to) {
79
+ if (cur > from)
80
+ out.push(cur);
81
+ cur = cal.nextDay(cur);
32
82
  }
33
83
  return out;
34
84
  },
@@ -39,11 +89,31 @@ export function identityProvider() {
39
89
  * Build a {@link TradingTimeScale} over the given discontinuity `provider`.
40
90
  * Configure like a d3 scale: `scaleTradingTime(provider).domain([t0, t1]).range([0, width])`.
41
91
  */
42
- export function scaleTradingTime(provider) {
92
+ export function scaleTradingTime(provider, options = {}) {
43
93
  let domain = [0, 1];
44
94
  let range = [0, 1];
45
- // A private d3 time scale, kept in sync with the domain, purely for tickFormat.
95
+ const zone = options.timeZone === undefined ? undefined : TimeZone.of(options.timeZone);
96
+ const cal = tickCalendarFor(zone?.id);
97
+ // A private d3 time scale purely for formatting. Local: d3's own `timeFormat`
98
+ // (and its multi-scale default) untouched. Zoned: the same specifiers on a
99
+ // civil-shifted date via `utcFormat`, and a `scaleUtc` multi-scale default
100
+ // on the shifted date so the fallback picks its unit in the zone too.
46
101
  const base = scaleTime();
102
+ const baseUtc = zone === undefined ? undefined : scaleUtc();
103
+ const fmt = (count, specifier) => {
104
+ if (zone === undefined) {
105
+ return specifier === undefined
106
+ ? base.tickFormat(count)
107
+ : base.tickFormat(count, specifier);
108
+ }
109
+ if (specifier !== undefined)
110
+ return zonedFormatter(zone, specifier);
111
+ const def = baseUtc.tickFormat(count);
112
+ return (d) => {
113
+ const t = +d;
114
+ return def(new Date(t + zone.offsetAt(t)));
115
+ };
116
+ };
47
117
  const totalLive = () => provider.distance(domain[0], domain[1]);
48
118
  const scale = ((value) => {
49
119
  const live = totalLive();
@@ -86,7 +156,7 @@ export function scaleTradingTime(provider) {
86
156
  if (laddered?.key !== key) {
87
157
  laddered = {
88
158
  key,
89
- value: buildTicks(provider, sessionOpens(), domain[1], count),
159
+ value: buildTicks(provider, sessionOpens(), domain[1], count, cal),
90
160
  };
91
161
  }
92
162
  return laddered.value;
@@ -110,8 +180,8 @@ export function scaleTradingTime(provider) {
110
180
  };
111
181
  scale.tickFormat = (count = 10, specifier) => {
112
182
  if (specifier !== undefined)
113
- return base.tickFormat(count, specifier);
114
- const defFmt = base.tickFormat(count);
183
+ return fmt(count, specifier);
184
+ const defFmt = fmt(count);
115
185
  if (!hasCalendar())
116
186
  return defFmt; // no calendar → d3 multi-scale default
117
187
  // Anchor labels at the grain {@link ticks} chose — one uniform format per
@@ -122,7 +192,7 @@ export function scaleTradingTime(provider) {
122
192
  // the dividers drawn at these instants agree.
123
193
  const { ticks, granularity } = resolved(count);
124
194
  const anchors = new Set(ticks);
125
- const anchorFmt = base.tickFormat(count, majorFormatFor(granularity));
195
+ const anchorFmt = fmt(count, majorFormatFor(granularity));
126
196
  return (d) => (anchors.has(+d) ? anchorFmt(d) : defFmt(d));
127
197
  };
128
198
  scale.tickBoundaries = (count = 10) => {
@@ -132,10 +202,10 @@ export function scaleTradingTime(provider) {
132
202
  const bg = boundaryGrainFor(granularity);
133
203
  if (bg === undefined)
134
204
  return () => undefined;
135
- const fmt = base.tickFormat(count, boundaryFormatFor(bg));
205
+ const bfmt = fmt(count, boundaryFormatFor(bg));
136
206
  const labelled = new Map();
137
- for (const t of boundaryTicks(ticks, granularity, domain[0])) {
138
- labelled.set(t, fmt(new Date(t)));
207
+ for (const t of boundaryTicks(ticks, granularity, domain[0], cal)) {
208
+ labelled.set(t, bfmt(new Date(t)));
139
209
  }
140
210
  return (value) => labelled.get(value);
141
211
  };
@@ -144,7 +214,7 @@ export function scaleTradingTime(provider) {
144
214
  // tickFormat. (The cursor readout doesn't route through here; it uses
145
215
  // readoutFormat, grain-aware.) Without a calendar there are no ladder
146
216
  // anchors, so every value falls through to the default.
147
- const defFmt = base.tickFormat(count);
217
+ const defFmt = fmt(count);
148
218
  if (!hasCalendar())
149
219
  return (value) => defFmt(new Date(value));
150
220
  const { ticks, granularity } = resolved(count);
@@ -153,13 +223,13 @@ export function scaleTradingTime(provider) {
153
223
  // opens exactly on a month's first session then promotes that tick to the
154
224
  // month (`apr 8 14 …`, not `1 8 14 …`) — the previous session was in
155
225
  // March — while a mid-session start still suppresses the false promotion.
156
- const specs = flatFormats(ticks, granularity, provider.clampDown(domain[0] - 1));
226
+ const specs = flatFormats(ticks, granularity, provider.clampDown(domain[0] - 1), cal);
157
227
  // One d3 formatter per distinct specifier; most ticks share the base one.
158
228
  const bySpec = new Map();
159
229
  const fmtFor = (spec) => {
160
230
  let f = bySpec.get(spec);
161
231
  if (f === undefined) {
162
- f = base.tickFormat(count, spec);
232
+ f = fmt(count, spec);
163
233
  bySpec.set(spec, f);
164
234
  }
165
235
  return f;
@@ -173,20 +243,20 @@ export function scaleTradingTime(provider) {
173
243
  // inline promotion (that context lives in the band row). Anchors get the
174
244
  // grain's flat base format; a non-tick instant (the cursor) reads the d3
175
245
  // multi-scale default, so the crosshair still shows a full timestamp.
176
- const defFmt = base.tickFormat(count);
246
+ const defFmt = fmt(count);
177
247
  if (!hasCalendar())
178
248
  return (value) => defFmt(new Date(value));
179
249
  const { ticks, granularity } = resolved(count);
180
250
  const anchors = new Set(ticks);
181
- const terse = base.tickFormat(count, flatBaseFormatFor(granularity));
251
+ const terse = fmt(count, flatBaseFormatFor(granularity));
182
252
  return (value) => anchors.has(value) ? terse(new Date(value)) : defFmt(new Date(value));
183
253
  };
184
254
  scale.readoutFormat = (count = 10) => {
185
- const defFmt = base.tickFormat(count);
255
+ const defFmt = fmt(count);
186
256
  if (!hasCalendar())
187
257
  return (value) => defFmt(new Date(value));
188
- const fmt = base.tickFormat(count, readoutFormatFor(resolved(count).granularity));
189
- return (value) => fmt(new Date(value));
258
+ const rfmt = fmt(count, readoutFormatFor(resolved(count).granularity));
259
+ return (value) => rfmt(new Date(value));
190
260
  };
191
261
  scale.grain = (count = 10) => hasCalendar() ? coarseUnitOf(resolved(count).granularity) : 'day';
192
262
  scale.bands = (count = 10) => {
@@ -196,7 +266,7 @@ export function scaleTradingTime(provider) {
196
266
  const bg = bandGrainFor(granularity);
197
267
  if (bg === undefined)
198
268
  return []; // year grain — nothing coarser to band
199
- const fmt = base.tickFormat(count, bandFormatFor(bg));
269
+ const bandFmt = fmt(count, bandFormatFor(bg));
200
270
  const tickSet = new Set(ticks);
201
271
  // A band's raw calendar start is a date, not necessarily a LIVE instant —
202
272
  // a month or day beginning on a collapsed weekend/holiday clamps onto the
@@ -207,13 +277,13 @@ export function scaleTradingTime(provider) {
207
277
  // can be resolved as one group rather than emitting (and then trying to
208
278
  // retract) a label per member.
209
279
  const candidates = [];
210
- let s = bandStartOf(domain[0], bg);
280
+ let s = bandStartOf(domain[0], bg, cal);
211
281
  // First band starts at (or before) the domain start — the partial left
212
282
  // band whose label the renderer pins at x=0; step to each next period
213
283
  // start still inside the domain. Bounded loop as a runaway guard.
214
284
  for (let i = 0; i < 100_000 && s < domain[1]; i++) {
215
285
  candidates.push({ s, live: provider.clampUp(s) });
216
- s = bandNext(s, bg);
286
+ s = bandNext(s, bg, cal);
217
287
  }
218
288
  const out = [];
219
289
  for (let i = 0; i < candidates.length;) {
@@ -249,9 +319,9 @@ export function scaleTradingTime(provider) {
249
319
  // Formatted from the representative's own RAW start, not the
250
320
  // clamped one — a genuinely-live rep reads its own date; a
251
321
  // gap-only run's rep reads whichever raw date it fell back to.
252
- label: fmt(new Date(rep.s)),
322
+ label: bandFmt(new Date(rep.s)),
253
323
  showLabel: !collides,
254
- shaded: bandShaded(rep.s, bg),
324
+ shaded: bandShaded(rep.s, bg, cal),
255
325
  });
256
326
  i = j;
257
327
  }
@@ -264,8 +334,7 @@ export function scaleTradingTime(provider) {
264
334
  const bg = boundaryGrainFor(granularity);
265
335
  if (bg === undefined)
266
336
  return undefined;
267
- const fmt = base.tickFormat(count, boundaryFormatFor(bg));
268
- return fmt(new Date(domain[0]));
337
+ return fmt(count, boundaryFormatFor(bg))(new Date(domain[0]));
269
338
  };
270
339
  /** Memoized like {@link resolved}: the draw pass asks once per frame, and
271
340
  * the populations only change with the domain / width / fade floor. */
@@ -282,7 +351,7 @@ export function scaleTradingTime(provider) {
282
351
  if (gridMemo?.key !== key) {
283
352
  gridMemo = {
284
353
  key,
285
- value: buildGridLevels(provider, sessionOpens(), domain[1], cap)
354
+ value: buildGridLevels(provider, sessionOpens(), domain[1], cap, cal)
286
355
  .map((l) => ({
287
356
  granularity: l.granularity,
288
357
  // The first open is the domain start itself — a window edge, not
@@ -312,7 +381,13 @@ export function scaleTradingTime(provider) {
312
381
  return scale;
313
382
  }
314
383
  scale.range = rangeFn;
315
- scale.copy = () => scaleTradingTime(provider.copy()).domain(domain).range(range);
384
+ scale.copy = () => scaleTradingTime(provider.copy(), options).domain(domain).range(range);
385
+ scale.withTimeZone = (timeZone) => scaleTradingTime(provider.withTimeZone?.(timeZone) ?? provider.copy(), {
386
+ timeZone,
387
+ })
388
+ .domain(domain)
389
+ .range(range);
390
+ scale.timeZone = () => zone?.id;
316
391
  return scale;
317
392
  }
318
393
  //# sourceMappingURL=tradingTimeScale.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pond-ts/charts",
3
- "version": "0.68.0",
3
+ "version": "0.70.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.68.0",
66
- "pond-ts": "^0.68.0",
65
+ "@pond-ts/react": "^0.70.0",
66
+ "pond-ts": "^0.70.0",
67
67
  "react": "^18.0.0 || ^19.0.0"
68
68
  },
69
69
  "devDependencies": {
@@ -73,6 +73,7 @@
73
73
  "@testing-library/react": "^16.3.2",
74
74
  "@types/d3-scale": "^4.0.9",
75
75
  "@types/d3-shape": "^3.1.8",
76
+ "@types/d3-time-format": "^4.0.3",
76
77
  "@types/react": "^19.0.0",
77
78
  "@types/react-dom": "^19.2.3",
78
79
  "happy-dom": "^20.9.0",
@@ -86,6 +87,7 @@
86
87
  },
87
88
  "dependencies": {
88
89
  "d3-scale": "^4.0.2",
89
- "d3-shape": "^3.2.0"
90
+ "d3-shape": "^3.2.0",
91
+ "d3-time-format": "^4.1.0"
90
92
  }
91
93
  }