@luxalgo/vela 0.7.8 → 0.8.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/NOTICE +28 -28
- package/dist/{DataProvider-CGa4KHRa.d.cts → DataProvider-BrlxntPN.d.cts} +1 -1
- package/dist/{DataProvider-CmWUtDVL.d.ts → DataProvider-DwFIoV4r.d.ts} +1 -1
- package/dist/{chunk-WTWGEB7K.js → chunk-2IAETM2I.js} +260 -2
- package/dist/{chunk-CAFCLMPF.js → chunk-BZQM2XO7.js} +1 -0
- package/dist/{chunk-M6GP7DAX.js → chunk-E3GU6AUI.js} +459 -37
- package/dist/{chunk-BFA32GOU.js → chunk-EZ5FWVLA.js} +5 -2
- package/dist/{chunk-PVGWNF7V.js → chunk-KA77UTRF.js} +2 -2
- package/dist/{chunk-N4ZZYLMP.js → chunk-QBPUT5SU.js} +686 -251
- package/dist/{contributions-DF_Ea07m.d.cts → contributions-CtlptxR8.d.cts} +187 -5
- package/dist/{contributions-nKOSeLy8.d.ts → contributions-tjNExo1o.d.ts} +187 -5
- package/dist/index.cjs +966 -247
- package/dist/index.d.cts +24 -8
- package/dist/index.d.ts +24 -8
- package/dist/index.js +28 -4
- package/dist/{options-D1AJKsn_.d.ts → options-BnEPUuVV.d.cts} +1 -1
- package/dist/{options-D1AJKsn_.d.cts → options-BnEPUuVV.d.ts} +1 -1
- package/dist/{plugin-6F04gjQe.d.cts → plugin-BWxJRKzS.d.cts} +10 -4
- package/dist/{plugin-BLsNipkc.d.ts → plugin-e2WUEtiI.d.ts} +10 -4
- package/dist/plugin.cjs +1 -0
- package/dist/plugin.d.cts +4 -4
- package/dist/plugin.d.ts +4 -4
- package/dist/plugin.js +2 -2
- package/dist/providers/binance.d.cts +2 -2
- package/dist/providers/binance.d.ts +2 -2
- package/dist/providers/coinbase.d.cts +2 -2
- package/dist/providers/coinbase.d.ts +2 -2
- package/dist/providers/hyperliquid.d.cts +2 -2
- package/dist/providers/hyperliquid.d.ts +2 -2
- package/dist/{statusline-model-DAFyqVmR.d.ts → statusline-model-DFY7tuHj.d.cts} +5 -3
- package/dist/{statusline-model-BoWJ3cWQ.d.cts → statusline-model-zKXtGIjj.d.ts} +5 -3
- package/dist/ui.cjs +263 -0
- package/dist/ui.d.cts +75 -2
- package/dist/ui.d.ts +75 -2
- package/dist/ui.js +3 -3
- package/dist/vela.global.js +968 -249
- package/dist/vela.global.min.js +73 -71
- package/dist/widget.cjs +1411 -292
- package/dist/widget.d.cts +26 -7
- package/dist/widget.d.ts +26 -7
- package/dist/widget.js +7 -7
- package/dist/workspace.cjs +1414 -292
- package/dist/workspace.d.cts +124 -6
- package/dist/workspace.d.ts +124 -6
- package/dist/workspace.js +6 -6
- package/package.json +1 -1
package/dist/vela.global.js
CHANGED
|
@@ -591,6 +591,7 @@ var Vela = (function (exports) {
|
|
|
591
591
|
registerIcon("chevron-up", S('<path d="M3.5 10 8 5.5 12.5 10"/>'));
|
|
592
592
|
registerIcon("chevrons-right", S('<path d="m4 3.5 4.5 4.5L4 12.5"/><path d="m8.5 3.5 4.5 4.5-4.5 4.5"/>'));
|
|
593
593
|
registerIcon("chevrons-left", S('<path d="M12 3.5 7.5 8l4.5 4.5"/><path d="M7.5 3.5 3 8l4.5 4.5"/>'));
|
|
594
|
+
registerIcon("replay", S('<path d="M14 4.2v7.6L8.6 8z"/><path d="M7.4 4.2v7.6L2 8z"/>'));
|
|
594
595
|
registerIcon("check", S('<path d="m2.8 8.4 3.4 3.4 7-7.6"/>'));
|
|
595
596
|
registerIcon("close", S('<path d="m3.8 3.8 8.4 8.4M12.2 3.8l-8.4 8.4"/>'));
|
|
596
597
|
registerIcon("pin", S('<path d="M5.6 2.5h4.8M6.4 2.5v3.6L4.4 8.4v.9h7.2v-.9L9.6 6.1V2.5M8 9.3v4.2"/>'));
|
|
@@ -7808,6 +7809,7 @@ var Vela = (function (exports) {
|
|
|
7808
7809
|
var FIRST_PAINT_BARS = 20;
|
|
7809
7810
|
var GAP_FACTOR = 1.5;
|
|
7810
7811
|
var HEAL_COOLDOWN_MS = 5e3;
|
|
7812
|
+
var MIN_TICK_UPDATE_MS = 16;
|
|
7811
7813
|
var EngineOrchestrator = class _EngineOrchestrator {
|
|
7812
7814
|
constructor(container, renderer, feed, engines, config, dataControl) {
|
|
7813
7815
|
this.renderer = renderer;
|
|
@@ -7860,6 +7862,32 @@ var Vela = (function (exports) {
|
|
|
7860
7862
|
this.lastHealAt = 0;
|
|
7861
7863
|
/** The `visibilitychange` catch-up listener (removed on destroy). */
|
|
7862
7864
|
this.onVisible = null;
|
|
7865
|
+
// ── bar replay: `rawBars` holds the history up to the cursor, the rest waits here ──
|
|
7866
|
+
/** Bars still hidden right of the replay cursor, oldest first; null ≡ replay off. */
|
|
7867
|
+
this.replayQueue = null;
|
|
7868
|
+
this.replayTimer = null;
|
|
7869
|
+
this.replayPlaying = false;
|
|
7870
|
+
this.replayIntervalMs = 1e3;
|
|
7871
|
+
/** The depth (`market.bars`) before a replay deepened the history — restored when it ends. */
|
|
7872
|
+
this.replayDepth = null;
|
|
7873
|
+
/** A start is loading older history before it can cut; `seek` = a replay was running
|
|
7874
|
+
* (and left silently to deepen), so abandoning the load must announce its end. */
|
|
7875
|
+
this.replayLoading = null;
|
|
7876
|
+
/** A replay carried over a timeframe/session switch, between the old series' silent end
|
|
7877
|
+
* and the cut of the new one: still ACTIVE for the outside (`replayState`), resumed
|
|
7878
|
+
* at `revealedEnd` once the new bars land, unless stopped (`cancelled`) meanwhile. */
|
|
7879
|
+
this.replayCarrying = null;
|
|
7880
|
+
/** Intrabar prices for the bars replay reveals — null ≡ whole bars. */
|
|
7881
|
+
this.replayTickSource = null;
|
|
7882
|
+
/** The bar being revealed tick by tick: its stored form (what it settles on), its ticks,
|
|
7883
|
+
* the next tick to apply, the candle built so far, the volume each tick adds when the
|
|
7884
|
+
* ticks carry none, and its pacing (ticks per update, ms between updates — one play
|
|
7885
|
+
* interval per tick — and the clock time tick 0 is due at, so late timers catch up). */
|
|
7886
|
+
this.replayForming = null;
|
|
7887
|
+
/** Tick requests by bar time, shared by prefetch and playback; `ticks` once they landed. */
|
|
7888
|
+
this.replayTickFetches = /* @__PURE__ */ new Map();
|
|
7889
|
+
/** Aborts the tick requests in flight — renewed whenever the replay moves on. */
|
|
7890
|
+
this.replayTickAbort = null;
|
|
7863
7891
|
/** What the last applied bar did: refine the open candle, or open a new one (which
|
|
7864
7892
|
* settles the previous). Read by the `script:run` cause attribution. */
|
|
7865
7893
|
this.barCause = "tick";
|
|
@@ -8083,6 +8111,10 @@ var Vela = (function (exports) {
|
|
|
8083
8111
|
const paint = (bars, final) => {
|
|
8084
8112
|
if (this.generation !== gen || !final && bars.length === 0) return;
|
|
8085
8113
|
if (!painted && !final && bars.length < Math.min(requested, FIRST_PAINT_BARS)) return;
|
|
8114
|
+
if (painted && this.replayQueue) {
|
|
8115
|
+
this.mergeHistoryIntoReplay(bars);
|
|
8116
|
+
return;
|
|
8117
|
+
}
|
|
8086
8118
|
this.setBarSeries(bars, painted ? { preserveView: true } : void 0);
|
|
8087
8119
|
if (!painted && bars.length > 0) {
|
|
8088
8120
|
painted = true;
|
|
@@ -8225,6 +8257,36 @@ var Vela = (function (exports) {
|
|
|
8225
8257
|
// exactly like a timeframe change; the cache keys the sessions apart.
|
|
8226
8258
|
next2.session !== void 0 && next2.session !== m.session || next2.data !== void 0;
|
|
8227
8259
|
const depthChanged = next2.bars !== void 0 && next2.bars !== m.bars;
|
|
8260
|
+
const sameClock = identityChanged && next2.data === void 0 && (next2.symbol === void 0 || next2.symbol === m.symbol) && (next2.timeframe !== void 0 || next2.session !== void 0);
|
|
8261
|
+
const pending = identityChanged ? this.replayCarrying : null;
|
|
8262
|
+
if (pending) {
|
|
8263
|
+
this.replayCarrying = null;
|
|
8264
|
+
if (!sameClock) {
|
|
8265
|
+
pending.cancelled = true;
|
|
8266
|
+
this.events.emit("replay:end", { reason: "market" });
|
|
8267
|
+
}
|
|
8268
|
+
}
|
|
8269
|
+
const replayCarry = this.replayQueue && sameClock && this.rawBars.length > 0 ? { revealedEnd: this.rawBars[this.rawBars.length - 1].time + this.barIntervalMs(), playing: this.replayPlaying, cancelled: false } : pending && sameClock ? { revealedEnd: pending.revealedEnd, playing: pending.playing, cancelled: false } : null;
|
|
8270
|
+
if (pending) pending.cancelled = true;
|
|
8271
|
+
if (replayCarry) this.replayCarrying = replayCarry;
|
|
8272
|
+
if (replayCarry && this.replayQueue) {
|
|
8273
|
+
this.endReplay(null, { restore: false, resume: false });
|
|
8274
|
+
const depth = this.replayDepth;
|
|
8275
|
+
this.replayDepth = null;
|
|
8276
|
+
if (depth && next2.bars === void 0) {
|
|
8277
|
+
if (depth.bars === void 0) delete m.bars;
|
|
8278
|
+
else m.bars = depth.bars;
|
|
8279
|
+
}
|
|
8280
|
+
}
|
|
8281
|
+
if (this.replayQueue && (identityChanged || depthChanged)) this.endReplay("market", { restore: !identityChanged, resume: false });
|
|
8282
|
+
if (this.replayLoading && (identityChanged || depthChanged)) {
|
|
8283
|
+
const depth = this.replayDepth;
|
|
8284
|
+
this.abandonReplayLoad("market");
|
|
8285
|
+
if (depth && next2.bars === void 0) {
|
|
8286
|
+
if (depth.bars === void 0) delete m.bars;
|
|
8287
|
+
else m.bars = depth.bars;
|
|
8288
|
+
}
|
|
8289
|
+
}
|
|
8228
8290
|
const depthOnly = depthChanged && !identityChanged && this.rawBars.length > 0 && !m.data?.length && ((next2.bars ?? 0) <= this.rawBars.length || typeof this.feed.loadRange === "function");
|
|
8229
8291
|
if (!identityChanged && !depthChanged) {
|
|
8230
8292
|
if (typeof next2.visibleRange === "string") this.setVisibleRangePreset(next2.visibleRange);
|
|
@@ -8282,15 +8344,20 @@ var Vela = (function (exports) {
|
|
|
8282
8344
|
const from = bars[Math.max(0, idx - (carriedCount - 1))].time;
|
|
8283
8345
|
this.renderer.setVisibleRange({ from, to: Math.max(carried.right, bars[idx].time + 1) });
|
|
8284
8346
|
}
|
|
8285
|
-
|
|
8347
|
+
const carry = replayCarry && !replayCarry.cancelled ? replayCarry : null;
|
|
8348
|
+
const carryFrom = carry ? carry.revealedEnd - this.barIntervalMs() : 0;
|
|
8349
|
+
const carryNow = carry != null && this.rawBars.length > 0 && carryFrom >= this.rawBars[0].time;
|
|
8350
|
+
const replayBack = carryNow && this.resumeCarriedReplay(this.replayStart({ from: carryFrom }), carry);
|
|
8351
|
+
if (!depthOnly && !replayBack) {
|
|
8286
8352
|
this.restartNativeIndicators();
|
|
8287
8353
|
this.restartChartTypeEngine();
|
|
8288
8354
|
this.reexecuteIndicators();
|
|
8289
8355
|
}
|
|
8290
|
-
this.startLive();
|
|
8356
|
+
if (!replayBack) this.startLive();
|
|
8291
8357
|
if (identityChanged) {
|
|
8292
8358
|
this.events.emit("market:changed", { symbol: m.symbol ?? "TEST", timeframe: m.timeframe ?? "60", prev: prev3 });
|
|
8293
8359
|
}
|
|
8360
|
+
if (carry && !carryNow) this.resumeCarriedReplay(this.replayStart({ from: carryFrom }), carry);
|
|
8294
8361
|
}
|
|
8295
8362
|
/**
|
|
8296
8363
|
* Restart every native indicator over the new market: a `NativeIndicatorContext`
|
|
@@ -8414,6 +8481,397 @@ var Vela = (function (exports) {
|
|
|
8414
8481
|
historyComplete() {
|
|
8415
8482
|
return this.historyCompletePromise;
|
|
8416
8483
|
}
|
|
8484
|
+
// ── bar replay ──────────────────────────────────────────────
|
|
8485
|
+
/**
|
|
8486
|
+
* Enter replay — or seek, when already replaying — keeping every bar opened at or before
|
|
8487
|
+
* `opts.from` and queueing the rest. A `from` inside the loaded bars cuts at once, even
|
|
8488
|
+
* while older history is still streaming in: what lands later joins the tape's head
|
|
8489
|
+
* (see {@link mergeHistoryIntoReplay}) and leaves the cut alone. An older `from` waits
|
|
8490
|
+
* for that load, then deepens if it still has to. Consumers restart over the cut exactly
|
|
8491
|
+
* as on a market switch — their models describe bars that are now hidden — and the
|
|
8492
|
+
* active chart-type engine is rebuilt without live data.
|
|
8493
|
+
*/
|
|
8494
|
+
async replayStart(opts) {
|
|
8495
|
+
if (this.rawBars.length === 0) {
|
|
8496
|
+
console.warn("[vela] chart.replay.start() ignored \u2014 the chart has no bars yet");
|
|
8497
|
+
return;
|
|
8498
|
+
}
|
|
8499
|
+
let gen = this.generation;
|
|
8500
|
+
if (opts.from < this.rawBars[0].time) {
|
|
8501
|
+
await this.historyCompletePromise;
|
|
8502
|
+
if (this.generation !== gen || this.rawBars.length === 0) return;
|
|
8503
|
+
}
|
|
8504
|
+
const oldest = this.rawBars[0].time;
|
|
8505
|
+
if (opts.from < oldest && this.canHeal() && typeof this.feed.loadRange === "function") {
|
|
8506
|
+
const held = this.rawBars.length + (this.replayQueue?.length ?? 0);
|
|
8507
|
+
const bars = held + Math.ceil((oldest - opts.from) / this.barIntervalMs()) + 1;
|
|
8508
|
+
const seek = this.replayQueue !== null;
|
|
8509
|
+
if (seek) this.endReplay(null, { restore: true, resume: false });
|
|
8510
|
+
this.replayDepth ?? (this.replayDepth = { bars: this.config.market.bars });
|
|
8511
|
+
const deepened = this.setMarket({ bars });
|
|
8512
|
+
this.replayLoading = { seek };
|
|
8513
|
+
await deepened;
|
|
8514
|
+
gen = this.generation;
|
|
8515
|
+
await this.historyCompletePromise;
|
|
8516
|
+
if (!this.replayLoading || this.generation !== gen || this.rawBars.length === 0) return;
|
|
8517
|
+
this.replayLoading = null;
|
|
8518
|
+
}
|
|
8519
|
+
this.settleFormingBar();
|
|
8520
|
+
this.dropTickFetches();
|
|
8521
|
+
const tape = this.replayQueue ? [...this.rawBars, ...this.replayQueue] : this.rawBars;
|
|
8522
|
+
let cut = 0;
|
|
8523
|
+
while (cut < tape.length && tape[cut].time <= opts.from) cut += 1;
|
|
8524
|
+
cut = Math.max(1, cut);
|
|
8525
|
+
if (cut >= tape.length) {
|
|
8526
|
+
console.warn("[vela] chart.replay.start() ignored \u2014 no bar opens after the requested time");
|
|
8527
|
+
return;
|
|
8528
|
+
}
|
|
8529
|
+
const wasPlaying = this.replayPlaying;
|
|
8530
|
+
this.clearReplayTimer();
|
|
8531
|
+
if (!this.replayQueue) this.stopLive();
|
|
8532
|
+
this.replayQueue = tape.slice(cut);
|
|
8533
|
+
this.setBarSeries(tape.slice(0, cut));
|
|
8534
|
+
this.restartBarConsumers({ blank: true });
|
|
8535
|
+
if (wasPlaying) this.events.emit("replay:pause", void 0);
|
|
8536
|
+
this.events.emit("replay:start", { cursorTime: tape[cut - 1].time, remaining: this.replayQueue.length });
|
|
8537
|
+
this.prefetchTicks();
|
|
8538
|
+
}
|
|
8539
|
+
/** Reveal the next queued bar through the live-bar path — or complete the bar forming
|
|
8540
|
+
* tick by tick; the last one ends the replay. */
|
|
8541
|
+
replayStep() {
|
|
8542
|
+
if (this.replayForming) {
|
|
8543
|
+
this.finishFormingBar();
|
|
8544
|
+
return true;
|
|
8545
|
+
}
|
|
8546
|
+
const queue = this.replayQueue;
|
|
8547
|
+
const bar = queue?.shift();
|
|
8548
|
+
if (!queue || !bar) return false;
|
|
8549
|
+
this.replayTickFetches.delete(bar.time);
|
|
8550
|
+
this.applyBar(bar);
|
|
8551
|
+
this.completeReveal(bar);
|
|
8552
|
+
this.prefetchTicks();
|
|
8553
|
+
return true;
|
|
8554
|
+
}
|
|
8555
|
+
/**
|
|
8556
|
+
* Follow up a replay carried onto a new series of the same symbol (another timeframe or
|
|
8557
|
+
* session). The cut asked for keeps the bars that CLOSED by the end of what was revealed —
|
|
8558
|
+
* every finer bar of the last revealed one, never a coarser bar still open at that point
|
|
8559
|
+
* (no look-ahead). Once it settles: playback resumes if it was on; if the replay did not
|
|
8560
|
+
* come back (nothing left after that point, or a later switch took over), it ends as a
|
|
8561
|
+
* market switch would. Returns whether the replay is back already — a start with no
|
|
8562
|
+
* history to wait for cuts synchronously (an async function runs up to its first await).
|
|
8563
|
+
*/
|
|
8564
|
+
resumeCarriedReplay(start, carry) {
|
|
8565
|
+
if (this.replayQueue && this.replayCarrying === carry) this.replayCarrying = null;
|
|
8566
|
+
void start.then(() => {
|
|
8567
|
+
if (this.replayCarrying === carry) this.replayCarrying = null;
|
|
8568
|
+
if (carry.cancelled) return;
|
|
8569
|
+
if (!this.replayQueue) this.events.emit("replay:end", { reason: "market" });
|
|
8570
|
+
else if (carry.playing) this.replayPlay();
|
|
8571
|
+
});
|
|
8572
|
+
return this.replayQueue !== null;
|
|
8573
|
+
}
|
|
8574
|
+
replayStepUpdate() {
|
|
8575
|
+
if (!this.replayQueue) return false;
|
|
8576
|
+
if (this.replayForming) {
|
|
8577
|
+
this.applyReplayTicks(1);
|
|
8578
|
+
return true;
|
|
8579
|
+
}
|
|
8580
|
+
const bar = this.replayQueue[0];
|
|
8581
|
+
if (!bar) return false;
|
|
8582
|
+
const entry = this.replayTickSource ? this.requestTicks(bar) : null;
|
|
8583
|
+
if (entry && !entry.ticks) {
|
|
8584
|
+
const epoch = this.replayTickAbort;
|
|
8585
|
+
void entry.done.then(() => {
|
|
8586
|
+
if (!this.replayPlaying && !this.replayForming && this.replayTickAbort === epoch && this.replayQueue?.[0] === bar) this.replayStepUpdate();
|
|
8587
|
+
});
|
|
8588
|
+
return true;
|
|
8589
|
+
}
|
|
8590
|
+
if (!entry?.ticks?.length) return this.replayStep();
|
|
8591
|
+
this.openFormingBar(entry.ticks, 1);
|
|
8592
|
+
return true;
|
|
8593
|
+
}
|
|
8594
|
+
replaySetTicks(source) {
|
|
8595
|
+
this.finishFormingBar();
|
|
8596
|
+
this.dropTickFetches();
|
|
8597
|
+
this.replayTickSource = source;
|
|
8598
|
+
this.prefetchTicks();
|
|
8599
|
+
}
|
|
8600
|
+
replayPlay(intervalMs2) {
|
|
8601
|
+
if (!this.replayQueue) return;
|
|
8602
|
+
if (intervalMs2 !== void 0) {
|
|
8603
|
+
if (!(Number.isFinite(intervalMs2) && intervalMs2 > 0)) {
|
|
8604
|
+
console.warn(`[vela] chart.replay.play(${intervalMs2}) ignored \u2014 the interval must be a positive number of ms`);
|
|
8605
|
+
return;
|
|
8606
|
+
}
|
|
8607
|
+
this.replayIntervalMs = intervalMs2;
|
|
8608
|
+
}
|
|
8609
|
+
this.clearReplayTimer();
|
|
8610
|
+
this.replayPlaying = true;
|
|
8611
|
+
if (this.replayForming) this.paceFormingBar(this.replayForming);
|
|
8612
|
+
this.scheduleReplayStep(this.replayForming?.delayMs);
|
|
8613
|
+
this.events.emit("replay:play", { intervalMs: this.replayIntervalMs });
|
|
8614
|
+
}
|
|
8615
|
+
replayPause() {
|
|
8616
|
+
if (!this.replayPlaying) return;
|
|
8617
|
+
this.clearReplayTimer();
|
|
8618
|
+
this.events.emit("replay:pause", void 0);
|
|
8619
|
+
}
|
|
8620
|
+
replayStop() {
|
|
8621
|
+
const carrying = this.replayCarrying;
|
|
8622
|
+
if (carrying) {
|
|
8623
|
+
carrying.cancelled = true;
|
|
8624
|
+
this.replayCarrying = null;
|
|
8625
|
+
this.events.emit("replay:end", { reason: "stopped" });
|
|
8626
|
+
}
|
|
8627
|
+
if (this.replayLoading) {
|
|
8628
|
+
const depth = this.replayDepth;
|
|
8629
|
+
this.abandonReplayLoad("stopped");
|
|
8630
|
+
void this.setMarket({ bars: depth?.bars ?? 500 });
|
|
8631
|
+
return;
|
|
8632
|
+
}
|
|
8633
|
+
this.endReplay("stopped", { restore: true, resume: true });
|
|
8634
|
+
}
|
|
8635
|
+
abandonReplayLoad(reason) {
|
|
8636
|
+
const loading = this.replayLoading;
|
|
8637
|
+
if (!loading) return;
|
|
8638
|
+
this.replayLoading = null;
|
|
8639
|
+
this.replayDepth = null;
|
|
8640
|
+
if (loading.seek) this.events.emit("replay:end", { reason });
|
|
8641
|
+
}
|
|
8642
|
+
replayState() {
|
|
8643
|
+
const active = this.replayQueue !== null;
|
|
8644
|
+
return {
|
|
8645
|
+
active: active || this.replayCarrying !== null,
|
|
8646
|
+
// a replay carried over a switch stays on while the new bars load
|
|
8647
|
+
playing: this.replayCarrying?.playing ?? this.replayPlaying,
|
|
8648
|
+
cursorTime: active ? this.rawBars[this.rawBars.length - 1]?.time ?? null : null,
|
|
8649
|
+
remaining: this.replayQueue?.length ?? 0,
|
|
8650
|
+
nextTime: this.replayForming?.bar.time ?? this.replayQueue?.[0]?.time ?? null,
|
|
8651
|
+
intervalMs: this.replayIntervalMs
|
|
8652
|
+
};
|
|
8653
|
+
}
|
|
8654
|
+
replayBounds() {
|
|
8655
|
+
const first2 = this.rawBars[0];
|
|
8656
|
+
const last2 = this.replayQueue?.[this.replayQueue.length - 1] ?? this.rawBars[this.rawBars.length - 1];
|
|
8657
|
+
return first2 && last2 ? { first: first2.time, last: last2.time } : null;
|
|
8658
|
+
}
|
|
8659
|
+
scheduleReplayStep(delayMs = this.replayIntervalMs) {
|
|
8660
|
+
this.replayTimer = setTimeout(() => {
|
|
8661
|
+
this.replayTimer = null;
|
|
8662
|
+
this.replayAdvance();
|
|
8663
|
+
}, delayMs);
|
|
8664
|
+
}
|
|
8665
|
+
/** One beat of timed playback: the forming bar's next ticks, else the next bar — whole,
|
|
8666
|
+
* or opened from its ticks (waiting for them when they have not landed yet). */
|
|
8667
|
+
replayAdvance() {
|
|
8668
|
+
if (!this.replayPlaying || !this.replayQueue) return;
|
|
8669
|
+
const forming = this.replayForming;
|
|
8670
|
+
if (forming) {
|
|
8671
|
+
const due = Math.floor((now() - forming.origin) / this.replayIntervalMs) + 1;
|
|
8672
|
+
this.applyReplayTicks(Math.max(forming.batch, due - forming.next));
|
|
8673
|
+
if (this.replayPlaying) this.scheduleReplayStep(forming.delayMs);
|
|
8674
|
+
return;
|
|
8675
|
+
}
|
|
8676
|
+
const bar = this.replayQueue[0];
|
|
8677
|
+
if (!bar) return;
|
|
8678
|
+
const entry = this.replayTickSource ? this.requestTicks(bar) : null;
|
|
8679
|
+
if (entry && !entry.ticks) {
|
|
8680
|
+
const epoch = this.replayTickAbort;
|
|
8681
|
+
void entry.done.then(() => {
|
|
8682
|
+
if (this.replayPlaying && this.replayTimer == null && this.replayTickAbort === epoch && this.replayQueue?.[0] === bar) this.replayAdvance();
|
|
8683
|
+
});
|
|
8684
|
+
return;
|
|
8685
|
+
}
|
|
8686
|
+
if (!entry?.ticks?.length) {
|
|
8687
|
+
if (this.replayStep() && this.replayPlaying) this.scheduleReplayStep();
|
|
8688
|
+
return;
|
|
8689
|
+
}
|
|
8690
|
+
this.openFormingBar(entry.ticks);
|
|
8691
|
+
if (this.replayPlaying) this.scheduleReplayStep(this.replayForming?.delayMs);
|
|
8692
|
+
}
|
|
8693
|
+
/** Take the next queued bar off the tape and open it at its first tick (the first
|
|
8694
|
+
* `count` ticks — default: one timed update's worth). */
|
|
8695
|
+
openFormingBar(ticks, count) {
|
|
8696
|
+
const bar = this.replayQueue.shift();
|
|
8697
|
+
const volumeEach = ticks.every((t) => t.volume === void 0) ? (bar.volume ?? 0) / ticks.length : null;
|
|
8698
|
+
const forming = { bar, ticks, next: 0, candle: null, volumeEach, batch: 1, delayMs: this.replayIntervalMs, origin: 0 };
|
|
8699
|
+
this.paceFormingBar(forming);
|
|
8700
|
+
this.replayForming = forming;
|
|
8701
|
+
this.applyReplayTicks(count ?? forming.batch);
|
|
8702
|
+
this.prefetchTicks();
|
|
8703
|
+
}
|
|
8704
|
+
/** One tick per interval, batched when the interval is shorter than a frame; the clock
|
|
8705
|
+
* is anchored on the ticks already applied (a resume or a pace change carries on from
|
|
8706
|
+
* there, never jumps). */
|
|
8707
|
+
paceFormingBar(f) {
|
|
8708
|
+
f.batch = Math.max(1, Math.ceil(MIN_TICK_UPDATE_MS / this.replayIntervalMs));
|
|
8709
|
+
f.delayMs = this.replayIntervalMs * f.batch;
|
|
8710
|
+
f.origin = now() - Math.max(0, f.next - 1) * this.replayIntervalMs;
|
|
8711
|
+
}
|
|
8712
|
+
/** Fold the next `count` ticks into the forming candle; the last one settles the bar. */
|
|
8713
|
+
applyReplayTicks(count) {
|
|
8714
|
+
const f = this.replayForming;
|
|
8715
|
+
if (!f) return;
|
|
8716
|
+
let c = f.candle;
|
|
8717
|
+
const stop = Math.min(f.ticks.length, f.next + count);
|
|
8718
|
+
for (; f.next < stop; f.next += 1) {
|
|
8719
|
+
const t = f.ticks[f.next];
|
|
8720
|
+
const v = f.volumeEach ?? t.volume ?? 0;
|
|
8721
|
+
const hi = Math.max(t.high ?? t.price, t.price);
|
|
8722
|
+
const lo = Math.min(t.low ?? t.price, t.price);
|
|
8723
|
+
if (c) {
|
|
8724
|
+
c = { ...c, high: Math.max(c.high, hi), low: Math.min(c.low, lo), close: t.price, volume: (c.volume ?? 0) + v };
|
|
8725
|
+
} else {
|
|
8726
|
+
const open2 = t.open ?? t.price;
|
|
8727
|
+
c = { time: f.bar.time, open: open2, high: Math.max(open2, hi), low: Math.min(open2, lo), close: t.price, volume: v };
|
|
8728
|
+
}
|
|
8729
|
+
}
|
|
8730
|
+
f.candle = c;
|
|
8731
|
+
if (f.next >= f.ticks.length) {
|
|
8732
|
+
this.finishFormingBar();
|
|
8733
|
+
return;
|
|
8734
|
+
}
|
|
8735
|
+
this.applyBar(c);
|
|
8736
|
+
this.events.emit("replay:tick", { cursorTime: f.bar.time, index: f.next - 1, count: f.ticks.length });
|
|
8737
|
+
}
|
|
8738
|
+
/** Settle the forming bar on its stored values and announce it revealed. */
|
|
8739
|
+
finishFormingBar() {
|
|
8740
|
+
const f = this.replayForming;
|
|
8741
|
+
if (!f) return;
|
|
8742
|
+
this.replayForming = null;
|
|
8743
|
+
this.replayTickFetches.delete(f.bar.time);
|
|
8744
|
+
this.applyBar(f.bar);
|
|
8745
|
+
this.events.emit("replay:tick", { cursorTime: f.bar.time, index: f.ticks.length - 1, count: f.ticks.length });
|
|
8746
|
+
this.completeReveal(f.bar);
|
|
8747
|
+
}
|
|
8748
|
+
/** Put the forming bar back in its stored form, silently — a seek or an end rebuilds
|
|
8749
|
+
* from the tape, which must hold the stored bar. */
|
|
8750
|
+
settleFormingBar() {
|
|
8751
|
+
const f = this.replayForming;
|
|
8752
|
+
if (!f) return;
|
|
8753
|
+
this.replayForming = null;
|
|
8754
|
+
this.applyBar(f.bar);
|
|
8755
|
+
}
|
|
8756
|
+
/**
|
|
8757
|
+
* A history answer landing mid-replay (a progressive snapshot — the whole answer so far):
|
|
8758
|
+
* the tape keeps its cut and its hidden queue; bars older than the tape join its head,
|
|
8759
|
+
* bars newer than it join the queue's end.
|
|
8760
|
+
*/
|
|
8761
|
+
mergeHistoryIntoReplay(bars) {
|
|
8762
|
+
const queue = this.replayQueue;
|
|
8763
|
+
const head = this.rawBars[0];
|
|
8764
|
+
if (!queue || !head) return;
|
|
8765
|
+
const tail = queue[queue.length - 1] ?? this.rawBars[this.rawBars.length - 1];
|
|
8766
|
+
let older = 0;
|
|
8767
|
+
while (older < bars.length && bars[older].time < head.time) older += 1;
|
|
8768
|
+
let newer = bars.length;
|
|
8769
|
+
while (newer > older && bars[newer - 1].time > tail.time) newer -= 1;
|
|
8770
|
+
if (newer < bars.length) queue.push(...bars.slice(newer));
|
|
8771
|
+
if (older > 0) {
|
|
8772
|
+
this.setBarSeries([...bars.slice(0, older), ...this.rawBars], { preserveView: true });
|
|
8773
|
+
this.notifySessionsBars("backfill");
|
|
8774
|
+
}
|
|
8775
|
+
}
|
|
8776
|
+
/** A bar is fully on screen: announce it, and end the replay if it was the last. */
|
|
8777
|
+
completeReveal(bar) {
|
|
8778
|
+
const queue = this.replayQueue;
|
|
8779
|
+
if (!queue) return;
|
|
8780
|
+
this.events.emit("replay:step", { cursorTime: bar.time, remaining: queue.length });
|
|
8781
|
+
if (queue.length === 0) this.endReplay("finished", { restore: true, resume: true });
|
|
8782
|
+
}
|
|
8783
|
+
/** Ask for the ticks of the next hidden bar ahead of time. */
|
|
8784
|
+
prefetchTicks() {
|
|
8785
|
+
const next2 = this.replayQueue?.[0];
|
|
8786
|
+
if (next2 && this.replayTickSource) this.requestTicks(next2);
|
|
8787
|
+
}
|
|
8788
|
+
/** The (shared) tick request of a queued bar — the source runs once per bar. */
|
|
8789
|
+
requestTicks(bar) {
|
|
8790
|
+
const known = this.replayTickFetches.get(bar.time);
|
|
8791
|
+
if (known) return known;
|
|
8792
|
+
const source = this.replayTickSource;
|
|
8793
|
+
this.replayTickAbort ?? (this.replayTickAbort = new AbortController());
|
|
8794
|
+
const signal = this.replayTickAbort.signal;
|
|
8795
|
+
const queue = this.replayQueue ?? [];
|
|
8796
|
+
const end = queue[queue.indexOf(bar) + 1]?.time ?? bar.time + this.barIntervalMs();
|
|
8797
|
+
const entry = { ticks: null, done: Promise.resolve() };
|
|
8798
|
+
const land = (ticks) => {
|
|
8799
|
+
entry.ticks = usableTicks(ticks);
|
|
8800
|
+
};
|
|
8801
|
+
const fail = (e) => {
|
|
8802
|
+
if (!signal.aborted) console.warn(`[vela] replay tick source failed \u2014 revealing the bar whole (${e instanceof Error ? e.message : String(e)})`);
|
|
8803
|
+
land([]);
|
|
8804
|
+
};
|
|
8805
|
+
try {
|
|
8806
|
+
const out = source(bar, { end, signal });
|
|
8807
|
+
if (Array.isArray(out)) land(out);
|
|
8808
|
+
else entry.done = Promise.resolve(out).then(land, fail);
|
|
8809
|
+
} catch (e) {
|
|
8810
|
+
fail(e);
|
|
8811
|
+
}
|
|
8812
|
+
this.replayTickFetches.set(bar.time, entry);
|
|
8813
|
+
return entry;
|
|
8814
|
+
}
|
|
8815
|
+
/** Abort the tick requests in flight and forget the landed ones. */
|
|
8816
|
+
dropTickFetches() {
|
|
8817
|
+
this.replayTickAbort?.abort();
|
|
8818
|
+
this.replayTickAbort = null;
|
|
8819
|
+
this.replayTickFetches.clear();
|
|
8820
|
+
}
|
|
8821
|
+
clearReplayTimer() {
|
|
8822
|
+
if (this.replayTimer != null) clearTimeout(this.replayTimer);
|
|
8823
|
+
this.replayTimer = null;
|
|
8824
|
+
this.replayPlaying = false;
|
|
8825
|
+
}
|
|
8826
|
+
/**
|
|
8827
|
+
* Leave replay. `restore` puts the queued bars back and restarts the consumers over the
|
|
8828
|
+
* full history (a market switch skips it: its load replaces everything anyway); `resume`
|
|
8829
|
+
* restarts live updates and heals the bars that closed while replaying. A null reason
|
|
8830
|
+
* leaves silently — a seek that re-enters right after.
|
|
8831
|
+
*/
|
|
8832
|
+
endReplay(reason, opts) {
|
|
8833
|
+
const queue = this.replayQueue;
|
|
8834
|
+
if (!queue) return;
|
|
8835
|
+
this.clearReplayTimer();
|
|
8836
|
+
if (opts.restore) this.settleFormingBar();
|
|
8837
|
+
else this.replayForming = null;
|
|
8838
|
+
this.dropTickFetches();
|
|
8839
|
+
this.replayQueue = null;
|
|
8840
|
+
const depth = reason ? this.replayDepth : null;
|
|
8841
|
+
if (reason) this.replayDepth = null;
|
|
8842
|
+
if (depth) {
|
|
8843
|
+
if (depth.bars === void 0) delete this.config.market.bars;
|
|
8844
|
+
else this.config.market.bars = depth.bars;
|
|
8845
|
+
}
|
|
8846
|
+
if (opts.restore) {
|
|
8847
|
+
const keep = depth ? depth.bars ?? 500 : Infinity;
|
|
8848
|
+
if (queue.length > 0 || this.rawBars.length > keep) {
|
|
8849
|
+
const full = [...this.rawBars, ...queue];
|
|
8850
|
+
this.setBarSeries(full.length > keep ? full.slice(full.length - keep) : full);
|
|
8851
|
+
this.restartBarConsumers({ blank: false });
|
|
8852
|
+
} else if (this.config.live) {
|
|
8853
|
+
this.restartNativeIndicators();
|
|
8854
|
+
this.restartChartTypeEngine();
|
|
8855
|
+
}
|
|
8856
|
+
}
|
|
8857
|
+
if (opts.resume && this.config.live) {
|
|
8858
|
+
this.startLive();
|
|
8859
|
+
const last2 = this.rawBars[this.rawBars.length - 1];
|
|
8860
|
+
if (last2 && this.canHeal()) void this.healGap(last2.time);
|
|
8861
|
+
}
|
|
8862
|
+
if (reason) this.events.emit("replay:end", { reason });
|
|
8863
|
+
}
|
|
8864
|
+
/**
|
|
8865
|
+
* The bars were replaced by a cut or a restore of the SAME market: every consumer re-runs.
|
|
8866
|
+
* A cut also blanks the mounted models first — they still paint the hidden bars' values,
|
|
8867
|
+
* which must not show until the re-runs land.
|
|
8868
|
+
*/
|
|
8869
|
+
restartBarConsumers(opts) {
|
|
8870
|
+
if (opts.blank) this.blankIndicatorVisuals();
|
|
8871
|
+
this.restartNativeIndicators();
|
|
8872
|
+
this.restartChartTypeEngine();
|
|
8873
|
+
this.reexecuteIndicators();
|
|
8874
|
+
}
|
|
8417
8875
|
/** The primary symbol as the data layer resolves it, or null if no symbol is set. */
|
|
8418
8876
|
qualifiedSymbol() {
|
|
8419
8877
|
const market = this.config.market;
|
|
@@ -8485,7 +8943,7 @@ var Vela = (function (exports) {
|
|
|
8485
8943
|
engine.start({
|
|
8486
8944
|
symbol: this.config.market.symbol ?? "TEST",
|
|
8487
8945
|
timeframe: this.config.market.timeframe ?? "60",
|
|
8488
|
-
live: this.config.live ?? false,
|
|
8946
|
+
live: (this.config.live ?? false) && this.replayQueue === null,
|
|
8489
8947
|
session: this.config.market.session,
|
|
8490
8948
|
bars: () => this.bars,
|
|
8491
8949
|
data: this.dataControl,
|
|
@@ -8611,6 +9069,7 @@ var Vela = (function (exports) {
|
|
|
8611
9069
|
record.session.notifyBars(reason);
|
|
8612
9070
|
} else if (record.native) record.native.instance.onBars();
|
|
8613
9071
|
}
|
|
9072
|
+
if (this.activeEngineStyle) this.typeEngines.get(this.activeEngineStyle)?.onBars?.();
|
|
8614
9073
|
}
|
|
8615
9074
|
/** Gap healing needs a ranged feed and a provider-backed series (offline `data` has no source). */
|
|
8616
9075
|
canHeal() {
|
|
@@ -8633,10 +9092,11 @@ var Vela = (function (exports) {
|
|
|
8633
9092
|
try {
|
|
8634
9093
|
const bars = await this.feed.loadRange(this.config.market, { from: fromMs });
|
|
8635
9094
|
if (this.generation !== gen) return;
|
|
9095
|
+
if (this.replayQueue) return;
|
|
8636
9096
|
for (const b of bars) this.applyBar(b, false);
|
|
8637
9097
|
} catch {
|
|
8638
9098
|
} finally {
|
|
8639
|
-
if (this.generation === gen) {
|
|
9099
|
+
if (this.generation === gen && !this.replayQueue) {
|
|
8640
9100
|
this.healing = false;
|
|
8641
9101
|
const pending = this.healBuffer;
|
|
8642
9102
|
this.healBuffer = [];
|
|
@@ -8941,6 +9401,13 @@ var Vela = (function (exports) {
|
|
|
8941
9401
|
destroy() {
|
|
8942
9402
|
this.bumpGeneration();
|
|
8943
9403
|
this.resolveHistoryComplete();
|
|
9404
|
+
this.clearReplayTimer();
|
|
9405
|
+
this.replayQueue = null;
|
|
9406
|
+
this.replayForming = null;
|
|
9407
|
+
this.replayCarrying = null;
|
|
9408
|
+
this.dropTickFetches();
|
|
9409
|
+
this.replayDepth = null;
|
|
9410
|
+
this.replayLoading = null;
|
|
8944
9411
|
this.stopLive();
|
|
8945
9412
|
if (this.viewportTimer != null) clearTimeout(this.viewportTimer);
|
|
8946
9413
|
this.viewportUnsub?.();
|
|
@@ -9039,6 +9506,7 @@ var Vela = (function (exports) {
|
|
|
9039
9506
|
this.emitScriptRun(id, cause, first2);
|
|
9040
9507
|
},
|
|
9041
9508
|
onAlert: (a) => {
|
|
9509
|
+
if (this.replayQueue || this.replayCarrying) return;
|
|
9042
9510
|
const indicator = record.options?.title ?? record.prepared?.meta.title ?? record.title;
|
|
9043
9511
|
this.events.emit("alert", { ...a, indicator });
|
|
9044
9512
|
handle.emit("alert", { id: a.id, message: a.message, title: a.title, time: a.time });
|
|
@@ -9065,7 +9533,7 @@ var Vela = (function (exports) {
|
|
|
9065
9533
|
id,
|
|
9066
9534
|
symbol: this.config.market.symbol ?? "TEST",
|
|
9067
9535
|
timeframe: this.config.market.timeframe ?? "60",
|
|
9068
|
-
live: this.config.live,
|
|
9536
|
+
live: this.config.live && this.replayQueue === null,
|
|
9069
9537
|
session: this.config.market.session,
|
|
9070
9538
|
bars: () => this.bars,
|
|
9071
9539
|
data: this.dataControl,
|
|
@@ -9468,17 +9936,17 @@ var Vela = (function (exports) {
|
|
|
9468
9936
|
}
|
|
9469
9937
|
emitContextChanged(id) {
|
|
9470
9938
|
if (!this.registry.get(id)?.session?.getContext) return;
|
|
9471
|
-
const
|
|
9472
|
-
if (
|
|
9473
|
-
this.contextEmitAt.set(id,
|
|
9939
|
+
const now2 = Date.now();
|
|
9940
|
+
if (now2 - (this.contextEmitAt.get(id) ?? 0) < 1e3) return;
|
|
9941
|
+
this.contextEmitAt.set(id, now2);
|
|
9474
9942
|
this.events.emit("context:changed", { id });
|
|
9475
9943
|
}
|
|
9476
9944
|
emitScriptRun(id, cause, first2) {
|
|
9477
9945
|
const record = this.registry.get(id);
|
|
9478
9946
|
if (!record || record.native || !this.events.hasListeners("script:run")) return;
|
|
9479
|
-
const
|
|
9480
|
-
if (cause === "tick" &&
|
|
9481
|
-
this.runEmitAt.set(id,
|
|
9947
|
+
const now2 = Date.now();
|
|
9948
|
+
if (cause === "tick" && now2 - (this.runEmitAt.get(id) ?? 0) < RUN_EMIT_THROTTLE_MS) return;
|
|
9949
|
+
this.runEmitAt.set(id, now2);
|
|
9482
9950
|
void Promise.resolve().then(() => this.buildScriptRun(record, cause, first2)).then((run) => {
|
|
9483
9951
|
if (run && this.registry.get(id)) this.events.emit("script:run", run);
|
|
9484
9952
|
});
|
|
@@ -9531,6 +9999,16 @@ var Vela = (function (exports) {
|
|
|
9531
9999
|
handle?.emit("error", { error });
|
|
9532
10000
|
}
|
|
9533
10001
|
};
|
|
10002
|
+
function now() {
|
|
10003
|
+
return typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
10004
|
+
}
|
|
10005
|
+
function usableTicks(value) {
|
|
10006
|
+
if (!Array.isArray(value)) return [];
|
|
10007
|
+
const optional = (n) => n === void 0 || Number.isFinite(n);
|
|
10008
|
+
return value.filter(
|
|
10009
|
+
(t) => t != null && Number.isFinite(t.price) && optional(t.volume) && optional(t.high) && optional(t.low) && optional(t.open)
|
|
10010
|
+
);
|
|
10011
|
+
}
|
|
9534
10012
|
function yieldToPaint() {
|
|
9535
10013
|
if (typeof requestAnimationFrame === "function") {
|
|
9536
10014
|
return new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
|
|
@@ -9731,6 +10209,12 @@ var Vela = (function (exports) {
|
|
|
9731
10209
|
onCrosshairMove(cb) {
|
|
9732
10210
|
return this.renderer.onCrosshairMove(cb);
|
|
9733
10211
|
}
|
|
10212
|
+
/** A click — or a touch tap — on the plot, never the end of a pan: the open time of
|
|
10213
|
+
* the bar under it (`null` off the bars). On touch, where a tap moves no crosshair,
|
|
10214
|
+
* this is how an interaction learns which bar was chosen. */
|
|
10215
|
+
onClick(cb) {
|
|
10216
|
+
return this.renderer.onClick(cb);
|
|
10217
|
+
}
|
|
9734
10218
|
/** Touch long-press on a price or time axis strip — silent no-op without the seam. */
|
|
9735
10219
|
onAxisLongPress(cb) {
|
|
9736
10220
|
return this.renderer.onAxisLongPress?.(cb) ?? (() => void 0);
|
|
@@ -10806,11 +11290,76 @@ var Vela = (function (exports) {
|
|
|
10806
11290
|
}
|
|
10807
11291
|
};
|
|
10808
11292
|
|
|
11293
|
+
// src/core/ReplayControl.ts
|
|
11294
|
+
var ReplayControl = class {
|
|
11295
|
+
constructor(ctrl) {
|
|
11296
|
+
this.ctrl = ctrl;
|
|
11297
|
+
}
|
|
11298
|
+
/**
|
|
11299
|
+
* Enter replay (or seek, when already replaying) at `opts.from`. Resolves once the
|
|
11300
|
+
* chart shows the rewound history — after any in-flight history load has finished.
|
|
11301
|
+
* Starts paused; call {@link play} or {@link step}.
|
|
11302
|
+
*/
|
|
11303
|
+
start(opts) {
|
|
11304
|
+
return this.ctrl.replayStart(opts);
|
|
11305
|
+
}
|
|
11306
|
+
/** Reveal the next bar — or, mid-way through a bar played tick by tick, complete it.
|
|
11307
|
+
* Returns false when replay is off or nothing is left. */
|
|
11308
|
+
step() {
|
|
11309
|
+
return this.ctrl.replayStep();
|
|
11310
|
+
}
|
|
11311
|
+
/** Reveal the next UPDATE: with {@link setTicks}, the next tick of the forming bar — or
|
|
11312
|
+
* the next bar opened at its first tick (once its ticks are in, if still loading);
|
|
11313
|
+
* otherwise the next whole bar, like {@link step}. Returns false when nothing is left. */
|
|
11314
|
+
stepUpdate() {
|
|
11315
|
+
return this.ctrl.replayStepUpdate();
|
|
11316
|
+
}
|
|
11317
|
+
/** Reveal one bar (one tick, with {@link setTicks}) every `intervalMs` (default: the last
|
|
11318
|
+
* interval used, else 1000). Calling it again while playing changes the pace. */
|
|
11319
|
+
play(intervalMs2) {
|
|
11320
|
+
this.ctrl.replayPlay(intervalMs2);
|
|
11321
|
+
return this;
|
|
11322
|
+
}
|
|
11323
|
+
pause() {
|
|
11324
|
+
this.ctrl.replayPause();
|
|
11325
|
+
return this;
|
|
11326
|
+
}
|
|
11327
|
+
/** Leave replay: the full history comes back and live updates resume. Also cancels a
|
|
11328
|
+
* `start()` still loading older history (the chart returns to its previous depth). */
|
|
11329
|
+
stop() {
|
|
11330
|
+
this.ctrl.replayStop();
|
|
11331
|
+
return this;
|
|
11332
|
+
}
|
|
11333
|
+
get state() {
|
|
11334
|
+
return this.ctrl.replayState();
|
|
11335
|
+
}
|
|
11336
|
+
/** The loaded history a replay can walk — hidden bars included while replaying. Null
|
|
11337
|
+
* before any bar loaded. A start older than `first` deepens the history first. */
|
|
11338
|
+
get bounds() {
|
|
11339
|
+
return this.ctrl.replayBounds();
|
|
11340
|
+
}
|
|
11341
|
+
/**
|
|
11342
|
+
* Play each revealed bar as a series of intrabar updates instead of whole: while
|
|
11343
|
+
* playing, the forming candle opens at the first tick, then every tick moves its close
|
|
11344
|
+
* and stretches its high and low, exactly like a live tick — indicators, chart types
|
|
11345
|
+
* and the `bar` event follow — and the last one settles it on the stored bar. The play
|
|
11346
|
+
* interval is then the time between two TICKS: a bar lasts as many intervals as it
|
|
11347
|
+
* has ticks (below ~16 ms, ticks are batched per frame). The next bar's ticks are
|
|
11348
|
+
* requested while the current one plays; playback waits for them. `null` goes back to
|
|
11349
|
+
* whole bars. Takes effect from the next bar; a bar forming when the source changes
|
|
11350
|
+
* completes at once.
|
|
11351
|
+
*/
|
|
11352
|
+
setTicks(source) {
|
|
11353
|
+
this.ctrl.replaySetTicks(source);
|
|
11354
|
+
return this;
|
|
11355
|
+
}
|
|
11356
|
+
};
|
|
11357
|
+
|
|
10809
11358
|
// src/core/util/wall-clock.ts
|
|
10810
11359
|
var BOUNDARY_SLACK_MS = 5;
|
|
10811
11360
|
var SecondClock = class _SecondClock {
|
|
10812
|
-
constructor(
|
|
10813
|
-
this.now =
|
|
11361
|
+
constructor(now2 = () => Date.now()) {
|
|
11362
|
+
this.now = now2;
|
|
10814
11363
|
this.subs = /* @__PURE__ */ new Set();
|
|
10815
11364
|
this.timer = null;
|
|
10816
11365
|
}
|
|
@@ -10826,15 +11375,15 @@ var Vela = (function (exports) {
|
|
|
10826
11375
|
};
|
|
10827
11376
|
}
|
|
10828
11377
|
/** Milliseconds from `now` to just past the next second boundary. */
|
|
10829
|
-
static delayToNextSecond(
|
|
10830
|
-
const intoSecond = (
|
|
11378
|
+
static delayToNextSecond(now2) {
|
|
11379
|
+
const intoSecond = (now2 % 1e3 + 1e3) % 1e3;
|
|
10831
11380
|
return 1e3 - intoSecond + BOUNDARY_SLACK_MS;
|
|
10832
11381
|
}
|
|
10833
11382
|
arm() {
|
|
10834
11383
|
this.timer = setTimeout(() => {
|
|
10835
11384
|
this.timer = null;
|
|
10836
|
-
const
|
|
10837
|
-
for (const cb of this.subs) cb(
|
|
11385
|
+
const now2 = this.now();
|
|
11386
|
+
for (const cb of this.subs) cb(now2);
|
|
10838
11387
|
if (this.subs.size > 0) this.arm();
|
|
10839
11388
|
}, _SecondClock.delayToNextSecond(this.now()));
|
|
10840
11389
|
}
|
|
@@ -19935,6 +20484,264 @@ ${overlayScrollbarCss(".vela-dialog *")}
|
|
|
19935
20484
|
}
|
|
19936
20485
|
};
|
|
19937
20486
|
|
|
20487
|
+
// src/ui/components/date-picker/controller.ts
|
|
20488
|
+
var MONTH_LABELS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
|
|
20489
|
+
var MONTH_SHORT = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
20490
|
+
var WEEKDAY_LABELS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
|
|
20491
|
+
function isoDate(d) {
|
|
20492
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
20493
|
+
}
|
|
20494
|
+
function normalizeDateInput(raw) {
|
|
20495
|
+
const m = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(raw.trim());
|
|
20496
|
+
if (!m) return null;
|
|
20497
|
+
const y = Number(m[1]);
|
|
20498
|
+
const mo = Number(m[2]);
|
|
20499
|
+
const d = Number(m[3]);
|
|
20500
|
+
const dt = new Date(y, mo - 1, d);
|
|
20501
|
+
if (dt.getFullYear() !== y || dt.getMonth() !== mo - 1 || dt.getDate() !== d) return null;
|
|
20502
|
+
return isoDate(dt);
|
|
20503
|
+
}
|
|
20504
|
+
function parseIsoDate(raw) {
|
|
20505
|
+
const iso = raw ? normalizeDateInput(raw) : null;
|
|
20506
|
+
if (!iso) return null;
|
|
20507
|
+
const [y, mo, d] = iso.split("-").map(Number);
|
|
20508
|
+
return new Date(y, mo - 1, d);
|
|
20509
|
+
}
|
|
20510
|
+
function datePickerController(opts = {}) {
|
|
20511
|
+
const today = opts.today ?? /* @__PURE__ */ new Date();
|
|
20512
|
+
let selected = parseIsoDate(opts.value);
|
|
20513
|
+
let year = selected?.getFullYear() ?? today.getFullYear();
|
|
20514
|
+
let month = selected?.getMonth() ?? today.getMonth();
|
|
20515
|
+
let panel = "date";
|
|
20516
|
+
const decade = () => Math.floor(year / 10) * 10;
|
|
20517
|
+
return {
|
|
20518
|
+
get panel() {
|
|
20519
|
+
return panel;
|
|
20520
|
+
},
|
|
20521
|
+
get year() {
|
|
20522
|
+
return year;
|
|
20523
|
+
},
|
|
20524
|
+
get month() {
|
|
20525
|
+
return month;
|
|
20526
|
+
},
|
|
20527
|
+
get value() {
|
|
20528
|
+
return selected ? isoDate(selected) : null;
|
|
20529
|
+
},
|
|
20530
|
+
header() {
|
|
20531
|
+
return {
|
|
20532
|
+
month: MONTH_LABELS[month] ?? "",
|
|
20533
|
+
monthVisible: panel === "date",
|
|
20534
|
+
year: panel === "year" ? `${decade()}-${decade() + 9}` : String(year),
|
|
20535
|
+
yearDisabled: panel === "year"
|
|
20536
|
+
// in the decade panel the year reads as its title
|
|
20537
|
+
};
|
|
20538
|
+
},
|
|
20539
|
+
navLabels() {
|
|
20540
|
+
if (panel === "date") return { prev: "Previous month", next: "Next month" };
|
|
20541
|
+
if (panel === "month") return { prev: "Previous year", next: "Next year" };
|
|
20542
|
+
return { prev: "Previous decade", next: "Next decade" };
|
|
20543
|
+
},
|
|
20544
|
+
cells() {
|
|
20545
|
+
const out = [];
|
|
20546
|
+
if (panel === "date") {
|
|
20547
|
+
const startPad = new Date(year, month, 1).getDay();
|
|
20548
|
+
const days = new Date(year, month + 1, 0).getDate();
|
|
20549
|
+
const todayIso = isoDate(today);
|
|
20550
|
+
const selectedIso = selected ? isoDate(selected) : "";
|
|
20551
|
+
for (let i = 0; i < startPad; i++) out.push({ label: "", value: "", checked: false, today: false, blank: true });
|
|
20552
|
+
for (let day = 1; day <= days; day++) {
|
|
20553
|
+
const iso = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
|
20554
|
+
out.push({ label: String(day), value: iso, checked: iso === selectedIso, today: iso === todayIso });
|
|
20555
|
+
}
|
|
20556
|
+
return out;
|
|
20557
|
+
}
|
|
20558
|
+
if (panel === "month") {
|
|
20559
|
+
for (let m = 0; m < 12; m++) {
|
|
20560
|
+
out.push({
|
|
20561
|
+
label: MONTH_SHORT[m] ?? "",
|
|
20562
|
+
value: m,
|
|
20563
|
+
checked: selected?.getFullYear() === year && selected.getMonth() === m,
|
|
20564
|
+
today: today.getFullYear() === year && today.getMonth() === m
|
|
20565
|
+
});
|
|
20566
|
+
}
|
|
20567
|
+
return out;
|
|
20568
|
+
}
|
|
20569
|
+
const d = decade();
|
|
20570
|
+
for (let y = d - 1; y <= d + 10; y++) {
|
|
20571
|
+
out.push({ label: String(y), value: y, checked: selected?.getFullYear() === y, today: today.getFullYear() === y, outside: y < d || y > d + 9 });
|
|
20572
|
+
}
|
|
20573
|
+
return out;
|
|
20574
|
+
},
|
|
20575
|
+
step(dir) {
|
|
20576
|
+
if (panel === "date") {
|
|
20577
|
+
month += dir;
|
|
20578
|
+
if (month < 0) {
|
|
20579
|
+
month = 11;
|
|
20580
|
+
year -= 1;
|
|
20581
|
+
}
|
|
20582
|
+
if (month > 11) {
|
|
20583
|
+
month = 0;
|
|
20584
|
+
year += 1;
|
|
20585
|
+
}
|
|
20586
|
+
} else {
|
|
20587
|
+
year += panel === "month" ? dir : dir * 10;
|
|
20588
|
+
}
|
|
20589
|
+
},
|
|
20590
|
+
showMonths() {
|
|
20591
|
+
panel = "month";
|
|
20592
|
+
},
|
|
20593
|
+
showYears() {
|
|
20594
|
+
panel = "year";
|
|
20595
|
+
},
|
|
20596
|
+
choose(cell) {
|
|
20597
|
+
if (cell.blank) return null;
|
|
20598
|
+
if (panel === "date") {
|
|
20599
|
+
selected = parseIsoDate(String(cell.value));
|
|
20600
|
+
return selected ? isoDate(selected) : null;
|
|
20601
|
+
}
|
|
20602
|
+
if (panel === "month") {
|
|
20603
|
+
month = Number(cell.value);
|
|
20604
|
+
panel = "date";
|
|
20605
|
+
return null;
|
|
20606
|
+
}
|
|
20607
|
+
year = Number(cell.value);
|
|
20608
|
+
panel = "month";
|
|
20609
|
+
return null;
|
|
20610
|
+
},
|
|
20611
|
+
setValue(value) {
|
|
20612
|
+
selected = parseIsoDate(value);
|
|
20613
|
+
if (selected) {
|
|
20614
|
+
year = selected.getFullYear();
|
|
20615
|
+
month = selected.getMonth();
|
|
20616
|
+
}
|
|
20617
|
+
panel = "date";
|
|
20618
|
+
}
|
|
20619
|
+
};
|
|
20620
|
+
}
|
|
20621
|
+
|
|
20622
|
+
// src/ui/components/date-picker/styles.ts
|
|
20623
|
+
var DATE_PICKER_STYLE_ID = "vela-date-picker-styles";
|
|
20624
|
+
var DATE_PICKER_CSS = `
|
|
20625
|
+
.vela-date-picker{color:var(--vela-fg);font:14px var(--vela-font);user-select:none;}
|
|
20626
|
+
.vela-date-picker [hidden]{display:none !important;}
|
|
20627
|
+
.vela-date-picker-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;}
|
|
20628
|
+
.vela-date-picker-title{flex:1;display:flex;align-items:center;justify-content:center;gap:2px;min-width:0;}
|
|
20629
|
+
.vela-date-picker-switch{border:none;background:transparent;color:var(--vela-fg-bright);font:inherit;font-weight:600;font-size:14px;padding:2px 6px;border-radius:4px;cursor:pointer;}
|
|
20630
|
+
.vela-date-picker-switch:not(:disabled):hover{background:var(--vela-hover);}
|
|
20631
|
+
.vela-date-picker-switch:disabled{cursor:default;}
|
|
20632
|
+
.vela-date-picker-nav{width:24px;height:24px;border:none;background:transparent;color:var(--vela-fg-muted);border-radius:4px;padding:0;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;font-size:14px;}
|
|
20633
|
+
.vela-date-picker-nav:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
|
|
20634
|
+
.vela-date-picker-week,.vela-date-picker-grid{display:grid;grid-template-columns:repeat(7,28px);gap:2px;}
|
|
20635
|
+
.vela-date-picker-week{margin-bottom:4px;color:var(--vela-fg-muted);font-size:11px;text-align:center;}
|
|
20636
|
+
.vela-date-picker-week span{line-height:20px;}
|
|
20637
|
+
.vela-date-picker-blank{width:28px;height:28px;}
|
|
20638
|
+
.vela-date-picker-day{width:28px;height:28px;border:none;background:transparent;color:inherit;border-radius:4px;padding:0;cursor:pointer;font:inherit;font-size:14px;}
|
|
20639
|
+
.vela-date-picker-day:hover{background:var(--vela-hover);}
|
|
20640
|
+
.vela-date-picker-day[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
|
|
20641
|
+
.vela-date-picker-day[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
|
|
20642
|
+
.vela-date-picker-cells{display:grid;grid-template-columns:repeat(3,1fr);gap:4px;width:calc(7 * 28px + 6 * 2px);}
|
|
20643
|
+
.vela-date-picker-cell{height:36px;border:none;background:transparent;color:inherit;border-radius:4px;padding:0 4px;cursor:pointer;font:inherit;font-size:14px;}
|
|
20644
|
+
.vela-date-picker-cell:hover{background:var(--vela-hover);}
|
|
20645
|
+
.vela-date-picker-cell[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
|
|
20646
|
+
.vela-date-picker-cell[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
|
|
20647
|
+
.vela-date-picker-cell[data-outside]{opacity:0.45;}
|
|
20648
|
+
`;
|
|
20649
|
+
|
|
20650
|
+
// src/ui/components/date-picker/view.ts
|
|
20651
|
+
var DatePicker = class {
|
|
20652
|
+
constructor(opts = {}) {
|
|
20653
|
+
this.opts = opts;
|
|
20654
|
+
const doc = document;
|
|
20655
|
+
injectStyles(DATE_PICKER_STYLE_ID, DATE_PICKER_CSS, doc);
|
|
20656
|
+
this.ctrl = datePickerController(opts);
|
|
20657
|
+
const button = (className) => {
|
|
20658
|
+
const b = doc.createElement("button");
|
|
20659
|
+
b.type = "button";
|
|
20660
|
+
b.className = className;
|
|
20661
|
+
return b;
|
|
20662
|
+
};
|
|
20663
|
+
this.prev = button("vela-date-picker-nav");
|
|
20664
|
+
this.prev.append(iconEl("chevron-left", doc));
|
|
20665
|
+
this.next = button("vela-date-picker-nav");
|
|
20666
|
+
this.next.append(iconEl("chevron-right", doc));
|
|
20667
|
+
this.monthBtn = button("vela-date-picker-switch");
|
|
20668
|
+
this.monthBtn.setAttribute("aria-label", "Choose month");
|
|
20669
|
+
this.yearBtn = button("vela-date-picker-switch");
|
|
20670
|
+
this.yearBtn.setAttribute("aria-label", "Choose year");
|
|
20671
|
+
const title = doc.createElement("div");
|
|
20672
|
+
title.className = "vela-date-picker-title";
|
|
20673
|
+
title.append(this.monthBtn, this.yearBtn);
|
|
20674
|
+
const head = doc.createElement("div");
|
|
20675
|
+
head.className = "vela-date-picker-head";
|
|
20676
|
+
head.append(this.prev, title, this.next);
|
|
20677
|
+
this.week = doc.createElement("div");
|
|
20678
|
+
this.week.className = "vela-date-picker-week";
|
|
20679
|
+
for (const d of WEEKDAY_LABELS) {
|
|
20680
|
+
const cell = doc.createElement("span");
|
|
20681
|
+
cell.textContent = d;
|
|
20682
|
+
this.week.appendChild(cell);
|
|
20683
|
+
}
|
|
20684
|
+
this.grid = doc.createElement("div");
|
|
20685
|
+
const on = (el, run) => el.addEventListener("click", (e) => {
|
|
20686
|
+
e.stopPropagation();
|
|
20687
|
+
run();
|
|
20688
|
+
this.paint();
|
|
20689
|
+
});
|
|
20690
|
+
on(this.prev, () => this.ctrl.step(-1));
|
|
20691
|
+
on(this.next, () => this.ctrl.step(1));
|
|
20692
|
+
on(this.monthBtn, () => this.ctrl.showMonths());
|
|
20693
|
+
on(this.yearBtn, () => this.ctrl.showYears());
|
|
20694
|
+
this.el = doc.createElement("div");
|
|
20695
|
+
this.el.className = "vela-date-picker";
|
|
20696
|
+
this.el.append(head, this.week, this.grid);
|
|
20697
|
+
this.paint();
|
|
20698
|
+
}
|
|
20699
|
+
get value() {
|
|
20700
|
+
return this.ctrl.value;
|
|
20701
|
+
}
|
|
20702
|
+
/** Rewrite the selection without emitting (the calendar opens on its month). */
|
|
20703
|
+
setValue(value) {
|
|
20704
|
+
this.ctrl.setValue(value);
|
|
20705
|
+
this.paint();
|
|
20706
|
+
}
|
|
20707
|
+
paint() {
|
|
20708
|
+
const doc = this.el.ownerDocument;
|
|
20709
|
+
const h = this.ctrl.header();
|
|
20710
|
+
this.monthBtn.hidden = !h.monthVisible;
|
|
20711
|
+
this.monthBtn.textContent = h.month;
|
|
20712
|
+
this.yearBtn.textContent = h.year;
|
|
20713
|
+
this.yearBtn.disabled = h.yearDisabled;
|
|
20714
|
+
const nav = this.ctrl.navLabels();
|
|
20715
|
+
this.prev.setAttribute("aria-label", nav.prev);
|
|
20716
|
+
this.next.setAttribute("aria-label", nav.next);
|
|
20717
|
+
const dates = this.ctrl.panel === "date";
|
|
20718
|
+
this.week.hidden = !dates;
|
|
20719
|
+
this.grid.className = dates ? "vela-date-picker-grid" : "vela-date-picker-cells";
|
|
20720
|
+
this.grid.replaceChildren(...this.ctrl.cells().map((cell) => this.cellEl(doc, cell, dates)));
|
|
20721
|
+
}
|
|
20722
|
+
cellEl(doc, cell, dates) {
|
|
20723
|
+
if (cell.blank) {
|
|
20724
|
+
const blank = doc.createElement("span");
|
|
20725
|
+
blank.className = "vela-date-picker-blank";
|
|
20726
|
+
return blank;
|
|
20727
|
+
}
|
|
20728
|
+
const b = doc.createElement("button");
|
|
20729
|
+
b.type = "button";
|
|
20730
|
+
b.className = dates ? "vela-date-picker-day" : "vela-date-picker-cell";
|
|
20731
|
+
b.textContent = cell.label;
|
|
20732
|
+
if (cell.checked) b.dataset.checked = "1";
|
|
20733
|
+
if (cell.today) b.dataset.today = "1";
|
|
20734
|
+
if (cell.outside) b.dataset.outside = "1";
|
|
20735
|
+
b.addEventListener("click", (e) => {
|
|
20736
|
+
e.stopPropagation();
|
|
20737
|
+
const picked = this.ctrl.choose(cell);
|
|
20738
|
+
if (picked) this.opts.onPick?.(picked);
|
|
20739
|
+
else this.paint();
|
|
20740
|
+
});
|
|
20741
|
+
return b;
|
|
20742
|
+
}
|
|
20743
|
+
};
|
|
20744
|
+
|
|
19938
20745
|
// src/ui/components/switch/controller.ts
|
|
19939
20746
|
function switchController(opts = {}) {
|
|
19940
20747
|
let checked = opts.checked ?? false;
|
|
@@ -21887,10 +22694,16 @@ ${overlayScrollbarCss(".vela-dialog *")}
|
|
|
21887
22694
|
this.calendarPop = null;
|
|
21888
22695
|
this.calendarAnchor = null;
|
|
21889
22696
|
},
|
|
21890
|
-
content: (el) =>
|
|
21891
|
-
|
|
21892
|
-
|
|
21893
|
-
|
|
22697
|
+
content: (el) => {
|
|
22698
|
+
const picker = new DatePicker({
|
|
22699
|
+
value: current,
|
|
22700
|
+
onPick: (iso) => {
|
|
22701
|
+
pop.hide();
|
|
22702
|
+
onPick(iso);
|
|
22703
|
+
}
|
|
22704
|
+
});
|
|
22705
|
+
el.append(picker.el);
|
|
22706
|
+
}
|
|
21894
22707
|
});
|
|
21895
22708
|
this.calendarPop = pop;
|
|
21896
22709
|
this.calendarAnchor = anchor;
|
|
@@ -22015,28 +22828,6 @@ ${overlayScrollbarCss(".vela-dialog *")}
|
|
|
22015
22828
|
.vela-ind-combo-chevron{position:absolute;right:0;top:0;bottom:0;width:26px;border:none;background:transparent;color:inherit;opacity:0.55;cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0;}
|
|
22016
22829
|
.vela-ind-combo-chevron:hover{opacity:0.9;}
|
|
22017
22830
|
.vela-ind-cal{background:var(--vela-bg);color:var(--vela-fg);border:none;border-radius:6px;box-shadow:var(--vela-shadow);font:14px var(--vela-font);padding:10px 12px;user-select:none;}
|
|
22018
|
-
.vela-ind-cal [hidden]{display:none !important;}
|
|
22019
|
-
.vela-ind-cal-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;}
|
|
22020
|
-
.vela-ind-cal-title{flex:1;display:flex;align-items:center;justify-content:center;gap:2px;min-width:0;}
|
|
22021
|
-
.vela-ind-cal-switch{border:none;background:transparent;color:var(--vela-fg-bright);font:inherit;font-weight:600;font-size:14px;padding:2px 6px;border-radius:4px;cursor:pointer;}
|
|
22022
|
-
.vela-ind-cal-switch:not(:disabled):hover{background:var(--vela-hover);}
|
|
22023
|
-
.vela-ind-cal-switch:disabled{cursor:default;}
|
|
22024
|
-
.vela-ind-cal-nav{width:24px;height:24px;border:none;background:transparent;color:var(--vela-fg-muted);border-radius:4px;padding:0;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;}
|
|
22025
|
-
.vela-ind-cal-nav:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
|
|
22026
|
-
.vela-ind-cal-week,.vela-ind-cal-grid{display:grid;grid-template-columns:repeat(7,28px);gap:2px;}
|
|
22027
|
-
.vela-ind-cal-week{margin-bottom:4px;color:var(--vela-fg-muted);font-size:11px;text-align:center;}
|
|
22028
|
-
.vela-ind-cal-week span{line-height:20px;}
|
|
22029
|
-
.vela-ind-cal-blank{width:28px;height:28px;}
|
|
22030
|
-
.vela-ind-cal-day{width:28px;height:28px;border:none;background:transparent;color:inherit;border-radius:4px;padding:0;cursor:pointer;font:inherit;font-size:14px;}
|
|
22031
|
-
.vela-ind-cal-day:hover{background:var(--vela-hover);}
|
|
22032
|
-
.vela-ind-cal-day[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
|
|
22033
|
-
.vela-ind-cal-day[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
|
|
22034
|
-
.vela-ind-cal-cells{display:grid;grid-template-columns:repeat(3,1fr);gap:4px;width:calc(7 * 28px + 6 * 2px);}
|
|
22035
|
-
.vela-ind-cal-cell{height:36px;border:none;background:transparent;color:inherit;border-radius:4px;padding:0 4px;cursor:pointer;font:inherit;font-size:14px;}
|
|
22036
|
-
.vela-ind-cal-cell:hover{background:var(--vela-hover);}
|
|
22037
|
-
.vela-ind-cal-cell[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
|
|
22038
|
-
.vela-ind-cal-cell[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
|
|
22039
|
-
.vela-ind-cal-cell[data-outside]{opacity:0.45;}
|
|
22040
22831
|
${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
|
|
22041
22832
|
.vela-ind-tab{font-weight:600;font-size:13px;line-height:20px;transition:color var(--vela-dur-fast) ease,border-color var(--vela-dur-fast) ease;}
|
|
22042
22833
|
.vela-ind-tab:not(.vela-ind-tab-active):hover{color:var(--vela-fg-bright);}
|
|
@@ -22070,174 +22861,6 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
|
|
|
22070
22861
|
const v = `${hh}:${mm}`;
|
|
22071
22862
|
return { value: v, label: v };
|
|
22072
22863
|
});
|
|
22073
|
-
var MONTH_LABELS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
|
|
22074
|
-
var MONTH_SHORT = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
22075
|
-
var WEEKDAY_LABELS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
|
|
22076
|
-
function normalizeDateInput(raw) {
|
|
22077
|
-
const t = raw.trim();
|
|
22078
|
-
const m = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(t);
|
|
22079
|
-
if (!m) return null;
|
|
22080
|
-
const y = Number(m[1]);
|
|
22081
|
-
const mo = Number(m[2]);
|
|
22082
|
-
const d = Number(m[3]);
|
|
22083
|
-
const dt = new Date(y, mo - 1, d);
|
|
22084
|
-
if (dt.getFullYear() !== y || dt.getMonth() !== mo - 1 || dt.getDate() !== d) return null;
|
|
22085
|
-
return isoDate(dt);
|
|
22086
|
-
}
|
|
22087
|
-
function parseIsoDate(raw) {
|
|
22088
|
-
const iso = normalizeDateInput(raw);
|
|
22089
|
-
if (!iso) return null;
|
|
22090
|
-
const [y, mo, d] = iso.split("-").map(Number);
|
|
22091
|
-
return new Date(y, mo - 1, d);
|
|
22092
|
-
}
|
|
22093
|
-
function isoDate(d) {
|
|
22094
|
-
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
22095
|
-
}
|
|
22096
|
-
function fillCalendar(el, current, onPick) {
|
|
22097
|
-
const today = /* @__PURE__ */ new Date();
|
|
22098
|
-
const selected = parseIsoDate(current);
|
|
22099
|
-
let year = selected?.getFullYear() ?? today.getFullYear();
|
|
22100
|
-
let month = selected?.getMonth() ?? today.getMonth();
|
|
22101
|
-
let mode = "date";
|
|
22102
|
-
const prev3 = document.createElement("button");
|
|
22103
|
-
prev3.type = "button";
|
|
22104
|
-
prev3.className = "vela-ind-cal-nav";
|
|
22105
|
-
prev3.innerHTML = iconAt("chevron-left", 14);
|
|
22106
|
-
const next2 = document.createElement("button");
|
|
22107
|
-
next2.type = "button";
|
|
22108
|
-
next2.className = "vela-ind-cal-nav";
|
|
22109
|
-
next2.innerHTML = iconAt("chevron-right", 14);
|
|
22110
|
-
const monthBtn = document.createElement("button");
|
|
22111
|
-
monthBtn.type = "button";
|
|
22112
|
-
monthBtn.className = "vela-ind-cal-switch";
|
|
22113
|
-
monthBtn.setAttribute("aria-label", "Choose month");
|
|
22114
|
-
const yearBtn = document.createElement("button");
|
|
22115
|
-
yearBtn.type = "button";
|
|
22116
|
-
yearBtn.className = "vela-ind-cal-switch";
|
|
22117
|
-
yearBtn.setAttribute("aria-label", "Choose year");
|
|
22118
|
-
const title = document.createElement("div");
|
|
22119
|
-
title.className = "vela-ind-cal-title";
|
|
22120
|
-
title.append(monthBtn, yearBtn);
|
|
22121
|
-
const head = document.createElement("div");
|
|
22122
|
-
head.className = "vela-ind-cal-head";
|
|
22123
|
-
head.append(prev3, title, next2);
|
|
22124
|
-
const week = document.createElement("div");
|
|
22125
|
-
week.className = "vela-ind-cal-week";
|
|
22126
|
-
for (const d of WEEKDAY_LABELS) {
|
|
22127
|
-
const cell = document.createElement("span");
|
|
22128
|
-
cell.textContent = d;
|
|
22129
|
-
week.appendChild(cell);
|
|
22130
|
-
}
|
|
22131
|
-
const grid = document.createElement("div");
|
|
22132
|
-
const paint = () => {
|
|
22133
|
-
const decade = Math.floor(year / 10) * 10;
|
|
22134
|
-
monthBtn.hidden = mode !== "date";
|
|
22135
|
-
monthBtn.textContent = MONTH_LABELS[month] ?? "";
|
|
22136
|
-
yearBtn.textContent = mode === "year" ? `${decade}-${decade + 9}` : String(year);
|
|
22137
|
-
yearBtn.disabled = mode === "year";
|
|
22138
|
-
week.hidden = mode !== "date";
|
|
22139
|
-
prev3.setAttribute("aria-label", mode === "date" ? "Previous month" : mode === "month" ? "Previous year" : "Previous decade");
|
|
22140
|
-
next2.setAttribute("aria-label", mode === "date" ? "Next month" : mode === "month" ? "Next year" : "Next decade");
|
|
22141
|
-
grid.className = mode === "date" ? "vela-ind-cal-grid" : "vela-ind-cal-cells";
|
|
22142
|
-
grid.replaceChildren();
|
|
22143
|
-
if (mode === "date") {
|
|
22144
|
-
const startPad = new Date(year, month, 1).getDay();
|
|
22145
|
-
const days = new Date(year, month + 1, 0).getDate();
|
|
22146
|
-
const todayIso = isoDate(today);
|
|
22147
|
-
const selectedIso = selected ? isoDate(selected) : "";
|
|
22148
|
-
for (let i = 0; i < startPad; i++) {
|
|
22149
|
-
const blank = document.createElement("span");
|
|
22150
|
-
blank.className = "vela-ind-cal-blank";
|
|
22151
|
-
grid.appendChild(blank);
|
|
22152
|
-
}
|
|
22153
|
-
for (let day = 1; day <= days; day++) {
|
|
22154
|
-
const iso = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
|
22155
|
-
const b = document.createElement("button");
|
|
22156
|
-
b.type = "button";
|
|
22157
|
-
b.className = "vela-ind-cal-day";
|
|
22158
|
-
b.textContent = String(day);
|
|
22159
|
-
if (iso === selectedIso) b.dataset.checked = "1";
|
|
22160
|
-
if (iso === todayIso) b.dataset.today = "1";
|
|
22161
|
-
b.addEventListener("click", (e) => {
|
|
22162
|
-
e.stopPropagation();
|
|
22163
|
-
onPick(iso);
|
|
22164
|
-
});
|
|
22165
|
-
grid.appendChild(b);
|
|
22166
|
-
}
|
|
22167
|
-
return;
|
|
22168
|
-
}
|
|
22169
|
-
if (mode === "month") {
|
|
22170
|
-
for (let m = 0; m < 12; m++) {
|
|
22171
|
-
const b = document.createElement("button");
|
|
22172
|
-
b.type = "button";
|
|
22173
|
-
b.className = "vela-ind-cal-cell";
|
|
22174
|
-
b.textContent = MONTH_SHORT[m] ?? "";
|
|
22175
|
-
if (selected?.getFullYear() === year && selected.getMonth() === m) b.dataset.checked = "1";
|
|
22176
|
-
if (today.getFullYear() === year && today.getMonth() === m) b.dataset.today = "1";
|
|
22177
|
-
b.addEventListener("click", (e) => {
|
|
22178
|
-
e.stopPropagation();
|
|
22179
|
-
month = m;
|
|
22180
|
-
mode = "date";
|
|
22181
|
-
paint();
|
|
22182
|
-
});
|
|
22183
|
-
grid.appendChild(b);
|
|
22184
|
-
}
|
|
22185
|
-
return;
|
|
22186
|
-
}
|
|
22187
|
-
for (let y = decade - 1; y <= decade + 10; y++) {
|
|
22188
|
-
const b = document.createElement("button");
|
|
22189
|
-
b.type = "button";
|
|
22190
|
-
b.className = "vela-ind-cal-cell";
|
|
22191
|
-
b.textContent = String(y);
|
|
22192
|
-
if (y < decade || y > decade + 9) b.dataset.outside = "1";
|
|
22193
|
-
if (selected?.getFullYear() === y) b.dataset.checked = "1";
|
|
22194
|
-
if (today.getFullYear() === y) b.dataset.today = "1";
|
|
22195
|
-
b.addEventListener("click", (e) => {
|
|
22196
|
-
e.stopPropagation();
|
|
22197
|
-
year = y;
|
|
22198
|
-
mode = "month";
|
|
22199
|
-
paint();
|
|
22200
|
-
});
|
|
22201
|
-
grid.appendChild(b);
|
|
22202
|
-
}
|
|
22203
|
-
};
|
|
22204
|
-
const step = (dir) => {
|
|
22205
|
-
if (mode === "date") {
|
|
22206
|
-
month += dir;
|
|
22207
|
-
if (month < 0) {
|
|
22208
|
-
month = 11;
|
|
22209
|
-
year -= 1;
|
|
22210
|
-
}
|
|
22211
|
-
if (month > 11) {
|
|
22212
|
-
month = 0;
|
|
22213
|
-
year += 1;
|
|
22214
|
-
}
|
|
22215
|
-
} else {
|
|
22216
|
-
year += mode === "month" ? dir : dir * 10;
|
|
22217
|
-
}
|
|
22218
|
-
paint();
|
|
22219
|
-
};
|
|
22220
|
-
prev3.addEventListener("click", (e) => {
|
|
22221
|
-
e.stopPropagation();
|
|
22222
|
-
step(-1);
|
|
22223
|
-
});
|
|
22224
|
-
next2.addEventListener("click", (e) => {
|
|
22225
|
-
e.stopPropagation();
|
|
22226
|
-
step(1);
|
|
22227
|
-
});
|
|
22228
|
-
monthBtn.addEventListener("click", (e) => {
|
|
22229
|
-
e.stopPropagation();
|
|
22230
|
-
mode = "month";
|
|
22231
|
-
paint();
|
|
22232
|
-
});
|
|
22233
|
-
yearBtn.addEventListener("click", (e) => {
|
|
22234
|
-
e.stopPropagation();
|
|
22235
|
-
mode = "year";
|
|
22236
|
-
paint();
|
|
22237
|
-
});
|
|
22238
|
-
paint();
|
|
22239
|
-
el.append(head, week, grid);
|
|
22240
|
-
}
|
|
22241
22864
|
function normalizeTimeInput(raw) {
|
|
22242
22865
|
const t = raw.trim();
|
|
22243
22866
|
const m = /^(\d{1,2}):(\d{2})$/.exec(t) ?? /^(\d{2})(\d{2})$/.exec(t);
|
|
@@ -23615,6 +24238,22 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
|
|
|
23615
24238
|
for (const t of chartTypes()) if (!out.includes(t.id)) out.push(t.id);
|
|
23616
24239
|
return out;
|
|
23617
24240
|
}
|
|
24241
|
+
function sanitizeCrosshairOverride(value) {
|
|
24242
|
+
if (!value || typeof value !== "object") return null;
|
|
24243
|
+
const v = value;
|
|
24244
|
+
const out = {};
|
|
24245
|
+
if (isBool(v.vertical)) out.vertical = v.vertical;
|
|
24246
|
+
if (isBool(v.horizontal)) out.horizontal = v.horizontal;
|
|
24247
|
+
if (isColor(v.color)) out.color = v.color;
|
|
24248
|
+
if (isNum(v.width)) out.width = Math.max(0.5, Math.min(8, v.width));
|
|
24249
|
+
if (isLineStyle(v.style)) out.style = v.style;
|
|
24250
|
+
if (isNum(v.opacity)) out.opacity = clampOpacity(v.opacity);
|
|
24251
|
+
const shade = v.shadeRight;
|
|
24252
|
+
if (shade && typeof shade === "object" && isColor(shade.color)) {
|
|
24253
|
+
out.shadeRight = isNum(shade.opacity) ? { color: shade.color, opacity: clampOpacity(shade.opacity) } : { color: shade.color };
|
|
24254
|
+
}
|
|
24255
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
24256
|
+
}
|
|
23618
24257
|
function isColor(v) {
|
|
23619
24258
|
return typeof v === "string" && v.trim().length > 0;
|
|
23620
24259
|
}
|
|
@@ -25276,7 +25915,7 @@ void main() {
|
|
|
25276
25915
|
|
|
25277
25916
|
// src/renderers/native/core/Animator.ts
|
|
25278
25917
|
var Animator = class {
|
|
25279
|
-
constructor(tick, raf3, cancel,
|
|
25918
|
+
constructor(tick, raf3, cancel, now2) {
|
|
25280
25919
|
this.tick = tick;
|
|
25281
25920
|
this.handle = null;
|
|
25282
25921
|
this.last = 0;
|
|
@@ -25290,7 +25929,7 @@ void main() {
|
|
|
25290
25929
|
};
|
|
25291
25930
|
this.raf = raf3 ?? ((cb) => requestAnimationFrame(cb));
|
|
25292
25931
|
this.cancel = cancel ?? ((h) => cancelAnimationFrame(h));
|
|
25293
|
-
this.now =
|
|
25932
|
+
this.now = now2 ?? (() => performance.now());
|
|
25294
25933
|
}
|
|
25295
25934
|
get active() {
|
|
25296
25935
|
return this.handle !== null;
|
|
@@ -26089,6 +26728,8 @@ void main() {
|
|
|
26089
26728
|
/** VPVR-layer config pushed by the VPVR native indicator (null ⇒ layer off). Ephemeral. */
|
|
26090
26729
|
this.vpvrLayer = null;
|
|
26091
26730
|
this.crosshair = null;
|
|
26731
|
+
/** Runtime override of the crosshair's lines and style (the `crosshairOverride` feature). Ephemeral. */
|
|
26732
|
+
this.crosshairOverride = null;
|
|
26092
26733
|
/** How the base price series is drawn on the price pane (candles by default). */
|
|
26093
26734
|
this.priceStyle = "candles";
|
|
26094
26735
|
/** Price-series base painting for the ACTIVE style (see ChartTypeDefinition.basePainting). */
|
|
@@ -28005,9 +28646,9 @@ void main() {
|
|
|
28005
28646
|
}
|
|
28006
28647
|
|
|
28007
28648
|
// src/renderers/native/chrome/countdown.ts
|
|
28008
|
-
function countdownText(barOpen, barMs,
|
|
28649
|
+
function countdownText(barOpen, barMs, now2) {
|
|
28009
28650
|
if (!(barMs > 0)) return null;
|
|
28010
|
-
const remaining = barOpen + barMs -
|
|
28651
|
+
const remaining = barOpen + barMs - now2;
|
|
28011
28652
|
if (remaining <= 0) return null;
|
|
28012
28653
|
return formatCountdown(remaining);
|
|
28013
28654
|
}
|
|
@@ -28819,19 +29460,35 @@ void main() {
|
|
|
28819
29460
|
const dataH = coords.height;
|
|
28820
29461
|
if (!ch || ch.x < 0 || ch.x > dataW || ch.y < 0 || ch.y > dataH) return;
|
|
28821
29462
|
const cs = scene.style.crosshair;
|
|
29463
|
+
const ov = scene.crosshairOverride;
|
|
29464
|
+
const vertical = ov?.vertical !== false;
|
|
29465
|
+
const horizontal = ov?.horizontal !== false;
|
|
28822
29466
|
ctx.font = `${scene.style.fontSize}px ${theme.fontFamily}`;
|
|
28823
29467
|
ctx.textBaseline = "middle";
|
|
28824
29468
|
const logical = Math.round(coords.xToLogical(ch.x));
|
|
28825
|
-
const x =
|
|
28826
|
-
|
|
28827
|
-
|
|
28828
|
-
|
|
28829
|
-
|
|
29469
|
+
const x = crisp(coords.logicalToX(logical), ov?.width ?? cs.width);
|
|
29470
|
+
if (vertical && ov?.shadeRight) {
|
|
29471
|
+
const from = Math.max(0, Math.round(coords.logicalToX(logical + 0.5)));
|
|
29472
|
+
if (from < dataW) {
|
|
29473
|
+
ctx.fillStyle = ov.shadeRight.color;
|
|
29474
|
+
ctx.globalAlpha = ov.shadeRight.opacity ?? 1;
|
|
29475
|
+
ctx.fillRect(from, 0, dataW - from, dataH);
|
|
29476
|
+
ctx.globalAlpha = 1;
|
|
29477
|
+
}
|
|
29478
|
+
}
|
|
29479
|
+
ctx.strokeStyle = ov?.color ?? cs.color ?? theme.textColor;
|
|
29480
|
+
ctx.lineWidth = ov?.width ?? cs.width;
|
|
29481
|
+
ctx.globalAlpha = ov?.opacity ?? cs.opacity;
|
|
29482
|
+
setDash3(ctx, ov?.style ?? cs.style);
|
|
28830
29483
|
ctx.beginPath();
|
|
28831
|
-
|
|
28832
|
-
|
|
28833
|
-
|
|
28834
|
-
|
|
29484
|
+
if (vertical) {
|
|
29485
|
+
ctx.moveTo(x, 0);
|
|
29486
|
+
ctx.lineTo(x, dataH);
|
|
29487
|
+
}
|
|
29488
|
+
if (horizontal) {
|
|
29489
|
+
ctx.moveTo(0, Math.round(ch.y) + 0.5);
|
|
29490
|
+
ctx.lineTo(dataW, Math.round(ch.y) + 0.5);
|
|
29491
|
+
}
|
|
28835
29492
|
ctx.stroke();
|
|
28836
29493
|
ctx.setLineDash([]);
|
|
28837
29494
|
ctx.lineWidth = 1;
|
|
@@ -28844,11 +29501,11 @@ void main() {
|
|
|
28844
29501
|
}
|
|
28845
29502
|
}
|
|
28846
29503
|
const chipBg = cs.labelBackground ?? theme.borderColor;
|
|
28847
|
-
if (pane && pane.axisFormat !== "none") {
|
|
29504
|
+
if (horizontal && pane && pane.axisFormat !== "none") {
|
|
28848
29505
|
const price = coords.yToPrice(ch.y, pane.scale, pane.bounds);
|
|
28849
29506
|
this.chip(ctx, dataW + 1, ch.y, formatAxisValue(pane.scale, pane.bounds.height, price, percentScaleFor(scene, pane), scene.priceMintick, pane.axisFormat), chipBg, "left", false, theme.background);
|
|
28850
29507
|
}
|
|
28851
|
-
this.chip(ctx, x, dataH + 1, formatTimeStamp(coords.logicalToTime(logical), scene.timezone, coords.barInterval), chipBg, "center", true, theme.background);
|
|
29508
|
+
if (vertical) this.chip(ctx, x, dataH + 1, formatTimeStamp(coords.logicalToTime(logical), scene.timezone, coords.barInterval), chipBg, "center", true, theme.background);
|
|
28852
29509
|
}
|
|
28853
29510
|
destroy() {
|
|
28854
29511
|
this.canvas = null;
|
|
@@ -28859,23 +29516,37 @@ void main() {
|
|
|
28859
29516
|
* along), with that bar's time chip in this chart's own timezone and — when the
|
|
28860
29517
|
* level resolved — the price chip on the right axis. The snap happened upstream
|
|
28861
29518
|
* (`externalCrossPx`, floor-to-containing-bar) — this method only draws. Chips
|
|
28862
|
-
* render slightly dimmed so the ghost still reads as foreign.
|
|
29519
|
+
* render slightly dimmed so the ghost still reads as foreign. A `crosshairOverride`
|
|
29520
|
+
* applies to the ghost as well — its line style, a dropped horizontal line, and the
|
|
29521
|
+
* `shadeRight` veil from the ghost bar's right edge — so a pick that spans several
|
|
29522
|
+
* charts reads the same on every one of them. */
|
|
28863
29523
|
drawExternal(ctx, ext, scene, coords, theme) {
|
|
28864
29524
|
const cs = scene.style.crosshair;
|
|
29525
|
+
const ov = scene.crosshairOverride;
|
|
28865
29526
|
const dataW = coords.width;
|
|
28866
29527
|
const dataH = coords.height;
|
|
28867
|
-
const x =
|
|
28868
|
-
if (
|
|
29528
|
+
const x = crisp(ext.x, ov?.width ?? cs.width);
|
|
29529
|
+
if (ov?.vertical === false) return;
|
|
29530
|
+
if (ov?.shadeRight) {
|
|
29531
|
+
const from = Math.max(0, Math.round(coords.logicalToX(Math.round(coords.xToLogical(ext.x)) + 0.5)));
|
|
29532
|
+
if (from < dataW) {
|
|
29533
|
+
ctx.fillStyle = ov.shadeRight.color;
|
|
29534
|
+
ctx.globalAlpha = ov.shadeRight.opacity ?? 1;
|
|
29535
|
+
ctx.fillRect(from, 0, dataW - from, dataH);
|
|
29536
|
+
ctx.globalAlpha = 1;
|
|
29537
|
+
}
|
|
29538
|
+
}
|
|
29539
|
+
if (ext.line === false || x < 0 || x > dataW) return;
|
|
28869
29540
|
ctx.font = `${scene.style.fontSize}px ${theme.fontFamily}`;
|
|
28870
29541
|
ctx.textBaseline = "middle";
|
|
28871
|
-
ctx.strokeStyle = cs.color ?? theme.textColor;
|
|
28872
|
-
ctx.lineWidth = cs.width;
|
|
28873
|
-
ctx.globalAlpha = cs.opacity * 0.55;
|
|
28874
|
-
setDash3(ctx, cs.style);
|
|
29542
|
+
ctx.strokeStyle = ov?.color ?? cs.color ?? theme.textColor;
|
|
29543
|
+
ctx.lineWidth = ov?.width ?? cs.width;
|
|
29544
|
+
ctx.globalAlpha = (ov?.opacity ?? cs.opacity) * 0.55;
|
|
29545
|
+
setDash3(ctx, ov?.style ?? cs.style);
|
|
28875
29546
|
ctx.beginPath();
|
|
28876
29547
|
ctx.moveTo(x, 0);
|
|
28877
29548
|
ctx.lineTo(x, dataH);
|
|
28878
|
-
if (ext.y != null) {
|
|
29549
|
+
if (ext.y != null && ov?.horizontal !== false) {
|
|
28879
29550
|
ctx.moveTo(0, Math.round(ext.y) + 0.5);
|
|
28880
29551
|
ctx.lineTo(dataW, Math.round(ext.y) + 0.5);
|
|
28881
29552
|
}
|
|
@@ -28884,7 +29555,7 @@ void main() {
|
|
|
28884
29555
|
ctx.lineWidth = 1;
|
|
28885
29556
|
ctx.globalAlpha = 0.8;
|
|
28886
29557
|
const chipBg = cs.labelBackground ?? theme.borderColor;
|
|
28887
|
-
if (ext.y != null && ext.price != null) {
|
|
29558
|
+
if (ext.y != null && ext.price != null && ov?.horizontal !== false) {
|
|
28888
29559
|
let pane;
|
|
28889
29560
|
for (const p of scene.panes.values()) {
|
|
28890
29561
|
if (ext.y >= p.bounds.top && ext.y <= p.bounds.top + p.bounds.height) {
|
|
@@ -28924,6 +29595,9 @@ void main() {
|
|
|
28924
29595
|
ctx.textAlign = "start";
|
|
28925
29596
|
}
|
|
28926
29597
|
};
|
|
29598
|
+
function crisp(px, width) {
|
|
29599
|
+
return Math.round(px) + (Math.round(width) % 2 === 1 ? 0.5 : 0);
|
|
29600
|
+
}
|
|
28927
29601
|
function setDash3(ctx, style) {
|
|
28928
29602
|
if (style === "dashed") ctx.setLineDash([6, 4]);
|
|
28929
29603
|
else if (style === "dotted") ctx.setLineDash([2, 3]);
|
|
@@ -37220,7 +37894,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
37220
37894
|
this.moveIndicatorCbs = /* @__PURE__ */ new Set();
|
|
37221
37895
|
this.priceStyleCbs = /* @__PURE__ */ new Set();
|
|
37222
37896
|
this.name = "native";
|
|
37223
|
-
this.features = ["logScale", "currentPriceLine", "priceLabel", "countdown", "upColor", "downColor", "glow", "animZoom", "animPan", "animScroll", "animAutoscale", "animLiveBar", "intro", "zoomAnchor", "axisDrag", "paneResize", "candleZOrder", "candleVisible", "seriesOrder", "highlights", "sessionZones", "gridlines", "axisLabels", "scaleMode", "invertScale", "paneScales", "autoScale", "timezone", "keyboard", "historyChords", "priceStyle", "priceBaseline", "baselinePrice", "settings", "attribution", "dialogHost", "tradeMarkers", "marks", "indicatorTitles", "indicatorValues"];
|
|
37897
|
+
this.features = ["logScale", "currentPriceLine", "priceLabel", "countdown", "upColor", "downColor", "glow", "animZoom", "animPan", "animScroll", "animAutoscale", "animLiveBar", "intro", "zoomAnchor", "axisDrag", "paneResize", "candleZOrder", "candleVisible", "seriesOrder", "highlights", "sessionZones", "gridlines", "axisLabels", "scaleMode", "invertScale", "paneScales", "autoScale", "timezone", "keyboard", "historyChords", "priceStyle", "priceBaseline", "baselinePrice", "settings", "attribution", "dialogHost", "tradeMarkers", "marks", "indicatorTitles", "indicatorValues", "crosshairOverride"];
|
|
37224
37898
|
/** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
|
|
37225
37899
|
* so moving onto the button doesn't count as leaving). */
|
|
37226
37900
|
this.onScrollProximityMove = (e) => {
|
|
@@ -37399,6 +38073,9 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
37399
38073
|
this.scene.marks = mergeMarksState(this.scene.marks, value);
|
|
37400
38074
|
this.markPopover?.close();
|
|
37401
38075
|
break;
|
|
38076
|
+
case "crosshairOverride":
|
|
38077
|
+
this.scene.crosshairOverride = sanitizeCrosshairOverride(value);
|
|
38078
|
+
break;
|
|
37402
38079
|
case "keyboard":
|
|
37403
38080
|
this.setKeyboardEnabled(Boolean(value));
|
|
37404
38081
|
return;
|
|
@@ -37512,6 +38189,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
37512
38189
|
return { ...this.scene.tradeMarkers, colors: { ...this.scene.tradeMarkers.colors } };
|
|
37513
38190
|
case "marks":
|
|
37514
38191
|
return { visible: this.scene.marks.visible, groups: { ...this.scene.marks.groups } };
|
|
38192
|
+
case "crosshairOverride":
|
|
38193
|
+
return this.scene.crosshairOverride ? { ...this.scene.crosshairOverride } : null;
|
|
37515
38194
|
case "keyboard":
|
|
37516
38195
|
return this.keyboardEnabled;
|
|
37517
38196
|
case "historyChords":
|
|
@@ -38090,8 +38769,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
38090
38769
|
for (const pane of this.scene.panes.values()) pane.scale = { ...pane.scaleTarget };
|
|
38091
38770
|
this.modelAlpha = 0;
|
|
38092
38771
|
const start = performance.now();
|
|
38093
|
-
const step = (
|
|
38094
|
-
const p = Math.min(1, (
|
|
38772
|
+
const step = (now2) => {
|
|
38773
|
+
const p = Math.min(1, (now2 - start) / duration);
|
|
38095
38774
|
this.scene.bars = p >= 1 ? real : real.map((b, i) => this.revealCandle(b, i, p, n, style));
|
|
38096
38775
|
this.paintData();
|
|
38097
38776
|
if (p < 1) {
|
|
@@ -38107,8 +38786,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
38107
38786
|
fadeInModels() {
|
|
38108
38787
|
const fade = Math.min(INTRO_MODEL_FADE_MS, this.intro.duration || INTRO_MODEL_FADE_MS);
|
|
38109
38788
|
const start = performance.now();
|
|
38110
|
-
const step = (
|
|
38111
|
-
this.modelAlpha = Math.min(1, (
|
|
38789
|
+
const step = (now2) => {
|
|
38790
|
+
this.modelAlpha = Math.min(1, (now2 - start) / fade);
|
|
38112
38791
|
this.paintData();
|
|
38113
38792
|
if (this.modelAlpha < 1) {
|
|
38114
38793
|
this.introRaf = requestAnimationFrame(step);
|
|
@@ -38631,10 +39310,13 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
38631
39310
|
this.animator.start();
|
|
38632
39311
|
}
|
|
38633
39312
|
} else if (!last2 || bar.time > last2.time) {
|
|
39313
|
+
const vp = this.coords.getViewport();
|
|
39314
|
+
const holdView = n > 0 && vp.rightOffset < 0 && this.scrollTargetRO === null;
|
|
38634
39315
|
this.bars.push(bar);
|
|
38635
39316
|
this.syncLiveEase(bar);
|
|
38636
39317
|
if (this.coords.barInterval > 0) this.coords.appendBar(bar.time);
|
|
38637
39318
|
else this.coords.setBars(this.bars.map((b) => b.time));
|
|
39319
|
+
if (holdView) this.coords.setViewport({ ...vp, rightOffset: vp.rightOffset - 1 });
|
|
38638
39320
|
} else {
|
|
38639
39321
|
return;
|
|
38640
39322
|
}
|
|
@@ -39316,10 +39998,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
39316
39998
|
if (this.markPulseRaf !== null || typeof requestAnimationFrame !== "function") return;
|
|
39317
39999
|
const tick = () => {
|
|
39318
40000
|
this.markPulseRaf = null;
|
|
39319
|
-
const
|
|
39320
|
-
if (this.scene.marksFlash && this.scene.marksFlash.until <=
|
|
40001
|
+
const now2 = frameNow();
|
|
40002
|
+
if (this.scene.marksFlash && this.scene.marksFlash.until <= now2) this.scene.marksFlash = null;
|
|
39321
40003
|
this.scheduler?.invalidate(2 /* Chrome */);
|
|
39322
|
-
const pulsing = this.scene.marksHoverKey !== null &&
|
|
40004
|
+
const pulsing = this.scene.marksHoverKey !== null && now2 - this.scene.marksHoverSince < MARK_PULSE_MS;
|
|
39323
40005
|
if (pulsing || this.scene.marksFlash !== null) this.markPulseRaf = requestAnimationFrame(tick);
|
|
39324
40006
|
};
|
|
39325
40007
|
this.markPulseRaf = requestAnimationFrame(tick);
|
|
@@ -39880,9 +40562,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
39880
40562
|
const ext = this.externalCross;
|
|
39881
40563
|
if (!ext || this.coords.barCount === 0) return null;
|
|
39882
40564
|
const logical = Math.floor(this.coords.timeToLogical(ext.time));
|
|
39883
|
-
if (logical <
|
|
39884
|
-
const x = this.coords.logicalToX(logical);
|
|
39885
|
-
if (!Number.isFinite(x)
|
|
40565
|
+
if (!(logical < this.coords.barCount)) return null;
|
|
40566
|
+
const x = this.coords.logicalToX(Math.max(logical, -1));
|
|
40567
|
+
if (!Number.isFinite(x)) return null;
|
|
40568
|
+
const line = logical >= 0 && x >= 0 && x <= this.coords.width;
|
|
39886
40569
|
let y = null;
|
|
39887
40570
|
if (ext.price != null) {
|
|
39888
40571
|
const pricePane = this.scene.orderedPanes().find((p) => p.kind === "price");
|
|
@@ -39891,7 +40574,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
39891
40574
|
if (!Number.isFinite(y) || y < pricePane.bounds.top || y > pricePane.bounds.top + pricePane.bounds.height) y = null;
|
|
39892
40575
|
}
|
|
39893
40576
|
}
|
|
39894
|
-
return { x, y, time: this.coords.logicalToTime(logical), price: y != null ? ext.price : null };
|
|
40577
|
+
return { x, y, time: this.coords.logicalToTime(Math.max(logical, 0)), price: y != null ? ext.price : null, line };
|
|
39895
40578
|
}
|
|
39896
40579
|
/** Set the sticky magnet mode (called by the drawings toolbar's 3-state button). */
|
|
39897
40580
|
setSnapMode(mode) {
|
|
@@ -44596,6 +45279,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
44596
45279
|
this.panesControl = new PanesControl(this.orchestrator);
|
|
44597
45280
|
this.drawingsControl = new DrawingsControl(this.orchestrator.drawings);
|
|
44598
45281
|
this.marksControl = new MarksControl(this.orchestrator.marks);
|
|
45282
|
+
this.replayControl = new ReplayControl(this.orchestrator);
|
|
44599
45283
|
}
|
|
44600
45284
|
/**
|
|
44601
45285
|
* Register a scripting engine so `addIndicator({ language })` can run that
|
|
@@ -44835,6 +45519,16 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
44835
45519
|
get marks() {
|
|
44836
45520
|
return this.marksControl;
|
|
44837
45521
|
}
|
|
45522
|
+
/**
|
|
45523
|
+
* The chart's bar-replay control surface: rewind to a past bar and reveal the
|
|
45524
|
+
* following ones by hand or on a timer, from the history already loaded —
|
|
45525
|
+
* `await chart.replay.start({ from })`, `chart.replay.play(2000)`, `chart.replay.step()`,
|
|
45526
|
+
* `chart.replay.stop()`. Revealed bars behave like live bars for indicators; live
|
|
45527
|
+
* updates pause meanwhile. Follow it with the `replay:*` events.
|
|
45528
|
+
*/
|
|
45529
|
+
get replay() {
|
|
45530
|
+
return this.replayControl;
|
|
45531
|
+
}
|
|
44838
45532
|
on(event, handler) {
|
|
44839
45533
|
return this.orchestrator.events.on(event, handler);
|
|
44840
45534
|
}
|
|
@@ -44893,6 +45587,28 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
44893
45587
|
return element;
|
|
44894
45588
|
}
|
|
44895
45589
|
|
|
45590
|
+
// src/core/replayTicks.ts
|
|
45591
|
+
function lowerTimeframeTicks(chart, timeframe) {
|
|
45592
|
+
return async (bar, { end, signal }) => {
|
|
45593
|
+
const { symbol, session } = chart.market;
|
|
45594
|
+
const resolved = symbol ? chart.data.resolve(symbol) : null;
|
|
45595
|
+
const provider = resolved ? chart.data.providerInstance(resolved.provider) : void 0;
|
|
45596
|
+
if (!resolved || !provider) return [];
|
|
45597
|
+
const bars = await provider.getBars(resolved.ticker, timeframe, { from: bar.time, to: end - 1, ...session ? { session } : {} });
|
|
45598
|
+
if (signal.aborted) return [];
|
|
45599
|
+
return barsToTicks(bars.filter((b) => b.time >= bar.time && b.time < end));
|
|
45600
|
+
};
|
|
45601
|
+
}
|
|
45602
|
+
function barsToTicks(bars) {
|
|
45603
|
+
return bars.map((b) => ({
|
|
45604
|
+
price: b.close,
|
|
45605
|
+
open: b.open,
|
|
45606
|
+
high: b.high,
|
|
45607
|
+
low: b.low,
|
|
45608
|
+
...b.volume === void 0 ? {} : { volume: b.volume }
|
|
45609
|
+
}));
|
|
45610
|
+
}
|
|
45611
|
+
|
|
44896
45612
|
// src/widget/topbar-composition.ts
|
|
44897
45613
|
var TOPBAR_BUILTIN_IDS = ["symbol", "timeframes", "style", "layout", "indicators", "actions", "undo-redo", "alerts", "panels", "screenshot"];
|
|
44898
45614
|
|
|
@@ -45824,8 +46540,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
45824
46540
|
/** Block until the spacing window opens and any pause has elapsed, then reserve the next slot. */
|
|
45825
46541
|
async waitForTurn() {
|
|
45826
46542
|
for (; ; ) {
|
|
45827
|
-
const
|
|
45828
|
-
const wait = Math.max(this.pausedUntil -
|
|
46543
|
+
const now2 = this.clock.now();
|
|
46544
|
+
const wait = Math.max(this.pausedUntil - now2, this.nextStartAt - now2);
|
|
45829
46545
|
if (wait <= 0) break;
|
|
45830
46546
|
await this.clock.delay(wait);
|
|
45831
46547
|
}
|
|
@@ -46286,6 +47002,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
46286
47002
|
exports.NativeRenderer = NativeRenderer;
|
|
46287
47003
|
exports.OVERRIDABLE_TOPBAR_IDS = OVERRIDABLE_TOPBAR_IDS;
|
|
46288
47004
|
exports.RendererControl = RendererControl;
|
|
47005
|
+
exports.ReplayControl = ReplayControl;
|
|
46289
47006
|
exports.SERIES_LINE = SERIES_LINE;
|
|
46290
47007
|
exports.SESSION_OFF = SESSION_OFF;
|
|
46291
47008
|
exports.SESSION_POST = SESSION_POST;
|
|
@@ -46300,6 +47017,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
46300
47017
|
exports.VALID = VALID;
|
|
46301
47018
|
exports.Vela = Vela;
|
|
46302
47019
|
exports.WARNING = WARNING;
|
|
47020
|
+
exports.barsToTicks = barsToTicks;
|
|
46303
47021
|
exports.buildColorPicker = buildColorPicker;
|
|
46304
47022
|
exports.buildToolbar = buildToolbar;
|
|
46305
47023
|
exports.categoricalColor = categoricalColor;
|
|
@@ -46318,6 +47036,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
46318
47036
|
exports.inputVisible = inputVisible;
|
|
46319
47037
|
exports.legendActions = legendActions;
|
|
46320
47038
|
exports.legendCallouts = legendCallouts;
|
|
47039
|
+
exports.lowerTimeframeTicks = lowerTimeframeTicks;
|
|
46321
47040
|
exports.nativeIndicatorDescriptors = nativeIndicatorDescriptors;
|
|
46322
47041
|
exports.nativeIndicatorTypes = nativeIndicatorTypes;
|
|
46323
47042
|
exports.normalizeSettingsRow = normalizeSettingsRow;
|