@luxalgo/vela 0.8.4 → 0.8.5
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/dist/{DataProvider-Byo8pEv0.d.ts → DataProvider-BUdlkx9I.d.ts} +1 -1
- package/dist/{DataProvider-Bw58CjYy.d.cts → DataProvider-DdqMxKdl.d.cts} +1 -1
- package/dist/{chunk-MOJ4T26N.js → chunk-BN4DNS3O.js} +27 -7
- package/dist/{chunk-UNUVPQFW.js → chunk-QYIO37K7.js} +482 -94
- package/dist/{contributions-DOm40g5-.d.cts → contributions-D8xuYc9U.d.cts} +37 -5
- package/dist/{contributions-BY3zSYiZ.d.ts → contributions-P9fBBuY1.d.ts} +37 -5
- package/dist/index.cjs +482 -94
- package/dist/index.d.cts +23 -7
- package/dist/index.d.ts +23 -7
- package/dist/index.js +1 -1
- package/dist/{options-CSXbjMM5.d.cts → options-BI5Tp32b.d.cts} +36 -1
- package/dist/{options-CSXbjMM5.d.ts → options-BI5Tp32b.d.ts} +36 -1
- package/dist/{plugin-CiQVyETg.d.cts → plugin-B8F1nor8.d.cts} +14 -8
- package/dist/{plugin-BDxUhGO3.d.ts → plugin-D5Z-Q6TX.d.ts} +14 -8
- package/dist/plugin.d.cts +4 -4
- package/dist/plugin.d.ts +4 -4
- 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-model-DUBX6Fib.d.cts → statusline-model-CTwtDIca.d.cts} +3 -3
- package/dist/{statusline-model-C9tYXuux.d.ts → statusline-model-D9kWC9X5.d.ts} +3 -3
- package/dist/ui.d.cts +1 -1
- package/dist/ui.d.ts +1 -1
- package/dist/vela.global.js +482 -94
- package/dist/vela.global.min.js +53 -53
- package/dist/widget.cjs +508 -100
- package/dist/widget.d.cts +6 -6
- package/dist/widget.d.ts +6 -6
- package/dist/widget.js +3 -3
- package/dist/workspace.cjs +508 -100
- package/dist/workspace.d.cts +5 -5
- package/dist/workspace.d.ts +5 -5
- package/dist/workspace.js +2 -2
- package/package.json +1 -1
|
@@ -4168,6 +4168,8 @@ var LEGEND_TITLE_STATUS_GAP_PX = 8;
|
|
|
4168
4168
|
var LEGEND_ACTION_GAP_PX = 6;
|
|
4169
4169
|
var LEGEND_CTL_CSS = `cursor:pointer;display:inline-flex;align-items:center;justify-content:center;border:none;line-height:0;padding:0;width:${LEGEND_CTL_PX}px;height:${LEGEND_CTL_PX}px;border-radius:3px;box-sizing:border-box;flex:none;`;
|
|
4170
4170
|
var LEGEND_ROW_MIN_H = LEGEND_CTL_PX + LEGEND_ROW_PAD_Y * 2;
|
|
4171
|
+
var STACK_RAISE = "stack:raise";
|
|
4172
|
+
var STACK_LOWER = "stack:lower";
|
|
4171
4173
|
var EYE_SVG = iconAt("eye", LEGEND_ICON_PX2);
|
|
4172
4174
|
var EYE_OFF_SVG = iconAt("eye-off", LEGEND_ICON_PX2);
|
|
4173
4175
|
var GEAR_SVG = iconAt("gear", LEGEND_ICON_PX2);
|
|
@@ -4197,6 +4199,10 @@ var InputsUI = class {
|
|
|
4197
4199
|
this.mobileLayout = false;
|
|
4198
4200
|
/** Pane move/merge hook — when set, rows get a "Move to" menu + become drag-to-pane sources. */
|
|
4199
4201
|
this.moveApi = null;
|
|
4202
|
+
/** Overlay restacking hook — when set, a row's menus offer to step it forward or back. */
|
|
4203
|
+
this.stackApi = null;
|
|
4204
|
+
/** What {@link orderRows} last ordered the rows by (skips unchanged frames). */
|
|
4205
|
+
this.rowOrderKey = "";
|
|
4200
4206
|
/** Host-contributed legend actions, resolved PER ROW at render time (see setLegendActions). */
|
|
4201
4207
|
this.legendActions = null;
|
|
4202
4208
|
/** Host-contributed callout bubbles, resolved PER ROW at render time (see setLegendCallouts). */
|
|
@@ -4335,6 +4341,10 @@ var InputsUI = class {
|
|
|
4335
4341
|
setMoveApi(api) {
|
|
4336
4342
|
this.moveApi = api;
|
|
4337
4343
|
}
|
|
4344
|
+
/** Enable the context menu's "Bring in front of …" entry for overlay indicators. Null disables it. */
|
|
4345
|
+
setStackApi(api) {
|
|
4346
|
+
this.stackApi = api;
|
|
4347
|
+
}
|
|
4338
4348
|
/**
|
|
4339
4349
|
* Wire the host-contributed row actions. Re-calling replaces the provider and
|
|
4340
4350
|
* re-projects the rows already on screen (a late `registerLegendAction` appears after
|
|
@@ -4480,16 +4490,20 @@ var InputsUI = class {
|
|
|
4480
4490
|
}
|
|
4481
4491
|
this.moveTargets = items.map((it) => it.target);
|
|
4482
4492
|
this.moveMenuId = id;
|
|
4483
|
-
const
|
|
4493
|
+
const stack = this.stackItems(id);
|
|
4494
|
+
const moves = items.map((it, i) => ({ id: String(i), label: it.label }));
|
|
4495
|
+
if (moves[0] && stack.length > 0) moves[0].separatorBefore = true;
|
|
4496
|
+
const descriptors = [...stack, ...moves];
|
|
4484
4497
|
if (!this.moveMenu) {
|
|
4485
4498
|
this.moveMenu = new Menu({
|
|
4486
4499
|
host: this.container,
|
|
4487
4500
|
items: descriptors,
|
|
4488
4501
|
placement: "bottom-start",
|
|
4489
4502
|
onSelect: (itemId) => {
|
|
4490
|
-
const target = this.moveTargets[Number(itemId)];
|
|
4491
4503
|
const rowId = this.moveMenuId;
|
|
4492
|
-
if (
|
|
4504
|
+
if (!rowId || this.runStackItem(itemId, rowId)) return;
|
|
4505
|
+
const target = this.moveTargets[Number(itemId)];
|
|
4506
|
+
if (target) api.move(rowId, target);
|
|
4493
4507
|
}
|
|
4494
4508
|
});
|
|
4495
4509
|
} else {
|
|
@@ -4502,18 +4516,24 @@ var InputsUI = class {
|
|
|
4502
4516
|
this.moveMenu?.close();
|
|
4503
4517
|
}
|
|
4504
4518
|
// ── legend row context menu (right-click) ───────────────────────────────
|
|
4505
|
-
/** Open the row's action menu at the pointer
|
|
4519
|
+
/** Open the row's action menu at the pointer: an overlay indicator's restacking steps
|
|
4520
|
+
* first (where a layer is there to pass), then the values toggle. */
|
|
4506
4521
|
openRowMenu(id, x, y) {
|
|
4507
4522
|
this.closeRowMenu();
|
|
4508
4523
|
this.closeMoveMenu();
|
|
4509
4524
|
const row2 = this.rows.get(id);
|
|
4510
4525
|
if (!row2) return;
|
|
4511
4526
|
this.rowMenuId = id;
|
|
4512
|
-
const
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4527
|
+
const stack = this.stackItems(id);
|
|
4528
|
+
const items = [
|
|
4529
|
+
...stack,
|
|
4530
|
+
{
|
|
4531
|
+
id: "values",
|
|
4532
|
+
label: "Indicator values",
|
|
4533
|
+
checked: this.rowValuesShown(row2),
|
|
4534
|
+
separatorBefore: stack.length > 0
|
|
4535
|
+
}
|
|
4536
|
+
];
|
|
4517
4537
|
if (!this.rowMenu) {
|
|
4518
4538
|
this.rowMenu = new Menu({
|
|
4519
4539
|
host: this.container,
|
|
@@ -4522,8 +4542,10 @@ var InputsUI = class {
|
|
|
4522
4542
|
// Right-click action menu — checked state reads as a leading ✓ (see the kit Menu).
|
|
4523
4543
|
checkmarks: true,
|
|
4524
4544
|
onSelect: (itemId) => {
|
|
4525
|
-
|
|
4526
|
-
|
|
4545
|
+
const rowId = this.rowMenuId;
|
|
4546
|
+
if (!rowId || this.runStackItem(itemId, rowId)) return;
|
|
4547
|
+
if (itemId !== "values") return;
|
|
4548
|
+
const target = this.rows.get(rowId);
|
|
4527
4549
|
if (!target) return;
|
|
4528
4550
|
target.showValues = !this.rowValuesShown(target);
|
|
4529
4551
|
this.renderRowValues(target);
|
|
@@ -4537,6 +4559,55 @@ var InputsUI = class {
|
|
|
4537
4559
|
closeRowMenu() {
|
|
4538
4560
|
this.rowMenu?.close();
|
|
4539
4561
|
}
|
|
4562
|
+
/** A row's one-step restacking entries (forward, then back), for whichever neighbor exists. */
|
|
4563
|
+
stackItems(id) {
|
|
4564
|
+
const api = this.stackApi;
|
|
4565
|
+
if (!api) return [];
|
|
4566
|
+
const items = [];
|
|
4567
|
+
const forward = this.stackLabel("Bring in front of", api.above(id), "the indicator in front");
|
|
4568
|
+
if (forward) items.push({ id: STACK_RAISE, label: forward });
|
|
4569
|
+
const back = this.stackLabel("Send behind", api.below(id), "the indicator behind");
|
|
4570
|
+
if (back) items.push({ id: STACK_LOWER, label: back });
|
|
4571
|
+
return items;
|
|
4572
|
+
}
|
|
4573
|
+
stackLabel(verb, neighbor, fallback) {
|
|
4574
|
+
if (neighbor === null) return null;
|
|
4575
|
+
if (neighbor === "price") return `${verb} price`;
|
|
4576
|
+
return `${verb} ${this.rows.get(neighbor.indicator)?.title ?? fallback}`;
|
|
4577
|
+
}
|
|
4578
|
+
/** Run a restacking entry; false when `itemId` is not one. */
|
|
4579
|
+
runStackItem(itemId, rowId) {
|
|
4580
|
+
if (itemId === STACK_RAISE) this.stackApi?.raise(rowId);
|
|
4581
|
+
else if (itemId === STACK_LOWER) this.stackApi?.lower(rowId);
|
|
4582
|
+
else return false;
|
|
4583
|
+
return true;
|
|
4584
|
+
}
|
|
4585
|
+
/**
|
|
4586
|
+
* List each pane's rows front-most first, the way the indicators stack (the object
|
|
4587
|
+
* tree's order). `backToFront` is the paint order; rows it doesn't name keep their
|
|
4588
|
+
* place after the others. Cheap to call every frame: no-op while nothing changed.
|
|
4589
|
+
*/
|
|
4590
|
+
orderRows(backToFront) {
|
|
4591
|
+
const key = `${backToFront.join(",")}|${[...this.rows.values()].map((r) => `${r.id}@${r.paneId}`).join(",")}`;
|
|
4592
|
+
if (key === this.rowOrderKey) return;
|
|
4593
|
+
this.rowOrderKey = key;
|
|
4594
|
+
const rank = new Map(backToFront.map((id, i) => [id, i]));
|
|
4595
|
+
for (const lg of this.legends.values()) {
|
|
4596
|
+
const els = [...this.rows.values()].filter((r) => r.el.parentElement === lg).map((r) => r.el);
|
|
4597
|
+
const dom = [...lg.children];
|
|
4598
|
+
const ofEl = new Map([...this.rows.values()].map((r) => [r.el, r.id]));
|
|
4599
|
+
const at = (el) => rank.get(ofEl.get(el)) ?? -1;
|
|
4600
|
+
els.sort((a, b) => {
|
|
4601
|
+
const ra = at(a);
|
|
4602
|
+
const rb = at(b);
|
|
4603
|
+
if (ra >= 0 && rb >= 0) return rb - ra;
|
|
4604
|
+
if (ra >= 0 || rb >= 0) return ra >= 0 ? -1 : 1;
|
|
4605
|
+
return dom.indexOf(a) - dom.indexOf(b);
|
|
4606
|
+
});
|
|
4607
|
+
const anchor = this.foldToggle?.parentElement === lg ? this.foldToggle : null;
|
|
4608
|
+
for (const el of els) lg.insertBefore(el, anchor);
|
|
4609
|
+
}
|
|
4610
|
+
}
|
|
4540
4611
|
/** Move a legend row to another pane (indicator merged/moved), tidying an emptied container. */
|
|
4541
4612
|
setPane(id, paneId) {
|
|
4542
4613
|
const row2 = this.rows.get(id);
|
|
@@ -6227,6 +6298,7 @@ var WebGL2Backend = class {
|
|
|
6227
6298
|
const i0 = Math.max(0, Math.floor(vr.from));
|
|
6228
6299
|
const i1 = Math.min(n - 1, Math.ceil(vr.to));
|
|
6229
6300
|
if (i1 < i0) return;
|
|
6301
|
+
const s1 = Math.max(i1, Math.ceil(vr.to));
|
|
6230
6302
|
gl.enable(gl.BLEND);
|
|
6231
6303
|
gl.enable(gl.SCISSOR_TEST);
|
|
6232
6304
|
const barColorMap = mergeBarColors(scene.indicators);
|
|
@@ -6285,8 +6357,8 @@ var WebGL2Backend = class {
|
|
|
6285
6357
|
b.alpha = this.modelAlpha;
|
|
6286
6358
|
const off = scene.offsetOf(m.id);
|
|
6287
6359
|
const mp = effPane(m);
|
|
6288
|
-
for (const f of m.fills) if (f.overlay !== true) this.emitFill(b, m, f, mp, coords, i0,
|
|
6289
|
-
for (const s of m.series) if (s.overlay !== true) this.emitSeries(b, s, mp, coords, i0,
|
|
6360
|
+
for (const f of m.fills) if (f.overlay !== true) this.emitFill(b, m, f, mp, coords, i0, s1, off);
|
|
6361
|
+
for (const s of m.series) if (s.overlay !== true) this.emitSeries(b, s, mp, coords, i0, s1, theme, off);
|
|
6290
6362
|
}
|
|
6291
6363
|
if (drawCandles && !candleDrawn) {
|
|
6292
6364
|
drawSlicesUpTo(scene.candleZ);
|
|
@@ -6297,11 +6369,11 @@ var WebGL2Backend = class {
|
|
|
6297
6369
|
if (isPrice) {
|
|
6298
6370
|
for (const m of scene.indicators.values()) {
|
|
6299
6371
|
const off = scene.offsetOf(m.id);
|
|
6300
|
-
for (const f of m.fills) if (f.overlay === true) this.emitFill(b, m, f, pane, coords, i0,
|
|
6372
|
+
for (const f of m.fills) if (f.overlay === true) this.emitFill(b, m, f, pane, coords, i0, s1, off);
|
|
6301
6373
|
}
|
|
6302
6374
|
for (const m of scene.indicators.values()) {
|
|
6303
6375
|
const off = scene.offsetOf(m.id);
|
|
6304
|
-
for (const s of m.series) if (s.overlay === true) this.emitSeries(b, s, pane, coords, i0,
|
|
6376
|
+
for (const s of m.series) if (s.overlay === true) this.emitSeries(b, s, pane, coords, i0, s1, theme, off);
|
|
6305
6377
|
}
|
|
6306
6378
|
}
|
|
6307
6379
|
drawSlicesUpTo(Infinity);
|
|
@@ -6323,7 +6395,7 @@ var WebGL2Backend = class {
|
|
|
6323
6395
|
if (this.glow > 0 && !this.screenProgram && !this.ensureScreenPipeline()) this.glow = 0;
|
|
6324
6396
|
if (this.glow > 0 && this.screenProgram) {
|
|
6325
6397
|
this.glowBatch.alpha = this.modelAlpha;
|
|
6326
|
-
this.renderGlow(gl, scene, coords, i0,
|
|
6398
|
+
this.renderGlow(gl, scene, coords, i0, s1);
|
|
6327
6399
|
}
|
|
6328
6400
|
}
|
|
6329
6401
|
/**
|
|
@@ -6880,7 +6952,7 @@ var WebGL2Backend = class {
|
|
|
6880
6952
|
const tickW = Math.max(1, Math.round(spacing * 0.35));
|
|
6881
6953
|
for (let i = i0; i <= i1; i += 1) {
|
|
6882
6954
|
const bar = s.bars[i - off];
|
|
6883
|
-
if (!bar) continue;
|
|
6955
|
+
if (!bar || !Number.isFinite(bar.open + bar.high + bar.low + bar.close)) continue;
|
|
6884
6956
|
const bc = s.barColors?.[i - off];
|
|
6885
6957
|
const body = parseColor(bc?.color ?? (bar.close >= bar.open ? up : down));
|
|
6886
6958
|
const x = Math.round(coords.logicalToX(i));
|
|
@@ -7533,6 +7605,8 @@ var InputController = class {
|
|
|
7533
7605
|
const tapRelease = this.dragging && !this.moved && (!wasTouch || Math.hypot(x - this.startX, y - this.startY) <= TOUCH_TAP_SLOP);
|
|
7534
7606
|
if (this.dragging && this.region === "drawing") {
|
|
7535
7607
|
this.deps.drawingsPointerUp?.(x, y, this.snapMode(e));
|
|
7608
|
+
} else if (this.dragging && this.region === "separator") {
|
|
7609
|
+
this.deps.endPaneResize?.();
|
|
7536
7610
|
} else if (tapRelease && this.region === "data") {
|
|
7537
7611
|
this.deps.onClick(x, y);
|
|
7538
7612
|
} else if (this.dragging && this.region === "data" && wasTouch) {
|
|
@@ -7554,6 +7628,7 @@ var InputController = class {
|
|
|
7554
7628
|
this.cancelLongPress();
|
|
7555
7629
|
if (!this.dragging) return;
|
|
7556
7630
|
if (this.region === "drawing" && !Number.isNaN(this.cursorX)) this.deps.drawingsPointerUp?.(this.cursorX, this.cursorY, this.snapMode(e));
|
|
7631
|
+
if (this.region === "separator") this.deps.endPaneResize?.();
|
|
7557
7632
|
if (this.region === "crosshair" || e.pointerType === "touch") this.deps.onPointerMove(null, null);
|
|
7558
7633
|
this.endGesture(e);
|
|
7559
7634
|
};
|
|
@@ -8266,9 +8341,12 @@ var SceneGraph = class {
|
|
|
8266
8341
|
setIndicatorZ(id, z) {
|
|
8267
8342
|
this.seriesZ.set(id, z);
|
|
8268
8343
|
}
|
|
8269
|
-
/** Snapshot of the current ordering for a UI/read API: `{ id, z }`
|
|
8344
|
+
/** Snapshot of the current ordering for a UI/read API: `{ id, z }` back to front, in the
|
|
8345
|
+
* order the indicators paint — on a shared key, mount order (see `orderedIndicatorsForPane`). */
|
|
8270
8346
|
indicatorZOrder() {
|
|
8271
|
-
|
|
8347
|
+
const mounted = new Map([...this.indicators.keys()].map((id, i) => [id, i]));
|
|
8348
|
+
const at = (id) => mounted.get(id) ?? Number.MAX_SAFE_INTEGER;
|
|
8349
|
+
return [...this.seriesZ.entries()].map(([id, z]) => ({ id, z })).sort((a, b) => a.z - b.z || at(a.id) - at(b.id));
|
|
8272
8350
|
}
|
|
8273
8351
|
/** The pane's series z keys (each indicator, plus the candles on the price pane), sorted
|
|
8274
8352
|
* ascending and de-duplicated — the boundaries a user drawing's z is slotted against. */
|
|
@@ -8296,16 +8374,19 @@ var SceneGraph = class {
|
|
|
8296
8374
|
for (const z of this.seriesZ.values()) if (z < min) min = z;
|
|
8297
8375
|
return min;
|
|
8298
8376
|
}
|
|
8377
|
+
/** Create the pane, or update an existing one in place. Without `heightWeight` an existing
|
|
8378
|
+
* pane keeps its weight (a user resize survives other indicators mounting onto it) and a
|
|
8379
|
+
* new one gets the default: 3 for the price pane, 1 for a study pane. */
|
|
8299
8380
|
ensurePane(id, kind, order, heightWeight) {
|
|
8300
8381
|
this.orderedCache = null;
|
|
8301
8382
|
const existing = this.panes.get(id);
|
|
8302
8383
|
if (existing) {
|
|
8303
8384
|
existing.order = order;
|
|
8304
|
-
existing.heightWeight = heightWeight;
|
|
8385
|
+
if (heightWeight !== void 0) existing.heightWeight = heightWeight;
|
|
8305
8386
|
existing.kind = kind;
|
|
8306
8387
|
return existing;
|
|
8307
8388
|
}
|
|
8308
|
-
const pane = { id, kind, order, heightWeight, bounds: { top: 0, height: 0 }, scale: { min: 0, max: 1 }, scaleTarget: { min: 0, max: 1 }, initialized: false, manualScale: null, collapsed: false, percentBaseline: 0 };
|
|
8389
|
+
const pane = { id, kind, order, heightWeight: heightWeight ?? (kind === "price" ? 3 : 1), bounds: { top: 0, height: 0 }, scale: { min: 0, max: 1 }, scaleTarget: { min: 0, max: 1 }, initialized: false, manualScale: null, collapsed: false, percentBaseline: 0 };
|
|
8309
8390
|
this.panes.set(id, pane);
|
|
8310
8391
|
return pane;
|
|
8311
8392
|
}
|
|
@@ -8362,6 +8443,7 @@ var Canvas2dBackend = class {
|
|
|
8362
8443
|
const i0 = Math.max(0, Math.floor(vr.from));
|
|
8363
8444
|
const i1 = Math.min(n - 1, Math.ceil(vr.to));
|
|
8364
8445
|
if (i1 < i0) return;
|
|
8446
|
+
const s1 = Math.max(i1, Math.ceil(vr.to));
|
|
8365
8447
|
const barColorMap = mergeBarColors2(scene.indicators);
|
|
8366
8448
|
const panes = scene.orderedPanes();
|
|
8367
8449
|
for (const pane of panes) {
|
|
@@ -8404,8 +8486,8 @@ var Canvas2dBackend = class {
|
|
|
8404
8486
|
ctx.globalAlpha = this.modelAlpha;
|
|
8405
8487
|
const off = scene.offsetOf(m.id);
|
|
8406
8488
|
const mp = effPane(m);
|
|
8407
|
-
for (const f of m.fills) if (f.overlay !== true) this.drawFill(ctx, m, f, mp, coords, i0,
|
|
8408
|
-
for (const s of m.series) if (s.overlay !== true) this.drawSeries(ctx, s, mp, coords, i0,
|
|
8489
|
+
for (const f of m.fills) if (f.overlay !== true) this.drawFill(ctx, m, f, mp, coords, i0, s1, off);
|
|
8490
|
+
for (const s of m.series) if (s.overlay !== true) this.drawSeries(ctx, s, mp, coords, i0, s1, theme, off);
|
|
8409
8491
|
}
|
|
8410
8492
|
if (drawCandles && !candleDrawn) {
|
|
8411
8493
|
drawSlicesUpTo(scene.candleZ);
|
|
@@ -8416,11 +8498,11 @@ var Canvas2dBackend = class {
|
|
|
8416
8498
|
ctx.globalAlpha = this.modelAlpha;
|
|
8417
8499
|
for (const m of scene.indicators.values()) {
|
|
8418
8500
|
const off = scene.offsetOf(m.id);
|
|
8419
|
-
for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0,
|
|
8501
|
+
for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0, s1, off);
|
|
8420
8502
|
}
|
|
8421
8503
|
for (const m of scene.indicators.values()) {
|
|
8422
8504
|
const off = scene.offsetOf(m.id);
|
|
8423
|
-
for (const s of m.series) if (s.overlay === true) this.drawSeries(ctx, s, pane, coords, i0,
|
|
8505
|
+
for (const s of m.series) if (s.overlay === true) this.drawSeries(ctx, s, pane, coords, i0, s1, theme, off);
|
|
8424
8506
|
}
|
|
8425
8507
|
}
|
|
8426
8508
|
drawSlicesUpTo(Infinity);
|
|
@@ -8872,7 +8954,7 @@ var Canvas2dBackend = class {
|
|
|
8872
8954
|
const tickW = Math.max(1, Math.round(spacing * 0.35));
|
|
8873
8955
|
for (let i = i0; i <= i1; i += 1) {
|
|
8874
8956
|
const b = s.bars[i - off];
|
|
8875
|
-
if (!b) continue;
|
|
8957
|
+
if (!b || !Number.isFinite(b.open + b.high + b.low + b.close)) continue;
|
|
8876
8958
|
const bc = s.barColors?.[i - off];
|
|
8877
8959
|
const body = bc?.color ?? (b.close >= b.open ? up : down);
|
|
8878
8960
|
const x = Math.round(coords.logicalToX(i)) + 0.5;
|
|
@@ -18994,6 +19076,58 @@ function stackLayers(entries, candleZ) {
|
|
|
18994
19076
|
return { below, above };
|
|
18995
19077
|
}
|
|
18996
19078
|
|
|
19079
|
+
// src/renderers/native/core/overlayStack.ts
|
|
19080
|
+
function priceStack(indicators, candleZ) {
|
|
19081
|
+
const out = [...indicators];
|
|
19082
|
+
const at = out.findIndex((e) => e.z >= candleZ);
|
|
19083
|
+
out.splice(at < 0 ? out.length : at, 0, { id: null, z: candleZ });
|
|
19084
|
+
return out;
|
|
19085
|
+
}
|
|
19086
|
+
function untie(stack) {
|
|
19087
|
+
const out = [];
|
|
19088
|
+
for (let i = 0; i < stack.length; ) {
|
|
19089
|
+
let j = i + 1;
|
|
19090
|
+
while (j < stack.length && stack[j].z === stack[i].z) j += 1;
|
|
19091
|
+
const run = j - i;
|
|
19092
|
+
if (run > 1) {
|
|
19093
|
+
const base = stack[i].z;
|
|
19094
|
+
const step = ((stack[j]?.z ?? base + 1) - base) / run;
|
|
19095
|
+
for (let k = 1; k < run; k += 1) {
|
|
19096
|
+
const id = stack[i + k].id;
|
|
19097
|
+
if (id !== null) out.push({ id, z: base + k * step });
|
|
19098
|
+
}
|
|
19099
|
+
}
|
|
19100
|
+
i = j;
|
|
19101
|
+
}
|
|
19102
|
+
return out;
|
|
19103
|
+
}
|
|
19104
|
+
function layerAbove(stack, id) {
|
|
19105
|
+
const i = stack.findIndex((e) => e.id === id);
|
|
19106
|
+
if (i < 0 || i === stack.length - 1) return null;
|
|
19107
|
+
const above = stack[i + 1];
|
|
19108
|
+
return above.id === null ? "price" : { indicator: above.id };
|
|
19109
|
+
}
|
|
19110
|
+
function layerBelow(stack, id) {
|
|
19111
|
+
const i = stack.findIndex((e) => e.id === id);
|
|
19112
|
+
if (i <= 0) return null;
|
|
19113
|
+
const below = stack[i - 1];
|
|
19114
|
+
return below.id === null ? "price" : { indicator: below.id };
|
|
19115
|
+
}
|
|
19116
|
+
function raisedZ(stack, id) {
|
|
19117
|
+
const i = stack.findIndex((e) => e.id === id);
|
|
19118
|
+
if (i < 0 || i === stack.length - 1) return null;
|
|
19119
|
+
const above = stack[i + 1];
|
|
19120
|
+
const next = stack[i + 2];
|
|
19121
|
+
return next ? (above.z + next.z) / 2 : above.z + 1;
|
|
19122
|
+
}
|
|
19123
|
+
function loweredZ(stack, id) {
|
|
19124
|
+
const i = stack.findIndex((e) => e.id === id);
|
|
19125
|
+
if (i <= 0) return null;
|
|
19126
|
+
const below = stack[i - 1];
|
|
19127
|
+
const prev2 = stack[i - 2];
|
|
19128
|
+
return prev2 ? (prev2.z + below.z) / 2 : below.z - 1;
|
|
19129
|
+
}
|
|
19130
|
+
|
|
18997
19131
|
// src/renderers/native/chrome/vela-logos.ts
|
|
18998
19132
|
var VELA_MARK_SVG = '<svg viewBox="0 -142 161.66 148" aria-hidden="true"><g fill="currentColor"><path d="M0 -140L32.91 -140L77.52 -62.73L61.06 -34.24Z"/><path d="M161.66 -140L128.75 -140L84.14 -62.73L100.59 -34.24Z"/><path d="M80.83 -52.04L95.85 -26.02L80.83 0L65.81 -26.02Z"/></g></svg>';
|
|
18999
19133
|
var VELA_LETTERS_SVG = '<svg viewBox="161.66 -142 310.57 148" aria-hidden="true"><g fill="currentColor"><path d="M226.3 1.17Q200.81 1.17 186.12 -12.99Q171.42 -27.15 171.42 -51.17Q171.42 -75.2 185.87 -89.6Q200.33 -104 224.25 -104Q247.4 -104 261.7 -90.58Q276.01 -77.15 276.3 -54.59Q276.3 -49.9 275.42 -45.02H200.81V-43.55Q201.2 -32.62 207.75 -26.42Q214.29 -20.21 225.03 -20.21Q233.72 -20.21 239.34 -23.93Q244.95 -27.64 246.81 -34.77H274.64Q272.1 -19.14 259.16 -8.98Q246.22 1.17 226.3 1.17ZM201.5 -63.38H247.88Q246.42 -72.75 240.27 -77.88Q234.12 -83.01 224.45 -83.01Q215.17 -83.01 208.92 -77.73Q202.67 -72.46 201.5 -63.38ZM287.54 0V-140.04H315.76V0ZM429.34 -23.83H432.56V0H418.11Q407.76 0 403.36 -4.44Q398.97 -8.89 399.16 -16.8Q387.83 1.17 365.57 1.17Q348.48 1.17 337.88 -6.74Q327.29 -14.65 327.29 -29Q327.29 -45.41 339.45 -54.3Q351.6 -63.18 374.55 -63.18H394.96V-68.16Q394.96 -75.2 390.08 -79.3Q385.2 -83.4 376.51 -83.4Q368.3 -83.4 363.03 -79.98Q357.76 -76.56 356.88 -70.8H329.73Q331.19 -85.84 344.08 -94.92Q356.97 -104 377.29 -104Q399.06 -104 410.88 -94.43Q422.7 -84.86 422.7 -66.6V-30.18Q422.7 -26.37 424.26 -25.1Q425.82 -23.83 429.34 -23.83ZM394.96 -42.38V-44.43H374.36Q365.47 -44.43 360.59 -40.92Q355.7 -37.4 355.7 -30.96Q355.7 -25.59 359.81 -22.41Q363.91 -19.24 370.94 -19.24Q381.97 -19.24 388.37 -25.54Q394.77 -31.84 394.96 -42.38Z"/><path fill-opacity="0.55" d="M437.44 -124.28H441.96V-113.41H445.34V-124.28H449.87V-127.4H437.44ZM451.43 -113.41H454.7V-123.29L458.97 -113.41H461.62L465.99 -123.29V-113.41H469.21V-127.4H464.53L460.42 -117.68L456.16 -127.4H451.43Z"/></g></svg>';
|
|
@@ -19644,6 +19778,8 @@ var NativeRenderer = class {
|
|
|
19644
19778
|
// pane just below it
|
|
19645
19779
|
this.resizeSplitStart = null;
|
|
19646
19780
|
// the two panes' shared span when the drag began
|
|
19781
|
+
this.resizeStartAboveWeight = 0;
|
|
19782
|
+
// the upper pane's weight when the drag began
|
|
19647
19783
|
/** The user drawings take part in pointer input (the runtime `drawingsInteractive` feature). */
|
|
19648
19784
|
this.drawingsInteractive = true;
|
|
19649
19785
|
this.hoverSeparatorY = null;
|
|
@@ -20286,6 +20422,7 @@ var NativeRenderer = class {
|
|
|
20286
20422
|
};
|
|
20287
20423
|
this.scene.candleZ = next.stacking.candles;
|
|
20288
20424
|
for (const [id, z] of Object.entries(next.stacking.series)) this.scene.setIndicatorZ(id, z);
|
|
20425
|
+
this.syncLegendOrder();
|
|
20289
20426
|
s.bars = { upColor: next.bars.upColor, downColor: next.bars.downColor };
|
|
20290
20427
|
s.line = { color: next.line.color, width: next.line.width };
|
|
20291
20428
|
s.area = { lineColor: next.area.lineColor, width: next.area.width, topColor: next.area.topColor, bottomColor: next.area.bottomColor };
|
|
@@ -20331,6 +20468,7 @@ var NativeRenderer = class {
|
|
|
20331
20468
|
if (typeof v.z === "number") this.scene.setIndicatorZ(v.id, v.z);
|
|
20332
20469
|
else if (v.to === "front") this.scene.bringIndicatorToFront(v.id);
|
|
20333
20470
|
else if (v.to === "back") this.scene.sendIndicatorToBack(v.id);
|
|
20471
|
+
this.syncLegendOrder();
|
|
20334
20472
|
}
|
|
20335
20473
|
/** Toggle the in-chart settings gear + dialog (the `settings` feature). The gear
|
|
20336
20474
|
* sits above the time axis, clear of the right price-axis strip. */
|
|
@@ -20718,7 +20856,7 @@ var NativeRenderer = class {
|
|
|
20718
20856
|
this.chrome.mount(this.chromeCanvas);
|
|
20719
20857
|
this.crosshairLayer.mount(this.cursorCanvas);
|
|
20720
20858
|
this.coords.setViewport(defaultViewport());
|
|
20721
|
-
this.scene.ensurePane(PRICE_PANE_ID, "price", 0
|
|
20859
|
+
this.scene.ensurePane(PRICE_PANE_ID, "price", 0);
|
|
20722
20860
|
this.scheduler = new Scheduler((level) => this.renderFrame(level));
|
|
20723
20861
|
this.animator = new Animator((dt) => this.animTick(dt));
|
|
20724
20862
|
this.targetBarSpacing = this.coords.getViewport().barSpacing;
|
|
@@ -20731,7 +20869,7 @@ var NativeRenderer = class {
|
|
|
20731
20869
|
onClick: (x, y) => {
|
|
20732
20870
|
if (this.handleMarkClick(x, y)) return;
|
|
20733
20871
|
this.userDrawings?.deselect();
|
|
20734
|
-
this.handleClick(x);
|
|
20872
|
+
this.handleClick(x, y);
|
|
20735
20873
|
},
|
|
20736
20874
|
onAxisLongPress: (axis, x, y) => {
|
|
20737
20875
|
for (const cb of this.axisLongPressCbs) cb({ axis, x, y });
|
|
@@ -20745,6 +20883,7 @@ var NativeRenderer = class {
|
|
|
20745
20883
|
paneSeparatorAt: (y) => this.paneSeparatorAt(y),
|
|
20746
20884
|
beginPaneResize: (y) => this.beginPaneResize(y),
|
|
20747
20885
|
paneResizeBy: (dy) => this.paneResizeBy(dy),
|
|
20886
|
+
endPaneResize: () => this.endPaneResize(),
|
|
20748
20887
|
resetPaneSize: (y) => this.resetPaneSize(y),
|
|
20749
20888
|
resetView: () => this.resetView(),
|
|
20750
20889
|
// User drawings claim a gesture before pan when armed / over a drawing.
|
|
@@ -20870,6 +21009,12 @@ var NativeRenderer = class {
|
|
|
20870
21009
|
for (const cb of this.moveIndicatorCbs) cb(id, target);
|
|
20871
21010
|
}
|
|
20872
21011
|
});
|
|
21012
|
+
this.inputsUI.setStackApi({
|
|
21013
|
+
above: (id) => layerAbove(this.overlayStack(), id),
|
|
21014
|
+
below: (id) => layerBelow(this.overlayStack(), id),
|
|
21015
|
+
raise: (id) => this.restackOverlay(id, raisedZ),
|
|
21016
|
+
lower: (id) => this.restackOverlay(id, loweredZ)
|
|
21017
|
+
});
|
|
20873
21018
|
this.paneControls = new PaneControls(this.plot, theme, {
|
|
20874
21019
|
panes: () => this.paneControlViews(),
|
|
20875
21020
|
rightAxis: () => this.rightAxisW,
|
|
@@ -21187,7 +21332,7 @@ var NativeRenderer = class {
|
|
|
21187
21332
|
}
|
|
21188
21333
|
// ── panes ──
|
|
21189
21334
|
ensurePane(pane) {
|
|
21190
|
-
this.scene.ensurePane(pane.id, pane.kind, pane.order, pane.heightWeight
|
|
21335
|
+
this.scene.ensurePane(pane.id, pane.kind, pane.order, pane.heightWeight);
|
|
21191
21336
|
this.layoutPanes();
|
|
21192
21337
|
this.paneControls?.refresh();
|
|
21193
21338
|
this.scheduler.invalidate(4 /* Full */);
|
|
@@ -21209,6 +21354,7 @@ var NativeRenderer = class {
|
|
|
21209
21354
|
model.ownScale = opts?.ownScale === true;
|
|
21210
21355
|
if (!model.ownScale) this.scene.dropIndicatorScale(handle.id);
|
|
21211
21356
|
this.inputsUI.setPane(handle.id, paneId);
|
|
21357
|
+
this.syncLegendOrder();
|
|
21212
21358
|
this.refreshAnchorOffset(model);
|
|
21213
21359
|
this.refreshAxisWidth();
|
|
21214
21360
|
this.layoutPanes();
|
|
@@ -21236,6 +21382,15 @@ var NativeRenderer = class {
|
|
|
21236
21382
|
this.paneControls?.refresh();
|
|
21237
21383
|
this.scheduler.invalidate(4 /* Full */);
|
|
21238
21384
|
}
|
|
21385
|
+
setPaneHeightWeight(paneId, weight) {
|
|
21386
|
+
const pane = this.scene.panes.get(paneId);
|
|
21387
|
+
if (!pane || !(weight > 0) || pane.heightWeight === weight) return;
|
|
21388
|
+
pane.heightWeight = weight;
|
|
21389
|
+
this.afterPaneLayoutChange();
|
|
21390
|
+
}
|
|
21391
|
+
paneHeightWeight(paneId) {
|
|
21392
|
+
return this.scene.panes.get(paneId)?.heightWeight ?? null;
|
|
21393
|
+
}
|
|
21239
21394
|
onPaneAction(cb) {
|
|
21240
21395
|
this.paneActionCbs.add(cb);
|
|
21241
21396
|
return () => this.paneActionCbs.delete(cb);
|
|
@@ -21330,6 +21485,7 @@ var NativeRenderer = class {
|
|
|
21330
21485
|
this.vpvrHidden = false;
|
|
21331
21486
|
}
|
|
21332
21487
|
if (model.ownScale) this.refreshAxisWidth();
|
|
21488
|
+
this.syncLegendOrder();
|
|
21333
21489
|
this.paneControls?.refresh();
|
|
21334
21490
|
this.scheduler.invalidate(4 /* Full */);
|
|
21335
21491
|
return { id: model.id };
|
|
@@ -21737,7 +21893,7 @@ var NativeRenderer = class {
|
|
|
21737
21893
|
this.setMarkHover(null);
|
|
21738
21894
|
this.scheduler.invalidate(1 /* Cursor */);
|
|
21739
21895
|
this.hoverLogical = null;
|
|
21740
|
-
const empty = { time: null, price: null, paneKind: null, values: /* @__PURE__ */ new Map(), ohlc: null };
|
|
21896
|
+
const empty = { time: null, price: null, paneKind: null, paneId: null, values: /* @__PURE__ */ new Map(), ohlc: null };
|
|
21741
21897
|
for (const cb of this.crosshairCbs) cb(empty);
|
|
21742
21898
|
return;
|
|
21743
21899
|
}
|
|
@@ -21768,14 +21924,23 @@ var NativeRenderer = class {
|
|
|
21768
21924
|
const bar = onBar ? this.bars[logical] : void 0;
|
|
21769
21925
|
const ohlc = bar ? { time: bar.time, open: bar.open, high: bar.high, low: bar.low, close: bar.close, volume: bar.volume } : null;
|
|
21770
21926
|
this.hoverLogical = onBar ? logical : null;
|
|
21771
|
-
const
|
|
21927
|
+
const onPane = inData && pane ? pane : null;
|
|
21928
|
+
const event = { time, price, paneKind: onPane?.kind ?? null, paneId: onPane?.id ?? null, values, ohlc };
|
|
21772
21929
|
for (const cb of this.crosshairCbs) cb(event);
|
|
21773
21930
|
}
|
|
21774
|
-
handleClick(x) {
|
|
21931
|
+
handleClick(x, y) {
|
|
21775
21932
|
if (this.coords.barCount === 0) return;
|
|
21776
21933
|
const logical = Math.round(this.coords.xToLogical(x));
|
|
21777
21934
|
const onBar = logical >= 0 && logical < this.coords.barCount;
|
|
21778
|
-
|
|
21935
|
+
const inData = x >= 0 && y >= 0 && x <= this.coords.width && y <= this.coords.height;
|
|
21936
|
+
const pane = inData ? this.paneNodeAtY(y) : null;
|
|
21937
|
+
const event = {
|
|
21938
|
+
time: onBar ? this.coords.logicalToTime(logical) : null,
|
|
21939
|
+
price: pane ? this.coords.yToPrice(y, pane.scale, pane.bounds) : null,
|
|
21940
|
+
paneKind: pane?.kind ?? null,
|
|
21941
|
+
paneId: pane?.id ?? null
|
|
21942
|
+
};
|
|
21943
|
+
for (const cb of this.clickCbs) cb(event);
|
|
21779
21944
|
}
|
|
21780
21945
|
/**
|
|
21781
21946
|
* Fan out (or collapse, with null) a multi-group mark stack; repaints the chrome tier when
|
|
@@ -22020,6 +22185,7 @@ var NativeRenderer = class {
|
|
|
22020
22185
|
const below = panes[i];
|
|
22021
22186
|
this.resizeAbove = above;
|
|
22022
22187
|
this.resizeBelow = below;
|
|
22188
|
+
this.resizeStartAboveWeight = above.heightWeight;
|
|
22023
22189
|
this.resizeSplitStart = {
|
|
22024
22190
|
combinedTop: above.bounds.top,
|
|
22025
22191
|
combinedHeight: above.bounds.height + below.bounds.height,
|
|
@@ -22040,6 +22206,16 @@ var NativeRenderer = class {
|
|
|
22040
22206
|
this.layoutPanes();
|
|
22041
22207
|
this.scheduler.invalidate(4 /* Full */);
|
|
22042
22208
|
}
|
|
22209
|
+
/** Release the separator: report the resize once, at the end of the drag, if it changed anything. */
|
|
22210
|
+
endPaneResize() {
|
|
22211
|
+
const above = this.resizeAbove;
|
|
22212
|
+
const below = this.resizeBelow;
|
|
22213
|
+
const startAbove = this.resizeStartAboveWeight;
|
|
22214
|
+
this.resizeAbove = null;
|
|
22215
|
+
this.resizeBelow = null;
|
|
22216
|
+
this.resizeSplitStart = null;
|
|
22217
|
+
if (above && below && above.heightWeight !== startAbove) this.emitPaneAction({ type: "resize", paneIds: [above.id, below.id] });
|
|
22218
|
+
}
|
|
22043
22219
|
/** Double-click a separator → split the two adjacent panes evenly (each gets half of
|
|
22044
22220
|
* their combined weight). A simple, predictable reset that leaves siblings untouched. */
|
|
22045
22221
|
resetPaneSize(y) {
|
|
@@ -22053,6 +22229,7 @@ var NativeRenderer = class {
|
|
|
22053
22229
|
below.heightWeight = half;
|
|
22054
22230
|
this.layoutPanes();
|
|
22055
22231
|
this.scheduler.invalidate(4 /* Full */);
|
|
22232
|
+
this.emitPaneAction({ type: "resize", paneIds: [above.id, below.id] });
|
|
22056
22233
|
}
|
|
22057
22234
|
// ── keyboard navigation / accessibility (item 11) ──
|
|
22058
22235
|
/** Enable/disable keyboard control: focusability + ARIA + key listeners + live region. */
|
|
@@ -22307,6 +22484,7 @@ var NativeRenderer = class {
|
|
|
22307
22484
|
/** Paint the below-data (L-1) + geometry (L0) + chrome (L1) layers from the current scene/coords. */
|
|
22308
22485
|
paintData() {
|
|
22309
22486
|
this.syncLayerCanvasOrder();
|
|
22487
|
+
this.syncLegendOrder();
|
|
22310
22488
|
this.stampScaleInvert();
|
|
22311
22489
|
this.backend.modelAlpha = this.modelAlpha;
|
|
22312
22490
|
this.backend.candleBodyAlpha = this.candleBodyAlpha;
|
|
@@ -22488,6 +22666,7 @@ var NativeRenderer = class {
|
|
|
22488
22666
|
const i0 = Math.max(0, Math.floor(vr.from));
|
|
22489
22667
|
const i1 = Math.min(n - 1, Math.ceil(vr.to));
|
|
22490
22668
|
if (i1 < i0) return;
|
|
22669
|
+
const s1 = Math.max(i1, Math.ceil(vr.to));
|
|
22491
22670
|
this.scene.percentBaseline = this.bars[i0]?.close ?? 0;
|
|
22492
22671
|
const panes = this.scene.orderedPanes();
|
|
22493
22672
|
const pricePane = panes.find((p) => p.kind === "price") ?? null;
|
|
@@ -22505,11 +22684,11 @@ var NativeRenderer = class {
|
|
|
22505
22684
|
const masterModels = models.filter((m) => m.ownScale !== true);
|
|
22506
22685
|
let dr = this.chrome.paneDrawingsRange(masterModels, this.scene, pane === pricePane, vr);
|
|
22507
22686
|
if (pane === pricePane) {
|
|
22508
|
-
const or = overlaySeriesRange(this.scene.indicators.values(), i0,
|
|
22687
|
+
const or = overlaySeriesRange(this.scene.indicators.values(), i0, s1, (id) => this.scene.offsetOf(id));
|
|
22509
22688
|
if (or) dr = dr ? { min: Math.min(dr.min, or.min), max: Math.max(dr.max, or.max) } : or;
|
|
22510
22689
|
}
|
|
22511
22690
|
const includeCandles = pane.kind === "price" && (!this.scene.candlesHidden || this.priceLayersAnchoredToBars(masterModels) || !this.paneHasMeasurableContent(masterModels, dr));
|
|
22512
|
-
pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0,
|
|
22691
|
+
pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, s1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id), margins);
|
|
22513
22692
|
pane.percentBaseline = pane.kind === "price" ? this.bars[i0]?.close ?? 0 : this.firstVisibleValue(masterModels, i0);
|
|
22514
22693
|
pane.axisFormat = void 0;
|
|
22515
22694
|
pane.axisBands = void 0;
|
|
@@ -22549,7 +22728,7 @@ var NativeRenderer = class {
|
|
|
22549
22728
|
continue;
|
|
22550
22729
|
}
|
|
22551
22730
|
const mdr = this.chrome.paneDrawingsRange([model], this.scene, false, vr);
|
|
22552
|
-
sl.scaleTarget = computePaneScale([model], this.bars, false, i0,
|
|
22731
|
+
sl.scaleTarget = computePaneScale([model], this.bars, false, i0, s1, mdr, false, (id) => this.scene.offsetOf(id), margins);
|
|
22553
22732
|
if (!animating || !sl.initialized) {
|
|
22554
22733
|
sl.scale = { ...sl.scaleTarget };
|
|
22555
22734
|
sl.initialized = true;
|
|
@@ -22620,6 +22799,30 @@ var NativeRenderer = class {
|
|
|
22620
22799
|
const master = models.find((m) => m.ownScale !== true) ?? models[0];
|
|
22621
22800
|
return master?.title || null;
|
|
22622
22801
|
}
|
|
22802
|
+
/**
|
|
22803
|
+
* The price pane's layers back to front, as the legend restacks them: every indicator
|
|
22804
|
+
* with a legend row, in paint order, plus the candles. The same layers and order the
|
|
22805
|
+
* object tree lists (see `indicatorZOrder`).
|
|
22806
|
+
*/
|
|
22807
|
+
overlayStack() {
|
|
22808
|
+
const painted = this.scene.orderedIndicatorsForPane(PRICE_PANE_ID).filter((m) => m.legend !== false).map((m) => ({ id: m.id, z: this.scene.zOf(m.id) }));
|
|
22809
|
+
return priceStack(painted, this.scene.candleZ);
|
|
22810
|
+
}
|
|
22811
|
+
/** List the legend rows the way the indicators stack, front-most first (no-op when unchanged). */
|
|
22812
|
+
syncLegendOrder() {
|
|
22813
|
+
this.inputsUI?.orderRows(this.scene.indicatorZOrder().map((e) => e.id));
|
|
22814
|
+
}
|
|
22815
|
+
/** Apply a legend restacking step: shared keys are spread apart first (the order stays),
|
|
22816
|
+
* so the step lands exactly one layer over. No-op when there is nothing to step over. */
|
|
22817
|
+
restackOverlay(id, step) {
|
|
22818
|
+
for (const fix of untie(this.overlayStack())) this.scene.setIndicatorZ(fix.id, fix.z);
|
|
22819
|
+
const z = step(this.overlayStack(), id);
|
|
22820
|
+
if (z === null) return;
|
|
22821
|
+
this.scene.setIndicatorZ(id, z);
|
|
22822
|
+
this.syncLegendOrder();
|
|
22823
|
+
this.scheduler.invalidate(4 /* Full */);
|
|
22824
|
+
for (const cb of this.configChangedCbs) cb();
|
|
22825
|
+
}
|
|
22623
22826
|
/** The pane a bespoke-layer native indicator (volume/vpvr) currently lives in (null = not mounted). */
|
|
22624
22827
|
nativeLayerPane(type) {
|
|
22625
22828
|
for (const m of this.scene.indicators.values()) {
|
|
@@ -23074,17 +23277,35 @@ function leadingPointsBefore(model, headTime) {
|
|
|
23074
23277
|
for (const s of model.series) {
|
|
23075
23278
|
const times = s.kind === "candle" || s.kind === "bar" ? s.bars : isLineLikeSeries(s) ? s.points : null;
|
|
23076
23279
|
if (!times || times.length === 0) continue;
|
|
23077
|
-
|
|
23078
|
-
|
|
23079
|
-
while (lo < hi) {
|
|
23080
|
-
const mid = lo + hi >> 1;
|
|
23081
|
-
if (times[mid].time < headTime) lo = mid + 1;
|
|
23082
|
-
else hi = mid;
|
|
23083
|
-
}
|
|
23084
|
-
return lo;
|
|
23280
|
+
const count = slotsBefore(times, headTime);
|
|
23281
|
+
if (count !== null) return count;
|
|
23085
23282
|
}
|
|
23086
23283
|
return null;
|
|
23087
23284
|
}
|
|
23285
|
+
function slotsBefore(times, headTime) {
|
|
23286
|
+
let lo = 0;
|
|
23287
|
+
let hi = times.length;
|
|
23288
|
+
while (lo < hi) {
|
|
23289
|
+
const mid = lo + hi >> 1;
|
|
23290
|
+
const t = times[mid]?.time;
|
|
23291
|
+
if (t === void 0) return slotsBeforeScan(times, headTime);
|
|
23292
|
+
if (t < headTime) lo = mid + 1;
|
|
23293
|
+
else hi = mid;
|
|
23294
|
+
}
|
|
23295
|
+
return lo;
|
|
23296
|
+
}
|
|
23297
|
+
function slotsBeforeScan(times, headTime) {
|
|
23298
|
+
let lastBefore = -1;
|
|
23299
|
+
let any = false;
|
|
23300
|
+
for (let i = 0; i < times.length; i += 1) {
|
|
23301
|
+
const t = times[i]?.time;
|
|
23302
|
+
if (t === void 0) continue;
|
|
23303
|
+
any = true;
|
|
23304
|
+
if (t >= headTime) return t === headTime ? i : lastBefore + 1;
|
|
23305
|
+
lastBefore = i;
|
|
23306
|
+
}
|
|
23307
|
+
return any ? times.length : null;
|
|
23308
|
+
}
|
|
23088
23309
|
function applyPatch(model, patch) {
|
|
23089
23310
|
if (patch.kind === "value") {
|
|
23090
23311
|
for (const delta of patch.series) {
|
|
@@ -23697,6 +23918,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
23697
23918
|
this.barTransform = null;
|
|
23698
23919
|
// ── chart-type data engines (SDK): one lazily-created engine per style id ──
|
|
23699
23920
|
this.typeEngines = /* @__PURE__ */ new Map();
|
|
23921
|
+
/** Engines suspended by the chart — their host pushes are dropped until they resume. */
|
|
23922
|
+
this.suspendedEngines = /* @__PURE__ */ new WeakSet();
|
|
23700
23923
|
/** Last-seen chart-type settings per type id (dialog edits + persisted restores) —
|
|
23701
23924
|
* replayed into a data engine whenever one is (re)created. */
|
|
23702
23925
|
this.typeSettings = /* @__PURE__ */ new Map();
|
|
@@ -23971,12 +24194,14 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
23971
24194
|
this.mergeHistoryIntoReplay(bars);
|
|
23972
24195
|
return;
|
|
23973
24196
|
}
|
|
23974
|
-
|
|
24197
|
+
const repaint = painted;
|
|
24198
|
+
this.setBarSeries(bars, repaint ? { preserveView: true } : void 0);
|
|
23975
24199
|
if (!painted && bars.length > 0) {
|
|
23976
24200
|
painted = true;
|
|
23977
24201
|
if (opts.firstLoad) this.activateBarLayers();
|
|
23978
24202
|
if (!final) this.historyState = "backfill";
|
|
23979
24203
|
}
|
|
24204
|
+
if (repaint && !final) this.notifySessionsBars("backfill");
|
|
23980
24205
|
};
|
|
23981
24206
|
const abort = new AbortController();
|
|
23982
24207
|
this.progressiveAbort = abort;
|
|
@@ -24179,10 +24404,15 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
24179
24404
|
this.setBarSeries([], { clearing: true });
|
|
24180
24405
|
this.blankIndicatorVisuals();
|
|
24181
24406
|
if (this.activeEngineStyle) {
|
|
24182
|
-
this.typeEngines.get(this.activeEngineStyle)
|
|
24407
|
+
this.suspendTypeEngine(this.typeEngines.get(this.activeEngineStyle));
|
|
24183
24408
|
this.renderer.setNativeData?.(this.activeEngineStyle, void 0);
|
|
24184
24409
|
this.renderer.setNativeData?.(`${this.activeEngineStyle}-pending`, []);
|
|
24185
24410
|
}
|
|
24411
|
+
for (const record of this.registry.all()) {
|
|
24412
|
+
if (!record.native?.started) continue;
|
|
24413
|
+
this.stopNativeInstance(record.native);
|
|
24414
|
+
this.renderer.setNativeData?.(this.nativeChannel(record), void 0);
|
|
24415
|
+
}
|
|
24186
24416
|
}
|
|
24187
24417
|
if (depthOnly) {
|
|
24188
24418
|
this.extendDepth(gen, m.bars ?? 500);
|
|
@@ -24209,6 +24439,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
24209
24439
|
this.restartChartTypeEngine();
|
|
24210
24440
|
this.reexecuteIndicators();
|
|
24211
24441
|
}
|
|
24442
|
+
if (depthOnly) this.releaseHeldNatives();
|
|
24212
24443
|
if (!replayBack) this.startLive();
|
|
24213
24444
|
if (identityChanged) {
|
|
24214
24445
|
this.events.emit("market:changed", { symbol: m.symbol ?? "TEST", timeframe: m.timeframe ?? "60", prev: prev2 });
|
|
@@ -24225,9 +24456,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
24225
24456
|
restartNativeIndicators() {
|
|
24226
24457
|
for (const record of this.registry.all()) {
|
|
24227
24458
|
if (!record.native) continue;
|
|
24228
|
-
|
|
24459
|
+
this.stopNativeInstance(record.native);
|
|
24229
24460
|
record.native.instance = record.native.descriptor.create();
|
|
24230
|
-
record.native.started = false;
|
|
24231
24461
|
record.pendingStructural = true;
|
|
24232
24462
|
if (record.hidden) {
|
|
24233
24463
|
record.native.stale = true;
|
|
@@ -24244,6 +24474,21 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
24244
24474
|
this.typeEngines.clear();
|
|
24245
24475
|
if (this.activeEngineStyle) void this.startChartTypeEngine(this.activeEngineStyle);
|
|
24246
24476
|
}
|
|
24477
|
+
/** Suspend a chart-type engine. Its host drops every push until the engine resumes:
|
|
24478
|
+
* work it left in flight lands in its own state, never on the scene. */
|
|
24479
|
+
suspendTypeEngine(engine) {
|
|
24480
|
+
if (!engine) return;
|
|
24481
|
+
engine.suspend();
|
|
24482
|
+
this.suspendedEngines.add(engine);
|
|
24483
|
+
}
|
|
24484
|
+
/** Stop a running native instance, then revoke its context: the teardown inside `stop()`
|
|
24485
|
+
* still reaches its layer, but work it left in flight (a fetch resolving later) does not. */
|
|
24486
|
+
stopNativeInstance(native) {
|
|
24487
|
+
if (native.started) native.instance.stop();
|
|
24488
|
+
native.started = false;
|
|
24489
|
+
native.revokeContext?.();
|
|
24490
|
+
native.revokeContext = void 0;
|
|
24491
|
+
}
|
|
24247
24492
|
/**
|
|
24248
24493
|
* The FIRST candles just painted (the quick preview, or the only load): activate the
|
|
24249
24494
|
* layers that need bars but NOT full history. The volume layer reads the chart's bars
|
|
@@ -24776,7 +25021,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
24776
25021
|
const def = chartType(id);
|
|
24777
25022
|
const want = def?.dataEngine ? id : null;
|
|
24778
25023
|
if (want === this.activeEngineStyle) return;
|
|
24779
|
-
if (this.activeEngineStyle) this.typeEngines.get(this.activeEngineStyle)
|
|
25024
|
+
if (this.activeEngineStyle) this.suspendTypeEngine(this.typeEngines.get(this.activeEngineStyle));
|
|
24780
25025
|
this.activeEngineStyle = want;
|
|
24781
25026
|
if (want) void this.startChartTypeEngine(want);
|
|
24782
25027
|
}
|
|
@@ -24787,6 +25032,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
24787
25032
|
if (this.activeEngineStyle !== id) return;
|
|
24788
25033
|
const existing = this.typeEngines.get(id);
|
|
24789
25034
|
if (existing) {
|
|
25035
|
+
this.suspendedEngines.delete(existing);
|
|
24790
25036
|
existing.resume();
|
|
24791
25037
|
return;
|
|
24792
25038
|
}
|
|
@@ -24796,6 +25042,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
24796
25042
|
this.typeEngines.set(id, engine);
|
|
24797
25043
|
const stored = this.typeSettings.get(id);
|
|
24798
25044
|
if (stored) engine.onSettings?.(stored);
|
|
25045
|
+
const reachesScene = () => this.typeEngines.get(id) === engine && !this.suspendedEngines.has(engine);
|
|
24799
25046
|
engine.start({
|
|
24800
25047
|
symbol: this.config.market.symbol ?? "TEST",
|
|
24801
25048
|
timeframe: this.config.market.timeframe ?? "60",
|
|
@@ -24803,8 +25050,12 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
24803
25050
|
session: this.config.market.session,
|
|
24804
25051
|
bars: () => this.bars,
|
|
24805
25052
|
data: this.dataControl,
|
|
24806
|
-
pushData: (data) =>
|
|
24807
|
-
|
|
25053
|
+
pushData: (data) => {
|
|
25054
|
+
if (reachesScene()) this.renderer.setNativeData?.(id, data);
|
|
25055
|
+
},
|
|
25056
|
+
pushPending: (ranges) => {
|
|
25057
|
+
if (reachesScene()) this.renderer.setNativeData?.(`${id}-pending`, ranges);
|
|
25058
|
+
}
|
|
24808
25059
|
});
|
|
24809
25060
|
}
|
|
24810
25061
|
/**
|
|
@@ -24812,8 +25063,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
24812
25063
|
* a pre-load style change is picked up by init's `setBarSeries`. With bars loaded, the
|
|
24813
25064
|
* view is rebuilt in place (viewport preserved) and every Pine indicator re-executes:
|
|
24814
25065
|
* its input data changed wholesale, which a streaming session's incremental context
|
|
24815
|
-
* cannot absorb.
|
|
24816
|
-
*
|
|
25066
|
+
* cannot absorb. Natives computed from `ctx.bars()` re-run over the new candles (a
|
|
25067
|
+
* synthetic price style is their source too); layer natives that read the renderer's
|
|
25068
|
+
* bars per frame ignore the poke.
|
|
24817
25069
|
*/
|
|
24818
25070
|
syncBarTransform(style) {
|
|
24819
25071
|
const transform = barTransformFor(style);
|
|
@@ -24823,6 +25075,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
24823
25075
|
this.bars = transform ? transform.full(this.rawBars) : this.rawBars;
|
|
24824
25076
|
this.renderer.setBars(this.bars, { preserveView: true });
|
|
24825
25077
|
this.reexecuteIndicators();
|
|
25078
|
+
if (this.switchingMarket) return;
|
|
25079
|
+
for (const record of this.registry.all()) {
|
|
25080
|
+
if (record.native?.started && record.renderHandle && !record.hidden) this.pokeNative(record);
|
|
25081
|
+
}
|
|
24826
25082
|
}
|
|
24827
25083
|
/**
|
|
24828
25084
|
* Blank every mounted indicator's painted output — series data, fills, backgrounds,
|
|
@@ -24923,10 +25179,39 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
24923
25179
|
if (record.session) {
|
|
24924
25180
|
record.pendingCause = cause;
|
|
24925
25181
|
record.session.notifyBars(reason);
|
|
24926
|
-
} else if (record.native)
|
|
25182
|
+
} else if (record.native) this.pokeNative(record);
|
|
24927
25183
|
}
|
|
24928
25184
|
if (this.activeEngineStyle) this.typeEngines.get(this.activeEngineStyle)?.onBars?.();
|
|
24929
25185
|
}
|
|
25186
|
+
/**
|
|
25187
|
+
* Tell a native its bars changed. A `waitsForHistory` native ignores every change while
|
|
25188
|
+
* the history is still loading — a backfill chunk, a deeper snapshot, a tick arriving
|
|
25189
|
+
* meanwhile — and its output stays on its bars through the anchor its emit carried.
|
|
25190
|
+
* Once the history is complete, a held one starts and a running one re-runs.
|
|
25191
|
+
*/
|
|
25192
|
+
pokeNative(record) {
|
|
25193
|
+
const native = record.native;
|
|
25194
|
+
if (!native.descriptor.waitsForHistory) {
|
|
25195
|
+
native.instance.onBars();
|
|
25196
|
+
return;
|
|
25197
|
+
}
|
|
25198
|
+
if (this.historyState === "backfill") return;
|
|
25199
|
+
if (native.heldForHistory) {
|
|
25200
|
+
native.heldForHistory = false;
|
|
25201
|
+
const handle = this.handles.get(record.id);
|
|
25202
|
+
if (handle) void this.startNativeIndicator(record.id, handle);
|
|
25203
|
+
return;
|
|
25204
|
+
}
|
|
25205
|
+
if (native.started) native.instance.onBars();
|
|
25206
|
+
}
|
|
25207
|
+
/** Start the natives still held for a history that has completed without poking them
|
|
25208
|
+
* (a depth-only switch finishes its trim while the switch gates every poke). */
|
|
25209
|
+
releaseHeldNatives() {
|
|
25210
|
+
if (this.historyState === "backfill") return;
|
|
25211
|
+
for (const record of this.registry.all()) {
|
|
25212
|
+
if (record.native?.heldForHistory && !record.hidden) this.pokeNative(record);
|
|
25213
|
+
}
|
|
25214
|
+
}
|
|
24930
25215
|
/** Gap healing needs a ranged feed and a provider-backed series (offline `data` has no source). */
|
|
24931
25216
|
canHeal() {
|
|
24932
25217
|
return !!this.feed.loadRange && !this.config.market.data?.length;
|
|
@@ -25218,7 +25503,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
25218
25503
|
if (record?.native?.type === "volume") this.volumeOptedOut = true;
|
|
25219
25504
|
if (record) this.settleCode(record, { ok: false, error: new Error(`[vela] updateCode("${id}") \u2014 the indicator was removed.`) });
|
|
25220
25505
|
record?.session?.stop();
|
|
25221
|
-
if (record?.native
|
|
25506
|
+
if (record?.native) this.stopNativeInstance(record.native);
|
|
25222
25507
|
this.handles.delete(id);
|
|
25223
25508
|
if (record?.renderHandle) this.renderer.removeIndicator(record.renderHandle);
|
|
25224
25509
|
const paneId = record?.model?.paneId;
|
|
@@ -25339,7 +25624,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
25339
25624
|
for (const record of this.registry.all()) {
|
|
25340
25625
|
this.settleCode(record, { ok: false, error: new Error("[vela] updateCode \u2014 the chart was destroyed.") });
|
|
25341
25626
|
record.session?.stop();
|
|
25342
|
-
if (record.native
|
|
25627
|
+
if (record.native) this.stopNativeInstance(record.native);
|
|
25343
25628
|
}
|
|
25344
25629
|
for (const engine of this.typeEngines.values()) engine.stop();
|
|
25345
25630
|
this.typeEngines.clear();
|
|
@@ -25466,7 +25751,19 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
25466
25751
|
await this.readyPromise;
|
|
25467
25752
|
const record = this.registry.get(id);
|
|
25468
25753
|
if (!record?.native || record.hidden) return;
|
|
25754
|
+
const native = record.native;
|
|
25755
|
+
if (native.started) return;
|
|
25756
|
+
if (native.descriptor.waitsForHistory && this.historyState === "backfill") {
|
|
25757
|
+
native.heldForHistory = true;
|
|
25758
|
+
this.mountNativePlaceholder(id, record);
|
|
25759
|
+
return;
|
|
25760
|
+
}
|
|
25761
|
+
native.heldForHistory = false;
|
|
25469
25762
|
const channel = this.nativeChannel(record);
|
|
25763
|
+
let alive = true;
|
|
25764
|
+
native.revokeContext = () => {
|
|
25765
|
+
alive = false;
|
|
25766
|
+
};
|
|
25470
25767
|
const ctx = {
|
|
25471
25768
|
id,
|
|
25472
25769
|
symbol: this.config.market.symbol ?? "TEST",
|
|
@@ -25475,14 +25772,18 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
25475
25772
|
session: this.config.market.session,
|
|
25476
25773
|
bars: () => this.bars,
|
|
25477
25774
|
data: this.dataControl,
|
|
25478
|
-
emit: (out) =>
|
|
25479
|
-
|
|
25775
|
+
emit: (out) => {
|
|
25776
|
+
if (alive) this.applyModel(id, this.buildNativeModel(record, out, this.bars[0]?.time));
|
|
25777
|
+
},
|
|
25778
|
+
pushData: (data) => {
|
|
25779
|
+
if (alive) this.renderer.setNativeData?.(channel, data);
|
|
25780
|
+
},
|
|
25480
25781
|
setStatus: (status) => {
|
|
25481
|
-
if (record.renderHandle && !record.hidden) this.renderer.setIndicatorStatus?.(record.renderHandle, status);
|
|
25782
|
+
if (alive && record.renderHandle && !record.hidden) this.renderer.setIndicatorStatus?.(record.renderHandle, status);
|
|
25482
25783
|
}
|
|
25483
25784
|
};
|
|
25484
|
-
|
|
25485
|
-
|
|
25785
|
+
native.instance.start(ctx, record.inputValues);
|
|
25786
|
+
native.started = true;
|
|
25486
25787
|
} catch (err) {
|
|
25487
25788
|
this.fail(id, handle, err);
|
|
25488
25789
|
}
|
|
@@ -25495,8 +25796,14 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
25495
25796
|
const { type, descriptor } = record.native;
|
|
25496
25797
|
return descriptor.multiInstance ? nativeInstanceChannel(type, record.id) : type;
|
|
25497
25798
|
}
|
|
25498
|
-
/**
|
|
25499
|
-
|
|
25799
|
+
/**
|
|
25800
|
+
* Wrap a native indicator's emitted visuals into a full IndicatorModel, tagged `native`.
|
|
25801
|
+
* `anchorTime` is the first bar the output is index-aligned to — the chart's first bar
|
|
25802
|
+
* when it emitted. Without it the renderer pins the output's first slot to whatever the
|
|
25803
|
+
* first bar is NOW, so history landing in front of a computed output (or the oldest
|
|
25804
|
+
* bars trimmed under it) would slide the whole plot by that many bars.
|
|
25805
|
+
*/
|
|
25806
|
+
buildNativeModel(record, out, anchorTime) {
|
|
25500
25807
|
const d = record.native.descriptor;
|
|
25501
25808
|
const type = record.native.type;
|
|
25502
25809
|
const channel = this.nativeChannel(record);
|
|
@@ -25507,6 +25814,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
25507
25814
|
overlay: d.overlay,
|
|
25508
25815
|
paneHint: d.paneHint,
|
|
25509
25816
|
native: { type, ...channel !== type ? { channel } : {} },
|
|
25817
|
+
...anchorTime != null ? { anchorTime } : {},
|
|
25510
25818
|
...d.legend === false ? { legend: false } : {},
|
|
25511
25819
|
...out.paneAxis != null ? { paneAxis: out.paneAxis } : {},
|
|
25512
25820
|
series: out.series ?? [],
|
|
@@ -25561,8 +25869,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
25561
25869
|
const paneId = this.routePane(id, model, record.options ?? {});
|
|
25562
25870
|
this.placeModel(model, id, paneId);
|
|
25563
25871
|
record.model = model;
|
|
25564
|
-
this.ensurePaneFor(paneId);
|
|
25872
|
+
const paneCreated = this.ensurePaneFor(paneId);
|
|
25565
25873
|
record.renderHandle = this.renderer.mountIndicator(model);
|
|
25874
|
+
if (paneCreated) this.events.emit("pane:changed", void 0);
|
|
25566
25875
|
record.pendingStructural = true;
|
|
25567
25876
|
if (record.hidden) {
|
|
25568
25877
|
this.renderer.setIndicatorVisible?.(record.renderHandle, false);
|
|
@@ -25586,24 +25895,49 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
25586
25895
|
const paneId = this.routePane(id, model, record.options ?? {});
|
|
25587
25896
|
this.placeModel(model, id, paneId);
|
|
25588
25897
|
record.model = model;
|
|
25589
|
-
this.ensurePaneFor(paneId);
|
|
25898
|
+
const paneCreated = this.ensurePaneFor(paneId);
|
|
25590
25899
|
record.renderHandle = this.renderer.mountIndicator(model);
|
|
25900
|
+
if (paneCreated) this.events.emit("pane:changed", void 0);
|
|
25591
25901
|
record.pendingStructural = true;
|
|
25592
25902
|
this.renderer.setIndicatorVisible?.(record.renderHandle, false);
|
|
25593
25903
|
this.announce(record, this.handles.get(id));
|
|
25594
25904
|
}
|
|
25905
|
+
/**
|
|
25906
|
+
* The legend row of a native HELD for the history (see `waitsForHistory`): an empty
|
|
25907
|
+
* model with the title and inputs schema, spinning, not announced — exactly a script
|
|
25908
|
+
* indicator's loading placeholder, so `ready`/`indicator:added` and `inspect()` still
|
|
25909
|
+
* mean "it produced output". A record that already has a row (restarted over a new
|
|
25910
|
+
* market, or shown again) keeps it and only spins. The first emit remounts over it.
|
|
25911
|
+
*/
|
|
25912
|
+
mountNativePlaceholder(id, record) {
|
|
25913
|
+
record.pendingStructural = true;
|
|
25914
|
+
if (!record.renderHandle) {
|
|
25915
|
+
const model = this.buildNativeModel(record, {});
|
|
25916
|
+
const paneId = this.routePane(id, model, record.options ?? {});
|
|
25917
|
+
this.placeModel(model, id, paneId);
|
|
25918
|
+
record.model = model;
|
|
25919
|
+
const paneCreated = this.ensurePaneFor(paneId);
|
|
25920
|
+
record.renderHandle = this.renderer.mountIndicator(model);
|
|
25921
|
+
if (paneCreated) this.events.emit("pane:changed", void 0);
|
|
25922
|
+
}
|
|
25923
|
+
this.setLoading(record, true);
|
|
25924
|
+
}
|
|
25595
25925
|
/** Flip the record's loading state and reflect it in the legend row (spinner on/off). */
|
|
25596
25926
|
setLoading(record, loading) {
|
|
25597
25927
|
record.loading = loading;
|
|
25598
25928
|
if (record.renderHandle && !record.hidden) this.renderer.setIndicatorStatus?.(record.renderHandle, loading ? "loading" : "idle");
|
|
25599
25929
|
}
|
|
25930
|
+
/** Make sure the renderer has the pane; true when it is new to the layout (the caller
|
|
25931
|
+
* emits `pane:changed` once the indicator's model sits on it, so `list()` shows it). */
|
|
25600
25932
|
ensurePaneFor(paneId) {
|
|
25601
|
-
|
|
25933
|
+
const created = !this.paneOrder.includes(paneId);
|
|
25934
|
+
if (created) this.paneOrder.push(paneId);
|
|
25602
25935
|
this.renderer.ensurePane({
|
|
25603
25936
|
id: paneId,
|
|
25604
25937
|
kind: paneId === "price" ? "price" : "study",
|
|
25605
25938
|
order: this.paneOrder.indexOf(paneId)
|
|
25606
25939
|
});
|
|
25940
|
+
return created;
|
|
25607
25941
|
}
|
|
25608
25942
|
/** True when any (still-registered) indicator sits on the given pane. */
|
|
25609
25943
|
paneStillUsed(paneId) {
|
|
@@ -25639,12 +25973,14 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
25639
25973
|
const panes = [];
|
|
25640
25974
|
for (const paneId of this.paneOrder) {
|
|
25641
25975
|
if (paneId !== "price" && !byPane.has(paneId)) continue;
|
|
25976
|
+
const heightWeight = this.renderer.paneHeightWeight?.(paneId) ?? null;
|
|
25642
25977
|
panes.push({
|
|
25643
25978
|
id: paneId,
|
|
25644
25979
|
kind: paneId === "price" ? "price" : "study",
|
|
25645
25980
|
order: panes.length,
|
|
25646
25981
|
collapsed: this.collapsedPanes.has(paneId),
|
|
25647
25982
|
maximized: this.maximizedPaneId === paneId,
|
|
25983
|
+
...heightWeight !== null ? { heightWeight } : {},
|
|
25648
25984
|
indicators: byPane.get(paneId) ?? []
|
|
25649
25985
|
});
|
|
25650
25986
|
}
|
|
@@ -25676,6 +26012,22 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
25676
26012
|
this.renderer.setPaneMaximized?.(paneId);
|
|
25677
26013
|
this.events.emit("pane:changed", void 0);
|
|
25678
26014
|
}
|
|
26015
|
+
setPaneHeightWeight(paneId, weight) {
|
|
26016
|
+
if (!this.renderer.setPaneHeightWeight) {
|
|
26017
|
+
console.warn("[vela] the active renderer does not support pane sizing \u2014 chart.panes.setHeightWeight ignored.");
|
|
26018
|
+
return;
|
|
26019
|
+
}
|
|
26020
|
+
if (typeof weight !== "number" || !Number.isFinite(weight) || weight <= 0) {
|
|
26021
|
+
console.warn(`[vela] chart.panes.setHeightWeight: weight must be a positive number (got ${String(weight)}).`);
|
|
26022
|
+
return;
|
|
26023
|
+
}
|
|
26024
|
+
if (!this.existingPaneIds().includes(paneId)) {
|
|
26025
|
+
console.warn(`[vela] chart.panes.setHeightWeight: no pane "${paneId}".`);
|
|
26026
|
+
return;
|
|
26027
|
+
}
|
|
26028
|
+
this.renderer.setPaneHeightWeight(paneId, weight);
|
|
26029
|
+
this.events.emit("pane:changed", void 0);
|
|
26030
|
+
}
|
|
25679
26031
|
/** IndicatorController/PaneController: move (or merge) an indicator to another pane. */
|
|
25680
26032
|
moveIndicator(id, target) {
|
|
25681
26033
|
if (!this.paneManagementSupported() || !this.renderer.setIndicatorPane) {
|
|
@@ -25773,6 +26125,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
25773
26125
|
this.maximizedPaneId = a.maximized ? a.paneId : null;
|
|
25774
26126
|
this.events.emit("pane:changed", void 0);
|
|
25775
26127
|
break;
|
|
26128
|
+
case "resize":
|
|
26129
|
+
this.events.emit("pane:changed", void 0);
|
|
26130
|
+
break;
|
|
25776
26131
|
}
|
|
25777
26132
|
}
|
|
25778
26133
|
/** Fire `indicator:added` + the handle's `ready` once, on the first computed model. */
|
|
@@ -25803,14 +26158,16 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
25803
26158
|
const paneId2 = this.routePane(id, model, record.options ?? {});
|
|
25804
26159
|
this.placeModel(model, id, paneId2);
|
|
25805
26160
|
record.model = model;
|
|
25806
|
-
this.ensurePaneFor(paneId2);
|
|
26161
|
+
const paneCreated = this.ensurePaneFor(paneId2);
|
|
25807
26162
|
record.renderHandle = this.renderer.mountIndicator(model);
|
|
25808
26163
|
record.pendingStructural = false;
|
|
26164
|
+
if (paneCreated) this.events.emit("pane:changed", void 0);
|
|
25809
26165
|
this.announce(record, handle);
|
|
25810
26166
|
return true;
|
|
25811
26167
|
}
|
|
25812
26168
|
let paneId = record.model?.paneId ?? "price";
|
|
25813
26169
|
const prevOwnScale = record.model?.ownScale === true;
|
|
26170
|
+
let rerouted = false;
|
|
25814
26171
|
if (record.loading && !record.paneLocked) {
|
|
25815
26172
|
const routed = this.routePane(id, model, record.options ?? {});
|
|
25816
26173
|
if (routed !== paneId) {
|
|
@@ -25821,6 +26178,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
25821
26178
|
this.ensurePaneFor(routed);
|
|
25822
26179
|
record.pendingStructural = true;
|
|
25823
26180
|
paneId = routed;
|
|
26181
|
+
rerouted = true;
|
|
25824
26182
|
}
|
|
25825
26183
|
}
|
|
25826
26184
|
this.placeModel(model, id, paneId);
|
|
@@ -25833,6 +26191,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
25833
26191
|
} else {
|
|
25834
26192
|
this.renderer.updateIndicator(record.renderHandle, modelToValuePatch(model));
|
|
25835
26193
|
}
|
|
26194
|
+
if (rerouted) this.events.emit("pane:changed", void 0);
|
|
25836
26195
|
if (record.loading) this.setLoading(record, false);
|
|
25837
26196
|
this.announce(record, handle);
|
|
25838
26197
|
return true;
|
|
@@ -25847,13 +26206,16 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
25847
26206
|
const overlay = options.overlay ?? model.overlay;
|
|
25848
26207
|
return overlay ? "price" : `pane-${id}`;
|
|
25849
26208
|
}
|
|
25850
|
-
/** Each named value series' last
|
|
25851
|
-
* engine fills, so this works without an execution context.
|
|
25852
|
-
|
|
26209
|
+
/** Each named value series' value on the chart's last bar — the model is the one source
|
|
26210
|
+
* every engine fills, so this works without an execution context. Points past the last
|
|
26211
|
+
* bar (a series shifted forward) are skipped by time. */
|
|
26212
|
+
static plotsAtLastBar(model, lastBarTime) {
|
|
25853
26213
|
const out = {};
|
|
25854
26214
|
for (const spec of model.series) {
|
|
25855
26215
|
if (!isLineLikeSeries(spec)) continue;
|
|
25856
|
-
|
|
26216
|
+
let i = spec.points.length - 1;
|
|
26217
|
+
if (lastBarTime !== void 0) while (i >= 0 && spec.points[i].time > lastBarTime) i -= 1;
|
|
26218
|
+
out[spec.title] = spec.points[i]?.value ?? null;
|
|
25857
26219
|
}
|
|
25858
26220
|
return out;
|
|
25859
26221
|
}
|
|
@@ -25909,7 +26271,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
25909
26271
|
// whatever produced this run.
|
|
25910
26272
|
forming: this.config.live && this.bars.length > 0,
|
|
25911
26273
|
complete: this.historyState !== "backfill",
|
|
25912
|
-
plots: _EngineOrchestrator.plotsAtLastBar(model),
|
|
26274
|
+
plots: _EngineOrchestrator.plotsAtLastBar(model, lastBar?.time),
|
|
25913
26275
|
vars: snapshot?.variables ?? {},
|
|
25914
26276
|
...snapshot?.strategy ? { strategy: snapshot.strategy } : {},
|
|
25915
26277
|
warnings: snapshot?.warnings ?? [],
|
|
@@ -25978,8 +26340,9 @@ function sameInputValue(a, b) {
|
|
|
25978
26340
|
return typeof a === "object" && a !== null && typeof b === "object" && b !== null && JSON.stringify(a) === JSON.stringify(b);
|
|
25979
26341
|
}
|
|
25980
26342
|
function blankedModel(model) {
|
|
26343
|
+
const { anchorTime: _anchor, ...rest } = model;
|
|
25981
26344
|
return {
|
|
25982
|
-
...
|
|
26345
|
+
...rest,
|
|
25983
26346
|
series: model.series.map((s) => s.kind === "candle" || s.kind === "bar" ? { ...s, bars: [] } : s.kind === "markers" ? { ...s, markers: [] } : { ...s, points: [] }),
|
|
25984
26347
|
fills: [],
|
|
25985
26348
|
backgrounds: [],
|
|
@@ -26002,12 +26365,14 @@ function modelToValuePatch(model) {
|
|
|
26002
26365
|
if (s.kind === "candle" || s.kind === "bar") {
|
|
26003
26366
|
series.push({ seriesId: s.id, kind: "bars", bars: s.bars, barColors: s.barColors ?? [] });
|
|
26004
26367
|
for (const b of s.bars) {
|
|
26368
|
+
if (!b) continue;
|
|
26005
26369
|
if (b.time < from) from = b.time;
|
|
26006
26370
|
if (b.time > to) to = b.time;
|
|
26007
26371
|
}
|
|
26008
26372
|
} else if (isLineLikeSeries(s)) {
|
|
26009
26373
|
series.push({ seriesId: s.id, kind: "points", points: s.points });
|
|
26010
26374
|
for (const p of s.points) {
|
|
26375
|
+
if (!p) continue;
|
|
26011
26376
|
if (p.time < from) from = p.time;
|
|
26012
26377
|
if (p.time > to) to = p.time;
|
|
26013
26378
|
}
|
|
@@ -26064,6 +26429,17 @@ var PanesControl = class {
|
|
|
26064
26429
|
if (this.guard()) this.controller.maximizePane(paneId);
|
|
26065
26430
|
return this;
|
|
26066
26431
|
}
|
|
26432
|
+
/**
|
|
26433
|
+
* Set a pane's height weight: the panes share the plot height in proportion to their
|
|
26434
|
+
* weights, so `setHeightWeight('price', 6)` next to two study panes at 1 gives the price
|
|
26435
|
+
* pane three quarters of the height. The weight sticks until the user drags a separator
|
|
26436
|
+
* or it is set again; a collapsed pane applies it when expanded. `weight` must be a
|
|
26437
|
+
* positive number.
|
|
26438
|
+
*/
|
|
26439
|
+
setHeightWeight(paneId, weight) {
|
|
26440
|
+
if (this.guard()) this.controller.setPaneHeightWeight(paneId, weight);
|
|
26441
|
+
return this;
|
|
26442
|
+
}
|
|
26067
26443
|
/** Move/merge an indicator to a pane — sugar for `handle.moveTo(...)` by id. */
|
|
26068
26444
|
moveIndicator(id, target) {
|
|
26069
26445
|
if (this.guard()) this.controller.moveIndicator(id, target);
|
|
@@ -26388,6 +26764,9 @@ function classicDescriptor(spec) {
|
|
|
26388
26764
|
overlay: spec.overlay,
|
|
26389
26765
|
// Studies stack (a 20 and a 200 moving average side by side) — unlike the layer-backed natives.
|
|
26390
26766
|
multiInstance: true,
|
|
26767
|
+
// A study's values depend on every bar before them (an EMA's seed, an RSI's
|
|
26768
|
+
// smoothing), so it computes once over the whole history, as script indicators do.
|
|
26769
|
+
waitsForHistory: true,
|
|
26391
26770
|
inputsSchema: () => spec.inputs,
|
|
26392
26771
|
defaultInputs: () => Object.fromEntries(spec.inputs.map((i) => [i.key, i.defval])),
|
|
26393
26772
|
create: () => new ClassicIndicator(spec)
|
|
@@ -27223,41 +27602,50 @@ var maEnvelope = {
|
|
|
27223
27602
|
return directionalChannel(bars, basis, map(basis, (x) => x * (1 + k)), map(basis, (x) => x * (1 - k)), ink, "envelope");
|
|
27224
27603
|
}
|
|
27225
27604
|
};
|
|
27226
|
-
var
|
|
27227
|
-
|
|
27228
|
-
|
|
27229
|
-
|
|
27230
|
-
|
|
27231
|
-
|
|
27232
|
-
|
|
27605
|
+
var leastSquaresMa = {
|
|
27606
|
+
type: "lsma",
|
|
27607
|
+
title: "Least Squares Moving Average",
|
|
27608
|
+
shortTitle: "LSMA",
|
|
27609
|
+
overlay: true,
|
|
27610
|
+
inputs: [
|
|
27611
|
+
sourceInput("Close", "source", "Source", "Price series the regression line is fitted to."),
|
|
27612
|
+
{ ...lengthInput(25, "length", "LSMA Length", 5e3, "Number of bars N in each least-squares fit. Every bar fits a straight line to its last N values and plots where that line ends, so the average leads turns that a same-length SMA trails, at the cost of overshooting after sharp moves."), min: 2 },
|
|
27613
|
+
intInput("offset", "Regression Offset", 0, 0, 500, "Bars to step back along each bar's fitted line. 0 plots the line's endpoint at the current bar; larger values read the fit further back, trading lead for smoothness."),
|
|
27614
|
+
...directionInputs()
|
|
27615
|
+
],
|
|
27616
|
+
compute: (bars, inputs) => {
|
|
27617
|
+
const src = sourceValues(bars, str3(inputs, "source", "Close"));
|
|
27618
|
+
const values = linreg(src, num3(inputs, "length", 25), Math.trunc(num3(inputs, "offset", 0)));
|
|
27619
|
+
return { plots: [slopeColoredPlot("lsma", "LSMA", values, inputs)] };
|
|
27620
|
+
}
|
|
27621
|
+
};
|
|
27622
|
+
var linearRegressionSlope = {
|
|
27623
|
+
type: "linear-regression-slope",
|
|
27624
|
+
title: "Linear Regression Slope",
|
|
27625
|
+
shortTitle: "LRS",
|
|
27233
27626
|
overlay: false,
|
|
27234
27627
|
inputs: [
|
|
27235
|
-
optionInput("mode", "Mode", LINREG_CURVE, [LINREG_CURVE, LINREG_SLOPE], "Curve plots the regression value on the price chart; Slope plots the per-bar slope of the fit in the pane."),
|
|
27236
27628
|
{ ...lengthInput(100, "length", "Length", 5e3, "Number of bars used to fit the linear regression."), min: 2 },
|
|
27237
27629
|
sourceInput("Close", "source", "Source", "Price series the regression is fitted to."),
|
|
27238
|
-
colorInput(BULLISH, "Rising", "bullColor", "Color of the
|
|
27239
|
-
colorInput(BEARISH, "Falling", "bearColor", "Color of the
|
|
27240
|
-
colorInput(NEUTRAL, "
|
|
27630
|
+
colorInput(BULLISH, "Rising", "bullColor", "Color of the slope line while the fit slopes upward."),
|
|
27631
|
+
colorInput(BEARISH, "Falling", "bearColor", "Color of the slope line while the fit slopes downward."),
|
|
27632
|
+
colorInput(NEUTRAL, "Slope Line", "lineColor", "Color of the slope line while the fit is flat.")
|
|
27241
27633
|
],
|
|
27242
27634
|
compute: (bars, inputs) => {
|
|
27243
27635
|
const len = num3(inputs, "length", 100);
|
|
27244
27636
|
const src = sourceValues(bars, str3(inputs, "source", "Close"));
|
|
27245
|
-
const
|
|
27246
|
-
const slope = zip(curve, linreg(src, len, 1), (a, b) => a - b);
|
|
27637
|
+
const slope = zip(linreg(src, len, 0), linreg(src, len, 1), (a, b) => a - b);
|
|
27247
27638
|
const bull = str3(inputs, "bullColor", BULLISH);
|
|
27248
27639
|
const bear = str3(inputs, "bearColor", BEARISH);
|
|
27249
27640
|
const flat = str3(inputs, "lineColor", NEUTRAL);
|
|
27250
27641
|
const colors = slope.map((s) => Number.isFinite(s) ? s > 0 ? bull : s < 0 ? bear : flat : null);
|
|
27251
|
-
|
|
27252
|
-
|
|
27253
|
-
|
|
27254
|
-
|
|
27255
|
-
};
|
|
27256
|
-
}
|
|
27257
|
-
return { plots: [{ key: "curve", title: "Curve", values: curve, color: flat, width: 2, colors, overlay: true }] };
|
|
27642
|
+
return {
|
|
27643
|
+
plots: [{ key: "slope", title: "Slope", values: slope, color: flat, width: 2, colors }],
|
|
27644
|
+
levels: [{ key: "zero", price: 0, color: transp(NEUTRAL, 50), lineStyle: "dashed" }]
|
|
27645
|
+
};
|
|
27258
27646
|
}
|
|
27259
27647
|
};
|
|
27260
|
-
var averageSpecs = [simpleMa, exponentialMa, movingAverage, smoothedMa, zlema, vidya, maEnvelope,
|
|
27648
|
+
var averageSpecs = [simpleMa, exponentialMa, movingAverage, smoothedMa, zlema, vidya, maEnvelope, leastSquaresMa, linearRegressionSlope];
|
|
27261
27649
|
|
|
27262
27650
|
// src/core/native-indicators/classics/bands.ts
|
|
27263
27651
|
var JAW_COLOR = "#ab47bc";
|