@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
package/dist/index.cjs CHANGED
@@ -5424,6 +5424,111 @@ function roundFloat(n) {
5424
5424
  return Math.round(n * 1e8) / 1e8;
5425
5425
  }
5426
5426
 
5427
+ // src/core/drawings/types/Magnifier.ts
5428
+ var MAGNIFIER_TIMEFRAME_OPTIONS = [
5429
+ { value: "auto", label: "Auto", ms: 0 },
5430
+ { value: "1", label: "1m", ms: 6e4 },
5431
+ { value: "5", label: "5m", ms: 3e5 },
5432
+ { value: "15", label: "15m", ms: 9e5 },
5433
+ { value: "30", label: "30m", ms: 18e5 },
5434
+ { value: "60", label: "1h", ms: 36e5 },
5435
+ { value: "240", label: "4h", ms: 144e5 },
5436
+ { value: "D", label: "1D", ms: 864e5 }
5437
+ ];
5438
+ function magnifierTimeframeLabel(value) {
5439
+ const opt = MAGNIFIER_TIMEFRAME_OPTIONS.find((o) => o.value === value);
5440
+ if (opt) return opt.label;
5441
+ const n = Number(value);
5442
+ if (Number.isFinite(n) && n > 0) {
5443
+ if (n % 1440 === 0) return `${n / 1440}D`;
5444
+ if (n % 60 === 0) return `${n / 60}h`;
5445
+ return `${n}m`;
5446
+ }
5447
+ return value;
5448
+ }
5449
+ function defaultMagnifierStyle() {
5450
+ return { timeframe: "auto", upColor: "", downColor: "" };
5451
+ }
5452
+ var Magnifier = class extends Drawing {
5453
+ constructor(init) {
5454
+ super(init);
5455
+ this.type = "magnifier";
5456
+ /** Pixel rect of the timeframe chip as painted last frame, caret included — the chip is
5457
+ * an interactive dropdown trigger, so the interaction layer needs the exact rect the
5458
+ * painter measured. Renderer-transient: never serialized, null while unpainted. */
5459
+ this.chipRect = null;
5460
+ if (!this.magnifier) this.magnifier = defaultMagnifierStyle();
5461
+ }
5462
+ anchorSchema() {
5463
+ return { min: 2, max: 2, slots: [{ role: "c1", free: "both" }, { role: "c2", free: "both" }] };
5464
+ }
5465
+ placementMode() {
5466
+ return "drag";
5467
+ }
5468
+ /** The pixel rectangle between the two corner anchors (painter + hit-test share it). */
5469
+ rect(proj) {
5470
+ const a = this.anchors[0];
5471
+ const b = this.anchors[1];
5472
+ if (!a || !b) return null;
5473
+ const ya = proj.yOf(a.price, this.paneId);
5474
+ const yb = proj.yOf(b.price, this.paneId);
5475
+ if (ya == null || yb == null) return null;
5476
+ return { x1: proj.xOf(a.time), y1: ya, x2: proj.xOf(b.time), y2: yb };
5477
+ }
5478
+ hitTest(px, py, proj, tol) {
5479
+ const r = this.rect(proj);
5480
+ if (!r) return false;
5481
+ if (pointInBox(px, py, r.x1, r.y1, r.x2, r.y2)) return true;
5482
+ const edges = [
5483
+ [r.x1, r.y1, r.x2, r.y1],
5484
+ [r.x2, r.y1, r.x2, r.y2],
5485
+ [r.x2, r.y2, r.x1, r.y2],
5486
+ [r.x1, r.y2, r.x1, r.y1]
5487
+ ];
5488
+ return edges.some((e) => distToSegment(px, py, e[0], e[1], e[2], e[3]) <= tol);
5489
+ }
5490
+ handlePoints(proj) {
5491
+ const r = this.rect(proj);
5492
+ return r ? [[r.x1, r.y1], [r.x2, r.y2]] : [];
5493
+ }
5494
+ hitHandle(px, py, proj, tol) {
5495
+ return handleAt(px, py, this.handlePoints(proj), tol + 3);
5496
+ }
5497
+ bounds(proj) {
5498
+ const r = this.rect(proj);
5499
+ if (!r) return null;
5500
+ return { x: Math.min(r.x1, r.x2), y: Math.min(r.y1, r.y2), w: Math.abs(r.x2 - r.x1), h: Math.abs(r.y2 - r.y1) };
5501
+ }
5502
+ priceRange() {
5503
+ const a = this.anchors[0];
5504
+ const b = this.anchors[1];
5505
+ if (!a || !b) return null;
5506
+ return { min: Math.min(a.price, b.price), max: Math.max(a.price, b.price) };
5507
+ }
5508
+ schema() {
5509
+ return {
5510
+ fields: [
5511
+ {
5512
+ path: "magnifier.timeframe",
5513
+ label: "Timeframe",
5514
+ kind: "select",
5515
+ options: MAGNIFIER_TIMEFRAME_OPTIONS,
5516
+ group: "behavior"
5517
+ },
5518
+ ...LINE_FIELDS.map((f) => ({ ...f, label: f.label.replace("Line", "Border") })),
5519
+ { path: "magnifier.upColor", label: "Up candles", kind: "color", group: "fill" },
5520
+ { path: "magnifier.downColor", label: "Down candles", kind: "color", group: "fill" }
5521
+ ]
5522
+ };
5523
+ }
5524
+ writeProps() {
5525
+ return { ...this.magnifier };
5526
+ }
5527
+ readProps(props) {
5528
+ this.magnifier = { ...defaultMagnifierStyle(), ...props };
5529
+ }
5530
+ };
5531
+
5427
5532
  // src/core/drawings/registry.ts
5428
5533
  var REGISTRY2 = /* @__PURE__ */ new Map();
