@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
@@ -8593,6 +8593,8 @@ var Vela = (function (exports) {
8593
8593
  this.barTransform = null;
8594
8594
  // ── chart-type data engines (SDK): one lazily-created engine per style id ──
8595
8595
  this.typeEngines = /* @__PURE__ */ new Map();
8596
+ /** Engines suspended by the chart — their host pushes are dropped until they resume. */
8597
+ this.suspendedEngines = /* @__PURE__ */ new WeakSet();
8596
8598
  /** Last-seen chart-type settings per type id (dialog edits + persisted restores) —
8597
8599
  * replayed into a data engine whenever one is (re)created. */
8598
8600
  this.typeSettings = /* @__PURE__ */ new Map();
@@ -8867,12 +8869,14 @@ var Vela = (function (exports) {
8867
8869
  this.mergeHistoryIntoReplay(bars);
8868
8870
  return;
8869
8871
  }
8870
- this.setBarSeries(bars, painted ? { preserveView: true } : void 0);
8872
+ const repaint = painted;
8873
+ this.setBarSeries(bars, repaint ? { preserveView: true } : void 0);
8871
8874
  if (!painted && bars.length > 0) {
8872
8875
  painted = true;
8873
8876
  if (opts.firstLoad) this.activateBarLayers();
8874
8877
  if (!final) this.historyState = "backfill";
8875
8878
  }
8879
+ if (repaint && !final) this.notifySessionsBars("backfill");
8876
8880
  };
8877
8881
  const abort = new AbortController();
8878
8882
  this.progressiveAbort = abort;
@@ -9075,10 +9079,15 @@ var Vela = (function (exports) {
9075
9079
  this.setBarSeries([], { clearing: true });
9076
9080
  this.blankIndicatorVisuals();
9077
9081
  if (this.activeEngineStyle) {
9078
- this.typeEngines.get(this.activeEngineStyle)?.suspend();
9082
+ this.suspendTypeEngine(this.typeEngines.get(this.activeEngineStyle));
9079
9083
  this.renderer.setNativeData?.(this.activeEngineStyle, void 0);
9080
9084
  this.renderer.setNativeData?.(`${this.activeEngineStyle}-pending`, []);
9081
9085
  }
9086
+ for (const record of this.registry.all()) {
9087
+ if (!record.native?.started) continue;
9088
+ this.stopNativeInstance(record.native);
9089
+ this.renderer.setNativeData?.(this.nativeChannel(record), void 0);
9090
+ }
9082
9091
  }
9083
9092
  if (depthOnly) {
9084
9093
  this.extendDepth(gen, m.bars ?? 500);
@@ -9105,6 +9114,7 @@ var Vela = (function (exports) {
9105
9114
  this.restartChartTypeEngine();
9106
9115
  this.reexecuteIndicators();
9107
9116
  }
9117
+ if (depthOnly) this.releaseHeldNatives();
9108
9118
  if (!replayBack) this.startLive();
9109
9119
  if (identityChanged) {
9110
9120
  this.events.emit("market:changed", { symbol: m.symbol ?? "TEST", timeframe: m.timeframe ?? "60", prev: prev3 });
@@ -9121,9 +9131,8 @@ var Vela = (function (exports) {
9121
9131
  restartNativeIndicators() {
9122
9132
  for (const record of this.registry.all()) {
9123
9133
  if (!record.native) continue;
9124
- if (record.native.started) record.native.instance.stop();
9134
+ this.stopNativeInstance(record.native);
9125
9135
  record.native.instance = record.native.descriptor.create();
9126
- record.native.started = false;
9127
9136
  record.pendingStructural = true;
9128
9137
  if (record.hidden) {
9129
9138
  record.native.stale = true;
@@ -9140,6 +9149,21 @@ var Vela = (function (exports) {
9140
9149
  this.typeEngines.clear();
9141
9150
  if (this.activeEngineStyle) void this.startChartTypeEngine(this.activeEngineStyle);
9142
9151
  }
9152
+ /** Suspend a chart-type engine. Its host drops every push until the engine resumes:
9153
+ * work it left in flight lands in its own state, never on the scene. */
9154
+ suspendTypeEngine(engine) {
9155
+ if (!engine) return;
9156
+ engine.suspend();
9157
+ this.suspendedEngines.add(engine);
9158
+ }
9159
+ /** Stop a running native instance, then revoke its context: the teardown inside `stop()`
9160
+ * still reaches its layer, but work it left in flight (a fetch resolving later) does not. */
9161
+ stopNativeInstance(native) {
9162
+ if (native.started) native.instance.stop();
9163
+ native.started = false;
9164
+ native.revokeContext?.();
9165
+ native.revokeContext = void 0;
9166
+ }
9143
9167
  /**
9144
9168
  * The FIRST candles just painted (the quick preview, or the only load): activate the
9145
9169
  * layers that need bars but NOT full history. The volume layer reads the chart's bars
@@ -9672,7 +9696,7 @@ var Vela = (function (exports) {
9672
9696
  const def = chartType(id);
9673
9697
  const want = def?.dataEngine ? id : null;
9674
9698
  if (want === this.activeEngineStyle) return;
9675
- if (this.activeEngineStyle) this.typeEngines.get(this.activeEngineStyle)?.suspend();
9699
+ if (this.activeEngineStyle) this.suspendTypeEngine(this.typeEngines.get(this.activeEngineStyle));
9676
9700
  this.activeEngineStyle = want;
9677
9701
  if (want) void this.startChartTypeEngine(want);
9678
9702
  }
@@ -9683,6 +9707,7 @@ var Vela = (function (exports) {
9683
9707
  if (this.activeEngineStyle !== id) return;
9684
9708
  const existing = this.typeEngines.get(id);
9685
9709
  if (existing) {
9710
+ this.suspendedEngines.delete(existing);
9686
9711
  existing.resume();
9687
9712
  return;
9688
9713
  }
@@ -9692,6 +9717,7 @@ var Vela = (function (exports) {
9692
9717
  this.typeEngines.set(id, engine);
9693
9718
  const stored = this.typeSettings.get(id);
9694
9719
  if (stored) engine.onSettings?.(stored);
9720
+ const reachesScene = () => this.typeEngines.get(id) === engine && !this.suspendedEngines.has(engine);
9695
9721
  engine.start({
9696
9722
  symbol: this.config.market.symbol ?? "TEST",
9697
9723
  timeframe: this.config.market.timeframe ?? "60",
@@ -9699,8 +9725,12 @@ var Vela = (function (exports) {
9699
9725
  session: this.config.market.session,
9700
9726
  bars: () => this.bars,
9701
9727
  data: this.dataControl,
9702
- pushData: (data) => this.renderer.setNativeData?.(id, data),
9703
- pushPending: (ranges) => this.renderer.setNativeData?.(`${id}-pending`, ranges)
9728
+ pushData: (data) => {
9729
+ if (reachesScene()) this.renderer.setNativeData?.(id, data);
9730
+ },
9731
+ pushPending: (ranges) => {
9732
+ if (reachesScene()) this.renderer.setNativeData?.(`${id}-pending`, ranges);
9733
+ }
9704
9734
  });
9705
9735
  }
9706
9736
  /**
@@ -9708,8 +9738,9 @@ var Vela = (function (exports) {
9708
9738
  * a pre-load style change is picked up by init's `setBarSeries`. With bars loaded, the
9709
9739
  * view is rebuilt in place (viewport preserved) and every Pine indicator re-executes:
9710
9740
  * its input data changed wholesale, which a streaming session's incremental context
9711
- * cannot absorb. Native indicators need nothing — their layers read the renderer's
9712
- * bars per frame.
9741
+ * cannot absorb. Natives computed from `ctx.bars()` re-run over the new candles (a
9742
+ * synthetic price style is their source too); layer natives that read the renderer's
9743
+ * bars per frame ignore the poke.
9713
9744
  */
9714
9745
  syncBarTransform(style) {
9715
9746
  const transform = barTransformFor(style);
@@ -9719,6 +9750,10 @@ var Vela = (function (exports) {
9719
9750
  this.bars = transform ? transform.full(this.rawBars) : this.rawBars;
9720
9751
  this.renderer.setBars(this.bars, { preserveView: true });
9721
9752
  this.reexecuteIndicators();
9753
+ if (this.switchingMarket) return;
9754
+ for (const record of this.registry.all()) {
9755
+ if (record.native?.started && record.renderHandle && !record.hidden) this.pokeNative(record);
9756
+ }
9722
9757
  }
9723
9758
  /**
9724
9759
  * Blank every mounted indicator's painted output — series data, fills, backgrounds,
@@ -9819,10 +9854,39 @@ var Vela = (function (exports) {
9819
9854
  if (record.session) {
9820
9855
  record.pendingCause = cause;
9821
9856
  record.session.notifyBars(reason);
9822
- } else if (record.native) record.native.instance.onBars();
9857
+ } else if (record.native) this.pokeNative(record);
9823
9858
  }
9824
9859
  if (this.activeEngineStyle) this.typeEngines.get(this.activeEngineStyle)?.onBars?.();
9825
9860
  }
9861
+ /**
9862
+ * Tell a native its bars changed. A `waitsForHistory` native ignores every change while
9863
+ * the history is still loading — a backfill chunk, a deeper snapshot, a tick arriving
9864
+ * meanwhile — and its output stays on its bars through the anchor its emit carried.
9865
+ * Once the history is complete, a held one starts and a running one re-runs.
9866
+ */
9867
+ pokeNative(record) {
9868
+ const native = record.native;
9869
+ if (!native.descriptor.waitsForHistory) {
9870
+ native.instance.onBars();
9871
+ return;
9872
+ }
9873
+ if (this.historyState === "backfill") return;
9874
+ if (native.heldForHistory) {
9875
+ native.heldForHistory = false;
9876
+ const handle = this.handles.get(record.id);
9877
+ if (handle) void this.startNativeIndicator(record.id, handle);
9878
+ return;
9879
+ }
9880
+ if (native.started) native.instance.onBars();
9881
+ }
9882
+ /** Start the natives still held for a history that has completed without poking them
9883
+ * (a depth-only switch finishes its trim while the switch gates every poke). */
9884
+ releaseHeldNatives() {
9885
+ if (this.historyState === "backfill") return;
9886
+ for (const record of this.registry.all()) {
9887
+ if (record.native?.heldForHistory && !record.hidden) this.pokeNative(record);
9888
+ }
9889
+ }
9826
9890
  /** Gap healing needs a ranged feed and a provider-backed series (offline `data` has no source). */
9827
9891
  canHeal() {
9828
9892
  return !!this.feed.loadRange && !this.config.market.data?.length;
@@ -10114,7 +10178,7 @@ var Vela = (function (exports) {
10114
10178
  if (record?.native?.type === "volume") this.volumeOptedOut = true;
10115
10179
  if (record) this.settleCode(record, { ok: false, error: new Error(`[vela] updateCode("${id}") \u2014 the indicator was removed.`) });
10116
10180
  record?.session?.stop();
10117
- if (record?.native?.started) record.native.instance.stop();
10181
+ if (record?.native) this.stopNativeInstance(record.native);
10118
10182
  this.handles.delete(id);
10119
10183
  if (record?.renderHandle) this.renderer.removeIndicator(record.renderHandle);
10120
10184
  const paneId = record?.model?.paneId;
@@ -10235,7 +10299,7 @@ var Vela = (function (exports) {
10235
10299
  for (const record of this.registry.all()) {
10236
10300
  this.settleCode(record, { ok: false, error: new Error("[vela] updateCode \u2014 the chart was destroyed.") });
10237
10301
  record.session?.stop();
10238
- if (record.native?.started) record.native.instance.stop();
10302
+ if (record.native) this.stopNativeInstance(record.native);
10239
10303
  }
10240
10304
  for (const engine of this.typeEngines.values()) engine.stop();
10241
10305
  this.typeEngines.clear();
@@ -10362,7 +10426,19 @@ var Vela = (function (exports) {
10362
10426
  await this.readyPromise;
10363
10427
  const record = this.registry.get(id);
10364
10428
  if (!record?.native || record.hidden) return;
10429
+ const native = record.native;
10430
+ if (native.started) return;
10431
+ if (native.descriptor.waitsForHistory && this.historyState === "backfill") {
10432
+ native.heldForHistory = true;
10433
+ this.mountNativePlaceholder(id, record);
10434
+ return;
10435
+ }
10436
+ native.heldForHistory = false;
10365
10437
  const channel = this.nativeChannel(record);
10438
+ let alive = true;
10439
+ native.revokeContext = () => {
10440
+ alive = false;
10441
+ };
10366
10442
  const ctx = {
10367
10443
  id,
10368
10444
  symbol: this.config.market.symbol ?? "TEST",
@@ -10371,14 +10447,18 @@ var Vela = (function (exports) {
10371
10447
  session: this.config.market.session,
10372
10448
  bars: () => this.bars,
10373
10449
  data: this.dataControl,
10374
- emit: (out) => this.applyModel(id, this.buildNativeModel(record, out)),
10375
- pushData: (data) => this.renderer.setNativeData?.(channel, data),
10450
+ emit: (out) => {
10451
+ if (alive) this.applyModel(id, this.buildNativeModel(record, out, this.bars[0]?.time));
10452
+ },
10453
+ pushData: (data) => {
10454
+ if (alive) this.renderer.setNativeData?.(channel, data);
10455
+ },
10376
10456
  setStatus: (status) => {
10377
- if (record.renderHandle && !record.hidden) this.renderer.setIndicatorStatus?.(record.renderHandle, status);
10457
+ if (alive && record.renderHandle && !record.hidden) this.renderer.setIndicatorStatus?.(record.renderHandle, status);
10378
10458
  }
10379
10459
  };
10380
- record.native.instance.start(ctx, record.inputValues);
10381
- record.native.started = true;
10460
+ native.instance.start(ctx, record.inputValues);
10461
+ native.started = true;
10382
10462
  } catch (err) {
10383
10463
  this.fail(id, handle, err);
10384
10464
  }
@@ -10391,8 +10471,14 @@ var Vela = (function (exports) {
10391
10471
  const { type, descriptor } = record.native;
10392
10472
  return descriptor.multiInstance ? nativeInstanceChannel(type, record.id) : type;
10393
10473
  }
10394
- /** Wrap a native indicator's emitted visuals into a full IndicatorModel, tagged `native`. */
10395
- buildNativeModel(record, out) {
10474
+ /**
10475
+ * Wrap a native indicator's emitted visuals into a full IndicatorModel, tagged `native`.
10476
+ * `anchorTime` is the first bar the output is index-aligned to — the chart's first bar
10477
+ * when it emitted. Without it the renderer pins the output's first slot to whatever the
10478
+ * first bar is NOW, so history landing in front of a computed output (or the oldest
10479
+ * bars trimmed under it) would slide the whole plot by that many bars.
10480
+ */
10481
+ buildNativeModel(record, out, anchorTime) {
10396
10482
  const d = record.native.descriptor;
10397
10483
  const type = record.native.type;
10398
10484
  const channel = this.nativeChannel(record);
@@ -10403,6 +10489,7 @@ var Vela = (function (exports) {
10403
10489
  overlay: d.overlay,
10404
10490
  paneHint: d.paneHint,
10405
10491
  native: { type, ...channel !== type ? { channel } : {} },
10492
+ ...anchorTime != null ? { anchorTime } : {},
10406
10493
  ...d.legend === false ? { legend: false } : {},
10407
10494
  ...out.paneAxis != null ? { paneAxis: out.paneAxis } : {},
10408
10495
  series: out.series ?? [],
@@ -10457,8 +10544,9 @@ var Vela = (function (exports) {
10457
10544
  const paneId = this.routePane(id, model, record.options ?? {});
10458
10545
  this.placeModel(model, id, paneId);
10459
10546
  record.model = model;
10460
- this.ensurePaneFor(paneId);
10547
+ const paneCreated = this.ensurePaneFor(paneId);
10461
10548
  record.renderHandle = this.renderer.mountIndicator(model);
10549
+ if (paneCreated) this.events.emit("pane:changed", void 0);
10462
10550
  record.pendingStructural = true;
10463
10551
  if (record.hidden) {
10464
10552
  this.renderer.setIndicatorVisible?.(record.renderHandle, false);
@@ -10482,24 +10570,49 @@ var Vela = (function (exports) {
10482
10570
  const paneId = this.routePane(id, model, record.options ?? {});
10483
10571
  this.placeModel(model, id, paneId);
10484
10572
  record.model = model;
10485
- this.ensurePaneFor(paneId);
10573
+ const paneCreated = this.ensurePaneFor(paneId);
10486
10574
  record.renderHandle = this.renderer.mountIndicator(model);
10575
+ if (paneCreated) this.events.emit("pane:changed", void 0);
10487
10576
  record.pendingStructural = true;
10488
10577
  this.renderer.setIndicatorVisible?.(record.renderHandle, false);
10489
10578
  this.announce(record, this.handles.get(id));
10490
10579
  }
10580
+ /**
10581
+ * The legend row of a native HELD for the history (see `waitsForHistory`): an empty
10582
+ * model with the title and inputs schema, spinning, not announced — exactly a script
10583
+ * indicator's loading placeholder, so `ready`/`indicator:added` and `inspect()` still
10584
+ * mean "it produced output". A record that already has a row (restarted over a new
10585
+ * market, or shown again) keeps it and only spins. The first emit remounts over it.
10586
+ */
10587
+ mountNativePlaceholder(id, record) {
10588
+ record.pendingStructural = true;
10589
+ if (!record.renderHandle) {
10590
+ const model = this.buildNativeModel(record, {});
10591
+ const paneId = this.routePane(id, model, record.options ?? {});
10592
+ this.placeModel(model, id, paneId);
10593
+ record.model = model;
10594
+ const paneCreated = this.ensurePaneFor(paneId);
10595
+ record.renderHandle = this.renderer.mountIndicator(model);
10596
+ if (paneCreated) this.events.emit("pane:changed", void 0);
10597
+ }
10598
+ this.setLoading(record, true);
10599
+ }
10491
10600
  /** Flip the record's loading state and reflect it in the legend row (spinner on/off). */
10492
10601
  setLoading(record, loading) {
10493
10602
  record.loading = loading;
10494
10603
  if (record.renderHandle && !record.hidden) this.renderer.setIndicatorStatus?.(record.renderHandle, loading ? "loading" : "idle");
10495
10604
  }
10605
+ /** Make sure the renderer has the pane; true when it is new to the layout (the caller
10606
+ * emits `pane:changed` once the indicator's model sits on it, so `list()` shows it). */
10496
10607
  ensurePaneFor(paneId) {
10497
- if (!this.paneOrder.includes(paneId)) this.paneOrder.push(paneId);
10608
+ const created = !this.paneOrder.includes(paneId);
10609
+ if (created) this.paneOrder.push(paneId);
10498
10610
  this.renderer.ensurePane({
10499
10611
  id: paneId,
10500
10612
  kind: paneId === "price" ? "price" : "study",
10501
10613
  order: this.paneOrder.indexOf(paneId)
10502
10614
  });
10615
+ return created;
10503
10616
  }
10504
10617
  /** True when any (still-registered) indicator sits on the given pane. */
10505
10618
  paneStillUsed(paneId) {
@@ -10535,12 +10648,14 @@ var Vela = (function (exports) {
10535
10648
  const panes = [];
10536
10649
  for (const paneId of this.paneOrder) {
10537
10650
  if (paneId !== "price" && !byPane.has(paneId)) continue;
10651
+ const heightWeight = this.renderer.paneHeightWeight?.(paneId) ?? null;
10538
10652
  panes.push({
10539
10653
  id: paneId,
10540
10654
  kind: paneId === "price" ? "price" : "study",
10541
10655
  order: panes.length,
10542
10656
  collapsed: this.collapsedPanes.has(paneId),
10543
10657
  maximized: this.maximizedPaneId === paneId,
10658
+ ...heightWeight !== null ? { heightWeight } : {},
10544
10659
  indicators: byPane.get(paneId) ?? []
10545
10660
  });
10546
10661
  }
@@ -10572,6 +10687,22 @@ var Vela = (function (exports) {
10572
10687
  this.renderer.setPaneMaximized?.(paneId);
10573
10688
  this.events.emit("pane:changed", void 0);
10574
10689
  }
10690
+ setPaneHeightWeight(paneId, weight) {
10691
+ if (!this.renderer.setPaneHeightWeight) {
10692
+ console.warn("[vela] the active renderer does not support pane sizing \u2014 chart.panes.setHeightWeight ignored.");
10693
+ return;
10694
+ }
10695
+ if (typeof weight !== "number" || !Number.isFinite(weight) || weight <= 0) {
10696
+ console.warn(`[vela] chart.panes.setHeightWeight: weight must be a positive number (got ${String(weight)}).`);
10697
+ return;
10698
+ }
10699
+ if (!this.existingPaneIds().includes(paneId)) {
10700
+ console.warn(`[vela] chart.panes.setHeightWeight: no pane "${paneId}".`);
10701
+ return;
10702
+ }
10703
+ this.renderer.setPaneHeightWeight(paneId, weight);
10704
+ this.events.emit("pane:changed", void 0);
10705
+ }
10575
10706
  /** IndicatorController/PaneController: move (or merge) an indicator to another pane. */
10576
10707
  moveIndicator(id, target) {
10577
10708
  if (!this.paneManagementSupported() || !this.renderer.setIndicatorPane) {
@@ -10669,6 +10800,9 @@ var Vela = (function (exports) {
10669
10800
  this.maximizedPaneId = a.maximized ? a.paneId : null;
10670
10801
  this.events.emit("pane:changed", void 0);
10671
10802
  break;
10803
+ case "resize":
10804
+ this.events.emit("pane:changed", void 0);
10805
+ break;
10672
10806
  }
10673
10807
  }
10674
10808
  /** Fire `indicator:added` + the handle's `ready` once, on the first computed model. */
@@ -10699,14 +10833,16 @@ var Vela = (function (exports) {
10699
10833
  const paneId2 = this.routePane(id, model, record.options ?? {});
10700
10834
  this.placeModel(model, id, paneId2);
10701
10835
  record.model = model;
10702
- this.ensurePaneFor(paneId2);
10836
+ const paneCreated = this.ensurePaneFor(paneId2);
10703
10837
  record.renderHandle = this.renderer.mountIndicator(model);
10704
10838
  record.pendingStructural = false;
10839
+ if (paneCreated) this.events.emit("pane:changed", void 0);
10705
10840
  this.announce(record, handle);
10706
10841
  return true;
10707
10842
  }
10708
10843
  let paneId = record.model?.paneId ?? "price";
10709
10844
  const prevOwnScale = record.model?.ownScale === true;
10845
+ let rerouted = false;
10710
10846
  if (record.loading && !record.paneLocked) {
10711
10847
  const routed = this.routePane(id, model, record.options ?? {});
10712
10848
  if (routed !== paneId) {
@@ -10717,6 +10853,7 @@ var Vela = (function (exports) {
10717
10853
  this.ensurePaneFor(routed);
10718
10854
  record.pendingStructural = true;
10719
10855
  paneId = routed;
10856
+ rerouted = true;
10720
10857
  }
10721
10858
  }
10722
10859
  this.placeModel(model, id, paneId);
@@ -10729,6 +10866,7 @@ var Vela = (function (exports) {
10729
10866
  } else {
10730
10867
  this.renderer.updateIndicator(record.renderHandle, modelToValuePatch(model));
10731
10868
  }
10869
+ if (rerouted) this.events.emit("pane:changed", void 0);
10732
10870
  if (record.loading) this.setLoading(record, false);
10733
10871
  this.announce(record, handle);
10734
10872
  return true;
@@ -10743,13 +10881,16 @@ var Vela = (function (exports) {
10743
10881
  const overlay = options.overlay ?? model.overlay;
10744
10882
  return overlay ? "price" : `pane-${id}`;
10745
10883
  }
10746
- /** Each named value series' last plotted value — the model is the one source every
10747
- * engine fills, so this works without an execution context. */
10748
- static plotsAtLastBar(model) {
10884
+ /** Each named value series' value on the chart's last bar — the model is the one source
10885
+ * every engine fills, so this works without an execution context. Points past the last
10886
+ * bar (a series shifted forward) are skipped by time. */
10887
+ static plotsAtLastBar(model, lastBarTime) {
10749
10888
  const out = {};
10750
10889
  for (const spec of model.series) {
10751
10890
  if (!isLineLikeSeries(spec)) continue;
10752
- out[spec.title] = spec.points[spec.points.length - 1]?.value ?? null;
10891
+ let i = spec.points.length - 1;
10892
+ if (lastBarTime !== void 0) while (i >= 0 && spec.points[i].time > lastBarTime) i -= 1;
10893
+ out[spec.title] = spec.points[i]?.value ?? null;
10753
10894
  }
10754
10895
  return out;
10755
10896
  }
@@ -10805,7 +10946,7 @@ var Vela = (function (exports) {
10805
10946
  // whatever produced this run.
10806
10947
  forming: this.config.live && this.bars.length > 0,
10807
10948
  complete: this.historyState !== "backfill",
10808
- plots: _EngineOrchestrator.plotsAtLastBar(model),
10949
+ plots: _EngineOrchestrator.plotsAtLastBar(model, lastBar?.time),
10809
10950
  vars: snapshot2?.variables ?? {},
10810
10951
  ...snapshot2?.strategy ? { strategy: snapshot2.strategy } : {},
10811
10952
  warnings: snapshot2?.warnings ?? [],
@@ -10874,8 +11015,9 @@ var Vela = (function (exports) {
10874
11015
  return typeof a === "object" && a !== null && typeof b === "object" && b !== null && JSON.stringify(a) === JSON.stringify(b);
10875
11016
  }
10876
11017
  function blankedModel(model) {
11018
+ const { anchorTime: _anchor, ...rest } = model;
10877
11019
  return {
10878
- ...model,
11020
+ ...rest,
10879
11021
  series: model.series.map((s) => s.kind === "candle" || s.kind === "bar" ? { ...s, bars: [] } : s.kind === "markers" ? { ...s, markers: [] } : { ...s, points: [] }),
10880
11022
  fills: [],
10881
11023
  backgrounds: [],
@@ -10898,12 +11040,14 @@ var Vela = (function (exports) {
10898
11040
  if (s.kind === "candle" || s.kind === "bar") {
10899
11041
  series.push({ seriesId: s.id, kind: "bars", bars: s.bars, barColors: s.barColors ?? [] });
10900
11042
  for (const b of s.bars) {
11043
+ if (!b) continue;
10901
11044
  if (b.time < from) from = b.time;
10902
11045
  if (b.time > to) to = b.time;
10903
11046
  }
10904
11047
  } else if (isLineLikeSeries(s)) {
10905
11048
  series.push({ seriesId: s.id, kind: "points", points: s.points });
10906
11049
  for (const p of s.points) {
11050
+ if (!p) continue;
10907
11051
  if (p.time < from) from = p.time;
10908
11052
  if (p.time > to) to = p.time;
10909
11053
  }
@@ -11218,6 +11362,17 @@ var Vela = (function (exports) {
11218
11362
  if (this.guard()) this.controller.maximizePane(paneId);
11219
11363
  return this;
11220
11364
  }
11365
+ /**
11366
+ * Set a pane's height weight: the panes share the plot height in proportion to their
11367
+ * weights, so `setHeightWeight('price', 6)` next to two study panes at 1 gives the price
11368
+ * pane three quarters of the height. The weight sticks until the user drags a separator
11369
+ * or it is set again; a collapsed pane applies it when expanded. `weight` must be a
11370
+ * positive number.
11371
+ */
11372
+ setHeightWeight(paneId, weight) {
11373
+ if (this.guard()) this.controller.setPaneHeightWeight(paneId, weight);
11374
+ return this;
11375
+ }
11221
11376
  /** Move/merge an indicator to a pane — sugar for `handle.moveTo(...)` by id. */
11222
11377
  moveIndicator(id, target) {
11223
11378
  if (this.guard()) this.controller.moveIndicator(id, target);
@@ -23982,6 +24137,8 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
23982
24137
  var LEGEND_ACTION_GAP_PX = 6;
23983
24138
  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;`;
23984
24139
  var LEGEND_ROW_MIN_H = LEGEND_CTL_PX + LEGEND_ROW_PAD_Y * 2;
24140
+ var STACK_RAISE = "stack:raise";
24141
+ var STACK_LOWER = "stack:lower";
23985
24142
  var EYE_SVG = iconAt("eye", LEGEND_ICON_PX2);
23986
24143
  var EYE_OFF_SVG = iconAt("eye-off", LEGEND_ICON_PX2);
23987
24144
  var GEAR_SVG = iconAt("gear", LEGEND_ICON_PX2);
@@ -24011,6 +24168,10 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
24011
24168
  this.mobileLayout = false;
24012
24169
  /** Pane move/merge hook — when set, rows get a "Move to" menu + become drag-to-pane sources. */
24013
24170
  this.moveApi = null;
24171
+ /** Overlay restacking hook — when set, a row's menus offer to step it forward or back. */
24172
+ this.stackApi = null;
24173
+ /** What {@link orderRows} last ordered the rows by (skips unchanged frames). */
24174
+ this.rowOrderKey = "";
24014
24175
  /** Host-contributed legend actions, resolved PER ROW at render time (see setLegendActions). */
24015
24176
  this.legendActions = null;
24016
24177
  /** Host-contributed callout bubbles, resolved PER ROW at render time (see setLegendCallouts). */
@@ -24149,6 +24310,10 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
24149
24310
  setMoveApi(api) {
24150
24311
  this.moveApi = api;
24151
24312
  }
24313
+ /** Enable the context menu's "Bring in front of …" entry for overlay indicators. Null disables it. */
24314
+ setStackApi(api) {
24315
+ this.stackApi = api;
24316
+ }
24152
24317
  /**
24153
24318
  * Wire the host-contributed row actions. Re-calling replaces the provider and
24154
24319
  * re-projects the rows already on screen (a late `registerLegendAction` appears after
@@ -24294,16 +24459,20 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
24294
24459
  }
24295
24460
  this.moveTargets = items.map((it) => it.target);
24296
24461
  this.moveMenuId = id;
24297
- const descriptors = items.map((it, i) => ({ id: String(i), label: it.label }));
24462
+ const stack = this.stackItems(id);
24463
+ const moves = items.map((it, i) => ({ id: String(i), label: it.label }));
24464
+ if (moves[0] && stack.length > 0) moves[0].separatorBefore = true;
24465
+ const descriptors = [...stack, ...moves];
24298
24466
  if (!this.moveMenu) {
24299
24467
  this.moveMenu = new Menu({
24300
24468
  host: this.container,
24301
24469
  items: descriptors,
24302
24470
  placement: "bottom-start",
24303
24471
  onSelect: (itemId) => {
24304
- const target = this.moveTargets[Number(itemId)];
24305
24472
  const rowId = this.moveMenuId;
24306
- if (target && rowId) api.move(rowId, target);
24473
+ if (!rowId || this.runStackItem(itemId, rowId)) return;
24474
+ const target = this.moveTargets[Number(itemId)];
24475
+ if (target) api.move(rowId, target);
24307
24476
  }
24308
24477
  });
24309
24478
  } else {
@@ -24316,18 +24485,24 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
24316
24485
  this.moveMenu?.close();
24317
24486
  }
24318
24487
  // ── legend row context menu (right-click) ───────────────────────────────
24319
- /** Open the row's action menu at the pointer. One entry today: the values toggle. */
24488
+ /** Open the row's action menu at the pointer: an overlay indicator's restacking steps
24489
+ * first (where a layer is there to pass), then the values toggle. */
24320
24490
  openRowMenu(id, x, y) {
24321
24491
  this.closeRowMenu();
24322
24492
  this.closeMoveMenu();
24323
24493
  const row2 = this.rows.get(id);
24324
24494
  if (!row2) return;
24325
24495
  this.rowMenuId = id;
24326
- const items = [{
24327
- id: "values",
24328
- label: "Indicator values",
24329
- checked: this.rowValuesShown(row2)
24330
- }];
24496
+ const stack = this.stackItems(id);
24497
+ const items = [
24498
+ ...stack,
24499
+ {
24500
+ id: "values",
24501
+ label: "Indicator values",
24502
+ checked: this.rowValuesShown(row2),
24503
+ separatorBefore: stack.length > 0
24504
+ }
24505
+ ];
24331
24506
  if (!this.rowMenu) {
24332
24507
  this.rowMenu = new Menu({
24333
24508
  host: this.container,
@@ -24336,8 +24511,10 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
24336
24511
  // Right-click action menu — checked state reads as a leading ✓ (see the kit Menu).
24337
24512
  checkmarks: true,
24338
24513
  onSelect: (itemId) => {
24339
- if (itemId !== "values" || !this.rowMenuId) return;
24340
- const target = this.rows.get(this.rowMenuId);
24514
+ const rowId = this.rowMenuId;
24515
+ if (!rowId || this.runStackItem(itemId, rowId)) return;
24516
+ if (itemId !== "values") return;
24517
+ const target = this.rows.get(rowId);
24341
24518
  if (!target) return;
24342
24519
  target.showValues = !this.rowValuesShown(target);
24343
24520
  this.renderRowValues(target);
@@ -24351,6 +24528,55 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
24351
24528
  closeRowMenu() {
24352
24529
  this.rowMenu?.close();
24353
24530
  }
24531
+ /** A row's one-step restacking entries (forward, then back), for whichever neighbor exists. */
24532
+ stackItems(id) {
24533
+ const api = this.stackApi;
24534
+ if (!api) return [];
24535
+ const items = [];
24536
+ const forward = this.stackLabel("Bring in front of", api.above(id), "the indicator in front");
24537
+ if (forward) items.push({ id: STACK_RAISE, label: forward });
24538
+ const back = this.stackLabel("Send behind", api.below(id), "the indicator behind");
24539
+ if (back) items.push({ id: STACK_LOWER, label: back });
24540
+ return items;
24541
+ }
24542
+ stackLabel(verb, neighbor, fallback) {
24543
+ if (neighbor === null) return null;
24544
+ if (neighbor === "price") return `${verb} price`;
24545
+ return `${verb} ${this.rows.get(neighbor.indicator)?.title ?? fallback}`;
24546
+ }
24547
+ /** Run a restacking entry; false when `itemId` is not one. */
24548
+ runStackItem(itemId, rowId) {
24549
+ if (itemId === STACK_RAISE) this.stackApi?.raise(rowId);
24550
+ else if (itemId === STACK_LOWER) this.stackApi?.lower(rowId);
24551
+ else return false;
24552
+ return true;
24553
+ }
24554
+ /**
24555
+ * List each pane's rows front-most first, the way the indicators stack (the object
24556
+ * tree's order). `backToFront` is the paint order; rows it doesn't name keep their
24557
+ * place after the others. Cheap to call every frame: no-op while nothing changed.
24558
+ */
24559
+ orderRows(backToFront) {
24560
+ const key = `${backToFront.join(",")}|${[...this.rows.values()].map((r) => `${r.id}@${r.paneId}`).join(",")}`;
24561
+ if (key === this.rowOrderKey) return;
24562
+ this.rowOrderKey = key;
24563
+ const rank = new Map(backToFront.map((id, i) => [id, i]));
24564
+ for (const lg of this.legends.values()) {
24565
+ const els = [...this.rows.values()].filter((r) => r.el.parentElement === lg).map((r) => r.el);
24566
+ const dom = [...lg.children];
24567
+ const ofEl = new Map([...this.rows.values()].map((r) => [r.el, r.id]));
24568
+ const at = (el) => rank.get(ofEl.get(el)) ?? -1;
24569
+ els.sort((a, b) => {
24570
+ const ra = at(a);
24571
+ const rb = at(b);
24572
+ if (ra >= 0 && rb >= 0) return rb - ra;
24573
+ if (ra >= 0 || rb >= 0) return ra >= 0 ? -1 : 1;
24574
+ return dom.indexOf(a) - dom.indexOf(b);
24575
+ });
24576
+ const anchor = this.foldToggle?.parentElement === lg ? this.foldToggle : null;
24577
+ for (const el of els) lg.insertBefore(el, anchor);
24578
+ }
24579
+ }
24354
24580
  /** Move a legend row to another pane (indicator merged/moved), tidying an emptied container. */
24355
24581
  setPane(id, paneId) {
24356
24582
  const row2 = this.rows.get(id);
@@ -26041,6 +26267,7 @@ void main() {
26041
26267
  const i0 = Math.max(0, Math.floor(vr.from));
26042
26268
  const i1 = Math.min(n - 1, Math.ceil(vr.to));
26043
26269
  if (i1 < i0) return;
26270
+ const s1 = Math.max(i1, Math.ceil(vr.to));
26044
26271
  gl.enable(gl.BLEND);
26045
26272
  gl.enable(gl.SCISSOR_TEST);
26046
26273
  const barColorMap = mergeBarColors(scene.indicators);
@@ -26099,8 +26326,8 @@ void main() {
26099
26326
  b.alpha = this.modelAlpha;
26100
26327
  const off = scene.offsetOf(m.id);
26101
26328
  const mp = effPane(m);
26102
- for (const f of m.fills) if (f.overlay !== true) this.emitFill(b, m, f, mp, coords, i0, i1, off);
26103
- for (const s of m.series) if (s.overlay !== true) this.emitSeries(b, s, mp, coords, i0, i1, theme, off);
26329
+ for (const f of m.fills) if (f.overlay !== true) this.emitFill(b, m, f, mp, coords, i0, s1, off);
26330
+ for (const s of m.series) if (s.overlay !== true) this.emitSeries(b, s, mp, coords, i0, s1, theme, off);
26104
26331
  }
26105
26332
  if (drawCandles && !candleDrawn) {
26106
26333
  drawSlicesUpTo(scene.candleZ);
@@ -26111,11 +26338,11 @@ void main() {
26111
26338
  if (isPrice) {
26112
26339
  for (const m of scene.indicators.values()) {
26113
26340
  const off = scene.offsetOf(m.id);
26114
- for (const f of m.fills) if (f.overlay === true) this.emitFill(b, m, f, pane, coords, i0, i1, off);
26341
+ for (const f of m.fills) if (f.overlay === true) this.emitFill(b, m, f, pane, coords, i0, s1, off);
26115
26342
  }
26116
26343
  for (const m of scene.indicators.values()) {
26117
26344
  const off = scene.offsetOf(m.id);
26118
- for (const s of m.series) if (s.overlay === true) this.emitSeries(b, s, pane, coords, i0, i1, theme, off);
26345
+ for (const s of m.series) if (s.overlay === true) this.emitSeries(b, s, pane, coords, i0, s1, theme, off);
26119
26346
  }
26120
26347
  }
26121
26348
  drawSlicesUpTo(Infinity);
@@ -26137,7 +26364,7 @@ void main() {
26137
26364
  if (this.glow > 0 && !this.screenProgram && !this.ensureScreenPipeline()) this.glow = 0;
26138
26365
  if (this.glow > 0 && this.screenProgram) {
26139
26366
  this.glowBatch.alpha = this.modelAlpha;
26140
- this.renderGlow(gl, scene, coords, i0, i1);
26367
+ this.renderGlow(gl, scene, coords, i0, s1);
26141
26368
  }
26142
26369
  }
26143
26370
  /**
@@ -26694,7 +26921,7 @@ void main() {
26694
26921
  const tickW = Math.max(1, Math.round(spacing * 0.35));
26695
26922
  for (let i = i0; i <= i1; i += 1) {
26696
26923
  const bar = s.bars[i - off];
26697
- if (!bar) continue;
26924
+ if (!bar || !Number.isFinite(bar.open + bar.high + bar.low + bar.close)) continue;
26698
26925
  const bc = s.barColors?.[i - off];
26699
26926
  const body = parseColor(bc?.color ?? (bar.close >= bar.open ? up : down));
26700
26927
  const x = Math.round(coords.logicalToX(i));
@@ -27347,6 +27574,8 @@ void main() {
27347
27574
  const tapRelease = this.dragging && !this.moved && (!wasTouch || Math.hypot(x - this.startX, y - this.startY) <= TOUCH_TAP_SLOP);
27348
27575
  if (this.dragging && this.region === "drawing") {
27349
27576
  this.deps.drawingsPointerUp?.(x, y, this.snapMode(e));
27577
+ } else if (this.dragging && this.region === "separator") {
27578
+ this.deps.endPaneResize?.();
27350
27579
  } else if (tapRelease && this.region === "data") {
27351
27580
  this.deps.onClick(x, y);
27352
27581
  } else if (this.dragging && this.region === "data" && wasTouch) {
@@ -27368,6 +27597,7 @@ void main() {
27368
27597
  this.cancelLongPress();
27369
27598
  if (!this.dragging) return;
27370
27599
  if (this.region === "drawing" && !Number.isNaN(this.cursorX)) this.deps.drawingsPointerUp?.(this.cursorX, this.cursorY, this.snapMode(e));
27600
+ if (this.region === "separator") this.deps.endPaneResize?.();
27371
27601
  if (this.region === "crosshair" || e.pointerType === "touch") this.deps.onPointerMove(null, null);
27372
27602
  this.endGesture(e);
27373
27603
  };
@@ -28044,9 +28274,12 @@ void main() {
28044
28274
  setIndicatorZ(id, z) {
28045
28275
  this.seriesZ.set(id, z);
28046
28276
  }
28047
- /** Snapshot of the current ordering for a UI/read API: `{ id, z }` sorted by z. */
28277
+ /** Snapshot of the current ordering for a UI/read API: `{ id, z }` back to front, in the
28278
+ * order the indicators paint — on a shared key, mount order (see `orderedIndicatorsForPane`). */
28048
28279
  indicatorZOrder() {
28049
- return [...this.seriesZ.entries()].map(([id, z]) => ({ id, z })).sort((a, b) => a.z - b.z);
28280
+ const mounted = new Map([...this.indicators.keys()].map((id, i) => [id, i]));
28281
+ const at = (id) => mounted.get(id) ?? Number.MAX_SAFE_INTEGER;
28282
+ return [...this.seriesZ.entries()].map(([id, z]) => ({ id, z })).sort((a, b) => a.z - b.z || at(a.id) - at(b.id));
28050
28283
  }
28051
28284
  /** The pane's series z keys (each indicator, plus the candles on the price pane), sorted
28052
28285
  * ascending and de-duplicated — the boundaries a user drawing's z is slotted against. */
@@ -28074,16 +28307,19 @@ void main() {
28074
28307
  for (const z of this.seriesZ.values()) if (z < min2) min2 = z;
28075
28308
  return min2;
28076
28309
  }
28310
+ /** Create the pane, or update an existing one in place. Without `heightWeight` an existing
28311
+ * pane keeps its weight (a user resize survives other indicators mounting onto it) and a
28312
+ * new one gets the default: 3 for the price pane, 1 for a study pane. */
28077
28313
  ensurePane(id, kind, order, heightWeight) {
28078
28314
  this.orderedCache = null;
28079
28315
  const existing = this.panes.get(id);
28080
28316
  if (existing) {
28081
28317
  existing.order = order;
28082
- existing.heightWeight = heightWeight;
28318
+ if (heightWeight !== void 0) existing.heightWeight = heightWeight;
28083
28319
  existing.kind = kind;
28084
28320
  return existing;
28085
28321
  }
28086
- 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 };
28322
+ 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 };
28087
28323
  this.panes.set(id, pane);
28088
28324
  return pane;
28089
28325
  }
@@ -28140,6 +28376,7 @@ void main() {
28140
28376
  const i0 = Math.max(0, Math.floor(vr.from));
28141
28377
  const i1 = Math.min(n - 1, Math.ceil(vr.to));
28142
28378
  if (i1 < i0) return;
28379
+ const s1 = Math.max(i1, Math.ceil(vr.to));
28143
28380
  const barColorMap = mergeBarColors2(scene.indicators);
28144
28381
  const panes = scene.orderedPanes();
28145
28382
  for (const pane of panes) {
@@ -28182,8 +28419,8 @@ void main() {
28182
28419
  ctx.globalAlpha = this.modelAlpha;
28183
28420
  const off = scene.offsetOf(m.id);
28184
28421
  const mp = effPane(m);
28185
- for (const f of m.fills) if (f.overlay !== true) this.drawFill(ctx, m, f, mp, coords, i0, i1, off);
28186
- for (const s of m.series) if (s.overlay !== true) this.drawSeries(ctx, s, mp, coords, i0, i1, theme, off);
28422
+ for (const f of m.fills) if (f.overlay !== true) this.drawFill(ctx, m, f, mp, coords, i0, s1, off);
28423
+ for (const s of m.series) if (s.overlay !== true) this.drawSeries(ctx, s, mp, coords, i0, s1, theme, off);
28187
28424
  }
28188
28425
  if (drawCandles && !candleDrawn) {
28189
28426
  drawSlicesUpTo(scene.candleZ);
@@ -28194,11 +28431,11 @@ void main() {
28194
28431
  ctx.globalAlpha = this.modelAlpha;
28195
28432
  for (const m of scene.indicators.values()) {
28196
28433
  const off = scene.offsetOf(m.id);
28197
- for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0, i1, off);
28434
+ for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0, s1, off);
28198
28435
  }
28199
28436
  for (const m of scene.indicators.values()) {
28200
28437
  const off = scene.offsetOf(m.id);
28201
- for (const s of m.series) if (s.overlay === true) this.drawSeries(ctx, s, pane, coords, i0, i1, theme, off);
28438
+ for (const s of m.series) if (s.overlay === true) this.drawSeries(ctx, s, pane, coords, i0, s1, theme, off);
28202
28439
  }
28203
28440
  }
28204
28441
  drawSlicesUpTo(Infinity);
@@ -28650,7 +28887,7 @@ void main() {
28650
28887
  const tickW = Math.max(1, Math.round(spacing * 0.35));
28651
28888
  for (let i = i0; i <= i1; i += 1) {
28652
28889
  const b = s.bars[i - off];
28653
- if (!b) continue;
28890
+ if (!b || !Number.isFinite(b.open + b.high + b.low + b.close)) continue;
28654
28891
  const bc = s.barColors?.[i - off];
28655
28892
  const body = bc?.color ?? (b.close >= b.open ? up : down);
28656
28893
  const x = Math.round(coords.logicalToX(i)) + 0.5;
@@ -39538,6 +39775,58 @@ ${overlayScrollbarCss(".vela-sd-pane")}
39538
39775
  return { below, above };
39539
39776
  }
39540
39777
 
39778
+ // src/renderers/native/core/overlayStack.ts
39779
+ function priceStack(indicators, candleZ) {
39780
+ const out = [...indicators];
39781
+ const at = out.findIndex((e) => e.z >= candleZ);
39782
+ out.splice(at < 0 ? out.length : at, 0, { id: null, z: candleZ });
39783
+ return out;
39784
+ }
39785
+ function untie(stack) {
39786
+ const out = [];
39787
+ for (let i = 0; i < stack.length; ) {
39788
+ let j = i + 1;
39789
+ while (j < stack.length && stack[j].z === stack[i].z) j += 1;
39790
+ const run = j - i;
39791
+ if (run > 1) {
39792
+ const base = stack[i].z;
39793
+ const step = ((stack[j]?.z ?? base + 1) - base) / run;
39794
+ for (let k = 1; k < run; k += 1) {
39795
+ const id = stack[i + k].id;
39796
+ if (id !== null) out.push({ id, z: base + k * step });
39797
+ }
39798
+ }
39799
+ i = j;
39800
+ }
39801
+ return out;
39802
+ }
39803
+ function layerAbove(stack, id) {
39804
+ const i = stack.findIndex((e) => e.id === id);
39805
+ if (i < 0 || i === stack.length - 1) return null;
39806
+ const above = stack[i + 1];
39807
+ return above.id === null ? "price" : { indicator: above.id };
39808
+ }
39809
+ function layerBelow(stack, id) {
39810
+ const i = stack.findIndex((e) => e.id === id);
39811
+ if (i <= 0) return null;
39812
+ const below = stack[i - 1];
39813
+ return below.id === null ? "price" : { indicator: below.id };
39814
+ }
39815
+ function raisedZ(stack, id) {
39816
+ const i = stack.findIndex((e) => e.id === id);
39817
+ if (i < 0 || i === stack.length - 1) return null;
39818
+ const above = stack[i + 1];
39819
+ const next2 = stack[i + 2];
39820
+ return next2 ? (above.z + next2.z) / 2 : above.z + 1;
39821
+ }
39822
+ function loweredZ(stack, id) {
39823
+ const i = stack.findIndex((e) => e.id === id);
39824
+ if (i <= 0) return null;
39825
+ const below = stack[i - 1];
39826
+ const prev3 = stack[i - 2];
39827
+ return prev3 ? (prev3.z + below.z) / 2 : below.z - 1;
39828
+ }
39829
+
39541
39830
  // src/renderers/native/chrome/vela-logos.ts
39542
39831
  var VELA_MARK_SVG = '<svg viewBox="0 -142 161.66 148" aria-hidden="true"><g fill="currentColor"><path d="M0 -140L32.91 -140L77.52 -62.73L61.06 -34.24Z"/><path d="M161.66 -140L128.75 -140L84.14 -62.73L100.59 -34.24Z"/><path d="M80.83 -52.04L95.85 -26.02L80.83 0L65.81 -26.02Z"/></g></svg>';
39543
39832
  var VELA_LETTERS_SVG = '<svg viewBox="161.66 -142 310.57 148" aria-hidden="true"><g fill="currentColor"><path d="M226.3 1.17Q200.81 1.17 186.12 -12.99Q171.42 -27.15 171.42 -51.17Q171.42 -75.2 185.87 -89.6Q200.33 -104 224.25 -104Q247.4 -104 261.7 -90.58Q276.01 -77.15 276.3 -54.59Q276.3 -49.9 275.42 -45.02H200.81V-43.55Q201.2 -32.62 207.75 -26.42Q214.29 -20.21 225.03 -20.21Q233.72 -20.21 239.34 -23.93Q244.95 -27.64 246.81 -34.77H274.64Q272.1 -19.14 259.16 -8.98Q246.22 1.17 226.3 1.17ZM201.5 -63.38H247.88Q246.42 -72.75 240.27 -77.88Q234.12 -83.01 224.45 -83.01Q215.17 -83.01 208.92 -77.73Q202.67 -72.46 201.5 -63.38ZM287.54 0V-140.04H315.76V0ZM429.34 -23.83H432.56V0H418.11Q407.76 0 403.36 -4.44Q398.97 -8.89 399.16 -16.8Q387.83 1.17 365.57 1.17Q348.48 1.17 337.88 -6.74Q327.29 -14.65 327.29 -29Q327.29 -45.41 339.45 -54.3Q351.6 -63.18 374.55 -63.18H394.96V-68.16Q394.96 -75.2 390.08 -79.3Q385.2 -83.4 376.51 -83.4Q368.3 -83.4 363.03 -79.98Q357.76 -76.56 356.88 -70.8H329.73Q331.19 -85.84 344.08 -94.92Q356.97 -104 377.29 -104Q399.06 -104 410.88 -94.43Q422.7 -84.86 422.7 -66.6V-30.18Q422.7 -26.37 424.26 -25.1Q425.82 -23.83 429.34 -23.83ZM394.96 -42.38V-44.43H374.36Q365.47 -44.43 360.59 -40.92Q355.7 -37.4 355.7 -30.96Q355.7 -25.59 359.81 -22.41Q363.91 -19.24 370.94 -19.24Q381.97 -19.24 388.37 -25.54Q394.77 -31.84 394.96 -42.38Z"/><path fill-opacity="0.55" d="M437.44 -124.28H441.96V-113.41H445.34V-124.28H449.87V-127.4H437.44ZM451.43 -113.41H454.7V-123.29L458.97 -113.41H461.62L465.99 -123.29V-113.41H469.21V-127.4H464.53L460.42 -117.68L456.16 -127.4H451.43Z"/></g></svg>';
@@ -40188,6 +40477,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
40188
40477
  // pane just below it
40189
40478
  this.resizeSplitStart = null;
40190
40479
  // the two panes' shared span when the drag began
40480
+ this.resizeStartAboveWeight = 0;
40481
+ // the upper pane's weight when the drag began
40191
40482
  /** The user drawings take part in pointer input (the runtime `drawingsInteractive` feature). */
40192
40483
  this.drawingsInteractive = true;
40193
40484
  this.hoverSeparatorY = null;
@@ -40830,6 +41121,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
40830
41121
  };
40831
41122
  this.scene.candleZ = next2.stacking.candles;
40832
41123
  for (const [id, z] of Object.entries(next2.stacking.series)) this.scene.setIndicatorZ(id, z);
41124
+ this.syncLegendOrder();
40833
41125
  s.bars = { upColor: next2.bars.upColor, downColor: next2.bars.downColor };
40834
41126
  s.line = { color: next2.line.color, width: next2.line.width };
40835
41127
  s.area = { lineColor: next2.area.lineColor, width: next2.area.width, topColor: next2.area.topColor, bottomColor: next2.area.bottomColor };
@@ -40875,6 +41167,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
40875
41167
  if (typeof v.z === "number") this.scene.setIndicatorZ(v.id, v.z);
40876
41168
  else if (v.to === "front") this.scene.bringIndicatorToFront(v.id);
40877
41169
  else if (v.to === "back") this.scene.sendIndicatorToBack(v.id);
41170
+ this.syncLegendOrder();
40878
41171
  }
40879
41172
  /** Toggle the in-chart settings gear + dialog (the `settings` feature). The gear
40880
41173
  * sits above the time axis, clear of the right price-axis strip. */
@@ -41262,7 +41555,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
41262
41555
  this.chrome.mount(this.chromeCanvas);
41263
41556
  this.crosshairLayer.mount(this.cursorCanvas);
41264
41557
  this.coords.setViewport(defaultViewport());
41265
- this.scene.ensurePane(PRICE_PANE_ID, "price", 0, 3);
41558
+ this.scene.ensurePane(PRICE_PANE_ID, "price", 0);
41266
41559
  this.scheduler = new Scheduler((level) => this.renderFrame(level));
41267
41560
  this.animator = new Animator((dt) => this.animTick(dt));
41268
41561
  this.targetBarSpacing = this.coords.getViewport().barSpacing;
@@ -41275,7 +41568,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
41275
41568
  onClick: (x, y) => {
41276
41569
  if (this.handleMarkClick(x, y)) return;
41277
41570
  this.userDrawings?.deselect();
41278
- this.handleClick(x);
41571
+ this.handleClick(x, y);
41279
41572
  },
41280
41573
  onAxisLongPress: (axis, x, y) => {
41281
41574
  for (const cb of this.axisLongPressCbs) cb({ axis, x, y });
@@ -41289,6 +41582,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
41289
41582
  paneSeparatorAt: (y) => this.paneSeparatorAt(y),
41290
41583
  beginPaneResize: (y) => this.beginPaneResize(y),
41291
41584
  paneResizeBy: (dy) => this.paneResizeBy(dy),
41585
+ endPaneResize: () => this.endPaneResize(),
41292
41586
  resetPaneSize: (y) => this.resetPaneSize(y),
41293
41587
  resetView: () => this.resetView(),
41294
41588
  // User drawings claim a gesture before pan when armed / over a drawing.
@@ -41414,6 +41708,12 @@ ${overlayScrollbarCss(".vela-sd-pane")}
41414
41708
  for (const cb of this.moveIndicatorCbs) cb(id, target);
41415
41709
  }
41416
41710
  });
41711
+ this.inputsUI.setStackApi({
41712
+ above: (id) => layerAbove(this.overlayStack(), id),
41713
+ below: (id) => layerBelow(this.overlayStack(), id),
41714
+ raise: (id) => this.restackOverlay(id, raisedZ),
41715
+ lower: (id) => this.restackOverlay(id, loweredZ)
41716
+ });
41417
41717
  this.paneControls = new PaneControls(this.plot, theme, {
41418
41718
  panes: () => this.paneControlViews(),
41419
41719
  rightAxis: () => this.rightAxisW,
@@ -41731,7 +42031,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
41731
42031
  }
41732
42032
  // ── panes ──
41733
42033
  ensurePane(pane) {
41734
- this.scene.ensurePane(pane.id, pane.kind, pane.order, pane.heightWeight ?? (pane.kind === "price" ? 3 : 1));
42034
+ this.scene.ensurePane(pane.id, pane.kind, pane.order, pane.heightWeight);
41735
42035
  this.layoutPanes();
41736
42036
  this.paneControls?.refresh();
41737
42037
  this.scheduler.invalidate(4 /* Full */);
@@ -41753,6 +42053,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
41753
42053
  model.ownScale = opts?.ownScale === true;
41754
42054
  if (!model.ownScale) this.scene.dropIndicatorScale(handle.id);
41755
42055
  this.inputsUI.setPane(handle.id, paneId);
42056
+ this.syncLegendOrder();
41756
42057
  this.refreshAnchorOffset(model);
41757
42058
  this.refreshAxisWidth();
41758
42059
  this.layoutPanes();
@@ -41780,6 +42081,15 @@ ${overlayScrollbarCss(".vela-sd-pane")}
41780
42081
  this.paneControls?.refresh();
41781
42082
  this.scheduler.invalidate(4 /* Full */);
41782
42083
  }
42084
+ setPaneHeightWeight(paneId, weight) {
42085
+ const pane = this.scene.panes.get(paneId);
42086
+ if (!pane || !(weight > 0) || pane.heightWeight === weight) return;
42087
+ pane.heightWeight = weight;
42088
+ this.afterPaneLayoutChange();
42089
+ }
42090
+ paneHeightWeight(paneId) {
42091
+ return this.scene.panes.get(paneId)?.heightWeight ?? null;
42092
+ }
41783
42093
  onPaneAction(cb) {
41784
42094
  this.paneActionCbs.add(cb);
41785
42095
  return () => this.paneActionCbs.delete(cb);
@@ -41874,6 +42184,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
41874
42184
  this.vpvrHidden = false;
41875
42185
  }
41876
42186
  if (model.ownScale) this.refreshAxisWidth();
42187
+ this.syncLegendOrder();
41877
42188
  this.paneControls?.refresh();
41878
42189
  this.scheduler.invalidate(4 /* Full */);
41879
42190
  return { id: model.id };
@@ -42281,7 +42592,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
42281
42592
  this.setMarkHover(null);
42282
42593
  this.scheduler.invalidate(1 /* Cursor */);
42283
42594
  this.hoverLogical = null;
42284
- const empty = { time: null, price: null, paneKind: null, values: /* @__PURE__ */ new Map(), ohlc: null };
42595
+ const empty = { time: null, price: null, paneKind: null, paneId: null, values: /* @__PURE__ */ new Map(), ohlc: null };
42285
42596
  for (const cb of this.crosshairCbs) cb(empty);
42286
42597
  return;
42287
42598
  }
@@ -42312,14 +42623,23 @@ ${overlayScrollbarCss(".vela-sd-pane")}
42312
42623
  const bar = onBar ? this.bars[logical] : void 0;
42313
42624
  const ohlc = bar ? { time: bar.time, open: bar.open, high: bar.high, low: bar.low, close: bar.close, volume: bar.volume } : null;
42314
42625
  this.hoverLogical = onBar ? logical : null;
42315
- const event = { time, price, paneKind: inData && pane ? pane.kind : null, values, ohlc };
42626
+ const onPane = inData && pane ? pane : null;
42627
+ const event = { time, price, paneKind: onPane?.kind ?? null, paneId: onPane?.id ?? null, values, ohlc };
42316
42628
  for (const cb of this.crosshairCbs) cb(event);
42317
42629
  }
42318
- handleClick(x) {
42630
+ handleClick(x, y) {
42319
42631
  if (this.coords.barCount === 0) return;
42320
42632
  const logical = Math.round(this.coords.xToLogical(x));
42321
42633
  const onBar = logical >= 0 && logical < this.coords.barCount;
42322
- for (const cb of this.clickCbs) cb({ time: onBar ? this.coords.logicalToTime(logical) : null, price: null });
42634
+ const inData = x >= 0 && y >= 0 && x <= this.coords.width && y <= this.coords.height;
42635
+ const pane = inData ? this.paneNodeAtY(y) : null;
42636
+ const event = {
42637
+ time: onBar ? this.coords.logicalToTime(logical) : null,
42638
+ price: pane ? this.coords.yToPrice(y, pane.scale, pane.bounds) : null,
42639
+ paneKind: pane?.kind ?? null,
42640
+ paneId: pane?.id ?? null
42641
+ };
42642
+ for (const cb of this.clickCbs) cb(event);
42323
42643
  }
42324
42644
  /**
42325
42645
  * Fan out (or collapse, with null) a multi-group mark stack; repaints the chrome tier when
@@ -42564,6 +42884,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
42564
42884
  const below = panes[i];
42565
42885
  this.resizeAbove = above;
42566
42886
  this.resizeBelow = below;
42887
+ this.resizeStartAboveWeight = above.heightWeight;
42567
42888
  this.resizeSplitStart = {
42568
42889
  combinedTop: above.bounds.top,
42569
42890
  combinedHeight: above.bounds.height + below.bounds.height,
@@ -42584,6 +42905,16 @@ ${overlayScrollbarCss(".vela-sd-pane")}
42584
42905
  this.layoutPanes();
42585
42906
  this.scheduler.invalidate(4 /* Full */);
42586
42907
  }
42908
+ /** Release the separator: report the resize once, at the end of the drag, if it changed anything. */
42909
+ endPaneResize() {
42910
+ const above = this.resizeAbove;
42911
+ const below = this.resizeBelow;
42912
+ const startAbove = this.resizeStartAboveWeight;
42913
+ this.resizeAbove = null;
42914
+ this.resizeBelow = null;
42915
+ this.resizeSplitStart = null;
42916
+ if (above && below && above.heightWeight !== startAbove) this.emitPaneAction({ type: "resize", paneIds: [above.id, below.id] });
42917
+ }
42587
42918
  /** Double-click a separator → split the two adjacent panes evenly (each gets half of
42588
42919
  * their combined weight). A simple, predictable reset that leaves siblings untouched. */
42589
42920
  resetPaneSize(y) {
@@ -42597,6 +42928,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
42597
42928
  below.heightWeight = half;
42598
42929
  this.layoutPanes();
42599
42930
  this.scheduler.invalidate(4 /* Full */);
42931
+ this.emitPaneAction({ type: "resize", paneIds: [above.id, below.id] });
42600
42932
  }
42601
42933
  // ── keyboard navigation / accessibility (item 11) ──
42602
42934
  /** Enable/disable keyboard control: focusability + ARIA + key listeners + live region. */
@@ -42851,6 +43183,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
42851
43183
  /** Paint the below-data (L-1) + geometry (L0) + chrome (L1) layers from the current scene/coords. */
42852
43184
  paintData() {
42853
43185
  this.syncLayerCanvasOrder();
43186
+ this.syncLegendOrder();
42854
43187
  this.stampScaleInvert();
42855
43188
  this.backend.modelAlpha = this.modelAlpha;
42856
43189
  this.backend.candleBodyAlpha = this.candleBodyAlpha;
@@ -43032,6 +43365,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
43032
43365
  const i0 = Math.max(0, Math.floor(vr.from));
43033
43366
  const i1 = Math.min(n - 1, Math.ceil(vr.to));
43034
43367
  if (i1 < i0) return;
43368
+ const s1 = Math.max(i1, Math.ceil(vr.to));
43035
43369
  this.scene.percentBaseline = this.bars[i0]?.close ?? 0;
43036
43370
  const panes = this.scene.orderedPanes();
43037
43371
  const pricePane = panes.find((p) => p.kind === "price") ?? null;
@@ -43049,11 +43383,11 @@ ${overlayScrollbarCss(".vela-sd-pane")}
43049
43383
  const masterModels = models.filter((m) => m.ownScale !== true);
43050
43384
  let dr = this.chrome.paneDrawingsRange(masterModels, this.scene, pane === pricePane, vr);
43051
43385
  if (pane === pricePane) {
43052
- const or2 = overlaySeriesRange(this.scene.indicators.values(), i0, i1, (id) => this.scene.offsetOf(id));
43386
+ const or2 = overlaySeriesRange(this.scene.indicators.values(), i0, s1, (id) => this.scene.offsetOf(id));
43053
43387
  if (or2) dr = dr ? { min: Math.min(dr.min, or2.min), max: Math.max(dr.max, or2.max) } : or2;
43054
43388
  }
43055
43389
  const includeCandles = pane.kind === "price" && (!this.scene.candlesHidden || this.priceLayersAnchoredToBars(masterModels) || !this.paneHasMeasurableContent(masterModels, dr));
43056
- pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id), margins);
43390
+ pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, s1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id), margins);
43057
43391
  pane.percentBaseline = pane.kind === "price" ? this.bars[i0]?.close ?? 0 : this.firstVisibleValue(masterModels, i0);
43058
43392
  pane.axisFormat = void 0;
43059
43393
  pane.axisBands = void 0;
@@ -43093,7 +43427,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
43093
43427
  continue;
43094
43428
  }
43095
43429
  const mdr = this.chrome.paneDrawingsRange([model], this.scene, false, vr);
43096
- sl.scaleTarget = computePaneScale([model], this.bars, false, i0, i1, mdr, false, (id) => this.scene.offsetOf(id), margins);
43430
+ sl.scaleTarget = computePaneScale([model], this.bars, false, i0, s1, mdr, false, (id) => this.scene.offsetOf(id), margins);
43097
43431
  if (!animating || !sl.initialized) {
43098
43432
  sl.scale = { ...sl.scaleTarget };
43099
43433
  sl.initialized = true;
@@ -43164,6 +43498,30 @@ ${overlayScrollbarCss(".vela-sd-pane")}
43164
43498
  const master = models.find((m) => m.ownScale !== true) ?? models[0];
43165
43499
  return master?.title || null;
43166
43500
  }
43501
+ /**
43502
+ * The price pane's layers back to front, as the legend restacks them: every indicator
43503
+ * with a legend row, in paint order, plus the candles. The same layers and order the
43504
+ * object tree lists (see `indicatorZOrder`).
43505
+ */
43506
+ overlayStack() {
43507
+ const painted = this.scene.orderedIndicatorsForPane(PRICE_PANE_ID).filter((m) => m.legend !== false).map((m) => ({ id: m.id, z: this.scene.zOf(m.id) }));
43508
+ return priceStack(painted, this.scene.candleZ);
43509
+ }
43510
+ /** List the legend rows the way the indicators stack, front-most first (no-op when unchanged). */
43511
+ syncLegendOrder() {
43512
+ this.inputsUI?.orderRows(this.scene.indicatorZOrder().map((e) => e.id));
43513
+ }
43514
+ /** Apply a legend restacking step: shared keys are spread apart first (the order stays),
43515
+ * so the step lands exactly one layer over. No-op when there is nothing to step over. */
43516
+ restackOverlay(id, step) {
43517
+ for (const fix of untie(this.overlayStack())) this.scene.setIndicatorZ(fix.id, fix.z);
43518
+ const z = step(this.overlayStack(), id);
43519
+ if (z === null) return;
43520
+ this.scene.setIndicatorZ(id, z);
43521
+ this.syncLegendOrder();
43522
+ this.scheduler.invalidate(4 /* Full */);
43523
+ for (const cb of this.configChangedCbs) cb();
43524
+ }
43167
43525
  /** The pane a bespoke-layer native indicator (volume/vpvr) currently lives in (null = not mounted). */
43168
43526
  nativeLayerPane(type) {
43169
43527
  for (const m of this.scene.indicators.values()) {
@@ -43618,17 +43976,35 @@ ${overlayScrollbarCss(".vela-sd-pane")}
43618
43976
  for (const s of model.series) {
43619
43977
  const times = s.kind === "candle" || s.kind === "bar" ? s.bars : isLineLikeSeries(s) ? s.points : null;
43620
43978
  if (!times || times.length === 0) continue;
43621
- let lo = 0;
43622
- let hi = times.length;
43623
- while (lo < hi) {
43624
- const mid = lo + hi >> 1;
43625
- if (times[mid].time < headTime) lo = mid + 1;
43626
- else hi = mid;
43627
- }
43628
- return lo;
43979
+ const count = slotsBefore(times, headTime);
43980
+ if (count !== null) return count;
43629
43981
  }
43630
43982
  return null;
43631
43983
  }
43984
+ function slotsBefore(times, headTime) {
43985
+ let lo = 0;
43986
+ let hi = times.length;
43987
+ while (lo < hi) {
43988
+ const mid = lo + hi >> 1;
43989
+ const t = times[mid]?.time;
43990
+ if (t === void 0) return slotsBeforeScan(times, headTime);
43991
+ if (t < headTime) lo = mid + 1;
43992
+ else hi = mid;
43993
+ }
43994
+ return lo;
43995
+ }
43996
+ function slotsBeforeScan(times, headTime) {
43997
+ let lastBefore = -1;
43998
+ let any = false;
43999
+ for (let i = 0; i < times.length; i += 1) {
44000
+ const t = times[i]?.time;
44001
+ if (t === void 0) continue;
44002
+ any = true;
44003
+ if (t >= headTime) return t === headTime ? i : lastBefore + 1;
44004
+ lastBefore = i;
44005
+ }
44006
+ return any ? times.length : null;
44007
+ }
43632
44008
  function applyPatch(model, patch) {
43633
44009
  if (patch.kind === "value") {
43634
44010
  for (const delta of patch.series) {
@@ -44041,6 +44417,9 @@ ${overlayScrollbarCss(".vela-sd-pane")}
44041
44417
  overlay: spec.overlay,
44042
44418
  // Studies stack (a 20 and a 200 moving average side by side) — unlike the layer-backed natives.
44043
44419
  multiInstance: true,
44420
+ // A study's values depend on every bar before them (an EMA's seed, an RSI's
44421
+ // smoothing), so it computes once over the whole history, as script indicators do.
44422
+ waitsForHistory: true,
44044
44423
  inputsSchema: () => spec.inputs,
44045
44424
  defaultInputs: () => Object.fromEntries(spec.inputs.map((i) => [i.key, i.defval])),
44046
44425
  create: () => new ClassicIndicator(spec)
@@ -44876,41 +45255,50 @@ ${overlayScrollbarCss(".vela-sd-pane")}
44876
45255
  return directionalChannel(bars, basis, map(basis, (x) => x * (1 + k)), map(basis, (x) => x * (1 - k)), ink, "envelope");
44877
45256
  }
44878
45257
  };
44879
- var LINREG_CURVE = "Curve";
44880
- var LINREG_SLOPE = "Slope";
44881
- var linearRegression = {
44882
- type: "linear-regression",
44883
- title: "Linear Regression Curve",
44884
- shortTitle: "LinReg",
44885
- // Slope lives in its own pane; Curve force-overlays itself onto the price candles.
45258
+ var leastSquaresMa = {
45259
+ type: "lsma",
45260
+ title: "Least Squares Moving Average",
45261
+ shortTitle: "LSMA",
45262
+ overlay: true,
45263
+ inputs: [
45264
+ sourceInput("Close", "source", "Source", "Price series the regression line is fitted to."),
45265
+ { ...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 },
45266
+ 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."),
45267
+ ...directionInputs()
45268
+ ],
45269
+ compute: (bars, inputs) => {
45270
+ const src = sourceValues(bars, str3(inputs, "source", "Close"));
45271
+ const values = linreg(src, num3(inputs, "length", 25), Math.trunc(num3(inputs, "offset", 0)));
45272
+ return { plots: [slopeColoredPlot("lsma", "LSMA", values, inputs)] };
45273
+ }
45274
+ };
45275
+ var linearRegressionSlope = {
45276
+ type: "linear-regression-slope",
45277
+ title: "Linear Regression Slope",
45278
+ shortTitle: "LRS",
44886
45279
  overlay: false,
44887
45280
  inputs: [
44888
- 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."),
44889
45281
  { ...lengthInput(100, "length", "Length", 5e3, "Number of bars used to fit the linear regression."), min: 2 },
44890
45282
  sourceInput("Close", "source", "Source", "Price series the regression is fitted to."),
44891
- colorInput(BULLISH, "Rising", "bullColor", "Color of the regression curve and slope line while the fit slopes upward."),
44892
- colorInput(BEARISH, "Falling", "bearColor", "Color of the regression curve and slope line while the fit slopes downward."),
44893
- colorInput(NEUTRAL, "Regression Line", "lineColor", "Color of the regression curve and slope line while the fit is flat.")
45283
+ colorInput(BULLISH, "Rising", "bullColor", "Color of the slope line while the fit slopes upward."),
45284
+ colorInput(BEARISH, "Falling", "bearColor", "Color of the slope line while the fit slopes downward."),
45285
+ colorInput(NEUTRAL, "Slope Line", "lineColor", "Color of the slope line while the fit is flat.")
44894
45286
  ],
44895
45287
  compute: (bars, inputs) => {
44896
45288
  const len = num3(inputs, "length", 100);
44897
45289
  const src = sourceValues(bars, str3(inputs, "source", "Close"));
44898
- const curve = linreg(src, len, 0);
44899
- const slope = zip(curve, linreg(src, len, 1), (a, b) => a - b);
45290
+ const slope = zip(linreg(src, len, 0), linreg(src, len, 1), (a, b) => a - b);
44900
45291
  const bull = str3(inputs, "bullColor", BULLISH);
44901
45292
  const bear = str3(inputs, "bearColor", BEARISH);
44902
45293
  const flat = str3(inputs, "lineColor", NEUTRAL);
44903
45294
  const colors = slope.map((s) => Number.isFinite(s) ? s > 0 ? bull : s < 0 ? bear : flat : null);
44904
- if (str3(inputs, "mode", LINREG_CURVE) === LINREG_SLOPE) {
44905
- return {
44906
- plots: [{ key: "slope", title: "Slope", values: slope, color: flat, width: 2, colors }],
44907
- levels: [{ key: "zero", price: 0, color: transp(NEUTRAL, 50), lineStyle: "dashed" }]
44908
- };
44909
- }
44910
- return { plots: [{ key: "curve", title: "Curve", values: curve, color: flat, width: 2, colors, overlay: true }] };
45295
+ return {
45296
+ plots: [{ key: "slope", title: "Slope", values: slope, color: flat, width: 2, colors }],
45297
+ levels: [{ key: "zero", price: 0, color: transp(NEUTRAL, 50), lineStyle: "dashed" }]
45298
+ };
44911
45299
  }
44912
45300
  };
44913
- var averageSpecs = [simpleMa, exponentialMa, movingAverage, smoothedMa, zlema, vidya, maEnvelope, linearRegression];
45301
+ var averageSpecs = [simpleMa, exponentialMa, movingAverage, smoothedMa, zlema, vidya, maEnvelope, leastSquaresMa, linearRegressionSlope];
44914
45302
 
44915
45303
  // src/core/native-indicators/classics/bands.ts
44916
45304
  var JAW_COLOR = "#ab47bc";