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