@funnest/charts 0.4.0 → 0.5.1
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/dist/chart.d.ts +31 -0
- package/dist/index.js +104 -15
- package/dist/render/renderer.d.ts +2 -1
- package/dist/settings.d.ts +2 -0
- package/package.json +1 -1
package/dist/chart.d.ts
CHANGED
|
@@ -141,6 +141,8 @@ export declare class Chart {
|
|
|
141
141
|
private priceAxisWidth;
|
|
142
142
|
private frame;
|
|
143
143
|
private crosshair;
|
|
144
|
+
/** Time (UTC ms) of a crosshair mirrored from a linked chart; see setSyncedCrosshair(). */
|
|
145
|
+
private syncedTime;
|
|
144
146
|
private lastCrosshairKey;
|
|
145
147
|
private lastRangeKey;
|
|
146
148
|
private lastLayoutKey;
|
|
@@ -155,6 +157,9 @@ export declare class Chart {
|
|
|
155
157
|
private unsubscribeBars;
|
|
156
158
|
private unsubscribeQuotes;
|
|
157
159
|
private latestQuote;
|
|
160
|
+
/** Symbol the quote stream belongs to; kept across interval changes of the same symbol. */
|
|
161
|
+
private quoteSymbol;
|
|
162
|
+
private quoteToken;
|
|
158
163
|
private connectionState;
|
|
159
164
|
private readonly displayTimezone;
|
|
160
165
|
private loadingHistory;
|
|
@@ -193,6 +198,30 @@ export declare class Chart {
|
|
|
193
198
|
scrollBars(bars: number): void;
|
|
194
199
|
/** True when the newest bar is visible. */
|
|
195
200
|
isAtRealtime(): boolean;
|
|
201
|
+
/**
|
|
202
|
+
* Mirrors another chart's crosshair: a vertical line at `time` (UTC ms) on the bar that
|
|
203
|
+
* contains it, with the time badge but no price line. `crosshairMove` fires with
|
|
204
|
+
* `source: 'sync'`, so legends follow along. A pointer on this chart takes precedence.
|
|
205
|
+
* Pass null to clear.
|
|
206
|
+
*/
|
|
207
|
+
setSyncedCrosshair(time: number | null): void;
|
|
208
|
+
/**
|
|
209
|
+
* The time span (UTC ms) between the plot's left and right edges. Edges past the data
|
|
210
|
+
* (the empty space right of the last bar) are extrapolated by the bar interval.
|
|
211
|
+
*/
|
|
212
|
+
getVisibleTimeRange(): {
|
|
213
|
+
from: number;
|
|
214
|
+
to: number;
|
|
215
|
+
} | null;
|
|
216
|
+
/** Scrolls and zooms so the plot spans `from`…`to` (UTC ms), e.g. to match a linked chart. */
|
|
217
|
+
setVisibleTimeRange(range: {
|
|
218
|
+
from: number;
|
|
219
|
+
to: number;
|
|
220
|
+
}): void;
|
|
221
|
+
/** Fractional bar index → time, interpolating between bars and extrapolating past either end. */
|
|
222
|
+
private indexToTime;
|
|
223
|
+
/** Inverse of indexToTime(). */
|
|
224
|
+
private timeToIndex;
|
|
196
225
|
/** Snapshot of the chart (without the crosshair) at device resolution. */
|
|
197
226
|
takeScreenshot(): HTMLCanvasElement;
|
|
198
227
|
/**
|
|
@@ -286,6 +315,8 @@ export declare class Chart {
|
|
|
286
315
|
private afterFrame;
|
|
287
316
|
private requestHistory;
|
|
288
317
|
private resolveCrosshair;
|
|
318
|
+
/** The bar containing `time`, as a crosshair without a price (nothing when it's off the data). */
|
|
319
|
+
private resolveSyncedCrosshair;
|
|
289
320
|
private paneAt;
|
|
290
321
|
private setTooltip;
|
|
291
322
|
private executionTooltip;
|
package/dist/index.js
CHANGED
|
@@ -1681,6 +1681,7 @@ var DEFAULT_CHART_SETTINGS = {
|
|
|
1681
1681
|
gridHorizontal: true,
|
|
1682
1682
|
gridVertical: true,
|
|
1683
1683
|
volume: true,
|
|
1684
|
+
navigationButtons: true,
|
|
1684
1685
|
upColor: null,
|
|
1685
1686
|
downColor: null
|
|
1686
1687
|
};
|
|
@@ -2239,6 +2240,17 @@ function drawOverlay(layer, m, ch, tooltip = null) {
|
|
|
2239
2240
|
if (tooltip) drawTooltip(p, f, tooltip);
|
|
2240
2241
|
if (!ch) return;
|
|
2241
2242
|
p.vLine(ch.x, 0, f.panesHeight, pal.crosshair, DASH);
|
|
2243
|
+
if (ch.source !== "sync") drawCrosshairValue(p, m, ch);
|
|
2244
|
+
const timeLabel = m.formatCrosshairTime(ch.time);
|
|
2245
|
+
const w = p.measure(timeLabel) + 16;
|
|
2246
|
+
const cx = Math.min(f.plotWidth - w / 2, Math.max(w / 2, ch.x));
|
|
2247
|
+
p.roundRect(cx - w / 2, f.panesHeight + 4, w, f.timeAxisHeight - 8, 4, pal.text);
|
|
2248
|
+
p.text(timeLabel, cx, f.panesHeight + f.timeAxisHeight / 2, pal.background, "center");
|
|
2249
|
+
}
|
|
2250
|
+
function drawCrosshairValue(p, m, ch) {
|
|
2251
|
+
const f = m.frame;
|
|
2252
|
+
const pal = f.palette;
|
|
2253
|
+
const ctx = p.ctx;
|
|
2242
2254
|
p.hLine(ch.y, 0, f.plotWidth, pal.crosshair, DASH);
|
|
2243
2255
|
if (ch.source === "touch") {
|
|
2244
2256
|
ctx.beginPath();
|
|
@@ -2255,11 +2267,6 @@ function drawOverlay(layer, m, ch, tooltip = null) {
|
|
|
2255
2267
|
const by = Math.min(f.panesHeight - BADGE_HEIGHT / 2, Math.max(BADGE_HEIGHT / 2, ch.y));
|
|
2256
2268
|
p.roundRect(f.plotWidth + 2, by - BADGE_HEIGHT / 2, f.width - f.plotWidth - 4, BADGE_HEIGHT, 4, pal.text);
|
|
2257
2269
|
p.text(valueLabel, f.plotWidth + 8, by, pal.background);
|
|
2258
|
-
const timeLabel = m.formatCrosshairTime(ch.time);
|
|
2259
|
-
const w = p.measure(timeLabel) + 16;
|
|
2260
|
-
const cx = Math.min(f.plotWidth - w / 2, Math.max(w / 2, ch.x));
|
|
2261
|
-
p.roundRect(cx - w / 2, f.panesHeight + 4, w, f.timeAxisHeight - 8, 4, pal.text);
|
|
2262
|
-
p.text(timeLabel, cx, f.panesHeight + f.timeAxisHeight / 2, pal.background, "center");
|
|
2263
2270
|
}
|
|
2264
2271
|
|
|
2265
2272
|
// src/resolution.ts
|
|
@@ -2385,6 +2392,8 @@ var Chart = class {
|
|
|
2385
2392
|
priceAxisWidth = MIN_PRICE_AXIS_WIDTH;
|
|
2386
2393
|
frame = null;
|
|
2387
2394
|
crosshair = null;
|
|
2395
|
+
/** Time (UTC ms) of a crosshair mirrored from a linked chart; see setSyncedCrosshair(). */
|
|
2396
|
+
syncedTime = null;
|
|
2388
2397
|
lastCrosshairKey = "";
|
|
2389
2398
|
lastRangeKey = "";
|
|
2390
2399
|
lastLayoutKey = "";
|
|
@@ -2399,6 +2408,9 @@ var Chart = class {
|
|
|
2399
2408
|
unsubscribeBars = null;
|
|
2400
2409
|
unsubscribeQuotes = null;
|
|
2401
2410
|
latestQuote = null;
|
|
2411
|
+
/** Symbol the quote stream belongs to; kept across interval changes of the same symbol. */
|
|
2412
|
+
quoteSymbol = null;
|
|
2413
|
+
quoteToken = 0;
|
|
2402
2414
|
connectionState = null;
|
|
2403
2415
|
displayTimezone;
|
|
2404
2416
|
loadingHistory = false;
|
|
@@ -2501,9 +2513,14 @@ var Chart = class {
|
|
|
2501
2513
|
const token = ++this.loadToken;
|
|
2502
2514
|
this.unsubscribeBars?.();
|
|
2503
2515
|
this.unsubscribeBars = null;
|
|
2504
|
-
this.unsubscribeQuotes
|
|
2505
|
-
|
|
2506
|
-
|
|
2516
|
+
const keepQuotes = this.unsubscribeQuotes !== null && this.quoteSymbol === symbol;
|
|
2517
|
+
if (!keepQuotes) {
|
|
2518
|
+
this.quoteToken++;
|
|
2519
|
+
this.unsubscribeQuotes?.();
|
|
2520
|
+
this.unsubscribeQuotes = null;
|
|
2521
|
+
this.latestQuote = null;
|
|
2522
|
+
this.quoteSymbol = null;
|
|
2523
|
+
}
|
|
2507
2524
|
this.emitter.emit("loadingChange", true);
|
|
2508
2525
|
let info;
|
|
2509
2526
|
let result;
|
|
@@ -2550,11 +2567,15 @@ var Chart = class {
|
|
|
2550
2567
|
this.emitter.emit("connectionChange", state);
|
|
2551
2568
|
}
|
|
2552
2569
|
);
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
this.
|
|
2556
|
-
this.
|
|
2557
|
-
|
|
2570
|
+
if (!keepQuotes) {
|
|
2571
|
+
const quoteToken = ++this.quoteToken;
|
|
2572
|
+
this.quoteSymbol = symbol;
|
|
2573
|
+
this.unsubscribeQuotes = feed.subscribeQuotes?.(info, (quote) => {
|
|
2574
|
+
if (quoteToken !== this.quoteToken) return;
|
|
2575
|
+
this.latestQuote = { ...this.latestQuote, ...quote };
|
|
2576
|
+
this.emitter.emit("quoteChange", this.latestQuote);
|
|
2577
|
+
}) ?? null;
|
|
2578
|
+
}
|
|
2558
2579
|
this.emitter.emit("symbolChange", info);
|
|
2559
2580
|
this.stateChanged();
|
|
2560
2581
|
}
|
|
@@ -2611,6 +2632,61 @@ var Chart = class {
|
|
|
2611
2632
|
isAtRealtime() {
|
|
2612
2633
|
return this.store.length === 0 || this.timeScale.visibleRange().to >= this.store.length - 1;
|
|
2613
2634
|
}
|
|
2635
|
+
/**
|
|
2636
|
+
* Mirrors another chart's crosshair: a vertical line at `time` (UTC ms) on the bar that
|
|
2637
|
+
* contains it, with the time badge but no price line. `crosshairMove` fires with
|
|
2638
|
+
* `source: 'sync'`, so legends follow along. A pointer on this chart takes precedence.
|
|
2639
|
+
* Pass null to clear.
|
|
2640
|
+
*/
|
|
2641
|
+
setSyncedCrosshair(time) {
|
|
2642
|
+
if (time === this.syncedTime) return;
|
|
2643
|
+
this.syncedTime = time;
|
|
2644
|
+
this.invalidate(false);
|
|
2645
|
+
}
|
|
2646
|
+
/**
|
|
2647
|
+
* The time span (UTC ms) between the plot's left and right edges. Edges past the data
|
|
2648
|
+
* (the empty space right of the last bar) are extrapolated by the bar interval.
|
|
2649
|
+
*/
|
|
2650
|
+
getVisibleTimeRange() {
|
|
2651
|
+
const ts = this.timeScale;
|
|
2652
|
+
if (this.store.length === 0 || ts.width <= 0) return null;
|
|
2653
|
+
return { from: this.indexToTime(ts.xToIndexFloat(0)), to: this.indexToTime(ts.xToIndexFloat(ts.width)) };
|
|
2654
|
+
}
|
|
2655
|
+
/** Scrolls and zooms so the plot spans `from`…`to` (UTC ms), e.g. to match a linked chart. */
|
|
2656
|
+
setVisibleTimeRange(range) {
|
|
2657
|
+
const ts = this.timeScale;
|
|
2658
|
+
if (this.store.length === 0 || ts.width <= 0 || !(range.to > range.from)) return;
|
|
2659
|
+
const from = this.timeToIndex(range.from);
|
|
2660
|
+
const to = this.timeToIndex(range.to);
|
|
2661
|
+
if (!(to > from)) return;
|
|
2662
|
+
ts.barSpacing = ts.width / (to - from);
|
|
2663
|
+
ts.rightOffset = to - 0.5 - (this.store.length - 1);
|
|
2664
|
+
ts.clamp();
|
|
2665
|
+
this.invalidate();
|
|
2666
|
+
}
|
|
2667
|
+
/** Fractional bar index → time, interpolating between bars and extrapolating past either end. */
|
|
2668
|
+
indexToTime(index) {
|
|
2669
|
+
const bars = this.store.bars;
|
|
2670
|
+
const last = bars.length - 1;
|
|
2671
|
+
const step = this.barInterval();
|
|
2672
|
+
if (index <= 0) return bars[0].time + index * step;
|
|
2673
|
+
if (index >= last) return bars[last].time + (index - last) * step;
|
|
2674
|
+
const i = Math.floor(index);
|
|
2675
|
+
const a = bars[i].time;
|
|
2676
|
+
return a + (bars[i + 1].time - a) * (index - i);
|
|
2677
|
+
}
|
|
2678
|
+
/** Inverse of indexToTime(). */
|
|
2679
|
+
timeToIndex(time) {
|
|
2680
|
+
const bars = this.store.bars;
|
|
2681
|
+
const last = bars.length - 1;
|
|
2682
|
+
const step = this.barInterval();
|
|
2683
|
+
if (time <= bars[0].time) return (time - bars[0].time) / step;
|
|
2684
|
+
if (time >= bars[last].time) return last + (time - bars[last].time) / step;
|
|
2685
|
+
const i = this.store.indexAtOrBefore(time);
|
|
2686
|
+
const a = bars[i].time;
|
|
2687
|
+
const b = bars[i + 1].time;
|
|
2688
|
+
return i + (b > a ? (time - a) / (b - a) : 0);
|
|
2689
|
+
}
|
|
2614
2690
|
/** Snapshot of the chart (without the crosshair) at device resolution. */
|
|
2615
2691
|
takeScreenshot() {
|
|
2616
2692
|
if (this.rafId || this.mainDirty) this.render();
|
|
@@ -3232,8 +3308,9 @@ var Chart = class {
|
|
|
3232
3308
|
resolveCrosshair(frame) {
|
|
3233
3309
|
const ch = this.crosshair;
|
|
3234
3310
|
const bars = this.store.bars;
|
|
3235
|
-
if (
|
|
3236
|
-
|
|
3311
|
+
if (bars.length === 0) return null;
|
|
3312
|
+
const outside = ch?.source === "mouse" && (ch.x < 0 || ch.x > frame.plotWidth || ch.y < 0 || ch.y > frame.panesHeight);
|
|
3313
|
+
if (!ch || outside) return this.syncedTime === null ? null : this.resolveSyncedCrosshair(this.syncedTime);
|
|
3237
3314
|
const index = Math.min(bars.length - 1, Math.max(0, this.timeScale.xToIndex(ch.x)));
|
|
3238
3315
|
const bar = bars[index];
|
|
3239
3316
|
const x = this.timeScale.indexToX(index);
|
|
@@ -3243,6 +3320,18 @@ var Chart = class {
|
|
|
3243
3320
|
const value = pane.kind === "main" ? roundToTick(raw, this.tickSize) : raw;
|
|
3244
3321
|
return { index, time: bar.time, bar, value, pane: pane.id, x, y, source: ch.source };
|
|
3245
3322
|
}
|
|
3323
|
+
/** The bar containing `time`, as a crosshair without a price (nothing when it's off the data). */
|
|
3324
|
+
resolveSyncedCrosshair(time) {
|
|
3325
|
+
const bars = this.store.bars;
|
|
3326
|
+
const last = bars[bars.length - 1];
|
|
3327
|
+
if (time < bars[0].time || time >= last.time + this.barInterval()) return null;
|
|
3328
|
+
const index = this.store.indexAtOrBefore(time);
|
|
3329
|
+
const bar = bars[index];
|
|
3330
|
+
if (!bar) return null;
|
|
3331
|
+
const x = this.timeScale.indexToX(index);
|
|
3332
|
+
if (x < 0 || x > this.timeScale.width) return null;
|
|
3333
|
+
return { index, time: bar.time, bar, value: bar.close, pane: "main", x, y: -1, source: "sync" };
|
|
3334
|
+
}
|
|
3246
3335
|
paneAt(frame, y) {
|
|
3247
3336
|
return frame.panes.find((p) => y >= p.top && y < p.top + p.height) ?? frame.panes[frame.panes.length - 1];
|
|
3248
3337
|
}
|
|
@@ -67,7 +67,8 @@ export interface CrosshairInfo {
|
|
|
67
67
|
pane: PaneId;
|
|
68
68
|
x: number;
|
|
69
69
|
y: number;
|
|
70
|
-
|
|
70
|
+
/** 'sync': mirrored from a linked chart (setSyncedCrosshair) — time only, `y` is -1. */
|
|
71
|
+
source: 'mouse' | 'touch' | 'sync';
|
|
71
72
|
}
|
|
72
73
|
/** Fill details shown next to an execution marker. */
|
|
73
74
|
export interface Tooltip {
|
package/dist/settings.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export interface ChartSettings {
|
|
|
12
12
|
gridVertical: boolean;
|
|
13
13
|
/** Volume pane under the price pane. */
|
|
14
14
|
volume: boolean;
|
|
15
|
+
/** Zoom/scroll buttons at the bottom center of the chart (@funnest/charts-ui). */
|
|
16
|
+
navigationButtons: boolean;
|
|
15
17
|
/** Up/down candle colors; null follows the theme. */
|
|
16
18
|
upColor: string | null;
|
|
17
19
|
downColor: string | null;
|