@luxalgo/vela 0.6.11 → 0.6.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -46
- package/dist/{DataProvider-BBf-jc6W.d.ts → DataProvider-DhzstpQb.d.ts} +1 -1
- package/dist/{DataProvider-p0TEyhlX.d.cts → DataProvider-DlMtrwqM.d.cts} +1 -1
- package/dist/{chunk-MTLJKZDZ.js → chunk-F4M24ANM.js} +122 -1
- package/dist/{chunk-IO3NYSQV.js → chunk-G52XAKZY.js} +184 -25
- package/dist/{chunk-73PEA4MU.js → chunk-JKGA36ZM.js} +635 -49
- package/dist/{contributions-Bbe2R-mQ.d.ts → contributions-37nni40G.d.ts} +6 -4
- package/dist/{contributions-CO01zWve.d.cts → contributions-lPojhTxI.d.cts} +6 -4
- package/dist/index.cjs +744 -37
- package/dist/index.d.cts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +2 -2
- package/dist/{options-yp7sA96q.d.cts → options-CYS5Wmlx.d.cts} +71 -2
- package/dist/{options-yp7sA96q.d.ts → options-CYS5Wmlx.d.ts} +71 -2
- package/dist/{plugin-CkkH8QnX.d.cts → plugin-Cwikpz1m.d.cts} +10 -3
- package/dist/{plugin-DwxjM3Ni.d.ts → plugin-DHyaoMjW.d.ts} +10 -3
- package/dist/plugin.cjs +110 -0
- package/dist/plugin.d.cts +4 -4
- package/dist/plugin.d.ts +4 -4
- package/dist/plugin.js +1 -1
- package/dist/providers/binance.d.cts +2 -2
- package/dist/providers/binance.d.ts +2 -2
- package/dist/providers/coinbase.d.cts +2 -2
- package/dist/providers/coinbase.d.ts +2 -2
- package/dist/providers/hyperliquid.d.cts +2 -2
- package/dist/providers/hyperliquid.d.ts +2 -2
- package/dist/{statusline-CHPDuKNp.d.ts → statusline-model-CiJm-riV.d.cts} +7 -85
- package/dist/{statusline-DTHZUFqK.d.cts → statusline-model-D-q_OOx9.d.ts} +7 -85
- package/dist/ui.d.cts +1 -1
- package/dist/ui.d.ts +1 -1
- package/dist/vela.global.js +744 -37
- package/dist/vela.global.min.js +52 -52
- package/dist/widget.cjs +927 -61
- package/dist/widget.d.cts +118 -7
- package/dist/widget.d.ts +118 -7
- package/dist/widget.js +4 -4
- package/dist/workspace.cjs +927 -61
- package/dist/workspace.d.cts +5 -5
- package/dist/workspace.d.ts +5 -5
- package/dist/workspace.js +3 -3
- package/package.json +1 -1
package/dist/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;
|
|
@@ -7302,7 +7602,6 @@ var RUN_EMIT_THROTTLE_MS = 1e3;
|
|
|
7302
7602
|
var PREVIEW_BARS = 300;
|
|
7303
7603
|
var SINGLE_LOAD_BARS = 5e3;
|
|
7304
7604
|
var CHUNK_BARS = 1e4;
|
|
7305
|
-
var FIRST_PAINT_BARS = 100;
|
|
7306
7605
|
var GAP_FACTOR = 1.5;
|
|
7307
7606
|
var HEAL_COOLDOWN_MS = 5e3;
|
|
7308
7607
|
var EngineOrchestrator = class _EngineOrchestrator {
|
|
@@ -7420,7 +7719,13 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
7420
7719
|
const initialStyle = this.renderer.readFeature("priceStyle");
|
|
7421
7720
|
if (typeof initialStyle === "string") this.priceStyle = initialStyle;
|
|
7422
7721
|
this.barTransform = barTransformFor(initialStyle);
|
|
7423
|
-
|
|
7722
|
+
const drawingSeries = new DrawingSeriesService({
|
|
7723
|
+
fetchBars: (tf, range) => this.fetchSeries(this.config.market.symbol ?? "", tf, range),
|
|
7724
|
+
canFetch: () => !!this.feed.loadRange && !this.config.market.data?.length && !!this.config.market.symbol,
|
|
7725
|
+
chartTimeframe: () => this.config.market.timeframe ?? "60",
|
|
7726
|
+
marketKey: () => `${this.config.market.symbol ?? ""}|${this.config.market.session ?? ""}`
|
|
7727
|
+
});
|
|
7728
|
+
this.drawings = new DrawingController(this.renderer, this.events, config.drawings, drawingSeries);
|
|
7424
7729
|
this.unresolvedUnsub = this.feed.onUnresolved?.((info) => {
|
|
7425
7730
|
this.endLoad();
|
|
7426
7731
|
this.events.emit("data:unresolved", info);
|
|
@@ -7572,7 +7877,6 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
7572
7877
|
let painted = false;
|
|
7573
7878
|
const paint = (bars, final) => {
|
|
7574
7879
|
if (this.generation !== gen || !final && bars.length === 0) return;
|
|
7575
|
-
if (!painted && !final && bars.length < Math.min(requested, FIRST_PAINT_BARS)) return;
|
|
7576
7880
|
this.setBarSeries(bars, painted ? { preserveView: true } : void 0);
|
|
7577
7881
|
if (!painted && bars.length > 0) {
|
|
7578
7882
|
painted = true;
|
|
@@ -7591,10 +7895,14 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
7591
7895
|
}
|
|
7592
7896
|
};
|
|
7593
7897
|
abort.signal.addEventListener("abort", () => signal(true), { once: true });
|
|
7594
|
-
this.feed.loadProgressive(
|
|
7595
|
-
|
|
7596
|
-
|
|
7597
|
-
|
|
7898
|
+
this.feed.loadProgressive(
|
|
7899
|
+
market,
|
|
7900
|
+
(bars) => {
|
|
7901
|
+
paint(bars, false);
|
|
7902
|
+
if (painted) signal(true);
|
|
7903
|
+
},
|
|
7904
|
+
{ signal: abort.signal }
|
|
7905
|
+
).then((full) => {
|
|
7598
7906
|
if (this.progressiveAbort === abort) this.progressiveAbort = null;
|
|
7599
7907
|
if (full == null) return signal(false);
|
|
7600
7908
|
if (this.generation !== gen) return signal(true);
|
|
@@ -7681,7 +7989,14 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
7681
7989
|
* an in-flight `setMarket` immediately (the config mutates before the load). */
|
|
7682
7990
|
marketSnapshot() {
|
|
7683
7991
|
const m = this.config.market;
|
|
7684
|
-
return {
|
|
7992
|
+
return {
|
|
7993
|
+
symbol: m.symbol,
|
|
7994
|
+
provider: parseSymbol(m.symbol ?? "").provider ?? void 0,
|
|
7995
|
+
timeframe: m.timeframe,
|
|
7996
|
+
bars: m.bars,
|
|
7997
|
+
session: m.session,
|
|
7998
|
+
offline: m.data !== void 0
|
|
7999
|
+
};
|
|
7685
8000
|
}
|
|
7686
8001
|
/**
|
|
7687
8002
|
* Switch the chart's market IN PLACE — no destroy/recreate. The renderer stays
|
|
@@ -7747,10 +8062,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
7747
8062
|
if (depthOnly) {
|
|
7748
8063
|
this.extendDepth(gen, m.bars ?? 500);
|
|
7749
8064
|
} else {
|
|
7750
|
-
await Promise.race([
|
|
7751
|
-
this.loadMarket(gen, { firstLoad: false }),
|
|
7752
|
-
new Promise((resolve) => this.supersedeWaiters.push(resolve))
|
|
7753
|
-
]);
|
|
8065
|
+
await Promise.race([this.loadMarket(gen, { firstLoad: false }), new Promise((resolve) => this.supersedeWaiters.push(resolve))]);
|
|
7754
8066
|
}
|
|
7755
8067
|
if (this.generation !== gen) return;
|
|
7756
8068
|
} finally {
|
|
@@ -8413,8 +8725,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
8413
8725
|
onModel: (model) => {
|
|
8414
8726
|
const first = !record.announced;
|
|
8415
8727
|
const cause = record.pendingCause ?? "history";
|
|
8728
|
+
if (!this.applyModel(id, model)) return;
|
|
8416
8729
|
record.pendingCause = void 0;
|
|
8417
|
-
this.applyModel(id, model);
|
|
8418
8730
|
this.emitContextChanged(id);
|
|
8419
8731
|
this.emitScriptRun(id, cause, first);
|
|
8420
8732
|
},
|
|
@@ -8707,10 +9019,16 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
8707
9019
|
* Apply an emitted model. First emission mounts (and routes the pane); a pending
|
|
8708
9020
|
* structural change (after an input edit) remounts idempotently; everything else
|
|
8709
9021
|
* (live tick / viewport re-run) value-patches.
|
|
9022
|
+
*
|
|
9023
|
+
* Returns false when the model was DEFERRED — an output-free model arriving while
|
|
9024
|
+
* the record is still loading and the chart has no bars (see below); every other
|
|
9025
|
+
* outcome, including the hidden drop, returns true so the caller's event semantics
|
|
9026
|
+
* stay unchanged.
|
|
8710
9027
|
*/
|
|
8711
9028
|
applyModel(id, model) {
|
|
8712
9029
|
const record = this.registry.get(id);
|
|
8713
|
-
if (!record || record.hidden) return;
|
|
9030
|
+
if (!record || record.hidden) return true;
|
|
9031
|
+
if (record.loading && this.bars.length === 0 && !_EngineOrchestrator.modelHasOutput(model)) return false;
|
|
8714
9032
|
const handle = this.handles.get(id);
|
|
8715
9033
|
if (!record.renderHandle) {
|
|
8716
9034
|
const paneId2 = this.routePane(id, model, record.options ?? {});
|
|
@@ -8720,7 +9038,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
8720
9038
|
record.renderHandle = this.renderer.mountIndicator(model);
|
|
8721
9039
|
record.pendingStructural = false;
|
|
8722
9040
|
this.announce(record, handle);
|
|
8723
|
-
return;
|
|
9041
|
+
return true;
|
|
8724
9042
|
}
|
|
8725
9043
|
let paneId = record.model?.paneId ?? "price";
|
|
8726
9044
|
const prevOwnScale = record.model?.ownScale === true;
|
|
@@ -8748,6 +9066,11 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
8748
9066
|
}
|
|
8749
9067
|
if (record.loading) this.setLoading(record, false);
|
|
8750
9068
|
this.announce(record, handle);
|
|
9069
|
+
return true;
|
|
9070
|
+
}
|
|
9071
|
+
/** True when the model carries ANY executed output — series, drawings, bar colors, or trades. */
|
|
9072
|
+
static modelHasOutput(model) {
|
|
9073
|
+
return model.series.length > 0 || model.fills.length > 0 || model.backgrounds.length > 0 || model.priceLines.length > 0 || (model.lines?.length ?? 0) > 0 || (model.boxes?.length ?? 0) > 0 || (model.labels?.length ?? 0) > 0 || (model.polylines?.length ?? 0) > 0 || (model.linefills?.length ?? 0) > 0 || (model.tables?.length ?? 0) > 0 || (model.barColors?.length ?? 0) > 0 || (model.trades?.length ?? 0) > 0;
|
|
8751
9074
|
}
|
|
8752
9075
|
routePane(id, model, options) {
|
|
8753
9076
|
if (options.pane === "new") return `pane-${id}`;
|
|
@@ -9450,6 +9773,7 @@ function intervalMs(timeframe) {
|
|
|
9450
9773
|
"4h": 144e5,
|
|
9451
9774
|
"1d": 864e5,
|
|
9452
9775
|
"1w": 6048e5,
|
|
9776
|
+
"1M": 2592e6,
|
|
9453
9777
|
"1": 6e4,
|
|
9454
9778
|
"5": 3e5,
|
|
9455
9779
|
"15": 9e5,
|
|
@@ -9457,7 +9781,8 @@ function intervalMs(timeframe) {
|
|
|
9457
9781
|
"60": 36e5,
|
|
9458
9782
|
"240": 144e5,
|
|
9459
9783
|
D: 864e5,
|
|
9460
|
-
W: 6048e5
|
|
9784
|
+
W: 6048e5,
|
|
9785
|
+
M: 2592e6
|
|
9461
9786
|
};
|
|
9462
9787
|
return map[timeframe] ?? 36e5;
|
|
9463
9788
|
}
|
|
@@ -18644,7 +18969,9 @@ var DrawingSceneRenderer = class {
|
|
|
18644
18969
|
const lo = Math.min(from, to);
|
|
18645
18970
|
const hi = Math.max(from, to);
|
|
18646
18971
|
const visible = (a, b, extend) => {
|
|
18647
|
-
if (extend
|
|
18972
|
+
if (extend === "both") return true;
|
|
18973
|
+
if (extend === "left") return Math.max(a, b) >= lo;
|
|
18974
|
+
if (extend === "right") return Math.min(a, b) <= hi;
|
|
18648
18975
|
return Math.max(a, b) >= lo && Math.min(a, b) <= hi;
|
|
18649
18976
|
};
|
|
18650
18977
|
let min = Infinity;
|
|
@@ -21208,6 +21535,17 @@ var DrawingPainter = class {
|
|
|
21208
21535
|
constructor() {
|
|
21209
21536
|
/** The current `paintAll` call's interaction state, visible to the per-type painters. */
|
|
21210
21537
|
this.targets = {};
|
|
21538
|
+
/** The chart's active series LOOK — style + resolved series colors — pushed by the
|
|
21539
|
+
* controller before each paint. The magnifier's inset mirrors both: candles/bars/line/
|
|
21540
|
+
* area restyle the paint (bar-transform styles like Heikin Ashi transform the fetched
|
|
21541
|
+
* bars; unknown/custom styles fall back to candles), and the colors default to the main
|
|
21542
|
+
* series' own so the inset reads as a finer copy of the chart. */
|
|
21543
|
+
this.seriesLook = {
|
|
21544
|
+
style: "candles",
|
|
21545
|
+
upColor: BULLISH,
|
|
21546
|
+
downColor: BEARISH,
|
|
21547
|
+
lineColor: BULLISH
|
|
21548
|
+
};
|
|
21211
21549
|
}
|
|
21212
21550
|
/** Paint every visible drawing, then selection handles for the targeted ones.
|
|
21213
21551
|
* Each drawing is clipped to its own pane's rect (and skipped entirely while that pane
|
|
@@ -21252,6 +21590,8 @@ var DrawingPainter = class {
|
|
|
21252
21590
|
ctx.globalAlpha = GHOST_ALPHA;
|
|
21253
21591
|
if (ghost instanceof RegressionChannel || ghost instanceof FixedRangeVolumeProfile) {
|
|
21254
21592
|
this.paintTimeSpanGhost(ctx, ghost, proj);
|
|
21593
|
+
} else if (ghost instanceof Magnifier) {
|
|
21594
|
+
this.paintMagnifierGhost(ctx, ghost, proj, theme);
|
|
21255
21595
|
} else this.paintOne(ctx, ghost, proj, theme);
|
|
21256
21596
|
ctx.globalAlpha = 1;
|
|
21257
21597
|
}
|
|
@@ -21385,6 +21725,10 @@ var DrawingPainter = class {
|
|
|
21385
21725
|
this.paintLabel(ctx, d, proj, theme);
|
|
21386
21726
|
return;
|
|
21387
21727
|
}
|
|
21728
|
+
if (d instanceof Magnifier) {
|
|
21729
|
+
this.paintMagnifier(ctx, d, proj, theme);
|
|
21730
|
+
return;
|
|
21731
|
+
}
|
|
21388
21732
|
if (d instanceof PatternDrawing) {
|
|
21389
21733
|
this.paintPattern(ctx, d, proj, theme);
|
|
21390
21734
|
return;
|
|
@@ -21877,6 +22221,216 @@ var DrawingPainter = class {
|
|
|
21877
22221
|
ctx.textBaseline = "alphabetic";
|
|
21878
22222
|
}
|
|
21879
22223
|
}
|
|
22224
|
+
/** Placement preview for the magnifier: a dashed rectangle outline only — no backdrop and
|
|
22225
|
+
* no series read, so dragging the area open never kicks a fetch per cursor move. */
|
|
22226
|
+
paintMagnifierGhost(ctx, d, proj, theme) {
|
|
22227
|
+
const r = d.rect(proj);
|
|
22228
|
+
if (!r) return;
|
|
22229
|
+
ctx.save();
|
|
22230
|
+
ctx.strokeStyle = d.style.lineColor || contrastColor(theme.background);
|
|
22231
|
+
ctx.lineWidth = 1;
|
|
22232
|
+
ctx.setLineDash([4, 4]);
|
|
22233
|
+
ctx.strokeRect(Math.min(r.x1, r.x2), Math.min(r.y1, r.y2), Math.abs(r.x2 - r.x1), Math.abs(r.y2 - r.y1));
|
|
22234
|
+
ctx.restore();
|
|
22235
|
+
}
|
|
22236
|
+
/** Paint a magnifier: an opaque theme-background inset whose interior shows the chart's
|
|
22237
|
+
* market at a finer timeframe — candles at their true time/price positions, clipped to
|
|
22238
|
+
* the rectangle. Bars come through `Projector.seriesInRange` (cache read; `loading` and
|
|
22239
|
+
* `unavailable` states paint a short notice instead). The lower-timeframe candles shift
|
|
22240
|
+
* half a chart bar LEFT of their raw time pixel so each chart candle's visual cell —
|
|
22241
|
+
* centered on its open time — subdivides in place. */
|
|
22242
|
+
paintMagnifier(ctx, d, proj, theme) {
|
|
22243
|
+
const r = d.rect(proj);
|
|
22244
|
+
const a = d.anchors[0];
|
|
22245
|
+
const b = d.anchors[1];
|
|
22246
|
+
if (!r || !a || !b) return;
|
|
22247
|
+
const x0 = Math.min(r.x1, r.x2);
|
|
22248
|
+
const x1 = Math.max(r.x1, r.x2);
|
|
22249
|
+
const y0 = Math.min(r.y1, r.y2);
|
|
22250
|
+
const y1 = Math.max(r.y1, r.y2);
|
|
22251
|
+
const w = x1 - x0;
|
|
22252
|
+
const h = y1 - y0;
|
|
22253
|
+
ctx.save();
|
|
22254
|
+
ctx.globalAlpha = 1;
|
|
22255
|
+
ctx.fillStyle = theme.background;
|
|
22256
|
+
ctx.fillRect(x0, y0, w, h);
|
|
22257
|
+
ctx.restore();
|
|
22258
|
+
const from = Math.min(a.time, b.time);
|
|
22259
|
+
const to = Math.max(a.time, b.time);
|
|
22260
|
+
const chartBars = proj.barsBetween ? proj.barsBetween(from, to) : 0;
|
|
22261
|
+
const chartMs = chartBars > 0 ? (to - from) / chartBars : 0;
|
|
22262
|
+
const res = proj.seriesInRange && chartMs > 0 && w > 1 && h > 1 ? proj.seriesInRange(d.magnifier.timeframe, from, to + chartMs) : void 0;
|
|
22263
|
+
let seriesBars = res?.state === "ready" || res?.state === "loading" ? res.bars ?? [] : [];
|
|
22264
|
+
if (res && (res.state === "ready" || res.state === "loading") && seriesBars.length > 0) {
|
|
22265
|
+
const look = this.seriesLook;
|
|
22266
|
+
const transform = look.style !== "candles" ? barTransformFor(look.style) : null;
|
|
22267
|
+
if (transform) seriesBars = transform.full(seriesBars);
|
|
22268
|
+
const mode = look.style === "bars" ? "bars" : look.style === "line" || look.style === "baseline" ? "line" : look.style === "area" ? "area" : "candles";
|
|
22269
|
+
const halfPitch = (proj.xOf(from + chartMs) - proj.xOf(from)) / 2;
|
|
22270
|
+
ctx.save();
|
|
22271
|
+
ctx.beginPath();
|
|
22272
|
+
ctx.rect(x0, y0, w, h);
|
|
22273
|
+
ctx.clip();
|
|
22274
|
+
if (mode === "line" || mode === "area") {
|
|
22275
|
+
this.paintMagnifierLine(ctx, d, proj, seriesBars, res.barMs, halfPitch, y1, mode === "area", d.magnifier.upColor || look.lineColor);
|
|
22276
|
+
} else {
|
|
22277
|
+
this.paintMagnifierBars(ctx, d, proj, seriesBars, res.barMs, halfPitch, x0, x1, mode, d.magnifier.upColor || look.upColor, d.magnifier.downColor || look.downColor);
|
|
22278
|
+
}
|
|
22279
|
+
ctx.restore();
|
|
22280
|
+
} else if (res) {
|
|
22281
|
+
const notice = res.state === "loading" ? "Loading\u2026" : res.state === "ready" ? "No lower-timeframe data" : res.reason === "too-wide" ? "Area too wide for this timeframe" : res.reason === "none-lower" ? "No lower timeframe available" : res.reason === "not-lower" ? "Pick a timeframe below the chart" : "No data source";
|
|
22282
|
+
this.paintMagnifierNotice(ctx, notice, x0, y0, w, h, theme);
|
|
22283
|
+
}
|
|
22284
|
+
ctx.save();
|
|
22285
|
+
ctx.strokeStyle = d.style.lineColor || contrastColor(theme.background);
|
|
22286
|
+
ctx.lineWidth = d.style.lineWidth || 1;
|
|
22287
|
+
ctx.setLineDash(dashPattern(d.style.lineStyle, d.style.lineWidth || 1));
|
|
22288
|
+
ctx.strokeRect(x0, y0, w, h);
|
|
22289
|
+
ctx.restore();
|
|
22290
|
+
if (w > 44) {
|
|
22291
|
+
const label = magnifierTimeframeLabel(res?.state === "ready" || res?.state === "loading" ? res.timeframe : d.magnifier.timeframe);
|
|
22292
|
+
const chipH = 17;
|
|
22293
|
+
const gap = 4;
|
|
22294
|
+
const pane = proj.paneRect?.(d.paneId);
|
|
22295
|
+
const paneBottom = pane ? pane.top + pane.height : proj.height;
|
|
22296
|
+
const below = y1 + gap + chipH <= paneBottom;
|
|
22297
|
+
const chipY = below ? y1 + gap : y1 - gap - chipH;
|
|
22298
|
+
ctx.save();
|
|
22299
|
+
ctx.font = `10px ${theme.fontFamily}`;
|
|
22300
|
+
const tw = ctx.measureText(label).width;
|
|
22301
|
+
const caretW = 11;
|
|
22302
|
+
const chipW = tw + 12 + caretW;
|
|
22303
|
+
roundRect(ctx, x0, chipY, chipW, chipH, 3);
|
|
22304
|
+
ctx.fillStyle = theme.background;
|
|
22305
|
+
ctx.fill();
|
|
22306
|
+
ctx.strokeStyle = withAlpha(theme.textColor, 0.28);
|
|
22307
|
+
ctx.lineWidth = 1;
|
|
22308
|
+
ctx.setLineDash([]);
|
|
22309
|
+
ctx.stroke();
|
|
22310
|
+
ctx.fillStyle = theme.textColor;
|
|
22311
|
+
ctx.textAlign = "left";
|
|
22312
|
+
ctx.textBaseline = "middle";
|
|
22313
|
+
ctx.fillText(label, x0 + 6, chipY + chipH / 2 + 0.5);
|
|
22314
|
+
const cxr = x0 + 6 + tw + 5;
|
|
22315
|
+
const cyr = chipY + chipH / 2;
|
|
22316
|
+
ctx.strokeStyle = withAlpha(theme.textColor, 0.7);
|
|
22317
|
+
ctx.lineWidth = 1.2;
|
|
22318
|
+
ctx.beginPath();
|
|
22319
|
+
ctx.moveTo(cxr, cyr - 1.5);
|
|
22320
|
+
ctx.lineTo(cxr + 2.5, cyr + 1.5);
|
|
22321
|
+
ctx.lineTo(cxr + 5, cyr - 1.5);
|
|
22322
|
+
ctx.stroke();
|
|
22323
|
+
ctx.restore();
|
|
22324
|
+
d.chipRect = { x: x0, y: chipY, w: chipW, h: chipH };
|
|
22325
|
+
} else {
|
|
22326
|
+
d.chipRect = null;
|
|
22327
|
+
}
|
|
22328
|
+
}
|
|
22329
|
+
/** The magnifier's candle/bar loop: each bar's cell spans its open→close time (shifted left
|
|
22330
|
+
* by half a chart bar). Candles: wick always, body once the cell is wide enough to carry
|
|
22331
|
+
* one. OHLC bars: the high–low spine with open/close ticks once the cell has the room. */
|
|
22332
|
+
paintMagnifierBars(ctx, d, proj, bars, barMs, halfPitch, x0, x1, mode, upColor, downColor) {
|
|
22333
|
+
ctx.setLineDash([]);
|
|
22334
|
+
ctx.lineWidth = 1;
|
|
22335
|
+
for (const bar of bars) {
|
|
22336
|
+
const cx0 = proj.xOf(bar.time) - halfPitch;
|
|
22337
|
+
const cx1 = proj.xOf(bar.time + barMs) - halfPitch;
|
|
22338
|
+
if (cx1 < x0 || cx0 > x1) continue;
|
|
22339
|
+
const yHigh = proj.yOf(bar.high, d.paneId);
|
|
22340
|
+
const yLow = proj.yOf(bar.low, d.paneId);
|
|
22341
|
+
const yOpen = proj.yOf(bar.open, d.paneId);
|
|
22342
|
+
const yClose = proj.yOf(bar.close, d.paneId);
|
|
22343
|
+
if (yHigh == null || yLow == null || yOpen == null || yClose == null) continue;
|
|
22344
|
+
const color = bar.close >= bar.open ? upColor : downColor;
|
|
22345
|
+
const cellW = cx1 - cx0;
|
|
22346
|
+
const cx = (cx0 + cx1) / 2;
|
|
22347
|
+
ctx.strokeStyle = color;
|
|
22348
|
+
ctx.beginPath();
|
|
22349
|
+
ctx.moveTo(cx, yHigh);
|
|
22350
|
+
ctx.lineTo(cx, yLow);
|
|
22351
|
+
ctx.stroke();
|
|
22352
|
+
if (cellW < 3) continue;
|
|
22353
|
+
if (mode === "bars") {
|
|
22354
|
+
const tick = Math.max(1, cellW * 0.35);
|
|
22355
|
+
ctx.beginPath();
|
|
22356
|
+
ctx.moveTo(cx - tick, yOpen);
|
|
22357
|
+
ctx.lineTo(cx, yOpen);
|
|
22358
|
+
ctx.moveTo(cx, yClose);
|
|
22359
|
+
ctx.lineTo(cx + tick, yClose);
|
|
22360
|
+
ctx.stroke();
|
|
22361
|
+
} else {
|
|
22362
|
+
const bw = Math.max(1, cellW * 0.7);
|
|
22363
|
+
ctx.fillStyle = color;
|
|
22364
|
+
ctx.fillRect(cx - bw / 2, Math.min(yOpen, yClose), bw, Math.max(1, Math.abs(yClose - yOpen)));
|
|
22365
|
+
}
|
|
22366
|
+
}
|
|
22367
|
+
}
|
|
22368
|
+
/** The magnifier's line/area rendering: a close polyline through each cell's center (same
|
|
22369
|
+
* half-chart-bar shift as the candles), with an optional translucent fill down to the
|
|
22370
|
+
* rectangle's bottom edge for the area style. Colored like the chart's own line series. */
|
|
22371
|
+
paintMagnifierLine(ctx, d, proj, bars, barMs, halfPitch, yBottom, area, color) {
|
|
22372
|
+
const pts = [];
|
|
22373
|
+
for (const bar of bars) {
|
|
22374
|
+
const y = proj.yOf(bar.close, d.paneId);
|
|
22375
|
+
if (y == null) continue;
|
|
22376
|
+
pts.push([proj.xOf(bar.time + barMs / 2) - halfPitch, y]);
|
|
22377
|
+
}
|
|
22378
|
+
if (pts.length < 2) return;
|
|
22379
|
+
if (area) {
|
|
22380
|
+
ctx.beginPath();
|
|
22381
|
+
ctx.moveTo(pts[0][0], yBottom);
|
|
22382
|
+
for (const [px, py] of pts) ctx.lineTo(px, py);
|
|
22383
|
+
ctx.lineTo(pts[pts.length - 1][0], yBottom);
|
|
22384
|
+
ctx.closePath();
|
|
22385
|
+
ctx.fillStyle = withAlpha(color, 0.15);
|
|
22386
|
+
ctx.fill();
|
|
22387
|
+
}
|
|
22388
|
+
ctx.setLineDash([]);
|
|
22389
|
+
ctx.lineWidth = 1.5;
|
|
22390
|
+
ctx.strokeStyle = color;
|
|
22391
|
+
ctx.beginPath();
|
|
22392
|
+
ctx.moveTo(pts[0][0], pts[0][1]);
|
|
22393
|
+
for (let i = 1; i < pts.length; i += 1) ctx.lineTo(pts[i][0], pts[i][1]);
|
|
22394
|
+
ctx.stroke();
|
|
22395
|
+
}
|
|
22396
|
+
/** Centered muted notice inside the magnifier rect (loading / unavailable states). */
|
|
22397
|
+
paintMagnifierNotice(ctx, text, x0, y0, w, h, theme) {
|
|
22398
|
+
if (w < 60 || h < 20) return;
|
|
22399
|
+
ctx.save();
|
|
22400
|
+
ctx.beginPath();
|
|
22401
|
+
ctx.rect(x0, y0, w, h);
|
|
22402
|
+
ctx.clip();
|
|
22403
|
+
ctx.font = `11px ${theme.fontFamily}`;
|
|
22404
|
+
ctx.fillStyle = withAlpha(theme.textColor, 0.55);
|
|
22405
|
+
ctx.textAlign = "center";
|
|
22406
|
+
ctx.textBaseline = "middle";
|
|
22407
|
+
ctx.fillText(text, x0 + w / 2, y0 + h / 2);
|
|
22408
|
+
ctx.restore();
|
|
22409
|
+
}
|
|
22410
|
+
/** A bottom-center pill prompting the armed tool's placement gesture (e.g. the magnifier's
|
|
22411
|
+
* "drag an area"). Painted by the drawings layer while the tool is armed and no placement
|
|
22412
|
+
* is in progress; chart-background fill so it reads as chrome over any content. */
|
|
22413
|
+
paintPlacementHint(ctx, text, theme, width, height) {
|
|
22414
|
+
ctx.save();
|
|
22415
|
+
ctx.font = `11px ${theme.fontFamily}`;
|
|
22416
|
+
const tw = ctx.measureText(text).width;
|
|
22417
|
+
const pillW = tw + 24;
|
|
22418
|
+
const pillH = 24;
|
|
22419
|
+
const x = (width - pillW) / 2;
|
|
22420
|
+
const y = height - pillH - 14;
|
|
22421
|
+
roundRect(ctx, x, y, pillW, pillH, pillH / 2);
|
|
22422
|
+
ctx.fillStyle = theme.background;
|
|
22423
|
+
ctx.fill();
|
|
22424
|
+
ctx.strokeStyle = withAlpha(theme.textColor, 0.28);
|
|
22425
|
+
ctx.lineWidth = 1;
|
|
22426
|
+
ctx.setLineDash([]);
|
|
22427
|
+
ctx.stroke();
|
|
22428
|
+
ctx.fillStyle = theme.textColor;
|
|
22429
|
+
ctx.textAlign = "center";
|
|
22430
|
+
ctx.textBaseline = "middle";
|
|
22431
|
+
ctx.fillText(text, width / 2, y + pillH / 2 + 0.5);
|
|
22432
|
+
ctx.restore();
|
|
22433
|
+
}
|
|
21880
22434
|
/** Paint a fixed-range volume profile: horizontal histogram rows (up/down split) anchored to
|
|
21881
22435
|
* the left or right of the time span, optional VAH / VAL / POC levels across the range, and
|
|
21882
22436
|
* optional developing POC / VA polylines. Recomputes from the two anchors on every paint. */
|
|
@@ -23292,8 +23846,9 @@ function ensureStyles3() {
|
|
|
23292
23846
|
if (!existing) document.head.appendChild(s);
|
|
23293
23847
|
}
|
|
23294
23848
|
var DrawingSettingsPopup = class {
|
|
23295
|
-
constructor(host, theme) {
|
|
23849
|
+
constructor(host, theme, chartBarMs = () => 0) {
|
|
23296
23850
|
this.host = host;
|
|
23851
|
+
this.chartBarMs = chartBarMs;
|
|
23297
23852
|
this.el = null;
|
|
23298
23853
|
this.tipEl = null;
|
|
23299
23854
|
// floating hover-label (above/below the toolbar)
|
|
@@ -23318,6 +23873,14 @@ var DrawingSettingsPopup = class {
|
|
|
23318
23873
|
this.theme = theme;
|
|
23319
23874
|
this.settingsDialog = new DrawingSettingsDialog(host, theme);
|
|
23320
23875
|
}
|
|
23876
|
+
/** The magnifier timeframe choices strictly below the chart's own bar duration
|
|
23877
|
+
* (`auto` rides along while at least one concrete lower step exists). */
|
|
23878
|
+
lowerTimeframeOptions() {
|
|
23879
|
+
const chartMs = this.chartBarMs();
|
|
23880
|
+
if (!(chartMs > 0)) return [...MAGNIFIER_TIMEFRAME_OPTIONS];
|
|
23881
|
+
const lower = MAGNIFIER_TIMEFRAME_OPTIONS.filter((o) => o.ms > 0 && o.ms < chartMs);
|
|
23882
|
+
return lower.length > 0 ? [MAGNIFIER_TIMEFRAME_OPTIONS[0], ...lower] : [];
|
|
23883
|
+
}
|
|
23321
23884
|
setTheme(theme) {
|
|
23322
23885
|
this.theme = theme;
|
|
23323
23886
|
this.settingsDialog.setTheme(theme);
|
|
@@ -23360,7 +23923,20 @@ var DrawingSettingsPopup = class {
|
|
|
23360
23923
|
const sz = drawing.size ?? "normal";
|
|
23361
23924
|
bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label: sizeLabel }));
|
|
23362
23925
|
}
|
|
23363
|
-
if (paths.has("
|
|
23926
|
+
if (paths.has("magnifier.timeframe") && drawing instanceof Magnifier) {
|
|
23927
|
+
const options = this.lowerTimeframeOptions();
|
|
23928
|
+
if (options.length > 0) {
|
|
23929
|
+
bar.appendChild(
|
|
23930
|
+
this.dropdown("Lower timeframe", options.map((o) => o.value), drawing.magnifier.timeframe, () => "", (v) => actions.patch({ "magnifier.timeframe": v }), {
|
|
23931
|
+
label: (v) => magnifierTimeframeLabel(String(v)),
|
|
23932
|
+
labelInTrigger: true
|
|
23933
|
+
})
|
|
23934
|
+
);
|
|
23935
|
+
}
|
|
23936
|
+
bar.appendChild(this.colorButton("Up candles", BUCKET_ICON, drawing.magnifier.upColor || t.upColor, (v) => actions.patch({ "magnifier.upColor": v })));
|
|
23937
|
+
bar.appendChild(this.colorButton("Down candles", BUCKET_ICON, drawing.magnifier.downColor || t.downColor, (v) => actions.patch({ "magnifier.downColor": v })));
|
|
23938
|
+
}
|
|
23939
|
+
if (paths.has("style.lineColor")) bar.appendChild(this.colorButton("Line color", BRUSH_ICON, drawing.style.lineColor || (drawing instanceof Magnifier ? contrastColor(this.theme.background) : DEFAULT_DRAWING_COLOR), (v) => actions.patch({ "style.lineColor": v })));
|
|
23364
23940
|
if (paths.has("style.lineWidth")) {
|
|
23365
23941
|
const wf = schema.fields.find((f) => f.path === "style.lineWidth");
|
|
23366
23942
|
if (wf?.kind === "number" && (wf.min ?? 1) > 1) {
|
|
@@ -23682,6 +24258,7 @@ var DrawingSettingsPopup = class {
|
|
|
23682
24258
|
this.colorPop = null;
|
|
23683
24259
|
this.colorOwner = null;
|
|
23684
24260
|
}
|
|
24261
|
+
opts.onClose?.();
|
|
23685
24262
|
}
|
|
23686
24263
|
});
|
|
23687
24264
|
const el = pop.el;
|
|
@@ -23700,6 +24277,49 @@ var DrawingSettingsPopup = class {
|
|
|
23700
24277
|
pop.show();
|
|
23701
24278
|
return pop;
|
|
23702
24279
|
}
|
|
24280
|
+
/**
|
|
24281
|
+
* A standalone timeframe menu for the magnifier's ON-CHART chip. The chip lives on
|
|
24282
|
+
* canvas, so a transient invisible anchor is dropped at its pixel rect for the popover
|
|
24283
|
+
* to position against, and removed again when the menu closes. Independent of the
|
|
24284
|
+
* quick toolbar — the chip works without selecting the drawing first.
|
|
24285
|
+
*/
|
|
24286
|
+
openMagnifierTimeframeMenu(rect, current, onPick) {
|
|
24287
|
+
ensureStyles3();
|
|
24288
|
+
closeOpenPopovers();
|
|
24289
|
+
const options = this.lowerTimeframeOptions();
|
|
24290
|
+
const anchor = document.createElement("div");
|
|
24291
|
+
anchor.style.cssText = `position:absolute;left:${rect.x}px;top:${rect.y}px;width:${rect.w}px;height:${rect.h}px;pointer-events:none;`;
|
|
24292
|
+
this.host.appendChild(anchor);
|
|
24293
|
+
this.menuPop = this.hostFloat(anchor, {
|
|
24294
|
+
zIndex: 26,
|
|
24295
|
+
padding: "4px",
|
|
24296
|
+
onClose: () => anchor.remove(),
|
|
24297
|
+
fill: (menu2, pop) => {
|
|
24298
|
+
if (options.length === 0) {
|
|
24299
|
+
const note = document.createElement("div");
|
|
24300
|
+
note.style.cssText = "padding:6px 10px;opacity:0.65;white-space:nowrap;";
|
|
24301
|
+
note.textContent = "No lower timeframe available";
|
|
24302
|
+
menu2.appendChild(note);
|
|
24303
|
+
return;
|
|
24304
|
+
}
|
|
24305
|
+
for (const o of options) {
|
|
24306
|
+
const item = document.createElement("button");
|
|
24307
|
+
item.type = "button";
|
|
24308
|
+
item.className = "vela-dpop-item";
|
|
24309
|
+
item.dataset.active = o.value === current ? "1" : "0";
|
|
24310
|
+
item.style.cssText = "display:flex;align-items:center;min-width:88px;padding:5px 10px;border:none;border-radius:5px;color:inherit;cursor:pointer;text-align:left;font:inherit;font-variant-numeric:tabular-nums;";
|
|
24311
|
+
item.textContent = o.label;
|
|
24312
|
+
item.addEventListener("click", (e) => {
|
|
24313
|
+
e.stopPropagation();
|
|
24314
|
+
pop.hide();
|
|
24315
|
+
onPick(o.value);
|
|
24316
|
+
});
|
|
24317
|
+
menu2.appendChild(item);
|
|
24318
|
+
}
|
|
24319
|
+
}
|
|
24320
|
+
});
|
|
24321
|
+
this.menuOwner = anchor;
|
|
24322
|
+
}
|
|
23703
24323
|
/** A floating list of one-shot actions (icon + label rows) opened by the kebab. */
|
|
23704
24324
|
openActionMenu(anchor, rows) {
|
|
23705
24325
|
this.menuPop = this.hostFloat(anchor, {
|
|
@@ -23810,10 +24430,13 @@ var DrawingSettingsPopup = class {
|
|
|
23810
24430
|
let cur = current;
|
|
23811
24431
|
const paint = (v) => {
|
|
23812
24432
|
b.replaceChildren();
|
|
23813
|
-
const
|
|
23814
|
-
|
|
23815
|
-
|
|
23816
|
-
|
|
24433
|
+
const glyph = render(v);
|
|
24434
|
+
if (glyph) {
|
|
24435
|
+
const ic = document.createElement("span");
|
|
24436
|
+
ic.style.cssText = "display:flex;";
|
|
24437
|
+
ic.innerHTML = sized(glyph);
|
|
24438
|
+
b.appendChild(ic);
|
|
24439
|
+
}
|
|
23817
24440
|
if (opts.label && opts.labelInTrigger) {
|
|
23818
24441
|
const tx = document.createElement("span");
|
|
23819
24442
|
tx.textContent = opts.label(v);
|
|
@@ -23855,10 +24478,13 @@ var DrawingSettingsPopup = class {
|
|
|
23855
24478
|
item.className = "vela-dpop-item";
|
|
23856
24479
|
item.dataset.active = active ? "1" : "0";
|
|
23857
24480
|
item.style.cssText = `display:flex;align-items:center;gap:8px;${label ? "min-width:118px;" : ""}padding:5px 8px;border:none;border-radius:5px;color:inherit;cursor:pointer;text-align:left;font:inherit;`;
|
|
23858
|
-
const
|
|
23859
|
-
|
|
23860
|
-
|
|
23861
|
-
|
|
24481
|
+
const glyph = render(v);
|
|
24482
|
+
if (glyph) {
|
|
24483
|
+
const ic = document.createElement("span");
|
|
24484
|
+
ic.style.cssText = "display:flex;flex:none;width:22px;justify-content:center;";
|
|
24485
|
+
ic.innerHTML = sized(glyph, 18);
|
|
24486
|
+
item.appendChild(ic);
|
|
24487
|
+
}
|
|
23862
24488
|
if (label) {
|
|
23863
24489
|
const tx = document.createElement("span");
|
|
23864
24490
|
tx.textContent = label(v);
|
|
@@ -24842,6 +25468,9 @@ var UserDrawingController = class {
|
|
|
24842
25468
|
this.intentCb = null;
|
|
24843
25469
|
/** Another chart's in-progress placement, mirrored here as a ghost (drawings sync). */
|
|
24844
25470
|
this.externalGhost = null;
|
|
25471
|
+
/** Core-pushed series gateway (finer-timeframe bars for data-driven drawings). */
|
|
25472
|
+
this.seriesGw = null;
|
|
25473
|
+
this.seriesGwUnsub = null;
|
|
24845
25474
|
/** Last draft fingerprint reported upstream — gates the per-render emission to actual changes. */
|
|
24846
25475
|
this.lastDraftKey = null;
|
|
24847
25476
|
this.measure = new MeasureOverlay();
|
|
@@ -24869,7 +25498,7 @@ var UserDrawingController = class {
|
|
|
24869
25498
|
this.textEditor = null;
|
|
24870
25499
|
this.painter = new DrawingPainter();
|
|
24871
25500
|
this.ctx = canvas.getContext("2d");
|
|
24872
|
-
this.popup = new DrawingSettingsPopup(overlayHost, deps.theme());
|
|
25501
|
+
this.popup = new DrawingSettingsPopup(overlayHost, deps.theme(), () => deps.chartBarMs());
|
|
24873
25502
|
this.toolbar = new DrawingToolbar(
|
|
24874
25503
|
toolbarHost,
|
|
24875
25504
|
deps.theme(),
|
|
@@ -24927,6 +25556,22 @@ var UserDrawingController = class {
|
|
|
24927
25556
|
const shown = this.toolbarVisible && !this.mobileLayout;
|
|
24928
25557
|
this.deps.setToolbarGutter(shown ? this.toolbarCollapsed ? TOOLBAR_COLLAPSED_WIDTH : TOOLBAR_WIDTH : 0);
|
|
24929
25558
|
}
|
|
25559
|
+
/** Core push: the series gateway data-driven drawings read finer-timeframe bars
|
|
25560
|
+
* through (surfaced to them as `Projector.seriesInRange`). A landed background
|
|
25561
|
+
* fetch repaints both this layer and the interleave slices under the series. */
|
|
25562
|
+
setSeriesGateway(gateway) {
|
|
25563
|
+
this.seriesGwUnsub?.();
|
|
25564
|
+
this.seriesGw = gateway;
|
|
25565
|
+
this.seriesGwUnsub = gateway.onUpdate(() => {
|
|
25566
|
+
this.invalidateSlices();
|
|
25567
|
+
this.render();
|
|
25568
|
+
this.deps.requestDataPaint();
|
|
25569
|
+
});
|
|
25570
|
+
}
|
|
25571
|
+
/** The pushed series gateway, or null before the core provides one. */
|
|
25572
|
+
get seriesGateway() {
|
|
25573
|
+
return this.seriesGw;
|
|
25574
|
+
}
|
|
24930
25575
|
/** Core push: mirror (or clear) another chart's in-progress placement as a ghost. */
|
|
24931
25576
|
setExternalGhost(doc) {
|
|
24932
25577
|
this.externalGhost = doc ? deserializeDrawing(doc) : null;
|
|
@@ -25044,8 +25689,34 @@ var UserDrawingController = class {
|
|
|
25044
25689
|
/** Should the drawing layer win this press (vs pan)? */
|
|
25045
25690
|
claim(x, y) {
|
|
25046
25691
|
if (this.measureMode || this.eraserMode) return true;
|
|
25692
|
+
if (this.magnifierChipAt(x, y)) return true;
|
|
25047
25693
|
return this.interaction.claim(x, y);
|
|
25048
25694
|
}
|
|
25695
|
+
/** The topmost visible (unlocked) magnifier whose timeframe chip contains (x, y) —
|
|
25696
|
+
* the chip's rect is what the painter measured last frame. */
|
|
25697
|
+
magnifierChipAt(x, y) {
|
|
25698
|
+
for (let i = this.drawings.length - 1; i >= 0; i -= 1) {
|
|
25699
|
+
const d = this.drawings[i];
|
|
25700
|
+
if (!(d instanceof Magnifier) || !d.visible || d.locked) continue;
|
|
25701
|
+
const r = d.chipRect;
|
|
25702
|
+
if (r && x >= r.x && x <= r.x + r.w && y >= r.y && y <= r.y + r.h) return d;
|
|
25703
|
+
}
|
|
25704
|
+
return null;
|
|
25705
|
+
}
|
|
25706
|
+
/** Open the on-chart chip's timeframe menu; the pick patches the drawing like a
|
|
25707
|
+
* settings-popup edit (same intent, same undo step). */
|
|
25708
|
+
openMagnifierChipMenu(drawing) {
|
|
25709
|
+
const rect = drawing.chipRect;
|
|
25710
|
+
if (!rect) return;
|
|
25711
|
+
const id = drawing.id;
|
|
25712
|
+
this.popup.openMagnifierTimeframeMenu(rect, drawing.magnifier.timeframe, (value) => {
|
|
25713
|
+
const d = this.drawings.find((x) => x.id === id);
|
|
25714
|
+
if (!(d instanceof Magnifier)) return;
|
|
25715
|
+
d.applySettings({ "magnifier.timeframe": value });
|
|
25716
|
+
this.render();
|
|
25717
|
+
this.emit({ kind: "edit", doc: d.serialize() });
|
|
25718
|
+
});
|
|
25719
|
+
}
|
|
25049
25720
|
/** Delete the (unlocked) drawing under the cursor. True when one was removed.
|
|
25050
25721
|
* Shared by the eraser (click + drag) and the middle-click shortcut. */
|
|
25051
25722
|
deleteAt(x, y) {
|
|
@@ -25091,6 +25762,13 @@ var UserDrawingController = class {
|
|
|
25091
25762
|
this.render();
|
|
25092
25763
|
return;
|
|
25093
25764
|
}
|
|
25765
|
+
if (this.activeTool == null) {
|
|
25766
|
+
const chipOwner = this.magnifierChipAt(x, y);
|
|
25767
|
+
if (chipOwner) {
|
|
25768
|
+
this.openMagnifierChipMenu(chipOwner);
|
|
25769
|
+
return;
|
|
25770
|
+
}
|
|
25771
|
+
}
|
|
25094
25772
|
this.interaction.down(x, y, snap, shift);
|
|
25095
25773
|
}
|
|
25096
25774
|
pointerMove(x, y, snap = "off", shift = false) {
|
|
@@ -25309,6 +25987,7 @@ var UserDrawingController = class {
|
|
|
25309
25987
|
/** Cursor hint while hovering — `'pointer'` over a drawing/handle, else null. */
|
|
25310
25988
|
cursorAt(x, y) {
|
|
25311
25989
|
if (this.eraserMode) return "pointer";
|
|
25990
|
+
if (this.activeTool == null && this.magnifierChipAt(x, y)) return "pointer";
|
|
25312
25991
|
return this.interaction.cursorAt(x, y);
|
|
25313
25992
|
}
|
|
25314
25993
|
/** Right-click: an explicit escape back to the pointer. Cancels an in-progress
|
|
@@ -25487,6 +26166,7 @@ var UserDrawingController = class {
|
|
|
25487
26166
|
if (!sctx) continue;
|
|
25488
26167
|
sctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
25489
26168
|
sctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
|
|
26169
|
+
this.painter.seriesLook = this.deps.seriesLook();
|
|
25490
26170
|
this.painter.paintAll(sctx, drawings, proj, theme, EMPTY_TARGETS);
|
|
25491
26171
|
const slices = out.get(paneId) ?? [];
|
|
25492
26172
|
slices.push({ beforeZ, canvas });
|
|
@@ -25514,6 +26194,7 @@ var UserDrawingController = class {
|
|
|
25514
26194
|
dragged: this.interaction.activeDragId(),
|
|
25515
26195
|
mutedLabel: edited instanceof TextLabel ? edited.id : null
|
|
25516
26196
|
};
|
|
26197
|
+
this.painter.seriesLook = this.deps.seriesLook();
|
|
25517
26198
|
this.painter.paintAll(ctx, this.drawings.filter((d) => !this.isInterleaved(d)), proj, this.deps.theme(), targets);
|
|
25518
26199
|
this.painter.paintHighlights(ctx, this.drawings.filter((d) => this.isInterleaved(d)), proj, handleIdsFor(targets));
|
|
25519
26200
|
this.layoutTextEditor();
|
|
@@ -25521,6 +26202,10 @@ var UserDrawingController = class {
|
|
|
25521
26202
|
if (ghost) this.painter.paintGhost(ctx, ghost, proj, this.deps.theme());
|
|
25522
26203
|
if (this.externalGhost) this.painter.paintGhost(ctx, this.externalGhost, proj, this.deps.theme());
|
|
25523
26204
|
this.emitDraft(ghost);
|
|
26205
|
+
if (this.activeTool && !ghost) {
|
|
26206
|
+
const hint = getDrawingType(this.activeTool)?.placementHint;
|
|
26207
|
+
if (hint) this.painter.paintPlacementHint(ctx, hint, this.deps.theme(), proj.width, proj.height);
|
|
26208
|
+
}
|
|
25524
26209
|
const markers = this.interaction.placingMarkers(proj);
|
|
25525
26210
|
if (markers) this.painter.paintHandles(ctx, markers);
|
|
25526
26211
|
const m = this.interaction.snapMarker();
|
|
@@ -25532,6 +26217,9 @@ var UserDrawingController = class {
|
|
|
25532
26217
|
this.closeTextEditor();
|
|
25533
26218
|
this.popup.destroy();
|
|
25534
26219
|
this.toolbar.destroy();
|
|
26220
|
+
this.seriesGwUnsub?.();
|
|
26221
|
+
this.seriesGwUnsub = null;
|
|
26222
|
+
this.seriesGw = null;
|
|
25535
26223
|
this.intentCb = null;
|
|
25536
26224
|
this.drawings = [];
|
|
25537
26225
|
}
|
|
@@ -25929,7 +26617,7 @@ function mergeSlices(indicator, user) {
|
|
|
25929
26617
|
}
|
|
25930
26618
|
|
|
25931
26619
|
// src/renderers/native/drawings/Projector.ts
|
|
25932
|
-
function createProjector(coords, paneOf, paneIdAtY, barsInRange) {
|
|
26620
|
+
function createProjector(coords, paneOf, paneIdAtY, barsInRange, seriesInRange) {
|
|
25933
26621
|
return {
|
|
25934
26622
|
xOf: (time) => coords.timeToX(time),
|
|
25935
26623
|
yOf: (price, paneId) => {
|
|
@@ -25950,6 +26638,7 @@ function createProjector(coords, paneOf, paneIdAtY, barsInRange) {
|
|
|
25950
26638
|
},
|
|
25951
26639
|
barsBetween: (t1, t2) => Math.abs(coords.timeToLogical(t2) - coords.timeToLogical(t1)),
|
|
25952
26640
|
barsInRange: barsInRange ? (from, to) => barsInRange(from, to) : void 0,
|
|
26641
|
+
seriesInRange,
|
|
25953
26642
|
width: coords.width,
|
|
25954
26643
|
height: coords.height
|
|
25955
26644
|
};
|
|
@@ -27953,6 +28642,23 @@ var NativeRenderer = class {
|
|
|
27953
28642
|
seriesBoundaries: (paneId) => this.scene.seriesBoundaries(paneId),
|
|
27954
28643
|
priceZ: (paneId) => paneId === PRICE_PANE_ID ? this.scene.candleZ : null,
|
|
27955
28644
|
requestDataPaint: () => this.scheduler.invalidate(3 /* Light */),
|
|
28645
|
+
// The look the price series ACTUALLY paints with: candle colors resolved through
|
|
28646
|
+
// the per-style override, line/area colors through their configured styles — so
|
|
28647
|
+
// series-mirroring content (the magnifier inset) matches the chart exactly.
|
|
28648
|
+
seriesLook: () => {
|
|
28649
|
+
const st = this.scene.style;
|
|
28650
|
+
const paint = effectiveCandlePaint(st.candle, this.scene.candleOverride, this.theme.upColor, this.theme.downColor);
|
|
28651
|
+
const barsUp = st.bars.upColor ?? this.theme.upColor;
|
|
28652
|
+
const barsDown = st.bars.downColor ?? this.theme.downColor;
|
|
28653
|
+
const style = this.scene.priceStyle;
|
|
28654
|
+
return {
|
|
28655
|
+
style,
|
|
28656
|
+
upColor: style === "bars" ? barsUp : paint.up,
|
|
28657
|
+
downColor: style === "bars" ? barsDown : paint.down,
|
|
28658
|
+
lineColor: style === "area" ? st.area.lineColor ?? this.theme.upColor : st.line.color ?? this.theme.upColor
|
|
28659
|
+
};
|
|
28660
|
+
},
|
|
28661
|
+
chartBarMs: () => this.coords.barInterval,
|
|
27956
28662
|
snap: (pt, paneId, mode, cursorPx) => this.snapToCandle(pt, paneId, mode, cursorPx),
|
|
27957
28663
|
setSnapMode: (mode) => this.setSnapMode(mode),
|
|
27958
28664
|
setToolbarGutter: (px) => this.setToolbarGutter(px)
|
|
@@ -29346,7 +30052,8 @@ var NativeRenderer = class {
|
|
|
29346
30052
|
return p ? { scale: p.scale, bounds: p.bounds, collapsed: p.collapsed } : null;
|
|
29347
30053
|
},
|
|
29348
30054
|
(y) => this.paneNodeAtY(y)?.id ?? null,
|
|
29349
|
-
(from, to) => this.barsInTimeRange(from, to)
|
|
30055
|
+
(from, to) => this.barsInTimeRange(from, to),
|
|
30056
|
+
this.userDrawings?.seriesGateway ? (tf, from, to) => this.userDrawings.seriesGateway.seriesInRange(tf, from, to) : void 0
|
|
29350
30057
|
);
|
|
29351
30058
|
}
|
|
29352
30059
|
/** OHLC bars whose open-time falls within `[from, to]` (inclusive) — the data a regression
|