@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.
package/CHANGELOG.md CHANGED
@@ -8,7 +8,8 @@ The `@pond-ts` packages — `pond-ts`, `@pond-ts/react`, `@pond-ts/charts`,
8
8
  tag, so this file covers them all. Pre-1.0: minor bumps may include new features
9
9
  and type-level changes; patch bumps are strictly additive.
10
10
 
11
- [Unreleased]: https://github.com/pjm17971/pond-ts/compare/v0.46.0...HEAD
11
+ [Unreleased]: https://github.com/pond-ts/pond/compare/v0.47.0...HEAD
12
+ [0.47.0]: https://github.com/pond-ts/pond/compare/v0.46.0...v0.47.0
12
13
  [0.46.0]: https://github.com/pjm17971/pond-ts/compare/v0.45.0...v0.46.0
13
14
  [0.45.0]: https://github.com/pjm17971/pond-ts/compare/v0.44.1...v0.45.0
14
15
  [0.44.1]: https://github.com/pjm17971/pond-ts/compare/v0.44.0...v0.44.1
@@ -45,13 +46,110 @@ and type-level changes; patch bumps are strictly additive.
45
46
 
46
47
  ## [Unreleased]
47
48
 
49
+ ## [0.47.0] — 2026-07-17
50
+
51
+ ### Added
52
+
53
+ - **charts:** `<ChartContainer>` gains **`grid`** (default `true`) and
54
+ **`sessionDividers`** (`'labeled' | 'all' | 'none'`, default `'none'`).
55
+ On a calendar (time) axis the vertical gridlines are the **full grain
56
+ populations** — every day / month / aligned clock instant in view, not just
57
+ the thinned instants the labels chose (the labels decorate the grid; they
58
+ don't define it). Each grain fades as a unit by its **calendar density**
59
+ (nominal gap-free spacing: full ≥ 15 px, gone ≤ 5 px, quadratic), so
60
+ zooming out dissolves the fine grain into the coarser one — a map-style
61
+ hierarchical grid with no pop when the label algorithm switches rung — and
62
+ collapsing weekends draws _fewer_ day lines at the _same_ strength, not
63
+ wider-spaced lines that jump to full opacity. Session dividers are opt-in
64
+ emphasis over that grid, and draw only at true collapse **seams** —
65
+ boundaries that removed time actually precedes, not every session roll
66
+ (`'all'` = every seam, the TradingView separator look, crowding lines
67
+ fading out; `'labeled'` = only seams the axis also labels) — so a
68
+ `grid={false}` plot is actually clean by default and a contiguous-sessions
69
+ calendar seams only where days were excised. `TradingTimeScale` gains
70
+ **`gridLevels(minGapPx)`** (the nested per-grain populations + nominal
71
+ spacing behind it). In `'all'` mode, crowding session-divider lines fade on
72
+ a quadratic ramp (full ≥ 28 px apart, gone ≤ 6 px) so zooming out dissolves
73
+ them to a clean plot rather than a permanent gray wash.
74
+ - **charts:** `<TimeAxis>` / `<XAxis>` gain a **`dateStyle`** prop
75
+ (`'flat' | 'stacked'`). `'flat'` — the new default — lays the date context
76
+ out on a **single row** the TradingView way: each tick that opens a coarser
77
+ calendar period is relabelled **inline** to it (the month at a month turn,
78
+ the year at a year turn, the date at a day turn under an intraday grain),
79
+ every other tick a terse label (`5`, `Feb`, `14:00`). `'stacked'` is the
80
+ two-row layout: a terse top row (the grain's bare unit) over a segmented
81
+ **band** row of the next-coarser period — **day** bands under intraday
82
+ ticks, **month** bands under day ticks, **year** bands under month/quarter
83
+ ticks — each a left-aligned label with a divider at its turn, **zebra-shaded**
84
+ by the band's calendar parity (pan/zoom-stable). The turn tick up top is
85
+ emphasized and joins its divider as one continuous boundary line; the partial
86
+ left band pins its label at the plot edge. Themeable via **`theme.axis.band`**
87
+ (`fill` — the shaded cell, "could be a background color"; `divider`; `label`).
88
+ `TradingTimeScale` gains `flatFormat(count)` (the flat single-row lookup),
89
+ `baseFormat(count)` (the terse stacked top row), and `bands(count)` (the band
90
+ segments); non-tick instants — the cursor readout — still read a full
91
+ timestamp.
92
+ The **window edge is never ticked** unless it is a genuine grain anchor (a
93
+ true session open, or an instant exactly on the grain — a midnight at day
94
+ grain, a month start at month grain): a mid-period edge tick sat pinned at
95
+ x=0 relabelling itself as the window panned (a sticky `8` at half-spacing, a
96
+ `15:23` under an hour grain) — misleading, and not something TradingView
97
+ draws. The first real calendar anchor now leads the axis.
98
+
99
+ ### Changed
100
+
101
+ - **Docs hosting.** The documentation site has moved from GitHub Pages
102
+ (`pjm17971.github.io/pond-ts`) to Cloudflare Pages under the custom domain
103
+ **<https://pond-ts.org>**. Storybook now deploys as its own project served at
104
+ `pond-ts.org/storybook/` (via a small routing Worker, `workers/router/`),
105
+ rather than nested into the docs build. Doc links in the README are repointed;
106
+ the deploy workflow keeps the same `v*`-tag gate. See `MIGRATION.md`.
107
+ - **charts:** the time axis now renders the **flat** single-row date style by
108
+ default (previously the two-row stacked layout). Every time chart — trading
109
+ or plain continuous, auto-rendered or explicit `<TimeAxis>` — picks this up.
110
+ Pass `dateStyle="stacked"` to keep the old two-row look. (An explicit
111
+ `format` / container `timeFormat`, a `transform`, or explicit `ticks` opt out
112
+ of both styles as before.)
113
+ - **charts:** the time-axis tick ladder's sub-month density now thins each
114
+ month in **session-index space** — the reverse-engineered TradingView
115
+ algorithm, validated label-for-label against its output on the 2026 NYSE
116
+ calendar. Marks sit an equal number of _bars_ apart, so on a collapsed
117
+ trading axis they are **evenly spaced in pixels**, with no weekend snapping:
118
+ a month opening on a weekend anchors on its first session. Two regimes per
119
+ month: at dense zooms, a **uniform anchored stride** (the month's first
120
+ session, then every k-th, truncated so the gap to the next month label
121
+ stays ≥ the stride — the slack lands at the month end); at coarse zooms
122
+ (≤ 3 marks per month, where a truncated stride would leave a hole of up to
123
+ ~2 strides before the month label and each ±1 stride change would crawl the
124
+ labels), a **balanced division** — mid-month, then thirds — whose marks
125
+ hold still across every zoom that maps to the same density. Month / year
126
+ starts stay pinned at every zoom; pans never reshuffle marks (indices are
127
+ queried per full calendar month, never per window); a stub live-edge month
128
+ earns proportionally fewer marks. Without a calendar provider the same
129
+ scheme runs in day-of-month space. Replaces the old day → weekly ~7×
130
+ density cliff; the separate Monday-anchored week grain was removed — the
131
+ session-stride band covers everything between every-session and month
132
+ grain.
133
+
134
+ ### Fixed
135
+
136
+ - **charts:** a multi-session time axis near the tick cap can no longer come
137
+ back lopsided — with several sessions each gaining a phase tick while the
138
+ span estimate sat within a few ticks of the cap, clock-rung enumeration hit
139
+ its internal bail and the truncated tick array was returned, leaving the
140
+ final sessions with no ticks at all. The enumeration budget now covers the
141
+ estimate's per-session undercount (`cap + opens.length + slack`) so
142
+ enumeration completes, and a still-truncated result is rejected in favor of
143
+ a coarser rung instead of being returned. (Follow-up to the v0.46.0
144
+ sliding-window grain-stability fix, flagged in its release spot-check.)
145
+
48
146
  ## [0.46.0] — 2026-07-14
49
147
 
50
148
  ### Changed
51
149
 
52
150
  - **charts:** the boundary (second-row) axis label's **context** now pins to
53
151
  the plot's left edge instead of riding the first tick: it shows the period
54
- the *domain start* is in, and a crossing label sliding toward the edge
152
+ the _domain start_ is in, and a crossing label sliding toward the edge
55
153
  pushes it off (the sticky-header behavior). On a live sliding window the
56
154
  old first-tick anchoring made `Jan 01` hop tick-to-tick as ticks scrolled
57
155
  out; pinned, it stays put until the period actually changes. Crossing
@@ -141,7 +239,7 @@ and type-level changes; patch bumps are strictly additive.
141
239
  values while **keeping the row count** — mirroring `rolling`'s `minSamples`,
142
240
  the one warm-up convention studies use so a smoothed line aligns on its
143
241
  source's time axis. (The existing `warmup` option is unchanged — it still
144
- *drops* the head rows; `minSamples` is the length-preserving counterpart.)
242
+ _drops_ the head rows; `minSamples` is the length-preserving counterpart.)
145
243
  - **core:** `TimeSeries.rolling` accepts a **count-based** window —
