@luxalgo/vela 0.6.9 → 0.6.10
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-CWmp31dA.d.ts} +49 -1
- package/dist/{DataProvider-DExJrfut.d.ts → DataProvider-l_eLMly_.d.cts} +49 -1
- package/dist/{chunk-EQCHJZOT.js → chunk-4KZVZ7QQ.js} +1 -1
- package/dist/{chunk-6WDDVMBJ.js → chunk-FFOP37FQ.js} +577 -321
- package/dist/{chunk-62SVGONC.js → chunk-G77Y7LK2.js} +2 -2
- package/dist/{chunk-WFSZBX3R.js → chunk-KG4YT3TI.js} +3 -1
- package/dist/{chunk-FCVIG7JG.js → chunk-MHY7MVXH.js} +5 -1
- package/dist/{chunk-STHSKXOR.js → chunk-NMTQXNT4.js} +591 -141
- package/dist/{contributions-D7PVZO2i.d.ts → contributions-BCz6Dr6a.d.ts} +6 -6
- package/dist/{contributions-C1U2Krwg.d.cts → contributions-D9vTzm5p.d.cts} +6 -6
- package/dist/index.cjs +580 -318
- package/dist/index.d.cts +31 -10
- package/dist/index.d.ts +31 -10
- package/dist/index.js +4 -4
- package/dist/{options-FM0peknS.d.ts → options-DSqHsQyN.d.cts} +6 -6
- package/dist/{options-FM0peknS.d.cts → options-DSqHsQyN.d.ts} +6 -6
- package/dist/{plugin-DfVqBz9p.d.cts → plugin-Bt8hLR8Z.d.cts} +3 -3
- package/dist/{plugin-7bkF32Rk.d.ts → plugin-CH7pfMrE.d.ts} +3 -3
- package/dist/plugin.cjs +3 -1
- package/dist/plugin.d.cts +4 -4
- package/dist/plugin.d.ts +4 -4
- package/dist/plugin.js +2 -2
- package/dist/providers/binance.d.cts +2 -2
- package/dist/providers/binance.d.ts +2 -2
- package/dist/providers/coinbase.d.cts +2 -2
- package/dist/providers/coinbase.d.ts +2 -2
- package/dist/providers/hyperliquid.d.cts +2 -2
- package/dist/providers/hyperliquid.d.ts +2 -2
- package/dist/{statusline-zdF4eZLr.d.ts → statusline-5K7y4Ya2.d.ts} +3 -3
- package/dist/{statusline-DOPiT6I6.d.cts → statusline-TYLQmoeh.d.cts} +3 -3
- 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 +580 -318
- package/dist/vela.global.min.js +50 -48
- package/dist/widget.cjs +1052 -340
- package/dist/widget.d.cts +17 -6
- package/dist/widget.d.ts +17 -6
- package/dist/widget.js +7 -7
- package/dist/workspace.cjs +1052 -340
- package/dist/workspace.d.cts +89 -5
- package/dist/workspace.d.ts +89 -5
- package/dist/workspace.js +6 -6
- package/package.json +1 -1
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
|
}
|
|
@@ -16836,7 +17013,7 @@ var DrawingToolbar = class {
|
|
|
16836
17013
|
this.root.replaceChildren();
|
|
16837
17014
|
this.groupCells.clear();
|
|
16838
17015
|
this.groupIcons.clear();
|
|
16839
|
-
this.cursorBtn = this.makeButton(CURSOR_ICON, "Cursor", () => this.
|
|
17016
|
+
this.cursorBtn = this.makeButton(CURSOR_ICON, "Cursor", () => this.onCursorClick());
|
|
16840
17017
|
this.root.appendChild(this.cursorBtn);
|
|
16841
17018
|
if (this.def.groups.length > 0) this.root.appendChild(this.divider());
|
|
16842
17019
|
for (const g of this.def.groups) {
|
|
@@ -16940,6 +17117,14 @@ var DrawingToolbar = class {
|
|
|
16940
17117
|
this.magnetIcon = icon2;
|
|
16941
17118
|
return cell;
|
|
16942
17119
|
}
|
|
17120
|
+
/** Cursor returns to select/idle: an active measure/eraser mode exits through its own
|
|
17121
|
+
* toggle callback (disarming a tool via `onArm(null)` alone can't — the host treats a
|
|
17122
|
+
* null arm as a no-op side effect of entering those modes), then the tool disarms. */
|
|
17123
|
+
onCursorClick() {
|
|
17124
|
+
if (this.measureActive) this.onMeasure();
|
|
17125
|
+
if (this.eraserActive) this.onEraser();
|
|
17126
|
+
this.onArm(null);
|
|
17127
|
+
}
|
|
16943
17128
|
/** Clicking the icon arms the group's last-used tool (it does NOT open the flyout). */
|
|
16944
17129
|
onGroupIconClick(group) {
|
|
16945
17130
|
const type = this.lastUsed.get(group.id) ?? group.tools[0]?.type;
|
|
@@ -17952,6 +18137,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
17952
18137
|
/** Invalidates detached async work (backfill loops, in-flight loads, gap heals):
|
|
17953
18138
|
* bumped by init(), setMarket() and destroy(). */
|
|
17954
18139
|
this.generation = 0;
|
|
18140
|
+
/** Aborts the in-flight PROGRESSIVE load's source polling on supersession — an
|
|
18141
|
+
* abandoned stream left polling to its own budget starves the browser's per-host
|
|
18142
|
+
* connection pool, and the NEXT symbol's very first fetch with it (measured). */
|
|
18143
|
+
this.progressiveAbort = null;
|
|
17955
18144
|
/** Awaiters racing a superseded load (setMarket callers) — released on every bump so they never hang. */
|
|
17956
18145
|
this.supersedeWaiters = [];
|
|
17957
18146
|
/** `history:complete` fired for the CURRENT load. Each market load re-arms the cycle
|
|
@@ -18094,6 +18283,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18094
18283
|
* superseded setMarket awaiters so their promises resolve instead of hanging. */
|
|
18095
18284
|
bumpGeneration() {
|
|
18096
18285
|
const gen = ++this.generation;
|
|
18286
|
+
this.progressiveAbort?.abort();
|
|
18287
|
+
this.progressiveAbort = null;
|
|
18097
18288
|
for (const w of this.supersedeWaiters.splice(0)) w();
|
|
18098
18289
|
return gen;
|
|
18099
18290
|
}
|
|
@@ -18147,7 +18338,47 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18147
18338
|
const requested = market.bars ?? 500;
|
|
18148
18339
|
const initialRange = market.visibleRange;
|
|
18149
18340
|
const deep = !market.data?.length && initialRange == null && requested > SINGLE_LOAD_BARS;
|
|
18150
|
-
|
|
18341
|
+
let progressiveServed = false;
|
|
18342
|
+
if (!market.data?.length && initialRange == null && this.feed.loadProgressive) {
|
|
18343
|
+
let painted = false;
|
|
18344
|
+
const paint = (bars, final) => {
|
|
18345
|
+
if (this.generation !== gen || !final && bars.length === 0) return;
|
|
18346
|
+
this.setBarSeries(bars, painted ? { preserveView: true } : void 0);
|
|
18347
|
+
if (!painted && bars.length > 0) {
|
|
18348
|
+
painted = true;
|
|
18349
|
+
if (opts.firstLoad) this.activateBarLayers();
|
|
18350
|
+
if (!final) this.historyState = "backfill";
|
|
18351
|
+
}
|
|
18352
|
+
};
|
|
18353
|
+
const abort = new AbortController();
|
|
18354
|
+
this.progressiveAbort = abort;
|
|
18355
|
+
progressiveServed = await new Promise((firstPaint) => {
|
|
18356
|
+
let signaled = false;
|
|
18357
|
+
const signal = (served) => {
|
|
18358
|
+
if (!signaled) {
|
|
18359
|
+
signaled = true;
|
|
18360
|
+
firstPaint(served);
|
|
18361
|
+
}
|
|
18362
|
+
};
|
|
18363
|
+
abort.signal.addEventListener("abort", () => signal(true), { once: true });
|
|
18364
|
+
this.feed.loadProgressive(market, (bars) => {
|
|
18365
|
+
paint(bars, false);
|
|
18366
|
+
if (painted) signal(true);
|
|
18367
|
+
}, { signal: abort.signal }).then((full) => {
|
|
18368
|
+
if (this.progressiveAbort === abort) this.progressiveAbort = null;
|
|
18369
|
+
if (full == null) return signal(false);
|
|
18370
|
+
if (this.generation !== gen) return signal(true);
|
|
18371
|
+
paint(full, true);
|
|
18372
|
+
this.completeHistory(full.length >= requested ? "depth" : "genesis");
|
|
18373
|
+
signal(true);
|
|
18374
|
+
}).catch(() => {
|
|
18375
|
+
if (this.progressiveAbort === abort) this.progressiveAbort = null;
|
|
18376
|
+
if (this.generation === gen) this.completeHistory("aborted");
|
|
18377
|
+
signal(true);
|
|
18378
|
+
});
|
|
18379
|
+
});
|
|
18380
|
+
}
|
|
18381
|
+
if (progressiveServed) ; else if (deep && this.feed.loadRange) {
|
|
18151
18382
|
const head = await this.feed.load({ ...market, bars: Math.min(requested, CHUNK_BARS) });
|
|
18152
18383
|
if (this.generation !== gen) return;
|
|
18153
18384
|
this.setBarSeries(head);
|
|
@@ -20890,6 +21121,9 @@ var CLOSE_SVG = iconAt("close", LEGEND_ICON_PX2);
|
|
|
20890
21121
|
var FOLD_SVG = iconAt("chevron-up", LEGEND_ICON_PX2);
|
|
20891
21122
|
var UNFOLD_SVG = iconAt("chevron-down", LEGEND_ICON_PX2);
|
|
20892
21123
|
var OVERVIEW_SVG = iconAt("objects", LEGEND_ICON_PX2);
|
|
21124
|
+
function legendCalloutsDisplay(open2, hasCallouts) {
|
|
21125
|
+
return !open2 && hasCallouts ? "inline-flex" : "none";
|
|
21126
|
+
}
|
|
20893
21127
|
var InputsUI = class {
|
|
20894
21128
|
constructor(container, theme, paneBoundsOf) {
|
|
20895
21129
|
this.container = container;
|
|
@@ -21093,7 +21327,7 @@ var InputsUI = class {
|
|
|
21093
21327
|
row.callouts = [];
|
|
21094
21328
|
row.calloutsEl.replaceChildren();
|
|
21095
21329
|
const views = this.legendCallouts?.(row.id) ?? [];
|
|
21096
|
-
row.calloutsEl.style.display = views.length > 0
|
|
21330
|
+
row.calloutsEl.style.display = legendCalloutsDisplay(row.highlighted, views.length > 0);
|
|
21097
21331
|
for (const view of views) {
|
|
21098
21332
|
const bubble = new CalloutBubble({
|
|
21099
21333
|
icon: view.icon,
|
|
@@ -21577,11 +21811,11 @@ var InputsUI = class {
|
|
|
21577
21811
|
row.controlsEl.style.display = open2 || row.hidden ? "inline-flex" : "none";
|
|
21578
21812
|
if (open2) {
|
|
21579
21813
|
row.el.appendChild(row.statusEl);
|
|
21580
|
-
row.
|
|
21814
|
+
for (const bubble of row.callouts) bubble.hidePanel();
|
|
21581
21815
|
} else {
|
|
21582
21816
|
row.el.insertBefore(row.statusEl, row.valuesEl);
|
|
21583
|
-
row.el.insertBefore(row.calloutsEl, row.statusEl);
|
|
21584
21817
|
}
|
|
21818
|
+
row.calloutsEl.style.display = legendCalloutsDisplay(open2, row.callouts.length > 0);
|
|
21585
21819
|
for (const child of Array.from(row.controlsEl.children)) {
|
|
21586
21820
|
if (!(child instanceof HTMLElement) || child === row.eyeEl) continue;
|
|
21587
21821
|
if (child === row.extrasEl) {
|
|
@@ -21671,7 +21905,7 @@ var ICONS = {
|
|
|
21671
21905
|
};
|
|
21672
21906
|
var STYLE_ID21 = "vela-pane-controls";
|
|
21673
21907
|
var ICON_PX = 12;
|
|
21674
|
-
var CLUSTER_PILL = "rgba(0,0,0,0.
|
|
21908
|
+
var CLUSTER_PILL = "rgba(0,0,0,0.65)";
|
|
21675
21909
|
function ensureStyles3() {
|
|
21676
21910
|
if (typeof document === "undefined" || document.getElementById(STYLE_ID21)) return;
|
|
21677
21911
|
const st = document.createElement("style");
|
|
@@ -21691,8 +21925,12 @@ var PaneControls = class {
|
|
|
21691
21925
|
this.deps = deps;
|
|
21692
21926
|
this.clusters = /* @__PURE__ */ new Map();
|
|
21693
21927
|
this.hoverPaneId = null;
|
|
21928
|
+
/** Mobile: hover clusters are meaningless without a cursor — suppressed; a
|
|
21929
|
+
* collapsed pane's standalone expand chip stays (the only way back up). */
|
|
21930
|
+
this.suspended = false;
|
|
21694
21931
|
/** Reveal the cluster for the pane under the cursor, resolved from the pointer's y in the plot. */
|
|
21695
21932
|
this.onPlotMove = (e) => {
|
|
21933
|
+
if (this.suspended) return;
|
|
21696
21934
|
const rect = this.plot.getBoundingClientRect();
|
|
21697
21935
|
const y = e.clientY - rect.top;
|
|
21698
21936
|
let hit = null;
|
|
@@ -21764,7 +22002,12 @@ var PaneControls = class {
|
|
|
21764
22002
|
}
|
|
21765
22003
|
if (p.count > 1) {
|
|
21766
22004
|
cluster.appendChild(
|
|
21767
|
-
this.button(p.maximized ? ICONS.restore : ICONS.maximize, p.maximized ? "Restore pane" : "Maximize pane", false, () => this.deps.onToggleMaximize(p.id), {
|
|
22005
|
+
this.button(p.maximized ? ICONS.restore : ICONS.maximize, p.maximized ? "Restore pane" : "Maximize pane", false, () => this.deps.onToggleMaximize(p.id), {
|
|
22006
|
+
role: "maximize",
|
|
22007
|
+
// Same inverse-chip treatment as the collapsed pane's expand toggle: the
|
|
22008
|
+
// maximized state must read as an active state, not just a swapped glyph.
|
|
22009
|
+
selected: p.maximized
|
|
22010
|
+
})
|
|
21768
22011
|
);
|
|
21769
22012
|
}
|
|
21770
22013
|
}
|
|
@@ -21801,17 +22044,18 @@ var PaneControls = class {
|
|
|
21801
22044
|
}
|
|
21802
22045
|
const hovered = id === this.hoverPaneId;
|
|
21803
22046
|
const hasButtons = cluster.children.length > 0;
|
|
21804
|
-
const
|
|
22047
|
+
const stateChipRole = p.collapsed ? "collapse" : !this.suspended && p.maximized ? "maximize" : null;
|
|
22048
|
+
const visible = hasButtons && (hovered || stateChipRole != null) && p.height > 8;
|
|
21805
22049
|
cluster.style.right = `${rightPx}px`;
|
|
21806
22050
|
cluster.style.top = p.collapsed ? `${p.top + Math.max(1, Math.round((p.height - 24) / 2))}px` : `${p.top + 4}px`;
|
|
21807
22051
|
cluster.style.display = visible ? "flex" : "none";
|
|
21808
22052
|
if (!visible) continue;
|
|
21809
|
-
const
|
|
21810
|
-
cluster.style.background =
|
|
22053
|
+
const soloChip = stateChipRole != null && !hovered;
|
|
22054
|
+
cluster.style.background = soloChip ? "transparent" : CLUSTER_PILL;
|
|
21811
22055
|
for (const child of cluster.children) {
|
|
21812
22056
|
const btn2 = child;
|
|
21813
22057
|
btn2.style.display = "inline-flex";
|
|
21814
|
-
btn2.style.visibility =
|
|
22058
|
+
btn2.style.visibility = soloChip && btn2.dataset.role !== stateChipRole ? "hidden" : "visible";
|
|
21815
22059
|
}
|
|
21816
22060
|
}
|
|
21817
22061
|
}
|
|
@@ -21821,6 +22065,14 @@ var PaneControls = class {
|
|
|
21821
22065
|
this.hoverPaneId = paneId;
|
|
21822
22066
|
this.reposition();
|
|
21823
22067
|
}
|
|
22068
|
+
/** Mobile suppression: no hover clusters (touch has no cursor; the shell's own
|
|
22069
|
+
* chrome covers maximize), while collapsed panes keep their expand chips. */
|
|
22070
|
+
setSuspended(on) {
|
|
22071
|
+
if (on === this.suspended) return;
|
|
22072
|
+
this.suspended = on;
|
|
22073
|
+
if (on) this.hoverPaneId = null;
|
|
22074
|
+
this.reposition();
|
|
22075
|
+
}
|
|
21824
22076
|
destroy() {
|
|
21825
22077
|
this.plot.removeEventListener("pointermove", this.onPlotMove);
|
|
21826
22078
|
this.plot.removeEventListener("pointerleave", this.onPlotLeave);
|
|
@@ -21971,160 +22223,6 @@ var AxisScaleButtons = class {
|
|
|
21971
22223
|
}
|
|
21972
22224
|
};
|
|
21973
22225
|
|
|
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
22226
|
// src/renderers/native/capabilities.ts
|
|
22129
22227
|
var NATIVE_CAPABILITIES = {
|
|
22130
22228
|
panes: true,
|
|
@@ -22145,7 +22243,7 @@ var NATIVE_CAPABILITIES = {
|
|
|
22145
22243
|
drawingDepth: true,
|
|
22146
22244
|
// drawings share the series' z space (backend-composited interleave layers)
|
|
22147
22245
|
tables: true,
|
|
22148
|
-
//
|
|
22246
|
+
// canvas-painted into the owning indicator's interleave slice
|
|
22149
22247
|
trades: true,
|
|
22150
22248
|
// strategy order-fill markers (arrows + labels + fill-price ticks)
|
|
22151
22249
|
inputsUI: true
|
|
@@ -22176,6 +22274,9 @@ function candleTier(spacing) {
|
|
|
22176
22274
|
if (spacing < CANDLE_BODY_MIN_SPACING) return "wick";
|
|
22177
22275
|
return "full";
|
|
22178
22276
|
}
|
|
22277
|
+
function snapY(yCss, dpr) {
|
|
22278
|
+
return Math.round(yCss * dpr) / dpr;
|
|
22279
|
+
}
|
|
22179
22280
|
function candleGeometry(xCss, spacing, dpr, bodyScale = 1) {
|
|
22180
22281
|
const wickDev = Math.max(1, Math.round(wickWidth(spacing) * dpr));
|
|
22181
22282
|
const wickLeftDev = Math.round(xCss * dpr - wickDev / 2);
|
|
@@ -22699,11 +22800,9 @@ var WebGL2Backend = class {
|
|
|
22699
22800
|
};
|
|
22700
22801
|
b.alpha = this.modelAlpha;
|
|
22701
22802
|
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
22803
|
if (isPrice) {
|
|
22704
22804
|
for (const m of scene.indicators.values()) {
|
|
22705
22805
|
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
22806
|
}
|
|
22708
22807
|
}
|
|
22709
22808
|
const drawCandles = isPrice && !scene.candlesHidden;
|
|
@@ -22719,6 +22818,7 @@ var WebGL2Backend = class {
|
|
|
22719
22818
|
b.alpha = this.modelAlpha;
|
|
22720
22819
|
const off = scene.offsetOf(m.id);
|
|
22721
22820
|
const mp = effPane(m);
|
|
22821
|
+
for (const f of m.fills) if (f.overlay !== true) this.emitFill(b, m, f, mp, coords, i0, i1, off);
|
|
22722
22822
|
for (const s of m.series) if (s.overlay !== true) this.emitSeries(b, s, mp, coords, i0, i1, theme, off);
|
|
22723
22823
|
}
|
|
22724
22824
|
if (drawCandles && !candleDrawn) {
|
|
@@ -22726,14 +22826,18 @@ var WebGL2Backend = class {
|
|
|
22726
22826
|
b.alpha = this.candleStructureAlpha;
|
|
22727
22827
|
this.emitPriceSeries(b, scene, i0, i1, coords, pane, theme, barColorMap, dataW);
|
|
22728
22828
|
}
|
|
22729
|
-
drawSlicesUpTo(Infinity);
|
|
22730
22829
|
b.alpha = this.modelAlpha;
|
|
22731
22830
|
if (isPrice) {
|
|
22831
|
+
for (const m of scene.indicators.values()) {
|
|
22832
|
+
const off = scene.offsetOf(m.id);
|
|
22833
|
+
for (const f of m.fills) if (f.overlay === true) this.emitFill(b, m, f, pane, coords, i0, i1, off);
|
|
22834
|
+
}
|
|
22732
22835
|
for (const m of scene.indicators.values()) {
|
|
22733
22836
|
const off = scene.offsetOf(m.id);
|
|
22734
22837
|
for (const s of m.series) if (s.overlay === true) this.emitSeries(b, s, pane, coords, i0, i1, theme, off);
|
|
22735
22838
|
}
|
|
22736
22839
|
}
|
|
22840
|
+
drawSlicesUpTo(Infinity);
|
|
22737
22841
|
for (const m of models) {
|
|
22738
22842
|
const mp = effPane(m);
|
|
22739
22843
|
for (const pl of m.priceLines) this.emitHline(b, pl, mp, coords, dataW, theme);
|
|
@@ -23128,12 +23232,12 @@ var WebGL2Backend = class {
|
|
|
23128
23232
|
if (drawBody) {
|
|
23129
23233
|
const oY = coords.priceToY(bar.open, pane.scale, pane.bounds);
|
|
23130
23234
|
const cY = coords.priceToY(bar.close, pane.scale, pane.bounds);
|
|
23131
|
-
bodyTop = Math.min(oY, cY);
|
|
23132
|
-
bodyH = Math.max(1, Math.
|
|
23235
|
+
bodyTop = snapY(Math.min(oY, cY), coords.dpr);
|
|
23236
|
+
bodyH = Math.max(1 / coords.dpr, snapY(Math.max(oY, cY), coords.dpr) - bodyTop);
|
|
23133
23237
|
}
|
|
23134
23238
|
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);
|
|
23239
|
+
const hY = snapY(coords.priceToY(bar.high, pane.scale, pane.bounds), coords.dpr);
|
|
23240
|
+
const lY = snapY(coords.priceToY(bar.low, pane.scale, pane.bounds), coords.dpr);
|
|
23137
23241
|
b.alpha = this.candleStructureAlpha;
|
|
23138
23242
|
const wCol = parseColor((isUp ? cs.wickUpColor : cs.wickDownColor) ?? (drawBody ? dir : bodyColorStr));
|
|
23139
23243
|
if (drawBody) {
|
|
@@ -24466,9 +24570,10 @@ var SceneGraph = class {
|
|
|
24466
24570
|
* so each indicator arrives behind the candles (and behind older indicators);
|
|
24467
24571
|
* `setIndicatorZ`/`bringToFront`/`sendToBack` change it. */
|
|
24468
24572
|
this.seriesZ = /* @__PURE__ */ new Map();
|
|
24469
|
-
/** Per-pane raster layers of
|
|
24573
|
+
/** Per-pane raster layers of drawings interleaved into the series stack (each
|
|
24574
|
+
* indicator's Pine drawings at its model's z, plus in-stack user drawings) — each a
|
|
24470
24575
|
* prepainted canvas the backend composites just before the series carrying `beforeZ`.
|
|
24471
|
-
* Rebuilt by the renderer per data frame
|
|
24576
|
+
* Rebuilt by the renderer per data frame. */
|
|
24472
24577
|
this.drawingSlices = /* @__PURE__ */ new Map();
|
|
24473
24578
|
/** Per-model index offset: the chart bar index of the model's `anchorTime` — its
|
|
24474
24579
|
* index-aligned payloads (dense series arrays, `bar_index` drawings) count from that
|
|
@@ -24703,11 +24808,9 @@ var Canvas2dBackend = class {
|
|
|
24703
24808
|
};
|
|
24704
24809
|
ctx.globalAlpha = this.modelAlpha;
|
|
24705
24810
|
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
24811
|
if (isPrice) {
|
|
24708
24812
|
for (const m of scene.indicators.values()) {
|
|
24709
24813
|
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
24814
|
}
|
|
24712
24815
|
}
|
|
24713
24816
|
const slices = scene.drawingSlices.get(pane.id) ?? [];
|
|
@@ -24731,6 +24834,7 @@ var Canvas2dBackend = class {
|
|
|
24731
24834
|
ctx.globalAlpha = this.modelAlpha;
|
|
24732
24835
|
const off = scene.offsetOf(m.id);
|
|
24733
24836
|
const mp = effPane(m);
|
|
24837
|
+
for (const f of m.fills) if (f.overlay !== true) this.drawFill(ctx, m, f, mp, coords, i0, i1, off);
|
|
24734
24838
|
for (const s of m.series) if (s.overlay !== true) this.drawSeries(ctx, s, mp, coords, i0, i1, theme, off);
|
|
24735
24839
|
}
|
|
24736
24840
|
if (drawCandles && !candleDrawn) {
|
|
@@ -24738,14 +24842,18 @@ var Canvas2dBackend = class {
|
|
|
24738
24842
|
ctx.globalAlpha = this.candleStructureAlpha;
|
|
24739
24843
|
this.drawPriceSeries(ctx, scene, i0, i1, coords, pane, theme, barColorMap, dataW);
|
|
24740
24844
|
}
|
|
24741
|
-
drawSlicesUpTo(Infinity);
|
|
24742
24845
|
if (isPrice) {
|
|
24743
24846
|
ctx.globalAlpha = this.modelAlpha;
|
|
24847
|
+
for (const m of scene.indicators.values()) {
|
|
24848
|
+
const off = scene.offsetOf(m.id);
|
|
24849
|
+
for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0, i1, off);
|
|
24850
|
+
}
|
|
24744
24851
|
for (const m of scene.indicators.values()) {
|
|
24745
24852
|
const off = scene.offsetOf(m.id);
|
|
24746
24853
|
for (const s of m.series) if (s.overlay === true) this.drawSeries(ctx, s, pane, coords, i0, i1, theme, off);
|
|
24747
24854
|
}
|
|
24748
24855
|
}
|
|
24856
|
+
drawSlicesUpTo(Infinity);
|
|
24749
24857
|
ctx.globalAlpha = this.modelAlpha;
|
|
24750
24858
|
for (const m of models) {
|
|
24751
24859
|
const mp = effPane(m);
|
|
@@ -24989,13 +25097,13 @@ var Canvas2dBackend = class {
|
|
|
24989
25097
|
if (drawBody) {
|
|
24990
25098
|
const oY = coords.priceToY(b.open, pane.scale, pane.bounds);
|
|
24991
25099
|
const cY = coords.priceToY(b.close, pane.scale, pane.bounds);
|
|
24992
|
-
top = Math.min(oY, cY);
|
|
24993
|
-
bodyH = Math.max(1, Math.
|
|
25100
|
+
top = snapY(Math.min(oY, cY), coords.dpr);
|
|
25101
|
+
bodyH = Math.max(1 / coords.dpr, snapY(Math.max(oY, cY), coords.dpr) - top);
|
|
24994
25102
|
}
|
|
24995
25103
|
if (cs.wickVisible) {
|
|
24996
25104
|
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);
|
|
25105
|
+
const hY = snapY(coords.priceToY(b.high, pane.scale, pane.bounds), coords.dpr);
|
|
25106
|
+
const lY = snapY(coords.priceToY(b.low, pane.scale, pane.bounds), coords.dpr);
|
|
24999
25107
|
ctx.globalAlpha = this.candleStructureAlpha;
|
|
25000
25108
|
ctx.strokeStyle = wick;
|
|
25001
25109
|
ctx.lineWidth = g.wickW;
|
|
@@ -25442,6 +25550,19 @@ function autoFontSize(lines, boxW, boxH, bold) {
|
|
|
25442
25550
|
|
|
25443
25551
|
// src/renderers/shared/DrawingSceneRenderer.ts
|
|
25444
25552
|
var EMPTY_DRAWING_SET = { lines: [], boxes: [], labels: [], polylines: [], linefills: [] };
|
|
25553
|
+
function modelDrawingSet(m, overlay) {
|
|
25554
|
+
const want = (d) => Boolean(d.overlay) === overlay;
|
|
25555
|
+
return {
|
|
25556
|
+
lines: (m.lines ?? []).filter(want),
|
|
25557
|
+
boxes: (m.boxes ?? []).filter(want),
|
|
25558
|
+
labels: (m.labels ?? []).filter(want),
|
|
25559
|
+
polylines: (m.polylines ?? []).filter(want),
|
|
25560
|
+
linefills: (m.linefills ?? []).filter(want)
|
|
25561
|
+
};
|
|
25562
|
+
}
|
|
25563
|
+
function drawingSetEmpty(s) {
|
|
25564
|
+
return !s.lines.length && !s.boxes.length && !s.labels.length && !s.polylines.length && !s.linefills.length;
|
|
25565
|
+
}
|
|
25445
25566
|
function fontSizePx(size) {
|
|
25446
25567
|
return size === "auto" ? 12 : namedFontSize(size);
|
|
25447
25568
|
}
|
|
@@ -26294,10 +26415,8 @@ var ChromeRenderer = class {
|
|
|
26294
26415
|
this.ctx = null;
|
|
26295
26416
|
// The color for axis tick labels — the host-passed surface text, set each frame in render().
|
|
26296
26417
|
this.axisTextColor = DARK_THEME.textColor;
|
|
26297
|
-
// Shared Pine-drawing renderer
|
|
26418
|
+
// Shared Pine-drawing renderer, used here for autoscale geometry only; widthCache persists.
|
|
26298
26419
|
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
26420
|
}
|
|
26302
26421
|
mount(canvas) {
|
|
26303
26422
|
this.canvas = canvas;
|
|
@@ -26321,8 +26440,8 @@ var ChromeRenderer = class {
|
|
|
26321
26440
|
*/
|
|
26322
26441
|
paneDrawingsRange(ownModels, scene, isPricePane, vr) {
|
|
26323
26442
|
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(
|
|
26443
|
+
for (const m of ownModels) dr = unionRange(dr, this.drawingsRange(modelDrawingSet(m, false), vr, scene.offsetOf(m.id)));
|
|
26444
|
+
if (isPricePane) for (const m of scene.indicators.values()) dr = unionRange(dr, this.drawingsRange(modelDrawingSet(m, true), vr, scene.offsetOf(m.id)));
|
|
26326
26445
|
return dr;
|
|
26327
26446
|
}
|
|
26328
26447
|
/** Clear the chrome canvas and draw drawings + axes + current-price line.
|
|
@@ -26339,7 +26458,6 @@ var ChromeRenderer = class {
|
|
|
26339
26458
|
const dataW = coords.width;
|
|
26340
26459
|
const dataH = coords.height;
|
|
26341
26460
|
this.axisTextColor = surface?.textColor ?? theme.textColor;
|
|
26342
|
-
this.labelTips = [];
|
|
26343
26461
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
26344
26462
|
ctx.clearRect(0, 0, fullW, fullH);
|
|
26345
26463
|
if (surface && (fullW > dataW || fullH > dataH)) {
|
|
@@ -26355,17 +26473,6 @@ var ChromeRenderer = class {
|
|
|
26355
26473
|
return;
|
|
26356
26474
|
}
|
|
26357
26475
|
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
26476
|
if (pricePane && !pricePane.collapsed && scene.tradeMarkers.visible) {
|
|
26370
26477
|
for (const m of scene.indicators.values()) {
|
|
26371
26478
|
if (m.trades?.length) this.renderTrades(ctx, coords, scene, theme, m.trades, pricePane, dataW);
|
|
@@ -26381,25 +26488,6 @@ var ChromeRenderer = class {
|
|
|
26381
26488
|
this.canvas = null;
|
|
26382
26489
|
this.ctx = null;
|
|
26383
26490
|
}
|
|
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
26491
|
drawingsRange(set, vr, indexOffset = 0) {
|
|
26404
26492
|
this.drawScene.setSet(set, indexOffset);
|
|
26405
26493
|
if (this.drawScene.isEmpty()) return null;
|
|
@@ -26433,34 +26521,6 @@ var ChromeRenderer = class {
|
|
|
26433
26521
|
);
|
|
26434
26522
|
ctx.restore();
|
|
26435
26523
|
}
|
|
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
26524
|
// ── axes ──
|
|
26465
26525
|
drawPriceAxes(ctx, scene, coords, theme, dataW, panes) {
|
|
26466
26526
|
ctx.strokeStyle = scene.style.borderColor ?? theme.borderColor;
|
|
@@ -30779,17 +30839,17 @@ function glyphIcon(glyph) {
|
|
|
30779
30839
|
return textGlyph(String(glyph), 15);
|
|
30780
30840
|
}
|
|
30781
30841
|
function stampSizeIcon(size) {
|
|
30782
|
-
return textGlyph("\u25CF", (
|
|
30842
|
+
return textGlyph("\u25CF", (SIZE_PX3[String(size)] ?? 13) + 4);
|
|
30783
30843
|
}
|
|
30784
30844
|
function sizeIcon(size) {
|
|
30785
30845
|
return textGlyph(String(size).charAt(0).toUpperCase(), 15);
|
|
30786
30846
|
}
|
|
30787
|
-
var
|
|
30847
|
+
var SIZE_PX3 = { small: 10, normal: 13, large: 16, huge: 20 };
|
|
30788
30848
|
function numbersSizeIcon(size) {
|
|
30789
|
-
return textGlyph("12", (
|
|
30849
|
+
return textGlyph("12", (SIZE_PX3[String(size)] ?? 13) - 1, 16.5, 'font-weight="600"');
|
|
30790
30850
|
}
|
|
30791
30851
|
function labelSizeIcon(size) {
|
|
30792
|
-
return textGlyph("T", (
|
|
30852
|
+
return textGlyph("T", (SIZE_PX3[String(size)] ?? 13) + 2);
|
|
30793
30853
|
}
|
|
30794
30854
|
function capitalize(s) {
|
|
30795
30855
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
@@ -31764,6 +31824,315 @@ var UserDrawingController = class {
|
|
|
31764
31824
|
}
|
|
31765
31825
|
};
|
|
31766
31826
|
|
|
31827
|
+
// src/renderers/shared/TableOverlay.ts
|
|
31828
|
+
var SIZE_PX4 = {
|
|
31829
|
+
auto: 13,
|
|
31830
|
+
tiny: 10,
|
|
31831
|
+
small: 11,
|
|
31832
|
+
normal: 13,
|
|
31833
|
+
large: 16,
|
|
31834
|
+
huge: 20
|
|
31835
|
+
};
|
|
31836
|
+
function fontPxOf(size) {
|
|
31837
|
+
if (typeof size === "number") return size > 0 ? size : SIZE_PX4.auto;
|
|
31838
|
+
return SIZE_PX4[size] ?? SIZE_PX4.auto;
|
|
31839
|
+
}
|
|
31840
|
+
function tableHasContent(t) {
|
|
31841
|
+
return t.cells.some((row) => row?.some((c) => c != null && !c.merged));
|
|
31842
|
+
}
|
|
31843
|
+
function mergeRenderPlan(t) {
|
|
31844
|
+
const span = /* @__PURE__ */ new Map();
|
|
31845
|
+
const omit = /* @__PURE__ */ new Set();
|
|
31846
|
+
for (const m of t.merges) {
|
|
31847
|
+
span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
|
|
31848
|
+
for (let r = m.startRow; r <= m.endRow; r += 1) {
|
|
31849
|
+
for (let c = m.startCol; c <= m.endCol; c += 1) {
|
|
31850
|
+
if (r !== m.startRow || c !== m.startCol) omit.add(`${r}:${c}`);
|
|
31851
|
+
}
|
|
31852
|
+
}
|
|
31853
|
+
}
|
|
31854
|
+
for (let r = 0; r < t.rows; r += 1) {
|
|
31855
|
+
for (let c = 0; c < t.columns; c += 1) {
|
|
31856
|
+
if (t.cells[r]?.[c]?.merged && !span.has(`${r}:${c}`)) omit.add(`${r}:${c}`);
|
|
31857
|
+
}
|
|
31858
|
+
}
|
|
31859
|
+
for (const key of span.keys()) omit.delete(key);
|
|
31860
|
+
return { span, omit };
|
|
31861
|
+
}
|
|
31862
|
+
|
|
31863
|
+
// src/renderers/shared/TableCanvasRenderer.ts
|
|
31864
|
+
var PAD_X = 6;
|
|
31865
|
+
var PAD_Y = 2;
|
|
31866
|
+
var MARGIN = 6;
|
|
31867
|
+
var LINE_HEIGHT = 1.2;
|
|
31868
|
+
function paintTable(ctx, t, args, tips) {
|
|
31869
|
+
if (!tableHasContent(t)) return;
|
|
31870
|
+
const layout = layoutTable(ctx, t, args);
|
|
31871
|
+
if (!layout || layout.w <= 0 || layout.h <= 0) return;
|
|
31872
|
+
const fw = t.frameColor && t.frameWidth > 0 ? t.frameWidth : 0;
|
|
31873
|
+
const { x, y } = anchorOrigin(t.position, layout.w + 2 * fw, layout.h + 2 * fw, args);
|
|
31874
|
+
const x0 = x + fw;
|
|
31875
|
+
const y0 = y + fw;
|
|
31876
|
+
if (t.bgColor) {
|
|
31877
|
+
ctx.fillStyle = t.bgColor;
|
|
31878
|
+
ctx.fillRect(x0, y0, layout.w, layout.h);
|
|
31879
|
+
}
|
|
31880
|
+
if (fw > 0 && t.frameColor) {
|
|
31881
|
+
ctx.strokeStyle = t.frameColor;
|
|
31882
|
+
ctx.lineWidth = fw;
|
|
31883
|
+
ctx.strokeRect(x + fw / 2, y + fw / 2, layout.w + fw, layout.h + fw);
|
|
31884
|
+
}
|
|
31885
|
+
const colX = [0];
|
|
31886
|
+
for (const w of layout.colW) colX.push(colX[colX.length - 1] + w);
|
|
31887
|
+
const rowY = [0];
|
|
31888
|
+
for (const h of layout.rowH) rowY.push(rowY[rowY.length - 1] + h);
|
|
31889
|
+
const prevBaseline = ctx.textBaseline;
|
|
31890
|
+
const prevAlign = ctx.textAlign;
|
|
31891
|
+
ctx.textBaseline = "middle";
|
|
31892
|
+
for (const box of layout.boxes) {
|
|
31893
|
+
const rx = x0 + colX[box.c];
|
|
31894
|
+
const ry = y0 + rowY[box.r];
|
|
31895
|
+
const rw = colX[box.c + box.cs] - colX[box.c];
|
|
31896
|
+
const rh = rowY[box.r + box.rs] - rowY[box.r];
|
|
31897
|
+
const cell = box.cell;
|
|
31898
|
+
if (cell.bgColor) {
|
|
31899
|
+
ctx.fillStyle = cell.bgColor;
|
|
31900
|
+
ctx.fillRect(rx, ry, rw, rh);
|
|
31901
|
+
}
|
|
31902
|
+
const text = cell.text ?? "";
|
|
31903
|
+
if (text.length > 0) {
|
|
31904
|
+
const px = fontPxOf(cell.textSize);
|
|
31905
|
+
ctx.font = cellFont(cell, px, args.theme);
|
|
31906
|
+
ctx.fillStyle = cell.textColor ?? args.theme.textColor;
|
|
31907
|
+
const lines = text.split("\n");
|
|
31908
|
+
const blockH = lines.length * px * LINE_HEIGHT;
|
|
31909
|
+
const blockTop = cell.vAlign === "top" ? ry + PAD_Y : cell.vAlign === "bottom" ? ry + rh - PAD_Y - blockH : ry + (rh - blockH) / 2;
|
|
31910
|
+
const tx = cell.hAlign === "left" ? rx + PAD_X : cell.hAlign === "right" ? rx + rw - PAD_X : rx + rw / 2;
|
|
31911
|
+
ctx.textAlign = cell.hAlign;
|
|
31912
|
+
lines.forEach((line, i) => ctx.fillText(line, tx, blockTop + (i + 0.5) * px * LINE_HEIGHT));
|
|
31913
|
+
}
|
|
31914
|
+
if (cell.tooltip) tips.push({ left: rx, top: ry, right: rx + rw, bottom: ry + rh, text: cell.tooltip });
|
|
31915
|
+
}
|
|
31916
|
+
ctx.textBaseline = prevBaseline;
|
|
31917
|
+
ctx.textAlign = prevAlign;
|
|
31918
|
+
if (t.borderColor && t.borderWidth > 0) {
|
|
31919
|
+
ctx.strokeStyle = t.borderColor;
|
|
31920
|
+
ctx.lineWidth = t.borderWidth;
|
|
31921
|
+
const seen = /* @__PURE__ */ new Set();
|
|
31922
|
+
ctx.beginPath();
|
|
31923
|
+
const edge = (ax, ay, bx, by) => {
|
|
31924
|
+
const key = `${ax},${ay},${bx},${by}`;
|
|
31925
|
+
if (seen.has(key)) return;
|
|
31926
|
+
seen.add(key);
|
|
31927
|
+
ctx.moveTo(ax, ay);
|
|
31928
|
+
ctx.lineTo(bx, by);
|
|
31929
|
+
};
|
|
31930
|
+
for (const box of layout.boxes) {
|
|
31931
|
+
const l = Math.round(x0 + colX[box.c]);
|
|
31932
|
+
const r = Math.round(x0 + colX[box.c + box.cs]);
|
|
31933
|
+
const tp = Math.round(y0 + rowY[box.r]);
|
|
31934
|
+
const bt = Math.round(y0 + rowY[box.r + box.rs]);
|
|
31935
|
+
edge(l, tp, r, tp);
|
|
31936
|
+
edge(l, bt, r, bt);
|
|
31937
|
+
edge(l, tp, l, bt);
|
|
31938
|
+
edge(r, tp, r, bt);
|
|
31939
|
+
}
|
|
31940
|
+
ctx.stroke();
|
|
31941
|
+
}
|
|
31942
|
+
}
|
|
31943
|
+
function layoutTable(ctx, t, args) {
|
|
31944
|
+
const { span, omit } = mergeRenderPlan(t);
|
|
31945
|
+
const colW = new Array(t.columns).fill(0);
|
|
31946
|
+
const rowH = new Array(t.rows).fill(0);
|
|
31947
|
+
const boxes = [];
|
|
31948
|
+
for (let r = 0; r < t.rows; r += 1) {
|
|
31949
|
+
for (let c = 0; c < t.columns; c += 1) {
|
|
31950
|
+
if (omit.has(`${r}:${c}`)) continue;
|
|
31951
|
+
const cell = t.cells[r]?.[c];
|
|
31952
|
+
if (cell == null) continue;
|
|
31953
|
+
const sp = span.get(`${r}:${c}`);
|
|
31954
|
+
boxes.push({ cell, r, c, cs: Math.min(sp?.cs ?? 1, t.columns - c), rs: Math.min(sp?.rs ?? 1, t.rows - r) });
|
|
31955
|
+
}
|
|
31956
|
+
}
|
|
31957
|
+
if (boxes.length === 0) return null;
|
|
31958
|
+
const sizeOf = (cell) => {
|
|
31959
|
+
const px = fontPxOf(cell.textSize);
|
|
31960
|
+
ctx.font = cellFont(cell, px, args.theme);
|
|
31961
|
+
const lines = (cell.text ?? "").split("\n");
|
|
31962
|
+
let maxW = 0;
|
|
31963
|
+
for (const line of lines) maxW = Math.max(maxW, ctx.measureText(line).width);
|
|
31964
|
+
let w2 = Math.ceil(maxW) + 2 * PAD_X;
|
|
31965
|
+
let h2 = Math.ceil(lines.length * px * LINE_HEIGHT) + 2 * PAD_Y;
|
|
31966
|
+
if (cell.width) w2 = Math.max(w2, cell.width / 100 * args.plotWidth);
|
|
31967
|
+
if (cell.height) h2 = Math.max(h2, cell.height / 100 * args.paneHeight);
|
|
31968
|
+
return { w: w2, h: h2 };
|
|
31969
|
+
};
|
|
31970
|
+
const spanning = [];
|
|
31971
|
+
for (const box of boxes) {
|
|
31972
|
+
const { w: w2, h: h2 } = sizeOf(box.cell);
|
|
31973
|
+
if (box.cs === 1) colW[box.c] = Math.max(colW[box.c], w2);
|
|
31974
|
+
if (box.rs === 1) rowH[box.r] = Math.max(rowH[box.r], h2);
|
|
31975
|
+
if (box.cs > 1 || box.rs > 1) spanning.push({ box, w: w2, h: h2 });
|
|
31976
|
+
}
|
|
31977
|
+
for (const { box, w: w2, h: h2 } of spanning) {
|
|
31978
|
+
if (box.cs > 1) {
|
|
31979
|
+
let sum = 0;
|
|
31980
|
+
for (let c = box.c; c < box.c + box.cs; c += 1) sum += colW[c];
|
|
31981
|
+
if (w2 > sum) for (let c = box.c; c < box.c + box.cs; c += 1) colW[c] += (w2 - sum) / box.cs;
|
|
31982
|
+
}
|
|
31983
|
+
if (box.rs > 1) {
|
|
31984
|
+
let sum = 0;
|
|
31985
|
+
for (let r = box.r; r < box.r + box.rs; r += 1) sum += rowH[r];
|
|
31986
|
+
if (h2 > sum) for (let r = box.r; r < box.r + box.rs; r += 1) rowH[r] += (h2 - sum) / box.rs;
|
|
31987
|
+
}
|
|
31988
|
+
}
|
|
31989
|
+
let w = 0;
|
|
31990
|
+
for (const cw of colW) w += cw;
|
|
31991
|
+
let h = 0;
|
|
31992
|
+
for (const rh of rowH) h += rh;
|
|
31993
|
+
return { colW, rowH, w, h, boxes };
|
|
31994
|
+
}
|
|
31995
|
+
function anchorOrigin(position, totalW, totalH, args) {
|
|
31996
|
+
let y;
|
|
31997
|
+
if (position.startsWith("top")) y = MARGIN;
|
|
31998
|
+
else if (position.startsWith("bottom")) y = args.paneHeight - MARGIN - totalH;
|
|
31999
|
+
else y = args.paneHeight / 2 - totalH / 2;
|
|
32000
|
+
let x;
|
|
32001
|
+
if (position.endsWith("left")) x = MARGIN;
|
|
32002
|
+
else if (position.endsWith("right")) x = args.plotWidth - MARGIN - totalW;
|
|
32003
|
+
else x = args.plotWidth / 2 - totalW / 2;
|
|
32004
|
+
return { x, y };
|
|
32005
|
+
}
|
|
32006
|
+
function cellFont(cell, px, theme) {
|
|
32007
|
+
const family = cell.fontFamily === "monospace" ? "monospace" : theme.fontFamily || "sans-serif";
|
|
32008
|
+
return `${cell.italic ? "italic " : ""}${cell.bold ? "bold " : ""}${px}px ${family}`;
|
|
32009
|
+
}
|
|
32010
|
+
|
|
32011
|
+
// src/renderers/native/drawings/IndicatorDrawingSlices.ts
|
|
32012
|
+
function indicatorSliceKey(z, boundaries) {
|
|
32013
|
+
return boundaries.find((b) => b > z) ?? Infinity;
|
|
32014
|
+
}
|
|
32015
|
+
var IndicatorDrawingSlices = class {
|
|
32016
|
+
constructor() {
|
|
32017
|
+
this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
|
|
32018
|
+
/** Slice canvas cache, keyed `paneId|beforeZ` — same lifecycle as the user-drawing cache. */
|
|
32019
|
+
this.sliceCache = /* @__PURE__ */ new Map();
|
|
32020
|
+
/** Tooltip hit-rects of every label drawn this frame, in plot coords (rebuilt per prepare). */
|
|
32021
|
+
this.tips = [];
|
|
32022
|
+
}
|
|
32023
|
+
/**
|
|
32024
|
+
* Rebuild the per-indicator drawing slices for this data frame. `ref` is the data
|
|
32025
|
+
* canvas the slices must match pixel-for-pixel (the backend composites them 1:1).
|
|
32026
|
+
* Runs from the renderer's data paint, just before the backend composites the scene.
|
|
32027
|
+
*/
|
|
32028
|
+
prepare(scene, coords, theme, ref) {
|
|
32029
|
+
this.tips = [];
|
|
32030
|
+
const out = /* @__PURE__ */ new Map();
|
|
32031
|
+
if (ref.width === 0 || ref.height === 0) {
|
|
32032
|
+
this.sliceCache.clear();
|
|
32033
|
+
return out;
|
|
32034
|
+
}
|
|
32035
|
+
this.drawScene.setDeps({
|
|
32036
|
+
timeToLogical: (ms) => coords.timeToLogical(ms),
|
|
32037
|
+
barAt: (logical) => {
|
|
32038
|
+
const b = scene.bars[Math.round(logical)];
|
|
32039
|
+
return b ? { high: b.high, low: b.low } : null;
|
|
32040
|
+
},
|
|
32041
|
+
theme
|
|
32042
|
+
});
|
|
32043
|
+
const dpr = coords.dpr;
|
|
32044
|
+
const dataW = coords.width;
|
|
32045
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
32046
|
+
const add = (paneId, beforeZ, entry) => {
|
|
32047
|
+
const key = `${paneId}|${beforeZ}`;
|
|
32048
|
+
const bucket = buckets.get(key);
|
|
32049
|
+
if (bucket) bucket.entries.push(entry);
|
|
32050
|
+
else buckets.set(key, { paneId, beforeZ, entries: [entry] });
|
|
32051
|
+
};
|
|
32052
|
+
for (const pane of scene.orderedPanes()) {
|
|
32053
|
+
if (pane.collapsed) continue;
|
|
32054
|
+
const boundaries = scene.seriesBoundaries(pane.id);
|
|
32055
|
+
for (const m of scene.orderedIndicatorsForPane(pane.id)) {
|
|
32056
|
+
const set = modelDrawingSet(m, false);
|
|
32057
|
+
const tables = (m.tables ?? []).filter((t) => !t.overlay);
|
|
32058
|
+
if (drawingSetEmpty(set) && tables.length === 0) continue;
|
|
32059
|
+
const sc = scene.scaleFor(m, pane);
|
|
32060
|
+
const mp = sc === pane.scale ? pane : { ...pane, scale: sc };
|
|
32061
|
+
const beforeZ = indicatorSliceKey(scene.zOf(m.id), boundaries);
|
|
32062
|
+
add(pane.id, beforeZ, { set, tables, pane: mp, indexOffset: scene.offsetOf(m.id) });
|
|
32063
|
+
}
|
|
32064
|
+
if (pane.kind === "price") {
|
|
32065
|
+
for (const m of scene.indicators.values()) {
|
|
32066
|
+
const set = modelDrawingSet(m, true);
|
|
32067
|
+
const tables = (m.tables ?? []).filter((t) => t.overlay === true);
|
|
32068
|
+
if (drawingSetEmpty(set) && tables.length === 0) continue;
|
|
32069
|
+
add(pane.id, Infinity, { set, tables, pane, indexOffset: scene.offsetOf(m.id) });
|
|
32070
|
+
}
|
|
32071
|
+
}
|
|
32072
|
+
}
|
|
32073
|
+
for (const [key, { paneId, beforeZ, entries }] of buckets) {
|
|
32074
|
+
let canvas = this.sliceCache.get(key);
|
|
32075
|
+
if (!canvas) {
|
|
32076
|
+
canvas = document.createElement("canvas");
|
|
32077
|
+
this.sliceCache.set(key, canvas);
|
|
32078
|
+
}
|
|
32079
|
+
if (canvas.width !== ref.width || canvas.height !== ref.height) {
|
|
32080
|
+
canvas.width = ref.width;
|
|
32081
|
+
canvas.height = ref.height;
|
|
32082
|
+
}
|
|
32083
|
+
const ctx = canvas.getContext("2d");
|
|
32084
|
+
if (!ctx) continue;
|
|
32085
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
32086
|
+
ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
|
|
32087
|
+
for (const e of entries) this.paintEntry(ctx, e, coords, dataW, theme);
|
|
32088
|
+
const slices = out.get(paneId) ?? [];
|
|
32089
|
+
slices.push({ beforeZ, canvas });
|
|
32090
|
+
out.set(paneId, slices);
|
|
32091
|
+
}
|
|
32092
|
+
for (const key of [...this.sliceCache.keys()]) if (!buckets.has(key)) this.sliceCache.delete(key);
|
|
32093
|
+
for (const slices of out.values()) slices.sort((a, b) => a.beforeZ - b.beforeZ);
|
|
32094
|
+
return out;
|
|
32095
|
+
}
|
|
32096
|
+
paintEntry(ctx, e, coords, dataW, theme) {
|
|
32097
|
+
const { pane } = e;
|
|
32098
|
+
const paneTips = [];
|
|
32099
|
+
ctx.save();
|
|
32100
|
+
ctx.translate(0, pane.bounds.top);
|
|
32101
|
+
ctx.beginPath();
|
|
32102
|
+
ctx.rect(0, 0, dataW, pane.bounds.height);
|
|
32103
|
+
ctx.clip();
|
|
32104
|
+
this.drawScene.setSet(e.set, e.indexOffset);
|
|
32105
|
+
this.drawScene.render(
|
|
32106
|
+
ctx,
|
|
32107
|
+
dataW,
|
|
32108
|
+
pane.bounds.height,
|
|
32109
|
+
(l) => coords.logicalToX(l),
|
|
32110
|
+
(price) => coords.priceToY(price, pane.scale, pane.bounds) - pane.bounds.top
|
|
32111
|
+
);
|
|
32112
|
+
paneTips.push(...this.drawScene.labelTipRegions());
|
|
32113
|
+
for (const t of e.tables) paintTable(ctx, t, { paneHeight: pane.bounds.height, plotWidth: dataW, theme }, paneTips);
|
|
32114
|
+
ctx.restore();
|
|
32115
|
+
for (const r of paneTips) {
|
|
32116
|
+
this.tips.push({ ...r, top: r.top + pane.bounds.top, bottom: r.bottom + pane.bounds.top });
|
|
32117
|
+
}
|
|
32118
|
+
}
|
|
32119
|
+
/** Tooltip of the topmost label or table cell under a plot-space point, or null. Fed by the last prepare. */
|
|
32120
|
+
labelTooltipAt(x, y) {
|
|
32121
|
+
for (let i = this.tips.length - 1; i >= 0; i -= 1) {
|
|
32122
|
+
const r = this.tips[i];
|
|
32123
|
+
if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return r.text;
|
|
32124
|
+
}
|
|
32125
|
+
return null;
|
|
32126
|
+
}
|
|
32127
|
+
};
|
|
32128
|
+
function mergeSlices(indicator, user) {
|
|
32129
|
+
const out = /* @__PURE__ */ new Map();
|
|
32130
|
+
for (const [paneId, slices] of indicator) out.set(paneId, [...slices]);
|
|
32131
|
+
for (const [paneId, slices] of user) out.set(paneId, [...out.get(paneId) ?? [], ...slices]);
|
|
32132
|
+
for (const slices of out.values()) slices.sort((a, b) => a.beforeZ - b.beforeZ);
|
|
32133
|
+
return out;
|
|
32134
|
+
}
|
|
32135
|
+
|
|
31767
32136
|
// src/renderers/native/drawings/Projector.ts
|
|
31768
32137
|
function createProjector(coords, paneOf, paneIdAtY, barsInRange) {
|
|
31769
32138
|
return {
|
|
@@ -32509,6 +32878,11 @@ var NativeRenderer = class {
|
|
|
32509
32878
|
this.vpvrRenderer = new VpvrRenderer();
|
|
32510
32879
|
this.resizeObserver = null;
|
|
32511
32880
|
this.dprMedia = null;
|
|
32881
|
+
/** Plot size in INTEGER device px, as last reported by the resize observer's
|
|
32882
|
+
* device-pixel-content-box — the browser's own statement of how many device pixels
|
|
32883
|
+
* it paints the plot into. `null` until the first report or where the box type is
|
|
32884
|
+
* unsupported (WebKit); syncSize then falls back to rounding the client rect. */
|
|
32885
|
+
this.plotDeviceSize = null;
|
|
32512
32886
|
this.coords = new CoordinateSystem();
|
|
32513
32887
|
this.scene = new SceneGraph();
|
|
32514
32888
|
// chosen at mount (WebGL2 if available, else canvas2d)
|
|
@@ -32516,6 +32890,8 @@ var NativeRenderer = class {
|
|
|
32516
32890
|
this.glowAmount = 0;
|
|
32517
32891
|
// WebGL2 neon-glow intensity (canvas2d ignores it)
|
|
32518
32892
|
this.chrome = new ChromeRenderer();
|
|
32893
|
+
/** Prepaints each indicator's Pine drawings into interleave slices at the model's z. */
|
|
32894
|
+
this.indicatorSlices = new IndicatorDrawingSlices();
|
|
32519
32895
|
/** Hover tooltips for Pine labels (canvas hit-rects collected by the chrome layer). */
|
|
32520
32896
|
this.labelTooltip = null;
|
|
32521
32897
|
this.crosshairLayer = new CrosshairRenderer();
|
|
@@ -32664,7 +33040,6 @@ var NativeRenderer = class {
|
|
|
32664
33040
|
this.toggleVisibleCbs = /* @__PURE__ */ new Set();
|
|
32665
33041
|
this.moveIndicatorCbs = /* @__PURE__ */ new Set();
|
|
32666
33042
|
this.priceStyleCbs = /* @__PURE__ */ new Set();
|
|
32667
|
-
this.tableOverlays = /* @__PURE__ */ new Map();
|
|
32668
33043
|
this.name = "native";
|
|
32669
33044
|
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
33045
|
/** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
|
|
@@ -32978,7 +33353,6 @@ var NativeRenderer = class {
|
|
|
32978
33353
|
* hidden) on clear so a re-show picks up the current theme.
|
|
32979
33354
|
*/
|
|
32980
33355
|
setLoading(loading) {
|
|
32981
|
-
for (const overlay of this.tableOverlays.values()) overlay.setVisible(!loading);
|
|
32982
33356
|
if (!loading || !this.wrapper) {
|
|
32983
33357
|
this.loadingEl?.remove();
|
|
32984
33358
|
this.loadingEl = null;
|
|
@@ -33640,7 +34014,7 @@ var NativeRenderer = class {
|
|
|
33640
34014
|
this.plot.addEventListener("pointerleave", this.onScrollProximityLeave);
|
|
33641
34015
|
this.labelTooltip = new LabelTooltip(this.plot, {
|
|
33642
34016
|
theme: () => this.chromeTheme(),
|
|
33643
|
-
lookup: (x, y) => this.
|
|
34017
|
+
lookup: (x, y) => this.indicatorSlices.labelTooltipAt(x, y)
|
|
33644
34018
|
});
|
|
33645
34019
|
this.userDrawings = new UserDrawingController(this.wrapper, this.plot, this.drawingsCanvas, {
|
|
33646
34020
|
projector: () => this.drawingProjector(),
|
|
@@ -33725,6 +34099,7 @@ var NativeRenderer = class {
|
|
|
33725
34099
|
this.emitPaneAction({ type: "maximize", paneId, maximized });
|
|
33726
34100
|
}
|
|
33727
34101
|
});
|
|
34102
|
+
this.paneControls.setSuspended(this.layoutMode === "mobile");
|
|
33728
34103
|
this.axisScaleButtons = new AxisScaleButtons(this.plot, theme, {
|
|
33729
34104
|
panes: () => this.axisScaleViews(),
|
|
33730
34105
|
rightAxis: () => this.rightAxisW,
|
|
@@ -33734,8 +34109,19 @@ var NativeRenderer = class {
|
|
|
33734
34109
|
if (pane) this.setPaneLog(paneId, !paneLogScale(this.scene, pane));
|
|
33735
34110
|
}
|
|
33736
34111
|
});
|
|
33737
|
-
this.resizeObserver = new ResizeObserver(() =>
|
|
34112
|
+
this.resizeObserver = new ResizeObserver((entries) => {
|
|
34113
|
+
for (const e of entries) {
|
|
34114
|
+
if (e.target !== this.plot) continue;
|
|
34115
|
+
const s = e.devicePixelContentBoxSize?.[0];
|
|
34116
|
+
if (s) this.plotDeviceSize = { width: s.inlineSize, height: s.blockSize };
|
|
34117
|
+
}
|
|
34118
|
+
this.resize();
|
|
34119
|
+
});
|
|
33738
34120
|
this.resizeObserver.observe(this.wrapper);
|
|
34121
|
+
try {
|
|
34122
|
+
this.resizeObserver.observe(this.plot, { box: "device-pixel-content-box" });
|
|
34123
|
+
} catch {
|
|
34124
|
+
}
|
|
33739
34125
|
this.watchDpr();
|
|
33740
34126
|
this.syncSize();
|
|
33741
34127
|
}
|
|
@@ -33860,8 +34246,6 @@ var NativeRenderer = class {
|
|
|
33860
34246
|
this.inputsUI?.destroy();
|
|
33861
34247
|
this.paneControls?.destroy();
|
|
33862
34248
|
this.axisScaleButtons?.destroy();
|
|
33863
|
-
for (const overlay of this.tableOverlays.values()) overlay.destroy();
|
|
33864
|
-
this.tableOverlays.clear();
|
|
33865
34249
|
this.resizeObserver?.disconnect();
|
|
33866
34250
|
this.resizeObserver = null;
|
|
33867
34251
|
this.dprMedia?.removeEventListener("change", this.onDprChange);
|
|
@@ -33890,6 +34274,7 @@ var NativeRenderer = class {
|
|
|
33890
34274
|
this.attributionEl = null;
|
|
33891
34275
|
this.mountContainer?.style.removeProperty("--vela-toolbar-gutter");
|
|
33892
34276
|
this.mountContainer?.style.removeProperty("--vela-scale-gutter");
|
|
34277
|
+
this.mountContainer?.style.removeProperty("--vela-bottom-gutter");
|
|
33893
34278
|
this.mountContainer?.style.removeProperty("--vela-price-pane-top");
|
|
33894
34279
|
this.mountContainer?.style.removeProperty("--vela-price-pane-bottom");
|
|
33895
34280
|
this.mountContainer = null;
|
|
@@ -33982,7 +34367,6 @@ var NativeRenderer = class {
|
|
|
33982
34367
|
ensurePane(pane) {
|
|
33983
34368
|
this.scene.ensurePane(pane.id, pane.kind, pane.order, pane.heightWeight ?? (pane.kind === "price" ? 3 : 1));
|
|
33984
34369
|
this.layoutPanes();
|
|
33985
|
-
this.repositionTables();
|
|
33986
34370
|
this.paneControls?.refresh();
|
|
33987
34371
|
this.scheduler.invalidate(4 /* Full */);
|
|
33988
34372
|
}
|
|
@@ -34004,17 +34388,14 @@ var NativeRenderer = class {
|
|
|
34004
34388
|
if (!model.ownScale) this.scene.dropIndicatorScale(handle.id);
|
|
34005
34389
|
this.inputsUI.setPane(handle.id, paneId);
|
|
34006
34390
|
this.refreshAnchorOffset(model);
|
|
34007
|
-
this.syncTables(model);
|
|
34008
34391
|
this.refreshAxisWidth();
|
|
34009
34392
|
this.layoutPanes();
|
|
34010
|
-
this.repositionTables();
|
|
34011
34393
|
this.paneControls?.refresh();
|
|
34012
34394
|
this.scheduler.invalidate(4 /* Full */);
|
|
34013
34395
|
}
|
|
34014
34396
|
orderPanes(orderedIds) {
|
|
34015
34397
|
this.scene.orderPanes(orderedIds);
|
|
34016
34398
|
this.layoutPanes();
|
|
34017
|
-
this.repositionTables();
|
|
34018
34399
|
this.paneControls?.refresh();
|
|
34019
34400
|
this.scheduler.invalidate(4 /* Full */);
|
|
34020
34401
|
}
|
|
@@ -34023,7 +34404,6 @@ var NativeRenderer = class {
|
|
|
34023
34404
|
if (!pane || pane.collapsed === collapsed) return;
|
|
34024
34405
|
pane.collapsed = collapsed;
|
|
34025
34406
|
this.layoutPanes();
|
|
34026
|
-
this.repositionTables();
|
|
34027
34407
|
this.paneControls?.refresh();
|
|
34028
34408
|
this.scheduler.invalidate(4 /* Full */);
|
|
34029
34409
|
}
|
|
@@ -34031,7 +34411,6 @@ var NativeRenderer = class {
|
|
|
34031
34411
|
if (paneId !== null && !this.scene.panes.has(paneId)) paneId = null;
|
|
34032
34412
|
this.maximizedPaneId = paneId;
|
|
34033
34413
|
this.layoutPanes();
|
|
34034
|
-
this.repositionTables();
|
|
34035
34414
|
this.paneControls?.refresh();
|
|
34036
34415
|
this.scheduler.invalidate(4 /* Full */);
|
|
34037
34416
|
}
|
|
@@ -34118,7 +34497,6 @@ var NativeRenderer = class {
|
|
|
34118
34497
|
native: !!model.native,
|
|
34119
34498
|
...model.props ? { props: model.props, propValues: model.propValues ?? {} } : {}
|
|
34120
34499
|
});
|
|
34121
|
-
this.syncTables(model);
|
|
34122
34500
|
if (model.native?.type === "volume") {
|
|
34123
34501
|
this.volumeActive = true;
|
|
34124
34502
|
this.volumeHidden = false;
|
|
@@ -34142,7 +34520,6 @@ var NativeRenderer = class {
|
|
|
34142
34520
|
}
|
|
34143
34521
|
}
|
|
34144
34522
|
applyPatch(model, patch);
|
|
34145
|
-
this.syncTables(model);
|
|
34146
34523
|
this.scheduler.invalidate(3 /* Light */);
|
|
34147
34524
|
}
|
|
34148
34525
|
removeIndicator(handle) {
|
|
@@ -34161,8 +34538,6 @@ var NativeRenderer = class {
|
|
|
34161
34538
|
this.scene.forgetAnchorOffset(handle.id);
|
|
34162
34539
|
this.scene.dropIndicatorScale(handle.id);
|
|
34163
34540
|
this.inputsUI.remove(handle.id);
|
|
34164
|
-
this.tableOverlays.get(handle.id)?.destroy();
|
|
34165
|
-
this.tableOverlays.delete(handle.id);
|
|
34166
34541
|
this.refreshAxisWidth();
|
|
34167
34542
|
this.paneControls?.refresh();
|
|
34168
34543
|
this.scheduler.invalidate(4 /* Full */);
|
|
@@ -34206,8 +34581,6 @@ var NativeRenderer = class {
|
|
|
34206
34581
|
}
|
|
34207
34582
|
if (!visible) {
|
|
34208
34583
|
this.scene.indicators.delete(handle.id);
|
|
34209
|
-
this.tableOverlays.get(handle.id)?.destroy();
|
|
34210
|
-
this.tableOverlays.delete(handle.id);
|
|
34211
34584
|
}
|
|
34212
34585
|
this.inputsUI.setVisible(handle.id, visible);
|
|
34213
34586
|
this.scheduler.invalidate(4 /* Full */);
|
|
@@ -34253,6 +34626,7 @@ var NativeRenderer = class {
|
|
|
34253
34626
|
this.userDrawings?.setLayoutMode(mode);
|
|
34254
34627
|
this.settingsDialog?.setLayoutMode(mode);
|
|
34255
34628
|
this.inputsUI?.setLayoutMode(mode);
|
|
34629
|
+
this.paneControls?.setSuspended(mode === "mobile");
|
|
34256
34630
|
if (this.scrollButton) {
|
|
34257
34631
|
const px = mode === "mobile" ? SCROLL_BTN_SIZE_TOUCH : SCROLL_BTN_SIZE;
|
|
34258
34632
|
this.scrollButton.style.width = `${px}px`;
|
|
@@ -34648,7 +35022,6 @@ var NativeRenderer = class {
|
|
|
34648
35022
|
/** Relayout + repaint + refresh the hover buttons after a collapse/maximize/order change. */
|
|
34649
35023
|
afterPaneLayoutChange() {
|
|
34650
35024
|
this.layoutPanes();
|
|
34651
|
-
this.repositionTables();
|
|
34652
35025
|
this.paneControls?.refresh();
|
|
34653
35026
|
this.scheduler.invalidate(4 /* Full */);
|
|
34654
35027
|
}
|
|
@@ -34729,7 +35102,6 @@ var NativeRenderer = class {
|
|
|
34729
35102
|
above.heightWeight = next.above;
|
|
34730
35103
|
below.heightWeight = next.below;
|
|
34731
35104
|
this.layoutPanes();
|
|
34732
|
-
this.repositionTables();
|
|
34733
35105
|
this.scheduler.invalidate(4 /* Full */);
|
|
34734
35106
|
}
|
|
34735
35107
|
/** Double-click a separator → split the two adjacent panes evenly (each gets half of
|
|
@@ -34744,7 +35116,6 @@ var NativeRenderer = class {
|
|
|
34744
35116
|
above.heightWeight = half;
|
|
34745
35117
|
below.heightWeight = half;
|
|
34746
35118
|
this.layoutPanes();
|
|
34747
|
-
this.repositionTables();
|
|
34748
35119
|
this.scheduler.invalidate(4 /* Full */);
|
|
34749
35120
|
}
|
|
34750
35121
|
// ── keyboard navigation / accessibility (item 11) ──
|
|
@@ -35023,7 +35394,10 @@ var NativeRenderer = class {
|
|
|
35023
35394
|
const liveActual = li >= 0 ? this.bars[li] : void 0;
|
|
35024
35395
|
const easeLive = !!liveActual && this.liveEaseTime === liveActual.time && (liveActual.high !== this.liveEaseHigh || liveActual.low !== this.liveEaseLow || liveActual.close !== this.liveEaseClose);
|
|
35025
35396
|
if (easeLive && liveActual) this.bars[li] = { ...liveActual, high: this.liveEaseHigh, low: this.liveEaseLow, close: this.liveEaseClose };
|
|
35026
|
-
this.scene.drawingSlices =
|
|
35397
|
+
this.scene.drawingSlices = mergeSlices(
|
|
35398
|
+
this.indicatorSlices.prepare(this.scene, this.coords, this.theme, this.dataCanvas),
|
|
35399
|
+
this.userDrawings?.prepareSlices(this.scene.orderedPanes().map((p) => p.id)) ?? /* @__PURE__ */ new Map()
|
|
35400
|
+
);
|
|
35027
35401
|
this.backdropRenderer.render(this.scene, this.coords, this.theme, gridAlpha);
|
|
35028
35402
|
this.backend.render(this.scene, this.coords, this.theme);
|
|
35029
35403
|
this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
|
|
@@ -35461,27 +35835,6 @@ var NativeRenderer = class {
|
|
|
35461
35835
|
}
|
|
35462
35836
|
return maxVol;
|
|
35463
35837
|
}
|
|
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
35838
|
layoutPanes() {
|
|
35486
35839
|
const panes = this.scene.orderedPanes();
|
|
35487
35840
|
const dataHeight = this.coords.height;
|
|
@@ -35532,6 +35885,7 @@ var NativeRenderer = class {
|
|
|
35532
35885
|
const visible = maxPane ? [maxPane] : this.scene.orderedPanes().filter((p) => !p.collapsed);
|
|
35533
35886
|
const paneBottom = visible.length ? Math.max(...visible.map((p) => p.bounds.top + p.bounds.height)) : dataHeight;
|
|
35534
35887
|
this.scrollBtnBottomPx = SCROLL_BTN_BOTTOM + Math.max(0, dataHeight - paneBottom);
|
|
35888
|
+
this.mountContainer?.style.setProperty("--vela-bottom-gutter", `${TIME_AXIS_H + Math.max(0, dataHeight - paneBottom)}px`);
|
|
35535
35889
|
this.scrollBtnRightPx = this.rightAxisW + SCROLL_BTN_RIGHT_INSET;
|
|
35536
35890
|
if (this.scrollButton) {
|
|
35537
35891
|
this.scrollButton.style.bottom = `${this.scrollBtnBottomPx}px`;
|
|
@@ -35624,30 +35978,33 @@ var NativeRenderer = class {
|
|
|
35624
35978
|
if (w <= 0 || h <= 0) return;
|
|
35625
35979
|
const dpr = window.devicePixelRatio || 1;
|
|
35626
35980
|
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
|
-
|
|
35981
|
+
const rect = this.plot.getBoundingClientRect();
|
|
35982
|
+
let bw = Math.max(1, Math.round(rect.width * dpr));
|
|
35983
|
+
let bh = Math.max(1, Math.round(rect.height * dpr));
|
|
35984
|
+
const dev = this.plotDeviceSize;
|
|
35985
|
+
if (dev && Math.abs(dev.width - rect.width * dpr) <= 1 && Math.abs(dev.height - rect.height * dpr) <= 1) {
|
|
35986
|
+
bw = Math.max(1, dev.width);
|
|
35987
|
+
bh = Math.max(1, dev.height);
|
|
35988
|
+
}
|
|
35989
|
+
const pw = bw / dpr;
|
|
35990
|
+
const ph = bh / dpr;
|
|
35991
|
+
const size = (canvas) => {
|
|
35992
|
+
canvas.width = bw;
|
|
35993
|
+
canvas.height = bh;
|
|
35994
|
+
canvas.style.width = `${pw}px`;
|
|
35995
|
+
canvas.style.height = `${ph}px`;
|
|
35996
|
+
};
|
|
35997
|
+
size(this.dataCanvas);
|
|
35998
|
+
size(this.backdropCanvas);
|
|
35999
|
+
size(this.volumeCanvas);
|
|
36000
|
+
for (const l of this.extLayers) size(l.canvas);
|
|
36001
|
+
size(this.vpvrCanvas);
|
|
36002
|
+
size(this.chromeCanvas);
|
|
36003
|
+
size(this.drawingsCanvas);
|
|
36004
|
+
size(this.cursorCanvas);
|
|
35647
36005
|
this.coords.setSize(Math.max(1, pw - this.rightAxisW), Math.max(1, ph - TIME_AXIS_H), dpr);
|
|
35648
36006
|
this.scene.crosshair = null;
|
|
35649
36007
|
this.layoutPanes();
|
|
35650
|
-
this.repositionTables();
|
|
35651
36008
|
this.userDrawings?.onResize();
|
|
35652
36009
|
if (!this.didInitialFit && this.coords.barCount > 0) {
|
|
35653
36010
|
this.fitContent();
|
|
@@ -36966,9 +37323,168 @@ var Watermark = class {
|
|
|
36966
37323
|
}
|
|
36967
37324
|
};
|
|
36968
37325
|
|
|
37326
|
+
// src/widget/cell-controls.ts
|
|
37327
|
+
var CELL_CONTROLS_PROXIMITY_PX = 120;
|
|
37328
|
+
var TIME_AXIS_H2 = 22;
|
|
37329
|
+
var CONTROLS_BOTTOM_PX = TIME_AXIS_H2 + 12;
|
|
37330
|
+
var CLUSTER_H2 = 24;
|
|
37331
|
+
var CLUSTER_PILL2 = "rgba(0,0,0,0.65)";
|
|
37332
|
+
var STYLE_ID26 = "vela-cell-controls";
|
|
37333
|
+
var CSS23 = `
|
|
37334
|
+
.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;}
|
|
37335
|
+
.vela-cc-btn svg{display:block;}
|
|
37336
|
+
.vela-cc-btn:hover{background:var(--vela-active);color:var(--vela-fg-bright);}
|
|
37337
|
+
.vela-cc-on,.vela-cc-on:hover{background:var(--vela-selected-bg);color:var(--vela-selected-fg);}
|
|
37338
|
+
.vela-cc-grip{cursor:grab;touch-action:none;}
|
|
37339
|
+
.vela-cc-grip:active{cursor:grabbing;}
|
|
37340
|
+
`;
|
|
37341
|
+
function nearBottomCenter(x, y, width, height, proximityPx = CELL_CONTROLS_PROXIMITY_PX) {
|
|
37342
|
+
const cx = width / 2;
|
|
37343
|
+
const cy = height - CONTROLS_BOTTOM_PX - CLUSTER_H2 / 2;
|
|
37344
|
+
return Math.hypot(x - cx, y - cy) <= proximityPx;
|
|
37345
|
+
}
|
|
37346
|
+
var CellControls = class {
|
|
37347
|
+
constructor(host, deps) {
|
|
37348
|
+
this.host = host;
|
|
37349
|
+
this.deps = deps;
|
|
37350
|
+
this.near = false;
|
|
37351
|
+
/** A grip drag is underway — the proximity reveal must not hide the cluster
|
|
37352
|
+
* while captured pointer moves sweep across the whole grid. */
|
|
37353
|
+
this.dragging = false;
|
|
37354
|
+
/** Mobile: the proximity reveal is meaningless without a cursor — the mobile
|
|
37355
|
+
* bar's maximize stop replaces the cluster. */
|
|
37356
|
+
this.suspended = false;
|
|
37357
|
+
this.onHostMove = (e) => {
|
|
37358
|
+
if (this.suspended) return;
|
|
37359
|
+
if (this.dragging) return;
|
|
37360
|
+
const rect = this.host.getBoundingClientRect();
|
|
37361
|
+
this.setNear(nearBottomCenter(e.clientX - rect.left, e.clientY - rect.top, rect.width, rect.height));
|
|
37362
|
+
};
|
|
37363
|
+
this.onHostLeave = () => {
|
|
37364
|
+
if (this.dragging) return;
|
|
37365
|
+
this.setNear(false);
|
|
37366
|
+
};
|
|
37367
|
+
injectStyles(STYLE_ID26, CSS23, host.ownerDocument);
|
|
37368
|
+
this.glider = new Glider(deps.chart);
|
|
37369
|
+
this.root = host.ownerDocument.createElement("div");
|
|
37370
|
+
Object.assign(this.root.style, {
|
|
37371
|
+
position: "absolute",
|
|
37372
|
+
left: "50%",
|
|
37373
|
+
bottom: `${CONTROLS_BOTTOM_PX}px`,
|
|
37374
|
+
transform: "translateX(-50%)",
|
|
37375
|
+
zIndex: "6",
|
|
37376
|
+
display: "none",
|
|
37377
|
+
// revealed by cursor proximity (onHostMove)
|
|
37378
|
+
gap: "2px",
|
|
37379
|
+
padding: "2px",
|
|
37380
|
+
borderRadius: "var(--vela-radius-md)",
|
|
37381
|
+
background: CLUSTER_PILL2,
|
|
37382
|
+
pointerEvents: "auto"
|
|
37383
|
+
});
|
|
37384
|
+
this.host.addEventListener("pointermove", this.onHostMove);
|
|
37385
|
+
this.host.addEventListener("pointerleave", this.onHostLeave);
|
|
37386
|
+
this.host.appendChild(this.root);
|
|
37387
|
+
this.refresh();
|
|
37388
|
+
}
|
|
37389
|
+
/** Rebuild the buttons (the multi-cell gate or the maximized state changed). */
|
|
37390
|
+
refresh() {
|
|
37391
|
+
this.root.textContent = "";
|
|
37392
|
+
const multi = this.deps.multiCell();
|
|
37393
|
+
const maximized = multi && this.deps.isMaximized();
|
|
37394
|
+
if (multi && !maximized) this.root.appendChild(this.makeGrip());
|
|
37395
|
+
this.root.appendChild(this.button("minus", "Zoom out", () => this.glider.zoom(ZOOM_OUT)));
|
|
37396
|
+
this.root.appendChild(this.button("plus", "Zoom in", () => this.glider.zoom(ZOOM_IN)));
|
|
37397
|
+
if (multi) {
|
|
37398
|
+
this.root.appendChild(
|
|
37399
|
+
this.button(maximized ? "restore" : "maximize", maximized ? "Restore layout" : "Maximize chart", () => this.deps.toggleMaximize(), {
|
|
37400
|
+
// The maximized state reads as an inverse chip (white-on-dark, dark-on-light),
|
|
37401
|
+
// the same active-state affordance as a collapsed pane's expand button.
|
|
37402
|
+
selected: maximized
|
|
37403
|
+
})
|
|
37404
|
+
);
|
|
37405
|
+
}
|
|
37406
|
+
this.root.appendChild(
|
|
37407
|
+
this.button("reset", "Reset chart", () => {
|
|
37408
|
+
this.glider.stop();
|
|
37409
|
+
this.deps.reset();
|
|
37410
|
+
})
|
|
37411
|
+
);
|
|
37412
|
+
}
|
|
37413
|
+
button(iconId, title, onClick, opts = {}) {
|
|
37414
|
+
const b = this.host.ownerDocument.createElement("button");
|
|
37415
|
+
b.type = "button";
|
|
37416
|
+
b.title = title;
|
|
37417
|
+
b.setAttribute("aria-label", title);
|
|
37418
|
+
b.className = opts.selected === true ? "vela-cc-btn vela-cc-on" : "vela-cc-btn";
|
|
37419
|
+
b.innerHTML = icon(iconId);
|
|
37420
|
+
b.addEventListener("click", (e) => {
|
|
37421
|
+
e.stopPropagation();
|
|
37422
|
+
onClick();
|
|
37423
|
+
});
|
|
37424
|
+
return b;
|
|
37425
|
+
}
|
|
37426
|
+
/** The drag handle (2×3 dot grip): press and drag onto another cell to trade
|
|
37427
|
+
* slots with it. The preview highlight follows the pointer; releasing outside
|
|
37428
|
+
* any other cell cancels. */
|
|
37429
|
+
makeGrip() {
|
|
37430
|
+
const b = this.host.ownerDocument.createElement("button");
|
|
37431
|
+
b.type = "button";
|
|
37432
|
+
b.title = "Drag to move chart";
|
|
37433
|
+
b.setAttribute("aria-label", "Drag to move chart");
|
|
37434
|
+
b.className = "vela-cc-btn vela-cc-grip";
|
|
37435
|
+
b.innerHTML = icon("grip");
|
|
37436
|
+
b.addEventListener("pointerdown", (e) => this.onGripDown(b, e));
|
|
37437
|
+
return b;
|
|
37438
|
+
}
|
|
37439
|
+
onGripDown(btn2, e) {
|
|
37440
|
+
if (e.button !== 0 && e.pointerType === "mouse") return;
|
|
37441
|
+
e.preventDefault();
|
|
37442
|
+
e.stopPropagation();
|
|
37443
|
+
try {
|
|
37444
|
+
btn2.setPointerCapture(e.pointerId);
|
|
37445
|
+
} catch {
|
|
37446
|
+
}
|
|
37447
|
+
this.dragging = true;
|
|
37448
|
+
let target = null;
|
|
37449
|
+
const move = (ev) => {
|
|
37450
|
+
target = this.deps.dragTargetAt(ev.clientX, ev.clientY);
|
|
37451
|
+
this.deps.previewDrop(target);
|
|
37452
|
+
};
|
|
37453
|
+
const finish = (commit) => () => {
|
|
37454
|
+
this.dragging = false;
|
|
37455
|
+
this.deps.previewDrop(null);
|
|
37456
|
+
btn2.removeEventListener("pointermove", move);
|
|
37457
|
+
btn2.removeEventListener("pointerup", onUp);
|
|
37458
|
+
btn2.removeEventListener("pointercancel", onCancel);
|
|
37459
|
+
if (commit && target != null) this.deps.dropOn(target);
|
|
37460
|
+
};
|
|
37461
|
+
const onUp = finish(true);
|
|
37462
|
+
const onCancel = finish(false);
|
|
37463
|
+
btn2.addEventListener("pointermove", move);
|
|
37464
|
+
btn2.addEventListener("pointerup", onUp);
|
|
37465
|
+
btn2.addEventListener("pointercancel", onCancel);
|
|
37466
|
+
}
|
|
37467
|
+
/** Mobile flips the cluster off entirely (and hides it if currently revealed). */
|
|
37468
|
+
setSuspended(on) {
|
|
37469
|
+
this.suspended = on;
|
|
37470
|
+
if (on) this.setNear(false);
|
|
37471
|
+
}
|
|
37472
|
+
setNear(near) {
|
|
37473
|
+
if (near === this.near) return;
|
|
37474
|
+
this.near = near;
|
|
37475
|
+
this.root.style.display = near ? "flex" : "none";
|
|
37476
|
+
}
|
|
37477
|
+
destroy() {
|
|
37478
|
+
this.glider.stop();
|
|
37479
|
+
this.host.removeEventListener("pointermove", this.onHostMove);
|
|
37480
|
+
this.host.removeEventListener("pointerleave", this.onHostLeave);
|
|
37481
|
+
this.root.remove();
|
|
37482
|
+
}
|
|
37483
|
+
};
|
|
37484
|
+
|
|
36969
37485
|
// src/widget/context-menu.ts
|
|
36970
37486
|
var PRICE_AXIS_W = 60;
|
|
36971
|
-
var
|
|
37487
|
+
var TIME_AXIS_H3 = 26;
|
|
36972
37488
|
var ChartContextMenu = class {
|
|
36973
37489
|
constructor(host, cbs) {
|
|
36974
37490
|
this.cbs = cbs;
|
|
@@ -37007,7 +37523,7 @@ var ChartContextMenu = class {
|
|
|
37007
37523
|
zoneOf(e) {
|
|
37008
37524
|
const rect = this.host.getBoundingClientRect();
|
|
37009
37525
|
if (e.clientX - rect.left > rect.width - PRICE_AXIS_W) return "price-axis";
|
|
37010
|
-
if (e.clientY - rect.top > rect.height -
|
|
37526
|
+
if (e.clientY - rect.top > rect.height - TIME_AXIS_H3) return "time-axis";
|
|
37011
37527
|
return "body";
|
|
37012
37528
|
}
|
|
37013
37529
|
/** The pane under the pointer, so every pane's price scale has its own menu. */
|
|
@@ -37341,11 +37857,18 @@ var ChartCell = class {
|
|
|
37341
37857
|
if (this.inner && this.state.symbol) this.marketStatus?.track(this.inner.data, this.state.symbol);
|
|
37342
37858
|
});
|
|
37343
37859
|
this.syncStatuslineColors();
|
|
37860
|
+
this.cellControls = new CellControls(this.host, {
|
|
37861
|
+
chart: () => this.inner,
|
|
37862
|
+
reset: () => this.resetView(),
|
|
37863
|
+
multiCell: () => deps.multiCell(),
|
|
37864
|
+
isMaximized: () => deps.isMaximized(id),
|
|
37865
|
+
toggleMaximize: () => deps.toggleMaximize(id),
|
|
37866
|
+
dragTargetAt: (x, y) => deps.cellDragTarget(id, x, y),
|
|
37867
|
+
previewDrop: (target) => deps.previewDropTarget(target),
|
|
37868
|
+
dropOn: (target) => deps.dropCell(id, target)
|
|
37869
|
+
});
|
|
37344
37870
|
this.contextMenu = new ChartContextMenu(this.host, {
|
|
37345
|
-
resetView: () =>
|
|
37346
|
-
this.inner?.renderer.set("autoScale", true);
|
|
37347
|
-
this.inner?.setVisibleRangePreset("ALL");
|
|
37348
|
-
},
|
|
37871
|
+
resetView: () => this.resetView(),
|
|
37349
37872
|
timezone: () => this.deps.timezone(),
|
|
37350
37873
|
setTimezone: (zone) => this.deps.setTimezone(zone),
|
|
37351
37874
|
// Right-clicking activates the cell first (capture-phase pointerdown), so the
|
|
@@ -37719,6 +38242,20 @@ var ChartCell = class {
|
|
|
37719
38242
|
this.inner.setVisibleRangePreset(preset.preset);
|
|
37720
38243
|
}
|
|
37721
38244
|
}
|
|
38245
|
+
/** Reset this cell's view: re-enable auto scale and frame the full history —
|
|
38246
|
+
* the same action the chart context menu offers. */
|
|
38247
|
+
resetView() {
|
|
38248
|
+
this.inner?.renderer.set("autoScale", true);
|
|
38249
|
+
this.inner?.setVisibleRangePreset("ALL");
|
|
38250
|
+
}
|
|
38251
|
+
/** Rebuild the view-controls cluster (the maximize gate or state changed). */
|
|
38252
|
+
refreshControls() {
|
|
38253
|
+
this.cellControls.refresh();
|
|
38254
|
+
}
|
|
38255
|
+
/** Mobile flips the per-cell cluster off (the shell's mobile bar replaces it). */
|
|
38256
|
+
setControlsSuspended(on) {
|
|
38257
|
+
this.cellControls.setSuspended(on);
|
|
38258
|
+
}
|
|
37722
38259
|
/** Make this cell the active one and put keyboard focus on its chart surface. */
|
|
37723
38260
|
focus() {
|
|
37724
38261
|
this.deps.activate(this.id);
|
|
@@ -38032,6 +38569,7 @@ var ChartCell = class {
|
|
|
38032
38569
|
destroy() {
|
|
38033
38570
|
this.destroyed = true;
|
|
38034
38571
|
this.offMarket();
|
|
38572
|
+
this.cellControls.destroy();
|
|
38035
38573
|
this.contextMenu.destroy();
|
|
38036
38574
|
this.history.destroy();
|
|
38037
38575
|
this.marketStatus?.stop();
|
|
@@ -38330,10 +38868,10 @@ var SplitterLayer = class {
|
|
|
38330
38868
|
var DEFAULT_TIMEFRAMES = ["1", "5", "15", "60", "240", "D", "W"];
|
|
38331
38869
|
var GAP_PX = 2;
|
|
38332
38870
|
var POOL_CAP = 16;
|
|
38333
|
-
var
|
|
38871
|
+
var TIME_AXIS_H4 = 22;
|
|
38334
38872
|
var ALERT_CAP = 50;
|
|
38335
|
-
var
|
|
38336
|
-
var
|
|
38873
|
+
var STYLE_ID27 = "vela-workspace";
|
|
38874
|
+
var CSS24 = `
|
|
38337
38875
|
.vela-workspace { position: relative; width: 100%; height: 100%; display: flex; flex-direction: column; background: var(--vela-bg); }
|
|
38338
38876
|
.vela-ws-main { position: relative; display: flex; flex-direction: row; flex: 1 1 auto; min-height: 0; }
|
|
38339
38877
|
.vela-ws-toolbar { position: relative; flex: none; }
|
|
@@ -38360,6 +38898,21 @@ var CSS23 = `
|
|
|
38360
38898
|
/* Mobile: the docked drawing-toolbar column would eat a phone-width grid \u2014 the shell's
|
|
38361
38899
|
drawings drawer + on-chart pill replace it (same policy as the widget's in-chart bar). */
|
|
38362
38900
|
[data-layout='mobile'] .vela-ws-toolbar { display: none; }
|
|
38901
|
+
/* A maximized cell owns the whole grid: the splitter strips have no seams to grab and
|
|
38902
|
+
the active ring would just outline the only visible chart \u2014 both are noise here. */
|
|
38903
|
+
.vela-ws-grid[data-maximized='1'] .vela-ws-splitter { display: none; }
|
|
38904
|
+
.vela-ws-grid[data-maximized='1'] .vela-cell[data-active='1']::after { display: none; }
|
|
38905
|
+
/* Drop-target preview while a cell's drag handle is held: a dashed ring + the same
|
|
38906
|
+
soft wash the splitter hover uses, over the chart, inert to the pointer. */
|
|
38907
|
+
.vela-cell[data-drop-target='1']::before {
|
|
38908
|
+
content: '';
|
|
38909
|
+
position: absolute;
|
|
38910
|
+
inset: 0;
|
|
38911
|
+
border: 2px dashed var(--vela-fg-bright);
|
|
38912
|
+
background: var(--vela-separator-hover-band);
|
|
38913
|
+
pointer-events: none;
|
|
38914
|
+
z-index: 11;
|
|
38915
|
+
}
|
|
38363
38916
|
`;
|
|
38364
38917
|
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
38918
|
function declaredOrder(cells) {
|
|
@@ -38389,6 +38942,9 @@ var VelaWorkspace = class {
|
|
|
38389
38942
|
* slots beyond the list get auto identities. Grows, never reorders. */
|
|
38390
38943
|
this.order = [];
|
|
38391
38944
|
this.activeId = null;
|
|
38945
|
+
/** The cell maximized over the whole grid (null = normal grid). TRANSIENT view
|
|
38946
|
+
* state — never persisted; any structural change (layout, applyState) restores. */
|
|
38947
|
+
this.maximizedId = null;
|
|
38392
38948
|
this.cellBackend = "auto";
|
|
38393
38949
|
this.destroyed = false;
|
|
38394
38950
|
this.shortcutsHelp = null;
|
|
@@ -38493,7 +39049,7 @@ var VelaWorkspace = class {
|
|
|
38493
39049
|
this.order = boot?.charts ? boot.charts.map((c) => c.id) : declaredOrder(opts.cells);
|
|
38494
39050
|
const bootActive = boot?.activeCellId ?? null;
|
|
38495
39051
|
const doc = hostEl.ownerDocument;
|
|
38496
|
-
injectStyles(
|
|
39052
|
+
injectStyles(STYLE_ID27, CSS24, doc);
|
|
38497
39053
|
this.root = doc.createElement("div");
|
|
38498
39054
|
this.root.className = "vela-workspace";
|
|
38499
39055
|
ensureUIHost(this.root, resolveTheme(opts.theme));
|
|
@@ -38603,7 +39159,11 @@ var VelaWorkspace = class {
|
|
|
38603
39159
|
if (attribution !== false) {
|
|
38604
39160
|
const background = resolveTheme(opts.theme).background;
|
|
38605
39161
|
const mark = typeof attribution === "string" && attribution.trim() ? createCustomMark(doc, attribution, background) : createAttributionMark(doc, background);
|
|
38606
|
-
Object.assign(mark.style, {
|
|
39162
|
+
Object.assign(mark.style, {
|
|
39163
|
+
left: "calc(var(--vela-toolbar-gutter, 0px) + 12px)",
|
|
39164
|
+
bottom: `calc(var(--vela-bottom-gutter, ${TIME_AXIS_H4}px) + 10px)`,
|
|
39165
|
+
zIndex: "11"
|
|
39166
|
+
});
|
|
38607
39167
|
this.gridEl.appendChild(mark);
|
|
38608
39168
|
this.attributionMark = mark;
|
|
38609
39169
|
}
|
|
@@ -38669,6 +39229,9 @@ var VelaWorkspace = class {
|
|
|
38669
39229
|
...topbarHas(this.topbarComp, "indicators") && (picker || this.indicatorsOverride) ? { onIndicatorsClick: this.indicatorsOverride ? () => this.runOverride(this.indicatorsOverride) : () => picker.open() } : {},
|
|
38670
39230
|
getContext: () => this.context(),
|
|
38671
39231
|
...this.drawingsEnabled ? { onDrawingsClick: () => this.openDrawingsDrawer() } : {},
|
|
39232
|
+
// Multi-chart only: the stop that isolates the ACTIVE chart (the
|
|
39233
|
+
// per-cell hover cluster has no cursor to reveal it on mobile).
|
|
39234
|
+
...this.monoLayout ? {} : { onMaximizeClick: () => this.toggleMobileMaximize() },
|
|
38672
39235
|
onMoreClick: () => this.openMoreDrawer(),
|
|
38673
39236
|
onSettingsClick: () => this.active.chart.renderer.openSettings()
|
|
38674
39237
|
}) : null;
|
|
@@ -38875,7 +39438,9 @@ var VelaWorkspace = class {
|
|
|
38875
39438
|
this.pool.clear();
|
|
38876
39439
|
for (const { id, ...cs } of st.charts.slice(liveCount)) this.pool.set(id, cs);
|
|
38877
39440
|
this.order = st.charts.map((c) => c.id);
|
|
39441
|
+
this.clearMaximized();
|
|
38878
39442
|
this.applyGrid();
|
|
39443
|
+
this.refreshCellControls();
|
|
38879
39444
|
const nextActive2 = st.activeCellId && this.cellsById.has(st.activeCellId) ? st.activeCellId : this.order[0] ?? null;
|
|
38880
39445
|
if (nextActive2 === this.activeId) this.projectActiveCell();
|
|
38881
39446
|
else this.setActiveCell(nextActive2);
|
|
@@ -38901,6 +39466,7 @@ var VelaWorkspace = class {
|
|
|
38901
39466
|
const def = this.monoLayout ? null : ensureLayout(st.layout);
|
|
38902
39467
|
if (def) this.def = def;
|
|
38903
39468
|
this.cellBackend = this.backendFor(this.def);
|
|
39469
|
+
this.clearMaximized();
|
|
38904
39470
|
this.applyGrid();
|
|
38905
39471
|
this.buildCells();
|
|
38906
39472
|
this.syncCellPresentation();
|
|
@@ -38964,6 +39530,7 @@ var VelaWorkspace = class {
|
|
|
38964
39530
|
setLayout(layout) {
|
|
38965
39531
|
if (this.destroyed) return;
|
|
38966
39532
|
if (this.monoLayout) return;
|
|
39533
|
+
this.clearMaximized();
|
|
38967
39534
|
const next = this.resolveLayout(layout);
|
|
38968
39535
|
const nextBackend = this.backendFor(next);
|
|
38969
39536
|
const rebuildAll = nextBackend !== this.cellBackend;
|
|
@@ -38984,6 +39551,7 @@ var VelaWorkspace = class {
|
|
|
38984
39551
|
this.buildCells();
|
|
38985
39552
|
this.alignNewCellStyles(preexisting);
|
|
38986
39553
|
this.syncCellPresentation();
|
|
39554
|
+
this.refreshCellControls();
|
|
38987
39555
|
this.topbar.setLayout(next.id);
|
|
38988
39556
|
const nextActive = activeAfterLayout(this.activeId, this.order.slice(0, next.cells.length));
|
|
38989
39557
|
if (nextActive === this.activeId) this.projectActiveCell();
|
|
@@ -38992,6 +39560,67 @@ var VelaWorkspace = class {
|
|
|
38992
39560
|
this.events.emit("layout:changed", { layout: next.id });
|
|
38993
39561
|
this.markStateDirty();
|
|
38994
39562
|
}
|
|
39563
|
+
/** The identity of the cell maximized over the whole grid, or null. */
|
|
39564
|
+
get maximizedCell() {
|
|
39565
|
+
return this.maximizedId;
|
|
39566
|
+
}
|
|
39567
|
+
/**
|
|
39568
|
+
* Maximize one cell over the whole grid, or restore the layout with `null`. Pure
|
|
39569
|
+
* presentation: the other cells stay alive underneath — charts, subscriptions and
|
|
39570
|
+
* state untouched — so restoring is instant. The maximized cell becomes the active
|
|
39571
|
+
* one. Transient view state (also reachable from each cell's bottom-center view
|
|
39572
|
+
* cluster): switching layouts or applying a state document restores the grid.
|
|
39573
|
+
*/
|
|
39574
|
+
maximizeCell(id) {
|
|
39575
|
+
if (this.destroyed) return;
|
|
39576
|
+
if (id != null && (!this.cellsById.has(id) || this.def.cells.length <= 1)) return;
|
|
39577
|
+
if (id === this.maximizedId) return;
|
|
39578
|
+
this.maximizedId = id;
|
|
39579
|
+
if (id) this.setActiveCell(id);
|
|
39580
|
+
this.applyGrid();
|
|
39581
|
+
this.refreshCellControls();
|
|
39582
|
+
this.syncMobileMaximize();
|
|
39583
|
+
this.events.emit("cell:maximized", { id });
|
|
39584
|
+
}
|
|
39585
|
+
/** The mobile bar's maximize stop: one press isolates the ACTIVE chart over the
|
|
39586
|
+
* grid; while something is already isolated — the chart, or a pane inside it
|
|
39587
|
+
* (mobile's double-tap) — the press restores that instead. Every branch re-syncs
|
|
39588
|
+
* the stop on its own (`maximizeCell` directly, `panes.maximize` via its
|
|
39589
|
+
* synchronous `pane:changed`). */
|
|
39590
|
+
toggleMobileMaximize() {
|
|
39591
|
+
const cell = this.activeId ? this.cellsById.get(this.activeId) : void 0;
|
|
39592
|
+
if (!cell) return;
|
|
39593
|
+
if (this.maximizedId) this.maximizeCell(null);
|
|
39594
|
+
else if (cell.chart.panes.list().some((p) => p.maximized)) cell.chart.panes.maximize(null);
|
|
39595
|
+
else this.maximizeCell(cell.id);
|
|
39596
|
+
}
|
|
39597
|
+
/** Keep the mobile bar's maximize stop truthful: lit (inverse chip, restore
|
|
39598
|
+
* glyph) while the active chart covers the grid OR one of its panes is
|
|
39599
|
+
* maximized — the state a double-tap toggles is otherwise invisible on mobile. */
|
|
39600
|
+
syncMobileMaximize() {
|
|
39601
|
+
if (!this.mobileBar) return;
|
|
39602
|
+
const cell = this.activeId ? this.cellsById.get(this.activeId) : void 0;
|
|
39603
|
+
const paneMax = cell ? cell.chart.panes.list().some((p) => p.maximized) : false;
|
|
39604
|
+
this.mobileBar.setMaximizeActive(this.maximizedId != null || paneMax);
|
|
39605
|
+
}
|
|
39606
|
+
/**
|
|
39607
|
+
* Trade the SLOTS of two live cells — the grid arrangement changes, the cells
|
|
39608
|
+
* themselves (charts, indicators, drawings, the active flag) stay untouched.
|
|
39609
|
+
* What each cell's drag handle commits; also callable directly by hosts.
|
|
39610
|
+
*/
|
|
39611
|
+
swapCells(a, b) {
|
|
39612
|
+
if (this.destroyed || a === b) return;
|
|
39613
|
+
const i = this.order.indexOf(a);
|
|
39614
|
+
const j = this.order.indexOf(b);
|
|
39615
|
+
if (i < 0 || j < 0 || !this.cellsById.has(a) || !this.cellsById.has(b)) return;
|
|
39616
|
+
[this.order[i], this.order[j]] = [this.order[j], this.order[i]];
|
|
39617
|
+
for (const [k] of this.def.cells.entries()) {
|
|
39618
|
+
const host = this.cellsById.get(this.order[k] ?? "")?.host;
|
|
39619
|
+
if (host) this.gridEl.appendChild(host);
|
|
39620
|
+
}
|
|
39621
|
+
this.applyGrid();
|
|
39622
|
+
this.markStateDirty();
|
|
39623
|
+
}
|
|
38995
39624
|
resize() {
|
|
38996
39625
|
this.splitters.layout();
|
|
38997
39626
|
}
|
|
@@ -39061,6 +39690,7 @@ var VelaWorkspace = class {
|
|
|
39061
39690
|
this.mobileBar?.renderActions();
|
|
39062
39691
|
this.mobileBar?.setSymbol(cell.symbol);
|
|
39063
39692
|
this.mobileBar?.setTimeframe(cell.timeframe);
|
|
39693
|
+
this.syncMobileMaximize();
|
|
39064
39694
|
this.drawingPill?.onChart(cell.chart);
|
|
39065
39695
|
const pushHistory = () => this.topbar.setHistoryState(cell.history.canUndo, cell.history.canRedo);
|
|
39066
39696
|
this.historyUnsub?.();
|
|
@@ -39150,8 +39780,78 @@ var VelaWorkspace = class {
|
|
|
39150
39780
|
const host = this.cellsById.get(this.order[i] ?? "")?.host;
|
|
39151
39781
|
if (host) host.style.gridArea = perCell[slot.id]?.gridArea ?? "";
|
|
39152
39782
|
}
|
|
39783
|
+
this.applyMaximizePresentation();
|
|
39784
|
+
this.mountAttributionMark();
|
|
39153
39785
|
this.splitters.layout();
|
|
39154
39786
|
}
|
|
39787
|
+
/** Overlay the maximize presentation on the freshly applied grid: EVERY cell spans
|
|
39788
|
+
* the full track grid — the maximized one on top, the siblings invisible beneath
|
|
39789
|
+
* it (their charts stay alive — restoring is instant). The siblings must span too:
|
|
39790
|
+
* left in their slots they would auto-flow into implicit zero-height rows, whose
|
|
39791
|
+
* gaps steal height from the maximized cell and collapse their renderers to 0.
|
|
39792
|
+
* The splitter strips and the active ring hide via the `data-maximized` rules. */
|
|
39793
|
+
applyMaximizePresentation() {
|
|
39794
|
+
const maxId = this.maximizedId;
|
|
39795
|
+
if (maxId) this.gridEl.dataset.maximized = "1";
|
|
39796
|
+
else delete this.gridEl.dataset.maximized;
|
|
39797
|
+
for (const [id, cell] of this.cellsById) {
|
|
39798
|
+
const style = cell.host.style;
|
|
39799
|
+
if (maxId) style.gridArea = "1 / 1 / -1 / -1";
|
|
39800
|
+
style.zIndex = maxId && id === maxId ? "5" : "";
|
|
39801
|
+
style.visibility = maxId && id !== maxId ? "hidden" : "";
|
|
39802
|
+
}
|
|
39803
|
+
}
|
|
39804
|
+
/** Rebuild every cell's view cluster (the maximize gate or state changed). */
|
|
39805
|
+
refreshCellControls() {
|
|
39806
|
+
for (const cell of this.cellsById.values()) cell.refreshControls();
|
|
39807
|
+
}
|
|
39808
|
+
/** Drop the transient maximize on a structural change (layout switch, state
|
|
39809
|
+
* document) — WITH the event, so hosts tracking `cell:maximized` never drift
|
|
39810
|
+
* from `maximizedCell`. The caller's own grid re-apply paints the restore. */
|
|
39811
|
+
clearMaximized() {
|
|
39812
|
+
if (this.maximizedId == null) return;
|
|
39813
|
+
this.maximizedId = null;
|
|
39814
|
+
this.events.emit("cell:maximized", { id: null });
|
|
39815
|
+
}
|
|
39816
|
+
/** The live cell under a viewport point, excluding `excludeId` and any host a
|
|
39817
|
+
* maximize has hidden — the drag handle's hit-test. */
|
|
39818
|
+
cellAtPoint(x, y, excludeId) {
|
|
39819
|
+
for (const [id, cell] of this.cellsById) {
|
|
39820
|
+
if (id === excludeId || cell.host.style.visibility === "hidden") continue;
|
|
39821
|
+
const r = cell.host.getBoundingClientRect();
|
|
39822
|
+
if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return id;
|
|
39823
|
+
}
|
|
39824
|
+
return null;
|
|
39825
|
+
}
|
|
39826
|
+
/** Mark one cell as the live drop target of a grip drag (null clears all) —
|
|
39827
|
+
* the `data-drop-target` stylesheet rule paints the dashed preview ring. */
|
|
39828
|
+
setDropTarget(id) {
|
|
39829
|
+
for (const [cid, cell] of this.cellsById) {
|
|
39830
|
+
if (cid === id) cell.host.dataset.dropTarget = "1";
|
|
39831
|
+
else delete cell.host.dataset.dropTarget;
|
|
39832
|
+
}
|
|
39833
|
+
}
|
|
39834
|
+
/** The cell whose bottom-left corner the grid's attribution mark floats in — the
|
|
39835
|
+
* maximized cell while one covers the grid, else the bottom-left slot's cell. */
|
|
39836
|
+
bottomLeftCell() {
|
|
39837
|
+
if (this.maximizedId) return this.cellsById.get(this.maximizedId);
|
|
39838
|
+
const grid = occupancyGrid(this.def);
|
|
39839
|
+
const slot = grid[grid.length - 1]?.[0];
|
|
39840
|
+
const idx = this.def.cells.findIndex((c) => (c.area ?? c.id) === slot);
|
|
39841
|
+
return this.cellsById.get(this.order[idx >= 0 ? idx : 0] ?? "");
|
|
39842
|
+
}
|
|
39843
|
+
/** Keep the shared attribution mark inside the BOTTOM-LEFT visible cell: its
|
|
39844
|
+
* offsets ride that cell's renderer-published `--vela-bottom-gutter` /
|
|
39845
|
+
* `--vela-toolbar-gutter`, so collapsed pane strips push the mark up without any
|
|
39846
|
+
* bookkeeping here. Re-run after anything that changes which host that is
|
|
39847
|
+
* (layout switch, maximize, cell rebuild); a destroyed host drops the mark from
|
|
39848
|
+
* the DOM, and this re-mount brings it back. */
|
|
39849
|
+
mountAttributionMark() {
|
|
39850
|
+
const mark = this.attributionMark;
|
|
39851
|
+
if (!mark) return;
|
|
39852
|
+
const host = this.bottomLeftCell()?.host ?? this.gridEl;
|
|
39853
|
+
if (mark.parentElement !== host) host.appendChild(mark);
|
|
39854
|
+
}
|
|
39155
39855
|
/** Create the cells the current layout wants but don't exist yet (pool-first).
|
|
39156
39856
|
* A slot's CELL IDENTITY is `order[i]` (declaration order — never the slot's own
|
|
39157
39857
|
* positional id); slots past the declared list mint an auto identity once. */
|
|
@@ -39183,6 +39883,12 @@ var VelaWorkspace = class {
|
|
|
39183
39883
|
setTimezone: (zone) => this.setTimezone(zone),
|
|
39184
39884
|
context: () => this.context(),
|
|
39185
39885
|
activate: (id2) => this.setActiveCell(id2),
|
|
39886
|
+
multiCell: () => !this.monoLayout && this.def.cells.length > 1,
|
|
39887
|
+
isMaximized: (id2) => this.maximizedId === id2,
|
|
39888
|
+
toggleMaximize: (id2) => this.maximizeCell(this.maximizedId === id2 ? null : id2),
|
|
39889
|
+
cellDragTarget: (id2, x, y) => this.cellAtPoint(x, y, id2),
|
|
39890
|
+
previewDropTarget: (target) => this.setDropTarget(target),
|
|
39891
|
+
dropCell: (id2, target) => this.swapCells(id2, target),
|
|
39186
39892
|
onMarketChanged: (id2) => this.onCellMarketChanged(id2),
|
|
39187
39893
|
onPriceStyleChanged: (id2) => this.onCellPriceStyleChanged(id2),
|
|
39188
39894
|
onIndicatorsChanged: (id2) => this.onCellIndicatorsChanged(id2),
|
|
@@ -39196,6 +39902,7 @@ var VelaWorkspace = class {
|
|
|
39196
39902
|
if (id === this.activeId) cell.host.dataset.active = "1";
|
|
39197
39903
|
this.wireCell(cell);
|
|
39198
39904
|
cell.chart.renderer.setLayoutMode(this.layoutCtl.current);
|
|
39905
|
+
cell.setControlsSuspended(this.layoutCtl.current === "mobile");
|
|
39199
39906
|
if (this.favs.length > 0) cell.chart.drawings.setFavorites(this.favs);
|
|
39200
39907
|
cell.setManifest(this.manifest, pooled?.indicators == null);
|
|
39201
39908
|
cell.restorePersistedExt();
|
|
@@ -39205,6 +39912,7 @@ var VelaWorkspace = class {
|
|
|
39205
39912
|
const host = this.cellsById.get(this.order[i] ?? "")?.host;
|
|
39206
39913
|
if (host) this.gridEl.appendChild(host);
|
|
39207
39914
|
}
|
|
39915
|
+
this.mountAttributionMark();
|
|
39208
39916
|
}
|
|
39209
39917
|
/** Per-cell chart subscriptions (trigger ② — the chart instance is stable for the
|
|
39210
39918
|
* cell's whole life, so these live and die with the cell). */
|
|
@@ -39264,6 +39972,9 @@ var VelaWorkspace = class {
|
|
|
39264
39972
|
chart.on("viewport:changed", (range) => this.propagateViewport(cell.id, range));
|
|
39265
39973
|
chart.renderer.onConfigChanged(() => this.propagateStylePrefs(cell.id));
|
|
39266
39974
|
chart.on("theme:changed", (t) => this.setTheme(t));
|
|
39975
|
+
chart.on("pane:changed", () => {
|
|
39976
|
+
if (cell.id === this.activeId) this.syncMobileMaximize();
|
|
39977
|
+
});
|
|
39267
39978
|
chart.renderer.onAxisLongPress((e) => {
|
|
39268
39979
|
if (this.layoutCtl.current !== "mobile") return;
|
|
39269
39980
|
if (e.axis === "time") this.openTimezoneDrawer();
|
|
@@ -39584,6 +40295,7 @@ var VelaWorkspace = class {
|
|
|
39584
40295
|
for (const cell of this.cellsById.values()) {
|
|
39585
40296
|
cell.chart.renderer.closeDialogs();
|
|
39586
40297
|
cell.chart.renderer.setLayoutMode(mode);
|
|
40298
|
+
cell.setControlsSuspended(mode === "mobile");
|
|
39587
40299
|
}
|
|
39588
40300
|
this.syncCellPresentation();
|
|
39589
40301
|
}
|