@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 +101 -3
- package/dist/ChartContainer.d.ts +30 -2
- package/dist/ChartContainer.js +5 -1
- package/dist/Layers.js +120 -18
- package/dist/XAxis.d.ts +18 -1
- package/dist/XAxis.js +140 -76
- package/dist/context.d.ts +8 -0
- package/dist/grid.d.ts +32 -5
- package/dist/grid.js +81 -13
- package/dist/theme.d.ts +13 -0
- package/dist/theme.js +5 -0
- package/dist/tickLadder.d.ts +136 -16
- package/dist/tickLadder.js +596 -25
- package/dist/tradingTimeScale.d.ts +83 -8
- package/dist/tradingTimeScale.js +108 -4
- package/package.json +4 -4
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/
|
|
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
|
|
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
|
-
|
|
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
|
package/dist/ChartContainer.d.ts
CHANGED
|
@@ -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
|
package/dist/ChartContainer.js
CHANGED
|
@@ -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
|
|
18
|
-
* daily chart where every candle is a new session) so the axis
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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
|