@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/workspace.cjs
CHANGED
|
@@ -13499,9 +13499,14 @@ function paneItems(snap, pane, units) {
|
|
|
13499
13499
|
return [...unitItems, ...rows2];
|
|
13500
13500
|
}
|
|
13501
13501
|
const zOf = new Map(snap.zOrder.map((e) => [e.id, e.z]));
|
|
13502
|
-
const
|
|
13503
|
-
|
|
13504
|
-
|
|
13502
|
+
const rank = new Map(snap.zOrder.map((e, i) => [e.id, i]));
|
|
13503
|
+
const rows = pane.indicators.map((i) => ({
|
|
13504
|
+
item: { kind: "row", row: indicatorRow(i) },
|
|
13505
|
+
z: zOf.get(i.id) ?? 0,
|
|
13506
|
+
rank: rank.get(i.id) ?? -1
|
|
13507
|
+
}));
|
|
13508
|
+
if (pane.kind === "price") rows.push({ item: { kind: "row", row: priceRow() }, z: snap.candleZ, rank: -Infinity });
|
|
13509
|
+
rows.sort((a, b) => b.z - a.z || b.rank - a.rank);
|
|
13505
13510
|
if (!snap.interleave) return [...unitItems, ...rows.map((r) => r.item)];
|
|
13506
13511
|
const entries = [...rows, ...units.map((unit) => ({ item: { kind: "unit", unit }, z: unitZ(unit) }))];
|
|
13507
13512
|
entries.sort((a, b) => a.z === b.z ? Number(a.item.kind === "unit") - Number(b.item.kind === "unit") : b.z - a.z);
|
|
@@ -14177,6 +14182,7 @@ var ObjectTree = class extends SidePanel {
|
|
|
14177
14182
|
for (const ev of REFRESH_EVENTS) {
|
|
14178
14183
|
this.unsubs.push(chart.on(ev, () => this.refresh()));
|
|
14179
14184
|
}
|
|
14185
|
+
this.unsubs.push(chart.renderer.onConfigChanged(() => this.refresh()));
|
|
14180
14186
|
this.unsubs.push(
|
|
14181
14187
|
chart.on("drawing:selected", ({ ids }) => {
|
|
14182
14188
|
this.selectedDrawings = new Set(ids);
|
|
@@ -19750,6 +19756,20 @@ function sanitizeDrawingTools(raw) {
|
|
|
19750
19756
|
if (typeof r.stay === "boolean") out.stay = r.stay;
|
|
19751
19757
|
return Object.keys(out).length > 0 ? out : null;
|
|
19752
19758
|
}
|
|
19759
|
+
function successorNative(entry) {
|
|
19760
|
+
const type = typeof entry === "string" ? entry : entry.type;
|
|
19761
|
+
if (type !== "linear-regression") return entry;
|
|
19762
|
+
const hidden = typeof entry === "object" && entry.hidden === true;
|
|
19763
|
+
const { mode, lineColor, ...rest } = typeof entry === "object" ? entry.inputs ?? {} : {};
|
|
19764
|
+
const slope = mode === "Slope";
|
|
19765
|
+
const inputs = slope ? { ...rest, ...lineColor !== void 0 ? { lineColor } : {} } : (
|
|
19766
|
+
// Saved deltas omit the curve's 100-bar default, which the LSMA's default is not.
|
|
19767
|
+
{ length: 100, ...rest, ...lineColor !== void 0 ? { neutralColor: lineColor } : {} }
|
|
19768
|
+
);
|
|
19769
|
+
const next = slope ? "linear-regression-slope" : "lsma";
|
|
19770
|
+
const hasInputs = Object.keys(inputs).length > 0;
|
|
19771
|
+
return hasInputs || hidden ? { type: next, ...hasInputs ? { inputs } : {}, ...hidden ? { hidden } : {} } : next;
|
|
19772
|
+
}
|
|
19753
19773
|
function sanitizeCell(raw) {
|
|
19754
19774
|
if (raw == null || typeof raw !== "object") return null;
|
|
19755
19775
|
const c = raw;
|
|
@@ -19789,7 +19809,7 @@ function sanitizeCell(raw) {
|
|
|
19789
19809
|
return [inputs || hidden ? { type: e.type, ...inputs ? { inputs } : {}, ...hidden ? { hidden } : {} } : e.type];
|
|
19790
19810
|
}
|
|
19791
19811
|
return [];
|
|
19792
|
-
}) : [];
|
|
19812
|
+
}).map(successorNative) : [];
|
|
19793
19813
|
out.indicators = { manifest, natives };
|
|
19794
19814
|
}
|
|
19795
19815
|
const ext = sanitizeExt(c.ext);
|
|
@@ -20581,6 +20601,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
20581
20601
|
this.barTransform = null;
|
|
20582
20602
|
// ── chart-type data engines (SDK): one lazily-created engine per style id ──
|
|
20583
20603
|
this.typeEngines = /* @__PURE__ */ new Map();
|
|
20604
|
+
/** Engines suspended by the chart — their host pushes are dropped until they resume. */
|
|
20605
|
+
this.suspendedEngines = /* @__PURE__ */ new WeakSet();
|
|
20584
20606
|
/** Last-seen chart-type settings per type id (dialog edits + persisted restores) —
|
|
20585
20607
|
* replayed into a data engine whenever one is (re)created. */
|
|
20586
20608
|
this.typeSettings = /* @__PURE__ */ new Map();
|
|
@@ -20855,12 +20877,14 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
20855
20877
|
this.mergeHistoryIntoReplay(bars);
|
|
20856
20878
|
return;
|
|
20857
20879
|
}
|
|
20858
|
-
|
|
20880
|
+
const repaint = painted;
|
|
20881
|
+
this.setBarSeries(bars, repaint ? { preserveView: true } : void 0);
|
|
20859
20882
|
if (!painted && bars.length > 0) {
|
|
20860
20883
|
painted = true;
|
|
20861
20884
|
if (opts.firstLoad) this.activateBarLayers();
|
|
20862
20885
|
if (!final) this.historyState = "backfill";
|
|
20863
20886
|
}
|
|
20887
|
+
if (repaint && !final) this.notifySessionsBars("backfill");
|
|
20864
20888
|
};
|
|
20865
20889
|
const abort = new AbortController();
|
|
20866
20890
|
this.progressiveAbort = abort;
|
|
@@ -21063,10 +21087,15 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
21063
21087
|
this.setBarSeries([], { clearing: true });
|
|
21064
21088
|
this.blankIndicatorVisuals();
|
|
21065
21089
|
if (this.activeEngineStyle) {
|
|
21066
|
-
this.typeEngines.get(this.activeEngineStyle)
|
|
21090
|
+
this.suspendTypeEngine(this.typeEngines.get(this.activeEngineStyle));
|
|
21067
21091
|
this.renderer.setNativeData?.(this.activeEngineStyle, void 0);
|
|
21068
21092
|
this.renderer.setNativeData?.(`${this.activeEngineStyle}-pending`, []);
|
|
21069
21093
|
}
|
|
21094
|
+
for (const record of this.registry.all()) {
|
|
21095
|
+
if (!record.native?.started) continue;
|
|
21096
|
+
this.stopNativeInstance(record.native);
|
|
21097
|
+
this.renderer.setNativeData?.(this.nativeChannel(record), void 0);
|
|
21098
|
+
}
|
|
21070
21099
|
}
|
|
21071
21100
|
if (depthOnly) {
|
|
21072
21101
|
this.extendDepth(gen, m.bars ?? 500);
|
|
@@ -21093,6 +21122,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
21093
21122
|
this.restartChartTypeEngine();
|
|
21094
21123
|
this.reexecuteIndicators();
|
|
21095
21124
|
}
|
|
21125
|
+
if (depthOnly) this.releaseHeldNatives();
|
|
21096
21126
|
if (!replayBack) this.startLive();
|
|
21097
21127
|
if (identityChanged) {
|
|
21098
21128
|
this.events.emit("market:changed", { symbol: m.symbol ?? "TEST", timeframe: m.timeframe ?? "60", prev: prev2 });
|
|
@@ -21109,9 +21139,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
21109
21139
|
restartNativeIndicators() {
|
|
21110
21140
|
for (const record of this.registry.all()) {
|
|
21111
21141
|
if (!record.native) continue;
|
|
21112
|
-
|
|
21142
|
+
this.stopNativeInstance(record.native);
|
|
21113
21143
|
record.native.instance = record.native.descriptor.create();
|
|
21114
|
-
record.native.started = false;
|
|
21115
21144
|
record.pendingStructural = true;
|
|
21116
21145
|
if (record.hidden) {
|
|
21117
21146
|
record.native.stale = true;
|
|
@@ -21128,6 +21157,21 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
21128
21157
|
this.typeEngines.clear();
|
|
21129
21158
|
if (this.activeEngineStyle) void this.startChartTypeEngine(this.activeEngineStyle);
|
|
21130
21159
|
}
|
|
21160
|
+
/** Suspend a chart-type engine. Its host drops every push until the engine resumes:
|
|
21161
|
+
* work it left in flight lands in its own state, never on the scene. */
|
|
21162
|
+
suspendTypeEngine(engine) {
|
|
21163
|
+
if (!engine) return;
|
|
21164
|
+
engine.suspend();
|
|
21165
|
+
this.suspendedEngines.add(engine);
|
|
21166
|
+
}
|
|
21167
|
+
/** Stop a running native instance, then revoke its context: the teardown inside `stop()`
|
|
21168
|
+
* still reaches its layer, but work it left in flight (a fetch resolving later) does not. */
|
|
21169
|
+
stopNativeInstance(native) {
|
|
21170
|
+
if (native.started) native.instance.stop();
|
|
21171
|
+
native.started = false;
|
|
21172
|
+
native.revokeContext?.();
|
|
21173
|
+
native.revokeContext = void 0;
|
|
21174
|
+
}
|
|
21131
21175
|
/**
|
|
21132
21176
|
* The FIRST candles just painted (the quick preview, or the only load): activate the
|
|
21133
21177
|
* layers that need bars but NOT full history. The volume layer reads the chart's bars
|
|
@@ -21660,7 +21704,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
21660
21704
|
const def = chartType(id);
|
|
21661
21705
|
const want = def?.dataEngine ? id : null;
|
|
21662
21706
|
if (want === this.activeEngineStyle) return;
|
|
21663
|
-
if (this.activeEngineStyle) this.typeEngines.get(this.activeEngineStyle)
|
|
21707
|
+
if (this.activeEngineStyle) this.suspendTypeEngine(this.typeEngines.get(this.activeEngineStyle));
|
|
21664
21708
|
this.activeEngineStyle = want;
|
|
21665
21709
|
if (want) void this.startChartTypeEngine(want);
|
|
21666
21710
|
}
|
|
@@ -21671,6 +21715,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
21671
21715
|
if (this.activeEngineStyle !== id) return;
|
|
21672
21716
|
const existing = this.typeEngines.get(id);
|
|
21673
21717
|
if (existing) {
|
|
21718
|
+
this.suspendedEngines.delete(existing);
|
|
21674
21719
|
existing.resume();
|
|
21675
21720
|
return;
|
|
21676
21721
|
}
|
|
@@ -21680,6 +21725,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
21680
21725
|
this.typeEngines.set(id, engine);
|
|
21681
21726
|
const stored = this.typeSettings.get(id);
|
|
21682
21727
|
if (stored) engine.onSettings?.(stored);
|
|
21728
|
+
const reachesScene = () => this.typeEngines.get(id) === engine && !this.suspendedEngines.has(engine);
|
|
21683
21729
|
engine.start({
|
|
21684
21730
|
symbol: this.config.market.symbol ?? "TEST",
|
|
21685
21731
|
timeframe: this.config.market.timeframe ?? "60",
|
|
@@ -21687,8 +21733,12 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
21687
21733
|
session: this.config.market.session,
|
|
21688
21734
|
bars: () => this.bars,
|
|
21689
21735
|
data: this.dataControl,
|
|
21690
|
-
pushData: (data) =>
|
|
21691
|
-
|
|
21736
|
+
pushData: (data) => {
|
|
21737
|
+
if (reachesScene()) this.renderer.setNativeData?.(id, data);
|
|
21738
|
+
},
|
|
21739
|
+
pushPending: (ranges) => {
|
|
21740
|
+
if (reachesScene()) this.renderer.setNativeData?.(`${id}-pending`, ranges);
|
|
21741
|
+
}
|
|
21692
21742
|
});
|
|
21693
21743
|
}
|
|
21694
21744
|
/**
|
|
@@ -21696,8 +21746,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
21696
21746
|
* a pre-load style change is picked up by init's `setBarSeries`. With bars loaded, the
|
|
21697
21747
|
* view is rebuilt in place (viewport preserved) and every Pine indicator re-executes:
|
|
21698
21748
|
* its input data changed wholesale, which a streaming session's incremental context
|
|
21699
|
-
* cannot absorb.
|
|
21700
|
-
*
|
|
21749
|
+
* cannot absorb. Natives computed from `ctx.bars()` re-run over the new candles (a
|
|
21750
|
+
* synthetic price style is their source too); layer natives that read the renderer's
|
|
21751
|
+
* bars per frame ignore the poke.
|
|
21701
21752
|
*/
|
|
21702
21753
|
syncBarTransform(style) {
|
|
21703
21754
|
const transform = barTransformFor(style);
|
|
@@ -21707,6 +21758,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
21707
21758
|
this.bars = transform ? transform.full(this.rawBars) : this.rawBars;
|
|
21708
21759
|
this.renderer.setBars(this.bars, { preserveView: true });
|
|
21709
21760
|
this.reexecuteIndicators();
|
|
21761
|
+
if (this.switchingMarket) return;
|
|
21762
|
+
for (const record of this.registry.all()) {
|
|
21763
|
+
if (record.native?.started && record.renderHandle && !record.hidden) this.pokeNative(record);
|
|
21764
|
+
}
|
|
21710
21765
|
}
|
|
21711
21766
|
/**
|
|
21712
21767
|
* Blank every mounted indicator's painted output — series data, fills, backgrounds,
|
|
@@ -21807,10 +21862,39 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
21807
21862
|
if (record.session) {
|
|
21808
21863
|
record.pendingCause = cause;
|
|
21809
21864
|
record.session.notifyBars(reason);
|
|
21810
|
-
} else if (record.native)
|
|
21865
|
+
} else if (record.native) this.pokeNative(record);
|
|
21811
21866
|
}
|
|
21812
21867
|
if (this.activeEngineStyle) this.typeEngines.get(this.activeEngineStyle)?.onBars?.();
|
|
21813
21868
|
}
|
|
21869
|
+
/**
|
|
21870
|
+
* Tell a native its bars changed. A `waitsForHistory` native ignores every change while
|
|
21871
|
+
* the history is still loading — a backfill chunk, a deeper snapshot, a tick arriving
|
|
21872
|
+
* meanwhile — and its output stays on its bars through the anchor its emit carried.
|
|
21873
|
+
* Once the history is complete, a held one starts and a running one re-runs.
|
|
21874
|
+
*/
|
|
21875
|
+
pokeNative(record) {
|
|
21876
|
+
const native = record.native;
|
|
21877
|
+
if (!native.descriptor.waitsForHistory) {
|
|
21878
|
+
native.instance.onBars();
|
|
21879
|
+
return;
|
|
21880
|
+
}
|
|
21881
|
+
if (this.historyState === "backfill") return;
|
|
21882
|
+
if (native.heldForHistory) {
|
|
21883
|
+
native.heldForHistory = false;
|
|
21884
|
+
const handle = this.handles.get(record.id);
|
|
21885
|
+
if (handle) void this.startNativeIndicator(record.id, handle);
|
|
21886
|
+
return;
|
|
21887
|
+
}
|
|
21888
|
+
if (native.started) native.instance.onBars();
|
|
21889
|
+
}
|
|
21890
|
+
/** Start the natives still held for a history that has completed without poking them
|
|
21891
|
+
* (a depth-only switch finishes its trim while the switch gates every poke). */
|
|
21892
|
+
releaseHeldNatives() {
|
|
21893
|
+
if (this.historyState === "backfill") return;
|
|
21894
|
+
for (const record of this.registry.all()) {
|
|
21895
|
+
if (record.native?.heldForHistory && !record.hidden) this.pokeNative(record);
|
|
21896
|
+
}
|
|
21897
|
+
}
|
|
21814
21898
|
/** Gap healing needs a ranged feed and a provider-backed series (offline `data` has no source). */
|
|
21815
21899
|
canHeal() {
|
|
21816
21900
|
return !!this.feed.loadRange && !this.config.market.data?.length;
|
|
@@ -22102,7 +22186,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
22102
22186
|
if (record?.native?.type === "volume") this.volumeOptedOut = true;
|
|
22103
22187
|
if (record) this.settleCode(record, { ok: false, error: new Error(`[vela] updateCode("${id}") \u2014 the indicator was removed.`) });
|
|
22104
22188
|
record?.session?.stop();
|
|
22105
|
-
if (record?.native
|
|
22189
|
+
if (record?.native) this.stopNativeInstance(record.native);
|
|
22106
22190
|
this.handles.delete(id);
|
|
22107
22191
|
if (record?.renderHandle) this.renderer.removeIndicator(record.renderHandle);
|
|
22108
22192
|
const paneId = record?.model?.paneId;
|
|
@@ -22223,7 +22307,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
22223
22307
|
for (const record of this.registry.all()) {
|
|
22224
22308
|
this.settleCode(record, { ok: false, error: new Error("[vela] updateCode \u2014 the chart was destroyed.") });
|
|
22225
22309
|
record.session?.stop();
|
|
22226
|
-
if (record.native
|
|
22310
|
+
if (record.native) this.stopNativeInstance(record.native);
|
|
22227
22311
|
}
|
|
22228
22312
|
for (const engine of this.typeEngines.values()) engine.stop();
|
|
22229
22313
|
this.typeEngines.clear();
|
|
@@ -22350,7 +22434,19 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
22350
22434
|
await this.readyPromise;
|
|
22351
22435
|
const record = this.registry.get(id);
|
|
22352
22436
|
if (!record?.native || record.hidden) return;
|
|
22437
|
+
const native = record.native;
|
|
22438
|
+
if (native.started) return;
|
|
22439
|
+
if (native.descriptor.waitsForHistory && this.historyState === "backfill") {
|
|
22440
|
+
native.heldForHistory = true;
|
|
22441
|
+
this.mountNativePlaceholder(id, record);
|
|
22442
|
+
return;
|
|
22443
|
+
}
|
|
22444
|
+
native.heldForHistory = false;
|
|
22353
22445
|
const channel = this.nativeChannel(record);
|
|
22446
|
+
let alive = true;
|
|
22447
|
+
native.revokeContext = () => {
|
|
22448
|
+
alive = false;
|
|
22449
|
+
};
|
|
22354
22450
|
const ctx = {
|
|
22355
22451
|
id,
|
|
22356
22452
|
symbol: this.config.market.symbol ?? "TEST",
|
|
@@ -22359,14 +22455,18 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
22359
22455
|
session: this.config.market.session,
|
|
22360
22456
|
bars: () => this.bars,
|
|
22361
22457
|
data: this.dataControl,
|
|
22362
|
-
emit: (out) =>
|
|
22363
|
-
|
|
22458
|
+
emit: (out) => {
|
|
22459
|
+
if (alive) this.applyModel(id, this.buildNativeModel(record, out, this.bars[0]?.time));
|
|
22460
|
+
},
|
|
22461
|
+
pushData: (data) => {
|
|
22462
|
+
if (alive) this.renderer.setNativeData?.(channel, data);
|
|
22463
|
+
},
|
|
22364
22464
|
setStatus: (status) => {
|
|
22365
|
-
if (record.renderHandle && !record.hidden) this.renderer.setIndicatorStatus?.(record.renderHandle, status);
|
|
22465
|
+
if (alive && record.renderHandle && !record.hidden) this.renderer.setIndicatorStatus?.(record.renderHandle, status);
|
|
22366
22466
|
}
|
|
22367
22467
|
};
|
|
22368
|
-
|
|
22369
|
-
|
|
22468
|
+
native.instance.start(ctx, record.inputValues);
|
|
22469
|
+
native.started = true;
|
|
22370
22470
|
} catch (err) {
|
|
22371
22471
|
this.fail(id, handle, err);
|
|
22372
22472
|
}
|
|
@@ -22379,8 +22479,14 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
22379
22479
|
const { type, descriptor } = record.native;
|
|
22380
22480
|
return descriptor.multiInstance ? nativeInstanceChannel(type, record.id) : type;
|
|
22381
22481
|
}
|
|
22382
|
-
/**
|
|
22383
|
-
|
|
22482
|
+
/**
|
|
22483
|
+
* Wrap a native indicator's emitted visuals into a full IndicatorModel, tagged `native`.
|
|
22484
|
+
* `anchorTime` is the first bar the output is index-aligned to — the chart's first bar
|
|
22485
|
+
* when it emitted. Without it the renderer pins the output's first slot to whatever the
|
|
22486
|
+
* first bar is NOW, so history landing in front of a computed output (or the oldest
|
|
22487
|
+
* bars trimmed under it) would slide the whole plot by that many bars.
|
|
22488
|
+
*/
|
|
22489
|
+
buildNativeModel(record, out, anchorTime) {
|
|
22384
22490
|
const d = record.native.descriptor;
|
|
22385
22491
|
const type = record.native.type;
|
|
22386
22492
|
const channel = this.nativeChannel(record);
|
|
@@ -22391,6 +22497,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
22391
22497
|
overlay: d.overlay,
|
|
22392
22498
|
paneHint: d.paneHint,
|
|
22393
22499
|
native: { type, ...channel !== type ? { channel } : {} },
|
|
22500
|
+
...anchorTime != null ? { anchorTime } : {},
|
|
22394
22501
|
...d.legend === false ? { legend: false } : {},
|
|
22395
22502
|
...out.paneAxis != null ? { paneAxis: out.paneAxis } : {},
|
|
22396
22503
|
series: out.series ?? [],
|
|
@@ -22445,8 +22552,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
22445
22552
|
const paneId = this.routePane(id, model, record.options ?? {});
|
|
22446
22553
|
this.placeModel(model, id, paneId);
|
|
22447
22554
|
record.model = model;
|
|
22448
|
-
this.ensurePaneFor(paneId);
|
|
22555
|
+
const paneCreated = this.ensurePaneFor(paneId);
|
|
22449
22556
|
record.renderHandle = this.renderer.mountIndicator(model);
|
|
22557
|
+
if (paneCreated) this.events.emit("pane:changed", void 0);
|
|
22450
22558
|
record.pendingStructural = true;
|
|
22451
22559
|
if (record.hidden) {
|
|
22452
22560
|
this.renderer.setIndicatorVisible?.(record.renderHandle, false);
|
|
@@ -22470,24 +22578,49 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
22470
22578
|
const paneId = this.routePane(id, model, record.options ?? {});
|
|
22471
22579
|
this.placeModel(model, id, paneId);
|
|
22472
22580
|
record.model = model;
|
|
22473
|
-
this.ensurePaneFor(paneId);
|
|
22581
|
+
const paneCreated = this.ensurePaneFor(paneId);
|
|
22474
22582
|
record.renderHandle = this.renderer.mountIndicator(model);
|
|
22583
|
+
if (paneCreated) this.events.emit("pane:changed", void 0);
|
|
22475
22584
|
record.pendingStructural = true;
|
|
22476
22585
|
this.renderer.setIndicatorVisible?.(record.renderHandle, false);
|
|
22477
22586
|
this.announce(record, this.handles.get(id));
|
|
22478
22587
|
}
|
|
22588
|
+
/**
|
|
22589
|
+
* The legend row of a native HELD for the history (see `waitsForHistory`): an empty
|
|
22590
|
+
* model with the title and inputs schema, spinning, not announced — exactly a script
|
|
22591
|
+
* indicator's loading placeholder, so `ready`/`indicator:added` and `inspect()` still
|
|
22592
|
+
* mean "it produced output". A record that already has a row (restarted over a new
|
|
22593
|
+
* market, or shown again) keeps it and only spins. The first emit remounts over it.
|
|
22594
|
+
*/
|
|
22595
|
+
mountNativePlaceholder(id, record) {
|
|
22596
|
+
record.pendingStructural = true;
|
|
22597
|
+
if (!record.renderHandle) {
|
|
22598
|
+
const model = this.buildNativeModel(record, {});
|
|
22599
|
+
const paneId = this.routePane(id, model, record.options ?? {});
|
|
22600
|
+
this.placeModel(model, id, paneId);
|
|
22601
|
+
record.model = model;
|
|
22602
|
+
const paneCreated = this.ensurePaneFor(paneId);
|
|
22603
|
+
record.renderHandle = this.renderer.mountIndicator(model);
|
|
22604
|
+
if (paneCreated) this.events.emit("pane:changed", void 0);
|
|
22605
|
+
}
|
|
22606
|
+
this.setLoading(record, true);
|
|
22607
|
+
}
|
|
22479
22608
|
/** Flip the record's loading state and reflect it in the legend row (spinner on/off). */
|
|
22480
22609
|
setLoading(record, loading) {
|
|
22481
22610
|
record.loading = loading;
|
|
22482
22611
|
if (record.renderHandle && !record.hidden) this.renderer.setIndicatorStatus?.(record.renderHandle, loading ? "loading" : "idle");
|
|
22483
22612
|
}
|
|
22613
|
+
/** Make sure the renderer has the pane; true when it is new to the layout (the caller
|
|
22614
|
+
* emits `pane:changed` once the indicator's model sits on it, so `list()` shows it). */
|
|
22484
22615
|
ensurePaneFor(paneId) {
|
|
22485
|
-
|
|
22616
|
+
const created = !this.paneOrder.includes(paneId);
|
|
22617
|
+
if (created) this.paneOrder.push(paneId);
|
|
22486
22618
|
this.renderer.ensurePane({
|
|
22487
22619
|
id: paneId,
|
|
22488
22620
|
kind: paneId === "price" ? "price" : "study",
|
|
22489
22621
|
order: this.paneOrder.indexOf(paneId)
|
|
22490
22622
|
});
|
|
22623
|
+
return created;
|
|
22491
22624
|
}
|
|
22492
22625
|
/** True when any (still-registered) indicator sits on the given pane. */
|
|
22493
22626
|
paneStillUsed(paneId) {
|
|
@@ -22523,12 +22656,14 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
22523
22656
|
const panes = [];
|
|
22524
22657
|
for (const paneId of this.paneOrder) {
|
|
22525
22658
|
if (paneId !== "price" && !byPane.has(paneId)) continue;
|
|
22659
|
+
const heightWeight = this.renderer.paneHeightWeight?.(paneId) ?? null;
|
|
22526
22660
|
panes.push({
|
|
22527
22661
|
id: paneId,
|
|
22528
22662
|
kind: paneId === "price" ? "price" : "study",
|
|
22529
22663
|
order: panes.length,
|
|
22530
22664
|
collapsed: this.collapsedPanes.has(paneId),
|
|
22531
22665
|
maximized: this.maximizedPaneId === paneId,
|
|
22666
|
+
...heightWeight !== null ? { heightWeight } : {},
|
|
22532
22667
|
indicators: byPane.get(paneId) ?? []
|
|
22533
22668
|
});
|
|
22534
22669
|
}
|
|
@@ -22560,6 +22695,22 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
22560
22695
|
this.renderer.setPaneMaximized?.(paneId);
|
|
22561
22696
|
this.events.emit("pane:changed", void 0);
|
|
22562
22697
|
}
|
|
22698
|
+
setPaneHeightWeight(paneId, weight) {
|
|
22699
|
+
if (!this.renderer.setPaneHeightWeight) {
|
|
22700
|
+
console.warn("[vela] the active renderer does not support pane sizing \u2014 chart.panes.setHeightWeight ignored.");
|
|
22701
|
+
return;
|
|
22702
|
+
}
|
|
22703
|
+
if (typeof weight !== "number" || !Number.isFinite(weight) || weight <= 0) {
|
|
22704
|
+
console.warn(`[vela] chart.panes.setHeightWeight: weight must be a positive number (got ${String(weight)}).`);
|
|
22705
|
+
return;
|
|
22706
|
+
}
|
|
22707
|
+
if (!this.existingPaneIds().includes(paneId)) {
|
|
22708
|
+
console.warn(`[vela] chart.panes.setHeightWeight: no pane "${paneId}".`);
|
|
22709
|
+
return;
|
|
22710
|
+
}
|
|
22711
|
+
this.renderer.setPaneHeightWeight(paneId, weight);
|
|
22712
|
+
this.events.emit("pane:changed", void 0);
|
|
22713
|
+
}
|
|
22563
22714
|
/** IndicatorController/PaneController: move (or merge) an indicator to another pane. */
|
|
22564
22715
|
moveIndicator(id, target) {
|
|
22565
22716
|
if (!this.paneManagementSupported() || !this.renderer.setIndicatorPane) {
|
|
@@ -22657,6 +22808,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
22657
22808
|
this.maximizedPaneId = a.maximized ? a.paneId : null;
|
|
22658
22809
|
this.events.emit("pane:changed", void 0);
|
|
22659
22810
|
break;
|
|
22811
|
+
case "resize":
|
|
22812
|
+
this.events.emit("pane:changed", void 0);
|
|
22813
|
+
break;
|
|
22660
22814
|
}
|
|
22661
22815
|
}
|
|
22662
22816
|
/** Fire `indicator:added` + the handle's `ready` once, on the first computed model. */
|
|
@@ -22687,14 +22841,16 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
22687
22841
|
const paneId2 = this.routePane(id, model, record.options ?? {});
|
|
22688
22842
|
this.placeModel(model, id, paneId2);
|
|
22689
22843
|
record.model = model;
|
|
22690
|
-
this.ensurePaneFor(paneId2);
|
|
22844
|
+
const paneCreated = this.ensurePaneFor(paneId2);
|
|
22691
22845
|
record.renderHandle = this.renderer.mountIndicator(model);
|
|
22692
22846
|
record.pendingStructural = false;
|
|
22847
|
+
if (paneCreated) this.events.emit("pane:changed", void 0);
|
|
22693
22848
|
this.announce(record, handle);
|
|
22694
22849
|
return true;
|
|
22695
22850
|
}
|
|
22696
22851
|
let paneId = record.model?.paneId ?? "price";
|
|
22697
22852
|
const prevOwnScale = record.model?.ownScale === true;
|
|
22853
|
+
let rerouted = false;
|
|
22698
22854
|
if (record.loading && !record.paneLocked) {
|
|
22699
22855
|
const routed = this.routePane(id, model, record.options ?? {});
|
|
22700
22856
|
if (routed !== paneId) {
|
|
@@ -22705,6 +22861,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
22705
22861
|
this.ensurePaneFor(routed);
|
|
22706
22862
|
record.pendingStructural = true;
|
|
22707
22863
|
paneId = routed;
|
|
22864
|
+
rerouted = true;
|
|
22708
22865
|
}
|
|
22709
22866
|
}
|
|
22710
22867
|
this.placeModel(model, id, paneId);
|
|
@@ -22717,6 +22874,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
22717
22874
|
} else {
|
|
22718
22875
|
this.renderer.updateIndicator(record.renderHandle, modelToValuePatch(model));
|
|
22719
22876
|
}
|
|
22877
|
+
if (rerouted) this.events.emit("pane:changed", void 0);
|
|
22720
22878
|
if (record.loading) this.setLoading(record, false);
|
|
22721
22879
|
this.announce(record, handle);
|
|
22722
22880
|
return true;
|
|
@@ -22731,13 +22889,16 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
22731
22889
|
const overlay = options.overlay ?? model.overlay;
|
|
22732
22890
|
return overlay ? "price" : `pane-${id}`;
|
|
22733
22891
|
}
|
|
22734
|
-
/** Each named value series' last
|
|
22735
|
-
* engine fills, so this works without an execution context.
|
|
22736
|
-
|
|
22892
|
+
/** Each named value series' value on the chart's last bar — the model is the one source
|
|
22893
|
+
* every engine fills, so this works without an execution context. Points past the last
|
|
22894
|
+
* bar (a series shifted forward) are skipped by time. */
|
|
22895
|
+
static plotsAtLastBar(model, lastBarTime) {
|
|
22737
22896
|
const out = {};
|
|
22738
22897
|
for (const spec of model.series) {
|
|
22739
22898
|
if (!isLineLikeSeries(spec)) continue;
|
|
22740
|
-
|
|
22899
|
+
let i = spec.points.length - 1;
|
|
22900
|
+
if (lastBarTime !== void 0) while (i >= 0 && spec.points[i].time > lastBarTime) i -= 1;
|
|
22901
|
+
out[spec.title] = spec.points[i]?.value ?? null;
|
|
22741
22902
|
}
|
|
22742
22903
|
return out;
|
|
22743
22904
|
}
|
|
@@ -22793,7 +22954,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
22793
22954
|
// whatever produced this run.
|
|
22794
22955
|
forming: this.config.live && this.bars.length > 0,
|
|
22795
22956
|
complete: this.historyState !== "backfill",
|
|
22796
|
-
plots: _EngineOrchestrator.plotsAtLastBar(model),
|
|
22957
|
+
plots: _EngineOrchestrator.plotsAtLastBar(model, lastBar?.time),
|
|
22797
22958
|
vars: snapshot?.variables ?? {},
|
|
22798
22959
|
...snapshot?.strategy ? { strategy: snapshot.strategy } : {},
|
|
22799
22960
|
warnings: snapshot?.warnings ?? [],
|
|
@@ -22862,8 +23023,9 @@ function sameInputValue(a, b) {
|
|
|
22862
23023
|
return typeof a === "object" && a !== null && typeof b === "object" && b !== null && JSON.stringify(a) === JSON.stringify(b);
|
|
22863
23024
|
}
|
|
22864
23025
|
function blankedModel(model) {
|
|
23026
|
+
const { anchorTime: _anchor, ...rest } = model;
|
|
22865
23027
|
return {
|
|
22866
|
-
...
|
|
23028
|
+
...rest,
|
|
22867
23029
|
series: model.series.map((s) => s.kind === "candle" || s.kind === "bar" ? { ...s, bars: [] } : s.kind === "markers" ? { ...s, markers: [] } : { ...s, points: [] }),
|
|
22868
23030
|
fills: [],
|
|
22869
23031
|
backgrounds: [],
|
|
@@ -22886,12 +23048,14 @@ function modelToValuePatch(model) {
|
|
|
22886
23048
|
if (s.kind === "candle" || s.kind === "bar") {
|
|
22887
23049
|
series.push({ seriesId: s.id, kind: "bars", bars: s.bars, barColors: s.barColors ?? [] });
|
|
22888
23050
|
for (const b of s.bars) {
|
|
23051
|
+
if (!b) continue;
|
|
22889
23052
|
if (b.time < from) from = b.time;
|
|
22890
23053
|
if (b.time > to) to = b.time;
|
|
22891
23054
|
}
|
|
22892
23055
|
} else if (isLineLikeSeries(s)) {
|
|
22893
23056
|
series.push({ seriesId: s.id, kind: "points", points: s.points });
|
|
22894
23057
|
for (const p of s.points) {
|
|
23058
|
+
if (!p) continue;
|
|
22895
23059
|
if (p.time < from) from = p.time;
|
|
22896
23060
|
if (p.time > to) to = p.time;
|
|
22897
23061
|
}
|
|
@@ -23189,6 +23353,17 @@ var PanesControl = class {
|
|
|
23189
23353
|
if (this.guard()) this.controller.maximizePane(paneId);
|
|
23190
23354
|
return this;
|
|
23191
23355
|
}
|
|
23356
|
+
/**
|
|
23357
|
+
* Set a pane's height weight: the panes share the plot height in proportion to their
|
|
23358
|
+
* weights, so `setHeightWeight('price', 6)` next to two study panes at 1 gives the price
|
|
23359
|
+
* pane three quarters of the height. The weight sticks until the user drags a separator
|
|
23360
|
+
* or it is set again; a collapsed pane applies it when expanded. `weight` must be a
|
|
23361
|
+
* positive number.
|
|
23362
|
+
*/
|
|
23363
|
+
setHeightWeight(paneId, weight) {
|
|
23364
|
+
if (this.guard()) this.controller.setPaneHeightWeight(paneId, weight);
|
|
23365
|
+
return this;
|
|
23366
|
+
}
|
|
23192
23367
|
/** Move/merge an indicator to a pane — sugar for `handle.moveTo(...)` by id. */
|
|
23193
23368
|
moveIndicator(id, target) {
|
|
23194
23369
|
if (this.guard()) this.controller.moveIndicator(id, target);
|
|
@@ -24461,6 +24636,8 @@ var LEGEND_TITLE_STATUS_GAP_PX = 8;
|
|
|
24461
24636
|
var LEGEND_ACTION_GAP_PX = 6;
|
|
24462
24637
|
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;`;
|
|
24463
24638
|
var LEGEND_ROW_MIN_H = LEGEND_CTL_PX + LEGEND_ROW_PAD_Y * 2;
|
|
24639
|
+
var STACK_RAISE = "stack:raise";
|
|
24640
|
+
var STACK_LOWER = "stack:lower";
|
|
24464
24641
|
var EYE_SVG = iconAt("eye", LEGEND_ICON_PX2);
|
|
24465
24642
|
var EYE_OFF_SVG = iconAt("eye-off", LEGEND_ICON_PX2);
|
|
24466
24643
|
var GEAR_SVG = iconAt("gear", LEGEND_ICON_PX2);
|
|
@@ -24490,6 +24667,10 @@ var InputsUI = class {
|
|
|
24490
24667
|
this.mobileLayout = false;
|
|
24491
24668
|
/** Pane move/merge hook — when set, rows get a "Move to" menu + become drag-to-pane sources. */
|
|
24492
24669
|
this.moveApi = null;
|
|
24670
|
+
/** Overlay restacking hook — when set, a row's menus offer to step it forward or back. */
|
|
24671
|
+
this.stackApi = null;
|
|
24672
|
+
/** What {@link orderRows} last ordered the rows by (skips unchanged frames). */
|
|
24673
|
+
this.rowOrderKey = "";
|
|
24493
24674
|
/** Host-contributed legend actions, resolved PER ROW at render time (see setLegendActions). */
|
|
24494
24675
|
this.legendActions = null;
|
|
24495
24676
|
/** Host-contributed callout bubbles, resolved PER ROW at render time (see setLegendCallouts). */
|
|
@@ -24628,6 +24809,10 @@ var InputsUI = class {
|
|
|
24628
24809
|
setMoveApi(api) {
|
|
24629
24810
|
this.moveApi = api;
|
|
24630
24811
|
}
|
|
24812
|
+
/** Enable the context menu's "Bring in front of …" entry for overlay indicators. Null disables it. */
|
|
24813
|
+
setStackApi(api) {
|
|
24814
|
+
this.stackApi = api;
|
|
24815
|
+
}
|
|
24631
24816
|
/**
|
|
24632
24817
|
* Wire the host-contributed row actions. Re-calling replaces the provider and
|
|
24633
24818
|
* re-projects the rows already on screen (a late `registerLegendAction` appears after
|
|
@@ -24773,16 +24958,20 @@ var InputsUI = class {
|
|
|
24773
24958
|
}
|
|
24774
24959
|
this.moveTargets = items.map((it) => it.target);
|
|
24775
24960
|
this.moveMenuId = id;
|
|
24776
|
-
const
|
|
24961
|
+
const stack = this.stackItems(id);
|
|
24962
|
+
const moves = items.map((it, i) => ({ id: String(i), label: it.label }));
|
|
24963
|
+
if (moves[0] && stack.length > 0) moves[0].separatorBefore = true;
|
|
24964
|
+
const descriptors = [...stack, ...moves];
|
|
24777
24965
|
if (!this.moveMenu) {
|
|
24778
24966
|
this.moveMenu = new Menu({
|
|
24779
24967
|
host: this.container,
|
|
24780
24968
|
items: descriptors,
|
|
24781
24969
|
placement: "bottom-start",
|
|
24782
24970
|
onSelect: (itemId) => {
|
|
24783
|
-
const target = this.moveTargets[Number(itemId)];
|
|
24784
24971
|
const rowId = this.moveMenuId;
|
|
24785
|
-
if (
|
|
24972
|
+
if (!rowId || this.runStackItem(itemId, rowId)) return;
|
|
24973
|
+
const target = this.moveTargets[Number(itemId)];
|
|
24974
|
+
if (target) api.move(rowId, target);
|
|
24786
24975
|
}
|
|
24787
24976
|
});
|
|
24788
24977
|
} else {
|
|
@@ -24795,18 +24984,24 @@ var InputsUI = class {
|
|
|
24795
24984
|
this.moveMenu?.close();
|
|
24796
24985
|
}
|
|
24797
24986
|
// ── legend row context menu (right-click) ───────────────────────────────
|
|
24798
|
-
/** Open the row's action menu at the pointer
|
|
24987
|
+
/** Open the row's action menu at the pointer: an overlay indicator's restacking steps
|
|
24988
|
+
* first (where a layer is there to pass), then the values toggle. */
|
|
24799
24989
|
openRowMenu(id, x, y) {
|
|
24800
24990
|
this.closeRowMenu();
|
|
24801
24991
|
this.closeMoveMenu();
|
|
24802
24992
|
const row2 = this.rows.get(id);
|
|
24803
24993
|
if (!row2) return;
|
|
24804
24994
|
this.rowMenuId = id;
|
|
24805
|
-
const
|
|
24806
|
-
|
|
24807
|
-
|
|
24808
|
-
|
|
24809
|
-
|
|
24995
|
+
const stack = this.stackItems(id);
|
|
24996
|
+
const items = [
|
|
24997
|
+
...stack,
|
|
24998
|
+
{
|
|
24999
|
+
id: "values",
|
|
25000
|
+
label: "Indicator values",
|
|
25001
|
+
checked: this.rowValuesShown(row2),
|
|
25002
|
+
separatorBefore: stack.length > 0
|
|
25003
|
+
}
|
|
25004
|
+
];
|
|
24810
25005
|
if (!this.rowMenu) {
|
|
24811
25006
|
this.rowMenu = new Menu({
|
|
24812
25007
|
host: this.container,
|
|
@@ -24815,8 +25010,10 @@ var InputsUI = class {
|
|
|
24815
25010
|
// Right-click action menu — checked state reads as a leading ✓ (see the kit Menu).
|
|
24816
25011
|
checkmarks: true,
|
|
24817
25012
|
onSelect: (itemId) => {
|
|
24818
|
-
|
|
24819
|
-
|
|
25013
|
+
const rowId = this.rowMenuId;
|
|
25014
|
+
if (!rowId || this.runStackItem(itemId, rowId)) return;
|
|
25015
|
+
if (itemId !== "values") return;
|
|
25016
|
+
const target = this.rows.get(rowId);
|
|
24820
25017
|
if (!target) return;
|
|
24821
25018
|
target.showValues = !this.rowValuesShown(target);
|
|
24822
25019
|
this.renderRowValues(target);
|
|
@@ -24830,6 +25027,55 @@ var InputsUI = class {
|
|
|
24830
25027
|
closeRowMenu() {
|
|
24831
25028
|
this.rowMenu?.close();
|
|
24832
25029
|
}
|
|
25030
|
+
/** A row's one-step restacking entries (forward, then back), for whichever neighbor exists. */
|
|
25031
|
+
stackItems(id) {
|
|
25032
|
+
const api = this.stackApi;
|
|
25033
|
+
if (!api) return [];
|
|
25034
|
+
const items = [];
|
|
25035
|
+
const forward = this.stackLabel("Bring in front of", api.above(id), "the indicator in front");
|
|
25036
|
+
if (forward) items.push({ id: STACK_RAISE, label: forward });
|
|
25037
|
+
const back = this.stackLabel("Send behind", api.below(id), "the indicator behind");
|
|
25038
|
+
if (back) items.push({ id: STACK_LOWER, label: back });
|
|
25039
|
+
return items;
|
|
25040
|
+
}
|
|
25041
|
+
stackLabel(verb, neighbor, fallback) {
|
|
25042
|
+
if (neighbor === null) return null;
|
|
25043
|
+
if (neighbor === "price") return `${verb} price`;
|
|
25044
|
+
return `${verb} ${this.rows.get(neighbor.indicator)?.title ?? fallback}`;
|
|
25045
|
+
}
|
|
25046
|
+
/** Run a restacking entry; false when `itemId` is not one. */
|
|
25047
|
+
runStackItem(itemId, rowId) {
|
|
25048
|
+
if (itemId === STACK_RAISE) this.stackApi?.raise(rowId);
|
|
25049
|
+
else if (itemId === STACK_LOWER) this.stackApi?.lower(rowId);
|
|
25050
|
+
else return false;
|
|
25051
|
+
return true;
|
|
25052
|
+
}
|
|
25053
|
+
/**
|
|
25054
|
+
* List each pane's rows front-most first, the way the indicators stack (the object
|
|
25055
|
+
* tree's order). `backToFront` is the paint order; rows it doesn't name keep their
|
|
25056
|
+
* place after the others. Cheap to call every frame: no-op while nothing changed.
|
|
25057
|
+
*/
|
|
25058
|
+
orderRows(backToFront) {
|
|
25059
|
+
const key = `${backToFront.join(",")}|${[...this.rows.values()].map((r) => `${r.id}@${r.paneId}`).join(",")}`;
|
|
25060
|
+
if (key === this.rowOrderKey) return;
|
|
25061
|
+
this.rowOrderKey = key;
|
|
25062
|
+
const rank = new Map(backToFront.map((id, i) => [id, i]));
|
|
25063
|
+
for (const lg of this.legends.values()) {
|
|
25064
|
+
const els = [...this.rows.values()].filter((r) => r.el.parentElement === lg).map((r) => r.el);
|
|
25065
|
+
const dom = [...lg.children];
|
|
25066
|
+
const ofEl = new Map([...this.rows.values()].map((r) => [r.el, r.id]));
|
|
25067
|
+
const at = (el) => rank.get(ofEl.get(el)) ?? -1;
|
|
25068
|
+
els.sort((a, b) => {
|
|
25069
|
+
const ra = at(a);
|
|
25070
|
+
const rb = at(b);
|
|
25071
|
+
if (ra >= 0 && rb >= 0) return rb - ra;
|
|
25072
|
+
if (ra >= 0 || rb >= 0) return ra >= 0 ? -1 : 1;
|
|
25073
|
+
return dom.indexOf(a) - dom.indexOf(b);
|
|
25074
|
+
});
|
|
25075
|
+
const anchor = this.foldToggle?.parentElement === lg ? this.foldToggle : null;
|
|
25076
|
+
for (const el of els) lg.insertBefore(el, anchor);
|
|
25077
|
+
}
|
|
25078
|
+
}
|
|
24833
25079
|
/** Move a legend row to another pane (indicator merged/moved), tidying an emptied container. */
|
|
24834
25080
|
setPane(id, paneId) {
|
|
24835
25081
|
const row2 = this.rows.get(id);
|
|
@@ -26169,6 +26415,7 @@ var WebGL2Backend = class {
|
|
|
26169
26415
|
const i0 = Math.max(0, Math.floor(vr.from));
|
|
26170
26416
|
const i1 = Math.min(n - 1, Math.ceil(vr.to));
|
|
26171
26417
|
if (i1 < i0) return;
|
|
26418
|
+
const s1 = Math.max(i1, Math.ceil(vr.to));
|
|
26172
26419
|
gl.enable(gl.BLEND);
|
|
26173
26420
|
gl.enable(gl.SCISSOR_TEST);
|
|
26174
26421
|
const barColorMap = mergeBarColors(scene.indicators);
|
|
@@ -26227,8 +26474,8 @@ var WebGL2Backend = class {
|
|
|
26227
26474
|
b.alpha = this.modelAlpha;
|
|
26228
26475
|
const off = scene.offsetOf(m.id);
|
|
26229
26476
|
const mp = effPane(m);
|
|
26230
|
-
for (const f of m.fills) if (f.overlay !== true) this.emitFill(b, m, f, mp, coords, i0,
|
|
26231
|
-
for (const s of m.series) if (s.overlay !== true) this.emitSeries(b, s, mp, coords, i0,
|
|
26477
|
+
for (const f of m.fills) if (f.overlay !== true) this.emitFill(b, m, f, mp, coords, i0, s1, off);
|
|
26478
|
+
for (const s of m.series) if (s.overlay !== true) this.emitSeries(b, s, mp, coords, i0, s1, theme, off);
|
|
26232
26479
|
}
|
|
26233
26480
|
if (drawCandles && !candleDrawn) {
|
|
26234
26481
|
drawSlicesUpTo(scene.candleZ);
|
|
@@ -26239,11 +26486,11 @@ var WebGL2Backend = class {
|
|
|
26239
26486
|
if (isPrice) {
|
|
26240
26487
|
for (const m of scene.indicators.values()) {
|
|
26241
26488
|
const off = scene.offsetOf(m.id);
|
|
26242
|
-
for (const f of m.fills) if (f.overlay === true) this.emitFill(b, m, f, pane, coords, i0,
|
|
26489
|
+
for (const f of m.fills) if (f.overlay === true) this.emitFill(b, m, f, pane, coords, i0, s1, off);
|
|
26243
26490
|
}
|
|
26244
26491
|
for (const m of scene.indicators.values()) {
|
|
26245
26492
|
const off = scene.offsetOf(m.id);
|
|
26246
|
-
for (const s of m.series) if (s.overlay === true) this.emitSeries(b, s, pane, coords, i0,
|
|
26493
|
+
for (const s of m.series) if (s.overlay === true) this.emitSeries(b, s, pane, coords, i0, s1, theme, off);
|
|
26247
26494
|
}
|
|
26248
26495
|
}
|
|
26249
26496
|
drawSlicesUpTo(Infinity);
|
|
@@ -26265,7 +26512,7 @@ var WebGL2Backend = class {
|
|
|
26265
26512
|
if (this.glow > 0 && !this.screenProgram && !this.ensureScreenPipeline()) this.glow = 0;
|
|
26266
26513
|
if (this.glow > 0 && this.screenProgram) {
|
|
26267
26514
|
this.glowBatch.alpha = this.modelAlpha;
|
|
26268
|
-
this.renderGlow(gl, scene, coords, i0,
|
|
26515
|
+
this.renderGlow(gl, scene, coords, i0, s1);
|
|
26269
26516
|
}
|
|
26270
26517
|
}
|
|
26271
26518
|
/**
|
|
@@ -26822,7 +27069,7 @@ var WebGL2Backend = class {
|
|
|
26822
27069
|
const tickW = Math.max(1, Math.round(spacing * 0.35));
|
|
26823
27070
|
for (let i = i0; i <= i1; i += 1) {
|
|
26824
27071
|
const bar = s.bars[i - off];
|
|
26825
|
-
if (!bar) continue;
|
|
27072
|
+
if (!bar || !Number.isFinite(bar.open + bar.high + bar.low + bar.close)) continue;
|
|
26826
27073
|
const bc = s.barColors?.[i - off];
|
|
26827
27074
|
const body = parseColor(bc?.color ?? (bar.close >= bar.open ? up : down));
|
|
26828
27075
|
const x = Math.round(coords.logicalToX(i));
|
|
@@ -27475,6 +27722,8 @@ var InputController = class {
|
|
|
27475
27722
|
const tapRelease = this.dragging && !this.moved && (!wasTouch || Math.hypot(x - this.startX, y - this.startY) <= TOUCH_TAP_SLOP);
|
|
27476
27723
|
if (this.dragging && this.region === "drawing") {
|
|
27477
27724
|
this.deps.drawingsPointerUp?.(x, y, this.snapMode(e));
|
|
27725
|
+
} else if (this.dragging && this.region === "separator") {
|
|
27726
|
+
this.deps.endPaneResize?.();
|
|
27478
27727
|
} else if (tapRelease && this.region === "data") {
|
|
27479
27728
|
this.deps.onClick(x, y);
|
|
27480
27729
|
} else if (this.dragging && this.region === "data" && wasTouch) {
|
|
@@ -27496,6 +27745,7 @@ var InputController = class {
|
|
|
27496
27745
|
this.cancelLongPress();
|
|
27497
27746
|
if (!this.dragging) return;
|
|
27498
27747
|
if (this.region === "drawing" && !Number.isNaN(this.cursorX)) this.deps.drawingsPointerUp?.(this.cursorX, this.cursorY, this.snapMode(e));
|
|
27748
|
+
if (this.region === "separator") this.deps.endPaneResize?.();
|
|
27499
27749
|
if (this.region === "crosshair" || e.pointerType === "touch") this.deps.onPointerMove(null, null);
|
|
27500
27750
|
this.endGesture(e);
|
|
27501
27751
|
};
|
|
@@ -28172,9 +28422,12 @@ var SceneGraph = class {
|
|
|
28172
28422
|
setIndicatorZ(id, z) {
|
|
28173
28423
|
this.seriesZ.set(id, z);
|
|
28174
28424
|
}
|
|
28175
|
-
/** Snapshot of the current ordering for a UI/read API: `{ id, z }`
|
|
28425
|
+
/** Snapshot of the current ordering for a UI/read API: `{ id, z }` back to front, in the
|
|
28426
|
+
* order the indicators paint — on a shared key, mount order (see `orderedIndicatorsForPane`). */
|
|
28176
28427
|
indicatorZOrder() {
|
|
28177
|
-
|
|
28428
|
+
const mounted = new Map([...this.indicators.keys()].map((id, i) => [id, i]));
|
|
28429
|
+
const at = (id) => mounted.get(id) ?? Number.MAX_SAFE_INTEGER;
|
|
28430
|
+
return [...this.seriesZ.entries()].map(([id, z]) => ({ id, z })).sort((a, b) => a.z - b.z || at(a.id) - at(b.id));
|
|
28178
28431
|
}
|
|
28179
28432
|
/** The pane's series z keys (each indicator, plus the candles on the price pane), sorted
|
|
28180
28433
|
* ascending and de-duplicated — the boundaries a user drawing's z is slotted against. */
|
|
@@ -28202,16 +28455,19 @@ var SceneGraph = class {
|
|
|
28202
28455
|
for (const z of this.seriesZ.values()) if (z < min) min = z;
|
|
28203
28456
|
return min;
|
|
28204
28457
|
}
|
|
28458
|
+
/** Create the pane, or update an existing one in place. Without `heightWeight` an existing
|
|
28459
|
+
* pane keeps its weight (a user resize survives other indicators mounting onto it) and a
|
|
28460
|
+
* new one gets the default: 3 for the price pane, 1 for a study pane. */
|
|
28205
28461
|
ensurePane(id, kind, order, heightWeight) {
|
|
28206
28462
|
this.orderedCache = null;
|
|
28207
28463
|
const existing = this.panes.get(id);
|
|
28208
28464
|
if (existing) {
|
|
28209
28465
|
existing.order = order;
|
|
28210
|
-
existing.heightWeight = heightWeight;
|
|
28466
|
+
if (heightWeight !== void 0) existing.heightWeight = heightWeight;
|
|
28211
28467
|
existing.kind = kind;
|
|
28212
28468
|
return existing;
|
|
28213
28469
|
}
|
|
28214
|
-
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 };
|
|
28470
|
+
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 };
|
|
28215
28471
|
this.panes.set(id, pane);
|
|
28216
28472
|
return pane;
|
|
28217
28473
|
}
|
|
@@ -28268,6 +28524,7 @@ var Canvas2dBackend = class {
|
|
|
28268
28524
|
const i0 = Math.max(0, Math.floor(vr.from));
|
|
28269
28525
|
const i1 = Math.min(n - 1, Math.ceil(vr.to));
|
|
28270
28526
|
if (i1 < i0) return;
|
|
28527
|
+
const s1 = Math.max(i1, Math.ceil(vr.to));
|
|
28271
28528
|
const barColorMap = mergeBarColors2(scene.indicators);
|
|
28272
28529
|
const panes = scene.orderedPanes();
|
|
28273
28530
|
for (const pane of panes) {
|
|
@@ -28310,8 +28567,8 @@ var Canvas2dBackend = class {
|
|
|
28310
28567
|
ctx.globalAlpha = this.modelAlpha;
|
|
28311
28568
|
const off = scene.offsetOf(m.id);
|
|
28312
28569
|
const mp = effPane(m);
|
|
28313
|
-
for (const f of m.fills) if (f.overlay !== true) this.drawFill(ctx, m, f, mp, coords, i0,
|
|
28314
|
-
for (const s of m.series) if (s.overlay !== true) this.drawSeries(ctx, s, mp, coords, i0,
|
|
28570
|
+
for (const f of m.fills) if (f.overlay !== true) this.drawFill(ctx, m, f, mp, coords, i0, s1, off);
|
|
28571
|
+
for (const s of m.series) if (s.overlay !== true) this.drawSeries(ctx, s, mp, coords, i0, s1, theme, off);
|
|
28315
28572
|
}
|
|
28316
28573
|
if (drawCandles && !candleDrawn) {
|
|
28317
28574
|
drawSlicesUpTo(scene.candleZ);
|
|
@@ -28322,11 +28579,11 @@ var Canvas2dBackend = class {
|
|
|
28322
28579
|
ctx.globalAlpha = this.modelAlpha;
|
|
28323
28580
|
for (const m of scene.indicators.values()) {
|
|
28324
28581
|
const off = scene.offsetOf(m.id);
|
|
28325
|
-
for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0,
|
|
28582
|
+
for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0, s1, off);
|
|
28326
28583
|
}
|
|
28327
28584
|
for (const m of scene.indicators.values()) {
|
|
28328
28585
|
const off = scene.offsetOf(m.id);
|
|
28329
|
-
for (const s of m.series) if (s.overlay === true) this.drawSeries(ctx, s, pane, coords, i0,
|
|
28586
|
+
for (const s of m.series) if (s.overlay === true) this.drawSeries(ctx, s, pane, coords, i0, s1, theme, off);
|
|
28330
28587
|
}
|
|
28331
28588
|
}
|
|
28332
28589
|
drawSlicesUpTo(Infinity);
|
|
@@ -28778,7 +29035,7 @@ var Canvas2dBackend = class {
|
|
|
28778
29035
|
const tickW = Math.max(1, Math.round(spacing * 0.35));
|
|
28779
29036
|
for (let i = i0; i <= i1; i += 1) {
|
|
28780
29037
|
const b = s.bars[i - off];
|
|
28781
|
-
if (!b) continue;
|
|
29038
|
+
if (!b || !Number.isFinite(b.open + b.high + b.low + b.close)) continue;
|
|
28782
29039
|
const bc = s.barColors?.[i - off];
|
|
28783
29040
|
const body = bc?.color ?? (b.close >= b.open ? up : down);
|
|
28784
29041
|
const x = Math.round(coords.logicalToX(i)) + 0.5;
|
|
@@ -38908,6 +39165,58 @@ function stackLayers(entries, candleZ) {
|
|
|
38908
39165
|
return { below, above };
|
|
38909
39166
|
}
|
|
38910
39167
|
|
|
39168
|
+
// src/renderers/native/core/overlayStack.ts
|
|
39169
|
+
function priceStack(indicators, candleZ) {
|
|
39170
|
+
const out = [...indicators];
|
|
39171
|
+
const at = out.findIndex((e) => e.z >= candleZ);
|
|
39172
|
+
out.splice(at < 0 ? out.length : at, 0, { id: null, z: candleZ });
|
|
39173
|
+
return out;
|
|
39174
|
+
}
|
|
39175
|
+
function untie(stack) {
|
|
39176
|
+
const out = [];
|
|
39177
|
+
for (let i = 0; i < stack.length; ) {
|
|
39178
|
+
let j = i + 1;
|
|
39179
|
+
while (j < stack.length && stack[j].z === stack[i].z) j += 1;
|
|
39180
|
+
const run = j - i;
|
|
39181
|
+
if (run > 1) {
|
|
39182
|
+
const base = stack[i].z;
|
|
39183
|
+
const step = ((stack[j]?.z ?? base + 1) - base) / run;
|
|
39184
|
+
for (let k = 1; k < run; k += 1) {
|
|
39185
|
+
const id = stack[i + k].id;
|
|
39186
|
+
if (id !== null) out.push({ id, z: base + k * step });
|
|
39187
|
+
}
|
|
39188
|
+
}
|
|
39189
|
+
i = j;
|
|
39190
|
+
}
|
|
39191
|
+
return out;
|
|
39192
|
+
}
|
|
39193
|
+
function layerAbove(stack, id) {
|
|
39194
|
+
const i = stack.findIndex((e) => e.id === id);
|
|
39195
|
+
if (i < 0 || i === stack.length - 1) return null;
|
|
39196
|
+
const above = stack[i + 1];
|
|
39197
|
+
return above.id === null ? "price" : { indicator: above.id };
|
|
39198
|
+
}
|
|
39199
|
+
function layerBelow(stack, id) {
|
|
39200
|
+
const i = stack.findIndex((e) => e.id === id);
|
|
39201
|
+
if (i <= 0) return null;
|
|
39202
|
+
const below = stack[i - 1];
|
|
39203
|
+
return below.id === null ? "price" : { indicator: below.id };
|
|
39204
|
+
}
|
|
39205
|
+
function raisedZ(stack, id) {
|
|
39206
|
+
const i = stack.findIndex((e) => e.id === id);
|
|
39207
|
+
if (i < 0 || i === stack.length - 1) return null;
|
|
39208
|
+
const above = stack[i + 1];
|
|
39209
|
+
const next = stack[i + 2];
|
|
39210
|
+
return next ? (above.z + next.z) / 2 : above.z + 1;
|
|
39211
|
+
}
|
|
39212
|
+
function loweredZ(stack, id) {
|
|
39213
|
+
const i = stack.findIndex((e) => e.id === id);
|
|
39214
|
+
if (i <= 0) return null;
|
|
39215
|
+
const below = stack[i - 1];
|
|
39216
|
+
const prev2 = stack[i - 2];
|
|
39217
|
+
return prev2 ? (prev2.z + below.z) / 2 : below.z - 1;
|
|
39218
|
+
}
|
|
39219
|
+
|
|
38911
39220
|
// src/renderers/shared/dom-raster.ts
|
|
38912
39221
|
function hasInk(color) {
|
|
38913
39222
|
if (!color || color === "transparent") return false;
|
|
@@ -39435,6 +39744,8 @@ var NativeRenderer = class {
|
|
|
39435
39744
|
// pane just below it
|
|
39436
39745
|
this.resizeSplitStart = null;
|
|
39437
39746
|
// the two panes' shared span when the drag began
|
|
39747
|
+
this.resizeStartAboveWeight = 0;
|
|
39748
|
+
// the upper pane's weight when the drag began
|
|
39438
39749
|
/** The user drawings take part in pointer input (the runtime `drawingsInteractive` feature). */
|
|
39439
39750
|
this.drawingsInteractive = true;
|
|
39440
39751
|
this.hoverSeparatorY = null;
|
|
@@ -40077,6 +40388,7 @@ var NativeRenderer = class {
|
|
|
40077
40388
|
};
|
|
40078
40389
|
this.scene.candleZ = next.stacking.candles;
|
|
40079
40390
|
for (const [id, z] of Object.entries(next.stacking.series)) this.scene.setIndicatorZ(id, z);
|
|
40391
|
+
this.syncLegendOrder();
|
|
40080
40392
|
s.bars = { upColor: next.bars.upColor, downColor: next.bars.downColor };
|
|
40081
40393
|
s.line = { color: next.line.color, width: next.line.width };
|
|
40082
40394
|
s.area = { lineColor: next.area.lineColor, width: next.area.width, topColor: next.area.topColor, bottomColor: next.area.bottomColor };
|
|
@@ -40122,6 +40434,7 @@ var NativeRenderer = class {
|
|
|
40122
40434
|
if (typeof v.z === "number") this.scene.setIndicatorZ(v.id, v.z);
|
|
40123
40435
|
else if (v.to === "front") this.scene.bringIndicatorToFront(v.id);
|
|
40124
40436
|
else if (v.to === "back") this.scene.sendIndicatorToBack(v.id);
|
|
40437
|
+
this.syncLegendOrder();
|
|
40125
40438
|
}
|
|
40126
40439
|
/** Toggle the in-chart settings gear + dialog (the `settings` feature). The gear
|
|
40127
40440
|
* sits above the time axis, clear of the right price-axis strip. */
|
|
@@ -40509,7 +40822,7 @@ var NativeRenderer = class {
|
|
|
40509
40822
|
this.chrome.mount(this.chromeCanvas);
|
|
40510
40823
|
this.crosshairLayer.mount(this.cursorCanvas);
|
|
40511
40824
|
this.coords.setViewport(defaultViewport());
|
|
40512
|
-
this.scene.ensurePane(PRICE_PANE_ID2, "price", 0
|
|
40825
|
+
this.scene.ensurePane(PRICE_PANE_ID2, "price", 0);
|
|
40513
40826
|
this.scheduler = new Scheduler((level) => this.renderFrame(level));
|
|
40514
40827
|
this.animator = new Animator((dt) => this.animTick(dt));
|
|
40515
40828
|
this.targetBarSpacing = this.coords.getViewport().barSpacing;
|
|
@@ -40522,7 +40835,7 @@ var NativeRenderer = class {
|
|
|
40522
40835
|
onClick: (x, y) => {
|
|
40523
40836
|
if (this.handleMarkClick(x, y)) return;
|
|
40524
40837
|
this.userDrawings?.deselect();
|
|
40525
|
-
this.handleClick(x);
|
|
40838
|
+
this.handleClick(x, y);
|
|
40526
40839
|
},
|
|
40527
40840
|
onAxisLongPress: (axis, x, y) => {
|
|
40528
40841
|
for (const cb of this.axisLongPressCbs) cb({ axis, x, y });
|
|
@@ -40536,6 +40849,7 @@ var NativeRenderer = class {
|
|
|
40536
40849
|
paneSeparatorAt: (y) => this.paneSeparatorAt(y),
|
|
40537
40850
|
beginPaneResize: (y) => this.beginPaneResize(y),
|
|
40538
40851
|
paneResizeBy: (dy) => this.paneResizeBy(dy),
|
|
40852
|
+
endPaneResize: () => this.endPaneResize(),
|
|
40539
40853
|
resetPaneSize: (y) => this.resetPaneSize(y),
|
|
40540
40854
|
resetView: () => this.resetView(),
|
|
40541
40855
|
// User drawings claim a gesture before pan when armed / over a drawing.
|
|
@@ -40661,6 +40975,12 @@ var NativeRenderer = class {
|
|
|
40661
40975
|
for (const cb of this.moveIndicatorCbs) cb(id, target);
|
|
40662
40976
|
}
|
|
40663
40977
|
});
|
|
40978
|
+
this.inputsUI.setStackApi({
|
|
40979
|
+
above: (id) => layerAbove(this.overlayStack(), id),
|
|
40980
|
+
below: (id) => layerBelow(this.overlayStack(), id),
|
|
40981
|
+
raise: (id) => this.restackOverlay(id, raisedZ),
|
|
40982
|
+
lower: (id) => this.restackOverlay(id, loweredZ)
|
|
40983
|
+
});
|
|
40664
40984
|
this.paneControls = new PaneControls(this.plot, theme, {
|
|
40665
40985
|
panes: () => this.paneControlViews(),
|
|
40666
40986
|
rightAxis: () => this.rightAxisW,
|
|
@@ -40978,7 +41298,7 @@ var NativeRenderer = class {
|
|
|
40978
41298
|
}
|
|
40979
41299
|
// ── panes ──
|
|
40980
41300
|
ensurePane(pane) {
|
|
40981
|
-
this.scene.ensurePane(pane.id, pane.kind, pane.order, pane.heightWeight
|
|
41301
|
+
this.scene.ensurePane(pane.id, pane.kind, pane.order, pane.heightWeight);
|
|
40982
41302
|
this.layoutPanes();
|
|
40983
41303
|
this.paneControls?.refresh();
|
|
40984
41304
|
this.scheduler.invalidate(4 /* Full */);
|
|
@@ -41000,6 +41320,7 @@ var NativeRenderer = class {
|
|
|
41000
41320
|
model.ownScale = opts?.ownScale === true;
|
|
41001
41321
|
if (!model.ownScale) this.scene.dropIndicatorScale(handle.id);
|
|
41002
41322
|
this.inputsUI.setPane(handle.id, paneId);
|
|
41323
|
+
this.syncLegendOrder();
|
|
41003
41324
|
this.refreshAnchorOffset(model);
|
|
41004
41325
|
this.refreshAxisWidth();
|
|
41005
41326
|
this.layoutPanes();
|
|
@@ -41027,6 +41348,15 @@ var NativeRenderer = class {
|
|
|
41027
41348
|
this.paneControls?.refresh();
|
|
41028
41349
|
this.scheduler.invalidate(4 /* Full */);
|
|
41029
41350
|
}
|
|
41351
|
+
setPaneHeightWeight(paneId, weight) {
|
|
41352
|
+
const pane = this.scene.panes.get(paneId);
|
|
41353
|
+
if (!pane || !(weight > 0) || pane.heightWeight === weight) return;
|
|
41354
|
+
pane.heightWeight = weight;
|
|
41355
|
+
this.afterPaneLayoutChange();
|
|
41356
|
+
}
|
|
41357
|
+
paneHeightWeight(paneId) {
|
|
41358
|
+
return this.scene.panes.get(paneId)?.heightWeight ?? null;
|
|
41359
|
+
}
|
|
41030
41360
|
onPaneAction(cb) {
|
|
41031
41361
|
this.paneActionCbs.add(cb);
|
|
41032
41362
|
return () => this.paneActionCbs.delete(cb);
|
|
@@ -41121,6 +41451,7 @@ var NativeRenderer = class {
|
|
|
41121
41451
|
this.vpvrHidden = false;
|
|
41122
41452
|
}
|
|
41123
41453
|
if (model.ownScale) this.refreshAxisWidth();
|
|
41454
|
+
this.syncLegendOrder();
|
|
41124
41455
|
this.paneControls?.refresh();
|
|
41125
41456
|
this.scheduler.invalidate(4 /* Full */);
|
|
41126
41457
|
return { id: model.id };
|
|
@@ -41528,7 +41859,7 @@ var NativeRenderer = class {
|
|
|
41528
41859
|
this.setMarkHover(null);
|
|
41529
41860
|
this.scheduler.invalidate(1 /* Cursor */);
|
|
41530
41861
|
this.hoverLogical = null;
|
|
41531
|
-
const empty = { time: null, price: null, paneKind: null, values: /* @__PURE__ */ new Map(), ohlc: null };
|
|
41862
|
+
const empty = { time: null, price: null, paneKind: null, paneId: null, values: /* @__PURE__ */ new Map(), ohlc: null };
|
|
41532
41863
|
for (const cb of this.crosshairCbs) cb(empty);
|
|
41533
41864
|
return;
|
|
41534
41865
|
}
|
|
@@ -41559,14 +41890,23 @@ var NativeRenderer = class {
|
|
|
41559
41890
|
const bar = onBar ? this.bars[logical] : void 0;
|
|
41560
41891
|
const ohlc = bar ? { time: bar.time, open: bar.open, high: bar.high, low: bar.low, close: bar.close, volume: bar.volume } : null;
|
|
41561
41892
|
this.hoverLogical = onBar ? logical : null;
|
|
41562
|
-
const
|
|
41893
|
+
const onPane = inData && pane ? pane : null;
|
|
41894
|
+
const event = { time, price, paneKind: onPane?.kind ?? null, paneId: onPane?.id ?? null, values, ohlc };
|
|
41563
41895
|
for (const cb of this.crosshairCbs) cb(event);
|
|
41564
41896
|
}
|
|
41565
|
-
handleClick(x) {
|
|
41897
|
+
handleClick(x, y) {
|
|
41566
41898
|
if (this.coords.barCount === 0) return;
|
|
41567
41899
|
const logical = Math.round(this.coords.xToLogical(x));
|
|
41568
41900
|
const onBar = logical >= 0 && logical < this.coords.barCount;
|
|
41569
|
-
|
|
41901
|
+
const inData = x >= 0 && y >= 0 && x <= this.coords.width && y <= this.coords.height;
|
|
41902
|
+
const pane = inData ? this.paneNodeAtY(y) : null;
|
|
41903
|
+
const event = {
|
|
41904
|
+
time: onBar ? this.coords.logicalToTime(logical) : null,
|
|
41905
|
+
price: pane ? this.coords.yToPrice(y, pane.scale, pane.bounds) : null,
|
|
41906
|
+
paneKind: pane?.kind ?? null,
|
|
41907
|
+
paneId: pane?.id ?? null
|
|
41908
|
+
};
|
|
41909
|
+
for (const cb of this.clickCbs) cb(event);
|
|
41570
41910
|
}
|
|
41571
41911
|
/**
|
|
41572
41912
|
* Fan out (or collapse, with null) a multi-group mark stack; repaints the chrome tier when
|
|
@@ -41811,6 +42151,7 @@ var NativeRenderer = class {
|
|
|
41811
42151
|
const below = panes[i];
|
|
41812
42152
|
this.resizeAbove = above;
|
|
41813
42153
|
this.resizeBelow = below;
|
|
42154
|
+
this.resizeStartAboveWeight = above.heightWeight;
|
|
41814
42155
|
this.resizeSplitStart = {
|
|
41815
42156
|
combinedTop: above.bounds.top,
|
|
41816
42157
|
combinedHeight: above.bounds.height + below.bounds.height,
|
|
@@ -41831,6 +42172,16 @@ var NativeRenderer = class {
|
|
|
41831
42172
|
this.layoutPanes();
|
|
41832
42173
|
this.scheduler.invalidate(4 /* Full */);
|
|
41833
42174
|
}
|
|
42175
|
+
/** Release the separator: report the resize once, at the end of the drag, if it changed anything. */
|
|
42176
|
+
endPaneResize() {
|
|
42177
|
+
const above = this.resizeAbove;
|
|
42178
|
+
const below = this.resizeBelow;
|
|
42179
|
+
const startAbove = this.resizeStartAboveWeight;
|
|
42180
|
+
this.resizeAbove = null;
|
|
42181
|
+
this.resizeBelow = null;
|
|
42182
|
+
this.resizeSplitStart = null;
|
|
42183
|
+
if (above && below && above.heightWeight !== startAbove) this.emitPaneAction({ type: "resize", paneIds: [above.id, below.id] });
|
|
42184
|
+
}
|
|
41834
42185
|
/** Double-click a separator → split the two adjacent panes evenly (each gets half of
|
|
41835
42186
|
* their combined weight). A simple, predictable reset that leaves siblings untouched. */
|
|
41836
42187
|
resetPaneSize(y) {
|
|
@@ -41844,6 +42195,7 @@ var NativeRenderer = class {
|
|
|
41844
42195
|
below.heightWeight = half;
|
|
41845
42196
|
this.layoutPanes();
|
|
41846
42197
|
this.scheduler.invalidate(4 /* Full */);
|
|
42198
|
+
this.emitPaneAction({ type: "resize", paneIds: [above.id, below.id] });
|
|
41847
42199
|
}
|
|
41848
42200
|
// ── keyboard navigation / accessibility (item 11) ──
|
|
41849
42201
|
/** Enable/disable keyboard control: focusability + ARIA + key listeners + live region. */
|
|
@@ -42098,6 +42450,7 @@ var NativeRenderer = class {
|
|
|
42098
42450
|
/** Paint the below-data (L-1) + geometry (L0) + chrome (L1) layers from the current scene/coords. */
|
|
42099
42451
|
paintData() {
|
|
42100
42452
|
this.syncLayerCanvasOrder();
|
|
42453
|
+
this.syncLegendOrder();
|
|
42101
42454
|
this.stampScaleInvert();
|
|
42102
42455
|
this.backend.modelAlpha = this.modelAlpha;
|
|
42103
42456
|
this.backend.candleBodyAlpha = this.candleBodyAlpha;
|
|
@@ -42279,6 +42632,7 @@ var NativeRenderer = class {
|
|
|
42279
42632
|
const i0 = Math.max(0, Math.floor(vr.from));
|
|
42280
42633
|
const i1 = Math.min(n - 1, Math.ceil(vr.to));
|
|
42281
42634
|
if (i1 < i0) return;
|
|
42635
|
+
const s1 = Math.max(i1, Math.ceil(vr.to));
|
|
42282
42636
|
this.scene.percentBaseline = this.bars[i0]?.close ?? 0;
|
|
42283
42637
|
const panes = this.scene.orderedPanes();
|
|
42284
42638
|
const pricePane = panes.find((p) => p.kind === "price") ?? null;
|
|
@@ -42296,11 +42650,11 @@ var NativeRenderer = class {
|
|
|
42296
42650
|
const masterModels = models.filter((m) => m.ownScale !== true);
|
|
42297
42651
|
let dr = this.chrome.paneDrawingsRange(masterModels, this.scene, pane === pricePane, vr);
|
|
42298
42652
|
if (pane === pricePane) {
|
|
42299
|
-
const or = overlaySeriesRange(this.scene.indicators.values(), i0,
|
|
42653
|
+
const or = overlaySeriesRange(this.scene.indicators.values(), i0, s1, (id) => this.scene.offsetOf(id));
|
|
42300
42654
|
if (or) dr = dr ? { min: Math.min(dr.min, or.min), max: Math.max(dr.max, or.max) } : or;
|
|
42301
42655
|
}
|
|
42302
42656
|
const includeCandles = pane.kind === "price" && (!this.scene.candlesHidden || this.priceLayersAnchoredToBars(masterModels) || !this.paneHasMeasurableContent(masterModels, dr));
|
|
42303
|
-
pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0,
|
|
42657
|
+
pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, s1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id), margins);
|
|
42304
42658
|
pane.percentBaseline = pane.kind === "price" ? this.bars[i0]?.close ?? 0 : this.firstVisibleValue(masterModels, i0);
|
|
42305
42659
|
pane.axisFormat = void 0;
|
|
42306
42660
|
pane.axisBands = void 0;
|
|
@@ -42340,7 +42694,7 @@ var NativeRenderer = class {
|
|
|
42340
42694
|
continue;
|
|
42341
42695
|
}
|
|
42342
42696
|
const mdr = this.chrome.paneDrawingsRange([model], this.scene, false, vr);
|
|
42343
|
-
sl.scaleTarget = computePaneScale([model], this.bars, false, i0,
|
|
42697
|
+
sl.scaleTarget = computePaneScale([model], this.bars, false, i0, s1, mdr, false, (id) => this.scene.offsetOf(id), margins);
|
|
42344
42698
|
if (!animating || !sl.initialized) {
|
|
42345
42699
|
sl.scale = { ...sl.scaleTarget };
|
|
42346
42700
|
sl.initialized = true;
|
|
@@ -42411,6 +42765,30 @@ var NativeRenderer = class {
|
|
|
42411
42765
|
const master = models.find((m) => m.ownScale !== true) ?? models[0];
|
|
42412
42766
|
return master?.title || null;
|
|
42413
42767
|
}
|
|
42768
|
+
/**
|
|
42769
|
+
* The price pane's layers back to front, as the legend restacks them: every indicator
|
|
42770
|
+
* with a legend row, in paint order, plus the candles. The same layers and order the
|
|
42771
|
+
* object tree lists (see `indicatorZOrder`).
|
|
42772
|
+
*/
|
|
42773
|
+
overlayStack() {
|
|
42774
|
+
const painted = this.scene.orderedIndicatorsForPane(PRICE_PANE_ID2).filter((m) => m.legend !== false).map((m) => ({ id: m.id, z: this.scene.zOf(m.id) }));
|
|
42775
|
+
return priceStack(painted, this.scene.candleZ);
|
|
42776
|
+
}
|
|
42777
|
+
/** List the legend rows the way the indicators stack, front-most first (no-op when unchanged). */
|
|
42778
|
+
syncLegendOrder() {
|
|
42779
|
+
this.inputsUI?.orderRows(this.scene.indicatorZOrder().map((e) => e.id));
|
|
42780
|
+
}
|
|
42781
|
+
/** Apply a legend restacking step: shared keys are spread apart first (the order stays),
|
|
42782
|
+
* so the step lands exactly one layer over. No-op when there is nothing to step over. */
|
|
42783
|
+
restackOverlay(id, step) {
|
|
42784
|
+
for (const fix of untie(this.overlayStack())) this.scene.setIndicatorZ(fix.id, fix.z);
|
|
42785
|
+
const z = step(this.overlayStack(), id);
|
|
42786
|
+
if (z === null) return;
|
|
42787
|
+
this.scene.setIndicatorZ(id, z);
|
|
42788
|
+
this.syncLegendOrder();
|
|
42789
|
+
this.scheduler.invalidate(4 /* Full */);
|
|
42790
|
+
for (const cb of this.configChangedCbs) cb();
|
|
42791
|
+
}
|
|
42414
42792
|
/** The pane a bespoke-layer native indicator (volume/vpvr) currently lives in (null = not mounted). */
|
|
42415
42793
|
nativeLayerPane(type) {
|
|
42416
42794
|
for (const m of this.scene.indicators.values()) {
|
|
@@ -42865,17 +43243,35 @@ function leadingPointsBefore(model, headTime) {
|
|
|
42865
43243
|
for (const s of model.series) {
|
|
42866
43244
|
const times = s.kind === "candle" || s.kind === "bar" ? s.bars : isLineLikeSeries(s) ? s.points : null;
|
|
42867
43245
|
if (!times || times.length === 0) continue;
|
|
42868
|
-
|
|
42869
|
-
|
|
42870
|
-
while (lo < hi) {
|
|
42871
|
-
const mid = lo + hi >> 1;
|
|
42872
|
-
if (times[mid].time < headTime) lo = mid + 1;
|
|
42873
|
-
else hi = mid;
|
|
42874
|
-
}
|
|
42875
|
-
return lo;
|
|
43246
|
+
const count = slotsBefore(times, headTime);
|
|
43247
|
+
if (count !== null) return count;
|
|
42876
43248
|
}
|
|
42877
43249
|
return null;
|
|
42878
43250
|
}
|
|
43251
|
+
function slotsBefore(times, headTime) {
|
|
43252
|
+
let lo = 0;
|
|
43253
|
+
let hi = times.length;
|
|
43254
|
+
while (lo < hi) {
|
|
43255
|
+
const mid = lo + hi >> 1;
|
|
43256
|
+
const t = times[mid]?.time;
|
|
43257
|
+
if (t === void 0) return slotsBeforeScan(times, headTime);
|
|
43258
|
+
if (t < headTime) lo = mid + 1;
|
|
43259
|
+
else hi = mid;
|
|
43260
|
+
}
|
|
43261
|
+
return lo;
|
|
43262
|
+
}
|
|
43263
|
+
function slotsBeforeScan(times, headTime) {
|
|
43264
|
+
let lastBefore = -1;
|
|
43265
|
+
let any = false;
|
|
43266
|
+
for (let i = 0; i < times.length; i += 1) {
|
|
43267
|
+
const t = times[i]?.time;
|
|
43268
|
+
if (t === void 0) continue;
|
|
43269
|
+
any = true;
|
|
43270
|
+
if (t >= headTime) return t === headTime ? i : lastBefore + 1;
|
|
43271
|
+
lastBefore = i;
|
|
43272
|
+
}
|
|
43273
|
+
return any ? times.length : null;
|
|
43274
|
+
}
|
|
42879
43275
|
function applyPatch(model, patch) {
|
|
42880
43276
|
if (patch.kind === "value") {
|
|
42881
43277
|
for (const delta of patch.series) {
|
|
@@ -43259,6 +43655,9 @@ function classicDescriptor(spec) {
|
|
|
43259
43655
|
overlay: spec.overlay,
|
|
43260
43656
|
// Studies stack (a 20 and a 200 moving average side by side) — unlike the layer-backed natives.
|
|
43261
43657
|
multiInstance: true,
|
|
43658
|
+
// A study's values depend on every bar before them (an EMA's seed, an RSI's
|
|
43659
|
+
// smoothing), so it computes once over the whole history, as script indicators do.
|
|
43660
|
+
waitsForHistory: true,
|
|
43262
43661
|
inputsSchema: () => spec.inputs,
|
|
43263
43662
|
defaultInputs: () => Object.fromEntries(spec.inputs.map((i) => [i.key, i.defval])),
|
|
43264
43663
|
create: () => new ClassicIndicator(spec)
|
|
@@ -44094,41 +44493,50 @@ var maEnvelope = {
|
|
|
44094
44493
|
return directionalChannel(bars, basis, map(basis, (x) => x * (1 + k)), map(basis, (x) => x * (1 - k)), ink, "envelope");
|
|
44095
44494
|
}
|
|
44096
44495
|
};
|
|
44097
|
-
var
|
|
44098
|
-
|
|
44099
|
-
|
|
44100
|
-
|
|
44101
|
-
|
|
44102
|
-
|
|
44103
|
-
|
|
44496
|
+
var leastSquaresMa = {
|
|
44497
|
+
type: "lsma",
|
|
44498
|
+
title: "Least Squares Moving Average",
|
|
44499
|
+
shortTitle: "LSMA",
|
|
44500
|
+
overlay: true,
|
|
44501
|
+
inputs: [
|
|
44502
|
+
sourceInput("Close", "source", "Source", "Price series the regression line is fitted to."),
|
|
44503
|
+
{ ...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 },
|
|
44504
|
+
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."),
|
|
44505
|
+
...directionInputs()
|
|
44506
|
+
],
|
|
44507
|
+
compute: (bars, inputs) => {
|
|
44508
|
+
const src = sourceValues(bars, str3(inputs, "source", "Close"));
|
|
44509
|
+
const values = linreg(src, num3(inputs, "length", 25), Math.trunc(num3(inputs, "offset", 0)));
|
|
44510
|
+
return { plots: [slopeColoredPlot("lsma", "LSMA", values, inputs)] };
|
|
44511
|
+
}
|
|
44512
|
+
};
|
|
44513
|
+
var linearRegressionSlope = {
|
|
44514
|
+
type: "linear-regression-slope",
|
|
44515
|
+
title: "Linear Regression Slope",
|
|
44516
|
+
shortTitle: "LRS",
|
|
44104
44517
|
overlay: false,
|
|
44105
44518
|
inputs: [
|
|
44106
|
-
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."),
|
|
44107
44519
|
{ ...lengthInput(100, "length", "Length", 5e3, "Number of bars used to fit the linear regression."), min: 2 },
|
|
44108
44520
|
sourceInput("Close", "source", "Source", "Price series the regression is fitted to."),
|
|
44109
|
-
colorInput(BULLISH, "Rising", "bullColor", "Color of the
|
|
44110
|
-
colorInput(BEARISH, "Falling", "bearColor", "Color of the
|
|
44111
|
-
colorInput(NEUTRAL, "
|
|
44521
|
+
colorInput(BULLISH, "Rising", "bullColor", "Color of the slope line while the fit slopes upward."),
|
|
44522
|
+
colorInput(BEARISH, "Falling", "bearColor", "Color of the slope line while the fit slopes downward."),
|
|
44523
|
+
colorInput(NEUTRAL, "Slope Line", "lineColor", "Color of the slope line while the fit is flat.")
|
|
44112
44524
|
],
|
|
44113
44525
|
compute: (bars, inputs) => {
|
|
44114
44526
|
const len = num3(inputs, "length", 100);
|
|
44115
44527
|
const src = sourceValues(bars, str3(inputs, "source", "Close"));
|
|
44116
|
-
const
|
|
44117
|
-
const slope = zip(curve, linreg(src, len, 1), (a, b) => a - b);
|
|
44528
|
+
const slope = zip(linreg(src, len, 0), linreg(src, len, 1), (a, b) => a - b);
|
|
44118
44529
|
const bull = str3(inputs, "bullColor", BULLISH);
|
|
44119
44530
|
const bear = str3(inputs, "bearColor", BEARISH);
|
|
44120
44531
|
const flat = str3(inputs, "lineColor", NEUTRAL);
|
|
44121
44532
|
const colors = slope.map((s) => Number.isFinite(s) ? s > 0 ? bull : s < 0 ? bear : flat : null);
|
|
44122
|
-
|
|
44123
|
-
|
|
44124
|
-
|
|
44125
|
-
|
|
44126
|
-
};
|
|
44127
|
-
}
|
|
44128
|
-
return { plots: [{ key: "curve", title: "Curve", values: curve, color: flat, width: 2, colors, overlay: true }] };
|
|
44533
|
+
return {
|
|
44534
|
+
plots: [{ key: "slope", title: "Slope", values: slope, color: flat, width: 2, colors }],
|
|
44535
|
+
levels: [{ key: "zero", price: 0, color: transp(NEUTRAL, 50), lineStyle: "dashed" }]
|
|
44536
|
+
};
|
|
44129
44537
|
}
|
|
44130
44538
|
};
|
|
44131
|
-
var averageSpecs = [simpleMa, exponentialMa, movingAverage, smoothedMa, zlema, vidya, maEnvelope,
|
|
44539
|
+
var averageSpecs = [simpleMa, exponentialMa, movingAverage, smoothedMa, zlema, vidya, maEnvelope, leastSquaresMa, linearRegressionSlope];
|
|
44132
44540
|
|
|
44133
44541
|
// src/core/native-indicators/classics/bands.ts
|
|
44134
44542
|
var JAW_COLOR = "#ab47bc";
|
|
@@ -48389,7 +48797,7 @@ var CellControls = class {
|
|
|
48389
48797
|
// src/widget/context-menu.ts
|
|
48390
48798
|
var PRICE_AXIS_W = 60;
|
|
48391
48799
|
var TIME_AXIS_H3 = 26;
|
|
48392
|
-
var OFF_PLOT = { price: null, time: null, paneKind: null };
|
|
48800
|
+
var OFF_PLOT = { price: null, time: null, paneKind: null, paneId: null };
|
|
48393
48801
|
var ChartContextMenu = class {
|
|
48394
48802
|
constructor(host, cbs) {
|
|
48395
48803
|
this.cbs = cbs;
|
|
@@ -48432,7 +48840,7 @@ var ChartContextMenu = class {
|
|
|
48432
48840
|
this.chart = chart;
|
|
48433
48841
|
this.crosshair = OFF_PLOT;
|
|
48434
48842
|
this.offCrosshair = chart.renderer.onCrosshairMove((e) => {
|
|
48435
|
-
this.crosshair = { price: e.price, time: e.time, paneKind: e.paneKind ?? null };
|
|
48843
|
+
this.crosshair = { price: e.price, time: e.time, paneKind: e.paneKind ?? null, paneId: e.paneId ?? null };
|
|
48436
48844
|
});
|
|
48437
48845
|
}
|
|
48438
48846
|
destroy() {
|