@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
@@ -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 rows = pane.indicators.map((i) => ({ item: { kind: "row", row: indicatorRow(i) }, z: zOf.get(i.id) ?? 0 }));
13503
- if (pane.kind === "price") rows.push({ item: { kind: "row", row: priceRow() }, z: snap.candleZ });
13504
- rows.sort((a, b) => b.z - a.z);
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
- this.setBarSeries(bars, painted ? { preserveView: true } : void 0);
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)?.suspend();
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
- if (record.native.started) record.native.instance.stop();
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)?.suspend();
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) => this.renderer.setNativeData?.(id, data),
21691
- pushPending: (ranges) => this.renderer.setNativeData?.(`${id}-pending`, ranges)
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. Native indicators need nothing — their layers read the renderer's
21700
- * bars per frame.
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) record.native.instance.onBars();
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?.started) record.native.instance.stop();
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?.started) record.native.instance.stop();
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) => this.applyModel(id, this.buildNativeModel(record, out)),
22363
- pushData: (data) => this.renderer.setNativeData?.(channel, data),
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
- record.native.instance.start(ctx, record.inputValues);
22369
- record.native.started = true;
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
- /** Wrap a native indicator's emitted visuals into a full IndicatorModel, tagged `native`. */
22383
- buildNativeModel(record, out) {
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
- if (!this.paneOrder.includes(paneId)) this.paneOrder.push(paneId);
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 plotted value — the model is the one source every
22735
- * engine fills, so this works without an execution context. */
22736
- static plotsAtLastBar(model) {
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
- out[spec.title] = spec.points[spec.points.length - 1]?.value ?? null;
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
- ...model,
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 descriptors = items.map((it, i) => ({ id: String(i), label: it.label }));
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 (target && rowId) api.move(rowId, target);
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. One entry today: the values toggle. */
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 items = [{
24806
- id: "values",
24807
- label: "Indicator values",
24808
- checked: this.rowValuesShown(row2)
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
- if (itemId !== "values" || !this.rowMenuId) return;
24819
- const target = this.rows.get(this.rowMenuId);
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, i1, off);
26231
- for (const s of m.series) if (s.overlay !== true) this.emitSeries(b, s, mp, coords, i0, i1, theme, off);
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, i1, off);
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, i1, theme, off);
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, i1);
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 }` sorted by 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
- return [...this.seriesZ.entries()].map(([id, z]) => ({ id, z })).sort((a, b) => a.z - b.z);
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, i1, off);
28314
- for (const s of m.series) if (s.overlay !== true) this.drawSeries(ctx, s, mp, coords, i0, i1, theme, off);
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, i1, off);
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, i1, theme, off);
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, 3);
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 ?? (pane.kind === "price" ? 3 : 1));
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 event = { time, price, paneKind: inData && pane ? pane.kind : null, values, ohlc };
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
- for (const cb of this.clickCbs) cb({ time: onBar ? this.coords.logicalToTime(logical) : null, price: null });
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, i1, (id) => this.scene.offsetOf(id));
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, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id), margins);
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, i1, mdr, false, (id) => this.scene.offsetOf(id), margins);
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
- let lo = 0;
42869
- let hi = times.length;
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 LINREG_CURVE = "Curve";
44098
- var LINREG_SLOPE = "Slope";
44099
- var linearRegression = {
44100
- type: "linear-regression",
44101
- title: "Linear Regression Curve",
44102
- shortTitle: "LinReg",
44103
- // Slope lives in its own pane; Curve force-overlays itself onto the price candles.
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 regression curve and slope line while the fit slopes upward."),
44110
- colorInput(BEARISH, "Falling", "bearColor", "Color of the regression curve and slope line while the fit slopes downward."),
44111
- colorInput(NEUTRAL, "Regression Line", "lineColor", "Color of the regression curve and slope line while the fit is flat.")
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 curve = linreg(src, len, 0);
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
- if (str3(inputs, "mode", LINREG_CURVE) === LINREG_SLOPE) {
44123
- return {
44124
- plots: [{ key: "slope", title: "Slope", values: slope, color: flat, width: 2, colors }],
44125
- levels: [{ key: "zero", price: 0, color: transp(NEUTRAL, 50), lineStyle: "dashed" }]
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, linearRegression];
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() {