@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/widget.cjs
CHANGED
|
@@ -125,6 +125,23 @@ var TypedEventBus = class {
|
|
|
125
125
|
}
|
|
126
126
|
};
|
|
127
127
|
|
|
128
|
+
// src/data/symbol-groups.ts
|
|
129
|
+
function isGroupRow(d) {
|
|
130
|
+
return d.group != null && d.ticker === d.group;
|
|
131
|
+
}
|
|
132
|
+
function groupKeyOf(d) {
|
|
133
|
+
return `${(d.prefix ?? d.provider ?? "").toLowerCase()}:${(d.group ?? "").toUpperCase()}`;
|
|
134
|
+
}
|
|
135
|
+
function groupMembers(pool, groupRow) {
|
|
136
|
+
const key = groupKeyOf(groupRow);
|
|
137
|
+
return pool.filter((s) => s.group != null && !isGroupRow(s) && groupKeyOf(s) === key);
|
|
138
|
+
}
|
|
139
|
+
function defaultMemberOf(pool, groupRow) {
|
|
140
|
+
const members = groupMembers(pool, groupRow);
|
|
141
|
+
const defaults2 = members.filter((m) => m.default);
|
|
142
|
+
return (defaults2.length === 1 ? defaults2[0] : members[0]) ?? null;
|
|
143
|
+
}
|
|
144
|
+
|
|
128
145
|
// src/data/ProviderRegistry.ts
|
|
129
146
|
var normName = (n) => n.trim().toLowerCase();
|
|
130
147
|
var normTicker = (t) => t.trim().toUpperCase();
|
|
@@ -214,17 +231,24 @@ var ProviderRegistry = class {
|
|
|
214
231
|
resolve(raw, opts = {}) {
|
|
215
232
|
const { provider, ticker } = this.parse(raw);
|
|
216
233
|
if (provider) {
|
|
217
|
-
if (this.entries.has(provider))
|
|
234
|
+
if (this.entries.has(provider)) {
|
|
235
|
+
const d = this.entries.get(provider).byTicker?.get(normTicker(ticker));
|
|
236
|
+
return { provider, ticker: d != null && isGroupRow(d) ? this.loadableTicker(provider, d) : ticker };
|
|
237
|
+
}
|
|
218
238
|
const key = `${provider}:${normTicker(ticker)}`;
|
|
219
239
|
for (const name of this.candidateOrder(opts.default)) {
|
|
220
240
|
const d = this.entries.get(name).prefixIndex?.get(key);
|
|
221
|
-
if (d) return { provider: name, ticker:
|
|
241
|
+
if (d) return { provider: name, ticker: this.loadableTicker(name, d) };
|
|
222
242
|
}
|
|
223
243
|
return null;
|
|
224
244
|
}
|
|
225
245
|
const norm = normTicker(ticker);
|
|
226
246
|
for (const name of this.candidateOrder(opts.default)) {
|
|
227
|
-
|
|
247
|
+
const e = this.entries.get(name);
|
|
248
|
+
if (e.index?.has(norm)) {
|
|
249
|
+
const d = e.byTicker?.get(norm);
|
|
250
|
+
return { provider: name, ticker: d != null && isGroupRow(d) ? this.loadableTicker(name, d) : ticker };
|
|
251
|
+
}
|
|
228
252
|
}
|
|
229
253
|
if (opts.lenient && this.entries.size === 1) {
|
|
230
254
|
const only = this.names()[0];
|
|
@@ -233,6 +257,13 @@ var ProviderRegistry = class {
|
|
|
233
257
|
}
|
|
234
258
|
return null;
|
|
235
259
|
}
|
|
260
|
+
/** What a resolved descriptor LOADS: itself — unless it is a GROUP row (listed, never
|
|
261
|
+
* served), which loads its default member (single `default`, else first listed). A
|
|
262
|
+
* memberless group keeps its own ticker and fails downstream like any unknown symbol. */
|
|
263
|
+
loadableTicker(name, d) {
|
|
264
|
+
if (!isGroupRow(d)) return d.ticker;
|
|
265
|
+
return defaultMemberOf(this.entries.get(name)?.descriptors ?? [], d)?.ticker ?? d.ticker;
|
|
266
|
+
}
|
|
236
267
|
/**
|
|
237
268
|
* Resolve now if possible, else resolve later — re-attempting after each provider
|
|
238
269
|
* settles. The promise stays pending until some registered provider can serve the
|
|
@@ -486,6 +517,28 @@ var CachingDataFeed = class {
|
|
|
486
517
|
this.store.merge(key, dropForming(bars));
|
|
487
518
|
return bars;
|
|
488
519
|
}
|
|
520
|
+
/**
|
|
521
|
+
* Progressive twin of {@link load}: a COLD load streams through the inner feed's
|
|
522
|
+
* progressive path — batches forwarded verbatim, the FINAL answer cached exactly as
|
|
523
|
+
* `load` caches — while a cache-covered load answers once through `load` itself (a
|
|
524
|
+
* warm chart has nothing to stream). Falls back to `load` wholesale when the inner
|
|
525
|
+
* feed lacks the capability, so callers may prefer this method unconditionally.
|
|
526
|
+
*/
|
|
527
|
+
async loadProgressive(cfg, onBatch, opts) {
|
|
528
|
+
if (cfg.data && cfg.data.length > 0) return this.inner.load(cfg);
|
|
529
|
+
if (!this.inner.loadProgressive) return null;
|
|
530
|
+
const symbol = cfg.symbol ?? "TEST";
|
|
531
|
+
const key = cacheKey(symbol, cfg.timeframe ?? "60", cfg.session);
|
|
532
|
+
const cached = this.store.get(key);
|
|
533
|
+
const n = cfg.bars ?? 500;
|
|
534
|
+
const lastCached = cached?.[cached.length - 1];
|
|
535
|
+
if (cached && lastCached && cached.length >= n - 1 && this.inner.loadRange) return this.load(cfg);
|
|
536
|
+
this.store.retainSymbol(symbol);
|
|
537
|
+
const bars = await this.inner.loadProgressive(cfg, onBatch, opts);
|
|
538
|
+
if (bars == null) return null;
|
|
539
|
+
this.store.merge(key, dropForming(bars));
|
|
540
|
+
return bars;
|
|
541
|
+
}
|
|
489
542
|
/**
|
|
490
543
|
* Cache-backed ranged fetch — the gateway engines use for secondary series
|
|
491
544
|
* (`request.security` HTF/LTF/cross-symbol) and the orchestrator's backward
|
|
@@ -731,6 +784,17 @@ var MultiProviderFeed = class {
|
|
|
731
784
|
this.prefetchSymbolInfo(resolved);
|
|
732
785
|
return this.cache.load(canonical(cfg, resolved));
|
|
733
786
|
}
|
|
787
|
+
/** Progressive twin of {@link load} — same resolution, the cache streams the batches. */
|
|
788
|
+
async loadProgressive(cfg, onBatch, opts) {
|
|
789
|
+
if (cfg.data && cfg.data.length > 0) {
|
|
790
|
+
this.liveBars = cfg.data.map((b) => ({ ...b }));
|
|
791
|
+
return cfg.data;
|
|
792
|
+
}
|
|
793
|
+
const resolved = await this.registry.whenResolvable(rawSymbol(cfg), { default: this.primaryProvider });
|
|
794
|
+
this.primaryProvider = resolved.provider;
|
|
795
|
+
this.prefetchSymbolInfo(resolved);
|
|
796
|
+
return this.cache.loadProgressive(canonical(cfg, resolved), onBatch, opts);
|
|
797
|
+
}
|
|
734
798
|
/**
|
|
735
799
|
* Synchronous per-symbol metadata for engines (Pine `syminfo.*`), served from the cache
|
|
736
800
|
* warmed by load(). Undefined until the prefetch lands (the engine then synthesizes a
|
|
@@ -818,6 +882,18 @@ var RegistryFetchFeed = class {
|
|
|
818
882
|
if (!provider) return Promise.resolve([]);
|
|
819
883
|
return safeBars(provider, ticker, cfg.timeframe ?? "60", { limit: cfg.bars ?? 500, session: cfg.session });
|
|
820
884
|
}
|
|
885
|
+
async loadProgressive(cfg, onBatch, opts) {
|
|
886
|
+
const { provider: name, ticker } = parseSymbol(cfg.symbol ?? "");
|
|
887
|
+
const provider = this.registry.get(name ?? "");
|
|
888
|
+
if (!provider) return [];
|
|
889
|
+
if (!provider.getBarsProgressive) return null;
|
|
890
|
+
try {
|
|
891
|
+
return await provider.getBarsProgressive(ticker, cfg.timeframe ?? "60", { limit: cfg.bars ?? 500, session: cfg.session }, onBatch, opts);
|
|
892
|
+
} catch (e) {
|
|
893
|
+
console.warn(`[vela] progressive fetch failed for ${ticker} ${cfg.timeframe ?? "60"} \u2014 ${e instanceof Error ? e.message : String(e)}`);
|
|
894
|
+
return [];
|
|
895
|
+
}
|
|
896
|
+
}
|
|
821
897
|
loadRange(cfg, range) {
|
|
822
898
|
const { provider: name, ticker } = parseSymbol(cfg.symbol ?? "");
|
|
823
899
|
const provider = this.registry.get(name ?? "");
|
|
@@ -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 = `
|
|
@@ -4677,6 +4945,44 @@ function legendActionsProviderFor(chart, context) {
|
|
|
4677
4945
|
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) }));
|
|
4678
4946
|
};
|
|
4679
4947
|
}
|
|
4948
|
+
var calloutRegistry = /* @__PURE__ */ new Map();
|
|
4949
|
+
function legendCallouts() {
|
|
4950
|
+
return [...calloutRegistry.values()].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
4951
|
+
}
|
|
4952
|
+
function legendCalloutsProviderFor(chart, context) {
|
|
4953
|
+
return (indicatorId) => {
|
|
4954
|
+
const handle = chart.indicators().find((h) => h.id === indicatorId);
|
|
4955
|
+
if (!handle) return [];
|
|
4956
|
+
const info = { id: handle.id, title: handle.title, ...handle.source !== void 0 ? { source: handle.source } : {} };
|
|
4957
|
+
const views = [];
|
|
4958
|
+
for (const d of legendCallouts()) {
|
|
4959
|
+
const spec = d.callout(info);
|
|
4960
|
+
if (!spec) continue;
|
|
4961
|
+
views.push({
|
|
4962
|
+
id: d.id,
|
|
4963
|
+
icon: spec.icon,
|
|
4964
|
+
background: spec.background,
|
|
4965
|
+
...spec.color !== void 0 ? { color: spec.color } : {},
|
|
4966
|
+
tooltip: spec.tooltip,
|
|
4967
|
+
...spec.content !== void 0 ? {
|
|
4968
|
+
content: {
|
|
4969
|
+
...spec.content.title !== void 0 ? { title: spec.content.title } : {},
|
|
4970
|
+
items: spec.content.items.map(
|
|
4971
|
+
(item) => item.type === "text" ? item : {
|
|
4972
|
+
type: "button",
|
|
4973
|
+
label: item.label,
|
|
4974
|
+
...item.primary !== void 0 ? { primary: item.primary } : {},
|
|
4975
|
+
...item.close !== void 0 ? { close: item.close } : {},
|
|
4976
|
+
run: () => item.run(context(), info)
|
|
4977
|
+
}
|
|
4978
|
+
)
|
|
4979
|
+
}
|
|
4980
|
+
} : {}
|
|
4981
|
+
});
|
|
4982
|
+
}
|
|
4983
|
+
return views;
|
|
4984
|
+
};
|
|
4985
|
+
}
|
|
4680
4986
|
var stateHandlers = /* @__PURE__ */ new Map();
|
|
4681
4987
|
function registerStatePersistence(handler) {
|
|
4682
4988
|
stateHandlers.set(handler.key, handler);
|
|
@@ -13516,6 +13822,17 @@ function dedupeSymbols(list) {
|
|
|
13516
13822
|
}
|
|
13517
13823
|
return out;
|
|
13518
13824
|
}
|
|
13825
|
+
function foldGroups(list) {
|
|
13826
|
+
const groupsAbove = /* @__PURE__ */ new Set();
|
|
13827
|
+
const out = [];
|
|
13828
|
+
for (const s of list) {
|
|
13829
|
+
if (isGroupRow(s)) {
|
|
13830
|
+
groupsAbove.add(groupKeyOf(s));
|
|
13831
|
+
out.push(s);
|
|
13832
|
+
} else if (s.group == null || !groupsAbove.has(groupKeyOf(s))) out.push(s);
|
|
13833
|
+
}
|
|
13834
|
+
return out;
|
|
13835
|
+
}
|
|
13519
13836
|
var TOP_TICKERS = ["BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT", "XRPUSDT", "DOGEUSDT", "ADAUSDT", "LINKUSDT"];
|
|
13520
13837
|
function parseQuery(raw, venues) {
|
|
13521
13838
|
const m = raw.match(/^\s*([^\s:]+)\s*[:\s]\s*(.*)$/);
|
|
@@ -13647,6 +13964,21 @@ var CSS8 = `
|
|
|
13647
13964
|
.vela-sp-badge[data-p='binance'] { color: #f0b90b; } /* palette-exempt: venue brand mark */
|
|
13648
13965
|
.vela-sp-badge[data-p='hyperliquid'] { color: #50d2c1; } /* palette-exempt: venue brand mark */
|
|
13649
13966
|
.vela-sp-empty { padding: var(--vela-space-3); color: var(--vela-fg-muted); text-align: center; }
|
|
13967
|
+
/* Grouped listings (futures roots): the chevron unfolds members inline, indented. */
|
|
13968
|
+
.vela-sp-expander {
|
|
13969
|
+
all: unset;
|
|
13970
|
+
flex: none;
|
|
13971
|
+
display: inline-flex;
|
|
13972
|
+
align-items: center;
|
|
13973
|
+
justify-content: center;
|
|
13974
|
+
width: 22px;
|
|
13975
|
+
height: 22px;
|
|
13976
|
+
border-radius: 5px;
|
|
13977
|
+
cursor: pointer;
|
|
13978
|
+
color: var(--vela-fg-muted);
|
|
13979
|
+
}
|
|
13980
|
+
.vela-sp-expander:hover { background: var(--vela-surface-elev); color: var(--vela-fg); }
|
|
13981
|
+
.vela-sp-row[data-member] { padding-left: 34px; }
|
|
13650
13982
|
`;
|
|
13651
13983
|
var PAGE = 100;
|
|
13652
13984
|
var SymbolPicker = class {
|
|
@@ -13658,6 +13990,11 @@ var SymbolPicker = class {
|
|
|
13658
13990
|
this.seed = "";
|
|
13659
13991
|
this.activeTab = "All";
|
|
13660
13992
|
this.visible = PAGE;
|
|
13993
|
+
/** The last filter pass returned fewer raw rows than asked — the pool is drained
|
|
13994
|
+
* (checked BEFORE folding: folding shortens pages without meaning exhaustion). */
|
|
13995
|
+
this.exhausted = false;
|
|
13996
|
+
/** Group rows currently expanded (venue-scoped keys) — members shown inline. */
|
|
13997
|
+
this.expanded = /* @__PURE__ */ new Set();
|
|
13661
13998
|
/** The ranked pool cache — `key` fingerprints the raw pool the ranking ran on. */
|
|
13662
13999
|
this.ranked = null;
|
|
13663
14000
|
this.ranking = false;
|
|
@@ -13672,7 +14009,7 @@ var SymbolPicker = class {
|
|
|
13672
14009
|
searchRow.append(iconEl("search", doc), this.input);
|
|
13673
14010
|
this.tabs = doc.createElement("div");
|
|
13674
14011
|
this.tabs.className = "vela-sp-tabs";
|
|
13675
|
-
for (const t of ["All", "Stocks", "ETFs", "Crypto", "Forex", "Commodities"]) {
|
|
14012
|
+
for (const t of ["All", "Stocks", "ETFs", "Crypto", "Futures", "Forex", "Commodities"]) {
|
|
13676
14013
|
const b = doc.createElement("button");
|
|
13677
14014
|
b.className = "vela-sp-tab";
|
|
13678
14015
|
b.textContent = t;
|
|
@@ -13688,7 +14025,7 @@ var SymbolPicker = class {
|
|
|
13688
14025
|
this.list = doc.createElement("div");
|
|
13689
14026
|
this.list.className = "vela-sp-list";
|
|
13690
14027
|
this.list.addEventListener("scroll", () => {
|
|
13691
|
-
if (this.
|
|
14028
|
+
if (this.exhausted) return;
|
|
13692
14029
|
if (this.list.scrollTop + this.list.clientHeight < this.list.scrollHeight - 200) return;
|
|
13693
14030
|
this.visible += PAGE;
|
|
13694
14031
|
this.grow();
|
|
@@ -13717,14 +14054,19 @@ var SymbolPicker = class {
|
|
|
13717
14054
|
else if (e.key === "ArrowUp") this.moveHighlight(-1);
|
|
13718
14055
|
else if (e.key === "Enter") {
|
|
13719
14056
|
const pick = this.rows[this.highlighted];
|
|
13720
|
-
if (pick) this.
|
|
14057
|
+
if (pick) this.pick(pick);
|
|
13721
14058
|
return;
|
|
13722
14059
|
} else return;
|
|
13723
14060
|
e.preventDefault();
|
|
13724
14061
|
});
|
|
13725
14062
|
this.list.addEventListener("click", (e) => {
|
|
13726
|
-
const
|
|
13727
|
-
|
|
14063
|
+
const target = e.target;
|
|
14064
|
+
const row = target.closest(".vela-sp-row");
|
|
14065
|
+
if (!row) return;
|
|
14066
|
+
const s = this.rows[Number(row.dataset.i)];
|
|
14067
|
+
if (!s) return;
|
|
14068
|
+
if (target.closest(".vela-sp-expander")) this.toggleExpand(s);
|
|
14069
|
+
else this.pick(s);
|
|
13728
14070
|
});
|
|
13729
14071
|
}
|
|
13730
14072
|
/** Wire where symbols come from (re-called on every widget rebuild). */
|
|
@@ -13741,6 +14083,27 @@ var SymbolPicker = class {
|
|
|
13741
14083
|
destroy() {
|
|
13742
14084
|
this.dialog.destroy();
|
|
13743
14085
|
}
|
|
14086
|
+
/** Route a row activation: a GROUP row loads its default member (the root itself is
|
|
14087
|
+
* listed, never loadable), any other row loads itself. */
|
|
14088
|
+
pick(s) {
|
|
14089
|
+
const target = isGroupRow(s) ? defaultMemberOf(this.pool(), s) ?? s : s;
|
|
14090
|
+
this.select(target.ticker, target.prefix ?? target.provider, this.opts.onSelect);
|
|
14091
|
+
}
|
|
14092
|
+
/** Expand/collapse a group row IN PLACE — same query, same page, same scroll; only
|
|
14093
|
+
* the member rows under the group appear or go. */
|
|
14094
|
+
toggleExpand(s) {
|
|
14095
|
+
const key = groupKeyOf(s);
|
|
14096
|
+
if (!this.expanded.delete(key)) this.expanded.add(key);
|
|
14097
|
+
const scrollTop = this.list.scrollTop;
|
|
14098
|
+
const focus = this.rows[this.highlighted];
|
|
14099
|
+
this.rows = this.computeRows();
|
|
14100
|
+
this.list.replaceChildren();
|
|
14101
|
+
this.rows.forEach((r, i) => this.list.appendChild(this.rowEl(r, i)));
|
|
14102
|
+
const at = focus ? this.rows.indexOf(focus) : -1;
|
|
14103
|
+
this.highlighted = at >= 0 ? at : Math.min(this.highlighted, Math.max(0, this.rows.length - 1));
|
|
14104
|
+
this.renderHighlight();
|
|
14105
|
+
this.list.scrollTop = scrollTop;
|
|
14106
|
+
}
|
|
13744
14107
|
select(ticker, venue, onSelect) {
|
|
13745
14108
|
this.close();
|
|
13746
14109
|
onSelect(venue ? `${venue}:${ticker}` : ticker);
|
|
@@ -13782,10 +14145,26 @@ var SymbolPicker = class {
|
|
|
13782
14145
|
return this.ranked?.result ?? raw;
|
|
13783
14146
|
}
|
|
13784
14147
|
computeRows() {
|
|
13785
|
-
const TAB_TYPES = { Crypto: ["crypto"], Stocks: ["stock"], ETFs: ["etf"], Forex: ["forex"], Commodities: ["commodity"] };
|
|
14148
|
+
const TAB_TYPES = { Crypto: ["crypto"], Stocks: ["stock"], ETFs: ["etf"], Futures: ["futures", "root"], Forex: ["forex"], Commodities: ["commodity"] };
|
|
13786
14149
|
const all = this.pool();
|
|
13787
14150
|
const pool = this.activeTab === "All" ? all : all.filter((s) => TAB_TYPES[this.activeTab]?.includes((s.type ?? "").toLowerCase()) || this.activeTab === "Crypto" && (s.type ?? "").toLowerCase() === "futures");
|
|
13788
|
-
|
|
14151
|
+
const filtered = filterSymbols(pool, this.input.value, this.visible, symbolRanking() ? false : TOP_TICKERS);
|
|
14152
|
+
this.exhausted = filtered.length < this.visible;
|
|
14153
|
+
const folded = foldGroups(filtered);
|
|
14154
|
+
if (!this.expanded.size) return folded;
|
|
14155
|
+
const keyOf = (s) => `${(s.prefix ?? s.provider ?? "").toLowerCase()}:${s.ticker.toUpperCase()}`;
|
|
14156
|
+
const present = new Set(folded.map(keyOf));
|
|
14157
|
+
const out = [];
|
|
14158
|
+
for (const s of folded) {
|
|
14159
|
+
out.push(s);
|
|
14160
|
+
if (!isGroupRow(s) || !this.expanded.has(groupKeyOf(s))) continue;
|
|
14161
|
+
for (const m of groupMembers(all, s)) {
|
|
14162
|
+
if (present.has(keyOf(m))) continue;
|
|
14163
|
+
present.add(keyOf(m));
|
|
14164
|
+
out.push(m);
|
|
14165
|
+
}
|
|
14166
|
+
}
|
|
14167
|
+
return out;
|
|
13789
14168
|
}
|
|
13790
14169
|
refresh() {
|
|
13791
14170
|
const doc = this.list.ownerDocument;
|
|
@@ -13800,19 +14179,20 @@ var SymbolPicker = class {
|
|
|
13800
14179
|
this.list.appendChild(empty);
|
|
13801
14180
|
return;
|
|
13802
14181
|
}
|
|
13803
|
-
|
|
14182
|
+
this.rows.forEach((s, i) => this.list.appendChild(this.rowEl(s, i)));
|
|
13804
14183
|
this.renderHighlight();
|
|
13805
14184
|
}
|
|
13806
14185
|
/** Append the page the grown `visible` just uncovered — rows already on screen stay put. */
|
|
13807
14186
|
grow() {
|
|
13808
14187
|
const already = this.rows.length;
|
|
13809
14188
|
this.rows = this.computeRows();
|
|
13810
|
-
|
|
14189
|
+
this.rows.slice(already).forEach((s, j) => this.list.appendChild(this.rowEl(s, already + j)));
|
|
13811
14190
|
}
|
|
13812
|
-
rowEl(s) {
|
|
14191
|
+
rowEl(s, i) {
|
|
13813
14192
|
const doc = this.list.ownerDocument;
|
|
13814
14193
|
const row = doc.createElement("div");
|
|
13815
14194
|
row.className = "vela-sp-row";
|
|
14195
|
+
row.dataset.i = String(i);
|
|
13816
14196
|
row.dataset.ticker = s.ticker;
|
|
13817
14197
|
const venue = s.prefix ?? s.provider;
|
|
13818
14198
|
if (venue) row.dataset.venue = venue;
|
|
@@ -13827,6 +14207,14 @@ var SymbolPicker = class {
|
|
|
13827
14207
|
d.textContent = s.description ?? (s.type ?? "");
|
|
13828
14208
|
main.append(t, d);
|
|
13829
14209
|
row.append(av, main);
|
|
14210
|
+
if (isGroupRow(s)) {
|
|
14211
|
+
row.dataset.group = "1";
|
|
14212
|
+
const expander = doc.createElement("button");
|
|
14213
|
+
expander.className = "vela-sp-expander";
|
|
14214
|
+
expander.setAttribute("aria-label", "Show contracts");
|
|
14215
|
+
expander.appendChild(iconEl(this.expanded.has(groupKeyOf(s)) ? "chevron-down" : "chevron-right", doc));
|
|
14216
|
+
row.appendChild(expander);
|
|
14217
|
+
} else if (s.group != null && this.expanded.has(groupKeyOf(s))) row.dataset.member = "1";
|
|
13830
14218
|
if (venue) {
|
|
13831
14219
|
const badge = doc.createElement("span");
|
|
13832
14220
|
badge.className = "vela-sp-badge";
|
|
@@ -14443,6 +14831,9 @@ var CSS13 = `
|
|
|
14443
14831
|
}
|
|
14444
14832
|
.vela-mb-item:active { background: var(--vela-hover); }
|
|
14445
14833
|
.vela-mb-item .vela-icon { font-size: 18px; width: 18px; height: 18px; }
|
|
14834
|
+
/* A lit stop (the maximize toggle while something is isolated): the inverse
|
|
14835
|
+
"selected" chip \u2014 white on the dark theme, dark on the light one. */
|
|
14836
|
+
.vela-mb-item.vela-mb-on, .vela-mb-item.vela-mb-on:active { background: var(--vela-selected-bg); color: var(--vela-selected-fg); }
|
|
14446
14837
|
/* Left-aligned contributed actions get their own stops (the built-in indicators
|
|
14447
14838
|
slot) \u2014 the wrapper is layout-transparent so each stop flexes like a sibling. */
|
|
14448
14839
|
.vela-mb-actions { display: contents; }
|
|
@@ -14483,9 +14874,11 @@ var MobileBar = class {
|
|
|
14483
14874
|
this.actionsHost.className = "vela-mb-actions";
|
|
14484
14875
|
const onDrawings = opts.onDrawingsClick;
|
|
14485
14876
|
const drawings = onDrawings ? item("vela-mb-drawings", "Drawings", onDrawings, "pen") : null;
|
|
14877
|
+
const onMaximize = opts.onMaximizeClick;
|
|
14878
|
+
this.maxEl = onMaximize ? item("vela-mb-maximize", "Maximize chart", onMaximize, "maximize") : null;
|
|
14486
14879
|
const more = item("vela-mb-more", "More", opts.onMoreClick, "kebab");
|
|
14487
14880
|
const settings = item("vela-mb-settings", "Chart settings", opts.onSettingsClick, "gear");
|
|
14488
|
-
this.el.append(this.symbolEl, this.tfEl, ...indicators ? [indicators] : [], this.actionsHost, ...drawings ? [drawings] : [], more, settings);
|
|
14881
|
+
this.el.append(this.symbolEl, this.tfEl, ...indicators ? [indicators] : [], this.actionsHost, ...drawings ? [drawings] : [], ...this.maxEl ? [this.maxEl] : [], more, settings);
|
|
14489
14882
|
host.appendChild(this.el);
|
|
14490
14883
|
this.renderActions();
|
|
14491
14884
|
}
|
|
@@ -14517,6 +14910,14 @@ var MobileBar = class {
|
|
|
14517
14910
|
setTimeframe(tf) {
|
|
14518
14911
|
this.tfEl.textContent = timeframeLabel(tf);
|
|
14519
14912
|
}
|
|
14913
|
+
/** Light the maximize stop while something is isolated (a chart over the grid,
|
|
14914
|
+
* or a maximized pane inside the active chart) — inverse chip + restore glyph. */
|
|
14915
|
+
setMaximizeActive(on) {
|
|
14916
|
+
if (!this.maxEl) return;
|
|
14917
|
+
this.maxEl.classList.toggle("vela-mb-on", on);
|
|
14918
|
+
this.maxEl.setAttribute("aria-label", on ? "Restore layout" : "Maximize chart");
|
|
14919
|
+
this.maxEl.replaceChildren(iconEl(on ? "restore" : "maximize", this.el.ownerDocument));
|
|
14920
|
+
}
|
|
14520
14921
|
destroy() {
|
|
14521
14922
|
this.el.remove();
|
|
14522
14923
|
}
|
|
@@ -16694,7 +17095,7 @@ var DrawingToolbar = class {
|
|
|
16694
17095
|
this.root.replaceChildren();
|
|
16695
17096
|
this.groupCells.clear();
|
|
16696
17097
|
this.groupIcons.clear();
|
|
16697
|
-
this.cursorBtn = this.makeButton(CURSOR_ICON, "Cursor", () => this.
|
|
17098
|
+
this.cursorBtn = this.makeButton(CURSOR_ICON, "Cursor", () => this.onCursorClick());
|
|
16698
17099
|
this.root.appendChild(this.cursorBtn);
|
|
16699
17100
|
if (this.def.groups.length > 0) this.root.appendChild(this.divider());
|
|
16700
17101
|
for (const g of this.def.groups) {
|
|
@@ -16798,6 +17199,14 @@ var DrawingToolbar = class {
|
|
|
16798
17199
|
this.magnetIcon = icon2;
|
|
16799
17200
|
return cell;
|
|
16800
17201
|
}
|
|
17202
|
+
/** Cursor returns to select/idle: an active measure/eraser mode exits through its own
|
|
17203
|
+
* toggle callback (disarming a tool via `onArm(null)` alone can't — the host treats a
|
|
17204
|
+
* null arm as a no-op side effect of entering those modes), then the tool disarms. */
|
|
17205
|
+
onCursorClick() {
|
|
17206
|
+
if (this.measureActive) this.onMeasure();
|
|
17207
|
+
if (this.eraserActive) this.onEraser();
|
|
17208
|
+
this.onArm(null);
|
|
17209
|
+
}
|
|
16801
17210
|
/** Clicking the icon arms the group's last-used tool (it does NOT open the flyout). */
|
|
16802
17211
|
onGroupIconClick(group) {
|
|
16803
17212
|
const type = this.lastUsed.get(group.id) ?? group.tools[0]?.type;
|
|
@@ -17578,6 +17987,7 @@ var IndicatorHandleImpl = class {
|
|
|
17578
17987
|
constructor(id, title, controller, source) {
|
|
17579
17988
|
this.controller = controller;
|
|
17580
17989
|
this.schema = [];
|
|
17990
|
+
this.propsSchema = [];
|
|
17581
17991
|
this.visibleState = true;
|
|
17582
17992
|
this.bus = new TypedEventBus();
|
|
17583
17993
|
this.id = id;
|
|
@@ -17587,6 +17997,9 @@ var IndicatorHandleImpl = class {
|
|
|
17587
17997
|
get inputs() {
|
|
17588
17998
|
return this.schema;
|
|
17589
17999
|
}
|
|
18000
|
+
get props() {
|
|
18001
|
+
return this.propsSchema;
|
|
18002
|
+
}
|
|
17590
18003
|
get visible() {
|
|
17591
18004
|
return this.visibleState;
|
|
17592
18005
|
}
|
|
@@ -17596,6 +18009,12 @@ var IndicatorHandleImpl = class {
|
|
|
17596
18009
|
setInputs(values) {
|
|
17597
18010
|
this.controller.applyInputs(this.id, values);
|
|
17598
18011
|
}
|
|
18012
|
+
setProp(key, value) {
|
|
18013
|
+
this.controller.applyProps(this.id, { [key]: value });
|
|
18014
|
+
}
|
|
18015
|
+
setProps(values) {
|
|
18016
|
+
this.controller.applyProps(this.id, values);
|
|
18017
|
+
}
|
|
17599
18018
|
setVisible(visible) {
|
|
17600
18019
|
this.controller.setVisible(this.id, visible);
|
|
17601
18020
|
}
|
|
@@ -17615,6 +18034,9 @@ var IndicatorHandleImpl = class {
|
|
|
17615
18034
|
setSchema(schema) {
|
|
17616
18035
|
this.schema = schema;
|
|
17617
18036
|
}
|
|
18037
|
+
setPropsSchema(schema) {
|
|
18038
|
+
this.propsSchema = schema;
|
|
18039
|
+
}
|
|
17618
18040
|
/** Sync the public `visible` getter when the orchestrator changes visibility (API or legend eye). */
|
|
17619
18041
|
setVisibleState(visible) {
|
|
17620
18042
|
this.visibleState = visible;
|
|
@@ -17628,6 +18050,8 @@ var IndicatorHandleImpl = class {
|
|
|
17628
18050
|
function summarizeModel(model) {
|
|
17629
18051
|
const series = {};
|
|
17630
18052
|
for (const s of model.series) series[s.kind] = (series[s.kind] ?? 0) + 1;
|
|
18053
|
+
const countOverlay = (items) => items?.filter((i) => i.overlay === true).length ?? 0;
|
|
18054
|
+
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);
|
|
17631
18055
|
return {
|
|
17632
18056
|
id: model.id,
|
|
17633
18057
|
title: model.title,
|
|
@@ -17647,7 +18071,8 @@ function summarizeModel(model) {
|
|
|
17647
18071
|
tables: model.tables?.length ?? 0,
|
|
17648
18072
|
barColors: model.barColors?.length ?? 0,
|
|
17649
18073
|
trades: model.trades?.length ?? 0,
|
|
17650
|
-
inputs: model.inputs.length
|
|
18074
|
+
inputs: model.inputs.length,
|
|
18075
|
+
forcedOverlay
|
|
17651
18076
|
};
|
|
17652
18077
|
}
|
|
17653
18078
|
function inspectModels(models) {
|
|
@@ -17668,7 +18093,8 @@ function inspectModels(models) {
|
|
|
17668
18093
|
linefills: sum((s) => s.linefills),
|
|
17669
18094
|
tables: sum((s) => s.tables),
|
|
17670
18095
|
barColors: sum((s) => s.barColors),
|
|
17671
|
-
trades: sum((s) => s.trades)
|
|
18096
|
+
trades: sum((s) => s.trades),
|
|
18097
|
+
forcedOverlay: sum((s) => s.forcedOverlay)
|
|
17672
18098
|
}
|
|
17673
18099
|
};
|
|
17674
18100
|
}
|
|
@@ -17779,6 +18205,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
17779
18205
|
/** Invalidates detached async work (backfill loops, in-flight loads, gap heals):
|
|
17780
18206
|
* bumped by init(), setMarket() and destroy(). */
|
|
17781
18207
|
this.generation = 0;
|
|
18208
|
+
/** Aborts the in-flight PROGRESSIVE load's source polling on supersession — an
|
|
18209
|
+
* abandoned stream left polling to its own budget starves the browser's per-host
|
|
18210
|
+
* connection pool, and the NEXT symbol's very first fetch with it (measured). */
|
|
18211
|
+
this.progressiveAbort = null;
|
|
17782
18212
|
/** Awaiters racing a superseded load (setMarket callers) — released on every bump so they never hang. */
|
|
17783
18213
|
this.supersedeWaiters = [];
|
|
17784
18214
|
/** `history:complete` fired for the CURRENT load. Each market load re-arms the cycle
|
|
@@ -17812,7 +18242,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
17812
18242
|
for (const engine of engines) this.registerEngine(engine.language, engine);
|
|
17813
18243
|
this.defaultLanguage = config.defaultLanguage;
|
|
17814
18244
|
this.renderer.mount(container, config.theme);
|
|
17815
|
-
this.renderer.onInputChange(
|
|
18245
|
+
this.renderer.onInputChange(
|
|
18246
|
+
(e) => e.kind === "prop" ? this.applyProps(e.indicatorId, { [e.key]: e.value }) : this.applyInputs(e.indicatorId, { [e.key]: e.value })
|
|
18247
|
+
);
|
|
17816
18248
|
this.renderer.onRemoveIndicator((id) => this.removeIndicator(id));
|
|
17817
18249
|
this.renderer.onToggleIndicatorVisible?.((id, visible) => this.setVisible(id, visible));
|
|
17818
18250
|
this.paneActionUnsub = this.renderer.onPaneAction?.((a) => this.handlePaneAction(a)) ?? null;
|
|
@@ -17919,6 +18351,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
17919
18351
|
* superseded setMarket awaiters so their promises resolve instead of hanging. */
|
|
17920
18352
|
bumpGeneration() {
|
|
17921
18353
|
const gen = ++this.generation;
|
|
18354
|
+
this.progressiveAbort?.abort();
|
|
18355
|
+
this.progressiveAbort = null;
|
|
17922
18356
|
for (const w of this.supersedeWaiters.splice(0)) w();
|
|
17923
18357
|
return gen;
|
|
17924
18358
|
}
|
|
@@ -17972,7 +18406,47 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
17972
18406
|
const requested = market.bars ?? 500;
|
|
17973
18407
|
const initialRange = market.visibleRange;
|
|
17974
18408
|
const deep = !market.data?.length && initialRange == null && requested > SINGLE_LOAD_BARS;
|
|
17975
|
-
|
|
18409
|
+
let progressiveServed = false;
|
|
18410
|
+
if (!market.data?.length && initialRange == null && this.feed.loadProgressive) {
|
|
18411
|
+
let painted = false;
|
|
18412
|
+
const paint = (bars, final) => {
|
|
18413
|
+
if (this.generation !== gen || !final && bars.length === 0) return;
|
|
18414
|
+
this.setBarSeries(bars, painted ? { preserveView: true } : void 0);
|
|
18415
|
+
if (!painted && bars.length > 0) {
|
|
18416
|
+
painted = true;
|
|
18417
|
+
if (opts.firstLoad) this.activateBarLayers();
|
|
18418
|
+
if (!final) this.historyState = "backfill";
|
|
18419
|
+
}
|
|
18420
|
+
};
|
|
18421
|
+
const abort = new AbortController();
|
|
18422
|
+
this.progressiveAbort = abort;
|
|
18423
|
+
progressiveServed = await new Promise((firstPaint) => {
|
|
18424
|
+
let signaled = false;
|
|
18425
|
+
const signal = (served) => {
|
|
18426
|
+
if (!signaled) {
|
|
18427
|
+
signaled = true;
|
|
18428
|
+
firstPaint(served);
|
|
18429
|
+
}
|
|
18430
|
+
};
|
|
18431
|
+
abort.signal.addEventListener("abort", () => signal(true), { once: true });
|
|
18432
|
+
this.feed.loadProgressive(market, (bars) => {
|
|
18433
|
+
paint(bars, false);
|
|
18434
|
+
if (painted) signal(true);
|
|
18435
|
+
}, { signal: abort.signal }).then((full) => {
|
|
18436
|
+
if (this.progressiveAbort === abort) this.progressiveAbort = null;
|
|
18437
|
+
if (full == null) return signal(false);
|
|
18438
|
+
if (this.generation !== gen) return signal(true);
|
|
18439
|
+
paint(full, true);
|
|
18440
|
+
this.completeHistory(full.length >= requested ? "depth" : "genesis");
|
|
18441
|
+
signal(true);
|
|
18442
|
+
}).catch(() => {
|
|
18443
|
+
if (this.progressiveAbort === abort) this.progressiveAbort = null;
|
|
18444
|
+
if (this.generation === gen) this.completeHistory("aborted");
|
|
18445
|
+
signal(true);
|
|
18446
|
+
});
|
|
18447
|
+
});
|
|
18448
|
+
}
|
|
18449
|
+
if (progressiveServed) ; else if (deep && this.feed.loadRange) {
|
|
17976
18450
|
const head = await this.feed.load({ ...market, bars: Math.min(requested, CHUNK_BARS) });
|
|
17977
18451
|
if (this.generation !== gen) return;
|
|
17978
18452
|
this.setBarSeries(head);
|
|
@@ -18492,7 +18966,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18492
18966
|
const title = options.title ?? "Indicator";
|
|
18493
18967
|
const handle = new IndicatorHandleImpl(id, title, this, source);
|
|
18494
18968
|
this.handles.set(id, handle);
|
|
18495
|
-
this.registry.add({ id, title, source, options, inputValues: { ...options.inputs ?? {} } });
|
|
18969
|
+
this.registry.add({ id, title, source, options, inputValues: { ...options.inputs ?? {} }, propValues: { ...options.props ?? {} } });
|
|
18496
18970
|
void this.startIndicator(id, source, options, handle);
|
|
18497
18971
|
return handle;
|
|
18498
18972
|
}
|
|
@@ -18515,7 +18989,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18515
18989
|
return handle;
|
|
18516
18990
|
}
|
|
18517
18991
|
const inputValues = { ...descriptor.defaultInputs(), ...options.inputs ?? {} };
|
|
18518
|
-
this.registry.add({ id, title, source: type, inputValues, native: { type, instance: descriptor.create(), descriptor } });
|
|
18992
|
+
this.registry.add({ id, title, source: type, inputValues, propValues: {}, native: { type, instance: descriptor.create(), descriptor } });
|
|
18519
18993
|
handle.setSchema(descriptor.inputsSchema());
|
|
18520
18994
|
void this.startNativeIndicator(id, handle);
|
|
18521
18995
|
return handle;
|
|
@@ -18568,6 +19042,19 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18568
19042
|
if (record.session) record.session.update(record.inputValues);
|
|
18569
19043
|
else if (record.native && !record.hidden) record.native.instance.setInputs(record.inputValues);
|
|
18570
19044
|
}
|
|
19045
|
+
/** IndicatorController: re-run an indicator with merged declaration-prop overrides.
|
|
19046
|
+
* Same lifecycle as {@link applyInputs} — a prop change replays the whole script.
|
|
19047
|
+
* Script indicators only: natives have no declaration props. */
|
|
19048
|
+
applyProps(id, values) {
|
|
19049
|
+
const record = this.registry.get(id);
|
|
19050
|
+
if (!record?.session) return;
|
|
19051
|
+
record.propValues = { ...record.propValues, ...values };
|
|
19052
|
+
if (record.renderHandle) this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
|
|
19053
|
+
record.pendingStructural = true;
|
|
19054
|
+
if (!record.hidden) this.setLoading(record, true);
|
|
19055
|
+
record.pendingCause = "inputs";
|
|
19056
|
+
record.session.update(record.inputValues, record.propValues);
|
|
19057
|
+
}
|
|
18571
19058
|
/** IndicatorController: tear down an indicator and (if now empty) its pane. */
|
|
18572
19059
|
/** Live handles of every indicator on the chart (script + native), insertion order. */
|
|
18573
19060
|
listIndicators() {
|
|
@@ -18716,6 +19203,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18716
19203
|
for (const input of prepared.inputs) defaults2[input.key] = input.defval;
|
|
18717
19204
|
record.inputValues = { ...defaults2, ...record.inputValues };
|
|
18718
19205
|
handle.setSchema(prepared.inputs);
|
|
19206
|
+
const propDefaults = {};
|
|
19207
|
+
for (const prop of prepared.props ?? []) propDefaults[prop.key] = prop.defval;
|
|
19208
|
+
record.propValues = { ...propDefaults, ...record.propValues };
|
|
19209
|
+
handle.setPropsSchema(prepared.props ?? []);
|
|
18719
19210
|
this.mountLoadingPlaceholder(id, record);
|
|
18720
19211
|
this.executeIndicator(id, handle);
|
|
18721
19212
|
} catch (err) {
|
|
@@ -18739,6 +19230,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18739
19230
|
getBars: () => this.bars,
|
|
18740
19231
|
fetchSeries: (sym, tf, range) => this.fetchSeries(sym, tf, range),
|
|
18741
19232
|
inputs: record.inputValues,
|
|
19233
|
+
props: record.propValues,
|
|
18742
19234
|
visibleRange: this.currentVisibleRange(),
|
|
18743
19235
|
mode,
|
|
18744
19236
|
// Mid-backfill session starts (add / re-show / price-style re-execute) let the
|
|
@@ -18832,6 +19324,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18832
19324
|
const meta = record.prepared.meta;
|
|
18833
19325
|
const model = {
|
|
18834
19326
|
id,
|
|
19327
|
+
// Deliberately the FULL title, never a shorttitle: while the script loads the
|
|
19328
|
+
// legend identifies it by its full name; the compact shorttitle arrives with
|
|
19329
|
+
// the first computed model and takes over from there.
|
|
18835
19330
|
title: record.options?.title ?? meta.title,
|
|
18836
19331
|
overlay: meta.overlay,
|
|
18837
19332
|
paneHint: meta.overlay ? "price" : "new",
|
|
@@ -18840,7 +19335,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18840
19335
|
backgrounds: [],
|
|
18841
19336
|
priceLines: [],
|
|
18842
19337
|
inputs: record.prepared.inputs,
|
|
18843
|
-
inputValues: record.inputValues
|
|
19338
|
+
inputValues: record.inputValues,
|
|
19339
|
+
...record.prepared.props ? { props: record.prepared.props, propValues: record.propValues } : {}
|
|
18844
19340
|
};
|
|
18845
19341
|
const paneId = this.routePane(id, model, record.options ?? {});
|
|
18846
19342
|
this.placeModel(model, id, paneId);
|
|
@@ -19073,7 +19569,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19073
19569
|
record.model = model;
|
|
19074
19570
|
if (record.pendingStructural) {
|
|
19075
19571
|
record.renderHandle = this.renderer.mountIndicator(model);
|
|
19076
|
-
this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues);
|
|
19572
|
+
this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
|
|
19077
19573
|
record.pendingStructural = false;
|
|
19078
19574
|
} else {
|
|
19079
19575
|
this.renderer.updateIndicator(record.renderHandle, modelToValuePatch(model));
|
|
@@ -19100,16 +19596,17 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19100
19596
|
placeModel(model, id, paneId) {
|
|
19101
19597
|
model.id = id;
|
|
19102
19598
|
model.paneId = paneId;
|
|
19103
|
-
|
|
19104
|
-
for (const
|
|
19105
|
-
for (const
|
|
19599
|
+
const paneOf = (item) => item.overlay === true ? "price" : paneId;
|
|
19600
|
+
for (const series of model.series) series.paneId = paneOf(series);
|
|
19601
|
+
for (const fill of model.fills) fill.paneId = paneOf(fill);
|
|
19602
|
+
for (const bg of model.backgrounds) bg.paneId = paneOf(bg);
|
|
19106
19603
|
for (const line of model.priceLines) line.paneId = paneId;
|
|
19107
|
-
if (model.lines) for (const ln of model.lines) ln.paneId =
|
|
19108
|
-
if (model.boxes) for (const bx of model.boxes) bx.paneId =
|
|
19109
|
-
if (model.labels) for (const lb of model.labels) lb.paneId =
|
|
19110
|
-
if (model.polylines) for (const pl of model.polylines) pl.paneId =
|
|
19111
|
-
if (model.linefills) for (const lf of model.linefills) lf.paneId =
|
|
19112
|
-
if (model.tables) for (const tb of model.tables) tb.paneId =
|
|
19604
|
+
if (model.lines) for (const ln of model.lines) ln.paneId = paneOf(ln);
|
|
19605
|
+
if (model.boxes) for (const bx of model.boxes) bx.paneId = paneOf(bx);
|
|
19606
|
+
if (model.labels) for (const lb of model.labels) lb.paneId = paneOf(lb);
|
|
19607
|
+
if (model.polylines) for (const pl of model.polylines) pl.paneId = paneOf(pl);
|
|
19608
|
+
if (model.linefills) for (const lf of model.linefills) lf.paneId = paneOf(lf);
|
|
19609
|
+
if (model.tables) for (const tb of model.tables) tb.paneId = paneOf(tb);
|
|
19113
19610
|
}
|
|
19114
19611
|
emitContextChanged(id) {
|
|
19115
19612
|
if (!this.registry.get(id)?.session?.getContext) return;
|
|
@@ -19262,6 +19759,15 @@ var RendererControl = class {
|
|
|
19262
19759
|
setLegendActions(provider) {
|
|
19263
19760
|
this.renderer.setLegendActions?.(provider);
|
|
19264
19761
|
}
|
|
19762
|
+
/**
|
|
19763
|
+
* Wire the legend rows' HOST-CONTRIBUTED callout bubbles (the shells route the
|
|
19764
|
+
* plugin registry through this; see `registerLegendCallout`). Silent on a renderer
|
|
19765
|
+
* without the seam — contributed callouts simply never show there, same graceful
|
|
19766
|
+
* degradation as {@link setLegendActions}.
|
|
19767
|
+
*/
|
|
19768
|
+
setLegendCallouts(provider) {
|
|
19769
|
+
this.renderer.setLegendCallouts?.(provider);
|
|
19770
|
+
}
|
|
19265
19771
|
/**
|
|
19266
19772
|
* Replace the indicator legend's fold toggle with a host action (or restore it with
|
|
19267
19773
|
* `null`) — multi-chart shells point the chip at their indicator overview instead of
|
|
@@ -19771,6 +20277,10 @@ function inputVisible(when, values) {
|
|
|
19771
20277
|
}
|
|
19772
20278
|
|
|
19773
20279
|
// src/renderers/shared/IndicatorInputsDialog.ts
|
|
20280
|
+
var PROPS_TAB = "Properties";
|
|
20281
|
+
function bagOf(row, decl) {
|
|
20282
|
+
return decl.prop ? row.propValues : row.values;
|
|
20283
|
+
}
|
|
19774
20284
|
var SOURCES = ["close", "open", "high", "low", "hl2", "hlc3", "ohlc4", "volume"];
|
|
19775
20285
|
var IndicatorInputsDialog = class {
|
|
19776
20286
|
constructor(host) {
|
|
@@ -19781,6 +20291,7 @@ var IndicatorInputsDialog = class {
|
|
|
19781
20291
|
this.openId = null;
|
|
19782
20292
|
this.row = null;
|
|
19783
20293
|
this.snapshot = null;
|
|
20294
|
+
this.propSnapshot = null;
|
|
19784
20295
|
this.dialogTips = [];
|
|
19785
20296
|
/** Re-applies every `when` gate against current values; set while the dialog is open. */
|
|
19786
20297
|
this.refreshVisibility = null;
|
|
@@ -19803,10 +20314,11 @@ var IndicatorInputsDialog = class {
|
|
|
19803
20314
|
}
|
|
19804
20315
|
open(row) {
|
|
19805
20316
|
this.close();
|
|
19806
|
-
if (row.inputs.length === 0) return;
|
|
20317
|
+
if (row.inputs.length === 0 && row.props.length === 0) return;
|
|
19807
20318
|
this.row = row;
|
|
19808
20319
|
this.openId = row.id;
|
|
19809
20320
|
this.snapshot = { ...row.values };
|
|
20321
|
+
this.propSnapshot = { ...row.propValues };
|
|
19810
20322
|
ensureDialogStyles();
|
|
19811
20323
|
const t = this.host.theme();
|
|
19812
20324
|
const border = "var(--vela-border)";
|
|
@@ -19814,7 +20326,7 @@ var IndicatorInputsDialog = class {
|
|
|
19814
20326
|
const host = this.host.dialogHost() ?? this.host.container;
|
|
19815
20327
|
const ui = new Dialog({
|
|
19816
20328
|
host,
|
|
19817
|
-
title: row.
|
|
20329
|
+
title: row.title,
|
|
19818
20330
|
// Non-modal: live-edit dialog — a modal machine locks pointer events on the
|
|
19819
20331
|
// whole body, killing the chart and the body-portaled popovers.
|
|
19820
20332
|
modal: false,
|
|
@@ -19846,6 +20358,16 @@ var IndicatorInputsDialog = class {
|
|
|
19846
20358
|
text: () => "Close"
|
|
19847
20359
|
}));
|
|
19848
20360
|
const tabDefs = tabInputs(row.inputs);
|
|
20361
|
+
if (row.props.length > 0) {
|
|
20362
|
+
const propDecls = row.props.map((p) => {
|
|
20363
|
+
const d = { ...p, prop: true };
|
|
20364
|
+
delete d.tab;
|
|
20365
|
+
return d;
|
|
20366
|
+
});
|
|
20367
|
+
const existing = tabDefs.find((t2) => t2.name === PROPS_TAB);
|
|
20368
|
+
if (existing) existing.inputs.push(...propDecls);
|
|
20369
|
+
else tabDefs.push({ name: PROPS_TAB, inputs: propDecls });
|
|
20370
|
+
}
|
|
19849
20371
|
const tabs = document.createElement("div");
|
|
19850
20372
|
tabs.style.cssText = `display:flex;gap:12px;padding:0 20px;border-bottom:1px solid ${border};flex:0 0 auto;`;
|
|
19851
20373
|
const tabEls = [];
|
|
@@ -19948,9 +20470,10 @@ var IndicatorInputsDialog = class {
|
|
|
19948
20470
|
this.openId = null;
|
|
19949
20471
|
this.row = null;
|
|
19950
20472
|
this.snapshot = null;
|
|
20473
|
+
this.propSnapshot = null;
|
|
19951
20474
|
ui?.destroy();
|
|
19952
20475
|
}
|
|
19953
|
-
/** Restore every input to its open-time value (re-running the indicator), then close. */
|
|
20476
|
+
/** Restore every input and prop to its open-time value (re-running the indicator), then close. */
|
|
19954
20477
|
revertAndClose() {
|
|
19955
20478
|
const row = this.row;
|
|
19956
20479
|
const snap = this.snapshot;
|
|
@@ -19964,6 +20487,17 @@ var IndicatorInputsDialog = class {
|
|
|
19964
20487
|
}
|
|
19965
20488
|
}
|
|
19966
20489
|
}
|
|
20490
|
+
const propSnap = this.propSnapshot;
|
|
20491
|
+
if (row && propSnap) {
|
|
20492
|
+
for (const p of row.props) {
|
|
20493
|
+
const snapped = propSnap[p.key];
|
|
20494
|
+
const before = snapped !== void 0 ? snapped : p.defval;
|
|
20495
|
+
if (row.propValues[p.key] !== before) {
|
|
20496
|
+
row.propValues[p.key] = before;
|
|
20497
|
+
this.host.onChange?.({ indicatorId: row.id, key: p.key, value: before, kind: "prop" });
|
|
20498
|
+
}
|
|
20499
|
+
}
|
|
20500
|
+
}
|
|
19967
20501
|
this.close();
|
|
19968
20502
|
}
|
|
19969
20503
|
/** The footer's reset button — same chip as Cancel, pinned to the LEFT edge
|
|
@@ -19981,19 +20515,27 @@ var IndicatorInputsDialog = class {
|
|
|
19981
20515
|
const row = this.row;
|
|
19982
20516
|
if (!row) return;
|
|
19983
20517
|
const snap = this.snapshot;
|
|
20518
|
+
const propSnap = this.propSnapshot;
|
|
19984
20519
|
for (const inp of row.inputs) {
|
|
19985
20520
|
if (row.values[inp.key] !== inp.defval) {
|
|
19986
20521
|
row.values[inp.key] = inp.defval;
|
|
19987
20522
|
this.host.onChange?.({ indicatorId: row.id, key: inp.key, value: inp.defval });
|
|
19988
20523
|
}
|
|
19989
20524
|
}
|
|
20525
|
+
for (const p of row.props) {
|
|
20526
|
+
if (row.propValues[p.key] !== p.defval) {
|
|
20527
|
+
row.propValues[p.key] = p.defval;
|
|
20528
|
+
this.host.onChange?.({ indicatorId: row.id, key: p.key, value: p.defval, kind: "prop" });
|
|
20529
|
+
}
|
|
20530
|
+
}
|
|
19990
20531
|
this.open(row);
|
|
19991
20532
|
if (snap) this.snapshot = snap;
|
|
20533
|
+
if (propSnap) this.propSnapshot = propSnap;
|
|
19992
20534
|
}
|
|
19993
20535
|
/** Write one edit through: store it, notify the host, and re-apply the `when` gates. */
|
|
19994
|
-
commit(row,
|
|
19995
|
-
row.
|
|
19996
|
-
this.host.onChange?.({ indicatorId: row.id, key, value });
|
|
20536
|
+
commit(row, decl, value) {
|
|
20537
|
+
bagOf(row, decl)[decl.key] = value;
|
|
20538
|
+
this.host.onChange?.({ indicatorId: row.id, key: decl.key, value, ...decl.prop ? { kind: "prop" } : {} });
|
|
19997
20539
|
this.refreshVisibility?.();
|
|
19998
20540
|
}
|
|
19999
20541
|
/** One settings row (or several `inline=` inputs) placed into a section's grid. */
|
|
@@ -20020,7 +20562,7 @@ var IndicatorInputsDialog = class {
|
|
|
20020
20562
|
const id = idOf(lead);
|
|
20021
20563
|
const info = lead.tooltip ? this.infoButton(lead.tooltip) : void 0;
|
|
20022
20564
|
if (lead.type === "bool") {
|
|
20023
|
-
const current = Boolean(row
|
|
20565
|
+
const current = Boolean(bagOf(row, lead)[lead.key] ?? lead.defval);
|
|
20024
20566
|
return append(fieldRow({
|
|
20025
20567
|
label: nameOf(lead),
|
|
20026
20568
|
id,
|
|
@@ -20029,7 +20571,7 @@ var IndicatorInputsDialog = class {
|
|
|
20029
20571
|
toggle: {
|
|
20030
20572
|
id,
|
|
20031
20573
|
checked: current,
|
|
20032
|
-
onChange: (v) => this.commit(row, lead
|
|
20574
|
+
onChange: (v) => this.commit(row, lead, v)
|
|
20033
20575
|
}
|
|
20034
20576
|
}));
|
|
20035
20577
|
}
|
|
@@ -20054,8 +20596,8 @@ var IndicatorInputsDialog = class {
|
|
|
20054
20596
|
}
|
|
20055
20597
|
/** Build the typed control for one input, committing edits live via `onChange`. */
|
|
20056
20598
|
buildControl(row, inp, id) {
|
|
20057
|
-
const current = row
|
|
20058
|
-
const emit = (value) => this.commit(row, inp
|
|
20599
|
+
const current = bagOf(row, inp)[inp.key] ?? inp.defval;
|
|
20600
|
+
const emit = (value) => this.commit(row, inp, value);
|
|
20059
20601
|
if (inp.type === "bool") {
|
|
20060
20602
|
return buildFieldControl({ kind: "switch", id, checked: Boolean(current), onChange: (v) => emit(v) }).el;
|
|
20061
20603
|
}
|
|
@@ -20066,7 +20608,7 @@ var IndicatorInputsDialog = class {
|
|
|
20066
20608
|
kind: "color",
|
|
20067
20609
|
id,
|
|
20068
20610
|
theme: this.host.theme(),
|
|
20069
|
-
get: () => String(row
|
|
20611
|
+
get: () => String(bagOf(row, inp)[inp.key] ?? inp.defval),
|
|
20070
20612
|
onChange: (v) => emit(v)
|
|
20071
20613
|
}).el;
|
|
20072
20614
|
}
|
|
@@ -20629,6 +21171,7 @@ function timeParts(ts) {
|
|
|
20629
21171
|
}
|
|
20630
21172
|
|
|
20631
21173
|
// src/renderers/shared/InputsUI.ts
|
|
21174
|
+
var LEGEND_AT_TOP_ATTR = "data-vela-pane-at-top";
|
|
20632
21175
|
var STATUS_KEYFRAMES = "@keyframes vela-ind-pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.3;transform:scale(.6)}}";
|
|
20633
21176
|
var LEGEND_ICON_PX2 = 16;
|
|
20634
21177
|
var LEGEND_CTL_PX = 18;
|
|
@@ -20646,6 +21189,9 @@ var CLOSE_SVG = iconAt("close", LEGEND_ICON_PX2);
|
|
|
20646
21189
|
var FOLD_SVG = iconAt("chevron-up", LEGEND_ICON_PX2);
|
|
20647
21190
|
var UNFOLD_SVG = iconAt("chevron-down", LEGEND_ICON_PX2);
|
|
20648
21191
|
var OVERVIEW_SVG = iconAt("objects", LEGEND_ICON_PX2);
|
|
21192
|
+
function legendCalloutsDisplay(open2, hasCallouts) {
|
|
21193
|
+
return !open2 && hasCallouts ? "inline-flex" : "none";
|
|
21194
|
+
}
|
|
20649
21195
|
var InputsUI = class {
|
|
20650
21196
|
constructor(container, theme, paneBoundsOf) {
|
|
20651
21197
|
this.container = container;
|
|
@@ -20667,10 +21213,14 @@ var InputsUI = class {
|
|
|
20667
21213
|
this.moveApi = null;
|
|
20668
21214
|
/** Host-contributed legend actions, resolved PER ROW at render time (see setLegendActions). */
|
|
20669
21215
|
this.legendActions = null;
|
|
21216
|
+
/** Host-contributed callout bubbles, resolved PER ROW at render time (see setLegendCallouts). */
|
|
21217
|
+
this.legendCallouts = null;
|
|
20670
21218
|
/** Chrome-tooltip disposers, per row id — a tip open at removal must not outlive its row. */
|
|
20671
21219
|
this.rowTips = /* @__PURE__ */ new Map();
|
|
20672
21220
|
/** Same, for the contributed extras only (rebuilt independently by setLegendActions). */
|
|
20673
21221
|
this.extrasTips = /* @__PURE__ */ new Map();
|
|
21222
|
+
/** Same, for the contributed callouts only (rebuilt independently by setLegendCallouts). */
|
|
21223
|
+
this.calloutTips = /* @__PURE__ */ new Map();
|
|
20674
21224
|
/** Open "Move to" menu (kept so it can be torn down). */
|
|
20675
21225
|
this.moveMenu = null;
|
|
20676
21226
|
this.moveTargets = [];
|
|
@@ -20828,6 +21378,42 @@ var InputsUI = class {
|
|
|
20828
21378
|
extrasEl.appendChild(btn2);
|
|
20829
21379
|
}
|
|
20830
21380
|
}
|
|
21381
|
+
/**
|
|
21382
|
+
* Wire the host-contributed callout bubbles. Same contract as
|
|
21383
|
+
* {@link setLegendActions}: re-calling replaces the provider and re-projects the
|
|
21384
|
+
* rows already on screen.
|
|
21385
|
+
*/
|
|
21386
|
+
setLegendCallouts(provider) {
|
|
21387
|
+
this.legendCallouts = provider;
|
|
21388
|
+
for (const row of this.rows.values()) this.renderCallouts(row);
|
|
21389
|
+
}
|
|
21390
|
+
/** (Re)build one row's callout bubbles — tinted icon circles beside the title whose
|
|
21391
|
+
* click (when the view carries content) deploys a panel of text and actions. */
|
|
21392
|
+
renderCallouts(row) {
|
|
21393
|
+
this.disposeTips(this.calloutTips, row.id);
|
|
21394
|
+
for (const bubble of row.callouts) bubble.destroy();
|
|
21395
|
+
row.callouts = [];
|
|
21396
|
+
row.calloutsEl.replaceChildren();
|
|
21397
|
+
const views = this.legendCallouts?.(row.id) ?? [];
|
|
21398
|
+
row.calloutsEl.style.display = legendCalloutsDisplay(row.highlighted, views.length > 0);
|
|
21399
|
+
for (const view of views) {
|
|
21400
|
+
const bubble = new CalloutBubble({
|
|
21401
|
+
icon: view.icon,
|
|
21402
|
+
background: view.background,
|
|
21403
|
+
...view.color !== void 0 ? { color: view.color } : {},
|
|
21404
|
+
label: view.tooltip,
|
|
21405
|
+
...view.content !== void 0 ? { panel: view.content } : {},
|
|
21406
|
+
// The panel portals into the plot host and self-tokens: it must work on
|
|
21407
|
+
// a bare chart, where no `.vela-ui` token ancestor exists.
|
|
21408
|
+
host: this.container,
|
|
21409
|
+
theme: () => this.theme
|
|
21410
|
+
});
|
|
21411
|
+
bubble.el.dataset.legendCallout = view.id;
|
|
21412
|
+
this.tip(this.calloutTips, row.id, bubble.el, view.tooltip);
|
|
21413
|
+
row.callouts.push(bubble);
|
|
21414
|
+
row.calloutsEl.appendChild(bubble.el);
|
|
21415
|
+
}
|
|
21416
|
+
}
|
|
20831
21417
|
/** Reposition the per-pane legend containers after a layout change. */
|
|
20832
21418
|
reposition() {
|
|
20833
21419
|
for (const [paneId, lg] of this.legends) this.positionLegend(lg, paneId);
|
|
@@ -20995,6 +21581,7 @@ var InputsUI = class {
|
|
|
20995
21581
|
}
|
|
20996
21582
|
positionLegend(lg, paneId) {
|
|
20997
21583
|
const bounds = this.paneBoundsOf ? this.paneBoundsOf(paneId) : { top: 0, height: Infinity };
|
|
21584
|
+
lg.toggleAttribute(LEGEND_AT_TOP_ATTR, bounds.top === 0);
|
|
20998
21585
|
lg.style.display = bounds.height < 4 || !this.titlesVisible ? "none" : "flex";
|
|
20999
21586
|
const collapsed = this.paneCollapse.has(paneId);
|
|
21000
21587
|
const masterId = this.paneCollapse.get(paneId) ?? null;
|
|
@@ -21070,13 +21657,13 @@ var InputsUI = class {
|
|
|
21070
21657
|
}
|
|
21071
21658
|
/** Create or update an indicator's legend row (in the legend for its pane). */
|
|
21072
21659
|
upsert(id, title, inputs, values, paneId = "price", opts = {}) {
|
|
21073
|
-
const settingsTitle = opts.settingsTitle ?? title;
|
|
21074
21660
|
const existing = this.rows.get(id);
|
|
21075
21661
|
if (existing) {
|
|
21076
21662
|
existing.title = title;
|
|
21077
|
-
existing.settingsTitle = settingsTitle;
|
|
21078
21663
|
existing.inputs = inputs;
|
|
21079
21664
|
existing.values = { ...values };
|
|
21665
|
+
existing.props = opts.props ?? [];
|
|
21666
|
+
existing.propValues = { ...opts.propValues ?? {} };
|
|
21080
21667
|
existing.titleEl.textContent = title;
|
|
21081
21668
|
if (existing.paneId !== paneId) {
|
|
21082
21669
|
existing.paneId = paneId;
|
|
@@ -21128,6 +21715,9 @@ var InputsUI = class {
|
|
|
21128
21715
|
titleWrap.appendChild(beta);
|
|
21129
21716
|
}
|
|
21130
21717
|
el.appendChild(titleWrap);
|
|
21718
|
+
const calloutsEl = document.createElement("span");
|
|
21719
|
+
calloutsEl.style.cssText = `display:none;align-items:center;gap:4px;flex:none;margin-left:${LEGEND_TITLE_STATUS_GAP_PX}px;`;
|
|
21720
|
+
el.appendChild(calloutsEl);
|
|
21131
21721
|
el.appendChild(statusEl);
|
|
21132
21722
|
const valuesEl = document.createElement("span");
|
|
21133
21723
|
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;`;
|
|
@@ -21144,13 +21734,13 @@ var InputsUI = class {
|
|
|
21144
21734
|
eye.className = "vela-ind-ctl";
|
|
21145
21735
|
eye.style.cssText = LEGEND_CTL_CSS;
|
|
21146
21736
|
eye.addEventListener("click", () => {
|
|
21147
|
-
const
|
|
21148
|
-
this.onToggleVisible?.(id, Boolean(
|
|
21737
|
+
const row2 = this.rows.get(id);
|
|
21738
|
+
this.onToggleVisible?.(id, Boolean(row2?.hidden));
|
|
21149
21739
|
});
|
|
21150
21740
|
controlsEl.appendChild(eye);
|
|
21151
21741
|
eyeEl = eye;
|
|
21152
21742
|
}
|
|
21153
|
-
if (inputs.length > 0) {
|
|
21743
|
+
if (inputs.length > 0 || (opts.props ?? []).length > 0) {
|
|
21154
21744
|
const gear = document.createElement("button");
|
|
21155
21745
|
gear.type = "button";
|
|
21156
21746
|
gear.setAttribute("aria-label", "Settings");
|
|
@@ -21190,7 +21780,9 @@ var InputsUI = class {
|
|
|
21190
21780
|
controlsEl.appendChild(close);
|
|
21191
21781
|
el.appendChild(controlsEl);
|
|
21192
21782
|
this.attach(this.legendFor(paneId), el, !!opts.native);
|
|
21193
|
-
|
|
21783
|
+
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 };
|
|
21784
|
+
this.rows.set(id, row);
|
|
21785
|
+
this.renderCallouts(row);
|
|
21194
21786
|
this.syncFoldToggle();
|
|
21195
21787
|
}
|
|
21196
21788
|
/** Place a row in its pane's legend — native rows PREPEND (pinned to the top), Pine rows append. */
|
|
@@ -21199,9 +21791,11 @@ var InputsUI = class {
|
|
|
21199
21791
|
else container.appendChild(el);
|
|
21200
21792
|
}
|
|
21201
21793
|
/** Reflect programmatic input changes (so a re-opened dialog shows current values). */
|
|
21202
|
-
setValues(id, values) {
|
|
21794
|
+
setValues(id, values, props) {
|
|
21203
21795
|
const row = this.rows.get(id);
|
|
21204
|
-
if (row)
|
|
21796
|
+
if (!row) return;
|
|
21797
|
+
row.values = { ...row.values, ...values };
|
|
21798
|
+
if (props) row.propValues = { ...row.propValues, ...props };
|
|
21205
21799
|
}
|
|
21206
21800
|
/**
|
|
21207
21801
|
* Reflect an indicator's live status in its legend row: `'loading'` shows three pulsing
|
|
@@ -21283,8 +21877,13 @@ var InputsUI = class {
|
|
|
21283
21877
|
syncRowActions(row) {
|
|
21284
21878
|
const open2 = row.highlighted;
|
|
21285
21879
|
row.controlsEl.style.display = open2 || row.hidden ? "inline-flex" : "none";
|
|
21286
|
-
if (open2)
|
|
21287
|
-
|
|
21880
|
+
if (open2) {
|
|
21881
|
+
row.el.appendChild(row.statusEl);
|
|
21882
|
+
for (const bubble of row.callouts) bubble.hidePanel();
|
|
21883
|
+
} else {
|
|
21884
|
+
row.el.insertBefore(row.statusEl, row.valuesEl);
|
|
21885
|
+
}
|
|
21886
|
+
row.calloutsEl.style.display = legendCalloutsDisplay(open2, row.callouts.length > 0);
|
|
21288
21887
|
for (const child of Array.from(row.controlsEl.children)) {
|
|
21289
21888
|
if (!(child instanceof HTMLElement) || child === row.eyeEl) continue;
|
|
21290
21889
|
if (child === row.extrasEl) {
|
|
@@ -21302,10 +21901,12 @@ var InputsUI = class {
|
|
|
21302
21901
|
}
|
|
21303
21902
|
remove(id) {
|
|
21304
21903
|
const row = this.rows.get(id);
|
|
21904
|
+
for (const bubble of row?.callouts ?? []) bubble.destroy();
|
|
21305
21905
|
row?.el.remove();
|
|
21306
21906
|
this.rows.delete(id);
|
|
21307
21907
|
this.disposeTips(this.rowTips, id);
|
|
21308
21908
|
this.disposeTips(this.extrasTips, id);
|
|
21909
|
+
this.disposeTips(this.calloutTips, id);
|
|
21309
21910
|
if (this.selectedId === id) this.selectedId = null;
|
|
21310
21911
|
if (this.inputsDialog.openId === id) this.inputsDialog.close();
|
|
21311
21912
|
this.syncFoldToggle();
|
|
@@ -21325,6 +21926,8 @@ var InputsUI = class {
|
|
|
21325
21926
|
this.rowMenu = null;
|
|
21326
21927
|
for (const id of [...this.rowTips.keys()]) this.disposeTips(this.rowTips, id);
|
|
21327
21928
|
for (const id of [...this.extrasTips.keys()]) this.disposeTips(this.extrasTips, id);
|
|
21929
|
+
for (const id of [...this.calloutTips.keys()]) this.disposeTips(this.calloutTips, id);
|
|
21930
|
+
for (const row of this.rows.values()) for (const bubble of row.callouts) bubble.destroy();
|
|
21328
21931
|
for (const lg of this.legends.values()) lg.remove();
|
|
21329
21932
|
this.legends.clear();
|
|
21330
21933
|
this.rows.clear();
|
|
@@ -21370,7 +21973,7 @@ var ICONS = {
|
|
|
21370
21973
|
};
|
|
21371
21974
|
var STYLE_ID21 = "vela-pane-controls";
|
|
21372
21975
|
var ICON_PX = 12;
|
|
21373
|
-
var CLUSTER_PILL = "rgba(0,0,0,0.
|
|
21976
|
+
var CLUSTER_PILL = "rgba(0,0,0,0.65)";
|
|
21374
21977
|
function ensureStyles3() {
|
|
21375
21978
|
if (typeof document === "undefined" || document.getElementById(STYLE_ID21)) return;
|
|
21376
21979
|
const st = document.createElement("style");
|
|
@@ -21390,8 +21993,12 @@ var PaneControls = class {
|
|
|
21390
21993
|
this.deps = deps;
|
|
21391
21994
|
this.clusters = /* @__PURE__ */ new Map();
|
|
21392
21995
|
this.hoverPaneId = null;
|
|
21996
|
+
/** Mobile: hover clusters are meaningless without a cursor — suppressed; a
|
|
21997
|
+
* collapsed pane's standalone expand chip stays (the only way back up). */
|
|
21998
|
+
this.suspended = false;
|
|
21393
21999
|
/** Reveal the cluster for the pane under the cursor, resolved from the pointer's y in the plot. */
|
|
21394
22000
|
this.onPlotMove = (e) => {
|
|
22001
|
+
if (this.suspended) return;
|
|
21395
22002
|
const rect = this.plot.getBoundingClientRect();
|
|
21396
22003
|
const y = e.clientY - rect.top;
|
|
21397
22004
|
let hit = null;
|
|
@@ -21463,7 +22070,12 @@ var PaneControls = class {
|
|
|
21463
22070
|
}
|
|
21464
22071
|
if (p.count > 1) {
|
|
21465
22072
|
cluster.appendChild(
|
|
21466
|
-
this.button(p.maximized ? ICONS.restore : ICONS.maximize, p.maximized ? "Restore pane" : "Maximize pane", false, () => this.deps.onToggleMaximize(p.id), {
|
|
22073
|
+
this.button(p.maximized ? ICONS.restore : ICONS.maximize, p.maximized ? "Restore pane" : "Maximize pane", false, () => this.deps.onToggleMaximize(p.id), {
|
|
22074
|
+
role: "maximize",
|
|
22075
|
+
// Same inverse-chip treatment as the collapsed pane's expand toggle: the
|
|
22076
|
+
// maximized state must read as an active state, not just a swapped glyph.
|
|
22077
|
+
selected: p.maximized
|
|
22078
|
+
})
|
|
21467
22079
|
);
|
|
21468
22080
|
}
|
|
21469
22081
|
}
|
|
@@ -21500,17 +22112,18 @@ var PaneControls = class {
|
|
|
21500
22112
|
}
|
|
21501
22113
|
const hovered = id === this.hoverPaneId;
|
|
21502
22114
|
const hasButtons = cluster.children.length > 0;
|
|
21503
|
-
const
|
|
22115
|
+
const stateChipRole = p.collapsed ? "collapse" : !this.suspended && p.maximized ? "maximize" : null;
|
|
22116
|
+
const visible = hasButtons && (hovered || stateChipRole != null) && p.height > 8;
|
|
21504
22117
|
cluster.style.right = `${rightPx}px`;
|
|
21505
22118
|
cluster.style.top = p.collapsed ? `${p.top + Math.max(1, Math.round((p.height - 24) / 2))}px` : `${p.top + 4}px`;
|
|
21506
22119
|
cluster.style.display = visible ? "flex" : "none";
|
|
21507
22120
|
if (!visible) continue;
|
|
21508
|
-
const
|
|
21509
|
-
cluster.style.background =
|
|
22121
|
+
const soloChip = stateChipRole != null && !hovered;
|
|
22122
|
+
cluster.style.background = soloChip ? "transparent" : CLUSTER_PILL;
|
|
21510
22123
|
for (const child of cluster.children) {
|
|
21511
22124
|
const btn2 = child;
|
|
21512
22125
|
btn2.style.display = "inline-flex";
|
|
21513
|
-
btn2.style.visibility =
|
|
22126
|
+
btn2.style.visibility = soloChip && btn2.dataset.role !== stateChipRole ? "hidden" : "visible";
|
|
21514
22127
|
}
|
|
21515
22128
|
}
|
|
21516
22129
|
}
|
|
@@ -21520,6 +22133,14 @@ var PaneControls = class {
|
|
|
21520
22133
|
this.hoverPaneId = paneId;
|
|
21521
22134
|
this.reposition();
|
|
21522
22135
|
}
|
|
22136
|
+
/** Mobile suppression: no hover clusters (touch has no cursor; the shell's own
|
|
22137
|
+
* chrome covers maximize), while collapsed panes keep their expand chips. */
|
|
22138
|
+
setSuspended(on) {
|
|
22139
|
+
if (on === this.suspended) return;
|
|
22140
|
+
this.suspended = on;
|
|
22141
|
+
if (on) this.hoverPaneId = null;
|
|
22142
|
+
this.reposition();
|
|
22143
|
+
}
|
|
21523
22144
|
destroy() {
|
|
21524
22145
|
this.plot.removeEventListener("pointermove", this.onPlotMove);
|
|
21525
22146
|
this.plot.removeEventListener("pointerleave", this.onPlotLeave);
|
|
@@ -21670,131 +22291,6 @@ var AxisScaleButtons = class {
|
|
|
21670
22291
|
}
|
|
21671
22292
|
};
|
|
21672
22293
|
|
|
21673
|
-
// src/renderers/shared/TableOverlay.ts
|
|
21674
|
-
var SIZE_PX3 = {
|
|
21675
|
-
auto: 13,
|
|
21676
|
-
tiny: 10,
|
|
21677
|
-
small: 11,
|
|
21678
|
-
normal: 13,
|
|
21679
|
-
large: 16,
|
|
21680
|
-
huge: 20
|
|
21681
|
-
};
|
|
21682
|
-
var TableOverlay = class {
|
|
21683
|
-
constructor(container, theme, paneBounds) {
|
|
21684
|
-
this.container = container;
|
|
21685
|
-
this.theme = theme;
|
|
21686
|
-
this.paneBounds = paneBounds;
|
|
21687
|
-
this.lastTables = [];
|
|
21688
|
-
if (getComputedStyle(container).position === "static") container.style.position = "relative";
|
|
21689
|
-
this.root = document.createElement("div");
|
|
21690
|
-
Object.assign(this.root.style, {
|
|
21691
|
-
position: "absolute",
|
|
21692
|
-
inset: "0",
|
|
21693
|
-
pointerEvents: "none",
|
|
21694
|
-
overflow: "hidden",
|
|
21695
|
-
zIndex: "3"
|
|
21696
|
-
});
|
|
21697
|
-
container.appendChild(this.root);
|
|
21698
|
-
}
|
|
21699
|
-
update(tables) {
|
|
21700
|
-
this.lastTables = tables;
|
|
21701
|
-
this.root.replaceChildren();
|
|
21702
|
-
for (const t of tables) this.root.appendChild(this.renderTable(t));
|
|
21703
|
-
}
|
|
21704
|
-
/** Re-render at the current pane geometry — after layout settles or on resize. */
|
|
21705
|
-
reposition() {
|
|
21706
|
-
if (this.root.isConnected) this.update(this.lastTables);
|
|
21707
|
-
}
|
|
21708
|
-
/** Show/hide the whole overlay. Tables anchor to pane corners, not to bars, so unlike the
|
|
21709
|
-
* series content they DON'T vanish with an emptied chart — the loading state hides them. */
|
|
21710
|
-
setVisible(visible) {
|
|
21711
|
-
this.root.style.display = visible ? "" : "none";
|
|
21712
|
-
}
|
|
21713
|
-
destroy() {
|
|
21714
|
-
this.root.remove();
|
|
21715
|
-
}
|
|
21716
|
-
renderTable(t) {
|
|
21717
|
-
const wrap = document.createElement("div");
|
|
21718
|
-
wrap.style.position = "absolute";
|
|
21719
|
-
this.anchor(wrap, t.position, this.paneBounds(t.paneId));
|
|
21720
|
-
const table = document.createElement("table");
|
|
21721
|
-
Object.assign(table.style, {
|
|
21722
|
-
borderCollapse: "collapse",
|
|
21723
|
-
background: t.bgColor ?? "transparent",
|
|
21724
|
-
fontFamily: this.theme.fontFamily || "sans-serif",
|
|
21725
|
-
// Frame as an inset shadow (not a `border`) so it stays independent of the
|
|
21726
|
-
// collapsed cell borders even when frame_width != border_width.
|
|
21727
|
-
boxShadow: t.frameColor && t.frameWidth > 0 ? `inset 0 0 0 ${t.frameWidth}px ${t.frameColor}` : "none",
|
|
21728
|
-
border: "none",
|
|
21729
|
-
tableLayout: "auto",
|
|
21730
|
-
// Re-enable pointer events on the table only (root is none) so cell tooltips work.
|
|
21731
|
-
pointerEvents: "auto"
|
|
21732
|
-
});
|
|
21733
|
-
const span = /* @__PURE__ */ new Map();
|
|
21734
|
-
const skip = /* @__PURE__ */ new Set();
|
|
21735
|
-
for (const m of t.merges) {
|
|
21736
|
-
span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
|
|
21737
|
-
for (let r = m.startRow; r <= m.endRow; r += 1) {
|
|
21738
|
-
for (let c = m.startCol; c <= m.endCol; c += 1) {
|
|
21739
|
-
if (r !== m.startRow || c !== m.startCol) skip.add(`${r}:${c}`);
|
|
21740
|
-
}
|
|
21741
|
-
}
|
|
21742
|
-
}
|
|
21743
|
-
const cellBorder = t.borderColor && t.borderWidth > 0 ? `${t.borderWidth}px solid ${t.borderColor}` : "none";
|
|
21744
|
-
for (let r = 0; r < t.rows; r += 1) {
|
|
21745
|
-
const tr = document.createElement("tr");
|
|
21746
|
-
for (let c = 0; c < t.columns; c += 1) {
|
|
21747
|
-
const cell = t.cells[r]?.[c] ?? null;
|
|
21748
|
-
if (skip.has(`${r}:${c}`) || cell?.merged) continue;
|
|
21749
|
-
const td = document.createElement("td");
|
|
21750
|
-
const sp = span.get(`${r}:${c}`);
|
|
21751
|
-
if (sp) {
|
|
21752
|
-
if (sp.cs > 1) td.colSpan = sp.cs;
|
|
21753
|
-
if (sp.rs > 1) td.rowSpan = sp.rs;
|
|
21754
|
-
}
|
|
21755
|
-
Object.assign(td.style, {
|
|
21756
|
-
border: cellBorder,
|
|
21757
|
-
padding: "2px 6px",
|
|
21758
|
-
background: cell?.bgColor ?? "transparent",
|
|
21759
|
-
color: cell?.textColor ?? this.theme.textColor,
|
|
21760
|
-
textAlign: cell?.hAlign ?? "center",
|
|
21761
|
-
verticalAlign: cell?.vAlign === "top" ? "top" : cell?.vAlign === "bottom" ? "bottom" : "middle",
|
|
21762
|
-
fontSize: `${SIZE_PX3[cell?.textSize ?? "normal"]}px`,
|
|
21763
|
-
fontFamily: cell?.fontFamily === "monospace" ? "monospace" : "inherit",
|
|
21764
|
-
fontWeight: cell?.bold ? "bold" : "normal",
|
|
21765
|
-
fontStyle: cell?.italic ? "italic" : "normal",
|
|
21766
|
-
whiteSpace: "pre-line"
|
|
21767
|
-
});
|
|
21768
|
-
if (cell?.tooltip) td.title = cell.tooltip;
|
|
21769
|
-
td.textContent = cell?.text ?? "";
|
|
21770
|
-
tr.appendChild(td);
|
|
21771
|
-
}
|
|
21772
|
-
table.appendChild(tr);
|
|
21773
|
-
}
|
|
21774
|
-
wrap.appendChild(table);
|
|
21775
|
-
return wrap;
|
|
21776
|
-
}
|
|
21777
|
-
/**
|
|
21778
|
-
* Position the wrapper at a Pine `position.*` corner/edge of the table's PANE
|
|
21779
|
-
* (not the whole chart), inset past the right price axis so it never overlaps
|
|
21780
|
-
* the Y-axis labels.
|
|
21781
|
-
*/
|
|
21782
|
-
anchor(el, position, b) {
|
|
21783
|
-
const m = 6;
|
|
21784
|
-
const containerH = this.root.clientHeight || this.container.clientHeight;
|
|
21785
|
-
const paneBottom = b.top + b.height;
|
|
21786
|
-
if (position.startsWith("top")) el.style.top = `${b.top + m}px`;
|
|
21787
|
-
else if (position.startsWith("bottom")) el.style.bottom = `${Math.max(0, containerH - paneBottom) + m}px`;
|
|
21788
|
-
else el.style.top = `${b.top + b.height / 2}px`;
|
|
21789
|
-
if (position.endsWith("left")) el.style.left = `${m}px`;
|
|
21790
|
-
else if (position.endsWith("right")) el.style.right = `${b.rightAxis + m}px`;
|
|
21791
|
-
else el.style.left = `calc(50% - ${b.rightAxis / 2}px)`;
|
|
21792
|
-
const tx = position.endsWith("center") ? "-50%" : "0";
|
|
21793
|
-
const ty = position.startsWith("middle") ? "-50%" : "0";
|
|
21794
|
-
if (tx !== "0" || ty !== "0") el.style.transform = `translate(${tx}, ${ty})`;
|
|
21795
|
-
}
|
|
21796
|
-
};
|
|
21797
|
-
|
|
21798
22294
|
// src/renderers/native/capabilities.ts
|
|
21799
22295
|
var NATIVE_CAPABILITIES = {
|
|
21800
22296
|
panes: true,
|
|
@@ -21815,7 +22311,7 @@ var NATIVE_CAPABILITIES = {
|
|
|
21815
22311
|
drawingDepth: true,
|
|
21816
22312
|
// drawings share the series' z space (backend-composited interleave layers)
|
|
21817
22313
|
tables: true,
|
|
21818
|
-
//
|
|
22314
|
+
// canvas-painted into the owning indicator's interleave slice
|
|
21819
22315
|
trades: true,
|
|
21820
22316
|
// strategy order-fill markers (arrows + labels + fill-price ticks)
|
|
21821
22317
|
inputsUI: true
|
|
@@ -21846,6 +22342,9 @@ function candleTier(spacing) {
|
|
|
21846
22342
|
if (spacing < CANDLE_BODY_MIN_SPACING) return "wick";
|
|
21847
22343
|
return "full";
|
|
21848
22344
|
}
|
|
22345
|
+
function snapY(yCss, dpr) {
|
|
22346
|
+
return Math.round(yCss * dpr) / dpr;
|
|
22347
|
+
}
|
|
21849
22348
|
function candleGeometry(xCss, spacing, dpr, bodyScale = 1) {
|
|
21850
22349
|
const wickDev = Math.max(1, Math.round(wickWidth(spacing) * dpr));
|
|
21851
22350
|
const wickLeftDev = Math.round(xCss * dpr - wickDev / 2);
|
|
@@ -22368,8 +22867,12 @@ var WebGL2Backend = class {
|
|
|
22368
22867
|
return sc === pane.scale ? pane : { ...pane, scale: sc };
|
|
22369
22868
|
};
|
|
22370
22869
|
b.alpha = this.modelAlpha;
|
|
22371
|
-
for (const m of models) for (const bgSpan of m.backgrounds) this.emitBackground(b, bgSpan, pane, coords);
|
|
22372
|
-
|
|
22870
|
+
for (const m of models) for (const bgSpan of m.backgrounds) if (bgSpan.overlay !== true) this.emitBackground(b, bgSpan, pane, coords);
|
|
22871
|
+
if (isPrice) {
|
|
22872
|
+
for (const m of scene.indicators.values()) {
|
|
22873
|
+
for (const bgSpan of m.backgrounds) if (bgSpan.overlay === true) this.emitBackground(b, bgSpan, pane, coords);
|
|
22874
|
+
}
|
|
22875
|
+
}
|
|
22373
22876
|
const drawCandles = isPrice && !scene.candlesHidden;
|
|
22374
22877
|
let candleDrawn = false;
|
|
22375
22878
|
for (const m of models) {
|
|
@@ -22383,15 +22886,26 @@ var WebGL2Backend = class {
|
|
|
22383
22886
|
b.alpha = this.modelAlpha;
|
|
22384
22887
|
const off = scene.offsetOf(m.id);
|
|
22385
22888
|
const mp = effPane(m);
|
|
22386
|
-
for (const
|
|
22889
|
+
for (const f of m.fills) if (f.overlay !== true) this.emitFill(b, m, f, mp, coords, i0, i1, off);
|
|
22890
|
+
for (const s of m.series) if (s.overlay !== true) this.emitSeries(b, s, mp, coords, i0, i1, theme, off);
|
|
22387
22891
|
}
|
|
22388
22892
|
if (drawCandles && !candleDrawn) {
|
|
22389
22893
|
drawSlicesUpTo(scene.candleZ);
|
|
22390
22894
|
b.alpha = this.candleStructureAlpha;
|
|
22391
22895
|
this.emitPriceSeries(b, scene, i0, i1, coords, pane, theme, barColorMap, dataW);
|
|
22392
22896
|
}
|
|
22393
|
-
drawSlicesUpTo(Infinity);
|
|
22394
22897
|
b.alpha = this.modelAlpha;
|
|
22898
|
+
if (isPrice) {
|
|
22899
|
+
for (const m of scene.indicators.values()) {
|
|
22900
|
+
const off = scene.offsetOf(m.id);
|
|
22901
|
+
for (const f of m.fills) if (f.overlay === true) this.emitFill(b, m, f, pane, coords, i0, i1, off);
|
|
22902
|
+
}
|
|
22903
|
+
for (const m of scene.indicators.values()) {
|
|
22904
|
+
const off = scene.offsetOf(m.id);
|
|
22905
|
+
for (const s of m.series) if (s.overlay === true) this.emitSeries(b, s, pane, coords, i0, i1, theme, off);
|
|
22906
|
+
}
|
|
22907
|
+
}
|
|
22908
|
+
drawSlicesUpTo(Infinity);
|
|
22395
22909
|
for (const m of models) {
|
|
22396
22910
|
const mp = effPane(m);
|
|
22397
22911
|
for (const pl of m.priceLines) this.emitHline(b, pl, mp, coords, dataW, theme);
|
|
@@ -22438,7 +22952,7 @@ var WebGL2Backend = class {
|
|
|
22438
22952
|
for (const pane of scene.orderedPanes()) {
|
|
22439
22953
|
const b = this.glowBatch;
|
|
22440
22954
|
b.reset();
|
|
22441
|
-
this.emitGlowSources(b, scene
|
|
22955
|
+
this.emitGlowSources(b, scene, pane, coords, i0, i1);
|
|
22442
22956
|
if (b.vertexCount === 0) continue;
|
|
22443
22957
|
const topH = Math.round(pane.bounds.top * dpr * 0.5);
|
|
22444
22958
|
const botH = Math.round((pane.bounds.top + pane.bounds.height) * dpr * 0.5);
|
|
@@ -22507,15 +23021,23 @@ var WebGL2Backend = class {
|
|
|
22507
23021
|
gl.bindVertexArray(null);
|
|
22508
23022
|
return true;
|
|
22509
23023
|
}
|
|
22510
|
-
/** The "neon" elements that glow: line/area/step lines + point markers (not candles/fills/bars).
|
|
22511
|
-
|
|
22512
|
-
|
|
22513
|
-
|
|
22514
|
-
|
|
22515
|
-
|
|
22516
|
-
|
|
22517
|
-
|
|
22518
|
-
|
|
23024
|
+
/** The "neon" elements that glow: line/area/step lines + point markers (not candles/fills/bars).
|
|
23025
|
+
* Routing mirrors the main pass: own series per pane, force_overlay series on the price pane. */
|
|
23026
|
+
emitGlowSources(b, scene, pane, coords, i0, i1) {
|
|
23027
|
+
const emitOne = (s, off) => {
|
|
23028
|
+
if (!isLineLikeSeries(s) || s.visible === false) return;
|
|
23029
|
+
if (s.kind === "histogram" || s.kind === "columns") return;
|
|
23030
|
+
if (s.kind === "circles" || s.kind === "cross") this.emitPointMarkers(b, s, pane, coords, i0, i1, off);
|
|
23031
|
+
else this.emitPolyline(b, s, pane, coords, i0, i1, s.kind === "step", off);
|
|
23032
|
+
};
|
|
23033
|
+
for (const m of scene.indicatorsForPane(pane.id)) {
|
|
23034
|
+
const off = scene.offsetOf(m.id);
|
|
23035
|
+
for (const s of m.series) if (s.overlay !== true) emitOne(s, off);
|
|
23036
|
+
}
|
|
23037
|
+
if (pane.kind === "price") {
|
|
23038
|
+
for (const m of scene.indicators.values()) {
|
|
23039
|
+
const off = scene.offsetOf(m.id);
|
|
23040
|
+
for (const s of m.series) if (s.overlay === true) emitOne(s, off);
|
|
22519
23041
|
}
|
|
22520
23042
|
}
|
|
22521
23043
|
}
|
|
@@ -22778,12 +23300,12 @@ var WebGL2Backend = class {
|
|
|
22778
23300
|
if (drawBody) {
|
|
22779
23301
|
const oY = coords.priceToY(bar.open, pane.scale, pane.bounds);
|
|
22780
23302
|
const cY = coords.priceToY(bar.close, pane.scale, pane.bounds);
|
|
22781
|
-
bodyTop = Math.min(oY, cY);
|
|
22782
|
-
bodyH = Math.max(1, Math.
|
|
23303
|
+
bodyTop = snapY(Math.min(oY, cY), coords.dpr);
|
|
23304
|
+
bodyH = Math.max(1 / coords.dpr, snapY(Math.max(oY, cY), coords.dpr) - bodyTop);
|
|
22783
23305
|
}
|
|
22784
23306
|
if (cs.wickVisible) {
|
|
22785
|
-
const hY = coords.priceToY(bar.high, pane.scale, pane.bounds);
|
|
22786
|
-
const lY = coords.priceToY(bar.low, pane.scale, pane.bounds);
|
|
23307
|
+
const hY = snapY(coords.priceToY(bar.high, pane.scale, pane.bounds), coords.dpr);
|
|
23308
|
+
const lY = snapY(coords.priceToY(bar.low, pane.scale, pane.bounds), coords.dpr);
|
|
22787
23309
|
b.alpha = this.candleStructureAlpha;
|
|
22788
23310
|
const wCol = parseColor((isUp ? cs.wickUpColor : cs.wickDownColor) ?? (drawBody ? dir : bodyColorStr));
|
|
22789
23311
|
if (drawBody) {
|
|
@@ -22798,9 +23320,9 @@ var WebGL2Backend = class {
|
|
|
22798
23320
|
b.alpha = this.candleBodyAlpha;
|
|
22799
23321
|
b.rect(g.bodyX, bodyTop, g.bodyW, bodyH, c);
|
|
22800
23322
|
}
|
|
22801
|
-
if (cs.borderVisible ||
|
|
23323
|
+
if (cs.borderVisible || fading && cs.bodyVisible) {
|
|
22802
23324
|
b.alpha = this.candleStructureAlpha;
|
|
22803
|
-
const bord = cs.borderVisible
|
|
23325
|
+
const bord = cs.borderVisible ? parseColor((isUp ? cs.borderUpColor : cs.borderDownColor) ?? bodyColorStr) : c;
|
|
22804
23326
|
const bw = Math.max(0, g.bodyW - 1);
|
|
22805
23327
|
const bh = Math.max(0, bodyH - 1);
|
|
22806
23328
|
b.rectStroke(g.bodyX + 0.5, bodyTop + 0.5, bw, bh, 1, bord);
|
|
@@ -24116,9 +24638,10 @@ var SceneGraph = class {
|
|
|
24116
24638
|
* so each indicator arrives behind the candles (and behind older indicators);
|
|
24117
24639
|
* `setIndicatorZ`/`bringToFront`/`sendToBack` change it. */
|
|
24118
24640
|
this.seriesZ = /* @__PURE__ */ new Map();
|
|
24119
|
-
/** Per-pane raster layers of
|
|
24641
|
+
/** Per-pane raster layers of drawings interleaved into the series stack (each
|
|
24642
|
+
* indicator's Pine drawings at its model's z, plus in-stack user drawings) — each a
|
|
24120
24643
|
* prepainted canvas the backend composites just before the series carrying `beforeZ`.
|
|
24121
|
-
* Rebuilt by the renderer per data frame
|
|
24644
|
+
* Rebuilt by the renderer per data frame. */
|
|
24122
24645
|
this.drawingSlices = /* @__PURE__ */ new Map();
|
|
24123
24646
|
/** Per-model index offset: the chart bar index of the model's `anchorTime` — its
|
|
24124
24647
|
* index-aligned payloads (dense series arrays, `bar_index` drawings) count from that
|
|
@@ -24352,8 +24875,12 @@ var Canvas2dBackend = class {
|
|
|
24352
24875
|
return sc === pane.scale ? pane : { ...pane, scale: sc };
|
|
24353
24876
|
};
|
|
24354
24877
|
ctx.globalAlpha = this.modelAlpha;
|
|
24355
|
-
for (const m of models) for (const bg of m.backgrounds) this.drawBackground(ctx, bg, pane, coords);
|
|
24356
|
-
|
|
24878
|
+
for (const m of models) for (const bg of m.backgrounds) if (bg.overlay !== true) this.drawBackground(ctx, bg, pane, coords);
|
|
24879
|
+
if (isPrice) {
|
|
24880
|
+
for (const m of scene.indicators.values()) {
|
|
24881
|
+
for (const bg of m.backgrounds) if (bg.overlay === true) this.drawBackground(ctx, bg, pane, coords);
|
|
24882
|
+
}
|
|
24883
|
+
}
|
|
24357
24884
|
const slices = scene.drawingSlices.get(pane.id) ?? [];
|
|
24358
24885
|
let si = 0;
|
|
24359
24886
|
const drawSlicesUpTo = (z) => {
|
|
@@ -24375,13 +24902,25 @@ var Canvas2dBackend = class {
|
|
|
24375
24902
|
ctx.globalAlpha = this.modelAlpha;
|
|
24376
24903
|
const off = scene.offsetOf(m.id);
|
|
24377
24904
|
const mp = effPane(m);
|
|
24378
|
-
for (const
|
|
24905
|
+
for (const f of m.fills) if (f.overlay !== true) this.drawFill(ctx, m, f, mp, coords, i0, i1, off);
|
|
24906
|
+
for (const s of m.series) if (s.overlay !== true) this.drawSeries(ctx, s, mp, coords, i0, i1, theme, off);
|
|
24379
24907
|
}
|
|
24380
24908
|
if (drawCandles && !candleDrawn) {
|
|
24381
24909
|
drawSlicesUpTo(scene.candleZ);
|
|
24382
24910
|
ctx.globalAlpha = this.candleStructureAlpha;
|
|
24383
24911
|
this.drawPriceSeries(ctx, scene, i0, i1, coords, pane, theme, barColorMap, dataW);
|
|
24384
24912
|
}
|
|
24913
|
+
if (isPrice) {
|
|
24914
|
+
ctx.globalAlpha = this.modelAlpha;
|
|
24915
|
+
for (const m of scene.indicators.values()) {
|
|
24916
|
+
const off = scene.offsetOf(m.id);
|
|
24917
|
+
for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0, i1, off);
|
|
24918
|
+
}
|
|
24919
|
+
for (const m of scene.indicators.values()) {
|
|
24920
|
+
const off = scene.offsetOf(m.id);
|
|
24921
|
+
for (const s of m.series) if (s.overlay === true) this.drawSeries(ctx, s, pane, coords, i0, i1, theme, off);
|
|
24922
|
+
}
|
|
24923
|
+
}
|
|
24385
24924
|
drawSlicesUpTo(Infinity);
|
|
24386
24925
|
ctx.globalAlpha = this.modelAlpha;
|
|
24387
24926
|
for (const m of models) {
|
|
@@ -24626,13 +25165,13 @@ var Canvas2dBackend = class {
|
|
|
24626
25165
|
if (drawBody) {
|
|
24627
25166
|
const oY = coords.priceToY(b.open, pane.scale, pane.bounds);
|
|
24628
25167
|
const cY = coords.priceToY(b.close, pane.scale, pane.bounds);
|
|
24629
|
-
top = Math.min(oY, cY);
|
|
24630
|
-
bodyH = Math.max(1, Math.
|
|
25168
|
+
top = snapY(Math.min(oY, cY), coords.dpr);
|
|
25169
|
+
bodyH = Math.max(1 / coords.dpr, snapY(Math.max(oY, cY), coords.dpr) - top);
|
|
24631
25170
|
}
|
|
24632
25171
|
if (cs.wickVisible) {
|
|
24633
25172
|
const wick = (up ? cs.wickUpColor : cs.wickDownColor) ?? (drawBody ? dir : color);
|
|
24634
|
-
const hY = coords.priceToY(b.high, pane.scale, pane.bounds);
|
|
24635
|
-
const lY = coords.priceToY(b.low, pane.scale, pane.bounds);
|
|
25173
|
+
const hY = snapY(coords.priceToY(b.high, pane.scale, pane.bounds), coords.dpr);
|
|
25174
|
+
const lY = snapY(coords.priceToY(b.low, pane.scale, pane.bounds), coords.dpr);
|
|
24636
25175
|
ctx.globalAlpha = this.candleStructureAlpha;
|
|
24637
25176
|
ctx.strokeStyle = wick;
|
|
24638
25177
|
ctx.lineWidth = g.wickW;
|
|
@@ -24654,9 +25193,9 @@ var Canvas2dBackend = class {
|
|
|
24654
25193
|
ctx.fillStyle = color;
|
|
24655
25194
|
ctx.fillRect(g.bodyX, top, g.bodyW, bodyH);
|
|
24656
25195
|
}
|
|
24657
|
-
if (cs.borderVisible ||
|
|
25196
|
+
if (cs.borderVisible || fading && cs.bodyVisible) {
|
|
24658
25197
|
ctx.globalAlpha = this.candleStructureAlpha;
|
|
24659
|
-
ctx.strokeStyle = cs.borderVisible
|
|
25198
|
+
ctx.strokeStyle = cs.borderVisible ? (up ? cs.borderUpColor : cs.borderDownColor) ?? color : color;
|
|
24660
25199
|
ctx.lineWidth = 1;
|
|
24661
25200
|
const bw = Math.max(0, g.bodyW - 1);
|
|
24662
25201
|
const bh = Math.max(0, bodyH - 1);
|
|
@@ -25079,6 +25618,19 @@ function autoFontSize(lines, boxW, boxH, bold) {
|
|
|
25079
25618
|
|
|
25080
25619
|
// src/renderers/shared/DrawingSceneRenderer.ts
|
|
25081
25620
|
var EMPTY_DRAWING_SET = { lines: [], boxes: [], labels: [], polylines: [], linefills: [] };
|
|
25621
|
+
function modelDrawingSet(m, overlay) {
|
|
25622
|
+
const want = (d) => Boolean(d.overlay) === overlay;
|
|
25623
|
+
return {
|
|
25624
|
+
lines: (m.lines ?? []).filter(want),
|
|
25625
|
+
boxes: (m.boxes ?? []).filter(want),
|
|
25626
|
+
labels: (m.labels ?? []).filter(want),
|
|
25627
|
+
polylines: (m.polylines ?? []).filter(want),
|
|
25628
|
+
linefills: (m.linefills ?? []).filter(want)
|
|
25629
|
+
};
|
|
25630
|
+
}
|
|
25631
|
+
function drawingSetEmpty(s) {
|
|
25632
|
+
return !s.lines.length && !s.boxes.length && !s.labels.length && !s.polylines.length && !s.linefills.length;
|
|
25633
|
+
}
|
|
25082
25634
|
function fontSizePx(size) {
|
|
25083
25635
|
return size === "auto" ? 12 : namedFontSize(size);
|
|
25084
25636
|
}
|
|
@@ -25088,6 +25640,8 @@ var DrawingSceneRenderer = class {
|
|
|
25088
25640
|
this.set = set;
|
|
25089
25641
|
/** measureText width cache, keyed by `${font} ${text}`, persists across frames. */
|
|
25090
25642
|
this.widthCache = /* @__PURE__ */ new Map();
|
|
25643
|
+
/** Tooltip hit-rects captured while drawing the CURRENT set (rebuilt per render). */
|
|
25644
|
+
this.tipRegions = [];
|
|
25091
25645
|
/**
|
|
25092
25646
|
* Index offset of the CURRENT set's model: its `xloc:'bar_index'` coordinates count
|
|
25093
25647
|
* from the model's anchor bar, so they shift by this to land on chart logical indices.
|
|
@@ -25108,8 +25662,13 @@ var DrawingSceneRenderer = class {
|
|
|
25108
25662
|
const s = this.set;
|
|
25109
25663
|
return !s.lines.length && !s.boxes.length && !s.labels.length && !s.polylines.length && !s.linefills.length;
|
|
25110
25664
|
}
|
|
25665
|
+
/** Tooltip hit-rects of the labels drawn by the LAST `render` call (same coords as `ctx`). */
|
|
25666
|
+
labelTipRegions() {
|
|
25667
|
+
return this.tipRegions;
|
|
25668
|
+
}
|
|
25111
25669
|
/** Draw the whole set into `ctx` using the supplied coordinate closures. */
|
|
25112
25670
|
render(ctx, W, H, xOf, yOf) {
|
|
25671
|
+
this.tipRegions = [];
|
|
25113
25672
|
if (this.isEmpty()) return;
|
|
25114
25673
|
ctx.save();
|
|
25115
25674
|
this.drawLinefills(ctx, W, H, xOf, yOf);
|
|
@@ -25457,13 +26016,35 @@ var DrawingSceneRenderer = class {
|
|
|
25457
26016
|
if (this.isPointShape(lb.style)) {
|
|
25458
26017
|
if (!lb.noFill) this.drawLabelShape(ctx, lb.style, px, py, fontPx, color);
|
|
25459
26018
|
if (lb.text) this.drawLabelText(ctx, lb, px, py + fontPx, fontPx);
|
|
25460
|
-
|
|
25461
|
-
|
|
26019
|
+
if (lb.tooltip) {
|
|
26020
|
+
const r = Math.max(4, fontPx * 0.6) + 3;
|
|
26021
|
+
this.tipRegions.push({ left: px - r, top: py - r, right: px + r, bottom: py + r, text: lb.tooltip });
|
|
26022
|
+
}
|
|
26023
|
+
} else if (lb.style === "none" || lb.style === "text_outline") {
|
|
26024
|
+
if (lb.text) {
|
|
26025
|
+
this.drawLabelText(ctx, lb, px, py, fontPx, lb.style === "text_outline");
|
|
26026
|
+
if (lb.tooltip) this.tipRegions.push(this.textRegion(ctx, lb, px, py, fontPx, lb.tooltip));
|
|
26027
|
+
}
|
|
25462
26028
|
} else {
|
|
25463
|
-
this.drawBubble(ctx, lb, px, py, fontPx, color);
|
|
26029
|
+
const r = this.drawBubble(ctx, lb, px, py, fontPx, color);
|
|
26030
|
+
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 });
|
|
25464
26031
|
}
|
|
25465
26032
|
}
|
|
25466
26033
|
}
|
|
26034
|
+
/** Canvas font for a label's text — family + Pine `text_formatting` (bold/italic). */
|
|
26035
|
+
labelFont(lb, fontPx) {
|
|
26036
|
+
const family = lb.fontFamily === "monospace" ? "monospace" : this.deps.theme.fontFamily || "sans-serif";
|
|
26037
|
+
return `${lb.italic ? "italic " : ""}${lb.bold ? "bold " : ""}${fontPx}px ${family}`;
|
|
26038
|
+
}
|
|
26039
|
+
/** Hover rect of a text-only label (style none/text_outline/noFill), centered like drawLabelText. */
|
|
26040
|
+
textRegion(ctx, lb, cx, cy, fontPx, text) {
|
|
26041
|
+
const font = this.labelFont(lb, fontPx);
|
|
26042
|
+
const lines = (lb.text ?? "").split("\n");
|
|
26043
|
+
const w = Math.max(1, ...lines.map((l) => this.measure(ctx, font, l)));
|
|
26044
|
+
const h = fontPx * 1.25 * lines.length;
|
|
26045
|
+
const left = lb.textAlign === "left" ? cx : lb.textAlign === "right" ? cx - w : cx - w / 2;
|
|
26046
|
+
return { left, top: cy - h / 2, right: left + w, bottom: cy + h / 2, text };
|
|
26047
|
+
}
|
|
25467
26048
|
isPointShape(style) {
|
|
25468
26049
|
switch (style) {
|
|
25469
26050
|
case "circle":
|
|
@@ -25482,8 +26063,7 @@ var DrawingSceneRenderer = class {
|
|
|
25482
26063
|
}
|
|
25483
26064
|
}
|
|
25484
26065
|
drawLabelText(ctx, lb, cx, cy, fontPx, outline = false) {
|
|
25485
|
-
|
|
25486
|
-
ctx.font = `${fontPx}px ${family}`;
|
|
26066
|
+
ctx.font = this.labelFont(lb, fontPx);
|
|
25487
26067
|
ctx.textAlign = lb.textAlign === "left" ? "left" : lb.textAlign === "right" ? "right" : "center";
|
|
25488
26068
|
ctx.textBaseline = "middle";
|
|
25489
26069
|
const lines = lb.text.split("\n");
|
|
@@ -25563,9 +26143,9 @@ var DrawingSceneRenderer = class {
|
|
|
25563
26143
|
ctx.fill();
|
|
25564
26144
|
}
|
|
25565
26145
|
}
|
|
26146
|
+
/** Draw a bubble label; returns the bubble body rect (for tooltip hit-testing). */
|
|
25566
26147
|
drawBubble(ctx, lb, px, py, fontPx, color) {
|
|
25567
|
-
|
|
25568
|
-
ctx.font = `${fontPx}px ${family}`;
|
|
26148
|
+
ctx.font = this.labelFont(lb, fontPx);
|
|
25569
26149
|
const lines = (lb.text ?? "").split("\n");
|
|
25570
26150
|
const padX = 6;
|
|
25571
26151
|
const padY = 4;
|
|
@@ -25623,38 +26203,51 @@ var DrawingSceneRenderer = class {
|
|
|
25623
26203
|
by = py - h - ptr;
|
|
25624
26204
|
pointer = "down";
|
|
25625
26205
|
}
|
|
25626
|
-
|
|
25627
|
-
|
|
25628
|
-
|
|
25629
|
-
if (pointer !== "none") {
|
|
25630
|
-
ctx.beginPath();
|
|
25631
|
-
if (pointer === "down") {
|
|
25632
|
-
ctx.moveTo(px - ptr, by + h);
|
|
25633
|
-
ctx.lineTo(px + ptr, by + h);
|
|
25634
|
-
ctx.lineTo(px, py);
|
|
25635
|
-
} else if (pointer === "up") {
|
|
25636
|
-
ctx.moveTo(px - ptr, by);
|
|
25637
|
-
ctx.lineTo(px + ptr, by);
|
|
25638
|
-
ctx.lineTo(px, py);
|
|
25639
|
-
} else if (pointer === "left") {
|
|
25640
|
-
ctx.moveTo(bx, py - ptr);
|
|
25641
|
-
ctx.lineTo(bx, py + ptr);
|
|
25642
|
-
ctx.lineTo(px, py);
|
|
25643
|
-
} else {
|
|
25644
|
-
ctx.moveTo(bx + w, py - ptr);
|
|
25645
|
-
ctx.lineTo(bx + w, py + ptr);
|
|
25646
|
-
ctx.lineTo(px, py);
|
|
25647
|
-
}
|
|
25648
|
-
ctx.closePath();
|
|
26206
|
+
if (!lb.noFill) {
|
|
26207
|
+
ctx.fillStyle = color;
|
|
26208
|
+
this.roundRect(ctx, bx, by, w, h, 4);
|
|
25649
26209
|
ctx.fill();
|
|
26210
|
+
if (pointer !== "none") {
|
|
26211
|
+
ctx.beginPath();
|
|
26212
|
+
if (pointer === "down") {
|
|
26213
|
+
ctx.moveTo(px - ptr, by + h);
|
|
26214
|
+
ctx.lineTo(px + ptr, by + h);
|
|
26215
|
+
ctx.lineTo(px, py);
|
|
26216
|
+
} else if (pointer === "up") {
|
|
26217
|
+
ctx.moveTo(px - ptr, by);
|
|
26218
|
+
ctx.lineTo(px + ptr, by);
|
|
26219
|
+
ctx.lineTo(px, py);
|
|
26220
|
+
} else if (pointer === "left") {
|
|
26221
|
+
ctx.moveTo(bx, py - ptr);
|
|
26222
|
+
ctx.lineTo(bx, py + ptr);
|
|
26223
|
+
ctx.lineTo(px, py);
|
|
26224
|
+
} else {
|
|
26225
|
+
ctx.moveTo(bx + w, py - ptr);
|
|
26226
|
+
ctx.lineTo(bx + w, py + ptr);
|
|
26227
|
+
ctx.lineTo(px, py);
|
|
26228
|
+
}
|
|
26229
|
+
ctx.closePath();
|
|
26230
|
+
ctx.fill();
|
|
26231
|
+
}
|
|
25650
26232
|
}
|
|
25651
26233
|
if (lb.text) {
|
|
25652
|
-
ctx.fillStyle = lb.textColor ?? contrastColor(color);
|
|
25653
|
-
ctx.textAlign = "center";
|
|
26234
|
+
ctx.fillStyle = lb.textColor ?? (lb.noFill ? this.deps.theme.textColor : contrastColor(color));
|
|
25654
26235
|
ctx.textBaseline = "middle";
|
|
26236
|
+
let tx;
|
|
26237
|
+
if (lb.textAlign === "left") {
|
|
26238
|
+
ctx.textAlign = "left";
|
|
26239
|
+
tx = bx + padX;
|
|
26240
|
+
} else if (lb.textAlign === "right") {
|
|
26241
|
+
ctx.textAlign = "right";
|
|
26242
|
+
tx = bx + w - padX;
|
|
26243
|
+
} else {
|
|
26244
|
+
ctx.textAlign = "center";
|
|
26245
|
+
tx = bx + w / 2;
|
|
26246
|
+
}
|
|
25655
26247
|
const startY = by + padY + lineH / 2;
|
|
25656
|
-
for (let i = 0; i < lines.length; i += 1) ctx.fillText(lines[i],
|
|
26248
|
+
for (let i = 0; i < lines.length; i += 1) ctx.fillText(lines[i], tx, startY + i * lineH);
|
|
25657
26249
|
}
|
|
26250
|
+
return { x: bx, y: by, w, h };
|
|
25658
26251
|
}
|
|
25659
26252
|
roundRect(ctx, x, y, w, h, r) {
|
|
25660
26253
|
const rr = Math.min(r, w / 2, h / 2);
|
|
@@ -25890,7 +26483,7 @@ var ChromeRenderer = class {
|
|
|
25890
26483
|
this.ctx = null;
|
|
25891
26484
|
// The color for axis tick labels — the host-passed surface text, set each frame in render().
|
|
25892
26485
|
this.axisTextColor = DARK_THEME.textColor;
|
|
25893
|
-
// Shared Pine-drawing renderer
|
|
26486
|
+
// Shared Pine-drawing renderer, used here for autoscale geometry only; widthCache persists.
|
|
25894
26487
|
this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
|
|
25895
26488
|
}
|
|
25896
26489
|
mount(canvas) {
|
|
@@ -25915,8 +26508,8 @@ var ChromeRenderer = class {
|
|
|
25915
26508
|
*/
|
|
25916
26509
|
paneDrawingsRange(ownModels, scene, isPricePane, vr) {
|
|
25917
26510
|
let dr = null;
|
|
25918
|
-
for (const m of ownModels) dr = unionRange(dr, this.drawingsRange(
|
|
25919
|
-
if (isPricePane) for (const m of scene.indicators.values()) dr = unionRange(dr, this.drawingsRange(
|
|
26511
|
+
for (const m of ownModels) dr = unionRange(dr, this.drawingsRange(modelDrawingSet(m, false), vr, scene.offsetOf(m.id)));
|
|
26512
|
+
if (isPricePane) for (const m of scene.indicators.values()) dr = unionRange(dr, this.drawingsRange(modelDrawingSet(m, true), vr, scene.offsetOf(m.id)));
|
|
25920
26513
|
return dr;
|
|
25921
26514
|
}
|
|
25922
26515
|
/** Clear the chrome canvas and draw drawings + axes + current-price line.
|
|
@@ -25948,17 +26541,6 @@ var ChromeRenderer = class {
|
|
|
25948
26541
|
return;
|
|
25949
26542
|
}
|
|
25950
26543
|
const pricePane = panes.find((p) => p.kind === "price") ?? null;
|
|
25951
|
-
for (const pane of panes) {
|
|
25952
|
-
if (pane.collapsed) continue;
|
|
25953
|
-
for (const m of scene.indicatorsForPane(pane.id)) {
|
|
25954
|
-
const sc = scene.scaleFor(m, pane);
|
|
25955
|
-
const mp = sc === pane.scale ? pane : { ...pane, scale: sc };
|
|
25956
|
-
this.renderDrawings(ctx, coords, this.ownDrawings(m), mp, dataW, scene.offsetOf(m.id));
|
|
25957
|
-
}
|
|
25958
|
-
}
|
|
25959
|
-
if (pricePane) {
|
|
25960
|
-
for (const m of scene.indicators.values()) this.renderDrawings(ctx, coords, this.overlayDrawings(m), pricePane, dataW, scene.offsetOf(m.id));
|
|
25961
|
-
}
|
|
25962
26544
|
if (pricePane && !pricePane.collapsed && scene.tradeMarkers.visible) {
|
|
25963
26545
|
for (const m of scene.indicators.values()) {
|
|
25964
26546
|
if (m.trades?.length) this.renderTrades(ctx, coords, scene, theme, m.trades, pricePane, dataW);
|
|
@@ -25974,25 +26556,6 @@ var ChromeRenderer = class {
|
|
|
25974
26556
|
this.canvas = null;
|
|
25975
26557
|
this.ctx = null;
|
|
25976
26558
|
}
|
|
25977
|
-
// ── Pine-drawing helpers (own vs force_overlay routing) ──
|
|
25978
|
-
ownDrawings(m) {
|
|
25979
|
-
return {
|
|
25980
|
-
lines: (m.lines ?? []).filter((d) => !d.overlay),
|
|
25981
|
-
boxes: (m.boxes ?? []).filter((d) => !d.overlay),
|
|
25982
|
-
labels: (m.labels ?? []).filter((d) => !d.overlay),
|
|
25983
|
-
polylines: (m.polylines ?? []).filter((d) => !d.overlay),
|
|
25984
|
-
linefills: (m.linefills ?? []).filter((d) => !d.overlay)
|
|
25985
|
-
};
|
|
25986
|
-
}
|
|
25987
|
-
overlayDrawings(m) {
|
|
25988
|
-
return {
|
|
25989
|
-
lines: (m.lines ?? []).filter((d) => d.overlay),
|
|
25990
|
-
boxes: (m.boxes ?? []).filter((d) => d.overlay),
|
|
25991
|
-
labels: (m.labels ?? []).filter((d) => d.overlay),
|
|
25992
|
-
polylines: (m.polylines ?? []).filter((d) => d.overlay),
|
|
25993
|
-
linefills: (m.linefills ?? []).filter((d) => d.overlay)
|
|
25994
|
-
};
|
|
25995
|
-
}
|
|
25996
26559
|
drawingsRange(set, vr, indexOffset = 0) {
|
|
25997
26560
|
this.drawScene.setSet(set, indexOffset);
|
|
25998
26561
|
if (this.drawScene.isEmpty()) return null;
|
|
@@ -26026,23 +26589,6 @@ var ChromeRenderer = class {
|
|
|
26026
26589
|
);
|
|
26027
26590
|
ctx.restore();
|
|
26028
26591
|
}
|
|
26029
|
-
renderDrawings(ctx, coords, set, pane, dataW, indexOffset = 0) {
|
|
26030
|
-
this.drawScene.setSet(set, indexOffset);
|
|
26031
|
-
if (this.drawScene.isEmpty()) return;
|
|
26032
|
-
ctx.save();
|
|
26033
|
-
ctx.translate(0, pane.bounds.top);
|
|
26034
|
-
ctx.beginPath();
|
|
26035
|
-
ctx.rect(0, 0, dataW, pane.bounds.height);
|
|
26036
|
-
ctx.clip();
|
|
26037
|
-
this.drawScene.render(
|
|
26038
|
-
ctx,
|
|
26039
|
-
dataW,
|
|
26040
|
-
pane.bounds.height,
|
|
26041
|
-
(l) => coords.logicalToX(l),
|
|
26042
|
-
(price) => coords.priceToY(price, pane.scale, pane.bounds) - pane.bounds.top
|
|
26043
|
-
);
|
|
26044
|
-
ctx.restore();
|
|
26045
|
-
}
|
|
26046
26592
|
// ── axes ──
|
|
26047
26593
|
drawPriceAxes(ctx, scene, coords, theme, dataW, panes) {
|
|
26048
26594
|
ctx.strokeStyle = scene.style.borderColor ?? theme.borderColor;
|
|
@@ -26258,6 +26804,68 @@ function formatCountdown(ms) {
|
|
|
26258
26804
|
return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`;
|
|
26259
26805
|
}
|
|
26260
26806
|
|
|
26807
|
+
// src/renderers/native/chrome/LabelTooltip.ts
|
|
26808
|
+
var HOVER_DELAY_MS = 350;
|
|
26809
|
+
var LabelTooltip = class {
|
|
26810
|
+
constructor(plot, deps) {
|
|
26811
|
+
this.plot = plot;
|
|
26812
|
+
this.deps = deps;
|
|
26813
|
+
this.tip = null;
|
|
26814
|
+
this.timer = null;
|
|
26815
|
+
/** Text of the currently open OR armed tip — dedupes moves inside one label. */
|
|
26816
|
+
this.current = null;
|
|
26817
|
+
this.onMove = (e) => {
|
|
26818
|
+
if (e.pointerType !== "mouse") return;
|
|
26819
|
+
const rect = this.plot.getBoundingClientRect();
|
|
26820
|
+
const x = e.clientX - rect.left;
|
|
26821
|
+
const y = e.clientY - rect.top;
|
|
26822
|
+
const text = this.deps.lookup(x, y);
|
|
26823
|
+
if (!text) {
|
|
26824
|
+
this.clear();
|
|
26825
|
+
return;
|
|
26826
|
+
}
|
|
26827
|
+
if (text === this.current) return;
|
|
26828
|
+
this.clear();
|
|
26829
|
+
this.current = text;
|
|
26830
|
+
this.timer = window.setTimeout(() => this.show(text, x, y), HOVER_DELAY_MS);
|
|
26831
|
+
};
|
|
26832
|
+
this.onLeave = () => {
|
|
26833
|
+
this.clear();
|
|
26834
|
+
};
|
|
26835
|
+
plot.addEventListener("pointermove", this.onMove);
|
|
26836
|
+
plot.addEventListener("pointerleave", this.onLeave);
|
|
26837
|
+
plot.addEventListener("pointerdown", this.onLeave);
|
|
26838
|
+
}
|
|
26839
|
+
destroy() {
|
|
26840
|
+
this.plot.removeEventListener("pointermove", this.onMove);
|
|
26841
|
+
this.plot.removeEventListener("pointerleave", this.onLeave);
|
|
26842
|
+
this.plot.removeEventListener("pointerdown", this.onLeave);
|
|
26843
|
+
this.clear();
|
|
26844
|
+
}
|
|
26845
|
+
show(text, x, y) {
|
|
26846
|
+
const doc = this.plot.ownerDocument;
|
|
26847
|
+
const tip = doc.createElement("div");
|
|
26848
|
+
tip.textContent = text;
|
|
26849
|
+
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;";
|
|
26850
|
+
applyChromeTokens(tip, this.deps.theme());
|
|
26851
|
+
this.plot.appendChild(tip);
|
|
26852
|
+
const pw = this.plot.clientWidth;
|
|
26853
|
+
const ph = this.plot.clientHeight;
|
|
26854
|
+
tip.style.left = `${Math.max(0, Math.min(x + 12, pw - tip.offsetWidth - 4))}px`;
|
|
26855
|
+
tip.style.top = `${Math.max(0, Math.min(y + 16, ph - tip.offsetHeight - 4))}px`;
|
|
26856
|
+
this.tip = tip;
|
|
26857
|
+
}
|
|
26858
|
+
clear() {
|
|
26859
|
+
if (this.timer !== null) {
|
|
26860
|
+
clearTimeout(this.timer);
|
|
26861
|
+
this.timer = null;
|
|
26862
|
+
}
|
|
26863
|
+
this.tip?.remove();
|
|
26864
|
+
this.tip = null;
|
|
26865
|
+
this.current = null;
|
|
26866
|
+
}
|
|
26867
|
+
};
|
|
26868
|
+
|
|
26261
26869
|
// src/renderers/native/chrome/CrosshairRenderer.ts
|
|
26262
26870
|
var CrosshairRenderer = class {
|
|
26263
26871
|
constructor() {
|
|
@@ -30299,17 +30907,17 @@ function glyphIcon(glyph) {
|
|
|
30299
30907
|
return textGlyph(String(glyph), 15);
|
|
30300
30908
|
}
|
|
30301
30909
|
function stampSizeIcon(size) {
|
|
30302
|
-
return textGlyph("\u25CF", (
|
|
30910
|
+
return textGlyph("\u25CF", (SIZE_PX3[String(size)] ?? 13) + 4);
|
|
30303
30911
|
}
|
|
30304
30912
|
function sizeIcon(size) {
|
|
30305
30913
|
return textGlyph(String(size).charAt(0).toUpperCase(), 15);
|
|
30306
30914
|
}
|
|
30307
|
-
var
|
|
30915
|
+
var SIZE_PX3 = { small: 10, normal: 13, large: 16, huge: 20 };
|
|
30308
30916
|
function numbersSizeIcon(size) {
|
|
30309
|
-
return textGlyph("12", (
|
|
30917
|
+
return textGlyph("12", (SIZE_PX3[String(size)] ?? 13) - 1, 16.5, 'font-weight="600"');
|
|
30310
30918
|
}
|
|
30311
30919
|
function labelSizeIcon(size) {
|
|
30312
|
-
return textGlyph("T", (
|
|
30920
|
+
return textGlyph("T", (SIZE_PX3[String(size)] ?? 13) + 2);
|
|
30313
30921
|
}
|
|
30314
30922
|
function capitalize(s) {
|
|
30315
30923
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
@@ -31284,6 +31892,315 @@ var UserDrawingController = class {
|
|
|
31284
31892
|
}
|
|
31285
31893
|
};
|
|
31286
31894
|
|
|
31895
|
+
// src/renderers/shared/TableOverlay.ts
|
|
31896
|
+
var SIZE_PX4 = {
|
|
31897
|
+
auto: 13,
|
|
31898
|
+
tiny: 10,
|
|
31899
|
+
small: 11,
|
|
31900
|
+
normal: 13,
|
|
31901
|
+
large: 16,
|
|
31902
|
+
huge: 20
|
|
31903
|
+
};
|
|
31904
|
+
function fontPxOf(size) {
|
|
31905
|
+
if (typeof size === "number") return size > 0 ? size : SIZE_PX4.auto;
|
|
31906
|
+
return SIZE_PX4[size] ?? SIZE_PX4.auto;
|
|
31907
|
+
}
|
|
31908
|
+
function tableHasContent(t) {
|
|
31909
|
+
return t.cells.some((row) => row?.some((c) => c != null && !c.merged));
|
|
31910
|
+
}
|
|
31911
|
+
function mergeRenderPlan(t) {
|
|
31912
|
+
const span = /* @__PURE__ */ new Map();
|
|
31913
|
+
const omit = /* @__PURE__ */ new Set();
|
|
31914
|
+
for (const m of t.merges) {
|
|
31915
|
+
span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
|
|
31916
|
+
for (let r = m.startRow; r <= m.endRow; r += 1) {
|
|
31917
|
+
for (let c = m.startCol; c <= m.endCol; c += 1) {
|
|
31918
|
+
if (r !== m.startRow || c !== m.startCol) omit.add(`${r}:${c}`);
|
|
31919
|
+
}
|
|
31920
|
+
}
|
|
31921
|
+
}
|
|
31922
|
+
for (let r = 0; r < t.rows; r += 1) {
|
|
31923
|
+
for (let c = 0; c < t.columns; c += 1) {
|
|
31924
|
+
if (t.cells[r]?.[c]?.merged && !span.has(`${r}:${c}`)) omit.add(`${r}:${c}`);
|
|
31925
|
+
}
|
|
31926
|
+
}
|
|
31927
|
+
for (const key of span.keys()) omit.delete(key);
|
|
31928
|
+
return { span, omit };
|
|
31929
|
+
}
|
|
31930
|
+
|
|
31931
|
+
// src/renderers/shared/TableCanvasRenderer.ts
|
|
31932
|
+
var PAD_X = 6;
|
|
31933
|
+
var PAD_Y = 2;
|
|
31934
|
+
var MARGIN = 6;
|
|
31935
|
+
var LINE_HEIGHT = 1.2;
|
|
31936
|
+
function paintTable(ctx, t, args, tips) {
|
|
31937
|
+
if (!tableHasContent(t)) return;
|
|
31938
|
+
const layout = layoutTable(ctx, t, args);
|
|
31939
|
+
if (!layout || layout.w <= 0 || layout.h <= 0) return;
|
|
31940
|
+
const fw = t.frameColor && t.frameWidth > 0 ? t.frameWidth : 0;
|
|
31941
|
+
const { x, y } = anchorOrigin(t.position, layout.w + 2 * fw, layout.h + 2 * fw, args);
|
|
31942
|
+
const x0 = x + fw;
|
|
31943
|
+
const y0 = y + fw;
|
|
31944
|
+
if (t.bgColor) {
|
|
31945
|
+
ctx.fillStyle = t.bgColor;
|
|
31946
|
+
ctx.fillRect(x0, y0, layout.w, layout.h);
|
|
31947
|
+
}
|
|
31948
|
+
if (fw > 0 && t.frameColor) {
|
|
31949
|
+
ctx.strokeStyle = t.frameColor;
|
|
31950
|
+
ctx.lineWidth = fw;
|
|
31951
|
+
ctx.strokeRect(x + fw / 2, y + fw / 2, layout.w + fw, layout.h + fw);
|
|
31952
|
+
}
|
|
31953
|
+
const colX = [0];
|
|
31954
|
+
for (const w of layout.colW) colX.push(colX[colX.length - 1] + w);
|
|
31955
|
+
const rowY = [0];
|
|
31956
|
+
for (const h of layout.rowH) rowY.push(rowY[rowY.length - 1] + h);
|
|
31957
|
+
const prevBaseline = ctx.textBaseline;
|
|
31958
|
+
const prevAlign = ctx.textAlign;
|
|
31959
|
+
ctx.textBaseline = "middle";
|
|
31960
|
+
for (const box of layout.boxes) {
|
|
31961
|
+
const rx = x0 + colX[box.c];
|
|
31962
|
+
const ry = y0 + rowY[box.r];
|
|
31963
|
+
const rw = colX[box.c + box.cs] - colX[box.c];
|
|
31964
|
+
const rh = rowY[box.r + box.rs] - rowY[box.r];
|
|
31965
|
+
const cell = box.cell;
|
|
31966
|
+
if (cell.bgColor) {
|
|
31967
|
+
ctx.fillStyle = cell.bgColor;
|
|
31968
|
+
ctx.fillRect(rx, ry, rw, rh);
|
|
31969
|
+
}
|
|
31970
|
+
const text = cell.text ?? "";
|
|
31971
|
+
if (text.length > 0) {
|
|
31972
|
+
const px = fontPxOf(cell.textSize);
|
|
31973
|
+
ctx.font = cellFont(cell, px, args.theme);
|
|
31974
|
+
ctx.fillStyle = cell.textColor ?? args.theme.textColor;
|
|
31975
|
+
const lines = text.split("\n");
|
|
31976
|
+
const blockH = lines.length * px * LINE_HEIGHT;
|
|
31977
|
+
const blockTop = cell.vAlign === "top" ? ry + PAD_Y : cell.vAlign === "bottom" ? ry + rh - PAD_Y - blockH : ry + (rh - blockH) / 2;
|
|
31978
|
+
const tx = cell.hAlign === "left" ? rx + PAD_X : cell.hAlign === "right" ? rx + rw - PAD_X : rx + rw / 2;
|
|
31979
|
+
ctx.textAlign = cell.hAlign;
|
|
31980
|
+
lines.forEach((line, i) => ctx.fillText(line, tx, blockTop + (i + 0.5) * px * LINE_HEIGHT));
|
|
31981
|
+
}
|
|
31982
|
+
if (cell.tooltip) tips.push({ left: rx, top: ry, right: rx + rw, bottom: ry + rh, text: cell.tooltip });
|
|
31983
|
+
}
|
|
31984
|
+
ctx.textBaseline = prevBaseline;
|
|
31985
|
+
ctx.textAlign = prevAlign;
|
|
31986
|
+
if (t.borderColor && t.borderWidth > 0) {
|
|
31987
|
+
ctx.strokeStyle = t.borderColor;
|
|
31988
|
+
ctx.lineWidth = t.borderWidth;
|
|
31989
|
+
const seen = /* @__PURE__ */ new Set();
|
|
31990
|
+
ctx.beginPath();
|
|
31991
|
+
const edge = (ax, ay, bx, by) => {
|
|
31992
|
+
const key = `${ax},${ay},${bx},${by}`;
|
|
31993
|
+
if (seen.has(key)) return;
|
|
31994
|
+
seen.add(key);
|
|
31995
|
+
ctx.moveTo(ax, ay);
|
|
31996
|
+
ctx.lineTo(bx, by);
|
|
31997
|
+
};
|
|
31998
|
+
for (const box of layout.boxes) {
|
|
31999
|
+
const l = Math.round(x0 + colX[box.c]);
|
|
32000
|
+
const r = Math.round(x0 + colX[box.c + box.cs]);
|
|
32001
|
+
const tp = Math.round(y0 + rowY[box.r]);
|
|
32002
|
+
const bt = Math.round(y0 + rowY[box.r + box.rs]);
|
|
32003
|
+
edge(l, tp, r, tp);
|
|
32004
|
+
edge(l, bt, r, bt);
|
|
32005
|
+
edge(l, tp, l, bt);
|
|
32006
|
+
edge(r, tp, r, bt);
|
|
32007
|
+
}
|
|
32008
|
+
ctx.stroke();
|
|
32009
|
+
}
|
|
32010
|
+
}
|
|
32011
|
+
function layoutTable(ctx, t, args) {
|
|
32012
|
+
const { span, omit } = mergeRenderPlan(t);
|
|
32013
|
+
const colW = new Array(t.columns).fill(0);
|
|
32014
|
+
const rowH = new Array(t.rows).fill(0);
|
|
32015
|
+
const boxes = [];
|
|
32016
|
+
for (let r = 0; r < t.rows; r += 1) {
|
|
32017
|
+
for (let c = 0; c < t.columns; c += 1) {
|
|
32018
|
+
if (omit.has(`${r}:${c}`)) continue;
|
|
32019
|
+
const cell = t.cells[r]?.[c];
|
|
32020
|
+
if (cell == null) continue;
|
|
32021
|
+
const sp = span.get(`${r}:${c}`);
|
|
32022
|
+
boxes.push({ cell, r, c, cs: Math.min(sp?.cs ?? 1, t.columns - c), rs: Math.min(sp?.rs ?? 1, t.rows - r) });
|
|
32023
|
+
}
|
|
32024
|
+
}
|
|
32025
|
+
if (boxes.length === 0) return null;
|
|
32026
|
+
const sizeOf = (cell) => {
|
|
32027
|
+
const px = fontPxOf(cell.textSize);
|
|
32028
|
+
ctx.font = cellFont(cell, px, args.theme);
|
|
32029
|
+
const lines = (cell.text ?? "").split("\n");
|
|
32030
|
+
let maxW = 0;
|
|
32031
|
+
for (const line of lines) maxW = Math.max(maxW, ctx.measureText(line).width);
|
|
32032
|
+
let w2 = Math.ceil(maxW) + 2 * PAD_X;
|
|
32033
|
+
let h2 = Math.ceil(lines.length * px * LINE_HEIGHT) + 2 * PAD_Y;
|
|
32034
|
+
if (cell.width) w2 = Math.max(w2, cell.width / 100 * args.plotWidth);
|
|
32035
|
+
if (cell.height) h2 = Math.max(h2, cell.height / 100 * args.paneHeight);
|
|
32036
|
+
return { w: w2, h: h2 };
|
|
32037
|
+
};
|
|
32038
|
+
const spanning = [];
|
|
32039
|
+
for (const box of boxes) {
|
|
32040
|
+
const { w: w2, h: h2 } = sizeOf(box.cell);
|
|
32041
|
+
if (box.cs === 1) colW[box.c] = Math.max(colW[box.c], w2);
|
|
32042
|
+
if (box.rs === 1) rowH[box.r] = Math.max(rowH[box.r], h2);
|
|
32043
|
+
if (box.cs > 1 || box.rs > 1) spanning.push({ box, w: w2, h: h2 });
|
|
32044
|
+
}
|
|
32045
|
+
for (const { box, w: w2, h: h2 } of spanning) {
|
|
32046
|
+
if (box.cs > 1) {
|
|
32047
|
+
let sum = 0;
|
|
32048
|
+
for (let c = box.c; c < box.c + box.cs; c += 1) sum += colW[c];
|
|
32049
|
+
if (w2 > sum) for (let c = box.c; c < box.c + box.cs; c += 1) colW[c] += (w2 - sum) / box.cs;
|
|
32050
|
+
}
|
|
32051
|
+
if (box.rs > 1) {
|
|
32052
|
+
let sum = 0;
|
|
32053
|
+
for (let r = box.r; r < box.r + box.rs; r += 1) sum += rowH[r];
|
|
32054
|
+
if (h2 > sum) for (let r = box.r; r < box.r + box.rs; r += 1) rowH[r] += (h2 - sum) / box.rs;
|
|
32055
|
+
}
|
|
32056
|
+
}
|
|
32057
|
+
let w = 0;
|
|
32058
|
+
for (const cw of colW) w += cw;
|
|
32059
|
+
let h = 0;
|
|
32060
|
+
for (const rh of rowH) h += rh;
|
|
32061
|
+
return { colW, rowH, w, h, boxes };
|
|
32062
|
+
}
|
|
32063
|
+
function anchorOrigin(position, totalW, totalH, args) {
|
|
32064
|
+
let y;
|
|
32065
|
+
if (position.startsWith("top")) y = MARGIN;
|
|
32066
|
+
else if (position.startsWith("bottom")) y = args.paneHeight - MARGIN - totalH;
|
|
32067
|
+
else y = args.paneHeight / 2 - totalH / 2;
|
|
32068
|
+
let x;
|
|
32069
|
+
if (position.endsWith("left")) x = MARGIN;
|
|
32070
|
+
else if (position.endsWith("right")) x = args.plotWidth - MARGIN - totalW;
|
|
32071
|
+
else x = args.plotWidth / 2 - totalW / 2;
|
|
32072
|
+
return { x, y };
|
|
32073
|
+
}
|
|
32074
|
+
function cellFont(cell, px, theme) {
|
|
32075
|
+
const family = cell.fontFamily === "monospace" ? "monospace" : theme.fontFamily || "sans-serif";
|
|
32076
|
+
return `${cell.italic ? "italic " : ""}${cell.bold ? "bold " : ""}${px}px ${family}`;
|
|
32077
|
+
}
|
|
32078
|
+
|
|
32079
|
+
// src/renderers/native/drawings/IndicatorDrawingSlices.ts
|
|
32080
|
+
function indicatorSliceKey(z, boundaries) {
|
|
32081
|
+
return boundaries.find((b) => b > z) ?? Infinity;
|
|
32082
|
+
}
|
|
32083
|
+
var IndicatorDrawingSlices = class {
|
|
32084
|
+
constructor() {
|
|
32085
|
+
this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
|
|
32086
|
+
/** Slice canvas cache, keyed `paneId|beforeZ` — same lifecycle as the user-drawing cache. */
|
|
32087
|
+
this.sliceCache = /* @__PURE__ */ new Map();
|
|
32088
|
+
/** Tooltip hit-rects of every label drawn this frame, in plot coords (rebuilt per prepare). */
|
|
32089
|
+
this.tips = [];
|
|
32090
|
+
}
|
|
32091
|
+
/**
|
|
32092
|
+
* Rebuild the per-indicator drawing slices for this data frame. `ref` is the data
|
|
32093
|
+
* canvas the slices must match pixel-for-pixel (the backend composites them 1:1).
|
|
32094
|
+
* Runs from the renderer's data paint, just before the backend composites the scene.
|
|
32095
|
+
*/
|
|
32096
|
+
prepare(scene, coords, theme, ref) {
|
|
32097
|
+
this.tips = [];
|
|
32098
|
+
const out = /* @__PURE__ */ new Map();
|
|
32099
|
+
if (ref.width === 0 || ref.height === 0) {
|
|
32100
|
+
this.sliceCache.clear();
|
|
32101
|
+
return out;
|
|
32102
|
+
}
|
|
32103
|
+
this.drawScene.setDeps({
|
|
32104
|
+
timeToLogical: (ms) => coords.timeToLogical(ms),
|
|
32105
|
+
barAt: (logical) => {
|
|
32106
|
+
const b = scene.bars[Math.round(logical)];
|
|
32107
|
+
return b ? { high: b.high, low: b.low } : null;
|
|
32108
|
+
},
|
|
32109
|
+
theme
|
|
32110
|
+
});
|
|
32111
|
+
const dpr = coords.dpr;
|
|
32112
|
+
const dataW = coords.width;
|
|
32113
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
32114
|
+
const add = (paneId, beforeZ, entry) => {
|
|
32115
|
+
const key = `${paneId}|${beforeZ}`;
|
|
32116
|
+
const bucket = buckets.get(key);
|
|
32117
|
+
if (bucket) bucket.entries.push(entry);
|
|
32118
|
+
else buckets.set(key, { paneId, beforeZ, entries: [entry] });
|
|
32119
|
+
};
|
|
32120
|
+
for (const pane of scene.orderedPanes()) {
|
|
32121
|
+
if (pane.collapsed) continue;
|
|
32122
|
+
const boundaries = scene.seriesBoundaries(pane.id);
|
|
32123
|
+
for (const m of scene.orderedIndicatorsForPane(pane.id)) {
|
|
32124
|
+
const set = modelDrawingSet(m, false);
|
|
32125
|
+
const tables = (m.tables ?? []).filter((t) => !t.overlay);
|
|
32126
|
+
if (drawingSetEmpty(set) && tables.length === 0) continue;
|
|
32127
|
+
const sc = scene.scaleFor(m, pane);
|
|
32128
|
+
const mp = sc === pane.scale ? pane : { ...pane, scale: sc };
|
|
32129
|
+
const beforeZ = indicatorSliceKey(scene.zOf(m.id), boundaries);
|
|
32130
|
+
add(pane.id, beforeZ, { set, tables, pane: mp, indexOffset: scene.offsetOf(m.id) });
|
|
32131
|
+
}
|
|
32132
|
+
if (pane.kind === "price") {
|
|
32133
|
+
for (const m of scene.indicators.values()) {
|
|
32134
|
+
const set = modelDrawingSet(m, true);
|
|
32135
|
+
const tables = (m.tables ?? []).filter((t) => t.overlay === true);
|
|
32136
|
+
if (drawingSetEmpty(set) && tables.length === 0) continue;
|
|
32137
|
+
add(pane.id, Infinity, { set, tables, pane, indexOffset: scene.offsetOf(m.id) });
|
|
32138
|
+
}
|
|
32139
|
+
}
|
|
32140
|
+
}
|
|
32141
|
+
for (const [key, { paneId, beforeZ, entries }] of buckets) {
|
|
32142
|
+
let canvas = this.sliceCache.get(key);
|
|
32143
|
+
if (!canvas) {
|
|
32144
|
+
canvas = document.createElement("canvas");
|
|
32145
|
+
this.sliceCache.set(key, canvas);
|
|
32146
|
+
}
|
|
32147
|
+
if (canvas.width !== ref.width || canvas.height !== ref.height) {
|
|
32148
|
+
canvas.width = ref.width;
|
|
32149
|
+
canvas.height = ref.height;
|
|
32150
|
+
}
|
|
32151
|
+
const ctx = canvas.getContext("2d");
|
|
32152
|
+
if (!ctx) continue;
|
|
32153
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
32154
|
+
ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
|
|
32155
|
+
for (const e of entries) this.paintEntry(ctx, e, coords, dataW, theme);
|
|
32156
|
+
const slices = out.get(paneId) ?? [];
|
|
32157
|
+
slices.push({ beforeZ, canvas });
|
|
32158
|
+
out.set(paneId, slices);
|
|
32159
|
+
}
|
|
32160
|
+
for (const key of [...this.sliceCache.keys()]) if (!buckets.has(key)) this.sliceCache.delete(key);
|
|
32161
|
+
for (const slices of out.values()) slices.sort((a, b) => a.beforeZ - b.beforeZ);
|
|
32162
|
+
return out;
|
|
32163
|
+
}
|
|
32164
|
+
paintEntry(ctx, e, coords, dataW, theme) {
|
|
32165
|
+
const { pane } = e;
|
|
32166
|
+
const paneTips = [];
|
|
32167
|
+
ctx.save();
|
|
32168
|
+
ctx.translate(0, pane.bounds.top);
|
|
32169
|
+
ctx.beginPath();
|
|
32170
|
+
ctx.rect(0, 0, dataW, pane.bounds.height);
|
|
32171
|
+
ctx.clip();
|
|
32172
|
+
this.drawScene.setSet(e.set, e.indexOffset);
|
|
32173
|
+
this.drawScene.render(
|
|
32174
|
+
ctx,
|
|
32175
|
+
dataW,
|
|
32176
|
+
pane.bounds.height,
|
|
32177
|
+
(l) => coords.logicalToX(l),
|
|
32178
|
+
(price) => coords.priceToY(price, pane.scale, pane.bounds) - pane.bounds.top
|
|
32179
|
+
);
|
|
32180
|
+
paneTips.push(...this.drawScene.labelTipRegions());
|
|
32181
|
+
for (const t of e.tables) paintTable(ctx, t, { paneHeight: pane.bounds.height, plotWidth: dataW, theme }, paneTips);
|
|
32182
|
+
ctx.restore();
|
|
32183
|
+
for (const r of paneTips) {
|
|
32184
|
+
this.tips.push({ ...r, top: r.top + pane.bounds.top, bottom: r.bottom + pane.bounds.top });
|
|
32185
|
+
}
|
|
32186
|
+
}
|
|
32187
|
+
/** Tooltip of the topmost label or table cell under a plot-space point, or null. Fed by the last prepare. */
|
|
32188
|
+
labelTooltipAt(x, y) {
|
|
32189
|
+
for (let i = this.tips.length - 1; i >= 0; i -= 1) {
|
|
32190
|
+
const r = this.tips[i];
|
|
32191
|
+
if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return r.text;
|
|
32192
|
+
}
|
|
32193
|
+
return null;
|
|
32194
|
+
}
|
|
32195
|
+
};
|
|
32196
|
+
function mergeSlices(indicator, user) {
|
|
32197
|
+
const out = /* @__PURE__ */ new Map();
|
|
32198
|
+
for (const [paneId, slices] of indicator) out.set(paneId, [...slices]);
|
|
32199
|
+
for (const [paneId, slices] of user) out.set(paneId, [...out.get(paneId) ?? [], ...slices]);
|
|
32200
|
+
for (const slices of out.values()) slices.sort((a, b) => a.beforeZ - b.beforeZ);
|
|
32201
|
+
return out;
|
|
32202
|
+
}
|
|
32203
|
+
|
|
31287
32204
|
// src/renderers/native/drawings/Projector.ts
|
|
31288
32205
|
function createProjector(coords, paneOf, paneIdAtY, barsInRange) {
|
|
31289
32206
|
return {
|
|
@@ -31335,19 +32252,8 @@ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log =
|
|
|
31335
32252
|
for (const model of models) {
|
|
31336
32253
|
const off = offsetOf(model.id);
|
|
31337
32254
|
for (const s of model.series) {
|
|
31338
|
-
if (s.
|
|
31339
|
-
|
|
31340
|
-
const b = s.bars[i - off];
|
|
31341
|
-
if (b) {
|
|
31342
|
-
consider(b.high);
|
|
31343
|
-
consider(b.low);
|
|
31344
|
-
}
|
|
31345
|
-
}
|
|
31346
|
-
} else if (isLineLikeSeries(s)) {
|
|
31347
|
-
for (let i = i0; i <= i1; i += 1) consider(s.points[i - off]?.value);
|
|
31348
|
-
if (s.kind === "histogram" || s.kind === "columns") consider(s.style?.base ?? 0);
|
|
31349
|
-
else if (s.style?.base != null) consider(s.style.base);
|
|
31350
|
-
}
|
|
32255
|
+
if (s.overlay === true) continue;
|
|
32256
|
+
considerSeries(s, i0, i1, off, consider);
|
|
31351
32257
|
}
|
|
31352
32258
|
for (const pl of model.priceLines) consider(pl.price);
|
|
31353
32259
|
}
|
|
@@ -31369,6 +32275,36 @@ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log =
|
|
|
31369
32275
|
const span = max - min;
|
|
31370
32276
|
return { min: min - span * MARGIN_BOTTOM, max: max + span * MARGIN_TOP };
|
|
31371
32277
|
}
|
|
32278
|
+
function considerSeries(s, i0, i1, off, consider) {
|
|
32279
|
+
if (s.kind === "candle" || s.kind === "bar") {
|
|
32280
|
+
for (let i = i0; i <= i1; i += 1) {
|
|
32281
|
+
const b = s.bars[i - off];
|
|
32282
|
+
if (b) {
|
|
32283
|
+
consider(b.high);
|
|
32284
|
+
consider(b.low);
|
|
32285
|
+
}
|
|
32286
|
+
}
|
|
32287
|
+
} else if (isLineLikeSeries(s)) {
|
|
32288
|
+
for (let i = i0; i <= i1; i += 1) consider(s.points[i - off]?.value);
|
|
32289
|
+
if (s.kind === "histogram" || s.kind === "columns") consider(s.style?.base ?? 0);
|
|
32290
|
+
else if (s.style?.base != null) consider(s.style.base);
|
|
32291
|
+
}
|
|
32292
|
+
}
|
|
32293
|
+
function overlaySeriesRange(models, i0, i1, offsetOf = () => 0) {
|
|
32294
|
+
let min = Infinity;
|
|
32295
|
+
let max = -Infinity;
|
|
32296
|
+
const consider = (v) => {
|
|
32297
|
+
if (v != null && Number.isFinite(v)) {
|
|
32298
|
+
if (v < min) min = v;
|
|
32299
|
+
if (v > max) max = v;
|
|
32300
|
+
}
|
|
32301
|
+
};
|
|
32302
|
+
for (const model of models) {
|
|
32303
|
+
const off = offsetOf(model.id);
|
|
32304
|
+
for (const s of model.series) if (s.overlay === true) considerSeries(s, i0, i1, off, consider);
|
|
32305
|
+
}
|
|
32306
|
+
return min === Infinity ? null : { min, max };
|
|
32307
|
+
}
|
|
31372
32308
|
function expandScaleByPixels(scale, heightPx, abovePx, belowPx) {
|
|
31373
32309
|
if (abovePx <= 0 && belowPx <= 0) return scale;
|
|
31374
32310
|
const content = heightPx - abovePx - belowPx;
|
|
@@ -32010,6 +32946,11 @@ var NativeRenderer = class {
|
|
|
32010
32946
|
this.vpvrRenderer = new VpvrRenderer();
|
|
32011
32947
|
this.resizeObserver = null;
|
|
32012
32948
|
this.dprMedia = null;
|
|
32949
|
+
/** Plot size in INTEGER device px, as last reported by the resize observer's
|
|
32950
|
+
* device-pixel-content-box — the browser's own statement of how many device pixels
|
|
32951
|
+
* it paints the plot into. `null` until the first report or where the box type is
|
|
32952
|
+
* unsupported (WebKit); syncSize then falls back to rounding the client rect. */
|
|
32953
|
+
this.plotDeviceSize = null;
|
|
32013
32954
|
this.coords = new CoordinateSystem();
|
|
32014
32955
|
this.scene = new SceneGraph();
|
|
32015
32956
|
// chosen at mount (WebGL2 if available, else canvas2d)
|
|
@@ -32017,6 +32958,10 @@ var NativeRenderer = class {
|
|
|
32017
32958
|
this.glowAmount = 0;
|
|
32018
32959
|
// WebGL2 neon-glow intensity (canvas2d ignores it)
|
|
32019
32960
|
this.chrome = new ChromeRenderer();
|
|
32961
|
+
/** Prepaints each indicator's Pine drawings into interleave slices at the model's z. */
|
|
32962
|
+
this.indicatorSlices = new IndicatorDrawingSlices();
|
|
32963
|
+
/** Hover tooltips for Pine labels (canvas hit-rects collected by the chrome layer). */
|
|
32964
|
+
this.labelTooltip = null;
|
|
32020
32965
|
this.crosshairLayer = new CrosshairRenderer();
|
|
32021
32966
|
/** 1 Hz repaint pump so the price-axis countdown-to-bar-close ticks; null when off. */
|
|
32022
32967
|
this.countdownTimer = null;
|
|
@@ -32027,6 +32972,8 @@ var NativeRenderer = class {
|
|
|
32027
32972
|
this.indicatorValuesOn = true;
|
|
32028
32973
|
/** Host-contributed legend actions — held here so a rebuild of the legend re-wires them. */
|
|
32029
32974
|
this.legendActionsProvider = null;
|
|
32975
|
+
/** Host-contributed legend callouts — held here so a rebuild of the legend re-wires them. */
|
|
32976
|
+
this.legendCalloutsProvider = null;
|
|
32030
32977
|
/** Host override of the legend's fold toggle — held here so a remount re-applies it. */
|
|
32031
32978
|
this.legendOverviewAction = null;
|
|
32032
32979
|
// ── keyboard navigation / accessibility (item 11) ──
|
|
@@ -32161,7 +33108,6 @@ var NativeRenderer = class {
|
|
|
32161
33108
|
this.toggleVisibleCbs = /* @__PURE__ */ new Set();
|
|
32162
33109
|
this.moveIndicatorCbs = /* @__PURE__ */ new Set();
|
|
32163
33110
|
this.priceStyleCbs = /* @__PURE__ */ new Set();
|
|
32164
|
-
this.tableOverlays = /* @__PURE__ */ new Map();
|
|
32165
33111
|
this.name = "native";
|
|
32166
33112
|
this.features = ["logScale", "currentPriceLine", "priceLabel", "countdown", "upColor", "downColor", "glow", "animZoom", "animPan", "intro", "zoomAnchor", "axisDrag", "paneResize", "candleZOrder", "candleVisible", "seriesOrder", "highlights", "sessionZones", "gridlines", "axisLabels", "scaleMode", "invertScale", "paneScales", "autoScale", "timezone", "keyboard", "historyChords", "priceStyle", "priceBaseline", "baselinePrice", "settings", "attribution", "dialogHost", "tradeMarkers", "indicatorTitles", "indicatorValues"];
|
|
32167
33113
|
/** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
|
|
@@ -32475,7 +33421,6 @@ var NativeRenderer = class {
|
|
|
32475
33421
|
* hidden) on clear so a re-show picks up the current theme.
|
|
32476
33422
|
*/
|
|
32477
33423
|
setLoading(loading) {
|
|
32478
|
-
for (const overlay of this.tableOverlays.values()) overlay.setVisible(!loading);
|
|
32479
33424
|
if (!loading || !this.wrapper) {
|
|
32480
33425
|
this.loadingEl?.remove();
|
|
32481
33426
|
this.loadingEl = null;
|
|
@@ -33135,6 +34080,10 @@ var NativeRenderer = class {
|
|
|
33135
34080
|
this.plot.appendChild(this.scrollButton);
|
|
33136
34081
|
this.plot.addEventListener("pointermove", this.onScrollProximityMove);
|
|
33137
34082
|
this.plot.addEventListener("pointerleave", this.onScrollProximityLeave);
|
|
34083
|
+
this.labelTooltip = new LabelTooltip(this.plot, {
|
|
34084
|
+
theme: () => this.chromeTheme(),
|
|
34085
|
+
lookup: (x, y) => this.indicatorSlices.labelTooltipAt(x, y)
|
|
34086
|
+
});
|
|
33138
34087
|
this.userDrawings = new UserDrawingController(this.wrapper, this.plot, this.drawingsCanvas, {
|
|
33139
34088
|
projector: () => this.drawingProjector(),
|
|
33140
34089
|
dpr: () => this.coords.dpr,
|
|
@@ -33174,9 +34123,10 @@ var NativeRenderer = class {
|
|
|
33174
34123
|
this.inputsUI.setDialogHost(this.dialogHost);
|
|
33175
34124
|
this.inputsUI.setSymbolPicker(this.symbolPicker);
|
|
33176
34125
|
this.inputsUI.setLegendActions(this.legendActionsProvider);
|
|
34126
|
+
this.inputsUI.setLegendCallouts(this.legendCalloutsProvider);
|
|
33177
34127
|
this.inputsUI.setLegendOverviewAction(this.legendOverviewAction);
|
|
33178
34128
|
this.inputsUI.setOnChange((c) => {
|
|
33179
|
-
for (const cb of this.inputChangeCbs) cb({ indicatorId: c.indicatorId, key: c.key, value: c.value });
|
|
34129
|
+
for (const cb of this.inputChangeCbs) cb({ indicatorId: c.indicatorId, key: c.key, value: c.value, ...c.kind ? { kind: c.kind } : {} });
|
|
33180
34130
|
});
|
|
33181
34131
|
this.inputsUI.setOnRemove((id) => {
|
|
33182
34132
|
for (const cb of this.removeIndicatorCbs) cb(id);
|
|
@@ -33217,6 +34167,7 @@ var NativeRenderer = class {
|
|
|
33217
34167
|
this.emitPaneAction({ type: "maximize", paneId, maximized });
|
|
33218
34168
|
}
|
|
33219
34169
|
});
|
|
34170
|
+
this.paneControls.setSuspended(this.layoutMode === "mobile");
|
|
33220
34171
|
this.axisScaleButtons = new AxisScaleButtons(this.plot, theme, {
|
|
33221
34172
|
panes: () => this.axisScaleViews(),
|
|
33222
34173
|
rightAxis: () => this.rightAxisW,
|
|
@@ -33226,8 +34177,19 @@ var NativeRenderer = class {
|
|
|
33226
34177
|
if (pane) this.setPaneLog(paneId, !paneLogScale(this.scene, pane));
|
|
33227
34178
|
}
|
|
33228
34179
|
});
|
|
33229
|
-
this.resizeObserver = new ResizeObserver(() =>
|
|
34180
|
+
this.resizeObserver = new ResizeObserver((entries) => {
|
|
34181
|
+
for (const e of entries) {
|
|
34182
|
+
if (e.target !== this.plot) continue;
|
|
34183
|
+
const s = e.devicePixelContentBoxSize?.[0];
|
|
34184
|
+
if (s) this.plotDeviceSize = { width: s.inlineSize, height: s.blockSize };
|
|
34185
|
+
}
|
|
34186
|
+
this.resize();
|
|
34187
|
+
});
|
|
33230
34188
|
this.resizeObserver.observe(this.wrapper);
|
|
34189
|
+
try {
|
|
34190
|
+
this.resizeObserver.observe(this.plot, { box: "device-pixel-content-box" });
|
|
34191
|
+
} catch {
|
|
34192
|
+
}
|
|
33231
34193
|
this.watchDpr();
|
|
33232
34194
|
this.syncSize();
|
|
33233
34195
|
}
|
|
@@ -33352,8 +34314,6 @@ var NativeRenderer = class {
|
|
|
33352
34314
|
this.inputsUI?.destroy();
|
|
33353
34315
|
this.paneControls?.destroy();
|
|
33354
34316
|
this.axisScaleButtons?.destroy();
|
|
33355
|
-
for (const overlay of this.tableOverlays.values()) overlay.destroy();
|
|
33356
|
-
this.tableOverlays.clear();
|
|
33357
34317
|
this.resizeObserver?.disconnect();
|
|
33358
34318
|
this.resizeObserver = null;
|
|
33359
34319
|
this.dprMedia?.removeEventListener("change", this.onDprChange);
|
|
@@ -33366,6 +34326,8 @@ var NativeRenderer = class {
|
|
|
33366
34326
|
this.settingsButton = null;
|
|
33367
34327
|
this.plot?.removeEventListener("pointermove", this.onScrollProximityMove);
|
|
33368
34328
|
this.plot?.removeEventListener("pointerleave", this.onScrollProximityLeave);
|
|
34329
|
+
this.labelTooltip?.destroy();
|
|
34330
|
+
this.labelTooltip = null;
|
|
33369
34331
|
this.scrollButton?.remove();
|
|
33370
34332
|
this.scrollButton = null;
|
|
33371
34333
|
for (const l of this.extLayers) l.instance.destroy?.();
|
|
@@ -33380,6 +34342,7 @@ var NativeRenderer = class {
|
|
|
33380
34342
|
this.attributionEl = null;
|
|
33381
34343
|
this.mountContainer?.style.removeProperty("--vela-toolbar-gutter");
|
|
33382
34344
|
this.mountContainer?.style.removeProperty("--vela-scale-gutter");
|
|
34345
|
+
this.mountContainer?.style.removeProperty("--vela-bottom-gutter");
|
|
33383
34346
|
this.mountContainer?.style.removeProperty("--vela-price-pane-top");
|
|
33384
34347
|
this.mountContainer?.style.removeProperty("--vela-price-pane-bottom");
|
|
33385
34348
|
this.mountContainer = null;
|
|
@@ -33472,7 +34435,6 @@ var NativeRenderer = class {
|
|
|
33472
34435
|
ensurePane(pane) {
|
|
33473
34436
|
this.scene.ensurePane(pane.id, pane.kind, pane.order, pane.heightWeight ?? (pane.kind === "price" ? 3 : 1));
|
|
33474
34437
|
this.layoutPanes();
|
|
33475
|
-
this.repositionTables();
|
|
33476
34438
|
this.paneControls?.refresh();
|
|
33477
34439
|
this.scheduler.invalidate(4 /* Full */);
|
|
33478
34440
|
}
|
|
@@ -33494,17 +34456,14 @@ var NativeRenderer = class {
|
|
|
33494
34456
|
if (!model.ownScale) this.scene.dropIndicatorScale(handle.id);
|
|
33495
34457
|
this.inputsUI.setPane(handle.id, paneId);
|
|
33496
34458
|
this.refreshAnchorOffset(model);
|
|
33497
|
-
this.syncTables(model);
|
|
33498
34459
|
this.refreshAxisWidth();
|
|
33499
34460
|
this.layoutPanes();
|
|
33500
|
-
this.repositionTables();
|
|
33501
34461
|
this.paneControls?.refresh();
|
|
33502
34462
|
this.scheduler.invalidate(4 /* Full */);
|
|
33503
34463
|
}
|
|
33504
34464
|
orderPanes(orderedIds) {
|
|
33505
34465
|
this.scene.orderPanes(orderedIds);
|
|
33506
34466
|
this.layoutPanes();
|
|
33507
|
-
this.repositionTables();
|
|
33508
34467
|
this.paneControls?.refresh();
|
|
33509
34468
|
this.scheduler.invalidate(4 /* Full */);
|
|
33510
34469
|
}
|
|
@@ -33513,7 +34472,6 @@ var NativeRenderer = class {
|
|
|
33513
34472
|
if (!pane || pane.collapsed === collapsed) return;
|
|
33514
34473
|
pane.collapsed = collapsed;
|
|
33515
34474
|
this.layoutPanes();
|
|
33516
|
-
this.repositionTables();
|
|
33517
34475
|
this.paneControls?.refresh();
|
|
33518
34476
|
this.scheduler.invalidate(4 /* Full */);
|
|
33519
34477
|
}
|
|
@@ -33521,7 +34479,6 @@ var NativeRenderer = class {
|
|
|
33521
34479
|
if (paneId !== null && !this.scene.panes.has(paneId)) paneId = null;
|
|
33522
34480
|
this.maximizedPaneId = paneId;
|
|
33523
34481
|
this.layoutPanes();
|
|
33524
|
-
this.repositionTables();
|
|
33525
34482
|
this.paneControls?.refresh();
|
|
33526
34483
|
this.scheduler.invalidate(4 /* Full */);
|
|
33527
34484
|
}
|
|
@@ -33606,9 +34563,8 @@ var NativeRenderer = class {
|
|
|
33606
34563
|
else this.scene.assignIndicatorZ(model.id);
|
|
33607
34564
|
this.inputsUI.upsert(model.id, model.shorttitle ?? model.title, model.inputs, model.inputValues, model.paneId, {
|
|
33608
34565
|
native: !!model.native,
|
|
33609
|
-
...model.
|
|
34566
|
+
...model.props ? { props: model.props, propValues: model.propValues ?? {} } : {}
|
|
33610
34567
|
});
|
|
33611
|
-
this.syncTables(model);
|
|
33612
34568
|
if (model.native?.type === "volume") {
|
|
33613
34569
|
this.volumeActive = true;
|
|
33614
34570
|
this.volumeHidden = false;
|
|
@@ -33632,7 +34588,6 @@ var NativeRenderer = class {
|
|
|
33632
34588
|
}
|
|
33633
34589
|
}
|
|
33634
34590
|
applyPatch(model, patch);
|
|
33635
|
-
this.syncTables(model);
|
|
33636
34591
|
this.scheduler.invalidate(3 /* Light */);
|
|
33637
34592
|
}
|
|
33638
34593
|
removeIndicator(handle) {
|
|
@@ -33651,14 +34606,12 @@ var NativeRenderer = class {
|
|
|
33651
34606
|
this.scene.forgetAnchorOffset(handle.id);
|
|
33652
34607
|
this.scene.dropIndicatorScale(handle.id);
|
|
33653
34608
|
this.inputsUI.remove(handle.id);
|
|
33654
|
-
this.tableOverlays.get(handle.id)?.destroy();
|
|
33655
|
-
this.tableOverlays.delete(handle.id);
|
|
33656
34609
|
this.refreshAxisWidth();
|
|
33657
34610
|
this.paneControls?.refresh();
|
|
33658
34611
|
this.scheduler.invalidate(4 /* Full */);
|
|
33659
34612
|
}
|
|
33660
|
-
setIndicatorInputs(handle, values) {
|
|
33661
|
-
this.inputsUI.setValues(handle.id, values);
|
|
34613
|
+
setIndicatorInputs(handle, values, props) {
|
|
34614
|
+
this.inputsUI.setValues(handle.id, values, props);
|
|
33662
34615
|
}
|
|
33663
34616
|
setSymbolPicker(picker) {
|
|
33664
34617
|
this.symbolPicker = picker;
|
|
@@ -33668,6 +34621,10 @@ var NativeRenderer = class {
|
|
|
33668
34621
|
this.legendActionsProvider = provider;
|
|
33669
34622
|
this.inputsUI?.setLegendActions(provider);
|
|
33670
34623
|
}
|
|
34624
|
+
setLegendCallouts(provider) {
|
|
34625
|
+
this.legendCalloutsProvider = provider;
|
|
34626
|
+
this.inputsUI?.setLegendCallouts(provider);
|
|
34627
|
+
}
|
|
33671
34628
|
setLegendOverviewAction(action) {
|
|
33672
34629
|
this.legendOverviewAction = action;
|
|
33673
34630
|
this.inputsUI?.setLegendOverviewAction(action);
|
|
@@ -33692,8 +34649,6 @@ var NativeRenderer = class {
|
|
|
33692
34649
|
}
|
|
33693
34650
|
if (!visible) {
|
|
33694
34651
|
this.scene.indicators.delete(handle.id);
|
|
33695
|
-
this.tableOverlays.get(handle.id)?.destroy();
|
|
33696
|
-
this.tableOverlays.delete(handle.id);
|
|
33697
34652
|
}
|
|
33698
34653
|
this.inputsUI.setVisible(handle.id, visible);
|
|
33699
34654
|
this.scheduler.invalidate(4 /* Full */);
|
|
@@ -33739,6 +34694,7 @@ var NativeRenderer = class {
|
|
|
33739
34694
|
this.userDrawings?.setLayoutMode(mode);
|
|
33740
34695
|
this.settingsDialog?.setLayoutMode(mode);
|
|
33741
34696
|
this.inputsUI?.setLayoutMode(mode);
|
|
34697
|
+
this.paneControls?.setSuspended(mode === "mobile");
|
|
33742
34698
|
if (this.scrollButton) {
|
|
33743
34699
|
const px = mode === "mobile" ? SCROLL_BTN_SIZE_TOUCH : SCROLL_BTN_SIZE;
|
|
33744
34700
|
this.scrollButton.style.width = `${px}px`;
|
|
@@ -34134,7 +35090,6 @@ var NativeRenderer = class {
|
|
|
34134
35090
|
/** Relayout + repaint + refresh the hover buttons after a collapse/maximize/order change. */
|
|
34135
35091
|
afterPaneLayoutChange() {
|
|
34136
35092
|
this.layoutPanes();
|
|
34137
|
-
this.repositionTables();
|
|
34138
35093
|
this.paneControls?.refresh();
|
|
34139
35094
|
this.scheduler.invalidate(4 /* Full */);
|
|
34140
35095
|
}
|
|
@@ -34215,7 +35170,6 @@ var NativeRenderer = class {
|
|
|
34215
35170
|
above.heightWeight = next.above;
|
|
34216
35171
|
below.heightWeight = next.below;
|
|
34217
35172
|
this.layoutPanes();
|
|
34218
|
-
this.repositionTables();
|
|
34219
35173
|
this.scheduler.invalidate(4 /* Full */);
|
|
34220
35174
|
}
|
|
34221
35175
|
/** Double-click a separator → split the two adjacent panes evenly (each gets half of
|
|
@@ -34230,7 +35184,6 @@ var NativeRenderer = class {
|
|
|
34230
35184
|
above.heightWeight = half;
|
|
34231
35185
|
below.heightWeight = half;
|
|
34232
35186
|
this.layoutPanes();
|
|
34233
|
-
this.repositionTables();
|
|
34234
35187
|
this.scheduler.invalidate(4 /* Full */);
|
|
34235
35188
|
}
|
|
34236
35189
|
// ── keyboard navigation / accessibility (item 11) ──
|
|
@@ -34509,7 +35462,10 @@ var NativeRenderer = class {
|
|
|
34509
35462
|
const liveActual = li >= 0 ? this.bars[li] : void 0;
|
|
34510
35463
|
const easeLive = !!liveActual && this.liveEaseTime === liveActual.time && (liveActual.high !== this.liveEaseHigh || liveActual.low !== this.liveEaseLow || liveActual.close !== this.liveEaseClose);
|
|
34511
35464
|
if (easeLive && liveActual) this.bars[li] = { ...liveActual, high: this.liveEaseHigh, low: this.liveEaseLow, close: this.liveEaseClose };
|
|
34512
|
-
this.scene.drawingSlices =
|
|
35465
|
+
this.scene.drawingSlices = mergeSlices(
|
|
35466
|
+
this.indicatorSlices.prepare(this.scene, this.coords, this.theme, this.dataCanvas),
|
|
35467
|
+
this.userDrawings?.prepareSlices(this.scene.orderedPanes().map((p) => p.id)) ?? /* @__PURE__ */ new Map()
|
|
35468
|
+
);
|
|
34513
35469
|
this.backdropRenderer.render(this.scene, this.coords, this.theme, gridAlpha);
|
|
34514
35470
|
this.backend.render(this.scene, this.coords, this.theme);
|
|
34515
35471
|
this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
|
|
@@ -34648,7 +35604,11 @@ var NativeRenderer = class {
|
|
|
34648
35604
|
}
|
|
34649
35605
|
const models = this.scene.indicatorsForPane(pane.id);
|
|
34650
35606
|
const masterModels = models.filter((m) => m.ownScale !== true);
|
|
34651
|
-
|
|
35607
|
+
let dr = this.chrome.paneDrawingsRange(masterModels, this.scene, pane === pricePane, vr);
|
|
35608
|
+
if (pane === pricePane) {
|
|
35609
|
+
const or = overlaySeriesRange(this.scene.indicators.values(), i0, i1, (id) => this.scene.offsetOf(id));
|
|
35610
|
+
if (or) dr = dr ? { min: Math.min(dr.min, or.min), max: Math.max(dr.max, or.max) } : or;
|
|
35611
|
+
}
|
|
34652
35612
|
const includeCandles = pane.kind === "price" && !this.scene.candlesHidden;
|
|
34653
35613
|
pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
|
|
34654
35614
|
pane.percentBaseline = pane.kind === "price" ? this.bars[i0]?.close ?? 0 : this.firstVisibleValue(masterModels, i0);
|
|
@@ -34922,6 +35882,7 @@ var NativeRenderer = class {
|
|
|
34922
35882
|
for (const m of models) {
|
|
34923
35883
|
const off = this.scene.offsetOf(m.id);
|
|
34924
35884
|
for (const s of m.series) {
|
|
35885
|
+
if (s.overlay === true) continue;
|
|
34925
35886
|
if (isLineLikeSeries(s)) {
|
|
34926
35887
|
const v = s.points[i0 - off]?.value;
|
|
34927
35888
|
if (v != null && Number.isFinite(v)) return v;
|
|
@@ -34942,27 +35903,6 @@ var NativeRenderer = class {
|
|
|
34942
35903
|
}
|
|
34943
35904
|
return maxVol;
|
|
34944
35905
|
}
|
|
34945
|
-
/** Create/update/destroy an indicator's DOM table overlay (anchored off real pane geometry). */
|
|
34946
|
-
syncTables(model) {
|
|
34947
|
-
const tables = model.tables ?? [];
|
|
34948
|
-
let overlay = this.tableOverlays.get(model.id);
|
|
34949
|
-
if (tables.length === 0) {
|
|
34950
|
-
if (overlay) {
|
|
34951
|
-
overlay.destroy();
|
|
34952
|
-
this.tableOverlays.delete(model.id);
|
|
34953
|
-
}
|
|
34954
|
-
return;
|
|
34955
|
-
}
|
|
34956
|
-
if (!overlay) {
|
|
34957
|
-
overlay = new TableOverlay(this.plot, this.theme, (id) => this.paneBoundsFor(id));
|
|
34958
|
-
overlay.setVisible(this.loadingEl === null);
|
|
34959
|
-
this.tableOverlays.set(model.id, overlay);
|
|
34960
|
-
}
|
|
34961
|
-
overlay.update(tables);
|
|
34962
|
-
}
|
|
34963
|
-
repositionTables() {
|
|
34964
|
-
for (const overlay of this.tableOverlays.values()) overlay.reposition();
|
|
34965
|
-
}
|
|
34966
35906
|
layoutPanes() {
|
|
34967
35907
|
const panes = this.scene.orderedPanes();
|
|
34968
35908
|
const dataHeight = this.coords.height;
|
|
@@ -35013,6 +35953,7 @@ var NativeRenderer = class {
|
|
|
35013
35953
|
const visible = maxPane ? [maxPane] : this.scene.orderedPanes().filter((p) => !p.collapsed);
|
|
35014
35954
|
const paneBottom = visible.length ? Math.max(...visible.map((p) => p.bounds.top + p.bounds.height)) : dataHeight;
|
|
35015
35955
|
this.scrollBtnBottomPx = SCROLL_BTN_BOTTOM + Math.max(0, dataHeight - paneBottom);
|
|
35956
|
+
this.mountContainer?.style.setProperty("--vela-bottom-gutter", `${TIME_AXIS_H + Math.max(0, dataHeight - paneBottom)}px`);
|
|
35016
35957
|
this.scrollBtnRightPx = this.rightAxisW + SCROLL_BTN_RIGHT_INSET;
|
|
35017
35958
|
if (this.scrollButton) {
|
|
35018
35959
|
this.scrollButton.style.bottom = `${this.scrollBtnBottomPx}px`;
|
|
@@ -35105,30 +36046,33 @@ var NativeRenderer = class {
|
|
|
35105
36046
|
if (w <= 0 || h <= 0) return;
|
|
35106
36047
|
const dpr = window.devicePixelRatio || 1;
|
|
35107
36048
|
this.plot.style.left = `${this.toolbarGutter}px`;
|
|
35108
|
-
const
|
|
35109
|
-
|
|
35110
|
-
|
|
35111
|
-
|
|
35112
|
-
|
|
35113
|
-
|
|
35114
|
-
|
|
35115
|
-
|
|
35116
|
-
|
|
35117
|
-
|
|
35118
|
-
|
|
35119
|
-
|
|
35120
|
-
|
|
35121
|
-
|
|
35122
|
-
|
|
35123
|
-
|
|
35124
|
-
this.
|
|
35125
|
-
this.
|
|
35126
|
-
this.
|
|
35127
|
-
|
|
36049
|
+
const rect = this.plot.getBoundingClientRect();
|
|
36050
|
+
let bw = Math.max(1, Math.round(rect.width * dpr));
|
|
36051
|
+
let bh = Math.max(1, Math.round(rect.height * dpr));
|
|
36052
|
+
const dev = this.plotDeviceSize;
|
|
36053
|
+
if (dev && Math.abs(dev.width - rect.width * dpr) <= 1 && Math.abs(dev.height - rect.height * dpr) <= 1) {
|
|
36054
|
+
bw = Math.max(1, dev.width);
|
|
36055
|
+
bh = Math.max(1, dev.height);
|
|
36056
|
+
}
|
|
36057
|
+
const pw = bw / dpr;
|
|
36058
|
+
const ph = bh / dpr;
|
|
36059
|
+
const size = (canvas) => {
|
|
36060
|
+
canvas.width = bw;
|
|
36061
|
+
canvas.height = bh;
|
|
36062
|
+
canvas.style.width = `${pw}px`;
|
|
36063
|
+
canvas.style.height = `${ph}px`;
|
|
36064
|
+
};
|
|
36065
|
+
size(this.dataCanvas);
|
|
36066
|
+
size(this.backdropCanvas);
|
|
36067
|
+
size(this.volumeCanvas);
|
|
36068
|
+
for (const l of this.extLayers) size(l.canvas);
|
|
36069
|
+
size(this.vpvrCanvas);
|
|
36070
|
+
size(this.chromeCanvas);
|
|
36071
|
+
size(this.drawingsCanvas);
|
|
36072
|
+
size(this.cursorCanvas);
|
|
35128
36073
|
this.coords.setSize(Math.max(1, pw - this.rightAxisW), Math.max(1, ph - TIME_AXIS_H), dpr);
|
|
35129
36074
|
this.scene.crosshair = null;
|
|
35130
36075
|
this.layoutPanes();
|
|
35131
|
-
this.repositionTables();
|
|
35132
36076
|
this.userDrawings?.onResize();
|
|
35133
36077
|
if (!this.didInitialFit && this.coords.barCount > 0) {
|
|
35134
36078
|
this.fitContent();
|
|
@@ -35794,45 +36738,24 @@ var CSS21 = `
|
|
|
35794
36738
|
}
|
|
35795
36739
|
.vela-statusline .vela-sl-symbol { font-weight: 600; font-size: var(--vela-font-size-lg); }
|
|
35796
36740
|
.vela-statusline .vela-sl-meta { color: var(--vela-fg-muted); font-size: var(--vela-font-size-md); font-weight: 600; }
|
|
35797
|
-
/* Market status badge \u2014 icon-only 16px circle, label on hover
|
|
35798
|
-
|
|
35799
|
-
|
|
35800
|
-
place-items: center;
|
|
35801
|
-
width: 16px;
|
|
35802
|
-
height: 16px;
|
|
35803
|
-
border-radius: 50%;
|
|
35804
|
-
flex: none;
|
|
35805
|
-
align-self: center;
|
|
35806
|
-
line-height: 0;
|
|
35807
|
-
cursor: default;
|
|
35808
|
-
}
|
|
35809
|
-
.vela-statusline .vela-sl-market svg {
|
|
35810
|
-
width: 12px;
|
|
35811
|
-
height: 12px;
|
|
35812
|
-
display: block;
|
|
35813
|
-
}
|
|
35814
|
-
/* Open wears the theme's up color; the other sessions are meaning constants from the
|
|
35815
|
-
* palette (amber pre, sky post, gray closed/holiday). Same ink at 20% for the circle. */
|
|
35816
|
-
.vela-statusline .vela-sl-market[data-status='open'] {
|
|
35817
|
-
background: color-mix(in srgb, var(--vela-up) 20%, transparent);
|
|
35818
|
-
color: var(--vela-up);
|
|
35819
|
-
}
|
|
35820
|
-
.vela-statusline .vela-sl-market[data-status='pre'] { background: color-mix(in srgb, ${SESSION_PRE} 20%, transparent); color: ${SESSION_PRE}; }
|
|
35821
|
-
.vela-statusline .vela-sl-market[data-status='post'] { background: color-mix(in srgb, ${SESSION_POST} 20%, transparent); color: ${SESSION_POST}; }
|
|
35822
|
-
.vela-statusline .vela-sl-market[data-status='closed'],
|
|
35823
|
-
.vela-statusline .vela-sl-market[data-status='holiday'] { background: color-mix(in srgb, ${SESSION_OFF} 20%, transparent); color: ${SESSION_OFF}; }
|
|
36741
|
+
/* Market status badge \u2014 a kit callout bubble (icon-only 16px circle, label on hover
|
|
36742
|
+
* via the kit tooltip); the session tint is applied per status in setMarketStatus. */
|
|
36743
|
+
.vela-statusline .vela-sl-market { align-self: center; }
|
|
35824
36744
|
.vela-statusline .vela-sl-ohlc { display: flex; gap: var(--vela-space-1); color: var(--vela-fg-muted); }
|
|
35825
36745
|
.vela-statusline .vela-sl-ohlc b { color: var(--vela-fg); font-weight: 500; }
|
|
35826
36746
|
/* The change value wears the SAME ink as the OHLC values (set inline per render) \u2014
|
|
35827
36747
|
* these are the pre-ink fallbacks only. */
|
|
35828
36748
|
.vela-statusline .vela-sl-change[data-dir='up'] { color: var(--vela-up); }
|
|
35829
36749
|
.vela-statusline .vela-sl-change[data-dir='down'] { color: var(--vela-down); }
|
|
35830
|
-
/* Stack the
|
|
35831
|
-
* The renderer
|
|
35832
|
-
*
|
|
35833
|
-
*
|
|
35834
|
-
*
|
|
35835
|
-
|
|
36750
|
+
/* Stack the TOP pane's legend below the status line (lower study panes stay put).
|
|
36751
|
+
* The renderer marks whichever legend sits at the plot's top edge \u2014 the price pane
|
|
36752
|
+
* normally, or a maximized study pane filling the plot \u2014 so the legend never merges
|
|
36753
|
+
* with the status line whichever pane owns the top. The renderer sets the legend's
|
|
36754
|
+
* inline top \u2014 shift with a transform, don't fight it. Scoped to hosts that actually
|
|
36755
|
+
* CARRY a status line (the marker class set by the Statusline constructor) \u2014 the
|
|
36756
|
+
* stylesheet is document-global, so a bare attribute selector here would shift every
|
|
36757
|
+
* chart on the page, including statusline-less ones. */
|
|
36758
|
+
.vela-has-statusline [${LEGEND_AT_TOP_ATTR}] { transform: translateY(26px); }
|
|
35836
36759
|
/* Mobile: two-line chip \u2014 logo / symbol / meta / market status on one aligned row, the
|
|
35837
36760
|
* bar change on the next. Full O/H/L/C stays hidden (too dense on a phone-width plot).
|
|
35838
36761
|
* GRID, not a wrapping flexbox: an absolutely positioned wrapping flex container sizes
|
|
@@ -35868,7 +36791,7 @@ var CSS21 = `
|
|
|
35868
36791
|
font-size: var(--vela-font-size-sm);
|
|
35869
36792
|
line-height: 1.2;
|
|
35870
36793
|
}
|
|
35871
|
-
[data-layout='mobile'] .vela-has-statusline [
|
|
36794
|
+
[data-layout='mobile'] .vela-has-statusline [${LEGEND_AT_TOP_ATTR}] { transform: translateY(40px); }
|
|
35872
36795
|
/* FIT mode (multi-chart cells \u2014 see setFitMode): the line never wraps; segments that
|
|
35873
36796
|
* don't fit are HIDDEN by fit() (change first, then meta, then the market badge), so
|
|
35874
36797
|
* overflow:hidden only guards the transient between a resize and the next measure.
|
|
@@ -35885,7 +36808,7 @@ var CSS21 = `
|
|
|
35885
36808
|
padding-left: 0;
|
|
35886
36809
|
}
|
|
35887
36810
|
/* One row again \u2014 the mobile two-line shift doesn't apply in fit mode. */
|
|
35888
|
-
[data-layout='mobile'] .vela-sl-fit-host.vela-has-statusline [
|
|
36811
|
+
[data-layout='mobile'] .vela-sl-fit-host.vela-has-statusline [${LEGEND_AT_TOP_ATTR}] { transform: translateY(26px); }
|
|
35889
36812
|
`;
|
|
35890
36813
|
function baseOfTicker(ticker) {
|
|
35891
36814
|
return ticker.replace(/[-_/]?(USDT|USDC|USD1|USDS|BUSD|USD|EUR|PERP)$/i, "") || ticker;
|
|
@@ -35897,6 +36820,13 @@ var MARKET_LABELS = {
|
|
|
35897
36820
|
closed: "Market Closed",
|
|
35898
36821
|
holiday: "Market Holiday"
|
|
35899
36822
|
};
|
|
36823
|
+
var MARKET_INKS = {
|
|
36824
|
+
open: "var(--vela-up)",
|
|
36825
|
+
pre: SESSION_PRE,
|
|
36826
|
+
post: SESSION_POST,
|
|
36827
|
+
closed: SESSION_OFF,
|
|
36828
|
+
holiday: SESSION_OFF
|
|
36829
|
+
};
|
|
35900
36830
|
function statuslineInkOf(renderer, priceStyle) {
|
|
35901
36831
|
const cfg = renderer.getConfig();
|
|
35902
36832
|
const c = (v) => typeof v === "string" ? v : null;
|
|
@@ -35955,8 +36885,15 @@ var Statusline = class {
|
|
|
35955
36885
|
this.symbolEl.textContent = ticker;
|
|
35956
36886
|
this.metaEl = doc.createElement("span");
|
|
35957
36887
|
this.metaEl.className = "vela-sl-meta";
|
|
35958
|
-
this.
|
|
35959
|
-
|
|
36888
|
+
this.marketBubble = new CalloutBubble({
|
|
36889
|
+
icon: "market-open",
|
|
36890
|
+
background: `color-mix(in srgb, ${MARKET_INKS.open} 20%, transparent)`,
|
|
36891
|
+
color: MARKET_INKS.open,
|
|
36892
|
+
label: MARKET_LABELS.open,
|
|
36893
|
+
host
|
|
36894
|
+
});
|
|
36895
|
+
this.marketEl = this.marketBubble.el;
|
|
36896
|
+
this.marketEl.classList.add("vela-sl-market");
|
|
35960
36897
|
this.ohlcEl = doc.createElement("span");
|
|
35961
36898
|
this.ohlcEl.className = "vela-sl-ohlc";
|
|
35962
36899
|
this.changeEl = doc.createElement("span");
|
|
@@ -36044,8 +36981,13 @@ var Statusline = class {
|
|
|
36044
36981
|
* hover label. Callers with no session model leave the constructor's 'open'. */
|
|
36045
36982
|
setMarketStatus(status) {
|
|
36046
36983
|
this.marketEl.dataset.status = status;
|
|
36047
|
-
|
|
36048
|
-
this.
|
|
36984
|
+
const ink = MARKET_INKS[status];
|
|
36985
|
+
this.marketBubble.set({
|
|
36986
|
+
icon: `market-${status}`,
|
|
36987
|
+
background: `color-mix(in srgb, ${ink} 20%, transparent)`,
|
|
36988
|
+
color: ink,
|
|
36989
|
+
label: MARKET_LABELS[status]
|
|
36990
|
+
});
|
|
36049
36991
|
this.marketTip.setContent(MARKET_LABELS[status]);
|
|
36050
36992
|
}
|
|
36051
36993
|
setPartVisible(part, visible) {
|
|
@@ -36078,6 +37020,7 @@ var Statusline = class {
|
|
|
36078
37020
|
this.fitRO?.disconnect();
|
|
36079
37021
|
this.fitRO = null;
|
|
36080
37022
|
this.marketTip.destroy();
|
|
37023
|
+
this.marketBubble.destroy();
|
|
36081
37024
|
this.host.classList.remove("vela-has-statusline", "vela-sl-fit-host");
|
|
36082
37025
|
this.el.remove();
|
|
36083
37026
|
}
|
|
@@ -36304,6 +37247,8 @@ var SessionShadingTracker = class {
|
|
|
36304
37247
|
this.spec = null;
|
|
36305
37248
|
this.ready = false;
|
|
36306
37249
|
this.session = "regular";
|
|
37250
|
+
/** Whether one bar is shorter than a civil day — only then do pre/post bars exist. */
|
|
37251
|
+
this.intraday = false;
|
|
36307
37252
|
this.covered = null;
|
|
36308
37253
|
/** The newest range seen — viewport moves during metadata resolution (a load's fit
|
|
36309
37254
|
* animation) must not be lost, so the resolution always expands the LATEST range. */
|
|
@@ -36316,6 +37261,8 @@ var SessionShadingTracker = class {
|
|
|
36316
37261
|
this.spec = null;
|
|
36317
37262
|
this.covered = null;
|
|
36318
37263
|
this.session = opts.session;
|
|
37264
|
+
const tfMs = timeframeMs(opts.timeframe);
|
|
37265
|
+
this.intraday = Number.isFinite(tfMs) && tfMs < DAY_MS2;
|
|
36319
37266
|
this.lastRange = opts.range;
|
|
36320
37267
|
void data.symbolInfo(symbol).catch(() => void 0).then((si) => {
|
|
36321
37268
|
if (my !== this.epoch) return;
|
|
@@ -36343,7 +37290,7 @@ var SessionShadingTracker = class {
|
|
|
36343
37290
|
if (force) this.onZones(null);
|
|
36344
37291
|
return;
|
|
36345
37292
|
}
|
|
36346
|
-
if (this.session !== "extended") {
|
|
37293
|
+
if (this.session !== "extended" || !this.intraday) {
|
|
36347
37294
|
if (force) this.onZones({ pre: [], post: [] });
|
|
36348
37295
|
return;
|
|
36349
37296
|
}
|
|
@@ -36444,9 +37391,168 @@ var Watermark = class {
|
|
|
36444
37391
|
}
|
|
36445
37392
|
};
|
|
36446
37393
|
|
|
37394
|
+
// src/widget/cell-controls.ts
|
|
37395
|
+
var CELL_CONTROLS_PROXIMITY_PX = 120;
|
|
37396
|
+
var TIME_AXIS_H2 = 22;
|
|
37397
|
+
var CONTROLS_BOTTOM_PX = TIME_AXIS_H2 + 12;
|
|
37398
|
+
var CLUSTER_H2 = 24;
|
|
37399
|
+
var CLUSTER_PILL2 = "rgba(0,0,0,0.65)";
|
|
37400
|
+
var STYLE_ID26 = "vela-cell-controls";
|
|
37401
|
+
var CSS23 = `
|
|
37402
|
+
.vela-cc-btn{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;padding:0;border:none;border-radius:var(--vela-radius-sm);background:transparent;line-height:0;font-size:12px;color:var(--vela-fg-muted);cursor:pointer;}
|
|
37403
|
+
.vela-cc-btn svg{display:block;}
|
|
37404
|
+
.vela-cc-btn:hover{background:var(--vela-active);color:var(--vela-fg-bright);}
|
|
37405
|
+
.vela-cc-on,.vela-cc-on:hover{background:var(--vela-selected-bg);color:var(--vela-selected-fg);}
|
|
37406
|
+
.vela-cc-grip{cursor:grab;touch-action:none;}
|
|
37407
|
+
.vela-cc-grip:active{cursor:grabbing;}
|
|
37408
|
+
`;
|
|
37409
|
+
function nearBottomCenter(x, y, width, height, proximityPx = CELL_CONTROLS_PROXIMITY_PX) {
|
|
37410
|
+
const cx = width / 2;
|
|
37411
|
+
const cy = height - CONTROLS_BOTTOM_PX - CLUSTER_H2 / 2;
|
|
37412
|
+
return Math.hypot(x - cx, y - cy) <= proximityPx;
|
|
37413
|
+
}
|
|
37414
|
+
var CellControls = class {
|
|
37415
|
+
constructor(host, deps) {
|
|
37416
|
+
this.host = host;
|
|
37417
|
+
this.deps = deps;
|
|
37418
|
+
this.near = false;
|
|
37419
|
+
/** A grip drag is underway — the proximity reveal must not hide the cluster
|
|
37420
|
+
* while captured pointer moves sweep across the whole grid. */
|
|
37421
|
+
this.dragging = false;
|
|
37422
|
+
/** Mobile: the proximity reveal is meaningless without a cursor — the mobile
|
|
37423
|
+
* bar's maximize stop replaces the cluster. */
|
|
37424
|
+
this.suspended = false;
|
|
37425
|
+
this.onHostMove = (e) => {
|
|
37426
|
+
if (this.suspended) return;
|
|
37427
|
+
if (this.dragging) return;
|
|
37428
|
+
const rect = this.host.getBoundingClientRect();
|
|
37429
|
+
this.setNear(nearBottomCenter(e.clientX - rect.left, e.clientY - rect.top, rect.width, rect.height));
|
|
37430
|
+
};
|
|
37431
|
+
this.onHostLeave = () => {
|
|
37432
|
+
if (this.dragging) return;
|
|
37433
|
+
this.setNear(false);
|
|
37434
|
+
};
|
|
37435
|
+
injectStyles(STYLE_ID26, CSS23, host.ownerDocument);
|
|
37436
|
+
this.glider = new Glider(deps.chart);
|
|
37437
|
+
this.root = host.ownerDocument.createElement("div");
|
|
37438
|
+
Object.assign(this.root.style, {
|
|
37439
|
+
position: "absolute",
|
|
37440
|
+
left: "50%",
|
|
37441
|
+
bottom: `${CONTROLS_BOTTOM_PX}px`,
|
|
37442
|
+
transform: "translateX(-50%)",
|
|
37443
|
+
zIndex: "6",
|
|
37444
|
+
display: "none",
|
|
37445
|
+
// revealed by cursor proximity (onHostMove)
|
|
37446
|
+
gap: "2px",
|
|
37447
|
+
padding: "2px",
|
|
37448
|
+
borderRadius: "var(--vela-radius-md)",
|
|
37449
|
+
background: CLUSTER_PILL2,
|
|
37450
|
+
pointerEvents: "auto"
|
|
37451
|
+
});
|
|
37452
|
+
this.host.addEventListener("pointermove", this.onHostMove);
|
|
37453
|
+
this.host.addEventListener("pointerleave", this.onHostLeave);
|
|
37454
|
+
this.host.appendChild(this.root);
|
|
37455
|
+
this.refresh();
|
|
37456
|
+
}
|
|
37457
|
+
/** Rebuild the buttons (the multi-cell gate or the maximized state changed). */
|
|
37458
|
+
refresh() {
|
|
37459
|
+
this.root.textContent = "";
|
|
37460
|
+
const multi = this.deps.multiCell();
|
|
37461
|
+
const maximized = multi && this.deps.isMaximized();
|
|
37462
|
+
if (multi && !maximized) this.root.appendChild(this.makeGrip());
|
|
37463
|
+
this.root.appendChild(this.button("minus", "Zoom out", () => this.glider.zoom(ZOOM_OUT)));
|
|
37464
|
+
this.root.appendChild(this.button("plus", "Zoom in", () => this.glider.zoom(ZOOM_IN)));
|
|
37465
|
+
if (multi) {
|
|
37466
|
+
this.root.appendChild(
|
|
37467
|
+
this.button(maximized ? "restore" : "maximize", maximized ? "Restore layout" : "Maximize chart", () => this.deps.toggleMaximize(), {
|
|
37468
|
+
// The maximized state reads as an inverse chip (white-on-dark, dark-on-light),
|
|
37469
|
+
// the same active-state affordance as a collapsed pane's expand button.
|
|
37470
|
+
selected: maximized
|
|
37471
|
+
})
|
|
37472
|
+
);
|
|
37473
|
+
}
|
|
37474
|
+
this.root.appendChild(
|
|
37475
|
+
this.button("reset", "Reset chart", () => {
|
|
37476
|
+
this.glider.stop();
|
|
37477
|
+
this.deps.reset();
|
|
37478
|
+
})
|
|
37479
|
+
);
|
|
37480
|
+
}
|
|
37481
|
+
button(iconId, title, onClick, opts = {}) {
|
|
37482
|
+
const b = this.host.ownerDocument.createElement("button");
|
|
37483
|
+
b.type = "button";
|
|
37484
|
+
b.title = title;
|
|
37485
|
+
b.setAttribute("aria-label", title);
|
|
37486
|
+
b.className = opts.selected === true ? "vela-cc-btn vela-cc-on" : "vela-cc-btn";
|
|
37487
|
+
b.innerHTML = icon(iconId);
|
|
37488
|
+
b.addEventListener("click", (e) => {
|
|
37489
|
+
e.stopPropagation();
|
|
37490
|
+
onClick();
|
|
37491
|
+
});
|
|
37492
|
+
return b;
|
|
37493
|
+
}
|
|
37494
|
+
/** The drag handle (2×3 dot grip): press and drag onto another cell to trade
|
|
37495
|
+
* slots with it. The preview highlight follows the pointer; releasing outside
|
|
37496
|
+
* any other cell cancels. */
|
|
37497
|
+
makeGrip() {
|
|
37498
|
+
const b = this.host.ownerDocument.createElement("button");
|
|
37499
|
+
b.type = "button";
|
|
37500
|
+
b.title = "Drag to move chart";
|
|
37501
|
+
b.setAttribute("aria-label", "Drag to move chart");
|
|
37502
|
+
b.className = "vela-cc-btn vela-cc-grip";
|
|
37503
|
+
b.innerHTML = icon("grip");
|
|
37504
|
+
b.addEventListener("pointerdown", (e) => this.onGripDown(b, e));
|
|
37505
|
+
return b;
|
|
37506
|
+
}
|
|
37507
|
+
onGripDown(btn2, e) {
|
|
37508
|
+
if (e.button !== 0 && e.pointerType === "mouse") return;
|
|
37509
|
+
e.preventDefault();
|
|
37510
|
+
e.stopPropagation();
|
|
37511
|
+
try {
|
|
37512
|
+
btn2.setPointerCapture(e.pointerId);
|
|
37513
|
+
} catch {
|
|
37514
|
+
}
|
|
37515
|
+
this.dragging = true;
|
|
37516
|
+
let target = null;
|
|
37517
|
+
const move = (ev) => {
|
|
37518
|
+
target = this.deps.dragTargetAt(ev.clientX, ev.clientY);
|
|
37519
|
+
this.deps.previewDrop(target);
|
|
37520
|
+
};
|
|
37521
|
+
const finish = (commit) => () => {
|
|
37522
|
+
this.dragging = false;
|
|
37523
|
+
this.deps.previewDrop(null);
|
|
37524
|
+
btn2.removeEventListener("pointermove", move);
|
|
37525
|
+
btn2.removeEventListener("pointerup", onUp);
|
|
37526
|
+
btn2.removeEventListener("pointercancel", onCancel);
|
|
37527
|
+
if (commit && target != null) this.deps.dropOn(target);
|
|
37528
|
+
};
|
|
37529
|
+
const onUp = finish(true);
|
|
37530
|
+
const onCancel = finish(false);
|
|
37531
|
+
btn2.addEventListener("pointermove", move);
|
|
37532
|
+
btn2.addEventListener("pointerup", onUp);
|
|
37533
|
+
btn2.addEventListener("pointercancel", onCancel);
|
|
37534
|
+
}
|
|
37535
|
+
/** Mobile flips the cluster off entirely (and hides it if currently revealed). */
|
|
37536
|
+
setSuspended(on) {
|
|
37537
|
+
this.suspended = on;
|
|
37538
|
+
if (on) this.setNear(false);
|
|
37539
|
+
}
|
|
37540
|
+
setNear(near) {
|
|
37541
|
+
if (near === this.near) return;
|
|
37542
|
+
this.near = near;
|
|
37543
|
+
this.root.style.display = near ? "flex" : "none";
|
|
37544
|
+
}
|
|
37545
|
+
destroy() {
|
|
37546
|
+
this.glider.stop();
|
|
37547
|
+
this.host.removeEventListener("pointermove", this.onHostMove);
|
|
37548
|
+
this.host.removeEventListener("pointerleave", this.onHostLeave);
|
|
37549
|
+
this.root.remove();
|
|
37550
|
+
}
|
|
37551
|
+
};
|
|
37552
|
+
|
|
36447
37553
|
// src/widget/context-menu.ts
|
|
36448
37554
|
var PRICE_AXIS_W = 60;
|
|
36449
|
-
var
|
|
37555
|
+
var TIME_AXIS_H3 = 26;
|
|
36450
37556
|
var ChartContextMenu = class {
|
|
36451
37557
|
constructor(host, cbs) {
|
|
36452
37558
|
this.cbs = cbs;
|
|
@@ -36485,7 +37591,7 @@ var ChartContextMenu = class {
|
|
|
36485
37591
|
zoneOf(e) {
|
|
36486
37592
|
const rect = this.host.getBoundingClientRect();
|
|
36487
37593
|
if (e.clientX - rect.left > rect.width - PRICE_AXIS_W) return "price-axis";
|
|
36488
|
-
if (e.clientY - rect.top > rect.height -
|
|
37594
|
+
if (e.clientY - rect.top > rect.height - TIME_AXIS_H3) return "time-axis";
|
|
36489
37595
|
return "body";
|
|
36490
37596
|
}
|
|
36491
37597
|
/** The pane under the pointer, so every pane's price scale has its own menu. */
|
|
@@ -36758,6 +37864,7 @@ var ChartCell = class {
|
|
|
36758
37864
|
this.inner.renderer.set("attribution", false);
|
|
36759
37865
|
this.inner.renderer.set("dialogHost", deps.dialogHost);
|
|
36760
37866
|
this.inner.renderer.setLegendActions(legendActionsProviderFor(this.inner, () => deps.context()));
|
|
37867
|
+
this.inner.renderer.setLegendCallouts(legendCalloutsProviderFor(this.inner, () => deps.context()));
|
|
36761
37868
|
if (this.inner.renderer.supports("historyChords")) this.inner.renderer.set("historyChords", false);
|
|
36762
37869
|
this.history.onChart(this.inner);
|
|
36763
37870
|
this.inner.renderer.onConfigChanged(() => {
|
|
@@ -36818,11 +37925,18 @@ var ChartCell = class {
|
|
|
36818
37925
|
if (this.inner && this.state.symbol) this.marketStatus?.track(this.inner.data, this.state.symbol);
|
|
36819
37926
|
});
|
|
36820
37927
|
this.syncStatuslineColors();
|
|
37928
|
+
this.cellControls = new CellControls(this.host, {
|
|
37929
|
+
chart: () => this.inner,
|
|
37930
|
+
reset: () => this.resetView(),
|
|
37931
|
+
multiCell: () => deps.multiCell(),
|
|
37932
|
+
isMaximized: () => deps.isMaximized(id),
|
|
37933
|
+
toggleMaximize: () => deps.toggleMaximize(id),
|
|
37934
|
+
dragTargetAt: (x, y) => deps.cellDragTarget(id, x, y),
|
|
37935
|
+
previewDrop: (target) => deps.previewDropTarget(target),
|
|
37936
|
+
dropOn: (target) => deps.dropCell(id, target)
|
|
37937
|
+
});
|
|
36821
37938
|
this.contextMenu = new ChartContextMenu(this.host, {
|
|
36822
|
-
resetView: () =>
|
|
36823
|
-
this.inner?.renderer.set("autoScale", true);
|
|
36824
|
-
this.inner?.setVisibleRangePreset("ALL");
|
|
36825
|
-
},
|
|
37939
|
+
resetView: () => this.resetView(),
|
|
36826
37940
|
timezone: () => this.deps.timezone(),
|
|
36827
37941
|
setTimezone: (zone) => this.deps.setTimezone(zone),
|
|
36828
37942
|
// Right-clicking activates the cell first (capture-phase pointerdown), so the
|
|
@@ -36874,20 +37988,30 @@ var ChartCell = class {
|
|
|
36874
37988
|
this.refreshNativeCatalog();
|
|
36875
37989
|
this.pushSettingsSections();
|
|
36876
37990
|
this.offMarket = this.inner.on("market:changed", ({ symbol: symbol2, timeframe }) => {
|
|
36877
|
-
this.
|
|
36878
|
-
this.state.provider = parseSymbol(symbol2).provider ?? void 0;
|
|
36879
|
-
this.state.timeframe = timeframe;
|
|
36880
|
-
this.state.session = normalizeSession(this.inner?.market.session);
|
|
36881
|
-
this.watermark?.update(symbol2, timeframe);
|
|
36882
|
-
this.statusline?.setSymbol(symbol2);
|
|
36883
|
-
this.statusline?.setMeta(timeframe, this.inner?.data.displayPrefix(symbol2) ?? this.state.provider ?? "");
|
|
36884
|
-
if (this.inner) this.statusline?.onChart(this.inner);
|
|
37991
|
+
this.projectMarket(symbol2, timeframe);
|
|
36885
37992
|
this.refreshNativeCatalog();
|
|
36886
37993
|
this.refreshSessionAvailable();
|
|
36887
37994
|
if (this.inner) this.marketStatus?.track(this.inner.data, symbol2);
|
|
36888
37995
|
this.deps.onMarketChanged(this.id);
|
|
36889
37996
|
});
|
|
36890
37997
|
}
|
|
37998
|
+
/**
|
|
37999
|
+
* Project a market identity into the cell state and its display overlays (watermark,
|
|
38000
|
+
* statusline), WITHOUT the data-dependent bookkeeping. Runs twice per user pick: once
|
|
38001
|
+
* optimistically from the cell setters — the labels reflect the pick immediately, not
|
|
38002
|
+
* after the bars load — and again from `market:changed` (the committed pass, and the
|
|
38003
|
+
* only pass for host `chart.setMarket` calls). Idempotent, so the double run converges.
|
|
38004
|
+
*/
|
|
38005
|
+
projectMarket(symbol, timeframe) {
|
|
38006
|
+
this.state.symbol = symbol;
|
|
38007
|
+
this.state.provider = parseSymbol(symbol).provider ?? void 0;
|
|
38008
|
+
this.state.timeframe = timeframe;
|
|
38009
|
+
this.state.session = normalizeSession(this.inner?.market.session);
|
|
38010
|
+
this.watermark?.update(symbol, timeframe);
|
|
38011
|
+
this.statusline?.setSymbol(symbol);
|
|
38012
|
+
this.statusline?.setMeta(timeframe, this.inner?.data.displayPrefix(symbol) ?? this.state.provider ?? "");
|
|
38013
|
+
if (this.inner) this.statusline?.onChart(this.inner);
|
|
38014
|
+
}
|
|
36891
38015
|
/**
|
|
36892
38016
|
* Does this cell's market HAVE sessions (RTH/ETH meaningful)? Derived from the
|
|
36893
38017
|
* symbol's own metadata (`syminfo.session !== '24x7'`), asynchronously — the
|
|
@@ -36933,7 +38057,7 @@ var ChartCell = class {
|
|
|
36933
38057
|
const requestedSpan = Math.max(this.state.bars ?? 1e3, this.rangeBars) * timeframeMs(this.state.timeframe ?? "60");
|
|
36934
38058
|
const fallbackSpan = Number.isFinite(requestedSpan) ? Math.max(3 * 864e5, requestedSpan) : 3 * 864e5;
|
|
36935
38059
|
const range = chart.getVisibleRange() ?? { from: now - fallbackSpan, to: now };
|
|
36936
|
-
this.sessionShading.track(chart.data, symbol, { session: this.session, range });
|
|
38060
|
+
this.sessionShading.track(chart.data, symbol, { session: this.session, timeframe: this.timeframe, range });
|
|
36937
38061
|
}
|
|
36938
38062
|
/** The session-shade colors live in the renderer CONFIG (persisted with it, edited
|
|
36939
38063
|
* live by the dialog swatch) — the cell only proxies them into its settings rows. */
|
|
@@ -37122,17 +38246,23 @@ var ChartCell = class {
|
|
|
37122
38246
|
get indicatorCount() {
|
|
37123
38247
|
return this.instances.length + this.nativeCatalog.filter((n) => n.present).length;
|
|
37124
38248
|
}
|
|
37125
|
-
/** Switch this cell's market in place (the chart instance survives).
|
|
38249
|
+
/** Switch this cell's market in place (the chart instance survives). The projection
|
|
38250
|
+
* is OPTIMISTIC — labels and chrome show the pick before the bars load; it follows
|
|
38251
|
+
* the setMarket call so the statusline reads the already-blanked chart. */
|
|
37126
38252
|
setSymbol(symbol) {
|
|
37127
38253
|
if (!this.inner || symbol === this.symbol) return;
|
|
37128
38254
|
this.unresolvedToasted = null;
|
|
37129
38255
|
void this.inner.setMarket({ symbol });
|
|
38256
|
+
this.projectMarket(symbol, this.timeframe);
|
|
38257
|
+
this.deps.onMarketChanged(this.id);
|
|
37130
38258
|
}
|
|
37131
38259
|
setTimeframe(timeframe) {
|
|
37132
38260
|
if (!this.inner || timeframe === this.timeframe) return;
|
|
37133
38261
|
this.activeRangeId = null;
|
|
37134
38262
|
this.rangeBars = 0;
|
|
37135
38263
|
void this.inner.setMarket({ timeframe, bars: this.state.bars });
|
|
38264
|
+
this.projectMarket(this.symbol, timeframe);
|
|
38265
|
+
this.deps.onMarketChanged(this.id);
|
|
37136
38266
|
}
|
|
37137
38267
|
/** Applied live (renderer feature) — no reload. */
|
|
37138
38268
|
setPriceStyle(style) {
|
|
@@ -37180,6 +38310,20 @@ var ChartCell = class {
|
|
|
37180
38310
|
this.inner.setVisibleRangePreset(preset.preset);
|
|
37181
38311
|
}
|
|
37182
38312
|
}
|
|
38313
|
+
/** Reset this cell's view: re-enable auto scale and frame the full history —
|
|
38314
|
+
* the same action the chart context menu offers. */
|
|
38315
|
+
resetView() {
|
|
38316
|
+
this.inner?.renderer.set("autoScale", true);
|
|
38317
|
+
this.inner?.setVisibleRangePreset("ALL");
|
|
38318
|
+
}
|
|
38319
|
+
/** Rebuild the view-controls cluster (the maximize gate or state changed). */
|
|
38320
|
+
refreshControls() {
|
|
38321
|
+
this.cellControls.refresh();
|
|
38322
|
+
}
|
|
38323
|
+
/** Mobile flips the per-cell cluster off (the shell's mobile bar replaces it). */
|
|
38324
|
+
setControlsSuspended(on) {
|
|
38325
|
+
this.cellControls.setSuspended(on);
|
|
38326
|
+
}
|
|
37183
38327
|
/** Make this cell the active one and put keyboard focus on its chart surface. */
|
|
37184
38328
|
focus() {
|
|
37185
38329
|
this.deps.activate(this.id);
|
|
@@ -37493,6 +38637,7 @@ var ChartCell = class {
|
|
|
37493
38637
|
destroy() {
|
|
37494
38638
|
this.destroyed = true;
|
|
37495
38639
|
this.offMarket();
|
|
38640
|
+
this.cellControls.destroy();
|
|
37496
38641
|
this.contextMenu.destroy();
|
|
37497
38642
|
this.history.destroy();
|
|
37498
38643
|
this.marketStatus?.stop();
|
|
@@ -37785,10 +38930,10 @@ var SplitterLayer = class {
|
|
|
37785
38930
|
var DEFAULT_TIMEFRAMES = ["1", "5", "15", "60", "240", "D", "W"];
|
|
37786
38931
|
var GAP_PX = 2;
|
|
37787
38932
|
var POOL_CAP = 16;
|
|
37788
|
-
var
|
|
38933
|
+
var TIME_AXIS_H4 = 22;
|
|
37789
38934
|
var ALERT_CAP = 50;
|
|
37790
|
-
var
|
|
37791
|
-
var
|
|
38935
|
+
var STYLE_ID27 = "vela-workspace";
|
|
38936
|
+
var CSS24 = `
|
|
37792
38937
|
.vela-workspace { position: relative; width: 100%; height: 100%; display: flex; flex-direction: column; background: var(--vela-bg); }
|
|
37793
38938
|
.vela-ws-main { position: relative; display: flex; flex-direction: row; flex: 1 1 auto; min-height: 0; }
|
|
37794
38939
|
.vela-ws-toolbar { position: relative; flex: none; }
|
|
@@ -37815,6 +38960,21 @@ var CSS23 = `
|
|
|
37815
38960
|
/* Mobile: the docked drawing-toolbar column would eat a phone-width grid \u2014 the shell's
|
|
37816
38961
|
drawings drawer + on-chart pill replace it (same policy as the widget's in-chart bar). */
|
|
37817
38962
|
[data-layout='mobile'] .vela-ws-toolbar { display: none; }
|
|
38963
|
+
/* A maximized cell owns the whole grid: the splitter strips have no seams to grab and
|
|
38964
|
+
the active ring would just outline the only visible chart \u2014 both are noise here. */
|
|
38965
|
+
.vela-ws-grid[data-maximized='1'] .vela-ws-splitter { display: none; }
|
|
38966
|
+
.vela-ws-grid[data-maximized='1'] .vela-cell[data-active='1']::after { display: none; }
|
|
38967
|
+
/* Drop-target preview while a cell's drag handle is held: a dashed ring + the same
|
|
38968
|
+
soft wash the splitter hover uses, over the chart, inert to the pointer. */
|
|
38969
|
+
.vela-cell[data-drop-target='1']::before {
|
|
38970
|
+
content: '';
|
|
38971
|
+
position: absolute;
|
|
38972
|
+
inset: 0;
|
|
38973
|
+
border: 2px dashed var(--vela-fg-bright);
|
|
38974
|
+
background: var(--vela-separator-hover-band);
|
|
38975
|
+
pointer-events: none;
|
|
38976
|
+
z-index: 11;
|
|
38977
|
+
}
|
|
37818
38978
|
`;
|
|
37819
38979
|
registerIcon("layout", svg16('<rect x="1.5" y="1.5" width="13" height="13" rx="1.5"/><path d="M8 1.5v13M1.5 8h13"/>'));
|
|
37820
38980
|
function declaredOrder(cells) {
|
|
@@ -37844,6 +39004,9 @@ var VelaWorkspace = class {
|
|
|
37844
39004
|
* slots beyond the list get auto identities. Grows, never reorders. */
|
|
37845
39005
|
this.order = [];
|
|
37846
39006
|
this.activeId = null;
|
|
39007
|
+
/** The cell maximized over the whole grid (null = normal grid). TRANSIENT view
|
|
39008
|
+
* state — never persisted; any structural change (layout, applyState) restores. */
|
|
39009
|
+
this.maximizedId = null;
|
|
37847
39010
|
this.cellBackend = "auto";
|
|
37848
39011
|
this.destroyed = false;
|
|
37849
39012
|
this.shortcutsHelp = null;
|
|
@@ -37948,7 +39111,7 @@ var VelaWorkspace = class {
|
|
|
37948
39111
|
this.order = boot?.charts ? boot.charts.map((c) => c.id) : declaredOrder(opts.cells);
|
|
37949
39112
|
const bootActive = boot?.activeCellId ?? null;
|
|
37950
39113
|
const doc = hostEl.ownerDocument;
|
|
37951
|
-
injectStyles(
|
|
39114
|
+
injectStyles(STYLE_ID27, CSS24, doc);
|
|
37952
39115
|
this.root = doc.createElement("div");
|
|
37953
39116
|
this.root.className = "vela-workspace";
|
|
37954
39117
|
ensureUIHost(this.root, resolveTheme(opts.theme));
|
|
@@ -38058,7 +39221,11 @@ var VelaWorkspace = class {
|
|
|
38058
39221
|
if (attribution !== false) {
|
|
38059
39222
|
const background = resolveTheme(opts.theme).background;
|
|
38060
39223
|
const mark = typeof attribution === "string" && attribution.trim() ? createCustomMark(doc, attribution, background) : createAttributionMark(doc, background);
|
|
38061
|
-
Object.assign(mark.style, {
|
|
39224
|
+
Object.assign(mark.style, {
|
|
39225
|
+
left: "calc(var(--vela-toolbar-gutter, 0px) + 12px)",
|
|
39226
|
+
bottom: `calc(var(--vela-bottom-gutter, ${TIME_AXIS_H4}px) + 10px)`,
|
|
39227
|
+
zIndex: "11"
|
|
39228
|
+
});
|
|
38062
39229
|
this.gridEl.appendChild(mark);
|
|
38063
39230
|
this.attributionMark = mark;
|
|
38064
39231
|
}
|
|
@@ -38124,6 +39291,9 @@ var VelaWorkspace = class {
|
|
|
38124
39291
|
...topbarHas(this.topbarComp, "indicators") && (picker || this.indicatorsOverride) ? { onIndicatorsClick: this.indicatorsOverride ? () => this.runOverride(this.indicatorsOverride) : () => picker.open() } : {},
|
|
38125
39292
|
getContext: () => this.context(),
|
|
38126
39293
|
...this.drawingsEnabled ? { onDrawingsClick: () => this.openDrawingsDrawer() } : {},
|
|
39294
|
+
// Multi-chart only: the stop that isolates the ACTIVE chart (the
|
|
39295
|
+
// per-cell hover cluster has no cursor to reveal it on mobile).
|
|
39296
|
+
...this.monoLayout ? {} : { onMaximizeClick: () => this.toggleMobileMaximize() },
|
|
38127
39297
|
onMoreClick: () => this.openMoreDrawer(),
|
|
38128
39298
|
onSettingsClick: () => this.active.chart.renderer.openSettings()
|
|
38129
39299
|
}) : null;
|
|
@@ -38238,7 +39408,10 @@ var VelaWorkspace = class {
|
|
|
38238
39408
|
this.topbar.renderActions();
|
|
38239
39409
|
this.mobileBar?.renderActions();
|
|
38240
39410
|
this.dock.refresh();
|
|
38241
|
-
for (const cell of this.cells())
|
|
39411
|
+
for (const cell of this.cells()) {
|
|
39412
|
+
cell.chart.renderer.setLegendActions(legendActionsProviderFor(cell.chart, () => this.context()));
|
|
39413
|
+
cell.chart.renderer.setLegendCallouts(legendCalloutsProviderFor(cell.chart, () => this.context()));
|
|
39414
|
+
}
|
|
38242
39415
|
}
|
|
38243
39416
|
// ── state surface (the SDK's read/restore of the whole grid's config + content) ──
|
|
38244
39417
|
/**
|
|
@@ -38327,7 +39500,9 @@ var VelaWorkspace = class {
|
|
|
38327
39500
|
this.pool.clear();
|
|
38328
39501
|
for (const { id, ...cs } of st.charts.slice(liveCount)) this.pool.set(id, cs);
|
|
38329
39502
|
this.order = st.charts.map((c) => c.id);
|
|
39503
|
+
this.clearMaximized();
|
|
38330
39504
|
this.applyGrid();
|
|
39505
|
+
this.refreshCellControls();
|
|
38331
39506
|
const nextActive2 = st.activeCellId && this.cellsById.has(st.activeCellId) ? st.activeCellId : this.order[0] ?? null;
|
|
38332
39507
|
if (nextActive2 === this.activeId) this.projectActiveCell();
|
|
38333
39508
|
else this.setActiveCell(nextActive2);
|
|
@@ -38353,6 +39528,7 @@ var VelaWorkspace = class {
|
|
|
38353
39528
|
const def = this.monoLayout ? null : ensureLayout(st.layout);
|
|
38354
39529
|
if (def) this.def = def;
|
|
38355
39530
|
this.cellBackend = this.backendFor(this.def);
|
|
39531
|
+
this.clearMaximized();
|
|
38356
39532
|
this.applyGrid();
|
|
38357
39533
|
this.buildCells();
|
|
38358
39534
|
this.syncCellPresentation();
|
|
@@ -38416,6 +39592,7 @@ var VelaWorkspace = class {
|
|
|
38416
39592
|
setLayout(layout) {
|
|
38417
39593
|
if (this.destroyed) return;
|
|
38418
39594
|
if (this.monoLayout) return;
|
|
39595
|
+
this.clearMaximized();
|
|
38419
39596
|
const next = this.resolveLayout(layout);
|
|
38420
39597
|
const nextBackend = this.backendFor(next);
|
|
38421
39598
|
const rebuildAll = nextBackend !== this.cellBackend;
|
|
@@ -38436,6 +39613,7 @@ var VelaWorkspace = class {
|
|
|
38436
39613
|
this.buildCells();
|
|
38437
39614
|
this.alignNewCellStyles(preexisting);
|
|
38438
39615
|
this.syncCellPresentation();
|
|
39616
|
+
this.refreshCellControls();
|
|
38439
39617
|
this.topbar.setLayout(next.id);
|
|
38440
39618
|
const nextActive = activeAfterLayout(this.activeId, this.order.slice(0, next.cells.length));
|
|
38441
39619
|
if (nextActive === this.activeId) this.projectActiveCell();
|
|
@@ -38444,6 +39622,67 @@ var VelaWorkspace = class {
|
|
|
38444
39622
|
this.events.emit("layout:changed", { layout: next.id });
|
|
38445
39623
|
this.markStateDirty();
|
|
38446
39624
|
}
|
|
39625
|
+
/** The identity of the cell maximized over the whole grid, or null. */
|
|
39626
|
+
get maximizedCell() {
|
|
39627
|
+
return this.maximizedId;
|
|
39628
|
+
}
|
|
39629
|
+
/**
|
|
39630
|
+
* Maximize one cell over the whole grid, or restore the layout with `null`. Pure
|
|
39631
|
+
* presentation: the other cells stay alive underneath — charts, subscriptions and
|
|
39632
|
+
* state untouched — so restoring is instant. The maximized cell becomes the active
|
|
39633
|
+
* one. Transient view state (also reachable from each cell's bottom-center view
|
|
39634
|
+
* cluster): switching layouts or applying a state document restores the grid.
|
|
39635
|
+
*/
|
|
39636
|
+
maximizeCell(id) {
|
|
39637
|
+
if (this.destroyed) return;
|
|
39638
|
+
if (id != null && (!this.cellsById.has(id) || this.def.cells.length <= 1)) return;
|
|
39639
|
+
if (id === this.maximizedId) return;
|
|
39640
|
+
this.maximizedId = id;
|
|
39641
|
+
if (id) this.setActiveCell(id);
|
|
39642
|
+
this.applyGrid();
|
|
39643
|
+
this.refreshCellControls();
|
|
39644
|
+
this.syncMobileMaximize();
|
|
39645
|
+
this.events.emit("cell:maximized", { id });
|
|
39646
|
+
}
|
|
39647
|
+
/** The mobile bar's maximize stop: one press isolates the ACTIVE chart over the
|
|
39648
|
+
* grid; while something is already isolated — the chart, or a pane inside it
|
|
39649
|
+
* (mobile's double-tap) — the press restores that instead. Every branch re-syncs
|
|
39650
|
+
* the stop on its own (`maximizeCell` directly, `panes.maximize` via its
|
|
39651
|
+
* synchronous `pane:changed`). */
|
|
39652
|
+
toggleMobileMaximize() {
|
|
39653
|
+
const cell = this.activeId ? this.cellsById.get(this.activeId) : void 0;
|
|
39654
|
+
if (!cell) return;
|
|
39655
|
+
if (this.maximizedId) this.maximizeCell(null);
|
|
39656
|
+
else if (cell.chart.panes.list().some((p) => p.maximized)) cell.chart.panes.maximize(null);
|
|
39657
|
+
else this.maximizeCell(cell.id);
|
|
39658
|
+
}
|
|
39659
|
+
/** Keep the mobile bar's maximize stop truthful: lit (inverse chip, restore
|
|
39660
|
+
* glyph) while the active chart covers the grid OR one of its panes is
|
|
39661
|
+
* maximized — the state a double-tap toggles is otherwise invisible on mobile. */
|
|
39662
|
+
syncMobileMaximize() {
|
|
39663
|
+
if (!this.mobileBar) return;
|
|
39664
|
+
const cell = this.activeId ? this.cellsById.get(this.activeId) : void 0;
|
|
39665
|
+
const paneMax = cell ? cell.chart.panes.list().some((p) => p.maximized) : false;
|
|
39666
|
+
this.mobileBar.setMaximizeActive(this.maximizedId != null || paneMax);
|
|
39667
|
+
}
|
|
39668
|
+
/**
|
|
39669
|
+
* Trade the SLOTS of two live cells — the grid arrangement changes, the cells
|
|
39670
|
+
* themselves (charts, indicators, drawings, the active flag) stay untouched.
|
|
39671
|
+
* What each cell's drag handle commits; also callable directly by hosts.
|
|
39672
|
+
*/
|
|
39673
|
+
swapCells(a, b) {
|
|
39674
|
+
if (this.destroyed || a === b) return;
|
|
39675
|
+
const i = this.order.indexOf(a);
|
|
39676
|
+
const j = this.order.indexOf(b);
|
|
39677
|
+
if (i < 0 || j < 0 || !this.cellsById.has(a) || !this.cellsById.has(b)) return;
|
|
39678
|
+
[this.order[i], this.order[j]] = [this.order[j], this.order[i]];
|
|
39679
|
+
for (const [k] of this.def.cells.entries()) {
|
|
39680
|
+
const host = this.cellsById.get(this.order[k] ?? "")?.host;
|
|
39681
|
+
if (host) this.gridEl.appendChild(host);
|
|
39682
|
+
}
|
|
39683
|
+
this.applyGrid();
|
|
39684
|
+
this.markStateDirty();
|
|
39685
|
+
}
|
|
38447
39686
|
resize() {
|
|
38448
39687
|
this.splitters.layout();
|
|
38449
39688
|
}
|
|
@@ -38513,6 +39752,7 @@ var VelaWorkspace = class {
|
|
|
38513
39752
|
this.mobileBar?.renderActions();
|
|
38514
39753
|
this.mobileBar?.setSymbol(cell.symbol);
|
|
38515
39754
|
this.mobileBar?.setTimeframe(cell.timeframe);
|
|
39755
|
+
this.syncMobileMaximize();
|
|
38516
39756
|
this.drawingPill?.onChart(cell.chart);
|
|
38517
39757
|
const pushHistory = () => this.topbar.setHistoryState(cell.history.canUndo, cell.history.canRedo);
|
|
38518
39758
|
this.historyUnsub?.();
|
|
@@ -38602,8 +39842,78 @@ var VelaWorkspace = class {
|
|
|
38602
39842
|
const host = this.cellsById.get(this.order[i] ?? "")?.host;
|
|
38603
39843
|
if (host) host.style.gridArea = perCell[slot.id]?.gridArea ?? "";
|
|
38604
39844
|
}
|
|
39845
|
+
this.applyMaximizePresentation();
|
|
39846
|
+
this.mountAttributionMark();
|
|
38605
39847
|
this.splitters.layout();
|
|
38606
39848
|
}
|
|
39849
|
+
/** Overlay the maximize presentation on the freshly applied grid: EVERY cell spans
|
|
39850
|
+
* the full track grid — the maximized one on top, the siblings invisible beneath
|
|
39851
|
+
* it (their charts stay alive — restoring is instant). The siblings must span too:
|
|
39852
|
+
* left in their slots they would auto-flow into implicit zero-height rows, whose
|
|
39853
|
+
* gaps steal height from the maximized cell and collapse their renderers to 0.
|
|
39854
|
+
* The splitter strips and the active ring hide via the `data-maximized` rules. */
|
|
39855
|
+
applyMaximizePresentation() {
|
|
39856
|
+
const maxId = this.maximizedId;
|
|
39857
|
+
if (maxId) this.gridEl.dataset.maximized = "1";
|
|
39858
|
+
else delete this.gridEl.dataset.maximized;
|
|
39859
|
+
for (const [id, cell] of this.cellsById) {
|
|
39860
|
+
const style = cell.host.style;
|
|
39861
|
+
if (maxId) style.gridArea = "1 / 1 / -1 / -1";
|
|
39862
|
+
style.zIndex = maxId && id === maxId ? "5" : "";
|
|
39863
|
+
style.visibility = maxId && id !== maxId ? "hidden" : "";
|
|
39864
|
+
}
|
|
39865
|
+
}
|
|
39866
|
+
/** Rebuild every cell's view cluster (the maximize gate or state changed). */
|
|
39867
|
+
refreshCellControls() {
|
|
39868
|
+
for (const cell of this.cellsById.values()) cell.refreshControls();
|
|
39869
|
+
}
|
|
39870
|
+
/** Drop the transient maximize on a structural change (layout switch, state
|
|
39871
|
+
* document) — WITH the event, so hosts tracking `cell:maximized` never drift
|
|
39872
|
+
* from `maximizedCell`. The caller's own grid re-apply paints the restore. */
|
|
39873
|
+
clearMaximized() {
|
|
39874
|
+
if (this.maximizedId == null) return;
|
|
39875
|
+
this.maximizedId = null;
|
|
39876
|
+
this.events.emit("cell:maximized", { id: null });
|
|
39877
|
+
}
|
|
39878
|
+
/** The live cell under a viewport point, excluding `excludeId` and any host a
|
|
39879
|
+
* maximize has hidden — the drag handle's hit-test. */
|
|
39880
|
+
cellAtPoint(x, y, excludeId) {
|
|
39881
|
+
for (const [id, cell] of this.cellsById) {
|
|
39882
|
+
if (id === excludeId || cell.host.style.visibility === "hidden") continue;
|
|
39883
|
+
const r = cell.host.getBoundingClientRect();
|
|
39884
|
+
if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return id;
|
|
39885
|
+
}
|
|
39886
|
+
return null;
|
|
39887
|
+
}
|
|
39888
|
+
/** Mark one cell as the live drop target of a grip drag (null clears all) —
|
|
39889
|
+
* the `data-drop-target` stylesheet rule paints the dashed preview ring. */
|
|
39890
|
+
setDropTarget(id) {
|
|
39891
|
+
for (const [cid, cell] of this.cellsById) {
|
|
39892
|
+
if (cid === id) cell.host.dataset.dropTarget = "1";
|
|
39893
|
+
else delete cell.host.dataset.dropTarget;
|
|
39894
|
+
}
|
|
39895
|
+
}
|
|
39896
|
+
/** The cell whose bottom-left corner the grid's attribution mark floats in — the
|
|
39897
|
+
* maximized cell while one covers the grid, else the bottom-left slot's cell. */
|
|
39898
|
+
bottomLeftCell() {
|
|
39899
|
+
if (this.maximizedId) return this.cellsById.get(this.maximizedId);
|
|
39900
|
+
const grid = occupancyGrid(this.def);
|
|
39901
|
+
const slot = grid[grid.length - 1]?.[0];
|
|
39902
|
+
const idx = this.def.cells.findIndex((c) => (c.area ?? c.id) === slot);
|
|
39903
|
+
return this.cellsById.get(this.order[idx >= 0 ? idx : 0] ?? "");
|
|
39904
|
+
}
|
|
39905
|
+
/** Keep the shared attribution mark inside the BOTTOM-LEFT visible cell: its
|
|
39906
|
+
* offsets ride that cell's renderer-published `--vela-bottom-gutter` /
|
|
39907
|
+
* `--vela-toolbar-gutter`, so collapsed pane strips push the mark up without any
|
|
39908
|
+
* bookkeeping here. Re-run after anything that changes which host that is
|
|
39909
|
+
* (layout switch, maximize, cell rebuild); a destroyed host drops the mark from
|
|
39910
|
+
* the DOM, and this re-mount brings it back. */
|
|
39911
|
+
mountAttributionMark() {
|
|
39912
|
+
const mark = this.attributionMark;
|
|
39913
|
+
if (!mark) return;
|
|
39914
|
+
const host = this.bottomLeftCell()?.host ?? this.gridEl;
|
|
39915
|
+
if (mark.parentElement !== host) host.appendChild(mark);
|
|
39916
|
+
}
|
|
38607
39917
|
/** Create the cells the current layout wants but don't exist yet (pool-first).
|
|
38608
39918
|
* A slot's CELL IDENTITY is `order[i]` (declaration order — never the slot's own
|
|
38609
39919
|
* positional id); slots past the declared list mint an auto identity once. */
|
|
@@ -38635,6 +39945,12 @@ var VelaWorkspace = class {
|
|
|
38635
39945
|
setTimezone: (zone) => this.setTimezone(zone),
|
|
38636
39946
|
context: () => this.context(),
|
|
38637
39947
|
activate: (id2) => this.setActiveCell(id2),
|
|
39948
|
+
multiCell: () => !this.monoLayout && this.def.cells.length > 1,
|
|
39949
|
+
isMaximized: (id2) => this.maximizedId === id2,
|
|
39950
|
+
toggleMaximize: (id2) => this.maximizeCell(this.maximizedId === id2 ? null : id2),
|
|
39951
|
+
cellDragTarget: (id2, x, y) => this.cellAtPoint(x, y, id2),
|
|
39952
|
+
previewDropTarget: (target) => this.setDropTarget(target),
|
|
39953
|
+
dropCell: (id2, target) => this.swapCells(id2, target),
|
|
38638
39954
|
onMarketChanged: (id2) => this.onCellMarketChanged(id2),
|
|
38639
39955
|
onPriceStyleChanged: (id2) => this.onCellPriceStyleChanged(id2),
|
|
38640
39956
|
onIndicatorsChanged: (id2) => this.onCellIndicatorsChanged(id2),
|
|
@@ -38648,6 +39964,7 @@ var VelaWorkspace = class {
|
|
|
38648
39964
|
if (id === this.activeId) cell.host.dataset.active = "1";
|
|
38649
39965
|
this.wireCell(cell);
|
|
38650
39966
|
cell.chart.renderer.setLayoutMode(this.layoutCtl.current);
|
|
39967
|
+
cell.setControlsSuspended(this.layoutCtl.current === "mobile");
|
|
38651
39968
|
if (this.favs.length > 0) cell.chart.drawings.setFavorites(this.favs);
|
|
38652
39969
|
cell.setManifest(this.manifest, pooled?.indicators == null);
|
|
38653
39970
|
cell.restorePersistedExt();
|
|
@@ -38657,6 +39974,7 @@ var VelaWorkspace = class {
|
|
|
38657
39974
|
const host = this.cellsById.get(this.order[i] ?? "")?.host;
|
|
38658
39975
|
if (host) this.gridEl.appendChild(host);
|
|
38659
39976
|
}
|
|
39977
|
+
this.mountAttributionMark();
|
|
38660
39978
|
}
|
|
38661
39979
|
/** Per-cell chart subscriptions (trigger ② — the chart instance is stable for the
|
|
38662
39980
|
* cell's whole life, so these live and die with the cell). */
|
|
@@ -38716,6 +40034,9 @@ var VelaWorkspace = class {
|
|
|
38716
40034
|
chart.on("viewport:changed", (range) => this.propagateViewport(cell.id, range));
|
|
38717
40035
|
chart.renderer.onConfigChanged(() => this.propagateStylePrefs(cell.id));
|
|
38718
40036
|
chart.on("theme:changed", (t) => this.setTheme(t));
|
|
40037
|
+
chart.on("pane:changed", () => {
|
|
40038
|
+
if (cell.id === this.activeId) this.syncMobileMaximize();
|
|
40039
|
+
});
|
|
38719
40040
|
chart.renderer.onAxisLongPress((e) => {
|
|
38720
40041
|
if (this.layoutCtl.current !== "mobile") return;
|
|
38721
40042
|
if (e.axis === "time") this.openTimezoneDrawer();
|
|
@@ -39036,6 +40357,7 @@ var VelaWorkspace = class {
|
|
|
39036
40357
|
for (const cell of this.cellsById.values()) {
|
|
39037
40358
|
cell.chart.renderer.closeDialogs();
|
|
39038
40359
|
cell.chart.renderer.setLayoutMode(mode);
|
|
40360
|
+
cell.setControlsSuspended(mode === "mobile");
|
|
39039
40361
|
}
|
|
39040
40362
|
this.syncCellPresentation();
|
|
39041
40363
|
}
|