@pond-ts/charts 0.44.0 → 0.45.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 +117 -1
- package/dist/ChartContainer.js +42 -8
- package/dist/Layers.js +12 -7
- package/dist/LineChart.d.ts +15 -1
- package/dist/LineChart.js +17 -2
- package/dist/XAxis.js +34 -6
- package/dist/context.d.ts +15 -0
- package/dist/format.d.ts +4 -2
- package/dist/format.js +7 -3
- package/dist/line.d.ts +25 -1
- package/dist/line.js +78 -13
- package/dist/tickLadder.d.ts +89 -0
- package/dist/tickLadder.js +265 -0
- package/dist/tradingTimeScale.d.ts +29 -20
- package/dist/tradingTimeScale.js +80 -82
- package/package.json +3 -3
package/dist/tradingTimeScale.js
CHANGED
|
@@ -1,73 +1,39 @@
|
|
|
1
1
|
import { scaleTime } from 'd3-scale';
|
|
2
|
+
import { boundaryFormatFor, boundaryGrainFor, boundaryTicks, buildTicks, majorFormatFor, } from './tickLadder.js';
|
|
3
|
+
// Grain selection lives in `tickLadder.ts` (the full hour1…year ladder plus
|
|
4
|
+
// the boundary-row helpers); re-exported here so existing imports keep working.
|
|
5
|
+
export { coarsenCalendar } from './tickLadder.js';
|
|
2
6
|
/**
|
|
3
|
-
* The
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* The trivial gap-free {@link DiscontinuityProvider}: live time **is** wall
|
|
8
|
+
* time, and every local midnight is a "session open". Backing a plain
|
|
9
|
+
* continuous time axis with `scaleTradingTime(identityProvider())` runs it
|
|
10
|
+
* through the same logical tick ladder as a trading-calendar axis — calendar
|
|
11
|
+
* days are the day anchors, so a year of data ticks on month starts and an
|
|
12
|
+
* afternoon ticks on clock-aligned hours, instead of d3's mixed multi-scale
|
|
13
|
+
* default.
|
|
9
14
|
*/
|
|
10
|
-
function
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const out = [];
|
|
31
|
-
let prev;
|
|
32
|
-
for (const t of opens) {
|
|
33
|
-
const k = bucketKey(t, g);
|
|
34
|
-
if (k !== prev) {
|
|
35
|
-
out.push(t);
|
|
36
|
-
prev = k;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return out;
|
|
40
|
-
}
|
|
41
|
-
const COARSENING_LADDER = [
|
|
42
|
-
'week',
|
|
43
|
-
'month',
|
|
44
|
-
'quarter',
|
|
45
|
-
'year',
|
|
46
|
-
];
|
|
47
|
-
/**
|
|
48
|
-
* Thin an ascending run of **session opens** down to about `count` axis ticks by
|
|
49
|
-
* **calendar grain** — the trading-terminal habit of labelling week / month /
|
|
50
|
-
* year starts rather than an arbitrary every-nth session. Picks the finest grain
|
|
51
|
-
* on the ladder (session → week → month → quarter → year) that yields at most
|
|
52
|
-
* `count` buckets and returns the first open in each; beyond yearly it decimates
|
|
53
|
-
* every-nth so the axis never crowds. Exported so the container can draw session
|
|
54
|
-
* dividers at the same instants the axis labels.
|
|
55
|
-
*/
|
|
56
|
-
export function coarsenCalendar(opens, count) {
|
|
57
|
-
if (opens.length <= count)
|
|
58
|
-
return { ticks: [...opens], granularity: 'session' };
|
|
59
|
-
for (const g of COARSENING_LADDER) {
|
|
60
|
-
const ticks = firstOfEachBucket(opens, g);
|
|
61
|
-
if (ticks.length <= count)
|
|
62
|
-
return { ticks, granularity: g };
|
|
63
|
-
}
|
|
64
|
-
// Coarser than yearly isn't a calendar grain — decimate the year starts.
|
|
65
|
-
const yearly = firstOfEachBucket(opens, 'year');
|
|
66
|
-
const step = Math.ceil(yearly.length / count);
|
|
67
|
-
return {
|
|
68
|
-
ticks: yearly.filter((_, i) => i % step === 0),
|
|
69
|
-
granularity: 'year',
|
|
15
|
+
export function identityProvider() {
|
|
16
|
+
const self = {
|
|
17
|
+
clampUp: (t) => t,
|
|
18
|
+
clampDown: (t) => t,
|
|
19
|
+
distance: (from, to) => to - from,
|
|
20
|
+
offset: (v, amount) => v + amount,
|
|
21
|
+
copy: () => self,
|
|
22
|
+
boundaries: (from, to) => {
|
|
23
|
+
const out = [];
|
|
24
|
+
const d = new Date(from);
|
|
25
|
+
// First local midnight strictly after `from`; step by calendar day (not
|
|
26
|
+
// 24h) so DST transitions stay on midnight.
|
|
27
|
+
let cur = new Date(d.getFullYear(), d.getMonth(), d.getDate() + 1);
|
|
28
|
+
while (cur.getTime() < to) {
|
|
29
|
+
if (cur.getTime() > from)
|
|
30
|
+
out.push(cur.getTime());
|
|
31
|
+
cur = new Date(cur.getFullYear(), cur.getMonth(), cur.getDate() + 1);
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
},
|
|
70
35
|
};
|
|
36
|
+
return self;
|
|
71
37
|
}
|
|
72
38
|
/**
|
|
73
39
|
* Build a {@link TradingTimeScale} over the given discontinuity `provider`.
|
|
@@ -97,17 +63,35 @@ export function scaleTradingTime(provider) {
|
|
|
97
63
|
const bounds = provider.boundaries?.(domain[0], domain[1]) ?? [];
|
|
98
64
|
return [domain[0], ...bounds];
|
|
99
65
|
};
|
|
66
|
+
/** Whether the provider has calendar structure to ladder on. Without a
|
|
67
|
+
* `boundaries` method there are no anchors — the even-spacing fallback. */
|
|
68
|
+
const hasCalendar = () => provider.boundaries !== undefined;
|
|
69
|
+
/** The ladder result for this domain at `count` — the single source `ticks`,
|
|
70
|
+
* `tickFormat`, and `tickBoundaries` all derive from, so the three agree.
|
|
71
|
+
* Memoized on `(domain, count)`: the three callers (plus gridlines /
|
|
72
|
+
* dividers) hit the same resolution per render, and on a wide continuous
|
|
73
|
+
* domain re-walking every day-open is the expensive part. */
|
|
74
|
+
let laddered = null;
|
|
75
|
+
const resolved = (count) => {
|
|
76
|
+
const key = `${domain[0]}:${domain[1]}:${count}`;
|
|
77
|
+
if (laddered?.key !== key) {
|
|
78
|
+
laddered = {
|
|
79
|
+
key,
|
|
80
|
+
value: buildTicks(provider, sessionOpens(), domain[1], count),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return laddered.value;
|
|
84
|
+
};
|
|
100
85
|
scale.ticks = (count = 10) => {
|
|
101
86
|
const live = totalLive();
|
|
102
87
|
if (live <= 0 || count < 1)
|
|
103
88
|
return [domain[0]];
|
|
104
|
-
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
// No boundaries (a single session / no calendar): fall back to interior
|
|
89
|
+
// Calendar-aware: walk the logical ladder over the session-open anchors
|
|
90
|
+
// (hour steps inside sessions, then day / week / month / quarter / year
|
|
91
|
+
// starts) — the trading-terminal look, never an arbitrary every-nth.
|
|
92
|
+
if (hasCalendar())
|
|
93
|
+
return resolved(count).ticks;
|
|
94
|
+
// No boundaries (no calendar structure at all): fall back to interior
|
|
111
95
|
// even-spaced ticks — endpoints excluded so none sits on the plot edge.
|
|
112
96
|
const out = [];
|
|
113
97
|
for (let i = 1; i < count; i++) {
|
|
@@ -118,20 +102,34 @@ export function scaleTradingTime(provider) {
|
|
|
118
102
|
scale.tickFormat = (count = 10, specifier) => {
|
|
119
103
|
if (specifier !== undefined)
|
|
120
104
|
return base.tickFormat(count, specifier);
|
|
121
|
-
const opens = sessionOpens();
|
|
122
105
|
const defFmt = base.tickFormat(count);
|
|
123
|
-
if (
|
|
106
|
+
if (!hasCalendar())
|
|
124
107
|
return defFmt; // no calendar → d3 multi-scale default
|
|
125
|
-
// Anchor
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
|
|
108
|
+
// Anchor labels at the grain {@link ticks} chose — one uniform format per
|
|
109
|
+
// grain (hours as `%H:%M`, days/weeks as `%b %d`, months/quarters as `%b`,
|
|
110
|
+
// years as `%Y`); the coarser context the label omits is the second row
|
|
111
|
+
// ({@link tickBoundaries}). Any other instant — a cursor readout — uses
|
|
112
|
+
// the d3 multi-scale default. Same grain as {@link ticks}, so labels and
|
|
113
|
+
// the dividers drawn at these instants agree.
|
|
114
|
+
const { ticks, granularity } = resolved(count);
|
|
131
115
|
const anchors = new Set(ticks);
|
|
132
|
-
const anchorFmt = base.tickFormat(count, granularity
|
|
116
|
+
const anchorFmt = base.tickFormat(count, majorFormatFor(granularity));
|
|
133
117
|
return (d) => (anchors.has(+d) ? anchorFmt(d) : defFmt(d));
|
|
134
118
|
};
|
|
119
|
+
scale.tickBoundaries = (count = 10) => {
|
|
120
|
+
if (!hasCalendar())
|
|
121
|
+
return () => undefined;
|
|
122
|
+
const { ticks, granularity } = resolved(count);
|
|
123
|
+
const bg = boundaryGrainFor(granularity);
|
|
124
|
+
if (bg === undefined)
|
|
125
|
+
return () => undefined;
|
|
126
|
+
const fmt = base.tickFormat(count, boundaryFormatFor(bg));
|
|
127
|
+
const labelled = new Map();
|
|
128
|
+
for (const t of boundaryTicks(ticks, granularity)) {
|
|
129
|
+
labelled.set(t, fmt(new Date(t)));
|
|
130
|
+
}
|
|
131
|
+
return (value) => labelled.get(value);
|
|
132
|
+
};
|
|
135
133
|
function domainFn(next) {
|
|
136
134
|
if (next === undefined)
|
|
137
135
|
return [domain[0], domain[1]];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pond-ts/charts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.45.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Canvas-rendered, streaming-first time-series charts for pond-ts",
|
|
6
6
|
"license": "MIT",
|
|
@@ -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.45.0",
|
|
42
|
+
"pond-ts": "^0.45.0",
|
|
43
43
|
"react": "^18.0.0 || ^19.0.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|