@luxalgo/vela 0.6.9 → 0.6.11

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 (46) hide show
  1. package/dist/{DataProvider-8Z95Q-RJ.d.cts → DataProvider-BBf-jc6W.d.ts} +49 -1
  2. package/dist/{DataProvider-DExJrfut.d.ts → DataProvider-p0TEyhlX.d.cts} +49 -1
  3. package/dist/{chunk-6WDDVMBJ.js → chunk-73PEA4MU.js} +707 -353
  4. package/dist/{chunk-62SVGONC.js → chunk-G77Y7LK2.js} +2 -2
  5. package/dist/{chunk-STHSKXOR.js → chunk-IO3NYSQV.js} +650 -157
  6. package/dist/{chunk-WFSZBX3R.js → chunk-KG4YT3TI.js} +3 -1
  7. package/dist/{chunk-FCVIG7JG.js → chunk-MHY7MVXH.js} +5 -1
  8. package/dist/{chunk-EQCHJZOT.js → chunk-MTLJKZDZ.js} +10 -2
  9. package/dist/{contributions-D7PVZO2i.d.ts → contributions-Bbe2R-mQ.d.ts} +20 -6
  10. package/dist/{contributions-C1U2Krwg.d.cts → contributions-CO01zWve.d.cts} +20 -6
  11. package/dist/index.cjs +719 -350
  12. package/dist/index.d.cts +37 -10
  13. package/dist/index.d.ts +37 -10
  14. package/dist/index.js +5 -5
  15. package/dist/{options-FM0peknS.d.ts → options-yp7sA96q.d.cts} +14 -7
  16. package/dist/{options-FM0peknS.d.cts → options-yp7sA96q.d.ts} +14 -7
  17. package/dist/{plugin-DfVqBz9p.d.cts → plugin-CkkH8QnX.d.cts} +3 -3
  18. package/dist/{plugin-7bkF32Rk.d.ts → plugin-DwxjM3Ni.d.ts} +3 -3
  19. package/dist/plugin.cjs +12 -1
  20. package/dist/plugin.d.cts +4 -4
  21. package/dist/plugin.d.ts +4 -4
  22. package/dist/plugin.js +3 -3
  23. package/dist/providers/binance.d.cts +2 -2
  24. package/dist/providers/binance.d.ts +2 -2
  25. package/dist/providers/coinbase.d.cts +2 -2
  26. package/dist/providers/coinbase.d.ts +2 -2
  27. package/dist/providers/hyperliquid.d.cts +2 -2
  28. package/dist/providers/hyperliquid.d.ts +2 -2
  29. package/dist/{statusline-DOPiT6I6.d.cts → statusline-CHPDuKNp.d.ts} +13 -5
  30. package/dist/{statusline-zdF4eZLr.d.ts → statusline-DTHZUFqK.d.cts} +13 -5
  31. package/dist/ui.cjs +7 -1
  32. package/dist/ui.d.cts +3 -1
  33. package/dist/ui.d.ts +3 -1
  34. package/dist/ui.js +3 -3
  35. package/dist/vela.global.js +719 -350
  36. package/dist/vela.global.min.js +51 -49
  37. package/dist/widget.cjs +1249 -388
  38. package/dist/widget.d.cts +17 -6
  39. package/dist/widget.d.ts +17 -6
  40. package/dist/widget.js +7 -7
  41. package/dist/workspace.cjs +1249 -388
  42. package/dist/workspace.d.cts +103 -10
  43. package/dist/workspace.d.ts +103 -10
  44. package/dist/workspace.js +6 -6
  45. package/package.json +1 -1
  46. /package/dist/{chunk-AOGZBKUE.js → chunk-H26BEHF4.js} +0 -0
@@ -1,6 +1,6 @@
1
- import { deserializeDrawing, chartTypes, rendererLayers, foldBaseModulation, registerChartType, rendererDefaults, getDrawingType, settingsRowVisible, normalizeSettingsRow, TextLabel, Callout, resetDrawingSettings, registerNativeIndicator, chartType, getNativeIndicator, nativeIndicatorDescriptors, drawingTypes, settingsRowValueKeys, formatDuration, RegressionChannel, FixedRangeVolumeProfile, SegmentDrawing, ArrowMark, GlyphStamp, RadialFib, FibSpiral, DedekindTessellation, MachFigure, GannSquare, FibRatios, MeasureBox, PositionTool, AnchoredVwap, PatternDrawing, Comment, PriceNote, Signpost, Note, PriceLabel, GANN_SQUARE_ARCS, lineSegmentIntersection, DEFAULT_DRAWING_COLOR, GLYPH_OPTIONS, STAMP_SIZE_OPTIONS, LINE_STYLE_OPTIONS, DEDEKIND_CURVATURE_OPTIONS, MACH_NUMBER_OPTIONS, MACH_WAVE_COUNT_OPTIONS, TEXT_SIZE_OPTIONS, createDrawing, inputVisible, tickerModifierIds, CalloutBase, DIRECTION_OPTIONS } from './chunk-EQCHJZOT.js';
2
- import { themeTokens, Dialog, closeOpenPopovers, closeWidthPopover, fieldSection, buildFieldControl, fieldSeparator, fieldGrid, fieldRow, blendOver, splitColor, CalloutBubble, Menu, TextArea, Popover, NumberInput, buildColorPicker, isPopoverOpen, eventDismissedPopover, toggleSelectList, openPopoverTrigger, fieldGridColumns, FIELD_GAP_PX, STATIC_TOKENS } from './chunk-FCVIG7JG.js';
3
- import { icon, iconAt, withAlpha, WARNING, ACCENT, NEUTRAL, BEARISH, BULLISH, isDarkColor, SERIES_LINE, SLATE, CROSSHAIR, TRADE_EXIT, TRADE_SHORT, TRADE_LONG, overlayScrollbarCss, SLATE_DEEP, VALID, INVALID, FIELD_FOCUS_CSS, FIELD_FOCUS_RING, svg24, svg24Solid } from './chunk-WFSZBX3R.js';
1
+ import { deserializeDrawing, chartTypes, rendererLayers, foldBaseModulation, registerChartType, rendererDefaults, getDrawingType, settingsRowVisible, normalizeSettingsRow, TextLabel, Callout, resetDrawingSettings, registerNativeIndicator, chartType, getNativeIndicator, nativeIndicatorDescriptors, drawingTypes, settingsRowValueKeys, formatDuration, RegressionChannel, FixedRangeVolumeProfile, SegmentDrawing, ArrowMark, GlyphStamp, RadialFib, FibSpiral, DedekindTessellation, MachFigure, GannSquare, FibRatios, MeasureBox, PositionTool, AnchoredVwap, PatternDrawing, Comment, PriceNote, Signpost, Note, PriceLabel, GANN_SQUARE_ARCS, lineSegmentIntersection, DEFAULT_DRAWING_COLOR, GLYPH_OPTIONS, STAMP_SIZE_OPTIONS, LINE_STYLE_OPTIONS, DEDEKIND_CURVATURE_OPTIONS, MACH_NUMBER_OPTIONS, MACH_WAVE_COUNT_OPTIONS, TEXT_SIZE_OPTIONS, createDrawing, inputVisible, tickerModifierIds, CalloutBase, DIRECTION_OPTIONS } from './chunk-MTLJKZDZ.js';
2
+ import { themeTokens, Dialog, closeOpenPopovers, closeWidthPopover, fieldSection, buildFieldControl, fieldSeparator, fieldGrid, fieldRow, blendOver, splitColor, CalloutBubble, Menu, TextArea, Popover, NumberInput, buildColorPicker, isPopoverOpen, eventDismissedPopover, toggleSelectList, openPopoverTrigger, fieldGridColumns, FIELD_GAP_PX, STATIC_TOKENS } from './chunk-MHY7MVXH.js';
3
+ import { icon, iconAt, withAlpha, WARNING, ACCENT, NEUTRAL, BEARISH, BULLISH, isDarkColor, SERIES_LINE, SLATE, CROSSHAIR, TRADE_EXIT, TRADE_SHORT, TRADE_LONG, overlayScrollbarCss, SLATE_DEEP, VALID, INVALID, FIELD_FOCUS_CSS, FIELD_FOCUS_RING, svg24, svg24Solid } from './chunk-KG4YT3TI.js';
4
4
 
5
5
  // src/core/events/EventBus.ts
