@luxalgo/vela 0.7.0 → 0.7.2

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 (45) hide show
  1. package/dist/{DataProvider-Bu9E5Zh5.d.ts → DataProvider-BALR0qPq.d.ts} +1 -1
  2. package/dist/{DataProvider-Ck2qyS_9.d.cts → DataProvider-D5uMiJYz.d.cts} +1 -1
  3. package/dist/{chunk-IFJJPXSV.js → chunk-BFA32GOU.js} +44 -16
  4. package/dist/{chunk-CZRNTHD6.js → chunk-BKHSQ4YM.js} +129 -125
  5. package/dist/{chunk-A4G64KVF.js → chunk-PTNO3YK2.js} +311 -111
  6. package/dist/{chunk-A347YL2P.js → chunk-QBWLINWL.js} +67 -9
  7. package/dist/{chunk-UHXG5J7C.js → chunk-ZADTVRUO.js} +1 -1
  8. package/dist/{contributions-BjQ7hyIx.d.ts → contributions-BbhbZd3c.d.ts} +31 -9
  9. package/dist/{contributions-Cv2Mutvn.d.cts → contributions-CcBFe-91.d.cts} +31 -9
  10. package/dist/index.cjs +391 -157
  11. package/dist/index.d.cts +52 -17
  12. package/dist/index.d.ts +52 -17
  13. package/dist/index.js +4 -4
  14. package/dist/{options-ex2_gtKp.d.ts → options-C7b_Qssu.d.cts} +42 -1
  15. package/dist/{options-ex2_gtKp.d.cts → options-C7b_Qssu.d.ts} +42 -1
  16. package/dist/{plugin-CiyhU7pi.d.ts → plugin-Cnix5ZBh.d.ts} +6 -4
  17. package/dist/{plugin-absNH7Yn.d.cts → plugin-D6MlX4--.d.cts} +6 -4
  18. package/dist/plugin.cjs +39 -15
  19. package/dist/plugin.d.cts +4 -4
  20. package/dist/plugin.d.ts +4 -4
  21. package/dist/plugin.js +2 -2
  22. package/dist/providers/binance.d.cts +2 -2
  23. package/dist/providers/binance.d.ts +2 -2
  24. package/dist/providers/coinbase.d.cts +2 -2
  25. package/dist/providers/coinbase.d.ts +2 -2
  26. package/dist/providers/hyperliquid.d.cts +2 -2
  27. package/dist/providers/hyperliquid.d.ts +2 -2
  28. package/dist/{statusline-model-BXoU4Kua.d.ts → statusline-model-D8NL7o4L.d.ts} +3 -3
  29. package/dist/{statusline-model-cRhPBLPO.d.cts → statusline-model-DUKt7_TQ.d.cts} +3 -3
  30. package/dist/ui.cjs +8 -4
  31. package/dist/ui.d.cts +10 -7
  32. package/dist/ui.d.ts +10 -7
  33. package/dist/ui.js +2 -2
  34. package/dist/vela.global.js +391 -157
  35. package/dist/vela.global.min.js +70 -70
  36. package/dist/widget.cjs +418 -128
  37. package/dist/widget.d.cts +6 -6
  38. package/dist/widget.d.ts +6 -6
  39. package/dist/widget.js +6 -6
  40. package/dist/workspace.cjs +418 -128
  41. package/dist/workspace.d.cts +39 -6
  42. package/dist/workspace.d.ts +39 -6
  43. package/dist/workspace.js +5 -5
  44. package/package.json +1 -1
  45. /package/dist/{chunk-FE6V2YAQ.js → chunk-JU4CWEBF.js} +0 -0
