@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/widget.cjs
CHANGED
|
@@ -677,6 +677,7 @@ function intervalMs(timeframe) {
|
|
|
677
677
|
"4h": 144e5,
|
|
678
678
|
"1d": 864e5,
|
|
679
679
|
"1w": 6048e5,
|
|
680
|
+
"1M": 2592e6,
|
|
680
681
|
"1": 6e4,
|
|
681
682
|
"5": 3e5,
|
|
682
683
|
"15": 9e5,
|
|
@@ -684,7 +685,8 @@ function intervalMs(timeframe) {
|
|
|
684
685
|
"60": 36e5,
|
|
685
686
|
"240": 144e5,
|
|
686
687
|
D: 864e5,
|
|
687
|
-
W: 6048e5
|
|
688
|
+
W: 6048e5,
|
|
689
|
+
M: 2592e6
|
|
688
690
|
};
|
|
689
691
|
return map[timeframe] ?? 36e5;
|
|
690
692
|
}
|
|
@@ -11241,6 +11243,111 @@ function roundFloat(n) {
|
|
|
11241
11243
|
return Math.round(n * 1e8) / 1e8;
|
|
11242
11244
|
}
|
|
11243
11245
|
|
|
11246
|
+
// src/core/drawings/types/Magnifier.ts
|
|
11247
|
+
var MAGNIFIER_TIMEFRAME_OPTIONS = [
|
|
11248
|
+
{ value: "auto", label: "Auto", ms: 0 },
|
|
11249
|
+
{ value: "1", label: "1m", ms: 6e4 },
|
|
11250
|
+
{ value: "5", label: "5m", ms: 3e5 },
|
|
11251
|
+
{ value: "15", label: "15m", ms: 9e5 },
|
|
11252
|
+
{ value: "30", label: "30m", ms: 18e5 },
|
|
11253
|
+
{ value: "60", label: "1h", ms: 36e5 },
|
|
11254
|
+
{ value: "240", label: "4h", ms: 144e5 },
|
|
11255
|
+
{ value: "D", label: "1D", ms: 864e5 }
|
|
11256
|
+
];
|
|
11257
|
+
function magnifierTimeframeLabel(value) {
|
|
11258
|
+
const opt = MAGNIFIER_TIMEFRAME_OPTIONS.find((o) => o.value === value);
|
|
11259
|
+
if (opt) return opt.label;
|
|
11260
|
+
const n = Number(value);
|
|
11261
|
+
if (Number.isFinite(n) && n > 0) {
|
|
11262
|
+
if (n % 1440 === 0) return `${n / 1440}D`;
|
|
11263
|
+
if (n % 60 === 0) return `${n / 60}h`;
|
|
11264
|
+
return `${n}m`;
|
|
11265
|
+
}
|
|
11266
|
+
return value;
|
|
11267
|
+
}
|
|
11268
|
+
function defaultMagnifierStyle() {
|
|
11269
|
+
return { timeframe: "auto", upColor: "", downColor: "" };
|
|
11270
|
+
}
|
|
11271
|
+
var Magnifier = class extends Drawing {
|
|
11272
|
+
constructor(init) {
|
|
11273
|
+
super(init);
|
|
11274
|
+
this.type = "magnifier";
|
|
11275
|
+
/** Pixel rect of the timeframe chip as painted last frame, caret included — the chip is
|
|
11276
|
+
* an interactive dropdown trigger, so the interaction layer needs the exact rect the
|
|
11277
|
+
* painter measured. Renderer-transient: never serialized, null while unpainted. */
|
|
11278
|
+
this.chipRect = null;
|
|
11279
|
+
if (!this.magnifier) this.magnifier = defaultMagnifierStyle();
|
|
11280
|
+
}
|
|
11281
|
+
anchorSchema() {
|
|
11282
|
+
return { min: 2, max: 2, slots: [{ role: "c1", free: "both" }, { role: "c2", free: "both" }] };
|
|
11283
|
+
}
|
|
11284
|
+
placementMode() {
|
|
11285
|
+
return "drag";
|
|
11286
|
+
}
|
|
11287
|
+
/** The pixel rectangle between the two corner anchors (painter + hit-test share it). */
|
|
11288
|
+
rect(proj) {
|
|
11289
|
+
const a = this.anchors[0];
|
|
11290
|
+
const b = this.anchors[1];
|
|
11291
|
+
if (!a || !b) return null;
|
|
11292
|
+
const ya = proj.yOf(a.price, this.paneId);
|
|
11293
|
+
const yb = proj.yOf(b.price, this.paneId);
|
|
11294
|
+
if (ya == null || yb == null) return null;
|
|
11295
|
+
return { x1: proj.xOf(a.time), y1: ya, x2: proj.xOf(b.time), y2: yb };
|
|
11296
|
+
}
|
|
11297
|
+
hitTest(px, py, proj, tol) {
|
|
11298
|
+
const r = this.rect(proj);
|
|
11299
|
+
if (!r) return false;
|
|
11300
|
+
if (pointInBox(px, py, r.x1, r.y1, r.x2, r.y2)) return true;
|
|
11301
|
+
const edges = [
|
|
11302
|
+
[r.x1, r.y1, r.x2, r.y1],
|
|
11303
|
+
[r.x2, r.y1, r.x2, r.y2],
|
|
11304
|
+
[r.x2, r.y2, r.x1, r.y2],
|
|
11305
|
+
[r.x1, r.y2, r.x1, r.y1]
|
|
11306
|
+
];
|
|
11307
|
+
return edges.some((e) => distToSegment(px, py, e[0], e[1], e[2], e[3]) <= tol);
|
|
11308
|
+
}
|
|
11309
|
+
handlePoints(proj) {
|
|
11310
|
+
const r = this.rect(proj);
|
|
11311
|
+
return r ? [[r.x1, r.y1], [r.x2, r.y2]] : [];
|
|
11312
|
+
}
|
|
11313
|
+
hitHandle(px, py, proj, tol) {
|
|
11314
|
+
return handleAt(px, py, this.handlePoints(proj), tol + 3);
|
|
11315
|
+
}
|
|
11316
|
+
bounds(proj) {
|
|
11317
|
+
const r = this.rect(proj);
|
|
11318
|
+
if (!r) return null;
|
|
11319
|
+
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) };
|
|
11320
|
+
}
|
|
11321
|
+
priceRange() {
|
|
11322
|
+
const a = this.anchors[0];
|
|
11323
|
+
const b = this.anchors[1];
|
|
11324
|
+
if (!a || !b) return null;
|
|
11325
|
+
return { min: Math.min(a.price, b.price), max: Math.max(a.price, b.price) };
|
|
11326
|
+
}
|
|
11327
|
+
schema() {
|
|
11328
|
+
return {
|
|
11329
|
+
fields: [
|
|
11330
|
+
{
|
|
11331
|
+
path: "magnifier.timeframe",
|
|
11332
|
+
label: "Timeframe",
|
|
11333
|
+
kind: "select",
|
|
11334
|
+
options: MAGNIFIER_TIMEFRAME_OPTIONS,
|
|
11335
|
+
group: "behavior"
|
|
11336
|
+
},
|
|
11337
|
+
...LINE_FIELDS.map((f) => ({ ...f, label: f.label.replace("Line", "Border") })),
|
|
11338
|
+
{ path: "magnifier.upColor", label: "Up candles", kind: "color", group: "fill" },
|
|
11339
|
+
{ path: "magnifier.downColor", label: "Down candles", kind: "color", group: "fill" }
|
|
11340
|
+
]
|
|
11341
|
+
};
|
|
11342
|
+
}
|
|
11343
|
+
writeProps() {
|
|
11344
|
+
return { ...this.magnifier };
|
|
11345
|
+
}
|
|
11346
|
+
readProps(props) {
|
|
11347
|
+
this.magnifier = { ...defaultMagnifierStyle(), ...props };
|
|
11348
|
+
}
|
|
11349
|
+
};
|
|
11350
|
+
|
|
11244
11351
|
// src/core/drawings/registry.ts
|
|
11245
11352
|
var REGISTRY = /* @__PURE__ */ new Map();
|
|
11246
11353
|
function registerDrawingType(meta) {
|
|
@@ -11900,6 +12007,22 @@ registerDrawingType({
|
|
|
11900
12007
|
defaultStyle: { lineColor: DEFAULT_DRAWING_COLOR, lineWidth: 1, lineStyle: "solid" },
|
|
11901
12008
|
create: (init) => new PositionTool(init)
|
|
11902
12009
|
});
|
|
12010
|
+
var MAGNIFIER_ICON = svg24(
|
|
12011
|
+
'<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"/>'
|
|
12012
|
+
);
|
|
12013
|
+
registerDrawingType({
|
|
12014
|
+
type: "magnifier",
|
|
12015
|
+
group: "measure",
|
|
12016
|
+
label: "Magnifier",
|
|
12017
|
+
icon: MAGNIFIER_ICON,
|
|
12018
|
+
// An empty border color means the THEME's contrast ink (white on dark, black on
|
|
12019
|
+
// light), resolved at paint time so it follows theme switches; a user pick wins.
|
|
12020
|
+
defaultStyle: { lineColor: "", lineWidth: 1, lineStyle: "solid" },
|
|
12021
|
+
coversSeries: true,
|
|
12022
|
+
// the inset's backdrop must sit over the base candles it replaces
|
|
12023
|
+
placementHint: "Drag an area on the chart to view it at a lower timeframe",
|
|
12024
|
+
create: (init) => new Magnifier(init)
|
|
12025
|
+
});
|
|
11903
12026
|
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"/>');
|
|
11904
12027
|
registerDrawingType({
|
|
11905
12028
|
type: "anchoredvwap",
|
|
@@ -16133,7 +16256,7 @@ var GEOMETRY_TYPES = ["dedekind", "sonic", "supersonic", "goldensonic", "goldens
|
|
|
16133
16256
|
var PATTERN_TYPES = ["xabcd", "abcd", "headshoulders"];
|
|
16134
16257
|
var ELLIOTT_TYPES = ["elliottimpulse", "elliottcorrection"];
|
|
16135
16258
|
var HARMONIC_TYPES = ["gartley", "bat", "butterfly", "crab", "shark", "cypher"];
|
|
16136
|
-
var MEASUREMENT_TYPES = ["position", "datepricerange"];
|
|
16259
|
+
var MEASUREMENT_TYPES = ["position", "datepricerange", "magnifier"];
|
|
16137
16260
|
var VOLUME_TYPES = ["anchoredvwap", "fixedrangevp"];
|
|
16138
16261
|
var BRUSH_TYPES = ["freehand", "highlighter"];
|
|
16139
16262
|
var ARROW_TYPES = ["arrow", "arrowmarkup", "arrowmarkdown"];
|
|
@@ -16269,7 +16392,7 @@ function buildToolbar(option) {
|
|
|
16269
16392
|
|
|
16270
16393
|
// src/core/drawings/DrawingController.ts
|
|
16271
16394
|
var DrawingController = class {
|
|
16272
|
-
constructor(renderer, events, option) {
|
|
16395
|
+
constructor(renderer, events, option, seriesGateway) {
|
|
16273
16396
|
this.events = events;
|
|
16274
16397
|
this.store = new DrawingStore();
|
|
16275
16398
|
this.history = new DrawingHistory();
|
|
@@ -16295,6 +16418,7 @@ var DrawingController = class {
|
|
|
16295
16418
|
const { definition, visible } = buildToolbar(option);
|
|
16296
16419
|
this.port.setToolbar(definition);
|
|
16297
16420
|
this.port.showToolbar(visible);
|
|
16421
|
+
if (seriesGateway) this.port.setSeriesGateway?.(seriesGateway);
|
|
16298
16422
|
this.subs.push(this.port.onDrawingIntent((i) => this.onIntent(i)));
|
|
16299
16423
|
this.subs.push(this.store.onChange(() => this.sync()));
|
|
16300
16424
|
}
|
|
@@ -16400,7 +16524,7 @@ var DrawingController = class {
|
|
|
16400
16524
|
style,
|
|
16401
16525
|
text: init.text,
|
|
16402
16526
|
props: init.props,
|
|
16403
|
-
zIndex: init.zIndex ?? this.startZ(init.paneId ?? "price")
|
|
16527
|
+
zIndex: init.zIndex ?? this.startZ(type, init.paneId ?? "price")
|
|
16404
16528
|
});
|
|
16405
16529
|
if (!d) return null;
|
|
16406
16530
|
this.history.record(this.store.serialize());
|
|
@@ -16475,10 +16599,14 @@ var DrawingController = class {
|
|
|
16475
16599
|
* (falling back to just under the pane's top series where there is no price — a study
|
|
16476
16600
|
* pane). Half a key down never ties a series; drawings tying each other paint in insertion
|
|
16477
16601
|
* order, so consecutive new drawings still stack newest-in-front. Undefined without a
|
|
16478
|
-
* shared z space — the store then places it over the other drawings, its own layer's top.
|
|
16479
|
-
|
|
16602
|
+
* shared z space — the store then places it over the other drawings, its own layer's top.
|
|
16603
|
+
* A type that COVERS the series (an opaque inset, `coversSeries`) instead starts just
|
|
16604
|
+
* above the whole stack — under the candles its content would be buried. */
|
|
16605
|
+
startZ(type, paneId) {
|
|
16480
16606
|
const range = this.port?.stackRange?.(paneId);
|
|
16481
|
-
|
|
16607
|
+
if (!range) return void 0;
|
|
16608
|
+
if (getDrawingType(type)?.coversSeries) return range.front + 0.5;
|
|
16609
|
+
return (range.price ?? range.front) - 0.5;
|
|
16482
16610
|
}
|
|
16483
16611
|
/** Programmatically select drawings (host UI → chart): shows the on-chart handles + toolbar.
|
|
16484
16612
|
* `additive` toggles membership (matching shift-click) instead of replacing. */
|
|
@@ -16623,7 +16751,7 @@ var DrawingController = class {
|
|
|
16623
16751
|
const style = last ? { ...i.doc.style, ...last } : i.doc.style;
|
|
16624
16752
|
const d = deserializeDrawing({ ...i.doc, id: this.store.nextId(), style });
|
|
16625
16753
|
if (!d) return;
|
|
16626
|
-
if (!d.zIndex) d.zIndex = this.startZ(d.paneId) ?? 0;
|
|
16754
|
+
if (!d.zIndex) d.zIndex = this.startZ(d.type, d.paneId) ?? 0;
|
|
16627
16755
|
this.history.record(before);
|
|
16628
16756
|
this.store.add(d);
|
|
16629
16757
|
this.captureStyle(d.id);
|
|
@@ -18141,6 +18269,180 @@ function presetToRange(preset, bars) {
|
|
|
18141
18269
|
return { from: Math.max(first, from), to: last };
|
|
18142
18270
|
}
|
|
18143
18271
|
|
|
18272
|
+
// src/core/engine/DrawingSeriesService.ts
|
|
18273
|
+
var MAX_BARS = 5e3;
|
|
18274
|
+
var PAD_FRAC = 0.25;
|
|
18275
|
+
var MAX_ENTRIES = 16;
|
|
18276
|
+
var RETRY_MS = 15e3;
|
|
18277
|
+
var AUTO_STEPS = ["240", "60", "30", "15", "5", "1"];
|
|
18278
|
+
var DrawingSeriesService = class {
|
|
18279
|
+
constructor(deps) {
|
|
18280
|
+
this.deps = deps;
|
|
18281
|
+
/** Cached windows per `market|timeframe` key, newest-used last (LRU across keys). */
|
|
18282
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
18283
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
18284
|
+
}
|
|
18285
|
+
seriesInRange(timeframe, from, to) {
|
|
18286
|
+
if (!this.deps.canFetch()) return { state: "unavailable", reason: "no-source" };
|
|
18287
|
+
const resolved = this.resolveTimeframe(timeframe);
|
|
18288
|
+
if (typeof resolved !== "string") return { state: "unavailable", reason: resolved.reason };
|
|
18289
|
+
const barMs = timeframeToMs(resolved);
|
|
18290
|
+
const lo = Math.min(from, to);
|
|
18291
|
+
const hi = Math.max(from, to);
|
|
18292
|
+
if (!(hi > lo) || !(barMs > 0)) return { state: "unavailable", reason: "not-lower" };
|
|
18293
|
+
if ((hi - lo) / barMs > MAX_BARS) return { state: "unavailable", reason: "too-wide" };
|
|
18294
|
+
const key = `${this.deps.marketKey()}|${resolved}`;
|
|
18295
|
+
const entries = this.cache.get(key) ?? [];
|
|
18296
|
+
const covering = entries.find((e) => e.from <= lo && e.to >= hi);
|
|
18297
|
+
if (covering) {
|
|
18298
|
+
if (covering.pending) return this.loading(entries, resolved, barMs, lo, hi);
|
|
18299
|
+
if (covering.failedAt > 0) {
|
|
18300
|
+
if (Date.now() - covering.failedAt < RETRY_MS) return this.loading(entries, resolved, barMs, lo, hi);
|
|
18301
|
+
entries.splice(entries.indexOf(covering), 1);
|
|
18302
|
+
} else {
|
|
18303
|
+
this.maybeRefresh(key, covering, resolved, barMs, hi);
|
|
18304
|
+
return { state: "ready", bars: this.slice(covering.bars, lo, hi), timeframe: resolved, barMs };
|
|
18305
|
+
}
|
|
18306
|
+
}
|
|
18307
|
+
this.fetchWindow(key, entries, resolved, lo, hi);
|
|
18308
|
+
return this.loading(entries, resolved, barMs, lo, hi);
|
|
18309
|
+
}
|
|
18310
|
+
onUpdate(listener) {
|
|
18311
|
+
this.listeners.add(listener);
|
|
18312
|
+
return () => this.listeners.delete(listener);
|
|
18313
|
+
}
|
|
18314
|
+
// ── internals ──
|
|
18315
|
+
/** `'auto'` → the largest standard step at least 4× finer than the chart (else the finest
|
|
18316
|
+
* step still below it); an explicit timeframe passes only when strictly finer. Failures
|
|
18317
|
+
* distinguish "this pick isn't lower" from "NOTHING lower exists" (the chart is already
|
|
18318
|
+
* at the finest offered step) so the consumer can word its notice honestly. */
|
|
18319
|
+
resolveTimeframe(timeframe) {
|
|
18320
|
+
const chartMs = timeframeToMs(this.deps.chartTimeframe());
|
|
18321
|
+
const finest = AUTO_STEPS[AUTO_STEPS.length - 1];
|
|
18322
|
+
if (timeframeToMs(finest) >= chartMs) return { reason: "none-lower" };
|
|
18323
|
+
const tf = timeframe.trim() || "auto";
|
|
18324
|
+
if (tf === "auto") {
|
|
18325
|
+
for (const step of AUTO_STEPS) {
|
|
18326
|
+
if (timeframeToMs(step) <= chartMs / 4) return step;
|
|
18327
|
+
}
|
|
18328
|
+
return finest;
|
|
18329
|
+
}
|
|
18330
|
+
return timeframeToMs(tf) < chartMs ? tf : { reason: "not-lower" };
|
|
18331
|
+
}
|
|
18332
|
+
/** The `loading` answer, carrying best-effort PARTIAL bars from settled overlapping
|
|
18333
|
+
* windows — a widened window keeps painting what it already has while it fetches. */
|
|
18334
|
+
loading(entries, timeframe, barMs, lo, hi) {
|
|
18335
|
+
const partial = /* @__PURE__ */ new Map();
|
|
18336
|
+
for (const e of entries) {
|
|
18337
|
+
if (e.pending || e.failedAt > 0) continue;
|
|
18338
|
+
if (e.to < lo || e.from > hi) continue;
|
|
18339
|
+
for (const b of this.slice(e.bars, lo, hi)) partial.set(b.time, b);
|
|
18340
|
+
}
|
|
18341
|
+
if (partial.size === 0) return { state: "loading", timeframe, barMs };
|
|
18342
|
+
const bars = [...partial.values()].sort((a, b) => a.time - b.time);
|
|
18343
|
+
return { state: "loading", timeframe, barMs, bars };
|
|
18344
|
+
}
|
|
18345
|
+
/** Kick ONE background fetch for the padded window. Any OVERLAPPING in-flight fetch
|
|
18346
|
+
* defers this one (a corner drag repaints per pointer move — kicking a window per
|
|
18347
|
+
* frame would spam the provider); when it lands, the next paint re-evaluates. */
|
|
18348
|
+
fetchWindow(key, entries, timeframe, lo, hi) {
|
|
18349
|
+
if (entries.some((e) => e.pending && e.to >= lo && e.from <= hi)) return;
|
|
18350
|
+
const pad = (hi - lo) * PAD_FRAC;
|
|
18351
|
+
const entry = { from: lo - pad, to: hi + pad, bars: [], fetchedAt: 0, pending: true, failedAt: 0 };
|
|
18352
|
+
entries.push(entry);
|
|
18353
|
+
this.cache.set(key, entries);
|
|
18354
|
+
this.evict();
|
|
18355
|
+
void this.deps.fetchBars(timeframe, { from: entry.from, to: entry.to }).then((bars) => {
|
|
18356
|
+
entry.bars = bars;
|
|
18357
|
+
entry.fetchedAt = Date.now();
|
|
18358
|
+
entry.pending = false;
|
|
18359
|
+
this.absorbOverlaps(key, entry);
|
|
18360
|
+
this.fire();
|
|
18361
|
+
}).catch(() => {
|
|
18362
|
+
entry.pending = false;
|
|
18363
|
+
entry.failedAt = Date.now();
|
|
18364
|
+
this.fire();
|
|
18365
|
+
});
|
|
18366
|
+
}
|
|
18367
|
+
/** A window whose right edge reaches the newest fetched bar refreshes at most once per
|
|
18368
|
+
* bar interval — new closed bars ride the feed's cache, only the live tail re-fetches. */
|
|
18369
|
+
maybeRefresh(key, entry, timeframe, barMs, hi) {
|
|
18370
|
+
if (entry.pending) return;
|
|
18371
|
+
const lastBar = entry.bars.length > 0 ? entry.bars[entry.bars.length - 1].time : entry.from;
|
|
18372
|
+
if (hi < lastBar) return;
|
|
18373
|
+
if (Date.now() - entry.fetchedAt < barMs) return;
|
|
18374
|
+
entry.pending = true;
|
|
18375
|
+
void this.deps.fetchBars(timeframe, { from: entry.from, to: entry.to }).then((bars) => {
|
|
18376
|
+
entry.bars = bars;
|
|
18377
|
+
entry.fetchedAt = Date.now();
|
|
18378
|
+
entry.pending = false;
|
|
18379
|
+
this.fire();
|
|
18380
|
+
}).catch(() => {
|
|
18381
|
+
entry.pending = false;
|
|
18382
|
+
entry.fetchedAt = Date.now();
|
|
18383
|
+
});
|
|
18384
|
+
}
|
|
18385
|
+
/** Merge windows that overlap `entry` into it (dedupe by bar time) so a key's list
|
|
18386
|
+
* converges instead of accumulating slivers. */
|
|
18387
|
+
absorbOverlaps(key, entry) {
|
|
18388
|
+
const entries = this.cache.get(key);
|
|
18389
|
+
if (!entries) return;
|
|
18390
|
+
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
18391
|
+
const other = entries[i];
|
|
18392
|
+
if (other === entry || other.pending || other.failedAt > 0) continue;
|
|
18393
|
+
if (other.to < entry.from || other.from > entry.to) continue;
|
|
18394
|
+
const byTime = /* @__PURE__ */ new Map();
|
|
18395
|
+
for (const b of other.bars) byTime.set(b.time, b);
|
|
18396
|
+
for (const b of entry.bars) byTime.set(b.time, b);
|
|
18397
|
+
entry.bars = [...byTime.values()].sort((a, b) => a.time - b.time);
|
|
18398
|
+
entry.from = Math.min(entry.from, other.from);
|
|
18399
|
+
entry.to = Math.max(entry.to, other.to);
|
|
18400
|
+
entry.fetchedAt = Math.min(entry.fetchedAt, other.fetchedAt || entry.fetchedAt);
|
|
18401
|
+
entries.splice(i, 1);
|
|
18402
|
+
}
|
|
18403
|
+
}
|
|
18404
|
+
/** Drop the oldest settled windows once the global count passes {@link MAX_ENTRIES}. */
|
|
18405
|
+
evict() {
|
|
18406
|
+
let total = 0;
|
|
18407
|
+
for (const entries of this.cache.values()) total += entries.length;
|
|
18408
|
+
while (total > MAX_ENTRIES) {
|
|
18409
|
+
let oldestKey = null;
|
|
18410
|
+
let oldestIdx = -1;
|
|
18411
|
+
let oldestAt = Infinity;
|
|
18412
|
+
for (const [key, entries2] of this.cache) {
|
|
18413
|
+
for (let i = 0; i < entries2.length; i += 1) {
|
|
18414
|
+
const e = entries2[i];
|
|
18415
|
+
if (e.pending) continue;
|
|
18416
|
+
const at = e.fetchedAt || e.failedAt;
|
|
18417
|
+
if (at < oldestAt) {
|
|
18418
|
+
oldestKey = key;
|
|
18419
|
+
oldestIdx = i;
|
|
18420
|
+
oldestAt = at;
|
|
18421
|
+
}
|
|
18422
|
+
}
|
|
18423
|
+
}
|
|
18424
|
+
if (oldestKey == null) return;
|
|
18425
|
+
const entries = this.cache.get(oldestKey);
|
|
18426
|
+
entries.splice(oldestIdx, 1);
|
|
18427
|
+
if (entries.length === 0) this.cache.delete(oldestKey);
|
|
18428
|
+
total -= 1;
|
|
18429
|
+
}
|
|
18430
|
+
}
|
|
18431
|
+
/** Bars whose open time falls within `[lo, hi]` (ascending input → linear scan is fine). */
|
|
18432
|
+
slice(bars, lo, hi) {
|
|
18433
|
+
const out = [];
|
|
18434
|
+
for (const b of bars) {
|
|
18435
|
+
if (b.time < lo) continue;
|
|
18436
|
+
if (b.time > hi) break;
|
|
18437
|
+
out.push(b);
|
|
18438
|
+
}
|
|
18439
|
+
return out;
|
|
18440
|
+
}
|
|
18441
|
+
fire() {
|
|
18442
|
+
for (const l of [...this.listeners]) l();
|
|
18443
|
+
}
|
|
18444
|
+
};
|
|
18445
|
+
|
|
18144
18446
|
// src/core/price-styles/BarTransform.ts
|
|
18145
18447
|
function barTransformFor(style) {
|
|
18146
18448
|
return chartType(style)?.barTransform ?? null;
|
|
@@ -18159,7 +18461,6 @@ var RUN_EMIT_THROTTLE_MS = 1e3;
|
|
|
18159
18461
|
var PREVIEW_BARS = 300;
|
|
18160
18462
|
var SINGLE_LOAD_BARS = 5e3;
|
|
18161
18463
|
var CHUNK_BARS = 1e4;
|
|
18162
|
-
var FIRST_PAINT_BARS = 100;
|
|
18163
18464
|
var GAP_FACTOR = 1.5;
|
|
18164
18465
|
var HEAL_COOLDOWN_MS = 5e3;
|
|
18165
18466
|
var EngineOrchestrator = class _EngineOrchestrator {
|
|
@@ -18277,7 +18578,13 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18277
18578
|
const initialStyle = this.renderer.readFeature("priceStyle");
|
|
18278
18579
|
if (typeof initialStyle === "string") this.priceStyle = initialStyle;
|
|
18279
18580
|
this.barTransform = barTransformFor(initialStyle);
|
|
18280
|
-
|
|
18581
|
+
const drawingSeries = new DrawingSeriesService({
|
|
18582
|
+
fetchBars: (tf, range) => this.fetchSeries(this.config.market.symbol ?? "", tf, range),
|
|
18583
|
+
canFetch: () => !!this.feed.loadRange && !this.config.market.data?.length && !!this.config.market.symbol,
|
|
18584
|
+
chartTimeframe: () => this.config.market.timeframe ?? "60",
|
|
18585
|
+
marketKey: () => `${this.config.market.symbol ?? ""}|${this.config.market.session ?? ""}`
|
|
18586
|
+
});
|
|
18587
|
+
this.drawings = new DrawingController(this.renderer, this.events, config.drawings, drawingSeries);
|
|
18281
18588
|
this.unresolvedUnsub = this.feed.onUnresolved?.((info) => {
|
|
18282
18589
|
this.endLoad();
|
|
18283
18590
|
this.events.emit("data:unresolved", info);
|
|
@@ -18429,7 +18736,6 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18429
18736
|
let painted = false;
|
|
18430
18737
|
const paint = (bars, final) => {
|
|
18431
18738
|
if (this.generation !== gen || !final && bars.length === 0) return;
|
|
18432
|
-
if (!painted && !final && bars.length < Math.min(requested, FIRST_PAINT_BARS)) return;
|
|
18433
18739
|
this.setBarSeries(bars, painted ? { preserveView: true } : void 0);
|
|
18434
18740
|
if (!painted && bars.length > 0) {
|
|
18435
18741
|
painted = true;
|
|
@@ -18448,10 +18754,14 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18448
18754
|
}
|
|
18449
18755
|
};
|
|
18450
18756
|
abort.signal.addEventListener("abort", () => signal(true), { once: true });
|
|
18451
|
-
this.feed.loadProgressive(
|
|
18452
|
-
|
|
18453
|
-
|
|
18454
|
-
|
|
18757
|
+
this.feed.loadProgressive(
|
|
18758
|
+
market,
|
|
18759
|
+
(bars) => {
|
|
18760
|
+
paint(bars, false);
|
|
18761
|
+
if (painted) signal(true);
|
|
18762
|
+
},
|
|
18763
|
+
{ signal: abort.signal }
|
|
18764
|
+
).then((full) => {
|
|
18455
18765
|
if (this.progressiveAbort === abort) this.progressiveAbort = null;
|
|
18456
18766
|
if (full == null) return signal(false);
|
|
18457
18767
|
if (this.generation !== gen) return signal(true);
|
|
@@ -18538,7 +18848,14 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18538
18848
|
* an in-flight `setMarket` immediately (the config mutates before the load). */
|
|
18539
18849
|
marketSnapshot() {
|
|
18540
18850
|
const m = this.config.market;
|
|
18541
|
-
return {
|
|
18851
|
+
return {
|
|
18852
|
+
symbol: m.symbol,
|
|
18853
|
+
provider: parseSymbol(m.symbol ?? "").provider ?? void 0,
|
|
18854
|
+
timeframe: m.timeframe,
|
|
18855
|
+
bars: m.bars,
|
|
18856
|
+
session: m.session,
|
|
18857
|
+
offline: m.data !== void 0
|
|
18858
|
+
};
|
|
18542
18859
|
}
|
|
18543
18860
|
/**
|
|
18544
18861
|
* Switch the chart's market IN PLACE — no destroy/recreate. The renderer stays
|
|
@@ -18604,10 +18921,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18604
18921
|
if (depthOnly) {
|
|
18605
18922
|
this.extendDepth(gen, m.bars ?? 500);
|
|
18606
18923
|
} else {
|
|
18607
|
-
await Promise.race([
|
|
18608
|
-
this.loadMarket(gen, { firstLoad: false }),
|
|
18609
|
-
new Promise((resolve) => this.supersedeWaiters.push(resolve))
|
|
18610
|
-
]);
|
|
18924
|
+
await Promise.race([this.loadMarket(gen, { firstLoad: false }), new Promise((resolve) => this.supersedeWaiters.push(resolve))]);
|
|
18611
18925
|
}
|
|
18612
18926
|
if (this.generation !== gen) return;
|
|
18613
18927
|
} finally {
|
|
@@ -19270,8 +19584,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19270
19584
|
onModel: (model) => {
|
|
19271
19585
|
const first = !record.announced;
|
|
19272
19586
|
const cause = record.pendingCause ?? "history";
|
|
19587
|
+
if (!this.applyModel(id, model)) return;
|
|
19273
19588
|
record.pendingCause = void 0;
|
|
19274
|
-
this.applyModel(id, model);
|
|
19275
19589
|
this.emitContextChanged(id);
|
|
19276
19590
|
this.emitScriptRun(id, cause, first);
|
|
19277
19591
|
},
|
|
@@ -19564,10 +19878,16 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19564
19878
|
* Apply an emitted model. First emission mounts (and routes the pane); a pending
|
|
19565
19879
|
* structural change (after an input edit) remounts idempotently; everything else
|
|
19566
19880
|
* (live tick / viewport re-run) value-patches.
|
|
19881
|
+
*
|
|
19882
|
+
* Returns false when the model was DEFERRED — an output-free model arriving while
|
|
19883
|
+
* the record is still loading and the chart has no bars (see below); every other
|
|
19884
|
+
* outcome, including the hidden drop, returns true so the caller's event semantics
|
|
19885
|
+
* stay unchanged.
|
|
19567
19886
|
*/
|
|
19568
19887
|
applyModel(id, model) {
|
|
19569
19888
|
const record = this.registry.get(id);
|
|
19570
|
-
if (!record || record.hidden) return;
|
|
19889
|
+
if (!record || record.hidden) return true;
|
|
19890
|
+
if (record.loading && this.bars.length === 0 && !_EngineOrchestrator.modelHasOutput(model)) return false;
|
|
19571
19891
|
const handle = this.handles.get(id);
|
|
19572
19892
|
if (!record.renderHandle) {
|
|
19573
19893
|
const paneId2 = this.routePane(id, model, record.options ?? {});
|
|
@@ -19577,7 +19897,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19577
19897
|
record.renderHandle = this.renderer.mountIndicator(model);
|
|
19578
19898
|
record.pendingStructural = false;
|
|
19579
19899
|
this.announce(record, handle);
|
|
19580
|
-
return;
|
|
19900
|
+
return true;
|
|
19581
19901
|
}
|
|
19582
19902
|
let paneId = record.model?.paneId ?? "price";
|
|
19583
19903
|
const prevOwnScale = record.model?.ownScale === true;
|
|
@@ -19605,6 +19925,11 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19605
19925
|
}
|
|
19606
19926
|
if (record.loading) this.setLoading(record, false);
|
|
19607
19927
|
this.announce(record, handle);
|
|
19928
|
+
return true;
|
|
19929
|
+
}
|
|
19930
|
+
/** True when the model carries ANY executed output — series, drawings, bar colors, or trades. */
|
|
19931
|
+
static modelHasOutput(model) {
|
|
19932
|
+
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;
|
|
19608
19933
|
}
|
|
19609
19934
|
routePane(id, model, options) {
|
|
19610
19935
|
if (options.pane === "new") return `pane-${id}`;
|
|
@@ -25739,7 +26064,9 @@ var DrawingSceneRenderer = class {
|
|
|
25739
26064
|
const lo = Math.min(from, to);
|
|
25740
26065
|
const hi = Math.max(from, to);
|
|
25741
26066
|
const visible = (a, b, extend) => {
|
|
25742
|
-
if (extend
|
|
26067
|
+
if (extend === "both") return true;
|
|
26068
|
+
if (extend === "left") return Math.max(a, b) >= lo;
|
|
26069
|
+
if (extend === "right") return Math.min(a, b) <= hi;
|
|
25743
26070
|
return Math.max(a, b) >= lo && Math.min(a, b) <= hi;
|
|
25744
26071
|
};
|
|
25745
26072
|
let min = Infinity;
|
|
@@ -28213,6 +28540,17 @@ var DrawingPainter = class {
|
|
|
28213
28540
|
constructor() {
|
|
28214
28541
|
/** The current `paintAll` call's interaction state, visible to the per-type painters. */
|
|
28215
28542
|
this.targets = {};
|
|
28543
|
+
/** The chart's active series LOOK — style + resolved series colors — pushed by the
|
|
28544
|
+
* controller before each paint. The magnifier's inset mirrors both: candles/bars/line/
|
|
28545
|
+
* area restyle the paint (bar-transform styles like Heikin Ashi transform the fetched
|
|
28546
|
+
* bars; unknown/custom styles fall back to candles), and the colors default to the main
|
|
28547
|
+
* series' own so the inset reads as a finer copy of the chart. */
|
|
28548
|
+
this.seriesLook = {
|
|
28549
|
+
style: "candles",
|
|
28550
|
+
upColor: BULLISH,
|
|
28551
|
+
downColor: BEARISH,
|
|
28552
|
+
lineColor: BULLISH
|
|
28553
|
+
};
|
|
28216
28554
|
}
|
|
28217
28555
|
/** Paint every visible drawing, then selection handles for the targeted ones.
|
|
28218
28556
|
* Each drawing is clipped to its own pane's rect (and skipped entirely while that pane
|
|
@@ -28257,6 +28595,8 @@ var DrawingPainter = class {
|
|
|
28257
28595
|
ctx.globalAlpha = GHOST_ALPHA;
|
|
28258
28596
|
if (ghost instanceof RegressionChannel || ghost instanceof FixedRangeVolumeProfile) {
|
|
28259
28597
|
this.paintTimeSpanGhost(ctx, ghost, proj);
|
|
28598
|
+
} else if (ghost instanceof Magnifier) {
|
|
28599
|
+
this.paintMagnifierGhost(ctx, ghost, proj, theme);
|
|
28260
28600
|
} else this.paintOne(ctx, ghost, proj, theme);
|
|
28261
28601
|
ctx.globalAlpha = 1;
|
|
28262
28602
|
}
|
|
@@ -28390,6 +28730,10 @@ var DrawingPainter = class {
|
|
|
28390
28730
|
this.paintLabel(ctx, d, proj, theme);
|
|
28391
28731
|
return;
|
|
28392
28732
|
}
|
|
28733
|
+
if (d instanceof Magnifier) {
|
|
28734
|
+
this.paintMagnifier(ctx, d, proj, theme);
|
|
28735
|
+
return;
|
|
28736
|
+
}
|
|
28393
28737
|
if (d instanceof PatternDrawing) {
|
|
28394
28738
|
this.paintPattern(ctx, d, proj, theme);
|
|
28395
28739
|
return;
|
|
@@ -28882,6 +29226,216 @@ var DrawingPainter = class {
|
|
|
28882
29226
|
ctx.textBaseline = "alphabetic";
|
|
28883
29227
|
}
|
|
28884
29228
|
}
|
|
29229
|
+
/** Placement preview for the magnifier: a dashed rectangle outline only — no backdrop and
|
|
29230
|
+
* no series read, so dragging the area open never kicks a fetch per cursor move. */
|
|
29231
|
+
paintMagnifierGhost(ctx, d, proj, theme) {
|
|
29232
|
+
const r = d.rect(proj);
|
|
29233
|
+
if (!r) return;
|
|
29234
|
+
ctx.save();
|
|
29235
|
+
ctx.strokeStyle = d.style.lineColor || contrastColor(theme.background);
|
|
29236
|
+
ctx.lineWidth = 1;
|
|
29237
|
+
ctx.setLineDash([4, 4]);
|
|
29238
|
+
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));
|
|
29239
|
+
ctx.restore();
|
|
29240
|
+
}
|
|
29241
|
+
/** Paint a magnifier: an opaque theme-background inset whose interior shows the chart's
|
|
29242
|
+
* market at a finer timeframe — candles at their true time/price positions, clipped to
|
|
29243
|
+
* the rectangle. Bars come through `Projector.seriesInRange` (cache read; `loading` and
|
|
29244
|
+
* `unavailable` states paint a short notice instead). The lower-timeframe candles shift
|
|
29245
|
+
* half a chart bar LEFT of their raw time pixel so each chart candle's visual cell —
|
|
29246
|
+
* centered on its open time — subdivides in place. */
|
|
29247
|
+
paintMagnifier(ctx, d, proj, theme) {
|
|
29248
|
+
const r = d.rect(proj);
|
|
29249
|
+
const a = d.anchors[0];
|
|
29250
|
+
const b = d.anchors[1];
|
|
29251
|
+
if (!r || !a || !b) return;
|
|
29252
|
+
const x0 = Math.min(r.x1, r.x2);
|
|
29253
|
+
const x1 = Math.max(r.x1, r.x2);
|
|
29254
|
+
const y0 = Math.min(r.y1, r.y2);
|
|
29255
|
+
const y1 = Math.max(r.y1, r.y2);
|
|
29256
|
+
const w = x1 - x0;
|
|
29257
|
+
const h = y1 - y0;
|
|
29258
|
+
ctx.save();
|
|
29259
|
+
ctx.globalAlpha = 1;
|
|
29260
|
+
ctx.fillStyle = theme.background;
|
|
29261
|
+
ctx.fillRect(x0, y0, w, h);
|
|
29262
|
+
ctx.restore();
|
|
29263
|
+
const from = Math.min(a.time, b.time);
|
|
29264
|
+
const to = Math.max(a.time, b.time);
|
|
29265
|
+
const chartBars = proj.barsBetween ? proj.barsBetween(from, to) : 0;
|
|
29266
|
+
const chartMs = chartBars > 0 ? (to - from) / chartBars : 0;
|
|
29267
|
+
const res = proj.seriesInRange && chartMs > 0 && w > 1 && h > 1 ? proj.seriesInRange(d.magnifier.timeframe, from, to + chartMs) : void 0;
|
|
29268
|
+
let seriesBars = res?.state === "ready" || res?.state === "loading" ? res.bars ?? [] : [];
|
|
29269
|
+
if (res && (res.state === "ready" || res.state === "loading") && seriesBars.length > 0) {
|
|
29270
|
+
const look = this.seriesLook;
|
|
29271
|
+
const transform = look.style !== "candles" ? barTransformFor(look.style) : null;
|
|
29272
|
+
if (transform) seriesBars = transform.full(seriesBars);
|
|
29273
|
+
const mode = look.style === "bars" ? "bars" : look.style === "line" || look.style === "baseline" ? "line" : look.style === "area" ? "area" : "candles";
|
|
29274
|
+
const halfPitch = (proj.xOf(from + chartMs) - proj.xOf(from)) / 2;
|
|
29275
|
+
ctx.save();
|
|
29276
|
+
ctx.beginPath();
|
|
29277
|
+
ctx.rect(x0, y0, w, h);
|
|
29278
|
+
ctx.clip();
|
|
29279
|
+
if (mode === "line" || mode === "area") {
|
|
29280
|
+
this.paintMagnifierLine(ctx, d, proj, seriesBars, res.barMs, halfPitch, y1, mode === "area", d.magnifier.upColor || look.lineColor);
|
|
29281
|
+
} else {
|
|
29282
|
+
this.paintMagnifierBars(ctx, d, proj, seriesBars, res.barMs, halfPitch, x0, x1, mode, d.magnifier.upColor || look.upColor, d.magnifier.downColor || look.downColor);
|
|
29283
|
+
}
|
|
29284
|
+
ctx.restore();
|
|
29285
|
+
} else if (res) {
|
|
29286
|
+
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";
|
|
29287
|
+
this.paintMagnifierNotice(ctx, notice, x0, y0, w, h, theme);
|
|
29288
|
+
}
|
|
29289
|
+
ctx.save();
|
|
29290
|
+
ctx.strokeStyle = d.style.lineColor || contrastColor(theme.background);
|
|
29291
|
+
ctx.lineWidth = d.style.lineWidth || 1;
|
|
29292
|
+
ctx.setLineDash(dashPattern(d.style.lineStyle, d.style.lineWidth || 1));
|
|
29293
|
+
ctx.strokeRect(x0, y0, w, h);
|
|
29294
|
+
ctx.restore();
|
|
29295
|
+
if (w > 44) {
|
|
29296
|
+
const label = magnifierTimeframeLabel(res?.state === "ready" || res?.state === "loading" ? res.timeframe : d.magnifier.timeframe);
|
|
29297
|
+
const chipH = 17;
|
|
29298
|
+
const gap = 4;
|
|
29299
|
+
const pane = proj.paneRect?.(d.paneId);
|
|
29300
|
+
const paneBottom = pane ? pane.top + pane.height : proj.height;
|
|
29301
|
+
const below = y1 + gap + chipH <= paneBottom;
|
|
29302
|
+
const chipY = below ? y1 + gap : y1 - gap - chipH;
|
|
29303
|
+
ctx.save();
|
|
29304
|
+
ctx.font = `10px ${theme.fontFamily}`;
|
|
29305
|
+
const tw = ctx.measureText(label).width;
|
|
29306
|
+
const caretW = 11;
|
|
29307
|
+
const chipW = tw + 12 + caretW;
|
|
29308
|
+
roundRect(ctx, x0, chipY, chipW, chipH, 3);
|
|
29309
|
+
ctx.fillStyle = theme.background;
|
|
29310
|
+
ctx.fill();
|
|
29311
|
+
ctx.strokeStyle = withAlpha(theme.textColor, 0.28);
|
|
29312
|
+
ctx.lineWidth = 1;
|
|
29313
|
+
ctx.setLineDash([]);
|
|
29314
|
+
ctx.stroke();
|
|
29315
|
+
ctx.fillStyle = theme.textColor;
|
|
29316
|
+
ctx.textAlign = "left";
|
|
29317
|
+
ctx.textBaseline = "middle";
|
|
29318
|
+
ctx.fillText(label, x0 + 6, chipY + chipH / 2 + 0.5);
|
|
29319
|
+
const cxr = x0 + 6 + tw + 5;
|
|
29320
|
+
const cyr = chipY + chipH / 2;
|
|
29321
|
+
ctx.strokeStyle = withAlpha(theme.textColor, 0.7);
|
|
29322
|
+
ctx.lineWidth = 1.2;
|
|
29323
|
+
ctx.beginPath();
|
|
29324
|
+
ctx.moveTo(cxr, cyr - 1.5);
|
|
29325
|
+
ctx.lineTo(cxr + 2.5, cyr + 1.5);
|
|
29326
|
+
ctx.lineTo(cxr + 5, cyr - 1.5);
|
|
29327
|
+
ctx.stroke();
|
|
29328
|
+
ctx.restore();
|
|
29329
|
+
d.chipRect = { x: x0, y: chipY, w: chipW, h: chipH };
|
|
29330
|
+
} else {
|
|
29331
|
+
d.chipRect = null;
|
|
29332
|
+
}
|
|
29333
|
+
}
|
|
29334
|
+
/** The magnifier's candle/bar loop: each bar's cell spans its open→close time (shifted left
|
|
29335
|
+
* by half a chart bar). Candles: wick always, body once the cell is wide enough to carry
|
|
29336
|
+
* one. OHLC bars: the high–low spine with open/close ticks once the cell has the room. */
|
|
29337
|
+
paintMagnifierBars(ctx, d, proj, bars, barMs, halfPitch, x0, x1, mode, upColor, downColor) {
|
|
29338
|
+
ctx.setLineDash([]);
|
|
29339
|
+
ctx.lineWidth = 1;
|
|
29340
|
+
for (const bar of bars) {
|
|
29341
|
+
const cx0 = proj.xOf(bar.time) - halfPitch;
|
|
29342
|
+
const cx1 = proj.xOf(bar.time + barMs) - halfPitch;
|
|
29343
|
+
if (cx1 < x0 || cx0 > x1) continue;
|
|
29344
|
+
const yHigh = proj.yOf(bar.high, d.paneId);
|
|
29345
|
+
const yLow = proj.yOf(bar.low, d.paneId);
|
|
29346
|
+
const yOpen = proj.yOf(bar.open, d.paneId);
|
|
29347
|
+
const yClose = proj.yOf(bar.close, d.paneId);
|
|
29348
|
+
if (yHigh == null || yLow == null || yOpen == null || yClose == null) continue;
|
|
29349
|
+
const color = bar.close >= bar.open ? upColor : downColor;
|
|
29350
|
+
const cellW = cx1 - cx0;
|
|
29351
|
+
const cx = (cx0 + cx1) / 2;
|
|
29352
|
+
ctx.strokeStyle = color;
|
|
29353
|
+
ctx.beginPath();
|
|
29354
|
+
ctx.moveTo(cx, yHigh);
|
|
29355
|
+
ctx.lineTo(cx, yLow);
|
|
29356
|
+
ctx.stroke();
|
|
29357
|
+
if (cellW < 3) continue;
|
|
29358
|
+
if (mode === "bars") {
|
|
29359
|
+
const tick = Math.max(1, cellW * 0.35);
|
|
29360
|
+
ctx.beginPath();
|
|
29361
|
+
ctx.moveTo(cx - tick, yOpen);
|
|
29362
|
+
ctx.lineTo(cx, yOpen);
|
|
29363
|
+
ctx.moveTo(cx, yClose);
|
|
29364
|
+
ctx.lineTo(cx + tick, yClose);
|
|
29365
|
+
ctx.stroke();
|
|
29366
|
+
} else {
|
|
29367
|
+
const bw = Math.max(1, cellW * 0.7);
|
|
29368
|
+
ctx.fillStyle = color;
|
|
29369
|
+
ctx.fillRect(cx - bw / 2, Math.min(yOpen, yClose), bw, Math.max(1, Math.abs(yClose - yOpen)));
|
|
29370
|
+
}
|
|
29371
|
+
}
|
|
29372
|
+
}
|
|
29373
|
+
/** The magnifier's line/area rendering: a close polyline through each cell's center (same
|
|
29374
|
+
* half-chart-bar shift as the candles), with an optional translucent fill down to the
|
|
29375
|
+
* rectangle's bottom edge for the area style. Colored like the chart's own line series. */
|
|
29376
|
+
paintMagnifierLine(ctx, d, proj, bars, barMs, halfPitch, yBottom, area, color) {
|
|
29377
|
+
const pts = [];
|
|
29378
|
+
for (const bar of bars) {
|
|
29379
|
+
const y = proj.yOf(bar.close, d.paneId);
|
|
29380
|
+
if (y == null) continue;
|
|
29381
|
+
pts.push([proj.xOf(bar.time + barMs / 2) - halfPitch, y]);
|
|
29382
|
+
}
|
|
29383
|
+
if (pts.length < 2) return;
|
|
29384
|
+
if (area) {
|
|
29385
|
+
ctx.beginPath();
|
|
29386
|
+
ctx.moveTo(pts[0][0], yBottom);
|
|
29387
|
+
for (const [px, py] of pts) ctx.lineTo(px, py);
|
|
29388
|
+
ctx.lineTo(pts[pts.length - 1][0], yBottom);
|
|
29389
|
+
ctx.closePath();
|
|
29390
|
+
ctx.fillStyle = withAlpha(color, 0.15);
|
|
29391
|
+
ctx.fill();
|
|
29392
|
+
}
|
|
29393
|
+
ctx.setLineDash([]);
|
|
29394
|
+
ctx.lineWidth = 1.5;
|
|
29395
|
+
ctx.strokeStyle = color;
|
|
29396
|
+
ctx.beginPath();
|
|
29397
|
+
ctx.moveTo(pts[0][0], pts[0][1]);
|
|
29398
|
+
for (let i = 1; i < pts.length; i += 1) ctx.lineTo(pts[i][0], pts[i][1]);
|
|
29399
|
+
ctx.stroke();
|
|
29400
|
+
}
|
|
29401
|
+
/** Centered muted notice inside the magnifier rect (loading / unavailable states). */
|
|
29402
|
+
paintMagnifierNotice(ctx, text, x0, y0, w, h, theme) {
|
|
29403
|
+
if (w < 60 || h < 20) return;
|
|
29404
|
+
ctx.save();
|
|
29405
|
+
ctx.beginPath();
|
|
29406
|
+
ctx.rect(x0, y0, w, h);
|
|
29407
|
+
ctx.clip();
|
|
29408
|
+
ctx.font = `11px ${theme.fontFamily}`;
|
|
29409
|
+
ctx.fillStyle = withAlpha(theme.textColor, 0.55);
|
|
29410
|
+
ctx.textAlign = "center";
|
|
29411
|
+
ctx.textBaseline = "middle";
|
|
29412
|
+
ctx.fillText(text, x0 + w / 2, y0 + h / 2);
|
|
29413
|
+
ctx.restore();
|
|
29414
|
+
}
|
|
29415
|
+
/** A bottom-center pill prompting the armed tool's placement gesture (e.g. the magnifier's
|
|
29416
|
+
* "drag an area"). Painted by the drawings layer while the tool is armed and no placement
|
|
29417
|
+
* is in progress; chart-background fill so it reads as chrome over any content. */
|
|
29418
|
+
paintPlacementHint(ctx, text, theme, width, height) {
|
|
29419
|
+
ctx.save();
|
|
29420
|
+
ctx.font = `11px ${theme.fontFamily}`;
|
|
29421
|
+
const tw = ctx.measureText(text).width;
|
|
29422
|
+
const pillW = tw + 24;
|
|
29423
|
+
const pillH = 24;
|
|
29424
|
+
const x = (width - pillW) / 2;
|
|
29425
|
+
const y = height - pillH - 14;
|
|
29426
|
+
roundRect(ctx, x, y, pillW, pillH, pillH / 2);
|
|
29427
|
+
ctx.fillStyle = theme.background;
|
|
29428
|
+
ctx.fill();
|
|
29429
|
+
ctx.strokeStyle = withAlpha(theme.textColor, 0.28);
|
|
29430
|
+
ctx.lineWidth = 1;
|
|
29431
|
+
ctx.setLineDash([]);
|
|
29432
|
+
ctx.stroke();
|
|
29433
|
+
ctx.fillStyle = theme.textColor;
|
|
29434
|
+
ctx.textAlign = "center";
|
|
29435
|
+
ctx.textBaseline = "middle";
|
|
29436
|
+
ctx.fillText(text, width / 2, y + pillH / 2 + 0.5);
|
|
29437
|
+
ctx.restore();
|
|
29438
|
+
}
|
|
28885
29439
|
/** Paint a fixed-range volume profile: horizontal histogram rows (up/down split) anchored to
|
|
28886
29440
|
* the left or right of the time span, optional VAH / VAL / POC levels across the range, and
|
|
28887
29441
|
* optional developing POC / VA polylines. Recomputes from the two anchors on every paint. */
|
|
@@ -30297,8 +30851,9 @@ function ensureStyles5() {
|
|
|
30297
30851
|
if (!existing) document.head.appendChild(s);
|
|
30298
30852
|
}
|
|
30299
30853
|
var DrawingSettingsPopup = class {
|
|
30300
|
-
constructor(host, theme) {
|
|
30854
|
+
constructor(host, theme, chartBarMs = () => 0) {
|
|
30301
30855
|
this.host = host;
|
|
30856
|
+
this.chartBarMs = chartBarMs;
|
|
30302
30857
|
this.el = null;
|
|
30303
30858
|
this.tipEl = null;
|
|
30304
30859
|
// floating hover-label (above/below the toolbar)
|
|
@@ -30323,6 +30878,14 @@ var DrawingSettingsPopup = class {
|
|
|
30323
30878
|
this.theme = theme;
|
|
30324
30879
|
this.settingsDialog = new DrawingSettingsDialog(host, theme);
|
|
30325
30880
|
}
|
|
30881
|
+
/** The magnifier timeframe choices strictly below the chart's own bar duration
|
|
30882
|
+
* (`auto` rides along while at least one concrete lower step exists). */
|
|
30883
|
+
lowerTimeframeOptions() {
|
|
30884
|
+
const chartMs = this.chartBarMs();
|
|
30885
|
+
if (!(chartMs > 0)) return [...MAGNIFIER_TIMEFRAME_OPTIONS];
|
|
30886
|
+
const lower = MAGNIFIER_TIMEFRAME_OPTIONS.filter((o) => o.ms > 0 && o.ms < chartMs);
|
|
30887
|
+
return lower.length > 0 ? [MAGNIFIER_TIMEFRAME_OPTIONS[0], ...lower] : [];
|
|
30888
|
+
}
|
|
30326
30889
|
setTheme(theme) {
|
|
30327
30890
|
this.theme = theme;
|
|
30328
30891
|
this.settingsDialog.setTheme(theme);
|
|
@@ -30365,7 +30928,20 @@ var DrawingSettingsPopup = class {
|
|
|
30365
30928
|
const sz = drawing.size ?? "normal";
|
|
30366
30929
|
bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label: sizeLabel }));
|
|
30367
30930
|
}
|
|
30368
|
-
if (paths.has("
|
|
30931
|
+
if (paths.has("magnifier.timeframe") && drawing instanceof Magnifier) {
|
|
30932
|
+
const options = this.lowerTimeframeOptions();
|
|
30933
|
+
if (options.length > 0) {
|
|
30934
|
+
bar.appendChild(
|
|
30935
|
+
this.dropdown("Lower timeframe", options.map((o) => o.value), drawing.magnifier.timeframe, () => "", (v) => actions.patch({ "magnifier.timeframe": v }), {
|
|
30936
|
+
label: (v) => magnifierTimeframeLabel(String(v)),
|
|
30937
|
+
labelInTrigger: true
|
|
30938
|
+
})
|
|
30939
|
+
);
|
|
30940
|
+
}
|
|
30941
|
+
bar.appendChild(this.colorButton("Up candles", BUCKET_ICON, drawing.magnifier.upColor || t.upColor, (v) => actions.patch({ "magnifier.upColor": v })));
|
|
30942
|
+
bar.appendChild(this.colorButton("Down candles", BUCKET_ICON, drawing.magnifier.downColor || t.downColor, (v) => actions.patch({ "magnifier.downColor": v })));
|
|
30943
|
+
}
|
|
30944
|
+
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 })));
|
|
30369
30945
|
if (paths.has("style.lineWidth")) {
|
|
30370
30946
|
const wf = schema.fields.find((f) => f.path === "style.lineWidth");
|
|
30371
30947
|
if (wf?.kind === "number" && (wf.min ?? 1) > 1) {
|
|
@@ -30687,6 +31263,7 @@ var DrawingSettingsPopup = class {
|
|
|
30687
31263
|
this.colorPop = null;
|
|
30688
31264
|
this.colorOwner = null;
|
|
30689
31265
|
}
|
|
31266
|
+
opts.onClose?.();
|
|
30690
31267
|
}
|
|
30691
31268
|
});
|
|
30692
31269
|
const el = pop.el;
|
|
@@ -30705,6 +31282,49 @@ var DrawingSettingsPopup = class {
|
|
|
30705
31282
|
pop.show();
|
|
30706
31283
|
return pop;
|
|
30707
31284
|
}
|
|
31285
|
+
/**
|
|
31286
|
+
* A standalone timeframe menu for the magnifier's ON-CHART chip. The chip lives on
|
|
31287
|
+
* canvas, so a transient invisible anchor is dropped at its pixel rect for the popover
|
|
31288
|
+
* to position against, and removed again when the menu closes. Independent of the
|
|
31289
|
+
* quick toolbar — the chip works without selecting the drawing first.
|
|
31290
|
+
*/
|
|
31291
|
+
openMagnifierTimeframeMenu(rect, current, onPick) {
|
|
31292
|
+
ensureStyles5();
|
|
31293
|
+
closeOpenPopovers();
|
|
31294
|
+
const options = this.lowerTimeframeOptions();
|
|
31295
|
+
const anchor = document.createElement("div");
|
|
31296
|
+
anchor.style.cssText = `position:absolute;left:${rect.x}px;top:${rect.y}px;width:${rect.w}px;height:${rect.h}px;pointer-events:none;`;
|
|
31297
|
+
this.host.appendChild(anchor);
|
|
31298
|
+
this.menuPop = this.hostFloat(anchor, {
|
|
31299
|
+
zIndex: 26,
|
|
31300
|
+
padding: "4px",
|
|
31301
|
+
onClose: () => anchor.remove(),
|
|
31302
|
+
fill: (menu2, pop) => {
|
|
31303
|
+
if (options.length === 0) {
|
|
31304
|
+
const note = document.createElement("div");
|
|
31305
|
+
note.style.cssText = "padding:6px 10px;opacity:0.65;white-space:nowrap;";
|
|
31306
|
+
note.textContent = "No lower timeframe available";
|
|
31307
|
+
menu2.appendChild(note);
|
|
31308
|
+
return;
|
|
31309
|
+
}
|
|
31310
|
+
for (const o of options) {
|
|
31311
|
+
const item = document.createElement("button");
|
|
31312
|
+
item.type = "button";
|
|
31313
|
+
item.className = "vela-dpop-item";
|
|
31314
|
+
item.dataset.active = o.value === current ? "1" : "0";
|
|
31315
|
+
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;";
|
|
31316
|
+
item.textContent = o.label;
|
|
31317
|
+
item.addEventListener("click", (e) => {
|
|
31318
|
+
e.stopPropagation();
|
|
31319
|
+
pop.hide();
|
|
31320
|
+
onPick(o.value);
|
|
31321
|
+
});
|
|
31322
|
+
menu2.appendChild(item);
|
|
31323
|
+
}
|
|
31324
|
+
}
|
|
31325
|
+
});
|
|
31326
|
+
this.menuOwner = anchor;
|
|
31327
|
+
}
|
|
30708
31328
|
/** A floating list of one-shot actions (icon + label rows) opened by the kebab. */
|
|
30709
31329
|
openActionMenu(anchor, rows) {
|
|
30710
31330
|
this.menuPop = this.hostFloat(anchor, {
|
|
@@ -30815,10 +31435,13 @@ var DrawingSettingsPopup = class {
|
|
|
30815
31435
|
let cur = current;
|
|
30816
31436
|
const paint = (v) => {
|
|
30817
31437
|
b.replaceChildren();
|
|
30818
|
-
const
|
|
30819
|
-
|
|
30820
|
-
|
|
30821
|
-
|
|
31438
|
+
const glyph = render(v);
|
|
31439
|
+
if (glyph) {
|
|
31440
|
+
const ic = document.createElement("span");
|
|
31441
|
+
ic.style.cssText = "display:flex;";
|
|
31442
|
+
ic.innerHTML = sized(glyph);
|
|
31443
|
+
b.appendChild(ic);
|
|
31444
|
+
}
|
|
30822
31445
|
if (opts.label && opts.labelInTrigger) {
|
|
30823
31446
|
const tx = document.createElement("span");
|
|
30824
31447
|
tx.textContent = opts.label(v);
|
|
@@ -30860,10 +31483,13 @@ var DrawingSettingsPopup = class {
|
|
|
30860
31483
|
item.className = "vela-dpop-item";
|
|
30861
31484
|
item.dataset.active = active ? "1" : "0";
|
|
30862
31485
|
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;`;
|
|
30863
|
-
const
|
|
30864
|
-
|
|
30865
|
-
|
|
30866
|
-
|
|
31486
|
+
const glyph = render(v);
|
|
31487
|
+
if (glyph) {
|
|
31488
|
+
const ic = document.createElement("span");
|
|
31489
|
+
ic.style.cssText = "display:flex;flex:none;width:22px;justify-content:center;";
|
|
31490
|
+
ic.innerHTML = sized(glyph, 18);
|
|
31491
|
+
item.appendChild(ic);
|
|
31492
|
+
}
|
|
30867
31493
|
if (label) {
|
|
30868
31494
|
const tx = document.createElement("span");
|
|
30869
31495
|
tx.textContent = label(v);
|
|
@@ -31215,6 +31841,9 @@ var UserDrawingController = class {
|
|
|
31215
31841
|
this.intentCb = null;
|
|
31216
31842
|
/** Another chart's in-progress placement, mirrored here as a ghost (drawings sync). */
|
|
31217
31843
|
this.externalGhost = null;
|
|
31844
|
+
/** Core-pushed series gateway (finer-timeframe bars for data-driven drawings). */
|
|
31845
|
+
this.seriesGw = null;
|
|
31846
|
+
this.seriesGwUnsub = null;
|
|
31218
31847
|
/** Last draft fingerprint reported upstream — gates the per-render emission to actual changes. */
|
|
31219
31848
|
this.lastDraftKey = null;
|
|
31220
31849
|
this.measure = new MeasureOverlay();
|
|
@@ -31242,7 +31871,7 @@ var UserDrawingController = class {
|
|
|
31242
31871
|
this.textEditor = null;
|
|
31243
31872
|
this.painter = new DrawingPainter();
|
|
31244
31873
|
this.ctx = canvas.getContext("2d");
|
|
31245
|
-
this.popup = new DrawingSettingsPopup(overlayHost, deps.theme());
|
|
31874
|
+
this.popup = new DrawingSettingsPopup(overlayHost, deps.theme(), () => deps.chartBarMs());
|
|
31246
31875
|
this.toolbar = new DrawingToolbar(
|
|
31247
31876
|
toolbarHost,
|
|
31248
31877
|
deps.theme(),
|
|
@@ -31300,6 +31929,22 @@ var UserDrawingController = class {
|
|
|
31300
31929
|
const shown = this.toolbarVisible && !this.mobileLayout;
|
|
31301
31930
|
this.deps.setToolbarGutter(shown ? this.toolbarCollapsed ? TOOLBAR_COLLAPSED_WIDTH : TOOLBAR_WIDTH : 0);
|
|
31302
31931
|
}
|
|
31932
|
+
/** Core push: the series gateway data-driven drawings read finer-timeframe bars
|
|
31933
|
+
* through (surfaced to them as `Projector.seriesInRange`). A landed background
|
|
31934
|
+
* fetch repaints both this layer and the interleave slices under the series. */
|
|
31935
|
+
setSeriesGateway(gateway) {
|
|
31936
|
+
this.seriesGwUnsub?.();
|
|
31937
|
+
this.seriesGw = gateway;
|
|
31938
|
+
this.seriesGwUnsub = gateway.onUpdate(() => {
|
|
31939
|
+
this.invalidateSlices();
|
|
31940
|
+
this.render();
|
|
31941
|
+
this.deps.requestDataPaint();
|
|
31942
|
+
});
|
|
31943
|
+
}
|
|
31944
|
+
/** The pushed series gateway, or null before the core provides one. */
|
|
31945
|
+
get seriesGateway() {
|
|
31946
|
+
return this.seriesGw;
|
|
31947
|
+
}
|
|
31303
31948
|
/** Core push: mirror (or clear) another chart's in-progress placement as a ghost. */
|
|
31304
31949
|
setExternalGhost(doc) {
|
|
31305
31950
|
this.externalGhost = doc ? deserializeDrawing(doc) : null;
|
|
@@ -31417,8 +32062,34 @@ var UserDrawingController = class {
|
|
|
31417
32062
|
/** Should the drawing layer win this press (vs pan)? */
|
|
31418
32063
|
claim(x, y) {
|
|
31419
32064
|
if (this.measureMode || this.eraserMode) return true;
|
|
32065
|
+
if (this.magnifierChipAt(x, y)) return true;
|
|
31420
32066
|
return this.interaction.claim(x, y);
|
|
31421
32067
|
}
|
|
32068
|
+
/** The topmost visible (unlocked) magnifier whose timeframe chip contains (x, y) —
|
|
32069
|
+
* the chip's rect is what the painter measured last frame. */
|
|
32070
|
+
magnifierChipAt(x, y) {
|
|
32071
|
+
for (let i = this.drawings.length - 1; i >= 0; i -= 1) {
|
|
32072
|
+
const d = this.drawings[i];
|
|
32073
|
+
if (!(d instanceof Magnifier) || !d.visible || d.locked) continue;
|
|
32074
|
+
const r = d.chipRect;
|
|
32075
|
+
if (r && x >= r.x && x <= r.x + r.w && y >= r.y && y <= r.y + r.h) return d;
|
|
32076
|
+
}
|
|
32077
|
+
return null;
|
|
32078
|
+
}
|
|
32079
|
+
/** Open the on-chart chip's timeframe menu; the pick patches the drawing like a
|
|
32080
|
+
* settings-popup edit (same intent, same undo step). */
|
|
32081
|
+
openMagnifierChipMenu(drawing) {
|
|
32082
|
+
const rect = drawing.chipRect;
|
|
32083
|
+
if (!rect) return;
|
|
32084
|
+
const id = drawing.id;
|
|
32085
|
+
this.popup.openMagnifierTimeframeMenu(rect, drawing.magnifier.timeframe, (value) => {
|
|
32086
|
+
const d = this.drawings.find((x) => x.id === id);
|
|
32087
|
+
if (!(d instanceof Magnifier)) return;
|
|
32088
|
+
d.applySettings({ "magnifier.timeframe": value });
|
|
32089
|
+
this.render();
|
|
32090
|
+
this.emit({ kind: "edit", doc: d.serialize() });
|
|
32091
|
+
});
|
|
32092
|
+
}
|
|
31422
32093
|
/** Delete the (unlocked) drawing under the cursor. True when one was removed.
|
|
31423
32094
|
* Shared by the eraser (click + drag) and the middle-click shortcut. */
|
|
31424
32095
|
deleteAt(x, y) {
|
|
@@ -31464,6 +32135,13 @@ var UserDrawingController = class {
|
|
|
31464
32135
|
this.render();
|
|
31465
32136
|
return;
|
|
31466
32137
|
}
|
|
32138
|
+
if (this.activeTool == null) {
|
|
32139
|
+
const chipOwner = this.magnifierChipAt(x, y);
|
|
32140
|
+
if (chipOwner) {
|
|
32141
|
+
this.openMagnifierChipMenu(chipOwner);
|
|
32142
|
+
return;
|
|
32143
|
+
}
|
|
32144
|
+
}
|
|
31467
32145
|
this.interaction.down(x, y, snap, shift);
|
|
31468
32146
|
}
|
|
31469
32147
|
pointerMove(x, y, snap = "off", shift = false) {
|
|
@@ -31682,6 +32360,7 @@ var UserDrawingController = class {
|
|
|
31682
32360
|
/** Cursor hint while hovering — `'pointer'` over a drawing/handle, else null. */
|
|
31683
32361
|
cursorAt(x, y) {
|
|
31684
32362
|
if (this.eraserMode) return "pointer";
|
|
32363
|
+
if (this.activeTool == null && this.magnifierChipAt(x, y)) return "pointer";
|
|
31685
32364
|
return this.interaction.cursorAt(x, y);
|
|
31686
32365
|
}
|
|
31687
32366
|
/** Right-click: an explicit escape back to the pointer. Cancels an in-progress
|
|
@@ -31860,6 +32539,7 @@ var UserDrawingController = class {
|
|
|
31860
32539
|
if (!sctx) continue;
|
|
31861
32540
|
sctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
31862
32541
|
sctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
|
|
32542
|
+
this.painter.seriesLook = this.deps.seriesLook();
|
|
31863
32543
|
this.painter.paintAll(sctx, drawings, proj, theme, EMPTY_TARGETS);
|
|
31864
32544
|
const slices = out.get(paneId) ?? [];
|
|
31865
32545
|
slices.push({ beforeZ, canvas });
|
|
@@ -31887,6 +32567,7 @@ var UserDrawingController = class {
|
|
|
31887
32567
|
dragged: this.interaction.activeDragId(),
|
|
31888
32568
|
mutedLabel: edited instanceof TextLabel ? edited.id : null
|
|
31889
32569
|
};
|
|
32570
|
+
this.painter.seriesLook = this.deps.seriesLook();
|
|
31890
32571
|
this.painter.paintAll(ctx, this.drawings.filter((d) => !this.isInterleaved(d)), proj, this.deps.theme(), targets);
|
|
31891
32572
|
this.painter.paintHighlights(ctx, this.drawings.filter((d) => this.isInterleaved(d)), proj, handleIdsFor(targets));
|
|
31892
32573
|
this.layoutTextEditor();
|
|
@@ -31894,6 +32575,10 @@ var UserDrawingController = class {
|
|
|
31894
32575
|
if (ghost) this.painter.paintGhost(ctx, ghost, proj, this.deps.theme());
|
|
31895
32576
|
if (this.externalGhost) this.painter.paintGhost(ctx, this.externalGhost, proj, this.deps.theme());
|
|
31896
32577
|
this.emitDraft(ghost);
|
|
32578
|
+
if (this.activeTool && !ghost) {
|
|
32579
|
+
const hint = getDrawingType(this.activeTool)?.placementHint;
|
|
32580
|
+
if (hint) this.painter.paintPlacementHint(ctx, hint, this.deps.theme(), proj.width, proj.height);
|
|
32581
|
+
}
|
|
31897
32582
|
const markers = this.interaction.placingMarkers(proj);
|
|
31898
32583
|
if (markers) this.painter.paintHandles(ctx, markers);
|
|
31899
32584
|
const m = this.interaction.snapMarker();
|
|
@@ -31905,6 +32590,9 @@ var UserDrawingController = class {
|
|
|
31905
32590
|
this.closeTextEditor();
|
|
31906
32591
|
this.popup.destroy();
|
|
31907
32592
|
this.toolbar.destroy();
|
|
32593
|
+
this.seriesGwUnsub?.();
|
|
32594
|
+
this.seriesGwUnsub = null;
|
|
32595
|
+
this.seriesGw = null;
|
|
31908
32596
|
this.intentCb = null;
|
|
31909
32597
|
this.drawings = [];
|
|
31910
32598
|
}
|
|
@@ -32302,7 +32990,7 @@ function mergeSlices(indicator, user) {
|
|
|
32302
32990
|
}
|
|
32303
32991
|
|
|
32304
32992
|
// src/renderers/native/drawings/Projector.ts
|
|
32305
|
-
function createProjector(coords, paneOf, paneIdAtY, barsInRange) {
|
|
32993
|
+
function createProjector(coords, paneOf, paneIdAtY, barsInRange, seriesInRange) {
|
|
32306
32994
|
return {
|
|
32307
32995
|
xOf: (time) => coords.timeToX(time),
|
|
32308
32996
|
yOf: (price, paneId) => {
|
|
@@ -32323,6 +33011,7 @@ function createProjector(coords, paneOf, paneIdAtY, barsInRange) {
|
|
|
32323
33011
|
},
|
|
32324
33012
|
barsBetween: (t1, t2) => Math.abs(coords.timeToLogical(t2) - coords.timeToLogical(t1)),
|
|
32325
33013
|
barsInRange: barsInRange ? (from, to) => barsInRange(from, to) : void 0,
|
|
33014
|
+
seriesInRange,
|
|
32326
33015
|
width: coords.width,
|
|
32327
33016
|
height: coords.height
|
|
32328
33017
|
};
|
|
@@ -34192,6 +34881,23 @@ var NativeRenderer = class {
|
|
|
34192
34881
|
seriesBoundaries: (paneId) => this.scene.seriesBoundaries(paneId),
|
|
34193
34882
|
priceZ: (paneId) => paneId === PRICE_PANE_ID2 ? this.scene.candleZ : null,
|
|
34194
34883
|
requestDataPaint: () => this.scheduler.invalidate(3 /* Light */),
|
|
34884
|
+
// The look the price series ACTUALLY paints with: candle colors resolved through
|
|
34885
|
+
// the per-style override, line/area colors through their configured styles — so
|
|
34886
|
+
// series-mirroring content (the magnifier inset) matches the chart exactly.
|
|
34887
|
+
seriesLook: () => {
|
|
34888
|
+
const st = this.scene.style;
|
|
34889
|
+
const paint = effectiveCandlePaint(st.candle, this.scene.candleOverride, this.theme.upColor, this.theme.downColor);
|
|
34890
|
+
const barsUp = st.bars.upColor ?? this.theme.upColor;
|
|
34891
|
+
const barsDown = st.bars.downColor ?? this.theme.downColor;
|
|
34892
|
+
const style = this.scene.priceStyle;
|
|
34893
|
+
return {
|
|
34894
|
+
style,
|
|
34895
|
+
upColor: style === "bars" ? barsUp : paint.up,
|
|
34896
|
+
downColor: style === "bars" ? barsDown : paint.down,
|
|
34897
|
+
lineColor: style === "area" ? st.area.lineColor ?? this.theme.upColor : st.line.color ?? this.theme.upColor
|
|
34898
|
+
};
|
|
34899
|
+
},
|
|
34900
|
+
chartBarMs: () => this.coords.barInterval,
|
|
34195
34901
|
snap: (pt, paneId, mode, cursorPx) => this.snapToCandle(pt, paneId, mode, cursorPx),
|
|
34196
34902
|
setSnapMode: (mode) => this.setSnapMode(mode),
|
|
34197
34903
|
setToolbarGutter: (px) => this.setToolbarGutter(px)
|
|
@@ -35585,7 +36291,8 @@ var NativeRenderer = class {
|
|
|
35585
36291
|
return p ? { scale: p.scale, bounds: p.bounds, collapsed: p.collapsed } : null;
|
|
35586
36292
|
},
|
|
35587
36293
|
(y) => this.paneNodeAtY(y)?.id ?? null,
|
|
35588
|
-
(from, to) => this.barsInTimeRange(from, to)
|
|
36294
|
+
(from, to) => this.barsInTimeRange(from, to),
|
|
36295
|
+
this.userDrawings?.seriesGateway ? (tf, from, to) => this.userDrawings.seriesGateway.seriesInRange(tf, from, to) : void 0
|
|
35589
36296
|
);
|
|
35590
36297
|
}
|
|
35591
36298
|
/** OHLC bars whose open-time falls within `[from, to]` (inclusive) — the data a regression
|
|
@@ -36792,6 +37499,21 @@ function resolveElement(container) {
|
|
|
36792
37499
|
// src/core/options.ts
|
|
36793
37500
|
var normalizeSession = (v) => v === "regular" || v === "extended" ? v : void 0;
|
|
36794
37501
|
|
|
37502
|
+
// src/widget/statusline-model.ts
|
|
37503
|
+
function segmentVisibility(parts, chartHidden = false) {
|
|
37504
|
+
return { avatar: parts.logo, symbol: parts.name, meta: parts.name, market: parts.market, ohlc: parts.ohlc && !chartHidden, change: parts.change && !chartHidden, eye: chartHidden };
|
|
37505
|
+
}
|
|
37506
|
+
function statuslineMenuItems(parts, chartVisible) {
|
|
37507
|
+
return [
|
|
37508
|
+
{ id: "part:logo", label: "Symbol logo", checked: parts.logo },
|
|
37509
|
+
{ id: "part:name", label: "Symbol name", checked: parts.name },
|
|
37510
|
+
{ id: "part:market", label: "Market status", checked: parts.market },
|
|
37511
|
+
{ id: "part:ohlc", label: "OHLC values", checked: parts.ohlc },
|
|
37512
|
+
{ id: "part:change", label: "Bar change values", checked: parts.change },
|
|
37513
|
+
{ id: "chart", label: chartVisible ? "Hide chart" : "Show chart", separatorBefore: true }
|
|
37514
|
+
];
|
|
37515
|
+
}
|
|
37516
|
+
|
|
36795
37517
|
// src/widget/format.ts
|
|
36796
37518
|
function decimalsFor(ref) {
|
|
36797
37519
|
const a = Math.abs(ref);
|
|
@@ -36839,7 +37561,13 @@ var CSS21 = `
|
|
|
36839
37561
|
padding: 2px 7px;
|
|
36840
37562
|
margin-left: -7px;
|
|
36841
37563
|
}
|
|
36842
|
-
|
|
37564
|
+
/* Hovering opens the chip the same way a legend row opens: solid chart background
|
|
37565
|
+
* plus the same inset neutral outline the indicator rows wear (InputsUI's
|
|
37566
|
+
* setRowHighlighted) \u2014 the two columns read as one family. */
|
|
37567
|
+
.vela-statusline:hover { background: var(--vela-bg); box-shadow: inset 0 0 0 1px var(--vela-border); }
|
|
37568
|
+
/* Chart hidden (the price series' eye \u2014 renderer 'candleVisible'): the line dims to
|
|
37569
|
+
* the same 0.5 wash a hidden indicator's legend row wears. */
|
|
37570
|
+
.vela-statusline.vela-sl-chart-hidden { opacity: 0.5; }
|
|
36843
37571
|
.vela-statusline .vela-sl-avatar {
|
|
36844
37572
|
width: 18px;
|
|
36845
37573
|
height: 18px;
|
|
@@ -36864,6 +37592,24 @@ var CSS21 = `
|
|
|
36864
37592
|
* these are the pre-ink fallbacks only. */
|
|
36865
37593
|
.vela-statusline .vela-sl-change[data-dir='up'] { color: var(--vela-up); }
|
|
36866
37594
|
.vela-statusline .vela-sl-change[data-dir='down'] { color: var(--vela-down); }
|
|
37595
|
+
/* The show-chart eye \u2014 out only while the chart is hidden (syncParts drives display),
|
|
37596
|
+
* replacing the value readout it took away. Same footprint as a legend action button. */
|
|
37597
|
+
.vela-statusline .vela-sl-eye {
|
|
37598
|
+
align-self: center;
|
|
37599
|
+
align-items: center;
|
|
37600
|
+
justify-content: center;
|
|
37601
|
+
width: 18px;
|
|
37602
|
+
height: 18px;
|
|
37603
|
+
padding: 0;
|
|
37604
|
+
border: none;
|
|
37605
|
+
border-radius: 3px;
|
|
37606
|
+
background: none;
|
|
37607
|
+
color: var(--vela-fg-muted);
|
|
37608
|
+
cursor: pointer;
|
|
37609
|
+
line-height: 0;
|
|
37610
|
+
flex: none;
|
|
37611
|
+
}
|
|
37612
|
+
.vela-statusline .vela-sl-eye:hover { color: var(--vela-fg); background: color-mix(in srgb, var(--vela-fg) 12%, transparent); }
|
|
36867
37613
|
/* Stack the TOP pane's legend below the status line (lower study panes stay put).
|
|
36868
37614
|
* The renderer marks whichever legend sits at the plot's top edge \u2014 the price pane
|
|
36869
37615
|
* normally, or a maximized study pane filling the plot \u2014 so the legend never merges
|
|
@@ -36973,7 +37719,12 @@ var Statusline = class {
|
|
|
36973
37719
|
constructor(host, symbol, iconFor) {
|
|
36974
37720
|
this.host = host;
|
|
36975
37721
|
this.iconFor = iconFor;
|
|
36976
|
-
this.parts = { name: true, market: true, ohlc: true, change: true };
|
|
37722
|
+
this.parts = { logo: true, name: true, market: true, ohlc: true, change: true };
|
|
37723
|
+
/** The right-click action menu — present once a host wires it via {@link attachMenu}. */
|
|
37724
|
+
this.menu = null;
|
|
37725
|
+
this.menuHooks = null;
|
|
37726
|
+
/** Mirror of the renderer's `candleVisible` — see {@link setChartHidden}. */
|
|
37727
|
+
this.chartHidden = false;
|
|
36977
37728
|
this.lastBar = null;
|
|
36978
37729
|
this.hoverBar = null;
|
|
36979
37730
|
this.unsubs = [];
|
|
@@ -36989,6 +37740,15 @@ var Statusline = class {
|
|
|
36989
37740
|
/** Fit mode (multi-chart cells): one row, overflowing segments hidden — see {@link setFitMode}. */
|
|
36990
37741
|
this.fitMode = false;
|
|
36991
37742
|
this.fitRO = null;
|
|
37743
|
+
this.onContextMenu = (e) => {
|
|
37744
|
+
if (!this.menu || !this.menuHooks) return;
|
|
37745
|
+
e.preventDefault();
|
|
37746
|
+
e.stopPropagation();
|
|
37747
|
+
const chartVisible = this.menuHooks.chartVisible();
|
|
37748
|
+
this.setChartHidden(!chartVisible);
|
|
37749
|
+
this.menu.setItems(statuslineMenuItems(this.parts, chartVisible));
|
|
37750
|
+
this.menu.openAt(e.clientX, e.clientY);
|
|
37751
|
+
};
|
|
36992
37752
|
const doc = host.ownerDocument;
|
|
36993
37753
|
injectStyles(STYLE_ID24, CSS21, doc);
|
|
36994
37754
|
host.classList.add("vela-has-statusline");
|
|
@@ -37015,9 +37775,21 @@ var Statusline = class {
|
|
|
37015
37775
|
this.ohlcEl.className = "vela-sl-ohlc";
|
|
37016
37776
|
this.changeEl = doc.createElement("span");
|
|
37017
37777
|
this.changeEl.className = "vela-sl-change";
|
|
37018
|
-
this.
|
|
37778
|
+
this.eyeEl = doc.createElement("button");
|
|
37779
|
+
this.eyeEl.type = "button";
|
|
37780
|
+
this.eyeEl.className = "vela-sl-eye";
|
|
37781
|
+
this.eyeEl.innerHTML = iconAt("eye-off", 14);
|
|
37782
|
+
this.eyeEl.setAttribute("aria-label", "Show chart");
|
|
37783
|
+
this.eyeEl.style.display = "none";
|
|
37784
|
+
this.eyeEl.addEventListener("click", (e) => {
|
|
37785
|
+
e.stopPropagation();
|
|
37786
|
+
this.menuHooks?.setChartVisible(true);
|
|
37787
|
+
this.setChartHidden(false);
|
|
37788
|
+
});
|
|
37789
|
+
this.el.append(this.avatarEl, this.symbolEl, this.metaEl, this.marketEl, this.ohlcEl, this.changeEl, this.eyeEl);
|
|
37019
37790
|
host.appendChild(this.el);
|
|
37020
37791
|
this.marketTip = new Tooltip(this.marketEl, { content: MARKET_LABELS.open, placement: "bottom" });
|
|
37792
|
+
this.eyeTip = new Tooltip(this.eyeEl, { content: "Show chart", placement: "bottom" });
|
|
37021
37793
|
this.setMarketStatus("open");
|
|
37022
37794
|
this.render();
|
|
37023
37795
|
}
|
|
@@ -37027,6 +37799,7 @@ var Statusline = class {
|
|
|
37027
37799
|
const fresh = tickerIconEl(this.el.ownerDocument, baseOfTicker(ticker), ticker, "vela-sl-avatar", this.iconFor?.(symbol));
|
|
37028
37800
|
this.avatarEl.replaceWith(fresh);
|
|
37029
37801
|
this.avatarEl = fresh;
|
|
37802
|
+
this.syncParts();
|
|
37030
37803
|
this.fit();
|
|
37031
37804
|
}
|
|
37032
37805
|
/**
|
|
@@ -37054,20 +37827,25 @@ var Statusline = class {
|
|
|
37054
37827
|
}
|
|
37055
37828
|
/** Project the parts config onto the segments (the baseline fit() prunes from). */
|
|
37056
37829
|
syncParts() {
|
|
37057
|
-
|
|
37058
|
-
this.
|
|
37059
|
-
this.
|
|
37060
|
-
this.
|
|
37830
|
+
const seg = segmentVisibility(this.parts, this.chartHidden);
|
|
37831
|
+
this.avatarEl.style.display = seg.avatar ? "" : "none";
|
|
37832
|
+
this.symbolEl.style.display = seg.symbol ? "" : "none";
|
|
37833
|
+
this.metaEl.style.display = seg.meta ? "" : "none";
|
|
37834
|
+
this.marketEl.style.display = seg.market ? "" : "none";
|
|
37835
|
+
this.ohlcEl.style.display = seg.ohlc ? "" : "none";
|
|
37836
|
+
this.changeEl.style.display = seg.change ? "" : "none";
|
|
37837
|
+
this.eyeEl.style.display = seg.eye ? "inline-flex" : "none";
|
|
37061
37838
|
}
|
|
37062
37839
|
/** Hide overflowing segments until the row fits its max-width (fit mode only). */
|
|
37063
37840
|
fit() {
|
|
37064
37841
|
if (!this.fitMode) return;
|
|
37065
37842
|
this.syncParts();
|
|
37843
|
+
const seg = segmentVisibility(this.parts, this.chartHidden);
|
|
37066
37844
|
const order = [
|
|
37067
|
-
[this.ohlcEl,
|
|
37068
|
-
[this.changeEl,
|
|
37069
|
-
[this.metaEl,
|
|
37070
|
-
[this.marketEl,
|
|
37845
|
+
[this.ohlcEl, seg.ohlc],
|
|
37846
|
+
[this.changeEl, seg.change],
|
|
37847
|
+
[this.metaEl, seg.meta],
|
|
37848
|
+
[this.marketEl, seg.market]
|
|
37071
37849
|
];
|
|
37072
37850
|
for (const [el, shown] of order) {
|
|
37073
37851
|
if (this.el.scrollWidth <= this.el.clientWidth) break;
|
|
@@ -37088,7 +37866,6 @@ var Statusline = class {
|
|
|
37088
37866
|
this.readout = readout;
|
|
37089
37867
|
this.render();
|
|
37090
37868
|
}
|
|
37091
|
-
/** Show/hide one part (the settings dialog's Status line tab drives these). */
|
|
37092
37869
|
/** The "· BINANCE · 1h" segment after the symbol — venue first, then resolution. */
|
|
37093
37870
|
setMeta(timeframe, provider) {
|
|
37094
37871
|
this.metaEl.textContent = `${provider ? `\xB7 ${provider.toUpperCase()} ` : ""}\xB7 ${timeframeLabel(timeframe)}`;
|
|
@@ -37107,6 +37884,9 @@ var Statusline = class {
|
|
|
37107
37884
|
});
|
|
37108
37885
|
this.marketTip.setContent(MARKET_LABELS[status]);
|
|
37109
37886
|
}
|
|
37887
|
+
/** Show/hide one part — the settings dialog's Status line tab and the right-click
|
|
37888
|
+
* menu both drive these. 'name' owns the venue/timeframe meta too (see
|
|
37889
|
+
* {@link segmentVisibility}). */
|
|
37110
37890
|
setPartVisible(part, visible) {
|
|
37111
37891
|
this.parts[part] = visible;
|
|
37112
37892
|
this.syncParts();
|
|
@@ -37115,6 +37895,47 @@ var Statusline = class {
|
|
|
37115
37895
|
partVisible(part) {
|
|
37116
37896
|
return this.parts[part];
|
|
37117
37897
|
}
|
|
37898
|
+
/** Mirror the chart's (price series') visibility: dim the whole line like a hidden
|
|
37899
|
+
* indicator's legend row, drop the value readout (OHLC + bar change — values of a
|
|
37900
|
+
* series that isn't painted), and put the show-chart eye out in its place. The
|
|
37901
|
+
* parts config is untouched, so showing the chart restores the readout exactly as
|
|
37902
|
+
* configured. Idempotent; {@link render} re-syncs it from the live renderer, so
|
|
37903
|
+
* toggles made elsewhere (the object tree's eye) converge too. */
|
|
37904
|
+
setChartHidden(hidden) {
|
|
37905
|
+
if (hidden === this.chartHidden) return;
|
|
37906
|
+
this.chartHidden = hidden;
|
|
37907
|
+
this.el.classList.toggle("vela-sl-chart-hidden", hidden);
|
|
37908
|
+
this.syncParts();
|
|
37909
|
+
this.fit();
|
|
37910
|
+
}
|
|
37911
|
+
/** Wire the right-click action menu: one checkable toggle per part plus hide/show
|
|
37912
|
+
* for the chart itself. The menu is built once; later calls just swap the hooks. */
|
|
37913
|
+
attachMenu(hooks) {
|
|
37914
|
+
this.menuHooks = hooks;
|
|
37915
|
+
if (this.menu) return;
|
|
37916
|
+
this.menu = new Menu({
|
|
37917
|
+
host: this.host,
|
|
37918
|
+
items: [],
|
|
37919
|
+
placement: "bottom-start",
|
|
37920
|
+
// Pointer-anchored action menu — checked state reads as a leading ✓ (the
|
|
37921
|
+
// same shape as the chart's own context menu).
|
|
37922
|
+
checkmarks: true,
|
|
37923
|
+
onSelect: (id) => this.runMenuItem(id)
|
|
37924
|
+
});
|
|
37925
|
+
this.el.addEventListener("contextmenu", this.onContextMenu);
|
|
37926
|
+
}
|
|
37927
|
+
runMenuItem(id) {
|
|
37928
|
+
const hooks = this.menuHooks;
|
|
37929
|
+
if (!hooks) return;
|
|
37930
|
+
if (id.startsWith("part:")) {
|
|
37931
|
+
const part = id.slice("part:".length);
|
|
37932
|
+
hooks.setPart(part, !this.parts[part]);
|
|
37933
|
+
} else if (id === "chart") {
|
|
37934
|
+
const next = !hooks.chartVisible();
|
|
37935
|
+
hooks.setChartVisible(next);
|
|
37936
|
+
this.setChartHidden(!next);
|
|
37937
|
+
}
|
|
37938
|
+
}
|
|
37118
37939
|
/** (Re)bind to a chart instance — called after every widget rebuild. */
|
|
37119
37940
|
onChart(chart) {
|
|
37120
37941
|
this.detach();
|
|
@@ -37136,7 +37957,11 @@ var Statusline = class {
|
|
|
37136
37957
|
this.detach();
|
|
37137
37958
|
this.fitRO?.disconnect();
|
|
37138
37959
|
this.fitRO = null;
|
|
37960
|
+
this.el.removeEventListener("contextmenu", this.onContextMenu);
|
|
37961
|
+
this.menu?.destroy();
|
|
37962
|
+
this.menu = null;
|
|
37139
37963
|
this.marketTip.destroy();
|
|
37964
|
+
this.eyeTip.destroy();
|
|
37140
37965
|
this.marketBubble.destroy();
|
|
37141
37966
|
this.host.classList.remove("vela-has-statusline", "vela-sl-fit-host");
|
|
37142
37967
|
this.el.remove();
|
|
@@ -37146,6 +37971,7 @@ var Statusline = class {
|
|
|
37146
37971
|
this.unsubs = [];
|
|
37147
37972
|
}
|
|
37148
37973
|
render() {
|
|
37974
|
+
if (this.menuHooks) this.setChartHidden(!this.menuHooks.chartVisible());
|
|
37149
37975
|
const bar = this.hoverBar ?? this.lastBar;
|
|
37150
37976
|
if (!bar) {
|
|
37151
37977
|
this.ohlcEl.replaceChildren();
|
|
@@ -37216,7 +38042,7 @@ var FETCH_BACK_MS = 4 * 864e5;
|
|
|
37216
38042
|
var FETCH_AHEAD_MS = 10 * 864e5;
|
|
37217
38043
|
var MIN_TIMER_MS = 15e3;
|
|
37218
38044
|
var MAX_TIMER_MS = 36e5;
|
|
37219
|
-
var
|
|
38045
|
+
var RETRY_MS2 = 6e4;
|
|
37220
38046
|
var MarketStatusTracker = class {
|
|
37221
38047
|
constructor(onStatus) {
|
|
37222
38048
|
this.onStatus = onStatus;
|
|
@@ -37253,7 +38079,7 @@ var MarketStatusTracker = class {
|
|
|
37253
38079
|
]);
|
|
37254
38080
|
if (my !== this.epoch) return;
|
|
37255
38081
|
if (!regular || !extended) {
|
|
37256
|
-
this.arm(my, data, symbol,
|
|
38082
|
+
this.arm(my, data, symbol, RETRY_MS2);
|
|
37257
38083
|
return;
|
|
37258
38084
|
}
|
|
37259
38085
|
const w = { regular, extended };
|
|
@@ -37964,7 +38790,14 @@ var ChartCell = class {
|
|
|
37964
38790
|
this.destroyed = false;
|
|
37965
38791
|
this.appTheme = deps.theme;
|
|
37966
38792
|
const symbol = prefixedSymbol(seed);
|
|
37967
|
-
this.state = {
|
|
38793
|
+
this.state = {
|
|
38794
|
+
symbol,
|
|
38795
|
+
provider: parseSymbol(symbol ?? "").provider ?? void 0,
|
|
38796
|
+
timeframe: seed.timeframe,
|
|
38797
|
+
priceStyle: seed.priceStyle,
|
|
38798
|
+
bars: seed.bars,
|
|
38799
|
+
session: normalizeSession(seed.session)
|
|
38800
|
+
};
|
|
37968
38801
|
const doc = gridHost.ownerDocument;
|
|
37969
38802
|
this.host = doc.createElement("div");
|
|
37970
38803
|
this.host.className = "vela-cell";
|
|
@@ -38052,6 +38885,11 @@ var ChartCell = class {
|
|
|
38052
38885
|
this.statusline = deps.statusline ? new Statusline(this.host, symbol ?? "", (sym) => this.inner?.data.symbolIcon(sym)) : null;
|
|
38053
38886
|
this.statusline?.setMeta(seed.timeframe ?? "60", this.state.provider ?? "");
|
|
38054
38887
|
this.statusline?.onChart(this.inner);
|
|
38888
|
+
this.statusline?.attachMenu({
|
|
38889
|
+
setPart: (part, visible) => this.setStatuslinePart(part, visible),
|
|
38890
|
+
chartVisible: () => this.inner?.renderer.get("candleVisible") !== false,
|
|
38891
|
+
setChartVisible: (visible) => this.inner?.renderer.set("candleVisible", visible)
|
|
38892
|
+
});
|
|
38055
38893
|
this.marketStatus = this.statusline ? new MarketStatusTracker((s) => this.statusline?.setMarketStatus(s)) : null;
|
|
38056
38894
|
void this.inner.data.ready().then(() => {
|
|
38057
38895
|
if (this.inner && this.state.symbol) {
|
|
@@ -38259,7 +39097,7 @@ var ChartCell = class {
|
|
|
38259
39097
|
kind: "select",
|
|
38260
39098
|
label: "Bars to fetch",
|
|
38261
39099
|
id: "bars",
|
|
38262
|
-
options: ["500", "1000", "2000", "5000", "10000", "20000"],
|
|
39100
|
+
options: ["500", "1000", "2000", "5000", "10000", "20000", "50000", "60000", "80000", "100000"],
|
|
38263
39101
|
get: () => String(this.state.bars ?? 1e3),
|
|
38264
39102
|
set: (v) => {
|
|
38265
39103
|
this.state.bars = Number(v);
|
|
@@ -38291,10 +39129,34 @@ var ChartCell = class {
|
|
|
38291
39129
|
id: "status-line",
|
|
38292
39130
|
rows: [
|
|
38293
39131
|
{ kind: "heading", label: "Status line", id: "parts" },
|
|
38294
|
-
{
|
|
38295
|
-
|
|
38296
|
-
|
|
38297
|
-
|
|
39132
|
+
{
|
|
39133
|
+
kind: "toggle",
|
|
39134
|
+
label: "Symbol name",
|
|
39135
|
+
id: "name",
|
|
39136
|
+
get: () => sl.partVisible("name"),
|
|
39137
|
+
set: (v) => this.setStatuslinePart("name", v)
|
|
39138
|
+
},
|
|
39139
|
+
{
|
|
39140
|
+
kind: "toggle",
|
|
39141
|
+
label: "Market status",
|
|
39142
|
+
id: "market",
|
|
39143
|
+
get: () => sl.partVisible("market"),
|
|
39144
|
+
set: (v) => this.setStatuslinePart("market", v)
|
|
39145
|
+
},
|
|
39146
|
+
{
|
|
39147
|
+
kind: "toggle",
|
|
39148
|
+
label: "OHLC values",
|
|
39149
|
+
id: "ohlc",
|
|
39150
|
+
get: () => sl.partVisible("ohlc"),
|
|
39151
|
+
set: (v) => this.setStatuslinePart("ohlc", v)
|
|
39152
|
+
},
|
|
39153
|
+
{
|
|
39154
|
+
kind: "toggle",
|
|
39155
|
+
label: "Bar change values",
|
|
39156
|
+
id: "change",
|
|
39157
|
+
get: () => sl.partVisible("change"),
|
|
39158
|
+
set: (v) => this.setStatuslinePart("change", v)
|
|
39159
|
+
},
|
|
38298
39160
|
{ kind: "heading", label: "Indicators", id: "indicators" },
|
|
38299
39161
|
{
|
|
38300
39162
|
kind: "toggle",
|
|
@@ -38347,7 +39209,7 @@ var ChartCell = class {
|
|
|
38347
39209
|
statusPrefs() {
|
|
38348
39210
|
const sl = this.statusline;
|
|
38349
39211
|
return {
|
|
38350
|
-
parts: sl ? { name: sl.partVisible("name"), market: sl.partVisible("market"), ohlc: sl.partVisible("ohlc"), change: sl.partVisible("change") } : null,
|
|
39212
|
+
parts: sl ? { logo: sl.partVisible("logo"), name: sl.partVisible("name"), market: sl.partVisible("market"), ohlc: sl.partVisible("ohlc"), change: sl.partVisible("change") } : null,
|
|
38351
39213
|
indicatorTitles: this.indicatorTitlesOn,
|
|
38352
39214
|
indicatorValues: this.indicatorValuesOn
|
|
38353
39215
|
};
|
|
@@ -38521,7 +39383,8 @@ var ChartCell = class {
|
|
|
38521
39383
|
if (this.manifest.length > 0) {
|
|
38522
39384
|
for (const item of led.manifest) {
|
|
38523
39385
|
const entry = this.manifest.find((e) => e.name === ledgerEntryName(item));
|
|
38524
|
-
if (entry)
|
|
39386
|
+
if (entry)
|
|
39387
|
+
this.addManifestInstance(entry, { record: false, ...typeof item === "object" ? { inputs: item.inputs, props: item.props } : {} });
|
|
38525
39388
|
}
|
|
38526
39389
|
this.pendingManifestNames = null;
|
|
38527
39390
|
} else if (!this.deps.manifestSettled()) {
|
|
@@ -38570,7 +39433,10 @@ var ChartCell = class {
|
|
|
38570
39433
|
* a persistence handler's `restore` runs silently, a user-driven call records.
|
|
38571
39434
|
*/
|
|
38572
39435
|
addExternalIndicator(entry) {
|
|
38573
|
-
this.addManifestInstance(
|
|
39436
|
+
this.addManifestInstance(
|
|
39437
|
+
{ ...entry, enabled: true },
|
|
39438
|
+
{ external: true, ...entry.inputs ? { inputs: entry.inputs } : {}, ...entry.props ? { props: entry.props } : {} }
|
|
39439
|
+
);
|
|
38574
39440
|
}
|
|
38575
39441
|
/** Add ONE instance of a manifest entry (repeatable — duplicates are legitimate). */
|
|
38576
39442
|
addManifestInstance(entry, opts = {}) {
|
|
@@ -39076,7 +39942,7 @@ var SplitterLayer = class {
|
|
|
39076
39942
|
};
|
|
39077
39943
|
|
|
39078
39944
|
// src/workspace/VelaWorkspace.ts
|
|
39079
|
-
var DEFAULT_TIMEFRAMES = ["1", "5", "15", "60", "240", "D", "W"];
|
|
39945
|
+
var DEFAULT_TIMEFRAMES = ["1", "5", "15", "30", "60", "240", "D", "W", "M"];
|
|
39080
39946
|
var GAP_PX = 2;
|
|
39081
39947
|
var POOL_CAP = 16;
|
|
39082
39948
|
var TIME_AXIS_H4 = 22;
|