@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.
Files changed (37) hide show
  1. package/dist/{DataProvider-Byo8pEv0.d.ts → DataProvider-BUdlkx9I.d.ts} +1 -1
  2. package/dist/{DataProvider-Bw58CjYy.d.cts → DataProvider-DdqMxKdl.d.cts} +1 -1
  3. package/dist/{chunk-MOJ4T26N.js → chunk-BN4DNS3O.js} +27 -7
  4. package/dist/{chunk-UNUVPQFW.js → chunk-QYIO37K7.js} +482 -94
  5. package/dist/{contributions-DOm40g5-.d.cts → contributions-D8xuYc9U.d.cts} +37 -5
  6. package/dist/{contributions-BY3zSYiZ.d.ts → contributions-P9fBBuY1.d.ts} +37 -5
  7. package/dist/index.cjs +482 -94
  8. package/dist/index.d.cts +23 -7
  9. package/dist/index.d.ts +23 -7
  10. package/dist/index.js +1 -1
  11. package/dist/{options-CSXbjMM5.d.cts → options-BI5Tp32b.d.cts} +36 -1
  12. package/dist/{options-CSXbjMM5.d.ts → options-BI5Tp32b.d.ts} +36 -1
  13. package/dist/{plugin-CiQVyETg.d.cts → plugin-B8F1nor8.d.cts} +14 -8
  14. package/dist/{plugin-BDxUhGO3.d.ts → plugin-D5Z-Q6TX.d.ts} +14 -8
  15. package/dist/plugin.d.cts +4 -4
  16. package/dist/plugin.d.ts +4 -4
  17. package/dist/providers/binance.d.cts +2 -2
  18. package/dist/providers/binance.d.ts +2 -2
  19. package/dist/providers/coinbase.d.cts +2 -2
  20. package/dist/providers/coinbase.d.ts +2 -2
  21. package/dist/providers/hyperliquid.d.cts +2 -2
  22. package/dist/providers/hyperliquid.d.ts +2 -2
  23. package/dist/{statusline-model-DUBX6Fib.d.cts → statusline-model-CTwtDIca.d.cts} +3 -3
  24. package/dist/{statusline-model-C9tYXuux.d.ts → statusline-model-D9kWC9X5.d.ts} +3 -3
  25. package/dist/ui.d.cts +1 -1
  26. package/dist/ui.d.ts +1 -1
  27. package/dist/vela.global.js +482 -94
  28. package/dist/vela.global.min.js +53 -53
  29. package/dist/widget.cjs +508 -100
  30. package/dist/widget.d.cts +6 -6
  31. package/dist/widget.d.ts +6 -6
  32. package/dist/widget.js +3 -3
  33. package/dist/workspace.cjs +508 -100
  34. package/dist/workspace.d.cts +5 -5
  35. package/dist/workspace.d.ts +5 -5
  36. package/dist/workspace.js +2 -2
  37. 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 rows = pane.indicators.map((i) => ({ item: { kind: "row", row: indicatorRow(i) }, z: zOf.get(i.id) ?? 0 }));
13557
- if (pane.kind === "price") rows.push({ item: { kind: "row", row: priceRow() }, z: snap.candleZ });
13558
- rows.sort((a, b) => b.z - a.z);
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
- this.setBarSeries(bars, painted ? { preserveView: true } : void 0);
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)?.suspend();
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
- if (record.native.started) record.native.instance.stop();
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)?.suspend();
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) => this.renderer.setNativeData?.(id, data),
21759
- pushPending: (ranges) => this.renderer.setNativeData?.(`${id}-pending`, ranges)
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. Native indicators need nothing — their layers read the renderer's
21768
- * bars per frame.
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) record.native.instance.onBars();
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?.started) record.native.instance.stop();
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?.started) record.native.instance.stop();
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) => this.applyModel(id, this.buildNativeModel(record, out)),
22431
- pushData: (data) => this.renderer.setNativeData?.(channel, data),
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
- record.native.instance.start(ctx, record.inputValues);
22437
- record.native.started = true;
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
- /** Wrap a native indicator's emitted visuals into a full IndicatorModel, tagged `native`. */
22451
- buildNativeModel(record, out) {
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
- if (!this.paneOrder.includes(paneId)) this.paneOrder.push(paneId);
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 plotted value — the model is the one source every
22803
- * engine fills, so this works without an execution context. */
22804
- static plotsAtLastBar(model) {
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
- out[spec.title] = spec.points[spec.points.length - 1]?.value ?? null;
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
- ...model,
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 descriptors = items.map((it, i) => ({ id: String(i), label: it.label }));
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 (target && rowId) api.move(rowId, target);
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. One entry today: the values toggle. */
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 items = [{
24874
- id: "values",
24875
- label: "Indicator values",
24876
- checked: this.rowValuesShown(row2)
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
- if (itemId !== "values" || !this.rowMenuId) return;
24887
- const target = this.rows.get(this.rowMenuId);
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, i1, off);
26299
- for (const s of m.series) if (s.overlay !== true) this.emitSeries(b, s, mp, coords, i0, i1, theme, off);
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, i1, off);
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, i1, theme, off);
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, i1);
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 }` sorted by 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
- return [...this.seriesZ.entries()].map(([id, z]) => ({ id, z })).sort((a, b) => a.z - b.z);
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, i1, off);
28382
- for (const s of m.series) if (s.overlay !== true) this.drawSeries(ctx, s, mp, coords, i0, i1, theme, off);
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, i1, off);
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, i1, theme, off);
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, 3);
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 ?? (pane.kind === "price" ? 3 : 1));
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 event = { time, price, paneKind: inData && pane ? pane.kind : null, values, ohlc };
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
- for (const cb of this.clickCbs) cb({ time: onBar ? this.coords.logicalToTime(logical) : null, price: null });
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, i1, (id) => this.scene.offsetOf(id));
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, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id), margins);
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, i1, mdr, false, (id) => this.scene.offsetOf(id), margins);
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
- let lo = 0;
42937
- let hi = times.length;
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 LINREG_CURVE = "Curve";
44166
- var LINREG_SLOPE = "Slope";
44167
- var linearRegression = {
44168
- type: "linear-regression",
44169
- title: "Linear Regression Curve",
44170
- shortTitle: "LinReg",
44171
- // Slope lives in its own pane; Curve force-overlays itself onto the price candles.
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 regression curve and slope line while the fit slopes upward."),
44178
- colorInput(BEARISH, "Falling", "bearColor", "Color of the regression curve and slope line while the fit slopes downward."),
44179
- colorInput(NEUTRAL, "Regression Line", "lineColor", "Color of the regression curve and slope line while the fit is flat.")
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 curve = linreg(src, len, 0);
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
- if (str3(inputs, "mode", LINREG_CURVE) === LINREG_SLOPE) {
44191
- return {
44192
- plots: [{ key: "slope", title: "Slope", values: slope, color: flat, width: 2, colors }],
44193
- levels: [{ key: "zero", price: 0, color: transp(NEUTRAL, 50), lineStyle: "dashed" }]
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, linearRegression];
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() {