@luxalgo/vela 0.6.11 → 0.6.13

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 (41) hide show
  1. package/README.md +110 -46
  2. package/dist/{DataProvider-BBf-jc6W.d.ts → DataProvider-DhzstpQb.d.ts} +1 -1
  3. package/dist/{DataProvider-p0TEyhlX.d.cts → DataProvider-DlMtrwqM.d.cts} +1 -1
  4. package/dist/{chunk-MTLJKZDZ.js → chunk-F4M24ANM.js} +122 -1
  5. package/dist/{chunk-IO3NYSQV.js → chunk-G52XAKZY.js} +184 -25
  6. package/dist/{chunk-73PEA4MU.js → chunk-JKGA36ZM.js} +635 -49
  7. package/dist/{contributions-Bbe2R-mQ.d.ts → contributions-37nni40G.d.ts} +6 -4
  8. package/dist/{contributions-CO01zWve.d.cts → contributions-lPojhTxI.d.cts} +6 -4
  9. package/dist/index.cjs +744 -37
  10. package/dist/index.d.cts +6 -6
  11. package/dist/index.d.ts +6 -6
  12. package/dist/index.js +2 -2
  13. package/dist/{options-yp7sA96q.d.cts → options-CYS5Wmlx.d.cts} +71 -2
  14. package/dist/{options-yp7sA96q.d.ts → options-CYS5Wmlx.d.ts} +71 -2
  15. package/dist/{plugin-CkkH8QnX.d.cts → plugin-Cwikpz1m.d.cts} +10 -3
  16. package/dist/{plugin-DwxjM3Ni.d.ts → plugin-DHyaoMjW.d.ts} +10 -3
  17. package/dist/plugin.cjs +110 -0
  18. package/dist/plugin.d.cts +4 -4
  19. package/dist/plugin.d.ts +4 -4
  20. package/dist/plugin.js +1 -1
  21. package/dist/providers/binance.d.cts +2 -2
  22. package/dist/providers/binance.d.ts +2 -2
  23. package/dist/providers/coinbase.d.cts +2 -2
  24. package/dist/providers/coinbase.d.ts +2 -2
  25. package/dist/providers/hyperliquid.d.cts +2 -2
  26. package/dist/providers/hyperliquid.d.ts +2 -2
  27. package/dist/{statusline-CHPDuKNp.d.ts → statusline-model-CiJm-riV.d.cts} +7 -85
  28. package/dist/{statusline-DTHZUFqK.d.cts → statusline-model-D-q_OOx9.d.ts} +7 -85
  29. package/dist/ui.d.cts +1 -1
  30. package/dist/ui.d.ts +1 -1
  31. package/dist/vela.global.js +744 -37
  32. package/dist/vela.global.min.js +52 -52
  33. package/dist/widget.cjs +927 -61
  34. package/dist/widget.d.cts +118 -7
  35. package/dist/widget.d.ts +118 -7
  36. package/dist/widget.js +4 -4
  37. package/dist/workspace.cjs +927 -61
  38. package/dist/workspace.d.cts +5 -5
  39. package/dist/workspace.d.ts +5 -5
  40. package/dist/workspace.js +3 -3
  41. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { deserializeDrawing, chartTypes, rendererLayers, foldBaseModulation, registerChartType, rendererDefaults, getDrawingType, settingsRowVisible, normalizeSettingsRow, TextLabel, Callout, resetDrawingSettings, registerNativeIndicator, chartType, getNativeIndicator, nativeIndicatorDescriptors, drawingTypes, settingsRowValueKeys, formatDuration, RegressionChannel, FixedRangeVolumeProfile, SegmentDrawing, ArrowMark, GlyphStamp, RadialFib, FibSpiral, DedekindTessellation, MachFigure, GannSquare, FibRatios, MeasureBox, PositionTool, AnchoredVwap, PatternDrawing, Comment, PriceNote, Signpost, Note, PriceLabel, GANN_SQUARE_ARCS, lineSegmentIntersection, DEFAULT_DRAWING_COLOR, GLYPH_OPTIONS, STAMP_SIZE_OPTIONS, LINE_STYLE_OPTIONS, DEDEKIND_CURVATURE_OPTIONS, MACH_NUMBER_OPTIONS, MACH_WAVE_COUNT_OPTIONS, TEXT_SIZE_OPTIONS, createDrawing, inputVisible, tickerModifierIds, CalloutBase, DIRECTION_OPTIONS } from './chunk-MTLJKZDZ.js';
