@luxalgo/vela 0.6.10 → 0.6.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -46
- package/dist/{DataProvider-CWmp31dA.d.ts → DataProvider-DhzstpQb.d.ts} +1 -1
- package/dist/{DataProvider-l_eLMly_.d.cts → DataProvider-DlMtrwqM.d.cts} +1 -1
- package/dist/{chunk-4KZVZ7QQ.js → chunk-F4M24ANM.js} +130 -1
- package/dist/{chunk-NMTQXNT4.js → chunk-IZV3CW5N.js} +245 -43
- package/dist/{chunk-FFOP37FQ.js → chunk-KV7FDWSL.js} +748 -70
- package/dist/{contributions-BCz6Dr6a.d.ts → contributions-37nni40G.d.ts} +20 -4
- package/dist/{contributions-D9vTzm5p.d.cts → contributions-lPojhTxI.d.cts} +20 -4
- package/dist/index.cjs +866 -58
- package/dist/index.d.cts +12 -6
- package/dist/index.d.ts +12 -6
- package/dist/index.js +3 -3
- package/dist/{options-DSqHsQyN.d.cts → options-CYS5Wmlx.d.cts} +78 -2
- package/dist/{options-DSqHsQyN.d.ts → options-CYS5Wmlx.d.ts} +78 -2
- package/dist/{plugin-Bt8hLR8Z.d.cts → plugin-Cwikpz1m.d.cts} +10 -3
- package/dist/{plugin-CH7pfMrE.d.ts → plugin-DHyaoMjW.d.ts} +10 -3
- package/dist/plugin.cjs +119 -0
- package/dist/plugin.d.cts +4 -4
- package/dist/plugin.d.ts +4 -4
- package/dist/plugin.js +2 -2
- package/dist/providers/binance.d.cts +2 -2
- package/dist/providers/binance.d.ts +2 -2
- package/dist/providers/coinbase.d.cts +2 -2
- package/dist/providers/coinbase.d.ts +2 -2
- package/dist/providers/hyperliquid.d.cts +2 -2
- package/dist/providers/hyperliquid.d.ts +2 -2
- package/dist/{statusline-5K7y4Ya2.d.ts → statusline-model-CiJm-riV.d.cts} +17 -87
- package/dist/{statusline-TYLQmoeh.d.cts → statusline-model-D-q_OOx9.d.ts} +17 -87
- package/dist/ui.d.cts +1 -1
- package/dist/ui.d.ts +1 -1
- package/dist/vela.global.js +866 -58
- package/dist/vela.global.min.js +52 -52
- package/dist/widget.cjs +1109 -100
- package/dist/widget.d.cts +118 -7
- package/dist/widget.d.ts +118 -7
- package/dist/widget.js +4 -4
- package/dist/workspace.cjs +1109 -100
- package/dist/workspace.d.cts +20 -11
- package/dist/workspace.d.ts +20 -11
- package/dist/workspace.js +3 -3
- package/package.json +1 -1
- /package/dist/{chunk-AOGZBKUE.js → chunk-H26BEHF4.js} +0 -0
package/dist/index.cjs
CHANGED
|
@@ -154,6 +154,12 @@ var IndicatorHandleImpl = class {
|
|
|
154
154
|
get visible() {
|
|
155
155
|
return this.visibleState;
|
|
156
156
|
}
|
|
157
|
+
inputValues() {
|
|
158
|
+
return this.controller.inputValuesOf(this.id);
|
|
159
|
+
}
|
|
160
|
+
propValues() {
|
|
161
|
+
return this.controller.propValuesOf(this.id);
|
|
162
|
+
}
|
|
157
163
|
setInput(key, value) {
|
|
158
164
|
this.controller.applyInputs(this.id, { [key]: value });
|
|
159
165
|
}
|
|
@@ -5418,6 +5424,111 @@ function roundFloat(n) {
|
|
|
5418
5424
|
return Math.round(n * 1e8) / 1e8;
|
|
5419
5425
|
}
|
|
5420
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
|
+
|
|
5421
5532
|
// src/core/drawings/registry.ts
|
|
5422
5533
|
var REGISTRY2 = /* @__PURE__ */ new Map();
|
|
5423
5534
|
function registerDrawingType(meta) {
|
|
@@ -6077,6 +6188,22 @@ registerDrawingType({
|
|
|
6077
6188
|
defaultStyle: { lineColor: DEFAULT_DRAWING_COLOR, lineWidth: 1, lineStyle: "solid" },
|
|
6078
6189
|
create: (init) => new PositionTool(init)
|
|
6079
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
|
+
});
|
|
6080
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"/>');
|
|
6081
6208
|
registerDrawingType({
|
|
6082
6209
|
type: "anchoredvwap",
|
|
@@ -6116,7 +6243,7 @@ var GEOMETRY_TYPES = ["dedekind", "sonic", "supersonic", "goldensonic", "goldens
|
|
|
6116
6243
|
var PATTERN_TYPES = ["xabcd", "abcd", "headshoulders"];
|
|
6117
6244
|
var ELLIOTT_TYPES = ["elliottimpulse", "elliottcorrection"];
|
|
6118
6245
|
var HARMONIC_TYPES = ["gartley", "bat", "butterfly", "crab", "shark", "cypher"];
|
|
6119
|
-
var MEASUREMENT_TYPES = ["position", "datepricerange"];
|
|
6246
|
+
var MEASUREMENT_TYPES = ["position", "datepricerange", "magnifier"];
|
|
6120
6247
|
var VOLUME_TYPES = ["anchoredvwap", "fixedrangevp"];
|
|
6121
6248
|
var BRUSH_TYPES = ["freehand", "highlighter"];
|
|
6122
6249
|
var ARROW_TYPES = ["arrow", "arrowmarkup", "arrowmarkdown"];
|
|
@@ -6480,7 +6607,7 @@ var DrawingHistory = class {
|
|
|
6480
6607
|
|
|
6481
6608
|
// src/core/drawings/DrawingController.ts
|
|
6482
6609
|
var DrawingController = class {
|
|
6483
|
-
constructor(renderer, events, option) {
|
|
6610
|
+
constructor(renderer, events, option, seriesGateway) {
|
|
6484
6611
|
this.events = events;
|
|
6485
6612
|
this.store = new DrawingStore();
|
|
6486
6613
|
this.history = new DrawingHistory();
|
|
@@ -6506,6 +6633,7 @@ var DrawingController = class {
|
|
|
6506
6633
|
const { definition, visible } = buildToolbar(option);
|
|
6507
6634
|
this.port.setToolbar(definition);
|
|
6508
6635
|
this.port.showToolbar(visible);
|
|
6636
|
+
if (seriesGateway) this.port.setSeriesGateway?.(seriesGateway);
|
|
6509
6637
|
this.subs.push(this.port.onDrawingIntent((i) => this.onIntent(i)));
|
|
6510
6638
|
this.subs.push(this.store.onChange(() => this.sync()));
|
|
6511
6639
|
}
|
|
@@ -6611,7 +6739,7 @@ var DrawingController = class {
|
|
|
6611
6739
|
style,
|
|
6612
6740
|
text: init.text,
|
|
6613
6741
|
props: init.props,
|
|
6614
|
-
zIndex: init.zIndex ?? this.startZ(init.paneId ?? "price")
|
|
6742
|
+
zIndex: init.zIndex ?? this.startZ(type, init.paneId ?? "price")
|
|
6615
6743
|
});
|
|
6616
6744
|
if (!d) return null;
|
|
6617
6745
|
this.history.record(this.store.serialize());
|
|
@@ -6686,10 +6814,14 @@ var DrawingController = class {
|
|
|
6686
6814
|
* (falling back to just under the pane's top series where there is no price — a study
|
|
6687
6815
|
* pane). Half a key down never ties a series; drawings tying each other paint in insertion
|
|
6688
6816
|
* order, so consecutive new drawings still stack newest-in-front. Undefined without a
|
|
6689
|
-
* shared z space — the store then places it over the other drawings, its own layer's top.
|
|
6690
|
-
|
|
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) {
|
|
6691
6821
|
const range = this.port?.stackRange?.(paneId);
|
|
6692
|
-
|
|
6822
|
+
if (!range) return void 0;
|
|
6823
|
+
if (getDrawingType(type)?.coversSeries) return range.front + 0.5;
|
|
6824
|
+
return (range.price ?? range.front) - 0.5;
|
|
6693
6825
|
}
|
|
6694
6826
|
/** Programmatically select drawings (host UI → chart): shows the on-chart handles + toolbar.
|
|
6695
6827
|
* `additive` toggles membership (matching shift-click) instead of replacing. */
|
|
@@ -6834,7 +6966,7 @@ var DrawingController = class {
|
|
|
6834
6966
|
const style = last ? { ...i.doc.style, ...last } : i.doc.style;
|
|
6835
6967
|
const d = deserializeDrawing({ ...i.doc, id: this.store.nextId(), style });
|
|
6836
6968
|
if (!d) return;
|
|
6837
|
-
if (!d.zIndex) d.zIndex = this.startZ(d.paneId) ?? 0;
|
|
6969
|
+
if (!d.zIndex) d.zIndex = this.startZ(d.type, d.paneId) ?? 0;
|
|
6838
6970
|
this.history.record(before);
|
|
6839
6971
|
this.store.add(d);
|
|
6840
6972
|
this.captureStyle(d.id);
|
|
@@ -6932,6 +7064,180 @@ function timeframeToMs(timeframe) {
|
|
|
6932
7064
|
return 36e5;
|
|
6933
7065
|
}
|
|
6934
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
|
+
|
|
6935
7241
|
// src/data/symbol-groups.ts
|
|
6936
7242
|
function isGroupRow(d) {
|
|
6937
7243
|
return d.group != null && d.ticker === d.group;
|
|
@@ -7296,6 +7602,7 @@ var RUN_EMIT_THROTTLE_MS = 1e3;
|
|
|
7296
7602
|
var PREVIEW_BARS = 300;
|
|
7297
7603
|
var SINGLE_LOAD_BARS = 5e3;
|
|
7298
7604
|
var CHUNK_BARS = 1e4;
|
|
7605
|
+
var FIRST_PAINT_BARS = 100;
|
|
7299
7606
|
var GAP_FACTOR = 1.5;
|
|
7300
7607
|
var HEAL_COOLDOWN_MS = 5e3;
|
|
7301
7608
|
var EngineOrchestrator = class _EngineOrchestrator {
|
|
@@ -7413,7 +7720,13 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
7413
7720
|
const initialStyle = this.renderer.readFeature("priceStyle");
|
|
7414
7721
|
if (typeof initialStyle === "string") this.priceStyle = initialStyle;
|
|
7415
7722
|
this.barTransform = barTransformFor(initialStyle);
|
|
7416
|
-
|
|
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);
|
|
7417
7730
|
this.unresolvedUnsub = this.feed.onUnresolved?.((info) => {
|
|
7418
7731
|
this.endLoad();
|
|
7419
7732
|
this.events.emit("data:unresolved", info);
|
|
@@ -7565,6 +7878,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
7565
7878
|
let painted = false;
|
|
7566
7879
|
const paint = (bars, final) => {
|
|
7567
7880
|
if (this.generation !== gen || !final && bars.length === 0) return;
|
|
7881
|
+
if (!painted && !final && bars.length < Math.min(requested, FIRST_PAINT_BARS)) return;
|
|
7568
7882
|
this.setBarSeries(bars, painted ? { preserveView: true } : void 0);
|
|
7569
7883
|
if (!painted && bars.length > 0) {
|
|
7570
7884
|
painted = true;
|
|
@@ -8195,6 +8509,15 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
8195
8509
|
record.pendingCause = "inputs";
|
|
8196
8510
|
if (record.session) record.session.update(record.inputValues);
|
|
8197
8511
|
else if (record.native && !record.hidden) record.native.instance.setInputs(record.inputValues);
|
|
8512
|
+
this.events.emit("indicator:inputs", { id });
|
|
8513
|
+
}
|
|
8514
|
+
/** IndicatorController: the CURRENT stored input values (defaults merged with edits). */
|
|
8515
|
+
inputValuesOf(id) {
|
|
8516
|
+
return { ...this.registry.get(id)?.inputValues };
|
|
8517
|
+
}
|
|
8518
|
+
/** IndicatorController: the CURRENT declaration-prop overrides. */
|
|
8519
|
+
propValuesOf(id) {
|
|
8520
|
+
return { ...this.registry.get(id)?.propValues };
|
|
8198
8521
|
}
|
|
8199
8522
|
/** IndicatorController: re-run an indicator with merged declaration-prop overrides.
|
|
8200
8523
|
* Same lifecycle as {@link applyInputs} — a prop change replays the whole script.
|
|
@@ -8208,6 +8531,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
8208
8531
|
if (!record.hidden) this.setLoading(record, true);
|
|
8209
8532
|
record.pendingCause = "inputs";
|
|
8210
8533
|
record.session.update(record.inputValues, record.propValues);
|
|
8534
|
+
this.events.emit("indicator:inputs", { id });
|
|
8211
8535
|
}
|
|
8212
8536
|
/** IndicatorController: tear down an indicator and (if now empty) its pane. */
|
|
8213
8537
|
/** Live handles of every indicator on the chart (script + native), insertion order. */
|
|
@@ -8395,8 +8719,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
8395
8719
|
onModel: (model) => {
|
|
8396
8720
|
const first = !record.announced;
|
|
8397
8721
|
const cause = record.pendingCause ?? "history";
|
|
8722
|
+
if (!this.applyModel(id, model)) return;
|
|
8398
8723
|
record.pendingCause = void 0;
|
|
8399
|
-
this.applyModel(id, model);
|
|
8400
8724
|
this.emitContextChanged(id);
|
|
8401
8725
|
this.emitScriptRun(id, cause, first);
|
|
8402
8726
|
},
|
|
@@ -8689,10 +9013,16 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
8689
9013
|
* Apply an emitted model. First emission mounts (and routes the pane); a pending
|
|
8690
9014
|
* structural change (after an input edit) remounts idempotently; everything else
|
|
8691
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.
|
|
8692
9021
|
*/
|
|
8693
9022
|
applyModel(id, model) {
|
|
8694
9023
|
const record = this.registry.get(id);
|
|
8695
|
-
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;
|
|
8696
9026
|
const handle = this.handles.get(id);
|
|
8697
9027
|
if (!record.renderHandle) {
|
|
8698
9028
|
const paneId2 = this.routePane(id, model, record.options ?? {});
|
|
@@ -8702,7 +9032,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
8702
9032
|
record.renderHandle = this.renderer.mountIndicator(model);
|
|
8703
9033
|
record.pendingStructural = false;
|
|
8704
9034
|
this.announce(record, handle);
|
|
8705
|
-
return;
|
|
9035
|
+
return true;
|
|
8706
9036
|
}
|
|
8707
9037
|
let paneId = record.model?.paneId ?? "price";
|
|
8708
9038
|
const prevOwnScale = record.model?.ownScale === true;
|
|
@@ -8730,6 +9060,11 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
8730
9060
|
}
|
|
8731
9061
|
if (record.loading) this.setLoading(record, false);
|
|
8732
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;
|
|
8733
9068
|
}
|
|
8734
9069
|
routePane(id, model, options) {
|
|
8735
9070
|
if (options.pane === "new") return `pane-${id}`;
|
|
@@ -9432,6 +9767,7 @@ function intervalMs(timeframe) {
|
|
|
9432
9767
|
"4h": 144e5,
|
|
9433
9768
|
"1d": 864e5,
|
|
9434
9769
|
"1w": 6048e5,
|
|
9770
|
+
"1M": 2592e6,
|
|
9435
9771
|
"1": 6e4,
|
|
9436
9772
|
"5": 3e5,
|
|
9437
9773
|
"15": 9e5,
|
|
@@ -9439,7 +9775,8 @@ function intervalMs(timeframe) {
|
|
|
9439
9775
|
"60": 36e5,
|
|
9440
9776
|
"240": 144e5,
|
|
9441
9777
|
D: 864e5,
|
|
9442
|
-
W: 6048e5
|
|
9778
|
+
W: 6048e5,
|
|
9779
|
+
M: 2592e6
|
|
9443
9780
|
};
|
|
9444
9781
|
return map[timeframe] ?? 36e5;
|
|
9445
9782
|
}
|
|
@@ -10995,6 +11332,14 @@ function inputVisible(when, values) {
|
|
|
10995
11332
|
const conds = Array.isArray(when) ? when : [when];
|
|
10996
11333
|
return conds.every((c) => c.anyOf ? c.anyOf.some((x) => x === values[c.key]) : values[c.key] === c.equals);
|
|
10997
11334
|
}
|
|
11335
|
+
function inputDeltas(schema, values) {
|
|
11336
|
+
const out = {};
|
|
11337
|
+
for (const s of schema) {
|
|
11338
|
+
const v = values[s.key];
|
|
11339
|
+
if (v !== void 0 && JSON.stringify(v) !== JSON.stringify(s.defval)) out[s.key] = v;
|
|
11340
|
+
}
|
|
11341
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
11342
|
+
}
|
|
10998
11343
|
|
|
10999
11344
|
// src/ui/components/select/controller.ts
|
|
11000
11345
|
function selectController(opts) {
|
|
@@ -16778,6 +17123,7 @@ var DOUBLE_TAP_MS = 350;
|
|
|
16778
17123
|
var DOUBLE_TAP_SLOP = 30;
|
|
16779
17124
|
var TIME_SCALE_K = 4e-3;
|
|
16780
17125
|
var WHEEL_ZOOM_K = 4e-3;
|
|
17126
|
+
var WHEEL_PRICE_DRAG_PX = 0.25;
|
|
16781
17127
|
function wheelZoomAnchor(coords, cursorX, rightEdge) {
|
|
16782
17128
|
if (rightEdge) return { logical: coords.rightEdgeLogical, x: coords.width };
|
|
16783
17129
|
return { logical: coords.xToLogical(cursorX), x: cursorX };
|
|
@@ -16908,7 +17254,7 @@ var InputController = class {
|
|
|
16908
17254
|
this.capture(e.pointerId);
|
|
16909
17255
|
return;
|
|
16910
17256
|
}
|
|
16911
|
-
if (e.shiftKey && this.regionAt(x, y) === "data" && this.deps.drawingsMeasureStart?.(x, y)) {
|
|
17257
|
+
if (e.shiftKey && this.regionAt(x, y) === "data" && this.deps.drawingsMeasureStart?.(x, y, this.snapMode(e))) {
|
|
16912
17258
|
this.region = "drawing";
|
|
16913
17259
|
this.capture(e.pointerId);
|
|
16914
17260
|
return;
|
|
@@ -17022,7 +17368,7 @@ var InputController = class {
|
|
|
17022
17368
|
const wasTouch = e.pointerType === "touch";
|
|
17023
17369
|
const tapRelease = this.dragging && !this.moved && (!wasTouch || Math.hypot(x - this.startX, y - this.startY) <= TOUCH_TAP_SLOP);
|
|
17024
17370
|
if (this.dragging && this.region === "drawing") {
|
|
17025
|
-
this.deps.drawingsPointerUp?.(x, y);
|
|
17371
|
+
this.deps.drawingsPointerUp?.(x, y, this.snapMode(e));
|
|
17026
17372
|
} else if (tapRelease && this.region === "data") {
|
|
17027
17373
|
this.deps.onClick(x, y);
|
|
17028
17374
|
} else if (this.dragging && this.region === "data") {
|
|
@@ -17043,7 +17389,7 @@ var InputController = class {
|
|
|
17043
17389
|
if (e.pointerType === "touch") this.touches.delete(e.pointerId);
|
|
17044
17390
|
this.cancelLongPress();
|
|
17045
17391
|
if (!this.dragging) return;
|
|
17046
|
-
if (this.region === "drawing" && !Number.isNaN(this.cursorX)) this.deps.drawingsPointerUp?.(this.cursorX, this.cursorY);
|
|
17392
|
+
if (this.region === "drawing" && !Number.isNaN(this.cursorX)) this.deps.drawingsPointerUp?.(this.cursorX, this.cursorY, this.snapMode(e));
|
|
17047
17393
|
if (this.region === "crosshair" || e.pointerType === "touch") this.deps.onPointerMove(null, null);
|
|
17048
17394
|
this.endGesture(e);
|
|
17049
17395
|
};
|
|
@@ -17059,6 +17405,12 @@ var InputController = class {
|
|
|
17059
17405
|
this.onWheel = (e) => {
|
|
17060
17406
|
e.preventDefault();
|
|
17061
17407
|
this.deps.drawingsClearTransient?.();
|
|
17408
|
+
const { x, y } = this.local(e);
|
|
17409
|
+
if (this.regionAt(x, y) === "price" && e.deltaY !== 0) {
|
|
17410
|
+
this.deps.beginPriceScale(x, y);
|
|
17411
|
+
this.deps.priceScaleBy(e.deltaY * WHEEL_PRICE_DRAG_PX);
|
|
17412
|
+
return;
|
|
17413
|
+
}
|
|
17062
17414
|
const coords = this.deps.getCoords();
|
|
17063
17415
|
const vp = coords.getViewport();
|
|
17064
17416
|
const pan = wheelPanDelta(e.deltaX, e.deltaY, e.shiftKey);
|
|
@@ -17066,9 +17418,8 @@ var InputController = class {
|
|
|
17066
17418
|
this.deps.apply({ barSpacing: vp.barSpacing, rightOffset: wheelPanRightOffset(vp.rightOffset, pan, coords.pxPerBar()) });
|
|
17067
17419
|
return;
|
|
17068
17420
|
}
|
|
17069
|
-
const cursorX = this.local(e).x;
|
|
17070
17421
|
const rightEdge = this.rightEdgeZoom && !(e.ctrlKey || e.metaKey);
|
|
17071
|
-
const anchor = wheelZoomAnchor(coords,
|
|
17422
|
+
const anchor = wheelZoomAnchor(coords, x, rightEdge);
|
|
17072
17423
|
const target = clampBarSpacing(vp.barSpacing * Math.exp(-e.deltaY * WHEEL_ZOOM_K));
|
|
17073
17424
|
this.deps.zoomTo(target, anchor.logical, anchor.x);
|
|
17074
17425
|
};
|
|
@@ -18550,6 +18901,15 @@ function drawingSetEmpty(s) {
|
|
|
18550
18901
|
function fontSizePx(size) {
|
|
18551
18902
|
return size === "auto" ? 12 : namedFontSize(size);
|
|
18552
18903
|
}
|
|
18904
|
+
function lineCoversWindow(a, b, extend, lo, hi) {
|
|
18905
|
+
const minX = Math.min(a, b);
|
|
18906
|
+
const maxX = Math.max(a, b);
|
|
18907
|
+
if (a === b) return a >= lo && a <= hi;
|
|
18908
|
+
if (extend === "both") return true;
|
|
18909
|
+
if (extend === "left") return maxX >= lo;
|
|
18910
|
+
if (extend === "right") return minX <= hi;
|
|
18911
|
+
return maxX >= lo && minX <= hi;
|
|
18912
|
+
}
|
|
18553
18913
|
var DrawingSceneRenderer = class {
|
|
18554
18914
|
constructor(deps, set = EMPTY_DRAWING_SET) {
|
|
18555
18915
|
this.deps = deps;
|
|
@@ -18603,7 +18963,9 @@ var DrawingSceneRenderer = class {
|
|
|
18603
18963
|
const lo = Math.min(from, to);
|
|
18604
18964
|
const hi = Math.max(from, to);
|
|
18605
18965
|
const visible = (a, b, extend) => {
|
|
18606
|
-
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;
|
|
18607
18969
|
return Math.max(a, b) >= lo && Math.min(a, b) <= hi;
|
|
18608
18970
|
};
|
|
18609
18971
|
let min = Infinity;
|
|
@@ -18614,7 +18976,7 @@ var DrawingSceneRenderer = class {
|
|
|
18614
18976
|
};
|
|
18615
18977
|
for (const ln of this.set.lines) {
|
|
18616
18978
|
if (ln.invisible) continue;
|
|
18617
|
-
if (!
|
|
18979
|
+
if (!lineCoversWindow(this.logicalOf(ln.xloc, ln.x1), this.logicalOf(ln.xloc, ln.x2), ln.extend, lo, hi)) continue;
|
|
18618
18980
|
fold(ln.y1);
|
|
18619
18981
|
fold(ln.y2);
|
|
18620
18982
|
}
|
|
@@ -21167,6 +21529,17 @@ var DrawingPainter = class {
|
|
|
21167
21529
|
constructor() {
|
|
21168
21530
|
/** The current `paintAll` call's interaction state, visible to the per-type painters. */
|
|
21169
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
|
+
};
|
|
21170
21543
|
}
|
|
21171
21544
|
/** Paint every visible drawing, then selection handles for the targeted ones.
|
|
21172
21545
|
* Each drawing is clipped to its own pane's rect (and skipped entirely while that pane
|
|
@@ -21211,6 +21584,8 @@ var DrawingPainter = class {
|
|
|
21211
21584
|
ctx.globalAlpha = GHOST_ALPHA;
|
|
21212
21585
|
if (ghost instanceof RegressionChannel || ghost instanceof FixedRangeVolumeProfile) {
|
|
21213
21586
|
this.paintTimeSpanGhost(ctx, ghost, proj);
|
|
21587
|
+
} else if (ghost instanceof Magnifier) {
|
|
21588
|
+
this.paintMagnifierGhost(ctx, ghost, proj, theme);
|
|
21214
21589
|
} else this.paintOne(ctx, ghost, proj, theme);
|
|
21215
21590
|
ctx.globalAlpha = 1;
|
|
21216
21591
|
}
|
|
@@ -21344,6 +21719,10 @@ var DrawingPainter = class {
|
|
|
21344
21719
|
this.paintLabel(ctx, d, proj, theme);
|
|
21345
21720
|
return;
|
|
21346
21721
|
}
|
|
21722
|
+
if (d instanceof Magnifier) {
|
|
21723
|
+
this.paintMagnifier(ctx, d, proj, theme);
|
|
21724
|
+
return;
|
|
21725
|
+
}
|
|
21347
21726
|
if (d instanceof PatternDrawing) {
|
|
21348
21727
|
this.paintPattern(ctx, d, proj, theme);
|
|
21349
21728
|
return;
|
|
@@ -21836,6 +22215,216 @@ var DrawingPainter = class {
|
|
|
21836
22215
|
ctx.textBaseline = "alphabetic";
|
|
21837
22216
|
}
|
|
21838
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
|
+
}
|
|
21839
22428
|
/** Paint a fixed-range volume profile: horizontal histogram rows (up/down split) anchored to
|
|
21840
22429
|
* the left or right of the time span, optional VAH / VAL / POC levels across the range, and
|
|
21841
22430
|
* optional developing POC / VA polylines. Recomputes from the two anchors on every paint. */
|
|
@@ -22554,6 +23143,22 @@ var DrawingInteraction = class {
|
|
|
22554
23143
|
this.snapAt = changed ? { point: snapped, paneId } : null;
|
|
22555
23144
|
return snapped;
|
|
22556
23145
|
}
|
|
23146
|
+
/**
|
|
23147
|
+
* Resolve a cursor pixel through the magnet and return the snapped pixel — the same
|
|
23148
|
+
* conversion drawing placement uses. Updates the snap-ring marker. The measure
|
|
23149
|
+
* ruler goes through this so its endpoints follow weak/strong/Ctrl magnet too.
|
|
23150
|
+
*/
|
|
23151
|
+
snapCursor(x, y, mode) {
|
|
23152
|
+
const proj = this.deps.projector();
|
|
23153
|
+
const paneId = proj.paneIdAtY(y) ?? "price";
|
|
23154
|
+
const point = this.resolve(x, y, paneId, mode);
|
|
23155
|
+
const sy = proj.yOf(point.price, paneId);
|
|
23156
|
+
return { x: proj.xOf(point.time), y: sy ?? y };
|
|
23157
|
+
}
|
|
23158
|
+
/** Drop the snap-ring marker (a transient mode ended without going through `up`). */
|
|
23159
|
+
clearSnapMarker() {
|
|
23160
|
+
this.snapAt = null;
|
|
23161
|
+
}
|
|
22557
23162
|
/** Resolve a pixel to a data point with the segment angle locked to 45° steps around
|
|
22558
23163
|
* `pivot` (Shift held on a line tool). Works in PIXEL space — the user reasons about
|
|
22559
23164
|
* the angle they see, not about time/price units. The magnet is bypassed: snapping
|
|
@@ -23235,8 +23840,9 @@ function ensureStyles3() {
|
|
|
23235
23840
|
if (!existing) document.head.appendChild(s);
|
|
23236
23841
|
}
|
|
23237
23842
|
var DrawingSettingsPopup = class {
|
|
23238
|
-
constructor(host, theme) {
|
|
23843
|
+
constructor(host, theme, chartBarMs = () => 0) {
|
|
23239
23844
|
this.host = host;
|
|
23845
|
+
this.chartBarMs = chartBarMs;
|
|
23240
23846
|
this.el = null;
|
|
23241
23847
|
this.tipEl = null;
|
|
23242
23848
|
// floating hover-label (above/below the toolbar)
|
|
@@ -23261,6 +23867,14 @@ var DrawingSettingsPopup = class {
|
|
|
23261
23867
|
this.theme = theme;
|
|
23262
23868
|
this.settingsDialog = new DrawingSettingsDialog(host, theme);
|
|
23263
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
|
+
}
|
|
23264
23878
|
setTheme(theme) {
|
|
23265
23879
|
this.theme = theme;
|
|
23266
23880
|
this.settingsDialog.setTheme(theme);
|
|
@@ -23303,7 +23917,20 @@ var DrawingSettingsPopup = class {
|
|
|
23303
23917
|
const sz = drawing.size ?? "normal";
|
|
23304
23918
|
bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label: sizeLabel }));
|
|
23305
23919
|
}
|
|
23306
|
-
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 })));
|
|
23307
23934
|
if (paths.has("style.lineWidth")) {
|
|
23308
23935
|
const wf = schema.fields.find((f) => f.path === "style.lineWidth");
|
|
23309
23936
|
if (wf?.kind === "number" && (wf.min ?? 1) > 1) {
|
|
@@ -23625,6 +24252,7 @@ var DrawingSettingsPopup = class {
|
|
|
23625
24252
|
this.colorPop = null;
|
|
23626
24253
|
this.colorOwner = null;
|
|
23627
24254
|
}
|
|
24255
|
+
opts.onClose?.();
|
|
23628
24256
|
}
|
|
23629
24257
|
});
|
|
23630
24258
|
const el = pop.el;
|
|
@@ -23643,6 +24271,49 @@ var DrawingSettingsPopup = class {
|
|
|
23643
24271
|
pop.show();
|
|
23644
24272
|
return pop;
|
|
23645
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
|
+
}
|
|
23646
24317
|
/** A floating list of one-shot actions (icon + label rows) opened by the kebab. */
|
|
23647
24318
|
openActionMenu(anchor, rows) {
|
|
23648
24319
|
this.menuPop = this.hostFloat(anchor, {
|
|
@@ -23753,10 +24424,13 @@ var DrawingSettingsPopup = class {
|
|
|
23753
24424
|
let cur = current;
|
|
23754
24425
|
const paint = (v) => {
|
|
23755
24426
|
b.replaceChildren();
|
|
23756
|
-
const
|
|
23757
|
-
|
|
23758
|
-
|
|
23759
|
-
|
|
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
|
+
}
|
|
23760
24434
|
if (opts.label && opts.labelInTrigger) {
|
|
23761
24435
|
const tx = document.createElement("span");
|
|
23762
24436
|
tx.textContent = opts.label(v);
|
|
@@ -23798,10 +24472,13 @@ var DrawingSettingsPopup = class {
|
|
|
23798
24472
|
item.className = "vela-dpop-item";
|
|
23799
24473
|
item.dataset.active = active ? "1" : "0";
|
|
23800
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;`;
|
|
23801
|
-
const
|
|
23802
|
-
|
|
23803
|
-
|
|
23804
|
-
|
|
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
|
+
}
|
|
23805
24482
|
if (label) {
|
|
23806
24483
|
const tx = document.createElement("span");
|
|
23807
24484
|
tx.textContent = label(v);
|
|
@@ -24591,30 +25268,39 @@ var MeasureOverlay = class {
|
|
|
24591
25268
|
isFinished() {
|
|
24592
25269
|
return this.state === "finished";
|
|
24593
25270
|
}
|
|
24594
|
-
/** A press: begin the measurement, or finish it on the second click.
|
|
24595
|
-
|
|
25271
|
+
/** A press: begin the measurement, or finish it on the second click.
|
|
25272
|
+
* `x,y` are the raw cursor (drag-slop vs click-move-click). `gx,gy` are the
|
|
25273
|
+
* graphic endpoints — magnet-snapped when the magnet is on, else the same as `x,y`. */
|
|
25274
|
+
down(x, y, gx = x, gy = y) {
|
|
24596
25275
|
if (this.state === "measuring") {
|
|
24597
|
-
this.end = { x, y };
|
|
25276
|
+
this.end = { x: gx, y: gy };
|
|
24598
25277
|
this.state = "finished";
|
|
24599
25278
|
return;
|
|
24600
25279
|
}
|
|
24601
|
-
this.start = { x, y };
|
|
24602
|
-
this.end = { x, y };
|
|
25280
|
+
this.start = { x: gx, y: gy };
|
|
25281
|
+
this.end = { x: gx, y: gy };
|
|
24603
25282
|
this.pressX = x;
|
|
24604
25283
|
this.pressY = y;
|
|
24605
25284
|
this.state = "measuring";
|
|
24606
25285
|
}
|
|
25286
|
+
/** Size the in-progress ruler. `x,y` are the graphic (magnet-snapped) cursor. */
|
|
24607
25287
|
move(x, y) {
|
|
24608
25288
|
if (this.state === "measuring") this.end = { x, y };
|
|
24609
25289
|
}
|
|
24610
25290
|
/** A release: finish if the press was actually dragged (press-drag-release), else wait
|
|
24611
|
-
* for the second click (click-move-click).
|
|
24612
|
-
|
|
25291
|
+
* for the second click (click-move-click). `x,y` are the raw cursor (slop); `gx,gy`
|
|
25292
|
+
* are the graphic end (magnet-snapped when the magnet is on). */
|
|
25293
|
+
up(x, y, gx = x, gy = y) {
|
|
24613
25294
|
if (this.state === "measuring" && Math.hypot(x - this.pressX, y - this.pressY) > DRAG_SLOP3) {
|
|
24614
|
-
this.end = { x, y };
|
|
25295
|
+
this.end = { x: gx, y: gy };
|
|
24615
25296
|
this.state = "finished";
|
|
24616
25297
|
}
|
|
24617
25298
|
}
|
|
25299
|
+
/** Current graphic endpoints in media pixels, or null when idle. */
|
|
25300
|
+
points() {
|
|
25301
|
+
if (!this.start || !this.end) return null;
|
|
25302
|
+
return { start: this.start, end: this.end };
|
|
25303
|
+
}
|
|
24618
25304
|
clear() {
|
|
24619
25305
|
this.state = "idle";
|
|
24620
25306
|
this.start = null;
|
|
@@ -24776,6 +25462,9 @@ var UserDrawingController = class {
|
|
|
24776
25462
|
this.intentCb = null;
|
|
24777
25463
|
/** Another chart's in-progress placement, mirrored here as a ghost (drawings sync). */
|
|
24778
25464
|
this.externalGhost = null;
|
|
25465
|
+
/** Core-pushed series gateway (finer-timeframe bars for data-driven drawings). */
|
|
25466
|
+
this.seriesGw = null;
|
|
25467
|
+
this.seriesGwUnsub = null;
|
|
24779
25468
|
/** Last draft fingerprint reported upstream — gates the per-render emission to actual changes. */
|
|
24780
25469
|
this.lastDraftKey = null;
|
|
24781
25470
|
this.measure = new MeasureOverlay();
|
|
@@ -24803,7 +25492,7 @@ var UserDrawingController = class {
|
|
|
24803
25492
|
this.textEditor = null;
|
|
24804
25493
|
this.painter = new DrawingPainter();
|
|
24805
25494
|
this.ctx = canvas.getContext("2d");
|
|
24806
|
-
this.popup = new DrawingSettingsPopup(overlayHost, deps.theme());
|
|
25495
|
+
this.popup = new DrawingSettingsPopup(overlayHost, deps.theme(), () => deps.chartBarMs());
|
|
24807
25496
|
this.toolbar = new DrawingToolbar(
|
|
24808
25497
|
toolbarHost,
|
|
24809
25498
|
deps.theme(),
|
|
@@ -24861,6 +25550,22 @@ var UserDrawingController = class {
|
|
|
24861
25550
|
const shown = this.toolbarVisible && !this.mobileLayout;
|
|
24862
25551
|
this.deps.setToolbarGutter(shown ? this.toolbarCollapsed ? TOOLBAR_COLLAPSED_WIDTH : TOOLBAR_WIDTH : 0);
|
|
24863
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
|
+
}
|
|
24864
25569
|
/** Core push: mirror (or clear) another chart's in-progress placement as a ghost. */
|
|
24865
25570
|
setExternalGhost(doc) {
|
|
24866
25571
|
this.externalGhost = doc ? deserializeDrawing(doc) : null;
|
|
@@ -24978,8 +25683,34 @@ var UserDrawingController = class {
|
|
|
24978
25683
|
/** Should the drawing layer win this press (vs pan)? */
|
|
24979
25684
|
claim(x, y) {
|
|
24980
25685
|
if (this.measureMode || this.eraserMode) return true;
|
|
25686
|
+
if (this.magnifierChipAt(x, y)) return true;
|
|
24981
25687
|
return this.interaction.claim(x, y);
|
|
24982
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
|
+
}
|
|
24983
25714
|
/** Delete the (unlocked) drawing under the cursor. True when one was removed.
|
|
24984
25715
|
* Shared by the eraser (click + drag) and the middle-click shortcut. */
|
|
24985
25716
|
deleteAt(x, y) {
|
|
@@ -24991,11 +25722,13 @@ var UserDrawingController = class {
|
|
|
24991
25722
|
}
|
|
24992
25723
|
/** Shift+press on the empty plot: arm the measure ruler AND start it at (x, y) in one
|
|
24993
25724
|
* gesture — the equivalent of clicking the toolbar's Measure button, then pressing.
|
|
24994
|
-
*
|
|
24995
|
-
|
|
25725
|
+
* `snap` is the effective magnet (sticky mode, or Ctrl/Cmd-forced strong). Returns
|
|
25726
|
+
* false when a mode/tool is already active (the normal press path owns it). */
|
|
25727
|
+
beginMeasureAt(x, y, snap = "off") {
|
|
24996
25728
|
if (this.measureMode || this.eraserMode || this.activeTool != null) return false;
|
|
24997
25729
|
this.withModeIntent(() => this.toggleMeasure());
|
|
24998
|
-
this.
|
|
25730
|
+
const g = this.interaction.snapCursor(x, y, snap);
|
|
25731
|
+
this.measure.down(x, y, g.x, g.y);
|
|
24999
25732
|
this.render();
|
|
25000
25733
|
return true;
|
|
25001
25734
|
}
|
|
@@ -25017,11 +25750,19 @@ var UserDrawingController = class {
|
|
|
25017
25750
|
return;
|
|
25018
25751
|
}
|
|
25019
25752
|
if (this.measureMode) {
|
|
25020
|
-
this.
|
|
25753
|
+
const g = this.interaction.snapCursor(x, y, snap);
|
|
25754
|
+
this.measure.down(x, y, g.x, g.y);
|
|
25021
25755
|
if (this.measure.isFinished()) this.withModeIntent(() => this.exitMeasure(false));
|
|
25022
25756
|
this.render();
|
|
25023
25757
|
return;
|
|
25024
25758
|
}
|
|
25759
|
+
if (this.activeTool == null) {
|
|
25760
|
+
const chipOwner = this.magnifierChipAt(x, y);
|
|
25761
|
+
if (chipOwner) {
|
|
25762
|
+
this.openMagnifierChipMenu(chipOwner);
|
|
25763
|
+
return;
|
|
25764
|
+
}
|
|
25765
|
+
}
|
|
25025
25766
|
this.interaction.down(x, y, snap, shift);
|
|
25026
25767
|
}
|
|
25027
25768
|
pointerMove(x, y, snap = "off", shift = false) {
|
|
@@ -25030,7 +25771,8 @@ var UserDrawingController = class {
|
|
|
25030
25771
|
return;
|
|
25031
25772
|
}
|
|
25032
25773
|
if (this.measureMode) {
|
|
25033
|
-
this.
|
|
25774
|
+
const g = this.interaction.snapCursor(x, y, snap);
|
|
25775
|
+
this.measure.move(g.x, g.y);
|
|
25034
25776
|
this.render();
|
|
25035
25777
|
return;
|
|
25036
25778
|
}
|
|
@@ -25048,13 +25790,14 @@ var UserDrawingController = class {
|
|
|
25048
25790
|
this.render();
|
|
25049
25791
|
}
|
|
25050
25792
|
}
|
|
25051
|
-
pointerUp(x, y) {
|
|
25793
|
+
pointerUp(x, y, snap = "off") {
|
|
25052
25794
|
if (this.eraserMode) {
|
|
25053
25795
|
this.erasing = false;
|
|
25054
25796
|
return;
|
|
25055
25797
|
}
|
|
25056
25798
|
if (this.measureMode) {
|
|
25057
|
-
this.
|
|
25799
|
+
const g = this.interaction.snapCursor(x, y, snap);
|
|
25800
|
+
this.measure.up(x, y, g.x, g.y);
|
|
25058
25801
|
if (this.measure.isFinished()) this.withModeIntent(() => this.exitMeasure(false));
|
|
25059
25802
|
this.render();
|
|
25060
25803
|
return;
|
|
@@ -25099,6 +25842,7 @@ var UserDrawingController = class {
|
|
|
25099
25842
|
/** Leave ruler mode. `clearGraphic` keeps a just-finished measurement on screen (false). */
|
|
25100
25843
|
exitMeasure(clearGraphic = true) {
|
|
25101
25844
|
this.measureMode = false;
|
|
25845
|
+
this.interaction.clearSnapMarker();
|
|
25102
25846
|
if (clearGraphic) this.measure.clear();
|
|
25103
25847
|
this.toolbar.setMeasureActive(false);
|
|
25104
25848
|
this.render();
|
|
@@ -25237,17 +25981,31 @@ var UserDrawingController = class {
|
|
|
25237
25981
|
/** Cursor hint while hovering — `'pointer'` over a drawing/handle, else null. */
|
|
25238
25982
|
cursorAt(x, y) {
|
|
25239
25983
|
if (this.eraserMode) return "pointer";
|
|
25984
|
+
if (this.activeTool == null && this.magnifierChipAt(x, y)) return "pointer";
|
|
25240
25985
|
return this.interaction.cursorAt(x, y);
|
|
25241
25986
|
}
|
|
25242
|
-
/** Right-click
|
|
25243
|
-
*
|
|
25244
|
-
*
|
|
25245
|
-
*
|
|
25987
|
+
/** Right-click: an explicit escape back to the pointer. Cancels an in-progress
|
|
25988
|
+
* placement or measurement, and also plain-disarms an armed-but-idle drawing
|
|
25989
|
+
* tool or the eraser — so a right-click ALWAYS reverts to the pointer, even in
|
|
25990
|
+
* stay-in-drawing-mode (where Escape would leave a drawing tool armed).
|
|
25991
|
+
* Persistent toggles (magnet, stay-mode, favorites) are untouched. Returns
|
|
25992
|
+
* whether the press was consumed; false lets the host's context menu open
|
|
25993
|
+
* normally. */
|
|
25246
25994
|
cancelPlacement() {
|
|
25247
|
-
if (
|
|
25248
|
-
|
|
25249
|
-
|
|
25250
|
-
|
|
25995
|
+
if (this.measureMode || this.eraserMode) {
|
|
25996
|
+
this.withModeIntent(() => this.measureMode ? this.exitMeasure() : this.exitEraser());
|
|
25997
|
+
return true;
|
|
25998
|
+
}
|
|
25999
|
+
if (this.interaction.isPlacing()) {
|
|
26000
|
+
this.interaction.cancel();
|
|
26001
|
+
if (this.activeTool != null) this.emit({ kind: "arm", type: null });
|
|
26002
|
+
return true;
|
|
26003
|
+
}
|
|
26004
|
+
if (this.activeTool != null) {
|
|
26005
|
+
this.emit({ kind: "arm", type: null });
|
|
26006
|
+
return true;
|
|
26007
|
+
}
|
|
26008
|
+
return false;
|
|
25251
26009
|
}
|
|
25252
26010
|
/** Double-click over a drawing → suppress the chart's view reset (single-click already
|
|
25253
26011
|
* opens settings). Returns true only when a drawing is under the cursor. */
|
|
@@ -25273,6 +26031,10 @@ var UserDrawingController = class {
|
|
|
25273
26031
|
return true;
|
|
25274
26032
|
}
|
|
25275
26033
|
if (this.interaction.cancel()) return true;
|
|
26034
|
+
if (this.measureMode) {
|
|
26035
|
+
this.withModeIntent(() => this.exitMeasure());
|
|
26036
|
+
return true;
|
|
26037
|
+
}
|
|
25276
26038
|
if (this.selectedIds.size) {
|
|
25277
26039
|
this.clearSelection();
|
|
25278
26040
|
return true;
|
|
@@ -25398,6 +26160,7 @@ var UserDrawingController = class {
|
|
|
25398
26160
|
if (!sctx) continue;
|
|
25399
26161
|
sctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
25400
26162
|
sctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
|
|
26163
|
+
this.painter.seriesLook = this.deps.seriesLook();
|
|
25401
26164
|
this.painter.paintAll(sctx, drawings, proj, theme, EMPTY_TARGETS);
|
|
25402
26165
|
const slices = out.get(paneId) ?? [];
|
|
25403
26166
|
slices.push({ beforeZ, canvas });
|
|
@@ -25425,6 +26188,7 @@ var UserDrawingController = class {
|
|
|
25425
26188
|
dragged: this.interaction.activeDragId(),
|
|
25426
26189
|
mutedLabel: edited instanceof TextLabel ? edited.id : null
|
|
25427
26190
|
};
|
|
26191
|
+
this.painter.seriesLook = this.deps.seriesLook();
|
|
25428
26192
|
this.painter.paintAll(ctx, this.drawings.filter((d) => !this.isInterleaved(d)), proj, this.deps.theme(), targets);
|
|
25429
26193
|
this.painter.paintHighlights(ctx, this.drawings.filter((d) => this.isInterleaved(d)), proj, handleIdsFor(targets));
|
|
25430
26194
|
this.layoutTextEditor();
|
|
@@ -25432,6 +26196,10 @@ var UserDrawingController = class {
|
|
|
25432
26196
|
if (ghost) this.painter.paintGhost(ctx, ghost, proj, this.deps.theme());
|
|
25433
26197
|
if (this.externalGhost) this.painter.paintGhost(ctx, this.externalGhost, proj, this.deps.theme());
|
|
25434
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
|
+
}
|
|
25435
26203
|
const markers = this.interaction.placingMarkers(proj);
|
|
25436
26204
|
if (markers) this.painter.paintHandles(ctx, markers);
|
|
25437
26205
|
const m = this.interaction.snapMarker();
|
|
@@ -25443,6 +26211,9 @@ var UserDrawingController = class {
|
|
|
25443
26211
|
this.closeTextEditor();
|
|
25444
26212
|
this.popup.destroy();
|
|
25445
26213
|
this.toolbar.destroy();
|
|
26214
|
+
this.seriesGwUnsub?.();
|
|
26215
|
+
this.seriesGwUnsub = null;
|
|
26216
|
+
this.seriesGw = null;
|
|
25446
26217
|
this.intentCb = null;
|
|
25447
26218
|
this.drawings = [];
|
|
25448
26219
|
}
|
|
@@ -25840,7 +26611,7 @@ function mergeSlices(indicator, user) {
|
|
|
25840
26611
|
}
|
|
25841
26612
|
|
|
25842
26613
|
// src/renderers/native/drawings/Projector.ts
|
|
25843
|
-
function createProjector(coords, paneOf, paneIdAtY, barsInRange) {
|
|
26614
|
+
function createProjector(coords, paneOf, paneIdAtY, barsInRange, seriesInRange) {
|
|
25844
26615
|
return {
|
|
25845
26616
|
xOf: (time) => coords.timeToX(time),
|
|
25846
26617
|
yOf: (price, paneId) => {
|
|
@@ -25861,6 +26632,7 @@ function createProjector(coords, paneOf, paneIdAtY, barsInRange) {
|
|
|
25861
26632
|
},
|
|
25862
26633
|
barsBetween: (t1, t2) => Math.abs(coords.timeToLogical(t2) - coords.timeToLogical(t1)),
|
|
25863
26634
|
barsInRange: barsInRange ? (from, to) => barsInRange(from, to) : void 0,
|
|
26635
|
+
seriesInRange,
|
|
25864
26636
|
width: coords.width,
|
|
25865
26637
|
height: coords.height
|
|
25866
26638
|
};
|
|
@@ -27831,13 +28603,13 @@ var NativeRenderer = class {
|
|
|
27831
28603
|
resetView: () => this.resetView(),
|
|
27832
28604
|
// User drawings claim a gesture before pan when armed / over a drawing.
|
|
27833
28605
|
drawingsClaim: (x, y) => this.userDrawings?.claim(x, y) ?? false,
|
|
27834
|
-
drawingsMeasureStart: (x, y) => this.userDrawings?.beginMeasureAt(x, y) ?? false,
|
|
28606
|
+
drawingsMeasureStart: (x, y, snap) => this.userDrawings?.beginMeasureAt(x, y, snap) ?? false,
|
|
27835
28607
|
drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y) ?? false,
|
|
27836
28608
|
drawingsCancelPlacement: () => this.userDrawings?.cancelPlacement() ?? false,
|
|
27837
28609
|
drawingsSnapMode: () => this.snapMode,
|
|
27838
28610
|
drawingsPointerDown: (x, y, snap, shift) => this.userDrawings?.pointerDown(x, y, snap, shift),
|
|
27839
28611
|
drawingsPointerMove: (x, y, snap, shift) => this.userDrawings?.pointerMove(x, y, snap, shift),
|
|
27840
|
-
drawingsPointerUp: (x, y) => this.userDrawings?.pointerUp(x, y),
|
|
28612
|
+
drawingsPointerUp: (x, y, snap) => this.userDrawings?.pointerUp(x, y, snap),
|
|
27841
28613
|
drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? null,
|
|
27842
28614
|
drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
|
|
27843
28615
|
drawingsClearTransient: () => this.userDrawings?.clearTransient()
|
|
@@ -27864,6 +28636,23 @@ var NativeRenderer = class {
|
|
|
27864
28636
|
seriesBoundaries: (paneId) => this.scene.seriesBoundaries(paneId),
|
|
27865
28637
|
priceZ: (paneId) => paneId === PRICE_PANE_ID ? this.scene.candleZ : null,
|
|
27866
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,
|
|
27867
28656
|
snap: (pt, paneId, mode, cursorPx) => this.snapToCandle(pt, paneId, mode, cursorPx),
|
|
27868
28657
|
setSnapMode: (mode) => this.setSnapMode(mode),
|
|
27869
28658
|
setToolbarGutter: (px) => this.setToolbarGutter(px)
|
|
@@ -28152,7 +28941,8 @@ var NativeRenderer = class {
|
|
|
28152
28941
|
}
|
|
28153
28942
|
const skipFit = opts?.preserveView === true && this.didInitialFit;
|
|
28154
28943
|
if (this.coords.width > 0 && !skipFit) {
|
|
28155
|
-
this.
|
|
28944
|
+
if (this.didInitialFit) this.reframeKeepZoom();
|
|
28945
|
+
else this.fitContent();
|
|
28156
28946
|
this.didInitialFit = true;
|
|
28157
28947
|
}
|
|
28158
28948
|
if (!this.introPlayed && this.bars.length > 0) {
|
|
@@ -28800,6 +29590,7 @@ var NativeRenderer = class {
|
|
|
28800
29590
|
this.scaleDragHeight = res.height;
|
|
28801
29591
|
this.scaleDragStart = { ...res.holder.scale };
|
|
28802
29592
|
res.holder.manualScale = { ...res.holder.scale };
|
|
29593
|
+
this.axisScaleButtons?.reposition();
|
|
28803
29594
|
this.scheduler.invalidate(4 /* Full */);
|
|
28804
29595
|
}
|
|
28805
29596
|
/** Rescale the grabbed scale around its center by the total drag (down ⇒ zoom out). */
|
|
@@ -28831,6 +29622,7 @@ var NativeRenderer = class {
|
|
|
28831
29622
|
const res = this.resolveScaleHolder(x, y);
|
|
28832
29623
|
if (!res) return;
|
|
28833
29624
|
res.holder.manualScale = null;
|
|
29625
|
+
this.axisScaleButtons?.reposition();
|
|
28834
29626
|
this.scheduler.invalidate(4 /* Full */);
|
|
28835
29627
|
}
|
|
28836
29628
|
/**
|
|
@@ -29254,7 +30046,8 @@ var NativeRenderer = class {
|
|
|
29254
30046
|
return p ? { scale: p.scale, bounds: p.bounds, collapsed: p.collapsed } : null;
|
|
29255
30047
|
},
|
|
29256
30048
|
(y) => this.paneNodeAtY(y)?.id ?? null,
|
|
29257
|
-
(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
|
|
29258
30051
|
);
|
|
29259
30052
|
}
|
|
29260
30053
|
/** OHLC bars whose open-time falls within `[from, to]` (inclusive) — the data a regression
|
|
@@ -29468,6 +30261,20 @@ var NativeRenderer = class {
|
|
|
29468
30261
|
this.coords.setViewport(v);
|
|
29469
30262
|
this.targetBarSpacing = v.barSpacing;
|
|
29470
30263
|
}
|
|
30264
|
+
/** Re-frame after a series replacement (a symbol/timeframe switch): keep the user's
|
|
30265
|
+
* zoom (bar spacing), re-anchor the newest bars at the default right offset.
|
|
30266
|
+
* `clampViewport`'s fit-all-bars floor deliberately does NOT apply — a progressive
|
|
30267
|
+
* head may still be backfilling toward the previous depth, and raising the spacing
|
|
30268
|
+
* to its temporary bar count would lose the zoom this exists to keep. */
|
|
30269
|
+
reframeKeepZoom() {
|
|
30270
|
+
this.animator?.stop();
|
|
30271
|
+
this.panVelocity = 0;
|
|
30272
|
+
for (const pane of this.scene.panes.values()) pane.manualScale = null;
|
|
30273
|
+
for (const sl of this.scene.indicatorScales.values()) sl.manualScale = null;
|
|
30274
|
+
const v = { barSpacing: clampBarSpacing(this.coords.getViewport().barSpacing), rightOffset: defaultViewport().rightOffset };
|
|
30275
|
+
this.coords.setViewport(v);
|
|
30276
|
+
this.targetBarSpacing = v.barSpacing;
|
|
30277
|
+
}
|
|
29471
30278
|
paneBoundsFor(paneId) {
|
|
29472
30279
|
const p = this.scene.panes.get(paneId);
|
|
29473
30280
|
return { top: p?.bounds.top ?? 0, height: p?.bounds.height ?? 0, rightAxis: this.rightAxisW };
|
|
@@ -30662,6 +31469,7 @@ exports.drawingTypes = drawingTypes;
|
|
|
30662
31469
|
exports.getDrawingType = getDrawingType;
|
|
30663
31470
|
exports.getNativeIndicator = getNativeIndicator;
|
|
30664
31471
|
exports.iconMarkup = iconMarkup;
|
|
31472
|
+
exports.inputDeltas = inputDeltas;
|
|
30665
31473
|
exports.inputVisible = inputVisible;
|
|
30666
31474
|
exports.legendActions = legendActions;
|
|
30667
31475
|
exports.legendCallouts = legendCallouts;
|