@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/index.cjs
CHANGED
|
@@ -8617,6 +8617,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
8617
8617
|
this.barTransform = null;
|
|
8618
8618
|
// ── chart-type data engines (SDK): one lazily-created engine per style id ──
|
|
8619
8619
|
this.typeEngines = /* @__PURE__ */ new Map();
|
|
8620
|
+
/** Engines suspended by the chart — their host pushes are dropped until they resume. */
|
|
8621
|
+
this.suspendedEngines = /* @__PURE__ */ new WeakSet();
|
|
8620
8622
|
/** Last-seen chart-type settings per type id (dialog edits + persisted restores) —
|
|
8621
8623
|
* replayed into a data engine whenever one is (re)created. */
|
|
8622
8624
|
this.typeSettings = /* @__PURE__ */ new Map();
|
|
@@ -8891,12 +8893,14 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
8891
8893
|
this.mergeHistoryIntoReplay(bars);
|
|
8892
8894
|
return;
|
|
8893
8895
|
}
|
|
8894
|
-
|
|
8896
|
+
const repaint = painted;
|
|
8897
|
+
this.setBarSeries(bars, repaint ? { preserveView: true } : void 0);
|
|
8895
8898
|
if (!painted && bars.length > 0) {
|
|
8896
8899
|
painted = true;
|
|
8897
8900
|
if (opts.firstLoad) this.activateBarLayers();
|
|
8898
8901
|
if (!final) this.historyState = "backfill";
|
|
8899
8902
|
}
|
|
8903
|
+
if (repaint && !final) this.notifySessionsBars("backfill");
|
|
8900
8904
|
};
|
|
8901
8905
|
const abort = new AbortController();
|
|
8902
8906
|
this.progressiveAbort = abort;
|
|
@@ -9099,10 +9103,15 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
9099
9103
|
this.setBarSeries([], { clearing: true });
|
|
9100
9104
|
this.blankIndicatorVisuals();
|
|
9101
9105
|
if (this.activeEngineStyle) {
|
|
9102
|
-
this.typeEngines.get(this.activeEngineStyle)
|
|
9106
|
+
this.suspendTypeEngine(this.typeEngines.get(this.activeEngineStyle));
|
|
9103
9107
|
this.renderer.setNativeData?.(this.activeEngineStyle, void 0);
|
|
9104
9108
|
this.renderer.setNativeData?.(`${this.activeEngineStyle}-pending`, []);
|
|
9105
9109
|
}
|
|
9110
|
+
for (const record of this.registry.all()) {
|
|
9111
|
+
if (!record.native?.started) continue;
|
|
9112
|
+
this.stopNativeInstance(record.native);
|
|
9113
|
+
this.renderer.setNativeData?.(this.nativeChannel(record), void 0);
|
|
9114
|
+
}
|
|
9106
9115
|
}
|
|
9107
9116
|
if (depthOnly) {
|
|
9108
9117
|
this.extendDepth(gen, m.bars ?? 500);
|
|
@@ -9129,6 +9138,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
9129
9138
|
this.restartChartTypeEngine();
|
|
9130
9139
|
this.reexecuteIndicators();
|
|
9131
9140
|
}
|
|
9141
|
+
if (depthOnly) this.releaseHeldNatives();
|
|
9132
9142
|
if (!replayBack) this.startLive();
|
|
9133
9143
|
if (identityChanged) {
|
|
9134
9144
|
this.events.emit("market:changed", { symbol: m.symbol ?? "TEST", timeframe: m.timeframe ?? "60", prev: prev2 });
|
|
@@ -9145,9 +9155,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
9145
9155
|
restartNativeIndicators() {
|
|
9146
9156
|
for (const record of this.registry.all()) {
|
|
9147
9157
|
if (!record.native) continue;
|
|
9148
|
-
|
|
9158
|
+
this.stopNativeInstance(record.native);
|
|
9149
9159
|
record.native.instance = record.native.descriptor.create();
|
|
9150
|
-
record.native.started = false;
|
|
9151
9160
|
record.pendingStructural = true;
|
|
9152
9161
|
if (record.hidden) {
|
|
9153
9162
|
record.native.stale = true;
|
|
@@ -9164,6 +9173,21 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
9164
9173
|
this.typeEngines.clear();
|
|
9165
9174
|
if (this.activeEngineStyle) void this.startChartTypeEngine(this.activeEngineStyle);
|
|
9166
9175
|
}
|
|
9176
|
+
/** Suspend a chart-type engine. Its host drops every push until the engine resumes:
|
|
9177
|
+
* work it left in flight lands in its own state, never on the scene. */
|
|
9178
|
+
suspendTypeEngine(engine) {
|
|
9179
|
+
if (!engine) return;
|
|
9180
|
+
engine.suspend();
|
|
9181
|
+
this.suspendedEngines.add(engine);
|
|
9182
|
+
}
|
|
9183
|
+
/** Stop a running native instance, then revoke its context: the teardown inside `stop()`
|
|
9184
|
+
* still reaches its layer, but work it left in flight (a fetch resolving later) does not. */
|
|
9185
|
+
stopNativeInstance(native) {
|
|
9186
|
+
if (native.started) native.instance.stop();
|
|
9187
|
+
native.started = false;
|
|
9188
|
+
native.revokeContext?.();
|
|
9189
|
+
native.revokeContext = void 0;
|
|
9190
|
+
}
|
|
9167
9191
|
/**
|
|
9168
9192
|
* The FIRST candles just painted (the quick preview, or the only load): activate the
|
|
9169
9193
|
* layers that need bars but NOT full history. The volume layer reads the chart's bars
|
|
@@ -9696,7 +9720,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
9696
9720
|
const def = chartType(id);
|
|
9697
9721
|
const want = def?.dataEngine ? id : null;
|
|
9698
9722
|
if (want === this.activeEngineStyle) return;
|
|
9699
|
-
if (this.activeEngineStyle) this.typeEngines.get(this.activeEngineStyle)
|
|
9723
|
+
if (this.activeEngineStyle) this.suspendTypeEngine(this.typeEngines.get(this.activeEngineStyle));
|
|
9700
9724
|
this.activeEngineStyle = want;
|
|
9701
9725
|
if (want) void this.startChartTypeEngine(want);
|
|
9702
9726
|
}
|
|
@@ -9707,6 +9731,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
9707
9731
|
if (this.activeEngineStyle !== id) return;
|
|
9708
9732
|
const existing = this.typeEngines.get(id);
|
|
9709
9733
|
if (existing) {
|
|
9734
|
+
this.suspendedEngines.delete(existing);
|
|
9710
9735
|
existing.resume();
|
|
9711
9736
|
return;
|
|
9712
9737
|
}
|
|
@@ -9716,6 +9741,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
9716
9741
|
this.typeEngines.set(id, engine);
|
|
9717
9742
|
const stored = this.typeSettings.get(id);
|
|
9718
9743
|
if (stored) engine.onSettings?.(stored);
|
|
9744
|
+
const reachesScene = () => this.typeEngines.get(id) === engine && !this.suspendedEngines.has(engine);
|
|
9719
9745
|
engine.start({
|
|
9720
9746
|
symbol: this.config.market.symbol ?? "TEST",
|
|
9721
9747
|
timeframe: this.config.market.timeframe ?? "60",
|
|
@@ -9723,8 +9749,12 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
9723
9749
|
session: this.config.market.session,
|
|
9724
9750
|
bars: () => this.bars,
|
|
9725
9751
|
data: this.dataControl,
|
|
9726
|
-
pushData: (data) =>
|
|
9727
|
-
|
|
9752
|
+
pushData: (data) => {
|
|
9753
|
+
if (reachesScene()) this.renderer.setNativeData?.(id, data);
|
|
9754
|
+
},
|
|
9755
|
+
pushPending: (ranges) => {
|
|
9756
|
+
if (reachesScene()) this.renderer.setNativeData?.(`${id}-pending`, ranges);
|
|
9757
|
+
}
|
|
9728
9758
|
});
|
|
9729
9759
|
}
|
|
9730
9760
|
/**
|
|
@@ -9732,8 +9762,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
9732
9762
|
* a pre-load style change is picked up by init's `setBarSeries`. With bars loaded, the
|
|
9733
9763
|
* view is rebuilt in place (viewport preserved) and every Pine indicator re-executes:
|
|
9734
9764
|
* its input data changed wholesale, which a streaming session's incremental context
|
|
9735
|
-
* cannot absorb.
|
|
9736
|
-
*
|
|
9765
|
+
* cannot absorb. Natives computed from `ctx.bars()` re-run over the new candles (a
|
|
9766
|
+
* synthetic price style is their source too); layer natives that read the renderer's
|
|
9767
|
+
* bars per frame ignore the poke.
|
|
9737
9768
|
*/
|
|
9738
9769
|
syncBarTransform(style) {
|
|
9739
9770
|
const transform = barTransformFor(style);
|
|
@@ -9743,6 +9774,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
9743
9774
|
this.bars = transform ? transform.full(this.rawBars) : this.rawBars;
|
|
9744
9775
|
this.renderer.setBars(this.bars, { preserveView: true });
|
|
9745
9776
|
this.reexecuteIndicators();
|
|
9777
|
+
if (this.switchingMarket) return;
|
|
9778
|
+
for (const record of this.registry.all()) {
|
|
9779
|
+
if (record.native?.started && record.renderHandle && !record.hidden) this.pokeNative(record);
|
|
9780
|
+
}
|
|
9746
9781
|
}
|
|
9747
9782
|
/**
|
|
9748
9783
|
* Blank every mounted indicator's painted output — series data, fills, backgrounds,
|
|
@@ -9843,10 +9878,39 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
9843
9878
|
if (record.session) {
|
|
9844
9879
|
record.pendingCause = cause;
|
|
9845
9880
|
record.session.notifyBars(reason);
|
|
9846
|
-
} else if (record.native)
|
|
9881
|
+
} else if (record.native) this.pokeNative(record);
|
|
9847
9882
|
}
|
|
9848
9883
|
if (this.activeEngineStyle) this.typeEngines.get(this.activeEngineStyle)?.onBars?.();
|
|
9849
9884
|
}
|
|
9885
|
+
/**
|
|
9886
|
+
* Tell a native its bars changed. A `waitsForHistory` native ignores every change while
|
|
9887
|
+
* the history is still loading — a backfill chunk, a deeper snapshot, a tick arriving
|
|
9888
|
+
* meanwhile — and its output stays on its bars through the anchor its emit carried.
|
|
9889
|
+
* Once the history is complete, a held one starts and a running one re-runs.
|
|
9890
|
+
*/
|
|
9891
|
+
pokeNative(record) {
|
|
9892
|
+
const native = record.native;
|
|
9893
|
+
if (!native.descriptor.waitsForHistory) {
|
|
9894
|
+
native.instance.onBars();
|
|
9895
|
+
return;
|
|
9896
|
+
}
|
|
9897
|
+
if (this.historyState === "backfill") return;
|
|
9898
|
+
if (native.heldForHistory) {
|
|
9899
|
+
native.heldForHistory = false;
|
|
9900
|
+
const handle = this.handles.get(record.id);
|
|
9901
|
+
if (handle) void this.startNativeIndicator(record.id, handle);
|
|
9902
|
+
return;
|
|
9903
|
+
}
|
|
9904
|
+
if (native.started) native.instance.onBars();
|
|
9905
|
+
}
|
|
9906
|
+
/** Start the natives still held for a history that has completed without poking them
|
|
9907
|
+
* (a depth-only switch finishes its trim while the switch gates every poke). */
|
|
9908
|
+
releaseHeldNatives() {
|
|
9909
|
+
if (this.historyState === "backfill") return;
|
|
9910
|
+
for (const record of this.registry.all()) {
|
|
9911
|
+
if (record.native?.heldForHistory && !record.hidden) this.pokeNative(record);
|
|
9912
|
+
}
|
|
9913
|
+
}
|
|
9850
9914
|
/** Gap healing needs a ranged feed and a provider-backed series (offline `data` has no source). */
|
|
9851
9915
|
canHeal() {
|
|
9852
9916
|
return !!this.feed.loadRange && !this.config.market.data?.length;
|
|
@@ -10138,7 +10202,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
10138
10202
|
if (record?.native?.type === "volume") this.volumeOptedOut = true;
|
|
10139
10203
|
if (record) this.settleCode(record, { ok: false, error: new Error(`[vela] updateCode("${id}") \u2014 the indicator was removed.`) });
|
|
10140
10204
|
record?.session?.stop();
|
|
10141
|
-
if (record?.native
|
|
10205
|
+
if (record?.native) this.stopNativeInstance(record.native);
|
|
10142
10206
|
this.handles.delete(id);
|
|
10143
10207
|
if (record?.renderHandle) this.renderer.removeIndicator(record.renderHandle);
|
|
10144
10208
|
const paneId = record?.model?.paneId;
|
|
@@ -10259,7 +10323,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
10259
10323
|
for (const record of this.registry.all()) {
|
|
10260
10324
|
this.settleCode(record, { ok: false, error: new Error("[vela] updateCode \u2014 the chart was destroyed.") });
|
|
10261
10325
|
record.session?.stop();
|
|
10262
|
-
if (record.native
|
|
10326
|
+
if (record.native) this.stopNativeInstance(record.native);
|
|
10263
10327
|
}
|
|
10264
10328
|
for (const engine of this.typeEngines.values()) engine.stop();
|
|
10265
10329
|
this.typeEngines.clear();
|
|
@@ -10386,7 +10450,19 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
10386
10450
|
await this.readyPromise;
|
|
10387
10451
|
const record = this.registry.get(id);
|
|
10388
10452
|
if (!record?.native || record.hidden) return;
|
|
10453
|
+
const native = record.native;
|
|
10454
|
+
if (native.started) return;
|
|
10455
|
+
if (native.descriptor.waitsForHistory && this.historyState === "backfill") {
|
|
10456
|
+
native.heldForHistory = true;
|
|
10457
|
+
this.mountNativePlaceholder(id, record);
|
|
10458
|
+
return;
|
|
10459
|
+
}
|
|
10460
|
+
native.heldForHistory = false;
|
|
10389
10461
|
const channel = this.nativeChannel(record);
|
|
10462
|
+
let alive = true;
|
|
10463
|
+
native.revokeContext = () => {
|
|
10464
|
+
alive = false;
|
|
10465
|
+
};
|
|
10390
10466
|
const ctx = {
|
|
10391
10467
|
id,
|
|
10392
10468
|
symbol: this.config.market.symbol ?? "TEST",
|
|
@@ -10395,14 +10471,18 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
10395
10471
|
session: this.config.market.session,
|
|
10396
10472
|
bars: () => this.bars,
|
|
10397
10473
|
data: this.dataControl,
|
|
10398
|
-
emit: (out) =>
|
|
10399
|
-
|
|
10474
|
+
emit: (out) => {
|
|
10475
|
+
if (alive) this.applyModel(id, this.buildNativeModel(record, out, this.bars[0]?.time));
|
|
10476
|
+
},
|
|
10477
|
+
pushData: (data) => {
|
|
10478
|
+
if (alive) this.renderer.setNativeData?.(channel, data);
|
|
10479
|
+
},
|
|
10400
10480
|
setStatus: (status) => {
|
|
10401
|
-
if (record.renderHandle && !record.hidden) this.renderer.setIndicatorStatus?.(record.renderHandle, status);
|
|
10481
|
+
if (alive && record.renderHandle && !record.hidden) this.renderer.setIndicatorStatus?.(record.renderHandle, status);
|
|
10402
10482
|
}
|
|
10403
10483
|
};
|
|
10404
|
-
|
|
10405
|
-
|
|
10484
|
+
native.instance.start(ctx, record.inputValues);
|
|
10485
|
+
native.started = true;
|
|
10406
10486
|
} catch (err) {
|
|
10407
10487
|
this.fail(id, handle, err);
|
|
10408
10488
|
}
|
|
@@ -10415,8 +10495,14 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
10415
10495
|
const { type, descriptor } = record.native;
|
|
10416
10496
|
return descriptor.multiInstance ? nativeInstanceChannel(type, record.id) : type;
|
|
10417
10497
|
}
|
|
10418
|
-
/**
|
|
10419
|
-
|
|
10498
|
+
/**
|
|
10499
|
+
* Wrap a native indicator's emitted visuals into a full IndicatorModel, tagged `native`.
|
|
10500
|
+
* `anchorTime` is the first bar the output is index-aligned to — the chart's first bar
|
|
10501
|
+
* when it emitted. Without it the renderer pins the output's first slot to whatever the
|
|
10502
|
+
* first bar is NOW, so history landing in front of a computed output (or the oldest
|
|
10503
|
+
* bars trimmed under it) would slide the whole plot by that many bars.
|
|
10504
|
+
*/
|
|
10505
|
+
buildNativeModel(record, out, anchorTime) {
|
|
10420
10506
|
const d = record.native.descriptor;
|
|
10421
10507
|
const type = record.native.type;
|
|
10422
10508
|
const channel = this.nativeChannel(record);
|
|
@@ -10427,6 +10513,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
10427
10513
|
overlay: d.overlay,
|
|
10428
10514
|
paneHint: d.paneHint,
|
|
10429
10515
|
native: { type, ...channel !== type ? { channel } : {} },
|
|
10516
|
+
...anchorTime != null ? { anchorTime } : {},
|
|
10430
10517
|
...d.legend === false ? { legend: false } : {},
|
|
10431
10518
|
...out.paneAxis != null ? { paneAxis: out.paneAxis } : {},
|
|
10432
10519
|
series: out.series ?? [],
|
|
@@ -10481,8 +10568,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
10481
10568
|
const paneId = this.routePane(id, model, record.options ?? {});
|
|
10482
10569
|
this.placeModel(model, id, paneId);
|
|
10483
10570
|
record.model = model;
|
|
10484
|
-
this.ensurePaneFor(paneId);
|
|
10571
|
+
const paneCreated = this.ensurePaneFor(paneId);
|
|
10485
10572
|
record.renderHandle = this.renderer.mountIndicator(model);
|
|
10573
|
+
if (paneCreated) this.events.emit("pane:changed", void 0);
|
|
10486
10574
|
record.pendingStructural = true;
|
|
10487
10575
|
if (record.hidden) {
|
|
10488
10576
|
this.renderer.setIndicatorVisible?.(record.renderHandle, false);
|
|
@@ -10506,24 +10594,49 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
10506
10594
|
const paneId = this.routePane(id, model, record.options ?? {});
|
|
10507
10595
|
this.placeModel(model, id, paneId);
|
|
10508
10596
|
record.model = model;
|
|
10509
|
-
this.ensurePaneFor(paneId);
|
|
10597
|
+
const paneCreated = this.ensurePaneFor(paneId);
|
|
10510
10598
|
record.renderHandle = this.renderer.mountIndicator(model);
|
|
10599
|
+
if (paneCreated) this.events.emit("pane:changed", void 0);
|
|
10511
10600
|
record.pendingStructural = true;
|
|
10512
10601
|
this.renderer.setIndicatorVisible?.(record.renderHandle, false);
|
|
10513
10602
|
this.announce(record, this.handles.get(id));
|
|
10514
10603
|
}
|
|
10604
|
+
/**
|
|
10605
|
+
* The legend row of a native HELD for the history (see `waitsForHistory`): an empty
|
|
10606
|
+
* model with the title and inputs schema, spinning, not announced — exactly a script
|
|
10607
|
+
* indicator's loading placeholder, so `ready`/`indicator:added` and `inspect()` still
|
|
10608
|
+
* mean "it produced output". A record that already has a row (restarted over a new
|
|
10609
|
+
* market, or shown again) keeps it and only spins. The first emit remounts over it.
|
|
10610
|
+
*/
|
|
10611
|
+
mountNativePlaceholder(id, record) {
|
|
10612
|
+
record.pendingStructural = true;
|
|
10613
|
+
if (!record.renderHandle) {
|
|
10614
|
+
const model = this.buildNativeModel(record, {});
|
|
10615
|
+
const paneId = this.routePane(id, model, record.options ?? {});
|
|
10616
|
+
this.placeModel(model, id, paneId);
|
|
10617
|
+
record.model = model;
|
|
10618
|
+
const paneCreated = this.ensurePaneFor(paneId);
|
|
10619
|
+
record.renderHandle = this.renderer.mountIndicator(model);
|
|
10620
|
+
if (paneCreated) this.events.emit("pane:changed", void 0);
|
|
10621
|
+
}
|
|
10622
|
+
this.setLoading(record, true);
|
|
10623
|
+
}
|
|
10515
10624
|
/** Flip the record's loading state and reflect it in the legend row (spinner on/off). */
|
|
10516
10625
|
setLoading(record, loading) {
|
|
10517
10626
|
record.loading = loading;
|
|
10518
10627
|
if (record.renderHandle && !record.hidden) this.renderer.setIndicatorStatus?.(record.renderHandle, loading ? "loading" : "idle");
|
|
10519
10628
|
}
|
|
10629
|
+
/** Make sure the renderer has the pane; true when it is new to the layout (the caller
|
|
10630
|
+
* emits `pane:changed` once the indicator's model sits on it, so `list()` shows it). */
|
|
10520
10631
|
ensurePaneFor(paneId) {
|
|
10521
|
-
|
|
10632
|
+
const created = !this.paneOrder.includes(paneId);
|
|
10633
|
+
if (created) this.paneOrder.push(paneId);
|
|
10522
10634
|
this.renderer.ensurePane({
|
|
10523
10635
|
id: paneId,
|
|
10524
10636
|
kind: paneId === "price" ? "price" : "study",
|
|
10525
10637
|
order: this.paneOrder.indexOf(paneId)
|
|
10526
10638
|
});
|
|
10639
|
+
return created;
|
|
10527
10640
|
}
|
|
10528
10641
|
/** True when any (still-registered) indicator sits on the given pane. */
|
|
10529
10642
|
paneStillUsed(paneId) {
|
|
@@ -10559,12 +10672,14 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
10559
10672
|
const panes = [];
|
|
10560
10673
|
for (const paneId of this.paneOrder) {
|
|
10561
10674
|
if (paneId !== "price" && !byPane.has(paneId)) continue;
|
|
10675
|
+
const heightWeight = this.renderer.paneHeightWeight?.(paneId) ?? null;
|
|
10562
10676
|
panes.push({
|
|
10563
10677
|
id: paneId,
|
|
10564
10678
|
kind: paneId === "price" ? "price" : "study",
|
|
10565
10679
|
order: panes.length,
|
|
10566
10680
|
collapsed: this.collapsedPanes.has(paneId),
|
|
10567
10681
|
maximized: this.maximizedPaneId === paneId,
|
|
10682
|
+
...heightWeight !== null ? { heightWeight } : {},
|
|
10568
10683
|
indicators: byPane.get(paneId) ?? []
|
|
10569
10684
|
});
|
|
10570
10685
|
}
|
|
@@ -10596,6 +10711,22 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
10596
10711
|
this.renderer.setPaneMaximized?.(paneId);
|
|
10597
10712
|
this.events.emit("pane:changed", void 0);
|
|
10598
10713
|
}
|
|
10714
|
+
setPaneHeightWeight(paneId, weight) {
|
|
10715
|
+
if (!this.renderer.setPaneHeightWeight) {
|
|
10716
|
+
console.warn("[vela] the active renderer does not support pane sizing \u2014 chart.panes.setHeightWeight ignored.");
|
|
10717
|
+
return;
|
|
10718
|
+
}
|
|
10719
|
+
if (typeof weight !== "number" || !Number.isFinite(weight) || weight <= 0) {
|
|
10720
|
+
console.warn(`[vela] chart.panes.setHeightWeight: weight must be a positive number (got ${String(weight)}).`);
|
|
10721
|
+
return;
|
|
10722
|
+
}
|
|
10723
|
+
if (!this.existingPaneIds().includes(paneId)) {
|
|
10724
|
+
console.warn(`[vela] chart.panes.setHeightWeight: no pane "${paneId}".`);
|
|
10725
|
+
return;
|
|
10726
|
+
}
|
|
10727
|
+
this.renderer.setPaneHeightWeight(paneId, weight);
|
|
10728
|
+
this.events.emit("pane:changed", void 0);
|
|
10729
|
+
}
|
|
10599
10730
|
/** IndicatorController/PaneController: move (or merge) an indicator to another pane. */
|
|
10600
10731
|
moveIndicator(id, target) {
|
|
10601
10732
|
if (!this.paneManagementSupported() || !this.renderer.setIndicatorPane) {
|
|
@@ -10693,6 +10824,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
10693
10824
|
this.maximizedPaneId = a.maximized ? a.paneId : null;
|
|
10694
10825
|
this.events.emit("pane:changed", void 0);
|
|
10695
10826
|
break;
|
|
10827
|
+
case "resize":
|
|
10828
|
+
this.events.emit("pane:changed", void 0);
|
|
10829
|
+
break;
|
|
10696
10830
|
}
|
|
10697
10831
|
}
|
|
10698
10832
|
/** Fire `indicator:added` + the handle's `ready` once, on the first computed model. */
|
|
@@ -10723,14 +10857,16 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
10723
10857
|
const paneId2 = this.routePane(id, model, record.options ?? {});
|
|
10724
10858
|
this.placeModel(model, id, paneId2);
|
|
10725
10859
|
record.model = model;
|
|
10726
|
-
this.ensurePaneFor(paneId2);
|
|
10860
|
+
const paneCreated = this.ensurePaneFor(paneId2);
|
|
10727
10861
|
record.renderHandle = this.renderer.mountIndicator(model);
|
|
10728
10862
|
record.pendingStructural = false;
|
|
10863
|
+
if (paneCreated) this.events.emit("pane:changed", void 0);
|
|
10729
10864
|
this.announce(record, handle);
|
|
10730
10865
|
return true;
|
|
10731
10866
|
}
|
|
10732
10867
|
let paneId = record.model?.paneId ?? "price";
|
|
10733
10868
|
const prevOwnScale = record.model?.ownScale === true;
|
|
10869
|
+
let rerouted = false;
|
|
10734
10870
|
if (record.loading && !record.paneLocked) {
|
|
10735
10871
|
const routed = this.routePane(id, model, record.options ?? {});
|
|
10736
10872
|
if (routed !== paneId) {
|
|
@@ -10741,6 +10877,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
10741
10877
|
this.ensurePaneFor(routed);
|
|
10742
10878
|
record.pendingStructural = true;
|
|
10743
10879
|
paneId = routed;
|
|
10880
|
+
rerouted = true;
|
|
10744
10881
|
}
|
|
10745
10882
|
}
|
|
10746
10883
|
this.placeModel(model, id, paneId);
|
|
@@ -10753,6 +10890,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
10753
10890
|
} else {
|
|
10754
10891
|
this.renderer.updateIndicator(record.renderHandle, modelToValuePatch(model));
|
|
10755
10892
|
}
|
|
10893
|
+
if (rerouted) this.events.emit("pane:changed", void 0);
|
|
10756
10894
|
if (record.loading) this.setLoading(record, false);
|
|
10757
10895
|
this.announce(record, handle);
|
|
10758
10896
|
return true;
|
|
@@ -10767,13 +10905,16 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
10767
10905
|
const overlay = options.overlay ?? model.overlay;
|
|
10768
10906
|
return overlay ? "price" : `pane-${id}`;
|
|
10769
10907
|
}
|
|
10770
|
-
/** Each named value series' last
|
|
10771
|
-
* engine fills, so this works without an execution context.
|
|
10772
|
-
|
|
10908
|
+
/** Each named value series' value on the chart's last bar — the model is the one source
|
|
10909
|
+
* every engine fills, so this works without an execution context. Points past the last
|
|
10910
|
+
* bar (a series shifted forward) are skipped by time. */
|
|
10911
|
+
static plotsAtLastBar(model, lastBarTime) {
|
|
10773
10912
|
const out = {};
|
|
10774
10913
|
for (const spec of model.series) {
|
|
10775
10914
|
if (!isLineLikeSeries(spec)) continue;
|
|
10776
|
-
|
|
10915
|
+
let i = spec.points.length - 1;
|
|
10916
|
+
if (lastBarTime !== void 0) while (i >= 0 && spec.points[i].time > lastBarTime) i -= 1;
|
|
10917
|
+
out[spec.title] = spec.points[i]?.value ?? null;
|
|
10777
10918
|
}
|
|
10778
10919
|
return out;
|
|
10779
10920
|
}
|
|
@@ -10829,7 +10970,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
10829
10970
|
// whatever produced this run.
|
|
10830
10971
|
forming: this.config.live && this.bars.length > 0,
|
|
10831
10972
|
complete: this.historyState !== "backfill",
|
|
10832
|
-
plots: _EngineOrchestrator.plotsAtLastBar(model),
|
|
10973
|
+
plots: _EngineOrchestrator.plotsAtLastBar(model, lastBar?.time),
|
|
10833
10974
|
vars: snapshot?.variables ?? {},
|
|
10834
10975
|
...snapshot?.strategy ? { strategy: snapshot.strategy } : {},
|
|
10835
10976
|
warnings: snapshot?.warnings ?? [],
|
|
@@ -10898,8 +11039,9 @@ function sameInputValue(a, b) {
|
|
|
10898
11039
|
return typeof a === "object" && a !== null && typeof b === "object" && b !== null && JSON.stringify(a) === JSON.stringify(b);
|
|
10899
11040
|
}
|
|
10900
11041
|
function blankedModel(model) {
|
|
11042
|
+
const { anchorTime: _anchor, ...rest } = model;
|
|
10901
11043
|
return {
|
|
10902
|
-
...
|
|
11044
|
+
...rest,
|
|
10903
11045
|
series: model.series.map((s) => s.kind === "candle" || s.kind === "bar" ? { ...s, bars: [] } : s.kind === "markers" ? { ...s, markers: [] } : { ...s, points: [] }),
|
|
10904
11046
|
fills: [],
|
|
10905
11047
|
backgrounds: [],
|
|
@@ -10922,12 +11064,14 @@ function modelToValuePatch(model) {
|
|
|
10922
11064
|
if (s.kind === "candle" || s.kind === "bar") {
|
|
10923
11065
|
series.push({ seriesId: s.id, kind: "bars", bars: s.bars, barColors: s.barColors ?? [] });
|
|
10924
11066
|
for (const b of s.bars) {
|
|
11067
|
+
if (!b) continue;
|
|
10925
11068
|
if (b.time < from) from = b.time;
|
|
10926
11069
|
if (b.time > to) to = b.time;
|
|
10927
11070
|
}
|
|
10928
11071
|
} else if (isLineLikeSeries(s)) {
|
|
10929
11072
|
series.push({ seriesId: s.id, kind: "points", points: s.points });
|
|
10930
11073
|
for (const p of s.points) {
|
|
11074
|
+
if (!p) continue;
|
|
10931
11075
|
if (p.time < from) from = p.time;
|
|
10932
11076
|
if (p.time > to) to = p.time;
|
|
10933
11077
|
}
|
|
@@ -11242,6 +11386,17 @@ var PanesControl = class {
|
|
|
11242
11386
|
if (this.guard()) this.controller.maximizePane(paneId);
|
|
11243
11387
|
return this;
|
|
11244
11388
|
}
|
|
11389
|
+
/**
|
|
11390
|
+
* Set a pane's height weight: the panes share the plot height in proportion to their
|
|
11391
|
+
* weights, so `setHeightWeight('price', 6)` next to two study panes at 1 gives the price
|
|
11392
|
+
* pane three quarters of the height. The weight sticks until the user drags a separator
|
|
11393
|
+
* or it is set again; a collapsed pane applies it when expanded. `weight` must be a
|
|
11394
|
+
* positive number.
|
|
11395
|
+
*/
|
|
11396
|
+
setHeightWeight(paneId, weight) {
|
|
11397
|
+
if (this.guard()) this.controller.setPaneHeightWeight(paneId, weight);
|
|
11398
|
+
return this;
|
|
11399
|
+
}
|
|
11245
11400
|
/** Move/merge an indicator to a pane — sugar for `handle.moveTo(...)` by id. */
|
|
11246
11401
|
moveIndicator(id, target) {
|
|
11247
11402
|
if (this.guard()) this.controller.moveIndicator(id, target);
|
|
@@ -16566,6 +16721,8 @@ var LEGEND_TITLE_STATUS_GAP_PX = 8;
|
|
|
16566
16721
|
var LEGEND_ACTION_GAP_PX = 6;
|
|
16567
16722
|
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;`;
|
|
16568
16723
|
var LEGEND_ROW_MIN_H = LEGEND_CTL_PX + LEGEND_ROW_PAD_Y * 2;
|
|
16724
|
+
var STACK_RAISE = "stack:raise";
|
|
16725
|
+
var STACK_LOWER = "stack:lower";
|
|
16569
16726
|
var EYE_SVG = iconAt("eye", LEGEND_ICON_PX2);
|
|
16570
16727
|
var EYE_OFF_SVG = iconAt("eye-off", LEGEND_ICON_PX2);
|
|
16571
16728
|
var GEAR_SVG = iconAt("gear", LEGEND_ICON_PX2);
|
|
@@ -16595,6 +16752,10 @@ var InputsUI = class {
|
|
|
16595
16752
|
this.mobileLayout = false;
|
|
16596
16753
|
/** Pane move/merge hook — when set, rows get a "Move to" menu + become drag-to-pane sources. */
|
|
16597
16754
|
this.moveApi = null;
|
|
16755
|
+
/** Overlay restacking hook — when set, a row's menus offer to step it forward or back. */
|
|
16756
|
+
this.stackApi = null;
|
|
16757
|
+
/** What {@link orderRows} last ordered the rows by (skips unchanged frames). */
|
|
16758
|
+
this.rowOrderKey = "";
|
|
16598
16759
|
/** Host-contributed legend actions, resolved PER ROW at render time (see setLegendActions). */
|
|
16599
16760
|
this.legendActions = null;
|
|
16600
16761
|
/** Host-contributed callout bubbles, resolved PER ROW at render time (see setLegendCallouts). */
|
|
@@ -16733,6 +16894,10 @@ var InputsUI = class {
|
|
|
16733
16894
|
setMoveApi(api) {
|
|
16734
16895
|
this.moveApi = api;
|
|
16735
16896
|
}
|
|
16897
|
+
/** Enable the context menu's "Bring in front of …" entry for overlay indicators. Null disables it. */
|
|
16898
|
+
setStackApi(api) {
|
|
16899
|
+
this.stackApi = api;
|
|
16900
|
+
}
|
|
16736
16901
|
/**
|
|
16737
16902
|
* Wire the host-contributed row actions. Re-calling replaces the provider and
|
|
16738
16903
|
* re-projects the rows already on screen (a late `registerLegendAction` appears after
|
|
@@ -16878,16 +17043,20 @@ var InputsUI = class {
|
|
|
16878
17043
|
}
|
|
16879
17044
|
this.moveTargets = items.map((it) => it.target);
|
|
16880
17045
|
this.moveMenuId = id;
|
|
16881
|
-
const
|
|
17046
|
+
const stack = this.stackItems(id);
|
|
17047
|
+
const moves = items.map((it, i) => ({ id: String(i), label: it.label }));
|
|
17048
|
+
if (moves[0] && stack.length > 0) moves[0].separatorBefore = true;
|
|
17049
|
+
const descriptors = [...stack, ...moves];
|
|
16882
17050
|
if (!this.moveMenu) {
|
|
16883
17051
|
this.moveMenu = new Menu({
|
|
16884
17052
|
host: this.container,
|
|
16885
17053
|
items: descriptors,
|
|
16886
17054
|
placement: "bottom-start",
|
|
16887
17055
|
onSelect: (itemId) => {
|
|
16888
|
-
const target = this.moveTargets[Number(itemId)];
|
|
16889
17056
|
const rowId = this.moveMenuId;
|
|
16890
|
-
if (
|
|
17057
|
+
if (!rowId || this.runStackItem(itemId, rowId)) return;
|
|
17058
|
+
const target = this.moveTargets[Number(itemId)];
|
|
17059
|
+
if (target) api.move(rowId, target);
|
|
16891
17060
|
}
|
|
16892
17061
|
});
|
|
16893
17062
|
} else {
|
|
@@ -16900,18 +17069,24 @@ var InputsUI = class {
|
|
|
16900
17069
|
this.moveMenu?.close();
|
|
16901
17070
|
}
|
|
16902
17071
|
// ── legend row context menu (right-click) ───────────────────────────────
|
|
16903
|
-
/** Open the row's action menu at the pointer
|
|
17072
|
+
/** Open the row's action menu at the pointer: an overlay indicator's restacking steps
|
|
17073
|
+
* first (where a layer is there to pass), then the values toggle. */
|
|
16904
17074
|
openRowMenu(id, x, y) {
|
|
16905
17075
|
this.closeRowMenu();
|
|
16906
17076
|
this.closeMoveMenu();
|
|
16907
17077
|
const row2 = this.rows.get(id);
|
|
16908
17078
|
if (!row2) return;
|
|
16909
17079
|
this.rowMenuId = id;
|
|
16910
|
-
const
|
|
16911
|
-
|
|
16912
|
-
|
|
16913
|
-
|
|
16914
|
-
|
|
17080
|
+
const stack = this.stackItems(id);
|
|
17081
|
+
const items = [
|
|
17082
|
+
...stack,
|
|
17083
|
+
{
|
|
17084
|
+
id: "values",
|
|
17085
|
+
label: "Indicator values",
|
|
17086
|
+
checked: this.rowValuesShown(row2),
|
|
17087
|
+
separatorBefore: stack.length > 0
|
|
17088
|
+
}
|
|
17089
|
+
];
|
|
16915
17090
|
if (!this.rowMenu) {
|
|
16916
17091
|
this.rowMenu = new Menu({
|
|
16917
17092
|
host: this.container,
|
|
@@ -16920,8 +17095,10 @@ var InputsUI = class {
|
|
|
16920
17095
|
// Right-click action menu — checked state reads as a leading ✓ (see the kit Menu).
|
|
16921
17096
|
checkmarks: true,
|
|
16922
17097
|
onSelect: (itemId) => {
|
|
16923
|
-
|
|
16924
|
-
|
|
17098
|
+
const rowId = this.rowMenuId;
|
|
17099
|
+
if (!rowId || this.runStackItem(itemId, rowId)) return;
|
|
17100
|
+
if (itemId !== "values") return;
|
|
17101
|
+
const target = this.rows.get(rowId);
|
|
16925
17102
|
if (!target) return;
|
|
16926
17103
|
target.showValues = !this.rowValuesShown(target);
|
|
16927
17104
|
this.renderRowValues(target);
|
|
@@ -16935,6 +17112,55 @@ var InputsUI = class {
|
|
|
16935
17112
|
closeRowMenu() {
|
|
16936
17113
|
this.rowMenu?.close();
|
|
16937
17114
|
}
|
|
17115
|
+
/** A row's one-step restacking entries (forward, then back), for whichever neighbor exists. */
|
|
17116
|
+
stackItems(id) {
|
|
17117
|
+
const api = this.stackApi;
|
|
17118
|
+
if (!api) return [];
|
|
17119
|
+
const items = [];
|
|
17120
|
+
const forward = this.stackLabel("Bring in front of", api.above(id), "the indicator in front");
|
|
17121
|
+
if (forward) items.push({ id: STACK_RAISE, label: forward });
|
|
17122
|
+
const back = this.stackLabel("Send behind", api.below(id), "the indicator behind");
|
|
17123
|
+
if (back) items.push({ id: STACK_LOWER, label: back });
|
|
17124
|
+
return items;
|
|
17125
|
+
}
|
|
17126
|
+
stackLabel(verb, neighbor, fallback) {
|
|
17127
|
+
if (neighbor === null) return null;
|
|
17128
|
+
if (neighbor === "price") return `${verb} price`;
|
|
17129
|
+
return `${verb} ${this.rows.get(neighbor.indicator)?.title ?? fallback}`;
|
|
17130
|
+
}
|
|
17131
|
+
/** Run a restacking entry; false when `itemId` is not one. */
|
|
17132
|
+
runStackItem(itemId, rowId) {
|
|
17133
|
+
if (itemId === STACK_RAISE) this.stackApi?.raise(rowId);
|
|
17134
|
+
else if (itemId === STACK_LOWER) this.stackApi?.lower(rowId);
|
|
17135
|
+
else return false;
|
|
17136
|
+
return true;
|
|
17137
|
+
}
|
|
17138
|
+
/**
|
|
17139
|
+
* List each pane's rows front-most first, the way the indicators stack (the object
|
|
17140
|
+
* tree's order). `backToFront` is the paint order; rows it doesn't name keep their
|
|
17141
|
+
* place after the others. Cheap to call every frame: no-op while nothing changed.
|
|
17142
|
+
*/
|
|
17143
|
+
orderRows(backToFront) {
|
|
17144
|
+
const key = `${backToFront.join(",")}|${[...this.rows.values()].map((r) => `${r.id}@${r.paneId}`).join(",")}`;
|
|
17145
|
+
if (key === this.rowOrderKey) return;
|
|
17146
|
+
this.rowOrderKey = key;
|
|
17147
|
+
const rank = new Map(backToFront.map((id, i) => [id, i]));
|
|
17148
|
+
for (const lg of this.legends.values()) {
|
|
17149
|
+
const els = [...this.rows.values()].filter((r) => r.el.parentElement === lg).map((r) => r.el);
|
|
17150
|
+
const dom = [...lg.children];
|
|
17151
|
+
const ofEl = new Map([...this.rows.values()].map((r) => [r.el, r.id]));
|
|
17152
|
+
const at = (el) => rank.get(ofEl.get(el)) ?? -1;
|
|
17153
|
+
els.sort((a, b) => {
|
|
17154
|
+
const ra = at(a);
|
|
17155
|
+
const rb = at(b);
|
|
17156
|
+
if (ra >= 0 && rb >= 0) return rb - ra;
|
|
17157
|
+
if (ra >= 0 || rb >= 0) return ra >= 0 ? -1 : 1;
|
|
17158
|
+
return dom.indexOf(a) - dom.indexOf(b);
|
|
17159
|
+
});
|
|
17160
|
+
const anchor = this.foldToggle?.parentElement === lg ? this.foldToggle : null;
|
|
17161
|
+
for (const el of els) lg.insertBefore(el, anchor);
|
|
17162
|
+
}
|
|
17163
|
+
}
|
|
16938
17164
|
/** Move a legend row to another pane (indicator merged/moved), tidying an emptied container. */
|
|
16939
17165
|
setPane(id, paneId) {
|
|
16940
17166
|
const row2 = this.rows.get(id);
|
|
@@ -18625,6 +18851,7 @@ var WebGL2Backend = class {
|
|
|
18625
18851
|
const i0 = Math.max(0, Math.floor(vr.from));
|
|
18626
18852
|
const i1 = Math.min(n - 1, Math.ceil(vr.to));
|
|
18627
18853
|
if (i1 < i0) return;
|
|
18854
|
+
const s1 = Math.max(i1, Math.ceil(vr.to));
|
|
18628
18855
|
gl.enable(gl.BLEND);
|
|
18629
18856
|
gl.enable(gl.SCISSOR_TEST);
|
|
18630
18857
|
const barColorMap = mergeBarColors(scene.indicators);
|
|
@@ -18683,8 +18910,8 @@ var WebGL2Backend = class {
|
|
|
18683
18910
|
b.alpha = this.modelAlpha;
|
|
18684
18911
|
const off = scene.offsetOf(m.id);
|
|
18685
18912
|
const mp = effPane(m);
|
|
18686
|
-
for (const f of m.fills) if (f.overlay !== true) this.emitFill(b, m, f, mp, coords, i0,
|
|
18687
|
-
for (const s of m.series) if (s.overlay !== true) this.emitSeries(b, s, mp, coords, i0,
|
|
18913
|
+
for (const f of m.fills) if (f.overlay !== true) this.emitFill(b, m, f, mp, coords, i0, s1, off);
|
|
18914
|
+
for (const s of m.series) if (s.overlay !== true) this.emitSeries(b, s, mp, coords, i0, s1, theme, off);
|
|
18688
18915
|
}
|
|
18689
18916
|
if (drawCandles && !candleDrawn) {
|
|
18690
18917
|
drawSlicesUpTo(scene.candleZ);
|
|
@@ -18695,11 +18922,11 @@ var WebGL2Backend = class {
|
|
|
18695
18922
|
if (isPrice) {
|
|
18696
18923
|
for (const m of scene.indicators.values()) {
|
|
18697
18924
|
const off = scene.offsetOf(m.id);
|
|
18698
|
-
for (const f of m.fills) if (f.overlay === true) this.emitFill(b, m, f, pane, coords, i0,
|
|
18925
|
+
for (const f of m.fills) if (f.overlay === true) this.emitFill(b, m, f, pane, coords, i0, s1, off);
|
|
18699
18926
|
}
|
|
18700
18927
|
for (const m of scene.indicators.values()) {
|
|
18701
18928
|
const off = scene.offsetOf(m.id);
|
|
18702
|
-
for (const s of m.series) if (s.overlay === true) this.emitSeries(b, s, pane, coords, i0,
|
|
18929
|
+
for (const s of m.series) if (s.overlay === true) this.emitSeries(b, s, pane, coords, i0, s1, theme, off);
|
|
18703
18930
|
}
|
|
18704
18931
|
}
|
|
18705
18932
|
drawSlicesUpTo(Infinity);
|
|
@@ -18721,7 +18948,7 @@ var WebGL2Backend = class {
|
|
|
18721
18948
|
if (this.glow > 0 && !this.screenProgram && !this.ensureScreenPipeline()) this.glow = 0;
|
|
18722
18949
|
if (this.glow > 0 && this.screenProgram) {
|
|
18723
18950
|
this.glowBatch.alpha = this.modelAlpha;
|
|
18724
|
-
this.renderGlow(gl, scene, coords, i0,
|
|
18951
|
+
this.renderGlow(gl, scene, coords, i0, s1);
|
|
18725
18952
|
}
|
|
18726
18953
|
}
|
|
18727
18954
|
/**
|
|
@@ -19278,7 +19505,7 @@ var WebGL2Backend = class {
|
|
|
19278
19505
|
const tickW = Math.max(1, Math.round(spacing * 0.35));
|
|
19279
19506
|
for (let i = i0; i <= i1; i += 1) {
|
|
19280
19507
|
const bar = s.bars[i - off];
|
|
19281
|
-
if (!bar) continue;
|
|
19508
|
+
if (!bar || !Number.isFinite(bar.open + bar.high + bar.low + bar.close)) continue;
|
|
19282
19509
|
const bc = s.barColors?.[i - off];
|
|
19283
19510
|
const body = parseColor(bc?.color ?? (bar.close >= bar.open ? up : down));
|
|
19284
19511
|
const x = Math.round(coords.logicalToX(i));
|
|
@@ -19931,6 +20158,8 @@ var InputController = class {
|
|
|
19931
20158
|
const tapRelease = this.dragging && !this.moved && (!wasTouch || Math.hypot(x - this.startX, y - this.startY) <= TOUCH_TAP_SLOP);
|
|
19932
20159
|
if (this.dragging && this.region === "drawing") {
|
|
19933
20160
|
this.deps.drawingsPointerUp?.(x, y, this.snapMode(e));
|
|
20161
|
+
} else if (this.dragging && this.region === "separator") {
|
|
20162
|
+
this.deps.endPaneResize?.();
|
|
19934
20163
|
} else if (tapRelease && this.region === "data") {
|
|
19935
20164
|
this.deps.onClick(x, y);
|
|
19936
20165
|
} else if (this.dragging && this.region === "data" && wasTouch) {
|
|
@@ -19952,6 +20181,7 @@ var InputController = class {
|
|
|
19952
20181
|
this.cancelLongPress();
|
|
19953
20182
|
if (!this.dragging) return;
|
|
19954
20183
|
if (this.region === "drawing" && !Number.isNaN(this.cursorX)) this.deps.drawingsPointerUp?.(this.cursorX, this.cursorY, this.snapMode(e));
|
|
20184
|
+
if (this.region === "separator") this.deps.endPaneResize?.();
|
|
19955
20185
|
if (this.region === "crosshair" || e.pointerType === "touch") this.deps.onPointerMove(null, null);
|
|
19956
20186
|
this.endGesture(e);
|
|
19957
20187
|
};
|
|
@@ -20628,9 +20858,12 @@ var SceneGraph = class {
|
|
|
20628
20858
|
setIndicatorZ(id, z) {
|
|
20629
20859
|
this.seriesZ.set(id, z);
|
|
20630
20860
|
}
|
|
20631
|
-
/** Snapshot of the current ordering for a UI/read API: `{ id, z }`
|
|
20861
|
+
/** Snapshot of the current ordering for a UI/read API: `{ id, z }` back to front, in the
|
|
20862
|
+
* order the indicators paint — on a shared key, mount order (see `orderedIndicatorsForPane`). */
|
|
20632
20863
|
indicatorZOrder() {
|
|
20633
|
-
|
|
20864
|
+
const mounted = new Map([...this.indicators.keys()].map((id, i) => [id, i]));
|
|
20865
|
+
const at = (id) => mounted.get(id) ?? Number.MAX_SAFE_INTEGER;
|
|
20866
|
+
return [...this.seriesZ.entries()].map(([id, z]) => ({ id, z })).sort((a, b) => a.z - b.z || at(a.id) - at(b.id));
|
|
20634
20867
|
}
|
|
20635
20868
|
/** The pane's series z keys (each indicator, plus the candles on the price pane), sorted
|
|
20636
20869
|
* ascending and de-duplicated — the boundaries a user drawing's z is slotted against. */
|
|
@@ -20658,16 +20891,19 @@ var SceneGraph = class {
|
|
|
20658
20891
|
for (const z of this.seriesZ.values()) if (z < min) min = z;
|
|
20659
20892
|
return min;
|
|
20660
20893
|
}
|
|
20894
|
+
/** Create the pane, or update an existing one in place. Without `heightWeight` an existing
|
|
20895
|
+
* pane keeps its weight (a user resize survives other indicators mounting onto it) and a
|
|
20896
|
+
* new one gets the default: 3 for the price pane, 1 for a study pane. */
|
|
20661
20897
|
ensurePane(id, kind, order, heightWeight) {
|
|
20662
20898
|
this.orderedCache = null;
|
|
20663
20899
|
const existing = this.panes.get(id);
|
|
20664
20900
|
if (existing) {
|
|
20665
20901
|
existing.order = order;
|
|
20666
|
-
existing.heightWeight = heightWeight;
|
|
20902
|
+
if (heightWeight !== void 0) existing.heightWeight = heightWeight;
|
|
20667
20903
|
existing.kind = kind;
|
|
20668
20904
|
return existing;
|
|
20669
20905
|
}
|
|
20670
|
-
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 };
|
|
20906
|
+
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 };
|
|
20671
20907
|
this.panes.set(id, pane);
|
|
20672
20908
|
return pane;
|
|
20673
20909
|
}
|
|
@@ -20724,6 +20960,7 @@ var Canvas2dBackend = class {
|
|
|
20724
20960
|
const i0 = Math.max(0, Math.floor(vr.from));
|
|
20725
20961
|
const i1 = Math.min(n - 1, Math.ceil(vr.to));
|
|
20726
20962
|
if (i1 < i0) return;
|
|
20963
|
+
const s1 = Math.max(i1, Math.ceil(vr.to));
|
|
20727
20964
|
const barColorMap = mergeBarColors2(scene.indicators);
|
|
20728
20965
|
const panes = scene.orderedPanes();
|
|
20729
20966
|
for (const pane of panes) {
|
|
@@ -20766,8 +21003,8 @@ var Canvas2dBackend = class {
|
|
|
20766
21003
|
ctx.globalAlpha = this.modelAlpha;
|
|
20767
21004
|
const off = scene.offsetOf(m.id);
|
|
20768
21005
|
const mp = effPane(m);
|
|
20769
|
-
for (const f of m.fills) if (f.overlay !== true) this.drawFill(ctx, m, f, mp, coords, i0,
|
|
20770
|
-
for (const s of m.series) if (s.overlay !== true) this.drawSeries(ctx, s, mp, coords, i0,
|
|
21006
|
+
for (const f of m.fills) if (f.overlay !== true) this.drawFill(ctx, m, f, mp, coords, i0, s1, off);
|
|
21007
|
+
for (const s of m.series) if (s.overlay !== true) this.drawSeries(ctx, s, mp, coords, i0, s1, theme, off);
|
|
20771
21008
|
}
|
|
20772
21009
|
if (drawCandles && !candleDrawn) {
|
|
20773
21010
|
drawSlicesUpTo(scene.candleZ);
|
|
@@ -20778,11 +21015,11 @@ var Canvas2dBackend = class {
|
|
|
20778
21015
|
ctx.globalAlpha = this.modelAlpha;
|
|
20779
21016
|
for (const m of scene.indicators.values()) {
|
|
20780
21017
|
const off = scene.offsetOf(m.id);
|
|
20781
|
-
for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0,
|
|
21018
|
+
for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0, s1, off);
|
|
20782
21019
|
}
|
|
20783
21020
|
for (const m of scene.indicators.values()) {
|
|
20784
21021
|
const off = scene.offsetOf(m.id);
|
|
20785
|
-
for (const s of m.series) if (s.overlay === true) this.drawSeries(ctx, s, pane, coords, i0,
|
|
21022
|
+
for (const s of m.series) if (s.overlay === true) this.drawSeries(ctx, s, pane, coords, i0, s1, theme, off);
|
|
20786
21023
|
}
|
|
20787
21024
|
}
|
|
20788
21025
|
drawSlicesUpTo(Infinity);
|
|
@@ -21234,7 +21471,7 @@ var Canvas2dBackend = class {
|
|
|
21234
21471
|
const tickW = Math.max(1, Math.round(spacing * 0.35));
|
|
21235
21472
|
for (let i = i0; i <= i1; i += 1) {
|
|
21236
21473
|
const b = s.bars[i - off];
|
|
21237
|
-
if (!b) continue;
|
|
21474
|
+
if (!b || !Number.isFinite(b.open + b.high + b.low + b.close)) continue;
|
|
21238
21475
|
const bc = s.barColors?.[i - off];
|
|
21239
21476
|
const body = bc?.color ?? (b.close >= b.open ? up : down);
|
|
21240
21477
|
const x = Math.round(coords.logicalToX(i)) + 0.5;
|
|
@@ -32122,6 +32359,58 @@ function stackLayers(entries, candleZ) {
|
|
|
32122
32359
|
return { below, above };
|
|
32123
32360
|
}
|
|
32124
32361
|
|
|
32362
|
+
// src/renderers/native/core/overlayStack.ts
|
|
32363
|
+
function priceStack(indicators, candleZ) {
|
|
32364
|
+
const out = [...indicators];
|
|
32365
|
+
const at = out.findIndex((e) => e.z >= candleZ);
|
|
32366
|
+
out.splice(at < 0 ? out.length : at, 0, { id: null, z: candleZ });
|
|
32367
|
+
return out;
|
|
32368
|
+
}
|
|
32369
|
+
function untie(stack) {
|
|
32370
|
+
const out = [];
|
|
32371
|
+
for (let i = 0; i < stack.length; ) {
|
|
32372
|
+
let j = i + 1;
|
|
32373
|
+
while (j < stack.length && stack[j].z === stack[i].z) j += 1;
|
|
32374
|
+
const run = j - i;
|
|
32375
|
+
if (run > 1) {
|
|
32376
|
+
const base = stack[i].z;
|
|
32377
|
+
const step = ((stack[j]?.z ?? base + 1) - base) / run;
|
|
32378
|
+
for (let k = 1; k < run; k += 1) {
|
|
32379
|
+
const id = stack[i + k].id;
|
|
32380
|
+
if (id !== null) out.push({ id, z: base + k * step });
|
|
32381
|
+
}
|
|
32382
|
+
}
|
|
32383
|
+
i = j;
|
|
32384
|
+
}
|
|
32385
|
+
return out;
|
|
32386
|
+
}
|
|
32387
|
+
function layerAbove(stack, id) {
|
|
32388
|
+
const i = stack.findIndex((e) => e.id === id);
|
|
32389
|
+
if (i < 0 || i === stack.length - 1) return null;
|
|
32390
|
+
const above = stack[i + 1];
|
|
32391
|
+
return above.id === null ? "price" : { indicator: above.id };
|
|
32392
|
+
}
|
|
32393
|
+
function layerBelow(stack, id) {
|
|
32394
|
+
const i = stack.findIndex((e) => e.id === id);
|
|
32395
|
+
if (i <= 0) return null;
|
|
32396
|
+
const below = stack[i - 1];
|
|
32397
|
+
return below.id === null ? "price" : { indicator: below.id };
|
|
32398
|
+
}
|
|
32399
|
+
function raisedZ(stack, id) {
|
|
32400
|
+
const i = stack.findIndex((e) => e.id === id);
|
|
32401
|
+
if (i < 0 || i === stack.length - 1) return null;
|
|
32402
|
+
const above = stack[i + 1];
|
|
32403
|
+
const next = stack[i + 2];
|
|
32404
|
+
return next ? (above.z + next.z) / 2 : above.z + 1;
|
|
32405
|
+
}
|
|
32406
|
+
function loweredZ(stack, id) {
|
|
32407
|
+
const i = stack.findIndex((e) => e.id === id);
|
|
32408
|
+
if (i <= 0) return null;
|
|
32409
|
+
const below = stack[i - 1];
|
|
32410
|
+
const prev2 = stack[i - 2];
|
|
32411
|
+
return prev2 ? (prev2.z + below.z) / 2 : below.z - 1;
|
|
32412
|
+
}
|
|
32413
|
+
|
|
32125
32414
|
// src/renderers/native/chrome/vela-logos.ts
|
|
32126
32415
|
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>';
|
|
32127
32416
|
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>';
|
|
@@ -32772,6 +33061,8 @@ var NativeRenderer = class {
|
|
|
32772
33061
|
// pane just below it
|
|
32773
33062
|
this.resizeSplitStart = null;
|
|
32774
33063
|
// the two panes' shared span when the drag began
|
|
33064
|
+
this.resizeStartAboveWeight = 0;
|
|
33065
|
+
// the upper pane's weight when the drag began
|
|
32775
33066
|
/** The user drawings take part in pointer input (the runtime `drawingsInteractive` feature). */
|
|
32776
33067
|
this.drawingsInteractive = true;
|
|
32777
33068
|
this.hoverSeparatorY = null;
|
|
@@ -33414,6 +33705,7 @@ var NativeRenderer = class {
|
|
|
33414
33705
|
};
|
|
33415
33706
|
this.scene.candleZ = next.stacking.candles;
|
|
33416
33707
|
for (const [id, z] of Object.entries(next.stacking.series)) this.scene.setIndicatorZ(id, z);
|
|
33708
|
+
this.syncLegendOrder();
|
|
33417
33709
|
s.bars = { upColor: next.bars.upColor, downColor: next.bars.downColor };
|
|
33418
33710
|
s.line = { color: next.line.color, width: next.line.width };
|
|
33419
33711
|
s.area = { lineColor: next.area.lineColor, width: next.area.width, topColor: next.area.topColor, bottomColor: next.area.bottomColor };
|
|
@@ -33459,6 +33751,7 @@ var NativeRenderer = class {
|
|
|
33459
33751
|
if (typeof v.z === "number") this.scene.setIndicatorZ(v.id, v.z);
|
|
33460
33752
|
else if (v.to === "front") this.scene.bringIndicatorToFront(v.id);
|
|
33461
33753
|
else if (v.to === "back") this.scene.sendIndicatorToBack(v.id);
|
|
33754
|
+
this.syncLegendOrder();
|
|
33462
33755
|
}
|
|
33463
33756
|
/** Toggle the in-chart settings gear + dialog (the `settings` feature). The gear
|
|
33464
33757
|
* sits above the time axis, clear of the right price-axis strip. */
|
|
@@ -33846,7 +34139,7 @@ var NativeRenderer = class {
|
|
|
33846
34139
|
this.chrome.mount(this.chromeCanvas);
|
|
33847
34140
|
this.crosshairLayer.mount(this.cursorCanvas);
|
|
33848
34141
|
this.coords.setViewport(defaultViewport());
|
|
33849
|
-
this.scene.ensurePane(PRICE_PANE_ID, "price", 0
|
|
34142
|
+
this.scene.ensurePane(PRICE_PANE_ID, "price", 0);
|
|
33850
34143
|
this.scheduler = new Scheduler((level) => this.renderFrame(level));
|
|
33851
34144
|
this.animator = new Animator((dt) => this.animTick(dt));
|
|
33852
34145
|
this.targetBarSpacing = this.coords.getViewport().barSpacing;
|
|
@@ -33859,7 +34152,7 @@ var NativeRenderer = class {
|
|
|
33859
34152
|
onClick: (x, y) => {
|
|
33860
34153
|
if (this.handleMarkClick(x, y)) return;
|
|
33861
34154
|
this.userDrawings?.deselect();
|
|
33862
|
-
this.handleClick(x);
|
|
34155
|
+
this.handleClick(x, y);
|
|
33863
34156
|
},
|
|
33864
34157
|
onAxisLongPress: (axis, x, y) => {
|
|
33865
34158
|
for (const cb of this.axisLongPressCbs) cb({ axis, x, y });
|
|
@@ -33873,6 +34166,7 @@ var NativeRenderer = class {
|
|
|
33873
34166
|
paneSeparatorAt: (y) => this.paneSeparatorAt(y),
|
|
33874
34167
|
beginPaneResize: (y) => this.beginPaneResize(y),
|
|
33875
34168
|
paneResizeBy: (dy) => this.paneResizeBy(dy),
|
|
34169
|
+
endPaneResize: () => this.endPaneResize(),
|
|
33876
34170
|
resetPaneSize: (y) => this.resetPaneSize(y),
|
|
33877
34171
|
resetView: () => this.resetView(),
|
|
33878
34172
|
// User drawings claim a gesture before pan when armed / over a drawing.
|
|
@@ -33998,6 +34292,12 @@ var NativeRenderer = class {
|
|
|
33998
34292
|
for (const cb of this.moveIndicatorCbs) cb(id, target);
|
|
33999
34293
|
}
|
|
34000
34294
|
});
|
|
34295
|
+
this.inputsUI.setStackApi({
|
|
34296
|
+
above: (id) => layerAbove(this.overlayStack(), id),
|
|
34297
|
+
below: (id) => layerBelow(this.overlayStack(), id),
|
|
34298
|
+
raise: (id) => this.restackOverlay(id, raisedZ),
|
|
34299
|
+
lower: (id) => this.restackOverlay(id, loweredZ)
|
|
34300
|
+
});
|
|
34001
34301
|
this.paneControls = new PaneControls(this.plot, theme, {
|
|
34002
34302
|
panes: () => this.paneControlViews(),
|
|
34003
34303
|
rightAxis: () => this.rightAxisW,
|
|
@@ -34315,7 +34615,7 @@ var NativeRenderer = class {
|
|
|
34315
34615
|
}
|
|
34316
34616
|
// ── panes ──
|
|
34317
34617
|
ensurePane(pane) {
|
|
34318
|
-
this.scene.ensurePane(pane.id, pane.kind, pane.order, pane.heightWeight
|
|
34618
|
+
this.scene.ensurePane(pane.id, pane.kind, pane.order, pane.heightWeight);
|
|
34319
34619
|
this.layoutPanes();
|
|
34320
34620
|
this.paneControls?.refresh();
|
|
34321
34621
|
this.scheduler.invalidate(4 /* Full */);
|
|
@@ -34337,6 +34637,7 @@ var NativeRenderer = class {
|
|
|
34337
34637
|
model.ownScale = opts?.ownScale === true;
|
|
34338
34638
|
if (!model.ownScale) this.scene.dropIndicatorScale(handle.id);
|
|
34339
34639
|
this.inputsUI.setPane(handle.id, paneId);
|
|
34640
|
+
this.syncLegendOrder();
|
|
34340
34641
|
this.refreshAnchorOffset(model);
|
|
34341
34642
|
this.refreshAxisWidth();
|
|
34342
34643
|
this.layoutPanes();
|
|
@@ -34364,6 +34665,15 @@ var NativeRenderer = class {
|
|
|
34364
34665
|
this.paneControls?.refresh();
|
|
34365
34666
|
this.scheduler.invalidate(4 /* Full */);
|
|
34366
34667
|
}
|
|
34668
|
+
setPaneHeightWeight(paneId, weight) {
|
|
34669
|
+
const pane = this.scene.panes.get(paneId);
|
|
34670
|
+
if (!pane || !(weight > 0) || pane.heightWeight === weight) return;
|
|
34671
|
+
pane.heightWeight = weight;
|
|
34672
|
+
this.afterPaneLayoutChange();
|
|
34673
|
+
}
|
|
34674
|
+
paneHeightWeight(paneId) {
|
|
34675
|
+
return this.scene.panes.get(paneId)?.heightWeight ?? null;
|
|
34676
|
+
}
|
|
34367
34677
|
onPaneAction(cb) {
|
|
34368
34678
|
this.paneActionCbs.add(cb);
|
|
34369
34679
|
return () => this.paneActionCbs.delete(cb);
|
|
@@ -34458,6 +34768,7 @@ var NativeRenderer = class {
|
|
|
34458
34768
|
this.vpvrHidden = false;
|
|
34459
34769
|
}
|
|
34460
34770
|
if (model.ownScale) this.refreshAxisWidth();
|
|
34771
|
+
this.syncLegendOrder();
|
|
34461
34772
|
this.paneControls?.refresh();
|
|
34462
34773
|
this.scheduler.invalidate(4 /* Full */);
|
|
34463
34774
|
return { id: model.id };
|
|
@@ -34865,7 +35176,7 @@ var NativeRenderer = class {
|
|
|
34865
35176
|
this.setMarkHover(null);
|
|
34866
35177
|
this.scheduler.invalidate(1 /* Cursor */);
|
|
34867
35178
|
this.hoverLogical = null;
|
|
34868
|
-
const empty = { time: null, price: null, paneKind: null, values: /* @__PURE__ */ new Map(), ohlc: null };
|
|
35179
|
+
const empty = { time: null, price: null, paneKind: null, paneId: null, values: /* @__PURE__ */ new Map(), ohlc: null };
|
|
34869
35180
|
for (const cb of this.crosshairCbs) cb(empty);
|
|
34870
35181
|
return;
|
|
34871
35182
|
}
|
|
@@ -34896,14 +35207,23 @@ var NativeRenderer = class {
|
|
|
34896
35207
|
const bar = onBar ? this.bars[logical] : void 0;
|
|
34897
35208
|
const ohlc = bar ? { time: bar.time, open: bar.open, high: bar.high, low: bar.low, close: bar.close, volume: bar.volume } : null;
|
|
34898
35209
|
this.hoverLogical = onBar ? logical : null;
|
|
34899
|
-
const
|
|
35210
|
+
const onPane = inData && pane ? pane : null;
|
|
35211
|
+
const event = { time, price, paneKind: onPane?.kind ?? null, paneId: onPane?.id ?? null, values, ohlc };
|
|
34900
35212
|
for (const cb of this.crosshairCbs) cb(event);
|
|
34901
35213
|
}
|
|
34902
|
-
handleClick(x) {
|
|
35214
|
+
handleClick(x, y) {
|
|
34903
35215
|
if (this.coords.barCount === 0) return;
|
|
34904
35216
|
const logical = Math.round(this.coords.xToLogical(x));
|
|
34905
35217
|
const onBar = logical >= 0 && logical < this.coords.barCount;
|
|
34906
|
-
|
|
35218
|
+
const inData = x >= 0 && y >= 0 && x <= this.coords.width && y <= this.coords.height;
|
|
35219
|
+
const pane = inData ? this.paneNodeAtY(y) : null;
|
|
35220
|
+
const event = {
|
|
35221
|
+
time: onBar ? this.coords.logicalToTime(logical) : null,
|
|
35222
|
+
price: pane ? this.coords.yToPrice(y, pane.scale, pane.bounds) : null,
|
|
35223
|
+
paneKind: pane?.kind ?? null,
|
|
35224
|
+
paneId: pane?.id ?? null
|
|
35225
|
+
};
|
|
35226
|
+
for (const cb of this.clickCbs) cb(event);
|
|
34907
35227
|
}
|
|
34908
35228
|
/**
|
|
34909
35229
|
* Fan out (or collapse, with null) a multi-group mark stack; repaints the chrome tier when
|
|
@@ -35148,6 +35468,7 @@ var NativeRenderer = class {
|
|
|
35148
35468
|
const below = panes[i];
|
|
35149
35469
|
this.resizeAbove = above;
|
|
35150
35470
|
this.resizeBelow = below;
|
|
35471
|
+
this.resizeStartAboveWeight = above.heightWeight;
|
|
35151
35472
|
this.resizeSplitStart = {
|
|
35152
35473
|
combinedTop: above.bounds.top,
|
|
35153
35474
|
combinedHeight: above.bounds.height + below.bounds.height,
|
|
@@ -35168,6 +35489,16 @@ var NativeRenderer = class {
|
|
|
35168
35489
|
this.layoutPanes();
|
|
35169
35490
|
this.scheduler.invalidate(4 /* Full */);
|
|
35170
35491
|
}
|
|
35492
|
+
/** Release the separator: report the resize once, at the end of the drag, if it changed anything. */
|
|
35493
|
+
endPaneResize() {
|
|
35494
|
+
const above = this.resizeAbove;
|
|
35495
|
+
const below = this.resizeBelow;
|
|
35496
|
+
const startAbove = this.resizeStartAboveWeight;
|
|
35497
|
+
this.resizeAbove = null;
|
|
35498
|
+
this.resizeBelow = null;
|
|
35499
|
+
this.resizeSplitStart = null;
|
|
35500
|
+
if (above && below && above.heightWeight !== startAbove) this.emitPaneAction({ type: "resize", paneIds: [above.id, below.id] });
|
|
35501
|
+
}
|
|
35171
35502
|
/** Double-click a separator → split the two adjacent panes evenly (each gets half of
|
|
35172
35503
|
* their combined weight). A simple, predictable reset that leaves siblings untouched. */
|
|
35173
35504
|
resetPaneSize(y) {
|
|
@@ -35181,6 +35512,7 @@ var NativeRenderer = class {
|
|
|
35181
35512
|
below.heightWeight = half;
|
|
35182
35513
|
this.layoutPanes();
|
|
35183
35514
|
this.scheduler.invalidate(4 /* Full */);
|
|
35515
|
+
this.emitPaneAction({ type: "resize", paneIds: [above.id, below.id] });
|
|
35184
35516
|
}
|
|
35185
35517
|
// ── keyboard navigation / accessibility (item 11) ──
|
|
35186
35518
|
/** Enable/disable keyboard control: focusability + ARIA + key listeners + live region. */
|
|
@@ -35435,6 +35767,7 @@ var NativeRenderer = class {
|
|
|
35435
35767
|
/** Paint the below-data (L-1) + geometry (L0) + chrome (L1) layers from the current scene/coords. */
|
|
35436
35768
|
paintData() {
|
|
35437
35769
|
this.syncLayerCanvasOrder();
|
|
35770
|
+
this.syncLegendOrder();
|
|
35438
35771
|
this.stampScaleInvert();
|
|
35439
35772
|
this.backend.modelAlpha = this.modelAlpha;
|
|
35440
35773
|
this.backend.candleBodyAlpha = this.candleBodyAlpha;
|
|
@@ -35616,6 +35949,7 @@ var NativeRenderer = class {
|
|
|
35616
35949
|
const i0 = Math.max(0, Math.floor(vr.from));
|
|
35617
35950
|
const i1 = Math.min(n - 1, Math.ceil(vr.to));
|
|
35618
35951
|
if (i1 < i0) return;
|
|
35952
|
+
const s1 = Math.max(i1, Math.ceil(vr.to));
|
|
35619
35953
|
this.scene.percentBaseline = this.bars[i0]?.close ?? 0;
|
|
35620
35954
|
const panes = this.scene.orderedPanes();
|
|
35621
35955
|
const pricePane = panes.find((p) => p.kind === "price") ?? null;
|
|
@@ -35633,11 +35967,11 @@ var NativeRenderer = class {
|
|
|
35633
35967
|
const masterModels = models.filter((m) => m.ownScale !== true);
|
|
35634
35968
|
let dr = this.chrome.paneDrawingsRange(masterModels, this.scene, pane === pricePane, vr);
|
|
35635
35969
|
if (pane === pricePane) {
|
|
35636
|
-
const or = overlaySeriesRange(this.scene.indicators.values(), i0,
|
|
35970
|
+
const or = overlaySeriesRange(this.scene.indicators.values(), i0, s1, (id) => this.scene.offsetOf(id));
|
|
35637
35971
|
if (or) dr = dr ? { min: Math.min(dr.min, or.min), max: Math.max(dr.max, or.max) } : or;
|
|
35638
35972
|
}
|
|
35639
35973
|
const includeCandles = pane.kind === "price" && (!this.scene.candlesHidden || this.priceLayersAnchoredToBars(masterModels) || !this.paneHasMeasurableContent(masterModels, dr));
|
|
35640
|
-
pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0,
|
|
35974
|
+
pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, s1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id), margins);
|
|
35641
35975
|
pane.percentBaseline = pane.kind === "price" ? this.bars[i0]?.close ?? 0 : this.firstVisibleValue(masterModels, i0);
|
|
35642
35976
|
pane.axisFormat = void 0;
|
|
35643
35977
|
pane.axisBands = void 0;
|
|
@@ -35677,7 +36011,7 @@ var NativeRenderer = class {
|
|
|
35677
36011
|
continue;
|
|
35678
36012
|
}
|
|
35679
36013
|
const mdr = this.chrome.paneDrawingsRange([model], this.scene, false, vr);
|
|
35680
|
-
sl.scaleTarget = computePaneScale([model], this.bars, false, i0,
|
|
36014
|
+
sl.scaleTarget = computePaneScale([model], this.bars, false, i0, s1, mdr, false, (id) => this.scene.offsetOf(id), margins);
|
|
35681
36015
|
if (!animating || !sl.initialized) {
|
|
35682
36016
|
sl.scale = { ...sl.scaleTarget };
|
|
35683
36017
|
sl.initialized = true;
|
|
@@ -35748,6 +36082,30 @@ var NativeRenderer = class {
|
|
|
35748
36082
|
const master = models.find((m) => m.ownScale !== true) ?? models[0];
|
|
35749
36083
|
return master?.title || null;
|
|
35750
36084
|
}
|
|
36085
|
+
/**
|
|
36086
|
+
* The price pane's layers back to front, as the legend restacks them: every indicator
|
|
36087
|
+
* with a legend row, in paint order, plus the candles. The same layers and order the
|
|
36088
|
+
* object tree lists (see `indicatorZOrder`).
|
|
36089
|
+
*/
|
|
36090
|
+
overlayStack() {
|
|
36091
|
+
const painted = this.scene.orderedIndicatorsForPane(PRICE_PANE_ID).filter((m) => m.legend !== false).map((m) => ({ id: m.id, z: this.scene.zOf(m.id) }));
|
|
36092
|
+
return priceStack(painted, this.scene.candleZ);
|
|
36093
|
+
}
|
|
36094
|
+
/** List the legend rows the way the indicators stack, front-most first (no-op when unchanged). */
|
|
36095
|
+
syncLegendOrder() {
|
|
36096
|
+
this.inputsUI?.orderRows(this.scene.indicatorZOrder().map((e) => e.id));
|
|
36097
|
+
}
|
|
36098
|
+
/** Apply a legend restacking step: shared keys are spread apart first (the order stays),
|
|
36099
|
+
* so the step lands exactly one layer over. No-op when there is nothing to step over. */
|
|
36100
|
+
restackOverlay(id, step) {
|
|
36101
|
+
for (const fix of untie(this.overlayStack())) this.scene.setIndicatorZ(fix.id, fix.z);
|
|
36102
|
+
const z = step(this.overlayStack(), id);
|
|
36103
|
+
if (z === null) return;
|
|
36104
|
+
this.scene.setIndicatorZ(id, z);
|
|
36105
|
+
this.syncLegendOrder();
|
|
36106
|
+
this.scheduler.invalidate(4 /* Full */);
|
|
36107
|
+
for (const cb of this.configChangedCbs) cb();
|
|
36108
|
+
}
|
|
35751
36109
|
/** The pane a bespoke-layer native indicator (volume/vpvr) currently lives in (null = not mounted). */
|
|
35752
36110
|
nativeLayerPane(type) {
|
|
35753
36111
|
for (const m of this.scene.indicators.values()) {
|
|
@@ -36202,17 +36560,35 @@ function leadingPointsBefore(model, headTime) {
|
|
|
36202
36560
|
for (const s of model.series) {
|
|
36203
36561
|
const times = s.kind === "candle" || s.kind === "bar" ? s.bars : isLineLikeSeries(s) ? s.points : null;
|
|
36204
36562
|
if (!times || times.length === 0) continue;
|
|
36205
|
-
|
|
36206
|
-
|
|
36207
|
-
while (lo < hi) {
|
|
36208
|
-
const mid = lo + hi >> 1;
|
|
36209
|
-
if (times[mid].time < headTime) lo = mid + 1;
|
|
36210
|
-
else hi = mid;
|
|
36211
|
-
}
|
|
36212
|
-
return lo;
|
|
36563
|
+
const count = slotsBefore(times, headTime);
|
|
36564
|
+
if (count !== null) return count;
|
|
36213
36565
|
}
|
|
36214
36566
|
return null;
|
|
36215
36567
|
}
|
|
36568
|
+
function slotsBefore(times, headTime) {
|
|
36569
|
+
let lo = 0;
|
|
36570
|
+
let hi = times.length;
|
|
36571
|
+
while (lo < hi) {
|
|
36572
|
+
const mid = lo + hi >> 1;
|
|
36573
|
+
const t = times[mid]?.time;
|
|
36574
|
+
if (t === void 0) return slotsBeforeScan(times, headTime);
|
|
36575
|
+
if (t < headTime) lo = mid + 1;
|
|
36576
|
+
else hi = mid;
|
|
36577
|
+
}
|
|
36578
|
+
return lo;
|
|
36579
|
+
}
|
|
36580
|
+
function slotsBeforeScan(times, headTime) {
|
|
36581
|
+
let lastBefore = -1;
|
|
36582
|
+
let any = false;
|
|
36583
|
+
for (let i = 0; i < times.length; i += 1) {
|
|
36584
|
+
const t = times[i]?.time;
|
|
36585
|
+
if (t === void 0) continue;
|
|
36586
|
+
any = true;
|
|
36587
|
+
if (t >= headTime) return t === headTime ? i : lastBefore + 1;
|
|
36588
|
+
lastBefore = i;
|
|
36589
|
+
}
|
|
36590
|
+
return any ? times.length : null;
|
|
36591
|
+
}
|
|
36216
36592
|
function applyPatch(model, patch) {
|
|
36217
36593
|
if (patch.kind === "value") {
|
|
36218
36594
|
for (const delta of patch.series) {
|
|
@@ -36625,6 +37001,9 @@ function classicDescriptor(spec) {
|
|
|
36625
37001
|
overlay: spec.overlay,
|
|
36626
37002
|
// Studies stack (a 20 and a 200 moving average side by side) — unlike the layer-backed natives.
|
|
36627
37003
|
multiInstance: true,
|
|
37004
|
+
// A study's values depend on every bar before them (an EMA's seed, an RSI's
|
|
37005
|
+
// smoothing), so it computes once over the whole history, as script indicators do.
|
|
37006
|
+
waitsForHistory: true,
|
|
36628
37007
|
inputsSchema: () => spec.inputs,
|
|
36629
37008
|
defaultInputs: () => Object.fromEntries(spec.inputs.map((i) => [i.key, i.defval])),
|
|
36630
37009
|
create: () => new ClassicIndicator(spec)
|
|
@@ -37460,41 +37839,50 @@ var maEnvelope = {
|
|
|
37460
37839
|
return directionalChannel(bars, basis, map(basis, (x) => x * (1 + k)), map(basis, (x) => x * (1 - k)), ink, "envelope");
|
|
37461
37840
|
}
|
|
37462
37841
|
};
|
|
37463
|
-
var
|
|
37464
|
-
|
|
37465
|
-
|
|
37466
|
-
|
|
37467
|
-
|
|
37468
|
-
|
|
37469
|
-
|
|
37842
|
+
var leastSquaresMa = {
|
|
37843
|
+
type: "lsma",
|
|
37844
|
+
title: "Least Squares Moving Average",
|
|
37845
|
+
shortTitle: "LSMA",
|
|
37846
|
+
overlay: true,
|
|
37847
|
+
inputs: [
|
|
37848
|
+
sourceInput("Close", "source", "Source", "Price series the regression line is fitted to."),
|
|
37849
|
+
{ ...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 },
|
|
37850
|
+
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."),
|
|
37851
|
+
...directionInputs()
|
|
37852
|
+
],
|
|
37853
|
+
compute: (bars, inputs) => {
|
|
37854
|
+
const src = sourceValues(bars, str3(inputs, "source", "Close"));
|
|
37855
|
+
const values = linreg(src, num3(inputs, "length", 25), Math.trunc(num3(inputs, "offset", 0)));
|
|
37856
|
+
return { plots: [slopeColoredPlot("lsma", "LSMA", values, inputs)] };
|
|
37857
|
+
}
|
|
37858
|
+
};
|
|
37859
|
+
var linearRegressionSlope = {
|
|
37860
|
+
type: "linear-regression-slope",
|
|
37861
|
+
title: "Linear Regression Slope",
|
|
37862
|
+
shortTitle: "LRS",
|
|
37470
37863
|
overlay: false,
|
|
37471
37864
|
inputs: [
|
|
37472
|
-
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."),
|
|
37473
37865
|
{ ...lengthInput(100, "length", "Length", 5e3, "Number of bars used to fit the linear regression."), min: 2 },
|
|
37474
37866
|
sourceInput("Close", "source", "Source", "Price series the regression is fitted to."),
|
|
37475
|
-
colorInput(BULLISH, "Rising", "bullColor", "Color of the
|
|
37476
|
-
colorInput(BEARISH, "Falling", "bearColor", "Color of the
|
|
37477
|
-
colorInput(NEUTRAL, "
|
|
37867
|
+
colorInput(BULLISH, "Rising", "bullColor", "Color of the slope line while the fit slopes upward."),
|
|
37868
|
+
colorInput(BEARISH, "Falling", "bearColor", "Color of the slope line while the fit slopes downward."),
|
|
37869
|
+
colorInput(NEUTRAL, "Slope Line", "lineColor", "Color of the slope line while the fit is flat.")
|
|
37478
37870
|
],
|
|
37479
37871
|
compute: (bars, inputs) => {
|
|
37480
37872
|
const len = num3(inputs, "length", 100);
|
|
37481
37873
|
const src = sourceValues(bars, str3(inputs, "source", "Close"));
|
|
37482
|
-
const
|
|
37483
|
-
const slope = zip(curve, linreg(src, len, 1), (a, b) => a - b);
|
|
37874
|
+
const slope = zip(linreg(src, len, 0), linreg(src, len, 1), (a, b) => a - b);
|
|
37484
37875
|
const bull = str3(inputs, "bullColor", BULLISH);
|
|
37485
37876
|
const bear = str3(inputs, "bearColor", BEARISH);
|
|
37486
37877
|
const flat = str3(inputs, "lineColor", NEUTRAL);
|
|
37487
37878
|
const colors = slope.map((s) => Number.isFinite(s) ? s > 0 ? bull : s < 0 ? bear : flat : null);
|
|
37488
|
-
|
|
37489
|
-
|
|
37490
|
-
|
|
37491
|
-
|
|
37492
|
-
};
|
|
37493
|
-
}
|
|
37494
|
-
return { plots: [{ key: "curve", title: "Curve", values: curve, color: flat, width: 2, colors, overlay: true }] };
|
|
37879
|
+
return {
|
|
37880
|
+
plots: [{ key: "slope", title: "Slope", values: slope, color: flat, width: 2, colors }],
|
|
37881
|
+
levels: [{ key: "zero", price: 0, color: transp(NEUTRAL, 50), lineStyle: "dashed" }]
|
|
37882
|
+
};
|
|
37495
37883
|
}
|
|
37496
37884
|
};
|
|
37497
|
-
var averageSpecs = [simpleMa, exponentialMa, movingAverage, smoothedMa, zlema, vidya, maEnvelope,
|
|
37885
|
+
var averageSpecs = [simpleMa, exponentialMa, movingAverage, smoothedMa, zlema, vidya, maEnvelope, leastSquaresMa, linearRegressionSlope];
|
|
37498
37886
|
|
|
37499
37887
|
// src/core/native-indicators/classics/bands.ts
|
|
37500
37888
|
var JAW_COLOR = "#ab47bc";
|