@luxalgo/vela 0.6.9 → 0.6.11
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/dist/{DataProvider-8Z95Q-RJ.d.cts → DataProvider-BBf-jc6W.d.ts} +49 -1
- package/dist/{DataProvider-DExJrfut.d.ts → DataProvider-p0TEyhlX.d.cts} +49 -1
- package/dist/{chunk-6WDDVMBJ.js → chunk-73PEA4MU.js} +707 -353
- package/dist/{chunk-62SVGONC.js → chunk-G77Y7LK2.js} +2 -2
- package/dist/{chunk-STHSKXOR.js → chunk-IO3NYSQV.js} +650 -157
- package/dist/{chunk-WFSZBX3R.js → chunk-KG4YT3TI.js} +3 -1
- package/dist/{chunk-FCVIG7JG.js → chunk-MHY7MVXH.js} +5 -1
- package/dist/{chunk-EQCHJZOT.js → chunk-MTLJKZDZ.js} +10 -2
- package/dist/{contributions-D7PVZO2i.d.ts → contributions-Bbe2R-mQ.d.ts} +20 -6
- package/dist/{contributions-C1U2Krwg.d.cts → contributions-CO01zWve.d.cts} +20 -6
- package/dist/index.cjs +719 -350
- package/dist/index.d.cts +37 -10
- package/dist/index.d.ts +37 -10
- package/dist/index.js +5 -5
- package/dist/{options-FM0peknS.d.ts → options-yp7sA96q.d.cts} +14 -7
- package/dist/{options-FM0peknS.d.cts → options-yp7sA96q.d.ts} +14 -7
- package/dist/{plugin-DfVqBz9p.d.cts → plugin-CkkH8QnX.d.cts} +3 -3
- package/dist/{plugin-7bkF32Rk.d.ts → plugin-DwxjM3Ni.d.ts} +3 -3
- package/dist/plugin.cjs +12 -1
- package/dist/plugin.d.cts +4 -4
- package/dist/plugin.d.ts +4 -4
- package/dist/plugin.js +3 -3
- 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-DOPiT6I6.d.cts → statusline-CHPDuKNp.d.ts} +13 -5
- package/dist/{statusline-zdF4eZLr.d.ts → statusline-DTHZUFqK.d.cts} +13 -5
- package/dist/ui.cjs +7 -1
- package/dist/ui.d.cts +3 -1
- package/dist/ui.d.ts +3 -1
- package/dist/ui.js +3 -3
- package/dist/vela.global.js +719 -350
- package/dist/vela.global.min.js +51 -49
- package/dist/widget.cjs +1249 -388
- package/dist/widget.d.cts +17 -6
- package/dist/widget.d.ts +17 -6
- package/dist/widget.js +7 -7
- package/dist/workspace.cjs +1249 -388
- package/dist/workspace.d.cts +103 -10
- package/dist/workspace.d.ts +103 -10
- package/dist/workspace.js +6 -6
- package/package.json +1 -1
- /package/dist/{chunk-AOGZBKUE.js → chunk-H26BEHF4.js} +0 -0
package/dist/workspace.cjs
CHANGED
|
@@ -125,6 +125,23 @@ var TypedEventBus = class {
|
|
|
125
125
|
}
|
|
126
126
|
};
|
|
127
127
|
|
|
128
|
+
// src/data/symbol-groups.ts
|
|
129
|
+
function isGroupRow(d) {
|
|
130
|
+
return d.group != null && d.ticker === d.group;
|
|
131
|
+
}
|
|
132
|
+
function groupKeyOf(d) {
|
|
133
|
+
return `${(d.prefix ?? d.provider ?? "").toLowerCase()}:${(d.group ?? "").toUpperCase()}`;
|
|
134
|
+
}
|
|
135
|
+
function groupMembers(pool, groupRow) {
|
|
136
|
+
const key = groupKeyOf(groupRow);
|
|
137
|
+
return pool.filter((s) => s.group != null && !isGroupRow(s) && groupKeyOf(s) === key);
|
|
138
|
+
}
|
|
139
|
+
function defaultMemberOf(pool, groupRow) {
|
|
140
|
+
const members = groupMembers(pool, groupRow);
|
|
141
|
+
const defaults2 = members.filter((m) => m.default);
|
|
142
|
+
return (defaults2.length === 1 ? defaults2[0] : members[0]) ?? null;
|
|
143
|
+
}
|
|
144
|
+
|
|
128
145
|
// src/data/ProviderRegistry.ts
|
|
129
146
|
var normName = (n) => n.trim().toLowerCase();
|
|
130
147
|
var normTicker = (t) => t.trim().toUpperCase();
|
|
@@ -214,17 +231,24 @@ var ProviderRegistry = class {
|
|
|
214
231
|
resolve(raw, opts = {}) {
|
|
215
232
|
const { provider, ticker } = this.parse(raw);
|
|
216
233
|
if (provider) {
|
|
217
|
-
if (this.entries.has(provider))
|
|
234
|
+
if (this.entries.has(provider)) {
|
|
235
|
+
const d = this.entries.get(provider).byTicker?.get(normTicker(ticker));
|
|
236
|
+
return { provider, ticker: d != null && isGroupRow(d) ? this.loadableTicker(provider, d) : ticker };
|
|
237
|
+
}
|
|
218
238
|
const key = `${provider}:${normTicker(ticker)}`;
|
|
219
239
|
for (const name of this.candidateOrder(opts.default)) {
|
|
220
240
|
const d = this.entries.get(name).prefixIndex?.get(key);
|
|
221
|
-
if (d) return { provider: name, ticker:
|
|
241
|
+
if (d) return { provider: name, ticker: this.loadableTicker(name, d) };
|
|
222
242
|
}
|
|
223
243
|
return null;
|
|
224
244
|
}
|
|
225
245
|
const norm = normTicker(ticker);
|
|
226
246
|
for (const name of this.candidateOrder(opts.default)) {
|
|
227
|
-
|
|
247
|
+
const e = this.entries.get(name);
|
|
248
|
+
if (e.index?.has(norm)) {
|
|
249
|
+
const d = e.byTicker?.get(norm);
|
|
250
|
+
return { provider: name, ticker: d != null && isGroupRow(d) ? this.loadableTicker(name, d) : ticker };
|
|
251
|
+
}
|
|
228
252
|
}
|
|
229
253
|
if (opts.lenient && this.entries.size === 1) {
|
|
230
254
|
const only = this.names()[0];
|
|
@@ -233,6 +257,13 @@ var ProviderRegistry = class {
|
|
|
233
257
|
}
|
|
234
258
|
return null;
|
|
235
259
|
}
|
|
260
|
+
/** What a resolved descriptor LOADS: itself — unless it is a GROUP row (listed, never
|
|
261
|
+
* served), which loads its default member (single `default`, else first listed). A
|
|
262
|
+
* memberless group keeps its own ticker and fails downstream like any unknown symbol. */
|
|
263
|
+
loadableTicker(name, d) {
|
|
264
|
+
if (!isGroupRow(d)) return d.ticker;
|
|
265
|
+
return defaultMemberOf(this.entries.get(name)?.descriptors ?? [], d)?.ticker ?? d.ticker;
|
|
266
|
+
}
|
|
236
267
|
/**
|
|
237
268
|
* Resolve now if possible, else resolve later — re-attempting after each provider
|
|
238
269
|
* settles. The promise stays pending until some registered provider can serve the
|
|
@@ -486,6 +517,28 @@ var CachingDataFeed = class {
|
|
|
486
517
|
this.store.merge(key, dropForming(bars));
|
|
487
518
|
return bars;
|
|
488
519
|
}
|
|
520
|
+
/**
|
|
521
|
+
* Progressive twin of {@link load}: a COLD load streams through the inner feed's
|
|
522
|
+
* progressive path — batches forwarded verbatim, the FINAL answer cached exactly as
|
|
523
|
+
* `load` caches — while a cache-covered load answers once through `load` itself (a
|
|
524
|
+
* warm chart has nothing to stream). Falls back to `load` wholesale when the inner
|
|
525
|
+
* feed lacks the capability, so callers may prefer this method unconditionally.
|
|
526
|
+
*/
|
|
527
|
+
async loadProgressive(cfg, onBatch, opts) {
|
|
528
|
+
if (cfg.data && cfg.data.length > 0) return this.inner.load(cfg);
|
|
529
|
+
if (!this.inner.loadProgressive) return null;
|
|
530
|
+
const symbol = cfg.symbol ?? "TEST";
|
|
531
|
+
const key = cacheKey(symbol, cfg.timeframe ?? "60", cfg.session);
|
|
532
|
+
const cached = this.store.get(key);
|
|
533
|
+
const n = cfg.bars ?? 500;
|
|
534
|
+
const lastCached = cached?.[cached.length - 1];
|
|
535
|
+
if (cached && lastCached && cached.length >= n - 1 && this.inner.loadRange) return this.load(cfg);
|
|
536
|
+
this.store.retainSymbol(symbol);
|
|
537
|
+
const bars = await this.inner.loadProgressive(cfg, onBatch, opts);
|
|
538
|
+
if (bars == null) return null;
|
|
539
|
+
this.store.merge(key, dropForming(bars));
|
|
540
|
+
return bars;
|
|
541
|
+
}
|
|
489
542
|
/**
|
|
490
543
|
* Cache-backed ranged fetch — the gateway engines use for secondary series
|
|
491
544
|
* (`request.security` HTF/LTF/cross-symbol) and the orchestrator's backward
|
|
@@ -731,6 +784,17 @@ var MultiProviderFeed = class {
|
|
|
731
784
|
this.prefetchSymbolInfo(resolved);
|
|
732
785
|
return this.cache.load(canonical(cfg, resolved));
|
|
733
786
|
}
|
|
787
|
+
/** Progressive twin of {@link load} — same resolution, the cache streams the batches. */
|
|
788
|
+
async loadProgressive(cfg, onBatch, opts) {
|
|
789
|
+
if (cfg.data && cfg.data.length > 0) {
|
|
790
|
+
this.liveBars = cfg.data.map((b) => ({ ...b }));
|
|
791
|
+
return cfg.data;
|
|
792
|
+
}
|
|
793
|
+
const resolved = await this.registry.whenResolvable(rawSymbol(cfg), { default: this.primaryProvider });
|
|
794
|
+
this.primaryProvider = resolved.provider;
|
|
795
|
+
this.prefetchSymbolInfo(resolved);
|
|
796
|
+
return this.cache.loadProgressive(canonical(cfg, resolved), onBatch, opts);
|
|
797
|
+
}
|
|
734
798
|
/**
|
|
735
799
|
* Synchronous per-symbol metadata for engines (Pine `syminfo.*`), served from the cache
|
|
736
800
|
* warmed by load(). Undefined until the prefetch lands (the engine then synthesizes a
|
|
@@ -818,6 +882,18 @@ var RegistryFetchFeed = class {
|
|
|
818
882
|
if (!provider) return Promise.resolve([]);
|
|
819
883
|
return safeBars(provider, ticker, cfg.timeframe ?? "60", { limit: cfg.bars ?? 500, session: cfg.session });
|
|
820
884
|
}
|
|
885
|
+
async loadProgressive(cfg, onBatch, opts) {
|
|
886
|
+
const { provider: name, ticker } = parseSymbol(cfg.symbol ?? "");
|
|
887
|
+
const provider = this.registry.get(name ?? "");
|
|
888
|
+
if (!provider) return [];
|
|
889
|
+
if (!provider.getBarsProgressive) return null;
|
|
890
|
+
try {
|
|
891
|
+
return await provider.getBarsProgressive(ticker, cfg.timeframe ?? "60", { limit: cfg.bars ?? 500, session: cfg.session }, onBatch, opts);
|
|
892
|
+
} catch (e) {
|
|
893
|
+
console.warn(`[vela] progressive fetch failed for ${ticker} ${cfg.timeframe ?? "60"} \u2014 ${e instanceof Error ? e.message : String(e)}`);
|
|
894
|
+
return [];
|
|
895
|
+
}
|
|
896
|
+
}
|
|
821
897
|
loadRange(cfg, range) {
|
|
822
898
|
const { provider: name, ticker } = parseSymbol(cfg.symbol ?? "");
|
|
823
899
|
const provider = this.registry.get(name ?? "");
|
|
@@ -1134,6 +1210,8 @@ registerIcon("folder-plus", S('<path d="M1.6 4.4a1 1 0 0 1 1-1h2.7l1.3 1.7h6.8a1
|
|
|
1134
1210
|
registerIcon("folder-minus", S('<path d="M1.6 4.4a1 1 0 0 1 1-1h2.7l1.3 1.7h6.8a1 1 0 0 1 1 1v6.5a1 1 0 0 1-1 1h-10.8a1 1 0 0 1-1-1z"/><path d="M6.2 9.4h3.6"/>'));
|
|
1135
1211
|
registerIcon("collapse", S('<path d="M3 8h10"/>'));
|
|
1136
1212
|
registerIcon("expand", S('<rect x="2.2" y="2.2" width="11.6" height="11.6" rx="1.4"/><path d="M8 5.4v5.2M5.4 8h5.2"/>'));
|
|
1213
|
+
registerIcon("plus", S('<path d="M8 2.8v10.4M2.8 8h10.4"/>'));
|
|
1214
|
+
registerIcon("minus", S('<path d="M2.8 8h10.4"/>'));
|
|
1137
1215
|
registerIcon("maximize", S('<path d="M2.5 6V3a.5.5 0 0 1 .5-.5h3M10 2.5h3a.5.5 0 0 1 .5.5v3M13.5 10v3a.5.5 0 0 1-.5.5h-3M6 13.5H3a.5.5 0 0 1-.5-.5v-3"/>'));
|
|
1138
1216
|
registerIcon("restore", S('<path d="M6.2 2.5v3.7H2.5M9.8 13.5V9.8h3.7M13.5 6.2H9.8V2.5M2.5 9.8h3.7v3.7"/>'));
|
|
1139
1217
|
registerIcon("star", S('<path d="M8 2.2l1.75 3.55 3.9.55-2.8 2.75.65 3.9L8 11.1l-3.5 1.85.65-3.9-2.8-2.75 3.9-.55z"/>'));
|
|
@@ -1141,7 +1219,7 @@ registerIcon("star-filled", S('<path d="M8 2.2l1.75 3.55 3.9.55-2.8 2.75.65 3.9L
|
|
|
1141
1219
|
registerIcon("grip", S('<circle cx="6" cy="3.5" r="1"/><circle cx="10" cy="3.5" r="1"/><circle cx="6" cy="8" r="1"/><circle cx="10" cy="8" r="1"/><circle cx="6" cy="12.5" r="1"/><circle cx="10" cy="12.5" r="1"/>', 'fill="currentColor" stroke="none"'));
|
|
1142
1220
|
registerIcon("kebab", S('<circle cx="8" cy="3.2" r="1.2"/><circle cx="8" cy="8" r="1.2"/><circle cx="8" cy="12.8" r="1.2"/>', 'fill="currentColor" stroke="none"'));
|
|
1143
1221
|
registerIcon("burger", S('<path d="M2.5 4.5h11M2.5 8h11M2.5 11.5h11"/>'));
|
|
1144
|
-
registerIcon("reset", S('<
|
|
1222
|
+
registerIcon("reset", S('<path d="M2 8a6 6 0 1 0 6-6 6.5 6.5 0 0 0-4.5 1.83L2 5.33"/><path d="M2 2v3.33h3.33"/>'));
|
|
1145
1223
|
registerIcon("pane-collapse", S('<path d="M2.6 9.4h4v4M13.4 6.6h-4v-4"/><path d="m9.4 6.6 4.2-4.2M2.4 13.6l4.2-4.2"/>'));
|
|
1146
1224
|
registerIcon("pane-expand", S('<path d="M9.8 2.4h3.8v3.8M6.2 13.6H2.4V9.8"/><path d="m13.6 2.4-4.4 4.4M2.4 13.6l4.4-4.4"/>'));
|
|
1147
1225
|
registerIcon("cursor", svg24('<path d="M5 3l6 16 2-6 6-2z"/>', 'fill="currentColor" stroke="none"'));
|
|
@@ -4307,6 +4385,10 @@ var CalloutBubble = class {
|
|
|
4307
4385
|
get open() {
|
|
4308
4386
|
return this.pop?.open ?? false;
|
|
4309
4387
|
}
|
|
4388
|
+
/** Close the deployed panel, if any. The bubble itself stays. */
|
|
4389
|
+
hidePanel() {
|
|
4390
|
+
this.pop?.hide();
|
|
4391
|
+
}
|
|
4310
4392
|
destroy() {
|
|
4311
4393
|
this.pop?.destroy();
|
|
4312
4394
|
this.pop = null;
|
|
@@ -13673,6 +13755,17 @@ var PanelDock = class {
|
|
|
13673
13755
|
for (const entry of this.entries) this.deps.chrome.setPanelActive(entry.id, entry.panel.open);
|
|
13674
13756
|
}
|
|
13675
13757
|
};
|
|
13758
|
+
function foldGroups(list) {
|
|
13759
|
+
const groupsAbove = /* @__PURE__ */ new Set();
|
|
13760
|
+
const out = [];
|
|
13761
|
+
for (const s of list) {
|
|
13762
|
+
if (isGroupRow(s)) {
|
|
13763
|
+
groupsAbove.add(groupKeyOf(s));
|
|
13764
|
+
out.push(s);
|
|
13765
|
+
} else if (s.group == null || !groupsAbove.has(groupKeyOf(s))) out.push(s);
|
|
13766
|
+
}
|
|
13767
|
+
return out;
|
|
13768
|
+
}
|
|
13676
13769
|
var TOP_TICKERS = ["BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT", "XRPUSDT", "DOGEUSDT", "ADAUSDT", "LINKUSDT"];
|
|
13677
13770
|
function parseQuery(raw, venues) {
|
|
13678
13771
|
const m = raw.match(/^\s*([^\s:]+)\s*[:\s]\s*(.*)$/);
|
|
@@ -13804,6 +13897,21 @@ var CSS8 = `
|
|
|
13804
13897
|
.vela-sp-badge[data-p='binance'] { color: #f0b90b; } /* palette-exempt: venue brand mark */
|
|
13805
13898
|
.vela-sp-badge[data-p='hyperliquid'] { color: #50d2c1; } /* palette-exempt: venue brand mark */
|
|
13806
13899
|
.vela-sp-empty { padding: var(--vela-space-3); color: var(--vela-fg-muted); text-align: center; }
|
|
13900
|
+
/* Grouped listings (futures roots): the chevron unfolds members inline, indented. */
|
|
13901
|
+
.vela-sp-expander {
|
|
13902
|
+
all: unset;
|
|
13903
|
+
flex: none;
|
|
13904
|
+
display: inline-flex;
|
|
13905
|
+
align-items: center;
|
|
13906
|
+
justify-content: center;
|
|
13907
|
+
width: 22px;
|
|
13908
|
+
height: 22px;
|
|
13909
|
+
border-radius: 5px;
|
|
13910
|
+
cursor: pointer;
|
|
13911
|
+
color: var(--vela-fg-muted);
|
|
13912
|
+
}
|
|
13913
|
+
.vela-sp-expander:hover { background: var(--vela-surface-elev); color: var(--vela-fg); }
|
|
13914
|
+
.vela-sp-row[data-member] { padding-left: 34px; }
|
|
13807
13915
|
`;
|
|
13808
13916
|
var PAGE = 100;
|
|
13809
13917
|
var SymbolPicker = class {
|
|
@@ -13815,6 +13923,11 @@ var SymbolPicker = class {
|
|
|
13815
13923
|
this.seed = "";
|
|
13816
13924
|
this.activeTab = "All";
|
|
13817
13925
|
this.visible = PAGE;
|
|
13926
|
+
/** The last filter pass returned fewer raw rows than asked — the pool is drained
|
|
13927
|
+
* (checked BEFORE folding: folding shortens pages without meaning exhaustion). */
|
|
13928
|
+
this.exhausted = false;
|
|
13929
|
+
/** Group rows currently expanded (venue-scoped keys) — members shown inline. */
|
|
13930
|
+
this.expanded = /* @__PURE__ */ new Set();
|
|
13818
13931
|
/** The ranked pool cache — `key` fingerprints the raw pool the ranking ran on. */
|
|
13819
13932
|
this.ranked = null;
|
|
13820
13933
|
this.ranking = false;
|
|
@@ -13829,7 +13942,7 @@ var SymbolPicker = class {
|
|
|
13829
13942
|
searchRow.append(iconEl("search", doc), this.input);
|
|
13830
13943
|
this.tabs = doc.createElement("div");
|
|
13831
13944
|
this.tabs.className = "vela-sp-tabs";
|
|
13832
|
-
for (const t of ["All", "Stocks", "ETFs", "Crypto", "Forex", "Commodities"]) {
|
|
13945
|
+
for (const t of ["All", "Stocks", "ETFs", "Crypto", "Futures", "Forex", "Commodities"]) {
|
|
13833
13946
|
const b = doc.createElement("button");
|
|
13834
13947
|
b.className = "vela-sp-tab";
|
|
13835
13948
|
b.textContent = t;
|
|
@@ -13845,7 +13958,7 @@ var SymbolPicker = class {
|
|
|
13845
13958
|
this.list = doc.createElement("div");
|
|
13846
13959
|
this.list.className = "vela-sp-list";
|
|
13847
13960
|
this.list.addEventListener("scroll", () => {
|
|
13848
|
-
if (this.
|
|
13961
|
+
if (this.exhausted) return;
|
|
13849
13962
|
if (this.list.scrollTop + this.list.clientHeight < this.list.scrollHeight - 200) return;
|
|
13850
13963
|
this.visible += PAGE;
|
|
13851
13964
|
this.grow();
|
|
@@ -13874,14 +13987,19 @@ var SymbolPicker = class {
|
|
|
13874
13987
|
else if (e.key === "ArrowUp") this.moveHighlight(-1);
|
|
13875
13988
|
else if (e.key === "Enter") {
|
|
13876
13989
|
const pick = this.rows[this.highlighted];
|
|
13877
|
-
if (pick) this.
|
|
13990
|
+
if (pick) this.pick(pick);
|
|
13878
13991
|
return;
|
|
13879
13992
|
} else return;
|
|
13880
13993
|
e.preventDefault();
|
|
13881
13994
|
});
|
|
13882
13995
|
this.list.addEventListener("click", (e) => {
|
|
13883
|
-
const
|
|
13884
|
-
|
|
13996
|
+
const target = e.target;
|
|
13997
|
+
const row = target.closest(".vela-sp-row");
|
|
13998
|
+
if (!row) return;
|
|
13999
|
+
const s = this.rows[Number(row.dataset.i)];
|
|
14000
|
+
if (!s) return;
|
|
14001
|
+
if (target.closest(".vela-sp-expander")) this.toggleExpand(s);
|
|
14002
|
+
else this.pick(s);
|
|
13885
14003
|
});
|
|
13886
14004
|
}
|
|
13887
14005
|
/** Wire where symbols come from (re-called on every widget rebuild). */
|
|
@@ -13898,6 +14016,27 @@ var SymbolPicker = class {
|
|
|
13898
14016
|
destroy() {
|
|
13899
14017
|
this.dialog.destroy();
|
|
13900
14018
|
}
|
|
14019
|
+
/** Route a row activation: a GROUP row loads its default member (the root itself is
|
|
14020
|
+
* listed, never loadable), any other row loads itself. */
|
|
14021
|
+
pick(s) {
|
|
14022
|
+
const target = isGroupRow(s) ? defaultMemberOf(this.pool(), s) ?? s : s;
|
|
14023
|
+
this.select(target.ticker, target.prefix ?? target.provider, this.opts.onSelect);
|
|
14024
|
+
}
|
|
14025
|
+
/** Expand/collapse a group row IN PLACE — same query, same page, same scroll; only
|
|
14026
|
+
* the member rows under the group appear or go. */
|
|
14027
|
+
toggleExpand(s) {
|
|
14028
|
+
const key = groupKeyOf(s);
|
|
14029
|
+
if (!this.expanded.delete(key)) this.expanded.add(key);
|
|
14030
|
+
const scrollTop = this.list.scrollTop;
|
|
14031
|
+
const focus = this.rows[this.highlighted];
|
|
14032
|
+
this.rows = this.computeRows();
|
|
14033
|
+
this.list.replaceChildren();
|
|
14034
|
+
this.rows.forEach((r, i) => this.list.appendChild(this.rowEl(r, i)));
|
|
14035
|
+
const at = focus ? this.rows.indexOf(focus) : -1;
|
|
14036
|
+
this.highlighted = at >= 0 ? at : Math.min(this.highlighted, Math.max(0, this.rows.length - 1));
|
|
14037
|
+
this.renderHighlight();
|
|
14038
|
+
this.list.scrollTop = scrollTop;
|
|
14039
|
+
}
|
|
13901
14040
|
select(ticker, venue, onSelect) {
|
|
13902
14041
|
this.close();
|
|
13903
14042
|
onSelect(venue ? `${venue}:${ticker}` : ticker);
|
|
@@ -13924,10 +14063,26 @@ var SymbolPicker = class {
|
|
|
13924
14063
|
return raw;
|
|
13925
14064
|
}
|
|
13926
14065
|
computeRows() {
|
|
13927
|
-
const TAB_TYPES = { Crypto: ["crypto"], Stocks: ["stock"], ETFs: ["etf"], Forex: ["forex"], Commodities: ["commodity"] };
|
|
14066
|
+
const TAB_TYPES = { Crypto: ["crypto"], Stocks: ["stock"], ETFs: ["etf"], Futures: ["futures", "root"], Forex: ["forex"], Commodities: ["commodity"] };
|
|
13928
14067
|
const all = this.pool();
|
|
13929
14068
|
const pool = this.activeTab === "All" ? all : all.filter((s) => TAB_TYPES[this.activeTab]?.includes((s.type ?? "").toLowerCase()) || this.activeTab === "Crypto" && (s.type ?? "").toLowerCase() === "futures");
|
|
13930
|
-
|
|
14069
|
+
const filtered = filterSymbols(pool, this.input.value, this.visible, TOP_TICKERS);
|
|
14070
|
+
this.exhausted = filtered.length < this.visible;
|
|
14071
|
+
const folded = foldGroups(filtered);
|
|
14072
|
+
if (!this.expanded.size) return folded;
|
|
14073
|
+
const keyOf = (s) => `${(s.prefix ?? s.provider ?? "").toLowerCase()}:${s.ticker.toUpperCase()}`;
|
|
14074
|
+
const present = new Set(folded.map(keyOf));
|
|
14075
|
+
const out = [];
|
|
14076
|
+
for (const s of folded) {
|
|
14077
|
+
out.push(s);
|
|
14078
|
+
if (!isGroupRow(s) || !this.expanded.has(groupKeyOf(s))) continue;
|
|
14079
|
+
for (const m of groupMembers(all, s)) {
|
|
14080
|
+
if (present.has(keyOf(m))) continue;
|
|
14081
|
+
present.add(keyOf(m));
|
|
14082
|
+
out.push(m);
|
|
14083
|
+
}
|
|
14084
|
+
}
|
|
14085
|
+
return out;
|
|
13931
14086
|
}
|
|
13932
14087
|
refresh() {
|
|
13933
14088
|
const doc = this.list.ownerDocument;
|
|
@@ -13942,19 +14097,20 @@ var SymbolPicker = class {
|
|
|
13942
14097
|
this.list.appendChild(empty);
|
|
13943
14098
|
return;
|
|
13944
14099
|
}
|
|
13945
|
-
|
|
14100
|
+
this.rows.forEach((s, i) => this.list.appendChild(this.rowEl(s, i)));
|
|
13946
14101
|
this.renderHighlight();
|
|
13947
14102
|
}
|
|
13948
14103
|
/** Append the page the grown `visible` just uncovered — rows already on screen stay put. */
|
|
13949
14104
|
grow() {
|
|
13950
14105
|
const already = this.rows.length;
|
|
13951
14106
|
this.rows = this.computeRows();
|
|
13952
|
-
|
|
14107
|
+
this.rows.slice(already).forEach((s, j) => this.list.appendChild(this.rowEl(s, already + j)));
|
|
13953
14108
|
}
|
|
13954
|
-
rowEl(s) {
|
|
14109
|
+
rowEl(s, i) {
|
|
13955
14110
|
const doc = this.list.ownerDocument;
|
|
13956
14111
|
const row = doc.createElement("div");
|
|
13957
14112
|
row.className = "vela-sp-row";
|
|
14113
|
+
row.dataset.i = String(i);
|
|
13958
14114
|
row.dataset.ticker = s.ticker;
|
|
13959
14115
|
const venue = s.prefix ?? s.provider;
|
|
13960
14116
|
if (venue) row.dataset.venue = venue;
|
|
@@ -13969,6 +14125,14 @@ var SymbolPicker = class {
|
|
|
13969
14125
|
d.textContent = s.description ?? (s.type ?? "");
|
|
13970
14126
|
main.append(t, d);
|
|
13971
14127
|
row.append(av, main);
|
|
14128
|
+
if (isGroupRow(s)) {
|
|
14129
|
+
row.dataset.group = "1";
|
|
14130
|
+
const expander = doc.createElement("button");
|
|
14131
|
+
expander.className = "vela-sp-expander";
|
|
14132
|
+
expander.setAttribute("aria-label", "Show contracts");
|
|
14133
|
+
expander.appendChild(iconEl(this.expanded.has(groupKeyOf(s)) ? "chevron-down" : "chevron-right", doc));
|
|
14134
|
+
row.appendChild(expander);
|
|
14135
|
+
} else if (s.group != null && this.expanded.has(groupKeyOf(s))) row.dataset.member = "1";
|
|
13972
14136
|
if (venue) {
|
|
13973
14137
|
const badge = doc.createElement("span");
|
|
13974
14138
|
badge.className = "vela-sp-badge";
|
|
@@ -14585,6 +14749,9 @@ var CSS13 = `
|
|
|
14585
14749
|
}
|
|
14586
14750
|
.vela-mb-item:active { background: var(--vela-hover); }
|
|
14587
14751
|
.vela-mb-item .vela-icon { font-size: 18px; width: 18px; height: 18px; }
|
|
14752
|
+
/* A lit stop (the maximize toggle while something is isolated): the inverse
|
|
14753
|
+
"selected" chip \u2014 white on the dark theme, dark on the light one. */
|
|
14754
|
+
.vela-mb-item.vela-mb-on, .vela-mb-item.vela-mb-on:active { background: var(--vela-selected-bg); color: var(--vela-selected-fg); }
|
|
14588
14755
|
/* Left-aligned contributed actions get their own stops (the built-in indicators
|
|
14589
14756
|
slot) \u2014 the wrapper is layout-transparent so each stop flexes like a sibling. */
|
|
14590
14757
|
.vela-mb-actions { display: contents; }
|
|
@@ -14625,9 +14792,11 @@ var MobileBar = class {
|
|
|
14625
14792
|
this.actionsHost.className = "vela-mb-actions";
|
|
14626
14793
|
const onDrawings = opts.onDrawingsClick;
|
|
14627
14794
|
const drawings = onDrawings ? item("vela-mb-drawings", "Drawings", onDrawings, "pen") : null;
|
|
14795
|
+
const onMaximize = opts.onMaximizeClick;
|
|
14796
|
+
this.maxEl = onMaximize ? item("vela-mb-maximize", "Maximize chart", onMaximize, "maximize") : null;
|
|
14628
14797
|
const more = item("vela-mb-more", "More", opts.onMoreClick, "kebab");
|
|
14629
14798
|
const settings = item("vela-mb-settings", "Chart settings", opts.onSettingsClick, "gear");
|
|
14630
|
-
this.el.append(this.symbolEl, this.tfEl, ...indicators ? [indicators] : [], this.actionsHost, ...drawings ? [drawings] : [], more, settings);
|
|
14799
|
+
this.el.append(this.symbolEl, this.tfEl, ...indicators ? [indicators] : [], this.actionsHost, ...drawings ? [drawings] : [], ...this.maxEl ? [this.maxEl] : [], more, settings);
|
|
14631
14800
|
host.appendChild(this.el);
|
|
14632
14801
|
this.renderActions();
|
|
14633
14802
|
}
|
|
@@ -14659,6 +14828,14 @@ var MobileBar = class {
|
|
|
14659
14828
|
setTimeframe(tf) {
|
|
14660
14829
|
this.tfEl.textContent = timeframeLabel(tf);
|
|
14661
14830
|
}
|
|
14831
|
+
/** Light the maximize stop while something is isolated (a chart over the grid,
|
|
14832
|
+
* or a maximized pane inside the active chart) — inverse chip + restore glyph. */
|
|
14833
|
+
setMaximizeActive(on) {
|
|
14834
|
+
if (!this.maxEl) return;
|
|
14835
|
+
this.maxEl.classList.toggle("vela-mb-on", on);
|
|
14836
|
+
this.maxEl.setAttribute("aria-label", on ? "Restore layout" : "Maximize chart");
|
|
14837
|
+
this.maxEl.replaceChildren(iconEl(on ? "restore" : "maximize", this.el.ownerDocument));
|
|
14838
|
+
}
|
|
14662
14839
|
destroy() {
|
|
14663
14840
|
this.el.remove();
|
|
14664
14841
|
}
|
|
@@ -16621,11 +16798,12 @@ async function resolveIndicators(config, fetchImpl = fetch) {
|
|
|
16621
16798
|
}
|
|
16622
16799
|
return out;
|
|
16623
16800
|
}
|
|
16801
|
+
var ledgerEntryName = (e) => typeof e === "string" ? e : e.name;
|
|
16624
16802
|
function indicatorLedger(i) {
|
|
16625
16803
|
const natives = [...i.present];
|
|
16626
16804
|
if (i.volumePending && !natives.includes("volume")) natives.push("volume");
|
|
16627
16805
|
return {
|
|
16628
|
-
manifest: i.manifestSettled ? [...i.
|
|
16806
|
+
manifest: i.manifestSettled ? [...i.instanceEntries] : [...i.pendingManifest ?? i.instanceEntries],
|
|
16629
16807
|
natives
|
|
16630
16808
|
};
|
|
16631
16809
|
}
|
|
@@ -16836,7 +17014,7 @@ var DrawingToolbar = class {
|
|
|
16836
17014
|
this.root.replaceChildren();
|
|
16837
17015
|
this.groupCells.clear();
|
|
16838
17016
|
this.groupIcons.clear();
|
|
16839
|
-
this.cursorBtn = this.makeButton(CURSOR_ICON, "Cursor", () => this.
|
|
17017
|
+
this.cursorBtn = this.makeButton(CURSOR_ICON, "Cursor", () => this.onCursorClick());
|
|
16840
17018
|
this.root.appendChild(this.cursorBtn);
|
|
16841
17019
|
if (this.def.groups.length > 0) this.root.appendChild(this.divider());
|
|
16842
17020
|
for (const g of this.def.groups) {
|
|
@@ -16940,6 +17118,14 @@ var DrawingToolbar = class {
|
|
|
16940
17118
|
this.magnetIcon = icon2;
|
|
16941
17119
|
return cell;
|
|
16942
17120
|
}
|
|
17121
|
+
/** Cursor returns to select/idle: an active measure/eraser mode exits through its own
|
|
17122
|
+
* toggle callback (disarming a tool via `onArm(null)` alone can't — the host treats a
|
|
17123
|
+
* null arm as a no-op side effect of entering those modes), then the tool disarms. */
|
|
17124
|
+
onCursorClick() {
|
|
17125
|
+
if (this.measureActive) this.onMeasure();
|
|
17126
|
+
if (this.eraserActive) this.onEraser();
|
|
17127
|
+
this.onArm(null);
|
|
17128
|
+
}
|
|
16943
17129
|
/** Clicking the icon arms the group's last-used tool (it does NOT open the flyout). */
|
|
16944
17130
|
onGroupIconClick(group) {
|
|
16945
17131
|
const type = this.lastUsed.get(group.id) ?? group.tools[0]?.type;
|
|
@@ -17551,7 +17737,17 @@ function sanitizeCell(raw) {
|
|
|
17551
17737
|
if (c.drawings != null && typeof c.drawings === "object") out.drawings = c.drawings;
|
|
17552
17738
|
const ind = c.indicators;
|
|
17553
17739
|
if (ind != null && typeof ind === "object") {
|
|
17554
|
-
const manifest = Array.isArray(ind.manifest) ? ind.manifest.
|
|
17740
|
+
const manifest = Array.isArray(ind.manifest) ? ind.manifest.flatMap((n) => {
|
|
17741
|
+
if (typeof n === "string") return [n];
|
|
17742
|
+
if (n != null && typeof n === "object" && typeof n.name === "string") {
|
|
17743
|
+
const e = n;
|
|
17744
|
+
const bag = (v) => v != null && typeof v === "object" && !Array.isArray(v) ? v : void 0;
|
|
17745
|
+
const inputs = bag(e.inputs);
|
|
17746
|
+
const props = bag(e.props);
|
|
17747
|
+
return [inputs || props ? { name: e.name, ...inputs ? { inputs } : {}, ...props ? { props } : {} } : e.name];
|
|
17748
|
+
}
|
|
17749
|
+
return [];
|
|
17750
|
+
}) : [];
|
|
17555
17751
|
const natives = Array.isArray(ind.natives) ? ind.natives.filter((n) => typeof n === "string") : [];
|
|
17556
17752
|
out.indicators = { manifest, natives };
|
|
17557
17753
|
}
|
|
@@ -17750,6 +17946,12 @@ var IndicatorHandleImpl = class {
|
|
|
17750
17946
|
get visible() {
|
|
17751
17947
|
return this.visibleState;
|
|
17752
17948
|
}
|
|
17949
|
+
inputValues() {
|
|
17950
|
+
return this.controller.inputValuesOf(this.id);
|
|
17951
|
+
}
|
|
17952
|
+
propValues() {
|
|
17953
|
+
return this.controller.propValuesOf(this.id);
|
|
17954
|
+
}
|
|
17753
17955
|
setInput(key, value) {
|
|
17754
17956
|
this.controller.applyInputs(this.id, { [key]: value });
|
|
17755
17957
|
}
|
|
@@ -17889,6 +18091,7 @@ var RUN_EMIT_THROTTLE_MS = 1e3;
|
|
|
17889
18091
|
var PREVIEW_BARS = 300;
|
|
17890
18092
|
var SINGLE_LOAD_BARS = 5e3;
|
|
17891
18093
|
var CHUNK_BARS = 1e4;
|
|
18094
|
+
var FIRST_PAINT_BARS = 100;
|
|
17892
18095
|
var GAP_FACTOR = 1.5;
|
|
17893
18096
|
var HEAL_COOLDOWN_MS = 5e3;
|
|
17894
18097
|
var EngineOrchestrator = class _EngineOrchestrator {
|
|
@@ -17952,6 +18155,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
17952
18155
|
/** Invalidates detached async work (backfill loops, in-flight loads, gap heals):
|
|
17953
18156
|
* bumped by init(), setMarket() and destroy(). */
|
|
17954
18157
|
this.generation = 0;
|
|
18158
|
+
/** Aborts the in-flight PROGRESSIVE load's source polling on supersession — an
|
|
18159
|
+
* abandoned stream left polling to its own budget starves the browser's per-host
|
|
18160
|
+
* connection pool, and the NEXT symbol's very first fetch with it (measured). */
|
|
18161
|
+
this.progressiveAbort = null;
|
|
17955
18162
|
/** Awaiters racing a superseded load (setMarket callers) — released on every bump so they never hang. */
|
|
17956
18163
|
this.supersedeWaiters = [];
|
|
17957
18164
|
/** `history:complete` fired for the CURRENT load. Each market load re-arms the cycle
|
|
@@ -18094,6 +18301,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18094
18301
|
* superseded setMarket awaiters so their promises resolve instead of hanging. */
|
|
18095
18302
|
bumpGeneration() {
|
|
18096
18303
|
const gen = ++this.generation;
|
|
18304
|
+
this.progressiveAbort?.abort();
|
|
18305
|
+
this.progressiveAbort = null;
|
|
18097
18306
|
for (const w of this.supersedeWaiters.splice(0)) w();
|
|
18098
18307
|
return gen;
|
|
18099
18308
|
}
|
|
@@ -18147,7 +18356,48 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18147
18356
|
const requested = market.bars ?? 500;
|
|
18148
18357
|
const initialRange = market.visibleRange;
|
|
18149
18358
|
const deep = !market.data?.length && initialRange == null && requested > SINGLE_LOAD_BARS;
|
|
18150
|
-
|
|
18359
|
+
let progressiveServed = false;
|
|
18360
|
+
if (!market.data?.length && initialRange == null && this.feed.loadProgressive) {
|
|
18361
|
+
let painted = false;
|
|
18362
|
+
const paint = (bars, final) => {
|
|
18363
|
+
if (this.generation !== gen || !final && bars.length === 0) return;
|
|
18364
|
+
if (!painted && !final && bars.length < Math.min(requested, FIRST_PAINT_BARS)) return;
|
|
18365
|
+
this.setBarSeries(bars, painted ? { preserveView: true } : void 0);
|
|
18366
|
+
if (!painted && bars.length > 0) {
|
|
18367
|
+
painted = true;
|
|
18368
|
+
if (opts.firstLoad) this.activateBarLayers();
|
|
18369
|
+
if (!final) this.historyState = "backfill";
|
|
18370
|
+
}
|
|
18371
|
+
};
|
|
18372
|
+
const abort = new AbortController();
|
|
18373
|
+
this.progressiveAbort = abort;
|
|
18374
|
+
progressiveServed = await new Promise((firstPaint) => {
|
|
18375
|
+
let signaled = false;
|
|
18376
|
+
const signal = (served) => {
|
|
18377
|
+
if (!signaled) {
|
|
18378
|
+
signaled = true;
|
|
18379
|
+
firstPaint(served);
|
|
18380
|
+
}
|
|
18381
|
+
};
|
|
18382
|
+
abort.signal.addEventListener("abort", () => signal(true), { once: true });
|
|
18383
|
+
this.feed.loadProgressive(market, (bars) => {
|
|
18384
|
+
paint(bars, false);
|
|
18385
|
+
if (painted) signal(true);
|
|
18386
|
+
}, { signal: abort.signal }).then((full) => {
|
|
18387
|
+
if (this.progressiveAbort === abort) this.progressiveAbort = null;
|
|
18388
|
+
if (full == null) return signal(false);
|
|
18389
|
+
if (this.generation !== gen) return signal(true);
|
|
18390
|
+
paint(full, true);
|
|
18391
|
+
this.completeHistory(full.length >= requested ? "depth" : "genesis");
|
|
18392
|
+
signal(true);
|
|
18393
|
+
}).catch(() => {
|
|
18394
|
+
if (this.progressiveAbort === abort) this.progressiveAbort = null;
|
|
18395
|
+
if (this.generation === gen) this.completeHistory("aborted");
|
|
18396
|
+
signal(true);
|
|
18397
|
+
});
|
|
18398
|
+
});
|
|
18399
|
+
}
|
|
18400
|
+
if (progressiveServed) ; else if (deep && this.feed.loadRange) {
|
|
18151
18401
|
const head = await this.feed.load({ ...market, bars: Math.min(requested, CHUNK_BARS) });
|
|
18152
18402
|
if (this.generation !== gen) return;
|
|
18153
18403
|
this.setBarSeries(head);
|
|
@@ -18742,6 +18992,15 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18742
18992
|
record.pendingCause = "inputs";
|
|
18743
18993
|
if (record.session) record.session.update(record.inputValues);
|
|
18744
18994
|
else if (record.native && !record.hidden) record.native.instance.setInputs(record.inputValues);
|
|
18995
|
+
this.events.emit("indicator:inputs", { id });
|
|
18996
|
+
}
|
|
18997
|
+
/** IndicatorController: the CURRENT stored input values (defaults merged with edits). */
|
|
18998
|
+
inputValuesOf(id) {
|
|
18999
|
+
return { ...this.registry.get(id)?.inputValues };
|
|
19000
|
+
}
|
|
19001
|
+
/** IndicatorController: the CURRENT declaration-prop overrides. */
|
|
19002
|
+
propValuesOf(id) {
|
|
19003
|
+
return { ...this.registry.get(id)?.propValues };
|
|
18745
19004
|
}
|
|
18746
19005
|
/** IndicatorController: re-run an indicator with merged declaration-prop overrides.
|
|
18747
19006
|
* Same lifecycle as {@link applyInputs} — a prop change replays the whole script.
|
|
@@ -18755,6 +19014,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18755
19014
|
if (!record.hidden) this.setLoading(record, true);
|
|
18756
19015
|
record.pendingCause = "inputs";
|
|
18757
19016
|
record.session.update(record.inputValues, record.propValues);
|
|
19017
|
+
this.events.emit("indicator:inputs", { id });
|
|
18758
19018
|
}
|
|
18759
19019
|
/** IndicatorController: tear down an indicator and (if now empty) its pane. */
|
|
18760
19020
|
/** Live handles of every indicator on the chart (script + native), insertion order. */
|
|
@@ -19976,6 +20236,14 @@ function inputVisible(when, values) {
|
|
|
19976
20236
|
const conds = Array.isArray(when) ? when : [when];
|
|
19977
20237
|
return conds.every((c) => c.anyOf ? c.anyOf.some((x) => x === values[c.key]) : values[c.key] === c.equals);
|
|
19978
20238
|
}
|
|
20239
|
+
function inputDeltas(schema, values) {
|
|
20240
|
+
const out = {};
|
|
20241
|
+
for (const s of schema) {
|
|
20242
|
+
const v = values[s.key];
|
|
20243
|
+
if (v !== void 0 && JSON.stringify(v) !== JSON.stringify(s.defval)) out[s.key] = v;
|
|
20244
|
+
}
|
|
20245
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
20246
|
+
}
|
|
19979
20247
|
|
|
19980
20248
|
// src/renderers/shared/IndicatorInputsDialog.ts
|
|
19981
20249
|
var PROPS_TAB = "Properties";
|
|
@@ -20890,6 +21158,9 @@ var CLOSE_SVG = iconAt("close", LEGEND_ICON_PX2);
|
|
|
20890
21158
|
var FOLD_SVG = iconAt("chevron-up", LEGEND_ICON_PX2);
|
|
20891
21159
|
var UNFOLD_SVG = iconAt("chevron-down", LEGEND_ICON_PX2);
|
|
20892
21160
|
var OVERVIEW_SVG = iconAt("objects", LEGEND_ICON_PX2);
|
|
21161
|
+
function legendCalloutsDisplay(open2, hasCallouts) {
|
|
21162
|
+
return !open2 && hasCallouts ? "inline-flex" : "none";
|
|
21163
|
+
}
|
|
20893
21164
|
var InputsUI = class {
|
|
20894
21165
|
constructor(container, theme, paneBoundsOf) {
|
|
20895
21166
|
this.container = container;
|
|
@@ -21093,7 +21364,7 @@ var InputsUI = class {
|
|
|
21093
21364
|
row.callouts = [];
|
|
21094
21365
|
row.calloutsEl.replaceChildren();
|
|
21095
21366
|
const views = this.legendCallouts?.(row.id) ?? [];
|
|
21096
|
-
row.calloutsEl.style.display = views.length > 0
|
|
21367
|
+
row.calloutsEl.style.display = legendCalloutsDisplay(row.highlighted, views.length > 0);
|
|
21097
21368
|
for (const view of views) {
|
|
21098
21369
|
const bubble = new CalloutBubble({
|
|
21099
21370
|
icon: view.icon,
|
|
@@ -21577,11 +21848,11 @@ var InputsUI = class {
|
|
|
21577
21848
|
row.controlsEl.style.display = open2 || row.hidden ? "inline-flex" : "none";
|
|
21578
21849
|
if (open2) {
|
|
21579
21850
|
row.el.appendChild(row.statusEl);
|
|
21580
|
-
row.
|
|
21851
|
+
for (const bubble of row.callouts) bubble.hidePanel();
|
|
21581
21852
|
} else {
|
|
21582
21853
|
row.el.insertBefore(row.statusEl, row.valuesEl);
|
|
21583
|
-
row.el.insertBefore(row.calloutsEl, row.statusEl);
|
|
21584
21854
|
}
|
|
21855
|
+
row.calloutsEl.style.display = legendCalloutsDisplay(open2, row.callouts.length > 0);
|
|
21585
21856
|
for (const child of Array.from(row.controlsEl.children)) {
|
|
21586
21857
|
if (!(child instanceof HTMLElement) || child === row.eyeEl) continue;
|
|
21587
21858
|
if (child === row.extrasEl) {
|
|
@@ -21671,7 +21942,7 @@ var ICONS = {
|
|
|
21671
21942
|
};
|
|
21672
21943
|
var STYLE_ID21 = "vela-pane-controls";
|
|
21673
21944
|
var ICON_PX = 12;
|
|
21674
|
-
var CLUSTER_PILL = "rgba(0,0,0,0.
|
|
21945
|
+
var CLUSTER_PILL = "rgba(0,0,0,0.65)";
|
|
21675
21946
|
function ensureStyles3() {
|
|
21676
21947
|
if (typeof document === "undefined" || document.getElementById(STYLE_ID21)) return;
|
|
21677
21948
|
const st = document.createElement("style");
|
|
@@ -21691,8 +21962,12 @@ var PaneControls = class {
|
|
|
21691
21962
|
this.deps = deps;
|
|
21692
21963
|
this.clusters = /* @__PURE__ */ new Map();
|
|
21693
21964
|
this.hoverPaneId = null;
|
|
21965
|
+
/** Mobile: hover clusters are meaningless without a cursor — suppressed; a
|
|
21966
|
+
* collapsed pane's standalone expand chip stays (the only way back up). */
|
|
21967
|
+
this.suspended = false;
|
|
21694
21968
|
/** Reveal the cluster for the pane under the cursor, resolved from the pointer's y in the plot. */
|
|
21695
21969
|
this.onPlotMove = (e) => {
|
|
21970
|
+
if (this.suspended) return;
|
|
21696
21971
|
const rect = this.plot.getBoundingClientRect();
|
|
21697
21972
|
const y = e.clientY - rect.top;
|
|
21698
21973
|
let hit = null;
|
|
@@ -21764,7 +22039,12 @@ var PaneControls = class {
|
|
|
21764
22039
|
}
|
|
21765
22040
|
if (p.count > 1) {
|
|
21766
22041
|
cluster.appendChild(
|
|
21767
|
-
this.button(p.maximized ? ICONS.restore : ICONS.maximize, p.maximized ? "Restore pane" : "Maximize pane", false, () => this.deps.onToggleMaximize(p.id), {
|
|
22042
|
+
this.button(p.maximized ? ICONS.restore : ICONS.maximize, p.maximized ? "Restore pane" : "Maximize pane", false, () => this.deps.onToggleMaximize(p.id), {
|
|
22043
|
+
role: "maximize",
|
|
22044
|
+
// Same inverse-chip treatment as the collapsed pane's expand toggle: the
|
|
22045
|
+
// maximized state must read as an active state, not just a swapped glyph.
|
|
22046
|
+
selected: p.maximized
|
|
22047
|
+
})
|
|
21768
22048
|
);
|
|
21769
22049
|
}
|
|
21770
22050
|
}
|
|
@@ -21801,17 +22081,18 @@ var PaneControls = class {
|
|
|
21801
22081
|
}
|
|
21802
22082
|
const hovered = id === this.hoverPaneId;
|
|
21803
22083
|
const hasButtons = cluster.children.length > 0;
|
|
21804
|
-
const
|
|
22084
|
+
const stateChipRole = p.collapsed ? "collapse" : !this.suspended && p.maximized ? "maximize" : null;
|
|
22085
|
+
const visible = hasButtons && (hovered || stateChipRole != null) && p.height > 8;
|
|
21805
22086
|
cluster.style.right = `${rightPx}px`;
|
|
21806
22087
|
cluster.style.top = p.collapsed ? `${p.top + Math.max(1, Math.round((p.height - 24) / 2))}px` : `${p.top + 4}px`;
|
|
21807
22088
|
cluster.style.display = visible ? "flex" : "none";
|
|
21808
22089
|
if (!visible) continue;
|
|
21809
|
-
const
|
|
21810
|
-
cluster.style.background =
|
|
22090
|
+
const soloChip = stateChipRole != null && !hovered;
|
|
22091
|
+
cluster.style.background = soloChip ? "transparent" : CLUSTER_PILL;
|
|
21811
22092
|
for (const child of cluster.children) {
|
|
21812
22093
|
const btn2 = child;
|
|
21813
22094
|
btn2.style.display = "inline-flex";
|
|
21814
|
-
btn2.style.visibility =
|
|
22095
|
+
btn2.style.visibility = soloChip && btn2.dataset.role !== stateChipRole ? "hidden" : "visible";
|
|
21815
22096
|
}
|
|
21816
22097
|
}
|
|
21817
22098
|
}
|
|
@@ -21821,6 +22102,14 @@ var PaneControls = class {
|
|
|
21821
22102
|
this.hoverPaneId = paneId;
|
|
21822
22103
|
this.reposition();
|
|
21823
22104
|
}
|
|
22105
|
+
/** Mobile suppression: no hover clusters (touch has no cursor; the shell's own
|
|
22106
|
+
* chrome covers maximize), while collapsed panes keep their expand chips. */
|
|
22107
|
+
setSuspended(on) {
|
|
22108
|
+
if (on === this.suspended) return;
|
|
22109
|
+
this.suspended = on;
|
|
22110
|
+
if (on) this.hoverPaneId = null;
|
|
22111
|
+
this.reposition();
|
|
22112
|
+
}
|
|
21824
22113
|
destroy() {
|
|
21825
22114
|
this.plot.removeEventListener("pointermove", this.onPlotMove);
|
|
21826
22115
|
this.plot.removeEventListener("pointerleave", this.onPlotLeave);
|
|
@@ -21971,160 +22260,6 @@ var AxisScaleButtons = class {
|
|
|
21971
22260
|
}
|
|
21972
22261
|
};
|
|
21973
22262
|
|
|
21974
|
-
// src/renderers/shared/TableOverlay.ts
|
|
21975
|
-
var SIZE_PX3 = {
|
|
21976
|
-
auto: 13,
|
|
21977
|
-
tiny: 10,
|
|
21978
|
-
small: 11,
|
|
21979
|
-
normal: 13,
|
|
21980
|
-
large: 16,
|
|
21981
|
-
huge: 20
|
|
21982
|
-
};
|
|
21983
|
-
function fontPxOf(size) {
|
|
21984
|
-
if (typeof size === "number") return size > 0 ? size : SIZE_PX3.auto;
|
|
21985
|
-
return SIZE_PX3[size] ?? SIZE_PX3.auto;
|
|
21986
|
-
}
|
|
21987
|
-
function tableHasContent(t) {
|
|
21988
|
-
return t.cells.some((row) => row?.some((c) => c != null && !c.merged));
|
|
21989
|
-
}
|
|
21990
|
-
function mergeRenderPlan(t) {
|
|
21991
|
-
const span = /* @__PURE__ */ new Map();
|
|
21992
|
-
const omit = /* @__PURE__ */ new Set();
|
|
21993
|
-
for (const m of t.merges) {
|
|
21994
|
-
span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
|
|
21995
|
-
for (let r = m.startRow; r <= m.endRow; r += 1) {
|
|
21996
|
-
for (let c = m.startCol; c <= m.endCol; c += 1) {
|
|
21997
|
-
if (r !== m.startRow || c !== m.startCol) omit.add(`${r}:${c}`);
|
|
21998
|
-
}
|
|
21999
|
-
}
|
|
22000
|
-
}
|
|
22001
|
-
for (let r = 0; r < t.rows; r += 1) {
|
|
22002
|
-
for (let c = 0; c < t.columns; c += 1) {
|
|
22003
|
-
if (t.cells[r]?.[c]?.merged && !span.has(`${r}:${c}`)) omit.add(`${r}:${c}`);
|
|
22004
|
-
}
|
|
22005
|
-
}
|
|
22006
|
-
for (const key of span.keys()) omit.delete(key);
|
|
22007
|
-
return { span, omit };
|
|
22008
|
-
}
|
|
22009
|
-
var TableOverlay = class {
|
|
22010
|
-
constructor(container, theme, paneBounds) {
|
|
22011
|
-
this.container = container;
|
|
22012
|
-
this.theme = theme;
|
|
22013
|
-
this.paneBounds = paneBounds;
|
|
22014
|
-
this.lastTables = [];
|
|
22015
|
-
if (getComputedStyle(container).position === "static") container.style.position = "relative";
|
|
22016
|
-
this.root = document.createElement("div");
|
|
22017
|
-
Object.assign(this.root.style, {
|
|
22018
|
-
position: "absolute",
|
|
22019
|
-
inset: "0",
|
|
22020
|
-
pointerEvents: "none",
|
|
22021
|
-
overflow: "hidden",
|
|
22022
|
-
zIndex: "3"
|
|
22023
|
-
});
|
|
22024
|
-
container.appendChild(this.root);
|
|
22025
|
-
}
|
|
22026
|
-
update(tables) {
|
|
22027
|
-
this.lastTables = tables;
|
|
22028
|
-
this.root.replaceChildren();
|
|
22029
|
-
for (const t of tables) {
|
|
22030
|
-
if (tableHasContent(t)) this.root.appendChild(this.renderTable(t));
|
|
22031
|
-
}
|
|
22032
|
-
}
|
|
22033
|
-
/** Re-render at the current pane geometry — after layout settles or on resize. */
|
|
22034
|
-
reposition() {
|
|
22035
|
-
if (this.root.isConnected) this.update(this.lastTables);
|
|
22036
|
-
}
|
|
22037
|
-
/** Show/hide the whole overlay. Tables anchor to pane corners, not to bars, so unlike the
|
|
22038
|
-
* series content they DON'T vanish with an emptied chart — the loading state hides them. */
|
|
22039
|
-
setVisible(visible) {
|
|
22040
|
-
this.root.style.display = visible ? "" : "none";
|
|
22041
|
-
}
|
|
22042
|
-
destroy() {
|
|
22043
|
-
this.root.remove();
|
|
22044
|
-
}
|
|
22045
|
-
renderTable(t) {
|
|
22046
|
-
const b = this.paneBounds(t.paneId);
|
|
22047
|
-
const wrap = document.createElement("div");
|
|
22048
|
-
wrap.style.position = "absolute";
|
|
22049
|
-
if (t.frameColor && t.frameWidth > 0) wrap.style.border = `${t.frameWidth}px solid ${t.frameColor}`;
|
|
22050
|
-
this.anchor(wrap, t.position, b);
|
|
22051
|
-
const table = document.createElement("table");
|
|
22052
|
-
Object.assign(table.style, {
|
|
22053
|
-
borderCollapse: "collapse",
|
|
22054
|
-
background: t.bgColor ?? "transparent",
|
|
22055
|
-
fontFamily: this.theme.fontFamily || "sans-serif",
|
|
22056
|
-
border: "none",
|
|
22057
|
-
tableLayout: "auto",
|
|
22058
|
-
// Re-enable pointer events on the table only (root is none) so cell tooltips work.
|
|
22059
|
-
pointerEvents: "auto"
|
|
22060
|
-
});
|
|
22061
|
-
const { span, omit } = mergeRenderPlan(t);
|
|
22062
|
-
const plotW = Math.max(0, (this.root.clientWidth || this.container.clientWidth) - b.rightAxis);
|
|
22063
|
-
const paneH = b.height;
|
|
22064
|
-
const cellBorder = t.borderColor && t.borderWidth > 0 ? `${t.borderWidth}px solid ${t.borderColor}` : "none";
|
|
22065
|
-
for (let r = 0; r < t.rows; r += 1) {
|
|
22066
|
-
const tr = document.createElement("tr");
|
|
22067
|
-
for (let c = 0; c < t.columns; c += 1) {
|
|
22068
|
-
if (omit.has(`${r}:${c}`)) continue;
|
|
22069
|
-
const cell = t.cells[r]?.[c] ?? null;
|
|
22070
|
-
const td = document.createElement("td");
|
|
22071
|
-
if (cell === null) {
|
|
22072
|
-
Object.assign(td.style, { padding: "0", border: "none" });
|
|
22073
|
-
tr.appendChild(td);
|
|
22074
|
-
continue;
|
|
22075
|
-
}
|
|
22076
|
-
const sp = span.get(`${r}:${c}`);
|
|
22077
|
-
if (sp) {
|
|
22078
|
-
if (sp.cs > 1) td.colSpan = sp.cs;
|
|
22079
|
-
if (sp.rs > 1) td.rowSpan = sp.rs;
|
|
22080
|
-
}
|
|
22081
|
-
Object.assign(td.style, {
|
|
22082
|
-
border: cellBorder,
|
|
22083
|
-
padding: "2px 6px",
|
|
22084
|
-
background: cell.bgColor ?? "transparent",
|
|
22085
|
-
color: cell.textColor ?? this.theme.textColor,
|
|
22086
|
-
textAlign: cell.hAlign,
|
|
22087
|
-
verticalAlign: cell.vAlign === "top" ? "top" : cell.vAlign === "bottom" ? "bottom" : "middle",
|
|
22088
|
-
fontSize: `${fontPxOf(cell.textSize)}px`,
|
|
22089
|
-
fontFamily: cell.fontFamily === "monospace" ? "monospace" : "inherit",
|
|
22090
|
-
fontWeight: cell.bold ? "bold" : "normal",
|
|
22091
|
-
fontStyle: cell.italic ? "italic" : "normal",
|
|
22092
|
-
// Pine cell text never wraps; `\n` still breaks lines. Wrapping
|
|
22093
|
-
// used to collapse unicode sparklines and ━━━ dividers.
|
|
22094
|
-
whiteSpace: "pre"
|
|
22095
|
-
});
|
|
22096
|
-
if (cell.width) td.style.width = `${cell.width / 100 * plotW}px`;
|
|
22097
|
-
if (cell.height) td.style.height = `${cell.height / 100 * paneH}px`;
|
|
22098
|
-
if (cell.tooltip) td.title = cell.tooltip;
|
|
22099
|
-
td.textContent = cell.text ?? "";
|
|
22100
|
-
tr.appendChild(td);
|
|
22101
|
-
}
|
|
22102
|
-
table.appendChild(tr);
|
|
22103
|
-
}
|
|
22104
|
-
wrap.appendChild(table);
|
|
22105
|
-
return wrap;
|
|
22106
|
-
}
|
|
22107
|
-
/**
|
|
22108
|
-
* Position the wrapper at a Pine `position.*` corner/edge of the table's PANE
|
|
22109
|
-
* (not the whole chart), inset past the right price axis so it never overlaps
|
|
22110
|
-
* the Y-axis labels.
|
|
22111
|
-
*/
|
|
22112
|
-
anchor(el, position, b) {
|
|
22113
|
-
const m = 6;
|
|
22114
|
-
const containerH = this.root.clientHeight || this.container.clientHeight;
|
|
22115
|
-
const paneBottom = b.top + b.height;
|
|
22116
|
-
if (position.startsWith("top")) el.style.top = `${b.top + m}px`;
|
|
22117
|
-
else if (position.startsWith("bottom")) el.style.bottom = `${Math.max(0, containerH - paneBottom) + m}px`;
|
|
22118
|
-
else el.style.top = `${b.top + b.height / 2}px`;
|
|
22119
|
-
if (position.endsWith("left")) el.style.left = `${m}px`;
|
|
22120
|
-
else if (position.endsWith("right")) el.style.right = `${b.rightAxis + m}px`;
|
|
22121
|
-
else el.style.left = `calc(50% - ${b.rightAxis / 2}px)`;
|
|
22122
|
-
const tx = position.endsWith("center") ? "-50%" : "0";
|
|
22123
|
-
const ty = position.startsWith("middle") ? "-50%" : "0";
|
|
22124
|
-
if (tx !== "0" || ty !== "0") el.style.transform = `translate(${tx}, ${ty})`;
|
|
22125
|
-
}
|
|
22126
|
-
};
|
|
22127
|
-
|
|
22128
22263
|
// src/renderers/native/capabilities.ts
|
|
22129
22264
|
var NATIVE_CAPABILITIES = {
|
|
22130
22265
|
panes: true,
|
|
@@ -22145,7 +22280,7 @@ var NATIVE_CAPABILITIES = {
|
|
|
22145
22280
|
drawingDepth: true,
|
|
22146
22281
|
// drawings share the series' z space (backend-composited interleave layers)
|
|
22147
22282
|
tables: true,
|
|
22148
|
-
//
|
|
22283
|
+
// canvas-painted into the owning indicator's interleave slice
|
|
22149
22284
|
trades: true,
|
|
22150
22285
|
// strategy order-fill markers (arrows + labels + fill-price ticks)
|
|
22151
22286
|
inputsUI: true
|
|
@@ -22176,6 +22311,9 @@ function candleTier(spacing) {
|
|
|
22176
22311
|
if (spacing < CANDLE_BODY_MIN_SPACING) return "wick";
|
|
22177
22312
|
return "full";
|
|
22178
22313
|
}
|
|
22314
|
+
function snapY(yCss, dpr) {
|
|
22315
|
+
return Math.round(yCss * dpr) / dpr;
|
|
22316
|
+
}
|
|
22179
22317
|
function candleGeometry(xCss, spacing, dpr, bodyScale = 1) {
|
|
22180
22318
|
const wickDev = Math.max(1, Math.round(wickWidth(spacing) * dpr));
|
|
22181
22319
|
const wickLeftDev = Math.round(xCss * dpr - wickDev / 2);
|
|
@@ -22699,11 +22837,9 @@ var WebGL2Backend = class {
|
|
|
22699
22837
|
};
|
|
22700
22838
|
b.alpha = this.modelAlpha;
|
|
22701
22839
|
for (const m of models) for (const bgSpan of m.backgrounds) if (bgSpan.overlay !== true) this.emitBackground(b, bgSpan, pane, coords);
|
|
22702
|
-
for (const m of models) for (const f of m.fills) if (f.overlay !== true) this.emitFill(b, m, f, effPane(m), coords, i0, i1, scene.offsetOf(m.id));
|
|
22703
22840
|
if (isPrice) {
|
|
22704
22841
|
for (const m of scene.indicators.values()) {
|
|
22705
22842
|
for (const bgSpan of m.backgrounds) if (bgSpan.overlay === true) this.emitBackground(b, bgSpan, pane, coords);
|
|
22706
|
-
for (const f of m.fills) if (f.overlay === true) this.emitFill(b, m, f, pane, coords, i0, i1, scene.offsetOf(m.id));
|
|
22707
22843
|
}
|
|
22708
22844
|
}
|
|
22709
22845
|
const drawCandles = isPrice && !scene.candlesHidden;
|
|
@@ -22719,6 +22855,7 @@ var WebGL2Backend = class {
|
|
|
22719
22855
|
b.alpha = this.modelAlpha;
|
|
22720
22856
|
const off = scene.offsetOf(m.id);
|
|
22721
22857
|
const mp = effPane(m);
|
|
22858
|
+
for (const f of m.fills) if (f.overlay !== true) this.emitFill(b, m, f, mp, coords, i0, i1, off);
|
|
22722
22859
|
for (const s of m.series) if (s.overlay !== true) this.emitSeries(b, s, mp, coords, i0, i1, theme, off);
|
|
22723
22860
|
}
|
|
22724
22861
|
if (drawCandles && !candleDrawn) {
|
|
@@ -22726,14 +22863,18 @@ var WebGL2Backend = class {
|
|
|
22726
22863
|
b.alpha = this.candleStructureAlpha;
|
|
22727
22864
|
this.emitPriceSeries(b, scene, i0, i1, coords, pane, theme, barColorMap, dataW);
|
|
22728
22865
|
}
|
|
22729
|
-
drawSlicesUpTo(Infinity);
|
|
22730
22866
|
b.alpha = this.modelAlpha;
|
|
22731
22867
|
if (isPrice) {
|
|
22868
|
+
for (const m of scene.indicators.values()) {
|
|
22869
|
+
const off = scene.offsetOf(m.id);
|
|
22870
|
+
for (const f of m.fills) if (f.overlay === true) this.emitFill(b, m, f, pane, coords, i0, i1, off);
|
|
22871
|
+
}
|
|
22732
22872
|
for (const m of scene.indicators.values()) {
|
|
22733
22873
|
const off = scene.offsetOf(m.id);
|
|
22734
22874
|
for (const s of m.series) if (s.overlay === true) this.emitSeries(b, s, pane, coords, i0, i1, theme, off);
|
|
22735
22875
|
}
|
|
22736
22876
|
}
|
|
22877
|
+
drawSlicesUpTo(Infinity);
|
|
22737
22878
|
for (const m of models) {
|
|
22738
22879
|
const mp = effPane(m);
|
|
22739
22880
|
for (const pl of m.priceLines) this.emitHline(b, pl, mp, coords, dataW, theme);
|
|
@@ -23128,12 +23269,12 @@ var WebGL2Backend = class {
|
|
|
23128
23269
|
if (drawBody) {
|
|
23129
23270
|
const oY = coords.priceToY(bar.open, pane.scale, pane.bounds);
|
|
23130
23271
|
const cY = coords.priceToY(bar.close, pane.scale, pane.bounds);
|
|
23131
|
-
bodyTop = Math.min(oY, cY);
|
|
23132
|
-
bodyH = Math.max(1, Math.
|
|
23272
|
+
bodyTop = snapY(Math.min(oY, cY), coords.dpr);
|
|
23273
|
+
bodyH = Math.max(1 / coords.dpr, snapY(Math.max(oY, cY), coords.dpr) - bodyTop);
|
|
23133
23274
|
}
|
|
23134
23275
|
if (cs.wickVisible) {
|
|
23135
|
-
const hY = coords.priceToY(bar.high, pane.scale, pane.bounds);
|
|
23136
|
-
const lY = coords.priceToY(bar.low, pane.scale, pane.bounds);
|
|
23276
|
+
const hY = snapY(coords.priceToY(bar.high, pane.scale, pane.bounds), coords.dpr);
|
|
23277
|
+
const lY = snapY(coords.priceToY(bar.low, pane.scale, pane.bounds), coords.dpr);
|
|
23137
23278
|
b.alpha = this.candleStructureAlpha;
|
|
23138
23279
|
const wCol = parseColor((isUp ? cs.wickUpColor : cs.wickDownColor) ?? (drawBody ? dir : bodyColorStr));
|
|
23139
23280
|
if (drawBody) {
|
|
@@ -23690,6 +23831,7 @@ var DOUBLE_TAP_MS = 350;
|
|
|
23690
23831
|
var DOUBLE_TAP_SLOP = 30;
|
|
23691
23832
|
var TIME_SCALE_K = 4e-3;
|
|
23692
23833
|
var WHEEL_ZOOM_K = 4e-3;
|
|
23834
|
+
var WHEEL_PRICE_DRAG_PX = 0.25;
|
|
23693
23835
|
function wheelZoomAnchor(coords, cursorX, rightEdge) {
|
|
23694
23836
|
if (rightEdge) return { logical: coords.rightEdgeLogical, x: coords.width };
|
|
23695
23837
|
return { logical: coords.xToLogical(cursorX), x: cursorX };
|
|
@@ -23820,7 +23962,7 @@ var InputController = class {
|
|
|
23820
23962
|
this.capture(e.pointerId);
|
|
23821
23963
|
return;
|
|
23822
23964
|
}
|
|
23823
|
-
if (e.shiftKey && this.regionAt(x, y) === "data" && this.deps.drawingsMeasureStart?.(x, y)) {
|
|
23965
|
+
if (e.shiftKey && this.regionAt(x, y) === "data" && this.deps.drawingsMeasureStart?.(x, y, this.snapMode(e))) {
|
|
23824
23966
|
this.region = "drawing";
|
|
23825
23967
|
this.capture(e.pointerId);
|
|
23826
23968
|
return;
|
|
@@ -23934,7 +24076,7 @@ var InputController = class {
|
|
|
23934
24076
|
const wasTouch = e.pointerType === "touch";
|
|
23935
24077
|
const tapRelease = this.dragging && !this.moved && (!wasTouch || Math.hypot(x - this.startX, y - this.startY) <= TOUCH_TAP_SLOP);
|
|
23936
24078
|
if (this.dragging && this.region === "drawing") {
|
|
23937
|
-
this.deps.drawingsPointerUp?.(x, y);
|
|
24079
|
+
this.deps.drawingsPointerUp?.(x, y, this.snapMode(e));
|
|
23938
24080
|
} else if (tapRelease && this.region === "data") {
|
|
23939
24081
|
this.deps.onClick(x, y);
|
|
23940
24082
|
} else if (this.dragging && this.region === "data") {
|
|
@@ -23955,7 +24097,7 @@ var InputController = class {
|
|
|
23955
24097
|
if (e.pointerType === "touch") this.touches.delete(e.pointerId);
|
|
23956
24098
|
this.cancelLongPress();
|
|
23957
24099
|
if (!this.dragging) return;
|
|
23958
|
-
if (this.region === "drawing" && !Number.isNaN(this.cursorX)) this.deps.drawingsPointerUp?.(this.cursorX, this.cursorY);
|
|
24100
|
+
if (this.region === "drawing" && !Number.isNaN(this.cursorX)) this.deps.drawingsPointerUp?.(this.cursorX, this.cursorY, this.snapMode(e));
|
|
23959
24101
|
if (this.region === "crosshair" || e.pointerType === "touch") this.deps.onPointerMove(null, null);
|
|
23960
24102
|
this.endGesture(e);
|
|
23961
24103
|
};
|
|
@@ -23971,6 +24113,12 @@ var InputController = class {
|
|
|
23971
24113
|
this.onWheel = (e) => {
|
|
23972
24114
|
e.preventDefault();
|
|
23973
24115
|
this.deps.drawingsClearTransient?.();
|
|
24116
|
+
const { x, y } = this.local(e);
|
|
24117
|
+
if (this.regionAt(x, y) === "price" && e.deltaY !== 0) {
|
|
24118
|
+
this.deps.beginPriceScale(x, y);
|
|
24119
|
+
this.deps.priceScaleBy(e.deltaY * WHEEL_PRICE_DRAG_PX);
|
|
24120
|
+
return;
|
|
24121
|
+
}
|
|
23974
24122
|
const coords = this.deps.getCoords();
|
|
23975
24123
|
const vp = coords.getViewport();
|
|
23976
24124
|
const pan = wheelPanDelta(e.deltaX, e.deltaY, e.shiftKey);
|
|
@@ -23978,9 +24126,8 @@ var InputController = class {
|
|
|
23978
24126
|
this.deps.apply({ barSpacing: vp.barSpacing, rightOffset: wheelPanRightOffset(vp.rightOffset, pan, coords.pxPerBar()) });
|
|
23979
24127
|
return;
|
|
23980
24128
|
}
|
|
23981
|
-
const cursorX = this.local(e).x;
|
|
23982
24129
|
const rightEdge = this.rightEdgeZoom && !(e.ctrlKey || e.metaKey);
|
|
23983
|
-
const anchor = wheelZoomAnchor(coords,
|
|
24130
|
+
const anchor = wheelZoomAnchor(coords, x, rightEdge);
|
|
23984
24131
|
const target = clampBarSpacing(vp.barSpacing * Math.exp(-e.deltaY * WHEEL_ZOOM_K));
|
|
23985
24132
|
this.deps.zoomTo(target, anchor.logical, anchor.x);
|
|
23986
24133
|
};
|
|
@@ -24466,9 +24613,10 @@ var SceneGraph = class {
|
|
|
24466
24613
|
* so each indicator arrives behind the candles (and behind older indicators);
|
|
24467
24614
|
* `setIndicatorZ`/`bringToFront`/`sendToBack` change it. */
|
|
24468
24615
|
this.seriesZ = /* @__PURE__ */ new Map();
|
|
24469
|
-
/** Per-pane raster layers of
|
|
24616
|
+
/** Per-pane raster layers of drawings interleaved into the series stack (each
|
|
24617
|
+
* indicator's Pine drawings at its model's z, plus in-stack user drawings) — each a
|
|
24470
24618
|
* prepainted canvas the backend composites just before the series carrying `beforeZ`.
|
|
24471
|
-
* Rebuilt by the renderer per data frame
|
|
24619
|
+
* Rebuilt by the renderer per data frame. */
|
|
24472
24620
|
this.drawingSlices = /* @__PURE__ */ new Map();
|
|
24473
24621
|
/** Per-model index offset: the chart bar index of the model's `anchorTime` — its
|
|
24474
24622
|
* index-aligned payloads (dense series arrays, `bar_index` drawings) count from that
|
|
@@ -24703,11 +24851,9 @@ var Canvas2dBackend = class {
|
|
|
24703
24851
|
};
|
|
24704
24852
|
ctx.globalAlpha = this.modelAlpha;
|
|
24705
24853
|
for (const m of models) for (const bg of m.backgrounds) if (bg.overlay !== true) this.drawBackground(ctx, bg, pane, coords);
|
|
24706
|
-
for (const m of models) for (const f of m.fills) if (f.overlay !== true) this.drawFill(ctx, m, f, effPane(m), coords, i0, i1, scene.offsetOf(m.id));
|
|
24707
24854
|
if (isPrice) {
|
|
24708
24855
|
for (const m of scene.indicators.values()) {
|
|
24709
24856
|
for (const bg of m.backgrounds) if (bg.overlay === true) this.drawBackground(ctx, bg, pane, coords);
|
|
24710
|
-
for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0, i1, scene.offsetOf(m.id));
|
|
24711
24857
|
}
|
|
24712
24858
|
}
|
|
24713
24859
|
const slices = scene.drawingSlices.get(pane.id) ?? [];
|
|
@@ -24731,6 +24877,7 @@ var Canvas2dBackend = class {
|
|
|
24731
24877
|
ctx.globalAlpha = this.modelAlpha;
|
|
24732
24878
|
const off = scene.offsetOf(m.id);
|
|
24733
24879
|
const mp = effPane(m);
|
|
24880
|
+
for (const f of m.fills) if (f.overlay !== true) this.drawFill(ctx, m, f, mp, coords, i0, i1, off);
|
|
24734
24881
|
for (const s of m.series) if (s.overlay !== true) this.drawSeries(ctx, s, mp, coords, i0, i1, theme, off);
|
|
24735
24882
|
}
|
|
24736
24883
|
if (drawCandles && !candleDrawn) {
|
|
@@ -24738,14 +24885,18 @@ var Canvas2dBackend = class {
|
|
|
24738
24885
|
ctx.globalAlpha = this.candleStructureAlpha;
|
|
24739
24886
|
this.drawPriceSeries(ctx, scene, i0, i1, coords, pane, theme, barColorMap, dataW);
|
|
24740
24887
|
}
|
|
24741
|
-
drawSlicesUpTo(Infinity);
|
|
24742
24888
|
if (isPrice) {
|
|
24743
24889
|
ctx.globalAlpha = this.modelAlpha;
|
|
24890
|
+
for (const m of scene.indicators.values()) {
|
|
24891
|
+
const off = scene.offsetOf(m.id);
|
|
24892
|
+
for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0, i1, off);
|
|
24893
|
+
}
|
|
24744
24894
|
for (const m of scene.indicators.values()) {
|
|
24745
24895
|
const off = scene.offsetOf(m.id);
|
|
24746
24896
|
for (const s of m.series) if (s.overlay === true) this.drawSeries(ctx, s, pane, coords, i0, i1, theme, off);
|
|
24747
24897
|
}
|
|
24748
24898
|
}
|
|
24899
|
+
drawSlicesUpTo(Infinity);
|
|
24749
24900
|
ctx.globalAlpha = this.modelAlpha;
|
|
24750
24901
|
for (const m of models) {
|
|
24751
24902
|
const mp = effPane(m);
|
|
@@ -24989,13 +25140,13 @@ var Canvas2dBackend = class {
|
|
|
24989
25140
|
if (drawBody) {
|
|
24990
25141
|
const oY = coords.priceToY(b.open, pane.scale, pane.bounds);
|
|
24991
25142
|
const cY = coords.priceToY(b.close, pane.scale, pane.bounds);
|
|
24992
|
-
top = Math.min(oY, cY);
|
|
24993
|
-
bodyH = Math.max(1, Math.
|
|
25143
|
+
top = snapY(Math.min(oY, cY), coords.dpr);
|
|
25144
|
+
bodyH = Math.max(1 / coords.dpr, snapY(Math.max(oY, cY), coords.dpr) - top);
|
|
24994
25145
|
}
|
|
24995
25146
|
if (cs.wickVisible) {
|
|
24996
25147
|
const wick = (up ? cs.wickUpColor : cs.wickDownColor) ?? (drawBody ? dir : color);
|
|
24997
|
-
const hY = coords.priceToY(b.high, pane.scale, pane.bounds);
|
|
24998
|
-
const lY = coords.priceToY(b.low, pane.scale, pane.bounds);
|
|
25148
|
+
const hY = snapY(coords.priceToY(b.high, pane.scale, pane.bounds), coords.dpr);
|
|
25149
|
+
const lY = snapY(coords.priceToY(b.low, pane.scale, pane.bounds), coords.dpr);
|
|
24999
25150
|
ctx.globalAlpha = this.candleStructureAlpha;
|
|
25000
25151
|
ctx.strokeStyle = wick;
|
|
25001
25152
|
ctx.lineWidth = g.wickW;
|
|
@@ -25442,9 +25593,31 @@ function autoFontSize(lines, boxW, boxH, bold) {
|
|
|
25442
25593
|
|
|
25443
25594
|
// src/renderers/shared/DrawingSceneRenderer.ts
|
|
25444
25595
|
var EMPTY_DRAWING_SET = { lines: [], boxes: [], labels: [], polylines: [], linefills: [] };
|
|
25596
|
+
function modelDrawingSet(m, overlay) {
|
|
25597
|
+
const want = (d) => Boolean(d.overlay) === overlay;
|
|
25598
|
+
return {
|
|
25599
|
+
lines: (m.lines ?? []).filter(want),
|
|
25600
|
+
boxes: (m.boxes ?? []).filter(want),
|
|
25601
|
+
labels: (m.labels ?? []).filter(want),
|
|
25602
|
+
polylines: (m.polylines ?? []).filter(want),
|
|
25603
|
+
linefills: (m.linefills ?? []).filter(want)
|
|
25604
|
+
};
|
|
25605
|
+
}
|
|
25606
|
+
function drawingSetEmpty(s) {
|
|
25607
|
+
return !s.lines.length && !s.boxes.length && !s.labels.length && !s.polylines.length && !s.linefills.length;
|
|
25608
|
+
}
|
|
25445
25609
|
function fontSizePx(size) {
|
|
25446
25610
|
return size === "auto" ? 12 : namedFontSize(size);
|
|
25447
25611
|
}
|
|
25612
|
+
function lineCoversWindow(a, b, extend, lo, hi) {
|
|
25613
|
+
const minX = Math.min(a, b);
|
|
25614
|
+
const maxX = Math.max(a, b);
|
|
25615
|
+
if (a === b) return a >= lo && a <= hi;
|
|
25616
|
+
if (extend === "both") return true;
|
|
25617
|
+
if (extend === "left") return maxX >= lo;
|
|
25618
|
+
if (extend === "right") return minX <= hi;
|
|
25619
|
+
return maxX >= lo && minX <= hi;
|
|
25620
|
+
}
|
|
25448
25621
|
var DrawingSceneRenderer = class {
|
|
25449
25622
|
constructor(deps, set = EMPTY_DRAWING_SET) {
|
|
25450
25623
|
this.deps = deps;
|
|
@@ -25509,7 +25682,7 @@ var DrawingSceneRenderer = class {
|
|
|
25509
25682
|
};
|
|
25510
25683
|
for (const ln of this.set.lines) {
|
|
25511
25684
|
if (ln.invisible) continue;
|
|
25512
|
-
if (!
|
|
25685
|
+
if (!lineCoversWindow(this.logicalOf(ln.xloc, ln.x1), this.logicalOf(ln.xloc, ln.x2), ln.extend, lo, hi)) continue;
|
|
25513
25686
|
fold(ln.y1);
|
|
25514
25687
|
fold(ln.y2);
|
|
25515
25688
|
}
|
|
@@ -26294,10 +26467,8 @@ var ChromeRenderer = class {
|
|
|
26294
26467
|
this.ctx = null;
|
|
26295
26468
|
// The color for axis tick labels — the host-passed surface text, set each frame in render().
|
|
26296
26469
|
this.axisTextColor = DARK_THEME.textColor;
|
|
26297
|
-
// Shared Pine-drawing renderer
|
|
26470
|
+
// Shared Pine-drawing renderer, used here for autoscale geometry only; widthCache persists.
|
|
26298
26471
|
this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
|
|
26299
|
-
// Tooltip hit-rects of every label drawn this frame, in plot coords (rebuilt per render).
|
|
26300
|
-
this.labelTips = [];
|
|
26301
26472
|
}
|
|
26302
26473
|
mount(canvas) {
|
|
26303
26474
|
this.canvas = canvas;
|
|
@@ -26321,8 +26492,8 @@ var ChromeRenderer = class {
|
|
|
26321
26492
|
*/
|
|
26322
26493
|
paneDrawingsRange(ownModels, scene, isPricePane, vr) {
|
|
26323
26494
|
let dr = null;
|
|
26324
|
-
for (const m of ownModels) dr = unionRange(dr, this.drawingsRange(
|
|
26325
|
-
if (isPricePane) for (const m of scene.indicators.values()) dr = unionRange(dr, this.drawingsRange(
|
|
26495
|
+
for (const m of ownModels) dr = unionRange(dr, this.drawingsRange(modelDrawingSet(m, false), vr, scene.offsetOf(m.id)));
|
|
26496
|
+
if (isPricePane) for (const m of scene.indicators.values()) dr = unionRange(dr, this.drawingsRange(modelDrawingSet(m, true), vr, scene.offsetOf(m.id)));
|
|
26326
26497
|
return dr;
|
|
26327
26498
|
}
|
|
26328
26499
|
/** Clear the chrome canvas and draw drawings + axes + current-price line.
|
|
@@ -26339,7 +26510,6 @@ var ChromeRenderer = class {
|
|
|
26339
26510
|
const dataW = coords.width;
|
|
26340
26511
|
const dataH = coords.height;
|
|
26341
26512
|
this.axisTextColor = surface?.textColor ?? theme.textColor;
|
|
26342
|
-
this.labelTips = [];
|
|
26343
26513
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
26344
26514
|
ctx.clearRect(0, 0, fullW, fullH);
|
|
26345
26515
|
if (surface && (fullW > dataW || fullH > dataH)) {
|
|
@@ -26355,17 +26525,6 @@ var ChromeRenderer = class {
|
|
|
26355
26525
|
return;
|
|
26356
26526
|
}
|
|
26357
26527
|
const pricePane = panes.find((p) => p.kind === "price") ?? null;
|
|
26358
|
-
for (const pane of panes) {
|
|
26359
|
-
if (pane.collapsed) continue;
|
|
26360
|
-
for (const m of scene.indicatorsForPane(pane.id)) {
|
|
26361
|
-
const sc = scene.scaleFor(m, pane);
|
|
26362
|
-
const mp = sc === pane.scale ? pane : { ...pane, scale: sc };
|
|
26363
|
-
this.renderDrawings(ctx, coords, this.ownDrawings(m), mp, dataW, scene.offsetOf(m.id));
|
|
26364
|
-
}
|
|
26365
|
-
}
|
|
26366
|
-
if (pricePane) {
|
|
26367
|
-
for (const m of scene.indicators.values()) this.renderDrawings(ctx, coords, this.overlayDrawings(m), pricePane, dataW, scene.offsetOf(m.id));
|
|
26368
|
-
}
|
|
26369
26528
|
if (pricePane && !pricePane.collapsed && scene.tradeMarkers.visible) {
|
|
26370
26529
|
for (const m of scene.indicators.values()) {
|
|
26371
26530
|
if (m.trades?.length) this.renderTrades(ctx, coords, scene, theme, m.trades, pricePane, dataW);
|
|
@@ -26381,25 +26540,6 @@ var ChromeRenderer = class {
|
|
|
26381
26540
|
this.canvas = null;
|
|
26382
26541
|
this.ctx = null;
|
|
26383
26542
|
}
|
|
26384
|
-
// ── Pine-drawing helpers (own vs force_overlay routing) ──
|
|
26385
|
-
ownDrawings(m) {
|
|
26386
|
-
return {
|
|
26387
|
-
lines: (m.lines ?? []).filter((d) => !d.overlay),
|
|
26388
|
-
boxes: (m.boxes ?? []).filter((d) => !d.overlay),
|
|
26389
|
-
labels: (m.labels ?? []).filter((d) => !d.overlay),
|
|
26390
|
-
polylines: (m.polylines ?? []).filter((d) => !d.overlay),
|
|
26391
|
-
linefills: (m.linefills ?? []).filter((d) => !d.overlay)
|
|
26392
|
-
};
|
|
26393
|
-
}
|
|
26394
|
-
overlayDrawings(m) {
|
|
26395
|
-
return {
|
|
26396
|
-
lines: (m.lines ?? []).filter((d) => d.overlay),
|
|
26397
|
-
boxes: (m.boxes ?? []).filter((d) => d.overlay),
|
|
26398
|
-
labels: (m.labels ?? []).filter((d) => d.overlay),
|
|
26399
|
-
polylines: (m.polylines ?? []).filter((d) => d.overlay),
|
|
26400
|
-
linefills: (m.linefills ?? []).filter((d) => d.overlay)
|
|
26401
|
-
};
|
|
26402
|
-
}
|
|
26403
26543
|
drawingsRange(set, vr, indexOffset = 0) {
|
|
26404
26544
|
this.drawScene.setSet(set, indexOffset);
|
|
26405
26545
|
if (this.drawScene.isEmpty()) return null;
|
|
@@ -26433,34 +26573,6 @@ var ChromeRenderer = class {
|
|
|
26433
26573
|
);
|
|
26434
26574
|
ctx.restore();
|
|
26435
26575
|
}
|
|
26436
|
-
renderDrawings(ctx, coords, set, pane, dataW, indexOffset = 0) {
|
|
26437
|
-
this.drawScene.setSet(set, indexOffset);
|
|
26438
|
-
if (this.drawScene.isEmpty()) return;
|
|
26439
|
-
ctx.save();
|
|
26440
|
-
ctx.translate(0, pane.bounds.top);
|
|
26441
|
-
ctx.beginPath();
|
|
26442
|
-
ctx.rect(0, 0, dataW, pane.bounds.height);
|
|
26443
|
-
ctx.clip();
|
|
26444
|
-
this.drawScene.render(
|
|
26445
|
-
ctx,
|
|
26446
|
-
dataW,
|
|
26447
|
-
pane.bounds.height,
|
|
26448
|
-
(l) => coords.logicalToX(l),
|
|
26449
|
-
(price) => coords.priceToY(price, pane.scale, pane.bounds) - pane.bounds.top
|
|
26450
|
-
);
|
|
26451
|
-
ctx.restore();
|
|
26452
|
-
for (const r of this.drawScene.labelTipRegions()) {
|
|
26453
|
-
this.labelTips.push({ ...r, top: r.top + pane.bounds.top, bottom: r.bottom + pane.bounds.top });
|
|
26454
|
-
}
|
|
26455
|
-
}
|
|
26456
|
-
/** Tooltip of the topmost label under a plot-space point, or null. Fed by the last render. */
|
|
26457
|
-
labelTooltipAt(x, y) {
|
|
26458
|
-
for (let i = this.labelTips.length - 1; i >= 0; i -= 1) {
|
|
26459
|
-
const r = this.labelTips[i];
|
|
26460
|
-
if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return r.text;
|
|
26461
|
-
}
|
|
26462
|
-
return null;
|
|
26463
|
-
}
|
|
26464
26576
|
// ── axes ──
|
|
26465
26577
|
drawPriceAxes(ctx, scene, coords, theme, dataW, panes) {
|
|
26466
26578
|
ctx.strokeStyle = scene.style.borderColor ?? theme.borderColor;
|
|
@@ -29420,6 +29532,22 @@ var DrawingInteraction = class {
|
|
|
29420
29532
|
this.snapAt = changed ? { point: snapped, paneId } : null;
|
|
29421
29533
|
return snapped;
|
|
29422
29534
|
}
|
|
29535
|
+
/**
|
|
29536
|
+
* Resolve a cursor pixel through the magnet and return the snapped pixel — the same
|
|
29537
|
+
* conversion drawing placement uses. Updates the snap-ring marker. The measure
|
|
29538
|
+
* ruler goes through this so its endpoints follow weak/strong/Ctrl magnet too.
|
|
29539
|
+
*/
|
|
29540
|
+
snapCursor(x, y, mode) {
|
|
29541
|
+
const proj = this.deps.projector();
|
|
29542
|
+
const paneId = proj.paneIdAtY(y) ?? "price";
|
|
29543
|
+
const point = this.resolve(x, y, paneId, mode);
|
|
29544
|
+
const sy = proj.yOf(point.price, paneId);
|
|
29545
|
+
return { x: proj.xOf(point.time), y: sy ?? y };
|
|
29546
|
+
}
|
|
29547
|
+
/** Drop the snap-ring marker (a transient mode ended without going through `up`). */
|
|
29548
|
+
clearSnapMarker() {
|
|
29549
|
+
this.snapAt = null;
|
|
29550
|
+
}
|
|
29423
29551
|
/** Resolve a pixel to a data point with the segment angle locked to 45° steps around
|
|
29424
29552
|
* `pivot` (Shift held on a line tool). Works in PIXEL space — the user reasons about
|
|
29425
29553
|
* the angle they see, not about time/price units. The magnet is bypassed: snapping
|
|
@@ -30779,17 +30907,17 @@ function glyphIcon(glyph) {
|
|
|
30779
30907
|
return textGlyph(String(glyph), 15);
|
|
30780
30908
|
}
|
|
30781
30909
|
function stampSizeIcon(size) {
|
|
30782
|
-
return textGlyph("\u25CF", (
|
|
30910
|
+
return textGlyph("\u25CF", (SIZE_PX3[String(size)] ?? 13) + 4);
|
|
30783
30911
|
}
|
|
30784
30912
|
function sizeIcon(size) {
|
|
30785
30913
|
return textGlyph(String(size).charAt(0).toUpperCase(), 15);
|
|
30786
30914
|
}
|
|
30787
|
-
var
|
|
30915
|
+
var SIZE_PX3 = { small: 10, normal: 13, large: 16, huge: 20 };
|
|
30788
30916
|
function numbersSizeIcon(size) {
|
|
30789
|
-
return textGlyph("12", (
|
|
30917
|
+
return textGlyph("12", (SIZE_PX3[String(size)] ?? 13) - 1, 16.5, 'font-weight="600"');
|
|
30790
30918
|
}
|
|
30791
30919
|
function labelSizeIcon(size) {
|
|
30792
|
-
return textGlyph("T", (
|
|
30920
|
+
return textGlyph("T", (SIZE_PX3[String(size)] ?? 13) + 2);
|
|
30793
30921
|
}
|
|
30794
30922
|
function capitalize(s) {
|
|
30795
30923
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
@@ -30825,30 +30953,39 @@ var MeasureOverlay = class {
|
|
|
30825
30953
|
isFinished() {
|
|
30826
30954
|
return this.state === "finished";
|
|
30827
30955
|
}
|
|
30828
|
-
/** A press: begin the measurement, or finish it on the second click.
|
|
30829
|
-
|
|
30956
|
+
/** A press: begin the measurement, or finish it on the second click.
|
|
30957
|
+
* `x,y` are the raw cursor (drag-slop vs click-move-click). `gx,gy` are the
|
|
30958
|
+
* graphic endpoints — magnet-snapped when the magnet is on, else the same as `x,y`. */
|
|
30959
|
+
down(x, y, gx = x, gy = y) {
|
|
30830
30960
|
if (this.state === "measuring") {
|
|
30831
|
-
this.end = { x, y };
|
|
30961
|
+
this.end = { x: gx, y: gy };
|
|
30832
30962
|
this.state = "finished";
|
|
30833
30963
|
return;
|
|
30834
30964
|
}
|
|
30835
|
-
this.start = { x, y };
|
|
30836
|
-
this.end = { x, y };
|
|
30965
|
+
this.start = { x: gx, y: gy };
|
|
30966
|
+
this.end = { x: gx, y: gy };
|
|
30837
30967
|
this.pressX = x;
|
|
30838
30968
|
this.pressY = y;
|
|
30839
30969
|
this.state = "measuring";
|
|
30840
30970
|
}
|
|
30971
|
+
/** Size the in-progress ruler. `x,y` are the graphic (magnet-snapped) cursor. */
|
|
30841
30972
|
move(x, y) {
|
|
30842
30973
|
if (this.state === "measuring") this.end = { x, y };
|
|
30843
30974
|
}
|
|
30844
30975
|
/** A release: finish if the press was actually dragged (press-drag-release), else wait
|
|
30845
|
-
* for the second click (click-move-click).
|
|
30846
|
-
|
|
30976
|
+
* for the second click (click-move-click). `x,y` are the raw cursor (slop); `gx,gy`
|
|
30977
|
+
* are the graphic end (magnet-snapped when the magnet is on). */
|
|
30978
|
+
up(x, y, gx = x, gy = y) {
|
|
30847
30979
|
if (this.state === "measuring" && Math.hypot(x - this.pressX, y - this.pressY) > DRAG_SLOP4) {
|
|
30848
|
-
this.end = { x, y };
|
|
30980
|
+
this.end = { x: gx, y: gy };
|
|
30849
30981
|
this.state = "finished";
|
|
30850
30982
|
}
|
|
30851
30983
|
}
|
|
30984
|
+
/** Current graphic endpoints in media pixels, or null when idle. */
|
|
30985
|
+
points() {
|
|
30986
|
+
if (!this.start || !this.end) return null;
|
|
30987
|
+
return { start: this.start, end: this.end };
|
|
30988
|
+
}
|
|
30852
30989
|
clear() {
|
|
30853
30990
|
this.state = "idle";
|
|
30854
30991
|
this.start = null;
|
|
@@ -31225,11 +31362,13 @@ var UserDrawingController = class {
|
|
|
31225
31362
|
}
|
|
31226
31363
|
/** Shift+press on the empty plot: arm the measure ruler AND start it at (x, y) in one
|
|
31227
31364
|
* gesture — the equivalent of clicking the toolbar's Measure button, then pressing.
|
|
31228
|
-
*
|
|
31229
|
-
|
|
31365
|
+
* `snap` is the effective magnet (sticky mode, or Ctrl/Cmd-forced strong). Returns
|
|
31366
|
+
* false when a mode/tool is already active (the normal press path owns it). */
|
|
31367
|
+
beginMeasureAt(x, y, snap = "off") {
|
|
31230
31368
|
if (this.measureMode || this.eraserMode || this.activeTool != null) return false;
|
|
31231
31369
|
this.withModeIntent(() => this.toggleMeasure());
|
|
31232
|
-
this.
|
|
31370
|
+
const g = this.interaction.snapCursor(x, y, snap);
|
|
31371
|
+
this.measure.down(x, y, g.x, g.y);
|
|
31233
31372
|
this.render();
|
|
31234
31373
|
return true;
|
|
31235
31374
|
}
|
|
@@ -31251,7 +31390,8 @@ var UserDrawingController = class {
|
|
|
31251
31390
|
return;
|
|
31252
31391
|
}
|
|
31253
31392
|
if (this.measureMode) {
|
|
31254
|
-
this.
|
|
31393
|
+
const g = this.interaction.snapCursor(x, y, snap);
|
|
31394
|
+
this.measure.down(x, y, g.x, g.y);
|
|
31255
31395
|
if (this.measure.isFinished()) this.withModeIntent(() => this.exitMeasure(false));
|
|
31256
31396
|
this.render();
|
|
31257
31397
|
return;
|
|
@@ -31264,7 +31404,8 @@ var UserDrawingController = class {
|
|
|
31264
31404
|
return;
|
|
31265
31405
|
}
|
|
31266
31406
|
if (this.measureMode) {
|
|
31267
|
-
this.
|
|
31407
|
+
const g = this.interaction.snapCursor(x, y, snap);
|
|
31408
|
+
this.measure.move(g.x, g.y);
|
|
31268
31409
|
this.render();
|
|
31269
31410
|
return;
|
|
31270
31411
|
}
|
|
@@ -31282,13 +31423,14 @@ var UserDrawingController = class {
|
|
|
31282
31423
|
this.render();
|
|
31283
31424
|
}
|
|
31284
31425
|
}
|
|
31285
|
-
pointerUp(x, y) {
|
|
31426
|
+
pointerUp(x, y, snap = "off") {
|
|
31286
31427
|
if (this.eraserMode) {
|
|
31287
31428
|
this.erasing = false;
|
|
31288
31429
|
return;
|
|
31289
31430
|
}
|
|
31290
31431
|
if (this.measureMode) {
|
|
31291
|
-
this.
|
|
31432
|
+
const g = this.interaction.snapCursor(x, y, snap);
|
|
31433
|
+
this.measure.up(x, y, g.x, g.y);
|
|
31292
31434
|
if (this.measure.isFinished()) this.withModeIntent(() => this.exitMeasure(false));
|
|
31293
31435
|
this.render();
|
|
31294
31436
|
return;
|
|
@@ -31333,6 +31475,7 @@ var UserDrawingController = class {
|
|
|
31333
31475
|
/** Leave ruler mode. `clearGraphic` keeps a just-finished measurement on screen (false). */
|
|
31334
31476
|
exitMeasure(clearGraphic = true) {
|
|
31335
31477
|
this.measureMode = false;
|
|
31478
|
+
this.interaction.clearSnapMarker();
|
|
31336
31479
|
if (clearGraphic) this.measure.clear();
|
|
31337
31480
|
this.toolbar.setMeasureActive(false);
|
|
31338
31481
|
this.render();
|
|
@@ -31473,15 +31616,28 @@ var UserDrawingController = class {
|
|
|
31473
31616
|
if (this.eraserMode) return "pointer";
|
|
31474
31617
|
return this.interaction.cursorAt(x, y);
|
|
31475
31618
|
}
|
|
31476
|
-
/** Right-click
|
|
31477
|
-
*
|
|
31478
|
-
*
|
|
31479
|
-
*
|
|
31619
|
+
/** Right-click: an explicit escape back to the pointer. Cancels an in-progress
|
|
31620
|
+
* placement or measurement, and also plain-disarms an armed-but-idle drawing
|
|
31621
|
+
* tool or the eraser — so a right-click ALWAYS reverts to the pointer, even in
|
|
31622
|
+
* stay-in-drawing-mode (where Escape would leave a drawing tool armed).
|
|
31623
|
+
* Persistent toggles (magnet, stay-mode, favorites) are untouched. Returns
|
|
31624
|
+
* whether the press was consumed; false lets the host's context menu open
|
|
31625
|
+
* normally. */
|
|
31480
31626
|
cancelPlacement() {
|
|
31481
|
-
if (
|
|
31482
|
-
|
|
31483
|
-
|
|
31484
|
-
|
|
31627
|
+
if (this.measureMode || this.eraserMode) {
|
|
31628
|
+
this.withModeIntent(() => this.measureMode ? this.exitMeasure() : this.exitEraser());
|
|
31629
|
+
return true;
|
|
31630
|
+
}
|
|
31631
|
+
if (this.interaction.isPlacing()) {
|
|
31632
|
+
this.interaction.cancel();
|
|
31633
|
+
if (this.activeTool != null) this.emit({ kind: "arm", type: null });
|
|
31634
|
+
return true;
|
|
31635
|
+
}
|
|
31636
|
+
if (this.activeTool != null) {
|
|
31637
|
+
this.emit({ kind: "arm", type: null });
|
|
31638
|
+
return true;
|
|
31639
|
+
}
|
|
31640
|
+
return false;
|
|
31485
31641
|
}
|
|
31486
31642
|
/** Double-click over a drawing → suppress the chart's view reset (single-click already
|
|
31487
31643
|
* opens settings). Returns true only when a drawing is under the cursor. */
|
|
@@ -31507,6 +31663,10 @@ var UserDrawingController = class {
|
|
|
31507
31663
|
return true;
|
|
31508
31664
|
}
|
|
31509
31665
|
if (this.interaction.cancel()) return true;
|
|
31666
|
+
if (this.measureMode) {
|
|
31667
|
+
this.withModeIntent(() => this.exitMeasure());
|
|
31668
|
+
return true;
|
|
31669
|
+
}
|
|
31510
31670
|
if (this.selectedIds.size) {
|
|
31511
31671
|
this.clearSelection();
|
|
31512
31672
|
return true;
|
|
@@ -31764,6 +31924,315 @@ var UserDrawingController = class {
|
|
|
31764
31924
|
}
|
|
31765
31925
|
};
|
|
31766
31926
|
|
|
31927
|
+
// src/renderers/shared/TableOverlay.ts
|
|
31928
|
+
var SIZE_PX4 = {
|
|
31929
|
+
auto: 13,
|
|
31930
|
+
tiny: 10,
|
|
31931
|
+
small: 11,
|
|
31932
|
+
normal: 13,
|
|
31933
|
+
large: 16,
|
|
31934
|
+
huge: 20
|
|
31935
|
+
};
|
|
31936
|
+
function fontPxOf(size) {
|
|
31937
|
+
if (typeof size === "number") return size > 0 ? size : SIZE_PX4.auto;
|
|
31938
|
+
return SIZE_PX4[size] ?? SIZE_PX4.auto;
|
|
31939
|
+
}
|
|
31940
|
+
function tableHasContent(t) {
|
|
31941
|
+
return t.cells.some((row) => row?.some((c) => c != null && !c.merged));
|
|
31942
|
+
}
|
|
31943
|
+
function mergeRenderPlan(t) {
|
|
31944
|
+
const span = /* @__PURE__ */ new Map();
|
|
31945
|
+
const omit = /* @__PURE__ */ new Set();
|
|
31946
|
+
for (const m of t.merges) {
|
|
31947
|
+
span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
|
|
31948
|
+
for (let r = m.startRow; r <= m.endRow; r += 1) {
|
|
31949
|
+
for (let c = m.startCol; c <= m.endCol; c += 1) {
|
|
31950
|
+
if (r !== m.startRow || c !== m.startCol) omit.add(`${r}:${c}`);
|
|
31951
|
+
}
|
|
31952
|
+
}
|
|
31953
|
+
}
|
|
31954
|
+
for (let r = 0; r < t.rows; r += 1) {
|
|
31955
|
+
for (let c = 0; c < t.columns; c += 1) {
|
|
31956
|
+
if (t.cells[r]?.[c]?.merged && !span.has(`${r}:${c}`)) omit.add(`${r}:${c}`);
|
|
31957
|
+
}
|
|
31958
|
+
}
|
|
31959
|
+
for (const key of span.keys()) omit.delete(key);
|
|
31960
|
+
return { span, omit };
|
|
31961
|
+
}
|
|
31962
|
+
|
|
31963
|
+
// src/renderers/shared/TableCanvasRenderer.ts
|
|
31964
|
+
var PAD_X = 6;
|
|
31965
|
+
var PAD_Y = 2;
|
|
31966
|
+
var MARGIN = 6;
|
|
31967
|
+
var LINE_HEIGHT = 1.2;
|
|
31968
|
+
function paintTable(ctx, t, args, tips) {
|
|
31969
|
+
if (!tableHasContent(t)) return;
|
|
31970
|
+
const layout = layoutTable(ctx, t, args);
|
|
31971
|
+
if (!layout || layout.w <= 0 || layout.h <= 0) return;
|
|
31972
|
+
const fw = t.frameColor && t.frameWidth > 0 ? t.frameWidth : 0;
|
|
31973
|
+
const { x, y } = anchorOrigin(t.position, layout.w + 2 * fw, layout.h + 2 * fw, args);
|
|
31974
|
+
const x0 = x + fw;
|
|
31975
|
+
const y0 = y + fw;
|
|
31976
|
+
if (t.bgColor) {
|
|
31977
|
+
ctx.fillStyle = t.bgColor;
|
|
31978
|
+
ctx.fillRect(x0, y0, layout.w, layout.h);
|
|
31979
|
+
}
|
|
31980
|
+
if (fw > 0 && t.frameColor) {
|
|
31981
|
+
ctx.strokeStyle = t.frameColor;
|
|
31982
|
+
ctx.lineWidth = fw;
|
|
31983
|
+
ctx.strokeRect(x + fw / 2, y + fw / 2, layout.w + fw, layout.h + fw);
|
|
31984
|
+
}
|
|
31985
|
+
const colX = [0];
|
|
31986
|
+
for (const w of layout.colW) colX.push(colX[colX.length - 1] + w);
|
|
31987
|
+
const rowY = [0];
|
|
31988
|
+
for (const h of layout.rowH) rowY.push(rowY[rowY.length - 1] + h);
|
|
31989
|
+
const prevBaseline = ctx.textBaseline;
|
|
31990
|
+
const prevAlign = ctx.textAlign;
|
|
31991
|
+
ctx.textBaseline = "middle";
|
|
31992
|
+
for (const box of layout.boxes) {
|
|
31993
|
+
const rx = x0 + colX[box.c];
|
|
31994
|
+
const ry = y0 + rowY[box.r];
|
|
31995
|
+
const rw = colX[box.c + box.cs] - colX[box.c];
|
|
31996
|
+
const rh = rowY[box.r + box.rs] - rowY[box.r];
|
|
31997
|
+
const cell = box.cell;
|
|
31998
|
+
if (cell.bgColor) {
|
|
31999
|
+
ctx.fillStyle = cell.bgColor;
|
|
32000
|
+
ctx.fillRect(rx, ry, rw, rh);
|
|
32001
|
+
}
|
|
32002
|
+
const text = cell.text ?? "";
|
|
32003
|
+
if (text.length > 0) {
|
|
32004
|
+
const px = fontPxOf(cell.textSize);
|
|
32005
|
+
ctx.font = cellFont(cell, px, args.theme);
|
|
32006
|
+
ctx.fillStyle = cell.textColor ?? args.theme.textColor;
|
|
32007
|
+
const lines = text.split("\n");
|
|
32008
|
+
const blockH = lines.length * px * LINE_HEIGHT;
|
|
32009
|
+
const blockTop = cell.vAlign === "top" ? ry + PAD_Y : cell.vAlign === "bottom" ? ry + rh - PAD_Y - blockH : ry + (rh - blockH) / 2;
|
|
32010
|
+
const tx = cell.hAlign === "left" ? rx + PAD_X : cell.hAlign === "right" ? rx + rw - PAD_X : rx + rw / 2;
|
|
32011
|
+
ctx.textAlign = cell.hAlign;
|
|
32012
|
+
lines.forEach((line, i) => ctx.fillText(line, tx, blockTop + (i + 0.5) * px * LINE_HEIGHT));
|
|
32013
|
+
}
|
|
32014
|
+
if (cell.tooltip) tips.push({ left: rx, top: ry, right: rx + rw, bottom: ry + rh, text: cell.tooltip });
|
|
32015
|
+
}
|
|
32016
|
+
ctx.textBaseline = prevBaseline;
|
|
32017
|
+
ctx.textAlign = prevAlign;
|
|
32018
|
+
if (t.borderColor && t.borderWidth > 0) {
|
|
32019
|
+
ctx.strokeStyle = t.borderColor;
|
|
32020
|
+
ctx.lineWidth = t.borderWidth;
|
|
32021
|
+
const seen = /* @__PURE__ */ new Set();
|
|
32022
|
+
ctx.beginPath();
|
|
32023
|
+
const edge = (ax, ay, bx, by) => {
|
|
32024
|
+
const key = `${ax},${ay},${bx},${by}`;
|
|
32025
|
+
if (seen.has(key)) return;
|
|
32026
|
+
seen.add(key);
|
|
32027
|
+
ctx.moveTo(ax, ay);
|
|
32028
|
+
ctx.lineTo(bx, by);
|
|
32029
|
+
};
|
|
32030
|
+
for (const box of layout.boxes) {
|
|
32031
|
+
const l = Math.round(x0 + colX[box.c]);
|
|
32032
|
+
const r = Math.round(x0 + colX[box.c + box.cs]);
|
|
32033
|
+
const tp = Math.round(y0 + rowY[box.r]);
|
|
32034
|
+
const bt = Math.round(y0 + rowY[box.r + box.rs]);
|
|
32035
|
+
edge(l, tp, r, tp);
|
|
32036
|
+
edge(l, bt, r, bt);
|
|
32037
|
+
edge(l, tp, l, bt);
|
|
32038
|
+
edge(r, tp, r, bt);
|
|
32039
|
+
}
|
|
32040
|
+
ctx.stroke();
|
|
32041
|
+
}
|
|
32042
|
+
}
|
|
32043
|
+
function layoutTable(ctx, t, args) {
|
|
32044
|
+
const { span, omit } = mergeRenderPlan(t);
|
|
32045
|
+
const colW = new Array(t.columns).fill(0);
|
|
32046
|
+
const rowH = new Array(t.rows).fill(0);
|
|
32047
|
+
const boxes = [];
|
|
32048
|
+
for (let r = 0; r < t.rows; r += 1) {
|
|
32049
|
+
for (let c = 0; c < t.columns; c += 1) {
|
|
32050
|
+
if (omit.has(`${r}:${c}`)) continue;
|
|
32051
|
+
const cell = t.cells[r]?.[c];
|
|
32052
|
+
if (cell == null) continue;
|
|
32053
|
+
const sp = span.get(`${r}:${c}`);
|
|
32054
|
+
boxes.push({ cell, r, c, cs: Math.min(sp?.cs ?? 1, t.columns - c), rs: Math.min(sp?.rs ?? 1, t.rows - r) });
|
|
32055
|
+
}
|
|
32056
|
+
}
|
|
32057
|
+
if (boxes.length === 0) return null;
|
|
32058
|
+
const sizeOf = (cell) => {
|
|
32059
|
+
const px = fontPxOf(cell.textSize);
|
|
32060
|
+
ctx.font = cellFont(cell, px, args.theme);
|
|
32061
|
+
const lines = (cell.text ?? "").split("\n");
|
|
32062
|
+
let maxW = 0;
|
|
32063
|
+
for (const line of lines) maxW = Math.max(maxW, ctx.measureText(line).width);
|
|
32064
|
+
let w2 = Math.ceil(maxW) + 2 * PAD_X;
|
|
32065
|
+
let h2 = Math.ceil(lines.length * px * LINE_HEIGHT) + 2 * PAD_Y;
|
|
32066
|
+
if (cell.width) w2 = Math.max(w2, cell.width / 100 * args.plotWidth);
|
|
32067
|
+
if (cell.height) h2 = Math.max(h2, cell.height / 100 * args.paneHeight);
|
|
32068
|
+
return { w: w2, h: h2 };
|
|
32069
|
+
};
|
|
32070
|
+
const spanning = [];
|
|
32071
|
+
for (const box of boxes) {
|
|
32072
|
+
const { w: w2, h: h2 } = sizeOf(box.cell);
|
|
32073
|
+
if (box.cs === 1) colW[box.c] = Math.max(colW[box.c], w2);
|
|
32074
|
+
if (box.rs === 1) rowH[box.r] = Math.max(rowH[box.r], h2);
|
|
32075
|
+
if (box.cs > 1 || box.rs > 1) spanning.push({ box, w: w2, h: h2 });
|
|
32076
|
+
}
|
|
32077
|
+
for (const { box, w: w2, h: h2 } of spanning) {
|
|
32078
|
+
if (box.cs > 1) {
|
|
32079
|
+
let sum = 0;
|
|
32080
|
+
for (let c = box.c; c < box.c + box.cs; c += 1) sum += colW[c];
|
|
32081
|
+
if (w2 > sum) for (let c = box.c; c < box.c + box.cs; c += 1) colW[c] += (w2 - sum) / box.cs;
|
|
32082
|
+
}
|
|
32083
|
+
if (box.rs > 1) {
|
|
32084
|
+
let sum = 0;
|
|
32085
|
+
for (let r = box.r; r < box.r + box.rs; r += 1) sum += rowH[r];
|
|
32086
|
+
if (h2 > sum) for (let r = box.r; r < box.r + box.rs; r += 1) rowH[r] += (h2 - sum) / box.rs;
|
|
32087
|
+
}
|
|
32088
|
+
}
|
|
32089
|
+
let w = 0;
|
|
32090
|
+
for (const cw of colW) w += cw;
|
|
32091
|
+
let h = 0;
|
|
32092
|
+
for (const rh of rowH) h += rh;
|
|
32093
|
+
return { colW, rowH, w, h, boxes };
|
|
32094
|
+
}
|
|
32095
|
+
function anchorOrigin(position, totalW, totalH, args) {
|
|
32096
|
+
let y;
|
|
32097
|
+
if (position.startsWith("top")) y = MARGIN;
|
|
32098
|
+
else if (position.startsWith("bottom")) y = args.paneHeight - MARGIN - totalH;
|
|
32099
|
+
else y = args.paneHeight / 2 - totalH / 2;
|
|
32100
|
+
let x;
|
|
32101
|
+
if (position.endsWith("left")) x = MARGIN;
|
|
32102
|
+
else if (position.endsWith("right")) x = args.plotWidth - MARGIN - totalW;
|
|
32103
|
+
else x = args.plotWidth / 2 - totalW / 2;
|
|
32104
|
+
return { x, y };
|
|
32105
|
+
}
|
|
32106
|
+
function cellFont(cell, px, theme) {
|
|
32107
|
+
const family = cell.fontFamily === "monospace" ? "monospace" : theme.fontFamily || "sans-serif";
|
|
32108
|
+
return `${cell.italic ? "italic " : ""}${cell.bold ? "bold " : ""}${px}px ${family}`;
|
|
32109
|
+
}
|
|
32110
|
+
|
|
32111
|
+
// src/renderers/native/drawings/IndicatorDrawingSlices.ts
|
|
32112
|
+
function indicatorSliceKey(z, boundaries) {
|
|
32113
|
+
return boundaries.find((b) => b > z) ?? Infinity;
|
|
32114
|
+
}
|
|
32115
|
+
var IndicatorDrawingSlices = class {
|
|
32116
|
+
constructor() {
|
|
32117
|
+
this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
|
|
32118
|
+
/** Slice canvas cache, keyed `paneId|beforeZ` — same lifecycle as the user-drawing cache. */
|
|
32119
|
+
this.sliceCache = /* @__PURE__ */ new Map();
|
|
32120
|
+
/** Tooltip hit-rects of every label drawn this frame, in plot coords (rebuilt per prepare). */
|
|
32121
|
+
this.tips = [];
|
|
32122
|
+
}
|
|
32123
|
+
/**
|
|
32124
|
+
* Rebuild the per-indicator drawing slices for this data frame. `ref` is the data
|
|
32125
|
+
* canvas the slices must match pixel-for-pixel (the backend composites them 1:1).
|
|
32126
|
+
* Runs from the renderer's data paint, just before the backend composites the scene.
|
|
32127
|
+
*/
|
|
32128
|
+
prepare(scene, coords, theme, ref) {
|
|
32129
|
+
this.tips = [];
|
|
32130
|
+
const out = /* @__PURE__ */ new Map();
|
|
32131
|
+
if (ref.width === 0 || ref.height === 0) {
|
|
32132
|
+
this.sliceCache.clear();
|
|
32133
|
+
return out;
|
|
32134
|
+
}
|
|
32135
|
+
this.drawScene.setDeps({
|
|
32136
|
+
timeToLogical: (ms) => coords.timeToLogical(ms),
|
|
32137
|
+
barAt: (logical) => {
|
|
32138
|
+
const b = scene.bars[Math.round(logical)];
|
|
32139
|
+
return b ? { high: b.high, low: b.low } : null;
|
|
32140
|
+
},
|
|
32141
|
+
theme
|
|
32142
|
+
});
|
|
32143
|
+
const dpr = coords.dpr;
|
|
32144
|
+
const dataW = coords.width;
|
|
32145
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
32146
|
+
const add = (paneId, beforeZ, entry) => {
|
|
32147
|
+
const key = `${paneId}|${beforeZ}`;
|
|
32148
|
+
const bucket = buckets.get(key);
|
|
32149
|
+
if (bucket) bucket.entries.push(entry);
|
|
32150
|
+
else buckets.set(key, { paneId, beforeZ, entries: [entry] });
|
|
32151
|
+
};
|
|
32152
|
+
for (const pane of scene.orderedPanes()) {
|
|
32153
|
+
if (pane.collapsed) continue;
|
|
32154
|
+
const boundaries = scene.seriesBoundaries(pane.id);
|
|
32155
|
+
for (const m of scene.orderedIndicatorsForPane(pane.id)) {
|
|
32156
|
+
const set = modelDrawingSet(m, false);
|
|
32157
|
+
const tables = (m.tables ?? []).filter((t) => !t.overlay);
|
|
32158
|
+
if (drawingSetEmpty(set) && tables.length === 0) continue;
|
|
32159
|
+
const sc = scene.scaleFor(m, pane);
|
|
32160
|
+
const mp = sc === pane.scale ? pane : { ...pane, scale: sc };
|
|
32161
|
+
const beforeZ = indicatorSliceKey(scene.zOf(m.id), boundaries);
|
|
32162
|
+
add(pane.id, beforeZ, { set, tables, pane: mp, indexOffset: scene.offsetOf(m.id) });
|
|
32163
|
+
}
|
|
32164
|
+
if (pane.kind === "price") {
|
|
32165
|
+
for (const m of scene.indicators.values()) {
|
|
32166
|
+
const set = modelDrawingSet(m, true);
|
|
32167
|
+
const tables = (m.tables ?? []).filter((t) => t.overlay === true);
|
|
32168
|
+
if (drawingSetEmpty(set) && tables.length === 0) continue;
|
|
32169
|
+
add(pane.id, Infinity, { set, tables, pane, indexOffset: scene.offsetOf(m.id) });
|
|
32170
|
+
}
|
|
32171
|
+
}
|
|
32172
|
+
}
|
|
32173
|
+
for (const [key, { paneId, beforeZ, entries }] of buckets) {
|
|
32174
|
+
let canvas = this.sliceCache.get(key);
|
|
32175
|
+
if (!canvas) {
|
|
32176
|
+
canvas = document.createElement("canvas");
|
|
32177
|
+
this.sliceCache.set(key, canvas);
|
|
32178
|
+
}
|
|
32179
|
+
if (canvas.width !== ref.width || canvas.height !== ref.height) {
|
|
32180
|
+
canvas.width = ref.width;
|
|
32181
|
+
canvas.height = ref.height;
|
|
32182
|
+
}
|
|
32183
|
+
const ctx = canvas.getContext("2d");
|
|
32184
|
+
if (!ctx) continue;
|
|
32185
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
32186
|
+
ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
|
|
32187
|
+
for (const e of entries) this.paintEntry(ctx, e, coords, dataW, theme);
|
|
32188
|
+
const slices = out.get(paneId) ?? [];
|
|
32189
|
+
slices.push({ beforeZ, canvas });
|
|
32190
|
+
out.set(paneId, slices);
|
|
32191
|
+
}
|
|
32192
|
+
for (const key of [...this.sliceCache.keys()]) if (!buckets.has(key)) this.sliceCache.delete(key);
|
|
32193
|
+
for (const slices of out.values()) slices.sort((a, b) => a.beforeZ - b.beforeZ);
|
|
32194
|
+
return out;
|
|
32195
|
+
}
|
|
32196
|
+
paintEntry(ctx, e, coords, dataW, theme) {
|
|
32197
|
+
const { pane } = e;
|
|
32198
|
+
const paneTips = [];
|
|
32199
|
+
ctx.save();
|
|
32200
|
+
ctx.translate(0, pane.bounds.top);
|
|
32201
|
+
ctx.beginPath();
|
|
32202
|
+
ctx.rect(0, 0, dataW, pane.bounds.height);
|
|
32203
|
+
ctx.clip();
|
|
32204
|
+
this.drawScene.setSet(e.set, e.indexOffset);
|
|
32205
|
+
this.drawScene.render(
|
|
32206
|
+
ctx,
|
|
32207
|
+
dataW,
|
|
32208
|
+
pane.bounds.height,
|
|
32209
|
+
(l) => coords.logicalToX(l),
|
|
32210
|
+
(price) => coords.priceToY(price, pane.scale, pane.bounds) - pane.bounds.top
|
|
32211
|
+
);
|
|
32212
|
+
paneTips.push(...this.drawScene.labelTipRegions());
|
|
32213
|
+
for (const t of e.tables) paintTable(ctx, t, { paneHeight: pane.bounds.height, plotWidth: dataW, theme }, paneTips);
|
|
32214
|
+
ctx.restore();
|
|
32215
|
+
for (const r of paneTips) {
|
|
32216
|
+
this.tips.push({ ...r, top: r.top + pane.bounds.top, bottom: r.bottom + pane.bounds.top });
|
|
32217
|
+
}
|
|
32218
|
+
}
|
|
32219
|
+
/** Tooltip of the topmost label or table cell under a plot-space point, or null. Fed by the last prepare. */
|
|
32220
|
+
labelTooltipAt(x, y) {
|
|
32221
|
+
for (let i = this.tips.length - 1; i >= 0; i -= 1) {
|
|
32222
|
+
const r = this.tips[i];
|
|
32223
|
+
if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return r.text;
|
|
32224
|
+
}
|
|
32225
|
+
return null;
|
|
32226
|
+
}
|
|
32227
|
+
};
|
|
32228
|
+
function mergeSlices(indicator, user) {
|
|
32229
|
+
const out = /* @__PURE__ */ new Map();
|
|
32230
|
+
for (const [paneId, slices] of indicator) out.set(paneId, [...slices]);
|
|
32231
|
+
for (const [paneId, slices] of user) out.set(paneId, [...out.get(paneId) ?? [], ...slices]);
|
|
32232
|
+
for (const slices of out.values()) slices.sort((a, b) => a.beforeZ - b.beforeZ);
|
|
32233
|
+
return out;
|
|
32234
|
+
}
|
|
32235
|
+
|
|
31767
32236
|
// src/renderers/native/drawings/Projector.ts
|
|
31768
32237
|
function createProjector(coords, paneOf, paneIdAtY, barsInRange) {
|
|
31769
32238
|
return {
|
|
@@ -32509,6 +32978,11 @@ var NativeRenderer = class {
|
|
|
32509
32978
|
this.vpvrRenderer = new VpvrRenderer();
|
|
32510
32979
|
this.resizeObserver = null;
|
|
32511
32980
|
this.dprMedia = null;
|
|
32981
|
+
/** Plot size in INTEGER device px, as last reported by the resize observer's
|
|
32982
|
+
* device-pixel-content-box — the browser's own statement of how many device pixels
|
|
32983
|
+
* it paints the plot into. `null` until the first report or where the box type is
|
|
32984
|
+
* unsupported (WebKit); syncSize then falls back to rounding the client rect. */
|
|
32985
|
+
this.plotDeviceSize = null;
|
|
32512
32986
|
this.coords = new CoordinateSystem();
|
|
32513
32987
|
this.scene = new SceneGraph();
|
|
32514
32988
|
// chosen at mount (WebGL2 if available, else canvas2d)
|
|
@@ -32516,6 +32990,8 @@ var NativeRenderer = class {
|
|
|
32516
32990
|
this.glowAmount = 0;
|
|
32517
32991
|
// WebGL2 neon-glow intensity (canvas2d ignores it)
|
|
32518
32992
|
this.chrome = new ChromeRenderer();
|
|
32993
|
+
/** Prepaints each indicator's Pine drawings into interleave slices at the model's z. */
|
|
32994
|
+
this.indicatorSlices = new IndicatorDrawingSlices();
|
|
32519
32995
|
/** Hover tooltips for Pine labels (canvas hit-rects collected by the chrome layer). */
|
|
32520
32996
|
this.labelTooltip = null;
|
|
32521
32997
|
this.crosshairLayer = new CrosshairRenderer();
|
|
@@ -32664,7 +33140,6 @@ var NativeRenderer = class {
|
|
|
32664
33140
|
this.toggleVisibleCbs = /* @__PURE__ */ new Set();
|
|
32665
33141
|
this.moveIndicatorCbs = /* @__PURE__ */ new Set();
|
|
32666
33142
|
this.priceStyleCbs = /* @__PURE__ */ new Set();
|
|
32667
|
-
this.tableOverlays = /* @__PURE__ */ new Map();
|
|
32668
33143
|
this.name = "native";
|
|
32669
33144
|
this.features = ["logScale", "currentPriceLine", "priceLabel", "countdown", "upColor", "downColor", "glow", "animZoom", "animPan", "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", "indicatorTitles", "indicatorValues"];
|
|
32670
33145
|
/** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
|
|
@@ -32978,7 +33453,6 @@ var NativeRenderer = class {
|
|
|
32978
33453
|
* hidden) on clear so a re-show picks up the current theme.
|
|
32979
33454
|
*/
|
|
32980
33455
|
setLoading(loading) {
|
|
32981
|
-
for (const overlay of this.tableOverlays.values()) overlay.setVisible(!loading);
|
|
32982
33456
|
if (!loading || !this.wrapper) {
|
|
32983
33457
|
this.loadingEl?.remove();
|
|
32984
33458
|
this.loadingEl = null;
|
|
@@ -33617,13 +34091,13 @@ var NativeRenderer = class {
|
|
|
33617
34091
|
resetView: () => this.resetView(),
|
|
33618
34092
|
// User drawings claim a gesture before pan when armed / over a drawing.
|
|
33619
34093
|
drawingsClaim: (x, y) => this.userDrawings?.claim(x, y) ?? false,
|
|
33620
|
-
drawingsMeasureStart: (x, y) => this.userDrawings?.beginMeasureAt(x, y) ?? false,
|
|
34094
|
+
drawingsMeasureStart: (x, y, snap) => this.userDrawings?.beginMeasureAt(x, y, snap) ?? false,
|
|
33621
34095
|
drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y) ?? false,
|
|
33622
34096
|
drawingsCancelPlacement: () => this.userDrawings?.cancelPlacement() ?? false,
|
|
33623
34097
|
drawingsSnapMode: () => this.snapMode,
|
|
33624
34098
|
drawingsPointerDown: (x, y, snap, shift) => this.userDrawings?.pointerDown(x, y, snap, shift),
|
|
33625
34099
|
drawingsPointerMove: (x, y, snap, shift) => this.userDrawings?.pointerMove(x, y, snap, shift),
|
|
33626
|
-
drawingsPointerUp: (x, y) => this.userDrawings?.pointerUp(x, y),
|
|
34100
|
+
drawingsPointerUp: (x, y, snap) => this.userDrawings?.pointerUp(x, y, snap),
|
|
33627
34101
|
drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? null,
|
|
33628
34102
|
drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
|
|
33629
34103
|
drawingsClearTransient: () => this.userDrawings?.clearTransient()
|
|
@@ -33640,7 +34114,7 @@ var NativeRenderer = class {
|
|
|
33640
34114
|
this.plot.addEventListener("pointerleave", this.onScrollProximityLeave);
|
|
33641
34115
|
this.labelTooltip = new LabelTooltip(this.plot, {
|
|
33642
34116
|
theme: () => this.chromeTheme(),
|
|
33643
|
-
lookup: (x, y) => this.
|
|
34117
|
+
lookup: (x, y) => this.indicatorSlices.labelTooltipAt(x, y)
|
|
33644
34118
|
});
|
|
33645
34119
|
this.userDrawings = new UserDrawingController(this.wrapper, this.plot, this.drawingsCanvas, {
|
|
33646
34120
|
projector: () => this.drawingProjector(),
|
|
@@ -33725,6 +34199,7 @@ var NativeRenderer = class {
|
|
|
33725
34199
|
this.emitPaneAction({ type: "maximize", paneId, maximized });
|
|
33726
34200
|
}
|
|
33727
34201
|
});
|
|
34202
|
+
this.paneControls.setSuspended(this.layoutMode === "mobile");
|
|
33728
34203
|
this.axisScaleButtons = new AxisScaleButtons(this.plot, theme, {
|
|
33729
34204
|
panes: () => this.axisScaleViews(),
|
|
33730
34205
|
rightAxis: () => this.rightAxisW,
|
|
@@ -33734,8 +34209,19 @@ var NativeRenderer = class {
|
|
|
33734
34209
|
if (pane) this.setPaneLog(paneId, !paneLogScale(this.scene, pane));
|
|
33735
34210
|
}
|
|
33736
34211
|
});
|
|
33737
|
-
this.resizeObserver = new ResizeObserver(() =>
|
|
34212
|
+
this.resizeObserver = new ResizeObserver((entries) => {
|
|
34213
|
+
for (const e of entries) {
|
|
34214
|
+
if (e.target !== this.plot) continue;
|
|
34215
|
+
const s = e.devicePixelContentBoxSize?.[0];
|
|
34216
|
+
if (s) this.plotDeviceSize = { width: s.inlineSize, height: s.blockSize };
|
|
34217
|
+
}
|
|
34218
|
+
this.resize();
|
|
34219
|
+
});
|
|
33738
34220
|
this.resizeObserver.observe(this.wrapper);
|
|
34221
|
+
try {
|
|
34222
|
+
this.resizeObserver.observe(this.plot, { box: "device-pixel-content-box" });
|
|
34223
|
+
} catch {
|
|
34224
|
+
}
|
|
33739
34225
|
this.watchDpr();
|
|
33740
34226
|
this.syncSize();
|
|
33741
34227
|
}
|
|
@@ -33860,8 +34346,6 @@ var NativeRenderer = class {
|
|
|
33860
34346
|
this.inputsUI?.destroy();
|
|
33861
34347
|
this.paneControls?.destroy();
|
|
33862
34348
|
this.axisScaleButtons?.destroy();
|
|
33863
|
-
for (const overlay of this.tableOverlays.values()) overlay.destroy();
|
|
33864
|
-
this.tableOverlays.clear();
|
|
33865
34349
|
this.resizeObserver?.disconnect();
|
|
33866
34350
|
this.resizeObserver = null;
|
|
33867
34351
|
this.dprMedia?.removeEventListener("change", this.onDprChange);
|
|
@@ -33890,6 +34374,7 @@ var NativeRenderer = class {
|
|
|
33890
34374
|
this.attributionEl = null;
|
|
33891
34375
|
this.mountContainer?.style.removeProperty("--vela-toolbar-gutter");
|
|
33892
34376
|
this.mountContainer?.style.removeProperty("--vela-scale-gutter");
|
|
34377
|
+
this.mountContainer?.style.removeProperty("--vela-bottom-gutter");
|
|
33893
34378
|
this.mountContainer?.style.removeProperty("--vela-price-pane-top");
|
|
33894
34379
|
this.mountContainer?.style.removeProperty("--vela-price-pane-bottom");
|
|
33895
34380
|
this.mountContainer = null;
|
|
@@ -33927,7 +34412,8 @@ var NativeRenderer = class {
|
|
|
33927
34412
|
}
|
|
33928
34413
|
const skipFit = opts?.preserveView === true && this.didInitialFit;
|
|
33929
34414
|
if (this.coords.width > 0 && !skipFit) {
|
|
33930
|
-
this.
|
|
34415
|
+
if (this.didInitialFit) this.reframeKeepZoom();
|
|
34416
|
+
else this.fitContent();
|
|
33931
34417
|
this.didInitialFit = true;
|
|
33932
34418
|
}
|
|
33933
34419
|
if (!this.introPlayed && this.bars.length > 0) {
|
|
@@ -33982,7 +34468,6 @@ var NativeRenderer = class {
|
|
|
33982
34468
|
ensurePane(pane) {
|
|
33983
34469
|
this.scene.ensurePane(pane.id, pane.kind, pane.order, pane.heightWeight ?? (pane.kind === "price" ? 3 : 1));
|
|
33984
34470
|
this.layoutPanes();
|
|
33985
|
-
this.repositionTables();
|
|
33986
34471
|
this.paneControls?.refresh();
|
|
33987
34472
|
this.scheduler.invalidate(4 /* Full */);
|
|
33988
34473
|
}
|
|
@@ -34004,17 +34489,14 @@ var NativeRenderer = class {
|
|
|
34004
34489
|
if (!model.ownScale) this.scene.dropIndicatorScale(handle.id);
|
|
34005
34490
|
this.inputsUI.setPane(handle.id, paneId);
|
|
34006
34491
|
this.refreshAnchorOffset(model);
|
|
34007
|
-
this.syncTables(model);
|
|
34008
34492
|
this.refreshAxisWidth();
|
|
34009
34493
|
this.layoutPanes();
|
|
34010
|
-
this.repositionTables();
|
|
34011
34494
|
this.paneControls?.refresh();
|
|
34012
34495
|
this.scheduler.invalidate(4 /* Full */);
|
|
34013
34496
|
}
|
|
34014
34497
|
orderPanes(orderedIds) {
|
|
34015
34498
|
this.scene.orderPanes(orderedIds);
|
|
34016
34499
|
this.layoutPanes();
|
|
34017
|
-
this.repositionTables();
|
|
34018
34500
|
this.paneControls?.refresh();
|
|
34019
34501
|
this.scheduler.invalidate(4 /* Full */);
|
|
34020
34502
|
}
|
|
@@ -34023,7 +34505,6 @@ var NativeRenderer = class {
|
|
|
34023
34505
|
if (!pane || pane.collapsed === collapsed) return;
|
|
34024
34506
|
pane.collapsed = collapsed;
|
|
34025
34507
|
this.layoutPanes();
|
|
34026
|
-
this.repositionTables();
|
|
34027
34508
|
this.paneControls?.refresh();
|
|
34028
34509
|
this.scheduler.invalidate(4 /* Full */);
|
|
34029
34510
|
}
|
|
@@ -34031,7 +34512,6 @@ var NativeRenderer = class {
|
|
|
34031
34512
|
if (paneId !== null && !this.scene.panes.has(paneId)) paneId = null;
|
|
34032
34513
|
this.maximizedPaneId = paneId;
|
|
34033
34514
|
this.layoutPanes();
|
|
34034
|
-
this.repositionTables();
|
|
34035
34515
|
this.paneControls?.refresh();
|
|
34036
34516
|
this.scheduler.invalidate(4 /* Full */);
|
|
34037
34517
|
}
|
|
@@ -34118,7 +34598,6 @@ var NativeRenderer = class {
|
|
|
34118
34598
|
native: !!model.native,
|
|
34119
34599
|
...model.props ? { props: model.props, propValues: model.propValues ?? {} } : {}
|
|
34120
34600
|
});
|
|
34121
|
-
this.syncTables(model);
|
|
34122
34601
|
if (model.native?.type === "volume") {
|
|
34123
34602
|
this.volumeActive = true;
|
|
34124
34603
|
this.volumeHidden = false;
|
|
@@ -34142,7 +34621,6 @@ var NativeRenderer = class {
|
|
|
34142
34621
|
}
|
|
34143
34622
|
}
|
|
34144
34623
|
applyPatch(model, patch);
|
|
34145
|
-
this.syncTables(model);
|
|
34146
34624
|
this.scheduler.invalidate(3 /* Light */);
|
|
34147
34625
|
}
|
|
34148
34626
|
removeIndicator(handle) {
|
|
@@ -34161,8 +34639,6 @@ var NativeRenderer = class {
|
|
|
34161
34639
|
this.scene.forgetAnchorOffset(handle.id);
|
|
34162
34640
|
this.scene.dropIndicatorScale(handle.id);
|
|
34163
34641
|
this.inputsUI.remove(handle.id);
|
|
34164
|
-
this.tableOverlays.get(handle.id)?.destroy();
|
|
34165
|
-
this.tableOverlays.delete(handle.id);
|
|
34166
34642
|
this.refreshAxisWidth();
|
|
34167
34643
|
this.paneControls?.refresh();
|
|
34168
34644
|
this.scheduler.invalidate(4 /* Full */);
|
|
@@ -34206,8 +34682,6 @@ var NativeRenderer = class {
|
|
|
34206
34682
|
}
|
|
34207
34683
|
if (!visible) {
|
|
34208
34684
|
this.scene.indicators.delete(handle.id);
|
|
34209
|
-
this.tableOverlays.get(handle.id)?.destroy();
|
|
34210
|
-
this.tableOverlays.delete(handle.id);
|
|
34211
34685
|
}
|
|
34212
34686
|
this.inputsUI.setVisible(handle.id, visible);
|
|
34213
34687
|
this.scheduler.invalidate(4 /* Full */);
|
|
@@ -34253,6 +34727,7 @@ var NativeRenderer = class {
|
|
|
34253
34727
|
this.userDrawings?.setLayoutMode(mode);
|
|
34254
34728
|
this.settingsDialog?.setLayoutMode(mode);
|
|
34255
34729
|
this.inputsUI?.setLayoutMode(mode);
|
|
34730
|
+
this.paneControls?.setSuspended(mode === "mobile");
|
|
34256
34731
|
if (this.scrollButton) {
|
|
34257
34732
|
const px = mode === "mobile" ? SCROLL_BTN_SIZE_TOUCH : SCROLL_BTN_SIZE;
|
|
34258
34733
|
this.scrollButton.style.width = `${px}px`;
|
|
@@ -34586,6 +35061,7 @@ var NativeRenderer = class {
|
|
|
34586
35061
|
this.scaleDragHeight = res.height;
|
|
34587
35062
|
this.scaleDragStart = { ...res.holder.scale };
|
|
34588
35063
|
res.holder.manualScale = { ...res.holder.scale };
|
|
35064
|
+
this.axisScaleButtons?.reposition();
|
|
34589
35065
|
this.scheduler.invalidate(4 /* Full */);
|
|
34590
35066
|
}
|
|
34591
35067
|
/** Rescale the grabbed scale around its center by the total drag (down ⇒ zoom out). */
|
|
@@ -34617,6 +35093,7 @@ var NativeRenderer = class {
|
|
|
34617
35093
|
const res = this.resolveScaleHolder(x, y);
|
|
34618
35094
|
if (!res) return;
|
|
34619
35095
|
res.holder.manualScale = null;
|
|
35096
|
+
this.axisScaleButtons?.reposition();
|
|
34620
35097
|
this.scheduler.invalidate(4 /* Full */);
|
|
34621
35098
|
}
|
|
34622
35099
|
/**
|
|
@@ -34648,7 +35125,6 @@ var NativeRenderer = class {
|
|
|
34648
35125
|
/** Relayout + repaint + refresh the hover buttons after a collapse/maximize/order change. */
|
|
34649
35126
|
afterPaneLayoutChange() {
|
|
34650
35127
|
this.layoutPanes();
|
|
34651
|
-
this.repositionTables();
|
|
34652
35128
|
this.paneControls?.refresh();
|
|
34653
35129
|
this.scheduler.invalidate(4 /* Full */);
|
|
34654
35130
|
}
|
|
@@ -34729,7 +35205,6 @@ var NativeRenderer = class {
|
|
|
34729
35205
|
above.heightWeight = next.above;
|
|
34730
35206
|
below.heightWeight = next.below;
|
|
34731
35207
|
this.layoutPanes();
|
|
34732
|
-
this.repositionTables();
|
|
34733
35208
|
this.scheduler.invalidate(4 /* Full */);
|
|
34734
35209
|
}
|
|
34735
35210
|
/** Double-click a separator → split the two adjacent panes evenly (each gets half of
|
|
@@ -34744,7 +35219,6 @@ var NativeRenderer = class {
|
|
|
34744
35219
|
above.heightWeight = half;
|
|
34745
35220
|
below.heightWeight = half;
|
|
34746
35221
|
this.layoutPanes();
|
|
34747
|
-
this.repositionTables();
|
|
34748
35222
|
this.scheduler.invalidate(4 /* Full */);
|
|
34749
35223
|
}
|
|
34750
35224
|
// ── keyboard navigation / accessibility (item 11) ──
|
|
@@ -35023,7 +35497,10 @@ var NativeRenderer = class {
|
|
|
35023
35497
|
const liveActual = li >= 0 ? this.bars[li] : void 0;
|
|
35024
35498
|
const easeLive = !!liveActual && this.liveEaseTime === liveActual.time && (liveActual.high !== this.liveEaseHigh || liveActual.low !== this.liveEaseLow || liveActual.close !== this.liveEaseClose);
|
|
35025
35499
|
if (easeLive && liveActual) this.bars[li] = { ...liveActual, high: this.liveEaseHigh, low: this.liveEaseLow, close: this.liveEaseClose };
|
|
35026
|
-
this.scene.drawingSlices =
|
|
35500
|
+
this.scene.drawingSlices = mergeSlices(
|
|
35501
|
+
this.indicatorSlices.prepare(this.scene, this.coords, this.theme, this.dataCanvas),
|
|
35502
|
+
this.userDrawings?.prepareSlices(this.scene.orderedPanes().map((p) => p.id)) ?? /* @__PURE__ */ new Map()
|
|
35503
|
+
);
|
|
35027
35504
|
this.backdropRenderer.render(this.scene, this.coords, this.theme, gridAlpha);
|
|
35028
35505
|
this.backend.render(this.scene, this.coords, this.theme);
|
|
35029
35506
|
this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
|
|
@@ -35254,6 +35731,20 @@ var NativeRenderer = class {
|
|
|
35254
35731
|
this.coords.setViewport(v);
|
|
35255
35732
|
this.targetBarSpacing = v.barSpacing;
|
|
35256
35733
|
}
|
|
35734
|
+
/** Re-frame after a series replacement (a symbol/timeframe switch): keep the user's
|
|
35735
|
+
* zoom (bar spacing), re-anchor the newest bars at the default right offset.
|
|
35736
|
+
* `clampViewport`'s fit-all-bars floor deliberately does NOT apply — a progressive
|
|
35737
|
+
* head may still be backfilling toward the previous depth, and raising the spacing
|
|
35738
|
+
* to its temporary bar count would lose the zoom this exists to keep. */
|
|
35739
|
+
reframeKeepZoom() {
|
|
35740
|
+
this.animator?.stop();
|
|
35741
|
+
this.panVelocity = 0;
|
|
35742
|
+
for (const pane of this.scene.panes.values()) pane.manualScale = null;
|
|
35743
|
+
for (const sl of this.scene.indicatorScales.values()) sl.manualScale = null;
|
|
35744
|
+
const v = { barSpacing: clampBarSpacing(this.coords.getViewport().barSpacing), rightOffset: defaultViewport().rightOffset };
|
|
35745
|
+
this.coords.setViewport(v);
|
|
35746
|
+
this.targetBarSpacing = v.barSpacing;
|
|
35747
|
+
}
|
|
35257
35748
|
paneBoundsFor(paneId) {
|
|
35258
35749
|
const p = this.scene.panes.get(paneId);
|
|
35259
35750
|
return { top: p?.bounds.top ?? 0, height: p?.bounds.height ?? 0, rightAxis: this.rightAxisW };
|
|
@@ -35461,27 +35952,6 @@ var NativeRenderer = class {
|
|
|
35461
35952
|
}
|
|
35462
35953
|
return maxVol;
|
|
35463
35954
|
}
|
|
35464
|
-
/** Create/update/destroy an indicator's DOM table overlay (anchored off real pane geometry). */
|
|
35465
|
-
syncTables(model) {
|
|
35466
|
-
const tables = model.tables ?? [];
|
|
35467
|
-
let overlay = this.tableOverlays.get(model.id);
|
|
35468
|
-
if (tables.length === 0) {
|
|
35469
|
-
if (overlay) {
|
|
35470
|
-
overlay.destroy();
|
|
35471
|
-
this.tableOverlays.delete(model.id);
|
|
35472
|
-
}
|
|
35473
|
-
return;
|
|
35474
|
-
}
|
|
35475
|
-
if (!overlay) {
|
|
35476
|
-
overlay = new TableOverlay(this.plot, this.theme, (id) => this.paneBoundsFor(id));
|
|
35477
|
-
overlay.setVisible(this.loadingEl === null);
|
|
35478
|
-
this.tableOverlays.set(model.id, overlay);
|
|
35479
|
-
}
|
|
35480
|
-
overlay.update(tables);
|
|
35481
|
-
}
|
|
35482
|
-
repositionTables() {
|
|
35483
|
-
for (const overlay of this.tableOverlays.values()) overlay.reposition();
|
|
35484
|
-
}
|
|
35485
35955
|
layoutPanes() {
|
|
35486
35956
|
const panes = this.scene.orderedPanes();
|
|
35487
35957
|
const dataHeight = this.coords.height;
|
|
@@ -35532,6 +36002,7 @@ var NativeRenderer = class {
|
|
|
35532
36002
|
const visible = maxPane ? [maxPane] : this.scene.orderedPanes().filter((p) => !p.collapsed);
|
|
35533
36003
|
const paneBottom = visible.length ? Math.max(...visible.map((p) => p.bounds.top + p.bounds.height)) : dataHeight;
|
|
35534
36004
|
this.scrollBtnBottomPx = SCROLL_BTN_BOTTOM + Math.max(0, dataHeight - paneBottom);
|
|
36005
|
+
this.mountContainer?.style.setProperty("--vela-bottom-gutter", `${TIME_AXIS_H + Math.max(0, dataHeight - paneBottom)}px`);
|
|
35535
36006
|
this.scrollBtnRightPx = this.rightAxisW + SCROLL_BTN_RIGHT_INSET;
|
|
35536
36007
|
if (this.scrollButton) {
|
|
35537
36008
|
this.scrollButton.style.bottom = `${this.scrollBtnBottomPx}px`;
|
|
@@ -35624,30 +36095,33 @@ var NativeRenderer = class {
|
|
|
35624
36095
|
if (w <= 0 || h <= 0) return;
|
|
35625
36096
|
const dpr = window.devicePixelRatio || 1;
|
|
35626
36097
|
this.plot.style.left = `${this.toolbarGutter}px`;
|
|
35627
|
-
const
|
|
35628
|
-
|
|
35629
|
-
|
|
35630
|
-
|
|
35631
|
-
|
|
35632
|
-
|
|
35633
|
-
|
|
35634
|
-
|
|
35635
|
-
|
|
35636
|
-
|
|
35637
|
-
|
|
35638
|
-
|
|
35639
|
-
|
|
35640
|
-
|
|
35641
|
-
|
|
35642
|
-
|
|
35643
|
-
this.
|
|
35644
|
-
this.
|
|
35645
|
-
this.
|
|
35646
|
-
|
|
36098
|
+
const rect = this.plot.getBoundingClientRect();
|
|
36099
|
+
let bw = Math.max(1, Math.round(rect.width * dpr));
|
|
36100
|
+
let bh = Math.max(1, Math.round(rect.height * dpr));
|
|
36101
|
+
const dev = this.plotDeviceSize;
|
|
36102
|
+
if (dev && Math.abs(dev.width - rect.width * dpr) <= 1 && Math.abs(dev.height - rect.height * dpr) <= 1) {
|
|
36103
|
+
bw = Math.max(1, dev.width);
|
|
36104
|
+
bh = Math.max(1, dev.height);
|
|
36105
|
+
}
|
|
36106
|
+
const pw = bw / dpr;
|
|
36107
|
+
const ph = bh / dpr;
|
|
36108
|
+
const size = (canvas) => {
|
|
36109
|
+
canvas.width = bw;
|
|
36110
|
+
canvas.height = bh;
|
|
36111
|
+
canvas.style.width = `${pw}px`;
|
|
36112
|
+
canvas.style.height = `${ph}px`;
|
|
36113
|
+
};
|
|
36114
|
+
size(this.dataCanvas);
|
|
36115
|
+
size(this.backdropCanvas);
|
|
36116
|
+
size(this.volumeCanvas);
|
|
36117
|
+
for (const l of this.extLayers) size(l.canvas);
|
|
36118
|
+
size(this.vpvrCanvas);
|
|
36119
|
+
size(this.chromeCanvas);
|
|
36120
|
+
size(this.drawingsCanvas);
|
|
36121
|
+
size(this.cursorCanvas);
|
|
35647
36122
|
this.coords.setSize(Math.max(1, pw - this.rightAxisW), Math.max(1, ph - TIME_AXIS_H), dpr);
|
|
35648
36123
|
this.scene.crosshair = null;
|
|
35649
36124
|
this.layoutPanes();
|
|
35650
|
-
this.repositionTables();
|
|
35651
36125
|
this.userDrawings?.onResize();
|
|
35652
36126
|
if (!this.didInitialFit && this.coords.barCount > 0) {
|
|
35653
36127
|
this.fitContent();
|
|
@@ -36966,9 +37440,182 @@ var Watermark = class {
|
|
|
36966
37440
|
}
|
|
36967
37441
|
};
|
|
36968
37442
|
|
|
37443
|
+
// src/widget/cell-controls.ts
|
|
37444
|
+
var CELL_CONTROLS_PROXIMITY_PX = 120;
|
|
37445
|
+
var TIME_AXIS_H2 = 22;
|
|
37446
|
+
var CONTROLS_BOTTOM_PX = TIME_AXIS_H2 + 12;
|
|
37447
|
+
var CLUSTER_H2 = 24;
|
|
37448
|
+
var CLUSTER_PILL2 = "rgba(0,0,0,0.65)";
|
|
37449
|
+
var CLUSTER_LEFT_CSS = "calc((100% + var(--vela-toolbar-gutter, 0px) - var(--vela-scale-gutter, 0px)) / 2)";
|
|
37450
|
+
var STYLE_ID26 = "vela-cell-controls";
|
|
37451
|
+
var CSS23 = `
|
|
37452
|
+
.vela-cc-btn{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;padding:0;border:none;border-radius:var(--vela-radius-sm);background:transparent;line-height:0;font-size:12px;color:var(--vela-fg-muted);cursor:pointer;}
|
|
37453
|
+
.vela-cc-btn svg{display:block;}
|
|
37454
|
+
.vela-cc-btn:hover{background:var(--vela-active);color:var(--vela-fg-bright);}
|
|
37455
|
+
.vela-cc-on,.vela-cc-on:hover{background:var(--vela-selected-bg);color:var(--vela-selected-fg);}
|
|
37456
|
+
.vela-cc-grip{cursor:grab;touch-action:none;}
|
|
37457
|
+
.vela-cc-grip:active{cursor:grabbing;}
|
|
37458
|
+
`;
|
|
37459
|
+
function plotCenterX(width, toolbarGutter = 0, scaleGutter = 0) {
|
|
37460
|
+
return (width + toolbarGutter - scaleGutter) / 2;
|
|
37461
|
+
}
|
|
37462
|
+
function nearBottomCenter(x, y, width, height, proximityPx = CELL_CONTROLS_PROXIMITY_PX, gutters = {}) {
|
|
37463
|
+
const cx = plotCenterX(width, gutters.toolbar ?? 0, gutters.scale ?? 0);
|
|
37464
|
+
const cy = height - CONTROLS_BOTTOM_PX - CLUSTER_H2 / 2;
|
|
37465
|
+
return Math.hypot(x - cx, y - cy) <= proximityPx;
|
|
37466
|
+
}
|
|
37467
|
+
var CellControls = class {
|
|
37468
|
+
constructor(host, deps) {
|
|
37469
|
+
this.host = host;
|
|
37470
|
+
this.deps = deps;
|
|
37471
|
+
this.near = false;
|
|
37472
|
+
/** A grip drag is underway — the proximity reveal must not hide the cluster
|
|
37473
|
+
* while captured pointer moves sweep across the whole grid. */
|
|
37474
|
+
this.dragging = false;
|
|
37475
|
+
/** Mobile: the proximity reveal is meaningless without a cursor — the mobile
|
|
37476
|
+
* bar's maximize stop replaces the cluster. */
|
|
37477
|
+
this.suspended = false;
|
|
37478
|
+
this.onHostMove = (e) => {
|
|
37479
|
+
if (this.suspended) return;
|
|
37480
|
+
if (this.dragging) return;
|
|
37481
|
+
const rect = this.host.getBoundingClientRect();
|
|
37482
|
+
this.setNear(nearBottomCenter(e.clientX - rect.left, e.clientY - rect.top, rect.width, rect.height, CELL_CONTROLS_PROXIMITY_PX, this.hostGutters()));
|
|
37483
|
+
};
|
|
37484
|
+
this.onHostLeave = () => {
|
|
37485
|
+
if (this.dragging) return;
|
|
37486
|
+
this.setNear(false);
|
|
37487
|
+
};
|
|
37488
|
+
injectStyles(STYLE_ID26, CSS23, host.ownerDocument);
|
|
37489
|
+
this.glider = new Glider(deps.chart);
|
|
37490
|
+
this.root = host.ownerDocument.createElement("div");
|
|
37491
|
+
Object.assign(this.root.style, {
|
|
37492
|
+
position: "absolute",
|
|
37493
|
+
left: CLUSTER_LEFT_CSS,
|
|
37494
|
+
bottom: `${CONTROLS_BOTTOM_PX}px`,
|
|
37495
|
+
transform: "translateX(-50%)",
|
|
37496
|
+
zIndex: "6",
|
|
37497
|
+
display: "none",
|
|
37498
|
+
// revealed by cursor proximity (onHostMove)
|
|
37499
|
+
gap: "2px",
|
|
37500
|
+
padding: "2px",
|
|
37501
|
+
borderRadius: "var(--vela-radius-md)",
|
|
37502
|
+
background: CLUSTER_PILL2,
|
|
37503
|
+
pointerEvents: "auto"
|
|
37504
|
+
});
|
|
37505
|
+
this.host.addEventListener("pointermove", this.onHostMove);
|
|
37506
|
+
this.host.addEventListener("pointerleave", this.onHostLeave);
|
|
37507
|
+
this.host.appendChild(this.root);
|
|
37508
|
+
this.refresh();
|
|
37509
|
+
}
|
|
37510
|
+
/** Rebuild the buttons (the multi-cell gate or the maximized state changed). */
|
|
37511
|
+
refresh() {
|
|
37512
|
+
this.root.textContent = "";
|
|
37513
|
+
const multi = this.deps.multiCell();
|
|
37514
|
+
const maximized = multi && this.deps.isMaximized();
|
|
37515
|
+
if (multi && !maximized) this.root.appendChild(this.makeGrip());
|
|
37516
|
+
this.root.appendChild(this.button("minus", "Zoom out", () => this.glider.zoom(ZOOM_OUT)));
|
|
37517
|
+
this.root.appendChild(this.button("plus", "Zoom in", () => this.glider.zoom(ZOOM_IN)));
|
|
37518
|
+
if (multi) {
|
|
37519
|
+
this.root.appendChild(
|
|
37520
|
+
this.button(maximized ? "restore" : "maximize", maximized ? "Restore layout" : "Maximize chart", () => this.deps.toggleMaximize(), {
|
|
37521
|
+
// The maximized state reads as an inverse chip (white-on-dark, dark-on-light),
|
|
37522
|
+
// the same active-state affordance as a collapsed pane's expand button.
|
|
37523
|
+
selected: maximized
|
|
37524
|
+
})
|
|
37525
|
+
);
|
|
37526
|
+
}
|
|
37527
|
+
this.root.appendChild(
|
|
37528
|
+
this.button("reset", "Reset chart", () => {
|
|
37529
|
+
this.glider.stop();
|
|
37530
|
+
this.deps.reset();
|
|
37531
|
+
})
|
|
37532
|
+
);
|
|
37533
|
+
}
|
|
37534
|
+
button(iconId, title, onClick, opts = {}) {
|
|
37535
|
+
const b = this.host.ownerDocument.createElement("button");
|
|
37536
|
+
b.type = "button";
|
|
37537
|
+
b.title = title;
|
|
37538
|
+
b.setAttribute("aria-label", title);
|
|
37539
|
+
b.className = opts.selected === true ? "vela-cc-btn vela-cc-on" : "vela-cc-btn";
|
|
37540
|
+
b.innerHTML = icon(iconId);
|
|
37541
|
+
b.addEventListener("click", (e) => {
|
|
37542
|
+
e.stopPropagation();
|
|
37543
|
+
onClick();
|
|
37544
|
+
});
|
|
37545
|
+
return b;
|
|
37546
|
+
}
|
|
37547
|
+
/** The drag handle (2×3 dot grip): press and drag onto another cell to trade
|
|
37548
|
+
* slots with it. The preview highlight follows the pointer; releasing outside
|
|
37549
|
+
* any other cell cancels. */
|
|
37550
|
+
makeGrip() {
|
|
37551
|
+
const b = this.host.ownerDocument.createElement("button");
|
|
37552
|
+
b.type = "button";
|
|
37553
|
+
b.title = "Drag to move chart";
|
|
37554
|
+
b.setAttribute("aria-label", "Drag to move chart");
|
|
37555
|
+
b.className = "vela-cc-btn vela-cc-grip";
|
|
37556
|
+
b.innerHTML = icon("grip");
|
|
37557
|
+
b.addEventListener("pointerdown", (e) => this.onGripDown(b, e));
|
|
37558
|
+
return b;
|
|
37559
|
+
}
|
|
37560
|
+
onGripDown(btn2, e) {
|
|
37561
|
+
if (e.button !== 0 && e.pointerType === "mouse") return;
|
|
37562
|
+
e.preventDefault();
|
|
37563
|
+
e.stopPropagation();
|
|
37564
|
+
try {
|
|
37565
|
+
btn2.setPointerCapture(e.pointerId);
|
|
37566
|
+
} catch {
|
|
37567
|
+
}
|
|
37568
|
+
this.dragging = true;
|
|
37569
|
+
let target = null;
|
|
37570
|
+
const move = (ev) => {
|
|
37571
|
+
target = this.deps.dragTargetAt(ev.clientX, ev.clientY);
|
|
37572
|
+
this.deps.previewDrop(target);
|
|
37573
|
+
};
|
|
37574
|
+
const finish = (commit) => () => {
|
|
37575
|
+
this.dragging = false;
|
|
37576
|
+
this.deps.previewDrop(null);
|
|
37577
|
+
btn2.removeEventListener("pointermove", move);
|
|
37578
|
+
btn2.removeEventListener("pointerup", onUp);
|
|
37579
|
+
btn2.removeEventListener("pointercancel", onCancel);
|
|
37580
|
+
if (commit && target != null) this.deps.dropOn(target);
|
|
37581
|
+
};
|
|
37582
|
+
const onUp = finish(true);
|
|
37583
|
+
const onCancel = finish(false);
|
|
37584
|
+
btn2.addEventListener("pointermove", move);
|
|
37585
|
+
btn2.addEventListener("pointerup", onUp);
|
|
37586
|
+
btn2.addEventListener("pointercancel", onCancel);
|
|
37587
|
+
}
|
|
37588
|
+
/** Mobile flips the cluster off entirely (and hides it if currently revealed). */
|
|
37589
|
+
setSuspended(on) {
|
|
37590
|
+
this.suspended = on;
|
|
37591
|
+
if (on) this.setNear(false);
|
|
37592
|
+
}
|
|
37593
|
+
/** Live renderer gutters on the cell host (0 when unpublished — a test stub). */
|
|
37594
|
+
hostGutters() {
|
|
37595
|
+
const view = this.host.ownerDocument.defaultView;
|
|
37596
|
+
if (!view) return { toolbar: 0, scale: 0 };
|
|
37597
|
+
const cs = view.getComputedStyle(this.host);
|
|
37598
|
+
return {
|
|
37599
|
+
toolbar: Number.parseFloat(cs.getPropertyValue("--vela-toolbar-gutter")) || 0,
|
|
37600
|
+
scale: Number.parseFloat(cs.getPropertyValue("--vela-scale-gutter")) || 0
|
|
37601
|
+
};
|
|
37602
|
+
}
|
|
37603
|
+
setNear(near) {
|
|
37604
|
+
if (near === this.near) return;
|
|
37605
|
+
this.near = near;
|
|
37606
|
+
this.root.style.display = near ? "flex" : "none";
|
|
37607
|
+
}
|
|
37608
|
+
destroy() {
|
|
37609
|
+
this.glider.stop();
|
|
37610
|
+
this.host.removeEventListener("pointermove", this.onHostMove);
|
|
37611
|
+
this.host.removeEventListener("pointerleave", this.onHostLeave);
|
|
37612
|
+
this.root.remove();
|
|
37613
|
+
}
|
|
37614
|
+
};
|
|
37615
|
+
|
|
36969
37616
|
// src/widget/context-menu.ts
|
|
36970
37617
|
var PRICE_AXIS_W = 60;
|
|
36971
|
-
var
|
|
37618
|
+
var TIME_AXIS_H3 = 26;
|
|
36972
37619
|
var ChartContextMenu = class {
|
|
36973
37620
|
constructor(host, cbs) {
|
|
36974
37621
|
this.cbs = cbs;
|
|
@@ -37007,7 +37654,7 @@ var ChartContextMenu = class {
|
|
|
37007
37654
|
zoneOf(e) {
|
|
37008
37655
|
const rect = this.host.getBoundingClientRect();
|
|
37009
37656
|
if (e.clientX - rect.left > rect.width - PRICE_AXIS_W) return "price-axis";
|
|
37010
|
-
if (e.clientY - rect.top > rect.height -
|
|
37657
|
+
if (e.clientY - rect.top > rect.height - TIME_AXIS_H3) return "time-axis";
|
|
37011
37658
|
return "body";
|
|
37012
37659
|
}
|
|
37013
37660
|
/** The pane under the pointer, so every pane's price scale has its own menu. */
|
|
@@ -37193,6 +37840,12 @@ function cellDrawings(opt) {
|
|
|
37193
37840
|
if (opt === true || opt == null) return { toolbar: false };
|
|
37194
37841
|
return { ...opt, toolbar: false };
|
|
37195
37842
|
}
|
|
37843
|
+
function instanceDeltas(handle) {
|
|
37844
|
+
if (!handle) return void 0;
|
|
37845
|
+
const inputs = inputDeltas(handle.inputs, handle.inputValues());
|
|
37846
|
+
const props = inputDeltas(handle.props, handle.propValues());
|
|
37847
|
+
return inputs || props ? { ...inputs ? { inputs } : {}, ...props ? { props } : {} } : void 0;
|
|
37848
|
+
}
|
|
37196
37849
|
var ChartCell = class {
|
|
37197
37850
|
constructor(id, gridHost, seed, deps) {
|
|
37198
37851
|
this.id = id;
|
|
@@ -37341,11 +37994,18 @@ var ChartCell = class {
|
|
|
37341
37994
|
if (this.inner && this.state.symbol) this.marketStatus?.track(this.inner.data, this.state.symbol);
|
|
37342
37995
|
});
|
|
37343
37996
|
this.syncStatuslineColors();
|
|
37997
|
+
this.cellControls = new CellControls(this.host, {
|
|
37998
|
+
chart: () => this.inner,
|
|
37999
|
+
reset: () => this.resetView(),
|
|
38000
|
+
multiCell: () => deps.multiCell(),
|
|
38001
|
+
isMaximized: () => deps.isMaximized(id),
|
|
38002
|
+
toggleMaximize: () => deps.toggleMaximize(id),
|
|
38003
|
+
dragTargetAt: (x, y) => deps.cellDragTarget(id, x, y),
|
|
38004
|
+
previewDrop: (target) => deps.previewDropTarget(target),
|
|
38005
|
+
dropOn: (target) => deps.dropCell(id, target)
|
|
38006
|
+
});
|
|
37344
38007
|
this.contextMenu = new ChartContextMenu(this.host, {
|
|
37345
|
-
resetView: () =>
|
|
37346
|
-
this.inner?.renderer.set("autoScale", true);
|
|
37347
|
-
this.inner?.setVisibleRangePreset("ALL");
|
|
37348
|
-
},
|
|
38008
|
+
resetView: () => this.resetView(),
|
|
37349
38009
|
timezone: () => this.deps.timezone(),
|
|
37350
38010
|
setTimezone: (zone) => this.deps.setTimezone(zone),
|
|
37351
38011
|
// Right-clicking activates the cell first (capture-phase pointerdown), so the
|
|
@@ -37361,6 +38021,7 @@ var ChartCell = class {
|
|
|
37361
38021
|
this.syncPresentNatives();
|
|
37362
38022
|
this.refreshNativeCatalog();
|
|
37363
38023
|
});
|
|
38024
|
+
this.inner.on("indicator:inputs", () => this.deps.onStateDirty());
|
|
37364
38025
|
this.inner.on("indicator:removed", ({ id: id2 }) => {
|
|
37365
38026
|
if (this.destroyed) return;
|
|
37366
38027
|
const idx = this.instances.findIndex((it) => it.handle?.id === id2);
|
|
@@ -37369,7 +38030,7 @@ var ChartCell = class {
|
|
|
37369
38030
|
this.instances.splice(idx, 1);
|
|
37370
38031
|
this.history.push({
|
|
37371
38032
|
undo: () => {
|
|
37372
|
-
snapshot.handle = this.addToChart(snapshot.entry);
|
|
38033
|
+
snapshot.handle = this.addToChart(snapshot.entry, snapshot.values);
|
|
37373
38034
|
this.instances.push(snapshot);
|
|
37374
38035
|
this.deps.onIndicatorsChanged(this.id);
|
|
37375
38036
|
},
|
|
@@ -37719,6 +38380,20 @@ var ChartCell = class {
|
|
|
37719
38380
|
this.inner.setVisibleRangePreset(preset.preset);
|
|
37720
38381
|
}
|
|
37721
38382
|
}
|
|
38383
|
+
/** Reset this cell's view: re-enable auto scale and frame the full history —
|
|
38384
|
+
* the same action the chart context menu offers. */
|
|
38385
|
+
resetView() {
|
|
38386
|
+
this.inner?.renderer.set("autoScale", true);
|
|
38387
|
+
this.inner?.setVisibleRangePreset("ALL");
|
|
38388
|
+
}
|
|
38389
|
+
/** Rebuild the view-controls cluster (the maximize gate or state changed). */
|
|
38390
|
+
refreshControls() {
|
|
38391
|
+
this.cellControls.refresh();
|
|
38392
|
+
}
|
|
38393
|
+
/** Mobile flips the per-cell cluster off (the shell's mobile bar replaces it). */
|
|
38394
|
+
setControlsSuspended(on) {
|
|
38395
|
+
this.cellControls.setSuspended(on);
|
|
38396
|
+
}
|
|
37722
38397
|
/** Make this cell the active one and put keyboard focus on its chart surface. */
|
|
37723
38398
|
focus() {
|
|
37724
38399
|
this.deps.activate(this.id);
|
|
@@ -37744,9 +38419,9 @@ var ChartCell = class {
|
|
|
37744
38419
|
this.manifest = list;
|
|
37745
38420
|
if (this.pendingManifestNames) {
|
|
37746
38421
|
if (list.length === 0) return;
|
|
37747
|
-
for (const
|
|
37748
|
-
const entry = list.find((e) => e.name ===
|
|
37749
|
-
if (entry) this.addManifestInstance(entry, { record: false });
|
|
38422
|
+
for (const led of this.pendingManifestNames) {
|
|
38423
|
+
const entry = list.find((e) => e.name === ledgerEntryName(led));
|
|
38424
|
+
if (entry) this.addManifestInstance(entry, { record: false, ...typeof led === "object" ? { inputs: led.inputs, props: led.props } : {} });
|
|
37750
38425
|
}
|
|
37751
38426
|
this.pendingManifestNames = null;
|
|
37752
38427
|
return;
|
|
@@ -37776,9 +38451,9 @@ var ChartCell = class {
|
|
|
37776
38451
|
}
|
|
37777
38452
|
for (const it of [...this.instances]) this.dropInstance(it);
|
|
37778
38453
|
if (this.manifest.length > 0) {
|
|
37779
|
-
for (const
|
|
37780
|
-
const entry = this.manifest.find((e) => e.name ===
|
|
37781
|
-
if (entry) this.addManifestInstance(entry, { record: false });
|
|
38454
|
+
for (const item of led.manifest) {
|
|
38455
|
+
const entry = this.manifest.find((e) => e.name === ledgerEntryName(item));
|
|
38456
|
+
if (entry) this.addManifestInstance(entry, { record: false, ...typeof item === "object" ? { inputs: item.inputs, props: item.props } : {} });
|
|
37782
38457
|
}
|
|
37783
38458
|
this.pendingManifestNames = null;
|
|
37784
38459
|
} else if (!this.deps.manifestSettled()) {
|
|
@@ -37827,12 +38502,13 @@ var ChartCell = class {
|
|
|
37827
38502
|
* a persistence handler's `restore` runs silently, a user-driven call records.
|
|
37828
38503
|
*/
|
|
37829
38504
|
addExternalIndicator(entry) {
|
|
37830
|
-
this.addManifestInstance({ ...entry, enabled: true }, { external: true });
|
|
38505
|
+
this.addManifestInstance({ ...entry, enabled: true }, { external: true, ...entry.inputs ? { inputs: entry.inputs } : {}, ...entry.props ? { props: entry.props } : {} });
|
|
37831
38506
|
}
|
|
37832
38507
|
/** Add ONE instance of a manifest entry (repeatable — duplicates are legitimate). */
|
|
37833
38508
|
addManifestInstance(entry, opts = {}) {
|
|
37834
38509
|
if (this.destroyed) return;
|
|
37835
|
-
const
|
|
38510
|
+
const values = opts.inputs || opts.props ? { inputs: opts.inputs, props: opts.props } : void 0;
|
|
38511
|
+
const it = { entry, handle: this.addToChart(entry, values), ...opts.external ? { external: true } : {}, ...values ? { values } : {} };
|
|
37836
38512
|
this.instances.push(it);
|
|
37837
38513
|
this.deps.onIndicatorsChanged(this.id);
|
|
37838
38514
|
if (opts.record === false) return;
|
|
@@ -37840,7 +38516,7 @@ var ChartCell = class {
|
|
|
37840
38516
|
this.history.push({
|
|
37841
38517
|
undo: () => this.dropInstance(snapshot),
|
|
37842
38518
|
redo: () => {
|
|
37843
|
-
snapshot.handle = this.addToChart(snapshot.entry);
|
|
38519
|
+
snapshot.handle = this.addToChart(snapshot.entry, snapshot.values);
|
|
37844
38520
|
this.instances.push(snapshot);
|
|
37845
38521
|
this.deps.onIndicatorsChanged(this.id);
|
|
37846
38522
|
}
|
|
@@ -37853,7 +38529,7 @@ var ChartCell = class {
|
|
|
37853
38529
|
const snapshot = it;
|
|
37854
38530
|
this.history.push({
|
|
37855
38531
|
undo: () => {
|
|
37856
|
-
snapshot.handle = this.addToChart(snapshot.entry);
|
|
38532
|
+
snapshot.handle = this.addToChart(snapshot.entry, snapshot.values);
|
|
37857
38533
|
this.instances.push(snapshot);
|
|
37858
38534
|
this.deps.onIndicatorsChanged(this.id);
|
|
37859
38535
|
},
|
|
@@ -37863,6 +38539,9 @@ var ChartCell = class {
|
|
|
37863
38539
|
dropInstance(it) {
|
|
37864
38540
|
const idx = this.instances.indexOf(it);
|
|
37865
38541
|
if (idx >= 0) this.instances.splice(idx, 1);
|
|
38542
|
+
const captured = instanceDeltas(it.handle);
|
|
38543
|
+
if (captured) it.values = captured;
|
|
38544
|
+
else delete it.values;
|
|
37866
38545
|
try {
|
|
37867
38546
|
it.handle?.remove();
|
|
37868
38547
|
} catch {
|
|
@@ -37905,9 +38584,13 @@ var ChartCell = class {
|
|
|
37905
38584
|
this.deps.onIndicatorsChanged(this.id);
|
|
37906
38585
|
});
|
|
37907
38586
|
}
|
|
37908
|
-
addToChart(entry) {
|
|
38587
|
+
addToChart(entry, values) {
|
|
37909
38588
|
try {
|
|
37910
|
-
return this.inner?.addIndicator(entry.script,
|
|
38589
|
+
return this.inner?.addIndicator(entry.script, {
|
|
38590
|
+
...entry.language !== void 0 ? { language: entry.language } : {},
|
|
38591
|
+
...values?.inputs ? { inputs: values.inputs } : {},
|
|
38592
|
+
...values?.props ? { props: values.props } : {}
|
|
38593
|
+
}) ?? null;
|
|
37911
38594
|
} catch (err) {
|
|
37912
38595
|
console.warn(`[vela] indicator "${entry.name}" failed to add:`, err);
|
|
37913
38596
|
return null;
|
|
@@ -38021,7 +38704,10 @@ var ChartCell = class {
|
|
|
38021
38704
|
// manifest — their plugin persists them via the `ext` seam instead.
|
|
38022
38705
|
indicators: indicatorLedger({
|
|
38023
38706
|
present: this.inner ? this.inner.presentNativeIndicators() : [],
|
|
38024
|
-
|
|
38707
|
+
instanceEntries: this.instances.filter((it) => !it.external).map((it) => {
|
|
38708
|
+
const d = it.handle ? instanceDeltas(it.handle) : it.values;
|
|
38709
|
+
return d ? { name: it.entry.name, ...d } : it.entry.name;
|
|
38710
|
+
}),
|
|
38025
38711
|
pendingManifest: this.pendingManifestNames,
|
|
38026
38712
|
manifestSettled: this.deps.manifestSettled(),
|
|
38027
38713
|
volumePending: this.volumeMayBePending && this.volumeIntent
|
|
@@ -38032,6 +38718,7 @@ var ChartCell = class {
|
|
|
38032
38718
|
destroy() {
|
|
38033
38719
|
this.destroyed = true;
|
|
38034
38720
|
this.offMarket();
|
|
38721
|
+
this.cellControls.destroy();
|
|
38035
38722
|
this.contextMenu.destroy();
|
|
38036
38723
|
this.history.destroy();
|
|
38037
38724
|
this.marketStatus?.stop();
|
|
@@ -38330,10 +39017,10 @@ var SplitterLayer = class {
|
|
|
38330
39017
|
var DEFAULT_TIMEFRAMES = ["1", "5", "15", "60", "240", "D", "W"];
|
|
38331
39018
|
var GAP_PX = 2;
|
|
38332
39019
|
var POOL_CAP = 16;
|
|
38333
|
-
var
|
|
39020
|
+
var TIME_AXIS_H4 = 22;
|
|
38334
39021
|
var ALERT_CAP = 50;
|
|
38335
|
-
var
|
|
38336
|
-
var
|
|
39022
|
+
var STYLE_ID27 = "vela-workspace";
|
|
39023
|
+
var CSS24 = `
|
|
38337
39024
|
.vela-workspace { position: relative; width: 100%; height: 100%; display: flex; flex-direction: column; background: var(--vela-bg); }
|
|
38338
39025
|
.vela-ws-main { position: relative; display: flex; flex-direction: row; flex: 1 1 auto; min-height: 0; }
|
|
38339
39026
|
.vela-ws-toolbar { position: relative; flex: none; }
|
|
@@ -38360,6 +39047,21 @@ var CSS23 = `
|
|
|
38360
39047
|
/* Mobile: the docked drawing-toolbar column would eat a phone-width grid \u2014 the shell's
|
|
38361
39048
|
drawings drawer + on-chart pill replace it (same policy as the widget's in-chart bar). */
|
|
38362
39049
|
[data-layout='mobile'] .vela-ws-toolbar { display: none; }
|
|
39050
|
+
/* A maximized cell owns the whole grid: the splitter strips have no seams to grab and
|
|
39051
|
+
the active ring would just outline the only visible chart \u2014 both are noise here. */
|
|
39052
|
+
.vela-ws-grid[data-maximized='1'] .vela-ws-splitter { display: none; }
|
|
39053
|
+
.vela-ws-grid[data-maximized='1'] .vela-cell[data-active='1']::after { display: none; }
|
|
39054
|
+
/* Drop-target preview while a cell's drag handle is held: a dashed ring + the same
|
|
39055
|
+
soft wash the splitter hover uses, over the chart, inert to the pointer. */
|
|
39056
|
+
.vela-cell[data-drop-target='1']::before {
|
|
39057
|
+
content: '';
|
|
39058
|
+
position: absolute;
|
|
39059
|
+
inset: 0;
|
|
39060
|
+
border: 2px dashed var(--vela-fg-bright);
|
|
39061
|
+
background: var(--vela-separator-hover-band);
|
|
39062
|
+
pointer-events: none;
|
|
39063
|
+
z-index: 11;
|
|
39064
|
+
}
|
|
38363
39065
|
`;
|
|
38364
39066
|
registerIcon("layout", svg16('<rect x="1.5" y="1.5" width="13" height="13" rx="1.5"/><path d="M8 1.5v13M1.5 8h13"/>'));
|
|
38365
39067
|
function declaredOrder(cells) {
|
|
@@ -38389,6 +39091,9 @@ var VelaWorkspace = class {
|
|
|
38389
39091
|
* slots beyond the list get auto identities. Grows, never reorders. */
|
|
38390
39092
|
this.order = [];
|
|
38391
39093
|
this.activeId = null;
|
|
39094
|
+
/** The cell maximized over the whole grid (null = normal grid). TRANSIENT view
|
|
39095
|
+
* state — never persisted; any structural change (layout, applyState) restores. */
|
|
39096
|
+
this.maximizedId = null;
|
|
38392
39097
|
this.cellBackend = "auto";
|
|
38393
39098
|
this.destroyed = false;
|
|
38394
39099
|
this.shortcutsHelp = null;
|
|
@@ -38493,7 +39198,7 @@ var VelaWorkspace = class {
|
|
|
38493
39198
|
this.order = boot?.charts ? boot.charts.map((c) => c.id) : declaredOrder(opts.cells);
|
|
38494
39199
|
const bootActive = boot?.activeCellId ?? null;
|
|
38495
39200
|
const doc = hostEl.ownerDocument;
|
|
38496
|
-
injectStyles(
|
|
39201
|
+
injectStyles(STYLE_ID27, CSS24, doc);
|
|
38497
39202
|
this.root = doc.createElement("div");
|
|
38498
39203
|
this.root.className = "vela-workspace";
|
|
38499
39204
|
ensureUIHost(this.root, resolveTheme(opts.theme));
|
|
@@ -38603,7 +39308,11 @@ var VelaWorkspace = class {
|
|
|
38603
39308
|
if (attribution !== false) {
|
|
38604
39309
|
const background = resolveTheme(opts.theme).background;
|
|
38605
39310
|
const mark = typeof attribution === "string" && attribution.trim() ? createCustomMark(doc, attribution, background) : createAttributionMark(doc, background);
|
|
38606
|
-
Object.assign(mark.style, {
|
|
39311
|
+
Object.assign(mark.style, {
|
|
39312
|
+
left: "calc(var(--vela-toolbar-gutter, 0px) + 12px)",
|
|
39313
|
+
bottom: `calc(var(--vela-bottom-gutter, ${TIME_AXIS_H4}px) + 10px)`,
|
|
39314
|
+
zIndex: "11"
|
|
39315
|
+
});
|
|
38607
39316
|
this.gridEl.appendChild(mark);
|
|
38608
39317
|
this.attributionMark = mark;
|
|
38609
39318
|
}
|
|
@@ -38669,6 +39378,9 @@ var VelaWorkspace = class {
|
|
|
38669
39378
|
...topbarHas(this.topbarComp, "indicators") && (picker || this.indicatorsOverride) ? { onIndicatorsClick: this.indicatorsOverride ? () => this.runOverride(this.indicatorsOverride) : () => picker.open() } : {},
|
|
38670
39379
|
getContext: () => this.context(),
|
|
38671
39380
|
...this.drawingsEnabled ? { onDrawingsClick: () => this.openDrawingsDrawer() } : {},
|
|
39381
|
+
// Multi-chart only: the stop that isolates the ACTIVE chart (the
|
|
39382
|
+
// per-cell hover cluster has no cursor to reveal it on mobile).
|
|
39383
|
+
...this.monoLayout ? {} : { onMaximizeClick: () => this.toggleMobileMaximize() },
|
|
38672
39384
|
onMoreClick: () => this.openMoreDrawer(),
|
|
38673
39385
|
onSettingsClick: () => this.active.chart.renderer.openSettings()
|
|
38674
39386
|
}) : null;
|
|
@@ -38875,7 +39587,9 @@ var VelaWorkspace = class {
|
|
|
38875
39587
|
this.pool.clear();
|
|
38876
39588
|
for (const { id, ...cs } of st.charts.slice(liveCount)) this.pool.set(id, cs);
|
|
38877
39589
|
this.order = st.charts.map((c) => c.id);
|
|
39590
|
+
this.clearMaximized();
|
|
38878
39591
|
this.applyGrid();
|
|
39592
|
+
this.refreshCellControls();
|
|
38879
39593
|
const nextActive2 = st.activeCellId && this.cellsById.has(st.activeCellId) ? st.activeCellId : this.order[0] ?? null;
|
|
38880
39594
|
if (nextActive2 === this.activeId) this.projectActiveCell();
|
|
38881
39595
|
else this.setActiveCell(nextActive2);
|
|
@@ -38901,6 +39615,7 @@ var VelaWorkspace = class {
|
|
|
38901
39615
|
const def = this.monoLayout ? null : ensureLayout(st.layout);
|
|
38902
39616
|
if (def) this.def = def;
|
|
38903
39617
|
this.cellBackend = this.backendFor(this.def);
|
|
39618
|
+
this.clearMaximized();
|
|
38904
39619
|
this.applyGrid();
|
|
38905
39620
|
this.buildCells();
|
|
38906
39621
|
this.syncCellPresentation();
|
|
@@ -38964,6 +39679,7 @@ var VelaWorkspace = class {
|
|
|
38964
39679
|
setLayout(layout) {
|
|
38965
39680
|
if (this.destroyed) return;
|
|
38966
39681
|
if (this.monoLayout) return;
|
|
39682
|
+
this.clearMaximized();
|
|
38967
39683
|
const next = this.resolveLayout(layout);
|
|
38968
39684
|
const nextBackend = this.backendFor(next);
|
|
38969
39685
|
const rebuildAll = nextBackend !== this.cellBackend;
|
|
@@ -38984,6 +39700,7 @@ var VelaWorkspace = class {
|
|
|
38984
39700
|
this.buildCells();
|
|
38985
39701
|
this.alignNewCellStyles(preexisting);
|
|
38986
39702
|
this.syncCellPresentation();
|
|
39703
|
+
this.refreshCellControls();
|
|
38987
39704
|
this.topbar.setLayout(next.id);
|
|
38988
39705
|
const nextActive = activeAfterLayout(this.activeId, this.order.slice(0, next.cells.length));
|
|
38989
39706
|
if (nextActive === this.activeId) this.projectActiveCell();
|
|
@@ -38992,6 +39709,67 @@ var VelaWorkspace = class {
|
|
|
38992
39709
|
this.events.emit("layout:changed", { layout: next.id });
|
|
38993
39710
|
this.markStateDirty();
|
|
38994
39711
|
}
|
|
39712
|
+
/** The identity of the cell maximized over the whole grid, or null. */
|
|
39713
|
+
get maximizedCell() {
|
|
39714
|
+
return this.maximizedId;
|
|
39715
|
+
}
|
|
39716
|
+
/**
|
|
39717
|
+
* Maximize one cell over the whole grid, or restore the layout with `null`. Pure
|
|
39718
|
+
* presentation: the other cells stay alive underneath — charts, subscriptions and
|
|
39719
|
+
* state untouched — so restoring is instant. The maximized cell becomes the active
|
|
39720
|
+
* one. Transient view state (also reachable from each cell's bottom-center view
|
|
39721
|
+
* cluster): switching layouts or applying a state document restores the grid.
|
|
39722
|
+
*/
|
|
39723
|
+
maximizeCell(id) {
|
|
39724
|
+
if (this.destroyed) return;
|
|
39725
|
+
if (id != null && (!this.cellsById.has(id) || this.def.cells.length <= 1)) return;
|
|
39726
|
+
if (id === this.maximizedId) return;
|
|
39727
|
+
this.maximizedId = id;
|
|
39728
|
+
if (id) this.setActiveCell(id);
|
|
39729
|
+
this.applyGrid();
|
|
39730
|
+
this.refreshCellControls();
|
|
39731
|
+
this.syncMobileMaximize();
|
|
39732
|
+
this.events.emit("cell:maximized", { id });
|
|
39733
|
+
}
|
|
39734
|
+
/** The mobile bar's maximize stop: one press isolates the ACTIVE chart over the
|
|
39735
|
+
* grid; while something is already isolated — the chart, or a pane inside it
|
|
39736
|
+
* (mobile's double-tap) — the press restores that instead. Every branch re-syncs
|
|
39737
|
+
* the stop on its own (`maximizeCell` directly, `panes.maximize` via its
|
|
39738
|
+
* synchronous `pane:changed`). */
|
|
39739
|
+
toggleMobileMaximize() {
|
|
39740
|
+
const cell = this.activeId ? this.cellsById.get(this.activeId) : void 0;
|
|
39741
|
+
if (!cell) return;
|
|
39742
|
+
if (this.maximizedId) this.maximizeCell(null);
|
|
39743
|
+
else if (cell.chart.panes.list().some((p) => p.maximized)) cell.chart.panes.maximize(null);
|
|
39744
|
+
else this.maximizeCell(cell.id);
|
|
39745
|
+
}
|
|
39746
|
+
/** Keep the mobile bar's maximize stop truthful: lit (inverse chip, restore
|
|
39747
|
+
* glyph) while the active chart covers the grid OR one of its panes is
|
|
39748
|
+
* maximized — the state a double-tap toggles is otherwise invisible on mobile. */
|
|
39749
|
+
syncMobileMaximize() {
|
|
39750
|
+
if (!this.mobileBar) return;
|
|
39751
|
+
const cell = this.activeId ? this.cellsById.get(this.activeId) : void 0;
|
|
39752
|
+
const paneMax = cell ? cell.chart.panes.list().some((p) => p.maximized) : false;
|
|
39753
|
+
this.mobileBar.setMaximizeActive(this.maximizedId != null || paneMax);
|
|
39754
|
+
}
|
|
39755
|
+
/**
|
|
39756
|
+
* Trade the SLOTS of two live cells — the grid arrangement changes, the cells
|
|
39757
|
+
* themselves (charts, indicators, drawings, the active flag) stay untouched.
|
|
39758
|
+
* What each cell's drag handle commits; also callable directly by hosts.
|
|
39759
|
+
*/
|
|
39760
|
+
swapCells(a, b) {
|
|
39761
|
+
if (this.destroyed || a === b) return;
|
|
39762
|
+
const i = this.order.indexOf(a);
|
|
39763
|
+
const j = this.order.indexOf(b);
|
|
39764
|
+
if (i < 0 || j < 0 || !this.cellsById.has(a) || !this.cellsById.has(b)) return;
|
|
39765
|
+
[this.order[i], this.order[j]] = [this.order[j], this.order[i]];
|
|
39766
|
+
for (const [k] of this.def.cells.entries()) {
|
|
39767
|
+
const host = this.cellsById.get(this.order[k] ?? "")?.host;
|
|
39768
|
+
if (host) this.gridEl.appendChild(host);
|
|
39769
|
+
}
|
|
39770
|
+
this.applyGrid();
|
|
39771
|
+
this.markStateDirty();
|
|
39772
|
+
}
|
|
38995
39773
|
resize() {
|
|
38996
39774
|
this.splitters.layout();
|
|
38997
39775
|
}
|
|
@@ -39061,6 +39839,7 @@ var VelaWorkspace = class {
|
|
|
39061
39839
|
this.mobileBar?.renderActions();
|
|
39062
39840
|
this.mobileBar?.setSymbol(cell.symbol);
|
|
39063
39841
|
this.mobileBar?.setTimeframe(cell.timeframe);
|
|
39842
|
+
this.syncMobileMaximize();
|
|
39064
39843
|
this.drawingPill?.onChart(cell.chart);
|
|
39065
39844
|
const pushHistory = () => this.topbar.setHistoryState(cell.history.canUndo, cell.history.canRedo);
|
|
39066
39845
|
this.historyUnsub?.();
|
|
@@ -39150,8 +39929,78 @@ var VelaWorkspace = class {
|
|
|
39150
39929
|
const host = this.cellsById.get(this.order[i] ?? "")?.host;
|
|
39151
39930
|
if (host) host.style.gridArea = perCell[slot.id]?.gridArea ?? "";
|
|
39152
39931
|
}
|
|
39932
|
+
this.applyMaximizePresentation();
|
|
39933
|
+
this.mountAttributionMark();
|
|
39153
39934
|
this.splitters.layout();
|
|
39154
39935
|
}
|
|
39936
|
+
/** Overlay the maximize presentation on the freshly applied grid: EVERY cell spans
|
|
39937
|
+
* the full track grid — the maximized one on top, the siblings invisible beneath
|
|
39938
|
+
* it (their charts stay alive — restoring is instant). The siblings must span too:
|
|
39939
|
+
* left in their slots they would auto-flow into implicit zero-height rows, whose
|
|
39940
|
+
* gaps steal height from the maximized cell and collapse their renderers to 0.
|
|
39941
|
+
* The splitter strips and the active ring hide via the `data-maximized` rules. */
|
|
39942
|
+
applyMaximizePresentation() {
|
|
39943
|
+
const maxId = this.maximizedId;
|
|
39944
|
+
if (maxId) this.gridEl.dataset.maximized = "1";
|
|
39945
|
+
else delete this.gridEl.dataset.maximized;
|
|
39946
|
+
for (const [id, cell] of this.cellsById) {
|
|
39947
|
+
const style = cell.host.style;
|
|
39948
|
+
if (maxId) style.gridArea = "1 / 1 / -1 / -1";
|
|
39949
|
+
style.zIndex = maxId && id === maxId ? "5" : "";
|
|
39950
|
+
style.visibility = maxId && id !== maxId ? "hidden" : "";
|
|
39951
|
+
}
|
|
39952
|
+
}
|
|
39953
|
+
/** Rebuild every cell's view cluster (the maximize gate or state changed). */
|
|
39954
|
+
refreshCellControls() {
|
|
39955
|
+
for (const cell of this.cellsById.values()) cell.refreshControls();
|
|
39956
|
+
}
|
|
39957
|
+
/** Drop the transient maximize on a structural change (layout switch, state
|
|
39958
|
+
* document) — WITH the event, so hosts tracking `cell:maximized` never drift
|
|
39959
|
+
* from `maximizedCell`. The caller's own grid re-apply paints the restore. */
|
|
39960
|
+
clearMaximized() {
|
|
39961
|
+
if (this.maximizedId == null) return;
|
|
39962
|
+
this.maximizedId = null;
|
|
39963
|
+
this.events.emit("cell:maximized", { id: null });
|
|
39964
|
+
}
|
|
39965
|
+
/** The live cell under a viewport point, excluding `excludeId` and any host a
|
|
39966
|
+
* maximize has hidden — the drag handle's hit-test. */
|
|
39967
|
+
cellAtPoint(x, y, excludeId) {
|
|
39968
|
+
for (const [id, cell] of this.cellsById) {
|
|
39969
|
+
if (id === excludeId || cell.host.style.visibility === "hidden") continue;
|
|
39970
|
+
const r = cell.host.getBoundingClientRect();
|
|
39971
|
+
if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return id;
|
|
39972
|
+
}
|
|
39973
|
+
return null;
|
|
39974
|
+
}
|
|
39975
|
+
/** Mark one cell as the live drop target of a grip drag (null clears all) —
|
|
39976
|
+
* the `data-drop-target` stylesheet rule paints the dashed preview ring. */
|
|
39977
|
+
setDropTarget(id) {
|
|
39978
|
+
for (const [cid, cell] of this.cellsById) {
|
|
39979
|
+
if (cid === id) cell.host.dataset.dropTarget = "1";
|
|
39980
|
+
else delete cell.host.dataset.dropTarget;
|
|
39981
|
+
}
|
|
39982
|
+
}
|
|
39983
|
+
/** The cell whose bottom-left corner the grid's attribution mark floats in — the
|
|
39984
|
+
* maximized cell while one covers the grid, else the bottom-left slot's cell. */
|
|
39985
|
+
bottomLeftCell() {
|
|
39986
|
+
if (this.maximizedId) return this.cellsById.get(this.maximizedId);
|
|
39987
|
+
const grid = occupancyGrid(this.def);
|
|
39988
|
+
const slot = grid[grid.length - 1]?.[0];
|
|
39989
|
+
const idx = this.def.cells.findIndex((c) => (c.area ?? c.id) === slot);
|
|
39990
|
+
return this.cellsById.get(this.order[idx >= 0 ? idx : 0] ?? "");
|
|
39991
|
+
}
|
|
39992
|
+
/** Keep the shared attribution mark inside the BOTTOM-LEFT visible cell: its
|
|
39993
|
+
* offsets ride that cell's renderer-published `--vela-bottom-gutter` /
|
|
39994
|
+
* `--vela-toolbar-gutter`, so collapsed pane strips push the mark up without any
|
|
39995
|
+
* bookkeeping here. Re-run after anything that changes which host that is
|
|
39996
|
+
* (layout switch, maximize, cell rebuild); a destroyed host drops the mark from
|
|
39997
|
+
* the DOM, and this re-mount brings it back. */
|
|
39998
|
+
mountAttributionMark() {
|
|
39999
|
+
const mark = this.attributionMark;
|
|
40000
|
+
if (!mark) return;
|
|
40001
|
+
const host = this.bottomLeftCell()?.host ?? this.gridEl;
|
|
40002
|
+
if (mark.parentElement !== host) host.appendChild(mark);
|
|
40003
|
+
}
|
|
39155
40004
|
/** Create the cells the current layout wants but don't exist yet (pool-first).
|
|
39156
40005
|
* A slot's CELL IDENTITY is `order[i]` (declaration order — never the slot's own
|
|
39157
40006
|
* positional id); slots past the declared list mint an auto identity once. */
|
|
@@ -39183,6 +40032,12 @@ var VelaWorkspace = class {
|
|
|
39183
40032
|
setTimezone: (zone) => this.setTimezone(zone),
|
|
39184
40033
|
context: () => this.context(),
|
|
39185
40034
|
activate: (id2) => this.setActiveCell(id2),
|
|
40035
|
+
multiCell: () => !this.monoLayout && this.def.cells.length > 1,
|
|
40036
|
+
isMaximized: (id2) => this.maximizedId === id2,
|
|
40037
|
+
toggleMaximize: (id2) => this.maximizeCell(this.maximizedId === id2 ? null : id2),
|
|
40038
|
+
cellDragTarget: (id2, x, y) => this.cellAtPoint(x, y, id2),
|
|
40039
|
+
previewDropTarget: (target) => this.setDropTarget(target),
|
|
40040
|
+
dropCell: (id2, target) => this.swapCells(id2, target),
|
|
39186
40041
|
onMarketChanged: (id2) => this.onCellMarketChanged(id2),
|
|
39187
40042
|
onPriceStyleChanged: (id2) => this.onCellPriceStyleChanged(id2),
|
|
39188
40043
|
onIndicatorsChanged: (id2) => this.onCellIndicatorsChanged(id2),
|
|
@@ -39196,6 +40051,7 @@ var VelaWorkspace = class {
|
|
|
39196
40051
|
if (id === this.activeId) cell.host.dataset.active = "1";
|
|
39197
40052
|
this.wireCell(cell);
|
|
39198
40053
|
cell.chart.renderer.setLayoutMode(this.layoutCtl.current);
|
|
40054
|
+
cell.setControlsSuspended(this.layoutCtl.current === "mobile");
|
|
39199
40055
|
if (this.favs.length > 0) cell.chart.drawings.setFavorites(this.favs);
|
|
39200
40056
|
cell.setManifest(this.manifest, pooled?.indicators == null);
|
|
39201
40057
|
cell.restorePersistedExt();
|
|
@@ -39205,6 +40061,7 @@ var VelaWorkspace = class {
|
|
|
39205
40061
|
const host = this.cellsById.get(this.order[i] ?? "")?.host;
|
|
39206
40062
|
if (host) this.gridEl.appendChild(host);
|
|
39207
40063
|
}
|
|
40064
|
+
this.mountAttributionMark();
|
|
39208
40065
|
}
|
|
39209
40066
|
/** Per-cell chart subscriptions (trigger ② — the chart instance is stable for the
|
|
39210
40067
|
* cell's whole life, so these live and die with the cell). */
|
|
@@ -39264,6 +40121,9 @@ var VelaWorkspace = class {
|
|
|
39264
40121
|
chart.on("viewport:changed", (range) => this.propagateViewport(cell.id, range));
|
|
39265
40122
|
chart.renderer.onConfigChanged(() => this.propagateStylePrefs(cell.id));
|
|
39266
40123
|
chart.on("theme:changed", (t) => this.setTheme(t));
|
|
40124
|
+
chart.on("pane:changed", () => {
|
|
40125
|
+
if (cell.id === this.activeId) this.syncMobileMaximize();
|
|
40126
|
+
});
|
|
39267
40127
|
chart.renderer.onAxisLongPress((e) => {
|
|
39268
40128
|
if (this.layoutCtl.current !== "mobile") return;
|
|
39269
40129
|
if (e.axis === "time") this.openTimezoneDrawer();
|
|
@@ -39584,6 +40444,7 @@ var VelaWorkspace = class {
|
|
|
39584
40444
|
for (const cell of this.cellsById.values()) {
|
|
39585
40445
|
cell.chart.renderer.closeDialogs();
|
|
39586
40446
|
cell.chart.renderer.setLayoutMode(mode);
|
|
40447
|
+
cell.setControlsSuspended(mode === "mobile");
|
|
39587
40448
|
}
|
|
39588
40449
|
this.syncCellPresentation();
|
|
39589
40450
|
}
|