5429
5534
  function registerDrawingType(meta) {
@@ -6083,6 +6188,22 @@ registerDrawingType({
6083
6188
  defaultStyle: { lineColor: DEFAULT_DRAWING_COLOR, lineWidth: 1, lineStyle: "solid" },
6084
6189
  create: (init) => new PositionTool(init)
6085
6190
  });
6191
+ var MAGNIFIER_ICON = svg24(
6192
+ '<circle cx="10.5" cy="10.5" r="6.5"/><path d="m15.3 15.3 5.2 5.2"/><path d="M8 12.5v-3M10.5 13.5v-5.5M13 12v-2"/>'
6193
+ );
6194
+ registerDrawingType({
6195
+ type: "magnifier",
6196
+ group: "measure",
6197
+ label: "Magnifier",
6198
+ icon: MAGNIFIER_ICON,
6199
+ // An empty border color means the THEME's contrast ink (white on dark, black on
6200
+ // light), resolved at paint time so it follows theme switches; a user pick wins.
6201
+ defaultStyle: { lineColor: "", lineWidth: 1, lineStyle: "solid" },
6202
+ coversSeries: true,
6203
+ // the inset's backdrop must sit over the base candles it replaces
6204
+ placementHint: "Drag an area on the chart to view it at a lower timeframe",
6205
+ create: (init) => new Magnifier(init)
6206
+ });
6086
6207
  var VWAP_ICON = svg24('<line x1="5" y1="3" x2="5" y2="21"/><path d="M5 16c4 0 5-9 8-9s3 5 8 3"/>');
6087
6208
  registerDrawingType({
6088
6209
  type: "anchoredvwap",
@@ -6122,7 +6243,7 @@ var GEOMETRY_TYPES = ["dedekind", "sonic", "supersonic", "goldensonic", "goldens
6122
6243
  var PATTERN_TYPES = ["xabcd", "abcd", "headshoulders"];
6123
6244
  var ELLIOTT_TYPES = ["elliottimpulse", "elliottcorrection"];
6124
6245
  var HARMONIC_TYPES = ["gartley", "bat", "butterfly", "crab", "shark", "cypher"];
6125
- var MEASUREMENT_TYPES = ["position", "datepricerange"];
6246
+ var MEASUREMENT_TYPES = ["position", "datepricerange", "magnifier"];
6126
6247
  var VOLUME_TYPES = ["anchoredvwap", "fixedrangevp"];
6127
6248
  var BRUSH_TYPES = ["freehand", "highlighter"];
6128
6249
  var ARROW_TYPES = ["arrow", "arrowmarkup", "arrowmarkdown"];
@@ -6486,7 +6607,7 @@ var DrawingHistory = class {
6486
6607
 
6487
6608
  // src/core/drawings/DrawingController.ts
6488
6609
  var DrawingController = class {
6489
- constructor(renderer, events, option) {
6610
+ constructor(renderer, events, option, seriesGateway) {
6490
6611
  this.events = events;
6491
6612
  this.store = new DrawingStore();
6492
6613
  this.history = new DrawingHistory();
@@ -6512,6 +6633,7 @@ var DrawingController = class {
6512
6633
  const { definition, visible } = buildToolbar(option);
6513
6634
  this.port.setToolbar(definition);
6514
6635
  this.port.showToolbar(visible);
6636
+ if (seriesGateway) this.port.setSeriesGateway?.(seriesGateway);
6515
6637
  this.subs.push(this.port.onDrawingIntent((i) => this.onIntent(i)));
6516
6638
  this.subs.push(this.store.onChange(() => this.sync()));
6517
6639
  }
@@ -6617,7 +6739,7 @@ var DrawingController = class {
6617
6739
  style,
6618
6740
  text: init.text,
6619
6741
  props: init.props,
6620
- zIndex: init.zIndex ?? this.startZ(init.paneId ?? "price")
6742
+ zIndex: init.zIndex ?? this.startZ(type, init.paneId ?? "price")
6621
6743
  });
6622
6744
  if (!d) return null;
6623
6745
  this.history.record(this.store.serialize());
@@ -6692,10 +6814,14 @@ var DrawingController = class {
6692
6814
  * (falling back to just under the pane's top series where there is no price — a study
6693
6815
  * pane). Half a key down never ties a series; drawings tying each other paint in insertion
6694
6816
  * order, so consecutive new drawings still stack newest-in-front. Undefined without a
6695
- * shared z space — the store then places it over the other drawings, its own layer's top. */
6696
- startZ(paneId) {
6817
+ * shared z space — the store then places it over the other drawings, its own layer's top.
6818
+ * A type that COVERS the series (an opaque inset, `coversSeries`) instead starts just
6819
+ * above the whole stack — under the candles its content would be buried. */
6820
+ startZ(type, paneId) {
6697
6821
  const range = this.port?.stackRange?.(paneId);
6698
- return range ? (range.price ?? range.front) - 0.5 : void 0;
6822
+ if (!range) return void 0;
6823
+ if (getDrawingType(type)?.coversSeries) return range.front + 0.5;
6824
+ return (range.price ?? range.front) - 0.5;
6699
6825
  }
6700
6826
  /** Programmatically select drawings (host UI → chart): shows the on-chart handles + toolbar.
6701
6827
  * `additive` toggles membership (matching shift-click) instead of replacing. */
@@ -6840,7 +6966,7 @@ var DrawingController = class {
6840
6966
  const style = last ? { ...i.doc.style, ...last } : i.doc.style;
6841
6967
  const d = deserializeDrawing({ ...i.doc, id: this.store.nextId(), style });
6842
6968
  if (!d) return;
6843
- if (!d.zIndex) d.zIndex = this.startZ(d.paneId) ?? 0;
6969
+ if (!d.zIndex) d.zIndex = this.startZ(d.type, d.paneId) ?? 0;
6844
6970
  this.history.record(before);
6845
6971
  this.store.add(d);
6846
6972
  this.captureStyle(d.id);
@@ -6938,6 +7064,180 @@ function timeframeToMs(timeframe) {
6938
7064
  return 36e5;
6939
7065
  }
6940
7066
 
7067
+ // src/core/engine/DrawingSeriesService.ts
7068
+ var MAX_BARS = 5e3;
7069
+ var PAD_FRAC = 0.25;
7070
+ var MAX_ENTRIES = 16;
7071
+ var RETRY_MS = 15e3;
7072
+ var AUTO_STEPS = ["240", "60", "30", "15", "5", "1"];
7073
+ var DrawingSeriesService = class {
7074
+ constructor(deps) {
7075
+ this.deps = deps;
7076
+ /** Cached windows per `market|timeframe` key, newest-used last (LRU across keys). */
7077
+ this.cache = /* @__PURE__ */ new Map();
7078
+ this.listeners = /* @__PURE__ */ new Set();
7079
+ }
7080
+ seriesInRange(timeframe, from, to) {
7081
+ if (!this.deps.canFetch()) return { state: "unavailable", reason: "no-source" };
7082
+ const resolved = this.resolveTimeframe(timeframe);
7083
+ if (typeof resolved !== "string") return { state: "unavailable", reason: resolved.reason };
7084
+ const barMs = timeframeToMs(resolved);
7085
+ const lo = Math.min(from, to);
7086
+ const hi = Math.max(from, to);
7087
+ if (!(hi > lo) || !(barMs > 0)) return { state: "unavailable", reason: "not-lower" };
7088
+ if ((hi - lo) / barMs > MAX_BARS) return { state: "unavailable", reason: "too-wide" };
7089
+ const key = `${this.deps.marketKey()}|${resolved}`;
7090
+ const entries = this.cache.get(key) ?? [];
7091
+ const covering = entries.find((e) => e.from <= lo && e.to >= hi);
7092
+ if (covering) {
7093
+ if (covering.pending) return this.loading(entries, resolved, barMs, lo, hi);
7094
+ if (covering.failedAt > 0) {
7095
+ if (Date.now() - covering.failedAt < RETRY_MS) return this.loading(entries, resolved, barMs, lo, hi);
7096
+ entries.splice(entries.indexOf(covering), 1);
7097
+ } else {
7098
+ this.maybeRefresh(key, covering, resolved, barMs, hi);
7099
+ return { state: "ready", bars: this.slice(covering.bars, lo, hi), timeframe: resolved, barMs };
7100
+ }
7101
+ }
7102
+ this.fetchWindow(key, entries, resolved, lo, hi);
7103
+ return this.loading(entries, resolved, barMs, lo, hi);
7104
+ }
7105
+ onUpdate(listener) {
7106
+ this.listeners.add(listener);
7107
+ return () => this.listeners.delete(listener);
7108
+ }
7109
+ // ── internals ──
7110
+ /** `'auto'` → the largest standard step at least 4× finer than the chart (else the finest
7111
+ * step still below it); an explicit timeframe passes only when strictly finer. Failures
7112
+ * distinguish "this pick isn't lower" from "NOTHING lower exists" (the chart is already
7113
+ * at the finest offered step) so the consumer can word its notice honestly. */
7114
+ resolveTimeframe(timeframe) {
7115
+ const chartMs = timeframeToMs(this.deps.chartTimeframe());
7116
+ const finest = AUTO_STEPS[AUTO_STEPS.length - 1];
7117
+ if (timeframeToMs(finest) >= chartMs) return { reason: "none-lower" };
7118
+ const tf = timeframe.trim() || "auto";
7119
+ if (tf === "auto") {
7120
+ for (const step of AUTO_STEPS) {
7121
+ if (timeframeToMs(step) <= chartMs / 4) return step;
7122
+ }
7123
+ return finest;
7124
+ }
7125
+ return timeframeToMs(tf) < chartMs ? tf : { reason: "not-lower" };
7126
+ }
7127
+ /** The `loading` answer, carrying best-effort PARTIAL bars from settled overlapping
7128
+ * windows — a widened window keeps painting what it already has while it fetches. */
7129
+ loading(entries, timeframe, barMs, lo, hi) {
7130
+ const partial = /* @__PURE__ */ new Map();
7131
+ for (const e of entries) {
7132
+ if (e.pending || e.failedAt > 0) continue;
7133
+ if (e.to < lo || e.from > hi) continue;
7134
+ for (const b of this.slice(e.bars, lo, hi)) partial.set(b.time, b);
7135
+ }
7136
+ if (partial.size === 0) return { state: "loading", timeframe, barMs };
7137
+ const bars = [...partial.values()].sort((a, b) => a.time - b.time);
7138
+ return { state: "loading", timeframe, barMs, bars };
7139
+ }
7140
+ /** Kick ONE background fetch for the padded window. Any OVERLAPPING in-flight fetch
7141
+ * defers this one (a corner drag repaints per pointer move — kicking a window per
7142
+ * frame would spam the provider); when it lands, the next paint re-evaluates. */
7143
+ fetchWindow(key, entries, timeframe, lo, hi) {
7144
+ if (entries.some((e) => e.pending && e.to >= lo && e.from <= hi)) return;
7145
+ const pad = (hi - lo) * PAD_FRAC;
7146
+ const entry = { from: lo - pad, to: hi + pad, bars: [], fetchedAt: 0, pending: true, failedAt: 0 };
7147
+ entries.push(entry);
7148
+ this.cache.set(key, entries);
7149
+ this.evict();
7150
+ void this.deps.fetchBars(timeframe, { from: entry.from, to: entry.to }).then((bars) => {
7151
+ entry.bars = bars;
7152
+ entry.fetchedAt = Date.now();
7153
+ entry.pending = false;
7154
+ this.absorbOverlaps(key, entry);
7155
+ this.fire();
7156
+ }).catch(() => {
7157
+ entry.pending = false;
7158
+ entry.failedAt = Date.now();
7159
+ this.fire();
7160
+ });
7161
+ }
7162
+ /** A window whose right edge reaches the newest fetched bar refreshes at most once per
7163
+ * bar interval — new closed bars ride the feed's cache, only the live tail re-fetches. */
7164
+ maybeRefresh(key, entry, timeframe, barMs, hi) {
7165
+ if (entry.pending) return;
7166
+ const lastBar = entry.bars.length > 0 ? entry.bars[entry.bars.length - 1].time : entry.from;
7167
+ if (hi < lastBar) return;
7168
+ if (Date.now() - entry.fetchedAt < barMs) return;
7169
+ entry.pending = true;
7170
+ void this.deps.fetchBars(timeframe, { from: entry.from, to: entry.to }).then((bars) => {
7171
+ entry.bars = bars;
7172
+ entry.fetchedAt = Date.now();
7173
+ entry.pending = false;
7174
+ this.fire();
7175
+ }).catch(() => {
7176
+ entry.pending = false;
7177
+ entry.fetchedAt = Date.now();
7178
+ });
7179
+ }
7180
+ /** Merge windows that overlap `entry` into it (dedupe by bar time) so a key's list
7181
+ * converges instead of accumulating slivers. */
7182
+ absorbOverlaps(key, entry) {
7183
+ const entries = this.cache.get(key);
7184
+ if (!entries) return;
7185
+ for (let i = entries.length - 1; i >= 0; i -= 1) {
7186
+ const other = entries[i];
7187
+ if (other === entry || other.pending || other.failedAt > 0) continue;
7188
+ if (other.to < entry.from || other.from > entry.to) continue;
7189
+ const byTime = /* @__PURE__ */ new Map();
7190
+ for (const b of other.bars) byTime.set(b.time, b);
7191
+ for (const b of entry.bars) byTime.set(b.time, b);
7192
+ entry.bars = [...byTime.values()].sort((a, b) => a.time - b.time);
7193
+ entry.from = Math.min(entry.from, other.from);
7194
+ entry.to = Math.max(entry.to, other.to);
7195
+ entry.fetchedAt = Math.min(entry.fetchedAt, other.fetchedAt || entry.fetchedAt);
7196
+ entries.splice(i, 1);
7197
+ }
7198
+ }
7199
+ /** Drop the oldest settled windows once the global count passes {@link MAX_ENTRIES}. */
7200
+ evict() {
7201
+ let total = 0;
7202
+ for (const entries of this.cache.values()) total += entries.length;
7203
+ while (total > MAX_ENTRIES) {
7204
+ let oldestKey = null;
7205
+ let oldestIdx = -1;
7206
+ let oldestAt = Infinity;
7207
+ for (const [key, entries2] of this.cache) {
7208
+ for (let i = 0; i < entries2.length; i += 1) {
7209
+ const e = entries2[i];
7210
+ if (e.pending) continue;
7211
+ const at = e.fetchedAt || e.failedAt;
7212
+ if (at < oldestAt) {
7213
+ oldestKey = key;
7214
+ oldestIdx = i;
7215
+ oldestAt = at;
7216
+ }
7217
+ }
7218
+ }
7219
+ if (oldestKey == null) return;
7220
+ const entries = this.cache.get(oldestKey);
7221
+ entries.splice(oldestIdx, 1);
7222
+ if (entries.length === 0) this.cache.delete(oldestKey);
7223
+ total -= 1;
7224
+ }
7225
+ }
7226
+ /** Bars whose open time falls within `[lo, hi]` (ascending input → linear scan is fine). */
7227
+ slice(bars, lo, hi) {
7228
+ const out = [];
7229
+ for (const b of bars) {
7230
+ if (b.time < lo) continue;
7231
+ if (b.time > hi) break;
7232
+ out.push(b);
7233
+ }
7234
+ return out;
7235
+ }
7236
+ fire() {
7237
+ for (const l of [...this.listeners]) l();
7238
+ }
7239
+ };
7240
+
6941
7241
  // src/data/symbol-groups.ts
6942
7242
  function isGroupRow(d) {
6943
7243
  return d.group != null && d.ticker === d.group;
@@ -7302,7 +7602,6 @@ var RUN_EMIT_THROTTLE_MS = 1e3;
7302
7602
  var PREVIEW_BARS = 300;
7303
7603
  var SINGLE_LOAD_BARS = 5e3;
7304
7604
  var CHUNK_BARS = 1e4;
7305
- var FIRST_PAINT_BARS = 100;
7306
7605
  var GAP_FACTOR = 1.5;
7307
7606
  var HEAL_COOLDOWN_MS = 5e3;
7308
7607
  var EngineOrchestrator = class _EngineOrchestrator {
@@ -7420,7 +7719,13 @@ var EngineOrchestrator = class _EngineOrchestrator {
7420
7719
  const initialStyle = this.renderer.readFeature("priceStyle");
7421
7720
  if (typeof initialStyle === "string") this.priceStyle = initialStyle;
7422
7721
  this.barTransform = barTransformFor(initialStyle);
7423
- this.drawings = new DrawingController(this.renderer, this.events, config.drawings);
7722
+ const drawingSeries = new DrawingSeriesService({
7723
+ fetchBars: (tf, range) => this.fetchSeries(this.config.market.symbol ?? "", tf, range),
7724
+ canFetch: () => !!this.feed.loadRange && !this.config.market.data?.length && !!this.config.market.symbol,
7725
+ chartTimeframe: () => this.config.market.timeframe ?? "60",
7726
+ marketKey: () => `${this.config.market.symbol ?? ""}|${this.config.market.session ?? ""}`
7727
+ });
7728
+ this.drawings = new DrawingController(this.renderer, this.events, config.drawings, drawingSeries);
7424
7729
  this.unresolvedUnsub = this.feed.onUnresolved?.((info) => {
7425
7730
  this.endLoad();
7426
7731
  this.events.emit("data:unresolved", info);
@@ -7572,7 +7877,6 @@ var EngineOrchestrator = class _EngineOrchestrator {
7572
7877
  let painted = false;
7573
7878
  const paint = (bars, final) => {
7574
7879
  if (this.generation !== gen || !final && bars.length === 0) return;
7575
- if (!painted && !final && bars.length < Math.min(requested, FIRST_PAINT_BARS)) return;
7576
7880
  this.setBarSeries(bars, painted ? { preserveView: true } : void 0);
7577
7881
  if (!painted && bars.length > 0) {
7578
7882
  painted = true;
@@ -7591,10 +7895,14 @@ var EngineOrchestrator = class _EngineOrchestrator {
7591
7895
  }
7592
7896
  };
7593
7897
  abort.signal.addEventListener("abort", () => signal(true), { once: true });
7594
- this.feed.loadProgressive(market, (bars) => {
7595
- paint(bars, false);
7596
- if (painted) signal(true);
7597
- }, { signal: abort.signal }).then((full) => {
7898
+ this.feed.loadProgressive(
7899
+ market,
7900
+ (bars) => {
7901
+ paint(bars, false);
7902
+ if (painted) signal(true);
7903
+ },
7904
+ { signal: abort.signal }
7905
+ ).then((full) => {
7598
7906
  if (this.progressiveAbort === abort) this.progressiveAbort = null;
7599
7907
  if (full == null) return signal(false);
7600
7908
  if (this.generation !== gen) return signal(true);
@@ -7681,7 +7989,14 @@ var EngineOrchestrator = class _EngineOrchestrator {
7681
7989
  * an in-flight `setMarket` immediately (the config mutates before the load). */
7682
7990
  marketSnapshot() {
7683
7991
  const m = this.config.market;
7684
- 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 };
7992
+ return {
7993
+ symbol: m.symbol,
7994
+ provider: parseSymbol(m.symbol ?? "").provider ?? void 0,
7995
+ timeframe: m.timeframe,
7996
+ bars: m.bars,
7997
+ session: m.session,
7998
+ offline: m.data !== void 0
7999
+ };
7685
8000
  }
7686
8001
  /**
7687
8002
  * Switch the chart's market IN PLACE — no destroy/recreate. The renderer stays
@@ -7747,10 +8062,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
7747
8062
  if (depthOnly) {
7748
8063
  this.extendDepth(gen, m.bars ?? 500);
7749
8064
  } else {
7750
- await Promise.race([
7751
- this.loadMarket(gen, { firstLoad: false }),
7752
- new Promise((resolve) => this.supersedeWaiters.push(resolve))
7753
- ]);
8065
+ await Promise.race([this.loadMarket(gen, { firstLoad: false }), new Promise((resolve) => this.supersedeWaiters.push(resolve))]);
7754
8066
  }
7755
8067
  if (this.generation !== gen) return;
7756
8068
  } finally {
@@ -8413,8 +8725,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
8413
8725
  onModel: (model) => {
8414
8726
  const first = !record.announced;
8415
8727
  const cause = record.pendingCause ?? "history";
8728
+ if (!this.applyModel(id, model)) return;
8416
8729
  record.pendingCause = void 0;
8417
- this.applyModel(id, model);
8418
8730
  this.emitContextChanged(id);
8419
8731
  this.emitScriptRun(id, cause, first);
8420
8732
  },
@@ -8707,10 +9019,16 @@ var EngineOrchestrator = class _EngineOrchestrator {
8707
9019
  * Apply an emitted model. First emission mounts (and routes the pane); a pending
8708
9020
  * structural change (after an input edit) remounts idempotently; everything else
8709
9021
  * (live tick / viewport re-run) value-patches.
9022
+ *
9023
+ * Returns false when the model was DEFERRED — an output-free model arriving while
9024
+ * the record is still loading and the chart has no bars (see below); every other
9025
+ * outcome, including the hidden drop, returns true so the caller's event semantics
9026
+ * stay unchanged.
8710
9027
  */
8711
9028
  applyModel(id, model) {
8712
9029
  const record = this.registry.get(id);
8713
- if (!record || record.hidden) return;
9030
+ if (!record || record.hidden) return true;
9031
+ if (record.loading && this.bars.length === 0 && !_EngineOrchestrator.modelHasOutput(model)) return false;
8714
9032
  const handle = this.handles.get(id);
8715
9033
  if (!record.renderHandle) {
8716
9034
  const paneId2 = this.routePane(id, model, record.options ?? {});
@@ -8720,7 +9038,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
8720
9038
  record.renderHandle = this.renderer.mountIndicator(model);
8721
9039
  record.pendingStructural = false;
8722
9040
  this.announce(record, handle);
8723
- return;
9041
+ return true;
8724
9042
  }
8725
9043
  let paneId = record.model?.paneId ?? "price";
8726
9044
  const prevOwnScale = record.model?.ownScale === true;
@@ -8748,6 +9066,11 @@ var EngineOrchestrator = class _EngineOrchestrator {
8748
9066
  }
8749
9067
  if (record.loading) this.setLoading(record, false);
8750
9068
  this.announce(record, handle);
9069
+ return true;
9070
+ }
9071
+ /** True when the model carries ANY executed output — series, drawings, bar colors, or trades. */
9072
+ static modelHasOutput(model) {
9073
+ 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;
8751
9074
  }
8752
9075
  routePane(id, model, options) {
8753
9076
  if (options.pane === "new") return `pane-${id}`;
@@ -9450,6 +9773,7 @@ function intervalMs(timeframe) {
9450
9773
  "4h": 144e5,
9451
9774
  "1d": 864e5,
9452
9775
  "1w": 6048e5,
9776
+ "1M": 2592e6,
9453
9777
  "1": 6e4,
9454
9778
  "5": 3e5,
9455
9779
  "15": 9e5,
@@ -9457,7 +9781,8 @@ function intervalMs(timeframe) {
9457
9781
  "60": 36e5,
9458
9782
  "240": 144e5,
9459
9783
  D: 864e5,
9460
- W: 6048e5
9784
+ W: 6048e5,
9785
+ M: 2592e6
9461
9786
  };
9462
9787
  return map[timeframe] ?? 36e5;
9463
9788
  }
@@ -18644,7 +18969,9 @@ var DrawingSceneRenderer = class {
18644
18969
  const lo = Math.min(from, to);
18645
18970
  const hi = Math.max(from, to);
18646
18971
  const visible = (a, b, extend) => {
18647
- if (extend !== "none") return true;
18972
+ if (extend === "both") return true;
18973
+ if (extend === "left") return Math.max(a, b) >= lo;
18974
+ if (extend === "right") return Math.min(a, b) <= hi;
18648
18975
  return Math.max(a, b) >= lo && Math.min(a, b) <= hi;
18649
18976
  };
18650
18977
  let min = Infinity;
@@ -21208,6 +21535,17 @@ var DrawingPainter = class {
21208
21535
  constructor() {
21209
21536
  /** The current `paintAll` call's interaction state, visible to the per-type painters. */
21210
21537
  this.targets = {};
21538
+ /** The chart's active series LOOK — style + resolved series colors — pushed by the
21539
+ * controller before each paint. The magnifier's inset mirrors both: candles/bars/line/
21540
+ * area restyle the paint (bar-transform styles like Heikin Ashi transform the fetched
21541
+ * bars; unknown/custom styles fall back to candles), and the colors default to the main
21542
+ * series' own so the inset reads as a finer copy of the chart. */
21543
+ this.seriesLook = {
21544
+ style: "candles",
21545
+ upColor: BULLISH,
21546
+ downColor: BEARISH,
21547
+ lineColor: BULLISH
21548
+ };
21211
21549
  }
21212
21550
  /** Paint every visible drawing, then selection handles for the targeted ones.
21213
21551
  * Each drawing is clipped to its own pane's rect (and skipped entirely while that pane
@@ -21252,6 +21590,8 @@ var DrawingPainter = class {
21252
21590
  ctx.globalAlpha = GHOST_ALPHA;
21253
21591
  if (ghost instanceof RegressionChannel || ghost instanceof FixedRangeVolumeProfile) {
21254
21592
  this.paintTimeSpanGhost(ctx, ghost, proj);
21593
+ } else if (ghost instanceof Magnifier) {
21594
+ this.paintMagnifierGhost(ctx, ghost, proj, theme);
21255
21595
  } else this.paintOne(ctx, ghost, proj, theme);
21256
21596
  ctx.globalAlpha = 1;
21257
21597
  }
@@ -21385,6 +21725,10 @@ var DrawingPainter = class {
21385
21725
  this.paintLabel(ctx, d, proj, theme);
21386
21726
  return;
21387
21727
  }
21728
+ if (d instanceof Magnifier) {
21729
+ this.paintMagnifier(ctx, d, proj, theme);
21730
+ return;
21731
+ }
21388
21732
  if (d instanceof PatternDrawing) {
21389
21733
  this.paintPattern(ctx, d, proj, theme);
21390
21734
  return;
@@ -21877,6 +22221,216 @@ var DrawingPainter = class {
21877
22221
  ctx.textBaseline = "alphabetic";
21878
22222
  }
21879
22223
  }
22224
+ /** Placement preview for the magnifier: a dashed rectangle outline only — no backdrop and
22225
+ * no series read, so dragging the area open never kicks a fetch per cursor move. */
22226
+ paintMagnifierGhost(ctx, d, proj, theme) {
22227
+ const r = d.rect(proj);
22228
+ if (!r) return;
22229
+ ctx.save();
22230
+ ctx.strokeStyle = d.style.lineColor || contrastColor(theme.background);
22231
+ ctx.lineWidth = 1;
22232
+ ctx.setLineDash([4, 4]);
22233
+ 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));
22234
+ ctx.restore();
22235
+ }
22236
+ /** Paint a magnifier: an opaque theme-background inset whose interior shows the chart's
22237
+ * market at a finer timeframe — candles at their true time/price positions, clipped to
22238
+ * the rectangle. Bars come through `Projector.seriesInRange` (cache read; `loading` and
22239
+ * `unavailable` states paint a short notice instead). The lower-timeframe candles shift
22240
+ * half a chart bar LEFT of their raw time pixel so each chart candle's visual cell —
22241
+ * centered on its open time — subdivides in place. */
22242
+ paintMagnifier(ctx, d, proj, theme) {
22243
+ const r = d.rect(proj);
22244
+ const a = d.anchors[0];
22245
+ const b = d.anchors[1];
22246
+ if (!r || !a || !b) return;
22247
+ const x0 = Math.min(r.x1, r.x2);
22248
+ const x1 = Math.max(r.x1, r.x2);
22249
+ const y0 = Math.min(r.y1, r.y2);
22250
+ const y1 = Math.max(r.y1, r.y2);
22251
+ const w = x1 - x0;
22252
+ const h = y1 - y0;
22253
+ ctx.save();
22254
+ ctx.globalAlpha = 1;
22255
+ ctx.fillStyle = theme.background;
22256
+ ctx.fillRect(x0, y0, w, h);
22257
+ ctx.restore();
22258
+ const from = Math.min(a.time, b.time);
22259
+ const to = Math.max(a.time, b.time);
22260
+ const chartBars = proj.barsBetween ? proj.barsBetween(from, to) : 0;
22261
+ const chartMs = chartBars > 0 ? (to - from) / chartBars : 0;
22262
+ const res = proj.seriesInRange && chartMs > 0 && w > 1 && h > 1 ? proj.seriesInRange(d.magnifier.timeframe, from, to + chartMs) : void 0;
22263
+ let seriesBars = res?.state === "ready" || res?.state === "loading" ? res.bars ?? [] : [];
22264
+ if (res && (res.state === "ready" || res.state === "loading") && seriesBars.length > 0) {
22265
+ const look = this.seriesLook;
22266
+ const transform = look.style !== "candles" ? barTransformFor(look.style) : null;
22267
+ if (transform) seriesBars = transform.full(seriesBars);
22268
+ const mode = look.style === "bars" ? "bars" : look.style === "line" || look.style === "baseline" ? "line" : look.style === "area" ? "area" : "candles";
22269
+ const halfPitch = (proj.xOf(from + chartMs) - proj.xOf(from)) / 2;
22270
+ ctx.save();
22271
+ ctx.beginPath();
22272
+ ctx.rect(x0, y0, w, h);
22273
+ ctx.clip();
22274
+ if (mode === "line" || mode === "area") {
22275
+ this.paintMagnifierLine(ctx, d, proj, seriesBars, res.barMs, halfPitch, y1, mode === "area", d.magnifier.upColor || look.lineColor);
22276
+ } else {
22277
+ this.paintMagnifierBars(ctx, d, proj, seriesBars, res.barMs, halfPitch, x0, x1, mode, d.magnifier.upColor || look.upColor, d.magnifier.downColor || look.downColor);
22278
+ }
22279
+ ctx.restore();
22280
+ } else if (res) {
22281
+ 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";
22282
+ this.paintMagnifierNotice(ctx, notice, x0, y0, w, h, theme);
22283
+ }
22284
+ ctx.save();
22285
+ ctx.strokeStyle = d.style.lineColor || contrastColor(theme.background);
22286
+ ctx.lineWidth = d.style.lineWidth || 1;
22287
+ ctx.setLineDash(dashPattern(d.style.lineStyle, d.style.lineWidth || 1));
22288
+ ctx.strokeRect(x0, y0, w, h);
22289
+ ctx.restore();
22290
+ if (w > 44) {
22291
+ const label = magnifierTimeframeLabel(res?.state === "ready" || res?.state === "loading" ? res.timeframe : d.magnifier.timeframe);
22292
+ const chipH = 17;
22293
+ const gap = 4;
22294
+ const pane = proj.paneRect?.(d.paneId);
22295
+ const paneBottom = pane ? pane.top + pane.height : proj.height;
22296
+ const below = y1 + gap + chipH <= paneBottom;
22297
+ const chipY = below ? y1 + gap : y1 - gap - chipH;
22298
+ ctx.save();
22299
+ ctx.font = `10px ${theme.fontFamily}`;
22300
+ const tw = ctx.measureText(label).width;
22301
+ const caretW = 11;
22302
+ const chipW = tw + 12 + caretW;
22303
+ roundRect(ctx, x0, chipY, chipW, chipH, 3);
22304
+ ctx.fillStyle = theme.background;
22305
+ ctx.fill();
22306
+ ctx.strokeStyle = withAlpha(theme.textColor, 0.28);
22307
+ ctx.lineWidth = 1;
22308
+ ctx.setLineDash([]);
22309
+ ctx.stroke();
22310
+ ctx.fillStyle = theme.textColor;
22311
+ ctx.textAlign = "left";
22312
+ ctx.textBaseline = "middle";
22313
+ ctx.fillText(label, x0 + 6, chipY + chipH / 2 + 0.5);
22314
+ const cxr = x0 + 6 + tw + 5;
22315
+ const cyr = chipY + chipH / 2;
22316
+ ctx.strokeStyle = withAlpha(theme.textColor, 0.7);
22317
+ ctx.lineWidth = 1.2;
22318
+ ctx.beginPath();
22319
+ ctx.moveTo(cxr, cyr - 1.5);
22320
+ ctx.lineTo(cxr + 2.5, cyr + 1.5);
22321
+ ctx.lineTo(cxr + 5, cyr - 1.5);
22322
+ ctx.stroke();
22323
+ ctx.restore();
22324
+ d.chipRect = { x: x0, y: chipY, w: chipW, h: chipH };
22325
+ } else {
22326
+ d.chipRect = null;
22327
+ }
22328
+ }
22329
+ /** The magnifier's candle/bar loop: each bar's cell spans its open→close time (shifted left
22330
+ * by half a chart bar). Candles: wick always, body once the cell is wide enough to carry
22331
+ * one. OHLC bars: the high–low spine with open/close ticks once the cell has the room. */
22332
+ paintMagnifierBars(ctx, d, proj, bars, barMs, halfPitch, x0, x1, mode, upColor, downColor) {
22333
+ ctx.setLineDash([]);
22334
+ ctx.lineWidth = 1;
22335
+ for (const bar of bars) {
22336
+ const cx0 = proj.xOf(bar.time) - halfPitch;
22337
+ const cx1 = proj.xOf(bar.time + barMs) - halfPitch;
22338
+ if (cx1 < x0 || cx0 > x1) continue;
22339
+ const yHigh = proj.yOf(bar.high, d.paneId);
22340
+ const yLow = proj.yOf(bar.low, d.paneId);
22341
+ const yOpen = proj.yOf(bar.open, d.paneId);
22342
+ const yClose = proj.yOf(bar.close, d.paneId);
22343
+ if (yHigh == null || yLow == null || yOpen == null || yClose == null) continue;
22344
+ const color = bar.close >= bar.open ? upColor : downColor;
22345
+ const cellW = cx1 - cx0;
22346
+ const cx = (cx0 + cx1) / 2;
22347
+ ctx.strokeStyle = color;
22348
+ ctx.beginPath();
22349
+ ctx.moveTo(cx, yHigh);
22350
+ ctx.lineTo(cx, yLow);
22351
+ ctx.stroke();
22352
+ if (cellW < 3) continue;
22353
+ if (mode === "bars") {
22354
+ const tick = Math.max(1, cellW * 0.35);
22355
+ ctx.beginPath();
22356
+ ctx.moveTo(cx - tick, yOpen);
22357
+ ctx.lineTo(cx, yOpen);
22358
+ ctx.moveTo(cx, yClose);
22359
+ ctx.lineTo(cx + tick, yClose);
22360
+ ctx.stroke();
22361
+ } else {
22362
+ const bw = Math.max(1, cellW * 0.7);
22363
+ ctx.fillStyle = color;
22364
+ ctx.fillRect(cx - bw / 2, Math.min(yOpen, yClose), bw, Math.max(1, Math.abs(yClose - yOpen)));
22365
+ }
22366
+ }
22367
+ }
22368
+ /** The magnifier's line/area rendering: a close polyline through each cell's center (same
22369
+ * half-chart-bar shift as the candles), with an optional translucent fill down to the
22370
+ * rectangle's bottom edge for the area style. Colored like the chart's own line series. */
22371
+ paintMagnifierLine(ctx, d, proj, bars, barMs, halfPitch, yBottom, area, color) {
22372
+ const pts = [];
22373
+ for (const bar of bars) {
22374
+ const y = proj.yOf(bar.close, d.paneId);
22375
+ if (y == null) continue;
22376
+ pts.push([proj.xOf(bar.time + barMs / 2) - halfPitch, y]);
22377
+ }
22378
+ if (pts.length < 2) return;
22379
+ if (area) {
22380
+ ctx.beginPath();
22381
+ ctx.moveTo(pts[0][0], yBottom);
22382
+ for (const [px, py] of pts) ctx.lineTo(px, py);
22383
+ ctx.lineTo(pts[pts.length - 1][0], yBottom);
22384
+ ctx.closePath();
22385
+ ctx.fillStyle = withAlpha(color, 0.15);
22386
+ ctx.fill();
22387
+ }
22388
+ ctx.setLineDash([]);
22389
+ ctx.lineWidth = 1.5;
22390
+ ctx.strokeStyle = color;
22391
+ ctx.beginPath();
22392
+ ctx.moveTo(pts[0][0], pts[0][1]);
22393
+ for (let i = 1; i < pts.length; i += 1) ctx.lineTo(pts[i][0], pts[i][1]);
22394
+ ctx.stroke();
22395
+ }
22396
+ /** Centered muted notice inside the magnifier rect (loading / unavailable states). */
22397
+ paintMagnifierNotice(ctx, text, x0, y0, w, h, theme) {
22398
+ if (w < 60 || h < 20) return;
22399
+ ctx.save();
22400
+ ctx.beginPath();
22401
+ ctx.rect(x0, y0, w, h);
22402
+ ctx.clip();
22403
+ ctx.font = `11px ${theme.fontFamily}`;
22404
+ ctx.fillStyle = withAlpha(theme.textColor, 0.55);
22405
+ ctx.textAlign = "center";
22406
+ ctx.textBaseline = "middle";
22407
+ ctx.fillText(text, x0 + w / 2, y0 + h / 2);
22408
+ ctx.restore();
22409
+ }
22410
+ /** A bottom-center pill prompting the armed tool's placement gesture (e.g. the magnifier's
22411
+ * "drag an area"). Painted by the drawings layer while the tool is armed and no placement
22412
+ * is in progress; chart-background fill so it reads as chrome over any content. */
22413
+ paintPlacementHint(ctx, text, theme, width, height) {
22414
+ ctx.save();
22415
+ ctx.font = `11px ${theme.fontFamily}`;
22416
+ const tw = ctx.measureText(text).width;
22417
+ const pillW = tw + 24;
22418
+ const pillH = 24;
22419
+ const x = (width - pillW) / 2;
22420
+ const y = height - pillH - 14;
22421
+ roundRect(ctx, x, y, pillW, pillH, pillH / 2);
22422
+ ctx.fillStyle = theme.background;
22423
+ ctx.fill();
22424
+ ctx.strokeStyle = withAlpha(theme.textColor, 0.28);
22425
+ ctx.lineWidth = 1;
22426
+ ctx.setLineDash([]);
22427
+ ctx.stroke();
22428
+ ctx.fillStyle = theme.textColor;
22429
+ ctx.textAlign = "center";
22430
+ ctx.textBaseline = "middle";
22431
+ ctx.fillText(text, width / 2, y + pillH / 2 + 0.5);
22432
+ ctx.restore();
22433
+ }
21880
22434
  /** Paint a fixed-range volume profile: horizontal histogram rows (up/down split) anchored to
21881
22435
  * the left or right of the time span, optional VAH / VAL / POC levels across the range, and
21882
22436
  * optional developing POC / VA polylines. Recomputes from the two anchors on every paint. */
@@ -23292,8 +23846,9 @@ function ensureStyles3() {
23292
23846
  if (!existing) document.head.appendChild(s);
23293
23847
  }
23294
23848
  var DrawingSettingsPopup = class {
23295
- constructor(host, theme) {
23849
+ constructor(host, theme, chartBarMs = () => 0) {
23296
23850
  this.host = host;
23851
+ this.chartBarMs = chartBarMs;
23297
23852
  this.el = null;
23298
23853
  this.tipEl = null;
23299
23854
  // floating hover-label (above/below the toolbar)
@@ -23318,6 +23873,14 @@ var DrawingSettingsPopup = class {
23318
23873
  this.theme = theme;
23319
23874
  this.settingsDialog = new DrawingSettingsDialog(host, theme);
23320
23875
  }
23876
+ /** The magnifier timeframe choices strictly below the chart's own bar duration
23877
+ * (`auto` rides along while at least one concrete lower step exists). */
23878
+ lowerTimeframeOptions() {
23879
+ const chartMs = this.chartBarMs();
23880
+ if (!(chartMs > 0)) return [...MAGNIFIER_TIMEFRAME_OPTIONS];
23881
+ const lower = MAGNIFIER_TIMEFRAME_OPTIONS.filter((o) => o.ms > 0 && o.ms < chartMs);
23882
+ return lower.length > 0 ? [MAGNIFIER_TIMEFRAME_OPTIONS[0], ...lower] : [];
23883
+ }
23321
23884
  setTheme(theme) {
23322
23885
  this.theme = theme;
23323
23886
  this.settingsDialog.setTheme(theme);
@@ -23360,7 +23923,20 @@ var DrawingSettingsPopup = class {
23360
23923
  const sz = drawing.size ?? "normal";
23361
23924
  bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label: sizeLabel }));
23362
23925
  }
23363
- 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 })));
23926
+ if (paths.has("magnifier.timeframe") && drawing instanceof Magnifier) {
23927
+ const options = this.lowerTimeframeOptions();
23928
+ if (options.length > 0) {
23929
+ bar.appendChild(
23930
+ this.dropdown("Lower timeframe", options.map((o) => o.value), drawing.magnifier.timeframe, () => "", (v) => actions.patch({ "magnifier.timeframe": v }), {
23931
+ label: (v) => magnifierTimeframeLabel(String(v)),
23932
+ labelInTrigger: true
23933
+ })
23934
+ );
23935
+ }
23936
+ bar.appendChild(this.colorButton("Up candles", BUCKET_ICON, drawing.magnifier.upColor || t.upColor, (v) => actions.patch({ "magnifier.upColor": v })));
23937
+ bar.appendChild(this.colorButton("Down candles", BUCKET_ICON, drawing.magnifier.downColor || t.downColor, (v) => actions.patch({ "magnifier.downColor": v })));
23938
+ }
23939
+ 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 })));
23364
23940
  if (paths.has("style.lineWidth")) {
23365
23941
  const wf = schema.fields.find((f) => f.path === "style.lineWidth");
23366
23942
  if (wf?.kind === "number" && (wf.min ?? 1) > 1) {
@@ -23682,6 +24258,7 @@ var DrawingSettingsPopup = class {
23682
24258
  this.colorPop = null;
23683
24259
  this.colorOwner = null;
23684
24260
  }
24261
+ opts.onClose?.();
23685
24262
  }
23686
24263
  });
23687
24264
  const el = pop.el;
@@ -23700,6 +24277,49 @@ var DrawingSettingsPopup = class {
23700
24277
  pop.show();
23701
24278
  return pop;
23702
24279
  }
24280
+ /**
24281
+ * A standalone timeframe menu for the magnifier's ON-CHART chip. The chip lives on
24282
+ * canvas, so a transient invisible anchor is dropped at its pixel rect for the popover
24283
+ * to position against, and removed again when the menu closes. Independent of the
24284
+ * quick toolbar — the chip works without selecting the drawing first.
24285
+ */
24286
+ openMagnifierTimeframeMenu(rect, current, onPick) {
24287
+ ensureStyles3();
24288
+ closeOpenPopovers();
24289
+ const options = this.lowerTimeframeOptions();
24290
+ const anchor = document.createElement("div");
24291
+ anchor.style.cssText = `position:absolute;left:${rect.x}px;top:${rect.y}px;width:${rect.w}px;height:${rect.h}px;pointer-events:none;`;
24292
+ this.host.appendChild(anchor);
24293
+ this.menuPop = this.hostFloat(anchor, {
24294
+ zIndex: 26,
24295
+ padding: "4px",
24296
+ onClose: () => anchor.remove(),
24297
+ fill: (menu2, pop) => {
24298
+ if (options.length === 0) {
24299
+ const note = document.createElement("div");
24300
+ note.style.cssText = "padding:6px 10px;opacity:0.65;white-space:nowrap;";
24301
+ note.textContent = "No lower timeframe available";
24302
+ menu2.appendChild(note);
24303
+ return;
24304
+ }
24305
+ for (const o of options) {
24306
+ const item = document.createElement("button");
24307
+ item.type = "button";
24308
+ item.className = "vela-dpop-item";
24309
+ item.dataset.active = o.value === current ? "1" : "0";
24310
+ 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;";
24311
+ item.textContent = o.label;
24312
+ item.addEventListener("click", (e) => {
24313
+ e.stopPropagation();
24314
+ pop.hide();
24315
+ onPick(o.value);
24316
+ });
24317
+ menu2.appendChild(item);
24318
+ }
24319
+ }
24320
+ });
24321
+ this.menuOwner = anchor;
24322
+ }
23703
24323
  /** A floating list of one-shot actions (icon + label rows) opened by the kebab. */
23704
24324
  openActionMenu(anchor, rows) {
23705
24325
  this.menuPop = this.hostFloat(anchor, {
@@ -23810,10 +24430,13 @@ var DrawingSettingsPopup = class {
23810
24430
  let cur = current;
23811
24431
  const paint = (v) => {
23812
24432
  b.replaceChildren();
23813
- const ic = document.createElement("span");
23814
- ic.style.cssText = "display:flex;";
23815
- ic.innerHTML = sized(render(v));
23816
- b.appendChild(ic);
24433
+ const glyph = render(v);
24434
+ if (glyph) {
24435
+ const ic = document.createElement("span");
24436
+ ic.style.cssText = "display:flex;";
24437
+ ic.innerHTML = sized(glyph);
24438
+ b.appendChild(ic);
24439
+ }
23817
24440
  if (opts.label && opts.labelInTrigger) {
23818
24441
  const tx = document.createElement("span");
23819
24442
  tx.textContent = opts.label(v);
@@ -23855,10 +24478,13 @@ var DrawingSettingsPopup = class {
23855
24478
  item.className = "vela-dpop-item";
23856
24479
  item.dataset.active = active ? "1" : "0";
23857
24480
  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;`;
23858
- const ic = document.createElement("span");
23859
- ic.style.cssText = "display:flex;flex:none;width:22px;justify-content:center;";
23860
- ic.innerHTML = sized(render(v), 18);
23861
- item.appendChild(ic);
24481
+ const glyph = render(v);
24482
+ if (glyph) {
24483
+ const ic = document.createElement("span");
24484
+ ic.style.cssText = "display:flex;flex:none;width:22px;justify-content:center;";
24485
+ ic.innerHTML = sized(glyph, 18);
24486
+ item.appendChild(ic);
24487
+ }
23862
24488
  if (label) {
23863
24489
  const tx = document.createElement("span");
23864
24490
  tx.textContent = label(v);
@@ -24842,6 +25468,9 @@ var UserDrawingController = class {
24842
25468
  this.intentCb = null;
24843
25469
  /** Another chart's in-progress placement, mirrored here as a ghost (drawings sync). */
24844
25470
  this.externalGhost = null;
25471
+ /** Core-pushed series gateway (finer-timeframe bars for data-driven drawings). */
25472
+ this.seriesGw = null;
25473
+ this.seriesGwUnsub = null;
24845
25474
  /** Last draft fingerprint reported upstream — gates the per-render emission to actual changes. */
24846
25475
  this.lastDraftKey = null;
24847
25476
  this.measure = new MeasureOverlay();
@@ -24869,7 +25498,7 @@ var UserDrawingController = class {
24869
25498
  this.textEditor = null;
24870
25499
  this.painter = new DrawingPainter();
24871
25500
  this.ctx = canvas.getContext("2d");
24872
- this.popup = new DrawingSettingsPopup(overlayHost, deps.theme());
25501
+ this.popup = new DrawingSettingsPopup(overlayHost, deps.theme(), () => deps.chartBarMs());
24873
25502
  this.toolbar = new DrawingToolbar(
24874
25503
  toolbarHost,
24875
25504
  deps.theme(),
@@ -24927,6 +25556,22 @@ var UserDrawingController = class {
24927
25556
  const shown = this.toolbarVisible && !this.mobileLayout;
24928
25557
  this.deps.setToolbarGutter(shown ? this.toolbarCollapsed ? TOOLBAR_COLLAPSED_WIDTH : TOOLBAR_WIDTH : 0);
24929
25558
  }
25559
+ /** Core push: the series gateway data-driven drawings read finer-timeframe bars
25560
+ * through (surfaced to them as `Projector.seriesInRange`). A landed background
25561
+ * fetch repaints both this layer and the interleave slices under the series. */
25562
+ setSeriesGateway(gateway) {
25563
+ this.seriesGwUnsub?.();
25564
+ this.seriesGw = gateway;
25565
+ this.seriesGwUnsub = gateway.onUpdate(() => {
25566
+ this.invalidateSlices();
25567
+ this.render();
25568
+ this.deps.requestDataPaint();
25569
+ });
25570
+ }
25571
+ /** The pushed series gateway, or null before the core provides one. */
25572
+ get seriesGateway() {
25573
+ return this.seriesGw;
25574
+ }
24930
25575
  /** Core push: mirror (or clear) another chart's in-progress placement as a ghost. */
24931
25576
  setExternalGhost(doc) {
24932
25577
  this.externalGhost = doc ? deserializeDrawing(doc) : null;
@@ -25044,8 +25689,34 @@ var UserDrawingController = class {
25044
25689
  /** Should the drawing layer win this press (vs pan)? */
25045
25690
  claim(x, y) {
25046
25691
  if (this.measureMode || this.eraserMode) return true;
25692
+ if (this.magnifierChipAt(x, y)) return true;
25047
25693
  return this.interaction.claim(x, y);
25048
25694
  }
25695
+ /** The topmost visible (unlocked) magnifier whose timeframe chip contains (x, y) —
25696
+ * the chip's rect is what the painter measured last frame. */
25697
+ magnifierChipAt(x, y) {
25698
+ for (let i = this.drawings.length - 1; i >= 0; i -= 1) {
25699
+ const d = this.drawings[i];
25700
+ if (!(d instanceof Magnifier) || !d.visible || d.locked) continue;
25701
+ const r = d.chipRect;
25702
+ if (r && x >= r.x && x <= r.x + r.w && y >= r.y && y <= r.y + r.h) return d;
25703
+ }
25704
+ return null;
25705
+ }
25706
+ /** Open the on-chart chip's timeframe menu; the pick patches the drawing like a
25707
+ * settings-popup edit (same intent, same undo step). */
25708
+ openMagnifierChipMenu(drawing) {
25709
+ const rect = drawing.chipRect;
25710
+ if (!rect) return;
25711
+ const id = drawing.id;
25712
+ this.popup.openMagnifierTimeframeMenu(rect, drawing.magnifier.timeframe, (value) => {
25713
+ const d = this.drawings.find((x) => x.id === id);
25714
+ if (!(d instanceof Magnifier)) return;
25715
+ d.applySettings({ "magnifier.timeframe": value });
25716
+ this.render();
25717
+ this.emit({ kind: "edit", doc: d.serialize() });
25718
+ });
25719
+ }
25049
25720
  /** Delete the (unlocked) drawing under the cursor. True when one was removed.
25050
25721
  * Shared by the eraser (click + drag) and the middle-click shortcut. */
25051
25722
  deleteAt(x, y) {
@@ -25091,6 +25762,13 @@ var UserDrawingController = class {
25091
25762
  this.render();
25092
25763
  return;
25093
25764
  }
25765
+ if (this.activeTool == null) {
25766
+ const chipOwner = this.magnifierChipAt(x, y);
25767
+ if (chipOwner) {
25768
+ this.openMagnifierChipMenu(chipOwner);
25769
+ return;
25770
+ }
25771
+ }
25094
25772
  this.interaction.down(x, y, snap, shift);
25095
25773
  }
25096
25774
  pointerMove(x, y, snap = "off", shift = false) {
@@ -25309,6 +25987,7 @@ var UserDrawingController = class {
25309
25987
  /** Cursor hint while hovering — `'pointer'` over a drawing/handle, else null. */
25310
25988
  cursorAt(x, y) {
25311
25989
  if (this.eraserMode) return "pointer";
25990
+ if (this.activeTool == null && this.magnifierChipAt(x, y)) return "pointer";
25312
25991
  return this.interaction.cursorAt(x, y);
25313
25992
  }
25314
25993
  /** Right-click: an explicit escape back to the pointer. Cancels an in-progress
@@ -25487,6 +26166,7 @@ var UserDrawingController = class {
25487
26166
  if (!sctx) continue;
25488
26167
  sctx.setTransform(dpr, 0, 0, dpr, 0, 0);
25489
26168
  sctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
26169
+ this.painter.seriesLook = this.deps.seriesLook();
25490
26170
  this.painter.paintAll(sctx, drawings, proj, theme, EMPTY_TARGETS);
25491
26171
  const slices = out.get(paneId) ?? [];
25492
26172
  slices.push({ beforeZ, canvas });
@@ -25514,6 +26194,7 @@ var UserDrawingController = class {
25514
26194
  dragged: this.interaction.activeDragId(),
25515
26195
  mutedLabel: edited instanceof TextLabel ? edited.id : null
25516
26196
  };
26197
+ this.painter.seriesLook = this.deps.seriesLook();
25517
26198
  this.painter.paintAll(ctx, this.drawings.filter((d) => !this.isInterleaved(d)), proj, this.deps.theme(), targets);
25518
26199
  this.painter.paintHighlights(ctx, this.drawings.filter((d) => this.isInterleaved(d)), proj, handleIdsFor(targets));
25519
26200
  this.layoutTextEditor();
@@ -25521,6 +26202,10 @@ var UserDrawingController = class {
25521
26202
  if (ghost) this.painter.paintGhost(ctx, ghost, proj, this.deps.theme());
25522
26203
  if (this.externalGhost) this.painter.paintGhost(ctx, this.externalGhost, proj, this.deps.theme());
25523
26204
  this.emitDraft(ghost);
26205
+ if (this.activeTool && !ghost) {
26206
+ const hint = getDrawingType(this.activeTool)?.placementHint;
26207
+ if (hint) this.painter.paintPlacementHint(ctx, hint, this.deps.theme(), proj.width, proj.height);
26208
+ }
25524
26209
  const markers = this.interaction.placingMarkers(proj);
25525
26210
  if (markers) this.painter.paintHandles(ctx, markers);
25526
26211
  const m = this.interaction.snapMarker();
@@ -25532,6 +26217,9 @@ var UserDrawingController = class {
25532
26217
  this.closeTextEditor();
25533
26218
  this.popup.destroy();
25534
26219
  this.toolbar.destroy();
26220
+ this.seriesGwUnsub?.();
26221
+ this.seriesGwUnsub = null;
26222
+ this.seriesGw = null;
25535
26223
  this.intentCb = null;
25536
26224
  this.drawings = [];
25537
26225
  }
@@ -25929,7 +26617,7 @@ function mergeSlices(indicator, user) {
25929
26617
  }
25930
26618
 
25931
26619
  // src/renderers/native/drawings/Projector.ts
25932
- function createProjector(coords, paneOf, paneIdAtY, barsInRange) {
26620
+ function createProjector(coords, paneOf, paneIdAtY, barsInRange, seriesInRange) {
25933
26621
  return {
25934
26622
  xOf: (time) => coords.timeToX(time),
25935
26623
  yOf: (price, paneId) => {
@@ -25950,6 +26638,7 @@ function createProjector(coords, paneOf, paneIdAtY, barsInRange) {
25950
26638
  },
25951
26639
  barsBetween: (t1, t2) => Math.abs(coords.timeToLogical(t2) - coords.timeToLogical(t1)),
25952
26640
  barsInRange: barsInRange ? (from, to) => barsInRange(from, to) : void 0,
26641
+ seriesInRange,
25953
26642
  width: coords.width,
25954
26643
  height: coords.height
25955
26644
  };
@@ -27953,6 +28642,23 @@ var NativeRenderer = class {
27953
28642
  seriesBoundaries: (paneId) => this.scene.seriesBoundaries(paneId),
27954
28643
  priceZ: (paneId) => paneId === PRICE_PANE_ID ? this.scene.candleZ : null,
27955
28644
  requestDataPaint: () => this.scheduler.invalidate(3 /* Light */),
28645
+ // The look the price series ACTUALLY paints with: candle colors resolved through
28646
+ // the per-style override, line/area colors through their configured styles — so
28647
+ // series-mirroring content (the magnifier inset) matches the chart exactly.
28648
+ seriesLook: () => {
28649
+ const st = this.scene.style;
28650
+ const paint = effectiveCandlePaint(st.candle, this.scene.candleOverride, this.theme.upColor, this.theme.downColor);
28651
+ const barsUp = st.bars.upColor ?? this.theme.upColor;
28652
+ const barsDown = st.bars.downColor ?? this.theme.downColor;
28653
+ const style = this.scene.priceStyle;
28654
+ return {
28655
+ style,
28656
+ upColor: style === "bars" ? barsUp : paint.up,
28657
+ downColor: style === "bars" ? barsDown : paint.down,
28658
+ lineColor: style === "area" ? st.area.lineColor ?? this.theme.upColor : st.line.color ?? this.theme.upColor
28659
+ };
28660
+ },
28661
+ chartBarMs: () => this.coords.barInterval,
27956
28662
  snap: (pt, paneId, mode, cursorPx) => this.snapToCandle(pt, paneId, mode, cursorPx),
27957
28663
  setSnapMode: (mode) => this.setSnapMode(mode),
27958
28664
  setToolbarGutter: (px) => this.setToolbarGutter(px)
@@ -29346,7 +30052,8 @@ var NativeRenderer = class {
29346
30052
  return p ? { scale: p.scale, bounds: p.bounds, collapsed: p.collapsed } : null;
29347
30053
  },
29348
30054
  (y) => this.paneNodeAtY(y)?.id ?? null,
29349
- (from, to) => this.barsInTimeRange(from, to)
30055
+ (from, to) => this.barsInTimeRange(from, to),
30056
+ this.userDrawings?.seriesGateway ? (tf, from, to) => this.userDrawings.seriesGateway.seriesInRange(tf, from, to) : void 0
29350
30057
  );
29351
30058
  }
29352
30059
  /** OHLC bars whose open-time falls within `[from, to]` (inclusive) — the data a regression