@luxalgo/vela 0.8.4 → 0.8.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/{DataProvider-Byo8pEv0.d.ts → DataProvider-BUdlkx9I.d.ts} +1 -1
  2. package/dist/{DataProvider-Bw58CjYy.d.cts → DataProvider-DdqMxKdl.d.cts} +1 -1
  3. package/dist/{chunk-MOJ4T26N.js → chunk-BN4DNS3O.js} +27 -7
  4. package/dist/{chunk-UNUVPQFW.js → chunk-QYIO37K7.js} +482 -94
  5. package/dist/{contributions-DOm40g5-.d.cts → contributions-D8xuYc9U.d.cts} +37 -5
  6. package/dist/{contributions-BY3zSYiZ.d.ts → contributions-P9fBBuY1.d.ts} +37 -5
  7. package/dist/index.cjs +482 -94
  8. package/dist/index.d.cts +23 -7
  9. package/dist/index.d.ts +23 -7
  10. package/dist/index.js +1 -1
  11. package/dist/{options-CSXbjMM5.d.cts → options-BI5Tp32b.d.cts} +36 -1
  12. package/dist/{options-CSXbjMM5.d.ts → options-BI5Tp32b.d.ts} +36 -1
  13. package/dist/{plugin-CiQVyETg.d.cts → plugin-B8F1nor8.d.cts} +14 -8
  14. package/dist/{plugin-BDxUhGO3.d.ts → plugin-D5Z-Q6TX.d.ts} +14 -8
  15. package/dist/plugin.d.cts +4 -4
  16. package/dist/plugin.d.ts +4 -4
  17. package/dist/providers/binance.d.cts +2 -2
  18. package/dist/providers/binance.d.ts +2 -2
  19. package/dist/providers/coinbase.d.cts +2 -2
  20. package/dist/providers/coinbase.d.ts +2 -2
  21. package/dist/providers/hyperliquid.d.cts +2 -2
  22. package/dist/providers/hyperliquid.d.ts +2 -2
  23. package/dist/{statusline-model-DUBX6Fib.d.cts → statusline-model-CTwtDIca.d.cts} +3 -3
  24. package/dist/{statusline-model-C9tYXuux.d.ts → statusline-model-D9kWC9X5.d.ts} +3 -3
  25. package/dist/ui.d.cts +1 -1
  26. package/dist/ui.d.ts +1 -1
  27. package/dist/vela.global.js +482 -94
  28. package/dist/vela.global.min.js +53 -53
  29. package/dist/widget.cjs +508 -100
  30. package/dist/widget.d.cts +6 -6
  31. package/dist/widget.d.ts +6 -6
  32. package/dist/widget.js +3 -3
  33. package/dist/workspace.cjs +508 -100
  34. package/dist/workspace.d.cts +5 -5
  35. package/dist/workspace.d.ts +5 -5
  36. package/dist/workspace.js +2 -2
  37. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -8617,6 +8617,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
8617
8617
  this.barTransform = null;
8618
8618
  // ── chart-type data engines (SDK): one lazily-created engine per style id ──
8619
8619
  this.typeEngines = /* @__PURE__ */ new Map();
8620
+ /** Engines suspended by the chart — their host pushes are dropped until they resume. */
8621
+ this.suspendedEngines = /* @__PURE__ */ new WeakSet();
8620
8622
  /** Last-seen chart-type settings per type id (dialog edits + persisted restores) —
8621
8623
  * replayed into a data engine whenever one is (re)created. */
8622
8624
  this.typeSettings = /* @__PURE__ */ new Map();
@@ -8891,12 +8893,14 @@ var EngineOrchestrator = class _EngineOrchestrator {
8891
8893
  this.mergeHistoryIntoReplay(bars);
8892
8894
  return;
8893
8895
  }
8894
- this.setBarSeries(bars, painted ? { preserveView: true } : void 0);
8896
+ const repaint = painted;
8897
+ this.setBarSeries(bars, repaint ? { preserveView: true } : void 0);
8895
8898
  if (!painted && bars.length > 0) {
8896
8899
  painted = true;
8897
8900
  if (opts.firstLoad) this.activateBarLayers();
8898
8901
  if (!final) this.historyState = "backfill";
8899
8902
  }
8903
+ if (repaint && !final) this.notifySessionsBars("backfill");
8900
8904
  };
8901
8905
  const abort = new AbortController();
8902
8906
  this.progressiveAbort = abort;
@@ -9099,10 +9103,15 @@ var EngineOrchestrator = class _EngineOrchestrator {
9099
9103
  this.setBarSeries([], { clearing: true });
9100
9104
  this.blankIndicatorVisuals();
9101
9105
  if (this.activeEngineStyle) {
9102
- this.typeEngines.get(this.activeEngineStyle)?.suspend();
9106
+ this.suspendTypeEngine(this.typeEngines.get(this.activeEngineStyle));
9103
9107
  this.renderer.setNativeData?.(this.activeEngineStyle, void 0);
9104
9108
  this.renderer.setNativeData?.(`${this.activeEngineStyle}-pending`, []);
9105
9109
  }
9110
+ for (const record of this.registry.all()) {
9111
+ if (!record.native?.started) continue;
9112
+ this.stopNativeInstance(record.native);
9113
+ this.renderer.setNativeData?.(this.nativeChannel(record), void 0);
9114
+ }
9106
9115
  }
9107
9116
  if (depthOnly) {
9108
9117
  this.extendDepth(gen, m.bars ?? 500);
@@ -9129,6 +9138,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
9129
9138
  this.restartChartTypeEngine();
9130
9139
  this.reexecuteIndicators();
9131
9140
  }
9141
+ if (depthOnly) this.releaseHeldNatives();
9132
9142
  if (!replayBack) this.startLive();