1
+ import { deserializeDrawing, chartTypes, rendererLayers, foldBaseModulation, registerChartType, rendererDefaults, getDrawingType, settingsRowVisible, normalizeSettingsRow, Magnifier, 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, magnifierTimeframeLabel, lineSegmentIntersection, DEFAULT_DRAWING_COLOR, MAGNIFIER_TIMEFRAME_OPTIONS, 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-F4M24ANM.js';
2
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
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
 
@@ -62,7 +62,7 @@ var GEOMETRY_TYPES = ["dedekind", "sonic", "supersonic", "goldensonic", "goldens
62
62
  var PATTERN_TYPES = ["xabcd", "abcd", "headshoulders"];
63
63
  var ELLIOTT_TYPES = ["elliottimpulse", "elliottcorrection"];
64
64
  var HARMONIC_TYPES = ["gartley", "bat", "butterfly", "crab", "shark", "cypher"];
65
- var MEASUREMENT_TYPES = ["position", "datepricerange"];
65
+ var MEASUREMENT_TYPES = ["position", "datepricerange", "magnifier"];
66
66
  var VOLUME_TYPES = ["anchoredvwap", "fixedrangevp"];
67
67
  var BRUSH_TYPES = ["freehand", "highlighter"];
68
68
  var ARROW_TYPES = ["arrow", "arrowmarkup", "arrowmarkdown"];
@@ -1138,6 +1138,7 @@ function intervalMs(timeframe) {
1138
1138
  "4h": 144e5,
1139
1139
  "1d": 864e5,
1140
1140
  "1w": 6048e5,
1141
+ "1M": 2592e6,
1141
1142
  "1": 6e4,
1142
1143
  "5": 3e5,
1143
1144
  "15": 9e5,
@@ -1145,7 +1146,8 @@ function intervalMs(timeframe) {
1145
1146
  "60": 36e5,
1146
1147
  "240": 144e5,
1147
1148
  D: 864e5,
1148
- W: 6048e5
1149
+ W: 6048e5,
1150
+ M: 2592e6
1149
1151
  };
1150
1152
  return map[timeframe] ?? 36e5;
1151
1153
  }
@@ -1859,7 +1861,7 @@ var DrawingHistory = class {
1859
1861
 
1860
1862
  // src/core/drawings/DrawingController.ts
1861
1863
  var DrawingController = class {
1862
- constructor(renderer, events, option) {
1864
+ constructor(renderer, events, option, seriesGateway) {
1863
1865
  this.events = events;
1864
1866
  this.store = new DrawingStore();
1865
1867
  this.history = new DrawingHistory();
@@ -1885,6 +1887,7 @@ var DrawingController = class {
1885
1887
  const { definition, visible } = buildToolbar(option);
1886
1888
  this.port.setToolbar(definition);
1887
1889
  this.port.showToolbar(visible);
1890
+ if (seriesGateway) this.port.setSeriesGateway?.(seriesGateway);
1888
1891
  this.subs.push(this.port.onDrawingIntent((i) => this.onIntent(i)));
1889
1892
  this.subs.push(this.store.onChange(() => this.sync()));
1890
1893
  }
@@ -1990,7 +1993,7 @@ var DrawingController = class {
1990
1993
  style,
1991
1994
  text: init.text,
1992
1995
  props: init.props,
1993
- zIndex: init.zIndex ?? this.startZ(init.paneId ?? "price")
1996
+ zIndex: init.zIndex ?? this.startZ(type, init.paneId ?? "price")
1994
1997
  });
1995
1998
  if (!d) return null;
1996
1999
  this.history.record(this.store.serialize());
@@ -2065,10 +2068,14 @@ var DrawingController = class {
2065
2068
  * (falling back to just under the pane's top series where there is no price — a study
2066
2069
  * pane). Half a key down never ties a series; drawings tying each other paint in insertion
2067
2070
  * order, so consecutive new drawings still stack newest-in-front. Undefined without a
2068
- * shared z space — the store then places it over the other drawings, its own layer's top. */
2069
- startZ(paneId) {
2071
+ * shared z space — the store then places it over the other drawings, its own layer's top.
2072
+ * A type that COVERS the series (an opaque inset, `coversSeries`) instead starts just
2073
+ * above the whole stack — under the candles its content would be buried. */
2074
+ startZ(type, paneId) {
2070
2075
  const range = this.port?.stackRange?.(paneId);
2071
- return range ? (range.price ?? range.front) - 0.5 : void 0;
2076
+ if (!range) return void 0;
2077
+ if (getDrawingType(type)?.coversSeries) return range.front + 0.5;
2078
+ return (range.price ?? range.front) - 0.5;
2072
2079
  }
2073
2080
  /** Programmatically select drawings (host UI → chart): shows the on-chart handles + toolbar.
2074
2081
  * `additive` toggles membership (matching shift-click) instead of replacing. */
@@ -2213,7 +2220,7 @@ var DrawingController = class {
2213
2220
  const style = last ? { ...i.doc.style, ...last } : i.doc.style;
2214
2221
  const d = deserializeDrawing({ ...i.doc, id: this.store.nextId(), style });
2215
2222
  if (!d) return;
2216
- if (!d.zIndex) d.zIndex = this.startZ(d.paneId) ?? 0;
2223
+ if (!d.zIndex) d.zIndex = this.startZ(d.type, d.paneId) ?? 0;
2217
2224
  this.history.record(before);
2218
2225
  this.store.add(d);
2219
2226
  this.captureStyle(d.id);
@@ -8699,7 +8706,9 @@ var DrawingSceneRenderer = class {
8699
8706
  const lo = Math.min(from, to);
8700
8707
  const hi = Math.max(from, to);
8701
8708
  const visible = (a, b, extend) => {
8702
- if (extend !== "none") return true;
8709
+ if (extend === "both") return true;
8710
+ if (extend === "left") return Math.max(a, b) >= lo;
8711
+ if (extend === "right") return Math.min(a, b) <= hi;
8703
8712
  return Math.max(a, b) >= lo && Math.min(a, b) <= hi;
8704
8713
  };
8705
8714
  let min = Infinity;
@@ -11154,6 +11163,18 @@ function timezoneOptions(current) {
11154
11163
  return [[normalized, tzMenuLabel(normalized, normalized)], ...options];
11155
11164
  }
11156
11165
 
11166
+ // src/core/price-styles/BarTransform.ts
11167
+ function barTransformFor(style) {
11168
+ return chartType(style)?.barTransform ?? null;
11169
+ }
11170
+ function parseExtendedTicker(symbol) {
11171
+ const at = symbol.lastIndexOf(";");
11172
+ if (at <= 0 || at === symbol.length - 1) return { symbol, modifier: null, transform: null };
11173
+ const modifier = symbol.slice(at + 1).toLowerCase();
11174
+ if (modifier !== "standard" && !tickerModifierIds().includes(modifier)) return { symbol, modifier: null, transform: null };
11175
+ return { symbol: symbol.slice(0, at), modifier, transform: barTransformFor(modifier) };
11176
+ }
11177
+
11157
11178
  // src/renderers/native/drawings/DrawingPainter.ts
11158
11179
  var HANDLE_RADIUS = 4.5;
11159
11180
  var HANDLE_BORDER = DEFAULT_DRAWING_COLOR;
@@ -11173,6 +11194,17 @@ var DrawingPainter = class {
11173
11194
  constructor() {
11174
11195
  /** The current `paintAll` call's interaction state, visible to the per-type painters. */
11175
11196
  this.targets = {};
11197
+ /** The chart's active series LOOK — style + resolved series colors — pushed by the
11198
+ * controller before each paint. The magnifier's inset mirrors both: candles/bars/line/
11199
+ * area restyle the paint (bar-transform styles like Heikin Ashi transform the fetched
11200
+ * bars; unknown/custom styles fall back to candles), and the colors default to the main
11201
+ * series' own so the inset reads as a finer copy of the chart. */
11202
+ this.seriesLook = {
11203
+ style: "candles",
11204
+ upColor: BULLISH,
11205
+ downColor: BEARISH,
11206
+ lineColor: BULLISH
11207
+ };
11176
11208
  }
11177
11209
  /** Paint every visible drawing, then selection handles for the targeted ones.
11178
11210
  * Each drawing is clipped to its own pane's rect (and skipped entirely while that pane
@@ -11217,6 +11249,8 @@ var DrawingPainter = class {
11217
11249
  ctx.globalAlpha = GHOST_ALPHA;
11218
11250
  if (ghost instanceof RegressionChannel || ghost instanceof FixedRangeVolumeProfile) {
11219
11251
  this.paintTimeSpanGhost(ctx, ghost, proj);
11252
+ } else if (ghost instanceof Magnifier) {
11253
+ this.paintMagnifierGhost(ctx, ghost, proj, theme);
11220
11254
  } else this.paintOne(ctx, ghost, proj, theme);
11221
11255
  ctx.globalAlpha = 1;
11222
11256
  }
@@ -11350,6 +11384,10 @@ var DrawingPainter = class {
11350
11384
  this.paintLabel(ctx, d, proj, theme);
11351
11385
  return;
11352
11386
  }
11387
+ if (d instanceof Magnifier) {
11388
+ this.paintMagnifier(ctx, d, proj, theme);
11389
+ return;
11390
+ }
11353
11391
  if (d instanceof PatternDrawing) {
11354
11392
  this.paintPattern(ctx, d, proj, theme);
11355
11393
  return;
@@ -11842,6 +11880,216 @@ var DrawingPainter = class {
11842
11880
  ctx.textBaseline = "alphabetic";
11843
11881
  }
11844
11882
  }
11883
+ /** Placement preview for the magnifier: a dashed rectangle outline only — no backdrop and
11884
+ * no series read, so dragging the area open never kicks a fetch per cursor move. */
11885
+ paintMagnifierGhost(ctx, d, proj, theme) {
11886
+ const r = d.rect(proj);
11887
+ if (!r) return;
11888
+ ctx.save();
11889
+ ctx.strokeStyle = d.style.lineColor || contrastColor(theme.background);
11890
+ ctx.lineWidth = 1;
11891
+ ctx.setLineDash([4, 4]);
11892
+ ctx.strokeRect(Math.min(r.x1, r.x2), Math.min(r.y1, r.y2), Math.abs(r.x2 - r.x1), Math.abs(r.y2 - r.y1));
11893
+ ctx.restore();
11894
+ }
11895
+ /** Paint a magnifier: an opaque theme-background inset whose interior shows the chart's
11896
+ * market at a finer timeframe — candles at their true time/price positions, clipped to
11897
+ * the rectangle. Bars come through `Projector.seriesInRange` (cache read; `loading` and
11898
+ * `unavailable` states paint a short notice instead). The lower-timeframe candles shift
11899
+ * half a chart bar LEFT of their raw time pixel so each chart candle's visual cell —
11900
+ * centered on its open time — subdivides in place. */
11901
+ paintMagnifier(ctx, d, proj, theme) {
11902
+ const r = d.rect(proj);
11903
+ const a = d.anchors[0];
11904
+ const b = d.anchors[1];
11905
+ if (!r || !a || !b) return;
11906
+ const x0 = Math.min(r.x1, r.x2);
11907
+ const x1 = Math.max(r.x1, r.x2);
11908
+ const y0 = Math.min(r.y1, r.y2);
11909
+ const y1 = Math.max(r.y1, r.y2);
11910
+ const w = x1 - x0;
11911
+ const h = y1 - y0;
11912
+ ctx.save();
11913
+ ctx.globalAlpha = 1;
11914
+ ctx.fillStyle = theme.background;
11915
+ ctx.fillRect(x0, y0, w, h);
11916
+ ctx.restore();
11917
+ const from = Math.min(a.time, b.time);
11918
+ const to = Math.max(a.time, b.time);
11919
+ const chartBars = proj.barsBetween ? proj.barsBetween(from, to) : 0;
11920
+ const chartMs = chartBars > 0 ? (to - from) / chartBars : 0;
11921
+ const res = proj.seriesInRange && chartMs > 0 && w > 1 && h > 1 ? proj.seriesInRange(d.magnifier.timeframe, from, to + chartMs) : void 0;
11922
+ let seriesBars = res?.state === "ready" || res?.state === "loading" ? res.bars ?? [] : [];
11923
+ if (res && (res.state === "ready" || res.state === "loading") && seriesBars.length > 0) {
11924
+ const look = this.seriesLook;
11925
+ const transform = look.style !== "candles" ? barTransformFor(look.style) : null;
11926
+ if (transform) seriesBars = transform.full(seriesBars);
11927
+ const mode = look.style === "bars" ? "bars" : look.style === "line" || look.style === "baseline" ? "line" : look.style === "area" ? "area" : "candles";
11928
+ const halfPitch = (proj.xOf(from + chartMs) - proj.xOf(from)) / 2;
11929
+ ctx.save();
11930
+ ctx.beginPath();
11931
+ ctx.rect(x0, y0, w, h);
11932
+ ctx.clip();
11933
+ if (mode === "line" || mode === "area") {
11934
+ this.paintMagnifierLine(ctx, d, proj, seriesBars, res.barMs, halfPitch, y1, mode === "area", d.magnifier.upColor || look.lineColor);
11935
+ } else {
11936
+ this.paintMagnifierBars(ctx, d, proj, seriesBars, res.barMs, halfPitch, x0, x1, mode, d.magnifier.upColor || look.upColor, d.magnifier.downColor || look.downColor);
11937
+ }
11938
+ ctx.restore();
11939
+ } else if (res) {
11940
+ const notice = res.state === "loading" ? "Loading\u2026" : res.state === "ready" ? "No lower-timeframe data" : res.reason === "too-wide" ? "Area too wide for this timeframe" : res.reason === "none-lower" ? "No lower timeframe available" : res.reason === "not-lower" ? "Pick a timeframe below the chart" : "No data source";
11941
+ this.paintMagnifierNotice(ctx, notice, x0, y0, w, h, theme);
11942
+ }
11943
+ ctx.save();
11944
+ ctx.strokeStyle = d.style.lineColor || contrastColor(theme.background);
11945
+ ctx.lineWidth = d.style.lineWidth || 1;
11946
+ ctx.setLineDash(dashPattern(d.style.lineStyle, d.style.lineWidth || 1));
11947
+ ctx.strokeRect(x0, y0, w, h);
11948
+ ctx.restore();
11949
+ if (w > 44) {
11950
+ const label = magnifierTimeframeLabel(res?.state === "ready" || res?.state === "loading" ? res.timeframe : d.magnifier.timeframe);
11951
+ const chipH = 17;
11952
+ const gap = 4;
11953
+ const pane = proj.paneRect?.(d.paneId);
11954
+ const paneBottom = pane ? pane.top + pane.height : proj.height;
11955
+ const below = y1 + gap + chipH <= paneBottom;
11956
+ const chipY = below ? y1 + gap : y1 - gap - chipH;
11957
+ ctx.save();
11958
+ ctx.font = `10px ${theme.fontFamily}`;
11959
+ const tw = ctx.measureText(label).width;
11960
+ const caretW = 11;
11961
+ const chipW = tw + 12 + caretW;
11962
+ roundRect(ctx, x0, chipY, chipW, chipH, 3);
11963
+ ctx.fillStyle = theme.background;
11964
+ ctx.fill();
11965
+ ctx.strokeStyle = withAlpha(theme.textColor, 0.28);
11966
+ ctx.lineWidth = 1;
11967
+ ctx.setLineDash([]);
11968
+ ctx.stroke();
11969
+ ctx.fillStyle = theme.textColor;
11970
+ ctx.textAlign = "left";
11971
+ ctx.textBaseline = "middle";
11972
+ ctx.fillText(label, x0 + 6, chipY + chipH / 2 + 0.5);
11973
+ const cxr = x0 + 6 + tw + 5;
11974
+ const cyr = chipY + chipH / 2;
11975
+ ctx.strokeStyle = withAlpha(theme.textColor, 0.7);
11976
+ ctx.lineWidth = 1.2;
11977
+ ctx.beginPath();
11978
+ ctx.moveTo(cxr, cyr - 1.5);
11979
+ ctx.lineTo(cxr + 2.5, cyr + 1.5);
11980
+ ctx.lineTo(cxr + 5, cyr - 1.5);
11981
+ ctx.stroke();
11982
+ ctx.restore();
11983
+ d.chipRect = { x: x0, y: chipY, w: chipW, h: chipH };
11984
+ } else {
11985
+ d.chipRect = null;
11986
+ }
11987
+ }
11988
+ /** The magnifier's candle/bar loop: each bar's cell spans its open→close time (shifted left
11989
+ * by half a chart bar). Candles: wick always, body once the cell is wide enough to carry
11990
+ * one. OHLC bars: the high–low spine with open/close ticks once the cell has the room. */
11991
+ paintMagnifierBars(ctx, d, proj, bars, barMs, halfPitch, x0, x1, mode, upColor, downColor) {
11992
+ ctx.setLineDash([]);
11993
+ ctx.lineWidth = 1;
11994
+ for (const bar of bars) {
11995
+ const cx0 = proj.xOf(bar.time) - halfPitch;
11996
+ const cx1 = proj.xOf(bar.time + barMs) - halfPitch;
11997
+ if (cx1 < x0 || cx0 > x1) continue;
11998
+ const yHigh = proj.yOf(bar.high, d.paneId);
11999
+ const yLow = proj.yOf(bar.low, d.paneId);
12000
+ const yOpen = proj.yOf(bar.open, d.paneId);
12001
+ const yClose = proj.yOf(bar.close, d.paneId);
12002
+ if (yHigh == null || yLow == null || yOpen == null || yClose == null) continue;
12003
+ const color = bar.close >= bar.open ? upColor : downColor;
12004
+ const cellW = cx1 - cx0;
12005
+ const cx = (cx0 + cx1) / 2;
12006
+ ctx.strokeStyle = color;
12007
+ ctx.beginPath();
12008
+ ctx.moveTo(cx, yHigh);
12009
+ ctx.lineTo(cx, yLow);
12010
+ ctx.stroke();
12011
+ if (cellW < 3) continue;
12012
+ if (mode === "bars") {
12013
+ const tick = Math.max(1, cellW * 0.35);
12014
+ ctx.beginPath();
12015
+ ctx.moveTo(cx - tick, yOpen);
12016
+ ctx.lineTo(cx, yOpen);
12017
+ ctx.moveTo(cx, yClose);
12018
+ ctx.lineTo(cx + tick, yClose);
12019
+ ctx.stroke();
12020
+ } else {
12021
+ const bw = Math.max(1, cellW * 0.7);
12022
+ ctx.fillStyle = color;
12023
+ ctx.fillRect(cx - bw / 2, Math.min(yOpen, yClose), bw, Math.max(1, Math.abs(yClose - yOpen)));
12024
+ }
12025
+ }
12026
+ }
12027
+ /** The magnifier's line/area rendering: a close polyline through each cell's center (same
12028
+ * half-chart-bar shift as the candles), with an optional translucent fill down to the
12029
+ * rectangle's bottom edge for the area style. Colored like the chart's own line series. */
12030
+ paintMagnifierLine(ctx, d, proj, bars, barMs, halfPitch, yBottom, area, color) {
12031
+ const pts = [];
12032
+ for (const bar of bars) {
12033
+ const y = proj.yOf(bar.close, d.paneId);
12034
+ if (y == null) continue;
12035
+ pts.push([proj.xOf(bar.time + barMs / 2) - halfPitch, y]);
12036
+ }
12037
+ if (pts.length < 2) return;
12038
+ if (area) {
12039
+ ctx.beginPath();
12040
+ ctx.moveTo(pts[0][0], yBottom);
12041
+ for (const [px, py] of pts) ctx.lineTo(px, py);
12042
+ ctx.lineTo(pts[pts.length - 1][0], yBottom);
12043
+ ctx.closePath();
12044
+ ctx.fillStyle = withAlpha(color, 0.15);
12045
+ ctx.fill();
12046
+ }
12047
+ ctx.setLineDash([]);
12048
+ ctx.lineWidth = 1.5;
12049
+ ctx.strokeStyle = color;
12050
+ ctx.beginPath();
12051
+ ctx.moveTo(pts[0][0], pts[0][1]);
12052
+ for (let i = 1; i < pts.length; i += 1) ctx.lineTo(pts[i][0], pts[i][1]);
12053
+ ctx.stroke();
12054
+ }
12055
+ /** Centered muted notice inside the magnifier rect (loading / unavailable states). */
12056
+ paintMagnifierNotice(ctx, text, x0, y0, w, h, theme) {
12057
+ if (w < 60 || h < 20) return;
12058
+ ctx.save();
12059
+ ctx.beginPath();
12060
+ ctx.rect(x0, y0, w, h);
12061
+ ctx.clip();
12062
+ ctx.font = `11px ${theme.fontFamily}`;
12063
+ ctx.fillStyle = withAlpha(theme.textColor, 0.55);
12064
+ ctx.textAlign = "center";
12065
+ ctx.textBaseline = "middle";
12066
+ ctx.fillText(text, x0 + w / 2, y0 + h / 2);
12067
+ ctx.restore();
12068
+ }
12069
+ /** A bottom-center pill prompting the armed tool's placement gesture (e.g. the magnifier's
12070
+ * "drag an area"). Painted by the drawings layer while the tool is armed and no placement
12071
+ * is in progress; chart-background fill so it reads as chrome over any content. */
12072
+ paintPlacementHint(ctx, text, theme, width, height) {
12073
+ ctx.save();
12074
+ ctx.font = `11px ${theme.fontFamily}`;
12075
+ const tw = ctx.measureText(text).width;
12076
+ const pillW = tw + 24;
12077
+ const pillH = 24;
12078
+ const x = (width - pillW) / 2;
12079
+ const y = height - pillH - 14;
12080
+ roundRect(ctx, x, y, pillW, pillH, pillH / 2);
12081
+ ctx.fillStyle = theme.background;
12082
+ ctx.fill();
12083
+ ctx.strokeStyle = withAlpha(theme.textColor, 0.28);
12084
+ ctx.lineWidth = 1;
12085
+ ctx.setLineDash([]);
12086
+ ctx.stroke();
12087
+ ctx.fillStyle = theme.textColor;
12088
+ ctx.textAlign = "center";
12089
+ ctx.textBaseline = "middle";
12090
+ ctx.fillText(text, width / 2, y + pillH / 2 + 0.5);
12091
+ ctx.restore();
12092
+ }
11845
12093
  /** Paint a fixed-range volume profile: horizontal histogram rows (up/down split) anchored to
11846
12094
  * the left or right of the time span, optional VAH / VAL / POC levels across the range, and
11847
12095
  * optional developing POC / VA polylines. Recomputes from the two anchors on every paint. */
@@ -13257,8 +13505,9 @@ function ensureStyles4() {
13257
13505
  if (!existing) document.head.appendChild(s);
13258
13506
  }
13259
13507
  var DrawingSettingsPopup = class {
13260
- constructor(host, theme) {
13508
+ constructor(host, theme, chartBarMs = () => 0) {
13261
13509
  this.host = host;
13510
+ this.chartBarMs = chartBarMs;
13262
13511
  this.el = null;
13263
13512
  this.tipEl = null;
13264
13513
  // floating hover-label (above/below the toolbar)
@@ -13283,6 +13532,14 @@ var DrawingSettingsPopup = class {
13283
13532
  this.theme = theme;
13284
13533
  this.settingsDialog = new DrawingSettingsDialog(host, theme);
13285
13534
  }
13535
+ /** The magnifier timeframe choices strictly below the chart's own bar duration
13536
+ * (`auto` rides along while at least one concrete lower step exists). */
13537
+ lowerTimeframeOptions() {
13538
+ const chartMs = this.chartBarMs();
13539
+ if (!(chartMs > 0)) return [...MAGNIFIER_TIMEFRAME_OPTIONS];
13540
+ const lower = MAGNIFIER_TIMEFRAME_OPTIONS.filter((o) => o.ms > 0 && o.ms < chartMs);
13541
+ return lower.length > 0 ? [MAGNIFIER_TIMEFRAME_OPTIONS[0], ...lower] : [];
13542
+ }
13286
13543
  setTheme(theme) {
13287
13544
  this.theme = theme;
13288
13545
  this.settingsDialog.setTheme(theme);
@@ -13325,7 +13582,20 @@ var DrawingSettingsPopup = class {
13325
13582
  const sz = drawing.size ?? "normal";
13326
13583
  bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label: sizeLabel }));
13327
13584
  }
13328
- if (paths.has("style.lineColor")) bar.appendChild(this.colorButton("Line color", BRUSH_ICON, drawing.style.lineColor || DEFAULT_DRAWING_COLOR, (v) => actions.patch({ "style.lineColor": v })));
13585
+ if (paths.has("magnifier.timeframe") && drawing instanceof Magnifier) {
13586
+ const options = this.lowerTimeframeOptions();
13587
+ if (options.length > 0) {
13588
+ bar.appendChild(
13589
+ this.dropdown("Lower timeframe", options.map((o) => o.value), drawing.magnifier.timeframe, () => "", (v) => actions.patch({ "magnifier.timeframe": v }), {
13590
+ label: (v) => magnifierTimeframeLabel(String(v)),
13591
+ labelInTrigger: true
13592
+ })
13593
+ );
13594
+ }
13595
+ bar.appendChild(this.colorButton("Up candles", BUCKET_ICON, drawing.magnifier.upColor || t.upColor, (v) => actions.patch({ "magnifier.upColor": v })));
13596
+ bar.appendChild(this.colorButton("Down candles", BUCKET_ICON, drawing.magnifier.downColor || t.downColor, (v) => actions.patch({ "magnifier.downColor": v })));
13597
+ }
13598
+ if (paths.has("style.lineColor")) bar.appendChild(this.colorButton("Line color", BRUSH_ICON, drawing.style.lineColor || (drawing instanceof Magnifier ? contrastColor(this.theme.background) : DEFAULT_DRAWING_COLOR), (v) => actions.patch({ "style.lineColor": v })));
13329
13599
  if (paths.has("style.lineWidth")) {
13330
13600
  const wf = schema.fields.find((f) => f.path === "style.lineWidth");
13331
13601
  if (wf?.kind === "number" && (wf.min ?? 1) > 1) {
@@ -13647,6 +13917,7 @@ var DrawingSettingsPopup = class {
13647
13917
  this.colorPop = null;
13648
13918
  this.colorOwner = null;
13649
13919
  }
13920
+ opts.onClose?.();
13650
13921
  }
13651
13922
  });
13652
13923
  const el = pop.el;
@@ -13665,6 +13936,49 @@ var DrawingSettingsPopup = class {
13665
13936
  pop.show();
13666
13937
  return pop;
13667
13938
  }
13939
+ /**
13940
+ * A standalone timeframe menu for the magnifier's ON-CHART chip. The chip lives on
13941
+ * canvas, so a transient invisible anchor is dropped at its pixel rect for the popover
13942
+ * to position against, and removed again when the menu closes. Independent of the
13943
+ * quick toolbar — the chip works without selecting the drawing first.
13944
+ */
13945
+ openMagnifierTimeframeMenu(rect, current, onPick) {
13946
+ ensureStyles4();
13947
+ closeOpenPopovers();
13948
+ const options = this.lowerTimeframeOptions();
13949
+ const anchor = document.createElement("div");
13950
+ anchor.style.cssText = `position:absolute;left:${rect.x}px;top:${rect.y}px;width:${rect.w}px;height:${rect.h}px;pointer-events:none;`;
13951
+ this.host.appendChild(anchor);
13952
+ this.menuPop = this.hostFloat(anchor, {
13953
+ zIndex: 26,
13954
+ padding: "4px",
13955
+ onClose: () => anchor.remove(),
13956
+ fill: (menu, pop) => {
13957
+ if (options.length === 0) {
13958
+ const note = document.createElement("div");
13959
+ note.style.cssText = "padding:6px 10px;opacity:0.65;white-space:nowrap;";
13960
+ note.textContent = "No lower timeframe available";
13961
+ menu.appendChild(note);
13962
+ return;
13963
+ }
13964
+ for (const o of options) {
13965
+ const item = document.createElement("button");
13966
+ item.type = "button";
13967
+ item.className = "vela-dpop-item";
13968
+ item.dataset.active = o.value === current ? "1" : "0";
13969
+ item.style.cssText = "display:flex;align-items:center;min-width:88px;padding:5px 10px;border:none;border-radius:5px;color:inherit;cursor:pointer;text-align:left;font:inherit;font-variant-numeric:tabular-nums;";
13970
+ item.textContent = o.label;
13971
+ item.addEventListener("click", (e) => {
13972
+ e.stopPropagation();
13973
+ pop.hide();
13974
+ onPick(o.value);
13975
+ });
13976
+ menu.appendChild(item);
13977
+ }
13978
+ }
13979
+ });
13980
+ this.menuOwner = anchor;
13981
+ }
13668
13982
  /** A floating list of one-shot actions (icon + label rows) opened by the kebab. */
13669
13983
  openActionMenu(anchor, rows) {
13670
13984
  this.menuPop = this.hostFloat(anchor, {
@@ -13775,10 +14089,13 @@ var DrawingSettingsPopup = class {
13775
14089
  let cur = current;
13776
14090
  const paint = (v) => {
13777
14091
  b.replaceChildren();
13778
- const ic = document.createElement("span");
13779
- ic.style.cssText = "display:flex;";
13780
- ic.innerHTML = sized(render(v));
13781
- b.appendChild(ic);
14092
+ const glyph = render(v);
14093
+ if (glyph) {
14094
+ const ic = document.createElement("span");
14095
+ ic.style.cssText = "display:flex;";
14096
+ ic.innerHTML = sized(glyph);
14097
+ b.appendChild(ic);
14098
+ }
13782
14099
  if (opts.label && opts.labelInTrigger) {
13783
14100
  const tx = document.createElement("span");
13784
14101
  tx.textContent = opts.label(v);
@@ -13820,10 +14137,13 @@ var DrawingSettingsPopup = class {
13820
14137
  item.className = "vela-dpop-item";
13821
14138
  item.dataset.active = active ? "1" : "0";
13822
14139
  item.style.cssText = `display:flex;align-items:center;gap:8px;${label ? "min-width:118px;" : ""}padding:5px 8px;border:none;border-radius:5px;color:inherit;cursor:pointer;text-align:left;font:inherit;`;
13823
- const ic = document.createElement("span");
13824
- ic.style.cssText = "display:flex;flex:none;width:22px;justify-content:center;";
13825
- ic.innerHTML = sized(render(v), 18);
13826
- item.appendChild(ic);
14140
+ const glyph = render(v);
14141
+ if (glyph) {
14142
+ const ic = document.createElement("span");
14143
+ ic.style.cssText = "display:flex;flex:none;width:22px;justify-content:center;";
14144
+ ic.innerHTML = sized(glyph, 18);
14145
+ item.appendChild(ic);
14146
+ }
13827
14147
  if (label) {
13828
14148
  const tx = document.createElement("span");
13829
14149
  tx.textContent = label(v);
@@ -14175,6 +14495,9 @@ var UserDrawingController = class {
14175
14495
  this.intentCb = null;
14176
14496
  /** Another chart's in-progress placement, mirrored here as a ghost (drawings sync). */
14177
14497
  this.externalGhost = null;
14498
+ /** Core-pushed series gateway (finer-timeframe bars for data-driven drawings). */
14499
+ this.seriesGw = null;
14500
+ this.seriesGwUnsub = null;
14178
14501
  /** Last draft fingerprint reported upstream — gates the per-render emission to actual changes. */
14179
14502
  this.lastDraftKey = null;
14180
14503
  this.measure = new MeasureOverlay();
@@ -14202,7 +14525,7 @@ var UserDrawingController = class {
14202
14525
  this.textEditor = null;
14203
14526
  this.painter = new DrawingPainter();
14204
14527
  this.ctx = canvas.getContext("2d");
14205
- this.popup = new DrawingSettingsPopup(overlayHost, deps.theme());
14528
+ this.popup = new DrawingSettingsPopup(overlayHost, deps.theme(), () => deps.chartBarMs());
14206
14529
  this.toolbar = new DrawingToolbar(
14207
14530
  toolbarHost,
14208
14531
  deps.theme(),
@@ -14260,6 +14583,22 @@ var UserDrawingController = class {
14260
14583
  const shown = this.toolbarVisible && !this.mobileLayout;
14261
14584
  this.deps.setToolbarGutter(shown ? this.toolbarCollapsed ? TOOLBAR_COLLAPSED_WIDTH : TOOLBAR_WIDTH : 0);
14262
14585
  }
14586
+ /** Core push: the series gateway data-driven drawings read finer-timeframe bars
14587
+ * through (surfaced to them as `Projector.seriesInRange`). A landed background
14588
+ * fetch repaints both this layer and the interleave slices under the series. */
14589
+ setSeriesGateway(gateway) {
14590
+ this.seriesGwUnsub?.();
14591
+ this.seriesGw = gateway;
14592
+ this.seriesGwUnsub = gateway.onUpdate(() => {
14593
+ this.invalidateSlices();
14594
+ this.render();
14595
+ this.deps.requestDataPaint();
14596
+ });
14597
+ }
14598
+ /** The pushed series gateway, or null before the core provides one. */
14599
+ get seriesGateway() {
14600
+ return this.seriesGw;
14601
+ }
14263
14602
  /** Core push: mirror (or clear) another chart's in-progress placement as a ghost. */
14264
14603
  setExternalGhost(doc) {
14265
14604
  this.externalGhost = doc ? deserializeDrawing(doc) : null;
@@ -14377,8 +14716,34 @@ var UserDrawingController = class {
14377
14716
  /** Should the drawing layer win this press (vs pan)? */
14378
14717
  claim(x, y) {
14379
14718
  if (this.measureMode || this.eraserMode) return true;
14719
+ if (this.magnifierChipAt(x, y)) return true;
14380
14720
  return this.interaction.claim(x, y);
14381
14721
  }
14722
+ /** The topmost visible (unlocked) magnifier whose timeframe chip contains (x, y) —
14723
+ * the chip's rect is what the painter measured last frame. */
14724
+ magnifierChipAt(x, y) {
14725
+ for (let i = this.drawings.length - 1; i >= 0; i -= 1) {
14726
+ const d = this.drawings[i];
14727
+ if (!(d instanceof Magnifier) || !d.visible || d.locked) continue;
14728
+ const r = d.chipRect;
14729
+ if (r && x >= r.x && x <= r.x + r.w && y >= r.y && y <= r.y + r.h) return d;
14730
+ }
14731
+ return null;
14732
+ }
14733
+ /** Open the on-chart chip's timeframe menu; the pick patches the drawing like a
14734
+ * settings-popup edit (same intent, same undo step). */
14735
+ openMagnifierChipMenu(drawing) {
14736
+ const rect = drawing.chipRect;
14737
+ if (!rect) return;
14738
+ const id = drawing.id;
14739
+ this.popup.openMagnifierTimeframeMenu(rect, drawing.magnifier.timeframe, (value) => {
14740
+ const d = this.drawings.find((x) => x.id === id);
14741
+ if (!(d instanceof Magnifier)) return;
14742
+ d.applySettings({ "magnifier.timeframe": value });
14743
+ this.render();
14744
+ this.emit({ kind: "edit", doc: d.serialize() });
14745
+ });
14746
+ }
14382
14747
  /** Delete the (unlocked) drawing under the cursor. True when one was removed.
14383
14748
  * Shared by the eraser (click + drag) and the middle-click shortcut. */
14384
14749
  deleteAt(x, y) {
@@ -14424,6 +14789,13 @@ var UserDrawingController = class {
14424
14789
  this.render();
14425
14790
  return;
14426
14791
  }
14792
+ if (this.activeTool == null) {
14793
+ const chipOwner = this.magnifierChipAt(x, y);
14794
+ if (chipOwner) {
14795
+ this.openMagnifierChipMenu(chipOwner);
14796
+ return;
14797
+ }
14798
+ }
14427
14799
  this.interaction.down(x, y, snap, shift);
14428
14800
  }
14429
14801
  pointerMove(x, y, snap = "off", shift = false) {
@@ -14642,6 +15014,7 @@ var UserDrawingController = class {
14642
15014
  /** Cursor hint while hovering — `'pointer'` over a drawing/handle, else null. */
14643
15015
  cursorAt(x, y) {
14644
15016
  if (this.eraserMode) return "pointer";
15017
+ if (this.activeTool == null && this.magnifierChipAt(x, y)) return "pointer";
14645
15018
  return this.interaction.cursorAt(x, y);
14646
15019
  }
14647
15020
  /** Right-click: an explicit escape back to the pointer. Cancels an in-progress
@@ -14820,6 +15193,7 @@ var UserDrawingController = class {
14820
15193
  if (!sctx) continue;
14821
15194
  sctx.setTransform(dpr, 0, 0, dpr, 0, 0);
14822
15195
  sctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
15196
+ this.painter.seriesLook = this.deps.seriesLook();
14823
15197
  this.painter.paintAll(sctx, drawings, proj, theme, EMPTY_TARGETS);
14824
15198
  const slices = out.get(paneId) ?? [];
14825
15199
  slices.push({ beforeZ, canvas });
@@ -14847,6 +15221,7 @@ var UserDrawingController = class {
14847
15221
  dragged: this.interaction.activeDragId(),
14848
15222
  mutedLabel: edited instanceof TextLabel ? edited.id : null
14849
15223
  };
15224
+ this.painter.seriesLook = this.deps.seriesLook();
14850
15225
  this.painter.paintAll(ctx, this.drawings.filter((d) => !this.isInterleaved(d)), proj, this.deps.theme(), targets);
14851
15226
  this.painter.paintHighlights(ctx, this.drawings.filter((d) => this.isInterleaved(d)), proj, handleIdsFor(targets));
14852
15227
  this.layoutTextEditor();
@@ -14854,6 +15229,10 @@ var UserDrawingController = class {
14854
15229
  if (ghost) this.painter.paintGhost(ctx, ghost, proj, this.deps.theme());
14855
15230
  if (this.externalGhost) this.painter.paintGhost(ctx, this.externalGhost, proj, this.deps.theme());
14856
15231
  this.emitDraft(ghost);
15232
+ if (this.activeTool && !ghost) {
15233
+ const hint = getDrawingType(this.activeTool)?.placementHint;
15234
+ if (hint) this.painter.paintPlacementHint(ctx, hint, this.deps.theme(), proj.width, proj.height);
15235
+ }
14857
15236
  const markers = this.interaction.placingMarkers(proj);
14858
15237
  if (markers) this.painter.paintHandles(ctx, markers);
14859
15238
  const m = this.interaction.snapMarker();
@@ -14865,6 +15244,9 @@ var UserDrawingController = class {
14865
15244
  this.closeTextEditor();
14866
15245
  this.popup.destroy();
14867
15246
  this.toolbar.destroy();
15247
+ this.seriesGwUnsub?.();
15248
+ this.seriesGwUnsub = null;
15249
+ this.seriesGw = null;
14868
15250
  this.intentCb = null;
14869
15251
  this.drawings = [];
14870
15252
  }
@@ -15262,7 +15644,7 @@ function mergeSlices(indicator, user) {
15262
15644
  }
15263
15645
 
15264
15646
  // src/renderers/native/drawings/Projector.ts
15265
- function createProjector(coords, paneOf, paneIdAtY, barsInRange) {
15647
+ function createProjector(coords, paneOf, paneIdAtY, barsInRange, seriesInRange) {
15266
15648
  return {
15267
15649
  xOf: (time) => coords.timeToX(time),
15268
15650
  yOf: (price, paneId) => {
@@ -15283,6 +15665,7 @@ function createProjector(coords, paneOf, paneIdAtY, barsInRange) {
15283
15665
  },
15284
15666
  barsBetween: (t1, t2) => Math.abs(coords.timeToLogical(t2) - coords.timeToLogical(t1)),
15285
15667
  barsInRange: barsInRange ? (from, to) => barsInRange(from, to) : void 0,
15668
+ seriesInRange,
15286
15669
  width: coords.width,
15287
15670
  height: coords.height
15288
15671
  };
@@ -17260,6 +17643,23 @@ var NativeRenderer = class {
17260
17643
  seriesBoundaries: (paneId) => this.scene.seriesBoundaries(paneId),
17261
17644
  priceZ: (paneId) => paneId === PRICE_PANE_ID ? this.scene.candleZ : null,
17262
17645
  requestDataPaint: () => this.scheduler.invalidate(3 /* Light */),
17646
+ // The look the price series ACTUALLY paints with: candle colors resolved through
17647
+ // the per-style override, line/area colors through their configured styles — so
17648
+ // series-mirroring content (the magnifier inset) matches the chart exactly.
17649
+ seriesLook: () => {
17650
+ const st = this.scene.style;
17651
+ const paint = effectiveCandlePaint(st.candle, this.scene.candleOverride, this.theme.upColor, this.theme.downColor);
17652
+ const barsUp = st.bars.upColor ?? this.theme.upColor;
17653
+ const barsDown = st.bars.downColor ?? this.theme.downColor;
17654
+ const style = this.scene.priceStyle;
17655
+ return {
17656
+ style,
17657
+ upColor: style === "bars" ? barsUp : paint.up,
17658
+ downColor: style === "bars" ? barsDown : paint.down,
17659
+ lineColor: style === "area" ? st.area.lineColor ?? this.theme.upColor : st.line.color ?? this.theme.upColor
17660
+ };
17661
+ },
17662
+ chartBarMs: () => this.coords.barInterval,
17263
17663
  snap: (pt, paneId, mode, cursorPx) => this.snapToCandle(pt, paneId, mode, cursorPx),
17264
17664
  setSnapMode: (mode) => this.setSnapMode(mode),
17265
17665
  setToolbarGutter: (px) => this.setToolbarGutter(px)
@@ -18653,7 +19053,8 @@ var NativeRenderer = class {
18653
19053
  return p ? { scale: p.scale, bounds: p.bounds, collapsed: p.collapsed } : null;
18654
19054
  },
18655
19055
  (y) => this.paneNodeAtY(y)?.id ?? null,
18656
- (from, to) => this.barsInTimeRange(from, to)
19056
+ (from, to) => this.barsInTimeRange(from, to),
19057
+ this.userDrawings?.seriesGateway ? (tf, from, to) => this.userDrawings.seriesGateway.seriesInRange(tf, from, to) : void 0
18657
19058
  );
18658
19059
  }
18659
19060
  /** OHLC bars whose open-time falls within `[from, to]` (inclusive) — the data a regression
@@ -19574,17 +19975,179 @@ function presetToRange(preset, bars) {
19574
19975
  return { from: Math.max(first, from), to: last };
19575
19976
  }
19576
19977
 
19577
- // src/core/price-styles/BarTransform.ts
19578
- function barTransformFor(style) {
19579
- return chartType(style)?.barTransform ?? null;
19580
- }
19581
- function parseExtendedTicker(symbol) {
19582
- const at = symbol.lastIndexOf(";");
19583
- if (at <= 0 || at === symbol.length - 1) return { symbol, modifier: null, transform: null };
19584
- const modifier = symbol.slice(at + 1).toLowerCase();
19585
- if (modifier !== "standard" && !tickerModifierIds().includes(modifier)) return { symbol, modifier: null, transform: null };
19586
- return { symbol: symbol.slice(0, at), modifier, transform: barTransformFor(modifier) };
19587
- }
19978
+ // src/core/engine/DrawingSeriesService.ts
19979
+ var MAX_BARS = 5e3;
19980
+ var PAD_FRAC = 0.25;
19981
+ var MAX_ENTRIES = 16;
19982
+ var RETRY_MS = 15e3;
19983
+ var AUTO_STEPS = ["240", "60", "30", "15", "5", "1"];
19984
+ var DrawingSeriesService = class {
19985
+ constructor(deps) {
19986
+ this.deps = deps;
19987
+ /** Cached windows per `market|timeframe` key, newest-used last (LRU across keys). */
19988
+ this.cache = /* @__PURE__ */ new Map();
19989
+ this.listeners = /* @__PURE__ */ new Set();
19990
+ }
19991
+ seriesInRange(timeframe, from, to) {
19992
+ if (!this.deps.canFetch()) return { state: "unavailable", reason: "no-source" };
19993
+ const resolved = this.resolveTimeframe(timeframe);
19994
+ if (typeof resolved !== "string") return { state: "unavailable", reason: resolved.reason };
19995
+ const barMs = timeframeToMs(resolved);
19996
+ const lo = Math.min(from, to);
19997
+ const hi = Math.max(from, to);
19998
+ if (!(hi > lo) || !(barMs > 0)) return { state: "unavailable", reason: "not-lower" };
19999
+ if ((hi - lo) / barMs > MAX_BARS) return { state: "unavailable", reason: "too-wide" };
20000
+ const key = `${this.deps.marketKey()}|${resolved}`;
20001
+ const entries = this.cache.get(key) ?? [];
20002
+ const covering = entries.find((e) => e.from <= lo && e.to >= hi);
20003
+ if (covering) {
20004
+ if (covering.pending) return this.loading(entries, resolved, barMs, lo, hi);
20005
+ if (covering.failedAt > 0) {
20006
+ if (Date.now() - covering.failedAt < RETRY_MS) return this.loading(entries, resolved, barMs, lo, hi);
20007
+ entries.splice(entries.indexOf(covering), 1);
20008
+ } else {
20009
+ this.maybeRefresh(key, covering, resolved, barMs, hi);
20010
+ return { state: "ready", bars: this.slice(covering.bars, lo, hi), timeframe: resolved, barMs };
20011
+ }
20012
+ }
20013
+ this.fetchWindow(key, entries, resolved, lo, hi);
20014
+ return this.loading(entries, resolved, barMs, lo, hi);
20015
+ }
20016
+ onUpdate(listener) {
20017
+ this.listeners.add(listener);
20018
+ return () => this.listeners.delete(listener);
20019
+ }
20020
+ // ── internals ──
20021
+ /** `'auto'` → the largest standard step at least 4× finer than the chart (else the finest
20022
+ * step still below it); an explicit timeframe passes only when strictly finer. Failures
20023
+ * distinguish "this pick isn't lower" from "NOTHING lower exists" (the chart is already
20024
+ * at the finest offered step) so the consumer can word its notice honestly. */
20025
+ resolveTimeframe(timeframe) {
20026
+ const chartMs = timeframeToMs(this.deps.chartTimeframe());
20027
+ const finest = AUTO_STEPS[AUTO_STEPS.length - 1];
20028
+ if (timeframeToMs(finest) >= chartMs) return { reason: "none-lower" };
20029
+ const tf = timeframe.trim() || "auto";
20030
+ if (tf === "auto") {
20031
+ for (const step of AUTO_STEPS) {
20032
+ if (timeframeToMs(step) <= chartMs / 4) return step;
20033
+ }
20034
+ return finest;
20035
+ }
20036
+ return timeframeToMs(tf) < chartMs ? tf : { reason: "not-lower" };
20037
+ }
20038
+ /** The `loading` answer, carrying best-effort PARTIAL bars from settled overlapping
20039
+ * windows — a widened window keeps painting what it already has while it fetches. */
20040
+ loading(entries, timeframe, barMs, lo, hi) {
20041
+ const partial = /* @__PURE__ */ new Map();
20042
+ for (const e of entries) {
20043
+ if (e.pending || e.failedAt > 0) continue;
20044
+ if (e.to < lo || e.from > hi) continue;
20045
+ for (const b of this.slice(e.bars, lo, hi)) partial.set(b.time, b);
20046
+ }
20047
+ if (partial.size === 0) return { state: "loading", timeframe, barMs };
20048
+ const bars = [...partial.values()].sort((a, b) => a.time - b.time);
20049
+ return { state: "loading", timeframe, barMs, bars };
20050
+ }
20051
+ /** Kick ONE background fetch for the padded window. Any OVERLAPPING in-flight fetch
20052
+ * defers this one (a corner drag repaints per pointer move — kicking a window per
20053
+ * frame would spam the provider); when it lands, the next paint re-evaluates. */
20054
+ fetchWindow(key, entries, timeframe, lo, hi) {
20055
+ if (entries.some((e) => e.pending && e.to >= lo && e.from <= hi)) return;
20056
+ const pad = (hi - lo) * PAD_FRAC;
20057
+ const entry = { from: lo - pad, to: hi + pad, bars: [], fetchedAt: 0, pending: true, failedAt: 0 };
20058
+ entries.push(entry);
20059
+ this.cache.set(key, entries);
20060
+ this.evict();
20061
+ void this.deps.fetchBars(timeframe, { from: entry.from, to: entry.to }).then((bars) => {
20062
+ entry.bars = bars;
20063
+ entry.fetchedAt = Date.now();
20064
+ entry.pending = false;
20065
+ this.absorbOverlaps(key, entry);
20066
+ this.fire();
20067
+ }).catch(() => {
20068
+ entry.pending = false;
20069
+ entry.failedAt = Date.now();
20070
+ this.fire();
20071
+ });
20072
+ }
20073
+ /** A window whose right edge reaches the newest fetched bar refreshes at most once per
20074
+ * bar interval — new closed bars ride the feed's cache, only the live tail re-fetches. */
20075
+ maybeRefresh(key, entry, timeframe, barMs, hi) {
20076
+ if (entry.pending) return;
20077
+ const lastBar = entry.bars.length > 0 ? entry.bars[entry.bars.length - 1].time : entry.from;
20078
+ if (hi < lastBar) return;
20079
+ if (Date.now() - entry.fetchedAt < barMs) return;
20080
+ entry.pending = true;
20081
+ void this.deps.fetchBars(timeframe, { from: entry.from, to: entry.to }).then((bars) => {
20082
+ entry.bars = bars;
20083
+ entry.fetchedAt = Date.now();
20084
+ entry.pending = false;
20085
+ this.fire();
20086
+ }).catch(() => {
20087
+ entry.pending = false;
20088
+ entry.fetchedAt = Date.now();
20089
+ });
20090
+ }
20091
+ /** Merge windows that overlap `entry` into it (dedupe by bar time) so a key's list
20092
+ * converges instead of accumulating slivers. */
20093
+ absorbOverlaps(key, entry) {
20094
+ const entries = this.cache.get(key);
20095
+ if (!entries) return;
20096
+ for (let i = entries.length - 1; i >= 0; i -= 1) {
20097
+ const other = entries[i];
20098
+ if (other === entry || other.pending || other.failedAt > 0) continue;
20099
+ if (other.to < entry.from || other.from > entry.to) continue;
20100
+ const byTime = /* @__PURE__ */ new Map();
20101
+ for (const b of other.bars) byTime.set(b.time, b);
20102
+ for (const b of entry.bars) byTime.set(b.time, b);
20103
+ entry.bars = [...byTime.values()].sort((a, b) => a.time - b.time);
20104
+ entry.from = Math.min(entry.from, other.from);
20105
+ entry.to = Math.max(entry.to, other.to);
20106
+ entry.fetchedAt = Math.min(entry.fetchedAt, other.fetchedAt || entry.fetchedAt);
20107
+ entries.splice(i, 1);
20108
+ }
20109
+ }
20110
+ /** Drop the oldest settled windows once the global count passes {@link MAX_ENTRIES}. */
20111
+ evict() {
20112
+ let total = 0;
20113
+ for (const entries of this.cache.values()) total += entries.length;
20114
+ while (total > MAX_ENTRIES) {
20115
+ let oldestKey = null;
20116
+ let oldestIdx = -1;
20117
+ let oldestAt = Infinity;
20118
+ for (const [key, entries2] of this.cache) {
20119
+ for (let i = 0; i < entries2.length; i += 1) {
20120
+ const e = entries2[i];
20121
+ if (e.pending) continue;
20122
+ const at = e.fetchedAt || e.failedAt;
20123
+ if (at < oldestAt) {
20124
+ oldestKey = key;
20125
+ oldestIdx = i;
20126
+ oldestAt = at;
20127
+ }
20128
+ }
20129
+ }
20130
+ if (oldestKey == null) return;
20131
+ const entries = this.cache.get(oldestKey);
20132
+ entries.splice(oldestIdx, 1);
20133
+ if (entries.length === 0) this.cache.delete(oldestKey);
20134
+ total -= 1;
20135
+ }
20136
+ }
20137
+ /** Bars whose open time falls within `[lo, hi]` (ascending input → linear scan is fine). */
20138
+ slice(bars, lo, hi) {
20139
+ const out = [];
20140
+ for (const b of bars) {
20141
+ if (b.time < lo) continue;
20142
+ if (b.time > hi) break;
20143
+ out.push(b);
20144
+ }
20145
+ return out;
20146
+ }
20147
+ fire() {
20148
+ for (const l of [...this.listeners]) l();
20149
+ }
20150
+ };
19588
20151
 
19589
20152
  // src/core/engine/EngineOrchestrator.ts
19590
20153
  var VIEWPORT_DEBOUNCE_MS = 150;
@@ -19592,7 +20155,6 @@ var RUN_EMIT_THROTTLE_MS = 1e3;
19592
20155
  var PREVIEW_BARS = 300;
19593
20156
  var SINGLE_LOAD_BARS = 5e3;
19594
20157
  var CHUNK_BARS = 1e4;
19595
- var FIRST_PAINT_BARS = 100;
19596
20158
  var GAP_FACTOR = 1.5;
19597
20159
  var HEAL_COOLDOWN_MS = 5e3;
19598
20160
  var EngineOrchestrator = class _EngineOrchestrator {
@@ -19710,7 +20272,13 @@ var EngineOrchestrator = class _EngineOrchestrator {
19710
20272
  const initialStyle = this.renderer.readFeature("priceStyle");
19711
20273
  if (typeof initialStyle === "string") this.priceStyle = initialStyle;
19712
20274
  this.barTransform = barTransformFor(initialStyle);
19713
- this.drawings = new DrawingController(this.renderer, this.events, config.drawings);
20275
+ const drawingSeries = new DrawingSeriesService({
20276
+ fetchBars: (tf, range) => this.fetchSeries(this.config.market.symbol ?? "", tf, range),
20277
+ canFetch: () => !!this.feed.loadRange && !this.config.market.data?.length && !!this.config.market.symbol,
20278
+ chartTimeframe: () => this.config.market.timeframe ?? "60",
20279
+ marketKey: () => `${this.config.market.symbol ?? ""}|${this.config.market.session ?? ""}`
20280
+ });
20281
+ this.drawings = new DrawingController(this.renderer, this.events, config.drawings, drawingSeries);
19714
20282
  this.unresolvedUnsub = this.feed.onUnresolved?.((info) => {
19715
20283
  this.endLoad();
19716
20284
  this.events.emit("data:unresolved", info);
@@ -19862,7 +20430,6 @@ var EngineOrchestrator = class _EngineOrchestrator {
19862
20430
  let painted = false;
19863
20431
  const paint = (bars, final) => {
19864
20432
  if (this.generation !== gen || !final && bars.length === 0) return;
19865
- if (!painted && !final && bars.length < Math.min(requested, FIRST_PAINT_BARS)) return;
19866
20433
  this.setBarSeries(bars, painted ? { preserveView: true } : void 0);
19867
20434
  if (!painted && bars.length > 0) {
19868
20435
  painted = true;
@@ -19881,10 +20448,14 @@ var EngineOrchestrator = class _EngineOrchestrator {
19881
20448
  }
19882
20449
  };
19883
20450
  abort.signal.addEventListener("abort", () => signal(true), { once: true });
19884
- this.feed.loadProgressive(market, (bars) => {
19885
- paint(bars, false);
19886
- if (painted) signal(true);
19887
- }, { signal: abort.signal }).then((full) => {
20451
+ this.feed.loadProgressive(
20452
+ market,
20453
+ (bars) => {
20454
+ paint(bars, false);
20455
+ if (painted) signal(true);
20456
+ },
20457
+ { signal: abort.signal }
20458
+ ).then((full) => {
19888
20459
  if (this.progressiveAbort === abort) this.progressiveAbort = null;
19889
20460
  if (full == null) return signal(false);
19890
20461
  if (this.generation !== gen) return signal(true);
@@ -19971,7 +20542,14 @@ var EngineOrchestrator = class _EngineOrchestrator {
19971
20542
  * an in-flight `setMarket` immediately (the config mutates before the load). */
19972
20543
  marketSnapshot() {
19973
20544
  const m = this.config.market;
19974
- return { symbol: m.symbol, provider: parseSymbol(m.symbol ?? "").provider ?? void 0, timeframe: m.timeframe, bars: m.bars, session: m.session, offline: m.data !== void 0 };
20545
+ return {
20546
+ symbol: m.symbol,
20547
+ provider: parseSymbol(m.symbol ?? "").provider ?? void 0,
20548
+ timeframe: m.timeframe,
20549
+ bars: m.bars,
20550
+ session: m.session,
20551
+ offline: m.data !== void 0
20552
+ };
19975
20553
  }
19976
20554
  /**
19977
20555
  * Switch the chart's market IN PLACE — no destroy/recreate. The renderer stays
@@ -20037,10 +20615,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
20037
20615
  if (depthOnly) {
20038
20616
  this.extendDepth(gen, m.bars ?? 500);
20039
20617
  } else {
20040
- await Promise.race([
20041
- this.loadMarket(gen, { firstLoad: false }),
20042
- new Promise((resolve) => this.supersedeWaiters.push(resolve))
20043
- ]);
20618
+ await Promise.race([this.loadMarket(gen, { firstLoad: false }), new Promise((resolve) => this.supersedeWaiters.push(resolve))]);
20044
20619
  }
20045
20620
  if (this.generation !== gen) return;
20046
20621
  } finally {
@@ -20703,8 +21278,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
20703
21278
  onModel: (model) => {
20704
21279
  const first = !record.announced;
20705
21280
  const cause = record.pendingCause ?? "history";
21281
+ if (!this.applyModel(id, model)) return;
20706
21282
  record.pendingCause = void 0;
20707
- this.applyModel(id, model);
20708
21283
  this.emitContextChanged(id);
20709
21284
  this.emitScriptRun(id, cause, first);
20710
21285
  },
@@ -20997,10 +21572,16 @@ var EngineOrchestrator = class _EngineOrchestrator {
20997
21572
  * Apply an emitted model. First emission mounts (and routes the pane); a pending
20998
21573
  * structural change (after an input edit) remounts idempotently; everything else
20999
21574
  * (live tick / viewport re-run) value-patches.
21575
+ *
21576
+ * Returns false when the model was DEFERRED — an output-free model arriving while
21577
+ * the record is still loading and the chart has no bars (see below); every other
21578
+ * outcome, including the hidden drop, returns true so the caller's event semantics
21579
+ * stay unchanged.
21000
21580
  */
21001
21581
  applyModel(id, model) {
21002
21582
  const record = this.registry.get(id);
21003
- if (!record || record.hidden) return;
21583
+ if (!record || record.hidden) return true;
21584
+ if (record.loading && this.bars.length === 0 && !_EngineOrchestrator.modelHasOutput(model)) return false;
21004
21585
  const handle = this.handles.get(id);
21005
21586
  if (!record.renderHandle) {
21006
21587
  const paneId2 = this.routePane(id, model, record.options ?? {});
@@ -21010,7 +21591,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
21010
21591
  record.renderHandle = this.renderer.mountIndicator(model);
21011
21592
  record.pendingStructural = false;
21012
21593
  this.announce(record, handle);
21013
- return;
21594
+ return true;
21014
21595
  }
21015
21596
  let paneId = record.model?.paneId ?? "price";
21016
21597
  const prevOwnScale = record.model?.ownScale === true;
@@ -21038,6 +21619,11 @@ var EngineOrchestrator = class _EngineOrchestrator {
21038
21619
  }
21039
21620
  if (record.loading) this.setLoading(record, false);
21040
21621
  this.announce(record, handle);
21622
+ return true;
21623
+ }
21624
+ /** True when the model carries ANY executed output — series, drawings, bar colors, or trades. */
21625
+ static modelHasOutput(model) {
21626
+ return model.series.length > 0 || model.fills.length > 0 || model.backgrounds.length > 0 || model.priceLines.length > 0 || (model.lines?.length ?? 0) > 0 || (model.boxes?.length ?? 0) > 0 || (model.labels?.length ?? 0) > 0 || (model.polylines?.length ?? 0) > 0 || (model.linefills?.length ?? 0) > 0 || (model.tables?.length ?? 0) > 0 || (model.barColors?.length ?? 0) > 0 || (model.trades?.length ?? 0) > 0;
21041
21627
  }
21042
21628
  routePane(id, model, options) {
21043
21629
  if (options.pane === "new") return `pane-${id}`;