@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.
- package/README.md +110 -46
- package/dist/{DataProvider-BBf-jc6W.d.ts → DataProvider-DhzstpQb.d.ts} +1 -1
- package/dist/{DataProvider-p0TEyhlX.d.cts → DataProvider-DlMtrwqM.d.cts} +1 -1
- package/dist/{chunk-MTLJKZDZ.js → chunk-F4M24ANM.js} +122 -1
- package/dist/{chunk-IO3NYSQV.js → chunk-G52XAKZY.js} +184 -25
- package/dist/{chunk-73PEA4MU.js → chunk-JKGA36ZM.js} +635 -49
- package/dist/{contributions-Bbe2R-mQ.d.ts → contributions-37nni40G.d.ts} +6 -4
- package/dist/{contributions-CO01zWve.d.cts → contributions-lPojhTxI.d.cts} +6 -4
- package/dist/index.cjs +744 -37
- package/dist/index.d.cts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +2 -2
- package/dist/{options-yp7sA96q.d.cts → options-CYS5Wmlx.d.cts} +71 -2
- package/dist/{options-yp7sA96q.d.ts → options-CYS5Wmlx.d.ts} +71 -2
- package/dist/{plugin-CkkH8QnX.d.cts → plugin-Cwikpz1m.d.cts} +10 -3
- package/dist/{plugin-DwxjM3Ni.d.ts → plugin-DHyaoMjW.d.ts} +10 -3
- package/dist/plugin.cjs +110 -0
- package/dist/plugin.d.cts +4 -4
- package/dist/plugin.d.ts +4 -4
- package/dist/plugin.js +1 -1
- package/dist/providers/binance.d.cts +2 -2
- package/dist/providers/binance.d.ts +2 -2
- package/dist/providers/coinbase.d.cts +2 -2
- package/dist/providers/coinbase.d.ts +2 -2
- package/dist/providers/hyperliquid.d.cts +2 -2
- package/dist/providers/hyperliquid.d.ts +2 -2
- package/dist/{statusline-CHPDuKNp.d.ts → statusline-model-CiJm-riV.d.cts} +7 -85
- package/dist/{statusline-DTHZUFqK.d.cts → statusline-model-D-q_OOx9.d.ts} +7 -85
- package/dist/ui.d.cts +1 -1
- package/dist/ui.d.ts +1 -1
- package/dist/vela.global.js +744 -37
- package/dist/vela.global.min.js +52 -52
- package/dist/widget.cjs +927 -61
- package/dist/widget.d.cts +118 -7
- package/dist/widget.d.ts +118 -7
- package/dist/widget.js +4 -4
- package/dist/workspace.cjs +927 -61
- package/dist/workspace.d.cts +5 -5
- package/dist/workspace.d.ts +5 -5
- package/dist/workspace.js +3 -3
- package/package.json +1 -1
package/dist/vela.global.js
CHANGED
|
@@ -5400,6 +5400,111 @@ var Vela = (function (exports) {
|
|
|
5400
5400
|
return Math.round(n * 1e8) / 1e8;
|
|
5401
5401
|
}
|
|
5402
5402
|
|
|
5403
|
+
// src/core/drawings/types/Magnifier.ts
|
|
5404
|
+
var MAGNIFIER_TIMEFRAME_OPTIONS = [
|
|
5405
|
+
{ value: "auto", label: "Auto", ms: 0 },
|
|
5406
|
+
{ value: "1", label: "1m", ms: 6e4 },
|
|
5407
|
+
{ value: "5", label: "5m", ms: 3e5 },
|
|
5408
|
+
{ value: "15", label: "15m", ms: 9e5 },
|
|
5409
|
+
{ value: "30", label: "30m", ms: 18e5 },
|
|
5410
|
+
{ value: "60", label: "1h", ms: 36e5 },
|
|
5411
|
+
{ value: "240", label: "4h", ms: 144e5 },
|
|
5412
|
+
{ value: "D", label: "1D", ms: 864e5 }
|
|
5413
|
+
];
|
|
5414
|
+
function magnifierTimeframeLabel(value) {
|
|
5415
|
+
const opt = MAGNIFIER_TIMEFRAME_OPTIONS.find((o) => o.value === value);
|
|
5416
|
+
if (opt) return opt.label;
|
|
5417
|
+
const n = Number(value);
|
|
5418
|
+
if (Number.isFinite(n) && n > 0) {
|
|
5419
|
+
if (n % 1440 === 0) return `${n / 1440}D`;
|
|
5420
|
+
if (n % 60 === 0) return `${n / 60}h`;
|
|
5421
|
+
return `${n}m`;
|
|
5422
|
+
}
|
|
5423
|
+
return value;
|
|
5424
|
+
}
|
|
5425
|
+
function defaultMagnifierStyle() {
|
|
5426
|
+
return { timeframe: "auto", upColor: "", downColor: "" };
|
|
5427
|
+
}
|
|
5428
|
+
var Magnifier = class extends Drawing {
|
|
5429
|
+
constructor(init) {
|
|
5430
|
+
super(init);
|
|
5431
|
+
this.type = "magnifier";
|
|
5432
|
+
/** Pixel rect of the timeframe chip as painted last frame, caret included — the chip is
|
|
5433
|
+
* an interactive dropdown trigger, so the interaction layer needs the exact rect the
|
|
5434
|
+
* painter measured. Renderer-transient: never serialized, null while unpainted. */
|
|
5435
|
+
this.chipRect = null;
|
|
5436
|
+
if (!this.magnifier) this.magnifier = defaultMagnifierStyle();
|
|
5437
|
+
}
|
|
5438
|
+
anchorSchema() {
|
|
5439
|
+
return { min: 2, max: 2, slots: [{ role: "c1", free: "both" }, { role: "c2", free: "both" }] };
|
|
5440
|
+
}
|
|
5441
|
+
placementMode() {
|
|
5442
|
+
return "drag";
|
|
5443
|
+
}
|
|
5444
|
+
/** The pixel rectangle between the two corner anchors (painter + hit-test share it). */
|
|
5445
|
+
rect(proj) {
|
|
5446
|
+
const a = this.anchors[0];
|
|
5447
|
+
const b = this.anchors[1];
|
|
5448
|
+
if (!a || !b) return null;
|
|
5449
|
+
const ya = proj.yOf(a.price, this.paneId);
|
|
5450
|
+
const yb = proj.yOf(b.price, this.paneId);
|
|
5451
|
+
if (ya == null || yb == null) return null;
|
|
5452
|
+
return { x1: proj.xOf(a.time), y1: ya, x2: proj.xOf(b.time), y2: yb };
|
|
5453
|
+
}
|
|
5454
|
+
hitTest(px, py, proj, tol) {
|
|
5455
|
+
const r = this.rect(proj);
|
|
5456
|
+
if (!r) return false;
|
|
5457
|
+
if (pointInBox(px, py, r.x1, r.y1, r.x2, r.y2)) return true;
|
|
5458
|
+
const edges = [
|
|
5459
|
+
[r.x1, r.y1, r.x2, r.y1],
|
|
5460
|
+
[r.x2, r.y1, r.x2, r.y2],
|
|
5461
|
+
[r.x2, r.y2, r.x1, r.y2],
|
|
5462
|
+
[r.x1, r.y2, r.x1, r.y1]
|
|
5463
|
+
];
|
|
5464
|
+
return edges.some((e) => distToSegment(px, py, e[0], e[1], e[2], e[3]) <= tol);
|
|
5465
|
+
}
|
|
5466
|
+
handlePoints(proj) {
|
|
5467
|
+
const r = this.rect(proj);
|
|
5468
|
+
return r ? [[r.x1, r.y1], [r.x2, r.y2]] : [];
|
|
5469
|
+
}
|
|
5470
|
+
hitHandle(px, py, proj, tol) {
|
|
5471
|
+
return handleAt(px, py, this.handlePoints(proj), tol + 3);
|
|
5472
|
+
}
|
|
5473
|
+
bounds(proj) {
|
|
5474
|
+
const r = this.rect(proj);
|
|
5475
|
+
if (!r) return null;
|
|
5476
|
+
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) };
|
|
5477
|
+
}
|
|
5478
|
+
priceRange() {
|
|
5479
|
+
const a = this.anchors[0];
|
|
5480
|
+
const b = this.anchors[1];
|
|
5481
|
+
if (!a || !b) return null;
|
|
5482
|
+
return { min: Math.min(a.price, b.price), max: Math.max(a.price, b.price) };
|
|
5483
|
+
}
|
|
5484
|
+
schema() {
|
|
5485
|
+
return {
|
|
5486
|
+
fields: [
|
|
5487
|
+
{
|
|
5488
|
+
path: "magnifier.timeframe",
|
|
5489
|
+
label: "Timeframe",
|
|
5490
|
+
kind: "select",
|
|
5491
|
+
options: MAGNIFIER_TIMEFRAME_OPTIONS,
|
|
5492
|
+
group: "behavior"
|
|
5493
|
+
},
|
|
5494
|
+
...LINE_FIELDS.map((f) => ({ ...f, label: f.label.replace("Line", "Border") })),
|
|
5495
|
+
{ path: "magnifier.upColor", label: "Up candles", kind: "color", group: "fill" },
|
|
5496
|
+
{ path: "magnifier.downColor", label: "Down candles", kind: "color", group: "fill" }
|
|
5497
|
+
]
|
|
5498
|
+
};
|
|
5499
|
+
}
|
|
5500
|
+
writeProps() {
|
|
5501
|
+
return { ...this.magnifier };
|
|
5502
|
+
}
|
|
5503
|
+
readProps(props) {
|
|
5504
|
+
this.magnifier = { ...defaultMagnifierStyle(), ...props };
|
|
5505
|
+
}
|
|
5506
|
+
};
|
|
5507
|
+
|
|
5403
5508
|
// src/core/drawings/registry.ts
|
|
5404
5509
|
var REGISTRY2 = /* @__PURE__ */ new Map();
|
|
5405
5510
|
function registerDrawingType(meta) {
|
|
@@ -6059,6 +6164,22 @@ var Vela = (function (exports) {
|
|
|
6059
6164
|
defaultStyle: { lineColor: DEFAULT_DRAWING_COLOR, lineWidth: 1, lineStyle: "solid" },
|
|
6060
6165
|
create: (init) => new PositionTool(init)
|
|
6061
6166
|
});
|
|
6167
|
+
var MAGNIFIER_ICON = svg24(
|
|
6168
|
+
'<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"/>'
|
|
6169
|
+
);
|
|
6170
|
+
registerDrawingType({
|
|
6171
|
+
type: "magnifier",
|
|
6172
|
+
group: "measure",
|
|
6173
|
+
label: "Magnifier",
|
|
6174
|
+
icon: MAGNIFIER_ICON,
|
|
6175
|
+
// An empty border color means the THEME's contrast ink (white on dark, black on
|
|
6176
|
+
// light), resolved at paint time so it follows theme switches; a user pick wins.
|
|
6177
|
+
defaultStyle: { lineColor: "", lineWidth: 1, lineStyle: "solid" },
|
|
6178
|
+
coversSeries: true,
|
|
6179
|
+
// the inset's backdrop must sit over the base candles it replaces
|
|
6180
|
+
placementHint: "Drag an area on the chart to view it at a lower timeframe",
|
|
6181
|
+
create: (init) => new Magnifier(init)
|
|
6182
|
+
});
|
|
6062
6183
|
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"/>');
|
|
6063
6184
|
registerDrawingType({
|
|
6064
6185
|
type: "anchoredvwap",
|
|
@@ -6098,7 +6219,7 @@ var Vela = (function (exports) {
|
|
|
6098
6219
|
var PATTERN_TYPES = ["xabcd", "abcd", "headshoulders"];
|
|
6099
6220
|
var ELLIOTT_TYPES = ["elliottimpulse", "elliottcorrection"];
|
|
6100
6221
|
var HARMONIC_TYPES = ["gartley", "bat", "butterfly", "crab", "shark", "cypher"];
|
|
6101
|
-
var MEASUREMENT_TYPES = ["position", "datepricerange"];
|
|
6222
|
+
var MEASUREMENT_TYPES = ["position", "datepricerange", "magnifier"];
|
|
6102
6223
|
var VOLUME_TYPES = ["anchoredvwap", "fixedrangevp"];
|
|
6103
6224
|
var BRUSH_TYPES = ["freehand", "highlighter"];
|
|
6104
6225
|
var ARROW_TYPES = ["arrow", "arrowmarkup", "arrowmarkdown"];
|
|
@@ -6462,7 +6583,7 @@ var Vela = (function (exports) {
|
|
|
6462
6583
|
|
|
6463
6584
|
// src/core/drawings/DrawingController.ts
|
|
6464
6585
|
var DrawingController = class {
|
|
6465
|
-
constructor(renderer, events, option) {
|
|
6586
|
+
constructor(renderer, events, option, seriesGateway) {
|
|
6466
6587
|
this.events = events;
|
|
6467
6588
|
this.store = new DrawingStore();
|
|
6468
6589
|
this.history = new DrawingHistory();
|
|
@@ -6488,6 +6609,7 @@ var Vela = (function (exports) {
|
|
|
6488
6609
|
const { definition, visible } = buildToolbar(option);
|
|
6489
6610
|
this.port.setToolbar(definition);
|
|
6490
6611
|
this.port.showToolbar(visible);
|
|
6612
|
+
if (seriesGateway) this.port.setSeriesGateway?.(seriesGateway);
|
|
6491
6613
|
this.subs.push(this.port.onDrawingIntent((i) => this.onIntent(i)));
|
|
6492
6614
|
this.subs.push(this.store.onChange(() => this.sync()));
|
|
6493
6615
|
}
|
|
@@ -6593,7 +6715,7 @@ var Vela = (function (exports) {
|
|
|
6593
6715
|
style,
|
|
6594
6716
|
text: init.text,
|
|
6595
6717
|
props: init.props,
|
|
6596
|
-
zIndex: init.zIndex ?? this.startZ(init.paneId ?? "price")
|
|
6718
|
+
zIndex: init.zIndex ?? this.startZ(type, init.paneId ?? "price")
|
|
6597
6719
|
});
|
|
6598
6720
|
if (!d) return null;
|
|
6599
6721
|
this.history.record(this.store.serialize());
|
|
@@ -6668,10 +6790,14 @@ var Vela = (function (exports) {
|
|
|
6668
6790
|
* (falling back to just under the pane's top series where there is no price — a study
|
|
6669
6791
|
* pane). Half a key down never ties a series; drawings tying each other paint in insertion
|
|
6670
6792
|
* order, so consecutive new drawings still stack newest-in-front. Undefined without a
|
|
6671
|
-
* shared z space — the store then places it over the other drawings, its own layer's top.
|
|
6672
|
-
|
|
6793
|
+
* shared z space — the store then places it over the other drawings, its own layer's top.
|
|
6794
|
+
* A type that COVERS the series (an opaque inset, `coversSeries`) instead starts just
|
|
6795
|
+
* above the whole stack — under the candles its content would be buried. */
|
|
6796
|
+
startZ(type, paneId) {
|
|
6673
6797
|
const range = this.port?.stackRange?.(paneId);
|
|
6674
|
-
|
|
6798
|
+
if (!range) return void 0;
|
|
6799
|
+
if (getDrawingType(type)?.coversSeries) return range.front + 0.5;
|
|
6800
|
+
return (range.price ?? range.front) - 0.5;
|
|
6675
6801
|
}
|
|
6676
6802
|
/** Programmatically select drawings (host UI → chart): shows the on-chart handles + toolbar.
|
|
6677
6803
|
* `additive` toggles membership (matching shift-click) instead of replacing. */
|
|
@@ -6816,7 +6942,7 @@ var Vela = (function (exports) {
|
|
|
6816
6942
|
const style = last2 ? { ...i.doc.style, ...last2 } : i.doc.style;
|
|
6817
6943
|
const d = deserializeDrawing({ ...i.doc, id: this.store.nextId(), style });
|
|
6818
6944
|
if (!d) return;
|
|
6819
|
-
if (!d.zIndex) d.zIndex = this.startZ(d.paneId) ?? 0;
|
|
6945
|
+
if (!d.zIndex) d.zIndex = this.startZ(d.type, d.paneId) ?? 0;
|
|
6820
6946
|
this.history.record(before);
|
|
6821
6947
|
this.store.add(d);
|
|
6822
6948
|
this.captureStyle(d.id);
|
|
@@ -6914,6 +7040,180 @@ var Vela = (function (exports) {
|
|
|
6914
7040
|
return 36e5;
|
|
6915
7041
|
}
|
|
6916
7042
|
|
|
7043
|
+
// src/core/engine/DrawingSeriesService.ts
|
|
7044
|
+
var MAX_BARS = 5e3;
|
|
7045
|
+
var PAD_FRAC = 0.25;
|
|
7046
|
+
var MAX_ENTRIES = 16;
|
|
7047
|
+
var RETRY_MS = 15e3;
|
|
7048
|
+
var AUTO_STEPS = ["240", "60", "30", "15", "5", "1"];
|
|
7049
|
+
var DrawingSeriesService = class {
|
|
7050
|
+
constructor(deps) {
|
|
7051
|
+
this.deps = deps;
|
|
7052
|
+
/** Cached windows per `market|timeframe` key, newest-used last (LRU across keys). */
|
|
7053
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
7054
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
7055
|
+
}
|
|
7056
|
+
seriesInRange(timeframe, from, to) {
|
|
7057
|
+
if (!this.deps.canFetch()) return { state: "unavailable", reason: "no-source" };
|
|
7058
|
+
const resolved = this.resolveTimeframe(timeframe);
|
|
7059
|
+
if (typeof resolved !== "string") return { state: "unavailable", reason: resolved.reason };
|
|
7060
|
+
const barMs = timeframeToMs(resolved);
|
|
7061
|
+
const lo = Math.min(from, to);
|
|
7062
|
+
const hi = Math.max(from, to);
|
|
7063
|
+
if (!(hi > lo) || !(barMs > 0)) return { state: "unavailable", reason: "not-lower" };
|
|
7064
|
+
if ((hi - lo) / barMs > MAX_BARS) return { state: "unavailable", reason: "too-wide" };
|
|
7065
|
+
const key = `${this.deps.marketKey()}|${resolved}`;
|
|
7066
|
+
const entries = this.cache.get(key) ?? [];
|
|
7067
|
+
const covering = entries.find((e) => e.from <= lo && e.to >= hi);
|
|
7068
|
+
if (covering) {
|
|
7069
|
+
if (covering.pending) return this.loading(entries, resolved, barMs, lo, hi);
|
|
7070
|
+
if (covering.failedAt > 0) {
|
|
7071
|
+
if (Date.now() - covering.failedAt < RETRY_MS) return this.loading(entries, resolved, barMs, lo, hi);
|
|
7072
|
+
entries.splice(entries.indexOf(covering), 1);
|
|
7073
|
+
} else {
|
|
7074
|
+
this.maybeRefresh(key, covering, resolved, barMs, hi);
|
|
7075
|
+
return { state: "ready", bars: this.slice(covering.bars, lo, hi), timeframe: resolved, barMs };
|
|
7076
|
+
}
|
|
7077
|
+
}
|
|
7078
|
+
this.fetchWindow(key, entries, resolved, lo, hi);
|
|
7079
|
+
return this.loading(entries, resolved, barMs, lo, hi);
|
|
7080
|
+
}
|
|
7081
|
+
onUpdate(listener) {
|
|
7082
|
+
this.listeners.add(listener);
|
|
7083
|
+
return () => this.listeners.delete(listener);
|
|
7084
|
+
}
|
|
7085
|
+
// ── internals ──
|
|
7086
|
+
/** `'auto'` → the largest standard step at least 4× finer than the chart (else the finest
|
|
7087
|
+
* step still below it); an explicit timeframe passes only when strictly finer. Failures
|
|
7088
|
+
* distinguish "this pick isn't lower" from "NOTHING lower exists" (the chart is already
|
|
7089
|
+
* at the finest offered step) so the consumer can word its notice honestly. */
|
|
7090
|
+
resolveTimeframe(timeframe) {
|
|
7091
|
+
const chartMs = timeframeToMs(this.deps.chartTimeframe());
|
|
7092
|
+
const finest = AUTO_STEPS[AUTO_STEPS.length - 1];
|
|
7093
|
+
if (timeframeToMs(finest) >= chartMs) return { reason: "none-lower" };
|
|
7094
|
+
const tf = timeframe.trim() || "auto";
|
|
7095
|
+
if (tf === "auto") {
|
|
7096
|
+
for (const step of AUTO_STEPS) {
|
|
7097
|
+
if (timeframeToMs(step) <= chartMs / 4) return step;
|
|
7098
|
+
}
|
|
7099
|
+
return finest;
|
|
7100
|
+
}
|
|
7101
|
+
return timeframeToMs(tf) < chartMs ? tf : { reason: "not-lower" };
|
|
7102
|
+
}
|
|
7103
|
+
/** The `loading` answer, carrying best-effort PARTIAL bars from settled overlapping
|
|
7104
|
+
* windows — a widened window keeps painting what it already has while it fetches. */
|
|
7105
|
+
loading(entries, timeframe, barMs, lo, hi) {
|
|
7106
|
+
const partial = /* @__PURE__ */ new Map();
|
|
7107
|
+
for (const e of entries) {
|
|
7108
|
+
if (e.pending || e.failedAt > 0) continue;
|
|
7109
|
+
if (e.to < lo || e.from > hi) continue;
|
|
7110
|
+
for (const b of this.slice(e.bars, lo, hi)) partial.set(b.time, b);
|
|
7111
|
+
}
|
|
7112
|
+
if (partial.size === 0) return { state: "loading", timeframe, barMs };
|
|
7113
|
+
const bars = [...partial.values()].sort((a, b) => a.time - b.time);
|
|
7114
|
+
return { state: "loading", timeframe, barMs, bars };
|
|
7115
|
+
}
|
|
7116
|
+
/** Kick ONE background fetch for the padded window. Any OVERLAPPING in-flight fetch
|
|
7117
|
+
* defers this one (a corner drag repaints per pointer move — kicking a window per
|
|
7118
|
+
* frame would spam the provider); when it lands, the next paint re-evaluates. */
|
|
7119
|
+
fetchWindow(key, entries, timeframe, lo, hi) {
|
|
7120
|
+
if (entries.some((e) => e.pending && e.to >= lo && e.from <= hi)) return;
|
|
7121
|
+
const pad = (hi - lo) * PAD_FRAC;
|
|
7122
|
+
const entry = { from: lo - pad, to: hi + pad, bars: [], fetchedAt: 0, pending: true, failedAt: 0 };
|
|
7123
|
+
entries.push(entry);
|
|
7124
|
+
this.cache.set(key, entries);
|
|
7125
|
+
this.evict();
|
|
7126
|
+
void this.deps.fetchBars(timeframe, { from: entry.from, to: entry.to }).then((bars) => {
|
|
7127
|
+
entry.bars = bars;
|
|
7128
|
+
entry.fetchedAt = Date.now();
|
|
7129
|
+
entry.pending = false;
|
|
7130
|
+
this.absorbOverlaps(key, entry);
|
|
7131
|
+
this.fire();
|
|
7132
|
+
}).catch(() => {
|
|
7133
|
+
entry.pending = false;
|
|
7134
|
+
entry.failedAt = Date.now();
|
|
7135
|
+
this.fire();
|
|
7136
|
+
});
|
|
7137
|
+
}
|
|
7138
|
+
/** A window whose right edge reaches the newest fetched bar refreshes at most once per
|
|
7139
|
+
* bar interval — new closed bars ride the feed's cache, only the live tail re-fetches. */
|
|
7140
|
+
maybeRefresh(key, entry, timeframe, barMs, hi) {
|
|
7141
|
+
if (entry.pending) return;
|
|
7142
|
+
const lastBar = entry.bars.length > 0 ? entry.bars[entry.bars.length - 1].time : entry.from;
|
|
7143
|
+
if (hi < lastBar) return;
|
|
7144
|
+
if (Date.now() - entry.fetchedAt < barMs) return;
|
|
7145
|
+
entry.pending = true;
|
|
7146
|
+
void this.deps.fetchBars(timeframe, { from: entry.from, to: entry.to }).then((bars) => {
|
|
7147
|
+
entry.bars = bars;
|
|
7148
|
+
entry.fetchedAt = Date.now();
|
|
7149
|
+
entry.pending = false;
|
|
7150
|
+
this.fire();
|
|
7151
|
+
}).catch(() => {
|
|
7152
|
+
entry.pending = false;
|
|
7153
|
+
entry.fetchedAt = Date.now();
|
|
7154
|
+
});
|
|
7155
|
+
}
|
|
7156
|
+
/** Merge windows that overlap `entry` into it (dedupe by bar time) so a key's list
|
|
7157
|
+
* converges instead of accumulating slivers. */
|
|
7158
|
+
absorbOverlaps(key, entry) {
|
|
7159
|
+
const entries = this.cache.get(key);
|
|
7160
|
+
if (!entries) return;
|
|
7161
|
+
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
7162
|
+
const other = entries[i];
|
|
7163
|
+
if (other === entry || other.pending || other.failedAt > 0) continue;
|
|
7164
|
+
if (other.to < entry.from || other.from > entry.to) continue;
|
|
7165
|
+
const byTime = /* @__PURE__ */ new Map();
|
|
7166
|
+
for (const b of other.bars) byTime.set(b.time, b);
|
|
7167
|
+
for (const b of entry.bars) byTime.set(b.time, b);
|
|
7168
|
+
entry.bars = [...byTime.values()].sort((a, b) => a.time - b.time);
|
|
7169
|
+
entry.from = Math.min(entry.from, other.from);
|
|
7170
|
+
entry.to = Math.max(entry.to, other.to);
|
|
7171
|
+
entry.fetchedAt = Math.min(entry.fetchedAt, other.fetchedAt || entry.fetchedAt);
|
|
7172
|
+
entries.splice(i, 1);
|
|
7173
|
+
}
|
|
7174
|
+
}
|
|
7175
|
+
/** Drop the oldest settled windows once the global count passes {@link MAX_ENTRIES}. */
|
|
7176
|
+
evict() {
|
|
7177
|
+
let total = 0;
|
|
7178
|
+
for (const entries of this.cache.values()) total += entries.length;
|
|
7179
|
+
while (total > MAX_ENTRIES) {
|
|
7180
|
+
let oldestKey = null;
|
|
7181
|
+
let oldestIdx = -1;
|
|
7182
|
+
let oldestAt = Infinity;
|
|
7183
|
+
for (const [key, entries2] of this.cache) {
|
|
7184
|
+
for (let i = 0; i < entries2.length; i += 1) {
|
|
7185
|
+
const e = entries2[i];
|
|
7186
|
+
if (e.pending) continue;
|
|
7187
|
+
const at = e.fetchedAt || e.failedAt;
|
|
7188
|
+
if (at < oldestAt) {
|
|
7189
|
+
oldestKey = key;
|
|
7190
|
+
oldestIdx = i;
|
|
7191
|
+
oldestAt = at;
|
|
7192
|
+
}
|
|
7193
|
+
}
|
|
7194
|
+
}
|
|
7195
|
+
if (oldestKey == null) return;
|
|
7196
|
+
const entries = this.cache.get(oldestKey);
|
|
7197
|
+
entries.splice(oldestIdx, 1);
|
|
7198
|
+
if (entries.length === 0) this.cache.delete(oldestKey);
|
|
7199
|
+
total -= 1;
|
|
7200
|
+
}
|
|
7201
|
+
}
|
|
7202
|
+
/** Bars whose open time falls within `[lo, hi]` (ascending input → linear scan is fine). */
|
|
7203
|
+
slice(bars, lo, hi) {
|
|
7204
|
+
const out = [];
|
|
7205
|
+
for (const b of bars) {
|
|
7206
|
+
if (b.time < lo) continue;
|
|
7207
|
+
if (b.time > hi) break;
|
|
7208
|
+
out.push(b);
|
|
7209
|
+
}
|
|
7210
|
+
return out;
|
|
7211
|
+
}
|
|
7212
|
+
fire() {
|
|
7213
|
+
for (const l of [...this.listeners]) l();
|
|
7214
|
+
}
|
|
7215
|
+
};
|
|
7216
|
+
|
|
6917
7217
|
// src/data/symbol-groups.ts
|
|
6918
7218
|
function isGroupRow(d) {
|
|
6919
7219
|
return d.group != null && d.ticker === d.group;
|
|
@@ -7278,7 +7578,6 @@ var Vela = (function (exports) {
|
|
|
7278
7578
|
var PREVIEW_BARS = 300;
|
|
7279
7579
|
var SINGLE_LOAD_BARS = 5e3;
|
|
7280
7580
|
var CHUNK_BARS = 1e4;
|
|
7281
|
-
var FIRST_PAINT_BARS = 100;
|
|
7282
7581
|
var GAP_FACTOR = 1.5;
|
|
7283
7582
|
var HEAL_COOLDOWN_MS = 5e3;
|
|
7284
7583
|
var EngineOrchestrator = class _EngineOrchestrator {
|
|
@@ -7396,7 +7695,13 @@ var Vela = (function (exports) {
|
|
|
7396
7695
|
const initialStyle = this.renderer.readFeature("priceStyle");
|
|
7397
7696
|
if (typeof initialStyle === "string") this.priceStyle = initialStyle;
|
|
7398
7697
|
this.barTransform = barTransformFor(initialStyle);
|
|
7399
|
-
|
|
7698
|
+
const drawingSeries = new DrawingSeriesService({
|
|
7699
|
+
fetchBars: (tf, range) => this.fetchSeries(this.config.market.symbol ?? "", tf, range),
|
|
7700
|
+
canFetch: () => !!this.feed.loadRange && !this.config.market.data?.length && !!this.config.market.symbol,
|
|
7701
|
+
chartTimeframe: () => this.config.market.timeframe ?? "60",
|
|
7702
|
+
marketKey: () => `${this.config.market.symbol ?? ""}|${this.config.market.session ?? ""}`
|
|
7703
|
+
});
|
|
7704
|
+
this.drawings = new DrawingController(this.renderer, this.events, config.drawings, drawingSeries);
|
|
7400
7705
|
this.unresolvedUnsub = this.feed.onUnresolved?.((info) => {
|
|
7401
7706
|
this.endLoad();
|
|
7402
7707
|
this.events.emit("data:unresolved", info);
|
|
@@ -7548,7 +7853,6 @@ var Vela = (function (exports) {
|
|
|
7548
7853
|
let painted = false;
|
|
7549
7854
|
const paint = (bars, final) => {
|
|
7550
7855
|
if (this.generation !== gen || !final && bars.length === 0) return;
|
|
7551
|
-
if (!painted && !final && bars.length < Math.min(requested, FIRST_PAINT_BARS)) return;
|
|
7552
7856
|
this.setBarSeries(bars, painted ? { preserveView: true } : void 0);
|
|
7553
7857
|
if (!painted && bars.length > 0) {
|
|
7554
7858
|
painted = true;
|
|
@@ -7567,10 +7871,14 @@ var Vela = (function (exports) {
|
|
|
7567
7871
|
}
|
|
7568
7872
|
};
|
|
7569
7873
|
abort.signal.addEventListener("abort", () => signal(true), { once: true });
|
|
7570
|
-
this.feed.loadProgressive(
|
|
7571
|
-
|
|
7572
|
-
|
|
7573
|
-
|
|
7874
|
+
this.feed.loadProgressive(
|
|
7875
|
+
market,
|
|
7876
|
+
(bars) => {
|
|
7877
|
+
paint(bars, false);
|
|
7878
|
+
if (painted) signal(true);
|
|
7879
|
+
},
|
|
7880
|
+
{ signal: abort.signal }
|
|
7881
|
+
).then((full) => {
|
|
7574
7882
|
if (this.progressiveAbort === abort) this.progressiveAbort = null;
|
|
7575
7883
|
if (full == null) return signal(false);
|
|
7576
7884
|
if (this.generation !== gen) return signal(true);
|
|
@@ -7657,7 +7965,14 @@ var Vela = (function (exports) {
|
|
|
7657
7965
|
* an in-flight `setMarket` immediately (the config mutates before the load). */
|
|
7658
7966
|
marketSnapshot() {
|
|
7659
7967
|
const m = this.config.market;
|
|
7660
|
-
return {
|
|
7968
|
+
return {
|
|
7969
|
+
symbol: m.symbol,
|
|
7970
|
+
provider: parseSymbol(m.symbol ?? "").provider ?? void 0,
|
|
7971
|
+
timeframe: m.timeframe,
|
|
7972
|
+
bars: m.bars,
|
|
7973
|
+
session: m.session,
|
|
7974
|
+
offline: m.data !== void 0
|
|
7975
|
+
};
|
|
7661
7976
|
}
|
|
7662
7977
|
/**
|
|
7663
7978
|
* Switch the chart's market IN PLACE — no destroy/recreate. The renderer stays
|
|
@@ -7723,10 +8038,7 @@ var Vela = (function (exports) {
|
|
|
7723
8038
|
if (depthOnly) {
|
|
7724
8039
|
this.extendDepth(gen, m.bars ?? 500);
|
|
7725
8040
|
} else {
|
|
7726
|
-
await Promise.race([
|
|
7727
|
-
this.loadMarket(gen, { firstLoad: false }),
|
|
7728
|
-
new Promise((resolve) => this.supersedeWaiters.push(resolve))
|
|
7729
|
-
]);
|
|
8041
|
+
await Promise.race([this.loadMarket(gen, { firstLoad: false }), new Promise((resolve) => this.supersedeWaiters.push(resolve))]);
|
|
7730
8042
|
}
|
|
7731
8043
|
if (this.generation !== gen) return;
|
|
7732
8044
|
} finally {
|
|
@@ -8389,8 +8701,8 @@ var Vela = (function (exports) {
|
|
|
8389
8701
|
onModel: (model) => {
|
|
8390
8702
|
const first2 = !record.announced;
|
|
8391
8703
|
const cause = record.pendingCause ?? "history";
|
|
8704
|
+
if (!this.applyModel(id, model)) return;
|
|
8392
8705
|
record.pendingCause = void 0;
|
|
8393
|
-
this.applyModel(id, model);
|
|
8394
8706
|
this.emitContextChanged(id);
|
|
8395
8707
|
this.emitScriptRun(id, cause, first2);
|
|
8396
8708
|
},
|
|
@@ -8683,10 +8995,16 @@ var Vela = (function (exports) {
|
|
|
8683
8995
|
* Apply an emitted model. First emission mounts (and routes the pane); a pending
|
|
8684
8996
|
* structural change (after an input edit) remounts idempotently; everything else
|
|
8685
8997
|
* (live tick / viewport re-run) value-patches.
|
|
8998
|
+
*
|
|
8999
|
+
* Returns false when the model was DEFERRED — an output-free model arriving while
|
|
9000
|
+
* the record is still loading and the chart has no bars (see below); every other
|
|
9001
|
+
* outcome, including the hidden drop, returns true so the caller's event semantics
|
|
9002
|
+
* stay unchanged.
|
|
8686
9003
|
*/
|
|
8687
9004
|
applyModel(id, model) {
|
|
8688
9005
|
const record = this.registry.get(id);
|
|
8689
|
-
if (!record || record.hidden) return;
|
|
9006
|
+
if (!record || record.hidden) return true;
|
|
9007
|
+
if (record.loading && this.bars.length === 0 && !_EngineOrchestrator.modelHasOutput(model)) return false;
|
|
8690
9008
|
const handle = this.handles.get(id);
|
|
8691
9009
|
if (!record.renderHandle) {
|
|
8692
9010
|
const paneId2 = this.routePane(id, model, record.options ?? {});
|
|
@@ -8696,7 +9014,7 @@ var Vela = (function (exports) {
|
|
|
8696
9014
|
record.renderHandle = this.renderer.mountIndicator(model);
|
|
8697
9015
|
record.pendingStructural = false;
|
|
8698
9016
|
this.announce(record, handle);
|
|
8699
|
-
return;
|
|
9017
|
+
return true;
|
|
8700
9018
|
}
|
|
8701
9019
|
let paneId = record.model?.paneId ?? "price";
|
|
8702
9020
|
const prevOwnScale = record.model?.ownScale === true;
|
|
@@ -8724,6 +9042,11 @@ var Vela = (function (exports) {
|
|
|
8724
9042
|
}
|
|
8725
9043
|
if (record.loading) this.setLoading(record, false);
|
|
8726
9044
|
this.announce(record, handle);
|
|
9045
|
+
return true;
|
|
9046
|
+
}
|
|
9047
|
+
/** True when the model carries ANY executed output — series, drawings, bar colors, or trades. */
|
|
9048
|
+
static modelHasOutput(model) {
|
|
9049
|
+
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;
|
|
8727
9050
|
}
|
|
8728
9051
|
routePane(id, model, options) {
|
|
8729
9052
|
if (options.pane === "new") return `pane-${id}`;
|
|
@@ -9426,6 +9749,7 @@ var Vela = (function (exports) {
|
|
|
9426
9749
|
"4h": 144e5,
|
|
9427
9750
|
"1d": 864e5,
|
|
9428
9751
|
"1w": 6048e5,
|
|
9752
|
+
"1M": 2592e6,
|
|
9429
9753
|
"1": 6e4,
|
|
9430
9754
|
"5": 3e5,
|
|
9431
9755
|
"15": 9e5,
|
|
@@ -9433,7 +9757,8 @@ var Vela = (function (exports) {
|
|
|
9433
9757
|
"60": 36e5,
|
|
9434
9758
|
"240": 144e5,
|
|
9435
9759
|
D: 864e5,
|
|
9436
|
-
W: 6048e5
|
|
9760
|
+
W: 6048e5,
|
|
9761
|
+
M: 2592e6
|
|
9437
9762
|
};
|
|
9438
9763
|
return map[timeframe] ?? 36e5;
|
|
9439
9764
|
}
|
|
@@ -26060,7 +26385,9 @@ void main() {
|
|
|
26060
26385
|
const lo = Math.min(from, to);
|
|
26061
26386
|
const hi = Math.max(from, to);
|
|
26062
26387
|
const visible = (a, b, extend) => {
|
|
26063
|
-
if (extend
|
|
26388
|
+
if (extend === "both") return true;
|
|
26389
|
+
if (extend === "left") return Math.max(a, b) >= lo;
|
|
26390
|
+
if (extend === "right") return Math.min(a, b) <= hi;
|
|
26064
26391
|
return Math.max(a, b) >= lo && Math.min(a, b) <= hi;
|
|
26065
26392
|
};
|
|
26066
26393
|
let min2 = Infinity;
|
|
@@ -28624,6 +28951,17 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
28624
28951
|
constructor() {
|
|
28625
28952
|
/** The current `paintAll` call's interaction state, visible to the per-type painters. */
|
|
28626
28953
|
this.targets = {};
|
|
28954
|
+
/** The chart's active series LOOK — style + resolved series colors — pushed by the
|
|
28955
|
+
* controller before each paint. The magnifier's inset mirrors both: candles/bars/line/
|
|
28956
|
+
* area restyle the paint (bar-transform styles like Heikin Ashi transform the fetched
|
|
28957
|
+
* bars; unknown/custom styles fall back to candles), and the colors default to the main
|
|
28958
|
+
* series' own so the inset reads as a finer copy of the chart. */
|
|
28959
|
+
this.seriesLook = {
|
|
28960
|
+
style: "candles",
|
|
28961
|
+
upColor: BULLISH,
|
|
28962
|
+
downColor: BEARISH,
|
|
28963
|
+
lineColor: BULLISH
|
|
28964
|
+
};
|
|
28627
28965
|
}
|
|
28628
28966
|
/** Paint every visible drawing, then selection handles for the targeted ones.
|
|
28629
28967
|
* Each drawing is clipped to its own pane's rect (and skipped entirely while that pane
|
|
@@ -28668,6 +29006,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
28668
29006
|
ctx.globalAlpha = GHOST_ALPHA;
|
|
28669
29007
|
if (ghost instanceof RegressionChannel || ghost instanceof FixedRangeVolumeProfile) {
|
|
28670
29008
|
this.paintTimeSpanGhost(ctx, ghost, proj);
|
|
29009
|
+
} else if (ghost instanceof Magnifier) {
|
|
29010
|
+
this.paintMagnifierGhost(ctx, ghost, proj, theme);
|
|
28671
29011
|
} else this.paintOne(ctx, ghost, proj, theme);
|
|
28672
29012
|
ctx.globalAlpha = 1;
|
|
28673
29013
|
}
|
|
@@ -28801,6 +29141,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
28801
29141
|
this.paintLabel(ctx, d, proj, theme);
|
|
28802
29142
|
return;
|
|
28803
29143
|
}
|
|
29144
|
+
if (d instanceof Magnifier) {
|
|
29145
|
+
this.paintMagnifier(ctx, d, proj, theme);
|
|
29146
|
+
return;
|
|
29147
|
+
}
|
|
28804
29148
|
if (d instanceof PatternDrawing) {
|
|
28805
29149
|
this.paintPattern(ctx, d, proj, theme);
|
|
28806
29150
|
return;
|
|
@@ -29293,6 +29637,216 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
29293
29637
|
ctx.textBaseline = "alphabetic";
|
|
29294
29638
|
}
|
|
29295
29639
|
}
|
|
29640
|
+
/** Placement preview for the magnifier: a dashed rectangle outline only — no backdrop and
|
|
29641
|
+
* no series read, so dragging the area open never kicks a fetch per cursor move. */
|
|
29642
|
+
paintMagnifierGhost(ctx, d, proj, theme) {
|
|
29643
|
+
const r = d.rect(proj);
|
|
29644
|
+
if (!r) return;
|
|
29645
|
+
ctx.save();
|
|
29646
|
+
ctx.strokeStyle = d.style.lineColor || contrastColor(theme.background);
|
|
29647
|
+
ctx.lineWidth = 1;
|
|
29648
|
+
ctx.setLineDash([4, 4]);
|
|
29649
|
+
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));
|
|
29650
|
+
ctx.restore();
|
|
29651
|
+
}
|
|
29652
|
+
/** Paint a magnifier: an opaque theme-background inset whose interior shows the chart's
|
|
29653
|
+
* market at a finer timeframe — candles at their true time/price positions, clipped to
|
|
29654
|
+
* the rectangle. Bars come through `Projector.seriesInRange` (cache read; `loading` and
|
|
29655
|
+
* `unavailable` states paint a short notice instead). The lower-timeframe candles shift
|
|
29656
|
+
* half a chart bar LEFT of their raw time pixel so each chart candle's visual cell —
|
|
29657
|
+
* centered on its open time — subdivides in place. */
|
|
29658
|
+
paintMagnifier(ctx, d, proj, theme) {
|
|
29659
|
+
const r = d.rect(proj);
|
|
29660
|
+
const a = d.anchors[0];
|
|
29661
|
+
const b = d.anchors[1];
|
|
29662
|
+
if (!r || !a || !b) return;
|
|
29663
|
+
const x0 = Math.min(r.x1, r.x2);
|
|
29664
|
+
const x1 = Math.max(r.x1, r.x2);
|
|
29665
|
+
const y0 = Math.min(r.y1, r.y2);
|
|
29666
|
+
const y1 = Math.max(r.y1, r.y2);
|
|
29667
|
+
const w = x1 - x0;
|
|
29668
|
+
const h = y1 - y0;
|
|
29669
|
+
ctx.save();
|
|
29670
|
+
ctx.globalAlpha = 1;
|
|
29671
|
+
ctx.fillStyle = theme.background;
|
|
29672
|
+
ctx.fillRect(x0, y0, w, h);
|
|
29673
|
+
ctx.restore();
|
|
29674
|
+
const from = Math.min(a.time, b.time);
|
|
29675
|
+
const to = Math.max(a.time, b.time);
|
|
29676
|
+
const chartBars = proj.barsBetween ? proj.barsBetween(from, to) : 0;
|
|
29677
|
+
const chartMs = chartBars > 0 ? (to - from) / chartBars : 0;
|
|
29678
|
+
const res = proj.seriesInRange && chartMs > 0 && w > 1 && h > 1 ? proj.seriesInRange(d.magnifier.timeframe, from, to + chartMs) : void 0;
|
|
29679
|
+
let seriesBars = res?.state === "ready" || res?.state === "loading" ? res.bars ?? [] : [];
|
|
29680
|
+
if (res && (res.state === "ready" || res.state === "loading") && seriesBars.length > 0) {
|
|
29681
|
+
const look = this.seriesLook;
|
|
29682
|
+
const transform = look.style !== "candles" ? barTransformFor(look.style) : null;
|
|
29683
|
+
if (transform) seriesBars = transform.full(seriesBars);
|
|
29684
|
+
const mode = look.style === "bars" ? "bars" : look.style === "line" || look.style === "baseline" ? "line" : look.style === "area" ? "area" : "candles";
|
|
29685
|
+
const halfPitch = (proj.xOf(from + chartMs) - proj.xOf(from)) / 2;
|
|
29686
|
+
ctx.save();
|
|
29687
|
+
ctx.beginPath();
|
|
29688
|
+
ctx.rect(x0, y0, w, h);
|
|
29689
|
+
ctx.clip();
|
|
29690
|
+
if (mode === "line" || mode === "area") {
|
|
29691
|
+
this.paintMagnifierLine(ctx, d, proj, seriesBars, res.barMs, halfPitch, y1, mode === "area", d.magnifier.upColor || look.lineColor);
|
|
29692
|
+
} else {
|
|
29693
|
+
this.paintMagnifierBars(ctx, d, proj, seriesBars, res.barMs, halfPitch, x0, x1, mode, d.magnifier.upColor || look.upColor, d.magnifier.downColor || look.downColor);
|
|
29694
|
+
}
|
|
29695
|
+
ctx.restore();
|
|
29696
|
+
} else if (res) {
|
|
29697
|
+
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";
|
|
29698
|
+
this.paintMagnifierNotice(ctx, notice, x0, y0, w, h, theme);
|
|
29699
|
+
}
|
|
29700
|
+
ctx.save();
|
|
29701
|
+
ctx.strokeStyle = d.style.lineColor || contrastColor(theme.background);
|
|
29702
|
+
ctx.lineWidth = d.style.lineWidth || 1;
|
|
29703
|
+
ctx.setLineDash(dashPattern(d.style.lineStyle, d.style.lineWidth || 1));
|
|
29704
|
+
ctx.strokeRect(x0, y0, w, h);
|
|
29705
|
+
ctx.restore();
|
|
29706
|
+
if (w > 44) {
|
|
29707
|
+
const label = magnifierTimeframeLabel(res?.state === "ready" || res?.state === "loading" ? res.timeframe : d.magnifier.timeframe);
|
|
29708
|
+
const chipH = 17;
|
|
29709
|
+
const gap = 4;
|
|
29710
|
+
const pane = proj.paneRect?.(d.paneId);
|
|
29711
|
+
const paneBottom = pane ? pane.top + pane.height : proj.height;
|
|
29712
|
+
const below = y1 + gap + chipH <= paneBottom;
|
|
29713
|
+
const chipY = below ? y1 + gap : y1 - gap - chipH;
|
|
29714
|
+
ctx.save();
|
|
29715
|
+
ctx.font = `10px ${theme.fontFamily}`;
|
|
29716
|
+
const tw = ctx.measureText(label).width;
|
|
29717
|
+
const caretW = 11;
|
|
29718
|
+
const chipW = tw + 12 + caretW;
|
|
29719
|
+
roundRect(ctx, x0, chipY, chipW, chipH, 3);
|
|
29720
|
+
ctx.fillStyle = theme.background;
|
|
29721
|
+
ctx.fill();
|
|
29722
|
+
ctx.strokeStyle = withAlpha(theme.textColor, 0.28);
|
|
29723
|
+
ctx.lineWidth = 1;
|
|
29724
|
+
ctx.setLineDash([]);
|
|
29725
|
+
ctx.stroke();
|
|
29726
|
+
ctx.fillStyle = theme.textColor;
|
|
29727
|
+
ctx.textAlign = "left";
|
|
29728
|
+
ctx.textBaseline = "middle";
|
|
29729
|
+
ctx.fillText(label, x0 + 6, chipY + chipH / 2 + 0.5);
|
|
29730
|
+
const cxr = x0 + 6 + tw + 5;
|
|
29731
|
+
const cyr = chipY + chipH / 2;
|
|
29732
|
+
ctx.strokeStyle = withAlpha(theme.textColor, 0.7);
|
|
29733
|
+
ctx.lineWidth = 1.2;
|
|
29734
|
+
ctx.beginPath();
|
|
29735
|
+
ctx.moveTo(cxr, cyr - 1.5);
|
|
29736
|
+
ctx.lineTo(cxr + 2.5, cyr + 1.5);
|
|
29737
|
+
ctx.lineTo(cxr + 5, cyr - 1.5);
|
|
29738
|
+
ctx.stroke();
|
|
29739
|
+
ctx.restore();
|
|
29740
|
+
d.chipRect = { x: x0, y: chipY, w: chipW, h: chipH };
|
|
29741
|
+
} else {
|
|
29742
|
+
d.chipRect = null;
|
|
29743
|
+
}
|
|
29744
|
+
}
|
|
29745
|
+
/** The magnifier's candle/bar loop: each bar's cell spans its open→close time (shifted left
|
|
29746
|
+
* by half a chart bar). Candles: wick always, body once the cell is wide enough to carry
|
|
29747
|
+
* one. OHLC bars: the high–low spine with open/close ticks once the cell has the room. */
|
|
29748
|
+
paintMagnifierBars(ctx, d, proj, bars, barMs, halfPitch, x0, x1, mode, upColor, downColor) {
|
|
29749
|
+
ctx.setLineDash([]);
|
|
29750
|
+
ctx.lineWidth = 1;
|
|
29751
|
+
for (const bar of bars) {
|
|
29752
|
+
const cx0 = proj.xOf(bar.time) - halfPitch;
|
|
29753
|
+
const cx1 = proj.xOf(bar.time + barMs) - halfPitch;
|
|
29754
|
+
if (cx1 < x0 || cx0 > x1) continue;
|
|
29755
|
+
const yHigh = proj.yOf(bar.high, d.paneId);
|
|
29756
|
+
const yLow = proj.yOf(bar.low, d.paneId);
|
|
29757
|
+
const yOpen = proj.yOf(bar.open, d.paneId);
|
|
29758
|
+
const yClose = proj.yOf(bar.close, d.paneId);
|
|
29759
|
+
if (yHigh == null || yLow == null || yOpen == null || yClose == null) continue;
|
|
29760
|
+
const color = bar.close >= bar.open ? upColor : downColor;
|
|
29761
|
+
const cellW = cx1 - cx0;
|
|
29762
|
+
const cx = (cx0 + cx1) / 2;
|
|
29763
|
+
ctx.strokeStyle = color;
|
|
29764
|
+
ctx.beginPath();
|
|
29765
|
+
ctx.moveTo(cx, yHigh);
|
|
29766
|
+
ctx.lineTo(cx, yLow);
|
|
29767
|
+
ctx.stroke();
|
|
29768
|
+
if (cellW < 3) continue;
|
|
29769
|
+
if (mode === "bars") {
|
|
29770
|
+
const tick = Math.max(1, cellW * 0.35);
|
|
29771
|
+
ctx.beginPath();
|
|
29772
|
+
ctx.moveTo(cx - tick, yOpen);
|
|
29773
|
+
ctx.lineTo(cx, yOpen);
|
|
29774
|
+
ctx.moveTo(cx, yClose);
|
|
29775
|
+
ctx.lineTo(cx + tick, yClose);
|
|
29776
|
+
ctx.stroke();
|
|
29777
|
+
} else {
|
|
29778
|
+
const bw = Math.max(1, cellW * 0.7);
|
|
29779
|
+
ctx.fillStyle = color;
|
|
29780
|
+
ctx.fillRect(cx - bw / 2, Math.min(yOpen, yClose), bw, Math.max(1, Math.abs(yClose - yOpen)));
|
|
29781
|
+
}
|
|
29782
|
+
}
|
|
29783
|
+
}
|
|
29784
|
+
/** The magnifier's line/area rendering: a close polyline through each cell's center (same
|
|
29785
|
+
* half-chart-bar shift as the candles), with an optional translucent fill down to the
|
|
29786
|
+
* rectangle's bottom edge for the area style. Colored like the chart's own line series. */
|
|
29787
|
+
paintMagnifierLine(ctx, d, proj, bars, barMs, halfPitch, yBottom, area, color) {
|
|
29788
|
+
const pts = [];
|
|
29789
|
+
for (const bar of bars) {
|
|
29790
|
+
const y = proj.yOf(bar.close, d.paneId);
|
|
29791
|
+
if (y == null) continue;
|
|
29792
|
+
pts.push([proj.xOf(bar.time + barMs / 2) - halfPitch, y]);
|
|
29793
|
+
}
|
|
29794
|
+
if (pts.length < 2) return;
|
|
29795
|
+
if (area) {
|
|
29796
|
+
ctx.beginPath();
|
|
29797
|
+
ctx.moveTo(pts[0][0], yBottom);
|
|
29798
|
+
for (const [px, py] of pts) ctx.lineTo(px, py);
|
|
29799
|
+
ctx.lineTo(pts[pts.length - 1][0], yBottom);
|
|
29800
|
+
ctx.closePath();
|
|
29801
|
+
ctx.fillStyle = withAlpha(color, 0.15);
|
|
29802
|
+
ctx.fill();
|
|
29803
|
+
}
|
|
29804
|
+
ctx.setLineDash([]);
|
|
29805
|
+
ctx.lineWidth = 1.5;
|
|
29806
|
+
ctx.strokeStyle = color;
|
|
29807
|
+
ctx.beginPath();
|
|
29808
|
+
ctx.moveTo(pts[0][0], pts[0][1]);
|
|
29809
|
+
for (let i = 1; i < pts.length; i += 1) ctx.lineTo(pts[i][0], pts[i][1]);
|
|
29810
|
+
ctx.stroke();
|
|
29811
|
+
}
|
|
29812
|
+
/** Centered muted notice inside the magnifier rect (loading / unavailable states). */
|
|
29813
|
+
paintMagnifierNotice(ctx, text, x0, y0, w, h, theme) {
|
|
29814
|
+
if (w < 60 || h < 20) return;
|
|
29815
|
+
ctx.save();
|
|
29816
|
+
ctx.beginPath();
|
|
29817
|
+
ctx.rect(x0, y0, w, h);
|
|
29818
|
+
ctx.clip();
|
|
29819
|
+
ctx.font = `11px ${theme.fontFamily}`;
|
|
29820
|
+
ctx.fillStyle = withAlpha(theme.textColor, 0.55);
|
|
29821
|
+
ctx.textAlign = "center";
|
|
29822
|
+
ctx.textBaseline = "middle";
|
|
29823
|
+
ctx.fillText(text, x0 + w / 2, y0 + h / 2);
|
|
29824
|
+
ctx.restore();
|
|
29825
|
+
}
|
|
29826
|
+
/** A bottom-center pill prompting the armed tool's placement gesture (e.g. the magnifier's
|
|
29827
|
+
* "drag an area"). Painted by the drawings layer while the tool is armed and no placement
|
|
29828
|
+
* is in progress; chart-background fill so it reads as chrome over any content. */
|
|
29829
|
+
paintPlacementHint(ctx, text, theme, width, height) {
|
|
29830
|
+
ctx.save();
|
|
29831
|
+
ctx.font = `11px ${theme.fontFamily}`;
|
|
29832
|
+
const tw = ctx.measureText(text).width;
|
|
29833
|
+
const pillW = tw + 24;
|
|
29834
|
+
const pillH = 24;
|
|
29835
|
+
const x = (width - pillW) / 2;
|
|
29836
|
+
const y = height - pillH - 14;
|
|
29837
|
+
roundRect(ctx, x, y, pillW, pillH, pillH / 2);
|
|
29838
|
+
ctx.fillStyle = theme.background;
|
|
29839
|
+
ctx.fill();
|
|
29840
|
+
ctx.strokeStyle = withAlpha(theme.textColor, 0.28);
|
|
29841
|
+
ctx.lineWidth = 1;
|
|
29842
|
+
ctx.setLineDash([]);
|
|
29843
|
+
ctx.stroke();
|
|
29844
|
+
ctx.fillStyle = theme.textColor;
|
|
29845
|
+
ctx.textAlign = "center";
|
|
29846
|
+
ctx.textBaseline = "middle";
|
|
29847
|
+
ctx.fillText(text, width / 2, y + pillH / 2 + 0.5);
|
|
29848
|
+
ctx.restore();
|
|
29849
|
+
}
|
|
29296
29850
|
/** Paint a fixed-range volume profile: horizontal histogram rows (up/down split) anchored to
|
|
29297
29851
|
* the left or right of the time span, optional VAH / VAL / POC levels across the range, and
|
|
29298
29852
|
* optional developing POC / VA polylines. Recomputes from the two anchors on every paint. */
|
|
@@ -30708,8 +31262,9 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
30708
31262
|
if (!existing) document.head.appendChild(s);
|
|
30709
31263
|
}
|
|
30710
31264
|
var DrawingSettingsPopup = class {
|
|
30711
|
-
constructor(host, theme) {
|
|
31265
|
+
constructor(host, theme, chartBarMs = () => 0) {
|
|
30712
31266
|
this.host = host;
|
|
31267
|
+
this.chartBarMs = chartBarMs;
|
|
30713
31268
|
this.el = null;
|
|
30714
31269
|
this.tipEl = null;
|
|
30715
31270
|
// floating hover-label (above/below the toolbar)
|
|
@@ -30734,6 +31289,14 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
30734
31289
|
this.theme = theme;
|
|
30735
31290
|
this.settingsDialog = new DrawingSettingsDialog(host, theme);
|
|
30736
31291
|
}
|
|
31292
|
+
/** The magnifier timeframe choices strictly below the chart's own bar duration
|
|
31293
|
+
* (`auto` rides along while at least one concrete lower step exists). */
|
|
31294
|
+
lowerTimeframeOptions() {
|
|
31295
|
+
const chartMs = this.chartBarMs();
|
|
31296
|
+
if (!(chartMs > 0)) return [...MAGNIFIER_TIMEFRAME_OPTIONS];
|
|
31297
|
+
const lower = MAGNIFIER_TIMEFRAME_OPTIONS.filter((o) => o.ms > 0 && o.ms < chartMs);
|
|
31298
|
+
return lower.length > 0 ? [MAGNIFIER_TIMEFRAME_OPTIONS[0], ...lower] : [];
|
|
31299
|
+
}
|
|
30737
31300
|
setTheme(theme) {
|
|
30738
31301
|
this.theme = theme;
|
|
30739
31302
|
this.settingsDialog.setTheme(theme);
|
|
@@ -30776,7 +31339,20 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
30776
31339
|
const sz = drawing.size ?? "normal";
|
|
30777
31340
|
bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label: sizeLabel }));
|
|
30778
31341
|
}
|
|
30779
|
-
if (paths.has("
|
|
31342
|
+
if (paths.has("magnifier.timeframe") && drawing instanceof Magnifier) {
|
|
31343
|
+
const options = this.lowerTimeframeOptions();
|
|
31344
|
+
if (options.length > 0) {
|
|
31345
|
+
bar.appendChild(
|
|
31346
|
+
this.dropdown("Lower timeframe", options.map((o) => o.value), drawing.magnifier.timeframe, () => "", (v) => actions.patch({ "magnifier.timeframe": v }), {
|
|
31347
|
+
label: (v) => magnifierTimeframeLabel(String(v)),
|
|
31348
|
+
labelInTrigger: true
|
|
31349
|
+
})
|
|
31350
|
+
);
|
|
31351
|
+
}
|
|
31352
|
+
bar.appendChild(this.colorButton("Up candles", BUCKET_ICON, drawing.magnifier.upColor || t.upColor, (v) => actions.patch({ "magnifier.upColor": v })));
|
|
31353
|
+
bar.appendChild(this.colorButton("Down candles", BUCKET_ICON, drawing.magnifier.downColor || t.downColor, (v) => actions.patch({ "magnifier.downColor": v })));
|
|
31354
|
+
}
|
|
31355
|
+
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 })));
|
|
30780
31356
|
if (paths.has("style.lineWidth")) {
|
|
30781
31357
|
const wf = schema.fields.find((f) => f.path === "style.lineWidth");
|
|
30782
31358
|
if (wf?.kind === "number" && (wf.min ?? 1) > 1) {
|
|
@@ -31098,6 +31674,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
31098
31674
|
this.colorPop = null;
|
|
31099
31675
|
this.colorOwner = null;
|
|
31100
31676
|
}
|
|
31677
|
+
opts.onClose?.();
|
|
31101
31678
|
}
|
|
31102
31679
|
});
|
|
31103
31680
|
const el = pop.el;
|
|
@@ -31116,6 +31693,49 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
31116
31693
|
pop.show();
|
|
31117
31694
|
return pop;
|
|
31118
31695
|
}
|
|
31696
|
+
/**
|
|
31697
|
+
* A standalone timeframe menu for the magnifier's ON-CHART chip. The chip lives on
|
|
31698
|
+
* canvas, so a transient invisible anchor is dropped at its pixel rect for the popover
|
|
31699
|
+
* to position against, and removed again when the menu closes. Independent of the
|
|
31700
|
+
* quick toolbar — the chip works without selecting the drawing first.
|
|
31701
|
+
*/
|
|
31702
|
+
openMagnifierTimeframeMenu(rect, current, onPick) {
|
|
31703
|
+
ensureStyles3();
|
|
31704
|
+
closeOpenPopovers();
|
|
31705
|
+
const options = this.lowerTimeframeOptions();
|
|
31706
|
+
const anchor = document.createElement("div");
|
|
31707
|
+
anchor.style.cssText = `position:absolute;left:${rect.x}px;top:${rect.y}px;width:${rect.w}px;height:${rect.h}px;pointer-events:none;`;
|
|
31708
|
+
this.host.appendChild(anchor);
|
|
31709
|
+
this.menuPop = this.hostFloat(anchor, {
|
|
31710
|
+
zIndex: 26,
|
|
31711
|
+
padding: "4px",
|
|
31712
|
+
onClose: () => anchor.remove(),
|
|
31713
|
+
fill: (menu, pop) => {
|
|
31714
|
+
if (options.length === 0) {
|
|
31715
|
+
const note = document.createElement("div");
|
|
31716
|
+
note.style.cssText = "padding:6px 10px;opacity:0.65;white-space:nowrap;";
|
|
31717
|
+
note.textContent = "No lower timeframe available";
|
|
31718
|
+
menu.appendChild(note);
|
|
31719
|
+
return;
|
|
31720
|
+
}
|
|
31721
|
+
for (const o of options) {
|
|
31722
|
+
const item = document.createElement("button");
|
|
31723
|
+
item.type = "button";
|
|
31724
|
+
item.className = "vela-dpop-item";
|
|
31725
|
+
item.dataset.active = o.value === current ? "1" : "0";
|
|
31726
|
+
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;";
|
|
31727
|
+
item.textContent = o.label;
|
|
31728
|
+
item.addEventListener("click", (e) => {
|
|
31729
|
+
e.stopPropagation();
|
|
31730
|
+
pop.hide();
|
|
31731
|
+
onPick(o.value);
|
|
31732
|
+
});
|
|
31733
|
+
menu.appendChild(item);
|
|
31734
|
+
}
|
|
31735
|
+
}
|
|
31736
|
+
});
|
|
31737
|
+
this.menuOwner = anchor;
|
|
31738
|
+
}
|
|
31119
31739
|
/** A floating list of one-shot actions (icon + label rows) opened by the kebab. */
|
|
31120
31740
|
openActionMenu(anchor, rows) {
|
|
31121
31741
|
this.menuPop = this.hostFloat(anchor, {
|
|
@@ -31226,10 +31846,13 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
31226
31846
|
let cur = current;
|
|
31227
31847
|
const paint = (v) => {
|
|
31228
31848
|
b.replaceChildren();
|
|
31229
|
-
const
|
|
31230
|
-
|
|
31231
|
-
|
|
31232
|
-
|
|
31849
|
+
const glyph = render(v);
|
|
31850
|
+
if (glyph) {
|
|
31851
|
+
const ic = document.createElement("span");
|
|
31852
|
+
ic.style.cssText = "display:flex;";
|
|
31853
|
+
ic.innerHTML = sized(glyph);
|
|
31854
|
+
b.appendChild(ic);
|
|
31855
|
+
}
|
|
31233
31856
|
if (opts.label && opts.labelInTrigger) {
|
|
31234
31857
|
const tx = document.createElement("span");
|
|
31235
31858
|
tx.textContent = opts.label(v);
|
|
@@ -31271,10 +31894,13 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
31271
31894
|
item.className = "vela-dpop-item";
|
|
31272
31895
|
item.dataset.active = active ? "1" : "0";
|
|
31273
31896
|
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;`;
|
|
31274
|
-
const
|
|
31275
|
-
|
|
31276
|
-
|
|
31277
|
-
|
|
31897
|
+
const glyph = render(v);
|
|
31898
|
+
if (glyph) {
|
|
31899
|
+
const ic = document.createElement("span");
|
|
31900
|
+
ic.style.cssText = "display:flex;flex:none;width:22px;justify-content:center;";
|
|
31901
|
+
ic.innerHTML = sized(glyph, 18);
|
|
31902
|
+
item.appendChild(ic);
|
|
31903
|
+
}
|
|
31278
31904
|
if (label) {
|
|
31279
31905
|
const tx = document.createElement("span");
|
|
31280
31906
|
tx.textContent = label(v);
|
|
@@ -32258,6 +32884,9 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
32258
32884
|
this.intentCb = null;
|
|
32259
32885
|
/** Another chart's in-progress placement, mirrored here as a ghost (drawings sync). */
|
|
32260
32886
|
this.externalGhost = null;
|
|
32887
|
+
/** Core-pushed series gateway (finer-timeframe bars for data-driven drawings). */
|
|
32888
|
+
this.seriesGw = null;
|
|
32889
|
+
this.seriesGwUnsub = null;
|
|
32261
32890
|
/** Last draft fingerprint reported upstream — gates the per-render emission to actual changes. */
|
|
32262
32891
|
this.lastDraftKey = null;
|
|
32263
32892
|
this.measure = new MeasureOverlay();
|
|
@@ -32285,7 +32914,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
32285
32914
|
this.textEditor = null;
|
|
32286
32915
|
this.painter = new DrawingPainter();
|
|
32287
32916
|
this.ctx = canvas.getContext("2d");
|
|
32288
|
-
this.popup = new DrawingSettingsPopup(overlayHost, deps.theme());
|
|
32917
|
+
this.popup = new DrawingSettingsPopup(overlayHost, deps.theme(), () => deps.chartBarMs());
|
|
32289
32918
|
this.toolbar = new DrawingToolbar(
|
|
32290
32919
|
toolbarHost,
|
|
32291
32920
|
deps.theme(),
|
|
@@ -32343,6 +32972,22 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
32343
32972
|
const shown = this.toolbarVisible && !this.mobileLayout;
|
|
32344
32973
|
this.deps.setToolbarGutter(shown ? this.toolbarCollapsed ? TOOLBAR_COLLAPSED_WIDTH : TOOLBAR_WIDTH : 0);
|
|
32345
32974
|
}
|
|
32975
|
+
/** Core push: the series gateway data-driven drawings read finer-timeframe bars
|
|
32976
|
+
* through (surfaced to them as `Projector.seriesInRange`). A landed background
|
|
32977
|
+
* fetch repaints both this layer and the interleave slices under the series. */
|
|
32978
|
+
setSeriesGateway(gateway) {
|
|
32979
|
+
this.seriesGwUnsub?.();
|
|
32980
|
+
this.seriesGw = gateway;
|
|
32981
|
+
this.seriesGwUnsub = gateway.onUpdate(() => {
|
|
32982
|
+
this.invalidateSlices();
|
|
32983
|
+
this.render();
|
|
32984
|
+
this.deps.requestDataPaint();
|
|
32985
|
+
});
|
|
32986
|
+
}
|
|
32987
|
+
/** The pushed series gateway, or null before the core provides one. */
|
|
32988
|
+
get seriesGateway() {
|
|
32989
|
+
return this.seriesGw;
|
|
32990
|
+
}
|
|
32346
32991
|
/** Core push: mirror (or clear) another chart's in-progress placement as a ghost. */
|
|
32347
32992
|
setExternalGhost(doc) {
|
|
32348
32993
|
this.externalGhost = doc ? deserializeDrawing(doc) : null;
|
|
@@ -32460,8 +33105,34 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
32460
33105
|
/** Should the drawing layer win this press (vs pan)? */
|
|
32461
33106
|
claim(x, y) {
|
|
32462
33107
|
if (this.measureMode || this.eraserMode) return true;
|
|
33108
|
+
if (this.magnifierChipAt(x, y)) return true;
|
|
32463
33109
|
return this.interaction.claim(x, y);
|
|
32464
33110
|
}
|
|
33111
|
+
/** The topmost visible (unlocked) magnifier whose timeframe chip contains (x, y) —
|
|
33112
|
+
* the chip's rect is what the painter measured last frame. */
|
|
33113
|
+
magnifierChipAt(x, y) {
|
|
33114
|
+
for (let i = this.drawings.length - 1; i >= 0; i -= 1) {
|
|
33115
|
+
const d = this.drawings[i];
|
|
33116
|
+
if (!(d instanceof Magnifier) || !d.visible || d.locked) continue;
|
|
33117
|
+
const r = d.chipRect;
|
|
33118
|
+
if (r && x >= r.x && x <= r.x + r.w && y >= r.y && y <= r.y + r.h) return d;
|
|
33119
|
+
}
|
|
33120
|
+
return null;
|
|
33121
|
+
}
|
|
33122
|
+
/** Open the on-chart chip's timeframe menu; the pick patches the drawing like a
|
|
33123
|
+
* settings-popup edit (same intent, same undo step). */
|
|
33124
|
+
openMagnifierChipMenu(drawing) {
|
|
33125
|
+
const rect = drawing.chipRect;
|
|
33126
|
+
if (!rect) return;
|
|
33127
|
+
const id = drawing.id;
|
|
33128
|
+
this.popup.openMagnifierTimeframeMenu(rect, drawing.magnifier.timeframe, (value) => {
|
|
33129
|
+
const d = this.drawings.find((x) => x.id === id);
|
|
33130
|
+
if (!(d instanceof Magnifier)) return;
|
|
33131
|
+
d.applySettings({ "magnifier.timeframe": value });
|
|
33132
|
+
this.render();
|
|
33133
|
+
this.emit({ kind: "edit", doc: d.serialize() });
|
|
33134
|
+
});
|
|
33135
|
+
}
|
|
32465
33136
|
/** Delete the (unlocked) drawing under the cursor. True when one was removed.
|
|
32466
33137
|
* Shared by the eraser (click + drag) and the middle-click shortcut. */
|
|
32467
33138
|
deleteAt(x, y) {
|
|
@@ -32507,6 +33178,13 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
32507
33178
|
this.render();
|
|
32508
33179
|
return;
|
|
32509
33180
|
}
|
|
33181
|
+
if (this.activeTool == null) {
|
|
33182
|
+
const chipOwner = this.magnifierChipAt(x, y);
|
|
33183
|
+
if (chipOwner) {
|
|
33184
|
+
this.openMagnifierChipMenu(chipOwner);
|
|
33185
|
+
return;
|
|
33186
|
+
}
|
|
33187
|
+
}
|
|
32510
33188
|
this.interaction.down(x, y, snap, shift3);
|
|
32511
33189
|
}
|
|
32512
33190
|
pointerMove(x, y, snap = "off", shift3 = false) {
|
|
@@ -32725,6 +33403,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
32725
33403
|
/** Cursor hint while hovering — `'pointer'` over a drawing/handle, else null. */
|
|
32726
33404
|
cursorAt(x, y) {
|
|
32727
33405
|
if (this.eraserMode) return "pointer";
|
|
33406
|
+
if (this.activeTool == null && this.magnifierChipAt(x, y)) return "pointer";
|
|
32728
33407
|
return this.interaction.cursorAt(x, y);
|
|
32729
33408
|
}
|
|
32730
33409
|
/** Right-click: an explicit escape back to the pointer. Cancels an in-progress
|
|
@@ -32903,6 +33582,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
32903
33582
|
if (!sctx) continue;
|
|
32904
33583
|
sctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
32905
33584
|
sctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
|
|
33585
|
+
this.painter.seriesLook = this.deps.seriesLook();
|
|
32906
33586
|
this.painter.paintAll(sctx, drawings, proj, theme, EMPTY_TARGETS);
|
|
32907
33587
|
const slices = out.get(paneId) ?? [];
|
|
32908
33588
|
slices.push({ beforeZ, canvas });
|
|
@@ -32930,6 +33610,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
32930
33610
|
dragged: this.interaction.activeDragId(),
|
|
32931
33611
|
mutedLabel: edited instanceof TextLabel ? edited.id : null
|
|
32932
33612
|
};
|
|
33613
|
+
this.painter.seriesLook = this.deps.seriesLook();
|
|
32933
33614
|
this.painter.paintAll(ctx, this.drawings.filter((d) => !this.isInterleaved(d)), proj, this.deps.theme(), targets);
|
|
32934
33615
|
this.painter.paintHighlights(ctx, this.drawings.filter((d) => this.isInterleaved(d)), proj, handleIdsFor(targets));
|
|
32935
33616
|
this.layoutTextEditor();
|
|
@@ -32937,6 +33618,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
32937
33618
|
if (ghost) this.painter.paintGhost(ctx, ghost, proj, this.deps.theme());
|
|
32938
33619
|
if (this.externalGhost) this.painter.paintGhost(ctx, this.externalGhost, proj, this.deps.theme());
|
|
32939
33620
|
this.emitDraft(ghost);
|
|
33621
|
+
if (this.activeTool && !ghost) {
|
|
33622
|
+
const hint = getDrawingType(this.activeTool)?.placementHint;
|
|
33623
|
+
if (hint) this.painter.paintPlacementHint(ctx, hint, this.deps.theme(), proj.width, proj.height);
|
|
33624
|
+
}
|
|
32940
33625
|
const markers = this.interaction.placingMarkers(proj);
|
|
32941
33626
|
if (markers) this.painter.paintHandles(ctx, markers);
|
|
32942
33627
|
const m = this.interaction.snapMarker();
|
|
@@ -32948,6 +33633,9 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
32948
33633
|
this.closeTextEditor();
|
|
32949
33634
|
this.popup.destroy();
|
|
32950
33635
|
this.toolbar.destroy();
|
|
33636
|
+
this.seriesGwUnsub?.();
|
|
33637
|
+
this.seriesGwUnsub = null;
|
|
33638
|
+
this.seriesGw = null;
|
|
32951
33639
|
this.intentCb = null;
|
|
32952
33640
|
this.drawings = [];
|
|
32953
33641
|
}
|
|
@@ -33345,7 +34033,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
33345
34033
|
}
|
|
33346
34034
|
|
|
33347
34035
|
// src/renderers/native/drawings/Projector.ts
|
|
33348
|
-
function createProjector(coords, paneOf, paneIdAtY, barsInRange) {
|
|
34036
|
+
function createProjector(coords, paneOf, paneIdAtY, barsInRange, seriesInRange) {
|
|
33349
34037
|
return {
|
|
33350
34038
|
xOf: (time) => coords.timeToX(time),
|
|
33351
34039
|
yOf: (price, paneId) => {
|
|
@@ -33366,6 +34054,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
33366
34054
|
},
|
|
33367
34055
|
barsBetween: (t1, t2) => Math.abs(coords.timeToLogical(t2) - coords.timeToLogical(t1)),
|
|
33368
34056
|
barsInRange: barsInRange ? (from, to) => barsInRange(from, to) : void 0,
|
|
34057
|
+
seriesInRange,
|
|
33369
34058
|
width: coords.width,
|
|
33370
34059
|
height: coords.height
|
|
33371
34060
|
};
|
|
@@ -35369,6 +36058,23 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
35369
36058
|
seriesBoundaries: (paneId) => this.scene.seriesBoundaries(paneId),
|
|
35370
36059
|
priceZ: (paneId) => paneId === PRICE_PANE_ID ? this.scene.candleZ : null,
|
|
35371
36060
|
requestDataPaint: () => this.scheduler.invalidate(3 /* Light */),
|
|
36061
|
+
// The look the price series ACTUALLY paints with: candle colors resolved through
|
|
36062
|
+
// the per-style override, line/area colors through their configured styles — so
|
|
36063
|
+
// series-mirroring content (the magnifier inset) matches the chart exactly.
|
|
36064
|
+
seriesLook: () => {
|
|
36065
|
+
const st = this.scene.style;
|
|
36066
|
+
const paint = effectiveCandlePaint(st.candle, this.scene.candleOverride, this.theme.upColor, this.theme.downColor);
|
|
36067
|
+
const barsUp = st.bars.upColor ?? this.theme.upColor;
|
|
36068
|
+
const barsDown = st.bars.downColor ?? this.theme.downColor;
|
|
36069
|
+
const style = this.scene.priceStyle;
|
|
36070
|
+
return {
|
|
36071
|
+
style,
|
|
36072
|
+
upColor: style === "bars" ? barsUp : paint.up,
|
|
36073
|
+
downColor: style === "bars" ? barsDown : paint.down,
|
|
36074
|
+
lineColor: style === "area" ? st.area.lineColor ?? this.theme.upColor : st.line.color ?? this.theme.upColor
|
|
36075
|
+
};
|
|
36076
|
+
},
|
|
36077
|
+
chartBarMs: () => this.coords.barInterval,
|
|
35372
36078
|
snap: (pt2, paneId, mode, cursorPx) => this.snapToCandle(pt2, paneId, mode, cursorPx),
|
|
35373
36079
|
setSnapMode: (mode) => this.setSnapMode(mode),
|
|
35374
36080
|
setToolbarGutter: (px) => this.setToolbarGutter(px)
|
|
@@ -36762,7 +37468,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
36762
37468
|
return p ? { scale: p.scale, bounds: p.bounds, collapsed: p.collapsed } : null;
|
|
36763
37469
|
},
|
|
36764
37470
|
(y) => this.paneNodeAtY(y)?.id ?? null,
|
|
36765
|
-
(from, to) => this.barsInTimeRange(from, to)
|
|
37471
|
+
(from, to) => this.barsInTimeRange(from, to),
|
|
37472
|
+
this.userDrawings?.seriesGateway ? (tf, from, to) => this.userDrawings.seriesGateway.seriesInRange(tf, from, to) : void 0
|
|
36766
37473
|
);
|
|
36767
37474
|
}
|
|
36768
37475
|
/** OHLC bars whose open-time falls within `[from, to]` (inclusive) — the data a regression
|