@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/widget.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;
|
|
@@ -13740,6 +13822,17 @@ function dedupeSymbols(list) {
|
|
|
13740
13822
|
}
|
|
13741
13823
|
return out;
|
|
13742
13824
|
}
|
|
13825
|
+
function foldGroups(list) {
|
|
13826
|
+
const groupsAbove = /* @__PURE__ */ new Set();
|
|
13827
|
+
const out = [];
|
|
13828
|
+
for (const s of list) {
|
|
13829
|
+
if (isGroupRow(s)) {
|
|
13830
|
+
groupsAbove.add(groupKeyOf(s));
|
|
13831
|
+
out.push(s);
|
|
13832
|
+
} else if (s.group == null || !groupsAbove.has(groupKeyOf(s))) out.push(s);
|
|
13833
|
+
}
|
|
13834
|
+
return out;
|
|
13835
|
+
}
|
|
13743
13836
|
var TOP_TICKERS = ["BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT", "XRPUSDT", "DOGEUSDT", "ADAUSDT", "LINKUSDT"];
|
|
13744
13837
|
function parseQuery(raw, venues) {
|
|
13745
13838
|
const m = raw.match(/^\s*([^\s:]+)\s*[:\s]\s*(.*)$/);
|
|
@@ -13871,6 +13964,21 @@ var CSS8 = `
|
|
|
13871
13964
|
.vela-sp-badge[data-p='binance'] { color: #f0b90b; } /* palette-exempt: venue brand mark */
|
|
13872
13965
|
.vela-sp-badge[data-p='hyperliquid'] { color: #50d2c1; } /* palette-exempt: venue brand mark */
|
|
13873
13966
|
.vela-sp-empty { padding: var(--vela-space-3); color: var(--vela-fg-muted); text-align: center; }
|
|
13967
|
+
/* Grouped listings (futures roots): the chevron unfolds members inline, indented. */
|
|
13968
|
+
.vela-sp-expander {
|
|
13969
|
+
all: unset;
|
|
13970
|
+
flex: none;
|
|
13971
|
+
display: inline-flex;
|
|
13972
|
+
align-items: center;
|
|
13973
|
+
justify-content: center;
|
|
13974
|
+
width: 22px;
|
|
13975
|
+
height: 22px;
|
|
13976
|
+
border-radius: 5px;
|
|
13977
|
+
cursor: pointer;
|
|
13978
|
+
color: var(--vela-fg-muted);
|
|
13979
|
+
}
|
|
13980
|
+
.vela-sp-expander:hover { background: var(--vela-surface-elev); color: var(--vela-fg); }
|
|
13981
|
+
.vela-sp-row[data-member] { padding-left: 34px; }
|
|
13874
13982
|
`;
|
|
13875
13983
|
var PAGE = 100;
|
|
13876
13984
|
var SymbolPicker = class {
|
|
@@ -13882,6 +13990,11 @@ var SymbolPicker = class {
|
|
|
13882
13990
|
this.seed = "";
|
|
13883
13991
|
this.activeTab = "All";
|
|
13884
13992
|
this.visible = PAGE;
|
|
13993
|
+
/** The last filter pass returned fewer raw rows than asked — the pool is drained
|
|
13994
|
+
* (checked BEFORE folding: folding shortens pages without meaning exhaustion). */
|
|
13995
|
+
this.exhausted = false;
|
|
13996
|
+
/** Group rows currently expanded (venue-scoped keys) — members shown inline. */
|
|
13997
|
+
this.expanded = /* @__PURE__ */ new Set();
|
|
13885
13998
|
/** The ranked pool cache — `key` fingerprints the raw pool the ranking ran on. */
|
|
13886
13999
|
this.ranked = null;
|
|
13887
14000
|
this.ranking = false;
|
|
@@ -13896,7 +14009,7 @@ var SymbolPicker = class {
|
|
|
13896
14009
|
searchRow.append(iconEl("search", doc), this.input);
|
|
13897
14010
|
this.tabs = doc.createElement("div");
|
|
13898
14011
|
this.tabs.className = "vela-sp-tabs";
|
|
13899
|
-
for (const t of ["All", "Stocks", "ETFs", "Crypto", "Forex", "Commodities"]) {
|
|
14012
|
+
for (const t of ["All", "Stocks", "ETFs", "Crypto", "Futures", "Forex", "Commodities"]) {
|
|
13900
14013
|
const b = doc.createElement("button");
|
|
13901
14014
|
b.className = "vela-sp-tab";
|
|
13902
14015
|
b.textContent = t;
|
|
@@ -13912,7 +14025,7 @@ var SymbolPicker = class {
|
|
|
13912
14025
|
this.list = doc.createElement("div");
|
|
13913
14026
|
this.list.className = "vela-sp-list";
|
|
13914
14027
|
this.list.addEventListener("scroll", () => {
|
|
13915
|
-
if (this.
|
|
14028
|
+
if (this.exhausted) return;
|
|
13916
14029
|
if (this.list.scrollTop + this.list.clientHeight < this.list.scrollHeight - 200) return;
|
|
13917
14030
|
this.visible += PAGE;
|
|
13918
14031
|
this.grow();
|
|
@@ -13941,14 +14054,19 @@ var SymbolPicker = class {
|
|
|
13941
14054
|
else if (e.key === "ArrowUp") this.moveHighlight(-1);
|
|
13942
14055
|
else if (e.key === "Enter") {
|
|
13943
14056
|
const pick = this.rows[this.highlighted];
|
|
13944
|
-
if (pick) this.
|
|
14057
|
+
if (pick) this.pick(pick);
|
|
13945
14058
|
return;
|
|
13946
14059
|
} else return;
|
|
13947
14060
|
e.preventDefault();
|
|
13948
14061
|
});
|
|
13949
14062
|
this.list.addEventListener("click", (e) => {
|
|
13950
|
-
const
|
|
13951
|
-
|
|
14063
|
+
const target = e.target;
|
|
14064
|
+
const row = target.closest(".vela-sp-row");
|
|
14065
|
+
if (!row) return;
|
|
14066
|
+
const s = this.rows[Number(row.dataset.i)];
|
|
14067
|
+
if (!s) return;
|
|
14068
|
+
if (target.closest(".vela-sp-expander")) this.toggleExpand(s);
|
|
14069
|
+
else this.pick(s);
|
|
13952
14070
|
});
|
|
13953
14071
|
}
|
|
13954
14072
|
/** Wire where symbols come from (re-called on every widget rebuild). */
|
|
@@ -13965,6 +14083,27 @@ var SymbolPicker = class {
|
|
|
13965
14083
|
destroy() {
|
|
13966
14084
|
this.dialog.destroy();
|
|
13967
14085
|
}
|
|
14086
|
+
/** Route a row activation: a GROUP row loads its default member (the root itself is
|
|
14087
|
+
* listed, never loadable), any other row loads itself. */
|
|
14088
|
+
pick(s) {
|
|
14089
|
+
const target = isGroupRow(s) ? defaultMemberOf(this.pool(), s) ?? s : s;
|
|
14090
|
+
this.select(target.ticker, target.prefix ?? target.provider, this.opts.onSelect);
|
|
14091
|
+
}
|
|
14092
|
+
/** Expand/collapse a group row IN PLACE — same query, same page, same scroll; only
|
|
14093
|
+
* the member rows under the group appear or go. */
|
|
14094
|
+
toggleExpand(s) {
|
|
14095
|
+
const key = groupKeyOf(s);
|
|
14096
|
+
if (!this.expanded.delete(key)) this.expanded.add(key);
|
|
14097
|
+
const scrollTop = this.list.scrollTop;
|
|
14098
|
+
const focus = this.rows[this.highlighted];
|
|
14099
|
+
this.rows = this.computeRows();
|
|
14100
|
+
this.list.replaceChildren();
|
|
14101
|
+
this.rows.forEach((r, i) => this.list.appendChild(this.rowEl(r, i)));
|
|
14102
|
+
const at = focus ? this.rows.indexOf(focus) : -1;
|
|
14103
|
+
this.highlighted = at >= 0 ? at : Math.min(this.highlighted, Math.max(0, this.rows.length - 1));
|
|
14104
|
+
this.renderHighlight();
|
|
14105
|
+
this.list.scrollTop = scrollTop;
|
|
14106
|
+
}
|
|
13968
14107
|
select(ticker, venue, onSelect) {
|
|
13969
14108
|
this.close();
|
|
13970
14109
|
onSelect(venue ? `${venue}:${ticker}` : ticker);
|
|
@@ -14006,10 +14145,26 @@ var SymbolPicker = class {
|
|
|
14006
14145
|
return this.ranked?.result ?? raw;
|
|
14007
14146
|
}
|
|
14008
14147
|
computeRows() {
|
|
14009
|
-
const TAB_TYPES = { Crypto: ["crypto"], Stocks: ["stock"], ETFs: ["etf"], Forex: ["forex"], Commodities: ["commodity"] };
|
|
14148
|
+
const TAB_TYPES = { Crypto: ["crypto"], Stocks: ["stock"], ETFs: ["etf"], Futures: ["futures", "root"], Forex: ["forex"], Commodities: ["commodity"] };
|
|
14010
14149
|
const all = this.pool();
|
|
14011
14150
|
const pool = this.activeTab === "All" ? all : all.filter((s) => TAB_TYPES[this.activeTab]?.includes((s.type ?? "").toLowerCase()) || this.activeTab === "Crypto" && (s.type ?? "").toLowerCase() === "futures");
|
|
14012
|
-
|
|
14151
|
+
const filtered = filterSymbols(pool, this.input.value, this.visible, symbolRanking() ? false : TOP_TICKERS);
|
|
14152
|
+
this.exhausted = filtered.length < this.visible;
|
|
14153
|
+
const folded = foldGroups(filtered);
|
|
14154
|
+
if (!this.expanded.size) return folded;
|
|
14155
|
+
const keyOf = (s) => `${(s.prefix ?? s.provider ?? "").toLowerCase()}:${s.ticker.toUpperCase()}`;
|
|
14156
|
+
const present = new Set(folded.map(keyOf));
|
|
14157
|
+
const out = [];
|
|
14158
|
+
for (const s of folded) {
|
|
14159
|
+
out.push(s);
|
|
14160
|
+
if (!isGroupRow(s) || !this.expanded.has(groupKeyOf(s))) continue;
|
|
14161
|
+
for (const m of groupMembers(all, s)) {
|
|
14162
|
+
if (present.has(keyOf(m))) continue;
|
|
14163
|
+
present.add(keyOf(m));
|
|
14164
|
+
out.push(m);
|
|
14165
|
+
}
|
|
14166
|
+
}
|
|
14167
|
+
return out;
|
|
14013
14168
|
}
|
|
14014
14169
|
refresh() {
|
|
14015
14170
|
const doc = this.list.ownerDocument;
|
|
@@ -14024,19 +14179,20 @@ var SymbolPicker = class {
|
|
|
14024
14179
|
this.list.appendChild(empty);
|
|
14025
14180
|
return;
|
|
14026
14181
|
}
|
|
14027
|
-
|
|
14182
|
+
this.rows.forEach((s, i) => this.list.appendChild(this.rowEl(s, i)));
|
|
14028
14183
|
this.renderHighlight();
|
|
14029
14184
|
}
|
|
14030
14185
|
/** Append the page the grown `visible` just uncovered — rows already on screen stay put. */
|
|
14031
14186
|
grow() {
|
|
14032
14187
|
const already = this.rows.length;
|
|
14033
14188
|
this.rows = this.computeRows();
|
|
14034
|
-
|
|
14189
|
+
this.rows.slice(already).forEach((s, j) => this.list.appendChild(this.rowEl(s, already + j)));
|
|
14035
14190
|
}
|
|
14036
|
-
rowEl(s) {
|
|
14191
|
+
rowEl(s, i) {
|
|
14037
14192
|
const doc = this.list.ownerDocument;
|
|
14038
14193
|
const row = doc.createElement("div");
|
|
14039
14194
|
row.className = "vela-sp-row";
|
|
14195
|
+
row.dataset.i = String(i);
|
|
14040
14196
|
row.dataset.ticker = s.ticker;
|
|
14041
14197
|
const venue = s.prefix ?? s.provider;
|
|
14042
14198
|
if (venue) row.dataset.venue = venue;
|
|
@@ -14051,6 +14207,14 @@ var SymbolPicker = class {
|
|
|
14051
14207
|
d.textContent = s.description ?? (s.type ?? "");
|
|
14052
14208
|
main.append(t, d);
|
|
14053
14209
|
row.append(av, main);
|
|
14210
|
+
if (isGroupRow(s)) {
|
|
14211
|
+
row.dataset.group = "1";
|
|
14212
|
+
const expander = doc.createElement("button");
|
|
14213
|
+
expander.className = "vela-sp-expander";
|
|
14214
|
+
expander.setAttribute("aria-label", "Show contracts");
|
|
14215
|
+
expander.appendChild(iconEl(this.expanded.has(groupKeyOf(s)) ? "chevron-down" : "chevron-right", doc));
|
|
14216
|
+
row.appendChild(expander);
|
|
14217
|
+
} else if (s.group != null && this.expanded.has(groupKeyOf(s))) row.dataset.member = "1";
|
|
14054
14218
|
if (venue) {
|
|
14055
14219
|
const badge = doc.createElement("span");
|
|
14056
14220
|
badge.className = "vela-sp-badge";
|
|
@@ -14667,6 +14831,9 @@ var CSS13 = `
|
|
|
14667
14831
|
}
|
|
14668
14832
|
.vela-mb-item:active { background: var(--vela-hover); }
|
|
14669
14833
|
.vela-mb-item .vela-icon { font-size: 18px; width: 18px; height: 18px; }
|
|
14834
|
+
/* A lit stop (the maximize toggle while something is isolated): the inverse
|
|
14835
|
+
"selected" chip \u2014 white on the dark theme, dark on the light one. */
|
|
14836
|
+
.vela-mb-item.vela-mb-on, .vela-mb-item.vela-mb-on:active { background: var(--vela-selected-bg); color: var(--vela-selected-fg); }
|
|
14670
14837
|
/* Left-aligned contributed actions get their own stops (the built-in indicators
|
|
14671
14838
|
slot) \u2014 the wrapper is layout-transparent so each stop flexes like a sibling. */
|
|
14672
14839
|
.vela-mb-actions { display: contents; }
|
|
@@ -14707,9 +14874,11 @@ var MobileBar = class {
|
|
|
14707
14874
|
this.actionsHost.className = "vela-mb-actions";
|
|
14708
14875
|
const onDrawings = opts.onDrawingsClick;
|
|
14709
14876
|
const drawings = onDrawings ? item("vela-mb-drawings", "Drawings", onDrawings, "pen") : null;
|
|
14877
|
+
const onMaximize = opts.onMaximizeClick;
|
|
14878
|
+
this.maxEl = onMaximize ? item("vela-mb-maximize", "Maximize chart", onMaximize, "maximize") : null;
|
|
14710
14879
|
const more = item("vela-mb-more", "More", opts.onMoreClick, "kebab");
|
|
14711
14880
|
const settings = item("vela-mb-settings", "Chart settings", opts.onSettingsClick, "gear");
|
|
14712
|
-
this.el.append(this.symbolEl, this.tfEl, ...indicators ? [indicators] : [], this.actionsHost, ...drawings ? [drawings] : [], more, settings);
|
|
14881
|
+
this.el.append(this.symbolEl, this.tfEl, ...indicators ? [indicators] : [], this.actionsHost, ...drawings ? [drawings] : [], ...this.maxEl ? [this.maxEl] : [], more, settings);
|
|
14713
14882
|
host.appendChild(this.el);
|
|
14714
14883
|
this.renderActions();
|
|
14715
14884
|
}
|
|
@@ -14741,6 +14910,14 @@ var MobileBar = class {
|
|
|
14741
14910
|
setTimeframe(tf) {
|
|
14742
14911
|
this.tfEl.textContent = timeframeLabel(tf);
|
|
14743
14912
|
}
|
|
14913
|
+
/** Light the maximize stop while something is isolated (a chart over the grid,
|
|
14914
|
+
* or a maximized pane inside the active chart) — inverse chip + restore glyph. */
|
|
14915
|
+
setMaximizeActive(on) {
|
|
14916
|
+
if (!this.maxEl) return;
|
|
14917
|
+
this.maxEl.classList.toggle("vela-mb-on", on);
|
|
14918
|
+
this.maxEl.setAttribute("aria-label", on ? "Restore layout" : "Maximize chart");
|
|
14919
|
+
this.maxEl.replaceChildren(iconEl(on ? "restore" : "maximize", this.el.ownerDocument));
|
|
14920
|
+
}
|
|
14744
14921
|
destroy() {
|
|
14745
14922
|
this.el.remove();
|
|
14746
14923
|
}
|
|
@@ -16918,7 +17095,7 @@ var DrawingToolbar = class {
|
|
|
16918
17095
|
this.root.replaceChildren();
|
|
16919
17096
|
this.groupCells.clear();
|
|
16920
17097
|
this.groupIcons.clear();
|
|
16921
|
-
this.cursorBtn = this.makeButton(CURSOR_ICON, "Cursor", () => this.
|
|
17098
|
+
this.cursorBtn = this.makeButton(CURSOR_ICON, "Cursor", () => this.onCursorClick());
|
|
16922
17099
|
this.root.appendChild(this.cursorBtn);
|
|
16923
17100
|
if (this.def.groups.length > 0) this.root.appendChild(this.divider());
|
|
16924
17101
|
for (const g of this.def.groups) {
|
|
@@ -17022,6 +17199,14 @@ var DrawingToolbar = class {
|
|
|
17022
17199
|
this.magnetIcon = icon2;
|
|
17023
17200
|
return cell;
|
|
17024
17201
|
}
|
|
17202
|
+
/** Cursor returns to select/idle: an active measure/eraser mode exits through its own
|
|
17203
|
+
* toggle callback (disarming a tool via `onArm(null)` alone can't — the host treats a
|
|
17204
|
+
* null arm as a no-op side effect of entering those modes), then the tool disarms. */
|
|
17205
|
+
onCursorClick() {
|
|
17206
|
+
if (this.measureActive) this.onMeasure();
|
|
17207
|
+
if (this.eraserActive) this.onEraser();
|
|
17208
|
+
this.onArm(null);
|
|
17209
|
+
}
|
|
17025
17210
|
/** Clicking the icon arms the group's last-used tool (it does NOT open the flyout). */
|
|
17026
17211
|
onGroupIconClick(group) {
|
|
17027
17212
|
const type = this.lastUsed.get(group.id) ?? group.tools[0]?.type;
|
|
@@ -18020,6 +18205,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18020
18205
|
/** Invalidates detached async work (backfill loops, in-flight loads, gap heals):
|
|
18021
18206
|
* bumped by init(), setMarket() and destroy(). */
|
|
18022
18207
|
this.generation = 0;
|
|
18208
|
+
/** Aborts the in-flight PROGRESSIVE load's source polling on supersession — an
|
|
18209
|
+
* abandoned stream left polling to its own budget starves the browser's per-host
|
|
18210
|
+
* connection pool, and the NEXT symbol's very first fetch with it (measured). */
|
|
18211
|
+
this.progressiveAbort = null;
|
|
18023
18212
|
/** Awaiters racing a superseded load (setMarket callers) — released on every bump so they never hang. */
|
|
18024
18213
|
this.supersedeWaiters = [];
|
|
18025
18214
|
/** `history:complete` fired for the CURRENT load. Each market load re-arms the cycle
|
|
@@ -18162,6 +18351,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18162
18351
|
* superseded setMarket awaiters so their promises resolve instead of hanging. */
|
|
18163
18352
|
bumpGeneration() {
|
|
18164
18353
|
const gen = ++this.generation;
|
|
18354
|
+
this.progressiveAbort?.abort();
|
|
18355
|
+
this.progressiveAbort = null;
|
|
18165
18356
|
for (const w of this.supersedeWaiters.splice(0)) w();
|
|
18166
18357
|
return gen;
|
|
18167
18358
|
}
|
|
@@ -18215,7 +18406,47 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18215
18406
|
const requested = market.bars ?? 500;
|
|
18216
18407
|
const initialRange = market.visibleRange;
|
|
18217
18408
|
const deep = !market.data?.length && initialRange == null && requested > SINGLE_LOAD_BARS;
|
|
18218
|
-
|
|
18409
|
+
let progressiveServed = false;
|
|
18410
|
+
if (!market.data?.length && initialRange == null && this.feed.loadProgressive) {
|
|
18411
|
+
let painted = false;
|
|
18412
|
+
const paint = (bars, final) => {
|
|
18413
|
+
if (this.generation !== gen || !final && bars.length === 0) return;
|
|
18414
|
+
this.setBarSeries(bars, painted ? { preserveView: true } : void 0);
|
|
18415
|
+
if (!painted && bars.length > 0) {
|
|
18416
|
+
painted = true;
|
|
18417
|
+
if (opts.firstLoad) this.activateBarLayers();
|
|
18418
|
+
if (!final) this.historyState = "backfill";
|
|
18419
|
+
}
|
|
18420
|
+
};
|
|
18421
|
+
const abort = new AbortController();
|
|
18422
|
+
this.progressiveAbort = abort;
|
|
18423
|
+
progressiveServed = await new Promise((firstPaint) => {
|
|
18424
|
+
let signaled = false;
|
|
18425
|
+
const signal = (served) => {
|
|
18426
|
+
if (!signaled) {
|
|
18427
|
+
signaled = true;
|
|
18428
|
+
firstPaint(served);
|
|
18429
|
+
}
|
|
18430
|
+
};
|
|
18431
|
+
abort.signal.addEventListener("abort", () => signal(true), { once: true });
|
|
18432
|
+
this.feed.loadProgressive(market, (bars) => {
|
|
18433
|
+
paint(bars, false);
|
|
18434
|
+
if (painted) signal(true);
|
|
18435
|
+
}, { signal: abort.signal }).then((full) => {
|
|
18436
|
+
if (this.progressiveAbort === abort) this.progressiveAbort = null;
|
|
18437
|
+
if (full == null) return signal(false);
|
|
18438
|
+
if (this.generation !== gen) return signal(true);
|
|
18439
|
+
paint(full, true);
|
|
18440
|
+
this.completeHistory(full.length >= requested ? "depth" : "genesis");
|
|
18441
|
+
signal(true);
|
|
18442
|
+
}).catch(() => {
|
|
18443
|
+
if (this.progressiveAbort === abort) this.progressiveAbort = null;
|
|
18444
|
+
if (this.generation === gen) this.completeHistory("aborted");
|
|
18445
|
+
signal(true);
|
|
18446
|
+
});
|
|
18447
|
+
});
|
|
18448
|
+
}
|
|
18449
|
+
if (progressiveServed) ; else if (deep && this.feed.loadRange) {
|
|
18219
18450
|
const head = await this.feed.load({ ...market, bars: Math.min(requested, CHUNK_BARS) });
|
|
18220
18451
|
if (this.generation !== gen) return;
|
|
18221
18452
|
this.setBarSeries(head);
|
|
@@ -20958,6 +21189,9 @@ var CLOSE_SVG = iconAt("close", LEGEND_ICON_PX2);
|
|
|
20958
21189
|
var FOLD_SVG = iconAt("chevron-up", LEGEND_ICON_PX2);
|
|
20959
21190
|
var UNFOLD_SVG = iconAt("chevron-down", LEGEND_ICON_PX2);
|
|
20960
21191
|
var OVERVIEW_SVG = iconAt("objects", LEGEND_ICON_PX2);
|
|
21192
|
+
function legendCalloutsDisplay(open2, hasCallouts) {
|
|
21193
|
+
return !open2 && hasCallouts ? "inline-flex" : "none";
|
|
21194
|
+
}
|
|
20961
21195
|
var InputsUI = class {
|
|
20962
21196
|
constructor(container, theme, paneBoundsOf) {
|
|
20963
21197
|
this.container = container;
|
|
@@ -21161,7 +21395,7 @@ var InputsUI = class {
|
|
|
21161
21395
|
row.callouts = [];
|
|
21162
21396
|
row.calloutsEl.replaceChildren();
|
|
21163
21397
|
const views = this.legendCallouts?.(row.id) ?? [];
|
|
21164
|
-
row.calloutsEl.style.display = views.length > 0
|
|
21398
|
+
row.calloutsEl.style.display = legendCalloutsDisplay(row.highlighted, views.length > 0);
|
|
21165
21399
|
for (const view of views) {
|
|
21166
21400
|
const bubble = new CalloutBubble({
|
|
21167
21401
|
icon: view.icon,
|
|
@@ -21645,11 +21879,11 @@ var InputsUI = class {
|
|
|
21645
21879
|
row.controlsEl.style.display = open2 || row.hidden ? "inline-flex" : "none";
|
|
21646
21880
|
if (open2) {
|
|
21647
21881
|
row.el.appendChild(row.statusEl);
|
|
21648
|
-
row.
|
|
21882
|
+
for (const bubble of row.callouts) bubble.hidePanel();
|
|
21649
21883
|
} else {
|
|
21650
21884
|
row.el.insertBefore(row.statusEl, row.valuesEl);
|
|
21651
|
-
row.el.insertBefore(row.calloutsEl, row.statusEl);
|
|
21652
21885
|
}
|
|
21886
|
+
row.calloutsEl.style.display = legendCalloutsDisplay(open2, row.callouts.length > 0);
|
|
21653
21887
|
for (const child of Array.from(row.controlsEl.children)) {
|
|
21654
21888
|
if (!(child instanceof HTMLElement) || child === row.eyeEl) continue;
|
|
21655
21889
|
if (child === row.extrasEl) {
|
|
@@ -21739,7 +21973,7 @@ var ICONS = {
|
|
|
21739
21973
|
};
|
|
21740
21974
|
var STYLE_ID21 = "vela-pane-controls";
|
|
21741
21975
|
var ICON_PX = 12;
|
|
21742
|
-
var CLUSTER_PILL = "rgba(0,0,0,0.
|
|
21976
|
+
var CLUSTER_PILL = "rgba(0,0,0,0.65)";
|
|
21743
21977
|
function ensureStyles3() {
|
|
21744
21978
|
if (typeof document === "undefined" || document.getElementById(STYLE_ID21)) return;
|
|
21745
21979
|
const st = document.createElement("style");
|
|
@@ -21759,8 +21993,12 @@ var PaneControls = class {
|
|
|
21759
21993
|
this.deps = deps;
|
|
21760
21994
|
this.clusters = /* @__PURE__ */ new Map();
|
|
21761
21995
|
this.hoverPaneId = null;
|
|
21996
|
+
/** Mobile: hover clusters are meaningless without a cursor — suppressed; a
|
|
21997
|
+
* collapsed pane's standalone expand chip stays (the only way back up). */
|
|
21998
|
+
this.suspended = false;
|
|
21762
21999
|
/** Reveal the cluster for the pane under the cursor, resolved from the pointer's y in the plot. */
|
|
21763
22000
|
this.onPlotMove = (e) => {
|
|
22001
|
+
if (this.suspended) return;
|
|
21764
22002
|
const rect = this.plot.getBoundingClientRect();
|
|
21765
22003
|
const y = e.clientY - rect.top;
|
|
21766
22004
|
let hit = null;
|
|
@@ -21832,7 +22070,12 @@ var PaneControls = class {
|
|
|
21832
22070
|
}
|
|
21833
22071
|
if (p.count > 1) {
|
|
21834
22072
|
cluster.appendChild(
|
|
21835
|
-
this.button(p.maximized ? ICONS.restore : ICONS.maximize, p.maximized ? "Restore pane" : "Maximize pane", false, () => this.deps.onToggleMaximize(p.id), {
|
|
22073
|
+
this.button(p.maximized ? ICONS.restore : ICONS.maximize, p.maximized ? "Restore pane" : "Maximize pane", false, () => this.deps.onToggleMaximize(p.id), {
|
|
22074
|
+
role: "maximize",
|
|
22075
|
+
// Same inverse-chip treatment as the collapsed pane's expand toggle: the
|
|
22076
|
+
// maximized state must read as an active state, not just a swapped glyph.
|
|
22077
|
+
selected: p.maximized
|
|
22078
|
+
})
|
|
21836
22079
|
);
|
|
21837
22080
|
}
|
|
21838
22081
|
}
|
|
@@ -21869,17 +22112,18 @@ var PaneControls = class {
|
|
|
21869
22112
|
}
|
|
21870
22113
|
const hovered = id === this.hoverPaneId;
|
|
21871
22114
|
const hasButtons = cluster.children.length > 0;
|
|
21872
|
-
const
|
|
22115
|
+
const stateChipRole = p.collapsed ? "collapse" : !this.suspended && p.maximized ? "maximize" : null;
|
|
22116
|
+
const visible = hasButtons && (hovered || stateChipRole != null) && p.height > 8;
|
|
21873
22117
|
cluster.style.right = `${rightPx}px`;
|
|
21874
22118
|
cluster.style.top = p.collapsed ? `${p.top + Math.max(1, Math.round((p.height - 24) / 2))}px` : `${p.top + 4}px`;
|
|
21875
22119
|
cluster.style.display = visible ? "flex" : "none";
|
|
21876
22120
|
if (!visible) continue;
|
|
21877
|
-
const
|
|
21878
|
-
cluster.style.background =
|
|
22121
|
+
const soloChip = stateChipRole != null && !hovered;
|
|
22122
|
+
cluster.style.background = soloChip ? "transparent" : CLUSTER_PILL;
|
|
21879
22123
|
for (const child of cluster.children) {
|
|
21880
22124
|
const btn2 = child;
|
|
21881
22125
|
btn2.style.display = "inline-flex";
|
|
21882
|
-
btn2.style.visibility =
|
|
22126
|
+
btn2.style.visibility = soloChip && btn2.dataset.role !== stateChipRole ? "hidden" : "visible";
|
|
21883
22127
|
}
|
|
21884
22128
|
}
|
|
21885
22129
|
}
|
|
@@ -21889,6 +22133,14 @@ var PaneControls = class {
|
|
|
21889
22133
|
this.hoverPaneId = paneId;
|
|
21890
22134
|
this.reposition();
|
|
21891
22135
|
}
|
|
22136
|
+
/** Mobile suppression: no hover clusters (touch has no cursor; the shell's own
|
|
22137
|
+
* chrome covers maximize), while collapsed panes keep their expand chips. */
|
|
22138
|
+
setSuspended(on) {
|
|
22139
|
+
if (on === this.suspended) return;
|
|
22140
|
+
this.suspended = on;
|
|
22141
|
+
if (on) this.hoverPaneId = null;
|
|
22142
|
+
this.reposition();
|
|
22143
|
+
}
|
|
21892
22144
|
destroy() {
|
|
21893
22145
|
this.plot.removeEventListener("pointermove", this.onPlotMove);
|
|
21894
22146
|
this.plot.removeEventListener("pointerleave", this.onPlotLeave);
|
|
@@ -22039,160 +22291,6 @@ var AxisScaleButtons = class {
|
|
|
22039
22291
|
}
|
|
22040
22292
|
};
|
|
22041
22293
|
|
|
22042
|
-
// src/renderers/shared/TableOverlay.ts
|
|
22043
|
-
var SIZE_PX3 = {
|
|
22044
|
-
auto: 13,
|
|
22045
|
-
tiny: 10,
|
|
22046
|
-
small: 11,
|
|
22047
|
-
normal: 13,
|
|
22048
|
-
large: 16,
|
|
22049
|
-
huge: 20
|
|
22050
|
-
};
|
|
22051
|
-
function fontPxOf(size) {
|
|
22052
|
-
if (typeof size === "number") return size > 0 ? size : SIZE_PX3.auto;
|
|
22053
|
-
return SIZE_PX3[size] ?? SIZE_PX3.auto;
|
|
22054
|
-
}
|
|
22055
|
-
function tableHasContent(t) {
|
|
22056
|
-
return t.cells.some((row) => row?.some((c) => c != null && !c.merged));
|
|
22057
|
-
}
|
|
22058
|
-
function mergeRenderPlan(t) {
|
|
22059
|
-
const span = /* @__PURE__ */ new Map();
|
|
22060
|
-
const omit = /* @__PURE__ */ new Set();
|
|
22061
|
-
for (const m of t.merges) {
|
|
22062
|
-
span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
|
|
22063
|
-
for (let r = m.startRow; r <= m.endRow; r += 1) {
|
|
22064
|
-
for (let c = m.startCol; c <= m.endCol; c += 1) {
|
|
22065
|
-
if (r !== m.startRow || c !== m.startCol) omit.add(`${r}:${c}`);
|
|
22066
|
-
}
|
|
22067
|
-
}
|
|
22068
|
-
}
|
|
22069
|
-
for (let r = 0; r < t.rows; r += 1) {
|
|
22070
|
-
for (let c = 0; c < t.columns; c += 1) {
|
|
22071
|
-
if (t.cells[r]?.[c]?.merged && !span.has(`${r}:${c}`)) omit.add(`${r}:${c}`);
|
|
22072
|
-
}
|
|
22073
|
-
}
|
|
22074
|
-
for (const key of span.keys()) omit.delete(key);
|
|
22075
|
-
return { span, omit };
|
|
22076
|
-
}
|
|
22077
|
-
var TableOverlay = class {
|
|
22078
|
-
constructor(container, theme, paneBounds) {
|
|
22079
|
-
this.container = container;
|
|
22080
|
-
this.theme = theme;
|
|
22081
|
-
this.paneBounds = paneBounds;
|
|
22082
|
-
this.lastTables = [];
|
|
22083
|
-
if (getComputedStyle(container).position === "static") container.style.position = "relative";
|
|
22084
|
-
this.root = document.createElement("div");
|
|
22085
|
-
Object.assign(this.root.style, {
|
|
22086
|
-
position: "absolute",
|
|
22087
|
-
inset: "0",
|
|
22088
|
-
pointerEvents: "none",
|
|
22089
|
-
overflow: "hidden",
|
|
22090
|
-
zIndex: "3"
|
|
22091
|
-
});
|
|
22092
|
-
container.appendChild(this.root);
|
|
22093
|
-
}
|
|
22094
|
-
update(tables) {
|
|
22095
|
-
this.lastTables = tables;
|
|
22096
|
-
this.root.replaceChildren();
|
|
22097
|
-
for (const t of tables) {
|
|
22098
|
-
if (tableHasContent(t)) this.root.appendChild(this.renderTable(t));
|
|
22099
|
-
}
|
|
22100
|
-
}
|
|
22101
|
-
/** Re-render at the current pane geometry — after layout settles or on resize. */
|
|
22102
|
-
reposition() {
|
|
22103
|
-
if (this.root.isConnected) this.update(this.lastTables);
|
|
22104
|
-
}
|
|
22105
|
-
/** Show/hide the whole overlay. Tables anchor to pane corners, not to bars, so unlike the
|
|
22106
|
-
* series content they DON'T vanish with an emptied chart — the loading state hides them. */
|
|
22107
|
-
setVisible(visible) {
|
|
22108
|
-
this.root.style.display = visible ? "" : "none";
|
|
22109
|
-
}
|
|
22110
|
-
destroy() {
|
|
22111
|
-
this.root.remove();
|
|
22112
|
-
}
|
|
22113
|
-
renderTable(t) {
|
|
22114
|
-
const b = this.paneBounds(t.paneId);
|
|
22115
|
-
const wrap = document.createElement("div");
|
|
22116
|
-
wrap.style.position = "absolute";
|
|
22117
|
-
if (t.frameColor && t.frameWidth > 0) wrap.style.border = `${t.frameWidth}px solid ${t.frameColor}`;
|
|
22118
|
-
this.anchor(wrap, t.position, b);
|
|
22119
|
-
const table = document.createElement("table");
|
|
22120
|
-
Object.assign(table.style, {
|
|
22121
|
-
borderCollapse: "collapse",
|
|
22122
|
-
background: t.bgColor ?? "transparent",
|
|
22123
|
-
fontFamily: this.theme.fontFamily || "sans-serif",
|
|
22124
|
-
border: "none",
|
|
22125
|
-
tableLayout: "auto",
|
|
22126
|
-
// Re-enable pointer events on the table only (root is none) so cell tooltips work.
|
|
22127
|
-
pointerEvents: "auto"
|
|
22128
|
-
});
|
|
22129
|
-
const { span, omit } = mergeRenderPlan(t);
|
|
22130
|
-
const plotW = Math.max(0, (this.root.clientWidth || this.container.clientWidth) - b.rightAxis);
|
|
22131
|
-
const paneH = b.height;
|
|
22132
|
-
const cellBorder = t.borderColor && t.borderWidth > 0 ? `${t.borderWidth}px solid ${t.borderColor}` : "none";
|
|
22133
|
-
for (let r = 0; r < t.rows; r += 1) {
|
|
22134
|
-
const tr = document.createElement("tr");
|
|
22135
|
-
for (let c = 0; c < t.columns; c += 1) {
|
|
22136
|
-
if (omit.has(`${r}:${c}`)) continue;
|
|
22137
|
-
const cell = t.cells[r]?.[c] ?? null;
|
|
22138
|
-
const td = document.createElement("td");
|
|
22139
|
-
if (cell === null) {
|
|
22140
|
-
Object.assign(td.style, { padding: "0", border: "none" });
|
|
22141
|
-
tr.appendChild(td);
|
|
22142
|
-
continue;
|
|
22143
|
-
}
|
|
22144
|
-
const sp = span.get(`${r}:${c}`);
|
|
22145
|
-
if (sp) {
|
|
22146
|
-
if (sp.cs > 1) td.colSpan = sp.cs;
|
|
22147
|
-
if (sp.rs > 1) td.rowSpan = sp.rs;
|
|
22148
|
-
}
|
|
22149
|
-
Object.assign(td.style, {
|
|
22150
|
-
border: cellBorder,
|
|
22151
|
-
padding: "2px 6px",
|
|
22152
|
-
background: cell.bgColor ?? "transparent",
|
|
22153
|
-
color: cell.textColor ?? this.theme.textColor,
|
|
22154
|
-
textAlign: cell.hAlign,
|
|
22155
|
-
verticalAlign: cell.vAlign === "top" ? "top" : cell.vAlign === "bottom" ? "bottom" : "middle",
|
|
22156
|
-
fontSize: `${fontPxOf(cell.textSize)}px`,
|
|
22157
|
-
fontFamily: cell.fontFamily === "monospace" ? "monospace" : "inherit",
|
|
22158
|
-
fontWeight: cell.bold ? "bold" : "normal",
|
|
22159
|
-
fontStyle: cell.italic ? "italic" : "normal",
|
|
22160
|
-
// Pine cell text never wraps; `\n` still breaks lines. Wrapping
|
|
22161
|
-
// used to collapse unicode sparklines and ━━━ dividers.
|
|
22162
|
-
whiteSpace: "pre"
|
|
22163
|
-
});
|
|
22164
|
-
if (cell.width) td.style.width = `${cell.width / 100 * plotW}px`;
|
|
22165
|
-
if (cell.height) td.style.height = `${cell.height / 100 * paneH}px`;
|
|
22166
|
-
if (cell.tooltip) td.title = cell.tooltip;
|
|
22167
|
-
td.textContent = cell.text ?? "";
|
|
22168
|
-
tr.appendChild(td);
|
|
22169
|
-
}
|
|
22170
|
-
table.appendChild(tr);
|
|
22171
|
-
}
|
|
22172
|
-
wrap.appendChild(table);
|
|
22173
|
-
return wrap;
|
|
22174
|
-
}
|
|
22175
|
-
/**
|
|
22176
|
-
* Position the wrapper at a Pine `position.*` corner/edge of the table's PANE
|
|
22177
|
-
* (not the whole chart), inset past the right price axis so it never overlaps
|
|
22178
|
-
* the Y-axis labels.
|
|
22179
|
-
*/
|
|
22180
|
-
anchor(el, position, b) {
|
|
22181
|
-
const m = 6;
|
|
22182
|
-
const containerH = this.root.clientHeight || this.container.clientHeight;
|
|
22183
|
-
const paneBottom = b.top + b.height;
|
|
22184
|
-
if (position.startsWith("top")) el.style.top = `${b.top + m}px`;
|
|
22185
|
-
else if (position.startsWith("bottom")) el.style.bottom = `${Math.max(0, containerH - paneBottom) + m}px`;
|
|
22186
|
-
else el.style.top = `${b.top + b.height / 2}px`;
|
|
22187
|
-
if (position.endsWith("left")) el.style.left = `${m}px`;
|
|
22188
|
-
else if (position.endsWith("right")) el.style.right = `${b.rightAxis + m}px`;
|
|
22189
|
-
else el.style.left = `calc(50% - ${b.rightAxis / 2}px)`;
|
|
22190
|
-
const tx = position.endsWith("center") ? "-50%" : "0";
|
|
22191
|
-
const ty = position.startsWith("middle") ? "-50%" : "0";
|
|
22192
|
-
if (tx !== "0" || ty !== "0") el.style.transform = `translate(${tx}, ${ty})`;
|
|
22193
|
-
}
|
|
22194
|
-
};
|
|
22195
|
-
|
|
22196
22294
|
// src/renderers/native/capabilities.ts
|
|
22197
22295
|
var NATIVE_CAPABILITIES = {
|
|
22198
22296
|
panes: true,
|
|
@@ -22213,7 +22311,7 @@ var NATIVE_CAPABILITIES = {
|
|
|
22213
22311
|
drawingDepth: true,
|
|
22214
22312
|
// drawings share the series' z space (backend-composited interleave layers)
|
|
22215
22313
|
tables: true,
|
|
22216
|
-
//
|
|
22314
|
+
// canvas-painted into the owning indicator's interleave slice
|
|
22217
22315
|
trades: true,
|
|
22218
22316
|
// strategy order-fill markers (arrows + labels + fill-price ticks)
|
|
22219
22317
|
inputsUI: true
|
|
@@ -22244,6 +22342,9 @@ function candleTier(spacing) {
|
|
|
22244
22342
|
if (spacing < CANDLE_BODY_MIN_SPACING) return "wick";
|
|
22245
22343
|
return "full";
|
|
22246
22344
|
}
|
|
22345
|
+
function snapY(yCss, dpr) {
|
|
22346
|
+
return Math.round(yCss * dpr) / dpr;
|
|
22347
|
+
}
|
|
22247
22348
|
function candleGeometry(xCss, spacing, dpr, bodyScale = 1) {
|
|
22248
22349
|
const wickDev = Math.max(1, Math.round(wickWidth(spacing) * dpr));
|
|
22249
22350
|
const wickLeftDev = Math.round(xCss * dpr - wickDev / 2);
|
|
@@ -22767,11 +22868,9 @@ var WebGL2Backend = class {
|
|
|
22767
22868
|
};
|
|
22768
22869
|
b.alpha = this.modelAlpha;
|
|
22769
22870
|
for (const m of models) for (const bgSpan of m.backgrounds) if (bgSpan.overlay !== true) this.emitBackground(b, bgSpan, pane, coords);
|
|
22770
|
-
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));
|
|
22771
22871
|
if (isPrice) {
|
|
22772
22872
|
for (const m of scene.indicators.values()) {
|
|
22773
22873
|
for (const bgSpan of m.backgrounds) if (bgSpan.overlay === true) this.emitBackground(b, bgSpan, pane, coords);
|
|
22774
|
-
for (const f of m.fills) if (f.overlay === true) this.emitFill(b, m, f, pane, coords, i0, i1, scene.offsetOf(m.id));
|
|
22775
22874
|
}
|
|
22776
22875
|
}
|
|
22777
22876
|
const drawCandles = isPrice && !scene.candlesHidden;
|
|
@@ -22787,6 +22886,7 @@ var WebGL2Backend = class {
|
|
|
22787
22886
|
b.alpha = this.modelAlpha;
|
|
22788
22887
|
const off = scene.offsetOf(m.id);
|
|
22789
22888
|
const mp = effPane(m);
|
|
22889
|
+
for (const f of m.fills) if (f.overlay !== true) this.emitFill(b, m, f, mp, coords, i0, i1, off);
|
|
22790
22890
|
for (const s of m.series) if (s.overlay !== true) this.emitSeries(b, s, mp, coords, i0, i1, theme, off);
|
|
22791
22891
|
}
|
|
22792
22892
|
if (drawCandles && !candleDrawn) {
|
|
@@ -22794,14 +22894,18 @@ var WebGL2Backend = class {
|
|
|
22794
22894
|
b.alpha = this.candleStructureAlpha;
|
|
22795
22895
|
this.emitPriceSeries(b, scene, i0, i1, coords, pane, theme, barColorMap, dataW);
|
|
22796
22896
|
}
|
|
22797
|
-
drawSlicesUpTo(Infinity);
|
|
22798
22897
|
b.alpha = this.modelAlpha;
|
|
22799
22898
|
if (isPrice) {
|
|
22899
|
+
for (const m of scene.indicators.values()) {
|
|
22900
|
+
const off = scene.offsetOf(m.id);
|
|
22901
|
+
for (const f of m.fills) if (f.overlay === true) this.emitFill(b, m, f, pane, coords, i0, i1, off);
|
|
22902
|
+
}
|
|
22800
22903
|
for (const m of scene.indicators.values()) {
|
|
22801
22904
|
const off = scene.offsetOf(m.id);
|
|
22802
22905
|
for (const s of m.series) if (s.overlay === true) this.emitSeries(b, s, pane, coords, i0, i1, theme, off);
|
|
22803
22906
|
}
|
|
22804
22907
|
}
|
|
22908
|
+
drawSlicesUpTo(Infinity);
|
|
22805
22909
|
for (const m of models) {
|
|
22806
22910
|
const mp = effPane(m);
|
|
22807
22911
|
for (const pl of m.priceLines) this.emitHline(b, pl, mp, coords, dataW, theme);
|
|
@@ -23196,12 +23300,12 @@ var WebGL2Backend = class {
|
|
|
23196
23300
|
if (drawBody) {
|
|
23197
23301
|
const oY = coords.priceToY(bar.open, pane.scale, pane.bounds);
|
|
23198
23302
|
const cY = coords.priceToY(bar.close, pane.scale, pane.bounds);
|
|
23199
|
-
bodyTop = Math.min(oY, cY);
|
|
23200
|
-
bodyH = Math.max(1, Math.
|
|
23303
|
+
bodyTop = snapY(Math.min(oY, cY), coords.dpr);
|
|
23304
|
+
bodyH = Math.max(1 / coords.dpr, snapY(Math.max(oY, cY), coords.dpr) - bodyTop);
|
|
23201
23305
|
}
|
|
23202
23306
|
if (cs.wickVisible) {
|
|
23203
|
-
const hY = coords.priceToY(bar.high, pane.scale, pane.bounds);
|
|
23204
|
-
const lY = coords.priceToY(bar.low, pane.scale, pane.bounds);
|
|
23307
|
+
const hY = snapY(coords.priceToY(bar.high, pane.scale, pane.bounds), coords.dpr);
|
|
23308
|
+
const lY = snapY(coords.priceToY(bar.low, pane.scale, pane.bounds), coords.dpr);
|
|
23205
23309
|
b.alpha = this.candleStructureAlpha;
|
|
23206
23310
|
const wCol = parseColor((isUp ? cs.wickUpColor : cs.wickDownColor) ?? (drawBody ? dir : bodyColorStr));
|
|
23207
23311
|
if (drawBody) {
|
|
@@ -24534,9 +24638,10 @@ var SceneGraph = class {
|
|
|
24534
24638
|
* so each indicator arrives behind the candles (and behind older indicators);
|
|
24535
24639
|
* `setIndicatorZ`/`bringToFront`/`sendToBack` change it. */
|
|
24536
24640
|
this.seriesZ = /* @__PURE__ */ new Map();
|
|
24537
|
-
/** Per-pane raster layers of
|
|
24641
|
+
/** Per-pane raster layers of drawings interleaved into the series stack (each
|
|
24642
|
+
* indicator's Pine drawings at its model's z, plus in-stack user drawings) — each a
|
|
24538
24643
|
* prepainted canvas the backend composites just before the series carrying `beforeZ`.
|
|
24539
|
-
* Rebuilt by the renderer per data frame
|
|
24644
|
+
* Rebuilt by the renderer per data frame. */
|
|
24540
24645
|
this.drawingSlices = /* @__PURE__ */ new Map();
|
|
24541
24646
|
/** Per-model index offset: the chart bar index of the model's `anchorTime` — its
|
|
24542
24647
|
* index-aligned payloads (dense series arrays, `bar_index` drawings) count from that
|
|
@@ -24771,11 +24876,9 @@ var Canvas2dBackend = class {
|
|
|
24771
24876
|
};
|
|
24772
24877
|
ctx.globalAlpha = this.modelAlpha;
|
|
24773
24878
|
for (const m of models) for (const bg of m.backgrounds) if (bg.overlay !== true) this.drawBackground(ctx, bg, pane, coords);
|
|
24774
|
-
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));
|
|
24775
24879
|
if (isPrice) {
|
|
24776
24880
|
for (const m of scene.indicators.values()) {
|
|
24777
24881
|
for (const bg of m.backgrounds) if (bg.overlay === true) this.drawBackground(ctx, bg, pane, coords);
|
|
24778
|
-
for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0, i1, scene.offsetOf(m.id));
|
|
24779
24882
|
}
|
|
24780
24883
|
}
|
|
24781
24884
|
const slices = scene.drawingSlices.get(pane.id) ?? [];
|
|
@@ -24799,6 +24902,7 @@ var Canvas2dBackend = class {
|
|
|
24799
24902
|
ctx.globalAlpha = this.modelAlpha;
|
|
24800
24903
|
const off = scene.offsetOf(m.id);
|
|
24801
24904
|
const mp = effPane(m);
|
|
24905
|
+
for (const f of m.fills) if (f.overlay !== true) this.drawFill(ctx, m, f, mp, coords, i0, i1, off);
|
|
24802
24906
|
for (const s of m.series) if (s.overlay !== true) this.drawSeries(ctx, s, mp, coords, i0, i1, theme, off);
|
|
24803
24907
|
}
|
|
24804
24908
|
if (drawCandles && !candleDrawn) {
|
|
@@ -24806,14 +24910,18 @@ var Canvas2dBackend = class {
|
|
|
24806
24910
|
ctx.globalAlpha = this.candleStructureAlpha;
|
|
24807
24911
|
this.drawPriceSeries(ctx, scene, i0, i1, coords, pane, theme, barColorMap, dataW);
|
|
24808
24912
|
}
|
|
24809
|
-
drawSlicesUpTo(Infinity);
|
|
24810
24913
|
if (isPrice) {
|
|
24811
24914
|
ctx.globalAlpha = this.modelAlpha;
|
|
24915
|
+
for (const m of scene.indicators.values()) {
|
|
24916
|
+
const off = scene.offsetOf(m.id);
|
|
24917
|
+
for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0, i1, off);
|
|
24918
|
+
}
|
|
24812
24919
|
for (const m of scene.indicators.values()) {
|
|
24813
24920
|
const off = scene.offsetOf(m.id);
|
|
24814
24921
|
for (const s of m.series) if (s.overlay === true) this.drawSeries(ctx, s, pane, coords, i0, i1, theme, off);
|
|
24815
24922
|
}
|
|
24816
24923
|
}
|
|
24924
|
+
drawSlicesUpTo(Infinity);
|
|
24817
24925
|
ctx.globalAlpha = this.modelAlpha;
|
|
24818
24926
|
for (const m of models) {
|
|
24819
24927
|
const mp = effPane(m);
|
|
@@ -25057,13 +25165,13 @@ var Canvas2dBackend = class {
|
|
|
25057
25165
|
if (drawBody) {
|
|
25058
25166
|
const oY = coords.priceToY(b.open, pane.scale, pane.bounds);
|
|
25059
25167
|
const cY = coords.priceToY(b.close, pane.scale, pane.bounds);
|
|
25060
|
-
top = Math.min(oY, cY);
|
|
25061
|
-
bodyH = Math.max(1, Math.
|
|
25168
|
+
top = snapY(Math.min(oY, cY), coords.dpr);
|
|
25169
|
+
bodyH = Math.max(1 / coords.dpr, snapY(Math.max(oY, cY), coords.dpr) - top);
|
|
25062
25170
|
}
|
|
25063
25171
|
if (cs.wickVisible) {
|
|
25064
25172
|
const wick = (up ? cs.wickUpColor : cs.wickDownColor) ?? (drawBody ? dir : color);
|
|
25065
|
-
const hY = coords.priceToY(b.high, pane.scale, pane.bounds);
|
|
25066
|
-
const lY = coords.priceToY(b.low, pane.scale, pane.bounds);
|
|
25173
|
+
const hY = snapY(coords.priceToY(b.high, pane.scale, pane.bounds), coords.dpr);
|
|
25174
|
+
const lY = snapY(coords.priceToY(b.low, pane.scale, pane.bounds), coords.dpr);
|
|
25067
25175
|
ctx.globalAlpha = this.candleStructureAlpha;
|
|
25068
25176
|
ctx.strokeStyle = wick;
|
|
25069
25177
|
ctx.lineWidth = g.wickW;
|
|
@@ -25510,6 +25618,19 @@ function autoFontSize(lines, boxW, boxH, bold) {
|
|
|
25510
25618
|
|
|
25511
25619
|
// src/renderers/shared/DrawingSceneRenderer.ts
|
|
25512
25620
|
var EMPTY_DRAWING_SET = { lines: [], boxes: [], labels: [], polylines: [], linefills: [] };
|
|
25621
|
+
function modelDrawingSet(m, overlay) {
|
|
25622
|
+
const want = (d) => Boolean(d.overlay) === overlay;
|
|
25623
|
+
return {
|
|
25624
|
+
lines: (m.lines ?? []).filter(want),
|
|
25625
|
+
boxes: (m.boxes ?? []).filter(want),
|
|
25626
|
+
labels: (m.labels ?? []).filter(want),
|
|
25627
|
+
polylines: (m.polylines ?? []).filter(want),
|
|
25628
|
+
linefills: (m.linefills ?? []).filter(want)
|
|
25629
|
+
};
|
|
25630
|
+
}
|
|
25631
|
+
function drawingSetEmpty(s) {
|
|
25632
|
+
return !s.lines.length && !s.boxes.length && !s.labels.length && !s.polylines.length && !s.linefills.length;
|
|
25633
|
+
}
|
|
25513
25634
|
function fontSizePx(size) {
|
|
25514
25635
|
return size === "auto" ? 12 : namedFontSize(size);
|
|
25515
25636
|
}
|
|
@@ -26362,10 +26483,8 @@ var ChromeRenderer = class {
|
|
|
26362
26483
|
this.ctx = null;
|
|
26363
26484
|
// The color for axis tick labels — the host-passed surface text, set each frame in render().
|
|
26364
26485
|
this.axisTextColor = DARK_THEME.textColor;
|
|
26365
|
-
// Shared Pine-drawing renderer
|
|
26486
|
+
// Shared Pine-drawing renderer, used here for autoscale geometry only; widthCache persists.
|
|
26366
26487
|
this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
|
|
26367
|
-
// Tooltip hit-rects of every label drawn this frame, in plot coords (rebuilt per render).
|
|
26368
|
-
this.labelTips = [];
|
|
26369
26488
|
}
|
|
26370
26489
|
mount(canvas) {
|
|
26371
26490
|
this.canvas = canvas;
|
|
@@ -26389,8 +26508,8 @@ var ChromeRenderer = class {
|
|
|
26389
26508
|
*/
|
|
26390
26509
|
paneDrawingsRange(ownModels, scene, isPricePane, vr) {
|
|
26391
26510
|
let dr = null;
|
|
26392
|
-
for (const m of ownModels) dr = unionRange(dr, this.drawingsRange(
|
|
26393
|
-
if (isPricePane) for (const m of scene.indicators.values()) dr = unionRange(dr, this.drawingsRange(
|
|
26511
|
+
for (const m of ownModels) dr = unionRange(dr, this.drawingsRange(modelDrawingSet(m, false), vr, scene.offsetOf(m.id)));
|
|
26512
|
+
if (isPricePane) for (const m of scene.indicators.values()) dr = unionRange(dr, this.drawingsRange(modelDrawingSet(m, true), vr, scene.offsetOf(m.id)));
|
|
26394
26513
|
return dr;
|
|
26395
26514
|
}
|
|
26396
26515
|
/** Clear the chrome canvas and draw drawings + axes + current-price line.
|
|
@@ -26407,7 +26526,6 @@ var ChromeRenderer = class {
|
|
|
26407
26526
|
const dataW = coords.width;
|
|
26408
26527
|
const dataH = coords.height;
|
|
26409
26528
|
this.axisTextColor = surface?.textColor ?? theme.textColor;
|
|
26410
|
-
this.labelTips = [];
|
|
26411
26529
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
26412
26530
|
ctx.clearRect(0, 0, fullW, fullH);
|
|
26413
26531
|
if (surface && (fullW > dataW || fullH > dataH)) {
|
|
@@ -26423,17 +26541,6 @@ var ChromeRenderer = class {
|
|
|
26423
26541
|
return;
|
|
26424
26542
|
}
|
|
26425
26543
|
const pricePane = panes.find((p) => p.kind === "price") ?? null;
|
|
26426
|
-
for (const pane of panes) {
|
|
26427
|
-
if (pane.collapsed) continue;
|
|
26428
|
-
for (const m of scene.indicatorsForPane(pane.id)) {
|
|
26429
|
-
const sc = scene.scaleFor(m, pane);
|
|
26430
|
-
const mp = sc === pane.scale ? pane : { ...pane, scale: sc };
|
|
26431
|
-
this.renderDrawings(ctx, coords, this.ownDrawings(m), mp, dataW, scene.offsetOf(m.id));
|
|
26432
|
-
}
|
|
26433
|
-
}
|
|
26434
|
-
if (pricePane) {
|
|
26435
|
-
for (const m of scene.indicators.values()) this.renderDrawings(ctx, coords, this.overlayDrawings(m), pricePane, dataW, scene.offsetOf(m.id));
|
|
26436
|
-
}
|
|
26437
26544
|
if (pricePane && !pricePane.collapsed && scene.tradeMarkers.visible) {
|
|
26438
26545
|
for (const m of scene.indicators.values()) {
|
|
26439
26546
|
if (m.trades?.length) this.renderTrades(ctx, coords, scene, theme, m.trades, pricePane, dataW);
|
|
@@ -26449,25 +26556,6 @@ var ChromeRenderer = class {
|
|
|
26449
26556
|
this.canvas = null;
|
|
26450
26557
|
this.ctx = null;
|
|
26451
26558
|
}
|
|
26452
|
-
// ── Pine-drawing helpers (own vs force_overlay routing) ──
|
|
26453
|
-
ownDrawings(m) {
|
|
26454
|
-
return {
|
|
26455
|
-
lines: (m.lines ?? []).filter((d) => !d.overlay),
|
|
26456
|
-
boxes: (m.boxes ?? []).filter((d) => !d.overlay),
|
|
26457
|
-
labels: (m.labels ?? []).filter((d) => !d.overlay),
|
|
26458
|
-
polylines: (m.polylines ?? []).filter((d) => !d.overlay),
|
|
26459
|
-
linefills: (m.linefills ?? []).filter((d) => !d.overlay)
|
|
26460
|
-
};
|
|
26461
|
-
}
|
|
26462
|
-
overlayDrawings(m) {
|
|
26463
|
-
return {
|
|
26464
|
-
lines: (m.lines ?? []).filter((d) => d.overlay),
|
|
26465
|
-
boxes: (m.boxes ?? []).filter((d) => d.overlay),
|
|
26466
|
-
labels: (m.labels ?? []).filter((d) => d.overlay),
|
|
26467
|
-
polylines: (m.polylines ?? []).filter((d) => d.overlay),
|
|
26468
|
-
linefills: (m.linefills ?? []).filter((d) => d.overlay)
|
|
26469
|
-
};
|
|
26470
|
-
}
|
|
26471
26559
|
drawingsRange(set, vr, indexOffset = 0) {
|
|
26472
26560
|
this.drawScene.setSet(set, indexOffset);
|
|
26473
26561
|
if (this.drawScene.isEmpty()) return null;
|
|
@@ -26501,34 +26589,6 @@ var ChromeRenderer = class {
|
|
|
26501
26589
|
);
|
|
26502
26590
|
ctx.restore();
|
|
26503
26591
|
}
|
|
26504
|
-
renderDrawings(ctx, coords, set, pane, dataW, indexOffset = 0) {
|
|
26505
|
-
this.drawScene.setSet(set, indexOffset);
|
|
26506
|
-
if (this.drawScene.isEmpty()) return;
|
|
26507
|
-
ctx.save();
|
|
26508
|
-
ctx.translate(0, pane.bounds.top);
|
|
26509
|
-
ctx.beginPath();
|
|
26510
|
-
ctx.rect(0, 0, dataW, pane.bounds.height);
|
|
26511
|
-
ctx.clip();
|
|
26512
|
-
this.drawScene.render(
|
|
26513
|
-
ctx,
|
|
26514
|
-
dataW,
|
|
26515
|
-
pane.bounds.height,
|
|
26516
|
-
(l) => coords.logicalToX(l),
|
|
26517
|
-
(price) => coords.priceToY(price, pane.scale, pane.bounds) - pane.bounds.top
|
|
26518
|
-
);
|
|
26519
|
-
ctx.restore();
|
|
26520
|
-
for (const r of this.drawScene.labelTipRegions()) {
|
|
26521
|
-
this.labelTips.push({ ...r, top: r.top + pane.bounds.top, bottom: r.bottom + pane.bounds.top });
|
|
26522
|
-
}
|
|
26523
|
-
}
|
|
26524
|
-
/** Tooltip of the topmost label under a plot-space point, or null. Fed by the last render. */
|
|
26525
|
-
labelTooltipAt(x, y) {
|
|
26526
|
-
for (let i = this.labelTips.length - 1; i >= 0; i -= 1) {
|
|
26527
|
-
const r = this.labelTips[i];
|
|
26528
|
-
if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return r.text;
|
|
26529
|
-
}
|
|
26530
|
-
return null;
|
|
26531
|
-
}
|
|
26532
26592
|
// ── axes ──
|
|
26533
26593
|
drawPriceAxes(ctx, scene, coords, theme, dataW, panes) {
|
|
26534
26594
|
ctx.strokeStyle = scene.style.borderColor ?? theme.borderColor;
|
|
@@ -30847,17 +30907,17 @@ function glyphIcon(glyph) {
|
|
|
30847
30907
|
return textGlyph(String(glyph), 15);
|
|
30848
30908
|
}
|
|
30849
30909
|
function stampSizeIcon(size) {
|
|
30850
|
-
return textGlyph("\u25CF", (
|
|
30910
|
+
return textGlyph("\u25CF", (SIZE_PX3[String(size)] ?? 13) + 4);
|
|
30851
30911
|
}
|
|
30852
30912
|
function sizeIcon(size) {
|
|
30853
30913
|
return textGlyph(String(size).charAt(0).toUpperCase(), 15);
|
|
30854
30914
|
}
|
|
30855
|
-
var
|
|
30915
|
+
var SIZE_PX3 = { small: 10, normal: 13, large: 16, huge: 20 };
|
|
30856
30916
|
function numbersSizeIcon(size) {
|
|
30857
|
-
return textGlyph("12", (
|
|
30917
|
+
return textGlyph("12", (SIZE_PX3[String(size)] ?? 13) - 1, 16.5, 'font-weight="600"');
|
|
30858
30918
|
}
|
|
30859
30919
|
function labelSizeIcon(size) {
|
|
30860
|
-
return textGlyph("T", (
|
|
30920
|
+
return textGlyph("T", (SIZE_PX3[String(size)] ?? 13) + 2);
|
|
30861
30921
|
}
|
|
30862
30922
|
function capitalize(s) {
|
|
30863
30923
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
@@ -31832,6 +31892,315 @@ var UserDrawingController = class {
|
|
|
31832
31892
|
}
|
|
31833
31893
|
};
|
|
31834
31894
|
|
|
31895
|
+
// src/renderers/shared/TableOverlay.ts
|
|
31896
|
+
var SIZE_PX4 = {
|
|
31897
|
+
auto: 13,
|
|
31898
|
+
tiny: 10,
|
|
31899
|
+
small: 11,
|
|
31900
|
+
normal: 13,
|
|
31901
|
+
large: 16,
|
|
31902
|
+
huge: 20
|
|
31903
|
+
};
|
|
31904
|
+
function fontPxOf(size) {
|
|
31905
|
+
if (typeof size === "number") return size > 0 ? size : SIZE_PX4.auto;
|
|
31906
|
+
return SIZE_PX4[size] ?? SIZE_PX4.auto;
|
|
31907
|
+
}
|
|
31908
|
+
function tableHasContent(t) {
|
|
31909
|
+
return t.cells.some((row) => row?.some((c) => c != null && !c.merged));
|
|
31910
|
+
}
|
|
31911
|
+
function mergeRenderPlan(t) {
|
|
31912
|
+
const span = /* @__PURE__ */ new Map();
|
|
31913
|
+
const omit = /* @__PURE__ */ new Set();
|
|
31914
|
+
for (const m of t.merges) {
|
|
31915
|
+
span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
|
|
31916
|
+
for (let r = m.startRow; r <= m.endRow; r += 1) {
|
|
31917
|
+
for (let c = m.startCol; c <= m.endCol; c += 1) {
|
|
31918
|
+
if (r !== m.startRow || c !== m.startCol) omit.add(`${r}:${c}`);
|
|
31919
|
+
}
|
|
31920
|
+
}
|
|
31921
|
+
}
|
|
31922
|
+
for (let r = 0; r < t.rows; r += 1) {
|
|
31923
|
+
for (let c = 0; c < t.columns; c += 1) {
|
|
31924
|
+
if (t.cells[r]?.[c]?.merged && !span.has(`${r}:${c}`)) omit.add(`${r}:${c}`);
|
|
31925
|
+
}
|
|
31926
|
+
}
|
|
31927
|
+
for (const key of span.keys()) omit.delete(key);
|
|
31928
|
+
return { span, omit };
|
|
31929
|
+
}
|
|
31930
|
+
|
|
31931
|
+
// src/renderers/shared/TableCanvasRenderer.ts
|
|
31932
|
+
var PAD_X = 6;
|
|
31933
|
+
var PAD_Y = 2;
|
|
31934
|
+
var MARGIN = 6;
|
|
31935
|
+
var LINE_HEIGHT = 1.2;
|
|
31936
|
+
function paintTable(ctx, t, args, tips) {
|
|
31937
|
+
if (!tableHasContent(t)) return;
|
|
31938
|
+
const layout = layoutTable(ctx, t, args);
|
|
31939
|
+
if (!layout || layout.w <= 0 || layout.h <= 0) return;
|
|
31940
|
+
const fw = t.frameColor && t.frameWidth > 0 ? t.frameWidth : 0;
|
|
31941
|
+
const { x, y } = anchorOrigin(t.position, layout.w + 2 * fw, layout.h + 2 * fw, args);
|
|
31942
|
+
const x0 = x + fw;
|
|
31943
|
+
const y0 = y + fw;
|
|
31944
|
+
if (t.bgColor) {
|
|
31945
|
+
ctx.fillStyle = t.bgColor;
|
|
31946
|
+
ctx.fillRect(x0, y0, layout.w, layout.h);
|
|
31947
|
+
}
|
|
31948
|
+
if (fw > 0 && t.frameColor) {
|
|
31949
|
+
ctx.strokeStyle = t.frameColor;
|
|
31950
|
+
ctx.lineWidth = fw;
|
|
31951
|
+
ctx.strokeRect(x + fw / 2, y + fw / 2, layout.w + fw, layout.h + fw);
|
|
31952
|
+
}
|
|
31953
|
+
const colX = [0];
|
|
31954
|
+
for (const w of layout.colW) colX.push(colX[colX.length - 1] + w);
|
|
31955
|
+
const rowY = [0];
|
|
31956
|
+
for (const h of layout.rowH) rowY.push(rowY[rowY.length - 1] + h);
|
|
31957
|
+
const prevBaseline = ctx.textBaseline;
|
|
31958
|
+
const prevAlign = ctx.textAlign;
|
|
31959
|
+
ctx.textBaseline = "middle";
|
|
31960
|
+
for (const box of layout.boxes) {
|
|
31961
|
+
const rx = x0 + colX[box.c];
|
|
31962
|
+
const ry = y0 + rowY[box.r];
|
|
31963
|
+
const rw = colX[box.c + box.cs] - colX[box.c];
|
|
31964
|
+
const rh = rowY[box.r + box.rs] - rowY[box.r];
|
|
31965
|
+
const cell = box.cell;
|
|
31966
|
+
if (cell.bgColor) {
|
|
31967
|
+
ctx.fillStyle = cell.bgColor;
|
|
31968
|
+
ctx.fillRect(rx, ry, rw, rh);
|
|
31969
|
+
}
|
|
31970
|
+
const text = cell.text ?? "";
|
|
31971
|
+
if (text.length > 0) {
|
|
31972
|
+
const px = fontPxOf(cell.textSize);
|
|
31973
|
+
ctx.font = cellFont(cell, px, args.theme);
|
|
31974
|
+
ctx.fillStyle = cell.textColor ?? args.theme.textColor;
|
|
31975
|
+
const lines = text.split("\n");
|
|
31976
|
+
const blockH = lines.length * px * LINE_HEIGHT;
|
|
31977
|
+
const blockTop = cell.vAlign === "top" ? ry + PAD_Y : cell.vAlign === "bottom" ? ry + rh - PAD_Y - blockH : ry + (rh - blockH) / 2;
|
|
31978
|
+
const tx = cell.hAlign === "left" ? rx + PAD_X : cell.hAlign === "right" ? rx + rw - PAD_X : rx + rw / 2;
|
|
31979
|
+
ctx.textAlign = cell.hAlign;
|
|
31980
|
+
lines.forEach((line, i) => ctx.fillText(line, tx, blockTop + (i + 0.5) * px * LINE_HEIGHT));
|
|
31981
|
+
}
|
|
31982
|
+
if (cell.tooltip) tips.push({ left: rx, top: ry, right: rx + rw, bottom: ry + rh, text: cell.tooltip });
|
|
31983
|
+
}
|
|
31984
|
+
ctx.textBaseline = prevBaseline;
|
|
31985
|
+
ctx.textAlign = prevAlign;
|
|
31986
|
+
if (t.borderColor && t.borderWidth > 0) {
|
|
31987
|
+
ctx.strokeStyle = t.borderColor;
|
|
31988
|
+
ctx.lineWidth = t.borderWidth;
|
|
31989
|
+
const seen = /* @__PURE__ */ new Set();
|
|
31990
|
+
ctx.beginPath();
|
|
31991
|
+
const edge = (ax, ay, bx, by) => {
|
|
31992
|
+
const key = `${ax},${ay},${bx},${by}`;
|
|
31993
|
+
if (seen.has(key)) return;
|
|
31994
|
+
seen.add(key);
|
|
31995
|
+
ctx.moveTo(ax, ay);
|
|
31996
|
+
ctx.lineTo(bx, by);
|
|
31997
|
+
};
|
|
31998
|
+
for (const box of layout.boxes) {
|
|
31999
|
+
const l = Math.round(x0 + colX[box.c]);
|
|
32000
|
+
const r = Math.round(x0 + colX[box.c + box.cs]);
|
|
32001
|
+
const tp = Math.round(y0 + rowY[box.r]);
|
|
32002
|
+
const bt = Math.round(y0 + rowY[box.r + box.rs]);
|
|
32003
|
+
edge(l, tp, r, tp);
|
|
32004
|
+
edge(l, bt, r, bt);
|
|
32005
|
+
edge(l, tp, l, bt);
|
|
32006
|
+
edge(r, tp, r, bt);
|
|
32007
|
+
}
|
|
32008
|
+
ctx.stroke();
|
|
32009
|
+
}
|
|
32010
|
+
}
|
|
32011
|
+
function layoutTable(ctx, t, args) {
|
|
32012
|
+
const { span, omit } = mergeRenderPlan(t);
|
|
32013
|
+
const colW = new Array(t.columns).fill(0);
|
|
32014
|
+
const rowH = new Array(t.rows).fill(0);
|
|
32015
|
+
const boxes = [];
|
|
32016
|
+
for (let r = 0; r < t.rows; r += 1) {
|
|
32017
|
+
for (let c = 0; c < t.columns; c += 1) {
|
|
32018
|
+
if (omit.has(`${r}:${c}`)) continue;
|
|
32019
|
+
const cell = t.cells[r]?.[c];
|
|
32020
|
+
if (cell == null) continue;
|
|
32021
|
+
const sp = span.get(`${r}:${c}`);
|
|
32022
|
+
boxes.push({ cell, r, c, cs: Math.min(sp?.cs ?? 1, t.columns - c), rs: Math.min(sp?.rs ?? 1, t.rows - r) });
|
|
32023
|
+
}
|
|
32024
|
+
}
|
|
32025
|
+
if (boxes.length === 0) return null;
|
|
32026
|
+
const sizeOf = (cell) => {
|
|
32027
|
+
const px = fontPxOf(cell.textSize);
|
|
32028
|
+
ctx.font = cellFont(cell, px, args.theme);
|
|
32029
|
+
const lines = (cell.text ?? "").split("\n");
|
|
32030
|
+
let maxW = 0;
|
|
32031
|
+
for (const line of lines) maxW = Math.max(maxW, ctx.measureText(line).width);
|
|
32032
|
+
let w2 = Math.ceil(maxW) + 2 * PAD_X;
|
|
32033
|
+
let h2 = Math.ceil(lines.length * px * LINE_HEIGHT) + 2 * PAD_Y;
|
|
32034
|
+
if (cell.width) w2 = Math.max(w2, cell.width / 100 * args.plotWidth);
|
|
32035
|
+
if (cell.height) h2 = Math.max(h2, cell.height / 100 * args.paneHeight);
|
|
32036
|
+
return { w: w2, h: h2 };
|
|
32037
|
+
};
|
|
32038
|
+
const spanning = [];
|
|
32039
|
+
for (const box of boxes) {
|
|
32040
|
+
const { w: w2, h: h2 } = sizeOf(box.cell);
|
|
32041
|
+
if (box.cs === 1) colW[box.c] = Math.max(colW[box.c], w2);
|
|
32042
|
+
if (box.rs === 1) rowH[box.r] = Math.max(rowH[box.r], h2);
|
|
32043
|
+
if (box.cs > 1 || box.rs > 1) spanning.push({ box, w: w2, h: h2 });
|
|
32044
|
+
}
|
|
32045
|
+
for (const { box, w: w2, h: h2 } of spanning) {
|
|
32046
|
+
if (box.cs > 1) {
|
|
32047
|
+
let sum = 0;
|
|
32048
|
+
for (let c = box.c; c < box.c + box.cs; c += 1) sum += colW[c];
|
|
32049
|
+
if (w2 > sum) for (let c = box.c; c < box.c + box.cs; c += 1) colW[c] += (w2 - sum) / box.cs;
|
|
32050
|
+
}
|
|
32051
|
+
if (box.rs > 1) {
|
|
32052
|
+
let sum = 0;
|
|
32053
|
+
for (let r = box.r; r < box.r + box.rs; r += 1) sum += rowH[r];
|
|
32054
|
+
if (h2 > sum) for (let r = box.r; r < box.r + box.rs; r += 1) rowH[r] += (h2 - sum) / box.rs;
|
|
32055
|
+
}
|
|
32056
|
+
}
|
|
32057
|
+
let w = 0;
|
|
32058
|
+
for (const cw of colW) w += cw;
|
|
32059
|
+
let h = 0;
|
|
32060
|
+
for (const rh of rowH) h += rh;
|
|
32061
|
+
return { colW, rowH, w, h, boxes };
|
|
32062
|
+
}
|
|
32063
|
+
function anchorOrigin(position, totalW, totalH, args) {
|
|
32064
|
+
let y;
|
|
32065
|
+
if (position.startsWith("top")) y = MARGIN;
|
|
32066
|
+
else if (position.startsWith("bottom")) y = args.paneHeight - MARGIN - totalH;
|
|
32067
|
+
else y = args.paneHeight / 2 - totalH / 2;
|
|
32068
|
+
let x;
|
|
32069
|
+
if (position.endsWith("left")) x = MARGIN;
|
|
32070
|
+
else if (position.endsWith("right")) x = args.plotWidth - MARGIN - totalW;
|
|
32071
|
+
else x = args.plotWidth / 2 - totalW / 2;
|
|
32072
|
+
return { x, y };
|
|
32073
|
+
}
|
|
32074
|
+
function cellFont(cell, px, theme) {
|
|
32075
|
+
const family = cell.fontFamily === "monospace" ? "monospace" : theme.fontFamily || "sans-serif";
|
|
32076
|
+
return `${cell.italic ? "italic " : ""}${cell.bold ? "bold " : ""}${px}px ${family}`;
|
|
32077
|
+
}
|
|
32078
|
+
|
|
32079
|
+
// src/renderers/native/drawings/IndicatorDrawingSlices.ts
|
|
32080
|
+
function indicatorSliceKey(z, boundaries) {
|
|
32081
|
+
return boundaries.find((b) => b > z) ?? Infinity;
|
|
32082
|
+
}
|
|
32083
|
+
var IndicatorDrawingSlices = class {
|
|
32084
|
+
constructor() {
|
|
32085
|
+
this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
|
|
32086
|
+
/** Slice canvas cache, keyed `paneId|beforeZ` — same lifecycle as the user-drawing cache. */
|
|
32087
|
+
this.sliceCache = /* @__PURE__ */ new Map();
|
|
32088
|
+
/** Tooltip hit-rects of every label drawn this frame, in plot coords (rebuilt per prepare). */
|
|
32089
|
+
this.tips = [];
|
|
32090
|
+
}
|
|
32091
|
+
/**
|
|
32092
|
+
* Rebuild the per-indicator drawing slices for this data frame. `ref` is the data
|
|
32093
|
+
* canvas the slices must match pixel-for-pixel (the backend composites them 1:1).
|
|
32094
|
+
* Runs from the renderer's data paint, just before the backend composites the scene.
|
|
32095
|
+
*/
|
|
32096
|
+
prepare(scene, coords, theme, ref) {
|
|
32097
|
+
this.tips = [];
|
|
32098
|
+
const out = /* @__PURE__ */ new Map();
|
|
32099
|
+
if (ref.width === 0 || ref.height === 0) {
|
|
32100
|
+
this.sliceCache.clear();
|
|
32101
|
+
return out;
|
|
32102
|
+
}
|
|
32103
|
+
this.drawScene.setDeps({
|
|
32104
|
+
timeToLogical: (ms) => coords.timeToLogical(ms),
|
|
32105
|
+
barAt: (logical) => {
|
|
32106
|
+
const b = scene.bars[Math.round(logical)];
|
|
32107
|
+
return b ? { high: b.high, low: b.low } : null;
|
|
32108
|
+
},
|
|
32109
|
+
theme
|
|
32110
|
+
});
|
|
32111
|
+
const dpr = coords.dpr;
|
|
32112
|
+
const dataW = coords.width;
|
|
32113
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
32114
|
+
const add = (paneId, beforeZ, entry) => {
|
|
32115
|
+
const key = `${paneId}|${beforeZ}`;
|
|
32116
|
+
const bucket = buckets.get(key);
|
|
32117
|
+
if (bucket) bucket.entries.push(entry);
|
|
32118
|
+
else buckets.set(key, { paneId, beforeZ, entries: [entry] });
|
|
32119
|
+
};
|
|
32120
|
+
for (const pane of scene.orderedPanes()) {
|
|
32121
|
+
if (pane.collapsed) continue;
|
|
32122
|
+
const boundaries = scene.seriesBoundaries(pane.id);
|
|
32123
|
+
for (const m of scene.orderedIndicatorsForPane(pane.id)) {
|
|
32124
|
+
const set = modelDrawingSet(m, false);
|
|
32125
|
+
const tables = (m.tables ?? []).filter((t) => !t.overlay);
|
|
32126
|
+
if (drawingSetEmpty(set) && tables.length === 0) continue;
|
|
32127
|
+
const sc = scene.scaleFor(m, pane);
|
|
32128
|
+
const mp = sc === pane.scale ? pane : { ...pane, scale: sc };
|
|
32129
|
+
const beforeZ = indicatorSliceKey(scene.zOf(m.id), boundaries);
|
|
32130
|
+
add(pane.id, beforeZ, { set, tables, pane: mp, indexOffset: scene.offsetOf(m.id) });
|
|
32131
|
+
}
|
|
32132
|
+
if (pane.kind === "price") {
|
|
32133
|
+
for (const m of scene.indicators.values()) {
|
|
32134
|
+
const set = modelDrawingSet(m, true);
|
|
32135
|
+
const tables = (m.tables ?? []).filter((t) => t.overlay === true);
|
|
32136
|
+
if (drawingSetEmpty(set) && tables.length === 0) continue;
|
|
32137
|
+
add(pane.id, Infinity, { set, tables, pane, indexOffset: scene.offsetOf(m.id) });
|
|
32138
|
+
}
|
|
32139
|
+
}
|
|
32140
|
+
}
|
|
32141
|
+
for (const [key, { paneId, beforeZ, entries }] of buckets) {
|
|
32142
|
+
let canvas = this.sliceCache.get(key);
|
|
32143
|
+
if (!canvas) {
|
|
32144
|
+
canvas = document.createElement("canvas");
|
|
32145
|
+
this.sliceCache.set(key, canvas);
|
|
32146
|
+
}
|
|
32147
|
+
if (canvas.width !== ref.width || canvas.height !== ref.height) {
|
|
32148
|
+
canvas.width = ref.width;
|
|
32149
|
+
canvas.height = ref.height;
|
|
32150
|
+
}
|
|
32151
|
+
const ctx = canvas.getContext("2d");
|
|
32152
|
+
if (!ctx) continue;
|
|
32153
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
32154
|
+
ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
|
|
32155
|
+
for (const e of entries) this.paintEntry(ctx, e, coords, dataW, theme);
|
|
32156
|
+
const slices = out.get(paneId) ?? [];
|
|
32157
|
+
slices.push({ beforeZ, canvas });
|
|
32158
|
+
out.set(paneId, slices);
|
|
32159
|
+
}
|
|
32160
|
+
for (const key of [...this.sliceCache.keys()]) if (!buckets.has(key)) this.sliceCache.delete(key);
|
|
32161
|
+
for (const slices of out.values()) slices.sort((a, b) => a.beforeZ - b.beforeZ);
|
|
32162
|
+
return out;
|
|
32163
|
+
}
|
|
32164
|
+
paintEntry(ctx, e, coords, dataW, theme) {
|
|
32165
|
+
const { pane } = e;
|
|
32166
|
+
const paneTips = [];
|
|
32167
|
+
ctx.save();
|
|
32168
|
+
ctx.translate(0, pane.bounds.top);
|
|
32169
|
+
ctx.beginPath();
|
|
32170
|
+
ctx.rect(0, 0, dataW, pane.bounds.height);
|
|
32171
|
+
ctx.clip();
|
|
32172
|
+
this.drawScene.setSet(e.set, e.indexOffset);
|
|
32173
|
+
this.drawScene.render(
|
|
32174
|
+
ctx,
|
|
32175
|
+
dataW,
|
|
32176
|
+
pane.bounds.height,
|
|
32177
|
+
(l) => coords.logicalToX(l),
|
|
32178
|
+
(price) => coords.priceToY(price, pane.scale, pane.bounds) - pane.bounds.top
|
|
32179
|
+
);
|
|
32180
|
+
paneTips.push(...this.drawScene.labelTipRegions());
|
|
32181
|
+
for (const t of e.tables) paintTable(ctx, t, { paneHeight: pane.bounds.height, plotWidth: dataW, theme }, paneTips);
|
|
32182
|
+
ctx.restore();
|
|
32183
|
+
for (const r of paneTips) {
|
|
32184
|
+
this.tips.push({ ...r, top: r.top + pane.bounds.top, bottom: r.bottom + pane.bounds.top });
|
|
32185
|
+
}
|
|
32186
|
+
}
|
|
32187
|
+
/** Tooltip of the topmost label or table cell under a plot-space point, or null. Fed by the last prepare. */
|
|
32188
|
+
labelTooltipAt(x, y) {
|
|
32189
|
+
for (let i = this.tips.length - 1; i >= 0; i -= 1) {
|
|
32190
|
+
const r = this.tips[i];
|
|
32191
|
+
if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return r.text;
|
|
32192
|
+
}
|
|
32193
|
+
return null;
|
|
32194
|
+
}
|
|
32195
|
+
};
|
|
32196
|
+
function mergeSlices(indicator, user) {
|
|
32197
|
+
const out = /* @__PURE__ */ new Map();
|
|
32198
|
+
for (const [paneId, slices] of indicator) out.set(paneId, [...slices]);
|
|
32199
|
+
for (const [paneId, slices] of user) out.set(paneId, [...out.get(paneId) ?? [], ...slices]);
|
|
32200
|
+
for (const slices of out.values()) slices.sort((a, b) => a.beforeZ - b.beforeZ);
|
|
32201
|
+
return out;
|
|
32202
|
+
}
|
|
32203
|
+
|
|
31835
32204
|
// src/renderers/native/drawings/Projector.ts
|
|
31836
32205
|
function createProjector(coords, paneOf, paneIdAtY, barsInRange) {
|
|
31837
32206
|
return {
|
|
@@ -32577,6 +32946,11 @@ var NativeRenderer = class {
|
|
|
32577
32946
|
this.vpvrRenderer = new VpvrRenderer();
|
|
32578
32947
|
this.resizeObserver = null;
|
|
32579
32948
|
this.dprMedia = null;
|
|
32949
|
+
/** Plot size in INTEGER device px, as last reported by the resize observer's
|
|
32950
|
+
* device-pixel-content-box — the browser's own statement of how many device pixels
|
|
32951
|
+
* it paints the plot into. `null` until the first report or where the box type is
|
|
32952
|
+
* unsupported (WebKit); syncSize then falls back to rounding the client rect. */
|
|
32953
|
+
this.plotDeviceSize = null;
|
|
32580
32954
|
this.coords = new CoordinateSystem();
|
|
32581
32955
|
this.scene = new SceneGraph();
|
|
32582
32956
|
// chosen at mount (WebGL2 if available, else canvas2d)
|
|
@@ -32584,6 +32958,8 @@ var NativeRenderer = class {
|
|
|
32584
32958
|
this.glowAmount = 0;
|
|
32585
32959
|
// WebGL2 neon-glow intensity (canvas2d ignores it)
|
|
32586
32960
|
this.chrome = new ChromeRenderer();
|
|
32961
|
+
/** Prepaints each indicator's Pine drawings into interleave slices at the model's z. */
|
|
32962
|
+
this.indicatorSlices = new IndicatorDrawingSlices();
|
|
32587
32963
|
/** Hover tooltips for Pine labels (canvas hit-rects collected by the chrome layer). */
|
|
32588
32964
|
this.labelTooltip = null;
|
|
32589
32965
|
this.crosshairLayer = new CrosshairRenderer();
|
|
@@ -32732,7 +33108,6 @@ var NativeRenderer = class {
|
|
|
32732
33108
|
this.toggleVisibleCbs = /* @__PURE__ */ new Set();
|
|
32733
33109
|
this.moveIndicatorCbs = /* @__PURE__ */ new Set();
|
|
32734
33110
|
this.priceStyleCbs = /* @__PURE__ */ new Set();
|
|
32735
|
-
this.tableOverlays = /* @__PURE__ */ new Map();
|
|
32736
33111
|
this.name = "native";
|
|
32737
33112
|
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"];
|
|
32738
33113
|
/** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
|
|
@@ -33046,7 +33421,6 @@ var NativeRenderer = class {
|
|
|
33046
33421
|
* hidden) on clear so a re-show picks up the current theme.
|
|
33047
33422
|
*/
|
|
33048
33423
|
setLoading(loading) {
|
|
33049
|
-
for (const overlay of this.tableOverlays.values()) overlay.setVisible(!loading);
|
|
33050
33424
|
if (!loading || !this.wrapper) {
|
|
33051
33425
|
this.loadingEl?.remove();
|
|
33052
33426
|
this.loadingEl = null;
|
|
@@ -33708,7 +34082,7 @@ var NativeRenderer = class {
|
|
|
33708
34082
|
this.plot.addEventListener("pointerleave", this.onScrollProximityLeave);
|
|
33709
34083
|
this.labelTooltip = new LabelTooltip(this.plot, {
|
|
33710
34084
|
theme: () => this.chromeTheme(),
|
|
33711
|
-
lookup: (x, y) => this.
|
|
34085
|
+
lookup: (x, y) => this.indicatorSlices.labelTooltipAt(x, y)
|
|
33712
34086
|
});
|
|
33713
34087
|
this.userDrawings = new UserDrawingController(this.wrapper, this.plot, this.drawingsCanvas, {
|
|
33714
34088
|
projector: () => this.drawingProjector(),
|
|
@@ -33793,6 +34167,7 @@ var NativeRenderer = class {
|
|
|
33793
34167
|
this.emitPaneAction({ type: "maximize", paneId, maximized });
|
|
33794
34168
|
}
|
|
33795
34169
|
});
|
|
34170
|
+
this.paneControls.setSuspended(this.layoutMode === "mobile");
|
|
33796
34171
|
this.axisScaleButtons = new AxisScaleButtons(this.plot, theme, {
|
|
33797
34172
|
panes: () => this.axisScaleViews(),
|
|
33798
34173
|
rightAxis: () => this.rightAxisW,
|
|
@@ -33802,8 +34177,19 @@ var NativeRenderer = class {
|
|
|
33802
34177
|
if (pane) this.setPaneLog(paneId, !paneLogScale(this.scene, pane));
|
|
33803
34178
|
}
|
|
33804
34179
|
});
|
|
33805
|
-
this.resizeObserver = new ResizeObserver(() =>
|
|
34180
|
+
this.resizeObserver = new ResizeObserver((entries) => {
|
|
34181
|
+
for (const e of entries) {
|
|
34182
|
+
if (e.target !== this.plot) continue;
|
|
34183
|
+
const s = e.devicePixelContentBoxSize?.[0];
|
|
34184
|
+
if (s) this.plotDeviceSize = { width: s.inlineSize, height: s.blockSize };
|
|
34185
|
+
}
|
|
34186
|
+
this.resize();
|
|
34187
|
+
});
|
|
33806
34188
|
this.resizeObserver.observe(this.wrapper);
|
|
34189
|
+
try {
|
|
34190
|
+
this.resizeObserver.observe(this.plot, { box: "device-pixel-content-box" });
|
|
34191
|
+
} catch {
|
|
34192
|
+
}
|
|
33807
34193
|
this.watchDpr();
|
|
33808
34194
|
this.syncSize();
|
|
33809
34195
|
}
|
|
@@ -33928,8 +34314,6 @@ var NativeRenderer = class {
|
|
|
33928
34314
|
this.inputsUI?.destroy();
|
|
33929
34315
|
this.paneControls?.destroy();
|
|
33930
34316
|
this.axisScaleButtons?.destroy();
|
|
33931
|
-
for (const overlay of this.tableOverlays.values()) overlay.destroy();
|
|
33932
|
-
this.tableOverlays.clear();
|
|
33933
34317
|
this.resizeObserver?.disconnect();
|
|
33934
34318
|
this.resizeObserver = null;
|
|
33935
34319
|
this.dprMedia?.removeEventListener("change", this.onDprChange);
|
|
@@ -33958,6 +34342,7 @@ var NativeRenderer = class {
|
|
|
33958
34342
|
this.attributionEl = null;
|
|
33959
34343
|
this.mountContainer?.style.removeProperty("--vela-toolbar-gutter");
|
|
33960
34344
|
this.mountContainer?.style.removeProperty("--vela-scale-gutter");
|
|
34345
|
+
this.mountContainer?.style.removeProperty("--vela-bottom-gutter");
|
|
33961
34346
|
this.mountContainer?.style.removeProperty("--vela-price-pane-top");
|
|
33962
34347
|
this.mountContainer?.style.removeProperty("--vela-price-pane-bottom");
|
|
33963
34348
|
this.mountContainer = null;
|
|
@@ -34050,7 +34435,6 @@ var NativeRenderer = class {
|
|
|
34050
34435
|
ensurePane(pane) {
|
|
34051
34436
|
this.scene.ensurePane(pane.id, pane.kind, pane.order, pane.heightWeight ?? (pane.kind === "price" ? 3 : 1));
|
|
34052
34437
|
this.layoutPanes();
|
|
34053
|
-
this.repositionTables();
|
|
34054
34438
|
this.paneControls?.refresh();
|
|
34055
34439
|
this.scheduler.invalidate(4 /* Full */);
|
|
34056
34440
|
}
|
|
@@ -34072,17 +34456,14 @@ var NativeRenderer = class {
|
|
|
34072
34456
|
if (!model.ownScale) this.scene.dropIndicatorScale(handle.id);
|
|
34073
34457
|
this.inputsUI.setPane(handle.id, paneId);
|
|
34074
34458
|
this.refreshAnchorOffset(model);
|
|
34075
|
-
this.syncTables(model);
|
|
34076
34459
|
this.refreshAxisWidth();
|
|
34077
34460
|
this.layoutPanes();
|
|
34078
|
-
this.repositionTables();
|
|
34079
34461
|
this.paneControls?.refresh();
|
|
34080
34462
|
this.scheduler.invalidate(4 /* Full */);
|
|
34081
34463
|
}
|
|
34082
34464
|
orderPanes(orderedIds) {
|
|
34083
34465
|
this.scene.orderPanes(orderedIds);
|
|
34084
34466
|
this.layoutPanes();
|
|
34085
|
-
this.repositionTables();
|
|
34086
34467
|
this.paneControls?.refresh();
|
|
34087
34468
|
this.scheduler.invalidate(4 /* Full */);
|
|
34088
34469
|
}
|
|
@@ -34091,7 +34472,6 @@ var NativeRenderer = class {
|
|
|
34091
34472
|
if (!pane || pane.collapsed === collapsed) return;
|
|
34092
34473
|
pane.collapsed = collapsed;
|
|
34093
34474
|
this.layoutPanes();
|
|
34094
|
-
this.repositionTables();
|
|
34095
34475
|
this.paneControls?.refresh();
|
|
34096
34476
|
this.scheduler.invalidate(4 /* Full */);
|
|
34097
34477
|
}
|
|
@@ -34099,7 +34479,6 @@ var NativeRenderer = class {
|
|
|
34099
34479
|
if (paneId !== null && !this.scene.panes.has(paneId)) paneId = null;
|
|
34100
34480
|
this.maximizedPaneId = paneId;
|
|
34101
34481
|
this.layoutPanes();
|
|
34102
|
-
this.repositionTables();
|
|
34103
34482
|
this.paneControls?.refresh();
|
|
34104
34483
|
this.scheduler.invalidate(4 /* Full */);
|
|
34105
34484
|
}
|
|
@@ -34186,7 +34565,6 @@ var NativeRenderer = class {
|
|
|
34186
34565
|
native: !!model.native,
|
|
34187
34566
|
...model.props ? { props: model.props, propValues: model.propValues ?? {} } : {}
|
|
34188
34567
|
});
|
|
34189
|
-
this.syncTables(model);
|
|
34190
34568
|
if (model.native?.type === "volume") {
|
|
34191
34569
|
this.volumeActive = true;
|
|
34192
34570
|
this.volumeHidden = false;
|
|
@@ -34210,7 +34588,6 @@ var NativeRenderer = class {
|
|
|
34210
34588
|
}
|
|
34211
34589
|
}
|
|
34212
34590
|
applyPatch(model, patch);
|
|
34213
|
-
this.syncTables(model);
|
|
34214
34591
|
this.scheduler.invalidate(3 /* Light */);
|
|
34215
34592
|
}
|
|
34216
34593
|
removeIndicator(handle) {
|
|
@@ -34229,8 +34606,6 @@ var NativeRenderer = class {
|
|
|
34229
34606
|
this.scene.forgetAnchorOffset(handle.id);
|
|
34230
34607
|
this.scene.dropIndicatorScale(handle.id);
|
|
34231
34608
|
this.inputsUI.remove(handle.id);
|
|
34232
|
-
this.tableOverlays.get(handle.id)?.destroy();
|
|
34233
|
-
this.tableOverlays.delete(handle.id);
|
|
34234
34609
|
this.refreshAxisWidth();
|
|
34235
34610
|
this.paneControls?.refresh();
|
|
34236
34611
|
this.scheduler.invalidate(4 /* Full */);
|
|
@@ -34274,8 +34649,6 @@ var NativeRenderer = class {
|
|
|
34274
34649
|
}
|
|
34275
34650
|
if (!visible) {
|
|
34276
34651
|
this.scene.indicators.delete(handle.id);
|
|
34277
|
-
this.tableOverlays.get(handle.id)?.destroy();
|
|
34278
|
-
this.tableOverlays.delete(handle.id);
|
|
34279
34652
|
}
|
|
34280
34653
|
this.inputsUI.setVisible(handle.id, visible);
|
|
34281
34654
|
this.scheduler.invalidate(4 /* Full */);
|
|
@@ -34321,6 +34694,7 @@ var NativeRenderer = class {
|
|
|
34321
34694
|
this.userDrawings?.setLayoutMode(mode);
|
|
34322
34695
|
this.settingsDialog?.setLayoutMode(mode);
|
|
34323
34696
|
this.inputsUI?.setLayoutMode(mode);
|
|
34697
|
+
this.paneControls?.setSuspended(mode === "mobile");
|
|
34324
34698
|
if (this.scrollButton) {
|
|
34325
34699
|
const px = mode === "mobile" ? SCROLL_BTN_SIZE_TOUCH : SCROLL_BTN_SIZE;
|
|
34326
34700
|
this.scrollButton.style.width = `${px}px`;
|
|
@@ -34716,7 +35090,6 @@ var NativeRenderer = class {
|
|
|
34716
35090
|
/** Relayout + repaint + refresh the hover buttons after a collapse/maximize/order change. */
|
|
34717
35091
|
afterPaneLayoutChange() {
|
|
34718
35092
|
this.layoutPanes();
|
|
34719
|
-
this.repositionTables();
|
|
34720
35093
|
this.paneControls?.refresh();
|
|
34721
35094
|
this.scheduler.invalidate(4 /* Full */);
|
|
34722
35095
|
}
|
|
@@ -34797,7 +35170,6 @@ var NativeRenderer = class {
|
|
|
34797
35170
|
above.heightWeight = next.above;
|
|
34798
35171
|
below.heightWeight = next.below;
|
|
34799
35172
|
this.layoutPanes();
|
|
34800
|
-
this.repositionTables();
|
|
34801
35173
|
this.scheduler.invalidate(4 /* Full */);
|
|
34802
35174
|
}
|
|
34803
35175
|
/** Double-click a separator → split the two adjacent panes evenly (each gets half of
|
|
@@ -34812,7 +35184,6 @@ var NativeRenderer = class {
|
|
|
34812
35184
|
above.heightWeight = half;
|
|
34813
35185
|
below.heightWeight = half;
|
|
34814
35186
|
this.layoutPanes();
|
|
34815
|
-
this.repositionTables();
|
|
34816
35187
|
this.scheduler.invalidate(4 /* Full */);
|
|
34817
35188
|
}
|
|
34818
35189
|
// ── keyboard navigation / accessibility (item 11) ──
|
|
@@ -35091,7 +35462,10 @@ var NativeRenderer = class {
|
|
|
35091
35462
|
const liveActual = li >= 0 ? this.bars[li] : void 0;
|
|
35092
35463
|
const easeLive = !!liveActual && this.liveEaseTime === liveActual.time && (liveActual.high !== this.liveEaseHigh || liveActual.low !== this.liveEaseLow || liveActual.close !== this.liveEaseClose);
|
|
35093
35464
|
if (easeLive && liveActual) this.bars[li] = { ...liveActual, high: this.liveEaseHigh, low: this.liveEaseLow, close: this.liveEaseClose };
|
|
35094
|
-
this.scene.drawingSlices =
|
|
35465
|
+
this.scene.drawingSlices = mergeSlices(
|
|
35466
|
+
this.indicatorSlices.prepare(this.scene, this.coords, this.theme, this.dataCanvas),
|
|
35467
|
+
this.userDrawings?.prepareSlices(this.scene.orderedPanes().map((p) => p.id)) ?? /* @__PURE__ */ new Map()
|
|
35468
|
+
);
|
|
35095
35469
|
this.backdropRenderer.render(this.scene, this.coords, this.theme, gridAlpha);
|
|
35096
35470
|
this.backend.render(this.scene, this.coords, this.theme);
|
|
35097
35471
|
this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
|
|
@@ -35529,27 +35903,6 @@ var NativeRenderer = class {
|
|
|
35529
35903
|
}
|
|
35530
35904
|
return maxVol;
|
|
35531
35905
|
}
|
|
35532
|
-
/** Create/update/destroy an indicator's DOM table overlay (anchored off real pane geometry). */
|
|
35533
|
-
syncTables(model) {
|
|
35534
|
-
const tables = model.tables ?? [];
|
|
35535
|
-
let overlay = this.tableOverlays.get(model.id);
|
|
35536
|
-
if (tables.length === 0) {
|
|
35537
|
-
if (overlay) {
|
|
35538
|
-
overlay.destroy();
|
|
35539
|
-
this.tableOverlays.delete(model.id);
|
|
35540
|
-
}
|
|
35541
|
-
return;
|
|
35542
|
-
}
|
|
35543
|
-
if (!overlay) {
|
|
35544
|
-
overlay = new TableOverlay(this.plot, this.theme, (id) => this.paneBoundsFor(id));
|
|
35545
|
-
overlay.setVisible(this.loadingEl === null);
|
|
35546
|
-
this.tableOverlays.set(model.id, overlay);
|
|
35547
|
-
}
|
|
35548
|
-
overlay.update(tables);
|
|
35549
|
-
}
|
|
35550
|
-
repositionTables() {
|
|
35551
|
-
for (const overlay of this.tableOverlays.values()) overlay.reposition();
|
|
35552
|
-
}
|
|
35553
35906
|
layoutPanes() {
|
|
35554
35907
|
const panes = this.scene.orderedPanes();
|
|
35555
35908
|
const dataHeight = this.coords.height;
|
|
@@ -35600,6 +35953,7 @@ var NativeRenderer = class {
|
|
|
35600
35953
|
const visible = maxPane ? [maxPane] : this.scene.orderedPanes().filter((p) => !p.collapsed);
|
|
35601
35954
|
const paneBottom = visible.length ? Math.max(...visible.map((p) => p.bounds.top + p.bounds.height)) : dataHeight;
|
|
35602
35955
|
this.scrollBtnBottomPx = SCROLL_BTN_BOTTOM + Math.max(0, dataHeight - paneBottom);
|
|
35956
|
+
this.mountContainer?.style.setProperty("--vela-bottom-gutter", `${TIME_AXIS_H + Math.max(0, dataHeight - paneBottom)}px`);
|
|
35603
35957
|
this.scrollBtnRightPx = this.rightAxisW + SCROLL_BTN_RIGHT_INSET;
|
|
35604
35958
|
if (this.scrollButton) {
|
|
35605
35959
|
this.scrollButton.style.bottom = `${this.scrollBtnBottomPx}px`;
|
|
@@ -35692,30 +36046,33 @@ var NativeRenderer = class {
|
|
|
35692
36046
|
if (w <= 0 || h <= 0) return;
|
|
35693
36047
|
const dpr = window.devicePixelRatio || 1;
|
|
35694
36048
|
this.plot.style.left = `${this.toolbarGutter}px`;
|
|
35695
|
-
const
|
|
35696
|
-
|
|
35697
|
-
|
|
35698
|
-
|
|
35699
|
-
|
|
35700
|
-
|
|
35701
|
-
|
|
35702
|
-
|
|
35703
|
-
|
|
35704
|
-
|
|
35705
|
-
|
|
35706
|
-
|
|
35707
|
-
|
|
35708
|
-
|
|
35709
|
-
|
|
35710
|
-
|
|
35711
|
-
this.
|
|
35712
|
-
this.
|
|
35713
|
-
this.
|
|
35714
|
-
|
|
36049
|
+
const rect = this.plot.getBoundingClientRect();
|
|
36050
|
+
let bw = Math.max(1, Math.round(rect.width * dpr));
|
|
36051
|
+
let bh = Math.max(1, Math.round(rect.height * dpr));
|
|
36052
|
+
const dev = this.plotDeviceSize;
|
|
36053
|
+
if (dev && Math.abs(dev.width - rect.width * dpr) <= 1 && Math.abs(dev.height - rect.height * dpr) <= 1) {
|
|
36054
|
+
bw = Math.max(1, dev.width);
|
|
36055
|
+
bh = Math.max(1, dev.height);
|
|
36056
|
+
}
|
|
36057
|
+
const pw = bw / dpr;
|
|
36058
|
+
const ph = bh / dpr;
|
|
36059
|
+
const size = (canvas) => {
|
|
36060
|
+
canvas.width = bw;
|
|
36061
|
+
canvas.height = bh;
|
|
36062
|
+
canvas.style.width = `${pw}px`;
|
|
36063
|
+
canvas.style.height = `${ph}px`;
|
|
36064
|
+
};
|
|
36065
|
+
size(this.dataCanvas);
|
|
36066
|
+
size(this.backdropCanvas);
|
|
36067
|
+
size(this.volumeCanvas);
|
|
36068
|
+
for (const l of this.extLayers) size(l.canvas);
|
|
36069
|
+
size(this.vpvrCanvas);
|
|
36070
|
+
size(this.chromeCanvas);
|
|
36071
|
+
size(this.drawingsCanvas);
|
|
36072
|
+
size(this.cursorCanvas);
|
|
35715
36073
|
this.coords.setSize(Math.max(1, pw - this.rightAxisW), Math.max(1, ph - TIME_AXIS_H), dpr);
|
|
35716
36074
|
this.scene.crosshair = null;
|
|
35717
36075
|
this.layoutPanes();
|
|
35718
|
-
this.repositionTables();
|
|
35719
36076
|
this.userDrawings?.onResize();
|
|
35720
36077
|
if (!this.didInitialFit && this.coords.barCount > 0) {
|
|
35721
36078
|
this.fitContent();
|
|
@@ -37034,9 +37391,168 @@ var Watermark = class {
|
|
|
37034
37391
|
}
|
|
37035
37392
|
};
|
|
37036
37393
|
|
|
37394
|
+
// src/widget/cell-controls.ts
|
|
37395
|
+
var CELL_CONTROLS_PROXIMITY_PX = 120;
|
|
37396
|
+
var TIME_AXIS_H2 = 22;
|
|
37397
|
+
var CONTROLS_BOTTOM_PX = TIME_AXIS_H2 + 12;
|
|
37398
|
+
var CLUSTER_H2 = 24;
|
|
37399
|
+
var CLUSTER_PILL2 = "rgba(0,0,0,0.65)";
|
|
37400
|
+
var STYLE_ID26 = "vela-cell-controls";
|
|
37401
|
+
var CSS23 = `
|
|
37402
|
+
.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;}
|
|
37403
|
+
.vela-cc-btn svg{display:block;}
|
|
37404
|
+
.vela-cc-btn:hover{background:var(--vela-active);color:var(--vela-fg-bright);}
|
|
37405
|
+
.vela-cc-on,.vela-cc-on:hover{background:var(--vela-selected-bg);color:var(--vela-selected-fg);}
|
|
37406
|
+
.vela-cc-grip{cursor:grab;touch-action:none;}
|
|
37407
|
+
.vela-cc-grip:active{cursor:grabbing;}
|
|
37408
|
+
`;
|
|
37409
|
+
function nearBottomCenter(x, y, width, height, proximityPx = CELL_CONTROLS_PROXIMITY_PX) {
|
|
37410
|
+
const cx = width / 2;
|
|
37411
|
+
const cy = height - CONTROLS_BOTTOM_PX - CLUSTER_H2 / 2;
|
|
37412
|
+
return Math.hypot(x - cx, y - cy) <= proximityPx;
|
|
37413
|
+
}
|
|
37414
|
+
var CellControls = class {
|
|
37415
|
+
constructor(host, deps) {
|
|
37416
|
+
this.host = host;
|
|
37417
|
+
this.deps = deps;
|
|
37418
|
+
this.near = false;
|
|
37419
|
+
/** A grip drag is underway — the proximity reveal must not hide the cluster
|
|
37420
|
+
* while captured pointer moves sweep across the whole grid. */
|
|
37421
|
+
this.dragging = false;
|
|
37422
|
+
/** Mobile: the proximity reveal is meaningless without a cursor — the mobile
|
|
37423
|
+
* bar's maximize stop replaces the cluster. */
|
|
37424
|
+
this.suspended = false;
|
|
37425
|
+
this.onHostMove = (e) => {
|
|
37426
|
+
if (this.suspended) return;
|
|
37427
|
+
if (this.dragging) return;
|
|
37428
|
+
const rect = this.host.getBoundingClientRect();
|
|
37429
|
+
this.setNear(nearBottomCenter(e.clientX - rect.left, e.clientY - rect.top, rect.width, rect.height));
|
|
37430
|
+
};
|
|
37431
|
+
this.onHostLeave = () => {
|
|
37432
|
+
if (this.dragging) return;
|
|
37433
|
+
this.setNear(false);
|
|
37434
|
+
};
|
|
37435
|
+
injectStyles(STYLE_ID26, CSS23, host.ownerDocument);
|
|
37436
|
+
this.glider = new Glider(deps.chart);
|
|
37437
|
+
this.root = host.ownerDocument.createElement("div");
|
|
37438
|
+
Object.assign(this.root.style, {
|
|
37439
|
+
position: "absolute",
|
|
37440
|
+
left: "50%",
|
|
37441
|
+
bottom: `${CONTROLS_BOTTOM_PX}px`,
|
|
37442
|
+
transform: "translateX(-50%)",
|
|
37443
|
+
zIndex: "6",
|
|
37444
|
+
display: "none",
|
|
37445
|
+
// revealed by cursor proximity (onHostMove)
|
|
37446
|
+
gap: "2px",
|
|
37447
|
+
padding: "2px",
|
|
37448
|
+
borderRadius: "var(--vela-radius-md)",
|
|
37449
|
+
background: CLUSTER_PILL2,
|
|
37450
|
+
pointerEvents: "auto"
|
|
37451
|
+
});
|
|
37452
|
+
this.host.addEventListener("pointermove", this.onHostMove);
|
|
37453
|
+
this.host.addEventListener("pointerleave", this.onHostLeave);
|
|
37454
|
+
this.host.appendChild(this.root);
|
|
37455
|
+
this.refresh();
|
|
37456
|
+
}
|
|
37457
|
+
/** Rebuild the buttons (the multi-cell gate or the maximized state changed). */
|
|
37458
|
+
refresh() {
|
|
37459
|
+
this.root.textContent = "";
|
|
37460
|
+
const multi = this.deps.multiCell();
|
|
37461
|
+
const maximized = multi && this.deps.isMaximized();
|
|
37462
|
+
if (multi && !maximized) this.root.appendChild(this.makeGrip());
|
|
37463
|
+
this.root.appendChild(this.button("minus", "Zoom out", () => this.glider.zoom(ZOOM_OUT)));
|
|
37464
|
+
this.root.appendChild(this.button("plus", "Zoom in", () => this.glider.zoom(ZOOM_IN)));
|
|
37465
|
+
if (multi) {
|
|
37466
|
+
this.root.appendChild(
|
|
37467
|
+
this.button(maximized ? "restore" : "maximize", maximized ? "Restore layout" : "Maximize chart", () => this.deps.toggleMaximize(), {
|
|
37468
|
+
// The maximized state reads as an inverse chip (white-on-dark, dark-on-light),
|
|
37469
|
+
// the same active-state affordance as a collapsed pane's expand button.
|
|
37470
|
+
selected: maximized
|
|
37471
|
+
})
|
|
37472
|
+
);
|
|
37473
|
+
}
|
|
37474
|
+
this.root.appendChild(
|
|
37475
|
+
this.button("reset", "Reset chart", () => {
|
|
37476
|
+
this.glider.stop();
|
|
37477
|
+
this.deps.reset();
|
|
37478
|
+
})
|
|
37479
|
+
);
|
|
37480
|
+
}
|
|
37481
|
+
button(iconId, title, onClick, opts = {}) {
|
|
37482
|
+
const b = this.host.ownerDocument.createElement("button");
|
|
37483
|
+
b.type = "button";
|
|
37484
|
+
b.title = title;
|
|
37485
|
+
b.setAttribute("aria-label", title);
|
|
37486
|
+
b.className = opts.selected === true ? "vela-cc-btn vela-cc-on" : "vela-cc-btn";
|
|
37487
|
+
b.innerHTML = icon(iconId);
|
|
37488
|
+
b.addEventListener("click", (e) => {
|
|
37489
|
+
e.stopPropagation();
|
|
37490
|
+
onClick();
|
|
37491
|
+
});
|
|
37492
|
+
return b;
|
|
37493
|
+
}
|
|
37494
|
+
/** The drag handle (2×3 dot grip): press and drag onto another cell to trade
|
|
37495
|
+
* slots with it. The preview highlight follows the pointer; releasing outside
|
|
37496
|
+
* any other cell cancels. */
|
|
37497
|
+
makeGrip() {
|
|
37498
|
+
const b = this.host.ownerDocument.createElement("button");
|
|
37499
|
+
b.type = "button";
|
|
37500
|
+
b.title = "Drag to move chart";
|
|
37501
|
+
b.setAttribute("aria-label", "Drag to move chart");
|
|
37502
|
+
b.className = "vela-cc-btn vela-cc-grip";
|
|
37503
|
+
b.innerHTML = icon("grip");
|
|
37504
|
+
b.addEventListener("pointerdown", (e) => this.onGripDown(b, e));
|
|
37505
|
+
return b;
|
|
37506
|
+
}
|
|
37507
|
+
onGripDown(btn2, e) {
|
|
37508
|
+
if (e.button !== 0 && e.pointerType === "mouse") return;
|
|
37509
|
+
e.preventDefault();
|
|
37510
|
+
e.stopPropagation();
|
|
37511
|
+
try {
|
|
37512
|
+
btn2.setPointerCapture(e.pointerId);
|
|
37513
|
+
} catch {
|
|
37514
|
+
}
|
|
37515
|
+
this.dragging = true;
|
|
37516
|
+
let target = null;
|
|
37517
|
+
const move = (ev) => {
|
|
37518
|
+
target = this.deps.dragTargetAt(ev.clientX, ev.clientY);
|
|
37519
|
+
this.deps.previewDrop(target);
|
|
37520
|
+
};
|
|
37521
|
+
const finish = (commit) => () => {
|
|
37522
|
+
this.dragging = false;
|
|
37523
|
+
this.deps.previewDrop(null);
|
|
37524
|
+
btn2.removeEventListener("pointermove", move);
|
|
37525
|
+
btn2.removeEventListener("pointerup", onUp);
|
|
37526
|
+
btn2.removeEventListener("pointercancel", onCancel);
|
|
37527
|
+
if (commit && target != null) this.deps.dropOn(target);
|
|
37528
|
+
};
|
|
37529
|
+
const onUp = finish(true);
|
|
37530
|
+
const onCancel = finish(false);
|
|
37531
|
+
btn2.addEventListener("pointermove", move);
|
|
37532
|
+
btn2.addEventListener("pointerup", onUp);
|
|
37533
|
+
btn2.addEventListener("pointercancel", onCancel);
|
|
37534
|
+
}
|
|
37535
|
+
/** Mobile flips the cluster off entirely (and hides it if currently revealed). */
|
|
37536
|
+
setSuspended(on) {
|
|
37537
|
+
this.suspended = on;
|
|
37538
|
+
if (on) this.setNear(false);
|
|
37539
|
+
}
|
|
37540
|
+
setNear(near) {
|
|
37541
|
+
if (near === this.near) return;
|
|
37542
|
+
this.near = near;
|
|
37543
|
+
this.root.style.display = near ? "flex" : "none";
|
|
37544
|
+
}
|
|
37545
|
+
destroy() {
|
|
37546
|
+
this.glider.stop();
|
|
37547
|
+
this.host.removeEventListener("pointermove", this.onHostMove);
|
|
37548
|
+
this.host.removeEventListener("pointerleave", this.onHostLeave);
|
|
37549
|
+
this.root.remove();
|
|
37550
|
+
}
|
|
37551
|
+
};
|
|
37552
|
+
|
|
37037
37553
|
// src/widget/context-menu.ts
|
|
37038
37554
|
var PRICE_AXIS_W = 60;
|
|
37039
|
-
var
|
|
37555
|
+
var TIME_AXIS_H3 = 26;
|
|
37040
37556
|
var ChartContextMenu = class {
|
|
37041
37557
|
constructor(host, cbs) {
|
|
37042
37558
|
this.cbs = cbs;
|
|
@@ -37075,7 +37591,7 @@ var ChartContextMenu = class {
|
|
|
37075
37591
|
zoneOf(e) {
|
|
37076
37592
|
const rect = this.host.getBoundingClientRect();
|
|
37077
37593
|
if (e.clientX - rect.left > rect.width - PRICE_AXIS_W) return "price-axis";
|
|
37078
|
-
if (e.clientY - rect.top > rect.height -
|
|
37594
|
+
if (e.clientY - rect.top > rect.height - TIME_AXIS_H3) return "time-axis";
|
|
37079
37595
|
return "body";
|
|
37080
37596
|
}
|
|
37081
37597
|
/** The pane under the pointer, so every pane's price scale has its own menu. */
|
|
@@ -37409,11 +37925,18 @@ var ChartCell = class {
|
|
|
37409
37925
|
if (this.inner && this.state.symbol) this.marketStatus?.track(this.inner.data, this.state.symbol);
|
|
37410
37926
|
});
|
|
37411
37927
|
this.syncStatuslineColors();
|
|
37928
|
+
this.cellControls = new CellControls(this.host, {
|
|
37929
|
+
chart: () => this.inner,
|
|
37930
|
+
reset: () => this.resetView(),
|
|
37931
|
+
multiCell: () => deps.multiCell(),
|
|
37932
|
+
isMaximized: () => deps.isMaximized(id),
|
|
37933
|
+
toggleMaximize: () => deps.toggleMaximize(id),
|
|
37934
|
+
dragTargetAt: (x, y) => deps.cellDragTarget(id, x, y),
|
|
37935
|
+
previewDrop: (target) => deps.previewDropTarget(target),
|
|
37936
|
+
dropOn: (target) => deps.dropCell(id, target)
|
|
37937
|
+
});
|
|
37412
37938
|
this.contextMenu = new ChartContextMenu(this.host, {
|
|
37413
|
-
resetView: () =>
|
|
37414
|
-
this.inner?.renderer.set("autoScale", true);
|
|
37415
|
-
this.inner?.setVisibleRangePreset("ALL");
|
|
37416
|
-
},
|
|
37939
|
+
resetView: () => this.resetView(),
|
|
37417
37940
|
timezone: () => this.deps.timezone(),
|
|
37418
37941
|
setTimezone: (zone) => this.deps.setTimezone(zone),
|
|
37419
37942
|
// Right-clicking activates the cell first (capture-phase pointerdown), so the
|
|
@@ -37787,6 +38310,20 @@ var ChartCell = class {
|
|
|
37787
38310
|
this.inner.setVisibleRangePreset(preset.preset);
|
|
37788
38311
|
}
|
|
37789
38312
|
}
|
|
38313
|
+
/** Reset this cell's view: re-enable auto scale and frame the full history —
|
|
38314
|
+
* the same action the chart context menu offers. */
|
|
38315
|
+
resetView() {
|
|
38316
|
+
this.inner?.renderer.set("autoScale", true);
|
|
38317
|
+
this.inner?.setVisibleRangePreset("ALL");
|
|
38318
|
+
}
|
|
38319
|
+
/** Rebuild the view-controls cluster (the maximize gate or state changed). */
|
|
38320
|
+
refreshControls() {
|
|
38321
|
+
this.cellControls.refresh();
|
|
38322
|
+
}
|
|
38323
|
+
/** Mobile flips the per-cell cluster off (the shell's mobile bar replaces it). */
|
|
38324
|
+
setControlsSuspended(on) {
|
|
38325
|
+
this.cellControls.setSuspended(on);
|
|
38326
|
+
}
|
|
37790
38327
|
/** Make this cell the active one and put keyboard focus on its chart surface. */
|
|
37791
38328
|
focus() {
|
|
37792
38329
|
this.deps.activate(this.id);
|
|
@@ -38100,6 +38637,7 @@ var ChartCell = class {
|
|
|
38100
38637
|
destroy() {
|
|
38101
38638
|
this.destroyed = true;
|
|
38102
38639
|
this.offMarket();
|
|
38640
|
+
this.cellControls.destroy();
|
|
38103
38641
|
this.contextMenu.destroy();
|
|
38104
38642
|
this.history.destroy();
|
|
38105
38643
|
this.marketStatus?.stop();
|
|
@@ -38392,10 +38930,10 @@ var SplitterLayer = class {
|
|
|
38392
38930
|
var DEFAULT_TIMEFRAMES = ["1", "5", "15", "60", "240", "D", "W"];
|
|
38393
38931
|
var GAP_PX = 2;
|
|
38394
38932
|
var POOL_CAP = 16;
|
|
38395
|
-
var
|
|
38933
|
+
var TIME_AXIS_H4 = 22;
|
|
38396
38934
|
var ALERT_CAP = 50;
|
|
38397
|
-
var
|
|
38398
|
-
var
|
|
38935
|
+
var STYLE_ID27 = "vela-workspace";
|
|
38936
|
+
var CSS24 = `
|
|
38399
38937
|
.vela-workspace { position: relative; width: 100%; height: 100%; display: flex; flex-direction: column; background: var(--vela-bg); }
|
|
38400
38938
|
.vela-ws-main { position: relative; display: flex; flex-direction: row; flex: 1 1 auto; min-height: 0; }
|
|
38401
38939
|
.vela-ws-toolbar { position: relative; flex: none; }
|
|
@@ -38422,6 +38960,21 @@ var CSS23 = `
|
|
|
38422
38960
|
/* Mobile: the docked drawing-toolbar column would eat a phone-width grid \u2014 the shell's
|
|
38423
38961
|
drawings drawer + on-chart pill replace it (same policy as the widget's in-chart bar). */
|
|
38424
38962
|
[data-layout='mobile'] .vela-ws-toolbar { display: none; }
|
|
38963
|
+
/* A maximized cell owns the whole grid: the splitter strips have no seams to grab and
|
|
38964
|
+
the active ring would just outline the only visible chart \u2014 both are noise here. */
|
|
38965
|
+
.vela-ws-grid[data-maximized='1'] .vela-ws-splitter { display: none; }
|
|
38966
|
+
.vela-ws-grid[data-maximized='1'] .vela-cell[data-active='1']::after { display: none; }
|
|
38967
|
+
/* Drop-target preview while a cell's drag handle is held: a dashed ring + the same
|
|
38968
|
+
soft wash the splitter hover uses, over the chart, inert to the pointer. */
|
|
38969
|
+
.vela-cell[data-drop-target='1']::before {
|
|
38970
|
+
content: '';
|
|
38971
|
+
position: absolute;
|
|
38972
|
+
inset: 0;
|
|
38973
|
+
border: 2px dashed var(--vela-fg-bright);
|
|
38974
|
+
background: var(--vela-separator-hover-band);
|
|
38975
|
+
pointer-events: none;
|
|
38976
|
+
z-index: 11;
|
|
38977
|
+
}
|
|
38425
38978
|
`;
|
|
38426
38979
|
registerIcon("layout", svg16('<rect x="1.5" y="1.5" width="13" height="13" rx="1.5"/><path d="M8 1.5v13M1.5 8h13"/>'));
|
|
38427
38980
|
function declaredOrder(cells) {
|
|
@@ -38451,6 +39004,9 @@ var VelaWorkspace = class {
|
|
|
38451
39004
|
* slots beyond the list get auto identities. Grows, never reorders. */
|
|
38452
39005
|
this.order = [];
|
|
38453
39006
|
this.activeId = null;
|
|
39007
|
+
/** The cell maximized over the whole grid (null = normal grid). TRANSIENT view
|
|
39008
|
+
* state — never persisted; any structural change (layout, applyState) restores. */
|
|
39009
|
+
this.maximizedId = null;
|
|
38454
39010
|
this.cellBackend = "auto";
|
|
38455
39011
|
this.destroyed = false;
|
|
38456
39012
|
this.shortcutsHelp = null;
|
|
@@ -38555,7 +39111,7 @@ var VelaWorkspace = class {
|
|
|
38555
39111
|
this.order = boot?.charts ? boot.charts.map((c) => c.id) : declaredOrder(opts.cells);
|
|
38556
39112
|
const bootActive = boot?.activeCellId ?? null;
|
|
38557
39113
|
const doc = hostEl.ownerDocument;
|
|
38558
|
-
injectStyles(
|
|
39114
|
+
injectStyles(STYLE_ID27, CSS24, doc);
|
|
38559
39115
|
this.root = doc.createElement("div");
|
|
38560
39116
|
this.root.className = "vela-workspace";
|
|
38561
39117
|
ensureUIHost(this.root, resolveTheme(opts.theme));
|
|
@@ -38665,7 +39221,11 @@ var VelaWorkspace = class {
|
|
|
38665
39221
|
if (attribution !== false) {
|
|
38666
39222
|
const background = resolveTheme(opts.theme).background;
|
|
38667
39223
|
const mark = typeof attribution === "string" && attribution.trim() ? createCustomMark(doc, attribution, background) : createAttributionMark(doc, background);
|
|
38668
|
-
Object.assign(mark.style, {
|
|
39224
|
+
Object.assign(mark.style, {
|
|
39225
|
+
left: "calc(var(--vela-toolbar-gutter, 0px) + 12px)",
|
|
39226
|
+
bottom: `calc(var(--vela-bottom-gutter, ${TIME_AXIS_H4}px) + 10px)`,
|
|
39227
|
+
zIndex: "11"
|
|
39228
|
+
});
|
|
38669
39229
|
this.gridEl.appendChild(mark);
|
|
38670
39230
|
this.attributionMark = mark;
|
|
38671
39231
|
}
|
|
@@ -38731,6 +39291,9 @@ var VelaWorkspace = class {
|
|
|
38731
39291
|
...topbarHas(this.topbarComp, "indicators") && (picker || this.indicatorsOverride) ? { onIndicatorsClick: this.indicatorsOverride ? () => this.runOverride(this.indicatorsOverride) : () => picker.open() } : {},
|
|
38732
39292
|
getContext: () => this.context(),
|
|
38733
39293
|
...this.drawingsEnabled ? { onDrawingsClick: () => this.openDrawingsDrawer() } : {},
|
|
39294
|
+
// Multi-chart only: the stop that isolates the ACTIVE chart (the
|
|
39295
|
+
// per-cell hover cluster has no cursor to reveal it on mobile).
|
|
39296
|
+
...this.monoLayout ? {} : { onMaximizeClick: () => this.toggleMobileMaximize() },
|
|
38734
39297
|
onMoreClick: () => this.openMoreDrawer(),
|
|
38735
39298
|
onSettingsClick: () => this.active.chart.renderer.openSettings()
|
|
38736
39299
|
}) : null;
|
|
@@ -38937,7 +39500,9 @@ var VelaWorkspace = class {
|
|
|
38937
39500
|
this.pool.clear();
|
|
38938
39501
|
for (const { id, ...cs } of st.charts.slice(liveCount)) this.pool.set(id, cs);
|
|
38939
39502
|
this.order = st.charts.map((c) => c.id);
|
|
39503
|
+
this.clearMaximized();
|
|
38940
39504
|
this.applyGrid();
|
|
39505
|
+
this.refreshCellControls();
|
|
38941
39506
|
const nextActive2 = st.activeCellId && this.cellsById.has(st.activeCellId) ? st.activeCellId : this.order[0] ?? null;
|
|
38942
39507
|
if (nextActive2 === this.activeId) this.projectActiveCell();
|
|
38943
39508
|
else this.setActiveCell(nextActive2);
|
|
@@ -38963,6 +39528,7 @@ var VelaWorkspace = class {
|
|
|
38963
39528
|
const def = this.monoLayout ? null : ensureLayout(st.layout);
|
|
38964
39529
|
if (def) this.def = def;
|
|
38965
39530
|
this.cellBackend = this.backendFor(this.def);
|
|
39531
|
+
this.clearMaximized();
|
|
38966
39532
|
this.applyGrid();
|
|
38967
39533
|
this.buildCells();
|
|
38968
39534
|
this.syncCellPresentation();
|
|
@@ -39026,6 +39592,7 @@ var VelaWorkspace = class {
|
|
|
39026
39592
|
setLayout(layout) {
|
|
39027
39593
|
if (this.destroyed) return;
|
|
39028
39594
|
if (this.monoLayout) return;
|
|
39595
|
+
this.clearMaximized();
|
|
39029
39596
|
const next = this.resolveLayout(layout);
|
|
39030
39597
|
const nextBackend = this.backendFor(next);
|
|
39031
39598
|
const rebuildAll = nextBackend !== this.cellBackend;
|
|
@@ -39046,6 +39613,7 @@ var VelaWorkspace = class {
|
|
|
39046
39613
|
this.buildCells();
|
|
39047
39614
|
this.alignNewCellStyles(preexisting);
|
|
39048
39615
|
this.syncCellPresentation();
|
|
39616
|
+
this.refreshCellControls();
|
|
39049
39617
|
this.topbar.setLayout(next.id);
|
|
39050
39618
|
const nextActive = activeAfterLayout(this.activeId, this.order.slice(0, next.cells.length));
|
|
39051
39619
|
if (nextActive === this.activeId) this.projectActiveCell();
|
|
@@ -39054,6 +39622,67 @@ var VelaWorkspace = class {
|
|
|
39054
39622
|
this.events.emit("layout:changed", { layout: next.id });
|
|
39055
39623
|
this.markStateDirty();
|
|
39056
39624
|
}
|
|
39625
|
+
/** The identity of the cell maximized over the whole grid, or null. */
|
|
39626
|
+
get maximizedCell() {
|
|
39627
|
+
return this.maximizedId;
|
|
39628
|
+
}
|
|
39629
|
+
/**
|
|
39630
|
+
* Maximize one cell over the whole grid, or restore the layout with `null`. Pure
|
|
39631
|
+
* presentation: the other cells stay alive underneath — charts, subscriptions and
|
|
39632
|
+
* state untouched — so restoring is instant. The maximized cell becomes the active
|
|
39633
|
+
* one. Transient view state (also reachable from each cell's bottom-center view
|
|
39634
|
+
* cluster): switching layouts or applying a state document restores the grid.
|
|
39635
|
+
*/
|
|
39636
|
+
maximizeCell(id) {
|
|
39637
|
+
if (this.destroyed) return;
|
|
39638
|
+
if (id != null && (!this.cellsById.has(id) || this.def.cells.length <= 1)) return;
|
|
39639
|
+
if (id === this.maximizedId) return;
|
|
39640
|
+
this.maximizedId = id;
|
|
39641
|
+
if (id) this.setActiveCell(id);
|
|
39642
|
+
this.applyGrid();
|
|
39643
|
+
this.refreshCellControls();
|
|
39644
|
+
this.syncMobileMaximize();
|
|
39645
|
+
this.events.emit("cell:maximized", { id });
|
|
39646
|
+
}
|
|
39647
|
+
/** The mobile bar's maximize stop: one press isolates the ACTIVE chart over the
|
|
39648
|
+
* grid; while something is already isolated — the chart, or a pane inside it
|
|
39649
|
+
* (mobile's double-tap) — the press restores that instead. Every branch re-syncs
|
|
39650
|
+
* the stop on its own (`maximizeCell` directly, `panes.maximize` via its
|
|
39651
|
+
* synchronous `pane:changed`). */
|
|
39652
|
+
toggleMobileMaximize() {
|
|
39653
|
+
const cell = this.activeId ? this.cellsById.get(this.activeId) : void 0;
|
|
39654
|
+
if (!cell) return;
|
|
39655
|
+
if (this.maximizedId) this.maximizeCell(null);
|
|
39656
|
+
else if (cell.chart.panes.list().some((p) => p.maximized)) cell.chart.panes.maximize(null);
|
|
39657
|
+
else this.maximizeCell(cell.id);
|
|
39658
|
+
}
|
|
39659
|
+
/** Keep the mobile bar's maximize stop truthful: lit (inverse chip, restore
|
|
39660
|
+
* glyph) while the active chart covers the grid OR one of its panes is
|
|
39661
|
+
* maximized — the state a double-tap toggles is otherwise invisible on mobile. */
|
|
39662
|
+
syncMobileMaximize() {
|
|
39663
|
+
if (!this.mobileBar) return;
|
|
39664
|
+
const cell = this.activeId ? this.cellsById.get(this.activeId) : void 0;
|
|
39665
|
+
const paneMax = cell ? cell.chart.panes.list().some((p) => p.maximized) : false;
|
|
39666
|
+
this.mobileBar.setMaximizeActive(this.maximizedId != null || paneMax);
|
|
39667
|
+
}
|
|
39668
|
+
/**
|
|
39669
|
+
* Trade the SLOTS of two live cells — the grid arrangement changes, the cells
|
|
39670
|
+
* themselves (charts, indicators, drawings, the active flag) stay untouched.
|
|
39671
|
+
* What each cell's drag handle commits; also callable directly by hosts.
|
|
39672
|
+
*/
|
|
39673
|
+
swapCells(a, b) {
|
|
39674
|
+
if (this.destroyed || a === b) return;
|
|
39675
|
+
const i = this.order.indexOf(a);
|
|
39676
|
+
const j = this.order.indexOf(b);
|
|
39677
|
+
if (i < 0 || j < 0 || !this.cellsById.has(a) || !this.cellsById.has(b)) return;
|
|
39678
|
+
[this.order[i], this.order[j]] = [this.order[j], this.order[i]];
|
|
39679
|
+
for (const [k] of this.def.cells.entries()) {
|
|
39680
|
+
const host = this.cellsById.get(this.order[k] ?? "")?.host;
|
|
39681
|
+
if (host) this.gridEl.appendChild(host);
|
|
39682
|
+
}
|
|
39683
|
+
this.applyGrid();
|
|
39684
|
+
this.markStateDirty();
|
|
39685
|
+
}
|
|
39057
39686
|
resize() {
|
|
39058
39687
|
this.splitters.layout();
|
|
39059
39688
|
}
|
|
@@ -39123,6 +39752,7 @@ var VelaWorkspace = class {
|
|
|
39123
39752
|
this.mobileBar?.renderActions();
|
|
39124
39753
|
this.mobileBar?.setSymbol(cell.symbol);
|
|
39125
39754
|
this.mobileBar?.setTimeframe(cell.timeframe);
|
|
39755
|
+
this.syncMobileMaximize();
|
|
39126
39756
|
this.drawingPill?.onChart(cell.chart);
|
|
39127
39757
|
const pushHistory = () => this.topbar.setHistoryState(cell.history.canUndo, cell.history.canRedo);
|
|
39128
39758
|
this.historyUnsub?.();
|
|
@@ -39212,8 +39842,78 @@ var VelaWorkspace = class {
|
|
|
39212
39842
|
const host = this.cellsById.get(this.order[i] ?? "")?.host;
|
|
39213
39843
|
if (host) host.style.gridArea = perCell[slot.id]?.gridArea ?? "";
|
|
39214
39844
|
}
|
|
39845
|
+
this.applyMaximizePresentation();
|
|
39846
|
+
this.mountAttributionMark();
|
|
39215
39847
|
this.splitters.layout();
|
|
39216
39848
|
}
|
|
39849
|
+
/** Overlay the maximize presentation on the freshly applied grid: EVERY cell spans
|
|
39850
|
+
* the full track grid — the maximized one on top, the siblings invisible beneath
|
|
39851
|
+
* it (their charts stay alive — restoring is instant). The siblings must span too:
|
|
39852
|
+
* left in their slots they would auto-flow into implicit zero-height rows, whose
|
|
39853
|
+
* gaps steal height from the maximized cell and collapse their renderers to 0.
|
|
39854
|
+
* The splitter strips and the active ring hide via the `data-maximized` rules. */
|
|
39855
|
+
applyMaximizePresentation() {
|
|
39856
|
+
const maxId = this.maximizedId;
|
|
39857
|
+
if (maxId) this.gridEl.dataset.maximized = "1";
|
|
39858
|
+
else delete this.gridEl.dataset.maximized;
|
|
39859
|
+
for (const [id, cell] of this.cellsById) {
|
|
39860
|
+
const style = cell.host.style;
|
|
39861
|
+
if (maxId) style.gridArea = "1 / 1 / -1 / -1";
|
|
39862
|
+
style.zIndex = maxId && id === maxId ? "5" : "";
|
|
39863
|
+
style.visibility = maxId && id !== maxId ? "hidden" : "";
|
|
39864
|
+
}
|
|
39865
|
+
}
|
|
39866
|
+
/** Rebuild every cell's view cluster (the maximize gate or state changed). */
|
|
39867
|
+
refreshCellControls() {
|
|
39868
|
+
for (const cell of this.cellsById.values()) cell.refreshControls();
|
|
39869
|
+
}
|
|
39870
|
+
/** Drop the transient maximize on a structural change (layout switch, state
|
|
39871
|
+
* document) — WITH the event, so hosts tracking `cell:maximized` never drift
|
|
39872
|
+
* from `maximizedCell`. The caller's own grid re-apply paints the restore. */
|
|
39873
|
+
clearMaximized() {
|
|
39874
|
+
if (this.maximizedId == null) return;
|
|
39875
|
+
this.maximizedId = null;
|
|
39876
|
+
this.events.emit("cell:maximized", { id: null });
|
|
39877
|
+
}
|
|
39878
|
+
/** The live cell under a viewport point, excluding `excludeId` and any host a
|
|
39879
|
+
* maximize has hidden — the drag handle's hit-test. */
|
|
39880
|
+
cellAtPoint(x, y, excludeId) {
|
|
39881
|
+
for (const [id, cell] of this.cellsById) {
|
|
39882
|
+
if (id === excludeId || cell.host.style.visibility === "hidden") continue;
|
|
39883
|
+
const r = cell.host.getBoundingClientRect();
|
|
39884
|
+
if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return id;
|
|
39885
|
+
}
|
|
39886
|
+
return null;
|
|
39887
|
+
}
|
|
39888
|
+
/** Mark one cell as the live drop target of a grip drag (null clears all) —
|
|
39889
|
+
* the `data-drop-target` stylesheet rule paints the dashed preview ring. */
|
|
39890
|
+
setDropTarget(id) {
|
|
39891
|
+
for (const [cid, cell] of this.cellsById) {
|
|
39892
|
+
if (cid === id) cell.host.dataset.dropTarget = "1";
|
|
39893
|
+
else delete cell.host.dataset.dropTarget;
|
|
39894
|
+
}
|
|
39895
|
+
}
|
|
39896
|
+
/** The cell whose bottom-left corner the grid's attribution mark floats in — the
|
|
39897
|
+
* maximized cell while one covers the grid, else the bottom-left slot's cell. */
|
|
39898
|
+
bottomLeftCell() {
|
|
39899
|
+
if (this.maximizedId) return this.cellsById.get(this.maximizedId);
|
|
39900
|
+
const grid = occupancyGrid(this.def);
|
|
39901
|
+
const slot = grid[grid.length - 1]?.[0];
|
|
39902
|
+
const idx = this.def.cells.findIndex((c) => (c.area ?? c.id) === slot);
|
|
39903
|
+
return this.cellsById.get(this.order[idx >= 0 ? idx : 0] ?? "");
|
|
39904
|
+
}
|
|
39905
|
+
/** Keep the shared attribution mark inside the BOTTOM-LEFT visible cell: its
|
|
39906
|
+
* offsets ride that cell's renderer-published `--vela-bottom-gutter` /
|
|
39907
|
+
* `--vela-toolbar-gutter`, so collapsed pane strips push the mark up without any
|
|
39908
|
+
* bookkeeping here. Re-run after anything that changes which host that is
|
|
39909
|
+
* (layout switch, maximize, cell rebuild); a destroyed host drops the mark from
|
|
39910
|
+
* the DOM, and this re-mount brings it back. */
|
|
39911
|
+
mountAttributionMark() {
|
|
39912
|
+
const mark = this.attributionMark;
|
|
39913
|
+
if (!mark) return;
|
|
39914
|
+
const host = this.bottomLeftCell()?.host ?? this.gridEl;
|
|
39915
|
+
if (mark.parentElement !== host) host.appendChild(mark);
|
|
39916
|
+
}
|
|
39217
39917
|
/** Create the cells the current layout wants but don't exist yet (pool-first).
|
|
39218
39918
|
* A slot's CELL IDENTITY is `order[i]` (declaration order — never the slot's own
|
|
39219
39919
|
* positional id); slots past the declared list mint an auto identity once. */
|
|
@@ -39245,6 +39945,12 @@ var VelaWorkspace = class {
|
|
|
39245
39945
|
setTimezone: (zone) => this.setTimezone(zone),
|
|
39246
39946
|
context: () => this.context(),
|
|
39247
39947
|
activate: (id2) => this.setActiveCell(id2),
|
|
39948
|
+
multiCell: () => !this.monoLayout && this.def.cells.length > 1,
|
|
39949
|
+
isMaximized: (id2) => this.maximizedId === id2,
|
|
39950
|
+
toggleMaximize: (id2) => this.maximizeCell(this.maximizedId === id2 ? null : id2),
|
|
39951
|
+
cellDragTarget: (id2, x, y) => this.cellAtPoint(x, y, id2),
|
|
39952
|
+
previewDropTarget: (target) => this.setDropTarget(target),
|
|
39953
|
+
dropCell: (id2, target) => this.swapCells(id2, target),
|
|
39248
39954
|
onMarketChanged: (id2) => this.onCellMarketChanged(id2),
|
|
39249
39955
|
onPriceStyleChanged: (id2) => this.onCellPriceStyleChanged(id2),
|
|
39250
39956
|
onIndicatorsChanged: (id2) => this.onCellIndicatorsChanged(id2),
|
|
@@ -39258,6 +39964,7 @@ var VelaWorkspace = class {
|
|
|
39258
39964
|
if (id === this.activeId) cell.host.dataset.active = "1";
|
|
39259
39965
|
this.wireCell(cell);
|
|
39260
39966
|
cell.chart.renderer.setLayoutMode(this.layoutCtl.current);
|
|
39967
|
+
cell.setControlsSuspended(this.layoutCtl.current === "mobile");
|
|
39261
39968
|
if (this.favs.length > 0) cell.chart.drawings.setFavorites(this.favs);
|
|
39262
39969
|
cell.setManifest(this.manifest, pooled?.indicators == null);
|
|
39263
39970
|
cell.restorePersistedExt();
|
|
@@ -39267,6 +39974,7 @@ var VelaWorkspace = class {
|
|
|
39267
39974
|
const host = this.cellsById.get(this.order[i] ?? "")?.host;
|
|
39268
39975
|
if (host) this.gridEl.appendChild(host);
|
|
39269
39976
|
}
|
|
39977
|
+
this.mountAttributionMark();
|
|
39270
39978
|
}
|
|
39271
39979
|
/** Per-cell chart subscriptions (trigger ② — the chart instance is stable for the
|
|
39272
39980
|
* cell's whole life, so these live and die with the cell). */
|
|
@@ -39326,6 +40034,9 @@ var VelaWorkspace = class {
|
|
|
39326
40034
|
chart.on("viewport:changed", (range) => this.propagateViewport(cell.id, range));
|
|
39327
40035
|
chart.renderer.onConfigChanged(() => this.propagateStylePrefs(cell.id));
|
|
39328
40036
|
chart.on("theme:changed", (t) => this.setTheme(t));
|
|
40037
|
+
chart.on("pane:changed", () => {
|
|
40038
|
+
if (cell.id === this.activeId) this.syncMobileMaximize();
|
|
40039
|
+
});
|
|
39329
40040
|
chart.renderer.onAxisLongPress((e) => {
|
|
39330
40041
|
if (this.layoutCtl.current !== "mobile") return;
|
|
39331
40042
|
if (e.axis === "time") this.openTimezoneDrawer();
|
|
@@ -39646,6 +40357,7 @@ var VelaWorkspace = class {
|
|
|
39646
40357
|
for (const cell of this.cellsById.values()) {
|
|
39647
40358
|
cell.chart.renderer.closeDialogs();
|
|
39648
40359
|
cell.chart.renderer.setLayoutMode(mode);
|
|
40360
|
+
cell.setControlsSuspended(mode === "mobile");
|
|
39649
40361
|
}
|
|
39650
40362
|
this.syncCellPresentation();
|
|
39651
40363
|
}
|