@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/widget.cjs
CHANGED
|
@@ -13553,9 +13553,14 @@ function paneItems(snap, pane, units) {
|
|
|
13553
13553
|
return [...unitItems, ...rows2];
|
|
13554
13554
|
}
|
|
13555
13555
|
const zOf = new Map(snap.zOrder.map((e) => [e.id, e.z]));
|
|
13556
|
-
const
|
|
13557
|
-
|
|
13558
|
-
|
|
13556
|
+
const rank = new Map(snap.zOrder.map((e, i) => [e.id, i]));
|
|
13557
|
+
const rows = pane.indicators.map((i) => ({
|
|
13558
|
+
item: { kind: "row", row: indicatorRow(i) },
|
|
13559
|
+
z: zOf.get(i.id) ?? 0,
|
|
13560
|
+
rank: rank.get(i.id) ?? -1
|
|
13561
|
+
}));
|
|
13562
|
+
if (pane.kind === "price") rows.push({ item: { kind: "row", row: priceRow() }, z: snap.candleZ, rank: -Infinity });
|
|
13563
|
+
rows.sort((a, b) => b.z - a.z || b.rank - a.rank);
|
|
13559
13564
|
if (!snap.interleave) return [...unitItems, ...rows.map((r) => r.item)];
|
|
13560
13565
|
const entries = [...rows, ...units.map((unit) => ({ item: { kind: "unit", unit }, z: unitZ(unit) }))];
|
|
13561
13566
|
entries.sort((a, b) => a.z === b.z ? Number(a.item.kind === "unit") - Number(b.item.kind === "unit") : b.z - a.z);
|
|
@@ -14231,6 +14236,7 @@ var ObjectTree = class extends SidePanel {
|
|
|
14231
14236
|
for (const ev of REFRESH_EVENTS) {
|
|
14232
14237
|
this.unsubs.push(chart.on(ev, () => this.refresh()));
|
|
14233
14238
|
}
|
|
14239
|
+
this.unsubs.push(chart.renderer.onConfigChanged(() => this.refresh()));
|
|
14234
14240
|
this.unsubs.push(
|
|
14235
14241
|
chart.on("drawing:selected", ({ ids }) => {
|
|
14236
14242
|
this.selectedDrawings = new Set(ids);
|
|
@@ -19832,6 +19838,20 @@ function sanitizeDrawingTools(raw) {
|
|
|
19832
19838
|
if (typeof r.stay === "boolean") out.stay = r.stay;
|
|
19833
19839
|
return Object.keys(out).length > 0 ? out : null;
|
|
19834
19840
|
}
|
|
19841
|
+
function successorNative(entry) {
|
|
19842
|
+
const type = typeof entry === "string" ? entry : entry.type;
|
|
19843
|
+
if (type !== "linear-regression") return entry;
|
|
19844
|
+
const hidden = typeof entry === "object" && entry.hidden === true;
|
|
19845
|
+
const { mode, lineColor, ...rest } = typeof entry === "object" ? entry.inputs ?? {} : {};
|
|
19846
|
+
const slope = mode === "Slope";
|
|
19847
|
+
const inputs = slope ? { ...rest, ...lineColor !== void 0 ? { lineColor } : {} } : (
|
|
19848
|
+
// Saved deltas omit the curve's 100-bar default, which the LSMA's default is not.
|
|
19849
|
+
{ length: 100, ...rest, ...lineColor !== void 0 ? { neutralColor: lineColor } : {} }
|
|
19850
|
+
);
|
|
19851
|
+
const next = slope ? "linear-regression-slope" : "lsma";
|
|
19852
|
+
const hasInputs = Object.keys(inputs).length > 0;
|
|
19853
|
+
return hasInputs || hidden ? { type: next, ...hasInputs ? { inputs } : {}, ...hidden ? { hidden } : {} } : next;
|
|
19854
|
+
}
|
|
19835
19855
|
function sanitizeCell(raw) {
|
|
19836
19856
|
if (raw == null || typeof raw !== "object") return null;
|
|
19837
19857
|
const c = raw;
|
|
@@ -19871,7 +19891,7 @@ function sanitizeCell(raw) {
|
|
|
19871
19891
|
return [inputs || hidden ? { type: e.type, ...inputs ? { inputs } : {}, ...hidden ? { hidden } : {} } : e.type];
|
|
19872
19892
|
}
|
|
19873
19893
|
return [];
|
|
19874
|
-
}) : [];
|
|
19894
|
+
}).map(successorNative) : [];
|
|
19875
19895
|
out.indicators = { manifest, natives };
|
|
19876
19896
|
}
|
|
19877
19897
|
const ext = sanitizeExt(c.ext);
|
|
@@ -20649,6 +20669,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
20649
20669
|
this.barTransform = null;
|
|
20650
20670
|
// ── chart-type data engines (SDK): one lazily-created engine per style id ──
|
|
20651
20671
|
this.typeEngines = /* @__PURE__ */ new Map();
|
|
20672
|
+
/** Engines suspended by the chart — their host pushes are dropped until they resume. */
|
|
20673
|
+
this.suspendedEngines = /* @__PURE__ */ new WeakSet();
|
|
20652
20674
|
/** Last-seen chart-type settings per type id (dialog edits + persisted restores) —
|
|
20653
20675
|
* replayed into a data engine whenever one is (re)created. */
|
|
20654
20676
|
this.typeSettings = /* @__PURE__ */ new Map();
|
|
@@ -20923,12 +20945,14 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
20923
20945
|
this.mergeHistoryIntoReplay(bars);
|
|
20924
20946
|
return;
|
|
20925
20947
|
}
|
|
20926
|
-
|
|
20948
|
+
const repaint = painted;
|
|
20949
|
+
this.setBarSeries(bars, repaint ? { preserveView: true } : void 0);
|
|
20927
20950
|
if (!painted && bars.length > 0) {
|
|
20928
20951
|
painted = true;
|
|
20929
20952
|
if (opts.firstLoad) this.activateBarLayers();
|
|
20930
20953
|
if (!final) this.historyState = "backfill";
|
|
20931
20954
|
}
|
|
20955
|
+
if (repaint && !final) this.notifySessionsBars("backfill");
|
|
20932
20956
|
};
|
|
20933
20957
|
const abort = new AbortController();
|
|
20934
20958
|
this.progressiveAbort = abort;
|
|
@@ -21131,10 +21155,15 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
21131
21155
|
this.setBarSeries([], { clearing: true });
|
|
21132
21156
|
this.blankIndicatorVisuals();
|
|
21133
21157
|
if (this.activeEngineStyle) {
|
|
21134
|
-
this.typeEngines.get(this.activeEngineStyle)
|
|
21158
|
+
this.suspendTypeEngine(this.typeEngines.get(this.activeEngineStyle));
|
|
21135
21159
|
this.renderer.setNativeData?.(this.activeEngineStyle, void 0);
|
|
21136
21160
|
this.renderer.setNativeData?.(`${this.activeEngineStyle}-pending`, []);
|
|
21137
21161
|
}
|
|
21162
|
+
for (const record of this.registry.all()) {
|
|
21163
|
+
if (!record.native?.started) continue;
|
|
21164
|
+
this.stopNativeInstance(record.native);
|
|
21165
|
+
this.renderer.setNativeData?.(this.nativeChannel(record), void 0);
|
|
21166
|
+
}
|
|
21138
21167
|
}
|
|
21139
21168
|
if (depthOnly) {
|
|
21140
21169
|
this.extendDepth(gen, m.bars ?? 500);
|
|
@@ -21161,6 +21190,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
21161
21190
|
this.restartChartTypeEngine();
|
|
21162
21191
|
this.reexecuteIndicators();
|
|
21163
21192
|
}
|
|
21193
|
+
if (depthOnly) this.releaseHeldNatives();
|
|
21164
21194
|
if (!replayBack) this.startLive();
|
|
21165
21195
|
if (identityChanged) {
|
|
21166
21196
|
this.events.emit("market:changed", { symbol: m.symbol ?? "TEST", timeframe: m.timeframe ?? "60", prev: prev2 });
|
|
@@ -21177,9 +21207,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
21177
21207
|
restartNativeIndicators() {
|
|
21178
21208
|
for (const record of this.registry.all()) {
|
|
21179
21209
|
if (!record.native) continue;
|
|
21180
|
-
|
|
21210
|
+
this.stopNativeInstance(record.native);
|
|
21181
21211
|
record.native.instance = record.native.descriptor.create();
|
|
21182
|
-
record.native.started = false;
|
|
21183
21212
|
record.pendingStructural = true;
|
|
21184
21213
|
if (record.hidden) {
|
|
21185
21214
|
record.native.stale = true;
|
|
@@ -21196,6 +21225,21 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
21196
21225
|
this.typeEngines.clear();
|
|
21197
21226
|
if (this.activeEngineStyle) void this.startChartTypeEngine(this.activeEngineStyle);
|
|
21198
21227
|
}
|
|
21228
|
+
/** Suspend a chart-type engine. Its host drops every push until the engine resumes:
|
|
21229
|
+
* work it left in flight lands in its own state, never on the scene. */
|
|
21230
|
+
suspendTypeEngine(engine) {
|
|
21231
|
+
if (!engine) return;
|
|
21232
|
+
engine.suspend();
|
|
21233
|
+
this.suspendedEngines.add(engine);
|
|
21234
|
+
}
|
|
21235
|
+
/** Stop a running native instance, then revoke its context: the teardown inside `stop()`
|
|
21236
|
+
* still reaches its layer, but work it left in flight (a fetch resolving later) does not. */
|
|
21237
|
+
stopNativeInstance(native) {
|
|
21238
|
+
if (native.started) native.instance.stop();
|
|
21239
|
+
native.started = false;
|
|
21240
|
+
native.revokeContext?.();
|
|
21241
|
+
native.revokeContext = void 0;
|
|
21242
|
+
}
|
|
21199
21243
|
/**
|
|
21200
21244
|
* The FIRST candles just painted (the quick preview, or the only load): activate the
|
|
21201
21245
|
* layers that need bars but NOT full history. The volume layer reads the chart's bars
|
|
@@ -21728,7 +21772,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
21728
21772
|
const def = chartType(id);
|
|
21729
21773
|
const want = def?.dataEngine ? id : null;
|
|
21730
21774
|
if (want === this.activeEngineStyle) return;
|
|
21731
|
-
if (this.activeEngineStyle) this.typeEngines.get(this.activeEngineStyle)
|
|
21775
|
+
if (this.activeEngineStyle) this.suspendTypeEngine(this.typeEngines.get(this.activeEngineStyle));
|
|
21732
21776
|
this.activeEngineStyle = want;
|
|
21733
21777
|
if (want) void this.startChartTypeEngine(want);
|
|
21734
21778
|
}
|
|
@@ -21739,6 +21783,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
21739
21783
|
if (this.activeEngineStyle !== id) return;
|
|
21740
21784
|
const existing = this.typeEngines.get(id);
|
|
21741
21785
|
if (existing) {
|
|
21786
|
+
this.suspendedEngines.delete(existing);
|
|
21742
21787
|
existing.resume();
|
|
21743
21788
|
return;
|
|
21744
21789
|
}
|
|
@@ -21748,6 +21793,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
21748
21793
|
this.typeEngines.set(id, engine);
|
|
21749
21794
|
const stored = this.typeSettings.get(id);
|
|
21750
21795
|
if (stored) engine.onSettings?.(stored);
|
|
21796
|
+
const reachesScene = () => this.typeEngines.get(id) === engine && !this.suspendedEngines.has(engine);
|
|
21751
21797
|
engine.start({
|
|
21752
21798
|
symbol: this.config.market.symbol ?? "TEST",
|
|
21753
21799
|
timeframe: this.config.market.timeframe ?? "60",
|
|
@@ -21755,8 +21801,12 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
21755
21801
|
session: this.config.market.session,
|
|
21756
21802
|
bars: () => this.bars,
|
|
21757
21803
|
data: this.dataControl,
|
|
21758
|
-
pushData: (data) =>
|
|
21759
|
-
|
|
21804
|
+
pushData: (data) => {
|
|
21805
|
+
if (reachesScene()) this.renderer.setNativeData?.(id, data);
|
|
21806
|
+
},
|
|
21807
|
+
pushPending: (ranges) => {
|
|
21808
|
+
if (reachesScene()) this.renderer.setNativeData?.(`${id}-pending`, ranges);
|
|
21809
|
+
}
|
|
21760
21810
|
});
|
|
21761
21811
|
}
|
|
21762
21812
|
/**
|
|
@@ -21764,8 +21814,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
21764
21814
|
* a pre-load style change is picked up by init's `setBarSeries`. With bars loaded, the
|
|
21765
21815
|
* view is rebuilt in place (viewport preserved) and every Pine indicator re-executes:
|
|
21766
21816
|
* its input data changed wholesale, which a streaming session's incremental context
|
|
21767
|
-
* cannot absorb.
|
|
21768
|
-
*
|
|
21817
|
+
* cannot absorb. Natives computed from `ctx.bars()` re-run over the new candles (a
|
|
21818
|
+
* synthetic price style is their source too); layer natives that read the renderer's
|
|
21819
|
+
* bars per frame ignore the poke.
|
|
21769
21820
|
*/
|
|
21770
21821
|
syncBarTransform(style) {
|
|
21771
21822
|
const transform = barTransformFor(style);
|
|
@@ -21775,6 +21826,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
21775
21826
|
this.bars = transform ? transform.full(this.rawBars) : this.rawBars;
|
|
21776
21827
|
this.renderer.setBars(this.bars, { preserveView: true });
|
|
21777
21828
|
this.reexecuteIndicators();
|
|
21829
|
+
if (this.switchingMarket) return;
|
|
21830
|
+
for (const record of this.registry.all()) {
|
|
21831
|
+
if (record.native?.started && record.renderHandle && !record.hidden) this.pokeNative(record);
|
|
21832
|
+
}
|
|
21778
21833
|
}
|
|
21779
21834
|
/**
|
|
21780
21835
|
* Blank every mounted indicator's painted output — series data, fills, backgrounds,
|
|
@@ -21875,10 +21930,39 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
21875
21930
|
if (record.session) {
|
|
21876
21931
|
record.pendingCause = cause;
|
|
21877
21932
|
record.session.notifyBars(reason);
|
|
21878
|
-
} else if (record.native)
|
|
21933
|
+
} else if (record.native) this.pokeNative(record);
|
|
21879
21934
|
}
|
|
21880
21935
|
if (this.activeEngineStyle) this.typeEngines.get(this.activeEngineStyle)?.onBars?.();
|
|
21881
21936
|
}
|
|
21937
|
+
/**
|
|
21938
|
+
* Tell a native its bars changed. A `waitsForHistory` native ignores every change while
|
|
21939
|
+
* the history is still loading — a backfill chunk, a deeper snapshot, a tick arriving
|
|
21940
|
+
* meanwhile — and its output stays on its bars through the anchor its emit carried.
|
|
21941
|
+
* Once the history is complete, a held one starts and a running one re-runs.
|
|
21942
|
+
*/
|
|
21943
|
+
pokeNative(record) {
|
|
21944
|
+
const native = record.native;
|
|
21945
|
+
if (!native.descriptor.waitsForHistory) {
|
|
21946
|
+
native.instance.onBars();
|
|
21947
|
+
return;
|
|
21948
|
+
}
|
|
21949
|
+
if (this.historyState === "backfill") return;
|
|
21950
|
+
if (native.heldForHistory) {
|
|
21951
|
+
native.heldForHistory = false;
|
|
21952
|
+
const handle = this.handles.get(record.id);
|
|
21953
|
+
if (handle) void this.startNativeIndicator(record.id, handle);
|
|
21954
|
+
return;
|
|
21955
|
+
}
|
|
21956
|
+
if (native.started) native.instance.onBars();
|
|
21957
|
+
}
|
|
21958
|
+
/** Start the natives still held for a history that has completed without poking them
|
|
21959
|
+
* (a depth-only switch finishes its trim while the switch gates every poke). */
|
|
21960
|
+
releaseHeldNatives() {
|
|
21961
|
+
if (this.historyState === "backfill") return;
|
|
21962
|
+
for (const record of this.registry.all()) {
|
|
21963
|
+
if (record.native?.heldForHistory && !record.hidden) this.pokeNative(record);
|
|
21964
|
+
}
|
|
21965
|
+
}
|
|
21882
21966
|
/** Gap healing needs a ranged feed and a provider-backed series (offline `data` has no source). */
|
|
21883
21967
|
canHeal() {
|
|
21884
21968
|
return !!this.feed.loadRange && !this.config.market.data?.length;
|
|
@@ -22170,7 +22254,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
22170
22254
|
if (record?.native?.type === "volume") this.volumeOptedOut = true;
|
|
22171
22255
|
if (record) this.settleCode(record, { ok: false, error: new Error(`[vela] updateCode("${id}") \u2014 the indicator was removed.`) });
|
|
22172
22256
|
record?.session?.stop();
|
|
22173
|
-
if (record?.native
|
|
22257
|
+
if (record?.native) this.stopNativeInstance(record.native);
|
|
22174
22258
|
this.handles.delete(id);
|
|
22175
22259
|
if (record?.renderHandle) this.renderer.removeIndicator(record.renderHandle);
|
|
22176
22260
|
const paneId = record?.model?.paneId;
|
|
@@ -22291,7 +22375,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
22291
22375
|
for (const record of this.registry.all()) {
|
|
22292
22376
|
this.settleCode(record, { ok: false, error: new Error("[vela] updateCode \u2014 the chart was destroyed.") });
|
|
22293
22377
|
record.session?.stop();
|
|
22294
|
-
if (record.native
|
|
22378
|
+
if (record.native) this.stopNativeInstance(record.native);
|
|
22295
22379
|
}
|
|
22296
22380
|
for (const engine of this.typeEngines.values()) engine.stop();
|
|
22297
22381
|
this.typeEngines.clear();
|
|
@@ -22418,7 +22502,19 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
22418
22502
|
await this.readyPromise;
|
|
22419
22503
|
const record = this.registry.get(id);
|
|
22420
22504
|
if (!record?.native || record.hidden) return;
|
|
22505
|
+
const native = record.native;
|
|
22506
|
+
if (native.started) return;
|
|
22507
|
+
if (native.descriptor.waitsForHistory && this.historyState === "backfill") {
|
|
22508
|
+
native.heldForHistory = true;
|
|
22509
|
+
this.mountNativePlaceholder(id, record);
|
|
22510
|
+
return;
|
|
22511
|
+
}
|
|
22512
|
+
native.heldForHistory = false;
|
|
22421
22513
|
const channel = this.nativeChannel(record);
|
|
22514
|
+
let alive = true;
|
|
22515
|
+
native.revokeContext = () => {
|
|
22516
|
+
alive = false;
|
|
22517
|
+
};
|
|
22422
22518
|
const ctx = {
|
|
22423
22519
|
id,
|
|
22424
22520
|
symbol: this.config.market.symbol ?? "TEST",
|
|
@@ -22427,14 +22523,18 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
22427
22523
|
session: this.config.market.session,
|
|
22428
22524
|
bars: () => this.bars,
|
|
22429
22525
|
data: this.dataControl,
|
|
22430
|
-
emit: (out) =>
|
|
22431
|
-
|
|
22526
|
+
emit: (out) => {
|
|
22527
|
+
if (alive) this.applyModel(id, this.buildNativeModel(record, out, this.bars[0]?.time));
|
|
22528
|
+
},
|
|
22529
|
+
pushData: (data) => {
|
|
22530
|
+
if (alive) this.renderer.setNativeData?.(channel, data);
|
|
22531
|
+
},
|
|
22432
22532
|
setStatus: (status) => {
|
|
22433
|
-
if (record.renderHandle && !record.hidden) this.renderer.setIndicatorStatus?.(record.renderHandle, status);
|
|
22533
|
+
if (alive && record.renderHandle && !record.hidden) this.renderer.setIndicatorStatus?.(record.renderHandle, status);
|
|
22434
22534
|
}
|
|
22435
22535
|
};
|
|
22436
|
-
|
|
22437
|
-
|
|
22536
|
+
native.instance.start(ctx, record.inputValues);
|
|
22537
|
+
native.started = true;
|
|
22438
22538
|
} catch (err) {
|
|
22439
22539
|
this.fail(id, handle, err);
|
|
22440
22540
|
}
|
|
@@ -22447,8 +22547,14 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
22447
22547
|
const { type, descriptor } = record.native;
|
|
22448
22548
|
return descriptor.multiInstance ? nativeInstanceChannel(type, record.id) : type;
|
|
22449
22549
|
}
|
|
22450
|
-
/**
|
|
22451
|
-
|
|
22550
|
+
/**
|
|
22551
|
+
* Wrap a native indicator's emitted visuals into a full IndicatorModel, tagged `native`.
|
|
22552
|
+
* `anchorTime` is the first bar the output is index-aligned to — the chart's first bar
|
|
22553
|
+
* when it emitted. Without it the renderer pins the output's first slot to whatever the
|
|
22554
|
+
* first bar is NOW, so history landing in front of a computed output (or the oldest
|
|
22555
|
+
* bars trimmed under it) would slide the whole plot by that many bars.
|
|
22556
|
+
*/
|
|
22557
|
+
buildNativeModel(record, out, anchorTime) {
|
|
22452
22558
|
const d = record.native.descriptor;
|
|
22453
22559
|
const type = record.native.type;
|
|
22454
22560
|
const channel = this.nativeChannel(record);
|
|
@@ -22459,6 +22565,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
22459
22565
|
overlay: d.overlay,
|
|
22460
22566
|
paneHint: d.paneHint,
|
|
22461
22567
|
native: { type, ...channel !== type ? { channel } : {} },
|
|
22568
|
+
...anchorTime != null ? { anchorTime } : {},
|
|
22462
22569
|
...d.legend === false ? { legend: false } : {},
|
|
22463
22570
|
...out.paneAxis != null ? { paneAxis: out.paneAxis } : {},
|
|
22464
22571
|
series: out.series ?? [],
|
|
@@ -22513,8 +22620,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
22513
22620
|
const paneId = this.routePane(id, model, record.options ?? {});
|
|
22514
22621
|
this.placeModel(model, id, paneId);
|
|
22515
22622
|
record.model = model;
|
|
22516
|
-
this.ensurePaneFor(paneId);
|
|
22623
|
+
const paneCreated = this.ensurePaneFor(paneId);
|
|
22517
22624
|
record.renderHandle = this.renderer.mountIndicator(model);
|
|
22625
|
+
if (paneCreated) this.events.emit("pane:changed", void 0);
|
|
22518
22626
|
record.pendingStructural = true;
|
|
22519
22627
|
if (record.hidden) {
|
|
22520
22628
|
this.renderer.setIndicatorVisible?.(record.renderHandle, false);
|
|
@@ -22538,24 +22646,49 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
22538
22646
|
const paneId = this.routePane(id, model, record.options ?? {});
|
|
22539
22647
|
this.placeModel(model, id, paneId);
|
|
22540
22648
|
record.model = model;
|
|
22541
|
-
this.ensurePaneFor(paneId);
|
|
22649
|
+
const paneCreated = this.ensurePaneFor(paneId);
|
|
22542
22650
|
record.renderHandle = this.renderer.mountIndicator(model);
|
|
22651
|
+
if (paneCreated) this.events.emit("pane:changed", void 0);
|
|
22543
22652
|
record.pendingStructural = true;
|
|
22544
22653
|
this.renderer.setIndicatorVisible?.(record.renderHandle, false);
|
|
22545
22654
|
this.announce(record, this.handles.get(id));
|
|
22546
22655
|
}
|
|
22656
|
+
/**
|
|
22657
|
+
* The legend row of a native HELD for the history (see `waitsForHistory`): an empty
|
|
22658
|
+
* model with the title and inputs schema, spinning, not announced — exactly a script
|
|
22659
|
+
* indicator's loading placeholder, so `ready`/`indicator:added` and `inspect()` still
|
|
22660
|
+
* mean "it produced output". A record that already has a row (restarted over a new
|
|
22661
|
+
* market, or shown again) keeps it and only spins. The first emit remounts over it.
|
|
22662
|
+
*/
|
|
22663
|
+
mountNativePlaceholder(id, record) {
|
|
22664
|
+
record.pendingStructural = true;
|
|
22665
|
+
if (!record.renderHandle) {
|
|
22666
|
+
const model = this.buildNativeModel(record, {});
|
|
22667
|
+
const paneId = this.routePane(id, model, record.options ?? {});
|
|
22668
|
+
this.placeModel(model, id, paneId);
|
|
22669
|
+
record.model = model;
|
|
22670
|
+
const paneCreated = this.ensurePaneFor(paneId);
|
|
22671
|
+
record.renderHandle = this.renderer.mountIndicator(model);
|
|
22672
|
+
if (paneCreated) this.events.emit("pane:changed", void 0);
|
|
22673
|
+
}
|
|
22674
|
+
this.setLoading(record, true);
|
|
22675
|
+
}
|
|
22547
22676
|
/** Flip the record's loading state and reflect it in the legend row (spinner on/off). */
|
|
22548
22677
|
setLoading(record, loading) {
|
|
22549
22678
|
record.loading = loading;
|
|
22550
22679
|
if (record.renderHandle && !record.hidden) this.renderer.setIndicatorStatus?.(record.renderHandle, loading ? "loading" : "idle");
|
|
22551
22680
|
}
|
|
22681
|
+
/** Make sure the renderer has the pane; true when it is new to the layout (the caller
|
|
22682
|
+
* emits `pane:changed` once the indicator's model sits on it, so `list()` shows it). */
|
|
22552
22683
|
ensurePaneFor(paneId) {
|
|
22553
|
-
|
|
22684
|
+
const created = !this.paneOrder.includes(paneId);
|
|
22685
|
+
if (created) this.paneOrder.push(paneId);
|
|
22554
22686
|
this.renderer.ensurePane({
|
|
22555
22687
|
id: paneId,
|
|
22556
22688
|
kind: paneId === "price" ? "price" : "study",
|
|
22557
22689
|
order: this.paneOrder.indexOf(paneId)
|
|
22558
22690
|
});
|
|
22691
|
+
return created;
|
|
22559
22692
|
}
|
|
22560
22693
|
/** True when any (still-registered) indicator sits on the given pane. */
|
|
22561
22694
|
paneStillUsed(paneId) {
|
|
@@ -22591,12 +22724,14 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
22591
22724
|
const panes = [];
|
|
22592
22725
|
for (const paneId of this.paneOrder) {
|
|
22593
22726
|
if (paneId !== "price" && !byPane.has(paneId)) continue;
|
|
22727
|
+
const heightWeight = this.renderer.paneHeightWeight?.(paneId) ?? null;
|
|
22594
22728
|
panes.push({
|
|
22595
22729
|
id: paneId,
|
|
22596
22730
|
kind: paneId === "price" ? "price" : "study",
|
|
22597
22731
|
order: panes.length,
|
|
22598
22732
|
collapsed: this.collapsedPanes.has(paneId),
|
|
22599
22733
|
maximized: this.maximizedPaneId === paneId,
|
|
22734
|
+
...heightWeight !== null ? { heightWeight } : {},
|
|
22600
22735
|
indicators: byPane.get(paneId) ?? []
|
|
22601
22736
|
});
|
|
22602
22737
|
}
|
|
@@ -22628,6 +22763,22 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
22628
22763
|
this.renderer.setPaneMaximized?.(paneId);
|
|
22629
22764
|
this.events.emit("pane:changed", void 0);
|
|
22630
22765
|
}
|
|
22766
|
+
setPaneHeightWeight(paneId, weight) {
|
|
22767
|
+
if (!this.renderer.setPaneHeightWeight) {
|
|
22768
|
+
console.warn("[vela] the active renderer does not support pane sizing \u2014 chart.panes.setHeightWeight ignored.");
|
|
22769
|
+
return;
|
|
22770
|
+
}
|
|
22771
|
+
if (typeof weight !== "number" || !Number.isFinite(weight) || weight <= 0) {
|
|
22772
|
+
console.warn(`[vela] chart.panes.setHeightWeight: weight must be a positive number (got ${String(weight)}).`);
|
|
22773
|
+
return;
|
|
22774
|
+
}
|
|
22775
|
+
if (!this.existingPaneIds().includes(paneId)) {
|
|
22776
|
+
console.warn(`[vela] chart.panes.setHeightWeight: no pane "${paneId}".`);
|
|
22777
|
+
return;
|
|
22778
|
+
}
|
|
22779
|
+
this.renderer.setPaneHeightWeight(paneId, weight);
|
|
22780
|
+
this.events.emit("pane:changed", void 0);
|
|
22781
|
+
}
|
|
22631
22782
|
/** IndicatorController/PaneController: move (or merge) an indicator to another pane. */
|
|
22632
22783
|
moveIndicator(id, target) {
|
|
22633
22784
|
if (!this.paneManagementSupported() || !this.renderer.setIndicatorPane) {
|
|
@@ -22725,6 +22876,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
22725
22876
|
this.maximizedPaneId = a.maximized ? a.paneId : null;
|
|
22726
22877
|
this.events.emit("pane:changed", void 0);
|
|
22727
22878
|
break;
|
|
22879
|
+
case "resize":
|
|
22880
|
+
this.events.emit("pane:changed", void 0);
|
|
22881
|
+
break;
|
|
22728
22882
|
}
|
|
22729
22883
|
}
|
|
22730
22884
|
/** Fire `indicator:added` + the handle's `ready` once, on the first computed model. */
|
|
@@ -22755,14 +22909,16 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
22755
22909
|
const paneId2 = this.routePane(id, model, record.options ?? {});
|
|
22756
22910
|
this.placeModel(model, id, paneId2);
|
|
22757
22911
|
record.model = model;
|
|
22758
|
-
this.ensurePaneFor(paneId2);
|
|
22912
|
+
const paneCreated = this.ensurePaneFor(paneId2);
|
|
22759
22913
|
record.renderHandle = this.renderer.mountIndicator(model);
|
|
22760
22914
|
record.pendingStructural = false;
|
|
22915
|
+
if (paneCreated) this.events.emit("pane:changed", void 0);
|
|
22761
22916
|
this.announce(record, handle);
|
|
22762
22917
|
return true;
|
|
22763
22918
|
}
|
|
22764
22919
|
let paneId = record.model?.paneId ?? "price";
|
|
22765
22920
|
const prevOwnScale = record.model?.ownScale === true;
|
|
22921
|
+
let rerouted = false;
|
|
22766
22922
|
if (record.loading && !record.paneLocked) {
|
|
22767
22923
|
const routed = this.routePane(id, model, record.options ?? {});
|
|
22768
22924
|
if (routed !== paneId) {
|
|
@@ -22773,6 +22929,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
22773
22929
|
this.ensurePaneFor(routed);
|
|
22774
22930
|
record.pendingStructural = true;
|
|
22775
22931
|
paneId = routed;
|
|
22932
|
+
rerouted = true;
|
|
22776
22933
|
}
|
|
22777
22934
|
}
|
|
22778
22935
|
this.placeModel(model, id, paneId);
|
|
@@ -22785,6 +22942,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
22785
22942
|
} else {
|
|
22786
22943
|
this.renderer.updateIndicator(record.renderHandle, modelToValuePatch(model));
|
|
22787
22944
|
}
|
|
22945
|
+
if (rerouted) this.events.emit("pane:changed", void 0);
|
|
22788
22946
|
if (record.loading) this.setLoading(record, false);
|
|
22789
22947
|
this.announce(record, handle);
|
|
22790
22948
|
return true;
|
|
@@ -22799,13 +22957,16 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
22799
22957
|
const overlay = options.overlay ?? model.overlay;
|
|
22800
22958
|
return overlay ? "price" : `pane-${id}`;
|
|
22801
22959
|
}
|
|
22802
|
-
/** Each named value series' last
|
|
22803
|
-
* engine fills, so this works without an execution context.
|
|
22804
|
-
|
|
22960
|
+
/** Each named value series' value on the chart's last bar — the model is the one source
|
|
22961
|
+
* every engine fills, so this works without an execution context. Points past the last
|
|
22962
|
+
* bar (a series shifted forward) are skipped by time. */
|
|
22963
|
+
static plotsAtLastBar(model, lastBarTime) {
|
|
22805
22964
|
const out = {};
|
|
22806
22965
|
for (const spec of model.series) {
|
|
22807
22966
|
if (!isLineLikeSeries(spec)) continue;
|
|
22808
|
-
|
|
22967
|
+
let i = spec.points.length - 1;
|
|
22968
|
+
if (lastBarTime !== void 0) while (i >= 0 && spec.points[i].time > lastBarTime) i -= 1;
|
|
22969
|
+
out[spec.title] = spec.points[i]?.value ?? null;
|
|
22809
22970
|
}
|
|
22810
22971
|
return out;
|
|
22811
22972
|
}
|
|
@@ -22861,7 +23022,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
22861
23022
|
// whatever produced this run.
|
|
22862
23023
|
forming: this.config.live && this.bars.length > 0,
|
|
22863
23024
|
complete: this.historyState !== "backfill",
|
|
22864
|
-
plots: _EngineOrchestrator.plotsAtLastBar(model),
|
|
23025
|
+
plots: _EngineOrchestrator.plotsAtLastBar(model, lastBar?.time),
|
|
22865
23026
|
vars: snapshot?.variables ?? {},
|
|
22866
23027
|
...snapshot?.strategy ? { strategy: snapshot.strategy } : {},
|
|
22867
23028
|
warnings: snapshot?.warnings ?? [],
|
|
@@ -22930,8 +23091,9 @@ function sameInputValue(a, b) {
|
|
|
22930
23091
|
return typeof a === "object" && a !== null && typeof b === "object" && b !== null && JSON.stringify(a) === JSON.stringify(b);
|
|
22931
23092
|
}
|
|
22932
23093
|
function blankedModel(model) {
|
|
23094
|
+
const { anchorTime: _anchor, ...rest } = model;
|
|
22933
23095
|
return {
|
|
22934
|
-
...
|
|
23096
|
+
...rest,
|
|
22935
23097
|
series: model.series.map((s) => s.kind === "candle" || s.kind === "bar" ? { ...s, bars: [] } : s.kind === "markers" ? { ...s, markers: [] } : { ...s, points: [] }),
|
|
22936
23098
|
fills: [],
|
|
22937
23099
|
backgrounds: [],
|
|
@@ -22954,12 +23116,14 @@ function modelToValuePatch(model) {
|
|
|
22954
23116
|
if (s.kind === "candle" || s.kind === "bar") {
|
|
22955
23117
|
series.push({ seriesId: s.id, kind: "bars", bars: s.bars, barColors: s.barColors ?? [] });
|
|
22956
23118
|
for (const b of s.bars) {
|
|
23119
|
+
if (!b) continue;
|
|
22957
23120
|
if (b.time < from) from = b.time;
|
|
22958
23121
|
if (b.time > to) to = b.time;
|
|
22959
23122
|
}
|
|
22960
23123
|
} else if (isLineLikeSeries(s)) {
|
|
22961
23124
|
series.push({ seriesId: s.id, kind: "points", points: s.points });
|
|
22962
23125
|
for (const p of s.points) {
|
|
23126
|
+
if (!p) continue;
|
|
22963
23127
|
if (p.time < from) from = p.time;
|
|
22964
23128
|
if (p.time > to) to = p.time;
|
|
22965
23129
|
}
|
|
@@ -23257,6 +23421,17 @@ var PanesControl = class {
|
|
|
23257
23421
|
if (this.guard()) this.controller.maximizePane(paneId);
|
|
23258
23422
|
return this;
|
|
23259
23423
|
}
|
|
23424
|
+
/**
|
|
23425
|
+
* Set a pane's height weight: the panes share the plot height in proportion to their
|
|
23426
|
+
* weights, so `setHeightWeight('price', 6)` next to two study panes at 1 gives the price
|
|
23427
|
+
* pane three quarters of the height. The weight sticks until the user drags a separator
|
|
23428
|
+
* or it is set again; a collapsed pane applies it when expanded. `weight` must be a
|
|
23429
|
+
* positive number.
|
|
23430
|
+
*/
|
|
23431
|
+
setHeightWeight(paneId, weight) {
|
|
23432
|
+
if (this.guard()) this.controller.setPaneHeightWeight(paneId, weight);
|
|
23433
|
+
return this;
|
|
23434
|
+
}
|
|
23260
23435
|
/** Move/merge an indicator to a pane — sugar for `handle.moveTo(...)` by id. */
|
|
23261
23436
|
moveIndicator(id, target) {
|
|
23262
23437
|
if (this.guard()) this.controller.moveIndicator(id, target);
|
|
@@ -24529,6 +24704,8 @@ var LEGEND_TITLE_STATUS_GAP_PX = 8;
|
|
|
24529
24704
|
var LEGEND_ACTION_GAP_PX = 6;
|
|
24530
24705
|
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;`;
|
|
24531
24706
|
var LEGEND_ROW_MIN_H = LEGEND_CTL_PX + LEGEND_ROW_PAD_Y * 2;
|
|
24707
|
+
var STACK_RAISE = "stack:raise";
|
|
24708
|
+
var STACK_LOWER = "stack:lower";
|
|
24532
24709
|
var EYE_SVG = iconAt("eye", LEGEND_ICON_PX2);
|
|
24533
24710
|
var EYE_OFF_SVG = iconAt("eye-off", LEGEND_ICON_PX2);
|
|
24534
24711
|
var GEAR_SVG = iconAt("gear", LEGEND_ICON_PX2);
|
|
@@ -24558,6 +24735,10 @@ var InputsUI = class {
|
|
|
24558
24735
|
this.mobileLayout = false;
|
|
24559
24736
|
/** Pane move/merge hook — when set, rows get a "Move to" menu + become drag-to-pane sources. */
|
|
24560
24737
|
this.moveApi = null;
|
|
24738
|
+
/** Overlay restacking hook — when set, a row's menus offer to step it forward or back. */
|
|
24739
|
+
this.stackApi = null;
|
|
24740
|
+
/** What {@link orderRows} last ordered the rows by (skips unchanged frames). */
|
|
24741
|
+
this.rowOrderKey = "";
|
|
24561
24742
|
/** Host-contributed legend actions, resolved PER ROW at render time (see setLegendActions). */
|
|
24562
24743
|
this.legendActions = null;
|
|
24563
24744
|
/** Host-contributed callout bubbles, resolved PER ROW at render time (see setLegendCallouts). */
|
|
@@ -24696,6 +24877,10 @@ var InputsUI = class {
|
|
|
24696
24877
|
setMoveApi(api) {
|
|
24697
24878
|
this.moveApi = api;
|
|
24698
24879
|
}
|
|
24880
|
+
/** Enable the context menu's "Bring in front of …" entry for overlay indicators. Null disables it. */
|
|
24881
|
+
setStackApi(api) {
|
|
24882
|
+
this.stackApi = api;
|
|
24883
|
+
}
|
|
24699
24884
|
/**
|
|
24700
24885
|
* Wire the host-contributed row actions. Re-calling replaces the provider and
|
|
24701
24886
|
* re-projects the rows already on screen (a late `registerLegendAction` appears after
|
|
@@ -24841,16 +25026,20 @@ var InputsUI = class {
|
|
|
24841
25026
|
}
|
|
24842
25027
|
this.moveTargets = items.map((it) => it.target);
|
|
24843
25028
|
this.moveMenuId = id;
|
|
24844
|
-
const
|
|
25029
|
+
const stack = this.stackItems(id);
|
|
25030
|
+
const moves = items.map((it, i) => ({ id: String(i), label: it.label }));
|
|
25031
|
+
if (moves[0] && stack.length > 0) moves[0].separatorBefore = true;
|
|
25032
|
+
const descriptors = [...stack, ...moves];
|
|
24845
25033
|
if (!this.moveMenu) {
|
|
24846
25034
|
this.moveMenu = new Menu({
|
|
24847
25035
|
host: this.container,
|
|
24848
25036
|
items: descriptors,
|
|
24849
25037
|
placement: "bottom-start",
|
|
24850
25038
|
onSelect: (itemId) => {
|
|
24851
|
-
const target = this.moveTargets[Number(itemId)];
|
|
24852
25039
|
const rowId = this.moveMenuId;
|
|
24853
|
-
if (
|
|
25040
|
+
if (!rowId || this.runStackItem(itemId, rowId)) return;
|
|
25041
|
+
const target = this.moveTargets[Number(itemId)];
|
|
25042
|
+
if (target) api.move(rowId, target);
|
|
24854
25043
|
}
|
|
24855
25044
|
});
|
|
24856
25045
|
} else {
|
|
@@ -24863,18 +25052,24 @@ var InputsUI = class {
|
|
|
24863
25052
|
this.moveMenu?.close();
|
|
24864
25053
|
}
|
|
24865
25054
|
// ── legend row context menu (right-click) ───────────────────────────────
|
|
24866
|
-
/** Open the row's action menu at the pointer
|
|
25055
|
+
/** Open the row's action menu at the pointer: an overlay indicator's restacking steps
|
|
25056
|
+
* first (where a layer is there to pass), then the values toggle. */
|
|
24867
25057
|
openRowMenu(id, x, y) {
|
|
24868
25058
|
this.closeRowMenu();
|
|
24869
25059
|
this.closeMoveMenu();
|
|
24870
25060
|
const row2 = this.rows.get(id);
|
|
24871
25061
|
if (!row2) return;
|
|
24872
25062
|
this.rowMenuId = id;
|
|
24873
|
-
const
|
|
24874
|
-
|
|
24875
|
-
|
|
24876
|
-
|
|
24877
|
-
|
|
25063
|
+
const stack = this.stackItems(id);
|
|
25064
|
+
const items = [
|
|
25065
|
+
...stack,
|
|
25066
|
+
{
|
|
25067
|
+
id: "values",
|
|
25068
|
+
label: "Indicator values",
|
|
25069
|
+
checked: this.rowValuesShown(row2),
|
|
25070
|
+
separatorBefore: stack.length > 0
|
|
25071
|
+
}
|
|
25072
|
+
];
|
|
24878
25073
|
if (!this.rowMenu) {
|
|
24879
25074
|
this.rowMenu = new Menu({
|
|
24880
25075
|
host: this.container,
|
|
@@ -24883,8 +25078,10 @@ var InputsUI = class {
|
|
|
24883
25078
|
// Right-click action menu — checked state reads as a leading ✓ (see the kit Menu).
|
|
24884
25079
|
checkmarks: true,
|
|
24885
25080
|
onSelect: (itemId) => {
|
|
24886
|
-
|
|
24887
|
-
|
|
25081
|
+
const rowId = this.rowMenuId;
|
|
25082
|
+
if (!rowId || this.runStackItem(itemId, rowId)) return;
|
|
25083
|
+
if (itemId !== "values") return;
|
|
25084
|
+
const target = this.rows.get(rowId);
|
|
24888
25085
|
if (!target) return;
|
|
24889
25086
|
target.showValues = !this.rowValuesShown(target);
|
|
24890
25087
|
this.renderRowValues(target);
|
|
@@ -24898,6 +25095,55 @@ var InputsUI = class {
|
|
|
24898
25095
|
closeRowMenu() {
|
|
24899
25096
|
this.rowMenu?.close();
|
|
24900
25097
|
}
|
|
25098
|
+
/** A row's one-step restacking entries (forward, then back), for whichever neighbor exists. */
|
|
25099
|
+
stackItems(id) {
|
|
25100
|
+
const api = this.stackApi;
|
|
25101
|
+
if (!api) return [];
|
|
25102
|
+
const items = [];
|
|
25103
|
+
const forward = this.stackLabel("Bring in front of", api.above(id), "the indicator in front");
|
|
25104
|
+
if (forward) items.push({ id: STACK_RAISE, label: forward });
|
|
25105
|
+
const back = this.stackLabel("Send behind", api.below(id), "the indicator behind");
|
|
25106
|
+
if (back) items.push({ id: STACK_LOWER, label: back });
|
|
25107
|
+
return items;
|
|
25108
|
+
}
|
|
25109
|
+
stackLabel(verb, neighbor, fallback) {
|
|
25110
|
+
if (neighbor === null) return null;
|
|
25111
|
+
if (neighbor === "price") return `${verb} price`;
|
|
25112
|
+
return `${verb} ${this.rows.get(neighbor.indicator)?.title ?? fallback}`;
|
|
25113
|
+
}
|
|
25114
|
+
/** Run a restacking entry; false when `itemId` is not one. */
|
|
25115
|
+
runStackItem(itemId, rowId) {
|
|
25116
|
+
if (itemId === STACK_RAISE) this.stackApi?.raise(rowId);
|
|
25117
|
+
else if (itemId === STACK_LOWER) this.stackApi?.lower(rowId);
|
|
25118
|
+
else return false;
|
|
25119
|
+
return true;
|
|
25120
|
+
}
|
|
25121
|
+
/**
|
|
25122
|
+
* List each pane's rows front-most first, the way the indicators stack (the object
|
|
25123
|
+
* tree's order). `backToFront` is the paint order; rows it doesn't name keep their
|
|
25124
|
+
* place after the others. Cheap to call every frame: no-op while nothing changed.
|
|
25125
|
+
*/
|
|
25126
|
+
orderRows(backToFront) {
|
|
25127
|
+
const key = `${backToFront.join(",")}|${[...this.rows.values()].map((r) => `${r.id}@${r.paneId}`).join(",")}`;
|
|
25128
|
+
if (key === this.rowOrderKey) return;
|
|
25129
|
+
this.rowOrderKey = key;
|
|
25130
|
+
const rank = new Map(backToFront.map((id, i) => [id, i]));
|
|
25131
|
+
for (const lg of this.legends.values()) {
|
|
25132
|
+
const els = [...this.rows.values()].filter((r) => r.el.parentElement === lg).map((r) => r.el);
|
|
25133
|
+
const dom = [...lg.children];
|
|
25134
|
+
const ofEl = new Map([...this.rows.values()].map((r) => [r.el, r.id]));
|
|
25135
|
+
const at = (el) => rank.get(ofEl.get(el)) ?? -1;
|
|
25136
|
+
els.sort((a, b) => {
|
|
25137
|
+
const ra = at(a);
|
|
25138
|
+
const rb = at(b);
|
|
25139
|
+
if (ra >= 0 && rb >= 0) return rb - ra;
|
|
25140
|
+
if (ra >= 0 || rb >= 0) return ra >= 0 ? -1 : 1;
|
|
25141
|
+
return dom.indexOf(a) - dom.indexOf(b);
|
|
25142
|
+
});
|
|
25143
|
+
const anchor = this.foldToggle?.parentElement === lg ? this.foldToggle : null;
|
|
25144
|
+
for (const el of els) lg.insertBefore(el, anchor);
|
|
25145
|
+
}
|
|
25146
|
+
}
|
|
24901
25147
|
/** Move a legend row to another pane (indicator merged/moved), tidying an emptied container. */
|
|
24902
25148
|
setPane(id, paneId) {
|
|
24903
25149
|
const row2 = this.rows.get(id);
|
|
@@ -26237,6 +26483,7 @@ var WebGL2Backend = class {
|
|
|
26237
26483
|
const i0 = Math.max(0, Math.floor(vr.from));
|
|
26238
26484
|
const i1 = Math.min(n - 1, Math.ceil(vr.to));
|
|
26239
26485
|
if (i1 < i0) return;
|
|
26486
|
+
const s1 = Math.max(i1, Math.ceil(vr.to));
|
|
26240
26487
|
gl.enable(gl.BLEND);
|
|
26241
26488
|
gl.enable(gl.SCISSOR_TEST);
|
|
26242
26489
|
const barColorMap = mergeBarColors(scene.indicators);
|
|
@@ -26295,8 +26542,8 @@ var WebGL2Backend = class {
|
|
|
26295
26542
|
b.alpha = this.modelAlpha;
|
|
26296
26543
|
const off = scene.offsetOf(m.id);
|
|
26297
26544
|
const mp = effPane(m);
|
|
26298
|
-
for (const f of m.fills) if (f.overlay !== true) this.emitFill(b, m, f, mp, coords, i0,
|
|
26299
|
-
for (const s of m.series) if (s.overlay !== true) this.emitSeries(b, s, mp, coords, i0,
|
|
26545
|
+
for (const f of m.fills) if (f.overlay !== true) this.emitFill(b, m, f, mp, coords, i0, s1, off);
|
|
26546
|
+
for (const s of m.series) if (s.overlay !== true) this.emitSeries(b, s, mp, coords, i0, s1, theme, off);
|
|
26300
26547
|
}
|
|
26301
26548
|
if (drawCandles && !candleDrawn) {
|
|
26302
26549
|
drawSlicesUpTo(scene.candleZ);
|
|
@@ -26307,11 +26554,11 @@ var WebGL2Backend = class {
|
|
|
26307
26554
|
if (isPrice) {
|
|
26308
26555
|
for (const m of scene.indicators.values()) {
|
|
26309
26556
|
const off = scene.offsetOf(m.id);
|
|
26310
|
-
for (const f of m.fills) if (f.overlay === true) this.emitFill(b, m, f, pane, coords, i0,
|
|
26557
|
+
for (const f of m.fills) if (f.overlay === true) this.emitFill(b, m, f, pane, coords, i0, s1, off);
|
|
26311
26558
|
}
|
|
26312
26559
|
for (const m of scene.indicators.values()) {
|
|
26313
26560
|
const off = scene.offsetOf(m.id);
|
|
26314
|
-
for (const s of m.series) if (s.overlay === true) this.emitSeries(b, s, pane, coords, i0,
|
|
26561
|
+
for (const s of m.series) if (s.overlay === true) this.emitSeries(b, s, pane, coords, i0, s1, theme, off);
|
|
26315
26562
|
}
|
|
26316
26563
|
}
|
|
26317
26564
|
drawSlicesUpTo(Infinity);
|
|
@@ -26333,7 +26580,7 @@ var WebGL2Backend = class {
|
|
|
26333
26580
|
if (this.glow > 0 && !this.screenProgram && !this.ensureScreenPipeline()) this.glow = 0;
|
|
26334
26581
|
if (this.glow > 0 && this.screenProgram) {
|
|
26335
26582
|
this.glowBatch.alpha = this.modelAlpha;
|
|
26336
|
-
this.renderGlow(gl, scene, coords, i0,
|
|
26583
|
+
this.renderGlow(gl, scene, coords, i0, s1);
|
|
26337
26584
|
}
|
|
26338
26585
|
}
|
|
26339
26586
|
/**
|
|
@@ -26890,7 +27137,7 @@ var WebGL2Backend = class {
|
|
|
26890
27137
|
const tickW = Math.max(1, Math.round(spacing * 0.35));
|
|
26891
27138
|
for (let i = i0; i <= i1; i += 1) {
|
|
26892
27139
|
const bar = s.bars[i - off];
|
|
26893
|
-
if (!bar) continue;
|
|
27140
|
+
if (!bar || !Number.isFinite(bar.open + bar.high + bar.low + bar.close)) continue;
|
|
26894
27141
|
const bc = s.barColors?.[i - off];
|
|
26895
27142
|
const body = parseColor(bc?.color ?? (bar.close >= bar.open ? up : down));
|
|
26896
27143
|
const x = Math.round(coords.logicalToX(i));
|
|
@@ -27543,6 +27790,8 @@ var InputController = class {
|
|
|
27543
27790
|
const tapRelease = this.dragging && !this.moved && (!wasTouch || Math.hypot(x - this.startX, y - this.startY) <= TOUCH_TAP_SLOP);
|
|
27544
27791
|
if (this.dragging && this.region === "drawing") {
|
|
27545
27792
|
this.deps.drawingsPointerUp?.(x, y, this.snapMode(e));
|
|
27793
|
+
} else if (this.dragging && this.region === "separator") {
|
|
27794
|
+
this.deps.endPaneResize?.();
|
|
27546
27795
|
} else if (tapRelease && this.region === "data") {
|
|
27547
27796
|
this.deps.onClick(x, y);
|
|
27548
27797
|
} else if (this.dragging && this.region === "data" && wasTouch) {
|
|
@@ -27564,6 +27813,7 @@ var InputController = class {
|
|
|
27564
27813
|
this.cancelLongPress();
|
|
27565
27814
|
if (!this.dragging) return;
|
|
27566
27815
|
if (this.region === "drawing" && !Number.isNaN(this.cursorX)) this.deps.drawingsPointerUp?.(this.cursorX, this.cursorY, this.snapMode(e));
|
|
27816
|
+
if (this.region === "separator") this.deps.endPaneResize?.();
|
|
27567
27817
|
if (this.region === "crosshair" || e.pointerType === "touch") this.deps.onPointerMove(null, null);
|
|
27568
27818
|
this.endGesture(e);
|
|
27569
27819
|
};
|
|
@@ -28240,9 +28490,12 @@ var SceneGraph = class {
|
|
|
28240
28490
|
setIndicatorZ(id, z) {
|
|
28241
28491
|
this.seriesZ.set(id, z);
|
|
28242
28492
|
}
|
|
28243
|
-
/** Snapshot of the current ordering for a UI/read API: `{ id, z }`
|
|
28493
|
+
/** Snapshot of the current ordering for a UI/read API: `{ id, z }` back to front, in the
|
|
28494
|
+
* order the indicators paint — on a shared key, mount order (see `orderedIndicatorsForPane`). */
|
|
28244
28495
|
indicatorZOrder() {
|
|
28245
|
-
|
|
28496
|
+
const mounted = new Map([...this.indicators.keys()].map((id, i) => [id, i]));
|
|
28497
|
+
const at = (id) => mounted.get(id) ?? Number.MAX_SAFE_INTEGER;
|
|
28498
|
+
return [...this.seriesZ.entries()].map(([id, z]) => ({ id, z })).sort((a, b) => a.z - b.z || at(a.id) - at(b.id));
|
|
28246
28499
|
}
|
|
28247
28500
|
/** The pane's series z keys (each indicator, plus the candles on the price pane), sorted
|
|
28248
28501
|
* ascending and de-duplicated — the boundaries a user drawing's z is slotted against. */
|
|
@@ -28270,16 +28523,19 @@ var SceneGraph = class {
|
|
|
28270
28523
|
for (const z of this.seriesZ.values()) if (z < min) min = z;
|
|
28271
28524
|
return min;
|
|
28272
28525
|
}
|
|
28526
|
+
/** Create the pane, or update an existing one in place. Without `heightWeight` an existing
|
|
28527
|
+
* pane keeps its weight (a user resize survives other indicators mounting onto it) and a
|
|
28528
|
+
* new one gets the default: 3 for the price pane, 1 for a study pane. */
|
|
28273
28529
|
ensurePane(id, kind, order, heightWeight) {
|
|
28274
28530
|
this.orderedCache = null;
|
|
28275
28531
|
const existing = this.panes.get(id);
|
|
28276
28532
|
if (existing) {
|
|
28277
28533
|
existing.order = order;
|
|
28278
|
-
existing.heightWeight = heightWeight;
|
|
28534
|
+
if (heightWeight !== void 0) existing.heightWeight = heightWeight;
|
|
28279
28535
|
existing.kind = kind;
|
|
28280
28536
|
return existing;
|
|
28281
28537
|
}
|
|
28282
|
-
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 };
|
|
28538
|
+
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 };
|
|
28283
28539
|
this.panes.set(id, pane);
|
|
28284
28540
|
return pane;
|
|
28285
28541
|
}
|
|
@@ -28336,6 +28592,7 @@ var Canvas2dBackend = class {
|
|
|
28336
28592
|
const i0 = Math.max(0, Math.floor(vr.from));
|
|
28337
28593
|
const i1 = Math.min(n - 1, Math.ceil(vr.to));
|
|
28338
28594
|
if (i1 < i0) return;
|
|
28595
|
+
const s1 = Math.max(i1, Math.ceil(vr.to));
|
|
28339
28596
|
const barColorMap = mergeBarColors2(scene.indicators);
|
|
28340
28597
|
const panes = scene.orderedPanes();
|
|
28341
28598
|
for (const pane of panes) {
|
|
@@ -28378,8 +28635,8 @@ var Canvas2dBackend = class {
|
|
|
28378
28635
|
ctx.globalAlpha = this.modelAlpha;
|
|
28379
28636
|
const off = scene.offsetOf(m.id);
|
|
28380
28637
|
const mp = effPane(m);
|
|
28381
|
-
for (const f of m.fills) if (f.overlay !== true) this.drawFill(ctx, m, f, mp, coords, i0,
|
|
28382
|
-
for (const s of m.series) if (s.overlay !== true) this.drawSeries(ctx, s, mp, coords, i0,
|
|
28638
|
+
for (const f of m.fills) if (f.overlay !== true) this.drawFill(ctx, m, f, mp, coords, i0, s1, off);
|
|
28639
|
+
for (const s of m.series) if (s.overlay !== true) this.drawSeries(ctx, s, mp, coords, i0, s1, theme, off);
|
|
28383
28640
|
}
|
|
28384
28641
|
if (drawCandles && !candleDrawn) {
|
|
28385
28642
|
drawSlicesUpTo(scene.candleZ);
|
|
@@ -28390,11 +28647,11 @@ var Canvas2dBackend = class {
|
|
|
28390
28647
|
ctx.globalAlpha = this.modelAlpha;
|
|
28391
28648
|
for (const m of scene.indicators.values()) {
|
|
28392
28649
|
const off = scene.offsetOf(m.id);
|
|
28393
|
-
for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0,
|
|
28650
|
+
for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0, s1, off);
|
|
28394
28651
|
}
|
|
28395
28652
|
for (const m of scene.indicators.values()) {
|
|
28396
28653
|
const off = scene.offsetOf(m.id);
|
|
28397
|
-
for (const s of m.series) if (s.overlay === true) this.drawSeries(ctx, s, pane, coords, i0,
|
|
28654
|
+
for (const s of m.series) if (s.overlay === true) this.drawSeries(ctx, s, pane, coords, i0, s1, theme, off);
|
|
28398
28655
|
}
|
|
28399
28656
|
}
|
|
28400
28657
|
drawSlicesUpTo(Infinity);
|
|
@@ -28846,7 +29103,7 @@ var Canvas2dBackend = class {
|
|
|
28846
29103
|
const tickW = Math.max(1, Math.round(spacing * 0.35));
|
|
28847
29104
|
for (let i = i0; i <= i1; i += 1) {
|
|
28848
29105
|
const b = s.bars[i - off];
|
|
28849
|
-
if (!b) continue;
|
|
29106
|
+
if (!b || !Number.isFinite(b.open + b.high + b.low + b.close)) continue;
|
|
28850
29107
|
const bc = s.barColors?.[i - off];
|
|
28851
29108
|
const body = bc?.color ?? (b.close >= b.open ? up : down);
|
|
28852
29109
|
const x = Math.round(coords.logicalToX(i)) + 0.5;
|
|
@@ -38976,6 +39233,58 @@ function stackLayers(entries, candleZ) {
|
|
|
38976
39233
|
return { below, above };
|
|
38977
39234
|
}
|
|
38978
39235
|
|
|
39236
|
+
// src/renderers/native/core/overlayStack.ts
|
|
39237
|
+
function priceStack(indicators, candleZ) {
|
|
39238
|
+
const out = [...indicators];
|
|
39239
|
+
const at = out.findIndex((e) => e.z >= candleZ);
|
|
39240
|
+
out.splice(at < 0 ? out.length : at, 0, { id: null, z: candleZ });
|
|
39241
|
+
return out;
|
|
39242
|
+
}
|
|
39243
|
+
function untie(stack) {
|
|
39244
|
+
const out = [];
|
|
39245
|
+
for (let i = 0; i < stack.length; ) {
|
|
39246
|
+
let j = i + 1;
|
|
39247
|
+
while (j < stack.length && stack[j].z === stack[i].z) j += 1;
|
|
39248
|
+
const run = j - i;
|
|
39249
|
+
if (run > 1) {
|
|
39250
|
+
const base = stack[i].z;
|
|
39251
|
+
const step = ((stack[j]?.z ?? base + 1) - base) / run;
|
|
39252
|
+
for (let k = 1; k < run; k += 1) {
|
|
39253
|
+
const id = stack[i + k].id;
|
|
39254
|
+
if (id !== null) out.push({ id, z: base + k * step });
|
|
39255
|
+
}
|
|
39256
|
+
}
|
|
39257
|
+
i = j;
|
|
39258
|
+
}
|
|
39259
|
+
return out;
|
|
39260
|
+
}
|
|
39261
|
+
function layerAbove(stack, id) {
|
|
39262
|
+
const i = stack.findIndex((e) => e.id === id);
|
|
39263
|
+
if (i < 0 || i === stack.length - 1) return null;
|
|
39264
|
+
const above = stack[i + 1];
|
|
39265
|
+
return above.id === null ? "price" : { indicator: above.id };
|
|
39266
|
+
}
|
|
39267
|
+
function layerBelow(stack, id) {
|
|
39268
|
+
const i = stack.findIndex((e) => e.id === id);
|
|
39269
|
+
if (i <= 0) return null;
|
|
39270
|
+
const below = stack[i - 1];
|
|
39271
|
+
return below.id === null ? "price" : { indicator: below.id };
|
|
39272
|
+
}
|
|
39273
|
+
function raisedZ(stack, id) {
|
|
39274
|
+
const i = stack.findIndex((e) => e.id === id);
|
|
39275
|
+
if (i < 0 || i === stack.length - 1) return null;
|
|
39276
|
+
const above = stack[i + 1];
|
|
39277
|
+
const next = stack[i + 2];
|
|
39278
|
+
return next ? (above.z + next.z) / 2 : above.z + 1;
|
|
39279
|
+
}
|
|
39280
|
+
function loweredZ(stack, id) {
|
|
39281
|
+
const i = stack.findIndex((e) => e.id === id);
|
|
39282
|
+
if (i <= 0) return null;
|
|
39283
|
+
const below = stack[i - 1];
|
|
39284
|
+
const prev2 = stack[i - 2];
|
|
39285
|
+
return prev2 ? (prev2.z + below.z) / 2 : below.z - 1;
|
|
39286
|
+
}
|
|
39287
|
+
|
|
38979
39288
|
// src/renderers/shared/dom-raster.ts
|
|
38980
39289
|
function hasInk(color) {
|
|
38981
39290
|
if (!color || color === "transparent") return false;
|
|
@@ -39503,6 +39812,8 @@ var NativeRenderer = class {
|
|
|
39503
39812
|
// pane just below it
|
|
39504
39813
|
this.resizeSplitStart = null;
|
|
39505
39814
|
// the two panes' shared span when the drag began
|
|
39815
|
+
this.resizeStartAboveWeight = 0;
|
|
39816
|
+
// the upper pane's weight when the drag began
|
|
39506
39817
|
/** The user drawings take part in pointer input (the runtime `drawingsInteractive` feature). */
|
|
39507
39818
|
this.drawingsInteractive = true;
|
|
39508
39819
|
this.hoverSeparatorY = null;
|
|
@@ -40145,6 +40456,7 @@ var NativeRenderer = class {
|
|
|
40145
40456
|
};
|
|
40146
40457
|
this.scene.candleZ = next.stacking.candles;
|
|
40147
40458
|
for (const [id, z] of Object.entries(next.stacking.series)) this.scene.setIndicatorZ(id, z);
|
|
40459
|
+
this.syncLegendOrder();
|
|
40148
40460
|
s.bars = { upColor: next.bars.upColor, downColor: next.bars.downColor };
|
|
40149
40461
|
s.line = { color: next.line.color, width: next.line.width };
|
|
40150
40462
|
s.area = { lineColor: next.area.lineColor, width: next.area.width, topColor: next.area.topColor, bottomColor: next.area.bottomColor };
|
|
@@ -40190,6 +40502,7 @@ var NativeRenderer = class {
|
|
|
40190
40502
|
if (typeof v.z === "number") this.scene.setIndicatorZ(v.id, v.z);
|
|
40191
40503
|
else if (v.to === "front") this.scene.bringIndicatorToFront(v.id);
|
|
40192
40504
|
else if (v.to === "back") this.scene.sendIndicatorToBack(v.id);
|
|
40505
|
+
this.syncLegendOrder();
|
|
40193
40506
|
}
|
|
40194
40507
|
/** Toggle the in-chart settings gear + dialog (the `settings` feature). The gear
|
|
40195
40508
|
* sits above the time axis, clear of the right price-axis strip. */
|
|
@@ -40577,7 +40890,7 @@ var NativeRenderer = class {
|
|
|
40577
40890
|
this.chrome.mount(this.chromeCanvas);
|
|
40578
40891
|
this.crosshairLayer.mount(this.cursorCanvas);
|
|
40579
40892
|
this.coords.setViewport(defaultViewport());
|
|
40580
|
-
this.scene.ensurePane(PRICE_PANE_ID2, "price", 0
|
|
40893
|
+
this.scene.ensurePane(PRICE_PANE_ID2, "price", 0);
|
|
40581
40894
|
this.scheduler = new Scheduler((level) => this.renderFrame(level));
|
|
40582
40895
|
this.animator = new Animator((dt) => this.animTick(dt));
|
|
40583
40896
|
this.targetBarSpacing = this.coords.getViewport().barSpacing;
|
|
@@ -40590,7 +40903,7 @@ var NativeRenderer = class {
|
|
|
40590
40903
|
onClick: (x, y) => {
|
|
40591
40904
|
if (this.handleMarkClick(x, y)) return;
|
|
40592
40905
|
this.userDrawings?.deselect();
|
|
40593
|
-
this.handleClick(x);
|
|
40906
|
+
this.handleClick(x, y);
|
|
40594
40907
|
},
|
|
40595
40908
|
onAxisLongPress: (axis, x, y) => {
|
|
40596
40909
|
for (const cb of this.axisLongPressCbs) cb({ axis, x, y });
|
|
@@ -40604,6 +40917,7 @@ var NativeRenderer = class {
|
|
|
40604
40917
|
paneSeparatorAt: (y) => this.paneSeparatorAt(y),
|
|
40605
40918
|
beginPaneResize: (y) => this.beginPaneResize(y),
|
|
40606
40919
|
paneResizeBy: (dy) => this.paneResizeBy(dy),
|
|
40920
|
+
endPaneResize: () => this.endPaneResize(),
|
|
40607
40921
|
resetPaneSize: (y) => this.resetPaneSize(y),
|
|
40608
40922
|
resetView: () => this.resetView(),
|
|
40609
40923
|
// User drawings claim a gesture before pan when armed / over a drawing.
|
|
@@ -40729,6 +41043,12 @@ var NativeRenderer = class {
|
|
|
40729
41043
|
for (const cb of this.moveIndicatorCbs) cb(id, target);
|
|
40730
41044
|
}
|
|
40731
41045
|
});
|
|
41046
|
+
this.inputsUI.setStackApi({
|
|
41047
|
+
above: (id) => layerAbove(this.overlayStack(), id),
|
|
41048
|
+
below: (id) => layerBelow(this.overlayStack(), id),
|
|
41049
|
+
raise: (id) => this.restackOverlay(id, raisedZ),
|
|
41050
|
+
lower: (id) => this.restackOverlay(id, loweredZ)
|
|
41051
|
+
});
|
|
40732
41052
|
this.paneControls = new PaneControls(this.plot, theme, {
|
|
40733
41053
|
panes: () => this.paneControlViews(),
|
|
40734
41054
|
rightAxis: () => this.rightAxisW,
|
|
@@ -41046,7 +41366,7 @@ var NativeRenderer = class {
|
|
|
41046
41366
|
}
|
|
41047
41367
|
// ── panes ──
|
|
41048
41368
|
ensurePane(pane) {
|
|
41049
|
-
this.scene.ensurePane(pane.id, pane.kind, pane.order, pane.heightWeight
|
|
41369
|
+
this.scene.ensurePane(pane.id, pane.kind, pane.order, pane.heightWeight);
|
|
41050
41370
|
this.layoutPanes();
|
|
41051
41371
|
this.paneControls?.refresh();
|
|
41052
41372
|
this.scheduler.invalidate(4 /* Full */);
|
|
@@ -41068,6 +41388,7 @@ var NativeRenderer = class {
|
|
|
41068
41388
|
model.ownScale = opts?.ownScale === true;
|
|
41069
41389
|
if (!model.ownScale) this.scene.dropIndicatorScale(handle.id);
|
|
41070
41390
|
this.inputsUI.setPane(handle.id, paneId);
|
|
41391
|
+
this.syncLegendOrder();
|
|
41071
41392
|
this.refreshAnchorOffset(model);
|
|
41072
41393
|
this.refreshAxisWidth();
|
|
41073
41394
|
this.layoutPanes();
|
|
@@ -41095,6 +41416,15 @@ var NativeRenderer = class {
|
|
|
41095
41416
|
this.paneControls?.refresh();
|
|
41096
41417
|
this.scheduler.invalidate(4 /* Full */);
|
|
41097
41418
|
}
|
|
41419
|
+
setPaneHeightWeight(paneId, weight) {
|
|
41420
|
+
const pane = this.scene.panes.get(paneId);
|
|
41421
|
+
if (!pane || !(weight > 0) || pane.heightWeight === weight) return;
|
|
41422
|
+
pane.heightWeight = weight;
|
|
41423
|
+
this.afterPaneLayoutChange();
|
|
41424
|
+
}
|
|
41425
|
+
paneHeightWeight(paneId) {
|
|
41426
|
+
return this.scene.panes.get(paneId)?.heightWeight ?? null;
|
|
41427
|
+
}
|
|
41098
41428
|
onPaneAction(cb) {
|
|
41099
41429
|
this.paneActionCbs.add(cb);
|
|
41100
41430
|
return () => this.paneActionCbs.delete(cb);
|
|
@@ -41189,6 +41519,7 @@ var NativeRenderer = class {
|
|
|
41189
41519
|
this.vpvrHidden = false;
|
|
41190
41520
|
}
|
|
41191
41521
|
if (model.ownScale) this.refreshAxisWidth();
|
|
41522
|
+
this.syncLegendOrder();
|
|
41192
41523
|
this.paneControls?.refresh();
|
|
41193
41524
|
this.scheduler.invalidate(4 /* Full */);
|
|
41194
41525
|
return { id: model.id };
|
|
@@ -41596,7 +41927,7 @@ var NativeRenderer = class {
|
|
|
41596
41927
|
this.setMarkHover(null);
|
|
41597
41928
|
this.scheduler.invalidate(1 /* Cursor */);
|
|
41598
41929
|
this.hoverLogical = null;
|
|
41599
|
-
const empty = { time: null, price: null, paneKind: null, values: /* @__PURE__ */ new Map(), ohlc: null };
|
|
41930
|
+
const empty = { time: null, price: null, paneKind: null, paneId: null, values: /* @__PURE__ */ new Map(), ohlc: null };
|
|
41600
41931
|
for (const cb of this.crosshairCbs) cb(empty);
|
|
41601
41932
|
return;
|
|
41602
41933
|
}
|
|
@@ -41627,14 +41958,23 @@ var NativeRenderer = class {
|
|
|
41627
41958
|
const bar = onBar ? this.bars[logical] : void 0;
|
|
41628
41959
|
const ohlc = bar ? { time: bar.time, open: bar.open, high: bar.high, low: bar.low, close: bar.close, volume: bar.volume } : null;
|
|
41629
41960
|
this.hoverLogical = onBar ? logical : null;
|
|
41630
|
-
const
|
|
41961
|
+
const onPane = inData && pane ? pane : null;
|
|
41962
|
+
const event = { time, price, paneKind: onPane?.kind ?? null, paneId: onPane?.id ?? null, values, ohlc };
|
|
41631
41963
|
for (const cb of this.crosshairCbs) cb(event);
|
|
41632
41964
|
}
|
|
41633
|
-
handleClick(x) {
|
|
41965
|
+
handleClick(x, y) {
|
|
41634
41966
|
if (this.coords.barCount === 0) return;
|
|
41635
41967
|
const logical = Math.round(this.coords.xToLogical(x));
|
|
41636
41968
|
const onBar = logical >= 0 && logical < this.coords.barCount;
|
|
41637
|
-
|
|
41969
|
+
const inData = x >= 0 && y >= 0 && x <= this.coords.width && y <= this.coords.height;
|
|
41970
|
+
const pane = inData ? this.paneNodeAtY(y) : null;
|
|
41971
|
+
const event = {
|
|
41972
|
+
time: onBar ? this.coords.logicalToTime(logical) : null,
|
|
41973
|
+
price: pane ? this.coords.yToPrice(y, pane.scale, pane.bounds) : null,
|
|
41974
|
+
paneKind: pane?.kind ?? null,
|
|
41975
|
+
paneId: pane?.id ?? null
|
|
41976
|
+
};
|
|
41977
|
+
for (const cb of this.clickCbs) cb(event);
|
|
41638
41978
|
}
|
|
41639
41979
|
/**
|
|
41640
41980
|
* Fan out (or collapse, with null) a multi-group mark stack; repaints the chrome tier when
|
|
@@ -41879,6 +42219,7 @@ var NativeRenderer = class {
|
|
|
41879
42219
|
const below = panes[i];
|
|
41880
42220
|
this.resizeAbove = above;
|
|
41881
42221
|
this.resizeBelow = below;
|
|
42222
|
+
this.resizeStartAboveWeight = above.heightWeight;
|
|
41882
42223
|
this.resizeSplitStart = {
|
|
41883
42224
|
combinedTop: above.bounds.top,
|
|
41884
42225
|
combinedHeight: above.bounds.height + below.bounds.height,
|
|
@@ -41899,6 +42240,16 @@ var NativeRenderer = class {
|
|
|
41899
42240
|
this.layoutPanes();
|
|
41900
42241
|
this.scheduler.invalidate(4 /* Full */);
|
|
41901
42242
|
}
|
|
42243
|
+
/** Release the separator: report the resize once, at the end of the drag, if it changed anything. */
|
|
42244
|
+
endPaneResize() {
|
|
42245
|
+
const above = this.resizeAbove;
|
|
42246
|
+
const below = this.resizeBelow;
|
|
42247
|
+
const startAbove = this.resizeStartAboveWeight;
|
|
42248
|
+
this.resizeAbove = null;
|
|
42249
|
+
this.resizeBelow = null;
|
|
42250
|
+
this.resizeSplitStart = null;
|
|
42251
|
+
if (above && below && above.heightWeight !== startAbove) this.emitPaneAction({ type: "resize", paneIds: [above.id, below.id] });
|
|
42252
|
+
}
|
|
41902
42253
|
/** Double-click a separator → split the two adjacent panes evenly (each gets half of
|
|
41903
42254
|
* their combined weight). A simple, predictable reset that leaves siblings untouched. */
|
|
41904
42255
|
resetPaneSize(y) {
|
|
@@ -41912,6 +42263,7 @@ var NativeRenderer = class {
|
|
|
41912
42263
|
below.heightWeight = half;
|
|
41913
42264
|
this.layoutPanes();
|
|
41914
42265
|
this.scheduler.invalidate(4 /* Full */);
|
|
42266
|
+
this.emitPaneAction({ type: "resize", paneIds: [above.id, below.id] });
|
|
41915
42267
|
}
|
|
41916
42268
|
// ── keyboard navigation / accessibility (item 11) ──
|
|
41917
42269
|
/** Enable/disable keyboard control: focusability + ARIA + key listeners + live region. */
|
|
@@ -42166,6 +42518,7 @@ var NativeRenderer = class {
|
|
|
42166
42518
|
/** Paint the below-data (L-1) + geometry (L0) + chrome (L1) layers from the current scene/coords. */
|
|
42167
42519
|
paintData() {
|
|
42168
42520
|
this.syncLayerCanvasOrder();
|
|
42521
|
+
this.syncLegendOrder();
|
|
42169
42522
|
this.stampScaleInvert();
|
|
42170
42523
|
this.backend.modelAlpha = this.modelAlpha;
|
|
42171
42524
|
this.backend.candleBodyAlpha = this.candleBodyAlpha;
|
|
@@ -42347,6 +42700,7 @@ var NativeRenderer = class {
|
|
|
42347
42700
|
const i0 = Math.max(0, Math.floor(vr.from));
|
|
42348
42701
|
const i1 = Math.min(n - 1, Math.ceil(vr.to));
|
|
42349
42702
|
if (i1 < i0) return;
|
|
42703
|
+
const s1 = Math.max(i1, Math.ceil(vr.to));
|
|
42350
42704
|
this.scene.percentBaseline = this.bars[i0]?.close ?? 0;
|
|
42351
42705
|
const panes = this.scene.orderedPanes();
|
|
42352
42706
|
const pricePane = panes.find((p) => p.kind === "price") ?? null;
|
|
@@ -42364,11 +42718,11 @@ var NativeRenderer = class {
|
|
|
42364
42718
|
const masterModels = models.filter((m) => m.ownScale !== true);
|
|
42365
42719
|
let dr = this.chrome.paneDrawingsRange(masterModels, this.scene, pane === pricePane, vr);
|
|
42366
42720
|
if (pane === pricePane) {
|
|
42367
|
-
const or = overlaySeriesRange(this.scene.indicators.values(), i0,
|
|
42721
|
+
const or = overlaySeriesRange(this.scene.indicators.values(), i0, s1, (id) => this.scene.offsetOf(id));
|
|
42368
42722
|
if (or) dr = dr ? { min: Math.min(dr.min, or.min), max: Math.max(dr.max, or.max) } : or;
|
|
42369
42723
|
}
|
|
42370
42724
|
const includeCandles = pane.kind === "price" && (!this.scene.candlesHidden || this.priceLayersAnchoredToBars(masterModels) || !this.paneHasMeasurableContent(masterModels, dr));
|
|
42371
|
-
pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0,
|
|
42725
|
+
pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, s1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id), margins);
|
|
42372
42726
|
pane.percentBaseline = pane.kind === "price" ? this.bars[i0]?.close ?? 0 : this.firstVisibleValue(masterModels, i0);
|
|
42373
42727
|
pane.axisFormat = void 0;
|
|
42374
42728
|
pane.axisBands = void 0;
|
|
@@ -42408,7 +42762,7 @@ var NativeRenderer = class {
|
|
|
42408
42762
|
continue;
|
|
42409
42763
|
}
|
|
42410
42764
|
const mdr = this.chrome.paneDrawingsRange([model], this.scene, false, vr);
|
|
42411
|
-
sl.scaleTarget = computePaneScale([model], this.bars, false, i0,
|
|
42765
|
+
sl.scaleTarget = computePaneScale([model], this.bars, false, i0, s1, mdr, false, (id) => this.scene.offsetOf(id), margins);
|
|
42412
42766
|
if (!animating || !sl.initialized) {
|
|
42413
42767
|
sl.scale = { ...sl.scaleTarget };
|
|
42414
42768
|
sl.initialized = true;
|
|
@@ -42479,6 +42833,30 @@ var NativeRenderer = class {
|
|
|
42479
42833
|
const master = models.find((m) => m.ownScale !== true) ?? models[0];
|
|
42480
42834
|
return master?.title || null;
|
|
42481
42835
|
}
|
|
42836
|
+
/**
|
|
42837
|
+
* The price pane's layers back to front, as the legend restacks them: every indicator
|
|
42838
|
+
* with a legend row, in paint order, plus the candles. The same layers and order the
|
|
42839
|
+
* object tree lists (see `indicatorZOrder`).
|
|
42840
|
+
*/
|
|
42841
|
+
overlayStack() {
|
|
42842
|
+
const painted = this.scene.orderedIndicatorsForPane(PRICE_PANE_ID2).filter((m) => m.legend !== false).map((m) => ({ id: m.id, z: this.scene.zOf(m.id) }));
|
|
42843
|
+
return priceStack(painted, this.scene.candleZ);
|
|
42844
|
+
}
|
|
42845
|
+
/** List the legend rows the way the indicators stack, front-most first (no-op when unchanged). */
|
|
42846
|
+
syncLegendOrder() {
|
|
42847
|
+
this.inputsUI?.orderRows(this.scene.indicatorZOrder().map((e) => e.id));
|
|
42848
|
+
}
|
|
42849
|
+
/** Apply a legend restacking step: shared keys are spread apart first (the order stays),
|
|
42850
|
+
* so the step lands exactly one layer over. No-op when there is nothing to step over. */
|
|
42851
|
+
restackOverlay(id, step) {
|
|
42852
|
+
for (const fix of untie(this.overlayStack())) this.scene.setIndicatorZ(fix.id, fix.z);
|
|
42853
|
+
const z = step(this.overlayStack(), id);
|
|
42854
|
+
if (z === null) return;
|
|
42855
|
+
this.scene.setIndicatorZ(id, z);
|
|
42856
|
+
this.syncLegendOrder();
|
|
42857
|
+
this.scheduler.invalidate(4 /* Full */);
|
|
42858
|
+
for (const cb of this.configChangedCbs) cb();
|
|
42859
|
+
}
|
|
42482
42860
|
/** The pane a bespoke-layer native indicator (volume/vpvr) currently lives in (null = not mounted). */
|
|
42483
42861
|
nativeLayerPane(type) {
|
|
42484
42862
|
for (const m of this.scene.indicators.values()) {
|
|
@@ -42933,17 +43311,35 @@ function leadingPointsBefore(model, headTime) {
|
|
|
42933
43311
|
for (const s of model.series) {
|
|
42934
43312
|
const times = s.kind === "candle" || s.kind === "bar" ? s.bars : isLineLikeSeries(s) ? s.points : null;
|
|
42935
43313
|
if (!times || times.length === 0) continue;
|
|
42936
|
-
|
|
42937
|
-
|
|
42938
|
-
while (lo < hi) {
|
|
42939
|
-
const mid = lo + hi >> 1;
|
|
42940
|
-
if (times[mid].time < headTime) lo = mid + 1;
|
|
42941
|
-
else hi = mid;
|
|
42942
|
-
}
|
|
42943
|
-
return lo;
|
|
43314
|
+
const count = slotsBefore(times, headTime);
|
|
43315
|
+
if (count !== null) return count;
|
|
42944
43316
|
}
|
|
42945
43317
|
return null;
|
|
42946
43318
|
}
|
|
43319
|
+
function slotsBefore(times, headTime) {
|
|
43320
|
+
let lo = 0;
|
|
43321
|
+
let hi = times.length;
|
|
43322
|
+
while (lo < hi) {
|
|
43323
|
+
const mid = lo + hi >> 1;
|
|
43324
|
+
const t = times[mid]?.time;
|
|
43325
|
+
if (t === void 0) return slotsBeforeScan(times, headTime);
|
|
43326
|
+
if (t < headTime) lo = mid + 1;
|
|
43327
|
+
else hi = mid;
|
|
43328
|
+
}
|
|
43329
|
+
return lo;
|
|
43330
|
+
}
|
|
43331
|
+
function slotsBeforeScan(times, headTime) {
|
|
43332
|
+
let lastBefore = -1;
|
|
43333
|
+
let any = false;
|
|
43334
|
+
for (let i = 0; i < times.length; i += 1) {
|
|
43335
|
+
const t = times[i]?.time;
|
|
43336
|
+
if (t === void 0) continue;
|
|
43337
|
+
any = true;
|
|
43338
|
+
if (t >= headTime) return t === headTime ? i : lastBefore + 1;
|
|
43339
|
+
lastBefore = i;
|
|
43340
|
+
}
|
|
43341
|
+
return any ? times.length : null;
|
|
43342
|
+
}
|
|
42947
43343
|
function applyPatch(model, patch) {
|
|
42948
43344
|
if (patch.kind === "value") {
|
|
42949
43345
|
for (const delta of patch.series) {
|
|
@@ -43327,6 +43723,9 @@ function classicDescriptor(spec) {
|
|
|
43327
43723
|
overlay: spec.overlay,
|
|
43328
43724
|
// Studies stack (a 20 and a 200 moving average side by side) — unlike the layer-backed natives.
|
|
43329
43725
|
multiInstance: true,
|
|
43726
|
+
// A study's values depend on every bar before them (an EMA's seed, an RSI's
|
|
43727
|
+
// smoothing), so it computes once over the whole history, as script indicators do.
|
|
43728
|
+
waitsForHistory: true,
|
|
43330
43729
|
inputsSchema: () => spec.inputs,
|
|
43331
43730
|
defaultInputs: () => Object.fromEntries(spec.inputs.map((i) => [i.key, i.defval])),
|
|
43332
43731
|
create: () => new ClassicIndicator(spec)
|
|
@@ -44162,41 +44561,50 @@ var maEnvelope = {
|
|
|
44162
44561
|
return directionalChannel(bars, basis, map(basis, (x) => x * (1 + k)), map(basis, (x) => x * (1 - k)), ink, "envelope");
|
|
44163
44562
|
}
|
|
44164
44563
|
};
|
|
44165
|
-
var
|
|
44166
|
-
|
|
44167
|
-
|
|
44168
|
-
|
|
44169
|
-
|
|
44170
|
-
|
|
44171
|
-
|
|
44564
|
+
var leastSquaresMa = {
|
|
44565
|
+
type: "lsma",
|
|
44566
|
+
title: "Least Squares Moving Average",
|
|
44567
|
+
shortTitle: "LSMA",
|
|
44568
|
+
overlay: true,
|
|
44569
|
+
inputs: [
|
|
44570
|
+
sourceInput("Close", "source", "Source", "Price series the regression line is fitted to."),
|
|
44571
|
+
{ ...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 },
|
|
44572
|
+
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."),
|
|
44573
|
+
...directionInputs()
|
|
44574
|
+
],
|
|
44575
|
+
compute: (bars, inputs) => {
|
|
44576
|
+
const src = sourceValues(bars, str3(inputs, "source", "Close"));
|
|
44577
|
+
const values = linreg(src, num3(inputs, "length", 25), Math.trunc(num3(inputs, "offset", 0)));
|
|
44578
|
+
return { plots: [slopeColoredPlot("lsma", "LSMA", values, inputs)] };
|
|
44579
|
+
}
|
|
44580
|
+
};
|
|
44581
|
+
var linearRegressionSlope = {
|
|
44582
|
+
type: "linear-regression-slope",
|
|
44583
|
+
title: "Linear Regression Slope",
|
|
44584
|
+
shortTitle: "LRS",
|
|
44172
44585
|
overlay: false,
|
|
44173
44586
|
inputs: [
|
|
44174
|
-
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."),
|
|
44175
44587
|
{ ...lengthInput(100, "length", "Length", 5e3, "Number of bars used to fit the linear regression."), min: 2 },
|
|
44176
44588
|
sourceInput("Close", "source", "Source", "Price series the regression is fitted to."),
|
|
44177
|
-
colorInput(BULLISH, "Rising", "bullColor", "Color of the
|
|
44178
|
-
colorInput(BEARISH, "Falling", "bearColor", "Color of the
|
|
44179
|
-
colorInput(NEUTRAL, "
|
|
44589
|
+
colorInput(BULLISH, "Rising", "bullColor", "Color of the slope line while the fit slopes upward."),
|
|
44590
|
+
colorInput(BEARISH, "Falling", "bearColor", "Color of the slope line while the fit slopes downward."),
|
|
44591
|
+
colorInput(NEUTRAL, "Slope Line", "lineColor", "Color of the slope line while the fit is flat.")
|
|
44180
44592
|
],
|
|
44181
44593
|
compute: (bars, inputs) => {
|
|
44182
44594
|
const len = num3(inputs, "length", 100);
|
|
44183
44595
|
const src = sourceValues(bars, str3(inputs, "source", "Close"));
|
|
44184
|
-
const
|
|
44185
|
-
const slope = zip(curve, linreg(src, len, 1), (a, b) => a - b);
|
|
44596
|
+
const slope = zip(linreg(src, len, 0), linreg(src, len, 1), (a, b) => a - b);
|
|
44186
44597
|
const bull = str3(inputs, "bullColor", BULLISH);
|
|
44187
44598
|
const bear = str3(inputs, "bearColor", BEARISH);
|
|
44188
44599
|
const flat = str3(inputs, "lineColor", NEUTRAL);
|
|
44189
44600
|
const colors = slope.map((s) => Number.isFinite(s) ? s > 0 ? bull : s < 0 ? bear : flat : null);
|
|
44190
|
-
|
|
44191
|
-
|
|
44192
|
-
|
|
44193
|
-
|
|
44194
|
-
};
|
|
44195
|
-
}
|
|
44196
|
-
return { plots: [{ key: "curve", title: "Curve", values: curve, color: flat, width: 2, colors, overlay: true }] };
|
|
44601
|
+
return {
|
|
44602
|
+
plots: [{ key: "slope", title: "Slope", values: slope, color: flat, width: 2, colors }],
|
|
44603
|
+
levels: [{ key: "zero", price: 0, color: transp(NEUTRAL, 50), lineStyle: "dashed" }]
|
|
44604
|
+
};
|
|
44197
44605
|
}
|
|
44198
44606
|
};
|
|
44199
|
-
var averageSpecs = [simpleMa, exponentialMa, movingAverage, smoothedMa, zlema, vidya, maEnvelope,
|
|
44607
|
+
var averageSpecs = [simpleMa, exponentialMa, movingAverage, smoothedMa, zlema, vidya, maEnvelope, leastSquaresMa, linearRegressionSlope];
|
|
44200
44608
|
|
|
44201
44609
|
// src/core/native-indicators/classics/bands.ts
|
|
44202
44610
|
var JAW_COLOR = "#ab47bc";
|
|
@@ -48457,7 +48865,7 @@ var CellControls = class {
|
|
|
48457
48865
|
// src/widget/context-menu.ts
|
|
48458
48866
|
var PRICE_AXIS_W = 60;
|
|
48459
48867
|
var TIME_AXIS_H3 = 26;
|
|
48460
|
-
var OFF_PLOT = { price: null, time: null, paneKind: null };
|
|
48868
|
+
var OFF_PLOT = { price: null, time: null, paneKind: null, paneId: null };
|
|
48461
48869
|
var ChartContextMenu = class {
|
|
48462
48870
|
constructor(host, cbs) {
|
|
48463
48871
|
this.cbs = cbs;
|
|
@@ -48500,7 +48908,7 @@ var ChartContextMenu = class {
|
|
|
48500
48908
|
this.chart = chart;
|
|
48501
48909
|
this.crosshair = OFF_PLOT;
|
|
48502
48910
|
this.offCrosshair = chart.renderer.onCrosshairMove((e) => {
|
|
48503
|
-
this.crosshair = { price: e.price, time: e.time, paneKind: e.paneKind ?? null };
|
|
48911
|
+
this.crosshair = { price: e.price, time: e.time, paneKind: e.paneKind ?? null, paneId: e.paneId ?? null };
|
|
48504
48912
|
});
|
|
48505
48913
|
}
|
|
48506
48914
|
destroy() {
|