@luxalgo/vela 0.6.17 → 0.6.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{DataProvider-B-Jz59Rf.d.ts → DataProvider-Cb-2lC5O.d.ts} +1 -1
- package/dist/{DataProvider-OfR3fXg9.d.cts → DataProvider-UtWw2CgJ.d.cts} +1 -1
- package/dist/{chunk-YCD72KGK.js → chunk-22QJKGXX.js} +565 -151
- package/dist/{chunk-OSZDSH7F.js → chunk-627A5HEV.js} +252 -120
- package/dist/{contributions-C3b0Sx5P.d.cts → contributions-5nz3PLRe.d.ts} +9 -4
- package/dist/{contributions-Swzz10fG.d.ts → contributions-DEPRtkmG.d.cts} +9 -4
- package/dist/index.cjs +563 -149
- package/dist/index.d.cts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +1 -1
- package/dist/{options-CX1lSYWA.d.cts → options-BCRmYALw.d.cts} +12 -3
- package/dist/{options-CX1lSYWA.d.ts → options-BCRmYALw.d.ts} +12 -3
- package/dist/{plugin-mvddudqd.d.ts → plugin-CUtfLeqz.d.ts} +3 -3
- package/dist/{plugin-BjnEE1zA.d.cts → plugin-DVUaROPo.d.cts} +3 -3
- package/dist/plugin.d.cts +4 -4
- package/dist/plugin.d.ts +4 -4
- package/dist/providers/binance.d.cts +2 -2
- package/dist/providers/binance.d.ts +2 -2
- package/dist/providers/coinbase.d.cts +2 -2
- package/dist/providers/coinbase.d.ts +2 -2
- package/dist/providers/hyperliquid.d.cts +2 -2
- package/dist/providers/hyperliquid.d.ts +2 -2
- package/dist/{statusline-model-IURCya0H.d.ts → statusline-model-hY9cK4BS.d.cts} +17 -7
- package/dist/{statusline-model-D91cU2fF.d.cts → statusline-model-wW3m4in1.d.ts} +17 -7
- package/dist/ui.d.cts +1 -1
- package/dist/ui.d.ts +1 -1
- package/dist/vela.global.js +563 -149
- package/dist/vela.global.min.js +53 -51
- package/dist/widget.cjs +814 -268
- package/dist/widget.d.cts +39 -12
- package/dist/widget.d.ts +39 -12
- package/dist/widget.js +3 -3
- package/dist/workspace.cjs +814 -268
- package/dist/workspace.d.cts +14 -5
- package/dist/workspace.d.ts +14 -5
- package/dist/workspace.js +2 -2
- package/package.json +1 -1
package/dist/vela.global.js
CHANGED
|
@@ -6900,15 +6900,18 @@ var Vela = (function (exports) {
|
|
|
6900
6900
|
next2 = [...ids];
|
|
6901
6901
|
}
|
|
6902
6902
|
this.selectedIds = next2.filter((id) => this.store.has(id));
|
|
6903
|
+
this.announceSelection();
|
|
6904
|
+
}
|
|
6905
|
+
/** Push the selection to the renderer and announce it (primary + the full list). */
|
|
6906
|
+
announceSelection() {
|
|
6903
6907
|
this.port?.setSelection(this.selectedIds);
|
|
6904
|
-
this.events.emit("drawing:selected", { id: this.selectedIds[0] ?? null });
|
|
6908
|
+
this.events.emit("drawing:selected", { id: this.selectedIds[0] ?? null, ids: [...this.selectedIds] });
|
|
6905
6909
|
}
|
|
6906
6910
|
/** Restore a history snapshot, reconciling selection against what survived. */
|
|
6907
6911
|
restoreSnapshot(doc) {
|
|
6908
6912
|
this.store.load(doc);
|
|
6909
6913
|
this.selectedIds = this.selectedIds.filter((id) => this.store.has(id));
|
|
6910
|
-
this.
|
|
6911
|
-
this.events.emit("drawing:selected", { id: this.selectedIds[0] ?? null });
|
|
6914
|
+
this.announceSelection();
|
|
6912
6915
|
}
|
|
6913
6916
|
/** Delete drawings as one undo step; prune them from the selection. */
|
|
6914
6917
|
deleteMany(ids) {
|
|
@@ -7041,6 +7044,9 @@ var Vela = (function (exports) {
|
|
|
7041
7044
|
case "duplicate":
|
|
7042
7045
|
this.duplicate(i.ids);
|
|
7043
7046
|
break;
|
|
7047
|
+
case "clone":
|
|
7048
|
+
if (this.enabled && i.docs.length) this.insertClones(i.docs);
|
|
7049
|
+
break;
|
|
7044
7050
|
case "copy":
|
|
7045
7051
|
this.copy(i.ids);
|
|
7046
7052
|
break;
|
|
@@ -8877,7 +8883,12 @@ var Vela = (function (exports) {
|
|
|
8877
8883
|
if (!model) continue;
|
|
8878
8884
|
const paneId = model.paneId ?? "price";
|
|
8879
8885
|
if (!byPane.has(paneId)) byPane.set(paneId, []);
|
|
8880
|
-
byPane.get(paneId).push({
|
|
8886
|
+
byPane.get(paneId).push({
|
|
8887
|
+
id: r.id,
|
|
8888
|
+
title: model.title || r.title,
|
|
8889
|
+
...model.shorttitle ? { shorttitle: model.shorttitle } : {},
|
|
8890
|
+
ownScale: model.ownScale === true
|
|
8891
|
+
});
|
|
8881
8892
|
}
|
|
8882
8893
|
const panes = [];
|
|
8883
8894
|
for (const paneId of this.paneOrder) {
|
|
@@ -22948,15 +22959,15 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
|
|
|
22948
22959
|
if (!hasOwnCandlePaint(style)) return null;
|
|
22949
22960
|
const bag = bags[style] ?? {};
|
|
22950
22961
|
const color = (v) => isColor(v) ? v : null;
|
|
22951
|
-
const
|
|
22962
|
+
const bool2 = (v) => isBool(v) ? v : null;
|
|
22952
22963
|
return {
|
|
22953
22964
|
upColor: color(bag.candleUpColor),
|
|
22954
22965
|
downColor: color(bag.candleDownColor),
|
|
22955
|
-
bodyVisible:
|
|
22956
|
-
borderVisible:
|
|
22966
|
+
bodyVisible: bool2(bag.candleBodyVisible),
|
|
22967
|
+
borderVisible: bool2(bag.candleBorderVisible),
|
|
22957
22968
|
borderUpColor: color(bag.candleBorderUpColor),
|
|
22958
22969
|
borderDownColor: color(bag.candleBorderDownColor),
|
|
22959
|
-
wickVisible:
|
|
22970
|
+
wickVisible: bool2(bag.candleWickVisible),
|
|
22960
22971
|
wickUpColor: color(bag.candleWickUpColor),
|
|
22961
22972
|
wickDownColor: color(bag.candleWickDownColor)
|
|
22962
22973
|
};
|
|
@@ -24747,7 +24758,7 @@ void main() {
|
|
|
24747
24758
|
if (e.repeat || e.key !== "Shift" && e.key !== "Control" && e.key !== "Meta") return;
|
|
24748
24759
|
if (Number.isNaN(this.cursorX)) return;
|
|
24749
24760
|
if (this.dragging && this.region !== "drawing") return;
|
|
24750
|
-
this.deps.drawingsPointerMove?.(this.cursorX, this.cursorY, this.snapMode(e), e.shiftKey);
|
|
24761
|
+
this.deps.drawingsPointerMove?.(this.cursorX, this.cursorY, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
|
|
24751
24762
|
};
|
|
24752
24763
|
this.onDown = (e) => {
|
|
24753
24764
|
if (e.button === 1) {
|
|
@@ -24779,7 +24790,7 @@ void main() {
|
|
|
24779
24790
|
this.startY = y;
|
|
24780
24791
|
if (this.deps.drawingsClaim?.(x, y)) {
|
|
24781
24792
|
this.region = "drawing";
|
|
24782
|
-
this.deps.drawingsPointerDown?.(x, y, this.snapMode(e), e.shiftKey);
|
|
24793
|
+
this.deps.drawingsPointerDown?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
|
|
24783
24794
|
this.capture(e.pointerId);
|
|
24784
24795
|
return;
|
|
24785
24796
|
}
|
|
@@ -24788,6 +24799,11 @@ void main() {
|
|
|
24788
24799
|
this.capture(e.pointerId);
|
|
24789
24800
|
return;
|
|
24790
24801
|
}
|
|
24802
|
+
if ((e.ctrlKey || e.metaKey) && this.regionAt(x, y) === "data" && this.deps.drawingsMarqueeStart?.(x, y)) {
|
|
24803
|
+
this.region = "drawing";
|
|
24804
|
+
this.capture(e.pointerId);
|
|
24805
|
+
return;
|
|
24806
|
+
}
|
|
24791
24807
|
const vp = this.deps.getCoords().getViewport();
|
|
24792
24808
|
this.deps.apply(vp);
|
|
24793
24809
|
this.startRightOffset = vp.rightOffset;
|
|
@@ -24836,7 +24852,7 @@ void main() {
|
|
|
24836
24852
|
this.deps.apply({ barSpacing, rightOffset: this.startRightOffset });
|
|
24837
24853
|
} else if (this.region === "drawing") {
|
|
24838
24854
|
if (Math.abs(x - this.startX) > slop || Math.abs(y - this.startY) > slop) this.moved = true;
|
|
24839
|
-
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
|
|
24855
|
+
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
|
|
24840
24856
|
} else {
|
|
24841
24857
|
const coords = this.deps.getCoords();
|
|
24842
24858
|
const vp = coords.getViewport();
|
|
@@ -24859,7 +24875,7 @@ void main() {
|
|
|
24859
24875
|
const drawCursor = this.deps.drawingsCursor?.(x, y);
|
|
24860
24876
|
const r = this.regionAt(x, y);
|
|
24861
24877
|
this.el.style.cursor = drawCursor ?? (r === "price" ? "ns-resize" : r === "time" ? "ew-resize" : r === "separator" ? "row-resize" : "");
|
|
24862
|
-
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
|
|
24878
|
+
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
|
|
24863
24879
|
}
|
|
24864
24880
|
if (e.pointerType !== "touch") this.deps.onPointerMove(x, y);
|
|
24865
24881
|
};
|
|
@@ -25200,12 +25216,12 @@ void main() {
|
|
|
25200
25216
|
function mergeTradeMarkersState(base, patch) {
|
|
25201
25217
|
const p = patch && typeof patch === "object" ? patch : {};
|
|
25202
25218
|
const c = p.colors && typeof p.colors === "object" ? p.colors : {};
|
|
25203
|
-
const
|
|
25219
|
+
const bool2 = (v, fb) => typeof v === "boolean" ? v : fb;
|
|
25204
25220
|
const color = (v, fb) => typeof v === "string" && v.trim().length > 0 ? v : fb;
|
|
25205
25221
|
return {
|
|
25206
|
-
visible:
|
|
25207
|
-
labels:
|
|
25208
|
-
qty:
|
|
25222
|
+
visible: bool2(p.visible, base.visible),
|
|
25223
|
+
labels: bool2(p.labels, base.labels),
|
|
25224
|
+
qty: bool2(p.qty, base.qty),
|
|
25209
25225
|
colors: {
|
|
25210
25226
|
long: color(c.long, base.colors.long),
|
|
25211
25227
|
short: color(c.short, base.colors.short),
|
|
@@ -29155,6 +29171,18 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
29155
29171
|
}
|
|
29156
29172
|
this.paintHandles(ctx, pts);
|
|
29157
29173
|
}
|
|
29174
|
+
/** The selection box a Ctrl/Cmd-drag on the empty plot sweeps: a faint fill in the handle
|
|
29175
|
+
* accent under a thin dashed outline. */
|
|
29176
|
+
paintMarquee(ctx, rect) {
|
|
29177
|
+
ctx.save();
|
|
29178
|
+
ctx.fillStyle = withAlpha(HANDLE_BORDER, 0.08);
|
|
29179
|
+
ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
|
|
29180
|
+
ctx.strokeStyle = HANDLE_BORDER;
|
|
29181
|
+
ctx.lineWidth = 1;
|
|
29182
|
+
ctx.setLineDash([4, 3]);
|
|
29183
|
+
ctx.strokeRect(rect.x + 0.5, rect.y + 0.5, rect.w, rect.h);
|
|
29184
|
+
ctx.restore();
|
|
29185
|
+
}
|
|
29158
29186
|
/** A hollow ring marking the candle point the magnet (Ctrl) will snap the next anchor to. */
|
|
29159
29187
|
paintSnapRing(ctx, x, y, theme) {
|
|
29160
29188
|
ctx.setLineDash([]);
|
|
@@ -30623,6 +30651,15 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
30623
30651
|
}
|
|
30624
30652
|
return null;
|
|
30625
30653
|
}
|
|
30654
|
+
function deletableSelection(selected, drawings) {
|
|
30655
|
+
const ids = [...selected];
|
|
30656
|
+
if (ids.length < 2) return ids;
|
|
30657
|
+
return ids.filter((id) => !drawings.find((d) => d.id === id)?.locked);
|
|
30658
|
+
}
|
|
30659
|
+
function deleteTargets(hit, selected, drawings, withSelection) {
|
|
30660
|
+
if (withSelection && selected.size >= 2 && selected.has(hit.id)) return deletableSelection(selected, drawings);
|
|
30661
|
+
return hit.locked ? [] : [hit.id];
|
|
30662
|
+
}
|
|
30626
30663
|
|
|
30627
30664
|
// src/renderers/native/drawings/DrawingInteraction.ts
|
|
30628
30665
|
var DRAG_SLOP2 = 3;
|
|
@@ -30726,10 +30763,32 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
30726
30763
|
activeDragId() {
|
|
30727
30764
|
return this.state.kind === "pressed" && this.state.moved ? this.state.id : null;
|
|
30728
30765
|
}
|
|
30766
|
+
/** The drawing under a press that has not been released yet (drag or click undecided), or null. */
|
|
30767
|
+
pressedId() {
|
|
30768
|
+
return this.state.kind === "pressed" ? this.state.id : null;
|
|
30769
|
+
}
|
|
30770
|
+
/** The copies a Ctrl-drag is moving (transient — not yet in the store), or null. */
|
|
30771
|
+
dragClones() {
|
|
30772
|
+
return this.state.kind === "pressed" && this.state.moved && this.state.clones ? this.state.clones.map((c) => c.d) : null;
|
|
30773
|
+
}
|
|
30774
|
+
/** The marquee box being swept, normalized (or null when none is in progress). */
|
|
30775
|
+
marqueeRect() {
|
|
30776
|
+
if (this.state.kind !== "marquee") return null;
|
|
30777
|
+
const { x0, y0, x1, y1 } = this.state;
|
|
30778
|
+
return { x: Math.min(x0, x1), y: Math.min(y0, y1), w: Math.abs(x1 - x0), h: Math.abs(y1 - y0) };
|
|
30779
|
+
}
|
|
30780
|
+
/** Ctrl/Cmd-press on the empty plot: start sweeping a selection box. False when a tool is
|
|
30781
|
+
* armed or a gesture is in flight (that path owns the press). */
|
|
30782
|
+
beginMarquee(x, y) {
|
|
30783
|
+
if (this.state.kind !== "idle" || this.deps.activeTool() != null) return false;
|
|
30784
|
+
this.state = { kind: "marquee", x0: x, y0: y, x1: x, y1: y };
|
|
30785
|
+
return true;
|
|
30786
|
+
}
|
|
30729
30787
|
/** A press: place the next anchor, or grab a drawing/handle (resolved as click or drag on release).
|
|
30730
30788
|
* `shift` over a drawing toggles it in/out of the selection (multi-select) instead of dragging;
|
|
30731
|
-
* while placing a line tool it locks the segment angle to 45° steps.
|
|
30732
|
-
|
|
30789
|
+
* while placing a line tool it locks the segment angle to 45° steps. `mod` (Ctrl/Cmd) over a
|
|
30790
|
+
* drawing also toggles on a click, but a body drag then moves a COPY. */
|
|
30791
|
+
down(x, y, mode = "off", shift4 = false, mod = false) {
|
|
30733
30792
|
const proj = this.deps.projector();
|
|
30734
30793
|
if (this.state.kind === "placing") {
|
|
30735
30794
|
if (this.state.draft.placementMode() !== "click") return;
|
|
@@ -30757,7 +30816,50 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
30757
30816
|
return;
|
|
30758
30817
|
}
|
|
30759
30818
|
const handle = hit.hitHandle(x, y, proj, HIT_TOLERANCE);
|
|
30760
|
-
|
|
30819
|
+
const riders = [];
|
|
30820
|
+
if (handle < 0 && this.deps.selectedIds().has(hit.id)) {
|
|
30821
|
+
for (const id of this.deps.selectedIds()) {
|
|
30822
|
+
const d = id === hit.id ? null : this.byId(id);
|
|
30823
|
+
if (d && !d.locked && d.visible) riders.push({ id, orig: cloneAnchors(d) });
|
|
30824
|
+
}
|
|
30825
|
+
}
|
|
30826
|
+
this.state = { kind: "pressed", id: hit.id, handle, orig: cloneAnchors(hit), px: x, py: y, moved: false, riders, mod, clones: null };
|
|
30827
|
+
}
|
|
30828
|
+
}
|
|
30829
|
+
/** The data-space delta a body drag from the press to (x, y) means on `paneId` — resolved per
|
|
30830
|
+
* pane so drawings riding along on another pane move by the same PIXELS, not the same price. */
|
|
30831
|
+
bodyDelta(paneId, x, y) {
|
|
30832
|
+
if (this.state.kind !== "pressed") return { dt: 0, dp: 0 };
|
|
30833
|
+
const proj = this.deps.projector();
|
|
30834
|
+
const from = proj.pxToPoint(this.state.px, this.state.py, paneId);
|
|
30835
|
+
const to = proj.pxToPoint(x, y, paneId);
|
|
30836
|
+
return { dt: to.time - from.time, dp: to.price - from.price };
|
|
30837
|
+
}
|
|
30838
|
+
/** The pressed drawing and its riders, each with its anchors at the press. */
|
|
30839
|
+
dragSources(d) {
|
|
30840
|
+
if (this.state.kind !== "pressed") return [];
|
|
30841
|
+
const out = [{ d, orig: this.state.orig }];
|
|
30842
|
+
for (const r of this.state.riders) {
|
|
30843
|
+
const rd = this.byId(r.id);
|
|
30844
|
+
if (rd) out.push({ d: rd, orig: r.orig });
|
|
30845
|
+
}
|
|
30846
|
+
return out;
|
|
30847
|
+
}
|
|
30848
|
+
/** Move the pressed drawing's body (and its riders) — or, on a Ctrl-drag, their copies — to the
|
|
30849
|
+
* cursor. The copies are built on the first move (so a Ctrl-click never creates anything), from
|
|
30850
|
+
* sources that have not moved yet, so their own anchors are the origin to translate from. */
|
|
30851
|
+
dragBody(d, x, y) {
|
|
30852
|
+
if (this.state.kind !== "pressed") return;
|
|
30853
|
+
this.snapAt = null;
|
|
30854
|
+
if (this.state.mod && !this.state.clones) {
|
|
30855
|
+
this.state.clones = this.dragSources(d).flatMap((s) => {
|
|
30856
|
+
const c = deserializeDrawing(s.d.serialize());
|
|
30857
|
+
return c ? [{ d: c, orig: cloneAnchors(c) }] : [];
|
|
30858
|
+
});
|
|
30859
|
+
}
|
|
30860
|
+
for (const t of this.state.clones ?? this.dragSources(d)) {
|
|
30861
|
+
const { dt, dp } = this.bodyDelta(t.d.paneId, x, y);
|
|
30862
|
+
t.d.anchors = t.d.translateBody(dt, dp, t.orig);
|
|
30761
30863
|
}
|
|
30762
30864
|
}
|
|
30763
30865
|
/** Pointer move: advance the placing ghost, or live-preview a drag once past the slop.
|
|
@@ -30781,12 +30883,20 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
30781
30883
|
this.deps.changed();
|
|
30782
30884
|
return;
|
|
30783
30885
|
}
|
|
30886
|
+
if (this.state.kind === "marquee") {
|
|
30887
|
+
this.state.x1 = x;
|
|
30888
|
+
this.state.y1 = y;
|
|
30889
|
+
this.deps.changed();
|
|
30890
|
+
return;
|
|
30891
|
+
}
|
|
30784
30892
|
if (this.state.kind !== "pressed") return;
|
|
30785
30893
|
const d = this.byId(this.state.id);
|
|
30786
30894
|
if (!d || d.locked) return;
|
|
30787
30895
|
if (!this.state.moved && Math.abs(x - this.state.px) <= DRAG_SLOP2 && Math.abs(y - this.state.py) <= DRAG_SLOP2) return;
|
|
30788
30896
|
this.state.moved = true;
|
|
30789
|
-
if (this.state.handle
|
|
30897
|
+
if (this.state.handle < 0) {
|
|
30898
|
+
this.dragBody(d, x, y);
|
|
30899
|
+
} else {
|
|
30790
30900
|
const a = d.anchors[this.state.handle];
|
|
30791
30901
|
if (a) {
|
|
30792
30902
|
const pivot = shift4 && ANGLE_SNAP_TYPES.has(d.type) && d.anchors.length === 2 ? d.anchors[1 - this.state.handle] : void 0;
|
|
@@ -30794,12 +30904,6 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
30794
30904
|
applyFree(a, point, d.anchorSchema().slots[this.state.handle]?.free ?? "both");
|
|
30795
30905
|
}
|
|
30796
30906
|
d.constrainHandleDrag(this.state.handle);
|
|
30797
|
-
} else {
|
|
30798
|
-
const pt2 = this.deps.projector().pxToPoint(x, y, d.paneId);
|
|
30799
|
-
this.snapAt = null;
|
|
30800
|
-
const dt = pt2.time - this.state.grab.time;
|
|
30801
|
-
const dp = pt2.price - this.state.grab.price;
|
|
30802
|
-
d.anchors = d.translateBody(dt, dp, this.state.orig);
|
|
30803
30907
|
}
|
|
30804
30908
|
this.deps.changed();
|
|
30805
30909
|
}
|
|
@@ -30819,16 +30923,46 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
30819
30923
|
}
|
|
30820
30924
|
return;
|
|
30821
30925
|
}
|
|
30926
|
+
if (this.state.kind === "marquee") {
|
|
30927
|
+
const rect = this.marqueeRect();
|
|
30928
|
+
this.state = { kind: "idle" };
|
|
30929
|
+
if (rect.w > DRAG_SLOP2 || rect.h > DRAG_SLOP2) this.selectWithin(rect);
|
|
30930
|
+
this.deps.changed();
|
|
30931
|
+
return;
|
|
30932
|
+
}
|
|
30822
30933
|
if (this.state.kind !== "pressed") return;
|
|
30823
|
-
const { id, moved } = this.state;
|
|
30934
|
+
const { id, moved, riders, mod, clones } = this.state;
|
|
30824
30935
|
this.state = { kind: "idle" };
|
|
30825
30936
|
const d = this.byId(id);
|
|
30826
|
-
if (moved) {
|
|
30827
|
-
if (
|
|
30828
|
-
this.deps.
|
|
30829
|
-
|
|
30830
|
-
|
|
30937
|
+
if (!moved) {
|
|
30938
|
+
if (mod) this.deps.emit({ kind: "select", ids: [id], additive: true });
|
|
30939
|
+
else this.deps.openSettings(id, x, y);
|
|
30940
|
+
return;
|
|
30941
|
+
}
|
|
30942
|
+
if (clones) {
|
|
30943
|
+
this.deps.emit({ kind: "clone", docs: clones.map((c) => c.d.serialize()) });
|
|
30944
|
+
} else if (d) {
|
|
30945
|
+
const docs = [d.serialize()];
|
|
30946
|
+
for (const r of riders) {
|
|
30947
|
+
const rd = this.byId(r.id);
|
|
30948
|
+
if (rd) docs.push(rd.serialize());
|
|
30949
|
+
}
|
|
30950
|
+
if (docs.length > 1) this.deps.emit({ kind: "edit-many", docs });
|
|
30951
|
+
else this.deps.emit({ kind: "edit", doc: docs[0] });
|
|
30952
|
+
}
|
|
30953
|
+
this.deps.changed();
|
|
30954
|
+
}
|
|
30955
|
+
/** Add every visible drawing whose on-screen bounds touch `rect` to the selection (a union,
|
|
30956
|
+
* so successive sweeps accumulate). No change when the box touches nothing new. */
|
|
30957
|
+
selectWithin(rect) {
|
|
30958
|
+
const proj = this.deps.projector();
|
|
30959
|
+
const ids = [...this.deps.selectedIds()];
|
|
30960
|
+
for (const d of this.deps.drawings()) {
|
|
30961
|
+
if (!d.visible || ids.includes(d.id)) continue;
|
|
30962
|
+
const b = d.bounds(proj);
|
|
30963
|
+
if (b && rectsIntersect(rect, b)) ids.push(d.id);
|
|
30831
30964
|
}
|
|
30965
|
+
if (ids.length > this.deps.selectedIds().size) this.deps.emit({ kind: "select", ids });
|
|
30832
30966
|
}
|
|
30833
30967
|
/** Cancel an in-progress placement or drag (Escape). Returns whether anything was cancelled. */
|
|
30834
30968
|
cancel() {
|
|
@@ -30841,8 +30975,15 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
30841
30975
|
return true;
|
|
30842
30976
|
}
|
|
30843
30977
|
if (this.state.kind === "pressed") {
|
|
30844
|
-
|
|
30845
|
-
|
|
30978
|
+
if (this.state.moved && !this.state.clones) {
|
|
30979
|
+
const d = this.byId(this.state.id);
|
|
30980
|
+
if (d) for (const s of this.dragSources(d)) s.d.anchors = s.orig.map((o) => ({ time: o.time, price: o.price }));
|
|
30981
|
+
}
|
|
30982
|
+
this.state = { kind: "idle" };
|
|
30983
|
+
this.deps.changed();
|
|
30984
|
+
return true;
|
|
30985
|
+
}
|
|
30986
|
+
if (this.state.kind === "marquee") {
|
|
30846
30987
|
this.state = { kind: "idle" };
|
|
30847
30988
|
this.deps.changed();
|
|
30848
30989
|
return true;
|
|
@@ -30957,6 +31098,9 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
30957
31098
|
function cloneAnchors(d) {
|
|
30958
31099
|
return d.anchors.map((a) => ({ time: a.time, price: a.price }));
|
|
30959
31100
|
}
|
|
31101
|
+
function rectsIntersect(a, b) {
|
|
31102
|
+
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;
|
|
31103
|
+
}
|
|
30960
31104
|
function applyFree(anchor, pt2, free) {
|
|
30961
31105
|
if (free === "both" || free === "x") anchor.time = pt2.time;
|
|
30962
31106
|
if (free === "both" || free === "y") anchor.price = pt2.price;
|
|
@@ -31342,10 +31486,27 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
31342
31486
|
}
|
|
31343
31487
|
|
|
31344
31488
|
// src/renderers/native/drawings/DrawingSettingsPopup.ts
|
|
31489
|
+
var MIXED = /* @__PURE__ */ Symbol("mixed");
|
|
31490
|
+
function sharedPaths(drawings) {
|
|
31491
|
+
const [first2, ...rest] = drawings;
|
|
31492
|
+
const paths = new Set(first2?.schema().fields.map((f) => f.path) ?? []);
|
|
31493
|
+
for (const d of rest) {
|
|
31494
|
+
const own = new Set(d.schema().fields.map((f) => f.path));
|
|
31495
|
+
for (const p of paths) if (!own.has(p)) paths.delete(p);
|
|
31496
|
+
}
|
|
31497
|
+
return paths;
|
|
31498
|
+
}
|
|
31499
|
+
function commonValue(drawings, read) {
|
|
31500
|
+
const first2 = read(drawings[0]);
|
|
31501
|
+
return drawings.every((d) => read(d) === first2) ? first2 : MIXED;
|
|
31502
|
+
}
|
|
31503
|
+
function distinctValues(drawings, read) {
|
|
31504
|
+
return [...new Set(drawings.map(read))];
|
|
31505
|
+
}
|
|
31345
31506
|
var BTN = 30;
|
|
31346
31507
|
var ICON = 21;
|
|
31347
31508
|
var STYLE_ID3 = "vela-drawing-popup-styles";
|
|
31348
|
-
var STYLE_REV = "
|
|
31509
|
+
var STYLE_REV = "4";
|
|
31349
31510
|
function ensureStyles3() {
|
|
31350
31511
|
if (typeof document === "undefined") return;
|
|
31351
31512
|
const existing = document.getElementById(STYLE_ID3);
|
|
@@ -31357,6 +31518,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
31357
31518
|
.vela-dpop-btn{background:transparent;color:var(--vela-fg-muted);transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
|
|
31358
31519
|
.vela-dpop-btn:hover{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
|
|
31359
31520
|
.vela-dpop-btn[data-active='1']{background:var(--vela-active);color:var(--vela-fg-bright);}
|
|
31521
|
+
/* Mixed (a multi-selection disagrees): the active fill at half strength behind a dashed ring. */
|
|
31522
|
+
.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;}
|
|
31360
31523
|
.vela-dpop-item{background:transparent;transition:background var(--vela-dur-fast) ease;}
|
|
31361
31524
|
.vela-dpop-item:hover{background:var(--vela-hover-strong);}
|
|
31362
31525
|
.vela-dpop-item[data-active='1']{background:var(--vela-active);}
|
|
@@ -31388,7 +31551,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
31388
31551
|
if (this.isOwnChrome(node)) return;
|
|
31389
31552
|
const cb = this.onClose;
|
|
31390
31553
|
this.close();
|
|
31391
|
-
cb?.();
|
|
31554
|
+
cb?.(e);
|
|
31392
31555
|
};
|
|
31393
31556
|
this.theme = theme;
|
|
31394
31557
|
this.settingsDialog = new DrawingSettingsDialog(host, theme);
|
|
@@ -31413,16 +31576,27 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
31413
31576
|
contains(node) {
|
|
31414
31577
|
return node != null && (this.isOwnChrome(node) || this.settingsDialog.contains(node));
|
|
31415
31578
|
}
|
|
31416
|
-
/** Open the quick toolbar for `
|
|
31417
|
-
* `
|
|
31418
|
-
|
|
31579
|
+
/** Open the quick toolbar for `drawings` (one, or a whole multi-selection), floating clear of
|
|
31580
|
+
* their `anchor` box. With several drawings the bar shows only the controls they ALL support,
|
|
31581
|
+
* a control whose values differ reads as mixed, and every edit applies to all of them —
|
|
31582
|
+
* per-drawing panels (levels, position sizing, the text field) stay single-drawing only.
|
|
31583
|
+
* `onClose` fires on a dismissal (an outside press, or Escape in the text field — not a
|
|
31584
|
+
* programmatic close), with the dismissing press when there is one, so the caller can tell a
|
|
31585
|
+
* modifier press (multi-select) from a plain one. */
|
|
31586
|
+
open(drawings, anchor, actions, onClose) {
|
|
31419
31587
|
this.close();
|
|
31588
|
+
const drawing = drawings[0];
|
|
31589
|
+
if (!drawing) return;
|
|
31420
31590
|
ensureStyles3();
|
|
31421
31591
|
this.onClose = onClose ?? null;
|
|
31422
31592
|
const t = this.theme;
|
|
31593
|
+
const multi = drawings.length > 1;
|
|
31423
31594
|
const schema = drawing.schema();
|
|
31424
|
-
const paths =
|
|
31425
|
-
const
|
|
31595
|
+
const paths = sharedPaths(drawings);
|
|
31596
|
+
const common = (read) => commonValue(drawings, read);
|
|
31597
|
+
const every = (pred) => drawings.every(pred);
|
|
31598
|
+
const swatch = (tip, glyph, read, path) => this.colorButton(tip, glyph, common(read), (v) => actions.patch({ [path]: v }), void 0, distinctValues(drawings, read));
|
|
31599
|
+
const textOnBar = multi || schema.textIsContent === true || !paths.has("text.value");
|
|
31426
31600
|
const el = document.createElement("div");
|
|
31427
31601
|
el.className = "vela-dpop";
|
|
31428
31602
|
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;`;
|
|
@@ -31436,75 +31610,84 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
31436
31610
|
bar.style.cssText = "display:flex;align-items:center;gap:2px;padding:4px;";
|
|
31437
31611
|
bar.appendChild(this.dragHandle());
|
|
31438
31612
|
if (paths.has("glyph")) {
|
|
31439
|
-
const cur =
|
|
31613
|
+
const cur = common((d) => d.glyph ?? GLYPH_OPTIONS[0]);
|
|
31440
31614
|
bar.appendChild(this.dropdown("Icon", GLYPH_OPTIONS, cur, (g) => glyphIcon(g), (v) => actions.patch({ glyph: v })));
|
|
31441
31615
|
}
|
|
31442
31616
|
if (paths.has("size")) {
|
|
31443
|
-
const sz =
|
|
31617
|
+
const sz = common((d) => d.size ?? "normal");
|
|
31444
31618
|
bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label: sizeLabel }));
|
|
31445
31619
|
}
|
|
31446
|
-
if (paths.has("magnifier.timeframe") &&
|
|
31620
|
+
if (paths.has("magnifier.timeframe") && every((d) => d instanceof Magnifier)) {
|
|
31621
|
+
const mag = (d) => d;
|
|
31447
31622
|
const options = this.lowerTimeframeOptions();
|
|
31448
31623
|
if (options.length > 0) {
|
|
31449
31624
|
bar.appendChild(
|
|
31450
|
-
this.dropdown("Lower timeframe", options.map((o) => o.value),
|
|
31625
|
+
this.dropdown("Lower timeframe", options.map((o) => o.value), common((d) => mag(d).magnifier.timeframe), () => "", (v) => actions.patch({ "magnifier.timeframe": v }), {
|
|
31451
31626
|
label: (v) => magnifierTimeframeLabel(String(v)),
|
|
31452
31627
|
labelInTrigger: true
|
|
31453
31628
|
})
|
|
31454
31629
|
);
|
|
31455
31630
|
}
|
|
31456
|
-
bar.appendChild(
|
|
31457
|
-
bar.appendChild(
|
|
31631
|
+
bar.appendChild(swatch("Up candles", BUCKET_ICON, (d) => mag(d).magnifier.upColor || t.upColor, "magnifier.upColor"));
|
|
31632
|
+
bar.appendChild(swatch("Down candles", BUCKET_ICON, (d) => mag(d).magnifier.downColor || t.downColor, "magnifier.downColor"));
|
|
31458
31633
|
}
|
|
31459
|
-
if (paths.has("style.lineColor")) bar.appendChild(
|
|
31634
|
+
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"));
|
|
31460
31635
|
if (paths.has("style.lineWidth")) {
|
|
31461
|
-
const
|
|
31462
|
-
|
|
31463
|
-
|
|
31636
|
+
const widthField2 = (d) => d.schema().fields.find((f) => f.path === "style.lineWidth");
|
|
31637
|
+
const wf = widthField2(drawing);
|
|
31638
|
+
const width = common((d) => d.style.lineWidth);
|
|
31639
|
+
const markers = every((d) => {
|
|
31640
|
+
const f = widthField2(d);
|
|
31641
|
+
return f?.kind === "number" && (f.min ?? 1) > 1;
|
|
31642
|
+
});
|
|
31643
|
+
if (markers && wf?.kind === "number") {
|
|
31644
|
+
bar.appendChild(this.widthInput("Line width", width, wf.min ?? 1, wf.max ?? 60, wf.step ?? 1, (v) => actions.patch({ "style.lineWidth": v })));
|
|
31464
31645
|
} else {
|
|
31465
|
-
bar.appendChild(this.dropdown("Line width", [1, 2, 3, 4],
|
|
31646
|
+
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 }));
|
|
31466
31647
|
}
|
|
31467
31648
|
}
|
|
31468
|
-
if (paths.has("style.lineStyle")) bar.appendChild(this.dropdown("Line style", LINE_STYLE_OPTIONS.map((o) => o.value),
|
|
31469
|
-
if (paths.has("style.fillColor")) bar.appendChild(
|
|
31649
|
+
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 }));
|
|
31650
|
+
if (paths.has("style.fillColor")) bar.appendChild(swatch("Fill", BUCKET_ICON, (d) => effectiveFillColor(d, this.theme) ?? d.style.fillColor ?? DEFAULT_DRAWING_COLOR, "style.fillColor"));
|
|
31470
31651
|
const isFrvp = paths.has("frvp.rows") && drawing instanceof FixedRangeVolumeProfile;
|
|
31471
|
-
const isPosition = paths.has("riskPercent") &&
|
|
31652
|
+
const isPosition = paths.has("riskPercent") && every((d) => d instanceof PositionTool);
|
|
31472
31653
|
if (isPosition) {
|
|
31473
|
-
const pos =
|
|
31474
|
-
bar.appendChild(
|
|
31475
|
-
bar.appendChild(
|
|
31654
|
+
const pos = (d) => d;
|
|
31655
|
+
bar.appendChild(swatch("Profit zone", BUCKET_ICON, (d) => pos(d).profitColor, "profitColor"));
|
|
31656
|
+
bar.appendChild(swatch("Loss zone", BUCKET_ICON, (d) => pos(d).lossColor, "lossColor"));
|
|
31476
31657
|
}
|
|
31477
|
-
const
|
|
31478
|
-
if (paths.has("reg.midColor") &&
|
|
31658
|
+
const regOf = (d) => d.reg;
|
|
31659
|
+
if (paths.has("reg.midColor") && every((d) => regOf(d) != null)) {
|
|
31660
|
+
const reg = (d) => regOf(d);
|
|
31479
31661
|
const styles = LINE_STYLE_OPTIONS.map((o) => o.value);
|
|
31480
|
-
bar.appendChild(
|
|
31481
|
-
bar.appendChild(this.dropdown("Midline style", styles, reg.midStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "reg.midStyle": v }), { label: styleLabel2 }));
|
|
31482
|
-
bar.appendChild(
|
|
31483
|
-
bar.appendChild(this.dropdown("Upper line style", styles, reg.upperStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "reg.upperStyle": v }), { label: styleLabel2 }));
|
|
31484
|
-
bar.appendChild(
|
|
31485
|
-
bar.appendChild(this.dropdown("Lower line style", styles, reg.lowerStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "reg.lowerStyle": v }), { label: styleLabel2 }));
|
|
31486
|
-
bar.appendChild(
|
|
31487
|
-
bar.appendChild(
|
|
31488
|
-
bar.appendChild(this.toggle("Show R\xB2", R2_ICON, reg.showR2, (v) => actions.patch({ "reg.showR2": v })));
|
|
31489
|
-
}
|
|
31490
|
-
const
|
|
31491
|
-
if (paths.has("vwap.midColor") &&
|
|
31662
|
+
bar.appendChild(swatch("Midline color", BRUSH_ICON, (d) => reg(d).midColor, "reg.midColor"));
|
|
31663
|
+
bar.appendChild(this.dropdown("Midline style", styles, common((d) => reg(d).midStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "reg.midStyle": v }), { label: styleLabel2 }));
|
|
31664
|
+
bar.appendChild(swatch("Upper line color", BRUSH_ICON, (d) => reg(d).upperColor, "reg.upperColor"));
|
|
31665
|
+
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 }));
|
|
31666
|
+
bar.appendChild(swatch("Lower line color", BRUSH_ICON, (d) => reg(d).lowerColor, "reg.lowerColor"));
|
|
31667
|
+
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 }));
|
|
31668
|
+
bar.appendChild(swatch("Upper fill", BUCKET_ICON, (d) => reg(d).upperFill, "reg.upperFill"));
|
|
31669
|
+
bar.appendChild(swatch("Lower fill", BUCKET_ICON, (d) => reg(d).lowerFill, "reg.lowerFill"));
|
|
31670
|
+
bar.appendChild(this.toggle("Show R\xB2", R2_ICON, common((d) => reg(d).showR2), (v) => actions.patch({ "reg.showR2": v })));
|
|
31671
|
+
}
|
|
31672
|
+
const vwapOf = (d) => d.vwap;
|
|
31673
|
+
if (paths.has("vwap.midColor") && every((d) => vwapOf(d) != null)) {
|
|
31674
|
+
const vwap2 = (d) => vwapOf(d);
|
|
31492
31675
|
const styles = LINE_STYLE_OPTIONS.map((o) => o.value);
|
|
31493
31676
|
const MULTS = [0.5, 1, 1.5, 2, 2.5, 3, 4, 5];
|
|
31494
|
-
bar.appendChild(
|
|
31495
|
-
bar.appendChild(this.dropdown("VWAP style", styles, vwap2.midStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "vwap.midStyle": v }), { label: styleLabel2 }));
|
|
31677
|
+
bar.appendChild(swatch("VWAP color", BRUSH_ICON, (d) => vwap2(d).midColor, "vwap.midColor"));
|
|
31678
|
+
bar.appendChild(this.dropdown("VWAP style", styles, common((d) => vwap2(d).midStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "vwap.midStyle": v }), { label: styleLabel2 }));
|
|
31496
31679
|
bar.appendChild(
|
|
31497
|
-
this.dropdown("Band multiplier", MULTS, vwap2.multiplier, () => BANDS_ICON, (v) => actions.patch({ "vwap.multiplier": v }), {
|
|
31680
|
+
this.dropdown("Band multiplier", MULTS, common((d) => vwap2(d).multiplier), () => BANDS_ICON, (v) => actions.patch({ "vwap.multiplier": v }), {
|
|
31498
31681
|
label: (v) => `${v}\u03C3`,
|
|
31499
31682
|
labelInTrigger: true
|
|
31500
31683
|
})
|
|
31501
31684
|
);
|
|
31502
|
-
bar.appendChild(
|
|
31503
|
-
bar.appendChild(
|
|
31504
|
-
bar.appendChild(
|
|
31685
|
+
bar.appendChild(swatch("Upper band color", BRUSH_ICON, (d) => vwap2(d).upperColor, "vwap.upperColor"));
|
|
31686
|
+
bar.appendChild(swatch("Lower band color", BRUSH_ICON, (d) => vwap2(d).lowerColor, "vwap.lowerColor"));
|
|
31687
|
+
bar.appendChild(swatch("Band fill", BUCKET_ICON, (d) => vwap2(d).bandFill, "vwap.bandFill"));
|
|
31505
31688
|
}
|
|
31506
31689
|
if (paths.has("maxCurvature")) {
|
|
31507
|
-
const cur =
|
|
31690
|
+
const cur = common((d) => d.maxCurvature ?? 24);
|
|
31508
31691
|
bar.appendChild(
|
|
31509
31692
|
this.dropdown("Max curvature", DEDEKIND_CURVATURE_OPTIONS, cur, () => DEDEKIND_ICON2, (v) => actions.patch({ maxCurvature: v }), {
|
|
31510
31693
|
label: (v) => `n=${v}`,
|
|
@@ -31513,7 +31696,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
31513
31696
|
);
|
|
31514
31697
|
}
|
|
31515
31698
|
if (paths.has("mach")) {
|
|
31516
|
-
const cur =
|
|
31699
|
+
const cur = common((d) => d.mach ?? 2);
|
|
31517
31700
|
bar.appendChild(
|
|
31518
31701
|
this.dropdown("Mach number", MACH_NUMBER_OPTIONS, cur, () => SUPERSONIC_ICON2, (v) => actions.patch({ mach: v }), {
|
|
31519
31702
|
label: (v) => `M=${v}`,
|
|
@@ -31522,7 +31705,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
31522
31705
|
);
|
|
31523
31706
|
}
|
|
31524
31707
|
if (paths.has("waveCount")) {
|
|
31525
|
-
const cur =
|
|
31708
|
+
const cur = common((d) => d.waveCount ?? 6);
|
|
31526
31709
|
bar.appendChild(
|
|
31527
31710
|
this.dropdown("Waves", MACH_WAVE_COUNT_OPTIONS, cur, () => SONIC_ICON2, (v) => actions.patch({ waveCount: v }), {
|
|
31528
31711
|
label: (v) => `${v}`,
|
|
@@ -31530,26 +31713,27 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
31530
31713
|
})
|
|
31531
31714
|
);
|
|
31532
31715
|
}
|
|
31533
|
-
const
|
|
31534
|
-
if (paths.has("showPrice")) bar.appendChild(this.toggle("Show price", PRICE_DELTA_ICON,
|
|
31535
|
-
if (paths.has("showDate")) bar.appendChild(this.toggle("Show date", DATE_DELTA_ICON,
|
|
31536
|
-
if (paths.has("text.color") && textOnBar) bar.appendChild(
|
|
31537
|
-
if (paths.has("text.size") && textOnBar) bar.appendChild(this.dropdown("Text size", TEXT_SIZE_OPTIONS.map((o) => o.value),
|
|
31538
|
-
|
|
31539
|
-
if (paths.has("text.
|
|
31540
|
-
if (paths.has("text.
|
|
31716
|
+
const flags = (d) => d;
|
|
31717
|
+
if (paths.has("showPrice")) bar.appendChild(this.toggle("Show price", PRICE_DELTA_ICON, common((d) => flags(d).showPrice !== false), (v) => actions.patch({ showPrice: v })));
|
|
31718
|
+
if (paths.has("showDate")) bar.appendChild(this.toggle("Show date", DATE_DELTA_ICON, common((d) => flags(d).showDate !== false), (v) => actions.patch({ showDate: v })));
|
|
31719
|
+
if (paths.has("text.color") && textOnBar) bar.appendChild(swatch("Text color", TYPE_ICON, (d) => d.text?.color || t.textColor, "text.color"));
|
|
31720
|
+
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 }));
|
|
31721
|
+
const textField = !multi && paths.has("text.value");
|
|
31722
|
+
if (paths.has("text.bold") && !textField) bar.appendChild(this.toggle("Bold", BOLD_ICON, common((d) => !!d.text?.bold), (v) => actions.patch({ "text.bold": v })));
|
|
31723
|
+
if (paths.has("text.italic") && !textField) bar.appendChild(this.toggle("Italic", ITALIC_ICON, common((d) => !!d.text?.italic), (v) => actions.patch({ "text.italic": v })));
|
|
31724
|
+
if (textField) bar.appendChild(this.iconBtn("Text", TYPE_ICON, () => this.toggleTextPanel(drawing, actions, !textOnBar)));
|
|
31541
31725
|
const editableLevels = drawing.editableLevels();
|
|
31542
|
-
if (editableLevels && !(
|
|
31543
|
-
const fib =
|
|
31726
|
+
if (editableLevels && every((d) => d.editableLevels() != null && !(d instanceof MachFigure))) {
|
|
31727
|
+
const fib = (d) => d;
|
|
31544
31728
|
const sizes = TEXT_SIZE_OPTIONS.map((o) => o.value);
|
|
31545
|
-
bar.appendChild(this.dropdown("Numbers size", sizes, fib.numbersSize ?? "small", (s) => numbersSizeIcon(s), (v) => actions.patch({ numbersSize: v }), { label: sizeLabel }));
|
|
31546
|
-
bar.appendChild(this.dropdown("Label size", sizes, fib.labelsSize ?? "normal", (s) => labelSizeIcon(s), (v) => actions.patch({ labelsSize: v }), { label: sizeLabel }));
|
|
31729
|
+
bar.appendChild(this.dropdown("Numbers size", sizes, common((d) => fib(d).numbersSize ?? "small"), (s) => numbersSizeIcon(s), (v) => actions.patch({ numbersSize: v }), { label: sizeLabel }));
|
|
31730
|
+
bar.appendChild(this.dropdown("Label size", sizes, common((d) => fib(d).labelsSize ?? "normal"), (s) => labelSizeIcon(s), (v) => actions.patch({ labelsSize: v }), { label: sizeLabel }));
|
|
31547
31731
|
}
|
|
31548
31732
|
bar.appendChild(this.divider());
|
|
31549
|
-
if (isFrvp) bar.appendChild(this.iconBtn("Settings", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "frvp")));
|
|
31550
|
-
if (isPosition) bar.appendChild(this.iconBtn("Position size", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "position")));
|
|
31551
|
-
if (editableLevels) bar.appendChild(this.iconBtn("Levels", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "levels")));
|
|
31552
|
-
bar.appendChild(this.toggle("Lock", LOCK_ICON,
|
|
31733
|
+
if (!multi && isFrvp) bar.appendChild(this.iconBtn("Settings", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "frvp")));
|
|
31734
|
+
if (!multi && isPosition) bar.appendChild(this.iconBtn("Position size", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "position")));
|
|
31735
|
+
if (!multi && editableLevels) bar.appendChild(this.iconBtn("Levels", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "levels")));
|
|
31736
|
+
bar.appendChild(this.toggle("Lock", LOCK_ICON, common((d) => d.locked), (v) => actions.setLocked(v), UNLOCK_ICON));
|
|
31553
31737
|
const del = this.iconBtn("Delete", TRASH_ICON, () => actions.remove());
|
|
31554
31738
|
del.style.color = "var(--vela-danger)";
|
|
31555
31739
|
bar.appendChild(del);
|
|
@@ -31735,7 +31919,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
31735
31919
|
});
|
|
31736
31920
|
return b;
|
|
31737
31921
|
}
|
|
31738
|
-
/** The trailing kebab overflow — z-order actions
|
|
31922
|
+
/** The trailing kebab overflow — duplicate, z-order actions, reset settings. */
|
|
31739
31923
|
kebabButton(actions) {
|
|
31740
31924
|
const b = this.base("More");
|
|
31741
31925
|
b.innerHTML = sized(KEBAB_ICON);
|
|
@@ -31747,6 +31931,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
31747
31931
|
return;
|
|
31748
31932
|
}
|
|
31749
31933
|
this.openActionMenu(b, [
|
|
31934
|
+
{ icon: CLONE_ICON, label: "Duplicate", onClick: () => actions.duplicate() },
|
|
31750
31935
|
{ icon: FRONT_ICON, label: "Bring to front", onClick: () => actions.reorder("front") },
|
|
31751
31936
|
{ icon: BACK_ICON, label: "Send to back", onClick: () => actions.reorder("back") },
|
|
31752
31937
|
{ icon: RESET_ICON, label: "Reset settings", onClick: () => actions.resetSettings() }
|
|
@@ -31903,26 +32088,30 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
31903
32088
|
this.tipEl?.remove();
|
|
31904
32089
|
this.tipEl = null;
|
|
31905
32090
|
}
|
|
31906
|
-
|
|
32091
|
+
/** An on/off button. A mixed state (the selected drawings disagree) reads as half-lit and
|
|
32092
|
+
* the first click turns it ON for all of them. `off` swaps in a distinct glyph while the
|
|
32093
|
+
* toggle is off (a lock reads as an open padlock until it is engaged); mixed shows `icon`. */
|
|
32094
|
+
toggle(tip, icon2, active, onChange, off) {
|
|
31907
32095
|
const b = this.base(tip);
|
|
31908
|
-
b.innerHTML = sized(icon2);
|
|
31909
32096
|
const set = (on2) => {
|
|
31910
|
-
b.dataset.active = on2 ? "1" : "0";
|
|
32097
|
+
b.dataset.active = on2 === MIXED ? "mixed" : on2 ? "1" : "0";
|
|
32098
|
+
b.innerHTML = sized(off && on2 === false ? off : icon2);
|
|
31911
32099
|
};
|
|
31912
32100
|
set(active);
|
|
31913
32101
|
let on = active;
|
|
31914
32102
|
b.addEventListener("click", () => {
|
|
31915
|
-
on = !on;
|
|
32103
|
+
on = on === MIXED ? true : !on;
|
|
31916
32104
|
set(on);
|
|
31917
32105
|
onChange(on);
|
|
31918
32106
|
});
|
|
31919
32107
|
return b;
|
|
31920
32108
|
}
|
|
31921
32109
|
/** An inline numeric width field for tools whose stroke range outgrows the 1–4px
|
|
31922
|
-
* ladder — the value is clamped to the schema's declared min/max on commit.
|
|
32110
|
+
* ladder — the value is clamped to the schema's declared min/max on commit. A mixed
|
|
32111
|
+
* value shows an empty field with a dash until a number is typed for all. */
|
|
31923
32112
|
widthInput(tip, value, min2, max2, step, onChange) {
|
|
31924
32113
|
const ni = new NumberInput({
|
|
31925
|
-
value,
|
|
32114
|
+
value: value === MIXED ? min2 : value,
|
|
31926
32115
|
min: min2,
|
|
31927
32116
|
max: max2,
|
|
31928
32117
|
step,
|
|
@@ -31934,13 +32123,18 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
31934
32123
|
title: tip,
|
|
31935
32124
|
onChange
|
|
31936
32125
|
});
|
|
32126
|
+
if (value === MIXED) {
|
|
32127
|
+
ni.input.value = "";
|
|
32128
|
+
ni.input.placeholder = "\u2014";
|
|
32129
|
+
}
|
|
31937
32130
|
ni.el.dataset.tip = tip;
|
|
31938
32131
|
trapChartKeys(ni.el);
|
|
31939
32132
|
return ni.el;
|
|
31940
32133
|
}
|
|
31941
32134
|
/** A pick-one dropdown: the trigger shows the current value's glyph (plus an optional inline
|
|
31942
32135
|
* text label — e.g. `2px` — for controls that would otherwise look alike), and clicking it
|
|
31943
|
-
* opens a floating list of every option so a value is one click away (no cycling through).
|
|
32136
|
+
* opens a floating list of every option so a value is one click away (no cycling through).
|
|
32137
|
+
* A mixed value shows a dash in the trigger and highlights no row. */
|
|
31944
32138
|
dropdown(tip, values, current, render, onChange, opts = {}) {
|
|
31945
32139
|
const b = this.base(tip);
|
|
31946
32140
|
b.style.width = "auto";
|
|
@@ -31950,14 +32144,20 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
31950
32144
|
let cur = current;
|
|
31951
32145
|
const paint = (v) => {
|
|
31952
32146
|
b.replaceChildren();
|
|
31953
|
-
|
|
32147
|
+
if (v === MIXED) {
|
|
32148
|
+
const dash = document.createElement("span");
|
|
32149
|
+
dash.textContent = "\u2014";
|
|
32150
|
+
dash.style.cssText = "min-width:18px;text-align:center;opacity:0.85;";
|
|
32151
|
+
b.appendChild(dash);
|
|
32152
|
+
}
|
|
32153
|
+
const glyph = v === MIXED ? "" : render(v);
|
|
31954
32154
|
if (glyph) {
|
|
31955
32155
|
const ic = document.createElement("span");
|
|
31956
32156
|
ic.style.cssText = "display:flex;";
|
|
31957
32157
|
ic.innerHTML = sized(glyph);
|
|
31958
32158
|
b.appendChild(ic);
|
|
31959
32159
|
}
|
|
31960
|
-
if (opts.label && opts.labelInTrigger) {
|
|
32160
|
+
if (opts.label && opts.labelInTrigger && v !== MIXED) {
|
|
31961
32161
|
const tx = document.createElement("span");
|
|
31962
32162
|
tx.textContent = opts.label(v);
|
|
31963
32163
|
tx.style.cssText = "font-size:11px;opacity:0.85;font-variant-numeric:tabular-nums;";
|
|
@@ -32027,8 +32227,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
32027
32227
|
}
|
|
32028
32228
|
/** A color control: an `icon` over a thin **colored underline** showing the current
|
|
32029
32229
|
* value (the common idiom), with an invisible native picker overlaid to edit it.
|
|
32030
|
-
* `iconSize` shrinks it for the floating text controls.
|
|
32031
|
-
|
|
32230
|
+
* `iconSize` shrinks it for the floating text controls. A mixed color paints the
|
|
32231
|
+
* underline striped with the colors `used` across the selection, and the picker then
|
|
32232
|
+
* leads with those colors so unifying onto one of them is a single click. */
|
|
32233
|
+
colorButton(tip, icon2, color, onChange, iconSize = 17, used = []) {
|
|
32032
32234
|
const b = document.createElement("button");
|
|
32033
32235
|
b.type = "button";
|
|
32034
32236
|
b.dataset.tip = tip;
|
|
@@ -32038,13 +32240,14 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
32038
32240
|
ic.style.cssText = "display:flex;";
|
|
32039
32241
|
ic.innerHTML = sized(icon2, iconSize);
|
|
32040
32242
|
const bar = document.createElement("span");
|
|
32041
|
-
bar.style.cssText = `display:block;height:3px;width:${Math.round(iconSize * 0.85)}px;border-radius:2px;background:${color};`;
|
|
32243
|
+
bar.style.cssText = `display:block;height:3px;width:${Math.round(iconSize * 0.85)}px;border-radius:2px;background:${color === MIXED ? stripes(used) : color};`;
|
|
32042
32244
|
b.append(ic, bar);
|
|
32043
32245
|
let cur = color;
|
|
32044
32246
|
b.addEventListener("pointerdown", (e) => e.stopPropagation());
|
|
32045
32247
|
b.addEventListener("click", (e) => {
|
|
32046
32248
|
e.stopPropagation();
|
|
32047
|
-
|
|
32249
|
+
const seed = cur === MIXED ? used[0] ?? DEFAULT_DRAWING_COLOR : cur;
|
|
32250
|
+
this.toggleColorPopover(b, seed, cur === MIXED ? used : [], (v) => {
|
|
32048
32251
|
cur = v;
|
|
32049
32252
|
bar.style.background = v;
|
|
32050
32253
|
onChange(v);
|
|
@@ -32053,20 +32256,42 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
32053
32256
|
return b;
|
|
32054
32257
|
}
|
|
32055
32258
|
/** Open the color popover for `anchor`, or close it if it's already this anchor's (toggle). */
|
|
32056
|
-
toggleColorPopover(anchor, color, onChange) {
|
|
32259
|
+
toggleColorPopover(anchor, color, used, onChange) {
|
|
32057
32260
|
if (this.colorOwner === anchor) {
|
|
32058
32261
|
this.closeColorPopover();
|
|
32059
32262
|
return;
|
|
32060
32263
|
}
|
|
32061
|
-
this.openColorPopover(anchor, color, onChange);
|
|
32264
|
+
this.openColorPopover(anchor, color, used, onChange);
|
|
32062
32265
|
}
|
|
32063
|
-
/** A floating RGB picker + opacity slider anchored to a color swatch — emits `#RRGGBB(AA)`.
|
|
32064
|
-
|
|
32266
|
+
/** A floating RGB picker + opacity slider anchored to a color swatch — emits `#RRGGBB(AA)`.
|
|
32267
|
+
* `used` (a mixed swatch's colors) leads the picker as one-click unify shortcuts. */
|
|
32268
|
+
openColorPopover(anchor, color, used, onChange) {
|
|
32065
32269
|
const t = this.theme;
|
|
32066
32270
|
this.colorPop = this.hostFloat(anchor, {
|
|
32067
32271
|
zIndex: 25,
|
|
32068
32272
|
padding: "10px",
|
|
32069
|
-
fill: (el) => {
|
|
32273
|
+
fill: (el, pop) => {
|
|
32274
|
+
if (used.length > 1) {
|
|
32275
|
+
const row = document.createElement("div");
|
|
32276
|
+
row.style.cssText = "display:flex;align-items:center;gap:6px;padding-bottom:9px;border-bottom:1px solid var(--vela-border);";
|
|
32277
|
+
const lbl = document.createElement("span");
|
|
32278
|
+
lbl.textContent = "In use";
|
|
32279
|
+
lbl.style.cssText = "font:var(--vela-font-size-sm) inherit;opacity:0.7;margin-right:2px;";
|
|
32280
|
+
row.appendChild(lbl);
|
|
32281
|
+
for (const c of used) {
|
|
32282
|
+
const sw = document.createElement("button");
|
|
32283
|
+
sw.type = "button";
|
|
32284
|
+
sw.dataset.tip = c;
|
|
32285
|
+
sw.style.cssText = `width:18px;height:18px;border-radius:4px;border:1px solid var(--vela-border-strong);cursor:pointer;background:${c};padding:0;`;
|
|
32286
|
+
sw.addEventListener("click", (e) => {
|
|
32287
|
+
e.stopPropagation();
|
|
32288
|
+
pop.hide();
|
|
32289
|
+
onChange(c);
|
|
32290
|
+
});
|
|
32291
|
+
row.appendChild(sw);
|
|
32292
|
+
}
|
|
32293
|
+
el.appendChild(row);
|
|
32294
|
+
}
|
|
32070
32295
|
el.appendChild(buildColorPicker(color, t, onChange));
|
|
32071
32296
|
}
|
|
32072
32297
|
});
|
|
@@ -32087,6 +32312,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
32087
32312
|
var PRICE_DELTA_ICON = icon("price-delta");
|
|
32088
32313
|
var DATE_DELTA_ICON = icon("date-delta");
|
|
32089
32314
|
var LOCK_ICON = icon("lock");
|
|
32315
|
+
var UNLOCK_ICON = icon("unlock");
|
|
32090
32316
|
var FRONT_ICON = icon("bring-front");
|
|
32091
32317
|
var BACK_ICON = icon("send-back");
|
|
32092
32318
|
var TRASH_ICON = icon("trash");
|
|
@@ -32094,6 +32320,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
32094
32320
|
var ITALIC_ICON = icon("italic");
|
|
32095
32321
|
var GRIP_ICON = icon("grip");
|
|
32096
32322
|
var KEBAB_ICON = icon("kebab");
|
|
32323
|
+
var CLONE_ICON = icon("clone");
|
|
32097
32324
|
var RESET_ICON = icon("reset");
|
|
32098
32325
|
var GEAR_ICON = icon("gear");
|
|
32099
32326
|
var CHEVRON_ICON = icon("chevron-down");
|
|
@@ -32102,6 +32329,12 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
32102
32329
|
var DEDEKIND_ICON2 = icon("dedekind");
|
|
32103
32330
|
var SONIC_ICON2 = icon("sonic");
|
|
32104
32331
|
var SUPERSONIC_ICON2 = icon("supersonic");
|
|
32332
|
+
function stripes(colors) {
|
|
32333
|
+
const bands = colors.length > 1 ? colors : ["var(--vela-fg-faint)", "var(--vela-fg-muted)"];
|
|
32334
|
+
const step = 100 / bands.length;
|
|
32335
|
+
const stops = bands.map((c, i) => `${c} ${i * step}% ${(i + 1) * step}%`).join(",");
|
|
32336
|
+
return `linear-gradient(90deg,${stops})`;
|
|
32337
|
+
}
|
|
32105
32338
|
function lineIcon(width, style) {
|
|
32106
32339
|
const dash = style === "dashed" ? "5,3" : style === "dotted" ? "1.5,3" : "";
|
|
32107
32340
|
return svg24(`<line x1="3.5" y1="12" x2="20.5" y2="12" stroke-width="${Number(width) || 2}" stroke-dasharray="${dash}"/>`);
|
|
@@ -33017,6 +33250,20 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
33017
33250
|
// reported exactly when it actually changes).
|
|
33018
33251
|
this.textEditor = null;
|
|
33019
33252
|
this.painter = new DrawingPainter();
|
|
33253
|
+
/** The open popup is the multi-selection bar (vs one drawing's own) — decides what a
|
|
33254
|
+
* shrinking selection does with it. */
|
|
33255
|
+
this.popupMulti = false;
|
|
33256
|
+
/** Dismiss-on-outside-press also clears the selection — except when the press is ABOUT the
|
|
33257
|
+
* selection: it carries a multi-select modifier (Shift / Ctrl / Cmd — it will add to the
|
|
33258
|
+
* selection or sweep a marquee onto it), or it landed on a drawing that is already selected
|
|
33259
|
+
* (the canvas handler ran first and is holding it for a drag or a click), so the drawings
|
|
33260
|
+
* whose bar just closed must stay selected. */
|
|
33261
|
+
this.onPopupDismissed = (e) => {
|
|
33262
|
+
if (e && (e.shiftKey || e.ctrlKey || e.metaKey)) return;
|
|
33263
|
+
const pressed = this.interaction.pressedId();
|
|
33264
|
+
if (pressed && this.selectedIds.has(pressed)) return;
|
|
33265
|
+
this.clearSelection();
|
|
33266
|
+
};
|
|
33020
33267
|
this.ctx = canvas.getContext("2d");
|
|
33021
33268
|
this.popup = new DrawingSettingsPopup(overlayHost, deps.theme(), () => deps.chartBarMs());
|
|
33022
33269
|
this.toolbar = new DrawingToolbar(
|
|
@@ -33193,6 +33440,12 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
33193
33440
|
}
|
|
33194
33441
|
setSelection(ids) {
|
|
33195
33442
|
this.selectedIds = new Set(ids);
|
|
33443
|
+
if (ids.length >= 2) this.openSettingsForSelection(ids);
|
|
33444
|
+
else if (this.popupMulti) {
|
|
33445
|
+
this.popupMulti = false;
|
|
33446
|
+
this.popup.close();
|
|
33447
|
+
if (ids.length === 1) this.openSettingsById(ids[0], 0, 0);
|
|
33448
|
+
}
|
|
33196
33449
|
this.render();
|
|
33197
33450
|
}
|
|
33198
33451
|
/** Programmatic twin of clicking the drawing: highlight it + float its settings popup. */
|
|
@@ -33238,12 +33491,16 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
33238
33491
|
});
|
|
33239
33492
|
}
|
|
33240
33493
|
/** Delete the (unlocked) drawing under the cursor. True when one was removed.
|
|
33241
|
-
* Shared by the eraser (click + drag) and the middle-click shortcut
|
|
33242
|
-
|
|
33494
|
+
* Shared by the eraser (click + drag) and the middle-click shortcut; the latter passes
|
|
33495
|
+
* `withSelection` so a hit on a member of a multi-selection removes the selection's
|
|
33496
|
+
* unlocked members — whichever member was hit, locked or not. */
|
|
33497
|
+
deleteAt(x, y, withSelection = false) {
|
|
33243
33498
|
const hit = topDrawingAt(this.drawings, x, y, this.deps.projector(), HIT_TOLERANCE);
|
|
33244
|
-
if (!hit
|
|
33499
|
+
if (!hit) return false;
|
|
33500
|
+
const ids = deleteTargets(hit, this.selectedIds, this.drawings, withSelection);
|
|
33501
|
+
if (ids.length === 0) return false;
|
|
33245
33502
|
this.popup.close();
|
|
33246
|
-
this.emit({ kind: "delete", ids
|
|
33503
|
+
this.emit({ kind: "delete", ids });
|
|
33247
33504
|
return true;
|
|
33248
33505
|
}
|
|
33249
33506
|
/** Shift+press on the empty plot: arm the measure ruler AND start it at (x, y) in one
|
|
@@ -33269,7 +33526,14 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
33269
33526
|
this.render();
|
|
33270
33527
|
}
|
|
33271
33528
|
}
|
|
33272
|
-
|
|
33529
|
+
/** Ctrl/Cmd+press on the empty plot: sweep a selection box (adds the drawings it touches to
|
|
33530
|
+
* the selection). Returns false when a mode/tool is active (the normal press path owns it). */
|
|
33531
|
+
beginMarqueeAt(x, y) {
|
|
33532
|
+
if (this.measureMode || this.eraserMode || !this.interaction.beginMarquee(x, y)) return false;
|
|
33533
|
+
this.render();
|
|
33534
|
+
return true;
|
|
33535
|
+
}
|
|
33536
|
+
pointerDown(x, y, snap = "off", shift4 = false, mod = false) {
|
|
33273
33537
|
if (this.eraserMode) {
|
|
33274
33538
|
this.erasing = true;
|
|
33275
33539
|
this.deleteAt(x, y);
|
|
@@ -33289,9 +33553,9 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
33289
33553
|
return;
|
|
33290
33554
|
}
|
|
33291
33555
|
}
|
|
33292
|
-
this.interaction.down(x, y, snap, shift4);
|
|
33556
|
+
this.interaction.down(x, y, snap, shift4, mod);
|
|
33293
33557
|
}
|
|
33294
|
-
pointerMove(x, y, snap = "off", shift4 = false) {
|
|
33558
|
+
pointerMove(x, y, snap = "off", shift4 = false, mod = false) {
|
|
33295
33559
|
if (this.eraserMode) {
|
|
33296
33560
|
if (this.erasing) this.deleteAt(x, y);
|
|
33297
33561
|
return;
|
|
@@ -33303,12 +33567,14 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
33303
33567
|
return;
|
|
33304
33568
|
}
|
|
33305
33569
|
this.interaction.move(x, y, snap, shift4);
|
|
33306
|
-
this.updateHover(x, y);
|
|
33570
|
+
this.updateHover(x, y, mod);
|
|
33307
33571
|
}
|
|
33308
|
-
/** Track which drawing is hovered so its handles appear on hover (and only then).
|
|
33309
|
-
|
|
33572
|
+
/** Track which drawing is hovered so its handles appear on hover (and only then). While
|
|
33573
|
+
* Ctrl/Cmd is held the cursor is picking a selection — handles then mark only what IS
|
|
33574
|
+
* selected, so a hovered candidate doesn't already read as selected. */
|
|
33575
|
+
updateHover(x, y, mod = false) {
|
|
33310
33576
|
let id = null;
|
|
33311
|
-
if (this.activeTool == null && !this.interaction.isPlacing() && !this.interaction.isDragging()) {
|
|
33577
|
+
if (!mod && this.activeTool == null && !this.interaction.isPlacing() && !this.interaction.isDragging()) {
|
|
33312
33578
|
id = topDrawingAt(this.drawings, x, y, this.deps.projector(), HIT_TOLERANCE)?.id ?? null;
|
|
33313
33579
|
}
|
|
33314
33580
|
if (id !== this.hoveredId) {
|
|
@@ -33328,7 +33594,12 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
33328
33594
|
this.render();
|
|
33329
33595
|
return;
|
|
33330
33596
|
}
|
|
33597
|
+
const dragged = this.interaction.isDragging() ? this.interaction.pressedId() : null;
|
|
33331
33598
|
this.interaction.up(x, y);
|
|
33599
|
+
if (dragged && this.selectedIds.has(dragged) && !this.popup.isOpen()) {
|
|
33600
|
+
if (this.selectedIds.size >= 2) this.openSettingsForSelection(this.selectionIds());
|
|
33601
|
+
else this.openSettingsById(dragged, x, y);
|
|
33602
|
+
}
|
|
33332
33603
|
}
|
|
33333
33604
|
/** Toggle the transient ruler. Arming it clears any drawing tool + selection. */
|
|
33334
33605
|
toggleMeasure() {
|
|
@@ -33593,7 +33864,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
33593
33864
|
this.emit({ kind: "duplicate", ids: this.selectionIds() });
|
|
33594
33865
|
break;
|
|
33595
33866
|
case "delete": {
|
|
33596
|
-
const ids = this.selectedIds.size ? this.
|
|
33867
|
+
const ids = this.selectedIds.size ? this.deletableSelection() : this.hoveredId ? [this.hoveredId] : [];
|
|
33597
33868
|
if (ids.length) {
|
|
33598
33869
|
this.popup.close();
|
|
33599
33870
|
this.emit({ kind: "delete", ids });
|
|
@@ -33609,6 +33880,9 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
33609
33880
|
selectionIds() {
|
|
33610
33881
|
return [...this.selectedIds];
|
|
33611
33882
|
}
|
|
33883
|
+
deletableSelection() {
|
|
33884
|
+
return deletableSelection(this.selectedIds, this.drawings);
|
|
33885
|
+
}
|
|
33612
33886
|
/** Move every selected (unlocked) drawing by a pixel delta — one edit/edit-many → one undo step. */
|
|
33613
33887
|
nudgeSelection(dx, dy) {
|
|
33614
33888
|
const proj = this.deps.projector();
|
|
@@ -33717,6 +33991,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
33717
33991
|
this.painter.seriesLook = this.deps.seriesLook();
|
|
33718
33992
|
this.painter.paintAll(ctx, this.drawings.filter((d) => !this.isInterleaved(d)), proj, this.deps.theme(), targets);
|
|
33719
33993
|
this.painter.paintHighlights(ctx, this.drawings.filter((d) => this.isInterleaved(d)), proj, handleIdsFor(targets));
|
|
33994
|
+
const clones = this.interaction.dragClones();
|
|
33995
|
+
if (clones) this.painter.paintAll(ctx, clones, proj, this.deps.theme(), { selected: new Set(clones.map((c) => c.id)) });
|
|
33720
33996
|
this.layoutTextEditor();
|
|
33721
33997
|
const ghost = this.interaction.ghost();
|
|
33722
33998
|
if (ghost) this.painter.paintGhost(ctx, ghost, proj, this.deps.theme());
|
|
@@ -33732,6 +34008,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
33732
34008
|
const my = m ? proj.yOf(m.point.price, m.paneId) : null;
|
|
33733
34009
|
if (m && my != null) this.painter.paintSnapRing(ctx, proj.xOf(m.point.time), my, this.deps.theme());
|
|
33734
34010
|
if (this.measure.isActive()) this.measure.paint(ctx, proj, this.deps.theme());
|
|
34011
|
+
const marquee = this.interaction.marqueeRect();
|
|
34012
|
+
if (marquee) this.painter.paintMarquee(ctx, marquee);
|
|
33735
34013
|
}
|
|
33736
34014
|
destroy() {
|
|
33737
34015
|
this.closeTextEditor();
|
|
@@ -33749,15 +34027,80 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
33749
34027
|
if (this.selectedIds.size) this.emit({ kind: "select", ids: [] });
|
|
33750
34028
|
this.render();
|
|
33751
34029
|
}
|
|
34030
|
+
/** Report the live drawings' current state as one edit (one undo step, however many). */
|
|
34031
|
+
emitEdits(drawings) {
|
|
34032
|
+
const docs = drawings.map((d) => d.serialize());
|
|
34033
|
+
if (docs.length === 1) this.emit({ kind: "edit", doc: docs[0] });
|
|
34034
|
+
else if (docs.length > 1) this.emit({ kind: "edit-many", docs });
|
|
34035
|
+
}
|
|
34036
|
+
/** The one bar for a multi-selection: it shows the controls all of its drawings share and
|
|
34037
|
+
* every action applies to all of them. Floats clear of their combined bounds. */
|
|
34038
|
+
openSettingsForSelection(ids) {
|
|
34039
|
+
const live = () => ids.map((id) => this.drawings.find((d) => d.id === id)).filter((d) => d != null);
|
|
34040
|
+
const drawings = live();
|
|
34041
|
+
if (drawings.length < 2) return;
|
|
34042
|
+
const proj = this.deps.projector();
|
|
34043
|
+
let anchor = null;
|
|
34044
|
+
for (const d of drawings) {
|
|
34045
|
+
const b = d.bounds(proj);
|
|
34046
|
+
if (!b) continue;
|
|
34047
|
+
if (!anchor) anchor = { ...b };
|
|
34048
|
+
else {
|
|
34049
|
+
const right = Math.max(anchor.x + anchor.w, b.x + b.w);
|
|
34050
|
+
const bottom = Math.max(anchor.y + anchor.h, b.y + b.h);
|
|
34051
|
+
anchor.x = Math.min(anchor.x, b.x);
|
|
34052
|
+
anchor.y = Math.min(anchor.y, b.y);
|
|
34053
|
+
anchor.w = right - anchor.x;
|
|
34054
|
+
anchor.h = bottom - anchor.y;
|
|
34055
|
+
}
|
|
34056
|
+
}
|
|
34057
|
+
this.popupMulti = true;
|
|
34058
|
+
this.emit({ kind: "settings", id: drawings[0].id });
|
|
34059
|
+
this.popup.open(drawings, anchor, {
|
|
34060
|
+
resolve: () => live()[0] ?? null,
|
|
34061
|
+
patch: (p) => {
|
|
34062
|
+
const ds = live();
|
|
34063
|
+
for (const d of ds) d.applySettings(p);
|
|
34064
|
+
this.render();
|
|
34065
|
+
this.emitEdits(ds);
|
|
34066
|
+
},
|
|
34067
|
+
setLocked: (v) => {
|
|
34068
|
+
const ds = live();
|
|
34069
|
+
for (const d of ds) d.locked = v;
|
|
34070
|
+
this.emitEdits(ds);
|
|
34071
|
+
},
|
|
34072
|
+
reorder: (to) => {
|
|
34073
|
+
for (const d of live()) this.emit({ kind: "reorder", id: d.id, to });
|
|
34074
|
+
},
|
|
34075
|
+
duplicate: () => {
|
|
34076
|
+
this.popup.close();
|
|
34077
|
+
this.emit({ kind: "duplicate", ids: live().map((d) => d.id) });
|
|
34078
|
+
},
|
|
34079
|
+
resetSettings: () => {
|
|
34080
|
+
const ds = live();
|
|
34081
|
+
for (const d of ds) resetDrawingSettings(d);
|
|
34082
|
+
this.render();
|
|
34083
|
+
this.emitEdits(ds);
|
|
34084
|
+
this.openSettingsForSelection(ids);
|
|
34085
|
+
},
|
|
34086
|
+
remove: () => {
|
|
34087
|
+
const ids2 = live().filter((d) => !d.locked).map((d) => d.id);
|
|
34088
|
+
if (ids2.length === 0) return;
|
|
34089
|
+
this.popup.close();
|
|
34090
|
+
this.emit({ kind: "delete", ids: ids2 });
|
|
34091
|
+
}
|
|
34092
|
+
}, this.onPopupDismissed);
|
|
34093
|
+
}
|
|
33752
34094
|
/** A click on a drawing opens its settings toolbar (text labels edit text here too). */
|
|
33753
34095
|
openSettingsById(id, _x, _y) {
|
|
33754
34096
|
const drawing = this.drawings.find((d) => d.id === id);
|
|
33755
34097
|
if (!drawing) return;
|
|
33756
34098
|
const live = () => this.drawings.find((d) => d.id === id);
|
|
34099
|
+
this.popupMulti = false;
|
|
33757
34100
|
this.emit({ kind: "select", ids: [id] });
|
|
33758
34101
|
this.emit({ kind: "settings", id });
|
|
33759
34102
|
const anchor = drawing.bounds(this.deps.projector());
|
|
33760
|
-
this.popup.open(drawing, anchor, {
|
|
34103
|
+
this.popup.open([drawing], anchor, {
|
|
33761
34104
|
// Sync rebuilds instances, so a panel that reads values back after a patch (e.g. the
|
|
33762
34105
|
// position tool's price fields, where one edit can flip another level) resolves fresh.
|
|
33763
34106
|
resolve: () => live() ?? null,
|
|
@@ -33775,6 +34118,11 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
33775
34118
|
this.emit({ kind: "edit", doc: d.serialize() });
|
|
33776
34119
|
},
|
|
33777
34120
|
reorder: (to) => this.emit({ kind: "reorder", id, to }),
|
|
34121
|
+
duplicate: () => {
|
|
34122
|
+
this.finishTextEditor(true);
|
|
34123
|
+
this.popup.close();
|
|
34124
|
+
this.emit({ kind: "duplicate", ids: [id] });
|
|
34125
|
+
},
|
|
33778
34126
|
resetSettings: () => {
|
|
33779
34127
|
const d = live();
|
|
33780
34128
|
if (!d) return;
|
|
@@ -33797,7 +34145,13 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
33797
34145
|
this.popup.close();
|
|
33798
34146
|
this.emit({ kind: "delete", ids: [id] });
|
|
33799
34147
|
}
|
|
33800
|
-
},
|
|
34148
|
+
}, this.onPopupDismissed);
|
|
34149
|
+
}
|
|
34150
|
+
/** A plain click on the empty plot: drop the selection. The popup's dismiss-on-outside-press
|
|
34151
|
+
* does the same, but a selection can exist with no bar open (a lone Ctrl-click pick, a
|
|
34152
|
+
* selection whose bar was dismissed by a modifier press) — this covers those. */
|
|
34153
|
+
deselect() {
|
|
34154
|
+
this.clearSelection();
|
|
33801
34155
|
}
|
|
33802
34156
|
/** Report placement progress upstream (`draft` intent) so the drawings sync can
|
|
33803
34157
|
* mirror the ghost live on linked charts. Called from every render; only actual
|
|
@@ -33823,6 +34177,13 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
33823
34177
|
}, 0);
|
|
33824
34178
|
return;
|
|
33825
34179
|
}
|
|
34180
|
+
if (i.kind === "clone") {
|
|
34181
|
+
const before = new Set(this.drawings.map((d) => d.id));
|
|
34182
|
+
this.intentCb?.(i);
|
|
34183
|
+
const fresh = this.drawings.filter((d) => !before.has(d.id));
|
|
34184
|
+
if (fresh.length === 1) this.openSettingsById(fresh[0].id, 0, 0);
|
|
34185
|
+
return;
|
|
34186
|
+
}
|
|
33826
34187
|
this.intentCb?.(i);
|
|
33827
34188
|
}
|
|
33828
34189
|
};
|
|
@@ -36134,7 +36495,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
36134
36495
|
zoomTo: (target, anchorLogical, anchorX) => this.zoomTo(target, anchorLogical, anchorX),
|
|
36135
36496
|
fling: (v) => this.fling(v),
|
|
36136
36497
|
onPointerMove: (x, y) => this.handlePointerMove(x, y),
|
|
36137
|
-
onClick: (x) =>
|
|
36498
|
+
onClick: (x) => {
|
|
36499
|
+
this.userDrawings?.deselect();
|
|
36500
|
+
this.handleClick(x);
|
|
36501
|
+
},
|
|
36138
36502
|
onAxisLongPress: (axis, x, y) => {
|
|
36139
36503
|
for (const cb of this.axisLongPressCbs) cb({ axis, x, y });
|
|
36140
36504
|
},
|
|
@@ -36152,11 +36516,13 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
36152
36516
|
// User drawings claim a gesture before pan when armed / over a drawing.
|
|
36153
36517
|
drawingsClaim: (x, y) => this.userDrawings?.claim(x, y) ?? false,
|
|
36154
36518
|
drawingsMeasureStart: (x, y, snap) => this.userDrawings?.beginMeasureAt(x, y, snap) ?? false,
|
|
36155
|
-
|
|
36519
|
+
drawingsMarqueeStart: (x, y) => this.userDrawings?.beginMarqueeAt(x, y) ?? false,
|
|
36520
|
+
drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y, true) ?? false,
|
|
36521
|
+
// middle-click: a selected hit takes the whole selection
|
|
36156
36522
|
drawingsCancelPlacement: () => this.userDrawings?.cancelPlacement() ?? false,
|
|
36157
36523
|
drawingsSnapMode: () => this.snapMode,
|
|
36158
|
-
drawingsPointerDown: (x, y, snap, shift4) => this.userDrawings?.pointerDown(x, y, snap, shift4),
|
|
36159
|
-
drawingsPointerMove: (x, y, snap, shift4) => this.userDrawings?.pointerMove(x, y, snap, shift4),
|
|
36524
|
+
drawingsPointerDown: (x, y, snap, shift4, mod) => this.userDrawings?.pointerDown(x, y, snap, shift4, mod),
|
|
36525
|
+
drawingsPointerMove: (x, y, snap, shift4, mod) => this.userDrawings?.pointerMove(x, y, snap, shift4, mod),
|
|
36160
36526
|
drawingsPointerUp: (x, y, snap) => this.userDrawings?.pointerUp(x, y, snap),
|
|
36161
36527
|
drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? null,
|
|
36162
36528
|
drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
|
|
@@ -38519,6 +38885,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
38519
38885
|
const v = inputs[key];
|
|
38520
38886
|
return typeof v === "string" && v !== "" ? v : fallback;
|
|
38521
38887
|
}
|
|
38888
|
+
function bool(inputs, key, fallback) {
|
|
38889
|
+
const v = inputs[key];
|
|
38890
|
+
return typeof v === "boolean" ? v : fallback;
|
|
38891
|
+
}
|
|
38522
38892
|
function toPoints(bars, values, colors) {
|
|
38523
38893
|
const pts = new Array(bars.length);
|
|
38524
38894
|
for (let i = 0; i < bars.length; i++) {
|
|
@@ -40050,10 +40420,6 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
40050
40420
|
plots: [
|
|
40051
40421
|
{ key: "up", title: "Aroon up", values: up, color: BULLISH, width: 2 },
|
|
40052
40422
|
{ key: "down", title: "Aroon down", values: down, color: BEARISH, width: 2 }
|
|
40053
|
-
],
|
|
40054
|
-
levels: [
|
|
40055
|
-
{ key: "upper", price: 100, color: NEUTRAL, lineStyle: "dotted" },
|
|
40056
|
-
{ key: "lower", price: 0, color: NEUTRAL, lineStyle: "dotted" }
|
|
40057
40423
|
]
|
|
40058
40424
|
};
|
|
40059
40425
|
}
|
|
@@ -40551,8 +40917,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
40551
40917
|
var DAY_MS2 = 864e5;
|
|
40552
40918
|
function periodKey(anchor, time) {
|
|
40553
40919
|
if (anchor === "Week") return Math.floor((Math.floor(time / DAY_MS2) + 3) / 7);
|
|
40554
|
-
if (anchor === "Month") {
|
|
40920
|
+
if (anchor === "Month" || anchor === "Quarter" || anchor === "Year") {
|
|
40555
40921
|
const d = new Date(time);
|
|
40922
|
+
if (anchor === "Year") return d.getUTCFullYear();
|
|
40923
|
+
if (anchor === "Quarter") return d.getUTCFullYear() * 4 + Math.floor(d.getUTCMonth() / 3);
|
|
40556
40924
|
return d.getUTCFullYear() * 12 + d.getUTCMonth();
|
|
40557
40925
|
}
|
|
40558
40926
|
return Math.floor(time / DAY_MS2);
|
|
@@ -40604,40 +40972,86 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
40604
40972
|
return { plots: [{ key: "sar", title: "PSAR", values, kind: "circles", color: SERIES_LINE, colors }] };
|
|
40605
40973
|
}
|
|
40606
40974
|
};
|
|
40975
|
+
var VWAP_BANDS_GROUP = "Bands";
|
|
40976
|
+
var VWAP_STYLE = "Style";
|
|
40977
|
+
var VWAP_BANDS = [
|
|
40978
|
+
{ on: "band1", mult: "band1Mult", color: "band1Color", fill: "band1Fill", fillColor: "band1FillColor", defMult: 1, defOn: true, defColor: BULLISH },
|
|
40979
|
+
{ on: "band2", mult: "band2Mult", color: "band2Color", fill: "band2Fill", fillColor: "band2FillColor", defMult: 2, defOn: false, defColor: WARNING },
|
|
40980
|
+
{ on: "band3", mult: "band3Mult", color: "band3Color", fill: "band3Fill", fillColor: "band3FillColor", defMult: 3, defOn: false, defColor: CATEGORICAL[4] }
|
|
40981
|
+
];
|
|
40607
40982
|
var vwap = {
|
|
40608
40983
|
type: "vwap",
|
|
40609
40984
|
title: "Volume Weighted Average Price",
|
|
40610
40985
|
shortTitle: "VWAP",
|
|
40611
40986
|
overlay: true,
|
|
40612
40987
|
inputs: [
|
|
40613
|
-
{ key: "anchor", title: "
|
|
40614
|
-
|
|
40615
|
-
|
|
40988
|
+
{ key: "anchor", title: "Period", type: "string", defval: "Day", options: ["Day", "Week", "Month", "Quarter", "Year"], tooltip: "Where the accumulation resets (UTC periods)" },
|
|
40989
|
+
sourceInput("HLC3"),
|
|
40990
|
+
// Each band is one row: its toggle leads, the multiplier follows unlabeled.
|
|
40991
|
+
...VWAP_BANDS.flatMap((b, i) => [
|
|
40992
|
+
{ key: b.on, title: `Band ${i + 1} multiplier`, type: "bool", defval: b.defOn, inline: b.on, group: VWAP_BANDS_GROUP },
|
|
40993
|
+
{ key: b.mult, title: "", type: "float", defval: b.defMult, min: 0.1, max: 10, step: 0.1, inline: b.on, group: VWAP_BANDS_GROUP }
|
|
40994
|
+
]),
|
|
40995
|
+
{ ...colorInput(INFO, "VWAP color"), group: VWAP_STYLE },
|
|
40996
|
+
// One row per band: its ink, then the fill toggle with the fill's own (translucent) color.
|
|
40997
|
+
...VWAP_BANDS.flatMap((b, i) => [
|
|
40998
|
+
{ ...colorInput(b.defColor, `Band ${i + 1} color`, b.color), inline: b.color, group: VWAP_STYLE },
|
|
40999
|
+
{ key: b.fill, title: "Fill", type: "bool", defval: true, inline: b.color, group: VWAP_STYLE },
|
|
41000
|
+
{ ...colorInput(withAlpha2(b.defColor), "", b.fillColor), inline: b.color, group: VWAP_STYLE }
|
|
41001
|
+
])
|
|
40616
41002
|
],
|
|
40617
41003
|
compute: (bars, inputs) => {
|
|
40618
41004
|
const anchor = str3(inputs, "anchor", "Day");
|
|
40619
|
-
const
|
|
40620
|
-
const
|
|
41005
|
+
const src = sourceValues(bars, str3(inputs, "source", "HLC3"));
|
|
41006
|
+
const n = bars.length;
|
|
41007
|
+
const values = new Array(n).fill(Number.NaN);
|
|
41008
|
+
const bands = VWAP_BANDS.filter((b) => bool(inputs, b.on, b.defOn)).map((b) => ({
|
|
41009
|
+
mult: Math.max(0, num3(inputs, b.mult, b.defMult)),
|
|
41010
|
+
color: str3(inputs, b.color, b.defColor),
|
|
41011
|
+
fill: bool(inputs, b.fill, true),
|
|
41012
|
+
fillColor: str3(inputs, b.fillColor, withAlpha2(b.defColor)),
|
|
41013
|
+
up: new Array(n).fill(Number.NaN),
|
|
41014
|
+
down: new Array(n).fill(Number.NaN)
|
|
41015
|
+
}));
|
|
40621
41016
|
let period = Number.NaN;
|
|
40622
41017
|
let cumPV = 0;
|
|
41018
|
+
let cumPV2 = 0;
|
|
40623
41019
|
let cumV = 0;
|
|
40624
|
-
for (let i = 0; i <
|
|
41020
|
+
for (let i = 0; i < n; i++) {
|
|
40625
41021
|
const b = bars[i];
|
|
40626
41022
|
const key = periodKey(anchor, b.time);
|
|
40627
41023
|
if (key !== period) {
|
|
40628
41024
|
period = key;
|
|
40629
41025
|
cumPV = 0;
|
|
41026
|
+
cumPV2 = 0;
|
|
40630
41027
|
cumV = 0;
|
|
40631
41028
|
}
|
|
40632
41029
|
const v = b.volume;
|
|
40633
41030
|
if (v != null && Number.isFinite(v) && v > 0) {
|
|
40634
|
-
const tp =
|
|
41031
|
+
const tp = src[i];
|
|
40635
41032
|
cumPV += tp * v;
|
|
41033
|
+
cumPV2 += tp * tp * v;
|
|
40636
41034
|
cumV += v;
|
|
40637
41035
|
}
|
|
40638
|
-
if (cumV
|
|
40639
|
-
|
|
40640
|
-
|
|
41036
|
+
if (cumV <= 0) continue;
|
|
41037
|
+
const mean = cumPV / cumV;
|
|
41038
|
+
const sd = Math.sqrt(Math.max(0, cumPV2 / cumV - mean * mean));
|
|
41039
|
+
values[i] = mean;
|
|
41040
|
+
for (const band of bands) {
|
|
41041
|
+
band.up[i] = mean + band.mult * sd;
|
|
41042
|
+
band.down[i] = mean - band.mult * sd;
|
|
41043
|
+
}
|
|
41044
|
+
}
|
|
41045
|
+
const plots = [{ key: "vwap", title: "VWAP", values, color: str3(inputs, "color", INFO), width: 2 }];
|
|
41046
|
+
const fills = [];
|
|
41047
|
+
bands.forEach((band, i) => {
|
|
41048
|
+
plots.push(
|
|
41049
|
+
{ key: `up${i}`, title: `Upper ${band.mult}\u03C3`, values: band.up, color: band.color },
|
|
41050
|
+
{ key: `down${i}`, title: `Lower ${band.mult}\u03C3`, values: band.down, color: band.color }
|
|
41051
|
+
);
|
|
41052
|
+
if (band.fill) fills.push({ key: `band${i}`, from: `up${i}`, to: `down${i}`, color: band.fillColor });
|
|
41053
|
+
});
|
|
41054
|
+
return { plots, bands: fills };
|
|
40641
41055
|
}
|
|
40642
41056
|
};
|
|
40643
41057
|
var pivotPoints = {
|