@luxalgo/vela 0.7.8 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +28 -28
- package/dist/{DataProvider-CGa4KHRa.d.cts → DataProvider-BrlxntPN.d.cts} +1 -1
- package/dist/{DataProvider-CmWUtDVL.d.ts → DataProvider-DwFIoV4r.d.ts} +1 -1
- package/dist/{chunk-WTWGEB7K.js → chunk-2IAETM2I.js} +260 -2
- package/dist/{chunk-CAFCLMPF.js → chunk-BZQM2XO7.js} +1 -0
- package/dist/{chunk-M6GP7DAX.js → chunk-E3GU6AUI.js} +459 -37
- package/dist/{chunk-BFA32GOU.js → chunk-EZ5FWVLA.js} +5 -2
- package/dist/{chunk-PVGWNF7V.js → chunk-KA77UTRF.js} +2 -2
- package/dist/{chunk-N4ZZYLMP.js → chunk-QBPUT5SU.js} +686 -251
- package/dist/{contributions-DF_Ea07m.d.cts → contributions-CtlptxR8.d.cts} +187 -5
- package/dist/{contributions-nKOSeLy8.d.ts → contributions-tjNExo1o.d.ts} +187 -5
- package/dist/index.cjs +966 -247
- package/dist/index.d.cts +24 -8
- package/dist/index.d.ts +24 -8
- package/dist/index.js +28 -4
- package/dist/{options-D1AJKsn_.d.ts → options-BnEPUuVV.d.cts} +1 -1
- package/dist/{options-D1AJKsn_.d.cts → options-BnEPUuVV.d.ts} +1 -1
- package/dist/{plugin-6F04gjQe.d.cts → plugin-BWxJRKzS.d.cts} +10 -4
- package/dist/{plugin-BLsNipkc.d.ts → plugin-e2WUEtiI.d.ts} +10 -4
- package/dist/plugin.cjs +1 -0
- package/dist/plugin.d.cts +4 -4
- package/dist/plugin.d.ts +4 -4
- package/dist/plugin.js +2 -2
- package/dist/providers/binance.d.cts +2 -2
- package/dist/providers/binance.d.ts +2 -2
- package/dist/providers/coinbase.d.cts +2 -2
- package/dist/providers/coinbase.d.ts +2 -2
- package/dist/providers/hyperliquid.d.cts +2 -2
- package/dist/providers/hyperliquid.d.ts +2 -2
- package/dist/{statusline-model-DAFyqVmR.d.ts → statusline-model-DFY7tuHj.d.cts} +5 -3
- package/dist/{statusline-model-BoWJ3cWQ.d.cts → statusline-model-zKXtGIjj.d.ts} +5 -3
- package/dist/ui.cjs +263 -0
- package/dist/ui.d.cts +75 -2
- package/dist/ui.d.ts +75 -2
- package/dist/ui.js +3 -3
- package/dist/vela.global.js +968 -249
- package/dist/vela.global.min.js +73 -71
- package/dist/widget.cjs +1411 -292
- package/dist/widget.d.cts +26 -7
- package/dist/widget.d.ts +26 -7
- package/dist/widget.js +7 -7
- package/dist/workspace.cjs +1414 -292
- package/dist/workspace.d.cts +124 -6
- package/dist/workspace.d.ts +124 -6
- package/dist/workspace.js +6 -6
- package/package.json +1 -1
package/dist/workspace.cjs
CHANGED
|
@@ -1230,6 +1230,7 @@ registerIcon("chevron-down", S('<path d="M3.5 6 8 10.5 12.5 6"/>'));
|
|
|
1230
1230
|
registerIcon("chevron-up", S('<path d="M3.5 10 8 5.5 12.5 10"/>'));
|
|
1231
1231
|
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"/>'));
|
|
1232
1232
|
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"/>'));
|
|
1233
|
+
registerIcon("replay", S('<path d="M14 4.2v7.6L8.6 8z"/><path d="M7.4 4.2v7.6L2 8z"/>'));
|
|
1233
1234
|
registerIcon("check", S('<path d="m2.8 8.4 3.4 3.4 7-7.6"/>'));
|
|
1234
1235
|
registerIcon("close", S('<path d="m3.8 3.8 8.4 8.4M12.2 3.8l-8.4 8.4"/>'));
|
|
1235
1236
|
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"/>'));
|
|
@@ -4550,6 +4551,264 @@ var CalloutBubble = class {
|
|
|
4550
4551
|
}
|
|
4551
4552
|
};
|
|
4552
4553
|
|
|
4554
|
+
// src/ui/components/date-picker/controller.ts
|
|
4555
|
+
var MONTH_LABELS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
|
|
4556
|
+
var MONTH_SHORT = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
4557
|
+
var WEEKDAY_LABELS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
|
|
4558
|
+
function isoDate(d) {
|
|
4559
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
4560
|
+
}
|
|
4561
|
+
function normalizeDateInput(raw) {
|
|
4562
|
+
const m = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(raw.trim());
|
|
4563
|
+
if (!m) return null;
|
|
4564
|
+
const y = Number(m[1]);
|
|
4565
|
+
const mo = Number(m[2]);
|
|
4566
|
+
const d = Number(m[3]);
|
|
4567
|
+
const dt = new Date(y, mo - 1, d);
|
|
4568
|
+
if (dt.getFullYear() !== y || dt.getMonth() !== mo - 1 || dt.getDate() !== d) return null;
|
|
4569
|
+
return isoDate(dt);
|
|
4570
|
+
}
|
|
4571
|
+
function parseIsoDate(raw) {
|
|
4572
|
+
const iso = raw ? normalizeDateInput(raw) : null;
|
|
4573
|
+
if (!iso) return null;
|
|
4574
|
+
const [y, mo, d] = iso.split("-").map(Number);
|
|
4575
|
+
return new Date(y, mo - 1, d);
|
|
4576
|
+
}
|
|
4577
|
+
function datePickerController(opts = {}) {
|
|
4578
|
+
const today = opts.today ?? /* @__PURE__ */ new Date();
|
|
4579
|
+
let selected = parseIsoDate(opts.value);
|
|
4580
|
+
let year = selected?.getFullYear() ?? today.getFullYear();
|
|
4581
|
+
let month = selected?.getMonth() ?? today.getMonth();
|
|
4582
|
+
let panel = "date";
|
|
4583
|
+
const decade = () => Math.floor(year / 10) * 10;
|
|
4584
|
+
return {
|
|
4585
|
+
get panel() {
|
|
4586
|
+
return panel;
|
|
4587
|
+
},
|
|
4588
|
+
get year() {
|
|
4589
|
+
return year;
|
|
4590
|
+
},
|
|
4591
|
+
get month() {
|
|
4592
|
+
return month;
|
|
4593
|
+
},
|
|
4594
|
+
get value() {
|
|
4595
|
+
return selected ? isoDate(selected) : null;
|
|
4596
|
+
},
|
|
4597
|
+
header() {
|
|
4598
|
+
return {
|
|
4599
|
+
month: MONTH_LABELS[month] ?? "",
|
|
4600
|
+
monthVisible: panel === "date",
|
|
4601
|
+
year: panel === "year" ? `${decade()}-${decade() + 9}` : String(year),
|
|
4602
|
+
yearDisabled: panel === "year"
|
|
4603
|
+
// in the decade panel the year reads as its title
|
|
4604
|
+
};
|
|
4605
|
+
},
|
|
4606
|
+
navLabels() {
|
|
4607
|
+
if (panel === "date") return { prev: "Previous month", next: "Next month" };
|
|
4608
|
+
if (panel === "month") return { prev: "Previous year", next: "Next year" };
|
|
4609
|
+
return { prev: "Previous decade", next: "Next decade" };
|
|
4610
|
+
},
|
|
4611
|
+
cells() {
|
|
4612
|
+
const out = [];
|
|
4613
|
+
if (panel === "date") {
|
|
4614
|
+
const startPad = new Date(year, month, 1).getDay();
|
|
4615
|
+
const days = new Date(year, month + 1, 0).getDate();
|
|
4616
|
+
const todayIso = isoDate(today);
|
|
4617
|
+
const selectedIso = selected ? isoDate(selected) : "";
|
|
4618
|
+
for (let i = 0; i < startPad; i++) out.push({ label: "", value: "", checked: false, today: false, blank: true });
|
|
4619
|
+
for (let day = 1; day <= days; day++) {
|
|
4620
|
+
const iso = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
|
4621
|
+
out.push({ label: String(day), value: iso, checked: iso === selectedIso, today: iso === todayIso });
|
|
4622
|
+
}
|
|
4623
|
+
return out;
|
|
4624
|
+
}
|
|
4625
|
+
if (panel === "month") {
|
|
4626
|
+
for (let m = 0; m < 12; m++) {
|
|
4627
|
+
out.push({
|
|
4628
|
+
label: MONTH_SHORT[m] ?? "",
|
|
4629
|
+
value: m,
|
|
4630
|
+
checked: selected?.getFullYear() === year && selected.getMonth() === m,
|
|
4631
|
+
today: today.getFullYear() === year && today.getMonth() === m
|
|
4632
|
+
});
|
|
4633
|
+
}
|
|
4634
|
+
return out;
|
|
4635
|
+
}
|
|
4636
|
+
const d = decade();
|
|
4637
|
+
for (let y = d - 1; y <= d + 10; y++) {
|
|
4638
|
+
out.push({ label: String(y), value: y, checked: selected?.getFullYear() === y, today: today.getFullYear() === y, outside: y < d || y > d + 9 });
|
|
4639
|
+
}
|
|
4640
|
+
return out;
|
|
4641
|
+
},
|
|
4642
|
+
step(dir) {
|
|
4643
|
+
if (panel === "date") {
|
|
4644
|
+
month += dir;
|
|
4645
|
+
if (month < 0) {
|
|
4646
|
+
month = 11;
|
|
4647
|
+
year -= 1;
|
|
4648
|
+
}
|
|
4649
|
+
if (month > 11) {
|
|
4650
|
+
month = 0;
|
|
4651
|
+
year += 1;
|
|
4652
|
+
}
|
|
4653
|
+
} else {
|
|
4654
|
+
year += panel === "month" ? dir : dir * 10;
|
|
4655
|
+
}
|
|
4656
|
+
},
|
|
4657
|
+
showMonths() {
|
|
4658
|
+
panel = "month";
|
|
4659
|
+
},
|
|
4660
|
+
showYears() {
|
|
4661
|
+
panel = "year";
|
|
4662
|
+
},
|
|
4663
|
+
choose(cell) {
|
|
4664
|
+
if (cell.blank) return null;
|
|
4665
|
+
if (panel === "date") {
|
|
4666
|
+
selected = parseIsoDate(String(cell.value));
|
|
4667
|
+
return selected ? isoDate(selected) : null;
|
|
4668
|
+
}
|
|
4669
|
+
if (panel === "month") {
|
|
4670
|
+
month = Number(cell.value);
|
|
4671
|
+
panel = "date";
|
|
4672
|
+
return null;
|
|
4673
|
+
}
|
|
4674
|
+
year = Number(cell.value);
|
|
4675
|
+
panel = "month";
|
|
4676
|
+
return null;
|
|
4677
|
+
},
|
|
4678
|
+
setValue(value) {
|
|
4679
|
+
selected = parseIsoDate(value);
|
|
4680
|
+
if (selected) {
|
|
4681
|
+
year = selected.getFullYear();
|
|
4682
|
+
month = selected.getMonth();
|
|
4683
|
+
}
|
|
4684
|
+
panel = "date";
|
|
4685
|
+
}
|
|
4686
|
+
};
|
|
4687
|
+
}
|
|
4688
|
+
|
|
4689
|
+
// src/ui/components/date-picker/styles.ts
|
|
4690
|
+
var DATE_PICKER_STYLE_ID = "vela-date-picker-styles";
|
|
4691
|
+
var DATE_PICKER_CSS = `
|
|
4692
|
+
.vela-date-picker{color:var(--vela-fg);font:14px var(--vela-font);user-select:none;}
|
|
4693
|
+
.vela-date-picker [hidden]{display:none !important;}
|
|
4694
|
+
.vela-date-picker-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;}
|
|
4695
|
+
.vela-date-picker-title{flex:1;display:flex;align-items:center;justify-content:center;gap:2px;min-width:0;}
|
|
4696
|
+
.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;}
|
|
4697
|
+
.vela-date-picker-switch:not(:disabled):hover{background:var(--vela-hover);}
|
|
4698
|
+
.vela-date-picker-switch:disabled{cursor:default;}
|
|
4699
|
+
.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;}
|
|
4700
|
+
.vela-date-picker-nav:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
|
|
4701
|
+
.vela-date-picker-week,.vela-date-picker-grid{display:grid;grid-template-columns:repeat(7,28px);gap:2px;}
|
|
4702
|
+
.vela-date-picker-week{margin-bottom:4px;color:var(--vela-fg-muted);font-size:11px;text-align:center;}
|
|
4703
|
+
.vela-date-picker-week span{line-height:20px;}
|
|
4704
|
+
.vela-date-picker-blank{width:28px;height:28px;}
|
|
4705
|
+
.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;}
|
|
4706
|
+
.vela-date-picker-day:hover{background:var(--vela-hover);}
|
|
4707
|
+
.vela-date-picker-day[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
|
|
4708
|
+
.vela-date-picker-day[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
|
|
4709
|
+
.vela-date-picker-cells{display:grid;grid-template-columns:repeat(3,1fr);gap:4px;width:calc(7 * 28px + 6 * 2px);}
|
|
4710
|
+
.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;}
|
|
4711
|
+
.vela-date-picker-cell:hover{background:var(--vela-hover);}
|
|
4712
|
+
.vela-date-picker-cell[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
|
|
4713
|
+
.vela-date-picker-cell[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
|
|
4714
|
+
.vela-date-picker-cell[data-outside]{opacity:0.45;}
|
|
4715
|
+
`;
|
|
4716
|
+
|
|
4717
|
+
// src/ui/components/date-picker/view.ts
|
|
4718
|
+
var DatePicker = class {
|
|
4719
|
+
constructor(opts = {}) {
|
|
4720
|
+
this.opts = opts;
|
|
4721
|
+
const doc = document;
|
|
4722
|
+
injectStyles(DATE_PICKER_STYLE_ID, DATE_PICKER_CSS, doc);
|
|
4723
|
+
this.ctrl = datePickerController(opts);
|
|
4724
|
+
const button = (className) => {
|
|
4725
|
+
const b = doc.createElement("button");
|
|
4726
|
+
b.type = "button";
|
|
4727
|
+
b.className = className;
|
|
4728
|
+
return b;
|
|
4729
|
+
};
|
|
4730
|
+
this.prev = button("vela-date-picker-nav");
|
|
4731
|
+
this.prev.append(iconEl("chevron-left", doc));
|
|
4732
|
+
this.next = button("vela-date-picker-nav");
|
|
4733
|
+
this.next.append(iconEl("chevron-right", doc));
|
|
4734
|
+
this.monthBtn = button("vela-date-picker-switch");
|
|
4735
|
+
this.monthBtn.setAttribute("aria-label", "Choose month");
|
|
4736
|
+
this.yearBtn = button("vela-date-picker-switch");
|
|
4737
|
+
this.yearBtn.setAttribute("aria-label", "Choose year");
|
|
4738
|
+
const title = doc.createElement("div");
|
|
4739
|
+
title.className = "vela-date-picker-title";
|
|
4740
|
+
title.append(this.monthBtn, this.yearBtn);
|
|
4741
|
+
const head = doc.createElement("div");
|
|
4742
|
+
head.className = "vela-date-picker-head";
|
|
4743
|
+
head.append(this.prev, title, this.next);
|
|
4744
|
+
this.week = doc.createElement("div");
|
|
4745
|
+
this.week.className = "vela-date-picker-week";
|
|
4746
|
+
for (const d of WEEKDAY_LABELS) {
|
|
4747
|
+
const cell = doc.createElement("span");
|
|
4748
|
+
cell.textContent = d;
|
|
4749
|
+
this.week.appendChild(cell);
|
|
4750
|
+
}
|
|
4751
|
+
this.grid = doc.createElement("div");
|
|
4752
|
+
const on = (el, run) => el.addEventListener("click", (e) => {
|
|
4753
|
+
e.stopPropagation();
|
|
4754
|
+
run();
|
|
4755
|
+
this.paint();
|
|
4756
|
+
});
|
|
4757
|
+
on(this.prev, () => this.ctrl.step(-1));
|
|
4758
|
+
on(this.next, () => this.ctrl.step(1));
|
|
4759
|
+
on(this.monthBtn, () => this.ctrl.showMonths());
|
|
4760
|
+
on(this.yearBtn, () => this.ctrl.showYears());
|
|
4761
|
+
this.el = doc.createElement("div");
|
|
4762
|
+
this.el.className = "vela-date-picker";
|
|
4763
|
+
this.el.append(head, this.week, this.grid);
|
|
4764
|
+
this.paint();
|
|
4765
|
+
}
|
|
4766
|
+
get value() {
|
|
4767
|
+
return this.ctrl.value;
|
|
4768
|
+
}
|
|
4769
|
+
/** Rewrite the selection without emitting (the calendar opens on its month). */
|
|
4770
|
+
setValue(value) {
|
|
4771
|
+
this.ctrl.setValue(value);
|
|
4772
|
+
this.paint();
|
|
4773
|
+
}
|
|
4774
|
+
paint() {
|
|
4775
|
+
const doc = this.el.ownerDocument;
|
|
4776
|
+
const h = this.ctrl.header();
|
|
4777
|
+
this.monthBtn.hidden = !h.monthVisible;
|
|
4778
|
+
this.monthBtn.textContent = h.month;
|
|
4779
|
+
this.yearBtn.textContent = h.year;
|
|
4780
|
+
this.yearBtn.disabled = h.yearDisabled;
|
|
4781
|
+
const nav = this.ctrl.navLabels();
|
|
4782
|
+
this.prev.setAttribute("aria-label", nav.prev);
|
|
4783
|
+
this.next.setAttribute("aria-label", nav.next);
|
|
4784
|
+
const dates = this.ctrl.panel === "date";
|
|
4785
|
+
this.week.hidden = !dates;
|
|
4786
|
+
this.grid.className = dates ? "vela-date-picker-grid" : "vela-date-picker-cells";
|
|
4787
|
+
this.grid.replaceChildren(...this.ctrl.cells().map((cell) => this.cellEl(doc, cell, dates)));
|
|
4788
|
+
}
|
|
4789
|
+
cellEl(doc, cell, dates) {
|
|
4790
|
+
if (cell.blank) {
|
|
4791
|
+
const blank = doc.createElement("span");
|
|
4792
|
+
blank.className = "vela-date-picker-blank";
|
|
4793
|
+
return blank;
|
|
4794
|
+
}
|
|
4795
|
+
const b = doc.createElement("button");
|
|
4796
|
+
b.type = "button";
|
|
4797
|
+
b.className = dates ? "vela-date-picker-day" : "vela-date-picker-cell";
|
|
4798
|
+
b.textContent = cell.label;
|
|
4799
|
+
if (cell.checked) b.dataset.checked = "1";
|
|
4800
|
+
if (cell.today) b.dataset.today = "1";
|
|
4801
|
+
if (cell.outside) b.dataset.outside = "1";
|
|
4802
|
+
b.addEventListener("click", (e) => {
|
|
4803
|
+
e.stopPropagation();
|
|
4804
|
+
const picked = this.ctrl.choose(cell);
|
|
4805
|
+
if (picked) this.opts.onPick?.(picked);
|
|
4806
|
+
else this.paint();
|
|
4807
|
+
});
|
|
4808
|
+
return b;
|
|
4809
|
+
}
|
|
4810
|
+
};
|
|
4811
|
+
|
|
4553
4812
|
// src/widget/layout-picker.ts
|
|
4554
4813
|
var STYLE_ID = "vela-widget-layout-picker-v14";
|
|
4555
4814
|
var CSS2 = `
|
|
@@ -4963,6 +5222,9 @@ function topbarHas(comp, id) {
|
|
|
4963
5222
|
}
|
|
4964
5223
|
|
|
4965
5224
|
// src/widget/contributions.ts
|
|
5225
|
+
function mobilePlacement(action) {
|
|
5226
|
+
return action.mobile ?? (action.align === "left" ? "bar" : "menu");
|
|
5227
|
+
}
|
|
4966
5228
|
var registry3 = /* @__PURE__ */ new Map();
|
|
4967
5229
|
var attachments = /* @__PURE__ */ new Map();
|
|
4968
5230
|
var panels = /* @__PURE__ */ new Map();
|
|
@@ -5150,6 +5412,22 @@ function priceStyleIds() {
|
|
|
5150
5412
|
for (const t of chartTypes()) if (!out.includes(t.id)) out.push(t.id);
|
|
5151
5413
|
return out;
|
|
5152
5414
|
}
|
|
5415
|
+
function sanitizeCrosshairOverride(value) {
|
|
5416
|
+
if (!value || typeof value !== "object") return null;
|
|
5417
|
+
const v = value;
|
|
5418
|
+
const out = {};
|
|
5419
|
+
if (isBool(v.vertical)) out.vertical = v.vertical;
|
|
5420
|
+
if (isBool(v.horizontal)) out.horizontal = v.horizontal;
|
|
5421
|
+
if (isColor(v.color)) out.color = v.color;
|
|
5422
|
+
if (isNum(v.width)) out.width = Math.max(0.5, Math.min(8, v.width));
|
|
5423
|
+
if (isLineStyle(v.style)) out.style = v.style;
|
|
5424
|
+
if (isNum(v.opacity)) out.opacity = clampOpacity(v.opacity);
|
|
5425
|
+
const shade = v.shadeRight;
|
|
5426
|
+
if (shade && typeof shade === "object" && isColor(shade.color)) {
|
|
5427
|
+
out.shadeRight = isNum(shade.opacity) ? { color: shade.color, opacity: clampOpacity(shade.opacity) } : { color: shade.color };
|
|
5428
|
+
}
|
|
5429
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
5430
|
+
}
|
|
5153
5431
|
function isColor(v) {
|
|
5154
5432
|
return typeof v === "string" && v.trim().length > 0;
|
|
5155
5433
|
}
|
|
@@ -6037,8 +6315,8 @@ var Topbar = class {
|
|
|
6037
6315
|
// src/core/util/wall-clock.ts
|
|
6038
6316
|
var BOUNDARY_SLACK_MS = 5;
|
|
6039
6317
|
var SecondClock = class _SecondClock {
|
|
6040
|
-
constructor(
|
|
6041
|
-
this.now =
|
|
6318
|
+
constructor(now2 = () => Date.now()) {
|
|
6319
|
+
this.now = now2;
|
|
6042
6320
|
this.subs = /* @__PURE__ */ new Set();
|
|
6043
6321
|
this.timer = null;
|
|
6044
6322
|
}
|
|
@@ -6054,15 +6332,15 @@ var SecondClock = class _SecondClock {
|
|
|
6054
6332
|
};
|
|
6055
6333
|
}
|
|
6056
6334
|
/** Milliseconds from `now` to just past the next second boundary. */
|
|
6057
|
-
static delayToNextSecond(
|
|
6058
|
-
const intoSecond = (
|
|
6335
|
+
static delayToNextSecond(now2) {
|
|
6336
|
+
const intoSecond = (now2 % 1e3 + 1e3) % 1e3;
|
|
6059
6337
|
return 1e3 - intoSecond + BOUNDARY_SLACK_MS;
|
|
6060
6338
|
}
|
|
6061
6339
|
arm() {
|
|
6062
6340
|
this.timer = setTimeout(() => {
|
|
6063
6341
|
this.timer = null;
|
|
6064
|
-
const
|
|
6065
|
-
for (const cb of this.subs) cb(
|
|
6342
|
+
const now2 = this.now();
|
|
6343
|
+
for (const cb of this.subs) cb(now2);
|
|
6066
6344
|
if (this.subs.size > 0) this.arm();
|
|
6067
6345
|
}, _SecondClock.delayToNextSecond(this.now()));
|
|
6068
6346
|
}
|
|
@@ -15288,16 +15566,16 @@ var MobileBar = class {
|
|
|
15288
15566
|
host.appendChild(this.el);
|
|
15289
15567
|
this.renderActions();
|
|
15290
15568
|
}
|
|
15291
|
-
/** Re-project the
|
|
15292
|
-
* indicators slot (call after
|
|
15293
|
-
*
|
|
15569
|
+
/** Re-project the contributed actions placed on the bar (left-aligned by default —
|
|
15570
|
+
* see `mobilePlacement`) as icon-only stops in the indicators slot (call after
|
|
15571
|
+
* registrations change). The rest are three-dots drawer rows. */
|
|
15294
15572
|
renderActions() {
|
|
15295
15573
|
const ctx = this.opts.getContext?.();
|
|
15296
15574
|
if (!ctx) return;
|
|
15297
15575
|
const doc = this.el.ownerDocument;
|
|
15298
15576
|
this.actionsHost.replaceChildren();
|
|
15299
15577
|
const builtin = new Set(TOPBAR_BUILTIN_IDS);
|
|
15300
|
-
for (const action of widgetActions("topbar", ctx).filter((a) => a
|
|
15578
|
+
for (const action of widgetActions("topbar", ctx).filter((a) => mobilePlacement(a) === "bar" && !builtin.has(a.id))) {
|
|
15301
15579
|
const b = doc.createElement("button");
|
|
15302
15580
|
b.className = "vela-mb-item";
|
|
15303
15581
|
b.setAttribute("aria-label", action.label);
|
|
@@ -15853,6 +16131,7 @@ var MoreDrawer = class {
|
|
|
15853
16131
|
const value = shape ? `${shape.cols} \xD7 ${shape.rows}` : this.opts.layout.presets().find((p) => p.checked)?.label;
|
|
15854
16132
|
list.appendChild(this.row(doc, "Layout", { icon: "layout", value, chevron: true, onClick: () => this.show("layout") }));
|
|
15855
16133
|
}
|
|
16134
|
+
for (const act of this.opts.primaryActions?.() ?? []) list.appendChild(this.actionRow(doc, act));
|
|
15856
16135
|
for (const panel of this.opts.panels()) {
|
|
15857
16136
|
list.appendChild(
|
|
15858
16137
|
this.row(doc, panel.title, {
|
|
@@ -15868,19 +16147,19 @@ var MoreDrawer = class {
|
|
|
15868
16147
|
const alertCount = this.opts.alerts().length;
|
|
15869
16148
|
list.appendChild(this.row(doc, "Alerts", { icon: "bell", value: alertCount > 0 ? String(alertCount) : void 0, chevron: true, onClick: () => this.show("alerts") }));
|
|
15870
16149
|
}
|
|
15871
|
-
for (const act of this.opts.actions())
|
|
15872
|
-
list.appendChild(
|
|
15873
|
-
this.row(doc, act.label, {
|
|
15874
|
-
icon: act.icon,
|
|
15875
|
-
onClick: () => {
|
|
15876
|
-
act.run();
|
|
15877
|
-
this.drawer.hide();
|
|
15878
|
-
}
|
|
15879
|
-
})
|
|
15880
|
-
);
|
|
15881
|
-
}
|
|
16150
|
+
for (const act of this.opts.actions()) list.appendChild(this.actionRow(doc, act));
|
|
15882
16151
|
this.drawer.body.appendChild(list);
|
|
15883
16152
|
}
|
|
16153
|
+
/** A contributed action's row: runs it and closes the drawer. */
|
|
16154
|
+
actionRow(doc, act) {
|
|
16155
|
+
return this.row(doc, act.label, {
|
|
16156
|
+
icon: act.icon,
|
|
16157
|
+
onClick: () => {
|
|
16158
|
+
act.run();
|
|
16159
|
+
this.drawer.hide();
|
|
16160
|
+
}
|
|
16161
|
+
});
|
|
16162
|
+
}
|
|
15884
16163
|
renderStyle(doc) {
|
|
15885
16164
|
const list = doc.createElement("div");
|
|
15886
16165
|
list.className = "vela-md-list";
|
|
@@ -18254,6 +18533,7 @@ function sanitizeCell(raw) {
|
|
|
18254
18533
|
if (typeof c.bars === "number" && Number.isFinite(c.bars) && c.bars > 0) out.bars = c.bars;
|
|
18255
18534
|
if (c.session === "regular" || c.session === "extended") out.session = c.session;
|
|
18256
18535
|
if (typeof c.watermark === "boolean") out.watermark = c.watermark;
|
|
18536
|
+
if (typeof c.replayWatermark === "boolean") out.replayWatermark = c.replayWatermark;
|
|
18257
18537
|
if (typeof c.indicatorTitles === "boolean") out.indicatorTitles = c.indicatorTitles;
|
|
18258
18538
|
if (typeof c.indicatorValues === "boolean") out.indicatorValues = c.indicatorValues;
|
|
18259
18539
|
if (c.rendererConfig != null && typeof c.rendererConfig === "object") out.rendererConfig = c.rendererConfig;
|
|
@@ -19006,6 +19286,7 @@ var CHUNK_BARS = 1e4;
|
|
|
19006
19286
|
var FIRST_PAINT_BARS = 20;
|
|
19007
19287
|
var GAP_FACTOR = 1.5;
|
|
19008
19288
|
var HEAL_COOLDOWN_MS = 5e3;
|
|
19289
|
+
var MIN_TICK_UPDATE_MS = 16;
|
|
19009
19290
|
var EngineOrchestrator = class _EngineOrchestrator {
|
|
19010
19291
|
constructor(container, renderer, feed, engines, config, dataControl) {
|
|
19011
19292
|
this.renderer = renderer;
|
|
@@ -19058,6 +19339,32 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19058
19339
|
this.lastHealAt = 0;
|
|
19059
19340
|
/** The `visibilitychange` catch-up listener (removed on destroy). */
|
|
19060
19341
|
this.onVisible = null;
|
|
19342
|
+
// ── bar replay: `rawBars` holds the history up to the cursor, the rest waits here ──
|
|
19343
|
+
/** Bars still hidden right of the replay cursor, oldest first; null ≡ replay off. */
|
|
19344
|
+
this.replayQueue = null;
|
|
19345
|
+
this.replayTimer = null;
|
|
19346
|
+
this.replayPlaying = false;
|
|
19347
|
+
this.replayIntervalMs = 1e3;
|
|
19348
|
+
/** The depth (`market.bars`) before a replay deepened the history — restored when it ends. */
|
|
19349
|
+
this.replayDepth = null;
|
|
19350
|
+
/** A start is loading older history before it can cut; `seek` = a replay was running
|
|
19351
|
+
* (and left silently to deepen), so abandoning the load must announce its end. */
|
|
19352
|
+
this.replayLoading = null;
|
|
19353
|
+
/** A replay carried over a timeframe/session switch, between the old series' silent end
|
|
19354
|
+
* and the cut of the new one: still ACTIVE for the outside (`replayState`), resumed
|
|
19355
|
+
* at `revealedEnd` once the new bars land, unless stopped (`cancelled`) meanwhile. */
|
|
19356
|
+
this.replayCarrying = null;
|
|
19357
|
+
/** Intrabar prices for the bars replay reveals — null ≡ whole bars. */
|
|
19358
|
+
this.replayTickSource = null;
|
|
19359
|
+
/** The bar being revealed tick by tick: its stored form (what it settles on), its ticks,
|
|
19360
|
+
* the next tick to apply, the candle built so far, the volume each tick adds when the
|
|
19361
|
+
* ticks carry none, and its pacing (ticks per update, ms between updates — one play
|
|
19362
|
+
* interval per tick — and the clock time tick 0 is due at, so late timers catch up). */
|
|
19363
|
+
this.replayForming = null;
|
|
19364
|
+
/** Tick requests by bar time, shared by prefetch and playback; `ticks` once they landed. */
|
|
19365
|
+
this.replayTickFetches = /* @__PURE__ */ new Map();
|
|
19366
|
+
/** Aborts the tick requests in flight — renewed whenever the replay moves on. */
|
|
19367
|
+
this.replayTickAbort = null;
|
|
19061
19368
|
/** What the last applied bar did: refine the open candle, or open a new one (which
|
|
19062
19369
|
* settles the previous). Read by the `script:run` cause attribution. */
|
|
19063
19370
|
this.barCause = "tick";
|
|
@@ -19281,6 +19588,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19281
19588
|
const paint = (bars, final) => {
|
|
19282
19589
|
if (this.generation !== gen || !final && bars.length === 0) return;
|
|
19283
19590
|
if (!painted && !final && bars.length < Math.min(requested, FIRST_PAINT_BARS)) return;
|
|
19591
|
+
if (painted && this.replayQueue) {
|
|
19592
|
+
this.mergeHistoryIntoReplay(bars);
|
|
19593
|
+
return;
|
|
19594
|
+
}
|
|
19284
19595
|
this.setBarSeries(bars, painted ? { preserveView: true } : void 0);
|
|
19285
19596
|
if (!painted && bars.length > 0) {
|
|
19286
19597
|
painted = true;
|
|
@@ -19423,6 +19734,36 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19423
19734
|
// exactly like a timeframe change; the cache keys the sessions apart.
|
|
19424
19735
|
next.session !== void 0 && next.session !== m.session || next.data !== void 0;
|
|
19425
19736
|
const depthChanged = next.bars !== void 0 && next.bars !== m.bars;
|
|
19737
|
+
const sameClock = identityChanged && next.data === void 0 && (next.symbol === void 0 || next.symbol === m.symbol) && (next.timeframe !== void 0 || next.session !== void 0);
|
|
19738
|
+
const pending = identityChanged ? this.replayCarrying : null;
|
|
19739
|
+
if (pending) {
|
|
19740
|
+
this.replayCarrying = null;
|
|
19741
|
+
if (!sameClock) {
|
|
19742
|
+
pending.cancelled = true;
|
|
19743
|
+
this.events.emit("replay:end", { reason: "market" });
|
|
19744
|
+
}
|
|
19745
|
+
}
|
|
19746
|
+
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;
|
|
19747
|
+
if (pending) pending.cancelled = true;
|
|
19748
|
+
if (replayCarry) this.replayCarrying = replayCarry;
|
|
19749
|
+
if (replayCarry && this.replayQueue) {
|
|
19750
|
+
this.endReplay(null, { restore: false, resume: false });
|
|
19751
|
+
const depth = this.replayDepth;
|
|
19752
|
+
this.replayDepth = null;
|
|
19753
|
+
if (depth && next.bars === void 0) {
|
|
19754
|
+
if (depth.bars === void 0) delete m.bars;
|
|
19755
|
+
else m.bars = depth.bars;
|
|
19756
|
+
}
|
|
19757
|
+
}
|
|
19758
|
+
if (this.replayQueue && (identityChanged || depthChanged)) this.endReplay("market", { restore: !identityChanged, resume: false });
|
|
19759
|
+
if (this.replayLoading && (identityChanged || depthChanged)) {
|
|
19760
|
+
const depth = this.replayDepth;
|
|
19761
|
+
this.abandonReplayLoad("market");
|
|
19762
|
+
if (depth && next.bars === void 0) {
|
|
19763
|
+
if (depth.bars === void 0) delete m.bars;
|
|
19764
|
+
else m.bars = depth.bars;
|
|
19765
|
+
}
|
|
19766
|
+
}
|
|
19426
19767
|
const depthOnly = depthChanged && !identityChanged && this.rawBars.length > 0 && !m.data?.length && ((next.bars ?? 0) <= this.rawBars.length || typeof this.feed.loadRange === "function");
|
|
19427
19768
|
if (!identityChanged && !depthChanged) {
|
|
19428
19769
|
if (typeof next.visibleRange === "string") this.setVisibleRangePreset(next.visibleRange);
|
|
@@ -19480,15 +19821,20 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19480
19821
|
const from = bars[Math.max(0, idx - (carriedCount - 1))].time;
|
|
19481
19822
|
this.renderer.setVisibleRange({ from, to: Math.max(carried.right, bars[idx].time + 1) });
|
|
19482
19823
|
}
|
|
19483
|
-
|
|
19824
|
+
const carry = replayCarry && !replayCarry.cancelled ? replayCarry : null;
|
|
19825
|
+
const carryFrom = carry ? carry.revealedEnd - this.barIntervalMs() : 0;
|
|
19826
|
+
const carryNow = carry != null && this.rawBars.length > 0 && carryFrom >= this.rawBars[0].time;
|
|
19827
|
+
const replayBack = carryNow && this.resumeCarriedReplay(this.replayStart({ from: carryFrom }), carry);
|
|
19828
|
+
if (!depthOnly && !replayBack) {
|
|
19484
19829
|
this.restartNativeIndicators();
|
|
19485
19830
|
this.restartChartTypeEngine();
|
|
19486
19831
|
this.reexecuteIndicators();
|
|
19487
19832
|
}
|
|
19488
|
-
this.startLive();
|
|
19833
|
+
if (!replayBack) this.startLive();
|
|
19489
19834
|
if (identityChanged) {
|
|
19490
19835
|
this.events.emit("market:changed", { symbol: m.symbol ?? "TEST", timeframe: m.timeframe ?? "60", prev: prev2 });
|
|
19491
19836
|
}
|
|
19837
|
+
if (carry && !carryNow) this.resumeCarriedReplay(this.replayStart({ from: carryFrom }), carry);
|
|
19492
19838
|
}
|
|
19493
19839
|
/**
|
|
19494
19840
|
* Restart every native indicator over the new market: a `NativeIndicatorContext`
|
|
@@ -19612,6 +19958,397 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19612
19958
|
historyComplete() {
|
|
19613
19959
|
return this.historyCompletePromise;
|
|
19614
19960
|
}
|
|
19961
|
+
// ── bar replay ──────────────────────────────────────────────
|
|
19962
|
+
/**
|
|
19963
|
+
* Enter replay — or seek, when already replaying — keeping every bar opened at or before
|
|
19964
|
+
* `opts.from` and queueing the rest. A `from` inside the loaded bars cuts at once, even
|
|
19965
|
+
* while older history is still streaming in: what lands later joins the tape's head
|
|
19966
|
+
* (see {@link mergeHistoryIntoReplay}) and leaves the cut alone. An older `from` waits
|
|
19967
|
+
* for that load, then deepens if it still has to. Consumers restart over the cut exactly
|
|
19968
|
+
* as on a market switch — their models describe bars that are now hidden — and the
|
|
19969
|
+
* active chart-type engine is rebuilt without live data.
|
|
19970
|
+
*/
|
|
19971
|
+
async replayStart(opts) {
|
|
19972
|
+
if (this.rawBars.length === 0) {
|
|
19973
|
+
console.warn("[vela] chart.replay.start() ignored \u2014 the chart has no bars yet");
|
|
19974
|
+
return;
|
|
19975
|
+
}
|
|
19976
|
+
let gen = this.generation;
|
|
19977
|
+
if (opts.from < this.rawBars[0].time) {
|
|
19978
|
+
await this.historyCompletePromise;
|
|
19979
|
+
if (this.generation !== gen || this.rawBars.length === 0) return;
|
|
19980
|
+
}
|
|
19981
|
+
const oldest = this.rawBars[0].time;
|
|
19982
|
+
if (opts.from < oldest && this.canHeal() && typeof this.feed.loadRange === "function") {
|
|
19983
|
+
const held = this.rawBars.length + (this.replayQueue?.length ?? 0);
|
|
19984
|
+
const bars = held + Math.ceil((oldest - opts.from) / this.barIntervalMs()) + 1;
|
|
19985
|
+
const seek = this.replayQueue !== null;
|
|
19986
|
+
if (seek) this.endReplay(null, { restore: true, resume: false });
|
|
19987
|
+
this.replayDepth ?? (this.replayDepth = { bars: this.config.market.bars });
|
|
19988
|
+
const deepened = this.setMarket({ bars });
|
|
19989
|
+
this.replayLoading = { seek };
|
|
19990
|
+
await deepened;
|
|
19991
|
+
gen = this.generation;
|
|
19992
|
+
await this.historyCompletePromise;
|
|
19993
|
+
if (!this.replayLoading || this.generation !== gen || this.rawBars.length === 0) return;
|
|
19994
|
+
this.replayLoading = null;
|
|
19995
|
+
}
|
|
19996
|
+
this.settleFormingBar();
|
|
19997
|
+
this.dropTickFetches();
|
|
19998
|
+
const tape = this.replayQueue ? [...this.rawBars, ...this.replayQueue] : this.rawBars;
|
|
19999
|
+
let cut = 0;
|
|
20000
|
+
while (cut < tape.length && tape[cut].time <= opts.from) cut += 1;
|
|
20001
|
+
cut = Math.max(1, cut);
|
|
20002
|
+
if (cut >= tape.length) {
|
|
20003
|
+
console.warn("[vela] chart.replay.start() ignored \u2014 no bar opens after the requested time");
|
|
20004
|
+
return;
|
|
20005
|
+
}
|
|
20006
|
+
const wasPlaying = this.replayPlaying;
|
|
20007
|
+
this.clearReplayTimer();
|
|
20008
|
+
if (!this.replayQueue) this.stopLive();
|
|
20009
|
+
this.replayQueue = tape.slice(cut);
|
|
20010
|
+
this.setBarSeries(tape.slice(0, cut));
|
|
20011
|
+
this.restartBarConsumers({ blank: true });
|
|
20012
|
+
if (wasPlaying) this.events.emit("replay:pause", void 0);
|
|
20013
|
+
this.events.emit("replay:start", { cursorTime: tape[cut - 1].time, remaining: this.replayQueue.length });
|
|
20014
|
+
this.prefetchTicks();
|
|
20015
|
+
}
|
|
20016
|
+
/** Reveal the next queued bar through the live-bar path — or complete the bar forming
|
|
20017
|
+
* tick by tick; the last one ends the replay. */
|
|
20018
|
+
replayStep() {
|
|
20019
|
+
if (this.replayForming) {
|
|
20020
|
+
this.finishFormingBar();
|
|
20021
|
+
return true;
|
|
20022
|
+
}
|
|
20023
|
+
const queue = this.replayQueue;
|
|
20024
|
+
const bar = queue?.shift();
|
|
20025
|
+
if (!queue || !bar) return false;
|
|
20026
|
+
this.replayTickFetches.delete(bar.time);
|
|
20027
|
+
this.applyBar(bar);
|
|
20028
|
+
this.completeReveal(bar);
|
|
20029
|
+
this.prefetchTicks();
|
|
20030
|
+
return true;
|
|
20031
|
+
}
|
|
20032
|
+
/**
|
|
20033
|
+
* Follow up a replay carried onto a new series of the same symbol (another timeframe or
|
|
20034
|
+
* session). The cut asked for keeps the bars that CLOSED by the end of what was revealed —
|
|
20035
|
+
* every finer bar of the last revealed one, never a coarser bar still open at that point
|
|
20036
|
+
* (no look-ahead). Once it settles: playback resumes if it was on; if the replay did not
|
|
20037
|
+
* come back (nothing left after that point, or a later switch took over), it ends as a
|
|
20038
|
+
* market switch would. Returns whether the replay is back already — a start with no
|
|
20039
|
+
* history to wait for cuts synchronously (an async function runs up to its first await).
|
|
20040
|
+
*/
|
|
20041
|
+
resumeCarriedReplay(start, carry) {
|
|
20042
|
+
if (this.replayQueue && this.replayCarrying === carry) this.replayCarrying = null;
|
|
20043
|
+
void start.then(() => {
|
|
20044
|
+
if (this.replayCarrying === carry) this.replayCarrying = null;
|
|
20045
|
+
if (carry.cancelled) return;
|
|
20046
|
+
if (!this.replayQueue) this.events.emit("replay:end", { reason: "market" });
|
|
20047
|
+
else if (carry.playing) this.replayPlay();
|
|
20048
|
+
});
|
|
20049
|
+
return this.replayQueue !== null;
|
|
20050
|
+
}
|
|
20051
|
+
replayStepUpdate() {
|
|
20052
|
+
if (!this.replayQueue) return false;
|
|
20053
|
+
if (this.replayForming) {
|
|
20054
|
+
this.applyReplayTicks(1);
|
|
20055
|
+
return true;
|
|
20056
|
+
}
|
|
20057
|
+
const bar = this.replayQueue[0];
|
|
20058
|
+
if (!bar) return false;
|
|
20059
|
+
const entry = this.replayTickSource ? this.requestTicks(bar) : null;
|
|
20060
|
+
if (entry && !entry.ticks) {
|
|
20061
|
+
const epoch = this.replayTickAbort;
|
|
20062
|
+
void entry.done.then(() => {
|
|
20063
|
+
if (!this.replayPlaying && !this.replayForming && this.replayTickAbort === epoch && this.replayQueue?.[0] === bar) this.replayStepUpdate();
|
|
20064
|
+
});
|
|
20065
|
+
return true;
|
|
20066
|
+
}
|
|
20067
|
+
if (!entry?.ticks?.length) return this.replayStep();
|
|
20068
|
+
this.openFormingBar(entry.ticks, 1);
|
|
20069
|
+
return true;
|
|
20070
|
+
}
|
|
20071
|
+
replaySetTicks(source) {
|
|
20072
|
+
this.finishFormingBar();
|
|
20073
|
+
this.dropTickFetches();
|
|
20074
|
+
this.replayTickSource = source;
|
|
20075
|
+
this.prefetchTicks();
|
|
20076
|
+
}
|
|
20077
|
+
replayPlay(intervalMs2) {
|
|
20078
|
+
if (!this.replayQueue) return;
|
|
20079
|
+
if (intervalMs2 !== void 0) {
|
|
20080
|
+
if (!(Number.isFinite(intervalMs2) && intervalMs2 > 0)) {
|
|
20081
|
+
console.warn(`[vela] chart.replay.play(${intervalMs2}) ignored \u2014 the interval must be a positive number of ms`);
|
|
20082
|
+
return;
|
|
20083
|
+
}
|
|
20084
|
+
this.replayIntervalMs = intervalMs2;
|
|
20085
|
+
}
|
|
20086
|
+
this.clearReplayTimer();
|
|
20087
|
+
this.replayPlaying = true;
|
|
20088
|
+
if (this.replayForming) this.paceFormingBar(this.replayForming);
|
|
20089
|
+
this.scheduleReplayStep(this.replayForming?.delayMs);
|
|
20090
|
+
this.events.emit("replay:play", { intervalMs: this.replayIntervalMs });
|
|
20091
|
+
}
|
|
20092
|
+
replayPause() {
|
|
20093
|
+
if (!this.replayPlaying) return;
|
|
20094
|
+
this.clearReplayTimer();
|
|
20095
|
+
this.events.emit("replay:pause", void 0);
|
|
20096
|
+
}
|
|
20097
|
+
replayStop() {
|
|
20098
|
+
const carrying = this.replayCarrying;
|
|
20099
|
+
if (carrying) {
|
|
20100
|
+
carrying.cancelled = true;
|
|
20101
|
+
this.replayCarrying = null;
|
|
20102
|
+
this.events.emit("replay:end", { reason: "stopped" });
|
|
20103
|
+
}
|
|
20104
|
+
if (this.replayLoading) {
|
|
20105
|
+
const depth = this.replayDepth;
|
|
20106
|
+
this.abandonReplayLoad("stopped");
|
|
20107
|
+
void this.setMarket({ bars: depth?.bars ?? 500 });
|
|
20108
|
+
return;
|
|
20109
|
+
}
|
|
20110
|
+
this.endReplay("stopped", { restore: true, resume: true });
|
|
20111
|
+
}
|
|
20112
|
+
abandonReplayLoad(reason) {
|
|
20113
|
+
const loading = this.replayLoading;
|
|
20114
|
+
if (!loading) return;
|
|
20115
|
+
this.replayLoading = null;
|
|
20116
|
+
this.replayDepth = null;
|
|
20117
|
+
if (loading.seek) this.events.emit("replay:end", { reason });
|
|
20118
|
+
}
|
|
20119
|
+
replayState() {
|
|
20120
|
+
const active = this.replayQueue !== null;
|
|
20121
|
+
return {
|
|
20122
|
+
active: active || this.replayCarrying !== null,
|
|
20123
|
+
// a replay carried over a switch stays on while the new bars load
|
|
20124
|
+
playing: this.replayCarrying?.playing ?? this.replayPlaying,
|
|
20125
|
+
cursorTime: active ? this.rawBars[this.rawBars.length - 1]?.time ?? null : null,
|
|
20126
|
+
remaining: this.replayQueue?.length ?? 0,
|
|
20127
|
+
nextTime: this.replayForming?.bar.time ?? this.replayQueue?.[0]?.time ?? null,
|
|
20128
|
+
intervalMs: this.replayIntervalMs
|
|
20129
|
+
};
|
|
20130
|
+
}
|
|
20131
|
+
replayBounds() {
|
|
20132
|
+
const first = this.rawBars[0];
|
|
20133
|
+
const last = this.replayQueue?.[this.replayQueue.length - 1] ?? this.rawBars[this.rawBars.length - 1];
|
|
20134
|
+
return first && last ? { first: first.time, last: last.time } : null;
|
|
20135
|
+
}
|
|
20136
|
+
scheduleReplayStep(delayMs = this.replayIntervalMs) {
|
|
20137
|
+
this.replayTimer = setTimeout(() => {
|
|
20138
|
+
this.replayTimer = null;
|
|
20139
|
+
this.replayAdvance();
|
|
20140
|
+
}, delayMs);
|
|
20141
|
+
}
|
|
20142
|
+
/** One beat of timed playback: the forming bar's next ticks, else the next bar — whole,
|
|
20143
|
+
* or opened from its ticks (waiting for them when they have not landed yet). */
|
|
20144
|
+
replayAdvance() {
|
|
20145
|
+
if (!this.replayPlaying || !this.replayQueue) return;
|
|
20146
|
+
const forming = this.replayForming;
|
|
20147
|
+
if (forming) {
|
|
20148
|
+
const due = Math.floor((now() - forming.origin) / this.replayIntervalMs) + 1;
|
|
20149
|
+
this.applyReplayTicks(Math.max(forming.batch, due - forming.next));
|
|
20150
|
+
if (this.replayPlaying) this.scheduleReplayStep(forming.delayMs);
|
|
20151
|
+
return;
|
|
20152
|
+
}
|
|
20153
|
+
const bar = this.replayQueue[0];
|
|
20154
|
+
if (!bar) return;
|
|
20155
|
+
const entry = this.replayTickSource ? this.requestTicks(bar) : null;
|
|
20156
|
+
if (entry && !entry.ticks) {
|
|
20157
|
+
const epoch = this.replayTickAbort;
|
|
20158
|
+
void entry.done.then(() => {
|
|
20159
|
+
if (this.replayPlaying && this.replayTimer == null && this.replayTickAbort === epoch && this.replayQueue?.[0] === bar) this.replayAdvance();
|
|
20160
|
+
});
|
|
20161
|
+
return;
|
|
20162
|
+
}
|
|
20163
|
+
if (!entry?.ticks?.length) {
|
|
20164
|
+
if (this.replayStep() && this.replayPlaying) this.scheduleReplayStep();
|
|
20165
|
+
return;
|
|
20166
|
+
}
|
|
20167
|
+
this.openFormingBar(entry.ticks);
|
|
20168
|
+
if (this.replayPlaying) this.scheduleReplayStep(this.replayForming?.delayMs);
|
|
20169
|
+
}
|
|
20170
|
+
/** Take the next queued bar off the tape and open it at its first tick (the first
|
|
20171
|
+
* `count` ticks — default: one timed update's worth). */
|
|
20172
|
+
openFormingBar(ticks, count) {
|
|
20173
|
+
const bar = this.replayQueue.shift();
|
|
20174
|
+
const volumeEach = ticks.every((t) => t.volume === void 0) ? (bar.volume ?? 0) / ticks.length : null;
|
|
20175
|
+
const forming = { bar, ticks, next: 0, candle: null, volumeEach, batch: 1, delayMs: this.replayIntervalMs, origin: 0 };
|
|
20176
|
+
this.paceFormingBar(forming);
|
|
20177
|
+
this.replayForming = forming;
|
|
20178
|
+
this.applyReplayTicks(count ?? forming.batch);
|
|
20179
|
+
this.prefetchTicks();
|
|
20180
|
+
}
|
|
20181
|
+
/** One tick per interval, batched when the interval is shorter than a frame; the clock
|
|
20182
|
+
* is anchored on the ticks already applied (a resume or a pace change carries on from
|
|
20183
|
+
* there, never jumps). */
|
|
20184
|
+
paceFormingBar(f) {
|
|
20185
|
+
f.batch = Math.max(1, Math.ceil(MIN_TICK_UPDATE_MS / this.replayIntervalMs));
|
|
20186
|
+
f.delayMs = this.replayIntervalMs * f.batch;
|
|
20187
|
+
f.origin = now() - Math.max(0, f.next - 1) * this.replayIntervalMs;
|
|
20188
|
+
}
|
|
20189
|
+
/** Fold the next `count` ticks into the forming candle; the last one settles the bar. */
|
|
20190
|
+
applyReplayTicks(count) {
|
|
20191
|
+
const f = this.replayForming;
|
|
20192
|
+
if (!f) return;
|
|
20193
|
+
let c = f.candle;
|
|
20194
|
+
const stop = Math.min(f.ticks.length, f.next + count);
|
|
20195
|
+
for (; f.next < stop; f.next += 1) {
|
|
20196
|
+
const t = f.ticks[f.next];
|
|
20197
|
+
const v = f.volumeEach ?? t.volume ?? 0;
|
|
20198
|
+
const hi = Math.max(t.high ?? t.price, t.price);
|
|
20199
|
+
const lo = Math.min(t.low ?? t.price, t.price);
|
|
20200
|
+
if (c) {
|
|
20201
|
+
c = { ...c, high: Math.max(c.high, hi), low: Math.min(c.low, lo), close: t.price, volume: (c.volume ?? 0) + v };
|
|
20202
|
+
} else {
|
|
20203
|
+
const open2 = t.open ?? t.price;
|
|
20204
|
+
c = { time: f.bar.time, open: open2, high: Math.max(open2, hi), low: Math.min(open2, lo), close: t.price, volume: v };
|
|
20205
|
+
}
|
|
20206
|
+
}
|
|
20207
|
+
f.candle = c;
|
|
20208
|
+
if (f.next >= f.ticks.length) {
|
|
20209
|
+
this.finishFormingBar();
|
|
20210
|
+
return;
|
|
20211
|
+
}
|
|
20212
|
+
this.applyBar(c);
|
|
20213
|
+
this.events.emit("replay:tick", { cursorTime: f.bar.time, index: f.next - 1, count: f.ticks.length });
|
|
20214
|
+
}
|
|
20215
|
+
/** Settle the forming bar on its stored values and announce it revealed. */
|
|
20216
|
+
finishFormingBar() {
|
|
20217
|
+
const f = this.replayForming;
|
|
20218
|
+
if (!f) return;
|
|
20219
|
+
this.replayForming = null;
|
|
20220
|
+
this.replayTickFetches.delete(f.bar.time);
|
|
20221
|
+
this.applyBar(f.bar);
|
|
20222
|
+
this.events.emit("replay:tick", { cursorTime: f.bar.time, index: f.ticks.length - 1, count: f.ticks.length });
|
|
20223
|
+
this.completeReveal(f.bar);
|
|
20224
|
+
}
|
|
20225
|
+
/** Put the forming bar back in its stored form, silently — a seek or an end rebuilds
|
|
20226
|
+
* from the tape, which must hold the stored bar. */
|
|
20227
|
+
settleFormingBar() {
|
|
20228
|
+
const f = this.replayForming;
|
|
20229
|
+
if (!f) return;
|
|
20230
|
+
this.replayForming = null;
|
|
20231
|
+
this.applyBar(f.bar);
|
|
20232
|
+
}
|
|
20233
|
+
/**
|
|
20234
|
+
* A history answer landing mid-replay (a progressive snapshot — the whole answer so far):
|
|
20235
|
+
* the tape keeps its cut and its hidden queue; bars older than the tape join its head,
|
|
20236
|
+
* bars newer than it join the queue's end.
|
|
20237
|
+
*/
|
|
20238
|
+
mergeHistoryIntoReplay(bars) {
|
|
20239
|
+
const queue = this.replayQueue;
|
|
20240
|
+
const head = this.rawBars[0];
|
|
20241
|
+
if (!queue || !head) return;
|
|
20242
|
+
const tail = queue[queue.length - 1] ?? this.rawBars[this.rawBars.length - 1];
|
|
20243
|
+
let older = 0;
|
|
20244
|
+
while (older < bars.length && bars[older].time < head.time) older += 1;
|
|
20245
|
+
let newer = bars.length;
|
|
20246
|
+
while (newer > older && bars[newer - 1].time > tail.time) newer -= 1;
|
|
20247
|
+
if (newer < bars.length) queue.push(...bars.slice(newer));
|
|
20248
|
+
if (older > 0) {
|
|
20249
|
+
this.setBarSeries([...bars.slice(0, older), ...this.rawBars], { preserveView: true });
|
|
20250
|
+
this.notifySessionsBars("backfill");
|
|
20251
|
+
}
|
|
20252
|
+
}
|
|
20253
|
+
/** A bar is fully on screen: announce it, and end the replay if it was the last. */
|
|
20254
|
+
completeReveal(bar) {
|
|
20255
|
+
const queue = this.replayQueue;
|
|
20256
|
+
if (!queue) return;
|
|
20257
|
+
this.events.emit("replay:step", { cursorTime: bar.time, remaining: queue.length });
|
|
20258
|
+
if (queue.length === 0) this.endReplay("finished", { restore: true, resume: true });
|
|
20259
|
+
}
|
|
20260
|
+
/** Ask for the ticks of the next hidden bar ahead of time. */
|
|
20261
|
+
prefetchTicks() {
|
|
20262
|
+
const next = this.replayQueue?.[0];
|
|
20263
|
+
if (next && this.replayTickSource) this.requestTicks(next);
|
|
20264
|
+
}
|
|
20265
|
+
/** The (shared) tick request of a queued bar — the source runs once per bar. */
|
|
20266
|
+
requestTicks(bar) {
|
|
20267
|
+
const known = this.replayTickFetches.get(bar.time);
|
|
20268
|
+
if (known) return known;
|
|
20269
|
+
const source = this.replayTickSource;
|
|
20270
|
+
this.replayTickAbort ?? (this.replayTickAbort = new AbortController());
|
|
20271
|
+
const signal = this.replayTickAbort.signal;
|
|
20272
|
+
const queue = this.replayQueue ?? [];
|
|
20273
|
+
const end = queue[queue.indexOf(bar) + 1]?.time ?? bar.time + this.barIntervalMs();
|
|
20274
|
+
const entry = { ticks: null, done: Promise.resolve() };
|
|
20275
|
+
const land = (ticks) => {
|
|
20276
|
+
entry.ticks = usableTicks(ticks);
|
|
20277
|
+
};
|
|
20278
|
+
const fail = (e) => {
|
|
20279
|
+
if (!signal.aborted) console.warn(`[vela] replay tick source failed \u2014 revealing the bar whole (${e instanceof Error ? e.message : String(e)})`);
|
|
20280
|
+
land([]);
|
|
20281
|
+
};
|
|
20282
|
+
try {
|
|
20283
|
+
const out = source(bar, { end, signal });
|
|
20284
|
+
if (Array.isArray(out)) land(out);
|
|
20285
|
+
else entry.done = Promise.resolve(out).then(land, fail);
|
|
20286
|
+
} catch (e) {
|
|
20287
|
+
fail(e);
|
|
20288
|
+
}
|
|
20289
|
+
this.replayTickFetches.set(bar.time, entry);
|
|
20290
|
+
return entry;
|
|
20291
|
+
}
|
|
20292
|
+
/** Abort the tick requests in flight and forget the landed ones. */
|
|
20293
|
+
dropTickFetches() {
|
|
20294
|
+
this.replayTickAbort?.abort();
|
|
20295
|
+
this.replayTickAbort = null;
|
|
20296
|
+
this.replayTickFetches.clear();
|
|
20297
|
+
}
|
|
20298
|
+
clearReplayTimer() {
|
|
20299
|
+
if (this.replayTimer != null) clearTimeout(this.replayTimer);
|
|
20300
|
+
this.replayTimer = null;
|
|
20301
|
+
this.replayPlaying = false;
|
|
20302
|
+
}
|
|
20303
|
+
/**
|
|
20304
|
+
* Leave replay. `restore` puts the queued bars back and restarts the consumers over the
|
|
20305
|
+
* full history (a market switch skips it: its load replaces everything anyway); `resume`
|
|
20306
|
+
* restarts live updates and heals the bars that closed while replaying. A null reason
|
|
20307
|
+
* leaves silently — a seek that re-enters right after.
|
|
20308
|
+
*/
|
|
20309
|
+
endReplay(reason, opts) {
|
|
20310
|
+
const queue = this.replayQueue;
|
|
20311
|
+
if (!queue) return;
|
|
20312
|
+
this.clearReplayTimer();
|
|
20313
|
+
if (opts.restore) this.settleFormingBar();
|
|
20314
|
+
else this.replayForming = null;
|
|
20315
|
+
this.dropTickFetches();
|
|
20316
|
+
this.replayQueue = null;
|
|
20317
|
+
const depth = reason ? this.replayDepth : null;
|
|
20318
|
+
if (reason) this.replayDepth = null;
|
|
20319
|
+
if (depth) {
|
|
20320
|
+
if (depth.bars === void 0) delete this.config.market.bars;
|
|
20321
|
+
else this.config.market.bars = depth.bars;
|
|
20322
|
+
}
|
|
20323
|
+
if (opts.restore) {
|
|
20324
|
+
const keep = depth ? depth.bars ?? 500 : Infinity;
|
|
20325
|
+
if (queue.length > 0 || this.rawBars.length > keep) {
|
|
20326
|
+
const full = [...this.rawBars, ...queue];
|
|
20327
|
+
this.setBarSeries(full.length > keep ? full.slice(full.length - keep) : full);
|
|
20328
|
+
this.restartBarConsumers({ blank: false });
|
|
20329
|
+
} else if (this.config.live) {
|
|
20330
|
+
this.restartNativeIndicators();
|
|
20331
|
+
this.restartChartTypeEngine();
|
|
20332
|
+
}
|
|
20333
|
+
}
|
|
20334
|
+
if (opts.resume && this.config.live) {
|
|
20335
|
+
this.startLive();
|
|
20336
|
+
const last = this.rawBars[this.rawBars.length - 1];
|
|
20337
|
+
if (last && this.canHeal()) void this.healGap(last.time);
|
|
20338
|
+
}
|
|
20339
|
+
if (reason) this.events.emit("replay:end", { reason });
|
|
20340
|
+
}
|
|
20341
|
+
/**
|
|
20342
|
+
* The bars were replaced by a cut or a restore of the SAME market: every consumer re-runs.
|
|
20343
|
+
* A cut also blanks the mounted models first — they still paint the hidden bars' values,
|
|
20344
|
+
* which must not show until the re-runs land.
|
|
20345
|
+
*/
|
|
20346
|
+
restartBarConsumers(opts) {
|
|
20347
|
+
if (opts.blank) this.blankIndicatorVisuals();
|
|
20348
|
+
this.restartNativeIndicators();
|
|
20349
|
+
this.restartChartTypeEngine();
|
|
20350
|
+
this.reexecuteIndicators();
|
|
20351
|
+
}
|
|
19615
20352
|
/** The primary symbol as the data layer resolves it, or null if no symbol is set. */
|
|
19616
20353
|
qualifiedSymbol() {
|
|
19617
20354
|
const market = this.config.market;
|
|
@@ -19683,7 +20420,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19683
20420
|
engine.start({
|
|
19684
20421
|
symbol: this.config.market.symbol ?? "TEST",
|
|
19685
20422
|
timeframe: this.config.market.timeframe ?? "60",
|
|
19686
|
-
live: this.config.live ?? false,
|
|
20423
|
+
live: (this.config.live ?? false) && this.replayQueue === null,
|
|
19687
20424
|
session: this.config.market.session,
|
|
19688
20425
|
bars: () => this.bars,
|
|
19689
20426
|
data: this.dataControl,
|
|
@@ -19809,6 +20546,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19809
20546
|
record.session.notifyBars(reason);
|
|
19810
20547
|
} else if (record.native) record.native.instance.onBars();
|
|
19811
20548
|
}
|
|
20549
|
+
if (this.activeEngineStyle) this.typeEngines.get(this.activeEngineStyle)?.onBars?.();
|
|
19812
20550
|
}
|
|
19813
20551
|
/** Gap healing needs a ranged feed and a provider-backed series (offline `data` has no source). */
|
|
19814
20552
|
canHeal() {
|
|
@@ -19831,10 +20569,11 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19831
20569
|
try {
|
|
19832
20570
|
const bars = await this.feed.loadRange(this.config.market, { from: fromMs });
|
|
19833
20571
|
if (this.generation !== gen) return;
|
|
20572
|
+
if (this.replayQueue) return;
|
|
19834
20573
|
for (const b of bars) this.applyBar(b, false);
|
|
19835
20574
|
} catch {
|
|
19836
20575
|
} finally {
|
|
19837
|
-
if (this.generation === gen) {
|
|
20576
|
+
if (this.generation === gen && !this.replayQueue) {
|
|
19838
20577
|
this.healing = false;
|
|
19839
20578
|
const pending = this.healBuffer;
|
|
19840
20579
|
this.healBuffer = [];
|
|
@@ -20139,6 +20878,13 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
20139
20878
|
destroy() {
|
|
20140
20879
|
this.bumpGeneration();
|
|
20141
20880
|
this.resolveHistoryComplete();
|
|
20881
|
+
this.clearReplayTimer();
|
|
20882
|
+
this.replayQueue = null;
|
|
20883
|
+
this.replayForming = null;
|
|
20884
|
+
this.replayCarrying = null;
|
|
20885
|
+
this.dropTickFetches();
|
|
20886
|
+
this.replayDepth = null;
|
|
20887
|
+
this.replayLoading = null;
|
|
20142
20888
|
this.stopLive();
|
|
20143
20889
|
if (this.viewportTimer != null) clearTimeout(this.viewportTimer);
|
|
20144
20890
|
this.viewportUnsub?.();
|
|
@@ -20237,6 +20983,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
20237
20983
|
this.emitScriptRun(id, cause, first);
|
|
20238
20984
|
},
|
|
20239
20985
|
onAlert: (a) => {
|
|
20986
|
+
if (this.replayQueue || this.replayCarrying) return;
|
|
20240
20987
|
const indicator = record.options?.title ?? record.prepared?.meta.title ?? record.title;
|
|
20241
20988
|
this.events.emit("alert", { ...a, indicator });
|
|
20242
20989
|
handle.emit("alert", { id: a.id, message: a.message, title: a.title, time: a.time });
|
|
@@ -20263,7 +21010,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
20263
21010
|
id,
|
|
20264
21011
|
symbol: this.config.market.symbol ?? "TEST",
|
|
20265
21012
|
timeframe: this.config.market.timeframe ?? "60",
|
|
20266
|
-
live: this.config.live,
|
|
21013
|
+
live: this.config.live && this.replayQueue === null,
|
|
20267
21014
|
session: this.config.market.session,
|
|
20268
21015
|
bars: () => this.bars,
|
|
20269
21016
|
data: this.dataControl,
|
|
@@ -20666,17 +21413,17 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
20666
21413
|
}
|
|
20667
21414
|
emitContextChanged(id) {
|
|
20668
21415
|
if (!this.registry.get(id)?.session?.getContext) return;
|
|
20669
|
-
const
|
|
20670
|
-
if (
|
|
20671
|
-
this.contextEmitAt.set(id,
|
|
21416
|
+
const now2 = Date.now();
|
|
21417
|
+
if (now2 - (this.contextEmitAt.get(id) ?? 0) < 1e3) return;
|
|
21418
|
+
this.contextEmitAt.set(id, now2);
|
|
20672
21419
|
this.events.emit("context:changed", { id });
|
|
20673
21420
|
}
|
|
20674
21421
|
emitScriptRun(id, cause, first) {
|
|
20675
21422
|
const record = this.registry.get(id);
|
|
20676
21423
|
if (!record || record.native || !this.events.hasListeners("script:run")) return;
|
|
20677
|
-
const
|
|
20678
|
-
if (cause === "tick" &&
|
|
20679
|
-
this.runEmitAt.set(id,
|
|
21424
|
+
const now2 = Date.now();
|
|
21425
|
+
if (cause === "tick" && now2 - (this.runEmitAt.get(id) ?? 0) < RUN_EMIT_THROTTLE_MS) return;
|
|
21426
|
+
this.runEmitAt.set(id, now2);
|
|
20680
21427
|
void Promise.resolve().then(() => this.buildScriptRun(record, cause, first)).then((run) => {
|
|
20681
21428
|
if (run && this.registry.get(id)) this.events.emit("script:run", run);
|
|
20682
21429
|
});
|
|
@@ -20729,6 +21476,16 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
20729
21476
|
handle?.emit("error", { error });
|
|
20730
21477
|
}
|
|
20731
21478
|
};
|
|
21479
|
+
function now() {
|
|
21480
|
+
return typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
21481
|
+
}
|
|
21482
|
+
function usableTicks(value) {
|
|
21483
|
+
if (!Array.isArray(value)) return [];
|
|
21484
|
+
const optional = (n) => n === void 0 || Number.isFinite(n);
|
|
21485
|
+
return value.filter(
|
|
21486
|
+
(t) => t != null && Number.isFinite(t.price) && optional(t.volume) && optional(t.high) && optional(t.low) && optional(t.open)
|
|
21487
|
+
);
|
|
21488
|
+
}
|
|
20732
21489
|
function yieldToPaint() {
|
|
20733
21490
|
if (typeof requestAnimationFrame === "function") {
|
|
20734
21491
|
return new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
|
|
@@ -20929,6 +21686,12 @@ var RendererControl = class {
|
|
|
20929
21686
|
onCrosshairMove(cb) {
|
|
20930
21687
|
return this.renderer.onCrosshairMove(cb);
|
|
20931
21688
|
}
|
|
21689
|
+
/** A click — or a touch tap — on the plot, never the end of a pan: the open time of
|
|
21690
|
+
* the bar under it (`null` off the bars). On touch, where a tap moves no crosshair,
|
|
21691
|
+
* this is how an interaction learns which bar was chosen. */
|
|
21692
|
+
onClick(cb) {
|
|
21693
|
+
return this.renderer.onClick(cb);
|
|
21694
|
+
}
|
|
20932
21695
|
/** Touch long-press on a price or time axis strip — silent no-op without the seam. */
|
|
20933
21696
|
onAxisLongPress(cb) {
|
|
20934
21697
|
return this.renderer.onAxisLongPress?.(cb) ?? (() => void 0);
|
|
@@ -21427,6 +22190,71 @@ var MarksControl = class {
|
|
|
21427
22190
|
}
|
|
21428
22191
|
};
|
|
21429
22192
|
|
|
22193
|
+
// src/core/ReplayControl.ts
|
|
22194
|
+
var ReplayControl = class {
|
|
22195
|
+
constructor(ctrl) {
|
|
22196
|
+
this.ctrl = ctrl;
|
|
22197
|
+
}
|
|
22198
|
+
/**
|
|
22199
|
+
* Enter replay (or seek, when already replaying) at `opts.from`. Resolves once the
|
|
22200
|
+
* chart shows the rewound history — after any in-flight history load has finished.
|
|
22201
|
+
* Starts paused; call {@link play} or {@link step}.
|
|
22202
|
+
*/
|
|
22203
|
+
start(opts) {
|
|
22204
|
+
return this.ctrl.replayStart(opts);
|
|
22205
|
+
}
|
|
22206
|
+
/** Reveal the next bar — or, mid-way through a bar played tick by tick, complete it.
|
|
22207
|
+
* Returns false when replay is off or nothing is left. */
|
|
22208
|
+
step() {
|
|
22209
|
+
return this.ctrl.replayStep();
|
|
22210
|
+
}
|
|
22211
|
+
/** Reveal the next UPDATE: with {@link setTicks}, the next tick of the forming bar — or
|
|
22212
|
+
* the next bar opened at its first tick (once its ticks are in, if still loading);
|
|
22213
|
+
* otherwise the next whole bar, like {@link step}. Returns false when nothing is left. */
|
|
22214
|
+
stepUpdate() {
|
|
22215
|
+
return this.ctrl.replayStepUpdate();
|
|
22216
|
+
}
|
|
22217
|
+
/** Reveal one bar (one tick, with {@link setTicks}) every `intervalMs` (default: the last
|
|
22218
|
+
* interval used, else 1000). Calling it again while playing changes the pace. */
|
|
22219
|
+
play(intervalMs2) {
|
|
22220
|
+
this.ctrl.replayPlay(intervalMs2);
|
|
22221
|
+
return this;
|
|
22222
|
+
}
|
|
22223
|
+
pause() {
|
|
22224
|
+
this.ctrl.replayPause();
|
|
22225
|
+
return this;
|
|
22226
|
+
}
|
|
22227
|
+
/** Leave replay: the full history comes back and live updates resume. Also cancels a
|
|
22228
|
+
* `start()` still loading older history (the chart returns to its previous depth). */
|
|
22229
|
+
stop() {
|
|
22230
|
+
this.ctrl.replayStop();
|
|
22231
|
+
return this;
|
|
22232
|
+
}
|
|
22233
|
+
get state() {
|
|
22234
|
+
return this.ctrl.replayState();
|
|
22235
|
+
}
|
|
22236
|
+
/** The loaded history a replay can walk — hidden bars included while replaying. Null
|
|
22237
|
+
* before any bar loaded. A start older than `first` deepens the history first. */
|
|
22238
|
+
get bounds() {
|
|
22239
|
+
return this.ctrl.replayBounds();
|
|
22240
|
+
}
|
|
22241
|
+
/**
|
|
22242
|
+
* Play each revealed bar as a series of intrabar updates instead of whole: while
|
|
22243
|
+
* playing, the forming candle opens at the first tick, then every tick moves its close
|
|
22244
|
+
* and stretches its high and low, exactly like a live tick — indicators, chart types
|
|
22245
|
+
* and the `bar` event follow — and the last one settles it on the stored bar. The play
|
|
22246
|
+
* interval is then the time between two TICKS: a bar lasts as many intervals as it
|
|
22247
|
+
* has ticks (below ~16 ms, ticks are batched per frame). The next bar's ticks are
|
|
22248
|
+
* requested while the current one plays; playback waits for them. `null` goes back to
|
|
22249
|
+
* whole bars. Takes effect from the next bar; a bar forming when the source changes
|
|
22250
|
+
* completes at once.
|
|
22251
|
+
*/
|
|
22252
|
+
setTicks(source) {
|
|
22253
|
+
this.ctrl.replaySetTicks(source);
|
|
22254
|
+
return this;
|
|
22255
|
+
}
|
|
22256
|
+
};
|
|
22257
|
+
|
|
21430
22258
|
// src/core/model/inputs.ts
|
|
21431
22259
|
function inputVisible(when, values) {
|
|
21432
22260
|
if (!when) return true;
|
|
@@ -22036,10 +22864,16 @@ var IndicatorInputsDialog = class {
|
|
|
22036
22864
|
this.calendarPop = null;
|
|
22037
22865
|
this.calendarAnchor = null;
|
|
22038
22866
|
},
|
|
22039
|
-
content: (el) =>
|
|
22040
|
-
|
|
22041
|
-
|
|
22042
|
-
|
|
22867
|
+
content: (el) => {
|
|
22868
|
+
const picker = new DatePicker({
|
|
22869
|
+
value: current,
|
|
22870
|
+
onPick: (iso) => {
|
|
22871
|
+
pop.hide();
|
|
22872
|
+
onPick(iso);
|
|
22873
|
+
}
|
|
22874
|
+
});
|
|
22875
|
+
el.append(picker.el);
|
|
22876
|
+
}
|
|
22043
22877
|
});
|
|
22044
22878
|
this.calendarPop = pop;
|
|
22045
22879
|
this.calendarAnchor = anchor;
|
|
@@ -22164,28 +22998,6 @@ function ensureDialogStyles() {
|
|
|
22164
22998
|
.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;}
|
|
22165
22999
|
.vela-ind-combo-chevron:hover{opacity:0.9;}
|
|
22166
23000
|
.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;}
|
|
22167
|
-
.vela-ind-cal [hidden]{display:none !important;}
|
|
22168
|
-
.vela-ind-cal-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;}
|
|
22169
|
-
.vela-ind-cal-title{flex:1;display:flex;align-items:center;justify-content:center;gap:2px;min-width:0;}
|
|
22170
|
-
.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;}
|
|
22171
|
-
.vela-ind-cal-switch:not(:disabled):hover{background:var(--vela-hover);}
|
|
22172
|
-
.vela-ind-cal-switch:disabled{cursor:default;}
|
|
22173
|
-
.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;}
|
|
22174
|
-
.vela-ind-cal-nav:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
|
|
22175
|
-
.vela-ind-cal-week,.vela-ind-cal-grid{display:grid;grid-template-columns:repeat(7,28px);gap:2px;}
|
|
22176
|
-
.vela-ind-cal-week{margin-bottom:4px;color:var(--vela-fg-muted);font-size:11px;text-align:center;}
|
|
22177
|
-
.vela-ind-cal-week span{line-height:20px;}
|
|
22178
|
-
.vela-ind-cal-blank{width:28px;height:28px;}
|
|
22179
|
-
.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;}
|
|
22180
|
-
.vela-ind-cal-day:hover{background:var(--vela-hover);}
|
|
22181
|
-
.vela-ind-cal-day[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
|
|
22182
|
-
.vela-ind-cal-day[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
|
|
22183
|
-
.vela-ind-cal-cells{display:grid;grid-template-columns:repeat(3,1fr);gap:4px;width:calc(7 * 28px + 6 * 2px);}
|
|
22184
|
-
.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;}
|
|
22185
|
-
.vela-ind-cal-cell:hover{background:var(--vela-hover);}
|
|
22186
|
-
.vela-ind-cal-cell[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
|
|
22187
|
-
.vela-ind-cal-cell[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
|
|
22188
|
-
.vela-ind-cal-cell[data-outside]{opacity:0.45;}
|
|
22189
23001
|
${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
|
|
22190
23002
|
.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;}
|
|
22191
23003
|
.vela-ind-tab:not(.vela-ind-tab-active):hover{color:var(--vela-fg-bright);}
|
|
@@ -22219,174 +23031,6 @@ var TIME_OPTIONS = Array.from({ length: 48 }, (_, i) => {
|
|
|
22219
23031
|
const v = `${hh}:${mm}`;
|
|
22220
23032
|
return { value: v, label: v };
|
|
22221
23033
|
});
|
|
22222
|
-
var MONTH_LABELS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
|
|
22223
|
-
var MONTH_SHORT = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
22224
|
-
var WEEKDAY_LABELS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
|
|
22225
|
-
function normalizeDateInput(raw) {
|
|
22226
|
-
const t = raw.trim();
|
|
22227
|
-
const m = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(t);
|
|
22228
|
-
if (!m) return null;
|
|
22229
|
-
const y = Number(m[1]);
|
|
22230
|
-
const mo = Number(m[2]);
|
|
22231
|
-
const d = Number(m[3]);
|
|
22232
|
-
const dt = new Date(y, mo - 1, d);
|
|
22233
|
-
if (dt.getFullYear() !== y || dt.getMonth() !== mo - 1 || dt.getDate() !== d) return null;
|
|
22234
|
-
return isoDate(dt);
|
|
22235
|
-
}
|
|
22236
|
-
function parseIsoDate(raw) {
|
|
22237
|
-
const iso = normalizeDateInput(raw);
|
|
22238
|
-
if (!iso) return null;
|
|
22239
|
-
const [y, mo, d] = iso.split("-").map(Number);
|
|
22240
|
-
return new Date(y, mo - 1, d);
|
|
22241
|
-
}
|
|
22242
|
-
function isoDate(d) {
|
|
22243
|
-
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
22244
|
-
}
|
|
22245
|
-
function fillCalendar(el, current, onPick) {
|
|
22246
|
-
const today = /* @__PURE__ */ new Date();
|
|
22247
|
-
const selected = parseIsoDate(current);
|
|
22248
|
-
let year = selected?.getFullYear() ?? today.getFullYear();
|
|
22249
|
-
let month = selected?.getMonth() ?? today.getMonth();
|
|
22250
|
-
let mode = "date";
|
|
22251
|
-
const prev2 = document.createElement("button");
|
|
22252
|
-
prev2.type = "button";
|
|
22253
|
-
prev2.className = "vela-ind-cal-nav";
|
|
22254
|
-
prev2.innerHTML = iconAt("chevron-left", 14);
|
|
22255
|
-
const next = document.createElement("button");
|
|
22256
|
-
next.type = "button";
|
|
22257
|
-
next.className = "vela-ind-cal-nav";
|
|
22258
|
-
next.innerHTML = iconAt("chevron-right", 14);
|
|
22259
|
-
const monthBtn = document.createElement("button");
|
|
22260
|
-
monthBtn.type = "button";
|
|
22261
|
-
monthBtn.className = "vela-ind-cal-switch";
|
|
22262
|
-
monthBtn.setAttribute("aria-label", "Choose month");
|
|
22263
|
-
const yearBtn = document.createElement("button");
|
|
22264
|
-
yearBtn.type = "button";
|
|
22265
|
-
yearBtn.className = "vela-ind-cal-switch";
|
|
22266
|
-
yearBtn.setAttribute("aria-label", "Choose year");
|
|
22267
|
-
const title = document.createElement("div");
|
|
22268
|
-
title.className = "vela-ind-cal-title";
|
|
22269
|
-
title.append(monthBtn, yearBtn);
|
|
22270
|
-
const head = document.createElement("div");
|
|
22271
|
-
head.className = "vela-ind-cal-head";
|
|
22272
|
-
head.append(prev2, title, next);
|
|
22273
|
-
const week = document.createElement("div");
|
|
22274
|
-
week.className = "vela-ind-cal-week";
|
|
22275
|
-
for (const d of WEEKDAY_LABELS) {
|
|
22276
|
-
const cell = document.createElement("span");
|
|
22277
|
-
cell.textContent = d;
|
|
22278
|
-
week.appendChild(cell);
|
|
22279
|
-
}
|
|
22280
|
-
const grid = document.createElement("div");
|
|
22281
|
-
const paint = () => {
|
|
22282
|
-
const decade = Math.floor(year / 10) * 10;
|
|
22283
|
-
monthBtn.hidden = mode !== "date";
|
|
22284
|
-
monthBtn.textContent = MONTH_LABELS[month] ?? "";
|
|
22285
|
-
yearBtn.textContent = mode === "year" ? `${decade}-${decade + 9}` : String(year);
|
|
22286
|
-
yearBtn.disabled = mode === "year";
|
|
22287
|
-
week.hidden = mode !== "date";
|
|
22288
|
-
prev2.setAttribute("aria-label", mode === "date" ? "Previous month" : mode === "month" ? "Previous year" : "Previous decade");
|
|
22289
|
-
next.setAttribute("aria-label", mode === "date" ? "Next month" : mode === "month" ? "Next year" : "Next decade");
|
|
22290
|
-
grid.className = mode === "date" ? "vela-ind-cal-grid" : "vela-ind-cal-cells";
|
|
22291
|
-
grid.replaceChildren();
|
|
22292
|
-
if (mode === "date") {
|
|
22293
|
-
const startPad = new Date(year, month, 1).getDay();
|
|
22294
|
-
const days = new Date(year, month + 1, 0).getDate();
|
|
22295
|
-
const todayIso = isoDate(today);
|
|
22296
|
-
const selectedIso = selected ? isoDate(selected) : "";
|
|
22297
|
-
for (let i = 0; i < startPad; i++) {
|
|
22298
|
-
const blank = document.createElement("span");
|
|
22299
|
-
blank.className = "vela-ind-cal-blank";
|
|
22300
|
-
grid.appendChild(blank);
|
|
22301
|
-
}
|
|
22302
|
-
for (let day = 1; day <= days; day++) {
|
|
22303
|
-
const iso = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
|
22304
|
-
const b = document.createElement("button");
|
|
22305
|
-
b.type = "button";
|
|
22306
|
-
b.className = "vela-ind-cal-day";
|
|
22307
|
-
b.textContent = String(day);
|
|
22308
|
-
if (iso === selectedIso) b.dataset.checked = "1";
|
|
22309
|
-
if (iso === todayIso) b.dataset.today = "1";
|
|
22310
|
-
b.addEventListener("click", (e) => {
|
|
22311
|
-
e.stopPropagation();
|
|
22312
|
-
onPick(iso);
|
|
22313
|
-
});
|
|
22314
|
-
grid.appendChild(b);
|
|
22315
|
-
}
|
|
22316
|
-
return;
|
|
22317
|
-
}
|
|
22318
|
-
if (mode === "month") {
|
|
22319
|
-
for (let m = 0; m < 12; m++) {
|
|
22320
|
-
const b = document.createElement("button");
|
|
22321
|
-
b.type = "button";
|
|
22322
|
-
b.className = "vela-ind-cal-cell";
|
|
22323
|
-
b.textContent = MONTH_SHORT[m] ?? "";
|
|
22324
|
-
if (selected?.getFullYear() === year && selected.getMonth() === m) b.dataset.checked = "1";
|
|
22325
|
-
if (today.getFullYear() === year && today.getMonth() === m) b.dataset.today = "1";
|
|
22326
|
-
b.addEventListener("click", (e) => {
|
|
22327
|
-
e.stopPropagation();
|
|
22328
|
-
month = m;
|
|
22329
|
-
mode = "date";
|
|
22330
|
-
paint();
|
|
22331
|
-
});
|
|
22332
|
-
grid.appendChild(b);
|
|
22333
|
-
}
|
|
22334
|
-
return;
|
|
22335
|
-
}
|
|
22336
|
-
for (let y = decade - 1; y <= decade + 10; y++) {
|
|
22337
|
-
const b = document.createElement("button");
|
|
22338
|
-
b.type = "button";
|
|
22339
|
-
b.className = "vela-ind-cal-cell";
|
|
22340
|
-
b.textContent = String(y);
|
|
22341
|
-
if (y < decade || y > decade + 9) b.dataset.outside = "1";
|
|
22342
|
-
if (selected?.getFullYear() === y) b.dataset.checked = "1";
|
|
22343
|
-
if (today.getFullYear() === y) b.dataset.today = "1";
|
|
22344
|
-
b.addEventListener("click", (e) => {
|
|
22345
|
-
e.stopPropagation();
|
|
22346
|
-
year = y;
|
|
22347
|
-
mode = "month";
|
|
22348
|
-
paint();
|
|
22349
|
-
});
|
|
22350
|
-
grid.appendChild(b);
|
|
22351
|
-
}
|
|
22352
|
-
};
|
|
22353
|
-
const step = (dir) => {
|
|
22354
|
-
if (mode === "date") {
|
|
22355
|
-
month += dir;
|
|
22356
|
-
if (month < 0) {
|
|
22357
|
-
month = 11;
|
|
22358
|
-
year -= 1;
|
|
22359
|
-
}
|
|
22360
|
-
if (month > 11) {
|
|
22361
|
-
month = 0;
|
|
22362
|
-
year += 1;
|
|
22363
|
-
}
|
|
22364
|
-
} else {
|
|
22365
|
-
year += mode === "month" ? dir : dir * 10;
|
|
22366
|
-
}
|
|
22367
|
-
paint();
|
|
22368
|
-
};
|
|
22369
|
-
prev2.addEventListener("click", (e) => {
|
|
22370
|
-
e.stopPropagation();
|
|
22371
|
-
step(-1);
|
|
22372
|
-
});
|
|
22373
|
-
next.addEventListener("click", (e) => {
|
|
22374
|
-
e.stopPropagation();
|
|
22375
|
-
step(1);
|
|
22376
|
-
});
|
|
22377
|
-
monthBtn.addEventListener("click", (e) => {
|
|
22378
|
-
e.stopPropagation();
|
|
22379
|
-
mode = "month";
|
|
22380
|
-
paint();
|
|
22381
|
-
});
|
|
22382
|
-
yearBtn.addEventListener("click", (e) => {
|
|
22383
|
-
e.stopPropagation();
|
|
22384
|
-
mode = "year";
|
|
22385
|
-
paint();
|
|
22386
|
-
});
|
|
22387
|
-
paint();
|
|
22388
|
-
el.append(head, week, grid);
|
|
22389
|
-
}
|
|
22390
23034
|
function normalizeTimeInput(raw) {
|
|
22391
23035
|
const t = raw.trim();
|
|
22392
23036
|
const m = /^(\d{1,2}):(\d{2})$/.exec(t) ?? /^(\d{2})(\d{2})$/.exec(t);
|
|
@@ -25090,7 +25734,7 @@ var Scheduler = class {
|
|
|
25090
25734
|
|
|
25091
25735
|
// src/renderers/native/core/Animator.ts
|
|
25092
25736
|
var Animator = class {
|
|
25093
|
-
constructor(tick, raf, cancel,
|
|
25737
|
+
constructor(tick, raf, cancel, now2) {
|
|
25094
25738
|
this.tick = tick;
|
|
25095
25739
|
this.handle = null;
|
|
25096
25740
|
this.last = 0;
|
|
@@ -25104,7 +25748,7 @@ var Animator = class {
|
|
|
25104
25748
|
};
|
|
25105
25749
|
this.raf = raf ?? ((cb) => requestAnimationFrame(cb));
|
|
25106
25750
|
this.cancel = cancel ?? ((h) => cancelAnimationFrame(h));
|
|
25107
|
-
this.now =
|
|
25751
|
+
this.now = now2 ?? (() => performance.now());
|
|
25108
25752
|
}
|
|
25109
25753
|
get active() {
|
|
25110
25754
|
return this.handle !== null;
|
|
@@ -25903,6 +26547,8 @@ var SceneGraph = class {
|
|
|
25903
26547
|
/** VPVR-layer config pushed by the VPVR native indicator (null ⇒ layer off). Ephemeral. */
|
|
25904
26548
|
this.vpvrLayer = null;
|
|
25905
26549
|
this.crosshair = null;
|
|
26550
|
+
/** Runtime override of the crosshair's lines and style (the `crosshairOverride` feature). Ephemeral. */
|
|
26551
|
+
this.crosshairOverride = null;
|
|
25906
26552
|
/** How the base price series is drawn on the price pane (candles by default). */
|
|
25907
26553
|
this.priceStyle = "candles";
|
|
25908
26554
|
/** Price-series base painting for the ACTIVE style (see ChartTypeDefinition.basePainting). */
|
|
@@ -27819,9 +28465,9 @@ function timeTicks(fromMs, toMs, target = 8, offsetMs = 0) {
|
|
|
27819
28465
|
}
|
|
27820
28466
|
|
|
27821
28467
|
// src/renderers/native/chrome/countdown.ts
|
|
27822
|
-
function countdownText(barOpen, barMs,
|
|
28468
|
+
function countdownText(barOpen, barMs, now2) {
|
|
27823
28469
|
if (!(barMs > 0)) return null;
|
|
27824
|
-
const remaining = barOpen + barMs -
|
|
28470
|
+
const remaining = barOpen + barMs - now2;
|
|
27825
28471
|
if (remaining <= 0) return null;
|
|
27826
28472
|
return formatCountdown(remaining);
|
|
27827
28473
|
}
|
|
@@ -28633,19 +29279,35 @@ var CrosshairRenderer = class {
|
|
|
28633
29279
|
const dataH = coords.height;
|
|
28634
29280
|
if (!ch || ch.x < 0 || ch.x > dataW || ch.y < 0 || ch.y > dataH) return;
|
|
28635
29281
|
const cs = scene.style.crosshair;
|
|
29282
|
+
const ov = scene.crosshairOverride;
|
|
29283
|
+
const vertical = ov?.vertical !== false;
|
|
29284
|
+
const horizontal = ov?.horizontal !== false;
|
|
28636
29285
|
ctx.font = `${scene.style.fontSize}px ${theme.fontFamily}`;
|
|
28637
29286
|
ctx.textBaseline = "middle";
|
|
28638
29287
|
const logical = Math.round(coords.xToLogical(ch.x));
|
|
28639
|
-
const x =
|
|
28640
|
-
|
|
28641
|
-
|
|
28642
|
-
|
|
28643
|
-
|
|
29288
|
+
const x = crisp(coords.logicalToX(logical), ov?.width ?? cs.width);
|
|
29289
|
+
if (vertical && ov?.shadeRight) {
|
|
29290
|
+
const from = Math.max(0, Math.round(coords.logicalToX(logical + 0.5)));
|
|
29291
|
+
if (from < dataW) {
|
|
29292
|
+
ctx.fillStyle = ov.shadeRight.color;
|
|
29293
|
+
ctx.globalAlpha = ov.shadeRight.opacity ?? 1;
|
|
29294
|
+
ctx.fillRect(from, 0, dataW - from, dataH);
|
|
29295
|
+
ctx.globalAlpha = 1;
|
|
29296
|
+
}
|
|
29297
|
+
}
|
|
29298
|
+
ctx.strokeStyle = ov?.color ?? cs.color ?? theme.textColor;
|
|
29299
|
+
ctx.lineWidth = ov?.width ?? cs.width;
|
|
29300
|
+
ctx.globalAlpha = ov?.opacity ?? cs.opacity;
|
|
29301
|
+
setDash3(ctx, ov?.style ?? cs.style);
|
|
28644
29302
|
ctx.beginPath();
|
|
28645
|
-
|
|
28646
|
-
|
|
28647
|
-
|
|
28648
|
-
|
|
29303
|
+
if (vertical) {
|
|
29304
|
+
ctx.moveTo(x, 0);
|
|
29305
|
+
ctx.lineTo(x, dataH);
|
|
29306
|
+
}
|
|
29307
|
+
if (horizontal) {
|
|
29308
|
+
ctx.moveTo(0, Math.round(ch.y) + 0.5);
|
|
29309
|
+
ctx.lineTo(dataW, Math.round(ch.y) + 0.5);
|
|
29310
|
+
}
|
|
28649
29311
|
ctx.stroke();
|
|
28650
29312
|
ctx.setLineDash([]);
|
|
28651
29313
|
ctx.lineWidth = 1;
|
|
@@ -28658,11 +29320,11 @@ var CrosshairRenderer = class {
|
|
|
28658
29320
|
}
|
|
28659
29321
|
}
|
|
28660
29322
|
const chipBg = cs.labelBackground ?? theme.borderColor;
|
|
28661
|
-
if (pane && pane.axisFormat !== "none") {
|
|
29323
|
+
if (horizontal && pane && pane.axisFormat !== "none") {
|
|
28662
29324
|
const price = coords.yToPrice(ch.y, pane.scale, pane.bounds);
|
|
28663
29325
|
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);
|
|
28664
29326
|
}
|
|
28665
|
-
this.chip(ctx, x, dataH + 1, formatTimeStamp(coords.logicalToTime(logical), scene.timezone, coords.barInterval), chipBg, "center", true, theme.background);
|
|
29327
|
+
if (vertical) this.chip(ctx, x, dataH + 1, formatTimeStamp(coords.logicalToTime(logical), scene.timezone, coords.barInterval), chipBg, "center", true, theme.background);
|
|
28666
29328
|
}
|
|
28667
29329
|
destroy() {
|
|
28668
29330
|
this.canvas = null;
|
|
@@ -28673,23 +29335,37 @@ var CrosshairRenderer = class {
|
|
|
28673
29335
|
* along), with that bar's time chip in this chart's own timezone and — when the
|
|
28674
29336
|
* level resolved — the price chip on the right axis. The snap happened upstream
|
|
28675
29337
|
* (`externalCrossPx`, floor-to-containing-bar) — this method only draws. Chips
|
|
28676
|
-
* render slightly dimmed so the ghost still reads as foreign.
|
|
29338
|
+
* render slightly dimmed so the ghost still reads as foreign. A `crosshairOverride`
|
|
29339
|
+
* applies to the ghost as well — its line style, a dropped horizontal line, and the
|
|
29340
|
+
* `shadeRight` veil from the ghost bar's right edge — so a pick that spans several
|
|
29341
|
+
* charts reads the same on every one of them. */
|
|
28677
29342
|
drawExternal(ctx, ext, scene, coords, theme) {
|
|
28678
29343
|
const cs = scene.style.crosshair;
|
|
29344
|
+
const ov = scene.crosshairOverride;
|
|
28679
29345
|
const dataW = coords.width;
|
|
28680
29346
|
const dataH = coords.height;
|
|
28681
|
-
const x =
|
|
28682
|
-
if (
|
|
29347
|
+
const x = crisp(ext.x, ov?.width ?? cs.width);
|
|
29348
|
+
if (ov?.vertical === false) return;
|
|
29349
|
+
if (ov?.shadeRight) {
|
|
29350
|
+
const from = Math.max(0, Math.round(coords.logicalToX(Math.round(coords.xToLogical(ext.x)) + 0.5)));
|
|
29351
|
+
if (from < dataW) {
|
|
29352
|
+
ctx.fillStyle = ov.shadeRight.color;
|
|
29353
|
+
ctx.globalAlpha = ov.shadeRight.opacity ?? 1;
|
|
29354
|
+
ctx.fillRect(from, 0, dataW - from, dataH);
|
|
29355
|
+
ctx.globalAlpha = 1;
|
|
29356
|
+
}
|
|
29357
|
+
}
|
|
29358
|
+
if (ext.line === false || x < 0 || x > dataW) return;
|
|
28683
29359
|
ctx.font = `${scene.style.fontSize}px ${theme.fontFamily}`;
|
|
28684
29360
|
ctx.textBaseline = "middle";
|
|
28685
|
-
ctx.strokeStyle = cs.color ?? theme.textColor;
|
|
28686
|
-
ctx.lineWidth = cs.width;
|
|
28687
|
-
ctx.globalAlpha = cs.opacity * 0.55;
|
|
28688
|
-
setDash3(ctx, cs.style);
|
|
29361
|
+
ctx.strokeStyle = ov?.color ?? cs.color ?? theme.textColor;
|
|
29362
|
+
ctx.lineWidth = ov?.width ?? cs.width;
|
|
29363
|
+
ctx.globalAlpha = (ov?.opacity ?? cs.opacity) * 0.55;
|
|
29364
|
+
setDash3(ctx, ov?.style ?? cs.style);
|
|
28689
29365
|
ctx.beginPath();
|
|
28690
29366
|
ctx.moveTo(x, 0);
|
|
28691
29367
|
ctx.lineTo(x, dataH);
|
|
28692
|
-
if (ext.y != null) {
|
|
29368
|
+
if (ext.y != null && ov?.horizontal !== false) {
|
|
28693
29369
|
ctx.moveTo(0, Math.round(ext.y) + 0.5);
|
|
28694
29370
|
ctx.lineTo(dataW, Math.round(ext.y) + 0.5);
|
|
28695
29371
|
}
|
|
@@ -28698,7 +29374,7 @@ var CrosshairRenderer = class {
|
|
|
28698
29374
|
ctx.lineWidth = 1;
|
|
28699
29375
|
ctx.globalAlpha = 0.8;
|
|
28700
29376
|
const chipBg = cs.labelBackground ?? theme.borderColor;
|
|
28701
|
-
if (ext.y != null && ext.price != null) {
|
|
29377
|
+
if (ext.y != null && ext.price != null && ov?.horizontal !== false) {
|
|
28702
29378
|
let pane;
|
|
28703
29379
|
for (const p of scene.panes.values()) {
|
|
28704
29380
|
if (ext.y >= p.bounds.top && ext.y <= p.bounds.top + p.bounds.height) {
|
|
@@ -28738,6 +29414,9 @@ var CrosshairRenderer = class {
|
|
|
28738
29414
|
ctx.textAlign = "start";
|
|
28739
29415
|
}
|
|
28740
29416
|
};
|
|
29417
|
+
function crisp(px, width) {
|
|
29418
|
+
return Math.round(px) + (Math.round(width) % 2 === 1 ? 0.5 : 0);
|
|
29419
|
+
}
|
|
28741
29420
|
function setDash3(ctx, style) {
|
|
28742
29421
|
if (style === "dashed") ctx.setLineDash([6, 4]);
|
|
28743
29422
|
else if (style === "dotted") ctx.setLineDash([2, 3]);
|
|
@@ -36176,7 +36855,7 @@ var NativeRenderer = class {
|
|
|
36176
36855
|
this.moveIndicatorCbs = /* @__PURE__ */ new Set();
|
|
36177
36856
|
this.priceStyleCbs = /* @__PURE__ */ new Set();
|
|
36178
36857
|
this.name = "native";
|
|
36179
|
-
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"];
|
|
36858
|
+
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"];
|
|
36180
36859
|
/** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
|
|
36181
36860
|
* so moving onto the button doesn't count as leaving). */
|
|
36182
36861
|
this.onScrollProximityMove = (e) => {
|
|
@@ -36355,6 +37034,9 @@ var NativeRenderer = class {
|
|
|
36355
37034
|
this.scene.marks = mergeMarksState(this.scene.marks, value);
|
|
36356
37035
|
this.markPopover?.close();
|
|
36357
37036
|
break;
|
|
37037
|
+
case "crosshairOverride":
|
|
37038
|
+
this.scene.crosshairOverride = sanitizeCrosshairOverride(value);
|
|
37039
|
+
break;
|
|
36358
37040
|
case "keyboard":
|
|
36359
37041
|
this.setKeyboardEnabled(Boolean(value));
|
|
36360
37042
|
return;
|
|
@@ -36468,6 +37150,8 @@ var NativeRenderer = class {
|
|
|
36468
37150
|
return { ...this.scene.tradeMarkers, colors: { ...this.scene.tradeMarkers.colors } };
|
|
36469
37151
|
case "marks":
|
|
36470
37152
|
return { visible: this.scene.marks.visible, groups: { ...this.scene.marks.groups } };
|
|
37153
|
+
case "crosshairOverride":
|
|
37154
|
+
return this.scene.crosshairOverride ? { ...this.scene.crosshairOverride } : null;
|
|
36471
37155
|
case "keyboard":
|
|
36472
37156
|
return this.keyboardEnabled;
|
|
36473
37157
|
case "historyChords":
|
|
@@ -37046,8 +37730,8 @@ var NativeRenderer = class {
|
|
|
37046
37730
|
for (const pane of this.scene.panes.values()) pane.scale = { ...pane.scaleTarget };
|
|
37047
37731
|
this.modelAlpha = 0;
|
|
37048
37732
|
const start = performance.now();
|
|
37049
|
-
const step = (
|
|
37050
|
-
const p = Math.min(1, (
|
|
37733
|
+
const step = (now2) => {
|
|
37734
|
+
const p = Math.min(1, (now2 - start) / duration);
|
|
37051
37735
|
this.scene.bars = p >= 1 ? real : real.map((b, i) => this.revealCandle(b, i, p, n, style));
|
|
37052
37736
|
this.paintData();
|
|
37053
37737
|
if (p < 1) {
|
|
@@ -37063,8 +37747,8 @@ var NativeRenderer = class {
|
|
|
37063
37747
|
fadeInModels() {
|
|
37064
37748
|
const fade = Math.min(INTRO_MODEL_FADE_MS, this.intro.duration || INTRO_MODEL_FADE_MS);
|
|
37065
37749
|
const start = performance.now();
|
|
37066
|
-
const step = (
|
|
37067
|
-
this.modelAlpha = Math.min(1, (
|
|
37750
|
+
const step = (now2) => {
|
|
37751
|
+
this.modelAlpha = Math.min(1, (now2 - start) / fade);
|
|
37068
37752
|
this.paintData();
|
|
37069
37753
|
if (this.modelAlpha < 1) {
|
|
37070
37754
|
this.introRaf = requestAnimationFrame(step);
|
|
@@ -37587,10 +38271,13 @@ var NativeRenderer = class {
|
|
|
37587
38271
|
this.animator.start();
|
|
37588
38272
|
}
|
|
37589
38273
|
} else if (!last || bar.time > last.time) {
|
|
38274
|
+
const vp = this.coords.getViewport();
|
|
38275
|
+
const holdView = n > 0 && vp.rightOffset < 0 && this.scrollTargetRO === null;
|
|
37590
38276
|
this.bars.push(bar);
|
|
37591
38277
|
this.syncLiveEase(bar);
|
|
37592
38278
|
if (this.coords.barInterval > 0) this.coords.appendBar(bar.time);
|
|
37593
38279
|
else this.coords.setBars(this.bars.map((b) => b.time));
|
|
38280
|
+
if (holdView) this.coords.setViewport({ ...vp, rightOffset: vp.rightOffset - 1 });
|
|
37594
38281
|
} else {
|
|
37595
38282
|
return;
|
|
37596
38283
|
}
|
|
@@ -38272,10 +38959,10 @@ var NativeRenderer = class {
|
|
|
38272
38959
|
if (this.markPulseRaf !== null || typeof requestAnimationFrame !== "function") return;
|
|
38273
38960
|
const tick = () => {
|
|
38274
38961
|
this.markPulseRaf = null;
|
|
38275
|
-
const
|
|
38276
|
-
if (this.scene.marksFlash && this.scene.marksFlash.until <=
|
|
38962
|
+
const now2 = frameNow();
|
|
38963
|
+
if (this.scene.marksFlash && this.scene.marksFlash.until <= now2) this.scene.marksFlash = null;
|
|
38277
38964
|
this.scheduler?.invalidate(2 /* Chrome */);
|
|
38278
|
-
const pulsing = this.scene.marksHoverKey !== null &&
|
|
38965
|
+
const pulsing = this.scene.marksHoverKey !== null && now2 - this.scene.marksHoverSince < MARK_PULSE_MS;
|
|
38279
38966
|
if (pulsing || this.scene.marksFlash !== null) this.markPulseRaf = requestAnimationFrame(tick);
|
|
38280
38967
|
};
|
|
38281
38968
|
this.markPulseRaf = requestAnimationFrame(tick);
|
|
@@ -38836,9 +39523,10 @@ var NativeRenderer = class {
|
|
|
38836
39523
|
const ext = this.externalCross;
|
|
38837
39524
|
if (!ext || this.coords.barCount === 0) return null;
|
|
38838
39525
|
const logical = Math.floor(this.coords.timeToLogical(ext.time));
|
|
38839
|
-
if (logical <
|
|
38840
|
-
const x = this.coords.logicalToX(logical);
|
|
38841
|
-
if (!Number.isFinite(x)
|
|
39526
|
+
if (!(logical < this.coords.barCount)) return null;
|
|
39527
|
+
const x = this.coords.logicalToX(Math.max(logical, -1));
|
|
39528
|
+
if (!Number.isFinite(x)) return null;
|
|
39529
|
+
const line = logical >= 0 && x >= 0 && x <= this.coords.width;
|
|
38842
39530
|
let y = null;
|
|
38843
39531
|
if (ext.price != null) {
|
|
38844
39532
|
const pricePane = this.scene.orderedPanes().find((p) => p.kind === "price");
|
|
@@ -38847,7 +39535,7 @@ var NativeRenderer = class {
|
|
|
38847
39535
|
if (!Number.isFinite(y) || y < pricePane.bounds.top || y > pricePane.bounds.top + pricePane.bounds.height) y = null;
|
|
38848
39536
|
}
|
|
38849
39537
|
}
|
|
38850
|
-
return { x, y, time: this.coords.logicalToTime(logical), price: y != null ? ext.price : null };
|
|
39538
|
+
return { x, y, time: this.coords.logicalToTime(Math.max(logical, 0)), price: y != null ? ext.price : null, line };
|
|
38851
39539
|
}
|
|
38852
39540
|
/** Set the sticky magnet mode (called by the drawings toolbar's 3-state button). */
|
|
38853
39541
|
setSnapMode(mode) {
|
|
@@ -43523,6 +44211,7 @@ var Vela = class {
|
|
|
43523
44211
|
this.panesControl = new PanesControl(this.orchestrator);
|
|
43524
44212
|
this.drawingsControl = new DrawingsControl(this.orchestrator.drawings);
|
|
43525
44213
|
this.marksControl = new MarksControl(this.orchestrator.marks);
|
|
44214
|
+
this.replayControl = new ReplayControl(this.orchestrator);
|
|
43526
44215
|
}
|
|
43527
44216
|
/**
|
|
43528
44217
|
* Register a scripting engine so `addIndicator({ language })` can run that
|
|
@@ -43762,6 +44451,16 @@ var Vela = class {
|
|
|
43762
44451
|
get marks() {
|
|
43763
44452
|
return this.marksControl;
|
|
43764
44453
|
}
|
|
44454
|
+
/**
|
|
44455
|
+
* The chart's bar-replay control surface: rewind to a past bar and reveal the
|
|
44456
|
+
* following ones by hand or on a timer, from the history already loaded —
|
|
44457
|
+
* `await chart.replay.start({ from })`, `chart.replay.play(2000)`, `chart.replay.step()`,
|
|
44458
|
+
* `chart.replay.stop()`. Revealed bars behave like live bars for indicators; live
|
|
44459
|
+
* updates pause meanwhile. Follow it with the `replay:*` events.
|
|
44460
|
+
*/
|
|
44461
|
+
get replay() {
|
|
44462
|
+
return this.replayControl;
|
|
44463
|
+
}
|
|
43765
44464
|
on(event, handler) {
|
|
43766
44465
|
return this.orchestrator.events.on(event, handler);
|
|
43767
44466
|
}
|
|
@@ -43957,9 +44656,15 @@ var CSS21 = `
|
|
|
43957
44656
|
}
|
|
43958
44657
|
.vela-statusline .vela-sl-symbol { font-weight: 600; font-size: var(--vela-font-size-lg); }
|
|
43959
44658
|
.vela-statusline .vela-sl-meta { color: var(--vela-fg-muted); font-size: var(--vela-font-size-md); font-weight: 600; }
|
|
44659
|
+
/* The meta opens with "\xB7 " \u2014 sit its dot one space-width after the ticker, as far as the
|
|
44660
|
+
* venue sits after it, not a full row gap away. */
|
|
44661
|
+
.vela-statusline .vela-sl-symbol + .vela-sl-meta { margin-left: calc(var(--vela-space-1) - var(--vela-space-2)); }
|
|
43960
44662
|
/* Market status badge \u2014 a kit callout bubble (icon-only 16px circle, label on hover
|
|
43961
|
-
* via the kit tooltip); the session tint is applied per status in setMarketStatus.
|
|
43962
|
-
|
|
44663
|
+
* via the kit tooltip); the session tint is applied per status in setMarketStatus. While
|
|
44664
|
+
* the chart replays past bars it wears the replay badge instead (the inverse chip). */
|
|
44665
|
+
.vela-statusline .vela-sl-market { align-self: center; display: inline-flex; }
|
|
44666
|
+
.vela-statusline .vela-sl-market > [hidden] { display: none !important; }
|
|
44667
|
+
.vela-statusline .vela-sl-replay-badge, .vela-statusline .vela-sl-replay-badge svg { display: block; width: 16px; height: 16px; }
|
|
43963
44668
|
.vela-statusline .vela-sl-ohlc { display: flex; gap: var(--vela-space-1); color: var(--vela-fg-muted); }
|
|
43964
44669
|
.vela-statusline .vela-sl-ohlc b { color: var(--vela-fg); font-weight: 500; }
|
|
43965
44670
|
/* The change value wears the SAME ink as the OHLC values (set inline per render) \u2014
|
|
@@ -44012,6 +44717,8 @@ var MARKET_LABELS = {
|
|
|
44012
44717
|
closed: "Market Closed",
|
|
44013
44718
|
holiday: "Market Holiday"
|
|
44014
44719
|
};
|
|
44720
|
+
var REPLAY_LABEL = "Replay Mode";
|
|
44721
|
+
var REPLAY_BADGE_SVG = '<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true"><circle cx="8" cy="8" r="8" style="fill: var(--vela-selected-bg)"/><path d="M11.7 4.75v6.5L7.2 8zM7.2 4.75v6.5L2.7 8z" style="fill: var(--vela-selected-fg); stroke: var(--vela-selected-fg); stroke-width: 0.8; stroke-linejoin: round"/></svg>';
|
|
44015
44722
|
var MARKET_INKS = {
|
|
44016
44723
|
open: "var(--vela-up)",
|
|
44017
44724
|
pre: SESSION_PRE,
|
|
@@ -44050,6 +44757,9 @@ var Statusline = class {
|
|
|
44050
44757
|
this.host = host;
|
|
44051
44758
|
this.iconFor = iconFor;
|
|
44052
44759
|
this.parts = { logo: true, name: true, market: true, ohlc: true, change: true };
|
|
44760
|
+
this.marketStatus = "open";
|
|
44761
|
+
/** The chart replays past bars — the badge shows the replay mode (see {@link setReplaying}). */
|
|
44762
|
+
this.replaying = false;
|
|
44053
44763
|
/** The right-click action menu — present once a host wires it via {@link attachMenu}. */
|
|
44054
44764
|
this.menu = null;
|
|
44055
44765
|
this.menuHooks = null;
|
|
@@ -44101,8 +44811,13 @@ var Statusline = class {
|
|
|
44101
44811
|
label: MARKET_LABELS.open,
|
|
44102
44812
|
host
|
|
44103
44813
|
});
|
|
44104
|
-
this.
|
|
44105
|
-
this.
|
|
44814
|
+
this.replayBadge = doc.createElement("span");
|
|
44815
|
+
this.replayBadge.className = "vela-sl-replay-badge";
|
|
44816
|
+
this.replayBadge.innerHTML = REPLAY_BADGE_SVG;
|
|
44817
|
+
this.replayBadge.hidden = true;
|
|
44818
|
+
this.marketEl = doc.createElement("span");
|
|
44819
|
+
this.marketEl.className = "vela-sl-market";
|
|
44820
|
+
this.marketEl.append(this.marketBubble.el, this.replayBadge);
|
|
44106
44821
|
this.ohlcEl = doc.createElement("span");
|
|
44107
44822
|
this.ohlcEl.className = "vela-sl-ohlc";
|
|
44108
44823
|
this.changeEl = doc.createElement("span");
|
|
@@ -44224,6 +44939,25 @@ var Statusline = class {
|
|
|
44224
44939
|
/** Dress the market badge for a session state: its icon, tinted circle, and the
|
|
44225
44940
|
* hover label. Callers with no session model leave the constructor's 'open'. */
|
|
44226
44941
|
setMarketStatus(status) {
|
|
44942
|
+
this.marketStatus = status;
|
|
44943
|
+
this.dressBadge();
|
|
44944
|
+
}
|
|
44945
|
+
/** The chart entered or left a bar replay: the badge shows the replay mode meanwhile
|
|
44946
|
+
* (the session state of a past bar says nothing about the market now). */
|
|
44947
|
+
setReplaying(replaying) {
|
|
44948
|
+
if (replaying === this.replaying) return;
|
|
44949
|
+
this.replaying = replaying;
|
|
44950
|
+
this.dressBadge();
|
|
44951
|
+
}
|
|
44952
|
+
dressBadge() {
|
|
44953
|
+
this.marketBubble.el.hidden = this.replaying;
|
|
44954
|
+
this.replayBadge.hidden = !this.replaying;
|
|
44955
|
+
if (this.replaying) {
|
|
44956
|
+
this.marketEl.dataset.status = "replay";
|
|
44957
|
+
this.marketTip.setContent(REPLAY_LABEL);
|
|
44958
|
+
return;
|
|
44959
|
+
}
|
|
44960
|
+
const status = this.marketStatus;
|
|
44227
44961
|
this.marketEl.dataset.status = status;
|
|
44228
44962
|
const ink = MARKET_INKS[status];
|
|
44229
44963
|
this.marketBubble.set({
|
|
@@ -44296,11 +45030,23 @@ var Statusline = class {
|
|
|
44296
45030
|
this.lastBar = b;
|
|
44297
45031
|
if (!this.hoverBar) this.render();
|
|
44298
45032
|
}),
|
|
45033
|
+
// A replay cut or restore replaces the newest bar without a `bar` event.
|
|
45034
|
+
chart.on("replay:start", () => {
|
|
45035
|
+
this.lastBar = null;
|
|
45036
|
+
this.setReplaying(true);
|
|
45037
|
+
this.render();
|
|
45038
|
+
}),
|
|
45039
|
+
chart.on("replay:end", () => {
|
|
45040
|
+
this.lastBar = null;
|
|
45041
|
+
this.setReplaying(false);
|
|
45042
|
+
this.render();
|
|
45043
|
+
}),
|
|
44299
45044
|
chart.renderer.onCrosshairMove((e) => {
|
|
44300
45045
|
this.hoverBar = e.ohlc;
|
|
44301
45046
|
this.render();
|
|
44302
45047
|
})
|
|
44303
45048
|
);
|
|
45049
|
+
this.setReplaying(chart.replay.state.active);
|
|
44304
45050
|
this.render();
|
|
44305
45051
|
}
|
|
44306
45052
|
destroy() {
|
|
@@ -44541,26 +45287,26 @@ function isWeekday(ms, tz) {
|
|
|
44541
45287
|
return false;
|
|
44542
45288
|
}
|
|
44543
45289
|
}
|
|
44544
|
-
function deriveMarketStatus(
|
|
44545
|
-
const within = (ws) => ws.find(([s, e]) =>
|
|
45290
|
+
function deriveMarketStatus(now2, w, tz, overnight = false) {
|
|
45291
|
+
const within = (ws) => ws.find(([s, e]) => now2 >= s && now2 < e);
|
|
44546
45292
|
if (within(w.regular)) return "open";
|
|
44547
45293
|
const ext = within(w.extended);
|
|
44548
45294
|
if (ext) {
|
|
44549
45295
|
if (overnight) return "extended";
|
|
44550
|
-
return w.regular.some(([s]) => s >=
|
|
45296
|
+
return w.regular.some(([s]) => s >= now2 && s < ext[1]) ? "pre" : "post";
|
|
44551
45297
|
}
|
|
44552
|
-
if (isWeekday(
|
|
44553
|
-
const today = civilDate(
|
|
45298
|
+
if (isWeekday(now2, tz)) {
|
|
45299
|
+
const today = civilDate(now2, tz);
|
|
44554
45300
|
if (today !== "" && !w.extended.some(([s]) => civilDate(s, tz) === today)) return "holiday";
|
|
44555
45301
|
}
|
|
44556
45302
|
return "closed";
|
|
44557
45303
|
}
|
|
44558
|
-
function nextStatusBoundary(
|
|
45304
|
+
function nextStatusBoundary(now2, w) {
|
|
44559
45305
|
let next = null;
|
|
44560
45306
|
for (const ws of [w.regular, w.extended]) {
|
|
44561
45307
|
for (const [s, e] of ws) {
|
|
44562
45308
|
for (const t of [s, e]) {
|
|
44563
|
-
if (t >
|
|
45309
|
+
if (t > now2 && (next == null || t < next)) next = t;
|
|
44564
45310
|
}
|
|
44565
45311
|
}
|
|
44566
45312
|
}
|
|
@@ -44599,8 +45345,8 @@ var MarketStatusTracker = class {
|
|
|
44599
45345
|
return;
|
|
44600
45346
|
}
|
|
44601
45347
|
const tz = typeof si?.timezone === "string" && si.timezone !== "" ? si.timezone : "Etc/UTC";
|
|
44602
|
-
const
|
|
44603
|
-
const range = { from:
|
|
45348
|
+
const now2 = Date.now();
|
|
45349
|
+
const range = { from: now2 - FETCH_BACK_MS, to: now2 + FETCH_AHEAD_MS };
|
|
44604
45350
|
const [regular, extended] = await Promise.all([
|
|
44605
45351
|
provider.getCalendar(resolved.ticker, { ...range, session: "regular" }).catch(() => null),
|
|
44606
45352
|
provider.getCalendar(resolved.ticker, { ...range, session: "extended" }).catch(() => null)
|
|
@@ -44612,9 +45358,9 @@ var MarketStatusTracker = class {
|
|
|
44612
45358
|
}
|
|
44613
45359
|
const w = { regular, extended };
|
|
44614
45360
|
const overnight = parseSessionSpec(si)?.overnight === true;
|
|
44615
|
-
this.onStatus(deriveMarketStatus(
|
|
44616
|
-
const boundary = nextStatusBoundary(
|
|
44617
|
-
const delay = Math.min(boundary != null ? boundary -
|
|
45361
|
+
this.onStatus(deriveMarketStatus(now2, w, tz, overnight));
|
|
45362
|
+
const boundary = nextStatusBoundary(now2, w);
|
|
45363
|
+
const delay = Math.min(boundary != null ? boundary - now2 : MAX_TIMER_MS, MAX_TIMER_MS);
|
|
44618
45364
|
this.arm(my, data, symbol, Math.max(delay, MIN_TIMER_MS));
|
|
44619
45365
|
}
|
|
44620
45366
|
arm(my, data, symbol, delay) {
|
|
@@ -44647,6 +45393,7 @@ var CSS22 = `
|
|
|
44647
45393
|
left: var(--vela-toolbar-gutter, 0px);
|
|
44648
45394
|
right: var(--vela-scale-gutter, 0px);
|
|
44649
45395
|
display: flex;
|
|
45396
|
+
flex-direction: column;
|
|
44650
45397
|
align-items: center;
|
|
44651
45398
|
justify-content: center;
|
|
44652
45399
|
overflow: hidden;
|
|
@@ -44660,6 +45407,14 @@ var CSS22 = `
|
|
|
44660
45407
|
user-select: none;
|
|
44661
45408
|
white-space: nowrap;
|
|
44662
45409
|
}
|
|
45410
|
+
.vela-watermark-replay {
|
|
45411
|
+
display: inline-flex;
|
|
45412
|
+
align-items: center;
|
|
45413
|
+
gap: 0.25em;
|
|
45414
|
+
margin-top: 0.1em;
|
|
45415
|
+
font-size: 0.8em;
|
|
45416
|
+
}
|
|
45417
|
+
.vela-watermark [hidden] { display: none !important; }
|
|
44663
45418
|
`;
|
|
44664
45419
|
function watermarkFontPx(availPx, textPxAtMax) {
|
|
44665
45420
|
if (textPxAtMax <= 0) return MAX_FONT_PX;
|
|
@@ -44670,6 +45425,10 @@ var Watermark = class {
|
|
|
44670
45425
|
this.resizeObserver = null;
|
|
44671
45426
|
/** The host's visibility preference (the persisted watermark toggle). */
|
|
44672
45427
|
this.shown = true;
|
|
45428
|
+
/** The host's preference for the replay line (its own persisted toggle). */
|
|
45429
|
+
this.replayShown = true;
|
|
45430
|
+
/** The chart is replaying past bars. */
|
|
45431
|
+
this.replaying = false;
|
|
44673
45432
|
/** A bar load is in flight with nothing painted — the loading affordance owns the
|
|
44674
45433
|
* canvas, so the mark stays out of its way. Starts true: the FIRST `load:start`
|
|
44675
45434
|
* fires during chart construction, before any subscriber can see it. */
|
|
@@ -44679,7 +45438,10 @@ var Watermark = class {
|
|
|
44679
45438
|
this.el.className = "vela-watermark";
|
|
44680
45439
|
this.el.dataset.velaScreenshot = "under";
|
|
44681
45440
|
this.text = host.ownerDocument.createElement("span");
|
|
44682
|
-
this.
|
|
45441
|
+
this.replayLine = host.ownerDocument.createElement("span");
|
|
45442
|
+
this.replayLine.className = "vela-watermark-replay";
|
|
45443
|
+
this.replayLine.append(iconEl("replay", host.ownerDocument), host.ownerDocument.createTextNode("Replay"));
|
|
45444
|
+
this.el.append(this.text, this.replayLine);
|
|
44683
45445
|
host.appendChild(this.el);
|
|
44684
45446
|
if (typeof ResizeObserver !== "undefined") {
|
|
44685
45447
|
this.resizeObserver = new ResizeObserver(() => this.fit());
|
|
@@ -44692,13 +45454,26 @@ var Watermark = class {
|
|
|
44692
45454
|
this.shown = visible;
|
|
44693
45455
|
this.sync();
|
|
44694
45456
|
}
|
|
45457
|
+
/** Show/hide the replay line (the host's toggle); it only ever shows while replaying. */
|
|
45458
|
+
setReplayVisible(visible) {
|
|
45459
|
+
this.replayShown = visible;
|
|
45460
|
+
this.sync();
|
|
45461
|
+
}
|
|
45462
|
+
/** The chart entered or left a bar replay. */
|
|
45463
|
+
setReplaying(replaying) {
|
|
45464
|
+
this.replaying = replaying;
|
|
45465
|
+
this.sync();
|
|
45466
|
+
}
|
|
44695
45467
|
/** Loading and the watermark never share the canvas — hidden while a load is up. */
|
|
44696
45468
|
setLoading(loading) {
|
|
44697
45469
|
this.loading = loading;
|
|
44698
45470
|
this.sync();
|
|
44699
45471
|
}
|
|
44700
45472
|
sync() {
|
|
44701
|
-
|
|
45473
|
+
const replay = this.replayShown && this.replaying;
|
|
45474
|
+
this.text.hidden = !this.shown;
|
|
45475
|
+
this.replayLine.hidden = !replay;
|
|
45476
|
+
this.el.style.display = (this.shown || replay) && !this.loading ? "" : "none";
|
|
44702
45477
|
}
|
|
44703
45478
|
update(symbol, timeframe) {
|
|
44704
45479
|
this.text.textContent = symbol ? `${parseSymbol(symbol).ticker} \xB7 ${timeframeLabel(timeframe)}` : "";
|
|
@@ -45287,8 +46062,12 @@ var ChartCell = class {
|
|
|
45287
46062
|
this.indicatorValuesOn = seed.indicatorValues ?? true;
|
|
45288
46063
|
if (!this.indicatorValuesOn) this.inner.renderer.set("indicatorValues", false);
|
|
45289
46064
|
this.watermarkOn = seed.watermark ?? deps.watermark;
|
|
46065
|
+
this.replayWatermarkOn = seed.replayWatermark ?? true;
|
|
45290
46066
|
this.watermark = deps.watermark ? new Watermark(this.host, symbol ?? "", seed.timeframe ?? "60") : null;
|
|
45291
46067
|
if (!this.watermarkOn) this.watermark?.setVisible(false);
|
|
46068
|
+
if (!this.replayWatermarkOn) this.watermark?.setReplayVisible(false);
|
|
46069
|
+
this.inner.on("replay:start", () => this.watermark?.setReplaying(true));
|
|
46070
|
+
this.inner.on("replay:end", () => this.watermark?.setReplaying(false));
|
|
45292
46071
|
this.statusline = deps.statusline ? new Statusline(this.host, symbol ?? "", (sym) => this.inner?.data.symbolIcon(sym)) : null;
|
|
45293
46072
|
this.statusline?.setMeta(seed.timeframe ?? "60", this.state.provider ?? "");
|
|
45294
46073
|
this.statusline?.onChart(this.inner);
|
|
@@ -45483,10 +46262,10 @@ var ChartCell = class {
|
|
|
45483
46262
|
const chart = this.inner;
|
|
45484
46263
|
const symbol = this.state.symbol;
|
|
45485
46264
|
if (!chart || !symbol) return;
|
|
45486
|
-
const
|
|
46265
|
+
const now2 = Date.now();
|
|
45487
46266
|
const requestedSpan = Math.max(this.state.bars ?? 1e3, this.rangeBars) * timeframeMs(this.state.timeframe ?? "60");
|
|
45488
46267
|
const fallbackSpan = Number.isFinite(requestedSpan) ? Math.max(3 * 864e5, requestedSpan) : 3 * 864e5;
|
|
45489
|
-
const range = chart.getVisibleRange() ?? { from:
|
|
46268
|
+
const range = chart.getVisibleRange() ?? { from: now2 - fallbackSpan, to: now2 };
|
|
45490
46269
|
this.sessionShading.track(chart.data, symbol, { session: this.session, timeframe: this.timeframe, range });
|
|
45491
46270
|
}
|
|
45492
46271
|
/** The session-shade colors live in the renderer CONFIG (persisted with it, edited
|
|
@@ -45602,6 +46381,13 @@ var ChartCell = class {
|
|
|
45602
46381
|
id: "visible",
|
|
45603
46382
|
get: () => this.watermarkOn,
|
|
45604
46383
|
set: (v) => this.setWatermarkVisible(v)
|
|
46384
|
+
},
|
|
46385
|
+
{
|
|
46386
|
+
kind: "toggle",
|
|
46387
|
+
label: "Replay watermark",
|
|
46388
|
+
id: "replay",
|
|
46389
|
+
get: () => this.replayWatermarkOn,
|
|
46390
|
+
set: (v) => this.setReplayWatermarkVisible(v)
|
|
45605
46391
|
}
|
|
45606
46392
|
]
|
|
45607
46393
|
};
|
|
@@ -45670,6 +46456,12 @@ var ChartCell = class {
|
|
|
45670
46456
|
this.watermark?.setVisible(visible);
|
|
45671
46457
|
this.deps.onStateDirty();
|
|
45672
46458
|
}
|
|
46459
|
+
/** Show/hide the "Replay" line under this cell's watermark while it replays (persisted per cell). */
|
|
46460
|
+
setReplayWatermarkVisible(visible) {
|
|
46461
|
+
this.replayWatermarkOn = visible;
|
|
46462
|
+
this.watermark?.setReplayVisible(visible);
|
|
46463
|
+
this.deps.onStateDirty();
|
|
46464
|
+
}
|
|
45673
46465
|
/** Show/hide this cell's indicator titles — the in-chart legend rows (persisted per cell). */
|
|
45674
46466
|
setIndicatorTitlesVisible(visible) {
|
|
45675
46467
|
this.indicatorTitlesOn = visible;
|
|
@@ -46143,6 +46935,7 @@ var ChartCell = class {
|
|
|
46143
46935
|
if (!this.inner || this.destroyed) return;
|
|
46144
46936
|
if (cs.priceStyle && cs.priceStyle !== this.priceStyle) this.setPriceStyle(cs.priceStyle);
|
|
46145
46937
|
if (cs.watermark !== void 0 && cs.watermark !== this.watermarkOn) this.setWatermarkVisible(cs.watermark);
|
|
46938
|
+
if (cs.replayWatermark !== void 0 && cs.replayWatermark !== this.replayWatermarkOn) this.setReplayWatermarkVisible(cs.replayWatermark);
|
|
46146
46939
|
if (cs.indicatorTitles !== void 0 && cs.indicatorTitles !== this.indicatorTitlesOn) this.setIndicatorTitlesVisible(cs.indicatorTitles);
|
|
46147
46940
|
if (cs.indicatorValues !== void 0 && cs.indicatorValues !== this.indicatorValuesOn) this.setIndicatorValuesVisible(cs.indicatorValues);
|
|
46148
46941
|
if (cs.rendererConfig != null) this.inner.renderer.applyConfig(cs.rendererConfig);
|
|
@@ -46177,6 +46970,7 @@ var ChartCell = class {
|
|
|
46177
46970
|
...live ? { symbol: live.symbol, provider: live.provider, timeframe: live.timeframe } : {},
|
|
46178
46971
|
priceStyle: this.priceStyle,
|
|
46179
46972
|
watermark: this.watermarkOn,
|
|
46973
|
+
replayWatermark: this.replayWatermarkOn,
|
|
46180
46974
|
indicatorTitles: this.indicatorTitlesOn,
|
|
46181
46975
|
indicatorValues: this.indicatorValuesOn,
|
|
46182
46976
|
rendererConfig: this.inner?.renderer.getConfig() ?? void 0,
|
|
@@ -46241,6 +47035,7 @@ function buildContext(host) {
|
|
|
46241
47035
|
setPriceStyle: (style) => host.active()?.setPriceStyle(style),
|
|
46242
47036
|
openSymbolSearch: (query) => host.openSymbolSearch(query),
|
|
46243
47037
|
togglePanel: (id, open2) => host.togglePanel(id, open2),
|
|
47038
|
+
dockStrip: (el) => host.dockStrip(el),
|
|
46244
47039
|
host: host.root,
|
|
46245
47040
|
toast: (message, kind) => host.toast(message, kind),
|
|
46246
47041
|
addIndicator: (entry) => host.active()?.addExternalIndicator(entry),
|
|
@@ -46252,10 +47047,303 @@ function buildContext(host) {
|
|
|
46252
47047
|
get activeCellId() {
|
|
46253
47048
|
return host.active()?.id ?? "";
|
|
46254
47049
|
},
|
|
46255
|
-
setActiveCell: (id) => host.setActiveCell(id)
|
|
47050
|
+
setActiveCell: (id) => host.setActiveCell(id),
|
|
47051
|
+
replay: host.replay
|
|
46256
47052
|
};
|
|
46257
47053
|
}
|
|
46258
47054
|
|
|
47055
|
+
// src/workspace/WorkspaceReplay.ts
|
|
47056
|
+
var MAX_BARS_PER_STEP = 1e5;
|
|
47057
|
+
var WorkspaceReplay = class {
|
|
47058
|
+
constructor(host) {
|
|
47059
|
+
this.host = host;
|
|
47060
|
+
this.bus = new TypedEventBus();
|
|
47061
|
+
/** The running session: the shared replay time (every chart shows the bars closed by it). */
|
|
47062
|
+
this.session = null;
|
|
47063
|
+
this.playing = false;
|
|
47064
|
+
this.intervalMs = 1e3;
|
|
47065
|
+
this.timer = null;
|
|
47066
|
+
/** Cells whose replay a symbol switch ended — restarted at the shared time once their bars land. */
|
|
47067
|
+
this.rejoining = /* @__PURE__ */ new Set();
|
|
47068
|
+
this.cellSubs = /* @__PURE__ */ new Map();
|
|
47069
|
+
/** Inside a group step: chart events are the controller's own doing. */
|
|
47070
|
+
this.stepping = false;
|
|
47071
|
+
/** Inside `finish`: the charts' `replay:end` events are the controller's own doing. */
|
|
47072
|
+
this.stopping = false;
|
|
47073
|
+
/** Driving a chart's own timer: its play/pause events are not the user's. */
|
|
47074
|
+
this.muted = 0;
|
|
47075
|
+
/** A chart finished during a group step — the session ends right after it. */
|
|
47076
|
+
this.finishedDuringStep = false;
|
|
47077
|
+
for (const cell of host.cells()) this.watch(cell);
|
|
47078
|
+
this.offHost = host.onCells((e) => {
|
|
47079
|
+
if (e.kind === "created") this.onCellCreated(e.id);
|
|
47080
|
+
else if (e.kind === "destroyed") this.onCellDestroyed(e.id);
|
|
47081
|
+
});
|
|
47082
|
+
}
|
|
47083
|
+
/**
|
|
47084
|
+
* Enter replay on every cell (or seek, when already replaying) — paused. Resolves once
|
|
47085
|
+
* every chart shows its rewound history (a chart whose history does not reach that far
|
|
47086
|
+
* loads older bars first). A chart with nothing to replay after the shared time stays
|
|
47087
|
+
* as it is.
|
|
47088
|
+
*/
|
|
47089
|
+
async start(opts) {
|
|
47090
|
+
const cells = this.host.cells();
|
|
47091
|
+
const ref = cells.find((c) => c.id === (opts.cell ?? this.host.activeId())) ?? cells[0];
|
|
47092
|
+
if (!ref) return;
|
|
47093
|
+
if (this.playing) this.pause();
|
|
47094
|
+
this.rejoining.clear();
|
|
47095
|
+
await ref.chart.replay.start({ from: opts.from });
|
|
47096
|
+
const cursor = ref.chart.replay.state.cursorTime;
|
|
47097
|
+
if (cursor == null) return;
|
|
47098
|
+
const session = { clock: barClose(cursor, ref.chart.market.timeframe) };
|
|
47099
|
+
this.session = session;
|
|
47100
|
+
await Promise.all(this.host.cells().filter((c) => c.id !== ref.id).map((c) => this.startCell(c, session.clock)));
|
|
47101
|
+
if (this.session !== session) return;
|
|
47102
|
+
this.syncPlayback();
|
|
47103
|
+
const s = this.activeChart()?.replay.state;
|
|
47104
|
+
this.bus.emit("replay:start", { cursorTime: s?.cursorTime ?? cursor, remaining: s?.remaining ?? 0 });
|
|
47105
|
+
}
|
|
47106
|
+
/** Reveal the next bar(s): the clock moves to the next bar close on any chart. Returns false when nothing is left. */
|
|
47107
|
+
step() {
|
|
47108
|
+
const solo = this.solo();
|
|
47109
|
+
return solo ? solo.replay.step() : this.stepGroup();
|
|
47110
|
+
}
|
|
47111
|
+
/** Reveal the next update — in a single-chart layout, the chart's `stepUpdate()` (tick replay); otherwise {@link step}. */
|
|
47112
|
+
stepUpdate() {
|
|
47113
|
+
const solo = this.solo();
|
|
47114
|
+
return solo ? solo.replay.stepUpdate() : this.stepGroup();
|
|
47115
|
+
}
|
|
47116
|
+
/** Advance one update every `intervalMs` (default: the last pace, initially 1000). Calling it again while playing changes the pace. */
|
|
47117
|
+
play(intervalMs2) {
|
|
47118
|
+
if (!this.session) return this;
|
|
47119
|
+
if (intervalMs2 !== void 0) {
|
|
47120
|
+
if (!(Number.isFinite(intervalMs2) && intervalMs2 > 0)) {
|
|
47121
|
+
console.warn(`[vela] workspace.replay.play(${intervalMs2}) ignored \u2014 the interval must be a positive number of ms`);
|
|
47122
|
+
return this;
|
|
47123
|
+
}
|
|
47124
|
+
this.intervalMs = intervalMs2;
|
|
47125
|
+
}
|
|
47126
|
+
this.playing = true;
|
|
47127
|
+
this.clearTimer();
|
|
47128
|
+
this.syncPlayback();
|
|
47129
|
+
this.bus.emit("replay:play", { intervalMs: this.intervalMs });
|
|
47130
|
+
return this;
|
|
47131
|
+
}
|
|
47132
|
+
pause() {
|
|
47133
|
+
if (!this.playing) return this;
|
|
47134
|
+
this.playing = false;
|
|
47135
|
+
this.syncPlayback();
|
|
47136
|
+
this.bus.emit("replay:pause", void 0);
|
|
47137
|
+
return this;
|
|
47138
|
+
}
|
|
47139
|
+
/** Leave replay on every chart: full history back, live updates resumed. */
|
|
47140
|
+
stop() {
|
|
47141
|
+
this.finish("stopped");
|
|
47142
|
+
return this;
|
|
47143
|
+
}
|
|
47144
|
+
/** `active`/`playing`/`intervalMs` for the whole workspace; the cursor fields read the ACTIVE cell's chart. */
|
|
47145
|
+
get state() {
|
|
47146
|
+
const s = this.session ? this.activeChart()?.replay.state : void 0;
|
|
47147
|
+
return {
|
|
47148
|
+
active: this.session !== null,
|
|
47149
|
+
playing: this.playing,
|
|
47150
|
+
cursorTime: s?.cursorTime ?? null,
|
|
47151
|
+
remaining: s?.remaining ?? 0,
|
|
47152
|
+
nextTime: s?.nextTime ?? null,
|
|
47153
|
+
intervalMs: this.intervalMs
|
|
47154
|
+
};
|
|
47155
|
+
}
|
|
47156
|
+
/** The active cell's replayable history (`chart.replay.bounds`). */
|
|
47157
|
+
get bounds() {
|
|
47158
|
+
return this.activeChart()?.replay.bounds ?? null;
|
|
47159
|
+
}
|
|
47160
|
+
on(event, handler) {
|
|
47161
|
+
return this.bus.on(event, handler);
|
|
47162
|
+
}
|
|
47163
|
+
destroy() {
|
|
47164
|
+
this.session = null;
|
|
47165
|
+
this.playing = false;
|
|
47166
|
+
this.clearTimer();
|
|
47167
|
+
this.offHost();
|
|
47168
|
+
for (const off of this.cellSubs.values()) off();
|
|
47169
|
+
this.cellSubs.clear();
|
|
47170
|
+
this.bus.clear();
|
|
47171
|
+
}
|
|
47172
|
+
// ── internals ──
|
|
47173
|
+
/** The one chart of a single-chart layout — it replays on its own, tick replay included. */
|
|
47174
|
+
solo() {
|
|
47175
|
+
const cells = this.host.cells();
|
|
47176
|
+
return cells.length === 1 ? cells[0].chart : null;
|
|
47177
|
+
}
|
|
47178
|
+
activeChart() {
|
|
47179
|
+
const cells = this.host.cells();
|
|
47180
|
+
return (cells.find((c) => c.id === this.host.activeId()) ?? cells[0])?.chart ?? null;
|
|
47181
|
+
}
|
|
47182
|
+
/** Rewind one chart to the shared time: every bar that closed by `clock`, none after. */
|
|
47183
|
+
async startCell(cell, clock) {
|
|
47184
|
+
const replay = cell.chart.replay;
|
|
47185
|
+
const from = lastOpenClosedBy(clock, cell.chart.market.timeframe);
|
|
47186
|
+
const bounds = replay.bounds;
|
|
47187
|
+
if (!bounds || bounds.last <= from) return;
|
|
47188
|
+
await replay.start({ from });
|
|
47189
|
+
}
|
|
47190
|
+
/** Move the clock to the next bar close on any chart; each chart reveals what closed by then. */
|
|
47191
|
+
stepGroup() {
|
|
47192
|
+
const session = this.session;
|
|
47193
|
+
if (!session) return false;
|
|
47194
|
+
const cells = this.host.cells();
|
|
47195
|
+
let next = Infinity;
|
|
47196
|
+
for (const c of cells) {
|
|
47197
|
+
const t = c.chart.replay.state.nextTime;
|
|
47198
|
+
if (t != null) next = Math.min(next, barClose(t, c.chart.market.timeframe));
|
|
47199
|
+
}
|
|
47200
|
+
if (!Number.isFinite(next)) return false;
|
|
47201
|
+
this.stepping = true;
|
|
47202
|
+
try {
|
|
47203
|
+
for (const c of cells) {
|
|
47204
|
+
const tf = c.chart.market.timeframe;
|
|
47205
|
+
for (let n = 0; n < MAX_BARS_PER_STEP; n += 1) {
|
|
47206
|
+
const t = c.chart.replay.state.nextTime;
|
|
47207
|
+
if (t == null || barClose(t, tf) > next) break;
|
|
47208
|
+
if (!c.chart.replay.step()) break;
|
|
47209
|
+
}
|
|
47210
|
+
}
|
|
47211
|
+
} finally {
|
|
47212
|
+
this.stepping = false;
|
|
47213
|
+
}
|
|
47214
|
+
session.clock = next;
|
|
47215
|
+
if (this.finishedDuringStep) {
|
|
47216
|
+
this.finishedDuringStep = false;
|
|
47217
|
+
this.finish("finished");
|
|
47218
|
+
return true;
|
|
47219
|
+
}
|
|
47220
|
+
const s = this.activeChart()?.replay.state;
|
|
47221
|
+
if (s?.cursorTime != null) this.bus.emit("replay:step", { cursorTime: s.cursorTime, remaining: s.remaining });
|
|
47222
|
+
return true;
|
|
47223
|
+
}
|
|
47224
|
+
/** Put the timers in the state `playing` asks for: a single chart runs its own (it paces
|
|
47225
|
+
* ticks), several run on the controller's clock. */
|
|
47226
|
+
syncPlayback() {
|
|
47227
|
+
const solo = this.solo();
|
|
47228
|
+
const run = this.playing && this.session !== null;
|
|
47229
|
+
this.muted += 1;
|
|
47230
|
+
try {
|
|
47231
|
+
for (const c of this.host.cells()) {
|
|
47232
|
+
if (c.chart === solo && run) {
|
|
47233
|
+
if (!c.chart.replay.state.playing || c.chart.replay.state.intervalMs !== this.intervalMs) c.chart.replay.play(this.intervalMs);
|
|
47234
|
+
} else if (c.chart.replay.state.playing) {
|
|
47235
|
+
c.chart.replay.pause();
|
|
47236
|
+
}
|
|
47237
|
+
}
|
|
47238
|
+
} finally {
|
|
47239
|
+
this.muted -= 1;
|
|
47240
|
+
}
|
|
47241
|
+
if (!run || solo) this.clearTimer();
|
|
47242
|
+
else if (this.timer == null) this.schedule();
|
|
47243
|
+
}
|
|
47244
|
+
schedule() {
|
|
47245
|
+
this.timer = setTimeout(() => {
|
|
47246
|
+
this.timer = null;
|
|
47247
|
+
if (!this.playing || !this.session || this.solo()) return;
|
|
47248
|
+
if (this.stepGroup() || this.rejoining.size > 0) {
|
|
47249
|
+
if (this.playing && this.session && this.timer == null) this.schedule();
|
|
47250
|
+
} else {
|
|
47251
|
+
this.pause();
|
|
47252
|
+
}
|
|
47253
|
+
}, this.intervalMs);
|
|
47254
|
+
}
|
|
47255
|
+
clearTimer() {
|
|
47256
|
+
if (this.timer != null) clearTimeout(this.timer);
|
|
47257
|
+
this.timer = null;
|
|
47258
|
+
}
|
|
47259
|
+
/** End the session on every chart (idempotent). */
|
|
47260
|
+
finish(reason) {
|
|
47261
|
+
const had = this.session !== null;
|
|
47262
|
+
this.session = null;
|
|
47263
|
+
this.playing = false;
|
|
47264
|
+
this.clearTimer();
|
|
47265
|
+
this.rejoining.clear();
|
|
47266
|
+
this.stopping = true;
|
|
47267
|
+
try {
|
|
47268
|
+
for (const c of this.host.cells()) c.chart.replay.stop();
|
|
47269
|
+
} finally {
|
|
47270
|
+
this.stopping = false;
|
|
47271
|
+
}
|
|
47272
|
+
if (had) this.bus.emit("replay:end", { reason });
|
|
47273
|
+
}
|
|
47274
|
+
watch(cell) {
|
|
47275
|
+
const { id, chart } = cell;
|
|
47276
|
+
const isActive = () => (this.host.activeId() ?? this.host.cells()[0]?.id) === id;
|
|
47277
|
+
const offs = [
|
|
47278
|
+
chart.on("replay:end", ({ reason }) => {
|
|
47279
|
+
if (!this.session || this.stopping) return;
|
|
47280
|
+
if (reason === "market") this.rejoining.add(id);
|
|
47281
|
+
else if (this.stepping) this.finishedDuringStep || (this.finishedDuringStep = reason === "finished");
|
|
47282
|
+
else this.finish(reason);
|
|
47283
|
+
}),
|
|
47284
|
+
chart.on("market:changed", () => {
|
|
47285
|
+
if (!this.rejoining.delete(id) || !this.session) return;
|
|
47286
|
+
void this.startCell(cell, this.session.clock).then(() => this.syncPlayback());
|
|
47287
|
+
}),
|
|
47288
|
+
chart.on("replay:step", (e) => {
|
|
47289
|
+
if (this.session && !this.stepping && isActive()) this.bus.emit("replay:step", e);
|
|
47290
|
+
}),
|
|
47291
|
+
chart.on("replay:tick", (e) => {
|
|
47292
|
+
if (this.session && isActive()) this.bus.emit("replay:tick", e);
|
|
47293
|
+
}),
|
|
47294
|
+
// A single chart runs its own timer: a pause it takes on its own (a seek) is the session's.
|
|
47295
|
+
chart.on("replay:pause", () => {
|
|
47296
|
+
if (!this.session || this.muted > 0 || !this.playing || chart !== this.solo()) return;
|
|
47297
|
+
this.playing = false;
|
|
47298
|
+
this.bus.emit("replay:pause", void 0);
|
|
47299
|
+
})
|
|
47300
|
+
];
|
|
47301
|
+
this.cellSubs.set(id, () => {
|
|
47302
|
+
for (const off of offs) off();
|
|
47303
|
+
});
|
|
47304
|
+
}
|
|
47305
|
+
onCellCreated(id) {
|
|
47306
|
+
const cell = this.host.cells().find((c) => c.id === id);
|
|
47307
|
+
if (!cell) return;
|
|
47308
|
+
this.watch(cell);
|
|
47309
|
+
const session = this.session;
|
|
47310
|
+
if (!session) return;
|
|
47311
|
+
this.rejoining.add(id);
|
|
47312
|
+
void cell.chart.ready().then(async () => {
|
|
47313
|
+
if (this.session !== session || !this.rejoining.delete(id)) return;
|
|
47314
|
+
await this.startCell(cell, session.clock);
|
|
47315
|
+
this.syncPlayback();
|
|
47316
|
+
});
|
|
47317
|
+
this.syncPlayback();
|
|
47318
|
+
}
|
|
47319
|
+
onCellDestroyed(id) {
|
|
47320
|
+
this.cellSubs.get(id)?.();
|
|
47321
|
+
this.cellSubs.delete(id);
|
|
47322
|
+
this.rejoining.delete(id);
|
|
47323
|
+
this.syncPlayback();
|
|
47324
|
+
}
|
|
47325
|
+
};
|
|
47326
|
+
function barClose(open2, timeframe) {
|
|
47327
|
+
const months = monthSpan(timeframe);
|
|
47328
|
+
if (months > 0) {
|
|
47329
|
+
const d = new Date(open2);
|
|
47330
|
+
return Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + months, d.getUTCDate(), d.getUTCHours(), d.getUTCMinutes());
|
|
47331
|
+
}
|
|
47332
|
+
return open2 + timeframeToMs(timeframe ?? "60");
|
|
47333
|
+
}
|
|
47334
|
+
function lastOpenClosedBy(time, timeframe) {
|
|
47335
|
+
const months = monthSpan(timeframe);
|
|
47336
|
+
if (months > 0) {
|
|
47337
|
+
const d = new Date(time);
|
|
47338
|
+
return Date.UTC(d.getUTCFullYear(), d.getUTCMonth() - months, d.getUTCDate(), d.getUTCHours(), d.getUTCMinutes());
|
|
47339
|
+
}
|
|
47340
|
+
return time - timeframeToMs(timeframe ?? "60");
|
|
47341
|
+
}
|
|
47342
|
+
function monthSpan(timeframe) {
|
|
47343
|
+
const m = /^(\d*)M$/.exec((timeframe ?? "").trim());
|
|
47344
|
+
return m ? Number(m[1] || 1) : 0;
|
|
47345
|
+
}
|
|
47346
|
+
|
|
46259
47347
|
// src/workspace/layouts.ts
|
|
46260
47348
|
var registry5 = /* @__PURE__ */ new Map();
|
|
46261
47349
|
function registerLayout(def) {
|
|
@@ -46560,6 +47648,8 @@ var CSS24 = `
|
|
|
46560
47648
|
.vela-workspace { position: relative; width: 100%; height: 100%; display: flex; flex-direction: column; background: var(--vela-bg); }
|
|
46561
47649
|
.vela-ws-main { position: relative; display: flex; flex-direction: row; flex: 1 1 auto; min-height: 0; }
|
|
46562
47650
|
.vela-ws-toolbar { position: relative; flex: none; }
|
|
47651
|
+
.vela-ws-strips { position: relative; flex: none; display: flex; flex-direction: column; min-width: 0; }
|
|
47652
|
+
.vela-ws-strips:empty { display: none; }
|
|
46563
47653
|
.vela-ws-grid { position: relative; flex: 1 1 auto; min-width: 0; display: grid; gap: ${GAP_PX}px; background: var(--vela-border-soft); }
|
|
46564
47654
|
.vela-cell { background: var(--vela-bg); position: relative; }
|
|
46565
47655
|
/* Active-cell highlight: an overlay ring ABOVE the chart's own canvas stack (a plain
|
|
@@ -46731,6 +47821,20 @@ var VelaWorkspace = class {
|
|
|
46731
47821
|
const hostEl = typeof container === "string" ? document.querySelector(container) : container;
|
|
46732
47822
|
if (!hostEl) throw new Error(`VelaWorkspace: container not found: ${String(container)}`);
|
|
46733
47823
|
this.opts = opts;
|
|
47824
|
+
this.replay = new WorkspaceReplay({
|
|
47825
|
+
cells: () => this.cells(),
|
|
47826
|
+
activeId: () => this.activeId,
|
|
47827
|
+
onCells: (handler) => {
|
|
47828
|
+
const offs = [
|
|
47829
|
+
this.events.on("cell:created", ({ id }) => handler({ kind: "created", id })),
|
|
47830
|
+
this.events.on("cell:destroyed", ({ id }) => handler({ kind: "destroyed", id })),
|
|
47831
|
+
this.events.on("cell:active", ({ id }) => handler({ kind: "active", id }))
|
|
47832
|
+
];
|
|
47833
|
+
return () => {
|
|
47834
|
+
for (const off of offs) off();
|
|
47835
|
+
};
|
|
47836
|
+
}
|
|
47837
|
+
});
|
|
46734
47838
|
this.persistKey = opts.persist === void 0 || opts.persist === false ? null : opts.persist === true ? "vela-workspace" : opts.persist;
|
|
46735
47839
|
this.storage = opts.storage ?? localStorageAdapter();
|
|
46736
47840
|
let boot = null;
|
|
@@ -46868,6 +47972,9 @@ var VelaWorkspace = class {
|
|
|
46868
47972
|
this.dock.refresh();
|
|
46869
47973
|
if (boot?.panels) this.dock.applyState(boot.panels);
|
|
46870
47974
|
this.root.appendChild(main);
|
|
47975
|
+
this.stripsEl = doc.createElement("div");
|
|
47976
|
+
this.stripsEl.className = "vela-ws-strips";
|
|
47977
|
+
this.root.appendChild(this.stripsEl);
|
|
46871
47978
|
this.toastHost = new Toast(this.gridEl);
|
|
46872
47979
|
const attribution = rendererDefaults().attribution;
|
|
46873
47980
|
if (attribution !== false) {
|
|
@@ -47102,9 +48209,16 @@ var VelaWorkspace = class {
|
|
|
47102
48209
|
setActiveCell: (id) => this.setActiveCell(id),
|
|
47103
48210
|
openSymbolSearch: (query) => this.symbolPicker.open(query ?? ""),
|
|
47104
48211
|
togglePanel: (id, open2) => this.dock.toggle(id, open2),
|
|
48212
|
+
dockStrip: (el) => {
|
|
48213
|
+
this.stripsEl.appendChild(el);
|
|
48214
|
+
return () => {
|
|
48215
|
+
if (el.parentElement === this.stripsEl) el.remove();
|
|
48216
|
+
};
|
|
48217
|
+
},
|
|
47105
48218
|
root: this.root,
|
|
47106
48219
|
toast: (message, kind) => this.toastHost.show(message, kind),
|
|
47107
|
-
stateDirty: () => this.markStateDirty()
|
|
48220
|
+
stateDirty: () => this.markStateDirty(),
|
|
48221
|
+
replay: this.replay
|
|
47108
48222
|
});
|
|
47109
48223
|
}
|
|
47110
48224
|
/** Re-project contributed topbar actions + side panels, and mount late-registered attachments. */
|
|
@@ -47412,6 +48526,7 @@ var VelaWorkspace = class {
|
|
|
47412
48526
|
if (this.destroyed) return;
|
|
47413
48527
|
this.flushPendingState();
|
|
47414
48528
|
this.destroyed = true;
|
|
48529
|
+
this.replay.destroy();
|
|
47415
48530
|
if (this.persistKey !== null && typeof window !== "undefined") window.removeEventListener("beforeunload", this.onUnload);
|
|
47416
48531
|
this.resizeObserver?.disconnect();
|
|
47417
48532
|
this.splitters.destroy();
|
|
@@ -48178,14 +49293,13 @@ var VelaWorkspace = class {
|
|
|
48178
49293
|
panels: () => has("panels") ? [...this.dock.list()] : [],
|
|
48179
49294
|
onTogglePanel: (id) => this.dock.toggle(id),
|
|
48180
49295
|
...has("alerts") ? { alerts: () => this.alerts.map((a) => ({ title: `${a.source} \xB7 ${a.title}`, message: a.message, time: a.time })) } : {},
|
|
48181
|
-
//
|
|
48182
|
-
//
|
|
48183
|
-
//
|
|
48184
|
-
//
|
|
48185
|
-
|
|
48186
|
-
|
|
48187
|
-
|
|
48188
|
-
},
|
|
49296
|
+
// Actions placed on the bottom bar (left-aligned by default) have their stop
|
|
49297
|
+
// there — only the menu-placed ones land in the drawer, or they would appear
|
|
49298
|
+
// twice: left (primary) ones with the primary rows, right ones at the end.
|
|
49299
|
+
// Built-in-id actions are slot OVERRIDES: they reach the drawer through the
|
|
49300
|
+
// slot's own routed button (screenshot) or stop (indicators), never as a row.
|
|
49301
|
+
primaryActions: () => this.drawerActions("left"),
|
|
49302
|
+
actions: () => this.drawerActions("right"),
|
|
48189
49303
|
// The desktop layout dropdown's whole surface — the grid canvas, the
|
|
48190
49304
|
// non-canvas presets and the sync switches — relocated into the kebab
|
|
48191
49305
|
// drawer (the topbar is hidden on mobile). Same reads as the topbar block;
|
|
@@ -48210,6 +49324,11 @@ var VelaWorkspace = class {
|
|
|
48210
49324
|
}));
|
|
48211
49325
|
this.moreDrawer.open();
|
|
48212
49326
|
}
|
|
49327
|
+
/** The contributed topbar actions the mobile menu lists, from one cluster. */
|
|
49328
|
+
drawerActions(cluster) {
|
|
49329
|
+
const builtin = new Set(TOPBAR_BUILTIN_IDS);
|
|
49330
|
+
return widgetActions("topbar", this.context()).filter((a) => mobilePlacement(a) === "menu" && a.align === "left" === (cluster === "left") && !builtin.has(a.id)).map((a) => ({ label: a.label, icon: a.icon, run: () => a.run(this.context()) }));
|
|
49331
|
+
}
|
|
48213
49332
|
openTimezoneDrawer() {
|
|
48214
49333
|
this.timezoneDrawer ?? (this.timezoneDrawer = new TimezoneDrawer({
|
|
48215
49334
|
host: this.root,
|
|
@@ -48376,12 +49495,15 @@ var VelaWorkspace = class {
|
|
|
48376
49495
|
exports.ChartCell = ChartCell;
|
|
48377
49496
|
exports.GRID_PICKER_MAX = GRID_PICKER_MAX;
|
|
48378
49497
|
exports.VelaWorkspace = VelaWorkspace;
|
|
49498
|
+
exports.WorkspaceReplay = WorkspaceReplay;
|
|
48379
49499
|
exports.activeAfterLayout = activeAfterLayout;
|
|
49500
|
+
exports.barClose = barClose;
|
|
48380
49501
|
exports.decodeState = decodeState;
|
|
48381
49502
|
exports.encodeState = encodeState;
|
|
48382
49503
|
exports.ensureLayout = ensureLayout;
|
|
48383
49504
|
exports.evenTracks = evenTracks;
|
|
48384
49505
|
exports.gridStyles = gridStyles;
|
|
49506
|
+
exports.lastOpenClosedBy = lastOpenClosedBy;
|
|
48385
49507
|
exports.layoutDefinition = layoutDefinition;
|
|
48386
49508
|
exports.layoutForGrid = layoutForGrid;
|
|
48387
49509
|
exports.layoutShape = layoutShape;
|