9133
9143
  if (identityChanged) {
9134
9144
  this.events.emit("market:changed", { symbol: m.symbol ?? "TEST", timeframe: m.timeframe ?? "60", prev: prev2 });
@@ -9145,9 +9155,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
9145
9155
  restartNativeIndicators() {
9146
9156
  for (const record of this.registry.all()) {
9147
9157
  if (!record.native) continue;
9148
- if (record.native.started) record.native.instance.stop();
9158
+ this.stopNativeInstance(record.native);
9149
9159
  record.native.instance = record.native.descriptor.create();
9150
- record.native.started = false;
9151
9160
  record.pendingStructural = true;
9152
9161
  if (record.hidden) {
9153
9162
  record.native.stale = true;
@@ -9164,6 +9173,21 @@ var EngineOrchestrator = class _EngineOrchestrator {
9164
9173
  this.typeEngines.clear();
9165
9174
  if (this.activeEngineStyle) void this.startChartTypeEngine(this.activeEngineStyle);
9166
9175
  }
9176
+ /** Suspend a chart-type engine. Its host drops every push until the engine resumes:
9177
+ * work it left in flight lands in its own state, never on the scene. */
9178
+ suspendTypeEngine(engine) {
9179
+ if (!engine) return;
9180
+ engine.suspend();
9181
+ this.suspendedEngines.add(engine);
9182
+ }
9183
+ /** Stop a running native instance, then revoke its context: the teardown inside `stop()`
9184
+ * still reaches its layer, but work it left in flight (a fetch resolving later) does not. */
9185
+ stopNativeInstance(native) {
9186
+ if (native.started) native.instance.stop();
9187
+ native.started = false;
9188
+ native.revokeContext?.();
9189
+ native.revokeContext = void 0;
9190
+ }
9167
9191
  /**
9168
9192
  * The FIRST candles just painted (the quick preview, or the only load): activate the
9169
9193
  * layers that need bars but NOT full history. The volume layer reads the chart's bars
@@ -9696,7 +9720,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
9696
9720
  const def = chartType(id);
9697
9721
  const want = def?.dataEngine ? id : null;
9698
9722
  if (want === this.activeEngineStyle) return;
9699
- if (this.activeEngineStyle) this.typeEngines.get(this.activeEngineStyle)?.suspend();
9723
+ if (this.activeEngineStyle) this.suspendTypeEngine(this.typeEngines.get(this.activeEngineStyle));
9700
9724
  this.activeEngineStyle = want;
9701
9725
  if (want) void this.startChartTypeEngine(want);
9702
9726
  }
@@ -9707,6 +9731,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
9707
9731
  if (this.activeEngineStyle !== id) return;
9708
9732
  const existing = this.typeEngines.get(id);
9709
9733
  if (existing) {
9734
+ this.suspendedEngines.delete(existing);
9710
9735
  existing.resume();
9711
9736
  return;
9712
9737
  }
@@ -9716,6 +9741,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
9716
9741
  this.typeEngines.set(id, engine);
9717
9742
  const stored = this.typeSettings.get(id);
9718
9743
  if (stored) engine.onSettings?.(stored);
9744
+ const reachesScene = () => this.typeEngines.get(id) === engine && !this.suspendedEngines.has(engine);
9719
9745
  engine.start({
9720
9746
  symbol: this.config.market.symbol ?? "TEST",
9721
9747
  timeframe: this.config.market.timeframe ?? "60",
@@ -9723,8 +9749,12 @@ var EngineOrchestrator = class _EngineOrchestrator {
9723
9749
  session: this.config.market.session,
9724
9750
  bars: () => this.bars,
9725
9751
  data: this.dataControl,
9726
- pushData: (data) => this.renderer.setNativeData?.(id, data),
9727
- pushPending: (ranges) => this.renderer.setNativeData?.(`${id}-pending`, ranges)
9752
+ pushData: (data) => {
9753
+ if (reachesScene()) this.renderer.setNativeData?.(id, data);
9754
+ },
9755
+ pushPending: (ranges) => {
9756
+ if (reachesScene()) this.renderer.setNativeData?.(`${id}-pending`, ranges);
9757
+ }
9728
9758
  });
9729
9759
  }
9730
9760
  /**
@@ -9732,8 +9762,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
9732
9762
  * a pre-load style change is picked up by init's `setBarSeries`. With bars loaded, the
9733
9763
  * view is rebuilt in place (viewport preserved) and every Pine indicator re-executes:
9734
9764
  * its input data changed wholesale, which a streaming session's incremental context
9735
- * cannot absorb. Native indicators need nothing — their layers read the renderer's
9736
- * bars per frame.
9765
+ * cannot absorb. Natives computed from `ctx.bars()` re-run over the new candles (a
9766
+ * synthetic price style is their source too); layer natives that read the renderer's
9767
+ * bars per frame ignore the poke.
9737
9768
  */
9738
9769
  syncBarTransform(style) {
9739
9770
  const transform = barTransformFor(style);
@@ -9743,6 +9774,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
9743
9774
  this.bars = transform ? transform.full(this.rawBars) : this.rawBars;
9744
9775
  this.renderer.setBars(this.bars, { preserveView: true });
9745
9776
  this.reexecuteIndicators();
9777
+ if (this.switchingMarket) return;
9778
+ for (const record of this.registry.all()) {
9779
+ if (record.native?.started && record.renderHandle && !record.hidden) this.pokeNative(record);
9780
+ }
9746
9781
  }
9747
9782
  /**
9748
9783
  * Blank every mounted indicator's painted output — series data, fills, backgrounds,
@@ -9843,10 +9878,39 @@ var EngineOrchestrator = class _EngineOrchestrator {
9843
9878
  if (record.session) {
9844
9879
  record.pendingCause = cause;
9845
9880
  record.session.notifyBars(reason);
9846
- } else if (record.native) record.native.instance.onBars();
9881
+ } else if (record.native) this.pokeNative(record);
9847
9882
  }
9848
9883
  if (this.activeEngineStyle) this.typeEngines.get(this.activeEngineStyle)?.onBars?.();
9849
9884
  }
9885
+ /**
9886
+ * Tell a native its bars changed. A `waitsForHistory` native ignores every change while
9887
+ * the history is still loading — a backfill chunk, a deeper snapshot, a tick arriving
9888
+ * meanwhile — and its output stays on its bars through the anchor its emit carried.
9889
+ * Once the history is complete, a held one starts and a running one re-runs.
9890
+ */
9891
+ pokeNative(record) {
9892
+ const native = record.native;
9893
+ if (!native.descriptor.waitsForHistory) {
9894
+ native.instance.onBars();
9895
+ return;
9896
+ }
9897
+ if (this.historyState === "backfill") return;
9898
+ if (native.heldForHistory) {
9899
+ native.heldForHistory = false;
9900
+ const handle = this.handles.get(record.id);
9901
+ if (handle) void this.startNativeIndicator(record.id, handle);
9902
+ return;
9903
+ }
9904
+ if (native.started) native.instance.onBars();
9905
+ }
9906
+ /** Start the natives still held for a history that has completed without poking them
9907
+ * (a depth-only switch finishes its trim while the switch gates every poke). */
9908
+ releaseHeldNatives() {
9909
+ if (this.historyState === "backfill") return;
9910
+ for (const record of this.registry.all()) {
9911
+ if (record.native?.heldForHistory && !record.hidden) this.pokeNative(record);
9912
+ }
9913
+ }
9850
9914
  /** Gap healing needs a ranged feed and a provider-backed series (offline `data` has no source). */
9851
9915
  canHeal() {
9852
9916
  return !!this.feed.loadRange && !this.config.market.data?.length;
@@ -10138,7 +10202,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
10138
10202
  if (record?.native?.type === "volume") this.volumeOptedOut = true;
10139
10203
  if (record) this.settleCode(record, { ok: false, error: new Error(`[vela] updateCode("${id}") \u2014 the indicator was removed.`) });
10140
10204
  record?.session?.stop();
10141
- if (record?.native?.started) record.native.instance.stop();
10205
+ if (record?.native) this.stopNativeInstance(record.native);
10142
10206
  this.handles.delete(id);
10143
10207
  if (record?.renderHandle) this.renderer.removeIndicator(record.renderHandle);
10144
10208
  const paneId = record?.model?.paneId;
@@ -10259,7 +10323,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
10259
10323
  for (const record of this.registry.all()) {
10260
10324
  this.settleCode(record, { ok: false, error: new Error("[vela] updateCode \u2014 the chart was destroyed.") });
10261
10325
  record.session?.stop();
10262
- if (record.native?.started) record.native.instance.stop();
10326
+ if (record.native) this.stopNativeInstance(record.native);
10263
10327
  }
10264
10328
  for (const engine of this.typeEngines.values()) engine.stop();
10265
10329
  this.typeEngines.clear();
@@ -10386,7 +10450,19 @@ var EngineOrchestrator = class _EngineOrchestrator {
10386
10450
  await this.readyPromise;
10387
10451
  const record = this.registry.get(id);
10388
10452
  if (!record?.native || record.hidden) return;
10453
+ const native = record.native;
10454
+ if (native.started) return;
10455
+ if (native.descriptor.waitsForHistory && this.historyState === "backfill") {
10456
+ native.heldForHistory = true;
10457
+ this.mountNativePlaceholder(id, record);
10458
+ return;
10459
+ }
10460
+ native.heldForHistory = false;
10389
10461
  const channel = this.nativeChannel(record);
10462
+ let alive = true;
10463
+ native.revokeContext = () => {
10464
+ alive = false;
10465
+ };
10390
10466
  const ctx = {
10391
10467
  id,
10392
10468
  symbol: this.config.market.symbol ?? "TEST",
@@ -10395,14 +10471,18 @@ var EngineOrchestrator = class _EngineOrchestrator {
10395
10471
  session: this.config.market.session,
10396
10472
  bars: () => this.bars,
10397
10473
  data: this.dataControl,
10398
- emit: (out) => this.applyModel(id, this.buildNativeModel(record, out)),
10399
- pushData: (data) => this.renderer.setNativeData?.(channel, data),
10474
+ emit: (out) => {
10475
+ if (alive) this.applyModel(id, this.buildNativeModel(record, out, this.bars[0]?.time));
10476
+ },
10477
+ pushData: (data) => {
10478
+ if (alive) this.renderer.setNativeData?.(channel, data);
10479
+ },
10400
10480
  setStatus: (status) => {
10401
- if (record.renderHandle && !record.hidden) this.renderer.setIndicatorStatus?.(record.renderHandle, status);
10481
+ if (alive && record.renderHandle && !record.hidden) this.renderer.setIndicatorStatus?.(record.renderHandle, status);
10402
10482
  }
10403
10483
  };
10404
- record.native.instance.start(ctx, record.inputValues);
10405
- record.native.started = true;
10484
+ native.instance.start(ctx, record.inputValues);
10485
+ native.started = true;
10406
10486
  } catch (err) {
10407
10487
  this.fail(id, handle, err);
10408
10488
  }
@@ -10415,8 +10495,14 @@ var EngineOrchestrator = class _EngineOrchestrator {
10415
10495
  const { type, descriptor } = record.native;
10416
10496
  return descriptor.multiInstance ? nativeInstanceChannel(type, record.id) : type;
10417
10497
  }
10418
- /** Wrap a native indicator's emitted visuals into a full IndicatorModel, tagged `native`. */
10419
- buildNativeModel(record, out) {
10498
+ /**
10499
+ * Wrap a native indicator's emitted visuals into a full IndicatorModel, tagged `native`.
10500
+ * `anchorTime` is the first bar the output is index-aligned to — the chart's first bar
10501
+ * when it emitted. Without it the renderer pins the output's first slot to whatever the
10502
+ * first bar is NOW, so history landing in front of a computed output (or the oldest
10503
+ * bars trimmed under it) would slide the whole plot by that many bars.
10504
+ */
10505
+ buildNativeModel(record, out, anchorTime) {
10420
10506
  const d = record.native.descriptor;
10421
10507
  const type = record.native.type;
10422
10508
  const channel = this.nativeChannel(record);
@@ -10427,6 +10513,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
10427
10513
  overlay: d.overlay,
10428
10514
  paneHint: d.paneHint,
10429
10515
  native: { type, ...channel !== type ? { channel } : {} },
10516
+ ...anchorTime != null ? { anchorTime } : {},
10430
10517
  ...d.legend === false ? { legend: false } : {},
10431
10518
  ...out.paneAxis != null ? { paneAxis: out.paneAxis } : {},
10432
10519
  series: out.series ?? [],
@@ -10481,8 +10568,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
10481
10568
  const paneId = this.routePane(id, model, record.options ?? {});
10482
10569
  this.placeModel(model, id, paneId);
10483
10570
  record.model = model;
10484
- this.ensurePaneFor(paneId);
10571
+ const paneCreated = this.ensurePaneFor(paneId);
10485
10572
  record.renderHandle = this.renderer.mountIndicator(model);
10573
+ if (paneCreated) this.events.emit("pane:changed", void 0);
10486
10574
  record.pendingStructural = true;
10487
10575
  if (record.hidden) {
10488
10576
  this.renderer.setIndicatorVisible?.(record.renderHandle, false);
@@ -10506,24 +10594,49 @@ var EngineOrchestrator = class _EngineOrchestrator {
10506
10594
  const paneId = this.routePane(id, model, record.options ?? {});
10507
10595
  this.placeModel(model, id, paneId);
10508
10596
  record.model = model;
10509
- this.ensurePaneFor(paneId);
10597
+ const paneCreated = this.ensurePaneFor(paneId);
10510
10598
  record.renderHandle = this.renderer.mountIndicator(model);
10599
+ if (paneCreated) this.events.emit("pane:changed", void 0);
10511
10600
  record.pendingStructural = true;
10512
10601
  this.renderer.setIndicatorVisible?.(record.renderHandle, false);
10513
10602
  this.announce(record, this.handles.get(id));
10514
10603
  }
10604
+ /**
10605
+ * The legend row of a native HELD for the history (see `waitsForHistory`): an empty
10606
+ * model with the title and inputs schema, spinning, not announced — exactly a script
10607
+ * indicator's loading placeholder, so `ready`/`indicator:added` and `inspect()` still
10608
+ * mean "it produced output". A record that already has a row (restarted over a new
10609
+ * market, or shown again) keeps it and only spins. The first emit remounts over it.
10610
+ */
10611
+ mountNativePlaceholder(id, record) {
10612
+ record.pendingStructural = true;
10613
+ if (!record.renderHandle) {
10614
+ const model = this.buildNativeModel(record, {});
10615
+ const paneId = this.routePane(id, model, record.options ?? {});
10616
+ this.placeModel(model, id, paneId);
10617
+ record.model = model;
10618
+ const paneCreated = this.ensurePaneFor(paneId);
10619
+ record.renderHandle = this.renderer.mountIndicator(model);
10620
+ if (paneCreated) this.events.emit("pane:changed", void 0);
10621
+ }
10622
+ this.setLoading(record, true);
10623
+ }
10515
10624
  /** Flip the record's loading state and reflect it in the legend row (spinner on/off). */
10516
10625
  setLoading(record, loading) {
10517
10626
  record.loading = loading;
10518
10627
  if (record.renderHandle && !record.hidden) this.renderer.setIndicatorStatus?.(record.renderHandle, loading ? "loading" : "idle");
10519
10628
  }
10629
+ /** Make sure the renderer has the pane; true when it is new to the layout (the caller
10630
+ * emits `pane:changed` once the indicator's model sits on it, so `list()` shows it). */
10520
10631
  ensurePaneFor(paneId) {
10521
- if (!this.paneOrder.includes(paneId)) this.paneOrder.push(paneId);
10632
+ const created = !this.paneOrder.includes(paneId);
10633
+ if (created) this.paneOrder.push(paneId);
10522
10634
  this.renderer.ensurePane({
10523
10635
  id: paneId,
10524
10636
  kind: paneId === "price" ? "price" : "study",
10525
10637
  order: this.paneOrder.indexOf(paneId)
10526
10638
  });
10639
+ return created;
10527
10640
  }
10528
10641
  /** True when any (still-registered) indicator sits on the given pane. */
10529
10642
  paneStillUsed(paneId) {
@@ -10559,12 +10672,14 @@ var EngineOrchestrator = class _EngineOrchestrator {
10559
10672
  const panes = [];
10560
10673
  for (const paneId of this.paneOrder) {
10561
10674
  if (paneId !== "price" && !byPane.has(paneId)) continue;
10675
+ const heightWeight = this.renderer.paneHeightWeight?.(paneId) ?? null;
10562
10676
  panes.push({
10563
10677
  id: paneId,
10564
10678
  kind: paneId === "price" ? "price" : "study",
10565
10679
  order: panes.length,
10566
10680
  collapsed: this.collapsedPanes.has(paneId),
10567
10681
  maximized: this.maximizedPaneId === paneId,
10682
+ ...heightWeight !== null ? { heightWeight } : {},
10568
10683
  indicators: byPane.get(paneId) ?? []
10569
10684
  });
10570
10685
  }
@@ -10596,6 +10711,22 @@ var EngineOrchestrator = class _EngineOrchestrator {
10596
10711
  this.renderer.setPaneMaximized?.(paneId);
10597
10712
  this.events.emit("pane:changed", void 0);
10598
10713
  }
10714
+ setPaneHeightWeight(paneId, weight) {
10715
+ if (!this.renderer.setPaneHeightWeight) {
10716
+ console.warn("[vela] the active renderer does not support pane sizing \u2014 chart.panes.setHeightWeight ignored.");
10717
+ return;
10718
+ }
10719
+ if (typeof weight !== "number" || !Number.isFinite(weight) || weight <= 0) {
10720
+ console.warn(`[vela] chart.panes.setHeightWeight: weight must be a positive number (got ${String(weight)}).`);
10721
+ return;
10722
+ }
10723
+ if (!this.existingPaneIds().includes(paneId)) {
10724
+ console.warn(`[vela] chart.panes.setHeightWeight: no pane "${paneId}".`);
10725
+ return;
10726
+ }
10727
+ this.renderer.setPaneHeightWeight(paneId, weight);
10728
+ this.events.emit("pane:changed", void 0);
10729
+ }
10599
10730
  /** IndicatorController/PaneController: move (or merge) an indicator to another pane. */
10600
10731
  moveIndicator(id, target) {
10601
10732
  if (!this.paneManagementSupported() || !this.renderer.setIndicatorPane) {
@@ -10693,6 +10824,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
10693
10824
  this.maximizedPaneId = a.maximized ? a.paneId : null;
10694
10825
  this.events.emit("pane:changed", void 0);
10695
10826
  break;
10827
+ case "resize":
10828
+ this.events.emit("pane:changed", void 0);
10829
+ break;
10696
10830
  }
10697
10831
  }
10698
10832
  /** Fire `indicator:added` + the handle's `ready` once, on the first computed model. */
@@ -10723,14 +10857,16 @@ var EngineOrchestrator = class _EngineOrchestrator {
10723
10857
  const paneId2 = this.routePane(id, model, record.options ?? {});
10724
10858
  this.placeModel(model, id, paneId2);
10725
10859
  record.model = model;
10726
- this.ensurePaneFor(paneId2);
10860
+ const paneCreated = this.ensurePaneFor(paneId2);
10727
10861
  record.renderHandle = this.renderer.mountIndicator(model);
10728
10862
  record.pendingStructural = false;
10863
+ if (paneCreated) this.events.emit("pane:changed", void 0);
10729
10864
  this.announce(record, handle);
10730
10865
  return true;
10731
10866
  }
10732
10867
  let paneId = record.model?.paneId ?? "price";
10733
10868
  const prevOwnScale = record.model?.ownScale === true;
10869
+ let rerouted = false;
10734
10870
  if (record.loading && !record.paneLocked) {
10735
10871
  const routed = this.routePane(id, model, record.options ?? {});
10736
10872
  if (routed !== paneId) {
@@ -10741,6 +10877,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
10741
10877
  this.ensurePaneFor(routed);
10742
10878
  record.pendingStructural = true;
10743
10879
  paneId = routed;
10880
+ rerouted = true;
10744
10881
  }
10745
10882
  }
10746
10883
  this.placeModel(model, id, paneId);
@@ -10753,6 +10890,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
10753
10890
  } else {
10754
10891
  this.renderer.updateIndicator(record.renderHandle, modelToValuePatch(model));
10755
10892
  }
10893
+ if (rerouted) this.events.emit("pane:changed", void 0);
10756
10894
  if (record.loading) this.setLoading(record, false);
10757
10895
  this.announce(record, handle);
10758
10896
  return true;
@@ -10767,13 +10905,16 @@ var EngineOrchestrator = class _EngineOrchestrator {
10767
10905
  const overlay = options.overlay ?? model.overlay;
10768
10906
  return overlay ? "price" : `pane-${id}`;
10769
10907
  }
10770
- /** Each named value series' last plotted value — the model is the one source every
10771
- * engine fills, so this works without an execution context. */
10772
- static plotsAtLastBar(model) {
10908
+ /** Each named value series' value on the chart's last bar — the model is the one source
10909
+ * every engine fills, so this works without an execution context. Points past the last
10910
+ * bar (a series shifted forward) are skipped by time. */
10911
+ static plotsAtLastBar(model, lastBarTime) {
10773
10912
  const out = {};
10774
10913
  for (const spec of model.series) {
10775
10914
  if (!isLineLikeSeries(spec)) continue;
10776
- out[spec.title] = spec.points[spec.points.length - 1]?.value ?? null;
10915
+ let i = spec.points.length - 1;
10916
+ if (lastBarTime !== void 0) while (i >= 0 && spec.points[i].time > lastBarTime) i -= 1;
10917
+ out[spec.title] = spec.points[i]?.value ?? null;
10777
10918
  }
10778
10919
  return out;
10779
10920
  }
@@ -10829,7 +10970,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
10829
10970
  // whatever produced this run.
10830
10971
  forming: this.config.live && this.bars.length > 0,
10831
10972
  complete: this.historyState !== "backfill",
10832
- plots: _EngineOrchestrator.plotsAtLastBar(model),
10973
+ plots: _EngineOrchestrator.plotsAtLastBar(model, lastBar?.time),
10833
10974
  vars: snapshot?.variables ?? {},
10834
10975
  ...snapshot?.strategy ? { strategy: snapshot.strategy } : {},
10835
10976
  warnings: snapshot?.warnings ?? [],
@@ -10898,8 +11039,9 @@ function sameInputValue(a, b) {
10898
11039
  return typeof a === "object" && a !== null && typeof b === "object" && b !== null && JSON.stringify(a) === JSON.stringify(b);
10899
11040
  }
10900
11041
  function blankedModel(model) {
11042
+ const { anchorTime: _anchor, ...rest } = model;
10901
11043
  return {
10902
- ...model,
11044
+ ...rest,
10903
11045
  series: model.series.map((s) => s.kind === "candle" || s.kind === "bar" ? { ...s, bars: [] } : s.kind === "markers" ? { ...s, markers: [] } : { ...s, points: [] }),
10904
11046
  fills: [],
10905
11047
  backgrounds: [],
@@ -10922,12 +11064,14 @@ function modelToValuePatch(model) {
10922
11064
  if (s.kind === "candle" || s.kind === "bar") {
10923
11065
  series.push({ seriesId: s.id, kind: "bars", bars: s.bars, barColors: s.barColors ?? [] });
10924
11066
  for (const b of s.bars) {
11067
+ if (!b) continue;
10925
11068
  if (b.time < from) from = b.time;
10926
11069
  if (b.time > to) to = b.time;
10927
11070
  }
10928
11071
  } else if (isLineLikeSeries(s)) {
10929
11072
  series.push({ seriesId: s.id, kind: "points", points: s.points });
10930
11073
  for (const p of s.points) {
11074
+ if (!p) continue;
10931
11075
  if (p.time < from) from = p.time;
10932
11076
  if (p.time > to) to = p.time;
10933
11077
  }
@@ -11242,6 +11386,17 @@ var PanesControl = class {
11242
11386
  if (this.guard()) this.controller.maximizePane(paneId);
11243
11387
  return this;
11244
11388
  }
11389
+ /**
11390
+ * Set a pane's height weight: the panes share the plot height in proportion to their
11391
+ * weights, so `setHeightWeight('price', 6)` next to two study panes at 1 gives the price
11392
+ * pane three quarters of the height. The weight sticks until the user drags a separator
11393
+ * or it is set again; a collapsed pane applies it when expanded. `weight` must be a
11394
+ * positive number.
11395
+ */
11396
+ setHeightWeight(paneId, weight) {
11397
+ if (this.guard()) this.controller.setPaneHeightWeight(paneId, weight);
11398
+ return this;
11399
+ }
11245
11400
  /** Move/merge an indicator to a pane — sugar for `handle.moveTo(...)` by id. */
11246
11401
  moveIndicator(id, target) {
11247
11402
  if (this.guard()) this.controller.moveIndicator(id, target);
@@ -16566,6 +16721,8 @@ var LEGEND_TITLE_STATUS_GAP_PX = 8;
16566
16721
  var LEGEND_ACTION_GAP_PX = 6;
16567
16722
  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;`;
16568
16723
  var LEGEND_ROW_MIN_H = LEGEND_CTL_PX + LEGEND_ROW_PAD_Y * 2;
16724
+ var STACK_RAISE = "stack:raise";
16725
+ var STACK_LOWER = "stack:lower";
16569
16726
  var EYE_SVG = iconAt("eye", LEGEND_ICON_PX2);
16570
16727
  var EYE_OFF_SVG = iconAt("eye-off", LEGEND_ICON_PX2);
16571
16728
  var GEAR_SVG = iconAt("gear", LEGEND_ICON_PX2);
@@ -16595,6 +16752,10 @@ var InputsUI = class {
16595
16752
  this.mobileLayout = false;
16596
16753
  /** Pane move/merge hook — when set, rows get a "Move to" menu + become drag-to-pane sources. */
16597
16754
  this.moveApi = null;
16755
+ /** Overlay restacking hook — when set, a row's menus offer to step it forward or back. */
16756
+ this.stackApi = null;
16757
+ /** What {@link orderRows} last ordered the rows by (skips unchanged frames). */
16758
+ this.rowOrderKey = "";
16598
16759
  /** Host-contributed legend actions, resolved PER ROW at render time (see setLegendActions). */
16599
16760
  this.legendActions = null;
16600
16761
  /** Host-contributed callout bubbles, resolved PER ROW at render time (see setLegendCallouts). */
@@ -16733,6 +16894,10 @@ var InputsUI = class {
16733
16894
  setMoveApi(api) {
16734
16895
  this.moveApi = api;
16735
16896
  }
16897
+ /** Enable the context menu's "Bring in front of …" entry for overlay indicators. Null disables it. */
16898
+ setStackApi(api) {
16899
+ this.stackApi = api;
16900
+ }
16736
16901
  /**
16737
16902
  * Wire the host-contributed row actions. Re-calling replaces the provider and
16738
16903
  * re-projects the rows already on screen (a late `registerLegendAction` appears after
@@ -16878,16 +17043,20 @@ var InputsUI = class {
16878
17043
  }
16879
17044
  this.moveTargets = items.map((it) => it.target);
16880
17045
  this.moveMenuId = id;
16881
- const descriptors = items.map((it, i) => ({ id: String(i), label: it.label }));
17046
+ const stack = this.stackItems(id);
17047
+ const moves = items.map((it, i) => ({ id: String(i), label: it.label }));
17048
+ if (moves[0] && stack.length > 0) moves[0].separatorBefore = true;
17049
+ const descriptors = [...stack, ...moves];
16882
17050
  if (!this.moveMenu) {
16883
17051
  this.moveMenu = new Menu({
16884
17052
  host: this.container,
16885
17053
  items: descriptors,
16886
17054
  placement: "bottom-start",
16887
17055
  onSelect: (itemId) => {
16888
- const target = this.moveTargets[Number(itemId)];
16889
17056
  const rowId = this.moveMenuId;
16890
- if (target && rowId) api.move(rowId, target);
17057
+ if (!rowId || this.runStackItem(itemId, rowId)) return;
17058
+ const target = this.moveTargets[Number(itemId)];
17059
+ if (target) api.move(rowId, target);
16891
17060
  }
16892
17061
  });
16893
17062
  } else {
@@ -16900,18 +17069,24 @@ var InputsUI = class {
16900
17069
  this.moveMenu?.close();
16901
17070
  }
16902
17071
  // ── legend row context menu (right-click) ───────────────────────────────
16903
- /** Open the row's action menu at the pointer. One entry today: the values toggle. */
17072
+ /** Open the row's action menu at the pointer: an overlay indicator's restacking steps
17073
+ * first (where a layer is there to pass), then the values toggle. */
16904
17074
  openRowMenu(id, x, y) {
16905
17075
  this.closeRowMenu();
16906
17076
  this.closeMoveMenu();
16907
17077
  const row2 = this.rows.get(id);
16908
17078
  if (!row2) return;
16909
17079
  this.rowMenuId = id;
16910
- const items = [{
16911
- id: "values",
16912
- label: "Indicator values",
16913
- checked: this.rowValuesShown(row2)
16914
- }];
17080
+ const stack = this.stackItems(id);
17081
+ const items = [
17082
+ ...stack,
17083
+ {
17084
+ id: "values",
17085
+ label: "Indicator values",
17086
+ checked: this.rowValuesShown(row2),
17087
+ separatorBefore: stack.length > 0
17088
+ }
17089
+ ];
16915
17090
  if (!this.rowMenu) {
16916
17091
  this.rowMenu = new Menu({
16917
17092
  host: this.container,
@@ -16920,8 +17095,10 @@ var InputsUI = class {
16920
17095
  // Right-click action menu — checked state reads as a leading ✓ (see the kit Menu).
16921
17096
  checkmarks: true,
16922
17097
  onSelect: (itemId) => {
16923
- if (itemId !== "values" || !this.rowMenuId) return;
16924
- const target = this.rows.get(this.rowMenuId);
17098
+ const rowId = this.rowMenuId;
17099
+ if (!rowId || this.runStackItem(itemId, rowId)) return;
17100
+ if (itemId !== "values") return;
17101
+ const target = this.rows.get(rowId);
16925
17102
  if (!target) return;
16926
17103
  target.showValues = !this.rowValuesShown(target);
16927
17104
  this.renderRowValues(target);
@@ -16935,6 +17112,55 @@ var InputsUI = class {
16935
17112
  closeRowMenu() {
16936
17113
  this.rowMenu?.close();
16937
17114
  }
17115
+ /** A row's one-step restacking entries (forward, then back), for whichever neighbor exists. */
17116
+ stackItems(id) {
17117
+ const api = this.stackApi;
17118
+ if (!api) return [];
17119
+ const items = [];
17120
+ const forward = this.stackLabel("Bring in front of", api.above(id), "the indicator in front");
17121
+ if (forward) items.push({ id: STACK_RAISE, label: forward });
17122
+ const back = this.stackLabel("Send behind", api.below(id), "the indicator behind");
17123
+ if (back) items.push({ id: STACK_LOWER, label: back });
17124
+ return items;
17125
+ }
17126
+ stackLabel(verb, neighbor, fallback) {
17127
+ if (neighbor === null) return null;
17128
+ if (neighbor === "price") return `${verb} price`;
17129
+ return `${verb} ${this.rows.get(neighbor.indicator)?.title ?? fallback}`;
17130
+ }
17131
+ /** Run a restacking entry; false when `itemId` is not one. */
17132
+ runStackItem(itemId, rowId) {
17133
+ if (itemId === STACK_RAISE) this.stackApi?.raise(rowId);
17134
+ else if (itemId === STACK_LOWER) this.stackApi?.lower(rowId);
17135
+ else return false;
17136
+ return true;
17137
+ }
17138
+ /**
17139
+ * List each pane's rows front-most first, the way the indicators stack (the object
17140
+ * tree's order). `backToFront` is the paint order; rows it doesn't name keep their
17141
+ * place after the others. Cheap to call every frame: no-op while nothing changed.
17142
+ */
17143
+ orderRows(backToFront) {
17144
+ const key = `${backToFront.join(",")}|${[...this.rows.values()].map((r) => `${r.id}@${r.paneId}`).join(",")}`;
17145
+ if (key === this.rowOrderKey) return;
17146
+ this.rowOrderKey = key;
17147
+ const rank = new Map(backToFront.map((id, i) => [id, i]));
17148
+ for (const lg of this.legends.values()) {
17149
+ const els = [...this.rows.values()].filter((r) => r.el.parentElement === lg).map((r) => r.el);
17150
+ const dom = [...lg.children];
17151
+ const ofEl = new Map([...this.rows.values()].map((r) => [r.el, r.id]));
17152
+ const at = (el) => rank.get(ofEl.get(el)) ?? -1;
17153
+ els.sort((a, b) => {
17154
+ const ra = at(a);
17155
+ const rb = at(b);
17156
+ if (ra >= 0 && rb >= 0) return rb - ra;
17157
+ if (ra >= 0 || rb >= 0) return ra >= 0 ? -1 : 1;
17158
+ return dom.indexOf(a) - dom.indexOf(b);
17159
+ });
17160
+ const anchor = this.foldToggle?.parentElement === lg ? this.foldToggle : null;
17161
+ for (const el of els) lg.insertBefore(el, anchor);
17162
+ }
17163
+ }
16938
17164
  /** Move a legend row to another pane (indicator merged/moved), tidying an emptied container. */
16939
17165
  setPane(id, paneId) {
16940
17166
  const row2 = this.rows.get(id);
@@ -18625,6 +18851,7 @@ var WebGL2Backend = class {
18625
18851
  const i0 = Math.max(0, Math.floor(vr.from));
18626
18852
  const i1 = Math.min(n - 1, Math.ceil(vr.to));
18627
18853
  if (i1 < i0) return;
18854
+ const s1 = Math.max(i1, Math.ceil(vr.to));
18628
18855
  gl.enable(gl.BLEND);
18629
18856
  gl.enable(gl.SCISSOR_TEST);
18630
18857
  const barColorMap = mergeBarColors(scene.indicators);
@@ -18683,8 +18910,8 @@ var WebGL2Backend = class {
18683
18910
  b.alpha = this.modelAlpha;
18684
18911
  const off = scene.offsetOf(m.id);
18685
18912
  const mp = effPane(m);
18686
- for (const f of m.fills) if (f.overlay !== true) this.emitFill(b, m, f, mp, coords, i0, i1, off);
18687
- for (const s of m.series) if (s.overlay !== true) this.emitSeries(b, s, mp, coords, i0, i1, theme, off);
18913
+ for (const f of m.fills) if (f.overlay !== true) this.emitFill(b, m, f, mp, coords, i0, s1, off);
18914
+ for (const s of m.series) if (s.overlay !== true) this.emitSeries(b, s, mp, coords, i0, s1, theme, off);
18688
18915
  }
18689
18916
  if (drawCandles && !candleDrawn) {
18690
18917
  drawSlicesUpTo(scene.candleZ);
@@ -18695,11 +18922,11 @@ var WebGL2Backend = class {
18695
18922
  if (isPrice) {
18696
18923
  for (const m of scene.indicators.values()) {
18697
18924
  const off = scene.offsetOf(m.id);
18698
- for (const f of m.fills) if (f.overlay === true) this.emitFill(b, m, f, pane, coords, i0, i1, off);
18925
+ for (const f of m.fills) if (f.overlay === true) this.emitFill(b, m, f, pane, coords, i0, s1, off);
18699
18926
  }
18700
18927
  for (const m of scene.indicators.values()) {
18701
18928
  const off = scene.offsetOf(m.id);
18702
- for (const s of m.series) if (s.overlay === true) this.emitSeries(b, s, pane, coords, i0, i1, theme, off);
18929
+ for (const s of m.series) if (s.overlay === true) this.emitSeries(b, s, pane, coords, i0, s1, theme, off);
18703
18930
  }
18704
18931
  }
18705
18932
  drawSlicesUpTo(Infinity);
@@ -18721,7 +18948,7 @@ var WebGL2Backend = class {
18721
18948
  if (this.glow > 0 && !this.screenProgram && !this.ensureScreenPipeline()) this.glow = 0;
18722
18949
  if (this.glow > 0 && this.screenProgram) {
18723
18950
  this.glowBatch.alpha = this.modelAlpha;
18724
- this.renderGlow(gl, scene, coords, i0, i1);
18951
+ this.renderGlow(gl, scene, coords, i0, s1);
18725
18952
  }
18726
18953
  }
18727
18954
  /**
@@ -19278,7 +19505,7 @@ var WebGL2Backend = class {
19278
19505
  const tickW = Math.max(1, Math.round(spacing * 0.35));
19279
19506
  for (let i = i0; i <= i1; i += 1) {
19280
19507
  const bar = s.bars[i - off];
19281
- if (!bar) continue;
19508
+ if (!bar || !Number.isFinite(bar.open + bar.high + bar.low + bar.close)) continue;
19282
19509
  const bc = s.barColors?.[i - off];
19283
19510
  const body = parseColor(bc?.color ?? (bar.close >= bar.open ? up : down));
19284
19511
  const x = Math.round(coords.logicalToX(i));
@@ -19931,6 +20158,8 @@ var InputController = class {
19931
20158
  const tapRelease = this.dragging && !this.moved && (!wasTouch || Math.hypot(x - this.startX, y - this.startY) <= TOUCH_TAP_SLOP);
19932
20159
  if (this.dragging && this.region === "drawing") {
19933
20160
  this.deps.drawingsPointerUp?.(x, y, this.snapMode(e));
20161
+ } else if (this.dragging && this.region === "separator") {
20162
+ this.deps.endPaneResize?.();
19934
20163
  } else if (tapRelease && this.region === "data") {
19935
20164
  this.deps.onClick(x, y);
19936
20165
  } else if (this.dragging && this.region === "data" && wasTouch) {
@@ -19952,6 +20181,7 @@ var InputController = class {
19952
20181
  this.cancelLongPress();
19953
20182
  if (!this.dragging) return;
19954
20183
  if (this.region === "drawing" && !Number.isNaN(this.cursorX)) this.deps.drawingsPointerUp?.(this.cursorX, this.cursorY, this.snapMode(e));
20184
+ if (this.region === "separator") this.deps.endPaneResize?.();
19955
20185
  if (this.region === "crosshair" || e.pointerType === "touch") this.deps.onPointerMove(null, null);
19956
20186
  this.endGesture(e);
19957
20187
  };
@@ -20628,9 +20858,12 @@ var SceneGraph = class {
20628
20858
  setIndicatorZ(id, z) {
20629
20859
  this.seriesZ.set(id, z);
20630
20860
  }
20631
- /** Snapshot of the current ordering for a UI/read API: `{ id, z }` sorted by z. */
20861
+ /** Snapshot of the current ordering for a UI/read API: `{ id, z }` back to front, in the
20862
+ * order the indicators paint — on a shared key, mount order (see `orderedIndicatorsForPane`). */
20632
20863
  indicatorZOrder() {
20633
- return [...this.seriesZ.entries()].map(([id, z]) => ({ id, z })).sort((a, b) => a.z - b.z);
20864
+ const mounted = new Map([...this.indicators.keys()].map((id, i) => [id, i]));
20865
+ const at = (id) => mounted.get(id) ?? Number.MAX_SAFE_INTEGER;
20866
+ return [...this.seriesZ.entries()].map(([id, z]) => ({ id, z })).sort((a, b) => a.z - b.z || at(a.id) - at(b.id));
20634
20867
  }
20635
20868
  /** The pane's series z keys (each indicator, plus the candles on the price pane), sorted
20636
20869
  * ascending and de-duplicated — the boundaries a user drawing's z is slotted against. */
@@ -20658,16 +20891,19 @@ var SceneGraph = class {
20658
20891
  for (const z of this.seriesZ.values()) if (z < min) min = z;
20659
20892
  return min;
20660
20893
  }
20894
+ /** Create the pane, or update an existing one in place. Without `heightWeight` an existing
20895
+ * pane keeps its weight (a user resize survives other indicators mounting onto it) and a
20896
+ * new one gets the default: 3 for the price pane, 1 for a study pane. */
20661
20897
  ensurePane(id, kind, order, heightWeight) {
20662
20898
  this.orderedCache = null;
20663
20899
  const existing = this.panes.get(id);
20664
20900
  if (existing) {
20665
20901
  existing.order = order;
20666
- existing.heightWeight = heightWeight;
20902
+ if (heightWeight !== void 0) existing.heightWeight = heightWeight;
20667
20903
  existing.kind = kind;
20668
20904
  return existing;
20669
20905
  }
20670
- 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 };
20906
+ 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 };
20671
20907
  this.panes.set(id, pane);
20672
20908
  return pane;
20673
20909
  }
@@ -20724,6 +20960,7 @@ var Canvas2dBackend = class {
20724
20960
  const i0 = Math.max(0, Math.floor(vr.from));
20725
20961
  const i1 = Math.min(n - 1, Math.ceil(vr.to));
20726
20962
  if (i1 < i0) return;
20963
+ const s1 = Math.max(i1, Math.ceil(vr.to));
20727
20964
  const barColorMap = mergeBarColors2(scene.indicators);
20728
20965
  const panes = scene.orderedPanes();
20729
20966
  for (const pane of panes) {
@@ -20766,8 +21003,8 @@ var Canvas2dBackend = class {
20766
21003
  ctx.globalAlpha = this.modelAlpha;
20767
21004
  const off = scene.offsetOf(m.id);
20768
21005
  const mp = effPane(m);
20769
- for (const f of m.fills) if (f.overlay !== true) this.drawFill(ctx, m, f, mp, coords, i0, i1, off);
20770
- for (const s of m.series) if (s.overlay !== true) this.drawSeries(ctx, s, mp, coords, i0, i1, theme, off);
21006
+ for (const f of m.fills) if (f.overlay !== true) this.drawFill(ctx, m, f, mp, coords, i0, s1, off);
21007
+ for (const s of m.series) if (s.overlay !== true) this.drawSeries(ctx, s, mp, coords, i0, s1, theme, off);
20771
21008
  }
20772
21009
  if (drawCandles && !candleDrawn) {
20773
21010
  drawSlicesUpTo(scene.candleZ);
@@ -20778,11 +21015,11 @@ var Canvas2dBackend = class {
20778
21015
  ctx.globalAlpha = this.modelAlpha;
20779
21016
  for (const m of scene.indicators.values()) {
20780
21017
  const off = scene.offsetOf(m.id);
20781
- for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0, i1, off);
21018
+ for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0, s1, off);
20782
21019
  }
20783
21020
  for (const m of scene.indicators.values()) {
20784
21021
  const off = scene.offsetOf(m.id);
20785
- for (const s of m.series) if (s.overlay === true) this.drawSeries(ctx, s, pane, coords, i0, i1, theme, off);
21022
+ for (const s of m.series) if (s.overlay === true) this.drawSeries(ctx, s, pane, coords, i0, s1, theme, off);
20786
21023
  }
20787
21024
  }
20788
21025
  drawSlicesUpTo(Infinity);
@@ -21234,7 +21471,7 @@ var Canvas2dBackend = class {
21234
21471
  const tickW = Math.max(1, Math.round(spacing * 0.35));
21235
21472
  for (let i = i0; i <= i1; i += 1) {
21236
21473
  const b = s.bars[i - off];
21237
- if (!b) continue;
21474
+ if (!b || !Number.isFinite(b.open + b.high + b.low + b.close)) continue;
21238
21475
  const bc = s.barColors?.[i - off];
21239
21476
  const body = bc?.color ?? (b.close >= b.open ? up : down);
21240
21477
  const x = Math.round(coords.logicalToX(i)) + 0.5;
@@ -32122,6 +32359,58 @@ function stackLayers(entries, candleZ) {
32122
32359
  return { below, above };
32123
32360
  }
32124
32361
 
32362
+ // src/renderers/native/core/overlayStack.ts
32363
+ function priceStack(indicators, candleZ) {
32364
+ const out = [...indicators];
32365
+ const at = out.findIndex((e) => e.z >= candleZ);
32366
+ out.splice(at < 0 ? out.length : at, 0, { id: null, z: candleZ });
32367
+ return out;
32368
+ }
32369
+ function untie(stack) {
32370
+ const out = [];
32371
+ for (let i = 0; i < stack.length; ) {
32372
+ let j = i + 1;
32373
+ while (j < stack.length && stack[j].z === stack[i].z) j += 1;
32374
+ const run = j - i;
32375
+ if (run > 1) {
32376
+ const base = stack[i].z;
32377
+ const step = ((stack[j]?.z ?? base + 1) - base) / run;
32378
+ for (let k = 1; k < run; k += 1) {
32379
+ const id = stack[i + k].id;
32380
+ if (id !== null) out.push({ id, z: base + k * step });
32381
+ }
32382
+ }
32383
+ i = j;
32384
+ }
32385
+ return out;
32386
+ }
32387
+ function layerAbove(stack, id) {
32388
+ const i = stack.findIndex((e) => e.id === id);
32389
+ if (i < 0 || i === stack.length - 1) return null;
32390
+ const above = stack[i + 1];
32391
+ return above.id === null ? "price" : { indicator: above.id };
32392
+ }
32393
+ function layerBelow(stack, id) {
32394
+ const i = stack.findIndex((e) => e.id === id);
32395
+ if (i <= 0) return null;
32396
+ const below = stack[i - 1];
32397
+ return below.id === null ? "price" : { indicator: below.id };
32398
+ }
32399
+ function raisedZ(stack, id) {
32400
+ const i = stack.findIndex((e) => e.id === id);
32401
+ if (i < 0 || i === stack.length - 1) return null;
32402
+ const above = stack[i + 1];
32403
+ const next = stack[i + 2];
32404
+ return next ? (above.z + next.z) / 2 : above.z + 1;
32405
+ }
32406
+ function loweredZ(stack, id) {
32407
+ const i = stack.findIndex((e) => e.id === id);
32408
+ if (i <= 0) return null;
32409
+ const below = stack[i - 1];
32410
+ const prev2 = stack[i - 2];
32411
+ return prev2 ? (prev2.z + below.z) / 2 : below.z - 1;
32412
+ }
32413
+
32125
32414
  // src/renderers/native/chrome/vela-logos.ts
32126
32415
  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>';
32127
32416
  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>';
@@ -32772,6 +33061,8 @@ var NativeRenderer = class {
32772
33061
  // pane just below it
32773
33062
  this.resizeSplitStart = null;
32774
33063
  // the two panes' shared span when the drag began
33064
+ this.resizeStartAboveWeight = 0;
33065
+ // the upper pane's weight when the drag began
32775
33066
  /** The user drawings take part in pointer input (the runtime `drawingsInteractive` feature). */
32776
33067
  this.drawingsInteractive = true;
32777
33068
  this.hoverSeparatorY = null;
@@ -33414,6 +33705,7 @@ var NativeRenderer = class {
33414
33705
  };
33415
33706
  this.scene.candleZ = next.stacking.candles;
33416
33707
  for (const [id, z] of Object.entries(next.stacking.series)) this.scene.setIndicatorZ(id, z);
33708
+ this.syncLegendOrder();
33417
33709
  s.bars = { upColor: next.bars.upColor, downColor: next.bars.downColor };
33418
33710
  s.line = { color: next.line.color, width: next.line.width };
33419
33711
  s.area = { lineColor: next.area.lineColor, width: next.area.width, topColor: next.area.topColor, bottomColor: next.area.bottomColor };
@@ -33459,6 +33751,7 @@ var NativeRenderer = class {
33459
33751
  if (typeof v.z === "number") this.scene.setIndicatorZ(v.id, v.z);
33460
33752
  else if (v.to === "front") this.scene.bringIndicatorToFront(v.id);
33461
33753
  else if (v.to === "back") this.scene.sendIndicatorToBack(v.id);
33754
+ this.syncLegendOrder();
33462
33755
  }
33463
33756
  /** Toggle the in-chart settings gear + dialog (the `settings` feature). The gear
33464
33757
  * sits above the time axis, clear of the right price-axis strip. */
@@ -33846,7 +34139,7 @@ var NativeRenderer = class {
33846
34139
  this.chrome.mount(this.chromeCanvas);
33847
34140
  this.crosshairLayer.mount(this.cursorCanvas);
33848
34141
  this.coords.setViewport(defaultViewport());
33849
- this.scene.ensurePane(PRICE_PANE_ID, "price", 0, 3);
34142
+ this.scene.ensurePane(PRICE_PANE_ID, "price", 0);
33850
34143
  this.scheduler = new Scheduler((level) => this.renderFrame(level));
33851
34144
  this.animator = new Animator((dt) => this.animTick(dt));
33852
34145
  this.targetBarSpacing = this.coords.getViewport().barSpacing;
@@ -33859,7 +34152,7 @@ var NativeRenderer = class {
33859
34152
  onClick: (x, y) => {
33860
34153
  if (this.handleMarkClick(x, y)) return;
33861
34154
  this.userDrawings?.deselect();
33862
- this.handleClick(x);
34155
+ this.handleClick(x, y);
33863
34156
  },
33864
34157
  onAxisLongPress: (axis, x, y) => {
33865
34158
  for (const cb of this.axisLongPressCbs) cb({ axis, x, y });
@@ -33873,6 +34166,7 @@ var NativeRenderer = class {
33873
34166
  paneSeparatorAt: (y) => this.paneSeparatorAt(y),
33874
34167
  beginPaneResize: (y) => this.beginPaneResize(y),
33875
34168
  paneResizeBy: (dy) => this.paneResizeBy(dy),
34169
+ endPaneResize: () => this.endPaneResize(),
33876
34170
  resetPaneSize: (y) => this.resetPaneSize(y),
33877
34171
  resetView: () => this.resetView(),
33878
34172
  // User drawings claim a gesture before pan when armed / over a drawing.
@@ -33998,6 +34292,12 @@ var NativeRenderer = class {
33998
34292
  for (const cb of this.moveIndicatorCbs) cb(id, target);
33999
34293
  }
34000
34294
  });
34295
+ this.inputsUI.setStackApi({
34296
+ above: (id) => layerAbove(this.overlayStack(), id),
34297
+ below: (id) => layerBelow(this.overlayStack(), id),
34298
+ raise: (id) => this.restackOverlay(id, raisedZ),
34299
+ lower: (id) => this.restackOverlay(id, loweredZ)
34300
+ });
34001
34301
  this.paneControls = new PaneControls(this.plot, theme, {
34002
34302
  panes: () => this.paneControlViews(),
34003
34303
  rightAxis: () => this.rightAxisW,
@@ -34315,7 +34615,7 @@ var NativeRenderer = class {
34315
34615
  }
34316
34616
  // ── panes ──
34317
34617
  ensurePane(pane) {
34318
- this.scene.ensurePane(pane.id, pane.kind, pane.order, pane.heightWeight ?? (pane.kind === "price" ? 3 : 1));
34618
+ this.scene.ensurePane(pane.id, pane.kind, pane.order, pane.heightWeight);
34319
34619
  this.layoutPanes();
34320
34620
  this.paneControls?.refresh();
34321
34621
  this.scheduler.invalidate(4 /* Full */);
@@ -34337,6 +34637,7 @@ var NativeRenderer = class {
34337
34637
  model.ownScale = opts?.ownScale === true;
34338
34638
  if (!model.ownScale) this.scene.dropIndicatorScale(handle.id);
34339
34639
  this.inputsUI.setPane(handle.id, paneId);
34640
+ this.syncLegendOrder();
34340
34641
  this.refreshAnchorOffset(model);
34341
34642
  this.refreshAxisWidth();
34342
34643
  this.layoutPanes();
@@ -34364,6 +34665,15 @@ var NativeRenderer = class {
34364
34665
  this.paneControls?.refresh();
34365
34666
  this.scheduler.invalidate(4 /* Full */);
34366
34667
  }
34668
+ setPaneHeightWeight(paneId, weight) {
34669
+ const pane = this.scene.panes.get(paneId);
34670
+ if (!pane || !(weight > 0) || pane.heightWeight === weight) return;
34671
+ pane.heightWeight = weight;
34672
+ this.afterPaneLayoutChange();
34673
+ }
34674
+ paneHeightWeight(paneId) {
34675
+ return this.scene.panes.get(paneId)?.heightWeight ?? null;
34676
+ }
34367
34677
  onPaneAction(cb) {
34368
34678
  this.paneActionCbs.add(cb);
34369
34679
  return () => this.paneActionCbs.delete(cb);
@@ -34458,6 +34768,7 @@ var NativeRenderer = class {
34458
34768
  this.vpvrHidden = false;
34459
34769
  }
34460
34770
  if (model.ownScale) this.refreshAxisWidth();
34771
+ this.syncLegendOrder();
34461
34772
  this.paneControls?.refresh();
34462
34773
  this.scheduler.invalidate(4 /* Full */);
34463
34774
  return { id: model.id };
@@ -34865,7 +35176,7 @@ var NativeRenderer = class {
34865
35176
  this.setMarkHover(null);
34866
35177
  this.scheduler.invalidate(1 /* Cursor */);
34867
35178
  this.hoverLogical = null;
34868
- const empty = { time: null, price: null, paneKind: null, values: /* @__PURE__ */ new Map(), ohlc: null };
35179
+ const empty = { time: null, price: null, paneKind: null, paneId: null, values: /* @__PURE__ */ new Map(), ohlc: null };
34869
35180
  for (const cb of this.crosshairCbs) cb(empty);
34870
35181
  return;
34871
35182
  }
@@ -34896,14 +35207,23 @@ var NativeRenderer = class {
34896
35207
  const bar = onBar ? this.bars[logical] : void 0;
34897
35208
  const ohlc = bar ? { time: bar.time, open: bar.open, high: bar.high, low: bar.low, close: bar.close, volume: bar.volume } : null;
34898
35209
  this.hoverLogical = onBar ? logical : null;
34899
- const event = { time, price, paneKind: inData && pane ? pane.kind : null, values, ohlc };
35210
+ const onPane = inData && pane ? pane : null;
35211
+ const event = { time, price, paneKind: onPane?.kind ?? null, paneId: onPane?.id ?? null, values, ohlc };
34900
35212
  for (const cb of this.crosshairCbs) cb(event);
34901
35213
  }
34902
- handleClick(x) {
35214
+ handleClick(x, y) {
34903
35215
  if (this.coords.barCount === 0) return;
34904
35216
  const logical = Math.round(this.coords.xToLogical(x));
34905
35217
  const onBar = logical >= 0 && logical < this.coords.barCount;
34906
- for (const cb of this.clickCbs) cb({ time: onBar ? this.coords.logicalToTime(logical) : null, price: null });
35218
+ const inData = x >= 0 && y >= 0 && x <= this.coords.width && y <= this.coords.height;
35219
+ const pane = inData ? this.paneNodeAtY(y) : null;
35220
+ const event = {
35221
+ time: onBar ? this.coords.logicalToTime(logical) : null,
35222
+ price: pane ? this.coords.yToPrice(y, pane.scale, pane.bounds) : null,
35223
+ paneKind: pane?.kind ?? null,
35224
+ paneId: pane?.id ?? null
35225
+ };
35226
+ for (const cb of this.clickCbs) cb(event);
34907
35227
  }
34908
35228
  /**
34909
35229
  * Fan out (or collapse, with null) a multi-group mark stack; repaints the chrome tier when
@@ -35148,6 +35468,7 @@ var NativeRenderer = class {
35148
35468
  const below = panes[i];
35149
35469
  this.resizeAbove = above;
35150
35470
  this.resizeBelow = below;
35471
+ this.resizeStartAboveWeight = above.heightWeight;
35151
35472
  this.resizeSplitStart = {
35152
35473
  combinedTop: above.bounds.top,
35153
35474
  combinedHeight: above.bounds.height + below.bounds.height,
@@ -35168,6 +35489,16 @@ var NativeRenderer = class {
35168
35489
  this.layoutPanes();
35169
35490
  this.scheduler.invalidate(4 /* Full */);
35170
35491
  }
35492
+ /** Release the separator: report the resize once, at the end of the drag, if it changed anything. */
35493
+ endPaneResize() {
35494
+ const above = this.resizeAbove;
35495
+ const below = this.resizeBelow;
35496
+ const startAbove = this.resizeStartAboveWeight;
35497
+ this.resizeAbove = null;
35498
+ this.resizeBelow = null;
35499
+ this.resizeSplitStart = null;
35500
+ if (above && below && above.heightWeight !== startAbove) this.emitPaneAction({ type: "resize", paneIds: [above.id, below.id] });
35501
+ }
35171
35502
  /** Double-click a separator → split the two adjacent panes evenly (each gets half of
35172
35503
  * their combined weight). A simple, predictable reset that leaves siblings untouched. */
35173
35504
  resetPaneSize(y) {
@@ -35181,6 +35512,7 @@ var NativeRenderer = class {
35181
35512
  below.heightWeight = half;
35182
35513
  this.layoutPanes();
35183
35514
  this.scheduler.invalidate(4 /* Full */);
35515
+ this.emitPaneAction({ type: "resize", paneIds: [above.id, below.id] });
35184
35516
  }
35185
35517
  // ── keyboard navigation / accessibility (item 11) ──
35186
35518
  /** Enable/disable keyboard control: focusability + ARIA + key listeners + live region. */
@@ -35435,6 +35767,7 @@ var NativeRenderer = class {
35435
35767
  /** Paint the below-data (L-1) + geometry (L0) + chrome (L1) layers from the current scene/coords. */
35436
35768
  paintData() {
35437
35769
  this.syncLayerCanvasOrder();
35770
+ this.syncLegendOrder();
35438
35771
  this.stampScaleInvert();
35439
35772
  this.backend.modelAlpha = this.modelAlpha;
35440
35773
  this.backend.candleBodyAlpha = this.candleBodyAlpha;
@@ -35616,6 +35949,7 @@ var NativeRenderer = class {
35616
35949
  const i0 = Math.max(0, Math.floor(vr.from));
35617
35950
  const i1 = Math.min(n - 1, Math.ceil(vr.to));
35618
35951
  if (i1 < i0) return;
35952
+ const s1 = Math.max(i1, Math.ceil(vr.to));
35619
35953
  this.scene.percentBaseline = this.bars[i0]?.close ?? 0;
35620
35954
  const panes = this.scene.orderedPanes();
35621
35955
  const pricePane = panes.find((p) => p.kind === "price") ?? null;
@@ -35633,11 +35967,11 @@ var NativeRenderer = class {
35633
35967
  const masterModels = models.filter((m) => m.ownScale !== true);
35634
35968
  let dr = this.chrome.paneDrawingsRange(masterModels, this.scene, pane === pricePane, vr);
35635
35969
  if (pane === pricePane) {
35636
- const or = overlaySeriesRange(this.scene.indicators.values(), i0, i1, (id) => this.scene.offsetOf(id));
35970
+ const or = overlaySeriesRange(this.scene.indicators.values(), i0, s1, (id) => this.scene.offsetOf(id));
35637
35971
  if (or) dr = dr ? { min: Math.min(dr.min, or.min), max: Math.max(dr.max, or.max) } : or;
35638
35972
  }
35639
35973
  const includeCandles = pane.kind === "price" && (!this.scene.candlesHidden || this.priceLayersAnchoredToBars(masterModels) || !this.paneHasMeasurableContent(masterModels, dr));
35640
- pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id), margins);
35974
+ pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, s1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id), margins);
35641
35975
  pane.percentBaseline = pane.kind === "price" ? this.bars[i0]?.close ?? 0 : this.firstVisibleValue(masterModels, i0);
35642
35976
  pane.axisFormat = void 0;
35643
35977
  pane.axisBands = void 0;
@@ -35677,7 +36011,7 @@ var NativeRenderer = class {
35677
36011
  continue;
35678
36012
  }
35679
36013
  const mdr = this.chrome.paneDrawingsRange([model], this.scene, false, vr);
35680
- sl.scaleTarget = computePaneScale([model], this.bars, false, i0, i1, mdr, false, (id) => this.scene.offsetOf(id), margins);
36014
+ sl.scaleTarget = computePaneScale([model], this.bars, false, i0, s1, mdr, false, (id) => this.scene.offsetOf(id), margins);
35681
36015
  if (!animating || !sl.initialized) {
35682
36016
  sl.scale = { ...sl.scaleTarget };
35683
36017
  sl.initialized = true;
@@ -35748,6 +36082,30 @@ var NativeRenderer = class {
35748
36082
  const master = models.find((m) => m.ownScale !== true) ?? models[0];
35749
36083
  return master?.title || null;
35750
36084
  }
36085
+ /**
36086
+ * The price pane's layers back to front, as the legend restacks them: every indicator
36087
+ * with a legend row, in paint order, plus the candles. The same layers and order the
36088
+ * object tree lists (see `indicatorZOrder`).
36089
+ */
36090
+ overlayStack() {
36091
+ const painted = this.scene.orderedIndicatorsForPane(PRICE_PANE_ID).filter((m) => m.legend !== false).map((m) => ({ id: m.id, z: this.scene.zOf(m.id) }));
36092
+ return priceStack(painted, this.scene.candleZ);
36093
+ }
36094
+ /** List the legend rows the way the indicators stack, front-most first (no-op when unchanged). */
36095
+ syncLegendOrder() {
36096
+ this.inputsUI?.orderRows(this.scene.indicatorZOrder().map((e) => e.id));
36097
+ }
36098
+ /** Apply a legend restacking step: shared keys are spread apart first (the order stays),
36099
+ * so the step lands exactly one layer over. No-op when there is nothing to step over. */
36100
+ restackOverlay(id, step) {
36101
+ for (const fix of untie(this.overlayStack())) this.scene.setIndicatorZ(fix.id, fix.z);
36102
+ const z = step(this.overlayStack(), id);
36103
+ if (z === null) return;
36104
+ this.scene.setIndicatorZ(id, z);
36105
+ this.syncLegendOrder();
36106
+ this.scheduler.invalidate(4 /* Full */);
36107
+ for (const cb of this.configChangedCbs) cb();
36108
+ }
35751
36109
  /** The pane a bespoke-layer native indicator (volume/vpvr) currently lives in (null = not mounted). */
35752
36110
  nativeLayerPane(type) {
35753
36111
  for (const m of this.scene.indicators.values()) {
@@ -36202,17 +36560,35 @@ function leadingPointsBefore(model, headTime) {
36202
36560
  for (const s of model.series) {
36203
36561
  const times = s.kind === "candle" || s.kind === "bar" ? s.bars : isLineLikeSeries(s) ? s.points : null;
36204
36562
  if (!times || times.length === 0) continue;
36205
- let lo = 0;
36206
- let hi = times.length;
36207
- while (lo < hi) {
36208
- const mid = lo + hi >> 1;
36209
- if (times[mid].time < headTime) lo = mid + 1;
36210
- else hi = mid;
36211
- }
36212
- return lo;
36563
+ const count = slotsBefore(times, headTime);
36564
+ if (count !== null) return count;
36213
36565
  }
36214
36566
  return null;
36215
36567
  }
36568
+ function slotsBefore(times, headTime) {
36569
+ let lo = 0;
36570
+ let hi = times.length;
36571
+ while (lo < hi) {
36572
+ const mid = lo + hi >> 1;
36573
+ const t = times[mid]?.time;
36574
+ if (t === void 0) return slotsBeforeScan(times, headTime);
36575
+ if (t < headTime) lo = mid + 1;
36576
+ else hi = mid;
36577
+ }
36578
+ return lo;
36579
+ }
36580
+ function slotsBeforeScan(times, headTime) {
36581
+ let lastBefore = -1;
36582
+ let any = false;
36583
+ for (let i = 0; i < times.length; i += 1) {
36584
+ const t = times[i]?.time;
36585
+ if (t === void 0) continue;
36586
+ any = true;
36587
+ if (t >= headTime) return t === headTime ? i : lastBefore + 1;
36588
+ lastBefore = i;
36589
+ }
36590
+ return any ? times.length : null;
36591
+ }
36216
36592
  function applyPatch(model, patch) {
36217
36593
  if (patch.kind === "value") {
36218
36594
  for (const delta of patch.series) {
@@ -36625,6 +37001,9 @@ function classicDescriptor(spec) {
36625
37001
  overlay: spec.overlay,
36626
37002
  // Studies stack (a 20 and a 200 moving average side by side) — unlike the layer-backed natives.
36627
37003
  multiInstance: true,
37004
+ // A study's values depend on every bar before them (an EMA's seed, an RSI's
37005
+ // smoothing), so it computes once over the whole history, as script indicators do.
37006
+ waitsForHistory: true,
36628
37007
  inputsSchema: () => spec.inputs,
36629
37008
  defaultInputs: () => Object.fromEntries(spec.inputs.map((i) => [i.key, i.defval])),
36630
37009
  create: () => new ClassicIndicator(spec)
@@ -37460,41 +37839,50 @@ var maEnvelope = {
37460
37839
  return directionalChannel(bars, basis, map(basis, (x) => x * (1 + k)), map(basis, (x) => x * (1 - k)), ink, "envelope");
37461
37840
  }
37462
37841
  };
37463
- var LINREG_CURVE = "Curve";
37464
- var LINREG_SLOPE = "Slope";
37465
- var linearRegression = {
37466
- type: "linear-regression",
37467
- title: "Linear Regression Curve",
37468
- shortTitle: "LinReg",
37469
- // Slope lives in its own pane; Curve force-overlays itself onto the price candles.
37842
+ var leastSquaresMa = {
37843
+ type: "lsma",
37844
+ title: "Least Squares Moving Average",
37845
+ shortTitle: "LSMA",
37846
+ overlay: true,
37847
+ inputs: [
37848
+ sourceInput("Close", "source", "Source", "Price series the regression line is fitted to."),
37849
+ { ...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 },
37850
+ 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."),
37851
+ ...directionInputs()
37852
+ ],
37853
+ compute: (bars, inputs) => {
37854
+ const src = sourceValues(bars, str3(inputs, "source", "Close"));
37855
+ const values = linreg(src, num3(inputs, "length", 25), Math.trunc(num3(inputs, "offset", 0)));
37856
+ return { plots: [slopeColoredPlot("lsma", "LSMA", values, inputs)] };
37857
+ }
37858
+ };
37859
+ var linearRegressionSlope = {
37860
+ type: "linear-regression-slope",
37861
+ title: "Linear Regression Slope",
37862
+ shortTitle: "LRS",
37470
37863
  overlay: false,
37471
37864
  inputs: [
37472
- 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."),
37473
37865
  { ...lengthInput(100, "length", "Length", 5e3, "Number of bars used to fit the linear regression."), min: 2 },
37474
37866
  sourceInput("Close", "source", "Source", "Price series the regression is fitted to."),
37475
- colorInput(BULLISH, "Rising", "bullColor", "Color of the regression curve and slope line while the fit slopes upward."),
37476
- colorInput(BEARISH, "Falling", "bearColor", "Color of the regression curve and slope line while the fit slopes downward."),
37477
- colorInput(NEUTRAL, "Regression Line", "lineColor", "Color of the regression curve and slope line while the fit is flat.")
37867
+ colorInput(BULLISH, "Rising", "bullColor", "Color of the slope line while the fit slopes upward."),
37868
+ colorInput(BEARISH, "Falling", "bearColor", "Color of the slope line while the fit slopes downward."),
37869
+ colorInput(NEUTRAL, "Slope Line", "lineColor", "Color of the slope line while the fit is flat.")
37478
37870
  ],
37479
37871
  compute: (bars, inputs) => {
37480
37872
  const len = num3(inputs, "length", 100);
37481
37873
  const src = sourceValues(bars, str3(inputs, "source", "Close"));
37482
- const curve = linreg(src, len, 0);
37483
- const slope = zip(curve, linreg(src, len, 1), (a, b) => a - b);
37874
+ const slope = zip(linreg(src, len, 0), linreg(src, len, 1), (a, b) => a - b);
37484
37875
  const bull = str3(inputs, "bullColor", BULLISH);
37485
37876
  const bear = str3(inputs, "bearColor", BEARISH);
37486
37877
  const flat = str3(inputs, "lineColor", NEUTRAL);
37487
37878
  const colors = slope.map((s) => Number.isFinite(s) ? s > 0 ? bull : s < 0 ? bear : flat : null);
37488
- if (str3(inputs, "mode", LINREG_CURVE) === LINREG_SLOPE) {
37489
- return {
37490
- plots: [{ key: "slope", title: "Slope", values: slope, color: flat, width: 2, colors }],
37491
- levels: [{ key: "zero", price: 0, color: transp(NEUTRAL, 50), lineStyle: "dashed" }]
37492
- };
37493
- }
37494
- return { plots: [{ key: "curve", title: "Curve", values: curve, color: flat, width: 2, colors, overlay: true }] };
37879
+ return {
37880
+ plots: [{ key: "slope", title: "Slope", values: slope, color: flat, width: 2, colors }],
37881
+ levels: [{ key: "zero", price: 0, color: transp(NEUTRAL, 50), lineStyle: "dashed" }]
37882
+ };
37495
37883
  }
37496
37884
  };
37497
- var averageSpecs = [simpleMa, exponentialMa, movingAverage, smoothedMa, zlema, vidya, maEnvelope, linearRegression];
37885
+ var averageSpecs = [simpleMa, exponentialMa, movingAverage, smoothedMa, zlema, vidya, maEnvelope, leastSquaresMa, linearRegressionSlope];
37498
37886
 
37499
37887
  // src/core/native-indicators/classics/bands.ts
37500
37888
  var JAW_COLOR = "#ab47bc";