@pond-ts/charts 0.68.0 → 0.69.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/dist/XAxis.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Fragment, useContext, useRef } from 'react';
2
+ import { Fragment, useContext, useMemo, useRef } from 'react';
3
3
  import { scaleLinear } from 'd3-scale';
4
+ import { TimeZone } from 'pond-ts';
4
5
  import { derivedTicks } from './derivedTicks.js';
5
6
  import { ContainerContext, CursorContext, } from './context.js';
6
7
  import { tickValues } from './yticks.js';
@@ -166,7 +167,7 @@ export function thinCategoryLabels(ticks, slot, plotWidth, fontSize, fontFamily)
166
167
  * plot's own drag, including `bounds` / `minDuration` and the trading calendar.
167
168
  * A category axis has no continuous domain and stays inert.
168
169
  */
169
- export function XAxis({ format, label, side = 'bottom', height, ticks: customTicks, transform, color, align = 'center', dateStyle = 'flat', onMouseEvent, } = {}) {
170
+ export function XAxis({ format, label, side = 'bottom', height, ticks: customTicks, transform, color, align = 'center', dateStyle = 'flat', timeZone, onMouseEvent, } = {}) {
170
171
  const container = useContext(ContainerContext);
171
172
  if (container === null) {
172
173
  throw new Error('<XAxis> must be rendered inside a <ChartContainer>');
@@ -175,7 +176,38 @@ export function XAxis({ format, label, side = 'bottom', height, ticks: customTic
175
176
  // `xTickCount` is the container's shared x-side count — the same value the x
176
177
  // gridlines and `formatTime` use, so labels and grid stay on the same instants
177
178
  // (width-derived on a trading-time axis).
178
- const { xScale, plotWidth, leftGutter, theme, formatTime, xKind, xTickCount, } = container;
179
+ const { xScale: containerScale, plotWidth, leftGutter, theme, formatTime: containerFormatTime, xKind, xTickCount, } = container;
180
+ // A per-strip zone: the container's shared scale re-derived with its
181
+ // calendar in `timeZone` — identical pixel mapping, so every `xScale(v)`
182
+ // below lands where the plot puts it, but ticks / labels / bands / this
183
+ // strip's pills come from this zone's ladder. Canonicalised (and validated)
184
+ // so an unknown id throws by name and `'utc'` / `'UTC'` memoize as one.
185
+ const zonedScale = useMemo(() => {
186
+ if (timeZone === undefined ||
187
+ xKind !== 'time' ||
188
+ !('withTimeZone' in containerScale)) {
189
+ return undefined;
190
+ }
191
+ const id = TimeZone.of(timeZone).id;
192
+ const s = containerScale;
193
+ return s.timeZone() === id ? undefined : s.withTimeZone(id);
194
+ }, [containerScale, timeZone, xKind]);
195
+ const xScale = zonedScale ?? containerScale;
196
+ // The label formatter this strip falls back to when no explicit `format`
197
+ // shapes it. The container's `formatTime` was resolved in the container's
198
+ // zone; a zoned strip needs the same channel in its own — the grain-aware
199
+ // default, or the container's `timeFormat` specifier re-resolved (a
200
+ // function `timeFormat` receives epoch ms and is used verbatim either way).
201
+ const formatTime = useMemo(() => {
202
+ if (zonedScale === undefined)
203
+ return containerFormatTime;
204
+ const custom = container.timeFormat;
205
+ if (typeof custom === 'function')
206
+ return custom;
207
+ return custom === undefined
208
+ ? zonedScale.readoutFormat(xTickCount)
209
+ : resolveTimeFormat(zonedScale, xTickCount, custom);
210
+ }, [zonedScale, containerFormatTime, container.timeFormat, xTickCount]);
179
211
  // The cursor's x-axis slot: did the mounted cursor in effect register one
180
212
  // (`renderXAxis` — the crosshair's time pill)? While hovering, that is the
181
213
  // **hovered row's** effective cursor — so a per-row override reaches this
package/dist/context.d.ts CHANGED
@@ -270,6 +270,20 @@ export interface ContainerFrame {
270
270
  * tick labels) without moving them.
271
271
  */
272
272
  readonly formatReadout?: ((value: number) => string) | undefined;
273
+ /**
274
+ * The IANA zone the time axis renders in — the container's resolved
275
+ * `timeZone` (explicit prop, else the calendar's), canonical id; `undefined`
276
+ * when the axis is in the runtime's local zone. For a consumer's own
277
+ * formatter (`timeFormat` / `cursorFormat` functions receive epoch ms) to
278
+ * read the same zone the ticks do.
279
+ */
280
+ readonly timeZone: string | undefined;
281
+ /**
282
+ * The container's raw `timeFormat` prop, for a strip that must re-resolve it
283
+ * in another zone (`<XAxis timeZone>`): a specifier string is re-resolved
284
+ * against that strip's zoned scale, a function is used verbatim.
285
+ */
286
+ readonly timeFormat: AxisFormat | undefined;
273
287
  /** Whether an explicit container `timeFormat` shaped {@link formatTime}. The
274
288
  * x axis suppresses its boundary (second) label row when it's set — a
275
289
  * custom format owns the whole label, so the ladder mustn't second-line it. */
package/dist/index.d.ts CHANGED
@@ -68,8 +68,8 @@ export type { ChartLegend, LegendRow, LegendItem } from './useChartLegend.js';
68
68
  export { useChartFrame } from './useChartFrame.js';
69
69
  export type { ChartFrame, ChartFrameRow, ChartBands, ChartBand, } from './useChartFrame.js';
70
70
  export type { ChartXScale } from './context.js';
71
- export { scaleTradingTime } from './tradingTimeScale.js';
72
- export type { TradingTimeScale, DiscontinuityProvider, TimeGrain, } from './tradingTimeScale.js';
71
+ export { scaleTradingTime, identityProvider } from './tradingTimeScale.js';
72
+ export type { TradingTimeScale, DiscontinuityProvider, TradingCalendarLike, ScaleTimeZoneOptions, TimeGrain, } from './tradingTimeScale.js';
73
73
  export { scaleBand } from './bandScale.js';
74
74
  export type { ScaleBand } from './bandScale.js';
75
75
  export { Region, Baseline, Marker, Zone } from './annotations.js';
package/dist/index.js CHANGED
@@ -58,7 +58,7 @@ export { useChartLegend } from './useChartLegend.js';
58
58
  // by mirroring the library's own gutter arithmetic — a duplicate that drifts
59
59
  // silently the moment the library changes how a gutter is sized.
60
60
  export { useChartFrame } from './useChartFrame.js';
61
- export { scaleTradingTime } from './tradingTimeScale.js';
61
+ export { scaleTradingTime, identityProvider } from './tradingTimeScale.js';
62
62
  // The ordinal category (band) scale — the transpose view's "columns on x" axis.
63
63
  export { scaleBand } from './bandScale.js';
64
64
  // Annotations — user-authored marks in the turquoise register (distinct from the
@@ -1,3 +1,4 @@
1
+ import { TimeZone } from 'pond-ts';
1
2
  import type { DiscontinuityProvider } from './tradingTimeScale.js';
2
3
  /**
3
4
  * The logical tick ladder — grain selection for a time axis. Ticks sit on real
@@ -34,16 +35,62 @@ export type TickGranularity = 'second1' | 'second5' | 'second15' | 'second30' |
34
35
  export type TimeGrain = 'year' | 'quarter' | 'month' | 'week' | 'day' | 'hour' | 'minute' | 'second';
35
36
  /** Collapse a {@link TickGranularity} to its coarse {@link TimeGrain} unit. */
36
37
  export declare function coarseUnitOf(g: TickGranularity): TimeGrain;
38
+ /** The calendar operations the tick ladder needs. Months are `1…12`. */
39
+ export interface TickCalendar {
40
+ /** Midnight of the day containing `t`. */
41
+ startOfDay(t: number): number;
42
+ /** Midnight of the day after the one containing `t`. */
43
+ nextDay(t: number): number;
44
+ /** Midnight of the Monday of the week containing `t`. */
45
+ startOfWeek(t: number): number;
46
+ /** The civil date of `t`. */
47
+ parts(t: number): {
48
+ year: number;
49
+ month: number;
50
+ day: number;
51
+ };
52
+ /** Midnight of the first of `month` (`1…12`; `13` carries into the next year, `0` into the previous). */
53
+ monthStart(year: number, month: number): number;
54
+ /** Days in `month` of `year`. */
55
+ daysInMonth(year: number, month: number): number;
56
+ /**
57
+ * The first clock-aligned `stepMs` multiple at or after `t`, counted from
58
+ * `t`'s own midnight — 00:00 / 03:00 / 06:00 … for a 3 h step. The local
59
+ * calendar steps **fixed milliseconds** from midnight (so on a DST day the
60
+ * later anchors drift off the wall clock by the shift, until the next
61
+ * midnight re-anchors — the pre-seam behaviour, kept); a zoned calendar
62
+ * aligns to the **wall clock**, so 6 h anchors read 00 / 06 / 12 / 18 on
63
+ * both sides of the jump, with one short or long gap at the transition.
64
+ * Always `>= t`.
65
+ */
66
+ nextAligned(t: number, stepMs: number): number;
67
+ /**
68
+ * The anchor after an aligned anchor `t` at `stepMs`. The local calendar
69
+ * steps `t + stepMs` — fixed elapsed milliseconds, the pre-seam loop
70
+ * verbatim, which is what keeps a session that spans a DST midnight (a
71
+ * futures 18:00 → 17:00 session) ticking exactly as it did. A zoned
72
+ * calendar re-aligns through {@link nextAligned} so the anchors stay on
73
+ * the wall clock across the jump.
74
+ */
75
+ nextAnchor(t: number, stepMs: number): number;
76
+ }
77
+ /** The runtime-local calendar — `Date`'s local accessors, exactly as the
78
+ * ladder computed before it had a zone. The default {@link TickCalendar}. */
79
+ export declare const localTickCalendar: TickCalendar;
80
+ /** A {@link TickCalendar} for an IANA zone, on core's `TimeZone`. */
81
+ export declare function zonedTickCalendar(zone: TimeZone): TickCalendar;
82
+ /** Resolve an optional IANA id to the calendar the ladder should use: the
83
+ * runtime-local calendar when `timeZone` is undefined, else the zone's. */
84
+ export declare function tickCalendarFor(timeZone: string | undefined): TickCalendar;
37
85
  /**
38
- * The local-time bucket key for `t` at grain `g` — two instants in the same
39
- * day / week / month / quarter / year share a key. Local time (not UTC) so it
40
- * agrees with the local `scaleTime` label formatter; the exchange's own time
41
- * zone is unknown to the scale (the deferred refinement), and a session open
42
- * sits well inside its local day, so runtime-local grouping matches the
43
- * exchange day in every ordinary case. Hour grains are never bucketed (each
44
- * anchor is its own tick), so they key by identity.
86
+ * The calendar bucket key for `t` at grain `g` — two instants in the same
87
+ * day / week / month / quarter / year share a key. Computed in `cal`'s zone
88
+ * (runtime-local by default) so it agrees with the label formatter for the
89
+ * same zone; a trading axis passes its exchange zone so the grain buckets by
90
+ * the exchange day rather than the viewer's. Hour grains are never bucketed
91
+ * (each anchor is its own tick), so they key by identity.
45
92
  */
46
- export declare function bucketKey(t: number, g: TickGranularity): number;
93
+ export declare function bucketKey(t: number, g: TickGranularity, cal?: TickCalendar): number;
47
94
  /**
48
95
  * Thin an ascending run of **session opens** down to about `count` axis ticks.
49
96
  * Picks the finest rung: every session → **per-month uniform session stride**
@@ -82,7 +129,7 @@ export declare function bucketKey(t: number, g: TickGranularity): number;
82
129
  * This is the day-and-coarser half of the ladder; {@link buildTicks} adds the
83
130
  * sub-day rungs.
84
131
  */
85
- export declare function coarsenCalendar(opens: readonly number[], count: number, spanDays?: number, provider?: DiscontinuityProvider): {
132
+ export declare function coarsenCalendar(opens: readonly number[], count: number, spanDays?: number, provider?: DiscontinuityProvider, cal?: TickCalendar): {
86
133
  ticks: number[];
87
134
  granularity: TickGranularity;
88
135
  };
@@ -105,7 +152,7 @@ export declare function coarsenCalendar(opens: readonly number[], count: number,
105
152
  * the live-span estimate first (like {@link buildTicks}) and skipped when they
106
153
  * add no anchor beyond the session opens themselves (that is the day level).
107
154
  */
108
- export declare function buildGridLevels(provider: DiscontinuityProvider, opens: readonly number[], domainEnd: number, cap: number): Array<{
155
+ export declare function buildGridLevels(provider: DiscontinuityProvider, opens: readonly number[], domainEnd: number, cap: number, cal?: TickCalendar): Array<{
109
156
  granularity: TickGranularity;
110
157
  values: number[];
111
158
  }>;
@@ -130,7 +177,7 @@ export declare function nominalStepMs(g: TickGranularity): number;
130
177
  * themselves fit — a year of daily sessions never wastes time generating hour
131
178
  * anchors.
132
179
  */
133
- export declare function buildTicks(provider: DiscontinuityProvider, opens: readonly number[], domainEnd: number, cap: number): {
180
+ export declare function buildTicks(provider: DiscontinuityProvider, opens: readonly number[], domainEnd: number, cap: number, cal?: TickCalendar): {
134
181
  ticks: number[];
135
182
  granularity: TickGranularity;
136
183
  };
@@ -181,14 +228,13 @@ export declare function bandFormatFor(g: TickGranularity): string;
181
228
  * same-shade day-bands side by side on a gappy calendar. A rare, cosmetic
182
229
  * consequence of keeping the shade fixed to the date rather than the slot.
183
230
  */
184
- export declare function bandShaded(t: number, g: TickGranularity): boolean;
185
- /** The local-time start of the band grain `g` containing `t` (the band's left
186
- * edge): local midnight, month start, or Jan 1. Through the Date ctor so DST
187
- * and month/year overflow normalize correctly. */
188
- export declare function bandStartOf(t: number, g: TickGranularity): number;
231
+ export declare function bandShaded(t: number, g: TickGranularity, cal?: TickCalendar): boolean;
232
+ /** The start of the band grain `g` containing `t` (the band's left edge) in
233
+ * `cal`'s zone: midnight, month start, or Jan 1. */
234
+ export declare function bandStartOf(t: number, g: TickGranularity, cal?: TickCalendar): number;
189
235
  /** The start of the band grain `g` **after** the one containing `t` — the next
190
- * local midnight / month start / Jan 1. */
191
- export declare function bandNext(t: number, g: TickGranularity): number;
236
+ * midnight / month start / Jan 1 in `cal`'s zone. */
237
+ export declare function bandNext(t: number, g: TickGranularity, cal?: TickCalendar): number;
192
238
  /**
193
239
  * Which of `ticks` (at grain `granularity`) carry a boundary label: every tick
194
240
  * whose boundary-grain bucket differs from the previous tick's — i.e. a
@@ -199,7 +245,7 @@ export declare function bandNext(t: number, g: TickGranularity): number;
199
245
  * tick-to-tick on a live sliding window). Empty when the grain has no
200
246
  * boundary row (year grain).
201
247
  */
202
- export declare function boundaryTicks(ticks: readonly number[], granularity: TickGranularity, domainStart?: number): number[];
248
+ export declare function boundaryTicks(ticks: readonly number[], granularity: TickGranularity, domainStart?: number, cal?: TickCalendar): number[];
203
249
  /**
204
250
  * The terse **base** (non-promoted) flat label format for grain `g` — the label
205
251
  * a tick carries when it opens no coarser period: the clock time for a sub-day
@@ -228,5 +274,5 @@ export declare function flatBaseFormatFor(g: TickGranularity): string;
228
274
  * opening at May 1 midnight reads `May 16 …`, not `1 16 …`). Without
229
275
  * `domainStart` the first tick is never promoted.
230
276
  */
231
- export declare function flatFormats(ticks: readonly number[], granularity: TickGranularity, domainStart?: number): string[];
277
+ export declare function flatFormats(ticks: readonly number[], granularity: TickGranularity, domainStart?: number, cal?: TickCalendar): string[];
232
278
  //# sourceMappingURL=tickLadder.d.ts.map