146
244
  `rolling({ count: N }, mapping, opts?)` reduces the last / next / centered
147
245
  `N` _rows_ (bars) by position instead of a time span. Unlike a duration
@@ -29,8 +29,11 @@ export interface ChartContainerProps {
29
29
  * this prop's identity changes, so memoize it — `const disc = useMemo(() =>
30
30
  * calendar.discontinuities(), [calendar])` — rather than calling
31
31
  * `.discontinuities()` inline in JSX, which would rebuild every render.
32
+ *
33
+ * Accepts an explicit `undefined` (a `cond ? provider : undefined` toggle
34
+ * under `exactOptionalPropertyTypes`), same as omitting it.
32
35
  */
33
- discontinuities?: DiscontinuityProvider;
36
+ discontinuities?: DiscontinuityProvider | undefined;
34
37
  /**
35
38
  * The **high-level** sugar for {@link discontinuities}: a trading calendar the
36
39
  * container derives the provider from itself (`calendar.discontinuities({
@@ -53,6 +56,31 @@ export interface ChartContainerProps {
53
56
  * carries its own metric).
54
57
  */
55
58
  spacing?: 'proportional' | 'uniform';
59
+ /**
60
+ * Draw the reference gridlines behind the data. On a calendar (time) axis
61
+ * the verticals are the **full grain populations** — every day / month /
62
+ * aligned clock instant in view, each grain fading by its calendar density
63
+ * — not just the labelled ticks (the labels decorate the grid; they don't
64
+ * define it). **Default `true`.** Set `false` for a clean backdrop —
65
+ * session dividers (below) are independent and still draw when enabled.
66
+ */
67
+ grid?: boolean;
68
+ /**
69
+ * Where to draw **session dividers** — the solid verticals at a trading
70
+ * calendar's collapse **seams**: boundaries that removed (closed-market)
71
+ * time actually precedes, not every session roll (only with a
72
+ * `discontinuities` / `calendar` provider). On a real exchange calendar
73
+ * every session open follows an overnight gap, so seams = session opens; a
74
+ * calendar of contiguous full-day sessions has seams only where days were
75
+ * excised (the weekend). **Default `'none'`** — the hierarchical grid
76
+ * already marks the calendar structure at every zoom, so dividers are
77
+ * opt-in emphasis: `'all'` draws one at *every* seam in view (the
78
+ * TradingView session-separator look, crowding lines fading out),
79
+ * `'labeled'` only at seams the axis also labels. Dividers are independent
80
+ * of {@link grid} — `'all'` + `grid={false}` is the
81
+ * separators-on-a-clean-plot look.
82
+ */
83
+ sessionDividers?: 'labeled' | 'all' | 'none';
56
84
  /** Total width in CSS pixels (plot + axis gutters). */
57
85
  width: number;
58
86
  /** Vertical space between rows in CSS pixels (not under the axis). Default 0. */
@@ -281,5 +309,5 @@ export interface ChartContainerProps {
281
309
  * {@link TimeAxis} at the bottom, aligned under the plots. Y axes are per-row
282
310
  * (`<YAxis>`).
283
311
  */
284
- export declare function ChartContainer({ range, width, rowGap, showAxis, trackerPosition, onTrackerChanged, selected, onSelect, hovered, onHover, panZoom, onTimeRangeChange, minDuration, cursor, cursorSequence, onRegionSelect, regionSelectModifier, cursorTime, crosshairSnap, editAnnotations, creating, onCreate, onSelectAnnotation, onHoverAnnotation, onEditAnnotation, snap, timeFormat, theme, discontinuities, calendar, spacing, children, }: ChartContainerProps): import("react/jsx-runtime").JSX.Element;
312
+ export declare function ChartContainer({ range, width, rowGap, showAxis, trackerPosition, onTrackerChanged, selected, onSelect, hovered, onHover, panZoom, onTimeRangeChange, minDuration, cursor, cursorSequence, onRegionSelect, regionSelectModifier, cursorTime, crosshairSnap, editAnnotations, creating, onCreate, onSelectAnnotation, onHoverAnnotation, onEditAnnotation, snap, timeFormat, theme, discontinuities, calendar, spacing, grid, sessionDividers, children, }: ChartContainerProps): import("react/jsx-runtime").JSX.Element;
285
313
  //# sourceMappingURL=ChartContainer.d.ts.map
@@ -43,7 +43,7 @@ function normalizeRange(range) {
43
43
  * {@link TimeAxis} at the bottom, aligned under the plots. Y axes are per-row
44
44
  * (`<YAxis>`).
45
45
  */
46
- export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trackerPosition, onTrackerChanged, selected, onSelect, hovered, onHover, panZoom = false, onTimeRangeChange, minDuration = 1, cursor = DEFAULT_CURSOR_MODE, cursorSequence, onRegionSelect, regionSelectModifier, cursorTime = false, crosshairSnap = true, editAnnotations = false, creating = null, onCreate, onSelectAnnotation, onHoverAnnotation, onEditAnnotation, snap = true, timeFormat, theme, discontinuities, calendar, spacing, children, }) {
46
+ export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trackerPosition, onTrackerChanged, selected, onSelect, hovered, onHover, panZoom = false, onTimeRangeChange, minDuration = 1, cursor = DEFAULT_CURSOR_MODE, cursorSequence, onRegionSelect, regionSelectModifier, cursorTime = false, crosshairSnap = true, editAnnotations = false, creating = null, onCreate, onSelectAnnotation, onHoverAnnotation, onEditAnnotation, snap = true, timeFormat, theme, discontinuities, calendar, spacing, grid = true, sessionDividers = 'none', children, }) {
47
47
  // The explicit base domain from `range` (a tuple or a TimeRange). `undefined`
48
48
  // ⇒ auto-fit (resolved from the layers below). Pan/zoom seeds from it; `seed`
49
49
  // is the placeholder while auto-fitting.
@@ -521,6 +521,8 @@ export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trac
521
521
  xScale,
522
522
  xKind: resolvedKind,
523
523
  discontinuities: xDiscontinuities,
524
+ grid,
525
+ sessionDividers,
524
526
  panZoom,
525
527
  minDuration,
526
528
  applyRange,
@@ -576,6 +578,8 @@ export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trac
576
578
  xScale,
577
579
  resolvedKind,
578
580
  xDiscontinuities,
581
+ grid,
582
+ sessionDividers,
579
583
  panZoom,
580
584
  minDuration,
581
585
  applyRange,
package/dist/Layers.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Children, cloneElement, isValidElement, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
3
3
  import { Canvas } from './Canvas.js';
4
- import { drawGrid, drawDividers, thinPixels } from './grid.js';
4
+ import { drawGrid, drawDividers, dividerAlphas, thinPixels } from './grid.js';
5
5
  import { cursorParts, bandRect, regionSpan } from './tracker.js';
6
6
  import { resolveSelection } from './select.js';
7
7
  import { panRange, zoomRange, panRangeTrading, zoomRangeTrading, } from './viewport.js';
@@ -14,9 +14,31 @@ import { ContainerContext, LayersContext, RowContext, } from './context.js';
14
14
  * container's shared `xTickCount` (as `<XAxis>` and `formatTime` do), which is
15
15
  * width-derived on a trading-time axis. */
16
16
  const GRID_TICKS = 5;
17
- /** Minimum px between session dividers — thins dense collapse points (e.g. a
18
- * daily chart where every candle is a new session) so the axis never crowds. */
17
+ /** Minimum px between `'labeled'` session dividers — thins dense collapse
18
+ * points (e.g. a daily chart where every candle is a new session) so the axis
19
+ * never crowds. (`'all'` mode fades instead of thinning — see below.) */
19
20
  const MIN_DIVIDER_PX = 40;
21
+ /** Calendar gridlines (one per day / month / aligned hour — the full grain
22
+ * populations, `TradingTimeScale.gridLevels`) are full-strength while their
23
+ * **nominal** spacing (`level.spacing` — calendar density, gap-free; NOT the
24
+ * measured on-screen gaps, so hiding weekends doesn't brighten the grid) is
25
+ * at least this — a day grain reaches full at a ~1.5-month window on a
26
+ * default-width plot, in either mode. Dashed faint lines tolerate more
27
+ * density than the solid session dividers, hence the tighter pair. */
28
+ const GRID_LINE_FULL_PX = 15;
29
+ /** …and fully faded once nominal spacing falls to this (the same quadratic
30
+ * wash-aware ramp as the session lines — alpha `t²`, so total ink → 0).
31
+ * Also the enumeration floor: a grain denser than this is skipped
32
+ * outright. */
33
+ const GRID_LINE_GONE_PX = 5;
34
+ /** `'all'`-mode session lines are full-strength while spaced at least this
35
+ * far apart (the ~1M-view session width on a default-width plot). */
36
+ const SESSION_LINE_FULL_PX = 28;
37
+ /** …and fully invisible once spacing falls to this — a smooth quadratic
38
+ * falloff between the two, so nothing pops in/out as you pan/zoom (a hard
39
+ * drop keyed to pixel clusters shifts phase with the window) and the plot
40
+ * zooms out to *clean*, not to a gray wash (see `dividerAlphas`). */
41
+ const SESSION_LINE_GONE_PX = 6;
20
42
  /** Wheel-zoom sensitivity: `factor = exp(deltaY * k)` (one ~100px notch ≈ ±15%). */
21
43
  const ZOOM_SENSITIVITY = 0.0015;
22
44
  /** Pointer slop (px): a drag must exceed this before it pans, and a click within
@@ -76,27 +98,107 @@ export function Layers({ children }) {
76
98
  // A category axis draws no vertical gridlines — a line through each bar
77
99
  // centre reads as noise; the bars are the structure.
78
100
  const xTickVals = container.xKind === 'category' ? [] : xScale.ticks(xTickCount);
79
- const xTicks = xTickVals.map((d) => xScale(+d));
80
- const yTicks = gridY
81
- ? (explicitY ?? gridY.ticks(GRID_TICKS)).map((t) => gridY(t))
82
- : [];
83
- drawGrid(ctx, xTicks, yTicks, w, h, gridColor, gridDash);
101
+ // The reference grid behind the data, opt-out via `grid={false}` for
102
+ // a clean backdrop (session dividers below stay independent of it).
103
+ if (container.grid) {
104
+ const yTicks = gridY
105
+ ? (explicitY ?? gridY.ticks(GRID_TICKS)).map((t) => gridY(t))
106
+ : [];
107
+ // On a calendar axis the verticals are the FULL grain populations —
108
+ // every day in the month, every month in the year, every aligned
109
+ // clock instant — not just the thinned instants the labels chose:
110
+ // the labels decorate the grid, they don't define it. Each grain
111
+ // fades AS A UNIT by its **nominal** (gap-free, calendar-density)
112
+ // spacing — `level.spacing`, the same wash-aware quadratic ramp as
113
+ // the session lines — so zooming out dissolves the fine grain into
114
+ // the coarser one instead of popping when the label algorithm
115
+ // switches rung, and collapsing weekends draws *fewer* day lines at
116
+ // the *same* strength (not wider-spaced lines that jump to full —
117
+ // the owner's same-zoom, different-weight complaint). Levels nest,
118
+ // so walk coarsest→finest and let the first (widest-spaced,
119
+ // strongest) claim shared anchors — a month start draws once, at
120
+ // month strength, never dimmed by the day crowd around it.
121
+ const levels = container.xKind === 'time' && 'gridLevels' in xScale
122
+ ? xScale.gridLevels(GRID_LINE_GONE_PX)
123
+ : [];
124
+ if (levels.length > 0) {
125
+ const xs = [];
126
+ const alphas = [];
127
+ const claimed = new Set();
128
+ for (let i = levels.length - 1; i >= 0; i--) {
129
+ const { values, spacing } = levels[i];
130
+ const t = Math.max(0, Math.min(1, (spacing - GRID_LINE_GONE_PX) /
131
+ (GRID_LINE_FULL_PX - GRID_LINE_GONE_PX)));
132
+ const alpha = t * t;
133
+ if (alpha <= 0.02)
134
+ continue;
135
+ for (const v of values) {
136
+ if (claimed.has(v))
137
+ continue;
138
+ claimed.add(v);
139
+ xs.push(xScale(v));
140
+ alphas.push(alpha);
141
+ }
142
+ }
143
+ drawGrid(ctx, xs, yTicks, w, h, gridColor, gridDash, alphas);
144
+ }
145
+ else {
146
+ // Value axis / no-calendar provider: verticals at the labelled
147
+ // ticks, the pre-hierarchical behavior.
148
+ const xTicks = xTickVals.map((d) => xScale(+d));
149
+ drawGrid(ctx, xTicks, yTicks, w, h, gridColor, gridDash);
150
+ }
151
+ }
84
152
  // Session dividers: solid verticals at the trading calendar's collapse
85
- // points (session/day opens), where closed time was removed from the axis.
86
- // Draw them at the axis ticks that are collapse points the same
87
- // calendar-coarsened instants the axis labels so a divider sits under
88
- // each date/month/year label, not at every session (which crowds).
153
+ // SEAMS where closed time was actually removed from the axis. Opt-in
154
+ // emphasis over the grid (default 'none' the grid's grain populations
155
+ // already mark the calendar): `'all'` draws one at every seam in view
156
+ // (the TradingView separator look, crowding lines fading out);
157
+ // `'labeled'` only at the axis ticks that are seams.
89
158
  const disc = container.discontinuities;
90
- if (disc?.boundaries) {
159
+ if (disc?.boundaries && container.sessionDividers !== 'none') {
91
160
  const [d0, d1] = container.timeRange;
92
161
  // Call as a method (not a detached reference) so a class-based provider
93
162
  // whose `boundaries` reads `this` keeps its receiver.
94
- const collapse = new Set(disc.boundaries(d0, d1));
95
- const bx = xTickVals
96
- .filter((t) => collapse.has(+t))
97
- .map((t) => xScale(+t));
163
+ //
164
+ // `boundaries` is the provider's session ROSTER — the tick ladder and
165
+ // the grid consume every session open as a date anchor. A divider,
166
+ // though, marks removed time, and a roster entry that no gap precedes
167
+ // (a demo calendar's contiguous 24h weekday "sessions", a seamless
168
+ // session roll) is roster structure, not a seam — the owner's "why
169
+ // session lines on every day when only weekends are removed?"
170
+ // (2026-07-16). Keep exactly the true seams: `b` is one iff the
171
+ // instant just before it has zero live width. For a real exchange
172
+ // calendar every open follows an overnight gap, so this keeps all.
173
+ //
174
+ // Test `<= 0`, not a positive tolerance: a collapsed gap makes
175
+ // `distance(b-1, b)` **exactly** 0 (both instants map to the same live
176
+ // position), so no epsilon is needed — and a positive tolerance is
177
+ // **unit-dependent**. Under `spacing: 'uniform'` distance is measured
178
+ // in session-units, where a contiguous (non-seam) boundary reads a
179
+ // tiny fraction (`1 / sessionMs`) that a `< 0.5` test would misread as
180
+ // a seam. Exact zero matches the semantic contract in either spacing,
181
+ // and agrees with the same probe in `buildTicks` (Codex review, #479).
182
+ const seams = disc
183
+ .boundaries(d0, d1)
184
+ .filter((b) => disc.distance(b - 1, b) <= 0);
185
+ const marks = container.sessionDividers === 'all'
186
+ ? seams
187
+ : (() => {
188
+ const collapse = new Set(seams);
189
+ return xTickVals.map((v) => +v).filter((t) => collapse.has(t));
190
+ })();
191
+ const bx = marks.map((t) => xScale(t));
98
192
  const dividerColor = container.theme.axis.sessionDivider ?? gridColor;
99
- drawDividers(ctx, thinPixels(bx, MIN_DIVIDER_PX), h, dividerColor);
193
+ if (container.sessionDividers === 'all') {
194
+ // Draw every boundary — no thinning (dropping a phase-dependent
195
+ // subset makes lines jump as the window slides). Crowding lines fade
196
+ // instead, so density falls off smoothly toward a clean plot.
197
+ drawDividers(ctx, bx, h, dividerColor, dividerAlphas(bx, SESSION_LINE_GONE_PX, SESSION_LINE_FULL_PX));
198
+ }
199
+ else {
200
+ drawDividers(ctx, thinPixels(bx, MIN_DIVIDER_PX), h, dividerColor);
201
+ }
100
202
  }
101
203
  for (const entry of layers) {
102
204
  const yScale = yScales.get(entry.axisId ?? defaultAxisId);
package/dist/XAxis.d.ts CHANGED
@@ -63,6 +63,23 @@ export interface XAxisProps {
63
63
  * for dense or wide labels that would collide when centred.
64
64
  */
65
65
  align?: 'auto' | 'center' | 'right';
66
+ /**
67
+ * How a **time** axis lays out its date context (ignored on value / category
68
+ * axes, and whenever a custom `format`, `transform`, or explicit `ticks`
69
+ * owns the labels).
70
+ * - **`'flat'` (default)** — one row: each tick that opens a coarser
71
+ * calendar period is relabelled **inline** to it (the month at a month
72
+ * turn, the year at a year turn, the date at a day turn under an intraday
73
+ * grain), every other tick a terse label (`5`, `Feb`, `14:00`). The
74
+ * TradingView look — the row reads `… 30 31 Feb 2 3 …`.
75
+ * - `'stacked'` — two rows: a terse top row (the grain's bare unit — `14:00`,
76
+ * `12`, `Feb`) over a segmented **band** row of the next-coarser period
77
+ * (day bands under intraday ticks, month bands under day ticks, year bands
78
+ * under month/quarter ticks). Each band is a left-aligned label + a divider
79
+ * at its turn, zebra-shaded by the band's calendar parity; the top-row tick
80
+ * on each turn is emphasized and joins its divider as one boundary line.
81
+ */
82
+ dateStyle?: 'flat' | 'stacked';
66
83
  }
67
84
  /**
68
85
  * The shared **x axis**, a sibling of {@link YAxis} for the horizontal axis. A
@@ -74,5 +91,5 @@ export interface XAxisProps {
74
91
  *
75
92
  * `<TimeAxis>` is the time-flavoured preset (`<XAxis />`).
76
93
  */
77
- export declare function XAxis({ format, label, side, height, ticks: customTicks, transform, color, align, }?: XAxisProps): import("react/jsx-runtime").JSX.Element;
94
+ export declare function XAxis({ format, label, side, height, ticks: customTicks, transform, color, align, dateStyle, }?: XAxisProps): import("react/jsx-runtime").JSX.Element;
78
95
  //# sourceMappingURL=XAxis.d.ts.map