@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/index.cjs
CHANGED
|
@@ -6924,15 +6924,18 @@ var DrawingController = class {
|
|
|
6924
6924
|
next = [...ids];
|
|
6925
6925
|
}
|
|
6926
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() {
|
|
6927
6931
|
this.port?.setSelection(this.selectedIds);
|
|
6928
|
-
this.events.emit("drawing:selected", { id: this.selectedIds[0] ?? null });
|
|
6932
|
+
this.events.emit("drawing:selected", { id: this.selectedIds[0] ?? null, ids: [...this.selectedIds] });
|
|
6929
6933
|
}
|
|
6930
6934
|
/** Restore a history snapshot, reconciling selection against what survived. */
|
|
6931
6935
|
restoreSnapshot(doc) {
|
|
6932
6936
|
this.store.load(doc);
|
|
6933
6937
|
this.selectedIds = this.selectedIds.filter((id) => this.store.has(id));
|
|
6934
|
-
this.
|
|
6935
|
-
this.events.emit("drawing:selected", { id: this.selectedIds[0] ?? null });
|
|
6938
|
+
this.announceSelection();
|
|
6936
6939
|
}
|
|
6937
6940
|
/** Delete drawings as one undo step; prune them from the selection. */
|
|
6938
6941
|
deleteMany(ids) {
|
|
@@ -7065,6 +7068,9 @@ var DrawingController = class {
|
|
|
7065
7068
|
case "duplicate":
|
|
7066
7069
|
this.duplicate(i.ids);
|
|
7067
7070
|
break;
|
|
7071
|
+
case "clone":
|
|
7072
|
+
if (this.enabled && i.docs.length) this.insertClones(i.docs);
|
|
7073
|
+
break;
|
|
7068
7074
|
case "copy":
|
|
7069
7075
|
this.copy(i.ids);
|
|
7070
7076
|
break;
|
|
@@ -8901,7 +8907,12 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
8901
8907
|
if (!model) continue;
|
|
8902
8908
|
const paneId = model.paneId ?? "price";
|
|
8903
8909
|
if (!byPane.has(paneId)) byPane.set(paneId, []);
|
|
8904
|
-
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
|
+
});
|
|
8905
8916
|
}
|
|
8906
8917
|
const panes = [];
|
|
8907
8918
|
for (const paneId of this.paneOrder) {
|
|
@@ -15532,15 +15543,15 @@ function candleOverrideFor(style, bags) {
|
|
|
15532
15543
|
if (!hasOwnCandlePaint(style)) return null;
|
|
15533
15544
|
const bag = bags[style] ?? {};
|
|
15534
15545
|
const color = (v) => isColor(v) ? v : null;
|
|
15535
|
-
const
|
|
15546
|
+
const bool2 = (v) => isBool(v) ? v : null;
|
|
15536
15547
|
return {
|
|
15537
15548
|
upColor: color(bag.candleUpColor),
|
|
15538
15549
|
downColor: color(bag.candleDownColor),
|
|
15539
|
-
bodyVisible:
|
|
15540
|
-
borderVisible:
|
|
15550
|
+
bodyVisible: bool2(bag.candleBodyVisible),
|
|
15551
|
+
borderVisible: bool2(bag.candleBorderVisible),
|
|
15541
15552
|
borderUpColor: color(bag.candleBorderUpColor),
|
|
15542
15553
|
borderDownColor: color(bag.candleBorderDownColor),
|
|
15543
|
-
wickVisible:
|
|
15554
|
+
wickVisible: bool2(bag.candleWickVisible),
|
|
15544
15555
|
wickUpColor: color(bag.candleWickUpColor),
|
|
15545
15556
|
wickDownColor: color(bag.candleWickDownColor)
|
|
15546
15557
|
};
|
|
@@ -17331,7 +17342,7 @@ var InputController = class {
|
|
|
17331
17342
|
if (e.repeat || e.key !== "Shift" && e.key !== "Control" && e.key !== "Meta") return;
|
|
17332
17343
|
if (Number.isNaN(this.cursorX)) return;
|
|
17333
17344
|
if (this.dragging && this.region !== "drawing") return;
|
|
17334
|
-
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);
|
|
17335
17346
|
};
|
|
17336
17347
|
this.onDown = (e) => {
|
|
17337
17348
|
if (e.button === 1) {
|
|
@@ -17363,7 +17374,7 @@ var InputController = class {
|
|
|
17363
17374
|
this.startY = y;
|
|
17364
17375
|
if (this.deps.drawingsClaim?.(x, y)) {
|
|
17365
17376
|
this.region = "drawing";
|
|
17366
|
-
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);
|
|
17367
17378
|
this.capture(e.pointerId);
|
|
17368
17379
|
return;
|
|
17369
17380
|
}
|
|
@@ -17372,6 +17383,11 @@ var InputController = class {
|
|
|
17372
17383
|
this.capture(e.pointerId);
|
|
17373
17384
|
return;
|
|
17374
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
|
+
}
|
|
17375
17391
|
const vp = this.deps.getCoords().getViewport();
|
|
17376
17392
|
this.deps.apply(vp);
|
|
17377
17393
|
this.startRightOffset = vp.rightOffset;
|
|
@@ -17420,7 +17436,7 @@ var InputController = class {
|
|
|
17420
17436
|
this.deps.apply({ barSpacing, rightOffset: this.startRightOffset });
|
|
17421
17437
|
} else if (this.region === "drawing") {
|
|
17422
17438
|
if (Math.abs(x - this.startX) > slop || Math.abs(y - this.startY) > slop) this.moved = true;
|
|
17423
|
-
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);
|
|
17424
17440
|
} else {
|
|
17425
17441
|
const coords = this.deps.getCoords();
|
|
17426
17442
|
const vp = coords.getViewport();
|
|
@@ -17443,7 +17459,7 @@ var InputController = class {
|
|
|
17443
17459
|
const drawCursor = this.deps.drawingsCursor?.(x, y);
|
|
17444
17460
|
const r = this.regionAt(x, y);
|
|
17445
17461
|
this.el.style.cursor = drawCursor ?? (r === "price" ? "ns-resize" : r === "time" ? "ew-resize" : r === "separator" ? "row-resize" : "");
|
|
17446
|
-
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);
|
|
17447
17463
|
}
|
|
17448
17464
|
if (e.pointerType !== "touch") this.deps.onPointerMove(x, y);
|
|
17449
17465
|
};
|
|
@@ -17784,12 +17800,12 @@ function defaultTradeMarkersState() {
|
|
|
17784
17800
|
function mergeTradeMarkersState(base, patch) {
|
|
17785
17801
|
const p = patch && typeof patch === "object" ? patch : {};
|
|
17786
17802
|
const c = p.colors && typeof p.colors === "object" ? p.colors : {};
|
|
17787
|
-
const
|
|
17803
|
+
const bool2 = (v, fb) => typeof v === "boolean" ? v : fb;
|
|
17788
17804
|
const color = (v, fb) => typeof v === "string" && v.trim().length > 0 ? v : fb;
|
|
17789
17805
|
return {
|
|
17790
|
-
visible:
|
|
17791
|
-
labels:
|
|
17792
|
-
qty:
|
|
17806
|
+
visible: bool2(p.visible, base.visible),
|
|
17807
|
+
labels: bool2(p.labels, base.labels),
|
|
17808
|
+
qty: bool2(p.qty, base.qty),
|
|
17793
17809
|
colors: {
|
|
17794
17810
|
long: color(c.long, base.colors.long),
|
|
17795
17811
|
short: color(c.short, base.colors.short),
|
|
@@ -21739,6 +21755,18 @@ var DrawingPainter = class {
|
|
|
21739
21755
|
}
|
|
21740
21756
|
this.paintHandles(ctx, pts);
|
|
21741
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
|
+
}
|
|
21742
21770
|
/** A hollow ring marking the candle point the magnet (Ctrl) will snap the next anchor to. */
|
|
21743
21771
|
paintSnapRing(ctx, x, y, theme) {
|
|
21744
21772
|
ctx.setLineDash([]);
|
|
@@ -23207,6 +23235,15 @@ function topDrawingAt(drawings, x, y, proj, tol = HIT_TOLERANCE) {
|
|
|
23207
23235
|
}
|
|
23208
23236
|
return null;
|
|
23209
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
|
+
}
|
|
23210
23247
|
|
|
23211
23248
|
// src/renderers/native/drawings/DrawingInteraction.ts
|
|
23212
23249
|
var DRAG_SLOP2 = 3;
|
|
@@ -23310,10 +23347,32 @@ var DrawingInteraction = class {
|
|
|
23310
23347
|
activeDragId() {
|
|
23311
23348
|
return this.state.kind === "pressed" && this.state.moved ? this.state.id : null;
|
|
23312
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
|
+
}
|
|
23313
23371
|
/** A press: place the next anchor, or grab a drawing/handle (resolved as click or drag on release).
|
|
23314
23372
|
* `shift` over a drawing toggles it in/out of the selection (multi-select) instead of dragging;
|
|
23315
|
-
* while placing a line tool it locks the segment angle to 45° steps.
|
|
23316
|
-
|
|
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) {
|
|
23317
23376
|
const proj = this.deps.projector();
|
|
23318
23377
|
if (this.state.kind === "placing") {
|
|
23319
23378
|
if (this.state.draft.placementMode() !== "click") return;
|
|
@@ -23341,7 +23400,50 @@ var DrawingInteraction = class {
|
|
|
23341
23400
|
return;
|
|
23342
23401
|
}
|
|
23343
23402
|
const handle = hit.hitHandle(x, y, proj, HIT_TOLERANCE);
|
|
23344
|
-
|
|
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);
|
|
23345
23447
|
}
|
|
23346
23448
|
}
|
|
23347
23449
|
/** Pointer move: advance the placing ghost, or live-preview a drag once past the slop.
|
|
@@ -23365,12 +23467,20 @@ var DrawingInteraction = class {
|
|
|
23365
23467
|
this.deps.changed();
|
|
23366
23468
|
return;
|
|
23367
23469
|
}
|
|
23470
|
+
if (this.state.kind === "marquee") {
|
|
23471
|
+
this.state.x1 = x;
|
|
23472
|
+
this.state.y1 = y;
|
|
23473
|
+
this.deps.changed();
|
|
23474
|
+
return;
|
|
23475
|
+
}
|
|
23368
23476
|
if (this.state.kind !== "pressed") return;
|
|
23369
23477
|
const d = this.byId(this.state.id);
|
|
23370
23478
|
if (!d || d.locked) return;
|
|
23371
23479
|
if (!this.state.moved && Math.abs(x - this.state.px) <= DRAG_SLOP2 && Math.abs(y - this.state.py) <= DRAG_SLOP2) return;
|
|
23372
23480
|
this.state.moved = true;
|
|
23373
|
-
if (this.state.handle
|
|
23481
|
+
if (this.state.handle < 0) {
|
|
23482
|
+
this.dragBody(d, x, y);
|
|
23483
|
+
} else {
|
|
23374
23484
|
const a = d.anchors[this.state.handle];
|
|
23375
23485
|
if (a) {
|
|
23376
23486
|
const pivot = shift2 && ANGLE_SNAP_TYPES.has(d.type) && d.anchors.length === 2 ? d.anchors[1 - this.state.handle] : void 0;
|
|
@@ -23378,12 +23488,6 @@ var DrawingInteraction = class {
|
|
|
23378
23488
|
applyFree(a, point, d.anchorSchema().slots[this.state.handle]?.free ?? "both");
|
|
23379
23489
|
}
|
|
23380
23490
|
d.constrainHandleDrag(this.state.handle);
|
|
23381
|
-
} else {
|
|
23382
|
-
const pt = this.deps.projector().pxToPoint(x, y, d.paneId);
|
|
23383
|
-
this.snapAt = null;
|
|
23384
|
-
const dt = pt.time - this.state.grab.time;
|
|
23385
|
-
const dp = pt.price - this.state.grab.price;
|
|
23386
|
-
d.anchors = d.translateBody(dt, dp, this.state.orig);
|
|
23387
23491
|
}
|
|
23388
23492
|
this.deps.changed();
|
|
23389
23493
|
}
|
|
@@ -23403,16 +23507,46 @@ var DrawingInteraction = class {
|
|
|
23403
23507
|
}
|
|
23404
23508
|
return;
|
|
23405
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
|
+
}
|
|
23406
23517
|
if (this.state.kind !== "pressed") return;
|
|
23407
|
-
const { id, moved } = this.state;
|
|
23518
|
+
const { id, moved, riders, mod, clones } = this.state;
|
|
23408
23519
|
this.state = { kind: "idle" };
|
|
23409
23520
|
const d = this.byId(id);
|
|
23410
|
-
if (moved) {
|
|
23411
|
-
if (
|
|
23412
|
-
this.deps.
|
|
23413
|
-
|
|
23414
|
-
|
|
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] });
|
|
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);
|
|
23415
23548
|
}
|
|
23549
|
+
if (ids.length > this.deps.selectedIds().size) this.deps.emit({ kind: "select", ids });
|
|
23416
23550
|
}
|
|
23417
23551
|
/** Cancel an in-progress placement or drag (Escape). Returns whether anything was cancelled. */
|
|
23418
23552
|
cancel() {
|
|
@@ -23425,8 +23559,15 @@ var DrawingInteraction = class {
|
|
|
23425
23559
|
return true;
|
|
23426
23560
|
}
|
|
23427
23561
|
if (this.state.kind === "pressed") {
|
|
23428
|
-
|
|
23429
|
-
|
|
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") {
|
|
23430
23571
|
this.state = { kind: "idle" };
|
|
23431
23572
|
this.deps.changed();
|
|
23432
23573
|
return true;
|
|
@@ -23541,6 +23682,9 @@ var DrawingInteraction = class {
|
|
|
23541
23682
|
function cloneAnchors(d) {
|
|
23542
23683
|
return d.anchors.map((a) => ({ time: a.time, price: a.price }));
|
|
23543
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
|
+
}
|
|
23544
23688
|
function applyFree(anchor, pt, free) {
|
|
23545
23689
|
if (free === "both" || free === "x") anchor.time = pt.time;
|
|
23546
23690
|
if (free === "both" || free === "y") anchor.price = pt.price;
|
|
@@ -23926,10 +24070,27 @@ function btn(label, primary, onClick) {
|
|
|
23926
24070
|
}
|
|
23927
24071
|
|
|
23928
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
|
+
}
|
|
23929
24090
|
var BTN = 30;
|
|
23930
24091
|
var ICON = 21;
|
|
23931
24092
|
var STYLE_ID3 = "vela-drawing-popup-styles";
|
|
23932
|
-
var STYLE_REV = "
|
|
24093
|
+
var STYLE_REV = "4";
|
|
23933
24094
|
function ensureStyles3() {
|
|
23934
24095
|
if (typeof document === "undefined") return;
|
|
23935
24096
|
const existing = document.getElementById(STYLE_ID3);
|
|
@@ -23941,6 +24102,8 @@ function ensureStyles3() {
|
|
|
23941
24102
|
.vela-dpop-btn{background:transparent;color:var(--vela-fg-muted);transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
|
|
23942
24103
|
.vela-dpop-btn:hover{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
|
|
23943
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;}
|
|
23944
24107
|
.vela-dpop-item{background:transparent;transition:background var(--vela-dur-fast) ease;}
|
|
23945
24108
|
.vela-dpop-item:hover{background:var(--vela-hover-strong);}
|
|
23946
24109
|
.vela-dpop-item[data-active='1']{background:var(--vela-active);}
|
|
@@ -23972,7 +24135,7 @@ var DrawingSettingsPopup = class {
|
|
|
23972
24135
|
if (this.isOwnChrome(node)) return;
|
|
23973
24136
|
const cb = this.onClose;
|
|
23974
24137
|
this.close();
|
|
23975
|
-
cb?.();
|
|
24138
|
+
cb?.(e);
|
|
23976
24139
|
};
|
|
23977
24140
|
this.theme = theme;
|
|
23978
24141
|
this.settingsDialog = new DrawingSettingsDialog(host, theme);
|
|
@@ -23997,16 +24160,27 @@ var DrawingSettingsPopup = class {
|
|
|
23997
24160
|
contains(node) {
|
|
23998
24161
|
return node != null && (this.isOwnChrome(node) || this.settingsDialog.contains(node));
|
|
23999
24162
|
}
|
|
24000
|
-
/** Open the quick toolbar for `
|
|
24001
|
-
* `
|
|
24002
|
-
|
|
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) {
|
|
24003
24171
|
this.close();
|
|
24172
|
+
const drawing = drawings[0];
|
|
24173
|
+
if (!drawing) return;
|
|
24004
24174
|
ensureStyles3();
|
|
24005
24175
|
this.onClose = onClose ?? null;
|
|
24006
24176
|
const t = this.theme;
|
|
24177
|
+
const multi = drawings.length > 1;
|
|
24007
24178
|
const schema = drawing.schema();
|
|
24008
|
-
const paths =
|
|
24009
|
-
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");
|
|
24010
24184
|
const el = document.createElement("div");
|
|
24011
24185
|
el.className = "vela-dpop";
|
|
24012
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;`;
|
|
@@ -24020,75 +24194,84 @@ var DrawingSettingsPopup = class {
|
|
|
24020
24194
|
bar.style.cssText = "display:flex;align-items:center;gap:2px;padding:4px;";
|
|
24021
24195
|
bar.appendChild(this.dragHandle());
|
|
24022
24196
|
if (paths.has("glyph")) {
|
|
24023
|
-
const cur =
|
|
24197
|
+
const cur = common((d) => d.glyph ?? GLYPH_OPTIONS[0]);
|
|
24024
24198
|
bar.appendChild(this.dropdown("Icon", GLYPH_OPTIONS, cur, (g) => glyphIcon(g), (v) => actions.patch({ glyph: v })));
|
|
24025
24199
|
}
|
|
24026
24200
|
if (paths.has("size")) {
|
|
24027
|
-
const sz =
|
|
24201
|
+
const sz = common((d) => d.size ?? "normal");
|
|
24028
24202
|
bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label: sizeLabel }));
|
|
24029
24203
|
}
|
|
24030
|
-
if (paths.has("magnifier.timeframe") &&
|
|
24204
|
+
if (paths.has("magnifier.timeframe") && every((d) => d instanceof Magnifier)) {
|
|
24205
|
+
const mag = (d) => d;
|
|
24031
24206
|
const options = this.lowerTimeframeOptions();
|
|
24032
24207
|
if (options.length > 0) {
|
|
24033
24208
|
bar.appendChild(
|
|
24034
|
-
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 }), {
|
|
24035
24210
|
label: (v) => magnifierTimeframeLabel(String(v)),
|
|
24036
24211
|
labelInTrigger: true
|
|
24037
24212
|
})
|
|
24038
24213
|
);
|
|
24039
24214
|
}
|
|
24040
|
-
bar.appendChild(
|
|
24041
|
-
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"));
|
|
24042
24217
|
}
|
|
24043
|
-
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"));
|
|
24044
24219
|
if (paths.has("style.lineWidth")) {
|
|
24045
|
-
const
|
|
24046
|
-
|
|
24047
|
-
|
|
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 })));
|
|
24048
24229
|
} else {
|
|
24049
|
-
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 }));
|
|
24050
24231
|
}
|
|
24051
24232
|
}
|
|
24052
|
-
if (paths.has("style.lineStyle")) bar.appendChild(this.dropdown("Line style", LINE_STYLE_OPTIONS.map((o) => o.value),
|
|
24053
|
-
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"));
|
|
24054
24235
|
const isFrvp = paths.has("frvp.rows") && drawing instanceof FixedRangeVolumeProfile;
|
|
24055
|
-
const isPosition = paths.has("riskPercent") &&
|
|
24236
|
+
const isPosition = paths.has("riskPercent") && every((d) => d instanceof PositionTool);
|
|
24056
24237
|
if (isPosition) {
|
|
24057
|
-
const pos =
|
|
24058
|
-
bar.appendChild(
|
|
24059
|
-
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"));
|
|
24060
24241
|
}
|
|
24061
|
-
const
|
|
24062
|
-
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);
|
|
24063
24245
|
const styles = LINE_STYLE_OPTIONS.map((o) => o.value);
|
|
24064
|
-
bar.appendChild(
|
|
24065
|
-
bar.appendChild(this.dropdown("Midline style", styles, reg.midStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "reg.midStyle": v }), { label: styleLabel2 }));
|
|
24066
|
-
bar.appendChild(
|
|
24067
|
-
bar.appendChild(this.dropdown("Upper line style", styles, reg.upperStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "reg.upperStyle": v }), { label: styleLabel2 }));
|
|
24068
|
-
bar.appendChild(
|
|
24069
|
-
bar.appendChild(this.dropdown("Lower line style", styles, reg.lowerStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "reg.lowerStyle": v }), { label: styleLabel2 }));
|
|
24070
|
-
bar.appendChild(
|
|
24071
|
-
bar.appendChild(
|
|
24072
|
-
bar.appendChild(this.toggle("Show R\xB2", R2_ICON, reg.showR2, (v) => actions.patch({ "reg.showR2": v })));
|
|
24073
|
-
}
|
|
24074
|
-
const
|
|
24075
|
-
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);
|
|
24076
24259
|
const styles = LINE_STYLE_OPTIONS.map((o) => o.value);
|
|
24077
24260
|
const MULTS = [0.5, 1, 1.5, 2, 2.5, 3, 4, 5];
|
|
24078
|
-
bar.appendChild(
|
|
24079
|
-
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 }));
|
|
24080
24263
|
bar.appendChild(
|
|
24081
|
-
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 }), {
|
|
24082
24265
|
label: (v) => `${v}\u03C3`,
|
|
24083
24266
|
labelInTrigger: true
|
|
24084
24267
|
})
|
|
24085
24268
|
);
|
|
24086
|
-
bar.appendChild(
|
|
24087
|
-
bar.appendChild(
|
|
24088
|
-
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"));
|
|
24089
24272
|
}
|
|
24090
24273
|
if (paths.has("maxCurvature")) {
|
|
24091
|
-
const cur =
|
|
24274
|
+
const cur = common((d) => d.maxCurvature ?? 24);
|
|
24092
24275
|
bar.appendChild(
|
|
24093
24276
|
this.dropdown("Max curvature", DEDEKIND_CURVATURE_OPTIONS, cur, () => DEDEKIND_ICON2, (v) => actions.patch({ maxCurvature: v }), {
|
|
24094
24277
|
label: (v) => `n=${v}`,
|
|
@@ -24097,7 +24280,7 @@ var DrawingSettingsPopup = class {
|
|
|
24097
24280
|
);
|
|
24098
24281
|
}
|
|
24099
24282
|
if (paths.has("mach")) {
|
|
24100
|
-
const cur =
|
|
24283
|
+
const cur = common((d) => d.mach ?? 2);
|
|
24101
24284
|
bar.appendChild(
|
|
24102
24285
|
this.dropdown("Mach number", MACH_NUMBER_OPTIONS, cur, () => SUPERSONIC_ICON2, (v) => actions.patch({ mach: v }), {
|
|
24103
24286
|
label: (v) => `M=${v}`,
|
|
@@ -24106,7 +24289,7 @@ var DrawingSettingsPopup = class {
|
|
|
24106
24289
|
);
|
|
24107
24290
|
}
|
|
24108
24291
|
if (paths.has("waveCount")) {
|
|
24109
|
-
const cur =
|
|
24292
|
+
const cur = common((d) => d.waveCount ?? 6);
|
|
24110
24293
|
bar.appendChild(
|
|
24111
24294
|
this.dropdown("Waves", MACH_WAVE_COUNT_OPTIONS, cur, () => SONIC_ICON2, (v) => actions.patch({ waveCount: v }), {
|
|
24112
24295
|
label: (v) => `${v}`,
|
|
@@ -24114,26 +24297,27 @@ var DrawingSettingsPopup = class {
|
|
|
24114
24297
|
})
|
|
24115
24298
|
);
|
|
24116
24299
|
}
|
|
24117
|
-
const
|
|
24118
|
-
if (paths.has("showPrice")) bar.appendChild(this.toggle("Show price", PRICE_DELTA_ICON,
|
|
24119
|
-
if (paths.has("showDate")) bar.appendChild(this.toggle("Show date", DATE_DELTA_ICON,
|
|
24120
|
-
if (paths.has("text.color") && textOnBar) bar.appendChild(
|
|
24121
|
-
if (paths.has("text.size") && textOnBar) bar.appendChild(this.dropdown("Text size", TEXT_SIZE_OPTIONS.map((o) => o.value),
|
|
24122
|
-
|
|
24123
|
-
if (paths.has("text.
|
|
24124
|
-
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)));
|
|
24125
24309
|
const editableLevels = drawing.editableLevels();
|
|
24126
|
-
if (editableLevels && !(
|
|
24127
|
-
const fib =
|
|
24310
|
+
if (editableLevels && every((d) => d.editableLevels() != null && !(d instanceof MachFigure))) {
|
|
24311
|
+
const fib = (d) => d;
|
|
24128
24312
|
const sizes = TEXT_SIZE_OPTIONS.map((o) => o.value);
|
|
24129
|
-
bar.appendChild(this.dropdown("Numbers size", sizes, fib.numbersSize ?? "small", (s) => numbersSizeIcon(s), (v) => actions.patch({ numbersSize: v }), { label: sizeLabel }));
|
|
24130
|
-
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 }));
|
|
24131
24315
|
}
|
|
24132
24316
|
bar.appendChild(this.divider());
|
|
24133
|
-
if (isFrvp) bar.appendChild(this.iconBtn("Settings", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "frvp")));
|
|
24134
|
-
if (isPosition) bar.appendChild(this.iconBtn("Position size", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "position")));
|
|
24135
|
-
if (editableLevels) bar.appendChild(this.iconBtn("Levels", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "levels")));
|
|
24136
|
-
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));
|
|
24137
24321
|
const del = this.iconBtn("Delete", TRASH_ICON, () => actions.remove());
|
|
24138
24322
|
del.style.color = "var(--vela-danger)";
|
|
24139
24323
|
bar.appendChild(del);
|
|
@@ -24319,7 +24503,7 @@ var DrawingSettingsPopup = class {
|
|
|
24319
24503
|
});
|
|
24320
24504
|
return b;
|
|
24321
24505
|
}
|
|
24322
|
-
/** The trailing kebab overflow — z-order actions
|
|
24506
|
+
/** The trailing kebab overflow — duplicate, z-order actions, reset settings. */
|
|
24323
24507
|
kebabButton(actions) {
|
|
24324
24508
|
const b = this.base("More");
|
|
24325
24509
|
b.innerHTML = sized(KEBAB_ICON);
|
|
@@ -24331,6 +24515,7 @@ var DrawingSettingsPopup = class {
|
|
|
24331
24515
|
return;
|
|
24332
24516
|
}
|
|
24333
24517
|
this.openActionMenu(b, [
|
|
24518
|
+
{ icon: CLONE_ICON, label: "Duplicate", onClick: () => actions.duplicate() },
|
|
24334
24519
|
{ icon: FRONT_ICON, label: "Bring to front", onClick: () => actions.reorder("front") },
|
|
24335
24520
|
{ icon: BACK_ICON, label: "Send to back", onClick: () => actions.reorder("back") },
|
|
24336
24521
|
{ icon: RESET_ICON, label: "Reset settings", onClick: () => actions.resetSettings() }
|
|
@@ -24487,26 +24672,30 @@ var DrawingSettingsPopup = class {
|
|
|
24487
24672
|
this.tipEl?.remove();
|
|
24488
24673
|
this.tipEl = null;
|
|
24489
24674
|
}
|
|
24490
|
-
|
|
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) {
|
|
24491
24679
|
const b = this.base(tip);
|
|
24492
|
-
b.innerHTML = sized(icon2);
|
|
24493
24680
|
const set = (on2) => {
|
|
24494
|
-
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);
|
|
24495
24683
|
};
|
|
24496
24684
|
set(active);
|
|
24497
24685
|
let on = active;
|
|
24498
24686
|
b.addEventListener("click", () => {
|
|
24499
|
-
on = !on;
|
|
24687
|
+
on = on === MIXED ? true : !on;
|
|
24500
24688
|
set(on);
|
|
24501
24689
|
onChange(on);
|
|
24502
24690
|
});
|
|
24503
24691
|
return b;
|
|
24504
24692
|
}
|
|
24505
24693
|
/** An inline numeric width field for tools whose stroke range outgrows the 1–4px
|
|
24506
|
-
* 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. */
|
|
24507
24696
|
widthInput(tip, value, min, max, step, onChange) {
|
|
24508
24697
|
const ni = new NumberInput({
|
|
24509
|
-
value,
|
|
24698
|
+
value: value === MIXED ? min : value,
|
|
24510
24699
|
min,
|
|
24511
24700
|
max,
|
|
24512
24701
|
step,
|
|
@@ -24518,13 +24707,18 @@ var DrawingSettingsPopup = class {
|
|
|
24518
24707
|
title: tip,
|
|
24519
24708
|
onChange
|
|
24520
24709
|
});
|
|
24710
|
+
if (value === MIXED) {
|
|
24711
|
+
ni.input.value = "";
|
|
24712
|
+
ni.input.placeholder = "\u2014";
|
|
24713
|
+
}
|
|
24521
24714
|
ni.el.dataset.tip = tip;
|
|
24522
24715
|
trapChartKeys(ni.el);
|
|
24523
24716
|
return ni.el;
|
|
24524
24717
|
}
|
|
24525
24718
|
/** A pick-one dropdown: the trigger shows the current value's glyph (plus an optional inline
|
|
24526
24719
|
* text label — e.g. `2px` — for controls that would otherwise look alike), and clicking it
|
|
24527
|
-
* 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. */
|
|
24528
24722
|
dropdown(tip, values, current, render, onChange, opts = {}) {
|
|
24529
24723
|
const b = this.base(tip);
|
|
24530
24724
|
b.style.width = "auto";
|
|
@@ -24534,14 +24728,20 @@ var DrawingSettingsPopup = class {
|
|
|
24534
24728
|
let cur = current;
|
|
24535
24729
|
const paint = (v) => {
|
|
24536
24730
|
b.replaceChildren();
|
|
24537
|
-
|
|
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);
|
|
24538
24738
|
if (glyph) {
|
|
24539
24739
|
const ic = document.createElement("span");
|
|
24540
24740
|
ic.style.cssText = "display:flex;";
|
|
24541
24741
|
ic.innerHTML = sized(glyph);
|
|
24542
24742
|
b.appendChild(ic);
|
|
24543
24743
|
}
|
|
24544
|
-
if (opts.label && opts.labelInTrigger) {
|
|
24744
|
+
if (opts.label && opts.labelInTrigger && v !== MIXED) {
|
|
24545
24745
|
const tx = document.createElement("span");
|
|
24546
24746
|
tx.textContent = opts.label(v);
|
|
24547
24747
|
tx.style.cssText = "font-size:11px;opacity:0.85;font-variant-numeric:tabular-nums;";
|
|
@@ -24611,8 +24811,10 @@ var DrawingSettingsPopup = class {
|
|
|
24611
24811
|
}
|
|
24612
24812
|
/** A color control: an `icon` over a thin **colored underline** showing the current
|
|
24613
24813
|
* value (the common idiom), with an invisible native picker overlaid to edit it.
|
|
24614
|
-
* `iconSize` shrinks it for the floating text controls.
|
|
24615
|
-
|
|
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 = []) {
|
|
24616
24818
|
const b = document.createElement("button");
|
|
24617
24819
|
b.type = "button";
|
|
24618
24820
|
b.dataset.tip = tip;
|
|
@@ -24622,13 +24824,14 @@ var DrawingSettingsPopup = class {
|
|
|
24622
24824
|
ic.style.cssText = "display:flex;";
|
|
24623
24825
|
ic.innerHTML = sized(icon2, iconSize);
|
|
24624
24826
|
const bar = document.createElement("span");
|
|
24625
|
-
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};`;
|
|
24626
24828
|
b.append(ic, bar);
|
|
24627
24829
|
let cur = color;
|
|
24628
24830
|
b.addEventListener("pointerdown", (e) => e.stopPropagation());
|
|
24629
24831
|
b.addEventListener("click", (e) => {
|
|
24630
24832
|
e.stopPropagation();
|
|
24631
|
-
|
|
24833
|
+
const seed = cur === MIXED ? used[0] ?? DEFAULT_DRAWING_COLOR : cur;
|
|
24834
|
+
this.toggleColorPopover(b, seed, cur === MIXED ? used : [], (v) => {
|
|
24632
24835
|
cur = v;
|
|
24633
24836
|
bar.style.background = v;
|
|
24634
24837
|
onChange(v);
|
|
@@ -24637,20 +24840,42 @@ var DrawingSettingsPopup = class {
|
|
|
24637
24840
|
return b;
|
|
24638
24841
|
}
|
|
24639
24842
|
/** Open the color popover for `anchor`, or close it if it's already this anchor's (toggle). */
|
|
24640
|
-
toggleColorPopover(anchor, color, onChange) {
|
|
24843
|
+
toggleColorPopover(anchor, color, used, onChange) {
|
|
24641
24844
|
if (this.colorOwner === anchor) {
|
|
24642
24845
|
this.closeColorPopover();
|
|
24643
24846
|
return;
|
|
24644
24847
|
}
|
|
24645
|
-
this.openColorPopover(anchor, color, onChange);
|
|
24848
|
+
this.openColorPopover(anchor, color, used, onChange);
|
|
24646
24849
|
}
|
|
24647
|
-
/** A floating RGB picker + opacity slider anchored to a color swatch — emits `#RRGGBB(AA)`.
|
|
24648
|
-
|
|
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) {
|
|
24649
24853
|
const t = this.theme;
|
|
24650
24854
|
this.colorPop = this.hostFloat(anchor, {
|
|
24651
24855
|
zIndex: 25,
|
|
24652
24856
|
padding: "10px",
|
|
24653
|
-
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
|
+
}
|
|
24654
24879
|
el.appendChild(buildColorPicker(color, t, onChange));
|
|
24655
24880
|
}
|
|
24656
24881
|
});
|
|
@@ -24671,6 +24896,7 @@ var TYPE_ICON = icon("type");
|
|
|
24671
24896
|
var PRICE_DELTA_ICON = icon("price-delta");
|
|
24672
24897
|
var DATE_DELTA_ICON = icon("date-delta");
|
|
24673
24898
|
var LOCK_ICON = icon("lock");
|
|
24899
|
+
var UNLOCK_ICON = icon("unlock");
|
|
24674
24900
|
var FRONT_ICON = icon("bring-front");
|
|
24675
24901
|
var BACK_ICON = icon("send-back");
|
|
24676
24902
|
var TRASH_ICON = icon("trash");
|
|
@@ -24678,6 +24904,7 @@ var BOLD_ICON = icon("bold");
|
|
|
24678
24904
|
var ITALIC_ICON = icon("italic");
|
|
24679
24905
|
var GRIP_ICON = icon("grip");
|
|
24680
24906
|
var KEBAB_ICON = icon("kebab");
|
|
24907
|
+
var CLONE_ICON = icon("clone");
|
|
24681
24908
|
var RESET_ICON = icon("reset");
|
|
24682
24909
|
var GEAR_ICON = icon("gear");
|
|
24683
24910
|
var CHEVRON_ICON = icon("chevron-down");
|
|
@@ -24686,6 +24913,12 @@ var BANDS_ICON = icon("bands");
|
|
|
24686
24913
|
var DEDEKIND_ICON2 = icon("dedekind");
|
|
24687
24914
|
var SONIC_ICON2 = icon("sonic");
|
|
24688
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
|
+
}
|
|
24689
24922
|
function lineIcon(width, style) {
|
|
24690
24923
|
const dash = style === "dashed" ? "5,3" : style === "dotted" ? "1.5,3" : "";
|
|
24691
24924
|
return svg24(`<line x1="3.5" y1="12" x2="20.5" y2="12" stroke-width="${Number(width) || 2}" stroke-dasharray="${dash}"/>`);
|
|
@@ -25601,6 +25834,20 @@ var UserDrawingController = class {
|
|
|
25601
25834
|
// reported exactly when it actually changes).
|
|
25602
25835
|
this.textEditor = null;
|
|
25603
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
|
+
};
|
|
25604
25851
|
this.ctx = canvas.getContext("2d");
|
|
25605
25852
|
this.popup = new DrawingSettingsPopup(overlayHost, deps.theme(), () => deps.chartBarMs());
|
|
25606
25853
|
this.toolbar = new DrawingToolbar(
|
|
@@ -25777,6 +26024,12 @@ var UserDrawingController = class {
|
|
|
25777
26024
|
}
|
|
25778
26025
|
setSelection(ids) {
|
|
25779
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
|
+
}
|
|
25780
26033
|
this.render();
|
|
25781
26034
|
}
|
|
25782
26035
|
/** Programmatic twin of clicking the drawing: highlight it + float its settings popup. */
|
|
@@ -25822,12 +26075,16 @@ var UserDrawingController = class {
|
|
|
25822
26075
|
});
|
|
25823
26076
|
}
|
|
25824
26077
|
/** Delete the (unlocked) drawing under the cursor. True when one was removed.
|
|
25825
|
-
* Shared by the eraser (click + drag) and the middle-click shortcut
|
|
25826
|
-
|
|
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) {
|
|
25827
26082
|
const hit = topDrawingAt(this.drawings, x, y, this.deps.projector(), HIT_TOLERANCE);
|
|
25828
|
-
if (!hit
|
|
26083
|
+
if (!hit) return false;
|
|
26084
|
+
const ids = deleteTargets(hit, this.selectedIds, this.drawings, withSelection);
|
|
26085
|
+
if (ids.length === 0) return false;
|
|
25829
26086
|
this.popup.close();
|
|
25830
|
-
this.emit({ kind: "delete", ids
|
|
26087
|
+
this.emit({ kind: "delete", ids });
|
|
25831
26088
|
return true;
|
|
25832
26089
|
}
|
|
25833
26090
|
/** Shift+press on the empty plot: arm the measure ruler AND start it at (x, y) in one
|
|
@@ -25853,7 +26110,14 @@ var UserDrawingController = class {
|
|
|
25853
26110
|
this.render();
|
|
25854
26111
|
}
|
|
25855
26112
|
}
|
|
25856
|
-
|
|
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) {
|
|
25857
26121
|
if (this.eraserMode) {
|
|
25858
26122
|
this.erasing = true;
|
|
25859
26123
|
this.deleteAt(x, y);
|
|
@@ -25873,9 +26137,9 @@ var UserDrawingController = class {
|
|
|
25873
26137
|
return;
|
|
25874
26138
|
}
|
|
25875
26139
|
}
|
|
25876
|
-
this.interaction.down(x, y, snap, shift2);
|
|
26140
|
+
this.interaction.down(x, y, snap, shift2, mod);
|
|
25877
26141
|
}
|
|
25878
|
-
pointerMove(x, y, snap = "off", shift2 = false) {
|
|
26142
|
+
pointerMove(x, y, snap = "off", shift2 = false, mod = false) {
|
|
25879
26143
|
if (this.eraserMode) {
|
|
25880
26144
|
if (this.erasing) this.deleteAt(x, y);
|
|
25881
26145
|
return;
|
|
@@ -25887,12 +26151,14 @@ var UserDrawingController = class {
|
|
|
25887
26151
|
return;
|
|
25888
26152
|
}
|
|
25889
26153
|
this.interaction.move(x, y, snap, shift2);
|
|
25890
|
-
this.updateHover(x, y);
|
|
26154
|
+
this.updateHover(x, y, mod);
|
|
25891
26155
|
}
|
|
25892
|
-
/** Track which drawing is hovered so its handles appear on hover (and only then).
|
|
25893
|
-
|
|
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) {
|
|
25894
26160
|
let id = null;
|
|
25895
|
-
if (this.activeTool == null && !this.interaction.isPlacing() && !this.interaction.isDragging()) {
|
|
26161
|
+
if (!mod && this.activeTool == null && !this.interaction.isPlacing() && !this.interaction.isDragging()) {
|
|
25896
26162
|
id = topDrawingAt(this.drawings, x, y, this.deps.projector(), HIT_TOLERANCE)?.id ?? null;
|
|
25897
26163
|
}
|
|
25898
26164
|
if (id !== this.hoveredId) {
|
|
@@ -25912,7 +26178,12 @@ var UserDrawingController = class {
|
|
|
25912
26178
|
this.render();
|
|
25913
26179
|
return;
|
|
25914
26180
|
}
|
|
26181
|
+
const dragged = this.interaction.isDragging() ? this.interaction.pressedId() : null;
|
|
25915
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
|
+
}
|
|
25916
26187
|
}
|
|
25917
26188
|
/** Toggle the transient ruler. Arming it clears any drawing tool + selection. */
|
|
25918
26189
|
toggleMeasure() {
|
|
@@ -26177,7 +26448,7 @@ var UserDrawingController = class {
|
|
|
26177
26448
|
this.emit({ kind: "duplicate", ids: this.selectionIds() });
|
|
26178
26449
|
break;
|
|
26179
26450
|
case "delete": {
|
|
26180
|
-
const ids = this.selectedIds.size ? this.
|
|
26451
|
+
const ids = this.selectedIds.size ? this.deletableSelection() : this.hoveredId ? [this.hoveredId] : [];
|
|
26181
26452
|
if (ids.length) {
|
|
26182
26453
|
this.popup.close();
|
|
26183
26454
|
this.emit({ kind: "delete", ids });
|
|
@@ -26193,6 +26464,9 @@ var UserDrawingController = class {
|
|
|
26193
26464
|
selectionIds() {
|
|
26194
26465
|
return [...this.selectedIds];
|
|
26195
26466
|
}
|
|
26467
|
+
deletableSelection() {
|
|
26468
|
+
return deletableSelection(this.selectedIds, this.drawings);
|
|
26469
|
+
}
|
|
26196
26470
|
/** Move every selected (unlocked) drawing by a pixel delta — one edit/edit-many → one undo step. */
|
|
26197
26471
|
nudgeSelection(dx, dy) {
|
|
26198
26472
|
const proj = this.deps.projector();
|
|
@@ -26301,6 +26575,8 @@ var UserDrawingController = class {
|
|
|
26301
26575
|
this.painter.seriesLook = this.deps.seriesLook();
|
|
26302
26576
|
this.painter.paintAll(ctx, this.drawings.filter((d) => !this.isInterleaved(d)), proj, this.deps.theme(), targets);
|
|
26303
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)) });
|
|
26304
26580
|
this.layoutTextEditor();
|
|
26305
26581
|
const ghost = this.interaction.ghost();
|
|
26306
26582
|
if (ghost) this.painter.paintGhost(ctx, ghost, proj, this.deps.theme());
|
|
@@ -26316,6 +26592,8 @@ var UserDrawingController = class {
|
|
|
26316
26592
|
const my = m ? proj.yOf(m.point.price, m.paneId) : null;
|
|
26317
26593
|
if (m && my != null) this.painter.paintSnapRing(ctx, proj.xOf(m.point.time), my, this.deps.theme());
|
|
26318
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);
|
|
26319
26597
|
}
|
|
26320
26598
|
destroy() {
|
|
26321
26599
|
this.closeTextEditor();
|
|
@@ -26333,15 +26611,80 @@ var UserDrawingController = class {
|
|
|
26333
26611
|
if (this.selectedIds.size) this.emit({ kind: "select", ids: [] });
|
|
26334
26612
|
this.render();
|
|
26335
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
|
+
}
|
|
26336
26678
|
/** A click on a drawing opens its settings toolbar (text labels edit text here too). */
|
|
26337
26679
|
openSettingsById(id, _x, _y) {
|
|
26338
26680
|
const drawing = this.drawings.find((d) => d.id === id);
|
|
26339
26681
|
if (!drawing) return;
|
|
26340
26682
|
const live = () => this.drawings.find((d) => d.id === id);
|
|
26683
|
+
this.popupMulti = false;
|
|
26341
26684
|
this.emit({ kind: "select", ids: [id] });
|
|
26342
26685
|
this.emit({ kind: "settings", id });
|
|
26343
26686
|
const anchor = drawing.bounds(this.deps.projector());
|
|
26344
|
-
this.popup.open(drawing, anchor, {
|
|
26687
|
+
this.popup.open([drawing], anchor, {
|
|
26345
26688
|
// Sync rebuilds instances, so a panel that reads values back after a patch (e.g. the
|
|
26346
26689
|
// position tool's price fields, where one edit can flip another level) resolves fresh.
|
|
26347
26690
|
resolve: () => live() ?? null,
|
|
@@ -26359,6 +26702,11 @@ var UserDrawingController = class {
|
|
|
26359
26702
|
this.emit({ kind: "edit", doc: d.serialize() });
|
|
26360
26703
|
},
|
|
26361
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
|
+
},
|
|
26362
26710
|
resetSettings: () => {
|
|
26363
26711
|
const d = live();
|
|
26364
26712
|
if (!d) return;
|
|
@@ -26381,7 +26729,13 @@ var UserDrawingController = class {
|
|
|
26381
26729
|
this.popup.close();
|
|
26382
26730
|
this.emit({ kind: "delete", ids: [id] });
|
|
26383
26731
|
}
|
|
26384
|
-
},
|
|
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();
|
|
26385
26739
|
}
|
|
26386
26740
|
/** Report placement progress upstream (`draft` intent) so the drawings sync can
|
|
26387
26741
|
* mirror the ghost live on linked charts. Called from every render; only actual
|
|
@@ -26407,6 +26761,13 @@ var UserDrawingController = class {
|
|
|
26407
26761
|
}, 0);
|
|
26408
26762
|
return;
|
|
26409
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
|
+
}
|
|
26410
26771
|
this.intentCb?.(i);
|
|
26411
26772
|
}
|
|
26412
26773
|
};
|
|
@@ -28718,7 +29079,10 @@ var NativeRenderer = class {
|
|
|
28718
29079
|
zoomTo: (target, anchorLogical, anchorX) => this.zoomTo(target, anchorLogical, anchorX),
|
|
28719
29080
|
fling: (v) => this.fling(v),
|
|
28720
29081
|
onPointerMove: (x, y) => this.handlePointerMove(x, y),
|
|
28721
|
-
onClick: (x) =>
|
|
29082
|
+
onClick: (x) => {
|
|
29083
|
+
this.userDrawings?.deselect();
|
|
29084
|
+
this.handleClick(x);
|
|
29085
|
+
},
|
|
28722
29086
|
onAxisLongPress: (axis, x, y) => {
|
|
28723
29087
|
for (const cb of this.axisLongPressCbs) cb({ axis, x, y });
|
|
28724
29088
|
},
|
|
@@ -28736,11 +29100,13 @@ var NativeRenderer = class {
|
|
|
28736
29100
|
// User drawings claim a gesture before pan when armed / over a drawing.
|
|
28737
29101
|
drawingsClaim: (x, y) => this.userDrawings?.claim(x, y) ?? false,
|
|
28738
29102
|
drawingsMeasureStart: (x, y, snap) => this.userDrawings?.beginMeasureAt(x, y, snap) ?? false,
|
|
28739
|
-
|
|
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
|
|
28740
29106
|
drawingsCancelPlacement: () => this.userDrawings?.cancelPlacement() ?? false,
|
|
28741
29107
|
drawingsSnapMode: () => this.snapMode,
|
|
28742
|
-
drawingsPointerDown: (x, y, snap, shift2) => this.userDrawings?.pointerDown(x, y, snap, shift2),
|
|
28743
|
-
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),
|
|
28744
29110
|
drawingsPointerUp: (x, y, snap) => this.userDrawings?.pointerUp(x, y, snap),
|
|
28745
29111
|
drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? null,
|
|
28746
29112
|
drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
|
|
@@ -31103,6 +31469,10 @@ function str3(inputs, key, fallback) {
|
|
|
31103
31469
|
const v = inputs[key];
|
|
31104
31470
|
return typeof v === "string" && v !== "" ? v : fallback;
|
|
31105
31471
|
}
|
|
31472
|
+
function bool(inputs, key, fallback) {
|
|
31473
|
+
const v = inputs[key];
|
|
31474
|
+
return typeof v === "boolean" ? v : fallback;
|
|
31475
|
+
}
|
|
31106
31476
|
function toPoints(bars, values, colors) {
|
|
31107
31477
|
const pts = new Array(bars.length);
|
|
31108
31478
|
for (let i = 0; i < bars.length; i++) {
|
|
@@ -32634,10 +33004,6 @@ var aroon = {
|
|
|
32634
33004
|
plots: [
|
|
32635
33005
|
{ key: "up", title: "Aroon up", values: up, color: BULLISH, width: 2 },
|
|
32636
33006
|
{ key: "down", title: "Aroon down", values: down, color: BEARISH, width: 2 }
|
|
32637
|
-
],
|
|
32638
|
-
levels: [
|
|
32639
|
-
{ key: "upper", price: 100, color: NEUTRAL, lineStyle: "dotted" },
|
|
32640
|
-
{ key: "lower", price: 0, color: NEUTRAL, lineStyle: "dotted" }
|
|
32641
33007
|
]
|
|
32642
33008
|
};
|
|
32643
33009
|
}
|
|
@@ -33135,8 +33501,10 @@ var volumeSpecs = [
|
|
|
33135
33501
|
var DAY_MS2 = 864e5;
|
|
33136
33502
|
function periodKey(anchor, time) {
|
|
33137
33503
|
if (anchor === "Week") return Math.floor((Math.floor(time / DAY_MS2) + 3) / 7);
|
|
33138
|
-
if (anchor === "Month") {
|
|
33504
|
+
if (anchor === "Month" || anchor === "Quarter" || anchor === "Year") {
|
|
33139
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);
|
|
33140
33508
|
return d.getUTCFullYear() * 12 + d.getUTCMonth();
|
|
33141
33509
|
}
|
|
33142
33510
|
return Math.floor(time / DAY_MS2);
|
|
@@ -33188,40 +33556,86 @@ var parabolicSar = {
|
|
|
33188
33556
|
return { plots: [{ key: "sar", title: "PSAR", values, kind: "circles", color: SERIES_LINE, colors }] };
|
|
33189
33557
|
}
|
|
33190
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
|
+
];
|
|
33191
33566
|
var vwap = {
|
|
33192
33567
|
type: "vwap",
|
|
33193
33568
|
title: "Volume Weighted Average Price",
|
|
33194
33569
|
shortTitle: "VWAP",
|
|
33195
33570
|
overlay: true,
|
|
33196
33571
|
inputs: [
|
|
33197
|
-
{ key: "anchor", title: "
|
|
33198
|
-
|
|
33199
|
-
|
|
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
|
+
])
|
|
33200
33586
|
],
|
|
33201
33587
|
compute: (bars, inputs) => {
|
|
33202
33588
|
const anchor = str3(inputs, "anchor", "Day");
|
|
33203
|
-
const
|
|
33204
|
-
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
|
+
}));
|
|
33205
33600
|
let period = Number.NaN;
|
|
33206
33601
|
let cumPV = 0;
|
|
33602
|
+
let cumPV2 = 0;
|
|
33207
33603
|
let cumV = 0;
|
|
33208
|
-
for (let i = 0; i <
|
|
33604
|
+
for (let i = 0; i < n; i++) {
|
|
33209
33605
|
const b = bars[i];
|
|
33210
33606
|
const key = periodKey(anchor, b.time);
|
|
33211
33607
|
if (key !== period) {
|
|
33212
33608
|
period = key;
|
|
33213
33609
|
cumPV = 0;
|
|
33610
|
+
cumPV2 = 0;
|
|
33214
33611
|
cumV = 0;
|
|
33215
33612
|
}
|
|
33216
33613
|
const v = b.volume;
|
|
33217
33614
|
if (v != null && Number.isFinite(v) && v > 0) {
|
|
33218
|
-
const tp =
|
|
33615
|
+
const tp = src[i];
|
|
33219
33616
|
cumPV += tp * v;
|
|
33617
|
+
cumPV2 += tp * tp * v;
|
|
33220
33618
|
cumV += v;
|
|
33221
33619
|
}
|
|
33222
|
-
if (cumV
|
|
33223
|
-
|
|
33224
|
-
|
|
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 };
|
|
33225
33639
|
}
|
|
33226
33640
|
};
|
|
33227
33641
|
var pivotPoints = {
|