@pond-ts/charts 0.45.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 +149 -2
- package/dist/ChartContainer.d.ts +30 -2
- package/dist/ChartContainer.js +6 -2
- package/dist/Layers.js +120 -18
- package/dist/XAxis.d.ts +44 -1
- package/dist/XAxis.js +207 -52
- package/dist/YAxis.d.ts +9 -1
- package/dist/YAxis.js +4 -4
- package/dist/annotations.d.ts +1 -1
- package/dist/annotations.js +17 -6
- package/dist/context.d.ts +8 -0
- package/dist/derivedTicks.d.ts +36 -0
- package/dist/derivedTicks.js +92 -0
- package/dist/grid.d.ts +32 -5
- package/dist/grid.js +81 -13
- package/dist/index.d.ts +1 -0
- package/dist/theme.d.ts +13 -0
- package/dist/theme.js +5 -0
- package/dist/tickLadder.d.ts +145 -22
- package/dist/tickLadder.js +627 -35
- package/dist/tradingTimeScale.d.ts +96 -12
- package/dist/tradingTimeScale.js +119 -5
- package/package.json +4 -4
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type TickGranularity } from './tickLadder.js';
|
|
1
2
|
/**
|
|
2
3
|
* The structural discontinuity-provider surface `scaleTradingTime` consumes to
|
|
3
4
|
* collapse closed-market time. Charts declares this **shape** itself and never
|
|
@@ -16,10 +17,16 @@ export interface DiscontinuityProvider {
|
|
|
16
17
|
offset(value: number, amount: number): number;
|
|
17
18
|
copy(): DiscontinuityProvider;
|
|
18
19
|
/**
|
|
19
|
-
* Optional: the
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* Optional: the **session roster** strictly inside `(from, to)` — each
|
|
21
|
+
* session/day open, in ascending order. Consumed two ways: the tick ladder
|
|
22
|
+
* and the calendar grid treat every entry as a **date anchor** (so a
|
|
23
|
+
* provider whose sessions are contiguous — the gap-free identity provider,
|
|
24
|
+
* a full-day-sessions demo — still reports every open); the container's
|
|
25
|
+
* **session dividers** draw only at the entries that are true collapse
|
|
26
|
+
* *seams* (removed time immediately precedes them — detected via
|
|
27
|
+
* {@link distance}), which on a real exchange calendar is all of them.
|
|
28
|
+
* A provider that omits this just collapses the axis silently. (A
|
|
29
|
+
* `TradingCalendar.discontinuities()` provider supplies it.)
|
|
23
30
|
*/
|
|
24
31
|
boundaries?(from: number, to: number): number[];
|
|
25
32
|
}
|
|
@@ -47,14 +54,17 @@ export interface TradingCalendarLike {
|
|
|
47
54
|
* wherever the container's `xScale` goes.
|
|
48
55
|
*
|
|
49
56
|
* Ticks are **calendar-aware** when the provider enumerates its gaps: `.ticks`
|
|
50
|
-
* walks the logical ladder (hour1 → hour3 → hour6 → hour12 → day →
|
|
51
|
-
*
|
|
57
|
+
* walks the logical ladder (hour1 → hour3 → hour6 → hour12 → day → month →
|
|
58
|
+
* quarter → year; the day rung also thins by per-month midpoint subdivision before
|
|
59
|
+
* month grain) and returns the finest rung that fits `count`, and
|
|
52
60
|
* `.tickFormat` labels each anchor at that grain (`%H:%M` for hours, `%b %d`
|
|
53
61
|
* for days/weeks, `%b` for months/quarters, `%Y` for years) while formatting
|
|
54
62
|
* any other instant (the cursor readout) with the d3 multi-scale default. The
|
|
55
63
|
* coarser context a label drops lives on `.tickBoundaries` — the second-row
|
|
56
|
-
* boundary labels (the date over a clock axis,
|
|
57
|
-
* month axis), one per boundary crossing plus the
|
|
64
|
+
* boundary labels of the **stacked** date style (the date over a clock axis,
|
|
65
|
+
* the year over a day / week / month axis), one per boundary crossing plus the
|
|
66
|
+
* first tick. `.flatFormat` is the **flat** (default) alternative: that context
|
|
67
|
+
* promoted inline into a single row, the TradingView look.
|
|
58
68
|
* Without a provider `boundaries` method it falls back to interior even-spaced
|
|
59
69
|
* time ticks.
|
|
60
70
|
*
|
|
@@ -74,12 +84,86 @@ export interface TradingTimeScale {
|
|
|
74
84
|
/**
|
|
75
85
|
* The **second-row** (boundary) label for a tick value, or `undefined` for
|
|
76
86
|
* ticks that don't open a new boundary period. Same grain selection as
|
|
77
|
-
* {@link ticks} at the same `count`, so the rows agree:
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
87
|
+
* {@link ticks} at the same `count`, so the rows agree: each tick starting
|
|
88
|
+
* a new day / year (whichever is the next-coarser unit the first-row label
|
|
89
|
+
* omits) carries the label — a **crossing**. The left-edge context (what
|
|
90
|
+
* period the domain starts in) is {@link boundaryContext}, pinned by the
|
|
91
|
+
* axis rather than riding a tick; year-grain ticks have no second row.
|
|
81
92
|
*/
|
|
82
93
|
tickBoundaries(count?: number): (value: number) => string | undefined;
|
|
94
|
+
/**
|
|
95
|
+
* **Flat**-style single-row labels: the coarsest calendar unit each tick
|
|
96
|
+
* opens (year / month / date) promoted **inline** into the one row, its terse
|
|
97
|
+
* base label (bare day-of-month, month abbrev, clock time) otherwise — the
|
|
98
|
+
* TradingView default axis, the alternative to the two-row {@link tickFormat}
|
|
99
|
+
* + {@link tickBoundaries} stack. Same grain selection as {@link ticks} at the
|
|
100
|
+
* same `count`, so the labels sit on the tick instants. A non-tick value (the
|
|
101
|
+
* cursor readout) formats with the d3 multi-scale default, like
|
|
102
|
+
* {@link tickFormat} — so ticks read terse while the crosshair reads a full
|
|
103
|
+
* timestamp.
|
|
104
|
+
*/
|
|
105
|
+
flatFormat(count?: number): (value: number) => string;
|
|
106
|
+
/**
|
|
107
|
+
* The boundary-row label for the **domain start** — the reader's left-edge
|
|
108
|
+
* context (`Jan 01` over an intraday axis, the year over a month axis),
|
|
109
|
+
* rendered pinned at the plot's left edge. A property of the domain, not of
|
|
110
|
+
* any tick — so it stays put on a live sliding window instead of hopping
|
|
111
|
+
* from tick to tick. `undefined` when the grain has no boundary row.
|
|
112
|
+
*/
|
|
113
|
+
boundaryContext(count?: number): string | undefined;
|
|
114
|
+
/**
|
|
115
|
+
* The **grid populations** — one level per ladder rung that fits the plot,
|
|
116
|
+
* finest first, each carrying its FULL anchor population in the domain
|
|
117
|
+
* (every aligned clock instant / session open / month / quarter / year
|
|
118
|
+
* start, strictly after the domain start — the window's left edge is not a
|
|
119
|
+
* calendar boundary). Where {@link ticks} thins one rung down to *labels*,
|
|
120
|
+
* these are the calendar structure the labels sit on: the container draws
|
|
121
|
+
* every anchor and fades each level as a unit by its `spacing`, so a
|
|
122
|
+
* crowding level dissolves while the coarser ones persist (the map-style
|
|
123
|
+
* hierarchical grid). Levels nest — de-duplicate coarsest-first.
|
|
124
|
+
*
|
|
125
|
+
* `spacing` is the level's **nominal** spacing in px — `width × the grain's
|
|
126
|
+
* wall-clock step / the domain's wall span` — i.e. its spacing on a gap-free
|
|
127
|
+
* axis. Keying the fade off calendar density rather than the measured
|
|
128
|
+
* on-screen gaps makes the look **mode-invariant**: collapsing weekends
|
|
129
|
+
* draws fewer day lines at the same strength, instead of wider-spaced day
|
|
130
|
+
* lines that jump to full opacity at the same zoom.
|
|
131
|
+
*
|
|
132
|
+
* `minGapPx` is the spacing at which a level's lines have fully faded; a
|
|
133
|
+
* level denser than that is omitted outright. Empty without a calendar
|
|
134
|
+
* (a provider that can't enumerate `boundaries`).
|
|
135
|
+
*/
|
|
136
|
+
gridLevels(minGapPx?: number): Array<{
|
|
137
|
+
granularity: TickGranularity;
|
|
138
|
+
values: number[];
|
|
139
|
+
spacing: number;
|
|
140
|
+
}>;
|
|
141
|
+
/**
|
|
142
|
+
* The **terse base** label for each tick — the grain's bare unit with **no**
|
|
143
|
+
* inline promotion (`14:00`, `12`, `Feb`, `2026`), the top row of the
|
|
144
|
+
* **stacked band** date style. Same anchor set as {@link ticks}; a non-tick
|
|
145
|
+
* instant (the cursor) falls through to the d3 multi-scale default. This is
|
|
146
|
+
* `flatFormat` without the promotions — the coarser context lives in the
|
|
147
|
+
* band row ({@link bands}) instead of inline.
|
|
148
|
+
*/
|
|
149
|
+
baseFormat(count?: number): (value: number) => string;
|
|
150
|
+
/**
|
|
151
|
+
* The **date bands** — the segmented second row of the stacked style. One
|
|
152
|
+
* entry per next-coarser calendar period touching the domain (day bands
|
|
153
|
+
* under intraday ticks, month bands under day ticks, year bands under
|
|
154
|
+
* month/quarter ticks), each `{ start, label, shaded }`: the period's start
|
|
155
|
+
* instant (the first band's `start` may precede the domain — its label pins
|
|
156
|
+
* at the left edge), the left-aligned label, and a stable zebra `shaded`
|
|
157
|
+
* flag keyed to the band's calendar identity (pan/zoom-invariant). The
|
|
158
|
+
* renderer draws a divider at each interior `start`, fills shaded bands, and
|
|
159
|
+
* emphasizes the top-row tick sitting at each `start`. Empty at year grain
|
|
160
|
+
* (nothing coarser to band) and without a calendar provider.
|
|
161
|
+
*/
|
|
162
|
+
bands(count?: number): Array<{
|
|
163
|
+
start: number;
|
|
164
|
+
label: string;
|
|
165
|
+
shaded: boolean;
|
|
166
|
+
}>;
|
|
83
167
|
domain(): [number, number];
|
|
84
168
|
domain(next: readonly [number, number]): TradingTimeScale;
|
|
85
169
|
range(): [number, number];
|
package/dist/tradingTimeScale.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { scaleTime } from 'd3-scale';
|
|
2
|
-
import { boundaryFormatFor, boundaryGrainFor, boundaryTicks, buildTicks, majorFormatFor, } from './tickLadder.js';
|
|
2
|
+
import { bandFormatFor, bandGrainFor, bandNext, bandShaded, bandStartOf, boundaryFormatFor, boundaryGrainFor, boundaryTicks, buildGridLevels, buildTicks, flatBaseFormatFor, flatFormats, majorFormatFor, nominalStepMs, } from './tickLadder.js';
|
|
3
3
|
// Grain selection lives in `tickLadder.ts` (the full hour1…year ladder plus
|
|
4
4
|
// the boundary-row helpers); re-exported here so existing imports keep working.
|
|
5
5
|
export { coarsenCalendar } from './tickLadder.js';
|
|
@@ -58,10 +58,19 @@ export function scaleTradingTime(provider) {
|
|
|
58
58
|
return provider.offset(domain[0], frac * totalLive());
|
|
59
59
|
};
|
|
60
60
|
/** The session-open instants in the domain — the first session's open (the
|
|
61
|
-
* left edge) plus each collapsed-gap boundary — the axis's date anchors.
|
|
61
|
+
* left edge) plus each collapsed-gap boundary — the axis's date anchors.
|
|
62
|
+
* Memoized per domain: both the label ladder ({@link resolved}) and the
|
|
63
|
+
* grid populations ({@link TradingTimeScale.gridLevels}) walk these every
|
|
64
|
+
* frame of a pan, and on a wide continuous domain the enumeration (one
|
|
65
|
+
* Date per day) is the expensive part. */
|
|
66
|
+
let opensMemo = null;
|
|
62
67
|
const sessionOpens = () => {
|
|
63
|
-
const
|
|
64
|
-
|
|
68
|
+
const key = `${domain[0]}:${domain[1]}`;
|
|
69
|
+
if (opensMemo?.key !== key) {
|
|
70
|
+
const bounds = provider.boundaries?.(domain[0], domain[1]) ?? [];
|
|
71
|
+
opensMemo = { key, value: [domain[0], ...bounds] };
|
|
72
|
+
}
|
|
73
|
+
return opensMemo.value;
|
|
65
74
|
};
|
|
66
75
|
/** Whether the provider has calendar structure to ladder on. Without a
|
|
67
76
|
* `boundaries` method there are no anchors — the even-spacing fallback. */
|
|
@@ -125,11 +134,116 @@ export function scaleTradingTime(provider) {
|
|
|
125
134
|
return () => undefined;
|
|
126
135
|
const fmt = base.tickFormat(count, boundaryFormatFor(bg));
|
|
127
136
|
const labelled = new Map();
|
|
128
|
-
for (const t of boundaryTicks(ticks, granularity)) {
|
|
137
|
+
for (const t of boundaryTicks(ticks, granularity, domain[0])) {
|
|
129
138
|
labelled.set(t, fmt(new Date(t)));
|
|
130
139
|
}
|
|
131
140
|
return (value) => labelled.get(value);
|
|
132
141
|
};
|
|
142
|
+
scale.flatFormat = (count = 10) => {
|
|
143
|
+
// A non-tick instant (the cursor readout) always uses the d3 multi-scale
|
|
144
|
+
// default — same as tickFormat, so the crosshair reads a full timestamp
|
|
145
|
+
// while the ticks read terse. Without a calendar there are no ladder
|
|
146
|
+
// anchors, so every value falls through to the default.
|
|
147
|
+
const defFmt = base.tickFormat(count);
|
|
148
|
+
if (!hasCalendar())
|
|
149
|
+
return (value) => defFmt(new Date(value));
|
|
150
|
+
const { ticks, granularity } = resolved(count);
|
|
151
|
+
// Seed the promotion walk from the previous **live** instant before the
|
|
152
|
+
// domain start (clampDown; identity on a continuous axis): a domain that
|
|
153
|
+
// opens exactly on a month's first session then promotes that tick to the
|
|
154
|
+
// month (`apr 8 14 …`, not `1 8 14 …`) — the previous session was in
|
|
155
|
+
// March — while a mid-session start still suppresses the false promotion.
|
|
156
|
+
const specs = flatFormats(ticks, granularity, provider.clampDown(domain[0] - 1));
|
|
157
|
+
// One d3 formatter per distinct specifier; most ticks share the base one.
|
|
158
|
+
const bySpec = new Map();
|
|
159
|
+
const fmtFor = (spec) => {
|
|
160
|
+
let f = bySpec.get(spec);
|
|
161
|
+
if (f === undefined) {
|
|
162
|
+
f = base.tickFormat(count, spec);
|
|
163
|
+
bySpec.set(spec, f);
|
|
164
|
+
}
|
|
165
|
+
return f;
|
|
166
|
+
};
|
|
167
|
+
const labelled = new Map();
|
|
168
|
+
ticks.forEach((t, i) => labelled.set(t, fmtFor(specs[i])(new Date(t))));
|
|
169
|
+
return (value) => labelled.get(value) ?? defFmt(new Date(value));
|
|
170
|
+
};
|
|
171
|
+
scale.baseFormat = (count = 10) => {
|
|
172
|
+
// The terse top row of the stacked band style: the grain's bare unit, no
|
|
173
|
+
// inline promotion (that context lives in the band row). Anchors get the
|
|
174
|
+
// grain's flat base format; a non-tick instant (the cursor) reads the d3
|
|
175
|
+
// multi-scale default, so the crosshair still shows a full timestamp.
|
|
176
|
+
const defFmt = base.tickFormat(count);
|
|
177
|
+
if (!hasCalendar())
|
|
178
|
+
return (value) => defFmt(new Date(value));
|
|
179
|
+
const { ticks, granularity } = resolved(count);
|
|
180
|
+
const anchors = new Set(ticks);
|
|
181
|
+
const terse = base.tickFormat(count, flatBaseFormatFor(granularity));
|
|
182
|
+
return (value) => anchors.has(value) ? terse(new Date(value)) : defFmt(new Date(value));
|
|
183
|
+
};
|
|
184
|
+
scale.bands = (count = 10) => {
|
|
185
|
+
if (!hasCalendar())
|
|
186
|
+
return [];
|
|
187
|
+
const bg = bandGrainFor(resolved(count).granularity);
|
|
188
|
+
if (bg === undefined)
|
|
189
|
+
return []; // year grain — nothing coarser to band
|
|
190
|
+
const fmt = base.tickFormat(count, bandFormatFor(bg));
|
|
191
|
+
const out = [];
|
|
192
|
+
// First band starts at (or before) the domain start — the partial left
|
|
193
|
+
// band whose label the renderer pins at x=0; step to each next period
|
|
194
|
+
// start still inside the domain. Bounded loop as a runaway guard.
|
|
195
|
+
let s = bandStartOf(domain[0], bg);
|
|
196
|
+
for (let i = 0; i < 100_000 && s < domain[1]; i++) {
|
|
197
|
+
out.push({
|
|
198
|
+
start: s,
|
|
199
|
+
label: fmt(new Date(s)),
|
|
200
|
+
shaded: bandShaded(s, bg),
|
|
201
|
+
});
|
|
202
|
+
s = bandNext(s, bg);
|
|
203
|
+
}
|
|
204
|
+
return out;
|
|
205
|
+
};
|
|
206
|
+
scale.boundaryContext = (count = 10) => {
|
|
207
|
+
if (!hasCalendar())
|
|
208
|
+
return undefined;
|
|
209
|
+
const { granularity } = resolved(count);
|
|
210
|
+
const bg = boundaryGrainFor(granularity);
|
|
211
|
+
if (bg === undefined)
|
|
212
|
+
return undefined;
|
|
213
|
+
const fmt = base.tickFormat(count, boundaryFormatFor(bg));
|
|
214
|
+
return fmt(new Date(domain[0]));
|
|
215
|
+
};
|
|
216
|
+
/** Memoized like {@link resolved}: the draw pass asks once per frame, and
|
|
217
|
+
* the populations only change with the domain / width / fade floor. */
|
|
218
|
+
let gridMemo = null;
|
|
219
|
+
scale.gridLevels = (minGapPx = 4) => {
|
|
220
|
+
if (!hasCalendar())
|
|
221
|
+
return [];
|
|
222
|
+
const width = Math.abs(range[1] - range[0]);
|
|
223
|
+
const wallSpan = domain[1] - domain[0];
|
|
224
|
+
if (wallSpan <= 0 || width <= 0)
|
|
225
|
+
return [];
|
|
226
|
+
const cap = Math.max(1, Math.floor(width / Math.max(1, minGapPx)));
|
|
227
|
+
const key = `${domain[0]}:${domain[1]}:${width}:${minGapPx}`;
|
|
228
|
+
if (gridMemo?.key !== key) {
|
|
229
|
+
gridMemo = {
|
|
230
|
+
key,
|
|
231
|
+
value: buildGridLevels(provider, sessionOpens(), domain[1], cap)
|
|
232
|
+
.map((l) => ({
|
|
233
|
+
granularity: l.granularity,
|
|
234
|
+
// The first open is the domain start itself — a window edge, not
|
|
235
|
+
// a boundary (and mid-period in general). Grid lines are the
|
|
236
|
+
// strictly-interior anchors.
|
|
237
|
+
values: l.values.filter((v) => v > domain[0]),
|
|
238
|
+
// Nominal (gap-free) spacing — the fade metric; see the
|
|
239
|
+
// interface doc for why it isn't the measured on-screen gap.
|
|
240
|
+
spacing: (width * nominalStepMs(l.granularity)) / wallSpan,
|
|
241
|
+
}))
|
|
242
|
+
.filter((l) => l.values.length > 0 && l.spacing > minGapPx),
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
return gridMemo.value;
|
|
246
|
+
};
|
|
133
247
|
function domainFn(next) {
|
|
134
248
|
if (next === undefined)
|
|
135
249
|
return [domain[0], domain[1]];
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pond-ts/charts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.47.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Canvas-rendered, streaming-first time-series charts for pond-ts",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "git+https://github.com/
|
|
9
|
+
"url": "git+https://github.com/pond-ts/pond.git",
|
|
10
10
|
"directory": "packages/charts"
|
|
11
11
|
},
|
|
12
12
|
"type": "module",
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
"perf": "PERF_BENCH=1 playwright test perf.spec.ts --workers=1"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
|
-
"@pond-ts/react": "^0.
|
|
42
|
-
"pond-ts": "^0.
|
|
41
|
+
"@pond-ts/react": "^0.47.0",
|
|
42
|
+
"pond-ts": "^0.47.0",
|
|
43
43
|
"react": "^18.0.0 || ^19.0.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|