package/dist/index.cjs CHANGED
@@ -75,6 +75,13 @@ function resolveAnimations(animations) {
75
75
  function isLineLikeSeries(spec) {
76
76
  return spec.kind !== "candle" && spec.kind !== "bar" && spec.kind !== "markers";
77
77
  }
78
+ function seriesShownOn(spec, surface) {
79
+ if (spec.display) return spec.display[surface] !== false;
80
+ return spec.visible !== false;
81
+ }
82
+ function seriesInScale(spec) {
83
+ return seriesShownOn(spec, "pane") || seriesShownOn(spec, "priceScale");
84
+ }
78
85
 
79
86
  // src/core/events/EventBus.ts
80
87
  var TypedEventBus = class {
@@ -144,6 +151,9 @@ var IndicatorRegistry = class {
144
151
  };
145
152
 
146
153
  // src/core/native-indicators/NativeIndicator.ts
154
+ function nativeInstanceChannel(type, instanceId) {
155
+ return `${type}#${instanceId}`;
156
+ }
147
157
  var REGISTRY = /* @__PURE__ */ new Map();
148
158
  function registerNativeIndicator(descriptor) {
149
159
  REGISTRY.set(descriptor.type, descriptor);
@@ -219,6 +229,9 @@ var IndicatorHandleImpl = class {
219
229
  setProps(values) {
220
230
  this.controller.applyProps(this.id, values);
221
231
  }
232
+ updateCode(source) {
233
+ this.controller.updateCode(this.id, source);
234
+ }
222
235
  setVisible(visible) {
223
236
  this.controller.setVisible(this.id, visible);
224
237
  }
@@ -241,6 +254,10 @@ var IndicatorHandleImpl = class {
241
254
  setPropsSchema(schema) {
242
255
  this.propsSchema = schema;
243
256
  }
257
+ /** Sync the public `source` once a code update has been prepared and taken over. */
258
+ setSource(source) {
259
+ this.source = source;
260
+ }
244
261
  /** Sync the public `visible` getter when the orchestrator changes visibility (API or legend eye). */
245
262
  setVisibleState(visible) {
246
263
  this.visibleState = visible;
@@ -2347,16 +2364,18 @@ var Pitchfork = class extends SegmentDrawing {
2347
2364
  geometry(proj) {
2348
2365
  const a = this.anchors[0];
2349
2366
  const b = this.anchors[1];
2350
- const c = this.anchors[2];
2351
- if (!a || !b || !c) return null;
2367
+ if (!a || !b) return null;
2352
2368
  const px = (p) => {
2353
2369
  const y = proj.yOf(p.price, this.paneId);
2354
2370
  return y == null ? null : [proj.xOf(p.time), y];
2355
2371
  };
2356
2372
  const P0 = px(a);
2357
2373
  const P1 = px(b);
2374
+ if (!P0 || !P1) return null;
2375
+ const c = this.anchors[2];
2376
+ if (!c) return { segments: [[P0[0], P0[1], P1[0], P1[1]]], fill: null };
2358
2377
  const P2 = px(c);
2359
- if (!P0 || !P1 || !P2) return null;
2378
+ if (!P2) return null;
2360
2379
  const mx = (P1[0] + P2[0]) / 2;
2361
2380
  const my = (P1[1] + P2[1]) / 2;
2362
2381
  const dx = mx - P0[0];
@@ -2403,16 +2422,19 @@ var PitchforkVariant = class extends SegmentDrawing {
2403
2422
  geometry(proj) {
2404
2423
  const a = this.anchors[0];
2405
2424
  const b = this.anchors[1];
2406
- const c = this.anchors[2];
2407
- if (!a || !b || !c) return null;
2425
+ if (!a || !b) return null;
2408
2426
  const px = (p) => {
2409
2427
  const y = proj.yOf(p.price, this.paneId);
2410
2428
  return y == null ? null : [proj.xOf(p.time), y];
2411
2429
  };
2412
- const S2 = px(this.medianStart(a, b, c));
2430
+ const P0 = px(a);
2413
2431
  const P1 = px(b);
2432
+ if (!P0 || !P1) return null;
2433
+ const c = this.anchors[2];
2434
+ if (!c) return { segments: [[P0[0], P0[1], P1[0], P1[1]]], fill: null };
2435
+ const S2 = px(this.medianStart(a, b, c));
2414
2436
  const P2 = px(c);
2415
- if (!S2 || !P1 || !P2) return null;
2437
+ if (!S2 || !P2) return null;
2416
2438
  const mx = (P1[0] + P2[0]) / 2;
2417
2439
  const my = (P1[1] + P2[1]) / 2;
2418
2440
  const dx = mx - S2[0];
@@ -2427,8 +2449,12 @@ var PitchforkVariant = class extends SegmentDrawing {
2427
2449
  // upper tine
2428
2450
  extendRay(P2[0], P2[1], P2[0] + dx, P2[1] + dy, "right", w, h),
2429
2451
  // lower tine
2430
- [P1[0], P1[1], P2[0], P2[1]]
2452
+ [P1[0], P1[1], P2[0], P2[1]],
2431
2453
  // base line
2454
+ // The median leaves from the shifted origin, not the pivot — without this construction
2455
+ // line the pivot handle would float unattached to the shape it defines.
2456
+ [P0[0], P0[1], P1[0], P1[1]]
2457
+ // pivot → first tine anchor
2432
2458
  ],
2433
2459
  fill: null
2434
2460
  };
@@ -2485,17 +2511,19 @@ var InsidePitchfork = class extends SegmentDrawing {
2485
2511
  geometry(proj) {
2486
2512
  const a = this.anchors[0];
2487
2513
  const b = this.anchors[1];
2488
- const c = this.anchors[2];
2489
- if (!a || !b || !c) return null;
2514
+ if (!a || !b) return null;
2490
2515
  const px = (p) => {
2491
2516
  const y = proj.yOf(p.price, this.paneId);
2492
2517
  return y == null ? null : [proj.xOf(p.time), y];
2493
2518
  };
2494
- const B = px({ time: (a.time + b.time) / 2, price: (a.price + b.price) / 2 });
2495
2519
  const P0 = px(a);
2496
2520
  const P1 = px(b);
2521
+ if (!P0 || !P1) return null;
2522
+ const c = this.anchors[2];
2523
+ if (!c) return { segments: [[P0[0], P0[1], P1[0], P1[1]]], fill: null };
2524
+ const B = px({ time: (a.time + b.time) / 2, price: (a.price + b.price) / 2 });
2497
2525
  const P2 = px(c);
2498
- if (!B || !P0 || !P1 || !P2) return null;
2526
+ if (!B || !P2) return null;
2499
2527
  const mx = (P1[0] + P2[0]) / 2;
2500
2528
  const my = (P1[1] + P2[1]) / 2;
2501
2529
  const dx = P2[0] - B[0];
@@ -2666,16 +2694,18 @@ var Triangle = class extends SegmentDrawing {
2666
2694
  geometry(proj) {
2667
2695
  const a = this.anchors[0];
2668
2696
  const b = this.anchors[1];
2669
- const c = this.anchors[2];
2670
- if (!a || !b || !c) return null;
2697
+ if (!a || !b) return null;
2671
2698
  const px = (p) => {
2672
2699
  const y = proj.yOf(p.price, this.paneId);
2673
2700
  return y == null ? null : [proj.xOf(p.time), y];
2674
2701
  };
2675
2702
  const A = px(a);
2676
2703
  const B = px(b);
2704
+ if (!A || !B) return null;
2705
+ const c = this.anchors[2];
2706
+ if (!c) return { segments: [[A[0], A[1], B[0], B[1]]], fill: null };
2677
2707
  const C = px(c);
2678
- if (!A || !B || !C) return null;
2708
+ if (!C) return null;
2679
2709
  return {
2680
2710
  segments: [
2681
2711
  [A[0], A[1], B[0], B[1]],
@@ -8204,6 +8234,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
8204
8234
  if (identityChanged) {
8205
8235
  this.beginLoad(false);
8206
8236
  this.setBarSeries([], { clearing: true });
8237
+ this.blankIndicatorVisuals();
8207
8238
  if (this.activeEngineStyle) {
8208
8239
  this.typeEngines.get(this.activeEngineStyle)?.suspend();
8209
8240
  this.renderer.setNativeData?.(this.activeEngineStyle, void 0);
@@ -8246,7 +8277,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
8246
8277
  restartNativeIndicators() {
8247
8278
  for (const record of this.registry.all()) {
8248
8279
  if (!record.native) continue;
8249
- record.native.instance.stop();
8280
+ if (record.native.started) record.native.instance.stop();
8250
8281
  record.native.instance = record.native.descriptor.create();
8251
8282
  record.native.started = false;
8252
8283
  record.pendingStructural = true;
@@ -8454,6 +8485,23 @@ var EngineOrchestrator = class _EngineOrchestrator {
8454
8485
  this.renderer.setBars(this.bars, { preserveView: true });
8455
8486
  this.reexecuteIndicators();
8456
8487
  }
8488
+ /**
8489
+ * Blank every mounted indicator's painted output — series data, fills, backgrounds,
8490
+ * price lines, drawings, bar colors, trades — while keeping the mount (legend row,
8491
+ * pane, settings) intact, via the same idempotent-by-id remount an input edit uses.
8492
+ * Called on a market identity switch: the fresh models arrive only after the new
8493
+ * bars load, so nothing blanked here is ever restored — each consumer's re-run
8494
+ * remounts a full model over it.
8495
+ */
8496
+ blankIndicatorVisuals() {
8497
+ for (const record of this.registry.all()) {
8498
+ if (record.hidden || !record.renderHandle || !record.model) continue;
8499
+ record.model = blankedModel(record.model);
8500
+ record.renderHandle = this.renderer.mountIndicator(record.model);
8501
+ record.pendingStructural = true;
8502
+ this.setLoading(record, true);
8503
+ }
8504
+ }
8457
8505
  /** Stop + re-run every visible Pine indicator (its input bars changed wholesale). */
8458
8506
  reexecuteIndicators() {
8459
8507
  for (const record of this.registry.all()) {
@@ -8698,6 +8746,61 @@ var EngineOrchestrator = class _EngineOrchestrator {
8698
8746
  record.session.update(record.inputValues, record.propValues);
8699
8747
  this.events.emit("indicator:inputs", { id });
8700
8748
  }
8749
+ /**
8750
+ * IndicatorController: replace a SCRIPT indicator's source in place (see
8751
+ * {@link IndicatorHandle.updateCode}). Prepare-first: the running session keeps
8752
+ * computing until the new source has compiled, so a broken edit costs the user
8753
+ * nothing but an `error` event. Natives have no script — warn and leave them alone.
8754
+ */
8755
+ updateCode(id, source) {
8756
+ const record = this.registry.get(id);
8757
+ const handle = this.handles.get(id);
8758
+ if (!record || !handle) return;
8759
+ if (record.native) {
8760
+ console.warn(`[vela] updateCode("${id}") \u2014 a native indicator has no script to update.`);
8761
+ return;
8762
+ }
8763
+ if (source === record.source && record.pendingSource === void 0) return;
8764
+ record.pendingSource = source;
8765
+ void this.swapSource(id, source, handle);
8766
+ }
8767
+ async swapSource(id, source, handle) {
8768
+ const engine = this.registry.get(id)?.engine ?? this.engineFor(this.registry.get(id)?.options?.language);
8769
+ let prepared;
8770
+ try {
8771
+ prepared = await engine.prepare(source, id);
8772
+ } catch (err) {
8773
+ const record2 = this.registry.get(id);
8774
+ if (record2?.pendingSource === source) {
8775
+ record2.pendingSource = void 0;
8776
+ this.fail(id, handle, err);
8777
+ }
8778
+ return;
8779
+ }
8780
+ const record = this.registry.get(id);
8781
+ if (!record || record.pendingSource !== source) return;
8782
+ record.pendingSource = void 0;
8783
+ record.source = source;
8784
+ record.engine = engine;
8785
+ record.prepared = prepared;
8786
+ handle.setSource(source);
8787
+ record.inputValues = valuesOnSchema(prepared.inputs, record.inputValues);
8788
+ record.propValues = valuesOnSchema(prepared.props ?? [], record.propValues);
8789
+ handle.setSchema(prepared.inputs);
8790
+ handle.setPropsSchema(prepared.props ?? []);
8791
+ record.session?.stop();
8792
+ record.session = void 0;
8793
+ record.pendingStructural = true;
8794
+ record.pendingCause = "code";
8795
+ if (record.hidden) return;
8796
+ if (record.renderHandle) {
8797
+ this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
8798
+ this.setLoading(record, true);
8799
+ } else {
8800
+ this.mountLoadingPlaceholder(id, record);
8801
+ }
8802
+ this.executeIndicator(id, handle);
8803
+ }
8701
8804
  /** IndicatorController: tear down an indicator and (if now empty) its pane. */
8702
8805
  /** Live handles of every indicator on the chart (script + native), insertion order. */
8703
8806
  listIndicators() {
@@ -8712,7 +8815,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
8712
8815
  const record = this.registry.remove(id);
8713
8816
  if (record?.native?.type === "volume") this.volumeOptedOut = true;
8714
8817
  record?.session?.stop();
8715
- record?.native?.instance.stop();
8818
+ if (record?.native?.started) record.native.instance.stop();
8716
8819
  this.handles.delete(id);
8717
8820
  if (record?.renderHandle) this.renderer.removeIndicator(record.renderHandle);
8718
8821
  const paneId = record?.model?.paneId;
@@ -8738,7 +8841,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
8738
8841
  if (!visible) {
8739
8842
  record.session?.stop();
8740
8843
  record.session = void 0;
8741
- record.native?.instance.suspend();
8844
+ if (record.native?.started) record.native.instance.suspend();
8742
8845
  if (record.renderHandle) this.renderer.setIndicatorVisible?.(record.renderHandle, false);
8743
8846
  else if (record.native) this.mountHiddenNativeRow(id, record);
8744
8847
  } else {
@@ -8819,7 +8922,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
8819
8922
  this.paneActionUnsub?.();
8820
8923
  for (const record of this.registry.all()) {
8821
8924
  record.session?.stop();
8822
- record.native?.instance.stop();
8925
+ if (record.native?.started) record.native.instance.stop();
8823
8926
  }
8824
8927
  for (const engine of this.typeEngines.values()) engine.stop();
8825
8928
  this.typeEngines.clear();
@@ -8843,6 +8946,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
8843
8946
  const engine = this.engineFor(options.language);
8844
8947
  record.engine = engine;
8845
8948
  const prepared = await engine.prepare(source, id);
8949
+ if (record.source !== source) return;
8846
8950
  record.prepared = prepared;
8847
8951
  const defaults2 = {};
8848
8952
  for (const input of prepared.inputs) defaults2[input.key] = input.defval;
@@ -8913,7 +9017,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
8913
9017
  await this.readyPromise;
8914
9018
  const record = this.registry.get(id);
8915
9019
  if (!record?.native || record.hidden) return;
9020
+ const channel = this.nativeChannel(record);
8916
9021
  const ctx = {
9022
+ id,
8917
9023
  symbol: this.config.market.symbol ?? "TEST",
8918
9024
  timeframe: this.config.market.timeframe ?? "60",
8919
9025
  live: this.config.live,
@@ -8921,7 +9027,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
8921
9027
  bars: () => this.bars,
8922
9028
  data: this.dataControl,
8923
9029
  emit: (out) => this.applyModel(id, this.buildNativeModel(record, out)),
8924
- pushData: (data) => this.renderer.setNativeData?.(record.native.type, data),
9030
+ pushData: (data) => this.renderer.setNativeData?.(channel, data),
8925
9031
  setStatus: (status) => {
8926
9032
  if (record.renderHandle && !record.hidden) this.renderer.setIndicatorStatus?.(record.renderHandle, status);
8927
9033
  }
@@ -8932,16 +9038,26 @@ var EngineOrchestrator = class _EngineOrchestrator {
8932
9038
  this.fail(id, handle, err);
8933
9039
  }
8934
9040
  }
9041
+ /** The renderer-layer channel a native instance's `pushData` lands on: the type itself
9042
+ * for a single-instance type (the layer id doubles as the channel), a per-instance
9043
+ * channel for a `multiInstance` type — otherwise every instance would overwrite the
9044
+ * one layer. Stamped on the model so the renderer mounts a dedicated layer for it. */
9045
+ nativeChannel(record) {
9046
+ const { type, descriptor } = record.native;
9047
+ return descriptor.multiInstance ? nativeInstanceChannel(type, record.id) : type;
9048
+ }
8935
9049
  /** Wrap a native indicator's emitted visuals into a full IndicatorModel, tagged `native`. */
8936
9050
  buildNativeModel(record, out) {
8937
9051
  const d = record.native.descriptor;
9052
+ const type = record.native.type;
9053
+ const channel = this.nativeChannel(record);
8938
9054
  return {
8939
9055
  id: record.id,
8940
9056
  title: record.title,
8941
9057
  ...d.shortTitle ? { shorttitle: d.shortTitle } : {},
8942
9058
  overlay: d.overlay,
8943
9059
  paneHint: d.paneHint,
8944
- native: { type: record.native.type },
9060
+ native: { type, ...channel !== type ? { channel } : {} },
8945
9061
  ...d.legend === false ? { legend: false } : {},
8946
9062
  ...out.paneAxis != null ? { paneAxis: out.paneAxis } : {},
8947
9063
  series: out.series ?? [],
@@ -9231,6 +9347,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
9231
9347
  applyModel(id, model) {
9232
9348
  const record = this.registry.get(id);
9233
9349
  if (!record || record.hidden) return true;
9350
+ if (this.switchingMarket) return false;
9234
9351
  if (record.loading && this.bars.length === 0 && !_EngineOrchestrator.modelHasOutput(model)) return false;
9235
9352
  const handle = this.handles.get(id);
9236
9353
  if (!record.renderHandle) {
@@ -9248,7 +9365,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
9248
9365
  if (record.loading && !record.paneLocked) {
9249
9366
  const routed = this.routePane(id, model, record.options ?? {});
9250
9367
  if (routed !== paneId) {
9251
- if (paneId !== "price") {
9368
+ if (paneId !== "price" && !this.registry.all().some((r) => r.id !== id && r.model?.paneId === paneId)) {
9252
9369
  this.renderer.removePane(paneId);
9253
9370
  this.forgetPane(paneId);
9254
9371
  }
@@ -9377,6 +9494,34 @@ function yieldToPaint() {
9377
9494
  }
9378
9495
  return new Promise((resolve) => setTimeout(resolve, 0));
9379
9496
  }
9497
+ function valuesOnSchema(schema, previous) {
9498
+ const out = {};
9499
+ const declared = /* @__PURE__ */ new Set();
9500
+ for (const s of schema) {
9501
+ out[s.key] = s.defval;
9502
+ declared.add(s.key);
9503
+ declared.add(s.title);
9504
+ }
9505
+ for (const [k, v] of Object.entries(previous)) if (declared.has(k)) out[k] = v;
9506
+ return out;
9507
+ }
9508
+ function blankedModel(model) {
9509
+ return {
9510
+ ...model,
9511
+ series: model.series.map((s) => s.kind === "candle" || s.kind === "bar" ? { ...s, bars: [] } : s.kind === "markers" ? { ...s, markers: [] } : { ...s, points: [] }),
9512
+ fills: [],
9513
+ backgrounds: [],
9514
+ priceLines: [],
9515
+ lines: [],
9516
+ boxes: [],
9517
+ labels: [],
9518
+ polylines: [],
9519
+ linefills: [],
9520
+ tables: [],
9521
+ barColors: [],
9522
+ trades: []
9523
+ };
9524
+ }
9380
9525
  function modelToValuePatch(model) {
9381
9526
  const series = [];
9382
9527
  let from = Number.POSITIVE_INFINITY;
@@ -10862,6 +11007,43 @@ function injectStyles(id, css, root = document) {
10862
11007
  host.appendChild(s);
10863
11008
  }
10864
11009
 
11010
+ // src/ui/tokens.ts
11011
+ var STATIC_ID = "vela-ui-tokens";
11012
+ var STATIC_DECLS = Object.entries(STATIC_TOKENS).map(([k, v]) => ` ${k}: ${v};`).join("\n");
11013
+ var STATIC_CSS = `
11014
+ .vela-ui, .vela-ui-layer {
11015
+ ${STATIC_DECLS}
11016
+ font-family: var(--vela-font, -apple-system, system-ui, sans-serif);
11017
+ box-sizing: border-box;
11018
+ /* Chrome text (titles, buttons, menus, readouts) is UI, not copy \u2014 never selectable. */
11019
+ user-select: none;
11020
+ -webkit-user-select: none;
11021
+ }
11022
+ .vela-ui *, .vela-ui-layer * { box-sizing: border-box; }
11023
+ /* Text ENTRY is the one exception: selection is part of editing. */
11024
+ .vela-ui :is(input, textarea), .vela-ui-layer :is(input, textarea) { user-select: text; -webkit-user-select: text; }
11025
+ /* iOS Safari zooms the page when a focused field is under 16px, and often
11026
+ keeps that zoom after the field blurs or a dialog closes. The shell's
11027
+ mobile size class is the honest gate \u2014 not a media query \u2014 so an embedded
11028
+ chart on a wide desktop still gets the rule when it is in the phone layout.
11029
+ Scoped to the kit root so a host page's own [data-layout] is never touched. */
11030
+ .vela-ui[data-layout='mobile'] :is(input, textarea, select) { font-size: 16px; }
11031
+ .vela-icon { display: inline-flex; align-items: center; flex: none; }
11032
+ .vela-icon svg { display: block; }
11033
+ `;
11034
+ function applyThemeTokens(el, t) {
11035
+ const tokens = themeTokens(t);
11036
+ for (const key in tokens) el.style.setProperty(key, tokens[key]);
11037
+ }
11038
+ function floatingLayerHost(from, fallback) {
11039
+ return from.closest(".vela-ui") ?? fallback ?? from;
11040
+ }
11041
+ function ensureUIHost(el, theme) {
11042
+ injectStyles(STATIC_ID, STATIC_CSS, el.getRootNode());
11043
+ el.classList.add("vela-ui");
11044
+ if (theme) applyThemeTokens(el, theme);
11045
+ }
11046
+
10865
11047
  // src/ui/icons.ts
10866
11048
  function iconEl(id, doc = document) {
10867
11049
  const span = doc.createElement("span");
@@ -10895,7 +11077,7 @@ var MENU_CSS = `
10895
11077
  /* The list is a <ul>: without this, block rows (the separator) paint a ::marker dot. */
10896
11078
  list-style: none;
10897
11079
  margin: 0;
10898
- background: var(--vela-surface-elev);
11080
+ background: var(--vela-surface);
10899
11081
  color: var(--vela-fg);
10900
11082
  border: 1px solid var(--vela-border-strong);
10901
11083
  border-radius: 6px;
@@ -11186,7 +11368,7 @@ var Menu = class {
11186
11368
  const anchor = opts.trigger ?? opts.host ?? document.body;
11187
11369
  const doc = anchor.ownerDocument;
11188
11370
  injectStyles(MENU_STYLE_ID, MENU_CSS, doc);
11189
- const host = opts.host ?? anchor.closest?.(".vela-ui") ?? doc.body;
11371
+ const host = floatingLayerHost(opts.host ?? anchor, doc.body);
11190
11372
  this.root = new Surface(doc, {
11191
11373
  host,
11192
11374
  placement: opts.placement,
@@ -11225,40 +11407,6 @@ var Menu = class {
11225
11407
  }
11226
11408
  };
11227
11409
 
11228
- // src/ui/tokens.ts
11229
- var STATIC_ID = "vela-ui-tokens";
11230
- var STATIC_DECLS = Object.entries(STATIC_TOKENS).map(([k, v]) => ` ${k}: ${v};`).join("\n");
11231
- var STATIC_CSS = `
11232
- .vela-ui, .vela-ui-layer {
11233
- ${STATIC_DECLS}
11234
- font-family: var(--vela-font, -apple-system, system-ui, sans-serif);
11235
- box-sizing: border-box;
11236
- /* Chrome text (titles, buttons, menus, readouts) is UI, not copy \u2014 never selectable. */
11237
- user-select: none;
11238
- -webkit-user-select: none;
11239
- }
11240
- .vela-ui *, .vela-ui-layer * { box-sizing: border-box; }
11241
- /* Text ENTRY is the one exception: selection is part of editing. */
11242
- .vela-ui :is(input, textarea), .vela-ui-layer :is(input, textarea) { user-select: text; -webkit-user-select: text; }
11243
- /* iOS Safari zooms the page when a focused field is under 16px, and often
11244
- keeps that zoom after the field blurs or a dialog closes. The shell's
11245
- mobile size class is the honest gate \u2014 not a media query \u2014 so an embedded
11246
- chart on a wide desktop still gets the rule when it is in the phone layout.
11247
- Scoped to the kit root so a host page's own [data-layout] is never touched. */
11248
- .vela-ui[data-layout='mobile'] :is(input, textarea, select) { font-size: 16px; }
11249
- .vela-icon { display: inline-flex; align-items: center; flex: none; }
11250
- .vela-icon svg { display: block; }
11251
- `;
11252
- function applyThemeTokens(el, t) {
11253
- const tokens = themeTokens(t);
11254
- for (const key in tokens) el.style.setProperty(key, tokens[key]);
11255
- }
11256
- function ensureUIHost(el, theme) {
11257
- injectStyles(STATIC_ID, STATIC_CSS, el.getRootNode());
11258
- el.classList.add("vela-ui");
11259
- if (theme) applyThemeTokens(el, theme);
11260
- }
11261
-
11262
11410
  // src/ui/components/popover/controller.ts
11263
11411
  function insetRect(r, inset) {
11264
11412
  return {
@@ -11365,7 +11513,8 @@ var Popover = class {
11365
11513
  const doc = opts.trigger.ownerDocument;
11366
11514
  injectStyles(POPOVER_STYLE_ID, POPOVER_CSS, doc);
11367
11515
  this.trigger = opts.trigger;
11368
- this.host = opts.host ?? doc.body;
11516
+ this.host = opts.host ?? floatingLayerHost(opts.trigger, doc.body);
11517
+ this.inheritsTokens = !opts.host && this.host !== doc.body;
11369
11518
  this.ctrl = popoverController(opts);
11370
11519
  this.boundary = opts.boundary ?? "viewport";
11371
11520
  this.theme = opts.theme;
@@ -11397,7 +11546,7 @@ var Popover = class {
11397
11546
  clearTimeout(this.leaveTimer);
11398
11547
  this.leaveTimer = null;
11399
11548
  }
11400
- ensureUIHost(this.el, this.theme);
11549
+ ensureUIHost(this.el, this.inheritsTokens ? void 0 : this.theme);
11401
11550
  if (this.fadeMs > 0) {
11402
11551
  this.el.style.transition = `opacity ${this.fadeMs}ms ease`;
11403
11552
  this.el.style.opacity = "0";
@@ -15703,33 +15852,32 @@ function candleGeometry(xCss, spacing, dpr, bodyScale = 1) {
15703
15852
  center: (wickLeftDev + wickDev / 2) / dpr
15704
15853
  };
15705
15854
  }
15706
- function aggregateCandleColumns(bars, i0, i1, xOf, yOf) {
15855
+ function aggregateCandleColumns(bars, i0, i1, xOf, yOf, colorOf2) {
15707
15856
  const out = [];
15708
15857
  let col = NaN;
15709
- let intervals = [];
15858
+ let runs = [];
15710
15859
  const flush = () => {
15711
- if (intervals.length === 0) return;
15712
- const merged = [intervals[0]];
15713
- for (let k = 1; k < intervals.length; k += 1) {
15714
- const prev2 = merged[merged.length - 1];
15715
- const next = intervals[k];
15716
- if (Math.abs(yOf(prev2.hi) - yOf(next.lo)) < 1) {
15717
- prev2.hi = next.hi;
15718
- if (next.headIdx < prev2.headIdx) {
15719
- prev2.headIdx = next.headIdx;
15720
- prev2.headTime = next.headTime;
15721
- prev2.open = next.open;
15722
- }
15723
- if (next.lastIdx > prev2.lastIdx) {
15724
- prev2.lastIdx = next.lastIdx;
15725
- prev2.close = next.close;
15860
+ if (runs.length === 0) return;
15861
+ runs.sort((a, b) => a.lo - b.lo);
15862
+ const merged = [];
15863
+ for (const next of runs) {
15864
+ let prev2;
15865
+ for (let k = merged.length - 1; k >= 0; k -= 1) {
15866
+ if (merged[k].color === next.color) {
15867
+ prev2 = merged[k];
15868
+ break;
15726
15869
  }
15870
+ }
15871
+ if (prev2 && Math.abs(yOf(prev2.hi) - yOf(next.lo)) < 1) {
15872
+ prev2.hi = next.hi;
15873
+ if (next.lastIdx > prev2.lastIdx) prev2.lastIdx = next.lastIdx;
15727
15874
  } else {
15728
15875
  merged.push(next);
15729
15876
  }
15730
15877
  }
15731
- for (const iv of merged) out.push({ x: col, hi: iv.hi, lo: iv.lo, headTime: iv.headTime, open: iv.open, close: iv.close });
15732
- intervals = [];
15878
+ merged.sort((a, b) => a.lastIdx - b.lastIdx);
15879
+ for (const r of merged) out.push({ x: col, hi: r.hi, lo: r.lo, color: r.color });
15880
+ runs = [];
15733
15881
  };
15734
15882
  for (let i = i0; i <= i1; i += 1) {
15735
15883
  const b = bars[i];
@@ -15739,26 +15887,17 @@ function aggregateCandleColumns(bars, i0, i1, xOf, yOf) {
15739
15887
  flush();
15740
15888
  col = x;
15741
15889
  }
15890
+ const color = colorOf2(b);
15742
15891
  let lo = b.low;
15743
15892
  let hi = b.high;
15744
- let headIdx = i;
15745
- let headTime = b.time;
15746
- let open2 = b.open;
15747
- for (let k = intervals.length - 1; k >= 0; k -= 1) {
15748
- const iv = intervals[k];
15749
- if (iv.lo > hi || iv.hi < lo) continue;
15750
- if (iv.lo < lo) lo = iv.lo;
15751
- if (iv.hi > hi) hi = iv.hi;
15752
- if (iv.headIdx < headIdx) {
15753
- headIdx = iv.headIdx;
15754
- headTime = iv.headTime;
15755
- open2 = iv.open;
15756
- }
15757
- intervals.splice(k, 1);
15758
- }
15759
- let insertAt = 0;
15760
- while (insertAt < intervals.length && intervals[insertAt].lo < lo) insertAt += 1;
15761
- intervals.splice(insertAt, 0, { lo, hi, headIdx, headTime, open: open2, lastIdx: i, close: b.close });
15893
+ for (let k = runs.length - 1; k >= 0; k -= 1) {
15894
+ const r = runs[k];
15895
+ if (r.color !== color || r.lo > hi || r.hi < lo) continue;
15896
+ if (r.lo < lo) lo = r.lo;
15897
+ if (r.hi > hi) hi = r.hi;
15898
+ runs.splice(k, 1);
15899
+ }
15900
+ runs.push({ lo, hi, color, lastIdx: i });
15762
15901
  }
15763
15902
  flush();
15764
15903
  return out;
@@ -16733,7 +16872,7 @@ var WebGL2Backend = class {
16733
16872
  * Routing mirrors the main pass: own series per pane, force_overlay series on the price pane. */
16734
16873
  emitGlowSources(b, scene, pane, coords, i0, i1) {
16735
16874
  const emitOne = (s, off) => {
16736
- if (!isLineLikeSeries(s) || s.visible === false) return;
16875
+ if (!isLineLikeSeries(s) || !seriesShownOn(s, "pane")) return;
16737
16876
  if (s.kind === "histogram" || s.kind === "columns") return;
16738
16877
  if (s.kind === "circles" || s.kind === "cross") this.emitPointMarkers(b, s, pane, coords, i0, i1, off);
16739
16878
  else this.emitPolyline(b, s, pane, coords, i0, i1, s.kind === "step", off);
@@ -16844,7 +16983,7 @@ var WebGL2Backend = class {
16844
16983
  const spacing = coords.bodySpacing();
16845
16984
  const tier = candleTier(spacing);
16846
16985
  if (tier === "aggregate") {
16847
- this.emitCandlesAggregated(b, bars, i0, i1, coords, pane, up, down, barColors);
16986
+ this.emitCandlesAggregated(b, bars, i0, i1, coords, pane, (bar) => barColors.get(bar.time) ?? (bar.close >= bar.open ? up : down));
16848
16987
  return;
16849
16988
  }
16850
16989
  const tickW = Math.max(1, Math.round(spacing * 0.35));
@@ -16985,14 +17124,17 @@ var WebGL2Backend = class {
16985
17124
  const spacing = coords.bodySpacing();
16986
17125
  const tier = candleTier(spacing);
16987
17126
  const paint = effectiveCandlePaint(scene.style.candle, scene.candleOverride, theme.upColor, theme.downColor);
17127
+ const up = paint.up;
17128
+ const down = paint.down;
17129
+ const cs = paint.candle;
16988
17130
  if (tier === "aggregate") {
16989
- this.emitCandlesAggregated(b, bars, i0, i1, coords, pane, paint.up, paint.down, barColors);
17131
+ this.emitCandlesAggregated(b, bars, i0, i1, coords, pane, (bar) => {
17132
+ const isUp = bar.close >= bar.open;
17133
+ return (isUp ? cs.wickUpColor : cs.wickDownColor) ?? barColors.get(bar.time) ?? (isUp ? up : down);
17134
+ });
16990
17135
  return;
16991
17136
  }
16992
17137
  const drawBody = tier === "full";
16993
- const up = paint.up;
16994
- const down = paint.down;
16995
- const cs = paint.candle;
16996
17138
  const fading = this.candleStructureAlpha > this.candleBodyAlpha + 1e-3;
16997
17139
  for (let i = i0; i <= i1; i += 1) {
16998
17140
  const bar = bars[i];
@@ -17041,24 +17183,26 @@ var WebGL2Backend = class {
17041
17183
  /**
17042
17184
  * Sub-pixel LOD (mirrors Canvas2dBackend.drawCandlesAggregated): bars sharing a
17043
17185
  * rounded pixel column collapse into high-low sticks — one per contiguous
17044
- * coverage run (see {@link aggregateCandleColumns}), so a price gap inside the
17045
- * column stays a void. Draw cost stays bounded by screen width, not bar count;
17046
- * stick color follows its own first-open→last-close.
17186
+ * SAME-COLOR coverage run (see {@link aggregateCandleColumns}), so a price gap
17187
+ * inside the column stays a void and every bar's extent keeps its own color. Draw
17188
+ * cost stays bounded by screen width, not bar count. `colorOf` is the calling
17189
+ * style's per-bar paint rule (candles and OHLC bars resolve theirs differently).
17047
17190
  */
17048
- emitCandlesAggregated(b, bars, i0, i1, coords, pane, up, down, barColors) {
17191
+ emitCandlesAggregated(b, bars, i0, i1, coords, pane, colorOf2) {
17049
17192
  const yOf = (price) => coords.priceToY(price, pane.scale, pane.bounds);
17050
- for (const s of aggregateCandleColumns(bars, i0, i1, (i) => coords.logicalToX(i), yOf)) {
17051
- const c = parseColor(barColors.get(s.headTime) ?? (s.close >= s.open ? up : down));
17193
+ for (const s of aggregateCandleColumns(bars, i0, i1, (i) => coords.logicalToX(i), yOf, colorOf2)) {
17194
+ const c = parseColor(s.color);
17052
17195
  const hY = yOf(s.hi);
17053
17196
  b.rect(s.x - 0.5, hY, 1, yOf(s.lo) - hY, c);
17054
17197
  }
17055
17198
  }
17056
17199
  emitSeries(b, spec, pane, coords, i0, i1, theme, off = 0) {
17200
+ if (!seriesShownOn(spec, "pane")) return;
17057
17201
  if (spec.kind === "candle" || spec.kind === "bar") {
17058
17202
  this.emitPlotCandles(b, spec, pane, coords, i0, i1, theme, off);
17059
17203
  return;
17060
17204
  }
17061
- if (!isLineLikeSeries(spec) || spec.visible === false) return;
17205
+ if (!isLineLikeSeries(spec)) return;
17062
17206
  switch (spec.kind) {
17063
17207
  case "histogram":
17064
17208
  case "columns":
@@ -18722,7 +18866,7 @@ var Canvas2dBackend = class {
18722
18866
  const spacing = coords.bodySpacing();
18723
18867
  const tier = candleTier(spacing);
18724
18868
  if (tier === "aggregate") {
18725
- this.drawCandlesAggregated(ctx, bars, i0, i1, coords, pane, up, down, barColors);
18869
+ this.drawCandlesAggregated(ctx, bars, i0, i1, coords, pane, (b) => barColors.get(b.time) ?? (b.close >= b.open ? up : down));
18726
18870
  return;
18727
18871
  }
18728
18872
  const tickW = Math.max(1, Math.round(spacing * 0.35));
@@ -18898,12 +19042,15 @@ var Canvas2dBackend = class {
18898
19042
  const spacing = coords.bodySpacing();
18899
19043
  const tier = candleTier(spacing);
18900
19044
  const paint = effectiveCandlePaint(scene.style.candle, scene.candleOverride, theme.upColor, theme.downColor);
19045
+ const cs = paint.candle;
18901
19046
  if (tier === "aggregate") {
18902
- this.drawCandlesAggregated(ctx, bars, i0, i1, coords, pane, paint.up, paint.down, barColors);
19047
+ this.drawCandlesAggregated(ctx, bars, i0, i1, coords, pane, (b) => {
19048
+ const up = b.close >= b.open;
19049
+ return (up ? cs.wickUpColor : cs.wickDownColor) ?? barColors.get(b.time) ?? (up ? paint.up : paint.down);
19050
+ });
18903
19051
  return;
18904
19052
  }
18905
19053
  const drawBody = tier === "full";
18906
- const cs = paint.candle;
18907
19054
  const fading = this.candleStructureAlpha > this.candleBodyAlpha + 1e-3;
18908
19055
  for (let i = i0; i <= i1; i += 1) {
18909
19056
  const b = bars[i];
@@ -18960,17 +19107,18 @@ var Canvas2dBackend = class {
18960
19107
  }
18961
19108
  /**
18962
19109
  * Sub-pixel LOD: bars sharing a rounded pixel column collapse into high-low
18963
- * sticks (one per contiguous coverage run — see {@link aggregateCandleColumns}),
18964
- * so draw cost is bounded by screen width (not bar count) when zoomed far out
18965
- * and a price gap inside the column stays a void. Each stick's color follows
18966
- * its first-open→last-close direction (a barcolor() on the head bar still wins).
19110
+ * sticks (one per contiguous SAME-COLOR coverage run — see
19111
+ * {@link aggregateCandleColumns}), so draw cost is bounded by screen width (not
19112
+ * bar count) when zoomed far out, a price gap inside the column stays a void, and
19113
+ * every bar's extent keeps its own color. `colorOf` is the calling style's per-bar
19114
+ * paint rule (candles and OHLC bars resolve theirs differently).
18967
19115
  */
18968
- drawCandlesAggregated(ctx, bars, i0, i1, coords, pane, up, down, barColors) {
19116
+ drawCandlesAggregated(ctx, bars, i0, i1, coords, pane, colorOf2) {
18969
19117
  const yOf = (price) => coords.priceToY(price, pane.scale, pane.bounds);
18970
19118
  ctx.lineWidth = 1;
18971
- for (const s of aggregateCandleColumns(bars, i0, i1, (i) => coords.logicalToX(i), yOf)) {
19119
+ for (const s of aggregateCandleColumns(bars, i0, i1, (i) => coords.logicalToX(i), yOf, colorOf2)) {
18972
19120
  const x = s.x + 0.5;
18973
- ctx.strokeStyle = barColors.get(s.headTime) ?? (s.close >= s.open ? up : down);
19121
+ ctx.strokeStyle = s.color;
18974
19122
  ctx.beginPath();
18975
19123
  ctx.moveTo(x, yOf(s.hi));
18976
19124
  ctx.lineTo(x, yOf(s.lo));
@@ -18978,11 +19126,12 @@ var Canvas2dBackend = class {
18978
19126
  }
18979
19127
  }
18980
19128
  drawSeries(ctx, spec, pane, coords, i0, i1, theme, off = 0) {
19129
+ if (!seriesShownOn(spec, "pane")) return;
18981
19130
  if (spec.kind === "candle" || spec.kind === "bar") {
18982
19131
  this.drawPlotCandles(ctx, spec, pane, coords, i0, i1, theme, off);
18983
19132
  return;
18984
19133
  }
18985
- if (!isLineLikeSeries(spec) || spec.visible === false) return;
19134
+ if (!isLineLikeSeries(spec)) return;
18986
19135
  switch (spec.kind) {
18987
19136
  case "histogram":
18988
19137
  case "columns":
@@ -27930,6 +28079,7 @@ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log =
27930
28079
  return { min: min - span * MARGIN_BOTTOM, max: max + span * MARGIN_TOP };
27931
28080
  }
27932
28081
  function considerSeries(s, i0, i1, off, consider) {
28082
+ if (!seriesInScale(s)) return;
27933
28083
  if (s.kind === "candle" || s.kind === "bar") {
27934
28084
  for (let i = i0; i <= i1; i += 1) {
27935
28085
  const b = s.bars[i - off];
@@ -28149,6 +28299,22 @@ var MarkPopover = class {
28149
28299
  this.openKey = null;
28150
28300
  /** Bumped per open/close — a lazy content resolving after its popup went away is dropped. */
28151
28301
  this.generation = 0;
28302
+ /**
28303
+ * The cluster whose popup was open when the current pointer press began. The anchor is
28304
+ * hit-transparent (the glyph is canvas pixels), so the shell's outside-dismiss closes the
28305
+ * popup on pointerdown; the click the renderer derives from the same pointerup must then
28306
+ * read as "close", not reopen that cluster. Captured on the document ahead of the shell's
28307
+ * (deferred) listener, cleared once the release has dispatched.
28308
+ */
28309
+ this.pressKey = null;
28310
+ this.onPress = () => {
28311
+ if (this.openKey === null) return;
28312
+ this.pressKey = this.openKey;
28313
+ const doc = this.deps.plot.ownerDocument;
28314
+ const clear = () => void setTimeout(() => this.pressKey = null, 0);
28315
+ doc.addEventListener("pointerup", clear, { capture: true, once: true });
28316
+ doc.addEventListener("pointercancel", clear, { capture: true, once: true });
28317
+ };
28152
28318
  const doc = deps.plot.ownerDocument;
28153
28319
  injectStyles(CALLOUT_STYLE_ID, CALLOUT_CSS, doc);
28154
28320
  injectStyles(MARKS_STYLE_ID, MARKS_CSS, doc);
@@ -28156,12 +28322,14 @@ var MarkPopover = class {
28156
28322
  this.anchor.className = "vela-marks-anchor";
28157
28323
  Object.assign(this.anchor.style, { position: "absolute", pointerEvents: "none", left: "0", top: "0", width: "0", height: "0" });
28158
28324
  deps.plot.appendChild(this.anchor);
28325
+ doc.addEventListener("pointerdown", this.onPress, true);
28159
28326
  }
28160
28327
  /** The cluster key the open popup belongs to, or null. */
28161
28328
  get key() {
28162
28329
  return this.openKey;
28163
28330
  }
28164
28331
  open(cluster, rect) {
28332
+ if (this.pressKey === cluster.key) return;
28165
28333
  this.close();
28166
28334
  this.place(rect);
28167
28335
  const gen = ++this.generation;
@@ -28209,6 +28377,7 @@ var MarkPopover = class {
28209
28377
  }
28210
28378
  destroy() {
28211
28379
  this.close();
28380
+ this.deps.plot.ownerDocument.removeEventListener("pointerdown", this.onPress, true);
28212
28381
  this.anchor.remove();
28213
28382
  }
28214
28383
  place(rect) {
@@ -29105,6 +29274,13 @@ var NativeRenderer = class {
29105
29274
  this.backdropRenderer = new BackdropRenderer();
29106
29275
  this.volumeRenderer = new VolumeRenderer();
29107
29276
  /** SDK renderer layers instantiated at mount ({@link registerRendererLayer}). */
29277
+ /**
29278
+ * The mounted SDK layers. One BASE entry per registered definition (channel = the layer
29279
+ * id, no owner of its own), plus one DEDICATED entry per mounted native indicator whose
29280
+ * model names its own channel (`native.channel` — an instance of a multi-instance type):
29281
+ * same definition, its own canvas, reading that instance's channel and following that
29282
+ * instance's pane and z key. `channel` is unique across the array.
29283
+ */
29108
29284
  this.extLayers = [];
29109
29285
  /** Last applied layer-canvas order (ids below + above the data canvas) — re-slotted only on change. */
29110
29286
  this.layerOrderSig = "";
@@ -30250,11 +30426,7 @@ var NativeRenderer = class {
30250
30426
  Object.assign(this.overlayRoot.style, { position: "absolute", inset: "0", pointerEvents: "none" });
30251
30427
  this.plot = document.createElement("div");
30252
30428
  Object.assign(this.plot.style, { position: "absolute", top: "0", right: "0", bottom: "0", left: "0" });
30253
- this.extLayers = rendererLayers().map((def) => {
30254
- const canvas = document.createElement("canvas");
30255
- Object.assign(canvas.style, { position: "absolute", inset: "0", width: "100%", height: "100%", pointerEvents: "none" });
30256
- return { def, instance: def.create(), canvas };
30257
- });
30429
+ this.extLayers = rendererLayers().map((def) => ({ def, instance: def.create(), canvas: this.createLayerCanvas(), channel: def.id, owner: null }));
30258
30430
  const below = this.extLayers.filter((l) => l.def.placement === "below-data").map((l) => l.canvas);
30259
30431
  const above = this.extLayers.filter((l) => l.def.placement !== "below-data").map((l) => l.canvas);
30260
30432
  this.plot.append(this.backdropCanvas, ...below, this.dataCanvas, this.volumeCanvas, this.vpvrCanvas, ...above, this.chromeCanvas, this.drawingsCanvas, this.cursorCanvas, this.overlayRoot);
@@ -30271,6 +30443,7 @@ var NativeRenderer = class {
30271
30443
  this.volumeRenderer.mount(this.volumeCanvas);
30272
30444
  this.vpvrRenderer.mount(this.vpvrCanvas);
30273
30445
  for (const l of this.extLayers) l.instance.mount(l.canvas);
30446
+ for (const m of this.scene.indicators.values()) this.ensureInstanceLayer(m);
30274
30447
  this.chrome.mount(this.chromeCanvas);
30275
30448
  this.crosshairLayer.mount(this.cursorCanvas);
30276
30449
  this.coords.setViewport(defaultViewport());
@@ -30863,6 +31036,7 @@ var NativeRenderer = class {
30863
31036
  this.refreshAnchorOffset(model);
30864
31037
  if (model.native && this.extLayers.some((l) => l.def.id === model.native.type)) this.scene.assignIndicatorZTop(model.id);
30865
31038
  else this.scene.assignIndicatorZ(model.id);
31039
+ this.ensureInstanceLayer(model);
30866
31040
  if (model.legend !== false) {
30867
31041
  this.inputsUI.upsert(model.id, model.shorttitle ?? model.title, model.inputs, model.inputValues, model.paneId, {
30868
31042
  native: !!model.native,
@@ -30906,6 +31080,7 @@ var NativeRenderer = class {
30906
31080
  this.scene.vpvrLayer = null;
30907
31081
  }
30908
31082
  this.scene.indicators.delete(handle.id);
31083
+ this.dropInstanceLayer(handle.id);
30909
31084
  this.scene.forgetIndicatorZ(handle.id);
30910
31085
  this.scene.forgetAnchorOffset(handle.id);
30911
31086
  this.scene.dropIndicatorScale(handle.id);
@@ -31699,29 +31874,31 @@ var NativeRenderer = class {
31699
31874
  up: bar.close >= bar.open
31700
31875
  };
31701
31876
  }
31702
- /** One group per indicator (name = indicator title), each with a row per drawable plot. */
31877
+ /** One group per indicator (name = indicator title), each with a row per plot shown in the data window. */
31703
31878
  dataWindowGroups(idx, pricePane) {
31704
31879
  const groups = [];
31705
31880
  for (const model of this.scene.indicators.values()) {
31706
- const rows = this.dataWindowRowsFor(model, idx, pricePane);
31881
+ const rows = this.readoutRowsFor(model, idx, pricePane, "dataWindow");
31707
31882
  if (rows.length) groups.push({ name: model.title, rows });
31708
31883
  }
31709
31884
  return groups;
31710
31885
  }
31711
- /** One indicator's readout at bar `idx`: a row per drawable plot, formatted on its pane's scale. */
31712
- dataWindowRowsFor(model, idx, pricePane) {
31886
+ /** One indicator's readout at bar `idx` for one value surface (the legend or the data window):
31887
+ * a row per plot shown on that surface, formatted on its pane's scale. A plot's `display`
31888
+ * decides per surface — a data-window-only plot has a row here and none in the legend. */
31889
+ readoutRowsFor(model, idx, pricePane, surface) {
31713
31890
  if (model.native?.type === "volume") return this.volumeReadoutRows(model, idx);
31714
31891
  const pane = (model.paneId ? this.scene.panes.get(model.paneId) : null) ?? pricePane;
31715
31892
  const off = this.scene.offsetOf(model.id);
31716
31893
  const rows = [];
31717
31894
  for (const s of model.series) {
31895
+ if (!seriesShownOn(s, surface)) continue;
31718
31896
  let value;
31719
31897
  let color;
31720
31898
  if (s.kind === "candle" || s.kind === "bar") {
31721
31899
  value = s.bars[idx - off]?.close;
31722
31900
  color = s.style?.up ?? this.theme.upColor;
31723
31901
  } else if (isLineLikeSeries(s)) {
31724
- if (s.visible === false) continue;
31725
31902
  value = s.points[idx - off]?.value;
31726
31903
  color = s.points[idx - off]?.color ?? s.style.color;
31727
31904
  } else {
@@ -31744,9 +31921,10 @@ var NativeRenderer = class {
31744
31921
  const color = bar.close >= bar.open ? cfg?.upColor ?? this.theme.upColor : cfg?.downColor ?? this.theme.downColor;
31745
31922
  return [{ label: model.title, value: formatVolume(vol), color }];
31746
31923
  }
31747
- /** Refresh the plot values beside every legend title — the same readout the data window
31748
- * shows (crosshair bar, else the latest bar), pushed per paint. A hidden indicator has
31749
- * no scene model, so its row is absent from the map and its readout clears. */
31924
+ /** Refresh the plot values beside every legend title — the same bar the data window reads
31925
+ * (crosshair bar, else the latest bar), pushed per paint, filtered to the plots whose
31926
+ * `display` includes the legend. A hidden indicator has no scene model, so its row is
31927
+ * absent from the map and its readout clears. */
31750
31928
  updateLegendValues() {
31751
31929
  if (!this.inputsUI) return;
31752
31930
  const n = this.bars.length;
@@ -31755,7 +31933,7 @@ var NativeRenderer = class {
31755
31933
  const idx = this.hoverLogical != null ? this.hoverLogical : n - 1;
31756
31934
  const pricePane = this.dataWindowPricePane();
31757
31935
  for (const model of this.scene.indicators.values()) {
31758
- values.set(model.id, this.dataWindowRowsFor(model, idx, pricePane).map((r) => ({ value: r.value, color: r.color })));
31936
+ values.set(model.id, this.readoutRowsFor(model, idx, pricePane, "legend").map((r) => ({ value: r.value, color: r.color })));
31759
31937
  }
31760
31938
  }
31761
31939
  this.inputsUI.setPlotValues(values);
@@ -31781,9 +31959,9 @@ var NativeRenderer = class {
31781
31959
  const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
31782
31960
  for (const l of this.extLayers) {
31783
31961
  if (!l.def.repaintOnCursor) continue;
31784
- const lp = this.layerPane(l.def.id) ?? pane;
31962
+ const lp = this.layerPane(l) ?? pane;
31785
31963
  if (lp.collapsed) continue;
31786
- l.instance.render(this.extLayerArgs(l.def.id, lp.scale, lp.bounds, nowMs));
31964
+ l.instance.render(this.extLayerArgs(l, lp.scale, lp.bounds, nowMs));
31787
31965
  if (this.animZoom.on && l.instance.animating?.()) this.animator.start();
31788
31966
  }
31789
31967
  }
@@ -31792,13 +31970,15 @@ var NativeRenderer = class {
31792
31970
  if (canvas.width === 0 || canvas.height === 0) return;
31793
31971
  canvas.getContext("2d")?.clearRect(0, 0, canvas.width, canvas.height);
31794
31972
  }
31795
- /** One frame's args for an SDK renderer layer (shared by the data + cursor paint paths). */
31796
- extLayerArgs(id, scale, bounds, nowMs) {
31973
+ /** One frame's args for an SDK renderer layer (shared by the data + cursor paint paths).
31974
+ * Data + pending come from the entry's channel (per instance for a dedicated entry);
31975
+ * the settings bag is the TYPE's — a chart type's dialog values, one bag per type. */
31976
+ extLayerArgs(l, scale, bounds, nowMs) {
31797
31977
  return {
31798
31978
  bars: this.scene.bars,
31799
- data: this.scene.nativeData.get(id),
31800
- settings: this.scene.nativeData.get(`${id}-settings`) ?? {},
31801
- pending: this.scene.nativePending.get(id) ?? [],
31979
+ data: this.scene.nativeData.get(l.channel),
31980
+ settings: this.scene.nativeData.get(`${l.def.id}-settings`) ?? {},
31981
+ pending: this.scene.nativePending.get(l.channel) ?? [],
31802
31982
  coords: this.coords,
31803
31983
  scale,
31804
31984
  bounds,
@@ -31841,12 +32021,12 @@ var NativeRenderer = class {
31841
32021
  const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
31842
32022
  let folded = null;
31843
32023
  for (const l of this.extLayers) {
31844
- const lp = this.layerPane(l.def.id) ?? pane;
32024
+ const lp = this.layerPane(l) ?? pane;
31845
32025
  if (lp.collapsed) {
31846
32026
  this.clearLayerCanvas(l.canvas);
31847
32027
  continue;
31848
32028
  }
31849
- const args = this.extLayerArgs(l.def.id, lp.scale, lp.bounds, nowMs);
32029
+ const args = this.extLayerArgs(l, lp.scale, lp.bounds, nowMs);
31850
32030
  l.instance.render(args);
31851
32031
  if (lp === pane) folded = foldBaseModulation(folded, l.instance.modulateBase?.(args) ?? null);
31852
32032
  if (this.animZoom.on && l.instance.animating?.()) this.animator.start();
@@ -32010,7 +32190,7 @@ var NativeRenderer = class {
32010
32190
  const or = overlaySeriesRange(this.scene.indicators.values(), i0, i1, (id) => this.scene.offsetOf(id));
32011
32191
  if (or) dr = dr ? { min: Math.min(dr.min, or.min), max: Math.max(dr.max, or.max) } : or;
32012
32192
  }
32013
- const includeCandles = pane.kind === "price" && !this.scene.candlesHidden;
32193
+ const includeCandles = pane.kind === "price" && (!this.scene.candlesHidden || this.priceLayersAnchoredToBars(masterModels));
32014
32194
  pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
32015
32195
  pane.percentBaseline = pane.kind === "price" ? this.bars[i0]?.close ?? 0 : this.firstVisibleValue(masterModels, i0);
32016
32196
  pane.axisFormat = void 0;
@@ -32129,19 +32309,60 @@ var NativeRenderer = class {
32129
32309
  }
32130
32310
  return null;
32131
32311
  }
32132
- /** The mounted native indicator that OWNS an SDK layer the one whose type equals the
32133
- * layer id (the id doubles as the data channel, so the pairing is the SDK's own
32134
- * contract). Null for chart-type channels and while the owner is hidden (a hidden
32135
- * indicator leaves the scene; its cleared data channel paints nothing anyway). */
32136
- layerOwner(layerId) {
32312
+ /** One transparent, pointer-transparent SDK layer canvas covering the plot. */
32313
+ createLayerCanvas() {
32314
+ const canvas = document.createElement("canvas");
32315
+ Object.assign(canvas.style, { position: "absolute", inset: "0", width: "100%", height: "100%", pointerEvents: "none" });
32316
+ return canvas;
32317
+ }
32318
+ /**
32319
+ * Mount the DEDICATED layer of a native indicator that names its own channel (an
32320
+ * instance of a multi-instance type painting through a registered layer): a fresh
32321
+ * instance of the layer's definition on its own canvas, reading that channel. Idempotent
32322
+ * — a show after hide re-mounts the model, and the layer (kept across the hide, like the
32323
+ * z key) is simply reused. Skipped while the renderer itself is unmounted (`mount()`
32324
+ * catches up from the scene).
32325
+ */
32326
+ ensureInstanceLayer(model) {
32327
+ const channel = model.native?.channel;
32328
+ if (!channel || !this.plot) return;
32329
+ if (this.extLayers.some((l) => l.owner === model.id)) return;
32330
+ const base = this.extLayers.find((l) => l.owner === null && l.def.id === model.native.type);
32331
+ if (!base) return;
32332
+ const canvas = this.createLayerCanvas();
32333
+ canvas.width = this.dataCanvas.width;
32334
+ canvas.height = this.dataCanvas.height;
32335
+ canvas.style.width = this.dataCanvas.style.width || "100%";
32336
+ canvas.style.height = this.dataCanvas.style.height || "100%";
32337
+ const entry = { def: base.def, instance: base.def.create(), canvas, channel, owner: model.id };
32338
+ this.extLayers.push(entry);
32339
+ this.plot.insertBefore(canvas, this.chromeCanvas);
32340
+ entry.instance.mount(canvas);
32341
+ }
32342
+ /** Tear down the dedicated layer of a removed indicator (a hide keeps it). */
32343
+ dropInstanceLayer(indicatorId) {
32344
+ const i = this.extLayers.findIndex((l2) => l2.owner === indicatorId);
32345
+ if (i < 0) return;
32346
+ const [l] = this.extLayers.splice(i, 1);
32347
+ l.instance.destroy?.();
32348
+ l.canvas.remove();
32349
+ }
32350
+ /** The mounted native indicator that OWNS an SDK layer entry. A dedicated entry is
32351
+ * owned by the instance it was mounted for; the base entry by the mounted indicator
32352
+ * whose type equals the layer id and that has no channel of its own (the id doubles
32353
+ * as the data channel, so the pairing is the SDK's own contract). Null for chart-type
32354
+ * channels and while the owner is hidden (a hidden indicator leaves the scene; its
32355
+ * cleared data channel paints nothing anyway). */
32356
+ layerOwner(l) {
32357
+ if (l.owner !== null) return this.scene.indicators.get(l.owner) ?? null;
32137
32358
  for (const m of this.scene.indicators.values()) {
32138
- if (m.native?.type === layerId) return m;
32359
+ if (m.native?.type === l.def.id && !m.native.channel) return m;
32139
32360
  }
32140
32361
  return null;
32141
32362
  }
32142
32363
  /** The pane an SDK layer paints on: its owner's pane, else the price pane. */
32143
- layerPane(layerId) {
32144
- const owner = this.layerOwner(layerId);
32364
+ layerPane(l) {
32365
+ const owner = this.layerOwner(l);
32145
32366
  const paneId = owner ? owner.paneId ?? PRICE_PANE_ID : PRICE_PANE_ID;
32146
32367
  return this.scene.panes.get(paneId) ?? null;
32147
32368
  }
@@ -32149,12 +32370,12 @@ var NativeRenderer = class {
32149
32370
  * owned layers by their owner's z key against the candles' (an indicator restacked
32150
32371
  * below the candles takes its layer canvas along), unowned by declared placement. */
32151
32372
  orderedLayerCanvases() {
32152
- const byId = new Map(this.extLayers.map((l) => [l.def.id, l.canvas]));
32373
+ const byId = new Map(this.extLayers.map((l) => [l.channel, l.canvas]));
32153
32374
  const { below, above } = stackLayers(
32154
32375
  this.extLayers.map((l) => {
32155
- const owner = this.layerOwner(l.def.id);
32376
+ const owner = this.layerOwner(l);
32156
32377
  return {
32157
- id: l.def.id,
32378
+ id: l.channel,
32158
32379
  placement: l.def.placement === "below-data" ? "below-data" : "above-data",
32159
32380
  ownerZ: owner ? this.scene.zOf(owner.id) : null
32160
32381
  };
@@ -32176,7 +32397,7 @@ var NativeRenderer = class {
32176
32397
  syncLayerCanvasOrder() {
32177
32398
  if (this.extLayers.length === 0 || !this.plot) return;
32178
32399
  const { below, above } = this.orderedLayerCanvases();
32179
- const sig = [...below.map((c) => this.extLayers.find((l) => l.canvas === c).def.id), "|", ...above.map((c) => this.extLayers.find((l) => l.canvas === c).def.id)].join(",");
32400
+ const sig = [...below.map((c) => this.extLayers.find((l) => l.canvas === c).channel), "|", ...above.map((c) => this.extLayers.find((l) => l.canvas === c).channel)].join(",");
32180
32401
  if (sig === this.layerOrderSig) return;
32181
32402
  this.layerOrderSig = sig;
32182
32403
  for (const c of below) this.plot.insertBefore(c, this.dataCanvas);
@@ -32200,6 +32421,17 @@ var NativeRenderer = class {
32200
32421
  (m) => m.series.length === 0 && !!m.native && this.extLayers.some((l) => l.def.id === m.native.type)
32201
32422
  );
32202
32423
  }
32424
+ /**
32425
+ * True when SDK-layer content on the price pane paints at BAR PRICES without
32426
+ * contributing a series: the active chart type's own layer, or an overlay layer native
32427
+ * (a series-less model whose type names a mounted layer). Hiding the candles must then
32428
+ * keep the bars in the price scale — with nothing else on the pane the scale would fall
32429
+ * to the {0,1} placeholder and every such layer would paint off-screen and vanish.
32430
+ */
32431
+ priceLayersAnchoredToBars(masterModels) {
32432
+ if (this.extLayers.some((l) => l.owner === null && l.def.id === this.scene.priceStyle)) return true;
32433
+ return masterModels.some((m) => m.series.length === 0 && !!m.native && this.extLayers.some((l) => l.def.id === m.native.type));
32434
+ }
32203
32435
  /** Per-pane scale state for a host UI (e.g. a price-axis context menu): the pane's pixel
32204
32436
  * band (`top`/`height`, so a click y maps to a pane) plus its current axis `mode`/`log`.
32205
32437
  * Top-to-bottom order. Every pane is independent — the price pane from the scene setting,
@@ -35704,6 +35936,8 @@ exports.rendererDefaults = rendererDefaults;
35704
35936
  exports.rendererLayers = rendererLayers;
35705
35937
  exports.resolveEngines = resolveEngines;
35706
35938
  exports.resolveTheme = resolveTheme;
35939
+ exports.seriesInScale = seriesInScale;
35940
+ exports.seriesShownOn = seriesShownOn;
35707
35941
  exports.settingsRowValueKeys = settingsRowValueKeys;
35708
35942
  exports.sharedBarStore = sharedBarStore;
35709
35943
  exports.sidePanels = sidePanels;