@luxalgo/vela 0.7.8 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +28 -28
- package/dist/{DataProvider-CGa4KHRa.d.cts → DataProvider-BrlxntPN.d.cts} +1 -1
- package/dist/{DataProvider-CmWUtDVL.d.ts → DataProvider-DwFIoV4r.d.ts} +1 -1
- package/dist/{chunk-WTWGEB7K.js → chunk-2IAETM2I.js} +260 -2
- package/dist/{chunk-CAFCLMPF.js → chunk-BZQM2XO7.js} +1 -0
- package/dist/{chunk-M6GP7DAX.js → chunk-E3GU6AUI.js} +459 -37
- package/dist/{chunk-BFA32GOU.js → chunk-EZ5FWVLA.js} +5 -2
- package/dist/{chunk-PVGWNF7V.js → chunk-KA77UTRF.js} +2 -2
- package/dist/{chunk-N4ZZYLMP.js → chunk-QBPUT5SU.js} +686 -251
- package/dist/{contributions-DF_Ea07m.d.cts → contributions-CtlptxR8.d.cts} +187 -5
- package/dist/{contributions-nKOSeLy8.d.ts → contributions-tjNExo1o.d.ts} +187 -5
- package/dist/index.cjs +966 -247
- package/dist/index.d.cts +24 -8
- package/dist/index.d.ts +24 -8
- package/dist/index.js +28 -4
- package/dist/{options-D1AJKsn_.d.ts → options-BnEPUuVV.d.cts} +1 -1
- package/dist/{options-D1AJKsn_.d.cts → options-BnEPUuVV.d.ts} +1 -1
- package/dist/{plugin-6F04gjQe.d.cts → plugin-BWxJRKzS.d.cts} +10 -4
- package/dist/{plugin-BLsNipkc.d.ts → plugin-e2WUEtiI.d.ts} +10 -4
- package/dist/plugin.cjs +1 -0
- package/dist/plugin.d.cts +4 -4
- package/dist/plugin.d.ts +4 -4
- package/dist/plugin.js +2 -2
- package/dist/providers/binance.d.cts +2 -2
- package/dist/providers/binance.d.ts +2 -2
- package/dist/providers/coinbase.d.cts +2 -2
- package/dist/providers/coinbase.d.ts +2 -2
- package/dist/providers/hyperliquid.d.cts +2 -2
- package/dist/providers/hyperliquid.d.ts +2 -2
- package/dist/{statusline-model-DAFyqVmR.d.ts → statusline-model-DFY7tuHj.d.cts} +5 -3
- package/dist/{statusline-model-BoWJ3cWQ.d.cts → statusline-model-zKXtGIjj.d.ts} +5 -3
- package/dist/ui.cjs +263 -0
- package/dist/ui.d.cts +75 -2
- package/dist/ui.d.ts +75 -2
- package/dist/ui.js +3 -3
- package/dist/vela.global.js +968 -249
- package/dist/vela.global.min.js +73 -71
- package/dist/widget.cjs +1411 -292
- package/dist/widget.d.cts +26 -7
- package/dist/widget.d.ts +26 -7
- package/dist/widget.js +7 -7
- package/dist/workspace.cjs +1414 -292
- package/dist/workspace.d.cts +124 -6
- package/dist/workspace.d.ts +124 -6
- package/dist/workspace.js +6 -6
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -615,6 +615,7 @@ registerIcon("chevron-down", S('<path d="M3.5 6 8 10.5 12.5 6"/>'));
|
|
|
615
615
|
registerIcon("chevron-up", S('<path d="M3.5 10 8 5.5 12.5 10"/>'));
|
|
616
616
|
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"/>'));
|
|
617
617
|
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"/>'));
|
|
618
|
+
registerIcon("replay", S('<path d="M14 4.2v7.6L8.6 8z"/><path d="M7.4 4.2v7.6L2 8z"/>'));
|
|
618
619
|
registerIcon("check", S('<path d="m2.8 8.4 3.4 3.4 7-7.6"/>'));
|
|
619
620
|
registerIcon("close", S('<path d="m3.8 3.8 8.4 8.4M12.2 3.8l-8.4 8.4"/>'));
|
|
620
621
|
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"/>'));
|
|
@@ -7832,6 +7833,7 @@ var CHUNK_BARS = 1e4;
|
|
|
7832
7833
|
var FIRST_PAINT_BARS = 20;
|
|
7833
7834
|
var GAP_FACTOR = 1.5;
|
|
7834
7835
|
var HEAL_COOLDOWN_MS = 5e3;
|
|
7836
|
+
var MIN_TICK_UPDATE_MS = 16;
|
|
7835
7837
|
var EngineOrchestrator = class _EngineOrchestrator {
|
|
7836
7838
|
constructor(container, renderer, feed, engines, config, dataControl) {
|
|
7837
7839
|
this.renderer = renderer;
|
|
@@ -7884,6 +7886,32 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
7884
7886
|
this.lastHealAt = 0;
|
|
7885
7887
|
/** The `visibilitychange` catch-up listener (removed on destroy). */
|
|
7886
7888
|
this.onVisible = null;
|
|
7889
|
+
// ── bar replay: `rawBars` holds the history up to the cursor, the rest waits here ──
|
|
7890
|
+
/** Bars still hidden right of the replay cursor, oldest first; null ≡ replay off. */
|
|
7891
|
+
this.replayQueue = null;
|
|
7892
|
+
this.replayTimer = null;
|
|
7893
|
+
this.replayPlaying = false;
|
|
7894
|
+
this.replayIntervalMs = 1e3;
|
|
7895
|
+
/** The depth (`market.bars`) before a replay deepened the history — restored when it ends. */
|
|
7896
|
+
this.replayDepth = null;
|
|
7897
|
+
/** A start is loading older history before it can cut; `seek` = a replay was running
|
|
7898
|
+
* (and left silently to deepen), so abandoning the load must announce its end. */
|
|
7899
|
+
this.replayLoading = null;
|
|
7900
|
+
/** A replay carried over a timeframe/session switch, between the old series' silent end
|
|
7901
|
+
* and the cut of the new one: still ACTIVE for the outside (`replayState`), resumed
|
|
7902
|
+
* at `revealedEnd` once the new bars land, unless stopped (`cancelled`) meanwhile. */
|
|
7903
|
+
this.replayCarrying = null;
|
|
7904
|
+
/** Intrabar prices for the bars replay reveals — null ≡ whole bars. */
|
|
7905
|
+
this.replayTickSource = null;
|
|
7906
|
+
/** The bar being revealed tick by tick: its stored form (what it settles on), its ticks,
|
|
7907
|
+
* the next tick to apply, the candle built so far, the volume each tick adds when the
|
|
7908
|
+
* ticks carry none, and its pacing (ticks per update, ms between updates — one play
|
|
7909
|
+
* interval per tick — and the clock time tick 0 is due at, so late timers catch up). */
|
|
7910
|
+
this.replayForming = null;
|
|
7911
|
+
/** Tick requests by bar time, shared by prefetch and playback; `ticks` once they landed. */
|
|
7912
|
+
this.replayTickFetches = /* @__PURE__ */ new Map();
|
|
7913
|
+
/** Aborts the tick requests in flight — renewed whenever the replay moves on. */
|
|
7914
|
+
this.replayTickAbort = null;
|
|
7887
7915
|
/** What the last applied bar did: refine the open candle, or open a new one (which
|
|
7888
7916
|
* settles the previous). Read by the `script:run` cause attribution. */
|
|
7889
7917
|
this.barCause = "tick";
|
|
@@ -8107,6 +8135,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
8107
8135
|
const paint = (bars, final) => {
|
|
8108
8136
|
if (this.generation !== gen || !final && bars.length === 0) return;
|
|
8109
8137
|
if (!painted && !final && bars.length < Math.min(requested, FIRST_PAINT_BARS)) return;
|
|
8138
|
+
if (painted && this.replayQueue) {
|
|
8139
|
+
this.mergeHistoryIntoReplay(bars);
|
|
8140
|
+
return;
|
|
8141
|
+
}
|
|
8110
8142
|
this.setBarSeries(bars, painted ? { preserveView: true } : void 0);
|
|
8111
8143
|
if (!painted && bars.length > 0) {
|
|
8112
8144
|
painted = true;
|
|
@@ -8249,6 +8281,36 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
8249
8281
|
// exactly like a timeframe change; the cache keys the sessions apart.
|
|
8250
8282
|
next.session !== void 0 && next.session !== m.session || next.data !== void 0;
|
|
8251
8283
|
const depthChanged = next.bars !== void 0 && next.bars !== m.bars;
|
|
8284
|
+
const sameClock = identityChanged && next.data === void 0 && (next.symbol === void 0 || next.symbol === m.symbol) && (next.timeframe !== void 0 || next.session !== void 0);
|
|
8285
|
+
const pending = identityChanged ? this.replayCarrying : null;
|
|
8286
|
+
if (pending) {
|
|
8287
|
+
this.replayCarrying = null;
|
|
8288
|
+
if (!sameClock) {
|
|
8289
|
+
pending.cancelled = true;
|
|
8290
|
+
this.events.emit("replay:end", { reason: "market" });
|
|
8291
|
+
}
|
|
8292
|
+
}
|
|
8293
|
+
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;
|
|
8294
|
+
if (pending) pending.cancelled = true;
|
|
8295
|
+
if (replayCarry) this.replayCarrying = replayCarry;
|
|
8296
|
+
if (replayCarry && this.replayQueue) {
|
|
8297
|
+
this.endReplay(null, { restore: false, resume: false });
|
|
8298
|
+
const depth = this.replayDepth;
|
|
8299
|
+
this.replayDepth = null;
|
|
8300
|
+
if (depth && next.bars === void 0) {
|
|
8301
|
+
if (depth.bars === void 0) delete m.bars;
|
|
8302
|
+
else m.bars = depth.bars;
|
|
8303
|
+
}
|
|
8304
|
+
}
|
|
8305
|
+
if (this.replayQueue && (identityChanged || depthChanged)) this.endReplay("market", { restore: !identityChanged, resume: false });
|
|
8306
|
+
if (this.replayLoading && (identityChanged || depthChanged)) {
|
|
8307
|
+
const depth = this.replayDepth;
|
|
8308
|
+
this.abandonReplayLoad("market");
|
|
8309
|
+
if (depth && next.bars === void 0) {
|
|
8310
|
+
if (depth.bars === void 0) delete m.bars;
|
|
8311
|
+
else m.bars = depth.bars;
|
|
8312
|
+
}
|
|
8313
|
+
}
|
|
8252
8314
|
const depthOnly = depthChanged && !identityChanged && this.rawBars.length > 0 && !m.data?.length && ((next.bars ?? 0) <= this.rawBars.length || typeof this.feed.loadRange === "function");
|
|
8253
8315
|
if (!identityChanged && !depthChanged) {
|
|
8254
8316
|
if (typeof next.visibleRange === "string") this.setVisibleRangePreset(next.visibleRange);
|
|
@@ -8306,15 +8368,20 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
8306
8368
|
const from = bars[Math.max(0, idx - (carriedCount - 1))].time;
|
|
8307
8369
|
this.renderer.setVisibleRange({ from, to: Math.max(carried.right, bars[idx].time + 1) });
|
|
8308
8370
|
}
|
|
8309
|
-
|
|
8371
|
+
const carry = replayCarry && !replayCarry.cancelled ? replayCarry : null;
|
|
8372
|
+
const carryFrom = carry ? carry.revealedEnd - this.barIntervalMs() : 0;
|
|
8373
|
+
const carryNow = carry != null && this.rawBars.length > 0 && carryFrom >= this.rawBars[0].time;
|
|
8374
|
+
const replayBack = carryNow && this.resumeCarriedReplay(this.replayStart({ from: carryFrom }), carry);
|
|
8375
|
+
if (!depthOnly && !replayBack) {
|
|
8310
8376
|
this.restartNativeIndicators();
|
|
8311
8377
|
this.restartChartTypeEngine();
|
|
8312
8378
|
this.reexecuteIndicators();
|
|
8313
8379
|
}
|
|
8314
|
-
this.startLive();
|
|
8380
|
+
if (!replayBack) this.startLive();
|
|
8315
8381
|
if (identityChanged) {
|
|
8316
8382
|
this.events.emit("market:changed", { symbol: m.symbol ?? "TEST", timeframe: m.timeframe ?? "60", prev: prev2 });
|
|
8317
8383
|
}
|
|
8384
|
+
if (carry && !carryNow) this.resumeCarriedReplay(this.replayStart({ from: carryFrom }), carry);
|
|
8318
8385
|
}
|
|
8319
8386
|
/**
|
|
8320
8387
|
* Restart every native indicator over the new market: a `NativeIndicatorContext`
|
|
@@ -8438,6 +8505,397 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
8438
8505
|
historyComplete() {
|
|
8439
8506
|
return this.historyCompletePromise;
|
|
8440
8507
|
}
|
|
8508
|
+
// ── bar replay ──────────────────────────────────────────────
|
|
8509
|
+
/**
|
|
8510
|
+
* Enter replay — or seek, when already replaying — keeping every bar opened at or before
|
|
8511
|
+
* `opts.from` and queueing the rest. A `from` inside the loaded bars cuts at once, even
|
|
8512
|
+
* while older history is still streaming in: what lands later joins the tape's head
|
|
8513
|
+
* (see {@link mergeHistoryIntoReplay}) and leaves the cut alone. An older `from` waits
|
|
8514
|
+
* for that load, then deepens if it still has to. Consumers restart over the cut exactly
|
|
8515
|
+
* as on a market switch — their models describe bars that are now hidden — and the
|
|
8516
|
+
* active chart-type engine is rebuilt without live data.
|
|
8517
|
+
*/
|
|
8518
|
+
async replayStart(opts) {
|
|
8519
|
+
if (this.rawBars.length === 0) {
|
|
8520
|
+
console.warn("[vela] chart.replay.start() ignored \u2014 the chart has no bars yet");
|
|
8521
|
+
return;
|
|
8522
|
+
}
|
|
8523
|
+
let gen = this.generation;
|
|
8524
|
+
if (opts.from < this.rawBars[0].time) {
|
|
8525
|
+
await this.historyCompletePromise;
|
|
8526
|
+
if (this.generation !== gen || this.rawBars.length === 0) return;
|
|
8527
|
+
}
|
|
8528
|
+
const oldest = this.rawBars[0].time;
|
|
8529
|
+
if (opts.from < oldest && this.canHeal() && typeof this.feed.loadRange === "function") {
|
|
8530
|
+
const held = this.rawBars.length + (this.replayQueue?.length ?? 0);
|
|
8531
|
+
const bars = held + Math.ceil((oldest - opts.from) / this.barIntervalMs()) + 1;
|
|
8532
|
+
const seek = this.replayQueue !== null;
|
|
8533
|
+
if (seek) this.endReplay(null, { restore: true, resume: false });
|
|
8534
|
+
this.replayDepth ?? (this.replayDepth = { bars: this.config.market.bars });
|
|
8535
|
+
const deepened = this.setMarket({ bars });
|
|
8536
|
+
this.replayLoading = { seek };
|
|
8537
|
+
await deepened;
|
|
8538
|
+
gen = this.generation;
|
|
8539
|
+
await this.historyCompletePromise;
|
|
8540
|
+
if (!this.replayLoading || this.generation !== gen || this.rawBars.length === 0) return;
|
|
8541
|
+
this.replayLoading = null;
|
|
8542
|
+
}
|
|
8543
|
+
this.settleFormingBar();
|
|
8544
|
+
this.dropTickFetches();
|
|
8545
|
+
const tape = this.replayQueue ? [...this.rawBars, ...this.replayQueue] : this.rawBars;
|
|
8546
|
+
let cut = 0;
|
|
8547
|
+
while (cut < tape.length && tape[cut].time <= opts.from) cut += 1;
|
|
8548
|
+
cut = Math.max(1, cut);
|
|
8549
|
+
if (cut >= tape.length) {
|
|
8550
|
+
console.warn("[vela] chart.replay.start() ignored \u2014 no bar opens after the requested time");
|
|
8551
|
+
return;
|
|
8552
|
+
}
|
|
8553
|
+
const wasPlaying = this.replayPlaying;
|
|
8554
|
+
this.clearReplayTimer();
|
|
8555
|
+
if (!this.replayQueue) this.stopLive();
|
|
8556
|
+
this.replayQueue = tape.slice(cut);
|
|
8557
|
+
this.setBarSeries(tape.slice(0, cut));
|
|
8558
|
+
this.restartBarConsumers({ blank: true });
|
|
8559
|
+
if (wasPlaying) this.events.emit("replay:pause", void 0);
|
|
8560
|
+
this.events.emit("replay:start", { cursorTime: tape[cut - 1].time, remaining: this.replayQueue.length });
|
|
8561
|
+
this.prefetchTicks();
|
|
8562
|
+
}
|
|
8563
|
+
/** Reveal the next queued bar through the live-bar path — or complete the bar forming
|
|
8564
|
+
* tick by tick; the last one ends the replay. */
|
|
8565
|
+
replayStep() {
|
|
8566
|
+
if (this.replayForming) {
|
|
8567
|
+
this.finishFormingBar();
|
|
8568
|
+
return true;
|
|
8569
|
+
}
|
|
8570
|
+
const queue = this.replayQueue;
|
|
8571
|
+
const bar = queue?.shift();
|
|
8572
|
+
if (!queue || !bar) return false;
|
|
8573
|
+
this.replayTickFetches.delete(bar.time);
|
|
8574
|
+
this.applyBar(bar);
|
|
8575
|
+
this.completeReveal(bar);
|
|
8576
|
+
this.prefetchTicks();
|
|
8577
|
+
return true;
|
|
8578
|
+
}
|
|
8579
|
+
/**
|
|
8580
|
+
* Follow up a replay carried onto a new series of the same symbol (another timeframe or
|
|
8581
|
+
* session). The cut asked for keeps the bars that CLOSED by the end of what was revealed —
|
|
8582
|
+
* every finer bar of the last revealed one, never a coarser bar still open at that point
|
|
8583
|
+
* (no look-ahead). Once it settles: playback resumes if it was on; if the replay did not
|
|
8584
|
+
* come back (nothing left after that point, or a later switch took over), it ends as a
|
|
8585
|
+
* market switch would. Returns whether the replay is back already — a start with no
|
|
8586
|
+
* history to wait for cuts synchronously (an async function runs up to its first await).
|
|
8587
|
+
*/
|
|
8588
|
+
resumeCarriedReplay(start, carry) {
|
|
8589
|
+
if (this.replayQueue && this.replayCarrying === carry) this.replayCarrying = null;
|
|
8590
|
+
void start.then(() => {
|
|
8591
|
+
if (this.replayCarrying === carry) this.replayCarrying = null;
|
|
8592
|
+
if (carry.cancelled) return;
|
|
8593
|
+
if (!this.replayQueue) this.events.emit("replay:end", { reason: "market" });
|
|
8594
|
+
else if (carry.playing) this.replayPlay();
|
|
8595
|
+
});
|
|
8596
|
+
return this.replayQueue !== null;
|
|
8597
|
+
}
|
|
8598
|
+
replayStepUpdate() {
|
|
8599
|
+
if (!this.replayQueue) return false;
|
|
8600
|
+
if (this.replayForming) {
|
|
8601
|
+
this.applyReplayTicks(1);
|
|
8602
|
+
return true;
|
|
8603
|
+
}
|
|
8604
|
+
const bar = this.replayQueue[0];
|
|
8605
|
+
if (!bar) return false;
|
|
8606
|
+
const entry = this.replayTickSource ? this.requestTicks(bar) : null;
|
|
8607
|
+
if (entry && !entry.ticks) {
|
|
8608
|
+
const epoch = this.replayTickAbort;
|
|
8609
|
+
void entry.done.then(() => {
|
|
8610
|
+
if (!this.replayPlaying && !this.replayForming && this.replayTickAbort === epoch && this.replayQueue?.[0] === bar) this.replayStepUpdate();
|
|
8611
|
+
});
|
|
8612
|
+
return true;
|
|
8613
|
+
}
|
|
8614
|
+
if (!entry?.ticks?.length) return this.replayStep();
|
|
8615
|
+
this.openFormingBar(entry.ticks, 1);
|
|
8616
|
+
return true;
|
|
8617
|
+
}
|
|
8618
|
+
replaySetTicks(source) {
|
|
8619
|
+
this.finishFormingBar();
|
|
8620
|
+
this.dropTickFetches();
|
|
8621
|
+
this.replayTickSource = source;
|
|
8622
|
+
this.prefetchTicks();
|
|
8623
|
+
}
|
|
8624
|
+
replayPlay(intervalMs2) {
|
|
8625
|
+
if (!this.replayQueue) return;
|
|
8626
|
+
if (intervalMs2 !== void 0) {
|
|
8627
|
+
if (!(Number.isFinite(intervalMs2) && intervalMs2 > 0)) {
|
|
8628
|
+
console.warn(`[vela] chart.replay.play(${intervalMs2}) ignored \u2014 the interval must be a positive number of ms`);
|
|
8629
|
+
return;
|
|
8630
|
+
}
|
|
8631
|
+
this.replayIntervalMs = intervalMs2;
|
|
8632
|
+
}
|
|
8633
|
+
this.clearReplayTimer();
|
|
8634
|
+
this.replayPlaying = true;
|
|
8635
|
+
if (this.replayForming) this.paceFormingBar(this.replayForming);
|
|
8636
|
+
this.scheduleReplayStep(this.replayForming?.delayMs);
|
|
8637
|
+
this.events.emit("replay:play", { intervalMs: this.replayIntervalMs });
|
|
8638
|
+
}
|
|
8639
|
+
replayPause() {
|
|
8640
|
+
if (!this.replayPlaying) return;
|
|
8641
|
+
this.clearReplayTimer();
|
|
8642
|
+
this.events.emit("replay:pause", void 0);
|
|
8643
|
+
}
|
|
8644
|
+
replayStop() {
|
|
8645
|
+
const carrying = this.replayCarrying;
|
|
8646
|
+
if (carrying) {
|
|
8647
|
+
carrying.cancelled = true;
|
|
8648
|
+
this.replayCarrying = null;
|
|
8649
|
+
this.events.emit("replay:end", { reason: "stopped" });
|
|
8650
|
+
}
|
|
8651
|
+
if (this.replayLoading) {
|
|
8652
|
+
const depth = this.replayDepth;
|
|
8653
|
+
this.abandonReplayLoad("stopped");
|
|
8654
|
+
void this.setMarket({ bars: depth?.bars ?? 500 });
|
|
8655
|
+
return;
|
|
8656
|
+
}
|
|
8657
|
+
this.endReplay("stopped", { restore: true, resume: true });
|
|
8658
|
+
}
|
|
8659
|
+
abandonReplayLoad(reason) {
|
|
8660
|
+
const loading = this.replayLoading;
|
|
8661
|
+
if (!loading) return;
|
|
8662
|
+
this.replayLoading = null;
|
|
8663
|
+
this.replayDepth = null;
|
|
8664
|
+
if (loading.seek) this.events.emit("replay:end", { reason });
|
|
8665
|
+
}
|
|
8666
|
+
replayState() {
|
|
8667
|
+
const active = this.replayQueue !== null;
|
|
8668
|
+
return {
|
|
8669
|
+
active: active || this.replayCarrying !== null,
|
|
8670
|
+
// a replay carried over a switch stays on while the new bars load
|
|
8671
|
+
playing: this.replayCarrying?.playing ?? this.replayPlaying,
|
|
8672
|
+
cursorTime: active ? this.rawBars[this.rawBars.length - 1]?.time ?? null : null,
|
|
8673
|
+
remaining: this.replayQueue?.length ?? 0,
|
|
8674
|
+
nextTime: this.replayForming?.bar.time ?? this.replayQueue?.[0]?.time ?? null,
|
|
8675
|
+
intervalMs: this.replayIntervalMs
|
|
8676
|
+
};
|
|
8677
|
+
}
|
|
8678
|
+
replayBounds() {
|
|
8679
|
+
const first = this.rawBars[0];
|
|
8680
|
+
const last = this.replayQueue?.[this.replayQueue.length - 1] ?? this.rawBars[this.rawBars.length - 1];
|
|
8681
|
+
return first && last ? { first: first.time, last: last.time } : null;
|
|
8682
|
+
}
|
|
8683
|
+
scheduleReplayStep(delayMs = this.replayIntervalMs) {
|
|
8684
|
+
this.replayTimer = setTimeout(() => {
|
|
8685
|
+
this.replayTimer = null;
|
|
8686
|
+
this.replayAdvance();
|
|
8687
|
+
}, delayMs);
|
|
8688
|
+
}
|
|
8689
|
+
/** One beat of timed playback: the forming bar's next ticks, else the next bar — whole,
|
|
8690
|
+
* or opened from its ticks (waiting for them when they have not landed yet). */
|
|
8691
|
+
replayAdvance() {
|
|
8692
|
+
if (!this.replayPlaying || !this.replayQueue) return;
|
|
8693
|
+
const forming = this.replayForming;
|
|
8694
|
+
if (forming) {
|
|
8695
|
+
const due = Math.floor((now() - forming.origin) / this.replayIntervalMs) + 1;
|
|
8696
|
+
this.applyReplayTicks(Math.max(forming.batch, due - forming.next));
|
|
8697
|
+
if (this.replayPlaying) this.scheduleReplayStep(forming.delayMs);
|
|
8698
|
+
return;
|
|
8699
|
+
}
|
|
8700
|
+
const bar = this.replayQueue[0];
|
|
8701
|
+
if (!bar) return;
|
|
8702
|
+
const entry = this.replayTickSource ? this.requestTicks(bar) : null;
|
|
8703
|
+
if (entry && !entry.ticks) {
|
|
8704
|
+
const epoch = this.replayTickAbort;
|
|
8705
|
+
void entry.done.then(() => {
|
|
8706
|
+
if (this.replayPlaying && this.replayTimer == null && this.replayTickAbort === epoch && this.replayQueue?.[0] === bar) this.replayAdvance();
|
|
8707
|
+
});
|
|
8708
|
+
return;
|
|
8709
|
+
}
|
|
8710
|
+
if (!entry?.ticks?.length) {
|
|
8711
|
+
if (this.replayStep() && this.replayPlaying) this.scheduleReplayStep();
|
|
8712
|
+
return;
|
|
8713
|
+
}
|
|
8714
|
+
this.openFormingBar(entry.ticks);
|
|
8715
|
+
if (this.replayPlaying) this.scheduleReplayStep(this.replayForming?.delayMs);
|
|
8716
|
+
}
|
|
8717
|
+
/** Take the next queued bar off the tape and open it at its first tick (the first
|
|
8718
|
+
* `count` ticks — default: one timed update's worth). */
|
|
8719
|
+
openFormingBar(ticks, count) {
|
|
8720
|
+
const bar = this.replayQueue.shift();
|
|
8721
|
+
const volumeEach = ticks.every((t) => t.volume === void 0) ? (bar.volume ?? 0) / ticks.length : null;
|
|
8722
|
+
const forming = { bar, ticks, next: 0, candle: null, volumeEach, batch: 1, delayMs: this.replayIntervalMs, origin: 0 };
|
|
8723
|
+
this.paceFormingBar(forming);
|
|
8724
|
+
this.replayForming = forming;
|
|
8725
|
+
this.applyReplayTicks(count ?? forming.batch);
|
|
8726
|
+
this.prefetchTicks();
|
|
8727
|
+
}
|
|
8728
|
+
/** One tick per interval, batched when the interval is shorter than a frame; the clock
|
|
8729
|
+
* is anchored on the ticks already applied (a resume or a pace change carries on from
|
|
8730
|
+
* there, never jumps). */
|
|
8731
|
+
paceFormingBar(f) {
|
|
8732
|
+
f.batch = Math.max(1, Math.ceil(MIN_TICK_UPDATE_MS / this.replayIntervalMs));
|
|
8733
|
+
f.delayMs = this.replayIntervalMs * f.batch;
|
|
8734
|
+
f.origin = now() - Math.max(0, f.next - 1) * this.replayIntervalMs;
|
|
8735
|
+
}
|
|
8736
|
+
/** Fold the next `count` ticks into the forming candle; the last one settles the bar. */
|
|
8737
|
+
applyReplayTicks(count) {
|
|
8738
|
+
const f = this.replayForming;
|
|
8739
|
+
if (!f) return;
|
|
8740
|
+
let c = f.candle;
|
|
8741
|
+
const stop = Math.min(f.ticks.length, f.next + count);
|
|
8742
|
+
for (; f.next < stop; f.next += 1) {
|
|
8743
|
+
const t = f.ticks[f.next];
|
|
8744
|
+
const v = f.volumeEach ?? t.volume ?? 0;
|
|
8745
|
+
const hi = Math.max(t.high ?? t.price, t.price);
|
|
8746
|
+
const lo = Math.min(t.low ?? t.price, t.price);
|
|
8747
|
+
if (c) {
|
|
8748
|
+
c = { ...c, high: Math.max(c.high, hi), low: Math.min(c.low, lo), close: t.price, volume: (c.volume ?? 0) + v };
|
|
8749
|
+
} else {
|
|
8750
|
+
const open2 = t.open ?? t.price;
|
|
8751
|
+
c = { time: f.bar.time, open: open2, high: Math.max(open2, hi), low: Math.min(open2, lo), close: t.price, volume: v };
|
|
8752
|
+
}
|
|
8753
|
+
}
|
|
8754
|
+
f.candle = c;
|
|
8755
|
+
if (f.next >= f.ticks.length) {
|
|
8756
|
+
this.finishFormingBar();
|
|
8757
|
+
return;
|
|
8758
|
+
}
|
|
8759
|
+
this.applyBar(c);
|
|
8760
|
+
this.events.emit("replay:tick", { cursorTime: f.bar.time, index: f.next - 1, count: f.ticks.length });
|
|
8761
|
+
}
|
|
8762
|
+
/** Settle the forming bar on its stored values and announce it revealed. */
|
|
8763
|
+
finishFormingBar() {
|
|
8764
|
+
const f = this.replayForming;
|
|
8765
|
+
if (!f) return;
|
|
8766
|
+
this.replayForming = null;
|
|
8767
|
+
this.replayTickFetches.delete(f.bar.time);
|
|
8768
|
+
this.applyBar(f.bar);
|
|
8769
|
+
this.events.emit("replay:tick", { cursorTime: f.bar.time, index: f.ticks.length - 1, count: f.ticks.length });
|
|
8770
|
+
this.completeReveal(f.bar);
|
|
8771
|
+
}
|
|
8772
|
+
/** Put the forming bar back in its stored form, silently — a seek or an end rebuilds
|
|
8773
|
+
* from the tape, which must hold the stored bar. */
|
|
8774
|
+
settleFormingBar() {
|
|
8775
|
+
const f = this.replayForming;
|
|
8776
|
+
if (!f) return;
|
|
8777
|
+
this.replayForming = null;
|
|
8778
|
+
this.applyBar(f.bar);
|
|
8779
|
+
}
|
|
8780
|
+
/**
|
|
8781
|
+
* A history answer landing mid-replay (a progressive snapshot — the whole answer so far):
|
|
8782
|
+
* the tape keeps its cut and its hidden queue; bars older than the tape join its head,
|
|
8783
|
+
* bars newer than it join the queue's end.
|
|
8784
|
+
*/
|
|
8785
|
+
mergeHistoryIntoReplay(bars) {
|
|
8786
|
+
const queue = this.replayQueue;
|
|
8787
|
+
const head = this.rawBars[0];
|
|
8788
|
+
if (!queue || !head) return;
|
|
8789
|
+
const tail = queue[queue.length - 1] ?? this.rawBars[this.rawBars.length - 1];
|
|
8790
|
+
let older = 0;
|
|
8791
|
+
while (older < bars.length && bars[older].time < head.time) older += 1;
|
|
8792
|
+
let newer = bars.length;
|
|
8793
|
+
while (newer > older && bars[newer - 1].time > tail.time) newer -= 1;
|
|
8794
|
+
if (newer < bars.length) queue.push(...bars.slice(newer));
|
|
8795
|
+
if (older > 0) {
|
|
8796
|
+
this.setBarSeries([...bars.slice(0, older), ...this.rawBars], { preserveView: true });
|
|
8797
|
+
this.notifySessionsBars("backfill");
|
|
8798
|
+
}
|
|
8799
|
+
}
|
|
8800
|
+
/** A bar is fully on screen: announce it, and end the replay if it was the last. */
|
|
8801
|
+
completeReveal(bar) {
|
|
8802
|
+
const queue = this.replayQueue;
|
|
8803
|
+
if (!queue) return;
|
|
8804
|
+
this.events.emit("replay:step", { cursorTime: bar.time, remaining: queue.length });
|
|
8805
|
+
if (queue.length === 0) this.endReplay("finished", { restore: true, resume: true });
|
|
8806
|
+
}
|
|
8807
|
+
/** Ask for the ticks of the next hidden bar ahead of time. */
|
|
8808
|
+
prefetchTicks() {
|
|
8809
|
+
const next = this.replayQueue?.[0];
|
|
8810
|
+
if (next && this.replayTickSource) this.requestTicks(next);
|
|
8811
|
+
}
|
|
8812
|
+
/** The (shared) tick request of a queued bar — the source runs once per bar. */
|
|
8813
|
+
requestTicks(bar) {
|
|
8814
|
+
const known = this.replayTickFetches.get(bar.time);
|
|
8815
|
+
if (known) return known;
|
|
8816
|
+
const source = this.replayTickSource;
|
|
8817
|
+
this.replayTickAbort ?? (this.replayTickAbort = new AbortController());
|
|
8818
|
+
const signal = this.replayTickAbort.signal;
|
|
8819
|
+
const queue = this.replayQueue ?? [];
|
|
8820
|
+
const end = queue[queue.indexOf(bar) + 1]?.time ?? bar.time + this.barIntervalMs();
|
|
8821
|
+
const entry = { ticks: null, done: Promise.resolve() };
|
|
8822
|
+
const land = (ticks) => {
|
|
8823
|
+
entry.ticks = usableTicks(ticks);
|
|
8824
|
+
};
|
|
8825
|
+
const fail = (e) => {
|
|
8826
|
+
if (!signal.aborted) console.warn(`[vela] replay tick source failed \u2014 revealing the bar whole (${e instanceof Error ? e.message : String(e)})`);
|
|
8827
|
+
land([]);
|
|
8828
|
+
};
|
|
8829
|
+
try {
|
|
8830
|
+
const out = source(bar, { end, signal });
|
|
8831
|
+
if (Array.isArray(out)) land(out);
|
|
8832
|
+
else entry.done = Promise.resolve(out).then(land, fail);
|
|
8833
|
+
} catch (e) {
|
|
8834
|
+
fail(e);
|
|
8835
|
+
}
|
|
8836
|
+
this.replayTickFetches.set(bar.time, entry);
|
|
8837
|
+
return entry;
|
|
8838
|
+
}
|
|
8839
|
+
/** Abort the tick requests in flight and forget the landed ones. */
|
|
8840
|
+
dropTickFetches() {
|
|
8841
|
+
this.replayTickAbort?.abort();
|
|
8842
|
+
this.replayTickAbort = null;
|
|
8843
|
+
this.replayTickFetches.clear();
|
|
8844
|
+
}
|
|
8845
|
+
clearReplayTimer() {
|
|
8846
|
+
if (this.replayTimer != null) clearTimeout(this.replayTimer);
|
|
8847
|
+
this.replayTimer = null;
|
|
8848
|
+
this.replayPlaying = false;
|
|
8849
|
+
}
|
|
8850
|
+
/**
|
|
8851
|
+
* Leave replay. `restore` puts the queued bars back and restarts the consumers over the
|
|
8852
|
+
* full history (a market switch skips it: its load replaces everything anyway); `resume`
|
|
8853
|
+
* restarts live updates and heals the bars that closed while replaying. A null reason
|
|
8854
|
+
* leaves silently — a seek that re-enters right after.
|
|
8855
|
+
*/
|
|
8856
|
+
endReplay(reason, opts) {
|
|
8857
|
+
const queue = this.replayQueue;
|
|
8858
|
+
if (!queue) return;
|
|
8859
|
+
this.clearReplayTimer();
|
|
8860
|
+
if (opts.restore) this.settleFormingBar();
|
|
8861
|
+
else this.replayForming = null;
|
|
8862
|
+
this.dropTickFetches();
|
|
8863
|
+
this.replayQueue = null;
|
|
8864
|
+
const depth = reason ? this.replayDepth : null;
|
|
8865
|
+
if (reason) this.replayDepth = null;
|
|
8866
|
+
if (depth) {
|
|
8867
|
+
if (depth.bars === void 0) delete this.config.market.bars;
|
|
8868
|
+
else this.config.market.bars = depth.bars;
|
|
8869
|
+
}
|
|
8870
|
+
if (opts.restore) {
|
|
8871
|
+
const keep = depth ? depth.bars ?? 500 : Infinity;
|
|
8872
|
+
if (queue.length > 0 || this.rawBars.length > keep) {
|
|
8873
|
+
const full = [...this.rawBars, ...queue];
|
|
8874
|
+
this.setBarSeries(full.length > keep ? full.slice(full.length - keep) : full);
|
|
8875
|
+
this.restartBarConsumers({ blank: false });
|
|
8876
|
+
} else if (this.config.live) {
|
|
8877
|
+
this.restartNativeIndicators();
|
|
8878
|
+
this.restartChartTypeEngine();
|
|
8879
|
+
}
|
|
8880
|
+
}
|
|
8881
|
+
if (opts.resume && this.config.live) {
|
|
8882
|
+
this.startLive();
|
|
8883
|
+
const last = this.rawBars[this.rawBars.length - 1];
|
|
8884
|
+
if (last && this.canHeal()) void this.healGap(last.time);
|
|
8885
|
+
}
|
|
8886
|
+
if (reason) this.events.emit("replay:end", { reason });
|
|
8887
|
+
}
|
|
8888
|
+
/**
|
|
8889
|
+
* The bars were replaced by a cut or a restore of the SAME market: every consumer re-runs.
|
|
8890
|
+
* A cut also blanks the mounted models first — they still paint the hidden bars' values,
|
|
8891
|
+
* which must not show until the re-runs land.
|
|
8892
|
+
*/
|
|
8893
|
+
restartBarConsumers(opts) {
|
|
8894
|
+
if (opts.blank) this.blankIndicatorVisuals();
|
|
8895
|
+
this.restartNativeIndicators();
|
|
8896
|
+
this.restartChartTypeEngine();
|
|
8897
|
+
this.reexecuteIndicators();
|
|
8898
|
+
}
|
|
8441
8899
|
/** The primary symbol as the data layer resolves it, or null if no symbol is set. */
|
|
8442
8900
|
qualifiedSymbol() {
|
|
8443
8901
|
const market = this.config.market;
|
|
@@ -8509,7 +8967,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
8509
8967
|
engine.start({
|
|
8510
8968
|
symbol: this.config.market.symbol ?? "TEST",
|
|
8511
8969
|
timeframe: this.config.market.timeframe ?? "60",
|
|
8512
|
-
live: this.config.live ?? false,
|
|
8970
|
+
live: (this.config.live ?? false) && this.replayQueue === null,
|
|
8513
8971
|
session: this.config.market.session,
|
|
8514
8972
|
bars: () => this.bars,
|
|
8515
8973
|
data: this.dataControl,
|
|
@@ -8635,6 +9093,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
8635
9093
|
record.session.notifyBars(reason);
|
|
8636
9094
|
} else if (record.native) record.native.instance.onBars();
|
|
8637
9095
|
}
|
|
9096
|
+
if (this.activeEngineStyle) this.typeEngines.get(this.activeEngineStyle)?.onBars?.();
|
|
8638
9097
|
}
|
|
8639
9098
|
/** Gap healing needs a ranged feed and a provider-backed series (offline `data` has no source). */
|
|
8640
9099
|
canHeal() {
|
|
@@ -8657,10 +9116,11 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
8657
9116
|
try {
|
|
8658
9117
|
const bars = await this.feed.loadRange(this.config.market, { from: fromMs });
|
|
8659
9118
|
if (this.generation !== gen) return;
|
|
9119
|
+
if (this.replayQueue) return;
|
|
8660
9120
|
for (const b of bars) this.applyBar(b, false);
|
|
8661
9121
|
} catch {
|
|
8662
9122
|
} finally {
|
|
8663
|
-
if (this.generation === gen) {
|
|
9123
|
+
if (this.generation === gen && !this.replayQueue) {
|
|
8664
9124
|
this.healing = false;
|
|
8665
9125
|
const pending = this.healBuffer;
|
|
8666
9126
|
this.healBuffer = [];
|
|
@@ -8965,6 +9425,13 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
8965
9425
|
destroy() {
|
|
8966
9426
|
this.bumpGeneration();
|
|
8967
9427
|
this.resolveHistoryComplete();
|
|
9428
|
+
this.clearReplayTimer();
|
|
9429
|
+
this.replayQueue = null;
|
|
9430
|
+
this.replayForming = null;
|
|
9431
|
+
this.replayCarrying = null;
|
|
9432
|
+
this.dropTickFetches();
|
|
9433
|
+
this.replayDepth = null;
|
|
9434
|
+
this.replayLoading = null;
|
|
8968
9435
|
this.stopLive();
|
|
8969
9436
|
if (this.viewportTimer != null) clearTimeout(this.viewportTimer);
|
|
8970
9437
|
this.viewportUnsub?.();
|
|
@@ -9063,6 +9530,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
9063
9530
|
this.emitScriptRun(id, cause, first);
|
|
9064
9531
|
},
|
|
9065
9532
|
onAlert: (a) => {
|
|
9533
|
+
if (this.replayQueue || this.replayCarrying) return;
|
|
9066
9534
|
const indicator = record.options?.title ?? record.prepared?.meta.title ?? record.title;
|
|
9067
9535
|
this.events.emit("alert", { ...a, indicator });
|
|
9068
9536
|
handle.emit("alert", { id: a.id, message: a.message, title: a.title, time: a.time });
|
|
@@ -9089,7 +9557,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
9089
9557
|
id,
|
|
9090
9558
|
symbol: this.config.market.symbol ?? "TEST",
|
|
9091
9559
|
timeframe: this.config.market.timeframe ?? "60",
|
|
9092
|
-
live: this.config.live,
|
|
9560
|
+
live: this.config.live && this.replayQueue === null,
|
|
9093
9561
|
session: this.config.market.session,
|
|
9094
9562
|
bars: () => this.bars,
|
|
9095
9563
|
data: this.dataControl,
|
|
@@ -9492,17 +9960,17 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
9492
9960
|
}
|
|
9493
9961
|
emitContextChanged(id) {
|
|
9494
9962
|
if (!this.registry.get(id)?.session?.getContext) return;
|
|
9495
|
-
const
|
|
9496
|
-
if (
|
|
9497
|
-
this.contextEmitAt.set(id,
|
|
9963
|
+
const now2 = Date.now();
|
|
9964
|
+
if (now2 - (this.contextEmitAt.get(id) ?? 0) < 1e3) return;
|
|
9965
|
+
this.contextEmitAt.set(id, now2);
|
|
9498
9966
|
this.events.emit("context:changed", { id });
|
|
9499
9967
|
}
|
|
9500
9968
|
emitScriptRun(id, cause, first) {
|
|
9501
9969
|
const record = this.registry.get(id);
|
|
9502
9970
|
if (!record || record.native || !this.events.hasListeners("script:run")) return;
|
|
9503
|
-
const
|
|
9504
|
-
if (cause === "tick" &&
|
|
9505
|
-
this.runEmitAt.set(id,
|
|
9971
|
+
const now2 = Date.now();
|
|
9972
|
+
if (cause === "tick" && now2 - (this.runEmitAt.get(id) ?? 0) < RUN_EMIT_THROTTLE_MS) return;
|
|
9973
|
+
this.runEmitAt.set(id, now2);
|
|
9506
9974
|
void Promise.resolve().then(() => this.buildScriptRun(record, cause, first)).then((run) => {
|
|
9507
9975
|
if (run && this.registry.get(id)) this.events.emit("script:run", run);
|
|
9508
9976
|
});
|
|
@@ -9555,6 +10023,16 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
9555
10023
|
handle?.emit("error", { error });
|
|
9556
10024
|
}
|
|
9557
10025
|
};
|
|
10026
|
+
function now() {
|
|
10027
|
+
return typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
10028
|
+
}
|
|
10029
|
+
function usableTicks(value) {
|
|
10030
|
+
if (!Array.isArray(value)) return [];
|
|
10031
|
+
const optional = (n) => n === void 0 || Number.isFinite(n);
|
|
10032
|
+
return value.filter(
|
|
10033
|
+
(t) => t != null && Number.isFinite(t.price) && optional(t.volume) && optional(t.high) && optional(t.low) && optional(t.open)
|
|
10034
|
+
);
|
|
10035
|
+
}
|
|
9558
10036
|
function yieldToPaint() {
|
|
9559
10037
|
if (typeof requestAnimationFrame === "function") {
|
|
9560
10038
|
return new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
|
|
@@ -9755,6 +10233,12 @@ var RendererControl = class {
|
|
|
9755
10233
|
onCrosshairMove(cb) {
|
|
9756
10234
|
return this.renderer.onCrosshairMove(cb);
|
|
9757
10235
|
}
|
|
10236
|
+
/** A click — or a touch tap — on the plot, never the end of a pan: the open time of
|
|
10237
|
+
* the bar under it (`null` off the bars). On touch, where a tap moves no crosshair,
|
|
10238
|
+
* this is how an interaction learns which bar was chosen. */
|
|
10239
|
+
onClick(cb) {
|
|
10240
|
+
return this.renderer.onClick(cb);
|
|
10241
|
+
}
|
|
9758
10242
|
/** Touch long-press on a price or time axis strip — silent no-op without the seam. */
|
|
9759
10243
|
onAxisLongPress(cb) {
|
|
9760
10244
|
return this.renderer.onAxisLongPress?.(cb) ?? (() => void 0);
|
|
@@ -10830,11 +11314,76 @@ var MarksControl = class {
|
|
|
10830
11314
|
}
|
|
10831
11315
|
};
|
|
10832
11316
|
|
|
11317
|
+
// src/core/ReplayControl.ts
|
|
11318
|
+
var ReplayControl = class {
|
|
11319
|
+
constructor(ctrl) {
|
|
11320
|
+
this.ctrl = ctrl;
|
|
11321
|
+
}
|
|
11322
|
+
/**
|
|
11323
|
+
* Enter replay (or seek, when already replaying) at `opts.from`. Resolves once the
|
|
11324
|
+
* chart shows the rewound history — after any in-flight history load has finished.
|
|
11325
|
+
* Starts paused; call {@link play} or {@link step}.
|
|
11326
|
+
*/
|
|
11327
|
+
start(opts) {
|
|
11328
|
+
return this.ctrl.replayStart(opts);
|
|
11329
|
+
}
|
|
11330
|
+
/** Reveal the next bar — or, mid-way through a bar played tick by tick, complete it.
|
|
11331
|
+
* Returns false when replay is off or nothing is left. */
|
|
11332
|
+
step() {
|
|
11333
|
+
return this.ctrl.replayStep();
|
|
11334
|
+
}
|
|
11335
|
+
/** Reveal the next UPDATE: with {@link setTicks}, the next tick of the forming bar — or
|
|
11336
|
+
* the next bar opened at its first tick (once its ticks are in, if still loading);
|
|
11337
|
+
* otherwise the next whole bar, like {@link step}. Returns false when nothing is left. */
|
|
11338
|
+
stepUpdate() {
|
|
11339
|
+
return this.ctrl.replayStepUpdate();
|
|
11340
|
+
}
|
|
11341
|
+
/** Reveal one bar (one tick, with {@link setTicks}) every `intervalMs` (default: the last
|
|
11342
|
+
* interval used, else 1000). Calling it again while playing changes the pace. */
|
|
11343
|
+
play(intervalMs2) {
|
|
11344
|
+
this.ctrl.replayPlay(intervalMs2);
|
|
11345
|
+
return this;
|
|
11346
|
+
}
|
|
11347
|
+
pause() {
|
|
11348
|
+
this.ctrl.replayPause();
|
|
11349
|
+
return this;
|
|
11350
|
+
}
|
|
11351
|
+
/** Leave replay: the full history comes back and live updates resume. Also cancels a
|
|
11352
|
+
* `start()` still loading older history (the chart returns to its previous depth). */
|
|
11353
|
+
stop() {
|
|
11354
|
+
this.ctrl.replayStop();
|
|
11355
|
+
return this;
|
|
11356
|
+
}
|
|
11357
|
+
get state() {
|
|
11358
|
+
return this.ctrl.replayState();
|
|
11359
|
+
}
|
|
11360
|
+
/** The loaded history a replay can walk — hidden bars included while replaying. Null
|
|
11361
|
+
* before any bar loaded. A start older than `first` deepens the history first. */
|
|
11362
|
+
get bounds() {
|
|
11363
|
+
return this.ctrl.replayBounds();
|
|
11364
|
+
}
|
|
11365
|
+
/**
|
|
11366
|
+
* Play each revealed bar as a series of intrabar updates instead of whole: while
|
|
11367
|
+
* playing, the forming candle opens at the first tick, then every tick moves its close
|
|
11368
|
+
* and stretches its high and low, exactly like a live tick — indicators, chart types
|
|
11369
|
+
* and the `bar` event follow — and the last one settles it on the stored bar. The play
|
|
11370
|
+
* interval is then the time between two TICKS: a bar lasts as many intervals as it
|
|
11371
|
+
* has ticks (below ~16 ms, ticks are batched per frame). The next bar's ticks are
|
|
11372
|
+
* requested while the current one plays; playback waits for them. `null` goes back to
|
|
11373
|
+
* whole bars. Takes effect from the next bar; a bar forming when the source changes
|
|
11374
|
+
* completes at once.
|
|
11375
|
+
*/
|
|
11376
|
+
setTicks(source) {
|
|
11377
|
+
this.ctrl.replaySetTicks(source);
|
|
11378
|
+
return this;
|
|
11379
|
+
}
|
|
11380
|
+
};
|
|
11381
|
+
|
|
10833
11382
|
// src/core/util/wall-clock.ts
|
|
10834
11383
|
var BOUNDARY_SLACK_MS = 5;
|
|
10835
11384
|
var SecondClock = class _SecondClock {
|
|
10836
|
-
constructor(
|
|
10837
|
-
this.now =
|
|
11385
|
+
constructor(now2 = () => Date.now()) {
|
|
11386
|
+
this.now = now2;
|
|
10838
11387
|
this.subs = /* @__PURE__ */ new Set();
|
|
10839
11388
|
this.timer = null;
|
|
10840
11389
|
}
|
|
@@ -10850,15 +11399,15 @@ var SecondClock = class _SecondClock {
|
|
|
10850
11399
|
};
|
|
10851
11400
|
}
|
|
10852
11401
|
/** Milliseconds from `now` to just past the next second boundary. */
|
|
10853
|
-
static delayToNextSecond(
|
|
10854
|
-
const intoSecond = (
|
|
11402
|
+
static delayToNextSecond(now2) {
|
|
11403
|
+
const intoSecond = (now2 % 1e3 + 1e3) % 1e3;
|
|
10855
11404
|
return 1e3 - intoSecond + BOUNDARY_SLACK_MS;
|
|
10856
11405
|
}
|
|
10857
11406
|
arm() {
|
|
10858
11407
|
this.timer = setTimeout(() => {
|
|
10859
11408
|
this.timer = null;
|
|
10860
|
-
const
|
|
10861
|
-
for (const cb of this.subs) cb(
|
|
11409
|
+
const now2 = this.now();
|
|
11410
|
+
for (const cb of this.subs) cb(now2);
|
|
10862
11411
|
if (this.subs.size > 0) this.arm();
|
|
10863
11412
|
}, _SecondClock.delayToNextSecond(this.now()));
|
|
10864
11413
|
}
|
|
@@ -12519,6 +13068,264 @@ var Dialog = class {
|
|
|
12519
13068
|
}
|
|
12520
13069
|
};
|
|
12521
13070
|
|
|
13071
|
+
// src/ui/components/date-picker/controller.ts
|
|
13072
|
+
var MONTH_LABELS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
|
|
13073
|
+
var MONTH_SHORT = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
13074
|
+
var WEEKDAY_LABELS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
|
|
13075
|
+
function isoDate(d) {
|
|
13076
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
13077
|
+
}
|
|
13078
|
+
function normalizeDateInput(raw) {
|
|
13079
|
+
const m = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(raw.trim());
|
|
13080
|
+
if (!m) return null;
|
|
13081
|
+
const y = Number(m[1]);
|
|
13082
|
+
const mo = Number(m[2]);
|
|
13083
|
+
const d = Number(m[3]);
|
|
13084
|
+
const dt = new Date(y, mo - 1, d);
|
|
13085
|
+
if (dt.getFullYear() !== y || dt.getMonth() !== mo - 1 || dt.getDate() !== d) return null;
|
|
13086
|
+
return isoDate(dt);
|
|
13087
|
+
}
|
|
13088
|
+
function parseIsoDate(raw) {
|
|
13089
|
+
const iso = raw ? normalizeDateInput(raw) : null;
|
|
13090
|
+
if (!iso) return null;
|
|
13091
|
+
const [y, mo, d] = iso.split("-").map(Number);
|
|
13092
|
+
return new Date(y, mo - 1, d);
|
|
13093
|
+
}
|
|
13094
|
+
function datePickerController(opts = {}) {
|
|
13095
|
+
const today = opts.today ?? /* @__PURE__ */ new Date();
|
|
13096
|
+
let selected = parseIsoDate(opts.value);
|
|
13097
|
+
let year = selected?.getFullYear() ?? today.getFullYear();
|
|
13098
|
+
let month = selected?.getMonth() ?? today.getMonth();
|
|
13099
|
+
let panel = "date";
|
|
13100
|
+
const decade = () => Math.floor(year / 10) * 10;
|
|
13101
|
+
return {
|
|
13102
|
+
get panel() {
|
|
13103
|
+
return panel;
|
|
13104
|
+
},
|
|
13105
|
+
get year() {
|
|
13106
|
+
return year;
|
|
13107
|
+
},
|
|
13108
|
+
get month() {
|
|
13109
|
+
return month;
|
|
13110
|
+
},
|
|
13111
|
+
get value() {
|
|
13112
|
+
return selected ? isoDate(selected) : null;
|
|
13113
|
+
},
|
|
13114
|
+
header() {
|
|
13115
|
+
return {
|
|
13116
|
+
month: MONTH_LABELS[month] ?? "",
|
|
13117
|
+
monthVisible: panel === "date",
|
|
13118
|
+
year: panel === "year" ? `${decade()}-${decade() + 9}` : String(year),
|
|
13119
|
+
yearDisabled: panel === "year"
|
|
13120
|
+
// in the decade panel the year reads as its title
|
|
13121
|
+
};
|
|
13122
|
+
},
|
|
13123
|
+
navLabels() {
|
|
13124
|
+
if (panel === "date") return { prev: "Previous month", next: "Next month" };
|
|
13125
|
+
if (panel === "month") return { prev: "Previous year", next: "Next year" };
|
|
13126
|
+
return { prev: "Previous decade", next: "Next decade" };
|
|
13127
|
+
},
|
|
13128
|
+
cells() {
|
|
13129
|
+
const out = [];
|
|
13130
|
+
if (panel === "date") {
|
|
13131
|
+
const startPad = new Date(year, month, 1).getDay();
|
|
13132
|
+
const days = new Date(year, month + 1, 0).getDate();
|
|
13133
|
+
const todayIso = isoDate(today);
|
|
13134
|
+
const selectedIso = selected ? isoDate(selected) : "";
|
|
13135
|
+
for (let i = 0; i < startPad; i++) out.push({ label: "", value: "", checked: false, today: false, blank: true });
|
|
13136
|
+
for (let day = 1; day <= days; day++) {
|
|
13137
|
+
const iso = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
|
13138
|
+
out.push({ label: String(day), value: iso, checked: iso === selectedIso, today: iso === todayIso });
|
|
13139
|
+
}
|
|
13140
|
+
return out;
|
|
13141
|
+
}
|
|
13142
|
+
if (panel === "month") {
|
|
13143
|
+
for (let m = 0; m < 12; m++) {
|
|
13144
|
+
out.push({
|
|
13145
|
+
label: MONTH_SHORT[m] ?? "",
|
|
13146
|
+
value: m,
|
|
13147
|
+
checked: selected?.getFullYear() === year && selected.getMonth() === m,
|
|
13148
|
+
today: today.getFullYear() === year && today.getMonth() === m
|
|
13149
|
+
});
|
|
13150
|
+
}
|
|
13151
|
+
return out;
|
|
13152
|
+
}
|
|
13153
|
+
const d = decade();
|
|
13154
|
+
for (let y = d - 1; y <= d + 10; y++) {
|
|
13155
|
+
out.push({ label: String(y), value: y, checked: selected?.getFullYear() === y, today: today.getFullYear() === y, outside: y < d || y > d + 9 });
|
|
13156
|
+
}
|
|
13157
|
+
return out;
|
|
13158
|
+
},
|
|
13159
|
+
step(dir) {
|
|
13160
|
+
if (panel === "date") {
|
|
13161
|
+
month += dir;
|
|
13162
|
+
if (month < 0) {
|
|
13163
|
+
month = 11;
|
|
13164
|
+
year -= 1;
|
|
13165
|
+
}
|
|
13166
|
+
if (month > 11) {
|
|
13167
|
+
month = 0;
|
|
13168
|
+
year += 1;
|
|
13169
|
+
}
|
|
13170
|
+
} else {
|
|
13171
|
+
year += panel === "month" ? dir : dir * 10;
|
|
13172
|
+
}
|
|
13173
|
+
},
|
|
13174
|
+
showMonths() {
|
|
13175
|
+
panel = "month";
|
|
13176
|
+
},
|
|
13177
|
+
showYears() {
|
|
13178
|
+
panel = "year";
|
|
13179
|
+
},
|
|
13180
|
+
choose(cell) {
|
|
13181
|
+
if (cell.blank) return null;
|
|
13182
|
+
if (panel === "date") {
|
|
13183
|
+
selected = parseIsoDate(String(cell.value));
|
|
13184
|
+
return selected ? isoDate(selected) : null;
|
|
13185
|
+
}
|
|
13186
|
+
if (panel === "month") {
|
|
13187
|
+
month = Number(cell.value);
|
|
13188
|
+
panel = "date";
|
|
13189
|
+
return null;
|
|
13190
|
+
}
|
|
13191
|
+
year = Number(cell.value);
|
|
13192
|
+
panel = "month";
|
|
13193
|
+
return null;
|
|
13194
|
+
},
|
|
13195
|
+
setValue(value) {
|
|
13196
|
+
selected = parseIsoDate(value);
|
|
13197
|
+
if (selected) {
|
|
13198
|
+
year = selected.getFullYear();
|
|
13199
|
+
month = selected.getMonth();
|
|
13200
|
+
}
|
|
13201
|
+
panel = "date";
|
|
13202
|
+
}
|
|
13203
|
+
};
|
|
13204
|
+
}
|
|
13205
|
+
|
|
13206
|
+
// src/ui/components/date-picker/styles.ts
|
|
13207
|
+
var DATE_PICKER_STYLE_ID = "vela-date-picker-styles";
|
|
13208
|
+
var DATE_PICKER_CSS = `
|
|
13209
|
+
.vela-date-picker{color:var(--vela-fg);font:14px var(--vela-font);user-select:none;}
|
|
13210
|
+
.vela-date-picker [hidden]{display:none !important;}
|
|
13211
|
+
.vela-date-picker-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;}
|
|
13212
|
+
.vela-date-picker-title{flex:1;display:flex;align-items:center;justify-content:center;gap:2px;min-width:0;}
|
|
13213
|
+
.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;}
|
|
13214
|
+
.vela-date-picker-switch:not(:disabled):hover{background:var(--vela-hover);}
|
|
13215
|
+
.vela-date-picker-switch:disabled{cursor:default;}
|
|
13216
|
+
.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;}
|
|
13217
|
+
.vela-date-picker-nav:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
|
|
13218
|
+
.vela-date-picker-week,.vela-date-picker-grid{display:grid;grid-template-columns:repeat(7,28px);gap:2px;}
|
|
13219
|
+
.vela-date-picker-week{margin-bottom:4px;color:var(--vela-fg-muted);font-size:11px;text-align:center;}
|
|
13220
|
+
.vela-date-picker-week span{line-height:20px;}
|
|
13221
|
+
.vela-date-picker-blank{width:28px;height:28px;}
|
|
13222
|
+
.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;}
|
|
13223
|
+
.vela-date-picker-day:hover{background:var(--vela-hover);}
|
|
13224
|
+
.vela-date-picker-day[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
|
|
13225
|
+
.vela-date-picker-day[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
|
|
13226
|
+
.vela-date-picker-cells{display:grid;grid-template-columns:repeat(3,1fr);gap:4px;width:calc(7 * 28px + 6 * 2px);}
|
|
13227
|
+
.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;}
|
|
13228
|
+
.vela-date-picker-cell:hover{background:var(--vela-hover);}
|
|
13229
|
+
.vela-date-picker-cell[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
|
|
13230
|
+
.vela-date-picker-cell[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
|
|
13231
|
+
.vela-date-picker-cell[data-outside]{opacity:0.45;}
|
|
13232
|
+
`;
|
|
13233
|
+
|
|
13234
|
+
// src/ui/components/date-picker/view.ts
|
|
13235
|
+
var DatePicker = class {
|
|
13236
|
+
constructor(opts = {}) {
|
|
13237
|
+
this.opts = opts;
|
|
13238
|
+
const doc = document;
|
|
13239
|
+
injectStyles(DATE_PICKER_STYLE_ID, DATE_PICKER_CSS, doc);
|
|
13240
|
+
this.ctrl = datePickerController(opts);
|
|
13241
|
+
const button = (className) => {
|
|
13242
|
+
const b = doc.createElement("button");
|
|
13243
|
+
b.type = "button";
|
|
13244
|
+
b.className = className;
|
|
13245
|
+
return b;
|
|
13246
|
+
};
|
|
13247
|
+
this.prev = button("vela-date-picker-nav");
|
|
13248
|
+
this.prev.append(iconEl("chevron-left", doc));
|
|
13249
|
+
this.next = button("vela-date-picker-nav");
|
|
13250
|
+
this.next.append(iconEl("chevron-right", doc));
|
|
13251
|
+
this.monthBtn = button("vela-date-picker-switch");
|
|
13252
|
+
this.monthBtn.setAttribute("aria-label", "Choose month");
|
|
13253
|
+
this.yearBtn = button("vela-date-picker-switch");
|
|
13254
|
+
this.yearBtn.setAttribute("aria-label", "Choose year");
|
|
13255
|
+
const title = doc.createElement("div");
|
|
13256
|
+
title.className = "vela-date-picker-title";
|
|
13257
|
+
title.append(this.monthBtn, this.yearBtn);
|
|
13258
|
+
const head = doc.createElement("div");
|
|
13259
|
+
head.className = "vela-date-picker-head";
|
|
13260
|
+
head.append(this.prev, title, this.next);
|
|
13261
|
+
this.week = doc.createElement("div");
|
|
13262
|
+
this.week.className = "vela-date-picker-week";
|
|
13263
|
+
for (const d of WEEKDAY_LABELS) {
|
|
13264
|
+
const cell = doc.createElement("span");
|
|
13265
|
+
cell.textContent = d;
|
|
13266
|
+
this.week.appendChild(cell);
|
|
13267
|
+
}
|
|
13268
|
+
this.grid = doc.createElement("div");
|
|
13269
|
+
const on = (el, run) => el.addEventListener("click", (e) => {
|
|
13270
|
+
e.stopPropagation();
|
|
13271
|
+
run();
|
|
13272
|
+
this.paint();
|
|
13273
|
+
});
|
|
13274
|
+
on(this.prev, () => this.ctrl.step(-1));
|
|
13275
|
+
on(this.next, () => this.ctrl.step(1));
|
|
13276
|
+
on(this.monthBtn, () => this.ctrl.showMonths());
|
|
13277
|
+
on(this.yearBtn, () => this.ctrl.showYears());
|
|
13278
|
+
this.el = doc.createElement("div");
|
|
13279
|
+
this.el.className = "vela-date-picker";
|
|
13280
|
+
this.el.append(head, this.week, this.grid);
|
|
13281
|
+
this.paint();
|
|
13282
|
+
}
|
|
13283
|
+
get value() {
|
|
13284
|
+
return this.ctrl.value;
|
|
13285
|
+
}
|
|
13286
|
+
/** Rewrite the selection without emitting (the calendar opens on its month). */
|
|
13287
|
+
setValue(value) {
|
|
13288
|
+
this.ctrl.setValue(value);
|
|
13289
|
+
this.paint();
|
|
13290
|
+
}
|
|
13291
|
+
paint() {
|
|
13292
|
+
const doc = this.el.ownerDocument;
|
|
13293
|
+
const h = this.ctrl.header();
|
|
13294
|
+
this.monthBtn.hidden = !h.monthVisible;
|
|
13295
|
+
this.monthBtn.textContent = h.month;
|
|
13296
|
+
this.yearBtn.textContent = h.year;
|
|
13297
|
+
this.yearBtn.disabled = h.yearDisabled;
|
|
13298
|
+
const nav = this.ctrl.navLabels();
|
|
13299
|
+
this.prev.setAttribute("aria-label", nav.prev);
|
|
13300
|
+
this.next.setAttribute("aria-label", nav.next);
|
|
13301
|
+
const dates = this.ctrl.panel === "date";
|
|
13302
|
+
this.week.hidden = !dates;
|
|
13303
|
+
this.grid.className = dates ? "vela-date-picker-grid" : "vela-date-picker-cells";
|
|
13304
|
+
this.grid.replaceChildren(...this.ctrl.cells().map((cell) => this.cellEl(doc, cell, dates)));
|
|
13305
|
+
}
|
|
13306
|
+
cellEl(doc, cell, dates) {
|
|
13307
|
+
if (cell.blank) {
|
|
13308
|
+
const blank = doc.createElement("span");
|
|
13309
|
+
blank.className = "vela-date-picker-blank";
|
|
13310
|
+
return blank;
|
|
13311
|
+
}
|
|
13312
|
+
const b = doc.createElement("button");
|
|
13313
|
+
b.type = "button";
|
|
13314
|
+
b.className = dates ? "vela-date-picker-day" : "vela-date-picker-cell";
|
|
13315
|
+
b.textContent = cell.label;
|
|
13316
|
+
if (cell.checked) b.dataset.checked = "1";
|
|
13317
|
+
if (cell.today) b.dataset.today = "1";
|
|
13318
|
+
if (cell.outside) b.dataset.outside = "1";
|
|
13319
|
+
b.addEventListener("click", (e) => {
|
|
13320
|
+
e.stopPropagation();
|
|
13321
|
+
const picked = this.ctrl.choose(cell);
|
|
13322
|
+
if (picked) this.opts.onPick?.(picked);
|
|
13323
|
+
else this.paint();
|
|
13324
|
+
});
|
|
13325
|
+
return b;
|
|
13326
|
+
}
|
|
13327
|
+
};
|
|
13328
|
+
|
|
12522
13329
|
// src/ui/components/switch/controller.ts
|
|
12523
13330
|
function switchController(opts = {}) {
|
|
12524
13331
|
let checked = opts.checked ?? false;
|
|
@@ -14471,10 +15278,16 @@ var IndicatorInputsDialog = class {
|
|
|
14471
15278
|
this.calendarPop = null;
|
|
14472
15279
|
this.calendarAnchor = null;
|
|
14473
15280
|
},
|
|
14474
|
-
content: (el) =>
|
|
14475
|
-
|
|
14476
|
-
|
|
14477
|
-
|
|
15281
|
+
content: (el) => {
|
|
15282
|
+
const picker = new DatePicker({
|
|
15283
|
+
value: current,
|
|
15284
|
+
onPick: (iso) => {
|
|
15285
|
+
pop.hide();
|
|
15286
|
+
onPick(iso);
|
|
15287
|
+
}
|
|
15288
|
+
});
|
|
15289
|
+
el.append(picker.el);
|
|
15290
|
+
}
|
|
14478
15291
|
});
|
|
14479
15292
|
this.calendarPop = pop;
|
|
14480
15293
|
this.calendarAnchor = anchor;
|
|
@@ -14599,28 +15412,6 @@ function ensureDialogStyles() {
|
|
|
14599
15412
|
.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;}
|
|
14600
15413
|
.vela-ind-combo-chevron:hover{opacity:0.9;}
|
|
14601
15414
|
.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;}
|
|
14602
|
-
.vela-ind-cal [hidden]{display:none !important;}
|
|
14603
|
-
.vela-ind-cal-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;}
|
|
14604
|
-
.vela-ind-cal-title{flex:1;display:flex;align-items:center;justify-content:center;gap:2px;min-width:0;}
|
|
14605
|
-
.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;}
|
|
14606
|
-
.vela-ind-cal-switch:not(:disabled):hover{background:var(--vela-hover);}
|
|
14607
|
-
.vela-ind-cal-switch:disabled{cursor:default;}
|
|
14608
|
-
.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;}
|
|
14609
|
-
.vela-ind-cal-nav:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
|
|
14610
|
-
.vela-ind-cal-week,.vela-ind-cal-grid{display:grid;grid-template-columns:repeat(7,28px);gap:2px;}
|
|
14611
|
-
.vela-ind-cal-week{margin-bottom:4px;color:var(--vela-fg-muted);font-size:11px;text-align:center;}
|
|
14612
|
-
.vela-ind-cal-week span{line-height:20px;}
|
|
14613
|
-
.vela-ind-cal-blank{width:28px;height:28px;}
|
|
14614
|
-
.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;}
|
|
14615
|
-
.vela-ind-cal-day:hover{background:var(--vela-hover);}
|
|
14616
|
-
.vela-ind-cal-day[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
|
|
14617
|
-
.vela-ind-cal-day[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
|
|
14618
|
-
.vela-ind-cal-cells{display:grid;grid-template-columns:repeat(3,1fr);gap:4px;width:calc(7 * 28px + 6 * 2px);}
|
|
14619
|
-
.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;}
|
|
14620
|
-
.vela-ind-cal-cell:hover{background:var(--vela-hover);}
|
|
14621
|
-
.vela-ind-cal-cell[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
|
|
14622
|
-
.vela-ind-cal-cell[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
|
|
14623
|
-
.vela-ind-cal-cell[data-outside]{opacity:0.45;}
|
|
14624
15415
|
${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
|
|
14625
15416
|
.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;}
|
|
14626
15417
|
.vela-ind-tab:not(.vela-ind-tab-active):hover{color:var(--vela-fg-bright);}
|
|
@@ -14654,174 +15445,6 @@ var TIME_OPTIONS = Array.from({ length: 48 }, (_, i) => {
|
|
|
14654
15445
|
const v = `${hh}:${mm}`;
|
|
14655
15446
|
return { value: v, label: v };
|
|
14656
15447
|
});
|
|
14657
|
-
var MONTH_LABELS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
|
|
14658
|
-
var MONTH_SHORT = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
14659
|
-
var WEEKDAY_LABELS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
|
|
14660
|
-
function normalizeDateInput(raw) {
|
|
14661
|
-
const t = raw.trim();
|
|
14662
|
-
const m = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(t);
|
|
14663
|
-
if (!m) return null;
|
|
14664
|
-
const y = Number(m[1]);
|
|
14665
|
-
const mo = Number(m[2]);
|
|
14666
|
-
const d = Number(m[3]);
|
|
14667
|
-
const dt = new Date(y, mo - 1, d);
|
|
14668
|
-
if (dt.getFullYear() !== y || dt.getMonth() !== mo - 1 || dt.getDate() !== d) return null;
|
|
14669
|
-
return isoDate(dt);
|
|
14670
|
-
}
|
|
14671
|
-
function parseIsoDate(raw) {
|
|
14672
|
-
const iso = normalizeDateInput(raw);
|
|
14673
|
-
if (!iso) return null;
|
|
14674
|
-
const [y, mo, d] = iso.split("-").map(Number);
|
|
14675
|
-
return new Date(y, mo - 1, d);
|
|
14676
|
-
}
|
|
14677
|
-
function isoDate(d) {
|
|
14678
|
-
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
14679
|
-
}
|
|
14680
|
-
function fillCalendar(el, current, onPick) {
|
|
14681
|
-
const today = /* @__PURE__ */ new Date();
|
|
14682
|
-
const selected = parseIsoDate(current);
|
|
14683
|
-
let year = selected?.getFullYear() ?? today.getFullYear();
|
|
14684
|
-
let month = selected?.getMonth() ?? today.getMonth();
|
|
14685
|
-
let mode = "date";
|
|
14686
|
-
const prev2 = document.createElement("button");
|
|
14687
|
-
prev2.type = "button";
|
|
14688
|
-
prev2.className = "vela-ind-cal-nav";
|
|
14689
|
-
prev2.innerHTML = iconAt("chevron-left", 14);
|
|
14690
|
-
const next = document.createElement("button");
|
|
14691
|
-
next.type = "button";
|
|
14692
|
-
next.className = "vela-ind-cal-nav";
|
|
14693
|
-
next.innerHTML = iconAt("chevron-right", 14);
|
|
14694
|
-
const monthBtn = document.createElement("button");
|
|
14695
|
-
monthBtn.type = "button";
|
|
14696
|
-
monthBtn.className = "vela-ind-cal-switch";
|
|
14697
|
-
monthBtn.setAttribute("aria-label", "Choose month");
|
|
14698
|
-
const yearBtn = document.createElement("button");
|
|
14699
|
-
yearBtn.type = "button";
|
|
14700
|
-
yearBtn.className = "vela-ind-cal-switch";
|
|
14701
|
-
yearBtn.setAttribute("aria-label", "Choose year");
|
|
14702
|
-
const title = document.createElement("div");
|
|
14703
|
-
title.className = "vela-ind-cal-title";
|
|
14704
|
-
title.append(monthBtn, yearBtn);
|
|
14705
|
-
const head = document.createElement("div");
|
|
14706
|
-
head.className = "vela-ind-cal-head";
|
|
14707
|
-
head.append(prev2, title, next);
|
|
14708
|
-
const week = document.createElement("div");
|
|
14709
|
-
week.className = "vela-ind-cal-week";
|
|
14710
|
-
for (const d of WEEKDAY_LABELS) {
|
|
14711
|
-
const cell = document.createElement("span");
|
|
14712
|
-
cell.textContent = d;
|
|
14713
|
-
week.appendChild(cell);
|
|
14714
|
-
}
|
|
14715
|
-
const grid = document.createElement("div");
|
|
14716
|
-
const paint = () => {
|
|
14717
|
-
const decade = Math.floor(year / 10) * 10;
|
|
14718
|
-
monthBtn.hidden = mode !== "date";
|
|
14719
|
-
monthBtn.textContent = MONTH_LABELS[month] ?? "";
|
|
14720
|
-
yearBtn.textContent = mode === "year" ? `${decade}-${decade + 9}` : String(year);
|
|
14721
|
-
yearBtn.disabled = mode === "year";
|
|
14722
|
-
week.hidden = mode !== "date";
|
|
14723
|
-
prev2.setAttribute("aria-label", mode === "date" ? "Previous month" : mode === "month" ? "Previous year" : "Previous decade");
|
|
14724
|
-
next.setAttribute("aria-label", mode === "date" ? "Next month" : mode === "month" ? "Next year" : "Next decade");
|
|
14725
|
-
grid.className = mode === "date" ? "vela-ind-cal-grid" : "vela-ind-cal-cells";
|
|
14726
|
-
grid.replaceChildren();
|
|
14727
|
-
if (mode === "date") {
|
|
14728
|
-
const startPad = new Date(year, month, 1).getDay();
|
|
14729
|
-
const days = new Date(year, month + 1, 0).getDate();
|
|
14730
|
-
const todayIso = isoDate(today);
|
|
14731
|
-
const selectedIso = selected ? isoDate(selected) : "";
|
|
14732
|
-
for (let i = 0; i < startPad; i++) {
|
|
14733
|
-
const blank = document.createElement("span");
|
|
14734
|
-
blank.className = "vela-ind-cal-blank";
|
|
14735
|
-
grid.appendChild(blank);
|
|
14736
|
-
}
|
|
14737
|
-
for (let day = 1; day <= days; day++) {
|
|
14738
|
-
const iso = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
|
14739
|
-
const b = document.createElement("button");
|
|
14740
|
-
b.type = "button";
|
|
14741
|
-
b.className = "vela-ind-cal-day";
|
|
14742
|
-
b.textContent = String(day);
|
|
14743
|
-
if (iso === selectedIso) b.dataset.checked = "1";
|
|
14744
|
-
if (iso === todayIso) b.dataset.today = "1";
|
|
14745
|
-
b.addEventListener("click", (e) => {
|
|
14746
|
-
e.stopPropagation();
|
|
14747
|
-
onPick(iso);
|
|
14748
|
-
});
|
|
14749
|
-
grid.appendChild(b);
|
|
14750
|
-
}
|
|
14751
|
-
return;
|
|
14752
|
-
}
|
|
14753
|
-
if (mode === "month") {
|
|
14754
|
-
for (let m = 0; m < 12; m++) {
|
|
14755
|
-
const b = document.createElement("button");
|
|
14756
|
-
b.type = "button";
|
|
14757
|
-
b.className = "vela-ind-cal-cell";
|
|
14758
|
-
b.textContent = MONTH_SHORT[m] ?? "";
|
|
14759
|
-
if (selected?.getFullYear() === year && selected.getMonth() === m) b.dataset.checked = "1";
|
|
14760
|
-
if (today.getFullYear() === year && today.getMonth() === m) b.dataset.today = "1";
|
|
14761
|
-
b.addEventListener("click", (e) => {
|
|
14762
|
-
e.stopPropagation();
|
|
14763
|
-
month = m;
|
|
14764
|
-
mode = "date";
|
|
14765
|
-
paint();
|
|
14766
|
-
});
|
|
14767
|
-
grid.appendChild(b);
|
|
14768
|
-
}
|
|
14769
|
-
return;
|
|
14770
|
-
}
|
|
14771
|
-
for (let y = decade - 1; y <= decade + 10; y++) {
|
|
14772
|
-
const b = document.createElement("button");
|
|
14773
|
-
b.type = "button";
|
|
14774
|
-
b.className = "vela-ind-cal-cell";
|
|
14775
|
-
b.textContent = String(y);
|
|
14776
|
-
if (y < decade || y > decade + 9) b.dataset.outside = "1";
|
|
14777
|
-
if (selected?.getFullYear() === y) b.dataset.checked = "1";
|
|
14778
|
-
if (today.getFullYear() === y) b.dataset.today = "1";
|
|
14779
|
-
b.addEventListener("click", (e) => {
|
|
14780
|
-
e.stopPropagation();
|
|
14781
|
-
year = y;
|
|
14782
|
-
mode = "month";
|
|
14783
|
-
paint();
|
|
14784
|
-
});
|
|
14785
|
-
grid.appendChild(b);
|
|
14786
|
-
}
|
|
14787
|
-
};
|
|
14788
|
-
const step = (dir) => {
|
|
14789
|
-
if (mode === "date") {
|
|
14790
|
-
month += dir;
|
|
14791
|
-
if (month < 0) {
|
|
14792
|
-
month = 11;
|
|
14793
|
-
year -= 1;
|
|
14794
|
-
}
|
|
14795
|
-
if (month > 11) {
|
|
14796
|
-
month = 0;
|
|
14797
|
-
year += 1;
|
|
14798
|
-
}
|
|
14799
|
-
} else {
|
|
14800
|
-
year += mode === "month" ? dir : dir * 10;
|
|
14801
|
-
}
|
|
14802
|
-
paint();
|
|
14803
|
-
};
|
|
14804
|
-
prev2.addEventListener("click", (e) => {
|
|
14805
|
-
e.stopPropagation();
|
|
14806
|
-
step(-1);
|
|
14807
|
-
});
|
|
14808
|
-
next.addEventListener("click", (e) => {
|
|
14809
|
-
e.stopPropagation();
|
|
14810
|
-
step(1);
|
|
14811
|
-
});
|
|
14812
|
-
monthBtn.addEventListener("click", (e) => {
|
|
14813
|
-
e.stopPropagation();
|
|
14814
|
-
mode = "month";
|
|
14815
|
-
paint();
|
|
14816
|
-
});
|
|
14817
|
-
yearBtn.addEventListener("click", (e) => {
|
|
14818
|
-
e.stopPropagation();
|
|
14819
|
-
mode = "year";
|
|
14820
|
-
paint();
|
|
14821
|
-
});
|
|
14822
|
-
paint();
|
|
14823
|
-
el.append(head, week, grid);
|
|
14824
|
-
}
|
|
14825
15448
|
function normalizeTimeInput(raw) {
|
|
14826
15449
|
const t = raw.trim();
|
|
14827
15450
|
const m = /^(\d{1,2}):(\d{2})$/.exec(t) ?? /^(\d{2})(\d{2})$/.exec(t);
|
|
@@ -16199,6 +16822,22 @@ function priceStyleIds() {
|
|
|
16199
16822
|
for (const t of chartTypes()) if (!out.includes(t.id)) out.push(t.id);
|
|
16200
16823
|
return out;
|
|
16201
16824
|
}
|
|
16825
|
+
function sanitizeCrosshairOverride(value) {
|
|
16826
|
+
if (!value || typeof value !== "object") return null;
|
|
16827
|
+
const v = value;
|
|
16828
|
+
const out = {};
|
|
16829
|
+
if (isBool(v.vertical)) out.vertical = v.vertical;
|
|
16830
|
+
if (isBool(v.horizontal)) out.horizontal = v.horizontal;
|
|
16831
|
+
if (isColor(v.color)) out.color = v.color;
|
|
16832
|
+
if (isNum(v.width)) out.width = Math.max(0.5, Math.min(8, v.width));
|
|
16833
|
+
if (isLineStyle(v.style)) out.style = v.style;
|
|
16834
|
+
if (isNum(v.opacity)) out.opacity = clampOpacity(v.opacity);
|
|
16835
|
+
const shade = v.shadeRight;
|
|
16836
|
+
if (shade && typeof shade === "object" && isColor(shade.color)) {
|
|
16837
|
+
out.shadeRight = isNum(shade.opacity) ? { color: shade.color, opacity: clampOpacity(shade.opacity) } : { color: shade.color };
|
|
16838
|
+
}
|
|
16839
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
16840
|
+
}
|
|
16202
16841
|
function isColor(v) {
|
|
16203
16842
|
return typeof v === "string" && v.trim().length > 0;
|
|
16204
16843
|
}
|
|
@@ -17860,7 +18499,7 @@ var Scheduler = class {
|
|
|
17860
18499
|
|
|
17861
18500
|
// src/renderers/native/core/Animator.ts
|
|
17862
18501
|
var Animator = class {
|
|
17863
|
-
constructor(tick, raf, cancel,
|
|
18502
|
+
constructor(tick, raf, cancel, now2) {
|
|
17864
18503
|
this.tick = tick;
|
|
17865
18504
|
this.handle = null;
|
|
17866
18505
|
this.last = 0;
|
|
@@ -17874,7 +18513,7 @@ var Animator = class {
|
|
|
17874
18513
|
};
|
|
17875
18514
|
this.raf = raf ?? ((cb) => requestAnimationFrame(cb));
|
|
17876
18515
|
this.cancel = cancel ?? ((h) => cancelAnimationFrame(h));
|
|
17877
|
-
this.now =
|
|
18516
|
+
this.now = now2 ?? (() => performance.now());
|
|
17878
18517
|
}
|
|
17879
18518
|
get active() {
|
|
17880
18519
|
return this.handle !== null;
|
|
@@ -18673,6 +19312,8 @@ var SceneGraph = class {
|
|
|
18673
19312
|
/** VPVR-layer config pushed by the VPVR native indicator (null ⇒ layer off). Ephemeral. */
|
|
18674
19313
|
this.vpvrLayer = null;
|
|
18675
19314
|
this.crosshair = null;
|
|
19315
|
+
/** Runtime override of the crosshair's lines and style (the `crosshairOverride` feature). Ephemeral. */
|
|
19316
|
+
this.crosshairOverride = null;
|
|
18676
19317
|
/** How the base price series is drawn on the price pane (candles by default). */
|
|
18677
19318
|
this.priceStyle = "candles";
|
|
18678
19319
|
/** Price-series base painting for the ACTIVE style (see ChartTypeDefinition.basePainting). */
|
|
@@ -20589,9 +21230,9 @@ function timeTicks(fromMs, toMs, target = 8, offsetMs = 0) {
|
|
|
20589
21230
|
}
|
|
20590
21231
|
|
|
20591
21232
|
// src/renderers/native/chrome/countdown.ts
|
|
20592
|
-
function countdownText(barOpen, barMs,
|
|
21233
|
+
function countdownText(barOpen, barMs, now2) {
|
|
20593
21234
|
if (!(barMs > 0)) return null;
|
|
20594
|
-
const remaining = barOpen + barMs -
|
|
21235
|
+
const remaining = barOpen + barMs - now2;
|
|
20595
21236
|
if (remaining <= 0) return null;
|
|
20596
21237
|
return formatCountdown(remaining);
|
|
20597
21238
|
}
|
|
@@ -21403,19 +22044,35 @@ var CrosshairRenderer = class {
|
|
|
21403
22044
|
const dataH = coords.height;
|
|
21404
22045
|
if (!ch || ch.x < 0 || ch.x > dataW || ch.y < 0 || ch.y > dataH) return;
|
|
21405
22046
|
const cs = scene.style.crosshair;
|
|
22047
|
+
const ov = scene.crosshairOverride;
|
|
22048
|
+
const vertical = ov?.vertical !== false;
|
|
22049
|
+
const horizontal = ov?.horizontal !== false;
|
|
21406
22050
|
ctx.font = `${scene.style.fontSize}px ${theme.fontFamily}`;
|
|
21407
22051
|
ctx.textBaseline = "middle";
|
|
21408
22052
|
const logical = Math.round(coords.xToLogical(ch.x));
|
|
21409
|
-
const x =
|
|
21410
|
-
|
|
21411
|
-
|
|
21412
|
-
|
|
21413
|
-
|
|
22053
|
+
const x = crisp(coords.logicalToX(logical), ov?.width ?? cs.width);
|
|
22054
|
+
if (vertical && ov?.shadeRight) {
|
|
22055
|
+
const from = Math.max(0, Math.round(coords.logicalToX(logical + 0.5)));
|
|
22056
|
+
if (from < dataW) {
|
|
22057
|
+
ctx.fillStyle = ov.shadeRight.color;
|
|
22058
|
+
ctx.globalAlpha = ov.shadeRight.opacity ?? 1;
|
|
22059
|
+
ctx.fillRect(from, 0, dataW - from, dataH);
|
|
22060
|
+
ctx.globalAlpha = 1;
|
|
22061
|
+
}
|
|
22062
|
+
}
|
|
22063
|
+
ctx.strokeStyle = ov?.color ?? cs.color ?? theme.textColor;
|
|
22064
|
+
ctx.lineWidth = ov?.width ?? cs.width;
|
|
22065
|
+
ctx.globalAlpha = ov?.opacity ?? cs.opacity;
|
|
22066
|
+
setDash3(ctx, ov?.style ?? cs.style);
|
|
21414
22067
|
ctx.beginPath();
|
|
21415
|
-
|
|
21416
|
-
|
|
21417
|
-
|
|
21418
|
-
|
|
22068
|
+
if (vertical) {
|
|
22069
|
+
ctx.moveTo(x, 0);
|
|
22070
|
+
ctx.lineTo(x, dataH);
|
|
22071
|
+
}
|
|
22072
|
+
if (horizontal) {
|
|
22073
|
+
ctx.moveTo(0, Math.round(ch.y) + 0.5);
|
|
22074
|
+
ctx.lineTo(dataW, Math.round(ch.y) + 0.5);
|
|
22075
|
+
}
|
|
21419
22076
|
ctx.stroke();
|
|
21420
22077
|
ctx.setLineDash([]);
|
|
21421
22078
|
ctx.lineWidth = 1;
|
|
@@ -21428,11 +22085,11 @@ var CrosshairRenderer = class {
|
|
|
21428
22085
|
}
|
|
21429
22086
|
}
|
|
21430
22087
|
const chipBg = cs.labelBackground ?? theme.borderColor;
|
|
21431
|
-
if (pane && pane.axisFormat !== "none") {
|
|
22088
|
+
if (horizontal && pane && pane.axisFormat !== "none") {
|
|
21432
22089
|
const price = coords.yToPrice(ch.y, pane.scale, pane.bounds);
|
|
21433
22090
|
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);
|
|
21434
22091
|
}
|
|
21435
|
-
this.chip(ctx, x, dataH + 1, formatTimeStamp(coords.logicalToTime(logical), scene.timezone, coords.barInterval), chipBg, "center", true, theme.background);
|
|
22092
|
+
if (vertical) this.chip(ctx, x, dataH + 1, formatTimeStamp(coords.logicalToTime(logical), scene.timezone, coords.barInterval), chipBg, "center", true, theme.background);
|
|
21436
22093
|
}
|
|
21437
22094
|
destroy() {
|
|
21438
22095
|
this.canvas = null;
|
|
@@ -21443,23 +22100,37 @@ var CrosshairRenderer = class {
|
|
|
21443
22100
|
* along), with that bar's time chip in this chart's own timezone and — when the
|
|
21444
22101
|
* level resolved — the price chip on the right axis. The snap happened upstream
|
|
21445
22102
|
* (`externalCrossPx`, floor-to-containing-bar) — this method only draws. Chips
|
|
21446
|
-
* render slightly dimmed so the ghost still reads as foreign.
|
|
22103
|
+
* render slightly dimmed so the ghost still reads as foreign. A `crosshairOverride`
|
|
22104
|
+
* applies to the ghost as well — its line style, a dropped horizontal line, and the
|
|
22105
|
+
* `shadeRight` veil from the ghost bar's right edge — so a pick that spans several
|
|
22106
|
+
* charts reads the same on every one of them. */
|
|
21447
22107
|
drawExternal(ctx, ext, scene, coords, theme) {
|
|
21448
22108
|
const cs = scene.style.crosshair;
|
|
22109
|
+
const ov = scene.crosshairOverride;
|
|
21449
22110
|
const dataW = coords.width;
|
|
21450
22111
|
const dataH = coords.height;
|
|
21451
|
-
const x =
|
|
21452
|
-
if (
|
|
22112
|
+
const x = crisp(ext.x, ov?.width ?? cs.width);
|
|
22113
|
+
if (ov?.vertical === false) return;
|
|
22114
|
+
if (ov?.shadeRight) {
|
|
22115
|
+
const from = Math.max(0, Math.round(coords.logicalToX(Math.round(coords.xToLogical(ext.x)) + 0.5)));
|
|
22116
|
+
if (from < dataW) {
|
|
22117
|
+
ctx.fillStyle = ov.shadeRight.color;
|
|
22118
|
+
ctx.globalAlpha = ov.shadeRight.opacity ?? 1;
|
|
22119
|
+
ctx.fillRect(from, 0, dataW - from, dataH);
|
|
22120
|
+
ctx.globalAlpha = 1;
|
|
22121
|
+
}
|
|
22122
|
+
}
|
|
22123
|
+
if (ext.line === false || x < 0 || x > dataW) return;
|
|
21453
22124
|
ctx.font = `${scene.style.fontSize}px ${theme.fontFamily}`;
|
|
21454
22125
|
ctx.textBaseline = "middle";
|
|
21455
|
-
ctx.strokeStyle = cs.color ?? theme.textColor;
|
|
21456
|
-
ctx.lineWidth = cs.width;
|
|
21457
|
-
ctx.globalAlpha = cs.opacity * 0.55;
|
|
21458
|
-
setDash3(ctx, cs.style);
|
|
22126
|
+
ctx.strokeStyle = ov?.color ?? cs.color ?? theme.textColor;
|
|
22127
|
+
ctx.lineWidth = ov?.width ?? cs.width;
|
|
22128
|
+
ctx.globalAlpha = (ov?.opacity ?? cs.opacity) * 0.55;
|
|
22129
|
+
setDash3(ctx, ov?.style ?? cs.style);
|
|
21459
22130
|
ctx.beginPath();
|
|
21460
22131
|
ctx.moveTo(x, 0);
|
|
21461
22132
|
ctx.lineTo(x, dataH);
|
|
21462
|
-
if (ext.y != null) {
|
|
22133
|
+
if (ext.y != null && ov?.horizontal !== false) {
|
|
21463
22134
|
ctx.moveTo(0, Math.round(ext.y) + 0.5);
|
|
21464
22135
|
ctx.lineTo(dataW, Math.round(ext.y) + 0.5);
|
|
21465
22136
|
}
|
|
@@ -21468,7 +22139,7 @@ var CrosshairRenderer = class {
|
|
|
21468
22139
|
ctx.lineWidth = 1;
|
|
21469
22140
|
ctx.globalAlpha = 0.8;
|
|
21470
22141
|
const chipBg = cs.labelBackground ?? theme.borderColor;
|
|
21471
|
-
if (ext.y != null && ext.price != null) {
|
|
22142
|
+
if (ext.y != null && ext.price != null && ov?.horizontal !== false) {
|
|
21472
22143
|
let pane;
|
|
21473
22144
|
for (const p of scene.panes.values()) {
|
|
21474
22145
|
if (ext.y >= p.bounds.top && ext.y <= p.bounds.top + p.bounds.height) {
|
|
@@ -21508,6 +22179,9 @@ var CrosshairRenderer = class {
|
|
|
21508
22179
|
ctx.textAlign = "start";
|
|
21509
22180
|
}
|
|
21510
22181
|
};
|
|
22182
|
+
function crisp(px, width) {
|
|
22183
|
+
return Math.round(px) + (Math.round(width) % 2 === 1 ? 0.5 : 0);
|
|
22184
|
+
}
|
|
21511
22185
|
function setDash3(ctx, style) {
|
|
21512
22186
|
if (style === "dashed") ctx.setLineDash([6, 4]);
|
|
21513
22187
|
else if (style === "dotted") ctx.setLineDash([2, 3]);
|
|
@@ -29804,7 +30478,7 @@ var NativeRenderer = class {
|
|
|
29804
30478
|
this.moveIndicatorCbs = /* @__PURE__ */ new Set();
|
|
29805
30479
|
this.priceStyleCbs = /* @__PURE__ */ new Set();
|
|
29806
30480
|
this.name = "native";
|
|
29807
|
-
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"];
|
|
30481
|
+
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"];
|
|
29808
30482
|
/** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
|
|
29809
30483
|
* so moving onto the button doesn't count as leaving). */
|
|
29810
30484
|
this.onScrollProximityMove = (e) => {
|
|
@@ -29983,6 +30657,9 @@ var NativeRenderer = class {
|
|
|
29983
30657
|
this.scene.marks = mergeMarksState(this.scene.marks, value);
|
|
29984
30658
|
this.markPopover?.close();
|
|
29985
30659
|
break;
|
|
30660
|
+
case "crosshairOverride":
|
|
30661
|
+
this.scene.crosshairOverride = sanitizeCrosshairOverride(value);
|
|
30662
|
+
break;
|
|
29986
30663
|
case "keyboard":
|
|
29987
30664
|
this.setKeyboardEnabled(Boolean(value));
|
|
29988
30665
|
return;
|
|
@@ -30096,6 +30773,8 @@ var NativeRenderer = class {
|
|
|
30096
30773
|
return { ...this.scene.tradeMarkers, colors: { ...this.scene.tradeMarkers.colors } };
|
|
30097
30774
|
case "marks":
|
|
30098
30775
|
return { visible: this.scene.marks.visible, groups: { ...this.scene.marks.groups } };
|
|
30776
|
+
case "crosshairOverride":
|
|
30777
|
+
return this.scene.crosshairOverride ? { ...this.scene.crosshairOverride } : null;
|
|
30099
30778
|
case "keyboard":
|
|
30100
30779
|
return this.keyboardEnabled;
|
|
30101
30780
|
case "historyChords":
|
|
@@ -30674,8 +31353,8 @@ var NativeRenderer = class {
|
|
|
30674
31353
|
for (const pane of this.scene.panes.values()) pane.scale = { ...pane.scaleTarget };
|
|
30675
31354
|
this.modelAlpha = 0;
|
|
30676
31355
|
const start = performance.now();
|
|
30677
|
-
const step = (
|
|
30678
|
-
const p = Math.min(1, (
|
|
31356
|
+
const step = (now2) => {
|
|
31357
|
+
const p = Math.min(1, (now2 - start) / duration);
|
|
30679
31358
|
this.scene.bars = p >= 1 ? real : real.map((b, i) => this.revealCandle(b, i, p, n, style));
|
|
30680
31359
|
this.paintData();
|
|
30681
31360
|
if (p < 1) {
|
|
@@ -30691,8 +31370,8 @@ var NativeRenderer = class {
|
|
|
30691
31370
|
fadeInModels() {
|
|
30692
31371
|
const fade = Math.min(INTRO_MODEL_FADE_MS, this.intro.duration || INTRO_MODEL_FADE_MS);
|
|
30693
31372
|
const start = performance.now();
|
|
30694
|
-
const step = (
|
|
30695
|
-
this.modelAlpha = Math.min(1, (
|
|
31373
|
+
const step = (now2) => {
|
|
31374
|
+
this.modelAlpha = Math.min(1, (now2 - start) / fade);
|
|
30696
31375
|
this.paintData();
|
|
30697
31376
|
if (this.modelAlpha < 1) {
|
|
30698
31377
|
this.introRaf = requestAnimationFrame(step);
|
|
@@ -31215,10 +31894,13 @@ var NativeRenderer = class {
|
|
|
31215
31894
|
this.animator.start();
|
|
31216
31895
|
}
|
|
31217
31896
|
} else if (!last || bar.time > last.time) {
|
|
31897
|
+
const vp = this.coords.getViewport();
|
|
31898
|
+
const holdView = n > 0 && vp.rightOffset < 0 && this.scrollTargetRO === null;
|
|
31218
31899
|
this.bars.push(bar);
|
|
31219
31900
|
this.syncLiveEase(bar);
|
|
31220
31901
|
if (this.coords.barInterval > 0) this.coords.appendBar(bar.time);
|
|
31221
31902
|
else this.coords.setBars(this.bars.map((b) => b.time));
|
|
31903
|
+
if (holdView) this.coords.setViewport({ ...vp, rightOffset: vp.rightOffset - 1 });
|
|
31222
31904
|
} else {
|
|
31223
31905
|
return;
|
|
31224
31906
|
}
|
|
@@ -31900,10 +32582,10 @@ var NativeRenderer = class {
|
|
|
31900
32582
|
if (this.markPulseRaf !== null || typeof requestAnimationFrame !== "function") return;
|
|
31901
32583
|
const tick = () => {
|
|
31902
32584
|
this.markPulseRaf = null;
|
|
31903
|
-
const
|
|
31904
|
-
if (this.scene.marksFlash && this.scene.marksFlash.until <=
|
|
32585
|
+
const now2 = frameNow();
|
|
32586
|
+
if (this.scene.marksFlash && this.scene.marksFlash.until <= now2) this.scene.marksFlash = null;
|
|
31905
32587
|
this.scheduler?.invalidate(2 /* Chrome */);
|
|
31906
|
-
const pulsing = this.scene.marksHoverKey !== null &&
|
|
32588
|
+
const pulsing = this.scene.marksHoverKey !== null && now2 - this.scene.marksHoverSince < MARK_PULSE_MS;
|
|
31907
32589
|
if (pulsing || this.scene.marksFlash !== null) this.markPulseRaf = requestAnimationFrame(tick);
|
|
31908
32590
|
};
|
|
31909
32591
|
this.markPulseRaf = requestAnimationFrame(tick);
|
|
@@ -32464,9 +33146,10 @@ var NativeRenderer = class {
|
|
|
32464
33146
|
const ext = this.externalCross;
|
|
32465
33147
|
if (!ext || this.coords.barCount === 0) return null;
|
|
32466
33148
|
const logical = Math.floor(this.coords.timeToLogical(ext.time));
|
|
32467
|
-
if (logical <
|
|
32468
|
-
const x = this.coords.logicalToX(logical);
|
|
32469
|
-
if (!Number.isFinite(x)
|
|
33149
|
+
if (!(logical < this.coords.barCount)) return null;
|
|
33150
|
+
const x = this.coords.logicalToX(Math.max(logical, -1));
|
|
33151
|
+
if (!Number.isFinite(x)) return null;
|
|
33152
|
+
const line = logical >= 0 && x >= 0 && x <= this.coords.width;
|
|
32470
33153
|
let y = null;
|
|
32471
33154
|
if (ext.price != null) {
|
|
32472
33155
|
const pricePane = this.scene.orderedPanes().find((p) => p.kind === "price");
|
|
@@ -32475,7 +33158,7 @@ var NativeRenderer = class {
|
|
|
32475
33158
|
if (!Number.isFinite(y) || y < pricePane.bounds.top || y > pricePane.bounds.top + pricePane.bounds.height) y = null;
|
|
32476
33159
|
}
|
|
32477
33160
|
}
|
|
32478
|
-
return { x, y, time: this.coords.logicalToTime(logical), price: y != null ? ext.price : null };
|
|
33161
|
+
return { x, y, time: this.coords.logicalToTime(Math.max(logical, 0)), price: y != null ? ext.price : null, line };
|
|
32479
33162
|
}
|
|
32480
33163
|
/** Set the sticky magnet mode (called by the drawings toolbar's 3-state button). */
|
|
32481
33164
|
setSnapMode(mode) {
|
|
@@ -37180,6 +37863,7 @@ var Vela = class {
|
|
|
37180
37863
|
this.panesControl = new PanesControl(this.orchestrator);
|
|
37181
37864
|
this.drawingsControl = new DrawingsControl(this.orchestrator.drawings);
|
|
37182
37865
|
this.marksControl = new MarksControl(this.orchestrator.marks);
|
|
37866
|
+
this.replayControl = new ReplayControl(this.orchestrator);
|
|
37183
37867
|
}
|
|
37184
37868
|
/**
|
|
37185
37869
|
* Register a scripting engine so `addIndicator({ language })` can run that
|
|
@@ -37419,6 +38103,16 @@ var Vela = class {
|
|
|
37419
38103
|
get marks() {
|
|
37420
38104
|
return this.marksControl;
|
|
37421
38105
|
}
|
|
38106
|
+
/**
|
|
38107
|
+
* The chart's bar-replay control surface: rewind to a past bar and reveal the
|
|
38108
|
+
* following ones by hand or on a timer, from the history already loaded —
|
|
38109
|
+
* `await chart.replay.start({ from })`, `chart.replay.play(2000)`, `chart.replay.step()`,
|
|
38110
|
+
* `chart.replay.stop()`. Revealed bars behave like live bars for indicators; live
|
|
38111
|
+
* updates pause meanwhile. Follow it with the `replay:*` events.
|
|
38112
|
+
*/
|
|
38113
|
+
get replay() {
|
|
38114
|
+
return this.replayControl;
|
|
38115
|
+
}
|
|
37422
38116
|
on(event, handler) {
|
|
37423
38117
|
return this.orchestrator.events.on(event, handler);
|
|
37424
38118
|
}
|
|
@@ -37477,6 +38171,28 @@ function resolveElement(container) {
|
|
|
37477
38171
|
return element;
|
|
37478
38172
|
}
|
|
37479
38173
|
|
|
38174
|
+
// src/core/replayTicks.ts
|
|
38175
|
+
function lowerTimeframeTicks(chart, timeframe) {
|
|
38176
|
+
return async (bar, { end, signal }) => {
|
|
38177
|
+
const { symbol, session } = chart.market;
|
|
38178
|
+
const resolved = symbol ? chart.data.resolve(symbol) : null;
|
|
38179
|
+
const provider = resolved ? chart.data.providerInstance(resolved.provider) : void 0;
|
|
38180
|
+
if (!resolved || !provider) return [];
|
|
38181
|
+
const bars = await provider.getBars(resolved.ticker, timeframe, { from: bar.time, to: end - 1, ...session ? { session } : {} });
|
|
38182
|
+
if (signal.aborted) return [];
|
|
38183
|
+
return barsToTicks(bars.filter((b) => b.time >= bar.time && b.time < end));
|
|
38184
|
+
};
|
|
38185
|
+
}
|
|
38186
|
+
function barsToTicks(bars) {
|
|
38187
|
+
return bars.map((b) => ({
|
|
38188
|
+
price: b.close,
|
|
38189
|
+
open: b.open,
|
|
38190
|
+
high: b.high,
|
|
38191
|
+
low: b.low,
|
|
38192
|
+
...b.volume === void 0 ? {} : { volume: b.volume }
|
|
38193
|
+
}));
|
|
38194
|
+
}
|
|
38195
|
+
|
|
37480
38196
|
// src/widget/topbar-composition.ts
|
|
37481
38197
|
var TOPBAR_BUILTIN_IDS = ["symbol", "timeframes", "style", "layout", "indicators", "actions", "undo-redo", "alerts", "panels", "screenshot"];
|
|
37482
38198
|
|
|
@@ -37637,6 +38353,7 @@ exports.NEUTRAL = NEUTRAL;
|
|
|
37637
38353
|
exports.NativeRenderer = NativeRenderer;
|
|
37638
38354
|
exports.OVERRIDABLE_TOPBAR_IDS = OVERRIDABLE_TOPBAR_IDS;
|
|
37639
38355
|
exports.RendererControl = RendererControl;
|
|
38356
|
+
exports.ReplayControl = ReplayControl;
|
|
37640
38357
|
exports.SERIES_LINE = SERIES_LINE;
|
|
37641
38358
|
exports.SESSION_OFF = SESSION_OFF;
|
|
37642
38359
|
exports.SESSION_POST = SESSION_POST;
|
|
@@ -37651,6 +38368,7 @@ exports.TypedEventBus = TypedEventBus;
|
|
|
37651
38368
|
exports.VALID = VALID;
|
|
37652
38369
|
exports.Vela = Vela;
|
|
37653
38370
|
exports.WARNING = WARNING;
|
|
38371
|
+
exports.barsToTicks = barsToTicks;
|
|
37654
38372
|
exports.buildToolbar = buildToolbar;
|
|
37655
38373
|
exports.categoricalColor = categoricalColor;
|
|
37656
38374
|
exports.chartType = chartType;
|
|
@@ -37667,6 +38385,7 @@ exports.inputDeltas = inputDeltas;
|
|
|
37667
38385
|
exports.inputVisible = inputVisible;
|
|
37668
38386
|
exports.legendActions = legendActions;
|
|
37669
38387
|
exports.legendCallouts = legendCallouts;
|
|
38388
|
+
exports.lowerTimeframeTicks = lowerTimeframeTicks;
|
|
37670
38389
|
exports.nativeIndicatorDescriptors = nativeIndicatorDescriptors;
|
|
37671
38390
|
exports.nativeIndicatorTypes = nativeIndicatorTypes;
|
|
37672
38391
|
exports.normalizeSettingsRow = normalizeSettingsRow;
|