@luxalgo/vela 0.6.10 → 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-CWmp31dA.d.ts → DataProvider-DhzstpQb.d.ts} +1 -1
- package/dist/{DataProvider-l_eLMly_.d.cts → DataProvider-DlMtrwqM.d.cts} +1 -1
- package/dist/{chunk-4KZVZ7QQ.js → chunk-F4M24ANM.js} +130 -1
- package/dist/{chunk-NMTQXNT4.js → chunk-IZV3CW5N.js} +245 -43
- package/dist/{chunk-FFOP37FQ.js → chunk-KV7FDWSL.js} +748 -70
- package/dist/{contributions-BCz6Dr6a.d.ts → contributions-37nni40G.d.ts} +20 -4
- package/dist/{contributions-D9vTzm5p.d.cts → contributions-lPojhTxI.d.cts} +20 -4
- package/dist/index.cjs +866 -58
- package/dist/index.d.cts +12 -6
- package/dist/index.d.ts +12 -6
- package/dist/index.js +3 -3
- package/dist/{options-DSqHsQyN.d.cts → options-CYS5Wmlx.d.cts} +78 -2
- package/dist/{options-DSqHsQyN.d.ts → options-CYS5Wmlx.d.ts} +78 -2
- package/dist/{plugin-Bt8hLR8Z.d.cts → plugin-Cwikpz1m.d.cts} +10 -3
- package/dist/{plugin-CH7pfMrE.d.ts → plugin-DHyaoMjW.d.ts} +10 -3
- package/dist/plugin.cjs +119 -0
- package/dist/plugin.d.cts +4 -4
- package/dist/plugin.d.ts +4 -4
- package/dist/plugin.js +2 -2
- 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-5K7y4Ya2.d.ts → statusline-model-CiJm-riV.d.cts} +17 -87
- package/dist/{statusline-TYLQmoeh.d.cts → statusline-model-D-q_OOx9.d.ts} +17 -87
- package/dist/ui.d.cts +1 -1
- package/dist/ui.d.ts +1 -1
- package/dist/vela.global.js +866 -58
- package/dist/vela.global.min.js +52 -52
- package/dist/widget.cjs +1109 -100
- package/dist/widget.d.cts +118 -7
- package/dist/widget.d.ts +118 -7
- package/dist/widget.js +4 -4
- package/dist/workspace.cjs +1109 -100
- package/dist/workspace.d.cts +20 -11
- package/dist/workspace.d.ts +20 -11
- package/dist/workspace.js +3 -3
- package/package.json +1 -1
- /package/dist/{chunk-AOGZBKUE.js → chunk-H26BEHF4.js} +0 -0
package/dist/vela.global.js
CHANGED
|
@@ -130,6 +130,12 @@ var Vela = (function (exports) {
|
|
|
130
130
|
get visible() {
|
|
131
131
|
return this.visibleState;
|
|
132
132
|
}
|
|
133
|
+
inputValues() {
|
|
134
|
+
return this.controller.inputValuesOf(this.id);
|
|
135
|
+
}
|
|
136
|
+
propValues() {
|
|
137
|
+
return this.controller.propValuesOf(this.id);
|
|
138
|
+
}
|
|
133
139
|
setInput(key, value) {
|
|
134
140
|
this.controller.applyInputs(this.id, { [key]: value });
|
|
135
141
|
}
|
|
@@ -5394,6 +5400,111 @@ var Vela = (function (exports) {
|
|
|
5394
5400
|
return Math.round(n * 1e8) / 1e8;
|
|
5395
5401
|
}
|
|
5396
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
|
+
|
|
5397
5508
|
// src/core/drawings/registry.ts
|
|
5398
5509
|
var REGISTRY2 = /* @__PURE__ */ new Map();
|
|
5399
5510
|
function registerDrawingType(meta) {
|
|
@@ -6053,6 +6164,22 @@ var Vela = (function (exports) {
|
|
|
6053
6164
|
defaultStyle: { lineColor: DEFAULT_DRAWING_COLOR, lineWidth: 1, lineStyle: "solid" },
|
|
6054
6165
|
create: (init) => new PositionTool(init)
|
|
6055
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
|
+
});
|
|
6056
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"/>');
|
|
6057
6184
|
registerDrawingType({
|
|
6058
6185
|
type: "anchoredvwap",
|
|
@@ -6092,7 +6219,7 @@ var Vela = (function (exports) {
|
|
|
6092
6219
|
var PATTERN_TYPES = ["xabcd", "abcd", "headshoulders"];
|
|
6093
6220
|
var ELLIOTT_TYPES = ["elliottimpulse", "elliottcorrection"];
|
|
6094
6221
|
var HARMONIC_TYPES = ["gartley", "bat", "butterfly", "crab", "shark", "cypher"];
|
|
6095
|
-
var MEASUREMENT_TYPES = ["position", "datepricerange"];
|
|
6222
|
+
var MEASUREMENT_TYPES = ["position", "datepricerange", "magnifier"];
|
|
6096
6223
|
var VOLUME_TYPES = ["anchoredvwap", "fixedrangevp"];
|
|
6097
6224
|
var BRUSH_TYPES = ["freehand", "highlighter"];
|
|
6098
6225
|
var ARROW_TYPES = ["arrow", "arrowmarkup", "arrowmarkdown"];
|
|
@@ -6456,7 +6583,7 @@ var Vela = (function (exports) {
|
|
|
6456
6583
|
|
|
6457
6584
|
// src/core/drawings/DrawingController.ts
|
|
6458
6585
|
var DrawingController = class {
|
|
6459
|
-
constructor(renderer, events, option) {
|
|
6586
|
+
constructor(renderer, events, option, seriesGateway) {
|
|
6460
6587
|
this.events = events;
|
|
6461
6588
|
this.store = new DrawingStore();
|
|
6462
6589
|
this.history = new DrawingHistory();
|
|
@@ -6482,6 +6609,7 @@ var Vela = (function (exports) {
|
|
|
6482
6609
|
const { definition, visible } = buildToolbar(option);
|
|
6483
6610
|
this.port.setToolbar(definition);
|
|
6484
6611
|
this.port.showToolbar(visible);
|
|
6612
|
+
if (seriesGateway) this.port.setSeriesGateway?.(seriesGateway);
|
|
6485
6613
|
this.subs.push(this.port.onDrawingIntent((i) => this.onIntent(i)));
|
|
6486
6614
|
this.subs.push(this.store.onChange(() => this.sync()));
|
|
6487
6615
|
}
|
|
@@ -6587,7 +6715,7 @@ var Vela = (function (exports) {
|
|
|
6587
6715
|
style,
|
|
6588
6716
|
text: init.text,
|
|
6589
6717
|
props: init.props,
|
|
6590
|
-
zIndex: init.zIndex ?? this.startZ(init.paneId ?? "price")
|
|
6718
|
+
zIndex: init.zIndex ?? this.startZ(type, init.paneId ?? "price")
|
|
6591
6719
|
});
|
|
6592
6720
|
if (!d) return null;
|
|
6593
6721
|
this.history.record(this.store.serialize());
|
|
@@ -6662,10 +6790,14 @@ var Vela = (function (exports) {
|
|
|
6662
6790
|
* (falling back to just under the pane's top series where there is no price — a study
|
|
6663
6791
|
* pane). Half a key down never ties a series; drawings tying each other paint in insertion
|
|
6664
6792
|
* order, so consecutive new drawings still stack newest-in-front. Undefined without a
|
|
6665
|
-
* shared z space — the store then places it over the other drawings, its own layer's top.
|
|
6666
|
-
|
|
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) {
|
|
6667
6797
|
const range = this.port?.stackRange?.(paneId);
|
|
6668
|
-
|
|
6798
|
+
if (!range) return void 0;
|
|
6799
|
+
if (getDrawingType(type)?.coversSeries) return range.front + 0.5;
|
|
6800
|
+
return (range.price ?? range.front) - 0.5;
|
|
6669
6801
|
}
|
|
6670
6802
|
/** Programmatically select drawings (host UI → chart): shows the on-chart handles + toolbar.
|
|
6671
6803
|
* `additive` toggles membership (matching shift-click) instead of replacing. */
|
|
@@ -6810,7 +6942,7 @@ var Vela = (function (exports) {
|
|
|
6810
6942
|
const style = last2 ? { ...i.doc.style, ...last2 } : i.doc.style;
|
|
6811
6943
|
const d = deserializeDrawing({ ...i.doc, id: this.store.nextId(), style });
|
|
6812
6944
|
if (!d) return;
|
|
6813
|
-
if (!d.zIndex) d.zIndex = this.startZ(d.paneId) ?? 0;
|
|
6945
|
+
if (!d.zIndex) d.zIndex = this.startZ(d.type, d.paneId) ?? 0;
|
|
6814
6946
|
this.history.record(before);
|
|
6815
6947
|
this.store.add(d);
|
|
6816
6948
|
this.captureStyle(d.id);
|
|
@@ -6908,6 +7040,180 @@ var Vela = (function (exports) {
|
|
|
6908
7040
|
return 36e5;
|
|
6909
7041
|
}
|
|
6910
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
|
+
|
|
6911
7217
|
// src/data/symbol-groups.ts
|
|
6912
7218
|
function isGroupRow(d) {
|
|
6913
7219
|
return d.group != null && d.ticker === d.group;
|
|
@@ -7272,6 +7578,7 @@ var Vela = (function (exports) {
|
|
|
7272
7578
|
var PREVIEW_BARS = 300;
|
|
7273
7579
|
var SINGLE_LOAD_BARS = 5e3;
|
|
7274
7580
|
var CHUNK_BARS = 1e4;
|
|
7581
|
+
var FIRST_PAINT_BARS = 100;
|
|
7275
7582
|
var GAP_FACTOR = 1.5;
|
|
7276
7583
|
var HEAL_COOLDOWN_MS = 5e3;
|
|
7277
7584
|
var EngineOrchestrator = class _EngineOrchestrator {
|
|
@@ -7389,7 +7696,13 @@ var Vela = (function (exports) {
|
|
|
7389
7696
|
const initialStyle = this.renderer.readFeature("priceStyle");
|
|
7390
7697
|
if (typeof initialStyle === "string") this.priceStyle = initialStyle;
|
|
7391
7698
|
this.barTransform = barTransformFor(initialStyle);
|
|
7392
|
-
|
|
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);
|
|
7393
7706
|
this.unresolvedUnsub = this.feed.onUnresolved?.((info) => {
|
|
7394
7707
|
this.endLoad();
|
|
7395
7708
|
this.events.emit("data:unresolved", info);
|
|
@@ -7541,6 +7854,7 @@ var Vela = (function (exports) {
|
|
|
7541
7854
|
let painted = false;
|
|
7542
7855
|
const paint = (bars, final) => {
|
|
7543
7856
|
if (this.generation !== gen || !final && bars.length === 0) return;
|
|
7857
|
+
if (!painted && !final && bars.length < Math.min(requested, FIRST_PAINT_BARS)) return;
|
|
7544
7858
|
this.setBarSeries(bars, painted ? { preserveView: true } : void 0);
|
|
7545
7859
|
if (!painted && bars.length > 0) {
|
|
7546
7860
|
painted = true;
|
|
@@ -8171,6 +8485,15 @@ var Vela = (function (exports) {
|
|
|
8171
8485
|
record.pendingCause = "inputs";
|
|
8172
8486
|
if (record.session) record.session.update(record.inputValues);
|
|
8173
8487
|
else if (record.native && !record.hidden) record.native.instance.setInputs(record.inputValues);
|
|
8488
|
+
this.events.emit("indicator:inputs", { id });
|
|
8489
|
+
}
|
|
8490
|
+
/** IndicatorController: the CURRENT stored input values (defaults merged with edits). */
|
|
8491
|
+
inputValuesOf(id) {
|
|
8492
|
+
return { ...this.registry.get(id)?.inputValues };
|
|
8493
|
+
}
|
|
8494
|
+
/** IndicatorController: the CURRENT declaration-prop overrides. */
|
|
8495
|
+
propValuesOf(id) {
|
|
8496
|
+
return { ...this.registry.get(id)?.propValues };
|
|
8174
8497
|
}
|
|
8175
8498
|
/** IndicatorController: re-run an indicator with merged declaration-prop overrides.
|
|
8176
8499
|
* Same lifecycle as {@link applyInputs} — a prop change replays the whole script.
|
|
@@ -8184,6 +8507,7 @@ var Vela = (function (exports) {
|
|
|
8184
8507
|
if (!record.hidden) this.setLoading(record, true);
|
|
8185
8508
|
record.pendingCause = "inputs";
|
|
8186
8509
|
record.session.update(record.inputValues, record.propValues);
|
|
8510
|
+
this.events.emit("indicator:inputs", { id });
|
|
8187
8511
|
}
|
|
8188
8512
|
/** IndicatorController: tear down an indicator and (if now empty) its pane. */
|
|
8189
8513
|
/** Live handles of every indicator on the chart (script + native), insertion order. */
|
|
@@ -8371,8 +8695,8 @@ var Vela = (function (exports) {
|
|
|
8371
8695
|
onModel: (model) => {
|
|
8372
8696
|
const first2 = !record.announced;
|
|
8373
8697
|
const cause = record.pendingCause ?? "history";
|
|
8698
|
+
if (!this.applyModel(id, model)) return;
|
|
8374
8699
|
record.pendingCause = void 0;
|
|
8375
|
-
this.applyModel(id, model);
|
|
8376
8700
|
this.emitContextChanged(id);
|
|
8377
8701
|
this.emitScriptRun(id, cause, first2);
|
|
8378
8702
|
},
|
|
@@ -8665,10 +8989,16 @@ var Vela = (function (exports) {
|
|
|
8665
8989
|
* Apply an emitted model. First emission mounts (and routes the pane); a pending
|
|
8666
8990
|
* structural change (after an input edit) remounts idempotently; everything else
|
|
8667
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.
|
|
8668
8997
|
*/
|
|
8669
8998
|
applyModel(id, model) {
|
|
8670
8999
|
const record = this.registry.get(id);
|
|
8671
|
-
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;
|
|
8672
9002
|
const handle = this.handles.get(id);
|
|
8673
9003
|
if (!record.renderHandle) {
|
|
8674
9004
|
const paneId2 = this.routePane(id, model, record.options ?? {});
|
|
@@ -8678,7 +9008,7 @@ var Vela = (function (exports) {
|
|
|
8678
9008
|
record.renderHandle = this.renderer.mountIndicator(model);
|
|
8679
9009
|
record.pendingStructural = false;
|
|
8680
9010
|
this.announce(record, handle);
|
|
8681
|
-
return;
|
|
9011
|
+
return true;
|
|
8682
9012
|
}
|
|
8683
9013
|
let paneId = record.model?.paneId ?? "price";
|
|
8684
9014
|
const prevOwnScale = record.model?.ownScale === true;
|
|
@@ -8706,6 +9036,11 @@ var Vela = (function (exports) {
|
|
|
8706
9036
|
}
|
|
8707
9037
|
if (record.loading) this.setLoading(record, false);
|
|
8708
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;
|
|
8709
9044
|
}
|
|
8710
9045
|
routePane(id, model, options) {
|
|
8711
9046
|
if (options.pane === "new") return `pane-${id}`;
|
|
@@ -9408,6 +9743,7 @@ var Vela = (function (exports) {
|
|
|
9408
9743
|
"4h": 144e5,
|
|
9409
9744
|
"1d": 864e5,
|
|
9410
9745
|
"1w": 6048e5,
|
|
9746
|
+
"1M": 2592e6,
|
|
9411
9747
|
"1": 6e4,
|
|
9412
9748
|
"5": 3e5,
|
|
9413
9749
|
"15": 9e5,
|
|
@@ -9415,7 +9751,8 @@ var Vela = (function (exports) {
|
|
|
9415
9751
|
"60": 36e5,
|
|
9416
9752
|
"240": 144e5,
|
|
9417
9753
|
D: 864e5,
|
|
9418
|
-
W: 6048e5
|
|
9754
|
+
W: 6048e5,
|
|
9755
|
+
M: 2592e6
|
|
9419
9756
|
};
|
|
9420
9757
|
return map[timeframe] ?? 36e5;
|
|
9421
9758
|
}
|
|
@@ -17222,6 +17559,14 @@ ${STATIC_DECLS}
|
|
|
17222
17559
|
const conds = Array.isArray(when) ? when : [when];
|
|
17223
17560
|
return conds.every((c) => c.anyOf ? c.anyOf.some((x) => x === values[c.key]) : values[c.key] === c.equals);
|
|
17224
17561
|
}
|
|
17562
|
+
function inputDeltas(schema, values) {
|
|
17563
|
+
const out = {};
|
|
17564
|
+
for (const s of schema) {
|
|
17565
|
+
const v = values[s.key];
|
|
17566
|
+
if (v !== void 0 && JSON.stringify(v) !== JSON.stringify(s.defval)) out[s.key] = v;
|
|
17567
|
+
}
|
|
17568
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
17569
|
+
}
|
|
17225
17570
|
|
|
17226
17571
|
// src/ui/components/select/controller.ts
|
|
17227
17572
|
function selectController(opts) {
|
|
@@ -24194,6 +24539,7 @@ void main() {
|
|
|
24194
24539
|
var DOUBLE_TAP_SLOP = 30;
|
|
24195
24540
|
var TIME_SCALE_K = 4e-3;
|
|
24196
24541
|
var WHEEL_ZOOM_K = 4e-3;
|
|
24542
|
+
var WHEEL_PRICE_DRAG_PX = 0.25;
|
|
24197
24543
|
function wheelZoomAnchor(coords, cursorX, rightEdge) {
|
|
24198
24544
|
if (rightEdge) return { logical: coords.rightEdgeLogical, x: coords.width };
|
|
24199
24545
|
return { logical: coords.xToLogical(cursorX), x: cursorX };
|
|
@@ -24324,7 +24670,7 @@ void main() {
|
|
|
24324
24670
|
this.capture(e.pointerId);
|
|
24325
24671
|
return;
|
|
24326
24672
|
}
|
|
24327
|
-
if (e.shiftKey && this.regionAt(x, y) === "data" && this.deps.drawingsMeasureStart?.(x, y)) {
|
|
24673
|
+
if (e.shiftKey && this.regionAt(x, y) === "data" && this.deps.drawingsMeasureStart?.(x, y, this.snapMode(e))) {
|
|
24328
24674
|
this.region = "drawing";
|
|
24329
24675
|
this.capture(e.pointerId);
|
|
24330
24676
|
return;
|
|
@@ -24438,7 +24784,7 @@ void main() {
|
|
|
24438
24784
|
const wasTouch = e.pointerType === "touch";
|
|
24439
24785
|
const tapRelease = this.dragging && !this.moved && (!wasTouch || Math.hypot(x - this.startX, y - this.startY) <= TOUCH_TAP_SLOP);
|
|
24440
24786
|
if (this.dragging && this.region === "drawing") {
|
|
24441
|
-
this.deps.drawingsPointerUp?.(x, y);
|
|
24787
|
+
this.deps.drawingsPointerUp?.(x, y, this.snapMode(e));
|
|
24442
24788
|
} else if (tapRelease && this.region === "data") {
|
|
24443
24789
|
this.deps.onClick(x, y);
|
|
24444
24790
|
} else if (this.dragging && this.region === "data") {
|
|
@@ -24459,7 +24805,7 @@ void main() {
|
|
|
24459
24805
|
if (e.pointerType === "touch") this.touches.delete(e.pointerId);
|
|
24460
24806
|
this.cancelLongPress();
|
|
24461
24807
|
if (!this.dragging) return;
|
|
24462
|
-
if (this.region === "drawing" && !Number.isNaN(this.cursorX)) this.deps.drawingsPointerUp?.(this.cursorX, this.cursorY);
|
|
24808
|
+
if (this.region === "drawing" && !Number.isNaN(this.cursorX)) this.deps.drawingsPointerUp?.(this.cursorX, this.cursorY, this.snapMode(e));
|
|
24463
24809
|
if (this.region === "crosshair" || e.pointerType === "touch") this.deps.onPointerMove(null, null);
|
|
24464
24810
|
this.endGesture(e);
|
|
24465
24811
|
};
|
|
@@ -24475,6 +24821,12 @@ void main() {
|
|
|
24475
24821
|
this.onWheel = (e) => {
|
|
24476
24822
|
e.preventDefault();
|
|
24477
24823
|
this.deps.drawingsClearTransient?.();
|
|
24824
|
+
const { x, y } = this.local(e);
|
|
24825
|
+
if (this.regionAt(x, y) === "price" && e.deltaY !== 0) {
|
|
24826
|
+
this.deps.beginPriceScale(x, y);
|
|
24827
|
+
this.deps.priceScaleBy(e.deltaY * WHEEL_PRICE_DRAG_PX);
|
|
24828
|
+
return;
|
|
24829
|
+
}
|
|
24478
24830
|
const coords = this.deps.getCoords();
|
|
24479
24831
|
const vp = coords.getViewport();
|
|
24480
24832
|
const pan = wheelPanDelta(e.deltaX, e.deltaY, e.shiftKey);
|
|
@@ -24482,9 +24834,8 @@ void main() {
|
|
|
24482
24834
|
this.deps.apply({ barSpacing: vp.barSpacing, rightOffset: wheelPanRightOffset(vp.rightOffset, pan, coords.pxPerBar()) });
|
|
24483
24835
|
return;
|
|
24484
24836
|
}
|
|
24485
|
-
const cursorX = this.local(e).x;
|
|
24486
24837
|
const rightEdge = this.rightEdgeZoom && !(e.ctrlKey || e.metaKey);
|
|
24487
|
-
const anchor = wheelZoomAnchor(coords,
|
|
24838
|
+
const anchor = wheelZoomAnchor(coords, x, rightEdge);
|
|
24488
24839
|
const target = clampBarSpacing(vp.barSpacing * Math.exp(-e.deltaY * WHEEL_ZOOM_K));
|
|
24489
24840
|
this.deps.zoomTo(target, anchor.logical, anchor.x);
|
|
24490
24841
|
};
|
|
@@ -25966,6 +26317,15 @@ void main() {
|
|
|
25966
26317
|
function fontSizePx(size3) {
|
|
25967
26318
|
return size3 === "auto" ? 12 : namedFontSize(size3);
|
|
25968
26319
|
}
|
|
26320
|
+
function lineCoversWindow(a, b, extend, lo, hi) {
|
|
26321
|
+
const minX = Math.min(a, b);
|
|
26322
|
+
const maxX = Math.max(a, b);
|
|
26323
|
+
if (a === b) return a >= lo && a <= hi;
|
|
26324
|
+
if (extend === "both") return true;
|
|
26325
|
+
if (extend === "left") return maxX >= lo;
|
|
26326
|
+
if (extend === "right") return minX <= hi;
|
|
26327
|
+
return maxX >= lo && minX <= hi;
|
|
26328
|
+
}
|
|
25969
26329
|
var DrawingSceneRenderer = class {
|
|
25970
26330
|
constructor(deps, set = EMPTY_DRAWING_SET) {
|
|
25971
26331
|
this.deps = deps;
|
|
@@ -26019,7 +26379,9 @@ void main() {
|
|
|
26019
26379
|
const lo = Math.min(from, to);
|
|
26020
26380
|
const hi = Math.max(from, to);
|
|
26021
26381
|
const visible = (a, b, extend) => {
|
|
26022
|
-
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;
|
|
26023
26385
|
return Math.max(a, b) >= lo && Math.min(a, b) <= hi;
|
|
26024
26386
|
};
|
|
26025
26387
|
let min2 = Infinity;
|
|
@@ -26030,7 +26392,7 @@ void main() {
|
|
|
26030
26392
|
};
|
|
26031
26393
|
for (const ln of this.set.lines) {
|
|
26032
26394
|
if (ln.invisible) continue;
|
|
26033
|
-
if (!
|
|
26395
|
+
if (!lineCoversWindow(this.logicalOf(ln.xloc, ln.x1), this.logicalOf(ln.xloc, ln.x2), ln.extend, lo, hi)) continue;
|
|
26034
26396
|
fold(ln.y1);
|
|
26035
26397
|
fold(ln.y2);
|
|
26036
26398
|
}
|
|
@@ -28583,6 +28945,17 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
28583
28945
|
constructor() {
|
|
28584
28946
|
/** The current `paintAll` call's interaction state, visible to the per-type painters. */
|
|
28585
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
|
+
};
|
|
28586
28959
|
}
|
|
28587
28960
|
/** Paint every visible drawing, then selection handles for the targeted ones.
|
|
28588
28961
|
* Each drawing is clipped to its own pane's rect (and skipped entirely while that pane
|
|
@@ -28627,6 +29000,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
28627
29000
|
ctx.globalAlpha = GHOST_ALPHA;
|
|
28628
29001
|
if (ghost instanceof RegressionChannel || ghost instanceof FixedRangeVolumeProfile) {
|
|
28629
29002
|
this.paintTimeSpanGhost(ctx, ghost, proj);
|
|
29003
|
+
} else if (ghost instanceof Magnifier) {
|
|
29004
|
+
this.paintMagnifierGhost(ctx, ghost, proj, theme);
|
|
28630
29005
|
} else this.paintOne(ctx, ghost, proj, theme);
|
|
28631
29006
|
ctx.globalAlpha = 1;
|
|
28632
29007
|
}
|
|
@@ -28760,6 +29135,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
28760
29135
|
this.paintLabel(ctx, d, proj, theme);
|
|
28761
29136
|
return;
|
|
28762
29137
|
}
|
|
29138
|
+
if (d instanceof Magnifier) {
|
|
29139
|
+
this.paintMagnifier(ctx, d, proj, theme);
|
|
29140
|
+
return;
|
|
29141
|
+
}
|
|
28763
29142
|
if (d instanceof PatternDrawing) {
|
|
28764
29143
|
this.paintPattern(ctx, d, proj, theme);
|
|
28765
29144
|
return;
|
|
@@ -29252,6 +29631,216 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
29252
29631
|
ctx.textBaseline = "alphabetic";
|
|
29253
29632
|
}
|
|
29254
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
|
+
}
|
|
29255
29844
|
/** Paint a fixed-range volume profile: horizontal histogram rows (up/down split) anchored to
|
|
29256
29845
|
* the left or right of the time span, optional VAH / VAL / POC levels across the range, and
|
|
29257
29846
|
* optional developing POC / VA polylines. Recomputes from the two anchors on every paint. */
|
|
@@ -29970,6 +30559,22 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
29970
30559
|
this.snapAt = changed ? { point: snapped, paneId } : null;
|
|
29971
30560
|
return snapped;
|
|
29972
30561
|
}
|
|
30562
|
+
/**
|
|
30563
|
+
* Resolve a cursor pixel through the magnet and return the snapped pixel — the same
|
|
30564
|
+
* conversion drawing placement uses. Updates the snap-ring marker. The measure
|
|
30565
|
+
* ruler goes through this so its endpoints follow weak/strong/Ctrl magnet too.
|
|
30566
|
+
*/
|
|
30567
|
+
snapCursor(x, y, mode) {
|
|
30568
|
+
const proj = this.deps.projector();
|
|
30569
|
+
const paneId = proj.paneIdAtY(y) ?? "price";
|
|
30570
|
+
const point = this.resolve(x, y, paneId, mode);
|
|
30571
|
+
const sy = proj.yOf(point.price, paneId);
|
|
30572
|
+
return { x: proj.xOf(point.time), y: sy ?? y };
|
|
30573
|
+
}
|
|
30574
|
+
/** Drop the snap-ring marker (a transient mode ended without going through `up`). */
|
|
30575
|
+
clearSnapMarker() {
|
|
30576
|
+
this.snapAt = null;
|
|
30577
|
+
}
|
|
29973
30578
|
/** Resolve a pixel to a data point with the segment angle locked to 45° steps around
|
|
29974
30579
|
* `pivot` (Shift held on a line tool). Works in PIXEL space — the user reasons about
|
|
29975
30580
|
* the angle they see, not about time/price units. The magnet is bypassed: snapping
|
|
@@ -30651,8 +31256,9 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
30651
31256
|
if (!existing) document.head.appendChild(s);
|
|
30652
31257
|
}
|
|
30653
31258
|
var DrawingSettingsPopup = class {
|
|
30654
|
-
constructor(host, theme) {
|
|
31259
|
+
constructor(host, theme, chartBarMs = () => 0) {
|
|
30655
31260
|
this.host = host;
|
|
31261
|
+
this.chartBarMs = chartBarMs;
|
|
30656
31262
|
this.el = null;
|
|
30657
31263
|
this.tipEl = null;
|
|
30658
31264
|
// floating hover-label (above/below the toolbar)
|
|
@@ -30677,6 +31283,14 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
30677
31283
|
this.theme = theme;
|
|
30678
31284
|
this.settingsDialog = new DrawingSettingsDialog(host, theme);
|
|
30679
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
|
+
}
|
|
30680
31294
|
setTheme(theme) {
|
|
30681
31295
|
this.theme = theme;
|
|
30682
31296
|
this.settingsDialog.setTheme(theme);
|
|
@@ -30719,7 +31333,20 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
30719
31333
|
const sz = drawing.size ?? "normal";
|
|
30720
31334
|
bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label: sizeLabel }));
|
|
30721
31335
|
}
|
|
30722
|
-
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 })));
|
|
30723
31350
|
if (paths.has("style.lineWidth")) {
|
|
30724
31351
|
const wf = schema.fields.find((f) => f.path === "style.lineWidth");
|
|
30725
31352
|
if (wf?.kind === "number" && (wf.min ?? 1) > 1) {
|
|
@@ -31041,6 +31668,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
31041
31668
|
this.colorPop = null;
|
|
31042
31669
|
this.colorOwner = null;
|
|
31043
31670
|
}
|
|
31671
|
+
opts.onClose?.();
|
|
31044
31672
|
}
|
|
31045
31673
|
});
|
|
31046
31674
|
const el = pop.el;
|
|
@@ -31059,6 +31687,49 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
31059
31687
|
pop.show();
|
|
31060
31688
|
return pop;
|
|
31061
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
|
+
}
|
|
31062
31733
|
/** A floating list of one-shot actions (icon + label rows) opened by the kebab. */
|
|
31063
31734
|
openActionMenu(anchor, rows) {
|
|
31064
31735
|
this.menuPop = this.hostFloat(anchor, {
|
|
@@ -31169,10 +31840,13 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
31169
31840
|
let cur = current;
|
|
31170
31841
|
const paint = (v) => {
|
|
31171
31842
|
b.replaceChildren();
|
|
31172
|
-
const
|
|
31173
|
-
|
|
31174
|
-
|
|
31175
|
-
|
|
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
|
+
}
|
|
31176
31850
|
if (opts.label && opts.labelInTrigger) {
|
|
31177
31851
|
const tx = document.createElement("span");
|
|
31178
31852
|
tx.textContent = opts.label(v);
|
|
@@ -31214,10 +31888,13 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
31214
31888
|
item.className = "vela-dpop-item";
|
|
31215
31889
|
item.dataset.active = active ? "1" : "0";
|
|
31216
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;`;
|
|
31217
|
-
const
|
|
31218
|
-
|
|
31219
|
-
|
|
31220
|
-
|
|
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
|
+
}
|
|
31221
31898
|
if (label) {
|
|
31222
31899
|
const tx = document.createElement("span");
|
|
31223
31900
|
tx.textContent = label(v);
|
|
@@ -32007,30 +32684,39 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
32007
32684
|
isFinished() {
|
|
32008
32685
|
return this.state === "finished";
|
|
32009
32686
|
}
|
|
32010
|
-
/** A press: begin the measurement, or finish it on the second click.
|
|
32011
|
-
|
|
32687
|
+
/** A press: begin the measurement, or finish it on the second click.
|
|
32688
|
+
* `x,y` are the raw cursor (drag-slop vs click-move-click). `gx,gy` are the
|
|
32689
|
+
* graphic endpoints — magnet-snapped when the magnet is on, else the same as `x,y`. */
|
|
32690
|
+
down(x, y, gx = x, gy = y) {
|
|
32012
32691
|
if (this.state === "measuring") {
|
|
32013
|
-
this.end = { x, y };
|
|
32692
|
+
this.end = { x: gx, y: gy };
|
|
32014
32693
|
this.state = "finished";
|
|
32015
32694
|
return;
|
|
32016
32695
|
}
|
|
32017
|
-
this.start = { x, y };
|
|
32018
|
-
this.end = { x, y };
|
|
32696
|
+
this.start = { x: gx, y: gy };
|
|
32697
|
+
this.end = { x: gx, y: gy };
|
|
32019
32698
|
this.pressX = x;
|
|
32020
32699
|
this.pressY = y;
|
|
32021
32700
|
this.state = "measuring";
|
|
32022
32701
|
}
|
|
32702
|
+
/** Size the in-progress ruler. `x,y` are the graphic (magnet-snapped) cursor. */
|
|
32023
32703
|
move(x, y) {
|
|
32024
32704
|
if (this.state === "measuring") this.end = { x, y };
|
|
32025
32705
|
}
|
|
32026
32706
|
/** A release: finish if the press was actually dragged (press-drag-release), else wait
|
|
32027
|
-
* for the second click (click-move-click).
|
|
32028
|
-
|
|
32707
|
+
* for the second click (click-move-click). `x,y` are the raw cursor (slop); `gx,gy`
|
|
32708
|
+
* are the graphic end (magnet-snapped when the magnet is on). */
|
|
32709
|
+
up(x, y, gx = x, gy = y) {
|
|
32029
32710
|
if (this.state === "measuring" && Math.hypot(x - this.pressX, y - this.pressY) > DRAG_SLOP3) {
|
|
32030
|
-
this.end = { x, y };
|
|
32711
|
+
this.end = { x: gx, y: gy };
|
|
32031
32712
|
this.state = "finished";
|
|
32032
32713
|
}
|
|
32033
32714
|
}
|
|
32715
|
+
/** Current graphic endpoints in media pixels, or null when idle. */
|
|
32716
|
+
points() {
|
|
32717
|
+
if (!this.start || !this.end) return null;
|
|
32718
|
+
return { start: this.start, end: this.end };
|
|
32719
|
+
}
|
|
32034
32720
|
clear() {
|
|
32035
32721
|
this.state = "idle";
|
|
32036
32722
|
this.start = null;
|
|
@@ -32192,6 +32878,9 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
32192
32878
|
this.intentCb = null;
|
|
32193
32879
|
/** Another chart's in-progress placement, mirrored here as a ghost (drawings sync). */
|
|
32194
32880
|
this.externalGhost = null;
|
|
32881
|
+
/** Core-pushed series gateway (finer-timeframe bars for data-driven drawings). */
|
|
32882
|
+
this.seriesGw = null;
|
|
32883
|
+
this.seriesGwUnsub = null;
|
|
32195
32884
|
/** Last draft fingerprint reported upstream — gates the per-render emission to actual changes. */
|
|
32196
32885
|
this.lastDraftKey = null;
|
|
32197
32886
|
this.measure = new MeasureOverlay();
|
|
@@ -32219,7 +32908,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
32219
32908
|
this.textEditor = null;
|
|
32220
32909
|
this.painter = new DrawingPainter();
|
|
32221
32910
|
this.ctx = canvas.getContext("2d");
|
|
32222
|
-
this.popup = new DrawingSettingsPopup(overlayHost, deps.theme());
|
|
32911
|
+
this.popup = new DrawingSettingsPopup(overlayHost, deps.theme(), () => deps.chartBarMs());
|
|
32223
32912
|
this.toolbar = new DrawingToolbar(
|
|
32224
32913
|
toolbarHost,
|
|
32225
32914
|
deps.theme(),
|
|
@@ -32277,6 +32966,22 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
32277
32966
|
const shown = this.toolbarVisible && !this.mobileLayout;
|
|
32278
32967
|
this.deps.setToolbarGutter(shown ? this.toolbarCollapsed ? TOOLBAR_COLLAPSED_WIDTH : TOOLBAR_WIDTH : 0);
|
|
32279
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
|
+
}
|
|
32280
32985
|
/** Core push: mirror (or clear) another chart's in-progress placement as a ghost. */
|
|
32281
32986
|
setExternalGhost(doc) {
|
|
32282
32987
|
this.externalGhost = doc ? deserializeDrawing(doc) : null;
|
|
@@ -32394,8 +33099,34 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
32394
33099
|
/** Should the drawing layer win this press (vs pan)? */
|
|
32395
33100
|
claim(x, y) {
|
|
32396
33101
|
if (this.measureMode || this.eraserMode) return true;
|
|
33102
|
+
if (this.magnifierChipAt(x, y)) return true;
|
|
32397
33103
|
return this.interaction.claim(x, y);
|
|
32398
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
|
+
}
|
|
32399
33130
|
/** Delete the (unlocked) drawing under the cursor. True when one was removed.
|
|
32400
33131
|
* Shared by the eraser (click + drag) and the middle-click shortcut. */
|
|
32401
33132
|
deleteAt(x, y) {
|
|
@@ -32407,11 +33138,13 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
32407
33138
|
}
|
|
32408
33139
|
/** Shift+press on the empty plot: arm the measure ruler AND start it at (x, y) in one
|
|
32409
33140
|
* gesture — the equivalent of clicking the toolbar's Measure button, then pressing.
|
|
32410
|
-
*
|
|
32411
|
-
|
|
33141
|
+
* `snap` is the effective magnet (sticky mode, or Ctrl/Cmd-forced strong). Returns
|
|
33142
|
+
* false when a mode/tool is already active (the normal press path owns it). */
|
|
33143
|
+
beginMeasureAt(x, y, snap = "off") {
|
|
32412
33144
|
if (this.measureMode || this.eraserMode || this.activeTool != null) return false;
|
|
32413
33145
|
this.withModeIntent(() => this.toggleMeasure());
|
|
32414
|
-
this.
|
|
33146
|
+
const g = this.interaction.snapCursor(x, y, snap);
|
|
33147
|
+
this.measure.down(x, y, g.x, g.y);
|
|
32415
33148
|
this.render();
|
|
32416
33149
|
return true;
|
|
32417
33150
|
}
|
|
@@ -32433,11 +33166,19 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
32433
33166
|
return;
|
|
32434
33167
|
}
|
|
32435
33168
|
if (this.measureMode) {
|
|
32436
|
-
this.
|
|
33169
|
+
const g = this.interaction.snapCursor(x, y, snap);
|
|
33170
|
+
this.measure.down(x, y, g.x, g.y);
|
|
32437
33171
|
if (this.measure.isFinished()) this.withModeIntent(() => this.exitMeasure(false));
|
|
32438
33172
|
this.render();
|
|
32439
33173
|
return;
|
|
32440
33174
|
}
|
|
33175
|
+
if (this.activeTool == null) {
|
|
33176
|
+
const chipOwner = this.magnifierChipAt(x, y);
|
|
33177
|
+
if (chipOwner) {
|
|
33178
|
+
this.openMagnifierChipMenu(chipOwner);
|
|
33179
|
+
return;
|
|
33180
|
+
}
|
|
33181
|
+
}
|
|
32441
33182
|
this.interaction.down(x, y, snap, shift3);
|
|
32442
33183
|
}
|
|
32443
33184
|
pointerMove(x, y, snap = "off", shift3 = false) {
|
|
@@ -32446,7 +33187,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
32446
33187
|
return;
|
|
32447
33188
|
}
|
|
32448
33189
|
if (this.measureMode) {
|
|
32449
|
-
this.
|
|
33190
|
+
const g = this.interaction.snapCursor(x, y, snap);
|
|
33191
|
+
this.measure.move(g.x, g.y);
|
|
32450
33192
|
this.render();
|
|
32451
33193
|
return;
|
|
32452
33194
|
}
|
|
@@ -32464,13 +33206,14 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
32464
33206
|
this.render();
|
|
32465
33207
|
}
|
|
32466
33208
|
}
|
|
32467
|
-
pointerUp(x, y) {
|
|
33209
|
+
pointerUp(x, y, snap = "off") {
|
|
32468
33210
|
if (this.eraserMode) {
|
|
32469
33211
|
this.erasing = false;
|
|
32470
33212
|
return;
|
|
32471
33213
|
}
|
|
32472
33214
|
if (this.measureMode) {
|
|
32473
|
-
this.
|
|
33215
|
+
const g = this.interaction.snapCursor(x, y, snap);
|
|
33216
|
+
this.measure.up(x, y, g.x, g.y);
|
|
32474
33217
|
if (this.measure.isFinished()) this.withModeIntent(() => this.exitMeasure(false));
|
|
32475
33218
|
this.render();
|
|
32476
33219
|
return;
|
|
@@ -32515,6 +33258,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
32515
33258
|
/** Leave ruler mode. `clearGraphic` keeps a just-finished measurement on screen (false). */
|
|
32516
33259
|
exitMeasure(clearGraphic = true) {
|
|
32517
33260
|
this.measureMode = false;
|
|
33261
|
+
this.interaction.clearSnapMarker();
|
|
32518
33262
|
if (clearGraphic) this.measure.clear();
|
|
32519
33263
|
this.toolbar.setMeasureActive(false);
|
|
32520
33264
|
this.render();
|
|
@@ -32653,17 +33397,31 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
32653
33397
|
/** Cursor hint while hovering — `'pointer'` over a drawing/handle, else null. */
|
|
32654
33398
|
cursorAt(x, y) {
|
|
32655
33399
|
if (this.eraserMode) return "pointer";
|
|
33400
|
+
if (this.activeTool == null && this.magnifierChipAt(x, y)) return "pointer";
|
|
32656
33401
|
return this.interaction.cursorAt(x, y);
|
|
32657
33402
|
}
|
|
32658
|
-
/** Right-click
|
|
32659
|
-
*
|
|
32660
|
-
*
|
|
32661
|
-
*
|
|
33403
|
+
/** Right-click: an explicit escape back to the pointer. Cancels an in-progress
|
|
33404
|
+
* placement or measurement, and also plain-disarms an armed-but-idle drawing
|
|
33405
|
+
* tool or the eraser — so a right-click ALWAYS reverts to the pointer, even in
|
|
33406
|
+
* stay-in-drawing-mode (where Escape would leave a drawing tool armed).
|
|
33407
|
+
* Persistent toggles (magnet, stay-mode, favorites) are untouched. Returns
|
|
33408
|
+
* whether the press was consumed; false lets the host's context menu open
|
|
33409
|
+
* normally. */
|
|
32662
33410
|
cancelPlacement() {
|
|
32663
|
-
if (
|
|
32664
|
-
|
|
32665
|
-
|
|
32666
|
-
|
|
33411
|
+
if (this.measureMode || this.eraserMode) {
|
|
33412
|
+
this.withModeIntent(() => this.measureMode ? this.exitMeasure() : this.exitEraser());
|
|
33413
|
+
return true;
|
|
33414
|
+
}
|
|
33415
|
+
if (this.interaction.isPlacing()) {
|
|
33416
|
+
this.interaction.cancel();
|
|
33417
|
+
if (this.activeTool != null) this.emit({ kind: "arm", type: null });
|
|
33418
|
+
return true;
|
|
33419
|
+
}
|
|
33420
|
+
if (this.activeTool != null) {
|
|
33421
|
+
this.emit({ kind: "arm", type: null });
|
|
33422
|
+
return true;
|
|
33423
|
+
}
|
|
33424
|
+
return false;
|
|
32667
33425
|
}
|
|
32668
33426
|
/** Double-click over a drawing → suppress the chart's view reset (single-click already
|
|
32669
33427
|
* opens settings). Returns true only when a drawing is under the cursor. */
|
|
@@ -32689,6 +33447,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
32689
33447
|
return true;
|
|
32690
33448
|
}
|
|
32691
33449
|
if (this.interaction.cancel()) return true;
|
|
33450
|
+
if (this.measureMode) {
|
|
33451
|
+
this.withModeIntent(() => this.exitMeasure());
|
|
33452
|
+
return true;
|
|
33453
|
+
}
|
|
32692
33454
|
if (this.selectedIds.size) {
|
|
32693
33455
|
this.clearSelection();
|
|
32694
33456
|
return true;
|
|
@@ -32814,6 +33576,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
32814
33576
|
if (!sctx) continue;
|
|
32815
33577
|
sctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
32816
33578
|
sctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
|
|
33579
|
+
this.painter.seriesLook = this.deps.seriesLook();
|
|
32817
33580
|
this.painter.paintAll(sctx, drawings, proj, theme, EMPTY_TARGETS);
|
|
32818
33581
|
const slices = out.get(paneId) ?? [];
|
|
32819
33582
|
slices.push({ beforeZ, canvas });
|
|
@@ -32841,6 +33604,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
32841
33604
|
dragged: this.interaction.activeDragId(),
|
|
32842
33605
|
mutedLabel: edited instanceof TextLabel ? edited.id : null
|
|
32843
33606
|
};
|
|
33607
|
+
this.painter.seriesLook = this.deps.seriesLook();
|
|
32844
33608
|
this.painter.paintAll(ctx, this.drawings.filter((d) => !this.isInterleaved(d)), proj, this.deps.theme(), targets);
|
|
32845
33609
|
this.painter.paintHighlights(ctx, this.drawings.filter((d) => this.isInterleaved(d)), proj, handleIdsFor(targets));
|
|
32846
33610
|
this.layoutTextEditor();
|
|
@@ -32848,6 +33612,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
32848
33612
|
if (ghost) this.painter.paintGhost(ctx, ghost, proj, this.deps.theme());
|
|
32849
33613
|
if (this.externalGhost) this.painter.paintGhost(ctx, this.externalGhost, proj, this.deps.theme());
|
|
32850
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
|
+
}
|
|
32851
33619
|
const markers = this.interaction.placingMarkers(proj);
|
|
32852
33620
|
if (markers) this.painter.paintHandles(ctx, markers);
|
|
32853
33621
|
const m = this.interaction.snapMarker();
|
|
@@ -32859,6 +33627,9 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
32859
33627
|
this.closeTextEditor();
|
|
32860
33628
|
this.popup.destroy();
|
|
32861
33629
|
this.toolbar.destroy();
|
|
33630
|
+
this.seriesGwUnsub?.();
|
|
33631
|
+
this.seriesGwUnsub = null;
|
|
33632
|
+
this.seriesGw = null;
|
|
32862
33633
|
this.intentCb = null;
|
|
32863
33634
|
this.drawings = [];
|
|
32864
33635
|
}
|
|
@@ -33256,7 +34027,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
33256
34027
|
}
|
|
33257
34028
|
|
|
33258
34029
|
// src/renderers/native/drawings/Projector.ts
|
|
33259
|
-
function createProjector(coords, paneOf, paneIdAtY, barsInRange) {
|
|
34030
|
+
function createProjector(coords, paneOf, paneIdAtY, barsInRange, seriesInRange) {
|
|
33260
34031
|
return {
|
|
33261
34032
|
xOf: (time) => coords.timeToX(time),
|
|
33262
34033
|
yOf: (price, paneId) => {
|
|
@@ -33277,6 +34048,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
33277
34048
|
},
|
|
33278
34049
|
barsBetween: (t1, t2) => Math.abs(coords.timeToLogical(t2) - coords.timeToLogical(t1)),
|
|
33279
34050
|
barsInRange: barsInRange ? (from, to) => barsInRange(from, to) : void 0,
|
|
34051
|
+
seriesInRange,
|
|
33280
34052
|
width: coords.width,
|
|
33281
34053
|
height: coords.height
|
|
33282
34054
|
};
|
|
@@ -35247,13 +36019,13 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
35247
36019
|
resetView: () => this.resetView(),
|
|
35248
36020
|
// User drawings claim a gesture before pan when armed / over a drawing.
|
|
35249
36021
|
drawingsClaim: (x, y) => this.userDrawings?.claim(x, y) ?? false,
|
|
35250
|
-
drawingsMeasureStart: (x, y) => this.userDrawings?.beginMeasureAt(x, y) ?? false,
|
|
36022
|
+
drawingsMeasureStart: (x, y, snap) => this.userDrawings?.beginMeasureAt(x, y, snap) ?? false,
|
|
35251
36023
|
drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y) ?? false,
|
|
35252
36024
|
drawingsCancelPlacement: () => this.userDrawings?.cancelPlacement() ?? false,
|
|
35253
36025
|
drawingsSnapMode: () => this.snapMode,
|
|
35254
36026
|
drawingsPointerDown: (x, y, snap, shift3) => this.userDrawings?.pointerDown(x, y, snap, shift3),
|
|
35255
36027
|
drawingsPointerMove: (x, y, snap, shift3) => this.userDrawings?.pointerMove(x, y, snap, shift3),
|
|
35256
|
-
drawingsPointerUp: (x, y) => this.userDrawings?.pointerUp(x, y),
|
|
36028
|
+
drawingsPointerUp: (x, y, snap) => this.userDrawings?.pointerUp(x, y, snap),
|
|
35257
36029
|
drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? null,
|
|
35258
36030
|
drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
|
|
35259
36031
|
drawingsClearTransient: () => this.userDrawings?.clearTransient()
|
|
@@ -35280,6 +36052,23 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
35280
36052
|
seriesBoundaries: (paneId) => this.scene.seriesBoundaries(paneId),
|
|
35281
36053
|
priceZ: (paneId) => paneId === PRICE_PANE_ID ? this.scene.candleZ : null,
|
|
35282
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,
|
|
35283
36072
|
snap: (pt2, paneId, mode, cursorPx) => this.snapToCandle(pt2, paneId, mode, cursorPx),
|
|
35284
36073
|
setSnapMode: (mode) => this.setSnapMode(mode),
|
|
35285
36074
|
setToolbarGutter: (px) => this.setToolbarGutter(px)
|
|
@@ -35568,7 +36357,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
35568
36357
|
}
|
|
35569
36358
|
const skipFit = opts?.preserveView === true && this.didInitialFit;
|
|
35570
36359
|
if (this.coords.width > 0 && !skipFit) {
|
|
35571
|
-
this.
|
|
36360
|
+
if (this.didInitialFit) this.reframeKeepZoom();
|
|
36361
|
+
else this.fitContent();
|
|
35572
36362
|
this.didInitialFit = true;
|
|
35573
36363
|
}
|
|
35574
36364
|
if (!this.introPlayed && this.bars.length > 0) {
|
|
@@ -36216,6 +37006,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
36216
37006
|
this.scaleDragHeight = res.height;
|
|
36217
37007
|
this.scaleDragStart = { ...res.holder.scale };
|
|
36218
37008
|
res.holder.manualScale = { ...res.holder.scale };
|
|
37009
|
+
this.axisScaleButtons?.reposition();
|
|
36219
37010
|
this.scheduler.invalidate(4 /* Full */);
|
|
36220
37011
|
}
|
|
36221
37012
|
/** Rescale the grabbed scale around its center by the total drag (down ⇒ zoom out). */
|
|
@@ -36247,6 +37038,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
36247
37038
|
const res = this.resolveScaleHolder(x, y);
|
|
36248
37039
|
if (!res) return;
|
|
36249
37040
|
res.holder.manualScale = null;
|
|
37041
|
+
this.axisScaleButtons?.reposition();
|
|
36250
37042
|
this.scheduler.invalidate(4 /* Full */);
|
|
36251
37043
|
}
|
|
36252
37044
|
/**
|
|
@@ -36670,7 +37462,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
36670
37462
|
return p ? { scale: p.scale, bounds: p.bounds, collapsed: p.collapsed } : null;
|
|
36671
37463
|
},
|
|
36672
37464
|
(y) => this.paneNodeAtY(y)?.id ?? null,
|
|
36673
|
-
(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
|
|
36674
37467
|
);
|
|
36675
37468
|
}
|
|
36676
37469
|
/** OHLC bars whose open-time falls within `[from, to]` (inclusive) — the data a regression
|
|
@@ -36884,6 +37677,20 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
36884
37677
|
this.coords.setViewport(v);
|
|
36885
37678
|
this.targetBarSpacing = v.barSpacing;
|
|
36886
37679
|
}
|
|
37680
|
+
/** Re-frame after a series replacement (a symbol/timeframe switch): keep the user's
|
|
37681
|
+
* zoom (bar spacing), re-anchor the newest bars at the default right offset.
|
|
37682
|
+
* `clampViewport`'s fit-all-bars floor deliberately does NOT apply — a progressive
|
|
37683
|
+
* head may still be backfilling toward the previous depth, and raising the spacing
|
|
37684
|
+
* to its temporary bar count would lose the zoom this exists to keep. */
|
|
37685
|
+
reframeKeepZoom() {
|
|
37686
|
+
this.animator?.stop();
|
|
37687
|
+
this.panVelocity = 0;
|
|
37688
|
+
for (const pane of this.scene.panes.values()) pane.manualScale = null;
|
|
37689
|
+
for (const sl of this.scene.indicatorScales.values()) sl.manualScale = null;
|
|
37690
|
+
const v = { barSpacing: clampBarSpacing(this.coords.getViewport().barSpacing), rightOffset: defaultViewport().rightOffset };
|
|
37691
|
+
this.coords.setViewport(v);
|
|
37692
|
+
this.targetBarSpacing = v.barSpacing;
|
|
37693
|
+
}
|
|
36887
37694
|
paneBoundsFor(paneId) {
|
|
36888
37695
|
const p = this.scene.panes.get(paneId);
|
|
36889
37696
|
return { top: p?.bounds.top ?? 0, height: p?.bounds.height ?? 0, rightAxis: this.rightAxisW };
|
|
@@ -39313,6 +40120,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
39313
40120
|
exports.getDrawingType = getDrawingType;
|
|
39314
40121
|
exports.getNativeIndicator = getNativeIndicator;
|
|
39315
40122
|
exports.iconMarkup = iconMarkup;
|
|
40123
|
+
exports.inputDeltas = inputDeltas;
|
|
39316
40124
|
exports.inputVisible = inputVisible;
|
|
39317
40125
|
exports.legendActions = legendActions;
|
|
39318
40126
|
exports.legendCallouts = legendCallouts;
|