@luxalgo/vela 0.6.8 → 0.6.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/{DataProvider-BSLlBpB9.d.ts → DataProvider-CWmp31dA.d.ts} +49 -1
  2. package/dist/{DataProvider-BsQM2WNH.d.cts → DataProvider-l_eLMly_.d.cts} +49 -1
  3. package/dist/{chunk-L3I2CCYO.js → chunk-4KZVZ7QQ.js} +49 -2
  4. package/dist/{chunk-SQETIBFU.js → chunk-FFOP37FQ.js} +996 -377
  5. package/dist/{chunk-2R7BANEM.js → chunk-G77Y7LK2.js} +2 -2
  6. package/dist/{chunk-PN5KWFZ4.js → chunk-KG4YT3TI.js} +5 -3
  7. package/dist/{chunk-OICPLNU7.js → chunk-MHY7MVXH.js} +192 -2
  8. package/dist/{chunk-N7LGMKCE.js → chunk-NMTQXNT4.js} +637 -164
  9. package/dist/{contributions-DLLdV9jD.d.ts → contributions-BCz6Dr6a.d.ts} +99 -7
  10. package/dist/{contributions-Vw-Hm58R.d.cts → contributions-D9vTzm5p.d.cts} +99 -7
  11. package/dist/index.cjs +1210 -383
  12. package/dist/index.d.cts +37 -11
  13. package/dist/index.d.ts +37 -11
  14. package/dist/index.js +4 -4
  15. package/dist/{options-BaVTMXaO.d.ts → options-DSqHsQyN.d.cts} +84 -7
  16. package/dist/{options-BaVTMXaO.d.cts → options-DSqHsQyN.d.ts} +84 -7
  17. package/dist/{plugin-BnJgjLAy.d.cts → plugin-Bt8hLR8Z.d.cts} +3 -3
  18. package/dist/{plugin-CC7rBrOY.d.ts → plugin-CH7pfMrE.d.ts} +3 -3
  19. package/dist/plugin.cjs +21 -3
  20. package/dist/plugin.d.cts +4 -4
  21. package/dist/plugin.d.ts +4 -4
  22. package/dist/plugin.js +2 -2
  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-CGkB2EOo.d.ts → statusline-5K7y4Ya2.d.ts} +5 -3
  30. package/dist/{statusline-DqzBqy42.d.cts → statusline-TYLQmoeh.d.cts} +5 -3
  31. package/dist/ui.cjs +208 -5
  32. package/dist/ui.d.cts +91 -2
  33. package/dist/ui.d.ts +91 -2
  34. package/dist/ui.js +3 -3
  35. package/dist/vela.global.js +1210 -383
  36. package/dist/vela.global.min.js +99 -48
  37. package/dist/widget.cjs +1771 -449
  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 +1771 -449
  42. package/dist/workspace.d.cts +100 -6
  43. package/dist/workspace.d.ts +100 -6
  44. package/dist/workspace.js +6 -6
  45. package/package.json +1 -1
