@luxalgo/vela 0.7.8 → 0.8.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/NOTICE +28 -28
- package/README.md +15 -7
- 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-N4ZZYLMP.js → chunk-ATGTCWHJ.js} +726 -273
- package/dist/{chunk-CAFCLMPF.js → chunk-BZQM2XO7.js} +1 -0
- package/dist/{chunk-M6GP7DAX.js → chunk-DBDXKZUF.js} +474 -47
- package/dist/{chunk-BFA32GOU.js → chunk-EZ5FWVLA.js} +5 -2
- package/dist/{chunk-PVGWNF7V.js → chunk-LMBYEGUR.js} +2 -2
- package/dist/{chunk-WTWGEB7K.js → chunk-NELQJCGK.js} +298 -5
- 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 +1044 -272
- 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 +301 -3
- 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 +1046 -274
- package/dist/vela.global.min.js +82 -87
- package/dist/widget.cjs +1504 -327
- package/dist/widget.d.cts +26 -7
- package/dist/widget.d.ts +26 -7
- package/dist/widget.js +7 -7
- package/dist/workspace.cjs +1507 -327
- package/dist/workspace.d.cts +125 -7
- package/dist/workspace.d.ts +125 -7
- package/dist/workspace.js +6 -6
- package/package.json +2 -2
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;
|
|
@@ -20656,14 +21463,49 @@ ${overlayScrollbarCss(".vela-dialog *")}
|
|
|
20656
21463
|
const knob = document.createElement("div");
|
|
20657
21464
|
knob.style.cssText = "position:absolute;top:50%;width:15px;height:15px;border-radius:50%;background:var(--vela-selected-bg);box-shadow:0 1px 3px rgba(0,0,0,0.55);transform:translate(-50%,-50%);pointer-events:none;";
|
|
20658
21465
|
track.appendChild(knob);
|
|
20659
|
-
const pctBox = document.createElement("
|
|
20660
|
-
pctBox.
|
|
21466
|
+
const pctBox = document.createElement("input");
|
|
21467
|
+
pctBox.type = "text";
|
|
21468
|
+
pctBox.inputMode = "numeric";
|
|
21469
|
+
pctBox.title = "Opacity (%)";
|
|
21470
|
+
pctBox.style.cssText = `width:46px;box-sizing:border-box;text-align:center;font:var(--vela-font-size-md) ${theme.fontFamily};color:var(--vela-fg);background:transparent;border:1px solid var(--vela-border);border-radius:5px;padding:3px 4px;outline:none;`;
|
|
21471
|
+
pctBox.addEventListener("focus", () => {
|
|
21472
|
+
pctBox.style.borderColor = "var(--vela-border-strong)";
|
|
21473
|
+
pctBox.select();
|
|
21474
|
+
});
|
|
20661
21475
|
opRow.append(track, pctBox);
|
|
20662
21476
|
const paintOpacity = () => {
|
|
20663
21477
|
track.style.background = `linear-gradient(to right, ${curHex}00, ${curHex}ff), ${CHECKER}`;
|
|
20664
21478
|
knob.style.left = `${curAlpha * 100}%`;
|
|
20665
|
-
pctBox.
|
|
21479
|
+
pctBox.value = `${Math.round(curAlpha * 100)}%`;
|
|
20666
21480
|
};
|
|
21481
|
+
const commitTyped = () => {
|
|
21482
|
+
pctBox.style.borderColor = "var(--vela-border)";
|
|
21483
|
+
const n = parseFloat(pctBox.value.replace("%", ""));
|
|
21484
|
+
if (Number.isFinite(n)) {
|
|
21485
|
+
const next2 = Math.max(0, Math.min(100, n)) / 100;
|
|
21486
|
+
if (Math.round(next2 * 100) !== Math.round(curAlpha * 100)) {
|
|
21487
|
+
curAlpha = next2;
|
|
21488
|
+
paintOpacity();
|
|
21489
|
+
emit();
|
|
21490
|
+
return;
|
|
21491
|
+
}
|
|
21492
|
+
}
|
|
21493
|
+
paintOpacity();
|
|
21494
|
+
};
|
|
21495
|
+
pctBox.addEventListener("change", commitTyped);
|
|
21496
|
+
pctBox.addEventListener("blur", () => {
|
|
21497
|
+
pctBox.style.borderColor = "var(--vela-border)";
|
|
21498
|
+
paintOpacity();
|
|
21499
|
+
});
|
|
21500
|
+
pctBox.addEventListener("keydown", (e) => {
|
|
21501
|
+
if (e.key === "Escape") return;
|
|
21502
|
+
e.stopPropagation();
|
|
21503
|
+
if (e.key === "Enter") {
|
|
21504
|
+
e.preventDefault();
|
|
21505
|
+
commitTyped();
|
|
21506
|
+
pctBox.blur();
|
|
21507
|
+
}
|
|
21508
|
+
});
|
|
20667
21509
|
let dragging = false;
|
|
20668
21510
|
const onDrag = (clientX) => {
|
|
20669
21511
|
const r = track.getBoundingClientRect();
|
|
@@ -21887,10 +22729,16 @@ ${overlayScrollbarCss(".vela-dialog *")}
|
|
|
21887
22729
|
this.calendarPop = null;
|
|
21888
22730
|
this.calendarAnchor = null;
|
|
21889
22731
|
},
|
|
21890
|
-
content: (el) =>
|
|
21891
|
-
|
|
21892
|
-
|
|
21893
|
-
|
|
22732
|
+
content: (el) => {
|
|
22733
|
+
const picker = new DatePicker({
|
|
22734
|
+
value: current,
|
|
22735
|
+
onPick: (iso) => {
|
|
22736
|
+
pop.hide();
|
|
22737
|
+
onPick(iso);
|
|
22738
|
+
}
|
|
22739
|
+
});
|
|
22740
|
+
el.append(picker.el);
|
|
22741
|
+
}
|
|
21894
22742
|
});
|
|
21895
22743
|
this.calendarPop = pop;
|
|
21896
22744
|
this.calendarAnchor = anchor;
|
|
@@ -22015,28 +22863,6 @@ ${overlayScrollbarCss(".vela-dialog *")}
|
|
|
22015
22863
|
.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
22864
|
.vela-ind-combo-chevron:hover{opacity:0.9;}
|
|
22017
22865
|
.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
22866
|
${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
|
|
22041
22867
|
.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
22868
|
.vela-ind-tab:not(.vela-ind-tab-active):hover{color:var(--vela-fg-bright);}
|
|
@@ -22070,174 +22896,6 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
|
|
|
22070
22896
|
const v = `${hh}:${mm}`;
|
|
22071
22897
|
return { value: v, label: v };
|
|
22072
22898
|
});
|
|
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
22899
|
function normalizeTimeInput(raw) {
|
|
22242
22900
|
const t = raw.trim();
|
|
22243
22901
|
const m = /^(\d{1,2}):(\d{2})$/.exec(t) ?? /^(\d{2})(\d{2})$/.exec(t);
|
|
@@ -23615,6 +24273,22 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
|
|
|
23615
24273
|
for (const t of chartTypes()) if (!out.includes(t.id)) out.push(t.id);
|
|
23616
24274
|
return out;
|
|
23617
24275
|
}
|
|
24276
|
+
function sanitizeCrosshairOverride(value) {
|
|
24277
|
+
if (!value || typeof value !== "object") return null;
|
|
24278
|
+
const v = value;
|
|
24279
|
+
const out = {};
|
|
24280
|
+
if (isBool(v.vertical)) out.vertical = v.vertical;
|
|
24281
|
+
if (isBool(v.horizontal)) out.horizontal = v.horizontal;
|
|
24282
|
+
if (isColor(v.color)) out.color = v.color;
|
|
24283
|
+
if (isNum(v.width)) out.width = Math.max(0.5, Math.min(8, v.width));
|
|
24284
|
+
if (isLineStyle(v.style)) out.style = v.style;
|
|
24285
|
+
if (isNum(v.opacity)) out.opacity = clampOpacity(v.opacity);
|
|
24286
|
+
const shade = v.shadeRight;
|
|
24287
|
+
if (shade && typeof shade === "object" && isColor(shade.color)) {
|
|
24288
|
+
out.shadeRight = isNum(shade.opacity) ? { color: shade.color, opacity: clampOpacity(shade.opacity) } : { color: shade.color };
|
|
24289
|
+
}
|
|
24290
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
24291
|
+
}
|
|
23618
24292
|
function isColor(v) {
|
|
23619
24293
|
return typeof v === "string" && v.trim().length > 0;
|
|
23620
24294
|
}
|
|
@@ -25276,7 +25950,7 @@ void main() {
|
|
|
25276
25950
|
|
|
25277
25951
|
// src/renderers/native/core/Animator.ts
|
|
25278
25952
|
var Animator = class {
|
|
25279
|
-
constructor(tick, raf3, cancel,
|
|
25953
|
+
constructor(tick, raf3, cancel, now2) {
|
|
25280
25954
|
this.tick = tick;
|
|
25281
25955
|
this.handle = null;
|
|
25282
25956
|
this.last = 0;
|
|
@@ -25290,7 +25964,7 @@ void main() {
|
|
|
25290
25964
|
};
|
|
25291
25965
|
this.raf = raf3 ?? ((cb) => requestAnimationFrame(cb));
|
|
25292
25966
|
this.cancel = cancel ?? ((h) => cancelAnimationFrame(h));
|
|
25293
|
-
this.now =
|
|
25967
|
+
this.now = now2 ?? (() => performance.now());
|
|
25294
25968
|
}
|
|
25295
25969
|
get active() {
|
|
25296
25970
|
return this.handle !== null;
|
|
@@ -25422,6 +26096,12 @@ void main() {
|
|
|
25422
26096
|
this.lastTapT = 0;
|
|
25423
26097
|
this.lastTapX = 0;
|
|
25424
26098
|
this.lastTapY = 0;
|
|
26099
|
+
// Whether the last two primary presses were claimed by the drawings layer. A double-click
|
|
26100
|
+
// is the last press plus the one before it; a tool-driven click pair must never fall through
|
|
26101
|
+
// to the pane maximize toggle, even when the tool disarmed itself between the two clicks
|
|
26102
|
+
// (a finished placement leaves plain cursor mode for the second click).
|
|
26103
|
+
this.lastPressDrawing = false;
|
|
26104
|
+
this.prevPressDrawing = false;
|
|
25425
26105
|
this.onTouchContextMenu = (e) => {
|
|
25426
26106
|
if (this.touches.size === 0) return;
|
|
25427
26107
|
if (!(e.target instanceof Node) || !this.el?.contains(e.target)) return;
|
|
@@ -25476,19 +26156,24 @@ void main() {
|
|
|
25476
26156
|
this.moved = false;
|
|
25477
26157
|
this.startX = x;
|
|
25478
26158
|
this.startY = y;
|
|
26159
|
+
this.prevPressDrawing = this.lastPressDrawing;
|
|
26160
|
+
this.lastPressDrawing = false;
|
|
25479
26161
|
if (this.deps.drawingsClaim?.(x, y)) {
|
|
25480
26162
|
this.region = "drawing";
|
|
26163
|
+
this.lastPressDrawing = true;
|
|
25481
26164
|
this.deps.drawingsPointerDown?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
|
|
25482
26165
|
this.capture(e.pointerId);
|
|
25483
26166
|
return;
|
|
25484
26167
|
}
|
|
25485
26168
|
if (e.shiftKey && this.regionAt(x, y) === "data" && this.deps.drawingsMeasureStart?.(x, y, this.snapMode(e))) {
|
|
25486
26169
|
this.region = "drawing";
|
|
26170
|
+
this.lastPressDrawing = true;
|
|
25487
26171
|
this.capture(e.pointerId);
|
|
25488
26172
|
return;
|
|
25489
26173
|
}
|
|
25490
26174
|
if ((e.ctrlKey || e.metaKey) && this.regionAt(x, y) === "data" && this.deps.drawingsMarqueeStart?.(x, y)) {
|
|
25491
26175
|
this.region = "drawing";
|
|
26176
|
+
this.lastPressDrawing = true;
|
|
25492
26177
|
this.capture(e.pointerId);
|
|
25493
26178
|
return;
|
|
25494
26179
|
}
|
|
@@ -25810,6 +26495,7 @@ void main() {
|
|
|
25810
26495
|
if (region === "price") this.deps.resetPriceScale(x, y);
|
|
25811
26496
|
else if (region === "separator") this.deps.resetPaneSize(y);
|
|
25812
26497
|
else if (region === "time") this.deps.resetView();
|
|
26498
|
+
else if (this.lastPressDrawing || this.prevPressDrawing) return;
|
|
25813
26499
|
else this.deps.dataDblClick(x, y);
|
|
25814
26500
|
}
|
|
25815
26501
|
};
|
|
@@ -26089,6 +26775,8 @@ void main() {
|
|
|
26089
26775
|
/** VPVR-layer config pushed by the VPVR native indicator (null ⇒ layer off). Ephemeral. */
|
|
26090
26776
|
this.vpvrLayer = null;
|
|
26091
26777
|
this.crosshair = null;
|
|
26778
|
+
/** Runtime override of the crosshair's lines and style (the `crosshairOverride` feature). Ephemeral. */
|
|
26779
|
+
this.crosshairOverride = null;
|
|
26092
26780
|
/** How the base price series is drawn on the price pane (candles by default). */
|
|
26093
26781
|
this.priceStyle = "candles";
|
|
26094
26782
|
/** Price-series base painting for the ACTIVE style (see ChartTypeDefinition.basePainting). */
|
|
@@ -27973,6 +28661,7 @@ void main() {
|
|
|
27973
28661
|
const step = pickStep(span / Math.max(1, target));
|
|
27974
28662
|
const zFrom = fromMs + offsetMs;
|
|
27975
28663
|
const zTo = toMs + offsetMs;
|
|
28664
|
+
if (step >= MONTH) return calendarTicks(zFrom, zTo, step >= YEAR ? 12 : step >= 3 * MONTH ? 3 : 1, offsetMs);
|
|
27976
28665
|
const first2 = Math.ceil(zFrom / step) * step;
|
|
27977
28666
|
const out = [];
|
|
27978
28667
|
for (let zt = first2; zt <= zTo; zt += step) {
|
|
@@ -28003,11 +28692,23 @@ void main() {
|
|
|
28003
28692
|
}
|
|
28004
28693
|
return out;
|
|
28005
28694
|
}
|
|
28695
|
+
function calendarTicks(zFrom, zTo, months, offsetMs) {
|
|
28696
|
+
const d0 = new Date(zFrom);
|
|
28697
|
+
let idx = d0.getUTCFullYear() * 12 + d0.getUTCMonth();
|
|
28698
|
+
if (Date.UTC(Math.floor(idx / 12), idx % 12, 1) < zFrom) idx += 1;
|
|
28699
|
+
idx = Math.ceil(idx / months) * months;
|
|
28700
|
+
const out = [];
|
|
28701
|
+
for (let zt = Date.UTC(Math.floor(idx / 12), idx % 12, 1); zt <= zTo; idx += months, zt = Date.UTC(Math.floor(idx / 12), idx % 12, 1)) {
|
|
28702
|
+
const label = months === 12 ? String(Math.floor(idx / 12)) : MONTHS[idx % 12];
|
|
28703
|
+
out.push({ time: zt - offsetMs, label, major: true });
|
|
28704
|
+
}
|
|
28705
|
+
return out;
|
|
28706
|
+
}
|
|
28006
28707
|
|
|
28007
28708
|
// src/renderers/native/chrome/countdown.ts
|
|
28008
|
-
function countdownText(barOpen, barMs,
|
|
28709
|
+
function countdownText(barOpen, barMs, now2) {
|
|
28009
28710
|
if (!(barMs > 0)) return null;
|
|
28010
|
-
const remaining = barOpen + barMs -
|
|
28711
|
+
const remaining = barOpen + barMs - now2;
|
|
28011
28712
|
if (remaining <= 0) return null;
|
|
28012
28713
|
return formatCountdown(remaining);
|
|
28013
28714
|
}
|
|
@@ -28819,19 +29520,35 @@ void main() {
|
|
|
28819
29520
|
const dataH = coords.height;
|
|
28820
29521
|
if (!ch || ch.x < 0 || ch.x > dataW || ch.y < 0 || ch.y > dataH) return;
|
|
28821
29522
|
const cs = scene.style.crosshair;
|
|
29523
|
+
const ov = scene.crosshairOverride;
|
|
29524
|
+
const vertical = ov?.vertical !== false;
|
|
29525
|
+
const horizontal = ov?.horizontal !== false;
|
|
28822
29526
|
ctx.font = `${scene.style.fontSize}px ${theme.fontFamily}`;
|
|
28823
29527
|
ctx.textBaseline = "middle";
|
|
28824
29528
|
const logical = Math.round(coords.xToLogical(ch.x));
|
|
28825
|
-
const x =
|
|
28826
|
-
|
|
28827
|
-
|
|
28828
|
-
|
|
28829
|
-
|
|
29529
|
+
const x = crisp(coords.logicalToX(logical), ov?.width ?? cs.width);
|
|
29530
|
+
if (vertical && ov?.shadeRight) {
|
|
29531
|
+
const from = Math.max(0, Math.round(coords.logicalToX(logical + 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
|
+
ctx.strokeStyle = ov?.color ?? cs.color ?? theme.textColor;
|
|
29540
|
+
ctx.lineWidth = ov?.width ?? cs.width;
|
|
29541
|
+
ctx.globalAlpha = ov?.opacity ?? cs.opacity;
|
|
29542
|
+
setDash3(ctx, ov?.style ?? cs.style);
|
|
28830
29543
|
ctx.beginPath();
|
|
28831
|
-
|
|
28832
|
-
|
|
28833
|
-
|
|
28834
|
-
|
|
29544
|
+
if (vertical) {
|
|
29545
|
+
ctx.moveTo(x, 0);
|
|
29546
|
+
ctx.lineTo(x, dataH);
|
|
29547
|
+
}
|
|
29548
|
+
if (horizontal) {
|
|
29549
|
+
ctx.moveTo(0, Math.round(ch.y) + 0.5);
|
|
29550
|
+
ctx.lineTo(dataW, Math.round(ch.y) + 0.5);
|
|
29551
|
+
}
|
|
28835
29552
|
ctx.stroke();
|
|
28836
29553
|
ctx.setLineDash([]);
|
|
28837
29554
|
ctx.lineWidth = 1;
|
|
@@ -28844,11 +29561,11 @@ void main() {
|
|
|
28844
29561
|
}
|
|
28845
29562
|
}
|
|
28846
29563
|
const chipBg = cs.labelBackground ?? theme.borderColor;
|
|
28847
|
-
if (pane && pane.axisFormat !== "none") {
|
|
29564
|
+
if (horizontal && pane && pane.axisFormat !== "none") {
|
|
28848
29565
|
const price = coords.yToPrice(ch.y, pane.scale, pane.bounds);
|
|
28849
29566
|
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
29567
|
}
|
|
28851
|
-
this.chip(ctx, x, dataH + 1, formatTimeStamp(coords.logicalToTime(logical), scene.timezone, coords.barInterval), chipBg, "center", true, theme.background);
|
|
29568
|
+
if (vertical) this.chip(ctx, x, dataH + 1, formatTimeStamp(coords.logicalToTime(logical), scene.timezone, coords.barInterval), chipBg, "center", true, theme.background);
|
|
28852
29569
|
}
|
|
28853
29570
|
destroy() {
|
|
28854
29571
|
this.canvas = null;
|
|
@@ -28859,23 +29576,37 @@ void main() {
|
|
|
28859
29576
|
* along), with that bar's time chip in this chart's own timezone and — when the
|
|
28860
29577
|
* level resolved — the price chip on the right axis. The snap happened upstream
|
|
28861
29578
|
* (`externalCrossPx`, floor-to-containing-bar) — this method only draws. Chips
|
|
28862
|
-
* render slightly dimmed so the ghost still reads as foreign.
|
|
29579
|
+
* render slightly dimmed so the ghost still reads as foreign. A `crosshairOverride`
|
|
29580
|
+
* applies to the ghost as well — its line style, a dropped horizontal line, and the
|
|
29581
|
+
* `shadeRight` veil from the ghost bar's right edge — so a pick that spans several
|
|
29582
|
+
* charts reads the same on every one of them. */
|
|
28863
29583
|
drawExternal(ctx, ext, scene, coords, theme) {
|
|
28864
29584
|
const cs = scene.style.crosshair;
|
|
29585
|
+
const ov = scene.crosshairOverride;
|
|
28865
29586
|
const dataW = coords.width;
|
|
28866
29587
|
const dataH = coords.height;
|
|
28867
|
-
const x =
|
|
28868
|
-
if (
|
|
29588
|
+
const x = crisp(ext.x, ov?.width ?? cs.width);
|
|
29589
|
+
if (ov?.vertical === false) return;
|
|
29590
|
+
if (ov?.shadeRight) {
|
|
29591
|
+
const from = Math.max(0, Math.round(coords.logicalToX(Math.round(coords.xToLogical(ext.x)) + 0.5)));
|
|
29592
|
+
if (from < dataW) {
|
|
29593
|
+
ctx.fillStyle = ov.shadeRight.color;
|
|
29594
|
+
ctx.globalAlpha = ov.shadeRight.opacity ?? 1;
|
|
29595
|
+
ctx.fillRect(from, 0, dataW - from, dataH);
|
|
29596
|
+
ctx.globalAlpha = 1;
|
|
29597
|
+
}
|
|
29598
|
+
}
|
|
29599
|
+
if (ext.line === false || x < 0 || x > dataW) return;
|
|
28869
29600
|
ctx.font = `${scene.style.fontSize}px ${theme.fontFamily}`;
|
|
28870
29601
|
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);
|
|
29602
|
+
ctx.strokeStyle = ov?.color ?? cs.color ?? theme.textColor;
|
|
29603
|
+
ctx.lineWidth = ov?.width ?? cs.width;
|
|
29604
|
+
ctx.globalAlpha = (ov?.opacity ?? cs.opacity) * 0.55;
|
|
29605
|
+
setDash3(ctx, ov?.style ?? cs.style);
|
|
28875
29606
|
ctx.beginPath();
|
|
28876
29607
|
ctx.moveTo(x, 0);
|
|
28877
29608
|
ctx.lineTo(x, dataH);
|
|
28878
|
-
if (ext.y != null) {
|
|
29609
|
+
if (ext.y != null && ov?.horizontal !== false) {
|
|
28879
29610
|
ctx.moveTo(0, Math.round(ext.y) + 0.5);
|
|
28880
29611
|
ctx.lineTo(dataW, Math.round(ext.y) + 0.5);
|
|
28881
29612
|
}
|
|
@@ -28884,7 +29615,7 @@ void main() {
|
|
|
28884
29615
|
ctx.lineWidth = 1;
|
|
28885
29616
|
ctx.globalAlpha = 0.8;
|
|
28886
29617
|
const chipBg = cs.labelBackground ?? theme.borderColor;
|
|
28887
|
-
if (ext.y != null && ext.price != null) {
|
|
29618
|
+
if (ext.y != null && ext.price != null && ov?.horizontal !== false) {
|
|
28888
29619
|
let pane;
|
|
28889
29620
|
for (const p of scene.panes.values()) {
|
|
28890
29621
|
if (ext.y >= p.bounds.top && ext.y <= p.bounds.top + p.bounds.height) {
|
|
@@ -28924,6 +29655,9 @@ void main() {
|
|
|
28924
29655
|
ctx.textAlign = "start";
|
|
28925
29656
|
}
|
|
28926
29657
|
};
|
|
29658
|
+
function crisp(px, width) {
|
|
29659
|
+
return Math.round(px) + (Math.round(width) % 2 === 1 ? 0.5 : 0);
|
|
29660
|
+
}
|
|
28927
29661
|
function setDash3(ctx, style) {
|
|
28928
29662
|
if (style === "dashed") ctx.setLineDash([6, 4]);
|
|
28929
29663
|
else if (style === "dotted") ctx.setLineDash([2, 3]);
|
|
@@ -36515,23 +37249,21 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
36515
37249
|
return { below, above };
|
|
36516
37250
|
}
|
|
36517
37251
|
|
|
36518
|
-
// src/renderers/native/chrome/
|
|
36519
|
-
var
|
|
36520
|
-
var
|
|
37252
|
+
// src/renderers/native/chrome/vela-logos.ts
|
|
37253
|
+
var VELA_MARK_SVG = '<svg viewBox="0 -142 161.66 148" aria-hidden="true"><g fill="currentColor"><path d="M0 -140L32.91 -140L77.52 -62.73L61.06 -34.24Z"/><path d="M161.66 -140L128.75 -140L84.14 -62.73L100.59 -34.24Z"/><path d="M80.83 -52.04L95.85 -26.02L80.83 0L65.81 -26.02Z"/></g></svg>';
|
|
37254
|
+
var VELA_LETTERS_SVG = '<svg viewBox="161.66 -142 310.57 148" aria-hidden="true"><g fill="currentColor"><path d="M226.3 1.17Q200.81 1.17 186.12 -12.99Q171.42 -27.15 171.42 -51.17Q171.42 -75.2 185.87 -89.6Q200.33 -104 224.25 -104Q247.4 -104 261.7 -90.58Q276.01 -77.15 276.3 -54.59Q276.3 -49.9 275.42 -45.02H200.81V-43.55Q201.2 -32.62 207.75 -26.42Q214.29 -20.21 225.03 -20.21Q233.72 -20.21 239.34 -23.93Q244.95 -27.64 246.81 -34.77H274.64Q272.1 -19.14 259.16 -8.98Q246.22 1.17 226.3 1.17ZM201.5 -63.38H247.88Q246.42 -72.75 240.27 -77.88Q234.12 -83.01 224.45 -83.01Q215.17 -83.01 208.92 -77.73Q202.67 -72.46 201.5 -63.38ZM287.54 0V-140.04H315.76V0ZM429.34 -23.83H432.56V0H418.11Q407.76 0 403.36 -4.44Q398.97 -8.89 399.16 -16.8Q387.83 1.17 365.57 1.17Q348.48 1.17 337.88 -6.74Q327.29 -14.65 327.29 -29Q327.29 -45.41 339.45 -54.3Q351.6 -63.18 374.55 -63.18H394.96V-68.16Q394.96 -75.2 390.08 -79.3Q385.2 -83.4 376.51 -83.4Q368.3 -83.4 363.03 -79.98Q357.76 -76.56 356.88 -70.8H329.73Q331.19 -85.84 344.08 -94.92Q356.97 -104 377.29 -104Q399.06 -104 410.88 -94.43Q422.7 -84.86 422.7 -66.6V-30.18Q422.7 -26.37 424.26 -25.1Q425.82 -23.83 429.34 -23.83ZM394.96 -42.38V-44.43H374.36Q365.47 -44.43 360.59 -40.92Q355.7 -37.4 355.7 -30.96Q355.7 -25.59 359.81 -22.41Q363.91 -19.24 370.94 -19.24Q381.97 -19.24 388.37 -25.54Q394.77 -31.84 394.96 -42.38Z"/><path fill-opacity="0.55" d="M437.44 -124.28H441.96V-113.41H445.34V-124.28H449.87V-127.4H437.44ZM451.43 -113.41H454.7V-123.29L458.97 -113.41H461.62L465.99 -123.29V-113.41H469.21V-127.4H464.53L460.42 -117.68L456.16 -127.4H451.43Z"/></g></svg>';
|
|
36521
37255
|
|
|
36522
37256
|
// src/renderers/native/chrome/AttributionMark.ts
|
|
36523
|
-
var ATTRIBUTION_URL = "https://
|
|
37257
|
+
var ATTRIBUTION_URL = "https://velacharts.dev/?utm_source=vela&utm_medium=attribution";
|
|
36524
37258
|
var STYLE_ID5 = "vela-attribution-styles";
|
|
36525
37259
|
var CSS2 = `
|
|
36526
37260
|
.vela-attribution {
|
|
36527
37261
|
text-decoration: none;
|
|
36528
37262
|
display: flex;
|
|
36529
37263
|
align-items: center;
|
|
36530
|
-
/*
|
|
36531
|
-
|
|
36532
|
-
|
|
36533
|
-
reads wrong because the symbol's ink hangs low and the wordmark descends. */
|
|
36534
|
-
gap: 4px;
|
|
37264
|
+
/* No gutter: the V is the wordmark's first letter, and the letters' viewBox starts
|
|
37265
|
+
where the V's ends, so the kit's own letter spacing comes with them. */
|
|
37266
|
+
gap: 0;
|
|
36535
37267
|
}
|
|
36536
37268
|
.vela-attribution .vela-attr-symbol {
|
|
36537
37269
|
flex: none;
|
|
@@ -36539,7 +37271,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
36539
37271
|
line-height: 0;
|
|
36540
37272
|
}
|
|
36541
37273
|
.vela-attribution .vela-attr-symbol svg {
|
|
36542
|
-
height:
|
|
37274
|
+
height: 24px;
|
|
36543
37275
|
width: auto;
|
|
36544
37276
|
display: block;
|
|
36545
37277
|
filter: drop-shadow(0 1px 2px var(--vela-attr-shadow, rgba(0,0,0,0.45)));
|
|
@@ -36550,15 +37282,13 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
36550
37282
|
opacity: 0;
|
|
36551
37283
|
display: flex;
|
|
36552
37284
|
align-items: center;
|
|
36553
|
-
/*
|
|
36554
|
-
position: relative;
|
|
36555
|
-
top: 3px;
|
|
37285
|
+
/* Same height and baseline as the V (one viewBox height), so no vertical offset. */
|
|
36556
37286
|
transform: translateX(-8px);
|
|
36557
37287
|
transition: max-width 0.3s ease, opacity 0.25s ease, transform 0.3s ease;
|
|
36558
37288
|
flex: none;
|
|
36559
37289
|
}
|
|
36560
37290
|
.vela-attribution .vela-attr-wordmark svg {
|
|
36561
|
-
height:
|
|
37291
|
+
height: 24px;
|
|
36562
37292
|
width: auto;
|
|
36563
37293
|
display: block;
|
|
36564
37294
|
filter: drop-shadow(0 1px 2px var(--vela-attr-shadow, rgba(0,0,0,0.45)));
|
|
@@ -36573,14 +37303,11 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
36573
37303
|
(data-layout): the workspace's single grid-wide mark lives OUTSIDE any renderer
|
|
36574
37304
|
container, so only the shell attribute reaches it. */
|
|
36575
37305
|
[data-vela-layout='mobile'] .vela-attribution .vela-attr-symbol svg,
|
|
36576
|
-
[data-layout='mobile'] .vela-attribution .vela-attr-symbol svg { height:
|
|
37306
|
+
[data-layout='mobile'] .vela-attribution .vela-attr-symbol svg { height: 20px; }
|
|
36577
37307
|
[data-vela-layout='mobile'] .vela-attribution .vela-attr-wordmark,
|
|
36578
|
-
[data-layout='mobile'] .vela-attribution .vela-attr-wordmark {
|
|
36579
|
-
top: 2px;
|
|
36580
|
-
transform: translateX(-6px);
|
|
36581
|
-
}
|
|
37308
|
+
[data-layout='mobile'] .vela-attribution .vela-attr-wordmark { transform: translateX(-6px); }
|
|
36582
37309
|
[data-vela-layout='mobile'] .vela-attribution .vela-attr-wordmark svg,
|
|
36583
|
-
[data-layout='mobile'] .vela-attribution .vela-attr-wordmark svg { height:
|
|
37310
|
+
[data-layout='mobile'] .vela-attribution .vela-attr-wordmark svg { height: 20px; }
|
|
36584
37311
|
[data-vela-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark,
|
|
36585
37312
|
[data-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark { transform: translateX(0); }
|
|
36586
37313
|
`;
|
|
@@ -36637,10 +37364,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
36637
37364
|
const symbol = doc.createElement("span");
|
|
36638
37365
|
symbol.className = "vela-attr-symbol";
|
|
36639
37366
|
symbol.setAttribute("aria-hidden", "true");
|
|
36640
|
-
symbol.innerHTML =
|
|
37367
|
+
symbol.innerHTML = VELA_MARK_SVG;
|
|
36641
37368
|
const wordmark = doc.createElement("span");
|
|
36642
37369
|
wordmark.className = "vela-attr-wordmark";
|
|
36643
|
-
wordmark.innerHTML =
|
|
37370
|
+
wordmark.innerHTML = VELA_LETTERS_SVG;
|
|
36644
37371
|
a.append(symbol, wordmark);
|
|
36645
37372
|
return a;
|
|
36646
37373
|
}
|
|
@@ -37220,7 +37947,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
37220
37947
|
this.moveIndicatorCbs = /* @__PURE__ */ new Set();
|
|
37221
37948
|
this.priceStyleCbs = /* @__PURE__ */ new Set();
|
|
37222
37949
|
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"];
|
|
37950
|
+
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
37951
|
/** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
|
|
37225
37952
|
* so moving onto the button doesn't count as leaving). */
|
|
37226
37953
|
this.onScrollProximityMove = (e) => {
|
|
@@ -37399,6 +38126,9 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
37399
38126
|
this.scene.marks = mergeMarksState(this.scene.marks, value);
|
|
37400
38127
|
this.markPopover?.close();
|
|
37401
38128
|
break;
|
|
38129
|
+
case "crosshairOverride":
|
|
38130
|
+
this.scene.crosshairOverride = sanitizeCrosshairOverride(value);
|
|
38131
|
+
break;
|
|
37402
38132
|
case "keyboard":
|
|
37403
38133
|
this.setKeyboardEnabled(Boolean(value));
|
|
37404
38134
|
return;
|
|
@@ -37512,6 +38242,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
37512
38242
|
return { ...this.scene.tradeMarkers, colors: { ...this.scene.tradeMarkers.colors } };
|
|
37513
38243
|
case "marks":
|
|
37514
38244
|
return { visible: this.scene.marks.visible, groups: { ...this.scene.marks.groups } };
|
|
38245
|
+
case "crosshairOverride":
|
|
38246
|
+
return this.scene.crosshairOverride ? { ...this.scene.crosshairOverride } : null;
|
|
37515
38247
|
case "keyboard":
|
|
37516
38248
|
return this.keyboardEnabled;
|
|
37517
38249
|
case "historyChords":
|
|
@@ -38090,8 +38822,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
38090
38822
|
for (const pane of this.scene.panes.values()) pane.scale = { ...pane.scaleTarget };
|
|
38091
38823
|
this.modelAlpha = 0;
|
|
38092
38824
|
const start = performance.now();
|
|
38093
|
-
const step = (
|
|
38094
|
-
const p = Math.min(1, (
|
|
38825
|
+
const step = (now2) => {
|
|
38826
|
+
const p = Math.min(1, (now2 - start) / duration);
|
|
38095
38827
|
this.scene.bars = p >= 1 ? real : real.map((b, i) => this.revealCandle(b, i, p, n, style));
|
|
38096
38828
|
this.paintData();
|
|
38097
38829
|
if (p < 1) {
|
|
@@ -38107,8 +38839,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
38107
38839
|
fadeInModels() {
|
|
38108
38840
|
const fade = Math.min(INTRO_MODEL_FADE_MS, this.intro.duration || INTRO_MODEL_FADE_MS);
|
|
38109
38841
|
const start = performance.now();
|
|
38110
|
-
const step = (
|
|
38111
|
-
this.modelAlpha = Math.min(1, (
|
|
38842
|
+
const step = (now2) => {
|
|
38843
|
+
this.modelAlpha = Math.min(1, (now2 - start) / fade);
|
|
38112
38844
|
this.paintData();
|
|
38113
38845
|
if (this.modelAlpha < 1) {
|
|
38114
38846
|
this.introRaf = requestAnimationFrame(step);
|
|
@@ -38631,10 +39363,13 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
38631
39363
|
this.animator.start();
|
|
38632
39364
|
}
|
|
38633
39365
|
} else if (!last2 || bar.time > last2.time) {
|
|
39366
|
+
const vp = this.coords.getViewport();
|
|
39367
|
+
const holdView = n > 0 && vp.rightOffset < 0 && this.scrollTargetRO === null;
|
|
38634
39368
|
this.bars.push(bar);
|
|
38635
39369
|
this.syncLiveEase(bar);
|
|
38636
39370
|
if (this.coords.barInterval > 0) this.coords.appendBar(bar.time);
|
|
38637
39371
|
else this.coords.setBars(this.bars.map((b) => b.time));
|
|
39372
|
+
if (holdView) this.coords.setViewport({ ...vp, rightOffset: vp.rightOffset - 1 });
|
|
38638
39373
|
} else {
|
|
38639
39374
|
return;
|
|
38640
39375
|
}
|
|
@@ -39316,10 +40051,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
39316
40051
|
if (this.markPulseRaf !== null || typeof requestAnimationFrame !== "function") return;
|
|
39317
40052
|
const tick = () => {
|
|
39318
40053
|
this.markPulseRaf = null;
|
|
39319
|
-
const
|
|
39320
|
-
if (this.scene.marksFlash && this.scene.marksFlash.until <=
|
|
40054
|
+
const now2 = frameNow();
|
|
40055
|
+
if (this.scene.marksFlash && this.scene.marksFlash.until <= now2) this.scene.marksFlash = null;
|
|
39321
40056
|
this.scheduler?.invalidate(2 /* Chrome */);
|
|
39322
|
-
const pulsing = this.scene.marksHoverKey !== null &&
|
|
40057
|
+
const pulsing = this.scene.marksHoverKey !== null && now2 - this.scene.marksHoverSince < MARK_PULSE_MS;
|
|
39323
40058
|
if (pulsing || this.scene.marksFlash !== null) this.markPulseRaf = requestAnimationFrame(tick);
|
|
39324
40059
|
};
|
|
39325
40060
|
this.markPulseRaf = requestAnimationFrame(tick);
|
|
@@ -39880,9 +40615,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
39880
40615
|
const ext = this.externalCross;
|
|
39881
40616
|
if (!ext || this.coords.barCount === 0) return null;
|
|
39882
40617
|
const logical = Math.floor(this.coords.timeToLogical(ext.time));
|
|
39883
|
-
if (logical <
|
|
39884
|
-
const x = this.coords.logicalToX(logical);
|
|
39885
|
-
if (!Number.isFinite(x)
|
|
40618
|
+
if (!(logical < this.coords.barCount)) return null;
|
|
40619
|
+
const x = this.coords.logicalToX(Math.max(logical, -1));
|
|
40620
|
+
if (!Number.isFinite(x)) return null;
|
|
40621
|
+
const line = logical >= 0 && x >= 0 && x <= this.coords.width;
|
|
39886
40622
|
let y = null;
|
|
39887
40623
|
if (ext.price != null) {
|
|
39888
40624
|
const pricePane = this.scene.orderedPanes().find((p) => p.kind === "price");
|
|
@@ -39891,7 +40627,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
39891
40627
|
if (!Number.isFinite(y) || y < pricePane.bounds.top || y > pricePane.bounds.top + pricePane.bounds.height) y = null;
|
|
39892
40628
|
}
|
|
39893
40629
|
}
|
|
39894
|
-
return { x, y, time: this.coords.logicalToTime(logical), price: y != null ? ext.price : null };
|
|
40630
|
+
return { x, y, time: this.coords.logicalToTime(Math.max(logical, 0)), price: y != null ? ext.price : null, line };
|
|
39895
40631
|
}
|
|
39896
40632
|
/** Set the sticky magnet mode (called by the drawings toolbar's 3-state button). */
|
|
39897
40633
|
setSnapMode(mode) {
|
|
@@ -44596,6 +45332,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
44596
45332
|
this.panesControl = new PanesControl(this.orchestrator);
|
|
44597
45333
|
this.drawingsControl = new DrawingsControl(this.orchestrator.drawings);
|
|
44598
45334
|
this.marksControl = new MarksControl(this.orchestrator.marks);
|
|
45335
|
+
this.replayControl = new ReplayControl(this.orchestrator);
|
|
44599
45336
|
}
|
|
44600
45337
|
/**
|
|
44601
45338
|
* Register a scripting engine so `addIndicator({ language })` can run that
|
|
@@ -44835,6 +45572,16 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
44835
45572
|
get marks() {
|
|
44836
45573
|
return this.marksControl;
|
|
44837
45574
|
}
|
|
45575
|
+
/**
|
|
45576
|
+
* The chart's bar-replay control surface: rewind to a past bar and reveal the
|
|
45577
|
+
* following ones by hand or on a timer, from the history already loaded —
|
|
45578
|
+
* `await chart.replay.start({ from })`, `chart.replay.play(2000)`, `chart.replay.step()`,
|
|
45579
|
+
* `chart.replay.stop()`. Revealed bars behave like live bars for indicators; live
|
|
45580
|
+
* updates pause meanwhile. Follow it with the `replay:*` events.
|
|
45581
|
+
*/
|
|
45582
|
+
get replay() {
|
|
45583
|
+
return this.replayControl;
|
|
45584
|
+
}
|
|
44838
45585
|
on(event, handler) {
|
|
44839
45586
|
return this.orchestrator.events.on(event, handler);
|
|
44840
45587
|
}
|
|
@@ -44893,6 +45640,28 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
44893
45640
|
return element;
|
|
44894
45641
|
}
|
|
44895
45642
|
|
|
45643
|
+
// src/core/replayTicks.ts
|
|
45644
|
+
function lowerTimeframeTicks(chart, timeframe) {
|
|
45645
|
+
return async (bar, { end, signal }) => {
|
|
45646
|
+
const { symbol, session } = chart.market;
|
|
45647
|
+
const resolved = symbol ? chart.data.resolve(symbol) : null;
|
|
45648
|
+
const provider = resolved ? chart.data.providerInstance(resolved.provider) : void 0;
|
|
45649
|
+
if (!resolved || !provider) return [];
|
|
45650
|
+
const bars = await provider.getBars(resolved.ticker, timeframe, { from: bar.time, to: end - 1, ...session ? { session } : {} });
|
|
45651
|
+
if (signal.aborted) return [];
|
|
45652
|
+
return barsToTicks(bars.filter((b) => b.time >= bar.time && b.time < end));
|
|
45653
|
+
};
|
|
45654
|
+
}
|
|
45655
|
+
function barsToTicks(bars) {
|
|
45656
|
+
return bars.map((b) => ({
|
|
45657
|
+
price: b.close,
|
|
45658
|
+
open: b.open,
|
|
45659
|
+
high: b.high,
|
|
45660
|
+
low: b.low,
|
|
45661
|
+
...b.volume === void 0 ? {} : { volume: b.volume }
|
|
45662
|
+
}));
|
|
45663
|
+
}
|
|
45664
|
+
|
|
44896
45665
|
// src/widget/topbar-composition.ts
|
|
44897
45666
|
var TOPBAR_BUILTIN_IDS = ["symbol", "timeframes", "style", "layout", "indicators", "actions", "undo-redo", "alerts", "panels", "screenshot"];
|
|
44898
45667
|
|
|
@@ -45824,8 +46593,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
45824
46593
|
/** Block until the spacing window opens and any pause has elapsed, then reserve the next slot. */
|
|
45825
46594
|
async waitForTurn() {
|
|
45826
46595
|
for (; ; ) {
|
|
45827
|
-
const
|
|
45828
|
-
const wait = Math.max(this.pausedUntil -
|
|
46596
|
+
const now2 = this.clock.now();
|
|
46597
|
+
const wait = Math.max(this.pausedUntil - now2, this.nextStartAt - now2);
|
|
45829
46598
|
if (wait <= 0) break;
|
|
45830
46599
|
await this.clock.delay(wait);
|
|
45831
46600
|
}
|
|
@@ -46286,6 +47055,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
46286
47055
|
exports.NativeRenderer = NativeRenderer;
|
|
46287
47056
|
exports.OVERRIDABLE_TOPBAR_IDS = OVERRIDABLE_TOPBAR_IDS;
|
|
46288
47057
|
exports.RendererControl = RendererControl;
|
|
47058
|
+
exports.ReplayControl = ReplayControl;
|
|
46289
47059
|
exports.SERIES_LINE = SERIES_LINE;
|
|
46290
47060
|
exports.SESSION_OFF = SESSION_OFF;
|
|
46291
47061
|
exports.SESSION_POST = SESSION_POST;
|
|
@@ -46300,6 +47070,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
46300
47070
|
exports.VALID = VALID;
|
|
46301
47071
|
exports.Vela = Vela;
|
|
46302
47072
|
exports.WARNING = WARNING;
|
|
47073
|
+
exports.barsToTicks = barsToTicks;
|
|
46303
47074
|
exports.buildColorPicker = buildColorPicker;
|
|
46304
47075
|
exports.buildToolbar = buildToolbar;
|
|
46305
47076
|
exports.categoricalColor = categoricalColor;
|
|
@@ -46318,6 +47089,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
46318
47089
|
exports.inputVisible = inputVisible;
|
|
46319
47090
|
exports.legendActions = legendActions;
|
|
46320
47091
|
exports.legendCallouts = legendCallouts;
|
|
47092
|
+
exports.lowerTimeframeTicks = lowerTimeframeTicks;
|
|
46321
47093
|
exports.nativeIndicatorDescriptors = nativeIndicatorDescriptors;
|
|
46322
47094
|
exports.nativeIndicatorTypes = nativeIndicatorTypes;
|
|
46323
47095
|
exports.normalizeSettingsRow = normalizeSettingsRow;
|