@luxalgo/vela 0.6.11 → 0.6.12
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-IZV3CW5N.js} +184 -25
- package/dist/{chunk-73PEA4MU.js → chunk-KV7FDWSL.js} +618 -38
- 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 +727 -26
- 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 +727 -26
- package/dist/vela.global.min.js +52 -52
- package/dist/widget.cjs +910 -50
- package/dist/widget.d.cts +118 -7
- package/dist/widget.d.ts +118 -7
- package/dist/widget.js +4 -4
- package/dist/workspace.cjs +910 -50
- 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;
|
|
@@ -7396,7 +7696,13 @@ var Vela = (function (exports) {
|
|
|
7396
7696
|
const initialStyle = this.renderer.readFeature("priceStyle");
|
|
7397
7697
|
if (typeof initialStyle === "string") this.priceStyle = initialStyle;
|
|
7398
7698
|
this.barTransform = barTransformFor(initialStyle);
|
|
7399
|
-
|
|
7699
|
+
const drawingSeries = new DrawingSeriesService({
|
|
7700
|
+
fetchBars: (tf, range) => this.fetchSeries(this.config.market.symbol ?? "", tf, range),
|
|
7701
|
+
canFetch: () => !!this.feed.loadRange && !this.config.market.data?.length && !!this.config.market.symbol,
|
|
7702
|
+
chartTimeframe: () => this.config.market.timeframe ?? "60",
|
|
7703
|
+
marketKey: () => `${this.config.market.symbol ?? ""}|${this.config.market.session ?? ""}`
|
|
7704
|
+
});
|
|
7705
|
+
this.drawings = new DrawingController(this.renderer, this.events, config.drawings, drawingSeries);
|
|
7400
7706
|
this.unresolvedUnsub = this.feed.onUnresolved?.((info) => {
|
|
7401
7707
|
this.endLoad();
|
|
7402
7708
|
this.events.emit("data:unresolved", info);
|
|
@@ -8389,8 +8695,8 @@ var Vela = (function (exports) {
|
|
|
8389
8695
|
onModel: (model) => {
|
|
8390
8696
|
const first2 = !record.announced;
|
|
8391
8697
|
const cause = record.pendingCause ?? "history";
|
|
8698
|
+
if (!this.applyModel(id, model)) return;
|
|
8392
8699
|
record.pendingCause = void 0;
|
|
8393
|
-
this.applyModel(id, model);
|
|
8394
8700
|
this.emitContextChanged(id);
|
|
8395
8701
|
this.emitScriptRun(id, cause, first2);
|
|
8396
8702
|
},
|
|
@@ -8683,10 +8989,16 @@ var Vela = (function (exports) {
|
|
|
8683
8989
|
* Apply an emitted model. First emission mounts (and routes the pane); a pending
|
|
8684
8990
|
* structural change (after an input edit) remounts idempotently; everything else
|
|
8685
8991
|
* (live tick / viewport re-run) value-patches.
|
|
8992
|
+
*
|
|
8993
|
+
* Returns false when the model was DEFERRED — an output-free model arriving while
|
|
8994
|
+
* the record is still loading and the chart has no bars (see below); every other
|
|
8995
|
+
* outcome, including the hidden drop, returns true so the caller's event semantics
|
|
8996
|
+
* stay unchanged.
|
|
8686
8997
|
*/
|
|
8687
8998
|
applyModel(id, model) {
|
|
8688
8999
|
const record = this.registry.get(id);
|
|
8689
|
-
if (!record || record.hidden) return;
|
|
9000
|
+
if (!record || record.hidden) return true;
|
|
9001
|
+
if (record.loading && this.bars.length === 0 && !_EngineOrchestrator.modelHasOutput(model)) return false;
|
|
8690
9002
|
const handle = this.handles.get(id);
|
|
8691
9003
|
if (!record.renderHandle) {
|
|
8692
9004
|
const paneId2 = this.routePane(id, model, record.options ?? {});
|
|
@@ -8696,7 +9008,7 @@ var Vela = (function (exports) {
|
|
|
8696
9008
|
record.renderHandle = this.renderer.mountIndicator(model);
|
|
8697
9009
|
record.pendingStructural = false;
|
|
8698
9010
|
this.announce(record, handle);
|
|
8699
|
-
return;
|
|
9011
|
+
return true;
|
|
8700
9012
|
}
|
|
8701
9013
|
let paneId = record.model?.paneId ?? "price";
|
|
8702
9014
|
const prevOwnScale = record.model?.ownScale === true;
|
|
@@ -8724,6 +9036,11 @@ var Vela = (function (exports) {
|
|
|
8724
9036
|
}
|
|
8725
9037
|
if (record.loading) this.setLoading(record, false);
|
|
8726
9038
|
this.announce(record, handle);
|
|
9039
|
+
return true;
|
|
9040
|
+
}
|
|
9041
|
+
/** True when the model carries ANY executed output — series, drawings, bar colors, or trades. */
|
|
9042
|
+
static modelHasOutput(model) {
|
|
9043
|
+
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
9044
|
}
|
|
8728
9045
|
routePane(id, model, options) {
|
|
8729
9046
|
if (options.pane === "new") return `pane-${id}`;
|
|
@@ -9426,6 +9743,7 @@ var Vela = (function (exports) {
|
|
|
9426
9743
|
"4h": 144e5,
|
|
9427
9744
|
"1d": 864e5,
|
|
9428
9745
|
"1w": 6048e5,
|
|
9746
|
+
"1M": 2592e6,
|
|
9429
9747
|
"1": 6e4,
|
|
9430
9748
|
"5": 3e5,
|
|
9431
9749
|
"15": 9e5,
|
|
@@ -9433,7 +9751,8 @@ var Vela = (function (exports) {
|
|
|
9433
9751
|
"60": 36e5,
|
|
9434
9752
|
"240": 144e5,
|
|
9435
9753
|
D: 864e5,
|
|
9436
|
-
W: 6048e5
|
|
9754
|
+
W: 6048e5,
|
|
9755
|
+
M: 2592e6
|
|
9437
9756
|
};
|
|
9438
9757
|
return map[timeframe] ?? 36e5;
|
|
9439
9758
|
}
|
|
@@ -26060,7 +26379,9 @@ void main() {
|
|
|
26060
26379
|
const lo = Math.min(from, to);
|
|
26061
26380
|
const hi = Math.max(from, to);
|
|
26062
26381
|
const visible = (a, b, extend) => {
|
|
26063
|
-
if (extend
|
|
26382
|
+
if (extend === "both") return true;
|
|
26383
|
+
if (extend === "left") return Math.max(a, b) >= lo;
|
|
26384
|
+
if (extend === "right") return Math.min(a, b) <= hi;
|
|
26064
26385
|
return Math.max(a, b) >= lo && Math.min(a, b) <= hi;
|
|
26065
26386
|
};
|
|
26066
26387
|
let min2 = Infinity;
|
|
@@ -28624,6 +28945,17 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
28624
28945
|
constructor() {
|
|
28625
28946
|
/** The current `paintAll` call's interaction state, visible to the per-type painters. */
|
|
28626
28947
|
this.targets = {};
|
|
28948
|
+
/** The chart's active series LOOK — style + resolved series colors — pushed by the
|
|
28949
|
+
* controller before each paint. The magnifier's inset mirrors both: candles/bars/line/
|
|
28950
|
+
* area restyle the paint (bar-transform styles like Heikin Ashi transform the fetched
|
|
28951
|
+
* bars; unknown/custom styles fall back to candles), and the colors default to the main
|
|
28952
|
+
* series' own so the inset reads as a finer copy of the chart. */
|
|
28953
|
+
this.seriesLook = {
|
|
28954
|
+
style: "candles",
|
|
28955
|
+
upColor: BULLISH,
|
|
28956
|
+
downColor: BEARISH,
|
|
28957
|
+
lineColor: BULLISH
|
|
28958
|
+
};
|
|
28627
28959
|
}
|
|
28628
28960
|
/** Paint every visible drawing, then selection handles for the targeted ones.
|
|
28629
28961
|
* Each drawing is clipped to its own pane's rect (and skipped entirely while that pane
|
|
@@ -28668,6 +29000,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
28668
29000
|
ctx.globalAlpha = GHOST_ALPHA;
|
|
28669
29001
|
if (ghost instanceof RegressionChannel || ghost instanceof FixedRangeVolumeProfile) {
|
|
28670
29002
|
this.paintTimeSpanGhost(ctx, ghost, proj);
|
|
29003
|
+
} else if (ghost instanceof Magnifier) {
|
|
29004
|
+
this.paintMagnifierGhost(ctx, ghost, proj, theme);
|
|
28671
29005
|
} else this.paintOne(ctx, ghost, proj, theme);
|
|
28672
29006
|
ctx.globalAlpha = 1;
|
|
28673
29007
|
}
|
|
@@ -28801,6 +29135,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
28801
29135
|
this.paintLabel(ctx, d, proj, theme);
|
|
28802
29136
|
return;
|
|
28803
29137
|
}
|
|
29138
|
+
if (d instanceof Magnifier) {
|
|
29139
|
+
this.paintMagnifier(ctx, d, proj, theme);
|
|
29140
|
+
return;
|
|
29141
|
+
}
|
|
28804
29142
|
if (d instanceof PatternDrawing) {
|
|
28805
29143
|
this.paintPattern(ctx, d, proj, theme);
|
|
28806
29144
|
return;
|
|
@@ -29293,6 +29631,216 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
29293
29631
|
ctx.textBaseline = "alphabetic";
|
|
29294
29632
|
}
|
|
29295
29633
|
}
|
|
29634
|
+
/** Placement preview for the magnifier: a dashed rectangle outline only — no backdrop and
|
|
29635
|
+
* no series read, so dragging the area open never kicks a fetch per cursor move. */
|
|
29636
|
+
paintMagnifierGhost(ctx, d, proj, theme) {
|
|
29637
|
+
const r = d.rect(proj);
|
|
29638
|
+
if (!r) return;
|
|
29639
|
+
ctx.save();
|
|
29640
|
+
ctx.strokeStyle = d.style.lineColor || contrastColor(theme.background);
|
|
29641
|
+
ctx.lineWidth = 1;
|
|
29642
|
+
ctx.setLineDash([4, 4]);
|
|
29643
|
+
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));
|
|
29644
|
+
ctx.restore();
|
|
29645
|
+
}
|
|
29646
|
+
/** Paint a magnifier: an opaque theme-background inset whose interior shows the chart's
|
|
29647
|
+
* market at a finer timeframe — candles at their true time/price positions, clipped to
|
|
29648
|
+
* the rectangle. Bars come through `Projector.seriesInRange` (cache read; `loading` and
|
|
29649
|
+
* `unavailable` states paint a short notice instead). The lower-timeframe candles shift
|
|
29650
|
+
* half a chart bar LEFT of their raw time pixel so each chart candle's visual cell —
|
|
29651
|
+
* centered on its open time — subdivides in place. */
|
|
29652
|
+
paintMagnifier(ctx, d, proj, theme) {
|
|
29653
|
+
const r = d.rect(proj);
|
|
29654
|
+
const a = d.anchors[0];
|
|
29655
|
+
const b = d.anchors[1];
|
|
29656
|
+
if (!r || !a || !b) return;
|
|
29657
|
+
const x0 = Math.min(r.x1, r.x2);
|
|
29658
|
+
const x1 = Math.max(r.x1, r.x2);
|
|
29659
|
+
const y0 = Math.min(r.y1, r.y2);
|
|
29660
|
+
const y1 = Math.max(r.y1, r.y2);
|
|
29661
|
+
const w = x1 - x0;
|
|
29662
|
+
const h = y1 - y0;
|
|
29663
|
+
ctx.save();
|
|
29664
|
+
ctx.globalAlpha = 1;
|
|
29665
|
+
ctx.fillStyle = theme.background;
|
|
29666
|
+
ctx.fillRect(x0, y0, w, h);
|
|
29667
|
+
ctx.restore();
|
|
29668
|
+
const from = Math.min(a.time, b.time);
|
|
29669
|
+
const to = Math.max(a.time, b.time);
|
|
29670
|
+
const chartBars = proj.barsBetween ? proj.barsBetween(from, to) : 0;
|
|
29671
|
+
const chartMs = chartBars > 0 ? (to - from) / chartBars : 0;
|
|
29672
|
+
const res = proj.seriesInRange && chartMs > 0 && w > 1 && h > 1 ? proj.seriesInRange(d.magnifier.timeframe, from, to + chartMs) : void 0;
|
|
29673
|
+
let seriesBars = res?.state === "ready" || res?.state === "loading" ? res.bars ?? [] : [];
|
|
29674
|
+
if (res && (res.state === "ready" || res.state === "loading") && seriesBars.length > 0) {
|
|
29675
|
+
const look = this.seriesLook;
|
|
29676
|
+
const transform = look.style !== "candles" ? barTransformFor(look.style) : null;
|
|
29677
|
+
if (transform) seriesBars = transform.full(seriesBars);
|
|
29678
|
+
const mode = look.style === "bars" ? "bars" : look.style === "line" || look.style === "baseline" ? "line" : look.style === "area" ? "area" : "candles";
|
|
29679
|
+
const halfPitch = (proj.xOf(from + chartMs) - proj.xOf(from)) / 2;
|
|
29680
|
+
ctx.save();
|
|
29681
|
+
ctx.beginPath();
|
|
29682
|
+
ctx.rect(x0, y0, w, h);
|
|
29683
|
+
ctx.clip();
|
|
29684
|
+
if (mode === "line" || mode === "area") {
|
|
29685
|
+
this.paintMagnifierLine(ctx, d, proj, seriesBars, res.barMs, halfPitch, y1, mode === "area", d.magnifier.upColor || look.lineColor);
|
|
29686
|
+
} else {
|
|
29687
|
+
this.paintMagnifierBars(ctx, d, proj, seriesBars, res.barMs, halfPitch, x0, x1, mode, d.magnifier.upColor || look.upColor, d.magnifier.downColor || look.downColor);
|
|
29688
|
+
}
|
|
29689
|
+
ctx.restore();
|
|
29690
|
+
} else if (res) {
|
|
29691
|
+
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";
|
|
29692
|
+
this.paintMagnifierNotice(ctx, notice, x0, y0, w, h, theme);
|
|
29693
|
+
}
|
|
29694
|
+
ctx.save();
|
|
29695
|
+
ctx.strokeStyle = d.style.lineColor || contrastColor(theme.background);
|
|
29696
|
+
ctx.lineWidth = d.style.lineWidth || 1;
|
|
29697
|
+
ctx.setLineDash(dashPattern(d.style.lineStyle, d.style.lineWidth || 1));
|
|
29698
|
+
ctx.strokeRect(x0, y0, w, h);
|
|
29699
|
+
ctx.restore();
|
|
29700
|
+
if (w > 44) {
|
|
29701
|
+
const label = magnifierTimeframeLabel(res?.state === "ready" || res?.state === "loading" ? res.timeframe : d.magnifier.timeframe);
|
|
29702
|
+
const chipH = 17;
|
|
29703
|
+
const gap = 4;
|
|
29704
|
+
const pane = proj.paneRect?.(d.paneId);
|
|
29705
|
+
const paneBottom = pane ? pane.top + pane.height : proj.height;
|
|
29706
|
+
const below = y1 + gap + chipH <= paneBottom;
|
|
29707
|
+
const chipY = below ? y1 + gap : y1 - gap - chipH;
|
|
29708
|
+
ctx.save();
|
|
29709
|
+
ctx.font = `10px ${theme.fontFamily}`;
|
|
29710
|
+
const tw = ctx.measureText(label).width;
|
|
29711
|
+
const caretW = 11;
|
|
29712
|
+
const chipW = tw + 12 + caretW;
|
|
29713
|
+
roundRect(ctx, x0, chipY, chipW, chipH, 3);
|
|
29714
|
+
ctx.fillStyle = theme.background;
|
|
29715
|
+
ctx.fill();
|
|
29716
|
+
ctx.strokeStyle = withAlpha(theme.textColor, 0.28);
|
|
29717
|
+
ctx.lineWidth = 1;
|
|
29718
|
+
ctx.setLineDash([]);
|
|
29719
|
+
ctx.stroke();
|
|
29720
|
+
ctx.fillStyle = theme.textColor;
|
|
29721
|
+
ctx.textAlign = "left";
|
|
29722
|
+
ctx.textBaseline = "middle";
|
|
29723
|
+
ctx.fillText(label, x0 + 6, chipY + chipH / 2 + 0.5);
|
|
29724
|
+
const cxr = x0 + 6 + tw + 5;
|
|
29725
|
+
const cyr = chipY + chipH / 2;
|
|
29726
|
+
ctx.strokeStyle = withAlpha(theme.textColor, 0.7);
|
|
29727
|
+
ctx.lineWidth = 1.2;
|
|
29728
|
+
ctx.beginPath();
|
|
29729
|
+
ctx.moveTo(cxr, cyr - 1.5);
|
|
29730
|
+
ctx.lineTo(cxr + 2.5, cyr + 1.5);
|
|
29731
|
+
ctx.lineTo(cxr + 5, cyr - 1.5);
|
|
29732
|
+
ctx.stroke();
|
|
29733
|
+
ctx.restore();
|
|
29734
|
+
d.chipRect = { x: x0, y: chipY, w: chipW, h: chipH };
|
|
29735
|
+
} else {
|
|
29736
|
+
d.chipRect = null;
|
|
29737
|
+
}
|
|
29738
|
+
}
|
|
29739
|
+
/** The magnifier's candle/bar loop: each bar's cell spans its open→close time (shifted left
|
|
29740
|
+
* by half a chart bar). Candles: wick always, body once the cell is wide enough to carry
|
|
29741
|
+
* one. OHLC bars: the high–low spine with open/close ticks once the cell has the room. */
|
|
29742
|
+
paintMagnifierBars(ctx, d, proj, bars, barMs, halfPitch, x0, x1, mode, upColor, downColor) {
|
|
29743
|
+
ctx.setLineDash([]);
|
|
29744
|
+
ctx.lineWidth = 1;
|
|
29745
|
+
for (const bar of bars) {
|
|
29746
|
+
const cx0 = proj.xOf(bar.time) - halfPitch;
|
|
29747
|
+
const cx1 = proj.xOf(bar.time + barMs) - halfPitch;
|
|
29748
|
+
if (cx1 < x0 || cx0 > x1) continue;
|
|
29749
|
+
const yHigh = proj.yOf(bar.high, d.paneId);
|
|
29750
|
+
const yLow = proj.yOf(bar.low, d.paneId);
|
|
29751
|
+
const yOpen = proj.yOf(bar.open, d.paneId);
|
|
29752
|
+
const yClose = proj.yOf(bar.close, d.paneId);
|
|
29753
|
+
if (yHigh == null || yLow == null || yOpen == null || yClose == null) continue;
|
|
29754
|
+
const color = bar.close >= bar.open ? upColor : downColor;
|
|
29755
|
+
const cellW = cx1 - cx0;
|
|
29756
|
+
const cx = (cx0 + cx1) / 2;
|
|
29757
|
+
ctx.strokeStyle = color;
|
|
29758
|
+
ctx.beginPath();
|
|
29759
|
+
ctx.moveTo(cx, yHigh);
|
|
29760
|
+
ctx.lineTo(cx, yLow);
|
|
29761
|
+
ctx.stroke();
|
|
29762
|
+
if (cellW < 3) continue;
|
|
29763
|
+
if (mode === "bars") {
|
|
29764
|
+
const tick = Math.max(1, cellW * 0.35);
|
|
29765
|
+
ctx.beginPath();
|
|
29766
|
+
ctx.moveTo(cx - tick, yOpen);
|
|
29767
|
+
ctx.lineTo(cx, yOpen);
|
|
29768
|
+
ctx.moveTo(cx, yClose);
|
|
29769
|
+
ctx.lineTo(cx + tick, yClose);
|
|
29770
|
+
ctx.stroke();
|
|
29771
|
+
} else {
|
|
29772
|
+
const bw = Math.max(1, cellW * 0.7);
|
|
29773
|
+
ctx.fillStyle = color;
|
|
29774
|
+
ctx.fillRect(cx - bw / 2, Math.min(yOpen, yClose), bw, Math.max(1, Math.abs(yClose - yOpen)));
|
|
29775
|
+
}
|
|
29776
|
+
}
|
|
29777
|
+
}
|
|
29778
|
+
/** The magnifier's line/area rendering: a close polyline through each cell's center (same
|
|
29779
|
+
* half-chart-bar shift as the candles), with an optional translucent fill down to the
|
|
29780
|
+
* rectangle's bottom edge for the area style. Colored like the chart's own line series. */
|
|
29781
|
+
paintMagnifierLine(ctx, d, proj, bars, barMs, halfPitch, yBottom, area, color) {
|
|
29782
|
+
const pts = [];
|
|
29783
|
+
for (const bar of bars) {
|
|
29784
|
+
const y = proj.yOf(bar.close, d.paneId);
|
|
29785
|
+
if (y == null) continue;
|
|
29786
|
+
pts.push([proj.xOf(bar.time + barMs / 2) - halfPitch, y]);
|
|
29787
|
+
}
|
|
29788
|
+
if (pts.length < 2) return;
|
|
29789
|
+
if (area) {
|
|
29790
|
+
ctx.beginPath();
|
|
29791
|
+
ctx.moveTo(pts[0][0], yBottom);
|
|
29792
|
+
for (const [px, py] of pts) ctx.lineTo(px, py);
|
|
29793
|
+
ctx.lineTo(pts[pts.length - 1][0], yBottom);
|
|
29794
|
+
ctx.closePath();
|
|
29795
|
+
ctx.fillStyle = withAlpha(color, 0.15);
|
|
29796
|
+
ctx.fill();
|
|
29797
|
+
}
|
|
29798
|
+
ctx.setLineDash([]);
|
|
29799
|
+
ctx.lineWidth = 1.5;
|
|
29800
|
+
ctx.strokeStyle = color;
|
|
29801
|
+
ctx.beginPath();
|
|
29802
|
+
ctx.moveTo(pts[0][0], pts[0][1]);
|
|
29803
|
+
for (let i = 1; i < pts.length; i += 1) ctx.lineTo(pts[i][0], pts[i][1]);
|
|
29804
|
+
ctx.stroke();
|
|
29805
|
+
}
|
|
29806
|
+
/** Centered muted notice inside the magnifier rect (loading / unavailable states). */
|
|
29807
|
+
paintMagnifierNotice(ctx, text, x0, y0, w, h, theme) {
|
|
29808
|
+
if (w < 60 || h < 20) return;
|
|
29809
|
+
ctx.save();
|
|
29810
|
+
ctx.beginPath();
|
|
29811
|
+
ctx.rect(x0, y0, w, h);
|
|
29812
|
+
ctx.clip();
|
|
29813
|
+
ctx.font = `11px ${theme.fontFamily}`;
|
|
29814
|
+
ctx.fillStyle = withAlpha(theme.textColor, 0.55);
|
|
29815
|
+
ctx.textAlign = "center";
|
|
29816
|
+
ctx.textBaseline = "middle";
|
|
29817
|
+
ctx.fillText(text, x0 + w / 2, y0 + h / 2);
|
|
29818
|
+
ctx.restore();
|
|
29819
|
+
}
|
|
29820
|
+
/** A bottom-center pill prompting the armed tool's placement gesture (e.g. the magnifier's
|
|
29821
|
+
* "drag an area"). Painted by the drawings layer while the tool is armed and no placement
|
|
29822
|
+
* is in progress; chart-background fill so it reads as chrome over any content. */
|
|
29823
|
+
paintPlacementHint(ctx, text, theme, width, height) {
|
|
29824
|
+
ctx.save();
|
|
29825
|
+
ctx.font = `11px ${theme.fontFamily}`;
|
|
29826
|
+
const tw = ctx.measureText(text).width;
|
|
29827
|
+
const pillW = tw + 24;
|
|
29828
|
+
const pillH = 24;
|
|
29829
|
+
const x = (width - pillW) / 2;
|
|
29830
|
+
const y = height - pillH - 14;
|
|
29831
|
+
roundRect(ctx, x, y, pillW, pillH, pillH / 2);
|
|
29832
|
+
ctx.fillStyle = theme.background;
|
|
29833
|
+
ctx.fill();
|
|
29834
|
+
ctx.strokeStyle = withAlpha(theme.textColor, 0.28);
|
|
29835
|
+
ctx.lineWidth = 1;
|
|
29836
|
+
ctx.setLineDash([]);
|
|
29837
|
+
ctx.stroke();
|
|
29838
|
+
ctx.fillStyle = theme.textColor;
|
|
29839
|
+
ctx.textAlign = "center";
|
|
29840
|
+
ctx.textBaseline = "middle";
|
|
29841
|
+
ctx.fillText(text, width / 2, y + pillH / 2 + 0.5);
|
|
29842
|
+
ctx.restore();
|
|
29843
|
+
}
|
|
29296
29844
|
/** Paint a fixed-range volume profile: horizontal histogram rows (up/down split) anchored to
|
|
29297
29845
|
* the left or right of the time span, optional VAH / VAL / POC levels across the range, and
|
|
29298
29846
|
* optional developing POC / VA polylines. Recomputes from the two anchors on every paint. */
|
|
@@ -30708,8 +31256,9 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
30708
31256
|
if (!existing) document.head.appendChild(s);
|
|
30709
31257
|
}
|
|
30710
31258
|
var DrawingSettingsPopup = class {
|
|
30711
|
-
constructor(host, theme) {
|
|
31259
|
+
constructor(host, theme, chartBarMs = () => 0) {
|
|
30712
31260
|
this.host = host;
|
|
31261
|
+
this.chartBarMs = chartBarMs;
|
|
30713
31262
|
this.el = null;
|
|
30714
31263
|
this.tipEl = null;
|
|
30715
31264
|
// floating hover-label (above/below the toolbar)
|
|
@@ -30734,6 +31283,14 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
30734
31283
|
this.theme = theme;
|
|
30735
31284
|
this.settingsDialog = new DrawingSettingsDialog(host, theme);
|
|
30736
31285
|
}
|
|
31286
|
+
/** The magnifier timeframe choices strictly below the chart's own bar duration
|
|
31287
|
+
* (`auto` rides along while at least one concrete lower step exists). */
|
|
31288
|
+
lowerTimeframeOptions() {
|
|
31289
|
+
const chartMs = this.chartBarMs();
|
|
31290
|
+
if (!(chartMs > 0)) return [...MAGNIFIER_TIMEFRAME_OPTIONS];
|
|
31291
|
+
const lower = MAGNIFIER_TIMEFRAME_OPTIONS.filter((o) => o.ms > 0 && o.ms < chartMs);
|
|
31292
|
+
return lower.length > 0 ? [MAGNIFIER_TIMEFRAME_OPTIONS[0], ...lower] : [];
|
|
31293
|
+
}
|
|
30737
31294
|
setTheme(theme) {
|
|
30738
31295
|
this.theme = theme;
|
|
30739
31296
|
this.settingsDialog.setTheme(theme);
|
|
@@ -30776,7 +31333,20 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
30776
31333
|
const sz = drawing.size ?? "normal";
|
|
30777
31334
|
bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label: sizeLabel }));
|
|
30778
31335
|
}
|
|
30779
|
-
if (paths.has("
|
|
31336
|
+
if (paths.has("magnifier.timeframe") && drawing instanceof Magnifier) {
|
|
31337
|
+
const options = this.lowerTimeframeOptions();
|
|
31338
|
+
if (options.length > 0) {
|
|
31339
|
+
bar.appendChild(
|
|
31340
|
+
this.dropdown("Lower timeframe", options.map((o) => o.value), drawing.magnifier.timeframe, () => "", (v) => actions.patch({ "magnifier.timeframe": v }), {
|
|
31341
|
+
label: (v) => magnifierTimeframeLabel(String(v)),
|
|
31342
|
+
labelInTrigger: true
|
|
31343
|
+
})
|
|
31344
|
+
);
|
|
31345
|
+
}
|
|
31346
|
+
bar.appendChild(this.colorButton("Up candles", BUCKET_ICON, drawing.magnifier.upColor || t.upColor, (v) => actions.patch({ "magnifier.upColor": v })));
|
|
31347
|
+
bar.appendChild(this.colorButton("Down candles", BUCKET_ICON, drawing.magnifier.downColor || t.downColor, (v) => actions.patch({ "magnifier.downColor": v })));
|
|
31348
|
+
}
|
|
31349
|
+
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
31350
|
if (paths.has("style.lineWidth")) {
|
|
30781
31351
|
const wf = schema.fields.find((f) => f.path === "style.lineWidth");
|
|
30782
31352
|
if (wf?.kind === "number" && (wf.min ?? 1) > 1) {
|
|
@@ -31098,6 +31668,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
31098
31668
|
this.colorPop = null;
|
|
31099
31669
|
this.colorOwner = null;
|
|
31100
31670
|
}
|
|
31671
|
+
opts.onClose?.();
|
|
31101
31672
|
}
|
|
31102
31673
|
});
|
|
31103
31674
|
const el = pop.el;
|
|
@@ -31116,6 +31687,49 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
31116
31687
|
pop.show();
|
|
31117
31688
|
return pop;
|
|
31118
31689
|
}
|
|
31690
|
+
/**
|
|
31691
|
+
* A standalone timeframe menu for the magnifier's ON-CHART chip. The chip lives on
|
|
31692
|
+
* canvas, so a transient invisible anchor is dropped at its pixel rect for the popover
|
|
31693
|
+
* to position against, and removed again when the menu closes. Independent of the
|
|
31694
|
+
* quick toolbar — the chip works without selecting the drawing first.
|
|
31695
|
+
*/
|
|
31696
|
+
openMagnifierTimeframeMenu(rect, current, onPick) {
|
|
31697
|
+
ensureStyles3();
|
|
31698
|
+
closeOpenPopovers();
|
|
31699
|
+
const options = this.lowerTimeframeOptions();
|
|
31700
|
+
const anchor = document.createElement("div");
|
|
31701
|
+
anchor.style.cssText = `position:absolute;left:${rect.x}px;top:${rect.y}px;width:${rect.w}px;height:${rect.h}px;pointer-events:none;`;
|
|
31702
|
+
this.host.appendChild(anchor);
|
|
31703
|
+
this.menuPop = this.hostFloat(anchor, {
|
|
31704
|
+
zIndex: 26,
|
|
31705
|
+
padding: "4px",
|
|
31706
|
+
onClose: () => anchor.remove(),
|
|
31707
|
+
fill: (menu, pop) => {
|
|
31708
|
+
if (options.length === 0) {
|
|
31709
|
+
const note = document.createElement("div");
|
|
31710
|
+
note.style.cssText = "padding:6px 10px;opacity:0.65;white-space:nowrap;";
|
|
31711
|
+
note.textContent = "No lower timeframe available";
|
|
31712
|
+
menu.appendChild(note);
|
|
31713
|
+
return;
|
|
31714
|
+
}
|
|
31715
|
+
for (const o of options) {
|
|
31716
|
+
const item = document.createElement("button");
|
|
31717
|
+
item.type = "button";
|
|
31718
|
+
item.className = "vela-dpop-item";
|
|
31719
|
+
item.dataset.active = o.value === current ? "1" : "0";
|
|
31720
|
+
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;";
|
|
31721
|
+
item.textContent = o.label;
|
|
31722
|
+
item.addEventListener("click", (e) => {
|
|
31723
|
+
e.stopPropagation();
|
|
31724
|
+
pop.hide();
|
|
31725
|
+
onPick(o.value);
|
|
31726
|
+
});
|
|
31727
|
+
menu.appendChild(item);
|
|
31728
|
+
}
|
|
31729
|
+
}
|
|
31730
|
+
});
|
|
31731
|
+
this.menuOwner = anchor;
|
|
31732
|
+
}
|
|
31119
31733
|
/** A floating list of one-shot actions (icon + label rows) opened by the kebab. */
|
|
31120
31734
|
openActionMenu(anchor, rows) {
|
|
31121
31735
|
this.menuPop = this.hostFloat(anchor, {
|
|
@@ -31226,10 +31840,13 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
31226
31840
|
let cur = current;
|
|
31227
31841
|
const paint = (v) => {
|
|
31228
31842
|
b.replaceChildren();
|
|
31229
|
-
const
|
|
31230
|
-
|
|
31231
|
-
|
|
31232
|
-
|
|
31843
|
+
const glyph = render(v);
|
|
31844
|
+
if (glyph) {
|
|
31845
|
+
const ic = document.createElement("span");
|
|
31846
|
+
ic.style.cssText = "display:flex;";
|
|
31847
|
+
ic.innerHTML = sized(glyph);
|
|
31848
|
+
b.appendChild(ic);
|
|
31849
|
+
}
|
|
31233
31850
|
if (opts.label && opts.labelInTrigger) {
|
|
31234
31851
|
const tx = document.createElement("span");
|
|
31235
31852
|
tx.textContent = opts.label(v);
|
|
@@ -31271,10 +31888,13 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
31271
31888
|
item.className = "vela-dpop-item";
|
|
31272
31889
|
item.dataset.active = active ? "1" : "0";
|
|
31273
31890
|
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
|
-
|
|
31891
|
+
const glyph = render(v);
|
|
31892
|
+
if (glyph) {
|
|
31893
|
+
const ic = document.createElement("span");
|
|
31894
|
+
ic.style.cssText = "display:flex;flex:none;width:22px;justify-content:center;";
|
|
31895
|
+
ic.innerHTML = sized(glyph, 18);
|
|
31896
|
+
item.appendChild(ic);
|
|
31897
|
+
}
|
|
31278
31898
|
if (label) {
|
|
31279
31899
|
const tx = document.createElement("span");
|
|
31280
31900
|
tx.textContent = label(v);
|
|
@@ -32258,6 +32878,9 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
32258
32878
|
this.intentCb = null;
|
|
32259
32879
|
/** Another chart's in-progress placement, mirrored here as a ghost (drawings sync). */
|
|
32260
32880
|
this.externalGhost = null;
|
|
32881
|
+
/** Core-pushed series gateway (finer-timeframe bars for data-driven drawings). */
|
|
32882
|
+
this.seriesGw = null;
|
|
32883
|
+
this.seriesGwUnsub = null;
|
|
32261
32884
|
/** Last draft fingerprint reported upstream — gates the per-render emission to actual changes. */
|
|
32262
32885
|
this.lastDraftKey = null;
|
|
32263
32886
|
this.measure = new MeasureOverlay();
|
|
@@ -32285,7 +32908,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
32285
32908
|
this.textEditor = null;
|
|
32286
32909
|
this.painter = new DrawingPainter();
|
|
32287
32910
|
this.ctx = canvas.getContext("2d");
|
|
32288
|
-
this.popup = new DrawingSettingsPopup(overlayHost, deps.theme());
|
|
32911
|
+
this.popup = new DrawingSettingsPopup(overlayHost, deps.theme(), () => deps.chartBarMs());
|
|
32289
32912
|
this.toolbar = new DrawingToolbar(
|
|
32290
32913
|
toolbarHost,
|
|
32291
32914
|
deps.theme(),
|
|
@@ -32343,6 +32966,22 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
32343
32966
|
const shown = this.toolbarVisible && !this.mobileLayout;
|
|
32344
32967
|
this.deps.setToolbarGutter(shown ? this.toolbarCollapsed ? TOOLBAR_COLLAPSED_WIDTH : TOOLBAR_WIDTH : 0);
|
|
32345
32968
|
}
|
|
32969
|
+
/** Core push: the series gateway data-driven drawings read finer-timeframe bars
|
|
32970
|
+
* through (surfaced to them as `Projector.seriesInRange`). A landed background
|
|
32971
|
+
* fetch repaints both this layer and the interleave slices under the series. */
|
|
32972
|
+
setSeriesGateway(gateway) {
|
|
32973
|
+
this.seriesGwUnsub?.();
|
|
32974
|
+
this.seriesGw = gateway;
|
|
32975
|
+
this.seriesGwUnsub = gateway.onUpdate(() => {
|
|
32976
|
+
this.invalidateSlices();
|
|
32977
|
+
this.render();
|
|
32978
|
+
this.deps.requestDataPaint();
|
|
32979
|
+
});
|
|
32980
|
+
}
|
|
32981
|
+
/** The pushed series gateway, or null before the core provides one. */
|
|
32982
|
+
get seriesGateway() {
|
|
32983
|
+
return this.seriesGw;
|
|
32984
|
+
}
|
|
32346
32985
|
/** Core push: mirror (or clear) another chart's in-progress placement as a ghost. */
|
|
32347
32986
|
setExternalGhost(doc) {
|
|
32348
32987
|
this.externalGhost = doc ? deserializeDrawing(doc) : null;
|
|
@@ -32460,8 +33099,34 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
32460
33099
|
/** Should the drawing layer win this press (vs pan)? */
|
|
32461
33100
|
claim(x, y) {
|
|
32462
33101
|
if (this.measureMode || this.eraserMode) return true;
|
|
33102
|
+
if (this.magnifierChipAt(x, y)) return true;
|
|
32463
33103
|
return this.interaction.claim(x, y);
|
|
32464
33104
|
}
|
|
33105
|
+
/** The topmost visible (unlocked) magnifier whose timeframe chip contains (x, y) —
|
|
33106
|
+
* the chip's rect is what the painter measured last frame. */
|
|
33107
|
+
magnifierChipAt(x, y) {
|
|
33108
|
+
for (let i = this.drawings.length - 1; i >= 0; i -= 1) {
|
|
33109
|
+
const d = this.drawings[i];
|
|
33110
|
+
if (!(d instanceof Magnifier) || !d.visible || d.locked) continue;
|
|
33111
|
+
const r = d.chipRect;
|
|
33112
|
+
if (r && x >= r.x && x <= r.x + r.w && y >= r.y && y <= r.y + r.h) return d;
|
|
33113
|
+
}
|
|
33114
|
+
return null;
|
|
33115
|
+
}
|
|
33116
|
+
/** Open the on-chart chip's timeframe menu; the pick patches the drawing like a
|
|
33117
|
+
* settings-popup edit (same intent, same undo step). */
|
|
33118
|
+
openMagnifierChipMenu(drawing) {
|
|
33119
|
+
const rect = drawing.chipRect;
|
|
33120
|
+
if (!rect) return;
|
|
33121
|
+
const id = drawing.id;
|
|
33122
|
+
this.popup.openMagnifierTimeframeMenu(rect, drawing.magnifier.timeframe, (value) => {
|
|
33123
|
+
const d = this.drawings.find((x) => x.id === id);
|
|
33124
|
+
if (!(d instanceof Magnifier)) return;
|
|
33125
|
+
d.applySettings({ "magnifier.timeframe": value });
|
|
33126
|
+
this.render();
|
|
33127
|
+
this.emit({ kind: "edit", doc: d.serialize() });
|
|
33128
|
+
});
|
|
33129
|
+
}
|
|
32465
33130
|
/** Delete the (unlocked) drawing under the cursor. True when one was removed.
|
|
32466
33131
|
* Shared by the eraser (click + drag) and the middle-click shortcut. */
|
|
32467
33132
|
deleteAt(x, y) {
|
|
@@ -32507,6 +33172,13 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
32507
33172
|
this.render();
|
|
32508
33173
|
return;
|
|
32509
33174
|
}
|
|
33175
|
+
if (this.activeTool == null) {
|
|
33176
|
+
const chipOwner = this.magnifierChipAt(x, y);
|
|
33177
|
+
if (chipOwner) {
|
|
33178
|
+
this.openMagnifierChipMenu(chipOwner);
|
|
33179
|
+
return;
|
|
33180
|
+
}
|
|
33181
|
+
}
|
|
32510
33182
|
this.interaction.down(x, y, snap, shift3);
|
|
32511
33183
|
}
|
|
32512
33184
|
pointerMove(x, y, snap = "off", shift3 = false) {
|
|
@@ -32725,6 +33397,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
32725
33397
|
/** Cursor hint while hovering — `'pointer'` over a drawing/handle, else null. */
|
|
32726
33398
|
cursorAt(x, y) {
|
|
32727
33399
|
if (this.eraserMode) return "pointer";
|
|
33400
|
+
if (this.activeTool == null && this.magnifierChipAt(x, y)) return "pointer";
|
|
32728
33401
|
return this.interaction.cursorAt(x, y);
|
|
32729
33402
|
}
|
|
32730
33403
|
/** Right-click: an explicit escape back to the pointer. Cancels an in-progress
|
|
@@ -32903,6 +33576,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
32903
33576
|
if (!sctx) continue;
|
|
32904
33577
|
sctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
32905
33578
|
sctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
|
|
33579
|
+
this.painter.seriesLook = this.deps.seriesLook();
|
|
32906
33580
|
this.painter.paintAll(sctx, drawings, proj, theme, EMPTY_TARGETS);
|
|
32907
33581
|
const slices = out.get(paneId) ?? [];
|
|
32908
33582
|
slices.push({ beforeZ, canvas });
|
|
@@ -32930,6 +33604,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
32930
33604
|
dragged: this.interaction.activeDragId(),
|
|
32931
33605
|
mutedLabel: edited instanceof TextLabel ? edited.id : null
|
|
32932
33606
|
};
|
|
33607
|
+
this.painter.seriesLook = this.deps.seriesLook();
|
|
32933
33608
|
this.painter.paintAll(ctx, this.drawings.filter((d) => !this.isInterleaved(d)), proj, this.deps.theme(), targets);
|
|
32934
33609
|
this.painter.paintHighlights(ctx, this.drawings.filter((d) => this.isInterleaved(d)), proj, handleIdsFor(targets));
|
|
32935
33610
|
this.layoutTextEditor();
|
|
@@ -32937,6 +33612,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
32937
33612
|
if (ghost) this.painter.paintGhost(ctx, ghost, proj, this.deps.theme());
|
|
32938
33613
|
if (this.externalGhost) this.painter.paintGhost(ctx, this.externalGhost, proj, this.deps.theme());
|
|
32939
33614
|
this.emitDraft(ghost);
|
|
33615
|
+
if (this.activeTool && !ghost) {
|
|
33616
|
+
const hint = getDrawingType(this.activeTool)?.placementHint;
|
|
33617
|
+
if (hint) this.painter.paintPlacementHint(ctx, hint, this.deps.theme(), proj.width, proj.height);
|
|
33618
|
+
}
|
|
32940
33619
|
const markers = this.interaction.placingMarkers(proj);
|
|
32941
33620
|
if (markers) this.painter.paintHandles(ctx, markers);
|
|
32942
33621
|
const m = this.interaction.snapMarker();
|
|
@@ -32948,6 +33627,9 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
32948
33627
|
this.closeTextEditor();
|
|
32949
33628
|
this.popup.destroy();
|
|
32950
33629
|
this.toolbar.destroy();
|
|
33630
|
+
this.seriesGwUnsub?.();
|
|
33631
|
+
this.seriesGwUnsub = null;
|
|
33632
|
+
this.seriesGw = null;
|
|
32951
33633
|
this.intentCb = null;
|
|
32952
33634
|
this.drawings = [];
|
|
32953
33635
|
}
|
|
@@ -33345,7 +34027,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
33345
34027
|
}
|
|
33346
34028
|
|
|
33347
34029
|
// src/renderers/native/drawings/Projector.ts
|
|
33348
|
-
function createProjector(coords, paneOf, paneIdAtY, barsInRange) {
|
|
34030
|
+
function createProjector(coords, paneOf, paneIdAtY, barsInRange, seriesInRange) {
|
|
33349
34031
|
return {
|
|
33350
34032
|
xOf: (time) => coords.timeToX(time),
|
|
33351
34033
|
yOf: (price, paneId) => {
|
|
@@ -33366,6 +34048,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
33366
34048
|
},
|
|
33367
34049
|
barsBetween: (t1, t2) => Math.abs(coords.timeToLogical(t2) - coords.timeToLogical(t1)),
|
|
33368
34050
|
barsInRange: barsInRange ? (from, to) => barsInRange(from, to) : void 0,
|
|
34051
|
+
seriesInRange,
|
|
33369
34052
|
width: coords.width,
|
|
33370
34053
|
height: coords.height
|
|
33371
34054
|
};
|
|
@@ -35369,6 +36052,23 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
35369
36052
|
seriesBoundaries: (paneId) => this.scene.seriesBoundaries(paneId),
|
|
35370
36053
|
priceZ: (paneId) => paneId === PRICE_PANE_ID ? this.scene.candleZ : null,
|
|
35371
36054
|
requestDataPaint: () => this.scheduler.invalidate(3 /* Light */),
|
|
36055
|
+
// The look the price series ACTUALLY paints with: candle colors resolved through
|
|
36056
|
+
// the per-style override, line/area colors through their configured styles — so
|
|
36057
|
+
// series-mirroring content (the magnifier inset) matches the chart exactly.
|
|
36058
|
+
seriesLook: () => {
|
|
36059
|
+
const st = this.scene.style;
|
|
36060
|
+
const paint = effectiveCandlePaint(st.candle, this.scene.candleOverride, this.theme.upColor, this.theme.downColor);
|
|
36061
|
+
const barsUp = st.bars.upColor ?? this.theme.upColor;
|
|
36062
|
+
const barsDown = st.bars.downColor ?? this.theme.downColor;
|
|
36063
|
+
const style = this.scene.priceStyle;
|
|
36064
|
+
return {
|
|
36065
|
+
style,
|
|
36066
|
+
upColor: style === "bars" ? barsUp : paint.up,
|
|
36067
|
+
downColor: style === "bars" ? barsDown : paint.down,
|
|
36068
|
+
lineColor: style === "area" ? st.area.lineColor ?? this.theme.upColor : st.line.color ?? this.theme.upColor
|
|
36069
|
+
};
|
|
36070
|
+
},
|
|
36071
|
+
chartBarMs: () => this.coords.barInterval,
|
|
35372
36072
|
snap: (pt2, paneId, mode, cursorPx) => this.snapToCandle(pt2, paneId, mode, cursorPx),
|
|
35373
36073
|
setSnapMode: (mode) => this.setSnapMode(mode),
|
|
35374
36074
|
setToolbarGutter: (px) => this.setToolbarGutter(px)
|
|
@@ -36762,7 +37462,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
36762
37462
|
return p ? { scale: p.scale, bounds: p.bounds, collapsed: p.collapsed } : null;
|
|
36763
37463
|
},
|
|
36764
37464
|
(y) => this.paneNodeAtY(y)?.id ?? null,
|
|
36765
|
-
(from, to) => this.barsInTimeRange(from, to)
|
|
37465
|
+
(from, to) => this.barsInTimeRange(from, to),
|
|
37466
|
+
this.userDrawings?.seriesGateway ? (tf, from, to) => this.userDrawings.seriesGateway.seriesInRange(tf, from, to) : void 0
|
|
36766
37467
|
);
|
|
36767
37468
|
}
|
|
36768
37469
|
/** OHLC bars whose open-time falls within `[from, to]` (inclusive) — the data a regression
|