6
6
  var TypedEventBus = class {
@@ -696,6 +696,23 @@ var BarStore = class {
696
696
  };
697
697
  var sharedBarStore = new BarStore();
698
698
 
699
+ // src/data/symbol-groups.ts
700
+ function isGroupRow(d) {
701
+ return d.group != null && d.ticker === d.group;
702
+ }
703
+ function groupKeyOf(d) {
704
+ return `${(d.prefix ?? d.provider ?? "").toLowerCase()}:${(d.group ?? "").toUpperCase()}`;
705
+ }
706
+ function groupMembers(pool, groupRow) {
707
+ const key = groupKeyOf(groupRow);
708
+ return pool.filter((s) => s.group != null && !isGroupRow(s) && groupKeyOf(s) === key);
709
+ }
710
+ function defaultMemberOf(pool, groupRow) {
711
+ const members = groupMembers(pool, groupRow);
712
+ const defaults = members.filter((m) => m.default);
713
+ return (defaults.length === 1 ? defaults[0] : members[0]) ?? null;
714
+ }
715
+
699
716
  // src/data/ProviderRegistry.ts
700
717
  var normName = (n) => n.trim().toLowerCase();
701
718
  var normTicker = (t) => t.trim().toUpperCase();
@@ -785,17 +802,24 @@ var ProviderRegistry = class {
785
802
  resolve(raw, opts = {}) {
786
803
  const { provider, ticker } = this.parse(raw);
787
804
  if (provider) {
788
- if (this.entries.has(provider)) return { provider, ticker };
805
+ if (this.entries.has(provider)) {
806
+ const d = this.entries.get(provider).byTicker?.get(normTicker(ticker));
807
+ return { provider, ticker: d != null && isGroupRow(d) ? this.loadableTicker(provider, d) : ticker };
808
+ }
789
809
  const key = `${provider}:${normTicker(ticker)}`;
790
810
  for (const name of this.candidateOrder(opts.default)) {
791
811
  const d = this.entries.get(name).prefixIndex?.get(key);
792
- if (d) return { provider: name, ticker: d.ticker };
812
+ if (d) return { provider: name, ticker: this.loadableTicker(name, d) };
793
813
  }
794
814
  return null;
795
815
  }
796
816
  const norm = normTicker(ticker);
797
817
  for (const name of this.candidateOrder(opts.default)) {
798
- if (this.entries.get(name).index?.has(norm)) return { provider: name, ticker };
818
+ const e = this.entries.get(name);
819
+ if (e.index?.has(norm)) {
820
+ const d = e.byTicker?.get(norm);
821
+ return { provider: name, ticker: d != null && isGroupRow(d) ? this.loadableTicker(name, d) : ticker };
822
+ }
799
823
  }
800
824
  if (opts.lenient && this.entries.size === 1) {
801
825
  const only = this.names()[0];
@@ -804,6 +828,13 @@ var ProviderRegistry = class {
804
828
  }
805
829
  return null;
806
830
  }
831
+ /** What a resolved descriptor LOADS: itself — unless it is a GROUP row (listed, never
832
+ * served), which loads its default member (single `default`, else first listed). A
833
+ * memberless group keeps its own ticker and fails downstream like any unknown symbol. */
834
+ loadableTicker(name, d) {
835
+ if (!isGroupRow(d)) return d.ticker;
836
+ return defaultMemberOf(this.entries.get(name)?.descriptors ?? [], d)?.ticker ?? d.ticker;
837
+ }
807
838
  /**
808
839
  * Resolve now if possible, else resolve later — re-attempting after each provider
809
840
  * settles. The promise stays pending until some registered provider can serve the
@@ -947,6 +978,28 @@ var CachingDataFeed = class {
947
978
  this.store.merge(key, dropForming(bars));
948
979
  return bars;
949
980
  }
981
+ /**
982
+ * Progressive twin of {@link load}: a COLD load streams through the inner feed's
983
+ * progressive path — batches forwarded verbatim, the FINAL answer cached exactly as
984
+ * `load` caches — while a cache-covered load answers once through `load` itself (a
985
+ * warm chart has nothing to stream). Falls back to `load` wholesale when the inner
986
+ * feed lacks the capability, so callers may prefer this method unconditionally.
987
+ */
988
+ async loadProgressive(cfg, onBatch, opts) {
989
+ if (cfg.data && cfg.data.length > 0) return this.inner.load(cfg);
990
+ if (!this.inner.loadProgressive) return null;
991
+ const symbol = cfg.symbol ?? "TEST";
992
+ const key = cacheKey(symbol, cfg.timeframe ?? "60", cfg.session);
993
+ const cached = this.store.get(key);
994
+ const n = cfg.bars ?? 500;
995
+ const lastCached = cached?.[cached.length - 1];
996
+ if (cached && lastCached && cached.length >= n - 1 && this.inner.loadRange) return this.load(cfg);
997
+ this.store.retainSymbol(symbol);
998
+ const bars = await this.inner.loadProgressive(cfg, onBatch, opts);
999
+ if (bars == null) return null;
1000
+ this.store.merge(key, dropForming(bars));
1001
+ return bars;
1002
+ }
950
1003
  /**
951
1004
  * Cache-backed ranged fetch — the gateway engines use for secondary series
952
1005
  * (`request.security` HTF/LTF/cross-symbol) and the orchestrator's backward
@@ -1192,6 +1245,17 @@ var MultiProviderFeed = class {
1192
1245
  this.prefetchSymbolInfo(resolved);
1193
1246
  return this.cache.load(canonical(cfg, resolved));
1194
1247
  }
1248
+ /** Progressive twin of {@link load} — same resolution, the cache streams the batches. */
1249
+ async loadProgressive(cfg, onBatch, opts) {
1250
+ if (cfg.data && cfg.data.length > 0) {
1251
+ this.liveBars = cfg.data.map((b) => ({ ...b }));
1252
+ return cfg.data;
1253
+ }
1254
+ const resolved = await this.registry.whenResolvable(rawSymbol(cfg), { default: this.primaryProvider });
1255
+ this.primaryProvider = resolved.provider;
1256
+ this.prefetchSymbolInfo(resolved);
1257
+ return this.cache.loadProgressive(canonical(cfg, resolved), onBatch, opts);
1258
+ }
1195
1259
  /**
1196
1260
  * Synchronous per-symbol metadata for engines (Pine `syminfo.*`), served from the cache
1197
1261
  * warmed by load(). Undefined until the prefetch lands (the engine then synthesizes a
@@ -1279,6 +1343,18 @@ var RegistryFetchFeed = class {
1279
1343
  if (!provider) return Promise.resolve([]);
1280
1344
  return safeBars(provider, ticker, cfg.timeframe ?? "60", { limit: cfg.bars ?? 500, session: cfg.session });
1281
1345
  }
1346
+ async loadProgressive(cfg, onBatch, opts) {
1347
+ const { provider: name, ticker } = parseSymbol(cfg.symbol ?? "");
1348
+ const provider = this.registry.get(name ?? "");
1349
+ if (!provider) return [];
1350
+ if (!provider.getBarsProgressive) return null;
1351
+ try {
1352
+ return await provider.getBarsProgressive(ticker, cfg.timeframe ?? "60", { limit: cfg.bars ?? 500, session: cfg.session }, onBatch, opts);
1353
+ } catch (e) {
1354
+ console.warn(`[vela] progressive fetch failed for ${ticker} ${cfg.timeframe ?? "60"} \u2014 ${e instanceof Error ? e.message : String(e)}`);
1355
+ return [];
1356
+ }
1357
+ }
1282
1358
  loadRange(cfg, range) {
1283
1359
  const { provider: name, ticker } = parseSymbol(cfg.symbol ?? "");
1284
1360
  const provider = this.registry.get(name ?? "");
@@ -2426,7 +2502,7 @@ var DrawingToolbar = class {
2426
2502
  this.root.replaceChildren();
2427
2503
  this.groupCells.clear();
2428
2504
  this.groupIcons.clear();
2429
- this.cursorBtn = this.makeButton(CURSOR_ICON, "Cursor", () => this.onArm(null));
2505
+ this.cursorBtn = this.makeButton(CURSOR_ICON, "Cursor", () => this.onCursorClick());
2430
2506
  this.root.appendChild(this.cursorBtn);
2431
2507
  if (this.def.groups.length > 0) this.root.appendChild(this.divider());
2432
2508
  for (const g of this.def.groups) {
@@ -2530,6 +2606,14 @@ var DrawingToolbar = class {
2530
2606
  this.magnetIcon = icon2;
2531
2607
  return cell;
2532
2608
  }
2609
+ /** Cursor returns to select/idle: an active measure/eraser mode exits through its own
2610
+ * toggle callback (disarming a tool via `onArm(null)` alone can't — the host treats a
2611
+ * null arm as a no-op side effect of entering those modes), then the tool disarms. */
2612
+ onCursorClick() {
2613
+ if (this.measureActive) this.onMeasure();
2614
+ if (this.eraserActive) this.onEraser();
2615
+ this.onArm(null);
2616
+ }
2533
2617
  /** Clicking the icon arms the group's last-used tool (it does NOT open the flyout). */
2534
2618
  onGroupIconClick(group) {
2535
2619
  const type = this.lastUsed.get(group.id) ?? group.tools[0]?.type;
@@ -3819,6 +3903,9 @@ var CLOSE_SVG = iconAt("close", LEGEND_ICON_PX2);
3819
3903
  var FOLD_SVG = iconAt("chevron-up", LEGEND_ICON_PX2);
3820
3904
  var UNFOLD_SVG = iconAt("chevron-down", LEGEND_ICON_PX2);
3821
3905
  var OVERVIEW_SVG = iconAt("objects", LEGEND_ICON_PX2);
3906
+ function legendCalloutsDisplay(open, hasCallouts) {
3907
+ return !open && hasCallouts ? "inline-flex" : "none";
3908
+ }
3822
3909
  var InputsUI = class {
3823
3910
  constructor(container, theme, paneBoundsOf) {
3824
3911
  this.container = container;
@@ -4022,7 +4109,7 @@ var InputsUI = class {
4022
4109
  row.callouts = [];
4023
4110
  row.calloutsEl.replaceChildren();
4024
4111
  const views = this.legendCallouts?.(row.id) ?? [];
4025
- row.calloutsEl.style.display = views.length > 0 ? "inline-flex" : "none";
4112
+ row.calloutsEl.style.display = legendCalloutsDisplay(row.highlighted, views.length > 0);
4026
4113
  for (const view of views) {
4027
4114
  const bubble = new CalloutBubble({
4028
4115
  icon: view.icon,
@@ -4506,11 +4593,11 @@ var InputsUI = class {
4506
4593
  row.controlsEl.style.display = open || row.hidden ? "inline-flex" : "none";
4507
4594
  if (open) {
4508
4595
  row.el.appendChild(row.statusEl);
4509
- row.el.appendChild(row.calloutsEl);
4596
+ for (const bubble of row.callouts) bubble.hidePanel();
4510
4597
  } else {
4511
4598
  row.el.insertBefore(row.statusEl, row.valuesEl);
4512
- row.el.insertBefore(row.calloutsEl, row.statusEl);
4513
4599
  }
4600
+ row.calloutsEl.style.display = legendCalloutsDisplay(open, row.callouts.length > 0);
4514
4601
  for (const child of Array.from(row.controlsEl.children)) {
4515
4602
  if (!(child instanceof HTMLElement) || child === row.eyeEl) continue;
4516
4603
  if (child === row.extrasEl) {
@@ -4600,7 +4687,7 @@ var ICONS = {
4600
4687
  };
4601
4688
  var STYLE_ID2 = "vela-pane-controls";
4602
4689
  var ICON_PX = 12;
4603
- var CLUSTER_PILL = "rgba(0,0,0,0.28)";
4690
+ var CLUSTER_PILL = "rgba(0,0,0,0.65)";
4604
4691
  function ensureStyles2() {
4605
4692
  if (typeof document === "undefined" || document.getElementById(STYLE_ID2)) return;
4606
4693
  const st = document.createElement("style");
@@ -4620,8 +4707,12 @@ var PaneControls = class {
4620
4707
  this.deps = deps;
4621
4708
  this.clusters = /* @__PURE__ */ new Map();
4622
4709
  this.hoverPaneId = null;
4710
+ /** Mobile: hover clusters are meaningless without a cursor — suppressed; a
4711
+ * collapsed pane's standalone expand chip stays (the only way back up). */
4712
+ this.suspended = false;
4623
4713
  /** Reveal the cluster for the pane under the cursor, resolved from the pointer's y in the plot. */
4624
4714
  this.onPlotMove = (e) => {
4715
+ if (this.suspended) return;
4625
4716
  const rect = this.plot.getBoundingClientRect();
4626
4717
  const y = e.clientY - rect.top;
4627
4718
  let hit = null;
@@ -4693,7 +4784,12 @@ var PaneControls = class {
4693
4784
  }
4694
4785
  if (p.count > 1) {
4695
4786
  cluster.appendChild(
4696
- this.button(p.maximized ? ICONS.restore : ICONS.maximize, p.maximized ? "Restore pane" : "Maximize pane", false, () => this.deps.onToggleMaximize(p.id), { role: "maximize" })
4787
+ this.button(p.maximized ? ICONS.restore : ICONS.maximize, p.maximized ? "Restore pane" : "Maximize pane", false, () => this.deps.onToggleMaximize(p.id), {
4788
+ role: "maximize",
4789
+ // Same inverse-chip treatment as the collapsed pane's expand toggle: the
4790
+ // maximized state must read as an active state, not just a swapped glyph.
4791
+ selected: p.maximized
4792
+ })
4697
4793
  );
4698
4794
  }
4699
4795
  }
@@ -4730,17 +4826,18 @@ var PaneControls = class {
4730
4826
  }
4731
4827
  const hovered = id === this.hoverPaneId;
4732
4828
  const hasButtons = cluster.children.length > 0;
4733
- const visible = hasButtons && (hovered || p.collapsed) && p.height > 8;
4829
+ const stateChipRole = p.collapsed ? "collapse" : !this.suspended && p.maximized ? "maximize" : null;
4830
+ const visible = hasButtons && (hovered || stateChipRole != null) && p.height > 8;
4734
4831
  cluster.style.right = `${rightPx}px`;
4735
4832
  cluster.style.top = p.collapsed ? `${p.top + Math.max(1, Math.round((p.height - 24) / 2))}px` : `${p.top + 4}px`;
4736
4833
  cluster.style.display = visible ? "flex" : "none";
4737
4834
  if (!visible) continue;
4738
- const soloExpand = p.collapsed && !hovered;
4739
- cluster.style.background = soloExpand ? "transparent" : CLUSTER_PILL;
4835
+ const soloChip = stateChipRole != null && !hovered;
4836
+ cluster.style.background = soloChip ? "transparent" : CLUSTER_PILL;
4740
4837
  for (const child of cluster.children) {
4741
4838
  const btn2 = child;
4742
4839
  btn2.style.display = "inline-flex";
4743
- btn2.style.visibility = soloExpand && btn2.dataset.role !== "collapse" ? "hidden" : "visible";
4840
+ btn2.style.visibility = soloChip && btn2.dataset.role !== stateChipRole ? "hidden" : "visible";
4744
4841
  }
4745
4842
  }
4746
4843
  }
@@ -4750,6 +4847,14 @@ var PaneControls = class {
4750
4847
  this.hoverPaneId = paneId;
4751
4848
  this.reposition();
4752
4849
  }
4850
+ /** Mobile suppression: no hover clusters (touch has no cursor; the shell's own
4851
+ * chrome covers maximize), while collapsed panes keep their expand chips. */
4852
+ setSuspended(on) {
4853
+ if (on === this.suspended) return;
4854
+ this.suspended = on;
4855
+ if (on) this.hoverPaneId = null;
4856
+ this.reposition();
4857
+ }
4753
4858
  destroy() {
4754
4859
  this.plot.removeEventListener("pointermove", this.onPlotMove);
4755
4860
  this.plot.removeEventListener("pointerleave", this.onPlotLeave);
@@ -4900,160 +5005,6 @@ var AxisScaleButtons = class {
4900
5005
  }
4901
5006
  };
4902
5007
 
4903
- // src/renderers/shared/TableOverlay.ts
4904
- var SIZE_PX = {
4905
- auto: 13,
4906
- tiny: 10,
4907
- small: 11,
4908
- normal: 13,
4909
- large: 16,
4910
- huge: 20
4911
- };
4912
- function fontPxOf(size) {
4913
- if (typeof size === "number") return size > 0 ? size : SIZE_PX.auto;
4914
- return SIZE_PX[size] ?? SIZE_PX.auto;
4915
- }
4916
- function tableHasContent(t) {
4917
- return t.cells.some((row) => row?.some((c) => c != null && !c.merged));
4918
- }
4919
- function mergeRenderPlan(t) {
4920
- const span = /* @__PURE__ */ new Map();
4921
- const omit = /* @__PURE__ */ new Set();
4922
- for (const m of t.merges) {
4923
- span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
4924
- for (let r = m.startRow; r <= m.endRow; r += 1) {
4925
- for (let c = m.startCol; c <= m.endCol; c += 1) {
4926
- if (r !== m.startRow || c !== m.startCol) omit.add(`${r}:${c}`);
4927
- }
4928
- }
4929
- }
4930
- for (let r = 0; r < t.rows; r += 1) {
4931
- for (let c = 0; c < t.columns; c += 1) {
4932
- if (t.cells[r]?.[c]?.merged && !span.has(`${r}:${c}`)) omit.add(`${r}:${c}`);
4933
- }
4934
- }
4935
- for (const key of span.keys()) omit.delete(key);
4936
- return { span, omit };
4937
- }
4938
- var TableOverlay = class {
4939
- constructor(container, theme, paneBounds) {
4940
- this.container = container;
4941
- this.theme = theme;
4942
- this.paneBounds = paneBounds;
4943
- this.lastTables = [];
4944
- if (getComputedStyle(container).position === "static") container.style.position = "relative";
4945
- this.root = document.createElement("div");
4946
- Object.assign(this.root.style, {
4947
- position: "absolute",
4948
- inset: "0",
4949
- pointerEvents: "none",
4950
- overflow: "hidden",
4951
- zIndex: "3"
4952
- });
4953
- container.appendChild(this.root);
4954
- }
4955
- update(tables) {
4956
- this.lastTables = tables;
4957
- this.root.replaceChildren();
4958
- for (const t of tables) {
4959
- if (tableHasContent(t)) this.root.appendChild(this.renderTable(t));
4960
- }
4961
- }
4962
- /** Re-render at the current pane geometry — after layout settles or on resize. */
4963
- reposition() {
4964
- if (this.root.isConnected) this.update(this.lastTables);
4965
- }
4966
- /** Show/hide the whole overlay. Tables anchor to pane corners, not to bars, so unlike the
4967
- * series content they DON'T vanish with an emptied chart — the loading state hides them. */
4968
- setVisible(visible) {
4969
- this.root.style.display = visible ? "" : "none";
4970
- }
4971
- destroy() {
4972
- this.root.remove();
4973
- }
4974
- renderTable(t) {
4975
- const b = this.paneBounds(t.paneId);
4976
- const wrap = document.createElement("div");
4977
- wrap.style.position = "absolute";
4978
- if (t.frameColor && t.frameWidth > 0) wrap.style.border = `${t.frameWidth}px solid ${t.frameColor}`;
4979
- this.anchor(wrap, t.position, b);
4980
- const table = document.createElement("table");
4981
- Object.assign(table.style, {
4982
- borderCollapse: "collapse",
4983
- background: t.bgColor ?? "transparent",
4984
- fontFamily: this.theme.fontFamily || "sans-serif",
4985
- border: "none",
4986
- tableLayout: "auto",
4987
- // Re-enable pointer events on the table only (root is none) so cell tooltips work.
4988
- pointerEvents: "auto"
4989
- });
4990
- const { span, omit } = mergeRenderPlan(t);
4991
- const plotW = Math.max(0, (this.root.clientWidth || this.container.clientWidth) - b.rightAxis);
4992
- const paneH = b.height;
4993
- const cellBorder = t.borderColor && t.borderWidth > 0 ? `${t.borderWidth}px solid ${t.borderColor}` : "none";
4994
- for (let r = 0; r < t.rows; r += 1) {
4995
- const tr = document.createElement("tr");
4996
- for (let c = 0; c < t.columns; c += 1) {
4997
- if (omit.has(`${r}:${c}`)) continue;
4998
- const cell = t.cells[r]?.[c] ?? null;
4999
- const td = document.createElement("td");
5000
- if (cell === null) {
5001
- Object.assign(td.style, { padding: "0", border: "none" });
5002
- tr.appendChild(td);
5003
- continue;
5004
- }
5005
- const sp = span.get(`${r}:${c}`);
5006
- if (sp) {
5007
- if (sp.cs > 1) td.colSpan = sp.cs;
5008
- if (sp.rs > 1) td.rowSpan = sp.rs;
5009
- }
5010
- Object.assign(td.style, {
5011
- border: cellBorder,
5012
- padding: "2px 6px",
5013
- background: cell.bgColor ?? "transparent",
5014
- color: cell.textColor ?? this.theme.textColor,
5015
- textAlign: cell.hAlign,
5016
- verticalAlign: cell.vAlign === "top" ? "top" : cell.vAlign === "bottom" ? "bottom" : "middle",
5017
- fontSize: `${fontPxOf(cell.textSize)}px`,
5018
- fontFamily: cell.fontFamily === "monospace" ? "monospace" : "inherit",
5019
- fontWeight: cell.bold ? "bold" : "normal",
5020
- fontStyle: cell.italic ? "italic" : "normal",
5021
- // Pine cell text never wraps; `\n` still breaks lines. Wrapping
5022
- // used to collapse unicode sparklines and ━━━ dividers.
5023
- whiteSpace: "pre"
5024
- });
5025
- if (cell.width) td.style.width = `${cell.width / 100 * plotW}px`;
5026
- if (cell.height) td.style.height = `${cell.height / 100 * paneH}px`;
5027
- if (cell.tooltip) td.title = cell.tooltip;
5028
- td.textContent = cell.text ?? "";
5029
- tr.appendChild(td);
5030
- }
5031
- table.appendChild(tr);
5032
- }
5033
- wrap.appendChild(table);
5034
- return wrap;
5035
- }
5036
- /**
5037
- * Position the wrapper at a Pine `position.*` corner/edge of the table's PANE
5038
- * (not the whole chart), inset past the right price axis so it never overlaps
5039
- * the Y-axis labels.
5040
- */
5041
- anchor(el, position, b) {
5042
- const m = 6;
5043
- const containerH = this.root.clientHeight || this.container.clientHeight;
5044
- const paneBottom = b.top + b.height;
5045
- if (position.startsWith("top")) el.style.top = `${b.top + m}px`;
5046
- else if (position.startsWith("bottom")) el.style.bottom = `${Math.max(0, containerH - paneBottom) + m}px`;
5047
- else el.style.top = `${b.top + b.height / 2}px`;
5048
- if (position.endsWith("left")) el.style.left = `${m}px`;
5049
- else if (position.endsWith("right")) el.style.right = `${b.rightAxis + m}px`;
5050
- else el.style.left = `calc(50% - ${b.rightAxis / 2}px)`;
5051
- const tx = position.endsWith("center") ? "-50%" : "0";
5052
- const ty = position.startsWith("middle") ? "-50%" : "0";
5053
- if (tx !== "0" || ty !== "0") el.style.transform = `translate(${tx}, ${ty})`;
5054
- }
5055
- };
5056
-
5057
5008
  // src/renderers/native/capabilities.ts
5058
5009
  var NATIVE_CAPABILITIES = {
5059
5010
  panes: true,
@@ -5074,7 +5025,7 @@ var NATIVE_CAPABILITIES = {
5074
5025
  drawingDepth: true,
5075
5026
  // drawings share the series' z space (backend-composited interleave layers)
5076
5027
  tables: true,
5077
- // reuses the DOM TableOverlay
5028
+ // canvas-painted into the owning indicator's interleave slice
5078
5029
  trades: true,
5079
5030
  // strategy order-fill markers (arrows + labels + fill-price ticks)
5080
5031
  inputsUI: true
@@ -5105,6 +5056,9 @@ function candleTier(spacing) {
5105
5056
  if (spacing < CANDLE_BODY_MIN_SPACING) return "wick";
5106
5057
  return "full";
5107
5058
  }
5059
+ function snapY(yCss, dpr) {
5060
+ return Math.round(yCss * dpr) / dpr;
5061
+ }
5108
5062
  function candleGeometry(xCss, spacing, dpr, bodyScale = 1) {
5109
5063
  const wickDev = Math.max(1, Math.round(wickWidth(spacing) * dpr));
5110
5064
  const wickLeftDev = Math.round(xCss * dpr - wickDev / 2);
@@ -5911,11 +5865,9 @@ var WebGL2Backend = class {
5911
5865
  };
5912
5866
  b.alpha = this.modelAlpha;
5913
5867
  for (const m of models) for (const bgSpan of m.backgrounds) if (bgSpan.overlay !== true) this.emitBackground(b, bgSpan, pane, coords);
5914
- for (const m of models) for (const f of m.fills) if (f.overlay !== true) this.emitFill(b, m, f, effPane(m), coords, i0, i1, scene.offsetOf(m.id));
5915
5868
  if (isPrice) {
5916
5869
  for (const m of scene.indicators.values()) {
5917
5870
  for (const bgSpan of m.backgrounds) if (bgSpan.overlay === true) this.emitBackground(b, bgSpan, pane, coords);
5918
- for (const f of m.fills) if (f.overlay === true) this.emitFill(b, m, f, pane, coords, i0, i1, scene.offsetOf(m.id));
5919
5871
  }
5920
5872
  }
5921
5873
  const drawCandles = isPrice && !scene.candlesHidden;
@@ -5931,6 +5883,7 @@ var WebGL2Backend = class {
5931
5883
  b.alpha = this.modelAlpha;
5932
5884
  const off = scene.offsetOf(m.id);
5933
5885
  const mp = effPane(m);
5886
+ for (const f of m.fills) if (f.overlay !== true) this.emitFill(b, m, f, mp, coords, i0, i1, off);
5934
5887
  for (const s of m.series) if (s.overlay !== true) this.emitSeries(b, s, mp, coords, i0, i1, theme, off);
5935
5888
  }
5936
5889
  if (drawCandles && !candleDrawn) {
@@ -5938,14 +5891,18 @@ var WebGL2Backend = class {
5938
5891
  b.alpha = this.candleStructureAlpha;
5939
5892
  this.emitPriceSeries(b, scene, i0, i1, coords, pane, theme, barColorMap, dataW);
5940
5893
  }
5941
- drawSlicesUpTo(Infinity);
5942
5894
  b.alpha = this.modelAlpha;
5943
5895
  if (isPrice) {
5896
+ for (const m of scene.indicators.values()) {
5897
+ const off = scene.offsetOf(m.id);
5898
+ for (const f of m.fills) if (f.overlay === true) this.emitFill(b, m, f, pane, coords, i0, i1, off);
5899
+ }
5944
5900
  for (const m of scene.indicators.values()) {
5945
5901
  const off = scene.offsetOf(m.id);
5946
5902
  for (const s of m.series) if (s.overlay === true) this.emitSeries(b, s, pane, coords, i0, i1, theme, off);
5947
5903
  }
5948
5904
  }
5905
+ drawSlicesUpTo(Infinity);
5949
5906
  for (const m of models) {
5950
5907
  const mp = effPane(m);
5951
5908
  for (const pl of m.priceLines) this.emitHline(b, pl, mp, coords, dataW, theme);
@@ -6340,12 +6297,12 @@ var WebGL2Backend = class {
6340
6297
  if (drawBody) {
6341
6298
  const oY = coords.priceToY(bar.open, pane.scale, pane.bounds);
6342
6299
  const cY = coords.priceToY(bar.close, pane.scale, pane.bounds);
6343
- bodyTop = Math.min(oY, cY);
6344
- bodyH = Math.max(1, Math.abs(cY - oY));
6300
+ bodyTop = snapY(Math.min(oY, cY), coords.dpr);
6301
+ bodyH = Math.max(1 / coords.dpr, snapY(Math.max(oY, cY), coords.dpr) - bodyTop);
6345
6302
  }
6346
6303
  if (cs.wickVisible) {
6347
- const hY = coords.priceToY(bar.high, pane.scale, pane.bounds);
6348
- const lY = coords.priceToY(bar.low, pane.scale, pane.bounds);
6304
+ const hY = snapY(coords.priceToY(bar.high, pane.scale, pane.bounds), coords.dpr);
6305
+ const lY = snapY(coords.priceToY(bar.low, pane.scale, pane.bounds), coords.dpr);
6349
6306
  b.alpha = this.candleStructureAlpha;
6350
6307
  const wCol = parseColor((isUp ? cs.wickUpColor : cs.wickDownColor) ?? (drawBody ? dir : bodyColorStr));
6351
6308
  if (drawBody) {
@@ -6902,6 +6859,7 @@ var DOUBLE_TAP_MS = 350;
6902
6859
  var DOUBLE_TAP_SLOP = 30;
6903
6860
  var TIME_SCALE_K = 4e-3;
6904
6861
  var WHEEL_ZOOM_K = 4e-3;
6862
+ var WHEEL_PRICE_DRAG_PX = 0.25;
6905
6863
  function wheelZoomAnchor(coords, cursorX, rightEdge) {
6906
6864
  if (rightEdge) return { logical: coords.rightEdgeLogical, x: coords.width };
6907
6865
  return { logical: coords.xToLogical(cursorX), x: cursorX };
@@ -7032,7 +6990,7 @@ var InputController = class {
7032
6990
  this.capture(e.pointerId);
7033
6991
  return;
7034
6992
  }
7035
- if (e.shiftKey && this.regionAt(x, y) === "data" && this.deps.drawingsMeasureStart?.(x, y)) {
6993
+ if (e.shiftKey && this.regionAt(x, y) === "data" && this.deps.drawingsMeasureStart?.(x, y, this.snapMode(e))) {
7036
6994
  this.region = "drawing";
7037
6995
  this.capture(e.pointerId);
7038
6996
  return;
@@ -7146,7 +7104,7 @@ var InputController = class {
7146
7104
  const wasTouch = e.pointerType === "touch";
7147
7105
  const tapRelease = this.dragging && !this.moved && (!wasTouch || Math.hypot(x - this.startX, y - this.startY) <= TOUCH_TAP_SLOP);
7148
7106
  if (this.dragging && this.region === "drawing") {
7149
- this.deps.drawingsPointerUp?.(x, y);
7107
+ this.deps.drawingsPointerUp?.(x, y, this.snapMode(e));
7150
7108
  } else if (tapRelease && this.region === "data") {
7151
7109
  this.deps.onClick(x, y);
7152
7110
  } else if (this.dragging && this.region === "data") {
@@ -7167,7 +7125,7 @@ var InputController = class {
7167
7125
  if (e.pointerType === "touch") this.touches.delete(e.pointerId);
7168
7126
  this.cancelLongPress();
7169
7127
  if (!this.dragging) return;
7170
- if (this.region === "drawing" && !Number.isNaN(this.cursorX)) this.deps.drawingsPointerUp?.(this.cursorX, this.cursorY);
7128
+ if (this.region === "drawing" && !Number.isNaN(this.cursorX)) this.deps.drawingsPointerUp?.(this.cursorX, this.cursorY, this.snapMode(e));
7171
7129
  if (this.region === "crosshair" || e.pointerType === "touch") this.deps.onPointerMove(null, null);
7172
7130
  this.endGesture(e);
7173
7131
  };
@@ -7183,6 +7141,12 @@ var InputController = class {
7183
7141
  this.onWheel = (e) => {
7184
7142
  e.preventDefault();
7185
7143
  this.deps.drawingsClearTransient?.();
7144
+ const { x, y } = this.local(e);
7145
+ if (this.regionAt(x, y) === "price" && e.deltaY !== 0) {
7146
+ this.deps.beginPriceScale(x, y);
7147
+ this.deps.priceScaleBy(e.deltaY * WHEEL_PRICE_DRAG_PX);
7148
+ return;
7149
+ }
7186
7150
  const coords = this.deps.getCoords();
7187
7151
  const vp = coords.getViewport();
7188
7152
  const pan = wheelPanDelta(e.deltaX, e.deltaY, e.shiftKey);
@@ -7190,9 +7154,8 @@ var InputController = class {
7190
7154
  this.deps.apply({ barSpacing: vp.barSpacing, rightOffset: wheelPanRightOffset(vp.rightOffset, pan, coords.pxPerBar()) });
7191
7155
  return;
7192
7156
  }
7193
- const cursorX = this.local(e).x;
7194
7157
  const rightEdge = this.rightEdgeZoom && !(e.ctrlKey || e.metaKey);
7195
- const anchor = wheelZoomAnchor(coords, cursorX, rightEdge);
7158
+ const anchor = wheelZoomAnchor(coords, x, rightEdge);
7196
7159
  const target = clampBarSpacing(vp.barSpacing * Math.exp(-e.deltaY * WHEEL_ZOOM_K));
7197
7160
  this.deps.zoomTo(target, anchor.logical, anchor.x);
7198
7161
  };
@@ -7678,9 +7641,10 @@ var SceneGraph = class {
7678
7641
  * so each indicator arrives behind the candles (and behind older indicators);
7679
7642
  * `setIndicatorZ`/`bringToFront`/`sendToBack` change it. */
7680
7643
  this.seriesZ = /* @__PURE__ */ new Map();
7681
- /** Per-pane raster layers of user drawings interleaved into the series stack each is a
7644
+ /** Per-pane raster layers of drawings interleaved into the series stack (each
7645
+ * indicator's Pine drawings at its model's z, plus in-stack user drawings) — each a
7682
7646
  * prepainted canvas the backend composites just before the series carrying `beforeZ`.
7683
- * Rebuilt by the renderer per data frame; empty when every drawing sits over the stack. */
7647
+ * Rebuilt by the renderer per data frame. */
7684
7648
  this.drawingSlices = /* @__PURE__ */ new Map();
7685
7649
  /** Per-model index offset: the chart bar index of the model's `anchorTime` — its
7686
7650
  * index-aligned payloads (dense series arrays, `bar_index` drawings) count from that
@@ -7915,11 +7879,9 @@ var Canvas2dBackend = class {
7915
7879
  };
7916
7880
  ctx.globalAlpha = this.modelAlpha;
7917
7881
  for (const m of models) for (const bg of m.backgrounds) if (bg.overlay !== true) this.drawBackground(ctx, bg, pane, coords);
7918
- for (const m of models) for (const f of m.fills) if (f.overlay !== true) this.drawFill(ctx, m, f, effPane(m), coords, i0, i1, scene.offsetOf(m.id));
7919
7882
  if (isPrice) {
7920
7883
  for (const m of scene.indicators.values()) {
7921
7884
  for (const bg of m.backgrounds) if (bg.overlay === true) this.drawBackground(ctx, bg, pane, coords);
7922
- for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0, i1, scene.offsetOf(m.id));
7923
7885
  }
7924
7886
  }
7925
7887
  const slices = scene.drawingSlices.get(pane.id) ?? [];
@@ -7943,6 +7905,7 @@ var Canvas2dBackend = class {
7943
7905
  ctx.globalAlpha = this.modelAlpha;
7944
7906
  const off = scene.offsetOf(m.id);
7945
7907
  const mp = effPane(m);
7908
+ for (const f of m.fills) if (f.overlay !== true) this.drawFill(ctx, m, f, mp, coords, i0, i1, off);
7946
7909
  for (const s of m.series) if (s.overlay !== true) this.drawSeries(ctx, s, mp, coords, i0, i1, theme, off);
7947
7910
  }
7948
7911
  if (drawCandles && !candleDrawn) {
@@ -7950,14 +7913,18 @@ var Canvas2dBackend = class {
7950
7913
  ctx.globalAlpha = this.candleStructureAlpha;
7951
7914
  this.drawPriceSeries(ctx, scene, i0, i1, coords, pane, theme, barColorMap, dataW);
7952
7915
  }
7953
- drawSlicesUpTo(Infinity);
7954
7916
  if (isPrice) {
7955
7917
  ctx.globalAlpha = this.modelAlpha;
7918
+ for (const m of scene.indicators.values()) {
7919
+ const off = scene.offsetOf(m.id);
7920
+ for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0, i1, off);
7921
+ }
7956
7922
  for (const m of scene.indicators.values()) {
7957
7923
  const off = scene.offsetOf(m.id);
7958
7924
  for (const s of m.series) if (s.overlay === true) this.drawSeries(ctx, s, pane, coords, i0, i1, theme, off);
7959
7925
  }
7960
7926
  }
7927
+ drawSlicesUpTo(Infinity);
7961
7928
  ctx.globalAlpha = this.modelAlpha;
7962
7929
  for (const m of models) {
7963
7930
  const mp = effPane(m);
@@ -8201,13 +8168,13 @@ var Canvas2dBackend = class {
8201
8168
  if (drawBody) {
8202
8169
  const oY = coords.priceToY(b.open, pane.scale, pane.bounds);
8203
8170
  const cY = coords.priceToY(b.close, pane.scale, pane.bounds);
8204
- top = Math.min(oY, cY);
8205
- bodyH = Math.max(1, Math.abs(cY - oY));
8171
+ top = snapY(Math.min(oY, cY), coords.dpr);
8172
+ bodyH = Math.max(1 / coords.dpr, snapY(Math.max(oY, cY), coords.dpr) - top);
8206
8173
  }
8207
8174
  if (cs.wickVisible) {
8208
8175
  const wick = (up ? cs.wickUpColor : cs.wickDownColor) ?? (drawBody ? dir : color);
8209
- const hY = coords.priceToY(b.high, pane.scale, pane.bounds);
8210
- const lY = coords.priceToY(b.low, pane.scale, pane.bounds);
8176
+ const hY = snapY(coords.priceToY(b.high, pane.scale, pane.bounds), coords.dpr);
8177
+ const lY = snapY(coords.priceToY(b.low, pane.scale, pane.bounds), coords.dpr);
8211
8178
  ctx.globalAlpha = this.candleStructureAlpha;
8212
8179
  ctx.strokeStyle = wick;
8213
8180
  ctx.lineWidth = g.wickW;
@@ -8654,9 +8621,31 @@ function autoFontSize(lines, boxW, boxH, bold) {
8654
8621
 
8655
8622
  // src/renderers/shared/DrawingSceneRenderer.ts
8656
8623
  var EMPTY_DRAWING_SET = { lines: [], boxes: [], labels: [], polylines: [], linefills: [] };
8624
+ function modelDrawingSet(m, overlay) {
8625
+ const want = (d) => Boolean(d.overlay) === overlay;
8626
+ return {
8627
+ lines: (m.lines ?? []).filter(want),
8628
+ boxes: (m.boxes ?? []).filter(want),
8629
+ labels: (m.labels ?? []).filter(want),
8630
+ polylines: (m.polylines ?? []).filter(want),
8631
+ linefills: (m.linefills ?? []).filter(want)
8632
+ };
8633
+ }
8634
+ function drawingSetEmpty(s) {
8635
+ return !s.lines.length && !s.boxes.length && !s.labels.length && !s.polylines.length && !s.linefills.length;
8636
+ }
8657
8637
  function fontSizePx(size) {
8658
8638
  return size === "auto" ? 12 : namedFontSize(size);
8659
8639
  }
8640
+ function lineCoversWindow(a, b, extend, lo, hi) {
8641
+ const minX = Math.min(a, b);
8642
+ const maxX = Math.max(a, b);
8643
+ if (a === b) return a >= lo && a <= hi;
8644
+ if (extend === "both") return true;
8645
+ if (extend === "left") return maxX >= lo;
8646
+ if (extend === "right") return minX <= hi;
8647
+ return maxX >= lo && minX <= hi;
8648
+ }
8660
8649
  var DrawingSceneRenderer = class {
8661
8650
  constructor(deps, set = EMPTY_DRAWING_SET) {
8662
8651
  this.deps = deps;
@@ -8721,7 +8710,7 @@ var DrawingSceneRenderer = class {
8721
8710
  };
8722
8711
  for (const ln of this.set.lines) {
8723
8712
  if (ln.invisible) continue;
8724
- if (!visible(this.logicalOf(ln.xloc, ln.x1), this.logicalOf(ln.xloc, ln.x2), ln.extend)) continue;
8713
+ if (!lineCoversWindow(this.logicalOf(ln.xloc, ln.x1), this.logicalOf(ln.xloc, ln.x2), ln.extend, lo, hi)) continue;
8725
8714
  fold(ln.y1);
8726
8715
  fold(ln.y2);
8727
8716
  }
@@ -9506,10 +9495,8 @@ var ChromeRenderer = class {
9506
9495
  this.ctx = null;
9507
9496
  // The color for axis tick labels — the host-passed surface text, set each frame in render().
9508
9497
  this.axisTextColor = DARK_THEME.textColor;
9509
- // Shared Pine-drawing renderer (line/box/label/polyline/linefill); widthCache persists.
9498
+ // Shared Pine-drawing renderer, used here for autoscale geometry only; widthCache persists.
9510
9499
  this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
9511
- // Tooltip hit-rects of every label drawn this frame, in plot coords (rebuilt per render).
9512
- this.labelTips = [];
9513
9500
  }
9514
9501
  mount(canvas) {
9515
9502
  this.canvas = canvas;
@@ -9533,8 +9520,8 @@ var ChromeRenderer = class {
9533
9520
  */
9534
9521
  paneDrawingsRange(ownModels, scene, isPricePane, vr) {
9535
9522
  let dr = null;
9536
- for (const m of ownModels) dr = unionRange(dr, this.drawingsRange(this.ownDrawings(m), vr, scene.offsetOf(m.id)));
9537
- if (isPricePane) for (const m of scene.indicators.values()) dr = unionRange(dr, this.drawingsRange(this.overlayDrawings(m), vr, scene.offsetOf(m.id)));
9523
+ for (const m of ownModels) dr = unionRange(dr, this.drawingsRange(modelDrawingSet(m, false), vr, scene.offsetOf(m.id)));
9524
+ if (isPricePane) for (const m of scene.indicators.values()) dr = unionRange(dr, this.drawingsRange(modelDrawingSet(m, true), vr, scene.offsetOf(m.id)));
9538
9525
  return dr;
9539
9526
  }
9540
9527
  /** Clear the chrome canvas and draw drawings + axes + current-price line.
@@ -9551,7 +9538,6 @@ var ChromeRenderer = class {
9551
9538
  const dataW = coords.width;
9552
9539
  const dataH = coords.height;
9553
9540
  this.axisTextColor = surface?.textColor ?? theme.textColor;
9554
- this.labelTips = [];
9555
9541
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
9556
9542
  ctx.clearRect(0, 0, fullW, fullH);
9557
9543
  if (surface && (fullW > dataW || fullH > dataH)) {
@@ -9567,17 +9553,6 @@ var ChromeRenderer = class {
9567
9553
  return;
9568
9554
  }
9569
9555
  const pricePane = panes.find((p) => p.kind === "price") ?? null;
9570
- for (const pane of panes) {
9571
- if (pane.collapsed) continue;
9572
- for (const m of scene.indicatorsForPane(pane.id)) {
9573
- const sc = scene.scaleFor(m, pane);
9574
- const mp = sc === pane.scale ? pane : { ...pane, scale: sc };
9575
- this.renderDrawings(ctx, coords, this.ownDrawings(m), mp, dataW, scene.offsetOf(m.id));
9576
- }
9577
- }
9578
- if (pricePane) {
9579
- for (const m of scene.indicators.values()) this.renderDrawings(ctx, coords, this.overlayDrawings(m), pricePane, dataW, scene.offsetOf(m.id));
9580
- }
9581
9556
  if (pricePane && !pricePane.collapsed && scene.tradeMarkers.visible) {
9582
9557
  for (const m of scene.indicators.values()) {
9583
9558
  if (m.trades?.length) this.renderTrades(ctx, coords, scene, theme, m.trades, pricePane, dataW);
@@ -9593,25 +9568,6 @@ var ChromeRenderer = class {
9593
9568
  this.canvas = null;
9594
9569
  this.ctx = null;
9595
9570
  }
9596
- // ── Pine-drawing helpers (own vs force_overlay routing) ──
9597
- ownDrawings(m) {
9598
- return {
9599
- lines: (m.lines ?? []).filter((d) => !d.overlay),
9600
- boxes: (m.boxes ?? []).filter((d) => !d.overlay),
9601
- labels: (m.labels ?? []).filter((d) => !d.overlay),
9602
- polylines: (m.polylines ?? []).filter((d) => !d.overlay),
9603
- linefills: (m.linefills ?? []).filter((d) => !d.overlay)
9604
- };
9605
- }
9606
- overlayDrawings(m) {
9607
- return {
9608
- lines: (m.lines ?? []).filter((d) => d.overlay),
9609
- boxes: (m.boxes ?? []).filter((d) => d.overlay),
9610
- labels: (m.labels ?? []).filter((d) => d.overlay),
9611
- polylines: (m.polylines ?? []).filter((d) => d.overlay),
9612
- linefills: (m.linefills ?? []).filter((d) => d.overlay)
9613
- };
9614
- }
9615
9571
  drawingsRange(set, vr, indexOffset = 0) {
9616
9572
  this.drawScene.setSet(set, indexOffset);
9617
9573
  if (this.drawScene.isEmpty()) return null;
@@ -9645,34 +9601,6 @@ var ChromeRenderer = class {
9645
9601
  );
9646
9602
  ctx.restore();
9647
9603
  }
9648
- renderDrawings(ctx, coords, set, pane, dataW, indexOffset = 0) {
9649
- this.drawScene.setSet(set, indexOffset);
9650
- if (this.drawScene.isEmpty()) return;
9651
- ctx.save();
9652
- ctx.translate(0, pane.bounds.top);
9653
- ctx.beginPath();
9654
- ctx.rect(0, 0, dataW, pane.bounds.height);
9655
- ctx.clip();
9656
- this.drawScene.render(
9657
- ctx,
9658
- dataW,
9659
- pane.bounds.height,
9660
- (l) => coords.logicalToX(l),
9661
- (price) => coords.priceToY(price, pane.scale, pane.bounds) - pane.bounds.top
9662
- );
9663
- ctx.restore();
9664
- for (const r of this.drawScene.labelTipRegions()) {
9665
- this.labelTips.push({ ...r, top: r.top + pane.bounds.top, bottom: r.bottom + pane.bounds.top });
9666
- }
9667
- }
9668
- /** Tooltip of the topmost label under a plot-space point, or null. Fed by the last render. */
9669
- labelTooltipAt(x, y) {
9670
- for (let i = this.labelTips.length - 1; i >= 0; i -= 1) {
9671
- const r = this.labelTips[i];
9672
- if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return r.text;
9673
- }
9674
- return null;
9675
- }
9676
9604
  // ── axes ──
9677
9605
  drawPriceAxes(ctx, scene, coords, theme, dataW, panes) {
9678
9606
  ctx.strokeStyle = scene.style.borderColor ?? theme.borderColor;
@@ -12632,6 +12560,22 @@ var DrawingInteraction = class {
12632
12560
  this.snapAt = changed ? { point: snapped, paneId } : null;
12633
12561
  return snapped;
12634
12562
  }
12563
+ /**
12564
+ * Resolve a cursor pixel through the magnet and return the snapped pixel — the same
12565
+ * conversion drawing placement uses. Updates the snap-ring marker. The measure
12566
+ * ruler goes through this so its endpoints follow weak/strong/Ctrl magnet too.
12567
+ */
12568
+ snapCursor(x, y, mode) {
12569
+ const proj = this.deps.projector();
12570
+ const paneId = proj.paneIdAtY(y) ?? "price";
12571
+ const point = this.resolve(x, y, paneId, mode);
12572
+ const sy = proj.yOf(point.price, paneId);
12573
+ return { x: proj.xOf(point.time), y: sy ?? y };
12574
+ }
12575
+ /** Drop the snap-ring marker (a transient mode ended without going through `up`). */
12576
+ clearSnapMarker() {
12577
+ this.snapAt = null;
12578
+ }
12635
12579
  /** Resolve a pixel to a data point with the segment angle locked to 45° steps around
12636
12580
  * `pivot` (Shift held on a line tool). Works in PIXEL space — the user reasons about
12637
12581
  * the angle they see, not about time/price units. The magnet is bypassed: snapping
@@ -13991,17 +13935,17 @@ function glyphIcon(glyph) {
13991
13935
  return textGlyph(String(glyph), 15);
13992
13936
  }
13993
13937
  function stampSizeIcon(size) {
13994
- return textGlyph("\u25CF", (SIZE_PX2[String(size)] ?? 13) + 4);
13938
+ return textGlyph("\u25CF", (SIZE_PX[String(size)] ?? 13) + 4);
13995
13939
  }
13996
13940
  function sizeIcon(size) {
13997
13941
  return textGlyph(String(size).charAt(0).toUpperCase(), 15);
13998
13942
  }
13999
- var SIZE_PX2 = { small: 10, normal: 13, large: 16, huge: 20 };
13943
+ var SIZE_PX = { small: 10, normal: 13, large: 16, huge: 20 };
14000
13944
  function numbersSizeIcon(size) {
14001
- return textGlyph("12", (SIZE_PX2[String(size)] ?? 13) - 1, 16.5, 'font-weight="600"');
13945
+ return textGlyph("12", (SIZE_PX[String(size)] ?? 13) - 1, 16.5, 'font-weight="600"');
14002
13946
  }
14003
13947
  function labelSizeIcon(size) {
14004
- return textGlyph("T", (SIZE_PX2[String(size)] ?? 13) + 2);
13948
+ return textGlyph("T", (SIZE_PX[String(size)] ?? 13) + 2);
14005
13949
  }
14006
13950
  function capitalize(s) {
14007
13951
  return s.charAt(0).toUpperCase() + s.slice(1);
@@ -14037,30 +13981,39 @@ var MeasureOverlay = class {
14037
13981
  isFinished() {
14038
13982
  return this.state === "finished";
14039
13983
  }
14040
- /** A press: begin the measurement, or finish it on the second click. */
14041
- down(x, y) {
13984
+ /** A press: begin the measurement, or finish it on the second click.
13985
+ * `x,y` are the raw cursor (drag-slop vs click-move-click). `gx,gy` are the
13986
+ * graphic endpoints — magnet-snapped when the magnet is on, else the same as `x,y`. */
13987
+ down(x, y, gx = x, gy = y) {
14042
13988
  if (this.state === "measuring") {
14043
- this.end = { x, y };
13989
+ this.end = { x: gx, y: gy };
14044
13990
  this.state = "finished";
14045
13991
  return;
14046
13992
  }
14047
- this.start = { x, y };
14048
- this.end = { x, y };
13993
+ this.start = { x: gx, y: gy };
13994
+ this.end = { x: gx, y: gy };
14049
13995
  this.pressX = x;
14050
13996
  this.pressY = y;
14051
13997
  this.state = "measuring";
14052
13998
  }
13999
+ /** Size the in-progress ruler. `x,y` are the graphic (magnet-snapped) cursor. */
14053
14000
  move(x, y) {
14054
14001
  if (this.state === "measuring") this.end = { x, y };
14055
14002
  }
14056
14003
  /** A release: finish if the press was actually dragged (press-drag-release), else wait
14057
- * for the second click (click-move-click). */
14058
- up(x, y) {
14004
+ * for the second click (click-move-click). `x,y` are the raw cursor (slop); `gx,gy`
14005
+ * are the graphic end (magnet-snapped when the magnet is on). */
14006
+ up(x, y, gx = x, gy = y) {
14059
14007
  if (this.state === "measuring" && Math.hypot(x - this.pressX, y - this.pressY) > DRAG_SLOP3) {
14060
- this.end = { x, y };
14008
+ this.end = { x: gx, y: gy };
14061
14009
  this.state = "finished";
14062
14010
  }
14063
14011
  }
14012
+ /** Current graphic endpoints in media pixels, or null when idle. */
14013
+ points() {
14014
+ if (!this.start || !this.end) return null;
14015
+ return { start: this.start, end: this.end };
14016
+ }
14064
14017
  clear() {
14065
14018
  this.state = "idle";
14066
14019
  this.start = null;
@@ -14437,11 +14390,13 @@ var UserDrawingController = class {
14437
14390
  }
14438
14391
  /** Shift+press on the empty plot: arm the measure ruler AND start it at (x, y) in one
14439
14392
  * gesture — the equivalent of clicking the toolbar's Measure button, then pressing.
14440
- * Returns false when a mode/tool is already active (the normal press path owns it). */
14441
- beginMeasureAt(x, y) {
14393
+ * `snap` is the effective magnet (sticky mode, or Ctrl/Cmd-forced strong). Returns
14394
+ * false when a mode/tool is already active (the normal press path owns it). */
14395
+ beginMeasureAt(x, y, snap = "off") {
14442
14396
  if (this.measureMode || this.eraserMode || this.activeTool != null) return false;
14443
14397
  this.withModeIntent(() => this.toggleMeasure());
14444
- this.measure.down(x, y);
14398
+ const g = this.interaction.snapCursor(x, y, snap);
14399
+ this.measure.down(x, y, g.x, g.y);
14445
14400
  this.render();
14446
14401
  return true;
14447
14402
  }
@@ -14463,7 +14418,8 @@ var UserDrawingController = class {
14463
14418
  return;
14464
14419
  }
14465
14420
  if (this.measureMode) {
14466
- this.measure.down(x, y);
14421
+ const g = this.interaction.snapCursor(x, y, snap);
14422
+ this.measure.down(x, y, g.x, g.y);
14467
14423
  if (this.measure.isFinished()) this.withModeIntent(() => this.exitMeasure(false));
14468
14424
  this.render();
14469
14425
  return;
@@ -14476,7 +14432,8 @@ var UserDrawingController = class {
14476
14432
  return;
14477
14433
  }
14478
14434
  if (this.measureMode) {
14479
- this.measure.move(x, y);
14435
+ const g = this.interaction.snapCursor(x, y, snap);
14436
+ this.measure.move(g.x, g.y);
14480
14437
  this.render();
14481
14438
  return;
14482
14439
  }
@@ -14494,13 +14451,14 @@ var UserDrawingController = class {
14494
14451
  this.render();
14495
14452
  }
14496
14453
  }
14497
- pointerUp(x, y) {
14454
+ pointerUp(x, y, snap = "off") {
14498
14455
  if (this.eraserMode) {
14499
14456
  this.erasing = false;
14500
14457
  return;
14501
14458
  }
14502
14459
  if (this.measureMode) {
14503
- this.measure.up(x, y);
14460
+ const g = this.interaction.snapCursor(x, y, snap);
14461
+ this.measure.up(x, y, g.x, g.y);
14504
14462
  if (this.measure.isFinished()) this.withModeIntent(() => this.exitMeasure(false));
14505
14463
  this.render();
14506
14464
  return;
@@ -14545,6 +14503,7 @@ var UserDrawingController = class {
14545
14503
  /** Leave ruler mode. `clearGraphic` keeps a just-finished measurement on screen (false). */
14546
14504
  exitMeasure(clearGraphic = true) {
14547
14505
  this.measureMode = false;
14506
+ this.interaction.clearSnapMarker();
14548
14507
  if (clearGraphic) this.measure.clear();
14549
14508
  this.toolbar.setMeasureActive(false);
14550
14509
  this.render();
@@ -14685,15 +14644,28 @@ var UserDrawingController = class {
14685
14644
  if (this.eraserMode) return "pointer";
14686
14645
  return this.interaction.cursorAt(x, y);
14687
14646
  }
14688
- /** Right-click while placing: cancel the in-progress drawing and revert to the
14689
- * pointer the gesture is an explicit escape, so it disarms even in
14690
- * stay-in-drawing-mode (where Escape would leave the tool armed). Returns whether
14691
- * the press was consumed; false lets the host's context menu open normally. */
14647
+ /** Right-click: an explicit escape back to the pointer. Cancels an in-progress
14648
+ * placement or measurement, and also plain-disarms an armed-but-idle drawing
14649
+ * tool or the eraser — so a right-click ALWAYS reverts to the pointer, even in
14650
+ * stay-in-drawing-mode (where Escape would leave a drawing tool armed).
14651
+ * Persistent toggles (magnet, stay-mode, favorites) are untouched. Returns
14652
+ * whether the press was consumed; false lets the host's context menu open
14653
+ * normally. */
14692
14654
  cancelPlacement() {
14693
- if (!this.interaction.isPlacing()) return false;
14694
- this.interaction.cancel();
14695
- if (this.activeTool != null) this.emit({ kind: "arm", type: null });
14696
- return true;
14655
+ if (this.measureMode || this.eraserMode) {
14656
+ this.withModeIntent(() => this.measureMode ? this.exitMeasure() : this.exitEraser());
14657
+ return true;
14658
+ }
14659
+ if (this.interaction.isPlacing()) {
14660
+ this.interaction.cancel();
14661
+ if (this.activeTool != null) this.emit({ kind: "arm", type: null });
14662
+ return true;
14663
+ }
14664
+ if (this.activeTool != null) {
14665
+ this.emit({ kind: "arm", type: null });
14666
+ return true;
14667
+ }
14668
+ return false;
14697
14669
  }
14698
14670
  /** Double-click over a drawing → suppress the chart's view reset (single-click already
14699
14671
  * opens settings). Returns true only when a drawing is under the cursor. */
@@ -14719,6 +14691,10 @@ var UserDrawingController = class {
14719
14691
  return true;
14720
14692
  }
14721
14693
  if (this.interaction.cancel()) return true;
14694
+ if (this.measureMode) {
14695
+ this.withModeIntent(() => this.exitMeasure());
14696
+ return true;
14697
+ }
14722
14698
  if (this.selectedIds.size) {
14723
14699
  this.clearSelection();
14724
14700
  return true;
@@ -14976,6 +14952,315 @@ var UserDrawingController = class {
14976
14952
  }
14977
14953
  };
14978
14954
 
14955
+ // src/renderers/shared/TableOverlay.ts
14956
+ var SIZE_PX2 = {
14957
+ auto: 13,
14958
+ tiny: 10,
14959
+ small: 11,
14960
+ normal: 13,
14961
+ large: 16,
14962
+ huge: 20
14963
+ };
14964
+ function fontPxOf(size) {
14965
+ if (typeof size === "number") return size > 0 ? size : SIZE_PX2.auto;
14966
+ return SIZE_PX2[size] ?? SIZE_PX2.auto;
14967
+ }
14968
+ function tableHasContent(t) {
14969
+ return t.cells.some((row) => row?.some((c) => c != null && !c.merged));
14970
+ }
14971
+ function mergeRenderPlan(t) {
14972
+ const span = /* @__PURE__ */ new Map();
14973
+ const omit = /* @__PURE__ */ new Set();
14974
+ for (const m of t.merges) {
14975
+ span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
14976
+ for (let r = m.startRow; r <= m.endRow; r += 1) {
14977
+ for (let c = m.startCol; c <= m.endCol; c += 1) {
14978
+ if (r !== m.startRow || c !== m.startCol) omit.add(`${r}:${c}`);
14979
+ }
14980
+ }
14981
+ }
14982
+ for (let r = 0; r < t.rows; r += 1) {
14983
+ for (let c = 0; c < t.columns; c += 1) {
14984
+ if (t.cells[r]?.[c]?.merged && !span.has(`${r}:${c}`)) omit.add(`${r}:${c}`);
14985
+ }
14986
+ }
14987
+ for (const key of span.keys()) omit.delete(key);
14988
+ return { span, omit };
14989
+ }
14990
+
14991
+ // src/renderers/shared/TableCanvasRenderer.ts
14992
+ var PAD_X = 6;
14993
+ var PAD_Y = 2;
14994
+ var MARGIN = 6;
14995
+ var LINE_HEIGHT = 1.2;
14996
+ function paintTable(ctx, t, args, tips) {
14997
+ if (!tableHasContent(t)) return;
14998
+ const layout = layoutTable(ctx, t, args);
14999
+ if (!layout || layout.w <= 0 || layout.h <= 0) return;
15000
+ const fw = t.frameColor && t.frameWidth > 0 ? t.frameWidth : 0;
15001
+ const { x, y } = anchorOrigin(t.position, layout.w + 2 * fw, layout.h + 2 * fw, args);
15002
+ const x0 = x + fw;
15003
+ const y0 = y + fw;
15004
+ if (t.bgColor) {
15005
+ ctx.fillStyle = t.bgColor;
15006
+ ctx.fillRect(x0, y0, layout.w, layout.h);
15007
+ }
15008
+ if (fw > 0 && t.frameColor) {
15009
+ ctx.strokeStyle = t.frameColor;
15010
+ ctx.lineWidth = fw;
15011
+ ctx.strokeRect(x + fw / 2, y + fw / 2, layout.w + fw, layout.h + fw);
15012
+ }
15013
+ const colX = [0];
15014
+ for (const w of layout.colW) colX.push(colX[colX.length - 1] + w);
15015
+ const rowY = [0];
15016
+ for (const h of layout.rowH) rowY.push(rowY[rowY.length - 1] + h);
15017
+ const prevBaseline = ctx.textBaseline;
15018
+ const prevAlign = ctx.textAlign;
15019
+ ctx.textBaseline = "middle";
15020
+ for (const box of layout.boxes) {
15021
+ const rx = x0 + colX[box.c];
15022
+ const ry = y0 + rowY[box.r];
15023
+ const rw = colX[box.c + box.cs] - colX[box.c];
15024
+ const rh = rowY[box.r + box.rs] - rowY[box.r];
15025
+ const cell = box.cell;
15026
+ if (cell.bgColor) {
15027
+ ctx.fillStyle = cell.bgColor;
15028
+ ctx.fillRect(rx, ry, rw, rh);
15029
+ }
15030
+ const text = cell.text ?? "";
15031
+ if (text.length > 0) {
15032
+ const px = fontPxOf(cell.textSize);
15033
+ ctx.font = cellFont(cell, px, args.theme);
15034
+ ctx.fillStyle = cell.textColor ?? args.theme.textColor;
15035
+ const lines = text.split("\n");
15036
+ const blockH = lines.length * px * LINE_HEIGHT;
15037
+ const blockTop = cell.vAlign === "top" ? ry + PAD_Y : cell.vAlign === "bottom" ? ry + rh - PAD_Y - blockH : ry + (rh - blockH) / 2;
15038
+ const tx = cell.hAlign === "left" ? rx + PAD_X : cell.hAlign === "right" ? rx + rw - PAD_X : rx + rw / 2;
15039
+ ctx.textAlign = cell.hAlign;
15040
+ lines.forEach((line, i) => ctx.fillText(line, tx, blockTop + (i + 0.5) * px * LINE_HEIGHT));
15041
+ }
15042
+ if (cell.tooltip) tips.push({ left: rx, top: ry, right: rx + rw, bottom: ry + rh, text: cell.tooltip });
15043
+ }
15044
+ ctx.textBaseline = prevBaseline;
15045
+ ctx.textAlign = prevAlign;
15046
+ if (t.borderColor && t.borderWidth > 0) {
15047
+ ctx.strokeStyle = t.borderColor;
15048
+ ctx.lineWidth = t.borderWidth;
15049
+ const seen = /* @__PURE__ */ new Set();
15050
+ ctx.beginPath();
15051
+ const edge = (ax, ay, bx, by) => {
15052
+ const key = `${ax},${ay},${bx},${by}`;
15053
+ if (seen.has(key)) return;
15054
+ seen.add(key);
15055
+ ctx.moveTo(ax, ay);
15056
+ ctx.lineTo(bx, by);
15057
+ };
15058
+ for (const box of layout.boxes) {
15059
+ const l = Math.round(x0 + colX[box.c]);
15060
+ const r = Math.round(x0 + colX[box.c + box.cs]);
15061
+ const tp = Math.round(y0 + rowY[box.r]);
15062
+ const bt = Math.round(y0 + rowY[box.r + box.rs]);
15063
+ edge(l, tp, r, tp);
15064
+ edge(l, bt, r, bt);
15065
+ edge(l, tp, l, bt);
15066
+ edge(r, tp, r, bt);
15067
+ }
15068
+ ctx.stroke();
15069
+ }
15070
+ }
15071
+ function layoutTable(ctx, t, args) {
15072
+ const { span, omit } = mergeRenderPlan(t);
15073
+ const colW = new Array(t.columns).fill(0);
15074
+ const rowH = new Array(t.rows).fill(0);
15075
+ const boxes = [];
15076
+ for (let r = 0; r < t.rows; r += 1) {
15077
+ for (let c = 0; c < t.columns; c += 1) {
15078
+ if (omit.has(`${r}:${c}`)) continue;
15079
+ const cell = t.cells[r]?.[c];
15080
+ if (cell == null) continue;
15081
+ const sp = span.get(`${r}:${c}`);
15082
+ boxes.push({ cell, r, c, cs: Math.min(sp?.cs ?? 1, t.columns - c), rs: Math.min(sp?.rs ?? 1, t.rows - r) });
15083
+ }
15084
+ }
15085
+ if (boxes.length === 0) return null;
15086
+ const sizeOf = (cell) => {
15087
+ const px = fontPxOf(cell.textSize);
15088
+ ctx.font = cellFont(cell, px, args.theme);
15089
+ const lines = (cell.text ?? "").split("\n");
15090
+ let maxW = 0;
15091
+ for (const line of lines) maxW = Math.max(maxW, ctx.measureText(line).width);
15092
+ let w2 = Math.ceil(maxW) + 2 * PAD_X;
15093
+ let h2 = Math.ceil(lines.length * px * LINE_HEIGHT) + 2 * PAD_Y;
15094
+ if (cell.width) w2 = Math.max(w2, cell.width / 100 * args.plotWidth);
15095
+ if (cell.height) h2 = Math.max(h2, cell.height / 100 * args.paneHeight);
15096
+ return { w: w2, h: h2 };
15097
+ };
15098
+ const spanning = [];
15099
+ for (const box of boxes) {
15100
+ const { w: w2, h: h2 } = sizeOf(box.cell);
15101
+ if (box.cs === 1) colW[box.c] = Math.max(colW[box.c], w2);
15102
+ if (box.rs === 1) rowH[box.r] = Math.max(rowH[box.r], h2);
15103
+ if (box.cs > 1 || box.rs > 1) spanning.push({ box, w: w2, h: h2 });
15104
+ }
15105
+ for (const { box, w: w2, h: h2 } of spanning) {
15106
+ if (box.cs > 1) {
15107
+ let sum = 0;
15108
+ for (let c = box.c; c < box.c + box.cs; c += 1) sum += colW[c];
15109
+ if (w2 > sum) for (let c = box.c; c < box.c + box.cs; c += 1) colW[c] += (w2 - sum) / box.cs;
15110
+ }
15111
+ if (box.rs > 1) {
15112
+ let sum = 0;
15113
+ for (let r = box.r; r < box.r + box.rs; r += 1) sum += rowH[r];
15114
+ if (h2 > sum) for (let r = box.r; r < box.r + box.rs; r += 1) rowH[r] += (h2 - sum) / box.rs;
15115
+ }
15116
+ }
15117
+ let w = 0;
15118
+ for (const cw of colW) w += cw;
15119
+ let h = 0;
15120
+ for (const rh of rowH) h += rh;
15121
+ return { colW, rowH, w, h, boxes };
15122
+ }
15123
+ function anchorOrigin(position, totalW, totalH, args) {
15124
+ let y;
15125
+ if (position.startsWith("top")) y = MARGIN;
15126
+ else if (position.startsWith("bottom")) y = args.paneHeight - MARGIN - totalH;
15127
+ else y = args.paneHeight / 2 - totalH / 2;
15128
+ let x;
15129
+ if (position.endsWith("left")) x = MARGIN;
15130
+ else if (position.endsWith("right")) x = args.plotWidth - MARGIN - totalW;
15131
+ else x = args.plotWidth / 2 - totalW / 2;
15132
+ return { x, y };
15133
+ }
15134
+ function cellFont(cell, px, theme) {
15135
+ const family = cell.fontFamily === "monospace" ? "monospace" : theme.fontFamily || "sans-serif";
15136
+ return `${cell.italic ? "italic " : ""}${cell.bold ? "bold " : ""}${px}px ${family}`;
15137
+ }
15138
+
15139
+ // src/renderers/native/drawings/IndicatorDrawingSlices.ts
15140
+ function indicatorSliceKey(z, boundaries) {
15141
+ return boundaries.find((b) => b > z) ?? Infinity;
15142
+ }
15143
+ var IndicatorDrawingSlices = class {
15144
+ constructor() {
15145
+ this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
15146
+ /** Slice canvas cache, keyed `paneId|beforeZ` — same lifecycle as the user-drawing cache. */
15147
+ this.sliceCache = /* @__PURE__ */ new Map();
15148
+ /** Tooltip hit-rects of every label drawn this frame, in plot coords (rebuilt per prepare). */
15149
+ this.tips = [];
15150
+ }
15151
+ /**
15152
+ * Rebuild the per-indicator drawing slices for this data frame. `ref` is the data
15153
+ * canvas the slices must match pixel-for-pixel (the backend composites them 1:1).
15154
+ * Runs from the renderer's data paint, just before the backend composites the scene.
15155
+ */
15156
+ prepare(scene, coords, theme, ref) {
15157
+ this.tips = [];
15158
+ const out = /* @__PURE__ */ new Map();
15159
+ if (ref.width === 0 || ref.height === 0) {
15160
+ this.sliceCache.clear();
15161
+ return out;
15162
+ }
15163
+ this.drawScene.setDeps({
15164
+ timeToLogical: (ms) => coords.timeToLogical(ms),
15165
+ barAt: (logical) => {
15166
+ const b = scene.bars[Math.round(logical)];
15167
+ return b ? { high: b.high, low: b.low } : null;
15168
+ },
15169
+ theme
15170
+ });
15171
+ const dpr = coords.dpr;
15172
+ const dataW = coords.width;
15173
+ const buckets = /* @__PURE__ */ new Map();
15174
+ const add = (paneId, beforeZ, entry) => {
15175
+ const key = `${paneId}|${beforeZ}`;
15176
+ const bucket = buckets.get(key);
15177
+ if (bucket) bucket.entries.push(entry);
15178
+ else buckets.set(key, { paneId, beforeZ, entries: [entry] });
15179
+ };
15180
+ for (const pane of scene.orderedPanes()) {
15181
+ if (pane.collapsed) continue;
15182
+ const boundaries = scene.seriesBoundaries(pane.id);
15183
+ for (const m of scene.orderedIndicatorsForPane(pane.id)) {
15184
+ const set = modelDrawingSet(m, false);
15185
+ const tables = (m.tables ?? []).filter((t) => !t.overlay);
15186
+ if (drawingSetEmpty(set) && tables.length === 0) continue;
15187
+ const sc = scene.scaleFor(m, pane);
15188
+ const mp = sc === pane.scale ? pane : { ...pane, scale: sc };
15189
+ const beforeZ = indicatorSliceKey(scene.zOf(m.id), boundaries);
15190
+ add(pane.id, beforeZ, { set, tables, pane: mp, indexOffset: scene.offsetOf(m.id) });
15191
+ }
15192
+ if (pane.kind === "price") {
15193
+ for (const m of scene.indicators.values()) {
15194
+ const set = modelDrawingSet(m, true);
15195
+ const tables = (m.tables ?? []).filter((t) => t.overlay === true);
15196
+ if (drawingSetEmpty(set) && tables.length === 0) continue;
15197
+ add(pane.id, Infinity, { set, tables, pane, indexOffset: scene.offsetOf(m.id) });
15198
+ }
15199
+ }
15200
+ }
15201
+ for (const [key, { paneId, beforeZ, entries }] of buckets) {
15202
+ let canvas = this.sliceCache.get(key);
15203
+ if (!canvas) {
15204
+ canvas = document.createElement("canvas");
15205
+ this.sliceCache.set(key, canvas);
15206
+ }
15207
+ if (canvas.width !== ref.width || canvas.height !== ref.height) {
15208
+ canvas.width = ref.width;
15209
+ canvas.height = ref.height;
15210
+ }
15211
+ const ctx = canvas.getContext("2d");
15212
+ if (!ctx) continue;
15213
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
15214
+ ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
15215
+ for (const e of entries) this.paintEntry(ctx, e, coords, dataW, theme);
15216
+ const slices = out.get(paneId) ?? [];
15217
+ slices.push({ beforeZ, canvas });
15218
+ out.set(paneId, slices);
15219
+ }
15220
+ for (const key of [...this.sliceCache.keys()]) if (!buckets.has(key)) this.sliceCache.delete(key);
15221
+ for (const slices of out.values()) slices.sort((a, b) => a.beforeZ - b.beforeZ);
15222
+ return out;
15223
+ }
15224
+ paintEntry(ctx, e, coords, dataW, theme) {
15225
+ const { pane } = e;
15226
+ const paneTips = [];
15227
+ ctx.save();
15228
+ ctx.translate(0, pane.bounds.top);
15229
+ ctx.beginPath();
15230
+ ctx.rect(0, 0, dataW, pane.bounds.height);
15231
+ ctx.clip();
15232
+ this.drawScene.setSet(e.set, e.indexOffset);
15233
+ this.drawScene.render(
15234
+ ctx,
15235
+ dataW,
15236
+ pane.bounds.height,
15237
+ (l) => coords.logicalToX(l),
15238
+ (price) => coords.priceToY(price, pane.scale, pane.bounds) - pane.bounds.top
15239
+ );
15240
+ paneTips.push(...this.drawScene.labelTipRegions());
15241
+ for (const t of e.tables) paintTable(ctx, t, { paneHeight: pane.bounds.height, plotWidth: dataW, theme }, paneTips);
15242
+ ctx.restore();
15243
+ for (const r of paneTips) {
15244
+ this.tips.push({ ...r, top: r.top + pane.bounds.top, bottom: r.bottom + pane.bounds.top });
15245
+ }
15246
+ }
15247
+ /** Tooltip of the topmost label or table cell under a plot-space point, or null. Fed by the last prepare. */
15248
+ labelTooltipAt(x, y) {
15249
+ for (let i = this.tips.length - 1; i >= 0; i -= 1) {
15250
+ const r = this.tips[i];
15251
+ if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return r.text;
15252
+ }
15253
+ return null;
15254
+ }
15255
+ };
15256
+ function mergeSlices(indicator, user) {
15257
+ const out = /* @__PURE__ */ new Map();
15258
+ for (const [paneId, slices] of indicator) out.set(paneId, [...slices]);
15259
+ for (const [paneId, slices] of user) out.set(paneId, [...out.get(paneId) ?? [], ...slices]);
15260
+ for (const slices of out.values()) slices.sort((a, b) => a.beforeZ - b.beforeZ);
15261
+ return out;
15262
+ }
15263
+
14979
15264
  // src/renderers/native/drawings/Projector.ts
14980
15265
  function createProjector(coords, paneOf, paneIdAtY, barsInRange) {
14981
15266
  return {
@@ -15829,6 +16114,11 @@ var NativeRenderer = class {
15829
16114
  this.vpvrRenderer = new VpvrRenderer();
15830
16115
  this.resizeObserver = null;
15831
16116
  this.dprMedia = null;
16117
+ /** Plot size in INTEGER device px, as last reported by the resize observer's
16118
+ * device-pixel-content-box — the browser's own statement of how many device pixels
16119
+ * it paints the plot into. `null` until the first report or where the box type is
16120
+ * unsupported (WebKit); syncSize then falls back to rounding the client rect. */
16121
+ this.plotDeviceSize = null;
15832
16122
  this.coords = new CoordinateSystem();
15833
16123
  this.scene = new SceneGraph();
15834
16124
  // chosen at mount (WebGL2 if available, else canvas2d)
@@ -15836,6 +16126,8 @@ var NativeRenderer = class {
15836
16126
  this.glowAmount = 0;
15837
16127
  // WebGL2 neon-glow intensity (canvas2d ignores it)
15838
16128
  this.chrome = new ChromeRenderer();
16129
+ /** Prepaints each indicator's Pine drawings into interleave slices at the model's z. */
16130
+ this.indicatorSlices = new IndicatorDrawingSlices();
15839
16131
  /** Hover tooltips for Pine labels (canvas hit-rects collected by the chrome layer). */
15840
16132
  this.labelTooltip = null;
15841
16133
  this.crosshairLayer = new CrosshairRenderer();
@@ -15984,7 +16276,6 @@ var NativeRenderer = class {
15984
16276
  this.toggleVisibleCbs = /* @__PURE__ */ new Set();
15985
16277
  this.moveIndicatorCbs = /* @__PURE__ */ new Set();
15986
16278
  this.priceStyleCbs = /* @__PURE__ */ new Set();
15987
- this.tableOverlays = /* @__PURE__ */ new Map();
15988
16279
  this.name = "native";
15989
16280
  this.features = ["logScale", "currentPriceLine", "priceLabel", "countdown", "upColor", "downColor", "glow", "animZoom", "animPan", "intro", "zoomAnchor", "axisDrag", "paneResize", "candleZOrder", "candleVisible", "seriesOrder", "highlights", "sessionZones", "gridlines", "axisLabels", "scaleMode", "invertScale", "paneScales", "autoScale", "timezone", "keyboard", "historyChords", "priceStyle", "priceBaseline", "baselinePrice", "settings", "attribution", "dialogHost", "tradeMarkers", "indicatorTitles", "indicatorValues"];
15990
16281
  /** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
@@ -16298,7 +16589,6 @@ var NativeRenderer = class {
16298
16589
  * hidden) on clear so a re-show picks up the current theme.
16299
16590
  */
16300
16591
  setLoading(loading) {
16301
- for (const overlay of this.tableOverlays.values()) overlay.setVisible(!loading);
16302
16592
  if (!loading || !this.wrapper) {
16303
16593
  this.loadingEl?.remove();
16304
16594
  this.loadingEl = null;
@@ -16937,13 +17227,13 @@ var NativeRenderer = class {
16937
17227
  resetView: () => this.resetView(),
16938
17228
  // User drawings claim a gesture before pan when armed / over a drawing.
16939
17229
  drawingsClaim: (x, y) => this.userDrawings?.claim(x, y) ?? false,
16940
- drawingsMeasureStart: (x, y) => this.userDrawings?.beginMeasureAt(x, y) ?? false,
17230
+ drawingsMeasureStart: (x, y, snap) => this.userDrawings?.beginMeasureAt(x, y, snap) ?? false,
16941
17231
  drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y) ?? false,
16942
17232
  drawingsCancelPlacement: () => this.userDrawings?.cancelPlacement() ?? false,
16943
17233
  drawingsSnapMode: () => this.snapMode,
16944
17234
  drawingsPointerDown: (x, y, snap, shift) => this.userDrawings?.pointerDown(x, y, snap, shift),
16945
17235
  drawingsPointerMove: (x, y, snap, shift) => this.userDrawings?.pointerMove(x, y, snap, shift),
16946
- drawingsPointerUp: (x, y) => this.userDrawings?.pointerUp(x, y),
17236
+ drawingsPointerUp: (x, y, snap) => this.userDrawings?.pointerUp(x, y, snap),
16947
17237
  drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? null,
16948
17238
  drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
16949
17239
  drawingsClearTransient: () => this.userDrawings?.clearTransient()
@@ -16960,7 +17250,7 @@ var NativeRenderer = class {
16960
17250
  this.plot.addEventListener("pointerleave", this.onScrollProximityLeave);
16961
17251
  this.labelTooltip = new LabelTooltip(this.plot, {
16962
17252
  theme: () => this.chromeTheme(),
16963
- lookup: (x, y) => this.chrome.labelTooltipAt(x, y)
17253
+ lookup: (x, y) => this.indicatorSlices.labelTooltipAt(x, y)
16964
17254
  });
16965
17255
  this.userDrawings = new UserDrawingController(this.wrapper, this.plot, this.drawingsCanvas, {
16966
17256
  projector: () => this.drawingProjector(),
@@ -17045,6 +17335,7 @@ var NativeRenderer = class {
17045
17335
  this.emitPaneAction({ type: "maximize", paneId, maximized });
17046
17336
  }
17047
17337
  });
17338
+ this.paneControls.setSuspended(this.layoutMode === "mobile");
17048
17339
  this.axisScaleButtons = new AxisScaleButtons(this.plot, theme, {
17049
17340
  panes: () => this.axisScaleViews(),
17050
17341
  rightAxis: () => this.rightAxisW,
@@ -17054,8 +17345,19 @@ var NativeRenderer = class {
17054
17345
  if (pane) this.setPaneLog(paneId, !paneLogScale(this.scene, pane));
17055
17346
  }
17056
17347
  });
17057
- this.resizeObserver = new ResizeObserver(() => this.resize());
17348
+ this.resizeObserver = new ResizeObserver((entries) => {
17349
+ for (const e of entries) {
17350
+ if (e.target !== this.plot) continue;
17351
+ const s = e.devicePixelContentBoxSize?.[0];
17352
+ if (s) this.plotDeviceSize = { width: s.inlineSize, height: s.blockSize };
17353
+ }
17354
+ this.resize();
17355
+ });
17058
17356
  this.resizeObserver.observe(this.wrapper);
17357
+ try {
17358
+ this.resizeObserver.observe(this.plot, { box: "device-pixel-content-box" });
17359
+ } catch {
17360
+ }
17059
17361
  this.watchDpr();
17060
17362
  this.syncSize();
17061
17363
  }
@@ -17180,8 +17482,6 @@ var NativeRenderer = class {
17180
17482
  this.inputsUI?.destroy();
17181
17483
  this.paneControls?.destroy();
17182
17484
  this.axisScaleButtons?.destroy();
17183
- for (const overlay of this.tableOverlays.values()) overlay.destroy();
17184
- this.tableOverlays.clear();
17185
17485
  this.resizeObserver?.disconnect();
17186
17486
  this.resizeObserver = null;
17187
17487
  this.dprMedia?.removeEventListener("change", this.onDprChange);
@@ -17210,6 +17510,7 @@ var NativeRenderer = class {
17210
17510
  this.attributionEl = null;
17211
17511
  this.mountContainer?.style.removeProperty("--vela-toolbar-gutter");
17212
17512
  this.mountContainer?.style.removeProperty("--vela-scale-gutter");
17513
+ this.mountContainer?.style.removeProperty("--vela-bottom-gutter");
17213
17514
  this.mountContainer?.style.removeProperty("--vela-price-pane-top");
17214
17515
  this.mountContainer?.style.removeProperty("--vela-price-pane-bottom");
17215
17516
  this.mountContainer = null;
@@ -17247,7 +17548,8 @@ var NativeRenderer = class {
17247
17548
  }
17248
17549
  const skipFit = opts?.preserveView === true && this.didInitialFit;
17249
17550
  if (this.coords.width > 0 && !skipFit) {
17250
- this.fitContent();
17551
+ if (this.didInitialFit) this.reframeKeepZoom();
17552
+ else this.fitContent();
17251
17553
  this.didInitialFit = true;
17252
17554
  }
17253
17555
  if (!this.introPlayed && this.bars.length > 0) {
@@ -17302,7 +17604,6 @@ var NativeRenderer = class {
17302
17604
  ensurePane(pane) {
17303
17605
  this.scene.ensurePane(pane.id, pane.kind, pane.order, pane.heightWeight ?? (pane.kind === "price" ? 3 : 1));
17304
17606
  this.layoutPanes();
17305
- this.repositionTables();
17306
17607
  this.paneControls?.refresh();
17307
17608
  this.scheduler.invalidate(4 /* Full */);
17308
17609
  }
@@ -17324,17 +17625,14 @@ var NativeRenderer = class {
17324
17625
  if (!model.ownScale) this.scene.dropIndicatorScale(handle.id);
17325
17626
  this.inputsUI.setPane(handle.id, paneId);
17326
17627
  this.refreshAnchorOffset(model);
17327
- this.syncTables(model);
17328
17628
  this.refreshAxisWidth();
17329
17629
  this.layoutPanes();
17330
- this.repositionTables();
17331
17630
  this.paneControls?.refresh();
17332
17631
  this.scheduler.invalidate(4 /* Full */);
17333
17632
  }
17334
17633
  orderPanes(orderedIds) {
17335
17634
  this.scene.orderPanes(orderedIds);
17336
17635
  this.layoutPanes();
17337
- this.repositionTables();
17338
17636
  this.paneControls?.refresh();
17339
17637
  this.scheduler.invalidate(4 /* Full */);
17340
17638
  }
@@ -17343,7 +17641,6 @@ var NativeRenderer = class {
17343
17641
  if (!pane || pane.collapsed === collapsed) return;
17344
17642
  pane.collapsed = collapsed;
17345
17643
  this.layoutPanes();
17346
- this.repositionTables();
17347
17644
  this.paneControls?.refresh();
17348
17645
  this.scheduler.invalidate(4 /* Full */);
17349
17646
  }
@@ -17351,7 +17648,6 @@ var NativeRenderer = class {
17351
17648
  if (paneId !== null && !this.scene.panes.has(paneId)) paneId = null;
17352
17649
  this.maximizedPaneId = paneId;
17353
17650
  this.layoutPanes();
17354
- this.repositionTables();
17355
17651
  this.paneControls?.refresh();
17356
17652
  this.scheduler.invalidate(4 /* Full */);
17357
17653
  }
@@ -17438,7 +17734,6 @@ var NativeRenderer = class {
17438
17734
  native: !!model.native,
17439
17735
  ...model.props ? { props: model.props, propValues: model.propValues ?? {} } : {}
17440
17736
  });
17441
- this.syncTables(model);
17442
17737
  if (model.native?.type === "volume") {
17443
17738
  this.volumeActive = true;
17444
17739
  this.volumeHidden = false;
@@ -17462,7 +17757,6 @@ var NativeRenderer = class {
17462
17757
  }
17463
17758
  }
17464
17759
  applyPatch(model, patch);
17465
- this.syncTables(model);
17466
17760
  this.scheduler.invalidate(3 /* Light */);
17467
17761
  }
17468
17762
  removeIndicator(handle) {
@@ -17481,8 +17775,6 @@ var NativeRenderer = class {
17481
17775
  this.scene.forgetAnchorOffset(handle.id);
17482
17776
  this.scene.dropIndicatorScale(handle.id);
17483
17777
  this.inputsUI.remove(handle.id);
17484
- this.tableOverlays.get(handle.id)?.destroy();
17485
- this.tableOverlays.delete(handle.id);
17486
17778
  this.refreshAxisWidth();
17487
17779
  this.paneControls?.refresh();
17488
17780
  this.scheduler.invalidate(4 /* Full */);
@@ -17526,8 +17818,6 @@ var NativeRenderer = class {
17526
17818
  }
17527
17819
  if (!visible) {
17528
17820
  this.scene.indicators.delete(handle.id);
17529
- this.tableOverlays.get(handle.id)?.destroy();
17530
- this.tableOverlays.delete(handle.id);
17531
17821
  }
17532
17822
  this.inputsUI.setVisible(handle.id, visible);
17533
17823
  this.scheduler.invalidate(4 /* Full */);
@@ -17573,6 +17863,7 @@ var NativeRenderer = class {
17573
17863
  this.userDrawings?.setLayoutMode(mode);
17574
17864
  this.settingsDialog?.setLayoutMode(mode);
17575
17865
  this.inputsUI?.setLayoutMode(mode);
17866
+ this.paneControls?.setSuspended(mode === "mobile");
17576
17867
  if (this.scrollButton) {
17577
17868
  const px = mode === "mobile" ? SCROLL_BTN_SIZE_TOUCH : SCROLL_BTN_SIZE;
17578
17869
  this.scrollButton.style.width = `${px}px`;
@@ -17906,6 +18197,7 @@ var NativeRenderer = class {
17906
18197
  this.scaleDragHeight = res.height;
17907
18198
  this.scaleDragStart = { ...res.holder.scale };
17908
18199
  res.holder.manualScale = { ...res.holder.scale };
18200
+ this.axisScaleButtons?.reposition();
17909
18201
  this.scheduler.invalidate(4 /* Full */);
17910
18202
  }
17911
18203
  /** Rescale the grabbed scale around its center by the total drag (down ⇒ zoom out). */
@@ -17937,6 +18229,7 @@ var NativeRenderer = class {
17937
18229
  const res = this.resolveScaleHolder(x, y);
17938
18230
  if (!res) return;
17939
18231
  res.holder.manualScale = null;
18232
+ this.axisScaleButtons?.reposition();
17940
18233
  this.scheduler.invalidate(4 /* Full */);
17941
18234
  }
17942
18235
  /**
@@ -17968,7 +18261,6 @@ var NativeRenderer = class {
17968
18261
  /** Relayout + repaint + refresh the hover buttons after a collapse/maximize/order change. */
17969
18262
  afterPaneLayoutChange() {
17970
18263
  this.layoutPanes();
17971
- this.repositionTables();
17972
18264
  this.paneControls?.refresh();
17973
18265
  this.scheduler.invalidate(4 /* Full */);
17974
18266
  }
@@ -18049,7 +18341,6 @@ var NativeRenderer = class {
18049
18341
  above.heightWeight = next.above;
18050
18342
  below.heightWeight = next.below;
18051
18343
  this.layoutPanes();
18052
- this.repositionTables();
18053
18344
  this.scheduler.invalidate(4 /* Full */);
18054
18345
  }
18055
18346
  /** Double-click a separator → split the two adjacent panes evenly (each gets half of
@@ -18064,7 +18355,6 @@ var NativeRenderer = class {
18064
18355
  above.heightWeight = half;
18065
18356
  below.heightWeight = half;
18066
18357
  this.layoutPanes();
18067
- this.repositionTables();
18068
18358
  this.scheduler.invalidate(4 /* Full */);
18069
18359
  }
18070
18360
  // ── keyboard navigation / accessibility (item 11) ──
@@ -18343,7 +18633,10 @@ var NativeRenderer = class {
18343
18633
  const liveActual = li >= 0 ? this.bars[li] : void 0;
18344
18634
  const easeLive = !!liveActual && this.liveEaseTime === liveActual.time && (liveActual.high !== this.liveEaseHigh || liveActual.low !== this.liveEaseLow || liveActual.close !== this.liveEaseClose);
18345
18635
  if (easeLive && liveActual) this.bars[li] = { ...liveActual, high: this.liveEaseHigh, low: this.liveEaseLow, close: this.liveEaseClose };
18346
- this.scene.drawingSlices = this.userDrawings?.prepareSlices(this.scene.orderedPanes().map((p) => p.id)) ?? /* @__PURE__ */ new Map();
18636
+ this.scene.drawingSlices = mergeSlices(
18637
+ this.indicatorSlices.prepare(this.scene, this.coords, this.theme, this.dataCanvas),
18638
+ this.userDrawings?.prepareSlices(this.scene.orderedPanes().map((p) => p.id)) ?? /* @__PURE__ */ new Map()
18639
+ );
18347
18640
  this.backdropRenderer.render(this.scene, this.coords, this.theme, gridAlpha);
18348
18641
  this.backend.render(this.scene, this.coords, this.theme);
18349
18642
  this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
@@ -18574,6 +18867,20 @@ var NativeRenderer = class {
18574
18867
  this.coords.setViewport(v);
18575
18868
  this.targetBarSpacing = v.barSpacing;
18576
18869
  }
18870
+ /** Re-frame after a series replacement (a symbol/timeframe switch): keep the user's
18871
+ * zoom (bar spacing), re-anchor the newest bars at the default right offset.
18872
+ * `clampViewport`'s fit-all-bars floor deliberately does NOT apply — a progressive
18873
+ * head may still be backfilling toward the previous depth, and raising the spacing
18874
+ * to its temporary bar count would lose the zoom this exists to keep. */
18875
+ reframeKeepZoom() {
18876
+ this.animator?.stop();
18877
+ this.panVelocity = 0;
18878
+ for (const pane of this.scene.panes.values()) pane.manualScale = null;
18879
+ for (const sl of this.scene.indicatorScales.values()) sl.manualScale = null;
18880
+ const v = { barSpacing: clampBarSpacing(this.coords.getViewport().barSpacing), rightOffset: defaultViewport().rightOffset };
18881
+ this.coords.setViewport(v);
18882
+ this.targetBarSpacing = v.barSpacing;
18883
+ }
18577
18884
  paneBoundsFor(paneId) {
18578
18885
  const p = this.scene.panes.get(paneId);
18579
18886
  return { top: p?.bounds.top ?? 0, height: p?.bounds.height ?? 0, rightAxis: this.rightAxisW };
@@ -18781,27 +19088,6 @@ var NativeRenderer = class {
18781
19088
  }
18782
19089
  return maxVol;
18783
19090
  }
18784
- /** Create/update/destroy an indicator's DOM table overlay (anchored off real pane geometry). */
18785
- syncTables(model) {
18786
- const tables = model.tables ?? [];
18787
- let overlay = this.tableOverlays.get(model.id);
18788
- if (tables.length === 0) {
18789
- if (overlay) {
18790
- overlay.destroy();
18791
- this.tableOverlays.delete(model.id);
18792
- }
18793
- return;
18794
- }
18795
- if (!overlay) {
18796
- overlay = new TableOverlay(this.plot, this.theme, (id) => this.paneBoundsFor(id));
18797
- overlay.setVisible(this.loadingEl === null);
18798
- this.tableOverlays.set(model.id, overlay);
18799
- }
18800
- overlay.update(tables);
18801
- }
18802
- repositionTables() {
18803
- for (const overlay of this.tableOverlays.values()) overlay.reposition();
18804
- }
18805
19091
  layoutPanes() {
18806
19092
  const panes = this.scene.orderedPanes();
18807
19093
  const dataHeight = this.coords.height;
@@ -18852,6 +19138,7 @@ var NativeRenderer = class {
18852
19138
  const visible = maxPane ? [maxPane] : this.scene.orderedPanes().filter((p) => !p.collapsed);
18853
19139
  const paneBottom = visible.length ? Math.max(...visible.map((p) => p.bounds.top + p.bounds.height)) : dataHeight;
18854
19140
  this.scrollBtnBottomPx = SCROLL_BTN_BOTTOM + Math.max(0, dataHeight - paneBottom);
19141
+ this.mountContainer?.style.setProperty("--vela-bottom-gutter", `${TIME_AXIS_H + Math.max(0, dataHeight - paneBottom)}px`);
18855
19142
  this.scrollBtnRightPx = this.rightAxisW + SCROLL_BTN_RIGHT_INSET;
18856
19143
  if (this.scrollButton) {
18857
19144
  this.scrollButton.style.bottom = `${this.scrollBtnBottomPx}px`;
@@ -18944,30 +19231,33 @@ var NativeRenderer = class {
18944
19231
  if (w <= 0 || h <= 0) return;
18945
19232
  const dpr = window.devicePixelRatio || 1;
18946
19233
  this.plot.style.left = `${this.toolbarGutter}px`;
18947
- const pw = Math.max(1, w - this.toolbarGutter);
18948
- const ph = h;
18949
- this.dataCanvas.width = Math.round(pw * dpr);
18950
- this.dataCanvas.height = Math.round(ph * dpr);
18951
- this.backdropCanvas.width = this.dataCanvas.width;
18952
- this.backdropCanvas.height = this.dataCanvas.height;
18953
- this.volumeCanvas.width = this.dataCanvas.width;
18954
- this.volumeCanvas.height = this.dataCanvas.height;
18955
- for (const l of this.extLayers) {
18956
- l.canvas.width = this.dataCanvas.width;
18957
- l.canvas.height = this.dataCanvas.height;
18958
- }
18959
- this.vpvrCanvas.width = this.dataCanvas.width;
18960
- this.vpvrCanvas.height = this.dataCanvas.height;
18961
- this.chromeCanvas.width = this.dataCanvas.width;
18962
- this.chromeCanvas.height = this.dataCanvas.height;
18963
- this.drawingsCanvas.width = this.dataCanvas.width;
18964
- this.drawingsCanvas.height = this.dataCanvas.height;
18965
- this.cursorCanvas.width = this.dataCanvas.width;
18966
- this.cursorCanvas.height = this.dataCanvas.height;
19234
+ const rect = this.plot.getBoundingClientRect();
19235
+ let bw = Math.max(1, Math.round(rect.width * dpr));
19236
+ let bh = Math.max(1, Math.round(rect.height * dpr));
19237
+ const dev = this.plotDeviceSize;
19238
+ if (dev && Math.abs(dev.width - rect.width * dpr) <= 1 && Math.abs(dev.height - rect.height * dpr) <= 1) {
19239
+ bw = Math.max(1, dev.width);
19240
+ bh = Math.max(1, dev.height);
19241
+ }
19242
+ const pw = bw / dpr;
19243
+ const ph = bh / dpr;
19244
+ const size = (canvas) => {
19245
+ canvas.width = bw;
19246
+ canvas.height = bh;
19247
+ canvas.style.width = `${pw}px`;
19248
+ canvas.style.height = `${ph}px`;
19249
+ };
19250
+ size(this.dataCanvas);
19251
+ size(this.backdropCanvas);
19252
+ size(this.volumeCanvas);
19253
+ for (const l of this.extLayers) size(l.canvas);
19254
+ size(this.vpvrCanvas);
19255
+ size(this.chromeCanvas);
19256
+ size(this.drawingsCanvas);
19257
+ size(this.cursorCanvas);
18967
19258
  this.coords.setSize(Math.max(1, pw - this.rightAxisW), Math.max(1, ph - TIME_AXIS_H), dpr);
18968
19259
  this.scene.crosshair = null;
18969
19260
  this.layoutPanes();
18970
- this.repositionTables();
18971
19261
  this.userDrawings?.onResize();
18972
19262
  if (!this.didInitialFit && this.coords.barCount > 0) {
18973
19263
  this.fitContent();
@@ -19157,6 +19447,12 @@ var IndicatorHandleImpl = class {
19157
19447
  get visible() {
19158
19448
  return this.visibleState;
19159
19449
  }
19450
+ inputValues() {
19451
+ return this.controller.inputValuesOf(this.id);
19452
+ }
19453
+ propValues() {
19454
+ return this.controller.propValuesOf(this.id);
19455
+ }
19160
19456
  setInput(key, value) {
19161
19457
  this.controller.applyInputs(this.id, { [key]: value });
19162
19458
  }
@@ -19296,6 +19592,7 @@ var RUN_EMIT_THROTTLE_MS = 1e3;
19296
19592
  var PREVIEW_BARS = 300;
19297
19593
  var SINGLE_LOAD_BARS = 5e3;
19298
19594
  var CHUNK_BARS = 1e4;
19595
+ var FIRST_PAINT_BARS = 100;
19299
19596
  var GAP_FACTOR = 1.5;
19300
19597
  var HEAL_COOLDOWN_MS = 5e3;
19301
19598
  var EngineOrchestrator = class _EngineOrchestrator {
@@ -19359,6 +19656,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
19359
19656
  /** Invalidates detached async work (backfill loops, in-flight loads, gap heals):
19360
19657
  * bumped by init(), setMarket() and destroy(). */
19361
19658
  this.generation = 0;
19659
+ /** Aborts the in-flight PROGRESSIVE load's source polling on supersession — an
19660
+ * abandoned stream left polling to its own budget starves the browser's per-host
19661
+ * connection pool, and the NEXT symbol's very first fetch with it (measured). */
19662
+ this.progressiveAbort = null;
19362
19663
  /** Awaiters racing a superseded load (setMarket callers) — released on every bump so they never hang. */
19363
19664
  this.supersedeWaiters = [];
19364
19665
  /** `history:complete` fired for the CURRENT load. Each market load re-arms the cycle
@@ -19501,6 +19802,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
19501
19802
  * superseded setMarket awaiters so their promises resolve instead of hanging. */
19502
19803
  bumpGeneration() {
19503
19804
  const gen = ++this.generation;
19805
+ this.progressiveAbort?.abort();
19806
+ this.progressiveAbort = null;
19504
19807
  for (const w of this.supersedeWaiters.splice(0)) w();
19505
19808
  return gen;
19506
19809
  }
@@ -19554,7 +19857,48 @@ var EngineOrchestrator = class _EngineOrchestrator {
19554
19857
  const requested = market.bars ?? 500;
19555
19858
  const initialRange = market.visibleRange;
19556
19859
  const deep = !market.data?.length && initialRange == null && requested > SINGLE_LOAD_BARS;
19557
- if (deep && this.feed.loadRange) {
19860
+ let progressiveServed = false;
19861
+ if (!market.data?.length && initialRange == null && this.feed.loadProgressive) {
19862
+ let painted = false;
19863
+ const paint = (bars, final) => {
19864
+ if (this.generation !== gen || !final && bars.length === 0) return;
19865
+ if (!painted && !final && bars.length < Math.min(requested, FIRST_PAINT_BARS)) return;
19866
+ this.setBarSeries(bars, painted ? { preserveView: true } : void 0);
19867
+ if (!painted && bars.length > 0) {
19868
+ painted = true;
19869
+ if (opts.firstLoad) this.activateBarLayers();
19870
+ if (!final) this.historyState = "backfill";
19871
+ }
19872
+ };
19873
+ const abort = new AbortController();
19874
+ this.progressiveAbort = abort;
19875
+ progressiveServed = await new Promise((firstPaint) => {
19876
+ let signaled = false;
19877
+ const signal = (served) => {
19878
+ if (!signaled) {
19879
+ signaled = true;
19880
+ firstPaint(served);
19881
+ }
19882
+ };
19883
+ abort.signal.addEventListener("abort", () => signal(true), { once: true });
19884
+ this.feed.loadProgressive(market, (bars) => {
19885
+ paint(bars, false);
19886
+ if (painted) signal(true);
19887
+ }, { signal: abort.signal }).then((full) => {
19888
+ if (this.progressiveAbort === abort) this.progressiveAbort = null;
19889
+ if (full == null) return signal(false);
19890
+ if (this.generation !== gen) return signal(true);
19891
+ paint(full, true);
19892
+ this.completeHistory(full.length >= requested ? "depth" : "genesis");
19893
+ signal(true);
19894
+ }).catch(() => {
19895
+ if (this.progressiveAbort === abort) this.progressiveAbort = null;
19896
+ if (this.generation === gen) this.completeHistory("aborted");
19897
+ signal(true);
19898
+ });
19899
+ });
19900
+ }
19901
+ if (progressiveServed) ; else if (deep && this.feed.loadRange) {
19558
19902
  const head = await this.feed.load({ ...market, bars: Math.min(requested, CHUNK_BARS) });
19559
19903
  if (this.generation !== gen) return;
19560
19904
  this.setBarSeries(head);
@@ -20149,6 +20493,15 @@ var EngineOrchestrator = class _EngineOrchestrator {
20149
20493
  record.pendingCause = "inputs";
20150
20494
  if (record.session) record.session.update(record.inputValues);
20151
20495
  else if (record.native && !record.hidden) record.native.instance.setInputs(record.inputValues);
20496
+ this.events.emit("indicator:inputs", { id });
20497
+ }
20498
+ /** IndicatorController: the CURRENT stored input values (defaults merged with edits). */
20499
+ inputValuesOf(id) {
20500
+ return { ...this.registry.get(id)?.inputValues };
20501
+ }
20502
+ /** IndicatorController: the CURRENT declaration-prop overrides. */
20503
+ propValuesOf(id) {
20504
+ return { ...this.registry.get(id)?.propValues };
20152
20505
  }
20153
20506
  /** IndicatorController: re-run an indicator with merged declaration-prop overrides.
20154
20507
  * Same lifecycle as {@link applyInputs} — a prop change replays the whole script.
@@ -20162,6 +20515,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
20162
20515
  if (!record.hidden) this.setLoading(record, true);
20163
20516
  record.pendingCause = "inputs";
20164
20517
  record.session.update(record.inputValues, record.propValues);
20518
+ this.events.emit("indicator:inputs", { id });
20165
20519
  }
20166
20520
  /** IndicatorController: tear down an indicator and (if now empty) its pane. */
20167
20521
  /** Live handles of every indicator on the chart (script + native), insertion order. */
@@ -21374,4 +21728,4 @@ function resolveElement(container) {
21374
21728
  return element;
21375
21729
  }
21376
21730
 
21377
- export { BUILTIN_PRICE_STYLES, BarStore, CachingDataFeed, DARK_THEME, DataControl, DrawingStore, DrawingToolbar, DrawingsControl, LEGEND_AT_TOP_ATTR, LIGHT_THEME, MultiProviderFeed, NativeRenderer, RendererControl, TIMEZONES, TypedEventBus, Vela, applyAttributionMarkTheme, buildToolbar, createAttributionMark, createCustomMark, defaultToolbar, normalizeTimezone, parseSymbol, priceStyleIds, registerBuiltinChartTypes, resolveTheme, sharedBarStore, timeframeToMs, tzButtonLabel, tzMenuLabel, tzOffset };
21731
+ export { BUILTIN_PRICE_STYLES, BarStore, CachingDataFeed, DARK_THEME, DataControl, DrawingStore, DrawingToolbar, DrawingsControl, LEGEND_AT_TOP_ATTR, LIGHT_THEME, MultiProviderFeed, NativeRenderer, RendererControl, TIMEZONES, TypedEventBus, Vela, applyAttributionMarkTheme, buildToolbar, createAttributionMark, createCustomMark, defaultMemberOf, defaultToolbar, groupKeyOf, groupMembers, isGroupRow, normalizeTimezone, parseSymbol, priceStyleIds, registerBuiltinChartTypes, resolveTheme, sharedBarStore, timeframeToMs, tzButtonLabel, tzMenuLabel, tzOffset };