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