@luxalgo/vela 0.8.4 → 0.8.5
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-Byo8pEv0.d.ts → DataProvider-BUdlkx9I.d.ts} +1 -1
- package/dist/{DataProvider-Bw58CjYy.d.cts → DataProvider-DdqMxKdl.d.cts} +1 -1
- package/dist/{chunk-MOJ4T26N.js → chunk-BN4DNS3O.js} +27 -7
- package/dist/{chunk-UNUVPQFW.js → chunk-QYIO37K7.js} +482 -94
- package/dist/{contributions-DOm40g5-.d.cts → contributions-D8xuYc9U.d.cts} +37 -5
- package/dist/{contributions-BY3zSYiZ.d.ts → contributions-P9fBBuY1.d.ts} +37 -5
- package/dist/index.cjs +482 -94
- package/dist/index.d.cts +23 -7
- package/dist/index.d.ts +23 -7
- package/dist/index.js +1 -1
- package/dist/{options-CSXbjMM5.d.cts → options-BI5Tp32b.d.cts} +36 -1
- package/dist/{options-CSXbjMM5.d.ts → options-BI5Tp32b.d.ts} +36 -1
- package/dist/{plugin-CiQVyETg.d.cts → plugin-B8F1nor8.d.cts} +14 -8
- package/dist/{plugin-BDxUhGO3.d.ts → plugin-D5Z-Q6TX.d.ts} +14 -8
- package/dist/plugin.d.cts +4 -4
- package/dist/plugin.d.ts +4 -4
- 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-model-DUBX6Fib.d.cts → statusline-model-CTwtDIca.d.cts} +3 -3
- package/dist/{statusline-model-C9tYXuux.d.ts → statusline-model-D9kWC9X5.d.ts} +3 -3
- package/dist/ui.d.cts +1 -1
- package/dist/ui.d.ts +1 -1
- package/dist/vela.global.js +482 -94
- package/dist/vela.global.min.js +53 -53
- package/dist/widget.cjs +508 -100
- package/dist/widget.d.cts +6 -6
- package/dist/widget.d.ts +6 -6
- package/dist/widget.js +3 -3
- package/dist/workspace.cjs +508 -100
- package/dist/workspace.d.cts +5 -5
- package/dist/workspace.d.ts +5 -5
- package/dist/workspace.js +2 -2
- package/package.json +1 -1
package/dist/vela.global.js
CHANGED
|
@@ -8593,6 +8593,8 @@ var Vela = (function (exports) {
|
|
|
8593
8593
|
this.barTransform = null;
|
|
8594
8594
|
// ── chart-type data engines (SDK): one lazily-created engine per style id ──
|
|
8595
8595
|
this.typeEngines = /* @__PURE__ */ new Map();
|
|
8596
|
+
/** Engines suspended by the chart — their host pushes are dropped until they resume. */
|
|
8597
|
+
this.suspendedEngines = /* @__PURE__ */ new WeakSet();
|
|
8596
8598
|
/** Last-seen chart-type settings per type id (dialog edits + persisted restores) —
|
|
8597
8599
|
* replayed into a data engine whenever one is (re)created. */
|
|
8598
8600
|
this.typeSettings = /* @__PURE__ */ new Map();
|
|
@@ -8867,12 +8869,14 @@ var Vela = (function (exports) {
|
|
|
8867
8869
|
this.mergeHistoryIntoReplay(bars);
|
|
8868
8870
|
return;
|
|
8869
8871
|
}
|
|
8870
|
-
|
|
8872
|
+
const repaint = painted;
|
|
8873
|
+
this.setBarSeries(bars, repaint ? { preserveView: true } : void 0);
|
|
8871
8874
|
if (!painted && bars.length > 0) {
|
|
8872
8875
|
painted = true;
|
|
8873
8876
|
if (opts.firstLoad) this.activateBarLayers();
|
|
8874
8877
|
if (!final) this.historyState = "backfill";
|
|
8875
8878
|
}
|
|
8879
|
+
if (repaint && !final) this.notifySessionsBars("backfill");
|
|
8876
8880
|
};
|
|
8877
8881
|
const abort = new AbortController();
|
|
8878
8882
|
this.progressiveAbort = abort;
|
|
@@ -9075,10 +9079,15 @@ var Vela = (function (exports) {
|
|
|
9075
9079
|
this.setBarSeries([], { clearing: true });
|
|
9076
9080
|
this.blankIndicatorVisuals();
|
|
9077
9081
|
if (this.activeEngineStyle) {
|
|
9078
|
-
this.typeEngines.get(this.activeEngineStyle)
|
|
9082
|
+
this.suspendTypeEngine(this.typeEngines.get(this.activeEngineStyle));
|
|
9079
9083
|
this.renderer.setNativeData?.(this.activeEngineStyle, void 0);
|
|
9080
9084
|
this.renderer.setNativeData?.(`${this.activeEngineStyle}-pending`, []);
|
|
9081
9085
|
}
|
|
9086
|
+
for (const record of this.registry.all()) {
|
|
9087
|
+
if (!record.native?.started) continue;
|
|
9088
|
+
this.stopNativeInstance(record.native);
|
|
9089
|
+
this.renderer.setNativeData?.(this.nativeChannel(record), void 0);
|
|
9090
|
+
}
|
|
9082
9091
|
}
|
|
9083
9092
|
if (depthOnly) {
|
|
9084
9093
|
this.extendDepth(gen, m.bars ?? 500);
|
|
@@ -9105,6 +9114,7 @@ var Vela = (function (exports) {
|
|
|
9105
9114
|
this.restartChartTypeEngine();
|
|
9106
9115
|
this.reexecuteIndicators();
|
|
9107
9116
|
}
|
|
9117
|
+
if (depthOnly) this.releaseHeldNatives();
|
|
9108
9118
|
if (!replayBack) this.startLive();
|
|
9109
9119
|
if (identityChanged) {
|
|
9110
9120
|
this.events.emit("market:changed", { symbol: m.symbol ?? "TEST", timeframe: m.timeframe ?? "60", prev: prev3 });
|
|
@@ -9121,9 +9131,8 @@ var Vela = (function (exports) {
|
|
|
9121
9131
|
restartNativeIndicators() {
|
|
9122
9132
|
for (const record of this.registry.all()) {
|
|
9123
9133
|
if (!record.native) continue;
|
|
9124
|
-
|
|
9134
|
+
this.stopNativeInstance(record.native);
|
|
9125
9135
|
record.native.instance = record.native.descriptor.create();
|
|
9126
|
-
record.native.started = false;
|
|
9127
9136
|
record.pendingStructural = true;
|
|
9128
9137
|
if (record.hidden) {
|
|
9129
9138
|
record.native.stale = true;
|
|
@@ -9140,6 +9149,21 @@ var Vela = (function (exports) {
|
|
|
9140
9149
|
this.typeEngines.clear();
|
|
9141
9150
|
if (this.activeEngineStyle) void this.startChartTypeEngine(this.activeEngineStyle);
|
|
9142
9151
|
}
|
|
9152
|
+
/** Suspend a chart-type engine. Its host drops every push until the engine resumes:
|
|
9153
|
+
* work it left in flight lands in its own state, never on the scene. */
|
|
9154
|
+
suspendTypeEngine(engine) {
|
|
9155
|
+
if (!engine) return;
|
|
9156
|
+
engine.suspend();
|
|
9157
|
+
this.suspendedEngines.add(engine);
|
|
9158
|
+
}
|
|
9159
|
+
/** Stop a running native instance, then revoke its context: the teardown inside `stop()`
|
|
9160
|
+
* still reaches its layer, but work it left in flight (a fetch resolving later) does not. */
|
|
9161
|
+
stopNativeInstance(native) {
|
|
9162
|
+
if (native.started) native.instance.stop();
|
|
9163
|
+
native.started = false;
|
|
9164
|
+
native.revokeContext?.();
|
|
9165
|
+
native.revokeContext = void 0;
|
|
9166
|
+
}
|
|
9143
9167
|
/**
|
|
9144
9168
|
* The FIRST candles just painted (the quick preview, or the only load): activate the
|
|
9145
9169
|
* layers that need bars but NOT full history. The volume layer reads the chart's bars
|
|
@@ -9672,7 +9696,7 @@ var Vela = (function (exports) {
|
|
|
9672
9696
|
const def = chartType(id);
|
|
9673
9697
|
const want = def?.dataEngine ? id : null;
|
|
9674
9698
|
if (want === this.activeEngineStyle) return;
|
|
9675
|
-
if (this.activeEngineStyle) this.typeEngines.get(this.activeEngineStyle)
|
|
9699
|
+
if (this.activeEngineStyle) this.suspendTypeEngine(this.typeEngines.get(this.activeEngineStyle));
|
|
9676
9700
|
this.activeEngineStyle = want;
|
|
9677
9701
|
if (want) void this.startChartTypeEngine(want);
|
|
9678
9702
|
}
|
|
@@ -9683,6 +9707,7 @@ var Vela = (function (exports) {
|
|
|
9683
9707
|
if (this.activeEngineStyle !== id) return;
|
|
9684
9708
|
const existing = this.typeEngines.get(id);
|
|
9685
9709
|
if (existing) {
|
|
9710
|
+
this.suspendedEngines.delete(existing);
|
|
9686
9711
|
existing.resume();
|
|
9687
9712
|
return;
|
|
9688
9713
|
}
|
|
@@ -9692,6 +9717,7 @@ var Vela = (function (exports) {
|
|
|
9692
9717
|
this.typeEngines.set(id, engine);
|
|
9693
9718
|
const stored = this.typeSettings.get(id);
|
|
9694
9719
|
if (stored) engine.onSettings?.(stored);
|
|
9720
|
+
const reachesScene = () => this.typeEngines.get(id) === engine && !this.suspendedEngines.has(engine);
|
|
9695
9721
|
engine.start({
|
|
9696
9722
|
symbol: this.config.market.symbol ?? "TEST",
|
|
9697
9723
|
timeframe: this.config.market.timeframe ?? "60",
|
|
@@ -9699,8 +9725,12 @@ var Vela = (function (exports) {
|
|
|
9699
9725
|
session: this.config.market.session,
|
|
9700
9726
|
bars: () => this.bars,
|
|
9701
9727
|
data: this.dataControl,
|
|
9702
|
-
pushData: (data) =>
|
|
9703
|
-
|
|
9728
|
+
pushData: (data) => {
|
|
9729
|
+
if (reachesScene()) this.renderer.setNativeData?.(id, data);
|
|
9730
|
+
},
|
|
9731
|
+
pushPending: (ranges) => {
|
|
9732
|
+
if (reachesScene()) this.renderer.setNativeData?.(`${id}-pending`, ranges);
|
|
9733
|
+
}
|
|
9704
9734
|
});
|
|
9705
9735
|
}
|
|
9706
9736
|
/**
|
|
@@ -9708,8 +9738,9 @@ var Vela = (function (exports) {
|
|
|
9708
9738
|
* a pre-load style change is picked up by init's `setBarSeries`. With bars loaded, the
|
|
9709
9739
|
* view is rebuilt in place (viewport preserved) and every Pine indicator re-executes:
|
|
9710
9740
|
* its input data changed wholesale, which a streaming session's incremental context
|
|
9711
|
-
* cannot absorb.
|
|
9712
|
-
*
|
|
9741
|
+
* cannot absorb. Natives computed from `ctx.bars()` re-run over the new candles (a
|
|
9742
|
+
* synthetic price style is their source too); layer natives that read the renderer's
|
|
9743
|
+
* bars per frame ignore the poke.
|
|
9713
9744
|
*/
|
|
9714
9745
|
syncBarTransform(style) {
|
|
9715
9746
|
const transform = barTransformFor(style);
|
|
@@ -9719,6 +9750,10 @@ var Vela = (function (exports) {
|
|
|
9719
9750
|
this.bars = transform ? transform.full(this.rawBars) : this.rawBars;
|
|
9720
9751
|
this.renderer.setBars(this.bars, { preserveView: true });
|
|
9721
9752
|
this.reexecuteIndicators();
|
|
9753
|
+
if (this.switchingMarket) return;
|
|
9754
|
+
for (const record of this.registry.all()) {
|
|
9755
|
+
if (record.native?.started && record.renderHandle && !record.hidden) this.pokeNative(record);
|
|
9756
|
+
}
|
|
9722
9757
|
}
|
|
9723
9758
|
/**
|
|
9724
9759
|
* Blank every mounted indicator's painted output — series data, fills, backgrounds,
|
|
@@ -9819,10 +9854,39 @@ var Vela = (function (exports) {
|
|
|
9819
9854
|
if (record.session) {
|
|
9820
9855
|
record.pendingCause = cause;
|
|
9821
9856
|
record.session.notifyBars(reason);
|
|
9822
|
-
} else if (record.native)
|
|
9857
|
+
} else if (record.native) this.pokeNative(record);
|
|
9823
9858
|
}
|
|
9824
9859
|
if (this.activeEngineStyle) this.typeEngines.get(this.activeEngineStyle)?.onBars?.();
|
|
9825
9860
|
}
|
|
9861
|
+
/**
|
|
9862
|
+
* Tell a native its bars changed. A `waitsForHistory` native ignores every change while
|
|
9863
|
+
* the history is still loading — a backfill chunk, a deeper snapshot, a tick arriving
|
|
9864
|
+
* meanwhile — and its output stays on its bars through the anchor its emit carried.
|
|
9865
|
+
* Once the history is complete, a held one starts and a running one re-runs.
|
|
9866
|
+
*/
|
|
9867
|
+
pokeNative(record) {
|
|
9868
|
+
const native = record.native;
|
|
9869
|
+
if (!native.descriptor.waitsForHistory) {
|
|
9870
|
+
native.instance.onBars();
|
|
9871
|
+
return;
|
|
9872
|
+
}
|
|
9873
|
+
if (this.historyState === "backfill") return;
|
|
9874
|
+
if (native.heldForHistory) {
|
|
9875
|
+
native.heldForHistory = false;
|
|
9876
|
+
const handle = this.handles.get(record.id);
|
|
9877
|
+
if (handle) void this.startNativeIndicator(record.id, handle);
|
|
9878
|
+
return;
|
|
9879
|
+
}
|
|
9880
|
+
if (native.started) native.instance.onBars();
|
|
9881
|
+
}
|
|
9882
|
+
/** Start the natives still held for a history that has completed without poking them
|
|
9883
|
+
* (a depth-only switch finishes its trim while the switch gates every poke). */
|
|
9884
|
+
releaseHeldNatives() {
|
|
9885
|
+
if (this.historyState === "backfill") return;
|
|
9886
|
+
for (const record of this.registry.all()) {
|
|
9887
|
+
if (record.native?.heldForHistory && !record.hidden) this.pokeNative(record);
|
|
9888
|
+
}
|
|
9889
|
+
}
|
|
9826
9890
|
/** Gap healing needs a ranged feed and a provider-backed series (offline `data` has no source). */
|
|
9827
9891
|
canHeal() {
|
|
9828
9892
|
return !!this.feed.loadRange && !this.config.market.data?.length;
|
|
@@ -10114,7 +10178,7 @@ var Vela = (function (exports) {
|
|
|
10114
10178
|
if (record?.native?.type === "volume") this.volumeOptedOut = true;
|
|
10115
10179
|
if (record) this.settleCode(record, { ok: false, error: new Error(`[vela] updateCode("${id}") \u2014 the indicator was removed.`) });
|
|
10116
10180
|
record?.session?.stop();
|
|
10117
|
-
if (record?.native
|
|
10181
|
+
if (record?.native) this.stopNativeInstance(record.native);
|
|
10118
10182
|
this.handles.delete(id);
|
|
10119
10183
|
if (record?.renderHandle) this.renderer.removeIndicator(record.renderHandle);
|
|
10120
10184
|
const paneId = record?.model?.paneId;
|
|
@@ -10235,7 +10299,7 @@ var Vela = (function (exports) {
|
|
|
10235
10299
|
for (const record of this.registry.all()) {
|
|
10236
10300
|
this.settleCode(record, { ok: false, error: new Error("[vela] updateCode \u2014 the chart was destroyed.") });
|
|
10237
10301
|
record.session?.stop();
|
|
10238
|
-
if (record.native
|
|
10302
|
+
if (record.native) this.stopNativeInstance(record.native);
|
|
10239
10303
|
}
|
|
10240
10304
|
for (const engine of this.typeEngines.values()) engine.stop();
|
|
10241
10305
|
this.typeEngines.clear();
|
|
@@ -10362,7 +10426,19 @@ var Vela = (function (exports) {
|
|
|
10362
10426
|
await this.readyPromise;
|
|
10363
10427
|
const record = this.registry.get(id);
|
|
10364
10428
|
if (!record?.native || record.hidden) return;
|
|
10429
|
+
const native = record.native;
|
|
10430
|
+
if (native.started) return;
|
|
10431
|
+
if (native.descriptor.waitsForHistory && this.historyState === "backfill") {
|
|
10432
|
+
native.heldForHistory = true;
|
|
10433
|
+
this.mountNativePlaceholder(id, record);
|
|
10434
|
+
return;
|
|
10435
|
+
}
|
|
10436
|
+
native.heldForHistory = false;
|
|
10365
10437
|
const channel = this.nativeChannel(record);
|
|
10438
|
+
let alive = true;
|
|
10439
|
+
native.revokeContext = () => {
|
|
10440
|
+
alive = false;
|
|
10441
|
+
};
|
|
10366
10442
|
const ctx = {
|
|
10367
10443
|
id,
|
|
10368
10444
|
symbol: this.config.market.symbol ?? "TEST",
|
|
@@ -10371,14 +10447,18 @@ var Vela = (function (exports) {
|
|
|
10371
10447
|
session: this.config.market.session,
|
|
10372
10448
|
bars: () => this.bars,
|
|
10373
10449
|
data: this.dataControl,
|
|
10374
|
-
emit: (out) =>
|
|
10375
|
-
|
|
10450
|
+
emit: (out) => {
|
|
10451
|
+
if (alive) this.applyModel(id, this.buildNativeModel(record, out, this.bars[0]?.time));
|
|
10452
|
+
},
|
|
10453
|
+
pushData: (data) => {
|
|
10454
|
+
if (alive) this.renderer.setNativeData?.(channel, data);
|
|
10455
|
+
},
|
|
10376
10456
|
setStatus: (status) => {
|
|
10377
|
-
if (record.renderHandle && !record.hidden) this.renderer.setIndicatorStatus?.(record.renderHandle, status);
|
|
10457
|
+
if (alive && record.renderHandle && !record.hidden) this.renderer.setIndicatorStatus?.(record.renderHandle, status);
|
|
10378
10458
|
}
|
|
10379
10459
|
};
|
|
10380
|
-
|
|
10381
|
-
|
|
10460
|
+
native.instance.start(ctx, record.inputValues);
|
|
10461
|
+
native.started = true;
|
|
10382
10462
|
} catch (err) {
|
|
10383
10463
|
this.fail(id, handle, err);
|
|
10384
10464
|
}
|
|
@@ -10391,8 +10471,14 @@ var Vela = (function (exports) {
|
|
|
10391
10471
|
const { type, descriptor } = record.native;
|
|
10392
10472
|
return descriptor.multiInstance ? nativeInstanceChannel(type, record.id) : type;
|
|
10393
10473
|
}
|
|
10394
|
-
/**
|
|
10395
|
-
|
|
10474
|
+
/**
|
|
10475
|
+
* Wrap a native indicator's emitted visuals into a full IndicatorModel, tagged `native`.
|
|
10476
|
+
* `anchorTime` is the first bar the output is index-aligned to — the chart's first bar
|
|
10477
|
+
* when it emitted. Without it the renderer pins the output's first slot to whatever the
|
|
10478
|
+
* first bar is NOW, so history landing in front of a computed output (or the oldest
|
|
10479
|
+
* bars trimmed under it) would slide the whole plot by that many bars.
|
|
10480
|
+
*/
|
|
10481
|
+
buildNativeModel(record, out, anchorTime) {
|
|
10396
10482
|
const d = record.native.descriptor;
|
|
10397
10483
|
const type = record.native.type;
|
|
10398
10484
|
const channel = this.nativeChannel(record);
|
|
@@ -10403,6 +10489,7 @@ var Vela = (function (exports) {
|
|
|
10403
10489
|
overlay: d.overlay,
|
|
10404
10490
|
paneHint: d.paneHint,
|
|
10405
10491
|
native: { type, ...channel !== type ? { channel } : {} },
|
|
10492
|
+
...anchorTime != null ? { anchorTime } : {},
|
|
10406
10493
|
...d.legend === false ? { legend: false } : {},
|
|
10407
10494
|
...out.paneAxis != null ? { paneAxis: out.paneAxis } : {},
|
|
10408
10495
|
series: out.series ?? [],
|
|
@@ -10457,8 +10544,9 @@ var Vela = (function (exports) {
|
|
|
10457
10544
|
const paneId = this.routePane(id, model, record.options ?? {});
|
|
10458
10545
|
this.placeModel(model, id, paneId);
|
|
10459
10546
|
record.model = model;
|
|
10460
|
-
this.ensurePaneFor(paneId);
|
|
10547
|
+
const paneCreated = this.ensurePaneFor(paneId);
|
|
10461
10548
|
record.renderHandle = this.renderer.mountIndicator(model);
|
|
10549
|
+
if (paneCreated) this.events.emit("pane:changed", void 0);
|
|
10462
10550
|
record.pendingStructural = true;
|
|
10463
10551
|
if (record.hidden) {
|
|
10464
10552
|
this.renderer.setIndicatorVisible?.(record.renderHandle, false);
|
|
@@ -10482,24 +10570,49 @@ var Vela = (function (exports) {
|
|
|
10482
10570
|
const paneId = this.routePane(id, model, record.options ?? {});
|
|
10483
10571
|
this.placeModel(model, id, paneId);
|
|
10484
10572
|
record.model = model;
|
|
10485
|
-
this.ensurePaneFor(paneId);
|
|
10573
|
+
const paneCreated = this.ensurePaneFor(paneId);
|
|
10486
10574
|
record.renderHandle = this.renderer.mountIndicator(model);
|
|
10575
|
+
if (paneCreated) this.events.emit("pane:changed", void 0);
|
|
10487
10576
|
record.pendingStructural = true;
|
|
10488
10577
|
this.renderer.setIndicatorVisible?.(record.renderHandle, false);
|
|
10489
10578
|
this.announce(record, this.handles.get(id));
|
|
10490
10579
|
}
|
|
10580
|
+
/**
|
|
10581
|
+
* The legend row of a native HELD for the history (see `waitsForHistory`): an empty
|
|
10582
|
+
* model with the title and inputs schema, spinning, not announced — exactly a script
|
|
10583
|
+
* indicator's loading placeholder, so `ready`/`indicator:added` and `inspect()` still
|
|
10584
|
+
* mean "it produced output". A record that already has a row (restarted over a new
|
|
10585
|
+
* market, or shown again) keeps it and only spins. The first emit remounts over it.
|
|
10586
|
+
*/
|
|
10587
|
+
mountNativePlaceholder(id, record) {
|
|
10588
|
+
record.pendingStructural = true;
|
|
10589
|
+
if (!record.renderHandle) {
|
|
10590
|
+
const model = this.buildNativeModel(record, {});
|
|
10591
|
+
const paneId = this.routePane(id, model, record.options ?? {});
|
|
10592
|
+
this.placeModel(model, id, paneId);
|
|
10593
|
+
record.model = model;
|
|
10594
|
+
const paneCreated = this.ensurePaneFor(paneId);
|
|
10595
|
+
record.renderHandle = this.renderer.mountIndicator(model);
|
|
10596
|
+
if (paneCreated) this.events.emit("pane:changed", void 0);
|
|
10597
|
+
}
|
|
10598
|
+
this.setLoading(record, true);
|
|
10599
|
+
}
|
|
10491
10600
|
/** Flip the record's loading state and reflect it in the legend row (spinner on/off). */
|
|
10492
10601
|
setLoading(record, loading) {
|
|
10493
10602
|
record.loading = loading;
|
|
10494
10603
|
if (record.renderHandle && !record.hidden) this.renderer.setIndicatorStatus?.(record.renderHandle, loading ? "loading" : "idle");
|
|
10495
10604
|
}
|
|
10605
|
+
/** Make sure the renderer has the pane; true when it is new to the layout (the caller
|
|
10606
|
+
* emits `pane:changed` once the indicator's model sits on it, so `list()` shows it). */
|
|
10496
10607
|
ensurePaneFor(paneId) {
|
|
10497
|
-
|
|
10608
|
+
const created = !this.paneOrder.includes(paneId);
|
|
10609
|
+
if (created) this.paneOrder.push(paneId);
|
|
10498
10610
|
this.renderer.ensurePane({
|
|
10499
10611
|
id: paneId,
|
|
10500
10612
|
kind: paneId === "price" ? "price" : "study",
|
|
10501
10613
|
order: this.paneOrder.indexOf(paneId)
|
|
10502
10614
|
});
|
|
10615
|
+
return created;
|
|
10503
10616
|
}
|
|
10504
10617
|
/** True when any (still-registered) indicator sits on the given pane. */
|
|
10505
10618
|
paneStillUsed(paneId) {
|
|
@@ -10535,12 +10648,14 @@ var Vela = (function (exports) {
|
|
|
10535
10648
|
const panes = [];
|
|
10536
10649
|
for (const paneId of this.paneOrder) {
|
|
10537
10650
|
if (paneId !== "price" && !byPane.has(paneId)) continue;
|
|
10651
|
+
const heightWeight = this.renderer.paneHeightWeight?.(paneId) ?? null;
|
|
10538
10652
|
panes.push({
|
|
10539
10653
|
id: paneId,
|
|
10540
10654
|
kind: paneId === "price" ? "price" : "study",
|
|
10541
10655
|
order: panes.length,
|
|
10542
10656
|
collapsed: this.collapsedPanes.has(paneId),
|
|
10543
10657
|
maximized: this.maximizedPaneId === paneId,
|
|
10658
|
+
...heightWeight !== null ? { heightWeight } : {},
|
|
10544
10659
|
indicators: byPane.get(paneId) ?? []
|
|
10545
10660
|
});
|
|
10546
10661
|
}
|
|
@@ -10572,6 +10687,22 @@ var Vela = (function (exports) {
|
|
|
10572
10687
|
this.renderer.setPaneMaximized?.(paneId);
|
|
10573
10688
|
this.events.emit("pane:changed", void 0);
|
|
10574
10689
|
}
|
|
10690
|
+
setPaneHeightWeight(paneId, weight) {
|
|
10691
|
+
if (!this.renderer.setPaneHeightWeight) {
|
|
10692
|
+
console.warn("[vela] the active renderer does not support pane sizing \u2014 chart.panes.setHeightWeight ignored.");
|
|
10693
|
+
return;
|
|
10694
|
+
}
|
|
10695
|
+
if (typeof weight !== "number" || !Number.isFinite(weight) || weight <= 0) {
|
|
10696
|
+
console.warn(`[vela] chart.panes.setHeightWeight: weight must be a positive number (got ${String(weight)}).`);
|
|
10697
|
+
return;
|
|
10698
|
+
}
|
|
10699
|
+
if (!this.existingPaneIds().includes(paneId)) {
|
|
10700
|
+
console.warn(`[vela] chart.panes.setHeightWeight: no pane "${paneId}".`);
|
|
10701
|
+
return;
|
|
10702
|
+
}
|
|
10703
|
+
this.renderer.setPaneHeightWeight(paneId, weight);
|
|
10704
|
+
this.events.emit("pane:changed", void 0);
|
|
10705
|
+
}
|
|
10575
10706
|
/** IndicatorController/PaneController: move (or merge) an indicator to another pane. */
|
|
10576
10707
|
moveIndicator(id, target) {
|
|
10577
10708
|
if (!this.paneManagementSupported() || !this.renderer.setIndicatorPane) {
|
|
@@ -10669,6 +10800,9 @@ var Vela = (function (exports) {
|
|
|
10669
10800
|
this.maximizedPaneId = a.maximized ? a.paneId : null;
|
|
10670
10801
|
this.events.emit("pane:changed", void 0);
|
|
10671
10802
|
break;
|
|
10803
|
+
case "resize":
|
|
10804
|
+
this.events.emit("pane:changed", void 0);
|
|
10805
|
+
break;
|
|
10672
10806
|
}
|
|
10673
10807
|
}
|
|
10674
10808
|
/** Fire `indicator:added` + the handle's `ready` once, on the first computed model. */
|
|
@@ -10699,14 +10833,16 @@ var Vela = (function (exports) {
|
|
|
10699
10833
|
const paneId2 = this.routePane(id, model, record.options ?? {});
|
|
10700
10834
|
this.placeModel(model, id, paneId2);
|
|
10701
10835
|
record.model = model;
|
|
10702
|
-
this.ensurePaneFor(paneId2);
|
|
10836
|
+
const paneCreated = this.ensurePaneFor(paneId2);
|
|
10703
10837
|
record.renderHandle = this.renderer.mountIndicator(model);
|
|
10704
10838
|
record.pendingStructural = false;
|
|
10839
|
+
if (paneCreated) this.events.emit("pane:changed", void 0);
|
|
10705
10840
|
this.announce(record, handle);
|
|
10706
10841
|
return true;
|
|
10707
10842
|
}
|
|
10708
10843
|
let paneId = record.model?.paneId ?? "price";
|
|
10709
10844
|
const prevOwnScale = record.model?.ownScale === true;
|
|
10845
|
+
let rerouted = false;
|
|
10710
10846
|
if (record.loading && !record.paneLocked) {
|
|
10711
10847
|
const routed = this.routePane(id, model, record.options ?? {});
|
|
10712
10848
|
if (routed !== paneId) {
|
|
@@ -10717,6 +10853,7 @@ var Vela = (function (exports) {
|
|
|
10717
10853
|
this.ensurePaneFor(routed);
|
|
10718
10854
|
record.pendingStructural = true;
|
|
10719
10855
|
paneId = routed;
|
|
10856
|
+
rerouted = true;
|
|
10720
10857
|
}
|
|
10721
10858
|
}
|
|
10722
10859
|
this.placeModel(model, id, paneId);
|
|
@@ -10729,6 +10866,7 @@ var Vela = (function (exports) {
|
|
|
10729
10866
|
} else {
|
|
10730
10867
|
this.renderer.updateIndicator(record.renderHandle, modelToValuePatch(model));
|
|
10731
10868
|
}
|
|
10869
|
+
if (rerouted) this.events.emit("pane:changed", void 0);
|
|
10732
10870
|
if (record.loading) this.setLoading(record, false);
|
|
10733
10871
|
this.announce(record, handle);
|
|
10734
10872
|
return true;
|
|
@@ -10743,13 +10881,16 @@ var Vela = (function (exports) {
|
|
|
10743
10881
|
const overlay = options.overlay ?? model.overlay;
|
|
10744
10882
|
return overlay ? "price" : `pane-${id}`;
|
|
10745
10883
|
}
|
|
10746
|
-
/** Each named value series' last
|
|
10747
|
-
* engine fills, so this works without an execution context.
|
|
10748
|
-
|
|
10884
|
+
/** Each named value series' value on the chart's last bar — the model is the one source
|
|
10885
|
+
* every engine fills, so this works without an execution context. Points past the last
|
|
10886
|
+
* bar (a series shifted forward) are skipped by time. */
|
|
10887
|
+
static plotsAtLastBar(model, lastBarTime) {
|
|
10749
10888
|
const out = {};
|
|
10750
10889
|
for (const spec of model.series) {
|
|
10751
10890
|
if (!isLineLikeSeries(spec)) continue;
|
|
10752
|
-
|
|
10891
|
+
let i = spec.points.length - 1;
|
|
10892
|
+
if (lastBarTime !== void 0) while (i >= 0 && spec.points[i].time > lastBarTime) i -= 1;
|
|
10893
|
+
out[spec.title] = spec.points[i]?.value ?? null;
|
|
10753
10894
|
}
|
|
10754
10895
|
return out;
|
|
10755
10896
|
}
|
|
@@ -10805,7 +10946,7 @@ var Vela = (function (exports) {
|
|
|
10805
10946
|
// whatever produced this run.
|
|
10806
10947
|
forming: this.config.live && this.bars.length > 0,
|
|
10807
10948
|
complete: this.historyState !== "backfill",
|
|
10808
|
-
plots: _EngineOrchestrator.plotsAtLastBar(model),
|
|
10949
|
+
plots: _EngineOrchestrator.plotsAtLastBar(model, lastBar?.time),
|
|
10809
10950
|
vars: snapshot2?.variables ?? {},
|
|
10810
10951
|
...snapshot2?.strategy ? { strategy: snapshot2.strategy } : {},
|
|
10811
10952
|
warnings: snapshot2?.warnings ?? [],
|
|
@@ -10874,8 +11015,9 @@ var Vela = (function (exports) {
|
|
|
10874
11015
|
return typeof a === "object" && a !== null && typeof b === "object" && b !== null && JSON.stringify(a) === JSON.stringify(b);
|
|
10875
11016
|
}
|
|
10876
11017
|
function blankedModel(model) {
|
|
11018
|
+
const { anchorTime: _anchor, ...rest } = model;
|
|
10877
11019
|
return {
|
|
10878
|
-
...
|
|
11020
|
+
...rest,
|
|
10879
11021
|
series: model.series.map((s) => s.kind === "candle" || s.kind === "bar" ? { ...s, bars: [] } : s.kind === "markers" ? { ...s, markers: [] } : { ...s, points: [] }),
|
|
10880
11022
|
fills: [],
|
|
10881
11023
|
backgrounds: [],
|
|
@@ -10898,12 +11040,14 @@ var Vela = (function (exports) {
|
|
|
10898
11040
|
if (s.kind === "candle" || s.kind === "bar") {
|
|
10899
11041
|
series.push({ seriesId: s.id, kind: "bars", bars: s.bars, barColors: s.barColors ?? [] });
|
|
10900
11042
|
for (const b of s.bars) {
|
|
11043
|
+
if (!b) continue;
|
|
10901
11044
|
if (b.time < from) from = b.time;
|
|
10902
11045
|
if (b.time > to) to = b.time;
|
|
10903
11046
|
}
|
|
10904
11047
|
} else if (isLineLikeSeries(s)) {
|
|
10905
11048
|
series.push({ seriesId: s.id, kind: "points", points: s.points });
|
|
10906
11049
|
for (const p of s.points) {
|
|
11050
|
+
if (!p) continue;
|
|
10907
11051
|
if (p.time < from) from = p.time;
|
|
10908
11052
|
if (p.time > to) to = p.time;
|
|
10909
11053
|
}
|
|
@@ -11218,6 +11362,17 @@ var Vela = (function (exports) {
|
|
|
11218
11362
|
if (this.guard()) this.controller.maximizePane(paneId);
|
|
11219
11363
|
return this;
|
|
11220
11364
|
}
|
|
11365
|
+
/**
|
|
11366
|
+
* Set a pane's height weight: the panes share the plot height in proportion to their
|
|
11367
|
+
* weights, so `setHeightWeight('price', 6)` next to two study panes at 1 gives the price
|
|
11368
|
+
* pane three quarters of the height. The weight sticks until the user drags a separator
|
|
11369
|
+
* or it is set again; a collapsed pane applies it when expanded. `weight` must be a
|
|
11370
|
+
* positive number.
|
|
11371
|
+
*/
|
|
11372
|
+
setHeightWeight(paneId, weight) {
|
|
11373
|
+
if (this.guard()) this.controller.setPaneHeightWeight(paneId, weight);
|
|
11374
|
+
return this;
|
|
11375
|
+
}
|
|
11221
11376
|
/** Move/merge an indicator to a pane — sugar for `handle.moveTo(...)` by id. */
|
|
11222
11377
|
moveIndicator(id, target) {
|
|
11223
11378
|
if (this.guard()) this.controller.moveIndicator(id, target);
|
|
@@ -23982,6 +24137,8 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
|
|
|
23982
24137
|
var LEGEND_ACTION_GAP_PX = 6;
|
|
23983
24138
|
var LEGEND_CTL_CSS = `cursor:pointer;display:inline-flex;align-items:center;justify-content:center;border:none;line-height:0;padding:0;width:${LEGEND_CTL_PX}px;height:${LEGEND_CTL_PX}px;border-radius:3px;box-sizing:border-box;flex:none;`;
|
|
23984
24139
|
var LEGEND_ROW_MIN_H = LEGEND_CTL_PX + LEGEND_ROW_PAD_Y * 2;
|
|
24140
|
+
var STACK_RAISE = "stack:raise";
|
|
24141
|
+
var STACK_LOWER = "stack:lower";
|
|
23985
24142
|
var EYE_SVG = iconAt("eye", LEGEND_ICON_PX2);
|
|
23986
24143
|
var EYE_OFF_SVG = iconAt("eye-off", LEGEND_ICON_PX2);
|
|
23987
24144
|
var GEAR_SVG = iconAt("gear", LEGEND_ICON_PX2);
|
|
@@ -24011,6 +24168,10 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
|
|
|
24011
24168
|
this.mobileLayout = false;
|
|
24012
24169
|
/** Pane move/merge hook — when set, rows get a "Move to" menu + become drag-to-pane sources. */
|
|
24013
24170
|
this.moveApi = null;
|
|
24171
|
+
/** Overlay restacking hook — when set, a row's menus offer to step it forward or back. */
|
|
24172
|
+
this.stackApi = null;
|
|
24173
|
+
/** What {@link orderRows} last ordered the rows by (skips unchanged frames). */
|
|
24174
|
+
this.rowOrderKey = "";
|
|
24014
24175
|
/** Host-contributed legend actions, resolved PER ROW at render time (see setLegendActions). */
|
|
24015
24176
|
this.legendActions = null;
|
|
24016
24177
|
/** Host-contributed callout bubbles, resolved PER ROW at render time (see setLegendCallouts). */
|
|
@@ -24149,6 +24310,10 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
|
|
|
24149
24310
|
setMoveApi(api) {
|
|
24150
24311
|
this.moveApi = api;
|
|
24151
24312
|
}
|
|
24313
|
+
/** Enable the context menu's "Bring in front of …" entry for overlay indicators. Null disables it. */
|
|
24314
|
+
setStackApi(api) {
|
|
24315
|
+
this.stackApi = api;
|
|
24316
|
+
}
|
|
24152
24317
|
/**
|
|
24153
24318
|
* Wire the host-contributed row actions. Re-calling replaces the provider and
|
|
24154
24319
|
* re-projects the rows already on screen (a late `registerLegendAction` appears after
|
|
@@ -24294,16 +24459,20 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
|
|
|
24294
24459
|
}
|
|
24295
24460
|
this.moveTargets = items.map((it) => it.target);
|
|
24296
24461
|
this.moveMenuId = id;
|
|
24297
|
-
const
|
|
24462
|
+
const stack = this.stackItems(id);
|
|
24463
|
+
const moves = items.map((it, i) => ({ id: String(i), label: it.label }));
|
|
24464
|
+
if (moves[0] && stack.length > 0) moves[0].separatorBefore = true;
|
|
24465
|
+
const descriptors = [...stack, ...moves];
|
|
24298
24466
|
if (!this.moveMenu) {
|
|
24299
24467
|
this.moveMenu = new Menu({
|
|
24300
24468
|
host: this.container,
|
|
24301
24469
|
items: descriptors,
|
|
24302
24470
|
placement: "bottom-start",
|
|
24303
24471
|
onSelect: (itemId) => {
|
|
24304
|
-
const target = this.moveTargets[Number(itemId)];
|
|
24305
24472
|
const rowId = this.moveMenuId;
|
|
24306
|
-
if (
|
|
24473
|
+
if (!rowId || this.runStackItem(itemId, rowId)) return;
|
|
24474
|
+
const target = this.moveTargets[Number(itemId)];
|
|
24475
|
+
if (target) api.move(rowId, target);
|
|
24307
24476
|
}
|
|
24308
24477
|
});
|
|
24309
24478
|
} else {
|
|
@@ -24316,18 +24485,24 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
|
|
|
24316
24485
|
this.moveMenu?.close();
|
|
24317
24486
|
}
|
|
24318
24487
|
// ── legend row context menu (right-click) ───────────────────────────────
|
|
24319
|
-
/** Open the row's action menu at the pointer
|
|
24488
|
+
/** Open the row's action menu at the pointer: an overlay indicator's restacking steps
|
|
24489
|
+
* first (where a layer is there to pass), then the values toggle. */
|
|
24320
24490
|
openRowMenu(id, x, y) {
|
|
24321
24491
|
this.closeRowMenu();
|
|
24322
24492
|
this.closeMoveMenu();
|
|
24323
24493
|
const row2 = this.rows.get(id);
|
|
24324
24494
|
if (!row2) return;
|
|
24325
24495
|
this.rowMenuId = id;
|
|
24326
|
-
const
|
|
24327
|
-
|
|
24328
|
-
|
|
24329
|
-
|
|
24330
|
-
|
|
24496
|
+
const stack = this.stackItems(id);
|
|
24497
|
+
const items = [
|
|
24498
|
+
...stack,
|
|
24499
|
+
{
|
|
24500
|
+
id: "values",
|
|
24501
|
+
label: "Indicator values",
|
|
24502
|
+
checked: this.rowValuesShown(row2),
|
|
24503
|
+
separatorBefore: stack.length > 0
|
|
24504
|
+
}
|
|
24505
|
+
];
|
|
24331
24506
|
if (!this.rowMenu) {
|
|
24332
24507
|
this.rowMenu = new Menu({
|
|
24333
24508
|
host: this.container,
|
|
@@ -24336,8 +24511,10 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
|
|
|
24336
24511
|
// Right-click action menu — checked state reads as a leading ✓ (see the kit Menu).
|
|
24337
24512
|
checkmarks: true,
|
|
24338
24513
|
onSelect: (itemId) => {
|
|
24339
|
-
|
|
24340
|
-
|
|
24514
|
+
const rowId = this.rowMenuId;
|
|
24515
|
+
if (!rowId || this.runStackItem(itemId, rowId)) return;
|
|
24516
|
+
if (itemId !== "values") return;
|
|
24517
|
+
const target = this.rows.get(rowId);
|
|
24341
24518
|
if (!target) return;
|
|
24342
24519
|
target.showValues = !this.rowValuesShown(target);
|
|
24343
24520
|
this.renderRowValues(target);
|
|
@@ -24351,6 +24528,55 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
|
|
|
24351
24528
|
closeRowMenu() {
|
|
24352
24529
|
this.rowMenu?.close();
|
|
24353
24530
|
}
|
|
24531
|
+
/** A row's one-step restacking entries (forward, then back), for whichever neighbor exists. */
|
|
24532
|
+
stackItems(id) {
|
|
24533
|
+
const api = this.stackApi;
|
|
24534
|
+
if (!api) return [];
|
|
24535
|
+
const items = [];
|
|
24536
|
+
const forward = this.stackLabel("Bring in front of", api.above(id), "the indicator in front");
|
|
24537
|
+
if (forward) items.push({ id: STACK_RAISE, label: forward });
|
|
24538
|
+
const back = this.stackLabel("Send behind", api.below(id), "the indicator behind");
|
|
24539
|
+
if (back) items.push({ id: STACK_LOWER, label: back });
|
|
24540
|
+
return items;
|
|
24541
|
+
}
|
|
24542
|
+
stackLabel(verb, neighbor, fallback) {
|
|
24543
|
+
if (neighbor === null) return null;
|
|
24544
|
+
if (neighbor === "price") return `${verb} price`;
|
|
24545
|
+
return `${verb} ${this.rows.get(neighbor.indicator)?.title ?? fallback}`;
|
|
24546
|
+
}
|
|
24547
|
+
/** Run a restacking entry; false when `itemId` is not one. */
|
|
24548
|
+
runStackItem(itemId, rowId) {
|
|
24549
|
+
if (itemId === STACK_RAISE) this.stackApi?.raise(rowId);
|
|
24550
|
+
else if (itemId === STACK_LOWER) this.stackApi?.lower(rowId);
|
|
24551
|
+
else return false;
|
|
24552
|
+
return true;
|
|
24553
|
+
}
|
|
24554
|
+
/**
|
|
24555
|
+
* List each pane's rows front-most first, the way the indicators stack (the object
|
|
24556
|
+
* tree's order). `backToFront` is the paint order; rows it doesn't name keep their
|
|
24557
|
+
* place after the others. Cheap to call every frame: no-op while nothing changed.
|
|
24558
|
+
*/
|
|
24559
|
+
orderRows(backToFront) {
|
|
24560
|
+
const key = `${backToFront.join(",")}|${[...this.rows.values()].map((r) => `${r.id}@${r.paneId}`).join(",")}`;
|
|
24561
|
+
if (key === this.rowOrderKey) return;
|
|
24562
|
+
this.rowOrderKey = key;
|
|
24563
|
+
const rank = new Map(backToFront.map((id, i) => [id, i]));
|
|
24564
|
+
for (const lg of this.legends.values()) {
|
|
24565
|
+
const els = [...this.rows.values()].filter((r) => r.el.parentElement === lg).map((r) => r.el);
|
|
24566
|
+
const dom = [...lg.children];
|
|
24567
|
+
const ofEl = new Map([...this.rows.values()].map((r) => [r.el, r.id]));
|
|
24568
|
+
const at = (el) => rank.get(ofEl.get(el)) ?? -1;
|
|
24569
|
+
els.sort((a, b) => {
|
|
24570
|
+
const ra = at(a);
|
|
24571
|
+
const rb = at(b);
|
|
24572
|
+
if (ra >= 0 && rb >= 0) return rb - ra;
|
|
24573
|
+
if (ra >= 0 || rb >= 0) return ra >= 0 ? -1 : 1;
|
|
24574
|
+
return dom.indexOf(a) - dom.indexOf(b);
|
|
24575
|
+
});
|
|
24576
|
+
const anchor = this.foldToggle?.parentElement === lg ? this.foldToggle : null;
|
|
24577
|
+
for (const el of els) lg.insertBefore(el, anchor);
|
|
24578
|
+
}
|
|
24579
|
+
}
|
|
24354
24580
|
/** Move a legend row to another pane (indicator merged/moved), tidying an emptied container. */
|
|
24355
24581
|
setPane(id, paneId) {
|
|
24356
24582
|
const row2 = this.rows.get(id);
|
|
@@ -26041,6 +26267,7 @@ void main() {
|
|
|
26041
26267
|
const i0 = Math.max(0, Math.floor(vr.from));
|
|
26042
26268
|
const i1 = Math.min(n - 1, Math.ceil(vr.to));
|
|
26043
26269
|
if (i1 < i0) return;
|
|
26270
|
+
const s1 = Math.max(i1, Math.ceil(vr.to));
|
|
26044
26271
|
gl.enable(gl.BLEND);
|
|
26045
26272
|
gl.enable(gl.SCISSOR_TEST);
|
|
26046
26273
|
const barColorMap = mergeBarColors(scene.indicators);
|
|
@@ -26099,8 +26326,8 @@ void main() {
|
|
|
26099
26326
|
b.alpha = this.modelAlpha;
|
|
26100
26327
|
const off = scene.offsetOf(m.id);
|
|
26101
26328
|
const mp = effPane(m);
|
|
26102
|
-
for (const f of m.fills) if (f.overlay !== true) this.emitFill(b, m, f, mp, coords, i0,
|
|
26103
|
-
for (const s of m.series) if (s.overlay !== true) this.emitSeries(b, s, mp, coords, i0,
|
|
26329
|
+
for (const f of m.fills) if (f.overlay !== true) this.emitFill(b, m, f, mp, coords, i0, s1, off);
|
|
26330
|
+
for (const s of m.series) if (s.overlay !== true) this.emitSeries(b, s, mp, coords, i0, s1, theme, off);
|
|
26104
26331
|
}
|
|
26105
26332
|
if (drawCandles && !candleDrawn) {
|
|
26106
26333
|
drawSlicesUpTo(scene.candleZ);
|
|
@@ -26111,11 +26338,11 @@ void main() {
|
|
|
26111
26338
|
if (isPrice) {
|
|
26112
26339
|
for (const m of scene.indicators.values()) {
|
|
26113
26340
|
const off = scene.offsetOf(m.id);
|
|
26114
|
-
for (const f of m.fills) if (f.overlay === true) this.emitFill(b, m, f, pane, coords, i0,
|
|
26341
|
+
for (const f of m.fills) if (f.overlay === true) this.emitFill(b, m, f, pane, coords, i0, s1, off);
|
|
26115
26342
|
}
|
|
26116
26343
|
for (const m of scene.indicators.values()) {
|
|
26117
26344
|
const off = scene.offsetOf(m.id);
|
|
26118
|
-
for (const s of m.series) if (s.overlay === true) this.emitSeries(b, s, pane, coords, i0,
|
|
26345
|
+
for (const s of m.series) if (s.overlay === true) this.emitSeries(b, s, pane, coords, i0, s1, theme, off);
|
|
26119
26346
|
}
|
|
26120
26347
|
}
|
|
26121
26348
|
drawSlicesUpTo(Infinity);
|
|
@@ -26137,7 +26364,7 @@ void main() {
|
|
|
26137
26364
|
if (this.glow > 0 && !this.screenProgram && !this.ensureScreenPipeline()) this.glow = 0;
|
|
26138
26365
|
if (this.glow > 0 && this.screenProgram) {
|
|
26139
26366
|
this.glowBatch.alpha = this.modelAlpha;
|
|
26140
|
-
this.renderGlow(gl, scene, coords, i0,
|
|
26367
|
+
this.renderGlow(gl, scene, coords, i0, s1);
|
|
26141
26368
|
}
|
|
26142
26369
|
}
|
|
26143
26370
|
/**
|
|
@@ -26694,7 +26921,7 @@ void main() {
|
|
|
26694
26921
|
const tickW = Math.max(1, Math.round(spacing * 0.35));
|
|
26695
26922
|
for (let i = i0; i <= i1; i += 1) {
|
|
26696
26923
|
const bar = s.bars[i - off];
|
|
26697
|
-
if (!bar) continue;
|
|
26924
|
+
if (!bar || !Number.isFinite(bar.open + bar.high + bar.low + bar.close)) continue;
|
|
26698
26925
|
const bc = s.barColors?.[i - off];
|
|
26699
26926
|
const body = parseColor(bc?.color ?? (bar.close >= bar.open ? up : down));
|
|
26700
26927
|
const x = Math.round(coords.logicalToX(i));
|
|
@@ -27347,6 +27574,8 @@ void main() {
|
|
|
27347
27574
|
const tapRelease = this.dragging && !this.moved && (!wasTouch || Math.hypot(x - this.startX, y - this.startY) <= TOUCH_TAP_SLOP);
|
|
27348
27575
|
if (this.dragging && this.region === "drawing") {
|
|
27349
27576
|
this.deps.drawingsPointerUp?.(x, y, this.snapMode(e));
|
|
27577
|
+
} else if (this.dragging && this.region === "separator") {
|
|
27578
|
+
this.deps.endPaneResize?.();
|
|
27350
27579
|
} else if (tapRelease && this.region === "data") {
|
|
27351
27580
|
this.deps.onClick(x, y);
|
|
27352
27581
|
} else if (this.dragging && this.region === "data" && wasTouch) {
|
|
@@ -27368,6 +27597,7 @@ void main() {
|
|
|
27368
27597
|
this.cancelLongPress();
|
|
27369
27598
|
if (!this.dragging) return;
|
|
27370
27599
|
if (this.region === "drawing" && !Number.isNaN(this.cursorX)) this.deps.drawingsPointerUp?.(this.cursorX, this.cursorY, this.snapMode(e));
|
|
27600
|
+
if (this.region === "separator") this.deps.endPaneResize?.();
|
|
27371
27601
|
if (this.region === "crosshair" || e.pointerType === "touch") this.deps.onPointerMove(null, null);
|
|
27372
27602
|
this.endGesture(e);
|
|
27373
27603
|
};
|
|
@@ -28044,9 +28274,12 @@ void main() {
|
|
|
28044
28274
|
setIndicatorZ(id, z) {
|
|
28045
28275
|
this.seriesZ.set(id, z);
|
|
28046
28276
|
}
|
|
28047
|
-
/** Snapshot of the current ordering for a UI/read API: `{ id, z }`
|
|
28277
|
+
/** Snapshot of the current ordering for a UI/read API: `{ id, z }` back to front, in the
|
|
28278
|
+
* order the indicators paint — on a shared key, mount order (see `orderedIndicatorsForPane`). */
|
|
28048
28279
|
indicatorZOrder() {
|
|
28049
|
-
|
|
28280
|
+
const mounted = new Map([...this.indicators.keys()].map((id, i) => [id, i]));
|
|
28281
|
+
const at = (id) => mounted.get(id) ?? Number.MAX_SAFE_INTEGER;
|
|
28282
|
+
return [...this.seriesZ.entries()].map(([id, z]) => ({ id, z })).sort((a, b) => a.z - b.z || at(a.id) - at(b.id));
|
|
28050
28283
|
}
|
|
28051
28284
|
/** The pane's series z keys (each indicator, plus the candles on the price pane), sorted
|
|
28052
28285
|
* ascending and de-duplicated — the boundaries a user drawing's z is slotted against. */
|
|
@@ -28074,16 +28307,19 @@ void main() {
|
|
|
28074
28307
|
for (const z of this.seriesZ.values()) if (z < min2) min2 = z;
|
|
28075
28308
|
return min2;
|
|
28076
28309
|
}
|
|
28310
|
+
/** Create the pane, or update an existing one in place. Without `heightWeight` an existing
|
|
28311
|
+
* pane keeps its weight (a user resize survives other indicators mounting onto it) and a
|
|
28312
|
+
* new one gets the default: 3 for the price pane, 1 for a study pane. */
|
|
28077
28313
|
ensurePane(id, kind, order, heightWeight) {
|
|
28078
28314
|
this.orderedCache = null;
|
|
28079
28315
|
const existing = this.panes.get(id);
|
|
28080
28316
|
if (existing) {
|
|
28081
28317
|
existing.order = order;
|
|
28082
|
-
existing.heightWeight = heightWeight;
|
|
28318
|
+
if (heightWeight !== void 0) existing.heightWeight = heightWeight;
|
|
28083
28319
|
existing.kind = kind;
|
|
28084
28320
|
return existing;
|
|
28085
28321
|
}
|
|
28086
|
-
const pane = { id, kind, order, heightWeight, bounds: { top: 0, height: 0 }, scale: { min: 0, max: 1 }, scaleTarget: { min: 0, max: 1 }, initialized: false, manualScale: null, collapsed: false, percentBaseline: 0 };
|
|
28322
|
+
const pane = { id, kind, order, heightWeight: heightWeight ?? (kind === "price" ? 3 : 1), bounds: { top: 0, height: 0 }, scale: { min: 0, max: 1 }, scaleTarget: { min: 0, max: 1 }, initialized: false, manualScale: null, collapsed: false, percentBaseline: 0 };
|
|
28087
28323
|
this.panes.set(id, pane);
|
|
28088
28324
|
return pane;
|
|
28089
28325
|
}
|
|
@@ -28140,6 +28376,7 @@ void main() {
|
|
|
28140
28376
|
const i0 = Math.max(0, Math.floor(vr.from));
|
|
28141
28377
|
const i1 = Math.min(n - 1, Math.ceil(vr.to));
|
|
28142
28378
|
if (i1 < i0) return;
|
|
28379
|
+
const s1 = Math.max(i1, Math.ceil(vr.to));
|
|
28143
28380
|
const barColorMap = mergeBarColors2(scene.indicators);
|
|
28144
28381
|
const panes = scene.orderedPanes();
|
|
28145
28382
|
for (const pane of panes) {
|
|
@@ -28182,8 +28419,8 @@ void main() {
|
|
|
28182
28419
|
ctx.globalAlpha = this.modelAlpha;
|
|
28183
28420
|
const off = scene.offsetOf(m.id);
|
|
28184
28421
|
const mp = effPane(m);
|
|
28185
|
-
for (const f of m.fills) if (f.overlay !== true) this.drawFill(ctx, m, f, mp, coords, i0,
|
|
28186
|
-
for (const s of m.series) if (s.overlay !== true) this.drawSeries(ctx, s, mp, coords, i0,
|
|
28422
|
+
for (const f of m.fills) if (f.overlay !== true) this.drawFill(ctx, m, f, mp, coords, i0, s1, off);
|
|
28423
|
+
for (const s of m.series) if (s.overlay !== true) this.drawSeries(ctx, s, mp, coords, i0, s1, theme, off);
|
|
28187
28424
|
}
|
|
28188
28425
|
if (drawCandles && !candleDrawn) {
|
|
28189
28426
|
drawSlicesUpTo(scene.candleZ);
|
|
@@ -28194,11 +28431,11 @@ void main() {
|
|
|
28194
28431
|
ctx.globalAlpha = this.modelAlpha;
|
|
28195
28432
|
for (const m of scene.indicators.values()) {
|
|
28196
28433
|
const off = scene.offsetOf(m.id);
|
|
28197
|
-
for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0,
|
|
28434
|
+
for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0, s1, off);
|
|
28198
28435
|
}
|
|
28199
28436
|
for (const m of scene.indicators.values()) {
|
|
28200
28437
|
const off = scene.offsetOf(m.id);
|
|
28201
|
-
for (const s of m.series) if (s.overlay === true) this.drawSeries(ctx, s, pane, coords, i0,
|
|
28438
|
+
for (const s of m.series) if (s.overlay === true) this.drawSeries(ctx, s, pane, coords, i0, s1, theme, off);
|
|
28202
28439
|
}
|
|
28203
28440
|
}
|
|
28204
28441
|
drawSlicesUpTo(Infinity);
|
|
@@ -28650,7 +28887,7 @@ void main() {
|
|
|
28650
28887
|
const tickW = Math.max(1, Math.round(spacing * 0.35));
|
|
28651
28888
|
for (let i = i0; i <= i1; i += 1) {
|
|
28652
28889
|
const b = s.bars[i - off];
|
|
28653
|
-
if (!b) continue;
|
|
28890
|
+
if (!b || !Number.isFinite(b.open + b.high + b.low + b.close)) continue;
|
|
28654
28891
|
const bc = s.barColors?.[i - off];
|
|
28655
28892
|
const body = bc?.color ?? (b.close >= b.open ? up : down);
|
|
28656
28893
|
const x = Math.round(coords.logicalToX(i)) + 0.5;
|
|
@@ -39538,6 +39775,58 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
39538
39775
|
return { below, above };
|
|
39539
39776
|
}
|
|
39540
39777
|
|
|
39778
|
+
// src/renderers/native/core/overlayStack.ts
|
|
39779
|
+
function priceStack(indicators, candleZ) {
|
|
39780
|
+
const out = [...indicators];
|
|
39781
|
+
const at = out.findIndex((e) => e.z >= candleZ);
|
|
39782
|
+
out.splice(at < 0 ? out.length : at, 0, { id: null, z: candleZ });
|
|
39783
|
+
return out;
|
|
39784
|
+
}
|
|
39785
|
+
function untie(stack) {
|
|
39786
|
+
const out = [];
|
|
39787
|
+
for (let i = 0; i < stack.length; ) {
|
|
39788
|
+
let j = i + 1;
|
|
39789
|
+
while (j < stack.length && stack[j].z === stack[i].z) j += 1;
|
|
39790
|
+
const run = j - i;
|
|
39791
|
+
if (run > 1) {
|
|
39792
|
+
const base = stack[i].z;
|
|
39793
|
+
const step = ((stack[j]?.z ?? base + 1) - base) / run;
|
|
39794
|
+
for (let k = 1; k < run; k += 1) {
|
|
39795
|
+
const id = stack[i + k].id;
|
|
39796
|
+
if (id !== null) out.push({ id, z: base + k * step });
|
|
39797
|
+
}
|
|
39798
|
+
}
|
|
39799
|
+
i = j;
|
|
39800
|
+
}
|
|
39801
|
+
return out;
|
|
39802
|
+
}
|
|
39803
|
+
function layerAbove(stack, id) {
|
|
39804
|
+
const i = stack.findIndex((e) => e.id === id);
|
|
39805
|
+
if (i < 0 || i === stack.length - 1) return null;
|
|
39806
|
+
const above = stack[i + 1];
|
|
39807
|
+
return above.id === null ? "price" : { indicator: above.id };
|
|
39808
|
+
}
|
|
39809
|
+
function layerBelow(stack, id) {
|
|
39810
|
+
const i = stack.findIndex((e) => e.id === id);
|
|
39811
|
+
if (i <= 0) return null;
|
|
39812
|
+
const below = stack[i - 1];
|
|
39813
|
+
return below.id === null ? "price" : { indicator: below.id };
|
|
39814
|
+
}
|
|
39815
|
+
function raisedZ(stack, id) {
|
|
39816
|
+
const i = stack.findIndex((e) => e.id === id);
|
|
39817
|
+
if (i < 0 || i === stack.length - 1) return null;
|
|
39818
|
+
const above = stack[i + 1];
|
|
39819
|
+
const next2 = stack[i + 2];
|
|
39820
|
+
return next2 ? (above.z + next2.z) / 2 : above.z + 1;
|
|
39821
|
+
}
|
|
39822
|
+
function loweredZ(stack, id) {
|
|
39823
|
+
const i = stack.findIndex((e) => e.id === id);
|
|
39824
|
+
if (i <= 0) return null;
|
|
39825
|
+
const below = stack[i - 1];
|
|
39826
|
+
const prev3 = stack[i - 2];
|
|
39827
|
+
return prev3 ? (prev3.z + below.z) / 2 : below.z - 1;
|
|
39828
|
+
}
|
|
39829
|
+
|
|
39541
39830
|
// src/renderers/native/chrome/vela-logos.ts
|
|
39542
39831
|
var VELA_MARK_SVG = '<svg viewBox="0 -142 161.66 148" aria-hidden="true"><g fill="currentColor"><path d="M0 -140L32.91 -140L77.52 -62.73L61.06 -34.24Z"/><path d="M161.66 -140L128.75 -140L84.14 -62.73L100.59 -34.24Z"/><path d="M80.83 -52.04L95.85 -26.02L80.83 0L65.81 -26.02Z"/></g></svg>';
|
|
39543
39832
|
var VELA_LETTERS_SVG = '<svg viewBox="161.66 -142 310.57 148" aria-hidden="true"><g fill="currentColor"><path d="M226.3 1.17Q200.81 1.17 186.12 -12.99Q171.42 -27.15 171.42 -51.17Q171.42 -75.2 185.87 -89.6Q200.33 -104 224.25 -104Q247.4 -104 261.7 -90.58Q276.01 -77.15 276.3 -54.59Q276.3 -49.9 275.42 -45.02H200.81V-43.55Q201.2 -32.62 207.75 -26.42Q214.29 -20.21 225.03 -20.21Q233.72 -20.21 239.34 -23.93Q244.95 -27.64 246.81 -34.77H274.64Q272.1 -19.14 259.16 -8.98Q246.22 1.17 226.3 1.17ZM201.5 -63.38H247.88Q246.42 -72.75 240.27 -77.88Q234.12 -83.01 224.45 -83.01Q215.17 -83.01 208.92 -77.73Q202.67 -72.46 201.5 -63.38ZM287.54 0V-140.04H315.76V0ZM429.34 -23.83H432.56V0H418.11Q407.76 0 403.36 -4.44Q398.97 -8.89 399.16 -16.8Q387.83 1.17 365.57 1.17Q348.48 1.17 337.88 -6.74Q327.29 -14.65 327.29 -29Q327.29 -45.41 339.45 -54.3Q351.6 -63.18 374.55 -63.18H394.96V-68.16Q394.96 -75.2 390.08 -79.3Q385.2 -83.4 376.51 -83.4Q368.3 -83.4 363.03 -79.98Q357.76 -76.56 356.88 -70.8H329.73Q331.19 -85.84 344.08 -94.92Q356.97 -104 377.29 -104Q399.06 -104 410.88 -94.43Q422.7 -84.86 422.7 -66.6V-30.18Q422.7 -26.37 424.26 -25.1Q425.82 -23.83 429.34 -23.83ZM394.96 -42.38V-44.43H374.36Q365.47 -44.43 360.59 -40.92Q355.7 -37.4 355.7 -30.96Q355.7 -25.59 359.81 -22.41Q363.91 -19.24 370.94 -19.24Q381.97 -19.24 388.37 -25.54Q394.77 -31.84 394.96 -42.38Z"/><path fill-opacity="0.55" d="M437.44 -124.28H441.96V-113.41H445.34V-124.28H449.87V-127.4H437.44ZM451.43 -113.41H454.7V-123.29L458.97 -113.41H461.62L465.99 -123.29V-113.41H469.21V-127.4H464.53L460.42 -117.68L456.16 -127.4H451.43Z"/></g></svg>';
|
|
@@ -40188,6 +40477,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
40188
40477
|
// pane just below it
|
|
40189
40478
|
this.resizeSplitStart = null;
|
|
40190
40479
|
// the two panes' shared span when the drag began
|
|
40480
|
+
this.resizeStartAboveWeight = 0;
|
|
40481
|
+
// the upper pane's weight when the drag began
|
|
40191
40482
|
/** The user drawings take part in pointer input (the runtime `drawingsInteractive` feature). */
|
|
40192
40483
|
this.drawingsInteractive = true;
|
|
40193
40484
|
this.hoverSeparatorY = null;
|
|
@@ -40830,6 +41121,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
40830
41121
|
};
|
|
40831
41122
|
this.scene.candleZ = next2.stacking.candles;
|
|
40832
41123
|
for (const [id, z] of Object.entries(next2.stacking.series)) this.scene.setIndicatorZ(id, z);
|
|
41124
|
+
this.syncLegendOrder();
|
|
40833
41125
|
s.bars = { upColor: next2.bars.upColor, downColor: next2.bars.downColor };
|
|
40834
41126
|
s.line = { color: next2.line.color, width: next2.line.width };
|
|
40835
41127
|
s.area = { lineColor: next2.area.lineColor, width: next2.area.width, topColor: next2.area.topColor, bottomColor: next2.area.bottomColor };
|
|
@@ -40875,6 +41167,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
40875
41167
|
if (typeof v.z === "number") this.scene.setIndicatorZ(v.id, v.z);
|
|
40876
41168
|
else if (v.to === "front") this.scene.bringIndicatorToFront(v.id);
|
|
40877
41169
|
else if (v.to === "back") this.scene.sendIndicatorToBack(v.id);
|
|
41170
|
+
this.syncLegendOrder();
|
|
40878
41171
|
}
|
|
40879
41172
|
/** Toggle the in-chart settings gear + dialog (the `settings` feature). The gear
|
|
40880
41173
|
* sits above the time axis, clear of the right price-axis strip. */
|
|
@@ -41262,7 +41555,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
41262
41555
|
this.chrome.mount(this.chromeCanvas);
|
|
41263
41556
|
this.crosshairLayer.mount(this.cursorCanvas);
|
|
41264
41557
|
this.coords.setViewport(defaultViewport());
|
|
41265
|
-
this.scene.ensurePane(PRICE_PANE_ID, "price", 0
|
|
41558
|
+
this.scene.ensurePane(PRICE_PANE_ID, "price", 0);
|
|
41266
41559
|
this.scheduler = new Scheduler((level) => this.renderFrame(level));
|
|
41267
41560
|
this.animator = new Animator((dt) => this.animTick(dt));
|
|
41268
41561
|
this.targetBarSpacing = this.coords.getViewport().barSpacing;
|
|
@@ -41275,7 +41568,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
41275
41568
|
onClick: (x, y) => {
|
|
41276
41569
|
if (this.handleMarkClick(x, y)) return;
|
|
41277
41570
|
this.userDrawings?.deselect();
|
|
41278
|
-
this.handleClick(x);
|
|
41571
|
+
this.handleClick(x, y);
|
|
41279
41572
|
},
|
|
41280
41573
|
onAxisLongPress: (axis, x, y) => {
|
|
41281
41574
|
for (const cb of this.axisLongPressCbs) cb({ axis, x, y });
|
|
@@ -41289,6 +41582,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
41289
41582
|
paneSeparatorAt: (y) => this.paneSeparatorAt(y),
|
|
41290
41583
|
beginPaneResize: (y) => this.beginPaneResize(y),
|
|
41291
41584
|
paneResizeBy: (dy) => this.paneResizeBy(dy),
|
|
41585
|
+
endPaneResize: () => this.endPaneResize(),
|
|
41292
41586
|
resetPaneSize: (y) => this.resetPaneSize(y),
|
|
41293
41587
|
resetView: () => this.resetView(),
|
|
41294
41588
|
// User drawings claim a gesture before pan when armed / over a drawing.
|
|
@@ -41414,6 +41708,12 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
41414
41708
|
for (const cb of this.moveIndicatorCbs) cb(id, target);
|
|
41415
41709
|
}
|
|
41416
41710
|
});
|
|
41711
|
+
this.inputsUI.setStackApi({
|
|
41712
|
+
above: (id) => layerAbove(this.overlayStack(), id),
|
|
41713
|
+
below: (id) => layerBelow(this.overlayStack(), id),
|
|
41714
|
+
raise: (id) => this.restackOverlay(id, raisedZ),
|
|
41715
|
+
lower: (id) => this.restackOverlay(id, loweredZ)
|
|
41716
|
+
});
|
|
41417
41717
|
this.paneControls = new PaneControls(this.plot, theme, {
|
|
41418
41718
|
panes: () => this.paneControlViews(),
|
|
41419
41719
|
rightAxis: () => this.rightAxisW,
|
|
@@ -41731,7 +42031,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
41731
42031
|
}
|
|
41732
42032
|
// ── panes ──
|
|
41733
42033
|
ensurePane(pane) {
|
|
41734
|
-
this.scene.ensurePane(pane.id, pane.kind, pane.order, pane.heightWeight
|
|
42034
|
+
this.scene.ensurePane(pane.id, pane.kind, pane.order, pane.heightWeight);
|
|
41735
42035
|
this.layoutPanes();
|
|
41736
42036
|
this.paneControls?.refresh();
|
|
41737
42037
|
this.scheduler.invalidate(4 /* Full */);
|
|
@@ -41753,6 +42053,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
41753
42053
|
model.ownScale = opts?.ownScale === true;
|
|
41754
42054
|
if (!model.ownScale) this.scene.dropIndicatorScale(handle.id);
|
|
41755
42055
|
this.inputsUI.setPane(handle.id, paneId);
|
|
42056
|
+
this.syncLegendOrder();
|
|
41756
42057
|
this.refreshAnchorOffset(model);
|
|
41757
42058
|
this.refreshAxisWidth();
|
|
41758
42059
|
this.layoutPanes();
|
|
@@ -41780,6 +42081,15 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
41780
42081
|
this.paneControls?.refresh();
|
|
41781
42082
|
this.scheduler.invalidate(4 /* Full */);
|
|
41782
42083
|
}
|
|
42084
|
+
setPaneHeightWeight(paneId, weight) {
|
|
42085
|
+
const pane = this.scene.panes.get(paneId);
|
|
42086
|
+
if (!pane || !(weight > 0) || pane.heightWeight === weight) return;
|
|
42087
|
+
pane.heightWeight = weight;
|
|
42088
|
+
this.afterPaneLayoutChange();
|
|
42089
|
+
}
|
|
42090
|
+
paneHeightWeight(paneId) {
|
|
42091
|
+
return this.scene.panes.get(paneId)?.heightWeight ?? null;
|
|
42092
|
+
}
|
|
41783
42093
|
onPaneAction(cb) {
|
|
41784
42094
|
this.paneActionCbs.add(cb);
|
|
41785
42095
|
return () => this.paneActionCbs.delete(cb);
|
|
@@ -41874,6 +42184,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
41874
42184
|
this.vpvrHidden = false;
|
|
41875
42185
|
}
|
|
41876
42186
|
if (model.ownScale) this.refreshAxisWidth();
|
|
42187
|
+
this.syncLegendOrder();
|
|
41877
42188
|
this.paneControls?.refresh();
|
|
41878
42189
|
this.scheduler.invalidate(4 /* Full */);
|
|
41879
42190
|
return { id: model.id };
|
|
@@ -42281,7 +42592,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
42281
42592
|
this.setMarkHover(null);
|
|
42282
42593
|
this.scheduler.invalidate(1 /* Cursor */);
|
|
42283
42594
|
this.hoverLogical = null;
|
|
42284
|
-
const empty = { time: null, price: null, paneKind: null, values: /* @__PURE__ */ new Map(), ohlc: null };
|
|
42595
|
+
const empty = { time: null, price: null, paneKind: null, paneId: null, values: /* @__PURE__ */ new Map(), ohlc: null };
|
|
42285
42596
|
for (const cb of this.crosshairCbs) cb(empty);
|
|
42286
42597
|
return;
|
|
42287
42598
|
}
|
|
@@ -42312,14 +42623,23 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
42312
42623
|
const bar = onBar ? this.bars[logical] : void 0;
|
|
42313
42624
|
const ohlc = bar ? { time: bar.time, open: bar.open, high: bar.high, low: bar.low, close: bar.close, volume: bar.volume } : null;
|
|
42314
42625
|
this.hoverLogical = onBar ? logical : null;
|
|
42315
|
-
const
|
|
42626
|
+
const onPane = inData && pane ? pane : null;
|
|
42627
|
+
const event = { time, price, paneKind: onPane?.kind ?? null, paneId: onPane?.id ?? null, values, ohlc };
|
|
42316
42628
|
for (const cb of this.crosshairCbs) cb(event);
|
|
42317
42629
|
}
|
|
42318
|
-
handleClick(x) {
|
|
42630
|
+
handleClick(x, y) {
|
|
42319
42631
|
if (this.coords.barCount === 0) return;
|
|
42320
42632
|
const logical = Math.round(this.coords.xToLogical(x));
|
|
42321
42633
|
const onBar = logical >= 0 && logical < this.coords.barCount;
|
|
42322
|
-
|
|
42634
|
+
const inData = x >= 0 && y >= 0 && x <= this.coords.width && y <= this.coords.height;
|
|
42635
|
+
const pane = inData ? this.paneNodeAtY(y) : null;
|
|
42636
|
+
const event = {
|
|
42637
|
+
time: onBar ? this.coords.logicalToTime(logical) : null,
|
|
42638
|
+
price: pane ? this.coords.yToPrice(y, pane.scale, pane.bounds) : null,
|
|
42639
|
+
paneKind: pane?.kind ?? null,
|
|
42640
|
+
paneId: pane?.id ?? null
|
|
42641
|
+
};
|
|
42642
|
+
for (const cb of this.clickCbs) cb(event);
|
|
42323
42643
|
}
|
|
42324
42644
|
/**
|
|
42325
42645
|
* Fan out (or collapse, with null) a multi-group mark stack; repaints the chrome tier when
|
|
@@ -42564,6 +42884,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
42564
42884
|
const below = panes[i];
|
|
42565
42885
|
this.resizeAbove = above;
|
|
42566
42886
|
this.resizeBelow = below;
|
|
42887
|
+
this.resizeStartAboveWeight = above.heightWeight;
|
|
42567
42888
|
this.resizeSplitStart = {
|
|
42568
42889
|
combinedTop: above.bounds.top,
|
|
42569
42890
|
combinedHeight: above.bounds.height + below.bounds.height,
|
|
@@ -42584,6 +42905,16 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
42584
42905
|
this.layoutPanes();
|
|
42585
42906
|
this.scheduler.invalidate(4 /* Full */);
|
|
42586
42907
|
}
|
|
42908
|
+
/** Release the separator: report the resize once, at the end of the drag, if it changed anything. */
|
|
42909
|
+
endPaneResize() {
|
|
42910
|
+
const above = this.resizeAbove;
|
|
42911
|
+
const below = this.resizeBelow;
|
|
42912
|
+
const startAbove = this.resizeStartAboveWeight;
|
|
42913
|
+
this.resizeAbove = null;
|
|
42914
|
+
this.resizeBelow = null;
|
|
42915
|
+
this.resizeSplitStart = null;
|
|
42916
|
+
if (above && below && above.heightWeight !== startAbove) this.emitPaneAction({ type: "resize", paneIds: [above.id, below.id] });
|
|
42917
|
+
}
|
|
42587
42918
|
/** Double-click a separator → split the two adjacent panes evenly (each gets half of
|
|
42588
42919
|
* their combined weight). A simple, predictable reset that leaves siblings untouched. */
|
|
42589
42920
|
resetPaneSize(y) {
|
|
@@ -42597,6 +42928,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
42597
42928
|
below.heightWeight = half;
|
|
42598
42929
|
this.layoutPanes();
|
|
42599
42930
|
this.scheduler.invalidate(4 /* Full */);
|
|
42931
|
+
this.emitPaneAction({ type: "resize", paneIds: [above.id, below.id] });
|
|
42600
42932
|
}
|
|
42601
42933
|
// ── keyboard navigation / accessibility (item 11) ──
|
|
42602
42934
|
/** Enable/disable keyboard control: focusability + ARIA + key listeners + live region. */
|
|
@@ -42851,6 +43183,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
42851
43183
|
/** Paint the below-data (L-1) + geometry (L0) + chrome (L1) layers from the current scene/coords. */
|
|
42852
43184
|
paintData() {
|
|
42853
43185
|
this.syncLayerCanvasOrder();
|
|
43186
|
+
this.syncLegendOrder();
|
|
42854
43187
|
this.stampScaleInvert();
|
|
42855
43188
|
this.backend.modelAlpha = this.modelAlpha;
|
|
42856
43189
|
this.backend.candleBodyAlpha = this.candleBodyAlpha;
|
|
@@ -43032,6 +43365,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
43032
43365
|
const i0 = Math.max(0, Math.floor(vr.from));
|
|
43033
43366
|
const i1 = Math.min(n - 1, Math.ceil(vr.to));
|
|
43034
43367
|
if (i1 < i0) return;
|
|
43368
|
+
const s1 = Math.max(i1, Math.ceil(vr.to));
|
|
43035
43369
|
this.scene.percentBaseline = this.bars[i0]?.close ?? 0;
|
|
43036
43370
|
const panes = this.scene.orderedPanes();
|
|
43037
43371
|
const pricePane = panes.find((p) => p.kind === "price") ?? null;
|
|
@@ -43049,11 +43383,11 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
43049
43383
|
const masterModels = models.filter((m) => m.ownScale !== true);
|
|
43050
43384
|
let dr = this.chrome.paneDrawingsRange(masterModels, this.scene, pane === pricePane, vr);
|
|
43051
43385
|
if (pane === pricePane) {
|
|
43052
|
-
const or2 = overlaySeriesRange(this.scene.indicators.values(), i0,
|
|
43386
|
+
const or2 = overlaySeriesRange(this.scene.indicators.values(), i0, s1, (id) => this.scene.offsetOf(id));
|
|
43053
43387
|
if (or2) dr = dr ? { min: Math.min(dr.min, or2.min), max: Math.max(dr.max, or2.max) } : or2;
|
|
43054
43388
|
}
|
|
43055
43389
|
const includeCandles = pane.kind === "price" && (!this.scene.candlesHidden || this.priceLayersAnchoredToBars(masterModels) || !this.paneHasMeasurableContent(masterModels, dr));
|
|
43056
|
-
pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0,
|
|
43390
|
+
pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, s1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id), margins);
|
|
43057
43391
|
pane.percentBaseline = pane.kind === "price" ? this.bars[i0]?.close ?? 0 : this.firstVisibleValue(masterModels, i0);
|
|
43058
43392
|
pane.axisFormat = void 0;
|
|
43059
43393
|
pane.axisBands = void 0;
|
|
@@ -43093,7 +43427,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
43093
43427
|
continue;
|
|
43094
43428
|
}
|
|
43095
43429
|
const mdr = this.chrome.paneDrawingsRange([model], this.scene, false, vr);
|
|
43096
|
-
sl.scaleTarget = computePaneScale([model], this.bars, false, i0,
|
|
43430
|
+
sl.scaleTarget = computePaneScale([model], this.bars, false, i0, s1, mdr, false, (id) => this.scene.offsetOf(id), margins);
|
|
43097
43431
|
if (!animating || !sl.initialized) {
|
|
43098
43432
|
sl.scale = { ...sl.scaleTarget };
|
|
43099
43433
|
sl.initialized = true;
|
|
@@ -43164,6 +43498,30 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
43164
43498
|
const master = models.find((m) => m.ownScale !== true) ?? models[0];
|
|
43165
43499
|
return master?.title || null;
|
|
43166
43500
|
}
|
|
43501
|
+
/**
|
|
43502
|
+
* The price pane's layers back to front, as the legend restacks them: every indicator
|
|
43503
|
+
* with a legend row, in paint order, plus the candles. The same layers and order the
|
|
43504
|
+
* object tree lists (see `indicatorZOrder`).
|
|
43505
|
+
*/
|
|
43506
|
+
overlayStack() {
|
|
43507
|
+
const painted = this.scene.orderedIndicatorsForPane(PRICE_PANE_ID).filter((m) => m.legend !== false).map((m) => ({ id: m.id, z: this.scene.zOf(m.id) }));
|
|
43508
|
+
return priceStack(painted, this.scene.candleZ);
|
|
43509
|
+
}
|
|
43510
|
+
/** List the legend rows the way the indicators stack, front-most first (no-op when unchanged). */
|
|
43511
|
+
syncLegendOrder() {
|
|
43512
|
+
this.inputsUI?.orderRows(this.scene.indicatorZOrder().map((e) => e.id));
|
|
43513
|
+
}
|
|
43514
|
+
/** Apply a legend restacking step: shared keys are spread apart first (the order stays),
|
|
43515
|
+
* so the step lands exactly one layer over. No-op when there is nothing to step over. */
|
|
43516
|
+
restackOverlay(id, step) {
|
|
43517
|
+
for (const fix of untie(this.overlayStack())) this.scene.setIndicatorZ(fix.id, fix.z);
|
|
43518
|
+
const z = step(this.overlayStack(), id);
|
|
43519
|
+
if (z === null) return;
|
|
43520
|
+
this.scene.setIndicatorZ(id, z);
|
|
43521
|
+
this.syncLegendOrder();
|
|
43522
|
+
this.scheduler.invalidate(4 /* Full */);
|
|
43523
|
+
for (const cb of this.configChangedCbs) cb();
|
|
43524
|
+
}
|
|
43167
43525
|
/** The pane a bespoke-layer native indicator (volume/vpvr) currently lives in (null = not mounted). */
|
|
43168
43526
|
nativeLayerPane(type) {
|
|
43169
43527
|
for (const m of this.scene.indicators.values()) {
|
|
@@ -43618,17 +43976,35 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
43618
43976
|
for (const s of model.series) {
|
|
43619
43977
|
const times = s.kind === "candle" || s.kind === "bar" ? s.bars : isLineLikeSeries(s) ? s.points : null;
|
|
43620
43978
|
if (!times || times.length === 0) continue;
|
|
43621
|
-
|
|
43622
|
-
|
|
43623
|
-
while (lo < hi) {
|
|
43624
|
-
const mid = lo + hi >> 1;
|
|
43625
|
-
if (times[mid].time < headTime) lo = mid + 1;
|
|
43626
|
-
else hi = mid;
|
|
43627
|
-
}
|
|
43628
|
-
return lo;
|
|
43979
|
+
const count = slotsBefore(times, headTime);
|
|
43980
|
+
if (count !== null) return count;
|
|
43629
43981
|
}
|
|
43630
43982
|
return null;
|
|
43631
43983
|
}
|
|
43984
|
+
function slotsBefore(times, headTime) {
|
|
43985
|
+
let lo = 0;
|
|
43986
|
+
let hi = times.length;
|
|
43987
|
+
while (lo < hi) {
|
|
43988
|
+
const mid = lo + hi >> 1;
|
|
43989
|
+
const t = times[mid]?.time;
|
|
43990
|
+
if (t === void 0) return slotsBeforeScan(times, headTime);
|
|
43991
|
+
if (t < headTime) lo = mid + 1;
|
|
43992
|
+
else hi = mid;
|
|
43993
|
+
}
|
|
43994
|
+
return lo;
|
|
43995
|
+
}
|
|
43996
|
+
function slotsBeforeScan(times, headTime) {
|
|
43997
|
+
let lastBefore = -1;
|
|
43998
|
+
let any = false;
|
|
43999
|
+
for (let i = 0; i < times.length; i += 1) {
|
|
44000
|
+
const t = times[i]?.time;
|
|
44001
|
+
if (t === void 0) continue;
|
|
44002
|
+
any = true;
|
|
44003
|
+
if (t >= headTime) return t === headTime ? i : lastBefore + 1;
|
|
44004
|
+
lastBefore = i;
|
|
44005
|
+
}
|
|
44006
|
+
return any ? times.length : null;
|
|
44007
|
+
}
|
|
43632
44008
|
function applyPatch(model, patch) {
|
|
43633
44009
|
if (patch.kind === "value") {
|
|
43634
44010
|
for (const delta of patch.series) {
|
|
@@ -44041,6 +44417,9 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
44041
44417
|
overlay: spec.overlay,
|
|
44042
44418
|
// Studies stack (a 20 and a 200 moving average side by side) — unlike the layer-backed natives.
|
|
44043
44419
|
multiInstance: true,
|
|
44420
|
+
// A study's values depend on every bar before them (an EMA's seed, an RSI's
|
|
44421
|
+
// smoothing), so it computes once over the whole history, as script indicators do.
|
|
44422
|
+
waitsForHistory: true,
|
|
44044
44423
|
inputsSchema: () => spec.inputs,
|
|
44045
44424
|
defaultInputs: () => Object.fromEntries(spec.inputs.map((i) => [i.key, i.defval])),
|
|
44046
44425
|
create: () => new ClassicIndicator(spec)
|
|
@@ -44876,41 +45255,50 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
44876
45255
|
return directionalChannel(bars, basis, map(basis, (x) => x * (1 + k)), map(basis, (x) => x * (1 - k)), ink, "envelope");
|
|
44877
45256
|
}
|
|
44878
45257
|
};
|
|
44879
|
-
var
|
|
44880
|
-
|
|
44881
|
-
|
|
44882
|
-
|
|
44883
|
-
|
|
44884
|
-
|
|
44885
|
-
|
|
45258
|
+
var leastSquaresMa = {
|
|
45259
|
+
type: "lsma",
|
|
45260
|
+
title: "Least Squares Moving Average",
|
|
45261
|
+
shortTitle: "LSMA",
|
|
45262
|
+
overlay: true,
|
|
45263
|
+
inputs: [
|
|
45264
|
+
sourceInput("Close", "source", "Source", "Price series the regression line is fitted to."),
|
|
45265
|
+
{ ...lengthInput(25, "length", "LSMA Length", 5e3, "Number of bars N in each least-squares fit. Every bar fits a straight line to its last N values and plots where that line ends, so the average leads turns that a same-length SMA trails, at the cost of overshooting after sharp moves."), min: 2 },
|
|
45266
|
+
intInput("offset", "Regression Offset", 0, 0, 500, "Bars to step back along each bar's fitted line. 0 plots the line's endpoint at the current bar; larger values read the fit further back, trading lead for smoothness."),
|
|
45267
|
+
...directionInputs()
|
|
45268
|
+
],
|
|
45269
|
+
compute: (bars, inputs) => {
|
|
45270
|
+
const src = sourceValues(bars, str3(inputs, "source", "Close"));
|
|
45271
|
+
const values = linreg(src, num3(inputs, "length", 25), Math.trunc(num3(inputs, "offset", 0)));
|
|
45272
|
+
return { plots: [slopeColoredPlot("lsma", "LSMA", values, inputs)] };
|
|
45273
|
+
}
|
|
45274
|
+
};
|
|
45275
|
+
var linearRegressionSlope = {
|
|
45276
|
+
type: "linear-regression-slope",
|
|
45277
|
+
title: "Linear Regression Slope",
|
|
45278
|
+
shortTitle: "LRS",
|
|
44886
45279
|
overlay: false,
|
|
44887
45280
|
inputs: [
|
|
44888
|
-
optionInput("mode", "Mode", LINREG_CURVE, [LINREG_CURVE, LINREG_SLOPE], "Curve plots the regression value on the price chart; Slope plots the per-bar slope of the fit in the pane."),
|
|
44889
45281
|
{ ...lengthInput(100, "length", "Length", 5e3, "Number of bars used to fit the linear regression."), min: 2 },
|
|
44890
45282
|
sourceInput("Close", "source", "Source", "Price series the regression is fitted to."),
|
|
44891
|
-
colorInput(BULLISH, "Rising", "bullColor", "Color of the
|
|
44892
|
-
colorInput(BEARISH, "Falling", "bearColor", "Color of the
|
|
44893
|
-
colorInput(NEUTRAL, "
|
|
45283
|
+
colorInput(BULLISH, "Rising", "bullColor", "Color of the slope line while the fit slopes upward."),
|
|
45284
|
+
colorInput(BEARISH, "Falling", "bearColor", "Color of the slope line while the fit slopes downward."),
|
|
45285
|
+
colorInput(NEUTRAL, "Slope Line", "lineColor", "Color of the slope line while the fit is flat.")
|
|
44894
45286
|
],
|
|
44895
45287
|
compute: (bars, inputs) => {
|
|
44896
45288
|
const len = num3(inputs, "length", 100);
|
|
44897
45289
|
const src = sourceValues(bars, str3(inputs, "source", "Close"));
|
|
44898
|
-
const
|
|
44899
|
-
const slope = zip(curve, linreg(src, len, 1), (a, b) => a - b);
|
|
45290
|
+
const slope = zip(linreg(src, len, 0), linreg(src, len, 1), (a, b) => a - b);
|
|
44900
45291
|
const bull = str3(inputs, "bullColor", BULLISH);
|
|
44901
45292
|
const bear = str3(inputs, "bearColor", BEARISH);
|
|
44902
45293
|
const flat = str3(inputs, "lineColor", NEUTRAL);
|
|
44903
45294
|
const colors = slope.map((s) => Number.isFinite(s) ? s > 0 ? bull : s < 0 ? bear : flat : null);
|
|
44904
|
-
|
|
44905
|
-
|
|
44906
|
-
|
|
44907
|
-
|
|
44908
|
-
};
|
|
44909
|
-
}
|
|
44910
|
-
return { plots: [{ key: "curve", title: "Curve", values: curve, color: flat, width: 2, colors, overlay: true }] };
|
|
45295
|
+
return {
|
|
45296
|
+
plots: [{ key: "slope", title: "Slope", values: slope, color: flat, width: 2, colors }],
|
|
45297
|
+
levels: [{ key: "zero", price: 0, color: transp(NEUTRAL, 50), lineStyle: "dashed" }]
|
|
45298
|
+
};
|
|
44911
45299
|
}
|
|
44912
45300
|
};
|
|
44913
|
-
var averageSpecs = [simpleMa, exponentialMa, movingAverage, smoothedMa, zlema, vidya, maEnvelope,
|
|
45301
|
+
var averageSpecs = [simpleMa, exponentialMa, movingAverage, smoothedMa, zlema, vidya, maEnvelope, leastSquaresMa, linearRegressionSlope];
|
|
44914
45302
|
|
|
44915
45303
|
// src/core/native-indicators/classics/bands.ts
|
|
44916
45304
|
var JAW_COLOR = "#ab47bc";
|