@@ -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-L3I2CCYO.js';
2
- import { themeTokens, Dialog, closeOpenPopovers, closeWidthPopover, fieldSection, buildFieldControl, fieldSeparator, fieldGrid, fieldRow, blendOver, splitColor, Menu, TextArea, Popover, NumberInput, buildColorPicker, isPopoverOpen, eventDismissedPopover, toggleSelectList, openPopoverTrigger, fieldGridColumns, FIELD_GAP_PX, STATIC_TOKENS } from './chunk-OICPLNU7.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-PN5KWFZ4.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-4KZVZ7QQ.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 {
@@ -428,6 +428,15 @@ var RendererControl = class {
428
428
  setLegendActions(provider) {
429
429
  this.renderer.setLegendActions?.(provider);
430
430
  }
431
+ /**
432
+ * Wire the legend rows' HOST-CONTRIBUTED callout bubbles (the shells route the
433
+ * plugin registry through this; see `registerLegendCallout`). Silent on a renderer
434
+ * without the seam — contributed callouts simply never show there, same graceful
435
+ * degradation as {@link setLegendActions}.
436
+ */
437
+ setLegendCallouts(provider) {
438
+ this.renderer.setLegendCallouts?.(provider);
439
+ }
431
440
  /**
432
441
  * Replace the indicator legend's fold toggle with a host action (or restore it with
433
442
  * `null`) — multi-chart shells point the chip at their indicator overview instead of
@@ -687,6 +696,23 @@ var BarStore = class {
687
696
  };
688
697
  var sharedBarStore = new BarStore();
689
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
+
690
716
  // src/data/ProviderRegistry.ts
691
717
  var normName = (n) => n.trim().toLowerCase();
692
718
  var normTicker = (t) => t.trim().toUpperCase();
@@ -776,17 +802,24 @@ var ProviderRegistry = class {
776
802
  resolve(raw, opts = {}) {
777
803
  const { provider, ticker } = this.parse(raw);
778
804
  if (provider) {
779
- 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
+ }
780
809
  const key = `${provider}:${normTicker(ticker)}`;
781
810
  for (const name of this.candidateOrder(opts.default)) {
782
811
  const d = this.entries.get(name).prefixIndex?.get(key);
783
- if (d) return { provider: name, ticker: d.ticker };
812
+ if (d) return { provider: name, ticker: this.loadableTicker(name, d) };
784
813
  }
785
814
  return null;
786
815
  }
787
816
  const norm = normTicker(ticker);
788
817
  for (const name of this.candidateOrder(opts.default)) {
789
- 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
+ }
790
823
  }
791
824
  if (opts.lenient && this.entries.size === 1) {
792
825
  const only = this.names()[0];
@@ -795,6 +828,13 @@ var ProviderRegistry = class {
795
828
  }
796
829
  return null;
797
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
+ }
798
838
  /**
799
839
  * Resolve now if possible, else resolve later — re-attempting after each provider
800
840
  * settles. The promise stays pending until some registered provider can serve the
@@ -938,6 +978,28 @@ var CachingDataFeed = class {
938
978
  this.store.merge(key, dropForming(bars));
939
979
  return bars;
940
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
+ }
941
1003
  /**
942
1004
  * Cache-backed ranged fetch — the gateway engines use for secondary series
943
1005
  * (`request.security` HTF/LTF/cross-symbol) and the orchestrator's backward
@@ -1183,6 +1245,17 @@ var MultiProviderFeed = class {
1183
1245
  this.prefetchSymbolInfo(resolved);
1184
1246
  return this.cache.load(canonical(cfg, resolved));
1185
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
+ }
1186
1259
  /**
1187
1260
  * Synchronous per-symbol metadata for engines (Pine `syminfo.*`), served from the cache
1188
1261
  * warmed by load(). Undefined until the prefetch lands (the engine then synthesizes a
@@ -1270,6 +1343,18 @@ var RegistryFetchFeed = class {
1270
1343
  if (!provider) return Promise.resolve([]);
1271
1344
  return safeBars(provider, ticker, cfg.timeframe ?? "60", { limit: cfg.bars ?? 500, session: cfg.session });
1272
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
+ }
1273
1358
  loadRange(cfg, range) {
1274
1359
  const { provider: name, ticker } = parseSymbol(cfg.symbol ?? "");
1275
1360
  const provider = this.registry.get(name ?? "");
@@ -2417,7 +2502,7 @@ var DrawingToolbar = class {
2417
2502
  this.root.replaceChildren();
2418
2503
  this.groupCells.clear();
2419
2504
  this.groupIcons.clear();
2420
- this.cursorBtn = this.makeButton(CURSOR_ICON, "Cursor", () => this.onArm(null));
2505
+ this.cursorBtn = this.makeButton(CURSOR_ICON, "Cursor", () => this.onCursorClick());
2421
2506
  this.root.appendChild(this.cursorBtn);
2422
2507
  if (this.def.groups.length > 0) this.root.appendChild(this.divider());
2423
2508
  for (const g of this.def.groups) {
@@ -2521,6 +2606,14 @@ var DrawingToolbar = class {
2521
2606
  this.magnetIcon = icon2;
2522
2607
  return cell;
2523
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
+ }
2524
2617
  /** Clicking the icon arms the group's last-used tool (it does NOT open the flyout). */
2525
2618
  onGroupIconClick(group) {
2526
2619
  const type = this.lastUsed.get(group.id) ?? group.tools[0]?.type;
@@ -2898,6 +2991,10 @@ function isLineLikeSeries(spec) {
2898
2991
  }
2899
2992
 
2900
2993
  // src/renderers/shared/IndicatorInputsDialog.ts
2994
+ var PROPS_TAB = "Properties";
2995
+ function bagOf(row, decl) {
2996
+ return decl.prop ? row.propValues : row.values;
2997
+ }
2901
2998
  var SOURCES = ["close", "open", "high", "low", "hl2", "hlc3", "ohlc4", "volume"];
2902
2999
  var IndicatorInputsDialog = class {
2903
3000
  constructor(host) {
@@ -2908,6 +3005,7 @@ var IndicatorInputsDialog = class {
2908
3005
  this.openId = null;
2909
3006
  this.row = null;
2910
3007
  this.snapshot = null;
3008
+ this.propSnapshot = null;
2911
3009
  this.dialogTips = [];
2912
3010
  /** Re-applies every `when` gate against current values; set while the dialog is open. */
2913
3011
  this.refreshVisibility = null;
@@ -2930,10 +3028,11 @@ var IndicatorInputsDialog = class {
2930
3028
  }
2931
3029
  open(row) {
2932
3030
  this.close();
2933
- if (row.inputs.length === 0) return;
3031
+ if (row.inputs.length === 0 && row.props.length === 0) return;
2934
3032
  this.row = row;
2935
3033
  this.openId = row.id;
2936
3034
  this.snapshot = { ...row.values };
3035
+ this.propSnapshot = { ...row.propValues };
2937
3036
  ensureDialogStyles();
2938
3037
  const t = this.host.theme();
2939
3038
  const border = "var(--vela-border)";
@@ -2941,7 +3040,7 @@ var IndicatorInputsDialog = class {
2941
3040
  const host = this.host.dialogHost() ?? this.host.container;
2942
3041
  const ui = new Dialog({
2943
3042
  host,
2944
- title: row.settingsTitle,
3043
+ title: row.title,
2945
3044
  // Non-modal: live-edit dialog — a modal machine locks pointer events on the
2946
3045
  // whole body, killing the chart and the body-portaled popovers.
2947
3046
  modal: false,
@@ -2973,6 +3072,16 @@ var IndicatorInputsDialog = class {
2973
3072
  text: () => "Close"
2974
3073
  }));
2975
3074
  const tabDefs = tabInputs(row.inputs);
3075
+ if (row.props.length > 0) {
3076
+ const propDecls = row.props.map((p) => {
3077
+ const d = { ...p, prop: true };
3078
+ delete d.tab;
3079
+ return d;
3080
+ });
3081
+ const existing = tabDefs.find((t2) => t2.name === PROPS_TAB);
3082
+ if (existing) existing.inputs.push(...propDecls);
3083
+ else tabDefs.push({ name: PROPS_TAB, inputs: propDecls });
3084
+ }
2976
3085
  const tabs = document.createElement("div");
2977
3086
  tabs.style.cssText = `display:flex;gap:12px;padding:0 20px;border-bottom:1px solid ${border};flex:0 0 auto;`;
2978
3087
  const tabEls = [];
@@ -3075,9 +3184,10 @@ var IndicatorInputsDialog = class {
3075
3184
  this.openId = null;
3076
3185
  this.row = null;
3077
3186
  this.snapshot = null;
3187
+ this.propSnapshot = null;
3078
3188
  ui?.destroy();
3079
3189
  }
3080
- /** Restore every input to its open-time value (re-running the indicator), then close. */
3190
+ /** Restore every input and prop to its open-time value (re-running the indicator), then close. */
3081
3191
  revertAndClose() {
3082
3192
  const row = this.row;
3083
3193
  const snap = this.snapshot;
@@ -3091,6 +3201,17 @@ var IndicatorInputsDialog = class {
3091
3201
  }
3092
3202
  }
3093
3203
  }
3204
+ const propSnap = this.propSnapshot;
3205
+ if (row && propSnap) {
3206
+ for (const p of row.props) {
3207
+ const snapped = propSnap[p.key];
3208
+ const before = snapped !== void 0 ? snapped : p.defval;
3209
+ if (row.propValues[p.key] !== before) {
3210
+ row.propValues[p.key] = before;
3211
+ this.host.onChange?.({ indicatorId: row.id, key: p.key, value: before, kind: "prop" });
3212
+ }
3213
+ }
3214
+ }
3094
3215
  this.close();
3095
3216
  }
3096
3217
  /** The footer's reset button — same chip as Cancel, pinned to the LEFT edge
@@ -3108,19 +3229,27 @@ var IndicatorInputsDialog = class {
3108
3229
  const row = this.row;
3109
3230
  if (!row) return;
3110
3231
  const snap = this.snapshot;
3232
+ const propSnap = this.propSnapshot;
3111
3233
  for (const inp of row.inputs) {
3112
3234
  if (row.values[inp.key] !== inp.defval) {
3113
3235
  row.values[inp.key] = inp.defval;
3114
3236
  this.host.onChange?.({ indicatorId: row.id, key: inp.key, value: inp.defval });
3115
3237
  }
3116
3238
  }
3239
+ for (const p of row.props) {
3240
+ if (row.propValues[p.key] !== p.defval) {
3241
+ row.propValues[p.key] = p.defval;
3242
+ this.host.onChange?.({ indicatorId: row.id, key: p.key, value: p.defval, kind: "prop" });
3243
+ }
3244
+ }
3117
3245
  this.open(row);
3118
3246
  if (snap) this.snapshot = snap;
3247
+ if (propSnap) this.propSnapshot = propSnap;
3119
3248
  }
3120
3249
  /** Write one edit through: store it, notify the host, and re-apply the `when` gates. */
3121
- commit(row, key, value) {
3122
- row.values[key] = value;
3123
- this.host.onChange?.({ indicatorId: row.id, key, value });
3250
+ commit(row, decl, value) {
3251
+ bagOf(row, decl)[decl.key] = value;
3252
+ this.host.onChange?.({ indicatorId: row.id, key: decl.key, value, ...decl.prop ? { kind: "prop" } : {} });
3124
3253
  this.refreshVisibility?.();
3125
3254
  }
3126
3255
  /** One settings row (or several `inline=` inputs) placed into a section's grid. */
@@ -3147,7 +3276,7 @@ var IndicatorInputsDialog = class {
3147
3276
  const id = idOf(lead);
3148
3277
  const info = lead.tooltip ? this.infoButton(lead.tooltip) : void 0;
3149
3278
  if (lead.type === "bool") {
3150
- const current = Boolean(row.values[lead.key] ?? lead.defval);
3279
+ const current = Boolean(bagOf(row, lead)[lead.key] ?? lead.defval);
3151
3280
  return append(fieldRow({
3152
3281
  label: nameOf(lead),
3153
3282
  id,
@@ -3156,7 +3285,7 @@ var IndicatorInputsDialog = class {
3156
3285
  toggle: {
3157
3286
  id,
3158
3287
  checked: current,
3159
- onChange: (v) => this.commit(row, lead.key, v)
3288
+ onChange: (v) => this.commit(row, lead, v)
3160
3289
  }
3161
3290
  }));
3162
3291
  }
@@ -3181,8 +3310,8 @@ var IndicatorInputsDialog = class {
3181
3310
  }
3182
3311
  /** Build the typed control for one input, committing edits live via `onChange`. */
3183
3312
  buildControl(row, inp, id) {
3184
- const current = row.values[inp.key] ?? inp.defval;
3185
- const emit = (value) => this.commit(row, inp.key, value);
3313
+ const current = bagOf(row, inp)[inp.key] ?? inp.defval;
3314
+ const emit = (value) => this.commit(row, inp, value);
3186
3315
  if (inp.type === "bool") {
3187
3316
  return buildFieldControl({ kind: "switch", id, checked: Boolean(current), onChange: (v) => emit(v) }).el;
3188
3317
  }
@@ -3193,7 +3322,7 @@ var IndicatorInputsDialog = class {
3193
3322
  kind: "color",
3194
3323
  id,
3195
3324
  theme: this.host.theme(),
3196
- get: () => String(row.values[inp.key] ?? inp.defval),
3325
+ get: () => String(bagOf(row, inp)[inp.key] ?? inp.defval),
3197
3326
  onChange: (v) => emit(v)
3198
3327
  }).el;
3199
3328
  }
@@ -3756,6 +3885,7 @@ function timeParts(ts) {
3756
3885
  }
3757
3886
 
3758
3887
  // src/renderers/shared/InputsUI.ts
3888
+ var LEGEND_AT_TOP_ATTR = "data-vela-pane-at-top";
3759
3889
  var STATUS_KEYFRAMES = "@keyframes vela-ind-pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.3;transform:scale(.6)}}";
3760
3890
  var LEGEND_ICON_PX2 = 16;
3761
3891
  var LEGEND_CTL_PX = 18;
@@ -3773,6 +3903,9 @@ var CLOSE_SVG = iconAt("close", LEGEND_ICON_PX2);
3773
3903
  var FOLD_SVG = iconAt("chevron-up", LEGEND_ICON_PX2);
3774
3904
  var UNFOLD_SVG = iconAt("chevron-down", LEGEND_ICON_PX2);
3775
3905
  var OVERVIEW_SVG = iconAt("objects", LEGEND_ICON_PX2);
3906
+ function legendCalloutsDisplay(open, hasCallouts) {
3907
+ return !open && hasCallouts ? "inline-flex" : "none";
3908
+ }
3776
3909
  var InputsUI = class {
3777
3910
  constructor(container, theme, paneBoundsOf) {
3778
3911
  this.container = container;
@@ -3794,10 +3927,14 @@ var InputsUI = class {
3794
3927
  this.moveApi = null;
3795
3928
  /** Host-contributed legend actions, resolved PER ROW at render time (see setLegendActions). */
3796
3929
  this.legendActions = null;
3930
+ /** Host-contributed callout bubbles, resolved PER ROW at render time (see setLegendCallouts). */
3931
+ this.legendCallouts = null;
3797
3932
  /** Chrome-tooltip disposers, per row id — a tip open at removal must not outlive its row. */
3798
3933
  this.rowTips = /* @__PURE__ */ new Map();
3799
3934
  /** Same, for the contributed extras only (rebuilt independently by setLegendActions). */
3800
3935
  this.extrasTips = /* @__PURE__ */ new Map();
3936
+ /** Same, for the contributed callouts only (rebuilt independently by setLegendCallouts). */
3937
+ this.calloutTips = /* @__PURE__ */ new Map();
3801
3938
  /** Open "Move to" menu (kept so it can be torn down). */
3802
3939
  this.moveMenu = null;
3803
3940
  this.moveTargets = [];
@@ -3955,6 +4092,42 @@ var InputsUI = class {
3955
4092
  extrasEl.appendChild(btn2);
3956
4093
  }
3957
4094
  }
4095
+ /**
4096
+ * Wire the host-contributed callout bubbles. Same contract as
4097
+ * {@link setLegendActions}: re-calling replaces the provider and re-projects the
4098
+ * rows already on screen.
4099
+ */
4100
+ setLegendCallouts(provider) {
4101
+ this.legendCallouts = provider;
4102
+ for (const row of this.rows.values()) this.renderCallouts(row);
4103
+ }
4104
+ /** (Re)build one row's callout bubbles — tinted icon circles beside the title whose
4105
+ * click (when the view carries content) deploys a panel of text and actions. */
4106
+ renderCallouts(row) {
4107
+ this.disposeTips(this.calloutTips, row.id);
4108
+ for (const bubble of row.callouts) bubble.destroy();
4109
+ row.callouts = [];
4110
+ row.calloutsEl.replaceChildren();
4111
+ const views = this.legendCallouts?.(row.id) ?? [];
4112
+ row.calloutsEl.style.display = legendCalloutsDisplay(row.highlighted, views.length > 0);
4113
+ for (const view of views) {
4114
+ const bubble = new CalloutBubble({
4115
+ icon: view.icon,
4116
+ background: view.background,
4117
+ ...view.color !== void 0 ? { color: view.color } : {},
4118
+ label: view.tooltip,
4119
+ ...view.content !== void 0 ? { panel: view.content } : {},
4120
+ // The panel portals into the plot host and self-tokens: it must work on
4121
+ // a bare chart, where no `.vela-ui` token ancestor exists.
4122
+ host: this.container,
4123
+ theme: () => this.theme
4124
+ });
4125
+ bubble.el.dataset.legendCallout = view.id;
4126
+ this.tip(this.calloutTips, row.id, bubble.el, view.tooltip);
4127
+ row.callouts.push(bubble);
4128
+ row.calloutsEl.appendChild(bubble.el);
4129
+ }
4130
+ }
3958
4131
  /** Reposition the per-pane legend containers after a layout change. */
3959
4132
  reposition() {
3960
4133
  for (const [paneId, lg] of this.legends) this.positionLegend(lg, paneId);
@@ -4122,6 +4295,7 @@ var InputsUI = class {
4122
4295
  }
4123
4296
  positionLegend(lg, paneId) {
4124
4297
  const bounds = this.paneBoundsOf ? this.paneBoundsOf(paneId) : { top: 0, height: Infinity };
4298
+ lg.toggleAttribute(LEGEND_AT_TOP_ATTR, bounds.top === 0);
4125
4299
  lg.style.display = bounds.height < 4 || !this.titlesVisible ? "none" : "flex";
4126
4300
  const collapsed = this.paneCollapse.has(paneId);
4127
4301
  const masterId = this.paneCollapse.get(paneId) ?? null;
@@ -4197,13 +4371,13 @@ var InputsUI = class {
4197
4371
  }
4198
4372
  /** Create or update an indicator's legend row (in the legend for its pane). */
4199
4373
  upsert(id, title, inputs, values, paneId = "price", opts = {}) {
4200
- const settingsTitle = opts.settingsTitle ?? title;
4201
4374
  const existing = this.rows.get(id);
4202
4375
  if (existing) {
4203
4376
  existing.title = title;
4204
- existing.settingsTitle = settingsTitle;
4205
4377
  existing.inputs = inputs;
4206
4378
  existing.values = { ...values };
4379
+ existing.props = opts.props ?? [];
4380
+ existing.propValues = { ...opts.propValues ?? {} };
4207
4381
  existing.titleEl.textContent = title;
4208
4382
  if (existing.paneId !== paneId) {
4209
4383
  existing.paneId = paneId;
@@ -4255,6 +4429,9 @@ var InputsUI = class {
4255
4429
  titleWrap.appendChild(beta);
4256
4430
  }
4257
4431
  el.appendChild(titleWrap);
4432
+ const calloutsEl = document.createElement("span");
4433
+ calloutsEl.style.cssText = `display:none;align-items:center;gap:4px;flex:none;margin-left:${LEGEND_TITLE_STATUS_GAP_PX}px;`;
4434
+ el.appendChild(calloutsEl);
4258
4435
  el.appendChild(statusEl);
4259
4436
  const valuesEl = document.createElement("span");
4260
4437
  valuesEl.style.cssText = `display:none;align-items:center;gap:5px;margin-left:${LEGEND_TITLE_VALUES_GAP_PX}px;white-space:nowrap;font-variant-numeric:tabular-nums;`;
@@ -4271,13 +4448,13 @@ var InputsUI = class {
4271
4448
  eye.className = "vela-ind-ctl";
4272
4449
  eye.style.cssText = LEGEND_CTL_CSS;
4273
4450
  eye.addEventListener("click", () => {
4274
- const row = this.rows.get(id);
4275
- this.onToggleVisible?.(id, Boolean(row?.hidden));
4451
+ const row2 = this.rows.get(id);
4452
+ this.onToggleVisible?.(id, Boolean(row2?.hidden));
4276
4453
  });
4277
4454
  controlsEl.appendChild(eye);
4278
4455
  eyeEl = eye;
4279
4456
  }
4280
- if (inputs.length > 0) {
4457
+ if (inputs.length > 0 || (opts.props ?? []).length > 0) {
4281
4458
  const gear = document.createElement("button");
4282
4459
  gear.type = "button";
4283
4460
  gear.setAttribute("aria-label", "Settings");
@@ -4317,7 +4494,9 @@ var InputsUI = class {
4317
4494
  controlsEl.appendChild(close);
4318
4495
  el.appendChild(controlsEl);
4319
4496
  this.attach(this.legendFor(paneId), el, !!opts.native);
4320
- this.rows.set(id, { id, title, settingsTitle, inputs, values: { ...values }, el, titleEl, statusEl, valuesEl, plotValues: [], plotValuesKey: "", showValues: null, highlighted: false, paneId, hidden: false, eyeEl, controlsEl, extrasEl, native: !!opts.native });
4497
+ const row = { id, title, inputs, values: { ...values }, props: opts.props ?? [], propValues: { ...opts.propValues ?? {} }, el, titleEl, statusEl, valuesEl, plotValues: [], plotValuesKey: "", showValues: null, highlighted: false, paneId, hidden: false, eyeEl, controlsEl, extrasEl, calloutsEl, callouts: [], native: !!opts.native };
4498
+ this.rows.set(id, row);
4499
+ this.renderCallouts(row);
4321
4500
  this.syncFoldToggle();
4322
4501
  }
4323
4502
  /** Place a row in its pane's legend — native rows PREPEND (pinned to the top), Pine rows append. */
@@ -4326,9 +4505,11 @@ var InputsUI = class {
4326
4505
  else container.appendChild(el);
4327
4506
  }
4328
4507
  /** Reflect programmatic input changes (so a re-opened dialog shows current values). */
4329
- setValues(id, values) {
4508
+ setValues(id, values, props) {
4330
4509
  const row = this.rows.get(id);
4331
- if (row) row.values = { ...row.values, ...values };
4510
+ if (!row) return;
4511
+ row.values = { ...row.values, ...values };
4512
+ if (props) row.propValues = { ...row.propValues, ...props };
4332
4513
  }
4333
4514
  /**
4334
4515
  * Reflect an indicator's live status in its legend row: `'loading'` shows three pulsing
@@ -4410,8 +4591,13 @@ var InputsUI = class {
4410
4591
  syncRowActions(row) {
4411
4592
  const open = row.highlighted;
4412
4593
  row.controlsEl.style.display = open || row.hidden ? "inline-flex" : "none";
4413
- if (open) row.el.appendChild(row.statusEl);
4414
- else row.el.insertBefore(row.statusEl, row.valuesEl);
4594
+ if (open) {
4595
+ row.el.appendChild(row.statusEl);
4596
+ for (const bubble of row.callouts) bubble.hidePanel();
4597
+ } else {
4598
+ row.el.insertBefore(row.statusEl, row.valuesEl);
4599
+ }
4600
+ row.calloutsEl.style.display = legendCalloutsDisplay(open, row.callouts.length > 0);
4415
4601
  for (const child of Array.from(row.controlsEl.children)) {
4416
4602
  if (!(child instanceof HTMLElement) || child === row.eyeEl) continue;
4417
4603
  if (child === row.extrasEl) {
@@ -4429,10 +4615,12 @@ var InputsUI = class {
4429
4615
  }
4430
4616
  remove(id) {
4431
4617
  const row = this.rows.get(id);
4618
+ for (const bubble of row?.callouts ?? []) bubble.destroy();
4432
4619
  row?.el.remove();
4433
4620
  this.rows.delete(id);
4434
4621
  this.disposeTips(this.rowTips, id);
4435
4622
  this.disposeTips(this.extrasTips, id);
4623
+ this.disposeTips(this.calloutTips, id);
4436
4624
  if (this.selectedId === id) this.selectedId = null;
4437
4625
  if (this.inputsDialog.openId === id) this.inputsDialog.close();
4438
4626
  this.syncFoldToggle();
@@ -4452,6 +4640,8 @@ var InputsUI = class {
4452
4640
  this.rowMenu = null;
4453
4641
  for (const id of [...this.rowTips.keys()]) this.disposeTips(this.rowTips, id);
4454
4642
  for (const id of [...this.extrasTips.keys()]) this.disposeTips(this.extrasTips, id);
4643
+ for (const id of [...this.calloutTips.keys()]) this.disposeTips(this.calloutTips, id);
4644
+ for (const row of this.rows.values()) for (const bubble of row.callouts) bubble.destroy();
4455
4645
  for (const lg of this.legends.values()) lg.remove();
4456
4646
  this.legends.clear();
4457
4647
  this.rows.clear();
@@ -4497,7 +4687,7 @@ var ICONS = {
4497
4687
  };
4498
4688
  var STYLE_ID2 = "vela-pane-controls";
4499
4689
  var ICON_PX = 12;
4500
- var CLUSTER_PILL = "rgba(0,0,0,0.28)";
4690
+ var CLUSTER_PILL = "rgba(0,0,0,0.65)";
4501
4691
  function ensureStyles2() {
4502
4692
  if (typeof document === "undefined" || document.getElementById(STYLE_ID2)) return;
4503
4693
  const st = document.createElement("style");
@@ -4517,8 +4707,12 @@ var PaneControls = class {
4517
4707
  this.deps = deps;
4518
4708
  this.clusters = /* @__PURE__ */ new Map();
4519
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;
4520
4713
  /** Reveal the cluster for the pane under the cursor, resolved from the pointer's y in the plot. */
4521
4714
  this.onPlotMove = (e) => {
4715
+ if (this.suspended) return;
4522
4716
  const rect = this.plot.getBoundingClientRect();
4523
4717
  const y = e.clientY - rect.top;
4524
4718
  let hit = null;
@@ -4590,7 +4784,12 @@ var PaneControls = class {
4590
4784
  }
4591
4785
  if (p.count > 1) {
4592
4786
  cluster.appendChild(
4593
- 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
+ })
4594
4793
  );
4595
4794
  }
4596
4795
  }
@@ -4627,17 +4826,18 @@ var PaneControls = class {
4627
4826
  }
4628
4827
  const hovered = id === this.hoverPaneId;
4629
4828
  const hasButtons = cluster.children.length > 0;
4630
- 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;
4631
4831
  cluster.style.right = `${rightPx}px`;
4632
4832
  cluster.style.top = p.collapsed ? `${p.top + Math.max(1, Math.round((p.height - 24) / 2))}px` : `${p.top + 4}px`;
4633
4833
  cluster.style.display = visible ? "flex" : "none";
4634
4834
  if (!visible) continue;
4635
- const soloExpand = p.collapsed && !hovered;
4636
- cluster.style.background = soloExpand ? "transparent" : CLUSTER_PILL;
4835
+ const soloChip = stateChipRole != null && !hovered;
4836
+ cluster.style.background = soloChip ? "transparent" : CLUSTER_PILL;
4637
4837
  for (const child of cluster.children) {
4638
4838
  const btn2 = child;
4639
4839
  btn2.style.display = "inline-flex";
4640
- btn2.style.visibility = soloExpand && btn2.dataset.role !== "collapse" ? "hidden" : "visible";
4840
+ btn2.style.visibility = soloChip && btn2.dataset.role !== stateChipRole ? "hidden" : "visible";
4641
4841
  }
4642
4842
  }
4643
4843
  }
@@ -4647,6 +4847,14 @@ var PaneControls = class {
4647
4847
  this.hoverPaneId = paneId;
4648
4848
  this.reposition();
4649
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
+ }
4650
4858
  destroy() {
4651
4859
  this.plot.removeEventListener("pointermove", this.onPlotMove);
4652
4860
  this.plot.removeEventListener("pointerleave", this.onPlotLeave);
@@ -4797,131 +5005,6 @@ var AxisScaleButtons = class {
4797
5005
  }
4798
5006
  };
4799
5007
 
4800
- // src/renderers/shared/TableOverlay.ts
4801
- var SIZE_PX = {
4802
- auto: 13,
4803
- tiny: 10,
4804
- small: 11,
4805
- normal: 13,
4806
- large: 16,
4807
- huge: 20
4808
- };
4809
- var TableOverlay = class {
4810
- constructor(container, theme, paneBounds) {
4811
- this.container = container;
4812
- this.theme = theme;
4813
- this.paneBounds = paneBounds;
4814
- this.lastTables = [];
4815
- if (getComputedStyle(container).position === "static") container.style.position = "relative";
4816
- this.root = document.createElement("div");
4817
- Object.assign(this.root.style, {
4818
- position: "absolute",
4819
- inset: "0",
4820
- pointerEvents: "none",
4821
- overflow: "hidden",
4822
- zIndex: "3"
4823
- });
4824
- container.appendChild(this.root);
4825
- }
4826
- update(tables) {
4827
- this.lastTables = tables;
4828
- this.root.replaceChildren();
4829
- for (const t of tables) this.root.appendChild(this.renderTable(t));
4830
- }
4831
- /** Re-render at the current pane geometry — after layout settles or on resize. */
4832
- reposition() {
4833
- if (this.root.isConnected) this.update(this.lastTables);
4834
- }
4835
- /** Show/hide the whole overlay. Tables anchor to pane corners, not to bars, so unlike the
4836
- * series content they DON'T vanish with an emptied chart — the loading state hides them. */
4837
- setVisible(visible) {
4838
- this.root.style.display = visible ? "" : "none";
4839
- }
4840
- destroy() {
4841
- this.root.remove();
4842
- }
4843
- renderTable(t) {
4844
- const wrap = document.createElement("div");
4845
- wrap.style.position = "absolute";
4846
- this.anchor(wrap, t.position, this.paneBounds(t.paneId));
4847
- const table = document.createElement("table");
4848
- Object.assign(table.style, {
4849
- borderCollapse: "collapse",
4850
- background: t.bgColor ?? "transparent",
4851
- fontFamily: this.theme.fontFamily || "sans-serif",
4852
- // Frame as an inset shadow (not a `border`) so it stays independent of the
4853
- // collapsed cell borders even when frame_width != border_width.
4854
- boxShadow: t.frameColor && t.frameWidth > 0 ? `inset 0 0 0 ${t.frameWidth}px ${t.frameColor}` : "none",
4855
- border: "none",
4856
- tableLayout: "auto",
4857
- // Re-enable pointer events on the table only (root is none) so cell tooltips work.
4858
- pointerEvents: "auto"
4859
- });
4860
- const span = /* @__PURE__ */ new Map();
4861
- const skip = /* @__PURE__ */ new Set();
4862
- for (const m of t.merges) {
4863
- span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
4864
- for (let r = m.startRow; r <= m.endRow; r += 1) {
4865
- for (let c = m.startCol; c <= m.endCol; c += 1) {
4866
- if (r !== m.startRow || c !== m.startCol) skip.add(`${r}:${c}`);
4867
- }
4868
- }
4869
- }
4870
- const cellBorder = t.borderColor && t.borderWidth > 0 ? `${t.borderWidth}px solid ${t.borderColor}` : "none";
4871
- for (let r = 0; r < t.rows; r += 1) {
4872
- const tr = document.createElement("tr");
4873
- for (let c = 0; c < t.columns; c += 1) {
4874
- const cell = t.cells[r]?.[c] ?? null;
4875
- if (skip.has(`${r}:${c}`) || cell?.merged) continue;
4876
- const td = document.createElement("td");
4877
- const sp = span.get(`${r}:${c}`);
4878
- if (sp) {
4879
- if (sp.cs > 1) td.colSpan = sp.cs;
4880
- if (sp.rs > 1) td.rowSpan = sp.rs;
4881
- }
4882
- Object.assign(td.style, {
4883
- border: cellBorder,
4884
- padding: "2px 6px",
4885
- background: cell?.bgColor ?? "transparent",
4886
- color: cell?.textColor ?? this.theme.textColor,
4887
- textAlign: cell?.hAlign ?? "center",
4888
- verticalAlign: cell?.vAlign === "top" ? "top" : cell?.vAlign === "bottom" ? "bottom" : "middle",
4889
- fontSize: `${SIZE_PX[cell?.textSize ?? "normal"]}px`,
4890
- fontFamily: cell?.fontFamily === "monospace" ? "monospace" : "inherit",
4891
- fontWeight: cell?.bold ? "bold" : "normal",
4892
- fontStyle: cell?.italic ? "italic" : "normal",
4893
- whiteSpace: "pre-line"
4894
- });
4895
- if (cell?.tooltip) td.title = cell.tooltip;
4896
- td.textContent = cell?.text ?? "";
4897
- tr.appendChild(td);
4898
- }
4899
- table.appendChild(tr);
4900
- }
4901
- wrap.appendChild(table);
4902
- return wrap;
4903
- }
4904
- /**
4905
- * Position the wrapper at a Pine `position.*` corner/edge of the table's PANE
4906
- * (not the whole chart), inset past the right price axis so it never overlaps
4907
- * the Y-axis labels.
4908
- */
4909
- anchor(el, position, b) {
4910
- const m = 6;
4911
- const containerH = this.root.clientHeight || this.container.clientHeight;
4912
- const paneBottom = b.top + b.height;
4913
- if (position.startsWith("top")) el.style.top = `${b.top + m}px`;
4914
- else if (position.startsWith("bottom")) el.style.bottom = `${Math.max(0, containerH - paneBottom) + m}px`;
4915
- else el.style.top = `${b.top + b.height / 2}px`;
4916
- if (position.endsWith("left")) el.style.left = `${m}px`;
4917
- else if (position.endsWith("right")) el.style.right = `${b.rightAxis + m}px`;
4918
- else el.style.left = `calc(50% - ${b.rightAxis / 2}px)`;
4919
- const tx = position.endsWith("center") ? "-50%" : "0";
4920
- const ty = position.startsWith("middle") ? "-50%" : "0";
4921
- if (tx !== "0" || ty !== "0") el.style.transform = `translate(${tx}, ${ty})`;
4922
- }
4923
- };
4924
-
4925
5008
  // src/renderers/native/capabilities.ts
4926
5009
  var NATIVE_CAPABILITIES = {
4927
5010
  panes: true,
@@ -4942,7 +5025,7 @@ var NATIVE_CAPABILITIES = {
4942
5025
  drawingDepth: true,
4943
5026
  // drawings share the series' z space (backend-composited interleave layers)
4944
5027
  tables: true,
4945
- // reuses the DOM TableOverlay
5028
+ // canvas-painted into the owning indicator's interleave slice
4946
5029
  trades: true,
4947
5030
  // strategy order-fill markers (arrows + labels + fill-price ticks)
4948
5031
  inputsUI: true
@@ -4973,6 +5056,9 @@ function candleTier(spacing) {
4973
5056
  if (spacing < CANDLE_BODY_MIN_SPACING) return "wick";
4974
5057
  return "full";
4975
5058
  }
5059
+ function snapY(yCss, dpr) {
5060
+ return Math.round(yCss * dpr) / dpr;
5061
+ }
4976
5062
  function candleGeometry(xCss, spacing, dpr, bodyScale = 1) {
4977
5063
  const wickDev = Math.max(1, Math.round(wickWidth(spacing) * dpr));
4978
5064
  const wickLeftDev = Math.round(xCss * dpr - wickDev / 2);
@@ -5778,8 +5864,12 @@ var WebGL2Backend = class {
5778
5864
  return sc === pane.scale ? pane : { ...pane, scale: sc };
5779
5865
  };
5780
5866
  b.alpha = this.modelAlpha;
5781
- for (const m of models) for (const bgSpan of m.backgrounds) this.emitBackground(b, bgSpan, pane, coords);
5782
- for (const m of models) for (const f of m.fills) this.emitFill(b, m, f, effPane(m), coords, i0, i1, scene.offsetOf(m.id));
5867
+ for (const m of models) for (const bgSpan of m.backgrounds) if (bgSpan.overlay !== true) this.emitBackground(b, bgSpan, pane, coords);
5868
+ if (isPrice) {
5869
+ for (const m of scene.indicators.values()) {
5870
+ for (const bgSpan of m.backgrounds) if (bgSpan.overlay === true) this.emitBackground(b, bgSpan, pane, coords);
5871
+ }
5872
+ }
5783
5873
  const drawCandles = isPrice && !scene.candlesHidden;
5784
5874
  let candleDrawn = false;
5785
5875
  for (const m of models) {
@@ -5793,15 +5883,26 @@ var WebGL2Backend = class {
5793
5883
  b.alpha = this.modelAlpha;
5794
5884
  const off = scene.offsetOf(m.id);
5795
5885
  const mp = effPane(m);
5796
- for (const s of m.series) this.emitSeries(b, s, mp, coords, i0, i1, theme, off);
5886
+ for (const f of m.fills) if (f.overlay !== true) this.emitFill(b, m, f, mp, coords, i0, i1, off);
5887
+ for (const s of m.series) if (s.overlay !== true) this.emitSeries(b, s, mp, coords, i0, i1, theme, off);
5797
5888
  }
5798
5889
  if (drawCandles && !candleDrawn) {
5799
5890
  drawSlicesUpTo(scene.candleZ);
5800
5891
  b.alpha = this.candleStructureAlpha;
5801
5892
  this.emitPriceSeries(b, scene, i0, i1, coords, pane, theme, barColorMap, dataW);
5802
5893
  }
5803
- drawSlicesUpTo(Infinity);
5804
5894
  b.alpha = this.modelAlpha;
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
+ }
5900
+ for (const m of scene.indicators.values()) {
5901
+ const off = scene.offsetOf(m.id);
5902
+ for (const s of m.series) if (s.overlay === true) this.emitSeries(b, s, pane, coords, i0, i1, theme, off);
5903
+ }
5904
+ }
5905
+ drawSlicesUpTo(Infinity);
5805
5906
  for (const m of models) {
5806
5907
  const mp = effPane(m);
5807
5908
  for (const pl of m.priceLines) this.emitHline(b, pl, mp, coords, dataW, theme);
@@ -5848,7 +5949,7 @@ var WebGL2Backend = class {
5848
5949
  for (const pane of scene.orderedPanes()) {
5849
5950
  const b = this.glowBatch;
5850
5951
  b.reset();
5851
- this.emitGlowSources(b, scene.indicatorsForPane(pane.id), pane, coords, i0, i1, (id) => scene.offsetOf(id));
5952
+ this.emitGlowSources(b, scene, pane, coords, i0, i1);
5852
5953
  if (b.vertexCount === 0) continue;
5853
5954
  const topH = Math.round(pane.bounds.top * dpr * 0.5);
5854
5955
  const botH = Math.round((pane.bounds.top + pane.bounds.height) * dpr * 0.5);
@@ -5917,15 +6018,23 @@ var WebGL2Backend = class {
5917
6018
  gl.bindVertexArray(null);
5918
6019
  return true;
5919
6020
  }
5920
- /** The "neon" elements that glow: line/area/step lines + point markers (not candles/fills/bars). */
5921
- emitGlowSources(b, models, pane, coords, i0, i1, offsetOf = () => 0) {
5922
- for (const m of models) {
5923
- const off = offsetOf(m.id);
5924
- for (const s of m.series) {
5925
- if (!isLineLikeSeries(s) || s.visible === false) continue;
5926
- if (s.kind === "histogram" || s.kind === "columns") continue;
5927
- if (s.kind === "circles" || s.kind === "cross") this.emitPointMarkers(b, s, pane, coords, i0, i1, off);
5928
- else this.emitPolyline(b, s, pane, coords, i0, i1, s.kind === "step", off);
6021
+ /** The "neon" elements that glow: line/area/step lines + point markers (not candles/fills/bars).
6022
+ * Routing mirrors the main pass: own series per pane, force_overlay series on the price pane. */
6023
+ emitGlowSources(b, scene, pane, coords, i0, i1) {
6024
+ const emitOne = (s, off) => {
6025
+ if (!isLineLikeSeries(s) || s.visible === false) return;
6026
+ if (s.kind === "histogram" || s.kind === "columns") return;
6027
+ if (s.kind === "circles" || s.kind === "cross") this.emitPointMarkers(b, s, pane, coords, i0, i1, off);
6028
+ else this.emitPolyline(b, s, pane, coords, i0, i1, s.kind === "step", off);
6029
+ };
6030
+ for (const m of scene.indicatorsForPane(pane.id)) {
6031
+ const off = scene.offsetOf(m.id);
6032
+ for (const s of m.series) if (s.overlay !== true) emitOne(s, off);
6033
+ }
6034
+ if (pane.kind === "price") {
6035
+ for (const m of scene.indicators.values()) {
6036
+ const off = scene.offsetOf(m.id);
6037
+ for (const s of m.series) if (s.overlay === true) emitOne(s, off);
5929
6038
  }
5930
6039
  }
5931
6040
  }
@@ -6188,12 +6297,12 @@ var WebGL2Backend = class {
6188
6297
  if (drawBody) {
6189
6298
  const oY = coords.priceToY(bar.open, pane.scale, pane.bounds);
6190
6299
  const cY = coords.priceToY(bar.close, pane.scale, pane.bounds);
6191
- bodyTop = Math.min(oY, cY);
6192
- 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);
6193
6302
  }
6194
6303
  if (cs.wickVisible) {
6195
- const hY = coords.priceToY(bar.high, pane.scale, pane.bounds);
6196
- 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);
6197
6306
  b.alpha = this.candleStructureAlpha;
6198
6307
  const wCol = parseColor((isUp ? cs.wickUpColor : cs.wickDownColor) ?? (drawBody ? dir : bodyColorStr));
6199
6308
  if (drawBody) {
@@ -6208,9 +6317,9 @@ var WebGL2Backend = class {
6208
6317
  b.alpha = this.candleBodyAlpha;
6209
6318
  b.rect(g.bodyX, bodyTop, g.bodyW, bodyH, c);
6210
6319
  }
6211
- if (cs.borderVisible || bc || fading && cs.bodyVisible) {
6320
+ if (cs.borderVisible || fading && cs.bodyVisible) {
6212
6321
  b.alpha = this.candleStructureAlpha;
6213
- const bord = cs.borderVisible || bc ? parseColor((isUp ? cs.borderUpColor : cs.borderDownColor) ?? dir) : c;
6322
+ const bord = cs.borderVisible ? parseColor((isUp ? cs.borderUpColor : cs.borderDownColor) ?? bodyColorStr) : c;
6214
6323
  const bw = Math.max(0, g.bodyW - 1);
6215
6324
  const bh = Math.max(0, bodyH - 1);
6216
6325
  b.rectStroke(g.bodyX + 0.5, bodyTop + 0.5, bw, bh, 1, bord);
@@ -7526,9 +7635,10 @@ var SceneGraph = class {
7526
7635
  * so each indicator arrives behind the candles (and behind older indicators);
7527
7636
  * `setIndicatorZ`/`bringToFront`/`sendToBack` change it. */
7528
7637
  this.seriesZ = /* @__PURE__ */ new Map();
7529
- /** Per-pane raster layers of user drawings interleaved into the series stack each is a
7638
+ /** Per-pane raster layers of drawings interleaved into the series stack (each
7639
+ * indicator's Pine drawings at its model's z, plus in-stack user drawings) — each a
7530
7640
  * prepainted canvas the backend composites just before the series carrying `beforeZ`.
7531
- * Rebuilt by the renderer per data frame; empty when every drawing sits over the stack. */
7641
+ * Rebuilt by the renderer per data frame. */
7532
7642
  this.drawingSlices = /* @__PURE__ */ new Map();
7533
7643
  /** Per-model index offset: the chart bar index of the model's `anchorTime` — its
7534
7644
  * index-aligned payloads (dense series arrays, `bar_index` drawings) count from that
@@ -7762,8 +7872,12 @@ var Canvas2dBackend = class {
7762
7872
  return sc === pane.scale ? pane : { ...pane, scale: sc };
7763
7873
  };
7764
7874
  ctx.globalAlpha = this.modelAlpha;
7765
- for (const m of models) for (const bg of m.backgrounds) this.drawBackground(ctx, bg, pane, coords);
7766
- for (const m of models) for (const f of m.fills) this.drawFill(ctx, m, f, effPane(m), coords, i0, i1, scene.offsetOf(m.id));
7875
+ for (const m of models) for (const bg of m.backgrounds) if (bg.overlay !== true) this.drawBackground(ctx, bg, pane, coords);
7876
+ if (isPrice) {
7877
+ for (const m of scene.indicators.values()) {
7878
+ for (const bg of m.backgrounds) if (bg.overlay === true) this.drawBackground(ctx, bg, pane, coords);
7879
+ }
7880
+ }
7767
7881
  const slices = scene.drawingSlices.get(pane.id) ?? [];
7768
7882
  let si = 0;
7769
7883
  const drawSlicesUpTo = (z) => {
@@ -7785,13 +7899,25 @@ var Canvas2dBackend = class {
7785
7899
  ctx.globalAlpha = this.modelAlpha;
7786
7900
  const off = scene.offsetOf(m.id);
7787
7901
  const mp = effPane(m);
7788
- for (const s of m.series) this.drawSeries(ctx, s, mp, coords, i0, i1, theme, off);
7902
+ for (const f of m.fills) if (f.overlay !== true) this.drawFill(ctx, m, f, mp, coords, i0, i1, off);
7903
+ for (const s of m.series) if (s.overlay !== true) this.drawSeries(ctx, s, mp, coords, i0, i1, theme, off);
7789
7904
  }
7790
7905
  if (drawCandles && !candleDrawn) {
7791
7906
  drawSlicesUpTo(scene.candleZ);
7792
7907
  ctx.globalAlpha = this.candleStructureAlpha;
7793
7908
  this.drawPriceSeries(ctx, scene, i0, i1, coords, pane, theme, barColorMap, dataW);
7794
7909
  }
7910
+ if (isPrice) {
7911
+ ctx.globalAlpha = this.modelAlpha;
7912
+ for (const m of scene.indicators.values()) {
7913
+ const off = scene.offsetOf(m.id);
7914
+ for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0, i1, off);
7915
+ }
7916
+ for (const m of scene.indicators.values()) {
7917
+ const off = scene.offsetOf(m.id);
7918
+ for (const s of m.series) if (s.overlay === true) this.drawSeries(ctx, s, pane, coords, i0, i1, theme, off);
7919
+ }
7920
+ }
7795
7921
  drawSlicesUpTo(Infinity);
7796
7922
  ctx.globalAlpha = this.modelAlpha;
7797
7923
  for (const m of models) {
@@ -8036,13 +8162,13 @@ var Canvas2dBackend = class {
8036
8162
  if (drawBody) {
8037
8163
  const oY = coords.priceToY(b.open, pane.scale, pane.bounds);
8038
8164
  const cY = coords.priceToY(b.close, pane.scale, pane.bounds);
8039
- top = Math.min(oY, cY);
8040
- bodyH = Math.max(1, Math.abs(cY - oY));
8165
+ top = snapY(Math.min(oY, cY), coords.dpr);
8166
+ bodyH = Math.max(1 / coords.dpr, snapY(Math.max(oY, cY), coords.dpr) - top);
8041
8167
  }
8042
8168
  if (cs.wickVisible) {
8043
8169
  const wick = (up ? cs.wickUpColor : cs.wickDownColor) ?? (drawBody ? dir : color);
8044
- const hY = coords.priceToY(b.high, pane.scale, pane.bounds);
8045
- const lY = coords.priceToY(b.low, pane.scale, pane.bounds);
8170
+ const hY = snapY(coords.priceToY(b.high, pane.scale, pane.bounds), coords.dpr);
8171
+ const lY = snapY(coords.priceToY(b.low, pane.scale, pane.bounds), coords.dpr);
8046
8172
  ctx.globalAlpha = this.candleStructureAlpha;
8047
8173
  ctx.strokeStyle = wick;
8048
8174
  ctx.lineWidth = g.wickW;
@@ -8064,9 +8190,9 @@ var Canvas2dBackend = class {
8064
8190
  ctx.fillStyle = color;
8065
8191
  ctx.fillRect(g.bodyX, top, g.bodyW, bodyH);
8066
8192
  }
8067
- if (cs.borderVisible || bc || fading && cs.bodyVisible) {
8193
+ if (cs.borderVisible || fading && cs.bodyVisible) {
8068
8194
  ctx.globalAlpha = this.candleStructureAlpha;
8069
- ctx.strokeStyle = cs.borderVisible || bc ? (up ? cs.borderUpColor : cs.borderDownColor) ?? dir : color;
8195
+ ctx.strokeStyle = cs.borderVisible ? (up ? cs.borderUpColor : cs.borderDownColor) ?? color : color;
8070
8196
  ctx.lineWidth = 1;
8071
8197
  const bw = Math.max(0, g.bodyW - 1);
8072
8198
  const bh = Math.max(0, bodyH - 1);
@@ -8489,6 +8615,19 @@ function autoFontSize(lines, boxW, boxH, bold) {
8489
8615
 
8490
8616
  // src/renderers/shared/DrawingSceneRenderer.ts
8491
8617
  var EMPTY_DRAWING_SET = { lines: [], boxes: [], labels: [], polylines: [], linefills: [] };
8618
+ function modelDrawingSet(m, overlay) {
8619
+ const want = (d) => Boolean(d.overlay) === overlay;
8620
+ return {
8621
+ lines: (m.lines ?? []).filter(want),
8622
+ boxes: (m.boxes ?? []).filter(want),
8623
+ labels: (m.labels ?? []).filter(want),
8624
+ polylines: (m.polylines ?? []).filter(want),
8625
+ linefills: (m.linefills ?? []).filter(want)
8626
+ };
8627
+ }
8628
+ function drawingSetEmpty(s) {
8629
+ return !s.lines.length && !s.boxes.length && !s.labels.length && !s.polylines.length && !s.linefills.length;
8630
+ }
8492
8631
  function fontSizePx(size) {
8493
8632
  return size === "auto" ? 12 : namedFontSize(size);
8494
8633
  }
@@ -8498,6 +8637,8 @@ var DrawingSceneRenderer = class {
8498
8637
  this.set = set;
8499
8638
  /** measureText width cache, keyed by `${font} ${text}`, persists across frames. */
8500
8639
  this.widthCache = /* @__PURE__ */ new Map();
8640
+ /** Tooltip hit-rects captured while drawing the CURRENT set (rebuilt per render). */
8641
+ this.tipRegions = [];
8501
8642
  /**
8502
8643
  * Index offset of the CURRENT set's model: its `xloc:'bar_index'` coordinates count
8503
8644
  * from the model's anchor bar, so they shift by this to land on chart logical indices.
@@ -8518,8 +8659,13 @@ var DrawingSceneRenderer = class {
8518
8659
  const s = this.set;
8519
8660
  return !s.lines.length && !s.boxes.length && !s.labels.length && !s.polylines.length && !s.linefills.length;
8520
8661
  }
8662
+ /** Tooltip hit-rects of the labels drawn by the LAST `render` call (same coords as `ctx`). */
8663
+ labelTipRegions() {
8664
+ return this.tipRegions;
8665
+ }
8521
8666
  /** Draw the whole set into `ctx` using the supplied coordinate closures. */
8522
8667
  render(ctx, W, H, xOf, yOf) {
8668
+ this.tipRegions = [];
8523
8669
  if (this.isEmpty()) return;
8524
8670
  ctx.save();
8525
8671
  this.drawLinefills(ctx, W, H, xOf, yOf);
@@ -8867,13 +9013,35 @@ var DrawingSceneRenderer = class {
8867
9013
  if (this.isPointShape(lb.style)) {
8868
9014
  if (!lb.noFill) this.drawLabelShape(ctx, lb.style, px, py, fontPx, color);
8869
9015
  if (lb.text) this.drawLabelText(ctx, lb, px, py + fontPx, fontPx);
8870
- } else if (lb.style === "none" || lb.style === "text_outline" || lb.noFill) {
8871
- if (lb.text) this.drawLabelText(ctx, lb, px, py, fontPx, lb.style === "text_outline");
9016
+ if (lb.tooltip) {
9017
+ const r = Math.max(4, fontPx * 0.6) + 3;
9018
+ this.tipRegions.push({ left: px - r, top: py - r, right: px + r, bottom: py + r, text: lb.tooltip });
9019
+ }
9020
+ } else if (lb.style === "none" || lb.style === "text_outline") {
9021
+ if (lb.text) {
9022
+ this.drawLabelText(ctx, lb, px, py, fontPx, lb.style === "text_outline");
9023
+ if (lb.tooltip) this.tipRegions.push(this.textRegion(ctx, lb, px, py, fontPx, lb.tooltip));
9024
+ }
8872
9025
  } else {
8873
- this.drawBubble(ctx, lb, px, py, fontPx, color);
9026
+ const r = this.drawBubble(ctx, lb, px, py, fontPx, color);
9027
+ if (lb.tooltip) this.tipRegions.push({ left: r.x, top: r.y, right: r.x + r.w, bottom: r.y + r.h, text: lb.tooltip });
8874
9028
  }
8875
9029
  }
8876
9030
  }
9031
+ /** Canvas font for a label's text — family + Pine `text_formatting` (bold/italic). */
9032
+ labelFont(lb, fontPx) {
9033
+ const family = lb.fontFamily === "monospace" ? "monospace" : this.deps.theme.fontFamily || "sans-serif";
9034
+ return `${lb.italic ? "italic " : ""}${lb.bold ? "bold " : ""}${fontPx}px ${family}`;
9035
+ }
9036
+ /** Hover rect of a text-only label (style none/text_outline/noFill), centered like drawLabelText. */
9037
+ textRegion(ctx, lb, cx, cy, fontPx, text) {
9038
+ const font = this.labelFont(lb, fontPx);
9039
+ const lines = (lb.text ?? "").split("\n");
9040
+ const w = Math.max(1, ...lines.map((l) => this.measure(ctx, font, l)));
9041
+ const h = fontPx * 1.25 * lines.length;
9042
+ const left = lb.textAlign === "left" ? cx : lb.textAlign === "right" ? cx - w : cx - w / 2;
9043
+ return { left, top: cy - h / 2, right: left + w, bottom: cy + h / 2, text };
9044
+ }
8877
9045
  isPointShape(style) {
8878
9046
  switch (style) {
8879
9047
  case "circle":
@@ -8892,8 +9060,7 @@ var DrawingSceneRenderer = class {
8892
9060
  }
8893
9061
  }
8894
9062
  drawLabelText(ctx, lb, cx, cy, fontPx, outline = false) {
8895
- const family = lb.fontFamily === "monospace" ? "monospace" : this.deps.theme.fontFamily || "sans-serif";
8896
- ctx.font = `${fontPx}px ${family}`;
9063
+ ctx.font = this.labelFont(lb, fontPx);
8897
9064
  ctx.textAlign = lb.textAlign === "left" ? "left" : lb.textAlign === "right" ? "right" : "center";
8898
9065
  ctx.textBaseline = "middle";
8899
9066
  const lines = lb.text.split("\n");
@@ -8973,9 +9140,9 @@ var DrawingSceneRenderer = class {
8973
9140
  ctx.fill();
8974
9141
  }
8975
9142
  }
9143
+ /** Draw a bubble label; returns the bubble body rect (for tooltip hit-testing). */
8976
9144
  drawBubble(ctx, lb, px, py, fontPx, color) {
8977
- const family = lb.fontFamily === "monospace" ? "monospace" : this.deps.theme.fontFamily || "sans-serif";
8978
- ctx.font = `${fontPx}px ${family}`;
9145
+ ctx.font = this.labelFont(lb, fontPx);
8979
9146
  const lines = (lb.text ?? "").split("\n");
8980
9147
  const padX = 6;
8981
9148
  const padY = 4;
@@ -9033,38 +9200,51 @@ var DrawingSceneRenderer = class {
9033
9200
  by = py - h - ptr;
9034
9201
  pointer = "down";
9035
9202
  }
9036
- ctx.fillStyle = color;
9037
- this.roundRect(ctx, bx, by, w, h, 4);
9038
- ctx.fill();
9039
- if (pointer !== "none") {
9040
- ctx.beginPath();
9041
- if (pointer === "down") {
9042
- ctx.moveTo(px - ptr, by + h);
9043
- ctx.lineTo(px + ptr, by + h);
9044
- ctx.lineTo(px, py);
9045
- } else if (pointer === "up") {
9046
- ctx.moveTo(px - ptr, by);
9047
- ctx.lineTo(px + ptr, by);
9048
- ctx.lineTo(px, py);
9049
- } else if (pointer === "left") {
9050
- ctx.moveTo(bx, py - ptr);
9051
- ctx.lineTo(bx, py + ptr);
9052
- ctx.lineTo(px, py);
9053
- } else {
9054
- ctx.moveTo(bx + w, py - ptr);
9055
- ctx.lineTo(bx + w, py + ptr);
9056
- ctx.lineTo(px, py);
9057
- }
9058
- ctx.closePath();
9203
+ if (!lb.noFill) {
9204
+ ctx.fillStyle = color;
9205
+ this.roundRect(ctx, bx, by, w, h, 4);
9059
9206
  ctx.fill();
9207
+ if (pointer !== "none") {
9208
+ ctx.beginPath();
9209
+ if (pointer === "down") {
9210
+ ctx.moveTo(px - ptr, by + h);
9211
+ ctx.lineTo(px + ptr, by + h);
9212
+ ctx.lineTo(px, py);
9213
+ } else if (pointer === "up") {
9214
+ ctx.moveTo(px - ptr, by);
9215
+ ctx.lineTo(px + ptr, by);
9216
+ ctx.lineTo(px, py);
9217
+ } else if (pointer === "left") {
9218
+ ctx.moveTo(bx, py - ptr);
9219
+ ctx.lineTo(bx, py + ptr);
9220
+ ctx.lineTo(px, py);
9221
+ } else {
9222
+ ctx.moveTo(bx + w, py - ptr);
9223
+ ctx.lineTo(bx + w, py + ptr);
9224
+ ctx.lineTo(px, py);
9225
+ }
9226
+ ctx.closePath();
9227
+ ctx.fill();
9228
+ }
9060
9229
  }
9061
9230
  if (lb.text) {
9062
- ctx.fillStyle = lb.textColor ?? contrastColor(color);
9063
- ctx.textAlign = "center";
9231
+ ctx.fillStyle = lb.textColor ?? (lb.noFill ? this.deps.theme.textColor : contrastColor(color));
9064
9232
  ctx.textBaseline = "middle";
9233
+ let tx;
9234
+ if (lb.textAlign === "left") {
9235
+ ctx.textAlign = "left";
9236
+ tx = bx + padX;
9237
+ } else if (lb.textAlign === "right") {
9238
+ ctx.textAlign = "right";
9239
+ tx = bx + w - padX;
9240
+ } else {
9241
+ ctx.textAlign = "center";
9242
+ tx = bx + w / 2;
9243
+ }
9065
9244
  const startY = by + padY + lineH / 2;
9066
- for (let i = 0; i < lines.length; i += 1) ctx.fillText(lines[i], bx + w / 2, startY + i * lineH);
9245
+ for (let i = 0; i < lines.length; i += 1) ctx.fillText(lines[i], tx, startY + i * lineH);
9067
9246
  }
9247
+ return { x: bx, y: by, w, h };
9068
9248
  }
9069
9249
  roundRect(ctx, x, y, w, h, r) {
9070
9250
  const rr = Math.min(r, w / 2, h / 2);
@@ -9300,7 +9480,7 @@ var ChromeRenderer = class {
9300
9480
  this.ctx = null;
9301
9481
  // The color for axis tick labels — the host-passed surface text, set each frame in render().
9302
9482
  this.axisTextColor = DARK_THEME.textColor;
9303
- // Shared Pine-drawing renderer (line/box/label/polyline/linefill); widthCache persists.
9483
+ // Shared Pine-drawing renderer, used here for autoscale geometry only; widthCache persists.
9304
9484
  this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
9305
9485
  }
9306
9486
  mount(canvas) {
@@ -9325,8 +9505,8 @@ var ChromeRenderer = class {
9325
9505
  */
9326
9506
  paneDrawingsRange(ownModels, scene, isPricePane, vr) {
9327
9507
  let dr = null;
9328
- for (const m of ownModels) dr = unionRange(dr, this.drawingsRange(this.ownDrawings(m), vr, scene.offsetOf(m.id)));
9329
- if (isPricePane) for (const m of scene.indicators.values()) dr = unionRange(dr, this.drawingsRange(this.overlayDrawings(m), vr, scene.offsetOf(m.id)));
9508
+ for (const m of ownModels) dr = unionRange(dr, this.drawingsRange(modelDrawingSet(m, false), vr, scene.offsetOf(m.id)));
9509
+ if (isPricePane) for (const m of scene.indicators.values()) dr = unionRange(dr, this.drawingsRange(modelDrawingSet(m, true), vr, scene.offsetOf(m.id)));
9330
9510
  return dr;
9331
9511
  }
9332
9512
  /** Clear the chrome canvas and draw drawings + axes + current-price line.
@@ -9358,17 +9538,6 @@ var ChromeRenderer = class {
9358
9538
  return;
9359
9539
  }
9360
9540
  const pricePane = panes.find((p) => p.kind === "price") ?? null;
9361
- for (const pane of panes) {
9362
- if (pane.collapsed) continue;
9363
- for (const m of scene.indicatorsForPane(pane.id)) {
9364
- const sc = scene.scaleFor(m, pane);
9365
- const mp = sc === pane.scale ? pane : { ...pane, scale: sc };
9366
- this.renderDrawings(ctx, coords, this.ownDrawings(m), mp, dataW, scene.offsetOf(m.id));
9367
- }
9368
- }
9369
- if (pricePane) {
9370
- for (const m of scene.indicators.values()) this.renderDrawings(ctx, coords, this.overlayDrawings(m), pricePane, dataW, scene.offsetOf(m.id));
9371
- }
9372
9541
  if (pricePane && !pricePane.collapsed && scene.tradeMarkers.visible) {
9373
9542
  for (const m of scene.indicators.values()) {
9374
9543
  if (m.trades?.length) this.renderTrades(ctx, coords, scene, theme, m.trades, pricePane, dataW);
@@ -9384,25 +9553,6 @@ var ChromeRenderer = class {
9384
9553
  this.canvas = null;
9385
9554
  this.ctx = null;
9386
9555
  }
9387
- // ── Pine-drawing helpers (own vs force_overlay routing) ──
9388
- ownDrawings(m) {
9389
- return {
9390
- lines: (m.lines ?? []).filter((d) => !d.overlay),
9391
- boxes: (m.boxes ?? []).filter((d) => !d.overlay),
9392
- labels: (m.labels ?? []).filter((d) => !d.overlay),
9393
- polylines: (m.polylines ?? []).filter((d) => !d.overlay),
9394
- linefills: (m.linefills ?? []).filter((d) => !d.overlay)
9395
- };
9396
- }
9397
- overlayDrawings(m) {
9398
- return {
9399
- lines: (m.lines ?? []).filter((d) => d.overlay),
9400
- boxes: (m.boxes ?? []).filter((d) => d.overlay),
9401
- labels: (m.labels ?? []).filter((d) => d.overlay),
9402
- polylines: (m.polylines ?? []).filter((d) => d.overlay),
9403
- linefills: (m.linefills ?? []).filter((d) => d.overlay)
9404
- };
9405
- }
9406
9556
  drawingsRange(set, vr, indexOffset = 0) {
9407
9557
  this.drawScene.setSet(set, indexOffset);
9408
9558
  if (this.drawScene.isEmpty()) return null;
@@ -9436,23 +9586,6 @@ var ChromeRenderer = class {
9436
9586
  );
9437
9587
  ctx.restore();
9438
9588
  }
9439
- renderDrawings(ctx, coords, set, pane, dataW, indexOffset = 0) {
9440
- this.drawScene.setSet(set, indexOffset);
9441
- if (this.drawScene.isEmpty()) return;
9442
- ctx.save();
9443
- ctx.translate(0, pane.bounds.top);
9444
- ctx.beginPath();
9445
- ctx.rect(0, 0, dataW, pane.bounds.height);
9446
- ctx.clip();
9447
- this.drawScene.render(
9448
- ctx,
9449
- dataW,
9450
- pane.bounds.height,
9451
- (l) => coords.logicalToX(l),
9452
- (price) => coords.priceToY(price, pane.scale, pane.bounds) - pane.bounds.top
9453
- );
9454
- ctx.restore();
9455
- }
9456
9589
  // ── axes ──
9457
9590
  drawPriceAxes(ctx, scene, coords, theme, dataW, panes) {
9458
9591
  ctx.strokeStyle = scene.style.borderColor ?? theme.borderColor;
@@ -9668,6 +9801,68 @@ function formatCountdown(ms) {
9668
9801
  return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`;
9669
9802
  }
9670
9803
 
9804
+ // src/renderers/native/chrome/LabelTooltip.ts
9805
+ var HOVER_DELAY_MS = 350;
9806
+ var LabelTooltip = class {
9807
+ constructor(plot, deps) {
9808
+ this.plot = plot;
9809
+ this.deps = deps;
9810
+ this.tip = null;
9811
+ this.timer = null;
9812
+ /** Text of the currently open OR armed tip — dedupes moves inside one label. */
9813
+ this.current = null;
9814
+ this.onMove = (e) => {
9815
+ if (e.pointerType !== "mouse") return;
9816
+ const rect = this.plot.getBoundingClientRect();
9817
+ const x = e.clientX - rect.left;
9818
+ const y = e.clientY - rect.top;
9819
+ const text = this.deps.lookup(x, y);
9820
+ if (!text) {
9821
+ this.clear();
9822
+ return;
9823
+ }
9824
+ if (text === this.current) return;
9825
+ this.clear();
9826
+ this.current = text;
9827
+ this.timer = window.setTimeout(() => this.show(text, x, y), HOVER_DELAY_MS);
9828
+ };
9829
+ this.onLeave = () => {
9830
+ this.clear();
9831
+ };
9832
+ plot.addEventListener("pointermove", this.onMove);
9833
+ plot.addEventListener("pointerleave", this.onLeave);
9834
+ plot.addEventListener("pointerdown", this.onLeave);
9835
+ }
9836
+ destroy() {
9837
+ this.plot.removeEventListener("pointermove", this.onMove);
9838
+ this.plot.removeEventListener("pointerleave", this.onLeave);
9839
+ this.plot.removeEventListener("pointerdown", this.onLeave);
9840
+ this.clear();
9841
+ }
9842
+ show(text, x, y) {
9843
+ const doc = this.plot.ownerDocument;
9844
+ const tip = doc.createElement("div");
9845
+ tip.textContent = text;
9846
+ tip.style.cssText = "position:absolute;z-index:var(--vela-z-tooltip);pointer-events:none;background:var(--vela-bg);border:1px solid var(--vela-border);color:var(--vela-fg);border-radius:var(--vela-radius-md);padding:4px 9px;box-shadow:var(--vela-shadow);font:var(--vela-font-size-md) var(--vela-font);max-width:260px;";
9847
+ applyChromeTokens(tip, this.deps.theme());
9848
+ this.plot.appendChild(tip);
9849
+ const pw = this.plot.clientWidth;
9850
+ const ph = this.plot.clientHeight;
9851
+ tip.style.left = `${Math.max(0, Math.min(x + 12, pw - tip.offsetWidth - 4))}px`;
9852
+ tip.style.top = `${Math.max(0, Math.min(y + 16, ph - tip.offsetHeight - 4))}px`;
9853
+ this.tip = tip;
9854
+ }
9855
+ clear() {
9856
+ if (this.timer !== null) {
9857
+ clearTimeout(this.timer);
9858
+ this.timer = null;
9859
+ }
9860
+ this.tip?.remove();
9861
+ this.tip = null;
9862
+ this.current = null;
9863
+ }
9864
+ };
9865
+
9671
9866
  // src/renderers/native/chrome/CrosshairRenderer.ts
9672
9867
  var CrosshairRenderer = class {
9673
9868
  constructor() {
@@ -13709,17 +13904,17 @@ function glyphIcon(glyph) {
13709
13904
  return textGlyph(String(glyph), 15);
13710
13905
  }
13711
13906
  function stampSizeIcon(size) {
13712
- return textGlyph("\u25CF", (SIZE_PX2[String(size)] ?? 13) + 4);
13907
+ return textGlyph("\u25CF", (SIZE_PX[String(size)] ?? 13) + 4);
13713
13908
  }
13714
13909
  function sizeIcon(size) {
13715
13910
  return textGlyph(String(size).charAt(0).toUpperCase(), 15);
13716
13911
  }
13717
- var SIZE_PX2 = { small: 10, normal: 13, large: 16, huge: 20 };
13912
+ var SIZE_PX = { small: 10, normal: 13, large: 16, huge: 20 };
13718
13913
  function numbersSizeIcon(size) {
13719
- return textGlyph("12", (SIZE_PX2[String(size)] ?? 13) - 1, 16.5, 'font-weight="600"');
13914
+ return textGlyph("12", (SIZE_PX[String(size)] ?? 13) - 1, 16.5, 'font-weight="600"');
13720
13915
  }
13721
13916
  function labelSizeIcon(size) {
13722
- return textGlyph("T", (SIZE_PX2[String(size)] ?? 13) + 2);
13917
+ return textGlyph("T", (SIZE_PX[String(size)] ?? 13) + 2);
13723
13918
  }
13724
13919
  function capitalize(s) {
13725
13920
  return s.charAt(0).toUpperCase() + s.slice(1);
@@ -14694,6 +14889,315 @@ var UserDrawingController = class {
14694
14889
  }
14695
14890
  };
14696
14891
 
14892
+ // src/renderers/shared/TableOverlay.ts
14893
+ var SIZE_PX2 = {
14894
+ auto: 13,
14895
+ tiny: 10,
14896
+ small: 11,
14897
+ normal: 13,
14898
+ large: 16,
14899
+ huge: 20
14900
+ };
14901
+ function fontPxOf(size) {
14902
+ if (typeof size === "number") return size > 0 ? size : SIZE_PX2.auto;
14903
+ return SIZE_PX2[size] ?? SIZE_PX2.auto;
14904
+ }
14905
+ function tableHasContent(t) {
14906
+ return t.cells.some((row) => row?.some((c) => c != null && !c.merged));
14907
+ }
14908
+ function mergeRenderPlan(t) {
14909
+ const span = /* @__PURE__ */ new Map();
14910
+ const omit = /* @__PURE__ */ new Set();
14911
+ for (const m of t.merges) {
14912
+ span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
14913
+ for (let r = m.startRow; r <= m.endRow; r += 1) {
14914
+ for (let c = m.startCol; c <= m.endCol; c += 1) {
14915
+ if (r !== m.startRow || c !== m.startCol) omit.add(`${r}:${c}`);
14916
+ }
14917
+ }
14918
+ }
14919
+ for (let r = 0; r < t.rows; r += 1) {
14920
+ for (let c = 0; c < t.columns; c += 1) {
14921
+ if (t.cells[r]?.[c]?.merged && !span.has(`${r}:${c}`)) omit.add(`${r}:${c}`);
14922
+ }
14923
+ }
14924
+ for (const key of span.keys()) omit.delete(key);
14925
+ return { span, omit };
14926
+ }
14927
+
14928
+ // src/renderers/shared/TableCanvasRenderer.ts
14929
+ var PAD_X = 6;
14930
+ var PAD_Y = 2;
14931
+ var MARGIN = 6;
14932
+ var LINE_HEIGHT = 1.2;
14933
+ function paintTable(ctx, t, args, tips) {
14934
+ if (!tableHasContent(t)) return;
14935
+ const layout = layoutTable(ctx, t, args);
14936
+ if (!layout || layout.w <= 0 || layout.h <= 0) return;
14937
+ const fw = t.frameColor && t.frameWidth > 0 ? t.frameWidth : 0;
14938
+ const { x, y } = anchorOrigin(t.position, layout.w + 2 * fw, layout.h + 2 * fw, args);
14939
+ const x0 = x + fw;
14940
+ const y0 = y + fw;
14941
+ if (t.bgColor) {
14942
+ ctx.fillStyle = t.bgColor;
14943
+ ctx.fillRect(x0, y0, layout.w, layout.h);
14944
+ }
14945
+ if (fw > 0 && t.frameColor) {
14946
+ ctx.strokeStyle = t.frameColor;
14947
+ ctx.lineWidth = fw;
14948
+ ctx.strokeRect(x + fw / 2, y + fw / 2, layout.w + fw, layout.h + fw);
14949
+ }
14950
+ const colX = [0];
14951
+ for (const w of layout.colW) colX.push(colX[colX.length - 1] + w);
14952
+ const rowY = [0];
14953
+ for (const h of layout.rowH) rowY.push(rowY[rowY.length - 1] + h);
14954
+ const prevBaseline = ctx.textBaseline;
14955
+ const prevAlign = ctx.textAlign;
14956
+ ctx.textBaseline = "middle";
14957
+ for (const box of layout.boxes) {
14958
+ const rx = x0 + colX[box.c];
14959
+ const ry = y0 + rowY[box.r];
14960
+ const rw = colX[box.c + box.cs] - colX[box.c];
14961
+ const rh = rowY[box.r + box.rs] - rowY[box.r];
14962
+ const cell = box.cell;
14963
+ if (cell.bgColor) {
14964
+ ctx.fillStyle = cell.bgColor;
14965
+ ctx.fillRect(rx, ry, rw, rh);
14966
+ }
14967
+ const text = cell.text ?? "";
14968
+ if (text.length > 0) {
14969
+ const px = fontPxOf(cell.textSize);
14970
+ ctx.font = cellFont(cell, px, args.theme);
14971
+ ctx.fillStyle = cell.textColor ?? args.theme.textColor;
14972
+ const lines = text.split("\n");
14973
+ const blockH = lines.length * px * LINE_HEIGHT;
14974
+ const blockTop = cell.vAlign === "top" ? ry + PAD_Y : cell.vAlign === "bottom" ? ry + rh - PAD_Y - blockH : ry + (rh - blockH) / 2;
14975
+ const tx = cell.hAlign === "left" ? rx + PAD_X : cell.hAlign === "right" ? rx + rw - PAD_X : rx + rw / 2;
14976
+ ctx.textAlign = cell.hAlign;
14977
+ lines.forEach((line, i) => ctx.fillText(line, tx, blockTop + (i + 0.5) * px * LINE_HEIGHT));
14978
+ }
14979
+ if (cell.tooltip) tips.push({ left: rx, top: ry, right: rx + rw, bottom: ry + rh, text: cell.tooltip });
14980
+ }
14981
+ ctx.textBaseline = prevBaseline;
14982
+ ctx.textAlign = prevAlign;
14983
+ if (t.borderColor && t.borderWidth > 0) {
14984
+ ctx.strokeStyle = t.borderColor;
14985
+ ctx.lineWidth = t.borderWidth;
14986
+ const seen = /* @__PURE__ */ new Set();
14987
+ ctx.beginPath();
14988
+ const edge = (ax, ay, bx, by) => {
14989
+ const key = `${ax},${ay},${bx},${by}`;
14990
+ if (seen.has(key)) return;
14991
+ seen.add(key);
14992
+ ctx.moveTo(ax, ay);
14993
+ ctx.lineTo(bx, by);
14994
+ };
14995
+ for (const box of layout.boxes) {
14996
+ const l = Math.round(x0 + colX[box.c]);
14997
+ const r = Math.round(x0 + colX[box.c + box.cs]);
14998
+ const tp = Math.round(y0 + rowY[box.r]);
14999
+ const bt = Math.round(y0 + rowY[box.r + box.rs]);
15000
+ edge(l, tp, r, tp);
15001
+ edge(l, bt, r, bt);
15002
+ edge(l, tp, l, bt);
15003
+ edge(r, tp, r, bt);
15004
+ }
15005
+ ctx.stroke();
15006
+ }
15007
+ }
15008
+ function layoutTable(ctx, t, args) {
15009
+ const { span, omit } = mergeRenderPlan(t);
15010
+ const colW = new Array(t.columns).fill(0);
15011
+ const rowH = new Array(t.rows).fill(0);
15012
+ const boxes = [];
15013
+ for (let r = 0; r < t.rows; r += 1) {
15014
+ for (let c = 0; c < t.columns; c += 1) {
15015
+ if (omit.has(`${r}:${c}`)) continue;
15016
+ const cell = t.cells[r]?.[c];
15017
+ if (cell == null) continue;
15018
+ const sp = span.get(`${r}:${c}`);
15019
+ boxes.push({ cell, r, c, cs: Math.min(sp?.cs ?? 1, t.columns - c), rs: Math.min(sp?.rs ?? 1, t.rows - r) });
15020
+ }
15021
+ }
15022
+ if (boxes.length === 0) return null;
15023
+ const sizeOf = (cell) => {
15024
+ const px = fontPxOf(cell.textSize);
15025
+ ctx.font = cellFont(cell, px, args.theme);
15026
+ const lines = (cell.text ?? "").split("\n");
15027
+ let maxW = 0;
15028
+ for (const line of lines) maxW = Math.max(maxW, ctx.measureText(line).width);
15029
+ let w2 = Math.ceil(maxW) + 2 * PAD_X;
15030
+ let h2 = Math.ceil(lines.length * px * LINE_HEIGHT) + 2 * PAD_Y;
15031
+ if (cell.width) w2 = Math.max(w2, cell.width / 100 * args.plotWidth);
15032
+ if (cell.height) h2 = Math.max(h2, cell.height / 100 * args.paneHeight);
15033
+ return { w: w2, h: h2 };
15034
+ };
15035
+ const spanning = [];
15036
+ for (const box of boxes) {
15037
+ const { w: w2, h: h2 } = sizeOf(box.cell);
15038
+ if (box.cs === 1) colW[box.c] = Math.max(colW[box.c], w2);
15039
+ if (box.rs === 1) rowH[box.r] = Math.max(rowH[box.r], h2);
15040
+ if (box.cs > 1 || box.rs > 1) spanning.push({ box, w: w2, h: h2 });
15041
+ }
15042
+ for (const { box, w: w2, h: h2 } of spanning) {
15043
+ if (box.cs > 1) {
15044
+ let sum = 0;
15045
+ for (let c = box.c; c < box.c + box.cs; c += 1) sum += colW[c];
15046
+ if (w2 > sum) for (let c = box.c; c < box.c + box.cs; c += 1) colW[c] += (w2 - sum) / box.cs;
15047
+ }
15048
+ if (box.rs > 1) {
15049
+ let sum = 0;
15050
+ for (let r = box.r; r < box.r + box.rs; r += 1) sum += rowH[r];
15051
+ if (h2 > sum) for (let r = box.r; r < box.r + box.rs; r += 1) rowH[r] += (h2 - sum) / box.rs;
15052
+ }
15053
+ }
15054
+ let w = 0;
15055
+ for (const cw of colW) w += cw;
15056
+ let h = 0;
15057
+ for (const rh of rowH) h += rh;
15058
+ return { colW, rowH, w, h, boxes };
15059
+ }
15060
+ function anchorOrigin(position, totalW, totalH, args) {
15061
+ let y;
15062
+ if (position.startsWith("top")) y = MARGIN;
15063
+ else if (position.startsWith("bottom")) y = args.paneHeight - MARGIN - totalH;
15064
+ else y = args.paneHeight / 2 - totalH / 2;
15065
+ let x;
15066
+ if (position.endsWith("left")) x = MARGIN;
15067
+ else if (position.endsWith("right")) x = args.plotWidth - MARGIN - totalW;
15068
+ else x = args.plotWidth / 2 - totalW / 2;
15069
+ return { x, y };
15070
+ }
15071
+ function cellFont(cell, px, theme) {
15072
+ const family = cell.fontFamily === "monospace" ? "monospace" : theme.fontFamily || "sans-serif";
15073
+ return `${cell.italic ? "italic " : ""}${cell.bold ? "bold " : ""}${px}px ${family}`;
15074
+ }
15075
+
15076
+ // src/renderers/native/drawings/IndicatorDrawingSlices.ts
15077
+ function indicatorSliceKey(z, boundaries) {
15078
+ return boundaries.find((b) => b > z) ?? Infinity;
15079
+ }
15080
+ var IndicatorDrawingSlices = class {
15081
+ constructor() {
15082
+ this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
15083
+ /** Slice canvas cache, keyed `paneId|beforeZ` — same lifecycle as the user-drawing cache. */
15084
+ this.sliceCache = /* @__PURE__ */ new Map();
15085
+ /** Tooltip hit-rects of every label drawn this frame, in plot coords (rebuilt per prepare). */
15086
+ this.tips = [];
15087
+ }
15088
+ /**
15089
+ * Rebuild the per-indicator drawing slices for this data frame. `ref` is the data
15090
+ * canvas the slices must match pixel-for-pixel (the backend composites them 1:1).
15091
+ * Runs from the renderer's data paint, just before the backend composites the scene.
15092
+ */
15093
+ prepare(scene, coords, theme, ref) {
15094
+ this.tips = [];
15095
+ const out = /* @__PURE__ */ new Map();
15096
+ if (ref.width === 0 || ref.height === 0) {
15097
+ this.sliceCache.clear();
15098
+ return out;
15099
+ }
15100
+ this.drawScene.setDeps({
15101
+ timeToLogical: (ms) => coords.timeToLogical(ms),
15102
+ barAt: (logical) => {
15103
+ const b = scene.bars[Math.round(logical)];
15104
+ return b ? { high: b.high, low: b.low } : null;
15105
+ },
15106
+ theme
15107
+ });
15108
+ const dpr = coords.dpr;
15109
+ const dataW = coords.width;
15110
+ const buckets = /* @__PURE__ */ new Map();
15111
+ const add = (paneId, beforeZ, entry) => {
15112
+ const key = `${paneId}|${beforeZ}`;
15113
+ const bucket = buckets.get(key);
15114
+ if (bucket) bucket.entries.push(entry);
15115
+ else buckets.set(key, { paneId, beforeZ, entries: [entry] });
15116
+ };
15117
+ for (const pane of scene.orderedPanes()) {
15118
+ if (pane.collapsed) continue;
15119
+ const boundaries = scene.seriesBoundaries(pane.id);
15120
+ for (const m of scene.orderedIndicatorsForPane(pane.id)) {
15121
+ const set = modelDrawingSet(m, false);
15122
+ const tables = (m.tables ?? []).filter((t) => !t.overlay);
15123
+ if (drawingSetEmpty(set) && tables.length === 0) continue;
15124
+ const sc = scene.scaleFor(m, pane);
15125
+ const mp = sc === pane.scale ? pane : { ...pane, scale: sc };
15126
+ const beforeZ = indicatorSliceKey(scene.zOf(m.id), boundaries);
15127
+ add(pane.id, beforeZ, { set, tables, pane: mp, indexOffset: scene.offsetOf(m.id) });
15128
+ }
15129
+ if (pane.kind === "price") {
15130
+ for (const m of scene.indicators.values()) {
15131
+ const set = modelDrawingSet(m, true);
15132
+ const tables = (m.tables ?? []).filter((t) => t.overlay === true);
15133
+ if (drawingSetEmpty(set) && tables.length === 0) continue;
15134
+ add(pane.id, Infinity, { set, tables, pane, indexOffset: scene.offsetOf(m.id) });
15135
+ }
15136
+ }
15137
+ }
15138
+ for (const [key, { paneId, beforeZ, entries }] of buckets) {
15139
+ let canvas = this.sliceCache.get(key);
15140
+ if (!canvas) {
15141
+ canvas = document.createElement("canvas");
15142
+ this.sliceCache.set(key, canvas);
15143
+ }
15144
+ if (canvas.width !== ref.width || canvas.height !== ref.height) {
15145
+ canvas.width = ref.width;
15146
+ canvas.height = ref.height;
15147
+ }
15148
+ const ctx = canvas.getContext("2d");
15149
+ if (!ctx) continue;
15150
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
15151
+ ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
15152
+ for (const e of entries) this.paintEntry(ctx, e, coords, dataW, theme);
15153
+ const slices = out.get(paneId) ?? [];
15154
+ slices.push({ beforeZ, canvas });
15155
+ out.set(paneId, slices);
15156
+ }
15157
+ for (const key of [...this.sliceCache.keys()]) if (!buckets.has(key)) this.sliceCache.delete(key);
15158
+ for (const slices of out.values()) slices.sort((a, b) => a.beforeZ - b.beforeZ);
15159
+ return out;
15160
+ }
15161
+ paintEntry(ctx, e, coords, dataW, theme) {
15162
+ const { pane } = e;
15163
+ const paneTips = [];
15164
+ ctx.save();
15165
+ ctx.translate(0, pane.bounds.top);
15166
+ ctx.beginPath();
15167
+ ctx.rect(0, 0, dataW, pane.bounds.height);
15168
+ ctx.clip();
15169
+ this.drawScene.setSet(e.set, e.indexOffset);
15170
+ this.drawScene.render(
15171
+ ctx,
15172
+ dataW,
15173
+ pane.bounds.height,
15174
+ (l) => coords.logicalToX(l),
15175
+ (price) => coords.priceToY(price, pane.scale, pane.bounds) - pane.bounds.top
15176
+ );
15177
+ paneTips.push(...this.drawScene.labelTipRegions());
15178
+ for (const t of e.tables) paintTable(ctx, t, { paneHeight: pane.bounds.height, plotWidth: dataW, theme }, paneTips);
15179
+ ctx.restore();
15180
+ for (const r of paneTips) {
15181
+ this.tips.push({ ...r, top: r.top + pane.bounds.top, bottom: r.bottom + pane.bounds.top });
15182
+ }
15183
+ }
15184
+ /** Tooltip of the topmost label or table cell under a plot-space point, or null. Fed by the last prepare. */
15185
+ labelTooltipAt(x, y) {
15186
+ for (let i = this.tips.length - 1; i >= 0; i -= 1) {
15187
+ const r = this.tips[i];
15188
+ if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return r.text;
15189
+ }
15190
+ return null;
15191
+ }
15192
+ };
15193
+ function mergeSlices(indicator, user) {
15194
+ const out = /* @__PURE__ */ new Map();
15195
+ for (const [paneId, slices] of indicator) out.set(paneId, [...slices]);
15196
+ for (const [paneId, slices] of user) out.set(paneId, [...out.get(paneId) ?? [], ...slices]);
15197
+ for (const slices of out.values()) slices.sort((a, b) => a.beforeZ - b.beforeZ);
15198
+ return out;
15199
+ }
15200
+
14697
15201
  // src/renderers/native/drawings/Projector.ts
14698
15202
  function createProjector(coords, paneOf, paneIdAtY, barsInRange) {
14699
15203
  return {
@@ -14745,19 +15249,8 @@ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log =
14745
15249
  for (const model of models) {
14746
15250
  const off = offsetOf(model.id);
14747
15251
  for (const s of model.series) {
14748
- if (s.kind === "candle" || s.kind === "bar") {
14749
- for (let i = i0; i <= i1; i += 1) {
14750
- const b = s.bars[i - off];
14751
- if (b) {
14752
- consider(b.high);
14753
- consider(b.low);
14754
- }
14755
- }
14756
- } else if (isLineLikeSeries(s)) {
14757
- for (let i = i0; i <= i1; i += 1) consider(s.points[i - off]?.value);
14758
- if (s.kind === "histogram" || s.kind === "columns") consider(s.style?.base ?? 0);
14759
- else if (s.style?.base != null) consider(s.style.base);
14760
- }
15252
+ if (s.overlay === true) continue;
15253
+ considerSeries(s, i0, i1, off, consider);
14761
15254
  }
14762
15255
  for (const pl of model.priceLines) consider(pl.price);
14763
15256
  }
@@ -14779,6 +15272,36 @@ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log =
14779
15272
  const span = max - min;
14780
15273
  return { min: min - span * MARGIN_BOTTOM, max: max + span * MARGIN_TOP };
14781
15274
  }
15275
+ function considerSeries(s, i0, i1, off, consider) {
15276
+ if (s.kind === "candle" || s.kind === "bar") {
15277
+ for (let i = i0; i <= i1; i += 1) {
15278
+ const b = s.bars[i - off];
15279
+ if (b) {
15280
+ consider(b.high);
15281
+ consider(b.low);
15282
+ }
15283
+ }
15284
+ } else if (isLineLikeSeries(s)) {
15285
+ for (let i = i0; i <= i1; i += 1) consider(s.points[i - off]?.value);
15286
+ if (s.kind === "histogram" || s.kind === "columns") consider(s.style?.base ?? 0);
15287
+ else if (s.style?.base != null) consider(s.style.base);
15288
+ }
15289
+ }
15290
+ function overlaySeriesRange(models, i0, i1, offsetOf = () => 0) {
15291
+ let min = Infinity;
15292
+ let max = -Infinity;
15293
+ const consider = (v) => {
15294
+ if (v != null && Number.isFinite(v)) {
15295
+ if (v < min) min = v;
15296
+ if (v > max) max = v;
15297
+ }
15298
+ };
15299
+ for (const model of models) {
15300
+ const off = offsetOf(model.id);
15301
+ for (const s of model.series) if (s.overlay === true) considerSeries(s, i0, i1, off, consider);
15302
+ }
15303
+ return min === Infinity ? null : { min, max };
15304
+ }
14782
15305
  function expandScaleByPixels(scale, heightPx, abovePx, belowPx) {
14783
15306
  if (abovePx <= 0 && belowPx <= 0) return scale;
14784
15307
  const content = heightPx - abovePx - belowPx;
@@ -15528,6 +16051,11 @@ var NativeRenderer = class {
15528
16051
  this.vpvrRenderer = new VpvrRenderer();
15529
16052
  this.resizeObserver = null;
15530
16053
  this.dprMedia = null;
16054
+ /** Plot size in INTEGER device px, as last reported by the resize observer's
16055
+ * device-pixel-content-box — the browser's own statement of how many device pixels
16056
+ * it paints the plot into. `null` until the first report or where the box type is
16057
+ * unsupported (WebKit); syncSize then falls back to rounding the client rect. */
16058
+ this.plotDeviceSize = null;
15531
16059
  this.coords = new CoordinateSystem();
15532
16060
  this.scene = new SceneGraph();
15533
16061
  // chosen at mount (WebGL2 if available, else canvas2d)
@@ -15535,6 +16063,10 @@ var NativeRenderer = class {
15535
16063
  this.glowAmount = 0;
15536
16064
  // WebGL2 neon-glow intensity (canvas2d ignores it)
15537
16065
  this.chrome = new ChromeRenderer();
16066
+ /** Prepaints each indicator's Pine drawings into interleave slices at the model's z. */
16067
+ this.indicatorSlices = new IndicatorDrawingSlices();
16068
+ /** Hover tooltips for Pine labels (canvas hit-rects collected by the chrome layer). */
16069
+ this.labelTooltip = null;
15538
16070
  this.crosshairLayer = new CrosshairRenderer();
15539
16071
  /** 1 Hz repaint pump so the price-axis countdown-to-bar-close ticks; null when off. */
15540
16072
  this.countdownTimer = null;
@@ -15545,6 +16077,8 @@ var NativeRenderer = class {
15545
16077
  this.indicatorValuesOn = true;
15546
16078
  /** Host-contributed legend actions — held here so a rebuild of the legend re-wires them. */
15547
16079
  this.legendActionsProvider = null;
16080
+ /** Host-contributed legend callouts — held here so a rebuild of the legend re-wires them. */
16081
+ this.legendCalloutsProvider = null;
15548
16082
  /** Host override of the legend's fold toggle — held here so a remount re-applies it. */
15549
16083
  this.legendOverviewAction = null;
15550
16084
  // ── keyboard navigation / accessibility (item 11) ──
@@ -15679,7 +16213,6 @@ var NativeRenderer = class {
15679
16213
  this.toggleVisibleCbs = /* @__PURE__ */ new Set();
15680
16214
  this.moveIndicatorCbs = /* @__PURE__ */ new Set();
15681
16215
  this.priceStyleCbs = /* @__PURE__ */ new Set();
15682
- this.tableOverlays = /* @__PURE__ */ new Map();
15683
16216
  this.name = "native";
15684
16217
  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"];
15685
16218
  /** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
@@ -15993,7 +16526,6 @@ var NativeRenderer = class {
15993
16526
  * hidden) on clear so a re-show picks up the current theme.
15994
16527
  */
15995
16528
  setLoading(loading) {
15996
- for (const overlay of this.tableOverlays.values()) overlay.setVisible(!loading);
15997
16529
  if (!loading || !this.wrapper) {
15998
16530
  this.loadingEl?.remove();
15999
16531
  this.loadingEl = null;
@@ -16653,6 +17185,10 @@ var NativeRenderer = class {
16653
17185
  this.plot.appendChild(this.scrollButton);
16654
17186
  this.plot.addEventListener("pointermove", this.onScrollProximityMove);
16655
17187
  this.plot.addEventListener("pointerleave", this.onScrollProximityLeave);
17188
+ this.labelTooltip = new LabelTooltip(this.plot, {
17189
+ theme: () => this.chromeTheme(),
17190
+ lookup: (x, y) => this.indicatorSlices.labelTooltipAt(x, y)
17191
+ });
16656
17192
  this.userDrawings = new UserDrawingController(this.wrapper, this.plot, this.drawingsCanvas, {
16657
17193
  projector: () => this.drawingProjector(),
16658
17194
  dpr: () => this.coords.dpr,
@@ -16692,9 +17228,10 @@ var NativeRenderer = class {
16692
17228
  this.inputsUI.setDialogHost(this.dialogHost);
16693
17229
  this.inputsUI.setSymbolPicker(this.symbolPicker);
16694
17230
  this.inputsUI.setLegendActions(this.legendActionsProvider);
17231
+ this.inputsUI.setLegendCallouts(this.legendCalloutsProvider);
16695
17232
  this.inputsUI.setLegendOverviewAction(this.legendOverviewAction);
16696
17233
  this.inputsUI.setOnChange((c) => {
16697
- for (const cb of this.inputChangeCbs) cb({ indicatorId: c.indicatorId, key: c.key, value: c.value });
17234
+ for (const cb of this.inputChangeCbs) cb({ indicatorId: c.indicatorId, key: c.key, value: c.value, ...c.kind ? { kind: c.kind } : {} });
16698
17235
  });
16699
17236
  this.inputsUI.setOnRemove((id) => {
16700
17237
  for (const cb of this.removeIndicatorCbs) cb(id);
@@ -16735,6 +17272,7 @@ var NativeRenderer = class {
16735
17272
  this.emitPaneAction({ type: "maximize", paneId, maximized });
16736
17273
  }
16737
17274
  });
17275
+ this.paneControls.setSuspended(this.layoutMode === "mobile");
16738
17276
  this.axisScaleButtons = new AxisScaleButtons(this.plot, theme, {
16739
17277
  panes: () => this.axisScaleViews(),
16740
17278
  rightAxis: () => this.rightAxisW,
@@ -16744,8 +17282,19 @@ var NativeRenderer = class {
16744
17282
  if (pane) this.setPaneLog(paneId, !paneLogScale(this.scene, pane));
16745
17283
  }
16746
17284
  });
16747
- this.resizeObserver = new ResizeObserver(() => this.resize());
17285
+ this.resizeObserver = new ResizeObserver((entries) => {
17286
+ for (const e of entries) {
17287
+ if (e.target !== this.plot) continue;
17288
+ const s = e.devicePixelContentBoxSize?.[0];
17289
+ if (s) this.plotDeviceSize = { width: s.inlineSize, height: s.blockSize };
17290
+ }
17291
+ this.resize();
17292
+ });
16748
17293
  this.resizeObserver.observe(this.wrapper);
17294
+ try {
17295
+ this.resizeObserver.observe(this.plot, { box: "device-pixel-content-box" });
17296
+ } catch {
17297
+ }
16749
17298
  this.watchDpr();
16750
17299
  this.syncSize();
16751
17300
  }
@@ -16870,8 +17419,6 @@ var NativeRenderer = class {
16870
17419
  this.inputsUI?.destroy();
16871
17420
  this.paneControls?.destroy();
16872
17421
  this.axisScaleButtons?.destroy();
16873
- for (const overlay of this.tableOverlays.values()) overlay.destroy();
16874
- this.tableOverlays.clear();
16875
17422
  this.resizeObserver?.disconnect();
16876
17423
  this.resizeObserver = null;
16877
17424
  this.dprMedia?.removeEventListener("change", this.onDprChange);
@@ -16884,6 +17431,8 @@ var NativeRenderer = class {
16884
17431
  this.settingsButton = null;
16885
17432
  this.plot?.removeEventListener("pointermove", this.onScrollProximityMove);
16886
17433
  this.plot?.removeEventListener("pointerleave", this.onScrollProximityLeave);
17434
+ this.labelTooltip?.destroy();
17435
+ this.labelTooltip = null;
16887
17436
  this.scrollButton?.remove();
16888
17437
  this.scrollButton = null;
16889
17438
  for (const l of this.extLayers) l.instance.destroy?.();
@@ -16898,6 +17447,7 @@ var NativeRenderer = class {
16898
17447
  this.attributionEl = null;
16899
17448
  this.mountContainer?.style.removeProperty("--vela-toolbar-gutter");
16900
17449
  this.mountContainer?.style.removeProperty("--vela-scale-gutter");
17450
+ this.mountContainer?.style.removeProperty("--vela-bottom-gutter");
16901
17451
  this.mountContainer?.style.removeProperty("--vela-price-pane-top");
16902
17452
  this.mountContainer?.style.removeProperty("--vela-price-pane-bottom");
16903
17453
  this.mountContainer = null;
@@ -16990,7 +17540,6 @@ var NativeRenderer = class {
16990
17540
  ensurePane(pane) {
16991
17541
  this.scene.ensurePane(pane.id, pane.kind, pane.order, pane.heightWeight ?? (pane.kind === "price" ? 3 : 1));
16992
17542
  this.layoutPanes();
16993
- this.repositionTables();
16994
17543
  this.paneControls?.refresh();
16995
17544
  this.scheduler.invalidate(4 /* Full */);
16996
17545
  }
@@ -17012,17 +17561,14 @@ var NativeRenderer = class {
17012
17561
  if (!model.ownScale) this.scene.dropIndicatorScale(handle.id);
17013
17562
  this.inputsUI.setPane(handle.id, paneId);
17014
17563
  this.refreshAnchorOffset(model);
17015
- this.syncTables(model);
17016
17564
  this.refreshAxisWidth();
17017
17565
  this.layoutPanes();
17018
- this.repositionTables();
17019
17566
  this.paneControls?.refresh();
17020
17567
  this.scheduler.invalidate(4 /* Full */);
17021
17568
  }
17022
17569
  orderPanes(orderedIds) {
17023
17570
  this.scene.orderPanes(orderedIds);
17024
17571
  this.layoutPanes();
17025
- this.repositionTables();
17026
17572
  this.paneControls?.refresh();
17027
17573
  this.scheduler.invalidate(4 /* Full */);
17028
17574
  }
@@ -17031,7 +17577,6 @@ var NativeRenderer = class {
17031
17577
  if (!pane || pane.collapsed === collapsed) return;
17032
17578
  pane.collapsed = collapsed;
17033
17579
  this.layoutPanes();
17034
- this.repositionTables();
17035
17580
  this.paneControls?.refresh();
17036
17581
  this.scheduler.invalidate(4 /* Full */);
17037
17582
  }
@@ -17039,7 +17584,6 @@ var NativeRenderer = class {
17039
17584
  if (paneId !== null && !this.scene.panes.has(paneId)) paneId = null;
17040
17585
  this.maximizedPaneId = paneId;
17041
17586
  this.layoutPanes();
17042
- this.repositionTables();
17043
17587
  this.paneControls?.refresh();
17044
17588
  this.scheduler.invalidate(4 /* Full */);
17045
17589
  }
@@ -17124,9 +17668,8 @@ var NativeRenderer = class {
17124
17668
  else this.scene.assignIndicatorZ(model.id);
17125
17669
  this.inputsUI.upsert(model.id, model.shorttitle ?? model.title, model.inputs, model.inputValues, model.paneId, {
17126
17670
  native: !!model.native,
17127
- ...model.shorttitle ? { settingsTitle: model.title } : {}
17671
+ ...model.props ? { props: model.props, propValues: model.propValues ?? {} } : {}
17128
17672
  });
17129
- this.syncTables(model);
17130
17673
  if (model.native?.type === "volume") {
17131
17674
  this.volumeActive = true;
17132
17675
  this.volumeHidden = false;
@@ -17150,7 +17693,6 @@ var NativeRenderer = class {
17150
17693
  }
17151
17694
  }
17152
17695
  applyPatch(model, patch);
17153
- this.syncTables(model);
17154
17696
  this.scheduler.invalidate(3 /* Light */);
17155
17697
  }
17156
17698
  removeIndicator(handle) {
@@ -17169,14 +17711,12 @@ var NativeRenderer = class {
17169
17711
  this.scene.forgetAnchorOffset(handle.id);
17170
17712
  this.scene.dropIndicatorScale(handle.id);
17171
17713
  this.inputsUI.remove(handle.id);
17172
- this.tableOverlays.get(handle.id)?.destroy();
17173
- this.tableOverlays.delete(handle.id);
17174
17714
  this.refreshAxisWidth();
17175
17715
  this.paneControls?.refresh();
17176
17716
  this.scheduler.invalidate(4 /* Full */);
17177
17717
  }
17178
- setIndicatorInputs(handle, values) {
17179
- this.inputsUI.setValues(handle.id, values);
17718
+ setIndicatorInputs(handle, values, props) {
17719
+ this.inputsUI.setValues(handle.id, values, props);
17180
17720
  }
17181
17721
  setSymbolPicker(picker) {
17182
17722
  this.symbolPicker = picker;
@@ -17186,6 +17726,10 @@ var NativeRenderer = class {
17186
17726
  this.legendActionsProvider = provider;
17187
17727
  this.inputsUI?.setLegendActions(provider);
17188
17728
  }
17729
+ setLegendCallouts(provider) {
17730
+ this.legendCalloutsProvider = provider;
17731
+ this.inputsUI?.setLegendCallouts(provider);
17732
+ }
17189
17733
  setLegendOverviewAction(action) {
17190
17734
  this.legendOverviewAction = action;
17191
17735
  this.inputsUI?.setLegendOverviewAction(action);
@@ -17210,8 +17754,6 @@ var NativeRenderer = class {
17210
17754
  }
17211
17755
  if (!visible) {
17212
17756
  this.scene.indicators.delete(handle.id);
17213
- this.tableOverlays.get(handle.id)?.destroy();
17214
- this.tableOverlays.delete(handle.id);
17215
17757
  }
17216
17758
  this.inputsUI.setVisible(handle.id, visible);
17217
17759
  this.scheduler.invalidate(4 /* Full */);
@@ -17257,6 +17799,7 @@ var NativeRenderer = class {
17257
17799
  this.userDrawings?.setLayoutMode(mode);
17258
17800
  this.settingsDialog?.setLayoutMode(mode);
17259
17801
  this.inputsUI?.setLayoutMode(mode);
17802
+ this.paneControls?.setSuspended(mode === "mobile");
17260
17803
  if (this.scrollButton) {
17261
17804
  const px = mode === "mobile" ? SCROLL_BTN_SIZE_TOUCH : SCROLL_BTN_SIZE;
17262
17805
  this.scrollButton.style.width = `${px}px`;
@@ -17652,7 +18195,6 @@ var NativeRenderer = class {
17652
18195
  /** Relayout + repaint + refresh the hover buttons after a collapse/maximize/order change. */
17653
18196
  afterPaneLayoutChange() {
17654
18197
  this.layoutPanes();
17655
- this.repositionTables();
17656
18198
  this.paneControls?.refresh();
17657
18199
  this.scheduler.invalidate(4 /* Full */);
17658
18200
  }
@@ -17733,7 +18275,6 @@ var NativeRenderer = class {
17733
18275
  above.heightWeight = next.above;
17734
18276
  below.heightWeight = next.below;
17735
18277
  this.layoutPanes();
17736
- this.repositionTables();
17737
18278
  this.scheduler.invalidate(4 /* Full */);
17738
18279
  }
17739
18280
  /** Double-click a separator → split the two adjacent panes evenly (each gets half of
@@ -17748,7 +18289,6 @@ var NativeRenderer = class {
17748
18289
  above.heightWeight = half;
17749
18290
  below.heightWeight = half;
17750
18291
  this.layoutPanes();
17751
- this.repositionTables();
17752
18292
  this.scheduler.invalidate(4 /* Full */);
17753
18293
  }
17754
18294
  // ── keyboard navigation / accessibility (item 11) ──
@@ -18027,7 +18567,10 @@ var NativeRenderer = class {
18027
18567
  const liveActual = li >= 0 ? this.bars[li] : void 0;
18028
18568
  const easeLive = !!liveActual && this.liveEaseTime === liveActual.time && (liveActual.high !== this.liveEaseHigh || liveActual.low !== this.liveEaseLow || liveActual.close !== this.liveEaseClose);
18029
18569
  if (easeLive && liveActual) this.bars[li] = { ...liveActual, high: this.liveEaseHigh, low: this.liveEaseLow, close: this.liveEaseClose };
18030
- this.scene.drawingSlices = this.userDrawings?.prepareSlices(this.scene.orderedPanes().map((p) => p.id)) ?? /* @__PURE__ */ new Map();
18570
+ this.scene.drawingSlices = mergeSlices(
18571
+ this.indicatorSlices.prepare(this.scene, this.coords, this.theme, this.dataCanvas),
18572
+ this.userDrawings?.prepareSlices(this.scene.orderedPanes().map((p) => p.id)) ?? /* @__PURE__ */ new Map()
18573
+ );
18031
18574
  this.backdropRenderer.render(this.scene, this.coords, this.theme, gridAlpha);
18032
18575
  this.backend.render(this.scene, this.coords, this.theme);
18033
18576
  this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
@@ -18166,7 +18709,11 @@ var NativeRenderer = class {
18166
18709
  }
18167
18710
  const models = this.scene.indicatorsForPane(pane.id);
18168
18711
  const masterModels = models.filter((m) => m.ownScale !== true);
18169
- const dr = this.chrome.paneDrawingsRange(masterModels, this.scene, pane === pricePane, vr);
18712
+ let dr = this.chrome.paneDrawingsRange(masterModels, this.scene, pane === pricePane, vr);
18713
+ if (pane === pricePane) {
18714
+ const or = overlaySeriesRange(this.scene.indicators.values(), i0, i1, (id) => this.scene.offsetOf(id));
18715
+ if (or) dr = dr ? { min: Math.min(dr.min, or.min), max: Math.max(dr.max, or.max) } : or;
18716
+ }
18170
18717
  const includeCandles = pane.kind === "price" && !this.scene.candlesHidden;
18171
18718
  pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
18172
18719
  pane.percentBaseline = pane.kind === "price" ? this.bars[i0]?.close ?? 0 : this.firstVisibleValue(masterModels, i0);
@@ -18440,6 +18987,7 @@ var NativeRenderer = class {
18440
18987
  for (const m of models) {
18441
18988
  const off = this.scene.offsetOf(m.id);
18442
18989
  for (const s of m.series) {
18990
+ if (s.overlay === true) continue;
18443
18991
  if (isLineLikeSeries(s)) {
18444
18992
  const v = s.points[i0 - off]?.value;
18445
18993
  if (v != null && Number.isFinite(v)) return v;
@@ -18460,27 +19008,6 @@ var NativeRenderer = class {
18460
19008
  }
18461
19009
  return maxVol;
18462
19010
  }
18463
- /** Create/update/destroy an indicator's DOM table overlay (anchored off real pane geometry). */
18464
- syncTables(model) {
18465
- const tables = model.tables ?? [];
18466
- let overlay = this.tableOverlays.get(model.id);
18467
- if (tables.length === 0) {
18468
- if (overlay) {
18469
- overlay.destroy();
18470
- this.tableOverlays.delete(model.id);
18471
- }
18472
- return;
18473
- }
18474
- if (!overlay) {
18475
- overlay = new TableOverlay(this.plot, this.theme, (id) => this.paneBoundsFor(id));
18476
- overlay.setVisible(this.loadingEl === null);
18477
- this.tableOverlays.set(model.id, overlay);
18478
- }
18479
- overlay.update(tables);
18480
- }
18481
- repositionTables() {
18482
- for (const overlay of this.tableOverlays.values()) overlay.reposition();
18483
- }
18484
19011
  layoutPanes() {
18485
19012
  const panes = this.scene.orderedPanes();
18486
19013
  const dataHeight = this.coords.height;
@@ -18531,6 +19058,7 @@ var NativeRenderer = class {
18531
19058
  const visible = maxPane ? [maxPane] : this.scene.orderedPanes().filter((p) => !p.collapsed);
18532
19059
  const paneBottom = visible.length ? Math.max(...visible.map((p) => p.bounds.top + p.bounds.height)) : dataHeight;
18533
19060
  this.scrollBtnBottomPx = SCROLL_BTN_BOTTOM + Math.max(0, dataHeight - paneBottom);
19061
+ this.mountContainer?.style.setProperty("--vela-bottom-gutter", `${TIME_AXIS_H + Math.max(0, dataHeight - paneBottom)}px`);
18534
19062
  this.scrollBtnRightPx = this.rightAxisW + SCROLL_BTN_RIGHT_INSET;
18535
19063
  if (this.scrollButton) {
18536
19064
  this.scrollButton.style.bottom = `${this.scrollBtnBottomPx}px`;
@@ -18623,30 +19151,33 @@ var NativeRenderer = class {
18623
19151
  if (w <= 0 || h <= 0) return;
18624
19152
  const dpr = window.devicePixelRatio || 1;
18625
19153
  this.plot.style.left = `${this.toolbarGutter}px`;
18626
- const pw = Math.max(1, w - this.toolbarGutter);
18627
- const ph = h;
18628
- this.dataCanvas.width = Math.round(pw * dpr);
18629
- this.dataCanvas.height = Math.round(ph * dpr);
18630
- this.backdropCanvas.width = this.dataCanvas.width;
18631
- this.backdropCanvas.height = this.dataCanvas.height;
18632
- this.volumeCanvas.width = this.dataCanvas.width;
18633
- this.volumeCanvas.height = this.dataCanvas.height;
18634
- for (const l of this.extLayers) {
18635
- l.canvas.width = this.dataCanvas.width;
18636
- l.canvas.height = this.dataCanvas.height;
18637
- }
18638
- this.vpvrCanvas.width = this.dataCanvas.width;
18639
- this.vpvrCanvas.height = this.dataCanvas.height;
18640
- this.chromeCanvas.width = this.dataCanvas.width;
18641
- this.chromeCanvas.height = this.dataCanvas.height;
18642
- this.drawingsCanvas.width = this.dataCanvas.width;
18643
- this.drawingsCanvas.height = this.dataCanvas.height;
18644
- this.cursorCanvas.width = this.dataCanvas.width;
18645
- this.cursorCanvas.height = this.dataCanvas.height;
19154
+ const rect = this.plot.getBoundingClientRect();
19155
+ let bw = Math.max(1, Math.round(rect.width * dpr));
19156
+ let bh = Math.max(1, Math.round(rect.height * dpr));
19157
+ const dev = this.plotDeviceSize;
19158
+ if (dev && Math.abs(dev.width - rect.width * dpr) <= 1 && Math.abs(dev.height - rect.height * dpr) <= 1) {
19159
+ bw = Math.max(1, dev.width);
19160
+ bh = Math.max(1, dev.height);
19161
+ }
19162
+ const pw = bw / dpr;
19163
+ const ph = bh / dpr;
19164
+ const size = (canvas) => {
19165
+ canvas.width = bw;
19166
+ canvas.height = bh;
19167
+ canvas.style.width = `${pw}px`;
19168
+ canvas.style.height = `${ph}px`;
19169
+ };
19170
+ size(this.dataCanvas);
19171
+ size(this.backdropCanvas);
19172
+ size(this.volumeCanvas);
19173
+ for (const l of this.extLayers) size(l.canvas);
19174
+ size(this.vpvrCanvas);
19175
+ size(this.chromeCanvas);
19176
+ size(this.drawingsCanvas);
19177
+ size(this.cursorCanvas);
18646
19178
  this.coords.setSize(Math.max(1, pw - this.rightAxisW), Math.max(1, ph - TIME_AXIS_H), dpr);
18647
19179
  this.scene.crosshair = null;
18648
19180
  this.layoutPanes();
18649
- this.repositionTables();
18650
19181
  this.userDrawings?.onResize();
18651
19182
  if (!this.didInitialFit && this.coords.barCount > 0) {
18652
19183
  this.fitContent();
@@ -18820,6 +19351,7 @@ var IndicatorHandleImpl = class {
18820
19351
  constructor(id, title, controller, source) {
18821
19352
  this.controller = controller;
18822
19353
  this.schema = [];
19354
+ this.propsSchema = [];
18823
19355
  this.visibleState = true;
18824
19356
  this.bus = new TypedEventBus();
18825
19357
  this.id = id;
@@ -18829,6 +19361,9 @@ var IndicatorHandleImpl = class {
18829
19361
  get inputs() {
18830
19362
  return this.schema;
18831
19363
  }
19364
+ get props() {
19365
+ return this.propsSchema;
19366
+ }
18832
19367
  get visible() {
18833
19368
  return this.visibleState;
18834
19369
  }
@@ -18838,6 +19373,12 @@ var IndicatorHandleImpl = class {
18838
19373
  setInputs(values) {
18839
19374
  this.controller.applyInputs(this.id, values);
18840
19375
  }
19376
+ setProp(key, value) {
19377
+ this.controller.applyProps(this.id, { [key]: value });
19378
+ }
19379
+ setProps(values) {
19380
+ this.controller.applyProps(this.id, values);
19381
+ }
18841
19382
  setVisible(visible) {
18842
19383
  this.controller.setVisible(this.id, visible);
18843
19384
  }
@@ -18857,6 +19398,9 @@ var IndicatorHandleImpl = class {
18857
19398
  setSchema(schema) {
18858
19399
  this.schema = schema;
18859
19400
  }
19401
+ setPropsSchema(schema) {
19402
+ this.propsSchema = schema;
19403
+ }
18860
19404
  /** Sync the public `visible` getter when the orchestrator changes visibility (API or legend eye). */
18861
19405
  setVisibleState(visible) {
18862
19406
  this.visibleState = visible;
@@ -18870,6 +19414,8 @@ var IndicatorHandleImpl = class {
18870
19414
  function summarizeModel(model) {
18871
19415
  const series = {};
18872
19416
  for (const s of model.series) series[s.kind] = (series[s.kind] ?? 0) + 1;
19417
+ const countOverlay = (items) => items?.filter((i) => i.overlay === true).length ?? 0;
19418
+ const forcedOverlay = countOverlay(model.series) + countOverlay(model.fills) + countOverlay(model.backgrounds) + countOverlay(model.lines) + countOverlay(model.boxes) + countOverlay(model.labels) + countOverlay(model.polylines) + countOverlay(model.linefills) + countOverlay(model.tables);
18873
19419
  return {
18874
19420
  id: model.id,
18875
19421
  title: model.title,
@@ -18889,7 +19435,8 @@ function summarizeModel(model) {
18889
19435
  tables: model.tables?.length ?? 0,
18890
19436
  barColors: model.barColors?.length ?? 0,
18891
19437
  trades: model.trades?.length ?? 0,
18892
- inputs: model.inputs.length
19438
+ inputs: model.inputs.length,
19439
+ forcedOverlay
18893
19440
  };
18894
19441
  }
18895
19442
  function inspectModels(models) {
@@ -18910,7 +19457,8 @@ function inspectModels(models) {
18910
19457
  linefills: sum((s) => s.linefills),
18911
19458
  tables: sum((s) => s.tables),
18912
19459
  barColors: sum((s) => s.barColors),
18913
- trades: sum((s) => s.trades)
19460
+ trades: sum((s) => s.trades),
19461
+ forcedOverlay: sum((s) => s.forcedOverlay)
18914
19462
  }
18915
19463
  };
18916
19464
  }
@@ -19021,6 +19569,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
19021
19569
  /** Invalidates detached async work (backfill loops, in-flight loads, gap heals):
19022
19570
  * bumped by init(), setMarket() and destroy(). */
19023
19571
  this.generation = 0;
19572
+ /** Aborts the in-flight PROGRESSIVE load's source polling on supersession — an
19573
+ * abandoned stream left polling to its own budget starves the browser's per-host
19574
+ * connection pool, and the NEXT symbol's very first fetch with it (measured). */
19575
+ this.progressiveAbort = null;
19024
19576
  /** Awaiters racing a superseded load (setMarket callers) — released on every bump so they never hang. */
19025
19577
  this.supersedeWaiters = [];
19026
19578
  /** `history:complete` fired for the CURRENT load. Each market load re-arms the cycle
@@ -19054,7 +19606,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
19054
19606
  for (const engine of engines) this.registerEngine(engine.language, engine);
19055
19607
  this.defaultLanguage = config.defaultLanguage;
19056
19608
  this.renderer.mount(container, config.theme);
19057
- this.renderer.onInputChange((e) => this.applyInputs(e.indicatorId, { [e.key]: e.value }));
19609
+ this.renderer.onInputChange(
19610
+ (e) => e.kind === "prop" ? this.applyProps(e.indicatorId, { [e.key]: e.value }) : this.applyInputs(e.indicatorId, { [e.key]: e.value })
19611
+ );
19058
19612
  this.renderer.onRemoveIndicator((id) => this.removeIndicator(id));
19059
19613
  this.renderer.onToggleIndicatorVisible?.((id, visible) => this.setVisible(id, visible));
19060
19614
  this.paneActionUnsub = this.renderer.onPaneAction?.((a) => this.handlePaneAction(a)) ?? null;
@@ -19161,6 +19715,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
19161
19715
  * superseded setMarket awaiters so their promises resolve instead of hanging. */
19162
19716
  bumpGeneration() {
19163
19717
  const gen = ++this.generation;
19718
+ this.progressiveAbort?.abort();
19719
+ this.progressiveAbort = null;
19164
19720
  for (const w of this.supersedeWaiters.splice(0)) w();
19165
19721
  return gen;
19166
19722
  }
@@ -19214,7 +19770,47 @@ var EngineOrchestrator = class _EngineOrchestrator {
19214
19770
  const requested = market.bars ?? 500;
19215
19771
  const initialRange = market.visibleRange;
19216
19772
  const deep = !market.data?.length && initialRange == null && requested > SINGLE_LOAD_BARS;
19217
- if (deep && this.feed.loadRange) {
19773
+ let progressiveServed = false;
19774
+ if (!market.data?.length && initialRange == null && this.feed.loadProgressive) {
19775
+ let painted = false;
19776
+ const paint = (bars, final) => {
19777
+ if (this.generation !== gen || !final && bars.length === 0) return;
19778
+ this.setBarSeries(bars, painted ? { preserveView: true } : void 0);
19779
+ if (!painted && bars.length > 0) {
19780
+ painted = true;
19781
+ if (opts.firstLoad) this.activateBarLayers();
19782
+ if (!final) this.historyState = "backfill";
19783
+ }
19784
+ };
19785
+ const abort = new AbortController();
19786
+ this.progressiveAbort = abort;
19787
+ progressiveServed = await new Promise((firstPaint) => {
19788
+ let signaled = false;
19789
+ const signal = (served) => {
19790
+ if (!signaled) {
19791
+ signaled = true;
19792
+ firstPaint(served);
19793
+ }
19794
+ };
19795
+ abort.signal.addEventListener("abort", () => signal(true), { once: true });
19796
+ this.feed.loadProgressive(market, (bars) => {
19797
+ paint(bars, false);
19798
+ if (painted) signal(true);
19799
+ }, { signal: abort.signal }).then((full) => {
19800
+ if (this.progressiveAbort === abort) this.progressiveAbort = null;
19801
+ if (full == null) return signal(false);
19802
+ if (this.generation !== gen) return signal(true);
19803
+ paint(full, true);
19804
+ this.completeHistory(full.length >= requested ? "depth" : "genesis");
19805
+ signal(true);
19806
+ }).catch(() => {
19807
+ if (this.progressiveAbort === abort) this.progressiveAbort = null;
19808
+ if (this.generation === gen) this.completeHistory("aborted");
19809
+ signal(true);
19810
+ });
19811
+ });
19812
+ }
19813
+ if (progressiveServed) ; else if (deep && this.feed.loadRange) {
19218
19814
  const head = await this.feed.load({ ...market, bars: Math.min(requested, CHUNK_BARS) });
19219
19815
  if (this.generation !== gen) return;
19220
19816
  this.setBarSeries(head);
@@ -19734,7 +20330,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
19734
20330
  const title = options.title ?? "Indicator";
19735
20331
  const handle = new IndicatorHandleImpl(id, title, this, source);
19736
20332
  this.handles.set(id, handle);
19737
- this.registry.add({ id, title, source, options, inputValues: { ...options.inputs ?? {} } });
20333
+ this.registry.add({ id, title, source, options, inputValues: { ...options.inputs ?? {} }, propValues: { ...options.props ?? {} } });
19738
20334
  void this.startIndicator(id, source, options, handle);
19739
20335
  return handle;
19740
20336
  }
@@ -19757,7 +20353,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
19757
20353
  return handle;
19758
20354
  }
19759
20355
  const inputValues = { ...descriptor.defaultInputs(), ...options.inputs ?? {} };
19760
- this.registry.add({ id, title, source: type, inputValues, native: { type, instance: descriptor.create(), descriptor } });
20356
+ this.registry.add({ id, title, source: type, inputValues, propValues: {}, native: { type, instance: descriptor.create(), descriptor } });
19761
20357
  handle.setSchema(descriptor.inputsSchema());
19762
20358
  void this.startNativeIndicator(id, handle);
19763
20359
  return handle;
@@ -19810,6 +20406,19 @@ var EngineOrchestrator = class _EngineOrchestrator {
19810
20406
  if (record.session) record.session.update(record.inputValues);
19811
20407
  else if (record.native && !record.hidden) record.native.instance.setInputs(record.inputValues);
19812
20408
  }
20409
+ /** IndicatorController: re-run an indicator with merged declaration-prop overrides.
20410
+ * Same lifecycle as {@link applyInputs} — a prop change replays the whole script.
20411
+ * Script indicators only: natives have no declaration props. */
20412
+ applyProps(id, values) {
20413
+ const record = this.registry.get(id);
20414
+ if (!record?.session) return;
20415
+ record.propValues = { ...record.propValues, ...values };
20416
+ if (record.renderHandle) this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
20417
+ record.pendingStructural = true;
20418
+ if (!record.hidden) this.setLoading(record, true);
20419
+ record.pendingCause = "inputs";
20420
+ record.session.update(record.inputValues, record.propValues);
20421
+ }
19813
20422
  /** IndicatorController: tear down an indicator and (if now empty) its pane. */
19814
20423
  /** Live handles of every indicator on the chart (script + native), insertion order. */
19815
20424
  listIndicators() {
@@ -19958,6 +20567,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
19958
20567
  for (const input of prepared.inputs) defaults[input.key] = input.defval;
19959
20568
  record.inputValues = { ...defaults, ...record.inputValues };
19960
20569
  handle.setSchema(prepared.inputs);
20570
+ const propDefaults = {};
20571
+ for (const prop of prepared.props ?? []) propDefaults[prop.key] = prop.defval;
20572
+ record.propValues = { ...propDefaults, ...record.propValues };
20573
+ handle.setPropsSchema(prepared.props ?? []);
19961
20574
  this.mountLoadingPlaceholder(id, record);
19962
20575
  this.executeIndicator(id, handle);
19963
20576
  } catch (err) {
@@ -19981,6 +20594,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
19981
20594
  getBars: () => this.bars,
19982
20595
  fetchSeries: (sym, tf, range) => this.fetchSeries(sym, tf, range),
19983
20596
  inputs: record.inputValues,
20597
+ props: record.propValues,
19984
20598
  visibleRange: this.currentVisibleRange(),
19985
20599
  mode,
19986
20600
  // Mid-backfill session starts (add / re-show / price-style re-execute) let the
@@ -20074,6 +20688,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
20074
20688
  const meta = record.prepared.meta;
20075
20689
  const model = {
20076
20690
  id,
20691
+ // Deliberately the FULL title, never a shorttitle: while the script loads the
20692
+ // legend identifies it by its full name; the compact shorttitle arrives with
20693
+ // the first computed model and takes over from there.
20077
20694
  title: record.options?.title ?? meta.title,
20078
20695
  overlay: meta.overlay,
20079
20696
  paneHint: meta.overlay ? "price" : "new",
@@ -20082,7 +20699,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
20082
20699
  backgrounds: [],
20083
20700
  priceLines: [],
20084
20701
  inputs: record.prepared.inputs,
20085
- inputValues: record.inputValues
20702
+ inputValues: record.inputValues,
20703
+ ...record.prepared.props ? { props: record.prepared.props, propValues: record.propValues } : {}
20086
20704
  };
20087
20705
  const paneId = this.routePane(id, model, record.options ?? {});
20088
20706
  this.placeModel(model, id, paneId);
@@ -20315,7 +20933,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
20315
20933
  record.model = model;
20316
20934
  if (record.pendingStructural) {
20317
20935
  record.renderHandle = this.renderer.mountIndicator(model);
20318
- this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues);
20936
+ this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
20319
20937
  record.pendingStructural = false;
20320
20938
  } else {
20321
20939
  this.renderer.updateIndicator(record.renderHandle, modelToValuePatch(model));
@@ -20342,16 +20960,17 @@ var EngineOrchestrator = class _EngineOrchestrator {
20342
20960
  placeModel(model, id, paneId) {
20343
20961
  model.id = id;
20344
20962
  model.paneId = paneId;
20345
- for (const series of model.series) series.paneId = paneId;
20346
- for (const fill of model.fills) fill.paneId = paneId;
20347
- for (const bg of model.backgrounds) bg.paneId = paneId;
20963
+ const paneOf = (item) => item.overlay === true ? "price" : paneId;
20964
+ for (const series of model.series) series.paneId = paneOf(series);
20965
+ for (const fill of model.fills) fill.paneId = paneOf(fill);
20966
+ for (const bg of model.backgrounds) bg.paneId = paneOf(bg);
20348
20967
  for (const line of model.priceLines) line.paneId = paneId;
20349
- if (model.lines) for (const ln of model.lines) ln.paneId = paneId;
20350
- if (model.boxes) for (const bx of model.boxes) bx.paneId = paneId;
20351
- if (model.labels) for (const lb of model.labels) lb.paneId = paneId;
20352
- if (model.polylines) for (const pl of model.polylines) pl.paneId = paneId;
20353
- if (model.linefills) for (const lf of model.linefills) lf.paneId = paneId;
20354
- if (model.tables) for (const tb of model.tables) tb.paneId = paneId;
20968
+ if (model.lines) for (const ln of model.lines) ln.paneId = paneOf(ln);
20969
+ if (model.boxes) for (const bx of model.boxes) bx.paneId = paneOf(bx);
20970
+ if (model.labels) for (const lb of model.labels) lb.paneId = paneOf(lb);
20971
+ if (model.polylines) for (const pl of model.polylines) pl.paneId = paneOf(pl);
20972
+ if (model.linefills) for (const lf of model.linefills) lf.paneId = paneOf(lf);
20973
+ if (model.tables) for (const tb of model.tables) tb.paneId = paneOf(tb);
20355
20974
  }
20356
20975
  emitContextChanged(id) {
20357
20976
  if (!this.registry.get(id)?.session?.getContext) return;
@@ -21011,4 +21630,4 @@ function resolveElement(container) {
21011
21630
  return element;
21012
21631
  }
21013
21632
 
21014
- export { BUILTIN_PRICE_STYLES, BarStore, CachingDataFeed, DARK_THEME, DataControl, DrawingStore, DrawingToolbar, DrawingsControl, LIGHT_THEME, MultiProviderFeed, NativeRenderer, RendererControl, TIMEZONES, TypedEventBus, Vela, applyAttributionMarkTheme, buildToolbar, createAttributionMark, createCustomMark, defaultToolbar, normalizeTimezone, parseSymbol, priceStyleIds, registerBuiltinChartTypes, resolveTheme, sharedBarStore, timeframeToMs, tzButtonLabel, tzMenuLabel, tzOffset };
21633
+ 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 };