@pond-ts/charts 0.68.0 → 0.70.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/AGENTS.md +12 -3
- package/API.md +104 -100
- package/CHANGELOG.md +147 -1
- package/README.md +4 -0
- package/dist/BandChart.d.ts +20 -1
- package/dist/BandChart.js +18 -2
- package/dist/ChartContainer.d.ts +23 -0
- package/dist/ChartContainer.js +15 -4
- package/dist/XAxis.d.ts +13 -1
- package/dist/XAxis.js +35 -3
- package/dist/band.d.ts +10 -2
- package/dist/band.js +38 -10
- package/dist/context.d.ts +14 -0
- package/dist/decimate.d.ts +10 -1
- package/dist/decimate.js +51 -10
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/tickLadder.d.ts +66 -20
- package/dist/tickLadder.js +178 -105
- package/dist/tradingAxis.fixture.d.ts +19 -0
- package/dist/tradingAxis.fixture.js +24 -0
- package/dist/tradingTimeScale.d.ts +44 -3
- package/dist/tradingTimeScale.js +113 -38
- package/package.json +6 -4
|
@@ -50,6 +50,25 @@ export declare function ticks(sessions: Session[], stepMs: number): TimeSeries<t
|
|
|
50
50
|
* pen-up. (Plain {@link ticks} walks continuously across sessions, so close ≈
|
|
51
51
|
* next open and the break is invisible — this is the fixture that shows it.) */
|
|
52
52
|
export declare function gappingTicks(sessions: Session[], stepMs: number): TimeSeries<typeof tickSchema>;
|
|
53
|
+
export declare const envelopeSchema: readonly [{
|
|
54
|
+
readonly name: "time";
|
|
55
|
+
readonly kind: "time";
|
|
56
|
+
}, {
|
|
57
|
+
readonly name: "price";
|
|
58
|
+
readonly kind: "number";
|
|
59
|
+
}, {
|
|
60
|
+
readonly name: "lo";
|
|
61
|
+
readonly kind: "number";
|
|
62
|
+
}, {
|
|
63
|
+
readonly name: "hi";
|
|
64
|
+
readonly kind: "number";
|
|
65
|
+
}];
|
|
66
|
+
/** {@link gappingTicks} plus a `lo` / `hi` envelope around the price (a slowly
|
|
67
|
+
* breathing spread), so a `<BandChart>` on the trading axis shows the same
|
|
68
|
+
* overnight jump the line does: connected, the fill runs a near-vertical sliver
|
|
69
|
+
* from one session's close to the next open; with `sessionBreaks` it ends at
|
|
70
|
+
* the close and re-starts at the open. */
|
|
71
|
+
export declare function gappingEnvelope(sessions: Session[], stepMs: number): TimeSeries<typeof envelopeSchema>;
|
|
53
72
|
export declare const OHLC: {
|
|
54
73
|
readonly open: {
|
|
55
74
|
readonly from: "price";
|
|
@@ -195,6 +195,30 @@ export function gappingTicks(sessions, stepMs) {
|
|
|
195
195
|
});
|
|
196
196
|
return new TimeSeries({ name: 'ticks', schema: tickSchema, rows });
|
|
197
197
|
}
|
|
198
|
+
export const envelopeSchema = [
|
|
199
|
+
{ name: 'time', kind: 'time' },
|
|
200
|
+
{ name: 'price', kind: 'number' },
|
|
201
|
+
{ name: 'lo', kind: 'number' },
|
|
202
|
+
{ name: 'hi', kind: 'number' },
|
|
203
|
+
];
|
|
204
|
+
/** {@link gappingTicks} plus a `lo` / `hi` envelope around the price (a slowly
|
|
205
|
+
* breathing spread), so a `<BandChart>` on the trading axis shows the same
|
|
206
|
+
* overnight jump the line does: connected, the fill runs a near-vertical sliver
|
|
207
|
+
* from one session's close to the next open; with `sessionBreaks` it ends at
|
|
208
|
+
* the close and re-starts at the open. */
|
|
209
|
+
export function gappingEnvelope(sessions, stepMs) {
|
|
210
|
+
const rows = [];
|
|
211
|
+
let i = 0;
|
|
212
|
+
sessions.forEach((s, si) => {
|
|
213
|
+
const base = 100 + si * 6; // each session gaps ~6 above the last
|
|
214
|
+
for (let t = s.open; t < s.close; t += stepMs, i++) {
|
|
215
|
+
const price = base + 4 * Math.sin(i / 18) + 1.5 * Math.sin(i / 3.5);
|
|
216
|
+
const spread = 1.5 + 0.8 * Math.sin(i / 40);
|
|
217
|
+
rows.push([t, price, price - spread, price + spread]);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
return new TimeSeries({ name: 'envelope', schema: envelopeSchema, rows });
|
|
221
|
+
}
|
|
198
222
|
export const OHLC = {
|
|
199
223
|
open: { from: 'price', using: 'first' },
|
|
200
224
|
high: { from: 'price', using: 'max' },
|
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import { type TickGranularity, type TimeGrain } from './tickLadder.js';
|
|
2
|
+
/** Zone options shared by {@link scaleTradingTime} and {@link identityProvider}. */
|
|
3
|
+
export interface ScaleTimeZoneOptions {
|
|
4
|
+
/**
|
|
5
|
+
* The IANA zone the axis's calendar runs in — ticks on that zone's
|
|
6
|
+
* midnights / Mondays / month starts, labels and readouts reading in it.
|
|
7
|
+
* **Omitted ⇒ the runtime's local zone** (the browser's), exactly as before
|
|
8
|
+
* the option existed. Any id `Intl` knows (`'UTC'`, `'Europe/Berlin'`, …);
|
|
9
|
+
* an unknown id throws `RangeError`.
|
|
10
|
+
*/
|
|
11
|
+
timeZone?: string | undefined;
|
|
12
|
+
}
|
|
2
13
|
/**
|
|
3
14
|
* The structural discontinuity-provider surface `scaleTradingTime` consumes to
|
|
4
15
|
* collapse closed-market time. Charts declares this **shape** itself and never
|
|
@@ -29,6 +40,15 @@ export interface DiscontinuityProvider {
|
|
|
29
40
|
* `TradingCalendar.discontinuities()` provider supplies it.)
|
|
30
41
|
*/
|
|
31
42
|
boundaries?(from: number, to: number): number[];
|
|
43
|
+
/**
|
|
44
|
+
* Optional: the same provider with its calendar in another zone. Only a
|
|
45
|
+
* provider whose gap topology *depends* on a zone needs it — the identity
|
|
46
|
+
* provider, whose "sessions" are calendar days and therefore move with the
|
|
47
|
+
* zone; a trading calendar's session opens are instants and do not. Used by
|
|
48
|
+
* {@link TradingTimeScale.withTimeZone} so a second `<XAxis timeZone>` can
|
|
49
|
+
* re-derive its day anchors in its own zone.
|
|
50
|
+
*/
|
|
51
|
+
withTimeZone?(timeZone: string | undefined): DiscontinuityProvider;
|
|
32
52
|
}
|
|
33
53
|
/**
|
|
34
54
|
* The high-level counterpart to a bare {@link DiscontinuityProvider}: anything
|
|
@@ -45,6 +65,14 @@ export interface TradingCalendarLike {
|
|
|
45
65
|
discontinuities(options?: {
|
|
46
66
|
spacing?: 'proportional' | 'uniform';
|
|
47
67
|
}): DiscontinuityProvider;
|
|
68
|
+
/**
|
|
69
|
+
* Optional: the exchange's IANA zone. When present and the container has no
|
|
70
|
+
* explicit `timeZone`, the axis renders in it — ticks on exchange-local day
|
|
71
|
+
* starts, labels and readouts in exchange time — instead of the viewer's
|
|
72
|
+
* zone. A `@pond-ts/financial` `TradingCalendar` built `fromRules` carries
|
|
73
|
+
* its rules' zone here.
|
|
74
|
+
*/
|
|
75
|
+
readonly timeZone?: string | undefined;
|
|
48
76
|
}
|
|
49
77
|
/**
|
|
50
78
|
* A d3-scale-shaped time scale whose pixel mapping runs through **trading time**
|
|
@@ -198,22 +226,35 @@ export interface TradingTimeScale {
|
|
|
198
226
|
range(): [number, number];
|
|
199
227
|
range(next: readonly [number, number]): TradingTimeScale;
|
|
200
228
|
copy(): TradingTimeScale;
|
|
229
|
+
/**
|
|
230
|
+
* The same pixel mapping (provider, domain, range) with its **calendar in
|
|
231
|
+
* another zone** — `undefined` for runtime-local. The scale's own zone is
|
|
232
|
+
* unchanged; this is how a second `<XAxis timeZone>` strip ticks and labels
|
|
233
|
+
* in its own zone over the container's shared x mapping. A provider that
|
|
234
|
+
* exposes {@link DiscontinuityProvider.withTimeZone} re-derives its day
|
|
235
|
+
* anchors; any other keeps its instants (a trading calendar's session opens
|
|
236
|
+
* are zone-independent).
|
|
237
|
+
*/
|
|
238
|
+
withTimeZone(timeZone: string | undefined): TradingTimeScale;
|
|
239
|
+
/** The IANA zone this scale's calendar runs in; `undefined` = runtime-local. */
|
|
240
|
+
timeZone(): string | undefined;
|
|
201
241
|
}
|
|
202
242
|
export { coarsenCalendar } from './tickLadder.js';
|
|
203
243
|
export type { TickGranularity, TimeGrain } from './tickLadder.js';
|
|
204
244
|
/**
|
|
205
245
|
* The trivial gap-free {@link DiscontinuityProvider}: live time **is** wall
|
|
206
|
-
* time, and every
|
|
246
|
+
* time, and every midnight (in `timeZone`, default runtime-local) is a
|
|
247
|
+
* "session open". Backing a plain
|
|
207
248
|
* continuous time axis with `scaleTradingTime(identityProvider())` runs it
|
|
208
249
|
* through the same logical tick ladder as a trading-calendar axis — calendar
|
|
209
250
|
* days are the day anchors, so a year of data ticks on month starts and an
|
|
210
251
|
* afternoon ticks on clock-aligned hours, instead of d3's mixed multi-scale
|
|
211
252
|
* default.
|
|
212
253
|
*/
|
|
213
|
-
export declare function identityProvider(): DiscontinuityProvider;
|
|
254
|
+
export declare function identityProvider(options?: ScaleTimeZoneOptions): DiscontinuityProvider;
|
|
214
255
|
/**
|
|
215
256
|
* Build a {@link TradingTimeScale} over the given discontinuity `provider`.
|
|
216
257
|
* Configure like a d3 scale: `scaleTradingTime(provider).domain([t0, t1]).range([0, width])`.
|
|
217
258
|
*/
|
|
218
|
-
export declare function scaleTradingTime(provider: DiscontinuityProvider): TradingTimeScale;
|
|
259
|
+
export declare function scaleTradingTime(provider: DiscontinuityProvider, options?: ScaleTimeZoneOptions): TradingTimeScale;
|
|
219
260
|
//# sourceMappingURL=tradingTimeScale.d.ts.map
|
package/dist/tradingTimeScale.js
CHANGED
|
@@ -1,34 +1,84 @@
|
|
|
1
|
-
import { scaleTime } from 'd3-scale';
|
|
2
|
-
import {
|
|
1
|
+
import { scaleTime, scaleUtc } from 'd3-scale';
|
|
2
|
+
import { utcFormat } from 'd3-time-format';
|
|
3
|
+
import { TimeZone } from 'pond-ts';
|
|
4
|
+
import { bandFormatFor, bandGrainFor, bandNext, bandShaded, bandStartOf, boundaryFormatFor, boundaryGrainFor, boundaryTicks, buildGridLevels, buildTicks, coarseUnitOf, flatBaseFormatFor, flatFormats, majorFormatFor, nominalStepMs, readoutFormatFor, tickCalendarFor, } from './tickLadder.js';
|
|
5
|
+
/**
|
|
6
|
+
* The d3 time-specifier formatter for a zone. Local (no zone) is d3's own
|
|
7
|
+
* `timeFormat` through the scale, untouched. A named zone formats a
|
|
8
|
+
* **civil-shifted** date — the instant moved by the zone's offset and then
|
|
9
|
+
* read in UTC — so every `%Y %m %d %H %M %S %a %b %p …` directive reads in the
|
|
10
|
+
* zone for free; `%Z` / `%z`, which the shift would render `UTC` / `+0000`,
|
|
11
|
+
* are substituted per instant from the zone itself. The two directives that
|
|
12
|
+
* print the *instant* rather than a wall-clock field — `%s` (epoch seconds)
|
|
13
|
+
* and `%Q` (epoch ms) — read the shifted instant, i.e. offset by the zone;
|
|
14
|
+
* they have no meaning on a zoned axis.
|
|
15
|
+
*/
|
|
16
|
+
function zonedFormatter(zone, specifier) {
|
|
17
|
+
// Tokenise so an escaped percent (`%%`) is never read as the start of a
|
|
18
|
+
// directive: `'%%Z'` is a literal `%Z`, not the zone name.
|
|
19
|
+
const tokens = specifier.match(/%%|%[Zz]|[^%]+|%/g) ?? [];
|
|
20
|
+
const hasZoneName = tokens.some((tok) => tok === '%Z' || tok === '%z');
|
|
21
|
+
if (!hasZoneName) {
|
|
22
|
+
const f = utcFormat(specifier);
|
|
23
|
+
return (d) => {
|
|
24
|
+
const t = +d;
|
|
25
|
+
return f(new Date(t + zone.offsetAt(t)));
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const cache = new Map();
|
|
29
|
+
return (d) => {
|
|
30
|
+
const t = +d;
|
|
31
|
+
const offset = zone.offsetAt(t);
|
|
32
|
+
const sign = offset < 0 ? '-' : '+';
|
|
33
|
+
const abs = Math.abs(offset) / 60_000;
|
|
34
|
+
const hh = String(Math.floor(abs / 60)).padStart(2, '0');
|
|
35
|
+
const mm = String(abs % 60).padStart(2, '0');
|
|
36
|
+
const resolved = tokens
|
|
37
|
+
.map((tok) => tok === '%Z'
|
|
38
|
+
? zone.abbreviation(t).replace(/%/g, '%%')
|
|
39
|
+
: tok === '%z'
|
|
40
|
+
? `${sign}${hh}${mm}`
|
|
41
|
+
: tok)
|
|
42
|
+
.join('');
|
|
43
|
+
let f = cache.get(resolved);
|
|
44
|
+
if (f === undefined) {
|
|
45
|
+
f = utcFormat(resolved);
|
|
46
|
+
cache.set(resolved, f);
|
|
47
|
+
}
|
|
48
|
+
return f(new Date(t + offset));
|
|
49
|
+
};
|
|
50
|
+
}
|
|
3
51
|
// Grain selection lives in `tickLadder.ts` (the full hour1…year ladder plus
|
|
4
52
|
// the boundary-row helpers); re-exported here so existing imports keep working.
|
|
5
53
|
export { coarsenCalendar } from './tickLadder.js';
|
|
6
54
|
/**
|
|
7
55
|
* The trivial gap-free {@link DiscontinuityProvider}: live time **is** wall
|
|
8
|
-
* time, and every
|
|
56
|
+
* time, and every midnight (in `timeZone`, default runtime-local) is a
|
|
57
|
+
* "session open". Backing a plain
|
|
9
58
|
* continuous time axis with `scaleTradingTime(identityProvider())` runs it
|
|
10
59
|
* through the same logical tick ladder as a trading-calendar axis — calendar
|
|
11
60
|
* days are the day anchors, so a year of data ticks on month starts and an
|
|
12
61
|
* afternoon ticks on clock-aligned hours, instead of d3's mixed multi-scale
|
|
13
62
|
* default.
|
|
14
63
|
*/
|
|
15
|
-
export function identityProvider() {
|
|
64
|
+
export function identityProvider(options = {}) {
|
|
65
|
+
const cal = tickCalendarFor(options.timeZone);
|
|
16
66
|
const self = {
|
|
17
67
|
clampUp: (t) => t,
|
|
18
68
|
clampDown: (t) => t,
|
|
19
69
|
distance: (from, to) => to - from,
|
|
20
70
|
offset: (v, amount) => v + amount,
|
|
21
71
|
copy: () => self,
|
|
72
|
+
withTimeZone: (timeZone) => identityProvider({ timeZone }),
|
|
22
73
|
boundaries: (from, to) => {
|
|
23
74
|
const out = [];
|
|
24
|
-
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
cur = new Date(cur.getFullYear(), cur.getMonth(), cur.getDate() + 1);
|
|
75
|
+
// First midnight (in the zone) strictly after `from`; step by calendar
|
|
76
|
+
// day (not 24h) so DST transitions stay on midnight.
|
|
77
|
+
let cur = cal.nextDay(from);
|
|
78
|
+
while (cur < to) {
|
|
79
|
+
if (cur > from)
|
|
80
|
+
out.push(cur);
|
|
81
|
+
cur = cal.nextDay(cur);
|
|
32
82
|
}
|
|
33
83
|
return out;
|
|
34
84
|
},
|
|
@@ -39,11 +89,31 @@ export function identityProvider() {
|
|
|
39
89
|
* Build a {@link TradingTimeScale} over the given discontinuity `provider`.
|
|
40
90
|
* Configure like a d3 scale: `scaleTradingTime(provider).domain([t0, t1]).range([0, width])`.
|
|
41
91
|
*/
|
|
42
|
-
export function scaleTradingTime(provider) {
|
|
92
|
+
export function scaleTradingTime(provider, options = {}) {
|
|
43
93
|
let domain = [0, 1];
|
|
44
94
|
let range = [0, 1];
|
|
45
|
-
|
|
95
|
+
const zone = options.timeZone === undefined ? undefined : TimeZone.of(options.timeZone);
|
|
96
|
+
const cal = tickCalendarFor(zone?.id);
|
|
97
|
+
// A private d3 time scale purely for formatting. Local: d3's own `timeFormat`
|
|
98
|
+
// (and its multi-scale default) untouched. Zoned: the same specifiers on a
|
|
99
|
+
// civil-shifted date via `utcFormat`, and a `scaleUtc` multi-scale default
|
|
100
|
+
// on the shifted date so the fallback picks its unit in the zone too.
|
|
46
101
|
const base = scaleTime();
|
|
102
|
+
const baseUtc = zone === undefined ? undefined : scaleUtc();
|
|
103
|
+
const fmt = (count, specifier) => {
|
|
104
|
+
if (zone === undefined) {
|
|
105
|
+
return specifier === undefined
|
|
106
|
+
? base.tickFormat(count)
|
|
107
|
+
: base.tickFormat(count, specifier);
|
|
108
|
+
}
|
|
109
|
+
if (specifier !== undefined)
|
|
110
|
+
return zonedFormatter(zone, specifier);
|
|
111
|
+
const def = baseUtc.tickFormat(count);
|
|
112
|
+
return (d) => {
|
|
113
|
+
const t = +d;
|
|
114
|
+
return def(new Date(t + zone.offsetAt(t)));
|
|
115
|
+
};
|
|
116
|
+
};
|
|
47
117
|
const totalLive = () => provider.distance(domain[0], domain[1]);
|
|
48
118
|
const scale = ((value) => {
|
|
49
119
|
const live = totalLive();
|
|
@@ -86,7 +156,7 @@ export function scaleTradingTime(provider) {
|
|
|
86
156
|
if (laddered?.key !== key) {
|
|
87
157
|
laddered = {
|
|
88
158
|
key,
|
|
89
|
-
value: buildTicks(provider, sessionOpens(), domain[1], count),
|
|
159
|
+
value: buildTicks(provider, sessionOpens(), domain[1], count, cal),
|
|
90
160
|
};
|
|
91
161
|
}
|
|
92
162
|
return laddered.value;
|
|
@@ -110,8 +180,8 @@ export function scaleTradingTime(provider) {
|
|
|
110
180
|
};
|
|
111
181
|
scale.tickFormat = (count = 10, specifier) => {
|
|
112
182
|
if (specifier !== undefined)
|
|
113
|
-
return
|
|
114
|
-
const defFmt =
|
|
183
|
+
return fmt(count, specifier);
|
|
184
|
+
const defFmt = fmt(count);
|
|
115
185
|
if (!hasCalendar())
|
|
116
186
|
return defFmt; // no calendar → d3 multi-scale default
|
|
117
187
|
// Anchor labels at the grain {@link ticks} chose — one uniform format per
|
|
@@ -122,7 +192,7 @@ export function scaleTradingTime(provider) {
|
|
|
122
192
|
// the dividers drawn at these instants agree.
|
|
123
193
|
const { ticks, granularity } = resolved(count);
|
|
124
194
|
const anchors = new Set(ticks);
|
|
125
|
-
const anchorFmt =
|
|
195
|
+
const anchorFmt = fmt(count, majorFormatFor(granularity));
|
|
126
196
|
return (d) => (anchors.has(+d) ? anchorFmt(d) : defFmt(d));
|
|
127
197
|
};
|
|
128
198
|
scale.tickBoundaries = (count = 10) => {
|
|
@@ -132,10 +202,10 @@ export function scaleTradingTime(provider) {
|
|
|
132
202
|
const bg = boundaryGrainFor(granularity);
|
|
133
203
|
if (bg === undefined)
|
|
134
204
|
return () => undefined;
|
|
135
|
-
const
|
|
205
|
+
const bfmt = fmt(count, boundaryFormatFor(bg));
|
|
136
206
|
const labelled = new Map();
|
|
137
|
-
for (const t of boundaryTicks(ticks, granularity, domain[0])) {
|
|
138
|
-
labelled.set(t,
|
|
207
|
+
for (const t of boundaryTicks(ticks, granularity, domain[0], cal)) {
|
|
208
|
+
labelled.set(t, bfmt(new Date(t)));
|
|
139
209
|
}
|
|
140
210
|
return (value) => labelled.get(value);
|
|
141
211
|
};
|
|
@@ -144,7 +214,7 @@ export function scaleTradingTime(provider) {
|
|
|
144
214
|
// tickFormat. (The cursor readout doesn't route through here; it uses
|
|
145
215
|
// readoutFormat, grain-aware.) Without a calendar there are no ladder
|
|
146
216
|
// anchors, so every value falls through to the default.
|
|
147
|
-
const defFmt =
|
|
217
|
+
const defFmt = fmt(count);
|
|
148
218
|
if (!hasCalendar())
|
|
149
219
|
return (value) => defFmt(new Date(value));
|
|
150
220
|
const { ticks, granularity } = resolved(count);
|
|
@@ -153,13 +223,13 @@ export function scaleTradingTime(provider) {
|
|
|
153
223
|
// opens exactly on a month's first session then promotes that tick to the
|
|
154
224
|
// month (`apr 8 14 …`, not `1 8 14 …`) — the previous session was in
|
|
155
225
|
// March — while a mid-session start still suppresses the false promotion.
|
|
156
|
-
const specs = flatFormats(ticks, granularity, provider.clampDown(domain[0] - 1));
|
|
226
|
+
const specs = flatFormats(ticks, granularity, provider.clampDown(domain[0] - 1), cal);
|
|
157
227
|
// One d3 formatter per distinct specifier; most ticks share the base one.
|
|
158
228
|
const bySpec = new Map();
|
|
159
229
|
const fmtFor = (spec) => {
|
|
160
230
|
let f = bySpec.get(spec);
|
|
161
231
|
if (f === undefined) {
|
|
162
|
-
f =
|
|
232
|
+
f = fmt(count, spec);
|
|
163
233
|
bySpec.set(spec, f);
|
|
164
234
|
}
|
|
165
235
|
return f;
|
|
@@ -173,20 +243,20 @@ export function scaleTradingTime(provider) {
|
|
|
173
243
|
// inline promotion (that context lives in the band row). Anchors get the
|
|
174
244
|
// grain's flat base format; a non-tick instant (the cursor) reads the d3
|
|
175
245
|
// multi-scale default, so the crosshair still shows a full timestamp.
|
|
176
|
-
const defFmt =
|
|
246
|
+
const defFmt = fmt(count);
|
|
177
247
|
if (!hasCalendar())
|
|
178
248
|
return (value) => defFmt(new Date(value));
|
|
179
249
|
const { ticks, granularity } = resolved(count);
|
|
180
250
|
const anchors = new Set(ticks);
|
|
181
|
-
const terse =
|
|
251
|
+
const terse = fmt(count, flatBaseFormatFor(granularity));
|
|
182
252
|
return (value) => anchors.has(value) ? terse(new Date(value)) : defFmt(new Date(value));
|
|
183
253
|
};
|
|
184
254
|
scale.readoutFormat = (count = 10) => {
|
|
185
|
-
const defFmt =
|
|
255
|
+
const defFmt = fmt(count);
|
|
186
256
|
if (!hasCalendar())
|
|
187
257
|
return (value) => defFmt(new Date(value));
|
|
188
|
-
const
|
|
189
|
-
return (value) =>
|
|
258
|
+
const rfmt = fmt(count, readoutFormatFor(resolved(count).granularity));
|
|
259
|
+
return (value) => rfmt(new Date(value));
|
|
190
260
|
};
|
|
191
261
|
scale.grain = (count = 10) => hasCalendar() ? coarseUnitOf(resolved(count).granularity) : 'day';
|
|
192
262
|
scale.bands = (count = 10) => {
|
|
@@ -196,7 +266,7 @@ export function scaleTradingTime(provider) {
|
|
|
196
266
|
const bg = bandGrainFor(granularity);
|
|
197
267
|
if (bg === undefined)
|
|
198
268
|
return []; // year grain — nothing coarser to band
|
|
199
|
-
const
|
|
269
|
+
const bandFmt = fmt(count, bandFormatFor(bg));
|
|
200
270
|
const tickSet = new Set(ticks);
|
|
201
271
|
// A band's raw calendar start is a date, not necessarily a LIVE instant —
|
|
202
272
|
// a month or day beginning on a collapsed weekend/holiday clamps onto the
|
|
@@ -207,13 +277,13 @@ export function scaleTradingTime(provider) {
|
|
|
207
277
|
// can be resolved as one group rather than emitting (and then trying to
|
|
208
278
|
// retract) a label per member.
|
|
209
279
|
const candidates = [];
|
|
210
|
-
let s = bandStartOf(domain[0], bg);
|
|
280
|
+
let s = bandStartOf(domain[0], bg, cal);
|
|
211
281
|
// First band starts at (or before) the domain start — the partial left
|
|
212
282
|
// band whose label the renderer pins at x=0; step to each next period
|
|
213
283
|
// start still inside the domain. Bounded loop as a runaway guard.
|
|
214
284
|
for (let i = 0; i < 100_000 && s < domain[1]; i++) {
|
|
215
285
|
candidates.push({ s, live: provider.clampUp(s) });
|
|
216
|
-
s = bandNext(s, bg);
|
|
286
|
+
s = bandNext(s, bg, cal);
|
|
217
287
|
}
|
|
218
288
|
const out = [];
|
|
219
289
|
for (let i = 0; i < candidates.length;) {
|
|
@@ -249,9 +319,9 @@ export function scaleTradingTime(provider) {
|
|
|
249
319
|
// Formatted from the representative's own RAW start, not the
|
|
250
320
|
// clamped one — a genuinely-live rep reads its own date; a
|
|
251
321
|
// gap-only run's rep reads whichever raw date it fell back to.
|
|
252
|
-
label:
|
|
322
|
+
label: bandFmt(new Date(rep.s)),
|
|
253
323
|
showLabel: !collides,
|
|
254
|
-
shaded: bandShaded(rep.s, bg),
|
|
324
|
+
shaded: bandShaded(rep.s, bg, cal),
|
|
255
325
|
});
|
|
256
326
|
i = j;
|
|
257
327
|
}
|
|
@@ -264,8 +334,7 @@ export function scaleTradingTime(provider) {
|
|
|
264
334
|
const bg = boundaryGrainFor(granularity);
|
|
265
335
|
if (bg === undefined)
|
|
266
336
|
return undefined;
|
|
267
|
-
|
|
268
|
-
return fmt(new Date(domain[0]));
|
|
337
|
+
return fmt(count, boundaryFormatFor(bg))(new Date(domain[0]));
|
|
269
338
|
};
|
|
270
339
|
/** Memoized like {@link resolved}: the draw pass asks once per frame, and
|
|
271
340
|
* the populations only change with the domain / width / fade floor. */
|
|
@@ -282,7 +351,7 @@ export function scaleTradingTime(provider) {
|
|
|
282
351
|
if (gridMemo?.key !== key) {
|
|
283
352
|
gridMemo = {
|
|
284
353
|
key,
|
|
285
|
-
value: buildGridLevels(provider, sessionOpens(), domain[1], cap)
|
|
354
|
+
value: buildGridLevels(provider, sessionOpens(), domain[1], cap, cal)
|
|
286
355
|
.map((l) => ({
|
|
287
356
|
granularity: l.granularity,
|
|
288
357
|
// The first open is the domain start itself — a window edge, not
|
|
@@ -312,7 +381,13 @@ export function scaleTradingTime(provider) {
|
|
|
312
381
|
return scale;
|
|
313
382
|
}
|
|
314
383
|
scale.range = rangeFn;
|
|
315
|
-
scale.copy = () => scaleTradingTime(provider.copy()).domain(domain).range(range);
|
|
384
|
+
scale.copy = () => scaleTradingTime(provider.copy(), options).domain(domain).range(range);
|
|
385
|
+
scale.withTimeZone = (timeZone) => scaleTradingTime(provider.withTimeZone?.(timeZone) ?? provider.copy(), {
|
|
386
|
+
timeZone,
|
|
387
|
+
})
|
|
388
|
+
.domain(domain)
|
|
389
|
+
.range(range);
|
|
390
|
+
scale.timeZone = () => zone?.id;
|
|
316
391
|
return scale;
|
|
317
392
|
}
|
|
318
393
|
//# sourceMappingURL=tradingTimeScale.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pond-ts/charts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.70.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Canvas-rendered, streaming-first React time-series charts for pond-ts: line, area, band, bar, scatter, box, candlestick, with cursors, selection and annotations",
|
|
6
6
|
"keywords": [
|
|
@@ -62,8 +62,8 @@
|
|
|
62
62
|
"perf": "PERF_BENCH=1 playwright test perf.spec.ts --workers=1"
|
|
63
63
|
},
|
|
64
64
|
"peerDependencies": {
|
|
65
|
-
"@pond-ts/react": "^0.
|
|
66
|
-
"pond-ts": "^0.
|
|
65
|
+
"@pond-ts/react": "^0.70.0",
|
|
66
|
+
"pond-ts": "^0.70.0",
|
|
67
67
|
"react": "^18.0.0 || ^19.0.0"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
@@ -73,6 +73,7 @@
|
|
|
73
73
|
"@testing-library/react": "^16.3.2",
|
|
74
74
|
"@types/d3-scale": "^4.0.9",
|
|
75
75
|
"@types/d3-shape": "^3.1.8",
|
|
76
|
+
"@types/d3-time-format": "^4.0.3",
|
|
76
77
|
"@types/react": "^19.0.0",
|
|
77
78
|
"@types/react-dom": "^19.2.3",
|
|
78
79
|
"happy-dom": "^20.9.0",
|
|
@@ -86,6 +87,7 @@
|
|
|
86
87
|
},
|
|
87
88
|
"dependencies": {
|
|
88
89
|
"d3-scale": "^4.0.2",
|
|
89
|
-
"d3-shape": "^3.2.0"
|
|
90
|
+
"d3-shape": "^3.2.0",
|
|
91
|
+
"d3-time-format": "^4.1.0"
|
|
90
92
|
}
|
|
91
93
|
}
|