@pond-ts/charts 0.46.0 → 0.48.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.
@@ -13,10 +13,27 @@ import type { DiscontinuityProvider } from './tradingTimeScale.js';
13
13
  * label doesn't carry (hours → the date, days/weeks → the month, months →
14
14
  * the year). The axis renders that as a second label row, once per boundary
15
15
  * crossing, so a month row reads `Dec Jan Feb …` with `2026` appearing exactly
16
- * where the year turns.
16
+ * where the year turns — the **stacked** date style.
17
+ *
18
+ * The **flat** date style (the default, the TradingView look) drops the second
19
+ * row: each tick that *opens* a coarser calendar period is relabelled **inline**
20
+ * to that period — the year at a year turn, the month at a month turn, the date
21
+ * at a day turn under an intraday grain — while every other tick keeps a terse
22
+ * base label (bare day-of-month, month abbrev, clock time). {@link flatFormats}
23
+ * computes the per-tick format specifiers for that single row.
17
24
  */
18
25
  /** The calendar grain a run of tick anchors is bucketed to. */
19
26
  export type TickGranularity = 'second1' | 'second5' | 'second15' | 'second30' | 'minute1' | 'minute5' | 'minute15' | 'minute30' | 'hour1' | 'hour3' | 'hour6' | 'hour12' | 'day' | 'week' | 'month' | 'quarter' | 'year';
27
+ /**
28
+ * The **coarse calendar unit** of a resolved grain — the public grain a
29
+ * `cursorFormat` callback branches on, with the internal 1/5/15/30 strides
30
+ * collapsed to their unit (`hour1|3|6|12` → `hour`). Stable across zoom steps
31
+ * within a unit, so a consumer's `grain === 'hour'` check doesn't churn as the
32
+ * stride changes.
33
+ */
34
+ export type TimeGrain = 'year' | 'quarter' | 'month' | 'week' | 'day' | 'hour' | 'minute' | 'second';
35
+ /** Collapse a {@link TickGranularity} to its coarse {@link TimeGrain} unit. */
36
+ export declare function coarseUnitOf(g: TickGranularity): TimeGrain;
20
37
  /**
21
38
  * The local-time bucket key for `t` at grain `g` — two instants in the same
22
39
  * day / week / month / quarter / year share a key. Local time (not UTC) so it
@@ -28,32 +45,86 @@ export type TickGranularity = 'second1' | 'second5' | 'second15' | 'second30' |
28
45
  */
29
46
  export declare function bucketKey(t: number, g: TickGranularity): number;
30
47
  /**
31
- * Thin an ascending run of **session opens** down to about `count` axis ticks by
32
- * **calendar grain** the trading-terminal habit of labelling week / month /
33
- * year starts rather than an arbitrary every-nth session. Picks the finest grain
34
- * on the ladder (day week month quarter year) that yields at most
35
- * `count` buckets and returns the first open in each; beyond yearly it decimates
36
- * every-nth so the axis never crowds. Exported so the container can draw session
37
- * dividers at the same instants the axis labels.
48
+ * Thin an ascending run of **session opens** down to about `count` axis ticks.
49
+ * Picks the finest rung: every session **per-month uniform session stride**
50
+ * (still day grain, month starts pinned) month quarter year; beyond
51
+ * yearly it decimates every-nth so the axis never crowds. Exported so the
52
+ * container can draw session dividers at the same instants the axis labels.
38
53
  *
39
- * `count` is a **cap**, not a target: grains jump by 4–12× up the ladder, so a
40
- * small fixed count over-coarsens long spans (a mid-year-anchored 12-month daily
41
- * run spans 6 quarter buckets capped at 5 it collapses to year grain, 2
42
- * ticks). Callers size the cap to the room the labels have — the container
43
- * derives it from plot width rather than passing a small constant.
54
+ * The day band thins each month to a **uniform session stride** the month's
55
+ * first session, then every `k`-th session, truncated so the gap to the next
56
+ * month start stays `k` (slack at the month end, never a cramped tick
57
+ * before the month label). The decoded-and-validated TradingView algorithm:
58
+ * with a `provider` the stride runs in **session-index space**
59
+ * ({@link subdivideMonthsBySession}) — marks an equal number of bars apart,
60
+ * evenly spaced pixels on a collapsed axis, no weekend snapping; without one
61
+ * it falls back to day-of-month space ({@link subdivideMonthsByDay}).
62
+ * Zooming steps the stride through the integers (…4 → 3 → 2 → 1), a ~one-bar
63
+ * density change that re-labels some interior marks; month / year starts stay
64
+ * pinned at every zoom, and pans never reshuffle anything (the stride derives
65
+ * from the span, the indices from the calendar). Schemes tried and rejected
66
+ * on the way here: a global even day-stride (can't pin month starts — the
67
+ * `Feb` label drifted with zoom), `round(i·L/div)` division (beats against
68
+ * the month length), and dyadic midpoint halving (perfect zoom-nesting, but
69
+ * 2× density jumps and ±1-day wobble inside non-power months; the owner's
70
+ * TradingView captures showed uniform strides re-labelling on zoom reads
71
+ * calmer than either wobble). There is deliberately no week rung: a
72
+ * Monday-anchored week can't pin month starts either, so the day band owns
73
+ * everything between every-session and month grain.
74
+ *
75
+ * `count` is a **cap**, not a target: coarser grains jump by 3–4× (month →
76
+ * quarter → year), so a small fixed count over-coarsens long spans. Callers
77
+ * size the cap to the room the labels have — the container derives it from plot
78
+ * width — rather than passing a small constant. `spanDays` is the domain's
79
+ * calendar-day span from the caller (stable at a fixed zoom); absent (a direct
80
+ * call), it falls back to the opens' own span.
44
81
  *
45
82
  * This is the day-and-coarser half of the ladder; {@link buildTicks} adds the
46
83
  * sub-day rungs.
47
84
  */
