@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/widget.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 = `
|
|
@@ -4967,6 +5226,9 @@ function pinnedTopbarActionIds(comp) {
|
|
|
4967
5226
|
}
|
|
4968
5227
|
|
|
4969
5228
|
// src/widget/contributions.ts
|
|
5229
|
+
function mobilePlacement(action) {
|
|
5230
|
+
return action.mobile ?? (action.align === "left" ? "bar" : "menu");
|
|
5231
|
+
}
|
|
4970
5232
|
var registry3 = /* @__PURE__ */ new Map();
|
|
4971
5233
|
var attachments = /* @__PURE__ */ new Map();
|
|
4972
5234
|
var panels = /* @__PURE__ */ new Map();
|
|
@@ -5204,6 +5466,22 @@ function priceStyleIds() {
|
|
|
5204
5466
|
for (const t of chartTypes()) if (!out.includes(t.id)) out.push(t.id);
|
|
5205
5467
|
return out;
|
|
5206
5468
|
}
|
|
5469
|
+
function sanitizeCrosshairOverride(value) {
|
|
5470
|
+
if (!value || typeof value !== "object") return null;
|
|
5471
|
+
const v = value;
|
|
5472
|
+
const out = {};
|
|
5473
|
+
if (isBool(v.vertical)) out.vertical = v.vertical;
|
|
5474
|
+
if (isBool(v.horizontal)) out.horizontal = v.horizontal;
|
|
5475
|
+
if (isColor(v.color)) out.color = v.color;
|
|
5476
|
+
if (isNum(v.width)) out.width = Math.max(0.5, Math.min(8, v.width));
|
|
5477
|
+
if (isLineStyle(v.style)) out.style = v.style;
|
|
5478
|
+
if (isNum(v.opacity)) out.opacity = clampOpacity(v.opacity);
|
|
5479
|
+
const shade = v.shadeRight;
|
|
5480
|
+
if (shade && typeof shade === "object" && isColor(shade.color)) {
|
|
5481
|
+
out.shadeRight = isNum(shade.opacity) ? { color: shade.color, opacity: clampOpacity(shade.opacity) } : { color: shade.color };
|
|
5482
|
+
}
|
|
5483
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
5484
|
+
}
|
|
5207
5485
|
function isColor(v) {
|
|
5208
5486
|
return typeof v === "string" && v.trim().length > 0;
|
|
5209
5487
|
}
|
|
@@ -6091,8 +6369,8 @@ var Topbar = class {
|
|
|
6091
6369
|
// src/core/util/wall-clock.ts
|
|
6092
6370
|
var BOUNDARY_SLACK_MS = 5;
|
|
6093
6371
|
var SecondClock = class _SecondClock {
|
|
6094
|
-
constructor(
|
|
6095
|
-
this.now =
|
|
6372
|
+
constructor(now2 = () => Date.now()) {
|
|
6373
|
+
this.now = now2;
|
|
6096
6374
|
this.subs = /* @__PURE__ */ new Set();
|
|
6097
6375
|
this.timer = null;
|
|
6098
6376
|
}
|
|
@@ -6108,15 +6386,15 @@ var SecondClock = class _SecondClock {
|
|
|
6108
6386
|
};
|
|
6109
6387
|
}
|
|
6110
6388
|
/** Milliseconds from `now` to just past the next second boundary. */
|
|
6111
|
-
static delayToNextSecond(
|
|
6112
|
-
const intoSecond = (
|
|
6389
|
+
static delayToNextSecond(now2) {
|
|
6390
|
+
const intoSecond = (now2 % 1e3 + 1e3) % 1e3;
|
|
6113
6391
|
return 1e3 - intoSecond + BOUNDARY_SLACK_MS;
|
|
6114
6392
|
}
|
|
6115
6393
|
arm() {
|
|
6116
6394
|
this.timer = setTimeout(() => {
|
|
6117
6395
|
this.timer = null;
|
|
6118
|
-
const
|
|
6119
|
-
for (const cb of this.subs) cb(
|
|
6396
|
+
const now2 = this.now();
|
|
6397
|
+
for (const cb of this.subs) cb(now2);
|
|
6120
6398
|
if (this.subs.size > 0) this.arm();
|
|
6121
6399
|
}, _SecondClock.delayToNextSecond(this.now()));
|
|
6122
6400
|
}
|
|
@@ -15370,16 +15648,16 @@ var MobileBar = class {
|
|
|
15370
15648
|
host.appendChild(this.el);
|
|
15371
15649
|
this.renderActions();
|
|
15372
15650
|
}
|
|
15373
|
-
/** Re-project the
|
|
15374
|
-
* indicators slot (call after
|
|
15375
|
-
*
|
|
15651
|
+
/** Re-project the contributed actions placed on the bar (left-aligned by default —
|
|
15652
|
+
* see `mobilePlacement`) as icon-only stops in the indicators slot (call after
|
|
15653
|
+
* registrations change). The rest are three-dots drawer rows. */
|
|
15376
15654
|
renderActions() {
|
|
15377
15655
|
const ctx = this.opts.getContext?.();
|
|
15378
15656
|
if (!ctx) return;
|
|
15379
15657
|
const doc = this.el.ownerDocument;
|
|
15380
15658
|
this.actionsHost.replaceChildren();
|
|
15381
15659
|
const builtin = new Set(TOPBAR_BUILTIN_IDS);
|
|
15382
|
-
for (const action of widgetActions("topbar", ctx).filter((a) => a
|
|
15660
|
+
for (const action of widgetActions("topbar", ctx).filter((a) => mobilePlacement(a) === "bar" && !builtin.has(a.id))) {
|
|
15383
15661
|
const b = doc.createElement("button");
|
|
15384
15662
|
b.className = "vela-mb-item";
|
|
15385
15663
|
b.setAttribute("aria-label", action.label);
|
|
@@ -15935,6 +16213,7 @@ var MoreDrawer = class {
|
|
|
15935
16213
|
const value = shape ? `${shape.cols} \xD7 ${shape.rows}` : this.opts.layout.presets().find((p) => p.checked)?.label;
|
|
15936
16214
|
list.appendChild(this.row(doc, "Layout", { icon: "layout", value, chevron: true, onClick: () => this.show("layout") }));
|
|
15937
16215
|
}
|
|
16216
|
+
for (const act of this.opts.primaryActions?.() ?? []) list.appendChild(this.actionRow(doc, act));
|
|
15938
16217
|
for (const panel of this.opts.panels()) {
|
|
15939
16218
|
list.appendChild(
|
|
15940
16219
|
this.row(doc, panel.title, {
|
|
@@ -15950,19 +16229,19 @@ var MoreDrawer = class {
|
|
|
15950
16229
|
const alertCount = this.opts.alerts().length;
|
|
15951
16230
|
list.appendChild(this.row(doc, "Alerts", { icon: "bell", value: alertCount > 0 ? String(alertCount) : void 0, chevron: true, onClick: () => this.show("alerts") }));
|
|
15952
16231
|
}
|
|
15953
|
-
for (const act of this.opts.actions())
|
|
15954
|
-
list.appendChild(
|
|
15955
|
-
this.row(doc, act.label, {
|
|
15956
|
-
icon: act.icon,
|
|
15957
|
-
onClick: () => {
|
|
15958
|
-
act.run();
|
|
15959
|
-
this.drawer.hide();
|
|
15960
|
-
}
|
|
15961
|
-
})
|
|
15962
|
-
);
|
|
15963
|
-
}
|
|
16232
|
+
for (const act of this.opts.actions()) list.appendChild(this.actionRow(doc, act));
|
|
15964
16233
|
this.drawer.body.appendChild(list);
|
|
15965
16234
|
}
|
|
16235
|
+
/** A contributed action's row: runs it and closes the drawer. */
|
|
16236
|
+
actionRow(doc, act) {
|
|
16237
|
+
return this.row(doc, act.label, {
|
|
16238
|
+
icon: act.icon,
|
|
16239
|
+
onClick: () => {
|
|
16240
|
+
act.run();
|
|
16241
|
+
this.drawer.hide();
|
|
16242
|
+
}
|
|
16243
|
+
});
|
|
16244
|
+
}
|
|
15966
16245
|
renderStyle(doc) {
|
|
15967
16246
|
const list = doc.createElement("div");
|
|
15968
16247
|
list.className = "vela-md-list";
|
|
@@ -18336,6 +18615,7 @@ function sanitizeCell(raw) {
|
|
|
18336
18615
|
if (typeof c.bars === "number" && Number.isFinite(c.bars) && c.bars > 0) out.bars = c.bars;
|
|
18337
18616
|
if (c.session === "regular" || c.session === "extended") out.session = c.session;
|
|
18338
18617
|
if (typeof c.watermark === "boolean") out.watermark = c.watermark;
|
|
18618
|
+
if (typeof c.replayWatermark === "boolean") out.replayWatermark = c.replayWatermark;
|
|
18339
18619
|
if (typeof c.indicatorTitles === "boolean") out.indicatorTitles = c.indicatorTitles;
|
|
18340
18620
|
if (typeof c.indicatorValues === "boolean") out.indicatorValues = c.indicatorValues;
|
|
18341
18621
|
if (c.rendererConfig != null && typeof c.rendererConfig === "object") out.rendererConfig = c.rendererConfig;
|
|
@@ -19074,6 +19354,7 @@ var CHUNK_BARS = 1e4;
|
|
|
19074
19354
|
var FIRST_PAINT_BARS = 20;
|
|
19075
19355
|
var GAP_FACTOR = 1.5;
|
|
19076
19356
|
var HEAL_COOLDOWN_MS = 5e3;
|
|
19357
|
+
var MIN_TICK_UPDATE_MS = 16;
|
|
19077
19358
|
var EngineOrchestrator = class _EngineOrchestrator {
|
|
19078
19359
|
constructor(container, renderer, feed, engines, config, dataControl) {
|
|
19079
19360
|
this.renderer = renderer;
|
|
@@ -19126,6 +19407,32 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19126
19407
|
this.lastHealAt = 0;
|
|
19127
19408
|
/** The `visibilitychange` catch-up listener (removed on destroy). */
|
|
19128
19409
|
this.onVisible = null;
|
|
19410
|
+
// ── bar replay: `rawBars` holds the history up to the cursor, the rest waits here ──
|
|
19411
|
+
/** Bars still hidden right of the replay cursor, oldest first; null ≡ replay off. */
|
|
19412
|
+
this.replayQueue = null;
|
|
19413
|
+
this.replayTimer = null;
|
|
19414
|
+
this.replayPlaying = false;
|
|
19415
|
+
this.replayIntervalMs = 1e3;
|
|
19416
|
+
/** The depth (`market.bars`) before a replay deepened the history — restored when it ends. */
|
|
19417
|
+
this.replayDepth = null;
|
|
19418
|
+
/** A start is loading older history before it can cut; `seek` = a replay was running
|
|
19419
|
+
* (and left silently to deepen), so abandoning the load must announce its end. */
|
|
19420
|
+
this.replayLoading = null;
|
|
19421
|
+
/** A replay carried over a timeframe/session switch, between the old series' silent end
|
|
19422
|
+
* and the cut of the new one: still ACTIVE for the outside (`replayState`), resumed
|
|
19423
|
+
* at `revealedEnd` once the new bars land, unless stopped (`cancelled`) meanwhile. */
|
|
19424
|
+
this.replayCarrying = null;
|
|
19425
|
+
/** Intrabar prices for the bars replay reveals — null ≡ whole bars. */
|
|
19426
|
+
this.replayTickSource = null;
|
|
19427
|
+
/** The bar being revealed tick by tick: its stored form (what it settles on), its ticks,
|
|
19428
|
+
* the next tick to apply, the candle built so far, the volume each tick adds when the
|
|
19429
|
+
* ticks carry none, and its pacing (ticks per update, ms between updates — one play
|
|
19430
|
+
* interval per tick — and the clock time tick 0 is due at, so late timers catch up). */
|
|
19431
|
+
this.replayForming = null;
|
|
19432
|
+
/** Tick requests by bar time, shared by prefetch and playback; `ticks` once they landed. */
|
|
19433
|
+
this.replayTickFetches = /* @__PURE__ */ new Map();
|
|
19434
|
+
/** Aborts the tick requests in flight — renewed whenever the replay moves on. */
|
|
19435
|
+
this.replayTickAbort = null;
|
|
19129
19436
|
/** What the last applied bar did: refine the open candle, or open a new one (which
|
|
19130
19437
|
* settles the previous). Read by the `script:run` cause attribution. */
|
|
19131
19438
|
this.barCause = "tick";
|
|
@@ -19349,6 +19656,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19349
19656
|
const paint = (bars, final) => {
|
|
19350
19657
|
if (this.generation !== gen || !final && bars.length === 0) return;
|
|
19351
19658
|
if (!painted && !final && bars.length < Math.min(requested, FIRST_PAINT_BARS)) return;
|
|
19659
|
+
if (painted && this.replayQueue) {
|
|
19660
|
+
this.mergeHistoryIntoReplay(bars);
|
|
19661
|
+
return;
|
|
19662
|
+
}
|
|
19352
19663
|
this.setBarSeries(bars, painted ? { preserveView: true } : void 0);
|
|
19353
19664
|
if (!painted && bars.length > 0) {
|
|
19354
19665
|
painted = true;
|
|
@@ -19491,6 +19802,36 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19491
19802
|
// exactly like a timeframe change; the cache keys the sessions apart.
|
|
19492
19803
|
next.session !== void 0 && next.session !== m.session || next.data !== void 0;
|
|
19493
19804
|
const depthChanged = next.bars !== void 0 && next.bars !== m.bars;
|
|
19805
|
+
const sameClock = identityChanged && next.data === void 0 && (next.symbol === void 0 || next.symbol === m.symbol) && (next.timeframe !== void 0 || next.session !== void 0);
|
|
19806
|
+
const pending = identityChanged ? this.replayCarrying : null;
|
|
19807
|
+
if (pending) {
|
|
19808
|
+
this.replayCarrying = null;
|
|
19809
|
+
if (!sameClock) {
|
|
19810
|
+
pending.cancelled = true;
|
|
19811
|
+
this.events.emit("replay:end", { reason: "market" });
|
|
19812
|
+
}
|
|
19813
|
+
}
|
|
19814
|
+
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;
|
|
19815
|
+
if (pending) pending.cancelled = true;
|
|
19816
|
+
if (replayCarry) this.replayCarrying = replayCarry;
|
|
19817
|
+
if (replayCarry && this.replayQueue) {
|
|
19818
|
+
this.endReplay(null, { restore: false, resume: false });
|
|
19819
|
+
const depth = this.replayDepth;
|
|
19820
|
+
this.replayDepth = null;
|
|
19821
|
+
if (depth && next.bars === void 0) {
|
|
19822
|
+
if (depth.bars === void 0) delete m.bars;
|
|
19823
|
+
else m.bars = depth.bars;
|
|
19824
|
+
}
|
|
19825
|
+
}
|
|
19826
|
+
if (this.replayQueue && (identityChanged || depthChanged)) this.endReplay("market", { restore: !identityChanged, resume: false });
|
|
19827
|
+
if (this.replayLoading && (identityChanged || depthChanged)) {
|
|
19828
|
+
const depth = this.replayDepth;
|
|
19829
|
+
this.abandonReplayLoad("market");
|
|
19830
|
+
if (depth && next.bars === void 0) {
|
|
19831
|
+
if (depth.bars === void 0) delete m.bars;
|
|
19832
|
+
else m.bars = depth.bars;
|
|
19833
|
+
}
|
|
19834
|
+
}
|
|
19494
19835
|
const depthOnly = depthChanged && !identityChanged && this.rawBars.length > 0 && !m.data?.length && ((next.bars ?? 0) <= this.rawBars.length || typeof this.feed.loadRange === "function");
|
|
19495
19836
|
if (!identityChanged && !depthChanged) {
|
|
19496
19837
|
if (typeof next.visibleRange === "string") this.setVisibleRangePreset(next.visibleRange);
|
|
@@ -19548,15 +19889,20 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19548
19889
|
const from = bars[Math.max(0, idx - (carriedCount - 1))].time;
|
|
19549
19890
|
this.renderer.setVisibleRange({ from, to: Math.max(carried.right, bars[idx].time + 1) });
|
|
19550
19891
|
}
|
|
19551
|
-
|
|
19892
|
+
const carry = replayCarry && !replayCarry.cancelled ? replayCarry : null;
|
|
19893
|
+
const carryFrom = carry ? carry.revealedEnd - this.barIntervalMs() : 0;
|
|
19894
|
+
const carryNow = carry != null && this.rawBars.length > 0 && carryFrom >= this.rawBars[0].time;
|
|
19895
|
+
const replayBack = carryNow && this.resumeCarriedReplay(this.replayStart({ from: carryFrom }), carry);
|
|
19896
|
+
if (!depthOnly && !replayBack) {
|
|
19552
19897
|
this.restartNativeIndicators();
|
|
19553
19898
|
this.restartChartTypeEngine();
|
|
19554
19899
|
this.reexecuteIndicators();
|
|
19555
19900
|
}
|
|
19556
|
-
this.startLive();
|
|
19901
|
+
if (!replayBack) this.startLive();
|
|
19557
19902
|
if (identityChanged) {
|
|
19558
19903
|
this.events.emit("market:changed", { symbol: m.symbol ?? "TEST", timeframe: m.timeframe ?? "60", prev: prev2 });
|
|
19559
19904
|
}
|
|
19905
|
+
if (carry && !carryNow) this.resumeCarriedReplay(this.replayStart({ from: carryFrom }), carry);
|
|
19560
19906
|
}
|
|
19561
19907
|
/**
|
|
19562
19908
|
* Restart every native indicator over the new market: a `NativeIndicatorContext`
|
|
@@ -19680,6 +20026,397 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19680
20026
|
historyComplete() {
|
|
19681
20027
|
return this.historyCompletePromise;
|
|
19682
20028
|
}
|
|
20029
|
+
// ── bar replay ──────────────────────────────────────────────
|
|
20030
|
+
/**
|
|
20031
|
+
* Enter replay — or seek, when already replaying — keeping every bar opened at or before
|
|
20032
|
+
* `opts.from` and queueing the rest. A `from` inside the loaded bars cuts at once, even
|
|
20033
|
+
* while older history is still streaming in: what lands later joins the tape's head
|
|
20034
|
+
* (see {@link mergeHistoryIntoReplay}) and leaves the cut alone. An older `from` waits
|
|
20035
|
+
* for that load, then deepens if it still has to. Consumers restart over the cut exactly
|
|
20036
|
+
* as on a market switch — their models describe bars that are now hidden — and the
|
|
20037
|
+
* active chart-type engine is rebuilt without live data.
|
|
20038
|
+
*/
|
|
20039
|
+
async replayStart(opts) {
|
|
20040
|
+
if (this.rawBars.length === 0) {
|
|
20041
|
+
console.warn("[vela] chart.replay.start() ignored \u2014 the chart has no bars yet");
|
|
20042
|
+
return;
|
|
20043
|
+
}
|
|
20044
|
+
let gen = this.generation;
|
|
20045
|
+
if (opts.from < this.rawBars[0].time) {
|
|
20046
|
+
await this.historyCompletePromise;
|
|
20047
|
+
if (this.generation !== gen || this.rawBars.length === 0) return;
|
|
20048
|
+
}
|
|
20049
|
+
const oldest = this.rawBars[0].time;
|
|
20050
|
+
if (opts.from < oldest && this.canHeal() && typeof this.feed.loadRange === "function") {
|
|
20051
|
+
const held = this.rawBars.length + (this.replayQueue?.length ?? 0);
|
|
20052
|
+
const bars = held + Math.ceil((oldest - opts.from) / this.barIntervalMs()) + 1;
|
|
20053
|
+
const seek = this.replayQueue !== null;
|
|
20054
|
+
if (seek) this.endReplay(null, { restore: true, resume: false });
|
|
20055
|
+
this.replayDepth ?? (this.replayDepth = { bars: this.config.market.bars });
|
|
20056
|
+
const deepened = this.setMarket({ bars });
|
|
20057
|
+
this.replayLoading = { seek };
|
|
20058
|
+
await deepened;
|
|
20059
|
+
gen = this.generation;
|
|
20060
|
+
await this.historyCompletePromise;
|
|
20061
|
+
if (!this.replayLoading || this.generation !== gen || this.rawBars.length === 0) return;
|
|
20062
|
+
this.replayLoading = null;
|
|
20063
|
+
}
|
|
20064
|
+
this.settleFormingBar();
|
|
20065
|
+
this.dropTickFetches();
|
|
20066
|
+
const tape = this.replayQueue ? [...this.rawBars, ...this.replayQueue] : this.rawBars;
|
|
20067
|
+
let cut = 0;
|
|
20068
|
+
while (cut < tape.length && tape[cut].time <= opts.from) cut += 1;
|
|
20069
|
+
cut = Math.max(1, cut);
|
|
20070
|
+
if (cut >= tape.length) {
|
|
20071
|
+
console.warn("[vela] chart.replay.start() ignored \u2014 no bar opens after the requested time");
|
|
20072
|
+
return;
|
|
20073
|
+
}
|
|
20074
|
+
const wasPlaying = this.replayPlaying;
|
|
20075
|
+
this.clearReplayTimer();
|
|
20076
|
+
if (!this.replayQueue) this.stopLive();
|
|
20077
|
+
this.replayQueue = tape.slice(cut);
|
|
20078
|
+
this.setBarSeries(tape.slice(0, cut));
|
|
20079
|
+
this.restartBarConsumers({ blank: true });
|
|
20080
|
+
if (wasPlaying) this.events.emit("replay:pause", void 0);
|
|
20081
|
+
this.events.emit("replay:start", { cursorTime: tape[cut - 1].time, remaining: this.replayQueue.length });
|
|
20082
|
+
this.prefetchTicks();
|
|
20083
|
+
}
|
|
20084
|
+
/** Reveal the next queued bar through the live-bar path — or complete the bar forming
|
|
20085
|
+
* tick by tick; the last one ends the replay. */
|
|
20086
|
+
replayStep() {
|
|
20087
|
+
if (this.replayForming) {
|
|
20088
|
+
this.finishFormingBar();
|
|
20089
|
+
return true;
|
|
20090
|
+
}
|
|
20091
|
+
const queue = this.replayQueue;
|
|
20092
|
+
const bar = queue?.shift();
|
|
20093
|
+
if (!queue || !bar) return false;
|
|
20094
|
+
this.replayTickFetches.delete(bar.time);
|
|
20095
|
+
this.applyBar(bar);
|
|
20096
|
+
this.completeReveal(bar);
|
|
20097
|
+
this.prefetchTicks();
|
|
20098
|
+
return true;
|
|
20099
|
+
}
|
|
20100
|
+
/**
|
|
20101
|
+
* Follow up a replay carried onto a new series of the same symbol (another timeframe or
|
|
20102
|
+
* session). The cut asked for keeps the bars that CLOSED by the end of what was revealed —
|
|
20103
|
+
* every finer bar of the last revealed one, never a coarser bar still open at that point
|
|
20104
|
+
* (no look-ahead). Once it settles: playback resumes if it was on; if the replay did not
|
|
20105
|
+
* come back (nothing left after that point, or a later switch took over), it ends as a
|
|
20106
|
+
* market switch would. Returns whether the replay is back already — a start with no
|
|
20107
|
+
* history to wait for cuts synchronously (an async function runs up to its first await).
|
|
20108
|
+
*/
|
|
20109
|
+
resumeCarriedReplay(start, carry) {
|
|
20110
|
+
if (this.replayQueue && this.replayCarrying === carry) this.replayCarrying = null;
|
|
20111
|
+
void start.then(() => {
|
|
20112
|
+
if (this.replayCarrying === carry) this.replayCarrying = null;
|
|
20113
|
+
if (carry.cancelled) return;
|
|
20114
|
+
if (!this.replayQueue) this.events.emit("replay:end", { reason: "market" });
|
|
20115
|
+
else if (carry.playing) this.replayPlay();
|
|
20116
|
+
});
|
|
20117
|
+
return this.replayQueue !== null;
|
|
20118
|
+
}
|
|
20119
|
+
replayStepUpdate() {
|
|
20120
|
+
if (!this.replayQueue) return false;
|
|
20121
|
+
if (this.replayForming) {
|
|
20122
|
+
this.applyReplayTicks(1);
|
|
20123
|
+
return true;
|
|
20124
|
+
}
|
|
20125
|
+
const bar = this.replayQueue[0];
|
|
20126
|
+
if (!bar) return false;
|
|
20127
|
+
const entry = this.replayTickSource ? this.requestTicks(bar) : null;
|
|
20128
|
+
if (entry && !entry.ticks) {
|
|
20129
|
+
const epoch = this.replayTickAbort;
|
|
20130
|
+
void entry.done.then(() => {
|
|
20131
|
+
if (!this.replayPlaying && !this.replayForming && this.replayTickAbort === epoch && this.replayQueue?.[0] === bar) this.replayStepUpdate();
|
|
20132
|
+
});
|
|
20133
|
+
return true;
|
|
20134
|
+
}
|
|
20135
|
+
if (!entry?.ticks?.length) return this.replayStep();
|
|
20136
|
+
this.openFormingBar(entry.ticks, 1);
|
|
20137
|
+
return true;
|
|
20138
|
+
}
|
|
20139
|
+
replaySetTicks(source) {
|
|
20140
|
+
this.finishFormingBar();
|
|
20141
|
+
this.dropTickFetches();
|
|
20142
|
+
this.replayTickSource = source;
|
|
20143
|
+
this.prefetchTicks();
|
|
20144
|
+
}
|
|
20145
|
+
replayPlay(intervalMs2) {
|
|
20146
|
+
if (!this.replayQueue) return;
|
|
20147
|
+
if (intervalMs2 !== void 0) {
|
|
20148
|
+
if (!(Number.isFinite(intervalMs2) && intervalMs2 > 0)) {
|
|
20149
|
+
console.warn(`[vela] chart.replay.play(${intervalMs2}) ignored \u2014 the interval must be a positive number of ms`);
|
|
20150
|
+
return;
|
|
20151
|
+
}
|
|
20152
|
+
this.replayIntervalMs = intervalMs2;
|
|
20153
|
+
}
|
|
20154
|
+
this.clearReplayTimer();
|
|
20155
|
+
this.replayPlaying = true;
|
|
20156
|
+
if (this.replayForming) this.paceFormingBar(this.replayForming);
|
|
20157
|
+
this.scheduleReplayStep(this.replayForming?.delayMs);
|
|
20158
|
+
this.events.emit("replay:play", { intervalMs: this.replayIntervalMs });
|
|
20159
|
+
}
|
|
20160
|
+
replayPause() {
|
|
20161
|
+
if (!this.replayPlaying) return;
|
|
20162
|
+
this.clearReplayTimer();
|
|
20163
|
+
this.events.emit("replay:pause", void 0);
|
|
20164
|
+
}
|
|
20165
|
+
replayStop() {
|
|
20166
|
+
const carrying = this.replayCarrying;
|
|
20167
|
+
if (carrying) {
|
|
20168
|
+
carrying.cancelled = true;
|
|
20169
|
+
this.replayCarrying = null;
|
|
20170
|
+
this.events.emit("replay:end", { reason: "stopped" });
|
|
20171
|
+
}
|
|
20172
|
+
if (this.replayLoading) {
|
|
20173
|
+
const depth = this.replayDepth;
|
|
20174
|
+
this.abandonReplayLoad("stopped");
|
|
20175
|
+
void this.setMarket({ bars: depth?.bars ?? 500 });
|
|
20176
|
+
return;
|
|
20177
|
+
}
|
|
20178
|
+
this.endReplay("stopped", { restore: true, resume: true });
|
|
20179
|
+
}
|
|
20180
|
+
abandonReplayLoad(reason) {
|
|
20181
|
+
const loading = this.replayLoading;
|
|
20182
|
+
if (!loading) return;
|
|
20183
|
+
this.replayLoading = null;
|
|
20184
|
+
this.replayDepth = null;
|
|
20185
|
+
if (loading.seek) this.events.emit("replay:end", { reason });
|
|
20186
|
+
}
|
|
20187
|
+
replayState() {
|
|
20188
|
+
const active = this.replayQueue !== null;
|
|
20189
|
+
return {
|
|
20190
|
+
active: active || this.replayCarrying !== null,
|
|
20191
|
+
// a replay carried over a switch stays on while the new bars load
|
|
20192
|
+
playing: this.replayCarrying?.playing ?? this.replayPlaying,
|
|
20193
|
+
cursorTime: active ? this.rawBars[this.rawBars.length - 1]?.time ?? null : null,
|
|
20194
|
+
remaining: this.replayQueue?.length ?? 0,
|
|
20195
|
+
nextTime: this.replayForming?.bar.time ?? this.replayQueue?.[0]?.time ?? null,
|
|
20196
|
+
intervalMs: this.replayIntervalMs
|
|
20197
|
+
};
|
|
20198
|
+
}
|
|
20199
|
+
replayBounds() {
|
|
20200
|
+
const first = this.rawBars[0];
|
|
20201
|
+
const last = this.replayQueue?.[this.replayQueue.length - 1] ?? this.rawBars[this.rawBars.length - 1];
|
|
20202
|
+
return first && last ? { first: first.time, last: last.time } : null;
|
|
20203
|
+
}
|
|
20204
|
+
scheduleReplayStep(delayMs = this.replayIntervalMs) {
|
|
20205
|
+
this.replayTimer = setTimeout(() => {
|
|
20206
|
+
this.replayTimer = null;
|
|
20207
|
+
this.replayAdvance();
|
|
20208
|
+
}, delayMs);
|
|
20209
|
+
}
|
|
20210
|
+
/** One beat of timed playback: the forming bar's next ticks, else the next bar — whole,
|
|
20211
|
+
* or opened from its ticks (waiting for them when they have not landed yet). */
|
|
20212
|
+
replayAdvance() {
|
|
20213
|
+
if (!this.replayPlaying || !this.replayQueue) return;
|
|
20214
|
+
const forming = this.replayForming;
|
|
20215
|
+
if (forming) {
|
|
20216
|
+
const due = Math.floor((now() - forming.origin) / this.replayIntervalMs) + 1;
|
|
20217
|
+
this.applyReplayTicks(Math.max(forming.batch, due - forming.next));
|
|
20218
|
+
if (this.replayPlaying) this.scheduleReplayStep(forming.delayMs);
|
|
20219
|
+
return;
|
|
20220
|
+
}
|
|
20221
|
+
const bar = this.replayQueue[0];
|
|
20222
|
+
if (!bar) return;
|
|
20223
|
+
const entry = this.replayTickSource ? this.requestTicks(bar) : null;
|
|
20224
|
+
if (entry && !entry.ticks) {
|
|
20225
|
+
const epoch = this.replayTickAbort;
|
|
20226
|
+
void entry.done.then(() => {
|
|
20227
|
+
if (this.replayPlaying && this.replayTimer == null && this.replayTickAbort === epoch && this.replayQueue?.[0] === bar) this.replayAdvance();
|
|
20228
|
+
});
|
|
20229
|
+
return;
|
|
20230
|
+
}
|
|
20231
|
+
if (!entry?.ticks?.length) {
|
|
20232
|
+
if (this.replayStep() && this.replayPlaying) this.scheduleReplayStep();
|
|
20233
|
+
return;
|
|
20234
|
+
}
|
|
20235
|
+
this.openFormingBar(entry.ticks);
|
|
20236
|
+
if (this.replayPlaying) this.scheduleReplayStep(this.replayForming?.delayMs);
|
|
20237
|
+
}
|
|
20238
|
+
/** Take the next queued bar off the tape and open it at its first tick (the first
|
|
20239
|
+
* `count` ticks — default: one timed update's worth). */
|
|
20240
|
+
openFormingBar(ticks, count) {
|
|
20241
|
+
const bar = this.replayQueue.shift();
|
|
20242
|
+
const volumeEach = ticks.every((t) => t.volume === void 0) ? (bar.volume ?? 0) / ticks.length : null;
|
|
20243
|
+
const forming = { bar, ticks, next: 0, candle: null, volumeEach, batch: 1, delayMs: this.replayIntervalMs, origin: 0 };
|
|
20244
|
+
this.paceFormingBar(forming);
|
|
20245
|
+
this.replayForming = forming;
|
|
20246
|
+
this.applyReplayTicks(count ?? forming.batch);
|
|
20247
|
+
this.prefetchTicks();
|
|
20248
|
+
}
|
|
20249
|
+
/** One tick per interval, batched when the interval is shorter than a frame; the clock
|
|
20250
|
+
* is anchored on the ticks already applied (a resume or a pace change carries on from
|
|
20251
|
+
* there, never jumps). */
|
|
20252
|
+
paceFormingBar(f) {
|
|
20253
|
+
f.batch = Math.max(1, Math.ceil(MIN_TICK_UPDATE_MS / this.replayIntervalMs));
|
|
20254
|
+
f.delayMs = this.replayIntervalMs * f.batch;
|
|
20255
|
+
f.origin = now() - Math.max(0, f.next - 1) * this.replayIntervalMs;
|
|
20256
|
+
}
|
|
20257
|
+
/** Fold the next `count` ticks into the forming candle; the last one settles the bar. */
|
|
20258
|
+
applyReplayTicks(count) {
|
|
20259
|
+
const f = this.replayForming;
|
|
20260
|
+
if (!f) return;
|
|
20261
|
+
let c = f.candle;
|
|
20262
|
+
const stop = Math.min(f.ticks.length, f.next + count);
|
|
20263
|
+
for (; f.next < stop; f.next += 1) {
|
|
20264
|
+
const t = f.ticks[f.next];
|
|
20265
|
+
const v = f.volumeEach ?? t.volume ?? 0;
|
|
20266
|
+
const hi = Math.max(t.high ?? t.price, t.price);
|
|
20267
|
+
const lo = Math.min(t.low ?? t.price, t.price);
|
|
20268
|
+
if (c) {
|
|
20269
|
+
c = { ...c, high: Math.max(c.high, hi), low: Math.min(c.low, lo), close: t.price, volume: (c.volume ?? 0) + v };
|
|
20270
|
+
} else {
|
|
20271
|
+
const open2 = t.open ?? t.price;
|
|
20272
|
+
c = { time: f.bar.time, open: open2, high: Math.max(open2, hi), low: Math.min(open2, lo), close: t.price, volume: v };
|
|
20273
|
+
}
|
|
20274
|
+
}
|
|
20275
|
+
f.candle = c;
|
|
20276
|
+
if (f.next >= f.ticks.length) {
|
|
20277
|
+
this.finishFormingBar();
|
|
20278
|
+
return;
|
|
20279
|
+
}
|
|
20280
|
+
this.applyBar(c);
|
|
20281
|
+
this.events.emit("replay:tick", { cursorTime: f.bar.time, index: f.next - 1, count: f.ticks.length });
|
|
20282
|
+
}
|
|
20283
|
+
/** Settle the forming bar on its stored values and announce it revealed. */
|
|
20284
|
+
finishFormingBar() {
|
|
20285
|
+
const f = this.replayForming;
|
|
20286
|
+
if (!f) return;
|
|
20287
|
+
this.replayForming = null;
|
|
20288
|
+
this.replayTickFetches.delete(f.bar.time);
|
|
20289
|
+
this.applyBar(f.bar);
|
|
20290
|
+
this.events.emit("replay:tick", { cursorTime: f.bar.time, index: f.ticks.length - 1, count: f.ticks.length });
|
|
20291
|
+
this.completeReveal(f.bar);
|
|
20292
|
+
}
|
|
20293
|
+
/** Put the forming bar back in its stored form, silently — a seek or an end rebuilds
|
|
20294
|
+
* from the tape, which must hold the stored bar. */
|
|
20295
|
+
settleFormingBar() {
|
|
20296
|
+
const f = this.replayForming;
|
|
20297
|
+
if (!f) return;
|
|
20298
|
+
this.replayForming = null;
|
|
20299
|
+
this.applyBar(f.bar);
|
|
20300
|
+
}
|
|
20301
|
+
/**
|
|
20302
|
+
* A history answer landing mid-replay (a progressive snapshot — the whole answer so far):
|
|
20303
|
+
* the tape keeps its cut and its hidden queue; bars older than the tape join its head,
|
|
20304
|
+
* bars newer than it join the queue's end.
|
|
20305
|
+
*/
|
|
20306
|
+
mergeHistoryIntoReplay(bars) {
|
|
20307
|
+
const queue = this.replayQueue;
|
|
20308
|
+
const head = this.rawBars[0];
|
|
20309
|
+
if (!queue || !head) return;
|
|
20310
|
+
const tail = queue[queue.length - 1] ?? this.rawBars[this.rawBars.length - 1];
|
|
20311
|
+
let older = 0;
|
|
20312
|
+
while (older < bars.length && bars[older].time < head.time) older += 1;
|
|
20313
|
+
let newer = bars.length;
|
|
20314
|
+
while (newer > older && bars[newer - 1].time > tail.time) newer -= 1;
|
|
20315
|
+
if (newer < bars.length) queue.push(...bars.slice(newer));
|
|
20316
|
+
if (older > 0) {
|
|
20317
|
+
this.setBarSeries([...bars.slice(0, older), ...this.rawBars], { preserveView: true });
|
|
20318
|
+
this.notifySessionsBars("backfill");
|
|
20319
|
+
}
|
|
20320
|
+
}
|
|
20321
|
+
/** A bar is fully on screen: announce it, and end the replay if it was the last. */
|
|
20322
|
+
completeReveal(bar) {
|
|
20323
|
+
const queue = this.replayQueue;
|
|
20324
|
+
if (!queue) return;
|
|
20325
|
+
this.events.emit("replay:step", { cursorTime: bar.time, remaining: queue.length });
|
|
20326
|
+
if (queue.length === 0) this.endReplay("finished", { restore: true, resume: true });
|
|
20327
|
+
}
|
|
20328
|
+
/** Ask for the ticks of the next hidden bar ahead of time. */
|
|
20329
|
+
prefetchTicks() {
|
|
20330
|
+
const next = this.replayQueue?.[0];
|
|
20331
|
+
if (next && this.replayTickSource) this.requestTicks(next);
|
|
20332
|
+
}
|
|
20333
|
+
/** The (shared) tick request of a queued bar — the source runs once per bar. */
|
|
20334
|
+
requestTicks(bar) {
|
|
20335
|
+
const known = this.replayTickFetches.get(bar.time);
|
|
20336
|
+
if (known) return known;
|
|
20337
|
+
const source = this.replayTickSource;
|
|
20338
|
+
this.replayTickAbort ?? (this.replayTickAbort = new AbortController());
|
|
20339
|
+
const signal = this.replayTickAbort.signal;
|
|
20340
|
+
const queue = this.replayQueue ?? [];
|
|
20341
|
+
const end = queue[queue.indexOf(bar) + 1]?.time ?? bar.time + this.barIntervalMs();
|
|
20342
|
+
const entry = { ticks: null, done: Promise.resolve() };
|
|
20343
|
+
const land = (ticks) => {
|
|
20344
|
+
entry.ticks = usableTicks(ticks);
|
|
20345
|
+
};
|
|
20346
|
+
const fail = (e) => {
|
|
20347
|
+
if (!signal.aborted) console.warn(`[vela] replay tick source failed \u2014 revealing the bar whole (${e instanceof Error ? e.message : String(e)})`);
|
|
20348
|
+
land([]);
|
|
20349
|
+
};
|
|
20350
|
+
try {
|
|
20351
|
+
const out = source(bar, { end, signal });
|
|
20352
|
+
if (Array.isArray(out)) land(out);
|
|
20353
|
+
else entry.done = Promise.resolve(out).then(land, fail);
|
|
20354
|
+
} catch (e) {
|
|
20355
|
+
fail(e);
|
|
20356
|
+
}
|
|
20357
|
+
this.replayTickFetches.set(bar.time, entry);
|
|
20358
|
+
return entry;
|
|
20359
|
+
}
|
|
20360
|
+
/** Abort the tick requests in flight and forget the landed ones. */
|
|
20361
|
+
dropTickFetches() {
|
|
20362
|
+
this.replayTickAbort?.abort();
|
|
20363
|
+
this.replayTickAbort = null;
|
|
20364
|
+
this.replayTickFetches.clear();
|
|
20365
|
+
}
|
|
20366
|
+
clearReplayTimer() {
|
|
20367
|
+
if (this.replayTimer != null) clearTimeout(this.replayTimer);
|
|
20368
|
+
this.replayTimer = null;
|
|
20369
|
+
this.replayPlaying = false;
|
|
20370
|
+
}
|
|
20371
|
+
/**
|
|
20372
|
+
* Leave replay. `restore` puts the queued bars back and restarts the consumers over the
|
|
20373
|
+
* full history (a market switch skips it: its load replaces everything anyway); `resume`
|
|
20374
|
+
* restarts live updates and heals the bars that closed while replaying. A null reason
|
|
20375
|
+
* leaves silently — a seek that re-enters right after.
|
|
20376
|
+
*/
|
|
20377
|
+
endReplay(reason, opts) {
|
|
20378
|
+
const queue = this.replayQueue;
|
|
20379
|
+
if (!queue) return;
|
|
20380
|
+
this.clearReplayTimer();
|
|
20381
|
+
if (opts.restore) this.settleFormingBar();
|
|
20382
|
+
else this.replayForming = null;
|
|
20383
|
+
this.dropTickFetches();
|
|
20384
|
+
this.replayQueue = null;
|
|
20385
|
+
const depth = reason ? this.replayDepth : null;
|
|
20386
|
+
if (reason) this.replayDepth = null;
|
|
20387
|
+
if (depth) {
|
|
20388
|
+
if (depth.bars === void 0) delete this.config.market.bars;
|
|
20389
|
+
else this.config.market.bars = depth.bars;
|
|
20390
|
+
}
|
|
20391
|
+
if (opts.restore) {
|
|
20392
|
+
const keep = depth ? depth.bars ?? 500 : Infinity;
|
|
20393
|
+
if (queue.length > 0 || this.rawBars.length > keep) {
|
|
20394
|
+
const full = [...this.rawBars, ...queue];
|
|
20395
|
+
this.setBarSeries(full.length > keep ? full.slice(full.length - keep) : full);
|
|
20396
|
+
this.restartBarConsumers({ blank: false });
|
|
20397
|
+
} else if (this.config.live) {
|
|
20398
|
+
this.restartNativeIndicators();
|
|
20399
|
+
this.restartChartTypeEngine();
|
|
20400
|
+
}
|
|
20401
|
+
}
|
|
20402
|
+
if (opts.resume && this.config.live) {
|
|
20403
|
+
this.startLive();
|
|
20404
|
+
const last = this.rawBars[this.rawBars.length - 1];
|
|
20405
|
+
if (last && this.canHeal()) void this.healGap(last.time);
|
|
20406
|
+
}
|
|
20407
|
+
if (reason) this.events.emit("replay:end", { reason });
|
|
20408
|
+
}
|
|
20409
|
+
/**
|
|
20410
|
+
* The bars were replaced by a cut or a restore of the SAME market: every consumer re-runs.
|
|
20411
|
+
* A cut also blanks the mounted models first — they still paint the hidden bars' values,
|
|
20412
|
+
* which must not show until the re-runs land.
|
|
20413
|
+
*/
|
|
20414
|
+
restartBarConsumers(opts) {
|
|
20415
|
+
if (opts.blank) this.blankIndicatorVisuals();
|
|
20416
|
+
this.restartNativeIndicators();
|
|
20417
|
+
this.restartChartTypeEngine();
|
|
20418
|
+
this.reexecuteIndicators();
|
|
20419
|
+
}
|
|
19683
20420
|
/** The primary symbol as the data layer resolves it, or null if no symbol is set. */
|
|
19684
20421
|
qualifiedSymbol() {
|
|
19685
20422
|
const market = this.config.market;
|
|
@@ -19751,7 +20488,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19751
20488
|
engine.start({
|
|
19752
20489
|
symbol: this.config.market.symbol ?? "TEST",
|
|
19753
20490
|
timeframe: this.config.market.timeframe ?? "60",
|
|
19754
|
-
live: this.config.live ?? false,
|
|
20491
|
+
live: (this.config.live ?? false) && this.replayQueue === null,
|
|
19755
20492
|
session: this.config.market.session,
|
|
19756
20493
|
bars: () => this.bars,
|
|
19757
20494
|
data: this.dataControl,
|
|
@@ -19877,6 +20614,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19877
20614
|
record.session.notifyBars(reason);
|
|
19878
20615
|
} else if (record.native) record.native.instance.onBars();
|
|
19879
20616
|
}
|
|
20617
|
+
if (this.activeEngineStyle) this.typeEngines.get(this.activeEngineStyle)?.onBars?.();
|
|
19880
20618
|
}
|
|
19881
20619
|
/** Gap healing needs a ranged feed and a provider-backed series (offline `data` has no source). */
|
|
19882
20620
|
canHeal() {
|
|
@@ -19899,10 +20637,11 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19899
20637
|
try {
|
|
19900
20638
|
const bars = await this.feed.loadRange(this.config.market, { from: fromMs });
|
|
19901
20639
|
if (this.generation !== gen) return;
|
|
20640
|
+
if (this.replayQueue) return;
|
|
19902
20641
|
for (const b of bars) this.applyBar(b, false);
|
|
19903
20642
|
} catch {
|
|
19904
20643
|
} finally {
|
|
19905
|
-
if (this.generation === gen) {
|
|
20644
|
+
if (this.generation === gen && !this.replayQueue) {
|
|
19906
20645
|
this.healing = false;
|
|
19907
20646
|
const pending = this.healBuffer;
|
|
19908
20647
|
this.healBuffer = [];
|
|
@@ -20207,6 +20946,13 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
20207
20946
|
destroy() {
|
|
20208
20947
|
this.bumpGeneration();
|
|
20209
20948
|
this.resolveHistoryComplete();
|
|
20949
|
+
this.clearReplayTimer();
|
|
20950
|
+
this.replayQueue = null;
|
|
20951
|
+
this.replayForming = null;
|
|
20952
|
+
this.replayCarrying = null;
|
|
20953
|
+
this.dropTickFetches();
|
|
20954
|
+
this.replayDepth = null;
|
|
20955
|
+
this.replayLoading = null;
|
|
20210
20956
|
this.stopLive();
|
|
20211
20957
|
if (this.viewportTimer != null) clearTimeout(this.viewportTimer);
|
|
20212
20958
|
this.viewportUnsub?.();
|
|
@@ -20305,6 +21051,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
20305
21051
|
this.emitScriptRun(id, cause, first);
|
|
20306
21052
|
},
|
|
20307
21053
|
onAlert: (a) => {
|
|
21054
|
+
if (this.replayQueue || this.replayCarrying) return;
|
|
20308
21055
|
const indicator = record.options?.title ?? record.prepared?.meta.title ?? record.title;
|
|
20309
21056
|
this.events.emit("alert", { ...a, indicator });
|
|
20310
21057
|
handle.emit("alert", { id: a.id, message: a.message, title: a.title, time: a.time });
|
|
@@ -20331,7 +21078,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
20331
21078
|
id,
|
|
20332
21079
|
symbol: this.config.market.symbol ?? "TEST",
|
|
20333
21080
|
timeframe: this.config.market.timeframe ?? "60",
|
|
20334
|
-
live: this.config.live,
|
|
21081
|
+
live: this.config.live && this.replayQueue === null,
|
|
20335
21082
|
session: this.config.market.session,
|
|
20336
21083
|
bars: () => this.bars,
|
|
20337
21084
|
data: this.dataControl,
|
|
@@ -20734,17 +21481,17 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
20734
21481
|
}
|
|
20735
21482
|
emitContextChanged(id) {
|
|
20736
21483
|
if (!this.registry.get(id)?.session?.getContext) return;
|
|
20737
|
-
const
|
|
20738
|
-
if (
|
|
20739
|
-
this.contextEmitAt.set(id,
|
|
21484
|
+
const now2 = Date.now();
|
|
21485
|
+
if (now2 - (this.contextEmitAt.get(id) ?? 0) < 1e3) return;
|
|
21486
|
+
this.contextEmitAt.set(id, now2);
|
|
20740
21487
|
this.events.emit("context:changed", { id });
|
|
20741
21488
|
}
|
|
20742
21489
|
emitScriptRun(id, cause, first) {
|
|
20743
21490
|
const record = this.registry.get(id);
|
|
20744
21491
|
if (!record || record.native || !this.events.hasListeners("script:run")) return;
|
|
20745
|
-
const
|
|
20746
|
-
if (cause === "tick" &&
|
|
20747
|
-
this.runEmitAt.set(id,
|
|
21492
|
+
const now2 = Date.now();
|
|
21493
|
+
if (cause === "tick" && now2 - (this.runEmitAt.get(id) ?? 0) < RUN_EMIT_THROTTLE_MS) return;
|
|
21494
|
+
this.runEmitAt.set(id, now2);
|
|
20748
21495
|
void Promise.resolve().then(() => this.buildScriptRun(record, cause, first)).then((run) => {
|
|
20749
21496
|
if (run && this.registry.get(id)) this.events.emit("script:run", run);
|
|
20750
21497
|
});
|
|
@@ -20797,6 +21544,16 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
20797
21544
|
handle?.emit("error", { error });
|
|
20798
21545
|
}
|
|
20799
21546
|
};
|
|
21547
|
+
function now() {
|
|
21548
|
+
return typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
21549
|
+
}
|
|
21550
|
+
function usableTicks(value) {
|
|
21551
|
+
if (!Array.isArray(value)) return [];
|
|
21552
|
+
const optional = (n) => n === void 0 || Number.isFinite(n);
|
|
21553
|
+
return value.filter(
|
|
21554
|
+
(t) => t != null && Number.isFinite(t.price) && optional(t.volume) && optional(t.high) && optional(t.low) && optional(t.open)
|
|
21555
|
+
);
|
|
21556
|
+
}
|
|
20800
21557
|
function yieldToPaint() {
|
|
20801
21558
|
if (typeof requestAnimationFrame === "function") {
|
|
20802
21559
|
return new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
|
|
@@ -20997,6 +21754,12 @@ var RendererControl = class {
|
|
|
20997
21754
|
onCrosshairMove(cb) {
|
|
20998
21755
|
return this.renderer.onCrosshairMove(cb);
|
|
20999
21756
|
}
|
|
21757
|
+
/** A click — or a touch tap — on the plot, never the end of a pan: the open time of
|
|
21758
|
+
* the bar under it (`null` off the bars). On touch, where a tap moves no crosshair,
|
|
21759
|
+
* this is how an interaction learns which bar was chosen. */
|
|
21760
|
+
onClick(cb) {
|
|
21761
|
+
return this.renderer.onClick(cb);
|
|
21762
|
+
}
|
|
21000
21763
|
/** Touch long-press on a price or time axis strip — silent no-op without the seam. */
|
|
21001
21764
|
onAxisLongPress(cb) {
|
|
21002
21765
|
return this.renderer.onAxisLongPress?.(cb) ?? (() => void 0);
|
|
@@ -21495,6 +22258,71 @@ var MarksControl = class {
|
|
|
21495
22258
|
}
|
|
21496
22259
|
};
|
|
21497
22260
|
|
|
22261
|
+
// src/core/ReplayControl.ts
|
|
22262
|
+
var ReplayControl = class {
|
|
22263
|
+
constructor(ctrl) {
|
|
22264
|
+
this.ctrl = ctrl;
|
|
22265
|
+
}
|
|
22266
|
+
/**
|
|
22267
|
+
* Enter replay (or seek, when already replaying) at `opts.from`. Resolves once the
|
|
22268
|
+
* chart shows the rewound history — after any in-flight history load has finished.
|
|
22269
|
+
* Starts paused; call {@link play} or {@link step}.
|
|
22270
|
+
*/
|
|
22271
|
+
start(opts) {
|
|
22272
|
+
return this.ctrl.replayStart(opts);
|
|
22273
|
+
}
|
|
22274
|
+
/** Reveal the next bar — or, mid-way through a bar played tick by tick, complete it.
|
|
22275
|
+
* Returns false when replay is off or nothing is left. */
|
|
22276
|
+
step() {
|
|
22277
|
+
return this.ctrl.replayStep();
|
|
22278
|
+
}
|
|
22279
|
+
/** Reveal the next UPDATE: with {@link setTicks}, the next tick of the forming bar — or
|
|
22280
|
+
* the next bar opened at its first tick (once its ticks are in, if still loading);
|
|
22281
|
+
* otherwise the next whole bar, like {@link step}. Returns false when nothing is left. */
|
|
22282
|
+
stepUpdate() {
|
|
22283
|
+
return this.ctrl.replayStepUpdate();
|
|
22284
|
+
}
|
|
22285
|
+
/** Reveal one bar (one tick, with {@link setTicks}) every `intervalMs` (default: the last
|
|
22286
|
+
* interval used, else 1000). Calling it again while playing changes the pace. */
|
|
22287
|
+
play(intervalMs2) {
|
|
22288
|
+
this.ctrl.replayPlay(intervalMs2);
|
|
22289
|
+
return this;
|
|
22290
|
+
}
|
|
22291
|
+
pause() {
|
|
22292
|
+
this.ctrl.replayPause();
|
|
22293
|
+
return this;
|
|
22294
|
+
}
|
|
22295
|
+
/** Leave replay: the full history comes back and live updates resume. Also cancels a
|
|
22296
|
+
* `start()` still loading older history (the chart returns to its previous depth). */
|
|
22297
|
+
stop() {
|
|
22298
|
+
this.ctrl.replayStop();
|
|
22299
|
+
return this;
|
|
22300
|
+
}
|
|
22301
|
+
get state() {
|
|
22302
|
+
return this.ctrl.replayState();
|
|
22303
|
+
}
|
|
22304
|
+
/** The loaded history a replay can walk — hidden bars included while replaying. Null
|
|
22305
|
+
* before any bar loaded. A start older than `first` deepens the history first. */
|
|
22306
|
+
get bounds() {
|
|
22307
|
+
return this.ctrl.replayBounds();
|
|
22308
|
+
}
|
|
22309
|
+
/**
|
|
22310
|
+
* Play each revealed bar as a series of intrabar updates instead of whole: while
|
|
22311
|
+
* playing, the forming candle opens at the first tick, then every tick moves its close
|
|
22312
|
+
* and stretches its high and low, exactly like a live tick — indicators, chart types
|
|
22313
|
+
* and the `bar` event follow — and the last one settles it on the stored bar. The play
|
|
22314
|
+
* interval is then the time between two TICKS: a bar lasts as many intervals as it
|
|
22315
|
+
* has ticks (below ~16 ms, ticks are batched per frame). The next bar's ticks are
|
|
22316
|
+
* requested while the current one plays; playback waits for them. `null` goes back to
|
|
22317
|
+
* whole bars. Takes effect from the next bar; a bar forming when the source changes
|
|
22318
|
+
* completes at once.
|
|
22319
|
+
*/
|
|
22320
|
+
setTicks(source) {
|
|
22321
|
+
this.ctrl.replaySetTicks(source);
|
|
22322
|
+
return this;
|
|
22323
|
+
}
|
|
22324
|
+
};
|
|
22325
|
+
|
|
21498
22326
|
// src/core/model/inputs.ts
|
|
21499
22327
|
function inputVisible(when, values) {
|
|
21500
22328
|
if (!when) return true;
|
|
@@ -22104,10 +22932,16 @@ var IndicatorInputsDialog = class {
|
|
|
22104
22932
|
this.calendarPop = null;
|
|
22105
22933
|
this.calendarAnchor = null;
|
|
22106
22934
|
},
|
|
22107
|
-
content: (el) =>
|
|
22108
|
-
|
|
22109
|
-
|
|
22110
|
-
|
|
22935
|
+
content: (el) => {
|
|
22936
|
+
const picker = new DatePicker({
|
|
22937
|
+
value: current,
|
|
22938
|
+
onPick: (iso) => {
|
|
22939
|
+
pop.hide();
|
|
22940
|
+
onPick(iso);
|
|
22941
|
+
}
|
|
22942
|
+
});
|
|
22943
|
+
el.append(picker.el);
|
|
22944
|
+
}
|
|
22111
22945
|
});
|
|
22112
22946
|
this.calendarPop = pop;
|
|
22113
22947
|
this.calendarAnchor = anchor;
|
|
@@ -22232,28 +23066,6 @@ function ensureDialogStyles() {
|
|
|
22232
23066
|
.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;}
|
|
22233
23067
|
.vela-ind-combo-chevron:hover{opacity:0.9;}
|
|
22234
23068
|
.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;}
|
|
22235
|
-
.vela-ind-cal [hidden]{display:none !important;}
|
|
22236
|
-
.vela-ind-cal-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;}
|
|
22237
|
-
.vela-ind-cal-title{flex:1;display:flex;align-items:center;justify-content:center;gap:2px;min-width:0;}
|
|
22238
|
-
.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;}
|
|
22239
|
-
.vela-ind-cal-switch:not(:disabled):hover{background:var(--vela-hover);}
|
|
22240
|
-
.vela-ind-cal-switch:disabled{cursor:default;}
|
|
22241
|
-
.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;}
|
|
22242
|
-
.vela-ind-cal-nav:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
|
|
22243
|
-
.vela-ind-cal-week,.vela-ind-cal-grid{display:grid;grid-template-columns:repeat(7,28px);gap:2px;}
|
|
22244
|
-
.vela-ind-cal-week{margin-bottom:4px;color:var(--vela-fg-muted);font-size:11px;text-align:center;}
|
|
22245
|
-
.vela-ind-cal-week span{line-height:20px;}
|
|
22246
|
-
.vela-ind-cal-blank{width:28px;height:28px;}
|
|
22247
|
-
.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;}
|
|
22248
|
-
.vela-ind-cal-day:hover{background:var(--vela-hover);}
|
|
22249
|
-
.vela-ind-cal-day[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
|
|
22250
|
-
.vela-ind-cal-day[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
|
|
22251
|
-
.vela-ind-cal-cells{display:grid;grid-template-columns:repeat(3,1fr);gap:4px;width:calc(7 * 28px + 6 * 2px);}
|
|
22252
|
-
.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;}
|
|
22253
|
-
.vela-ind-cal-cell:hover{background:var(--vela-hover);}
|
|
22254
|
-
.vela-ind-cal-cell[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
|
|
22255
|
-
.vela-ind-cal-cell[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
|
|
22256
|
-
.vela-ind-cal-cell[data-outside]{opacity:0.45;}
|
|
22257
23069
|
${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
|
|
22258
23070
|
.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;}
|
|
22259
23071
|
.vela-ind-tab:not(.vela-ind-tab-active):hover{color:var(--vela-fg-bright);}
|
|
@@ -22287,174 +23099,6 @@ var TIME_OPTIONS = Array.from({ length: 48 }, (_, i) => {
|
|
|
22287
23099
|
const v = `${hh}:${mm}`;
|
|
22288
23100
|
return { value: v, label: v };
|
|
22289
23101
|
});
|
|
22290
|
-
var MONTH_LABELS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
|
|
22291
|
-
var MONTH_SHORT = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
22292
|
-
var WEEKDAY_LABELS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
|
|
22293
|
-
function normalizeDateInput(raw) {
|
|
22294
|
-
const t = raw.trim();
|
|
22295
|
-
const m = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(t);
|
|
22296
|
-
if (!m) return null;
|
|
22297
|
-
const y = Number(m[1]);
|
|
22298
|
-
const mo = Number(m[2]);
|
|
22299
|
-
const d = Number(m[3]);
|
|
22300
|
-
const dt = new Date(y, mo - 1, d);
|
|
22301
|
-
if (dt.getFullYear() !== y || dt.getMonth() !== mo - 1 || dt.getDate() !== d) return null;
|
|
22302
|
-
return isoDate(dt);
|
|
22303
|
-
}
|
|
22304
|
-
function parseIsoDate(raw) {
|
|
22305
|
-
const iso = normalizeDateInput(raw);
|
|
22306
|
-
if (!iso) return null;
|
|
22307
|
-
const [y, mo, d] = iso.split("-").map(Number);
|
|
22308
|
-
return new Date(y, mo - 1, d);
|
|
22309
|
-
}
|
|
22310
|
-
function isoDate(d) {
|
|
22311
|
-
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
22312
|
-
}
|
|
22313
|
-
function fillCalendar(el, current, onPick) {
|
|
22314
|
-
const today = /* @__PURE__ */ new Date();
|
|
22315
|
-
const selected = parseIsoDate(current);
|
|
22316
|
-
let year = selected?.getFullYear() ?? today.getFullYear();
|
|
22317
|
-
let month = selected?.getMonth() ?? today.getMonth();
|
|
22318
|
-
let mode = "date";
|
|
22319
|
-
const prev2 = document.createElement("button");
|
|
22320
|
-
prev2.type = "button";
|
|
22321
|
-
prev2.className = "vela-ind-cal-nav";
|
|
22322
|
-
prev2.innerHTML = iconAt("chevron-left", 14);
|
|
22323
|
-
const next = document.createElement("button");
|
|
22324
|
-
next.type = "button";
|
|
22325
|
-
next.className = "vela-ind-cal-nav";
|
|
22326
|
-
next.innerHTML = iconAt("chevron-right", 14);
|
|
22327
|
-
const monthBtn = document.createElement("button");
|
|
22328
|
-
monthBtn.type = "button";
|
|
22329
|
-
monthBtn.className = "vela-ind-cal-switch";
|
|
22330
|
-
monthBtn.setAttribute("aria-label", "Choose month");
|
|
22331
|
-
const yearBtn = document.createElement("button");
|
|
22332
|
-
yearBtn.type = "button";
|
|
22333
|
-
yearBtn.className = "vela-ind-cal-switch";
|
|
22334
|
-
yearBtn.setAttribute("aria-label", "Choose year");
|
|
22335
|
-
const title = document.createElement("div");
|
|
22336
|
-
title.className = "vela-ind-cal-title";
|
|
22337
|
-
title.append(monthBtn, yearBtn);
|
|
22338
|
-
const head = document.createElement("div");
|
|
22339
|
-
head.className = "vela-ind-cal-head";
|
|
22340
|
-
head.append(prev2, title, next);
|
|
22341
|
-
const week = document.createElement("div");
|
|
22342
|
-
week.className = "vela-ind-cal-week";
|
|
22343
|
-
for (const d of WEEKDAY_LABELS) {
|
|
22344
|
-
const cell = document.createElement("span");
|
|
22345
|
-
cell.textContent = d;
|
|
22346
|
-
week.appendChild(cell);
|
|
22347
|
-
}
|
|
22348
|
-
const grid = document.createElement("div");
|
|
22349
|
-
const paint = () => {
|
|
22350
|
-
const decade = Math.floor(year / 10) * 10;
|
|
22351
|
-
monthBtn.hidden = mode !== "date";
|
|
22352
|
-
monthBtn.textContent = MONTH_LABELS[month] ?? "";
|
|
22353
|
-
yearBtn.textContent = mode === "year" ? `${decade}-${decade + 9}` : String(year);
|
|
22354
|
-
yearBtn.disabled = mode === "year";
|
|
22355
|
-
week.hidden = mode !== "date";
|
|
22356
|
-
prev2.setAttribute("aria-label", mode === "date" ? "Previous month" : mode === "month" ? "Previous year" : "Previous decade");
|
|
22357
|
-
next.setAttribute("aria-label", mode === "date" ? "Next month" : mode === "month" ? "Next year" : "Next decade");
|
|
22358
|
-
grid.className = mode === "date" ? "vela-ind-cal-grid" : "vela-ind-cal-cells";
|
|
22359
|
-
grid.replaceChildren();
|
|
22360
|
-
if (mode === "date") {
|
|
22361
|
-
const startPad = new Date(year, month, 1).getDay();
|
|
22362
|
-
const days = new Date(year, month + 1, 0).getDate();
|
|
22363
|
-
const todayIso = isoDate(today);
|
|
22364
|
-
const selectedIso = selected ? isoDate(selected) : "";
|
|
22365
|
-
for (let i = 0; i < startPad; i++) {
|
|
22366
|
-
const blank = document.createElement("span");
|
|
22367
|
-
blank.className = "vela-ind-cal-blank";
|
|
22368
|
-
grid.appendChild(blank);
|
|
22369
|
-
}
|
|
22370
|
-
for (let day = 1; day <= days; day++) {
|
|
22371
|
-
const iso = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
|
22372
|
-
const b = document.createElement("button");
|
|
22373
|
-
b.type = "button";
|
|
22374
|
-
b.className = "vela-ind-cal-day";
|
|
22375
|
-
b.textContent = String(day);
|
|
22376
|
-
if (iso === selectedIso) b.dataset.checked = "1";
|
|
22377
|
-
if (iso === todayIso) b.dataset.today = "1";
|
|
22378
|
-
b.addEventListener("click", (e) => {
|
|
22379
|
-
e.stopPropagation();
|
|
22380
|
-
onPick(iso);
|
|
22381
|
-
});
|
|
22382
|
-
grid.appendChild(b);
|
|
22383
|
-
}
|
|
22384
|
-
return;
|
|
22385
|
-
}
|
|
22386
|
-
if (mode === "month") {
|
|
22387
|
-
for (let m = 0; m < 12; m++) {
|
|
22388
|
-
const b = document.createElement("button");
|
|
22389
|
-
b.type = "button";
|
|
22390
|
-
b.className = "vela-ind-cal-cell";
|
|
22391
|
-
b.textContent = MONTH_SHORT[m] ?? "";
|
|
22392
|
-
if (selected?.getFullYear() === year && selected.getMonth() === m) b.dataset.checked = "1";
|
|
22393
|
-
if (today.getFullYear() === year && today.getMonth() === m) b.dataset.today = "1";
|
|
22394
|
-
b.addEventListener("click", (e) => {
|
|
22395
|
-
e.stopPropagation();
|
|
22396
|
-
month = m;
|
|
22397
|
-
mode = "date";
|
|
22398
|
-
paint();
|
|
22399
|
-
});
|
|
22400
|
-
grid.appendChild(b);
|
|
22401
|
-
}
|
|
22402
|
-
return;
|
|
22403
|
-
}
|
|
22404
|
-
for (let y = decade - 1; y <= decade + 10; y++) {
|
|
22405
|
-
const b = document.createElement("button");
|
|
22406
|
-
b.type = "button";
|
|
22407
|
-
b.className = "vela-ind-cal-cell";
|
|
22408
|
-
b.textContent = String(y);
|
|
22409
|
-
if (y < decade || y > decade + 9) b.dataset.outside = "1";
|
|
22410
|
-
if (selected?.getFullYear() === y) b.dataset.checked = "1";
|
|
22411
|
-
if (today.getFullYear() === y) b.dataset.today = "1";
|
|
22412
|
-
b.addEventListener("click", (e) => {
|
|
22413
|
-
e.stopPropagation();
|
|
22414
|
-
year = y;
|
|
22415
|
-
mode = "month";
|
|
22416
|
-
paint();
|
|
22417
|
-
});
|
|
22418
|
-
grid.appendChild(b);
|
|
22419
|
-
}
|
|
22420
|
-
};
|
|
22421
|
-
const step = (dir) => {
|
|
22422
|
-
if (mode === "date") {
|
|
22423
|
-
month += dir;
|
|
22424
|
-
if (month < 0) {
|
|
22425
|
-
month = 11;
|
|
22426
|
-
year -= 1;
|
|
22427
|
-
}
|
|
22428
|
-
if (month > 11) {
|
|
22429
|
-
month = 0;
|
|
22430
|
-
year += 1;
|
|
22431
|
-
}
|
|
22432
|
-
} else {
|
|
22433
|
-
year += mode === "month" ? dir : dir * 10;
|
|
22434
|
-
}
|
|
22435
|
-
paint();
|
|
22436
|
-
};
|
|
22437
|
-
prev2.addEventListener("click", (e) => {
|
|
22438
|
-
e.stopPropagation();
|
|
22439
|
-
step(-1);
|
|
22440
|
-
});
|
|
22441
|
-
next.addEventListener("click", (e) => {
|
|
22442
|
-
e.stopPropagation();
|
|
22443
|
-
step(1);
|
|
22444
|
-
});
|
|
22445
|
-
monthBtn.addEventListener("click", (e) => {
|
|
22446
|
-
e.stopPropagation();
|
|
22447
|
-
mode = "month";
|
|
22448
|
-
paint();
|
|
22449
|
-
});
|
|
22450
|
-
yearBtn.addEventListener("click", (e) => {
|
|
22451
|
-
e.stopPropagation();
|
|
22452
|
-
mode = "year";
|
|
22453
|
-
paint();
|
|
22454
|
-
});
|
|
22455
|
-
paint();
|
|
22456
|
-
el.append(head, week, grid);
|
|
22457
|
-
}
|
|
22458
23102
|
function normalizeTimeInput(raw) {
|
|
22459
23103
|
const t = raw.trim();
|
|
22460
23104
|
const m = /^(\d{1,2}):(\d{2})$/.exec(t) ?? /^(\d{2})(\d{2})$/.exec(t);
|
|
@@ -25158,7 +25802,7 @@ var Scheduler = class {
|
|
|
25158
25802
|
|
|
25159
25803
|
// src/renderers/native/core/Animator.ts
|
|
25160
25804
|
var Animator = class {
|
|
25161
|
-
constructor(tick, raf, cancel,
|
|
25805
|
+
constructor(tick, raf, cancel, now2) {
|
|
25162
25806
|
this.tick = tick;
|
|
25163
25807
|
this.handle = null;
|
|
25164
25808
|
this.last = 0;
|
|
@@ -25172,7 +25816,7 @@ var Animator = class {
|
|
|
25172
25816
|
};
|
|
25173
25817
|
this.raf = raf ?? ((cb) => requestAnimationFrame(cb));
|
|
25174
25818
|
this.cancel = cancel ?? ((h) => cancelAnimationFrame(h));
|
|
25175
|
-
this.now =
|
|
25819
|
+
this.now = now2 ?? (() => performance.now());
|
|
25176
25820
|
}
|
|
25177
25821
|
get active() {
|
|
25178
25822
|
return this.handle !== null;
|
|
@@ -25971,6 +26615,8 @@ var SceneGraph = class {
|
|
|
25971
26615
|
/** VPVR-layer config pushed by the VPVR native indicator (null ⇒ layer off). Ephemeral. */
|
|
25972
26616
|
this.vpvrLayer = null;
|
|
25973
26617
|
this.crosshair = null;
|
|
26618
|
+
/** Runtime override of the crosshair's lines and style (the `crosshairOverride` feature). Ephemeral. */
|
|
26619
|
+
this.crosshairOverride = null;
|
|
25974
26620
|
/** How the base price series is drawn on the price pane (candles by default). */
|
|
25975
26621
|
this.priceStyle = "candles";
|
|
25976
26622
|
/** Price-series base painting for the ACTIVE style (see ChartTypeDefinition.basePainting). */
|
|
@@ -27887,9 +28533,9 @@ function timeTicks(fromMs, toMs, target = 8, offsetMs = 0) {
|
|
|
27887
28533
|
}
|
|
27888
28534
|
|
|
27889
28535
|
// src/renderers/native/chrome/countdown.ts
|
|
27890
|
-
function countdownText(barOpen, barMs,
|
|
28536
|
+
function countdownText(barOpen, barMs, now2) {
|
|
27891
28537
|
if (!(barMs > 0)) return null;
|
|
27892
|
-
const remaining = barOpen + barMs -
|
|
28538
|
+
const remaining = barOpen + barMs - now2;
|
|
27893
28539
|
if (remaining <= 0) return null;
|
|
27894
28540
|
return formatCountdown(remaining);
|
|
27895
28541
|
}
|
|
@@ -28701,19 +29347,35 @@ var CrosshairRenderer = class {
|
|
|
28701
29347
|
const dataH = coords.height;
|
|
28702
29348
|
if (!ch || ch.x < 0 || ch.x > dataW || ch.y < 0 || ch.y > dataH) return;
|
|
28703
29349
|
const cs = scene.style.crosshair;
|
|
29350
|
+
const ov = scene.crosshairOverride;
|
|
29351
|
+
const vertical = ov?.vertical !== false;
|
|
29352
|
+
const horizontal = ov?.horizontal !== false;
|
|
28704
29353
|
ctx.font = `${scene.style.fontSize}px ${theme.fontFamily}`;
|
|
28705
29354
|
ctx.textBaseline = "middle";
|
|
28706
29355
|
const logical = Math.round(coords.xToLogical(ch.x));
|
|
28707
|
-
const x =
|
|
28708
|
-
|
|
28709
|
-
|
|
28710
|
-
|
|
28711
|
-
|
|
29356
|
+
const x = crisp(coords.logicalToX(logical), ov?.width ?? cs.width);
|
|
29357
|
+
if (vertical && ov?.shadeRight) {
|
|
29358
|
+
const from = Math.max(0, Math.round(coords.logicalToX(logical + 0.5)));
|
|
29359
|
+
if (from < dataW) {
|
|
29360
|
+
ctx.fillStyle = ov.shadeRight.color;
|
|
29361
|
+
ctx.globalAlpha = ov.shadeRight.opacity ?? 1;
|
|
29362
|
+
ctx.fillRect(from, 0, dataW - from, dataH);
|
|
29363
|
+
ctx.globalAlpha = 1;
|
|
29364
|
+
}
|
|
29365
|
+
}
|
|
29366
|
+
ctx.strokeStyle = ov?.color ?? cs.color ?? theme.textColor;
|
|
29367
|
+
ctx.lineWidth = ov?.width ?? cs.width;
|
|
29368
|
+
ctx.globalAlpha = ov?.opacity ?? cs.opacity;
|
|
29369
|
+
setDash3(ctx, ov?.style ?? cs.style);
|
|
28712
29370
|
ctx.beginPath();
|
|
28713
|
-
|
|
28714
|
-
|
|
28715
|
-
|
|
28716
|
-
|
|
29371
|
+
if (vertical) {
|
|
29372
|
+
ctx.moveTo(x, 0);
|
|
29373
|
+
ctx.lineTo(x, dataH);
|
|
29374
|
+
}
|
|
29375
|
+
if (horizontal) {
|
|
29376
|
+
ctx.moveTo(0, Math.round(ch.y) + 0.5);
|
|
29377
|
+
ctx.lineTo(dataW, Math.round(ch.y) + 0.5);
|
|
29378
|
+
}
|
|
28717
29379
|
ctx.stroke();
|
|
28718
29380
|
ctx.setLineDash([]);
|
|
28719
29381
|
ctx.lineWidth = 1;
|
|
@@ -28726,11 +29388,11 @@ var CrosshairRenderer = class {
|
|
|
28726
29388
|
}
|
|
28727
29389
|
}
|
|
28728
29390
|
const chipBg = cs.labelBackground ?? theme.borderColor;
|
|
28729
|
-
if (pane && pane.axisFormat !== "none") {
|
|
29391
|
+
if (horizontal && pane && pane.axisFormat !== "none") {
|
|
28730
29392
|
const price = coords.yToPrice(ch.y, pane.scale, pane.bounds);
|
|
28731
29393
|
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);
|
|
28732
29394
|
}
|
|
28733
|
-
this.chip(ctx, x, dataH + 1, formatTimeStamp(coords.logicalToTime(logical), scene.timezone, coords.barInterval), chipBg, "center", true, theme.background);
|
|
29395
|
+
if (vertical) this.chip(ctx, x, dataH + 1, formatTimeStamp(coords.logicalToTime(logical), scene.timezone, coords.barInterval), chipBg, "center", true, theme.background);
|
|
28734
29396
|
}
|
|
28735
29397
|
destroy() {
|
|
28736
29398
|
this.canvas = null;
|
|
@@ -28741,23 +29403,37 @@ var CrosshairRenderer = class {
|
|
|
28741
29403
|
* along), with that bar's time chip in this chart's own timezone and — when the
|
|
28742
29404
|
* level resolved — the price chip on the right axis. The snap happened upstream
|
|
28743
29405
|
* (`externalCrossPx`, floor-to-containing-bar) — this method only draws. Chips
|
|
28744
|
-
* render slightly dimmed so the ghost still reads as foreign.
|
|
29406
|
+
* render slightly dimmed so the ghost still reads as foreign. A `crosshairOverride`
|
|
29407
|
+
* applies to the ghost as well — its line style, a dropped horizontal line, and the
|
|
29408
|
+
* `shadeRight` veil from the ghost bar's right edge — so a pick that spans several
|
|
29409
|
+
* charts reads the same on every one of them. */
|
|
28745
29410
|
drawExternal(ctx, ext, scene, coords, theme) {
|
|
28746
29411
|
const cs = scene.style.crosshair;
|
|
29412
|
+
const ov = scene.crosshairOverride;
|
|
28747
29413
|
const dataW = coords.width;
|
|
28748
29414
|
const dataH = coords.height;
|
|
28749
|
-
const x =
|
|
28750
|
-
if (
|
|
29415
|
+
const x = crisp(ext.x, ov?.width ?? cs.width);
|
|
29416
|
+
if (ov?.vertical === false) return;
|
|
29417
|
+
if (ov?.shadeRight) {
|
|
29418
|
+
const from = Math.max(0, Math.round(coords.logicalToX(Math.round(coords.xToLogical(ext.x)) + 0.5)));
|
|
29419
|
+
if (from < dataW) {
|
|
29420
|
+
ctx.fillStyle = ov.shadeRight.color;
|
|
29421
|
+
ctx.globalAlpha = ov.shadeRight.opacity ?? 1;
|
|
29422
|
+
ctx.fillRect(from, 0, dataW - from, dataH);
|
|
29423
|
+
ctx.globalAlpha = 1;
|
|
29424
|
+
}
|
|
29425
|
+
}
|
|
29426
|
+
if (ext.line === false || x < 0 || x > dataW) return;
|
|
28751
29427
|
ctx.font = `${scene.style.fontSize}px ${theme.fontFamily}`;
|
|
28752
29428
|
ctx.textBaseline = "middle";
|
|
28753
|
-
ctx.strokeStyle = cs.color ?? theme.textColor;
|
|
28754
|
-
ctx.lineWidth = cs.width;
|
|
28755
|
-
ctx.globalAlpha = cs.opacity * 0.55;
|
|
28756
|
-
setDash3(ctx, cs.style);
|
|
29429
|
+
ctx.strokeStyle = ov?.color ?? cs.color ?? theme.textColor;
|
|
29430
|
+
ctx.lineWidth = ov?.width ?? cs.width;
|
|
29431
|
+
ctx.globalAlpha = (ov?.opacity ?? cs.opacity) * 0.55;
|
|
29432
|
+
setDash3(ctx, ov?.style ?? cs.style);
|
|
28757
29433
|
ctx.beginPath();
|
|
28758
29434
|
ctx.moveTo(x, 0);
|
|
28759
29435
|
ctx.lineTo(x, dataH);
|
|
28760
|
-
if (ext.y != null) {
|
|
29436
|
+
if (ext.y != null && ov?.horizontal !== false) {
|
|
28761
29437
|
ctx.moveTo(0, Math.round(ext.y) + 0.5);
|
|
28762
29438
|
ctx.lineTo(dataW, Math.round(ext.y) + 0.5);
|
|
28763
29439
|
}
|
|
@@ -28766,7 +29442,7 @@ var CrosshairRenderer = class {
|
|
|
28766
29442
|
ctx.lineWidth = 1;
|
|
28767
29443
|
ctx.globalAlpha = 0.8;
|
|
28768
29444
|
const chipBg = cs.labelBackground ?? theme.borderColor;
|
|
28769
|
-
if (ext.y != null && ext.price != null) {
|
|
29445
|
+
if (ext.y != null && ext.price != null && ov?.horizontal !== false) {
|
|
28770
29446
|
let pane;
|
|
28771
29447
|
for (const p of scene.panes.values()) {
|
|
28772
29448
|
if (ext.y >= p.bounds.top && ext.y <= p.bounds.top + p.bounds.height) {
|
|
@@ -28806,6 +29482,9 @@ var CrosshairRenderer = class {
|
|
|
28806
29482
|
ctx.textAlign = "start";
|
|
28807
29483
|
}
|
|
28808
29484
|
};
|
|
29485
|
+
function crisp(px, width) {
|
|
29486
|
+
return Math.round(px) + (Math.round(width) % 2 === 1 ? 0.5 : 0);
|
|
29487
|
+
}
|
|
28809
29488
|
function setDash3(ctx, style) {
|
|
28810
29489
|
if (style === "dashed") ctx.setLineDash([6, 4]);
|
|
28811
29490
|
else if (style === "dotted") ctx.setLineDash([2, 3]);
|
|
@@ -36244,7 +36923,7 @@ var NativeRenderer = class {
|
|
|
36244
36923
|
this.moveIndicatorCbs = /* @__PURE__ */ new Set();
|
|
36245
36924
|
this.priceStyleCbs = /* @__PURE__ */ new Set();
|
|
36246
36925
|
this.name = "native";
|
|
36247
|
-
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"];
|
|
36926
|
+
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"];
|
|
36248
36927
|
/** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
|
|
36249
36928
|
* so moving onto the button doesn't count as leaving). */
|
|
36250
36929
|
this.onScrollProximityMove = (e) => {
|
|
@@ -36423,6 +37102,9 @@ var NativeRenderer = class {
|
|
|
36423
37102
|
this.scene.marks = mergeMarksState(this.scene.marks, value);
|
|
36424
37103
|
this.markPopover?.close();
|
|
36425
37104
|
break;
|
|
37105
|
+
case "crosshairOverride":
|
|
37106
|
+
this.scene.crosshairOverride = sanitizeCrosshairOverride(value);
|
|
37107
|
+
break;
|
|
36426
37108
|
case "keyboard":
|
|
36427
37109
|
this.setKeyboardEnabled(Boolean(value));
|
|
36428
37110
|
return;
|
|
@@ -36536,6 +37218,8 @@ var NativeRenderer = class {
|
|
|
36536
37218
|
return { ...this.scene.tradeMarkers, colors: { ...this.scene.tradeMarkers.colors } };
|
|
36537
37219
|
case "marks":
|
|
36538
37220
|
return { visible: this.scene.marks.visible, groups: { ...this.scene.marks.groups } };
|
|
37221
|
+
case "crosshairOverride":
|
|
37222
|
+
return this.scene.crosshairOverride ? { ...this.scene.crosshairOverride } : null;
|
|
36539
37223
|
case "keyboard":
|
|
36540
37224
|
return this.keyboardEnabled;
|
|
36541
37225
|
case "historyChords":
|
|
@@ -37114,8 +37798,8 @@ var NativeRenderer = class {
|
|
|
37114
37798
|
for (const pane of this.scene.panes.values()) pane.scale = { ...pane.scaleTarget };
|
|
37115
37799
|
this.modelAlpha = 0;
|
|
37116
37800
|
const start = performance.now();
|
|
37117
|
-
const step = (
|
|
37118
|
-
const p = Math.min(1, (
|
|
37801
|
+
const step = (now2) => {
|
|
37802
|
+
const p = Math.min(1, (now2 - start) / duration);
|
|
37119
37803
|
this.scene.bars = p >= 1 ? real : real.map((b, i) => this.revealCandle(b, i, p, n, style));
|
|
37120
37804
|
this.paintData();
|
|
37121
37805
|
if (p < 1) {
|
|
@@ -37131,8 +37815,8 @@ var NativeRenderer = class {
|
|
|
37131
37815
|
fadeInModels() {
|
|
37132
37816
|
const fade = Math.min(INTRO_MODEL_FADE_MS, this.intro.duration || INTRO_MODEL_FADE_MS);
|
|
37133
37817
|
const start = performance.now();
|
|
37134
|
-
const step = (
|
|
37135
|
-
this.modelAlpha = Math.min(1, (
|
|
37818
|
+
const step = (now2) => {
|
|
37819
|
+
this.modelAlpha = Math.min(1, (now2 - start) / fade);
|
|
37136
37820
|
this.paintData();
|
|
37137
37821
|
if (this.modelAlpha < 1) {
|
|
37138
37822
|
this.introRaf = requestAnimationFrame(step);
|
|
@@ -37655,10 +38339,13 @@ var NativeRenderer = class {
|
|
|
37655
38339
|
this.animator.start();
|
|
37656
38340
|
}
|
|
37657
38341
|
} else if (!last || bar.time > last.time) {
|
|
38342
|
+
const vp = this.coords.getViewport();
|
|
38343
|
+
const holdView = n > 0 && vp.rightOffset < 0 && this.scrollTargetRO === null;
|
|
37658
38344
|
this.bars.push(bar);
|
|
37659
38345
|
this.syncLiveEase(bar);
|
|
37660
38346
|
if (this.coords.barInterval > 0) this.coords.appendBar(bar.time);
|
|
37661
38347
|
else this.coords.setBars(this.bars.map((b) => b.time));
|
|
38348
|
+
if (holdView) this.coords.setViewport({ ...vp, rightOffset: vp.rightOffset - 1 });
|
|
37662
38349
|
} else {
|
|
37663
38350
|
return;
|
|
37664
38351
|
}
|
|
@@ -38340,10 +39027,10 @@ var NativeRenderer = class {
|
|
|
38340
39027
|
if (this.markPulseRaf !== null || typeof requestAnimationFrame !== "function") return;
|
|
38341
39028
|
const tick = () => {
|
|
38342
39029
|
this.markPulseRaf = null;
|
|
38343
|
-
const
|
|
38344
|
-
if (this.scene.marksFlash && this.scene.marksFlash.until <=
|
|
39030
|
+
const now2 = frameNow();
|
|
39031
|
+
if (this.scene.marksFlash && this.scene.marksFlash.until <= now2) this.scene.marksFlash = null;
|
|
38345
39032
|
this.scheduler?.invalidate(2 /* Chrome */);
|
|
38346
|
-
const pulsing = this.scene.marksHoverKey !== null &&
|
|
39033
|
+
const pulsing = this.scene.marksHoverKey !== null && now2 - this.scene.marksHoverSince < MARK_PULSE_MS;
|
|
38347
39034
|
if (pulsing || this.scene.marksFlash !== null) this.markPulseRaf = requestAnimationFrame(tick);
|
|
38348
39035
|
};
|
|
38349
39036
|
this.markPulseRaf = requestAnimationFrame(tick);
|
|
@@ -38904,9 +39591,10 @@ var NativeRenderer = class {
|
|
|
38904
39591
|
const ext = this.externalCross;
|
|
38905
39592
|
if (!ext || this.coords.barCount === 0) return null;
|
|
38906
39593
|
const logical = Math.floor(this.coords.timeToLogical(ext.time));
|
|
38907
|
-
if (logical <
|
|
38908
|
-
const x = this.coords.logicalToX(logical);
|
|
38909
|
-
if (!Number.isFinite(x)
|
|
39594
|
+
if (!(logical < this.coords.barCount)) return null;
|
|
39595
|
+
const x = this.coords.logicalToX(Math.max(logical, -1));
|
|
39596
|
+
if (!Number.isFinite(x)) return null;
|
|
39597
|
+
const line = logical >= 0 && x >= 0 && x <= this.coords.width;
|
|
38910
39598
|
let y = null;
|
|
38911
39599
|
if (ext.price != null) {
|
|
38912
39600
|
const pricePane = this.scene.orderedPanes().find((p) => p.kind === "price");
|
|
@@ -38915,7 +39603,7 @@ var NativeRenderer = class {
|
|
|
38915
39603
|
if (!Number.isFinite(y) || y < pricePane.bounds.top || y > pricePane.bounds.top + pricePane.bounds.height) y = null;
|
|
38916
39604
|
}
|
|
38917
39605
|
}
|
|
38918
|
-
return { x, y, time: this.coords.logicalToTime(logical), price: y != null ? ext.price : null };
|
|
39606
|
+
return { x, y, time: this.coords.logicalToTime(Math.max(logical, 0)), price: y != null ? ext.price : null, line };
|
|
38919
39607
|
}
|
|
38920
39608
|
/** Set the sticky magnet mode (called by the drawings toolbar's 3-state button). */
|
|
38921
39609
|
setSnapMode(mode) {
|
|
@@ -43591,6 +44279,7 @@ var Vela = class {
|
|
|
43591
44279
|
this.panesControl = new PanesControl(this.orchestrator);
|
|
43592
44280
|
this.drawingsControl = new DrawingsControl(this.orchestrator.drawings);
|
|
43593
44281
|
this.marksControl = new MarksControl(this.orchestrator.marks);
|
|
44282
|
+
this.replayControl = new ReplayControl(this.orchestrator);
|
|
43594
44283
|
}
|
|
43595
44284
|
/**
|
|
43596
44285
|
* Register a scripting engine so `addIndicator({ language })` can run that
|
|
@@ -43830,6 +44519,16 @@ var Vela = class {
|
|
|
43830
44519
|
get marks() {
|
|
43831
44520
|
return this.marksControl;
|
|
43832
44521
|
}
|
|
44522
|
+
/**
|
|
44523
|
+
* The chart's bar-replay control surface: rewind to a past bar and reveal the
|
|
44524
|
+
* following ones by hand or on a timer, from the history already loaded —
|
|
44525
|
+
* `await chart.replay.start({ from })`, `chart.replay.play(2000)`, `chart.replay.step()`,
|
|
44526
|
+
* `chart.replay.stop()`. Revealed bars behave like live bars for indicators; live
|
|
44527
|
+
* updates pause meanwhile. Follow it with the `replay:*` events.
|
|
44528
|
+
*/
|
|
44529
|
+
get replay() {
|
|
44530
|
+
return this.replayControl;
|
|
44531
|
+
}
|
|
43833
44532
|
on(event, handler) {
|
|
43834
44533
|
return this.orchestrator.events.on(event, handler);
|
|
43835
44534
|
}
|
|
@@ -44025,9 +44724,15 @@ var CSS21 = `
|
|
|
44025
44724
|
}
|
|
44026
44725
|
.vela-statusline .vela-sl-symbol { font-weight: 600; font-size: var(--vela-font-size-lg); }
|
|
44027
44726
|
.vela-statusline .vela-sl-meta { color: var(--vela-fg-muted); font-size: var(--vela-font-size-md); font-weight: 600; }
|
|
44727
|
+
/* The meta opens with "\xB7 " \u2014 sit its dot one space-width after the ticker, as far as the
|
|
44728
|
+
* venue sits after it, not a full row gap away. */
|
|
44729
|
+
.vela-statusline .vela-sl-symbol + .vela-sl-meta { margin-left: calc(var(--vela-space-1) - var(--vela-space-2)); }
|
|
44028
44730
|
/* Market status badge \u2014 a kit callout bubble (icon-only 16px circle, label on hover
|
|
44029
|
-
* via the kit tooltip); the session tint is applied per status in setMarketStatus.
|
|
44030
|
-
|
|
44731
|
+
* via the kit tooltip); the session tint is applied per status in setMarketStatus. While
|
|
44732
|
+
* the chart replays past bars it wears the replay badge instead (the inverse chip). */
|
|
44733
|
+
.vela-statusline .vela-sl-market { align-self: center; display: inline-flex; }
|
|
44734
|
+
.vela-statusline .vela-sl-market > [hidden] { display: none !important; }
|
|
44735
|
+
.vela-statusline .vela-sl-replay-badge, .vela-statusline .vela-sl-replay-badge svg { display: block; width: 16px; height: 16px; }
|
|
44031
44736
|
.vela-statusline .vela-sl-ohlc { display: flex; gap: var(--vela-space-1); color: var(--vela-fg-muted); }
|
|
44032
44737
|
.vela-statusline .vela-sl-ohlc b { color: var(--vela-fg); font-weight: 500; }
|
|
44033
44738
|
/* The change value wears the SAME ink as the OHLC values (set inline per render) \u2014
|
|
@@ -44080,6 +44785,8 @@ var MARKET_LABELS = {
|
|
|
44080
44785
|
closed: "Market Closed",
|
|
44081
44786
|
holiday: "Market Holiday"
|
|
44082
44787
|
};
|
|
44788
|
+
var REPLAY_LABEL = "Replay Mode";
|
|
44789
|
+
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>';
|
|
44083
44790
|
var MARKET_INKS = {
|
|
44084
44791
|
open: "var(--vela-up)",
|
|
44085
44792
|
pre: SESSION_PRE,
|
|
@@ -44118,6 +44825,9 @@ var Statusline = class {
|
|
|
44118
44825
|
this.host = host;
|
|
44119
44826
|
this.iconFor = iconFor;
|
|
44120
44827
|
this.parts = { logo: true, name: true, market: true, ohlc: true, change: true };
|
|
44828
|
+
this.marketStatus = "open";
|
|
44829
|
+
/** The chart replays past bars — the badge shows the replay mode (see {@link setReplaying}). */
|
|
44830
|
+
this.replaying = false;
|
|
44121
44831
|
/** The right-click action menu — present once a host wires it via {@link attachMenu}. */
|
|
44122
44832
|
this.menu = null;
|
|
44123
44833
|
this.menuHooks = null;
|
|
@@ -44169,8 +44879,13 @@ var Statusline = class {
|
|
|
44169
44879
|
label: MARKET_LABELS.open,
|
|
44170
44880
|
host
|
|
44171
44881
|
});
|
|
44172
|
-
this.
|
|
44173
|
-
this.
|
|
44882
|
+
this.replayBadge = doc.createElement("span");
|
|
44883
|
+
this.replayBadge.className = "vela-sl-replay-badge";
|
|
44884
|
+
this.replayBadge.innerHTML = REPLAY_BADGE_SVG;
|
|
44885
|
+
this.replayBadge.hidden = true;
|
|
44886
|
+
this.marketEl = doc.createElement("span");
|
|
44887
|
+
this.marketEl.className = "vela-sl-market";
|
|
44888
|
+
this.marketEl.append(this.marketBubble.el, this.replayBadge);
|
|
44174
44889
|
this.ohlcEl = doc.createElement("span");
|
|
44175
44890
|
this.ohlcEl.className = "vela-sl-ohlc";
|
|
44176
44891
|
this.changeEl = doc.createElement("span");
|
|
@@ -44292,6 +45007,25 @@ var Statusline = class {
|
|
|
44292
45007
|
/** Dress the market badge for a session state: its icon, tinted circle, and the
|
|
44293
45008
|
* hover label. Callers with no session model leave the constructor's 'open'. */
|
|
44294
45009
|
setMarketStatus(status) {
|
|
45010
|
+
this.marketStatus = status;
|
|
45011
|
+
this.dressBadge();
|
|
45012
|
+
}
|
|
45013
|
+
/** The chart entered or left a bar replay: the badge shows the replay mode meanwhile
|
|
45014
|
+
* (the session state of a past bar says nothing about the market now). */
|
|
45015
|
+
setReplaying(replaying) {
|
|
45016
|
+
if (replaying === this.replaying) return;
|
|
45017
|
+
this.replaying = replaying;
|
|
45018
|
+
this.dressBadge();
|
|
45019
|
+
}
|
|
45020
|
+
dressBadge() {
|
|
45021
|
+
this.marketBubble.el.hidden = this.replaying;
|
|
45022
|
+
this.replayBadge.hidden = !this.replaying;
|
|
45023
|
+
if (this.replaying) {
|
|
45024
|
+
this.marketEl.dataset.status = "replay";
|
|
45025
|
+
this.marketTip.setContent(REPLAY_LABEL);
|
|
45026
|
+
return;
|
|
45027
|
+
}
|
|
45028
|
+
const status = this.marketStatus;
|
|
44295
45029
|
this.marketEl.dataset.status = status;
|
|
44296
45030
|
const ink = MARKET_INKS[status];
|
|
44297
45031
|
this.marketBubble.set({
|
|
@@ -44364,11 +45098,23 @@ var Statusline = class {
|
|
|
44364
45098
|
this.lastBar = b;
|
|
44365
45099
|
if (!this.hoverBar) this.render();
|
|
44366
45100
|
}),
|
|
45101
|
+
// A replay cut or restore replaces the newest bar without a `bar` event.
|
|
45102
|
+
chart.on("replay:start", () => {
|
|
45103
|
+
this.lastBar = null;
|
|
45104
|
+
this.setReplaying(true);
|
|
45105
|
+
this.render();
|
|
45106
|
+
}),
|
|
45107
|
+
chart.on("replay:end", () => {
|
|
45108
|
+
this.lastBar = null;
|
|
45109
|
+
this.setReplaying(false);
|
|
45110
|
+
this.render();
|
|
45111
|
+
}),
|
|
44367
45112
|
chart.renderer.onCrosshairMove((e) => {
|
|
44368
45113
|
this.hoverBar = e.ohlc;
|
|
44369
45114
|
this.render();
|
|
44370
45115
|
})
|
|
44371
45116
|
);
|
|
45117
|
+
this.setReplaying(chart.replay.state.active);
|
|
44372
45118
|
this.render();
|
|
44373
45119
|
}
|
|
44374
45120
|
destroy() {
|
|
@@ -44609,26 +45355,26 @@ function isWeekday(ms, tz) {
|
|
|
44609
45355
|
return false;
|
|
44610
45356
|
}
|
|
44611
45357
|
}
|
|
44612
|
-
function deriveMarketStatus(
|
|
44613
|
-
const within = (ws) => ws.find(([s, e]) =>
|
|
45358
|
+
function deriveMarketStatus(now2, w, tz, overnight = false) {
|
|
45359
|
+
const within = (ws) => ws.find(([s, e]) => now2 >= s && now2 < e);
|
|
44614
45360
|
if (within(w.regular)) return "open";
|
|
44615
45361
|
const ext = within(w.extended);
|
|
44616
45362
|
if (ext) {
|
|
44617
45363
|
if (overnight) return "extended";
|
|
44618
|
-
return w.regular.some(([s]) => s >=
|
|
45364
|
+
return w.regular.some(([s]) => s >= now2 && s < ext[1]) ? "pre" : "post";
|
|
44619
45365
|
}
|
|
44620
|
-
if (isWeekday(
|
|
44621
|
-
const today = civilDate(
|
|
45366
|
+
if (isWeekday(now2, tz)) {
|
|
45367
|
+
const today = civilDate(now2, tz);
|
|
44622
45368
|
if (today !== "" && !w.extended.some(([s]) => civilDate(s, tz) === today)) return "holiday";
|
|
44623
45369
|
}
|
|
44624
45370
|
return "closed";
|
|
44625
45371
|
}
|
|
44626
|
-
function nextStatusBoundary(
|
|
45372
|
+
function nextStatusBoundary(now2, w) {
|
|
44627
45373
|
let next = null;
|
|
44628
45374
|
for (const ws of [w.regular, w.extended]) {
|
|
44629
45375
|
for (const [s, e] of ws) {
|
|
44630
45376
|
for (const t of [s, e]) {
|
|
44631
|
-
if (t >
|
|
45377
|
+
if (t > now2 && (next == null || t < next)) next = t;
|
|
44632
45378
|
}
|
|
44633
45379
|
}
|
|
44634
45380
|
}
|
|
@@ -44667,8 +45413,8 @@ var MarketStatusTracker = class {
|
|
|
44667
45413
|
return;
|
|
44668
45414
|
}
|
|
44669
45415
|
const tz = typeof si?.timezone === "string" && si.timezone !== "" ? si.timezone : "Etc/UTC";
|
|
44670
|
-
const
|
|
44671
|
-
const range = { from:
|
|
45416
|
+
const now2 = Date.now();
|
|
45417
|
+
const range = { from: now2 - FETCH_BACK_MS, to: now2 + FETCH_AHEAD_MS };
|
|
44672
45418
|
const [regular, extended] = await Promise.all([
|
|
44673
45419
|
provider.getCalendar(resolved.ticker, { ...range, session: "regular" }).catch(() => null),
|
|
44674
45420
|
provider.getCalendar(resolved.ticker, { ...range, session: "extended" }).catch(() => null)
|
|
@@ -44680,9 +45426,9 @@ var MarketStatusTracker = class {
|
|
|
44680
45426
|
}
|
|
44681
45427
|
const w = { regular, extended };
|
|
44682
45428
|
const overnight = parseSessionSpec(si)?.overnight === true;
|
|
44683
|
-
this.onStatus(deriveMarketStatus(
|
|
44684
|
-
const boundary = nextStatusBoundary(
|
|
44685
|
-
const delay = Math.min(boundary != null ? boundary -
|
|
45429
|
+
this.onStatus(deriveMarketStatus(now2, w, tz, overnight));
|
|
45430
|
+
const boundary = nextStatusBoundary(now2, w);
|
|
45431
|
+
const delay = Math.min(boundary != null ? boundary - now2 : MAX_TIMER_MS, MAX_TIMER_MS);
|
|
44686
45432
|
this.arm(my, data, symbol, Math.max(delay, MIN_TIMER_MS));
|
|
44687
45433
|
}
|
|
44688
45434
|
arm(my, data, symbol, delay) {
|
|
@@ -44715,6 +45461,7 @@ var CSS22 = `
|
|
|
44715
45461
|
left: var(--vela-toolbar-gutter, 0px);
|
|
44716
45462
|
right: var(--vela-scale-gutter, 0px);
|
|
44717
45463
|
display: flex;
|
|
45464
|
+
flex-direction: column;
|
|
44718
45465
|
align-items: center;
|
|
44719
45466
|
justify-content: center;
|
|
44720
45467
|
overflow: hidden;
|
|
@@ -44728,6 +45475,14 @@ var CSS22 = `
|
|
|
44728
45475
|
user-select: none;
|
|
44729
45476
|
white-space: nowrap;
|
|
44730
45477
|
}
|
|
45478
|
+
.vela-watermark-replay {
|
|
45479
|
+
display: inline-flex;
|
|
45480
|
+
align-items: center;
|
|
45481
|
+
gap: 0.25em;
|
|
45482
|
+
margin-top: 0.1em;
|
|
45483
|
+
font-size: 0.8em;
|
|
45484
|
+
}
|
|
45485
|
+
.vela-watermark [hidden] { display: none !important; }
|
|
44731
45486
|
`;
|
|
44732
45487
|
function watermarkFontPx(availPx, textPxAtMax) {
|
|
44733
45488
|
if (textPxAtMax <= 0) return MAX_FONT_PX;
|
|
@@ -44738,6 +45493,10 @@ var Watermark = class {
|
|
|
44738
45493
|
this.resizeObserver = null;
|
|
44739
45494
|
/** The host's visibility preference (the persisted watermark toggle). */
|
|
44740
45495
|
this.shown = true;
|
|
45496
|
+
/** The host's preference for the replay line (its own persisted toggle). */
|
|
45497
|
+
this.replayShown = true;
|
|
45498
|
+
/** The chart is replaying past bars. */
|
|
45499
|
+
this.replaying = false;
|
|
44741
45500
|
/** A bar load is in flight with nothing painted — the loading affordance owns the
|
|
44742
45501
|
* canvas, so the mark stays out of its way. Starts true: the FIRST `load:start`
|
|
44743
45502
|
* fires during chart construction, before any subscriber can see it. */
|
|
@@ -44747,7 +45506,10 @@ var Watermark = class {
|
|
|
44747
45506
|
this.el.className = "vela-watermark";
|
|
44748
45507
|
this.el.dataset.velaScreenshot = "under";
|
|
44749
45508
|
this.text = host.ownerDocument.createElement("span");
|
|
44750
|
-
this.
|
|
45509
|
+
this.replayLine = host.ownerDocument.createElement("span");
|
|
45510
|
+
this.replayLine.className = "vela-watermark-replay";
|
|
45511
|
+
this.replayLine.append(iconEl("replay", host.ownerDocument), host.ownerDocument.createTextNode("Replay"));
|
|
45512
|
+
this.el.append(this.text, this.replayLine);
|
|
44751
45513
|
host.appendChild(this.el);
|
|
44752
45514
|
if (typeof ResizeObserver !== "undefined") {
|
|
44753
45515
|
this.resizeObserver = new ResizeObserver(() => this.fit());
|
|
@@ -44760,13 +45522,26 @@ var Watermark = class {
|
|
|
44760
45522
|
this.shown = visible;
|
|
44761
45523
|
this.sync();
|
|
44762
45524
|
}
|
|
45525
|
+
/** Show/hide the replay line (the host's toggle); it only ever shows while replaying. */
|
|
45526
|
+
setReplayVisible(visible) {
|
|
45527
|
+
this.replayShown = visible;
|
|
45528
|
+
this.sync();
|
|
45529
|
+
}
|
|
45530
|
+
/** The chart entered or left a bar replay. */
|
|
45531
|
+
setReplaying(replaying) {
|
|
45532
|
+
this.replaying = replaying;
|
|
45533
|
+
this.sync();
|
|
45534
|
+
}
|
|
44763
45535
|
/** Loading and the watermark never share the canvas — hidden while a load is up. */
|
|
44764
45536
|
setLoading(loading) {
|
|
44765
45537
|
this.loading = loading;
|
|
44766
45538
|
this.sync();
|
|
44767
45539
|
}
|
|
44768
45540
|
sync() {
|
|
44769
|
-
|
|
45541
|
+
const replay = this.replayShown && this.replaying;
|
|
45542
|
+
this.text.hidden = !this.shown;
|
|
45543
|
+
this.replayLine.hidden = !replay;
|
|
45544
|
+
this.el.style.display = (this.shown || replay) && !this.loading ? "" : "none";
|
|
44770
45545
|
}
|
|
44771
45546
|
update(symbol, timeframe) {
|
|
44772
45547
|
this.text.textContent = symbol ? `${parseSymbol(symbol).ticker} \xB7 ${timeframeLabel(timeframe)}` : "";
|
|
@@ -45355,8 +46130,12 @@ var ChartCell = class {
|
|
|
45355
46130
|
this.indicatorValuesOn = seed.indicatorValues ?? true;
|
|
45356
46131
|
if (!this.indicatorValuesOn) this.inner.renderer.set("indicatorValues", false);
|
|
45357
46132
|
this.watermarkOn = seed.watermark ?? deps.watermark;
|
|
46133
|
+
this.replayWatermarkOn = seed.replayWatermark ?? true;
|
|
45358
46134
|
this.watermark = deps.watermark ? new Watermark(this.host, symbol ?? "", seed.timeframe ?? "60") : null;
|
|
45359
46135
|
if (!this.watermarkOn) this.watermark?.setVisible(false);
|
|
46136
|
+
if (!this.replayWatermarkOn) this.watermark?.setReplayVisible(false);
|
|
46137
|
+
this.inner.on("replay:start", () => this.watermark?.setReplaying(true));
|
|
46138
|
+
this.inner.on("replay:end", () => this.watermark?.setReplaying(false));
|
|
45360
46139
|
this.statusline = deps.statusline ? new Statusline(this.host, symbol ?? "", (sym) => this.inner?.data.symbolIcon(sym)) : null;
|
|
45361
46140
|
this.statusline?.setMeta(seed.timeframe ?? "60", this.state.provider ?? "");
|
|
45362
46141
|
this.statusline?.onChart(this.inner);
|
|
@@ -45551,10 +46330,10 @@ var ChartCell = class {
|
|
|
45551
46330
|
const chart = this.inner;
|
|
45552
46331
|
const symbol = this.state.symbol;
|
|
45553
46332
|
if (!chart || !symbol) return;
|
|
45554
|
-
const
|
|
46333
|
+
const now2 = Date.now();
|
|
45555
46334
|
const requestedSpan = Math.max(this.state.bars ?? 1e3, this.rangeBars) * timeframeMs(this.state.timeframe ?? "60");
|
|
45556
46335
|
const fallbackSpan = Number.isFinite(requestedSpan) ? Math.max(3 * 864e5, requestedSpan) : 3 * 864e5;
|
|
45557
|
-
const range = chart.getVisibleRange() ?? { from:
|
|
46336
|
+
const range = chart.getVisibleRange() ?? { from: now2 - fallbackSpan, to: now2 };
|
|
45558
46337
|
this.sessionShading.track(chart.data, symbol, { session: this.session, timeframe: this.timeframe, range });
|
|
45559
46338
|
}
|
|
45560
46339
|
/** The session-shade colors live in the renderer CONFIG (persisted with it, edited
|
|
@@ -45670,6 +46449,13 @@ var ChartCell = class {
|
|
|
45670
46449
|
id: "visible",
|
|
45671
46450
|
get: () => this.watermarkOn,
|
|
45672
46451
|
set: (v) => this.setWatermarkVisible(v)
|
|
46452
|
+
},
|
|
46453
|
+
{
|
|
46454
|
+
kind: "toggle",
|
|
46455
|
+
label: "Replay watermark",
|
|
46456
|
+
id: "replay",
|
|
46457
|
+
get: () => this.replayWatermarkOn,
|
|
46458
|
+
set: (v) => this.setReplayWatermarkVisible(v)
|
|
45673
46459
|
}
|
|
45674
46460
|
]
|
|
45675
46461
|
};
|
|
@@ -45738,6 +46524,12 @@ var ChartCell = class {
|
|
|
45738
46524
|
this.watermark?.setVisible(visible);
|
|
45739
46525
|
this.deps.onStateDirty();
|
|
45740
46526
|
}
|
|
46527
|
+
/** Show/hide the "Replay" line under this cell's watermark while it replays (persisted per cell). */
|
|
46528
|
+
setReplayWatermarkVisible(visible) {
|
|
46529
|
+
this.replayWatermarkOn = visible;
|
|
46530
|
+
this.watermark?.setReplayVisible(visible);
|
|
46531
|
+
this.deps.onStateDirty();
|
|
46532
|
+
}
|
|
45741
46533
|
/** Show/hide this cell's indicator titles — the in-chart legend rows (persisted per cell). */
|
|
45742
46534
|
setIndicatorTitlesVisible(visible) {
|
|
45743
46535
|
this.indicatorTitlesOn = visible;
|
|
@@ -46211,6 +47003,7 @@ var ChartCell = class {
|
|
|
46211
47003
|
if (!this.inner || this.destroyed) return;
|
|
46212
47004
|
if (cs.priceStyle && cs.priceStyle !== this.priceStyle) this.setPriceStyle(cs.priceStyle);
|
|
46213
47005
|
if (cs.watermark !== void 0 && cs.watermark !== this.watermarkOn) this.setWatermarkVisible(cs.watermark);
|
|
47006
|
+
if (cs.replayWatermark !== void 0 && cs.replayWatermark !== this.replayWatermarkOn) this.setReplayWatermarkVisible(cs.replayWatermark);
|
|
46214
47007
|
if (cs.indicatorTitles !== void 0 && cs.indicatorTitles !== this.indicatorTitlesOn) this.setIndicatorTitlesVisible(cs.indicatorTitles);
|
|
46215
47008
|
if (cs.indicatorValues !== void 0 && cs.indicatorValues !== this.indicatorValuesOn) this.setIndicatorValuesVisible(cs.indicatorValues);
|
|
46216
47009
|
if (cs.rendererConfig != null) this.inner.renderer.applyConfig(cs.rendererConfig);
|
|
@@ -46245,6 +47038,7 @@ var ChartCell = class {
|
|
|
46245
47038
|
...live ? { symbol: live.symbol, provider: live.provider, timeframe: live.timeframe } : {},
|
|
46246
47039
|
priceStyle: this.priceStyle,
|
|
46247
47040
|
watermark: this.watermarkOn,
|
|
47041
|
+
replayWatermark: this.replayWatermarkOn,
|
|
46248
47042
|
indicatorTitles: this.indicatorTitlesOn,
|
|
46249
47043
|
indicatorValues: this.indicatorValuesOn,
|
|
46250
47044
|
rendererConfig: this.inner?.renderer.getConfig() ?? void 0,
|
|
@@ -46309,6 +47103,7 @@ function buildContext(host) {
|
|
|
46309
47103
|
setPriceStyle: (style) => host.active()?.setPriceStyle(style),
|
|
46310
47104
|
openSymbolSearch: (query) => host.openSymbolSearch(query),
|
|
46311
47105
|
togglePanel: (id, open2) => host.togglePanel(id, open2),
|
|
47106
|
+
dockStrip: (el) => host.dockStrip(el),
|
|
46312
47107
|
host: host.root,
|
|
46313
47108
|
toast: (message, kind) => host.toast(message, kind),
|
|
46314
47109
|
addIndicator: (entry) => host.active()?.addExternalIndicator(entry),
|
|
@@ -46320,10 +47115,303 @@ function buildContext(host) {
|
|
|
46320
47115
|
get activeCellId() {
|
|
46321
47116
|
return host.active()?.id ?? "";
|
|
46322
47117
|
},
|
|
46323
|
-
setActiveCell: (id) => host.setActiveCell(id)
|
|
47118
|
+
setActiveCell: (id) => host.setActiveCell(id),
|
|
47119
|
+
replay: host.replay
|
|
46324
47120
|
};
|
|
46325
47121
|
}
|
|
46326
47122
|
|
|
47123
|
+
// src/workspace/WorkspaceReplay.ts
|
|
47124
|
+
var MAX_BARS_PER_STEP = 1e5;
|
|
47125
|
+
var WorkspaceReplay = class {
|
|
47126
|
+
constructor(host) {
|
|
47127
|
+
this.host = host;
|
|
47128
|
+
this.bus = new TypedEventBus();
|
|
47129
|
+
/** The running session: the shared replay time (every chart shows the bars closed by it). */
|
|
47130
|
+
this.session = null;
|
|
47131
|
+
this.playing = false;
|
|
47132
|
+
this.intervalMs = 1e3;
|
|
47133
|
+
this.timer = null;
|
|
47134
|
+
/** Cells whose replay a symbol switch ended — restarted at the shared time once their bars land. */
|
|
47135
|
+
this.rejoining = /* @__PURE__ */ new Set();
|
|
47136
|
+
this.cellSubs = /* @__PURE__ */ new Map();
|
|
47137
|
+
/** Inside a group step: chart events are the controller's own doing. */
|
|
47138
|
+
this.stepping = false;
|
|
47139
|
+
/** Inside `finish`: the charts' `replay:end` events are the controller's own doing. */
|
|
47140
|
+
this.stopping = false;
|
|
47141
|
+
/** Driving a chart's own timer: its play/pause events are not the user's. */
|
|
47142
|
+
this.muted = 0;
|
|
47143
|
+
/** A chart finished during a group step — the session ends right after it. */
|
|
47144
|
+
this.finishedDuringStep = false;
|
|
47145
|
+
for (const cell of host.cells()) this.watch(cell);
|
|
47146
|
+
this.offHost = host.onCells((e) => {
|
|
47147
|
+
if (e.kind === "created") this.onCellCreated(e.id);
|
|
47148
|
+
else if (e.kind === "destroyed") this.onCellDestroyed(e.id);
|
|
47149
|
+
});
|
|
47150
|
+
}
|
|
47151
|
+
/**
|
|
47152
|
+
* Enter replay on every cell (or seek, when already replaying) — paused. Resolves once
|
|
47153
|
+
* every chart shows its rewound history (a chart whose history does not reach that far
|
|
47154
|
+
* loads older bars first). A chart with nothing to replay after the shared time stays
|
|
47155
|
+
* as it is.
|
|
47156
|
+
*/
|
|
47157
|
+
async start(opts) {
|
|
47158
|
+
const cells = this.host.cells();
|
|
47159
|
+
const ref = cells.find((c) => c.id === (opts.cell ?? this.host.activeId())) ?? cells[0];
|
|
47160
|
+
if (!ref) return;
|
|
47161
|
+
if (this.playing) this.pause();
|
|
47162
|
+
this.rejoining.clear();
|
|
47163
|
+
await ref.chart.replay.start({ from: opts.from });
|
|
47164
|
+
const cursor = ref.chart.replay.state.cursorTime;
|
|
47165
|
+
if (cursor == null) return;
|
|
47166
|
+
const session = { clock: barClose(cursor, ref.chart.market.timeframe) };
|
|
47167
|
+
this.session = session;
|
|
47168
|
+
await Promise.all(this.host.cells().filter((c) => c.id !== ref.id).map((c) => this.startCell(c, session.clock)));
|
|
47169
|
+
if (this.session !== session) return;
|
|
47170
|
+
this.syncPlayback();
|
|
47171
|
+
const s = this.activeChart()?.replay.state;
|
|
47172
|
+
this.bus.emit("replay:start", { cursorTime: s?.cursorTime ?? cursor, remaining: s?.remaining ?? 0 });
|
|
47173
|
+
}
|
|
47174
|
+
/** Reveal the next bar(s): the clock moves to the next bar close on any chart. Returns false when nothing is left. */
|
|
47175
|
+
step() {
|
|
47176
|
+
const solo = this.solo();
|
|
47177
|
+
return solo ? solo.replay.step() : this.stepGroup();
|
|
47178
|
+
}
|
|
47179
|
+
/** Reveal the next update — in a single-chart layout, the chart's `stepUpdate()` (tick replay); otherwise {@link step}. */
|
|
47180
|
+
stepUpdate() {
|
|
47181
|
+
const solo = this.solo();
|
|
47182
|
+
return solo ? solo.replay.stepUpdate() : this.stepGroup();
|
|
47183
|
+
}
|
|
47184
|
+
/** Advance one update every `intervalMs` (default: the last pace, initially 1000). Calling it again while playing changes the pace. */
|
|
47185
|
+
play(intervalMs2) {
|
|
47186
|
+
if (!this.session) return this;
|
|
47187
|
+
if (intervalMs2 !== void 0) {
|
|
47188
|
+
if (!(Number.isFinite(intervalMs2) && intervalMs2 > 0)) {
|
|
47189
|
+
console.warn(`[vela] workspace.replay.play(${intervalMs2}) ignored \u2014 the interval must be a positive number of ms`);
|
|
47190
|
+
return this;
|
|
47191
|
+
}
|
|
47192
|
+
this.intervalMs = intervalMs2;
|
|
47193
|
+
}
|
|
47194
|
+
this.playing = true;
|
|
47195
|
+
this.clearTimer();
|
|
47196
|
+
this.syncPlayback();
|
|
47197
|
+
this.bus.emit("replay:play", { intervalMs: this.intervalMs });
|
|
47198
|
+
return this;
|
|
47199
|
+
}
|
|
47200
|
+
pause() {
|
|
47201
|
+
if (!this.playing) return this;
|
|
47202
|
+
this.playing = false;
|
|
47203
|
+
this.syncPlayback();
|
|
47204
|
+
this.bus.emit("replay:pause", void 0);
|
|
47205
|
+
return this;
|
|
47206
|
+
}
|
|
47207
|
+
/** Leave replay on every chart: full history back, live updates resumed. */
|
|
47208
|
+
stop() {
|
|
47209
|
+
this.finish("stopped");
|
|
47210
|
+
return this;
|
|
47211
|
+
}
|
|
47212
|
+
/** `active`/`playing`/`intervalMs` for the whole workspace; the cursor fields read the ACTIVE cell's chart. */
|
|
47213
|
+
get state() {
|
|
47214
|
+
const s = this.session ? this.activeChart()?.replay.state : void 0;
|
|
47215
|
+
return {
|
|
47216
|
+
active: this.session !== null,
|
|
47217
|
+
playing: this.playing,
|
|
47218
|
+
cursorTime: s?.cursorTime ?? null,
|
|
47219
|
+
remaining: s?.remaining ?? 0,
|
|
47220
|
+
nextTime: s?.nextTime ?? null,
|
|
47221
|
+
intervalMs: this.intervalMs
|
|
47222
|
+
};
|
|
47223
|
+
}
|
|
47224
|
+
/** The active cell's replayable history (`chart.replay.bounds`). */
|
|
47225
|
+
get bounds() {
|
|
47226
|
+
return this.activeChart()?.replay.bounds ?? null;
|
|
47227
|
+
}
|
|
47228
|
+
on(event, handler) {
|
|
47229
|
+
return this.bus.on(event, handler);
|
|
47230
|
+
}
|
|
47231
|
+
destroy() {
|
|
47232
|
+
this.session = null;
|
|
47233
|
+
this.playing = false;
|
|
47234
|
+
this.clearTimer();
|
|
47235
|
+
this.offHost();
|
|
47236
|
+
for (const off of this.cellSubs.values()) off();
|
|
47237
|
+
this.cellSubs.clear();
|
|
47238
|
+
this.bus.clear();
|
|
47239
|
+
}
|
|
47240
|
+
// ── internals ──
|
|
47241
|
+
/** The one chart of a single-chart layout — it replays on its own, tick replay included. */
|
|
47242
|
+
solo() {
|
|
47243
|
+
const cells = this.host.cells();
|
|
47244
|
+
return cells.length === 1 ? cells[0].chart : null;
|
|
47245
|
+
}
|
|
47246
|
+
activeChart() {
|
|
47247
|
+
const cells = this.host.cells();
|
|
47248
|
+
return (cells.find((c) => c.id === this.host.activeId()) ?? cells[0])?.chart ?? null;
|
|
47249
|
+
}
|
|
47250
|
+
/** Rewind one chart to the shared time: every bar that closed by `clock`, none after. */
|
|
47251
|
+
async startCell(cell, clock) {
|
|
47252
|
+
const replay = cell.chart.replay;
|
|
47253
|
+
const from = lastOpenClosedBy(clock, cell.chart.market.timeframe);
|
|
47254
|
+
const bounds = replay.bounds;
|
|
47255
|
+
if (!bounds || bounds.last <= from) return;
|
|
47256
|
+
await replay.start({ from });
|
|
47257
|
+
}
|
|
47258
|
+
/** Move the clock to the next bar close on any chart; each chart reveals what closed by then. */
|
|
47259
|
+
stepGroup() {
|
|
47260
|
+
const session = this.session;
|
|
47261
|
+
if (!session) return false;
|
|
47262
|
+
const cells = this.host.cells();
|
|
47263
|
+
let next = Infinity;
|
|
47264
|
+
for (const c of cells) {
|
|
47265
|
+
const t = c.chart.replay.state.nextTime;
|
|
47266
|
+
if (t != null) next = Math.min(next, barClose(t, c.chart.market.timeframe));
|
|
47267
|
+
}
|
|
47268
|
+
if (!Number.isFinite(next)) return false;
|
|
47269
|
+
this.stepping = true;
|
|
47270
|
+
try {
|
|
47271
|
+
for (const c of cells) {
|
|
47272
|
+
const tf = c.chart.market.timeframe;
|
|
47273
|
+
for (let n = 0; n < MAX_BARS_PER_STEP; n += 1) {
|
|
47274
|
+
const t = c.chart.replay.state.nextTime;
|
|
47275
|
+
if (t == null || barClose(t, tf) > next) break;
|
|
47276
|
+
if (!c.chart.replay.step()) break;
|
|
47277
|
+
}
|
|
47278
|
+
}
|
|
47279
|
+
} finally {
|
|
47280
|
+
this.stepping = false;
|
|
47281
|
+
}
|
|
47282
|
+
session.clock = next;
|
|
47283
|
+
if (this.finishedDuringStep) {
|
|
47284
|
+
this.finishedDuringStep = false;
|
|
47285
|
+
this.finish("finished");
|
|
47286
|
+
return true;
|
|
47287
|
+
}
|
|
47288
|
+
const s = this.activeChart()?.replay.state;
|
|
47289
|
+
if (s?.cursorTime != null) this.bus.emit("replay:step", { cursorTime: s.cursorTime, remaining: s.remaining });
|
|
47290
|
+
return true;
|
|
47291
|
+
}
|
|
47292
|
+
/** Put the timers in the state `playing` asks for: a single chart runs its own (it paces
|
|
47293
|
+
* ticks), several run on the controller's clock. */
|
|
47294
|
+
syncPlayback() {
|
|
47295
|
+
const solo = this.solo();
|
|
47296
|
+
const run = this.playing && this.session !== null;
|
|
47297
|
+
this.muted += 1;
|
|
47298
|
+
try {
|
|
47299
|
+
for (const c of this.host.cells()) {
|
|
47300
|
+
if (c.chart === solo && run) {
|
|
47301
|
+
if (!c.chart.replay.state.playing || c.chart.replay.state.intervalMs !== this.intervalMs) c.chart.replay.play(this.intervalMs);
|
|
47302
|
+
} else if (c.chart.replay.state.playing) {
|
|
47303
|
+
c.chart.replay.pause();
|
|
47304
|
+
}
|
|
47305
|
+
}
|
|
47306
|
+
} finally {
|
|
47307
|
+
this.muted -= 1;
|
|
47308
|
+
}
|
|
47309
|
+
if (!run || solo) this.clearTimer();
|
|
47310
|
+
else if (this.timer == null) this.schedule();
|
|
47311
|
+
}
|
|
47312
|
+
schedule() {
|
|
47313
|
+
this.timer = setTimeout(() => {
|
|
47314
|
+
this.timer = null;
|
|
47315
|
+
if (!this.playing || !this.session || this.solo()) return;
|
|
47316
|
+
if (this.stepGroup() || this.rejoining.size > 0) {
|
|
47317
|
+
if (this.playing && this.session && this.timer == null) this.schedule();
|
|
47318
|
+
} else {
|
|
47319
|
+
this.pause();
|
|
47320
|
+
}
|
|
47321
|
+
}, this.intervalMs);
|
|
47322
|
+
}
|
|
47323
|
+
clearTimer() {
|
|
47324
|
+
if (this.timer != null) clearTimeout(this.timer);
|
|
47325
|
+
this.timer = null;
|
|
47326
|
+
}
|
|
47327
|
+
/** End the session on every chart (idempotent). */
|
|
47328
|
+
finish(reason) {
|
|
47329
|
+
const had = this.session !== null;
|
|
47330
|
+
this.session = null;
|
|
47331
|
+
this.playing = false;
|
|
47332
|
+
this.clearTimer();
|
|
47333
|
+
this.rejoining.clear();
|
|
47334
|
+
this.stopping = true;
|
|
47335
|
+
try {
|
|
47336
|
+
for (const c of this.host.cells()) c.chart.replay.stop();
|
|
47337
|
+
} finally {
|
|
47338
|
+
this.stopping = false;
|
|
47339
|
+
}
|
|
47340
|
+
if (had) this.bus.emit("replay:end", { reason });
|
|
47341
|
+
}
|
|
47342
|
+
watch(cell) {
|
|
47343
|
+
const { id, chart } = cell;
|
|
47344
|
+
const isActive = () => (this.host.activeId() ?? this.host.cells()[0]?.id) === id;
|
|
47345
|
+
const offs = [
|
|
47346
|
+
chart.on("replay:end", ({ reason }) => {
|
|
47347
|
+
if (!this.session || this.stopping) return;
|
|
47348
|
+
if (reason === "market") this.rejoining.add(id);
|
|
47349
|
+
else if (this.stepping) this.finishedDuringStep || (this.finishedDuringStep = reason === "finished");
|
|
47350
|
+
else this.finish(reason);
|
|
47351
|
+
}),
|
|
47352
|
+
chart.on("market:changed", () => {
|
|
47353
|
+
if (!this.rejoining.delete(id) || !this.session) return;
|
|
47354
|
+
void this.startCell(cell, this.session.clock).then(() => this.syncPlayback());
|
|
47355
|
+
}),
|
|
47356
|
+
chart.on("replay:step", (e) => {
|
|
47357
|
+
if (this.session && !this.stepping && isActive()) this.bus.emit("replay:step", e);
|
|
47358
|
+
}),
|
|
47359
|
+
chart.on("replay:tick", (e) => {
|
|
47360
|
+
if (this.session && isActive()) this.bus.emit("replay:tick", e);
|
|
47361
|
+
}),
|
|
47362
|
+
// A single chart runs its own timer: a pause it takes on its own (a seek) is the session's.
|
|
47363
|
+
chart.on("replay:pause", () => {
|
|
47364
|
+
if (!this.session || this.muted > 0 || !this.playing || chart !== this.solo()) return;
|
|
47365
|
+
this.playing = false;
|
|
47366
|
+
this.bus.emit("replay:pause", void 0);
|
|
47367
|
+
})
|
|
47368
|
+
];
|
|
47369
|
+
this.cellSubs.set(id, () => {
|
|
47370
|
+
for (const off of offs) off();
|
|
47371
|
+
});
|
|
47372
|
+
}
|
|
47373
|
+
onCellCreated(id) {
|
|
47374
|
+
const cell = this.host.cells().find((c) => c.id === id);
|
|
47375
|
+
if (!cell) return;
|
|
47376
|
+
this.watch(cell);
|
|
47377
|
+
const session = this.session;
|
|
47378
|
+
if (!session) return;
|
|
47379
|
+
this.rejoining.add(id);
|
|
47380
|
+
void cell.chart.ready().then(async () => {
|
|
47381
|
+
if (this.session !== session || !this.rejoining.delete(id)) return;
|
|
47382
|
+
await this.startCell(cell, session.clock);
|
|
47383
|
+
this.syncPlayback();
|
|
47384
|
+
});
|
|
47385
|
+
this.syncPlayback();
|
|
47386
|
+
}
|
|
47387
|
+
onCellDestroyed(id) {
|
|
47388
|
+
this.cellSubs.get(id)?.();
|
|
47389
|
+
this.cellSubs.delete(id);
|
|
47390
|
+
this.rejoining.delete(id);
|
|
47391
|
+
this.syncPlayback();
|
|
47392
|
+
}
|
|
47393
|
+
};
|
|
47394
|
+
function barClose(open2, timeframe) {
|
|
47395
|
+
const months = monthSpan(timeframe);
|
|
47396
|
+
if (months > 0) {
|
|
47397
|
+
const d = new Date(open2);
|
|
47398
|
+
return Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + months, d.getUTCDate(), d.getUTCHours(), d.getUTCMinutes());
|
|
47399
|
+
}
|
|
47400
|
+
return open2 + timeframeToMs(timeframe ?? "60");
|
|
47401
|
+
}
|
|
47402
|
+
function lastOpenClosedBy(time, timeframe) {
|
|
47403
|
+
const months = monthSpan(timeframe);
|
|
47404
|
+
if (months > 0) {
|
|
47405
|
+
const d = new Date(time);
|
|
47406
|
+
return Date.UTC(d.getUTCFullYear(), d.getUTCMonth() - months, d.getUTCDate(), d.getUTCHours(), d.getUTCMinutes());
|
|
47407
|
+
}
|
|
47408
|
+
return time - timeframeToMs(timeframe ?? "60");
|
|
47409
|
+
}
|
|
47410
|
+
function monthSpan(timeframe) {
|
|
47411
|
+
const m = /^(\d*)M$/.exec((timeframe ?? "").trim());
|
|
47412
|
+
return m ? Number(m[1] || 1) : 0;
|
|
47413
|
+
}
|
|
47414
|
+
|
|
46327
47415
|
// src/workspace/layouts.ts
|
|
46328
47416
|
var registry5 = /* @__PURE__ */ new Map();
|
|
46329
47417
|
function registerLayout(def) {
|
|
@@ -46622,6 +47710,8 @@ var CSS24 = `
|
|
|
46622
47710
|
.vela-workspace { position: relative; width: 100%; height: 100%; display: flex; flex-direction: column; background: var(--vela-bg); }
|
|
46623
47711
|
.vela-ws-main { position: relative; display: flex; flex-direction: row; flex: 1 1 auto; min-height: 0; }
|
|
46624
47712
|
.vela-ws-toolbar { position: relative; flex: none; }
|
|
47713
|
+
.vela-ws-strips { position: relative; flex: none; display: flex; flex-direction: column; min-width: 0; }
|
|
47714
|
+
.vela-ws-strips:empty { display: none; }
|
|
46625
47715
|
.vela-ws-grid { position: relative; flex: 1 1 auto; min-width: 0; display: grid; gap: ${GAP_PX}px; background: var(--vela-border-soft); }
|
|
46626
47716
|
.vela-cell { background: var(--vela-bg); position: relative; }
|
|
46627
47717
|
/* Active-cell highlight: an overlay ring ABOVE the chart's own canvas stack (a plain
|
|
@@ -46793,6 +47883,20 @@ var VelaWorkspace = class {
|
|
|
46793
47883
|
const hostEl = typeof container === "string" ? document.querySelector(container) : container;
|
|
46794
47884
|
if (!hostEl) throw new Error(`VelaWorkspace: container not found: ${String(container)}`);
|
|
46795
47885
|
this.opts = opts;
|
|
47886
|
+
this.replay = new WorkspaceReplay({
|
|
47887
|
+
cells: () => this.cells(),
|
|
47888
|
+
activeId: () => this.activeId,
|
|
47889
|
+
onCells: (handler) => {
|
|
47890
|
+
const offs = [
|
|
47891
|
+
this.events.on("cell:created", ({ id }) => handler({ kind: "created", id })),
|
|
47892
|
+
this.events.on("cell:destroyed", ({ id }) => handler({ kind: "destroyed", id })),
|
|
47893
|
+
this.events.on("cell:active", ({ id }) => handler({ kind: "active", id }))
|
|
47894
|
+
];
|
|
47895
|
+
return () => {
|
|
47896
|
+
for (const off of offs) off();
|
|
47897
|
+
};
|
|
47898
|
+
}
|
|
47899
|
+
});
|
|
46796
47900
|
this.persistKey = opts.persist === void 0 || opts.persist === false ? null : opts.persist === true ? "vela-workspace" : opts.persist;
|
|
46797
47901
|
this.storage = opts.storage ?? localStorageAdapter();
|
|
46798
47902
|
let boot = null;
|
|
@@ -46930,6 +48034,9 @@ var VelaWorkspace = class {
|
|
|
46930
48034
|
this.dock.refresh();
|
|
46931
48035
|
if (boot?.panels) this.dock.applyState(boot.panels);
|
|
46932
48036
|
this.root.appendChild(main);
|
|
48037
|
+
this.stripsEl = doc.createElement("div");
|
|
48038
|
+
this.stripsEl.className = "vela-ws-strips";
|
|
48039
|
+
this.root.appendChild(this.stripsEl);
|
|
46933
48040
|
this.toastHost = new Toast(this.gridEl);
|
|
46934
48041
|
const attribution = rendererDefaults().attribution;
|
|
46935
48042
|
if (attribution !== false) {
|
|
@@ -47164,9 +48271,16 @@ var VelaWorkspace = class {
|
|
|
47164
48271
|
setActiveCell: (id) => this.setActiveCell(id),
|
|
47165
48272
|
openSymbolSearch: (query) => this.symbolPicker.open(query ?? ""),
|
|
47166
48273
|
togglePanel: (id, open2) => this.dock.toggle(id, open2),
|
|
48274
|
+
dockStrip: (el) => {
|
|
48275
|
+
this.stripsEl.appendChild(el);
|
|
48276
|
+
return () => {
|
|
48277
|
+
if (el.parentElement === this.stripsEl) el.remove();
|
|
48278
|
+
};
|
|
48279
|
+
},
|
|
47167
48280
|
root: this.root,
|
|
47168
48281
|
toast: (message, kind) => this.toastHost.show(message, kind),
|
|
47169
|
-
stateDirty: () => this.markStateDirty()
|
|
48282
|
+
stateDirty: () => this.markStateDirty(),
|
|
48283
|
+
replay: this.replay
|
|
47170
48284
|
});
|
|
47171
48285
|
}
|
|
47172
48286
|
/** Re-project contributed topbar actions + side panels, and mount late-registered attachments. */
|
|
@@ -47474,6 +48588,7 @@ var VelaWorkspace = class {
|
|
|
47474
48588
|
if (this.destroyed) return;
|
|
47475
48589
|
this.flushPendingState();
|
|
47476
48590
|
this.destroyed = true;
|
|
48591
|
+
this.replay.destroy();
|
|
47477
48592
|
if (this.persistKey !== null && typeof window !== "undefined") window.removeEventListener("beforeunload", this.onUnload);
|
|
47478
48593
|
this.resizeObserver?.disconnect();
|
|
47479
48594
|
this.splitters.destroy();
|
|
@@ -48240,14 +49355,13 @@ var VelaWorkspace = class {
|
|
|
48240
49355
|
panels: () => has("panels") ? [...this.dock.list()] : [],
|
|
48241
49356
|
onTogglePanel: (id) => this.dock.toggle(id),
|
|
48242
49357
|
...has("alerts") ? { alerts: () => this.alerts.map((a) => ({ title: `${a.source} \xB7 ${a.title}`, message: a.message, time: a.time })) } : {},
|
|
48243
|
-
//
|
|
48244
|
-
//
|
|
48245
|
-
//
|
|
48246
|
-
//
|
|
48247
|
-
|
|
48248
|
-
|
|
48249
|
-
|
|
48250
|
-
},
|
|
49358
|
+
// Actions placed on the bottom bar (left-aligned by default) have their stop
|
|
49359
|
+
// there — only the menu-placed ones land in the drawer, or they would appear
|
|
49360
|
+
// twice: left (primary) ones with the primary rows, right ones at the end.
|
|
49361
|
+
// Built-in-id actions are slot OVERRIDES: they reach the drawer through the
|
|
49362
|
+
// slot's own routed button (screenshot) or stop (indicators), never as a row.
|
|
49363
|
+
primaryActions: () => this.drawerActions("left"),
|
|
49364
|
+
actions: () => this.drawerActions("right"),
|
|
48251
49365
|
// The desktop layout dropdown's whole surface — the grid canvas, the
|
|
48252
49366
|
// non-canvas presets and the sync switches — relocated into the kebab
|
|
48253
49367
|
// drawer (the topbar is hidden on mobile). Same reads as the topbar block;
|
|
@@ -48272,6 +49386,11 @@ var VelaWorkspace = class {
|
|
|
48272
49386
|
}));
|
|
48273
49387
|
this.moreDrawer.open();
|
|
48274
49388
|
}
|
|
49389
|
+
/** The contributed topbar actions the mobile menu lists, from one cluster. */
|
|
49390
|
+
drawerActions(cluster) {
|
|
49391
|
+
const builtin = new Set(TOPBAR_BUILTIN_IDS);
|
|
49392
|
+
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()) }));
|
|
49393
|
+
}
|
|
48275
49394
|
openTimezoneDrawer() {
|
|
48276
49395
|
this.timezoneDrawer ?? (this.timezoneDrawer = new TimezoneDrawer({
|
|
48277
49396
|
host: this.root,
|