@luxalgo/vela 0.6.8 → 0.6.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{DataProvider-BSLlBpB9.d.ts → DataProvider-CWmp31dA.d.ts} +49 -1
- package/dist/{DataProvider-BsQM2WNH.d.cts → DataProvider-l_eLMly_.d.cts} +49 -1
- package/dist/{chunk-L3I2CCYO.js → chunk-4KZVZ7QQ.js} +49 -2
- package/dist/{chunk-SQETIBFU.js → chunk-FFOP37FQ.js} +996 -377
- package/dist/{chunk-2R7BANEM.js → chunk-G77Y7LK2.js} +2 -2
- package/dist/{chunk-PN5KWFZ4.js → chunk-KG4YT3TI.js} +5 -3
- package/dist/{chunk-OICPLNU7.js → chunk-MHY7MVXH.js} +192 -2
- package/dist/{chunk-N7LGMKCE.js → chunk-NMTQXNT4.js} +637 -164
- package/dist/{contributions-DLLdV9jD.d.ts → contributions-BCz6Dr6a.d.ts} +99 -7
- package/dist/{contributions-Vw-Hm58R.d.cts → contributions-D9vTzm5p.d.cts} +99 -7
- package/dist/index.cjs +1210 -383
- package/dist/index.d.cts +37 -11
- package/dist/index.d.ts +37 -11
- package/dist/index.js +4 -4
- package/dist/{options-BaVTMXaO.d.ts → options-DSqHsQyN.d.cts} +84 -7
- package/dist/{options-BaVTMXaO.d.cts → options-DSqHsQyN.d.ts} +84 -7
- package/dist/{plugin-BnJgjLAy.d.cts → plugin-Bt8hLR8Z.d.cts} +3 -3
- package/dist/{plugin-CC7rBrOY.d.ts → plugin-CH7pfMrE.d.ts} +3 -3
- package/dist/plugin.cjs +21 -3
- package/dist/plugin.d.cts +4 -4
- package/dist/plugin.d.ts +4 -4
- package/dist/plugin.js +2 -2
- package/dist/providers/binance.d.cts +2 -2
- package/dist/providers/binance.d.ts +2 -2
- package/dist/providers/coinbase.d.cts +2 -2
- package/dist/providers/coinbase.d.ts +2 -2
- package/dist/providers/hyperliquid.d.cts +2 -2
- package/dist/providers/hyperliquid.d.ts +2 -2
- package/dist/{statusline-CGkB2EOo.d.ts → statusline-5K7y4Ya2.d.ts} +5 -3
- package/dist/{statusline-DqzBqy42.d.cts → statusline-TYLQmoeh.d.cts} +5 -3
- package/dist/ui.cjs +208 -5
- package/dist/ui.d.cts +91 -2
- package/dist/ui.d.ts +91 -2
- package/dist/ui.js +3 -3
- package/dist/vela.global.js +1210 -383
- package/dist/vela.global.min.js +99 -48
- package/dist/widget.cjs +1771 -449
- package/dist/widget.d.cts +17 -6
- package/dist/widget.d.ts +17 -6
- package/dist/widget.js +7 -7
- package/dist/workspace.cjs +1771 -449
- package/dist/workspace.d.cts +100 -6
- package/dist/workspace.d.ts +100 -6
- package/dist/workspace.js +6 -6
- package/package.json +1 -1
package/dist/workspace.cjs
CHANGED
|
@@ -125,6 +125,23 @@ var TypedEventBus = class {
|
|
|
125
125
|
}
|
|
126
126
|
};
|
|
127
127
|
|
|
128
|
+
// src/data/symbol-groups.ts
|
|
129
|
+
function isGroupRow(d) {
|
|
130
|
+
return d.group != null && d.ticker === d.group;
|
|
131
|
+
}
|
|
132
|
+
function groupKeyOf(d) {
|
|
133
|
+
return `${(d.prefix ?? d.provider ?? "").toLowerCase()}:${(d.group ?? "").toUpperCase()}`;
|
|
134
|
+
}
|
|
135
|
+
function groupMembers(pool, groupRow) {
|
|
136
|
+
const key = groupKeyOf(groupRow);
|
|
137
|
+
return pool.filter((s) => s.group != null && !isGroupRow(s) && groupKeyOf(s) === key);
|
|
138
|
+
}
|
|
139
|
+
function defaultMemberOf(pool, groupRow) {
|
|
140
|
+
const members = groupMembers(pool, groupRow);
|
|
141
|
+
const defaults2 = members.filter((m) => m.default);
|
|
142
|
+
return (defaults2.length === 1 ? defaults2[0] : members[0]) ?? null;
|
|
143
|
+
}
|
|
144
|
+
|
|
128
145
|
// src/data/ProviderRegistry.ts
|
|
129
146
|
var normName = (n) => n.trim().toLowerCase();
|
|
130
147
|
var normTicker = (t) => t.trim().toUpperCase();
|
|
@@ -214,17 +231,24 @@ var ProviderRegistry = class {
|
|
|
214
231
|
resolve(raw, opts = {}) {
|
|
215
232
|
const { provider, ticker } = this.parse(raw);
|
|
216
233
|
if (provider) {
|
|
217
|
-
if (this.entries.has(provider))
|
|
234
|
+
if (this.entries.has(provider)) {
|
|
235
|
+
const d = this.entries.get(provider).byTicker?.get(normTicker(ticker));
|
|
236
|
+
return { provider, ticker: d != null && isGroupRow(d) ? this.loadableTicker(provider, d) : ticker };
|
|
237
|
+
}
|
|
218
238
|
const key = `${provider}:${normTicker(ticker)}`;
|
|
219
239
|
for (const name of this.candidateOrder(opts.default)) {
|
|
220
240
|
const d = this.entries.get(name).prefixIndex?.get(key);
|
|
221
|
-
if (d) return { provider: name, ticker:
|
|
241
|
+
if (d) return { provider: name, ticker: this.loadableTicker(name, d) };
|
|
222
242
|
}
|
|
223
243
|
return null;
|
|
224
244
|
}
|
|
225
245
|
const norm = normTicker(ticker);
|
|
226
246
|
for (const name of this.candidateOrder(opts.default)) {
|
|
227
|
-
|
|
247
|
+
const e = this.entries.get(name);
|
|
248
|
+
if (e.index?.has(norm)) {
|
|
249
|
+
const d = e.byTicker?.get(norm);
|
|
250
|
+
return { provider: name, ticker: d != null && isGroupRow(d) ? this.loadableTicker(name, d) : ticker };
|
|
251
|
+
}
|
|
228
252
|
}
|
|
229
253
|
if (opts.lenient && this.entries.size === 1) {
|
|
230
254
|
const only = this.names()[0];
|
|
@@ -233,6 +257,13 @@ var ProviderRegistry = class {
|
|
|
233
257
|
}
|
|
234
258
|
return null;
|
|
235
259
|
}
|
|
260
|
+
/** What a resolved descriptor LOADS: itself — unless it is a GROUP row (listed, never
|
|
261
|
+
* served), which loads its default member (single `default`, else first listed). A
|
|
262
|
+
* memberless group keeps its own ticker and fails downstream like any unknown symbol. */
|
|
263
|
+
loadableTicker(name, d) {
|
|
264
|
+
if (!isGroupRow(d)) return d.ticker;
|
|
265
|
+
return defaultMemberOf(this.entries.get(name)?.descriptors ?? [], d)?.ticker ?? d.ticker;
|
|
266
|
+
}
|
|
236
267
|
/**
|
|
237
268
|
* Resolve now if possible, else resolve later — re-attempting after each provider
|
|
238
269
|
* settles. The promise stays pending until some registered provider can serve the
|
|
@@ -486,6 +517,28 @@ var CachingDataFeed = class {
|
|
|
486
517
|
this.store.merge(key, dropForming(bars));
|
|
487
518
|
return bars;
|
|
488
519
|
}
|
|
520
|
+
/**
|
|
521
|
+
* Progressive twin of {@link load}: a COLD load streams through the inner feed's
|
|
522
|
+
* progressive path — batches forwarded verbatim, the FINAL answer cached exactly as
|
|
523
|
+
* `load` caches — while a cache-covered load answers once through `load` itself (a
|
|
524
|
+
* warm chart has nothing to stream). Falls back to `load` wholesale when the inner
|
|
525
|
+
* feed lacks the capability, so callers may prefer this method unconditionally.
|
|
526
|
+
*/
|
|
527
|
+
async loadProgressive(cfg, onBatch, opts) {
|
|
528
|
+
if (cfg.data && cfg.data.length > 0) return this.inner.load(cfg);
|
|
529
|
+
if (!this.inner.loadProgressive) return null;
|
|
530
|
+
const symbol = cfg.symbol ?? "TEST";
|
|
531
|
+
const key = cacheKey(symbol, cfg.timeframe ?? "60", cfg.session);
|
|
532
|
+
const cached = this.store.get(key);
|
|
533
|
+
const n = cfg.bars ?? 500;
|
|
534
|
+
const lastCached = cached?.[cached.length - 1];
|
|
535
|
+
if (cached && lastCached && cached.length >= n - 1 && this.inner.loadRange) return this.load(cfg);
|
|
536
|
+
this.store.retainSymbol(symbol);
|
|
537
|
+
const bars = await this.inner.loadProgressive(cfg, onBatch, opts);
|
|
538
|
+
if (bars == null) return null;
|
|
539
|
+
this.store.merge(key, dropForming(bars));
|
|
540
|
+
return bars;
|
|
541
|
+
}
|
|
489
542
|
/**
|
|
490
543
|
* Cache-backed ranged fetch — the gateway engines use for secondary series
|
|
491
544
|
* (`request.security` HTF/LTF/cross-symbol) and the orchestrator's backward
|
|
@@ -731,6 +784,17 @@ var MultiProviderFeed = class {
|
|
|
731
784
|
this.prefetchSymbolInfo(resolved);
|
|
732
785
|
return this.cache.load(canonical(cfg, resolved));
|
|
733
786
|
}
|
|
787
|
+
/** Progressive twin of {@link load} — same resolution, the cache streams the batches. */
|
|
788
|
+
async loadProgressive(cfg, onBatch, opts) {
|
|
789
|
+
if (cfg.data && cfg.data.length > 0) {
|
|
790
|
+
this.liveBars = cfg.data.map((b) => ({ ...b }));
|
|
791
|
+
return cfg.data;
|
|
792
|
+
}
|
|
793
|
+
const resolved = await this.registry.whenResolvable(rawSymbol(cfg), { default: this.primaryProvider });
|
|
794
|
+
this.primaryProvider = resolved.provider;
|
|
795
|
+
this.prefetchSymbolInfo(resolved);
|
|
796
|
+
return this.cache.loadProgressive(canonical(cfg, resolved), onBatch, opts);
|
|
797
|
+
}
|
|
734
798
|
/**
|
|
735
799
|
* Synchronous per-symbol metadata for engines (Pine `syminfo.*`), served from the cache
|
|
736
800
|
* warmed by load(). Undefined until the prefetch lands (the engine then synthesizes a
|
|
@@ -818,6 +882,18 @@ var RegistryFetchFeed = class {
|
|
|
818
882
|
if (!provider) return Promise.resolve([]);
|
|
819
883
|
return safeBars(provider, ticker, cfg.timeframe ?? "60", { limit: cfg.bars ?? 500, session: cfg.session });
|
|
820
884
|
}
|
|
885
|
+
async loadProgressive(cfg, onBatch, opts) {
|
|
886
|
+
const { provider: name, ticker } = parseSymbol(cfg.symbol ?? "");
|
|
887
|
+
const provider = this.registry.get(name ?? "");
|
|
888
|
+
if (!provider) return [];
|
|
889
|
+
if (!provider.getBarsProgressive) return null;
|
|
890
|
+
try {
|
|
891
|
+
return await provider.getBarsProgressive(ticker, cfg.timeframe ?? "60", { limit: cfg.bars ?? 500, session: cfg.session }, onBatch, opts);
|
|
892
|
+
} catch (e) {
|
|
893
|
+
console.warn(`[vela] progressive fetch failed for ${ticker} ${cfg.timeframe ?? "60"} \u2014 ${e instanceof Error ? e.message : String(e)}`);
|
|
894
|
+
return [];
|
|
895
|
+
}
|
|
896
|
+
}
|
|
821
897
|
loadRange(cfg, range) {
|
|
822
898
|
const { provider: name, ticker } = parseSymbol(cfg.symbol ?? "");
|
|
823
899
|
const provider = this.registry.get(name ?? "");
|
|
@@ -1097,11 +1173,11 @@ registerIcon(
|
|
|
1097
1173
|
);
|
|
1098
1174
|
registerIcon(
|
|
1099
1175
|
"market-pre",
|
|
1100
|
-
svg24('<path d="M12 2v8"/><path d="m4.93 10.93 1.41 1.41"/><path d="M2 18h2"/><path d="M20 18h2"/><path d="m19.07 10.93-1.41 1.41"/><path d="M22 22H2"/><path d="
|
|
1176
|
+
svg24('<path d="M12 2v8"/><path d="m4.93 10.93 1.41 1.41"/><path d="M2 18h2"/><path d="M20 18h2"/><path d="m19.07 10.93-1.41 1.41"/><path d="M22 22H2"/><path d="m8 6 4-4 4 4"/><path d="M16 18a4 4 0 0 0-8 0"/>')
|
|
1101
1177
|
);
|
|
1102
1178
|
registerIcon(
|
|
1103
1179
|
"market-post",
|
|
1104
|
-
svg24('<path d="M12 10V2"/><path d="m4.93 10.93 1.41 1.41"/><path d="M2 18h2"/><path d="M20 18h2"/><path d="m19.07 10.93-1.41 1.41"/><path d="M22 22H2"/><path d="m16
|
|
1180
|
+
svg24('<path d="M12 10V2"/><path d="m4.93 10.93 1.41 1.41"/><path d="M2 18h2"/><path d="M20 18h2"/><path d="m19.07 10.93-1.41 1.41"/><path d="M22 22H2"/><path d="m16 14-4 4-4-4"/><path d="M16 6a4 4 0 0 0-8 0"/>')
|
|
1105
1181
|
);
|
|
1106
1182
|
registerIcon("market-closed", svg24('<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/>'));
|
|
1107
1183
|
registerIcon(
|
|
@@ -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"'));
|
|
@@ -4195,6 +4273,196 @@ function buildFieldControl(desc) {
|
|
|
4195
4273
|
return { el };
|
|
4196
4274
|
}
|
|
4197
4275
|
|
|
4276
|
+
// src/ui/components/callout-bubble/controller.ts
|
|
4277
|
+
function closesPanel(item) {
|
|
4278
|
+
return item.close !== false;
|
|
4279
|
+
}
|
|
4280
|
+
function calloutPanelRows(items) {
|
|
4281
|
+
const rows = [];
|
|
4282
|
+
for (const item of items) {
|
|
4283
|
+
if (item.type === "text") {
|
|
4284
|
+
rows.push({ type: "text", text: item.text });
|
|
4285
|
+
continue;
|
|
4286
|
+
}
|
|
4287
|
+
const last = rows[rows.length - 1];
|
|
4288
|
+
if (last && last.type === "buttons") last.buttons.push(item);
|
|
4289
|
+
else rows.push({ type: "buttons", buttons: [item] });
|
|
4290
|
+
}
|
|
4291
|
+
return rows;
|
|
4292
|
+
}
|
|
4293
|
+
|
|
4294
|
+
// src/ui/components/callout-bubble/styles.ts
|
|
4295
|
+
var CALLOUT_STYLE_ID = "vela-ui-callout-bubble";
|
|
4296
|
+
var CALLOUT_CSS = `
|
|
4297
|
+
.vela-callout {
|
|
4298
|
+
display: inline-grid;
|
|
4299
|
+
place-items: center;
|
|
4300
|
+
border-radius: 50%;
|
|
4301
|
+
flex: none;
|
|
4302
|
+
line-height: 0;
|
|
4303
|
+
box-sizing: border-box;
|
|
4304
|
+
cursor: default;
|
|
4305
|
+
user-select: none;
|
|
4306
|
+
-webkit-user-select: none;
|
|
4307
|
+
}
|
|
4308
|
+
.vela-callout[role='button'] { cursor: pointer; }
|
|
4309
|
+
.vela-callout svg { display: block; }
|
|
4310
|
+
/* The deployed panel \u2014 carries the kit's elevated-card look itself (the popover
|
|
4311
|
+
shell is bare positioning chrome). */
|
|
4312
|
+
.vela-callout-panel {
|
|
4313
|
+
display: flex;
|
|
4314
|
+
flex-direction: column;
|
|
4315
|
+
gap: 8px;
|
|
4316
|
+
padding: 10px 12px;
|
|
4317
|
+
max-width: 280px;
|
|
4318
|
+
box-sizing: border-box;
|
|
4319
|
+
background: var(--vela-surface-elev);
|
|
4320
|
+
border: 1px solid var(--vela-border-strong);
|
|
4321
|
+
border-radius: 6px;
|
|
4322
|
+
box-shadow: var(--vela-shadow);
|
|
4323
|
+
color: var(--vela-fg);
|
|
4324
|
+
font: var(--vela-font-size-md) var(--vela-font);
|
|
4325
|
+
}
|
|
4326
|
+
.vela-callout-title { font-weight: 600; color: var(--vela-fg-bright); }
|
|
4327
|
+
.vela-callout-text { color: var(--vela-fg-muted); line-height: 1.45; white-space: pre-line; }
|
|
4328
|
+
.vela-callout-actions { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
4329
|
+
.vela-callout-btn {
|
|
4330
|
+
cursor: pointer;
|
|
4331
|
+
height: 26px;
|
|
4332
|
+
padding: 0 10px;
|
|
4333
|
+
border-radius: 5px;
|
|
4334
|
+
border: 1px solid var(--vela-border);
|
|
4335
|
+
background: transparent;
|
|
4336
|
+
color: var(--vela-fg);
|
|
4337
|
+
font-size: var(--vela-font-size-md);
|
|
4338
|
+
font-family: inherit;
|
|
4339
|
+
transition: background var(--vela-dur-fast) ease, color var(--vela-dur-fast) ease, opacity var(--vela-dur-fast) ease, border-color var(--vela-dur-fast) ease;
|
|
4340
|
+
}
|
|
4341
|
+
.vela-callout-btn:hover { background: var(--vela-hover); color: var(--vela-fg-bright); border-color: var(--vela-fg-muted); }
|
|
4342
|
+
.vela-callout-btn-primary { border-color: var(--vela-selected-bg); background: var(--vela-selected-bg); color: var(--vela-selected-fg); }
|
|
4343
|
+
.vela-callout-btn-primary:hover { background: var(--vela-selected-bg); color: var(--vela-selected-fg); opacity: 0.85; border-color: var(--vela-selected-bg); }
|
|
4344
|
+
`;
|
|
4345
|
+
|
|
4346
|
+
// src/ui/components/callout-bubble/view.ts
|
|
4347
|
+
var CalloutBubble = class {
|
|
4348
|
+
constructor(opts) {
|
|
4349
|
+
this.pop = null;
|
|
4350
|
+
this.spec = { icon: opts.icon, background: opts.background, label: opts.label };
|
|
4351
|
+
if (opts.color !== void 0) this.spec.color = opts.color;
|
|
4352
|
+
if (opts.panel !== void 0) this.spec.panel = opts.panel;
|
|
4353
|
+
this.size = opts.size ?? 16;
|
|
4354
|
+
this.host = opts.host;
|
|
4355
|
+
this.theme = opts.theme;
|
|
4356
|
+
this.boundary = opts.boundary;
|
|
4357
|
+
const doc = (opts.host ?? document.body).ownerDocument;
|
|
4358
|
+
injectStyles(CALLOUT_STYLE_ID, CALLOUT_CSS, doc);
|
|
4359
|
+
this.el = doc.createElement("span");
|
|
4360
|
+
this.el.className = "vela-callout";
|
|
4361
|
+
this.el.style.width = `${this.size}px`;
|
|
4362
|
+
this.el.style.height = `${this.size}px`;
|
|
4363
|
+
this.el.addEventListener("click", (e) => {
|
|
4364
|
+
if (!this.spec.panel) return;
|
|
4365
|
+
e.stopPropagation();
|
|
4366
|
+
this.toggle();
|
|
4367
|
+
});
|
|
4368
|
+
this.el.addEventListener("dblclick", (e) => {
|
|
4369
|
+
if (this.spec.panel) e.stopPropagation();
|
|
4370
|
+
});
|
|
4371
|
+
this.el.addEventListener("keydown", (e) => {
|
|
4372
|
+
if (!this.spec.panel || e.key !== "Enter" && e.key !== " ") return;
|
|
4373
|
+
e.preventDefault();
|
|
4374
|
+
this.toggle();
|
|
4375
|
+
});
|
|
4376
|
+
this.dress();
|
|
4377
|
+
}
|
|
4378
|
+
/** Re-dress the bubble (a status change: new icon, tint, label, panel). */
|
|
4379
|
+
set(spec) {
|
|
4380
|
+
this.spec = { ...this.spec, ...spec };
|
|
4381
|
+
if ("panel" in spec) this.pop?.hide();
|
|
4382
|
+
this.dress();
|
|
4383
|
+
}
|
|
4384
|
+
/** Whether the deployed panel is currently open. */
|
|
4385
|
+
get open() {
|
|
4386
|
+
return this.pop?.open ?? false;
|
|
4387
|
+
}
|
|
4388
|
+
/** Close the deployed panel, if any. The bubble itself stays. */
|
|
4389
|
+
hidePanel() {
|
|
4390
|
+
this.pop?.hide();
|
|
4391
|
+
}
|
|
4392
|
+
destroy() {
|
|
4393
|
+
this.pop?.destroy();
|
|
4394
|
+
this.pop = null;
|
|
4395
|
+
this.el.remove();
|
|
4396
|
+
}
|
|
4397
|
+
dress() {
|
|
4398
|
+
const clickable = this.spec.panel !== void 0;
|
|
4399
|
+
this.el.style.background = this.spec.background;
|
|
4400
|
+
this.el.style.color = this.spec.color ?? "";
|
|
4401
|
+
this.el.innerHTML = iconAt(this.spec.icon, this.size - 4);
|
|
4402
|
+
this.el.setAttribute("aria-label", this.spec.label);
|
|
4403
|
+
if (clickable) {
|
|
4404
|
+
this.el.setAttribute("role", "button");
|
|
4405
|
+
this.el.tabIndex = 0;
|
|
4406
|
+
} else {
|
|
4407
|
+
this.el.removeAttribute("role");
|
|
4408
|
+
this.el.removeAttribute("tabindex");
|
|
4409
|
+
}
|
|
4410
|
+
}
|
|
4411
|
+
toggle() {
|
|
4412
|
+
if (this.pop?.open) {
|
|
4413
|
+
this.pop.hide();
|
|
4414
|
+
return;
|
|
4415
|
+
}
|
|
4416
|
+
const panel = this.spec.panel;
|
|
4417
|
+
if (!panel) return;
|
|
4418
|
+
this.pop?.destroy();
|
|
4419
|
+
this.pop = new Popover({
|
|
4420
|
+
trigger: this.el,
|
|
4421
|
+
gap: 6,
|
|
4422
|
+
content: (body) => this.buildPanel(body, panel),
|
|
4423
|
+
...this.host ? { host: this.host } : {},
|
|
4424
|
+
...this.theme ? { theme: this.theme() } : {},
|
|
4425
|
+
...this.boundary ? { boundary: this.boundary } : {}
|
|
4426
|
+
});
|
|
4427
|
+
this.pop.show();
|
|
4428
|
+
}
|
|
4429
|
+
buildPanel(body, panel) {
|
|
4430
|
+
const doc = body.ownerDocument;
|
|
4431
|
+
const root = doc.createElement("div");
|
|
4432
|
+
root.className = "vela-callout-panel";
|
|
4433
|
+
if (panel.title) {
|
|
4434
|
+
const title = doc.createElement("div");
|
|
4435
|
+
title.className = "vela-callout-title";
|
|
4436
|
+
title.textContent = panel.title;
|
|
4437
|
+
root.appendChild(title);
|
|
4438
|
+
}
|
|
4439
|
+
for (const row of calloutPanelRows(panel.items)) {
|
|
4440
|
+
if (row.type === "text") {
|
|
4441
|
+
const text = doc.createElement("div");
|
|
4442
|
+
text.className = "vela-callout-text";
|
|
4443
|
+
text.textContent = row.text;
|
|
4444
|
+
root.appendChild(text);
|
|
4445
|
+
continue;
|
|
4446
|
+
}
|
|
4447
|
+
const actions = doc.createElement("div");
|
|
4448
|
+
actions.className = "vela-callout-actions";
|
|
4449
|
+
for (const item of row.buttons) {
|
|
4450
|
+
const btn2 = doc.createElement("button");
|
|
4451
|
+
btn2.type = "button";
|
|
4452
|
+
btn2.className = "vela-callout-btn" + (item.primary ? " vela-callout-btn-primary" : "");
|
|
4453
|
+
btn2.textContent = item.label;
|
|
4454
|
+
btn2.addEventListener("click", () => {
|
|
4455
|
+
item.run();
|
|
4456
|
+
if (closesPanel(item)) this.pop?.hide();
|
|
4457
|
+
});
|
|
4458
|
+
actions.appendChild(btn2);
|
|
4459
|
+
}
|
|
4460
|
+
root.appendChild(actions);
|
|
4461
|
+
}
|
|
4462
|
+
body.appendChild(root);
|
|
4463
|
+
}
|
|
4464
|
+
};
|
|
4465
|
+
|
|
4198
4466
|
// src/widget/layout-picker.ts
|
|
4199
4467
|
var STYLE_ID = "vela-widget-layout-picker-v14";
|
|
4200
4468
|
var CSS2 = `
|
|
@@ -4642,6 +4910,44 @@ function legendActionsProviderFor(chart, context) {
|
|
|
4642
4910
|
return legendActions().filter((d) => !d.when || d.when(info)).map((d) => ({ id: d.id, icon: d.icon, tooltip: d.tooltip, run: () => d.run(context(), info) }));
|
|
4643
4911
|
};
|
|
4644
4912
|
}
|
|
4913
|
+
var calloutRegistry = /* @__PURE__ */ new Map();
|
|
4914
|
+
function legendCallouts() {
|
|
4915
|
+
return [...calloutRegistry.values()].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
4916
|
+
}
|
|
4917
|
+
function legendCalloutsProviderFor(chart, context) {
|
|
4918
|
+
return (indicatorId) => {
|
|
4919
|
+
const handle = chart.indicators().find((h) => h.id === indicatorId);
|
|
4920
|
+
if (!handle) return [];
|
|
4921
|
+
const info = { id: handle.id, title: handle.title, ...handle.source !== void 0 ? { source: handle.source } : {} };
|
|
4922
|
+
const views = [];
|
|
4923
|
+
for (const d of legendCallouts()) {
|
|
4924
|
+
const spec = d.callout(info);
|
|
4925
|
+
if (!spec) continue;
|
|
4926
|
+
views.push({
|
|
4927
|
+
id: d.id,
|
|
4928
|
+
icon: spec.icon,
|
|
4929
|
+
background: spec.background,
|
|
4930
|
+
...spec.color !== void 0 ? { color: spec.color } : {},
|
|
4931
|
+
tooltip: spec.tooltip,
|
|
4932
|
+
...spec.content !== void 0 ? {
|
|
4933
|
+
content: {
|
|
4934
|
+
...spec.content.title !== void 0 ? { title: spec.content.title } : {},
|
|
4935
|
+
items: spec.content.items.map(
|
|
4936
|
+
(item) => item.type === "text" ? item : {
|
|
4937
|
+
type: "button",
|
|
4938
|
+
label: item.label,
|
|
4939
|
+
...item.primary !== void 0 ? { primary: item.primary } : {},
|
|
4940
|
+
...item.close !== void 0 ? { close: item.close } : {},
|
|
4941
|
+
run: () => item.run(context(), info)
|
|
4942
|
+
}
|
|
4943
|
+
)
|
|
4944
|
+
}
|
|
4945
|
+
} : {}
|
|
4946
|
+
});
|
|
4947
|
+
}
|
|
4948
|
+
return views;
|
|
4949
|
+
};
|
|
4950
|
+
}
|
|
4645
4951
|
var stateHandlers = /* @__PURE__ */ new Map();
|
|
4646
4952
|
function statePersistenceHandlers(scope) {
|
|
4647
4953
|
return [...stateHandlers.values()].filter((h) => h.scope === scope);
|
|
@@ -13449,6 +13755,17 @@ var PanelDock = class {
|
|
|
13449
13755
|
for (const entry of this.entries) this.deps.chrome.setPanelActive(entry.id, entry.panel.open);
|
|
13450
13756
|
}
|
|
13451
13757
|
};
|
|
13758
|
+
function foldGroups(list) {
|
|
13759
|
+
const groupsAbove = /* @__PURE__ */ new Set();
|
|
13760
|
+
const out = [];
|
|
13761
|
+
for (const s of list) {
|
|
13762
|
+
if (isGroupRow(s)) {
|
|
13763
|
+
groupsAbove.add(groupKeyOf(s));
|
|
13764
|
+
out.push(s);
|
|
13765
|
+
} else if (s.group == null || !groupsAbove.has(groupKeyOf(s))) out.push(s);
|
|
13766
|
+
}
|
|
13767
|
+
return out;
|
|
13768
|
+
}
|
|
13452
13769
|
var TOP_TICKERS = ["BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT", "XRPUSDT", "DOGEUSDT", "ADAUSDT", "LINKUSDT"];
|
|
13453
13770
|
function parseQuery(raw, venues) {
|
|
13454
13771
|
const m = raw.match(/^\s*([^\s:]+)\s*[:\s]\s*(.*)$/);
|
|
@@ -13580,6 +13897,21 @@ var CSS8 = `
|
|
|
13580
13897
|
.vela-sp-badge[data-p='binance'] { color: #f0b90b; } /* palette-exempt: venue brand mark */
|
|
13581
13898
|
.vela-sp-badge[data-p='hyperliquid'] { color: #50d2c1; } /* palette-exempt: venue brand mark */
|
|
13582
13899
|
.vela-sp-empty { padding: var(--vela-space-3); color: var(--vela-fg-muted); text-align: center; }
|
|
13900
|
+
/* Grouped listings (futures roots): the chevron unfolds members inline, indented. */
|
|
13901
|
+
.vela-sp-expander {
|
|
13902
|
+
all: unset;
|
|
13903
|
+
flex: none;
|
|
13904
|
+
display: inline-flex;
|
|
13905
|
+
align-items: center;
|
|
13906
|
+
justify-content: center;
|
|
13907
|
+
width: 22px;
|
|
13908
|
+
height: 22px;
|
|
13909
|
+
border-radius: 5px;
|
|
13910
|
+
cursor: pointer;
|
|
13911
|
+
color: var(--vela-fg-muted);
|
|
13912
|
+
}
|
|
13913
|
+
.vela-sp-expander:hover { background: var(--vela-surface-elev); color: var(--vela-fg); }
|
|
13914
|
+
.vela-sp-row[data-member] { padding-left: 34px; }
|
|
13583
13915
|
`;
|
|
13584
13916
|
var PAGE = 100;
|
|
13585
13917
|
var SymbolPicker = class {
|
|
@@ -13591,6 +13923,11 @@ var SymbolPicker = class {
|
|
|
13591
13923
|
this.seed = "";
|
|
13592
13924
|
this.activeTab = "All";
|
|
13593
13925
|
this.visible = PAGE;
|
|
13926
|
+
/** The last filter pass returned fewer raw rows than asked — the pool is drained
|
|
13927
|
+
* (checked BEFORE folding: folding shortens pages without meaning exhaustion). */
|
|
13928
|
+
this.exhausted = false;
|
|
13929
|
+
/** Group rows currently expanded (venue-scoped keys) — members shown inline. */
|
|
13930
|
+
this.expanded = /* @__PURE__ */ new Set();
|
|
13594
13931
|
/** The ranked pool cache — `key` fingerprints the raw pool the ranking ran on. */
|
|
13595
13932
|
this.ranked = null;
|
|
13596
13933
|
this.ranking = false;
|
|
@@ -13605,7 +13942,7 @@ var SymbolPicker = class {
|
|
|
13605
13942
|
searchRow.append(iconEl("search", doc), this.input);
|
|
13606
13943
|
this.tabs = doc.createElement("div");
|
|
13607
13944
|
this.tabs.className = "vela-sp-tabs";
|
|
13608
|
-
for (const t of ["All", "Stocks", "ETFs", "Crypto", "Forex", "Commodities"]) {
|
|
13945
|
+
for (const t of ["All", "Stocks", "ETFs", "Crypto", "Futures", "Forex", "Commodities"]) {
|
|
13609
13946
|
const b = doc.createElement("button");
|
|
13610
13947
|
b.className = "vela-sp-tab";
|
|
13611
13948
|
b.textContent = t;
|
|
@@ -13621,7 +13958,7 @@ var SymbolPicker = class {
|
|
|
13621
13958
|
this.list = doc.createElement("div");
|
|
13622
13959
|
this.list.className = "vela-sp-list";
|
|
13623
13960
|
this.list.addEventListener("scroll", () => {
|
|
13624
|
-
if (this.
|
|
13961
|
+
if (this.exhausted) return;
|
|
13625
13962
|
if (this.list.scrollTop + this.list.clientHeight < this.list.scrollHeight - 200) return;
|
|
13626
13963
|
this.visible += PAGE;
|
|
13627
13964
|
this.grow();
|
|
@@ -13650,14 +13987,19 @@ var SymbolPicker = class {
|
|
|
13650
13987
|
else if (e.key === "ArrowUp") this.moveHighlight(-1);
|
|
13651
13988
|
else if (e.key === "Enter") {
|
|
13652
13989
|
const pick = this.rows[this.highlighted];
|
|
13653
|
-
if (pick) this.
|
|
13990
|
+
if (pick) this.pick(pick);
|
|
13654
13991
|
return;
|
|
13655
13992
|
} else return;
|
|
13656
13993
|
e.preventDefault();
|
|
13657
13994
|
});
|
|
13658
13995
|
this.list.addEventListener("click", (e) => {
|
|
13659
|
-
const
|
|
13660
|
-
|
|
13996
|
+
const target = e.target;
|
|
13997
|
+
const row = target.closest(".vela-sp-row");
|
|
13998
|
+
if (!row) return;
|
|
13999
|
+
const s = this.rows[Number(row.dataset.i)];
|
|
14000
|
+
if (!s) return;
|
|
14001
|
+
if (target.closest(".vela-sp-expander")) this.toggleExpand(s);
|
|
14002
|
+
else this.pick(s);
|
|
13661
14003
|
});
|
|
13662
14004
|
}
|
|
13663
14005
|
/** Wire where symbols come from (re-called on every widget rebuild). */
|
|
@@ -13674,6 +14016,27 @@ var SymbolPicker = class {
|
|
|
13674
14016
|
destroy() {
|
|
13675
14017
|
this.dialog.destroy();
|
|
13676
14018
|
}
|
|
14019
|
+
/** Route a row activation: a GROUP row loads its default member (the root itself is
|
|
14020
|
+
* listed, never loadable), any other row loads itself. */
|
|
14021
|
+
pick(s) {
|
|
14022
|
+
const target = isGroupRow(s) ? defaultMemberOf(this.pool(), s) ?? s : s;
|
|
14023
|
+
this.select(target.ticker, target.prefix ?? target.provider, this.opts.onSelect);
|
|
14024
|
+
}
|
|
14025
|
+
/** Expand/collapse a group row IN PLACE — same query, same page, same scroll; only
|
|
14026
|
+
* the member rows under the group appear or go. */
|
|
14027
|
+
toggleExpand(s) {
|
|
14028
|
+
const key = groupKeyOf(s);
|
|
14029
|
+
if (!this.expanded.delete(key)) this.expanded.add(key);
|
|
14030
|
+
const scrollTop = this.list.scrollTop;
|
|
14031
|
+
const focus = this.rows[this.highlighted];
|
|
14032
|
+
this.rows = this.computeRows();
|
|
14033
|
+
this.list.replaceChildren();
|
|
14034
|
+
this.rows.forEach((r, i) => this.list.appendChild(this.rowEl(r, i)));
|
|
14035
|
+
const at = focus ? this.rows.indexOf(focus) : -1;
|
|
14036
|
+
this.highlighted = at >= 0 ? at : Math.min(this.highlighted, Math.max(0, this.rows.length - 1));
|
|
14037
|
+
this.renderHighlight();
|
|
14038
|
+
this.list.scrollTop = scrollTop;
|
|
14039
|
+
}
|
|
13677
14040
|
select(ticker, venue, onSelect) {
|
|
13678
14041
|
this.close();
|
|
13679
14042
|
onSelect(venue ? `${venue}:${ticker}` : ticker);
|
|
@@ -13700,10 +14063,26 @@ var SymbolPicker = class {
|
|
|
13700
14063
|
return raw;
|
|
13701
14064
|
}
|
|
13702
14065
|
computeRows() {
|
|
13703
|
-
const TAB_TYPES = { Crypto: ["crypto"], Stocks: ["stock"], ETFs: ["etf"], Forex: ["forex"], Commodities: ["commodity"] };
|
|
14066
|
+
const TAB_TYPES = { Crypto: ["crypto"], Stocks: ["stock"], ETFs: ["etf"], Futures: ["futures", "root"], Forex: ["forex"], Commodities: ["commodity"] };
|
|
13704
14067
|
const all = this.pool();
|
|
13705
14068
|
const pool = this.activeTab === "All" ? all : all.filter((s) => TAB_TYPES[this.activeTab]?.includes((s.type ?? "").toLowerCase()) || this.activeTab === "Crypto" && (s.type ?? "").toLowerCase() === "futures");
|
|
13706
|
-
|
|
14069
|
+
const filtered = filterSymbols(pool, this.input.value, this.visible, TOP_TICKERS);
|
|
14070
|
+
this.exhausted = filtered.length < this.visible;
|
|
14071
|
+
const folded = foldGroups(filtered);
|
|
14072
|
+
if (!this.expanded.size) return folded;
|
|
14073
|
+
const keyOf = (s) => `${(s.prefix ?? s.provider ?? "").toLowerCase()}:${s.ticker.toUpperCase()}`;
|
|
14074
|
+
const present = new Set(folded.map(keyOf));
|
|
14075
|
+
const out = [];
|
|
14076
|
+
for (const s of folded) {
|
|
14077
|
+
out.push(s);
|
|
14078
|
+
if (!isGroupRow(s) || !this.expanded.has(groupKeyOf(s))) continue;
|
|
14079
|
+
for (const m of groupMembers(all, s)) {
|
|
14080
|
+
if (present.has(keyOf(m))) continue;
|
|
14081
|
+
present.add(keyOf(m));
|
|
14082
|
+
out.push(m);
|
|
14083
|
+
}
|
|
14084
|
+
}
|
|
14085
|
+
return out;
|
|
13707
14086
|
}
|
|
13708
14087
|
refresh() {
|
|
13709
14088
|
const doc = this.list.ownerDocument;
|
|
@@ -13718,19 +14097,20 @@ var SymbolPicker = class {
|
|
|
13718
14097
|
this.list.appendChild(empty);
|
|
13719
14098
|
return;
|
|
13720
14099
|
}
|
|
13721
|
-
|
|
14100
|
+
this.rows.forEach((s, i) => this.list.appendChild(this.rowEl(s, i)));
|
|
13722
14101
|
this.renderHighlight();
|
|
13723
14102
|
}
|
|
13724
14103
|
/** Append the page the grown `visible` just uncovered — rows already on screen stay put. */
|
|
13725
14104
|
grow() {
|
|
13726
14105
|
const already = this.rows.length;
|
|
13727
14106
|
this.rows = this.computeRows();
|
|
13728
|
-
|
|
14107
|
+
this.rows.slice(already).forEach((s, j) => this.list.appendChild(this.rowEl(s, already + j)));
|
|
13729
14108
|
}
|
|
13730
|
-
rowEl(s) {
|
|
14109
|
+
rowEl(s, i) {
|
|
13731
14110
|
const doc = this.list.ownerDocument;
|
|
13732
14111
|
const row = doc.createElement("div");
|
|
13733
14112
|
row.className = "vela-sp-row";
|
|
14113
|
+
row.dataset.i = String(i);
|
|
13734
14114
|
row.dataset.ticker = s.ticker;
|
|
13735
14115
|
const venue = s.prefix ?? s.provider;
|
|
13736
14116
|
if (venue) row.dataset.venue = venue;
|
|
@@ -13745,6 +14125,14 @@ var SymbolPicker = class {
|
|
|
13745
14125
|
d.textContent = s.description ?? (s.type ?? "");
|
|
13746
14126
|
main.append(t, d);
|
|
13747
14127
|
row.append(av, main);
|
|
14128
|
+
if (isGroupRow(s)) {
|
|
14129
|
+
row.dataset.group = "1";
|
|
14130
|
+
const expander = doc.createElement("button");
|
|
14131
|
+
expander.className = "vela-sp-expander";
|
|
14132
|
+
expander.setAttribute("aria-label", "Show contracts");
|
|
14133
|
+
expander.appendChild(iconEl(this.expanded.has(groupKeyOf(s)) ? "chevron-down" : "chevron-right", doc));
|
|
14134
|
+
row.appendChild(expander);
|
|
14135
|
+
} else if (s.group != null && this.expanded.has(groupKeyOf(s))) row.dataset.member = "1";
|
|
13748
14136
|
if (venue) {
|
|
13749
14137
|
const badge = doc.createElement("span");
|
|
13750
14138
|
badge.className = "vela-sp-badge";
|
|
@@ -14361,6 +14749,9 @@ var CSS13 = `
|
|
|
14361
14749
|
}
|
|
14362
14750
|
.vela-mb-item:active { background: var(--vela-hover); }
|
|
14363
14751
|
.vela-mb-item .vela-icon { font-size: 18px; width: 18px; height: 18px; }
|
|
14752
|
+
/* A lit stop (the maximize toggle while something is isolated): the inverse
|
|
14753
|
+
"selected" chip \u2014 white on the dark theme, dark on the light one. */
|
|
14754
|
+
.vela-mb-item.vela-mb-on, .vela-mb-item.vela-mb-on:active { background: var(--vela-selected-bg); color: var(--vela-selected-fg); }
|
|
14364
14755
|
/* Left-aligned contributed actions get their own stops (the built-in indicators
|
|
14365
14756
|
slot) \u2014 the wrapper is layout-transparent so each stop flexes like a sibling. */
|
|
14366
14757
|
.vela-mb-actions { display: contents; }
|
|
@@ -14401,9 +14792,11 @@ var MobileBar = class {
|
|
|
14401
14792
|
this.actionsHost.className = "vela-mb-actions";
|
|
14402
14793
|
const onDrawings = opts.onDrawingsClick;
|
|
14403
14794
|
const drawings = onDrawings ? item("vela-mb-drawings", "Drawings", onDrawings, "pen") : null;
|
|
14795
|
+
const onMaximize = opts.onMaximizeClick;
|
|
14796
|
+
this.maxEl = onMaximize ? item("vela-mb-maximize", "Maximize chart", onMaximize, "maximize") : null;
|
|
14404
14797
|
const more = item("vela-mb-more", "More", opts.onMoreClick, "kebab");
|
|
14405
14798
|
const settings = item("vela-mb-settings", "Chart settings", opts.onSettingsClick, "gear");
|
|
14406
|
-
this.el.append(this.symbolEl, this.tfEl, ...indicators ? [indicators] : [], this.actionsHost, ...drawings ? [drawings] : [], more, settings);
|
|
14799
|
+
this.el.append(this.symbolEl, this.tfEl, ...indicators ? [indicators] : [], this.actionsHost, ...drawings ? [drawings] : [], ...this.maxEl ? [this.maxEl] : [], more, settings);
|
|
14407
14800
|
host.appendChild(this.el);
|
|
14408
14801
|
this.renderActions();
|
|
14409
14802
|
}
|
|
@@ -14435,6 +14828,14 @@ var MobileBar = class {
|
|
|
14435
14828
|
setTimeframe(tf) {
|
|
14436
14829
|
this.tfEl.textContent = timeframeLabel(tf);
|
|
14437
14830
|
}
|
|
14831
|
+
/** Light the maximize stop while something is isolated (a chart over the grid,
|
|
14832
|
+
* or a maximized pane inside the active chart) — inverse chip + restore glyph. */
|
|
14833
|
+
setMaximizeActive(on) {
|
|
14834
|
+
if (!this.maxEl) return;
|
|
14835
|
+
this.maxEl.classList.toggle("vela-mb-on", on);
|
|
14836
|
+
this.maxEl.setAttribute("aria-label", on ? "Restore layout" : "Maximize chart");
|
|
14837
|
+
this.maxEl.replaceChildren(iconEl(on ? "restore" : "maximize", this.el.ownerDocument));
|
|
14838
|
+
}
|
|
14438
14839
|
destroy() {
|
|
14439
14840
|
this.el.remove();
|
|
14440
14841
|
}
|
|
@@ -16612,7 +17013,7 @@ var DrawingToolbar = class {
|
|
|
16612
17013
|
this.root.replaceChildren();
|
|
16613
17014
|
this.groupCells.clear();
|
|
16614
17015
|
this.groupIcons.clear();
|
|
16615
|
-
this.cursorBtn = this.makeButton(CURSOR_ICON, "Cursor", () => this.
|
|
17016
|
+
this.cursorBtn = this.makeButton(CURSOR_ICON, "Cursor", () => this.onCursorClick());
|
|
16616
17017
|
this.root.appendChild(this.cursorBtn);
|
|
16617
17018
|
if (this.def.groups.length > 0) this.root.appendChild(this.divider());
|
|
16618
17019
|
for (const g of this.def.groups) {
|
|
@@ -16716,6 +17117,14 @@ var DrawingToolbar = class {
|
|
|
16716
17117
|
this.magnetIcon = icon2;
|
|
16717
17118
|
return cell;
|
|
16718
17119
|
}
|
|
17120
|
+
/** Cursor returns to select/idle: an active measure/eraser mode exits through its own
|
|
17121
|
+
* toggle callback (disarming a tool via `onArm(null)` alone can't — the host treats a
|
|
17122
|
+
* null arm as a no-op side effect of entering those modes), then the tool disarms. */
|
|
17123
|
+
onCursorClick() {
|
|
17124
|
+
if (this.measureActive) this.onMeasure();
|
|
17125
|
+
if (this.eraserActive) this.onEraser();
|
|
17126
|
+
this.onArm(null);
|
|
17127
|
+
}
|
|
16719
17128
|
/** Clicking the icon arms the group's last-used tool (it does NOT open the flyout). */
|
|
16720
17129
|
onGroupIconClick(group) {
|
|
16721
17130
|
const type = this.lastUsed.get(group.id) ?? group.tools[0]?.type;
|
|
@@ -17510,6 +17919,7 @@ var IndicatorHandleImpl = class {
|
|
|
17510
17919
|
constructor(id, title, controller, source) {
|
|
17511
17920
|
this.controller = controller;
|
|
17512
17921
|
this.schema = [];
|
|
17922
|
+
this.propsSchema = [];
|
|
17513
17923
|
this.visibleState = true;
|
|
17514
17924
|
this.bus = new TypedEventBus();
|
|
17515
17925
|
this.id = id;
|
|
@@ -17519,6 +17929,9 @@ var IndicatorHandleImpl = class {
|
|
|
17519
17929
|
get inputs() {
|
|
17520
17930
|
return this.schema;
|
|
17521
17931
|
}
|
|
17932
|
+
get props() {
|
|
17933
|
+
return this.propsSchema;
|
|
17934
|
+
}
|
|
17522
17935
|
get visible() {
|
|
17523
17936
|
return this.visibleState;
|
|
17524
17937
|
}
|
|
@@ -17528,6 +17941,12 @@ var IndicatorHandleImpl = class {
|
|
|
17528
17941
|
setInputs(values) {
|
|
17529
17942
|
this.controller.applyInputs(this.id, values);
|
|
17530
17943
|
}
|
|
17944
|
+
setProp(key, value) {
|
|
17945
|
+
this.controller.applyProps(this.id, { [key]: value });
|
|
17946
|
+
}
|
|
17947
|
+
setProps(values) {
|
|
17948
|
+
this.controller.applyProps(this.id, values);
|
|
17949
|
+
}
|
|
17531
17950
|
setVisible(visible) {
|
|
17532
17951
|
this.controller.setVisible(this.id, visible);
|
|
17533
17952
|
}
|
|
@@ -17547,6 +17966,9 @@ var IndicatorHandleImpl = class {
|
|
|
17547
17966
|
setSchema(schema) {
|
|
17548
17967
|
this.schema = schema;
|
|
17549
17968
|
}
|
|
17969
|
+
setPropsSchema(schema) {
|
|
17970
|
+
this.propsSchema = schema;
|
|
17971
|
+
}
|
|
17550
17972
|
/** Sync the public `visible` getter when the orchestrator changes visibility (API or legend eye). */
|
|
17551
17973
|
setVisibleState(visible) {
|
|
17552
17974
|
this.visibleState = visible;
|
|
@@ -17560,6 +17982,8 @@ var IndicatorHandleImpl = class {
|
|
|
17560
17982
|
function summarizeModel(model) {
|
|
17561
17983
|
const series = {};
|
|
17562
17984
|
for (const s of model.series) series[s.kind] = (series[s.kind] ?? 0) + 1;
|
|
17985
|
+
const countOverlay = (items) => items?.filter((i) => i.overlay === true).length ?? 0;
|
|
17986
|
+
const forcedOverlay = countOverlay(model.series) + countOverlay(model.fills) + countOverlay(model.backgrounds) + countOverlay(model.lines) + countOverlay(model.boxes) + countOverlay(model.labels) + countOverlay(model.polylines) + countOverlay(model.linefills) + countOverlay(model.tables);
|
|
17563
17987
|
return {
|
|
17564
17988
|
id: model.id,
|
|
17565
17989
|
title: model.title,
|
|
@@ -17579,7 +18003,8 @@ function summarizeModel(model) {
|
|
|
17579
18003
|
tables: model.tables?.length ?? 0,
|
|
17580
18004
|
barColors: model.barColors?.length ?? 0,
|
|
17581
18005
|
trades: model.trades?.length ?? 0,
|
|
17582
|
-
inputs: model.inputs.length
|
|
18006
|
+
inputs: model.inputs.length,
|
|
18007
|
+
forcedOverlay
|
|
17583
18008
|
};
|
|
17584
18009
|
}
|
|
17585
18010
|
function inspectModels(models) {
|
|
@@ -17600,7 +18025,8 @@ function inspectModels(models) {
|
|
|
17600
18025
|
linefills: sum((s) => s.linefills),
|
|
17601
18026
|
tables: sum((s) => s.tables),
|
|
17602
18027
|
barColors: sum((s) => s.barColors),
|
|
17603
|
-
trades: sum((s) => s.trades)
|
|
18028
|
+
trades: sum((s) => s.trades),
|
|
18029
|
+
forcedOverlay: sum((s) => s.forcedOverlay)
|
|
17604
18030
|
}
|
|
17605
18031
|
};
|
|
17606
18032
|
}
|
|
@@ -17711,6 +18137,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
17711
18137
|
/** Invalidates detached async work (backfill loops, in-flight loads, gap heals):
|
|
17712
18138
|
* bumped by init(), setMarket() and destroy(). */
|
|
17713
18139
|
this.generation = 0;
|
|
18140
|
+
/** Aborts the in-flight PROGRESSIVE load's source polling on supersession — an
|
|
18141
|
+
* abandoned stream left polling to its own budget starves the browser's per-host
|
|
18142
|
+
* connection pool, and the NEXT symbol's very first fetch with it (measured). */
|
|
18143
|
+
this.progressiveAbort = null;
|
|
17714
18144
|
/** Awaiters racing a superseded load (setMarket callers) — released on every bump so they never hang. */
|
|
17715
18145
|
this.supersedeWaiters = [];
|
|
17716
18146
|
/** `history:complete` fired for the CURRENT load. Each market load re-arms the cycle
|
|
@@ -17744,7 +18174,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
17744
18174
|
for (const engine of engines) this.registerEngine(engine.language, engine);
|
|
17745
18175
|
this.defaultLanguage = config.defaultLanguage;
|
|
17746
18176
|
this.renderer.mount(container, config.theme);
|
|
17747
|
-
this.renderer.onInputChange(
|
|
18177
|
+
this.renderer.onInputChange(
|
|
18178
|
+
(e) => e.kind === "prop" ? this.applyProps(e.indicatorId, { [e.key]: e.value }) : this.applyInputs(e.indicatorId, { [e.key]: e.value })
|
|
18179
|
+
);
|
|
17748
18180
|
this.renderer.onRemoveIndicator((id) => this.removeIndicator(id));
|
|
17749
18181
|
this.renderer.onToggleIndicatorVisible?.((id, visible) => this.setVisible(id, visible));
|
|
17750
18182
|
this.paneActionUnsub = this.renderer.onPaneAction?.((a) => this.handlePaneAction(a)) ?? null;
|
|
@@ -17851,6 +18283,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
17851
18283
|
* superseded setMarket awaiters so their promises resolve instead of hanging. */
|
|
17852
18284
|
bumpGeneration() {
|
|
17853
18285
|
const gen = ++this.generation;
|
|
18286
|
+
this.progressiveAbort?.abort();
|
|
18287
|
+
this.progressiveAbort = null;
|
|
17854
18288
|
for (const w of this.supersedeWaiters.splice(0)) w();
|
|
17855
18289
|
return gen;
|
|
17856
18290
|
}
|
|
@@ -17904,7 +18338,47 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
17904
18338
|
const requested = market.bars ?? 500;
|
|
17905
18339
|
const initialRange = market.visibleRange;
|
|
17906
18340
|
const deep = !market.data?.length && initialRange == null && requested > SINGLE_LOAD_BARS;
|
|
17907
|
-
|
|
18341
|
+
let progressiveServed = false;
|
|
18342
|
+
if (!market.data?.length && initialRange == null && this.feed.loadProgressive) {
|
|
18343
|
+
let painted = false;
|
|
18344
|
+
const paint = (bars, final) => {
|
|
18345
|
+
if (this.generation !== gen || !final && bars.length === 0) return;
|
|
18346
|
+
this.setBarSeries(bars, painted ? { preserveView: true } : void 0);
|
|
18347
|
+
if (!painted && bars.length > 0) {
|
|
18348
|
+
painted = true;
|
|
18349
|
+
if (opts.firstLoad) this.activateBarLayers();
|
|
18350
|
+
if (!final) this.historyState = "backfill";
|
|
18351
|
+
}
|
|
18352
|
+
};
|
|
18353
|
+
const abort = new AbortController();
|
|
18354
|
+
this.progressiveAbort = abort;
|
|
18355
|
+
progressiveServed = await new Promise((firstPaint) => {
|
|
18356
|
+
let signaled = false;
|
|
18357
|
+
const signal = (served) => {
|
|
18358
|
+
if (!signaled) {
|
|
18359
|
+
signaled = true;
|
|
18360
|
+
firstPaint(served);
|
|
18361
|
+
}
|
|
18362
|
+
};
|
|
18363
|
+
abort.signal.addEventListener("abort", () => signal(true), { once: true });
|
|
18364
|
+
this.feed.loadProgressive(market, (bars) => {
|
|
18365
|
+
paint(bars, false);
|
|
18366
|
+
if (painted) signal(true);
|
|
18367
|
+
}, { signal: abort.signal }).then((full) => {
|
|
18368
|
+
if (this.progressiveAbort === abort) this.progressiveAbort = null;
|
|
18369
|
+
if (full == null) return signal(false);
|
|
18370
|
+
if (this.generation !== gen) return signal(true);
|
|
18371
|
+
paint(full, true);
|
|
18372
|
+
this.completeHistory(full.length >= requested ? "depth" : "genesis");
|
|
18373
|
+
signal(true);
|
|
18374
|
+
}).catch(() => {
|
|
18375
|
+
if (this.progressiveAbort === abort) this.progressiveAbort = null;
|
|
18376
|
+
if (this.generation === gen) this.completeHistory("aborted");
|
|
18377
|
+
signal(true);
|
|
18378
|
+
});
|
|
18379
|
+
});
|
|
18380
|
+
}
|
|
18381
|
+
if (progressiveServed) ; else if (deep && this.feed.loadRange) {
|
|
17908
18382
|
const head = await this.feed.load({ ...market, bars: Math.min(requested, CHUNK_BARS) });
|
|
17909
18383
|
if (this.generation !== gen) return;
|
|
17910
18384
|
this.setBarSeries(head);
|
|
@@ -18424,7 +18898,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18424
18898
|
const title = options.title ?? "Indicator";
|
|
18425
18899
|
const handle = new IndicatorHandleImpl(id, title, this, source);
|
|
18426
18900
|
this.handles.set(id, handle);
|
|
18427
|
-
this.registry.add({ id, title, source, options, inputValues: { ...options.inputs ?? {} } });
|
|
18901
|
+
this.registry.add({ id, title, source, options, inputValues: { ...options.inputs ?? {} }, propValues: { ...options.props ?? {} } });
|
|
18428
18902
|
void this.startIndicator(id, source, options, handle);
|
|
18429
18903
|
return handle;
|
|
18430
18904
|
}
|
|
@@ -18447,7 +18921,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18447
18921
|
return handle;
|
|
18448
18922
|
}
|
|
18449
18923
|
const inputValues = { ...descriptor.defaultInputs(), ...options.inputs ?? {} };
|
|
18450
|
-
this.registry.add({ id, title, source: type, inputValues, native: { type, instance: descriptor.create(), descriptor } });
|
|
18924
|
+
this.registry.add({ id, title, source: type, inputValues, propValues: {}, native: { type, instance: descriptor.create(), descriptor } });
|
|
18451
18925
|
handle.setSchema(descriptor.inputsSchema());
|
|
18452
18926
|
void this.startNativeIndicator(id, handle);
|
|
18453
18927
|
return handle;
|
|
@@ -18500,6 +18974,19 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18500
18974
|
if (record.session) record.session.update(record.inputValues);
|
|
18501
18975
|
else if (record.native && !record.hidden) record.native.instance.setInputs(record.inputValues);
|
|
18502
18976
|
}
|
|
18977
|
+
/** IndicatorController: re-run an indicator with merged declaration-prop overrides.
|
|
18978
|
+
* Same lifecycle as {@link applyInputs} — a prop change replays the whole script.
|
|
18979
|
+
* Script indicators only: natives have no declaration props. */
|
|
18980
|
+
applyProps(id, values) {
|
|
18981
|
+
const record = this.registry.get(id);
|
|
18982
|
+
if (!record?.session) return;
|
|
18983
|
+
record.propValues = { ...record.propValues, ...values };
|
|
18984
|
+
if (record.renderHandle) this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
|
|
18985
|
+
record.pendingStructural = true;
|
|
18986
|
+
if (!record.hidden) this.setLoading(record, true);
|
|
18987
|
+
record.pendingCause = "inputs";
|
|
18988
|
+
record.session.update(record.inputValues, record.propValues);
|
|
18989
|
+
}
|
|
18503
18990
|
/** IndicatorController: tear down an indicator and (if now empty) its pane. */
|
|
18504
18991
|
/** Live handles of every indicator on the chart (script + native), insertion order. */
|
|
18505
18992
|
listIndicators() {
|
|
@@ -18648,6 +19135,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18648
19135
|
for (const input of prepared.inputs) defaults2[input.key] = input.defval;
|
|
18649
19136
|
record.inputValues = { ...defaults2, ...record.inputValues };
|
|
18650
19137
|
handle.setSchema(prepared.inputs);
|
|
19138
|
+
const propDefaults = {};
|
|
19139
|
+
for (const prop of prepared.props ?? []) propDefaults[prop.key] = prop.defval;
|
|
19140
|
+
record.propValues = { ...propDefaults, ...record.propValues };
|
|
19141
|
+
handle.setPropsSchema(prepared.props ?? []);
|
|
18651
19142
|
this.mountLoadingPlaceholder(id, record);
|
|
18652
19143
|
this.executeIndicator(id, handle);
|
|
18653
19144
|
} catch (err) {
|
|
@@ -18671,6 +19162,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18671
19162
|
getBars: () => this.bars,
|
|
18672
19163
|
fetchSeries: (sym, tf, range) => this.fetchSeries(sym, tf, range),
|
|
18673
19164
|
inputs: record.inputValues,
|
|
19165
|
+
props: record.propValues,
|
|
18674
19166
|
visibleRange: this.currentVisibleRange(),
|
|
18675
19167
|
mode,
|
|
18676
19168
|
// Mid-backfill session starts (add / re-show / price-style re-execute) let the
|
|
@@ -18764,6 +19256,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18764
19256
|
const meta = record.prepared.meta;
|
|
18765
19257
|
const model = {
|
|
18766
19258
|
id,
|
|
19259
|
+
// Deliberately the FULL title, never a shorttitle: while the script loads the
|
|
19260
|
+
// legend identifies it by its full name; the compact shorttitle arrives with
|
|
19261
|
+
// the first computed model and takes over from there.
|
|
18767
19262
|
title: record.options?.title ?? meta.title,
|
|
18768
19263
|
overlay: meta.overlay,
|
|
18769
19264
|
paneHint: meta.overlay ? "price" : "new",
|
|
@@ -18772,7 +19267,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18772
19267
|
backgrounds: [],
|
|
18773
19268
|
priceLines: [],
|
|
18774
19269
|
inputs: record.prepared.inputs,
|
|
18775
|
-
inputValues: record.inputValues
|
|
19270
|
+
inputValues: record.inputValues,
|
|
19271
|
+
...record.prepared.props ? { props: record.prepared.props, propValues: record.propValues } : {}
|
|
18776
19272
|
};
|
|
18777
19273
|
const paneId = this.routePane(id, model, record.options ?? {});
|
|
18778
19274
|
this.placeModel(model, id, paneId);
|
|
@@ -19005,7 +19501,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19005
19501
|
record.model = model;
|
|
19006
19502
|
if (record.pendingStructural) {
|
|
19007
19503
|
record.renderHandle = this.renderer.mountIndicator(model);
|
|
19008
|
-
this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues);
|
|
19504
|
+
this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
|
|
19009
19505
|
record.pendingStructural = false;
|
|
19010
19506
|
} else {
|
|
19011
19507
|
this.renderer.updateIndicator(record.renderHandle, modelToValuePatch(model));
|
|
@@ -19032,16 +19528,17 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19032
19528
|
placeModel(model, id, paneId) {
|
|
19033
19529
|
model.id = id;
|
|
19034
19530
|
model.paneId = paneId;
|
|
19035
|
-
|
|
19036
|
-
for (const
|
|
19037
|
-
for (const
|
|
19531
|
+
const paneOf = (item) => item.overlay === true ? "price" : paneId;
|
|
19532
|
+
for (const series of model.series) series.paneId = paneOf(series);
|
|
19533
|
+
for (const fill of model.fills) fill.paneId = paneOf(fill);
|
|
19534
|
+
for (const bg of model.backgrounds) bg.paneId = paneOf(bg);
|
|
19038
19535
|
for (const line of model.priceLines) line.paneId = paneId;
|
|
19039
|
-
if (model.lines) for (const ln of model.lines) ln.paneId =
|
|
19040
|
-
if (model.boxes) for (const bx of model.boxes) bx.paneId =
|
|
19041
|
-
if (model.labels) for (const lb of model.labels) lb.paneId =
|
|
19042
|
-
if (model.polylines) for (const pl of model.polylines) pl.paneId =
|
|
19043
|
-
if (model.linefills) for (const lf of model.linefills) lf.paneId =
|
|
19044
|
-
if (model.tables) for (const tb of model.tables) tb.paneId =
|
|
19536
|
+
if (model.lines) for (const ln of model.lines) ln.paneId = paneOf(ln);
|
|
19537
|
+
if (model.boxes) for (const bx of model.boxes) bx.paneId = paneOf(bx);
|
|
19538
|
+
if (model.labels) for (const lb of model.labels) lb.paneId = paneOf(lb);
|
|
19539
|
+
if (model.polylines) for (const pl of model.polylines) pl.paneId = paneOf(pl);
|
|
19540
|
+
if (model.linefills) for (const lf of model.linefills) lf.paneId = paneOf(lf);
|
|
19541
|
+
if (model.tables) for (const tb of model.tables) tb.paneId = paneOf(tb);
|
|
19045
19542
|
}
|
|
19046
19543
|
emitContextChanged(id) {
|
|
19047
19544
|
if (!this.registry.get(id)?.session?.getContext) return;
|
|
@@ -19194,6 +19691,15 @@ var RendererControl = class {
|
|
|
19194
19691
|
setLegendActions(provider) {
|
|
19195
19692
|
this.renderer.setLegendActions?.(provider);
|
|
19196
19693
|
}
|
|
19694
|
+
/**
|
|
19695
|
+
* Wire the legend rows' HOST-CONTRIBUTED callout bubbles (the shells route the
|
|
19696
|
+
* plugin registry through this; see `registerLegendCallout`). Silent on a renderer
|
|
19697
|
+
* without the seam — contributed callouts simply never show there, same graceful
|
|
19698
|
+
* degradation as {@link setLegendActions}.
|
|
19699
|
+
*/
|
|
19700
|
+
setLegendCallouts(provider) {
|
|
19701
|
+
this.renderer.setLegendCallouts?.(provider);
|
|
19702
|
+
}
|
|
19197
19703
|
/**
|
|
19198
19704
|
* Replace the indicator legend's fold toggle with a host action (or restore it with
|
|
19199
19705
|
* `null`) — multi-chart shells point the chip at their indicator overview instead of
|
|
@@ -19703,6 +20209,10 @@ function inputVisible(when, values) {
|
|
|
19703
20209
|
}
|
|
19704
20210
|
|
|
19705
20211
|
// src/renderers/shared/IndicatorInputsDialog.ts
|
|
20212
|
+
var PROPS_TAB = "Properties";
|
|
20213
|
+
function bagOf(row, decl) {
|
|
20214
|
+
return decl.prop ? row.propValues : row.values;
|
|
20215
|
+
}
|
|
19706
20216
|
var SOURCES = ["close", "open", "high", "low", "hl2", "hlc3", "ohlc4", "volume"];
|
|
19707
20217
|
var IndicatorInputsDialog = class {
|
|
19708
20218
|
constructor(host) {
|
|
@@ -19713,6 +20223,7 @@ var IndicatorInputsDialog = class {
|
|
|
19713
20223
|
this.openId = null;
|
|
19714
20224
|
this.row = null;
|
|
19715
20225
|
this.snapshot = null;
|
|
20226
|
+
this.propSnapshot = null;
|
|
19716
20227
|
this.dialogTips = [];
|
|
19717
20228
|
/** Re-applies every `when` gate against current values; set while the dialog is open. */
|
|
19718
20229
|
this.refreshVisibility = null;
|
|
@@ -19735,10 +20246,11 @@ var IndicatorInputsDialog = class {
|
|
|
19735
20246
|
}
|
|
19736
20247
|
open(row) {
|
|
19737
20248
|
this.close();
|
|
19738
|
-
if (row.inputs.length === 0) return;
|
|
20249
|
+
if (row.inputs.length === 0 && row.props.length === 0) return;
|
|
19739
20250
|
this.row = row;
|
|
19740
20251
|
this.openId = row.id;
|
|
19741
20252
|
this.snapshot = { ...row.values };
|
|
20253
|
+
this.propSnapshot = { ...row.propValues };
|
|
19742
20254
|
ensureDialogStyles();
|
|
19743
20255
|
const t = this.host.theme();
|
|
19744
20256
|
const border = "var(--vela-border)";
|
|
@@ -19746,7 +20258,7 @@ var IndicatorInputsDialog = class {
|
|
|
19746
20258
|
const host = this.host.dialogHost() ?? this.host.container;
|
|
19747
20259
|
const ui = new Dialog({
|
|
19748
20260
|
host,
|
|
19749
|
-
title: row.
|
|
20261
|
+
title: row.title,
|
|
19750
20262
|
// Non-modal: live-edit dialog — a modal machine locks pointer events on the
|
|
19751
20263
|
// whole body, killing the chart and the body-portaled popovers.
|
|
19752
20264
|
modal: false,
|
|
@@ -19778,6 +20290,16 @@ var IndicatorInputsDialog = class {
|
|
|
19778
20290
|
text: () => "Close"
|
|
19779
20291
|
}));
|
|
19780
20292
|
const tabDefs = tabInputs(row.inputs);
|
|
20293
|
+
if (row.props.length > 0) {
|
|
20294
|
+
const propDecls = row.props.map((p) => {
|
|
20295
|
+
const d = { ...p, prop: true };
|
|
20296
|
+
delete d.tab;
|
|
20297
|
+
return d;
|
|
20298
|
+
});
|
|
20299
|
+
const existing = tabDefs.find((t2) => t2.name === PROPS_TAB);
|
|
20300
|
+
if (existing) existing.inputs.push(...propDecls);
|
|
20301
|
+
else tabDefs.push({ name: PROPS_TAB, inputs: propDecls });
|
|
20302
|
+
}
|
|
19781
20303
|
const tabs = document.createElement("div");
|
|
19782
20304
|
tabs.style.cssText = `display:flex;gap:12px;padding:0 20px;border-bottom:1px solid ${border};flex:0 0 auto;`;
|
|
19783
20305
|
const tabEls = [];
|
|
@@ -19880,9 +20402,10 @@ var IndicatorInputsDialog = class {
|
|
|
19880
20402
|
this.openId = null;
|
|
19881
20403
|
this.row = null;
|
|
19882
20404
|
this.snapshot = null;
|
|
20405
|
+
this.propSnapshot = null;
|
|
19883
20406
|
ui?.destroy();
|
|
19884
20407
|
}
|
|
19885
|
-
/** Restore every input to its open-time value (re-running the indicator), then close. */
|
|
20408
|
+
/** Restore every input and prop to its open-time value (re-running the indicator), then close. */
|
|
19886
20409
|
revertAndClose() {
|
|
19887
20410
|
const row = this.row;
|
|
19888
20411
|
const snap = this.snapshot;
|
|
@@ -19896,6 +20419,17 @@ var IndicatorInputsDialog = class {
|
|
|
19896
20419
|
}
|
|
19897
20420
|
}
|
|
19898
20421
|
}
|
|
20422
|
+
const propSnap = this.propSnapshot;
|
|
20423
|
+
if (row && propSnap) {
|
|
20424
|
+
for (const p of row.props) {
|
|
20425
|
+
const snapped = propSnap[p.key];
|
|
20426
|
+
const before = snapped !== void 0 ? snapped : p.defval;
|
|
20427
|
+
if (row.propValues[p.key] !== before) {
|
|
20428
|
+
row.propValues[p.key] = before;
|
|
20429
|
+
this.host.onChange?.({ indicatorId: row.id, key: p.key, value: before, kind: "prop" });
|
|
20430
|
+
}
|
|
20431
|
+
}
|
|
20432
|
+
}
|
|
19899
20433
|
this.close();
|
|
19900
20434
|
}
|
|
19901
20435
|
/** The footer's reset button — same chip as Cancel, pinned to the LEFT edge
|
|
@@ -19913,19 +20447,27 @@ var IndicatorInputsDialog = class {
|
|
|
19913
20447
|
const row = this.row;
|
|
19914
20448
|
if (!row) return;
|
|
19915
20449
|
const snap = this.snapshot;
|
|
20450
|
+
const propSnap = this.propSnapshot;
|
|
19916
20451
|
for (const inp of row.inputs) {
|
|
19917
20452
|
if (row.values[inp.key] !== inp.defval) {
|
|
19918
20453
|
row.values[inp.key] = inp.defval;
|
|
19919
20454
|
this.host.onChange?.({ indicatorId: row.id, key: inp.key, value: inp.defval });
|
|
19920
20455
|
}
|
|
19921
20456
|
}
|
|
20457
|
+
for (const p of row.props) {
|
|
20458
|
+
if (row.propValues[p.key] !== p.defval) {
|
|
20459
|
+
row.propValues[p.key] = p.defval;
|
|
20460
|
+
this.host.onChange?.({ indicatorId: row.id, key: p.key, value: p.defval, kind: "prop" });
|
|
20461
|
+
}
|
|
20462
|
+
}
|
|
19922
20463
|
this.open(row);
|
|
19923
20464
|
if (snap) this.snapshot = snap;
|
|
20465
|
+
if (propSnap) this.propSnapshot = propSnap;
|
|
19924
20466
|
}
|
|
19925
20467
|
/** Write one edit through: store it, notify the host, and re-apply the `when` gates. */
|
|
19926
|
-
commit(row,
|
|
19927
|
-
row.
|
|
19928
|
-
this.host.onChange?.({ indicatorId: row.id, key, value });
|
|
20468
|
+
commit(row, decl, value) {
|
|
20469
|
+
bagOf(row, decl)[decl.key] = value;
|
|
20470
|
+
this.host.onChange?.({ indicatorId: row.id, key: decl.key, value, ...decl.prop ? { kind: "prop" } : {} });
|
|
19929
20471
|
this.refreshVisibility?.();
|
|
19930
20472
|
}
|
|
19931
20473
|
/** One settings row (or several `inline=` inputs) placed into a section's grid. */
|
|
@@ -19952,7 +20494,7 @@ var IndicatorInputsDialog = class {
|
|
|
19952
20494
|
const id = idOf(lead);
|
|
19953
20495
|
const info = lead.tooltip ? this.infoButton(lead.tooltip) : void 0;
|
|
19954
20496
|
if (lead.type === "bool") {
|
|
19955
|
-
const current = Boolean(row
|
|
20497
|
+
const current = Boolean(bagOf(row, lead)[lead.key] ?? lead.defval);
|
|
19956
20498
|
return append(fieldRow({
|
|
19957
20499
|
label: nameOf(lead),
|
|
19958
20500
|
id,
|
|
@@ -19961,7 +20503,7 @@ var IndicatorInputsDialog = class {
|
|
|
19961
20503
|
toggle: {
|
|
19962
20504
|
id,
|
|
19963
20505
|
checked: current,
|
|
19964
|
-
onChange: (v) => this.commit(row, lead
|
|
20506
|
+
onChange: (v) => this.commit(row, lead, v)
|
|
19965
20507
|
}
|
|
19966
20508
|
}));
|
|
19967
20509
|
}
|
|
@@ -19986,8 +20528,8 @@ var IndicatorInputsDialog = class {
|
|
|
19986
20528
|
}
|
|
19987
20529
|
/** Build the typed control for one input, committing edits live via `onChange`. */
|
|
19988
20530
|
buildControl(row, inp, id) {
|
|
19989
|
-
const current = row
|
|
19990
|
-
const emit = (value) => this.commit(row, inp
|
|
20531
|
+
const current = bagOf(row, inp)[inp.key] ?? inp.defval;
|
|
20532
|
+
const emit = (value) => this.commit(row, inp, value);
|
|
19991
20533
|
if (inp.type === "bool") {
|
|
19992
20534
|
return buildFieldControl({ kind: "switch", id, checked: Boolean(current), onChange: (v) => emit(v) }).el;
|
|
19993
20535
|
}
|
|
@@ -19998,7 +20540,7 @@ var IndicatorInputsDialog = class {
|
|
|
19998
20540
|
kind: "color",
|
|
19999
20541
|
id,
|
|
20000
20542
|
theme: this.host.theme(),
|
|
20001
|
-
get: () => String(row
|
|
20543
|
+
get: () => String(bagOf(row, inp)[inp.key] ?? inp.defval),
|
|
20002
20544
|
onChange: (v) => emit(v)
|
|
20003
20545
|
}).el;
|
|
20004
20546
|
}
|
|
@@ -20561,6 +21103,7 @@ function timeParts(ts) {
|
|
|
20561
21103
|
}
|
|
20562
21104
|
|
|
20563
21105
|
// src/renderers/shared/InputsUI.ts
|
|
21106
|
+
var LEGEND_AT_TOP_ATTR = "data-vela-pane-at-top";
|
|
20564
21107
|
var STATUS_KEYFRAMES = "@keyframes vela-ind-pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.3;transform:scale(.6)}}";
|
|
20565
21108
|
var LEGEND_ICON_PX2 = 16;
|
|
20566
21109
|
var LEGEND_CTL_PX = 18;
|
|
@@ -20578,6 +21121,9 @@ var CLOSE_SVG = iconAt("close", LEGEND_ICON_PX2);
|
|
|
20578
21121
|
var FOLD_SVG = iconAt("chevron-up", LEGEND_ICON_PX2);
|
|
20579
21122
|
var UNFOLD_SVG = iconAt("chevron-down", LEGEND_ICON_PX2);
|
|
20580
21123
|
var OVERVIEW_SVG = iconAt("objects", LEGEND_ICON_PX2);
|
|
21124
|
+
function legendCalloutsDisplay(open2, hasCallouts) {
|
|
21125
|
+
return !open2 && hasCallouts ? "inline-flex" : "none";
|
|
21126
|
+
}
|
|
20581
21127
|
var InputsUI = class {
|
|
20582
21128
|
constructor(container, theme, paneBoundsOf) {
|
|
20583
21129
|
this.container = container;
|
|
@@ -20599,10 +21145,14 @@ var InputsUI = class {
|
|
|
20599
21145
|
this.moveApi = null;
|
|
20600
21146
|
/** Host-contributed legend actions, resolved PER ROW at render time (see setLegendActions). */
|
|
20601
21147
|
this.legendActions = null;
|
|
21148
|
+
/** Host-contributed callout bubbles, resolved PER ROW at render time (see setLegendCallouts). */
|
|
21149
|
+
this.legendCallouts = null;
|
|
20602
21150
|
/** Chrome-tooltip disposers, per row id — a tip open at removal must not outlive its row. */
|
|
20603
21151
|
this.rowTips = /* @__PURE__ */ new Map();
|
|
20604
21152
|
/** Same, for the contributed extras only (rebuilt independently by setLegendActions). */
|
|
20605
21153
|
this.extrasTips = /* @__PURE__ */ new Map();
|
|
21154
|
+
/** Same, for the contributed callouts only (rebuilt independently by setLegendCallouts). */
|
|
21155
|
+
this.calloutTips = /* @__PURE__ */ new Map();
|
|
20606
21156
|
/** Open "Move to" menu (kept so it can be torn down). */
|
|
20607
21157
|
this.moveMenu = null;
|
|
20608
21158
|
this.moveTargets = [];
|
|
@@ -20760,6 +21310,42 @@ var InputsUI = class {
|
|
|
20760
21310
|
extrasEl.appendChild(btn2);
|
|
20761
21311
|
}
|
|
20762
21312
|
}
|
|
21313
|
+
/**
|
|
21314
|
+
* Wire the host-contributed callout bubbles. Same contract as
|
|
21315
|
+
* {@link setLegendActions}: re-calling replaces the provider and re-projects the
|
|
21316
|
+
* rows already on screen.
|
|
21317
|
+
*/
|
|
21318
|
+
setLegendCallouts(provider) {
|
|
21319
|
+
this.legendCallouts = provider;
|
|
21320
|
+
for (const row of this.rows.values()) this.renderCallouts(row);
|
|
21321
|
+
}
|
|
21322
|
+
/** (Re)build one row's callout bubbles — tinted icon circles beside the title whose
|
|
21323
|
+
* click (when the view carries content) deploys a panel of text and actions. */
|
|
21324
|
+
renderCallouts(row) {
|
|
21325
|
+
this.disposeTips(this.calloutTips, row.id);
|
|
21326
|
+
for (const bubble of row.callouts) bubble.destroy();
|
|
21327
|
+
row.callouts = [];
|
|
21328
|
+
row.calloutsEl.replaceChildren();
|
|
21329
|
+
const views = this.legendCallouts?.(row.id) ?? [];
|
|
21330
|
+
row.calloutsEl.style.display = legendCalloutsDisplay(row.highlighted, views.length > 0);
|
|
21331
|
+
for (const view of views) {
|
|
21332
|
+
const bubble = new CalloutBubble({
|
|
21333
|
+
icon: view.icon,
|
|
21334
|
+
background: view.background,
|
|
21335
|
+
...view.color !== void 0 ? { color: view.color } : {},
|
|
21336
|
+
label: view.tooltip,
|
|
21337
|
+
...view.content !== void 0 ? { panel: view.content } : {},
|
|
21338
|
+
// The panel portals into the plot host and self-tokens: it must work on
|
|
21339
|
+
// a bare chart, where no `.vela-ui` token ancestor exists.
|
|
21340
|
+
host: this.container,
|
|
21341
|
+
theme: () => this.theme
|
|
21342
|
+
});
|
|
21343
|
+
bubble.el.dataset.legendCallout = view.id;
|
|
21344
|
+
this.tip(this.calloutTips, row.id, bubble.el, view.tooltip);
|
|
21345
|
+
row.callouts.push(bubble);
|
|
21346
|
+
row.calloutsEl.appendChild(bubble.el);
|
|
21347
|
+
}
|
|
21348
|
+
}
|
|
20763
21349
|
/** Reposition the per-pane legend containers after a layout change. */
|
|
20764
21350
|
reposition() {
|
|
20765
21351
|
for (const [paneId, lg] of this.legends) this.positionLegend(lg, paneId);
|
|
@@ -20927,6 +21513,7 @@ var InputsUI = class {
|
|
|
20927
21513
|
}
|
|
20928
21514
|
positionLegend(lg, paneId) {
|
|
20929
21515
|
const bounds = this.paneBoundsOf ? this.paneBoundsOf(paneId) : { top: 0, height: Infinity };
|
|
21516
|
+
lg.toggleAttribute(LEGEND_AT_TOP_ATTR, bounds.top === 0);
|
|
20930
21517
|
lg.style.display = bounds.height < 4 || !this.titlesVisible ? "none" : "flex";
|
|
20931
21518
|
const collapsed = this.paneCollapse.has(paneId);
|
|
20932
21519
|
const masterId = this.paneCollapse.get(paneId) ?? null;
|
|
@@ -21002,13 +21589,13 @@ var InputsUI = class {
|
|
|
21002
21589
|
}
|
|
21003
21590
|
/** Create or update an indicator's legend row (in the legend for its pane). */
|
|
21004
21591
|
upsert(id, title, inputs, values, paneId = "price", opts = {}) {
|
|
21005
|
-
const settingsTitle = opts.settingsTitle ?? title;
|
|
21006
21592
|
const existing = this.rows.get(id);
|
|
21007
21593
|
if (existing) {
|
|
21008
21594
|
existing.title = title;
|
|
21009
|
-
existing.settingsTitle = settingsTitle;
|
|
21010
21595
|
existing.inputs = inputs;
|
|
21011
21596
|
existing.values = { ...values };
|
|
21597
|
+
existing.props = opts.props ?? [];
|
|
21598
|
+
existing.propValues = { ...opts.propValues ?? {} };
|
|
21012
21599
|
existing.titleEl.textContent = title;
|
|
21013
21600
|
if (existing.paneId !== paneId) {
|
|
21014
21601
|
existing.paneId = paneId;
|
|
@@ -21060,6 +21647,9 @@ var InputsUI = class {
|
|
|
21060
21647
|
titleWrap.appendChild(beta);
|
|
21061
21648
|
}
|
|
21062
21649
|
el.appendChild(titleWrap);
|
|
21650
|
+
const calloutsEl = document.createElement("span");
|
|
21651
|
+
calloutsEl.style.cssText = `display:none;align-items:center;gap:4px;flex:none;margin-left:${LEGEND_TITLE_STATUS_GAP_PX}px;`;
|
|
21652
|
+
el.appendChild(calloutsEl);
|
|
21063
21653
|
el.appendChild(statusEl);
|
|
21064
21654
|
const valuesEl = document.createElement("span");
|
|
21065
21655
|
valuesEl.style.cssText = `display:none;align-items:center;gap:5px;margin-left:${LEGEND_TITLE_VALUES_GAP_PX}px;white-space:nowrap;font-variant-numeric:tabular-nums;`;
|
|
@@ -21076,13 +21666,13 @@ var InputsUI = class {
|
|
|
21076
21666
|
eye.className = "vela-ind-ctl";
|
|
21077
21667
|
eye.style.cssText = LEGEND_CTL_CSS;
|
|
21078
21668
|
eye.addEventListener("click", () => {
|
|
21079
|
-
const
|
|
21080
|
-
this.onToggleVisible?.(id, Boolean(
|
|
21669
|
+
const row2 = this.rows.get(id);
|
|
21670
|
+
this.onToggleVisible?.(id, Boolean(row2?.hidden));
|
|
21081
21671
|
});
|
|
21082
21672
|
controlsEl.appendChild(eye);
|
|
21083
21673
|
eyeEl = eye;
|
|
21084
21674
|
}
|
|
21085
|
-
if (inputs.length > 0) {
|
|
21675
|
+
if (inputs.length > 0 || (opts.props ?? []).length > 0) {
|
|
21086
21676
|
const gear = document.createElement("button");
|
|
21087
21677
|
gear.type = "button";
|
|
21088
21678
|
gear.setAttribute("aria-label", "Settings");
|
|
@@ -21122,7 +21712,9 @@ var InputsUI = class {
|
|
|
21122
21712
|
controlsEl.appendChild(close);
|
|
21123
21713
|
el.appendChild(controlsEl);
|
|
21124
21714
|
this.attach(this.legendFor(paneId), el, !!opts.native);
|
|
21125
|
-
|
|
21715
|
+
const row = { id, title, inputs, values: { ...values }, props: opts.props ?? [], propValues: { ...opts.propValues ?? {} }, el, titleEl, statusEl, valuesEl, plotValues: [], plotValuesKey: "", showValues: null, highlighted: false, paneId, hidden: false, eyeEl, controlsEl, extrasEl, calloutsEl, callouts: [], native: !!opts.native };
|
|
21716
|
+
this.rows.set(id, row);
|
|
21717
|
+
this.renderCallouts(row);
|
|
21126
21718
|
this.syncFoldToggle();
|
|
21127
21719
|
}
|
|
21128
21720
|
/** Place a row in its pane's legend — native rows PREPEND (pinned to the top), Pine rows append. */
|
|
@@ -21131,9 +21723,11 @@ var InputsUI = class {
|
|
|
21131
21723
|
else container.appendChild(el);
|
|
21132
21724
|
}
|
|
21133
21725
|
/** Reflect programmatic input changes (so a re-opened dialog shows current values). */
|
|
21134
|
-
setValues(id, values) {
|
|
21726
|
+
setValues(id, values, props) {
|
|
21135
21727
|
const row = this.rows.get(id);
|
|
21136
|
-
if (row)
|
|
21728
|
+
if (!row) return;
|
|
21729
|
+
row.values = { ...row.values, ...values };
|
|
21730
|
+
if (props) row.propValues = { ...row.propValues, ...props };
|
|
21137
21731
|
}
|
|
21138
21732
|
/**
|
|
21139
21733
|
* Reflect an indicator's live status in its legend row: `'loading'` shows three pulsing
|
|
@@ -21215,8 +21809,13 @@ var InputsUI = class {
|
|
|
21215
21809
|
syncRowActions(row) {
|
|
21216
21810
|
const open2 = row.highlighted;
|
|
21217
21811
|
row.controlsEl.style.display = open2 || row.hidden ? "inline-flex" : "none";
|
|
21218
|
-
if (open2)
|
|
21219
|
-
|
|
21812
|
+
if (open2) {
|
|
21813
|
+
row.el.appendChild(row.statusEl);
|
|
21814
|
+
for (const bubble of row.callouts) bubble.hidePanel();
|
|
21815
|
+
} else {
|
|
21816
|
+
row.el.insertBefore(row.statusEl, row.valuesEl);
|
|
21817
|
+
}
|
|
21818
|
+
row.calloutsEl.style.display = legendCalloutsDisplay(open2, row.callouts.length > 0);
|
|
21220
21819
|
for (const child of Array.from(row.controlsEl.children)) {
|
|
21221
21820
|
if (!(child instanceof HTMLElement) || child === row.eyeEl) continue;
|
|
21222
21821
|
if (child === row.extrasEl) {
|
|
@@ -21234,10 +21833,12 @@ var InputsUI = class {
|
|
|
21234
21833
|
}
|
|
21235
21834
|
remove(id) {
|
|
21236
21835
|
const row = this.rows.get(id);
|
|
21836
|
+
for (const bubble of row?.callouts ?? []) bubble.destroy();
|
|
21237
21837
|
row?.el.remove();
|
|
21238
21838
|
this.rows.delete(id);
|
|
21239
21839
|
this.disposeTips(this.rowTips, id);
|
|
21240
21840
|
this.disposeTips(this.extrasTips, id);
|
|
21841
|
+
this.disposeTips(this.calloutTips, id);
|
|
21241
21842
|
if (this.selectedId === id) this.selectedId = null;
|
|
21242
21843
|
if (this.inputsDialog.openId === id) this.inputsDialog.close();
|
|
21243
21844
|
this.syncFoldToggle();
|
|
@@ -21257,6 +21858,8 @@ var InputsUI = class {
|
|
|
21257
21858
|
this.rowMenu = null;
|
|
21258
21859
|
for (const id of [...this.rowTips.keys()]) this.disposeTips(this.rowTips, id);
|
|
21259
21860
|
for (const id of [...this.extrasTips.keys()]) this.disposeTips(this.extrasTips, id);
|
|
21861
|
+
for (const id of [...this.calloutTips.keys()]) this.disposeTips(this.calloutTips, id);
|
|
21862
|
+
for (const row of this.rows.values()) for (const bubble of row.callouts) bubble.destroy();
|
|
21260
21863
|
for (const lg of this.legends.values()) lg.remove();
|
|
21261
21864
|
this.legends.clear();
|
|
21262
21865
|
this.rows.clear();
|
|
@@ -21302,7 +21905,7 @@ var ICONS = {
|
|
|
21302
21905
|
};
|
|
21303
21906
|
var STYLE_ID21 = "vela-pane-controls";
|
|
21304
21907
|
var ICON_PX = 12;
|
|
21305
|
-
var CLUSTER_PILL = "rgba(0,0,0,0.
|
|
21908
|
+
var CLUSTER_PILL = "rgba(0,0,0,0.65)";
|
|
21306
21909
|
function ensureStyles3() {
|
|
21307
21910
|
if (typeof document === "undefined" || document.getElementById(STYLE_ID21)) return;
|
|
21308
21911
|
const st = document.createElement("style");
|
|
@@ -21322,8 +21925,12 @@ var PaneControls = class {
|
|
|
21322
21925
|
this.deps = deps;
|
|
21323
21926
|
this.clusters = /* @__PURE__ */ new Map();
|
|
21324
21927
|
this.hoverPaneId = null;
|
|
21928
|
+
/** Mobile: hover clusters are meaningless without a cursor — suppressed; a
|
|
21929
|
+
* collapsed pane's standalone expand chip stays (the only way back up). */
|
|
21930
|
+
this.suspended = false;
|
|
21325
21931
|
/** Reveal the cluster for the pane under the cursor, resolved from the pointer's y in the plot. */
|
|
21326
21932
|
this.onPlotMove = (e) => {
|
|
21933
|
+
if (this.suspended) return;
|
|
21327
21934
|
const rect = this.plot.getBoundingClientRect();
|
|
21328
21935
|
const y = e.clientY - rect.top;
|
|
21329
21936
|
let hit = null;
|
|
@@ -21395,7 +22002,12 @@ var PaneControls = class {
|
|
|
21395
22002
|
}
|
|
21396
22003
|
if (p.count > 1) {
|
|
21397
22004
|
cluster.appendChild(
|
|
21398
|
-
this.button(p.maximized ? ICONS.restore : ICONS.maximize, p.maximized ? "Restore pane" : "Maximize pane", false, () => this.deps.onToggleMaximize(p.id), {
|
|
22005
|
+
this.button(p.maximized ? ICONS.restore : ICONS.maximize, p.maximized ? "Restore pane" : "Maximize pane", false, () => this.deps.onToggleMaximize(p.id), {
|
|
22006
|
+
role: "maximize",
|
|
22007
|
+
// Same inverse-chip treatment as the collapsed pane's expand toggle: the
|
|
22008
|
+
// maximized state must read as an active state, not just a swapped glyph.
|
|
22009
|
+
selected: p.maximized
|
|
22010
|
+
})
|
|
21399
22011
|
);
|
|
21400
22012
|
}
|
|
21401
22013
|
}
|
|
@@ -21432,17 +22044,18 @@ var PaneControls = class {
|
|
|
21432
22044
|
}
|
|
21433
22045
|
const hovered = id === this.hoverPaneId;
|
|
21434
22046
|
const hasButtons = cluster.children.length > 0;
|
|
21435
|
-
const
|
|
22047
|
+
const stateChipRole = p.collapsed ? "collapse" : !this.suspended && p.maximized ? "maximize" : null;
|
|
22048
|
+
const visible = hasButtons && (hovered || stateChipRole != null) && p.height > 8;
|
|
21436
22049
|
cluster.style.right = `${rightPx}px`;
|
|
21437
22050
|
cluster.style.top = p.collapsed ? `${p.top + Math.max(1, Math.round((p.height - 24) / 2))}px` : `${p.top + 4}px`;
|
|
21438
22051
|
cluster.style.display = visible ? "flex" : "none";
|
|
21439
22052
|
if (!visible) continue;
|
|
21440
|
-
const
|
|
21441
|
-
cluster.style.background =
|
|
22053
|
+
const soloChip = stateChipRole != null && !hovered;
|
|
22054
|
+
cluster.style.background = soloChip ? "transparent" : CLUSTER_PILL;
|
|
21442
22055
|
for (const child of cluster.children) {
|
|
21443
22056
|
const btn2 = child;
|
|
21444
22057
|
btn2.style.display = "inline-flex";
|
|
21445
|
-
btn2.style.visibility =
|
|
22058
|
+
btn2.style.visibility = soloChip && btn2.dataset.role !== stateChipRole ? "hidden" : "visible";
|
|
21446
22059
|
}
|
|
21447
22060
|
}
|
|
21448
22061
|
}
|
|
@@ -21452,6 +22065,14 @@ var PaneControls = class {
|
|
|
21452
22065
|
this.hoverPaneId = paneId;
|
|
21453
22066
|
this.reposition();
|
|
21454
22067
|
}
|
|
22068
|
+
/** Mobile suppression: no hover clusters (touch has no cursor; the shell's own
|
|
22069
|
+
* chrome covers maximize), while collapsed panes keep their expand chips. */
|
|
22070
|
+
setSuspended(on) {
|
|
22071
|
+
if (on === this.suspended) return;
|
|
22072
|
+
this.suspended = on;
|
|
22073
|
+
if (on) this.hoverPaneId = null;
|
|
22074
|
+
this.reposition();
|
|
22075
|
+
}
|
|
21455
22076
|
destroy() {
|
|
21456
22077
|
this.plot.removeEventListener("pointermove", this.onPlotMove);
|
|
21457
22078
|
this.plot.removeEventListener("pointerleave", this.onPlotLeave);
|
|
@@ -21602,131 +22223,6 @@ var AxisScaleButtons = class {
|
|
|
21602
22223
|
}
|
|
21603
22224
|
};
|
|
21604
22225
|
|
|
21605
|
-
// src/renderers/shared/TableOverlay.ts
|
|
21606
|
-
var SIZE_PX3 = {
|
|
21607
|
-
auto: 13,
|
|
21608
|
-
tiny: 10,
|
|
21609
|
-
small: 11,
|
|
21610
|
-
normal: 13,
|
|
21611
|
-
large: 16,
|
|
21612
|
-
huge: 20
|
|
21613
|
-
};
|
|
21614
|
-
var TableOverlay = class {
|
|
21615
|
-
constructor(container, theme, paneBounds) {
|
|
21616
|
-
this.container = container;
|
|
21617
|
-
this.theme = theme;
|
|
21618
|
-
this.paneBounds = paneBounds;
|
|
21619
|
-
this.lastTables = [];
|
|
21620
|
-
if (getComputedStyle(container).position === "static") container.style.position = "relative";
|
|
21621
|
-
this.root = document.createElement("div");
|
|
21622
|
-
Object.assign(this.root.style, {
|
|
21623
|
-
position: "absolute",
|
|
21624
|
-
inset: "0",
|
|
21625
|
-
pointerEvents: "none",
|
|
21626
|
-
overflow: "hidden",
|
|
21627
|
-
zIndex: "3"
|
|
21628
|
-
});
|
|
21629
|
-
container.appendChild(this.root);
|
|
21630
|
-
}
|
|
21631
|
-
update(tables) {
|
|
21632
|
-
this.lastTables = tables;
|
|
21633
|
-
this.root.replaceChildren();
|
|
21634
|
-
for (const t of tables) this.root.appendChild(this.renderTable(t));
|
|
21635
|
-
}
|
|
21636
|
-
/** Re-render at the current pane geometry — after layout settles or on resize. */
|
|
21637
|
-
reposition() {
|
|
21638
|
-
if (this.root.isConnected) this.update(this.lastTables);
|
|
21639
|
-
}
|
|
21640
|
-
/** Show/hide the whole overlay. Tables anchor to pane corners, not to bars, so unlike the
|
|
21641
|
-
* series content they DON'T vanish with an emptied chart — the loading state hides them. */
|
|
21642
|
-
setVisible(visible) {
|
|
21643
|
-
this.root.style.display = visible ? "" : "none";
|
|
21644
|
-
}
|
|
21645
|
-
destroy() {
|
|
21646
|
-
this.root.remove();
|
|
21647
|
-
}
|
|
21648
|
-
renderTable(t) {
|
|
21649
|
-
const wrap = document.createElement("div");
|
|
21650
|
-
wrap.style.position = "absolute";
|
|
21651
|
-
this.anchor(wrap, t.position, this.paneBounds(t.paneId));
|
|
21652
|
-
const table = document.createElement("table");
|
|
21653
|
-
Object.assign(table.style, {
|
|
21654
|
-
borderCollapse: "collapse",
|
|
21655
|
-
background: t.bgColor ?? "transparent",
|
|
21656
|
-
fontFamily: this.theme.fontFamily || "sans-serif",
|
|
21657
|
-
// Frame as an inset shadow (not a `border`) so it stays independent of the
|
|
21658
|
-
// collapsed cell borders even when frame_width != border_width.
|
|
21659
|
-
boxShadow: t.frameColor && t.frameWidth > 0 ? `inset 0 0 0 ${t.frameWidth}px ${t.frameColor}` : "none",
|
|
21660
|
-
border: "none",
|
|
21661
|
-
tableLayout: "auto",
|
|
21662
|
-
// Re-enable pointer events on the table only (root is none) so cell tooltips work.
|
|
21663
|
-
pointerEvents: "auto"
|
|
21664
|
-
});
|
|
21665
|
-
const span = /* @__PURE__ */ new Map();
|
|
21666
|
-
const skip = /* @__PURE__ */ new Set();
|
|
21667
|
-
for (const m of t.merges) {
|
|
21668
|
-
span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
|
|
21669
|
-
for (let r = m.startRow; r <= m.endRow; r += 1) {
|
|
21670
|
-
for (let c = m.startCol; c <= m.endCol; c += 1) {
|
|
21671
|
-
if (r !== m.startRow || c !== m.startCol) skip.add(`${r}:${c}`);
|
|
21672
|
-
}
|
|
21673
|
-
}
|
|
21674
|
-
}
|
|
21675
|
-
const cellBorder = t.borderColor && t.borderWidth > 0 ? `${t.borderWidth}px solid ${t.borderColor}` : "none";
|
|
21676
|
-
for (let r = 0; r < t.rows; r += 1) {
|
|
21677
|
-
const tr = document.createElement("tr");
|
|
21678
|
-
for (let c = 0; c < t.columns; c += 1) {
|
|
21679
|
-
const cell = t.cells[r]?.[c] ?? null;
|
|
21680
|
-
if (skip.has(`${r}:${c}`) || cell?.merged) continue;
|
|
21681
|
-
const td = document.createElement("td");
|
|
21682
|
-
const sp = span.get(`${r}:${c}`);
|
|
21683
|
-
if (sp) {
|
|
21684
|
-
if (sp.cs > 1) td.colSpan = sp.cs;
|
|
21685
|
-
if (sp.rs > 1) td.rowSpan = sp.rs;
|
|
21686
|
-
}
|
|
21687
|
-
Object.assign(td.style, {
|
|
21688
|
-
border: cellBorder,
|
|
21689
|
-
padding: "2px 6px",
|
|
21690
|
-
background: cell?.bgColor ?? "transparent",
|
|
21691
|
-
color: cell?.textColor ?? this.theme.textColor,
|
|
21692
|
-
textAlign: cell?.hAlign ?? "center",
|
|
21693
|
-
verticalAlign: cell?.vAlign === "top" ? "top" : cell?.vAlign === "bottom" ? "bottom" : "middle",
|
|
21694
|
-
fontSize: `${SIZE_PX3[cell?.textSize ?? "normal"]}px`,
|
|
21695
|
-
fontFamily: cell?.fontFamily === "monospace" ? "monospace" : "inherit",
|
|
21696
|
-
fontWeight: cell?.bold ? "bold" : "normal",
|
|
21697
|
-
fontStyle: cell?.italic ? "italic" : "normal",
|
|
21698
|
-
whiteSpace: "pre-line"
|
|
21699
|
-
});
|
|
21700
|
-
if (cell?.tooltip) td.title = cell.tooltip;
|
|
21701
|
-
td.textContent = cell?.text ?? "";
|
|
21702
|
-
tr.appendChild(td);
|
|
21703
|
-
}
|
|
21704
|
-
table.appendChild(tr);
|
|
21705
|
-
}
|
|
21706
|
-
wrap.appendChild(table);
|
|
21707
|
-
return wrap;
|
|
21708
|
-
}
|
|
21709
|
-
/**
|
|
21710
|
-
* Position the wrapper at a Pine `position.*` corner/edge of the table's PANE
|
|
21711
|
-
* (not the whole chart), inset past the right price axis so it never overlaps
|
|
21712
|
-
* the Y-axis labels.
|
|
21713
|
-
*/
|
|
21714
|
-
anchor(el, position, b) {
|
|
21715
|
-
const m = 6;
|
|
21716
|
-
const containerH = this.root.clientHeight || this.container.clientHeight;
|
|
21717
|
-
const paneBottom = b.top + b.height;
|
|
21718
|
-
if (position.startsWith("top")) el.style.top = `${b.top + m}px`;
|
|
21719
|
-
else if (position.startsWith("bottom")) el.style.bottom = `${Math.max(0, containerH - paneBottom) + m}px`;
|
|
21720
|
-
else el.style.top = `${b.top + b.height / 2}px`;
|
|
21721
|
-
if (position.endsWith("left")) el.style.left = `${m}px`;
|
|
21722
|
-
else if (position.endsWith("right")) el.style.right = `${b.rightAxis + m}px`;
|
|
21723
|
-
else el.style.left = `calc(50% - ${b.rightAxis / 2}px)`;
|
|
21724
|
-
const tx = position.endsWith("center") ? "-50%" : "0";
|
|
21725
|
-
const ty = position.startsWith("middle") ? "-50%" : "0";
|
|
21726
|
-
if (tx !== "0" || ty !== "0") el.style.transform = `translate(${tx}, ${ty})`;
|
|
21727
|
-
}
|
|
21728
|
-
};
|
|
21729
|
-
|
|
21730
22226
|
// src/renderers/native/capabilities.ts
|
|
21731
22227
|
var NATIVE_CAPABILITIES = {
|
|
21732
22228
|
panes: true,
|
|
@@ -21747,7 +22243,7 @@ var NATIVE_CAPABILITIES = {
|
|
|
21747
22243
|
drawingDepth: true,
|
|
21748
22244
|
// drawings share the series' z space (backend-composited interleave layers)
|
|
21749
22245
|
tables: true,
|
|
21750
|
-
//
|
|
22246
|
+
// canvas-painted into the owning indicator's interleave slice
|
|
21751
22247
|
trades: true,
|
|
21752
22248
|
// strategy order-fill markers (arrows + labels + fill-price ticks)
|
|
21753
22249
|
inputsUI: true
|
|
@@ -21778,6 +22274,9 @@ function candleTier(spacing) {
|
|
|
21778
22274
|
if (spacing < CANDLE_BODY_MIN_SPACING) return "wick";
|
|
21779
22275
|
return "full";
|
|
21780
22276
|
}
|
|
22277
|
+
function snapY(yCss, dpr) {
|
|
22278
|
+
return Math.round(yCss * dpr) / dpr;
|
|
22279
|
+
}
|
|
21781
22280
|
function candleGeometry(xCss, spacing, dpr, bodyScale = 1) {
|
|
21782
22281
|
const wickDev = Math.max(1, Math.round(wickWidth(spacing) * dpr));
|
|
21783
22282
|
const wickLeftDev = Math.round(xCss * dpr - wickDev / 2);
|
|
@@ -22300,8 +22799,12 @@ var WebGL2Backend = class {
|
|
|
22300
22799
|
return sc === pane.scale ? pane : { ...pane, scale: sc };
|
|
22301
22800
|
};
|
|
22302
22801
|
b.alpha = this.modelAlpha;
|
|
22303
|
-
for (const m of models) for (const bgSpan of m.backgrounds) this.emitBackground(b, bgSpan, pane, coords);
|
|
22304
|
-
|
|
22802
|
+
for (const m of models) for (const bgSpan of m.backgrounds) if (bgSpan.overlay !== true) this.emitBackground(b, bgSpan, pane, coords);
|
|
22803
|
+
if (isPrice) {
|
|
22804
|
+
for (const m of scene.indicators.values()) {
|
|
22805
|
+
for (const bgSpan of m.backgrounds) if (bgSpan.overlay === true) this.emitBackground(b, bgSpan, pane, coords);
|
|
22806
|
+
}
|
|
22807
|
+
}
|
|
22305
22808
|
const drawCandles = isPrice && !scene.candlesHidden;
|
|
22306
22809
|
let candleDrawn = false;
|
|
22307
22810
|
for (const m of models) {
|
|
@@ -22315,15 +22818,26 @@ var WebGL2Backend = class {
|
|
|
22315
22818
|
b.alpha = this.modelAlpha;
|
|
22316
22819
|
const off = scene.offsetOf(m.id);
|
|
22317
22820
|
const mp = effPane(m);
|
|
22318
|
-
for (const
|
|
22821
|
+
for (const f of m.fills) if (f.overlay !== true) this.emitFill(b, m, f, mp, coords, i0, i1, off);
|
|
22822
|
+
for (const s of m.series) if (s.overlay !== true) this.emitSeries(b, s, mp, coords, i0, i1, theme, off);
|
|
22319
22823
|
}
|
|
22320
22824
|
if (drawCandles && !candleDrawn) {
|
|
22321
22825
|
drawSlicesUpTo(scene.candleZ);
|
|
22322
22826
|
b.alpha = this.candleStructureAlpha;
|
|
22323
22827
|
this.emitPriceSeries(b, scene, i0, i1, coords, pane, theme, barColorMap, dataW);
|
|
22324
22828
|
}
|
|
22325
|
-
drawSlicesUpTo(Infinity);
|
|
22326
22829
|
b.alpha = this.modelAlpha;
|
|
22830
|
+
if (isPrice) {
|
|
22831
|
+
for (const m of scene.indicators.values()) {
|
|
22832
|
+
const off = scene.offsetOf(m.id);
|
|
22833
|
+
for (const f of m.fills) if (f.overlay === true) this.emitFill(b, m, f, pane, coords, i0, i1, off);
|
|
22834
|
+
}
|
|
22835
|
+
for (const m of scene.indicators.values()) {
|
|
22836
|
+
const off = scene.offsetOf(m.id);
|
|
22837
|
+
for (const s of m.series) if (s.overlay === true) this.emitSeries(b, s, pane, coords, i0, i1, theme, off);
|
|
22838
|
+
}
|
|
22839
|
+
}
|
|
22840
|
+
drawSlicesUpTo(Infinity);
|
|
22327
22841
|
for (const m of models) {
|
|
22328
22842
|
const mp = effPane(m);
|
|
22329
22843
|
for (const pl of m.priceLines) this.emitHline(b, pl, mp, coords, dataW, theme);
|
|
@@ -22370,7 +22884,7 @@ var WebGL2Backend = class {
|
|
|
22370
22884
|
for (const pane of scene.orderedPanes()) {
|
|
22371
22885
|
const b = this.glowBatch;
|
|
22372
22886
|
b.reset();
|
|
22373
|
-
this.emitGlowSources(b, scene
|
|
22887
|
+
this.emitGlowSources(b, scene, pane, coords, i0, i1);
|
|
22374
22888
|
if (b.vertexCount === 0) continue;
|
|
22375
22889
|
const topH = Math.round(pane.bounds.top * dpr * 0.5);
|
|
22376
22890
|
const botH = Math.round((pane.bounds.top + pane.bounds.height) * dpr * 0.5);
|
|
@@ -22439,15 +22953,23 @@ var WebGL2Backend = class {
|
|
|
22439
22953
|
gl.bindVertexArray(null);
|
|
22440
22954
|
return true;
|
|
22441
22955
|
}
|
|
22442
|
-
/** The "neon" elements that glow: line/area/step lines + point markers (not candles/fills/bars).
|
|
22443
|
-
|
|
22444
|
-
|
|
22445
|
-
|
|
22446
|
-
|
|
22447
|
-
|
|
22448
|
-
|
|
22449
|
-
|
|
22450
|
-
|
|
22956
|
+
/** The "neon" elements that glow: line/area/step lines + point markers (not candles/fills/bars).
|
|
22957
|
+
* Routing mirrors the main pass: own series per pane, force_overlay series on the price pane. */
|
|
22958
|
+
emitGlowSources(b, scene, pane, coords, i0, i1) {
|
|
22959
|
+
const emitOne = (s, off) => {
|
|
22960
|
+
if (!isLineLikeSeries(s) || s.visible === false) return;
|
|
22961
|
+
if (s.kind === "histogram" || s.kind === "columns") return;
|
|
22962
|
+
if (s.kind === "circles" || s.kind === "cross") this.emitPointMarkers(b, s, pane, coords, i0, i1, off);
|
|
22963
|
+
else this.emitPolyline(b, s, pane, coords, i0, i1, s.kind === "step", off);
|
|
22964
|
+
};
|
|
22965
|
+
for (const m of scene.indicatorsForPane(pane.id)) {
|
|
22966
|
+
const off = scene.offsetOf(m.id);
|
|
22967
|
+
for (const s of m.series) if (s.overlay !== true) emitOne(s, off);
|
|
22968
|
+
}
|
|
22969
|
+
if (pane.kind === "price") {
|
|
22970
|
+
for (const m of scene.indicators.values()) {
|
|
22971
|
+
const off = scene.offsetOf(m.id);
|
|
22972
|
+
for (const s of m.series) if (s.overlay === true) emitOne(s, off);
|
|
22451
22973
|
}
|
|
22452
22974
|
}
|
|
22453
22975
|
}
|
|
@@ -22710,12 +23232,12 @@ var WebGL2Backend = class {
|
|
|
22710
23232
|
if (drawBody) {
|
|
22711
23233
|
const oY = coords.priceToY(bar.open, pane.scale, pane.bounds);
|
|
22712
23234
|
const cY = coords.priceToY(bar.close, pane.scale, pane.bounds);
|
|
22713
|
-
bodyTop = Math.min(oY, cY);
|
|
22714
|
-
bodyH = Math.max(1, Math.
|
|
23235
|
+
bodyTop = snapY(Math.min(oY, cY), coords.dpr);
|
|
23236
|
+
bodyH = Math.max(1 / coords.dpr, snapY(Math.max(oY, cY), coords.dpr) - bodyTop);
|
|
22715
23237
|
}
|
|
22716
23238
|
if (cs.wickVisible) {
|
|
22717
|
-
const hY = coords.priceToY(bar.high, pane.scale, pane.bounds);
|
|
22718
|
-
const lY = coords.priceToY(bar.low, pane.scale, pane.bounds);
|
|
23239
|
+
const hY = snapY(coords.priceToY(bar.high, pane.scale, pane.bounds), coords.dpr);
|
|
23240
|
+
const lY = snapY(coords.priceToY(bar.low, pane.scale, pane.bounds), coords.dpr);
|
|
22719
23241
|
b.alpha = this.candleStructureAlpha;
|
|
22720
23242
|
const wCol = parseColor((isUp ? cs.wickUpColor : cs.wickDownColor) ?? (drawBody ? dir : bodyColorStr));
|
|
22721
23243
|
if (drawBody) {
|
|
@@ -22730,9 +23252,9 @@ var WebGL2Backend = class {
|
|
|
22730
23252
|
b.alpha = this.candleBodyAlpha;
|
|
22731
23253
|
b.rect(g.bodyX, bodyTop, g.bodyW, bodyH, c);
|
|
22732
23254
|
}
|
|
22733
|
-
if (cs.borderVisible ||
|
|
23255
|
+
if (cs.borderVisible || fading && cs.bodyVisible) {
|
|
22734
23256
|
b.alpha = this.candleStructureAlpha;
|
|
22735
|
-
const bord = cs.borderVisible
|
|
23257
|
+
const bord = cs.borderVisible ? parseColor((isUp ? cs.borderUpColor : cs.borderDownColor) ?? bodyColorStr) : c;
|
|
22736
23258
|
const bw = Math.max(0, g.bodyW - 1);
|
|
22737
23259
|
const bh = Math.max(0, bodyH - 1);
|
|
22738
23260
|
b.rectStroke(g.bodyX + 0.5, bodyTop + 0.5, bw, bh, 1, bord);
|
|
@@ -24048,9 +24570,10 @@ var SceneGraph = class {
|
|
|
24048
24570
|
* so each indicator arrives behind the candles (and behind older indicators);
|
|
24049
24571
|
* `setIndicatorZ`/`bringToFront`/`sendToBack` change it. */
|
|
24050
24572
|
this.seriesZ = /* @__PURE__ */ new Map();
|
|
24051
|
-
/** Per-pane raster layers of
|
|
24573
|
+
/** Per-pane raster layers of drawings interleaved into the series stack (each
|
|
24574
|
+
* indicator's Pine drawings at its model's z, plus in-stack user drawings) — each a
|
|
24052
24575
|
* prepainted canvas the backend composites just before the series carrying `beforeZ`.
|
|
24053
|
-
* Rebuilt by the renderer per data frame
|
|
24576
|
+
* Rebuilt by the renderer per data frame. */
|
|
24054
24577
|
this.drawingSlices = /* @__PURE__ */ new Map();
|
|
24055
24578
|
/** Per-model index offset: the chart bar index of the model's `anchorTime` — its
|
|
24056
24579
|
* index-aligned payloads (dense series arrays, `bar_index` drawings) count from that
|
|
@@ -24284,8 +24807,12 @@ var Canvas2dBackend = class {
|
|
|
24284
24807
|
return sc === pane.scale ? pane : { ...pane, scale: sc };
|
|
24285
24808
|
};
|
|
24286
24809
|
ctx.globalAlpha = this.modelAlpha;
|
|
24287
|
-
for (const m of models) for (const bg of m.backgrounds) this.drawBackground(ctx, bg, pane, coords);
|
|
24288
|
-
|
|
24810
|
+
for (const m of models) for (const bg of m.backgrounds) if (bg.overlay !== true) this.drawBackground(ctx, bg, pane, coords);
|
|
24811
|
+
if (isPrice) {
|
|
24812
|
+
for (const m of scene.indicators.values()) {
|
|
24813
|
+
for (const bg of m.backgrounds) if (bg.overlay === true) this.drawBackground(ctx, bg, pane, coords);
|
|
24814
|
+
}
|
|
24815
|
+
}
|
|
24289
24816
|
const slices = scene.drawingSlices.get(pane.id) ?? [];
|
|
24290
24817
|
let si = 0;
|
|
24291
24818
|
const drawSlicesUpTo = (z) => {
|
|
@@ -24307,13 +24834,25 @@ var Canvas2dBackend = class {
|
|
|
24307
24834
|
ctx.globalAlpha = this.modelAlpha;
|
|
24308
24835
|
const off = scene.offsetOf(m.id);
|
|
24309
24836
|
const mp = effPane(m);
|
|
24310
|
-
for (const
|
|
24837
|
+
for (const f of m.fills) if (f.overlay !== true) this.drawFill(ctx, m, f, mp, coords, i0, i1, off);
|
|
24838
|
+
for (const s of m.series) if (s.overlay !== true) this.drawSeries(ctx, s, mp, coords, i0, i1, theme, off);
|
|
24311
24839
|
}
|
|
24312
24840
|
if (drawCandles && !candleDrawn) {
|
|
24313
24841
|
drawSlicesUpTo(scene.candleZ);
|
|
24314
24842
|
ctx.globalAlpha = this.candleStructureAlpha;
|
|
24315
24843
|
this.drawPriceSeries(ctx, scene, i0, i1, coords, pane, theme, barColorMap, dataW);
|
|
24316
24844
|
}
|
|
24845
|
+
if (isPrice) {
|
|
24846
|
+
ctx.globalAlpha = this.modelAlpha;
|
|
24847
|
+
for (const m of scene.indicators.values()) {
|
|
24848
|
+
const off = scene.offsetOf(m.id);
|
|
24849
|
+
for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0, i1, off);
|
|
24850
|
+
}
|
|
24851
|
+
for (const m of scene.indicators.values()) {
|
|
24852
|
+
const off = scene.offsetOf(m.id);
|
|
24853
|
+
for (const s of m.series) if (s.overlay === true) this.drawSeries(ctx, s, pane, coords, i0, i1, theme, off);
|
|
24854
|
+
}
|
|
24855
|
+
}
|
|
24317
24856
|
drawSlicesUpTo(Infinity);
|
|
24318
24857
|
ctx.globalAlpha = this.modelAlpha;
|
|
24319
24858
|
for (const m of models) {
|
|
@@ -24558,13 +25097,13 @@ var Canvas2dBackend = class {
|
|
|
24558
25097
|
if (drawBody) {
|
|
24559
25098
|
const oY = coords.priceToY(b.open, pane.scale, pane.bounds);
|
|
24560
25099
|
const cY = coords.priceToY(b.close, pane.scale, pane.bounds);
|
|
24561
|
-
top = Math.min(oY, cY);
|
|
24562
|
-
bodyH = Math.max(1, Math.
|
|
25100
|
+
top = snapY(Math.min(oY, cY), coords.dpr);
|
|
25101
|
+
bodyH = Math.max(1 / coords.dpr, snapY(Math.max(oY, cY), coords.dpr) - top);
|
|
24563
25102
|
}
|
|
24564
25103
|
if (cs.wickVisible) {
|
|
24565
25104
|
const wick = (up ? cs.wickUpColor : cs.wickDownColor) ?? (drawBody ? dir : color);
|
|
24566
|
-
const hY = coords.priceToY(b.high, pane.scale, pane.bounds);
|
|
24567
|
-
const lY = coords.priceToY(b.low, pane.scale, pane.bounds);
|
|
25105
|
+
const hY = snapY(coords.priceToY(b.high, pane.scale, pane.bounds), coords.dpr);
|
|
25106
|
+
const lY = snapY(coords.priceToY(b.low, pane.scale, pane.bounds), coords.dpr);
|
|
24568
25107
|
ctx.globalAlpha = this.candleStructureAlpha;
|
|
24569
25108
|
ctx.strokeStyle = wick;
|
|
24570
25109
|
ctx.lineWidth = g.wickW;
|
|
@@ -24586,9 +25125,9 @@ var Canvas2dBackend = class {
|
|
|
24586
25125
|
ctx.fillStyle = color;
|
|
24587
25126
|
ctx.fillRect(g.bodyX, top, g.bodyW, bodyH);
|
|
24588
25127
|
}
|
|
24589
|
-
if (cs.borderVisible ||
|
|
25128
|
+
if (cs.borderVisible || fading && cs.bodyVisible) {
|
|
24590
25129
|
ctx.globalAlpha = this.candleStructureAlpha;
|
|
24591
|
-
ctx.strokeStyle = cs.borderVisible
|
|
25130
|
+
ctx.strokeStyle = cs.borderVisible ? (up ? cs.borderUpColor : cs.borderDownColor) ?? color : color;
|
|
24592
25131
|
ctx.lineWidth = 1;
|
|
24593
25132
|
const bw = Math.max(0, g.bodyW - 1);
|
|
24594
25133
|
const bh = Math.max(0, bodyH - 1);
|
|
@@ -25011,6 +25550,19 @@ function autoFontSize(lines, boxW, boxH, bold) {
|
|
|
25011
25550
|
|
|
25012
25551
|
// src/renderers/shared/DrawingSceneRenderer.ts
|
|
25013
25552
|
var EMPTY_DRAWING_SET = { lines: [], boxes: [], labels: [], polylines: [], linefills: [] };
|
|
25553
|
+
function modelDrawingSet(m, overlay) {
|
|
25554
|
+
const want = (d) => Boolean(d.overlay) === overlay;
|
|
25555
|
+
return {
|
|
25556
|
+
lines: (m.lines ?? []).filter(want),
|
|
25557
|
+
boxes: (m.boxes ?? []).filter(want),
|
|
25558
|
+
labels: (m.labels ?? []).filter(want),
|
|
25559
|
+
polylines: (m.polylines ?? []).filter(want),
|
|
25560
|
+
linefills: (m.linefills ?? []).filter(want)
|
|
25561
|
+
};
|
|
25562
|
+
}
|
|
25563
|
+
function drawingSetEmpty(s) {
|
|
25564
|
+
return !s.lines.length && !s.boxes.length && !s.labels.length && !s.polylines.length && !s.linefills.length;
|
|
25565
|
+
}
|
|
25014
25566
|
function fontSizePx(size) {
|
|
25015
25567
|
return size === "auto" ? 12 : namedFontSize(size);
|
|
25016
25568
|
}
|
|
@@ -25020,6 +25572,8 @@ var DrawingSceneRenderer = class {
|
|
|
25020
25572
|
this.set = set;
|
|
25021
25573
|
/** measureText width cache, keyed by `${font} ${text}`, persists across frames. */
|
|
25022
25574
|
this.widthCache = /* @__PURE__ */ new Map();
|
|
25575
|
+
/** Tooltip hit-rects captured while drawing the CURRENT set (rebuilt per render). */
|
|
25576
|
+
this.tipRegions = [];
|
|
25023
25577
|
/**
|
|
25024
25578
|
* Index offset of the CURRENT set's model: its `xloc:'bar_index'` coordinates count
|
|
25025
25579
|
* from the model's anchor bar, so they shift by this to land on chart logical indices.
|
|
@@ -25040,8 +25594,13 @@ var DrawingSceneRenderer = class {
|
|
|
25040
25594
|
const s = this.set;
|
|
25041
25595
|
return !s.lines.length && !s.boxes.length && !s.labels.length && !s.polylines.length && !s.linefills.length;
|
|
25042
25596
|
}
|
|
25597
|
+
/** Tooltip hit-rects of the labels drawn by the LAST `render` call (same coords as `ctx`). */
|
|
25598
|
+
labelTipRegions() {
|
|
25599
|
+
return this.tipRegions;
|
|
25600
|
+
}
|
|
25043
25601
|
/** Draw the whole set into `ctx` using the supplied coordinate closures. */
|
|
25044
25602
|
render(ctx, W, H, xOf, yOf) {
|
|
25603
|
+
this.tipRegions = [];
|
|
25045
25604
|
if (this.isEmpty()) return;
|
|
25046
25605
|
ctx.save();
|
|
25047
25606
|
this.drawLinefills(ctx, W, H, xOf, yOf);
|
|
@@ -25389,13 +25948,35 @@ var DrawingSceneRenderer = class {
|
|
|
25389
25948
|
if (this.isPointShape(lb.style)) {
|
|
25390
25949
|
if (!lb.noFill) this.drawLabelShape(ctx, lb.style, px, py, fontPx, color);
|
|
25391
25950
|
if (lb.text) this.drawLabelText(ctx, lb, px, py + fontPx, fontPx);
|
|
25392
|
-
|
|
25393
|
-
|
|
25951
|
+
if (lb.tooltip) {
|
|
25952
|
+
const r = Math.max(4, fontPx * 0.6) + 3;
|
|
25953
|
+
this.tipRegions.push({ left: px - r, top: py - r, right: px + r, bottom: py + r, text: lb.tooltip });
|
|
25954
|
+
}
|
|
25955
|
+
} else if (lb.style === "none" || lb.style === "text_outline") {
|
|
25956
|
+
if (lb.text) {
|
|
25957
|
+
this.drawLabelText(ctx, lb, px, py, fontPx, lb.style === "text_outline");
|
|
25958
|
+
if (lb.tooltip) this.tipRegions.push(this.textRegion(ctx, lb, px, py, fontPx, lb.tooltip));
|
|
25959
|
+
}
|
|
25394
25960
|
} else {
|
|
25395
|
-
this.drawBubble(ctx, lb, px, py, fontPx, color);
|
|
25961
|
+
const r = this.drawBubble(ctx, lb, px, py, fontPx, color);
|
|
25962
|
+
if (lb.tooltip) this.tipRegions.push({ left: r.x, top: r.y, right: r.x + r.w, bottom: r.y + r.h, text: lb.tooltip });
|
|
25396
25963
|
}
|
|
25397
25964
|
}
|
|
25398
25965
|
}
|
|
25966
|
+
/** Canvas font for a label's text — family + Pine `text_formatting` (bold/italic). */
|
|
25967
|
+
labelFont(lb, fontPx) {
|
|
25968
|
+
const family = lb.fontFamily === "monospace" ? "monospace" : this.deps.theme.fontFamily || "sans-serif";
|
|
25969
|
+
return `${lb.italic ? "italic " : ""}${lb.bold ? "bold " : ""}${fontPx}px ${family}`;
|
|
25970
|
+
}
|
|
25971
|
+
/** Hover rect of a text-only label (style none/text_outline/noFill), centered like drawLabelText. */
|
|
25972
|
+
textRegion(ctx, lb, cx, cy, fontPx, text) {
|
|
25973
|
+
const font = this.labelFont(lb, fontPx);
|
|
25974
|
+
const lines = (lb.text ?? "").split("\n");
|
|
25975
|
+
const w = Math.max(1, ...lines.map((l) => this.measure(ctx, font, l)));
|
|
25976
|
+
const h = fontPx * 1.25 * lines.length;
|
|
25977
|
+
const left = lb.textAlign === "left" ? cx : lb.textAlign === "right" ? cx - w : cx - w / 2;
|
|
25978
|
+
return { left, top: cy - h / 2, right: left + w, bottom: cy + h / 2, text };
|
|
25979
|
+
}
|
|
25399
25980
|
isPointShape(style) {
|
|
25400
25981
|
switch (style) {
|
|
25401
25982
|
case "circle":
|
|
@@ -25414,8 +25995,7 @@ var DrawingSceneRenderer = class {
|
|
|
25414
25995
|
}
|
|
25415
25996
|
}
|
|
25416
25997
|
drawLabelText(ctx, lb, cx, cy, fontPx, outline = false) {
|
|
25417
|
-
|
|
25418
|
-
ctx.font = `${fontPx}px ${family}`;
|
|
25998
|
+
ctx.font = this.labelFont(lb, fontPx);
|
|
25419
25999
|
ctx.textAlign = lb.textAlign === "left" ? "left" : lb.textAlign === "right" ? "right" : "center";
|
|
25420
26000
|
ctx.textBaseline = "middle";
|
|
25421
26001
|
const lines = lb.text.split("\n");
|
|
@@ -25495,9 +26075,9 @@ var DrawingSceneRenderer = class {
|
|
|
25495
26075
|
ctx.fill();
|
|
25496
26076
|
}
|
|
25497
26077
|
}
|
|
26078
|
+
/** Draw a bubble label; returns the bubble body rect (for tooltip hit-testing). */
|
|
25498
26079
|
drawBubble(ctx, lb, px, py, fontPx, color) {
|
|
25499
|
-
|
|
25500
|
-
ctx.font = `${fontPx}px ${family}`;
|
|
26080
|
+
ctx.font = this.labelFont(lb, fontPx);
|
|
25501
26081
|
const lines = (lb.text ?? "").split("\n");
|
|
25502
26082
|
const padX = 6;
|
|
25503
26083
|
const padY = 4;
|
|
@@ -25555,38 +26135,51 @@ var DrawingSceneRenderer = class {
|
|
|
25555
26135
|
by = py - h - ptr;
|
|
25556
26136
|
pointer = "down";
|
|
25557
26137
|
}
|
|
25558
|
-
|
|
25559
|
-
|
|
25560
|
-
|
|
25561
|
-
if (pointer !== "none") {
|
|
25562
|
-
ctx.beginPath();
|
|
25563
|
-
if (pointer === "down") {
|
|
25564
|
-
ctx.moveTo(px - ptr, by + h);
|
|
25565
|
-
ctx.lineTo(px + ptr, by + h);
|
|
25566
|
-
ctx.lineTo(px, py);
|
|
25567
|
-
} else if (pointer === "up") {
|
|
25568
|
-
ctx.moveTo(px - ptr, by);
|
|
25569
|
-
ctx.lineTo(px + ptr, by);
|
|
25570
|
-
ctx.lineTo(px, py);
|
|
25571
|
-
} else if (pointer === "left") {
|
|
25572
|
-
ctx.moveTo(bx, py - ptr);
|
|
25573
|
-
ctx.lineTo(bx, py + ptr);
|
|
25574
|
-
ctx.lineTo(px, py);
|
|
25575
|
-
} else {
|
|
25576
|
-
ctx.moveTo(bx + w, py - ptr);
|
|
25577
|
-
ctx.lineTo(bx + w, py + ptr);
|
|
25578
|
-
ctx.lineTo(px, py);
|
|
25579
|
-
}
|
|
25580
|
-
ctx.closePath();
|
|
26138
|
+
if (!lb.noFill) {
|
|
26139
|
+
ctx.fillStyle = color;
|
|
26140
|
+
this.roundRect(ctx, bx, by, w, h, 4);
|
|
25581
26141
|
ctx.fill();
|
|
26142
|
+
if (pointer !== "none") {
|
|
26143
|
+
ctx.beginPath();
|
|
26144
|
+
if (pointer === "down") {
|
|
26145
|
+
ctx.moveTo(px - ptr, by + h);
|
|
26146
|
+
ctx.lineTo(px + ptr, by + h);
|
|
26147
|
+
ctx.lineTo(px, py);
|
|
26148
|
+
} else if (pointer === "up") {
|
|
26149
|
+
ctx.moveTo(px - ptr, by);
|
|
26150
|
+
ctx.lineTo(px + ptr, by);
|
|
26151
|
+
ctx.lineTo(px, py);
|
|
26152
|
+
} else if (pointer === "left") {
|
|
26153
|
+
ctx.moveTo(bx, py - ptr);
|
|
26154
|
+
ctx.lineTo(bx, py + ptr);
|
|
26155
|
+
ctx.lineTo(px, py);
|
|
26156
|
+
} else {
|
|
26157
|
+
ctx.moveTo(bx + w, py - ptr);
|
|
26158
|
+
ctx.lineTo(bx + w, py + ptr);
|
|
26159
|
+
ctx.lineTo(px, py);
|
|
26160
|
+
}
|
|
26161
|
+
ctx.closePath();
|
|
26162
|
+
ctx.fill();
|
|
26163
|
+
}
|
|
25582
26164
|
}
|
|
25583
26165
|
if (lb.text) {
|
|
25584
|
-
ctx.fillStyle = lb.textColor ?? contrastColor(color);
|
|
25585
|
-
ctx.textAlign = "center";
|
|
26166
|
+
ctx.fillStyle = lb.textColor ?? (lb.noFill ? this.deps.theme.textColor : contrastColor(color));
|
|
25586
26167
|
ctx.textBaseline = "middle";
|
|
26168
|
+
let tx;
|
|
26169
|
+
if (lb.textAlign === "left") {
|
|
26170
|
+
ctx.textAlign = "left";
|
|
26171
|
+
tx = bx + padX;
|
|
26172
|
+
} else if (lb.textAlign === "right") {
|
|
26173
|
+
ctx.textAlign = "right";
|
|
26174
|
+
tx = bx + w - padX;
|
|
26175
|
+
} else {
|
|
26176
|
+
ctx.textAlign = "center";
|
|
26177
|
+
tx = bx + w / 2;
|
|
26178
|
+
}
|
|
25587
26179
|
const startY = by + padY + lineH / 2;
|
|
25588
|
-
for (let i = 0; i < lines.length; i += 1) ctx.fillText(lines[i],
|
|
26180
|
+
for (let i = 0; i < lines.length; i += 1) ctx.fillText(lines[i], tx, startY + i * lineH);
|
|
25589
26181
|
}
|
|
26182
|
+
return { x: bx, y: by, w, h };
|
|
25590
26183
|
}
|
|
25591
26184
|
roundRect(ctx, x, y, w, h, r) {
|
|
25592
26185
|
const rr = Math.min(r, w / 2, h / 2);
|
|
@@ -25822,7 +26415,7 @@ var ChromeRenderer = class {
|
|
|
25822
26415
|
this.ctx = null;
|
|
25823
26416
|
// The color for axis tick labels — the host-passed surface text, set each frame in render().
|
|
25824
26417
|
this.axisTextColor = DARK_THEME.textColor;
|
|
25825
|
-
// Shared Pine-drawing renderer
|
|
26418
|
+
// Shared Pine-drawing renderer, used here for autoscale geometry only; widthCache persists.
|
|
25826
26419
|
this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
|
|
25827
26420
|
}
|
|
25828
26421
|
mount(canvas) {
|
|
@@ -25847,8 +26440,8 @@ var ChromeRenderer = class {
|
|
|
25847
26440
|
*/
|
|
25848
26441
|
paneDrawingsRange(ownModels, scene, isPricePane, vr) {
|
|
25849
26442
|
let dr = null;
|
|
25850
|
-
for (const m of ownModels) dr = unionRange(dr, this.drawingsRange(
|
|
25851
|
-
if (isPricePane) for (const m of scene.indicators.values()) dr = unionRange(dr, this.drawingsRange(
|
|
26443
|
+
for (const m of ownModels) dr = unionRange(dr, this.drawingsRange(modelDrawingSet(m, false), vr, scene.offsetOf(m.id)));
|
|
26444
|
+
if (isPricePane) for (const m of scene.indicators.values()) dr = unionRange(dr, this.drawingsRange(modelDrawingSet(m, true), vr, scene.offsetOf(m.id)));
|
|
25852
26445
|
return dr;
|
|
25853
26446
|
}
|
|
25854
26447
|
/** Clear the chrome canvas and draw drawings + axes + current-price line.
|
|
@@ -25880,17 +26473,6 @@ var ChromeRenderer = class {
|
|
|
25880
26473
|
return;
|
|
25881
26474
|
}
|
|
25882
26475
|
const pricePane = panes.find((p) => p.kind === "price") ?? null;
|
|
25883
|
-
for (const pane of panes) {
|
|
25884
|
-
if (pane.collapsed) continue;
|
|
25885
|
-
for (const m of scene.indicatorsForPane(pane.id)) {
|
|
25886
|
-
const sc = scene.scaleFor(m, pane);
|
|
25887
|
-
const mp = sc === pane.scale ? pane : { ...pane, scale: sc };
|
|
25888
|
-
this.renderDrawings(ctx, coords, this.ownDrawings(m), mp, dataW, scene.offsetOf(m.id));
|
|
25889
|
-
}
|
|
25890
|
-
}
|
|
25891
|
-
if (pricePane) {
|
|
25892
|
-
for (const m of scene.indicators.values()) this.renderDrawings(ctx, coords, this.overlayDrawings(m), pricePane, dataW, scene.offsetOf(m.id));
|
|
25893
|
-
}
|
|
25894
26476
|
if (pricePane && !pricePane.collapsed && scene.tradeMarkers.visible) {
|
|
25895
26477
|
for (const m of scene.indicators.values()) {
|
|
25896
26478
|
if (m.trades?.length) this.renderTrades(ctx, coords, scene, theme, m.trades, pricePane, dataW);
|
|
@@ -25906,25 +26488,6 @@ var ChromeRenderer = class {
|
|
|
25906
26488
|
this.canvas = null;
|
|
25907
26489
|
this.ctx = null;
|
|
25908
26490
|
}
|
|
25909
|
-
// ── Pine-drawing helpers (own vs force_overlay routing) ──
|
|
25910
|
-
ownDrawings(m) {
|
|
25911
|
-
return {
|
|
25912
|
-
lines: (m.lines ?? []).filter((d) => !d.overlay),
|
|
25913
|
-
boxes: (m.boxes ?? []).filter((d) => !d.overlay),
|
|
25914
|
-
labels: (m.labels ?? []).filter((d) => !d.overlay),
|
|
25915
|
-
polylines: (m.polylines ?? []).filter((d) => !d.overlay),
|
|
25916
|
-
linefills: (m.linefills ?? []).filter((d) => !d.overlay)
|
|
25917
|
-
};
|
|
25918
|
-
}
|
|
25919
|
-
overlayDrawings(m) {
|
|
25920
|
-
return {
|
|
25921
|
-
lines: (m.lines ?? []).filter((d) => d.overlay),
|
|
25922
|
-
boxes: (m.boxes ?? []).filter((d) => d.overlay),
|
|
25923
|
-
labels: (m.labels ?? []).filter((d) => d.overlay),
|
|
25924
|
-
polylines: (m.polylines ?? []).filter((d) => d.overlay),
|
|
25925
|
-
linefills: (m.linefills ?? []).filter((d) => d.overlay)
|
|
25926
|
-
};
|
|
25927
|
-
}
|
|
25928
26491
|
drawingsRange(set, vr, indexOffset = 0) {
|
|
25929
26492
|
this.drawScene.setSet(set, indexOffset);
|
|
25930
26493
|
if (this.drawScene.isEmpty()) return null;
|
|
@@ -25958,23 +26521,6 @@ var ChromeRenderer = class {
|
|
|
25958
26521
|
);
|
|
25959
26522
|
ctx.restore();
|
|
25960
26523
|
}
|
|
25961
|
-
renderDrawings(ctx, coords, set, pane, dataW, indexOffset = 0) {
|
|
25962
|
-
this.drawScene.setSet(set, indexOffset);
|
|
25963
|
-
if (this.drawScene.isEmpty()) return;
|
|
25964
|
-
ctx.save();
|
|
25965
|
-
ctx.translate(0, pane.bounds.top);
|
|
25966
|
-
ctx.beginPath();
|
|
25967
|
-
ctx.rect(0, 0, dataW, pane.bounds.height);
|
|
25968
|
-
ctx.clip();
|
|
25969
|
-
this.drawScene.render(
|
|
25970
|
-
ctx,
|
|
25971
|
-
dataW,
|
|
25972
|
-
pane.bounds.height,
|
|
25973
|
-
(l) => coords.logicalToX(l),
|
|
25974
|
-
(price) => coords.priceToY(price, pane.scale, pane.bounds) - pane.bounds.top
|
|
25975
|
-
);
|
|
25976
|
-
ctx.restore();
|
|
25977
|
-
}
|
|
25978
26524
|
// ── axes ──
|
|
25979
26525
|
drawPriceAxes(ctx, scene, coords, theme, dataW, panes) {
|
|
25980
26526
|
ctx.strokeStyle = scene.style.borderColor ?? theme.borderColor;
|
|
@@ -26190,6 +26736,68 @@ function formatCountdown(ms) {
|
|
|
26190
26736
|
return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`;
|
|
26191
26737
|
}
|
|
26192
26738
|
|
|
26739
|
+
// src/renderers/native/chrome/LabelTooltip.ts
|
|
26740
|
+
var HOVER_DELAY_MS = 350;
|
|
26741
|
+
var LabelTooltip = class {
|
|
26742
|
+
constructor(plot, deps) {
|
|
26743
|
+
this.plot = plot;
|
|
26744
|
+
this.deps = deps;
|
|
26745
|
+
this.tip = null;
|
|
26746
|
+
this.timer = null;
|
|
26747
|
+
/** Text of the currently open OR armed tip — dedupes moves inside one label. */
|
|
26748
|
+
this.current = null;
|
|
26749
|
+
this.onMove = (e) => {
|
|
26750
|
+
if (e.pointerType !== "mouse") return;
|
|
26751
|
+
const rect = this.plot.getBoundingClientRect();
|
|
26752
|
+
const x = e.clientX - rect.left;
|
|
26753
|
+
const y = e.clientY - rect.top;
|
|
26754
|
+
const text = this.deps.lookup(x, y);
|
|
26755
|
+
if (!text) {
|
|
26756
|
+
this.clear();
|
|
26757
|
+
return;
|
|
26758
|
+
}
|
|
26759
|
+
if (text === this.current) return;
|
|
26760
|
+
this.clear();
|
|
26761
|
+
this.current = text;
|
|
26762
|
+
this.timer = window.setTimeout(() => this.show(text, x, y), HOVER_DELAY_MS);
|
|
26763
|
+
};
|
|
26764
|
+
this.onLeave = () => {
|
|
26765
|
+
this.clear();
|
|
26766
|
+
};
|
|
26767
|
+
plot.addEventListener("pointermove", this.onMove);
|
|
26768
|
+
plot.addEventListener("pointerleave", this.onLeave);
|
|
26769
|
+
plot.addEventListener("pointerdown", this.onLeave);
|
|
26770
|
+
}
|
|
26771
|
+
destroy() {
|
|
26772
|
+
this.plot.removeEventListener("pointermove", this.onMove);
|
|
26773
|
+
this.plot.removeEventListener("pointerleave", this.onLeave);
|
|
26774
|
+
this.plot.removeEventListener("pointerdown", this.onLeave);
|
|
26775
|
+
this.clear();
|
|
26776
|
+
}
|
|
26777
|
+
show(text, x, y) {
|
|
26778
|
+
const doc = this.plot.ownerDocument;
|
|
26779
|
+
const tip = doc.createElement("div");
|
|
26780
|
+
tip.textContent = text;
|
|
26781
|
+
tip.style.cssText = "position:absolute;z-index:var(--vela-z-tooltip);pointer-events:none;background:var(--vela-bg);border:1px solid var(--vela-border);color:var(--vela-fg);border-radius:var(--vela-radius-md);padding:4px 9px;box-shadow:var(--vela-shadow);font:var(--vela-font-size-md) var(--vela-font);max-width:260px;";
|
|
26782
|
+
applyChromeTokens(tip, this.deps.theme());
|
|
26783
|
+
this.plot.appendChild(tip);
|
|
26784
|
+
const pw = this.plot.clientWidth;
|
|
26785
|
+
const ph = this.plot.clientHeight;
|
|
26786
|
+
tip.style.left = `${Math.max(0, Math.min(x + 12, pw - tip.offsetWidth - 4))}px`;
|
|
26787
|
+
tip.style.top = `${Math.max(0, Math.min(y + 16, ph - tip.offsetHeight - 4))}px`;
|
|
26788
|
+
this.tip = tip;
|
|
26789
|
+
}
|
|
26790
|
+
clear() {
|
|
26791
|
+
if (this.timer !== null) {
|
|
26792
|
+
clearTimeout(this.timer);
|
|
26793
|
+
this.timer = null;
|
|
26794
|
+
}
|
|
26795
|
+
this.tip?.remove();
|
|
26796
|
+
this.tip = null;
|
|
26797
|
+
this.current = null;
|
|
26798
|
+
}
|
|
26799
|
+
};
|
|
26800
|
+
|
|
26193
26801
|
// src/renderers/native/chrome/CrosshairRenderer.ts
|
|
26194
26802
|
var CrosshairRenderer = class {
|
|
26195
26803
|
constructor() {
|
|
@@ -30231,17 +30839,17 @@ function glyphIcon(glyph) {
|
|
|
30231
30839
|
return textGlyph(String(glyph), 15);
|
|
30232
30840
|
}
|
|
30233
30841
|
function stampSizeIcon(size) {
|
|
30234
|
-
return textGlyph("\u25CF", (
|
|
30842
|
+
return textGlyph("\u25CF", (SIZE_PX3[String(size)] ?? 13) + 4);
|
|
30235
30843
|
}
|
|
30236
30844
|
function sizeIcon(size) {
|
|
30237
30845
|
return textGlyph(String(size).charAt(0).toUpperCase(), 15);
|
|
30238
30846
|
}
|
|
30239
|
-
var
|
|
30847
|
+
var SIZE_PX3 = { small: 10, normal: 13, large: 16, huge: 20 };
|
|
30240
30848
|
function numbersSizeIcon(size) {
|
|
30241
|
-
return textGlyph("12", (
|
|
30849
|
+
return textGlyph("12", (SIZE_PX3[String(size)] ?? 13) - 1, 16.5, 'font-weight="600"');
|
|
30242
30850
|
}
|
|
30243
30851
|
function labelSizeIcon(size) {
|
|
30244
|
-
return textGlyph("T", (
|
|
30852
|
+
return textGlyph("T", (SIZE_PX3[String(size)] ?? 13) + 2);
|
|
30245
30853
|
}
|
|
30246
30854
|
function capitalize(s) {
|
|
30247
30855
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
@@ -31216,6 +31824,315 @@ var UserDrawingController = class {
|
|
|
31216
31824
|
}
|
|
31217
31825
|
};
|
|
31218
31826
|
|
|
31827
|
+
// src/renderers/shared/TableOverlay.ts
|
|
31828
|
+
var SIZE_PX4 = {
|
|
31829
|
+
auto: 13,
|
|
31830
|
+
tiny: 10,
|
|
31831
|
+
small: 11,
|
|
31832
|
+
normal: 13,
|
|
31833
|
+
large: 16,
|
|
31834
|
+
huge: 20
|
|
31835
|
+
};
|
|
31836
|
+
function fontPxOf(size) {
|
|
31837
|
+
if (typeof size === "number") return size > 0 ? size : SIZE_PX4.auto;
|
|
31838
|
+
return SIZE_PX4[size] ?? SIZE_PX4.auto;
|
|
31839
|
+
}
|
|
31840
|
+
function tableHasContent(t) {
|
|
31841
|
+
return t.cells.some((row) => row?.some((c) => c != null && !c.merged));
|
|
31842
|
+
}
|
|
31843
|
+
function mergeRenderPlan(t) {
|
|
31844
|
+
const span = /* @__PURE__ */ new Map();
|
|
31845
|
+
const omit = /* @__PURE__ */ new Set();
|
|
31846
|
+
for (const m of t.merges) {
|
|
31847
|
+
span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
|
|
31848
|
+
for (let r = m.startRow; r <= m.endRow; r += 1) {
|
|
31849
|
+
for (let c = m.startCol; c <= m.endCol; c += 1) {
|
|
31850
|
+
if (r !== m.startRow || c !== m.startCol) omit.add(`${r}:${c}`);
|
|
31851
|
+
}
|
|
31852
|
+
}
|
|
31853
|
+
}
|
|
31854
|
+
for (let r = 0; r < t.rows; r += 1) {
|
|
31855
|
+
for (let c = 0; c < t.columns; c += 1) {
|
|
31856
|
+
if (t.cells[r]?.[c]?.merged && !span.has(`${r}:${c}`)) omit.add(`${r}:${c}`);
|
|
31857
|
+
}
|
|
31858
|
+
}
|
|
31859
|
+
for (const key of span.keys()) omit.delete(key);
|
|
31860
|
+
return { span, omit };
|
|
31861
|
+
}
|
|
31862
|
+
|
|
31863
|
+
// src/renderers/shared/TableCanvasRenderer.ts
|
|
31864
|
+
var PAD_X = 6;
|
|
31865
|
+
var PAD_Y = 2;
|
|
31866
|
+
var MARGIN = 6;
|
|
31867
|
+
var LINE_HEIGHT = 1.2;
|
|
31868
|
+
function paintTable(ctx, t, args, tips) {
|
|
31869
|
+
if (!tableHasContent(t)) return;
|
|
31870
|
+
const layout = layoutTable(ctx, t, args);
|
|
31871
|
+
if (!layout || layout.w <= 0 || layout.h <= 0) return;
|
|
31872
|
+
const fw = t.frameColor && t.frameWidth > 0 ? t.frameWidth : 0;
|
|
31873
|
+
const { x, y } = anchorOrigin(t.position, layout.w + 2 * fw, layout.h + 2 * fw, args);
|
|
31874
|
+
const x0 = x + fw;
|
|
31875
|
+
const y0 = y + fw;
|
|
31876
|
+
if (t.bgColor) {
|
|
31877
|
+
ctx.fillStyle = t.bgColor;
|
|
31878
|
+
ctx.fillRect(x0, y0, layout.w, layout.h);
|
|
31879
|
+
}
|
|
31880
|
+
if (fw > 0 && t.frameColor) {
|
|
31881
|
+
ctx.strokeStyle = t.frameColor;
|
|
31882
|
+
ctx.lineWidth = fw;
|
|
31883
|
+
ctx.strokeRect(x + fw / 2, y + fw / 2, layout.w + fw, layout.h + fw);
|
|
31884
|
+
}
|
|
31885
|
+
const colX = [0];
|
|
31886
|
+
for (const w of layout.colW) colX.push(colX[colX.length - 1] + w);
|
|
31887
|
+
const rowY = [0];
|
|
31888
|
+
for (const h of layout.rowH) rowY.push(rowY[rowY.length - 1] + h);
|
|
31889
|
+
const prevBaseline = ctx.textBaseline;
|
|
31890
|
+
const prevAlign = ctx.textAlign;
|
|
31891
|
+
ctx.textBaseline = "middle";
|
|
31892
|
+
for (const box of layout.boxes) {
|
|
31893
|
+
const rx = x0 + colX[box.c];
|
|
31894
|
+
const ry = y0 + rowY[box.r];
|
|
31895
|
+
const rw = colX[box.c + box.cs] - colX[box.c];
|
|
31896
|
+
const rh = rowY[box.r + box.rs] - rowY[box.r];
|
|
31897
|
+
const cell = box.cell;
|
|
31898
|
+
if (cell.bgColor) {
|
|
31899
|
+
ctx.fillStyle = cell.bgColor;
|
|
31900
|
+
ctx.fillRect(rx, ry, rw, rh);
|
|
31901
|
+
}
|
|
31902
|
+
const text = cell.text ?? "";
|
|
31903
|
+
if (text.length > 0) {
|
|
31904
|
+
const px = fontPxOf(cell.textSize);
|
|
31905
|
+
ctx.font = cellFont(cell, px, args.theme);
|
|
31906
|
+
ctx.fillStyle = cell.textColor ?? args.theme.textColor;
|
|
31907
|
+
const lines = text.split("\n");
|
|
31908
|
+
const blockH = lines.length * px * LINE_HEIGHT;
|
|
31909
|
+
const blockTop = cell.vAlign === "top" ? ry + PAD_Y : cell.vAlign === "bottom" ? ry + rh - PAD_Y - blockH : ry + (rh - blockH) / 2;
|
|
31910
|
+
const tx = cell.hAlign === "left" ? rx + PAD_X : cell.hAlign === "right" ? rx + rw - PAD_X : rx + rw / 2;
|
|
31911
|
+
ctx.textAlign = cell.hAlign;
|
|
31912
|
+
lines.forEach((line, i) => ctx.fillText(line, tx, blockTop + (i + 0.5) * px * LINE_HEIGHT));
|
|
31913
|
+
}
|
|
31914
|
+
if (cell.tooltip) tips.push({ left: rx, top: ry, right: rx + rw, bottom: ry + rh, text: cell.tooltip });
|
|
31915
|
+
}
|
|
31916
|
+
ctx.textBaseline = prevBaseline;
|
|
31917
|
+
ctx.textAlign = prevAlign;
|
|
31918
|
+
if (t.borderColor && t.borderWidth > 0) {
|
|
31919
|
+
ctx.strokeStyle = t.borderColor;
|
|
31920
|
+
ctx.lineWidth = t.borderWidth;
|
|
31921
|
+
const seen = /* @__PURE__ */ new Set();
|
|
31922
|
+
ctx.beginPath();
|
|
31923
|
+
const edge = (ax, ay, bx, by) => {
|
|
31924
|
+
const key = `${ax},${ay},${bx},${by}`;
|
|
31925
|
+
if (seen.has(key)) return;
|
|
31926
|
+
seen.add(key);
|
|
31927
|
+
ctx.moveTo(ax, ay);
|
|
31928
|
+
ctx.lineTo(bx, by);
|
|
31929
|
+
};
|
|
31930
|
+
for (const box of layout.boxes) {
|
|
31931
|
+
const l = Math.round(x0 + colX[box.c]);
|
|
31932
|
+
const r = Math.round(x0 + colX[box.c + box.cs]);
|
|
31933
|
+
const tp = Math.round(y0 + rowY[box.r]);
|
|
31934
|
+
const bt = Math.round(y0 + rowY[box.r + box.rs]);
|
|
31935
|
+
edge(l, tp, r, tp);
|
|
31936
|
+
edge(l, bt, r, bt);
|
|
31937
|
+
edge(l, tp, l, bt);
|
|
31938
|
+
edge(r, tp, r, bt);
|
|
31939
|
+
}
|
|
31940
|
+
ctx.stroke();
|
|
31941
|
+
}
|
|
31942
|
+
}
|
|
31943
|
+
function layoutTable(ctx, t, args) {
|
|
31944
|
+
const { span, omit } = mergeRenderPlan(t);
|
|
31945
|
+
const colW = new Array(t.columns).fill(0);
|
|
31946
|
+
const rowH = new Array(t.rows).fill(0);
|
|
31947
|
+
const boxes = [];
|
|
31948
|
+
for (let r = 0; r < t.rows; r += 1) {
|
|
31949
|
+
for (let c = 0; c < t.columns; c += 1) {
|
|
31950
|
+
if (omit.has(`${r}:${c}`)) continue;
|
|
31951
|
+
const cell = t.cells[r]?.[c];
|
|
31952
|
+
if (cell == null) continue;
|
|
31953
|
+
const sp = span.get(`${r}:${c}`);
|
|
31954
|
+
boxes.push({ cell, r, c, cs: Math.min(sp?.cs ?? 1, t.columns - c), rs: Math.min(sp?.rs ?? 1, t.rows - r) });
|
|
31955
|
+
}
|
|
31956
|
+
}
|
|
31957
|
+
if (boxes.length === 0) return null;
|
|
31958
|
+
const sizeOf = (cell) => {
|
|
31959
|
+
const px = fontPxOf(cell.textSize);
|
|
31960
|
+
ctx.font = cellFont(cell, px, args.theme);
|
|
31961
|
+
const lines = (cell.text ?? "").split("\n");
|
|
31962
|
+
let maxW = 0;
|
|
31963
|
+
for (const line of lines) maxW = Math.max(maxW, ctx.measureText(line).width);
|
|
31964
|
+
let w2 = Math.ceil(maxW) + 2 * PAD_X;
|
|
31965
|
+
let h2 = Math.ceil(lines.length * px * LINE_HEIGHT) + 2 * PAD_Y;
|
|
31966
|
+
if (cell.width) w2 = Math.max(w2, cell.width / 100 * args.plotWidth);
|
|
31967
|
+
if (cell.height) h2 = Math.max(h2, cell.height / 100 * args.paneHeight);
|
|
31968
|
+
return { w: w2, h: h2 };
|
|
31969
|
+
};
|
|
31970
|
+
const spanning = [];
|
|
31971
|
+
for (const box of boxes) {
|
|
31972
|
+
const { w: w2, h: h2 } = sizeOf(box.cell);
|
|
31973
|
+
if (box.cs === 1) colW[box.c] = Math.max(colW[box.c], w2);
|
|
31974
|
+
if (box.rs === 1) rowH[box.r] = Math.max(rowH[box.r], h2);
|
|
31975
|
+
if (box.cs > 1 || box.rs > 1) spanning.push({ box, w: w2, h: h2 });
|
|
31976
|
+
}
|
|
31977
|
+
for (const { box, w: w2, h: h2 } of spanning) {
|
|
31978
|
+
if (box.cs > 1) {
|
|
31979
|
+
let sum = 0;
|
|
31980
|
+
for (let c = box.c; c < box.c + box.cs; c += 1) sum += colW[c];
|
|
31981
|
+
if (w2 > sum) for (let c = box.c; c < box.c + box.cs; c += 1) colW[c] += (w2 - sum) / box.cs;
|
|
31982
|
+
}
|
|
31983
|
+
if (box.rs > 1) {
|
|
31984
|
+
let sum = 0;
|
|
31985
|
+
for (let r = box.r; r < box.r + box.rs; r += 1) sum += rowH[r];
|
|
31986
|
+
if (h2 > sum) for (let r = box.r; r < box.r + box.rs; r += 1) rowH[r] += (h2 - sum) / box.rs;
|
|
31987
|
+
}
|
|
31988
|
+
}
|
|
31989
|
+
let w = 0;
|
|
31990
|
+
for (const cw of colW) w += cw;
|
|
31991
|
+
let h = 0;
|
|
31992
|
+
for (const rh of rowH) h += rh;
|
|
31993
|
+
return { colW, rowH, w, h, boxes };
|
|
31994
|
+
}
|
|
31995
|
+
function anchorOrigin(position, totalW, totalH, args) {
|
|
31996
|
+
let y;
|
|
31997
|
+
if (position.startsWith("top")) y = MARGIN;
|
|
31998
|
+
else if (position.startsWith("bottom")) y = args.paneHeight - MARGIN - totalH;
|
|
31999
|
+
else y = args.paneHeight / 2 - totalH / 2;
|
|
32000
|
+
let x;
|
|
32001
|
+
if (position.endsWith("left")) x = MARGIN;
|
|
32002
|
+
else if (position.endsWith("right")) x = args.plotWidth - MARGIN - totalW;
|
|
32003
|
+
else x = args.plotWidth / 2 - totalW / 2;
|
|
32004
|
+
return { x, y };
|
|
32005
|
+
}
|
|
32006
|
+
function cellFont(cell, px, theme) {
|
|
32007
|
+
const family = cell.fontFamily === "monospace" ? "monospace" : theme.fontFamily || "sans-serif";
|
|
32008
|
+
return `${cell.italic ? "italic " : ""}${cell.bold ? "bold " : ""}${px}px ${family}`;
|
|
32009
|
+
}
|
|
32010
|
+
|
|
32011
|
+
// src/renderers/native/drawings/IndicatorDrawingSlices.ts
|
|
32012
|
+
function indicatorSliceKey(z, boundaries) {
|
|
32013
|
+
return boundaries.find((b) => b > z) ?? Infinity;
|
|
32014
|
+
}
|
|
32015
|
+
var IndicatorDrawingSlices = class {
|
|
32016
|
+
constructor() {
|
|
32017
|
+
this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
|
|
32018
|
+
/** Slice canvas cache, keyed `paneId|beforeZ` — same lifecycle as the user-drawing cache. */
|
|
32019
|
+
this.sliceCache = /* @__PURE__ */ new Map();
|
|
32020
|
+
/** Tooltip hit-rects of every label drawn this frame, in plot coords (rebuilt per prepare). */
|
|
32021
|
+
this.tips = [];
|
|
32022
|
+
}
|
|
32023
|
+
/**
|
|
32024
|
+
* Rebuild the per-indicator drawing slices for this data frame. `ref` is the data
|
|
32025
|
+
* canvas the slices must match pixel-for-pixel (the backend composites them 1:1).
|
|
32026
|
+
* Runs from the renderer's data paint, just before the backend composites the scene.
|
|
32027
|
+
*/
|
|
32028
|
+
prepare(scene, coords, theme, ref) {
|
|
32029
|
+
this.tips = [];
|
|
32030
|
+
const out = /* @__PURE__ */ new Map();
|
|
32031
|
+
if (ref.width === 0 || ref.height === 0) {
|
|
32032
|
+
this.sliceCache.clear();
|
|
32033
|
+
return out;
|
|
32034
|
+
}
|
|
32035
|
+
this.drawScene.setDeps({
|
|
32036
|
+
timeToLogical: (ms) => coords.timeToLogical(ms),
|
|
32037
|
+
barAt: (logical) => {
|
|
32038
|
+
const b = scene.bars[Math.round(logical)];
|
|
32039
|
+
return b ? { high: b.high, low: b.low } : null;
|
|
32040
|
+
},
|
|
32041
|
+
theme
|
|
32042
|
+
});
|
|
32043
|
+
const dpr = coords.dpr;
|
|
32044
|
+
const dataW = coords.width;
|
|
32045
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
32046
|
+
const add = (paneId, beforeZ, entry) => {
|
|
32047
|
+
const key = `${paneId}|${beforeZ}`;
|
|
32048
|
+
const bucket = buckets.get(key);
|
|
32049
|
+
if (bucket) bucket.entries.push(entry);
|
|
32050
|
+
else buckets.set(key, { paneId, beforeZ, entries: [entry] });
|
|
32051
|
+
};
|
|
32052
|
+
for (const pane of scene.orderedPanes()) {
|
|
32053
|
+
if (pane.collapsed) continue;
|
|
32054
|
+
const boundaries = scene.seriesBoundaries(pane.id);
|
|
32055
|
+
for (const m of scene.orderedIndicatorsForPane(pane.id)) {
|
|
32056
|
+
const set = modelDrawingSet(m, false);
|
|
32057
|
+
const tables = (m.tables ?? []).filter((t) => !t.overlay);
|
|
32058
|
+
if (drawingSetEmpty(set) && tables.length === 0) continue;
|
|
32059
|
+
const sc = scene.scaleFor(m, pane);
|
|
32060
|
+
const mp = sc === pane.scale ? pane : { ...pane, scale: sc };
|
|
32061
|
+
const beforeZ = indicatorSliceKey(scene.zOf(m.id), boundaries);
|
|
32062
|
+
add(pane.id, beforeZ, { set, tables, pane: mp, indexOffset: scene.offsetOf(m.id) });
|
|
32063
|
+
}
|
|
32064
|
+
if (pane.kind === "price") {
|
|
32065
|
+
for (const m of scene.indicators.values()) {
|
|
32066
|
+
const set = modelDrawingSet(m, true);
|
|
32067
|
+
const tables = (m.tables ?? []).filter((t) => t.overlay === true);
|
|
32068
|
+
if (drawingSetEmpty(set) && tables.length === 0) continue;
|
|
32069
|
+
add(pane.id, Infinity, { set, tables, pane, indexOffset: scene.offsetOf(m.id) });
|
|
32070
|
+
}
|
|
32071
|
+
}
|
|
32072
|
+
}
|
|
32073
|
+
for (const [key, { paneId, beforeZ, entries }] of buckets) {
|
|
32074
|
+
let canvas = this.sliceCache.get(key);
|
|
32075
|
+
if (!canvas) {
|
|
32076
|
+
canvas = document.createElement("canvas");
|
|
32077
|
+
this.sliceCache.set(key, canvas);
|
|
32078
|
+
}
|
|
32079
|
+
if (canvas.width !== ref.width || canvas.height !== ref.height) {
|
|
32080
|
+
canvas.width = ref.width;
|
|
32081
|
+
canvas.height = ref.height;
|
|
32082
|
+
}
|
|
32083
|
+
const ctx = canvas.getContext("2d");
|
|
32084
|
+
if (!ctx) continue;
|
|
32085
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
32086
|
+
ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
|
|
32087
|
+
for (const e of entries) this.paintEntry(ctx, e, coords, dataW, theme);
|
|
32088
|
+
const slices = out.get(paneId) ?? [];
|
|
32089
|
+
slices.push({ beforeZ, canvas });
|
|
32090
|
+
out.set(paneId, slices);
|
|
32091
|
+
}
|
|
32092
|
+
for (const key of [...this.sliceCache.keys()]) if (!buckets.has(key)) this.sliceCache.delete(key);
|
|
32093
|
+
for (const slices of out.values()) slices.sort((a, b) => a.beforeZ - b.beforeZ);
|
|
32094
|
+
return out;
|
|
32095
|
+
}
|
|
32096
|
+
paintEntry(ctx, e, coords, dataW, theme) {
|
|
32097
|
+
const { pane } = e;
|
|
32098
|
+
const paneTips = [];
|
|
32099
|
+
ctx.save();
|
|
32100
|
+
ctx.translate(0, pane.bounds.top);
|
|
32101
|
+
ctx.beginPath();
|
|
32102
|
+
ctx.rect(0, 0, dataW, pane.bounds.height);
|
|
32103
|
+
ctx.clip();
|
|
32104
|
+
this.drawScene.setSet(e.set, e.indexOffset);
|
|
32105
|
+
this.drawScene.render(
|
|
32106
|
+
ctx,
|
|
32107
|
+
dataW,
|
|
32108
|
+
pane.bounds.height,
|
|
32109
|
+
(l) => coords.logicalToX(l),
|
|
32110
|
+
(price) => coords.priceToY(price, pane.scale, pane.bounds) - pane.bounds.top
|
|
32111
|
+
);
|
|
32112
|
+
paneTips.push(...this.drawScene.labelTipRegions());
|
|
32113
|
+
for (const t of e.tables) paintTable(ctx, t, { paneHeight: pane.bounds.height, plotWidth: dataW, theme }, paneTips);
|
|
32114
|
+
ctx.restore();
|
|
32115
|
+
for (const r of paneTips) {
|
|
32116
|
+
this.tips.push({ ...r, top: r.top + pane.bounds.top, bottom: r.bottom + pane.bounds.top });
|
|
32117
|
+
}
|
|
32118
|
+
}
|
|
32119
|
+
/** Tooltip of the topmost label or table cell under a plot-space point, or null. Fed by the last prepare. */
|
|
32120
|
+
labelTooltipAt(x, y) {
|
|
32121
|
+
for (let i = this.tips.length - 1; i >= 0; i -= 1) {
|
|
32122
|
+
const r = this.tips[i];
|
|
32123
|
+
if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return r.text;
|
|
32124
|
+
}
|
|
32125
|
+
return null;
|
|
32126
|
+
}
|
|
32127
|
+
};
|
|
32128
|
+
function mergeSlices(indicator, user) {
|
|
32129
|
+
const out = /* @__PURE__ */ new Map();
|
|
32130
|
+
for (const [paneId, slices] of indicator) out.set(paneId, [...slices]);
|
|
32131
|
+
for (const [paneId, slices] of user) out.set(paneId, [...out.get(paneId) ?? [], ...slices]);
|
|
32132
|
+
for (const slices of out.values()) slices.sort((a, b) => a.beforeZ - b.beforeZ);
|
|
32133
|
+
return out;
|
|
32134
|
+
}
|
|
32135
|
+
|
|
31219
32136
|
// src/renderers/native/drawings/Projector.ts
|
|
31220
32137
|
function createProjector(coords, paneOf, paneIdAtY, barsInRange) {
|
|
31221
32138
|
return {
|
|
@@ -31267,19 +32184,8 @@ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log =
|
|
|
31267
32184
|
for (const model of models) {
|
|
31268
32185
|
const off = offsetOf(model.id);
|
|
31269
32186
|
for (const s of model.series) {
|
|
31270
|
-
if (s.
|
|
31271
|
-
|
|
31272
|
-
const b = s.bars[i - off];
|
|
31273
|
-
if (b) {
|
|
31274
|
-
consider(b.high);
|
|
31275
|
-
consider(b.low);
|
|
31276
|
-
}
|
|
31277
|
-
}
|
|
31278
|
-
} else if (isLineLikeSeries(s)) {
|
|
31279
|
-
for (let i = i0; i <= i1; i += 1) consider(s.points[i - off]?.value);
|
|
31280
|
-
if (s.kind === "histogram" || s.kind === "columns") consider(s.style?.base ?? 0);
|
|
31281
|
-
else if (s.style?.base != null) consider(s.style.base);
|
|
31282
|
-
}
|
|
32187
|
+
if (s.overlay === true) continue;
|
|
32188
|
+
considerSeries(s, i0, i1, off, consider);
|
|
31283
32189
|
}
|
|
31284
32190
|
for (const pl of model.priceLines) consider(pl.price);
|
|
31285
32191
|
}
|
|
@@ -31301,6 +32207,36 @@ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log =
|
|
|
31301
32207
|
const span = max - min;
|
|
31302
32208
|
return { min: min - span * MARGIN_BOTTOM, max: max + span * MARGIN_TOP };
|
|
31303
32209
|
}
|
|
32210
|
+
function considerSeries(s, i0, i1, off, consider) {
|
|
32211
|
+
if (s.kind === "candle" || s.kind === "bar") {
|
|
32212
|
+
for (let i = i0; i <= i1; i += 1) {
|
|
32213
|
+
const b = s.bars[i - off];
|
|
32214
|
+
if (b) {
|
|
32215
|
+
consider(b.high);
|
|
32216
|
+
consider(b.low);
|
|
32217
|
+
}
|
|
32218
|
+
}
|
|
32219
|
+
} else if (isLineLikeSeries(s)) {
|
|
32220
|
+
for (let i = i0; i <= i1; i += 1) consider(s.points[i - off]?.value);
|
|
32221
|
+
if (s.kind === "histogram" || s.kind === "columns") consider(s.style?.base ?? 0);
|
|
32222
|
+
else if (s.style?.base != null) consider(s.style.base);
|
|
32223
|
+
}
|
|
32224
|
+
}
|
|
32225
|
+
function overlaySeriesRange(models, i0, i1, offsetOf = () => 0) {
|
|
32226
|
+
let min = Infinity;
|
|
32227
|
+
let max = -Infinity;
|
|
32228
|
+
const consider = (v) => {
|
|
32229
|
+
if (v != null && Number.isFinite(v)) {
|
|
32230
|
+
if (v < min) min = v;
|
|
32231
|
+
if (v > max) max = v;
|
|
32232
|
+
}
|
|
32233
|
+
};
|
|
32234
|
+
for (const model of models) {
|
|
32235
|
+
const off = offsetOf(model.id);
|
|
32236
|
+
for (const s of model.series) if (s.overlay === true) considerSeries(s, i0, i1, off, consider);
|
|
32237
|
+
}
|
|
32238
|
+
return min === Infinity ? null : { min, max };
|
|
32239
|
+
}
|
|
31304
32240
|
function expandScaleByPixels(scale, heightPx, abovePx, belowPx) {
|
|
31305
32241
|
if (abovePx <= 0 && belowPx <= 0) return scale;
|
|
31306
32242
|
const content = heightPx - abovePx - belowPx;
|
|
@@ -31942,6 +32878,11 @@ var NativeRenderer = class {
|
|
|
31942
32878
|
this.vpvrRenderer = new VpvrRenderer();
|
|
31943
32879
|
this.resizeObserver = null;
|
|
31944
32880
|
this.dprMedia = null;
|
|
32881
|
+
/** Plot size in INTEGER device px, as last reported by the resize observer's
|
|
32882
|
+
* device-pixel-content-box — the browser's own statement of how many device pixels
|
|
32883
|
+
* it paints the plot into. `null` until the first report or where the box type is
|
|
32884
|
+
* unsupported (WebKit); syncSize then falls back to rounding the client rect. */
|
|
32885
|
+
this.plotDeviceSize = null;
|
|
31945
32886
|
this.coords = new CoordinateSystem();
|
|
31946
32887
|
this.scene = new SceneGraph();
|
|
31947
32888
|
// chosen at mount (WebGL2 if available, else canvas2d)
|
|
@@ -31949,6 +32890,10 @@ var NativeRenderer = class {
|
|
|
31949
32890
|
this.glowAmount = 0;
|
|
31950
32891
|
// WebGL2 neon-glow intensity (canvas2d ignores it)
|
|
31951
32892
|
this.chrome = new ChromeRenderer();
|
|
32893
|
+
/** Prepaints each indicator's Pine drawings into interleave slices at the model's z. */
|
|
32894
|
+
this.indicatorSlices = new IndicatorDrawingSlices();
|
|
32895
|
+
/** Hover tooltips for Pine labels (canvas hit-rects collected by the chrome layer). */
|
|
32896
|
+
this.labelTooltip = null;
|
|
31952
32897
|
this.crosshairLayer = new CrosshairRenderer();
|
|
31953
32898
|
/** 1 Hz repaint pump so the price-axis countdown-to-bar-close ticks; null when off. */
|
|
31954
32899
|
this.countdownTimer = null;
|
|
@@ -31959,6 +32904,8 @@ var NativeRenderer = class {
|
|
|
31959
32904
|
this.indicatorValuesOn = true;
|
|
31960
32905
|
/** Host-contributed legend actions — held here so a rebuild of the legend re-wires them. */
|
|
31961
32906
|
this.legendActionsProvider = null;
|
|
32907
|
+
/** Host-contributed legend callouts — held here so a rebuild of the legend re-wires them. */
|
|
32908
|
+
this.legendCalloutsProvider = null;
|
|
31962
32909
|
/** Host override of the legend's fold toggle — held here so a remount re-applies it. */
|
|
31963
32910
|
this.legendOverviewAction = null;
|
|
31964
32911
|
// ── keyboard navigation / accessibility (item 11) ──
|
|
@@ -32093,7 +33040,6 @@ var NativeRenderer = class {
|
|
|
32093
33040
|
this.toggleVisibleCbs = /* @__PURE__ */ new Set();
|
|
32094
33041
|
this.moveIndicatorCbs = /* @__PURE__ */ new Set();
|
|
32095
33042
|
this.priceStyleCbs = /* @__PURE__ */ new Set();
|
|
32096
|
-
this.tableOverlays = /* @__PURE__ */ new Map();
|
|
32097
33043
|
this.name = "native";
|
|
32098
33044
|
this.features = ["logScale", "currentPriceLine", "priceLabel", "countdown", "upColor", "downColor", "glow", "animZoom", "animPan", "intro", "zoomAnchor", "axisDrag", "paneResize", "candleZOrder", "candleVisible", "seriesOrder", "highlights", "sessionZones", "gridlines", "axisLabels", "scaleMode", "invertScale", "paneScales", "autoScale", "timezone", "keyboard", "historyChords", "priceStyle", "priceBaseline", "baselinePrice", "settings", "attribution", "dialogHost", "tradeMarkers", "indicatorTitles", "indicatorValues"];
|
|
32099
33045
|
/** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
|
|
@@ -32407,7 +33353,6 @@ var NativeRenderer = class {
|
|
|
32407
33353
|
* hidden) on clear so a re-show picks up the current theme.
|
|
32408
33354
|
*/
|
|
32409
33355
|
setLoading(loading) {
|
|
32410
|
-
for (const overlay of this.tableOverlays.values()) overlay.setVisible(!loading);
|
|
32411
33356
|
if (!loading || !this.wrapper) {
|
|
32412
33357
|
this.loadingEl?.remove();
|
|
32413
33358
|
this.loadingEl = null;
|
|
@@ -33067,6 +34012,10 @@ var NativeRenderer = class {
|
|
|
33067
34012
|
this.plot.appendChild(this.scrollButton);
|
|
33068
34013
|
this.plot.addEventListener("pointermove", this.onScrollProximityMove);
|
|
33069
34014
|
this.plot.addEventListener("pointerleave", this.onScrollProximityLeave);
|
|
34015
|
+
this.labelTooltip = new LabelTooltip(this.plot, {
|
|
34016
|
+
theme: () => this.chromeTheme(),
|
|
34017
|
+
lookup: (x, y) => this.indicatorSlices.labelTooltipAt(x, y)
|
|
34018
|
+
});
|
|
33070
34019
|
this.userDrawings = new UserDrawingController(this.wrapper, this.plot, this.drawingsCanvas, {
|
|
33071
34020
|
projector: () => this.drawingProjector(),
|
|
33072
34021
|
dpr: () => this.coords.dpr,
|
|
@@ -33106,9 +34055,10 @@ var NativeRenderer = class {
|
|
|
33106
34055
|
this.inputsUI.setDialogHost(this.dialogHost);
|
|
33107
34056
|
this.inputsUI.setSymbolPicker(this.symbolPicker);
|
|
33108
34057
|
this.inputsUI.setLegendActions(this.legendActionsProvider);
|
|
34058
|
+
this.inputsUI.setLegendCallouts(this.legendCalloutsProvider);
|
|
33109
34059
|
this.inputsUI.setLegendOverviewAction(this.legendOverviewAction);
|
|
33110
34060
|
this.inputsUI.setOnChange((c) => {
|
|
33111
|
-
for (const cb of this.inputChangeCbs) cb({ indicatorId: c.indicatorId, key: c.key, value: c.value });
|
|
34061
|
+
for (const cb of this.inputChangeCbs) cb({ indicatorId: c.indicatorId, key: c.key, value: c.value, ...c.kind ? { kind: c.kind } : {} });
|
|
33112
34062
|
});
|
|
33113
34063
|
this.inputsUI.setOnRemove((id) => {
|
|
33114
34064
|
for (const cb of this.removeIndicatorCbs) cb(id);
|
|
@@ -33149,6 +34099,7 @@ var NativeRenderer = class {
|
|
|
33149
34099
|
this.emitPaneAction({ type: "maximize", paneId, maximized });
|
|
33150
34100
|
}
|
|
33151
34101
|
});
|
|
34102
|
+
this.paneControls.setSuspended(this.layoutMode === "mobile");
|
|
33152
34103
|
this.axisScaleButtons = new AxisScaleButtons(this.plot, theme, {
|
|
33153
34104
|
panes: () => this.axisScaleViews(),
|
|
33154
34105
|
rightAxis: () => this.rightAxisW,
|
|
@@ -33158,8 +34109,19 @@ var NativeRenderer = class {
|
|
|
33158
34109
|
if (pane) this.setPaneLog(paneId, !paneLogScale(this.scene, pane));
|
|
33159
34110
|
}
|
|
33160
34111
|
});
|
|
33161
|
-
this.resizeObserver = new ResizeObserver(() =>
|
|
34112
|
+
this.resizeObserver = new ResizeObserver((entries) => {
|
|
34113
|
+
for (const e of entries) {
|
|
34114
|
+
if (e.target !== this.plot) continue;
|
|
34115
|
+
const s = e.devicePixelContentBoxSize?.[0];
|
|
34116
|
+
if (s) this.plotDeviceSize = { width: s.inlineSize, height: s.blockSize };
|
|
34117
|
+
}
|
|
34118
|
+
this.resize();
|
|
34119
|
+
});
|
|
33162
34120
|
this.resizeObserver.observe(this.wrapper);
|
|
34121
|
+
try {
|
|
34122
|
+
this.resizeObserver.observe(this.plot, { box: "device-pixel-content-box" });
|
|
34123
|
+
} catch {
|
|
34124
|
+
}
|
|
33163
34125
|
this.watchDpr();
|
|
33164
34126
|
this.syncSize();
|
|
33165
34127
|
}
|
|
@@ -33284,8 +34246,6 @@ var NativeRenderer = class {
|
|
|
33284
34246
|
this.inputsUI?.destroy();
|
|
33285
34247
|
this.paneControls?.destroy();
|
|
33286
34248
|
this.axisScaleButtons?.destroy();
|
|
33287
|
-
for (const overlay of this.tableOverlays.values()) overlay.destroy();
|
|
33288
|
-
this.tableOverlays.clear();
|
|
33289
34249
|
this.resizeObserver?.disconnect();
|
|
33290
34250
|
this.resizeObserver = null;
|
|
33291
34251
|
this.dprMedia?.removeEventListener("change", this.onDprChange);
|
|
@@ -33298,6 +34258,8 @@ var NativeRenderer = class {
|
|
|
33298
34258
|
this.settingsButton = null;
|
|
33299
34259
|
this.plot?.removeEventListener("pointermove", this.onScrollProximityMove);
|
|
33300
34260
|
this.plot?.removeEventListener("pointerleave", this.onScrollProximityLeave);
|
|
34261
|
+
this.labelTooltip?.destroy();
|
|
34262
|
+
this.labelTooltip = null;
|
|
33301
34263
|
this.scrollButton?.remove();
|
|
33302
34264
|
this.scrollButton = null;
|
|
33303
34265
|
for (const l of this.extLayers) l.instance.destroy?.();
|
|
@@ -33312,6 +34274,7 @@ var NativeRenderer = class {
|
|
|
33312
34274
|
this.attributionEl = null;
|
|
33313
34275
|
this.mountContainer?.style.removeProperty("--vela-toolbar-gutter");
|
|
33314
34276
|
this.mountContainer?.style.removeProperty("--vela-scale-gutter");
|
|
34277
|
+
this.mountContainer?.style.removeProperty("--vela-bottom-gutter");
|
|
33315
34278
|
this.mountContainer?.style.removeProperty("--vela-price-pane-top");
|
|
33316
34279
|
this.mountContainer?.style.removeProperty("--vela-price-pane-bottom");
|
|
33317
34280
|
this.mountContainer = null;
|
|
@@ -33404,7 +34367,6 @@ var NativeRenderer = class {
|
|
|
33404
34367
|
ensurePane(pane) {
|
|
33405
34368
|
this.scene.ensurePane(pane.id, pane.kind, pane.order, pane.heightWeight ?? (pane.kind === "price" ? 3 : 1));
|
|
33406
34369
|
this.layoutPanes();
|
|
33407
|
-
this.repositionTables();
|
|
33408
34370
|
this.paneControls?.refresh();
|
|
33409
34371
|
this.scheduler.invalidate(4 /* Full */);
|
|
33410
34372
|
}
|
|
@@ -33426,17 +34388,14 @@ var NativeRenderer = class {
|
|
|
33426
34388
|
if (!model.ownScale) this.scene.dropIndicatorScale(handle.id);
|
|
33427
34389
|
this.inputsUI.setPane(handle.id, paneId);
|
|
33428
34390
|
this.refreshAnchorOffset(model);
|
|
33429
|
-
this.syncTables(model);
|
|
33430
34391
|
this.refreshAxisWidth();
|
|
33431
34392
|
this.layoutPanes();
|
|
33432
|
-
this.repositionTables();
|
|
33433
34393
|
this.paneControls?.refresh();
|
|
33434
34394
|
this.scheduler.invalidate(4 /* Full */);
|
|
33435
34395
|
}
|
|
33436
34396
|
orderPanes(orderedIds) {
|
|
33437
34397
|
this.scene.orderPanes(orderedIds);
|
|
33438
34398
|
this.layoutPanes();
|
|
33439
|
-
this.repositionTables();
|
|
33440
34399
|
this.paneControls?.refresh();
|
|
33441
34400
|
this.scheduler.invalidate(4 /* Full */);
|
|
33442
34401
|
}
|
|
@@ -33445,7 +34404,6 @@ var NativeRenderer = class {
|
|
|
33445
34404
|
if (!pane || pane.collapsed === collapsed) return;
|
|
33446
34405
|
pane.collapsed = collapsed;
|
|
33447
34406
|
this.layoutPanes();
|
|
33448
|
-
this.repositionTables();
|
|
33449
34407
|
this.paneControls?.refresh();
|
|
33450
34408
|
this.scheduler.invalidate(4 /* Full */);
|
|
33451
34409
|
}
|
|
@@ -33453,7 +34411,6 @@ var NativeRenderer = class {
|
|
|
33453
34411
|
if (paneId !== null && !this.scene.panes.has(paneId)) paneId = null;
|
|
33454
34412
|
this.maximizedPaneId = paneId;
|
|
33455
34413
|
this.layoutPanes();
|
|
33456
|
-
this.repositionTables();
|
|
33457
34414
|
this.paneControls?.refresh();
|
|
33458
34415
|
this.scheduler.invalidate(4 /* Full */);
|
|
33459
34416
|
}
|
|
@@ -33538,9 +34495,8 @@ var NativeRenderer = class {
|
|
|
33538
34495
|
else this.scene.assignIndicatorZ(model.id);
|
|
33539
34496
|
this.inputsUI.upsert(model.id, model.shorttitle ?? model.title, model.inputs, model.inputValues, model.paneId, {
|
|
33540
34497
|
native: !!model.native,
|
|
33541
|
-
...model.
|
|
34498
|
+
...model.props ? { props: model.props, propValues: model.propValues ?? {} } : {}
|
|
33542
34499
|
});
|
|
33543
|
-
this.syncTables(model);
|
|
33544
34500
|
if (model.native?.type === "volume") {
|
|
33545
34501
|
this.volumeActive = true;
|
|
33546
34502
|
this.volumeHidden = false;
|
|
@@ -33564,7 +34520,6 @@ var NativeRenderer = class {
|
|
|
33564
34520
|
}
|
|
33565
34521
|
}
|
|
33566
34522
|
applyPatch(model, patch);
|
|
33567
|
-
this.syncTables(model);
|
|
33568
34523
|
this.scheduler.invalidate(3 /* Light */);
|
|
33569
34524
|
}
|
|
33570
34525
|
removeIndicator(handle) {
|
|
@@ -33583,14 +34538,12 @@ var NativeRenderer = class {
|
|
|
33583
34538
|
this.scene.forgetAnchorOffset(handle.id);
|
|
33584
34539
|
this.scene.dropIndicatorScale(handle.id);
|
|
33585
34540
|
this.inputsUI.remove(handle.id);
|
|
33586
|
-
this.tableOverlays.get(handle.id)?.destroy();
|
|
33587
|
-
this.tableOverlays.delete(handle.id);
|
|
33588
34541
|
this.refreshAxisWidth();
|
|
33589
34542
|
this.paneControls?.refresh();
|
|
33590
34543
|
this.scheduler.invalidate(4 /* Full */);
|
|
33591
34544
|
}
|
|
33592
|
-
setIndicatorInputs(handle, values) {
|
|
33593
|
-
this.inputsUI.setValues(handle.id, values);
|
|
34545
|
+
setIndicatorInputs(handle, values, props) {
|
|
34546
|
+
this.inputsUI.setValues(handle.id, values, props);
|
|
33594
34547
|
}
|
|
33595
34548
|
setSymbolPicker(picker) {
|
|
33596
34549
|
this.symbolPicker = picker;
|
|
@@ -33600,6 +34553,10 @@ var NativeRenderer = class {
|
|
|
33600
34553
|
this.legendActionsProvider = provider;
|
|
33601
34554
|
this.inputsUI?.setLegendActions(provider);
|
|
33602
34555
|
}
|
|
34556
|
+
setLegendCallouts(provider) {
|
|
34557
|
+
this.legendCalloutsProvider = provider;
|
|
34558
|
+
this.inputsUI?.setLegendCallouts(provider);
|
|
34559
|
+
}
|
|
33603
34560
|
setLegendOverviewAction(action) {
|
|
33604
34561
|
this.legendOverviewAction = action;
|
|
33605
34562
|
this.inputsUI?.setLegendOverviewAction(action);
|
|
@@ -33624,8 +34581,6 @@ var NativeRenderer = class {
|
|
|
33624
34581
|
}
|
|
33625
34582
|
if (!visible) {
|
|
33626
34583
|
this.scene.indicators.delete(handle.id);
|
|
33627
|
-
this.tableOverlays.get(handle.id)?.destroy();
|
|
33628
|
-
this.tableOverlays.delete(handle.id);
|
|
33629
34584
|
}
|
|
33630
34585
|
this.inputsUI.setVisible(handle.id, visible);
|
|
33631
34586
|
this.scheduler.invalidate(4 /* Full */);
|
|
@@ -33671,6 +34626,7 @@ var NativeRenderer = class {
|
|
|
33671
34626
|
this.userDrawings?.setLayoutMode(mode);
|
|
33672
34627
|
this.settingsDialog?.setLayoutMode(mode);
|
|
33673
34628
|
this.inputsUI?.setLayoutMode(mode);
|
|
34629
|
+
this.paneControls?.setSuspended(mode === "mobile");
|
|
33674
34630
|
if (this.scrollButton) {
|
|
33675
34631
|
const px = mode === "mobile" ? SCROLL_BTN_SIZE_TOUCH : SCROLL_BTN_SIZE;
|
|
33676
34632
|
this.scrollButton.style.width = `${px}px`;
|
|
@@ -34066,7 +35022,6 @@ var NativeRenderer = class {
|
|
|
34066
35022
|
/** Relayout + repaint + refresh the hover buttons after a collapse/maximize/order change. */
|
|
34067
35023
|
afterPaneLayoutChange() {
|
|
34068
35024
|
this.layoutPanes();
|
|
34069
|
-
this.repositionTables();
|
|
34070
35025
|
this.paneControls?.refresh();
|
|
34071
35026
|
this.scheduler.invalidate(4 /* Full */);
|
|
34072
35027
|
}
|
|
@@ -34147,7 +35102,6 @@ var NativeRenderer = class {
|
|
|
34147
35102
|
above.heightWeight = next.above;
|
|
34148
35103
|
below.heightWeight = next.below;
|
|
34149
35104
|
this.layoutPanes();
|
|
34150
|
-
this.repositionTables();
|
|
34151
35105
|
this.scheduler.invalidate(4 /* Full */);
|
|
34152
35106
|
}
|
|
34153
35107
|
/** Double-click a separator → split the two adjacent panes evenly (each gets half of
|
|
@@ -34162,7 +35116,6 @@ var NativeRenderer = class {
|
|
|
34162
35116
|
above.heightWeight = half;
|
|
34163
35117
|
below.heightWeight = half;
|
|
34164
35118
|
this.layoutPanes();
|
|
34165
|
-
this.repositionTables();
|
|
34166
35119
|
this.scheduler.invalidate(4 /* Full */);
|
|
34167
35120
|
}
|
|
34168
35121
|
// ── keyboard navigation / accessibility (item 11) ──
|
|
@@ -34441,7 +35394,10 @@ var NativeRenderer = class {
|
|
|
34441
35394
|
const liveActual = li >= 0 ? this.bars[li] : void 0;
|
|
34442
35395
|
const easeLive = !!liveActual && this.liveEaseTime === liveActual.time && (liveActual.high !== this.liveEaseHigh || liveActual.low !== this.liveEaseLow || liveActual.close !== this.liveEaseClose);
|
|
34443
35396
|
if (easeLive && liveActual) this.bars[li] = { ...liveActual, high: this.liveEaseHigh, low: this.liveEaseLow, close: this.liveEaseClose };
|
|
34444
|
-
this.scene.drawingSlices =
|
|
35397
|
+
this.scene.drawingSlices = mergeSlices(
|
|
35398
|
+
this.indicatorSlices.prepare(this.scene, this.coords, this.theme, this.dataCanvas),
|
|
35399
|
+
this.userDrawings?.prepareSlices(this.scene.orderedPanes().map((p) => p.id)) ?? /* @__PURE__ */ new Map()
|
|
35400
|
+
);
|
|
34445
35401
|
this.backdropRenderer.render(this.scene, this.coords, this.theme, gridAlpha);
|
|
34446
35402
|
this.backend.render(this.scene, this.coords, this.theme);
|
|
34447
35403
|
this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
|
|
@@ -34580,7 +35536,11 @@ var NativeRenderer = class {
|
|
|
34580
35536
|
}
|
|
34581
35537
|
const models = this.scene.indicatorsForPane(pane.id);
|
|
34582
35538
|
const masterModels = models.filter((m) => m.ownScale !== true);
|
|
34583
|
-
|
|
35539
|
+
let dr = this.chrome.paneDrawingsRange(masterModels, this.scene, pane === pricePane, vr);
|
|
35540
|
+
if (pane === pricePane) {
|
|
35541
|
+
const or = overlaySeriesRange(this.scene.indicators.values(), i0, i1, (id) => this.scene.offsetOf(id));
|
|
35542
|
+
if (or) dr = dr ? { min: Math.min(dr.min, or.min), max: Math.max(dr.max, or.max) } : or;
|
|
35543
|
+
}
|
|
34584
35544
|
const includeCandles = pane.kind === "price" && !this.scene.candlesHidden;
|
|
34585
35545
|
pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
|
|
34586
35546
|
pane.percentBaseline = pane.kind === "price" ? this.bars[i0]?.close ?? 0 : this.firstVisibleValue(masterModels, i0);
|
|
@@ -34854,6 +35814,7 @@ var NativeRenderer = class {
|
|
|
34854
35814
|
for (const m of models) {
|
|
34855
35815
|
const off = this.scene.offsetOf(m.id);
|
|
34856
35816
|
for (const s of m.series) {
|
|
35817
|
+
if (s.overlay === true) continue;
|
|
34857
35818
|
if (isLineLikeSeries(s)) {
|
|
34858
35819
|
const v = s.points[i0 - off]?.value;
|
|
34859
35820
|
if (v != null && Number.isFinite(v)) return v;
|
|
@@ -34874,27 +35835,6 @@ var NativeRenderer = class {
|
|
|
34874
35835
|
}
|
|
34875
35836
|
return maxVol;
|
|
34876
35837
|
}
|
|
34877
|
-
/** Create/update/destroy an indicator's DOM table overlay (anchored off real pane geometry). */
|
|
34878
|
-
syncTables(model) {
|
|
34879
|
-
const tables = model.tables ?? [];
|
|
34880
|
-
let overlay = this.tableOverlays.get(model.id);
|
|
34881
|
-
if (tables.length === 0) {
|
|
34882
|
-
if (overlay) {
|
|
34883
|
-
overlay.destroy();
|
|
34884
|
-
this.tableOverlays.delete(model.id);
|
|
34885
|
-
}
|
|
34886
|
-
return;
|
|
34887
|
-
}
|
|
34888
|
-
if (!overlay) {
|
|
34889
|
-
overlay = new TableOverlay(this.plot, this.theme, (id) => this.paneBoundsFor(id));
|
|
34890
|
-
overlay.setVisible(this.loadingEl === null);
|
|
34891
|
-
this.tableOverlays.set(model.id, overlay);
|
|
34892
|
-
}
|
|
34893
|
-
overlay.update(tables);
|
|
34894
|
-
}
|
|
34895
|
-
repositionTables() {
|
|
34896
|
-
for (const overlay of this.tableOverlays.values()) overlay.reposition();
|
|
34897
|
-
}
|
|
34898
35838
|
layoutPanes() {
|
|
34899
35839
|
const panes = this.scene.orderedPanes();
|
|
34900
35840
|
const dataHeight = this.coords.height;
|
|
@@ -34945,6 +35885,7 @@ var NativeRenderer = class {
|
|
|
34945
35885
|
const visible = maxPane ? [maxPane] : this.scene.orderedPanes().filter((p) => !p.collapsed);
|
|
34946
35886
|
const paneBottom = visible.length ? Math.max(...visible.map((p) => p.bounds.top + p.bounds.height)) : dataHeight;
|
|
34947
35887
|
this.scrollBtnBottomPx = SCROLL_BTN_BOTTOM + Math.max(0, dataHeight - paneBottom);
|
|
35888
|
+
this.mountContainer?.style.setProperty("--vela-bottom-gutter", `${TIME_AXIS_H + Math.max(0, dataHeight - paneBottom)}px`);
|
|
34948
35889
|
this.scrollBtnRightPx = this.rightAxisW + SCROLL_BTN_RIGHT_INSET;
|
|
34949
35890
|
if (this.scrollButton) {
|
|
34950
35891
|
this.scrollButton.style.bottom = `${this.scrollBtnBottomPx}px`;
|
|
@@ -35037,30 +35978,33 @@ var NativeRenderer = class {
|
|
|
35037
35978
|
if (w <= 0 || h <= 0) return;
|
|
35038
35979
|
const dpr = window.devicePixelRatio || 1;
|
|
35039
35980
|
this.plot.style.left = `${this.toolbarGutter}px`;
|
|
35040
|
-
const
|
|
35041
|
-
|
|
35042
|
-
|
|
35043
|
-
|
|
35044
|
-
|
|
35045
|
-
|
|
35046
|
-
|
|
35047
|
-
|
|
35048
|
-
|
|
35049
|
-
|
|
35050
|
-
|
|
35051
|
-
|
|
35052
|
-
|
|
35053
|
-
|
|
35054
|
-
|
|
35055
|
-
|
|
35056
|
-
this.
|
|
35057
|
-
this.
|
|
35058
|
-
this.
|
|
35059
|
-
|
|
35981
|
+
const rect = this.plot.getBoundingClientRect();
|
|
35982
|
+
let bw = Math.max(1, Math.round(rect.width * dpr));
|
|
35983
|
+
let bh = Math.max(1, Math.round(rect.height * dpr));
|
|
35984
|
+
const dev = this.plotDeviceSize;
|
|
35985
|
+
if (dev && Math.abs(dev.width - rect.width * dpr) <= 1 && Math.abs(dev.height - rect.height * dpr) <= 1) {
|
|
35986
|
+
bw = Math.max(1, dev.width);
|
|
35987
|
+
bh = Math.max(1, dev.height);
|
|
35988
|
+
}
|
|
35989
|
+
const pw = bw / dpr;
|
|
35990
|
+
const ph = bh / dpr;
|
|
35991
|
+
const size = (canvas) => {
|
|
35992
|
+
canvas.width = bw;
|
|
35993
|
+
canvas.height = bh;
|
|
35994
|
+
canvas.style.width = `${pw}px`;
|
|
35995
|
+
canvas.style.height = `${ph}px`;
|
|
35996
|
+
};
|
|
35997
|
+
size(this.dataCanvas);
|
|
35998
|
+
size(this.backdropCanvas);
|
|
35999
|
+
size(this.volumeCanvas);
|
|
36000
|
+
for (const l of this.extLayers) size(l.canvas);
|
|
36001
|
+
size(this.vpvrCanvas);
|
|
36002
|
+
size(this.chromeCanvas);
|
|
36003
|
+
size(this.drawingsCanvas);
|
|
36004
|
+
size(this.cursorCanvas);
|
|
35060
36005
|
this.coords.setSize(Math.max(1, pw - this.rightAxisW), Math.max(1, ph - TIME_AXIS_H), dpr);
|
|
35061
36006
|
this.scene.crosshair = null;
|
|
35062
36007
|
this.layoutPanes();
|
|
35063
|
-
this.repositionTables();
|
|
35064
36008
|
this.userDrawings?.onResize();
|
|
35065
36009
|
if (!this.didInitialFit && this.coords.barCount > 0) {
|
|
35066
36010
|
this.fitContent();
|
|
@@ -35726,45 +36670,24 @@ var CSS21 = `
|
|
|
35726
36670
|
}
|
|
35727
36671
|
.vela-statusline .vela-sl-symbol { font-weight: 600; font-size: var(--vela-font-size-lg); }
|
|
35728
36672
|
.vela-statusline .vela-sl-meta { color: var(--vela-fg-muted); font-size: var(--vela-font-size-md); font-weight: 600; }
|
|
35729
|
-
/* Market status badge \u2014 icon-only 16px circle, label on hover
|
|
35730
|
-
|
|
35731
|
-
|
|
35732
|
-
place-items: center;
|
|
35733
|
-
width: 16px;
|
|
35734
|
-
height: 16px;
|
|
35735
|
-
border-radius: 50%;
|
|
35736
|
-
flex: none;
|
|
35737
|
-
align-self: center;
|
|
35738
|
-
line-height: 0;
|
|
35739
|
-
cursor: default;
|
|
35740
|
-
}
|
|
35741
|
-
.vela-statusline .vela-sl-market svg {
|
|
35742
|
-
width: 12px;
|
|
35743
|
-
height: 12px;
|
|
35744
|
-
display: block;
|
|
35745
|
-
}
|
|
35746
|
-
/* Open wears the theme's up color; the other sessions are meaning constants from the
|
|
35747
|
-
* palette (amber pre, sky post, gray closed/holiday). Same ink at 20% for the circle. */
|
|
35748
|
-
.vela-statusline .vela-sl-market[data-status='open'] {
|
|
35749
|
-
background: color-mix(in srgb, var(--vela-up) 20%, transparent);
|
|
35750
|
-
color: var(--vela-up);
|
|
35751
|
-
}
|
|
35752
|
-
.vela-statusline .vela-sl-market[data-status='pre'] { background: color-mix(in srgb, ${SESSION_PRE} 20%, transparent); color: ${SESSION_PRE}; }
|
|
35753
|
-
.vela-statusline .vela-sl-market[data-status='post'] { background: color-mix(in srgb, ${SESSION_POST} 20%, transparent); color: ${SESSION_POST}; }
|
|
35754
|
-
.vela-statusline .vela-sl-market[data-status='closed'],
|
|
35755
|
-
.vela-statusline .vela-sl-market[data-status='holiday'] { background: color-mix(in srgb, ${SESSION_OFF} 20%, transparent); color: ${SESSION_OFF}; }
|
|
36673
|
+
/* Market status badge \u2014 a kit callout bubble (icon-only 16px circle, label on hover
|
|
36674
|
+
* via the kit tooltip); the session tint is applied per status in setMarketStatus. */
|
|
36675
|
+
.vela-statusline .vela-sl-market { align-self: center; }
|
|
35756
36676
|
.vela-statusline .vela-sl-ohlc { display: flex; gap: var(--vela-space-1); color: var(--vela-fg-muted); }
|
|
35757
36677
|
.vela-statusline .vela-sl-ohlc b { color: var(--vela-fg); font-weight: 500; }
|
|
35758
36678
|
/* The change value wears the SAME ink as the OHLC values (set inline per render) \u2014
|
|
35759
36679
|
* these are the pre-ink fallbacks only. */
|
|
35760
36680
|
.vela-statusline .vela-sl-change[data-dir='up'] { color: var(--vela-up); }
|
|
35761
36681
|
.vela-statusline .vela-sl-change[data-dir='down'] { color: var(--vela-down); }
|
|
35762
|
-
/* Stack the
|
|
35763
|
-
* The renderer
|
|
35764
|
-
*
|
|
35765
|
-
*
|
|
35766
|
-
*
|
|
35767
|
-
|
|
36682
|
+
/* Stack the TOP pane's legend below the status line (lower study panes stay put).
|
|
36683
|
+
* The renderer marks whichever legend sits at the plot's top edge \u2014 the price pane
|
|
36684
|
+
* normally, or a maximized study pane filling the plot \u2014 so the legend never merges
|
|
36685
|
+
* with the status line whichever pane owns the top. The renderer sets the legend's
|
|
36686
|
+
* inline top \u2014 shift with a transform, don't fight it. Scoped to hosts that actually
|
|
36687
|
+
* CARRY a status line (the marker class set by the Statusline constructor) \u2014 the
|
|
36688
|
+
* stylesheet is document-global, so a bare attribute selector here would shift every
|
|
36689
|
+
* chart on the page, including statusline-less ones. */
|
|
36690
|
+
.vela-has-statusline [${LEGEND_AT_TOP_ATTR}] { transform: translateY(26px); }
|
|
35768
36691
|
/* Mobile: two-line chip \u2014 logo / symbol / meta / market status on one aligned row, the
|
|
35769
36692
|
* bar change on the next. Full O/H/L/C stays hidden (too dense on a phone-width plot).
|
|
35770
36693
|
* GRID, not a wrapping flexbox: an absolutely positioned wrapping flex container sizes
|
|
@@ -35800,7 +36723,7 @@ var CSS21 = `
|
|
|
35800
36723
|
font-size: var(--vela-font-size-sm);
|
|
35801
36724
|
line-height: 1.2;
|
|
35802
36725
|
}
|
|
35803
|
-
[data-layout='mobile'] .vela-has-statusline [
|
|
36726
|
+
[data-layout='mobile'] .vela-has-statusline [${LEGEND_AT_TOP_ATTR}] { transform: translateY(40px); }
|
|
35804
36727
|
/* FIT mode (multi-chart cells \u2014 see setFitMode): the line never wraps; segments that
|
|
35805
36728
|
* don't fit are HIDDEN by fit() (change first, then meta, then the market badge), so
|
|
35806
36729
|
* overflow:hidden only guards the transient between a resize and the next measure.
|
|
@@ -35817,7 +36740,7 @@ var CSS21 = `
|
|
|
35817
36740
|
padding-left: 0;
|
|
35818
36741
|
}
|
|
35819
36742
|
/* One row again \u2014 the mobile two-line shift doesn't apply in fit mode. */
|
|
35820
|
-
[data-layout='mobile'] .vela-sl-fit-host.vela-has-statusline [
|
|
36743
|
+
[data-layout='mobile'] .vela-sl-fit-host.vela-has-statusline [${LEGEND_AT_TOP_ATTR}] { transform: translateY(26px); }
|
|
35821
36744
|
`;
|
|
35822
36745
|
function baseOfTicker(ticker) {
|
|
35823
36746
|
return ticker.replace(/[-_/]?(USDT|USDC|USD1|USDS|BUSD|USD|EUR|PERP)$/i, "") || ticker;
|
|
@@ -35829,6 +36752,13 @@ var MARKET_LABELS = {
|
|
|
35829
36752
|
closed: "Market Closed",
|
|
35830
36753
|
holiday: "Market Holiday"
|
|
35831
36754
|
};
|
|
36755
|
+
var MARKET_INKS = {
|
|
36756
|
+
open: "var(--vela-up)",
|
|
36757
|
+
pre: SESSION_PRE,
|
|
36758
|
+
post: SESSION_POST,
|
|
36759
|
+
closed: SESSION_OFF,
|
|
36760
|
+
holiday: SESSION_OFF
|
|
36761
|
+
};
|
|
35832
36762
|
function statuslineInkOf(renderer, priceStyle) {
|
|
35833
36763
|
const cfg = renderer.getConfig();
|
|
35834
36764
|
const c = (v) => typeof v === "string" ? v : null;
|
|
@@ -35887,8 +36817,15 @@ var Statusline = class {
|
|
|
35887
36817
|
this.symbolEl.textContent = ticker;
|
|
35888
36818
|
this.metaEl = doc.createElement("span");
|
|
35889
36819
|
this.metaEl.className = "vela-sl-meta";
|
|
35890
|
-
this.
|
|
35891
|
-
|
|
36820
|
+
this.marketBubble = new CalloutBubble({
|
|
36821
|
+
icon: "market-open",
|
|
36822
|
+
background: `color-mix(in srgb, ${MARKET_INKS.open} 20%, transparent)`,
|
|
36823
|
+
color: MARKET_INKS.open,
|
|
36824
|
+
label: MARKET_LABELS.open,
|
|
36825
|
+
host
|
|
36826
|
+
});
|
|
36827
|
+
this.marketEl = this.marketBubble.el;
|
|
36828
|
+
this.marketEl.classList.add("vela-sl-market");
|
|
35892
36829
|
this.ohlcEl = doc.createElement("span");
|
|
35893
36830
|
this.ohlcEl.className = "vela-sl-ohlc";
|
|
35894
36831
|
this.changeEl = doc.createElement("span");
|
|
@@ -35976,8 +36913,13 @@ var Statusline = class {
|
|
|
35976
36913
|
* hover label. Callers with no session model leave the constructor's 'open'. */
|
|
35977
36914
|
setMarketStatus(status) {
|
|
35978
36915
|
this.marketEl.dataset.status = status;
|
|
35979
|
-
|
|
35980
|
-
this.
|
|
36916
|
+
const ink = MARKET_INKS[status];
|
|
36917
|
+
this.marketBubble.set({
|
|
36918
|
+
icon: `market-${status}`,
|
|
36919
|
+
background: `color-mix(in srgb, ${ink} 20%, transparent)`,
|
|
36920
|
+
color: ink,
|
|
36921
|
+
label: MARKET_LABELS[status]
|
|
36922
|
+
});
|
|
35981
36923
|
this.marketTip.setContent(MARKET_LABELS[status]);
|
|
35982
36924
|
}
|
|
35983
36925
|
setPartVisible(part, visible) {
|
|
@@ -36010,6 +36952,7 @@ var Statusline = class {
|
|
|
36010
36952
|
this.fitRO?.disconnect();
|
|
36011
36953
|
this.fitRO = null;
|
|
36012
36954
|
this.marketTip.destroy();
|
|
36955
|
+
this.marketBubble.destroy();
|
|
36013
36956
|
this.host.classList.remove("vela-has-statusline", "vela-sl-fit-host");
|
|
36014
36957
|
this.el.remove();
|
|
36015
36958
|
}
|
|
@@ -36236,6 +37179,8 @@ var SessionShadingTracker = class {
|
|
|
36236
37179
|
this.spec = null;
|
|
36237
37180
|
this.ready = false;
|
|
36238
37181
|
this.session = "regular";
|
|
37182
|
+
/** Whether one bar is shorter than a civil day — only then do pre/post bars exist. */
|
|
37183
|
+
this.intraday = false;
|
|
36239
37184
|
this.covered = null;
|
|
36240
37185
|
/** The newest range seen — viewport moves during metadata resolution (a load's fit
|
|
36241
37186
|
* animation) must not be lost, so the resolution always expands the LATEST range. */
|
|
@@ -36248,6 +37193,8 @@ var SessionShadingTracker = class {
|
|
|
36248
37193
|
this.spec = null;
|
|
36249
37194
|
this.covered = null;
|
|
36250
37195
|
this.session = opts.session;
|
|
37196
|
+
const tfMs = timeframeMs(opts.timeframe);
|
|
37197
|
+
this.intraday = Number.isFinite(tfMs) && tfMs < DAY_MS2;
|
|
36251
37198
|
this.lastRange = opts.range;
|
|
36252
37199
|
void data.symbolInfo(symbol).catch(() => void 0).then((si) => {
|
|
36253
37200
|
if (my !== this.epoch) return;
|
|
@@ -36275,7 +37222,7 @@ var SessionShadingTracker = class {
|
|
|
36275
37222
|
if (force) this.onZones(null);
|
|
36276
37223
|
return;
|
|
36277
37224
|
}
|
|
36278
|
-
if (this.session !== "extended") {
|
|
37225
|
+
if (this.session !== "extended" || !this.intraday) {
|
|
36279
37226
|
if (force) this.onZones({ pre: [], post: [] });
|
|
36280
37227
|
return;
|
|
36281
37228
|
}
|
|
@@ -36376,9 +37323,168 @@ var Watermark = class {
|
|
|
36376
37323
|
}
|
|
36377
37324
|
};
|
|
36378
37325
|
|
|
37326
|
+
// src/widget/cell-controls.ts
|
|
37327
|
+
var CELL_CONTROLS_PROXIMITY_PX = 120;
|
|
37328
|
+
var TIME_AXIS_H2 = 22;
|
|
37329
|
+
var CONTROLS_BOTTOM_PX = TIME_AXIS_H2 + 12;
|
|
37330
|
+
var CLUSTER_H2 = 24;
|
|
37331
|
+
var CLUSTER_PILL2 = "rgba(0,0,0,0.65)";
|
|
37332
|
+
var STYLE_ID26 = "vela-cell-controls";
|
|
37333
|
+
var CSS23 = `
|
|
37334
|
+
.vela-cc-btn{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;padding:0;border:none;border-radius:var(--vela-radius-sm);background:transparent;line-height:0;font-size:12px;color:var(--vela-fg-muted);cursor:pointer;}
|
|
37335
|
+
.vela-cc-btn svg{display:block;}
|
|
37336
|
+
.vela-cc-btn:hover{background:var(--vela-active);color:var(--vela-fg-bright);}
|
|
37337
|
+
.vela-cc-on,.vela-cc-on:hover{background:var(--vela-selected-bg);color:var(--vela-selected-fg);}
|
|
37338
|
+
.vela-cc-grip{cursor:grab;touch-action:none;}
|
|
37339
|
+
.vela-cc-grip:active{cursor:grabbing;}
|
|
37340
|
+
`;
|
|
37341
|
+
function nearBottomCenter(x, y, width, height, proximityPx = CELL_CONTROLS_PROXIMITY_PX) {
|
|
37342
|
+
const cx = width / 2;
|
|
37343
|
+
const cy = height - CONTROLS_BOTTOM_PX - CLUSTER_H2 / 2;
|
|
37344
|
+
return Math.hypot(x - cx, y - cy) <= proximityPx;
|
|
37345
|
+
}
|
|
37346
|
+
var CellControls = class {
|
|
37347
|
+
constructor(host, deps) {
|
|
37348
|
+
this.host = host;
|
|
37349
|
+
this.deps = deps;
|
|
37350
|
+
this.near = false;
|
|
37351
|
+
/** A grip drag is underway — the proximity reveal must not hide the cluster
|
|
37352
|
+
* while captured pointer moves sweep across the whole grid. */
|
|
37353
|
+
this.dragging = false;
|
|
37354
|
+
/** Mobile: the proximity reveal is meaningless without a cursor — the mobile
|
|
37355
|
+
* bar's maximize stop replaces the cluster. */
|
|
37356
|
+
this.suspended = false;
|
|
37357
|
+
this.onHostMove = (e) => {
|
|
37358
|
+
if (this.suspended) return;
|
|
37359
|
+
if (this.dragging) return;
|
|
37360
|
+
const rect = this.host.getBoundingClientRect();
|
|
37361
|
+
this.setNear(nearBottomCenter(e.clientX - rect.left, e.clientY - rect.top, rect.width, rect.height));
|
|
37362
|
+
};
|
|
37363
|
+
this.onHostLeave = () => {
|
|
37364
|
+
if (this.dragging) return;
|
|
37365
|
+
this.setNear(false);
|
|
37366
|
+
};
|
|
37367
|
+
injectStyles(STYLE_ID26, CSS23, host.ownerDocument);
|
|
37368
|
+
this.glider = new Glider(deps.chart);
|
|
37369
|
+
this.root = host.ownerDocument.createElement("div");
|
|
37370
|
+
Object.assign(this.root.style, {
|
|
37371
|
+
position: "absolute",
|
|
37372
|
+
left: "50%",
|
|
37373
|
+
bottom: `${CONTROLS_BOTTOM_PX}px`,
|
|
37374
|
+
transform: "translateX(-50%)",
|
|
37375
|
+
zIndex: "6",
|
|
37376
|
+
display: "none",
|
|
37377
|
+
// revealed by cursor proximity (onHostMove)
|
|
37378
|
+
gap: "2px",
|
|
37379
|
+
padding: "2px",
|
|
37380
|
+
borderRadius: "var(--vela-radius-md)",
|
|
37381
|
+
background: CLUSTER_PILL2,
|
|
37382
|
+
pointerEvents: "auto"
|
|
37383
|
+
});
|
|
37384
|
+
this.host.addEventListener("pointermove", this.onHostMove);
|
|
37385
|
+
this.host.addEventListener("pointerleave", this.onHostLeave);
|
|
37386
|
+
this.host.appendChild(this.root);
|
|
37387
|
+
this.refresh();
|
|
37388
|
+
}
|
|
37389
|
+
/** Rebuild the buttons (the multi-cell gate or the maximized state changed). */
|
|
37390
|
+
refresh() {
|
|
37391
|
+
this.root.textContent = "";
|
|
37392
|
+
const multi = this.deps.multiCell();
|
|
37393
|
+
const maximized = multi && this.deps.isMaximized();
|
|
37394
|
+
if (multi && !maximized) this.root.appendChild(this.makeGrip());
|
|
37395
|
+
this.root.appendChild(this.button("minus", "Zoom out", () => this.glider.zoom(ZOOM_OUT)));
|
|
37396
|
+
this.root.appendChild(this.button("plus", "Zoom in", () => this.glider.zoom(ZOOM_IN)));
|
|
37397
|
+
if (multi) {
|
|
37398
|
+
this.root.appendChild(
|
|
37399
|
+
this.button(maximized ? "restore" : "maximize", maximized ? "Restore layout" : "Maximize chart", () => this.deps.toggleMaximize(), {
|
|
37400
|
+
// The maximized state reads as an inverse chip (white-on-dark, dark-on-light),
|
|
37401
|
+
// the same active-state affordance as a collapsed pane's expand button.
|
|
37402
|
+
selected: maximized
|
|
37403
|
+
})
|
|
37404
|
+
);
|
|
37405
|
+
}
|
|
37406
|
+
this.root.appendChild(
|
|
37407
|
+
this.button("reset", "Reset chart", () => {
|
|
37408
|
+
this.glider.stop();
|
|
37409
|
+
this.deps.reset();
|
|
37410
|
+
})
|
|
37411
|
+
);
|
|
37412
|
+
}
|
|
37413
|
+
button(iconId, title, onClick, opts = {}) {
|
|
37414
|
+
const b = this.host.ownerDocument.createElement("button");
|
|
37415
|
+
b.type = "button";
|
|
37416
|
+
b.title = title;
|
|
37417
|
+
b.setAttribute("aria-label", title);
|
|
37418
|
+
b.className = opts.selected === true ? "vela-cc-btn vela-cc-on" : "vela-cc-btn";
|
|
37419
|
+
b.innerHTML = icon(iconId);
|
|
37420
|
+
b.addEventListener("click", (e) => {
|
|
37421
|
+
e.stopPropagation();
|
|
37422
|
+
onClick();
|
|
37423
|
+
});
|
|
37424
|
+
return b;
|
|
37425
|
+
}
|
|
37426
|
+
/** The drag handle (2×3 dot grip): press and drag onto another cell to trade
|
|
37427
|
+
* slots with it. The preview highlight follows the pointer; releasing outside
|
|
37428
|
+
* any other cell cancels. */
|
|
37429
|
+
makeGrip() {
|
|
37430
|
+
const b = this.host.ownerDocument.createElement("button");
|
|
37431
|
+
b.type = "button";
|
|
37432
|
+
b.title = "Drag to move chart";
|
|
37433
|
+
b.setAttribute("aria-label", "Drag to move chart");
|
|
37434
|
+
b.className = "vela-cc-btn vela-cc-grip";
|
|
37435
|
+
b.innerHTML = icon("grip");
|
|
37436
|
+
b.addEventListener("pointerdown", (e) => this.onGripDown(b, e));
|
|
37437
|
+
return b;
|
|
37438
|
+
}
|
|
37439
|
+
onGripDown(btn2, e) {
|
|
37440
|
+
if (e.button !== 0 && e.pointerType === "mouse") return;
|
|
37441
|
+
e.preventDefault();
|
|
37442
|
+
e.stopPropagation();
|
|
37443
|
+
try {
|
|
37444
|
+
btn2.setPointerCapture(e.pointerId);
|
|
37445
|
+
} catch {
|
|
37446
|
+
}
|
|
37447
|
+
this.dragging = true;
|
|
37448
|
+
let target = null;
|
|
37449
|
+
const move = (ev) => {
|
|
37450
|
+
target = this.deps.dragTargetAt(ev.clientX, ev.clientY);
|
|
37451
|
+
this.deps.previewDrop(target);
|
|
37452
|
+
};
|
|
37453
|
+
const finish = (commit) => () => {
|
|
37454
|
+
this.dragging = false;
|
|
37455
|
+
this.deps.previewDrop(null);
|
|
37456
|
+
btn2.removeEventListener("pointermove", move);
|
|
37457
|
+
btn2.removeEventListener("pointerup", onUp);
|
|
37458
|
+
btn2.removeEventListener("pointercancel", onCancel);
|
|
37459
|
+
if (commit && target != null) this.deps.dropOn(target);
|
|
37460
|
+
};
|
|
37461
|
+
const onUp = finish(true);
|
|
37462
|
+
const onCancel = finish(false);
|
|
37463
|
+
btn2.addEventListener("pointermove", move);
|
|
37464
|
+
btn2.addEventListener("pointerup", onUp);
|
|
37465
|
+
btn2.addEventListener("pointercancel", onCancel);
|
|
37466
|
+
}
|
|
37467
|
+
/** Mobile flips the cluster off entirely (and hides it if currently revealed). */
|
|
37468
|
+
setSuspended(on) {
|
|
37469
|
+
this.suspended = on;
|
|
37470
|
+
if (on) this.setNear(false);
|
|
37471
|
+
}
|
|
37472
|
+
setNear(near) {
|
|
37473
|
+
if (near === this.near) return;
|
|
37474
|
+
this.near = near;
|
|
37475
|
+
this.root.style.display = near ? "flex" : "none";
|
|
37476
|
+
}
|
|
37477
|
+
destroy() {
|
|
37478
|
+
this.glider.stop();
|
|
37479
|
+
this.host.removeEventListener("pointermove", this.onHostMove);
|
|
37480
|
+
this.host.removeEventListener("pointerleave", this.onHostLeave);
|
|
37481
|
+
this.root.remove();
|
|
37482
|
+
}
|
|
37483
|
+
};
|
|
37484
|
+
|
|
36379
37485
|
// src/widget/context-menu.ts
|
|
36380
37486
|
var PRICE_AXIS_W = 60;
|
|
36381
|
-
var
|
|
37487
|
+
var TIME_AXIS_H3 = 26;
|
|
36382
37488
|
var ChartContextMenu = class {
|
|
36383
37489
|
constructor(host, cbs) {
|
|
36384
37490
|
this.cbs = cbs;
|
|
@@ -36417,7 +37523,7 @@ var ChartContextMenu = class {
|
|
|
36417
37523
|
zoneOf(e) {
|
|
36418
37524
|
const rect = this.host.getBoundingClientRect();
|
|
36419
37525
|
if (e.clientX - rect.left > rect.width - PRICE_AXIS_W) return "price-axis";
|
|
36420
|
-
if (e.clientY - rect.top > rect.height -
|
|
37526
|
+
if (e.clientY - rect.top > rect.height - TIME_AXIS_H3) return "time-axis";
|
|
36421
37527
|
return "body";
|
|
36422
37528
|
}
|
|
36423
37529
|
/** The pane under the pointer, so every pane's price scale has its own menu. */
|
|
@@ -36690,6 +37796,7 @@ var ChartCell = class {
|
|
|
36690
37796
|
this.inner.renderer.set("attribution", false);
|
|
36691
37797
|
this.inner.renderer.set("dialogHost", deps.dialogHost);
|
|
36692
37798
|
this.inner.renderer.setLegendActions(legendActionsProviderFor(this.inner, () => deps.context()));
|
|
37799
|
+
this.inner.renderer.setLegendCallouts(legendCalloutsProviderFor(this.inner, () => deps.context()));
|
|
36693
37800
|
if (this.inner.renderer.supports("historyChords")) this.inner.renderer.set("historyChords", false);
|
|
36694
37801
|
this.history.onChart(this.inner);
|
|
36695
37802
|
this.inner.renderer.onConfigChanged(() => {
|
|
@@ -36750,11 +37857,18 @@ var ChartCell = class {
|
|
|
36750
37857
|
if (this.inner && this.state.symbol) this.marketStatus?.track(this.inner.data, this.state.symbol);
|
|
36751
37858
|
});
|
|
36752
37859
|
this.syncStatuslineColors();
|
|
37860
|
+
this.cellControls = new CellControls(this.host, {
|
|
37861
|
+
chart: () => this.inner,
|
|
37862
|
+
reset: () => this.resetView(),
|
|
37863
|
+
multiCell: () => deps.multiCell(),
|
|
37864
|
+
isMaximized: () => deps.isMaximized(id),
|
|
37865
|
+
toggleMaximize: () => deps.toggleMaximize(id),
|
|
37866
|
+
dragTargetAt: (x, y) => deps.cellDragTarget(id, x, y),
|
|
37867
|
+
previewDrop: (target) => deps.previewDropTarget(target),
|
|
37868
|
+
dropOn: (target) => deps.dropCell(id, target)
|
|
37869
|
+
});
|
|
36753
37870
|
this.contextMenu = new ChartContextMenu(this.host, {
|
|
36754
|
-
resetView: () =>
|
|
36755
|
-
this.inner?.renderer.set("autoScale", true);
|
|
36756
|
-
this.inner?.setVisibleRangePreset("ALL");
|
|
36757
|
-
},
|
|
37871
|
+
resetView: () => this.resetView(),
|
|
36758
37872
|
timezone: () => this.deps.timezone(),
|
|
36759
37873
|
setTimezone: (zone) => this.deps.setTimezone(zone),
|
|
36760
37874
|
// Right-clicking activates the cell first (capture-phase pointerdown), so the
|
|
@@ -36806,20 +37920,30 @@ var ChartCell = class {
|
|
|
36806
37920
|
this.refreshNativeCatalog();
|
|
36807
37921
|
this.pushSettingsSections();
|
|
36808
37922
|
this.offMarket = this.inner.on("market:changed", ({ symbol: symbol2, timeframe }) => {
|
|
36809
|
-
this.
|
|
36810
|
-
this.state.provider = parseSymbol(symbol2).provider ?? void 0;
|
|
36811
|
-
this.state.timeframe = timeframe;
|
|
36812
|
-
this.state.session = normalizeSession(this.inner?.market.session);
|
|
36813
|
-
this.watermark?.update(symbol2, timeframe);
|
|
36814
|
-
this.statusline?.setSymbol(symbol2);
|
|
36815
|
-
this.statusline?.setMeta(timeframe, this.inner?.data.displayPrefix(symbol2) ?? this.state.provider ?? "");
|
|
36816
|
-
if (this.inner) this.statusline?.onChart(this.inner);
|
|
37923
|
+
this.projectMarket(symbol2, timeframe);
|
|
36817
37924
|
this.refreshNativeCatalog();
|
|
36818
37925
|
this.refreshSessionAvailable();
|
|
36819
37926
|
if (this.inner) this.marketStatus?.track(this.inner.data, symbol2);
|
|
36820
37927
|
this.deps.onMarketChanged(this.id);
|
|
36821
37928
|
});
|
|
36822
37929
|
}
|
|
37930
|
+
/**
|
|
37931
|
+
* Project a market identity into the cell state and its display overlays (watermark,
|
|
37932
|
+
* statusline), WITHOUT the data-dependent bookkeeping. Runs twice per user pick: once
|
|
37933
|
+
* optimistically from the cell setters — the labels reflect the pick immediately, not
|
|
37934
|
+
* after the bars load — and again from `market:changed` (the committed pass, and the
|
|
37935
|
+
* only pass for host `chart.setMarket` calls). Idempotent, so the double run converges.
|
|
37936
|
+
*/
|
|
37937
|
+
projectMarket(symbol, timeframe) {
|
|
37938
|
+
this.state.symbol = symbol;
|
|
37939
|
+
this.state.provider = parseSymbol(symbol).provider ?? void 0;
|
|
37940
|
+
this.state.timeframe = timeframe;
|
|
37941
|
+
this.state.session = normalizeSession(this.inner?.market.session);
|
|
37942
|
+
this.watermark?.update(symbol, timeframe);
|
|
37943
|
+
this.statusline?.setSymbol(symbol);
|
|
37944
|
+
this.statusline?.setMeta(timeframe, this.inner?.data.displayPrefix(symbol) ?? this.state.provider ?? "");
|
|
37945
|
+
if (this.inner) this.statusline?.onChart(this.inner);
|
|
37946
|
+
}
|
|
36823
37947
|
/**
|
|
36824
37948
|
* Does this cell's market HAVE sessions (RTH/ETH meaningful)? Derived from the
|
|
36825
37949
|
* symbol's own metadata (`syminfo.session !== '24x7'`), asynchronously — the
|
|
@@ -36865,7 +37989,7 @@ var ChartCell = class {
|
|
|
36865
37989
|
const requestedSpan = Math.max(this.state.bars ?? 1e3, this.rangeBars) * timeframeMs(this.state.timeframe ?? "60");
|
|
36866
37990
|
const fallbackSpan = Number.isFinite(requestedSpan) ? Math.max(3 * 864e5, requestedSpan) : 3 * 864e5;
|
|
36867
37991
|
const range = chart.getVisibleRange() ?? { from: now - fallbackSpan, to: now };
|
|
36868
|
-
this.sessionShading.track(chart.data, symbol, { session: this.session, range });
|
|
37992
|
+
this.sessionShading.track(chart.data, symbol, { session: this.session, timeframe: this.timeframe, range });
|
|
36869
37993
|
}
|
|
36870
37994
|
/** The session-shade colors live in the renderer CONFIG (persisted with it, edited
|
|
36871
37995
|
* live by the dialog swatch) — the cell only proxies them into its settings rows. */
|
|
@@ -37054,17 +38178,23 @@ var ChartCell = class {
|
|
|
37054
38178
|
get indicatorCount() {
|
|
37055
38179
|
return this.instances.length + this.nativeCatalog.filter((n) => n.present).length;
|
|
37056
38180
|
}
|
|
37057
|
-
/** Switch this cell's market in place (the chart instance survives).
|
|
38181
|
+
/** Switch this cell's market in place (the chart instance survives). The projection
|
|
38182
|
+
* is OPTIMISTIC — labels and chrome show the pick before the bars load; it follows
|
|
38183
|
+
* the setMarket call so the statusline reads the already-blanked chart. */
|
|
37058
38184
|
setSymbol(symbol) {
|
|
37059
38185
|
if (!this.inner || symbol === this.symbol) return;
|
|
37060
38186
|
this.unresolvedToasted = null;
|
|
37061
38187
|
void this.inner.setMarket({ symbol });
|
|
38188
|
+
this.projectMarket(symbol, this.timeframe);
|
|
38189
|
+
this.deps.onMarketChanged(this.id);
|
|
37062
38190
|
}
|
|
37063
38191
|
setTimeframe(timeframe) {
|
|
37064
38192
|
if (!this.inner || timeframe === this.timeframe) return;
|
|
37065
38193
|
this.activeRangeId = null;
|
|
37066
38194
|
this.rangeBars = 0;
|
|
37067
38195
|
void this.inner.setMarket({ timeframe, bars: this.state.bars });
|
|
38196
|
+
this.projectMarket(this.symbol, timeframe);
|
|
38197
|
+
this.deps.onMarketChanged(this.id);
|
|
37068
38198
|
}
|
|
37069
38199
|
/** Applied live (renderer feature) — no reload. */
|
|
37070
38200
|
setPriceStyle(style) {
|
|
@@ -37112,6 +38242,20 @@ var ChartCell = class {
|
|
|
37112
38242
|
this.inner.setVisibleRangePreset(preset.preset);
|
|
37113
38243
|
}
|
|
37114
38244
|
}
|
|
38245
|
+
/** Reset this cell's view: re-enable auto scale and frame the full history —
|
|
38246
|
+
* the same action the chart context menu offers. */
|
|
38247
|
+
resetView() {
|
|
38248
|
+
this.inner?.renderer.set("autoScale", true);
|
|
38249
|
+
this.inner?.setVisibleRangePreset("ALL");
|
|
38250
|
+
}
|
|
38251
|
+
/** Rebuild the view-controls cluster (the maximize gate or state changed). */
|
|
38252
|
+
refreshControls() {
|
|
38253
|
+
this.cellControls.refresh();
|
|
38254
|
+
}
|
|
38255
|
+
/** Mobile flips the per-cell cluster off (the shell's mobile bar replaces it). */
|
|
38256
|
+
setControlsSuspended(on) {
|
|
38257
|
+
this.cellControls.setSuspended(on);
|
|
38258
|
+
}
|
|
37115
38259
|
/** Make this cell the active one and put keyboard focus on its chart surface. */
|
|
37116
38260
|
focus() {
|
|
37117
38261
|
this.deps.activate(this.id);
|
|
@@ -37425,6 +38569,7 @@ var ChartCell = class {
|
|
|
37425
38569
|
destroy() {
|
|
37426
38570
|
this.destroyed = true;
|
|
37427
38571
|
this.offMarket();
|
|
38572
|
+
this.cellControls.destroy();
|
|
37428
38573
|
this.contextMenu.destroy();
|
|
37429
38574
|
this.history.destroy();
|
|
37430
38575
|
this.marketStatus?.stop();
|
|
@@ -37723,10 +38868,10 @@ var SplitterLayer = class {
|
|
|
37723
38868
|
var DEFAULT_TIMEFRAMES = ["1", "5", "15", "60", "240", "D", "W"];
|
|
37724
38869
|
var GAP_PX = 2;
|
|
37725
38870
|
var POOL_CAP = 16;
|
|
37726
|
-
var
|
|
38871
|
+
var TIME_AXIS_H4 = 22;
|
|
37727
38872
|
var ALERT_CAP = 50;
|
|
37728
|
-
var
|
|
37729
|
-
var
|
|
38873
|
+
var STYLE_ID27 = "vela-workspace";
|
|
38874
|
+
var CSS24 = `
|
|
37730
38875
|
.vela-workspace { position: relative; width: 100%; height: 100%; display: flex; flex-direction: column; background: var(--vela-bg); }
|
|
37731
38876
|
.vela-ws-main { position: relative; display: flex; flex-direction: row; flex: 1 1 auto; min-height: 0; }
|
|
37732
38877
|
.vela-ws-toolbar { position: relative; flex: none; }
|
|
@@ -37753,6 +38898,21 @@ var CSS23 = `
|
|
|
37753
38898
|
/* Mobile: the docked drawing-toolbar column would eat a phone-width grid \u2014 the shell's
|
|
37754
38899
|
drawings drawer + on-chart pill replace it (same policy as the widget's in-chart bar). */
|
|
37755
38900
|
[data-layout='mobile'] .vela-ws-toolbar { display: none; }
|
|
38901
|
+
/* A maximized cell owns the whole grid: the splitter strips have no seams to grab and
|
|
38902
|
+
the active ring would just outline the only visible chart \u2014 both are noise here. */
|
|
38903
|
+
.vela-ws-grid[data-maximized='1'] .vela-ws-splitter { display: none; }
|
|
38904
|
+
.vela-ws-grid[data-maximized='1'] .vela-cell[data-active='1']::after { display: none; }
|
|
38905
|
+
/* Drop-target preview while a cell's drag handle is held: a dashed ring + the same
|
|
38906
|
+
soft wash the splitter hover uses, over the chart, inert to the pointer. */
|
|
38907
|
+
.vela-cell[data-drop-target='1']::before {
|
|
38908
|
+
content: '';
|
|
38909
|
+
position: absolute;
|
|
38910
|
+
inset: 0;
|
|
38911
|
+
border: 2px dashed var(--vela-fg-bright);
|
|
38912
|
+
background: var(--vela-separator-hover-band);
|
|
38913
|
+
pointer-events: none;
|
|
38914
|
+
z-index: 11;
|
|
38915
|
+
}
|
|
37756
38916
|
`;
|
|
37757
38917
|
registerIcon("layout", svg16('<rect x="1.5" y="1.5" width="13" height="13" rx="1.5"/><path d="M8 1.5v13M1.5 8h13"/>'));
|
|
37758
38918
|
function declaredOrder(cells) {
|
|
@@ -37782,6 +38942,9 @@ var VelaWorkspace = class {
|
|
|
37782
38942
|
* slots beyond the list get auto identities. Grows, never reorders. */
|
|
37783
38943
|
this.order = [];
|
|
37784
38944
|
this.activeId = null;
|
|
38945
|
+
/** The cell maximized over the whole grid (null = normal grid). TRANSIENT view
|
|
38946
|
+
* state — never persisted; any structural change (layout, applyState) restores. */
|
|
38947
|
+
this.maximizedId = null;
|
|
37785
38948
|
this.cellBackend = "auto";
|
|
37786
38949
|
this.destroyed = false;
|
|
37787
38950
|
this.shortcutsHelp = null;
|
|
@@ -37886,7 +39049,7 @@ var VelaWorkspace = class {
|
|
|
37886
39049
|
this.order = boot?.charts ? boot.charts.map((c) => c.id) : declaredOrder(opts.cells);
|
|
37887
39050
|
const bootActive = boot?.activeCellId ?? null;
|
|
37888
39051
|
const doc = hostEl.ownerDocument;
|
|
37889
|
-
injectStyles(
|
|
39052
|
+
injectStyles(STYLE_ID27, CSS24, doc);
|
|
37890
39053
|
this.root = doc.createElement("div");
|
|
37891
39054
|
this.root.className = "vela-workspace";
|
|
37892
39055
|
ensureUIHost(this.root, resolveTheme(opts.theme));
|
|
@@ -37996,7 +39159,11 @@ var VelaWorkspace = class {
|
|
|
37996
39159
|
if (attribution !== false) {
|
|
37997
39160
|
const background = resolveTheme(opts.theme).background;
|
|
37998
39161
|
const mark = typeof attribution === "string" && attribution.trim() ? createCustomMark(doc, attribution, background) : createAttributionMark(doc, background);
|
|
37999
|
-
Object.assign(mark.style, {
|
|
39162
|
+
Object.assign(mark.style, {
|
|
39163
|
+
left: "calc(var(--vela-toolbar-gutter, 0px) + 12px)",
|
|
39164
|
+
bottom: `calc(var(--vela-bottom-gutter, ${TIME_AXIS_H4}px) + 10px)`,
|
|
39165
|
+
zIndex: "11"
|
|
39166
|
+
});
|
|
38000
39167
|
this.gridEl.appendChild(mark);
|
|
38001
39168
|
this.attributionMark = mark;
|
|
38002
39169
|
}
|
|
@@ -38062,6 +39229,9 @@ var VelaWorkspace = class {
|
|
|
38062
39229
|
...topbarHas(this.topbarComp, "indicators") && (picker || this.indicatorsOverride) ? { onIndicatorsClick: this.indicatorsOverride ? () => this.runOverride(this.indicatorsOverride) : () => picker.open() } : {},
|
|
38063
39230
|
getContext: () => this.context(),
|
|
38064
39231
|
...this.drawingsEnabled ? { onDrawingsClick: () => this.openDrawingsDrawer() } : {},
|
|
39232
|
+
// Multi-chart only: the stop that isolates the ACTIVE chart (the
|
|
39233
|
+
// per-cell hover cluster has no cursor to reveal it on mobile).
|
|
39234
|
+
...this.monoLayout ? {} : { onMaximizeClick: () => this.toggleMobileMaximize() },
|
|
38065
39235
|
onMoreClick: () => this.openMoreDrawer(),
|
|
38066
39236
|
onSettingsClick: () => this.active.chart.renderer.openSettings()
|
|
38067
39237
|
}) : null;
|
|
@@ -38176,7 +39346,10 @@ var VelaWorkspace = class {
|
|
|
38176
39346
|
this.topbar.renderActions();
|
|
38177
39347
|
this.mobileBar?.renderActions();
|
|
38178
39348
|
this.dock.refresh();
|
|
38179
|
-
for (const cell of this.cells())
|
|
39349
|
+
for (const cell of this.cells()) {
|
|
39350
|
+
cell.chart.renderer.setLegendActions(legendActionsProviderFor(cell.chart, () => this.context()));
|
|
39351
|
+
cell.chart.renderer.setLegendCallouts(legendCalloutsProviderFor(cell.chart, () => this.context()));
|
|
39352
|
+
}
|
|
38180
39353
|
}
|
|
38181
39354
|
// ── state surface (the SDK's read/restore of the whole grid's config + content) ──
|
|
38182
39355
|
/**
|
|
@@ -38265,7 +39438,9 @@ var VelaWorkspace = class {
|
|
|
38265
39438
|
this.pool.clear();
|
|
38266
39439
|
for (const { id, ...cs } of st.charts.slice(liveCount)) this.pool.set(id, cs);
|
|
38267
39440
|
this.order = st.charts.map((c) => c.id);
|
|
39441
|
+
this.clearMaximized();
|
|
38268
39442
|
this.applyGrid();
|
|
39443
|
+
this.refreshCellControls();
|
|
38269
39444
|
const nextActive2 = st.activeCellId && this.cellsById.has(st.activeCellId) ? st.activeCellId : this.order[0] ?? null;
|
|
38270
39445
|
if (nextActive2 === this.activeId) this.projectActiveCell();
|
|
38271
39446
|
else this.setActiveCell(nextActive2);
|
|
@@ -38291,6 +39466,7 @@ var VelaWorkspace = class {
|
|
|
38291
39466
|
const def = this.monoLayout ? null : ensureLayout(st.layout);
|
|
38292
39467
|
if (def) this.def = def;
|
|
38293
39468
|
this.cellBackend = this.backendFor(this.def);
|
|
39469
|
+
this.clearMaximized();
|
|
38294
39470
|
this.applyGrid();
|
|
38295
39471
|
this.buildCells();
|
|
38296
39472
|
this.syncCellPresentation();
|
|
@@ -38354,6 +39530,7 @@ var VelaWorkspace = class {
|
|
|
38354
39530
|
setLayout(layout) {
|
|
38355
39531
|
if (this.destroyed) return;
|
|
38356
39532
|
if (this.monoLayout) return;
|
|
39533
|
+
this.clearMaximized();
|
|
38357
39534
|
const next = this.resolveLayout(layout);
|
|
38358
39535
|
const nextBackend = this.backendFor(next);
|
|
38359
39536
|
const rebuildAll = nextBackend !== this.cellBackend;
|
|
@@ -38374,6 +39551,7 @@ var VelaWorkspace = class {
|
|
|
38374
39551
|
this.buildCells();
|
|
38375
39552
|
this.alignNewCellStyles(preexisting);
|
|
38376
39553
|
this.syncCellPresentation();
|
|
39554
|
+
this.refreshCellControls();
|
|
38377
39555
|
this.topbar.setLayout(next.id);
|
|
38378
39556
|
const nextActive = activeAfterLayout(this.activeId, this.order.slice(0, next.cells.length));
|
|
38379
39557
|
if (nextActive === this.activeId) this.projectActiveCell();
|
|
@@ -38382,6 +39560,67 @@ var VelaWorkspace = class {
|
|
|
38382
39560
|
this.events.emit("layout:changed", { layout: next.id });
|
|
38383
39561
|
this.markStateDirty();
|
|
38384
39562
|
}
|
|
39563
|
+
/** The identity of the cell maximized over the whole grid, or null. */
|
|
39564
|
+
get maximizedCell() {
|
|
39565
|
+
return this.maximizedId;
|
|
39566
|
+
}
|
|
39567
|
+
/**
|
|
39568
|
+
* Maximize one cell over the whole grid, or restore the layout with `null`. Pure
|
|
39569
|
+
* presentation: the other cells stay alive underneath — charts, subscriptions and
|
|
39570
|
+
* state untouched — so restoring is instant. The maximized cell becomes the active
|
|
39571
|
+
* one. Transient view state (also reachable from each cell's bottom-center view
|
|
39572
|
+
* cluster): switching layouts or applying a state document restores the grid.
|
|
39573
|
+
*/
|
|
39574
|
+
maximizeCell(id) {
|
|
39575
|
+
if (this.destroyed) return;
|
|
39576
|
+
if (id != null && (!this.cellsById.has(id) || this.def.cells.length <= 1)) return;
|
|
39577
|
+
if (id === this.maximizedId) return;
|
|
39578
|
+
this.maximizedId = id;
|
|
39579
|
+
if (id) this.setActiveCell(id);
|
|
39580
|
+
this.applyGrid();
|
|
39581
|
+
this.refreshCellControls();
|
|
39582
|
+
this.syncMobileMaximize();
|
|
39583
|
+
this.events.emit("cell:maximized", { id });
|
|
39584
|
+
}
|
|
39585
|
+
/** The mobile bar's maximize stop: one press isolates the ACTIVE chart over the
|
|
39586
|
+
* grid; while something is already isolated — the chart, or a pane inside it
|
|
39587
|
+
* (mobile's double-tap) — the press restores that instead. Every branch re-syncs
|
|
39588
|
+
* the stop on its own (`maximizeCell` directly, `panes.maximize` via its
|
|
39589
|
+
* synchronous `pane:changed`). */
|
|
39590
|
+
toggleMobileMaximize() {
|
|
39591
|
+
const cell = this.activeId ? this.cellsById.get(this.activeId) : void 0;
|
|
39592
|
+
if (!cell) return;
|
|
39593
|
+
if (this.maximizedId) this.maximizeCell(null);
|
|
39594
|
+
else if (cell.chart.panes.list().some((p) => p.maximized)) cell.chart.panes.maximize(null);
|
|
39595
|
+
else this.maximizeCell(cell.id);
|
|
39596
|
+
}
|
|
39597
|
+
/** Keep the mobile bar's maximize stop truthful: lit (inverse chip, restore
|
|
39598
|
+
* glyph) while the active chart covers the grid OR one of its panes is
|
|
39599
|
+
* maximized — the state a double-tap toggles is otherwise invisible on mobile. */
|
|
39600
|
+
syncMobileMaximize() {
|
|
39601
|
+
if (!this.mobileBar) return;
|
|
39602
|
+
const cell = this.activeId ? this.cellsById.get(this.activeId) : void 0;
|
|
39603
|
+
const paneMax = cell ? cell.chart.panes.list().some((p) => p.maximized) : false;
|
|
39604
|
+
this.mobileBar.setMaximizeActive(this.maximizedId != null || paneMax);
|
|
39605
|
+
}
|
|
39606
|
+
/**
|
|
39607
|
+
* Trade the SLOTS of two live cells — the grid arrangement changes, the cells
|
|
39608
|
+
* themselves (charts, indicators, drawings, the active flag) stay untouched.
|
|
39609
|
+
* What each cell's drag handle commits; also callable directly by hosts.
|
|
39610
|
+
*/
|
|
39611
|
+
swapCells(a, b) {
|
|
39612
|
+
if (this.destroyed || a === b) return;
|
|
39613
|
+
const i = this.order.indexOf(a);
|
|
39614
|
+
const j = this.order.indexOf(b);
|
|
39615
|
+
if (i < 0 || j < 0 || !this.cellsById.has(a) || !this.cellsById.has(b)) return;
|
|
39616
|
+
[this.order[i], this.order[j]] = [this.order[j], this.order[i]];
|
|
39617
|
+
for (const [k] of this.def.cells.entries()) {
|
|
39618
|
+
const host = this.cellsById.get(this.order[k] ?? "")?.host;
|
|
39619
|
+
if (host) this.gridEl.appendChild(host);
|
|
39620
|
+
}
|
|
39621
|
+
this.applyGrid();
|
|
39622
|
+
this.markStateDirty();
|
|
39623
|
+
}
|
|
38385
39624
|
resize() {
|
|
38386
39625
|
this.splitters.layout();
|
|
38387
39626
|
}
|
|
@@ -38451,6 +39690,7 @@ var VelaWorkspace = class {
|
|
|
38451
39690
|
this.mobileBar?.renderActions();
|
|
38452
39691
|
this.mobileBar?.setSymbol(cell.symbol);
|
|
38453
39692
|
this.mobileBar?.setTimeframe(cell.timeframe);
|
|
39693
|
+
this.syncMobileMaximize();
|
|
38454
39694
|
this.drawingPill?.onChart(cell.chart);
|
|
38455
39695
|
const pushHistory = () => this.topbar.setHistoryState(cell.history.canUndo, cell.history.canRedo);
|
|
38456
39696
|
this.historyUnsub?.();
|
|
@@ -38540,8 +39780,78 @@ var VelaWorkspace = class {
|
|
|
38540
39780
|
const host = this.cellsById.get(this.order[i] ?? "")?.host;
|
|
38541
39781
|
if (host) host.style.gridArea = perCell[slot.id]?.gridArea ?? "";
|
|
38542
39782
|
}
|
|
39783
|
+
this.applyMaximizePresentation();
|
|
39784
|
+
this.mountAttributionMark();
|
|
38543
39785
|
this.splitters.layout();
|
|
38544
39786
|
}
|
|
39787
|
+
/** Overlay the maximize presentation on the freshly applied grid: EVERY cell spans
|
|
39788
|
+
* the full track grid — the maximized one on top, the siblings invisible beneath
|
|
39789
|
+
* it (their charts stay alive — restoring is instant). The siblings must span too:
|
|
39790
|
+
* left in their slots they would auto-flow into implicit zero-height rows, whose
|
|
39791
|
+
* gaps steal height from the maximized cell and collapse their renderers to 0.
|
|
39792
|
+
* The splitter strips and the active ring hide via the `data-maximized` rules. */
|
|
39793
|
+
applyMaximizePresentation() {
|
|
39794
|
+
const maxId = this.maximizedId;
|
|
39795
|
+
if (maxId) this.gridEl.dataset.maximized = "1";
|
|
39796
|
+
else delete this.gridEl.dataset.maximized;
|
|
39797
|
+
for (const [id, cell] of this.cellsById) {
|
|
39798
|
+
const style = cell.host.style;
|
|
39799
|
+
if (maxId) style.gridArea = "1 / 1 / -1 / -1";
|
|
39800
|
+
style.zIndex = maxId && id === maxId ? "5" : "";
|
|
39801
|
+
style.visibility = maxId && id !== maxId ? "hidden" : "";
|
|
39802
|
+
}
|
|
39803
|
+
}
|
|
39804
|
+
/** Rebuild every cell's view cluster (the maximize gate or state changed). */
|
|
39805
|
+
refreshCellControls() {
|
|
39806
|
+
for (const cell of this.cellsById.values()) cell.refreshControls();
|
|
39807
|
+
}
|
|
39808
|
+
/** Drop the transient maximize on a structural change (layout switch, state
|
|
39809
|
+
* document) — WITH the event, so hosts tracking `cell:maximized` never drift
|
|
39810
|
+
* from `maximizedCell`. The caller's own grid re-apply paints the restore. */
|
|
39811
|
+
clearMaximized() {
|
|
39812
|
+
if (this.maximizedId == null) return;
|
|
39813
|
+
this.maximizedId = null;
|
|
39814
|
+
this.events.emit("cell:maximized", { id: null });
|
|
39815
|
+
}
|
|
39816
|
+
/** The live cell under a viewport point, excluding `excludeId` and any host a
|
|
39817
|
+
* maximize has hidden — the drag handle's hit-test. */
|
|
39818
|
+
cellAtPoint(x, y, excludeId) {
|
|
39819
|
+
for (const [id, cell] of this.cellsById) {
|
|
39820
|
+
if (id === excludeId || cell.host.style.visibility === "hidden") continue;
|
|
39821
|
+
const r = cell.host.getBoundingClientRect();
|
|
39822
|
+
if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return id;
|
|
39823
|
+
}
|
|
39824
|
+
return null;
|
|
39825
|
+
}
|
|
39826
|
+
/** Mark one cell as the live drop target of a grip drag (null clears all) —
|
|
39827
|
+
* the `data-drop-target` stylesheet rule paints the dashed preview ring. */
|
|
39828
|
+
setDropTarget(id) {
|
|
39829
|
+
for (const [cid, cell] of this.cellsById) {
|
|
39830
|
+
if (cid === id) cell.host.dataset.dropTarget = "1";
|
|
39831
|
+
else delete cell.host.dataset.dropTarget;
|
|
39832
|
+
}
|
|
39833
|
+
}
|
|
39834
|
+
/** The cell whose bottom-left corner the grid's attribution mark floats in — the
|
|
39835
|
+
* maximized cell while one covers the grid, else the bottom-left slot's cell. */
|
|
39836
|
+
bottomLeftCell() {
|
|
39837
|
+
if (this.maximizedId) return this.cellsById.get(this.maximizedId);
|
|
39838
|
+
const grid = occupancyGrid(this.def);
|
|
39839
|
+
const slot = grid[grid.length - 1]?.[0];
|
|
39840
|
+
const idx = this.def.cells.findIndex((c) => (c.area ?? c.id) === slot);
|
|
39841
|
+
return this.cellsById.get(this.order[idx >= 0 ? idx : 0] ?? "");
|
|
39842
|
+
}
|
|
39843
|
+
/** Keep the shared attribution mark inside the BOTTOM-LEFT visible cell: its
|
|
39844
|
+
* offsets ride that cell's renderer-published `--vela-bottom-gutter` /
|
|
39845
|
+
* `--vela-toolbar-gutter`, so collapsed pane strips push the mark up without any
|
|
39846
|
+
* bookkeeping here. Re-run after anything that changes which host that is
|
|
39847
|
+
* (layout switch, maximize, cell rebuild); a destroyed host drops the mark from
|
|
39848
|
+
* the DOM, and this re-mount brings it back. */
|
|
39849
|
+
mountAttributionMark() {
|
|
39850
|
+
const mark = this.attributionMark;
|
|
39851
|
+
if (!mark) return;
|
|
39852
|
+
const host = this.bottomLeftCell()?.host ?? this.gridEl;
|
|
39853
|
+
if (mark.parentElement !== host) host.appendChild(mark);
|
|
39854
|
+
}
|
|
38545
39855
|
/** Create the cells the current layout wants but don't exist yet (pool-first).
|
|
38546
39856
|
* A slot's CELL IDENTITY is `order[i]` (declaration order — never the slot's own
|
|
38547
39857
|
* positional id); slots past the declared list mint an auto identity once. */
|
|
@@ -38573,6 +39883,12 @@ var VelaWorkspace = class {
|
|
|
38573
39883
|
setTimezone: (zone) => this.setTimezone(zone),
|
|
38574
39884
|
context: () => this.context(),
|
|
38575
39885
|
activate: (id2) => this.setActiveCell(id2),
|
|
39886
|
+
multiCell: () => !this.monoLayout && this.def.cells.length > 1,
|
|
39887
|
+
isMaximized: (id2) => this.maximizedId === id2,
|
|
39888
|
+
toggleMaximize: (id2) => this.maximizeCell(this.maximizedId === id2 ? null : id2),
|
|
39889
|
+
cellDragTarget: (id2, x, y) => this.cellAtPoint(x, y, id2),
|
|
39890
|
+
previewDropTarget: (target) => this.setDropTarget(target),
|
|
39891
|
+
dropCell: (id2, target) => this.swapCells(id2, target),
|
|
38576
39892
|
onMarketChanged: (id2) => this.onCellMarketChanged(id2),
|
|
38577
39893
|
onPriceStyleChanged: (id2) => this.onCellPriceStyleChanged(id2),
|
|
38578
39894
|
onIndicatorsChanged: (id2) => this.onCellIndicatorsChanged(id2),
|
|
@@ -38586,6 +39902,7 @@ var VelaWorkspace = class {
|
|
|
38586
39902
|
if (id === this.activeId) cell.host.dataset.active = "1";
|
|
38587
39903
|
this.wireCell(cell);
|
|
38588
39904
|
cell.chart.renderer.setLayoutMode(this.layoutCtl.current);
|
|
39905
|
+
cell.setControlsSuspended(this.layoutCtl.current === "mobile");
|
|
38589
39906
|
if (this.favs.length > 0) cell.chart.drawings.setFavorites(this.favs);
|
|
38590
39907
|
cell.setManifest(this.manifest, pooled?.indicators == null);
|
|
38591
39908
|
cell.restorePersistedExt();
|
|
@@ -38595,6 +39912,7 @@ var VelaWorkspace = class {
|
|
|
38595
39912
|
const host = this.cellsById.get(this.order[i] ?? "")?.host;
|
|
38596
39913
|
if (host) this.gridEl.appendChild(host);
|
|
38597
39914
|
}
|
|
39915
|
+
this.mountAttributionMark();
|
|
38598
39916
|
}
|
|
38599
39917
|
/** Per-cell chart subscriptions (trigger ② — the chart instance is stable for the
|
|
38600
39918
|
* cell's whole life, so these live and die with the cell). */
|
|
@@ -38654,6 +39972,9 @@ var VelaWorkspace = class {
|
|
|
38654
39972
|
chart.on("viewport:changed", (range) => this.propagateViewport(cell.id, range));
|
|
38655
39973
|
chart.renderer.onConfigChanged(() => this.propagateStylePrefs(cell.id));
|
|
38656
39974
|
chart.on("theme:changed", (t) => this.setTheme(t));
|
|
39975
|
+
chart.on("pane:changed", () => {
|
|
39976
|
+
if (cell.id === this.activeId) this.syncMobileMaximize();
|
|
39977
|
+
});
|
|
38657
39978
|
chart.renderer.onAxisLongPress((e) => {
|
|
38658
39979
|
if (this.layoutCtl.current !== "mobile") return;
|
|
38659
39980
|
if (e.axis === "time") this.openTimezoneDrawer();
|
|
@@ -38974,6 +40295,7 @@ var VelaWorkspace = class {
|
|
|
38974
40295
|
for (const cell of this.cellsById.values()) {
|
|
38975
40296
|
cell.chart.renderer.closeDialogs();
|
|
38976
40297
|
cell.chart.renderer.setLayoutMode(mode);
|
|
40298
|
+
cell.setControlsSuspended(mode === "mobile");
|
|
38977
40299
|
}
|
|
38978
40300
|
this.syncCellPresentation();
|
|
38979
40301
|
}
|