@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/index.cjs
CHANGED
|
@@ -138,6 +138,7 @@ var IndicatorHandleImpl = class {
|
|
|
138
138
|
constructor(id, title, controller, source) {
|
|
139
139
|
this.controller = controller;
|
|
140
140
|
this.schema = [];
|
|
141
|
+
this.propsSchema = [];
|
|
141
142
|
this.visibleState = true;
|
|
142
143
|
this.bus = new TypedEventBus();
|
|
143
144
|
this.id = id;
|
|
@@ -147,6 +148,9 @@ var IndicatorHandleImpl = class {
|
|
|
147
148
|
get inputs() {
|
|
148
149
|
return this.schema;
|
|
149
150
|
}
|
|
151
|
+
get props() {
|
|
152
|
+
return this.propsSchema;
|
|
153
|
+
}
|
|
150
154
|
get visible() {
|
|
151
155
|
return this.visibleState;
|
|
152
156
|
}
|
|
@@ -156,6 +160,12 @@ var IndicatorHandleImpl = class {
|
|
|
156
160
|
setInputs(values) {
|
|
157
161
|
this.controller.applyInputs(this.id, values);
|
|
158
162
|
}
|
|
163
|
+
setProp(key, value) {
|
|
164
|
+
this.controller.applyProps(this.id, { [key]: value });
|
|
165
|
+
}
|
|
166
|
+
setProps(values) {
|
|
167
|
+
this.controller.applyProps(this.id, values);
|
|
168
|
+
}
|
|
159
169
|
setVisible(visible) {
|
|
160
170
|
this.controller.setVisible(this.id, visible);
|
|
161
171
|
}
|
|
@@ -175,6 +185,9 @@ var IndicatorHandleImpl = class {
|
|
|
175
185
|
setSchema(schema) {
|
|
176
186
|
this.schema = schema;
|
|
177
187
|
}
|
|
188
|
+
setPropsSchema(schema) {
|
|
189
|
+
this.propsSchema = schema;
|
|
190
|
+
}
|
|
178
191
|
/** Sync the public `visible` getter when the orchestrator changes visibility (API or legend eye). */
|
|
179
192
|
setVisibleState(visible) {
|
|
180
193
|
this.visibleState = visible;
|
|
@@ -188,6 +201,8 @@ var IndicatorHandleImpl = class {
|
|
|
188
201
|
function summarizeModel(model) {
|
|
189
202
|
const series = {};
|
|
190
203
|
for (const s of model.series) series[s.kind] = (series[s.kind] ?? 0) + 1;
|
|
204
|
+
const countOverlay = (items) => items?.filter((i) => i.overlay === true).length ?? 0;
|
|
205
|
+
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);
|
|
191
206
|
return {
|
|
192
207
|
id: model.id,
|
|
193
208
|
title: model.title,
|
|
@@ -207,7 +222,8 @@ function summarizeModel(model) {
|
|
|
207
222
|
tables: model.tables?.length ?? 0,
|
|
208
223
|
barColors: model.barColors?.length ?? 0,
|
|
209
224
|
trades: model.trades?.length ?? 0,
|
|
210
|
-
inputs: model.inputs.length
|
|
225
|
+
inputs: model.inputs.length,
|
|
226
|
+
forcedOverlay
|
|
211
227
|
};
|
|
212
228
|
}
|
|
213
229
|
function inspectModels(models) {
|
|
@@ -228,7 +244,8 @@ function inspectModels(models) {
|
|
|
228
244
|
linefills: sum((s) => s.linefills),
|
|
229
245
|
tables: sum((s) => s.tables),
|
|
230
246
|
barColors: sum((s) => s.barColors),
|
|
231
|
-
trades: sum((s) => s.trades)
|
|
247
|
+
trades: sum((s) => s.trades),
|
|
248
|
+
forcedOverlay: sum((s) => s.forcedOverlay)
|
|
232
249
|
}
|
|
233
250
|
};
|
|
234
251
|
}
|
|
@@ -498,11 +515,11 @@ registerIcon(
|
|
|
498
515
|
);
|
|
499
516
|
registerIcon(
|
|
500
517
|
"market-pre",
|
|
501
|
-
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="
|
|
518
|
+
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"/>')
|
|
502
519
|
);
|
|
503
520
|
registerIcon(
|
|
504
521
|
"market-post",
|
|
505
|
-
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
|
|
522
|
+
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"/>')
|
|
506
523
|
);
|
|
507
524
|
registerIcon("market-closed", svg24('<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/>'));
|
|
508
525
|
registerIcon(
|
|
@@ -535,6 +552,8 @@ registerIcon("folder-plus", S('<path d="M1.6 4.4a1 1 0 0 1 1-1h2.7l1.3 1.7h6.8a1
|
|
|
535
552
|
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"/>'));
|
|
536
553
|
registerIcon("collapse", S('<path d="M3 8h10"/>'));
|
|
537
554
|
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"/>'));
|
|
555
|
+
registerIcon("plus", S('<path d="M8 2.8v10.4M2.8 8h10.4"/>'));
|
|
556
|
+
registerIcon("minus", S('<path d="M2.8 8h10.4"/>'));
|
|
538
557
|
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"/>'));
|
|
539
558
|
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"/>'));
|
|
540
559
|
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"/>'));
|
|
@@ -542,7 +561,7 @@ registerIcon("star-filled", S('<path d="M8 2.2l1.75 3.55 3.9.55-2.8 2.75.65 3.9L
|
|
|
542
561
|
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"'));
|
|
543
562
|
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"'));
|
|
544
563
|
registerIcon("burger", S('<path d="M2.5 4.5h11M2.5 8h11M2.5 11.5h11"/>'));
|
|
545
|
-
registerIcon("reset", S('<
|
|
564
|
+
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"/>'));
|
|
546
565
|
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"/>'));
|
|
547
566
|
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"/>'));
|
|
548
567
|
registerIcon("cursor", svg24('<path d="M5 3l6 16 2-6 6-2z"/>', 'fill="currentColor" stroke="none"'));
|
|
@@ -6913,6 +6932,23 @@ function timeframeToMs(timeframe) {
|
|
|
6913
6932
|
return 36e5;
|
|
6914
6933
|
}
|
|
6915
6934
|
|
|
6935
|
+
// src/data/symbol-groups.ts
|
|
6936
|
+
function isGroupRow(d) {
|
|
6937
|
+
return d.group != null && d.ticker === d.group;
|
|
6938
|
+
}
|
|
6939
|
+
function groupKeyOf(d) {
|
|
6940
|
+
return `${(d.prefix ?? d.provider ?? "").toLowerCase()}:${(d.group ?? "").toUpperCase()}`;
|
|
6941
|
+
}
|
|
6942
|
+
function groupMembers(pool, groupRow) {
|
|
6943
|
+
const key = groupKeyOf(groupRow);
|
|
6944
|
+
return pool.filter((s) => s.group != null && !isGroupRow(s) && groupKeyOf(s) === key);
|
|
6945
|
+
}
|
|
6946
|
+
function defaultMemberOf(pool, groupRow) {
|
|
6947
|
+
const members = groupMembers(pool, groupRow);
|
|
6948
|
+
const defaults2 = members.filter((m) => m.default);
|
|
6949
|
+
return (defaults2.length === 1 ? defaults2[0] : members[0]) ?? null;
|
|
6950
|
+
}
|
|
6951
|
+
|
|
6916
6952
|
// src/data/ProviderRegistry.ts
|
|
6917
6953
|
var normName = (n) => n.trim().toLowerCase();
|
|
6918
6954
|
var normTicker = (t) => t.trim().toUpperCase();
|
|
@@ -7002,17 +7038,24 @@ var ProviderRegistry = class {
|
|
|
7002
7038
|
resolve(raw, opts = {}) {
|
|
7003
7039
|
const { provider, ticker } = this.parse(raw);
|
|
7004
7040
|
if (provider) {
|
|
7005
|
-
if (this.entries.has(provider))
|
|
7041
|
+
if (this.entries.has(provider)) {
|
|
7042
|
+
const d = this.entries.get(provider).byTicker?.get(normTicker(ticker));
|
|
7043
|
+
return { provider, ticker: d != null && isGroupRow(d) ? this.loadableTicker(provider, d) : ticker };
|
|
7044
|
+
}
|
|
7006
7045
|
const key = `${provider}:${normTicker(ticker)}`;
|
|
7007
7046
|
for (const name of this.candidateOrder(opts.default)) {
|
|
7008
7047
|
const d = this.entries.get(name).prefixIndex?.get(key);
|
|
7009
|
-
if (d) return { provider: name, ticker:
|
|
7048
|
+
if (d) return { provider: name, ticker: this.loadableTicker(name, d) };
|
|
7010
7049
|
}
|
|
7011
7050
|
return null;
|
|
7012
7051
|
}
|
|
7013
7052
|
const norm = normTicker(ticker);
|
|
7014
7053
|
for (const name of this.candidateOrder(opts.default)) {
|
|
7015
|
-
|
|
7054
|
+
const e = this.entries.get(name);
|
|
7055
|
+
if (e.index?.has(norm)) {
|
|
7056
|
+
const d = e.byTicker?.get(norm);
|
|
7057
|
+
return { provider: name, ticker: d != null && isGroupRow(d) ? this.loadableTicker(name, d) : ticker };
|
|
7058
|
+
}
|
|
7016
7059
|
}
|
|
7017
7060
|
if (opts.lenient && this.entries.size === 1) {
|
|
7018
7061
|
const only = this.names()[0];
|
|
@@ -7021,6 +7064,13 @@ var ProviderRegistry = class {
|
|
|
7021
7064
|
}
|
|
7022
7065
|
return null;
|
|
7023
7066
|
}
|
|
7067
|
+
/** What a resolved descriptor LOADS: itself — unless it is a GROUP row (listed, never
|
|
7068
|
+
* served), which loads its default member (single `default`, else first listed). A
|
|
7069
|
+
* memberless group keeps its own ticker and fails downstream like any unknown symbol. */
|
|
7070
|
+
loadableTicker(name, d) {
|
|
7071
|
+
if (!isGroupRow(d)) return d.ticker;
|
|
7072
|
+
return defaultMemberOf(this.entries.get(name)?.descriptors ?? [], d)?.ticker ?? d.ticker;
|
|
7073
|
+
}
|
|
7024
7074
|
/**
|
|
7025
7075
|
* Resolve now if possible, else resolve later — re-attempting after each provider
|
|
7026
7076
|
* settles. The promise stays pending until some registered provider can serve the
|
|
@@ -7309,6 +7359,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
7309
7359
|
/** Invalidates detached async work (backfill loops, in-flight loads, gap heals):
|
|
7310
7360
|
* bumped by init(), setMarket() and destroy(). */
|
|
7311
7361
|
this.generation = 0;
|
|
7362
|
+
/** Aborts the in-flight PROGRESSIVE load's source polling on supersession — an
|
|
7363
|
+
* abandoned stream left polling to its own budget starves the browser's per-host
|
|
7364
|
+
* connection pool, and the NEXT symbol's very first fetch with it (measured). */
|
|
7365
|
+
this.progressiveAbort = null;
|
|
7312
7366
|
/** Awaiters racing a superseded load (setMarket callers) — released on every bump so they never hang. */
|
|
7313
7367
|
this.supersedeWaiters = [];
|
|
7314
7368
|
/** `history:complete` fired for the CURRENT load. Each market load re-arms the cycle
|
|
@@ -7342,7 +7396,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
7342
7396
|
for (const engine of engines) this.registerEngine(engine.language, engine);
|
|
7343
7397
|
this.defaultLanguage = config.defaultLanguage;
|
|
7344
7398
|
this.renderer.mount(container, config.theme);
|
|
7345
|
-
this.renderer.onInputChange(
|
|
7399
|
+
this.renderer.onInputChange(
|
|
7400
|
+
(e) => e.kind === "prop" ? this.applyProps(e.indicatorId, { [e.key]: e.value }) : this.applyInputs(e.indicatorId, { [e.key]: e.value })
|
|
7401
|
+
);
|
|
7346
7402
|
this.renderer.onRemoveIndicator((id) => this.removeIndicator(id));
|
|
7347
7403
|
this.renderer.onToggleIndicatorVisible?.((id, visible) => this.setVisible(id, visible));
|
|
7348
7404
|
this.paneActionUnsub = this.renderer.onPaneAction?.((a) => this.handlePaneAction(a)) ?? null;
|
|
@@ -7449,6 +7505,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
7449
7505
|
* superseded setMarket awaiters so their promises resolve instead of hanging. */
|
|
7450
7506
|
bumpGeneration() {
|
|
7451
7507
|
const gen = ++this.generation;
|
|
7508
|
+
this.progressiveAbort?.abort();
|
|
7509
|
+
this.progressiveAbort = null;
|
|
7452
7510
|
for (const w of this.supersedeWaiters.splice(0)) w();
|
|
7453
7511
|
return gen;
|
|
7454
7512
|
}
|
|
@@ -7502,7 +7560,47 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
7502
7560
|
const requested = market.bars ?? 500;
|
|
7503
7561
|
const initialRange = market.visibleRange;
|
|
7504
7562
|
const deep = !market.data?.length && initialRange == null && requested > SINGLE_LOAD_BARS;
|
|
7505
|
-
|
|
7563
|
+
let progressiveServed = false;
|
|
7564
|
+
if (!market.data?.length && initialRange == null && this.feed.loadProgressive) {
|
|
7565
|
+
let painted = false;
|
|
7566
|
+
const paint = (bars, final) => {
|
|
7567
|
+
if (this.generation !== gen || !final && bars.length === 0) return;
|
|
7568
|
+
this.setBarSeries(bars, painted ? { preserveView: true } : void 0);
|
|
7569
|
+
if (!painted && bars.length > 0) {
|
|
7570
|
+
painted = true;
|
|
7571
|
+
if (opts.firstLoad) this.activateBarLayers();
|
|
7572
|
+
if (!final) this.historyState = "backfill";
|
|
7573
|
+
}
|
|
7574
|
+
};
|
|
7575
|
+
const abort = new AbortController();
|
|
7576
|
+
this.progressiveAbort = abort;
|
|
7577
|
+
progressiveServed = await new Promise((firstPaint) => {
|
|
7578
|
+
let signaled = false;
|
|
7579
|
+
const signal = (served) => {
|
|
7580
|
+
if (!signaled) {
|
|
7581
|
+
signaled = true;
|
|
7582
|
+
firstPaint(served);
|
|
7583
|
+
}
|
|
7584
|
+
};
|
|
7585
|
+
abort.signal.addEventListener("abort", () => signal(true), { once: true });
|
|
7586
|
+
this.feed.loadProgressive(market, (bars) => {
|
|
7587
|
+
paint(bars, false);
|
|
7588
|
+
if (painted) signal(true);
|
|
7589
|
+
}, { signal: abort.signal }).then((full) => {
|
|
7590
|
+
if (this.progressiveAbort === abort) this.progressiveAbort = null;
|
|
7591
|
+
if (full == null) return signal(false);
|
|
7592
|
+
if (this.generation !== gen) return signal(true);
|
|
7593
|
+
paint(full, true);
|
|
7594
|
+
this.completeHistory(full.length >= requested ? "depth" : "genesis");
|
|
7595
|
+
signal(true);
|
|
7596
|
+
}).catch(() => {
|
|
7597
|
+
if (this.progressiveAbort === abort) this.progressiveAbort = null;
|
|
7598
|
+
if (this.generation === gen) this.completeHistory("aborted");
|
|
7599
|
+
signal(true);
|
|
7600
|
+
});
|
|
7601
|
+
});
|
|
7602
|
+
}
|
|
7603
|
+
if (progressiveServed) ; else if (deep && this.feed.loadRange) {
|
|
7506
7604
|
const head = await this.feed.load({ ...market, bars: Math.min(requested, CHUNK_BARS) });
|
|
7507
7605
|
if (this.generation !== gen) return;
|
|
7508
7606
|
this.setBarSeries(head);
|
|
@@ -8022,7 +8120,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
8022
8120
|
const title = options.title ?? "Indicator";
|
|
8023
8121
|
const handle = new IndicatorHandleImpl(id, title, this, source);
|
|
8024
8122
|
this.handles.set(id, handle);
|
|
8025
|
-
this.registry.add({ id, title, source, options, inputValues: { ...options.inputs ?? {} } });
|
|
8123
|
+
this.registry.add({ id, title, source, options, inputValues: { ...options.inputs ?? {} }, propValues: { ...options.props ?? {} } });
|
|
8026
8124
|
void this.startIndicator(id, source, options, handle);
|
|
8027
8125
|
return handle;
|
|
8028
8126
|
}
|
|
@@ -8045,7 +8143,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
8045
8143
|
return handle;
|
|
8046
8144
|
}
|
|
8047
8145
|
const inputValues = { ...descriptor.defaultInputs(), ...options.inputs ?? {} };
|
|
8048
|
-
this.registry.add({ id, title, source: type, inputValues, native: { type, instance: descriptor.create(), descriptor } });
|
|
8146
|
+
this.registry.add({ id, title, source: type, inputValues, propValues: {}, native: { type, instance: descriptor.create(), descriptor } });
|
|
8049
8147
|
handle.setSchema(descriptor.inputsSchema());
|
|
8050
8148
|
void this.startNativeIndicator(id, handle);
|
|
8051
8149
|
return handle;
|
|
@@ -8098,6 +8196,19 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
8098
8196
|
if (record.session) record.session.update(record.inputValues);
|
|
8099
8197
|
else if (record.native && !record.hidden) record.native.instance.setInputs(record.inputValues);
|
|
8100
8198
|
}
|
|
8199
|
+
/** IndicatorController: re-run an indicator with merged declaration-prop overrides.
|
|
8200
|
+
* Same lifecycle as {@link applyInputs} — a prop change replays the whole script.
|
|
8201
|
+
* Script indicators only: natives have no declaration props. */
|
|
8202
|
+
applyProps(id, values) {
|
|
8203
|
+
const record = this.registry.get(id);
|
|
8204
|
+
if (!record?.session) return;
|
|
8205
|
+
record.propValues = { ...record.propValues, ...values };
|
|
8206
|
+
if (record.renderHandle) this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
|
|
8207
|
+
record.pendingStructural = true;
|
|
8208
|
+
if (!record.hidden) this.setLoading(record, true);
|
|
8209
|
+
record.pendingCause = "inputs";
|
|
8210
|
+
record.session.update(record.inputValues, record.propValues);
|
|
8211
|
+
}
|
|
8101
8212
|
/** IndicatorController: tear down an indicator and (if now empty) its pane. */
|
|
8102
8213
|
/** Live handles of every indicator on the chart (script + native), insertion order. */
|
|
8103
8214
|
listIndicators() {
|
|
@@ -8246,6 +8357,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
8246
8357
|
for (const input of prepared.inputs) defaults2[input.key] = input.defval;
|
|
8247
8358
|
record.inputValues = { ...defaults2, ...record.inputValues };
|
|
8248
8359
|
handle.setSchema(prepared.inputs);
|
|
8360
|
+
const propDefaults = {};
|
|
8361
|
+
for (const prop of prepared.props ?? []) propDefaults[prop.key] = prop.defval;
|
|
8362
|
+
record.propValues = { ...propDefaults, ...record.propValues };
|
|
8363
|
+
handle.setPropsSchema(prepared.props ?? []);
|
|
8249
8364
|
this.mountLoadingPlaceholder(id, record);
|
|
8250
8365
|
this.executeIndicator(id, handle);
|
|
8251
8366
|
} catch (err) {
|
|
@@ -8269,6 +8384,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
8269
8384
|
getBars: () => this.bars,
|
|
8270
8385
|
fetchSeries: (sym, tf, range) => this.fetchSeries(sym, tf, range),
|
|
8271
8386
|
inputs: record.inputValues,
|
|
8387
|
+
props: record.propValues,
|
|
8272
8388
|
visibleRange: this.currentVisibleRange(),
|
|
8273
8389
|
mode,
|
|
8274
8390
|
// Mid-backfill session starts (add / re-show / price-style re-execute) let the
|
|
@@ -8362,6 +8478,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
8362
8478
|
const meta = record.prepared.meta;
|
|
8363
8479
|
const model = {
|
|
8364
8480
|
id,
|
|
8481
|
+
// Deliberately the FULL title, never a shorttitle: while the script loads the
|
|
8482
|
+
// legend identifies it by its full name; the compact shorttitle arrives with
|
|
8483
|
+
// the first computed model and takes over from there.
|
|
8365
8484
|
title: record.options?.title ?? meta.title,
|
|
8366
8485
|
overlay: meta.overlay,
|
|
8367
8486
|
paneHint: meta.overlay ? "price" : "new",
|
|
@@ -8370,7 +8489,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
8370
8489
|
backgrounds: [],
|
|
8371
8490
|
priceLines: [],
|
|
8372
8491
|
inputs: record.prepared.inputs,
|
|
8373
|
-
inputValues: record.inputValues
|
|
8492
|
+
inputValues: record.inputValues,
|
|
8493
|
+
...record.prepared.props ? { props: record.prepared.props, propValues: record.propValues } : {}
|
|
8374
8494
|
};
|
|
8375
8495
|
const paneId = this.routePane(id, model, record.options ?? {});
|
|
8376
8496
|
this.placeModel(model, id, paneId);
|
|
@@ -8603,7 +8723,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
8603
8723
|
record.model = model;
|
|
8604
8724
|
if (record.pendingStructural) {
|
|
8605
8725
|
record.renderHandle = this.renderer.mountIndicator(model);
|
|
8606
|
-
this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues);
|
|
8726
|
+
this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
|
|
8607
8727
|
record.pendingStructural = false;
|
|
8608
8728
|
} else {
|
|
8609
8729
|
this.renderer.updateIndicator(record.renderHandle, modelToValuePatch(model));
|
|
@@ -8630,16 +8750,17 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
8630
8750
|
placeModel(model, id, paneId) {
|
|
8631
8751
|
model.id = id;
|
|
8632
8752
|
model.paneId = paneId;
|
|
8633
|
-
|
|
8634
|
-
for (const
|
|
8635
|
-
for (const
|
|
8753
|
+
const paneOf = (item) => item.overlay === true ? "price" : paneId;
|
|
8754
|
+
for (const series of model.series) series.paneId = paneOf(series);
|
|
8755
|
+
for (const fill of model.fills) fill.paneId = paneOf(fill);
|
|
8756
|
+
for (const bg of model.backgrounds) bg.paneId = paneOf(bg);
|
|
8636
8757
|
for (const line of model.priceLines) line.paneId = paneId;
|
|
8637
|
-
if (model.lines) for (const ln of model.lines) ln.paneId =
|
|
8638
|
-
if (model.boxes) for (const bx of model.boxes) bx.paneId =
|
|
8639
|
-
if (model.labels) for (const lb of model.labels) lb.paneId =
|
|
8640
|
-
if (model.polylines) for (const pl of model.polylines) pl.paneId =
|
|
8641
|
-
if (model.linefills) for (const lf of model.linefills) lf.paneId =
|
|
8642
|
-
if (model.tables) for (const tb of model.tables) tb.paneId =
|
|
8758
|
+
if (model.lines) for (const ln of model.lines) ln.paneId = paneOf(ln);
|
|
8759
|
+
if (model.boxes) for (const bx of model.boxes) bx.paneId = paneOf(bx);
|
|
8760
|
+
if (model.labels) for (const lb of model.labels) lb.paneId = paneOf(lb);
|
|
8761
|
+
if (model.polylines) for (const pl of model.polylines) pl.paneId = paneOf(pl);
|
|
8762
|
+
if (model.linefills) for (const lf of model.linefills) lf.paneId = paneOf(lf);
|
|
8763
|
+
if (model.tables) for (const tb of model.tables) tb.paneId = paneOf(tb);
|
|
8643
8764
|
}
|
|
8644
8765
|
emitContextChanged(id) {
|
|
8645
8766
|
if (!this.registry.get(id)?.session?.getContext) return;
|
|
@@ -8792,6 +8913,15 @@ var RendererControl = class {
|
|
|
8792
8913
|
setLegendActions(provider) {
|
|
8793
8914
|
this.renderer.setLegendActions?.(provider);
|
|
8794
8915
|
}
|
|
8916
|
+
/**
|
|
8917
|
+
* Wire the legend rows' HOST-CONTRIBUTED callout bubbles (the shells route the
|
|
8918
|
+
* plugin registry through this; see `registerLegendCallout`). Silent on a renderer
|
|
8919
|
+
* without the seam — contributed callouts simply never show there, same graceful
|
|
8920
|
+
* degradation as {@link setLegendActions}.
|
|
8921
|
+
*/
|
|
8922
|
+
setLegendCallouts(provider) {
|
|
8923
|
+
this.renderer.setLegendCallouts?.(provider);
|
|
8924
|
+
}
|
|
8795
8925
|
/**
|
|
8796
8926
|
* Replace the indicator legend's fold toggle with a host action (or restore it with
|
|
8797
8927
|
* `null`) — multi-chart shells point the chip at their indicator overview instead of
|
|
@@ -9142,6 +9272,28 @@ var CachingDataFeed = class {
|
|
|
9142
9272
|
this.store.merge(key, dropForming(bars));
|
|
9143
9273
|
return bars;
|
|
9144
9274
|
}
|
|
9275
|
+
/**
|
|
9276
|
+
* Progressive twin of {@link load}: a COLD load streams through the inner feed's
|
|
9277
|
+
* progressive path — batches forwarded verbatim, the FINAL answer cached exactly as
|
|
9278
|
+
* `load` caches — while a cache-covered load answers once through `load` itself (a
|
|
9279
|
+
* warm chart has nothing to stream). Falls back to `load` wholesale when the inner
|
|
9280
|
+
* feed lacks the capability, so callers may prefer this method unconditionally.
|
|
9281
|
+
*/
|
|
9282
|
+
async loadProgressive(cfg, onBatch, opts) {
|
|
9283
|
+
if (cfg.data && cfg.data.length > 0) return this.inner.load(cfg);
|
|
9284
|
+
if (!this.inner.loadProgressive) return null;
|
|
9285
|
+
const symbol = cfg.symbol ?? "TEST";
|
|
9286
|
+
const key = cacheKey(symbol, cfg.timeframe ?? "60", cfg.session);
|
|
9287
|
+
const cached = this.store.get(key);
|
|
9288
|
+
const n = cfg.bars ?? 500;
|
|
9289
|
+
const lastCached = cached?.[cached.length - 1];
|
|
9290
|
+
if (cached && lastCached && cached.length >= n - 1 && this.inner.loadRange) return this.load(cfg);
|
|
9291
|
+
this.store.retainSymbol(symbol);
|
|
9292
|
+
const bars = await this.inner.loadProgressive(cfg, onBatch, opts);
|
|
9293
|
+
if (bars == null) return null;
|
|
9294
|
+
this.store.merge(key, dropForming(bars));
|
|
9295
|
+
return bars;
|
|
9296
|
+
}
|
|
9145
9297
|
/**
|
|
9146
9298
|
* Cache-backed ranged fetch — the gateway engines use for secondary series
|
|
9147
9299
|
* (`request.security` HTF/LTF/cross-symbol) and the orchestrator's backward
|
|
@@ -9387,6 +9539,17 @@ var MultiProviderFeed = class {
|
|
|
9387
9539
|
this.prefetchSymbolInfo(resolved);
|
|
9388
9540
|
return this.cache.load(canonical(cfg, resolved));
|
|
9389
9541
|
}
|
|
9542
|
+
/** Progressive twin of {@link load} — same resolution, the cache streams the batches. */
|
|
9543
|
+
async loadProgressive(cfg, onBatch, opts) {
|
|
9544
|
+
if (cfg.data && cfg.data.length > 0) {
|
|
9545
|
+
this.liveBars = cfg.data.map((b) => ({ ...b }));
|
|
9546
|
+
return cfg.data;
|
|
9547
|
+
}
|
|
9548
|
+
const resolved = await this.registry.whenResolvable(rawSymbol(cfg), { default: this.primaryProvider });
|
|
9549
|
+
this.primaryProvider = resolved.provider;
|
|
9550
|
+
this.prefetchSymbolInfo(resolved);
|
|
9551
|
+
return this.cache.loadProgressive(canonical(cfg, resolved), onBatch, opts);
|
|
9552
|
+
}
|
|
9390
9553
|
/**
|
|
9391
9554
|
* Synchronous per-symbol metadata for engines (Pine `syminfo.*`), served from the cache
|
|
9392
9555
|
* warmed by load(). Undefined until the prefetch lands (the engine then synthesizes a
|
|
@@ -9474,6 +9637,18 @@ var RegistryFetchFeed = class {
|
|
|
9474
9637
|
if (!provider) return Promise.resolve([]);
|
|
9475
9638
|
return safeBars(provider, ticker, cfg.timeframe ?? "60", { limit: cfg.bars ?? 500, session: cfg.session });
|
|
9476
9639
|
}
|
|
9640
|
+
async loadProgressive(cfg, onBatch, opts) {
|
|
9641
|
+
const { provider: name, ticker } = parseSymbol(cfg.symbol ?? "");
|
|
9642
|
+
const provider = this.registry.get(name ?? "");
|
|
9643
|
+
if (!provider) return [];
|
|
9644
|
+
if (!provider.getBarsProgressive) return null;
|
|
9645
|
+
try {
|
|
9646
|
+
return await provider.getBarsProgressive(ticker, cfg.timeframe ?? "60", { limit: cfg.bars ?? 500, session: cfg.session }, onBatch, opts);
|
|
9647
|
+
} catch (e) {
|
|
9648
|
+
console.warn(`[vela] progressive fetch failed for ${ticker} ${cfg.timeframe ?? "60"} \u2014 ${e instanceof Error ? e.message : String(e)}`);
|
|
9649
|
+
return [];
|
|
9650
|
+
}
|
|
9651
|
+
}
|
|
9477
9652
|
loadRange(cfg, range) {
|
|
9478
9653
|
const { provider: name, ticker } = parseSymbol(cfg.symbol ?? "");
|
|
9479
9654
|
const provider = this.registry.get(name ?? "");
|
|
@@ -10379,13 +10554,6 @@ var Menu = class {
|
|
|
10379
10554
|
}
|
|
10380
10555
|
};
|
|
10381
10556
|
|
|
10382
|
-
// src/core/model/inputs.ts
|
|
10383
|
-
function inputVisible(when, values) {
|
|
10384
|
-
if (!when) return true;
|
|
10385
|
-
const conds = Array.isArray(when) ? when : [when];
|
|
10386
|
-
return conds.every((c) => c.anyOf ? c.anyOf.some((x) => x === values[c.key]) : values[c.key] === c.equals);
|
|
10387
|
-
}
|
|
10388
|
-
|
|
10389
10557
|
// src/ui/tokens.ts
|
|
10390
10558
|
var STATIC_ID = "vela-ui-tokens";
|
|
10391
10559
|
var STATIC_DECLS = Object.entries(STATIC_TOKENS).map(([k, v]) => ` ${k}: ${v};`).join("\n");
|
|
@@ -10631,6 +10799,203 @@ var Popover = class {
|
|
|
10631
10799
|
}
|
|
10632
10800
|
};
|
|
10633
10801
|
|
|
10802
|
+
// src/ui/components/callout-bubble/controller.ts
|
|
10803
|
+
function closesPanel(item) {
|
|
10804
|
+
return item.close !== false;
|
|
10805
|
+
}
|
|
10806
|
+
function calloutPanelRows(items) {
|
|
10807
|
+
const rows = [];
|
|
10808
|
+
for (const item of items) {
|
|
10809
|
+
if (item.type === "text") {
|
|
10810
|
+
rows.push({ type: "text", text: item.text });
|
|
10811
|
+
continue;
|
|
10812
|
+
}
|
|
10813
|
+
const last = rows[rows.length - 1];
|
|
10814
|
+
if (last && last.type === "buttons") last.buttons.push(item);
|
|
10815
|
+
else rows.push({ type: "buttons", buttons: [item] });
|
|
10816
|
+
}
|
|
10817
|
+
return rows;
|
|
10818
|
+
}
|
|
10819
|
+
|
|
10820
|
+
// src/ui/components/callout-bubble/styles.ts
|
|
10821
|
+
var CALLOUT_STYLE_ID = "vela-ui-callout-bubble";
|
|
10822
|
+
var CALLOUT_CSS = `
|
|
10823
|
+
.vela-callout {
|
|
10824
|
+
display: inline-grid;
|
|
10825
|
+
place-items: center;
|
|
10826
|
+
border-radius: 50%;
|
|
10827
|
+
flex: none;
|
|
10828
|
+
line-height: 0;
|
|
10829
|
+
box-sizing: border-box;
|
|
10830
|
+
cursor: default;
|
|
10831
|
+
user-select: none;
|
|
10832
|
+
-webkit-user-select: none;
|
|
10833
|
+
}
|
|
10834
|
+
.vela-callout[role='button'] { cursor: pointer; }
|
|
10835
|
+
.vela-callout svg { display: block; }
|
|
10836
|
+
/* The deployed panel \u2014 carries the kit's elevated-card look itself (the popover
|
|
10837
|
+
shell is bare positioning chrome). */
|
|
10838
|
+
.vela-callout-panel {
|
|
10839
|
+
display: flex;
|
|
10840
|
+
flex-direction: column;
|
|
10841
|
+
gap: 8px;
|
|
10842
|
+
padding: 10px 12px;
|
|
10843
|
+
max-width: 280px;
|
|
10844
|
+
box-sizing: border-box;
|
|
10845
|
+
background: var(--vela-surface-elev);
|
|
10846
|
+
border: 1px solid var(--vela-border-strong);
|
|
10847
|
+
border-radius: 6px;
|
|
10848
|
+
box-shadow: var(--vela-shadow);
|
|
10849
|
+
color: var(--vela-fg);
|
|
10850
|
+
font: var(--vela-font-size-md) var(--vela-font);
|
|
10851
|
+
}
|
|
10852
|
+
.vela-callout-title { font-weight: 600; color: var(--vela-fg-bright); }
|
|
10853
|
+
.vela-callout-text { color: var(--vela-fg-muted); line-height: 1.45; white-space: pre-line; }
|
|
10854
|
+
.vela-callout-actions { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
10855
|
+
.vela-callout-btn {
|
|
10856
|
+
cursor: pointer;
|
|
10857
|
+
height: 26px;
|
|
10858
|
+
padding: 0 10px;
|
|
10859
|
+
border-radius: 5px;
|
|
10860
|
+
border: 1px solid var(--vela-border);
|
|
10861
|
+
background: transparent;
|
|
10862
|
+
color: var(--vela-fg);
|
|
10863
|
+
font-size: var(--vela-font-size-md);
|
|
10864
|
+
font-family: inherit;
|
|
10865
|
+
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;
|
|
10866
|
+
}
|
|
10867
|
+
.vela-callout-btn:hover { background: var(--vela-hover); color: var(--vela-fg-bright); border-color: var(--vela-fg-muted); }
|
|
10868
|
+
.vela-callout-btn-primary { border-color: var(--vela-selected-bg); background: var(--vela-selected-bg); color: var(--vela-selected-fg); }
|
|
10869
|
+
.vela-callout-btn-primary:hover { background: var(--vela-selected-bg); color: var(--vela-selected-fg); opacity: 0.85; border-color: var(--vela-selected-bg); }
|
|
10870
|
+
`;
|
|
10871
|
+
|
|
10872
|
+
// src/ui/components/callout-bubble/view.ts
|
|
10873
|
+
var CalloutBubble = class {
|
|
10874
|
+
constructor(opts) {
|
|
10875
|
+
this.pop = null;
|
|
10876
|
+
this.spec = { icon: opts.icon, background: opts.background, label: opts.label };
|
|
10877
|
+
if (opts.color !== void 0) this.spec.color = opts.color;
|
|
10878
|
+
if (opts.panel !== void 0) this.spec.panel = opts.panel;
|
|
10879
|
+
this.size = opts.size ?? 16;
|
|
10880
|
+
this.host = opts.host;
|
|
10881
|
+
this.theme = opts.theme;
|
|
10882
|
+
this.boundary = opts.boundary;
|
|
10883
|
+
const doc = (opts.host ?? document.body).ownerDocument;
|
|
10884
|
+
injectStyles(CALLOUT_STYLE_ID, CALLOUT_CSS, doc);
|
|
10885
|
+
this.el = doc.createElement("span");
|
|
10886
|
+
this.el.className = "vela-callout";
|
|
10887
|
+
this.el.style.width = `${this.size}px`;
|
|
10888
|
+
this.el.style.height = `${this.size}px`;
|
|
10889
|
+
this.el.addEventListener("click", (e) => {
|
|
10890
|
+
if (!this.spec.panel) return;
|
|
10891
|
+
e.stopPropagation();
|
|
10892
|
+
this.toggle();
|
|
10893
|
+
});
|
|
10894
|
+
this.el.addEventListener("dblclick", (e) => {
|
|
10895
|
+
if (this.spec.panel) e.stopPropagation();
|
|
10896
|
+
});
|
|
10897
|
+
this.el.addEventListener("keydown", (e) => {
|
|
10898
|
+
if (!this.spec.panel || e.key !== "Enter" && e.key !== " ") return;
|
|
10899
|
+
e.preventDefault();
|
|
10900
|
+
this.toggle();
|
|
10901
|
+
});
|
|
10902
|
+
this.dress();
|
|
10903
|
+
}
|
|
10904
|
+
/** Re-dress the bubble (a status change: new icon, tint, label, panel). */
|
|
10905
|
+
set(spec) {
|
|
10906
|
+
this.spec = { ...this.spec, ...spec };
|
|
10907
|
+
if ("panel" in spec) this.pop?.hide();
|
|
10908
|
+
this.dress();
|
|
10909
|
+
}
|
|
10910
|
+
/** Whether the deployed panel is currently open. */
|
|
10911
|
+
get open() {
|
|
10912
|
+
return this.pop?.open ?? false;
|
|
10913
|
+
}
|
|
10914
|
+
/** Close the deployed panel, if any. The bubble itself stays. */
|
|
10915
|
+
hidePanel() {
|
|
10916
|
+
this.pop?.hide();
|
|
10917
|
+
}
|
|
10918
|
+
destroy() {
|
|
10919
|
+
this.pop?.destroy();
|
|
10920
|
+
this.pop = null;
|
|
10921
|
+
this.el.remove();
|
|
10922
|
+
}
|
|
10923
|
+
dress() {
|
|
10924
|
+
const clickable = this.spec.panel !== void 0;
|
|
10925
|
+
this.el.style.background = this.spec.background;
|
|
10926
|
+
this.el.style.color = this.spec.color ?? "";
|
|
10927
|
+
this.el.innerHTML = iconAt(this.spec.icon, this.size - 4);
|
|
10928
|
+
this.el.setAttribute("aria-label", this.spec.label);
|
|
10929
|
+
if (clickable) {
|
|
10930
|
+
this.el.setAttribute("role", "button");
|
|
10931
|
+
this.el.tabIndex = 0;
|
|
10932
|
+
} else {
|
|
10933
|
+
this.el.removeAttribute("role");
|
|
10934
|
+
this.el.removeAttribute("tabindex");
|
|
10935
|
+
}
|
|
10936
|
+
}
|
|
10937
|
+
toggle() {
|
|
10938
|
+
if (this.pop?.open) {
|
|
10939
|
+
this.pop.hide();
|
|
10940
|
+
return;
|
|
10941
|
+
}
|
|
10942
|
+
const panel = this.spec.panel;
|
|
10943
|
+
if (!panel) return;
|
|
10944
|
+
this.pop?.destroy();
|
|
10945
|
+
this.pop = new Popover({
|
|
10946
|
+
trigger: this.el,
|
|
10947
|
+
gap: 6,
|
|
10948
|
+
content: (body) => this.buildPanel(body, panel),
|
|
10949
|
+
...this.host ? { host: this.host } : {},
|
|
10950
|
+
...this.theme ? { theme: this.theme() } : {},
|
|
10951
|
+
...this.boundary ? { boundary: this.boundary } : {}
|
|
10952
|
+
});
|
|
10953
|
+
this.pop.show();
|
|
10954
|
+
}
|
|
10955
|
+
buildPanel(body, panel) {
|
|
10956
|
+
const doc = body.ownerDocument;
|
|
10957
|
+
const root = doc.createElement("div");
|
|
10958
|
+
root.className = "vela-callout-panel";
|
|
10959
|
+
if (panel.title) {
|
|
10960
|
+
const title = doc.createElement("div");
|
|
10961
|
+
title.className = "vela-callout-title";
|
|
10962
|
+
title.textContent = panel.title;
|
|
10963
|
+
root.appendChild(title);
|
|
10964
|
+
}
|
|
10965
|
+
for (const row of calloutPanelRows(panel.items)) {
|
|
10966
|
+
if (row.type === "text") {
|
|
10967
|
+
const text = doc.createElement("div");
|
|
10968
|
+
text.className = "vela-callout-text";
|
|
10969
|
+
text.textContent = row.text;
|
|
10970
|
+
root.appendChild(text);
|
|
10971
|
+
continue;
|
|
10972
|
+
}
|
|
10973
|
+
const actions = doc.createElement("div");
|
|
10974
|
+
actions.className = "vela-callout-actions";
|
|
10975
|
+
for (const item of row.buttons) {
|
|
10976
|
+
const btn2 = doc.createElement("button");
|
|
10977
|
+
btn2.type = "button";
|
|
10978
|
+
btn2.className = "vela-callout-btn" + (item.primary ? " vela-callout-btn-primary" : "");
|
|
10979
|
+
btn2.textContent = item.label;
|
|
10980
|
+
btn2.addEventListener("click", () => {
|
|
10981
|
+
item.run();
|
|
10982
|
+
if (closesPanel(item)) this.pop?.hide();
|
|
10983
|
+
});
|
|
10984
|
+
actions.appendChild(btn2);
|
|
10985
|
+
}
|
|
10986
|
+
root.appendChild(actions);
|
|
10987
|
+
}
|
|
10988
|
+
body.appendChild(root);
|
|
10989
|
+
}
|
|
10990
|
+
};
|
|
10991
|
+
|
|
10992
|
+
// src/core/model/inputs.ts
|
|
10993
|
+
function inputVisible(when, values) {
|
|
10994
|
+
if (!when) return true;
|
|
10995
|
+
const conds = Array.isArray(when) ? when : [when];
|
|
10996
|
+
return conds.every((c) => c.anyOf ? c.anyOf.some((x) => x === values[c.key]) : values[c.key] === c.equals);
|
|
10997
|
+
}
|
|
10998
|
+
|
|
10634
10999
|
// src/ui/components/select/controller.ts
|
|
10635
11000
|
function selectController(opts) {
|
|
10636
11001
|
const options = opts.options;
|
|
@@ -12545,6 +12910,10 @@ function buildFieldControl(desc) {
|
|
|
12545
12910
|
}
|
|
12546
12911
|
|
|
12547
12912
|
// src/renderers/shared/IndicatorInputsDialog.ts
|
|
12913
|
+
var PROPS_TAB = "Properties";
|
|
12914
|
+
function bagOf(row, decl) {
|
|
12915
|
+
return decl.prop ? row.propValues : row.values;
|
|
12916
|
+
}
|
|
12548
12917
|
var SOURCES = ["close", "open", "high", "low", "hl2", "hlc3", "ohlc4", "volume"];
|
|
12549
12918
|
var IndicatorInputsDialog = class {
|
|
12550
12919
|
constructor(host) {
|
|
@@ -12555,6 +12924,7 @@ var IndicatorInputsDialog = class {
|
|
|
12555
12924
|
this.openId = null;
|
|
12556
12925
|
this.row = null;
|
|
12557
12926
|
this.snapshot = null;
|
|
12927
|
+
this.propSnapshot = null;
|
|
12558
12928
|
this.dialogTips = [];
|
|
12559
12929
|
/** Re-applies every `when` gate against current values; set while the dialog is open. */
|
|
12560
12930
|
this.refreshVisibility = null;
|
|
@@ -12577,10 +12947,11 @@ var IndicatorInputsDialog = class {
|
|
|
12577
12947
|
}
|
|
12578
12948
|
open(row) {
|
|
12579
12949
|
this.close();
|
|
12580
|
-
if (row.inputs.length === 0) return;
|
|
12950
|
+
if (row.inputs.length === 0 && row.props.length === 0) return;
|
|
12581
12951
|
this.row = row;
|
|
12582
12952
|
this.openId = row.id;
|
|
12583
12953
|
this.snapshot = { ...row.values };
|
|
12954
|
+
this.propSnapshot = { ...row.propValues };
|
|
12584
12955
|
ensureDialogStyles();
|
|
12585
12956
|
const t = this.host.theme();
|
|
12586
12957
|
const border = "var(--vela-border)";
|
|
@@ -12588,7 +12959,7 @@ var IndicatorInputsDialog = class {
|
|
|
12588
12959
|
const host = this.host.dialogHost() ?? this.host.container;
|
|
12589
12960
|
const ui = new Dialog({
|
|
12590
12961
|
host,
|
|
12591
|
-
title: row.
|
|
12962
|
+
title: row.title,
|
|
12592
12963
|
// Non-modal: live-edit dialog — a modal machine locks pointer events on the
|
|
12593
12964
|
// whole body, killing the chart and the body-portaled popovers.
|
|
12594
12965
|
modal: false,
|
|
@@ -12620,6 +12991,16 @@ var IndicatorInputsDialog = class {
|
|
|
12620
12991
|
text: () => "Close"
|
|
12621
12992
|
}));
|
|
12622
12993
|
const tabDefs = tabInputs(row.inputs);
|
|
12994
|
+
if (row.props.length > 0) {
|
|
12995
|
+
const propDecls = row.props.map((p) => {
|
|
12996
|
+
const d = { ...p, prop: true };
|
|
12997
|
+
delete d.tab;
|
|
12998
|
+
return d;
|
|
12999
|
+
});
|
|
13000
|
+
const existing = tabDefs.find((t2) => t2.name === PROPS_TAB);
|
|
13001
|
+
if (existing) existing.inputs.push(...propDecls);
|
|
13002
|
+
else tabDefs.push({ name: PROPS_TAB, inputs: propDecls });
|
|
13003
|
+
}
|
|
12623
13004
|
const tabs = document.createElement("div");
|
|
12624
13005
|
tabs.style.cssText = `display:flex;gap:12px;padding:0 20px;border-bottom:1px solid ${border};flex:0 0 auto;`;
|
|
12625
13006
|
const tabEls = [];
|
|
@@ -12722,9 +13103,10 @@ var IndicatorInputsDialog = class {
|
|
|
12722
13103
|
this.openId = null;
|
|
12723
13104
|
this.row = null;
|
|
12724
13105
|
this.snapshot = null;
|
|
13106
|
+
this.propSnapshot = null;
|
|
12725
13107
|
ui?.destroy();
|
|
12726
13108
|
}
|
|
12727
|
-
/** Restore every input to its open-time value (re-running the indicator), then close. */
|
|
13109
|
+
/** Restore every input and prop to its open-time value (re-running the indicator), then close. */
|
|
12728
13110
|
revertAndClose() {
|
|
12729
13111
|
const row = this.row;
|
|
12730
13112
|
const snap = this.snapshot;
|
|
@@ -12738,6 +13120,17 @@ var IndicatorInputsDialog = class {
|
|
|
12738
13120
|
}
|
|
12739
13121
|
}
|
|
12740
13122
|
}
|
|
13123
|
+
const propSnap = this.propSnapshot;
|
|
13124
|
+
if (row && propSnap) {
|
|
13125
|
+
for (const p of row.props) {
|
|
13126
|
+
const snapped = propSnap[p.key];
|
|
13127
|
+
const before = snapped !== void 0 ? snapped : p.defval;
|
|
13128
|
+
if (row.propValues[p.key] !== before) {
|
|
13129
|
+
row.propValues[p.key] = before;
|
|
13130
|
+
this.host.onChange?.({ indicatorId: row.id, key: p.key, value: before, kind: "prop" });
|
|
13131
|
+
}
|
|
13132
|
+
}
|
|
13133
|
+
}
|
|
12741
13134
|
this.close();
|
|
12742
13135
|
}
|
|
12743
13136
|
/** The footer's reset button — same chip as Cancel, pinned to the LEFT edge
|
|
@@ -12755,19 +13148,27 @@ var IndicatorInputsDialog = class {
|
|
|
12755
13148
|
const row = this.row;
|
|
12756
13149
|
if (!row) return;
|
|
12757
13150
|
const snap = this.snapshot;
|
|
13151
|
+
const propSnap = this.propSnapshot;
|
|
12758
13152
|
for (const inp of row.inputs) {
|
|
12759
13153
|
if (row.values[inp.key] !== inp.defval) {
|
|
12760
13154
|
row.values[inp.key] = inp.defval;
|
|
12761
13155
|
this.host.onChange?.({ indicatorId: row.id, key: inp.key, value: inp.defval });
|
|
12762
13156
|
}
|
|
12763
13157
|
}
|
|
13158
|
+
for (const p of row.props) {
|
|
13159
|
+
if (row.propValues[p.key] !== p.defval) {
|
|
13160
|
+
row.propValues[p.key] = p.defval;
|
|
13161
|
+
this.host.onChange?.({ indicatorId: row.id, key: p.key, value: p.defval, kind: "prop" });
|
|
13162
|
+
}
|
|
13163
|
+
}
|
|
12764
13164
|
this.open(row);
|
|
12765
13165
|
if (snap) this.snapshot = snap;
|
|
13166
|
+
if (propSnap) this.propSnapshot = propSnap;
|
|
12766
13167
|
}
|
|
12767
13168
|
/** Write one edit through: store it, notify the host, and re-apply the `when` gates. */
|
|
12768
|
-
commit(row,
|
|
12769
|
-
row.
|
|
12770
|
-
this.host.onChange?.({ indicatorId: row.id, key, value });
|
|
13169
|
+
commit(row, decl, value) {
|
|
13170
|
+
bagOf(row, decl)[decl.key] = value;
|
|
13171
|
+
this.host.onChange?.({ indicatorId: row.id, key: decl.key, value, ...decl.prop ? { kind: "prop" } : {} });
|
|
12771
13172
|
this.refreshVisibility?.();
|
|
12772
13173
|
}
|
|
12773
13174
|
/** One settings row (or several `inline=` inputs) placed into a section's grid. */
|
|
@@ -12794,7 +13195,7 @@ var IndicatorInputsDialog = class {
|
|
|
12794
13195
|
const id = idOf(lead);
|
|
12795
13196
|
const info = lead.tooltip ? this.infoButton(lead.tooltip) : void 0;
|
|
12796
13197
|
if (lead.type === "bool") {
|
|
12797
|
-
const current = Boolean(row
|
|
13198
|
+
const current = Boolean(bagOf(row, lead)[lead.key] ?? lead.defval);
|
|
12798
13199
|
return append(fieldRow({
|
|
12799
13200
|
label: nameOf(lead),
|
|
12800
13201
|
id,
|
|
@@ -12803,7 +13204,7 @@ var IndicatorInputsDialog = class {
|
|
|
12803
13204
|
toggle: {
|
|
12804
13205
|
id,
|
|
12805
13206
|
checked: current,
|
|
12806
|
-
onChange: (v) => this.commit(row, lead
|
|
13207
|
+
onChange: (v) => this.commit(row, lead, v)
|
|
12807
13208
|
}
|
|
12808
13209
|
}));
|
|
12809
13210
|
}
|
|
@@ -12828,8 +13229,8 @@ var IndicatorInputsDialog = class {
|
|
|
12828
13229
|
}
|
|
12829
13230
|
/** Build the typed control for one input, committing edits live via `onChange`. */
|
|
12830
13231
|
buildControl(row, inp, id) {
|
|
12831
|
-
const current = row
|
|
12832
|
-
const emit = (value) => this.commit(row, inp
|
|
13232
|
+
const current = bagOf(row, inp)[inp.key] ?? inp.defval;
|
|
13233
|
+
const emit = (value) => this.commit(row, inp, value);
|
|
12833
13234
|
if (inp.type === "bool") {
|
|
12834
13235
|
return buildFieldControl({ kind: "switch", id, checked: Boolean(current), onChange: (v) => emit(v) }).el;
|
|
12835
13236
|
}
|
|
@@ -12840,7 +13241,7 @@ var IndicatorInputsDialog = class {
|
|
|
12840
13241
|
kind: "color",
|
|
12841
13242
|
id,
|
|
12842
13243
|
theme: this.host.theme(),
|
|
12843
|
-
get: () => String(row
|
|
13244
|
+
get: () => String(bagOf(row, inp)[inp.key] ?? inp.defval),
|
|
12844
13245
|
onChange: (v) => emit(v)
|
|
12845
13246
|
}).el;
|
|
12846
13247
|
}
|
|
@@ -13403,6 +13804,7 @@ function timeParts(ts) {
|
|
|
13403
13804
|
}
|
|
13404
13805
|
|
|
13405
13806
|
// src/renderers/shared/InputsUI.ts
|
|
13807
|
+
var LEGEND_AT_TOP_ATTR = "data-vela-pane-at-top";
|
|
13406
13808
|
var STATUS_KEYFRAMES = "@keyframes vela-ind-pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.3;transform:scale(.6)}}";
|
|
13407
13809
|
var LEGEND_ICON_PX2 = 16;
|
|
13408
13810
|
var LEGEND_CTL_PX = 18;
|
|
@@ -13420,6 +13822,9 @@ var CLOSE_SVG = iconAt("close", LEGEND_ICON_PX2);
|
|
|
13420
13822
|
var FOLD_SVG = iconAt("chevron-up", LEGEND_ICON_PX2);
|
|
13421
13823
|
var UNFOLD_SVG = iconAt("chevron-down", LEGEND_ICON_PX2);
|
|
13422
13824
|
var OVERVIEW_SVG = iconAt("objects", LEGEND_ICON_PX2);
|
|
13825
|
+
function legendCalloutsDisplay(open2, hasCallouts) {
|
|
13826
|
+
return !open2 && hasCallouts ? "inline-flex" : "none";
|
|
13827
|
+
}
|
|
13423
13828
|
var InputsUI = class {
|
|
13424
13829
|
constructor(container, theme, paneBoundsOf) {
|
|
13425
13830
|
this.container = container;
|
|
@@ -13441,10 +13846,14 @@ var InputsUI = class {
|
|
|
13441
13846
|
this.moveApi = null;
|
|
13442
13847
|
/** Host-contributed legend actions, resolved PER ROW at render time (see setLegendActions). */
|
|
13443
13848
|
this.legendActions = null;
|
|
13849
|
+
/** Host-contributed callout bubbles, resolved PER ROW at render time (see setLegendCallouts). */
|
|
13850
|
+
this.legendCallouts = null;
|
|
13444
13851
|
/** Chrome-tooltip disposers, per row id — a tip open at removal must not outlive its row. */
|
|
13445
13852
|
this.rowTips = /* @__PURE__ */ new Map();
|
|
13446
13853
|
/** Same, for the contributed extras only (rebuilt independently by setLegendActions). */
|
|
13447
13854
|
this.extrasTips = /* @__PURE__ */ new Map();
|
|
13855
|
+
/** Same, for the contributed callouts only (rebuilt independently by setLegendCallouts). */
|
|
13856
|
+
this.calloutTips = /* @__PURE__ */ new Map();
|
|
13448
13857
|
/** Open "Move to" menu (kept so it can be torn down). */
|
|
13449
13858
|
this.moveMenu = null;
|
|
13450
13859
|
this.moveTargets = [];
|
|
@@ -13602,6 +14011,42 @@ var InputsUI = class {
|
|
|
13602
14011
|
extrasEl.appendChild(btn2);
|
|
13603
14012
|
}
|
|
13604
14013
|
}
|
|
14014
|
+
/**
|
|
14015
|
+
* Wire the host-contributed callout bubbles. Same contract as
|
|
14016
|
+
* {@link setLegendActions}: re-calling replaces the provider and re-projects the
|
|
14017
|
+
* rows already on screen.
|
|
14018
|
+
*/
|
|
14019
|
+
setLegendCallouts(provider) {
|
|
14020
|
+
this.legendCallouts = provider;
|
|
14021
|
+
for (const row of this.rows.values()) this.renderCallouts(row);
|
|
14022
|
+
}
|
|
14023
|
+
/** (Re)build one row's callout bubbles — tinted icon circles beside the title whose
|
|
14024
|
+
* click (when the view carries content) deploys a panel of text and actions. */
|
|
14025
|
+
renderCallouts(row) {
|
|
14026
|
+
this.disposeTips(this.calloutTips, row.id);
|
|
14027
|
+
for (const bubble of row.callouts) bubble.destroy();
|
|
14028
|
+
row.callouts = [];
|
|
14029
|
+
row.calloutsEl.replaceChildren();
|
|
14030
|
+
const views = this.legendCallouts?.(row.id) ?? [];
|
|
14031
|
+
row.calloutsEl.style.display = legendCalloutsDisplay(row.highlighted, views.length > 0);
|
|
14032
|
+
for (const view of views) {
|
|
14033
|
+
const bubble = new CalloutBubble({
|
|
14034
|
+
icon: view.icon,
|
|
14035
|
+
background: view.background,
|
|
14036
|
+
...view.color !== void 0 ? { color: view.color } : {},
|
|
14037
|
+
label: view.tooltip,
|
|
14038
|
+
...view.content !== void 0 ? { panel: view.content } : {},
|
|
14039
|
+
// The panel portals into the plot host and self-tokens: it must work on
|
|
14040
|
+
// a bare chart, where no `.vela-ui` token ancestor exists.
|
|
14041
|
+
host: this.container,
|
|
14042
|
+
theme: () => this.theme
|
|
14043
|
+
});
|
|
14044
|
+
bubble.el.dataset.legendCallout = view.id;
|
|
14045
|
+
this.tip(this.calloutTips, row.id, bubble.el, view.tooltip);
|
|
14046
|
+
row.callouts.push(bubble);
|
|
14047
|
+
row.calloutsEl.appendChild(bubble.el);
|
|
14048
|
+
}
|
|
14049
|
+
}
|
|
13605
14050
|
/** Reposition the per-pane legend containers after a layout change. */
|
|
13606
14051
|
reposition() {
|
|
13607
14052
|
for (const [paneId, lg] of this.legends) this.positionLegend(lg, paneId);
|
|
@@ -13769,6 +14214,7 @@ var InputsUI = class {
|
|
|
13769
14214
|
}
|
|
13770
14215
|
positionLegend(lg, paneId) {
|
|
13771
14216
|
const bounds = this.paneBoundsOf ? this.paneBoundsOf(paneId) : { top: 0, height: Infinity };
|
|
14217
|
+
lg.toggleAttribute(LEGEND_AT_TOP_ATTR, bounds.top === 0);
|
|
13772
14218
|
lg.style.display = bounds.height < 4 || !this.titlesVisible ? "none" : "flex";
|
|
13773
14219
|
const collapsed = this.paneCollapse.has(paneId);
|
|
13774
14220
|
const masterId = this.paneCollapse.get(paneId) ?? null;
|
|
@@ -13844,13 +14290,13 @@ var InputsUI = class {
|
|
|
13844
14290
|
}
|
|
13845
14291
|
/** Create or update an indicator's legend row (in the legend for its pane). */
|
|
13846
14292
|
upsert(id, title, inputs, values, paneId = "price", opts = {}) {
|
|
13847
|
-
const settingsTitle = opts.settingsTitle ?? title;
|
|
13848
14293
|
const existing = this.rows.get(id);
|
|
13849
14294
|
if (existing) {
|
|
13850
14295
|
existing.title = title;
|
|
13851
|
-
existing.settingsTitle = settingsTitle;
|
|
13852
14296
|
existing.inputs = inputs;
|
|
13853
14297
|
existing.values = { ...values };
|
|
14298
|
+
existing.props = opts.props ?? [];
|
|
14299
|
+
existing.propValues = { ...opts.propValues ?? {} };
|
|
13854
14300
|
existing.titleEl.textContent = title;
|
|
13855
14301
|
if (existing.paneId !== paneId) {
|
|
13856
14302
|
existing.paneId = paneId;
|
|
@@ -13902,6 +14348,9 @@ var InputsUI = class {
|
|
|
13902
14348
|
titleWrap.appendChild(beta);
|
|
13903
14349
|
}
|
|
13904
14350
|
el.appendChild(titleWrap);
|
|
14351
|
+
const calloutsEl = document.createElement("span");
|
|
14352
|
+
calloutsEl.style.cssText = `display:none;align-items:center;gap:4px;flex:none;margin-left:${LEGEND_TITLE_STATUS_GAP_PX}px;`;
|
|
14353
|
+
el.appendChild(calloutsEl);
|
|
13905
14354
|
el.appendChild(statusEl);
|
|
13906
14355
|
const valuesEl = document.createElement("span");
|
|
13907
14356
|
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;`;
|
|
@@ -13918,13 +14367,13 @@ var InputsUI = class {
|
|
|
13918
14367
|
eye.className = "vela-ind-ctl";
|
|
13919
14368
|
eye.style.cssText = LEGEND_CTL_CSS;
|
|
13920
14369
|
eye.addEventListener("click", () => {
|
|
13921
|
-
const
|
|
13922
|
-
this.onToggleVisible?.(id, Boolean(
|
|
14370
|
+
const row2 = this.rows.get(id);
|
|
14371
|
+
this.onToggleVisible?.(id, Boolean(row2?.hidden));
|
|
13923
14372
|
});
|
|
13924
14373
|
controlsEl.appendChild(eye);
|
|
13925
14374
|
eyeEl = eye;
|
|
13926
14375
|
}
|
|
13927
|
-
if (inputs.length > 0) {
|
|
14376
|
+
if (inputs.length > 0 || (opts.props ?? []).length > 0) {
|
|
13928
14377
|
const gear = document.createElement("button");
|
|
13929
14378
|
gear.type = "button";
|
|
13930
14379
|
gear.setAttribute("aria-label", "Settings");
|
|
@@ -13964,7 +14413,9 @@ var InputsUI = class {
|
|
|
13964
14413
|
controlsEl.appendChild(close);
|
|
13965
14414
|
el.appendChild(controlsEl);
|
|
13966
14415
|
this.attach(this.legendFor(paneId), el, !!opts.native);
|
|
13967
|
-
|
|
14416
|
+
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 };
|
|
14417
|
+
this.rows.set(id, row);
|
|
14418
|
+
this.renderCallouts(row);
|
|
13968
14419
|
this.syncFoldToggle();
|
|
13969
14420
|
}
|
|
13970
14421
|
/** Place a row in its pane's legend — native rows PREPEND (pinned to the top), Pine rows append. */
|
|
@@ -13973,9 +14424,11 @@ var InputsUI = class {
|
|
|
13973
14424
|
else container.appendChild(el);
|
|
13974
14425
|
}
|
|
13975
14426
|
/** Reflect programmatic input changes (so a re-opened dialog shows current values). */
|
|
13976
|
-
setValues(id, values) {
|
|
14427
|
+
setValues(id, values, props) {
|
|
13977
14428
|
const row = this.rows.get(id);
|
|
13978
|
-
if (row)
|
|
14429
|
+
if (!row) return;
|
|
14430
|
+
row.values = { ...row.values, ...values };
|
|
14431
|
+
if (props) row.propValues = { ...row.propValues, ...props };
|
|
13979
14432
|
}
|
|
13980
14433
|
/**
|
|
13981
14434
|
* Reflect an indicator's live status in its legend row: `'loading'` shows three pulsing
|
|
@@ -14057,8 +14510,13 @@ var InputsUI = class {
|
|
|
14057
14510
|
syncRowActions(row) {
|
|
14058
14511
|
const open2 = row.highlighted;
|
|
14059
14512
|
row.controlsEl.style.display = open2 || row.hidden ? "inline-flex" : "none";
|
|
14060
|
-
if (open2)
|
|
14061
|
-
|
|
14513
|
+
if (open2) {
|
|
14514
|
+
row.el.appendChild(row.statusEl);
|
|
14515
|
+
for (const bubble of row.callouts) bubble.hidePanel();
|
|
14516
|
+
} else {
|
|
14517
|
+
row.el.insertBefore(row.statusEl, row.valuesEl);
|
|
14518
|
+
}
|
|
14519
|
+
row.calloutsEl.style.display = legendCalloutsDisplay(open2, row.callouts.length > 0);
|
|
14062
14520
|
for (const child of Array.from(row.controlsEl.children)) {
|
|
14063
14521
|
if (!(child instanceof HTMLElement) || child === row.eyeEl) continue;
|
|
14064
14522
|
if (child === row.extrasEl) {
|
|
@@ -14076,10 +14534,12 @@ var InputsUI = class {
|
|
|
14076
14534
|
}
|
|
14077
14535
|
remove(id) {
|
|
14078
14536
|
const row = this.rows.get(id);
|
|
14537
|
+
for (const bubble of row?.callouts ?? []) bubble.destroy();
|
|
14079
14538
|
row?.el.remove();
|
|
14080
14539
|
this.rows.delete(id);
|
|
14081
14540
|
this.disposeTips(this.rowTips, id);
|
|
14082
14541
|
this.disposeTips(this.extrasTips, id);
|
|
14542
|
+
this.disposeTips(this.calloutTips, id);
|
|
14083
14543
|
if (this.selectedId === id) this.selectedId = null;
|
|
14084
14544
|
if (this.inputsDialog.openId === id) this.inputsDialog.close();
|
|
14085
14545
|
this.syncFoldToggle();
|
|
@@ -14099,6 +14559,8 @@ var InputsUI = class {
|
|
|
14099
14559
|
this.rowMenu = null;
|
|
14100
14560
|
for (const id of [...this.rowTips.keys()]) this.disposeTips(this.rowTips, id);
|
|
14101
14561
|
for (const id of [...this.extrasTips.keys()]) this.disposeTips(this.extrasTips, id);
|
|
14562
|
+
for (const id of [...this.calloutTips.keys()]) this.disposeTips(this.calloutTips, id);
|
|
14563
|
+
for (const row of this.rows.values()) for (const bubble of row.callouts) bubble.destroy();
|
|
14102
14564
|
for (const lg of this.legends.values()) lg.remove();
|
|
14103
14565
|
this.legends.clear();
|
|
14104
14566
|
this.rows.clear();
|
|
@@ -14144,7 +14606,7 @@ var ICONS = {
|
|
|
14144
14606
|
};
|
|
14145
14607
|
var STYLE_ID = "vela-pane-controls";
|
|
14146
14608
|
var ICON_PX = 12;
|
|
14147
|
-
var CLUSTER_PILL = "rgba(0,0,0,0.
|
|
14609
|
+
var CLUSTER_PILL = "rgba(0,0,0,0.65)";
|
|
14148
14610
|
function ensureStyles() {
|
|
14149
14611
|
if (typeof document === "undefined" || document.getElementById(STYLE_ID)) return;
|
|
14150
14612
|
const st = document.createElement("style");
|
|
@@ -14164,8 +14626,12 @@ var PaneControls = class {
|
|
|
14164
14626
|
this.deps = deps;
|
|
14165
14627
|
this.clusters = /* @__PURE__ */ new Map();
|
|
14166
14628
|
this.hoverPaneId = null;
|
|
14629
|
+
/** Mobile: hover clusters are meaningless without a cursor — suppressed; a
|
|
14630
|
+
* collapsed pane's standalone expand chip stays (the only way back up). */
|
|
14631
|
+
this.suspended = false;
|
|
14167
14632
|
/** Reveal the cluster for the pane under the cursor, resolved from the pointer's y in the plot. */
|
|
14168
14633
|
this.onPlotMove = (e) => {
|
|
14634
|
+
if (this.suspended) return;
|
|
14169
14635
|
const rect = this.plot.getBoundingClientRect();
|
|
14170
14636
|
const y = e.clientY - rect.top;
|
|
14171
14637
|
let hit = null;
|
|
@@ -14237,7 +14703,12 @@ var PaneControls = class {
|
|
|
14237
14703
|
}
|
|
14238
14704
|
if (p.count > 1) {
|
|
14239
14705
|
cluster.appendChild(
|
|
14240
|
-
this.button(p.maximized ? ICONS.restore : ICONS.maximize, p.maximized ? "Restore pane" : "Maximize pane", false, () => this.deps.onToggleMaximize(p.id), {
|
|
14706
|
+
this.button(p.maximized ? ICONS.restore : ICONS.maximize, p.maximized ? "Restore pane" : "Maximize pane", false, () => this.deps.onToggleMaximize(p.id), {
|
|
14707
|
+
role: "maximize",
|
|
14708
|
+
// Same inverse-chip treatment as the collapsed pane's expand toggle: the
|
|
14709
|
+
// maximized state must read as an active state, not just a swapped glyph.
|
|
14710
|
+
selected: p.maximized
|
|
14711
|
+
})
|
|
14241
14712
|
);
|
|
14242
14713
|
}
|
|
14243
14714
|
}
|
|
@@ -14274,17 +14745,18 @@ var PaneControls = class {
|
|
|
14274
14745
|
}
|
|
14275
14746
|
const hovered = id === this.hoverPaneId;
|
|
14276
14747
|
const hasButtons = cluster.children.length > 0;
|
|
14277
|
-
const
|
|
14748
|
+
const stateChipRole = p.collapsed ? "collapse" : !this.suspended && p.maximized ? "maximize" : null;
|
|
14749
|
+
const visible = hasButtons && (hovered || stateChipRole != null) && p.height > 8;
|
|
14278
14750
|
cluster.style.right = `${rightPx}px`;
|
|
14279
14751
|
cluster.style.top = p.collapsed ? `${p.top + Math.max(1, Math.round((p.height - 24) / 2))}px` : `${p.top + 4}px`;
|
|
14280
14752
|
cluster.style.display = visible ? "flex" : "none";
|
|
14281
14753
|
if (!visible) continue;
|
|
14282
|
-
const
|
|
14283
|
-
cluster.style.background =
|
|
14754
|
+
const soloChip = stateChipRole != null && !hovered;
|
|
14755
|
+
cluster.style.background = soloChip ? "transparent" : CLUSTER_PILL;
|
|
14284
14756
|
for (const child of cluster.children) {
|
|
14285
14757
|
const btn2 = child;
|
|
14286
14758
|
btn2.style.display = "inline-flex";
|
|
14287
|
-
btn2.style.visibility =
|
|
14759
|
+
btn2.style.visibility = soloChip && btn2.dataset.role !== stateChipRole ? "hidden" : "visible";
|
|
14288
14760
|
}
|
|
14289
14761
|
}
|
|
14290
14762
|
}
|
|
@@ -14294,6 +14766,14 @@ var PaneControls = class {
|
|
|
14294
14766
|
this.hoverPaneId = paneId;
|
|
14295
14767
|
this.reposition();
|
|
14296
14768
|
}
|
|
14769
|
+
/** Mobile suppression: no hover clusters (touch has no cursor; the shell's own
|
|
14770
|
+
* chrome covers maximize), while collapsed panes keep their expand chips. */
|
|
14771
|
+
setSuspended(on) {
|
|
14772
|
+
if (on === this.suspended) return;
|
|
14773
|
+
this.suspended = on;
|
|
14774
|
+
if (on) this.hoverPaneId = null;
|
|
14775
|
+
this.reposition();
|
|
14776
|
+
}
|
|
14297
14777
|
destroy() {
|
|
14298
14778
|
this.plot.removeEventListener("pointermove", this.onPlotMove);
|
|
14299
14779
|
this.plot.removeEventListener("pointerleave", this.onPlotLeave);
|
|
@@ -14444,131 +14924,6 @@ var AxisScaleButtons = class {
|
|
|
14444
14924
|
}
|
|
14445
14925
|
};
|
|
14446
14926
|
|
|
14447
|
-
// src/renderers/shared/TableOverlay.ts
|
|
14448
|
-
var SIZE_PX3 = {
|
|
14449
|
-
auto: 13,
|
|
14450
|
-
tiny: 10,
|
|
14451
|
-
small: 11,
|
|
14452
|
-
normal: 13,
|
|
14453
|
-
large: 16,
|
|
14454
|
-
huge: 20
|
|
14455
|
-
};
|
|
14456
|
-
var TableOverlay = class {
|
|
14457
|
-
constructor(container, theme, paneBounds) {
|
|
14458
|
-
this.container = container;
|
|
14459
|
-
this.theme = theme;
|
|
14460
|
-
this.paneBounds = paneBounds;
|
|
14461
|
-
this.lastTables = [];
|
|
14462
|
-
if (getComputedStyle(container).position === "static") container.style.position = "relative";
|
|
14463
|
-
this.root = document.createElement("div");
|
|
14464
|
-
Object.assign(this.root.style, {
|
|
14465
|
-
position: "absolute",
|
|
14466
|
-
inset: "0",
|
|
14467
|
-
pointerEvents: "none",
|
|
14468
|
-
overflow: "hidden",
|
|
14469
|
-
zIndex: "3"
|
|
14470
|
-
});
|
|
14471
|
-
container.appendChild(this.root);
|
|
14472
|
-
}
|
|
14473
|
-
update(tables) {
|
|
14474
|
-
this.lastTables = tables;
|
|
14475
|
-
this.root.replaceChildren();
|
|
14476
|
-
for (const t of tables) this.root.appendChild(this.renderTable(t));
|
|
14477
|
-
}
|
|
14478
|
-
/** Re-render at the current pane geometry — after layout settles or on resize. */
|
|
14479
|
-
reposition() {
|
|
14480
|
-
if (this.root.isConnected) this.update(this.lastTables);
|
|
14481
|
-
}
|
|
14482
|
-
/** Show/hide the whole overlay. Tables anchor to pane corners, not to bars, so unlike the
|
|
14483
|
-
* series content they DON'T vanish with an emptied chart — the loading state hides them. */
|
|
14484
|
-
setVisible(visible) {
|
|
14485
|
-
this.root.style.display = visible ? "" : "none";
|
|
14486
|
-
}
|
|
14487
|
-
destroy() {
|
|
14488
|
-
this.root.remove();
|
|
14489
|
-
}
|
|
14490
|
-
renderTable(t) {
|
|
14491
|
-
const wrap = document.createElement("div");
|
|
14492
|
-
wrap.style.position = "absolute";
|
|
14493
|
-
this.anchor(wrap, t.position, this.paneBounds(t.paneId));
|
|
14494
|
-
const table = document.createElement("table");
|
|
14495
|
-
Object.assign(table.style, {
|
|
14496
|
-
borderCollapse: "collapse",
|
|
14497
|
-
background: t.bgColor ?? "transparent",
|
|
14498
|
-
fontFamily: this.theme.fontFamily || "sans-serif",
|
|
14499
|
-
// Frame as an inset shadow (not a `border`) so it stays independent of the
|
|
14500
|
-
// collapsed cell borders even when frame_width != border_width.
|
|
14501
|
-
boxShadow: t.frameColor && t.frameWidth > 0 ? `inset 0 0 0 ${t.frameWidth}px ${t.frameColor}` : "none",
|
|
14502
|
-
border: "none",
|
|
14503
|
-
tableLayout: "auto",
|
|
14504
|
-
// Re-enable pointer events on the table only (root is none) so cell tooltips work.
|
|
14505
|
-
pointerEvents: "auto"
|
|
14506
|
-
});
|
|
14507
|
-
const span = /* @__PURE__ */ new Map();
|
|
14508
|
-
const skip = /* @__PURE__ */ new Set();
|
|
14509
|
-
for (const m of t.merges) {
|
|
14510
|
-
span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
|
|
14511
|
-
for (let r = m.startRow; r <= m.endRow; r += 1) {
|
|
14512
|
-
for (let c = m.startCol; c <= m.endCol; c += 1) {
|
|
14513
|
-
if (r !== m.startRow || c !== m.startCol) skip.add(`${r}:${c}`);
|
|
14514
|
-
}
|
|
14515
|
-
}
|
|
14516
|
-
}
|
|
14517
|
-
const cellBorder = t.borderColor && t.borderWidth > 0 ? `${t.borderWidth}px solid ${t.borderColor}` : "none";
|
|
14518
|
-
for (let r = 0; r < t.rows; r += 1) {
|
|
14519
|
-
const tr = document.createElement("tr");
|
|
14520
|
-
for (let c = 0; c < t.columns; c += 1) {
|
|
14521
|
-
const cell = t.cells[r]?.[c] ?? null;
|
|
14522
|
-
if (skip.has(`${r}:${c}`) || cell?.merged) continue;
|
|
14523
|
-
const td = document.createElement("td");
|
|
14524
|
-
const sp = span.get(`${r}:${c}`);
|
|
14525
|
-
if (sp) {
|
|
14526
|
-
if (sp.cs > 1) td.colSpan = sp.cs;
|
|
14527
|
-
if (sp.rs > 1) td.rowSpan = sp.rs;
|
|
14528
|
-
}
|
|
14529
|
-
Object.assign(td.style, {
|
|
14530
|
-
border: cellBorder,
|
|
14531
|
-
padding: "2px 6px",
|
|
14532
|
-
background: cell?.bgColor ?? "transparent",
|
|
14533
|
-
color: cell?.textColor ?? this.theme.textColor,
|
|
14534
|
-
textAlign: cell?.hAlign ?? "center",
|
|
14535
|
-
verticalAlign: cell?.vAlign === "top" ? "top" : cell?.vAlign === "bottom" ? "bottom" : "middle",
|
|
14536
|
-
fontSize: `${SIZE_PX3[cell?.textSize ?? "normal"]}px`,
|
|
14537
|
-
fontFamily: cell?.fontFamily === "monospace" ? "monospace" : "inherit",
|
|
14538
|
-
fontWeight: cell?.bold ? "bold" : "normal",
|
|
14539
|
-
fontStyle: cell?.italic ? "italic" : "normal",
|
|
14540
|
-
whiteSpace: "pre-line"
|
|
14541
|
-
});
|
|
14542
|
-
if (cell?.tooltip) td.title = cell.tooltip;
|
|
14543
|
-
td.textContent = cell?.text ?? "";
|
|
14544
|
-
tr.appendChild(td);
|
|
14545
|
-
}
|
|
14546
|
-
table.appendChild(tr);
|
|
14547
|
-
}
|
|
14548
|
-
wrap.appendChild(table);
|
|
14549
|
-
return wrap;
|
|
14550
|
-
}
|
|
14551
|
-
/**
|
|
14552
|
-
* Position the wrapper at a Pine `position.*` corner/edge of the table's PANE
|
|
14553
|
-
* (not the whole chart), inset past the right price axis so it never overlaps
|
|
14554
|
-
* the Y-axis labels.
|
|
14555
|
-
*/
|
|
14556
|
-
anchor(el, position, b) {
|
|
14557
|
-
const m = 6;
|
|
14558
|
-
const containerH = this.root.clientHeight || this.container.clientHeight;
|
|
14559
|
-
const paneBottom = b.top + b.height;
|
|
14560
|
-
if (position.startsWith("top")) el.style.top = `${b.top + m}px`;
|
|
14561
|
-
else if (position.startsWith("bottom")) el.style.bottom = `${Math.max(0, containerH - paneBottom) + m}px`;
|
|
14562
|
-
else el.style.top = `${b.top + b.height / 2}px`;
|
|
14563
|
-
if (position.endsWith("left")) el.style.left = `${m}px`;
|
|
14564
|
-
else if (position.endsWith("right")) el.style.right = `${b.rightAxis + m}px`;
|
|
14565
|
-
else el.style.left = `calc(50% - ${b.rightAxis / 2}px)`;
|
|
14566
|
-
const tx = position.endsWith("center") ? "-50%" : "0";
|
|
14567
|
-
const ty = position.startsWith("middle") ? "-50%" : "0";
|
|
14568
|
-
if (tx !== "0" || ty !== "0") el.style.transform = `translate(${tx}, ${ty})`;
|
|
14569
|
-
}
|
|
14570
|
-
};
|
|
14571
|
-
|
|
14572
14927
|
// src/renderers/native/capabilities.ts
|
|
14573
14928
|
var NATIVE_CAPABILITIES = {
|
|
14574
14929
|
panes: true,
|
|
@@ -14589,7 +14944,7 @@ var NATIVE_CAPABILITIES = {
|
|
|
14589
14944
|
drawingDepth: true,
|
|
14590
14945
|
// drawings share the series' z space (backend-composited interleave layers)
|
|
14591
14946
|
tables: true,
|
|
14592
|
-
//
|
|
14947
|
+
// canvas-painted into the owning indicator's interleave slice
|
|
14593
14948
|
trades: true,
|
|
14594
14949
|
// strategy order-fill markers (arrows + labels + fill-price ticks)
|
|
14595
14950
|
inputsUI: true
|
|
@@ -14620,6 +14975,9 @@ function candleTier(spacing) {
|
|
|
14620
14975
|
if (spacing < CANDLE_BODY_MIN_SPACING) return "wick";
|
|
14621
14976
|
return "full";
|
|
14622
14977
|
}
|
|
14978
|
+
function snapY(yCss, dpr) {
|
|
14979
|
+
return Math.round(yCss * dpr) / dpr;
|
|
14980
|
+
}
|
|
14623
14981
|
function candleGeometry(xCss, spacing, dpr, bodyScale = 1) {
|
|
14624
14982
|
const wickDev = Math.max(1, Math.round(wickWidth(spacing) * dpr));
|
|
14625
14983
|
const wickLeftDev = Math.round(xCss * dpr - wickDev / 2);
|
|
@@ -15425,8 +15783,12 @@ var WebGL2Backend = class {
|
|
|
15425
15783
|
return sc === pane.scale ? pane : { ...pane, scale: sc };
|
|
15426
15784
|
};
|
|
15427
15785
|
b.alpha = this.modelAlpha;
|
|
15428
|
-
for (const m of models) for (const bgSpan of m.backgrounds) this.emitBackground(b, bgSpan, pane, coords);
|
|
15429
|
-
|
|
15786
|
+
for (const m of models) for (const bgSpan of m.backgrounds) if (bgSpan.overlay !== true) this.emitBackground(b, bgSpan, pane, coords);
|
|
15787
|
+
if (isPrice) {
|
|
15788
|
+
for (const m of scene.indicators.values()) {
|
|
15789
|
+
for (const bgSpan of m.backgrounds) if (bgSpan.overlay === true) this.emitBackground(b, bgSpan, pane, coords);
|
|
15790
|
+
}
|
|
15791
|
+
}
|
|
15430
15792
|
const drawCandles = isPrice && !scene.candlesHidden;
|
|
15431
15793
|
let candleDrawn = false;
|
|
15432
15794
|
for (const m of models) {
|
|
@@ -15440,15 +15802,26 @@ var WebGL2Backend = class {
|
|
|
15440
15802
|
b.alpha = this.modelAlpha;
|
|
15441
15803
|
const off = scene.offsetOf(m.id);
|
|
15442
15804
|
const mp = effPane(m);
|
|
15443
|
-
for (const
|
|
15805
|
+
for (const f of m.fills) if (f.overlay !== true) this.emitFill(b, m, f, mp, coords, i0, i1, off);
|
|
15806
|
+
for (const s of m.series) if (s.overlay !== true) this.emitSeries(b, s, mp, coords, i0, i1, theme, off);
|
|
15444
15807
|
}
|
|
15445
15808
|
if (drawCandles && !candleDrawn) {
|
|
15446
15809
|
drawSlicesUpTo(scene.candleZ);
|
|
15447
15810
|
b.alpha = this.candleStructureAlpha;
|
|
15448
15811
|
this.emitPriceSeries(b, scene, i0, i1, coords, pane, theme, barColorMap, dataW);
|
|
15449
15812
|
}
|
|
15450
|
-
drawSlicesUpTo(Infinity);
|
|
15451
15813
|
b.alpha = this.modelAlpha;
|
|
15814
|
+
if (isPrice) {
|
|
15815
|
+
for (const m of scene.indicators.values()) {
|
|
15816
|
+
const off = scene.offsetOf(m.id);
|
|
15817
|
+
for (const f of m.fills) if (f.overlay === true) this.emitFill(b, m, f, pane, coords, i0, i1, off);
|
|
15818
|
+
}
|
|
15819
|
+
for (const m of scene.indicators.values()) {
|
|
15820
|
+
const off = scene.offsetOf(m.id);
|
|
15821
|
+
for (const s of m.series) if (s.overlay === true) this.emitSeries(b, s, pane, coords, i0, i1, theme, off);
|
|
15822
|
+
}
|
|
15823
|
+
}
|
|
15824
|
+
drawSlicesUpTo(Infinity);
|
|
15452
15825
|
for (const m of models) {
|
|
15453
15826
|
const mp = effPane(m);
|
|
15454
15827
|
for (const pl of m.priceLines) this.emitHline(b, pl, mp, coords, dataW, theme);
|
|
@@ -15495,7 +15868,7 @@ var WebGL2Backend = class {
|
|
|
15495
15868
|
for (const pane of scene.orderedPanes()) {
|
|
15496
15869
|
const b = this.glowBatch;
|
|
15497
15870
|
b.reset();
|
|
15498
|
-
this.emitGlowSources(b, scene
|
|
15871
|
+
this.emitGlowSources(b, scene, pane, coords, i0, i1);
|
|
15499
15872
|
if (b.vertexCount === 0) continue;
|
|
15500
15873
|
const topH = Math.round(pane.bounds.top * dpr * 0.5);
|
|
15501
15874
|
const botH = Math.round((pane.bounds.top + pane.bounds.height) * dpr * 0.5);
|
|
@@ -15564,15 +15937,23 @@ var WebGL2Backend = class {
|
|
|
15564
15937
|
gl.bindVertexArray(null);
|
|
15565
15938
|
return true;
|
|
15566
15939
|
}
|
|
15567
|
-
/** The "neon" elements that glow: line/area/step lines + point markers (not candles/fills/bars).
|
|
15568
|
-
|
|
15569
|
-
|
|
15570
|
-
|
|
15571
|
-
|
|
15572
|
-
|
|
15573
|
-
|
|
15574
|
-
|
|
15575
|
-
|
|
15940
|
+
/** The "neon" elements that glow: line/area/step lines + point markers (not candles/fills/bars).
|
|
15941
|
+
* Routing mirrors the main pass: own series per pane, force_overlay series on the price pane. */
|
|
15942
|
+
emitGlowSources(b, scene, pane, coords, i0, i1) {
|
|
15943
|
+
const emitOne = (s, off) => {
|
|
15944
|
+
if (!isLineLikeSeries(s) || s.visible === false) return;
|
|
15945
|
+
if (s.kind === "histogram" || s.kind === "columns") return;
|
|
15946
|
+
if (s.kind === "circles" || s.kind === "cross") this.emitPointMarkers(b, s, pane, coords, i0, i1, off);
|
|
15947
|
+
else this.emitPolyline(b, s, pane, coords, i0, i1, s.kind === "step", off);
|
|
15948
|
+
};
|
|
15949
|
+
for (const m of scene.indicatorsForPane(pane.id)) {
|
|
15950
|
+
const off = scene.offsetOf(m.id);
|
|
15951
|
+
for (const s of m.series) if (s.overlay !== true) emitOne(s, off);
|
|
15952
|
+
}
|
|
15953
|
+
if (pane.kind === "price") {
|
|
15954
|
+
for (const m of scene.indicators.values()) {
|
|
15955
|
+
const off = scene.offsetOf(m.id);
|
|
15956
|
+
for (const s of m.series) if (s.overlay === true) emitOne(s, off);
|
|
15576
15957
|
}
|
|
15577
15958
|
}
|
|
15578
15959
|
}
|
|
@@ -15835,12 +16216,12 @@ var WebGL2Backend = class {
|
|
|
15835
16216
|
if (drawBody) {
|
|
15836
16217
|
const oY = coords.priceToY(bar.open, pane.scale, pane.bounds);
|
|
15837
16218
|
const cY = coords.priceToY(bar.close, pane.scale, pane.bounds);
|
|
15838
|
-
bodyTop = Math.min(oY, cY);
|
|
15839
|
-
bodyH = Math.max(1, Math.
|
|
16219
|
+
bodyTop = snapY(Math.min(oY, cY), coords.dpr);
|
|
16220
|
+
bodyH = Math.max(1 / coords.dpr, snapY(Math.max(oY, cY), coords.dpr) - bodyTop);
|
|
15840
16221
|
}
|
|
15841
16222
|
if (cs.wickVisible) {
|
|
15842
|
-
const hY = coords.priceToY(bar.high, pane.scale, pane.bounds);
|
|
15843
|
-
const lY = coords.priceToY(bar.low, pane.scale, pane.bounds);
|
|
16223
|
+
const hY = snapY(coords.priceToY(bar.high, pane.scale, pane.bounds), coords.dpr);
|
|
16224
|
+
const lY = snapY(coords.priceToY(bar.low, pane.scale, pane.bounds), coords.dpr);
|
|
15844
16225
|
b.alpha = this.candleStructureAlpha;
|
|
15845
16226
|
const wCol = parseColor((isUp ? cs.wickUpColor : cs.wickDownColor) ?? (drawBody ? dir : bodyColorStr));
|
|
15846
16227
|
if (drawBody) {
|
|
@@ -15855,9 +16236,9 @@ var WebGL2Backend = class {
|
|
|
15855
16236
|
b.alpha = this.candleBodyAlpha;
|
|
15856
16237
|
b.rect(g.bodyX, bodyTop, g.bodyW, bodyH, c);
|
|
15857
16238
|
}
|
|
15858
|
-
if (cs.borderVisible ||
|
|
16239
|
+
if (cs.borderVisible || fading && cs.bodyVisible) {
|
|
15859
16240
|
b.alpha = this.candleStructureAlpha;
|
|
15860
|
-
const bord = cs.borderVisible
|
|
16241
|
+
const bord = cs.borderVisible ? parseColor((isUp ? cs.borderUpColor : cs.borderDownColor) ?? bodyColorStr) : c;
|
|
15861
16242
|
const bw = Math.max(0, g.bodyW - 1);
|
|
15862
16243
|
const bh = Math.max(0, bodyH - 1);
|
|
15863
16244
|
b.rectStroke(g.bodyX + 0.5, bodyTop + 0.5, bw, bh, 1, bord);
|
|
@@ -17173,9 +17554,10 @@ var SceneGraph = class {
|
|
|
17173
17554
|
* so each indicator arrives behind the candles (and behind older indicators);
|
|
17174
17555
|
* `setIndicatorZ`/`bringToFront`/`sendToBack` change it. */
|
|
17175
17556
|
this.seriesZ = /* @__PURE__ */ new Map();
|
|
17176
|
-
/** Per-pane raster layers of
|
|
17557
|
+
/** Per-pane raster layers of drawings interleaved into the series stack (each
|
|
17558
|
+
* indicator's Pine drawings at its model's z, plus in-stack user drawings) — each a
|
|
17177
17559
|
* prepainted canvas the backend composites just before the series carrying `beforeZ`.
|
|
17178
|
-
* Rebuilt by the renderer per data frame
|
|
17560
|
+
* Rebuilt by the renderer per data frame. */
|
|
17179
17561
|
this.drawingSlices = /* @__PURE__ */ new Map();
|
|
17180
17562
|
/** Per-model index offset: the chart bar index of the model's `anchorTime` — its
|
|
17181
17563
|
* index-aligned payloads (dense series arrays, `bar_index` drawings) count from that
|
|
@@ -17409,8 +17791,12 @@ var Canvas2dBackend = class {
|
|
|
17409
17791
|
return sc === pane.scale ? pane : { ...pane, scale: sc };
|
|
17410
17792
|
};
|
|
17411
17793
|
ctx.globalAlpha = this.modelAlpha;
|
|
17412
|
-
for (const m of models) for (const bg of m.backgrounds) this.drawBackground(ctx, bg, pane, coords);
|
|
17413
|
-
|
|
17794
|
+
for (const m of models) for (const bg of m.backgrounds) if (bg.overlay !== true) this.drawBackground(ctx, bg, pane, coords);
|
|
17795
|
+
if (isPrice) {
|
|
17796
|
+
for (const m of scene.indicators.values()) {
|
|
17797
|
+
for (const bg of m.backgrounds) if (bg.overlay === true) this.drawBackground(ctx, bg, pane, coords);
|
|
17798
|
+
}
|
|
17799
|
+
}
|
|
17414
17800
|
const slices = scene.drawingSlices.get(pane.id) ?? [];
|
|
17415
17801
|
let si = 0;
|
|
17416
17802
|
const drawSlicesUpTo = (z) => {
|
|
@@ -17432,13 +17818,25 @@ var Canvas2dBackend = class {
|
|
|
17432
17818
|
ctx.globalAlpha = this.modelAlpha;
|
|
17433
17819
|
const off = scene.offsetOf(m.id);
|
|
17434
17820
|
const mp = effPane(m);
|
|
17435
|
-
for (const
|
|
17821
|
+
for (const f of m.fills) if (f.overlay !== true) this.drawFill(ctx, m, f, mp, coords, i0, i1, off);
|
|
17822
|
+
for (const s of m.series) if (s.overlay !== true) this.drawSeries(ctx, s, mp, coords, i0, i1, theme, off);
|
|
17436
17823
|
}
|
|
17437
17824
|
if (drawCandles && !candleDrawn) {
|
|
17438
17825
|
drawSlicesUpTo(scene.candleZ);
|
|
17439
17826
|
ctx.globalAlpha = this.candleStructureAlpha;
|
|
17440
17827
|
this.drawPriceSeries(ctx, scene, i0, i1, coords, pane, theme, barColorMap, dataW);
|
|
17441
17828
|
}
|
|
17829
|
+
if (isPrice) {
|
|
17830
|
+
ctx.globalAlpha = this.modelAlpha;
|
|
17831
|
+
for (const m of scene.indicators.values()) {
|
|
17832
|
+
const off = scene.offsetOf(m.id);
|
|
17833
|
+
for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0, i1, off);
|
|
17834
|
+
}
|
|
17835
|
+
for (const m of scene.indicators.values()) {
|
|
17836
|
+
const off = scene.offsetOf(m.id);
|
|
17837
|
+
for (const s of m.series) if (s.overlay === true) this.drawSeries(ctx, s, pane, coords, i0, i1, theme, off);
|
|
17838
|
+
}
|
|
17839
|
+
}
|
|
17442
17840
|
drawSlicesUpTo(Infinity);
|
|
17443
17841
|
ctx.globalAlpha = this.modelAlpha;
|
|
17444
17842
|
for (const m of models) {
|
|
@@ -17683,13 +18081,13 @@ var Canvas2dBackend = class {
|
|
|
17683
18081
|
if (drawBody) {
|
|
17684
18082
|
const oY = coords.priceToY(b.open, pane.scale, pane.bounds);
|
|
17685
18083
|
const cY = coords.priceToY(b.close, pane.scale, pane.bounds);
|
|
17686
|
-
top = Math.min(oY, cY);
|
|
17687
|
-
bodyH = Math.max(1, Math.
|
|
18084
|
+
top = snapY(Math.min(oY, cY), coords.dpr);
|
|
18085
|
+
bodyH = Math.max(1 / coords.dpr, snapY(Math.max(oY, cY), coords.dpr) - top);
|
|
17688
18086
|
}
|
|
17689
18087
|
if (cs.wickVisible) {
|
|
17690
18088
|
const wick = (up ? cs.wickUpColor : cs.wickDownColor) ?? (drawBody ? dir : color);
|
|
17691
|
-
const hY = coords.priceToY(b.high, pane.scale, pane.bounds);
|
|
17692
|
-
const lY = coords.priceToY(b.low, pane.scale, pane.bounds);
|
|
18089
|
+
const hY = snapY(coords.priceToY(b.high, pane.scale, pane.bounds), coords.dpr);
|
|
18090
|
+
const lY = snapY(coords.priceToY(b.low, pane.scale, pane.bounds), coords.dpr);
|
|
17693
18091
|
ctx.globalAlpha = this.candleStructureAlpha;
|
|
17694
18092
|
ctx.strokeStyle = wick;
|
|
17695
18093
|
ctx.lineWidth = g.wickW;
|
|
@@ -17711,9 +18109,9 @@ var Canvas2dBackend = class {
|
|
|
17711
18109
|
ctx.fillStyle = color;
|
|
17712
18110
|
ctx.fillRect(g.bodyX, top, g.bodyW, bodyH);
|
|
17713
18111
|
}
|
|
17714
|
-
if (cs.borderVisible ||
|
|
18112
|
+
if (cs.borderVisible || fading && cs.bodyVisible) {
|
|
17715
18113
|
ctx.globalAlpha = this.candleStructureAlpha;
|
|
17716
|
-
ctx.strokeStyle = cs.borderVisible
|
|
18114
|
+
ctx.strokeStyle = cs.borderVisible ? (up ? cs.borderUpColor : cs.borderDownColor) ?? color : color;
|
|
17717
18115
|
ctx.lineWidth = 1;
|
|
17718
18116
|
const bw = Math.max(0, g.bodyW - 1);
|
|
17719
18117
|
const bh = Math.max(0, bodyH - 1);
|
|
@@ -18136,6 +18534,19 @@ function autoFontSize(lines, boxW, boxH, bold) {
|
|
|
18136
18534
|
|
|
18137
18535
|
// src/renderers/shared/DrawingSceneRenderer.ts
|
|
18138
18536
|
var EMPTY_DRAWING_SET = { lines: [], boxes: [], labels: [], polylines: [], linefills: [] };
|
|
18537
|
+
function modelDrawingSet(m, overlay) {
|
|
18538
|
+
const want = (d) => Boolean(d.overlay) === overlay;
|
|
18539
|
+
return {
|
|
18540
|
+
lines: (m.lines ?? []).filter(want),
|
|
18541
|
+
boxes: (m.boxes ?? []).filter(want),
|
|
18542
|
+
labels: (m.labels ?? []).filter(want),
|
|
18543
|
+
polylines: (m.polylines ?? []).filter(want),
|
|
18544
|
+
linefills: (m.linefills ?? []).filter(want)
|
|
18545
|
+
};
|
|
18546
|
+
}
|
|
18547
|
+
function drawingSetEmpty(s) {
|
|
18548
|
+
return !s.lines.length && !s.boxes.length && !s.labels.length && !s.polylines.length && !s.linefills.length;
|
|
18549
|
+
}
|
|
18139
18550
|
function fontSizePx(size) {
|
|
18140
18551
|
return size === "auto" ? 12 : namedFontSize(size);
|
|
18141
18552
|
}
|
|
@@ -18145,6 +18556,8 @@ var DrawingSceneRenderer = class {
|
|
|
18145
18556
|
this.set = set;
|
|
18146
18557
|
/** measureText width cache, keyed by `${font} ${text}`, persists across frames. */
|
|
18147
18558
|
this.widthCache = /* @__PURE__ */ new Map();
|
|
18559
|
+
/** Tooltip hit-rects captured while drawing the CURRENT set (rebuilt per render). */
|
|
18560
|
+
this.tipRegions = [];
|
|
18148
18561
|
/**
|
|
18149
18562
|
* Index offset of the CURRENT set's model: its `xloc:'bar_index'` coordinates count
|
|
18150
18563
|
* from the model's anchor bar, so they shift by this to land on chart logical indices.
|
|
@@ -18165,8 +18578,13 @@ var DrawingSceneRenderer = class {
|
|
|
18165
18578
|
const s = this.set;
|
|
18166
18579
|
return !s.lines.length && !s.boxes.length && !s.labels.length && !s.polylines.length && !s.linefills.length;
|
|
18167
18580
|
}
|
|
18581
|
+
/** Tooltip hit-rects of the labels drawn by the LAST `render` call (same coords as `ctx`). */
|
|
18582
|
+
labelTipRegions() {
|
|
18583
|
+
return this.tipRegions;
|
|
18584
|
+
}
|
|
18168
18585
|
/** Draw the whole set into `ctx` using the supplied coordinate closures. */
|
|
18169
18586
|
render(ctx, W, H, xOf, yOf) {
|
|
18587
|
+
this.tipRegions = [];
|
|
18170
18588
|
if (this.isEmpty()) return;
|
|
18171
18589
|
ctx.save();
|
|
18172
18590
|
this.drawLinefills(ctx, W, H, xOf, yOf);
|
|
@@ -18514,13 +18932,35 @@ var DrawingSceneRenderer = class {
|
|
|
18514
18932
|
if (this.isPointShape(lb.style)) {
|
|
18515
18933
|
if (!lb.noFill) this.drawLabelShape(ctx, lb.style, px, py, fontPx, color);
|
|
18516
18934
|
if (lb.text) this.drawLabelText(ctx, lb, px, py + fontPx, fontPx);
|
|
18517
|
-
|
|
18518
|
-
|
|
18935
|
+
if (lb.tooltip) {
|
|
18936
|
+
const r = Math.max(4, fontPx * 0.6) + 3;
|
|
18937
|
+
this.tipRegions.push({ left: px - r, top: py - r, right: px + r, bottom: py + r, text: lb.tooltip });
|
|
18938
|
+
}
|
|
18939
|
+
} else if (lb.style === "none" || lb.style === "text_outline") {
|
|
18940
|
+
if (lb.text) {
|
|
18941
|
+
this.drawLabelText(ctx, lb, px, py, fontPx, lb.style === "text_outline");
|
|
18942
|
+
if (lb.tooltip) this.tipRegions.push(this.textRegion(ctx, lb, px, py, fontPx, lb.tooltip));
|
|
18943
|
+
}
|
|
18519
18944
|
} else {
|
|
18520
|
-
this.drawBubble(ctx, lb, px, py, fontPx, color);
|
|
18945
|
+
const r = this.drawBubble(ctx, lb, px, py, fontPx, color);
|
|
18946
|
+
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 });
|
|
18521
18947
|
}
|
|
18522
18948
|
}
|
|
18523
18949
|
}
|
|
18950
|
+
/** Canvas font for a label's text — family + Pine `text_formatting` (bold/italic). */
|
|
18951
|
+
labelFont(lb, fontPx) {
|
|
18952
|
+
const family = lb.fontFamily === "monospace" ? "monospace" : this.deps.theme.fontFamily || "sans-serif";
|
|
18953
|
+
return `${lb.italic ? "italic " : ""}${lb.bold ? "bold " : ""}${fontPx}px ${family}`;
|
|
18954
|
+
}
|
|
18955
|
+
/** Hover rect of a text-only label (style none/text_outline/noFill), centered like drawLabelText. */
|
|
18956
|
+
textRegion(ctx, lb, cx, cy, fontPx, text) {
|
|
18957
|
+
const font = this.labelFont(lb, fontPx);
|
|
18958
|
+
const lines = (lb.text ?? "").split("\n");
|
|
18959
|
+
const w = Math.max(1, ...lines.map((l) => this.measure(ctx, font, l)));
|
|
18960
|
+
const h = fontPx * 1.25 * lines.length;
|
|
18961
|
+
const left = lb.textAlign === "left" ? cx : lb.textAlign === "right" ? cx - w : cx - w / 2;
|
|
18962
|
+
return { left, top: cy - h / 2, right: left + w, bottom: cy + h / 2, text };
|
|
18963
|
+
}
|
|
18524
18964
|
isPointShape(style) {
|
|
18525
18965
|
switch (style) {
|
|
18526
18966
|
case "circle":
|
|
@@ -18539,8 +18979,7 @@ var DrawingSceneRenderer = class {
|
|
|
18539
18979
|
}
|
|
18540
18980
|
}
|
|
18541
18981
|
drawLabelText(ctx, lb, cx, cy, fontPx, outline = false) {
|
|
18542
|
-
|
|
18543
|
-
ctx.font = `${fontPx}px ${family}`;
|
|
18982
|
+
ctx.font = this.labelFont(lb, fontPx);
|
|
18544
18983
|
ctx.textAlign = lb.textAlign === "left" ? "left" : lb.textAlign === "right" ? "right" : "center";
|
|
18545
18984
|
ctx.textBaseline = "middle";
|
|
18546
18985
|
const lines = lb.text.split("\n");
|
|
@@ -18620,9 +19059,9 @@ var DrawingSceneRenderer = class {
|
|
|
18620
19059
|
ctx.fill();
|
|
18621
19060
|
}
|
|
18622
19061
|
}
|
|
19062
|
+
/** Draw a bubble label; returns the bubble body rect (for tooltip hit-testing). */
|
|
18623
19063
|
drawBubble(ctx, lb, px, py, fontPx, color) {
|
|
18624
|
-
|
|
18625
|
-
ctx.font = `${fontPx}px ${family}`;
|
|
19064
|
+
ctx.font = this.labelFont(lb, fontPx);
|
|
18626
19065
|
const lines = (lb.text ?? "").split("\n");
|
|
18627
19066
|
const padX = 6;
|
|
18628
19067
|
const padY = 4;
|
|
@@ -18680,38 +19119,51 @@ var DrawingSceneRenderer = class {
|
|
|
18680
19119
|
by = py - h - ptr;
|
|
18681
19120
|
pointer = "down";
|
|
18682
19121
|
}
|
|
18683
|
-
|
|
18684
|
-
|
|
18685
|
-
|
|
18686
|
-
if (pointer !== "none") {
|
|
18687
|
-
ctx.beginPath();
|
|
18688
|
-
if (pointer === "down") {
|
|
18689
|
-
ctx.moveTo(px - ptr, by + h);
|
|
18690
|
-
ctx.lineTo(px + ptr, by + h);
|
|
18691
|
-
ctx.lineTo(px, py);
|
|
18692
|
-
} else if (pointer === "up") {
|
|
18693
|
-
ctx.moveTo(px - ptr, by);
|
|
18694
|
-
ctx.lineTo(px + ptr, by);
|
|
18695
|
-
ctx.lineTo(px, py);
|
|
18696
|
-
} else if (pointer === "left") {
|
|
18697
|
-
ctx.moveTo(bx, py - ptr);
|
|
18698
|
-
ctx.lineTo(bx, py + ptr);
|
|
18699
|
-
ctx.lineTo(px, py);
|
|
18700
|
-
} else {
|
|
18701
|
-
ctx.moveTo(bx + w, py - ptr);
|
|
18702
|
-
ctx.lineTo(bx + w, py + ptr);
|
|
18703
|
-
ctx.lineTo(px, py);
|
|
18704
|
-
}
|
|
18705
|
-
ctx.closePath();
|
|
19122
|
+
if (!lb.noFill) {
|
|
19123
|
+
ctx.fillStyle = color;
|
|
19124
|
+
this.roundRect(ctx, bx, by, w, h, 4);
|
|
18706
19125
|
ctx.fill();
|
|
19126
|
+
if (pointer !== "none") {
|
|
19127
|
+
ctx.beginPath();
|
|
19128
|
+
if (pointer === "down") {
|
|
19129
|
+
ctx.moveTo(px - ptr, by + h);
|
|
19130
|
+
ctx.lineTo(px + ptr, by + h);
|
|
19131
|
+
ctx.lineTo(px, py);
|
|
19132
|
+
} else if (pointer === "up") {
|
|
19133
|
+
ctx.moveTo(px - ptr, by);
|
|
19134
|
+
ctx.lineTo(px + ptr, by);
|
|
19135
|
+
ctx.lineTo(px, py);
|
|
19136
|
+
} else if (pointer === "left") {
|
|
19137
|
+
ctx.moveTo(bx, py - ptr);
|
|
19138
|
+
ctx.lineTo(bx, py + ptr);
|
|
19139
|
+
ctx.lineTo(px, py);
|
|
19140
|
+
} else {
|
|
19141
|
+
ctx.moveTo(bx + w, py - ptr);
|
|
19142
|
+
ctx.lineTo(bx + w, py + ptr);
|
|
19143
|
+
ctx.lineTo(px, py);
|
|
19144
|
+
}
|
|
19145
|
+
ctx.closePath();
|
|
19146
|
+
ctx.fill();
|
|
19147
|
+
}
|
|
18707
19148
|
}
|
|
18708
19149
|
if (lb.text) {
|
|
18709
|
-
ctx.fillStyle = lb.textColor ?? contrastColor(color);
|
|
18710
|
-
ctx.textAlign = "center";
|
|
19150
|
+
ctx.fillStyle = lb.textColor ?? (lb.noFill ? this.deps.theme.textColor : contrastColor(color));
|
|
18711
19151
|
ctx.textBaseline = "middle";
|
|
19152
|
+
let tx;
|
|
19153
|
+
if (lb.textAlign === "left") {
|
|
19154
|
+
ctx.textAlign = "left";
|
|
19155
|
+
tx = bx + padX;
|
|
19156
|
+
} else if (lb.textAlign === "right") {
|
|
19157
|
+
ctx.textAlign = "right";
|
|
19158
|
+
tx = bx + w - padX;
|
|
19159
|
+
} else {
|
|
19160
|
+
ctx.textAlign = "center";
|
|
19161
|
+
tx = bx + w / 2;
|
|
19162
|
+
}
|
|
18712
19163
|
const startY = by + padY + lineH / 2;
|
|
18713
|
-
for (let i = 0; i < lines.length; i += 1) ctx.fillText(lines[i],
|
|
19164
|
+
for (let i = 0; i < lines.length; i += 1) ctx.fillText(lines[i], tx, startY + i * lineH);
|
|
18714
19165
|
}
|
|
19166
|
+
return { x: bx, y: by, w, h };
|
|
18715
19167
|
}
|
|
18716
19168
|
roundRect(ctx, x, y, w, h, r) {
|
|
18717
19169
|
const rr = Math.min(r, w / 2, h / 2);
|
|
@@ -18947,7 +19399,7 @@ var ChromeRenderer = class {
|
|
|
18947
19399
|
this.ctx = null;
|
|
18948
19400
|
// The color for axis tick labels — the host-passed surface text, set each frame in render().
|
|
18949
19401
|
this.axisTextColor = DARK_THEME.textColor;
|
|
18950
|
-
// Shared Pine-drawing renderer
|
|
19402
|
+
// Shared Pine-drawing renderer, used here for autoscale geometry only; widthCache persists.
|
|
18951
19403
|
this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
|
|
18952
19404
|
}
|
|
18953
19405
|
mount(canvas) {
|
|
@@ -18972,8 +19424,8 @@ var ChromeRenderer = class {
|
|
|
18972
19424
|
*/
|
|
18973
19425
|
paneDrawingsRange(ownModels, scene, isPricePane, vr) {
|
|
18974
19426
|
let dr = null;
|
|
18975
|
-
for (const m of ownModels) dr = unionRange(dr, this.drawingsRange(
|
|
18976
|
-
if (isPricePane) for (const m of scene.indicators.values()) dr = unionRange(dr, this.drawingsRange(
|
|
19427
|
+
for (const m of ownModels) dr = unionRange(dr, this.drawingsRange(modelDrawingSet(m, false), vr, scene.offsetOf(m.id)));
|
|
19428
|
+
if (isPricePane) for (const m of scene.indicators.values()) dr = unionRange(dr, this.drawingsRange(modelDrawingSet(m, true), vr, scene.offsetOf(m.id)));
|
|
18977
19429
|
return dr;
|
|
18978
19430
|
}
|
|
18979
19431
|
/** Clear the chrome canvas and draw drawings + axes + current-price line.
|
|
@@ -19005,17 +19457,6 @@ var ChromeRenderer = class {
|
|
|
19005
19457
|
return;
|
|
19006
19458
|
}
|
|
19007
19459
|
const pricePane = panes.find((p) => p.kind === "price") ?? null;
|
|
19008
|
-
for (const pane of panes) {
|
|
19009
|
-
if (pane.collapsed) continue;
|
|
19010
|
-
for (const m of scene.indicatorsForPane(pane.id)) {
|
|
19011
|
-
const sc = scene.scaleFor(m, pane);
|
|
19012
|
-
const mp = sc === pane.scale ? pane : { ...pane, scale: sc };
|
|
19013
|
-
this.renderDrawings(ctx, coords, this.ownDrawings(m), mp, dataW, scene.offsetOf(m.id));
|
|
19014
|
-
}
|
|
19015
|
-
}
|
|
19016
|
-
if (pricePane) {
|
|
19017
|
-
for (const m of scene.indicators.values()) this.renderDrawings(ctx, coords, this.overlayDrawings(m), pricePane, dataW, scene.offsetOf(m.id));
|
|
19018
|
-
}
|
|
19019
19460
|
if (pricePane && !pricePane.collapsed && scene.tradeMarkers.visible) {
|
|
19020
19461
|
for (const m of scene.indicators.values()) {
|
|
19021
19462
|
if (m.trades?.length) this.renderTrades(ctx, coords, scene, theme, m.trades, pricePane, dataW);
|
|
@@ -19031,25 +19472,6 @@ var ChromeRenderer = class {
|
|
|
19031
19472
|
this.canvas = null;
|
|
19032
19473
|
this.ctx = null;
|
|
19033
19474
|
}
|
|
19034
|
-
// ── Pine-drawing helpers (own vs force_overlay routing) ──
|
|
19035
|
-
ownDrawings(m) {
|
|
19036
|
-
return {
|
|
19037
|
-
lines: (m.lines ?? []).filter((d) => !d.overlay),
|
|
19038
|
-
boxes: (m.boxes ?? []).filter((d) => !d.overlay),
|
|
19039
|
-
labels: (m.labels ?? []).filter((d) => !d.overlay),
|
|
19040
|
-
polylines: (m.polylines ?? []).filter((d) => !d.overlay),
|
|
19041
|
-
linefills: (m.linefills ?? []).filter((d) => !d.overlay)
|
|
19042
|
-
};
|
|
19043
|
-
}
|
|
19044
|
-
overlayDrawings(m) {
|
|
19045
|
-
return {
|
|
19046
|
-
lines: (m.lines ?? []).filter((d) => d.overlay),
|
|
19047
|
-
boxes: (m.boxes ?? []).filter((d) => d.overlay),
|
|
19048
|
-
labels: (m.labels ?? []).filter((d) => d.overlay),
|
|
19049
|
-
polylines: (m.polylines ?? []).filter((d) => d.overlay),
|
|
19050
|
-
linefills: (m.linefills ?? []).filter((d) => d.overlay)
|
|
19051
|
-
};
|
|
19052
|
-
}
|
|
19053
19475
|
drawingsRange(set, vr, indexOffset = 0) {
|
|
19054
19476
|
this.drawScene.setSet(set, indexOffset);
|
|
19055
19477
|
if (this.drawScene.isEmpty()) return null;
|
|
@@ -19083,23 +19505,6 @@ var ChromeRenderer = class {
|
|
|
19083
19505
|
);
|
|
19084
19506
|
ctx.restore();
|
|
19085
19507
|
}
|
|
19086
|
-
renderDrawings(ctx, coords, set, pane, dataW, indexOffset = 0) {
|
|
19087
|
-
this.drawScene.setSet(set, indexOffset);
|
|
19088
|
-
if (this.drawScene.isEmpty()) return;
|
|
19089
|
-
ctx.save();
|
|
19090
|
-
ctx.translate(0, pane.bounds.top);
|
|
19091
|
-
ctx.beginPath();
|
|
19092
|
-
ctx.rect(0, 0, dataW, pane.bounds.height);
|
|
19093
|
-
ctx.clip();
|
|
19094
|
-
this.drawScene.render(
|
|
19095
|
-
ctx,
|
|
19096
|
-
dataW,
|
|
19097
|
-
pane.bounds.height,
|
|
19098
|
-
(l) => coords.logicalToX(l),
|
|
19099
|
-
(price) => coords.priceToY(price, pane.scale, pane.bounds) - pane.bounds.top
|
|
19100
|
-
);
|
|
19101
|
-
ctx.restore();
|
|
19102
|
-
}
|
|
19103
19508
|
// ── axes ──
|
|
19104
19509
|
drawPriceAxes(ctx, scene, coords, theme, dataW, panes) {
|
|
19105
19510
|
ctx.strokeStyle = scene.style.borderColor ?? theme.borderColor;
|
|
@@ -19315,6 +19720,68 @@ function formatCountdown(ms) {
|
|
|
19315
19720
|
return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`;
|
|
19316
19721
|
}
|
|
19317
19722
|
|
|
19723
|
+
// src/renderers/native/chrome/LabelTooltip.ts
|
|
19724
|
+
var HOVER_DELAY_MS = 350;
|
|
19725
|
+
var LabelTooltip = class {
|
|
19726
|
+
constructor(plot, deps) {
|
|
19727
|
+
this.plot = plot;
|
|
19728
|
+
this.deps = deps;
|
|
19729
|
+
this.tip = null;
|
|
19730
|
+
this.timer = null;
|
|
19731
|
+
/** Text of the currently open OR armed tip — dedupes moves inside one label. */
|
|
19732
|
+
this.current = null;
|
|
19733
|
+
this.onMove = (e) => {
|
|
19734
|
+
if (e.pointerType !== "mouse") return;
|
|
19735
|
+
const rect = this.plot.getBoundingClientRect();
|
|
19736
|
+
const x = e.clientX - rect.left;
|
|
19737
|
+
const y = e.clientY - rect.top;
|
|
19738
|
+
const text = this.deps.lookup(x, y);
|
|
19739
|
+
if (!text) {
|
|
19740
|
+
this.clear();
|
|
19741
|
+
return;
|
|
19742
|
+
}
|
|
19743
|
+
if (text === this.current) return;
|
|
19744
|
+
this.clear();
|
|
19745
|
+
this.current = text;
|
|
19746
|
+
this.timer = window.setTimeout(() => this.show(text, x, y), HOVER_DELAY_MS);
|
|
19747
|
+
};
|
|
19748
|
+
this.onLeave = () => {
|
|
19749
|
+
this.clear();
|
|
19750
|
+
};
|
|
19751
|
+
plot.addEventListener("pointermove", this.onMove);
|
|
19752
|
+
plot.addEventListener("pointerleave", this.onLeave);
|
|
19753
|
+
plot.addEventListener("pointerdown", this.onLeave);
|
|
19754
|
+
}
|
|
19755
|
+
destroy() {
|
|
19756
|
+
this.plot.removeEventListener("pointermove", this.onMove);
|
|
19757
|
+
this.plot.removeEventListener("pointerleave", this.onLeave);
|
|
19758
|
+
this.plot.removeEventListener("pointerdown", this.onLeave);
|
|
19759
|
+
this.clear();
|
|
19760
|
+
}
|
|
19761
|
+
show(text, x, y) {
|
|
19762
|
+
const doc = this.plot.ownerDocument;
|
|
19763
|
+
const tip = doc.createElement("div");
|
|
19764
|
+
tip.textContent = text;
|
|
19765
|
+
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;";
|
|
19766
|
+
applyChromeTokens(tip, this.deps.theme());
|
|
19767
|
+
this.plot.appendChild(tip);
|
|
19768
|
+
const pw = this.plot.clientWidth;
|
|
19769
|
+
const ph = this.plot.clientHeight;
|
|
19770
|
+
tip.style.left = `${Math.max(0, Math.min(x + 12, pw - tip.offsetWidth - 4))}px`;
|
|
19771
|
+
tip.style.top = `${Math.max(0, Math.min(y + 16, ph - tip.offsetHeight - 4))}px`;
|
|
19772
|
+
this.tip = tip;
|
|
19773
|
+
}
|
|
19774
|
+
clear() {
|
|
19775
|
+
if (this.timer !== null) {
|
|
19776
|
+
clearTimeout(this.timer);
|
|
19777
|
+
this.timer = null;
|
|
19778
|
+
}
|
|
19779
|
+
this.tip?.remove();
|
|
19780
|
+
this.tip = null;
|
|
19781
|
+
this.current = null;
|
|
19782
|
+
}
|
|
19783
|
+
};
|
|
19784
|
+
|
|
19318
19785
|
// src/renderers/native/chrome/CrosshairRenderer.ts
|
|
19319
19786
|
var CrosshairRenderer = class {
|
|
19320
19787
|
constructor() {
|
|
@@ -23446,17 +23913,17 @@ function glyphIcon(glyph) {
|
|
|
23446
23913
|
return textGlyph(String(glyph), 15);
|
|
23447
23914
|
}
|
|
23448
23915
|
function stampSizeIcon(size) {
|
|
23449
|
-
return textGlyph("\u25CF", (
|
|
23916
|
+
return textGlyph("\u25CF", (SIZE_PX3[String(size)] ?? 13) + 4);
|
|
23450
23917
|
}
|
|
23451
23918
|
function sizeIcon(size) {
|
|
23452
23919
|
return textGlyph(String(size).charAt(0).toUpperCase(), 15);
|
|
23453
23920
|
}
|
|
23454
|
-
var
|
|
23921
|
+
var SIZE_PX3 = { small: 10, normal: 13, large: 16, huge: 20 };
|
|
23455
23922
|
function numbersSizeIcon(size) {
|
|
23456
|
-
return textGlyph("12", (
|
|
23923
|
+
return textGlyph("12", (SIZE_PX3[String(size)] ?? 13) - 1, 16.5, 'font-weight="600"');
|
|
23457
23924
|
}
|
|
23458
23925
|
function labelSizeIcon(size) {
|
|
23459
|
-
return textGlyph("T", (
|
|
23926
|
+
return textGlyph("T", (SIZE_PX3[String(size)] ?? 13) + 2);
|
|
23460
23927
|
}
|
|
23461
23928
|
function capitalize(s) {
|
|
23462
23929
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
@@ -23620,7 +24087,7 @@ var DrawingToolbar = class {
|
|
|
23620
24087
|
this.root.replaceChildren();
|
|
23621
24088
|
this.groupCells.clear();
|
|
23622
24089
|
this.groupIcons.clear();
|
|
23623
|
-
this.cursorBtn = this.makeButton(CURSOR_ICON, "Cursor", () => this.
|
|
24090
|
+
this.cursorBtn = this.makeButton(CURSOR_ICON, "Cursor", () => this.onCursorClick());
|
|
23624
24091
|
this.root.appendChild(this.cursorBtn);
|
|
23625
24092
|
if (this.def.groups.length > 0) this.root.appendChild(this.divider());
|
|
23626
24093
|
for (const g of this.def.groups) {
|
|
@@ -23724,6 +24191,14 @@ var DrawingToolbar = class {
|
|
|
23724
24191
|
this.magnetIcon = icon2;
|
|
23725
24192
|
return cell;
|
|
23726
24193
|
}
|
|
24194
|
+
/** Cursor returns to select/idle: an active measure/eraser mode exits through its own
|
|
24195
|
+
* toggle callback (disarming a tool via `onArm(null)` alone can't — the host treats a
|
|
24196
|
+
* null arm as a no-op side effect of entering those modes), then the tool disarms. */
|
|
24197
|
+
onCursorClick() {
|
|
24198
|
+
if (this.measureActive) this.onMeasure();
|
|
24199
|
+
if (this.eraserActive) this.onEraser();
|
|
24200
|
+
this.onArm(null);
|
|
24201
|
+
}
|
|
23727
24202
|
/** Clicking the icon arms the group's last-used tool (it does NOT open the flyout). */
|
|
23728
24203
|
onGroupIconClick(group) {
|
|
23729
24204
|
const type = this.lastUsed.get(group.id) ?? group.tools[0]?.type;
|
|
@@ -25055,6 +25530,315 @@ var UserDrawingController = class {
|
|
|
25055
25530
|
}
|
|
25056
25531
|
};
|
|
25057
25532
|
|
|
25533
|
+
// src/renderers/shared/TableOverlay.ts
|
|
25534
|
+
var SIZE_PX4 = {
|
|
25535
|
+
auto: 13,
|
|
25536
|
+
tiny: 10,
|
|
25537
|
+
small: 11,
|
|
25538
|
+
normal: 13,
|
|
25539
|
+
large: 16,
|
|
25540
|
+
huge: 20
|
|
25541
|
+
};
|
|
25542
|
+
function fontPxOf(size) {
|
|
25543
|
+
if (typeof size === "number") return size > 0 ? size : SIZE_PX4.auto;
|
|
25544
|
+
return SIZE_PX4[size] ?? SIZE_PX4.auto;
|
|
25545
|
+
}
|
|
25546
|
+
function tableHasContent(t) {
|
|
25547
|
+
return t.cells.some((row) => row?.some((c) => c != null && !c.merged));
|
|
25548
|
+
}
|
|
25549
|
+
function mergeRenderPlan(t) {
|
|
25550
|
+
const span = /* @__PURE__ */ new Map();
|
|
25551
|
+
const omit = /* @__PURE__ */ new Set();
|
|
25552
|
+
for (const m of t.merges) {
|
|
25553
|
+
span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
|
|
25554
|
+
for (let r = m.startRow; r <= m.endRow; r += 1) {
|
|
25555
|
+
for (let c = m.startCol; c <= m.endCol; c += 1) {
|
|
25556
|
+
if (r !== m.startRow || c !== m.startCol) omit.add(`${r}:${c}`);
|
|
25557
|
+
}
|
|
25558
|
+
}
|
|
25559
|
+
}
|
|
25560
|
+
for (let r = 0; r < t.rows; r += 1) {
|
|
25561
|
+
for (let c = 0; c < t.columns; c += 1) {
|
|
25562
|
+
if (t.cells[r]?.[c]?.merged && !span.has(`${r}:${c}`)) omit.add(`${r}:${c}`);
|
|
25563
|
+
}
|
|
25564
|
+
}
|
|
25565
|
+
for (const key of span.keys()) omit.delete(key);
|
|
25566
|
+
return { span, omit };
|
|
25567
|
+
}
|
|
25568
|
+
|
|
25569
|
+
// src/renderers/shared/TableCanvasRenderer.ts
|
|
25570
|
+
var PAD_X = 6;
|
|
25571
|
+
var PAD_Y = 2;
|
|
25572
|
+
var MARGIN = 6;
|
|
25573
|
+
var LINE_HEIGHT = 1.2;
|
|
25574
|
+
function paintTable(ctx, t, args, tips) {
|
|
25575
|
+
if (!tableHasContent(t)) return;
|
|
25576
|
+
const layout = layoutTable(ctx, t, args);
|
|
25577
|
+
if (!layout || layout.w <= 0 || layout.h <= 0) return;
|
|
25578
|
+
const fw = t.frameColor && t.frameWidth > 0 ? t.frameWidth : 0;
|
|
25579
|
+
const { x, y } = anchorOrigin(t.position, layout.w + 2 * fw, layout.h + 2 * fw, args);
|
|
25580
|
+
const x0 = x + fw;
|
|
25581
|
+
const y0 = y + fw;
|
|
25582
|
+
if (t.bgColor) {
|
|
25583
|
+
ctx.fillStyle = t.bgColor;
|
|
25584
|
+
ctx.fillRect(x0, y0, layout.w, layout.h);
|
|
25585
|
+
}
|
|
25586
|
+
if (fw > 0 && t.frameColor) {
|
|
25587
|
+
ctx.strokeStyle = t.frameColor;
|
|
25588
|
+
ctx.lineWidth = fw;
|
|
25589
|
+
ctx.strokeRect(x + fw / 2, y + fw / 2, layout.w + fw, layout.h + fw);
|
|
25590
|
+
}
|
|
25591
|
+
const colX = [0];
|
|
25592
|
+
for (const w of layout.colW) colX.push(colX[colX.length - 1] + w);
|
|
25593
|
+
const rowY = [0];
|
|
25594
|
+
for (const h of layout.rowH) rowY.push(rowY[rowY.length - 1] + h);
|
|
25595
|
+
const prevBaseline = ctx.textBaseline;
|
|
25596
|
+
const prevAlign = ctx.textAlign;
|
|
25597
|
+
ctx.textBaseline = "middle";
|
|
25598
|
+
for (const box of layout.boxes) {
|
|
25599
|
+
const rx = x0 + colX[box.c];
|
|
25600
|
+
const ry = y0 + rowY[box.r];
|
|
25601
|
+
const rw = colX[box.c + box.cs] - colX[box.c];
|
|
25602
|
+
const rh = rowY[box.r + box.rs] - rowY[box.r];
|
|
25603
|
+
const cell = box.cell;
|
|
25604
|
+
if (cell.bgColor) {
|
|
25605
|
+
ctx.fillStyle = cell.bgColor;
|
|
25606
|
+
ctx.fillRect(rx, ry, rw, rh);
|
|
25607
|
+
}
|
|
25608
|
+
const text = cell.text ?? "";
|
|
25609
|
+
if (text.length > 0) {
|
|
25610
|
+
const px = fontPxOf(cell.textSize);
|
|
25611
|
+
ctx.font = cellFont(cell, px, args.theme);
|
|
25612
|
+
ctx.fillStyle = cell.textColor ?? args.theme.textColor;
|
|
25613
|
+
const lines = text.split("\n");
|
|
25614
|
+
const blockH = lines.length * px * LINE_HEIGHT;
|
|
25615
|
+
const blockTop = cell.vAlign === "top" ? ry + PAD_Y : cell.vAlign === "bottom" ? ry + rh - PAD_Y - blockH : ry + (rh - blockH) / 2;
|
|
25616
|
+
const tx = cell.hAlign === "left" ? rx + PAD_X : cell.hAlign === "right" ? rx + rw - PAD_X : rx + rw / 2;
|
|
25617
|
+
ctx.textAlign = cell.hAlign;
|
|
25618
|
+
lines.forEach((line, i) => ctx.fillText(line, tx, blockTop + (i + 0.5) * px * LINE_HEIGHT));
|
|
25619
|
+
}
|
|
25620
|
+
if (cell.tooltip) tips.push({ left: rx, top: ry, right: rx + rw, bottom: ry + rh, text: cell.tooltip });
|
|
25621
|
+
}
|
|
25622
|
+
ctx.textBaseline = prevBaseline;
|
|
25623
|
+
ctx.textAlign = prevAlign;
|
|
25624
|
+
if (t.borderColor && t.borderWidth > 0) {
|
|
25625
|
+
ctx.strokeStyle = t.borderColor;
|
|
25626
|
+
ctx.lineWidth = t.borderWidth;
|
|
25627
|
+
const seen = /* @__PURE__ */ new Set();
|
|
25628
|
+
ctx.beginPath();
|
|
25629
|
+
const edge = (ax, ay, bx, by) => {
|
|
25630
|
+
const key = `${ax},${ay},${bx},${by}`;
|
|
25631
|
+
if (seen.has(key)) return;
|
|
25632
|
+
seen.add(key);
|
|
25633
|
+
ctx.moveTo(ax, ay);
|
|
25634
|
+
ctx.lineTo(bx, by);
|
|
25635
|
+
};
|
|
25636
|
+
for (const box of layout.boxes) {
|
|
25637
|
+
const l = Math.round(x0 + colX[box.c]);
|
|
25638
|
+
const r = Math.round(x0 + colX[box.c + box.cs]);
|
|
25639
|
+
const tp = Math.round(y0 + rowY[box.r]);
|
|
25640
|
+
const bt = Math.round(y0 + rowY[box.r + box.rs]);
|
|
25641
|
+
edge(l, tp, r, tp);
|
|
25642
|
+
edge(l, bt, r, bt);
|
|
25643
|
+
edge(l, tp, l, bt);
|
|
25644
|
+
edge(r, tp, r, bt);
|
|
25645
|
+
}
|
|
25646
|
+
ctx.stroke();
|
|
25647
|
+
}
|
|
25648
|
+
}
|
|
25649
|
+
function layoutTable(ctx, t, args) {
|
|
25650
|
+
const { span, omit } = mergeRenderPlan(t);
|
|
25651
|
+
const colW = new Array(t.columns).fill(0);
|
|
25652
|
+
const rowH = new Array(t.rows).fill(0);
|
|
25653
|
+
const boxes = [];
|
|
25654
|
+
for (let r = 0; r < t.rows; r += 1) {
|
|
25655
|
+
for (let c = 0; c < t.columns; c += 1) {
|
|
25656
|
+
if (omit.has(`${r}:${c}`)) continue;
|
|
25657
|
+
const cell = t.cells[r]?.[c];
|
|
25658
|
+
if (cell == null) continue;
|
|
25659
|
+
const sp = span.get(`${r}:${c}`);
|
|
25660
|
+
boxes.push({ cell, r, c, cs: Math.min(sp?.cs ?? 1, t.columns - c), rs: Math.min(sp?.rs ?? 1, t.rows - r) });
|
|
25661
|
+
}
|
|
25662
|
+
}
|
|
25663
|
+
if (boxes.length === 0) return null;
|
|
25664
|
+
const sizeOf = (cell) => {
|
|
25665
|
+
const px = fontPxOf(cell.textSize);
|
|
25666
|
+
ctx.font = cellFont(cell, px, args.theme);
|
|
25667
|
+
const lines = (cell.text ?? "").split("\n");
|
|
25668
|
+
let maxW = 0;
|
|
25669
|
+
for (const line of lines) maxW = Math.max(maxW, ctx.measureText(line).width);
|
|
25670
|
+
let w2 = Math.ceil(maxW) + 2 * PAD_X;
|
|
25671
|
+
let h2 = Math.ceil(lines.length * px * LINE_HEIGHT) + 2 * PAD_Y;
|
|
25672
|
+
if (cell.width) w2 = Math.max(w2, cell.width / 100 * args.plotWidth);
|
|
25673
|
+
if (cell.height) h2 = Math.max(h2, cell.height / 100 * args.paneHeight);
|
|
25674
|
+
return { w: w2, h: h2 };
|
|
25675
|
+
};
|
|
25676
|
+
const spanning = [];
|
|
25677
|
+
for (const box of boxes) {
|
|
25678
|
+
const { w: w2, h: h2 } = sizeOf(box.cell);
|
|
25679
|
+
if (box.cs === 1) colW[box.c] = Math.max(colW[box.c], w2);
|
|
25680
|
+
if (box.rs === 1) rowH[box.r] = Math.max(rowH[box.r], h2);
|
|
25681
|
+
if (box.cs > 1 || box.rs > 1) spanning.push({ box, w: w2, h: h2 });
|
|
25682
|
+
}
|
|
25683
|
+
for (const { box, w: w2, h: h2 } of spanning) {
|
|
25684
|
+
if (box.cs > 1) {
|
|
25685
|
+
let sum = 0;
|
|
25686
|
+
for (let c = box.c; c < box.c + box.cs; c += 1) sum += colW[c];
|
|
25687
|
+
if (w2 > sum) for (let c = box.c; c < box.c + box.cs; c += 1) colW[c] += (w2 - sum) / box.cs;
|
|
25688
|
+
}
|
|
25689
|
+
if (box.rs > 1) {
|
|
25690
|
+
let sum = 0;
|
|
25691
|
+
for (let r = box.r; r < box.r + box.rs; r += 1) sum += rowH[r];
|
|
25692
|
+
if (h2 > sum) for (let r = box.r; r < box.r + box.rs; r += 1) rowH[r] += (h2 - sum) / box.rs;
|
|
25693
|
+
}
|
|
25694
|
+
}
|
|
25695
|
+
let w = 0;
|
|
25696
|
+
for (const cw of colW) w += cw;
|
|
25697
|
+
let h = 0;
|
|
25698
|
+
for (const rh of rowH) h += rh;
|
|
25699
|
+
return { colW, rowH, w, h, boxes };
|
|
25700
|
+
}
|
|
25701
|
+
function anchorOrigin(position, totalW, totalH, args) {
|
|
25702
|
+
let y;
|
|
25703
|
+
if (position.startsWith("top")) y = MARGIN;
|
|
25704
|
+
else if (position.startsWith("bottom")) y = args.paneHeight - MARGIN - totalH;
|
|
25705
|
+
else y = args.paneHeight / 2 - totalH / 2;
|
|
25706
|
+
let x;
|
|
25707
|
+
if (position.endsWith("left")) x = MARGIN;
|
|
25708
|
+
else if (position.endsWith("right")) x = args.plotWidth - MARGIN - totalW;
|
|
25709
|
+
else x = args.plotWidth / 2 - totalW / 2;
|
|
25710
|
+
return { x, y };
|
|
25711
|
+
}
|
|
25712
|
+
function cellFont(cell, px, theme) {
|
|
25713
|
+
const family = cell.fontFamily === "monospace" ? "monospace" : theme.fontFamily || "sans-serif";
|
|
25714
|
+
return `${cell.italic ? "italic " : ""}${cell.bold ? "bold " : ""}${px}px ${family}`;
|
|
25715
|
+
}
|
|
25716
|
+
|
|
25717
|
+
// src/renderers/native/drawings/IndicatorDrawingSlices.ts
|
|
25718
|
+
function indicatorSliceKey(z, boundaries) {
|
|
25719
|
+
return boundaries.find((b) => b > z) ?? Infinity;
|
|
25720
|
+
}
|
|
25721
|
+
var IndicatorDrawingSlices = class {
|
|
25722
|
+
constructor() {
|
|
25723
|
+
this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
|
|
25724
|
+
/** Slice canvas cache, keyed `paneId|beforeZ` — same lifecycle as the user-drawing cache. */
|
|
25725
|
+
this.sliceCache = /* @__PURE__ */ new Map();
|
|
25726
|
+
/** Tooltip hit-rects of every label drawn this frame, in plot coords (rebuilt per prepare). */
|
|
25727
|
+
this.tips = [];
|
|
25728
|
+
}
|
|
25729
|
+
/**
|
|
25730
|
+
* Rebuild the per-indicator drawing slices for this data frame. `ref` is the data
|
|
25731
|
+
* canvas the slices must match pixel-for-pixel (the backend composites them 1:1).
|
|
25732
|
+
* Runs from the renderer's data paint, just before the backend composites the scene.
|
|
25733
|
+
*/
|
|
25734
|
+
prepare(scene, coords, theme, ref) {
|
|
25735
|
+
this.tips = [];
|
|
25736
|
+
const out = /* @__PURE__ */ new Map();
|
|
25737
|
+
if (ref.width === 0 || ref.height === 0) {
|
|
25738
|
+
this.sliceCache.clear();
|
|
25739
|
+
return out;
|
|
25740
|
+
}
|
|
25741
|
+
this.drawScene.setDeps({
|
|
25742
|
+
timeToLogical: (ms) => coords.timeToLogical(ms),
|
|
25743
|
+
barAt: (logical) => {
|
|
25744
|
+
const b = scene.bars[Math.round(logical)];
|
|
25745
|
+
return b ? { high: b.high, low: b.low } : null;
|
|
25746
|
+
},
|
|
25747
|
+
theme
|
|
25748
|
+
});
|
|
25749
|
+
const dpr = coords.dpr;
|
|
25750
|
+
const dataW = coords.width;
|
|
25751
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
25752
|
+
const add = (paneId, beforeZ, entry) => {
|
|
25753
|
+
const key = `${paneId}|${beforeZ}`;
|
|
25754
|
+
const bucket = buckets.get(key);
|
|
25755
|
+
if (bucket) bucket.entries.push(entry);
|
|
25756
|
+
else buckets.set(key, { paneId, beforeZ, entries: [entry] });
|
|
25757
|
+
};
|
|
25758
|
+
for (const pane of scene.orderedPanes()) {
|
|
25759
|
+
if (pane.collapsed) continue;
|
|
25760
|
+
const boundaries = scene.seriesBoundaries(pane.id);
|
|
25761
|
+
for (const m of scene.orderedIndicatorsForPane(pane.id)) {
|
|
25762
|
+
const set = modelDrawingSet(m, false);
|
|
25763
|
+
const tables = (m.tables ?? []).filter((t) => !t.overlay);
|
|
25764
|
+
if (drawingSetEmpty(set) && tables.length === 0) continue;
|
|
25765
|
+
const sc = scene.scaleFor(m, pane);
|
|
25766
|
+
const mp = sc === pane.scale ? pane : { ...pane, scale: sc };
|
|
25767
|
+
const beforeZ = indicatorSliceKey(scene.zOf(m.id), boundaries);
|
|
25768
|
+
add(pane.id, beforeZ, { set, tables, pane: mp, indexOffset: scene.offsetOf(m.id) });
|
|
25769
|
+
}
|
|
25770
|
+
if (pane.kind === "price") {
|
|
25771
|
+
for (const m of scene.indicators.values()) {
|
|
25772
|
+
const set = modelDrawingSet(m, true);
|
|
25773
|
+
const tables = (m.tables ?? []).filter((t) => t.overlay === true);
|
|
25774
|
+
if (drawingSetEmpty(set) && tables.length === 0) continue;
|
|
25775
|
+
add(pane.id, Infinity, { set, tables, pane, indexOffset: scene.offsetOf(m.id) });
|
|
25776
|
+
}
|
|
25777
|
+
}
|
|
25778
|
+
}
|
|
25779
|
+
for (const [key, { paneId, beforeZ, entries }] of buckets) {
|
|
25780
|
+
let canvas = this.sliceCache.get(key);
|
|
25781
|
+
if (!canvas) {
|
|
25782
|
+
canvas = document.createElement("canvas");
|
|
25783
|
+
this.sliceCache.set(key, canvas);
|
|
25784
|
+
}
|
|
25785
|
+
if (canvas.width !== ref.width || canvas.height !== ref.height) {
|
|
25786
|
+
canvas.width = ref.width;
|
|
25787
|
+
canvas.height = ref.height;
|
|
25788
|
+
}
|
|
25789
|
+
const ctx = canvas.getContext("2d");
|
|
25790
|
+
if (!ctx) continue;
|
|
25791
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
25792
|
+
ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
|
|
25793
|
+
for (const e of entries) this.paintEntry(ctx, e, coords, dataW, theme);
|
|
25794
|
+
const slices = out.get(paneId) ?? [];
|
|
25795
|
+
slices.push({ beforeZ, canvas });
|
|
25796
|
+
out.set(paneId, slices);
|
|
25797
|
+
}
|
|
25798
|
+
for (const key of [...this.sliceCache.keys()]) if (!buckets.has(key)) this.sliceCache.delete(key);
|
|
25799
|
+
for (const slices of out.values()) slices.sort((a, b) => a.beforeZ - b.beforeZ);
|
|
25800
|
+
return out;
|
|
25801
|
+
}
|
|
25802
|
+
paintEntry(ctx, e, coords, dataW, theme) {
|
|
25803
|
+
const { pane } = e;
|
|
25804
|
+
const paneTips = [];
|
|
25805
|
+
ctx.save();
|
|
25806
|
+
ctx.translate(0, pane.bounds.top);
|
|
25807
|
+
ctx.beginPath();
|
|
25808
|
+
ctx.rect(0, 0, dataW, pane.bounds.height);
|
|
25809
|
+
ctx.clip();
|
|
25810
|
+
this.drawScene.setSet(e.set, e.indexOffset);
|
|
25811
|
+
this.drawScene.render(
|
|
25812
|
+
ctx,
|
|
25813
|
+
dataW,
|
|
25814
|
+
pane.bounds.height,
|
|
25815
|
+
(l) => coords.logicalToX(l),
|
|
25816
|
+
(price) => coords.priceToY(price, pane.scale, pane.bounds) - pane.bounds.top
|
|
25817
|
+
);
|
|
25818
|
+
paneTips.push(...this.drawScene.labelTipRegions());
|
|
25819
|
+
for (const t of e.tables) paintTable(ctx, t, { paneHeight: pane.bounds.height, plotWidth: dataW, theme }, paneTips);
|
|
25820
|
+
ctx.restore();
|
|
25821
|
+
for (const r of paneTips) {
|
|
25822
|
+
this.tips.push({ ...r, top: r.top + pane.bounds.top, bottom: r.bottom + pane.bounds.top });
|
|
25823
|
+
}
|
|
25824
|
+
}
|
|
25825
|
+
/** Tooltip of the topmost label or table cell under a plot-space point, or null. Fed by the last prepare. */
|
|
25826
|
+
labelTooltipAt(x, y) {
|
|
25827
|
+
for (let i = this.tips.length - 1; i >= 0; i -= 1) {
|
|
25828
|
+
const r = this.tips[i];
|
|
25829
|
+
if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return r.text;
|
|
25830
|
+
}
|
|
25831
|
+
return null;
|
|
25832
|
+
}
|
|
25833
|
+
};
|
|
25834
|
+
function mergeSlices(indicator, user) {
|
|
25835
|
+
const out = /* @__PURE__ */ new Map();
|
|
25836
|
+
for (const [paneId, slices] of indicator) out.set(paneId, [...slices]);
|
|
25837
|
+
for (const [paneId, slices] of user) out.set(paneId, [...out.get(paneId) ?? [], ...slices]);
|
|
25838
|
+
for (const slices of out.values()) slices.sort((a, b) => a.beforeZ - b.beforeZ);
|
|
25839
|
+
return out;
|
|
25840
|
+
}
|
|
25841
|
+
|
|
25058
25842
|
// src/renderers/native/drawings/Projector.ts
|
|
25059
25843
|
function createProjector(coords, paneOf, paneIdAtY, barsInRange) {
|
|
25060
25844
|
return {
|
|
@@ -25106,19 +25890,8 @@ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log =
|
|
|
25106
25890
|
for (const model of models) {
|
|
25107
25891
|
const off = offsetOf(model.id);
|
|
25108
25892
|
for (const s of model.series) {
|
|
25109
|
-
if (s.
|
|
25110
|
-
|
|
25111
|
-
const b = s.bars[i - off];
|
|
25112
|
-
if (b) {
|
|
25113
|
-
consider(b.high);
|
|
25114
|
-
consider(b.low);
|
|
25115
|
-
}
|
|
25116
|
-
}
|
|
25117
|
-
} else if (isLineLikeSeries(s)) {
|
|
25118
|
-
for (let i = i0; i <= i1; i += 1) consider(s.points[i - off]?.value);
|
|
25119
|
-
if (s.kind === "histogram" || s.kind === "columns") consider(s.style?.base ?? 0);
|
|
25120
|
-
else if (s.style?.base != null) consider(s.style.base);
|
|
25121
|
-
}
|
|
25893
|
+
if (s.overlay === true) continue;
|
|
25894
|
+
considerSeries(s, i0, i1, off, consider);
|
|
25122
25895
|
}
|
|
25123
25896
|
for (const pl of model.priceLines) consider(pl.price);
|
|
25124
25897
|
}
|
|
@@ -25140,6 +25913,36 @@ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log =
|
|
|
25140
25913
|
const span = max - min;
|
|
25141
25914
|
return { min: min - span * MARGIN_BOTTOM, max: max + span * MARGIN_TOP };
|
|
25142
25915
|
}
|
|
25916
|
+
function considerSeries(s, i0, i1, off, consider) {
|
|
25917
|
+
if (s.kind === "candle" || s.kind === "bar") {
|
|
25918
|
+
for (let i = i0; i <= i1; i += 1) {
|
|
25919
|
+
const b = s.bars[i - off];
|
|
25920
|
+
if (b) {
|
|
25921
|
+
consider(b.high);
|
|
25922
|
+
consider(b.low);
|
|
25923
|
+
}
|
|
25924
|
+
}
|
|
25925
|
+
} else if (isLineLikeSeries(s)) {
|
|
25926
|
+
for (let i = i0; i <= i1; i += 1) consider(s.points[i - off]?.value);
|
|
25927
|
+
if (s.kind === "histogram" || s.kind === "columns") consider(s.style?.base ?? 0);
|
|
25928
|
+
else if (s.style?.base != null) consider(s.style.base);
|
|
25929
|
+
}
|
|
25930
|
+
}
|
|
25931
|
+
function overlaySeriesRange(models, i0, i1, offsetOf = () => 0) {
|
|
25932
|
+
let min = Infinity;
|
|
25933
|
+
let max = -Infinity;
|
|
25934
|
+
const consider = (v) => {
|
|
25935
|
+
if (v != null && Number.isFinite(v)) {
|
|
25936
|
+
if (v < min) min = v;
|
|
25937
|
+
if (v > max) max = v;
|
|
25938
|
+
}
|
|
25939
|
+
};
|
|
25940
|
+
for (const model of models) {
|
|
25941
|
+
const off = offsetOf(model.id);
|
|
25942
|
+
for (const s of model.series) if (s.overlay === true) considerSeries(s, i0, i1, off, consider);
|
|
25943
|
+
}
|
|
25944
|
+
return min === Infinity ? null : { min, max };
|
|
25945
|
+
}
|
|
25143
25946
|
function expandScaleByPixels(scale, heightPx, abovePx, belowPx) {
|
|
25144
25947
|
if (abovePx <= 0 && belowPx <= 0) return scale;
|
|
25145
25948
|
const content = heightPx - abovePx - belowPx;
|
|
@@ -25915,6 +26718,11 @@ var NativeRenderer = class {
|
|
|
25915
26718
|
this.vpvrRenderer = new VpvrRenderer();
|
|
25916
26719
|
this.resizeObserver = null;
|
|
25917
26720
|
this.dprMedia = null;
|
|
26721
|
+
/** Plot size in INTEGER device px, as last reported by the resize observer's
|
|
26722
|
+
* device-pixel-content-box — the browser's own statement of how many device pixels
|
|
26723
|
+
* it paints the plot into. `null` until the first report or where the box type is
|
|
26724
|
+
* unsupported (WebKit); syncSize then falls back to rounding the client rect. */
|
|
26725
|
+
this.plotDeviceSize = null;
|
|
25918
26726
|
this.coords = new CoordinateSystem();
|
|
25919
26727
|
this.scene = new SceneGraph();
|
|
25920
26728
|
// chosen at mount (WebGL2 if available, else canvas2d)
|
|
@@ -25922,6 +26730,10 @@ var NativeRenderer = class {
|
|
|
25922
26730
|
this.glowAmount = 0;
|
|
25923
26731
|
// WebGL2 neon-glow intensity (canvas2d ignores it)
|
|
25924
26732
|
this.chrome = new ChromeRenderer();
|
|
26733
|
+
/** Prepaints each indicator's Pine drawings into interleave slices at the model's z. */
|
|
26734
|
+
this.indicatorSlices = new IndicatorDrawingSlices();
|
|
26735
|
+
/** Hover tooltips for Pine labels (canvas hit-rects collected by the chrome layer). */
|
|
26736
|
+
this.labelTooltip = null;
|
|
25925
26737
|
this.crosshairLayer = new CrosshairRenderer();
|
|
25926
26738
|
/** 1 Hz repaint pump so the price-axis countdown-to-bar-close ticks; null when off. */
|
|
25927
26739
|
this.countdownTimer = null;
|
|
@@ -25932,6 +26744,8 @@ var NativeRenderer = class {
|
|
|
25932
26744
|
this.indicatorValuesOn = true;
|
|
25933
26745
|
/** Host-contributed legend actions — held here so a rebuild of the legend re-wires them. */
|
|
25934
26746
|
this.legendActionsProvider = null;
|
|
26747
|
+
/** Host-contributed legend callouts — held here so a rebuild of the legend re-wires them. */
|
|
26748
|
+
this.legendCalloutsProvider = null;
|
|
25935
26749
|
/** Host override of the legend's fold toggle — held here so a remount re-applies it. */
|
|
25936
26750
|
this.legendOverviewAction = null;
|
|
25937
26751
|
// ── keyboard navigation / accessibility (item 11) ──
|
|
@@ -26066,7 +26880,6 @@ var NativeRenderer = class {
|
|
|
26066
26880
|
this.toggleVisibleCbs = /* @__PURE__ */ new Set();
|
|
26067
26881
|
this.moveIndicatorCbs = /* @__PURE__ */ new Set();
|
|
26068
26882
|
this.priceStyleCbs = /* @__PURE__ */ new Set();
|
|
26069
|
-
this.tableOverlays = /* @__PURE__ */ new Map();
|
|
26070
26883
|
this.name = "native";
|
|
26071
26884
|
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"];
|
|
26072
26885
|
/** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
|
|
@@ -26380,7 +27193,6 @@ var NativeRenderer = class {
|
|
|
26380
27193
|
* hidden) on clear so a re-show picks up the current theme.
|
|
26381
27194
|
*/
|
|
26382
27195
|
setLoading(loading) {
|
|
26383
|
-
for (const overlay of this.tableOverlays.values()) overlay.setVisible(!loading);
|
|
26384
27196
|
if (!loading || !this.wrapper) {
|
|
26385
27197
|
this.loadingEl?.remove();
|
|
26386
27198
|
this.loadingEl = null;
|
|
@@ -27040,6 +27852,10 @@ var NativeRenderer = class {
|
|
|
27040
27852
|
this.plot.appendChild(this.scrollButton);
|
|
27041
27853
|
this.plot.addEventListener("pointermove", this.onScrollProximityMove);
|
|
27042
27854
|
this.plot.addEventListener("pointerleave", this.onScrollProximityLeave);
|
|
27855
|
+
this.labelTooltip = new LabelTooltip(this.plot, {
|
|
27856
|
+
theme: () => this.chromeTheme(),
|
|
27857
|
+
lookup: (x, y) => this.indicatorSlices.labelTooltipAt(x, y)
|
|
27858
|
+
});
|
|
27043
27859
|
this.userDrawings = new UserDrawingController(this.wrapper, this.plot, this.drawingsCanvas, {
|
|
27044
27860
|
projector: () => this.drawingProjector(),
|
|
27045
27861
|
dpr: () => this.coords.dpr,
|
|
@@ -27079,9 +27895,10 @@ var NativeRenderer = class {
|
|
|
27079
27895
|
this.inputsUI.setDialogHost(this.dialogHost);
|
|
27080
27896
|
this.inputsUI.setSymbolPicker(this.symbolPicker);
|
|
27081
27897
|
this.inputsUI.setLegendActions(this.legendActionsProvider);
|
|
27898
|
+
this.inputsUI.setLegendCallouts(this.legendCalloutsProvider);
|
|
27082
27899
|
this.inputsUI.setLegendOverviewAction(this.legendOverviewAction);
|
|
27083
27900
|
this.inputsUI.setOnChange((c) => {
|
|
27084
|
-
for (const cb of this.inputChangeCbs) cb({ indicatorId: c.indicatorId, key: c.key, value: c.value });
|
|
27901
|
+
for (const cb of this.inputChangeCbs) cb({ indicatorId: c.indicatorId, key: c.key, value: c.value, ...c.kind ? { kind: c.kind } : {} });
|
|
27085
27902
|
});
|
|
27086
27903
|
this.inputsUI.setOnRemove((id) => {
|
|
27087
27904
|
for (const cb of this.removeIndicatorCbs) cb(id);
|
|
@@ -27122,6 +27939,7 @@ var NativeRenderer = class {
|
|
|
27122
27939
|
this.emitPaneAction({ type: "maximize", paneId, maximized });
|
|
27123
27940
|
}
|
|
27124
27941
|
});
|
|
27942
|
+
this.paneControls.setSuspended(this.layoutMode === "mobile");
|
|
27125
27943
|
this.axisScaleButtons = new AxisScaleButtons(this.plot, theme, {
|
|
27126
27944
|
panes: () => this.axisScaleViews(),
|
|
27127
27945
|
rightAxis: () => this.rightAxisW,
|
|
@@ -27131,8 +27949,19 @@ var NativeRenderer = class {
|
|
|
27131
27949
|
if (pane) this.setPaneLog(paneId, !paneLogScale(this.scene, pane));
|
|
27132
27950
|
}
|
|
27133
27951
|
});
|
|
27134
|
-
this.resizeObserver = new ResizeObserver(() =>
|
|
27952
|
+
this.resizeObserver = new ResizeObserver((entries) => {
|
|
27953
|
+
for (const e of entries) {
|
|
27954
|
+
if (e.target !== this.plot) continue;
|
|
27955
|
+
const s = e.devicePixelContentBoxSize?.[0];
|
|
27956
|
+
if (s) this.plotDeviceSize = { width: s.inlineSize, height: s.blockSize };
|
|
27957
|
+
}
|
|
27958
|
+
this.resize();
|
|
27959
|
+
});
|
|
27135
27960
|
this.resizeObserver.observe(this.wrapper);
|
|
27961
|
+
try {
|
|
27962
|
+
this.resizeObserver.observe(this.plot, { box: "device-pixel-content-box" });
|
|
27963
|
+
} catch {
|
|
27964
|
+
}
|
|
27136
27965
|
this.watchDpr();
|
|
27137
27966
|
this.syncSize();
|
|
27138
27967
|
}
|
|
@@ -27257,8 +28086,6 @@ var NativeRenderer = class {
|
|
|
27257
28086
|
this.inputsUI?.destroy();
|
|
27258
28087
|
this.paneControls?.destroy();
|
|
27259
28088
|
this.axisScaleButtons?.destroy();
|
|
27260
|
-
for (const overlay of this.tableOverlays.values()) overlay.destroy();
|
|
27261
|
-
this.tableOverlays.clear();
|
|
27262
28089
|
this.resizeObserver?.disconnect();
|
|
27263
28090
|
this.resizeObserver = null;
|
|
27264
28091
|
this.dprMedia?.removeEventListener("change", this.onDprChange);
|
|
@@ -27271,6 +28098,8 @@ var NativeRenderer = class {
|
|
|
27271
28098
|
this.settingsButton = null;
|
|
27272
28099
|
this.plot?.removeEventListener("pointermove", this.onScrollProximityMove);
|
|
27273
28100
|
this.plot?.removeEventListener("pointerleave", this.onScrollProximityLeave);
|
|
28101
|
+
this.labelTooltip?.destroy();
|
|
28102
|
+
this.labelTooltip = null;
|
|
27274
28103
|
this.scrollButton?.remove();
|
|
27275
28104
|
this.scrollButton = null;
|
|
27276
28105
|
for (const l of this.extLayers) l.instance.destroy?.();
|
|
@@ -27285,6 +28114,7 @@ var NativeRenderer = class {
|
|
|
27285
28114
|
this.attributionEl = null;
|
|
27286
28115
|
this.mountContainer?.style.removeProperty("--vela-toolbar-gutter");
|
|
27287
28116
|
this.mountContainer?.style.removeProperty("--vela-scale-gutter");
|
|
28117
|
+
this.mountContainer?.style.removeProperty("--vela-bottom-gutter");
|
|
27288
28118
|
this.mountContainer?.style.removeProperty("--vela-price-pane-top");
|
|
27289
28119
|
this.mountContainer?.style.removeProperty("--vela-price-pane-bottom");
|
|
27290
28120
|
this.mountContainer = null;
|
|
@@ -27377,7 +28207,6 @@ var NativeRenderer = class {
|
|
|
27377
28207
|
ensurePane(pane) {
|
|
27378
28208
|
this.scene.ensurePane(pane.id, pane.kind, pane.order, pane.heightWeight ?? (pane.kind === "price" ? 3 : 1));
|
|
27379
28209
|
this.layoutPanes();
|
|
27380
|
-
this.repositionTables();
|
|
27381
28210
|
this.paneControls?.refresh();
|
|
27382
28211
|
this.scheduler.invalidate(4 /* Full */);
|
|
27383
28212
|
}
|
|
@@ -27399,17 +28228,14 @@ var NativeRenderer = class {
|
|
|
27399
28228
|
if (!model.ownScale) this.scene.dropIndicatorScale(handle.id);
|
|
27400
28229
|
this.inputsUI.setPane(handle.id, paneId);
|
|
27401
28230
|
this.refreshAnchorOffset(model);
|
|
27402
|
-
this.syncTables(model);
|
|
27403
28231
|
this.refreshAxisWidth();
|
|
27404
28232
|
this.layoutPanes();
|
|
27405
|
-
this.repositionTables();
|
|
27406
28233
|
this.paneControls?.refresh();
|
|
27407
28234
|
this.scheduler.invalidate(4 /* Full */);
|
|
27408
28235
|
}
|
|
27409
28236
|
orderPanes(orderedIds) {
|
|
27410
28237
|
this.scene.orderPanes(orderedIds);
|
|
27411
28238
|
this.layoutPanes();
|
|
27412
|
-
this.repositionTables();
|
|
27413
28239
|
this.paneControls?.refresh();
|
|
27414
28240
|
this.scheduler.invalidate(4 /* Full */);
|
|
27415
28241
|
}
|
|
@@ -27418,7 +28244,6 @@ var NativeRenderer = class {
|
|
|
27418
28244
|
if (!pane || pane.collapsed === collapsed) return;
|
|
27419
28245
|
pane.collapsed = collapsed;
|
|
27420
28246
|
this.layoutPanes();
|
|
27421
|
-
this.repositionTables();
|
|
27422
28247
|
this.paneControls?.refresh();
|
|
27423
28248
|
this.scheduler.invalidate(4 /* Full */);
|
|
27424
28249
|
}
|
|
@@ -27426,7 +28251,6 @@ var NativeRenderer = class {
|
|
|
27426
28251
|
if (paneId !== null && !this.scene.panes.has(paneId)) paneId = null;
|
|
27427
28252
|
this.maximizedPaneId = paneId;
|
|
27428
28253
|
this.layoutPanes();
|
|
27429
|
-
this.repositionTables();
|
|
27430
28254
|
this.paneControls?.refresh();
|
|
27431
28255
|
this.scheduler.invalidate(4 /* Full */);
|
|
27432
28256
|
}
|
|
@@ -27511,9 +28335,8 @@ var NativeRenderer = class {
|
|
|
27511
28335
|
else this.scene.assignIndicatorZ(model.id);
|
|
27512
28336
|
this.inputsUI.upsert(model.id, model.shorttitle ?? model.title, model.inputs, model.inputValues, model.paneId, {
|
|
27513
28337
|
native: !!model.native,
|
|
27514
|
-
...model.
|
|
28338
|
+
...model.props ? { props: model.props, propValues: model.propValues ?? {} } : {}
|
|
27515
28339
|
});
|
|
27516
|
-
this.syncTables(model);
|
|
27517
28340
|
if (model.native?.type === "volume") {
|
|
27518
28341
|
this.volumeActive = true;
|
|
27519
28342
|
this.volumeHidden = false;
|
|
@@ -27537,7 +28360,6 @@ var NativeRenderer = class {
|
|
|
27537
28360
|
}
|
|
27538
28361
|
}
|
|
27539
28362
|
applyPatch(model, patch);
|
|
27540
|
-
this.syncTables(model);
|
|
27541
28363
|
this.scheduler.invalidate(3 /* Light */);
|
|
27542
28364
|
}
|
|
27543
28365
|
removeIndicator(handle) {
|
|
@@ -27556,14 +28378,12 @@ var NativeRenderer = class {
|
|
|
27556
28378
|
this.scene.forgetAnchorOffset(handle.id);
|
|
27557
28379
|
this.scene.dropIndicatorScale(handle.id);
|
|
27558
28380
|
this.inputsUI.remove(handle.id);
|
|
27559
|
-
this.tableOverlays.get(handle.id)?.destroy();
|
|
27560
|
-
this.tableOverlays.delete(handle.id);
|
|
27561
28381
|
this.refreshAxisWidth();
|
|
27562
28382
|
this.paneControls?.refresh();
|
|
27563
28383
|
this.scheduler.invalidate(4 /* Full */);
|
|
27564
28384
|
}
|
|
27565
|
-
setIndicatorInputs(handle, values) {
|
|
27566
|
-
this.inputsUI.setValues(handle.id, values);
|
|
28385
|
+
setIndicatorInputs(handle, values, props) {
|
|
28386
|
+
this.inputsUI.setValues(handle.id, values, props);
|
|
27567
28387
|
}
|
|
27568
28388
|
setSymbolPicker(picker) {
|
|
27569
28389
|
this.symbolPicker = picker;
|
|
@@ -27573,6 +28393,10 @@ var NativeRenderer = class {
|
|
|
27573
28393
|
this.legendActionsProvider = provider;
|
|
27574
28394
|
this.inputsUI?.setLegendActions(provider);
|
|
27575
28395
|
}
|
|
28396
|
+
setLegendCallouts(provider) {
|
|
28397
|
+
this.legendCalloutsProvider = provider;
|
|
28398
|
+
this.inputsUI?.setLegendCallouts(provider);
|
|
28399
|
+
}
|
|
27576
28400
|
setLegendOverviewAction(action) {
|
|
27577
28401
|
this.legendOverviewAction = action;
|
|
27578
28402
|
this.inputsUI?.setLegendOverviewAction(action);
|
|
@@ -27597,8 +28421,6 @@ var NativeRenderer = class {
|
|
|
27597
28421
|
}
|
|
27598
28422
|
if (!visible) {
|
|
27599
28423
|
this.scene.indicators.delete(handle.id);
|
|
27600
|
-
this.tableOverlays.get(handle.id)?.destroy();
|
|
27601
|
-
this.tableOverlays.delete(handle.id);
|
|
27602
28424
|
}
|
|
27603
28425
|
this.inputsUI.setVisible(handle.id, visible);
|
|
27604
28426
|
this.scheduler.invalidate(4 /* Full */);
|
|
@@ -27644,6 +28466,7 @@ var NativeRenderer = class {
|
|
|
27644
28466
|
this.userDrawings?.setLayoutMode(mode);
|
|
27645
28467
|
this.settingsDialog?.setLayoutMode(mode);
|
|
27646
28468
|
this.inputsUI?.setLayoutMode(mode);
|
|
28469
|
+
this.paneControls?.setSuspended(mode === "mobile");
|
|
27647
28470
|
if (this.scrollButton) {
|
|
27648
28471
|
const px = mode === "mobile" ? SCROLL_BTN_SIZE_TOUCH : SCROLL_BTN_SIZE;
|
|
27649
28472
|
this.scrollButton.style.width = `${px}px`;
|
|
@@ -28039,7 +28862,6 @@ var NativeRenderer = class {
|
|
|
28039
28862
|
/** Relayout + repaint + refresh the hover buttons after a collapse/maximize/order change. */
|
|
28040
28863
|
afterPaneLayoutChange() {
|
|
28041
28864
|
this.layoutPanes();
|
|
28042
|
-
this.repositionTables();
|
|
28043
28865
|
this.paneControls?.refresh();
|
|
28044
28866
|
this.scheduler.invalidate(4 /* Full */);
|
|
28045
28867
|
}
|
|
@@ -28120,7 +28942,6 @@ var NativeRenderer = class {
|
|
|
28120
28942
|
above.heightWeight = next.above;
|
|
28121
28943
|
below.heightWeight = next.below;
|
|
28122
28944
|
this.layoutPanes();
|
|
28123
|
-
this.repositionTables();
|
|
28124
28945
|
this.scheduler.invalidate(4 /* Full */);
|
|
28125
28946
|
}
|
|
28126
28947
|
/** Double-click a separator → split the two adjacent panes evenly (each gets half of
|
|
@@ -28135,7 +28956,6 @@ var NativeRenderer = class {
|
|
|
28135
28956
|
above.heightWeight = half;
|
|
28136
28957
|
below.heightWeight = half;
|
|
28137
28958
|
this.layoutPanes();
|
|
28138
|
-
this.repositionTables();
|
|
28139
28959
|
this.scheduler.invalidate(4 /* Full */);
|
|
28140
28960
|
}
|
|
28141
28961
|
// ── keyboard navigation / accessibility (item 11) ──
|
|
@@ -28414,7 +29234,10 @@ var NativeRenderer = class {
|
|
|
28414
29234
|
const liveActual = li >= 0 ? this.bars[li] : void 0;
|
|
28415
29235
|
const easeLive = !!liveActual && this.liveEaseTime === liveActual.time && (liveActual.high !== this.liveEaseHigh || liveActual.low !== this.liveEaseLow || liveActual.close !== this.liveEaseClose);
|
|
28416
29236
|
if (easeLive && liveActual) this.bars[li] = { ...liveActual, high: this.liveEaseHigh, low: this.liveEaseLow, close: this.liveEaseClose };
|
|
28417
|
-
this.scene.drawingSlices =
|
|
29237
|
+
this.scene.drawingSlices = mergeSlices(
|
|
29238
|
+
this.indicatorSlices.prepare(this.scene, this.coords, this.theme, this.dataCanvas),
|
|
29239
|
+
this.userDrawings?.prepareSlices(this.scene.orderedPanes().map((p) => p.id)) ?? /* @__PURE__ */ new Map()
|
|
29240
|
+
);
|
|
28418
29241
|
this.backdropRenderer.render(this.scene, this.coords, this.theme, gridAlpha);
|
|
28419
29242
|
this.backend.render(this.scene, this.coords, this.theme);
|
|
28420
29243
|
this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
|
|
@@ -28553,7 +29376,11 @@ var NativeRenderer = class {
|
|
|
28553
29376
|
}
|
|
28554
29377
|
const models = this.scene.indicatorsForPane(pane.id);
|
|
28555
29378
|
const masterModels = models.filter((m) => m.ownScale !== true);
|
|
28556
|
-
|
|
29379
|
+
let dr = this.chrome.paneDrawingsRange(masterModels, this.scene, pane === pricePane, vr);
|
|
29380
|
+
if (pane === pricePane) {
|
|
29381
|
+
const or = overlaySeriesRange(this.scene.indicators.values(), i0, i1, (id) => this.scene.offsetOf(id));
|
|
29382
|
+
if (or) dr = dr ? { min: Math.min(dr.min, or.min), max: Math.max(dr.max, or.max) } : or;
|
|
29383
|
+
}
|
|
28557
29384
|
const includeCandles = pane.kind === "price" && !this.scene.candlesHidden;
|
|
28558
29385
|
pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
|
|
28559
29386
|
pane.percentBaseline = pane.kind === "price" ? this.bars[i0]?.close ?? 0 : this.firstVisibleValue(masterModels, i0);
|
|
@@ -28827,6 +29654,7 @@ var NativeRenderer = class {
|
|
|
28827
29654
|
for (const m of models) {
|
|
28828
29655
|
const off = this.scene.offsetOf(m.id);
|
|
28829
29656
|
for (const s of m.series) {
|
|
29657
|
+
if (s.overlay === true) continue;
|
|
28830
29658
|
if (isLineLikeSeries(s)) {
|
|
28831
29659
|
const v = s.points[i0 - off]?.value;
|
|
28832
29660
|
if (v != null && Number.isFinite(v)) return v;
|
|
@@ -28847,27 +29675,6 @@ var NativeRenderer = class {
|
|
|
28847
29675
|
}
|
|
28848
29676
|
return maxVol;
|
|
28849
29677
|
}
|
|
28850
|
-
/** Create/update/destroy an indicator's DOM table overlay (anchored off real pane geometry). */
|
|
28851
|
-
syncTables(model) {
|
|
28852
|
-
const tables = model.tables ?? [];
|
|
28853
|
-
let overlay = this.tableOverlays.get(model.id);
|
|
28854
|
-
if (tables.length === 0) {
|
|
28855
|
-
if (overlay) {
|
|
28856
|
-
overlay.destroy();
|
|
28857
|
-
this.tableOverlays.delete(model.id);
|
|
28858
|
-
}
|
|
28859
|
-
return;
|
|
28860
|
-
}
|
|
28861
|
-
if (!overlay) {
|
|
28862
|
-
overlay = new TableOverlay(this.plot, this.theme, (id) => this.paneBoundsFor(id));
|
|
28863
|
-
overlay.setVisible(this.loadingEl === null);
|
|
28864
|
-
this.tableOverlays.set(model.id, overlay);
|
|
28865
|
-
}
|
|
28866
|
-
overlay.update(tables);
|
|
28867
|
-
}
|
|
28868
|
-
repositionTables() {
|
|
28869
|
-
for (const overlay of this.tableOverlays.values()) overlay.reposition();
|
|
28870
|
-
}
|
|
28871
29678
|
layoutPanes() {
|
|
28872
29679
|
const panes = this.scene.orderedPanes();
|
|
28873
29680
|
const dataHeight = this.coords.height;
|
|
@@ -28918,6 +29725,7 @@ var NativeRenderer = class {
|
|
|
28918
29725
|
const visible = maxPane ? [maxPane] : this.scene.orderedPanes().filter((p) => !p.collapsed);
|
|
28919
29726
|
const paneBottom = visible.length ? Math.max(...visible.map((p) => p.bounds.top + p.bounds.height)) : dataHeight;
|
|
28920
29727
|
this.scrollBtnBottomPx = SCROLL_BTN_BOTTOM + Math.max(0, dataHeight - paneBottom);
|
|
29728
|
+
this.mountContainer?.style.setProperty("--vela-bottom-gutter", `${TIME_AXIS_H + Math.max(0, dataHeight - paneBottom)}px`);
|
|
28921
29729
|
this.scrollBtnRightPx = this.rightAxisW + SCROLL_BTN_RIGHT_INSET;
|
|
28922
29730
|
if (this.scrollButton) {
|
|
28923
29731
|
this.scrollButton.style.bottom = `${this.scrollBtnBottomPx}px`;
|
|
@@ -29010,30 +29818,33 @@ var NativeRenderer = class {
|
|
|
29010
29818
|
if (w <= 0 || h <= 0) return;
|
|
29011
29819
|
const dpr = window.devicePixelRatio || 1;
|
|
29012
29820
|
this.plot.style.left = `${this.toolbarGutter}px`;
|
|
29013
|
-
const
|
|
29014
|
-
|
|
29015
|
-
|
|
29016
|
-
|
|
29017
|
-
|
|
29018
|
-
|
|
29019
|
-
|
|
29020
|
-
|
|
29021
|
-
|
|
29022
|
-
|
|
29023
|
-
|
|
29024
|
-
|
|
29025
|
-
|
|
29026
|
-
|
|
29027
|
-
|
|
29028
|
-
|
|
29029
|
-
this.
|
|
29030
|
-
this.
|
|
29031
|
-
this.
|
|
29032
|
-
|
|
29821
|
+
const rect = this.plot.getBoundingClientRect();
|
|
29822
|
+
let bw = Math.max(1, Math.round(rect.width * dpr));
|
|
29823
|
+
let bh = Math.max(1, Math.round(rect.height * dpr));
|
|
29824
|
+
const dev = this.plotDeviceSize;
|
|
29825
|
+
if (dev && Math.abs(dev.width - rect.width * dpr) <= 1 && Math.abs(dev.height - rect.height * dpr) <= 1) {
|
|
29826
|
+
bw = Math.max(1, dev.width);
|
|
29827
|
+
bh = Math.max(1, dev.height);
|
|
29828
|
+
}
|
|
29829
|
+
const pw = bw / dpr;
|
|
29830
|
+
const ph = bh / dpr;
|
|
29831
|
+
const size = (canvas) => {
|
|
29832
|
+
canvas.width = bw;
|
|
29833
|
+
canvas.height = bh;
|
|
29834
|
+
canvas.style.width = `${pw}px`;
|
|
29835
|
+
canvas.style.height = `${ph}px`;
|
|
29836
|
+
};
|
|
29837
|
+
size(this.dataCanvas);
|
|
29838
|
+
size(this.backdropCanvas);
|
|
29839
|
+
size(this.volumeCanvas);
|
|
29840
|
+
for (const l of this.extLayers) size(l.canvas);
|
|
29841
|
+
size(this.vpvrCanvas);
|
|
29842
|
+
size(this.chromeCanvas);
|
|
29843
|
+
size(this.drawingsCanvas);
|
|
29844
|
+
size(this.cursorCanvas);
|
|
29033
29845
|
this.coords.setSize(Math.max(1, pw - this.rightAxisW), Math.max(1, ph - TIME_AXIS_H), dpr);
|
|
29034
29846
|
this.scene.crosshair = null;
|
|
29035
29847
|
this.layoutPanes();
|
|
29036
|
-
this.repositionTables();
|
|
29037
29848
|
this.userDrawings?.onResize();
|
|
29038
29849
|
if (!this.didInitialFit && this.coords.barCount > 0) {
|
|
29039
29850
|
this.fitContent();
|
|
@@ -29732,6 +30543,19 @@ function unregisterLegendAction(id) {
|
|
|
29732
30543
|
function legendActions() {
|
|
29733
30544
|
return [...legendRegistry.values()].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
29734
30545
|
}
|
|
30546
|
+
var calloutRegistry = /* @__PURE__ */ new Map();
|
|
30547
|
+
function registerLegendCallout(desc) {
|
|
30548
|
+
calloutRegistry.set(desc.id, desc);
|
|
30549
|
+
return () => {
|
|
30550
|
+
if (calloutRegistry.get(desc.id) === desc) calloutRegistry.delete(desc.id);
|
|
30551
|
+
};
|
|
30552
|
+
}
|
|
30553
|
+
function unregisterLegendCallout(id) {
|
|
30554
|
+
calloutRegistry.delete(id);
|
|
30555
|
+
}
|
|
30556
|
+
function legendCallouts() {
|
|
30557
|
+
return [...calloutRegistry.values()].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
30558
|
+
}
|
|
29735
30559
|
var stateHandlers = /* @__PURE__ */ new Map();
|
|
29736
30560
|
function registerStatePersistence(handler) {
|
|
29737
30561
|
stateHandlers.set(handler.key, handler);
|
|
@@ -29840,6 +30664,7 @@ exports.getNativeIndicator = getNativeIndicator;
|
|
|
29840
30664
|
exports.iconMarkup = iconMarkup;
|
|
29841
30665
|
exports.inputVisible = inputVisible;
|
|
29842
30666
|
exports.legendActions = legendActions;
|
|
30667
|
+
exports.legendCallouts = legendCallouts;
|
|
29843
30668
|
exports.nativeIndicatorDescriptors = nativeIndicatorDescriptors;
|
|
29844
30669
|
exports.nativeIndicatorTypes = nativeIndicatorTypes;
|
|
29845
30670
|
exports.normalizeSettingsRow = normalizeSettingsRow;
|
|
@@ -29848,6 +30673,7 @@ exports.registerDefaultEngine = registerDefaultEngine;
|
|
|
29848
30673
|
exports.registerDrawingType = registerDrawingType;
|
|
29849
30674
|
exports.registerIcon = registerIcon;
|
|
29850
30675
|
exports.registerLegendAction = registerLegendAction;
|
|
30676
|
+
exports.registerLegendCallout = registerLegendCallout;
|
|
29851
30677
|
exports.registerNativeIndicator = registerNativeIndicator;
|
|
29852
30678
|
exports.registerRendererDefaults = registerRendererDefaults;
|
|
29853
30679
|
exports.registerRendererLayer = registerRendererLayer;
|
|
@@ -29872,6 +30698,7 @@ exports.topbarActionOverride = topbarActionOverride;
|
|
|
29872
30698
|
exports.unregisterChartType = unregisterChartType;
|
|
29873
30699
|
exports.unregisterDefaultEngine = unregisterDefaultEngine;
|
|
29874
30700
|
exports.unregisterLegendAction = unregisterLegendAction;
|
|
30701
|
+
exports.unregisterLegendCallout = unregisterLegendCallout;
|
|
29875
30702
|
exports.unregisterNativeIndicator = unregisterNativeIndicator;
|
|
29876
30703
|
exports.unregisterRendererDefaults = unregisterRendererDefaults;
|
|
29877
30704
|
exports.unregisterRendererLayer = unregisterRendererLayer;
|