@luxalgo/vela 0.6.9 → 0.6.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{DataProvider-8Z95Q-RJ.d.cts → DataProvider-BBf-jc6W.d.ts} +49 -1
- package/dist/{DataProvider-DExJrfut.d.ts → DataProvider-p0TEyhlX.d.cts} +49 -1
- package/dist/{chunk-6WDDVMBJ.js → chunk-73PEA4MU.js} +707 -353
- package/dist/{chunk-62SVGONC.js → chunk-G77Y7LK2.js} +2 -2
- package/dist/{chunk-STHSKXOR.js → chunk-IO3NYSQV.js} +650 -157
- package/dist/{chunk-WFSZBX3R.js → chunk-KG4YT3TI.js} +3 -1
- package/dist/{chunk-FCVIG7JG.js → chunk-MHY7MVXH.js} +5 -1
- package/dist/{chunk-EQCHJZOT.js → chunk-MTLJKZDZ.js} +10 -2
- package/dist/{contributions-D7PVZO2i.d.ts → contributions-Bbe2R-mQ.d.ts} +20 -6
- package/dist/{contributions-C1U2Krwg.d.cts → contributions-CO01zWve.d.cts} +20 -6
- package/dist/index.cjs +719 -350
- package/dist/index.d.cts +37 -10
- package/dist/index.d.ts +37 -10
- package/dist/index.js +5 -5
- package/dist/{options-FM0peknS.d.ts → options-yp7sA96q.d.cts} +14 -7
- package/dist/{options-FM0peknS.d.cts → options-yp7sA96q.d.ts} +14 -7
- package/dist/{plugin-DfVqBz9p.d.cts → plugin-CkkH8QnX.d.cts} +3 -3
- package/dist/{plugin-7bkF32Rk.d.ts → plugin-DwxjM3Ni.d.ts} +3 -3
- package/dist/plugin.cjs +12 -1
- package/dist/plugin.d.cts +4 -4
- package/dist/plugin.d.ts +4 -4
- package/dist/plugin.js +3 -3
- package/dist/providers/binance.d.cts +2 -2
- package/dist/providers/binance.d.ts +2 -2
- package/dist/providers/coinbase.d.cts +2 -2
- package/dist/providers/coinbase.d.ts +2 -2
- package/dist/providers/hyperliquid.d.cts +2 -2
- package/dist/providers/hyperliquid.d.ts +2 -2
- package/dist/{statusline-DOPiT6I6.d.cts → statusline-CHPDuKNp.d.ts} +13 -5
- package/dist/{statusline-zdF4eZLr.d.ts → statusline-DTHZUFqK.d.cts} +13 -5
- package/dist/ui.cjs +7 -1
- package/dist/ui.d.cts +3 -1
- package/dist/ui.d.ts +3 -1
- package/dist/ui.js +3 -3
- package/dist/vela.global.js +719 -350
- package/dist/vela.global.min.js +51 -49
- package/dist/widget.cjs +1249 -388
- package/dist/widget.d.cts +17 -6
- package/dist/widget.d.ts +17 -6
- package/dist/widget.js +7 -7
- package/dist/workspace.cjs +1249 -388
- package/dist/workspace.d.cts +103 -10
- package/dist/workspace.d.ts +103 -10
- package/dist/workspace.js +6 -6
- package/package.json +1 -1
- /package/dist/{chunk-AOGZBKUE.js → chunk-H26BEHF4.js} +0 -0
package/dist/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
|
}
|
|
@@ -16703,11 +16880,12 @@ async function resolveIndicators(config, fetchImpl = fetch) {
|
|
|
16703
16880
|
}
|
|
16704
16881
|
return out;
|
|
16705
16882
|
}
|
|
16883
|
+
var ledgerEntryName = (e) => typeof e === "string" ? e : e.name;
|
|
16706
16884
|
function indicatorLedger(i) {
|
|
16707
16885
|
const natives = [...i.present];
|
|
16708
16886
|
if (i.volumePending && !natives.includes("volume")) natives.push("volume");
|
|
16709
16887
|
return {
|
|
16710
|
-
manifest: i.manifestSettled ? [...i.
|
|
16888
|
+
manifest: i.manifestSettled ? [...i.instanceEntries] : [...i.pendingManifest ?? i.instanceEntries],
|
|
16711
16889
|
natives
|
|
16712
16890
|
};
|
|
16713
16891
|
}
|
|
@@ -16918,7 +17096,7 @@ var DrawingToolbar = class {
|
|
|
16918
17096
|
this.root.replaceChildren();
|
|
16919
17097
|
this.groupCells.clear();
|
|
16920
17098
|
this.groupIcons.clear();
|
|
16921
|
-
this.cursorBtn = this.makeButton(CURSOR_ICON, "Cursor", () => this.
|
|
17099
|
+
this.cursorBtn = this.makeButton(CURSOR_ICON, "Cursor", () => this.onCursorClick());
|
|
16922
17100
|
this.root.appendChild(this.cursorBtn);
|
|
16923
17101
|
if (this.def.groups.length > 0) this.root.appendChild(this.divider());
|
|
16924
17102
|
for (const g of this.def.groups) {
|
|
@@ -17022,6 +17200,14 @@ var DrawingToolbar = class {
|
|
|
17022
17200
|
this.magnetIcon = icon2;
|
|
17023
17201
|
return cell;
|
|
17024
17202
|
}
|
|
17203
|
+
/** Cursor returns to select/idle: an active measure/eraser mode exits through its own
|
|
17204
|
+
* toggle callback (disarming a tool via `onArm(null)` alone can't — the host treats a
|
|
17205
|
+
* null arm as a no-op side effect of entering those modes), then the tool disarms. */
|
|
17206
|
+
onCursorClick() {
|
|
17207
|
+
if (this.measureActive) this.onMeasure();
|
|
17208
|
+
if (this.eraserActive) this.onEraser();
|
|
17209
|
+
this.onArm(null);
|
|
17210
|
+
}
|
|
17025
17211
|
/** Clicking the icon arms the group's last-used tool (it does NOT open the flyout). */
|
|
17026
17212
|
onGroupIconClick(group) {
|
|
17027
17213
|
const type = this.lastUsed.get(group.id) ?? group.tools[0]?.type;
|
|
@@ -17633,7 +17819,17 @@ function sanitizeCell(raw) {
|
|
|
17633
17819
|
if (c.drawings != null && typeof c.drawings === "object") out.drawings = c.drawings;
|
|
17634
17820
|
const ind = c.indicators;
|
|
17635
17821
|
if (ind != null && typeof ind === "object") {
|
|
17636
|
-
const manifest = Array.isArray(ind.manifest) ? ind.manifest.
|
|
17822
|
+
const manifest = Array.isArray(ind.manifest) ? ind.manifest.flatMap((n) => {
|
|
17823
|
+
if (typeof n === "string") return [n];
|
|
17824
|
+
if (n != null && typeof n === "object" && typeof n.name === "string") {
|
|
17825
|
+
const e = n;
|
|
17826
|
+
const bag = (v) => v != null && typeof v === "object" && !Array.isArray(v) ? v : void 0;
|
|
17827
|
+
const inputs = bag(e.inputs);
|
|
17828
|
+
const props = bag(e.props);
|
|
17829
|
+
return [inputs || props ? { name: e.name, ...inputs ? { inputs } : {}, ...props ? { props } : {} } : e.name];
|
|
17830
|
+
}
|
|
17831
|
+
return [];
|
|
17832
|
+
}) : [];
|
|
17637
17833
|
const natives = Array.isArray(ind.natives) ? ind.natives.filter((n) => typeof n === "string") : [];
|
|
17638
17834
|
out.indicators = { manifest, natives };
|
|
17639
17835
|
}
|
|
@@ -17818,6 +18014,12 @@ var IndicatorHandleImpl = class {
|
|
|
17818
18014
|
get visible() {
|
|
17819
18015
|
return this.visibleState;
|
|
17820
18016
|
}
|
|
18017
|
+
inputValues() {
|
|
18018
|
+
return this.controller.inputValuesOf(this.id);
|
|
18019
|
+
}
|
|
18020
|
+
propValues() {
|
|
18021
|
+
return this.controller.propValuesOf(this.id);
|
|
18022
|
+
}
|
|
17821
18023
|
setInput(key, value) {
|
|
17822
18024
|
this.controller.applyInputs(this.id, { [key]: value });
|
|
17823
18025
|
}
|
|
@@ -17957,6 +18159,7 @@ var RUN_EMIT_THROTTLE_MS = 1e3;
|
|
|
17957
18159
|
var PREVIEW_BARS = 300;
|
|
17958
18160
|
var SINGLE_LOAD_BARS = 5e3;
|
|
17959
18161
|
var CHUNK_BARS = 1e4;
|
|
18162
|
+
var FIRST_PAINT_BARS = 100;
|
|
17960
18163
|
var GAP_FACTOR = 1.5;
|
|
17961
18164
|
var HEAL_COOLDOWN_MS = 5e3;
|
|
17962
18165
|
var EngineOrchestrator = class _EngineOrchestrator {
|
|
@@ -18020,6 +18223,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18020
18223
|
/** Invalidates detached async work (backfill loops, in-flight loads, gap heals):
|
|
18021
18224
|
* bumped by init(), setMarket() and destroy(). */
|
|
18022
18225
|
this.generation = 0;
|
|
18226
|
+
/** Aborts the in-flight PROGRESSIVE load's source polling on supersession — an
|
|
18227
|
+
* abandoned stream left polling to its own budget starves the browser's per-host
|
|
18228
|
+
* connection pool, and the NEXT symbol's very first fetch with it (measured). */
|
|
18229
|
+
this.progressiveAbort = null;
|
|
18023
18230
|
/** Awaiters racing a superseded load (setMarket callers) — released on every bump so they never hang. */
|
|
18024
18231
|
this.supersedeWaiters = [];
|
|
18025
18232
|
/** `history:complete` fired for the CURRENT load. Each market load re-arms the cycle
|
|
@@ -18162,6 +18369,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18162
18369
|
* superseded setMarket awaiters so their promises resolve instead of hanging. */
|
|
18163
18370
|
bumpGeneration() {
|
|
18164
18371
|
const gen = ++this.generation;
|
|
18372
|
+
this.progressiveAbort?.abort();
|
|
18373
|
+
this.progressiveAbort = null;
|
|
18165
18374
|
for (const w of this.supersedeWaiters.splice(0)) w();
|
|
18166
18375
|
return gen;
|
|
18167
18376
|
}
|
|
@@ -18215,7 +18424,48 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18215
18424
|
const requested = market.bars ?? 500;
|
|
18216
18425
|
const initialRange = market.visibleRange;
|
|
18217
18426
|
const deep = !market.data?.length && initialRange == null && requested > SINGLE_LOAD_BARS;
|
|
18218
|
-
|
|
18427
|
+
let progressiveServed = false;
|
|
18428
|
+
if (!market.data?.length && initialRange == null && this.feed.loadProgressive) {
|
|
18429
|
+
let painted = false;
|
|
18430
|
+
const paint = (bars, final) => {
|
|
18431
|
+
if (this.generation !== gen || !final && bars.length === 0) return;
|
|
18432
|
+
if (!painted && !final && bars.length < Math.min(requested, FIRST_PAINT_BARS)) return;
|
|
18433
|
+
this.setBarSeries(bars, painted ? { preserveView: true } : void 0);
|
|
18434
|
+
if (!painted && bars.length > 0) {
|
|
18435
|
+
painted = true;
|
|
18436
|
+
if (opts.firstLoad) this.activateBarLayers();
|
|
18437
|
+
if (!final) this.historyState = "backfill";
|
|
18438
|
+
}
|
|
18439
|
+
};
|
|
18440
|
+
const abort = new AbortController();
|
|
18441
|
+
this.progressiveAbort = abort;
|
|
18442
|
+
progressiveServed = await new Promise((firstPaint) => {
|
|
18443
|
+
let signaled = false;
|
|
18444
|
+
const signal = (served) => {
|
|
18445
|
+
if (!signaled) {
|
|
18446
|
+
signaled = true;
|
|
18447
|
+
firstPaint(served);
|
|
18448
|
+
}
|
|
18449
|
+
};
|
|
18450
|
+
abort.signal.addEventListener("abort", () => signal(true), { once: true });
|
|
18451
|
+
this.feed.loadProgressive(market, (bars) => {
|
|
18452
|
+
paint(bars, false);
|
|
18453
|
+
if (painted) signal(true);
|
|
18454
|
+
}, { signal: abort.signal }).then((full) => {
|
|
18455
|
+
if (this.progressiveAbort === abort) this.progressiveAbort = null;
|
|
18456
|
+
if (full == null) return signal(false);
|
|
18457
|
+
if (this.generation !== gen) return signal(true);
|
|
18458
|
+
paint(full, true);
|
|
18459
|
+
this.completeHistory(full.length >= requested ? "depth" : "genesis");
|
|
18460
|
+
signal(true);
|
|
18461
|
+
}).catch(() => {
|
|
18462
|
+
if (this.progressiveAbort === abort) this.progressiveAbort = null;
|
|
18463
|
+
if (this.generation === gen) this.completeHistory("aborted");
|
|
18464
|
+
signal(true);
|
|
18465
|
+
});
|
|
18466
|
+
});
|
|
18467
|
+
}
|
|
18468
|
+
if (progressiveServed) ; else if (deep && this.feed.loadRange) {
|
|
18219
18469
|
const head = await this.feed.load({ ...market, bars: Math.min(requested, CHUNK_BARS) });
|
|
18220
18470
|
if (this.generation !== gen) return;
|
|
18221
18471
|
this.setBarSeries(head);
|
|
@@ -18810,6 +19060,15 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18810
19060
|
record.pendingCause = "inputs";
|
|
18811
19061
|
if (record.session) record.session.update(record.inputValues);
|
|
18812
19062
|
else if (record.native && !record.hidden) record.native.instance.setInputs(record.inputValues);
|
|
19063
|
+
this.events.emit("indicator:inputs", { id });
|
|
19064
|
+
}
|
|
19065
|
+
/** IndicatorController: the CURRENT stored input values (defaults merged with edits). */
|
|
19066
|
+
inputValuesOf(id) {
|
|
19067
|
+
return { ...this.registry.get(id)?.inputValues };
|
|
19068
|
+
}
|
|
19069
|
+
/** IndicatorController: the CURRENT declaration-prop overrides. */
|
|
19070
|
+
propValuesOf(id) {
|
|
19071
|
+
return { ...this.registry.get(id)?.propValues };
|
|
18813
19072
|
}
|
|
18814
19073
|
/** IndicatorController: re-run an indicator with merged declaration-prop overrides.
|
|
18815
19074
|
* Same lifecycle as {@link applyInputs} — a prop change replays the whole script.
|
|
@@ -18823,6 +19082,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18823
19082
|
if (!record.hidden) this.setLoading(record, true);
|
|
18824
19083
|
record.pendingCause = "inputs";
|
|
18825
19084
|
record.session.update(record.inputValues, record.propValues);
|
|
19085
|
+
this.events.emit("indicator:inputs", { id });
|
|
18826
19086
|
}
|
|
18827
19087
|
/** IndicatorController: tear down an indicator and (if now empty) its pane. */
|
|
18828
19088
|
/** Live handles of every indicator on the chart (script + native), insertion order. */
|
|
@@ -20044,6 +20304,14 @@ function inputVisible(when, values) {
|
|
|
20044
20304
|
const conds = Array.isArray(when) ? when : [when];
|
|
20045
20305
|
return conds.every((c) => c.anyOf ? c.anyOf.some((x) => x === values[c.key]) : values[c.key] === c.equals);
|
|
20046
20306
|
}
|
|
20307
|
+
function inputDeltas(schema, values) {
|
|
20308
|
+
const out = {};
|
|
20309
|
+
for (const s of schema) {
|
|
20310
|
+
const v = values[s.key];
|
|
20311
|
+
if (v !== void 0 && JSON.stringify(v) !== JSON.stringify(s.defval)) out[s.key] = v;
|
|
20312
|
+
}
|
|
20313
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
20314
|
+
}
|
|
20047
20315
|
|
|
20048
20316
|
// src/renderers/shared/IndicatorInputsDialog.ts
|
|
20049
20317
|
var PROPS_TAB = "Properties";
|
|
@@ -20958,6 +21226,9 @@ var CLOSE_SVG = iconAt("close", LEGEND_ICON_PX2);
|
|
|
20958
21226
|
var FOLD_SVG = iconAt("chevron-up", LEGEND_ICON_PX2);
|
|
20959
21227
|
var UNFOLD_SVG = iconAt("chevron-down", LEGEND_ICON_PX2);
|
|
20960
21228
|
var OVERVIEW_SVG = iconAt("objects", LEGEND_ICON_PX2);
|
|
21229
|
+
function legendCalloutsDisplay(open2, hasCallouts) {
|
|
21230
|
+
return !open2 && hasCallouts ? "inline-flex" : "none";
|
|
21231
|
+
}
|
|
20961
21232
|
var InputsUI = class {
|
|
20962
21233
|
constructor(container, theme, paneBoundsOf) {
|
|
20963
21234
|
this.container = container;
|
|
@@ -21161,7 +21432,7 @@ var InputsUI = class {
|
|
|
21161
21432
|
row.callouts = [];
|
|
21162
21433
|
row.calloutsEl.replaceChildren();
|
|
21163
21434
|
const views = this.legendCallouts?.(row.id) ?? [];
|
|
21164
|
-
row.calloutsEl.style.display = views.length > 0
|
|
21435
|
+
row.calloutsEl.style.display = legendCalloutsDisplay(row.highlighted, views.length > 0);
|
|
21165
21436
|
for (const view of views) {
|
|
21166
21437
|
const bubble = new CalloutBubble({
|
|
21167
21438
|
icon: view.icon,
|
|
@@ -21645,11 +21916,11 @@ var InputsUI = class {
|
|
|
21645
21916
|
row.controlsEl.style.display = open2 || row.hidden ? "inline-flex" : "none";
|
|
21646
21917
|
if (open2) {
|
|
21647
21918
|
row.el.appendChild(row.statusEl);
|
|
21648
|
-
row.
|
|
21919
|
+
for (const bubble of row.callouts) bubble.hidePanel();
|
|
21649
21920
|
} else {
|
|
21650
21921
|
row.el.insertBefore(row.statusEl, row.valuesEl);
|
|
21651
|
-
row.el.insertBefore(row.calloutsEl, row.statusEl);
|
|
21652
21922
|
}
|
|
21923
|
+
row.calloutsEl.style.display = legendCalloutsDisplay(open2, row.callouts.length > 0);
|
|
21653
21924
|
for (const child of Array.from(row.controlsEl.children)) {
|
|
21654
21925
|
if (!(child instanceof HTMLElement) || child === row.eyeEl) continue;
|
|
21655
21926
|
if (child === row.extrasEl) {
|
|
@@ -21739,7 +22010,7 @@ var ICONS = {
|
|
|
21739
22010
|
};
|
|
21740
22011
|
var STYLE_ID21 = "vela-pane-controls";
|
|
21741
22012
|
var ICON_PX = 12;
|
|
21742
|
-
var CLUSTER_PILL = "rgba(0,0,0,0.
|
|
22013
|
+
var CLUSTER_PILL = "rgba(0,0,0,0.65)";
|
|
21743
22014
|
function ensureStyles3() {
|
|
21744
22015
|
if (typeof document === "undefined" || document.getElementById(STYLE_ID21)) return;
|
|
21745
22016
|
const st = document.createElement("style");
|
|
@@ -21759,8 +22030,12 @@ var PaneControls = class {
|
|
|
21759
22030
|
this.deps = deps;
|
|
21760
22031
|
this.clusters = /* @__PURE__ */ new Map();
|
|
21761
22032
|
this.hoverPaneId = null;
|
|
22033
|
+
/** Mobile: hover clusters are meaningless without a cursor — suppressed; a
|
|
22034
|
+
* collapsed pane's standalone expand chip stays (the only way back up). */
|
|
22035
|
+
this.suspended = false;
|
|
21762
22036
|
/** Reveal the cluster for the pane under the cursor, resolved from the pointer's y in the plot. */
|
|
21763
22037
|
this.onPlotMove = (e) => {
|
|
22038
|
+
if (this.suspended) return;
|
|
21764
22039
|
const rect = this.plot.getBoundingClientRect();
|
|
21765
22040
|
const y = e.clientY - rect.top;
|
|
21766
22041
|
let hit = null;
|
|
@@ -21832,7 +22107,12 @@ var PaneControls = class {
|
|
|
21832
22107
|
}
|
|
21833
22108
|
if (p.count > 1) {
|
|
21834
22109
|
cluster.appendChild(
|
|
21835
|
-
this.button(p.maximized ? ICONS.restore : ICONS.maximize, p.maximized ? "Restore pane" : "Maximize pane", false, () => this.deps.onToggleMaximize(p.id), {
|
|
22110
|
+
this.button(p.maximized ? ICONS.restore : ICONS.maximize, p.maximized ? "Restore pane" : "Maximize pane", false, () => this.deps.onToggleMaximize(p.id), {
|
|
22111
|
+
role: "maximize",
|
|
22112
|
+
// Same inverse-chip treatment as the collapsed pane's expand toggle: the
|
|
22113
|
+
// maximized state must read as an active state, not just a swapped glyph.
|
|
22114
|
+
selected: p.maximized
|
|
22115
|
+
})
|
|
21836
22116
|
);
|
|
21837
22117
|
}
|
|
21838
22118
|
}
|
|
@@ -21869,17 +22149,18 @@ var PaneControls = class {
|
|
|
21869
22149
|
}
|
|
21870
22150
|
const hovered = id === this.hoverPaneId;
|
|
21871
22151
|
const hasButtons = cluster.children.length > 0;
|
|
21872
|
-
const
|
|
22152
|
+
const stateChipRole = p.collapsed ? "collapse" : !this.suspended && p.maximized ? "maximize" : null;
|
|
22153
|
+
const visible = hasButtons && (hovered || stateChipRole != null) && p.height > 8;
|
|
21873
22154
|
cluster.style.right = `${rightPx}px`;
|
|
21874
22155
|
cluster.style.top = p.collapsed ? `${p.top + Math.max(1, Math.round((p.height - 24) / 2))}px` : `${p.top + 4}px`;
|
|
21875
22156
|
cluster.style.display = visible ? "flex" : "none";
|
|
21876
22157
|
if (!visible) continue;
|
|
21877
|
-
const
|
|
21878
|
-
cluster.style.background =
|
|
22158
|
+
const soloChip = stateChipRole != null && !hovered;
|
|
22159
|
+
cluster.style.background = soloChip ? "transparent" : CLUSTER_PILL;
|
|
21879
22160
|
for (const child of cluster.children) {
|
|
21880
22161
|
const btn2 = child;
|
|
21881
22162
|
btn2.style.display = "inline-flex";
|
|
21882
|
-
btn2.style.visibility =
|
|
22163
|
+
btn2.style.visibility = soloChip && btn2.dataset.role !== stateChipRole ? "hidden" : "visible";
|
|
21883
22164
|
}
|
|
21884
22165
|
}
|
|
21885
22166
|
}
|
|
@@ -21889,6 +22170,14 @@ var PaneControls = class {
|
|
|
21889
22170
|
this.hoverPaneId = paneId;
|
|
21890
22171
|
this.reposition();
|
|
21891
22172
|
}
|
|
22173
|
+
/** Mobile suppression: no hover clusters (touch has no cursor; the shell's own
|
|
22174
|
+
* chrome covers maximize), while collapsed panes keep their expand chips. */
|
|
22175
|
+
setSuspended(on) {
|
|
22176
|
+
if (on === this.suspended) return;
|
|
22177
|
+
this.suspended = on;
|
|
22178
|
+
if (on) this.hoverPaneId = null;
|
|
22179
|
+
this.reposition();
|
|
22180
|
+
}
|
|
21892
22181
|
destroy() {
|
|
21893
22182
|
this.plot.removeEventListener("pointermove", this.onPlotMove);
|
|
21894
22183
|
this.plot.removeEventListener("pointerleave", this.onPlotLeave);
|
|
@@ -22039,160 +22328,6 @@ var AxisScaleButtons = class {
|
|
|
22039
22328
|
}
|
|
22040
22329
|
};
|
|
22041
22330
|
|
|
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
22331
|
// src/renderers/native/capabilities.ts
|
|
22197
22332
|
var NATIVE_CAPABILITIES = {
|
|
22198
22333
|
panes: true,
|
|
@@ -22213,7 +22348,7 @@ var NATIVE_CAPABILITIES = {
|
|
|
22213
22348
|
drawingDepth: true,
|
|
22214
22349
|
// drawings share the series' z space (backend-composited interleave layers)
|
|
22215
22350
|
tables: true,
|
|
22216
|
-
//
|
|
22351
|
+
// canvas-painted into the owning indicator's interleave slice
|
|
22217
22352
|
trades: true,
|
|
22218
22353
|
// strategy order-fill markers (arrows + labels + fill-price ticks)
|
|
22219
22354
|
inputsUI: true
|
|
@@ -22244,6 +22379,9 @@ function candleTier(spacing) {
|
|
|
22244
22379
|
if (spacing < CANDLE_BODY_MIN_SPACING) return "wick";
|
|
22245
22380
|
return "full";
|
|
22246
22381
|
}
|
|
22382
|
+
function snapY(yCss, dpr) {
|
|
22383
|
+
return Math.round(yCss * dpr) / dpr;
|
|
22384
|
+
}
|
|
22247
22385
|
function candleGeometry(xCss, spacing, dpr, bodyScale = 1) {
|
|
22248
22386
|
const wickDev = Math.max(1, Math.round(wickWidth(spacing) * dpr));
|
|
22249
22387
|
const wickLeftDev = Math.round(xCss * dpr - wickDev / 2);
|
|
@@ -22767,11 +22905,9 @@ var WebGL2Backend = class {
|
|
|
22767
22905
|
};
|
|
22768
22906
|
b.alpha = this.modelAlpha;
|
|
22769
22907
|
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
22908
|
if (isPrice) {
|
|
22772
22909
|
for (const m of scene.indicators.values()) {
|
|
22773
22910
|
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
22911
|
}
|
|
22776
22912
|
}
|
|
22777
22913
|
const drawCandles = isPrice && !scene.candlesHidden;
|
|
@@ -22787,6 +22923,7 @@ var WebGL2Backend = class {
|
|
|
22787
22923
|
b.alpha = this.modelAlpha;
|
|
22788
22924
|
const off = scene.offsetOf(m.id);
|
|
22789
22925
|
const mp = effPane(m);
|
|
22926
|
+
for (const f of m.fills) if (f.overlay !== true) this.emitFill(b, m, f, mp, coords, i0, i1, off);
|
|
22790
22927
|
for (const s of m.series) if (s.overlay !== true) this.emitSeries(b, s, mp, coords, i0, i1, theme, off);
|
|
22791
22928
|
}
|
|
22792
22929
|
if (drawCandles && !candleDrawn) {
|
|
@@ -22794,14 +22931,18 @@ var WebGL2Backend = class {
|
|
|
22794
22931
|
b.alpha = this.candleStructureAlpha;
|
|
22795
22932
|
this.emitPriceSeries(b, scene, i0, i1, coords, pane, theme, barColorMap, dataW);
|
|
22796
22933
|
}
|
|
22797
|
-
drawSlicesUpTo(Infinity);
|
|
22798
22934
|
b.alpha = this.modelAlpha;
|
|
22799
22935
|
if (isPrice) {
|
|
22936
|
+
for (const m of scene.indicators.values()) {
|
|
22937
|
+
const off = scene.offsetOf(m.id);
|
|
22938
|
+
for (const f of m.fills) if (f.overlay === true) this.emitFill(b, m, f, pane, coords, i0, i1, off);
|
|
22939
|
+
}
|
|
22800
22940
|
for (const m of scene.indicators.values()) {
|
|
22801
22941
|
const off = scene.offsetOf(m.id);
|
|
22802
22942
|
for (const s of m.series) if (s.overlay === true) this.emitSeries(b, s, pane, coords, i0, i1, theme, off);
|
|
22803
22943
|
}
|
|
22804
22944
|
}
|
|
22945
|
+
drawSlicesUpTo(Infinity);
|
|
22805
22946
|
for (const m of models) {
|
|
22806
22947
|
const mp = effPane(m);
|
|
22807
22948
|
for (const pl of m.priceLines) this.emitHline(b, pl, mp, coords, dataW, theme);
|
|
@@ -23196,12 +23337,12 @@ var WebGL2Backend = class {
|
|
|
23196
23337
|
if (drawBody) {
|
|
23197
23338
|
const oY = coords.priceToY(bar.open, pane.scale, pane.bounds);
|
|
23198
23339
|
const cY = coords.priceToY(bar.close, pane.scale, pane.bounds);
|
|
23199
|
-
bodyTop = Math.min(oY, cY);
|
|
23200
|
-
bodyH = Math.max(1, Math.
|
|
23340
|
+
bodyTop = snapY(Math.min(oY, cY), coords.dpr);
|
|
23341
|
+
bodyH = Math.max(1 / coords.dpr, snapY(Math.max(oY, cY), coords.dpr) - bodyTop);
|
|
23201
23342
|
}
|
|
23202
23343
|
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);
|
|
23344
|
+
const hY = snapY(coords.priceToY(bar.high, pane.scale, pane.bounds), coords.dpr);
|
|
23345
|
+
const lY = snapY(coords.priceToY(bar.low, pane.scale, pane.bounds), coords.dpr);
|
|
23205
23346
|
b.alpha = this.candleStructureAlpha;
|
|
23206
23347
|
const wCol = parseColor((isUp ? cs.wickUpColor : cs.wickDownColor) ?? (drawBody ? dir : bodyColorStr));
|
|
23207
23348
|
if (drawBody) {
|
|
@@ -23758,6 +23899,7 @@ var DOUBLE_TAP_MS = 350;
|
|
|
23758
23899
|
var DOUBLE_TAP_SLOP = 30;
|
|
23759
23900
|
var TIME_SCALE_K = 4e-3;
|
|
23760
23901
|
var WHEEL_ZOOM_K = 4e-3;
|
|
23902
|
+
var WHEEL_PRICE_DRAG_PX = 0.25;
|
|
23761
23903
|
function wheelZoomAnchor(coords, cursorX, rightEdge) {
|
|
23762
23904
|
if (rightEdge) return { logical: coords.rightEdgeLogical, x: coords.width };
|
|
23763
23905
|
return { logical: coords.xToLogical(cursorX), x: cursorX };
|
|
@@ -23888,7 +24030,7 @@ var InputController = class {
|
|
|
23888
24030
|
this.capture(e.pointerId);
|
|
23889
24031
|
return;
|
|
23890
24032
|
}
|
|
23891
|
-
if (e.shiftKey && this.regionAt(x, y) === "data" && this.deps.drawingsMeasureStart?.(x, y)) {
|
|
24033
|
+
if (e.shiftKey && this.regionAt(x, y) === "data" && this.deps.drawingsMeasureStart?.(x, y, this.snapMode(e))) {
|
|
23892
24034
|
this.region = "drawing";
|
|
23893
24035
|
this.capture(e.pointerId);
|
|
23894
24036
|
return;
|
|
@@ -24002,7 +24144,7 @@ var InputController = class {
|
|
|
24002
24144
|
const wasTouch = e.pointerType === "touch";
|
|
24003
24145
|
const tapRelease = this.dragging && !this.moved && (!wasTouch || Math.hypot(x - this.startX, y - this.startY) <= TOUCH_TAP_SLOP);
|
|
24004
24146
|
if (this.dragging && this.region === "drawing") {
|
|
24005
|
-
this.deps.drawingsPointerUp?.(x, y);
|
|
24147
|
+
this.deps.drawingsPointerUp?.(x, y, this.snapMode(e));
|
|
24006
24148
|
} else if (tapRelease && this.region === "data") {
|
|
24007
24149
|
this.deps.onClick(x, y);
|
|
24008
24150
|
} else if (this.dragging && this.region === "data") {
|
|
@@ -24023,7 +24165,7 @@ var InputController = class {
|
|
|
24023
24165
|
if (e.pointerType === "touch") this.touches.delete(e.pointerId);
|
|
24024
24166
|
this.cancelLongPress();
|
|
24025
24167
|
if (!this.dragging) return;
|
|
24026
|
-
if (this.region === "drawing" && !Number.isNaN(this.cursorX)) this.deps.drawingsPointerUp?.(this.cursorX, this.cursorY);
|
|
24168
|
+
if (this.region === "drawing" && !Number.isNaN(this.cursorX)) this.deps.drawingsPointerUp?.(this.cursorX, this.cursorY, this.snapMode(e));
|
|
24027
24169
|
if (this.region === "crosshair" || e.pointerType === "touch") this.deps.onPointerMove(null, null);
|
|
24028
24170
|
this.endGesture(e);
|
|
24029
24171
|
};
|
|
@@ -24039,6 +24181,12 @@ var InputController = class {
|
|
|
24039
24181
|
this.onWheel = (e) => {
|
|
24040
24182
|
e.preventDefault();
|
|
24041
24183
|
this.deps.drawingsClearTransient?.();
|
|
24184
|
+
const { x, y } = this.local(e);
|
|
24185
|
+
if (this.regionAt(x, y) === "price" && e.deltaY !== 0) {
|
|
24186
|
+
this.deps.beginPriceScale(x, y);
|
|
24187
|
+
this.deps.priceScaleBy(e.deltaY * WHEEL_PRICE_DRAG_PX);
|
|
24188
|
+
return;
|
|
24189
|
+
}
|
|
24042
24190
|
const coords = this.deps.getCoords();
|
|
24043
24191
|
const vp = coords.getViewport();
|
|
24044
24192
|
const pan = wheelPanDelta(e.deltaX, e.deltaY, e.shiftKey);
|
|
@@ -24046,9 +24194,8 @@ var InputController = class {
|
|
|
24046
24194
|
this.deps.apply({ barSpacing: vp.barSpacing, rightOffset: wheelPanRightOffset(vp.rightOffset, pan, coords.pxPerBar()) });
|
|
24047
24195
|
return;
|
|
24048
24196
|
}
|
|
24049
|
-
const cursorX = this.local(e).x;
|
|
24050
24197
|
const rightEdge = this.rightEdgeZoom && !(e.ctrlKey || e.metaKey);
|
|
24051
|
-
const anchor = wheelZoomAnchor(coords,
|
|
24198
|
+
const anchor = wheelZoomAnchor(coords, x, rightEdge);
|
|
24052
24199
|
const target = clampBarSpacing(vp.barSpacing * Math.exp(-e.deltaY * WHEEL_ZOOM_K));
|
|
24053
24200
|
this.deps.zoomTo(target, anchor.logical, anchor.x);
|
|
24054
24201
|
};
|
|
@@ -24534,9 +24681,10 @@ var SceneGraph = class {
|
|
|
24534
24681
|
* so each indicator arrives behind the candles (and behind older indicators);
|
|
24535
24682
|
* `setIndicatorZ`/`bringToFront`/`sendToBack` change it. */
|
|
24536
24683
|
this.seriesZ = /* @__PURE__ */ new Map();
|
|
24537
|
-
/** Per-pane raster layers of
|
|
24684
|
+
/** Per-pane raster layers of drawings interleaved into the series stack (each
|
|
24685
|
+
* indicator's Pine drawings at its model's z, plus in-stack user drawings) — each a
|
|
24538
24686
|
* prepainted canvas the backend composites just before the series carrying `beforeZ`.
|
|
24539
|
-
* Rebuilt by the renderer per data frame
|
|
24687
|
+
* Rebuilt by the renderer per data frame. */
|
|
24540
24688
|
this.drawingSlices = /* @__PURE__ */ new Map();
|
|
24541
24689
|
/** Per-model index offset: the chart bar index of the model's `anchorTime` — its
|
|
24542
24690
|
* index-aligned payloads (dense series arrays, `bar_index` drawings) count from that
|
|
@@ -24771,11 +24919,9 @@ var Canvas2dBackend = class {
|
|
|
24771
24919
|
};
|
|
24772
24920
|
ctx.globalAlpha = this.modelAlpha;
|
|
24773
24921
|
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
24922
|
if (isPrice) {
|
|
24776
24923
|
for (const m of scene.indicators.values()) {
|
|
24777
24924
|
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
24925
|
}
|
|
24780
24926
|
}
|
|
24781
24927
|
const slices = scene.drawingSlices.get(pane.id) ?? [];
|
|
@@ -24799,6 +24945,7 @@ var Canvas2dBackend = class {
|
|
|
24799
24945
|
ctx.globalAlpha = this.modelAlpha;
|
|
24800
24946
|
const off = scene.offsetOf(m.id);
|
|
24801
24947
|
const mp = effPane(m);
|
|
24948
|
+
for (const f of m.fills) if (f.overlay !== true) this.drawFill(ctx, m, f, mp, coords, i0, i1, off);
|
|
24802
24949
|
for (const s of m.series) if (s.overlay !== true) this.drawSeries(ctx, s, mp, coords, i0, i1, theme, off);
|
|
24803
24950
|
}
|
|
24804
24951
|
if (drawCandles && !candleDrawn) {
|
|
@@ -24806,14 +24953,18 @@ var Canvas2dBackend = class {
|
|
|
24806
24953
|
ctx.globalAlpha = this.candleStructureAlpha;
|
|
24807
24954
|
this.drawPriceSeries(ctx, scene, i0, i1, coords, pane, theme, barColorMap, dataW);
|
|
24808
24955
|
}
|
|
24809
|
-
drawSlicesUpTo(Infinity);
|
|
24810
24956
|
if (isPrice) {
|
|
24811
24957
|
ctx.globalAlpha = this.modelAlpha;
|
|
24958
|
+
for (const m of scene.indicators.values()) {
|
|
24959
|
+
const off = scene.offsetOf(m.id);
|
|
24960
|
+
for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0, i1, off);
|
|
24961
|
+
}
|
|
24812
24962
|
for (const m of scene.indicators.values()) {
|
|
24813
24963
|
const off = scene.offsetOf(m.id);
|
|
24814
24964
|
for (const s of m.series) if (s.overlay === true) this.drawSeries(ctx, s, pane, coords, i0, i1, theme, off);
|
|
24815
24965
|
}
|
|
24816
24966
|
}
|
|
24967
|
+
drawSlicesUpTo(Infinity);
|
|
24817
24968
|
ctx.globalAlpha = this.modelAlpha;
|
|
24818
24969
|
for (const m of models) {
|
|
24819
24970
|
const mp = effPane(m);
|
|
@@ -25057,13 +25208,13 @@ var Canvas2dBackend = class {
|
|
|
25057
25208
|
if (drawBody) {
|
|
25058
25209
|
const oY = coords.priceToY(b.open, pane.scale, pane.bounds);
|
|
25059
25210
|
const cY = coords.priceToY(b.close, pane.scale, pane.bounds);
|
|
25060
|
-
top = Math.min(oY, cY);
|
|
25061
|
-
bodyH = Math.max(1, Math.
|
|
25211
|
+
top = snapY(Math.min(oY, cY), coords.dpr);
|
|
25212
|
+
bodyH = Math.max(1 / coords.dpr, snapY(Math.max(oY, cY), coords.dpr) - top);
|
|
25062
25213
|
}
|
|
25063
25214
|
if (cs.wickVisible) {
|
|
25064
25215
|
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);
|
|
25216
|
+
const hY = snapY(coords.priceToY(b.high, pane.scale, pane.bounds), coords.dpr);
|
|
25217
|
+
const lY = snapY(coords.priceToY(b.low, pane.scale, pane.bounds), coords.dpr);
|
|
25067
25218
|
ctx.globalAlpha = this.candleStructureAlpha;
|
|
25068
25219
|
ctx.strokeStyle = wick;
|
|
25069
25220
|
ctx.lineWidth = g.wickW;
|
|
@@ -25510,9 +25661,31 @@ function autoFontSize(lines, boxW, boxH, bold) {
|
|
|
25510
25661
|
|
|
25511
25662
|
// src/renderers/shared/DrawingSceneRenderer.ts
|
|
25512
25663
|
var EMPTY_DRAWING_SET = { lines: [], boxes: [], labels: [], polylines: [], linefills: [] };
|
|
25664
|
+
function modelDrawingSet(m, overlay) {
|
|
25665
|
+
const want = (d) => Boolean(d.overlay) === overlay;
|
|
25666
|
+
return {
|
|
25667
|
+
lines: (m.lines ?? []).filter(want),
|
|
25668
|
+
boxes: (m.boxes ?? []).filter(want),
|
|
25669
|
+
labels: (m.labels ?? []).filter(want),
|
|
25670
|
+
polylines: (m.polylines ?? []).filter(want),
|
|
25671
|
+
linefills: (m.linefills ?? []).filter(want)
|
|
25672
|
+
};
|
|
25673
|
+
}
|
|
25674
|
+
function drawingSetEmpty(s) {
|
|
25675
|
+
return !s.lines.length && !s.boxes.length && !s.labels.length && !s.polylines.length && !s.linefills.length;
|
|
25676
|
+
}
|
|
25513
25677
|
function fontSizePx(size) {
|
|
25514
25678
|
return size === "auto" ? 12 : namedFontSize(size);
|
|
25515
25679
|
}
|
|
25680
|
+
function lineCoversWindow(a, b, extend, lo, hi) {
|
|
25681
|
+
const minX = Math.min(a, b);
|
|
25682
|
+
const maxX = Math.max(a, b);
|
|
25683
|
+
if (a === b) return a >= lo && a <= hi;
|
|
25684
|
+
if (extend === "both") return true;
|
|
25685
|
+
if (extend === "left") return maxX >= lo;
|
|
25686
|
+
if (extend === "right") return minX <= hi;
|
|
25687
|
+
return maxX >= lo && minX <= hi;
|
|
25688
|
+
}
|
|
25516
25689
|
var DrawingSceneRenderer = class {
|
|
25517
25690
|
constructor(deps, set = EMPTY_DRAWING_SET) {
|
|
25518
25691
|
this.deps = deps;
|
|
@@ -25577,7 +25750,7 @@ var DrawingSceneRenderer = class {
|
|
|
25577
25750
|
};
|
|
25578
25751
|
for (const ln of this.set.lines) {
|
|
25579
25752
|
if (ln.invisible) continue;
|
|
25580
|
-
if (!
|
|
25753
|
+
if (!lineCoversWindow(this.logicalOf(ln.xloc, ln.x1), this.logicalOf(ln.xloc, ln.x2), ln.extend, lo, hi)) continue;
|
|
25581
25754
|
fold(ln.y1);
|
|
25582
25755
|
fold(ln.y2);
|
|
25583
25756
|
}
|
|
@@ -26362,10 +26535,8 @@ var ChromeRenderer = class {
|
|
|
26362
26535
|
this.ctx = null;
|
|
26363
26536
|
// The color for axis tick labels — the host-passed surface text, set each frame in render().
|
|
26364
26537
|
this.axisTextColor = DARK_THEME.textColor;
|
|
26365
|
-
// Shared Pine-drawing renderer
|
|
26538
|
+
// Shared Pine-drawing renderer, used here for autoscale geometry only; widthCache persists.
|
|
26366
26539
|
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
26540
|
}
|
|
26370
26541
|
mount(canvas) {
|
|
26371
26542
|
this.canvas = canvas;
|
|
@@ -26389,8 +26560,8 @@ var ChromeRenderer = class {
|
|
|
26389
26560
|
*/
|
|
26390
26561
|
paneDrawingsRange(ownModels, scene, isPricePane, vr) {
|
|
26391
26562
|
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(
|
|
26563
|
+
for (const m of ownModels) dr = unionRange(dr, this.drawingsRange(modelDrawingSet(m, false), vr, scene.offsetOf(m.id)));
|
|
26564
|
+
if (isPricePane) for (const m of scene.indicators.values()) dr = unionRange(dr, this.drawingsRange(modelDrawingSet(m, true), vr, scene.offsetOf(m.id)));
|
|
26394
26565
|
return dr;
|
|
26395
26566
|
}
|
|
26396
26567
|
/** Clear the chrome canvas and draw drawings + axes + current-price line.
|
|
@@ -26407,7 +26578,6 @@ var ChromeRenderer = class {
|
|
|
26407
26578
|
const dataW = coords.width;
|
|
26408
26579
|
const dataH = coords.height;
|
|
26409
26580
|
this.axisTextColor = surface?.textColor ?? theme.textColor;
|
|
26410
|
-
this.labelTips = [];
|
|
26411
26581
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
26412
26582
|
ctx.clearRect(0, 0, fullW, fullH);
|
|
26413
26583
|
if (surface && (fullW > dataW || fullH > dataH)) {
|
|
@@ -26423,17 +26593,6 @@ var ChromeRenderer = class {
|
|
|
26423
26593
|
return;
|
|
26424
26594
|
}
|
|
26425
26595
|
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
26596
|
if (pricePane && !pricePane.collapsed && scene.tradeMarkers.visible) {
|
|
26438
26597
|
for (const m of scene.indicators.values()) {
|
|
26439
26598
|
if (m.trades?.length) this.renderTrades(ctx, coords, scene, theme, m.trades, pricePane, dataW);
|
|
@@ -26449,25 +26608,6 @@ var ChromeRenderer = class {
|
|
|
26449
26608
|
this.canvas = null;
|
|
26450
26609
|
this.ctx = null;
|
|
26451
26610
|
}
|
|
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
26611
|
drawingsRange(set, vr, indexOffset = 0) {
|
|
26472
26612
|
this.drawScene.setSet(set, indexOffset);
|
|
26473
26613
|
if (this.drawScene.isEmpty()) return null;
|
|
@@ -26501,34 +26641,6 @@ var ChromeRenderer = class {
|
|
|
26501
26641
|
);
|
|
26502
26642
|
ctx.restore();
|
|
26503
26643
|
}
|
|
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
26644
|
// ── axes ──
|
|
26533
26645
|
drawPriceAxes(ctx, scene, coords, theme, dataW, panes) {
|
|
26534
26646
|
ctx.strokeStyle = scene.style.borderColor ?? theme.borderColor;
|
|
@@ -29488,6 +29600,22 @@ var DrawingInteraction = class {
|
|
|
29488
29600
|
this.snapAt = changed ? { point: snapped, paneId } : null;
|
|
29489
29601
|
return snapped;
|
|
29490
29602
|
}
|
|
29603
|
+
/**
|
|
29604
|
+
* Resolve a cursor pixel through the magnet and return the snapped pixel — the same
|
|
29605
|
+
* conversion drawing placement uses. Updates the snap-ring marker. The measure
|
|
29606
|
+
* ruler goes through this so its endpoints follow weak/strong/Ctrl magnet too.
|
|
29607
|
+
*/
|
|
29608
|
+
snapCursor(x, y, mode) {
|
|
29609
|
+
const proj = this.deps.projector();
|
|
29610
|
+
const paneId = proj.paneIdAtY(y) ?? "price";
|
|
29611
|
+
const point = this.resolve(x, y, paneId, mode);
|
|
29612
|
+
const sy = proj.yOf(point.price, paneId);
|
|
29613
|
+
return { x: proj.xOf(point.time), y: sy ?? y };
|
|
29614
|
+
}
|
|
29615
|
+
/** Drop the snap-ring marker (a transient mode ended without going through `up`). */
|
|
29616
|
+
clearSnapMarker() {
|
|
29617
|
+
this.snapAt = null;
|
|
29618
|
+
}
|
|
29491
29619
|
/** Resolve a pixel to a data point with the segment angle locked to 45° steps around
|
|
29492
29620
|
* `pivot` (Shift held on a line tool). Works in PIXEL space — the user reasons about
|
|
29493
29621
|
* the angle they see, not about time/price units. The magnet is bypassed: snapping
|
|
@@ -30847,17 +30975,17 @@ function glyphIcon(glyph) {
|
|
|
30847
30975
|
return textGlyph(String(glyph), 15);
|
|
30848
30976
|
}
|
|
30849
30977
|
function stampSizeIcon(size) {
|
|
30850
|
-
return textGlyph("\u25CF", (
|
|
30978
|
+
return textGlyph("\u25CF", (SIZE_PX3[String(size)] ?? 13) + 4);
|
|
30851
30979
|
}
|
|
30852
30980
|
function sizeIcon(size) {
|
|
30853
30981
|
return textGlyph(String(size).charAt(0).toUpperCase(), 15);
|
|
30854
30982
|
}
|
|
30855
|
-
var
|
|
30983
|
+
var SIZE_PX3 = { small: 10, normal: 13, large: 16, huge: 20 };
|
|
30856
30984
|
function numbersSizeIcon(size) {
|
|
30857
|
-
return textGlyph("12", (
|
|
30985
|
+
return textGlyph("12", (SIZE_PX3[String(size)] ?? 13) - 1, 16.5, 'font-weight="600"');
|
|
30858
30986
|
}
|
|
30859
30987
|
function labelSizeIcon(size) {
|
|
30860
|
-
return textGlyph("T", (
|
|
30988
|
+
return textGlyph("T", (SIZE_PX3[String(size)] ?? 13) + 2);
|
|
30861
30989
|
}
|
|
30862
30990
|
function capitalize(s) {
|
|
30863
30991
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
@@ -30893,30 +31021,39 @@ var MeasureOverlay = class {
|
|
|
30893
31021
|
isFinished() {
|
|
30894
31022
|
return this.state === "finished";
|
|
30895
31023
|
}
|
|
30896
|
-
/** A press: begin the measurement, or finish it on the second click.
|
|
30897
|
-
|
|
31024
|
+
/** A press: begin the measurement, or finish it on the second click.
|
|
31025
|
+
* `x,y` are the raw cursor (drag-slop vs click-move-click). `gx,gy` are the
|
|
31026
|
+
* graphic endpoints — magnet-snapped when the magnet is on, else the same as `x,y`. */
|
|
31027
|
+
down(x, y, gx = x, gy = y) {
|
|
30898
31028
|
if (this.state === "measuring") {
|
|
30899
|
-
this.end = { x, y };
|
|
31029
|
+
this.end = { x: gx, y: gy };
|
|
30900
31030
|
this.state = "finished";
|
|
30901
31031
|
return;
|
|
30902
31032
|
}
|
|
30903
|
-
this.start = { x, y };
|
|
30904
|
-
this.end = { x, y };
|
|
31033
|
+
this.start = { x: gx, y: gy };
|
|
31034
|
+
this.end = { x: gx, y: gy };
|
|
30905
31035
|
this.pressX = x;
|
|
30906
31036
|
this.pressY = y;
|
|
30907
31037
|
this.state = "measuring";
|
|
30908
31038
|
}
|
|
31039
|
+
/** Size the in-progress ruler. `x,y` are the graphic (magnet-snapped) cursor. */
|
|
30909
31040
|
move(x, y) {
|
|
30910
31041
|
if (this.state === "measuring") this.end = { x, y };
|
|
30911
31042
|
}
|
|
30912
31043
|
/** A release: finish if the press was actually dragged (press-drag-release), else wait
|
|
30913
|
-
* for the second click (click-move-click).
|
|
30914
|
-
|
|
31044
|
+
* for the second click (click-move-click). `x,y` are the raw cursor (slop); `gx,gy`
|
|
31045
|
+
* are the graphic end (magnet-snapped when the magnet is on). */
|
|
31046
|
+
up(x, y, gx = x, gy = y) {
|
|
30915
31047
|
if (this.state === "measuring" && Math.hypot(x - this.pressX, y - this.pressY) > DRAG_SLOP4) {
|
|
30916
|
-
this.end = { x, y };
|
|
31048
|
+
this.end = { x: gx, y: gy };
|
|
30917
31049
|
this.state = "finished";
|
|
30918
31050
|
}
|
|
30919
31051
|
}
|
|
31052
|
+
/** Current graphic endpoints in media pixels, or null when idle. */
|
|
31053
|
+
points() {
|
|
31054
|
+
if (!this.start || !this.end) return null;
|
|
31055
|
+
return { start: this.start, end: this.end };
|
|
31056
|
+
}
|
|
30920
31057
|
clear() {
|
|
30921
31058
|
this.state = "idle";
|
|
30922
31059
|
this.start = null;
|
|
@@ -31293,11 +31430,13 @@ var UserDrawingController = class {
|
|
|
31293
31430
|
}
|
|
31294
31431
|
/** Shift+press on the empty plot: arm the measure ruler AND start it at (x, y) in one
|
|
31295
31432
|
* gesture — the equivalent of clicking the toolbar's Measure button, then pressing.
|
|
31296
|
-
*
|
|
31297
|
-
|
|
31433
|
+
* `snap` is the effective magnet (sticky mode, or Ctrl/Cmd-forced strong). Returns
|
|
31434
|
+
* false when a mode/tool is already active (the normal press path owns it). */
|
|
31435
|
+
beginMeasureAt(x, y, snap = "off") {
|
|
31298
31436
|
if (this.measureMode || this.eraserMode || this.activeTool != null) return false;
|
|
31299
31437
|
this.withModeIntent(() => this.toggleMeasure());
|
|
31300
|
-
this.
|
|
31438
|
+
const g = this.interaction.snapCursor(x, y, snap);
|
|
31439
|
+
this.measure.down(x, y, g.x, g.y);
|
|
31301
31440
|
this.render();
|
|
31302
31441
|
return true;
|
|
31303
31442
|
}
|
|
@@ -31319,7 +31458,8 @@ var UserDrawingController = class {
|
|
|
31319
31458
|
return;
|
|
31320
31459
|
}
|
|
31321
31460
|
if (this.measureMode) {
|
|
31322
|
-
this.
|
|
31461
|
+
const g = this.interaction.snapCursor(x, y, snap);
|
|
31462
|
+
this.measure.down(x, y, g.x, g.y);
|
|
31323
31463
|
if (this.measure.isFinished()) this.withModeIntent(() => this.exitMeasure(false));
|
|
31324
31464
|
this.render();
|
|
31325
31465
|
return;
|
|
@@ -31332,7 +31472,8 @@ var UserDrawingController = class {
|
|
|
31332
31472
|
return;
|
|
31333
31473
|
}
|
|
31334
31474
|
if (this.measureMode) {
|
|
31335
|
-
this.
|
|
31475
|
+
const g = this.interaction.snapCursor(x, y, snap);
|
|
31476
|
+
this.measure.move(g.x, g.y);
|
|
31336
31477
|
this.render();
|
|
31337
31478
|
return;
|
|
31338
31479
|
}
|
|
@@ -31350,13 +31491,14 @@ var UserDrawingController = class {
|
|
|
31350
31491
|
this.render();
|
|
31351
31492
|
}
|
|
31352
31493
|
}
|
|
31353
|
-
pointerUp(x, y) {
|
|
31494
|
+
pointerUp(x, y, snap = "off") {
|
|
31354
31495
|
if (this.eraserMode) {
|
|
31355
31496
|
this.erasing = false;
|
|
31356
31497
|
return;
|
|
31357
31498
|
}
|
|
31358
31499
|
if (this.measureMode) {
|
|
31359
|
-
this.
|
|
31500
|
+
const g = this.interaction.snapCursor(x, y, snap);
|
|
31501
|
+
this.measure.up(x, y, g.x, g.y);
|
|
31360
31502
|
if (this.measure.isFinished()) this.withModeIntent(() => this.exitMeasure(false));
|
|
31361
31503
|
this.render();
|
|
31362
31504
|
return;
|
|
@@ -31401,6 +31543,7 @@ var UserDrawingController = class {
|
|
|
31401
31543
|
/** Leave ruler mode. `clearGraphic` keeps a just-finished measurement on screen (false). */
|
|
31402
31544
|
exitMeasure(clearGraphic = true) {
|
|
31403
31545
|
this.measureMode = false;
|
|
31546
|
+
this.interaction.clearSnapMarker();
|
|
31404
31547
|
if (clearGraphic) this.measure.clear();
|
|
31405
31548
|
this.toolbar.setMeasureActive(false);
|
|
31406
31549
|
this.render();
|
|
@@ -31541,15 +31684,28 @@ var UserDrawingController = class {
|
|
|
31541
31684
|
if (this.eraserMode) return "pointer";
|
|
31542
31685
|
return this.interaction.cursorAt(x, y);
|
|
31543
31686
|
}
|
|
31544
|
-
/** Right-click
|
|
31545
|
-
*
|
|
31546
|
-
*
|
|
31547
|
-
*
|
|
31687
|
+
/** Right-click: an explicit escape back to the pointer. Cancels an in-progress
|
|
31688
|
+
* placement or measurement, and also plain-disarms an armed-but-idle drawing
|
|
31689
|
+
* tool or the eraser — so a right-click ALWAYS reverts to the pointer, even in
|
|
31690
|
+
* stay-in-drawing-mode (where Escape would leave a drawing tool armed).
|
|
31691
|
+
* Persistent toggles (magnet, stay-mode, favorites) are untouched. Returns
|
|
31692
|
+
* whether the press was consumed; false lets the host's context menu open
|
|
31693
|
+
* normally. */
|
|
31548
31694
|
cancelPlacement() {
|
|
31549
|
-
if (
|
|
31550
|
-
|
|
31551
|
-
|
|
31552
|
-
|
|
31695
|
+
if (this.measureMode || this.eraserMode) {
|
|
31696
|
+
this.withModeIntent(() => this.measureMode ? this.exitMeasure() : this.exitEraser());
|
|
31697
|
+
return true;
|
|
31698
|
+
}
|
|
31699
|
+
if (this.interaction.isPlacing()) {
|
|
31700
|
+
this.interaction.cancel();
|
|
31701
|
+
if (this.activeTool != null) this.emit({ kind: "arm", type: null });
|
|
31702
|
+
return true;
|
|
31703
|
+
}
|
|
31704
|
+
if (this.activeTool != null) {
|
|
31705
|
+
this.emit({ kind: "arm", type: null });
|
|
31706
|
+
return true;
|
|
31707
|
+
}
|
|
31708
|
+
return false;
|
|
31553
31709
|
}
|
|
31554
31710
|
/** Double-click over a drawing → suppress the chart's view reset (single-click already
|
|
31555
31711
|
* opens settings). Returns true only when a drawing is under the cursor. */
|
|
@@ -31575,6 +31731,10 @@ var UserDrawingController = class {
|
|
|
31575
31731
|
return true;
|
|
31576
31732
|
}
|
|
31577
31733
|
if (this.interaction.cancel()) return true;
|
|
31734
|
+
if (this.measureMode) {
|
|
31735
|
+
this.withModeIntent(() => this.exitMeasure());
|
|
31736
|
+
return true;
|
|
31737
|
+
}
|
|
31578
31738
|
if (this.selectedIds.size) {
|
|
31579
31739
|
this.clearSelection();
|
|
31580
31740
|
return true;
|
|
@@ -31832,6 +31992,315 @@ var UserDrawingController = class {
|
|
|
31832
31992
|
}
|
|
31833
31993
|
};
|
|
31834
31994
|
|
|
31995
|
+
// src/renderers/shared/TableOverlay.ts
|
|
31996
|
+
var SIZE_PX4 = {
|
|
31997
|
+
auto: 13,
|
|
31998
|
+
tiny: 10,
|
|
31999
|
+
small: 11,
|
|
32000
|
+
normal: 13,
|
|
32001
|
+
large: 16,
|
|
32002
|
+
huge: 20
|
|
32003
|
+
};
|
|
32004
|
+
function fontPxOf(size) {
|
|
32005
|
+
if (typeof size === "number") return size > 0 ? size : SIZE_PX4.auto;
|
|
32006
|
+
return SIZE_PX4[size] ?? SIZE_PX4.auto;
|
|
32007
|
+
}
|
|
32008
|
+
function tableHasContent(t) {
|
|
32009
|
+
return t.cells.some((row) => row?.some((c) => c != null && !c.merged));
|
|
32010
|
+
}
|
|
32011
|
+
function mergeRenderPlan(t) {
|
|
32012
|
+
const span = /* @__PURE__ */ new Map();
|
|
32013
|
+
const omit = /* @__PURE__ */ new Set();
|
|
32014
|
+
for (const m of t.merges) {
|
|
32015
|
+
span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
|
|
32016
|
+
for (let r = m.startRow; r <= m.endRow; r += 1) {
|
|
32017
|
+
for (let c = m.startCol; c <= m.endCol; c += 1) {
|
|
32018
|
+
if (r !== m.startRow || c !== m.startCol) omit.add(`${r}:${c}`);
|
|
32019
|
+
}
|
|
32020
|
+
}
|
|
32021
|
+
}
|
|
32022
|
+
for (let r = 0; r < t.rows; r += 1) {
|
|
32023
|
+
for (let c = 0; c < t.columns; c += 1) {
|
|
32024
|
+
if (t.cells[r]?.[c]?.merged && !span.has(`${r}:${c}`)) omit.add(`${r}:${c}`);
|
|
32025
|
+
}
|
|
32026
|
+
}
|
|
32027
|
+
for (const key of span.keys()) omit.delete(key);
|
|
32028
|
+
return { span, omit };
|
|
32029
|
+
}
|
|
32030
|
+
|
|
32031
|
+
// src/renderers/shared/TableCanvasRenderer.ts
|
|
32032
|
+
var PAD_X = 6;
|
|
32033
|
+
var PAD_Y = 2;
|
|
32034
|
+
var MARGIN = 6;
|
|
32035
|
+
var LINE_HEIGHT = 1.2;
|
|
32036
|
+
function paintTable(ctx, t, args, tips) {
|
|
32037
|
+
if (!tableHasContent(t)) return;
|
|
32038
|
+
const layout = layoutTable(ctx, t, args);
|
|
32039
|
+
if (!layout || layout.w <= 0 || layout.h <= 0) return;
|
|
32040
|
+
const fw = t.frameColor && t.frameWidth > 0 ? t.frameWidth : 0;
|
|
32041
|
+
const { x, y } = anchorOrigin(t.position, layout.w + 2 * fw, layout.h + 2 * fw, args);
|
|
32042
|
+
const x0 = x + fw;
|
|
32043
|
+
const y0 = y + fw;
|
|
32044
|
+
if (t.bgColor) {
|
|
32045
|
+
ctx.fillStyle = t.bgColor;
|
|
32046
|
+
ctx.fillRect(x0, y0, layout.w, layout.h);
|
|
32047
|
+
}
|
|
32048
|
+
if (fw > 0 && t.frameColor) {
|
|
32049
|
+
ctx.strokeStyle = t.frameColor;
|
|
32050
|
+
ctx.lineWidth = fw;
|
|
32051
|
+
ctx.strokeRect(x + fw / 2, y + fw / 2, layout.w + fw, layout.h + fw);
|
|
32052
|
+
}
|
|
32053
|
+
const colX = [0];
|
|
32054
|
+
for (const w of layout.colW) colX.push(colX[colX.length - 1] + w);
|
|
32055
|
+
const rowY = [0];
|
|
32056
|
+
for (const h of layout.rowH) rowY.push(rowY[rowY.length - 1] + h);
|
|
32057
|
+
const prevBaseline = ctx.textBaseline;
|
|
32058
|
+
const prevAlign = ctx.textAlign;
|
|
32059
|
+
ctx.textBaseline = "middle";
|
|
32060
|
+
for (const box of layout.boxes) {
|
|
32061
|
+
const rx = x0 + colX[box.c];
|
|
32062
|
+
const ry = y0 + rowY[box.r];
|
|
32063
|
+
const rw = colX[box.c + box.cs] - colX[box.c];
|
|
32064
|
+
const rh = rowY[box.r + box.rs] - rowY[box.r];
|
|
32065
|
+
const cell = box.cell;
|
|
32066
|
+
if (cell.bgColor) {
|
|
32067
|
+
ctx.fillStyle = cell.bgColor;
|
|
32068
|
+
ctx.fillRect(rx, ry, rw, rh);
|
|
32069
|
+
}
|
|
32070
|
+
const text = cell.text ?? "";
|
|
32071
|
+
if (text.length > 0) {
|
|
32072
|
+
const px = fontPxOf(cell.textSize);
|
|
32073
|
+
ctx.font = cellFont(cell, px, args.theme);
|
|
32074
|
+
ctx.fillStyle = cell.textColor ?? args.theme.textColor;
|
|
32075
|
+
const lines = text.split("\n");
|
|
32076
|
+
const blockH = lines.length * px * LINE_HEIGHT;
|
|
32077
|
+
const blockTop = cell.vAlign === "top" ? ry + PAD_Y : cell.vAlign === "bottom" ? ry + rh - PAD_Y - blockH : ry + (rh - blockH) / 2;
|
|
32078
|
+
const tx = cell.hAlign === "left" ? rx + PAD_X : cell.hAlign === "right" ? rx + rw - PAD_X : rx + rw / 2;
|
|
32079
|
+
ctx.textAlign = cell.hAlign;
|
|
32080
|
+
lines.forEach((line, i) => ctx.fillText(line, tx, blockTop + (i + 0.5) * px * LINE_HEIGHT));
|
|
32081
|
+
}
|
|
32082
|
+
if (cell.tooltip) tips.push({ left: rx, top: ry, right: rx + rw, bottom: ry + rh, text: cell.tooltip });
|
|
32083
|
+
}
|
|
32084
|
+
ctx.textBaseline = prevBaseline;
|
|
32085
|
+
ctx.textAlign = prevAlign;
|
|
32086
|
+
if (t.borderColor && t.borderWidth > 0) {
|
|
32087
|
+
ctx.strokeStyle = t.borderColor;
|
|
32088
|
+
ctx.lineWidth = t.borderWidth;
|
|
32089
|
+
const seen = /* @__PURE__ */ new Set();
|
|
32090
|
+
ctx.beginPath();
|
|
32091
|
+
const edge = (ax, ay, bx, by) => {
|
|
32092
|
+
const key = `${ax},${ay},${bx},${by}`;
|
|
32093
|
+
if (seen.has(key)) return;
|
|
32094
|
+
seen.add(key);
|
|
32095
|
+
ctx.moveTo(ax, ay);
|
|
32096
|
+
ctx.lineTo(bx, by);
|
|
32097
|
+
};
|
|
32098
|
+
for (const box of layout.boxes) {
|
|
32099
|
+
const l = Math.round(x0 + colX[box.c]);
|
|
32100
|
+
const r = Math.round(x0 + colX[box.c + box.cs]);
|
|
32101
|
+
const tp = Math.round(y0 + rowY[box.r]);
|
|
32102
|
+
const bt = Math.round(y0 + rowY[box.r + box.rs]);
|
|
32103
|
+
edge(l, tp, r, tp);
|
|
32104
|
+
edge(l, bt, r, bt);
|
|
32105
|
+
edge(l, tp, l, bt);
|
|
32106
|
+
edge(r, tp, r, bt);
|
|
32107
|
+
}
|
|
32108
|
+
ctx.stroke();
|
|
32109
|
+
}
|
|
32110
|
+
}
|
|
32111
|
+
function layoutTable(ctx, t, args) {
|
|
32112
|
+
const { span, omit } = mergeRenderPlan(t);
|
|
32113
|
+
const colW = new Array(t.columns).fill(0);
|
|
32114
|
+
const rowH = new Array(t.rows).fill(0);
|
|
32115
|
+
const boxes = [];
|
|
32116
|
+
for (let r = 0; r < t.rows; r += 1) {
|
|
32117
|
+
for (let c = 0; c < t.columns; c += 1) {
|
|
32118
|
+
if (omit.has(`${r}:${c}`)) continue;
|
|
32119
|
+
const cell = t.cells[r]?.[c];
|
|
32120
|
+
if (cell == null) continue;
|
|
32121
|
+
const sp = span.get(`${r}:${c}`);
|
|
32122
|
+
boxes.push({ cell, r, c, cs: Math.min(sp?.cs ?? 1, t.columns - c), rs: Math.min(sp?.rs ?? 1, t.rows - r) });
|
|
32123
|
+
}
|
|
32124
|
+
}
|
|
32125
|
+
if (boxes.length === 0) return null;
|
|
32126
|
+
const sizeOf = (cell) => {
|
|
32127
|
+
const px = fontPxOf(cell.textSize);
|
|
32128
|
+
ctx.font = cellFont(cell, px, args.theme);
|
|
32129
|
+
const lines = (cell.text ?? "").split("\n");
|
|
32130
|
+
let maxW = 0;
|
|
32131
|
+
for (const line of lines) maxW = Math.max(maxW, ctx.measureText(line).width);
|
|
32132
|
+
let w2 = Math.ceil(maxW) + 2 * PAD_X;
|
|
32133
|
+
let h2 = Math.ceil(lines.length * px * LINE_HEIGHT) + 2 * PAD_Y;
|
|
32134
|
+
if (cell.width) w2 = Math.max(w2, cell.width / 100 * args.plotWidth);
|
|
32135
|
+
if (cell.height) h2 = Math.max(h2, cell.height / 100 * args.paneHeight);
|
|
32136
|
+
return { w: w2, h: h2 };
|
|
32137
|
+
};
|
|
32138
|
+
const spanning = [];
|
|
32139
|
+
for (const box of boxes) {
|
|
32140
|
+
const { w: w2, h: h2 } = sizeOf(box.cell);
|
|
32141
|
+
if (box.cs === 1) colW[box.c] = Math.max(colW[box.c], w2);
|
|
32142
|
+
if (box.rs === 1) rowH[box.r] = Math.max(rowH[box.r], h2);
|
|
32143
|
+
if (box.cs > 1 || box.rs > 1) spanning.push({ box, w: w2, h: h2 });
|
|
32144
|
+
}
|
|
32145
|
+
for (const { box, w: w2, h: h2 } of spanning) {
|
|
32146
|
+
if (box.cs > 1) {
|
|
32147
|
+
let sum = 0;
|
|
32148
|
+
for (let c = box.c; c < box.c + box.cs; c += 1) sum += colW[c];
|
|
32149
|
+
if (w2 > sum) for (let c = box.c; c < box.c + box.cs; c += 1) colW[c] += (w2 - sum) / box.cs;
|
|
32150
|
+
}
|
|
32151
|
+
if (box.rs > 1) {
|
|
32152
|
+
let sum = 0;
|
|
32153
|
+
for (let r = box.r; r < box.r + box.rs; r += 1) sum += rowH[r];
|
|
32154
|
+
if (h2 > sum) for (let r = box.r; r < box.r + box.rs; r += 1) rowH[r] += (h2 - sum) / box.rs;
|
|
32155
|
+
}
|
|
32156
|
+
}
|
|
32157
|
+
let w = 0;
|
|
32158
|
+
for (const cw of colW) w += cw;
|
|
32159
|
+
let h = 0;
|
|
32160
|
+
for (const rh of rowH) h += rh;
|
|
32161
|
+
return { colW, rowH, w, h, boxes };
|
|
32162
|
+
}
|
|
32163
|
+
function anchorOrigin(position, totalW, totalH, args) {
|
|
32164
|
+
let y;
|
|
32165
|
+
if (position.startsWith("top")) y = MARGIN;
|
|
32166
|
+
else if (position.startsWith("bottom")) y = args.paneHeight - MARGIN - totalH;
|
|
32167
|
+
else y = args.paneHeight / 2 - totalH / 2;
|
|
32168
|
+
let x;
|
|
32169
|
+
if (position.endsWith("left")) x = MARGIN;
|
|
32170
|
+
else if (position.endsWith("right")) x = args.plotWidth - MARGIN - totalW;
|
|
32171
|
+
else x = args.plotWidth / 2 - totalW / 2;
|
|
32172
|
+
return { x, y };
|
|
32173
|
+
}
|
|
32174
|
+
function cellFont(cell, px, theme) {
|
|
32175
|
+
const family = cell.fontFamily === "monospace" ? "monospace" : theme.fontFamily || "sans-serif";
|
|
32176
|
+
return `${cell.italic ? "italic " : ""}${cell.bold ? "bold " : ""}${px}px ${family}`;
|
|
32177
|
+
}
|
|
32178
|
+
|
|
32179
|
+
// src/renderers/native/drawings/IndicatorDrawingSlices.ts
|
|
32180
|
+
function indicatorSliceKey(z, boundaries) {
|
|
32181
|
+
return boundaries.find((b) => b > z) ?? Infinity;
|
|
32182
|
+
}
|
|
32183
|
+
var IndicatorDrawingSlices = class {
|
|
32184
|
+
constructor() {
|
|
32185
|
+
this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
|
|
32186
|
+
/** Slice canvas cache, keyed `paneId|beforeZ` — same lifecycle as the user-drawing cache. */
|
|
32187
|
+
this.sliceCache = /* @__PURE__ */ new Map();
|
|
32188
|
+
/** Tooltip hit-rects of every label drawn this frame, in plot coords (rebuilt per prepare). */
|
|
32189
|
+
this.tips = [];
|
|
32190
|
+
}
|
|
32191
|
+
/**
|
|
32192
|
+
* Rebuild the per-indicator drawing slices for this data frame. `ref` is the data
|
|
32193
|
+
* canvas the slices must match pixel-for-pixel (the backend composites them 1:1).
|
|
32194
|
+
* Runs from the renderer's data paint, just before the backend composites the scene.
|
|
32195
|
+
*/
|
|
32196
|
+
prepare(scene, coords, theme, ref) {
|
|
32197
|
+
this.tips = [];
|
|
32198
|
+
const out = /* @__PURE__ */ new Map();
|
|
32199
|
+
if (ref.width === 0 || ref.height === 0) {
|
|
32200
|
+
this.sliceCache.clear();
|
|
32201
|
+
return out;
|
|
32202
|
+
}
|
|
32203
|
+
this.drawScene.setDeps({
|
|
32204
|
+
timeToLogical: (ms) => coords.timeToLogical(ms),
|
|
32205
|
+
barAt: (logical) => {
|
|
32206
|
+
const b = scene.bars[Math.round(logical)];
|
|
32207
|
+
return b ? { high: b.high, low: b.low } : null;
|
|
32208
|
+
},
|
|
32209
|
+
theme
|
|
32210
|
+
});
|
|
32211
|
+
const dpr = coords.dpr;
|
|
32212
|
+
const dataW = coords.width;
|
|
32213
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
32214
|
+
const add = (paneId, beforeZ, entry) => {
|
|
32215
|
+
const key = `${paneId}|${beforeZ}`;
|
|
32216
|
+
const bucket = buckets.get(key);
|
|
32217
|
+
if (bucket) bucket.entries.push(entry);
|
|
32218
|
+
else buckets.set(key, { paneId, beforeZ, entries: [entry] });
|
|
32219
|
+
};
|
|
32220
|
+
for (const pane of scene.orderedPanes()) {
|
|
32221
|
+
if (pane.collapsed) continue;
|
|
32222
|
+
const boundaries = scene.seriesBoundaries(pane.id);
|
|
32223
|
+
for (const m of scene.orderedIndicatorsForPane(pane.id)) {
|
|
32224
|
+
const set = modelDrawingSet(m, false);
|
|
32225
|
+
const tables = (m.tables ?? []).filter((t) => !t.overlay);
|
|
32226
|
+
if (drawingSetEmpty(set) && tables.length === 0) continue;
|
|
32227
|
+
const sc = scene.scaleFor(m, pane);
|
|
32228
|
+
const mp = sc === pane.scale ? pane : { ...pane, scale: sc };
|
|
32229
|
+
const beforeZ = indicatorSliceKey(scene.zOf(m.id), boundaries);
|
|
32230
|
+
add(pane.id, beforeZ, { set, tables, pane: mp, indexOffset: scene.offsetOf(m.id) });
|
|
32231
|
+
}
|
|
32232
|
+
if (pane.kind === "price") {
|
|
32233
|
+
for (const m of scene.indicators.values()) {
|
|
32234
|
+
const set = modelDrawingSet(m, true);
|
|
32235
|
+
const tables = (m.tables ?? []).filter((t) => t.overlay === true);
|
|
32236
|
+
if (drawingSetEmpty(set) && tables.length === 0) continue;
|
|
32237
|
+
add(pane.id, Infinity, { set, tables, pane, indexOffset: scene.offsetOf(m.id) });
|
|
32238
|
+
}
|
|
32239
|
+
}
|
|
32240
|
+
}
|
|
32241
|
+
for (const [key, { paneId, beforeZ, entries }] of buckets) {
|
|
32242
|
+
let canvas = this.sliceCache.get(key);
|
|
32243
|
+
if (!canvas) {
|
|
32244
|
+
canvas = document.createElement("canvas");
|
|
32245
|
+
this.sliceCache.set(key, canvas);
|
|
32246
|
+
}
|
|
32247
|
+
if (canvas.width !== ref.width || canvas.height !== ref.height) {
|
|
32248
|
+
canvas.width = ref.width;
|
|
32249
|
+
canvas.height = ref.height;
|
|
32250
|
+
}
|
|
32251
|
+
const ctx = canvas.getContext("2d");
|
|
32252
|
+
if (!ctx) continue;
|
|
32253
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
32254
|
+
ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
|
|
32255
|
+
for (const e of entries) this.paintEntry(ctx, e, coords, dataW, theme);
|
|
32256
|
+
const slices = out.get(paneId) ?? [];
|
|
32257
|
+
slices.push({ beforeZ, canvas });
|
|
32258
|
+
out.set(paneId, slices);
|
|
32259
|
+
}
|
|
32260
|
+
for (const key of [...this.sliceCache.keys()]) if (!buckets.has(key)) this.sliceCache.delete(key);
|
|
32261
|
+
for (const slices of out.values()) slices.sort((a, b) => a.beforeZ - b.beforeZ);
|
|
32262
|
+
return out;
|
|
32263
|
+
}
|
|
32264
|
+
paintEntry(ctx, e, coords, dataW, theme) {
|
|
32265
|
+
const { pane } = e;
|
|
32266
|
+
const paneTips = [];
|
|
32267
|
+
ctx.save();
|
|
32268
|
+
ctx.translate(0, pane.bounds.top);
|
|
32269
|
+
ctx.beginPath();
|
|
32270
|
+
ctx.rect(0, 0, dataW, pane.bounds.height);
|
|
32271
|
+
ctx.clip();
|
|
32272
|
+
this.drawScene.setSet(e.set, e.indexOffset);
|
|
32273
|
+
this.drawScene.render(
|
|
32274
|
+
ctx,
|
|
32275
|
+
dataW,
|
|
32276
|
+
pane.bounds.height,
|
|
32277
|
+
(l) => coords.logicalToX(l),
|
|
32278
|
+
(price) => coords.priceToY(price, pane.scale, pane.bounds) - pane.bounds.top
|
|
32279
|
+
);
|
|
32280
|
+
paneTips.push(...this.drawScene.labelTipRegions());
|
|
32281
|
+
for (const t of e.tables) paintTable(ctx, t, { paneHeight: pane.bounds.height, plotWidth: dataW, theme }, paneTips);
|
|
32282
|
+
ctx.restore();
|
|
32283
|
+
for (const r of paneTips) {
|
|
32284
|
+
this.tips.push({ ...r, top: r.top + pane.bounds.top, bottom: r.bottom + pane.bounds.top });
|
|
32285
|
+
}
|
|
32286
|
+
}
|
|
32287
|
+
/** Tooltip of the topmost label or table cell under a plot-space point, or null. Fed by the last prepare. */
|
|
32288
|
+
labelTooltipAt(x, y) {
|
|
32289
|
+
for (let i = this.tips.length - 1; i >= 0; i -= 1) {
|
|
32290
|
+
const r = this.tips[i];
|
|
32291
|
+
if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return r.text;
|
|
32292
|
+
}
|
|
32293
|
+
return null;
|
|
32294
|
+
}
|
|
32295
|
+
};
|
|
32296
|
+
function mergeSlices(indicator, user) {
|
|
32297
|
+
const out = /* @__PURE__ */ new Map();
|
|
32298
|
+
for (const [paneId, slices] of indicator) out.set(paneId, [...slices]);
|
|
32299
|
+
for (const [paneId, slices] of user) out.set(paneId, [...out.get(paneId) ?? [], ...slices]);
|
|
32300
|
+
for (const slices of out.values()) slices.sort((a, b) => a.beforeZ - b.beforeZ);
|
|
32301
|
+
return out;
|
|
32302
|
+
}
|
|
32303
|
+
|
|
31835
32304
|
// src/renderers/native/drawings/Projector.ts
|
|
31836
32305
|
function createProjector(coords, paneOf, paneIdAtY, barsInRange) {
|
|
31837
32306
|
return {
|
|
@@ -32577,6 +33046,11 @@ var NativeRenderer = class {
|
|
|
32577
33046
|
this.vpvrRenderer = new VpvrRenderer();
|
|
32578
33047
|
this.resizeObserver = null;
|
|
32579
33048
|
this.dprMedia = null;
|
|
33049
|
+
/** Plot size in INTEGER device px, as last reported by the resize observer's
|
|
33050
|
+
* device-pixel-content-box — the browser's own statement of how many device pixels
|
|
33051
|
+
* it paints the plot into. `null` until the first report or where the box type is
|
|
33052
|
+
* unsupported (WebKit); syncSize then falls back to rounding the client rect. */
|
|
33053
|
+
this.plotDeviceSize = null;
|
|
32580
33054
|
this.coords = new CoordinateSystem();
|
|
32581
33055
|
this.scene = new SceneGraph();
|
|
32582
33056
|
// chosen at mount (WebGL2 if available, else canvas2d)
|
|
@@ -32584,6 +33058,8 @@ var NativeRenderer = class {
|
|
|
32584
33058
|
this.glowAmount = 0;
|
|
32585
33059
|
// WebGL2 neon-glow intensity (canvas2d ignores it)
|
|
32586
33060
|
this.chrome = new ChromeRenderer();
|
|
33061
|
+
/** Prepaints each indicator's Pine drawings into interleave slices at the model's z. */
|
|
33062
|
+
this.indicatorSlices = new IndicatorDrawingSlices();
|
|
32587
33063
|
/** Hover tooltips for Pine labels (canvas hit-rects collected by the chrome layer). */
|
|
32588
33064
|
this.labelTooltip = null;
|
|
32589
33065
|
this.crosshairLayer = new CrosshairRenderer();
|
|
@@ -32732,7 +33208,6 @@ var NativeRenderer = class {
|
|
|
32732
33208
|
this.toggleVisibleCbs = /* @__PURE__ */ new Set();
|
|
32733
33209
|
this.moveIndicatorCbs = /* @__PURE__ */ new Set();
|
|
32734
33210
|
this.priceStyleCbs = /* @__PURE__ */ new Set();
|
|
32735
|
-
this.tableOverlays = /* @__PURE__ */ new Map();
|
|
32736
33211
|
this.name = "native";
|
|
32737
33212
|
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
33213
|
/** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
|
|
@@ -33046,7 +33521,6 @@ var NativeRenderer = class {
|
|
|
33046
33521
|
* hidden) on clear so a re-show picks up the current theme.
|
|
33047
33522
|
*/
|
|
33048
33523
|
setLoading(loading) {
|
|
33049
|
-
for (const overlay of this.tableOverlays.values()) overlay.setVisible(!loading);
|
|
33050
33524
|
if (!loading || !this.wrapper) {
|
|
33051
33525
|
this.loadingEl?.remove();
|
|
33052
33526
|
this.loadingEl = null;
|
|
@@ -33685,13 +34159,13 @@ var NativeRenderer = class {
|
|
|
33685
34159
|
resetView: () => this.resetView(),
|
|
33686
34160
|
// User drawings claim a gesture before pan when armed / over a drawing.
|
|
33687
34161
|
drawingsClaim: (x, y) => this.userDrawings?.claim(x, y) ?? false,
|
|
33688
|
-
drawingsMeasureStart: (x, y) => this.userDrawings?.beginMeasureAt(x, y) ?? false,
|
|
34162
|
+
drawingsMeasureStart: (x, y, snap) => this.userDrawings?.beginMeasureAt(x, y, snap) ?? false,
|
|
33689
34163
|
drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y) ?? false,
|
|
33690
34164
|
drawingsCancelPlacement: () => this.userDrawings?.cancelPlacement() ?? false,
|
|
33691
34165
|
drawingsSnapMode: () => this.snapMode,
|
|
33692
34166
|
drawingsPointerDown: (x, y, snap, shift) => this.userDrawings?.pointerDown(x, y, snap, shift),
|
|
33693
34167
|
drawingsPointerMove: (x, y, snap, shift) => this.userDrawings?.pointerMove(x, y, snap, shift),
|
|
33694
|
-
drawingsPointerUp: (x, y) => this.userDrawings?.pointerUp(x, y),
|
|
34168
|
+
drawingsPointerUp: (x, y, snap) => this.userDrawings?.pointerUp(x, y, snap),
|
|
33695
34169
|
drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? null,
|
|
33696
34170
|
drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
|
|
33697
34171
|
drawingsClearTransient: () => this.userDrawings?.clearTransient()
|
|
@@ -33708,7 +34182,7 @@ var NativeRenderer = class {
|
|
|
33708
34182
|
this.plot.addEventListener("pointerleave", this.onScrollProximityLeave);
|
|
33709
34183
|
this.labelTooltip = new LabelTooltip(this.plot, {
|
|
33710
34184
|
theme: () => this.chromeTheme(),
|
|
33711
|
-
lookup: (x, y) => this.
|
|
34185
|
+
lookup: (x, y) => this.indicatorSlices.labelTooltipAt(x, y)
|
|
33712
34186
|
});
|
|
33713
34187
|
this.userDrawings = new UserDrawingController(this.wrapper, this.plot, this.drawingsCanvas, {
|
|
33714
34188
|
projector: () => this.drawingProjector(),
|
|
@@ -33793,6 +34267,7 @@ var NativeRenderer = class {
|
|
|
33793
34267
|
this.emitPaneAction({ type: "maximize", paneId, maximized });
|
|
33794
34268
|
}
|
|
33795
34269
|
});
|
|
34270
|
+
this.paneControls.setSuspended(this.layoutMode === "mobile");
|
|
33796
34271
|
this.axisScaleButtons = new AxisScaleButtons(this.plot, theme, {
|
|
33797
34272
|
panes: () => this.axisScaleViews(),
|
|
33798
34273
|
rightAxis: () => this.rightAxisW,
|
|
@@ -33802,8 +34277,19 @@ var NativeRenderer = class {
|
|
|
33802
34277
|
if (pane) this.setPaneLog(paneId, !paneLogScale(this.scene, pane));
|
|
33803
34278
|
}
|
|
33804
34279
|
});
|
|
33805
|
-
this.resizeObserver = new ResizeObserver(() =>
|
|
34280
|
+
this.resizeObserver = new ResizeObserver((entries) => {
|
|
34281
|
+
for (const e of entries) {
|
|
34282
|
+
if (e.target !== this.plot) continue;
|
|
34283
|
+
const s = e.devicePixelContentBoxSize?.[0];
|
|
34284
|
+
if (s) this.plotDeviceSize = { width: s.inlineSize, height: s.blockSize };
|
|
34285
|
+
}
|
|
34286
|
+
this.resize();
|
|
34287
|
+
});
|
|
33806
34288
|
this.resizeObserver.observe(this.wrapper);
|
|
34289
|
+
try {
|
|
34290
|
+
this.resizeObserver.observe(this.plot, { box: "device-pixel-content-box" });
|
|
34291
|
+
} catch {
|
|
34292
|
+
}
|
|
33807
34293
|
this.watchDpr();
|
|
33808
34294
|
this.syncSize();
|
|
33809
34295
|
}
|
|
@@ -33928,8 +34414,6 @@ var NativeRenderer = class {
|
|
|
33928
34414
|
this.inputsUI?.destroy();
|
|
33929
34415
|
this.paneControls?.destroy();
|
|
33930
34416
|
this.axisScaleButtons?.destroy();
|
|
33931
|
-
for (const overlay of this.tableOverlays.values()) overlay.destroy();
|
|
33932
|
-
this.tableOverlays.clear();
|
|
33933
34417
|
this.resizeObserver?.disconnect();
|
|
33934
34418
|
this.resizeObserver = null;
|
|
33935
34419
|
this.dprMedia?.removeEventListener("change", this.onDprChange);
|
|
@@ -33958,6 +34442,7 @@ var NativeRenderer = class {
|
|
|
33958
34442
|
this.attributionEl = null;
|
|
33959
34443
|
this.mountContainer?.style.removeProperty("--vela-toolbar-gutter");
|
|
33960
34444
|
this.mountContainer?.style.removeProperty("--vela-scale-gutter");
|
|
34445
|
+
this.mountContainer?.style.removeProperty("--vela-bottom-gutter");
|
|
33961
34446
|
this.mountContainer?.style.removeProperty("--vela-price-pane-top");
|
|
33962
34447
|
this.mountContainer?.style.removeProperty("--vela-price-pane-bottom");
|
|
33963
34448
|
this.mountContainer = null;
|
|
@@ -33995,7 +34480,8 @@ var NativeRenderer = class {
|
|
|
33995
34480
|
}
|
|
33996
34481
|
const skipFit = opts?.preserveView === true && this.didInitialFit;
|
|
33997
34482
|
if (this.coords.width > 0 && !skipFit) {
|
|
33998
|
-
this.
|
|
34483
|
+
if (this.didInitialFit) this.reframeKeepZoom();
|
|
34484
|
+
else this.fitContent();
|
|
33999
34485
|
this.didInitialFit = true;
|
|
34000
34486
|
}
|
|
34001
34487
|
if (!this.introPlayed && this.bars.length > 0) {
|
|
@@ -34050,7 +34536,6 @@ var NativeRenderer = class {
|
|
|
34050
34536
|
ensurePane(pane) {
|
|
34051
34537
|
this.scene.ensurePane(pane.id, pane.kind, pane.order, pane.heightWeight ?? (pane.kind === "price" ? 3 : 1));
|
|
34052
34538
|
this.layoutPanes();
|
|
34053
|
-
this.repositionTables();
|
|
34054
34539
|
this.paneControls?.refresh();
|
|
34055
34540
|
this.scheduler.invalidate(4 /* Full */);
|
|
34056
34541
|
}
|
|
@@ -34072,17 +34557,14 @@ var NativeRenderer = class {
|
|
|
34072
34557
|
if (!model.ownScale) this.scene.dropIndicatorScale(handle.id);
|
|
34073
34558
|
this.inputsUI.setPane(handle.id, paneId);
|
|
34074
34559
|
this.refreshAnchorOffset(model);
|
|
34075
|
-
this.syncTables(model);
|
|
34076
34560
|
this.refreshAxisWidth();
|
|
34077
34561
|
this.layoutPanes();
|
|
34078
|
-
this.repositionTables();
|
|
34079
34562
|
this.paneControls?.refresh();
|
|
34080
34563
|
this.scheduler.invalidate(4 /* Full */);
|
|
34081
34564
|
}
|
|
34082
34565
|
orderPanes(orderedIds) {
|
|
34083
34566
|
this.scene.orderPanes(orderedIds);
|
|
34084
34567
|
this.layoutPanes();
|
|
34085
|
-
this.repositionTables();
|
|
34086
34568
|
this.paneControls?.refresh();
|
|
34087
34569
|
this.scheduler.invalidate(4 /* Full */);
|
|
34088
34570
|
}
|
|
@@ -34091,7 +34573,6 @@ var NativeRenderer = class {
|
|
|
34091
34573
|
if (!pane || pane.collapsed === collapsed) return;
|
|
34092
34574
|
pane.collapsed = collapsed;
|
|
34093
34575
|
this.layoutPanes();
|
|
34094
|
-
this.repositionTables();
|
|
34095
34576
|
this.paneControls?.refresh();
|
|
34096
34577
|
this.scheduler.invalidate(4 /* Full */);
|
|
34097
34578
|
}
|
|
@@ -34099,7 +34580,6 @@ var NativeRenderer = class {
|
|
|
34099
34580
|
if (paneId !== null && !this.scene.panes.has(paneId)) paneId = null;
|
|
34100
34581
|
this.maximizedPaneId = paneId;
|
|
34101
34582
|
this.layoutPanes();
|
|
34102
|
-
this.repositionTables();
|
|
34103
34583
|
this.paneControls?.refresh();
|
|
34104
34584
|
this.scheduler.invalidate(4 /* Full */);
|
|
34105
34585
|
}
|
|
@@ -34186,7 +34666,6 @@ var NativeRenderer = class {
|
|
|
34186
34666
|
native: !!model.native,
|
|
34187
34667
|
...model.props ? { props: model.props, propValues: model.propValues ?? {} } : {}
|
|
34188
34668
|
});
|
|
34189
|
-
this.syncTables(model);
|
|
34190
34669
|
if (model.native?.type === "volume") {
|
|
34191
34670
|
this.volumeActive = true;
|
|
34192
34671
|
this.volumeHidden = false;
|
|
@@ -34210,7 +34689,6 @@ var NativeRenderer = class {
|
|
|
34210
34689
|
}
|
|
34211
34690
|
}
|
|
34212
34691
|
applyPatch(model, patch);
|
|
34213
|
-
this.syncTables(model);
|
|
34214
34692
|
this.scheduler.invalidate(3 /* Light */);
|
|
34215
34693
|
}
|
|
34216
34694
|
removeIndicator(handle) {
|
|
@@ -34229,8 +34707,6 @@ var NativeRenderer = class {
|
|
|
34229
34707
|
this.scene.forgetAnchorOffset(handle.id);
|
|
34230
34708
|
this.scene.dropIndicatorScale(handle.id);
|
|
34231
34709
|
this.inputsUI.remove(handle.id);
|
|
34232
|
-
this.tableOverlays.get(handle.id)?.destroy();
|
|
34233
|
-
this.tableOverlays.delete(handle.id);
|
|
34234
34710
|
this.refreshAxisWidth();
|
|
34235
34711
|
this.paneControls?.refresh();
|
|
34236
34712
|
this.scheduler.invalidate(4 /* Full */);
|
|
@@ -34274,8 +34750,6 @@ var NativeRenderer = class {
|
|
|
34274
34750
|
}
|
|
34275
34751
|
if (!visible) {
|
|
34276
34752
|
this.scene.indicators.delete(handle.id);
|
|
34277
|
-
this.tableOverlays.get(handle.id)?.destroy();
|
|
34278
|
-
this.tableOverlays.delete(handle.id);
|
|
34279
34753
|
}
|
|
34280
34754
|
this.inputsUI.setVisible(handle.id, visible);
|
|
34281
34755
|
this.scheduler.invalidate(4 /* Full */);
|
|
@@ -34321,6 +34795,7 @@ var NativeRenderer = class {
|
|
|
34321
34795
|
this.userDrawings?.setLayoutMode(mode);
|
|
34322
34796
|
this.settingsDialog?.setLayoutMode(mode);
|
|
34323
34797
|
this.inputsUI?.setLayoutMode(mode);
|
|
34798
|
+
this.paneControls?.setSuspended(mode === "mobile");
|
|
34324
34799
|
if (this.scrollButton) {
|
|
34325
34800
|
const px = mode === "mobile" ? SCROLL_BTN_SIZE_TOUCH : SCROLL_BTN_SIZE;
|
|
34326
34801
|
this.scrollButton.style.width = `${px}px`;
|
|
@@ -34654,6 +35129,7 @@ var NativeRenderer = class {
|
|
|
34654
35129
|
this.scaleDragHeight = res.height;
|
|
34655
35130
|
this.scaleDragStart = { ...res.holder.scale };
|
|
34656
35131
|
res.holder.manualScale = { ...res.holder.scale };
|
|
35132
|
+
this.axisScaleButtons?.reposition();
|
|
34657
35133
|
this.scheduler.invalidate(4 /* Full */);
|
|
34658
35134
|
}
|
|
34659
35135
|
/** Rescale the grabbed scale around its center by the total drag (down ⇒ zoom out). */
|
|
@@ -34685,6 +35161,7 @@ var NativeRenderer = class {
|
|
|
34685
35161
|
const res = this.resolveScaleHolder(x, y);
|
|
34686
35162
|
if (!res) return;
|
|
34687
35163
|
res.holder.manualScale = null;
|
|
35164
|
+
this.axisScaleButtons?.reposition();
|
|
34688
35165
|
this.scheduler.invalidate(4 /* Full */);
|
|
34689
35166
|
}
|
|
34690
35167
|
/**
|
|
@@ -34716,7 +35193,6 @@ var NativeRenderer = class {
|
|
|
34716
35193
|
/** Relayout + repaint + refresh the hover buttons after a collapse/maximize/order change. */
|
|
34717
35194
|
afterPaneLayoutChange() {
|
|
34718
35195
|
this.layoutPanes();
|
|
34719
|
-
this.repositionTables();
|
|
34720
35196
|
this.paneControls?.refresh();
|
|
34721
35197
|
this.scheduler.invalidate(4 /* Full */);
|
|
34722
35198
|
}
|
|
@@ -34797,7 +35273,6 @@ var NativeRenderer = class {
|
|
|
34797
35273
|
above.heightWeight = next.above;
|
|
34798
35274
|
below.heightWeight = next.below;
|
|
34799
35275
|
this.layoutPanes();
|
|
34800
|
-
this.repositionTables();
|
|
34801
35276
|
this.scheduler.invalidate(4 /* Full */);
|
|
34802
35277
|
}
|
|
34803
35278
|
/** Double-click a separator → split the two adjacent panes evenly (each gets half of
|
|
@@ -34812,7 +35287,6 @@ var NativeRenderer = class {
|
|
|
34812
35287
|
above.heightWeight = half;
|
|
34813
35288
|
below.heightWeight = half;
|
|
34814
35289
|
this.layoutPanes();
|
|
34815
|
-
this.repositionTables();
|
|
34816
35290
|
this.scheduler.invalidate(4 /* Full */);
|
|
34817
35291
|
}
|
|
34818
35292
|
// ── keyboard navigation / accessibility (item 11) ──
|
|
@@ -35091,7 +35565,10 @@ var NativeRenderer = class {
|
|
|
35091
35565
|
const liveActual = li >= 0 ? this.bars[li] : void 0;
|
|
35092
35566
|
const easeLive = !!liveActual && this.liveEaseTime === liveActual.time && (liveActual.high !== this.liveEaseHigh || liveActual.low !== this.liveEaseLow || liveActual.close !== this.liveEaseClose);
|
|
35093
35567
|
if (easeLive && liveActual) this.bars[li] = { ...liveActual, high: this.liveEaseHigh, low: this.liveEaseLow, close: this.liveEaseClose };
|
|
35094
|
-
this.scene.drawingSlices =
|
|
35568
|
+
this.scene.drawingSlices = mergeSlices(
|
|
35569
|
+
this.indicatorSlices.prepare(this.scene, this.coords, this.theme, this.dataCanvas),
|
|
35570
|
+
this.userDrawings?.prepareSlices(this.scene.orderedPanes().map((p) => p.id)) ?? /* @__PURE__ */ new Map()
|
|
35571
|
+
);
|
|
35095
35572
|
this.backdropRenderer.render(this.scene, this.coords, this.theme, gridAlpha);
|
|
35096
35573
|
this.backend.render(this.scene, this.coords, this.theme);
|
|
35097
35574
|
this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
|
|
@@ -35322,6 +35799,20 @@ var NativeRenderer = class {
|
|
|
35322
35799
|
this.coords.setViewport(v);
|
|
35323
35800
|
this.targetBarSpacing = v.barSpacing;
|
|
35324
35801
|
}
|
|
35802
|
+
/** Re-frame after a series replacement (a symbol/timeframe switch): keep the user's
|
|
35803
|
+
* zoom (bar spacing), re-anchor the newest bars at the default right offset.
|
|
35804
|
+
* `clampViewport`'s fit-all-bars floor deliberately does NOT apply — a progressive
|
|
35805
|
+
* head may still be backfilling toward the previous depth, and raising the spacing
|
|
35806
|
+
* to its temporary bar count would lose the zoom this exists to keep. */
|
|
35807
|
+
reframeKeepZoom() {
|
|
35808
|
+
this.animator?.stop();
|
|
35809
|
+
this.panVelocity = 0;
|
|
35810
|
+
for (const pane of this.scene.panes.values()) pane.manualScale = null;
|
|
35811
|
+
for (const sl of this.scene.indicatorScales.values()) sl.manualScale = null;
|
|
35812
|
+
const v = { barSpacing: clampBarSpacing(this.coords.getViewport().barSpacing), rightOffset: defaultViewport().rightOffset };
|
|
35813
|
+
this.coords.setViewport(v);
|
|
35814
|
+
this.targetBarSpacing = v.barSpacing;
|
|
35815
|
+
}
|
|
35325
35816
|
paneBoundsFor(paneId) {
|
|
35326
35817
|
const p = this.scene.panes.get(paneId);
|
|
35327
35818
|
return { top: p?.bounds.top ?? 0, height: p?.bounds.height ?? 0, rightAxis: this.rightAxisW };
|
|
@@ -35529,27 +36020,6 @@ var NativeRenderer = class {
|
|
|
35529
36020
|
}
|
|
35530
36021
|
return maxVol;
|
|
35531
36022
|
}
|
|
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
36023
|
layoutPanes() {
|
|
35554
36024
|
const panes = this.scene.orderedPanes();
|
|
35555
36025
|
const dataHeight = this.coords.height;
|
|
@@ -35600,6 +36070,7 @@ var NativeRenderer = class {
|
|
|
35600
36070
|
const visible = maxPane ? [maxPane] : this.scene.orderedPanes().filter((p) => !p.collapsed);
|
|
35601
36071
|
const paneBottom = visible.length ? Math.max(...visible.map((p) => p.bounds.top + p.bounds.height)) : dataHeight;
|
|
35602
36072
|
this.scrollBtnBottomPx = SCROLL_BTN_BOTTOM + Math.max(0, dataHeight - paneBottom);
|
|
36073
|
+
this.mountContainer?.style.setProperty("--vela-bottom-gutter", `${TIME_AXIS_H + Math.max(0, dataHeight - paneBottom)}px`);
|
|
35603
36074
|
this.scrollBtnRightPx = this.rightAxisW + SCROLL_BTN_RIGHT_INSET;
|
|
35604
36075
|
if (this.scrollButton) {
|
|
35605
36076
|
this.scrollButton.style.bottom = `${this.scrollBtnBottomPx}px`;
|
|
@@ -35692,30 +36163,33 @@ var NativeRenderer = class {
|
|
|
35692
36163
|
if (w <= 0 || h <= 0) return;
|
|
35693
36164
|
const dpr = window.devicePixelRatio || 1;
|
|
35694
36165
|
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
|
-
|
|
36166
|
+
const rect = this.plot.getBoundingClientRect();
|
|
36167
|
+
let bw = Math.max(1, Math.round(rect.width * dpr));
|
|
36168
|
+
let bh = Math.max(1, Math.round(rect.height * dpr));
|
|
36169
|
+
const dev = this.plotDeviceSize;
|
|
36170
|
+
if (dev && Math.abs(dev.width - rect.width * dpr) <= 1 && Math.abs(dev.height - rect.height * dpr) <= 1) {
|
|
36171
|
+
bw = Math.max(1, dev.width);
|
|
36172
|
+
bh = Math.max(1, dev.height);
|
|
36173
|
+
}
|
|
36174
|
+
const pw = bw / dpr;
|
|
36175
|
+
const ph = bh / dpr;
|
|
36176
|
+
const size = (canvas) => {
|
|
36177
|
+
canvas.width = bw;
|
|
36178
|
+
canvas.height = bh;
|
|
36179
|
+
canvas.style.width = `${pw}px`;
|
|
36180
|
+
canvas.style.height = `${ph}px`;
|
|
36181
|
+
};
|
|
36182
|
+
size(this.dataCanvas);
|
|
36183
|
+
size(this.backdropCanvas);
|
|
36184
|
+
size(this.volumeCanvas);
|
|
36185
|
+
for (const l of this.extLayers) size(l.canvas);
|
|
36186
|
+
size(this.vpvrCanvas);
|
|
36187
|
+
size(this.chromeCanvas);
|
|
36188
|
+
size(this.drawingsCanvas);
|
|
36189
|
+
size(this.cursorCanvas);
|
|
35715
36190
|
this.coords.setSize(Math.max(1, pw - this.rightAxisW), Math.max(1, ph - TIME_AXIS_H), dpr);
|
|
35716
36191
|
this.scene.crosshair = null;
|
|
35717
36192
|
this.layoutPanes();
|
|
35718
|
-
this.repositionTables();
|
|
35719
36193
|
this.userDrawings?.onResize();
|
|
35720
36194
|
if (!this.didInitialFit && this.coords.barCount > 0) {
|
|
35721
36195
|
this.fitContent();
|
|
@@ -37034,9 +37508,182 @@ var Watermark = class {
|
|
|
37034
37508
|
}
|
|
37035
37509
|
};
|
|
37036
37510
|
|
|
37511
|
+
// src/widget/cell-controls.ts
|
|
37512
|
+
var CELL_CONTROLS_PROXIMITY_PX = 120;
|
|
37513
|
+
var TIME_AXIS_H2 = 22;
|
|
37514
|
+
var CONTROLS_BOTTOM_PX = TIME_AXIS_H2 + 12;
|
|
37515
|
+
var CLUSTER_H2 = 24;
|
|
37516
|
+
var CLUSTER_PILL2 = "rgba(0,0,0,0.65)";
|
|
37517
|
+
var CLUSTER_LEFT_CSS = "calc((100% + var(--vela-toolbar-gutter, 0px) - var(--vela-scale-gutter, 0px)) / 2)";
|
|
37518
|
+
var STYLE_ID26 = "vela-cell-controls";
|
|
37519
|
+
var CSS23 = `
|
|
37520
|
+
.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;}
|
|
37521
|
+
.vela-cc-btn svg{display:block;}
|
|
37522
|
+
.vela-cc-btn:hover{background:var(--vela-active);color:var(--vela-fg-bright);}
|
|
37523
|
+
.vela-cc-on,.vela-cc-on:hover{background:var(--vela-selected-bg);color:var(--vela-selected-fg);}
|
|
37524
|
+
.vela-cc-grip{cursor:grab;touch-action:none;}
|
|
37525
|
+
.vela-cc-grip:active{cursor:grabbing;}
|
|
37526
|
+
`;
|
|
37527
|
+
function plotCenterX(width, toolbarGutter = 0, scaleGutter = 0) {
|
|
37528
|
+
return (width + toolbarGutter - scaleGutter) / 2;
|
|
37529
|
+
}
|
|
37530
|
+
function nearBottomCenter(x, y, width, height, proximityPx = CELL_CONTROLS_PROXIMITY_PX, gutters = {}) {
|
|
37531
|
+
const cx = plotCenterX(width, gutters.toolbar ?? 0, gutters.scale ?? 0);
|
|
37532
|
+
const cy = height - CONTROLS_BOTTOM_PX - CLUSTER_H2 / 2;
|
|
37533
|
+
return Math.hypot(x - cx, y - cy) <= proximityPx;
|
|
37534
|
+
}
|
|
37535
|
+
var CellControls = class {
|
|
37536
|
+
constructor(host, deps) {
|
|
37537
|
+
this.host = host;
|
|
37538
|
+
this.deps = deps;
|
|
37539
|
+
this.near = false;
|
|
37540
|
+
/** A grip drag is underway — the proximity reveal must not hide the cluster
|
|
37541
|
+
* while captured pointer moves sweep across the whole grid. */
|
|
37542
|
+
this.dragging = false;
|
|
37543
|
+
/** Mobile: the proximity reveal is meaningless without a cursor — the mobile
|
|
37544
|
+
* bar's maximize stop replaces the cluster. */
|
|
37545
|
+
this.suspended = false;
|
|
37546
|
+
this.onHostMove = (e) => {
|
|
37547
|
+
if (this.suspended) return;
|
|
37548
|
+
if (this.dragging) return;
|
|
37549
|
+
const rect = this.host.getBoundingClientRect();
|
|
37550
|
+
this.setNear(nearBottomCenter(e.clientX - rect.left, e.clientY - rect.top, rect.width, rect.height, CELL_CONTROLS_PROXIMITY_PX, this.hostGutters()));
|
|
37551
|
+
};
|
|
37552
|
+
this.onHostLeave = () => {
|
|
37553
|
+
if (this.dragging) return;
|
|
37554
|
+
this.setNear(false);
|
|
37555
|
+
};
|
|
37556
|
+
injectStyles(STYLE_ID26, CSS23, host.ownerDocument);
|
|
37557
|
+
this.glider = new Glider(deps.chart);
|
|
37558
|
+
this.root = host.ownerDocument.createElement("div");
|
|
37559
|
+
Object.assign(this.root.style, {
|
|
37560
|
+
position: "absolute",
|
|
37561
|
+
left: CLUSTER_LEFT_CSS,
|
|
37562
|
+
bottom: `${CONTROLS_BOTTOM_PX}px`,
|
|
37563
|
+
transform: "translateX(-50%)",
|
|
37564
|
+
zIndex: "6",
|
|
37565
|
+
display: "none",
|
|
37566
|
+
// revealed by cursor proximity (onHostMove)
|
|
37567
|
+
gap: "2px",
|
|
37568
|
+
padding: "2px",
|
|
37569
|
+
borderRadius: "var(--vela-radius-md)",
|
|
37570
|
+
background: CLUSTER_PILL2,
|
|
37571
|
+
pointerEvents: "auto"
|
|
37572
|
+
});
|
|
37573
|
+
this.host.addEventListener("pointermove", this.onHostMove);
|
|
37574
|
+
this.host.addEventListener("pointerleave", this.onHostLeave);
|
|
37575
|
+
this.host.appendChild(this.root);
|
|
37576
|
+
this.refresh();
|
|
37577
|
+
}
|
|
37578
|
+
/** Rebuild the buttons (the multi-cell gate or the maximized state changed). */
|
|
37579
|
+
refresh() {
|
|
37580
|
+
this.root.textContent = "";
|
|
37581
|
+
const multi = this.deps.multiCell();
|
|
37582
|
+
const maximized = multi && this.deps.isMaximized();
|
|
37583
|
+
if (multi && !maximized) this.root.appendChild(this.makeGrip());
|
|
37584
|
+
this.root.appendChild(this.button("minus", "Zoom out", () => this.glider.zoom(ZOOM_OUT)));
|
|
37585
|
+
this.root.appendChild(this.button("plus", "Zoom in", () => this.glider.zoom(ZOOM_IN)));
|
|
37586
|
+
if (multi) {
|
|
37587
|
+
this.root.appendChild(
|
|
37588
|
+
this.button(maximized ? "restore" : "maximize", maximized ? "Restore layout" : "Maximize chart", () => this.deps.toggleMaximize(), {
|
|
37589
|
+
// The maximized state reads as an inverse chip (white-on-dark, dark-on-light),
|
|
37590
|
+
// the same active-state affordance as a collapsed pane's expand button.
|
|
37591
|
+
selected: maximized
|
|
37592
|
+
})
|
|
37593
|
+
);
|
|
37594
|
+
}
|
|
37595
|
+
this.root.appendChild(
|
|
37596
|
+
this.button("reset", "Reset chart", () => {
|
|
37597
|
+
this.glider.stop();
|
|
37598
|
+
this.deps.reset();
|
|
37599
|
+
})
|
|
37600
|
+
);
|
|
37601
|
+
}
|
|
37602
|
+
button(iconId, title, onClick, opts = {}) {
|
|
37603
|
+
const b = this.host.ownerDocument.createElement("button");
|
|
37604
|
+
b.type = "button";
|
|
37605
|
+
b.title = title;
|
|
37606
|
+
b.setAttribute("aria-label", title);
|
|
37607
|
+
b.className = opts.selected === true ? "vela-cc-btn vela-cc-on" : "vela-cc-btn";
|
|
37608
|
+
b.innerHTML = icon(iconId);
|
|
37609
|
+
b.addEventListener("click", (e) => {
|
|
37610
|
+
e.stopPropagation();
|
|
37611
|
+
onClick();
|
|
37612
|
+
});
|
|
37613
|
+
return b;
|
|
37614
|
+
}
|
|
37615
|
+
/** The drag handle (2×3 dot grip): press and drag onto another cell to trade
|
|
37616
|
+
* slots with it. The preview highlight follows the pointer; releasing outside
|
|
37617
|
+
* any other cell cancels. */
|
|
37618
|
+
makeGrip() {
|
|
37619
|
+
const b = this.host.ownerDocument.createElement("button");
|
|
37620
|
+
b.type = "button";
|
|
37621
|
+
b.title = "Drag to move chart";
|
|
37622
|
+
b.setAttribute("aria-label", "Drag to move chart");
|
|
37623
|
+
b.className = "vela-cc-btn vela-cc-grip";
|
|
37624
|
+
b.innerHTML = icon("grip");
|
|
37625
|
+
b.addEventListener("pointerdown", (e) => this.onGripDown(b, e));
|
|
37626
|
+
return b;
|
|
37627
|
+
}
|
|
37628
|
+
onGripDown(btn2, e) {
|
|
37629
|
+
if (e.button !== 0 && e.pointerType === "mouse") return;
|
|
37630
|
+
e.preventDefault();
|
|
37631
|
+
e.stopPropagation();
|
|
37632
|
+
try {
|
|
37633
|
+
btn2.setPointerCapture(e.pointerId);
|
|
37634
|
+
} catch {
|
|
37635
|
+
}
|
|
37636
|
+
this.dragging = true;
|
|
37637
|
+
let target = null;
|
|
37638
|
+
const move = (ev) => {
|
|
37639
|
+
target = this.deps.dragTargetAt(ev.clientX, ev.clientY);
|
|
37640
|
+
this.deps.previewDrop(target);
|
|
37641
|
+
};
|
|
37642
|
+
const finish = (commit) => () => {
|
|
37643
|
+
this.dragging = false;
|
|
37644
|
+
this.deps.previewDrop(null);
|
|
37645
|
+
btn2.removeEventListener("pointermove", move);
|
|
37646
|
+
btn2.removeEventListener("pointerup", onUp);
|
|
37647
|
+
btn2.removeEventListener("pointercancel", onCancel);
|
|
37648
|
+
if (commit && target != null) this.deps.dropOn(target);
|
|
37649
|
+
};
|
|
37650
|
+
const onUp = finish(true);
|
|
37651
|
+
const onCancel = finish(false);
|
|
37652
|
+
btn2.addEventListener("pointermove", move);
|
|
37653
|
+
btn2.addEventListener("pointerup", onUp);
|
|
37654
|
+
btn2.addEventListener("pointercancel", onCancel);
|
|
37655
|
+
}
|
|
37656
|
+
/** Mobile flips the cluster off entirely (and hides it if currently revealed). */
|
|
37657
|
+
setSuspended(on) {
|
|
37658
|
+
this.suspended = on;
|
|
37659
|
+
if (on) this.setNear(false);
|
|
37660
|
+
}
|
|
37661
|
+
/** Live renderer gutters on the cell host (0 when unpublished — a test stub). */
|
|
37662
|
+
hostGutters() {
|
|
37663
|
+
const view = this.host.ownerDocument.defaultView;
|
|
37664
|
+
if (!view) return { toolbar: 0, scale: 0 };
|
|
37665
|
+
const cs = view.getComputedStyle(this.host);
|
|
37666
|
+
return {
|
|
37667
|
+
toolbar: Number.parseFloat(cs.getPropertyValue("--vela-toolbar-gutter")) || 0,
|
|
37668
|
+
scale: Number.parseFloat(cs.getPropertyValue("--vela-scale-gutter")) || 0
|
|
37669
|
+
};
|
|
37670
|
+
}
|
|
37671
|
+
setNear(near) {
|
|
37672
|
+
if (near === this.near) return;
|
|
37673
|
+
this.near = near;
|
|
37674
|
+
this.root.style.display = near ? "flex" : "none";
|
|
37675
|
+
}
|
|
37676
|
+
destroy() {
|
|
37677
|
+
this.glider.stop();
|
|
37678
|
+
this.host.removeEventListener("pointermove", this.onHostMove);
|
|
37679
|
+
this.host.removeEventListener("pointerleave", this.onHostLeave);
|
|
37680
|
+
this.root.remove();
|
|
37681
|
+
}
|
|
37682
|
+
};
|
|
37683
|
+
|
|
37037
37684
|
// src/widget/context-menu.ts
|
|
37038
37685
|
var PRICE_AXIS_W = 60;
|
|
37039
|
-
var
|
|
37686
|
+
var TIME_AXIS_H3 = 26;
|
|
37040
37687
|
var ChartContextMenu = class {
|
|
37041
37688
|
constructor(host, cbs) {
|
|
37042
37689
|
this.cbs = cbs;
|
|
@@ -37075,7 +37722,7 @@ var ChartContextMenu = class {
|
|
|
37075
37722
|
zoneOf(e) {
|
|
37076
37723
|
const rect = this.host.getBoundingClientRect();
|
|
37077
37724
|
if (e.clientX - rect.left > rect.width - PRICE_AXIS_W) return "price-axis";
|
|
37078
|
-
if (e.clientY - rect.top > rect.height -
|
|
37725
|
+
if (e.clientY - rect.top > rect.height - TIME_AXIS_H3) return "time-axis";
|
|
37079
37726
|
return "body";
|
|
37080
37727
|
}
|
|
37081
37728
|
/** The pane under the pointer, so every pane's price scale has its own menu. */
|
|
@@ -37261,6 +37908,12 @@ function cellDrawings(opt) {
|
|
|
37261
37908
|
if (opt === true || opt == null) return { toolbar: false };
|
|
37262
37909
|
return { ...opt, toolbar: false };
|
|
37263
37910
|
}
|
|
37911
|
+
function instanceDeltas(handle) {
|
|
37912
|
+
if (!handle) return void 0;
|
|
37913
|
+
const inputs = inputDeltas(handle.inputs, handle.inputValues());
|
|
37914
|
+
const props = inputDeltas(handle.props, handle.propValues());
|
|
37915
|
+
return inputs || props ? { ...inputs ? { inputs } : {}, ...props ? { props } : {} } : void 0;
|
|
37916
|
+
}
|
|
37264
37917
|
var ChartCell = class {
|
|
37265
37918
|
constructor(id, gridHost, seed, deps) {
|
|
37266
37919
|
this.id = id;
|
|
@@ -37409,11 +38062,18 @@ var ChartCell = class {
|
|
|
37409
38062
|
if (this.inner && this.state.symbol) this.marketStatus?.track(this.inner.data, this.state.symbol);
|
|
37410
38063
|
});
|
|
37411
38064
|
this.syncStatuslineColors();
|
|
38065
|
+
this.cellControls = new CellControls(this.host, {
|
|
38066
|
+
chart: () => this.inner,
|
|
38067
|
+
reset: () => this.resetView(),
|
|
38068
|
+
multiCell: () => deps.multiCell(),
|
|
38069
|
+
isMaximized: () => deps.isMaximized(id),
|
|
38070
|
+
toggleMaximize: () => deps.toggleMaximize(id),
|
|
38071
|
+
dragTargetAt: (x, y) => deps.cellDragTarget(id, x, y),
|
|
38072
|
+
previewDrop: (target) => deps.previewDropTarget(target),
|
|
38073
|
+
dropOn: (target) => deps.dropCell(id, target)
|
|
38074
|
+
});
|
|
37412
38075
|
this.contextMenu = new ChartContextMenu(this.host, {
|
|
37413
|
-
resetView: () =>
|
|
37414
|
-
this.inner?.renderer.set("autoScale", true);
|
|
37415
|
-
this.inner?.setVisibleRangePreset("ALL");
|
|
37416
|
-
},
|
|
38076
|
+
resetView: () => this.resetView(),
|
|
37417
38077
|
timezone: () => this.deps.timezone(),
|
|
37418
38078
|
setTimezone: (zone) => this.deps.setTimezone(zone),
|
|
37419
38079
|
// Right-clicking activates the cell first (capture-phase pointerdown), so the
|
|
@@ -37429,6 +38089,7 @@ var ChartCell = class {
|
|
|
37429
38089
|
this.syncPresentNatives();
|
|
37430
38090
|
this.refreshNativeCatalog();
|
|
37431
38091
|
});
|
|
38092
|
+
this.inner.on("indicator:inputs", () => this.deps.onStateDirty());
|
|
37432
38093
|
this.inner.on("indicator:removed", ({ id: id2 }) => {
|
|
37433
38094
|
if (this.destroyed) return;
|
|
37434
38095
|
const idx = this.instances.findIndex((it) => it.handle?.id === id2);
|
|
@@ -37437,7 +38098,7 @@ var ChartCell = class {
|
|
|
37437
38098
|
this.instances.splice(idx, 1);
|
|
37438
38099
|
this.history.push({
|
|
37439
38100
|
undo: () => {
|
|
37440
|
-
snapshot.handle = this.addToChart(snapshot.entry);
|
|
38101
|
+
snapshot.handle = this.addToChart(snapshot.entry, snapshot.values);
|
|
37441
38102
|
this.instances.push(snapshot);
|
|
37442
38103
|
this.deps.onIndicatorsChanged(this.id);
|
|
37443
38104
|
},
|
|
@@ -37787,6 +38448,20 @@ var ChartCell = class {
|
|
|
37787
38448
|
this.inner.setVisibleRangePreset(preset.preset);
|
|
37788
38449
|
}
|
|
37789
38450
|
}
|
|
38451
|
+
/** Reset this cell's view: re-enable auto scale and frame the full history —
|
|
38452
|
+
* the same action the chart context menu offers. */
|
|
38453
|
+
resetView() {
|
|
38454
|
+
this.inner?.renderer.set("autoScale", true);
|
|
38455
|
+
this.inner?.setVisibleRangePreset("ALL");
|
|
38456
|
+
}
|
|
38457
|
+
/** Rebuild the view-controls cluster (the maximize gate or state changed). */
|
|
38458
|
+
refreshControls() {
|
|
38459
|
+
this.cellControls.refresh();
|
|
38460
|
+
}
|
|
38461
|
+
/** Mobile flips the per-cell cluster off (the shell's mobile bar replaces it). */
|
|
38462
|
+
setControlsSuspended(on) {
|
|
38463
|
+
this.cellControls.setSuspended(on);
|
|
38464
|
+
}
|
|
37790
38465
|
/** Make this cell the active one and put keyboard focus on its chart surface. */
|
|
37791
38466
|
focus() {
|
|
37792
38467
|
this.deps.activate(this.id);
|
|
@@ -37812,9 +38487,9 @@ var ChartCell = class {
|
|
|
37812
38487
|
this.manifest = list;
|
|
37813
38488
|
if (this.pendingManifestNames) {
|
|
37814
38489
|
if (list.length === 0) return;
|
|
37815
|
-
for (const
|
|
37816
|
-
const entry = list.find((e) => e.name ===
|
|
37817
|
-
if (entry) this.addManifestInstance(entry, { record: false });
|
|
38490
|
+
for (const led of this.pendingManifestNames) {
|
|
38491
|
+
const entry = list.find((e) => e.name === ledgerEntryName(led));
|
|
38492
|
+
if (entry) this.addManifestInstance(entry, { record: false, ...typeof led === "object" ? { inputs: led.inputs, props: led.props } : {} });
|
|
37818
38493
|
}
|
|
37819
38494
|
this.pendingManifestNames = null;
|
|
37820
38495
|
return;
|
|
@@ -37844,9 +38519,9 @@ var ChartCell = class {
|
|
|
37844
38519
|
}
|
|
37845
38520
|
for (const it of [...this.instances]) this.dropInstance(it);
|
|
37846
38521
|
if (this.manifest.length > 0) {
|
|
37847
|
-
for (const
|
|
37848
|
-
const entry = this.manifest.find((e) => e.name ===
|
|
37849
|
-
if (entry) this.addManifestInstance(entry, { record: false });
|
|
38522
|
+
for (const item of led.manifest) {
|
|
38523
|
+
const entry = this.manifest.find((e) => e.name === ledgerEntryName(item));
|
|
38524
|
+
if (entry) this.addManifestInstance(entry, { record: false, ...typeof item === "object" ? { inputs: item.inputs, props: item.props } : {} });
|
|
37850
38525
|
}
|
|
37851
38526
|
this.pendingManifestNames = null;
|
|
37852
38527
|
} else if (!this.deps.manifestSettled()) {
|
|
@@ -37895,12 +38570,13 @@ var ChartCell = class {
|
|
|
37895
38570
|
* a persistence handler's `restore` runs silently, a user-driven call records.
|
|
37896
38571
|
*/
|
|
37897
38572
|
addExternalIndicator(entry) {
|
|
37898
|
-
this.addManifestInstance({ ...entry, enabled: true }, { external: true });
|
|
38573
|
+
this.addManifestInstance({ ...entry, enabled: true }, { external: true, ...entry.inputs ? { inputs: entry.inputs } : {}, ...entry.props ? { props: entry.props } : {} });
|
|
37899
38574
|
}
|
|
37900
38575
|
/** Add ONE instance of a manifest entry (repeatable — duplicates are legitimate). */
|
|
37901
38576
|
addManifestInstance(entry, opts = {}) {
|
|
37902
38577
|
if (this.destroyed) return;
|
|
37903
|
-
const
|
|
38578
|
+
const values = opts.inputs || opts.props ? { inputs: opts.inputs, props: opts.props } : void 0;
|
|
38579
|
+
const it = { entry, handle: this.addToChart(entry, values), ...opts.external ? { external: true } : {}, ...values ? { values } : {} };
|
|
37904
38580
|
this.instances.push(it);
|
|
37905
38581
|
this.deps.onIndicatorsChanged(this.id);
|
|
37906
38582
|
if (opts.record === false) return;
|
|
@@ -37908,7 +38584,7 @@ var ChartCell = class {
|
|
|
37908
38584
|
this.history.push({
|
|
37909
38585
|
undo: () => this.dropInstance(snapshot),
|
|
37910
38586
|
redo: () => {
|
|
37911
|
-
snapshot.handle = this.addToChart(snapshot.entry);
|
|
38587
|
+
snapshot.handle = this.addToChart(snapshot.entry, snapshot.values);
|
|
37912
38588
|
this.instances.push(snapshot);
|
|
37913
38589
|
this.deps.onIndicatorsChanged(this.id);
|
|
37914
38590
|
}
|
|
@@ -37921,7 +38597,7 @@ var ChartCell = class {
|
|
|
37921
38597
|
const snapshot = it;
|
|
37922
38598
|
this.history.push({
|
|
37923
38599
|
undo: () => {
|
|
37924
|
-
snapshot.handle = this.addToChart(snapshot.entry);
|
|
38600
|
+
snapshot.handle = this.addToChart(snapshot.entry, snapshot.values);
|
|
37925
38601
|
this.instances.push(snapshot);
|
|
37926
38602
|
this.deps.onIndicatorsChanged(this.id);
|
|
37927
38603
|
},
|
|
@@ -37931,6 +38607,9 @@ var ChartCell = class {
|
|
|
37931
38607
|
dropInstance(it) {
|
|
37932
38608
|
const idx = this.instances.indexOf(it);
|
|
37933
38609
|
if (idx >= 0) this.instances.splice(idx, 1);
|
|
38610
|
+
const captured = instanceDeltas(it.handle);
|
|
38611
|
+
if (captured) it.values = captured;
|
|
38612
|
+
else delete it.values;
|
|
37934
38613
|
try {
|
|
37935
38614
|
it.handle?.remove();
|
|
37936
38615
|
} catch {
|
|
@@ -37973,9 +38652,13 @@ var ChartCell = class {
|
|
|
37973
38652
|
this.deps.onIndicatorsChanged(this.id);
|
|
37974
38653
|
});
|
|
37975
38654
|
}
|
|
37976
|
-
addToChart(entry) {
|
|
38655
|
+
addToChart(entry, values) {
|
|
37977
38656
|
try {
|
|
37978
|
-
return this.inner?.addIndicator(entry.script,
|
|
38657
|
+
return this.inner?.addIndicator(entry.script, {
|
|
38658
|
+
...entry.language !== void 0 ? { language: entry.language } : {},
|
|
38659
|
+
...values?.inputs ? { inputs: values.inputs } : {},
|
|
38660
|
+
...values?.props ? { props: values.props } : {}
|
|
38661
|
+
}) ?? null;
|
|
37979
38662
|
} catch (err) {
|
|
37980
38663
|
console.warn(`[vela] indicator "${entry.name}" failed to add:`, err);
|
|
37981
38664
|
return null;
|
|
@@ -38089,7 +38772,10 @@ var ChartCell = class {
|
|
|
38089
38772
|
// manifest — their plugin persists them via the `ext` seam instead.
|
|
38090
38773
|
indicators: indicatorLedger({
|
|
38091
38774
|
present: this.inner ? this.inner.presentNativeIndicators() : [],
|
|
38092
|
-
|
|
38775
|
+
instanceEntries: this.instances.filter((it) => !it.external).map((it) => {
|
|
38776
|
+
const d = it.handle ? instanceDeltas(it.handle) : it.values;
|
|
38777
|
+
return d ? { name: it.entry.name, ...d } : it.entry.name;
|
|
38778
|
+
}),
|
|
38093
38779
|
pendingManifest: this.pendingManifestNames,
|
|
38094
38780
|
manifestSettled: this.deps.manifestSettled(),
|
|
38095
38781
|
volumePending: this.volumeMayBePending && this.volumeIntent
|
|
@@ -38100,6 +38786,7 @@ var ChartCell = class {
|
|
|
38100
38786
|
destroy() {
|
|
38101
38787
|
this.destroyed = true;
|
|
38102
38788
|
this.offMarket();
|
|
38789
|
+
this.cellControls.destroy();
|
|
38103
38790
|
this.contextMenu.destroy();
|
|
38104
38791
|
this.history.destroy();
|
|
38105
38792
|
this.marketStatus?.stop();
|
|
@@ -38392,10 +39079,10 @@ var SplitterLayer = class {
|
|
|
38392
39079
|
var DEFAULT_TIMEFRAMES = ["1", "5", "15", "60", "240", "D", "W"];
|
|
38393
39080
|
var GAP_PX = 2;
|
|
38394
39081
|
var POOL_CAP = 16;
|
|
38395
|
-
var
|
|
39082
|
+
var TIME_AXIS_H4 = 22;
|
|
38396
39083
|
var ALERT_CAP = 50;
|
|
38397
|
-
var
|
|
38398
|
-
var
|
|
39084
|
+
var STYLE_ID27 = "vela-workspace";
|
|
39085
|
+
var CSS24 = `
|
|
38399
39086
|
.vela-workspace { position: relative; width: 100%; height: 100%; display: flex; flex-direction: column; background: var(--vela-bg); }
|
|
38400
39087
|
.vela-ws-main { position: relative; display: flex; flex-direction: row; flex: 1 1 auto; min-height: 0; }
|
|
38401
39088
|
.vela-ws-toolbar { position: relative; flex: none; }
|
|
@@ -38422,6 +39109,21 @@ var CSS23 = `
|
|
|
38422
39109
|
/* Mobile: the docked drawing-toolbar column would eat a phone-width grid \u2014 the shell's
|
|
38423
39110
|
drawings drawer + on-chart pill replace it (same policy as the widget's in-chart bar). */
|
|
38424
39111
|
[data-layout='mobile'] .vela-ws-toolbar { display: none; }
|
|
39112
|
+
/* A maximized cell owns the whole grid: the splitter strips have no seams to grab and
|
|
39113
|
+
the active ring would just outline the only visible chart \u2014 both are noise here. */
|
|
39114
|
+
.vela-ws-grid[data-maximized='1'] .vela-ws-splitter { display: none; }
|
|
39115
|
+
.vela-ws-grid[data-maximized='1'] .vela-cell[data-active='1']::after { display: none; }
|
|
39116
|
+
/* Drop-target preview while a cell's drag handle is held: a dashed ring + the same
|
|
39117
|
+
soft wash the splitter hover uses, over the chart, inert to the pointer. */
|
|
39118
|
+
.vela-cell[data-drop-target='1']::before {
|
|
39119
|
+
content: '';
|
|
39120
|
+
position: absolute;
|
|
39121
|
+
inset: 0;
|
|
39122
|
+
border: 2px dashed var(--vela-fg-bright);
|
|
39123
|
+
background: var(--vela-separator-hover-band);
|
|
39124
|
+
pointer-events: none;
|
|
39125
|
+
z-index: 11;
|
|
39126
|
+
}
|
|
38425
39127
|
`;
|
|
38426
39128
|
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
39129
|
function declaredOrder(cells) {
|
|
@@ -38451,6 +39153,9 @@ var VelaWorkspace = class {
|
|
|
38451
39153
|
* slots beyond the list get auto identities. Grows, never reorders. */
|
|
38452
39154
|
this.order = [];
|
|
38453
39155
|
this.activeId = null;
|
|
39156
|
+
/** The cell maximized over the whole grid (null = normal grid). TRANSIENT view
|
|
39157
|
+
* state — never persisted; any structural change (layout, applyState) restores. */
|
|
39158
|
+
this.maximizedId = null;
|
|
38454
39159
|
this.cellBackend = "auto";
|
|
38455
39160
|
this.destroyed = false;
|
|
38456
39161
|
this.shortcutsHelp = null;
|
|
@@ -38555,7 +39260,7 @@ var VelaWorkspace = class {
|
|
|
38555
39260
|
this.order = boot?.charts ? boot.charts.map((c) => c.id) : declaredOrder(opts.cells);
|
|
38556
39261
|
const bootActive = boot?.activeCellId ?? null;
|
|
38557
39262
|
const doc = hostEl.ownerDocument;
|
|
38558
|
-
injectStyles(
|
|
39263
|
+
injectStyles(STYLE_ID27, CSS24, doc);
|
|
38559
39264
|
this.root = doc.createElement("div");
|
|
38560
39265
|
this.root.className = "vela-workspace";
|
|
38561
39266
|
ensureUIHost(this.root, resolveTheme(opts.theme));
|
|
@@ -38665,7 +39370,11 @@ var VelaWorkspace = class {
|
|
|
38665
39370
|
if (attribution !== false) {
|
|
38666
39371
|
const background = resolveTheme(opts.theme).background;
|
|
38667
39372
|
const mark = typeof attribution === "string" && attribution.trim() ? createCustomMark(doc, attribution, background) : createAttributionMark(doc, background);
|
|
38668
|
-
Object.assign(mark.style, {
|
|
39373
|
+
Object.assign(mark.style, {
|
|
39374
|
+
left: "calc(var(--vela-toolbar-gutter, 0px) + 12px)",
|
|
39375
|
+
bottom: `calc(var(--vela-bottom-gutter, ${TIME_AXIS_H4}px) + 10px)`,
|
|
39376
|
+
zIndex: "11"
|
|
39377
|
+
});
|
|
38669
39378
|
this.gridEl.appendChild(mark);
|
|
38670
39379
|
this.attributionMark = mark;
|
|
38671
39380
|
}
|
|
@@ -38731,6 +39440,9 @@ var VelaWorkspace = class {
|
|
|
38731
39440
|
...topbarHas(this.topbarComp, "indicators") && (picker || this.indicatorsOverride) ? { onIndicatorsClick: this.indicatorsOverride ? () => this.runOverride(this.indicatorsOverride) : () => picker.open() } : {},
|
|
38732
39441
|
getContext: () => this.context(),
|
|
38733
39442
|
...this.drawingsEnabled ? { onDrawingsClick: () => this.openDrawingsDrawer() } : {},
|
|
39443
|
+
// Multi-chart only: the stop that isolates the ACTIVE chart (the
|
|
39444
|
+
// per-cell hover cluster has no cursor to reveal it on mobile).
|
|
39445
|
+
...this.monoLayout ? {} : { onMaximizeClick: () => this.toggleMobileMaximize() },
|
|
38734
39446
|
onMoreClick: () => this.openMoreDrawer(),
|
|
38735
39447
|
onSettingsClick: () => this.active.chart.renderer.openSettings()
|
|
38736
39448
|
}) : null;
|
|
@@ -38937,7 +39649,9 @@ var VelaWorkspace = class {
|
|
|
38937
39649
|
this.pool.clear();
|
|
38938
39650
|
for (const { id, ...cs } of st.charts.slice(liveCount)) this.pool.set(id, cs);
|
|
38939
39651
|
this.order = st.charts.map((c) => c.id);
|
|
39652
|
+
this.clearMaximized();
|
|
38940
39653
|
this.applyGrid();
|
|
39654
|
+
this.refreshCellControls();
|
|
38941
39655
|
const nextActive2 = st.activeCellId && this.cellsById.has(st.activeCellId) ? st.activeCellId : this.order[0] ?? null;
|
|
38942
39656
|
if (nextActive2 === this.activeId) this.projectActiveCell();
|
|
38943
39657
|
else this.setActiveCell(nextActive2);
|
|
@@ -38963,6 +39677,7 @@ var VelaWorkspace = class {
|
|
|
38963
39677
|
const def = this.monoLayout ? null : ensureLayout(st.layout);
|
|
38964
39678
|
if (def) this.def = def;
|
|
38965
39679
|
this.cellBackend = this.backendFor(this.def);
|
|
39680
|
+
this.clearMaximized();
|
|
38966
39681
|
this.applyGrid();
|
|
38967
39682
|
this.buildCells();
|
|
38968
39683
|
this.syncCellPresentation();
|
|
@@ -39026,6 +39741,7 @@ var VelaWorkspace = class {
|
|
|
39026
39741
|
setLayout(layout) {
|
|
39027
39742
|
if (this.destroyed) return;
|
|
39028
39743
|
if (this.monoLayout) return;
|
|
39744
|
+
this.clearMaximized();
|
|
39029
39745
|
const next = this.resolveLayout(layout);
|
|
39030
39746
|
const nextBackend = this.backendFor(next);
|
|
39031
39747
|
const rebuildAll = nextBackend !== this.cellBackend;
|
|
@@ -39046,6 +39762,7 @@ var VelaWorkspace = class {
|
|
|
39046
39762
|
this.buildCells();
|
|
39047
39763
|
this.alignNewCellStyles(preexisting);
|
|
39048
39764
|
this.syncCellPresentation();
|
|
39765
|
+
this.refreshCellControls();
|
|
39049
39766
|
this.topbar.setLayout(next.id);
|
|
39050
39767
|
const nextActive = activeAfterLayout(this.activeId, this.order.slice(0, next.cells.length));
|
|
39051
39768
|
if (nextActive === this.activeId) this.projectActiveCell();
|
|
@@ -39054,6 +39771,67 @@ var VelaWorkspace = class {
|
|
|
39054
39771
|
this.events.emit("layout:changed", { layout: next.id });
|
|
39055
39772
|
this.markStateDirty();
|
|
39056
39773
|
}
|
|
39774
|
+
/** The identity of the cell maximized over the whole grid, or null. */
|
|
39775
|
+
get maximizedCell() {
|
|
39776
|
+
return this.maximizedId;
|
|
39777
|
+
}
|
|
39778
|
+
/**
|
|
39779
|
+
* Maximize one cell over the whole grid, or restore the layout with `null`. Pure
|
|
39780
|
+
* presentation: the other cells stay alive underneath — charts, subscriptions and
|
|
39781
|
+
* state untouched — so restoring is instant. The maximized cell becomes the active
|
|
39782
|
+
* one. Transient view state (also reachable from each cell's bottom-center view
|
|
39783
|
+
* cluster): switching layouts or applying a state document restores the grid.
|
|
39784
|
+
*/
|
|
39785
|
+
maximizeCell(id) {
|
|
39786
|
+
if (this.destroyed) return;
|
|
39787
|
+
if (id != null && (!this.cellsById.has(id) || this.def.cells.length <= 1)) return;
|
|
39788
|
+
if (id === this.maximizedId) return;
|
|
39789
|
+
this.maximizedId = id;
|
|
39790
|
+
if (id) this.setActiveCell(id);
|
|
39791
|
+
this.applyGrid();
|
|
39792
|
+
this.refreshCellControls();
|
|
39793
|
+
this.syncMobileMaximize();
|
|
39794
|
+
this.events.emit("cell:maximized", { id });
|
|
39795
|
+
}
|
|
39796
|
+
/** The mobile bar's maximize stop: one press isolates the ACTIVE chart over the
|
|
39797
|
+
* grid; while something is already isolated — the chart, or a pane inside it
|
|
39798
|
+
* (mobile's double-tap) — the press restores that instead. Every branch re-syncs
|
|
39799
|
+
* the stop on its own (`maximizeCell` directly, `panes.maximize` via its
|
|
39800
|
+
* synchronous `pane:changed`). */
|
|
39801
|
+
toggleMobileMaximize() {
|
|
39802
|
+
const cell = this.activeId ? this.cellsById.get(this.activeId) : void 0;
|
|
39803
|
+
if (!cell) return;
|
|
39804
|
+
if (this.maximizedId) this.maximizeCell(null);
|
|
39805
|
+
else if (cell.chart.panes.list().some((p) => p.maximized)) cell.chart.panes.maximize(null);
|
|
39806
|
+
else this.maximizeCell(cell.id);
|
|
39807
|
+
}
|
|
39808
|
+
/** Keep the mobile bar's maximize stop truthful: lit (inverse chip, restore
|
|
39809
|
+
* glyph) while the active chart covers the grid OR one of its panes is
|
|
39810
|
+
* maximized — the state a double-tap toggles is otherwise invisible on mobile. */
|
|
39811
|
+
syncMobileMaximize() {
|
|
39812
|
+
if (!this.mobileBar) return;
|
|
39813
|
+
const cell = this.activeId ? this.cellsById.get(this.activeId) : void 0;
|
|
39814
|
+
const paneMax = cell ? cell.chart.panes.list().some((p) => p.maximized) : false;
|
|
39815
|
+
this.mobileBar.setMaximizeActive(this.maximizedId != null || paneMax);
|
|
39816
|
+
}
|
|
39817
|
+
/**
|
|
39818
|
+
* Trade the SLOTS of two live cells — the grid arrangement changes, the cells
|
|
39819
|
+
* themselves (charts, indicators, drawings, the active flag) stay untouched.
|
|
39820
|
+
* What each cell's drag handle commits; also callable directly by hosts.
|
|
39821
|
+
*/
|
|
39822
|
+
swapCells(a, b) {
|
|
39823
|
+
if (this.destroyed || a === b) return;
|
|
39824
|
+
const i = this.order.indexOf(a);
|
|
39825
|
+
const j = this.order.indexOf(b);
|
|
39826
|
+
if (i < 0 || j < 0 || !this.cellsById.has(a) || !this.cellsById.has(b)) return;
|
|
39827
|
+
[this.order[i], this.order[j]] = [this.order[j], this.order[i]];
|
|
39828
|
+
for (const [k] of this.def.cells.entries()) {
|
|
39829
|
+
const host = this.cellsById.get(this.order[k] ?? "")?.host;
|
|
39830
|
+
if (host) this.gridEl.appendChild(host);
|
|
39831
|
+
}
|
|
39832
|
+
this.applyGrid();
|
|
39833
|
+
this.markStateDirty();
|
|
39834
|
+
}
|
|
39057
39835
|
resize() {
|
|
39058
39836
|
this.splitters.layout();
|
|
39059
39837
|
}
|
|
@@ -39123,6 +39901,7 @@ var VelaWorkspace = class {
|
|
|
39123
39901
|
this.mobileBar?.renderActions();
|
|
39124
39902
|
this.mobileBar?.setSymbol(cell.symbol);
|
|
39125
39903
|
this.mobileBar?.setTimeframe(cell.timeframe);
|
|
39904
|
+
this.syncMobileMaximize();
|
|
39126
39905
|
this.drawingPill?.onChart(cell.chart);
|
|
39127
39906
|
const pushHistory = () => this.topbar.setHistoryState(cell.history.canUndo, cell.history.canRedo);
|
|
39128
39907
|
this.historyUnsub?.();
|
|
@@ -39212,8 +39991,78 @@ var VelaWorkspace = class {
|
|
|
39212
39991
|
const host = this.cellsById.get(this.order[i] ?? "")?.host;
|
|
39213
39992
|
if (host) host.style.gridArea = perCell[slot.id]?.gridArea ?? "";
|
|
39214
39993
|
}
|
|
39994
|
+
this.applyMaximizePresentation();
|
|
39995
|
+
this.mountAttributionMark();
|
|
39215
39996
|
this.splitters.layout();
|
|
39216
39997
|
}
|
|
39998
|
+
/** Overlay the maximize presentation on the freshly applied grid: EVERY cell spans
|
|
39999
|
+
* the full track grid — the maximized one on top, the siblings invisible beneath
|
|
40000
|
+
* it (their charts stay alive — restoring is instant). The siblings must span too:
|
|
40001
|
+
* left in their slots they would auto-flow into implicit zero-height rows, whose
|
|
40002
|
+
* gaps steal height from the maximized cell and collapse their renderers to 0.
|
|
40003
|
+
* The splitter strips and the active ring hide via the `data-maximized` rules. */
|
|
40004
|
+
applyMaximizePresentation() {
|
|
40005
|
+
const maxId = this.maximizedId;
|
|
40006
|
+
if (maxId) this.gridEl.dataset.maximized = "1";
|
|
40007
|
+
else delete this.gridEl.dataset.maximized;
|
|
40008
|
+
for (const [id, cell] of this.cellsById) {
|
|
40009
|
+
const style = cell.host.style;
|
|
40010
|
+
if (maxId) style.gridArea = "1 / 1 / -1 / -1";
|
|
40011
|
+
style.zIndex = maxId && id === maxId ? "5" : "";
|
|
40012
|
+
style.visibility = maxId && id !== maxId ? "hidden" : "";
|
|
40013
|
+
}
|
|
40014
|
+
}
|
|
40015
|
+
/** Rebuild every cell's view cluster (the maximize gate or state changed). */
|
|
40016
|
+
refreshCellControls() {
|
|
40017
|
+
for (const cell of this.cellsById.values()) cell.refreshControls();
|
|
40018
|
+
}
|
|
40019
|
+
/** Drop the transient maximize on a structural change (layout switch, state
|
|
40020
|
+
* document) — WITH the event, so hosts tracking `cell:maximized` never drift
|
|
40021
|
+
* from `maximizedCell`. The caller's own grid re-apply paints the restore. */
|
|
40022
|
+
clearMaximized() {
|
|
40023
|
+
if (this.maximizedId == null) return;
|
|
40024
|
+
this.maximizedId = null;
|
|
40025
|
+
this.events.emit("cell:maximized", { id: null });
|
|
40026
|
+
}
|
|
40027
|
+
/** The live cell under a viewport point, excluding `excludeId` and any host a
|
|
40028
|
+
* maximize has hidden — the drag handle's hit-test. */
|
|
40029
|
+
cellAtPoint(x, y, excludeId) {
|
|
40030
|
+
for (const [id, cell] of this.cellsById) {
|
|
40031
|
+
if (id === excludeId || cell.host.style.visibility === "hidden") continue;
|
|
40032
|
+
const r = cell.host.getBoundingClientRect();
|
|
40033
|
+
if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return id;
|
|
40034
|
+
}
|
|
40035
|
+
return null;
|
|
40036
|
+
}
|
|
40037
|
+
/** Mark one cell as the live drop target of a grip drag (null clears all) —
|
|
40038
|
+
* the `data-drop-target` stylesheet rule paints the dashed preview ring. */
|
|
40039
|
+
setDropTarget(id) {
|
|
40040
|
+
for (const [cid, cell] of this.cellsById) {
|
|
40041
|
+
if (cid === id) cell.host.dataset.dropTarget = "1";
|
|
40042
|
+
else delete cell.host.dataset.dropTarget;
|
|
40043
|
+
}
|
|
40044
|
+
}
|
|
40045
|
+
/** The cell whose bottom-left corner the grid's attribution mark floats in — the
|
|
40046
|
+
* maximized cell while one covers the grid, else the bottom-left slot's cell. */
|
|
40047
|
+
bottomLeftCell() {
|
|
40048
|
+
if (this.maximizedId) return this.cellsById.get(this.maximizedId);
|
|
40049
|
+
const grid = occupancyGrid(this.def);
|
|
40050
|
+
const slot = grid[grid.length - 1]?.[0];
|
|
40051
|
+
const idx = this.def.cells.findIndex((c) => (c.area ?? c.id) === slot);
|
|
40052
|
+
return this.cellsById.get(this.order[idx >= 0 ? idx : 0] ?? "");
|
|
40053
|
+
}
|
|
40054
|
+
/** Keep the shared attribution mark inside the BOTTOM-LEFT visible cell: its
|
|
40055
|
+
* offsets ride that cell's renderer-published `--vela-bottom-gutter` /
|
|
40056
|
+
* `--vela-toolbar-gutter`, so collapsed pane strips push the mark up without any
|
|
40057
|
+
* bookkeeping here. Re-run after anything that changes which host that is
|
|
40058
|
+
* (layout switch, maximize, cell rebuild); a destroyed host drops the mark from
|
|
40059
|
+
* the DOM, and this re-mount brings it back. */
|
|
40060
|
+
mountAttributionMark() {
|
|
40061
|
+
const mark = this.attributionMark;
|
|
40062
|
+
if (!mark) return;
|
|
40063
|
+
const host = this.bottomLeftCell()?.host ?? this.gridEl;
|
|
40064
|
+
if (mark.parentElement !== host) host.appendChild(mark);
|
|
40065
|
+
}
|
|
39217
40066
|
/** Create the cells the current layout wants but don't exist yet (pool-first).
|
|
39218
40067
|
* A slot's CELL IDENTITY is `order[i]` (declaration order — never the slot's own
|
|
39219
40068
|
* positional id); slots past the declared list mint an auto identity once. */
|
|
@@ -39245,6 +40094,12 @@ var VelaWorkspace = class {
|
|
|
39245
40094
|
setTimezone: (zone) => this.setTimezone(zone),
|
|
39246
40095
|
context: () => this.context(),
|
|
39247
40096
|
activate: (id2) => this.setActiveCell(id2),
|
|
40097
|
+
multiCell: () => !this.monoLayout && this.def.cells.length > 1,
|
|
40098
|
+
isMaximized: (id2) => this.maximizedId === id2,
|
|
40099
|
+
toggleMaximize: (id2) => this.maximizeCell(this.maximizedId === id2 ? null : id2),
|
|
40100
|
+
cellDragTarget: (id2, x, y) => this.cellAtPoint(x, y, id2),
|
|
40101
|
+
previewDropTarget: (target) => this.setDropTarget(target),
|
|
40102
|
+
dropCell: (id2, target) => this.swapCells(id2, target),
|
|
39248
40103
|
onMarketChanged: (id2) => this.onCellMarketChanged(id2),
|
|
39249
40104
|
onPriceStyleChanged: (id2) => this.onCellPriceStyleChanged(id2),
|
|
39250
40105
|
onIndicatorsChanged: (id2) => this.onCellIndicatorsChanged(id2),
|
|
@@ -39258,6 +40113,7 @@ var VelaWorkspace = class {
|
|
|
39258
40113
|
if (id === this.activeId) cell.host.dataset.active = "1";
|
|
39259
40114
|
this.wireCell(cell);
|
|
39260
40115
|
cell.chart.renderer.setLayoutMode(this.layoutCtl.current);
|
|
40116
|
+
cell.setControlsSuspended(this.layoutCtl.current === "mobile");
|
|
39261
40117
|
if (this.favs.length > 0) cell.chart.drawings.setFavorites(this.favs);
|
|
39262
40118
|
cell.setManifest(this.manifest, pooled?.indicators == null);
|
|
39263
40119
|
cell.restorePersistedExt();
|
|
@@ -39267,6 +40123,7 @@ var VelaWorkspace = class {
|
|
|
39267
40123
|
const host = this.cellsById.get(this.order[i] ?? "")?.host;
|
|
39268
40124
|
if (host) this.gridEl.appendChild(host);
|
|
39269
40125
|
}
|
|
40126
|
+
this.mountAttributionMark();
|
|
39270
40127
|
}
|
|
39271
40128
|
/** Per-cell chart subscriptions (trigger ② — the chart instance is stable for the
|
|
39272
40129
|
* cell's whole life, so these live and die with the cell). */
|
|
@@ -39326,6 +40183,9 @@ var VelaWorkspace = class {
|
|
|
39326
40183
|
chart.on("viewport:changed", (range) => this.propagateViewport(cell.id, range));
|
|
39327
40184
|
chart.renderer.onConfigChanged(() => this.propagateStylePrefs(cell.id));
|
|
39328
40185
|
chart.on("theme:changed", (t) => this.setTheme(t));
|
|
40186
|
+
chart.on("pane:changed", () => {
|
|
40187
|
+
if (cell.id === this.activeId) this.syncMobileMaximize();
|
|
40188
|
+
});
|
|
39329
40189
|
chart.renderer.onAxisLongPress((e) => {
|
|
39330
40190
|
if (this.layoutCtl.current !== "mobile") return;
|
|
39331
40191
|
if (e.axis === "time") this.openTimezoneDrawer();
|
|
@@ -39646,6 +40506,7 @@ var VelaWorkspace = class {
|
|
|
39646
40506
|
for (const cell of this.cellsById.values()) {
|
|
39647
40507
|
cell.chart.renderer.closeDialogs();
|
|
39648
40508
|
cell.chart.renderer.setLayoutMode(mode);
|
|
40509
|
+
cell.setControlsSuspended(mode === "mobile");
|
|
39649
40510
|
}
|
|
39650
40511
|
this.syncCellPresentation();
|
|
39651
40512
|
}
|