@luxalgo/vela 0.6.16 → 0.6.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/{DataProvider-XLdJB86x.d.ts → DataProvider-Cb-2lC5O.d.ts} +1 -1
- package/dist/{DataProvider-xH2VBcFT.d.cts → DataProvider-UtWw2CgJ.d.cts} +1 -1
- package/dist/{chunk-GIJYO4LO.js → chunk-22QJKGXX.js} +666 -189
- package/dist/{chunk-SEOO6YG7.js → chunk-BXF7VNNE.js} +181 -111
- package/dist/{chunk-6DPAYEGO.js → chunk-CAFCLMPF.js} +1 -0
- package/dist/{chunk-I77F2SVE.js → chunk-IFJJPXSV.js} +1 -1
- package/dist/{chunk-MGU3USTP.js → chunk-T5Z5YUCF.js} +1 -1
- package/dist/{chunk-Q3TFJNJW.js → chunk-XCBJU674.js} +2 -2
- package/dist/{contributions-BEkxwA5F.d.cts → contributions-5nz3PLRe.d.ts} +9 -4
- package/dist/{contributions-CoAX7_SF.d.ts → contributions-DEPRtkmG.d.cts} +9 -4
- package/dist/index.cjs +662 -185
- package/dist/index.d.cts +15 -6
- package/dist/index.d.ts +15 -6
- package/dist/index.js +4 -4
- package/dist/{options-bdSAtp7y.d.ts → options-BCRmYALw.d.cts} +24 -5
- package/dist/{options-bdSAtp7y.d.cts → options-BCRmYALw.d.ts} +24 -5
- package/dist/{plugin-CNUgbRNq.d.ts → plugin-CUtfLeqz.d.ts} +3 -3
- package/dist/{plugin-Dxyq35Zf.d.cts → plugin-DVUaROPo.d.cts} +3 -3
- package/dist/plugin.cjs +1 -0
- package/dist/plugin.d.cts +4 -4
- package/dist/plugin.d.ts +4 -4
- package/dist/plugin.js +2 -2
- package/dist/providers/binance.d.cts +2 -2
- package/dist/providers/binance.d.ts +2 -2
- package/dist/providers/coinbase.d.cts +2 -2
- package/dist/providers/coinbase.d.ts +2 -2
- package/dist/providers/hyperliquid.d.cts +2 -2
- package/dist/providers/hyperliquid.d.ts +2 -2
- package/dist/{statusline-model-CbPza-Wc.d.cts → statusline-model-7ECSDtgt.d.cts} +3 -3
- package/dist/{statusline-model-D7cWkw8A.d.ts → statusline-model-D1Fhs8Dk.d.ts} +3 -3
- package/dist/ui.cjs +1 -0
- package/dist/ui.d.cts +1 -1
- package/dist/ui.d.ts +1 -1
- package/dist/ui.js +3 -3
- package/dist/vela.global.js +662 -185
- package/dist/vela.global.min.js +52 -53
- package/dist/widget.cjs +839 -291
- package/dist/widget.d.cts +39 -12
- package/dist/widget.d.ts +39 -12
- package/dist/widget.js +7 -7
- package/dist/workspace.cjs +839 -291
- package/dist/workspace.d.cts +5 -5
- package/dist/workspace.d.ts +5 -5
- package/dist/workspace.js +6 -6
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { deserializeDrawing, chartTypes, rendererLayers, foldBaseModulation, registerChartType, rendererDefaults, getDrawingType, settingsRowVisible, normalizeSettingsRow, Magnifier, TextLabel, Callout, resetDrawingSettings, registerNativeIndicator, chartType, getNativeIndicator, nativeIndicatorDescriptors, drawingTypes, settingsRowValueKeys, formatDuration, RegressionChannel, FixedRangeVolumeProfile, SegmentDrawing, ArrowMark, GlyphStamp, RadialFib, FibSpiral, DedekindTessellation, MachFigure, GannSquare, FibRatios, MeasureBox, PositionTool, AnchoredVwap, PatternDrawing, Comment, PriceNote, Signpost, Note, PriceLabel, GANN_SQUARE_ARCS, magnifierTimeframeLabel, lineSegmentIntersection,
|
|
2
|
-
import { themeTokens, Dialog, closeOpenPopovers, closeWidthPopover, fieldSection, buildFieldControl, fieldSeparator, fieldGrid, fieldRow, blendOver, splitColor, CalloutBubble, Menu, TextArea, Popover, NumberInput, buildColorPicker, isPopoverOpen, eventDismissedPopover, toggleSelectList, openPopoverTrigger, fieldGridColumns, FIELD_GAP_PX, STATIC_TOKENS } from './chunk-
|
|
3
|
-
import { icon, iconAt, withAlpha, WARNING, ACCENT, NEUTRAL,
|
|
1
|
+
import { deserializeDrawing, chartTypes, rendererLayers, foldBaseModulation, registerChartType, rendererDefaults, getDrawingType, settingsRowVisible, normalizeSettingsRow, Magnifier, TextLabel, Callout, resetDrawingSettings, registerNativeIndicator, chartType, getNativeIndicator, nativeIndicatorDescriptors, drawingTypes, settingsRowValueKeys, formatDuration, RegressionChannel, FixedRangeVolumeProfile, DEFAULT_DRAWING_COLOR, SegmentDrawing, ArrowMark, GlyphStamp, RadialFib, FibSpiral, DedekindTessellation, MachFigure, GannSquare, FibRatios, MeasureBox, PositionTool, AnchoredVwap, PatternDrawing, Comment, PriceNote, Signpost, Note, PriceLabel, GANN_SQUARE_ARCS, magnifierTimeframeLabel, lineSegmentIntersection, MAGNIFIER_TIMEFRAME_OPTIONS, GLYPH_OPTIONS, STAMP_SIZE_OPTIONS, LINE_STYLE_OPTIONS, DEDEKIND_CURVATURE_OPTIONS, MACH_NUMBER_OPTIONS, MACH_WAVE_COUNT_OPTIONS, TEXT_SIZE_OPTIONS, createDrawing, inputVisible, tickerModifierIds, CalloutBase, DIRECTION_OPTIONS, stableSeriesId } from './chunk-IFJJPXSV.js';
|
|
2
|
+
import { themeTokens, Dialog, closeOpenPopovers, closeWidthPopover, fieldSection, buildFieldControl, fieldSeparator, fieldGrid, fieldRow, blendOver, splitColor, CalloutBubble, Menu, TextArea, Popover, NumberInput, buildColorPicker, isPopoverOpen, eventDismissedPopover, toggleSelectList, openPopoverTrigger, fieldGridColumns, FIELD_GAP_PX, STATIC_TOKENS } from './chunk-T5Z5YUCF.js';
|
|
3
|
+
import { icon, iconAt, withAlpha, WARNING, ACCENT, NEUTRAL, BULLISH, CATEGORICAL, SERIES_LINE, BEARISH, isDarkColor, INFO, CHIP_PLATE, CROSSHAIR, TRADE_EXIT, TRADE_SHORT, TRADE_LONG, overlayScrollbarCss, SLATE_DEEP, VALID, INVALID, SLATE, FIELD_FOCUS_CSS, FIELD_FOCUS_RING, svg24, svg24Solid } from './chunk-CAFCLMPF.js';
|
|
4
4
|
|
|
5
5
|
// src/core/events/EventBus.ts
|
|
6
6
|
var TypedEventBus = class {
|
|
@@ -2161,15 +2161,18 @@ var DrawingController = class {
|
|
|
2161
2161
|
next = [...ids];
|
|
2162
2162
|
}
|
|
2163
2163
|
this.selectedIds = next.filter((id) => this.store.has(id));
|
|
2164
|
+
this.announceSelection();
|
|
2165
|
+
}
|
|
2166
|
+
/** Push the selection to the renderer and announce it (primary + the full list). */
|
|
2167
|
+
announceSelection() {
|
|
2164
2168
|
this.port?.setSelection(this.selectedIds);
|
|
2165
|
-
this.events.emit("drawing:selected", { id: this.selectedIds[0] ?? null });
|
|
2169
|
+
this.events.emit("drawing:selected", { id: this.selectedIds[0] ?? null, ids: [...this.selectedIds] });
|
|
2166
2170
|
}
|
|
2167
2171
|
/** Restore a history snapshot, reconciling selection against what survived. */
|
|
2168
2172
|
restoreSnapshot(doc) {
|
|
2169
2173
|
this.store.load(doc);
|
|
2170
2174
|
this.selectedIds = this.selectedIds.filter((id) => this.store.has(id));
|
|
2171
|
-
this.
|
|
2172
|
-
this.events.emit("drawing:selected", { id: this.selectedIds[0] ?? null });
|
|
2175
|
+
this.announceSelection();
|
|
2173
2176
|
}
|
|
2174
2177
|
/** Delete drawings as one undo step; prune them from the selection. */
|
|
2175
2178
|
deleteMany(ids) {
|
|
@@ -2302,6 +2305,9 @@ var DrawingController = class {
|
|
|
2302
2305
|
case "duplicate":
|
|
2303
2306
|
this.duplicate(i.ids);
|
|
2304
2307
|
break;
|
|
2308
|
+
case "clone":
|
|
2309
|
+
if (this.enabled && i.docs.length) this.insertClones(i.docs);
|
|
2310
|
+
break;
|
|
2305
2311
|
case "copy":
|
|
2306
2312
|
this.copy(i.ids);
|
|
2307
2313
|
break;
|
|
@@ -2368,6 +2374,23 @@ function attachChromeTooltip(anchor, opts) {
|
|
|
2368
2374
|
clear();
|
|
2369
2375
|
};
|
|
2370
2376
|
}
|
|
2377
|
+
function chromeHint(text, opts) {
|
|
2378
|
+
const px = opts.size ?? 16;
|
|
2379
|
+
const el = document.createElement("span");
|
|
2380
|
+
el.className = "vela-hint";
|
|
2381
|
+
el.setAttribute("role", "img");
|
|
2382
|
+
el.setAttribute("aria-label", text);
|
|
2383
|
+
el.innerHTML = iconAt("info", px);
|
|
2384
|
+
el.style.cssText = `flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:${px}px;height:${px}px;border-radius:50%;cursor:help;user-select:none;background:var(--vela-hover);color:var(--vela-fg-muted);transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;`;
|
|
2385
|
+
const hover = (on) => {
|
|
2386
|
+
el.style.background = on ? "var(--vela-active)" : "var(--vela-hover)";
|
|
2387
|
+
el.style.color = on ? "var(--vela-fg-bright)" : "var(--vela-fg-muted)";
|
|
2388
|
+
};
|
|
2389
|
+
el.addEventListener("pointerenter", () => hover(true));
|
|
2390
|
+
el.addEventListener("pointerleave", () => hover(false));
|
|
2391
|
+
const dispose = attachChromeTooltip(el, { host: opts.host, theme: opts.theme, text: () => text, wrap: true, delayMs: 250 });
|
|
2392
|
+
return { el, dispose };
|
|
2393
|
+
}
|
|
2371
2394
|
|
|
2372
2395
|
// src/renderers/native/drawings/DrawingToolbar.ts
|
|
2373
2396
|
var TOOLBAR_WIDTH = 44;
|
|
@@ -3001,6 +3024,24 @@ function arrowHitHtml(svg) {
|
|
|
3001
3024
|
return `<span class="vela-dtb-hit vela-dtb-hit--arrow">${iconSpan(svg, 11)}</span>`;
|
|
3002
3025
|
}
|
|
3003
3026
|
|
|
3027
|
+
// src/core/options.ts
|
|
3028
|
+
var normalizeSession = (v) => v === "regular" || v === "extended" ? v : void 0;
|
|
3029
|
+
var LIVE_BAR_EASE_DEFAULT_MS = 90;
|
|
3030
|
+
var LIVE_BAR_EASE_MAX_MS = 1e3;
|
|
3031
|
+
function resolveLiveBarEaseMs(value) {
|
|
3032
|
+
if (value === true) return LIVE_BAR_EASE_DEFAULT_MS;
|
|
3033
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return 0;
|
|
3034
|
+
return Math.min(value, LIVE_BAR_EASE_MAX_MS);
|
|
3035
|
+
}
|
|
3036
|
+
function resolveAnimations(animations) {
|
|
3037
|
+
if (typeof animations === "boolean") return { animZoom: animations, animPan: animations, animLiveBar: 0 };
|
|
3038
|
+
return {
|
|
3039
|
+
animZoom: animations?.zoom ?? true,
|
|
3040
|
+
animPan: animations?.pan ?? true,
|
|
3041
|
+
animLiveBar: resolveLiveBarEaseMs(animations?.liveBar)
|
|
3042
|
+
};
|
|
3043
|
+
}
|
|
3044
|
+
|
|
3004
3045
|
// src/core/model/series.ts
|
|
3005
3046
|
function isLineLikeSeries(spec) {
|
|
3006
3047
|
return spec.kind !== "candle" && spec.kind !== "bar" && spec.kind !== "markers";
|
|
@@ -3373,14 +3414,11 @@ var IndicatorInputsDialog = class {
|
|
|
3373
3414
|
buildTextField(id, current, emit) {
|
|
3374
3415
|
return buildFieldControl({ kind: "text", id, value: current, onChange: emit }).el;
|
|
3375
3416
|
}
|
|
3376
|
-
/**
|
|
3417
|
+
/** The shared ⓘ hint carrying an input's `tooltip` (see {@link chromeHint}). */
|
|
3377
3418
|
infoButton(tooltip) {
|
|
3378
|
-
const
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
b.className = "vela-ind-hint";
|
|
3382
|
-
b.style.cssText = "flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:50%;font-weight:600;font-size:11px;font-family:inherit;line-height:1;cursor:help;user-select:none;";
|
|
3383
|
-
return b;
|
|
3419
|
+
const { el, dispose } = chromeHint(tooltip, { host: this.host.container, theme: () => this.host.theme(), size: 18 });
|
|
3420
|
+
this.dialogTips.push(dispose);
|
|
3421
|
+
return el;
|
|
3384
3422
|
}
|
|
3385
3423
|
/** A dropdown over plain string options (value === label) — a thin case of {@link selectPairs}. */
|
|
3386
3424
|
select(id, options, current, emit) {
|
|
@@ -3817,8 +3855,6 @@ function ensureDialogStyles() {
|
|
|
3817
3855
|
${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
|
|
3818
3856
|
.vela-ind-tab{font-weight:600;font-size:13px;line-height:20px;transition:color var(--vela-dur-fast) ease,border-color var(--vela-dur-fast) ease;}
|
|
3819
3857
|
.vela-ind-tab:not(.vela-ind-tab-active):hover{color:var(--vela-fg-bright);}
|
|
3820
|
-
.vela-ind-hint{background:var(--vela-hover);color:var(--vela-fg-muted);transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
|
|
3821
|
-
.vela-ind-hint:hover{background:var(--vela-active);color:var(--vela-fg-bright);}
|
|
3822
3858
|
.vela-ind-ctl,.vela-ind-close{background-color:transparent;color:inherit;transition:color var(--vela-dur-fast) ease,background-color var(--vela-dur-fast) ease;}
|
|
3823
3859
|
.vela-ind-ctl svg,.vela-ind-close svg{width:${LEGEND_ICON_PX}px;height:${LEGEND_ICON_PX}px;display:block;flex:none;stroke-width:1;}
|
|
3824
3860
|
.vela-ind-ctl:hover{background-color:var(--vela-hover-strong);}
|
|
@@ -5209,15 +5245,15 @@ function candleOverrideFor(style, bags) {
|
|
|
5209
5245
|
if (!hasOwnCandlePaint(style)) return null;
|
|
5210
5246
|
const bag = bags[style] ?? {};
|
|
5211
5247
|
const color = (v) => isColor(v) ? v : null;
|
|
5212
|
-
const
|
|
5248
|
+
const bool2 = (v) => isBool(v) ? v : null;
|
|
5213
5249
|
return {
|
|
5214
5250
|
upColor: color(bag.candleUpColor),
|
|
5215
5251
|
downColor: color(bag.candleDownColor),
|
|
5216
|
-
bodyVisible:
|
|
5217
|
-
borderVisible:
|
|
5252
|
+
bodyVisible: bool2(bag.candleBodyVisible),
|
|
5253
|
+
borderVisible: bool2(bag.candleBorderVisible),
|
|
5218
5254
|
borderUpColor: color(bag.candleBorderUpColor),
|
|
5219
5255
|
borderDownColor: color(bag.candleBorderDownColor),
|
|
5220
|
-
wickVisible:
|
|
5256
|
+
wickVisible: bool2(bag.candleWickVisible),
|
|
5221
5257
|
wickUpColor: color(bag.candleWickUpColor),
|
|
5222
5258
|
wickDownColor: color(bag.candleWickDownColor)
|
|
5223
5259
|
};
|
|
@@ -5362,7 +5398,8 @@ function mergeConfig(base, patch) {
|
|
|
5362
5398
|
labelsVisible: isBool(ps.labelsVisible) ? ps.labelsVisible : base.priceScale.labelsVisible,
|
|
5363
5399
|
currentPriceLine: isBool(ps.currentPriceLine) ? ps.currentPriceLine : base.priceScale.currentPriceLine,
|
|
5364
5400
|
priceLabel: isBool(ps.priceLabel) ? ps.priceLabel : base.priceScale.priceLabel,
|
|
5365
|
-
countdown: isBool(ps.countdown) ? ps.countdown : base.priceScale.countdown
|
|
5401
|
+
countdown: isBool(ps.countdown) ? ps.countdown : base.priceScale.countdown,
|
|
5402
|
+
animateLastPrice: isBool(ps.animateLastPrice) ? ps.animateLastPrice : base.priceScale.animateLastPrice
|
|
5366
5403
|
},
|
|
5367
5404
|
panes: {
|
|
5368
5405
|
separatorColor: isColor(panes.separatorColor) ? panes.separatorColor : base.panes.separatorColor
|
|
@@ -7007,7 +7044,7 @@ var InputController = class {
|
|
|
7007
7044
|
if (e.repeat || e.key !== "Shift" && e.key !== "Control" && e.key !== "Meta") return;
|
|
7008
7045
|
if (Number.isNaN(this.cursorX)) return;
|
|
7009
7046
|
if (this.dragging && this.region !== "drawing") return;
|
|
7010
|
-
this.deps.drawingsPointerMove?.(this.cursorX, this.cursorY, this.snapMode(e), e.shiftKey);
|
|
7047
|
+
this.deps.drawingsPointerMove?.(this.cursorX, this.cursorY, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
|
|
7011
7048
|
};
|
|
7012
7049
|
this.onDown = (e) => {
|
|
7013
7050
|
if (e.button === 1) {
|
|
@@ -7039,7 +7076,7 @@ var InputController = class {
|
|
|
7039
7076
|
this.startY = y;
|
|
7040
7077
|
if (this.deps.drawingsClaim?.(x, y)) {
|
|
7041
7078
|
this.region = "drawing";
|
|
7042
|
-
this.deps.drawingsPointerDown?.(x, y, this.snapMode(e), e.shiftKey);
|
|
7079
|
+
this.deps.drawingsPointerDown?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
|
|
7043
7080
|
this.capture(e.pointerId);
|
|
7044
7081
|
return;
|
|
7045
7082
|
}
|
|
@@ -7048,6 +7085,11 @@ var InputController = class {
|
|
|
7048
7085
|
this.capture(e.pointerId);
|
|
7049
7086
|
return;
|
|
7050
7087
|
}
|
|
7088
|
+
if ((e.ctrlKey || e.metaKey) && this.regionAt(x, y) === "data" && this.deps.drawingsMarqueeStart?.(x, y)) {
|
|
7089
|
+
this.region = "drawing";
|
|
7090
|
+
this.capture(e.pointerId);
|
|
7091
|
+
return;
|
|
7092
|
+
}
|
|
7051
7093
|
const vp = this.deps.getCoords().getViewport();
|
|
7052
7094
|
this.deps.apply(vp);
|
|
7053
7095
|
this.startRightOffset = vp.rightOffset;
|
|
@@ -7096,7 +7138,7 @@ var InputController = class {
|
|
|
7096
7138
|
this.deps.apply({ barSpacing, rightOffset: this.startRightOffset });
|
|
7097
7139
|
} else if (this.region === "drawing") {
|
|
7098
7140
|
if (Math.abs(x - this.startX) > slop || Math.abs(y - this.startY) > slop) this.moved = true;
|
|
7099
|
-
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
|
|
7141
|
+
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
|
|
7100
7142
|
} else {
|
|
7101
7143
|
const coords = this.deps.getCoords();
|
|
7102
7144
|
const vp = coords.getViewport();
|
|
@@ -7119,7 +7161,7 @@ var InputController = class {
|
|
|
7119
7161
|
const drawCursor = this.deps.drawingsCursor?.(x, y);
|
|
7120
7162
|
const r = this.regionAt(x, y);
|
|
7121
7163
|
this.el.style.cursor = drawCursor ?? (r === "price" ? "ns-resize" : r === "time" ? "ew-resize" : r === "separator" ? "row-resize" : "");
|
|
7122
|
-
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
|
|
7164
|
+
this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
|
|
7123
7165
|
}
|
|
7124
7166
|
if (e.pointerType !== "touch") this.deps.onPointerMove(x, y);
|
|
7125
7167
|
};
|
|
@@ -7460,12 +7502,12 @@ function defaultTradeMarkersState() {
|
|
|
7460
7502
|
function mergeTradeMarkersState(base, patch) {
|
|
7461
7503
|
const p = patch && typeof patch === "object" ? patch : {};
|
|
7462
7504
|
const c = p.colors && typeof p.colors === "object" ? p.colors : {};
|
|
7463
|
-
const
|
|
7505
|
+
const bool2 = (v, fb) => typeof v === "boolean" ? v : fb;
|
|
7464
7506
|
const color = (v, fb) => typeof v === "string" && v.trim().length > 0 ? v : fb;
|
|
7465
7507
|
return {
|
|
7466
|
-
visible:
|
|
7467
|
-
labels:
|
|
7468
|
-
qty:
|
|
7508
|
+
visible: bool2(p.visible, base.visible),
|
|
7509
|
+
labels: bool2(p.labels, base.labels),
|
|
7510
|
+
qty: bool2(p.qty, base.qty),
|
|
7469
7511
|
colors: {
|
|
7470
7512
|
long: color(c.long, base.colors.long),
|
|
7471
7513
|
short: color(c.short, base.colors.short),
|
|
@@ -10149,6 +10191,8 @@ var BUILTIN_SETTINGS_IDS = [
|
|
|
10149
10191
|
"symbol.style.baseline.fill-bottom",
|
|
10150
10192
|
"symbol.style.baseline.base-level",
|
|
10151
10193
|
"symbol.style.baseline.width",
|
|
10194
|
+
"symbol.animation",
|
|
10195
|
+
"symbol.animation.price-changes",
|
|
10152
10196
|
"symbol.timezone",
|
|
10153
10197
|
"scales",
|
|
10154
10198
|
"scales.price-scale",
|
|
@@ -10291,8 +10335,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
10291
10335
|
.vela-sd-mobile .vela-select-trigger,.vela-sd-mobile .vela-num input,.vela-sd-mobile .vela-width-field{height:34px;}
|
|
10292
10336
|
.vela-sd-mobile .vela-sd-close{width:40px;height:40px;}
|
|
10293
10337
|
.vela-sd-mobile .vela-sd-btn{height:38px;}
|
|
10294
|
-
.vela-sd-mobile .vela-sd-row span,.vela-sd-mobile .vela-sd-bool span,.vela-sd-mobile .vela-field-label{white-space:normal !important;}
|
|
10295
|
-
`;
|
|
10338
|
+
.vela-sd-mobile .vela-sd-row span,.vela-sd-mobile .vela-sd-bool span,.vela-sd-mobile .vela-field-label{white-space:normal !important;}`;
|
|
10296
10339
|
if (!existing) document.head.appendChild(st);
|
|
10297
10340
|
}
|
|
10298
10341
|
var SettingsDialog = class {
|
|
@@ -10313,6 +10356,8 @@ var SettingsDialog = class {
|
|
|
10313
10356
|
this.themeControl = null;
|
|
10314
10357
|
/** The built tabs, by title — how `showSection` reaches a pane while the dialog is open. */
|
|
10315
10358
|
this.tabs = [];
|
|
10359
|
+
this.hintTips = [];
|
|
10360
|
+
// ⓘ tooltip disposers of the open build (see hint)
|
|
10316
10361
|
/** Active instance-strip tab per chart type — remembered across dialog rebuilds. */
|
|
10317
10362
|
this.typeActiveInstance = /* @__PURE__ */ new Map();
|
|
10318
10363
|
/** Active TOC group per structured pane (`<typeId>/<pane>` → group label). */
|
|
@@ -10501,6 +10546,13 @@ var SettingsDialog = class {
|
|
|
10501
10546
|
body.append(g);
|
|
10502
10547
|
}
|
|
10503
10548
|
showActive(config.series.style);
|
|
10549
|
+
body.append(sid(this.sectionTitle("Animation"), "symbol.animation"));
|
|
10550
|
+
body.append(sid(this.boolRow(
|
|
10551
|
+
"Animate price changes",
|
|
10552
|
+
config.priceScale.animateLastPrice,
|
|
10553
|
+
(v) => this.emit({ priceScale: { animateLastPrice: v } }),
|
|
10554
|
+
this.hint("Glide the live bar to each new price instead of snapping.")
|
|
10555
|
+
), "symbol.animation.price-changes"));
|
|
10504
10556
|
body.append(sid(this.sectionTitle("Time zone"), "symbol.timezone"));
|
|
10505
10557
|
body.append(sid(this.selectRowLabeled("Time zone", normalizeTimezone(config.timeScale.timezone), timezoneOptions(config.timeScale.timezone), (v) => this.emit({ timeScale: { timezone: v } })), "symbol.timezone"));
|
|
10506
10558
|
const renderHostSections = (placement) => {
|
|
@@ -10708,6 +10760,8 @@ var SettingsDialog = class {
|
|
|
10708
10760
|
this.ui = null;
|
|
10709
10761
|
this.root = null;
|
|
10710
10762
|
this.tabs = [];
|
|
10763
|
+
for (const dispose of this.hintTips) dispose();
|
|
10764
|
+
this.hintTips = [];
|
|
10711
10765
|
ui?.destroy();
|
|
10712
10766
|
}
|
|
10713
10767
|
destroy() {
|
|
@@ -11121,23 +11175,31 @@ var SettingsDialog = class {
|
|
|
11121
11175
|
}).el
|
|
11122
11176
|
});
|
|
11123
11177
|
}
|
|
11124
|
-
/** A toggle row: the checkbox sits to the LEFT of its label (never in the control area).
|
|
11125
|
-
|
|
11126
|
-
|
|
11178
|
+
/** A toggle row: the checkbox sits to the LEFT of its label (never in the control area).
|
|
11179
|
+
* `info` (see {@link hint}) rides after the label. */
|
|
11180
|
+
boolRow(label, value, onChange, info) {
|
|
11181
|
+
return this.toggleRow(label, value, onChange, [], void 0, info);
|
|
11127
11182
|
}
|
|
11128
11183
|
/** An enable row: checkbox + label in the label column, dependent controls in the
|
|
11129
11184
|
* shared control column; the control group dims and ignores input while the toggle
|
|
11130
11185
|
* is off. With `get`, the row re-reads its state on a 'vela-sync' event. */
|
|
11131
|
-
toggleRow(label, value, onToggle, controls, get) {
|
|
11186
|
+
toggleRow(label, value, onToggle, controls, get, info) {
|
|
11132
11187
|
return fieldRow({
|
|
11133
11188
|
label,
|
|
11134
11189
|
labelSize: "sm",
|
|
11135
11190
|
bool: controls.length === 0,
|
|
11136
11191
|
toggle: { checked: value, onChange: onToggle, get },
|
|
11137
11192
|
control: controls,
|
|
11193
|
+
info,
|
|
11138
11194
|
className: controls.length === 0 ? "vela-sd-bool" : "vela-sd-row"
|
|
11139
11195
|
});
|
|
11140
11196
|
}
|
|
11197
|
+
/** The shared ⓘ hint after a row label (see {@link chromeHint}). Disposed with the dialog. */
|
|
11198
|
+
hint(text) {
|
|
11199
|
+
const { el, dispose } = chromeHint(text, { host: this.container, theme: () => this.theme });
|
|
11200
|
+
this.hintTips.push(dispose);
|
|
11201
|
+
return el;
|
|
11202
|
+
}
|
|
11141
11203
|
numberRow(label, value, min, max, step, onChange) {
|
|
11142
11204
|
return fieldRow({
|
|
11143
11205
|
label,
|
|
@@ -11317,6 +11379,18 @@ var DrawingPainter = class {
|
|
|
11317
11379
|
}
|
|
11318
11380
|
this.paintHandles(ctx, pts);
|
|
11319
11381
|
}
|
|
11382
|
+
/** The selection box a Ctrl/Cmd-drag on the empty plot sweeps: a faint fill in the handle
|
|
11383
|
+
* accent under a thin dashed outline. */
|
|
11384
|
+
paintMarquee(ctx, rect) {
|
|
11385
|
+
ctx.save();
|
|
11386
|
+
ctx.fillStyle = withAlpha(HANDLE_BORDER, 0.08);
|
|
11387
|
+
ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
|
|
11388
|
+
ctx.strokeStyle = HANDLE_BORDER;
|
|
11389
|
+
ctx.lineWidth = 1;
|
|
11390
|
+
ctx.setLineDash([4, 3]);
|
|
11391
|
+
ctx.strokeRect(rect.x + 0.5, rect.y + 0.5, rect.w, rect.h);
|
|
11392
|
+
ctx.restore();
|
|
11393
|
+
}
|
|
11320
11394
|
/** A hollow ring marking the candle point the magnet (Ctrl) will snap the next anchor to. */
|
|
11321
11395
|
paintSnapRing(ctx, x, y, theme) {
|
|
11322
11396
|
ctx.setLineDash([]);
|
|
@@ -12785,6 +12859,15 @@ function topDrawingAt(drawings, x, y, proj, tol = HIT_TOLERANCE) {
|
|
|
12785
12859
|
}
|
|
12786
12860
|
return null;
|
|
12787
12861
|
}
|
|
12862
|
+
function deletableSelection(selected, drawings) {
|
|
12863
|
+
const ids = [...selected];
|
|
12864
|
+
if (ids.length < 2) return ids;
|
|
12865
|
+
return ids.filter((id) => !drawings.find((d) => d.id === id)?.locked);
|
|
12866
|
+
}
|
|
12867
|
+
function deleteTargets(hit, selected, drawings, withSelection) {
|
|
12868
|
+
if (withSelection && selected.size >= 2 && selected.has(hit.id)) return deletableSelection(selected, drawings);
|
|
12869
|
+
return hit.locked ? [] : [hit.id];
|
|
12870
|
+
}
|
|
12788
12871
|
|
|
12789
12872
|
// src/renderers/native/drawings/DrawingInteraction.ts
|
|
12790
12873
|
var DRAG_SLOP2 = 3;
|
|
@@ -12888,10 +12971,32 @@ var DrawingInteraction = class {
|
|
|
12888
12971
|
activeDragId() {
|
|
12889
12972
|
return this.state.kind === "pressed" && this.state.moved ? this.state.id : null;
|
|
12890
12973
|
}
|
|
12974
|
+
/** The drawing under a press that has not been released yet (drag or click undecided), or null. */
|
|
12975
|
+
pressedId() {
|
|
12976
|
+
return this.state.kind === "pressed" ? this.state.id : null;
|
|
12977
|
+
}
|
|
12978
|
+
/** The copies a Ctrl-drag is moving (transient — not yet in the store), or null. */
|
|
12979
|
+
dragClones() {
|
|
12980
|
+
return this.state.kind === "pressed" && this.state.moved && this.state.clones ? this.state.clones.map((c) => c.d) : null;
|
|
12981
|
+
}
|
|
12982
|
+
/** The marquee box being swept, normalized (or null when none is in progress). */
|
|
12983
|
+
marqueeRect() {
|
|
12984
|
+
if (this.state.kind !== "marquee") return null;
|
|
12985
|
+
const { x0, y0, x1, y1 } = this.state;
|
|
12986
|
+
return { x: Math.min(x0, x1), y: Math.min(y0, y1), w: Math.abs(x1 - x0), h: Math.abs(y1 - y0) };
|
|
12987
|
+
}
|
|
12988
|
+
/** Ctrl/Cmd-press on the empty plot: start sweeping a selection box. False when a tool is
|
|
12989
|
+
* armed or a gesture is in flight (that path owns the press). */
|
|
12990
|
+
beginMarquee(x, y) {
|
|
12991
|
+
if (this.state.kind !== "idle" || this.deps.activeTool() != null) return false;
|
|
12992
|
+
this.state = { kind: "marquee", x0: x, y0: y, x1: x, y1: y };
|
|
12993
|
+
return true;
|
|
12994
|
+
}
|
|
12891
12995
|
/** A press: place the next anchor, or grab a drawing/handle (resolved as click or drag on release).
|
|
12892
12996
|
* `shift` over a drawing toggles it in/out of the selection (multi-select) instead of dragging;
|
|
12893
|
-
* while placing a line tool it locks the segment angle to 45° steps.
|
|
12894
|
-
|
|
12997
|
+
* while placing a line tool it locks the segment angle to 45° steps. `mod` (Ctrl/Cmd) over a
|
|
12998
|
+
* drawing also toggles on a click, but a body drag then moves a COPY. */
|
|
12999
|
+
down(x, y, mode = "off", shift2 = false, mod = false) {
|
|
12895
13000
|
const proj = this.deps.projector();
|
|
12896
13001
|
if (this.state.kind === "placing") {
|
|
12897
13002
|
if (this.state.draft.placementMode() !== "click") return;
|
|
@@ -12919,7 +13024,50 @@ var DrawingInteraction = class {
|
|
|
12919
13024
|
return;
|
|
12920
13025
|
}
|
|
12921
13026
|
const handle = hit.hitHandle(x, y, proj, HIT_TOLERANCE);
|
|
12922
|
-
|
|
13027
|
+
const riders = [];
|
|
13028
|
+
if (handle < 0 && this.deps.selectedIds().has(hit.id)) {
|
|
13029
|
+
for (const id of this.deps.selectedIds()) {
|
|
13030
|
+
const d = id === hit.id ? null : this.byId(id);
|
|
13031
|
+
if (d && !d.locked && d.visible) riders.push({ id, orig: cloneAnchors(d) });
|
|
13032
|
+
}
|
|
13033
|
+
}
|
|
13034
|
+
this.state = { kind: "pressed", id: hit.id, handle, orig: cloneAnchors(hit), px: x, py: y, moved: false, riders, mod, clones: null };
|
|
13035
|
+
}
|
|
13036
|
+
}
|
|
13037
|
+
/** The data-space delta a body drag from the press to (x, y) means on `paneId` — resolved per
|
|
13038
|
+
* pane so drawings riding along on another pane move by the same PIXELS, not the same price. */
|
|
13039
|
+
bodyDelta(paneId, x, y) {
|
|
13040
|
+
if (this.state.kind !== "pressed") return { dt: 0, dp: 0 };
|
|
13041
|
+
const proj = this.deps.projector();
|
|
13042
|
+
const from = proj.pxToPoint(this.state.px, this.state.py, paneId);
|
|
13043
|
+
const to = proj.pxToPoint(x, y, paneId);
|
|
13044
|
+
return { dt: to.time - from.time, dp: to.price - from.price };
|
|
13045
|
+
}
|
|
13046
|
+
/** The pressed drawing and its riders, each with its anchors at the press. */
|
|
13047
|
+
dragSources(d) {
|
|
13048
|
+
if (this.state.kind !== "pressed") return [];
|
|
13049
|
+
const out = [{ d, orig: this.state.orig }];
|
|
13050
|
+
for (const r of this.state.riders) {
|
|
13051
|
+
const rd = this.byId(r.id);
|
|
13052
|
+
if (rd) out.push({ d: rd, orig: r.orig });
|
|
13053
|
+
}
|
|
13054
|
+
return out;
|
|
13055
|
+
}
|
|
13056
|
+
/** Move the pressed drawing's body (and its riders) — or, on a Ctrl-drag, their copies — to the
|
|
13057
|
+
* cursor. The copies are built on the first move (so a Ctrl-click never creates anything), from
|
|
13058
|
+
* sources that have not moved yet, so their own anchors are the origin to translate from. */
|
|
13059
|
+
dragBody(d, x, y) {
|
|
13060
|
+
if (this.state.kind !== "pressed") return;
|
|
13061
|
+
this.snapAt = null;
|
|
13062
|
+
if (this.state.mod && !this.state.clones) {
|
|
13063
|
+
this.state.clones = this.dragSources(d).flatMap((s) => {
|
|
13064
|
+
const c = deserializeDrawing(s.d.serialize());
|
|
13065
|
+
return c ? [{ d: c, orig: cloneAnchors(c) }] : [];
|
|
13066
|
+
});
|
|
13067
|
+
}
|
|
13068
|
+
for (const t of this.state.clones ?? this.dragSources(d)) {
|
|
13069
|
+
const { dt, dp } = this.bodyDelta(t.d.paneId, x, y);
|
|
13070
|
+
t.d.anchors = t.d.translateBody(dt, dp, t.orig);
|
|
12923
13071
|
}
|
|
12924
13072
|
}
|
|
12925
13073
|
/** Pointer move: advance the placing ghost, or live-preview a drag once past the slop.
|
|
@@ -12943,12 +13091,20 @@ var DrawingInteraction = class {
|
|
|
12943
13091
|
this.deps.changed();
|
|
12944
13092
|
return;
|
|
12945
13093
|
}
|
|
13094
|
+
if (this.state.kind === "marquee") {
|
|
13095
|
+
this.state.x1 = x;
|
|
13096
|
+
this.state.y1 = y;
|
|
13097
|
+
this.deps.changed();
|
|
13098
|
+
return;
|
|
13099
|
+
}
|
|
12946
13100
|
if (this.state.kind !== "pressed") return;
|
|
12947
13101
|
const d = this.byId(this.state.id);
|
|
12948
13102
|
if (!d || d.locked) return;
|
|
12949
13103
|
if (!this.state.moved && Math.abs(x - this.state.px) <= DRAG_SLOP2 && Math.abs(y - this.state.py) <= DRAG_SLOP2) return;
|
|
12950
13104
|
this.state.moved = true;
|
|
12951
|
-
if (this.state.handle
|
|
13105
|
+
if (this.state.handle < 0) {
|
|
13106
|
+
this.dragBody(d, x, y);
|
|
13107
|
+
} else {
|
|
12952
13108
|
const a = d.anchors[this.state.handle];
|
|
12953
13109
|
if (a) {
|
|
12954
13110
|
const pivot = shift2 && ANGLE_SNAP_TYPES.has(d.type) && d.anchors.length === 2 ? d.anchors[1 - this.state.handle] : void 0;
|
|
@@ -12956,12 +13112,6 @@ var DrawingInteraction = class {
|
|
|
12956
13112
|
applyFree(a, point, d.anchorSchema().slots[this.state.handle]?.free ?? "both");
|
|
12957
13113
|
}
|
|
12958
13114
|
d.constrainHandleDrag(this.state.handle);
|
|
12959
|
-
} else {
|
|
12960
|
-
const pt = this.deps.projector().pxToPoint(x, y, d.paneId);
|
|
12961
|
-
this.snapAt = null;
|
|
12962
|
-
const dt = pt.time - this.state.grab.time;
|
|
12963
|
-
const dp = pt.price - this.state.grab.price;
|
|
12964
|
-
d.anchors = d.translateBody(dt, dp, this.state.orig);
|
|
12965
13115
|
}
|
|
12966
13116
|
this.deps.changed();
|
|
12967
13117
|
}
|
|
@@ -12981,16 +13131,46 @@ var DrawingInteraction = class {
|
|
|
12981
13131
|
}
|
|
12982
13132
|
return;
|
|
12983
13133
|
}
|
|
13134
|
+
if (this.state.kind === "marquee") {
|
|
13135
|
+
const rect = this.marqueeRect();
|
|
13136
|
+
this.state = { kind: "idle" };
|
|
13137
|
+
if (rect.w > DRAG_SLOP2 || rect.h > DRAG_SLOP2) this.selectWithin(rect);
|
|
13138
|
+
this.deps.changed();
|
|
13139
|
+
return;
|
|
13140
|
+
}
|
|
12984
13141
|
if (this.state.kind !== "pressed") return;
|
|
12985
|
-
const { id, moved } = this.state;
|
|
13142
|
+
const { id, moved, riders, mod, clones } = this.state;
|
|
12986
13143
|
this.state = { kind: "idle" };
|
|
12987
13144
|
const d = this.byId(id);
|
|
12988
|
-
if (moved) {
|
|
12989
|
-
if (
|
|
12990
|
-
this.deps.
|
|
12991
|
-
|
|
12992
|
-
this.deps.openSettings(id, x, y);
|
|
13145
|
+
if (!moved) {
|
|
13146
|
+
if (mod) this.deps.emit({ kind: "select", ids: [id], additive: true });
|
|
13147
|
+
else this.deps.openSettings(id, x, y);
|
|
13148
|
+
return;
|
|
12993
13149
|
}
|
|
13150
|
+
if (clones) {
|
|
13151
|
+
this.deps.emit({ kind: "clone", docs: clones.map((c) => c.d.serialize()) });
|
|
13152
|
+
} else if (d) {
|
|
13153
|
+
const docs = [d.serialize()];
|
|
13154
|
+
for (const r of riders) {
|
|
13155
|
+
const rd = this.byId(r.id);
|
|
13156
|
+
if (rd) docs.push(rd.serialize());
|
|
13157
|
+
}
|
|
13158
|
+
if (docs.length > 1) this.deps.emit({ kind: "edit-many", docs });
|
|
13159
|
+
else this.deps.emit({ kind: "edit", doc: docs[0] });
|
|
13160
|
+
}
|
|
13161
|
+
this.deps.changed();
|
|
13162
|
+
}
|
|
13163
|
+
/** Add every visible drawing whose on-screen bounds touch `rect` to the selection (a union,
|
|
13164
|
+
* so successive sweeps accumulate). No change when the box touches nothing new. */
|
|
13165
|
+
selectWithin(rect) {
|
|
13166
|
+
const proj = this.deps.projector();
|
|
13167
|
+
const ids = [...this.deps.selectedIds()];
|
|
13168
|
+
for (const d of this.deps.drawings()) {
|
|
13169
|
+
if (!d.visible || ids.includes(d.id)) continue;
|
|
13170
|
+
const b = d.bounds(proj);
|
|
13171
|
+
if (b && rectsIntersect(rect, b)) ids.push(d.id);
|
|
13172
|
+
}
|
|
13173
|
+
if (ids.length > this.deps.selectedIds().size) this.deps.emit({ kind: "select", ids });
|
|
12994
13174
|
}
|
|
12995
13175
|
/** Cancel an in-progress placement or drag (Escape). Returns whether anything was cancelled. */
|
|
12996
13176
|
cancel() {
|
|
@@ -13003,8 +13183,15 @@ var DrawingInteraction = class {
|
|
|
13003
13183
|
return true;
|
|
13004
13184
|
}
|
|
13005
13185
|
if (this.state.kind === "pressed") {
|
|
13006
|
-
|
|
13007
|
-
|
|
13186
|
+
if (this.state.moved && !this.state.clones) {
|
|
13187
|
+
const d = this.byId(this.state.id);
|
|
13188
|
+
if (d) for (const s of this.dragSources(d)) s.d.anchors = s.orig.map((o) => ({ time: o.time, price: o.price }));
|
|
13189
|
+
}
|
|
13190
|
+
this.state = { kind: "idle" };
|
|
13191
|
+
this.deps.changed();
|
|
13192
|
+
return true;
|
|
13193
|
+
}
|
|
13194
|
+
if (this.state.kind === "marquee") {
|
|
13008
13195
|
this.state = { kind: "idle" };
|
|
13009
13196
|
this.deps.changed();
|
|
13010
13197
|
return true;
|
|
@@ -13119,6 +13306,9 @@ var DrawingInteraction = class {
|
|
|
13119
13306
|
function cloneAnchors(d) {
|
|
13120
13307
|
return d.anchors.map((a) => ({ time: a.time, price: a.price }));
|
|
13121
13308
|
}
|
|
13309
|
+
function rectsIntersect(a, b) {
|
|
13310
|
+
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;
|
|
13311
|
+
}
|
|
13122
13312
|
function applyFree(anchor, pt, free) {
|
|
13123
13313
|
if (free === "both" || free === "x") anchor.time = pt.time;
|
|
13124
13314
|
if (free === "both" || free === "y") anchor.price = pt.price;
|
|
@@ -13504,10 +13694,27 @@ function btn(label, primary, onClick) {
|
|
|
13504
13694
|
}
|
|
13505
13695
|
|
|
13506
13696
|
// src/renderers/native/drawings/DrawingSettingsPopup.ts
|
|
13697
|
+
var MIXED = /* @__PURE__ */ Symbol("mixed");
|
|
13698
|
+
function sharedPaths(drawings) {
|
|
13699
|
+
const [first, ...rest] = drawings;
|
|
13700
|
+
const paths = new Set(first?.schema().fields.map((f) => f.path) ?? []);
|
|
13701
|
+
for (const d of rest) {
|
|
13702
|
+
const own = new Set(d.schema().fields.map((f) => f.path));
|
|
13703
|
+
for (const p of paths) if (!own.has(p)) paths.delete(p);
|
|
13704
|
+
}
|
|
13705
|
+
return paths;
|
|
13706
|
+
}
|
|
13707
|
+
function commonValue(drawings, read) {
|
|
13708
|
+
const first = read(drawings[0]);
|
|
13709
|
+
return drawings.every((d) => read(d) === first) ? first : MIXED;
|
|
13710
|
+
}
|
|
13711
|
+
function distinctValues(drawings, read) {
|
|
13712
|
+
return [...new Set(drawings.map(read))];
|
|
13713
|
+
}
|
|
13507
13714
|
var BTN = 30;
|
|
13508
13715
|
var ICON = 21;
|
|
13509
13716
|
var STYLE_ID4 = "vela-drawing-popup-styles";
|
|
13510
|
-
var STYLE_REV = "
|
|
13717
|
+
var STYLE_REV = "4";
|
|
13511
13718
|
function ensureStyles4() {
|
|
13512
13719
|
if (typeof document === "undefined") return;
|
|
13513
13720
|
const existing = document.getElementById(STYLE_ID4);
|
|
@@ -13519,6 +13726,8 @@ function ensureStyles4() {
|
|
|
13519
13726
|
.vela-dpop-btn{background:transparent;color:var(--vela-fg-muted);transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
|
|
13520
13727
|
.vela-dpop-btn:hover{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
|
|
13521
13728
|
.vela-dpop-btn[data-active='1']{background:var(--vela-active);color:var(--vela-fg-bright);}
|
|
13729
|
+
/* Mixed (a multi-selection disagrees): the active fill at half strength behind a dashed ring. */
|
|
13730
|
+
.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;}
|
|
13522
13731
|
.vela-dpop-item{background:transparent;transition:background var(--vela-dur-fast) ease;}
|
|
13523
13732
|
.vela-dpop-item:hover{background:var(--vela-hover-strong);}
|
|
13524
13733
|
.vela-dpop-item[data-active='1']{background:var(--vela-active);}
|
|
@@ -13550,7 +13759,7 @@ var DrawingSettingsPopup = class {
|
|
|
13550
13759
|
if (this.isOwnChrome(node)) return;
|
|
13551
13760
|
const cb = this.onClose;
|
|
13552
13761
|
this.close();
|
|
13553
|
-
cb?.();
|
|
13762
|
+
cb?.(e);
|
|
13554
13763
|
};
|
|
13555
13764
|
this.theme = theme;
|
|
13556
13765
|
this.settingsDialog = new DrawingSettingsDialog(host, theme);
|
|
@@ -13575,16 +13784,27 @@ var DrawingSettingsPopup = class {
|
|
|
13575
13784
|
contains(node) {
|
|
13576
13785
|
return node != null && (this.isOwnChrome(node) || this.settingsDialog.contains(node));
|
|
13577
13786
|
}
|
|
13578
|
-
/** Open the quick toolbar for `
|
|
13579
|
-
* `
|
|
13580
|
-
|
|
13787
|
+
/** Open the quick toolbar for `drawings` (one, or a whole multi-selection), floating clear of
|
|
13788
|
+
* their `anchor` box. With several drawings the bar shows only the controls they ALL support,
|
|
13789
|
+
* a control whose values differ reads as mixed, and every edit applies to all of them —
|
|
13790
|
+
* per-drawing panels (levels, position sizing, the text field) stay single-drawing only.
|
|
13791
|
+
* `onClose` fires on a dismissal (an outside press, or Escape in the text field — not a
|
|
13792
|
+
* programmatic close), with the dismissing press when there is one, so the caller can tell a
|
|
13793
|
+
* modifier press (multi-select) from a plain one. */
|
|
13794
|
+
open(drawings, anchor, actions, onClose) {
|
|
13581
13795
|
this.close();
|
|
13796
|
+
const drawing = drawings[0];
|
|
13797
|
+
if (!drawing) return;
|
|
13582
13798
|
ensureStyles4();
|
|
13583
13799
|
this.onClose = onClose ?? null;
|
|
13584
13800
|
const t = this.theme;
|
|
13801
|
+
const multi = drawings.length > 1;
|
|
13585
13802
|
const schema = drawing.schema();
|
|
13586
|
-
const paths =
|
|
13587
|
-
const
|
|
13803
|
+
const paths = sharedPaths(drawings);
|
|
13804
|
+
const common = (read) => commonValue(drawings, read);
|
|
13805
|
+
const every = (pred) => drawings.every(pred);
|
|
13806
|
+
const swatch = (tip, glyph, read, path) => this.colorButton(tip, glyph, common(read), (v) => actions.patch({ [path]: v }), void 0, distinctValues(drawings, read));
|
|
13807
|
+
const textOnBar = multi || schema.textIsContent === true || !paths.has("text.value");
|
|
13588
13808
|
const el = document.createElement("div");
|
|
13589
13809
|
el.className = "vela-dpop";
|
|
13590
13810
|
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;`;
|
|
@@ -13598,75 +13818,84 @@ var DrawingSettingsPopup = class {
|
|
|
13598
13818
|
bar.style.cssText = "display:flex;align-items:center;gap:2px;padding:4px;";
|
|
13599
13819
|
bar.appendChild(this.dragHandle());
|
|
13600
13820
|
if (paths.has("glyph")) {
|
|
13601
|
-
const cur =
|
|
13821
|
+
const cur = common((d) => d.glyph ?? GLYPH_OPTIONS[0]);
|
|
13602
13822
|
bar.appendChild(this.dropdown("Icon", GLYPH_OPTIONS, cur, (g) => glyphIcon(g), (v) => actions.patch({ glyph: v })));
|
|
13603
13823
|
}
|
|
13604
13824
|
if (paths.has("size")) {
|
|
13605
|
-
const sz =
|
|
13825
|
+
const sz = common((d) => d.size ?? "normal");
|
|
13606
13826
|
bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label: sizeLabel }));
|
|
13607
13827
|
}
|
|
13608
|
-
if (paths.has("magnifier.timeframe") &&
|
|
13828
|
+
if (paths.has("magnifier.timeframe") && every((d) => d instanceof Magnifier)) {
|
|
13829
|
+
const mag = (d) => d;
|
|
13609
13830
|
const options = this.lowerTimeframeOptions();
|
|
13610
13831
|
if (options.length > 0) {
|
|
13611
13832
|
bar.appendChild(
|
|
13612
|
-
this.dropdown("Lower timeframe", options.map((o) => o.value),
|
|
13833
|
+
this.dropdown("Lower timeframe", options.map((o) => o.value), common((d) => mag(d).magnifier.timeframe), () => "", (v) => actions.patch({ "magnifier.timeframe": v }), {
|
|
13613
13834
|
label: (v) => magnifierTimeframeLabel(String(v)),
|
|
13614
13835
|
labelInTrigger: true
|
|
13615
13836
|
})
|
|
13616
13837
|
);
|
|
13617
13838
|
}
|
|
13618
|
-
bar.appendChild(
|
|
13619
|
-
bar.appendChild(
|
|
13839
|
+
bar.appendChild(swatch("Up candles", BUCKET_ICON, (d) => mag(d).magnifier.upColor || t.upColor, "magnifier.upColor"));
|
|
13840
|
+
bar.appendChild(swatch("Down candles", BUCKET_ICON, (d) => mag(d).magnifier.downColor || t.downColor, "magnifier.downColor"));
|
|
13620
13841
|
}
|
|
13621
|
-
if (paths.has("style.lineColor")) bar.appendChild(
|
|
13842
|
+
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"));
|
|
13622
13843
|
if (paths.has("style.lineWidth")) {
|
|
13623
|
-
const
|
|
13624
|
-
|
|
13625
|
-
|
|
13844
|
+
const widthField = (d) => d.schema().fields.find((f) => f.path === "style.lineWidth");
|
|
13845
|
+
const wf = widthField(drawing);
|
|
13846
|
+
const width = common((d) => d.style.lineWidth);
|
|
13847
|
+
const markers = every((d) => {
|
|
13848
|
+
const f = widthField(d);
|
|
13849
|
+
return f?.kind === "number" && (f.min ?? 1) > 1;
|
|
13850
|
+
});
|
|
13851
|
+
if (markers && wf?.kind === "number") {
|
|
13852
|
+
bar.appendChild(this.widthInput("Line width", width, wf.min ?? 1, wf.max ?? 60, wf.step ?? 1, (v) => actions.patch({ "style.lineWidth": v })));
|
|
13626
13853
|
} else {
|
|
13627
|
-
bar.appendChild(this.dropdown("Line width", [1, 2, 3, 4],
|
|
13854
|
+
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 }));
|
|
13628
13855
|
}
|
|
13629
13856
|
}
|
|
13630
|
-
if (paths.has("style.lineStyle")) bar.appendChild(this.dropdown("Line style", LINE_STYLE_OPTIONS.map((o) => o.value),
|
|
13631
|
-
if (paths.has("style.fillColor")) bar.appendChild(
|
|
13857
|
+
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 }));
|
|
13858
|
+
if (paths.has("style.fillColor")) bar.appendChild(swatch("Fill", BUCKET_ICON, (d) => effectiveFillColor(d, this.theme) ?? d.style.fillColor ?? DEFAULT_DRAWING_COLOR, "style.fillColor"));
|
|
13632
13859
|
const isFrvp = paths.has("frvp.rows") && drawing instanceof FixedRangeVolumeProfile;
|
|
13633
|
-
const isPosition = paths.has("riskPercent") &&
|
|
13860
|
+
const isPosition = paths.has("riskPercent") && every((d) => d instanceof PositionTool);
|
|
13634
13861
|
if (isPosition) {
|
|
13635
|
-
const pos =
|
|
13636
|
-
bar.appendChild(
|
|
13637
|
-
bar.appendChild(
|
|
13862
|
+
const pos = (d) => d;
|
|
13863
|
+
bar.appendChild(swatch("Profit zone", BUCKET_ICON, (d) => pos(d).profitColor, "profitColor"));
|
|
13864
|
+
bar.appendChild(swatch("Loss zone", BUCKET_ICON, (d) => pos(d).lossColor, "lossColor"));
|
|
13638
13865
|
}
|
|
13639
|
-
const
|
|
13640
|
-
if (paths.has("reg.midColor") &&
|
|
13866
|
+
const regOf = (d) => d.reg;
|
|
13867
|
+
if (paths.has("reg.midColor") && every((d) => regOf(d) != null)) {
|
|
13868
|
+
const reg = (d) => regOf(d);
|
|
13641
13869
|
const styles = LINE_STYLE_OPTIONS.map((o) => o.value);
|
|
13642
|
-
bar.appendChild(
|
|
13643
|
-
bar.appendChild(this.dropdown("Midline style", styles, reg.midStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "reg.midStyle": v }), { label: styleLabel2 }));
|
|
13644
|
-
bar.appendChild(
|
|
13645
|
-
bar.appendChild(this.dropdown("Upper line style", styles, reg.upperStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "reg.upperStyle": v }), { label: styleLabel2 }));
|
|
13646
|
-
bar.appendChild(
|
|
13647
|
-
bar.appendChild(this.dropdown("Lower line style", styles, reg.lowerStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "reg.lowerStyle": v }), { label: styleLabel2 }));
|
|
13648
|
-
bar.appendChild(
|
|
13649
|
-
bar.appendChild(
|
|
13650
|
-
bar.appendChild(this.toggle("Show R\xB2", R2_ICON, reg.showR2, (v) => actions.patch({ "reg.showR2": v })));
|
|
13651
|
-
}
|
|
13652
|
-
const
|
|
13653
|
-
if (paths.has("vwap.midColor") &&
|
|
13870
|
+
bar.appendChild(swatch("Midline color", BRUSH_ICON, (d) => reg(d).midColor, "reg.midColor"));
|
|
13871
|
+
bar.appendChild(this.dropdown("Midline style", styles, common((d) => reg(d).midStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "reg.midStyle": v }), { label: styleLabel2 }));
|
|
13872
|
+
bar.appendChild(swatch("Upper line color", BRUSH_ICON, (d) => reg(d).upperColor, "reg.upperColor"));
|
|
13873
|
+
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 }));
|
|
13874
|
+
bar.appendChild(swatch("Lower line color", BRUSH_ICON, (d) => reg(d).lowerColor, "reg.lowerColor"));
|
|
13875
|
+
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 }));
|
|
13876
|
+
bar.appendChild(swatch("Upper fill", BUCKET_ICON, (d) => reg(d).upperFill, "reg.upperFill"));
|
|
13877
|
+
bar.appendChild(swatch("Lower fill", BUCKET_ICON, (d) => reg(d).lowerFill, "reg.lowerFill"));
|
|
13878
|
+
bar.appendChild(this.toggle("Show R\xB2", R2_ICON, common((d) => reg(d).showR2), (v) => actions.patch({ "reg.showR2": v })));
|
|
13879
|
+
}
|
|
13880
|
+
const vwapOf = (d) => d.vwap;
|
|
13881
|
+
if (paths.has("vwap.midColor") && every((d) => vwapOf(d) != null)) {
|
|
13882
|
+
const vwap2 = (d) => vwapOf(d);
|
|
13654
13883
|
const styles = LINE_STYLE_OPTIONS.map((o) => o.value);
|
|
13655
13884
|
const MULTS = [0.5, 1, 1.5, 2, 2.5, 3, 4, 5];
|
|
13656
|
-
bar.appendChild(
|
|
13657
|
-
bar.appendChild(this.dropdown("VWAP style", styles, vwap2.midStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "vwap.midStyle": v }), { label: styleLabel2 }));
|
|
13885
|
+
bar.appendChild(swatch("VWAP color", BRUSH_ICON, (d) => vwap2(d).midColor, "vwap.midColor"));
|
|
13886
|
+
bar.appendChild(this.dropdown("VWAP style", styles, common((d) => vwap2(d).midStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "vwap.midStyle": v }), { label: styleLabel2 }));
|
|
13658
13887
|
bar.appendChild(
|
|
13659
|
-
this.dropdown("Band multiplier", MULTS, vwap2.multiplier, () => BANDS_ICON, (v) => actions.patch({ "vwap.multiplier": v }), {
|
|
13888
|
+
this.dropdown("Band multiplier", MULTS, common((d) => vwap2(d).multiplier), () => BANDS_ICON, (v) => actions.patch({ "vwap.multiplier": v }), {
|
|
13660
13889
|
label: (v) => `${v}\u03C3`,
|
|
13661
13890
|
labelInTrigger: true
|
|
13662
13891
|
})
|
|
13663
13892
|
);
|
|
13664
|
-
bar.appendChild(
|
|
13665
|
-
bar.appendChild(
|
|
13666
|
-
bar.appendChild(
|
|
13893
|
+
bar.appendChild(swatch("Upper band color", BRUSH_ICON, (d) => vwap2(d).upperColor, "vwap.upperColor"));
|
|
13894
|
+
bar.appendChild(swatch("Lower band color", BRUSH_ICON, (d) => vwap2(d).lowerColor, "vwap.lowerColor"));
|
|
13895
|
+
bar.appendChild(swatch("Band fill", BUCKET_ICON, (d) => vwap2(d).bandFill, "vwap.bandFill"));
|
|
13667
13896
|
}
|
|
13668
13897
|
if (paths.has("maxCurvature")) {
|
|
13669
|
-
const cur =
|
|
13898
|
+
const cur = common((d) => d.maxCurvature ?? 24);
|
|
13670
13899
|
bar.appendChild(
|
|
13671
13900
|
this.dropdown("Max curvature", DEDEKIND_CURVATURE_OPTIONS, cur, () => DEDEKIND_ICON, (v) => actions.patch({ maxCurvature: v }), {
|
|
13672
13901
|
label: (v) => `n=${v}`,
|
|
@@ -13675,7 +13904,7 @@ var DrawingSettingsPopup = class {
|
|
|
13675
13904
|
);
|
|
13676
13905
|
}
|
|
13677
13906
|
if (paths.has("mach")) {
|
|
13678
|
-
const cur =
|
|
13907
|
+
const cur = common((d) => d.mach ?? 2);
|
|
13679
13908
|
bar.appendChild(
|
|
13680
13909
|
this.dropdown("Mach number", MACH_NUMBER_OPTIONS, cur, () => SUPERSONIC_ICON, (v) => actions.patch({ mach: v }), {
|
|
13681
13910
|
label: (v) => `M=${v}`,
|
|
@@ -13684,7 +13913,7 @@ var DrawingSettingsPopup = class {
|
|
|
13684
13913
|
);
|
|
13685
13914
|
}
|
|
13686
13915
|
if (paths.has("waveCount")) {
|
|
13687
|
-
const cur =
|
|
13916
|
+
const cur = common((d) => d.waveCount ?? 6);
|
|
13688
13917
|
bar.appendChild(
|
|
13689
13918
|
this.dropdown("Waves", MACH_WAVE_COUNT_OPTIONS, cur, () => SONIC_ICON, (v) => actions.patch({ waveCount: v }), {
|
|
13690
13919
|
label: (v) => `${v}`,
|
|
@@ -13692,26 +13921,27 @@ var DrawingSettingsPopup = class {
|
|
|
13692
13921
|
})
|
|
13693
13922
|
);
|
|
13694
13923
|
}
|
|
13695
|
-
const
|
|
13696
|
-
if (paths.has("showPrice")) bar.appendChild(this.toggle("Show price", PRICE_DELTA_ICON,
|
|
13697
|
-
if (paths.has("showDate")) bar.appendChild(this.toggle("Show date", DATE_DELTA_ICON,
|
|
13698
|
-
if (paths.has("text.color") && textOnBar) bar.appendChild(
|
|
13699
|
-
if (paths.has("text.size") && textOnBar) bar.appendChild(this.dropdown("Text size", TEXT_SIZE_OPTIONS.map((o) => o.value),
|
|
13700
|
-
|
|
13701
|
-
if (paths.has("text.
|
|
13702
|
-
if (paths.has("text.
|
|
13924
|
+
const flags = (d) => d;
|
|
13925
|
+
if (paths.has("showPrice")) bar.appendChild(this.toggle("Show price", PRICE_DELTA_ICON, common((d) => flags(d).showPrice !== false), (v) => actions.patch({ showPrice: v })));
|
|
13926
|
+
if (paths.has("showDate")) bar.appendChild(this.toggle("Show date", DATE_DELTA_ICON, common((d) => flags(d).showDate !== false), (v) => actions.patch({ showDate: v })));
|
|
13927
|
+
if (paths.has("text.color") && textOnBar) bar.appendChild(swatch("Text color", TYPE_ICON, (d) => d.text?.color || t.textColor, "text.color"));
|
|
13928
|
+
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 }));
|
|
13929
|
+
const textField = !multi && paths.has("text.value");
|
|
13930
|
+
if (paths.has("text.bold") && !textField) bar.appendChild(this.toggle("Bold", BOLD_ICON, common((d) => !!d.text?.bold), (v) => actions.patch({ "text.bold": v })));
|
|
13931
|
+
if (paths.has("text.italic") && !textField) bar.appendChild(this.toggle("Italic", ITALIC_ICON, common((d) => !!d.text?.italic), (v) => actions.patch({ "text.italic": v })));
|
|
13932
|
+
if (textField) bar.appendChild(this.iconBtn("Text", TYPE_ICON, () => this.toggleTextPanel(drawing, actions, !textOnBar)));
|
|
13703
13933
|
const editableLevels = drawing.editableLevels();
|
|
13704
|
-
if (editableLevels && !(
|
|
13705
|
-
const fib =
|
|
13934
|
+
if (editableLevels && every((d) => d.editableLevels() != null && !(d instanceof MachFigure))) {
|
|
13935
|
+
const fib = (d) => d;
|
|
13706
13936
|
const sizes = TEXT_SIZE_OPTIONS.map((o) => o.value);
|
|
13707
|
-
bar.appendChild(this.dropdown("Numbers size", sizes, fib.numbersSize ?? "small", (s) => numbersSizeIcon(s), (v) => actions.patch({ numbersSize: v }), { label: sizeLabel }));
|
|
13708
|
-
bar.appendChild(this.dropdown("Label size", sizes, fib.labelsSize ?? "normal", (s) => labelSizeIcon(s), (v) => actions.patch({ labelsSize: v }), { label: sizeLabel }));
|
|
13937
|
+
bar.appendChild(this.dropdown("Numbers size", sizes, common((d) => fib(d).numbersSize ?? "small"), (s) => numbersSizeIcon(s), (v) => actions.patch({ numbersSize: v }), { label: sizeLabel }));
|
|
13938
|
+
bar.appendChild(this.dropdown("Label size", sizes, common((d) => fib(d).labelsSize ?? "normal"), (s) => labelSizeIcon(s), (v) => actions.patch({ labelsSize: v }), { label: sizeLabel }));
|
|
13709
13939
|
}
|
|
13710
13940
|
bar.appendChild(this.divider());
|
|
13711
|
-
if (isFrvp) bar.appendChild(this.iconBtn("Settings", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "frvp")));
|
|
13712
|
-
if (isPosition) bar.appendChild(this.iconBtn("Position size", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "position")));
|
|
13713
|
-
if (editableLevels) bar.appendChild(this.iconBtn("Levels", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "levels")));
|
|
13714
|
-
bar.appendChild(this.toggle("Lock", LOCK_ICON,
|
|
13941
|
+
if (!multi && isFrvp) bar.appendChild(this.iconBtn("Settings", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "frvp")));
|
|
13942
|
+
if (!multi && isPosition) bar.appendChild(this.iconBtn("Position size", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "position")));
|
|
13943
|
+
if (!multi && editableLevels) bar.appendChild(this.iconBtn("Levels", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "levels")));
|
|
13944
|
+
bar.appendChild(this.toggle("Lock", LOCK_ICON, common((d) => d.locked), (v) => actions.setLocked(v), UNLOCK_ICON));
|
|
13715
13945
|
const del = this.iconBtn("Delete", TRASH_ICON, () => actions.remove());
|
|
13716
13946
|
del.style.color = "var(--vela-danger)";
|
|
13717
13947
|
bar.appendChild(del);
|
|
@@ -13897,7 +14127,7 @@ var DrawingSettingsPopup = class {
|
|
|
13897
14127
|
});
|
|
13898
14128
|
return b;
|
|
13899
14129
|
}
|
|
13900
|
-
/** The trailing kebab overflow — z-order actions
|
|
14130
|
+
/** The trailing kebab overflow — duplicate, z-order actions, reset settings. */
|
|
13901
14131
|
kebabButton(actions) {
|
|
13902
14132
|
const b = this.base("More");
|
|
13903
14133
|
b.innerHTML = sized(KEBAB_ICON);
|
|
@@ -13909,6 +14139,7 @@ var DrawingSettingsPopup = class {
|
|
|
13909
14139
|
return;
|
|
13910
14140
|
}
|
|
13911
14141
|
this.openActionMenu(b, [
|
|
14142
|
+
{ icon: CLONE_ICON, label: "Duplicate", onClick: () => actions.duplicate() },
|
|
13912
14143
|
{ icon: FRONT_ICON, label: "Bring to front", onClick: () => actions.reorder("front") },
|
|
13913
14144
|
{ icon: BACK_ICON, label: "Send to back", onClick: () => actions.reorder("back") },
|
|
13914
14145
|
{ icon: RESET_ICON, label: "Reset settings", onClick: () => actions.resetSettings() }
|
|
@@ -14065,26 +14296,30 @@ var DrawingSettingsPopup = class {
|
|
|
14065
14296
|
this.tipEl?.remove();
|
|
14066
14297
|
this.tipEl = null;
|
|
14067
14298
|
}
|
|
14068
|
-
|
|
14299
|
+
/** An on/off button. A mixed state (the selected drawings disagree) reads as half-lit and
|
|
14300
|
+
* the first click turns it ON for all of them. `off` swaps in a distinct glyph while the
|
|
14301
|
+
* toggle is off (a lock reads as an open padlock until it is engaged); mixed shows `icon`. */
|
|
14302
|
+
toggle(tip, icon2, active, onChange, off) {
|
|
14069
14303
|
const b = this.base(tip);
|
|
14070
|
-
b.innerHTML = sized(icon2);
|
|
14071
14304
|
const set = (on2) => {
|
|
14072
|
-
b.dataset.active = on2 ? "1" : "0";
|
|
14305
|
+
b.dataset.active = on2 === MIXED ? "mixed" : on2 ? "1" : "0";
|
|
14306
|
+
b.innerHTML = sized(off && on2 === false ? off : icon2);
|
|
14073
14307
|
};
|
|
14074
14308
|
set(active);
|
|
14075
14309
|
let on = active;
|
|
14076
14310
|
b.addEventListener("click", () => {
|
|
14077
|
-
on = !on;
|
|
14311
|
+
on = on === MIXED ? true : !on;
|
|
14078
14312
|
set(on);
|
|
14079
14313
|
onChange(on);
|
|
14080
14314
|
});
|
|
14081
14315
|
return b;
|
|
14082
14316
|
}
|
|
14083
14317
|
/** An inline numeric width field for tools whose stroke range outgrows the 1–4px
|
|
14084
|
-
* ladder — the value is clamped to the schema's declared min/max on commit.
|
|
14318
|
+
* ladder — the value is clamped to the schema's declared min/max on commit. A mixed
|
|
14319
|
+
* value shows an empty field with a dash until a number is typed for all. */
|
|
14085
14320
|
widthInput(tip, value, min, max, step, onChange) {
|
|
14086
14321
|
const ni = new NumberInput({
|
|
14087
|
-
value,
|
|
14322
|
+
value: value === MIXED ? min : value,
|
|
14088
14323
|
min,
|
|
14089
14324
|
max,
|
|
14090
14325
|
step,
|
|
@@ -14096,13 +14331,18 @@ var DrawingSettingsPopup = class {
|
|
|
14096
14331
|
title: tip,
|
|
14097
14332
|
onChange
|
|
14098
14333
|
});
|
|
14334
|
+
if (value === MIXED) {
|
|
14335
|
+
ni.input.value = "";
|
|
14336
|
+
ni.input.placeholder = "\u2014";
|
|
14337
|
+
}
|
|
14099
14338
|
ni.el.dataset.tip = tip;
|
|
14100
14339
|
trapChartKeys(ni.el);
|
|
14101
14340
|
return ni.el;
|
|
14102
14341
|
}
|
|
14103
14342
|
/** A pick-one dropdown: the trigger shows the current value's glyph (plus an optional inline
|
|
14104
14343
|
* text label — e.g. `2px` — for controls that would otherwise look alike), and clicking it
|
|
14105
|
-
* opens a floating list of every option so a value is one click away (no cycling through).
|
|
14344
|
+
* opens a floating list of every option so a value is one click away (no cycling through).
|
|
14345
|
+
* A mixed value shows a dash in the trigger and highlights no row. */
|
|
14106
14346
|
dropdown(tip, values, current, render, onChange, opts = {}) {
|
|
14107
14347
|
const b = this.base(tip);
|
|
14108
14348
|
b.style.width = "auto";
|
|
@@ -14112,14 +14352,20 @@ var DrawingSettingsPopup = class {
|
|
|
14112
14352
|
let cur = current;
|
|
14113
14353
|
const paint = (v) => {
|
|
14114
14354
|
b.replaceChildren();
|
|
14115
|
-
|
|
14355
|
+
if (v === MIXED) {
|
|
14356
|
+
const dash = document.createElement("span");
|
|
14357
|
+
dash.textContent = "\u2014";
|
|
14358
|
+
dash.style.cssText = "min-width:18px;text-align:center;opacity:0.85;";
|
|
14359
|
+
b.appendChild(dash);
|
|
14360
|
+
}
|
|
14361
|
+
const glyph = v === MIXED ? "" : render(v);
|
|
14116
14362
|
if (glyph) {
|
|
14117
14363
|
const ic = document.createElement("span");
|
|
14118
14364
|
ic.style.cssText = "display:flex;";
|
|
14119
14365
|
ic.innerHTML = sized(glyph);
|
|
14120
14366
|
b.appendChild(ic);
|
|
14121
14367
|
}
|
|
14122
|
-
if (opts.label && opts.labelInTrigger) {
|
|
14368
|
+
if (opts.label && opts.labelInTrigger && v !== MIXED) {
|
|
14123
14369
|
const tx = document.createElement("span");
|
|
14124
14370
|
tx.textContent = opts.label(v);
|
|
14125
14371
|
tx.style.cssText = "font-size:11px;opacity:0.85;font-variant-numeric:tabular-nums;";
|
|
@@ -14189,8 +14435,10 @@ var DrawingSettingsPopup = class {
|
|
|
14189
14435
|
}
|
|
14190
14436
|
/** A color control: an `icon` over a thin **colored underline** showing the current
|
|
14191
14437
|
* value (the common idiom), with an invisible native picker overlaid to edit it.
|
|
14192
|
-
* `iconSize` shrinks it for the floating text controls.
|
|
14193
|
-
|
|
14438
|
+
* `iconSize` shrinks it for the floating text controls. A mixed color paints the
|
|
14439
|
+
* underline striped with the colors `used` across the selection, and the picker then
|
|
14440
|
+
* leads with those colors so unifying onto one of them is a single click. */
|
|
14441
|
+
colorButton(tip, icon2, color, onChange, iconSize = 17, used = []) {
|
|
14194
14442
|
const b = document.createElement("button");
|
|
14195
14443
|
b.type = "button";
|
|
14196
14444
|
b.dataset.tip = tip;
|
|
@@ -14200,13 +14448,14 @@ var DrawingSettingsPopup = class {
|
|
|
14200
14448
|
ic.style.cssText = "display:flex;";
|
|
14201
14449
|
ic.innerHTML = sized(icon2, iconSize);
|
|
14202
14450
|
const bar = document.createElement("span");
|
|
14203
|
-
bar.style.cssText = `display:block;height:3px;width:${Math.round(iconSize * 0.85)}px;border-radius:2px;background:${color};`;
|
|
14451
|
+
bar.style.cssText = `display:block;height:3px;width:${Math.round(iconSize * 0.85)}px;border-radius:2px;background:${color === MIXED ? stripes(used) : color};`;
|
|
14204
14452
|
b.append(ic, bar);
|
|
14205
14453
|
let cur = color;
|
|
14206
14454
|
b.addEventListener("pointerdown", (e) => e.stopPropagation());
|
|
14207
14455
|
b.addEventListener("click", (e) => {
|
|
14208
14456
|
e.stopPropagation();
|
|
14209
|
-
|
|
14457
|
+
const seed = cur === MIXED ? used[0] ?? DEFAULT_DRAWING_COLOR : cur;
|
|
14458
|
+
this.toggleColorPopover(b, seed, cur === MIXED ? used : [], (v) => {
|
|
14210
14459
|
cur = v;
|
|
14211
14460
|
bar.style.background = v;
|
|
14212
14461
|
onChange(v);
|
|
@@ -14215,20 +14464,42 @@ var DrawingSettingsPopup = class {
|
|
|
14215
14464
|
return b;
|
|
14216
14465
|
}
|
|
14217
14466
|
/** Open the color popover for `anchor`, or close it if it's already this anchor's (toggle). */
|
|
14218
|
-
toggleColorPopover(anchor, color, onChange) {
|
|
14467
|
+
toggleColorPopover(anchor, color, used, onChange) {
|
|
14219
14468
|
if (this.colorOwner === anchor) {
|
|
14220
14469
|
this.closeColorPopover();
|
|
14221
14470
|
return;
|
|
14222
14471
|
}
|
|
14223
|
-
this.openColorPopover(anchor, color, onChange);
|
|
14472
|
+
this.openColorPopover(anchor, color, used, onChange);
|
|
14224
14473
|
}
|
|
14225
|
-
/** A floating RGB picker + opacity slider anchored to a color swatch — emits `#RRGGBB(AA)`.
|
|
14226
|
-
|
|
14474
|
+
/** A floating RGB picker + opacity slider anchored to a color swatch — emits `#RRGGBB(AA)`.
|
|
14475
|
+
* `used` (a mixed swatch's colors) leads the picker as one-click unify shortcuts. */
|
|
14476
|
+
openColorPopover(anchor, color, used, onChange) {
|
|
14227
14477
|
const t = this.theme;
|
|
14228
14478
|
this.colorPop = this.hostFloat(anchor, {
|
|
14229
14479
|
zIndex: 25,
|
|
14230
14480
|
padding: "10px",
|
|
14231
|
-
fill: (el) => {
|
|
14481
|
+
fill: (el, pop) => {
|
|
14482
|
+
if (used.length > 1) {
|
|
14483
|
+
const row = document.createElement("div");
|
|
14484
|
+
row.style.cssText = "display:flex;align-items:center;gap:6px;padding-bottom:9px;border-bottom:1px solid var(--vela-border);";
|
|
14485
|
+
const lbl = document.createElement("span");
|
|
14486
|
+
lbl.textContent = "In use";
|
|
14487
|
+
lbl.style.cssText = "font:var(--vela-font-size-sm) inherit;opacity:0.7;margin-right:2px;";
|
|
14488
|
+
row.appendChild(lbl);
|
|
14489
|
+
for (const c of used) {
|
|
14490
|
+
const sw = document.createElement("button");
|
|
14491
|
+
sw.type = "button";
|
|
14492
|
+
sw.dataset.tip = c;
|
|
14493
|
+
sw.style.cssText = `width:18px;height:18px;border-radius:4px;border:1px solid var(--vela-border-strong);cursor:pointer;background:${c};padding:0;`;
|
|
14494
|
+
sw.addEventListener("click", (e) => {
|
|
14495
|
+
e.stopPropagation();
|
|
14496
|
+
pop.hide();
|
|
14497
|
+
onChange(c);
|
|
14498
|
+
});
|
|
14499
|
+
row.appendChild(sw);
|
|
14500
|
+
}
|
|
14501
|
+
el.appendChild(row);
|
|
14502
|
+
}
|
|
14232
14503
|
el.appendChild(buildColorPicker(color, t, onChange));
|
|
14233
14504
|
}
|
|
14234
14505
|
});
|
|
@@ -14249,6 +14520,7 @@ var TYPE_ICON = icon("type");
|
|
|
14249
14520
|
var PRICE_DELTA_ICON = icon("price-delta");
|
|
14250
14521
|
var DATE_DELTA_ICON = icon("date-delta");
|
|
14251
14522
|
var LOCK_ICON = icon("lock");
|
|
14523
|
+
var UNLOCK_ICON = icon("unlock");
|
|
14252
14524
|
var FRONT_ICON = icon("bring-front");
|
|
14253
14525
|
var BACK_ICON = icon("send-back");
|
|
14254
14526
|
var TRASH_ICON = icon("trash");
|
|
@@ -14256,6 +14528,7 @@ var BOLD_ICON = icon("bold");
|
|
|
14256
14528
|
var ITALIC_ICON = icon("italic");
|
|
14257
14529
|
var GRIP_ICON = icon("grip");
|
|
14258
14530
|
var KEBAB_ICON = icon("kebab");
|
|
14531
|
+
var CLONE_ICON = icon("clone");
|
|
14259
14532
|
var RESET_ICON = icon("reset");
|
|
14260
14533
|
var GEAR_ICON = icon("gear");
|
|
14261
14534
|
var CHEVRON_ICON = icon("chevron-down");
|
|
@@ -14264,6 +14537,12 @@ var BANDS_ICON = icon("bands");
|
|
|
14264
14537
|
var DEDEKIND_ICON = icon("dedekind");
|
|
14265
14538
|
var SONIC_ICON = icon("sonic");
|
|
14266
14539
|
var SUPERSONIC_ICON = icon("supersonic");
|
|
14540
|
+
function stripes(colors) {
|
|
14541
|
+
const bands = colors.length > 1 ? colors : ["var(--vela-fg-faint)", "var(--vela-fg-muted)"];
|
|
14542
|
+
const step = 100 / bands.length;
|
|
14543
|
+
const stops = bands.map((c, i) => `${c} ${i * step}% ${(i + 1) * step}%`).join(",");
|
|
14544
|
+
return `linear-gradient(90deg,${stops})`;
|
|
14545
|
+
}
|
|
14267
14546
|
function lineIcon(width, style) {
|
|
14268
14547
|
const dash = style === "dashed" ? "5,3" : style === "dotted" ? "1.5,3" : "";
|
|
14269
14548
|
return svg24(`<line x1="3.5" y1="12" x2="20.5" y2="12" stroke-width="${Number(width) || 2}" stroke-dasharray="${dash}"/>`);
|
|
@@ -14547,6 +14826,20 @@ var UserDrawingController = class {
|
|
|
14547
14826
|
// reported exactly when it actually changes).
|
|
14548
14827
|
this.textEditor = null;
|
|
14549
14828
|
this.painter = new DrawingPainter();
|
|
14829
|
+
/** The open popup is the multi-selection bar (vs one drawing's own) — decides what a
|
|
14830
|
+
* shrinking selection does with it. */
|
|
14831
|
+
this.popupMulti = false;
|
|
14832
|
+
/** Dismiss-on-outside-press also clears the selection — except when the press is ABOUT the
|
|
14833
|
+
* selection: it carries a multi-select modifier (Shift / Ctrl / Cmd — it will add to the
|
|
14834
|
+
* selection or sweep a marquee onto it), or it landed on a drawing that is already selected
|
|
14835
|
+
* (the canvas handler ran first and is holding it for a drag or a click), so the drawings
|
|
14836
|
+
* whose bar just closed must stay selected. */
|
|
14837
|
+
this.onPopupDismissed = (e) => {
|
|
14838
|
+
if (e && (e.shiftKey || e.ctrlKey || e.metaKey)) return;
|
|
14839
|
+
const pressed = this.interaction.pressedId();
|
|
14840
|
+
if (pressed && this.selectedIds.has(pressed)) return;
|
|
14841
|
+
this.clearSelection();
|
|
14842
|
+
};
|
|
14550
14843
|
this.ctx = canvas.getContext("2d");
|
|
14551
14844
|
this.popup = new DrawingSettingsPopup(overlayHost, deps.theme(), () => deps.chartBarMs());
|
|
14552
14845
|
this.toolbar = new DrawingToolbar(
|
|
@@ -14723,6 +15016,12 @@ var UserDrawingController = class {
|
|
|
14723
15016
|
}
|
|
14724
15017
|
setSelection(ids) {
|
|
14725
15018
|
this.selectedIds = new Set(ids);
|
|
15019
|
+
if (ids.length >= 2) this.openSettingsForSelection(ids);
|
|
15020
|
+
else if (this.popupMulti) {
|
|
15021
|
+
this.popupMulti = false;
|
|
15022
|
+
this.popup.close();
|
|
15023
|
+
if (ids.length === 1) this.openSettingsById(ids[0], 0, 0);
|
|
15024
|
+
}
|
|
14726
15025
|
this.render();
|
|
14727
15026
|
}
|
|
14728
15027
|
/** Programmatic twin of clicking the drawing: highlight it + float its settings popup. */
|
|
@@ -14768,12 +15067,16 @@ var UserDrawingController = class {
|
|
|
14768
15067
|
});
|
|
14769
15068
|
}
|
|
14770
15069
|
/** Delete the (unlocked) drawing under the cursor. True when one was removed.
|
|
14771
|
-
* Shared by the eraser (click + drag) and the middle-click shortcut
|
|
14772
|
-
|
|
15070
|
+
* Shared by the eraser (click + drag) and the middle-click shortcut; the latter passes
|
|
15071
|
+
* `withSelection` so a hit on a member of a multi-selection removes the selection's
|
|
15072
|
+
* unlocked members — whichever member was hit, locked or not. */
|
|
15073
|
+
deleteAt(x, y, withSelection = false) {
|
|
14773
15074
|
const hit = topDrawingAt(this.drawings, x, y, this.deps.projector(), HIT_TOLERANCE);
|
|
14774
|
-
if (!hit
|
|
15075
|
+
if (!hit) return false;
|
|
15076
|
+
const ids = deleteTargets(hit, this.selectedIds, this.drawings, withSelection);
|
|
15077
|
+
if (ids.length === 0) return false;
|
|
14775
15078
|
this.popup.close();
|
|
14776
|
-
this.emit({ kind: "delete", ids
|
|
15079
|
+
this.emit({ kind: "delete", ids });
|
|
14777
15080
|
return true;
|
|
14778
15081
|
}
|
|
14779
15082
|
/** Shift+press on the empty plot: arm the measure ruler AND start it at (x, y) in one
|
|
@@ -14799,7 +15102,14 @@ var UserDrawingController = class {
|
|
|
14799
15102
|
this.render();
|
|
14800
15103
|
}
|
|
14801
15104
|
}
|
|
14802
|
-
|
|
15105
|
+
/** Ctrl/Cmd+press on the empty plot: sweep a selection box (adds the drawings it touches to
|
|
15106
|
+
* the selection). Returns false when a mode/tool is active (the normal press path owns it). */
|
|
15107
|
+
beginMarqueeAt(x, y) {
|
|
15108
|
+
if (this.measureMode || this.eraserMode || !this.interaction.beginMarquee(x, y)) return false;
|
|
15109
|
+
this.render();
|
|
15110
|
+
return true;
|
|
15111
|
+
}
|
|
15112
|
+
pointerDown(x, y, snap = "off", shift2 = false, mod = false) {
|
|
14803
15113
|
if (this.eraserMode) {
|
|
14804
15114
|
this.erasing = true;
|
|
14805
15115
|
this.deleteAt(x, y);
|
|
@@ -14819,9 +15129,9 @@ var UserDrawingController = class {
|
|
|
14819
15129
|
return;
|
|
14820
15130
|
}
|
|
14821
15131
|
}
|
|
14822
|
-
this.interaction.down(x, y, snap, shift2);
|
|
15132
|
+
this.interaction.down(x, y, snap, shift2, mod);
|
|
14823
15133
|
}
|
|
14824
|
-
pointerMove(x, y, snap = "off", shift2 = false) {
|
|
15134
|
+
pointerMove(x, y, snap = "off", shift2 = false, mod = false) {
|
|
14825
15135
|
if (this.eraserMode) {
|
|
14826
15136
|
if (this.erasing) this.deleteAt(x, y);
|
|
14827
15137
|
return;
|
|
@@ -14833,12 +15143,14 @@ var UserDrawingController = class {
|
|
|
14833
15143
|
return;
|
|
14834
15144
|
}
|
|
14835
15145
|
this.interaction.move(x, y, snap, shift2);
|
|
14836
|
-
this.updateHover(x, y);
|
|
15146
|
+
this.updateHover(x, y, mod);
|
|
14837
15147
|
}
|
|
14838
|
-
/** Track which drawing is hovered so its handles appear on hover (and only then).
|
|
14839
|
-
|
|
15148
|
+
/** Track which drawing is hovered so its handles appear on hover (and only then). While
|
|
15149
|
+
* Ctrl/Cmd is held the cursor is picking a selection — handles then mark only what IS
|
|
15150
|
+
* selected, so a hovered candidate doesn't already read as selected. */
|
|
15151
|
+
updateHover(x, y, mod = false) {
|
|
14840
15152
|
let id = null;
|
|
14841
|
-
if (this.activeTool == null && !this.interaction.isPlacing() && !this.interaction.isDragging()) {
|
|
15153
|
+
if (!mod && this.activeTool == null && !this.interaction.isPlacing() && !this.interaction.isDragging()) {
|
|
14842
15154
|
id = topDrawingAt(this.drawings, x, y, this.deps.projector(), HIT_TOLERANCE)?.id ?? null;
|
|
14843
15155
|
}
|
|
14844
15156
|
if (id !== this.hoveredId) {
|
|
@@ -14858,7 +15170,12 @@ var UserDrawingController = class {
|
|
|
14858
15170
|
this.render();
|
|
14859
15171
|
return;
|
|
14860
15172
|
}
|
|
15173
|
+
const dragged = this.interaction.isDragging() ? this.interaction.pressedId() : null;
|
|
14861
15174
|
this.interaction.up(x, y);
|
|
15175
|
+
if (dragged && this.selectedIds.has(dragged) && !this.popup.isOpen()) {
|
|
15176
|
+
if (this.selectedIds.size >= 2) this.openSettingsForSelection(this.selectionIds());
|
|
15177
|
+
else this.openSettingsById(dragged, x, y);
|
|
15178
|
+
}
|
|
14862
15179
|
}
|
|
14863
15180
|
/** Toggle the transient ruler. Arming it clears any drawing tool + selection. */
|
|
14864
15181
|
toggleMeasure() {
|
|
@@ -15123,7 +15440,7 @@ var UserDrawingController = class {
|
|
|
15123
15440
|
this.emit({ kind: "duplicate", ids: this.selectionIds() });
|
|
15124
15441
|
break;
|
|
15125
15442
|
case "delete": {
|
|
15126
|
-
const ids = this.selectedIds.size ? this.
|
|
15443
|
+
const ids = this.selectedIds.size ? this.deletableSelection() : this.hoveredId ? [this.hoveredId] : [];
|
|
15127
15444
|
if (ids.length) {
|
|
15128
15445
|
this.popup.close();
|
|
15129
15446
|
this.emit({ kind: "delete", ids });
|
|
@@ -15139,6 +15456,9 @@ var UserDrawingController = class {
|
|
|
15139
15456
|
selectionIds() {
|
|
15140
15457
|
return [...this.selectedIds];
|
|
15141
15458
|
}
|
|
15459
|
+
deletableSelection() {
|
|
15460
|
+
return deletableSelection(this.selectedIds, this.drawings);
|
|
15461
|
+
}
|
|
15142
15462
|
/** Move every selected (unlocked) drawing by a pixel delta — one edit/edit-many → one undo step. */
|
|
15143
15463
|
nudgeSelection(dx, dy) {
|
|
15144
15464
|
const proj = this.deps.projector();
|
|
@@ -15247,6 +15567,8 @@ var UserDrawingController = class {
|
|
|
15247
15567
|
this.painter.seriesLook = this.deps.seriesLook();
|
|
15248
15568
|
this.painter.paintAll(ctx, this.drawings.filter((d) => !this.isInterleaved(d)), proj, this.deps.theme(), targets);
|
|
15249
15569
|
this.painter.paintHighlights(ctx, this.drawings.filter((d) => this.isInterleaved(d)), proj, handleIdsFor(targets));
|
|
15570
|
+
const clones = this.interaction.dragClones();
|
|
15571
|
+
if (clones) this.painter.paintAll(ctx, clones, proj, this.deps.theme(), { selected: new Set(clones.map((c) => c.id)) });
|
|
15250
15572
|
this.layoutTextEditor();
|
|
15251
15573
|
const ghost = this.interaction.ghost();
|
|
15252
15574
|
if (ghost) this.painter.paintGhost(ctx, ghost, proj, this.deps.theme());
|
|
@@ -15262,6 +15584,8 @@ var UserDrawingController = class {
|
|
|
15262
15584
|
const my = m ? proj.yOf(m.point.price, m.paneId) : null;
|
|
15263
15585
|
if (m && my != null) this.painter.paintSnapRing(ctx, proj.xOf(m.point.time), my, this.deps.theme());
|
|
15264
15586
|
if (this.measure.isActive()) this.measure.paint(ctx, proj, this.deps.theme());
|
|
15587
|
+
const marquee = this.interaction.marqueeRect();
|
|
15588
|
+
if (marquee) this.painter.paintMarquee(ctx, marquee);
|
|
15265
15589
|
}
|
|
15266
15590
|
destroy() {
|
|
15267
15591
|
this.closeTextEditor();
|
|
@@ -15279,15 +15603,80 @@ var UserDrawingController = class {
|
|
|
15279
15603
|
if (this.selectedIds.size) this.emit({ kind: "select", ids: [] });
|
|
15280
15604
|
this.render();
|
|
15281
15605
|
}
|
|
15606
|
+
/** Report the live drawings' current state as one edit (one undo step, however many). */
|
|
15607
|
+
emitEdits(drawings) {
|
|
15608
|
+
const docs = drawings.map((d) => d.serialize());
|
|
15609
|
+
if (docs.length === 1) this.emit({ kind: "edit", doc: docs[0] });
|
|
15610
|
+
else if (docs.length > 1) this.emit({ kind: "edit-many", docs });
|
|
15611
|
+
}
|
|
15612
|
+
/** The one bar for a multi-selection: it shows the controls all of its drawings share and
|
|
15613
|
+
* every action applies to all of them. Floats clear of their combined bounds. */
|
|
15614
|
+
openSettingsForSelection(ids) {
|
|
15615
|
+
const live = () => ids.map((id) => this.drawings.find((d) => d.id === id)).filter((d) => d != null);
|
|
15616
|
+
const drawings = live();
|
|
15617
|
+
if (drawings.length < 2) return;
|
|
15618
|
+
const proj = this.deps.projector();
|
|
15619
|
+
let anchor = null;
|
|
15620
|
+
for (const d of drawings) {
|
|
15621
|
+
const b = d.bounds(proj);
|
|
15622
|
+
if (!b) continue;
|
|
15623
|
+
if (!anchor) anchor = { ...b };
|
|
15624
|
+
else {
|
|
15625
|
+
const right = Math.max(anchor.x + anchor.w, b.x + b.w);
|
|
15626
|
+
const bottom = Math.max(anchor.y + anchor.h, b.y + b.h);
|
|
15627
|
+
anchor.x = Math.min(anchor.x, b.x);
|
|
15628
|
+
anchor.y = Math.min(anchor.y, b.y);
|
|
15629
|
+
anchor.w = right - anchor.x;
|
|
15630
|
+
anchor.h = bottom - anchor.y;
|
|
15631
|
+
}
|
|
15632
|
+
}
|
|
15633
|
+
this.popupMulti = true;
|
|
15634
|
+
this.emit({ kind: "settings", id: drawings[0].id });
|
|
15635
|
+
this.popup.open(drawings, anchor, {
|
|
15636
|
+
resolve: () => live()[0] ?? null,
|
|
15637
|
+
patch: (p) => {
|
|
15638
|
+
const ds = live();
|
|
15639
|
+
for (const d of ds) d.applySettings(p);
|
|
15640
|
+
this.render();
|
|
15641
|
+
this.emitEdits(ds);
|
|
15642
|
+
},
|
|
15643
|
+
setLocked: (v) => {
|
|
15644
|
+
const ds = live();
|
|
15645
|
+
for (const d of ds) d.locked = v;
|
|
15646
|
+
this.emitEdits(ds);
|
|
15647
|
+
},
|
|
15648
|
+
reorder: (to) => {
|
|
15649
|
+
for (const d of live()) this.emit({ kind: "reorder", id: d.id, to });
|
|
15650
|
+
},
|
|
15651
|
+
duplicate: () => {
|
|
15652
|
+
this.popup.close();
|
|
15653
|
+
this.emit({ kind: "duplicate", ids: live().map((d) => d.id) });
|
|
15654
|
+
},
|
|
15655
|
+
resetSettings: () => {
|
|
15656
|
+
const ds = live();
|
|
15657
|
+
for (const d of ds) resetDrawingSettings(d);
|
|
15658
|
+
this.render();
|
|
15659
|
+
this.emitEdits(ds);
|
|
15660
|
+
this.openSettingsForSelection(ids);
|
|
15661
|
+
},
|
|
15662
|
+
remove: () => {
|
|
15663
|
+
const ids2 = live().filter((d) => !d.locked).map((d) => d.id);
|
|
15664
|
+
if (ids2.length === 0) return;
|
|
15665
|
+
this.popup.close();
|
|
15666
|
+
this.emit({ kind: "delete", ids: ids2 });
|
|
15667
|
+
}
|
|
15668
|
+
}, this.onPopupDismissed);
|
|
15669
|
+
}
|
|
15282
15670
|
/** A click on a drawing opens its settings toolbar (text labels edit text here too). */
|
|
15283
15671
|
openSettingsById(id, _x, _y) {
|
|
15284
15672
|
const drawing = this.drawings.find((d) => d.id === id);
|
|
15285
15673
|
if (!drawing) return;
|
|
15286
15674
|
const live = () => this.drawings.find((d) => d.id === id);
|
|
15675
|
+
this.popupMulti = false;
|
|
15287
15676
|
this.emit({ kind: "select", ids: [id] });
|
|
15288
15677
|
this.emit({ kind: "settings", id });
|
|
15289
15678
|
const anchor = drawing.bounds(this.deps.projector());
|
|
15290
|
-
this.popup.open(drawing, anchor, {
|
|
15679
|
+
this.popup.open([drawing], anchor, {
|
|
15291
15680
|
// Sync rebuilds instances, so a panel that reads values back after a patch (e.g. the
|
|
15292
15681
|
// position tool's price fields, where one edit can flip another level) resolves fresh.
|
|
15293
15682
|
resolve: () => live() ?? null,
|
|
@@ -15305,6 +15694,11 @@ var UserDrawingController = class {
|
|
|
15305
15694
|
this.emit({ kind: "edit", doc: d.serialize() });
|
|
15306
15695
|
},
|
|
15307
15696
|
reorder: (to) => this.emit({ kind: "reorder", id, to }),
|
|
15697
|
+
duplicate: () => {
|
|
15698
|
+
this.finishTextEditor(true);
|
|
15699
|
+
this.popup.close();
|
|
15700
|
+
this.emit({ kind: "duplicate", ids: [id] });
|
|
15701
|
+
},
|
|
15308
15702
|
resetSettings: () => {
|
|
15309
15703
|
const d = live();
|
|
15310
15704
|
if (!d) return;
|
|
@@ -15327,7 +15721,13 @@ var UserDrawingController = class {
|
|
|
15327
15721
|
this.popup.close();
|
|
15328
15722
|
this.emit({ kind: "delete", ids: [id] });
|
|
15329
15723
|
}
|
|
15330
|
-
},
|
|
15724
|
+
}, this.onPopupDismissed);
|
|
15725
|
+
}
|
|
15726
|
+
/** A plain click on the empty plot: drop the selection. The popup's dismiss-on-outside-press
|
|
15727
|
+
* does the same, but a selection can exist with no bar open (a lone Ctrl-click pick, a
|
|
15728
|
+
* selection whose bar was dismissed by a modifier press) — this covers those. */
|
|
15729
|
+
deselect() {
|
|
15730
|
+
this.clearSelection();
|
|
15331
15731
|
}
|
|
15332
15732
|
/** Report placement progress upstream (`draft` intent) so the drawings sync can
|
|
15333
15733
|
* mirror the ghost live on linked charts. Called from every render; only actual
|
|
@@ -15353,6 +15753,13 @@ var UserDrawingController = class {
|
|
|
15353
15753
|
}, 0);
|
|
15354
15754
|
return;
|
|
15355
15755
|
}
|
|
15756
|
+
if (i.kind === "clone") {
|
|
15757
|
+
const before = new Set(this.drawings.map((d) => d.id));
|
|
15758
|
+
this.intentCb?.(i);
|
|
15759
|
+
const fresh = this.drawings.filter((d) => !before.has(d.id));
|
|
15760
|
+
if (fresh.length === 1) this.openSettingsById(fresh[0].id, 0, 0);
|
|
15761
|
+
return;
|
|
15762
|
+
}
|
|
15356
15763
|
this.intentCb?.(i);
|
|
15357
15764
|
}
|
|
15358
15765
|
};
|
|
@@ -16491,7 +16898,6 @@ var ZOOM_OUT_MARGIN_BARS = 6;
|
|
|
16491
16898
|
var ZOOM_TAU_MS = 70;
|
|
16492
16899
|
var SCALE_TAU_MS = 80;
|
|
16493
16900
|
var FLING_TAU_MS = 110;
|
|
16494
|
-
var LIVE_BAR_TAU_MS = 90;
|
|
16495
16901
|
var SCROLL_TO_TAU_MS = 130;
|
|
16496
16902
|
var FLING_STOP_PX = 0.02;
|
|
16497
16903
|
var PRICE_SCALE_K = 4e-3;
|
|
@@ -16562,9 +16968,13 @@ var NativeRenderer = class {
|
|
|
16562
16968
|
/** Drawings layer self-serves Ctrl+Z/Y (see the `historyChords` feature). */
|
|
16563
16969
|
this.historyChordsEnabled = true;
|
|
16564
16970
|
this.liveRegion = null;
|
|
16565
|
-
// ── animation state (eased zoom + inertial pan) ──
|
|
16971
|
+
// ── animation state (eased zoom + inertial pan + live-bar glide) ──
|
|
16566
16972
|
this.animZoom = true;
|
|
16567
16973
|
this.animPan = true;
|
|
16974
|
+
this.animLiveBarMs = 0;
|
|
16975
|
+
// forming-bar OHLC glide time-constant; 0 = each tick snaps
|
|
16976
|
+
this.animLiveBarOnMs = LIVE_BAR_EASE_DEFAULT_MS;
|
|
16977
|
+
// the duration the settings-dialog on/off toggle restores (last non-zero value configured)
|
|
16568
16978
|
// Brand default candles.
|
|
16569
16979
|
this.candleUp = BULLISH;
|
|
16570
16980
|
this.candleDown = BEARISH;
|
|
@@ -16689,7 +17099,7 @@ var NativeRenderer = class {
|
|
|
16689
17099
|
this.moveIndicatorCbs = /* @__PURE__ */ new Set();
|
|
16690
17100
|
this.priceStyleCbs = /* @__PURE__ */ new Set();
|
|
16691
17101
|
this.name = "native";
|
|
16692
|
-
this.features = ["logScale", "currentPriceLine", "priceLabel", "countdown", "upColor", "downColor", "glow", "animZoom", "animPan", "intro", "zoomAnchor", "axisDrag", "paneResize", "candleZOrder", "candleVisible", "seriesOrder", "highlights", "sessionZones", "gridlines", "axisLabels", "scaleMode", "invertScale", "paneScales", "autoScale", "timezone", "keyboard", "historyChords", "priceStyle", "priceBaseline", "baselinePrice", "settings", "attribution", "dialogHost", "tradeMarkers", "indicatorTitles", "indicatorValues"];
|
|
17102
|
+
this.features = ["logScale", "currentPriceLine", "priceLabel", "countdown", "upColor", "downColor", "glow", "animZoom", "animPan", "animLiveBar", "intro", "zoomAnchor", "axisDrag", "paneResize", "candleZOrder", "candleVisible", "seriesOrder", "highlights", "sessionZones", "gridlines", "axisLabels", "scaleMode", "invertScale", "paneScales", "autoScale", "timezone", "keyboard", "historyChords", "priceStyle", "priceBaseline", "baselinePrice", "settings", "attribution", "dialogHost", "tradeMarkers", "indicatorTitles", "indicatorValues"];
|
|
16693
17103
|
/** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
|
|
16694
17104
|
* so moving onto the button doesn't count as leaving). */
|
|
16695
17105
|
this.onScrollProximityMove = (e) => {
|
|
@@ -16723,6 +17133,7 @@ var NativeRenderer = class {
|
|
|
16723
17133
|
this.backendMode = opts.nativeBackend;
|
|
16724
17134
|
this.animZoom = opts.animZoom;
|
|
16725
17135
|
this.animPan = opts.animPan;
|
|
17136
|
+
this.setLiveBarEase(resolveLiveBarEaseMs(opts.animLiveBar));
|
|
16726
17137
|
this.glowAmount = opts.glow;
|
|
16727
17138
|
this.candleUp = opts.upColor;
|
|
16728
17139
|
this.candleDown = opts.downColor;
|
|
@@ -16772,6 +17183,10 @@ var NativeRenderer = class {
|
|
|
16772
17183
|
case "animPan":
|
|
16773
17184
|
this.animPan = Boolean(value);
|
|
16774
17185
|
return;
|
|
17186
|
+
case "animLiveBar":
|
|
17187
|
+
this.setLiveBarEase(resolveLiveBarEaseMs(value));
|
|
17188
|
+
return;
|
|
17189
|
+
// affects the next tick only; a glide in flight finishes at the new rate (or snaps at 0)
|
|
16775
17190
|
case "intro": {
|
|
16776
17191
|
const s = value === false || value === "none" || value === "off" || value == null ? "" : String(value);
|
|
16777
17192
|
this.introStyle = s;
|
|
@@ -16908,6 +17323,8 @@ var NativeRenderer = class {
|
|
|
16908
17323
|
return this.animZoom;
|
|
16909
17324
|
case "animPan":
|
|
16910
17325
|
return this.animPan;
|
|
17326
|
+
case "animLiveBar":
|
|
17327
|
+
return this.animLiveBarMs;
|
|
16911
17328
|
case "intro":
|
|
16912
17329
|
return this.introStyle;
|
|
16913
17330
|
case "zoomAnchor":
|
|
@@ -17076,7 +17493,8 @@ var NativeRenderer = class {
|
|
|
17076
17493
|
labelsVisible: this.scene.showAxisLabels,
|
|
17077
17494
|
currentPriceLine: this.scene.showPriceLine,
|
|
17078
17495
|
priceLabel: this.scene.showPriceLabel,
|
|
17079
|
-
countdown: this.scene.showCountdown
|
|
17496
|
+
countdown: this.scene.showCountdown,
|
|
17497
|
+
animateLastPrice: this.animLiveBarMs > 0
|
|
17080
17498
|
},
|
|
17081
17499
|
panes: { separatorColor: s.separatorColor ?? t.borderColor },
|
|
17082
17500
|
trades: {
|
|
@@ -17183,6 +17601,7 @@ var NativeRenderer = class {
|
|
|
17183
17601
|
this.scene.showPriceLabel = next.priceScale.priceLabel;
|
|
17184
17602
|
this.scene.showCountdown = next.priceScale.countdown;
|
|
17185
17603
|
this.syncCountdownTimer();
|
|
17604
|
+
this.animLiveBarMs = next.priceScale.animateLastPrice ? this.animLiveBarOnMs : 0;
|
|
17186
17605
|
s.separatorColor = keepInherit(s.separatorColor, next.panes.separatorColor, prevTheme.borderColor);
|
|
17187
17606
|
this.scene.tradeMarkers = {
|
|
17188
17607
|
visible: next.trades.visible,
|
|
@@ -17626,7 +18045,10 @@ var NativeRenderer = class {
|
|
|
17626
18045
|
zoomTo: (target, anchorLogical, anchorX) => this.zoomTo(target, anchorLogical, anchorX),
|
|
17627
18046
|
fling: (v) => this.fling(v),
|
|
17628
18047
|
onPointerMove: (x, y) => this.handlePointerMove(x, y),
|
|
17629
|
-
onClick: (x) =>
|
|
18048
|
+
onClick: (x) => {
|
|
18049
|
+
this.userDrawings?.deselect();
|
|
18050
|
+
this.handleClick(x);
|
|
18051
|
+
},
|
|
17630
18052
|
onAxisLongPress: (axis, x, y) => {
|
|
17631
18053
|
for (const cb of this.axisLongPressCbs) cb({ axis, x, y });
|
|
17632
18054
|
},
|
|
@@ -17644,11 +18066,13 @@ var NativeRenderer = class {
|
|
|
17644
18066
|
// User drawings claim a gesture before pan when armed / over a drawing.
|
|
17645
18067
|
drawingsClaim: (x, y) => this.userDrawings?.claim(x, y) ?? false,
|
|
17646
18068
|
drawingsMeasureStart: (x, y, snap) => this.userDrawings?.beginMeasureAt(x, y, snap) ?? false,
|
|
17647
|
-
|
|
18069
|
+
drawingsMarqueeStart: (x, y) => this.userDrawings?.beginMarqueeAt(x, y) ?? false,
|
|
18070
|
+
drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y, true) ?? false,
|
|
18071
|
+
// middle-click: a selected hit takes the whole selection
|
|
17648
18072
|
drawingsCancelPlacement: () => this.userDrawings?.cancelPlacement() ?? false,
|
|
17649
18073
|
drawingsSnapMode: () => this.snapMode,
|
|
17650
|
-
drawingsPointerDown: (x, y, snap, shift2) => this.userDrawings?.pointerDown(x, y, snap, shift2),
|
|
17651
|
-
drawingsPointerMove: (x, y, snap, shift2) => this.userDrawings?.pointerMove(x, y, snap, shift2),
|
|
18074
|
+
drawingsPointerDown: (x, y, snap, shift2, mod) => this.userDrawings?.pointerDown(x, y, snap, shift2, mod),
|
|
18075
|
+
drawingsPointerMove: (x, y, snap, shift2, mod) => this.userDrawings?.pointerMove(x, y, snap, shift2, mod),
|
|
17652
18076
|
drawingsPointerUp: (x, y, snap) => this.userDrawings?.pointerUp(x, y, snap),
|
|
17653
18077
|
drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? null,
|
|
17654
18078
|
drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
|
|
@@ -17999,8 +18423,11 @@ var NativeRenderer = class {
|
|
|
17999
18423
|
const last = this.bars[n - 1];
|
|
18000
18424
|
if (last && bar.time === last.time) {
|
|
18001
18425
|
this.bars[n - 1] = bar;
|
|
18002
|
-
if (this.liveEaseTime !== bar.time)
|
|
18003
|
-
|
|
18426
|
+
if (this.animLiveBarMs <= 0 || this.liveEaseTime !== bar.time) {
|
|
18427
|
+
this.syncLiveEase(bar);
|
|
18428
|
+
} else {
|
|
18429
|
+
this.animator.start();
|
|
18430
|
+
}
|
|
18004
18431
|
} else if (!last || bar.time > last.time) {
|
|
18005
18432
|
this.bars.push(bar);
|
|
18006
18433
|
this.syncLiveEase(bar);
|
|
@@ -18012,6 +18439,12 @@ var NativeRenderer = class {
|
|
|
18012
18439
|
this.scene.bars = this.bars;
|
|
18013
18440
|
this.scheduler.invalidate(4 /* Full */);
|
|
18014
18441
|
}
|
|
18442
|
+
/** Set the live-bar glide duration (0 = off). A non-zero value is also remembered as
|
|
18443
|
+
* what the config's on/off toggle (`priceScale.animateLastPrice`) switches back on to. */
|
|
18444
|
+
setLiveBarEase(ms) {
|
|
18445
|
+
this.animLiveBarMs = ms;
|
|
18446
|
+
if (ms > 0) this.animLiveBarOnMs = ms;
|
|
18447
|
+
}
|
|
18015
18448
|
/** Snap the eased forming-bar state to `bar` — no glide (a fresh bar or the first tick of one). */
|
|
18016
18449
|
syncLiveEase(bar) {
|
|
18017
18450
|
this.liveEaseTime = bar.time;
|
|
@@ -18024,9 +18457,10 @@ var NativeRenderer = class {
|
|
|
18024
18457
|
const target = this.bars[this.bars.length - 1];
|
|
18025
18458
|
if (!target || this.liveEaseTime !== target.time) return false;
|
|
18026
18459
|
const eps = Math.max(1e-9, Math.abs(target.close) * 1e-6);
|
|
18027
|
-
const
|
|
18028
|
-
const
|
|
18029
|
-
const
|
|
18460
|
+
const tau = this.animLiveBarMs;
|
|
18461
|
+
const nh = easeToward(this.liveEaseHigh, target.high, dtMs, tau);
|
|
18462
|
+
const nl = easeToward(this.liveEaseLow, target.low, dtMs, tau);
|
|
18463
|
+
const nc = easeToward(this.liveEaseClose, target.close, dtMs, tau);
|
|
18030
18464
|
const active = Math.abs(nh - target.high) > eps || Math.abs(nl - target.low) > eps || Math.abs(nc - target.close) > eps;
|
|
18031
18465
|
this.liveEaseHigh = active ? nh : target.high;
|
|
18032
18466
|
this.liveEaseLow = active ? nl : target.low;
|
|
@@ -21462,7 +21896,12 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
21462
21896
|
if (!model) continue;
|
|
21463
21897
|
const paneId = model.paneId ?? "price";
|
|
21464
21898
|
if (!byPane.has(paneId)) byPane.set(paneId, []);
|
|
21465
|
-
byPane.get(paneId).push({
|
|
21899
|
+
byPane.get(paneId).push({
|
|
21900
|
+
id: r.id,
|
|
21901
|
+
title: model.title || r.title,
|
|
21902
|
+
...model.shorttitle ? { shorttitle: model.shorttitle } : {},
|
|
21903
|
+
ownScale: model.ownScale === true
|
|
21904
|
+
});
|
|
21466
21905
|
}
|
|
21467
21906
|
const panes = [];
|
|
21468
21907
|
for (const paneId of this.paneOrder) {
|
|
@@ -22032,6 +22471,10 @@ function str3(inputs, key, fallback) {
|
|
|
22032
22471
|
const v = inputs[key];
|
|
22033
22472
|
return typeof v === "string" && v !== "" ? v : fallback;
|
|
22034
22473
|
}
|
|
22474
|
+
function bool(inputs, key, fallback) {
|
|
22475
|
+
const v = inputs[key];
|
|
22476
|
+
return typeof v === "boolean" ? v : fallback;
|
|
22477
|
+
}
|
|
22035
22478
|
function toPoints(bars, values, colors) {
|
|
22036
22479
|
const pts = new Array(bars.length);
|
|
22037
22480
|
for (let i = 0; i < bars.length; i++) {
|
|
@@ -23563,10 +24006,6 @@ var aroon = {
|
|
|
23563
24006
|
plots: [
|
|
23564
24007
|
{ key: "up", title: "Aroon up", values: up, color: BULLISH, width: 2 },
|
|
23565
24008
|
{ key: "down", title: "Aroon down", values: down, color: BEARISH, width: 2 }
|
|
23566
|
-
],
|
|
23567
|
-
levels: [
|
|
23568
|
-
{ key: "upper", price: 100, color: NEUTRAL, lineStyle: "dotted" },
|
|
23569
|
-
{ key: "lower", price: 0, color: NEUTRAL, lineStyle: "dotted" }
|
|
23570
24009
|
]
|
|
23571
24010
|
};
|
|
23572
24011
|
}
|
|
@@ -24064,8 +24503,10 @@ var volumeSpecs = [
|
|
|
24064
24503
|
var DAY_MS2 = 864e5;
|
|
24065
24504
|
function periodKey(anchor, time) {
|
|
24066
24505
|
if (anchor === "Week") return Math.floor((Math.floor(time / DAY_MS2) + 3) / 7);
|
|
24067
|
-
if (anchor === "Month") {
|
|
24506
|
+
if (anchor === "Month" || anchor === "Quarter" || anchor === "Year") {
|
|
24068
24507
|
const d = new Date(time);
|
|
24508
|
+
if (anchor === "Year") return d.getUTCFullYear();
|
|
24509
|
+
if (anchor === "Quarter") return d.getUTCFullYear() * 4 + Math.floor(d.getUTCMonth() / 3);
|
|
24069
24510
|
return d.getUTCFullYear() * 12 + d.getUTCMonth();
|
|
24070
24511
|
}
|
|
24071
24512
|
return Math.floor(time / DAY_MS2);
|
|
@@ -24117,40 +24558,86 @@ var parabolicSar = {
|
|
|
24117
24558
|
return { plots: [{ key: "sar", title: "PSAR", values, kind: "circles", color: SERIES_LINE, colors }] };
|
|
24118
24559
|
}
|
|
24119
24560
|
};
|
|
24561
|
+
var VWAP_BANDS_GROUP = "Bands";
|
|
24562
|
+
var VWAP_STYLE = "Style";
|
|
24563
|
+
var VWAP_BANDS = [
|
|
24564
|
+
{ on: "band1", mult: "band1Mult", color: "band1Color", fill: "band1Fill", fillColor: "band1FillColor", defMult: 1, defOn: true, defColor: BULLISH },
|
|
24565
|
+
{ on: "band2", mult: "band2Mult", color: "band2Color", fill: "band2Fill", fillColor: "band2FillColor", defMult: 2, defOn: false, defColor: WARNING },
|
|
24566
|
+
{ on: "band3", mult: "band3Mult", color: "band3Color", fill: "band3Fill", fillColor: "band3FillColor", defMult: 3, defOn: false, defColor: CATEGORICAL[4] }
|
|
24567
|
+
];
|
|
24120
24568
|
var vwap = {
|
|
24121
24569
|
type: "vwap",
|
|
24122
24570
|
title: "Volume Weighted Average Price",
|
|
24123
24571
|
shortTitle: "VWAP",
|
|
24124
24572
|
overlay: true,
|
|
24125
24573
|
inputs: [
|
|
24126
|
-
{ key: "anchor", title: "
|
|
24127
|
-
|
|
24128
|
-
|
|
24574
|
+
{ key: "anchor", title: "Period", type: "string", defval: "Day", options: ["Day", "Week", "Month", "Quarter", "Year"], tooltip: "Where the accumulation resets (UTC periods)" },
|
|
24575
|
+
sourceInput("HLC3"),
|
|
24576
|
+
// Each band is one row: its toggle leads, the multiplier follows unlabeled.
|
|
24577
|
+
...VWAP_BANDS.flatMap((b, i) => [
|
|
24578
|
+
{ key: b.on, title: `Band ${i + 1} multiplier`, type: "bool", defval: b.defOn, inline: b.on, group: VWAP_BANDS_GROUP },
|
|
24579
|
+
{ key: b.mult, title: "", type: "float", defval: b.defMult, min: 0.1, max: 10, step: 0.1, inline: b.on, group: VWAP_BANDS_GROUP }
|
|
24580
|
+
]),
|
|
24581
|
+
{ ...colorInput(INFO, "VWAP color"), group: VWAP_STYLE },
|
|
24582
|
+
// One row per band: its ink, then the fill toggle with the fill's own (translucent) color.
|
|
24583
|
+
...VWAP_BANDS.flatMap((b, i) => [
|
|
24584
|
+
{ ...colorInput(b.defColor, `Band ${i + 1} color`, b.color), inline: b.color, group: VWAP_STYLE },
|
|
24585
|
+
{ key: b.fill, title: "Fill", type: "bool", defval: true, inline: b.color, group: VWAP_STYLE },
|
|
24586
|
+
{ ...colorInput(withAlpha2(b.defColor), "", b.fillColor), inline: b.color, group: VWAP_STYLE }
|
|
24587
|
+
])
|
|
24129
24588
|
],
|
|
24130
24589
|
compute: (bars, inputs) => {
|
|
24131
24590
|
const anchor = str3(inputs, "anchor", "Day");
|
|
24132
|
-
const
|
|
24133
|
-
const
|
|
24591
|
+
const src = sourceValues(bars, str3(inputs, "source", "HLC3"));
|
|
24592
|
+
const n = bars.length;
|
|
24593
|
+
const values = new Array(n).fill(Number.NaN);
|
|
24594
|
+
const bands = VWAP_BANDS.filter((b) => bool(inputs, b.on, b.defOn)).map((b) => ({
|
|
24595
|
+
mult: Math.max(0, num3(inputs, b.mult, b.defMult)),
|
|
24596
|
+
color: str3(inputs, b.color, b.defColor),
|
|
24597
|
+
fill: bool(inputs, b.fill, true),
|
|
24598
|
+
fillColor: str3(inputs, b.fillColor, withAlpha2(b.defColor)),
|
|
24599
|
+
up: new Array(n).fill(Number.NaN),
|
|
24600
|
+
down: new Array(n).fill(Number.NaN)
|
|
24601
|
+
}));
|
|
24134
24602
|
let period = Number.NaN;
|
|
24135
24603
|
let cumPV = 0;
|
|
24604
|
+
let cumPV2 = 0;
|
|
24136
24605
|
let cumV = 0;
|
|
24137
|
-
for (let i = 0; i <
|
|
24606
|
+
for (let i = 0; i < n; i++) {
|
|
24138
24607
|
const b = bars[i];
|
|
24139
24608
|
const key = periodKey(anchor, b.time);
|
|
24140
24609
|
if (key !== period) {
|
|
24141
24610
|
period = key;
|
|
24142
24611
|
cumPV = 0;
|
|
24612
|
+
cumPV2 = 0;
|
|
24143
24613
|
cumV = 0;
|
|
24144
24614
|
}
|
|
24145
24615
|
const v = b.volume;
|
|
24146
24616
|
if (v != null && Number.isFinite(v) && v > 0) {
|
|
24147
|
-
const tp =
|
|
24617
|
+
const tp = src[i];
|
|
24148
24618
|
cumPV += tp * v;
|
|
24619
|
+
cumPV2 += tp * tp * v;
|
|
24149
24620
|
cumV += v;
|
|
24150
24621
|
}
|
|
24151
|
-
if (cumV
|
|
24152
|
-
|
|
24153
|
-
|
|
24622
|
+
if (cumV <= 0) continue;
|
|
24623
|
+
const mean = cumPV / cumV;
|
|
24624
|
+
const sd = Math.sqrt(Math.max(0, cumPV2 / cumV - mean * mean));
|
|
24625
|
+
values[i] = mean;
|
|
24626
|
+
for (const band of bands) {
|
|
24627
|
+
band.up[i] = mean + band.mult * sd;
|
|
24628
|
+
band.down[i] = mean - band.mult * sd;
|
|
24629
|
+
}
|
|
24630
|
+
}
|
|
24631
|
+
const plots = [{ key: "vwap", title: "VWAP", values, color: str3(inputs, "color", INFO), width: 2 }];
|
|
24632
|
+
const fills = [];
|
|
24633
|
+
bands.forEach((band, i) => {
|
|
24634
|
+
plots.push(
|
|
24635
|
+
{ key: `up${i}`, title: `Upper ${band.mult}\u03C3`, values: band.up, color: band.color },
|
|
24636
|
+
{ key: `down${i}`, title: `Lower ${band.mult}\u03C3`, values: band.down, color: band.color }
|
|
24637
|
+
);
|
|
24638
|
+
if (band.fill) fills.push({ key: `band${i}`, from: `up${i}`, to: `down${i}`, color: band.fillColor });
|
|
24639
|
+
});
|
|
24640
|
+
return { plots, bands: fills };
|
|
24154
24641
|
}
|
|
24155
24642
|
};
|
|
24156
24643
|
var pivotPoints = {
|
|
@@ -24341,21 +24828,11 @@ var Vela = class {
|
|
|
24341
24828
|
registerClassicIndicators();
|
|
24342
24829
|
const element = resolveElement(container);
|
|
24343
24830
|
const theme = resolveTheme(options.theme);
|
|
24344
|
-
let animZoom = true;
|
|
24345
|
-
let animPan = true;
|
|
24346
|
-
if (typeof options.animations === "boolean") {
|
|
24347
|
-
animZoom = options.animations;
|
|
24348
|
-
animPan = options.animations;
|
|
24349
|
-
} else if (options.animations) {
|
|
24350
|
-
animZoom = options.animations.zoom ?? true;
|
|
24351
|
-
animPan = options.animations.pan ?? true;
|
|
24352
|
-
}
|
|
24353
24831
|
const display = {
|
|
24354
24832
|
currentPriceLine: options.currentPriceLine ?? true,
|
|
24355
24833
|
logScale: options.logScale ?? false,
|
|
24356
24834
|
nativeBackend: options.nativeBackend ?? "auto",
|
|
24357
|
-
|
|
24358
|
-
animPan,
|
|
24835
|
+
...resolveAnimations(options.animations),
|
|
24359
24836
|
glow: options.glow ?? 0,
|
|
24360
24837
|
upColor: options.upColor ?? BULLISH,
|
|
24361
24838
|
downColor: options.downColor ?? BEARISH,
|
|
@@ -24670,4 +25147,4 @@ function resolveElement(container) {
|
|
|
24670
25147
|
return element;
|
|
24671
25148
|
}
|
|
24672
25149
|
|
|
24673
|
-
export { BUILTIN_PRICE_STYLES, BarStore, CachingDataFeed, DARK_THEME, DataControl, DrawingStore, DrawingToolbar, DrawingsControl, LEGEND_AT_TOP_ATTR, LIGHT_THEME, MultiProviderFeed, NativeRenderer, RendererControl, TIMEZONES, TypedEventBus, Vela, applyAttributionMarkTheme, buildToolbar, createAttributionMark, createCustomMark, defaultMemberOf, defaultToolbar, groupKeyOf, groupMembers, isGroupRow, normalizeTimezone, parseSymbol, priceStyleIds, registerBuiltinChartTypes, resolveTheme, sharedBarStore, timeframeToMs, tzButtonLabel, tzMenuLabel, tzOffset };
|
|
25150
|
+
export { BUILTIN_PRICE_STYLES, BarStore, CachingDataFeed, DARK_THEME, DataControl, DrawingStore, DrawingToolbar, DrawingsControl, LEGEND_AT_TOP_ATTR, LIGHT_THEME, MultiProviderFeed, NativeRenderer, RendererControl, TIMEZONES, TypedEventBus, Vela, applyAttributionMarkTheme, buildToolbar, createAttributionMark, createCustomMark, defaultMemberOf, defaultToolbar, groupKeyOf, groupMembers, isGroupRow, normalizeSession, normalizeTimezone, parseSymbol, priceStyleIds, registerBuiltinChartTypes, resolveTheme, sharedBarStore, timeframeToMs, tzButtonLabel, tzMenuLabel, tzOffset };
|