@pond-ts/charts 0.41.0 → 0.42.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 +74 -5
- package/dist/BarChart.d.ts +117 -54
- package/dist/BarChart.js +241 -104
- package/dist/ChartContainer.d.ts +52 -6
- package/dist/ChartContainer.js +81 -4
- package/dist/Layers.js +47 -8
- package/dist/ScatterChart.d.ts +15 -3
- package/dist/ScatterChart.js +23 -5
- package/dist/bars.d.ts +96 -5
- package/dist/bars.js +157 -10
- package/dist/context.d.ts +54 -19
- package/dist/data.d.ts +102 -1
- package/dist/data.js +147 -0
- package/dist/grid.d.ts +14 -0
- package/dist/grid.js +36 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +5 -1
- package/dist/scatter.d.ts +9 -7
- package/dist/scatter.js +12 -8
- package/dist/theme.d.ts +7 -0
- package/dist/theme.js +1 -0
- package/dist/tradingTimeScale.d.ts +97 -0
- package/dist/tradingTimeScale.js +152 -0
- package/dist/viewport.d.ts +23 -0
- package/dist/viewport.js +51 -0
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -45,13 +45,16 @@ export type { BarChartProps } from './BarChart.js';
|
|
|
45
45
|
export { Candlestick } from './Candlestick.js';
|
|
46
46
|
export type { CandlestickProps } from './Candlestick.js';
|
|
47
47
|
export type { CandleVariant, ColorBy } from './ohlc.js';
|
|
48
|
+
export { scaleTradingTime } from './tradingTimeScale.js';
|
|
49
|
+
export type { TradingTimeScale, DiscontinuityProvider, } from './tradingTimeScale.js';
|
|
48
50
|
export { Region, Baseline, Marker } from './annotations.js';
|
|
49
51
|
export type { RegionProps, BaselineProps, MarkerProps } from './annotations.js';
|
|
50
52
|
export type { AnnotationKind, CreateSpec } from './context.js';
|
|
51
53
|
export { YAxisIndicator, createLiveValue } from './indicators.js';
|
|
52
54
|
export type { YAxisIndicatorProps, LiveValue } from './indicators.js';
|
|
53
|
-
export { fromTimeSeries, bandFromTimeSeries, boxFromTimeSeries, barsFromTimeSeries, ohlcFromTimeSeries, } from './data.js';
|
|
54
|
-
export type { ChartSeries, BandSeries, BoxSeries, BoxColumns, BarSeries, OhlcSeries, OhlcColumns, } from './data.js';
|
|
55
|
+
export { fromTimeSeries, bandFromTimeSeries, boxFromTimeSeries, barsFromTimeSeries, ohlcFromTimeSeries, stacksFromGroups, stacksFromColumns, stacksFromBins, } from './data.js';
|
|
56
|
+
export type { ChartSeries, BandSeries, BoxSeries, BoxColumns, BarSeries, OhlcSeries, OhlcColumns, StackedBarSeries, BinRecord, StacksFromBinsOptions, } from './data.js';
|
|
57
|
+
export type { Orientation } from './bars.js';
|
|
55
58
|
export type { RadiusEncoding, ColorEncoding } from './encoding.js';
|
|
56
59
|
export type { Curve } from './curve.js';
|
|
57
60
|
export type { GapMode } from './gaps.js';
|
package/dist/index.js
CHANGED
|
@@ -30,13 +30,17 @@ export { ScatterChart } from './ScatterChart.js';
|
|
|
30
30
|
export { BoxPlot } from './BoxPlot.js';
|
|
31
31
|
export { BarChart } from './BarChart.js';
|
|
32
32
|
export { Candlestick } from './Candlestick.js';
|
|
33
|
+
export { scaleTradingTime } from './tradingTimeScale.js';
|
|
33
34
|
// Annotations — user-authored marks in the turquoise register (distinct from the
|
|
34
35
|
// data): a shaded span, a horizontal value line, a vertical x line.
|
|
35
36
|
export { Region, Baseline, Marker } from './annotations.js';
|
|
36
37
|
// Axis indicators — a value pill pinned to an axis edge (the ChartIQ live tag).
|
|
37
38
|
// `createLiveValue` is the high-frequency, isolated-repaint update path.
|
|
38
39
|
export { YAxisIndicator, createLiveValue } from './indicators.js';
|
|
39
|
-
export { fromTimeSeries, bandFromTimeSeries, boxFromTimeSeries, barsFromTimeSeries, ohlcFromTimeSeries,
|
|
40
|
+
export { fromTimeSeries, bandFromTimeSeries, boxFromTimeSeries, barsFromTimeSeries, ohlcFromTimeSeries,
|
|
41
|
+
// Stacked / histogram readers — assemble a StackedBarSeries from pond's own
|
|
42
|
+
// aggregation output: a Map of grouped series, a wide series, or byColumn bins.
|
|
43
|
+
stacksFromGroups, stacksFromColumns, stacksFromBins, } from './data.js';
|
|
40
44
|
export { defaultTheme, estelaTheme } from './theme.js';
|
|
41
45
|
// CSS-custom-property → ChartTheme bridge: build a theme from a design system's
|
|
42
46
|
// tokens (`cssVarTheme`), and a hook that re-resolves it on a `data-theme`
|
package/dist/scatter.d.ts
CHANGED
|
@@ -44,13 +44,15 @@ export declare function scatterExtent(cs: ChartSeries): [number, number] | null;
|
|
|
44
44
|
* @param labelAt optional per-point text label; `undefined` ⇒ no labels drawn.
|
|
45
45
|
* @param font `theme.font` (family + size) for label text.
|
|
46
46
|
* @param selected the container's current selection (or `null`).
|
|
47
|
-
* @param
|
|
48
|
-
* `
|
|
47
|
+
* @param seriesId this layer's stable series identity (its `id` prop, or
|
|
48
|
+
* `undefined` when the layer isn't selectable) — the series half
|
|
49
|
+
* of the selection match. A point lights only when the selection's
|
|
50
|
+
* `id` matches, keyed to the sample by its `key`.
|
|
49
51
|
*/
|
|
50
52
|
export declare function drawScatter(ctx: CanvasRenderingContext2D, cs: ChartSeries, xScale: Scale, yScale: Scale, style: ScatterStyle, encoding: ResolvedEncoding, keyAt: (i: number) => number, labelAt: ((i: number) => string | undefined) | undefined, font: {
|
|
51
53
|
readonly family: string;
|
|
52
54
|
readonly size: number;
|
|
53
|
-
}, selected: SelectInfo | null,
|
|
55
|
+
}, selected: SelectInfo | null, seriesId: string | undefined): void;
|
|
54
56
|
/**
|
|
55
57
|
* Hit-test plot-pixel `(qx, qy)` against the scatter's points — the topmost
|
|
56
58
|
* point whose circle contains the click, or `null`. "Topmost" = the
|
|
@@ -59,12 +61,12 @@ export declare function drawScatter(ctx: CanvasRenderingContext2D, cs: ChartSeri
|
|
|
59
61
|
*
|
|
60
62
|
* A point's hit radius is its drawn radius (data-driven or base) — clicking the
|
|
61
63
|
* visible disc selects it. Distance is compared squared (no `sqrt` in the loop).
|
|
62
|
-
* Returns the point's {@link SelectInfo} with
|
|
63
|
-
*
|
|
64
|
-
* and the
|
|
64
|
+
* Returns the point's {@link SelectInfo} with the series `id` (the selection
|
|
65
|
+
* identity), `key = keyAt(i)` (its event `begin` — click provenance), the encoded
|
|
66
|
+
* fill colour (so the readout swatch matches the mark), and the display `label`.
|
|
65
67
|
*
|
|
66
68
|
* Pure: takes the same `xScale`/`yScale` the row hands to `draw`, so it
|
|
67
69
|
* unit-tests without a DOM (mirrors the `sampleAt` / `resolveSelection` split).
|
|
68
70
|
*/
|
|
69
|
-
export declare function hitTestScatter(cs: ChartSeries, qx: number, qy: number, xScale: Scale, yScale: Scale, encoding: ResolvedEncoding, keyAt: (i: number) => number, seriesLabel: string): SelectInfo | null;
|
|
71
|
+
export declare function hitTestScatter(cs: ChartSeries, qx: number, qy: number, xScale: Scale, yScale: Scale, encoding: ResolvedEncoding, keyAt: (i: number) => number, id: string, seriesLabel: string): SelectInfo | null;
|
|
70
72
|
//# sourceMappingURL=scatter.d.ts.map
|
package/dist/scatter.js
CHANGED
|
@@ -109,13 +109,16 @@ export function scatterExtent(cs) {
|
|
|
109
109
|
* @param labelAt optional per-point text label; `undefined` ⇒ no labels drawn.
|
|
110
110
|
* @param font `theme.font` (family + size) for label text.
|
|
111
111
|
* @param selected the container's current selection (or `null`).
|
|
112
|
-
* @param
|
|
113
|
-
* `
|
|
112
|
+
* @param seriesId this layer's stable series identity (its `id` prop, or
|
|
113
|
+
* `undefined` when the layer isn't selectable) — the series half
|
|
114
|
+
* of the selection match. A point lights only when the selection's
|
|
115
|
+
* `id` matches, keyed to the sample by its `key`.
|
|
114
116
|
*/
|
|
115
|
-
export function drawScatter(ctx, cs, xScale, yScale, style, encoding, keyAt, labelAt, font, selected,
|
|
117
|
+
export function drawScatter(ctx, cs, xScale, yScale, style, encoding, keyAt, labelAt, font, selected, seriesId) {
|
|
116
118
|
ctx.save();
|
|
117
119
|
// The selection only lights up a point of *this* series; resolve the key once.
|
|
118
|
-
|
|
120
|
+
// A no-id (non-selectable) layer passes `undefined` and never matches.
|
|
121
|
+
const selectedKey = selected !== null && selected.id === seriesId ? selected.key : null;
|
|
119
122
|
let selPx = 0;
|
|
120
123
|
let selPy = 0;
|
|
121
124
|
let selR = 0;
|
|
@@ -183,14 +186,14 @@ const LABEL_GAP = 4;
|
|
|
183
186
|
*
|
|
184
187
|
* A point's hit radius is its drawn radius (data-driven or base) — clicking the
|
|
185
188
|
* visible disc selects it. Distance is compared squared (no `sqrt` in the loop).
|
|
186
|
-
* Returns the point's {@link SelectInfo} with
|
|
187
|
-
*
|
|
188
|
-
* and the
|
|
189
|
+
* Returns the point's {@link SelectInfo} with the series `id` (the selection
|
|
190
|
+
* identity), `key = keyAt(i)` (its event `begin` — click provenance), the encoded
|
|
191
|
+
* fill colour (so the readout swatch matches the mark), and the display `label`.
|
|
189
192
|
*
|
|
190
193
|
* Pure: takes the same `xScale`/`yScale` the row hands to `draw`, so it
|
|
191
194
|
* unit-tests without a DOM (mirrors the `sampleAt` / `resolveSelection` split).
|
|
192
195
|
*/
|
|
193
|
-
export function hitTestScatter(cs, qx, qy, xScale, yScale, encoding, keyAt, seriesLabel) {
|
|
196
|
+
export function hitTestScatter(cs, qx, qy, xScale, yScale, encoding, keyAt, id, seriesLabel) {
|
|
194
197
|
for (let i = cs.length - 1; i >= 0; i -= 1) {
|
|
195
198
|
if (!isPoint(cs, i))
|
|
196
199
|
continue;
|
|
@@ -201,6 +204,7 @@ export function hitTestScatter(cs, qx, qy, xScale, yScale, encoding, keyAt, seri
|
|
|
201
204
|
const dy = qy - py;
|
|
202
205
|
if (dx * dx + dy * dy <= r * r) {
|
|
203
206
|
return {
|
|
207
|
+
id,
|
|
204
208
|
key: keyAt(i),
|
|
205
209
|
value: cs.y[i],
|
|
206
210
|
color: encoding.colorAt(i),
|
package/dist/theme.d.ts
CHANGED
|
@@ -104,6 +104,13 @@ export interface ChartTheme {
|
|
|
104
104
|
readonly grid: string;
|
|
105
105
|
/** Gridline dash pattern (px on/off pairs); `[]` for solid. */
|
|
106
106
|
readonly gridDash: readonly number[];
|
|
107
|
+
/**
|
|
108
|
+
* Stroke for **session dividers** — the solid verticals a trading-time axis
|
|
109
|
+
* draws at each collapsed gap (session/day open). Optional; falls back to
|
|
110
|
+
* {@link grid}. Set it a touch stronger than the gridlines so a session
|
|
111
|
+
* boundary reads as structural.
|
|
112
|
+
*/
|
|
113
|
+
readonly sessionDivider?: string;
|
|
107
114
|
/**
|
|
108
115
|
* Typography for the axis **title** — the rotated y-axis unit strip and the
|
|
109
116
|
* x-axis label (distinct from the per-tick `label` colour above). Omit a
|
package/dist/theme.js
CHANGED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The structural discontinuity-provider surface `scaleTradingTime` consumes to
|
|
3
|
+
* collapse closed-market time. Charts declares this **shape** itself and never
|
|
4
|
+
* imports `@pond-ts/financial` — a `TradingCalendar.discontinuities()` provider
|
|
5
|
+
* satisfies it structurally, so the packages stay decoupled (trading-calendar
|
|
6
|
+
* RFC §6.1). Domain values are epoch-milliseconds.
|
|
7
|
+
*/
|
|
8
|
+
export interface DiscontinuityProvider {
|
|
9
|
+
/** A value in a removed gap → its next live instant; a live value unchanged. */
|
|
10
|
+
clampUp(value: number): number;
|
|
11
|
+
/** A value in a removed gap → its previous live instant; a live value unchanged. */
|
|
12
|
+
clampDown(value: number): number;
|
|
13
|
+
/** Signed live (non-gap) distance from `from` to `to`. */
|
|
14
|
+
distance(from: number, to: number): number;
|
|
15
|
+
/** Advance `value` by `amount` live-ms, skipping gaps (inverse of {@link distance}). */
|
|
16
|
+
offset(value: number, amount: number): number;
|
|
17
|
+
copy(): DiscontinuityProvider;
|
|
18
|
+
/**
|
|
19
|
+
* Optional: the domain positions of collapsed gaps strictly inside `(from,
|
|
20
|
+
* to)` — session/day opens where closed time was removed. The container draws
|
|
21
|
+
* a **session divider** at each; a provider that omits it just collapses the
|
|
22
|
+
* axis silently. (A `TradingCalendar.discontinuities()` provider supplies it.)
|
|
23
|
+
*/
|
|
24
|
+
boundaries?(from: number, to: number): number[];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The high-level counterpart to a bare {@link DiscontinuityProvider}: anything
|
|
28
|
+
* that can *produce* one from an optional `spacing` choice. A
|
|
29
|
+
* `@pond-ts/financial` `TradingCalendar` satisfies this structurally (its
|
|
30
|
+
* `discontinuities` accepts a superset of these options), so a consumer can
|
|
31
|
+
* hand `<ChartContainer calendar={cal} spacing="uniform" />` instead of calling
|
|
32
|
+
* `cal.discontinuities({ spacing })` themselves — and charts still never imports
|
|
33
|
+
* the financial package (RFC §6.1). For the full option matrix (a bar `period`,
|
|
34
|
+
* a scoped `range`) build the provider yourself and pass the low-level
|
|
35
|
+
* `discontinuities` prop.
|
|
36
|
+
*/
|
|
37
|
+
export interface TradingCalendarLike {
|
|
38
|
+
discontinuities(options?: {
|
|
39
|
+
spacing?: 'proportional' | 'uniform';
|
|
40
|
+
}): DiscontinuityProvider;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A d3-scale-shaped time scale whose pixel mapping runs through **trading time**
|
|
44
|
+
* — closed-market gaps (weekends, holidays, overnight, lunch breaks) collapse to
|
|
45
|
+
* nothing while time stays proportional within each session. Exposes the slice
|
|
46
|
+
* of the d3 `ScaleTime` surface `@pond-ts/charts` actually uses, so it drops in
|
|
47
|
+
* wherever the container's `xScale` goes.
|
|
48
|
+
*
|
|
49
|
+
* Ticks are **calendar-aware** when the provider enumerates its gaps: `.ticks`
|
|
50
|
+
* returns the session opens thinned to a calendar grain (week / month / year
|
|
51
|
+
* starts, whichever fits ~`count`), and `.tickFormat` labels each anchor with a
|
|
52
|
+
* **date** (`%b %d`) — or the **year** (`%Y`) when ticks are a year apart —
|
|
53
|
+
* while formatting any other instant (a mid-session tick, the cursor readout)
|
|
54
|
+
* with the d3 multi-scale default. Without a provider `boundaries` method it
|
|
55
|
+
* falls back to interior even-spaced time ticks.
|
|
56
|
+
*
|
|
57
|
+
* **Out-of-domain behavior.** Within the calendar the scale extrapolates like a
|
|
58
|
+
* normal scale — a live instant *before* the domain start maps to a negative
|
|
59
|
+
* pixel (so off-plot marks are still culled). Instants outside the calendar
|
|
60
|
+
* entirely (before the first session / after the last) have no trading-time
|
|
61
|
+
* position and **clamp** to the near edge rather than extrapolating into
|
|
62
|
+
* meaningless space. In practice marks are always in-calendar, so this only
|
|
63
|
+
* affects data beyond the calendar's absolute extremes.
|
|
64
|
+
*/
|
|
65
|
+
export interface TradingTimeScale {
|
|
66
|
+
(value: number): number;
|
|
67
|
+
invert(pixel: number): number;
|
|
68
|
+
ticks(count?: number): number[];
|
|
69
|
+
tickFormat(count?: number, specifier?: string): (date: Date) => string;
|
|
70
|
+
domain(): [number, number];
|
|
71
|
+
domain(next: readonly [number, number]): TradingTimeScale;
|
|
72
|
+
range(): [number, number];
|
|
73
|
+
range(next: readonly [number, number]): TradingTimeScale;
|
|
74
|
+
copy(): TradingTimeScale;
|
|
75
|
+
}
|
|
76
|
+
/** The calendar grain a run of session opens is bucketed to for axis ticks. */
|
|
77
|
+
type TickGranularity = 'session' | 'week' | 'month' | 'quarter' | 'year';
|
|
78
|
+
/**
|
|
79
|
+
* Thin an ascending run of **session opens** down to about `count` axis ticks by
|
|
80
|
+
* **calendar grain** — the trading-terminal habit of labelling week / month /
|
|
81
|
+
* year starts rather than an arbitrary every-nth session. Picks the finest grain
|
|
82
|
+
* on the ladder (session → week → month → quarter → year) that yields at most
|
|
83
|
+
* `count` buckets and returns the first open in each; beyond yearly it decimates
|
|
84
|
+
* every-nth so the axis never crowds. Exported so the container can draw session
|
|
85
|
+
* dividers at the same instants the axis labels.
|
|
86
|
+
*/
|
|
87
|
+
export declare function coarsenCalendar(opens: readonly number[], count: number): {
|
|
88
|
+
ticks: number[];
|
|
89
|
+
granularity: TickGranularity;
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Build a {@link TradingTimeScale} over the given discontinuity `provider`.
|
|
93
|
+
* Configure like a d3 scale: `scaleTradingTime(provider).domain([t0, t1]).range([0, width])`.
|
|
94
|
+
*/
|
|
95
|
+
export declare function scaleTradingTime(provider: DiscontinuityProvider): TradingTimeScale;
|
|
96
|
+
export {};
|
|
97
|
+
//# sourceMappingURL=tradingTimeScale.d.ts.map
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { scaleTime } from 'd3-scale';
|
|
2
|
+
/**
|
|
3
|
+
* The local-time bucket key for `t` at grain `g` — two instants in the same
|
|
4
|
+
* week / month / quarter / year share a key. Local time (not UTC) so it agrees
|
|
5
|
+
* with the local `scaleTime` label formatter; the exchange's own time zone is
|
|
6
|
+
* unknown to the scale (the deferred refinement), and a session open sits well
|
|
7
|
+
* inside its local day, so runtime-local grouping matches the exchange day in
|
|
8
|
+
* every ordinary case.
|
|
9
|
+
*/
|
|
10
|
+
function bucketKey(t, g) {
|
|
11
|
+
if (g === 'session')
|
|
12
|
+
return t; // every open its own bucket
|
|
13
|
+
const d = new Date(t);
|
|
14
|
+
switch (g) {
|
|
15
|
+
case 'week': {
|
|
16
|
+
const dow = (d.getDay() + 6) % 7; // 0 = Monday
|
|
17
|
+
// Local midnight of this week's Monday (Date normalizes a negative date).
|
|
18
|
+
return new Date(d.getFullYear(), d.getMonth(), d.getDate() - dow).getTime();
|
|
19
|
+
}
|
|
20
|
+
case 'month':
|
|
21
|
+
return d.getFullYear() * 12 + d.getMonth();
|
|
22
|
+
case 'quarter':
|
|
23
|
+
return d.getFullYear() * 4 + Math.floor(d.getMonth() / 3);
|
|
24
|
+
case 'year':
|
|
25
|
+
return d.getFullYear();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** The first instant of each distinct `g`-bucket in the ascending list `opens`. */
|
|
29
|
+
function firstOfEachBucket(opens, g) {
|
|
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',
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Build a {@link TradingTimeScale} over the given discontinuity `provider`.
|
|
74
|
+
* Configure like a d3 scale: `scaleTradingTime(provider).domain([t0, t1]).range([0, width])`.
|
|
75
|
+
*/
|
|
76
|
+
export function scaleTradingTime(provider) {
|
|
77
|
+
let domain = [0, 1];
|
|
78
|
+
let range = [0, 1];
|
|
79
|
+
// A private d3 time scale, kept in sync with the domain, purely for tickFormat.
|
|
80
|
+
const base = scaleTime();
|
|
81
|
+
const totalLive = () => provider.distance(domain[0], domain[1]);
|
|
82
|
+
const scale = ((value) => {
|
|
83
|
+
const live = totalLive();
|
|
84
|
+
const span = range[1] - range[0];
|
|
85
|
+
if (live === 0)
|
|
86
|
+
return range[0];
|
|
87
|
+
return range[0] + (provider.distance(domain[0], value) / live) * span;
|
|
88
|
+
});
|
|
89
|
+
scale.invert = (pixel) => {
|
|
90
|
+
const span = range[1] - range[0];
|
|
91
|
+
const frac = span === 0 ? 0 : (pixel - range[0]) / span;
|
|
92
|
+
return provider.offset(domain[0], frac * totalLive());
|
|
93
|
+
};
|
|
94
|
+
/** The session-open instants in the domain — the first session's open (the
|
|
95
|
+
* left edge) plus each collapsed-gap boundary — the axis's date anchors. */
|
|
96
|
+
const sessionOpens = () => {
|
|
97
|
+
const bounds = provider.boundaries?.(domain[0], domain[1]) ?? [];
|
|
98
|
+
return [domain[0], ...bounds];
|
|
99
|
+
};
|
|
100
|
+
scale.ticks = (count = 10) => {
|
|
101
|
+
const live = totalLive();
|
|
102
|
+
if (live <= 0 || count < 1)
|
|
103
|
+
return [domain[0]];
|
|
104
|
+
const opens = sessionOpens();
|
|
105
|
+
// Calendar-aware: label the session opens (each a new day), thinned to
|
|
106
|
+
// week / month / year starts by grain rather than an arbitrary every-nth
|
|
107
|
+
// session — the trading-terminal look.
|
|
108
|
+
if (opens.length > 1)
|
|
109
|
+
return coarsenCalendar(opens, count).ticks;
|
|
110
|
+
// No boundaries (a single session / no calendar): fall back to interior
|
|
111
|
+
// even-spaced ticks — endpoints excluded so none sits on the plot edge.
|
|
112
|
+
const out = [];
|
|
113
|
+
for (let i = 1; i < count; i++) {
|
|
114
|
+
out.push(provider.offset(domain[0], (i / count) * live));
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
};
|
|
118
|
+
scale.tickFormat = (count = 10, specifier) => {
|
|
119
|
+
if (specifier !== undefined)
|
|
120
|
+
return base.tickFormat(count, specifier);
|
|
121
|
+
const opens = sessionOpens();
|
|
122
|
+
const defFmt = base.tickFormat(count);
|
|
123
|
+
if (opens.length <= 1)
|
|
124
|
+
return defFmt; // no calendar → d3 multi-scale default
|
|
125
|
+
// Anchor label at each coarsened session-open tick: a **date** (`%b %d`), or
|
|
126
|
+
// the **year** when ticks are a year apart (a plain date would drop the year
|
|
127
|
+
// the reader needs). Any other instant — a cursor readout — uses the d3
|
|
128
|
+
// multi-scale default. Same grain as {@link ticks}, so labels and the
|
|
129
|
+
// dividers drawn at these instants agree.
|
|
130
|
+
const { ticks, granularity } = coarsenCalendar(opens, count);
|
|
131
|
+
const anchors = new Set(ticks);
|
|
132
|
+
const anchorFmt = base.tickFormat(count, granularity === 'year' ? '%Y' : '%b %d');
|
|
133
|
+
return (d) => (anchors.has(+d) ? anchorFmt(d) : defFmt(d));
|
|
134
|
+
};
|
|
135
|
+
function domainFn(next) {
|
|
136
|
+
if (next === undefined)
|
|
137
|
+
return [domain[0], domain[1]];
|
|
138
|
+
domain = [next[0], next[1]];
|
|
139
|
+
return scale;
|
|
140
|
+
}
|
|
141
|
+
scale.domain = domainFn;
|
|
142
|
+
function rangeFn(next) {
|
|
143
|
+
if (next === undefined)
|
|
144
|
+
return [range[0], range[1]];
|
|
145
|
+
range = [next[0], next[1]];
|
|
146
|
+
return scale;
|
|
147
|
+
}
|
|
148
|
+
scale.range = rangeFn;
|
|
149
|
+
scale.copy = () => scaleTradingTime(provider.copy()).domain(domain).range(range);
|
|
150
|
+
return scale;
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=tradingTimeScale.js.map
|
package/dist/viewport.d.ts
CHANGED
|
@@ -17,4 +17,27 @@ export declare function panRange(range: TimeRange, dt: number): [number, number]
|
|
|
17
17
|
* pivot keeps its fractional position in the window.
|
|
18
18
|
*/
|
|
19
19
|
export declare function zoomRange(range: TimeRange, pivot: number, factor: number, minDuration?: number): [number, number];
|
|
20
|
+
/**
|
|
21
|
+
* The slice of a discontinuity provider the trading-time viewport math needs —
|
|
22
|
+
* a structural subset of the charts `DiscontinuityProvider` (so `viewport.ts`
|
|
23
|
+
* stays free of any provider dependency).
|
|
24
|
+
*/
|
|
25
|
+
export interface ViewportDiscontinuity {
|
|
26
|
+
distance(from: number, to: number): number;
|
|
27
|
+
offset(value: number, amount: number): number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Pan a range on a **trading-time** axis: shift both endpoints by the same
|
|
31
|
+
* amount of *trading* time, so the pan feels uniform on screen even across
|
|
32
|
+
* collapsed gaps (a raw-ms shift would jump at each weekend/holiday). `fraction`
|
|
33
|
+
* is the signed share of the plot width dragged — the caller passes `-dx/plotWidth`
|
|
34
|
+
* (drag right → reveal earlier data → negative).
|
|
35
|
+
*/
|
|
36
|
+
export declare function panRangeTrading(range: TimeRange, fraction: number, provider: ViewportDiscontinuity): [number, number];
|
|
37
|
+
/**
|
|
38
|
+
* Zoom a **trading-time** range around `pivot` by `factor` (`< 1` in, `> 1` out),
|
|
39
|
+
* scaling the *trading* distance from the pivot to each endpoint so the pivot's
|
|
40
|
+
* on-screen position holds. Floors the visible trading time at `minLive`.
|
|
41
|
+
*/
|
|
42
|
+
export declare function zoomRangeTrading(range: TimeRange, pivot: number, factor: number, provider: ViewportDiscontinuity, minLive?: number): [number, number];
|
|
20
43
|
//# sourceMappingURL=viewport.d.ts.map
|
package/dist/viewport.js
CHANGED
|
@@ -27,4 +27,55 @@ export function zoomRange(range, pivot, factor, minDuration = 1) {
|
|
|
27
27
|
const frac = span > 0 ? (pivot - range[0]) / span : 0.5;
|
|
28
28
|
return [pivot - minDuration * frac, pivot + minDuration * (1 - frac)];
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Pan a range on a **trading-time** axis: shift both endpoints by the same
|
|
32
|
+
* amount of *trading* time, so the pan feels uniform on screen even across
|
|
33
|
+
* collapsed gaps (a raw-ms shift would jump at each weekend/holiday). `fraction`
|
|
34
|
+
* is the signed share of the plot width dragged — the caller passes `-dx/plotWidth`
|
|
35
|
+
* (drag right → reveal earlier data → negative).
|
|
36
|
+
*/
|
|
37
|
+
export function panRangeTrading(range, fraction, provider) {
|
|
38
|
+
const span = provider.distance(range[0], range[1]);
|
|
39
|
+
const shift = fraction * span;
|
|
40
|
+
// Anchor on the endpoint being pushed toward its boundary and rebuild the
|
|
41
|
+
// other from the preserved span, so panning into *either* calendar edge stops
|
|
42
|
+
// (the window holds its trading width) rather than shrinking or collapsing.
|
|
43
|
+
if (shift <= 0) {
|
|
44
|
+
const start = provider.offset(range[0], shift);
|
|
45
|
+
return [start, provider.offset(start, span)];
|
|
46
|
+
}
|
|
47
|
+
const end = provider.offset(range[1], shift);
|
|
48
|
+
return [provider.offset(end, -span), end];
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Zoom a **trading-time** range around `pivot` by `factor` (`< 1` in, `> 1` out),
|
|
52
|
+
* scaling the *trading* distance from the pivot to each endpoint so the pivot's
|
|
53
|
+
* on-screen position holds. Floors the visible trading time at `minLive`.
|
|
54
|
+
*/
|
|
55
|
+
export function zoomRangeTrading(range, pivot, factor, provider, minLive = 1) {
|
|
56
|
+
const left = provider.distance(range[0], pivot); // trading-ms d0 → pivot (≥ 0)
|
|
57
|
+
const right = provider.distance(pivot, range[1]); // trading-ms pivot → d1 (≥ 0)
|
|
58
|
+
let nl = left * factor;
|
|
59
|
+
let nr = right * factor;
|
|
60
|
+
if (nl + nr < minLive) {
|
|
61
|
+
const total = left + right;
|
|
62
|
+
const frac = total > 0 ? left / total : 0.5;
|
|
63
|
+
nl = minLive * frac;
|
|
64
|
+
nr = minLive * (1 - frac);
|
|
65
|
+
}
|
|
66
|
+
let d0 = provider.offset(pivot, -nl);
|
|
67
|
+
let d1 = provider.offset(pivot, nr);
|
|
68
|
+
// If one side clamped at a calendar edge (couldn't extend as far as asked),
|
|
69
|
+
// give the shortfall to the other side so the visible trading span — and the
|
|
70
|
+
// `minLive` floor — is preserved. (The pivot's *fraction* can then drift at
|
|
71
|
+
// the edge: there is no trading time before the first / after the last session
|
|
72
|
+
// to hold it against.)
|
|
73
|
+
const shortLeft = nl - provider.distance(d0, pivot);
|
|
74
|
+
const shortRight = nr - provider.distance(pivot, d1);
|
|
75
|
+
if (shortLeft > 0)
|
|
76
|
+
d1 = provider.offset(pivot, nr + shortLeft);
|
|
77
|
+
else if (shortRight > 0)
|
|
78
|
+
d0 = provider.offset(pivot, -(nl + shortRight));
|
|
79
|
+
return [d0, d1];
|
|
80
|
+
}
|
|
30
81
|
//# sourceMappingURL=viewport.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pond-ts/charts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.42.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.42.0",
|
|
42
|
+
"pond-ts": "^0.42.0",
|
|
43
43
|
"react": "^18.0.0 || ^19.0.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|