48
- export declare function coarsenCalendar(opens: readonly number[], count: number): {
85
+ export declare function coarsenCalendar(opens: readonly number[], count: number, spanDays?: number, provider?: DiscontinuityProvider): {
49
86
  ticks: number[];
50
87
  granularity: TickGranularity;
51
88
  };
89
+ /**
90
+ * The **grid populations** behind {@link buildTicks}' labels: every ladder rung
91
+ * that fits `cap` lines, each carrying its FULL anchor population — every
92
+ * aligned clock instant, every session open, every month / quarter / year
93
+ * start — finest rung first. The axis *labels* are a thinned subset of one
94
+ * rung; the grid is the calendar structure itself, so the container draws
95
+ * every anchor of each returned level and fades a level's lines by their pixel
96
+ * spacing (a crowding level dissolves while the coarser ones persist — the
97
+ * map-style hierarchical grid). Levels **nest** (a month start is a session
98
+ * open; an aligned hour sits inside its session; there is no week rung), so a
99
+ * consumer de-duplicates shared anchors coarsest-first and each line draws
100
+ * once, at its coarsest membership's (widest-spaced, so strongest) alpha.
101
+ *
102
+ * `cap` is the max lines per level — the caller derives it from plot width ÷
103
+ * the fade-out spacing, so a level too dense to be visible at all is simply
104
+ * absent rather than enumerated and thrown away. Sub-day rungs are gated on
105
+ * the live-span estimate first (like {@link buildTicks}) and skipped when they
106
+ * add no anchor beyond the session opens themselves (that is the day level).
107
+ */
108
+ export declare function buildGridLevels(provider: DiscontinuityProvider, opens: readonly number[], domainEnd: number, cap: number): Array<{
109
+ granularity: TickGranularity;
110
+ values: number[];
111
+ }>;
112
+ /**
113
+ * The **nominal wall-clock step** of grain `g` in ms — the calendar time one
114
+ * grid cell of that grain covers (a day is a day whether or not its weekend
115
+ * neighbours are drawn; a month is ~30.44 days). This is what the grid's
116
+ * density fade keys off: `width × step / wallSpan` is a grain's spacing on a
117
+ * gap-free axis, and using it (rather than the measured on-screen gaps) makes
118
+ * the fade **mode-invariant** — collapsing weekends draws fewer day lines at
119
+ * the *same* strength, instead of wider-spaced lines that jump to full
120
+ * opacity at the same zoom.
121
+ */
122
+ export declare function nominalStepMs(g: TickGranularity): number;
52
123
  /**
53
124
  * The full-ladder grain selection: given the provider, the domain, and the
54
125
  * width-derived `cap`, walk the clock rungs (1s … 30s, 1m … 30m, 1h … 12h)
55
- * then day week → month → quarter → year (then decimate) and return the
56
- * first rung that fits.
126
+ * then day (thinned by a per-month uniform session stride) → month → quarter → year
127
+ * (then decimate) and return the first rung that fits.
57
128
  * `opens` are the session-open anchors (`[domain start, ...boundaries]`) the
58
129
  * caller already has. Sub-day rungs are only reachable when the opens
59
130
  * themselves fit — a year of daily sessions never wastes time generating hour
@@ -78,6 +149,46 @@ export declare function majorFormatFor(g: TickGranularity): string;
78
149
  * everything else. Never repeat a unit the first row already shows
79
150
  * (`Jan 2026` under a `Jan 05` tick reads as noise). */
80
151
  export declare function boundaryFormatFor(g: TickGranularity): string;
152
+ /** The **band grain** under `g`-grain ticks (the segmented stacked second row):
153
+ * the next coarser unit — sub-day → day, day/week → month, month/quarter →
154
+ * year, year → none. */
155
+ export declare function bandGrainFor(g: TickGranularity): TickGranularity | undefined;
156
+ /**
157
+ * d3 specifier for the **cursor / marker readout** at grain `g` — a hovered
158
+ * instant formatted at the axis's own granularity, never finer. A day-or-coarser
159
+ * axis reads a **date** (no time-of-day), so a daily bar at a foreign-tz
160
+ * midnight can't render as `02 AM`; a sub-day axis reads date **+** clock. This
161
+ * is the grain-aware default that replaces d3's multi-scale default for the
162
+ * readout (a `cursorFormat` override, when given, wins over it). Unambiguous by
163
+ * design — the readout carries the year / date the terse tick labels omit.
164
+ */
165
+ export declare function readoutFormatFor(g: TickGranularity): string;
166
+ /** d3 specifier for a **band** label at band grain `g`: the date for a day band
167
+ * (`Jan 12`), the full month for a month band (`January`), the year for a year
168
+ * band (`2031`). Left-aligned in the band by the renderer. */
169
+ export declare function bandFormatFor(g: TickGranularity): string;
170
+ /**
171
+ * The **zebra parity** of the band starting at `t` (band grain `g`) — `true`
172
+ * when the band is shaded. A stable, pan/zoom-invariant flag derived from the
173
+ * band's own calendar identity: the year number, the months-since-epoch, or
174
+ * the **UTC**-day index (UTC so a DST shift never flips a band's shade). Odd
175
+ * index → shaded, matching the reference frames (2031 / 2033 grey).
176
+ *
177
+ * Parity is **absolute** (per calendar period), not per-visible-position — the
178
+ * price of pan-stability. Calendar-consecutive bands always differ, and
179
+ * collapsed **weekends** stay clean (Fri→Mon is a 3-index step, odd), but a
180
+ * lone skipped weekday — a single **holiday**, a 2-index step — can place two
181
+ * same-shade day-bands side by side on a gappy calendar. A rare, cosmetic
182
+ * consequence of keeping the shade fixed to the date rather than the slot.
183
+ */
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;
189
+ /** 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;
81
192
  /**
82
193
  * Which of `ticks` (at grain `granularity`) carry a boundary label: every tick
83
194
  * whose boundary-grain bucket differs from the previous tick's — i.e. a
@@ -89,4 +200,33 @@ export declare function boundaryFormatFor(g: TickGranularity): string;
89
200
  * boundary row (year grain).
90
201
  */
91
202
  export declare function boundaryTicks(ticks: readonly number[], granularity: TickGranularity, domainStart?: number): number[];
203
+ /**
204
+ * The terse **base** (non-promoted) flat label format for grain `g` — the label
205
+ * a tick carries when it opens no coarser period: the clock time for a sub-day
206
+ * grain, a bare day-of-month for day / week (`5`, not `Jan 5` — the month rides
207
+ * the promoted month-start tick), the month abbrev for month / quarter, the
208
+ * year for year. Terser than {@link majorFormatFor} (which carries the month on
209
+ * every day tick) because the flat row leans on inline promotions for context.
210
+ */
211
+ export declare function flatBaseFormatFor(g: TickGranularity): string;
212
+ /**
213
+ * The **flat** (single-row) label format specifier for each tick, parallel to
214
+ * `ticks` (already at grain `granularity`). Each tick shows the coarsest
215
+ * calendar period it *opens* — a year / month / date promotion — and its terse
216
+ * {@link flatBaseFormatFor} label otherwise, so the one row reads
217
+ * `… 30 31 Feb 2 3 …` with `Feb` where the month turns and the year where it
218
+ * turns. A tick "opens" level L when its L-bucket differs from the previous
219
+ * tick's; the coarsest changed level wins (a Jan-1 tick promotes to the year,
220
+ * not the month).
221
+ *
222
+ * `domainStart` seeds the walk (like {@link boundaryTicks}): the first tick is
223
+ * promoted only if it crosses a period relative to the instant just *before*
224
+ * the domain's left edge — so a window opening mid-month doesn't falsely
225
+ * promote its first tick (and a live sliding window doesn't flicker the
226
+ * leftmost label), while a domain starting *exactly* on a boundary still
227
+ * promotes the boundary tick (it truly is the period's first instant: a window
228
+ * opening at May 1 midnight reads `May 16 …`, not `1 16 …`). Without
229
+ * `domainStart` the first tick is never promoted.
230
+ */
231
+ export declare function flatFormats(ticks: readonly number[], granularity: TickGranularity, domainStart?: number): string[];
92
232
  //# sourceMappingURL=tickLadder.d.ts.map