@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/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;
|
|
@@ -13240,14 +14047,49 @@ function buildColorPicker(color, theme, onChange, options = {}) {
|
|
|
13240
14047
|
const knob = document.createElement("div");
|
|
13241
14048
|
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;";
|
|
13242
14049
|
track.appendChild(knob);
|
|
13243
|
-
const pctBox = document.createElement("
|
|
13244
|
-
pctBox.
|
|
14050
|
+
const pctBox = document.createElement("input");
|
|
14051
|
+
pctBox.type = "text";
|
|
14052
|
+
pctBox.inputMode = "numeric";
|
|
14053
|
+
pctBox.title = "Opacity (%)";
|
|
14054
|
+
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;`;
|
|
14055
|
+
pctBox.addEventListener("focus", () => {
|
|
14056
|
+
pctBox.style.borderColor = "var(--vela-border-strong)";
|
|
14057
|
+
pctBox.select();
|
|
14058
|
+
});
|
|
13245
14059
|
opRow.append(track, pctBox);
|
|
13246
14060
|
const paintOpacity = () => {
|
|
13247
14061
|
track.style.background = `linear-gradient(to right, ${curHex}00, ${curHex}ff), ${CHECKER}`;
|
|
13248
14062
|
knob.style.left = `${curAlpha * 100}%`;
|
|
13249
|
-
pctBox.
|
|
14063
|
+
pctBox.value = `${Math.round(curAlpha * 100)}%`;
|
|
13250
14064
|
};
|
|
14065
|
+
const commitTyped = () => {
|
|
14066
|
+
pctBox.style.borderColor = "var(--vela-border)";
|
|
14067
|
+
const n = parseFloat(pctBox.value.replace("%", ""));
|
|
14068
|
+
if (Number.isFinite(n)) {
|
|
14069
|
+
const next = Math.max(0, Math.min(100, n)) / 100;
|
|
14070
|
+
if (Math.round(next * 100) !== Math.round(curAlpha * 100)) {
|
|
14071
|
+
curAlpha = next;
|
|
14072
|
+
paintOpacity();
|
|
14073
|
+
emit();
|
|
14074
|
+
return;
|
|
14075
|
+
}
|
|
14076
|
+
}
|
|
14077
|
+
paintOpacity();
|
|
14078
|
+
};
|
|
14079
|
+
pctBox.addEventListener("change", commitTyped);
|
|
14080
|
+
pctBox.addEventListener("blur", () => {
|
|
14081
|
+
pctBox.style.borderColor = "var(--vela-border)";
|
|
14082
|
+
paintOpacity();
|
|
14083
|
+
});
|
|
14084
|
+
pctBox.addEventListener("keydown", (e) => {
|
|
14085
|
+
if (e.key === "Escape") return;
|
|
14086
|
+
e.stopPropagation();
|
|
14087
|
+
if (e.key === "Enter") {
|
|
14088
|
+
e.preventDefault();
|
|
14089
|
+
commitTyped();
|
|
14090
|
+
pctBox.blur();
|
|
14091
|
+
}
|
|
14092
|
+
});
|
|
13251
14093
|
let dragging = false;
|
|
13252
14094
|
const onDrag = (clientX) => {
|
|
13253
14095
|
const r = track.getBoundingClientRect();
|
|
@@ -14471,10 +15313,16 @@ var IndicatorInputsDialog = class {
|
|
|
14471
15313
|
this.calendarPop = null;
|
|
14472
15314
|
this.calendarAnchor = null;
|
|
14473
15315
|
},
|
|
14474
|
-
content: (el) =>
|
|
14475
|
-
|
|
14476
|
-
|
|
14477
|
-
|
|
15316
|
+
content: (el) => {
|
|
15317
|
+
const picker = new DatePicker({
|
|
15318
|
+
value: current,
|
|
15319
|
+
onPick: (iso) => {
|
|
15320
|
+
pop.hide();
|
|
15321
|
+
onPick(iso);
|
|
15322
|
+
}
|
|
15323
|
+
});
|
|
15324
|
+
el.append(picker.el);
|
|
15325
|
+
}
|
|
14478
15326
|
});
|
|
14479
15327
|
this.calendarPop = pop;
|
|
14480
15328
|
this.calendarAnchor = anchor;
|
|
@@ -14599,28 +15447,6 @@ function ensureDialogStyles() {
|
|
|
14599
15447
|
.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
15448
|
.vela-ind-combo-chevron:hover{opacity:0.9;}
|
|
14601
15449
|
.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
15450
|
${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
|
|
14625
15451
|
.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
15452
|
.vela-ind-tab:not(.vela-ind-tab-active):hover{color:var(--vela-fg-bright);}
|
|
@@ -14654,174 +15480,6 @@ var TIME_OPTIONS = Array.from({ length: 48 }, (_, i) => {
|
|
|
14654
15480
|
const v = `${hh}:${mm}`;
|
|
14655
15481
|
return { value: v, label: v };
|
|
14656
15482
|
});
|
|
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
15483
|
function normalizeTimeInput(raw) {
|
|
14826
15484
|
const t = raw.trim();
|
|
14827
15485
|
const m = /^(\d{1,2}):(\d{2})$/.exec(t) ?? /^(\d{2})(\d{2})$/.exec(t);
|
|
@@ -16199,6 +16857,22 @@ function priceStyleIds() {
|
|
|
16199
16857
|
for (const t of chartTypes()) if (!out.includes(t.id)) out.push(t.id);
|
|
16200
16858
|
return out;
|
|
16201
16859
|
}
|
|
16860
|
+
function sanitizeCrosshairOverride(value) {
|
|
16861
|
+
if (!value || typeof value !== "object") return null;
|
|
16862
|
+
const v = value;
|
|
16863
|
+
const out = {};
|
|
16864
|
+
if (isBool(v.vertical)) out.vertical = v.vertical;
|
|
16865
|
+
if (isBool(v.horizontal)) out.horizontal = v.horizontal;
|
|
16866
|
+
if (isColor(v.color)) out.color = v.color;
|
|
16867
|
+
if (isNum(v.width)) out.width = Math.max(0.5, Math.min(8, v.width));
|
|
16868
|
+
if (isLineStyle(v.style)) out.style = v.style;
|
|
16869
|
+
if (isNum(v.opacity)) out.opacity = clampOpacity(v.opacity);
|
|
16870
|
+
const shade = v.shadeRight;
|
|
16871
|
+
if (shade && typeof shade === "object" && isColor(shade.color)) {
|
|
16872
|
+
out.shadeRight = isNum(shade.opacity) ? { color: shade.color, opacity: clampOpacity(shade.opacity) } : { color: shade.color };
|
|
16873
|
+
}
|
|
16874
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
16875
|
+
}
|
|
16202
16876
|
function isColor(v) {
|
|
16203
16877
|
return typeof v === "string" && v.trim().length > 0;
|
|
16204
16878
|
}
|
|
@@ -17860,7 +18534,7 @@ var Scheduler = class {
|
|
|
17860
18534
|
|
|
17861
18535
|
// src/renderers/native/core/Animator.ts
|
|
17862
18536
|
var Animator = class {
|
|
17863
|
-
constructor(tick, raf, cancel,
|
|
18537
|
+
constructor(tick, raf, cancel, now2) {
|
|
17864
18538
|
this.tick = tick;
|
|
17865
18539
|
this.handle = null;
|
|
17866
18540
|
this.last = 0;
|
|
@@ -17874,7 +18548,7 @@ var Animator = class {
|
|
|
17874
18548
|
};
|
|
17875
18549
|
this.raf = raf ?? ((cb) => requestAnimationFrame(cb));
|
|
17876
18550
|
this.cancel = cancel ?? ((h) => cancelAnimationFrame(h));
|
|
17877
|
-
this.now =
|
|
18551
|
+
this.now = now2 ?? (() => performance.now());
|
|
17878
18552
|
}
|
|
17879
18553
|
get active() {
|
|
17880
18554
|
return this.handle !== null;
|
|
@@ -18006,6 +18680,12 @@ var InputController = class {
|
|
|
18006
18680
|
this.lastTapT = 0;
|
|
18007
18681
|
this.lastTapX = 0;
|
|
18008
18682
|
this.lastTapY = 0;
|
|
18683
|
+
// Whether the last two primary presses were claimed by the drawings layer. A double-click
|
|
18684
|
+
// is the last press plus the one before it; a tool-driven click pair must never fall through
|
|
18685
|
+
// to the pane maximize toggle, even when the tool disarmed itself between the two clicks
|
|
18686
|
+
// (a finished placement leaves plain cursor mode for the second click).
|
|
18687
|
+
this.lastPressDrawing = false;
|
|
18688
|
+
this.prevPressDrawing = false;
|
|
18009
18689
|
this.onTouchContextMenu = (e) => {
|
|
18010
18690
|
if (this.touches.size === 0) return;
|
|
18011
18691
|
if (!(e.target instanceof Node) || !this.el?.contains(e.target)) return;
|
|
@@ -18060,19 +18740,24 @@ var InputController = class {
|
|
|
18060
18740
|
this.moved = false;
|
|
18061
18741
|
this.startX = x;
|
|
18062
18742
|
this.startY = y;
|
|
18743
|
+
this.prevPressDrawing = this.lastPressDrawing;
|
|
18744
|
+
this.lastPressDrawing = false;
|
|
18063
18745
|
if (this.deps.drawingsClaim?.(x, y)) {
|
|
18064
18746
|
this.region = "drawing";
|
|
18747
|
+
this.lastPressDrawing = true;
|
|
18065
18748
|
this.deps.drawingsPointerDown?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
|
|
18066
18749
|
this.capture(e.pointerId);
|
|
18067
18750
|
return;
|
|
18068
18751
|
}
|
|
18069
18752
|
if (e.shiftKey && this.regionAt(x, y) === "data" && this.deps.drawingsMeasureStart?.(x, y, this.snapMode(e))) {
|
|
18070
18753
|
this.region = "drawing";
|
|
18754
|
+
this.lastPressDrawing = true;
|
|
18071
18755
|
this.capture(e.pointerId);
|
|
18072
18756
|
return;
|
|
18073
18757
|
}
|
|
18074
18758
|
if ((e.ctrlKey || e.metaKey) && this.regionAt(x, y) === "data" && this.deps.drawingsMarqueeStart?.(x, y)) {
|
|
18075
18759
|
this.region = "drawing";
|
|
18760
|
+
this.lastPressDrawing = true;
|
|
18076
18761
|
this.capture(e.pointerId);
|
|
18077
18762
|
return;
|
|
18078
18763
|
}
|
|
@@ -18394,6 +19079,7 @@ var InputController = class {
|
|
|
18394
19079
|
if (region === "price") this.deps.resetPriceScale(x, y);
|
|
18395
19080
|
else if (region === "separator") this.deps.resetPaneSize(y);
|
|
18396
19081
|
else if (region === "time") this.deps.resetView();
|
|
19082
|
+
else if (this.lastPressDrawing || this.prevPressDrawing) return;
|
|
18397
19083
|
else this.deps.dataDblClick(x, y);
|
|
18398
19084
|
}
|
|
18399
19085
|
};
|
|
@@ -18673,6 +19359,8 @@ var SceneGraph = class {
|
|
|
18673
19359
|
/** VPVR-layer config pushed by the VPVR native indicator (null ⇒ layer off). Ephemeral. */
|
|
18674
19360
|
this.vpvrLayer = null;
|
|
18675
19361
|
this.crosshair = null;
|
|
19362
|
+
/** Runtime override of the crosshair's lines and style (the `crosshairOverride` feature). Ephemeral. */
|
|
19363
|
+
this.crosshairOverride = null;
|
|
18676
19364
|
/** How the base price series is drawn on the price pane (candles by default). */
|
|
18677
19365
|
this.priceStyle = "candles";
|
|
18678
19366
|
/** Price-series base painting for the ACTIVE style (see ChartTypeDefinition.basePainting). */
|
|
@@ -20557,6 +21245,7 @@ function timeTicks(fromMs, toMs, target = 8, offsetMs = 0) {
|
|
|
20557
21245
|
const step = pickStep(span / Math.max(1, target));
|
|
20558
21246
|
const zFrom = fromMs + offsetMs;
|
|
20559
21247
|
const zTo = toMs + offsetMs;
|
|
21248
|
+
if (step >= MONTH) return calendarTicks(zFrom, zTo, step >= YEAR ? 12 : step >= 3 * MONTH ? 3 : 1, offsetMs);
|
|
20560
21249
|
const first = Math.ceil(zFrom / step) * step;
|
|
20561
21250
|
const out = [];
|
|
20562
21251
|
for (let zt = first; zt <= zTo; zt += step) {
|
|
@@ -20587,11 +21276,23 @@ function timeTicks(fromMs, toMs, target = 8, offsetMs = 0) {
|
|
|
20587
21276
|
}
|
|
20588
21277
|
return out;
|
|
20589
21278
|
}
|
|
21279
|
+
function calendarTicks(zFrom, zTo, months, offsetMs) {
|
|
21280
|
+
const d0 = new Date(zFrom);
|
|
21281
|
+
let idx = d0.getUTCFullYear() * 12 + d0.getUTCMonth();
|
|
21282
|
+
if (Date.UTC(Math.floor(idx / 12), idx % 12, 1) < zFrom) idx += 1;
|
|
21283
|
+
idx = Math.ceil(idx / months) * months;
|
|
21284
|
+
const out = [];
|
|
21285
|
+
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)) {
|
|
21286
|
+
const label = months === 12 ? String(Math.floor(idx / 12)) : MONTHS[idx % 12];
|
|
21287
|
+
out.push({ time: zt - offsetMs, label, major: true });
|
|
21288
|
+
}
|
|
21289
|
+
return out;
|
|
21290
|
+
}
|
|
20590
21291
|
|
|
20591
21292
|
// src/renderers/native/chrome/countdown.ts
|
|
20592
|
-
function countdownText(barOpen, barMs,
|
|
21293
|
+
function countdownText(barOpen, barMs, now2) {
|
|
20593
21294
|
if (!(barMs > 0)) return null;
|
|
20594
|
-
const remaining = barOpen + barMs -
|
|
21295
|
+
const remaining = barOpen + barMs - now2;
|
|
20595
21296
|
if (remaining <= 0) return null;
|
|
20596
21297
|
return formatCountdown(remaining);
|
|
20597
21298
|
}
|
|
@@ -21403,19 +22104,35 @@ var CrosshairRenderer = class {
|
|
|
21403
22104
|
const dataH = coords.height;
|
|
21404
22105
|
if (!ch || ch.x < 0 || ch.x > dataW || ch.y < 0 || ch.y > dataH) return;
|
|
21405
22106
|
const cs = scene.style.crosshair;
|
|
22107
|
+
const ov = scene.crosshairOverride;
|
|
22108
|
+
const vertical = ov?.vertical !== false;
|
|
22109
|
+
const horizontal = ov?.horizontal !== false;
|
|
21406
22110
|
ctx.font = `${scene.style.fontSize}px ${theme.fontFamily}`;
|
|
21407
22111
|
ctx.textBaseline = "middle";
|
|
21408
22112
|
const logical = Math.round(coords.xToLogical(ch.x));
|
|
21409
|
-
const x =
|
|
21410
|
-
|
|
21411
|
-
|
|
21412
|
-
|
|
21413
|
-
|
|
22113
|
+
const x = crisp(coords.logicalToX(logical), ov?.width ?? cs.width);
|
|
22114
|
+
if (vertical && ov?.shadeRight) {
|
|
22115
|
+
const from = Math.max(0, Math.round(coords.logicalToX(logical + 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
|
+
ctx.strokeStyle = ov?.color ?? cs.color ?? theme.textColor;
|
|
22124
|
+
ctx.lineWidth = ov?.width ?? cs.width;
|
|
22125
|
+
ctx.globalAlpha = ov?.opacity ?? cs.opacity;
|
|
22126
|
+
setDash3(ctx, ov?.style ?? cs.style);
|
|
21414
22127
|
ctx.beginPath();
|
|
21415
|
-
|
|
21416
|
-
|
|
21417
|
-
|
|
21418
|
-
|
|
22128
|
+
if (vertical) {
|
|
22129
|
+
ctx.moveTo(x, 0);
|
|
22130
|
+
ctx.lineTo(x, dataH);
|
|
22131
|
+
}
|
|
22132
|
+
if (horizontal) {
|
|
22133
|
+
ctx.moveTo(0, Math.round(ch.y) + 0.5);
|
|
22134
|
+
ctx.lineTo(dataW, Math.round(ch.y) + 0.5);
|
|
22135
|
+
}
|
|
21419
22136
|
ctx.stroke();
|
|
21420
22137
|
ctx.setLineDash([]);
|
|
21421
22138
|
ctx.lineWidth = 1;
|
|
@@ -21428,11 +22145,11 @@ var CrosshairRenderer = class {
|
|
|
21428
22145
|
}
|
|
21429
22146
|
}
|
|
21430
22147
|
const chipBg = cs.labelBackground ?? theme.borderColor;
|
|
21431
|
-
if (pane && pane.axisFormat !== "none") {
|
|
22148
|
+
if (horizontal && pane && pane.axisFormat !== "none") {
|
|
21432
22149
|
const price = coords.yToPrice(ch.y, pane.scale, pane.bounds);
|
|
21433
22150
|
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
22151
|
}
|
|
21435
|
-
this.chip(ctx, x, dataH + 1, formatTimeStamp(coords.logicalToTime(logical), scene.timezone, coords.barInterval), chipBg, "center", true, theme.background);
|
|
22152
|
+
if (vertical) this.chip(ctx, x, dataH + 1, formatTimeStamp(coords.logicalToTime(logical), scene.timezone, coords.barInterval), chipBg, "center", true, theme.background);
|
|
21436
22153
|
}
|
|
21437
22154
|
destroy() {
|
|
21438
22155
|
this.canvas = null;
|
|
@@ -21443,23 +22160,37 @@ var CrosshairRenderer = class {
|
|
|
21443
22160
|
* along), with that bar's time chip in this chart's own timezone and — when the
|
|
21444
22161
|
* level resolved — the price chip on the right axis. The snap happened upstream
|
|
21445
22162
|
* (`externalCrossPx`, floor-to-containing-bar) — this method only draws. Chips
|
|
21446
|
-
* render slightly dimmed so the ghost still reads as foreign.
|
|
22163
|
+
* render slightly dimmed so the ghost still reads as foreign. A `crosshairOverride`
|
|
22164
|
+
* applies to the ghost as well — its line style, a dropped horizontal line, and the
|
|
22165
|
+
* `shadeRight` veil from the ghost bar's right edge — so a pick that spans several
|
|
22166
|
+
* charts reads the same on every one of them. */
|
|
21447
22167
|
drawExternal(ctx, ext, scene, coords, theme) {
|
|
21448
22168
|
const cs = scene.style.crosshair;
|
|
22169
|
+
const ov = scene.crosshairOverride;
|
|
21449
22170
|
const dataW = coords.width;
|
|
21450
22171
|
const dataH = coords.height;
|
|
21451
|
-
const x =
|
|
21452
|
-
if (
|
|
22172
|
+
const x = crisp(ext.x, ov?.width ?? cs.width);
|
|
22173
|
+
if (ov?.vertical === false) return;
|
|
22174
|
+
if (ov?.shadeRight) {
|
|
22175
|
+
const from = Math.max(0, Math.round(coords.logicalToX(Math.round(coords.xToLogical(ext.x)) + 0.5)));
|
|
22176
|
+
if (from < dataW) {
|
|
22177
|
+
ctx.fillStyle = ov.shadeRight.color;
|
|
22178
|
+
ctx.globalAlpha = ov.shadeRight.opacity ?? 1;
|
|
22179
|
+
ctx.fillRect(from, 0, dataW - from, dataH);
|
|
22180
|
+
ctx.globalAlpha = 1;
|
|
22181
|
+
}
|
|
22182
|
+
}
|
|
22183
|
+
if (ext.line === false || x < 0 || x > dataW) return;
|
|
21453
22184
|
ctx.font = `${scene.style.fontSize}px ${theme.fontFamily}`;
|
|
21454
22185
|
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);
|
|
22186
|
+
ctx.strokeStyle = ov?.color ?? cs.color ?? theme.textColor;
|
|
22187
|
+
ctx.lineWidth = ov?.width ?? cs.width;
|
|
22188
|
+
ctx.globalAlpha = (ov?.opacity ?? cs.opacity) * 0.55;
|
|
22189
|
+
setDash3(ctx, ov?.style ?? cs.style);
|
|
21459
22190
|
ctx.beginPath();
|
|
21460
22191
|
ctx.moveTo(x, 0);
|
|
21461
22192
|
ctx.lineTo(x, dataH);
|
|
21462
|
-
if (ext.y != null) {
|
|
22193
|
+
if (ext.y != null && ov?.horizontal !== false) {
|
|
21463
22194
|
ctx.moveTo(0, Math.round(ext.y) + 0.5);
|
|
21464
22195
|
ctx.lineTo(dataW, Math.round(ext.y) + 0.5);
|
|
21465
22196
|
}
|
|
@@ -21468,7 +22199,7 @@ var CrosshairRenderer = class {
|
|
|
21468
22199
|
ctx.lineWidth = 1;
|
|
21469
22200
|
ctx.globalAlpha = 0.8;
|
|
21470
22201
|
const chipBg = cs.labelBackground ?? theme.borderColor;
|
|
21471
|
-
if (ext.y != null && ext.price != null) {
|
|
22202
|
+
if (ext.y != null && ext.price != null && ov?.horizontal !== false) {
|
|
21472
22203
|
let pane;
|
|
21473
22204
|
for (const p of scene.panes.values()) {
|
|
21474
22205
|
if (ext.y >= p.bounds.top && ext.y <= p.bounds.top + p.bounds.height) {
|
|
@@ -21508,6 +22239,9 @@ var CrosshairRenderer = class {
|
|
|
21508
22239
|
ctx.textAlign = "start";
|
|
21509
22240
|
}
|
|
21510
22241
|
};
|
|
22242
|
+
function crisp(px, width) {
|
|
22243
|
+
return Math.round(px) + (Math.round(width) % 2 === 1 ? 0.5 : 0);
|
|
22244
|
+
}
|
|
21511
22245
|
function setDash3(ctx, style) {
|
|
21512
22246
|
if (style === "dashed") ctx.setLineDash([6, 4]);
|
|
21513
22247
|
else if (style === "dotted") ctx.setLineDash([2, 3]);
|
|
@@ -29099,23 +29833,21 @@ function stackLayers(entries, candleZ) {
|
|
|
29099
29833
|
return { below, above };
|
|
29100
29834
|
}
|
|
29101
29835
|
|
|
29102
|
-
// src/renderers/native/chrome/
|
|
29103
|
-
var
|
|
29104
|
-
var
|
|
29836
|
+
// src/renderers/native/chrome/vela-logos.ts
|
|
29837
|
+
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>';
|
|
29838
|
+
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>';
|
|
29105
29839
|
|
|
29106
29840
|
// src/renderers/native/chrome/AttributionMark.ts
|
|
29107
|
-
var ATTRIBUTION_URL = "https://
|
|
29841
|
+
var ATTRIBUTION_URL = "https://velacharts.dev/?utm_source=vela&utm_medium=attribution";
|
|
29108
29842
|
var STYLE_ID5 = "vela-attribution-styles";
|
|
29109
29843
|
var CSS2 = `
|
|
29110
29844
|
.vela-attribution {
|
|
29111
29845
|
text-decoration: none;
|
|
29112
29846
|
display: flex;
|
|
29113
29847
|
align-items: center;
|
|
29114
|
-
/*
|
|
29115
|
-
|
|
29116
|
-
|
|
29117
|
-
reads wrong because the symbol's ink hangs low and the wordmark descends. */
|
|
29118
|
-
gap: 4px;
|
|
29848
|
+
/* No gutter: the V is the wordmark's first letter, and the letters' viewBox starts
|
|
29849
|
+
where the V's ends, so the kit's own letter spacing comes with them. */
|
|
29850
|
+
gap: 0;
|
|
29119
29851
|
}
|
|
29120
29852
|
.vela-attribution .vela-attr-symbol {
|
|
29121
29853
|
flex: none;
|
|
@@ -29123,7 +29855,7 @@ var CSS2 = `
|
|
|
29123
29855
|
line-height: 0;
|
|
29124
29856
|
}
|
|
29125
29857
|
.vela-attribution .vela-attr-symbol svg {
|
|
29126
|
-
height:
|
|
29858
|
+
height: 24px;
|
|
29127
29859
|
width: auto;
|
|
29128
29860
|
display: block;
|
|
29129
29861
|
filter: drop-shadow(0 1px 2px var(--vela-attr-shadow, rgba(0,0,0,0.45)));
|
|
@@ -29134,15 +29866,13 @@ var CSS2 = `
|
|
|
29134
29866
|
opacity: 0;
|
|
29135
29867
|
display: flex;
|
|
29136
29868
|
align-items: center;
|
|
29137
|
-
/*
|
|
29138
|
-
position: relative;
|
|
29139
|
-
top: 3px;
|
|
29869
|
+
/* Same height and baseline as the V (one viewBox height), so no vertical offset. */
|
|
29140
29870
|
transform: translateX(-8px);
|
|
29141
29871
|
transition: max-width 0.3s ease, opacity 0.25s ease, transform 0.3s ease;
|
|
29142
29872
|
flex: none;
|
|
29143
29873
|
}
|
|
29144
29874
|
.vela-attribution .vela-attr-wordmark svg {
|
|
29145
|
-
height:
|
|
29875
|
+
height: 24px;
|
|
29146
29876
|
width: auto;
|
|
29147
29877
|
display: block;
|
|
29148
29878
|
filter: drop-shadow(0 1px 2px var(--vela-attr-shadow, rgba(0,0,0,0.45)));
|
|
@@ -29157,14 +29887,11 @@ var CSS2 = `
|
|
|
29157
29887
|
(data-layout): the workspace's single grid-wide mark lives OUTSIDE any renderer
|
|
29158
29888
|
container, so only the shell attribute reaches it. */
|
|
29159
29889
|
[data-vela-layout='mobile'] .vela-attribution .vela-attr-symbol svg,
|
|
29160
|
-
[data-layout='mobile'] .vela-attribution .vela-attr-symbol svg { height:
|
|
29890
|
+
[data-layout='mobile'] .vela-attribution .vela-attr-symbol svg { height: 20px; }
|
|
29161
29891
|
[data-vela-layout='mobile'] .vela-attribution .vela-attr-wordmark,
|
|
29162
|
-
[data-layout='mobile'] .vela-attribution .vela-attr-wordmark {
|
|
29163
|
-
top: 2px;
|
|
29164
|
-
transform: translateX(-6px);
|
|
29165
|
-
}
|
|
29892
|
+
[data-layout='mobile'] .vela-attribution .vela-attr-wordmark { transform: translateX(-6px); }
|
|
29166
29893
|
[data-vela-layout='mobile'] .vela-attribution .vela-attr-wordmark svg,
|
|
29167
|
-
[data-layout='mobile'] .vela-attribution .vela-attr-wordmark svg { height:
|
|
29894
|
+
[data-layout='mobile'] .vela-attribution .vela-attr-wordmark svg { height: 20px; }
|
|
29168
29895
|
[data-vela-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark,
|
|
29169
29896
|
[data-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark { transform: translateX(0); }
|
|
29170
29897
|
`;
|
|
@@ -29221,10 +29948,10 @@ function createAttributionMark(doc, background) {
|
|
|
29221
29948
|
const symbol = doc.createElement("span");
|
|
29222
29949
|
symbol.className = "vela-attr-symbol";
|
|
29223
29950
|
symbol.setAttribute("aria-hidden", "true");
|
|
29224
|
-
symbol.innerHTML =
|
|
29951
|
+
symbol.innerHTML = VELA_MARK_SVG;
|
|
29225
29952
|
const wordmark = doc.createElement("span");
|
|
29226
29953
|
wordmark.className = "vela-attr-wordmark";
|
|
29227
|
-
wordmark.innerHTML =
|
|
29954
|
+
wordmark.innerHTML = VELA_LETTERS_SVG;
|
|
29228
29955
|
a.append(symbol, wordmark);
|
|
29229
29956
|
return a;
|
|
29230
29957
|
}
|
|
@@ -29804,7 +30531,7 @@ var NativeRenderer = class {
|
|
|
29804
30531
|
this.moveIndicatorCbs = /* @__PURE__ */ new Set();
|
|
29805
30532
|
this.priceStyleCbs = /* @__PURE__ */ new Set();
|
|
29806
30533
|
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"];
|
|
30534
|
+
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
30535
|
/** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
|
|
29809
30536
|
* so moving onto the button doesn't count as leaving). */
|
|
29810
30537
|
this.onScrollProximityMove = (e) => {
|
|
@@ -29983,6 +30710,9 @@ var NativeRenderer = class {
|
|
|
29983
30710
|
this.scene.marks = mergeMarksState(this.scene.marks, value);
|
|
29984
30711
|
this.markPopover?.close();
|
|
29985
30712
|
break;
|
|
30713
|
+
case "crosshairOverride":
|
|
30714
|
+
this.scene.crosshairOverride = sanitizeCrosshairOverride(value);
|
|
30715
|
+
break;
|
|
29986
30716
|
case "keyboard":
|
|
29987
30717
|
this.setKeyboardEnabled(Boolean(value));
|
|
29988
30718
|
return;
|
|
@@ -30096,6 +30826,8 @@ var NativeRenderer = class {
|
|
|
30096
30826
|
return { ...this.scene.tradeMarkers, colors: { ...this.scene.tradeMarkers.colors } };
|
|
30097
30827
|
case "marks":
|
|
30098
30828
|
return { visible: this.scene.marks.visible, groups: { ...this.scene.marks.groups } };
|
|
30829
|
+
case "crosshairOverride":
|
|
30830
|
+
return this.scene.crosshairOverride ? { ...this.scene.crosshairOverride } : null;
|
|
30099
30831
|
case "keyboard":
|
|
30100
30832
|
return this.keyboardEnabled;
|
|
30101
30833
|
case "historyChords":
|
|
@@ -30674,8 +31406,8 @@ var NativeRenderer = class {
|
|
|
30674
31406
|
for (const pane of this.scene.panes.values()) pane.scale = { ...pane.scaleTarget };
|
|
30675
31407
|
this.modelAlpha = 0;
|
|
30676
31408
|
const start = performance.now();
|
|
30677
|
-
const step = (
|
|
30678
|
-
const p = Math.min(1, (
|
|
31409
|
+
const step = (now2) => {
|
|
31410
|
+
const p = Math.min(1, (now2 - start) / duration);
|
|
30679
31411
|
this.scene.bars = p >= 1 ? real : real.map((b, i) => this.revealCandle(b, i, p, n, style));
|
|
30680
31412
|
this.paintData();
|
|
30681
31413
|
if (p < 1) {
|
|
@@ -30691,8 +31423,8 @@ var NativeRenderer = class {
|
|
|
30691
31423
|
fadeInModels() {
|
|
30692
31424
|
const fade = Math.min(INTRO_MODEL_FADE_MS, this.intro.duration || INTRO_MODEL_FADE_MS);
|
|
30693
31425
|
const start = performance.now();
|
|
30694
|
-
const step = (
|
|
30695
|
-
this.modelAlpha = Math.min(1, (
|
|
31426
|
+
const step = (now2) => {
|
|
31427
|
+
this.modelAlpha = Math.min(1, (now2 - start) / fade);
|
|
30696
31428
|
this.paintData();
|
|
30697
31429
|
if (this.modelAlpha < 1) {
|
|
30698
31430
|
this.introRaf = requestAnimationFrame(step);
|
|
@@ -31215,10 +31947,13 @@ var NativeRenderer = class {
|
|
|
31215
31947
|
this.animator.start();
|
|
31216
31948
|
}
|
|
31217
31949
|
} else if (!last || bar.time > last.time) {
|
|
31950
|
+
const vp = this.coords.getViewport();
|
|
31951
|
+
const holdView = n > 0 && vp.rightOffset < 0 && this.scrollTargetRO === null;
|
|
31218
31952
|
this.bars.push(bar);
|
|
31219
31953
|
this.syncLiveEase(bar);
|
|
31220
31954
|
if (this.coords.barInterval > 0) this.coords.appendBar(bar.time);
|
|
31221
31955
|
else this.coords.setBars(this.bars.map((b) => b.time));
|
|
31956
|
+
if (holdView) this.coords.setViewport({ ...vp, rightOffset: vp.rightOffset - 1 });
|
|
31222
31957
|
} else {
|
|
31223
31958
|
return;
|
|
31224
31959
|
}
|
|
@@ -31900,10 +32635,10 @@ var NativeRenderer = class {
|
|
|
31900
32635
|
if (this.markPulseRaf !== null || typeof requestAnimationFrame !== "function") return;
|
|
31901
32636
|
const tick = () => {
|
|
31902
32637
|
this.markPulseRaf = null;
|
|
31903
|
-
const
|
|
31904
|
-
if (this.scene.marksFlash && this.scene.marksFlash.until <=
|
|
32638
|
+
const now2 = frameNow();
|
|
32639
|
+
if (this.scene.marksFlash && this.scene.marksFlash.until <= now2) this.scene.marksFlash = null;
|
|
31905
32640
|
this.scheduler?.invalidate(2 /* Chrome */);
|
|
31906
|
-
const pulsing = this.scene.marksHoverKey !== null &&
|
|
32641
|
+
const pulsing = this.scene.marksHoverKey !== null && now2 - this.scene.marksHoverSince < MARK_PULSE_MS;
|
|
31907
32642
|
if (pulsing || this.scene.marksFlash !== null) this.markPulseRaf = requestAnimationFrame(tick);
|
|
31908
32643
|
};
|
|
31909
32644
|
this.markPulseRaf = requestAnimationFrame(tick);
|
|
@@ -32464,9 +33199,10 @@ var NativeRenderer = class {
|
|
|
32464
33199
|
const ext = this.externalCross;
|
|
32465
33200
|
if (!ext || this.coords.barCount === 0) return null;
|
|
32466
33201
|
const logical = Math.floor(this.coords.timeToLogical(ext.time));
|
|
32467
|
-
if (logical <
|
|
32468
|
-
const x = this.coords.logicalToX(logical);
|
|
32469
|
-
if (!Number.isFinite(x)
|
|
33202
|
+
if (!(logical < this.coords.barCount)) return null;
|
|
33203
|
+
const x = this.coords.logicalToX(Math.max(logical, -1));
|
|
33204
|
+
if (!Number.isFinite(x)) return null;
|
|
33205
|
+
const line = logical >= 0 && x >= 0 && x <= this.coords.width;
|
|
32470
33206
|
let y = null;
|
|
32471
33207
|
if (ext.price != null) {
|
|
32472
33208
|
const pricePane = this.scene.orderedPanes().find((p) => p.kind === "price");
|
|
@@ -32475,7 +33211,7 @@ var NativeRenderer = class {
|
|
|
32475
33211
|
if (!Number.isFinite(y) || y < pricePane.bounds.top || y > pricePane.bounds.top + pricePane.bounds.height) y = null;
|
|
32476
33212
|
}
|
|
32477
33213
|
}
|
|
32478
|
-
return { x, y, time: this.coords.logicalToTime(logical), price: y != null ? ext.price : null };
|
|
33214
|
+
return { x, y, time: this.coords.logicalToTime(Math.max(logical, 0)), price: y != null ? ext.price : null, line };
|
|
32479
33215
|
}
|
|
32480
33216
|
/** Set the sticky magnet mode (called by the drawings toolbar's 3-state button). */
|
|
32481
33217
|
setSnapMode(mode) {
|
|
@@ -37180,6 +37916,7 @@ var Vela = class {
|
|
|
37180
37916
|
this.panesControl = new PanesControl(this.orchestrator);
|
|
37181
37917
|
this.drawingsControl = new DrawingsControl(this.orchestrator.drawings);
|
|
37182
37918
|
this.marksControl = new MarksControl(this.orchestrator.marks);
|
|
37919
|
+
this.replayControl = new ReplayControl(this.orchestrator);
|
|
37183
37920
|
}
|
|
37184
37921
|
/**
|
|
37185
37922
|
* Register a scripting engine so `addIndicator({ language })` can run that
|
|
@@ -37419,6 +38156,16 @@ var Vela = class {
|
|
|
37419
38156
|
get marks() {
|
|
37420
38157
|
return this.marksControl;
|
|
37421
38158
|
}
|
|
38159
|
+
/**
|
|
38160
|
+
* The chart's bar-replay control surface: rewind to a past bar and reveal the
|
|
38161
|
+
* following ones by hand or on a timer, from the history already loaded —
|
|
38162
|
+
* `await chart.replay.start({ from })`, `chart.replay.play(2000)`, `chart.replay.step()`,
|
|
38163
|
+
* `chart.replay.stop()`. Revealed bars behave like live bars for indicators; live
|
|
38164
|
+
* updates pause meanwhile. Follow it with the `replay:*` events.
|
|
38165
|
+
*/
|
|
38166
|
+
get replay() {
|
|
38167
|
+
return this.replayControl;
|
|
38168
|
+
}
|
|
37422
38169
|
on(event, handler) {
|
|
37423
38170
|
return this.orchestrator.events.on(event, handler);
|
|
37424
38171
|
}
|
|
@@ -37477,6 +38224,28 @@ function resolveElement(container) {
|
|
|
37477
38224
|
return element;
|
|
37478
38225
|
}
|
|
37479
38226
|
|
|
38227
|
+
// src/core/replayTicks.ts
|
|
38228
|
+
function lowerTimeframeTicks(chart, timeframe) {
|
|
38229
|
+
return async (bar, { end, signal }) => {
|
|
38230
|
+
const { symbol, session } = chart.market;
|
|
38231
|
+
const resolved = symbol ? chart.data.resolve(symbol) : null;
|
|
38232
|
+
const provider = resolved ? chart.data.providerInstance(resolved.provider) : void 0;
|
|
38233
|
+
if (!resolved || !provider) return [];
|
|
38234
|
+
const bars = await provider.getBars(resolved.ticker, timeframe, { from: bar.time, to: end - 1, ...session ? { session } : {} });
|
|
38235
|
+
if (signal.aborted) return [];
|
|
38236
|
+
return barsToTicks(bars.filter((b) => b.time >= bar.time && b.time < end));
|
|
38237
|
+
};
|
|
38238
|
+
}
|
|
38239
|
+
function barsToTicks(bars) {
|
|
38240
|
+
return bars.map((b) => ({
|
|
38241
|
+
price: b.close,
|
|
38242
|
+
open: b.open,
|
|
38243
|
+
high: b.high,
|
|
38244
|
+
low: b.low,
|
|
38245
|
+
...b.volume === void 0 ? {} : { volume: b.volume }
|
|
38246
|
+
}));
|
|
38247
|
+
}
|
|
38248
|
+
|
|
37480
38249
|
// src/widget/topbar-composition.ts
|
|
37481
38250
|
var TOPBAR_BUILTIN_IDS = ["symbol", "timeframes", "style", "layout", "indicators", "actions", "undo-redo", "alerts", "panels", "screenshot"];
|
|
37482
38251
|
|
|
@@ -37637,6 +38406,7 @@ exports.NEUTRAL = NEUTRAL;
|
|
|
37637
38406
|
exports.NativeRenderer = NativeRenderer;
|
|
37638
38407
|
exports.OVERRIDABLE_TOPBAR_IDS = OVERRIDABLE_TOPBAR_IDS;
|
|
37639
38408
|
exports.RendererControl = RendererControl;
|
|
38409
|
+
exports.ReplayControl = ReplayControl;
|
|
37640
38410
|
exports.SERIES_LINE = SERIES_LINE;
|
|
37641
38411
|
exports.SESSION_OFF = SESSION_OFF;
|
|
37642
38412
|
exports.SESSION_POST = SESSION_POST;
|
|
@@ -37651,6 +38421,7 @@ exports.TypedEventBus = TypedEventBus;
|
|
|
37651
38421
|
exports.VALID = VALID;
|
|
37652
38422
|
exports.Vela = Vela;
|
|
37653
38423
|
exports.WARNING = WARNING;
|
|
38424
|
+
exports.barsToTicks = barsToTicks;
|
|
37654
38425
|
exports.buildToolbar = buildToolbar;
|
|
37655
38426
|
exports.categoricalColor = categoricalColor;
|
|
37656
38427
|
exports.chartType = chartType;
|
|
@@ -37667,6 +38438,7 @@ exports.inputDeltas = inputDeltas;
|
|
|
37667
38438
|
exports.inputVisible = inputVisible;
|
|
37668
38439
|
exports.legendActions = legendActions;
|
|
37669
38440
|
exports.legendCallouts = legendCallouts;
|
|
38441
|
+
exports.lowerTimeframeTicks = lowerTimeframeTicks;
|
|
37670
38442
|
exports.nativeIndicatorDescriptors = nativeIndicatorDescriptors;
|
|
37671
38443
|
exports.nativeIndicatorTypes = nativeIndicatorTypes;
|
|
37672
38444
|
exports.normalizeSettingsRow = normalizeSettingsRow;
|