@luxalgo/vela 0.6.16 → 0.6.18
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 +4 -0
- package/dist/{DataProvider-XLdJB86x.d.ts → DataProvider-Cb-2lC5O.d.ts} +1 -1
- package/dist/{DataProvider-xH2VBcFT.d.cts → DataProvider-UtWw2CgJ.d.cts} +1 -1
- package/dist/{chunk-GIJYO4LO.js → chunk-22QJKGXX.js} +666 -189
- package/dist/{chunk-SEOO6YG7.js → chunk-BXF7VNNE.js} +181 -111
- package/dist/{chunk-6DPAYEGO.js → chunk-CAFCLMPF.js} +1 -0
- package/dist/{chunk-I77F2SVE.js → chunk-IFJJPXSV.js} +1 -1
- package/dist/{chunk-MGU3USTP.js → chunk-T5Z5YUCF.js} +1 -1
- package/dist/{chunk-Q3TFJNJW.js → chunk-XCBJU674.js} +2 -2
- package/dist/{contributions-BEkxwA5F.d.cts → contributions-5nz3PLRe.d.ts} +9 -4
- package/dist/{contributions-CoAX7_SF.d.ts → contributions-DEPRtkmG.d.cts} +9 -4
- package/dist/index.cjs +662 -185
- package/dist/index.d.cts +15 -6
- package/dist/index.d.ts +15 -6
- package/dist/index.js +4 -4
- package/dist/{options-bdSAtp7y.d.ts → options-BCRmYALw.d.cts} +24 -5
- package/dist/{options-bdSAtp7y.d.cts → options-BCRmYALw.d.ts} +24 -5
- package/dist/{plugin-CNUgbRNq.d.ts → plugin-CUtfLeqz.d.ts} +3 -3
- package/dist/{plugin-Dxyq35Zf.d.cts → plugin-DVUaROPo.d.cts} +3 -3
- package/dist/plugin.cjs +1 -0
- package/dist/plugin.d.cts +4 -4
- package/dist/plugin.d.ts +4 -4
- package/dist/plugin.js +2 -2
- package/dist/providers/binance.d.cts +2 -2
- package/dist/providers/binance.d.ts +2 -2
- package/dist/providers/coinbase.d.cts +2 -2
- package/dist/providers/coinbase.d.ts +2 -2
- package/dist/providers/hyperliquid.d.cts +2 -2
- package/dist/providers/hyperliquid.d.ts +2 -2
- package/dist/{statusline-model-CbPza-Wc.d.cts → statusline-model-7ECSDtgt.d.cts} +3 -3
- package/dist/{statusline-model-D7cWkw8A.d.ts → statusline-model-D1Fhs8Dk.d.ts} +3 -3
- package/dist/ui.cjs +1 -0
- package/dist/ui.d.cts +1 -1
- package/dist/ui.d.ts +1 -1
- package/dist/ui.js +3 -3
- package/dist/vela.global.js +662 -185
- package/dist/vela.global.min.js +52 -53
- package/dist/widget.cjs +839 -291
- package/dist/widget.d.cts +39 -12
- package/dist/widget.d.ts +39 -12
- package/dist/widget.js +7 -7
- package/dist/workspace.cjs +839 -291
- package/dist/workspace.d.cts +5 -5
- package/dist/workspace.d.ts +5 -5
- package/dist/workspace.js +6 -6
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -25,6 +25,23 @@ function _interopNamespace(e) {
|
|
|
25
25
|
var menu__namespace = /*#__PURE__*/_interopNamespace(menu);
|
|
26
26
|
var dialog__namespace = /*#__PURE__*/_interopNamespace(dialog);
|
|
27
27
|
|
|
28
|
+
// src/core/options.ts
|
|
29
|
+
var LIVE_BAR_EASE_DEFAULT_MS = 90;
|
|
30
|
+
var LIVE_BAR_EASE_MAX_MS = 1e3;
|
|
31
|
+
function resolveLiveBarEaseMs(value) {
|
|
32
|
+
if (value === true) return LIVE_BAR_EASE_DEFAULT_MS;
|
|
33
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return 0;
|
|
34
|
+
return Math.min(value, LIVE_BAR_EASE_MAX_MS);
|
|
35
|
+
}
|
|
36
|
+
function resolveAnimations(animations) {
|
|
37
|
+
if (typeof animations === "boolean") return { animZoom: animations, animPan: animations, animLiveBar: 0 };
|
|
38
|
+
return {
|
|
39
|
+
animZoom: animations?.zoom ?? true,
|
|
40
|
+
animPan: animations?.pan ?? true,
|
|
41
|
+
animLiveBar: resolveLiveBarEaseMs(animations?.liveBar)
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
28
45
|
// src/core/model/series.ts
|
|
29
46
|
function isLineLikeSeries(spec) {
|
|
30
47
|
return spec.kind !== "candle" && spec.kind !== "bar" && spec.kind !== "markers";
|
|
@@ -575,6 +592,7 @@ registerIcon("star-filled", S('<path d="M8 2.2l1.75 3.55 3.9.55-2.8 2.75.65 3.9L
|
|
|
575
592
|
registerIcon("grip", S('<circle cx="6" cy="3.5" r="1"/><circle cx="10" cy="3.5" r="1"/><circle cx="6" cy="8" r="1"/><circle cx="10" cy="8" r="1"/><circle cx="6" cy="12.5" r="1"/><circle cx="10" cy="12.5" r="1"/>', 'fill="currentColor" stroke="none"'));
|
|
576
593
|
registerIcon("kebab", S('<circle cx="8" cy="3.2" r="1.2"/><circle cx="8" cy="8" r="1.2"/><circle cx="8" cy="12.8" r="1.2"/>', 'fill="currentColor" stroke="none"'));
|
|
577
594
|
registerIcon("burger", S('<path d="M2.5 4.5h11M2.5 8h11M2.5 11.5h11"/>'));
|
|
595
|
+
registerIcon("info", S('<path d="M8 7.4v4.4"/><circle cx="8" cy="4.6" r="0.9" fill="currentColor" stroke="none"/>'));
|
|
578
596
|
registerIcon("reset", S('<path d="M2 8a6 6 0 1 0 6-6 6.5 6.5 0 0 0-4.5 1.83L2 5.33"/><path d="M2 2v3.33h3.33"/>'));
|
|
579
597
|
registerIcon("pane-collapse", S('<path d="M2.6 9.4h4v4M13.4 6.6h-4v-4"/><path d="m9.4 6.6 4.2-4.2M2.4 13.6l4.2-4.2"/>'));
|
|
580
598
|
registerIcon("pane-expand", S('<path d="M9.8 2.4h3.8v3.8M6.2 13.6H2.4V9.8"/><path d="m13.6 2.4-4.4 4.4M2.4 13.6l4.4-4.4"/>'));
|
|
@@ -6906,15 +6924,18 @@ var DrawingController = class {
|
|
|
6906
6924
|
next = [...ids];
|
|
6907
6925
|
}
|
|
6908
6926
|
this.selectedIds = next.filter((id) => this.store.has(id));
|
|
6927
|
+
this.announceSelection();
|
|
6928
|
+
}
|
|
6929
|
+
/** Push the selection to the renderer and announce it (primary + the full list). */
|
|
6930
|
+
announceSelection() {
|
|
6909
6931
|
this.port?.setSelection(this.selectedIds);
|
|
6910
|
-
this.events.emit("drawing:selected", { id: this.selectedIds[0] ?? null });
|
|
6932
|
+
this.events.emit("drawing:selected", { id: this.selectedIds[0] ?? null, ids: [...this.selectedIds] });
|
|
6911
6933
|
}
|
|
6912
6934
|
/** Restore a history snapshot, reconciling selection against what survived. */
|
|
6913
6935
|
restoreSnapshot(doc) {
|
|
6914
6936
|
this.store.load(doc);
|
|
6915
6937
|
this.selectedIds = this.selectedIds.filter((id) => this.store.has(id));
|
|
6916
|
-
this.
|
|
6917
|
-
this.events.emit("drawing:selected", { id: this.selectedIds[0] ?? null });
|
|
6938
|
+
this.announceSelection();
|
|
6918
6939
|
}
|
|
6919
6940
|
/** Delete drawings as one undo step; prune them from the selection. */
|
|
6920
6941
|
deleteMany(ids) {
|
|
@@ -7047,6 +7068,9 @@ var DrawingController = class {
|
|
|
7047
7068
|
case "duplicate":
|
|
7048
7069
|
this.duplicate(i.ids);
|
|
7049
7070
|
break;
|
|
7071
|
+
case "clone":
|
|
7072
|
+
if (this.enabled && i.docs.length) this.insertClones(i.docs);
|
|
7073
|
+
break;
|
|
7050
7074
|
case "copy":
|
|
7051
7075
|
this.copy(i.ids);
|
|
7052
7076
|
break;
|
|
@@ -8883,7 +8907,12 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
8883
8907
|
if (!model) continue;
|
|
8884
8908
|
const paneId = model.paneId ?? "price";
|
|
8885
8909
|
if (!byPane.has(paneId)) byPane.set(paneId, []);
|
|
8886
|
-
byPane.get(paneId).push({
|
|
8910
|
+
byPane.get(paneId).push({
|
|
8911
|
+
id: r.id,
|
|
8912
|
+
title: model.title || r.title,
|
|
8913
|
+
...model.shorttitle ? { shorttitle: model.shorttitle } : {},
|
|
8914
|
+
ownScale: model.ownScale === true
|
|
8915
|
+
});
|
|
8887
8916
|
}
|
|
8888
8917
|
const panes = [];
|
|
8889
8918
|
for (const paneId of this.paneOrder) {
|
|
@@ -10525,6 +10554,23 @@ function attachChromeTooltip(anchor, opts) {
|
|
|
10525
10554
|
clear();
|
|
10526
10555
|
};
|
|
10527
10556
|
}
|
|
10557
|
+
function chromeHint(text, opts) {
|
|
10558
|
+
const px = opts.size ?? 16;
|
|
10559
|
+
const el = document.createElement("span");
|
|
10560
|
+
el.className = "vela-hint";
|
|
10561
|
+
el.setAttribute("role", "img");
|
|
10562
|
+
el.setAttribute("aria-label", text);
|
|
10563
|
+
el.innerHTML = iconAt("info", px);
|
|
10564
|
+
el.style.cssText = `flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:${px}px;height:${px}px;border-radius:50%;cursor:help;user-select:none;background:var(--vela-hover);color:var(--vela-fg-muted);transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;`;
|
|
10565
|
+
const hover = (on) => {
|
|
10566
|
+
el.style.background = on ? "var(--vela-active)" : "var(--vela-hover)";
|
|
10567
|
+
el.style.color = on ? "var(--vela-fg-bright)" : "var(--vela-fg-muted)";
|
|
10568
|
+
};
|
|
10569
|
+
el.addEventListener("pointerenter", () => hover(true));
|
|
10570
|
+
el.addEventListener("pointerleave", () => hover(false));
|
|
10571
|
+
const dispose = attachChromeTooltip(el, { host: opts.host, theme: opts.theme, text: () => text, wrap: true, delayMs: 250 });
|
|
10572
|
+
return { el, dispose };
|
|
10573
|
+
}
|
|
10528
10574
|
function runMachine(machine3, props, render) {
|
|
10529
10575
|
const m = new vanilla.VanillaMachine(machine3, props);
|
|
10530
10576
|
const unsub = m.subscribe(render);
|
|
@@ -13666,14 +13712,11 @@ var IndicatorInputsDialog = class {
|
|
|
13666
13712
|
buildTextField(id, current, emit) {
|
|
13667
13713
|
return buildFieldControl({ kind: "text", id, value: current, onChange: emit }).el;
|
|
13668
13714
|
}
|
|
13669
|
-
/**
|
|
13715
|
+
/** The shared ⓘ hint carrying an input's `tooltip` (see {@link chromeHint}). */
|
|
13670
13716
|
infoButton(tooltip) {
|
|
13671
|
-
const
|
|
13672
|
-
|
|
13673
|
-
|
|
13674
|
-
b.className = "vela-ind-hint";
|
|
13675
|
-
b.style.cssText = "flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:50%;font-weight:600;font-size:11px;font-family:inherit;line-height:1;cursor:help;user-select:none;";
|
|
13676
|
-
return b;
|
|
13717
|
+
const { el, dispose } = chromeHint(tooltip, { host: this.host.container, theme: () => this.host.theme(), size: 18 });
|
|
13718
|
+
this.dialogTips.push(dispose);
|
|
13719
|
+
return el;
|
|
13677
13720
|
}
|
|
13678
13721
|
/** A dropdown over plain string options (value === label) — a thin case of {@link selectPairs}. */
|
|
13679
13722
|
select(id, options, current, emit) {
|
|
@@ -14110,8 +14153,6 @@ function ensureDialogStyles() {
|
|
|
14110
14153
|
${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
|
|
14111
14154
|
.vela-ind-tab{font-weight:600;font-size:13px;line-height:20px;transition:color var(--vela-dur-fast) ease,border-color var(--vela-dur-fast) ease;}
|
|
14112
14155
|
.vela-ind-tab:not(.vela-ind-tab-active):hover{color:var(--vela-fg-bright);}
|
|
14113
|
-
.vela-ind-hint{background:var(--vela-hover);color:var(--vela-fg-muted);transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
|
|
14114
|
-
.vela-ind-hint:hover{background:var(--vela-active);color:var(--vela-fg-bright);}
|
|
14115
14156
|
.vela-ind-ctl,.vela-ind-close{background-color:transparent;color:inherit;transition:color var(--vela-dur-fast) ease,background-color var(--vela-dur-fast) ease;}
|
|
14116
14157
|
.vela-ind-ctl svg,.vela-ind-close svg{width:${LEGEND_ICON_PX}px;height:${LEGEND_ICON_PX}px;display:block;flex:none;stroke-width:1;}
|
|
14117
14158
|
.vela-ind-ctl:hover{background-color:var(--vela-hover-strong);}
|
|
@@ -15502,15 +15543,15 @@ function candleOverrideFor(style, bags) {
|
|
|
15502
15543
|
if (!hasOwnCandlePaint(style)) return null;
|
|
15503
15544
|
const bag = bags[style] ?? {};
|
|
15504
15545
|
const color = (v) => isColor(v) ? v : null;
|
|
15505
|
-
const
|
|
15546
|
+
const bool2 = (v) => isBool(v) ? v : null;
|
|
15506
15547
|
return {
|
|
15507
15548
|
upColor: color(bag.candleUpColor),
|
|
15508
15549
|
downColor: color(bag.candleDownColor),
|
|
15509
|
-
bodyVisible:
|
|
15510
|
-
borderVisible:
|
|
15550
|
+
bodyVisible: bool2(bag.candleBodyVisible),
|
|
15551
|
+
borderVisible: bool2(bag.candleBorderVisible),
|
|
15511
15552
|
borderUpColor: color(bag.candleBorderUpColor),
|
|
15512
15553
|
borderDownColor: color(bag.candleBorderDownColor),
|
|
15513
|
-
wickVisible:
|
|
15554
|
+
wickVisible: bool2(bag.candleWickVisible),
|
|
15514
15555
|
wickUpColor: color(bag.candleWickUpColor),
|
|
15515
15556
|
wickDownColor: color(bag.candleWickDownColor)
|
|
15516
15557
|
};
|
|
@@ -15655,7 +15696,8 @@ function mergeConfig(base, patch) {
|
|
|
15655
15696
|
labelsVisible: isBool(ps.labelsVisible) ? ps.labelsVisible : base.priceScale.labelsVisible,
|
|
15656
15697
|
currentPriceLine: isBool(ps.currentPriceLine) ? ps.currentPriceLine : base.priceScale.currentPriceLine,
|
|
15657
15698
|
priceLabel: isBool(ps.priceLabel) ? ps.priceLabel : base.priceScale.priceLabel,
|
|
15658
|
-
countdown: isBool(ps.countdown) ? ps.countdown : base.priceScale.countdown
|
|
15699
|
+
countdown: isBool(ps.countdown) ? ps.countdown : base.priceScale.countdown,
|
|
15700
|
+
animateLastPrice: isBool(ps.animateLastPrice) ? ps.animateLastPrice : base.priceScale.animateLastPrice
|
|
15659
15701
|
},
|
|
15660
15702
|
panes: {
|
|
15661
15703
|
separatorColor: isColor(panes.separatorColor) ? panes.separatorColor : base.panes.separatorColor
|
|
@@ -17300,7 +17342,7 @@ var InputController = class {
|
|
|
17300
17342
|
if (e.repeat || e.key !== "Shift" && e.key !== "Control" && e.key !== "Meta") return;
|
|
17301
17343
|
if (Number.isNaN(this.cursorX)) return;
|
|
17302
17344
|
if (this.dragging && this.region !== "drawing") return;
|
|
17303
|
-
this.deps.drawingsPointerMove?.(this.cursorX, this.cursorY, this.snapMode(e), e.shiftKey);
|
|
17345
|
+
this.deps.drawingsPointerMove?.(this.cursorX, this.cursorY, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
|
|
17304
17346
|
};
|
|
17305
17347
|
this.onDown = (e) => {
|
|
17306
17348
|
if (e.button === 1) {
|
|
@@ -17332,7 +17374,7 @@ var InputController = class {
|
|
|
17332
17374
|
this.startY = y;
|
|
17333
17375
|
if (this.deps.drawingsClaim?.(x, y)) {
|
|
17334
17376
|
this.region = "drawing";
|
|
17335
|
-
this.deps.drawingsPointerDown?.(x, y, this.snapMode(e), e.shiftKey);
|
|
17377
|
+
this.deps.drawingsPointerDown?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
|
|
17336
17378
|
this.capture(e.pointerId);
|
|
17337
17379
|
return;
|
|
17338
17380
|
}
|
|
@@ -17341,6 +17383,11 @@ var InputController = class {
|
|
|
17341
17383
|
this.capture(e.pointerId);
|
|
17342
17384
|
return;
|
|
17343
17385
|
}
|
|
17386
|
+
if ((e.ctrlKey || e.metaKey) && this.regionAt(x, y) === "data" && this.deps.drawingsMarqueeStart?.(x, y)) {
|
|
17387
|
+
this.region = "drawing";
|
|
17388
|
+
this.capture(e.pointerId);
|
|
17389
|
+
return;
|
|
17390
|
+
}
|
|
17344
17391
|
const vp = this.deps.getCoords().getViewport();
|
|
17345
17392
|
this.deps.apply(vp);
|
|
17346
17393
|
this.startRightOffset = vp.rightOffset;
|
|
@@ -17389,7 +17436,7 @@ var InputController = class {
|
|
|
17389
17436
|
this.deps.apply({ barSpacing, rightOffset: this.startRightOffset });
|
|
17390
17437
|
} else if (this.region === "drawing") {
|
|
17391
17438
|
if (Math.abs(x - this.startX) > slop || Math.abs(y - this.startY) > slop) this.moved = true;
|
|
17392
|
-
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
|
|
17439
|
+
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
|
|
17393
17440
|
} else {
|
|
17394
17441
|
const coords = this.deps.getCoords();
|
|
17395
17442
|
const vp = coords.getViewport();
|
|
@@ -17412,7 +17459,7 @@ var InputController = class {
|
|
|
17412
17459
|
const drawCursor = this.deps.drawingsCursor?.(x, y);
|
|
17413
17460
|
const r = this.regionAt(x, y);
|
|
17414
17461
|
this.el.style.cursor = drawCursor ?? (r === "price" ? "ns-resize" : r === "time" ? "ew-resize" : r === "separator" ? "row-resize" : "");
|
|
17415
|
-
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
|
|
17462
|
+
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
|
|
17416
17463
|
}
|
|
17417
17464
|
if (e.pointerType !== "touch") this.deps.onPointerMove(x, y);
|
|
17418
17465
|
};
|
|
@@ -17753,12 +17800,12 @@ function defaultTradeMarkersState() {
|
|
|
17753
17800
|
function mergeTradeMarkersState(base, patch) {
|
|
17754
17801
|
const p = patch && typeof patch === "object" ? patch : {};
|
|
17755
17802
|
const c = p.colors && typeof p.colors === "object" ? p.colors : {};
|
|
17756
|
-
const
|
|
17803
|
+
const bool2 = (v, fb) => typeof v === "boolean" ? v : fb;
|
|
17757
17804
|
const color = (v, fb) => typeof v === "string" && v.trim().length > 0 ? v : fb;
|
|
17758
17805
|
return {
|
|
17759
|
-
visible:
|
|
17760
|
-
labels:
|
|
17761
|
-
qty:
|
|
17806
|
+
visible: bool2(p.visible, base.visible),
|
|
17807
|
+
labels: bool2(p.labels, base.labels),
|
|
17808
|
+
qty: bool2(p.qty, base.qty),
|
|
17762
17809
|
colors: {
|
|
17763
17810
|
long: color(c.long, base.colors.long),
|
|
17764
17811
|
short: color(c.short, base.colors.short),
|
|
@@ -20519,6 +20566,8 @@ var BUILTIN_SETTINGS_IDS = [
|
|
|
20519
20566
|
"symbol.style.baseline.fill-bottom",
|
|
20520
20567
|
"symbol.style.baseline.base-level",
|
|
20521
20568
|
"symbol.style.baseline.width",
|
|
20569
|
+
"symbol.animation",
|
|
20570
|
+
"symbol.animation.price-changes",
|
|
20522
20571
|
"symbol.timezone",
|
|
20523
20572
|
"scales",
|
|
20524
20573
|
"scales.price-scale",
|
|
@@ -20661,8 +20710,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
20661
20710
|
.vela-sd-mobile .vela-select-trigger,.vela-sd-mobile .vela-num input,.vela-sd-mobile .vela-width-field{height:34px;}
|
|
20662
20711
|
.vela-sd-mobile .vela-sd-close{width:40px;height:40px;}
|
|
20663
20712
|
.vela-sd-mobile .vela-sd-btn{height:38px;}
|
|
20664
|
-
.vela-sd-mobile .vela-sd-row span,.vela-sd-mobile .vela-sd-bool span,.vela-sd-mobile .vela-field-label{white-space:normal !important;}
|
|
20665
|
-
`;
|
|
20713
|
+
.vela-sd-mobile .vela-sd-row span,.vela-sd-mobile .vela-sd-bool span,.vela-sd-mobile .vela-field-label{white-space:normal !important;}`;
|
|
20666
20714
|
if (!existing) document.head.appendChild(st);
|
|
20667
20715
|
}
|
|
20668
20716
|
var SettingsDialog = class {
|
|
@@ -20683,6 +20731,8 @@ var SettingsDialog = class {
|
|
|
20683
20731
|
this.themeControl = null;
|
|
20684
20732
|
/** The built tabs, by title — how `showSection` reaches a pane while the dialog is open. */
|
|
20685
20733
|
this.tabs = [];
|
|
20734
|
+
this.hintTips = [];
|
|
20735
|
+
// ⓘ tooltip disposers of the open build (see hint)
|
|
20686
20736
|
/** Active instance-strip tab per chart type — remembered across dialog rebuilds. */
|
|
20687
20737
|
this.typeActiveInstance = /* @__PURE__ */ new Map();
|
|
20688
20738
|
/** Active TOC group per structured pane (`<typeId>/<pane>` → group label). */
|
|
@@ -20871,6 +20921,13 @@ var SettingsDialog = class {
|
|
|
20871
20921
|
body.append(g);
|
|
20872
20922
|
}
|
|
20873
20923
|
showActive(config.series.style);
|
|
20924
|
+
body.append(sid(this.sectionTitle("Animation"), "symbol.animation"));
|
|
20925
|
+
body.append(sid(this.boolRow(
|
|
20926
|
+
"Animate price changes",
|
|
20927
|
+
config.priceScale.animateLastPrice,
|
|
20928
|
+
(v) => this.emit({ priceScale: { animateLastPrice: v } }),
|
|
20929
|
+
this.hint("Glide the live bar to each new price instead of snapping.")
|
|
20930
|
+
), "symbol.animation.price-changes"));
|
|
20874
20931
|
body.append(sid(this.sectionTitle("Time zone"), "symbol.timezone"));
|
|
20875
20932
|
body.append(sid(this.selectRowLabeled("Time zone", normalizeTimezone(config.timeScale.timezone), timezoneOptions(config.timeScale.timezone), (v) => this.emit({ timeScale: { timezone: v } })), "symbol.timezone"));
|
|
20876
20933
|
const renderHostSections = (placement) => {
|
|
@@ -21078,6 +21135,8 @@ var SettingsDialog = class {
|
|
|
21078
21135
|
this.ui = null;
|
|
21079
21136
|
this.root = null;
|
|
21080
21137
|
this.tabs = [];
|
|
21138
|
+
for (const dispose of this.hintTips) dispose();
|
|
21139
|
+
this.hintTips = [];
|
|
21081
21140
|
ui?.destroy();
|
|
21082
21141
|
}
|
|
21083
21142
|
destroy() {
|
|
@@ -21491,23 +21550,31 @@ var SettingsDialog = class {
|
|
|
21491
21550
|
}).el
|
|
21492
21551
|
});
|
|
21493
21552
|
}
|
|
21494
|
-
/** A toggle row: the checkbox sits to the LEFT of its label (never in the control area).
|
|
21495
|
-
|
|
21496
|
-
|
|
21553
|
+
/** A toggle row: the checkbox sits to the LEFT of its label (never in the control area).
|
|
21554
|
+
* `info` (see {@link hint}) rides after the label. */
|
|
21555
|
+
boolRow(label, value, onChange, info) {
|
|
21556
|
+
return this.toggleRow(label, value, onChange, [], void 0, info);
|
|
21497
21557
|
}
|
|
21498
21558
|
/** An enable row: checkbox + label in the label column, dependent controls in the
|
|
21499
21559
|
* shared control column; the control group dims and ignores input while the toggle
|
|
21500
21560
|
* is off. With `get`, the row re-reads its state on a 'vela-sync' event. */
|
|
21501
|
-
toggleRow(label, value, onToggle, controls, get) {
|
|
21561
|
+
toggleRow(label, value, onToggle, controls, get, info) {
|
|
21502
21562
|
return fieldRow({
|
|
21503
21563
|
label,
|
|
21504
21564
|
labelSize: "sm",
|
|
21505
21565
|
bool: controls.length === 0,
|
|
21506
21566
|
toggle: { checked: value, onChange: onToggle, get },
|
|
21507
21567
|
control: controls,
|
|
21568
|
+
info,
|
|
21508
21569
|
className: controls.length === 0 ? "vela-sd-bool" : "vela-sd-row"
|
|
21509
21570
|
});
|
|
21510
21571
|
}
|
|
21572
|
+
/** The shared ⓘ hint after a row label (see {@link chromeHint}). Disposed with the dialog. */
|
|
21573
|
+
hint(text) {
|
|
21574
|
+
const { el, dispose } = chromeHint(text, { host: this.container, theme: () => this.theme });
|
|
21575
|
+
this.hintTips.push(dispose);
|
|
21576
|
+
return el;
|
|
21577
|
+
}
|
|
21511
21578
|
numberRow(label, value, min, max, step, onChange) {
|
|
21512
21579
|
return fieldRow({
|
|
21513
21580
|
label,
|
|
@@ -21688,6 +21755,18 @@ var DrawingPainter = class {
|
|
|
21688
21755
|
}
|
|
21689
21756
|
this.paintHandles(ctx, pts);
|
|
21690
21757
|
}
|
|
21758
|
+
/** The selection box a Ctrl/Cmd-drag on the empty plot sweeps: a faint fill in the handle
|
|
21759
|
+
* accent under a thin dashed outline. */
|
|
21760
|
+
paintMarquee(ctx, rect) {
|
|
21761
|
+
ctx.save();
|
|
21762
|
+
ctx.fillStyle = withAlpha(HANDLE_BORDER, 0.08);
|
|
21763
|
+
ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
|
|
21764
|
+
ctx.strokeStyle = HANDLE_BORDER;
|
|
21765
|
+
ctx.lineWidth = 1;
|
|
21766
|
+
ctx.setLineDash([4, 3]);
|
|
21767
|
+
ctx.strokeRect(rect.x + 0.5, rect.y + 0.5, rect.w, rect.h);
|
|
21768
|
+
ctx.restore();
|
|
21769
|
+
}
|
|
21691
21770
|
/** A hollow ring marking the candle point the magnet (Ctrl) will snap the next anchor to. */
|
|
21692
21771
|
paintSnapRing(ctx, x, y, theme) {
|
|
21693
21772
|
ctx.setLineDash([]);
|
|
@@ -23156,6 +23235,15 @@ function topDrawingAt(drawings, x, y, proj, tol = HIT_TOLERANCE) {
|
|
|
23156
23235
|
}
|
|
23157
23236
|
return null;
|
|
23158
23237
|
}
|
|
23238
|
+
function deletableSelection(selected, drawings) {
|
|
23239
|
+
const ids = [...selected];
|
|
23240
|
+
if (ids.length < 2) return ids;
|
|
23241
|
+
return ids.filter((id) => !drawings.find((d) => d.id === id)?.locked);
|
|
23242
|
+
}
|
|
23243
|
+
function deleteTargets(hit, selected, drawings, withSelection) {
|
|
23244
|
+
if (withSelection && selected.size >= 2 && selected.has(hit.id)) return deletableSelection(selected, drawings);
|
|
23245
|
+
return hit.locked ? [] : [hit.id];
|
|
23246
|
+
}
|
|
23159
23247
|
|
|
23160
23248
|
// src/renderers/native/drawings/DrawingInteraction.ts
|
|
23161
23249
|
var DRAG_SLOP2 = 3;
|
|
@@ -23259,10 +23347,32 @@ var DrawingInteraction = class {
|
|
|
23259
23347
|
activeDragId() {
|
|
23260
23348
|
return this.state.kind === "pressed" && this.state.moved ? this.state.id : null;
|
|
23261
23349
|
}
|
|
23350
|
+
/** The drawing under a press that has not been released yet (drag or click undecided), or null. */
|
|
23351
|
+
pressedId() {
|
|
23352
|
+
return this.state.kind === "pressed" ? this.state.id : null;
|
|
23353
|
+
}
|
|
23354
|
+
/** The copies a Ctrl-drag is moving (transient — not yet in the store), or null. */
|
|
23355
|
+
dragClones() {
|
|
23356
|
+
return this.state.kind === "pressed" && this.state.moved && this.state.clones ? this.state.clones.map((c) => c.d) : null;
|
|
23357
|
+
}
|
|
23358
|
+
/** The marquee box being swept, normalized (or null when none is in progress). */
|
|
23359
|
+
marqueeRect() {
|
|
23360
|
+
if (this.state.kind !== "marquee") return null;
|
|
23361
|
+
const { x0, y0, x1, y1 } = this.state;
|
|
23362
|
+
return { x: Math.min(x0, x1), y: Math.min(y0, y1), w: Math.abs(x1 - x0), h: Math.abs(y1 - y0) };
|
|
23363
|
+
}
|
|
23364
|
+
/** Ctrl/Cmd-press on the empty plot: start sweeping a selection box. False when a tool is
|
|
23365
|
+
* armed or a gesture is in flight (that path owns the press). */
|
|
23366
|
+
beginMarquee(x, y) {
|
|
23367
|
+
if (this.state.kind !== "idle" || this.deps.activeTool() != null) return false;
|
|
23368
|
+
this.state = { kind: "marquee", x0: x, y0: y, x1: x, y1: y };
|
|
23369
|
+
return true;
|
|
23370
|
+
}
|
|
23262
23371
|
/** A press: place the next anchor, or grab a drawing/handle (resolved as click or drag on release).
|
|
23263
23372
|
* `shift` over a drawing toggles it in/out of the selection (multi-select) instead of dragging;
|
|
23264
|
-
* while placing a line tool it locks the segment angle to 45° steps.
|
|
23265
|
-
|
|
23373
|
+
* while placing a line tool it locks the segment angle to 45° steps. `mod` (Ctrl/Cmd) over a
|
|
23374
|
+
* drawing also toggles on a click, but a body drag then moves a COPY. */
|
|
23375
|
+
down(x, y, mode = "off", shift2 = false, mod = false) {
|
|
23266
23376
|
const proj = this.deps.projector();
|
|
23267
23377
|
if (this.state.kind === "placing") {
|
|
23268
23378
|
if (this.state.draft.placementMode() !== "click") return;
|
|
@@ -23290,7 +23400,50 @@ var DrawingInteraction = class {
|
|
|
23290
23400
|
return;
|
|
23291
23401
|
}
|
|
23292
23402
|
const handle = hit.hitHandle(x, y, proj, HIT_TOLERANCE);
|
|
23293
|
-
|
|
23403
|
+
const riders = [];
|
|
23404
|
+
if (handle < 0 && this.deps.selectedIds().has(hit.id)) {
|
|
23405
|
+
for (const id of this.deps.selectedIds()) {
|
|
23406
|
+
const d = id === hit.id ? null : this.byId(id);
|
|
23407
|
+
if (d && !d.locked && d.visible) riders.push({ id, orig: cloneAnchors(d) });
|
|
23408
|
+
}
|
|
23409
|
+
}
|
|
23410
|
+
this.state = { kind: "pressed", id: hit.id, handle, orig: cloneAnchors(hit), px: x, py: y, moved: false, riders, mod, clones: null };
|
|
23411
|
+
}
|
|
23412
|
+
}
|
|
23413
|
+
/** The data-space delta a body drag from the press to (x, y) means on `paneId` — resolved per
|
|
23414
|
+
* pane so drawings riding along on another pane move by the same PIXELS, not the same price. */
|
|
23415
|
+
bodyDelta(paneId, x, y) {
|
|
23416
|
+
if (this.state.kind !== "pressed") return { dt: 0, dp: 0 };
|
|
23417
|
+
const proj = this.deps.projector();
|
|
23418
|
+
const from = proj.pxToPoint(this.state.px, this.state.py, paneId);
|
|
23419
|
+
const to = proj.pxToPoint(x, y, paneId);
|
|
23420
|
+
return { dt: to.time - from.time, dp: to.price - from.price };
|
|
23421
|
+
}
|
|
23422
|
+
/** The pressed drawing and its riders, each with its anchors at the press. */
|
|
23423
|
+
dragSources(d) {
|
|
23424
|
+
if (this.state.kind !== "pressed") return [];
|
|
23425
|
+
const out = [{ d, orig: this.state.orig }];
|
|
23426
|
+
for (const r of this.state.riders) {
|
|
23427
|
+
const rd = this.byId(r.id);
|
|
23428
|
+
if (rd) out.push({ d: rd, orig: r.orig });
|
|
23429
|
+
}
|
|
23430
|
+
return out;
|
|
23431
|
+
}
|
|
23432
|
+
/** Move the pressed drawing's body (and its riders) — or, on a Ctrl-drag, their copies — to the
|
|
23433
|
+
* cursor. The copies are built on the first move (so a Ctrl-click never creates anything), from
|
|
23434
|
+
* sources that have not moved yet, so their own anchors are the origin to translate from. */
|
|
23435
|
+
dragBody(d, x, y) {
|
|
23436
|
+
if (this.state.kind !== "pressed") return;
|
|
23437
|
+
this.snapAt = null;
|
|
23438
|
+
if (this.state.mod && !this.state.clones) {
|
|
23439
|
+
this.state.clones = this.dragSources(d).flatMap((s) => {
|
|
23440
|
+
const c = deserializeDrawing(s.d.serialize());
|
|
23441
|
+
return c ? [{ d: c, orig: cloneAnchors(c) }] : [];
|
|
23442
|
+
});
|
|
23443
|
+
}
|
|
23444
|
+
for (const t of this.state.clones ?? this.dragSources(d)) {
|
|
23445
|
+
const { dt, dp } = this.bodyDelta(t.d.paneId, x, y);
|
|
23446
|
+
t.d.anchors = t.d.translateBody(dt, dp, t.orig);
|
|
23294
23447
|
}
|
|
23295
23448
|
}
|
|
23296
23449
|
/** Pointer move: advance the placing ghost, or live-preview a drag once past the slop.
|
|
@@ -23314,12 +23467,20 @@ var DrawingInteraction = class {
|
|
|
23314
23467
|
this.deps.changed();
|
|
23315
23468
|
return;
|
|
23316
23469
|
}
|
|
23470
|
+
if (this.state.kind === "marquee") {
|
|
23471
|
+
this.state.x1 = x;
|
|
23472
|
+
this.state.y1 = y;
|
|
23473
|
+
this.deps.changed();
|
|
23474
|
+
return;
|
|
23475
|
+
}
|
|
23317
23476
|
if (this.state.kind !== "pressed") return;
|
|
23318
23477
|
const d = this.byId(this.state.id);
|
|
23319
23478
|
if (!d || d.locked) return;
|
|
23320
23479
|
if (!this.state.moved && Math.abs(x - this.state.px) <= DRAG_SLOP2 && Math.abs(y - this.state.py) <= DRAG_SLOP2) return;
|
|
23321
23480
|
this.state.moved = true;
|
|
23322
|
-
if (this.state.handle
|
|
23481
|
+
if (this.state.handle < 0) {
|
|
23482
|
+
this.dragBody(d, x, y);
|
|
23483
|
+
} else {
|
|
23323
23484
|
const a = d.anchors[this.state.handle];
|
|
23324
23485
|
if (a) {
|
|
23325
23486
|
const pivot = shift2 && ANGLE_SNAP_TYPES.has(d.type) && d.anchors.length === 2 ? d.anchors[1 - this.state.handle] : void 0;
|
|
@@ -23327,12 +23488,6 @@ var DrawingInteraction = class {
|
|
|
23327
23488
|
applyFree(a, point, d.anchorSchema().slots[this.state.handle]?.free ?? "both");
|
|
23328
23489
|
}
|
|
23329
23490
|
d.constrainHandleDrag(this.state.handle);
|
|
23330
|
-
} else {
|
|
23331
|
-
const pt = this.deps.projector().pxToPoint(x, y, d.paneId);
|
|
23332
|
-
this.snapAt = null;
|
|
23333
|
-
const dt = pt.time - this.state.grab.time;
|
|
23334
|
-
const dp = pt.price - this.state.grab.price;
|
|
23335
|
-
d.anchors = d.translateBody(dt, dp, this.state.orig);
|
|
23336
23491
|
}
|
|
23337
23492
|
this.deps.changed();
|
|
23338
23493
|
}
|
|
@@ -23352,16 +23507,46 @@ var DrawingInteraction = class {
|
|
|
23352
23507
|
}
|
|
23353
23508
|
return;
|
|
23354
23509
|
}
|
|
23510
|
+
if (this.state.kind === "marquee") {
|
|
23511
|
+
const rect = this.marqueeRect();
|
|
23512
|
+
this.state = { kind: "idle" };
|
|
23513
|
+
if (rect.w > DRAG_SLOP2 || rect.h > DRAG_SLOP2) this.selectWithin(rect);
|
|
23514
|
+
this.deps.changed();
|
|
23515
|
+
return;
|
|
23516
|
+
}
|
|
23355
23517
|
if (this.state.kind !== "pressed") return;
|
|
23356
|
-
const { id, moved } = this.state;
|
|
23518
|
+
const { id, moved, riders, mod, clones } = this.state;
|
|
23357
23519
|
this.state = { kind: "idle" };
|
|
23358
23520
|
const d = this.byId(id);
|
|
23359
|
-
if (moved) {
|
|
23360
|
-
if (
|
|
23361
|
-
this.deps.
|
|
23362
|
-
|
|
23363
|
-
|
|
23521
|
+
if (!moved) {
|
|
23522
|
+
if (mod) this.deps.emit({ kind: "select", ids: [id], additive: true });
|
|
23523
|
+
else this.deps.openSettings(id, x, y);
|
|
23524
|
+
return;
|
|
23525
|
+
}
|
|
23526
|
+
if (clones) {
|
|
23527
|
+
this.deps.emit({ kind: "clone", docs: clones.map((c) => c.d.serialize()) });
|
|
23528
|
+
} else if (d) {
|
|
23529
|
+
const docs = [d.serialize()];
|
|
23530
|
+
for (const r of riders) {
|
|
23531
|
+
const rd = this.byId(r.id);
|
|
23532
|
+
if (rd) docs.push(rd.serialize());
|
|
23533
|
+
}
|
|
23534
|
+
if (docs.length > 1) this.deps.emit({ kind: "edit-many", docs });
|
|
23535
|
+
else this.deps.emit({ kind: "edit", doc: docs[0] });
|
|
23364
23536
|
}
|
|
23537
|
+
this.deps.changed();
|
|
23538
|
+
}
|
|
23539
|
+
/** Add every visible drawing whose on-screen bounds touch `rect` to the selection (a union,
|
|
23540
|
+
* so successive sweeps accumulate). No change when the box touches nothing new. */
|
|
23541
|
+
selectWithin(rect) {
|
|
23542
|
+
const proj = this.deps.projector();
|
|
23543
|
+
const ids = [...this.deps.selectedIds()];
|
|
23544
|
+
for (const d of this.deps.drawings()) {
|
|
23545
|
+
if (!d.visible || ids.includes(d.id)) continue;
|
|
23546
|
+
const b = d.bounds(proj);
|
|
23547
|
+
if (b && rectsIntersect(rect, b)) ids.push(d.id);
|
|
23548
|
+
}
|
|
23549
|
+
if (ids.length > this.deps.selectedIds().size) this.deps.emit({ kind: "select", ids });
|
|
23365
23550
|
}
|
|
23366
23551
|
/** Cancel an in-progress placement or drag (Escape). Returns whether anything was cancelled. */
|
|
23367
23552
|
cancel() {
|
|
@@ -23374,8 +23559,15 @@ var DrawingInteraction = class {
|
|
|
23374
23559
|
return true;
|
|
23375
23560
|
}
|
|
23376
23561
|
if (this.state.kind === "pressed") {
|
|
23377
|
-
|
|
23378
|
-
|
|
23562
|
+
if (this.state.moved && !this.state.clones) {
|
|
23563
|
+
const d = this.byId(this.state.id);
|
|
23564
|
+
if (d) for (const s of this.dragSources(d)) s.d.anchors = s.orig.map((o) => ({ time: o.time, price: o.price }));
|
|
23565
|
+
}
|
|
23566
|
+
this.state = { kind: "idle" };
|
|
23567
|
+
this.deps.changed();
|
|
23568
|
+
return true;
|
|
23569
|
+
}
|
|
23570
|
+
if (this.state.kind === "marquee") {
|
|
23379
23571
|
this.state = { kind: "idle" };
|
|
23380
23572
|
this.deps.changed();
|
|
23381
23573
|
return true;
|
|
@@ -23490,6 +23682,9 @@ var DrawingInteraction = class {
|
|
|
23490
23682
|
function cloneAnchors(d) {
|
|
23491
23683
|
return d.anchors.map((a) => ({ time: a.time, price: a.price }));
|
|
23492
23684
|
}
|
|
23685
|
+
function rectsIntersect(a, b) {
|
|
23686
|
+
return a.x <= b.x + b.w && b.x <= a.x + a.w && a.y <= b.y + b.h && b.y <= a.y + a.h;
|
|
23687
|
+
}
|
|
23493
23688
|
function applyFree(anchor, pt, free) {
|
|
23494
23689
|
if (free === "both" || free === "x") anchor.time = pt.time;
|
|
23495
23690
|
if (free === "both" || free === "y") anchor.price = pt.price;
|
|
@@ -23875,10 +24070,27 @@ function btn(label, primary, onClick) {
|
|
|
23875
24070
|
}
|
|
23876
24071
|
|
|
23877
24072
|
// src/renderers/native/drawings/DrawingSettingsPopup.ts
|
|
24073
|
+
var MIXED = /* @__PURE__ */ Symbol("mixed");
|
|
24074
|
+
function sharedPaths(drawings) {
|
|
24075
|
+
const [first, ...rest] = drawings;
|
|
24076
|
+
const paths = new Set(first?.schema().fields.map((f) => f.path) ?? []);
|
|
24077
|
+
for (const d of rest) {
|
|
24078
|
+
const own = new Set(d.schema().fields.map((f) => f.path));
|
|
24079
|
+
for (const p of paths) if (!own.has(p)) paths.delete(p);
|
|
24080
|
+
}
|
|
24081
|
+
return paths;
|
|
24082
|
+
}
|
|
24083
|
+
function commonValue(drawings, read) {
|
|
24084
|
+
const first = read(drawings[0]);
|
|
24085
|
+
return drawings.every((d) => read(d) === first) ? first : MIXED;
|
|
24086
|
+
}
|
|
24087
|
+
function distinctValues(drawings, read) {
|
|
24088
|
+
return [...new Set(drawings.map(read))];
|
|
24089
|
+
}
|
|
23878
24090
|
var BTN = 30;
|
|
23879
24091
|
var ICON = 21;
|
|
23880
24092
|
var STYLE_ID3 = "vela-drawing-popup-styles";
|
|
23881
|
-
var STYLE_REV = "
|
|
24093
|
+
var STYLE_REV = "4";
|
|
23882
24094
|
function ensureStyles3() {
|
|
23883
24095
|
if (typeof document === "undefined") return;
|
|
23884
24096
|
const existing = document.getElementById(STYLE_ID3);
|
|
@@ -23890,6 +24102,8 @@ function ensureStyles3() {
|
|
|
23890
24102
|
.vela-dpop-btn{background:transparent;color:var(--vela-fg-muted);transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
|
|
23891
24103
|
.vela-dpop-btn:hover{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
|
|
23892
24104
|
.vela-dpop-btn[data-active='1']{background:var(--vela-active);color:var(--vela-fg-bright);}
|
|
24105
|
+
/* Mixed (a multi-selection disagrees): the active fill at half strength behind a dashed ring. */
|
|
24106
|
+
.vela-dpop-btn[data-active='mixed']{background:var(--vela-hover-strong);color:var(--vela-fg-bright);outline:1px dashed var(--vela-fg-muted);outline-offset:-2px;}
|
|
23893
24107
|
.vela-dpop-item{background:transparent;transition:background var(--vela-dur-fast) ease;}
|
|
23894
24108
|
.vela-dpop-item:hover{background:var(--vela-hover-strong);}
|
|
23895
24109
|
.vela-dpop-item[data-active='1']{background:var(--vela-active);}
|
|
@@ -23921,7 +24135,7 @@ var DrawingSettingsPopup = class {
|
|
|
23921
24135
|
if (this.isOwnChrome(node)) return;
|
|
23922
24136
|
const cb = this.onClose;
|
|
23923
24137
|
this.close();
|
|
23924
|
-
cb?.();
|
|
24138
|
+
cb?.(e);
|
|
23925
24139
|
};
|
|
23926
24140
|
this.theme = theme;
|
|
23927
24141
|
this.settingsDialog = new DrawingSettingsDialog(host, theme);
|
|
@@ -23946,16 +24160,27 @@ var DrawingSettingsPopup = class {
|
|
|
23946
24160
|
contains(node) {
|
|
23947
24161
|
return node != null && (this.isOwnChrome(node) || this.settingsDialog.contains(node));
|
|
23948
24162
|
}
|
|
23949
|
-
/** Open the quick toolbar for `
|
|
23950
|
-
* `
|
|
23951
|
-
|
|
24163
|
+
/** Open the quick toolbar for `drawings` (one, or a whole multi-selection), floating clear of
|
|
24164
|
+
* their `anchor` box. With several drawings the bar shows only the controls they ALL support,
|
|
24165
|
+
* a control whose values differ reads as mixed, and every edit applies to all of them —
|
|
24166
|
+
* per-drawing panels (levels, position sizing, the text field) stay single-drawing only.
|
|
24167
|
+
* `onClose` fires on a dismissal (an outside press, or Escape in the text field — not a
|
|
24168
|
+
* programmatic close), with the dismissing press when there is one, so the caller can tell a
|
|
24169
|
+
* modifier press (multi-select) from a plain one. */
|
|
24170
|
+
open(drawings, anchor, actions, onClose) {
|
|
23952
24171
|
this.close();
|
|
24172
|
+
const drawing = drawings[0];
|
|
24173
|
+
if (!drawing) return;
|
|
23953
24174
|
ensureStyles3();
|
|
23954
24175
|
this.onClose = onClose ?? null;
|
|
23955
24176
|
const t = this.theme;
|
|
24177
|
+
const multi = drawings.length > 1;
|
|
23956
24178
|
const schema = drawing.schema();
|
|
23957
|
-
const paths =
|
|
23958
|
-
const
|
|
24179
|
+
const paths = sharedPaths(drawings);
|
|
24180
|
+
const common = (read) => commonValue(drawings, read);
|
|
24181
|
+
const every = (pred) => drawings.every(pred);
|
|
24182
|
+
const swatch = (tip, glyph, read, path) => this.colorButton(tip, glyph, common(read), (v) => actions.patch({ [path]: v }), void 0, distinctValues(drawings, read));
|
|
24183
|
+
const textOnBar = multi || schema.textIsContent === true || !paths.has("text.value");
|
|
23959
24184
|
const el = document.createElement("div");
|
|
23960
24185
|
el.className = "vela-dpop";
|
|
23961
24186
|
el.style.cssText = `position:absolute;z-index:22;background:${t.background};border:1px solid var(--vela-border);border-radius:var(--vela-radius-lg);box-shadow:var(--vela-shadow);color:${t.textColor};font:var(--vela-font-size-md) ${t.fontFamily};display:flex;flex-direction:column;pointer-events:auto;overflow:hidden;`;
|
|
@@ -23969,75 +24194,84 @@ var DrawingSettingsPopup = class {
|
|
|
23969
24194
|
bar.style.cssText = "display:flex;align-items:center;gap:2px;padding:4px;";
|
|
23970
24195
|
bar.appendChild(this.dragHandle());
|
|
23971
24196
|
if (paths.has("glyph")) {
|
|
23972
|
-
const cur =
|
|
24197
|
+
const cur = common((d) => d.glyph ?? GLYPH_OPTIONS[0]);
|
|
23973
24198
|
bar.appendChild(this.dropdown("Icon", GLYPH_OPTIONS, cur, (g) => glyphIcon(g), (v) => actions.patch({ glyph: v })));
|
|
23974
24199
|
}
|
|
23975
24200
|
if (paths.has("size")) {
|
|
23976
|
-
const sz =
|
|
24201
|
+
const sz = common((d) => d.size ?? "normal");
|
|
23977
24202
|
bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label: sizeLabel }));
|
|
23978
24203
|
}
|
|
23979
|
-
if (paths.has("magnifier.timeframe") &&
|
|
24204
|
+
if (paths.has("magnifier.timeframe") && every((d) => d instanceof Magnifier)) {
|
|
24205
|
+
const mag = (d) => d;
|
|
23980
24206
|
const options = this.lowerTimeframeOptions();
|
|
23981
24207
|
if (options.length > 0) {
|
|
23982
24208
|
bar.appendChild(
|
|
23983
|
-
this.dropdown("Lower timeframe", options.map((o) => o.value),
|
|
24209
|
+
this.dropdown("Lower timeframe", options.map((o) => o.value), common((d) => mag(d).magnifier.timeframe), () => "", (v) => actions.patch({ "magnifier.timeframe": v }), {
|
|
23984
24210
|
label: (v) => magnifierTimeframeLabel(String(v)),
|
|
23985
24211
|
labelInTrigger: true
|
|
23986
24212
|
})
|
|
23987
24213
|
);
|
|
23988
24214
|
}
|
|
23989
|
-
bar.appendChild(
|
|
23990
|
-
bar.appendChild(
|
|
24215
|
+
bar.appendChild(swatch("Up candles", BUCKET_ICON, (d) => mag(d).magnifier.upColor || t.upColor, "magnifier.upColor"));
|
|
24216
|
+
bar.appendChild(swatch("Down candles", BUCKET_ICON, (d) => mag(d).magnifier.downColor || t.downColor, "magnifier.downColor"));
|
|
23991
24217
|
}
|
|
23992
|
-
if (paths.has("style.lineColor")) bar.appendChild(
|
|
24218
|
+
if (paths.has("style.lineColor")) bar.appendChild(swatch("Line color", BRUSH_ICON, (d) => d.style.lineColor || (d instanceof Magnifier ? contrastColor(this.theme.background) : DEFAULT_DRAWING_COLOR), "style.lineColor"));
|
|
23993
24219
|
if (paths.has("style.lineWidth")) {
|
|
23994
|
-
const
|
|
23995
|
-
|
|
23996
|
-
|
|
24220
|
+
const widthField2 = (d) => d.schema().fields.find((f) => f.path === "style.lineWidth");
|
|
24221
|
+
const wf = widthField2(drawing);
|
|
24222
|
+
const width = common((d) => d.style.lineWidth);
|
|
24223
|
+
const markers = every((d) => {
|
|
24224
|
+
const f = widthField2(d);
|
|
24225
|
+
return f?.kind === "number" && (f.min ?? 1) > 1;
|
|
24226
|
+
});
|
|
24227
|
+
if (markers && wf?.kind === "number") {
|
|
24228
|
+
bar.appendChild(this.widthInput("Line width", width, wf.min ?? 1, wf.max ?? 60, wf.step ?? 1, (v) => actions.patch({ "style.lineWidth": v })));
|
|
23997
24229
|
} else {
|
|
23998
|
-
bar.appendChild(this.dropdown("Line width", [1, 2, 3, 4],
|
|
24230
|
+
bar.appendChild(this.dropdown("Line width", [1, 2, 3, 4], width, (w) => lineIcon(w, "solid"), (v) => actions.patch({ "style.lineWidth": v }), { label: (v) => `${v}px`, labelInTrigger: true }));
|
|
23999
24231
|
}
|
|
24000
24232
|
}
|
|
24001
|
-
if (paths.has("style.lineStyle")) bar.appendChild(this.dropdown("Line style", LINE_STYLE_OPTIONS.map((o) => o.value),
|
|
24002
|
-
if (paths.has("style.fillColor")) bar.appendChild(
|
|
24233
|
+
if (paths.has("style.lineStyle")) bar.appendChild(this.dropdown("Line style", LINE_STYLE_OPTIONS.map((o) => o.value), common((d) => d.style.lineStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "style.lineStyle": v }), { label: styleLabel2 }));
|
|
24234
|
+
if (paths.has("style.fillColor")) bar.appendChild(swatch("Fill", BUCKET_ICON, (d) => effectiveFillColor(d, this.theme) ?? d.style.fillColor ?? DEFAULT_DRAWING_COLOR, "style.fillColor"));
|
|
24003
24235
|
const isFrvp = paths.has("frvp.rows") && drawing instanceof FixedRangeVolumeProfile;
|
|
24004
|
-
const isPosition = paths.has("riskPercent") &&
|
|
24236
|
+
const isPosition = paths.has("riskPercent") && every((d) => d instanceof PositionTool);
|
|
24005
24237
|
if (isPosition) {
|
|
24006
|
-
const pos =
|
|
24007
|
-
bar.appendChild(
|
|
24008
|
-
bar.appendChild(
|
|
24238
|
+
const pos = (d) => d;
|
|
24239
|
+
bar.appendChild(swatch("Profit zone", BUCKET_ICON, (d) => pos(d).profitColor, "profitColor"));
|
|
24240
|
+
bar.appendChild(swatch("Loss zone", BUCKET_ICON, (d) => pos(d).lossColor, "lossColor"));
|
|
24009
24241
|
}
|
|
24010
|
-
const
|
|
24011
|
-
if (paths.has("reg.midColor") &&
|
|
24242
|
+
const regOf = (d) => d.reg;
|
|
24243
|
+
if (paths.has("reg.midColor") && every((d) => regOf(d) != null)) {
|
|
24244
|
+
const reg = (d) => regOf(d);
|
|
24012
24245
|
const styles = LINE_STYLE_OPTIONS.map((o) => o.value);
|
|
24013
|
-
bar.appendChild(
|
|
24014
|
-
bar.appendChild(this.dropdown("Midline style", styles, reg.midStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "reg.midStyle": v }), { label: styleLabel2 }));
|
|
24015
|
-
bar.appendChild(
|
|
24016
|
-
bar.appendChild(this.dropdown("Upper line style", styles, reg.upperStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "reg.upperStyle": v }), { label: styleLabel2 }));
|
|
24017
|
-
bar.appendChild(
|
|
24018
|
-
bar.appendChild(this.dropdown("Lower line style", styles, reg.lowerStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "reg.lowerStyle": v }), { label: styleLabel2 }));
|
|
24019
|
-
bar.appendChild(
|
|
24020
|
-
bar.appendChild(
|
|
24021
|
-
bar.appendChild(this.toggle("Show R\xB2", R2_ICON, reg.showR2, (v) => actions.patch({ "reg.showR2": v })));
|
|
24022
|
-
}
|
|
24023
|
-
const
|
|
24024
|
-
if (paths.has("vwap.midColor") &&
|
|
24246
|
+
bar.appendChild(swatch("Midline color", BRUSH_ICON, (d) => reg(d).midColor, "reg.midColor"));
|
|
24247
|
+
bar.appendChild(this.dropdown("Midline style", styles, common((d) => reg(d).midStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "reg.midStyle": v }), { label: styleLabel2 }));
|
|
24248
|
+
bar.appendChild(swatch("Upper line color", BRUSH_ICON, (d) => reg(d).upperColor, "reg.upperColor"));
|
|
24249
|
+
bar.appendChild(this.dropdown("Upper line style", styles, common((d) => reg(d).upperStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "reg.upperStyle": v }), { label: styleLabel2 }));
|
|
24250
|
+
bar.appendChild(swatch("Lower line color", BRUSH_ICON, (d) => reg(d).lowerColor, "reg.lowerColor"));
|
|
24251
|
+
bar.appendChild(this.dropdown("Lower line style", styles, common((d) => reg(d).lowerStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "reg.lowerStyle": v }), { label: styleLabel2 }));
|
|
24252
|
+
bar.appendChild(swatch("Upper fill", BUCKET_ICON, (d) => reg(d).upperFill, "reg.upperFill"));
|
|
24253
|
+
bar.appendChild(swatch("Lower fill", BUCKET_ICON, (d) => reg(d).lowerFill, "reg.lowerFill"));
|
|
24254
|
+
bar.appendChild(this.toggle("Show R\xB2", R2_ICON, common((d) => reg(d).showR2), (v) => actions.patch({ "reg.showR2": v })));
|
|
24255
|
+
}
|
|
24256
|
+
const vwapOf = (d) => d.vwap;
|
|
24257
|
+
if (paths.has("vwap.midColor") && every((d) => vwapOf(d) != null)) {
|
|
24258
|
+
const vwap2 = (d) => vwapOf(d);
|
|
24025
24259
|
const styles = LINE_STYLE_OPTIONS.map((o) => o.value);
|
|
24026
24260
|
const MULTS = [0.5, 1, 1.5, 2, 2.5, 3, 4, 5];
|
|
24027
|
-
bar.appendChild(
|
|
24028
|
-
bar.appendChild(this.dropdown("VWAP style", styles, vwap2.midStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "vwap.midStyle": v }), { label: styleLabel2 }));
|
|
24261
|
+
bar.appendChild(swatch("VWAP color", BRUSH_ICON, (d) => vwap2(d).midColor, "vwap.midColor"));
|
|
24262
|
+
bar.appendChild(this.dropdown("VWAP style", styles, common((d) => vwap2(d).midStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "vwap.midStyle": v }), { label: styleLabel2 }));
|
|
24029
24263
|
bar.appendChild(
|
|
24030
|
-
this.dropdown("Band multiplier", MULTS, vwap2.multiplier, () => BANDS_ICON, (v) => actions.patch({ "vwap.multiplier": v }), {
|
|
24264
|
+
this.dropdown("Band multiplier", MULTS, common((d) => vwap2(d).multiplier), () => BANDS_ICON, (v) => actions.patch({ "vwap.multiplier": v }), {
|
|
24031
24265
|
label: (v) => `${v}\u03C3`,
|
|
24032
24266
|
labelInTrigger: true
|
|
24033
24267
|
})
|
|
24034
24268
|
);
|
|
24035
|
-
bar.appendChild(
|
|
24036
|
-
bar.appendChild(
|
|
24037
|
-
bar.appendChild(
|
|
24269
|
+
bar.appendChild(swatch("Upper band color", BRUSH_ICON, (d) => vwap2(d).upperColor, "vwap.upperColor"));
|
|
24270
|
+
bar.appendChild(swatch("Lower band color", BRUSH_ICON, (d) => vwap2(d).lowerColor, "vwap.lowerColor"));
|
|
24271
|
+
bar.appendChild(swatch("Band fill", BUCKET_ICON, (d) => vwap2(d).bandFill, "vwap.bandFill"));
|
|
24038
24272
|
}
|
|
24039
24273
|
if (paths.has("maxCurvature")) {
|
|
24040
|
-
const cur =
|
|
24274
|
+
const cur = common((d) => d.maxCurvature ?? 24);
|
|
24041
24275
|
bar.appendChild(
|
|
24042
24276
|
this.dropdown("Max curvature", DEDEKIND_CURVATURE_OPTIONS, cur, () => DEDEKIND_ICON2, (v) => actions.patch({ maxCurvature: v }), {
|
|
24043
24277
|
label: (v) => `n=${v}`,
|
|
@@ -24046,7 +24280,7 @@ var DrawingSettingsPopup = class {
|
|
|
24046
24280
|
);
|
|
24047
24281
|
}
|
|
24048
24282
|
if (paths.has("mach")) {
|
|
24049
|
-
const cur =
|
|
24283
|
+
const cur = common((d) => d.mach ?? 2);
|
|
24050
24284
|
bar.appendChild(
|
|
24051
24285
|
this.dropdown("Mach number", MACH_NUMBER_OPTIONS, cur, () => SUPERSONIC_ICON2, (v) => actions.patch({ mach: v }), {
|
|
24052
24286
|
label: (v) => `M=${v}`,
|
|
@@ -24055,7 +24289,7 @@ var DrawingSettingsPopup = class {
|
|
|
24055
24289
|
);
|
|
24056
24290
|
}
|
|
24057
24291
|
if (paths.has("waveCount")) {
|
|
24058
|
-
const cur =
|
|
24292
|
+
const cur = common((d) => d.waveCount ?? 6);
|
|
24059
24293
|
bar.appendChild(
|
|
24060
24294
|
this.dropdown("Waves", MACH_WAVE_COUNT_OPTIONS, cur, () => SONIC_ICON2, (v) => actions.patch({ waveCount: v }), {
|
|
24061
24295
|
label: (v) => `${v}`,
|
|
@@ -24063,26 +24297,27 @@ var DrawingSettingsPopup = class {
|
|
|
24063
24297
|
})
|
|
24064
24298
|
);
|
|
24065
24299
|
}
|
|
24066
|
-
const
|
|
24067
|
-
if (paths.has("showPrice")) bar.appendChild(this.toggle("Show price", PRICE_DELTA_ICON,
|
|
24068
|
-
if (paths.has("showDate")) bar.appendChild(this.toggle("Show date", DATE_DELTA_ICON,
|
|
24069
|
-
if (paths.has("text.color") && textOnBar) bar.appendChild(
|
|
24070
|
-
if (paths.has("text.size") && textOnBar) bar.appendChild(this.dropdown("Text size", TEXT_SIZE_OPTIONS.map((o) => o.value),
|
|
24071
|
-
|
|
24072
|
-
if (paths.has("text.
|
|
24073
|
-
if (paths.has("text.
|
|
24300
|
+
const flags = (d) => d;
|
|
24301
|
+
if (paths.has("showPrice")) bar.appendChild(this.toggle("Show price", PRICE_DELTA_ICON, common((d) => flags(d).showPrice !== false), (v) => actions.patch({ showPrice: v })));
|
|
24302
|
+
if (paths.has("showDate")) bar.appendChild(this.toggle("Show date", DATE_DELTA_ICON, common((d) => flags(d).showDate !== false), (v) => actions.patch({ showDate: v })));
|
|
24303
|
+
if (paths.has("text.color") && textOnBar) bar.appendChild(swatch("Text color", TYPE_ICON, (d) => d.text?.color || t.textColor, "text.color"));
|
|
24304
|
+
if (paths.has("text.size") && textOnBar) bar.appendChild(this.dropdown("Text size", TEXT_SIZE_OPTIONS.map((o) => o.value), common((d) => d.text?.size ?? "normal"), (s) => labelSizeIcon(s), (v) => actions.patch({ "text.size": v }), { label: sizeLabel }));
|
|
24305
|
+
const textField = !multi && paths.has("text.value");
|
|
24306
|
+
if (paths.has("text.bold") && !textField) bar.appendChild(this.toggle("Bold", BOLD_ICON, common((d) => !!d.text?.bold), (v) => actions.patch({ "text.bold": v })));
|
|
24307
|
+
if (paths.has("text.italic") && !textField) bar.appendChild(this.toggle("Italic", ITALIC_ICON, common((d) => !!d.text?.italic), (v) => actions.patch({ "text.italic": v })));
|
|
24308
|
+
if (textField) bar.appendChild(this.iconBtn("Text", TYPE_ICON, () => this.toggleTextPanel(drawing, actions, !textOnBar)));
|
|
24074
24309
|
const editableLevels = drawing.editableLevels();
|
|
24075
|
-
if (editableLevels && !(
|
|
24076
|
-
const fib =
|
|
24310
|
+
if (editableLevels && every((d) => d.editableLevels() != null && !(d instanceof MachFigure))) {
|
|
24311
|
+
const fib = (d) => d;
|
|
24077
24312
|
const sizes = TEXT_SIZE_OPTIONS.map((o) => o.value);
|
|
24078
|
-
bar.appendChild(this.dropdown("Numbers size", sizes, fib.numbersSize ?? "small", (s) => numbersSizeIcon(s), (v) => actions.patch({ numbersSize: v }), { label: sizeLabel }));
|
|
24079
|
-
bar.appendChild(this.dropdown("Label size", sizes, fib.labelsSize ?? "normal", (s) => labelSizeIcon(s), (v) => actions.patch({ labelsSize: v }), { label: sizeLabel }));
|
|
24313
|
+
bar.appendChild(this.dropdown("Numbers size", sizes, common((d) => fib(d).numbersSize ?? "small"), (s) => numbersSizeIcon(s), (v) => actions.patch({ numbersSize: v }), { label: sizeLabel }));
|
|
24314
|
+
bar.appendChild(this.dropdown("Label size", sizes, common((d) => fib(d).labelsSize ?? "normal"), (s) => labelSizeIcon(s), (v) => actions.patch({ labelsSize: v }), { label: sizeLabel }));
|
|
24080
24315
|
}
|
|
24081
24316
|
bar.appendChild(this.divider());
|
|
24082
|
-
if (isFrvp) bar.appendChild(this.iconBtn("Settings", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "frvp")));
|
|
24083
|
-
if (isPosition) bar.appendChild(this.iconBtn("Position size", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "position")));
|
|
24084
|
-
if (editableLevels) bar.appendChild(this.iconBtn("Levels", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "levels")));
|
|
24085
|
-
bar.appendChild(this.toggle("Lock", LOCK_ICON,
|
|
24317
|
+
if (!multi && isFrvp) bar.appendChild(this.iconBtn("Settings", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "frvp")));
|
|
24318
|
+
if (!multi && isPosition) bar.appendChild(this.iconBtn("Position size", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "position")));
|
|
24319
|
+
if (!multi && editableLevels) bar.appendChild(this.iconBtn("Levels", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "levels")));
|
|
24320
|
+
bar.appendChild(this.toggle("Lock", LOCK_ICON, common((d) => d.locked), (v) => actions.setLocked(v), UNLOCK_ICON));
|
|
24086
24321
|
const del = this.iconBtn("Delete", TRASH_ICON, () => actions.remove());
|
|
24087
24322
|
del.style.color = "var(--vela-danger)";
|
|
24088
24323
|
bar.appendChild(del);
|
|
@@ -24268,7 +24503,7 @@ var DrawingSettingsPopup = class {
|
|
|
24268
24503
|
});
|
|
24269
24504
|
return b;
|
|
24270
24505
|
}
|
|
24271
|
-
/** The trailing kebab overflow — z-order actions
|
|
24506
|
+
/** The trailing kebab overflow — duplicate, z-order actions, reset settings. */
|
|
24272
24507
|
kebabButton(actions) {
|
|
24273
24508
|
const b = this.base("More");
|
|
24274
24509
|
b.innerHTML = sized(KEBAB_ICON);
|
|
@@ -24280,6 +24515,7 @@ var DrawingSettingsPopup = class {
|
|
|
24280
24515
|
return;
|
|
24281
24516
|
}
|
|
24282
24517
|
this.openActionMenu(b, [
|
|
24518
|
+
{ icon: CLONE_ICON, label: "Duplicate", onClick: () => actions.duplicate() },
|
|
24283
24519
|
{ icon: FRONT_ICON, label: "Bring to front", onClick: () => actions.reorder("front") },
|
|
24284
24520
|
{ icon: BACK_ICON, label: "Send to back", onClick: () => actions.reorder("back") },
|
|
24285
24521
|
{ icon: RESET_ICON, label: "Reset settings", onClick: () => actions.resetSettings() }
|
|
@@ -24436,26 +24672,30 @@ var DrawingSettingsPopup = class {
|
|
|
24436
24672
|
this.tipEl?.remove();
|
|
24437
24673
|
this.tipEl = null;
|
|
24438
24674
|
}
|
|
24439
|
-
|
|
24675
|
+
/** An on/off button. A mixed state (the selected drawings disagree) reads as half-lit and
|
|
24676
|
+
* the first click turns it ON for all of them. `off` swaps in a distinct glyph while the
|
|
24677
|
+
* toggle is off (a lock reads as an open padlock until it is engaged); mixed shows `icon`. */
|
|
24678
|
+
toggle(tip, icon2, active, onChange, off) {
|
|
24440
24679
|
const b = this.base(tip);
|
|
24441
|
-
b.innerHTML = sized(icon2);
|
|
24442
24680
|
const set = (on2) => {
|
|
24443
|
-
b.dataset.active = on2 ? "1" : "0";
|
|
24681
|
+
b.dataset.active = on2 === MIXED ? "mixed" : on2 ? "1" : "0";
|
|
24682
|
+
b.innerHTML = sized(off && on2 === false ? off : icon2);
|
|
24444
24683
|
};
|
|
24445
24684
|
set(active);
|
|
24446
24685
|
let on = active;
|
|
24447
24686
|
b.addEventListener("click", () => {
|
|
24448
|
-
on = !on;
|
|
24687
|
+
on = on === MIXED ? true : !on;
|
|
24449
24688
|
set(on);
|
|
24450
24689
|
onChange(on);
|
|
24451
24690
|
});
|
|
24452
24691
|
return b;
|
|
24453
24692
|
}
|
|
24454
24693
|
/** An inline numeric width field for tools whose stroke range outgrows the 1–4px
|
|
24455
|
-
* ladder — the value is clamped to the schema's declared min/max on commit.
|
|
24694
|
+
* ladder — the value is clamped to the schema's declared min/max on commit. A mixed
|
|
24695
|
+
* value shows an empty field with a dash until a number is typed for all. */
|
|
24456
24696
|
widthInput(tip, value, min, max, step, onChange) {
|
|
24457
24697
|
const ni = new NumberInput({
|
|
24458
|
-
value,
|
|
24698
|
+
value: value === MIXED ? min : value,
|
|
24459
24699
|
min,
|
|
24460
24700
|
max,
|
|
24461
24701
|
step,
|
|
@@ -24467,13 +24707,18 @@ var DrawingSettingsPopup = class {
|
|
|
24467
24707
|
title: tip,
|
|
24468
24708
|
onChange
|
|
24469
24709
|
});
|
|
24710
|
+
if (value === MIXED) {
|
|
24711
|
+
ni.input.value = "";
|
|
24712
|
+
ni.input.placeholder = "\u2014";
|
|
24713
|
+
}
|
|
24470
24714
|
ni.el.dataset.tip = tip;
|
|
24471
24715
|
trapChartKeys(ni.el);
|
|
24472
24716
|
return ni.el;
|
|
24473
24717
|
}
|
|
24474
24718
|
/** A pick-one dropdown: the trigger shows the current value's glyph (plus an optional inline
|
|
24475
24719
|
* text label — e.g. `2px` — for controls that would otherwise look alike), and clicking it
|
|
24476
|
-
* opens a floating list of every option so a value is one click away (no cycling through).
|
|
24720
|
+
* opens a floating list of every option so a value is one click away (no cycling through).
|
|
24721
|
+
* A mixed value shows a dash in the trigger and highlights no row. */
|
|
24477
24722
|
dropdown(tip, values, current, render, onChange, opts = {}) {
|
|
24478
24723
|
const b = this.base(tip);
|
|
24479
24724
|
b.style.width = "auto";
|
|
@@ -24483,14 +24728,20 @@ var DrawingSettingsPopup = class {
|
|
|
24483
24728
|
let cur = current;
|
|
24484
24729
|
const paint = (v) => {
|
|
24485
24730
|
b.replaceChildren();
|
|
24486
|
-
|
|
24731
|
+
if (v === MIXED) {
|
|
24732
|
+
const dash = document.createElement("span");
|
|
24733
|
+
dash.textContent = "\u2014";
|
|
24734
|
+
dash.style.cssText = "min-width:18px;text-align:center;opacity:0.85;";
|
|
24735
|
+
b.appendChild(dash);
|
|
24736
|
+
}
|
|
24737
|
+
const glyph = v === MIXED ? "" : render(v);
|
|
24487
24738
|
if (glyph) {
|
|
24488
24739
|
const ic = document.createElement("span");
|
|
24489
24740
|
ic.style.cssText = "display:flex;";
|
|
24490
24741
|
ic.innerHTML = sized(glyph);
|
|
24491
24742
|
b.appendChild(ic);
|
|
24492
24743
|
}
|
|
24493
|
-
if (opts.label && opts.labelInTrigger) {
|
|
24744
|
+
if (opts.label && opts.labelInTrigger && v !== MIXED) {
|
|
24494
24745
|
const tx = document.createElement("span");
|
|
24495
24746
|
tx.textContent = opts.label(v);
|
|
24496
24747
|
tx.style.cssText = "font-size:11px;opacity:0.85;font-variant-numeric:tabular-nums;";
|
|
@@ -24560,8 +24811,10 @@ var DrawingSettingsPopup = class {
|
|
|
24560
24811
|
}
|
|
24561
24812
|
/** A color control: an `icon` over a thin **colored underline** showing the current
|
|
24562
24813
|
* value (the common idiom), with an invisible native picker overlaid to edit it.
|
|
24563
|
-
* `iconSize` shrinks it for the floating text controls.
|
|
24564
|
-
|
|
24814
|
+
* `iconSize` shrinks it for the floating text controls. A mixed color paints the
|
|
24815
|
+
* underline striped with the colors `used` across the selection, and the picker then
|
|
24816
|
+
* leads with those colors so unifying onto one of them is a single click. */
|
|
24817
|
+
colorButton(tip, icon2, color, onChange, iconSize = 17, used = []) {
|
|
24565
24818
|
const b = document.createElement("button");
|
|
24566
24819
|
b.type = "button";
|
|
24567
24820
|
b.dataset.tip = tip;
|
|
@@ -24571,13 +24824,14 @@ var DrawingSettingsPopup = class {
|
|
|
24571
24824
|
ic.style.cssText = "display:flex;";
|
|
24572
24825
|
ic.innerHTML = sized(icon2, iconSize);
|
|
24573
24826
|
const bar = document.createElement("span");
|
|
24574
|
-
bar.style.cssText = `display:block;height:3px;width:${Math.round(iconSize * 0.85)}px;border-radius:2px;background:${color};`;
|
|
24827
|
+
bar.style.cssText = `display:block;height:3px;width:${Math.round(iconSize * 0.85)}px;border-radius:2px;background:${color === MIXED ? stripes(used) : color};`;
|
|
24575
24828
|
b.append(ic, bar);
|
|
24576
24829
|
let cur = color;
|
|
24577
24830
|
b.addEventListener("pointerdown", (e) => e.stopPropagation());
|
|
24578
24831
|
b.addEventListener("click", (e) => {
|
|
24579
24832
|
e.stopPropagation();
|
|
24580
|
-
|
|
24833
|
+
const seed = cur === MIXED ? used[0] ?? DEFAULT_DRAWING_COLOR : cur;
|
|
24834
|
+
this.toggleColorPopover(b, seed, cur === MIXED ? used : [], (v) => {
|
|
24581
24835
|
cur = v;
|
|
24582
24836
|
bar.style.background = v;
|
|
24583
24837
|
onChange(v);
|
|
@@ -24586,20 +24840,42 @@ var DrawingSettingsPopup = class {
|
|
|
24586
24840
|
return b;
|
|
24587
24841
|
}
|
|
24588
24842
|
/** Open the color popover for `anchor`, or close it if it's already this anchor's (toggle). */
|
|
24589
|
-
toggleColorPopover(anchor, color, onChange) {
|
|
24843
|
+
toggleColorPopover(anchor, color, used, onChange) {
|
|
24590
24844
|
if (this.colorOwner === anchor) {
|
|
24591
24845
|
this.closeColorPopover();
|
|
24592
24846
|
return;
|
|
24593
24847
|
}
|
|
24594
|
-
this.openColorPopover(anchor, color, onChange);
|
|
24848
|
+
this.openColorPopover(anchor, color, used, onChange);
|
|
24595
24849
|
}
|
|
24596
|
-
/** A floating RGB picker + opacity slider anchored to a color swatch — emits `#RRGGBB(AA)`.
|
|
24597
|
-
|
|
24850
|
+
/** A floating RGB picker + opacity slider anchored to a color swatch — emits `#RRGGBB(AA)`.
|
|
24851
|
+
* `used` (a mixed swatch's colors) leads the picker as one-click unify shortcuts. */
|
|
24852
|
+
openColorPopover(anchor, color, used, onChange) {
|
|
24598
24853
|
const t = this.theme;
|
|
24599
24854
|
this.colorPop = this.hostFloat(anchor, {
|
|
24600
24855
|
zIndex: 25,
|
|
24601
24856
|
padding: "10px",
|
|
24602
|
-
fill: (el) => {
|
|
24857
|
+
fill: (el, pop) => {
|
|
24858
|
+
if (used.length > 1) {
|
|
24859
|
+
const row = document.createElement("div");
|
|
24860
|
+
row.style.cssText = "display:flex;align-items:center;gap:6px;padding-bottom:9px;border-bottom:1px solid var(--vela-border);";
|
|
24861
|
+
const lbl = document.createElement("span");
|
|
24862
|
+
lbl.textContent = "In use";
|
|
24863
|
+
lbl.style.cssText = "font:var(--vela-font-size-sm) inherit;opacity:0.7;margin-right:2px;";
|
|
24864
|
+
row.appendChild(lbl);
|
|
24865
|
+
for (const c of used) {
|
|
24866
|
+
const sw = document.createElement("button");
|
|
24867
|
+
sw.type = "button";
|
|
24868
|
+
sw.dataset.tip = c;
|
|
24869
|
+
sw.style.cssText = `width:18px;height:18px;border-radius:4px;border:1px solid var(--vela-border-strong);cursor:pointer;background:${c};padding:0;`;
|
|
24870
|
+
sw.addEventListener("click", (e) => {
|
|
24871
|
+
e.stopPropagation();
|
|
24872
|
+
pop.hide();
|
|
24873
|
+
onChange(c);
|
|
24874
|
+
});
|
|
24875
|
+
row.appendChild(sw);
|
|
24876
|
+
}
|
|
24877
|
+
el.appendChild(row);
|
|
24878
|
+
}
|
|
24603
24879
|
el.appendChild(buildColorPicker(color, t, onChange));
|
|
24604
24880
|
}
|
|
24605
24881
|
});
|
|
@@ -24620,6 +24896,7 @@ var TYPE_ICON = icon("type");
|
|
|
24620
24896
|
var PRICE_DELTA_ICON = icon("price-delta");
|
|
24621
24897
|
var DATE_DELTA_ICON = icon("date-delta");
|
|
24622
24898
|
var LOCK_ICON = icon("lock");
|
|
24899
|
+
var UNLOCK_ICON = icon("unlock");
|
|
24623
24900
|
var FRONT_ICON = icon("bring-front");
|
|
24624
24901
|
var BACK_ICON = icon("send-back");
|
|
24625
24902
|
var TRASH_ICON = icon("trash");
|
|
@@ -24627,6 +24904,7 @@ var BOLD_ICON = icon("bold");
|
|
|
24627
24904
|
var ITALIC_ICON = icon("italic");
|
|
24628
24905
|
var GRIP_ICON = icon("grip");
|
|
24629
24906
|
var KEBAB_ICON = icon("kebab");
|
|
24907
|
+
var CLONE_ICON = icon("clone");
|
|
24630
24908
|
var RESET_ICON = icon("reset");
|
|
24631
24909
|
var GEAR_ICON = icon("gear");
|
|
24632
24910
|
var CHEVRON_ICON = icon("chevron-down");
|
|
@@ -24635,6 +24913,12 @@ var BANDS_ICON = icon("bands");
|
|
|
24635
24913
|
var DEDEKIND_ICON2 = icon("dedekind");
|
|
24636
24914
|
var SONIC_ICON2 = icon("sonic");
|
|
24637
24915
|
var SUPERSONIC_ICON2 = icon("supersonic");
|
|
24916
|
+
function stripes(colors) {
|
|
24917
|
+
const bands = colors.length > 1 ? colors : ["var(--vela-fg-faint)", "var(--vela-fg-muted)"];
|
|
24918
|
+
const step = 100 / bands.length;
|
|
24919
|
+
const stops = bands.map((c, i) => `${c} ${i * step}% ${(i + 1) * step}%`).join(",");
|
|
24920
|
+
return `linear-gradient(90deg,${stops})`;
|
|
24921
|
+
}
|
|
24638
24922
|
function lineIcon(width, style) {
|
|
24639
24923
|
const dash = style === "dashed" ? "5,3" : style === "dotted" ? "1.5,3" : "";
|
|
24640
24924
|
return svg24(`<line x1="3.5" y1="12" x2="20.5" y2="12" stroke-width="${Number(width) || 2}" stroke-dasharray="${dash}"/>`);
|
|
@@ -25550,6 +25834,20 @@ var UserDrawingController = class {
|
|
|
25550
25834
|
// reported exactly when it actually changes).
|
|
25551
25835
|
this.textEditor = null;
|
|
25552
25836
|
this.painter = new DrawingPainter();
|
|
25837
|
+
/** The open popup is the multi-selection bar (vs one drawing's own) — decides what a
|
|
25838
|
+
* shrinking selection does with it. */
|
|
25839
|
+
this.popupMulti = false;
|
|
25840
|
+
/** Dismiss-on-outside-press also clears the selection — except when the press is ABOUT the
|
|
25841
|
+
* selection: it carries a multi-select modifier (Shift / Ctrl / Cmd — it will add to the
|
|
25842
|
+
* selection or sweep a marquee onto it), or it landed on a drawing that is already selected
|
|
25843
|
+
* (the canvas handler ran first and is holding it for a drag or a click), so the drawings
|
|
25844
|
+
* whose bar just closed must stay selected. */
|
|
25845
|
+
this.onPopupDismissed = (e) => {
|
|
25846
|
+
if (e && (e.shiftKey || e.ctrlKey || e.metaKey)) return;
|
|
25847
|
+
const pressed = this.interaction.pressedId();
|
|
25848
|
+
if (pressed && this.selectedIds.has(pressed)) return;
|
|
25849
|
+
this.clearSelection();
|
|
25850
|
+
};
|
|
25553
25851
|
this.ctx = canvas.getContext("2d");
|
|
25554
25852
|
this.popup = new DrawingSettingsPopup(overlayHost, deps.theme(), () => deps.chartBarMs());
|
|
25555
25853
|
this.toolbar = new DrawingToolbar(
|
|
@@ -25726,6 +26024,12 @@ var UserDrawingController = class {
|
|
|
25726
26024
|
}
|
|
25727
26025
|
setSelection(ids) {
|
|
25728
26026
|
this.selectedIds = new Set(ids);
|
|
26027
|
+
if (ids.length >= 2) this.openSettingsForSelection(ids);
|
|
26028
|
+
else if (this.popupMulti) {
|
|
26029
|
+
this.popupMulti = false;
|
|
26030
|
+
this.popup.close();
|
|
26031
|
+
if (ids.length === 1) this.openSettingsById(ids[0], 0, 0);
|
|
26032
|
+
}
|
|
25729
26033
|
this.render();
|
|
25730
26034
|
}
|
|
25731
26035
|
/** Programmatic twin of clicking the drawing: highlight it + float its settings popup. */
|
|
@@ -25771,12 +26075,16 @@ var UserDrawingController = class {
|
|
|
25771
26075
|
});
|
|
25772
26076
|
}
|
|
25773
26077
|
/** Delete the (unlocked) drawing under the cursor. True when one was removed.
|
|
25774
|
-
* Shared by the eraser (click + drag) and the middle-click shortcut
|
|
25775
|
-
|
|
26078
|
+
* Shared by the eraser (click + drag) and the middle-click shortcut; the latter passes
|
|
26079
|
+
* `withSelection` so a hit on a member of a multi-selection removes the selection's
|
|
26080
|
+
* unlocked members — whichever member was hit, locked or not. */
|
|
26081
|
+
deleteAt(x, y, withSelection = false) {
|
|
25776
26082
|
const hit = topDrawingAt(this.drawings, x, y, this.deps.projector(), HIT_TOLERANCE);
|
|
25777
|
-
if (!hit
|
|
26083
|
+
if (!hit) return false;
|
|
26084
|
+
const ids = deleteTargets(hit, this.selectedIds, this.drawings, withSelection);
|
|
26085
|
+
if (ids.length === 0) return false;
|
|
25778
26086
|
this.popup.close();
|
|
25779
|
-
this.emit({ kind: "delete", ids
|
|
26087
|
+
this.emit({ kind: "delete", ids });
|
|
25780
26088
|
return true;
|
|
25781
26089
|
}
|
|
25782
26090
|
/** Shift+press on the empty plot: arm the measure ruler AND start it at (x, y) in one
|
|
@@ -25802,7 +26110,14 @@ var UserDrawingController = class {
|
|
|
25802
26110
|
this.render();
|
|
25803
26111
|
}
|
|
25804
26112
|
}
|
|
25805
|
-
|
|
26113
|
+
/** Ctrl/Cmd+press on the empty plot: sweep a selection box (adds the drawings it touches to
|
|
26114
|
+
* the selection). Returns false when a mode/tool is active (the normal press path owns it). */
|
|
26115
|
+
beginMarqueeAt(x, y) {
|
|
26116
|
+
if (this.measureMode || this.eraserMode || !this.interaction.beginMarquee(x, y)) return false;
|
|
26117
|
+
this.render();
|
|
26118
|
+
return true;
|
|
26119
|
+
}
|
|
26120
|
+
pointerDown(x, y, snap = "off", shift2 = false, mod = false) {
|
|
25806
26121
|
if (this.eraserMode) {
|
|
25807
26122
|
this.erasing = true;
|
|
25808
26123
|
this.deleteAt(x, y);
|
|
@@ -25822,9 +26137,9 @@ var UserDrawingController = class {
|
|
|
25822
26137
|
return;
|
|
25823
26138
|
}
|
|
25824
26139
|
}
|
|
25825
|
-
this.interaction.down(x, y, snap, shift2);
|
|
26140
|
+
this.interaction.down(x, y, snap, shift2, mod);
|
|
25826
26141
|
}
|
|
25827
|
-
pointerMove(x, y, snap = "off", shift2 = false) {
|
|
26142
|
+
pointerMove(x, y, snap = "off", shift2 = false, mod = false) {
|
|
25828
26143
|
if (this.eraserMode) {
|
|
25829
26144
|
if (this.erasing) this.deleteAt(x, y);
|
|
25830
26145
|
return;
|
|
@@ -25836,12 +26151,14 @@ var UserDrawingController = class {
|
|
|
25836
26151
|
return;
|
|
25837
26152
|
}
|
|
25838
26153
|
this.interaction.move(x, y, snap, shift2);
|
|
25839
|
-
this.updateHover(x, y);
|
|
26154
|
+
this.updateHover(x, y, mod);
|
|
25840
26155
|
}
|
|
25841
|
-
/** Track which drawing is hovered so its handles appear on hover (and only then).
|
|
25842
|
-
|
|
26156
|
+
/** Track which drawing is hovered so its handles appear on hover (and only then). While
|
|
26157
|
+
* Ctrl/Cmd is held the cursor is picking a selection — handles then mark only what IS
|
|
26158
|
+
* selected, so a hovered candidate doesn't already read as selected. */
|
|
26159
|
+
updateHover(x, y, mod = false) {
|
|
25843
26160
|
let id = null;
|
|
25844
|
-
if (this.activeTool == null && !this.interaction.isPlacing() && !this.interaction.isDragging()) {
|
|
26161
|
+
if (!mod && this.activeTool == null && !this.interaction.isPlacing() && !this.interaction.isDragging()) {
|
|
25845
26162
|
id = topDrawingAt(this.drawings, x, y, this.deps.projector(), HIT_TOLERANCE)?.id ?? null;
|
|
25846
26163
|
}
|
|
25847
26164
|
if (id !== this.hoveredId) {
|
|
@@ -25861,7 +26178,12 @@ var UserDrawingController = class {
|
|
|
25861
26178
|
this.render();
|
|
25862
26179
|
return;
|
|
25863
26180
|
}
|
|
26181
|
+
const dragged = this.interaction.isDragging() ? this.interaction.pressedId() : null;
|
|
25864
26182
|
this.interaction.up(x, y);
|
|
26183
|
+
if (dragged && this.selectedIds.has(dragged) && !this.popup.isOpen()) {
|
|
26184
|
+
if (this.selectedIds.size >= 2) this.openSettingsForSelection(this.selectionIds());
|
|
26185
|
+
else this.openSettingsById(dragged, x, y);
|
|
26186
|
+
}
|
|
25865
26187
|
}
|
|
25866
26188
|
/** Toggle the transient ruler. Arming it clears any drawing tool + selection. */
|
|
25867
26189
|
toggleMeasure() {
|
|
@@ -26126,7 +26448,7 @@ var UserDrawingController = class {
|
|
|
26126
26448
|
this.emit({ kind: "duplicate", ids: this.selectionIds() });
|
|
26127
26449
|
break;
|
|
26128
26450
|
case "delete": {
|
|
26129
|
-
const ids = this.selectedIds.size ? this.
|
|
26451
|
+
const ids = this.selectedIds.size ? this.deletableSelection() : this.hoveredId ? [this.hoveredId] : [];
|
|
26130
26452
|
if (ids.length) {
|
|
26131
26453
|
this.popup.close();
|
|
26132
26454
|
this.emit({ kind: "delete", ids });
|
|
@@ -26142,6 +26464,9 @@ var UserDrawingController = class {
|
|
|
26142
26464
|
selectionIds() {
|
|
26143
26465
|
return [...this.selectedIds];
|
|
26144
26466
|
}
|
|
26467
|
+
deletableSelection() {
|
|
26468
|
+
return deletableSelection(this.selectedIds, this.drawings);
|
|
26469
|
+
}
|
|
26145
26470
|
/** Move every selected (unlocked) drawing by a pixel delta — one edit/edit-many → one undo step. */
|
|
26146
26471
|
nudgeSelection(dx, dy) {
|
|
26147
26472
|
const proj = this.deps.projector();
|
|
@@ -26250,6 +26575,8 @@ var UserDrawingController = class {
|
|
|
26250
26575
|
this.painter.seriesLook = this.deps.seriesLook();
|
|
26251
26576
|
this.painter.paintAll(ctx, this.drawings.filter((d) => !this.isInterleaved(d)), proj, this.deps.theme(), targets);
|
|
26252
26577
|
this.painter.paintHighlights(ctx, this.drawings.filter((d) => this.isInterleaved(d)), proj, handleIdsFor(targets));
|
|
26578
|
+
const clones = this.interaction.dragClones();
|
|
26579
|
+
if (clones) this.painter.paintAll(ctx, clones, proj, this.deps.theme(), { selected: new Set(clones.map((c) => c.id)) });
|
|
26253
26580
|
this.layoutTextEditor();
|
|
26254
26581
|
const ghost = this.interaction.ghost();
|
|
26255
26582
|
if (ghost) this.painter.paintGhost(ctx, ghost, proj, this.deps.theme());
|
|
@@ -26265,6 +26592,8 @@ var UserDrawingController = class {
|
|
|
26265
26592
|
const my = m ? proj.yOf(m.point.price, m.paneId) : null;
|
|
26266
26593
|
if (m && my != null) this.painter.paintSnapRing(ctx, proj.xOf(m.point.time), my, this.deps.theme());
|
|
26267
26594
|
if (this.measure.isActive()) this.measure.paint(ctx, proj, this.deps.theme());
|
|
26595
|
+
const marquee = this.interaction.marqueeRect();
|
|
26596
|
+
if (marquee) this.painter.paintMarquee(ctx, marquee);
|
|
26268
26597
|
}
|
|
26269
26598
|
destroy() {
|
|
26270
26599
|
this.closeTextEditor();
|
|
@@ -26282,15 +26611,80 @@ var UserDrawingController = class {
|
|
|
26282
26611
|
if (this.selectedIds.size) this.emit({ kind: "select", ids: [] });
|
|
26283
26612
|
this.render();
|
|
26284
26613
|
}
|
|
26614
|
+
/** Report the live drawings' current state as one edit (one undo step, however many). */
|
|
26615
|
+
emitEdits(drawings) {
|
|
26616
|
+
const docs = drawings.map((d) => d.serialize());
|
|
26617
|
+
if (docs.length === 1) this.emit({ kind: "edit", doc: docs[0] });
|
|
26618
|
+
else if (docs.length > 1) this.emit({ kind: "edit-many", docs });
|
|
26619
|
+
}
|
|
26620
|
+
/** The one bar for a multi-selection: it shows the controls all of its drawings share and
|
|
26621
|
+
* every action applies to all of them. Floats clear of their combined bounds. */
|
|
26622
|
+
openSettingsForSelection(ids) {
|
|
26623
|
+
const live = () => ids.map((id) => this.drawings.find((d) => d.id === id)).filter((d) => d != null);
|
|
26624
|
+
const drawings = live();
|
|
26625
|
+
if (drawings.length < 2) return;
|
|
26626
|
+
const proj = this.deps.projector();
|
|
26627
|
+
let anchor = null;
|
|
26628
|
+
for (const d of drawings) {
|
|
26629
|
+
const b = d.bounds(proj);
|
|
26630
|
+
if (!b) continue;
|
|
26631
|
+
if (!anchor) anchor = { ...b };
|
|
26632
|
+
else {
|
|
26633
|
+
const right = Math.max(anchor.x + anchor.w, b.x + b.w);
|
|
26634
|
+
const bottom = Math.max(anchor.y + anchor.h, b.y + b.h);
|
|
26635
|
+
anchor.x = Math.min(anchor.x, b.x);
|
|
26636
|
+
anchor.y = Math.min(anchor.y, b.y);
|
|
26637
|
+
anchor.w = right - anchor.x;
|
|
26638
|
+
anchor.h = bottom - anchor.y;
|
|
26639
|
+
}
|
|
26640
|
+
}
|
|
26641
|
+
this.popupMulti = true;
|
|
26642
|
+
this.emit({ kind: "settings", id: drawings[0].id });
|
|
26643
|
+
this.popup.open(drawings, anchor, {
|
|
26644
|
+
resolve: () => live()[0] ?? null,
|
|
26645
|
+
patch: (p) => {
|
|
26646
|
+
const ds = live();
|
|
26647
|
+
for (const d of ds) d.applySettings(p);
|
|
26648
|
+
this.render();
|
|
26649
|
+
this.emitEdits(ds);
|
|
26650
|
+
},
|
|
26651
|
+
setLocked: (v) => {
|
|
26652
|
+
const ds = live();
|
|
26653
|
+
for (const d of ds) d.locked = v;
|
|
26654
|
+
this.emitEdits(ds);
|
|
26655
|
+
},
|
|
26656
|
+
reorder: (to) => {
|
|
26657
|
+
for (const d of live()) this.emit({ kind: "reorder", id: d.id, to });
|
|
26658
|
+
},
|
|
26659
|
+
duplicate: () => {
|
|
26660
|
+
this.popup.close();
|
|
26661
|
+
this.emit({ kind: "duplicate", ids: live().map((d) => d.id) });
|
|
26662
|
+
},
|
|
26663
|
+
resetSettings: () => {
|
|
26664
|
+
const ds = live();
|
|
26665
|
+
for (const d of ds) resetDrawingSettings(d);
|
|
26666
|
+
this.render();
|
|
26667
|
+
this.emitEdits(ds);
|
|
26668
|
+
this.openSettingsForSelection(ids);
|
|
26669
|
+
},
|
|
26670
|
+
remove: () => {
|
|
26671
|
+
const ids2 = live().filter((d) => !d.locked).map((d) => d.id);
|
|
26672
|
+
if (ids2.length === 0) return;
|
|
26673
|
+
this.popup.close();
|
|
26674
|
+
this.emit({ kind: "delete", ids: ids2 });
|
|
26675
|
+
}
|
|
26676
|
+
}, this.onPopupDismissed);
|
|
26677
|
+
}
|
|
26285
26678
|
/** A click on a drawing opens its settings toolbar (text labels edit text here too). */
|
|
26286
26679
|
openSettingsById(id, _x, _y) {
|
|
26287
26680
|
const drawing = this.drawings.find((d) => d.id === id);
|
|
26288
26681
|
if (!drawing) return;
|
|
26289
26682
|
const live = () => this.drawings.find((d) => d.id === id);
|
|
26683
|
+
this.popupMulti = false;
|
|
26290
26684
|
this.emit({ kind: "select", ids: [id] });
|
|
26291
26685
|
this.emit({ kind: "settings", id });
|
|
26292
26686
|
const anchor = drawing.bounds(this.deps.projector());
|
|
26293
|
-
this.popup.open(drawing, anchor, {
|
|
26687
|
+
this.popup.open([drawing], anchor, {
|
|
26294
26688
|
// Sync rebuilds instances, so a panel that reads values back after a patch (e.g. the
|
|
26295
26689
|
// position tool's price fields, where one edit can flip another level) resolves fresh.
|
|
26296
26690
|
resolve: () => live() ?? null,
|
|
@@ -26308,6 +26702,11 @@ var UserDrawingController = class {
|
|
|
26308
26702
|
this.emit({ kind: "edit", doc: d.serialize() });
|
|
26309
26703
|
},
|
|
26310
26704
|
reorder: (to) => this.emit({ kind: "reorder", id, to }),
|
|
26705
|
+
duplicate: () => {
|
|
26706
|
+
this.finishTextEditor(true);
|
|
26707
|
+
this.popup.close();
|
|
26708
|
+
this.emit({ kind: "duplicate", ids: [id] });
|
|
26709
|
+
},
|
|
26311
26710
|
resetSettings: () => {
|
|
26312
26711
|
const d = live();
|
|
26313
26712
|
if (!d) return;
|
|
@@ -26330,7 +26729,13 @@ var UserDrawingController = class {
|
|
|
26330
26729
|
this.popup.close();
|
|
26331
26730
|
this.emit({ kind: "delete", ids: [id] });
|
|
26332
26731
|
}
|
|
26333
|
-
},
|
|
26732
|
+
}, this.onPopupDismissed);
|
|
26733
|
+
}
|
|
26734
|
+
/** A plain click on the empty plot: drop the selection. The popup's dismiss-on-outside-press
|
|
26735
|
+
* does the same, but a selection can exist with no bar open (a lone Ctrl-click pick, a
|
|
26736
|
+
* selection whose bar was dismissed by a modifier press) — this covers those. */
|
|
26737
|
+
deselect() {
|
|
26738
|
+
this.clearSelection();
|
|
26334
26739
|
}
|
|
26335
26740
|
/** Report placement progress upstream (`draft` intent) so the drawings sync can
|
|
26336
26741
|
* mirror the ghost live on linked charts. Called from every render; only actual
|
|
@@ -26356,6 +26761,13 @@ var UserDrawingController = class {
|
|
|
26356
26761
|
}, 0);
|
|
26357
26762
|
return;
|
|
26358
26763
|
}
|
|
26764
|
+
if (i.kind === "clone") {
|
|
26765
|
+
const before = new Set(this.drawings.map((d) => d.id));
|
|
26766
|
+
this.intentCb?.(i);
|
|
26767
|
+
const fresh = this.drawings.filter((d) => !before.has(d.id));
|
|
26768
|
+
if (fresh.length === 1) this.openSettingsById(fresh[0].id, 0, 0);
|
|
26769
|
+
return;
|
|
26770
|
+
}
|
|
26359
26771
|
this.intentCb?.(i);
|
|
26360
26772
|
}
|
|
26361
26773
|
};
|
|
@@ -27520,7 +27932,6 @@ var ZOOM_OUT_MARGIN_BARS = 6;
|
|
|
27520
27932
|
var ZOOM_TAU_MS = 70;
|
|
27521
27933
|
var SCALE_TAU_MS = 80;
|
|
27522
27934
|
var FLING_TAU_MS = 110;
|
|
27523
|
-
var LIVE_BAR_TAU_MS = 90;
|
|
27524
27935
|
var SCROLL_TO_TAU_MS = 130;
|
|
27525
27936
|
var FLING_STOP_PX = 0.02;
|
|
27526
27937
|
var PRICE_SCALE_K = 4e-3;
|
|
@@ -27591,9 +28002,13 @@ var NativeRenderer = class {
|
|
|
27591
28002
|
/** Drawings layer self-serves Ctrl+Z/Y (see the `historyChords` feature). */
|
|
27592
28003
|
this.historyChordsEnabled = true;
|
|
27593
28004
|
this.liveRegion = null;
|
|
27594
|
-
// ── animation state (eased zoom + inertial pan) ──
|
|
28005
|
+
// ── animation state (eased zoom + inertial pan + live-bar glide) ──
|
|
27595
28006
|
this.animZoom = true;
|
|
27596
28007
|
this.animPan = true;
|
|
28008
|
+
this.animLiveBarMs = 0;
|
|
28009
|
+
// forming-bar OHLC glide time-constant; 0 = each tick snaps
|
|
28010
|
+
this.animLiveBarOnMs = LIVE_BAR_EASE_DEFAULT_MS;
|
|
28011
|
+
// the duration the settings-dialog on/off toggle restores (last non-zero value configured)
|
|
27597
28012
|
// Brand default candles.
|
|
27598
28013
|
this.candleUp = BULLISH;
|
|
27599
28014
|
this.candleDown = BEARISH;
|
|
@@ -27718,7 +28133,7 @@ var NativeRenderer = class {
|
|
|
27718
28133
|
this.moveIndicatorCbs = /* @__PURE__ */ new Set();
|
|
27719
28134
|
this.priceStyleCbs = /* @__PURE__ */ new Set();
|
|
27720
28135
|
this.name = "native";
|
|
27721
|
-
this.features = ["logScale", "currentPriceLine", "priceLabel", "countdown", "upColor", "downColor", "glow", "animZoom", "animPan", "intro", "zoomAnchor", "axisDrag", "paneResize", "candleZOrder", "candleVisible", "seriesOrder", "highlights", "sessionZones", "gridlines", "axisLabels", "scaleMode", "invertScale", "paneScales", "autoScale", "timezone", "keyboard", "historyChords", "priceStyle", "priceBaseline", "baselinePrice", "settings", "attribution", "dialogHost", "tradeMarkers", "indicatorTitles", "indicatorValues"];
|
|
28136
|
+
this.features = ["logScale", "currentPriceLine", "priceLabel", "countdown", "upColor", "downColor", "glow", "animZoom", "animPan", "animLiveBar", "intro", "zoomAnchor", "axisDrag", "paneResize", "candleZOrder", "candleVisible", "seriesOrder", "highlights", "sessionZones", "gridlines", "axisLabels", "scaleMode", "invertScale", "paneScales", "autoScale", "timezone", "keyboard", "historyChords", "priceStyle", "priceBaseline", "baselinePrice", "settings", "attribution", "dialogHost", "tradeMarkers", "indicatorTitles", "indicatorValues"];
|
|
27722
28137
|
/** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
|
|
27723
28138
|
* so moving onto the button doesn't count as leaving). */
|
|
27724
28139
|
this.onScrollProximityMove = (e) => {
|
|
@@ -27752,6 +28167,7 @@ var NativeRenderer = class {
|
|
|
27752
28167
|
this.backendMode = opts.nativeBackend;
|
|
27753
28168
|
this.animZoom = opts.animZoom;
|
|
27754
28169
|
this.animPan = opts.animPan;
|
|
28170
|
+
this.setLiveBarEase(resolveLiveBarEaseMs(opts.animLiveBar));
|
|
27755
28171
|
this.glowAmount = opts.glow;
|
|
27756
28172
|
this.candleUp = opts.upColor;
|
|
27757
28173
|
this.candleDown = opts.downColor;
|
|
@@ -27801,6 +28217,10 @@ var NativeRenderer = class {
|
|
|
27801
28217
|
case "animPan":
|
|
27802
28218
|
this.animPan = Boolean(value);
|
|
27803
28219
|
return;
|
|
28220
|
+
case "animLiveBar":
|
|
28221
|
+
this.setLiveBarEase(resolveLiveBarEaseMs(value));
|
|
28222
|
+
return;
|
|
28223
|
+
// affects the next tick only; a glide in flight finishes at the new rate (or snaps at 0)
|
|
27804
28224
|
case "intro": {
|
|
27805
28225
|
const s = value === false || value === "none" || value === "off" || value == null ? "" : String(value);
|
|
27806
28226
|
this.introStyle = s;
|
|
@@ -27937,6 +28357,8 @@ var NativeRenderer = class {
|
|
|
27937
28357
|
return this.animZoom;
|
|
27938
28358
|
case "animPan":
|
|
27939
28359
|
return this.animPan;
|
|
28360
|
+
case "animLiveBar":
|
|
28361
|
+
return this.animLiveBarMs;
|
|
27940
28362
|
case "intro":
|
|
27941
28363
|
return this.introStyle;
|
|
27942
28364
|
case "zoomAnchor":
|
|
@@ -28105,7 +28527,8 @@ var NativeRenderer = class {
|
|
|
28105
28527
|
labelsVisible: this.scene.showAxisLabels,
|
|
28106
28528
|
currentPriceLine: this.scene.showPriceLine,
|
|
28107
28529
|
priceLabel: this.scene.showPriceLabel,
|
|
28108
|
-
countdown: this.scene.showCountdown
|
|
28530
|
+
countdown: this.scene.showCountdown,
|
|
28531
|
+
animateLastPrice: this.animLiveBarMs > 0
|
|
28109
28532
|
},
|
|
28110
28533
|
panes: { separatorColor: s.separatorColor ?? t.borderColor },
|
|
28111
28534
|
trades: {
|
|
@@ -28212,6 +28635,7 @@ var NativeRenderer = class {
|
|
|
28212
28635
|
this.scene.showPriceLabel = next.priceScale.priceLabel;
|
|
28213
28636
|
this.scene.showCountdown = next.priceScale.countdown;
|
|
28214
28637
|
this.syncCountdownTimer();
|
|
28638
|
+
this.animLiveBarMs = next.priceScale.animateLastPrice ? this.animLiveBarOnMs : 0;
|
|
28215
28639
|
s.separatorColor = keepInherit(s.separatorColor, next.panes.separatorColor, prevTheme.borderColor);
|
|
28216
28640
|
this.scene.tradeMarkers = {
|
|
28217
28641
|
visible: next.trades.visible,
|
|
@@ -28655,7 +29079,10 @@ var NativeRenderer = class {
|
|
|
28655
29079
|
zoomTo: (target, anchorLogical, anchorX) => this.zoomTo(target, anchorLogical, anchorX),
|
|
28656
29080
|
fling: (v) => this.fling(v),
|
|
28657
29081
|
onPointerMove: (x, y) => this.handlePointerMove(x, y),
|
|
28658
|
-
onClick: (x) =>
|
|
29082
|
+
onClick: (x) => {
|
|
29083
|
+
this.userDrawings?.deselect();
|
|
29084
|
+
this.handleClick(x);
|
|
29085
|
+
},
|
|
28659
29086
|
onAxisLongPress: (axis, x, y) => {
|
|
28660
29087
|
for (const cb of this.axisLongPressCbs) cb({ axis, x, y });
|
|
28661
29088
|
},
|
|
@@ -28673,11 +29100,13 @@ var NativeRenderer = class {
|
|
|
28673
29100
|
// User drawings claim a gesture before pan when armed / over a drawing.
|
|
28674
29101
|
drawingsClaim: (x, y) => this.userDrawings?.claim(x, y) ?? false,
|
|
28675
29102
|
drawingsMeasureStart: (x, y, snap) => this.userDrawings?.beginMeasureAt(x, y, snap) ?? false,
|
|
28676
|
-
|
|
29103
|
+
drawingsMarqueeStart: (x, y) => this.userDrawings?.beginMarqueeAt(x, y) ?? false,
|
|
29104
|
+
drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y, true) ?? false,
|
|
29105
|
+
// middle-click: a selected hit takes the whole selection
|
|
28677
29106
|
drawingsCancelPlacement: () => this.userDrawings?.cancelPlacement() ?? false,
|
|
28678
29107
|
drawingsSnapMode: () => this.snapMode,
|
|
28679
|
-
drawingsPointerDown: (x, y, snap, shift2) => this.userDrawings?.pointerDown(x, y, snap, shift2),
|
|
28680
|
-
drawingsPointerMove: (x, y, snap, shift2) => this.userDrawings?.pointerMove(x, y, snap, shift2),
|
|
29108
|
+
drawingsPointerDown: (x, y, snap, shift2, mod) => this.userDrawings?.pointerDown(x, y, snap, shift2, mod),
|
|
29109
|
+
drawingsPointerMove: (x, y, snap, shift2, mod) => this.userDrawings?.pointerMove(x, y, snap, shift2, mod),
|
|
28681
29110
|
drawingsPointerUp: (x, y, snap) => this.userDrawings?.pointerUp(x, y, snap),
|
|
28682
29111
|
drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? null,
|
|
28683
29112
|
drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
|
|
@@ -29028,8 +29457,11 @@ var NativeRenderer = class {
|
|
|
29028
29457
|
const last = this.bars[n - 1];
|
|
29029
29458
|
if (last && bar.time === last.time) {
|
|
29030
29459
|
this.bars[n - 1] = bar;
|
|
29031
|
-
if (this.liveEaseTime !== bar.time)
|
|
29032
|
-
|
|
29460
|
+
if (this.animLiveBarMs <= 0 || this.liveEaseTime !== bar.time) {
|
|
29461
|
+
this.syncLiveEase(bar);
|
|
29462
|
+
} else {
|
|
29463
|
+
this.animator.start();
|
|
29464
|
+
}
|
|
29033
29465
|
} else if (!last || bar.time > last.time) {
|
|
29034
29466
|
this.bars.push(bar);
|
|
29035
29467
|
this.syncLiveEase(bar);
|
|
@@ -29041,6 +29473,12 @@ var NativeRenderer = class {
|
|
|
29041
29473
|
this.scene.bars = this.bars;
|
|
29042
29474
|
this.scheduler.invalidate(4 /* Full */);
|
|
29043
29475
|
}
|
|
29476
|
+
/** Set the live-bar glide duration (0 = off). A non-zero value is also remembered as
|
|
29477
|
+
* what the config's on/off toggle (`priceScale.animateLastPrice`) switches back on to. */
|
|
29478
|
+
setLiveBarEase(ms) {
|
|
29479
|
+
this.animLiveBarMs = ms;
|
|
29480
|
+
if (ms > 0) this.animLiveBarOnMs = ms;
|
|
29481
|
+
}
|
|
29044
29482
|
/** Snap the eased forming-bar state to `bar` — no glide (a fresh bar or the first tick of one). */
|
|
29045
29483
|
syncLiveEase(bar) {
|
|
29046
29484
|
this.liveEaseTime = bar.time;
|
|
@@ -29053,9 +29491,10 @@ var NativeRenderer = class {
|
|
|
29053
29491
|
const target = this.bars[this.bars.length - 1];
|
|
29054
29492
|
if (!target || this.liveEaseTime !== target.time) return false;
|
|
29055
29493
|
const eps = Math.max(1e-9, Math.abs(target.close) * 1e-6);
|
|
29056
|
-
const
|
|
29057
|
-
const
|
|
29058
|
-
const
|
|
29494
|
+
const tau = this.animLiveBarMs;
|
|
29495
|
+
const nh = easeToward(this.liveEaseHigh, target.high, dtMs, tau);
|
|
29496
|
+
const nl = easeToward(this.liveEaseLow, target.low, dtMs, tau);
|
|
29497
|
+
const nc = easeToward(this.liveEaseClose, target.close, dtMs, tau);
|
|
29059
29498
|
const active = Math.abs(nh - target.high) > eps || Math.abs(nl - target.low) > eps || Math.abs(nc - target.close) > eps;
|
|
29060
29499
|
this.liveEaseHigh = active ? nh : target.high;
|
|
29061
29500
|
this.liveEaseLow = active ? nl : target.low;
|
|
@@ -31030,6 +31469,10 @@ function str3(inputs, key, fallback) {
|
|
|
31030
31469
|
const v = inputs[key];
|
|
31031
31470
|
return typeof v === "string" && v !== "" ? v : fallback;
|
|
31032
31471
|
}
|
|
31472
|
+
function bool(inputs, key, fallback) {
|
|
31473
|
+
const v = inputs[key];
|
|
31474
|
+
return typeof v === "boolean" ? v : fallback;
|
|
31475
|
+
}
|
|
31033
31476
|
function toPoints(bars, values, colors) {
|
|
31034
31477
|
const pts = new Array(bars.length);
|
|
31035
31478
|
for (let i = 0; i < bars.length; i++) {
|
|
@@ -32561,10 +33004,6 @@ var aroon = {
|
|
|
32561
33004
|
plots: [
|
|
32562
33005
|
{ key: "up", title: "Aroon up", values: up, color: BULLISH, width: 2 },
|
|
32563
33006
|
{ key: "down", title: "Aroon down", values: down, color: BEARISH, width: 2 }
|
|
32564
|
-
],
|
|
32565
|
-
levels: [
|
|
32566
|
-
{ key: "upper", price: 100, color: NEUTRAL, lineStyle: "dotted" },
|
|
32567
|
-
{ key: "lower", price: 0, color: NEUTRAL, lineStyle: "dotted" }
|
|
32568
33007
|
]
|
|
32569
33008
|
};
|
|
32570
33009
|
}
|
|
@@ -33062,8 +33501,10 @@ var volumeSpecs = [
|
|
|
33062
33501
|
var DAY_MS2 = 864e5;
|
|
33063
33502
|
function periodKey(anchor, time) {
|
|
33064
33503
|
if (anchor === "Week") return Math.floor((Math.floor(time / DAY_MS2) + 3) / 7);
|
|
33065
|
-
if (anchor === "Month") {
|
|
33504
|
+
if (anchor === "Month" || anchor === "Quarter" || anchor === "Year") {
|
|
33066
33505
|
const d = new Date(time);
|
|
33506
|
+
if (anchor === "Year") return d.getUTCFullYear();
|
|
33507
|
+
if (anchor === "Quarter") return d.getUTCFullYear() * 4 + Math.floor(d.getUTCMonth() / 3);
|
|
33067
33508
|
return d.getUTCFullYear() * 12 + d.getUTCMonth();
|
|
33068
33509
|
}
|
|
33069
33510
|
return Math.floor(time / DAY_MS2);
|
|
@@ -33115,40 +33556,86 @@ var parabolicSar = {
|
|
|
33115
33556
|
return { plots: [{ key: "sar", title: "PSAR", values, kind: "circles", color: SERIES_LINE, colors }] };
|
|
33116
33557
|
}
|
|
33117
33558
|
};
|
|
33559
|
+
var VWAP_BANDS_GROUP = "Bands";
|
|
33560
|
+
var VWAP_STYLE = "Style";
|
|
33561
|
+
var VWAP_BANDS = [
|
|
33562
|
+
{ on: "band1", mult: "band1Mult", color: "band1Color", fill: "band1Fill", fillColor: "band1FillColor", defMult: 1, defOn: true, defColor: BULLISH },
|
|
33563
|
+
{ on: "band2", mult: "band2Mult", color: "band2Color", fill: "band2Fill", fillColor: "band2FillColor", defMult: 2, defOn: false, defColor: WARNING },
|
|
33564
|
+
{ on: "band3", mult: "band3Mult", color: "band3Color", fill: "band3Fill", fillColor: "band3FillColor", defMult: 3, defOn: false, defColor: CATEGORICAL[4] }
|
|
33565
|
+
];
|
|
33118
33566
|
var vwap = {
|
|
33119
33567
|
type: "vwap",
|
|
33120
33568
|
title: "Volume Weighted Average Price",
|
|
33121
33569
|
shortTitle: "VWAP",
|
|
33122
33570
|
overlay: true,
|
|
33123
33571
|
inputs: [
|
|
33124
|
-
{ key: "anchor", title: "
|
|
33125
|
-
|
|
33126
|
-
|
|
33572
|
+
{ key: "anchor", title: "Period", type: "string", defval: "Day", options: ["Day", "Week", "Month", "Quarter", "Year"], tooltip: "Where the accumulation resets (UTC periods)" },
|
|
33573
|
+
sourceInput("HLC3"),
|
|
33574
|
+
// Each band is one row: its toggle leads, the multiplier follows unlabeled.
|
|
33575
|
+
...VWAP_BANDS.flatMap((b, i) => [
|
|
33576
|
+
{ key: b.on, title: `Band ${i + 1} multiplier`, type: "bool", defval: b.defOn, inline: b.on, group: VWAP_BANDS_GROUP },
|
|
33577
|
+
{ key: b.mult, title: "", type: "float", defval: b.defMult, min: 0.1, max: 10, step: 0.1, inline: b.on, group: VWAP_BANDS_GROUP }
|
|
33578
|
+
]),
|
|
33579
|
+
{ ...colorInput(INFO, "VWAP color"), group: VWAP_STYLE },
|
|
33580
|
+
// One row per band: its ink, then the fill toggle with the fill's own (translucent) color.
|
|
33581
|
+
...VWAP_BANDS.flatMap((b, i) => [
|
|
33582
|
+
{ ...colorInput(b.defColor, `Band ${i + 1} color`, b.color), inline: b.color, group: VWAP_STYLE },
|
|
33583
|
+
{ key: b.fill, title: "Fill", type: "bool", defval: true, inline: b.color, group: VWAP_STYLE },
|
|
33584
|
+
{ ...colorInput(withAlpha2(b.defColor), "", b.fillColor), inline: b.color, group: VWAP_STYLE }
|
|
33585
|
+
])
|
|
33127
33586
|
],
|
|
33128
33587
|
compute: (bars, inputs) => {
|
|
33129
33588
|
const anchor = str3(inputs, "anchor", "Day");
|
|
33130
|
-
const
|
|
33131
|
-
const
|
|
33589
|
+
const src = sourceValues(bars, str3(inputs, "source", "HLC3"));
|
|
33590
|
+
const n = bars.length;
|
|
33591
|
+
const values = new Array(n).fill(Number.NaN);
|
|
33592
|
+
const bands = VWAP_BANDS.filter((b) => bool(inputs, b.on, b.defOn)).map((b) => ({
|
|
33593
|
+
mult: Math.max(0, num3(inputs, b.mult, b.defMult)),
|
|
33594
|
+
color: str3(inputs, b.color, b.defColor),
|
|
33595
|
+
fill: bool(inputs, b.fill, true),
|
|
33596
|
+
fillColor: str3(inputs, b.fillColor, withAlpha2(b.defColor)),
|
|
33597
|
+
up: new Array(n).fill(Number.NaN),
|
|
33598
|
+
down: new Array(n).fill(Number.NaN)
|
|
33599
|
+
}));
|
|
33132
33600
|
let period = Number.NaN;
|
|
33133
33601
|
let cumPV = 0;
|
|
33602
|
+
let cumPV2 = 0;
|
|
33134
33603
|
let cumV = 0;
|
|
33135
|
-
for (let i = 0; i <
|
|
33604
|
+
for (let i = 0; i < n; i++) {
|
|
33136
33605
|
const b = bars[i];
|
|
33137
33606
|
const key = periodKey(anchor, b.time);
|
|
33138
33607
|
if (key !== period) {
|
|
33139
33608
|
period = key;
|
|
33140
33609
|
cumPV = 0;
|
|
33610
|
+
cumPV2 = 0;
|
|
33141
33611
|
cumV = 0;
|
|
33142
33612
|
}
|
|
33143
33613
|
const v = b.volume;
|
|
33144
33614
|
if (v != null && Number.isFinite(v) && v > 0) {
|
|
33145
|
-
const tp =
|
|
33615
|
+
const tp = src[i];
|
|
33146
33616
|
cumPV += tp * v;
|
|
33617
|
+
cumPV2 += tp * tp * v;
|
|
33147
33618
|
cumV += v;
|
|
33148
33619
|
}
|
|
33149
|
-
if (cumV
|
|
33150
|
-
|
|
33151
|
-
|
|
33620
|
+
if (cumV <= 0) continue;
|
|
33621
|
+
const mean = cumPV / cumV;
|
|
33622
|
+
const sd = Math.sqrt(Math.max(0, cumPV2 / cumV - mean * mean));
|
|
33623
|
+
values[i] = mean;
|
|
33624
|
+
for (const band of bands) {
|
|
33625
|
+
band.up[i] = mean + band.mult * sd;
|
|
33626
|
+
band.down[i] = mean - band.mult * sd;
|
|
33627
|
+
}
|
|
33628
|
+
}
|
|
33629
|
+
const plots = [{ key: "vwap", title: "VWAP", values, color: str3(inputs, "color", INFO), width: 2 }];
|
|
33630
|
+
const fills = [];
|
|
33631
|
+
bands.forEach((band, i) => {
|
|
33632
|
+
plots.push(
|
|
33633
|
+
{ key: `up${i}`, title: `Upper ${band.mult}\u03C3`, values: band.up, color: band.color },
|
|
33634
|
+
{ key: `down${i}`, title: `Lower ${band.mult}\u03C3`, values: band.down, color: band.color }
|
|
33635
|
+
);
|
|
33636
|
+
if (band.fill) fills.push({ key: `band${i}`, from: `up${i}`, to: `down${i}`, color: band.fillColor });
|
|
33637
|
+
});
|
|
33638
|
+
return { plots, bands: fills };
|
|
33152
33639
|
}
|
|
33153
33640
|
};
|
|
33154
33641
|
var pivotPoints = {
|
|
@@ -33339,21 +33826,11 @@ var Vela = class {
|
|
|
33339
33826
|
registerClassicIndicators();
|
|
33340
33827
|
const element = resolveElement(container);
|
|
33341
33828
|
const theme = resolveTheme(options.theme);
|
|
33342
|
-
let animZoom = true;
|
|
33343
|
-
let animPan = true;
|
|
33344
|
-
if (typeof options.animations === "boolean") {
|
|
33345
|
-
animZoom = options.animations;
|
|
33346
|
-
animPan = options.animations;
|
|
33347
|
-
} else if (options.animations) {
|
|
33348
|
-
animZoom = options.animations.zoom ?? true;
|
|
33349
|
-
animPan = options.animations.pan ?? true;
|
|
33350
|
-
}
|
|
33351
33829
|
const display = {
|
|
33352
33830
|
currentPriceLine: options.currentPriceLine ?? true,
|
|
33353
33831
|
logScale: options.logScale ?? false,
|
|
33354
33832
|
nativeBackend: options.nativeBackend ?? "auto",
|
|
33355
|
-
|
|
33356
|
-
animPan,
|
|
33833
|
+
...resolveAnimations(options.animations),
|
|
33357
33834
|
glow: options.glow ?? 0,
|
|
33358
33835
|
upColor: options.upColor ?? BULLISH,
|
|
33359
33836
|
downColor: options.downColor ?? BEARISH,
|