@luxalgo/vela 0.6.21 → 0.6.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{DataProvider-Cb-2lC5O.d.ts → DataProvider-Bu9E5Zh5.d.ts} +1 -1
- package/dist/{DataProvider-UtWw2CgJ.d.cts → DataProvider-Ck2qyS_9.d.cts} +1 -1
- package/dist/{chunk-BRPFYKTT.js → chunk-A347YL2P.js} +30 -7
- package/dist/{chunk-TRZQQUTR.js → chunk-A4G64KVF.js} +1442 -126
- package/dist/{chunk-T5Z5YUCF.js → chunk-CZRNTHD6.js} +24 -2
- package/dist/{chunk-XCBJU674.js → chunk-UHXG5J7C.js} +1 -1
- package/dist/{contributions-D8HdlKmp.d.cts → contributions-BjQ7hyIx.d.ts} +108 -3
- package/dist/{contributions-FqIWhN4p.d.ts → contributions-Cv2Mutvn.d.cts} +108 -3
- package/dist/index.cjs +1465 -125
- package/dist/index.d.cts +76 -20
- package/dist/index.d.ts +76 -20
- package/dist/index.js +2 -2
- package/dist/{options-BCRmYALw.d.cts → options-ex2_gtKp.d.cts} +200 -12
- package/dist/{options-BCRmYALw.d.ts → options-ex2_gtKp.d.ts} +200 -12
- package/dist/{plugin-B4mnNAeh.d.ts → plugin-CiyhU7pi.d.ts} +3 -3
- package/dist/{plugin-Ccupy_BV.d.cts → plugin-absNH7Yn.d.cts} +3 -3
- package/dist/plugin.d.cts +4 -4
- package/dist/plugin.d.ts +4 -4
- package/dist/providers/binance.d.cts +2 -2
- package/dist/providers/binance.d.ts +2 -2
- package/dist/providers/coinbase.d.cts +2 -2
- package/dist/providers/coinbase.d.ts +2 -2
- package/dist/providers/hyperliquid.d.cts +2 -2
- package/dist/providers/hyperliquid.d.ts +2 -2
- package/dist/{statusline-model-CCP1zm_Z.d.ts → statusline-model-BXoU4Kua.d.ts} +8 -4
- package/dist/{statusline-model-E7CMw5pQ.d.cts → statusline-model-cRhPBLPO.d.cts} +8 -4
- package/dist/ui.cjs +24 -2
- package/dist/ui.d.cts +9 -3
- package/dist/ui.d.ts +9 -3
- package/dist/ui.js +2 -2
- package/dist/vela.global.js +1465 -125
- package/dist/vela.global.min.js +77 -53
- package/dist/widget.cjs +1490 -129
- package/dist/widget.d.cts +6 -6
- package/dist/widget.d.ts +6 -6
- package/dist/widget.js +5 -5
- package/dist/workspace.cjs +1490 -129
- package/dist/workspace.d.cts +8 -5
- package/dist/workspace.d.ts +8 -5
- package/dist/workspace.js +4 -4
- package/package.json +1 -1
package/dist/widget.cjs
CHANGED
|
@@ -1908,7 +1908,7 @@ function intersectRects(a, b) {
|
|
|
1908
1908
|
return { left, top, right, bottom, width: right - left, height: bottom - top };
|
|
1909
1909
|
}
|
|
1910
1910
|
function placePopover(a) {
|
|
1911
|
-
let left = a.align === "end" ? a.trigger.right - a.pop.width : a.trigger.left;
|
|
1911
|
+
let left = a.align === "end" ? a.trigger.right - a.pop.width : a.align === "center" ? a.trigger.left + a.trigger.width / 2 - a.pop.width / 2 : a.trigger.left;
|
|
1912
1912
|
const below = a.trigger.bottom + a.gap;
|
|
1913
1913
|
const above = a.trigger.top - a.pop.height - a.gap;
|
|
1914
1914
|
const fitsBelow = below + a.pop.height <= a.clamp.bottom;
|
|
@@ -1980,6 +1980,8 @@ var Popover = class {
|
|
|
1980
1980
|
this.onKey = null;
|
|
1981
1981
|
this.onReflow = null;
|
|
1982
1982
|
this.shown = false;
|
|
1983
|
+
/** The pending removal of a fading-out shell; a show() that reuses the shell cancels it. */
|
|
1984
|
+
this.leaveTimer = null;
|
|
1983
1985
|
const doc = opts.trigger.ownerDocument;
|
|
1984
1986
|
injectStyles(POPOVER_STYLE_ID, POPOVER_CSS, doc);
|
|
1985
1987
|
this.trigger = opts.trigger;
|
|
@@ -1987,6 +1989,7 @@ var Popover = class {
|
|
|
1987
1989
|
this.ctrl = popoverController(opts);
|
|
1988
1990
|
this.boundary = opts.boundary ?? "viewport";
|
|
1989
1991
|
this.theme = opts.theme;
|
|
1992
|
+
this.fadeMs = Math.max(0, opts.fadeMs ?? 0);
|
|
1990
1993
|
this.el = doc.createElement("div");
|
|
1991
1994
|
this.el.className = "vela-popover vela-ui-layer" + (opts.className ? ` ${opts.className}` : "");
|
|
1992
1995
|
this.el.dataset.position = this.ctrl.position;
|
|
@@ -2010,11 +2013,21 @@ var Popover = class {
|
|
|
2010
2013
|
return;
|
|
2011
2014
|
}
|
|
2012
2015
|
if (open && open !== this) open.hide();
|
|
2016
|
+
if (this.leaveTimer !== null) {
|
|
2017
|
+
clearTimeout(this.leaveTimer);
|
|
2018
|
+
this.leaveTimer = null;
|
|
2019
|
+
}
|
|
2013
2020
|
ensureUIHost(this.el, this.theme);
|
|
2021
|
+
if (this.fadeMs > 0) {
|
|
2022
|
+
this.el.style.transition = `opacity ${this.fadeMs}ms ease`;
|
|
2023
|
+
this.el.style.opacity = "0";
|
|
2024
|
+
this.el.style.pointerEvents = "";
|
|
2025
|
+
}
|
|
2014
2026
|
this.host.appendChild(this.el);
|
|
2015
2027
|
this.shown = true;
|
|
2016
2028
|
open = this;
|
|
2017
2029
|
this.place();
|
|
2030
|
+
if (this.fadeMs > 0) this.el.style.opacity = "1";
|
|
2018
2031
|
const onOutside = (ev) => {
|
|
2019
2032
|
const t = ev.target;
|
|
2020
2033
|
if (this.el.contains(t) || this.trigger.contains(t)) return;
|
|
@@ -2047,7 +2060,16 @@ var Popover = class {
|
|
|
2047
2060
|
this.onOutside = null;
|
|
2048
2061
|
this.onKey = null;
|
|
2049
2062
|
this.onReflow = null;
|
|
2050
|
-
this.
|
|
2063
|
+
if (this.fadeMs > 0) {
|
|
2064
|
+
this.el.style.opacity = "0";
|
|
2065
|
+
this.el.style.pointerEvents = "none";
|
|
2066
|
+
this.leaveTimer = setTimeout(() => {
|
|
2067
|
+
this.leaveTimer = null;
|
|
2068
|
+
this.el.remove();
|
|
2069
|
+
}, this.fadeMs);
|
|
2070
|
+
} else {
|
|
2071
|
+
this.el.remove();
|
|
2072
|
+
}
|
|
2051
2073
|
this.shown = false;
|
|
2052
2074
|
if (open === this) open = null;
|
|
2053
2075
|
this.ctrl.onClose?.();
|
|
@@ -5200,9 +5222,12 @@ function mergeConfig(base, patch) {
|
|
|
5200
5222
|
const gh = asObject(grid.horzLines);
|
|
5201
5223
|
const cross = asObject(p.crosshair);
|
|
5202
5224
|
const ps = asObject(p.priceScale);
|
|
5225
|
+
const anim = asObject(p.animations);
|
|
5203
5226
|
const panes = asObject(p.panes);
|
|
5204
5227
|
const trades = asObject(p.trades);
|
|
5205
5228
|
const ts = asObject(p.timeScale);
|
|
5229
|
+
const marks = asObject(p.marks);
|
|
5230
|
+
const markGroups = asObject(marks.groups);
|
|
5206
5231
|
const candles = asObject(p.candles);
|
|
5207
5232
|
const bars = asObject(p.bars);
|
|
5208
5233
|
const line = asObject(p.line);
|
|
@@ -5249,6 +5274,12 @@ function mergeConfig(base, patch) {
|
|
|
5249
5274
|
countdown: isBool(ps.countdown) ? ps.countdown : base.priceScale.countdown,
|
|
5250
5275
|
animateLastPrice: isBool(ps.animateLastPrice) ? ps.animateLastPrice : base.priceScale.animateLastPrice
|
|
5251
5276
|
},
|
|
5277
|
+
animations: {
|
|
5278
|
+
zoom: isBool(anim.zoom) ? anim.zoom : base.animations.zoom,
|
|
5279
|
+
pan: isBool(anim.pan) ? anim.pan : base.animations.pan,
|
|
5280
|
+
autoscale: isBool(anim.autoscale) ? anim.autoscale : base.animations.autoscale,
|
|
5281
|
+
intro: isBool(anim.intro) ? anim.intro : base.animations.intro
|
|
5282
|
+
},
|
|
5252
5283
|
panes: {
|
|
5253
5284
|
separatorColor: isColor(panes.separatorColor) ? panes.separatorColor : base.panes.separatorColor
|
|
5254
5285
|
},
|
|
@@ -5263,6 +5294,15 @@ function mergeConfig(base, patch) {
|
|
|
5263
5294
|
timeScale: {
|
|
5264
5295
|
timezone: typeof ts.timezone === "string" && ts.timezone ? ts.timezone : base.timeScale.timezone
|
|
5265
5296
|
},
|
|
5297
|
+
marks: {
|
|
5298
|
+
visible: isBool(marks.visible) ? marks.visible : base.marks.visible,
|
|
5299
|
+
// Additive like `stacking.series`: a patch names only the groups it carries, so a
|
|
5300
|
+
// choice stored for a group the host has not registered yet survives verbatim.
|
|
5301
|
+
groups: {
|
|
5302
|
+
...base.marks.groups,
|
|
5303
|
+
...Object.fromEntries(Object.entries(markGroups).filter(([, v]) => isBool(v)))
|
|
5304
|
+
}
|
|
5305
|
+
},
|
|
5266
5306
|
candles: {
|
|
5267
5307
|
upColor: isColor(candles.upColor) ? candles.upColor : base.candles.upColor,
|
|
5268
5308
|
downColor: isColor(candles.downColor) ? candles.downColor : base.candles.downColor,
|
|
@@ -5975,6 +6015,40 @@ var Topbar = class {
|
|
|
5975
6015
|
}
|
|
5976
6016
|
};
|
|
5977
6017
|
|
|
6018
|
+
// src/core/util/wall-clock.ts
|
|
6019
|
+
var BOUNDARY_SLACK_MS = 5;
|
|
6020
|
+
var SecondClock = class _SecondClock {
|
|
6021
|
+
constructor(now = () => Date.now()) {
|
|
6022
|
+
this.now = now;
|
|
6023
|
+
this.subs = /* @__PURE__ */ new Set();
|
|
6024
|
+
this.timer = null;
|
|
6025
|
+
}
|
|
6026
|
+
onTick(cb) {
|
|
6027
|
+
this.subs.add(cb);
|
|
6028
|
+
if (this.timer == null) this.arm();
|
|
6029
|
+
return () => {
|
|
6030
|
+
this.subs.delete(cb);
|
|
6031
|
+
if (this.subs.size === 0 && this.timer != null) {
|
|
6032
|
+
clearTimeout(this.timer);
|
|
6033
|
+
this.timer = null;
|
|
6034
|
+
}
|
|
6035
|
+
};
|
|
6036
|
+
}
|
|
6037
|
+
/** Milliseconds from `now` to just past the next second boundary. */
|
|
6038
|
+
static delayToNextSecond(now) {
|
|
6039
|
+
const intoSecond = (now % 1e3 + 1e3) % 1e3;
|
|
6040
|
+
return 1e3 - intoSecond + BOUNDARY_SLACK_MS;
|
|
6041
|
+
}
|
|
6042
|
+
arm() {
|
|
6043
|
+
this.timer = setTimeout(() => {
|
|
6044
|
+
this.timer = null;
|
|
6045
|
+
const now = this.now();
|
|
6046
|
+
for (const cb of this.subs) cb(now);
|
|
6047
|
+
if (this.subs.size > 0) this.arm();
|
|
6048
|
+
}, _SecondClock.delayToNextSecond(this.now()));
|
|
6049
|
+
}
|
|
6050
|
+
};
|
|
6051
|
+
|
|
5978
6052
|
// src/core/timezones.ts
|
|
5979
6053
|
var TIMEZONES = [
|
|
5980
6054
|
{ value: "Etc/UTC", label: "UTC" },
|
|
@@ -6154,7 +6228,6 @@ var Bottombar = class {
|
|
|
6154
6228
|
this.rangeButtons = /* @__PURE__ */ new Map();
|
|
6155
6229
|
this.sessionButtons = /* @__PURE__ */ new Map();
|
|
6156
6230
|
this.sessionEl = null;
|
|
6157
|
-
this.timer = null;
|
|
6158
6231
|
this.timezone = opts.timezone;
|
|
6159
6232
|
const doc = host.ownerDocument;
|
|
6160
6233
|
injectStyles(STYLE_ID3, CSS4, doc);
|
|
@@ -6219,7 +6292,7 @@ var Bottombar = class {
|
|
|
6219
6292
|
}
|
|
6220
6293
|
});
|
|
6221
6294
|
this.tick();
|
|
6222
|
-
this.
|
|
6295
|
+
this.unsubClock = (opts.clock ?? new SecondClock()).onTick(() => this.tick());
|
|
6223
6296
|
}
|
|
6224
6297
|
setTimezone(zone) {
|
|
6225
6298
|
this.timezone = zone;
|
|
@@ -6248,7 +6321,7 @@ var Bottombar = class {
|
|
|
6248
6321
|
}
|
|
6249
6322
|
}
|
|
6250
6323
|
destroy() {
|
|
6251
|
-
|
|
6324
|
+
this.unsubClock();
|
|
6252
6325
|
this.tzMenu.destroy();
|
|
6253
6326
|
this.settingsTip?.destroy();
|
|
6254
6327
|
this.el.remove();
|
|
@@ -14975,6 +15048,11 @@ function followStep(cur, target, follow = FOLLOW, epsFrac = 15e-4) {
|
|
|
14975
15048
|
const done = Math.abs(target.from - nf) <= eps && Math.abs(target.to - nt) <= eps;
|
|
14976
15049
|
return { cur: done ? { ...target } : { from: nf, to: nt }, done };
|
|
14977
15050
|
}
|
|
15051
|
+
function zoomAnimated(chart) {
|
|
15052
|
+
const control = chart.renderer;
|
|
15053
|
+
if (!control || !control.supports("animZoom")) return true;
|
|
15054
|
+
return Boolean(control.get("animZoom"));
|
|
15055
|
+
}
|
|
14978
15056
|
var Glider = class {
|
|
14979
15057
|
constructor(chart) {
|
|
14980
15058
|
this.chart = chart;
|
|
@@ -15002,8 +15080,14 @@ var Glider = class {
|
|
|
15002
15080
|
const chart = this.chart();
|
|
15003
15081
|
const base = this.target ?? chart?.getVisibleRange();
|
|
15004
15082
|
if (!chart || !base) return;
|
|
15083
|
+
const target = make(base);
|
|
15084
|
+
if (!zoomAnimated(chart)) {
|
|
15085
|
+
this.stop();
|
|
15086
|
+
chart.setVisibleRange(target);
|
|
15087
|
+
return;
|
|
15088
|
+
}
|
|
15005
15089
|
this.cur ?? (this.cur = { ...base });
|
|
15006
|
-
this.target =
|
|
15090
|
+
this.target = target;
|
|
15007
15091
|
if (!this.raf) this.tick();
|
|
15008
15092
|
}
|
|
15009
15093
|
tick() {
|
|
@@ -15123,6 +15207,7 @@ var CSS13 = `
|
|
|
15123
15207
|
font-weight: 600;
|
|
15124
15208
|
-webkit-tap-highlight-color: transparent;
|
|
15125
15209
|
}
|
|
15210
|
+
.vela-mb-item[hidden] { display: none !important; }
|
|
15126
15211
|
.vela-mb-item:active { background: var(--vela-hover); }
|
|
15127
15212
|
.vela-mb-item .vela-icon { font-size: 18px; width: 18px; height: 18px; }
|
|
15128
15213
|
/* A lit stop (the maximize toggle while something is isolated): the inverse
|
|
@@ -15212,6 +15297,12 @@ var MobileBar = class {
|
|
|
15212
15297
|
this.maxEl.setAttribute("aria-label", on ? "Restore layout" : "Maximize chart");
|
|
15213
15298
|
this.maxEl.replaceChildren(iconEl(on ? "restore" : "maximize", this.el.ownerDocument));
|
|
15214
15299
|
}
|
|
15300
|
+
/** Show the maximize stop only while the grid holds more than one chart — on a
|
|
15301
|
+
* one-cell layout there is nothing to isolate, so the stop would be a dead press. */
|
|
15302
|
+
setMaximizeVisible(visible) {
|
|
15303
|
+
if (!this.maxEl) return;
|
|
15304
|
+
this.maxEl.hidden = !visible;
|
|
15305
|
+
}
|
|
15215
15306
|
destroy() {
|
|
15216
15307
|
this.el.remove();
|
|
15217
15308
|
}
|
|
@@ -18272,19 +18363,48 @@ function localStorageAdapter(storageKey) {
|
|
|
18272
18363
|
|
|
18273
18364
|
// src/core/options.ts
|
|
18274
18365
|
var normalizeSession = (v) => v === "regular" || v === "extended" ? v : void 0;
|
|
18366
|
+
var ZOOM_EASE_DEFAULT_MS = 70;
|
|
18367
|
+
var PAN_INERTIA_DEFAULT_MS = 110;
|
|
18368
|
+
var SCROLL_EASE_DEFAULT_MS = 130;
|
|
18369
|
+
var AUTOSCALE_EASE_DEFAULT_MS = 80;
|
|
18275
18370
|
var LIVE_BAR_EASE_DEFAULT_MS = 90;
|
|
18276
|
-
var
|
|
18277
|
-
|
|
18278
|
-
|
|
18371
|
+
var INTRO_DURATION_DEFAULT_MS = 650;
|
|
18372
|
+
var ANIMATION_EASE_MAX_MS = 1e3;
|
|
18373
|
+
var LIVE_BAR_EASE_MAX_MS = ANIMATION_EASE_MAX_MS;
|
|
18374
|
+
var INTRO_DURATION_MAX_MS = 5e3;
|
|
18375
|
+
function resolveEaseMs(value, defaultMs, maxMs = ANIMATION_EASE_MAX_MS) {
|
|
18376
|
+
if (value === true) return defaultMs;
|
|
18279
18377
|
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return 0;
|
|
18280
|
-
return Math.min(value,
|
|
18378
|
+
return Math.min(value, maxMs);
|
|
18379
|
+
}
|
|
18380
|
+
function resolveLiveBarEaseMs(value) {
|
|
18381
|
+
return resolveEaseMs(value, LIVE_BAR_EASE_DEFAULT_MS, LIVE_BAR_EASE_MAX_MS);
|
|
18382
|
+
}
|
|
18383
|
+
function resolveIntro(value) {
|
|
18384
|
+
const off = { style: false, duration: 0 };
|
|
18385
|
+
if (value === true) return { style: "settle", duration: INTRO_DURATION_DEFAULT_MS };
|
|
18386
|
+
if (value === "settle" || value === "grow") return { style: value, duration: INTRO_DURATION_DEFAULT_MS };
|
|
18387
|
+
if (value && typeof value === "object") {
|
|
18388
|
+
const o = value;
|
|
18389
|
+
const d = o.duration;
|
|
18390
|
+
return {
|
|
18391
|
+
style: o.style === "grow" ? "grow" : "settle",
|
|
18392
|
+
duration: typeof d === "number" && Number.isFinite(d) && d > 0 ? Math.min(d, INTRO_DURATION_MAX_MS) : INTRO_DURATION_DEFAULT_MS
|
|
18393
|
+
};
|
|
18394
|
+
}
|
|
18395
|
+
return off;
|
|
18281
18396
|
}
|
|
18282
18397
|
function resolveAnimations(animations) {
|
|
18283
|
-
if (
|
|
18398
|
+
if (animations === false) return { animZoom: 0, animPan: 0, animScroll: 0, animAutoscale: 0, animLiveBar: 0, animIntro: { style: false, duration: 0 } };
|
|
18399
|
+
const cfg = animations === true || animations == null ? {} : animations;
|
|
18400
|
+
const animPan = resolveEaseMs(cfg.pan ?? true, PAN_INERTIA_DEFAULT_MS);
|
|
18284
18401
|
return {
|
|
18285
|
-
animZoom:
|
|
18286
|
-
animPan
|
|
18287
|
-
|
|
18402
|
+
animZoom: resolveEaseMs(cfg.zoom ?? true, ZOOM_EASE_DEFAULT_MS),
|
|
18403
|
+
animPan,
|
|
18404
|
+
animScroll: cfg.scroll === void 0 ? animPan > 0 ? SCROLL_EASE_DEFAULT_MS : 0 : resolveEaseMs(cfg.scroll, SCROLL_EASE_DEFAULT_MS),
|
|
18405
|
+
animAutoscale: resolveEaseMs(cfg.autoscale ?? true, AUTOSCALE_EASE_DEFAULT_MS),
|
|
18406
|
+
animLiveBar: resolveLiveBarEaseMs(cfg.liveBar),
|
|
18407
|
+
animIntro: resolveIntro(cfg.intro ?? true)
|
|
18288
18408
|
};
|
|
18289
18409
|
}
|
|
18290
18410
|
|
|
@@ -18499,6 +18619,93 @@ function presetToRange(preset, bars) {
|
|
|
18499
18619
|
return { from: Math.max(first, from), to: last };
|
|
18500
18620
|
}
|
|
18501
18621
|
|
|
18622
|
+
// src/core/marks/MarksController.ts
|
|
18623
|
+
var MarksController = class {
|
|
18624
|
+
constructor(renderer, events) {
|
|
18625
|
+
this.renderer = renderer;
|
|
18626
|
+
/** Insertion-ordered — the order a cluster falls back to for equal times. */
|
|
18627
|
+
this.marks = /* @__PURE__ */ new Map();
|
|
18628
|
+
this.groups = /* @__PURE__ */ new Map();
|
|
18629
|
+
this.subs = [];
|
|
18630
|
+
this.enabled = !!renderer.capabilities.timelineMarks && typeof renderer.setTimelineMarks === "function";
|
|
18631
|
+
if (this.enabled && renderer.onMarkClick) this.subs.push(renderer.onMarkClick((e) => events.emit("mark:click", e)));
|
|
18632
|
+
}
|
|
18633
|
+
/** Whether the active renderer paints timeline marks. */
|
|
18634
|
+
get supported() {
|
|
18635
|
+
return this.enabled;
|
|
18636
|
+
}
|
|
18637
|
+
/** Add (or replace, by id) one mark. */
|
|
18638
|
+
add(mark) {
|
|
18639
|
+
this.marks.set(mark.id, validateMark(mark));
|
|
18640
|
+
this.sync();
|
|
18641
|
+
}
|
|
18642
|
+
/** Replace the whole set — a market switch. */
|
|
18643
|
+
set(marks) {
|
|
18644
|
+
this.marks.clear();
|
|
18645
|
+
for (const m of marks) this.marks.set(m.id, validateMark(m));
|
|
18646
|
+
this.sync();
|
|
18647
|
+
}
|
|
18648
|
+
remove(id) {
|
|
18649
|
+
const had = this.marks.delete(id);
|
|
18650
|
+
if (had) this.sync();
|
|
18651
|
+
return had;
|
|
18652
|
+
}
|
|
18653
|
+
clear() {
|
|
18654
|
+
if (this.marks.size === 0) return;
|
|
18655
|
+
this.marks.clear();
|
|
18656
|
+
this.sync();
|
|
18657
|
+
}
|
|
18658
|
+
/** Every mark, in insertion order (shallow copies — mutating one changes nothing). */
|
|
18659
|
+
all() {
|
|
18660
|
+
return [...this.marks.values()].map((m) => ({ ...m, glyph: { ...m.glyph } }));
|
|
18661
|
+
}
|
|
18662
|
+
/** Define (or replace) a group's presentation — its settings label and default visibility. */
|
|
18663
|
+
defineGroup(group) {
|
|
18664
|
+
if (!group || typeof group.id !== "string" || group.id.length === 0) throw new Error("[vela] marks.defineGroup: `id` must be a non-empty string");
|
|
18665
|
+
if (typeof group.label !== "string") throw new Error(`[vela] marks.defineGroup: group "${group.id}" needs a string \`label\``);
|
|
18666
|
+
this.groups.set(group.id, { ...group });
|
|
18667
|
+
this.sync();
|
|
18668
|
+
}
|
|
18669
|
+
/** The defined groups, in definition order. */
|
|
18670
|
+
groupDefinitions() {
|
|
18671
|
+
return [...this.groups.values()].map((g) => ({ ...g }));
|
|
18672
|
+
}
|
|
18673
|
+
/**
|
|
18674
|
+
* Show or hide one group's marks. The choice lives in the renderer's cosmetic config
|
|
18675
|
+
* (the `marks` feature) — what the settings dialog's Events checkboxes edit and what
|
|
18676
|
+
* a persisted chart restores — so it warns + no-ops on a renderer without it.
|
|
18677
|
+
*/
|
|
18678
|
+
setGroupVisible(id, visible) {
|
|
18679
|
+
if (!this.renderer.features.includes("marks")) {
|
|
18680
|
+
console.warn(`[vela] renderer "${this.renderer.name}" does not paint timeline marks \u2014 setGroupVisible ignored.`);
|
|
18681
|
+
return;
|
|
18682
|
+
}
|
|
18683
|
+
this.renderer.applyFeature("marks", { groups: { [id]: visible } });
|
|
18684
|
+
}
|
|
18685
|
+
/** A group's effective visibility: the user's (persisted) choice, else the group's declared default, else visible. */
|
|
18686
|
+
isGroupVisible(id) {
|
|
18687
|
+
const state = this.renderer.readFeature("marks");
|
|
18688
|
+
const chosen = state?.groups?.[id];
|
|
18689
|
+
if (typeof chosen === "boolean") return chosen;
|
|
18690
|
+
return this.groups.get(id)?.visible !== false;
|
|
18691
|
+
}
|
|
18692
|
+
destroy() {
|
|
18693
|
+
for (const unsub of this.subs) unsub();
|
|
18694
|
+
this.subs.length = 0;
|
|
18695
|
+
}
|
|
18696
|
+
sync() {
|
|
18697
|
+
if (!this.enabled) return;
|
|
18698
|
+
this.renderer.setTimelineMarks([...this.marks.values()], [...this.groups.values()]);
|
|
18699
|
+
}
|
|
18700
|
+
};
|
|
18701
|
+
function validateMark(mark) {
|
|
18702
|
+
if (!mark || typeof mark !== "object") throw new Error("[vela] marks: a mark must be an object");
|
|
18703
|
+
if (typeof mark.id !== "string" || mark.id.length === 0) throw new Error("[vela] marks: `id` must be a non-empty string");
|
|
18704
|
+
if (typeof mark.time !== "number" || !Number.isFinite(mark.time)) throw new Error(`[vela] marks: mark "${mark.id}" needs a finite epoch-ms \`time\``);
|
|
18705
|
+
if (!mark.glyph || typeof mark.glyph !== "object" || typeof mark.glyph.color !== "string") throw new Error(`[vela] marks: mark "${mark.id}" needs a glyph with a \`color\``);
|
|
18706
|
+
return { ...mark, glyph: { ...mark.glyph } };
|
|
18707
|
+
}
|
|
18708
|
+
|
|
18502
18709
|
// src/core/engine/DrawingSeriesService.ts
|
|
18503
18710
|
var MAX_BARS = 5e3;
|
|
18504
18711
|
var PAD_FRAC = 0.25;
|
|
@@ -18816,6 +19023,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18816
19023
|
marketKey: () => `${this.config.market.symbol ?? ""}|${this.config.market.session ?? ""}`
|
|
18817
19024
|
});
|
|
18818
19025
|
this.drawings = new DrawingController(this.renderer, this.events, config.drawings, drawingSeries);
|
|
19026
|
+
this.marks = new MarksController(this.renderer, this.events);
|
|
18819
19027
|
this.unresolvedUnsub = this.feed.onUnresolved?.((info) => {
|
|
18820
19028
|
this.endLoad();
|
|
18821
19029
|
this.events.emit("data:unresolved", info);
|
|
@@ -19680,6 +19888,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19680
19888
|
record.session = void 0;
|
|
19681
19889
|
record.native?.instance.suspend();
|
|
19682
19890
|
if (record.renderHandle) this.renderer.setIndicatorVisible?.(record.renderHandle, false);
|
|
19891
|
+
else if (record.native) this.mountHiddenNativeRow(id, record);
|
|
19683
19892
|
} else {
|
|
19684
19893
|
if (record.renderHandle) this.renderer.setIndicatorVisible?.(record.renderHandle, true);
|
|
19685
19894
|
record.pendingStructural = true;
|
|
@@ -19767,6 +19976,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19767
19976
|
this.unresolvedUnsub = null;
|
|
19768
19977
|
this.feed.destroy?.();
|
|
19769
19978
|
this.drawings.destroy();
|
|
19979
|
+
this.marks.destroy();
|
|
19770
19980
|
this.renderer.destroy();
|
|
19771
19981
|
this.events.clear();
|
|
19772
19982
|
}
|
|
@@ -19880,6 +20090,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19880
20090
|
overlay: d.overlay,
|
|
19881
20091
|
paneHint: d.paneHint,
|
|
19882
20092
|
native: { type: record.native.type },
|
|
20093
|
+
...d.legend === false ? { legend: false } : {},
|
|
19883
20094
|
...out.paneAxis != null ? { paneAxis: out.paneAxis } : {},
|
|
19884
20095
|
series: out.series ?? [],
|
|
19885
20096
|
fills: out.fills ?? [],
|
|
@@ -19902,9 +20113,17 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19902
20113
|
* over it in place (`pendingStructural`), clears the spinner, and only THEN fires
|
|
19903
20114
|
* `indicator:added`/`ready` — so event semantics and `inspect()` (which skips
|
|
19904
20115
|
* loading records) still mean "the indicator produced output".
|
|
20116
|
+
*
|
|
20117
|
+
* A HIDDEN record mounts too — dimmed, no spinner (its session never starts while
|
|
20118
|
+
* hidden, so nothing is computing and no model will ever arrive to mount the row
|
|
20119
|
+
* later). Without this an indicator ADDED hidden (a restored ledger/ext entry) had
|
|
20120
|
+
* no legend row at all: invisible AND unreachable — the eye that unhides it never
|
|
20121
|
+
* existed. The hidden mount announces immediately for the same reason: the "first
|
|
20122
|
+
* computed model" that normally announces cannot come until the indicator is shown,
|
|
20123
|
+
* and host UIs (object tree, landing watchers) must know it exists NOW.
|
|
19905
20124
|
*/
|
|
19906
20125
|
mountLoadingPlaceholder(id, record) {
|
|
19907
|
-
if (record.renderHandle ||
|
|
20126
|
+
if (record.renderHandle || !record.prepared) return;
|
|
19908
20127
|
const meta = record.prepared.meta;
|
|
19909
20128
|
const model = {
|
|
19910
20129
|
id,
|
|
@@ -19928,8 +20147,34 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19928
20147
|
this.ensurePaneFor(paneId);
|
|
19929
20148
|
record.renderHandle = this.renderer.mountIndicator(model);
|
|
19930
20149
|
record.pendingStructural = true;
|
|
20150
|
+
if (record.hidden) {
|
|
20151
|
+
this.renderer.setIndicatorVisible?.(record.renderHandle, false);
|
|
20152
|
+
this.announce(record, this.handles.get(id));
|
|
20153
|
+
return;
|
|
20154
|
+
}
|
|
19931
20155
|
this.setLoading(record, true);
|
|
19932
20156
|
}
|
|
20157
|
+
/**
|
|
20158
|
+
* Mount the legend row for a NATIVE indicator that is being hidden BEFORE it ever
|
|
20159
|
+
* started (a restored-hidden ledger entry: `startNativeIndicator` bails on hidden
|
|
20160
|
+
* records, so no model — and therefore no row — would ever mount). The native
|
|
20161
|
+
* counterpart of {@link mountLoadingPlaceholder}'s hidden branch: an empty model
|
|
20162
|
+
* carries the title + inputs schema, the renderer marks the row hidden, and the
|
|
20163
|
+
* announce makes the indicator visible to host UIs. Showing later STARTS the
|
|
20164
|
+
* instance (the `started` flag path) and its first emit remounts over this row.
|
|
20165
|
+
*/
|
|
20166
|
+
mountHiddenNativeRow(id, record) {
|
|
20167
|
+
if (record.renderHandle || !record.native) return;
|
|
20168
|
+
const model = this.buildNativeModel(record, {});
|
|
20169
|
+
const paneId = this.routePane(id, model, record.options ?? {});
|
|
20170
|
+
this.placeModel(model, id, paneId);
|
|
20171
|
+
record.model = model;
|
|
20172
|
+
this.ensurePaneFor(paneId);
|
|
20173
|
+
record.renderHandle = this.renderer.mountIndicator(model);
|
|
20174
|
+
record.pendingStructural = true;
|
|
20175
|
+
this.renderer.setIndicatorVisible?.(record.renderHandle, false);
|
|
20176
|
+
this.announce(record, this.handles.get(id));
|
|
20177
|
+
}
|
|
19933
20178
|
/** Flip the record's loading state and reflect it in the legend row (spinner on/off). */
|
|
19934
20179
|
setLoading(record, loading) {
|
|
19935
20180
|
record.loading = loading;
|
|
@@ -19964,6 +20209,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19964
20209
|
for (const r of this.registry.all()) {
|
|
19965
20210
|
const model = r.model;
|
|
19966
20211
|
if (!model) continue;
|
|
20212
|
+
if (model.legend === false) continue;
|
|
19967
20213
|
const paneId = model.paneId ?? "price";
|
|
19968
20214
|
if (!byPane.has(paneId)) byPane.set(paneId, []);
|
|
19969
20215
|
byPane.get(paneId).push({
|
|
@@ -20539,6 +20785,16 @@ var RendererControl = class {
|
|
|
20539
20785
|
this.renderer.setLayoutMode?.(mode);
|
|
20540
20786
|
return this;
|
|
20541
20787
|
}
|
|
20788
|
+
/**
|
|
20789
|
+
* Drive the renderer's time-of-day chrome (the countdown-to-bar-close chip) from the
|
|
20790
|
+
* host's own second pulse, so it ticks in step with a host clock display instead of
|
|
20791
|
+
* on a separate timer that can read a different second. `null` hands the pulse back
|
|
20792
|
+
* to the renderer. Silent no-op on a renderer without time-of-day chrome.
|
|
20793
|
+
*/
|
|
20794
|
+
setWallClock(clock) {
|
|
20795
|
+
this.renderer.setWallClock?.(clock);
|
|
20796
|
+
return this;
|
|
20797
|
+
}
|
|
20542
20798
|
};
|
|
20543
20799
|
|
|
20544
20800
|
// src/core/PanesControl.ts
|
|
@@ -20878,6 +21134,61 @@ var DrawingsControl = class {
|
|
|
20878
21134
|
}
|
|
20879
21135
|
};
|
|
20880
21136
|
|
|
21137
|
+
// src/core/MarksControl.ts
|
|
21138
|
+
var MarksControl = class {
|
|
21139
|
+
constructor(ctrl) {
|
|
21140
|
+
this.ctrl = ctrl;
|
|
21141
|
+
}
|
|
21142
|
+
/** Whether the active renderer paints timeline marks. */
|
|
21143
|
+
get supported() {
|
|
21144
|
+
return this.ctrl.supported;
|
|
21145
|
+
}
|
|
21146
|
+
/** Add one mark; an existing id is replaced in place. */
|
|
21147
|
+
add(mark) {
|
|
21148
|
+
this.ctrl.add(mark);
|
|
21149
|
+
return this;
|
|
21150
|
+
}
|
|
21151
|
+
/** Replace the whole set (a market switch). */
|
|
21152
|
+
set(marks) {
|
|
21153
|
+
this.ctrl.set(marks);
|
|
21154
|
+
return this;
|
|
21155
|
+
}
|
|
21156
|
+
remove(id) {
|
|
21157
|
+
this.ctrl.remove(id);
|
|
21158
|
+
return this;
|
|
21159
|
+
}
|
|
21160
|
+
clear() {
|
|
21161
|
+
this.ctrl.clear();
|
|
21162
|
+
return this;
|
|
21163
|
+
}
|
|
21164
|
+
/** Every mark, in insertion order. */
|
|
21165
|
+
all() {
|
|
21166
|
+
return this.ctrl.all();
|
|
21167
|
+
}
|
|
21168
|
+
/**
|
|
21169
|
+
* Define a visibility group's presentation: the label of its checkbox in chart
|
|
21170
|
+
* settings (the Events tab) and its default visibility. Marks may name a group
|
|
21171
|
+
* that was never defined — it then shows its capitalized id.
|
|
21172
|
+
*/
|
|
21173
|
+
defineGroup(group) {
|
|
21174
|
+
this.ctrl.defineGroup(group);
|
|
21175
|
+
return this;
|
|
21176
|
+
}
|
|
21177
|
+
/** The defined groups, in definition order. */
|
|
21178
|
+
groups() {
|
|
21179
|
+
return this.ctrl.groupDefinitions();
|
|
21180
|
+
}
|
|
21181
|
+
/** Show or hide one group's marks — the same switch as the settings checkbox, persisted with the chart's config. */
|
|
21182
|
+
setGroupVisible(id, visible = true) {
|
|
21183
|
+
this.ctrl.setGroupVisible(id, visible);
|
|
21184
|
+
return this;
|
|
21185
|
+
}
|
|
21186
|
+
/** A group's effective visibility (the user's choice, else the group's declared default). */
|
|
21187
|
+
isGroupVisible(id) {
|
|
21188
|
+
return this.ctrl.isGroupVisible(id);
|
|
21189
|
+
}
|
|
21190
|
+
};
|
|
21191
|
+
|
|
20881
21192
|
// src/core/model/inputs.ts
|
|
20882
21193
|
function inputVisible(when, values) {
|
|
20883
21194
|
if (!when) return true;
|
|
@@ -22926,6 +23237,8 @@ var NATIVE_CAPABILITIES = {
|
|
|
22926
23237
|
// canvas-painted into the owning indicator's interleave slice
|
|
22927
23238
|
trades: true,
|
|
22928
23239
|
// strategy order-fill markers (arrows + labels + fill-price ticks)
|
|
23240
|
+
timelineMarks: true,
|
|
23241
|
+
// host events on a lane above the time axis (glyphs + detail popup)
|
|
22929
23242
|
inputsUI: true
|
|
22930
23243
|
// reuses the DOM InputsUI
|
|
22931
23244
|
};
|
|
@@ -24493,6 +24806,31 @@ var Animator = class {
|
|
|
24493
24806
|
}
|
|
24494
24807
|
}
|
|
24495
24808
|
};
|
|
24809
|
+
var EaseSetting = class {
|
|
24810
|
+
/** `defaultMs` is what the on/off switch restores when nothing else was configured;
|
|
24811
|
+
* `initialMs` (default: `defaultMs`) is the starting value — 0 for a motion that
|
|
24812
|
+
* ships off. */
|
|
24813
|
+
constructor(defaultMs, initialMs = defaultMs) {
|
|
24814
|
+
this.onMs = defaultMs;
|
|
24815
|
+
this.ms = initialMs;
|
|
24816
|
+
}
|
|
24817
|
+
/** The active time-constant; 0 when off. */
|
|
24818
|
+
get tau() {
|
|
24819
|
+
return this.ms;
|
|
24820
|
+
}
|
|
24821
|
+
get on() {
|
|
24822
|
+
return this.ms > 0;
|
|
24823
|
+
}
|
|
24824
|
+
/** Set the time-constant (0 = off). A non-zero value becomes what `toggle(true)` restores. */
|
|
24825
|
+
set(ms) {
|
|
24826
|
+
this.ms = ms;
|
|
24827
|
+
if (ms > 0) this.onMs = ms;
|
|
24828
|
+
}
|
|
24829
|
+
/** On/off only — the duration stays the last one configured. */
|
|
24830
|
+
toggle(on) {
|
|
24831
|
+
this.ms = on ? this.onMs : 0;
|
|
24832
|
+
}
|
|
24833
|
+
};
|
|
24496
24834
|
function easeToward(current, target, dtMs, tauMs) {
|
|
24497
24835
|
if (tauMs <= 0) return target;
|
|
24498
24836
|
return current + (target - current) * (1 - Math.exp(-dtMs / tauMs));
|
|
@@ -25216,6 +25554,25 @@ function drawTextLines(ctx, lines, x, firstY, step, color) {
|
|
|
25216
25554
|
for (let i = 0; i < lines.length; i += 1) ctx.fillText(lines[i], x, firstY + i * step);
|
|
25217
25555
|
}
|
|
25218
25556
|
|
|
25557
|
+
// src/renderers/shared/marks-state.ts
|
|
25558
|
+
function defaultMarksState() {
|
|
25559
|
+
return { visible: true, groups: {} };
|
|
25560
|
+
}
|
|
25561
|
+
function mergeMarksState(base, patch) {
|
|
25562
|
+
if (typeof patch === "boolean") return { visible: patch, groups: { ...base.groups } };
|
|
25563
|
+
const p = patch && typeof patch === "object" ? patch : {};
|
|
25564
|
+
const g = p.groups && typeof p.groups === "object" ? p.groups : {};
|
|
25565
|
+
const groups = { ...base.groups };
|
|
25566
|
+
for (const [id, v] of Object.entries(g)) if (typeof v === "boolean") groups[id] = v;
|
|
25567
|
+
return { visible: typeof p.visible === "boolean" ? p.visible : base.visible, groups };
|
|
25568
|
+
}
|
|
25569
|
+
function markGroupVisible(state, groupId, groups) {
|
|
25570
|
+
if (groupId === void 0) return true;
|
|
25571
|
+
const chosen = state.groups[groupId];
|
|
25572
|
+
if (typeof chosen === "boolean") return chosen;
|
|
25573
|
+
return groups.find((g) => g.id === groupId)?.visible !== false;
|
|
25574
|
+
}
|
|
25575
|
+
|
|
25219
25576
|
// src/renderers/native/core/SceneGraph.ts
|
|
25220
25577
|
var SceneGraph = class {
|
|
25221
25578
|
constructor() {
|
|
@@ -25279,6 +25636,20 @@ var SceneGraph = class {
|
|
|
25279
25636
|
/** Strategy trade-marker display (the `tradeMarkers` feature): master toggle, the
|
|
25280
25637
|
* two text lines, and the palette. Trade markers always paint on the price pane. */
|
|
25281
25638
|
this.tradeMarkers = defaultTradeMarkersState();
|
|
25639
|
+
/** Timeline-mark display (the `marks` feature): the lane's master toggle + per-group visibility. */
|
|
25640
|
+
this.marks = defaultMarksState();
|
|
25641
|
+
/** The host's timeline marks + group definitions (`setTimelineMarks`), painted on the lane above the time axis. */
|
|
25642
|
+
this.timelineMarks = [];
|
|
25643
|
+
this.markGroups = [];
|
|
25644
|
+
/** The mark stack (bar index) fanned out by hover or tap, if any. */
|
|
25645
|
+
this.marksExpandedStack = null;
|
|
25646
|
+
/** The lane glyph (cluster key) under the pointer — it pulses — and when the hover began (frame-clock ms). */
|
|
25647
|
+
this.marksHoverKey = null;
|
|
25648
|
+
this.marksHoverSince = 0;
|
|
25649
|
+
/** The cluster whose popup is open: its glyph paints filled ("active"). */
|
|
25650
|
+
this.marksActiveKey = null;
|
|
25651
|
+
/** A content-less click's brief filled flash — the cluster key and the frame-clock time it ends. */
|
|
25652
|
+
this.marksFlash = null;
|
|
25282
25653
|
/** Renderer-owned shaded time bands (session highlighting), behind grid + data. */
|
|
25283
25654
|
this.highlights = [];
|
|
25284
25655
|
/** Pre/post-market bands pushed by the host (`sessionZones` feature); null ⇒ no sessions. */
|
|
@@ -27127,6 +27498,315 @@ function timeTicks(fromMs, toMs, target = 8, offsetMs = 0) {
|
|
|
27127
27498
|
return out;
|
|
27128
27499
|
}
|
|
27129
27500
|
|
|
27501
|
+
// src/renderers/native/chrome/countdown.ts
|
|
27502
|
+
function countdownText(barOpen, barMs, now) {
|
|
27503
|
+
if (!(barMs > 0)) return null;
|
|
27504
|
+
const remaining = barOpen + barMs - now;
|
|
27505
|
+
if (remaining <= 0) return null;
|
|
27506
|
+
return formatCountdown(remaining);
|
|
27507
|
+
}
|
|
27508
|
+
function formatCountdown(ms) {
|
|
27509
|
+
const total = Math.max(0, Math.ceil(ms / 1e3));
|
|
27510
|
+
const s = total % 60;
|
|
27511
|
+
const m = Math.floor(total / 60) % 60;
|
|
27512
|
+
const h = Math.floor(total / 3600);
|
|
27513
|
+
const pad = (v) => String(v).padStart(2, "0");
|
|
27514
|
+
return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`;
|
|
27515
|
+
}
|
|
27516
|
+
|
|
27517
|
+
// src/renderers/native/chrome/contrast.ts
|
|
27518
|
+
function tagTextColor(bg, over) {
|
|
27519
|
+
const [r, g, b, a] = parseColor(bg);
|
|
27520
|
+
let R = r;
|
|
27521
|
+
let G = g;
|
|
27522
|
+
let B = b;
|
|
27523
|
+
if (a < 1) {
|
|
27524
|
+
const [or, og, ob] = parseColor(over);
|
|
27525
|
+
R = r * a + or * (1 - a);
|
|
27526
|
+
G = g * a + og * (1 - a);
|
|
27527
|
+
B = b * a + ob * (1 - a);
|
|
27528
|
+
}
|
|
27529
|
+
const lin = (c) => c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
27530
|
+
const L = 0.2126 * lin(R) + 0.7152 * lin(G) + 0.0722 * lin(B);
|
|
27531
|
+
return L >= 0.4 ? "#000000" : "#ffffff";
|
|
27532
|
+
}
|
|
27533
|
+
|
|
27534
|
+
// src/renderers/native/chrome/marks/layout.ts
|
|
27535
|
+
var MARK_GLYPH_PX = 16;
|
|
27536
|
+
var MARK_CLUSTER_PX = 20;
|
|
27537
|
+
var MARK_LANE_INSET = 4;
|
|
27538
|
+
var MARK_DECK_STEP = 3;
|
|
27539
|
+
var MARK_FAN_GAP = 4;
|
|
27540
|
+
var MARK_HIT_PAD = 3;
|
|
27541
|
+
var MARK_FAN_HOLD = 8;
|
|
27542
|
+
function snapMarkBar(time, barTimes, intervalMs2) {
|
|
27543
|
+
const n = barTimes.length;
|
|
27544
|
+
if (n === 0 || !(intervalMs2 > 0) || !Number.isFinite(time)) return null;
|
|
27545
|
+
if (time < barTimes[0]) return null;
|
|
27546
|
+
const last = barTimes[n - 1];
|
|
27547
|
+
if (time >= last + intervalMs2) return n - 1 + Math.floor((time - last) / intervalMs2);
|
|
27548
|
+
let lo = 0;
|
|
27549
|
+
let hi = n - 1;
|
|
27550
|
+
while (lo < hi) {
|
|
27551
|
+
const mid = lo + hi + 1 >> 1;
|
|
27552
|
+
if (barTimes[mid] <= time) lo = mid;
|
|
27553
|
+
else hi = mid - 1;
|
|
27554
|
+
}
|
|
27555
|
+
if (time < barTimes[lo] + intervalMs2) return lo;
|
|
27556
|
+
return lo + 1;
|
|
27557
|
+
}
|
|
27558
|
+
function clusterMarks(marks, barTimes, intervalMs2, hidden) {
|
|
27559
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
27560
|
+
marks.forEach((m, seq) => {
|
|
27561
|
+
if (m.group !== void 0 && hidden(m.group)) return;
|
|
27562
|
+
const bar = snapMarkBar(m.time, barTimes, intervalMs2);
|
|
27563
|
+
if (bar === null) return;
|
|
27564
|
+
const key = `${bar}|${m.group ?? ""}`;
|
|
27565
|
+
let c = byKey.get(key);
|
|
27566
|
+
if (!c) {
|
|
27567
|
+
c = { key, bar, group: m.group, marks: [], seq: [] };
|
|
27568
|
+
byKey.set(key, c);
|
|
27569
|
+
}
|
|
27570
|
+
c.marks.push(m);
|
|
27571
|
+
c.seq.push(seq);
|
|
27572
|
+
});
|
|
27573
|
+
const out = [];
|
|
27574
|
+
for (const c of byKey.values()) {
|
|
27575
|
+
const order = c.marks.map((m, i) => ({ m, seq: c.seq[i] })).sort((a, b) => a.m.time - b.m.time || a.seq - b.seq);
|
|
27576
|
+
out.push({ key: c.key, bar: c.bar, group: c.group, marks: order.map((o) => o.m) });
|
|
27577
|
+
}
|
|
27578
|
+
return out;
|
|
27579
|
+
}
|
|
27580
|
+
function groupRank(groups, clusters) {
|
|
27581
|
+
const rank = /* @__PURE__ */ new Map();
|
|
27582
|
+
groups.forEach((g, i) => rank.set(g.id, i));
|
|
27583
|
+
for (const c of clusters) {
|
|
27584
|
+
if (c.group !== void 0 && !rank.has(c.group)) rank.set(c.group, rank.size);
|
|
27585
|
+
}
|
|
27586
|
+
return (group) => group === void 0 ? Number.MAX_SAFE_INTEGER : rank.get(group) ?? Number.MAX_SAFE_INTEGER - 1;
|
|
27587
|
+
}
|
|
27588
|
+
function layoutMarkLane(input) {
|
|
27589
|
+
const clusters = clusterMarks(input.marks, input.barTimes, input.intervalMs, input.hidden);
|
|
27590
|
+
const rankOf = groupRank(input.groups, clusters);
|
|
27591
|
+
const byBar = /* @__PURE__ */ new Map();
|
|
27592
|
+
for (const c of clusters) {
|
|
27593
|
+
const list = byBar.get(c.bar);
|
|
27594
|
+
if (list) list.push(c);
|
|
27595
|
+
else byBar.set(c.bar, [c]);
|
|
27596
|
+
}
|
|
27597
|
+
const glyphs = [];
|
|
27598
|
+
const stacks = /* @__PURE__ */ new Map();
|
|
27599
|
+
for (const [bar, list] of byBar) {
|
|
27600
|
+
const x = input.xOf(bar);
|
|
27601
|
+
if (!Number.isFinite(x) || x < -MARK_CLUSTER_PX || x > input.dataW + MARK_CLUSTER_PX) continue;
|
|
27602
|
+
list.sort((a, b) => rankOf(a.group) - rankOf(b.group));
|
|
27603
|
+
const multi = list.length > 1;
|
|
27604
|
+
const expanded = multi && input.expanded === bar;
|
|
27605
|
+
const decked = multi && !expanded;
|
|
27606
|
+
const placed = [];
|
|
27607
|
+
const deckSize = list[0].marks.length > 1 ? MARK_CLUSTER_PX : MARK_GLYPH_PX;
|
|
27608
|
+
let bottom = input.axisY - MARK_LANE_INSET;
|
|
27609
|
+
list.forEach((cluster, depth) => {
|
|
27610
|
+
const size = decked ? deckSize : cluster.marks.length > 1 ? MARK_CLUSTER_PX : MARK_GLYPH_PX;
|
|
27611
|
+
let y;
|
|
27612
|
+
if (expanded) {
|
|
27613
|
+
y = bottom - size / 2;
|
|
27614
|
+
bottom -= size + MARK_FAN_GAP;
|
|
27615
|
+
} else {
|
|
27616
|
+
y = input.axisY - MARK_LANE_INSET - size / 2 - depth * MARK_DECK_STEP;
|
|
27617
|
+
}
|
|
27618
|
+
placed.push({ cluster, x, y, size, stack: bar, depth, decked });
|
|
27619
|
+
});
|
|
27620
|
+
for (let i = placed.length - 1; i >= 0; i--) glyphs.push(placed[i]);
|
|
27621
|
+
stacks.set(bar, placed);
|
|
27622
|
+
}
|
|
27623
|
+
return { glyphs, stacks };
|
|
27624
|
+
}
|
|
27625
|
+
function markGlyphAt(layout, x, y) {
|
|
27626
|
+
for (let i = layout.glyphs.length - 1; i >= 0; i--) {
|
|
27627
|
+
const g = layout.glyphs[i];
|
|
27628
|
+
if (g.decked && g.depth !== 0) continue;
|
|
27629
|
+
const r = g.size / 2 + MARK_HIT_PAD;
|
|
27630
|
+
if (Math.abs(x - g.x) <= r && Math.abs(y - g.y) <= r) return g;
|
|
27631
|
+
}
|
|
27632
|
+
return null;
|
|
27633
|
+
}
|
|
27634
|
+
function markStackAt(layout, x, y) {
|
|
27635
|
+
for (const [bar, placed] of layout.stacks) {
|
|
27636
|
+
for (const g of placed) {
|
|
27637
|
+
const r = g.size / 2 + MARK_HIT_PAD;
|
|
27638
|
+
if (Math.abs(x - g.x) <= r && Math.abs(y - g.y) <= r) return bar;
|
|
27639
|
+
}
|
|
27640
|
+
if (placed.length > 1 && !placed[0].decked) {
|
|
27641
|
+
const top = placed[placed.length - 1];
|
|
27642
|
+
const base = placed[0];
|
|
27643
|
+
const r = Math.max(top.size, base.size) / 2 + MARK_HIT_PAD;
|
|
27644
|
+
if (Math.abs(x - base.x) <= r && y >= top.y - top.size / 2 - MARK_FAN_HOLD && y <= base.y + base.size / 2 + MARK_HIT_PAD) return bar;
|
|
27645
|
+
}
|
|
27646
|
+
}
|
|
27647
|
+
return null;
|
|
27648
|
+
}
|
|
27649
|
+
function clusterTooltip(cluster, groups) {
|
|
27650
|
+
const first = cluster.marks[0];
|
|
27651
|
+
if (!first) return null;
|
|
27652
|
+
if (cluster.marks.length === 1) return first.tooltip ?? first.title ?? null;
|
|
27653
|
+
const label = cluster.group !== void 0 ? markGroupLabel(cluster.group, groups) : first.title ?? first.tooltip ?? "Marks";
|
|
27654
|
+
return `${label} \xB7 ${cluster.marks.length}`;
|
|
27655
|
+
}
|
|
27656
|
+
function markGroupLabel(groupId, groups) {
|
|
27657
|
+
const def = groups.find((g) => g.id === groupId);
|
|
27658
|
+
if (def) return def.label;
|
|
27659
|
+
return groupId.charAt(0).toUpperCase() + groupId.slice(1);
|
|
27660
|
+
}
|
|
27661
|
+
function effectiveMarkGroups(marks, groups) {
|
|
27662
|
+
const out = groups.map((g) => ({ ...g }));
|
|
27663
|
+
const seen = new Set(out.map((g) => g.id));
|
|
27664
|
+
for (const m of marks) {
|
|
27665
|
+
if (m.group === void 0 || seen.has(m.group)) continue;
|
|
27666
|
+
seen.add(m.group);
|
|
27667
|
+
out.push({ id: m.group, label: markGroupLabel(m.group, groups) });
|
|
27668
|
+
}
|
|
27669
|
+
return out;
|
|
27670
|
+
}
|
|
27671
|
+
|
|
27672
|
+
// src/renderers/native/chrome/marks/paint.ts
|
|
27673
|
+
var MARK_PULSE_MS = 360;
|
|
27674
|
+
var MARK_PULSE_AMPLITUDE = 0.1;
|
|
27675
|
+
var ACTIVE_INK = "#ffffff";
|
|
27676
|
+
function pulseScale(elapsedMs) {
|
|
27677
|
+
if (!(elapsedMs > 0) || elapsedMs >= MARK_PULSE_MS) return 1;
|
|
27678
|
+
return 1 + MARK_PULSE_AMPLITUDE * Math.sin(Math.PI * elapsedMs / MARK_PULSE_MS);
|
|
27679
|
+
}
|
|
27680
|
+
function paintMarkLane(ctx, layout, deps) {
|
|
27681
|
+
if (layout.glyphs.length === 0) return;
|
|
27682
|
+
ctx.save();
|
|
27683
|
+
ctx.setLineDash([]);
|
|
27684
|
+
ctx.textAlign = "center";
|
|
27685
|
+
ctx.textBaseline = "middle";
|
|
27686
|
+
for (const g of layout.glyphs) {
|
|
27687
|
+
const mark = g.cluster.marks[0];
|
|
27688
|
+
if (!mark) continue;
|
|
27689
|
+
const key = g.cluster.key;
|
|
27690
|
+
const color = mark.glyph.color;
|
|
27691
|
+
const active = key === deps.activeKey || key === deps.flashKey;
|
|
27692
|
+
const size = g.size * (key === deps.hoverKey ? pulseScale(deps.nowMs - deps.hoverSince) : 1);
|
|
27693
|
+
if (g.depth === 0) {
|
|
27694
|
+
const sx = Math.round(g.x) + 0.5;
|
|
27695
|
+
ctx.lineWidth = 1;
|
|
27696
|
+
ctx.strokeStyle = deps.stemColor;
|
|
27697
|
+
ctx.beginPath();
|
|
27698
|
+
ctx.moveTo(sx, g.y + g.size / 2);
|
|
27699
|
+
ctx.lineTo(sx, deps.axisY);
|
|
27700
|
+
ctx.stroke();
|
|
27701
|
+
}
|
|
27702
|
+
const shape = mark.glyph.shape ?? "circle";
|
|
27703
|
+
const center = traceShape(ctx, shape, g.x, g.y, size);
|
|
27704
|
+
ctx.lineWidth = 4;
|
|
27705
|
+
ctx.strokeStyle = deps.background;
|
|
27706
|
+
ctx.stroke();
|
|
27707
|
+
ctx.fillStyle = active ? color : deps.background;
|
|
27708
|
+
ctx.fill();
|
|
27709
|
+
ctx.lineWidth = 1.5;
|
|
27710
|
+
ctx.strokeStyle = color;
|
|
27711
|
+
ctx.stroke();
|
|
27712
|
+
const ink = active ? ACTIVE_INK : color;
|
|
27713
|
+
const symbolPx = Math.round(size * 0.62);
|
|
27714
|
+
if (mark.glyph.icon) {
|
|
27715
|
+
const img = deps.icons.get(mark.glyph.icon, ink, symbolPx, deps.dpr);
|
|
27716
|
+
if (img) ctx.drawImage(img, center.x - symbolPx / 2, center.y - symbolPx / 2, symbolPx, symbolPx);
|
|
27717
|
+
} else if (mark.glyph.letter) {
|
|
27718
|
+
ctx.fillStyle = ink;
|
|
27719
|
+
ctx.font = `600 ${Math.round(size * 0.58)}px ${deps.fontFamily}`;
|
|
27720
|
+
ctx.fillText(mark.glyph.letter.slice(0, 2), center.x, center.y + 0.5);
|
|
27721
|
+
}
|
|
27722
|
+
}
|
|
27723
|
+
ctx.restore();
|
|
27724
|
+
}
|
|
27725
|
+
function traceShape(ctx, shape, x, y, size) {
|
|
27726
|
+
const r = size / 2;
|
|
27727
|
+
ctx.beginPath();
|
|
27728
|
+
switch (shape) {
|
|
27729
|
+
case "square": {
|
|
27730
|
+
const c = Math.min(3, r / 2);
|
|
27731
|
+
roundedRect(ctx, x - r, y - r, size, size, c);
|
|
27732
|
+
return { x, y };
|
|
27733
|
+
}
|
|
27734
|
+
case "diamond":
|
|
27735
|
+
ctx.moveTo(x, y - r);
|
|
27736
|
+
ctx.lineTo(x + r, y);
|
|
27737
|
+
ctx.lineTo(x, y + r);
|
|
27738
|
+
ctx.lineTo(x - r, y);
|
|
27739
|
+
ctx.closePath();
|
|
27740
|
+
return { x, y };
|
|
27741
|
+
case "pin": {
|
|
27742
|
+
const hr = r * 0.82;
|
|
27743
|
+
const hy = y - r + hr;
|
|
27744
|
+
ctx.arc(x, hy, hr, Math.PI * 0.75, Math.PI * 0.25, false);
|
|
27745
|
+
ctx.lineTo(x, y + r);
|
|
27746
|
+
ctx.closePath();
|
|
27747
|
+
return { x, y: hy };
|
|
27748
|
+
}
|
|
27749
|
+
case "circle":
|
|
27750
|
+
default:
|
|
27751
|
+
ctx.arc(x, y, r, 0, Math.PI * 2);
|
|
27752
|
+
return { x, y };
|
|
27753
|
+
}
|
|
27754
|
+
}
|
|
27755
|
+
function roundedRect(ctx, x, y, w, h, radius) {
|
|
27756
|
+
ctx.moveTo(x + radius, y);
|
|
27757
|
+
ctx.lineTo(x + w - radius, y);
|
|
27758
|
+
ctx.quadraticCurveTo(x + w, y, x + w, y + radius);
|
|
27759
|
+
ctx.lineTo(x + w, y + h - radius);
|
|
27760
|
+
ctx.quadraticCurveTo(x + w, y + h, x + w - radius, y + h);
|
|
27761
|
+
ctx.lineTo(x + radius, y + h);
|
|
27762
|
+
ctx.quadraticCurveTo(x, y + h, x, y + h - radius);
|
|
27763
|
+
ctx.lineTo(x, y + radius);
|
|
27764
|
+
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
27765
|
+
ctx.closePath();
|
|
27766
|
+
}
|
|
27767
|
+
var MarkIconRaster = class {
|
|
27768
|
+
constructor(onReady) {
|
|
27769
|
+
this.onReady = onReady;
|
|
27770
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
27771
|
+
}
|
|
27772
|
+
get(icon2, ink, px, dpr) {
|
|
27773
|
+
const key = `${icon2}|${ink}|${px}|${dpr}`;
|
|
27774
|
+
if (this.cache.has(key)) {
|
|
27775
|
+
const img2 = this.cache.get(key);
|
|
27776
|
+
return img2 && img2.complete && img2.naturalWidth > 0 ? img2 : null;
|
|
27777
|
+
}
|
|
27778
|
+
const markup = iconMarkup(icon2);
|
|
27779
|
+
if (!markup || typeof document === "undefined" || typeof Image === "undefined" || typeof XMLSerializer === "undefined") {
|
|
27780
|
+
this.cache.set(key, null);
|
|
27781
|
+
return null;
|
|
27782
|
+
}
|
|
27783
|
+
const svg = standaloneSvg(markup, ink, Math.max(1, Math.ceil(px * dpr)));
|
|
27784
|
+
if (!svg) {
|
|
27785
|
+
this.cache.set(key, null);
|
|
27786
|
+
return null;
|
|
27787
|
+
}
|
|
27788
|
+
const img = new Image();
|
|
27789
|
+
img.onload = () => this.onReady();
|
|
27790
|
+
img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
|
|
27791
|
+
this.cache.set(key, img);
|
|
27792
|
+
return null;
|
|
27793
|
+
}
|
|
27794
|
+
clear() {
|
|
27795
|
+
this.cache.clear();
|
|
27796
|
+
}
|
|
27797
|
+
};
|
|
27798
|
+
function standaloneSvg(markup, ink, px) {
|
|
27799
|
+
const tpl = document.createElement("template");
|
|
27800
|
+
tpl.innerHTML = markup;
|
|
27801
|
+
const svg = tpl.content.firstElementChild;
|
|
27802
|
+
if (!svg || svg.tagName.toLowerCase() !== "svg") return null;
|
|
27803
|
+
svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
|
27804
|
+
svg.setAttribute("width", String(px));
|
|
27805
|
+
svg.setAttribute("height", String(px));
|
|
27806
|
+
svg.setAttribute("color", ink);
|
|
27807
|
+
return new XMLSerializer().serializeToString(svg);
|
|
27808
|
+
}
|
|
27809
|
+
|
|
27130
27810
|
// src/renderers/native/chrome/ChromeRenderer.ts
|
|
27131
27811
|
var ChromeRenderer = class {
|
|
27132
27812
|
constructor() {
|
|
@@ -27136,11 +27816,40 @@ var ChromeRenderer = class {
|
|
|
27136
27816
|
this.axisTextColor = DARK_THEME.textColor;
|
|
27137
27817
|
// Shared Pine-drawing renderer, used here for autoscale geometry only; widthCache persists.
|
|
27138
27818
|
this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
|
|
27819
|
+
/** The timeline-mark lane as laid out by the last frame — what hover/click hit-test against. */
|
|
27820
|
+
this.markLayout = { glyphs: [], stacks: /* @__PURE__ */ new Map() };
|
|
27821
|
+
/** Registry icons rasterized for the lane; the owner is asked for a chrome repaint when one lands. */
|
|
27822
|
+
this.markIcons = new MarkIconRaster(() => this.onMarkIconReady?.());
|
|
27823
|
+
this.onMarkIconReady = null;
|
|
27824
|
+
/** Bar open times of the current series, rebuilt only when the array or its length changes (a live tick keeps both). */
|
|
27825
|
+
this.barTimesSrc = null;
|
|
27826
|
+
this.barTimesCache = [];
|
|
27139
27827
|
}
|
|
27140
27828
|
mount(canvas) {
|
|
27141
27829
|
this.canvas = canvas;
|
|
27142
27830
|
this.ctx = canvas.getContext("2d");
|
|
27143
27831
|
}
|
|
27832
|
+
/** Where to ask for a chrome repaint when a lane icon finishes rasterizing. */
|
|
27833
|
+
setMarkIconReady(cb) {
|
|
27834
|
+
this.onMarkIconReady = cb;
|
|
27835
|
+
}
|
|
27836
|
+
/** The interactive mark glyph under a plot point (last frame's layout), or null. */
|
|
27837
|
+
markGlyphAt(x, y) {
|
|
27838
|
+
return markGlyphAt(this.markLayout, x, y);
|
|
27839
|
+
}
|
|
27840
|
+
/** The mark stack (bar index) whose glyphs — or the gaps of its fan — cover a plot point. */
|
|
27841
|
+
markStackAt(x, y) {
|
|
27842
|
+
return markStackAt(this.markLayout, x, y);
|
|
27843
|
+
}
|
|
27844
|
+
/** A glyph of the last frame by its cluster key — how an open popup follows its anchor. */
|
|
27845
|
+
markGlyphByKey(key) {
|
|
27846
|
+
return this.markLayout.glyphs.find((g) => g.cluster.key === key) ?? null;
|
|
27847
|
+
}
|
|
27848
|
+
/** Hover text of the mark glyph under a plot point, or null. */
|
|
27849
|
+
markTooltipAt(x, y, groups) {
|
|
27850
|
+
const g = this.markGlyphAt(x, y);
|
|
27851
|
+
return g ? clusterTooltip(g.cluster, groups) : null;
|
|
27852
|
+
}
|
|
27144
27853
|
/** Wire the drawing coordinate resolvers + theme (call once per frame before use). */
|
|
27145
27854
|
prepare(scene, coords, theme) {
|
|
27146
27855
|
this.drawScene.setDeps({
|
|
@@ -27189,6 +27898,7 @@ var ChromeRenderer = class {
|
|
|
27189
27898
|
const panes = scene.orderedPanes();
|
|
27190
27899
|
if (coords.barCount === 0) {
|
|
27191
27900
|
this.drawPaneSeparators(ctx, scene, theme, fullW, panes);
|
|
27901
|
+
this.markLayout = { glyphs: [], stacks: /* @__PURE__ */ new Map() };
|
|
27192
27902
|
return;
|
|
27193
27903
|
}
|
|
27194
27904
|
const pricePane = panes.find((p) => p.kind === "price") ?? null;
|
|
@@ -27202,6 +27912,46 @@ var ChromeRenderer = class {
|
|
|
27202
27912
|
this.drawPaneSeparators(ctx, scene, theme, fullW, panes);
|
|
27203
27913
|
this.drawPriceLineAndCountdown(ctx, scene, coords, theme, dataW, pricePane);
|
|
27204
27914
|
this.drawTimeAxis(ctx, scene, coords, theme, dataW, dataH, fullH);
|
|
27915
|
+
this.drawMarkLane(ctx, scene, coords, theme, dataW, dataH);
|
|
27916
|
+
}
|
|
27917
|
+
/** The timeline-mark lane — after the axis, so the tokens read over the plot's bottom edge. */
|
|
27918
|
+
drawMarkLane(ctx, scene, coords, theme, dataW, dataH) {
|
|
27919
|
+
if (!scene.marks.visible || scene.timelineMarks.length === 0) {
|
|
27920
|
+
this.markLayout = { glyphs: [], stacks: /* @__PURE__ */ new Map() };
|
|
27921
|
+
return;
|
|
27922
|
+
}
|
|
27923
|
+
this.markLayout = layoutMarkLane({
|
|
27924
|
+
marks: scene.timelineMarks,
|
|
27925
|
+
groups: scene.markGroups,
|
|
27926
|
+
hidden: (groupId) => !markGroupVisible(scene.marks, groupId, scene.markGroups),
|
|
27927
|
+
barTimes: this.barTimes(scene),
|
|
27928
|
+
intervalMs: coords.barInterval,
|
|
27929
|
+
xOf: (bar) => coords.logicalToX(bar),
|
|
27930
|
+
axisY: dataH,
|
|
27931
|
+
dataW,
|
|
27932
|
+
expanded: scene.marksExpandedStack
|
|
27933
|
+
});
|
|
27934
|
+
const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
27935
|
+
paintMarkLane(ctx, this.markLayout, {
|
|
27936
|
+
axisY: dataH,
|
|
27937
|
+
background: theme.background,
|
|
27938
|
+
stemColor: scene.style.borderColor ?? theme.borderColor,
|
|
27939
|
+
fontFamily: theme.fontFamily,
|
|
27940
|
+
dpr: coords.dpr,
|
|
27941
|
+
icons: this.markIcons,
|
|
27942
|
+
hoverKey: scene.marksHoverKey,
|
|
27943
|
+
hoverSince: scene.marksHoverSince,
|
|
27944
|
+
activeKey: scene.marksActiveKey,
|
|
27945
|
+
flashKey: scene.marksFlash && scene.marksFlash.until > nowMs ? scene.marksFlash.key : null,
|
|
27946
|
+
nowMs
|
|
27947
|
+
});
|
|
27948
|
+
}
|
|
27949
|
+
barTimes(scene) {
|
|
27950
|
+
if (this.barTimesSrc !== scene.bars || this.barTimesCache.length !== scene.bars.length) {
|
|
27951
|
+
this.barTimesSrc = scene.bars;
|
|
27952
|
+
this.barTimesCache = scene.bars.map((b) => b.time);
|
|
27953
|
+
}
|
|
27954
|
+
return this.barTimesCache;
|
|
27205
27955
|
}
|
|
27206
27956
|
destroy() {
|
|
27207
27957
|
this.canvas = null;
|
|
@@ -27317,7 +28067,8 @@ var ChromeRenderer = class {
|
|
|
27317
28067
|
* - the countdown-to-bar-close chip (`showCountdown`).
|
|
27318
28068
|
* When the label and countdown are both on they merge into one stacked block (countdown
|
|
27319
28069
|
* under the label, text flushed left); a lone label or countdown is centered on the
|
|
27320
|
-
* price level with centered text. The countdown
|
|
28070
|
+
* price level with centered text. The countdown repaints on the renderer's second pulse
|
|
28071
|
+
* and disappears once the bar has closed, until the next bar arrives.
|
|
27321
28072
|
*/
|
|
27322
28073
|
drawPriceLineAndCountdown(ctx, scene, coords, theme, dataW, pricePane) {
|
|
27323
28074
|
const n = scene.bars.length;
|
|
@@ -27337,17 +28088,16 @@ var ChromeRenderer = class {
|
|
|
27337
28088
|
ctx.stroke();
|
|
27338
28089
|
setDash2(ctx, "solid");
|
|
27339
28090
|
}
|
|
27340
|
-
const
|
|
27341
|
-
const showCountdown =
|
|
28091
|
+
const cdText = scene.showCountdown ? countdownText(last.time, coords.barInterval, Date.now()) : null;
|
|
28092
|
+
const showCountdown = cdText !== null;
|
|
27342
28093
|
const showLabel = scene.showPriceLabel;
|
|
27343
28094
|
if (!showLabel && !showCountdown) return;
|
|
27344
28095
|
const priceText = formatAxisValue(pricePane.scale, pricePane.bounds.height, last.close, percentScaleFor(scene, pricePane), scene.priceMintick);
|
|
27345
|
-
const cdText = showCountdown ? formatCountdown(last.time + interval - Date.now()) : "";
|
|
27346
28096
|
const PAD = 8;
|
|
27347
28097
|
const x = dataW + 1;
|
|
27348
28098
|
const textColor = tagTextColor(color, theme.background);
|
|
27349
28099
|
ctx.textBaseline = "middle";
|
|
27350
|
-
if (showLabel &&
|
|
28100
|
+
if (showLabel && cdText !== null) {
|
|
27351
28101
|
const w2 = Math.max(ctx.measureText(priceText).width, ctx.measureText(cdText).width) + PAD;
|
|
27352
28102
|
const top = y - 8;
|
|
27353
28103
|
const tx = x + PAD / 2;
|
|
@@ -27360,7 +28110,7 @@ var ChromeRenderer = class {
|
|
|
27360
28110
|
ctx.textAlign = "start";
|
|
27361
28111
|
return;
|
|
27362
28112
|
}
|
|
27363
|
-
const text = showLabel ? priceText : cdText;
|
|
28113
|
+
const text = showLabel ? priceText : cdText ?? "";
|
|
27364
28114
|
const w = ctx.measureText(text).width + PAD;
|
|
27365
28115
|
ctx.fillStyle = color;
|
|
27366
28116
|
ctx.fillRect(x, y - 8, w, 16);
|
|
@@ -27431,29 +28181,6 @@ function setDash2(ctx, style) {
|
|
|
27431
28181
|
else if (style === "dotted") ctx.setLineDash([2, 3]);
|
|
27432
28182
|
else ctx.setLineDash([]);
|
|
27433
28183
|
}
|
|
27434
|
-
function tagTextColor(bg, over) {
|
|
27435
|
-
const [r, g, b, a] = parseColor(bg);
|
|
27436
|
-
let R = r;
|
|
27437
|
-
let G = g;
|
|
27438
|
-
let B = b;
|
|
27439
|
-
if (a < 1) {
|
|
27440
|
-
const [or, og, ob] = parseColor(over);
|
|
27441
|
-
R = r * a + or * (1 - a);
|
|
27442
|
-
G = g * a + og * (1 - a);
|
|
27443
|
-
B = b * a + ob * (1 - a);
|
|
27444
|
-
}
|
|
27445
|
-
const lin = (c) => c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
27446
|
-
const L = 0.2126 * lin(R) + 0.7152 * lin(G) + 0.0722 * lin(B);
|
|
27447
|
-
return L >= 0.4 ? "#000000" : "#ffffff";
|
|
27448
|
-
}
|
|
27449
|
-
function formatCountdown(ms) {
|
|
27450
|
-
const total = Math.max(0, Math.floor(ms / 1e3));
|
|
27451
|
-
const s = total % 60;
|
|
27452
|
-
const m = Math.floor(total / 60) % 60;
|
|
27453
|
-
const h = Math.floor(total / 3600);
|
|
27454
|
-
const pad = (v) => String(v).padStart(2, "0");
|
|
27455
|
-
return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`;
|
|
27456
|
-
}
|
|
27457
28184
|
|
|
27458
28185
|
// src/renderers/native/chrome/LabelTooltip.ts
|
|
27459
28186
|
var HOVER_DELAY_MS = 350;
|
|
@@ -27659,6 +28386,11 @@ function setDash3(ctx, style) {
|
|
|
27659
28386
|
function settingsIdSlug(label) {
|
|
27660
28387
|
return label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
27661
28388
|
}
|
|
28389
|
+
var MARKS_SETTINGS_ID = "events";
|
|
28390
|
+
var MARKS_GROUPS_SETTINGS_ID = "events.groups";
|
|
28391
|
+
function markGroupSettingsId(groupId) {
|
|
28392
|
+
return `${MARKS_GROUPS_SETTINGS_ID}.${settingsIdSlug(groupId)}`;
|
|
28393
|
+
}
|
|
27662
28394
|
function settingsIdHidden(id, hidden) {
|
|
27663
28395
|
if (hidden.size === 0) return false;
|
|
27664
28396
|
let path = id;
|
|
@@ -27752,7 +28484,11 @@ var BUILTIN_SETTINGS_IDS = [
|
|
|
27752
28484
|
"symbol.style.baseline.base-level",
|
|
27753
28485
|
"symbol.style.baseline.width",
|
|
27754
28486
|
"symbol.animation",
|
|
28487
|
+
"symbol.animation.zoom",
|
|
28488
|
+
"symbol.animation.pan",
|
|
28489
|
+
"symbol.animation.autoscale",
|
|
27755
28490
|
"symbol.animation.price-changes",
|
|
28491
|
+
"symbol.animation.intro",
|
|
27756
28492
|
"symbol.timezone",
|
|
27757
28493
|
"scales",
|
|
27758
28494
|
"scales.price-scale",
|
|
@@ -27778,8 +28514,13 @@ var BUILTIN_SETTINGS_IDS = [
|
|
|
27778
28514
|
"canvas.grid.horizontal",
|
|
27779
28515
|
"canvas.theme"
|
|
27780
28516
|
];
|
|
27781
|
-
function settingsIdCatalog(hostSections) {
|
|
28517
|
+
function settingsIdCatalog(hostSections, markGroups = []) {
|
|
27782
28518
|
const ids = new Set(BUILTIN_SETTINGS_IDS);
|
|
28519
|
+
if (markGroups.length > 0) {
|
|
28520
|
+
ids.add(MARKS_SETTINGS_ID);
|
|
28521
|
+
ids.add(MARKS_GROUPS_SETTINGS_ID);
|
|
28522
|
+
for (const g of markGroups) ids.add(markGroupSettingsId(g.id));
|
|
28523
|
+
}
|
|
27783
28524
|
for (const def of chartTypes()) {
|
|
27784
28525
|
if (hasOwnCandlePaint(def.id)) {
|
|
27785
28526
|
const style = `symbol.style.${def.id}`;
|
|
@@ -27819,7 +28560,7 @@ function styleLabel(id) {
|
|
|
27819
28560
|
return chartType(id)?.label ?? BUILTIN_STYLE_LABELS2[id] ?? id;
|
|
27820
28561
|
}
|
|
27821
28562
|
var SD_STYLE_ID = "vela-settings-controls";
|
|
27822
|
-
var SD_STYLE_REV = "
|
|
28563
|
+
var SD_STYLE_REV = "6";
|
|
27823
28564
|
var SETTINGS_BORDER = "var(--vela-border)";
|
|
27824
28565
|
function ensureControlStyles() {
|
|
27825
28566
|
if (typeof document === "undefined") return;
|
|
@@ -27895,7 +28636,15 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
27895
28636
|
.vela-sd-mobile .vela-select-trigger,.vela-sd-mobile .vela-num input,.vela-sd-mobile .vela-width-field{height:34px;}
|
|
27896
28637
|
.vela-sd-mobile .vela-sd-close{width:40px;height:40px;}
|
|
27897
28638
|
.vela-sd-mobile .vela-sd-btn{height:38px;}
|
|
27898
|
-
.vela-sd-mobile .vela-sd-row span,.vela-sd-mobile .vela-sd-bool span,.vela-sd-mobile .vela-field-label{white-space:normal !important;}
|
|
28639
|
+
.vela-sd-mobile .vela-sd-row span,.vela-sd-mobile .vela-sd-bool span,.vela-sd-mobile .vela-field-label{white-space:normal !important;}
|
|
28640
|
+
/* The wrap rule above is for ROW LABELS only: a select's closed value must keep its
|
|
28641
|
+
single-line ellipsis, or a long option wraps to several lines inside the 34px
|
|
28642
|
+
trigger and spills over the rows around it. Three classes so it outranks the
|
|
28643
|
+
two-classes-plus-element selector above. The kit's fixed 100px column is a desktop
|
|
28644
|
+
alignment device; on mobile the trigger hugs its value instead (the grid's control
|
|
28645
|
+
column is max-content), capped so a long option still ellipsizes before the label. */
|
|
28646
|
+
.vela-sd-mobile .vela-select-trigger .vela-select-label{white-space:nowrap !important;}
|
|
28647
|
+
.vela-sd-mobile .vela-select:not([data-fill]){width:auto;min-width:100px;max-width:min(220px,55vw);}`;
|
|
27899
28648
|
if (!existing) document.head.appendChild(st);
|
|
27900
28649
|
}
|
|
27901
28650
|
var SettingsDialog = class {
|
|
@@ -27910,6 +28659,9 @@ var SettingsDialog = class {
|
|
|
27910
28659
|
this.config = null;
|
|
27911
28660
|
this.syncTypeTabs = null;
|
|
27912
28661
|
this.hostSections = [];
|
|
28662
|
+
/** The timeline-mark groups (defined + named by marks) — one checkbox each on the Events tab. */
|
|
28663
|
+
this.markGroups = [];
|
|
28664
|
+
this.markGroupVisible = () => true;
|
|
27913
28665
|
/** The Canvas → Theme row: current app theme + where a pick is raised. The row is a
|
|
27914
28666
|
* host callback, NOT a config patch — the app theme stays out of the persisted
|
|
27915
28667
|
* `ChartConfig`, so exported templates never carry it. */
|
|
@@ -27934,6 +28686,11 @@ var SettingsDialog = class {
|
|
|
27934
28686
|
setHostSections(sections) {
|
|
27935
28687
|
this.hostSections = sections;
|
|
27936
28688
|
}
|
|
28689
|
+
/** The timeline-mark groups and their current visibility — the Events tab's rows on next open. */
|
|
28690
|
+
setMarkGroups(groups, visible) {
|
|
28691
|
+
this.markGroups = groups;
|
|
28692
|
+
this.markGroupVisible = visible;
|
|
28693
|
+
}
|
|
27937
28694
|
/** Replace the visibility policy — an open dialog rebuilds in place to honor it. */
|
|
27938
28695
|
setHiddenSettings(ids) {
|
|
27939
28696
|
const next = new Set(ids);
|
|
@@ -28107,12 +28864,36 @@ var SettingsDialog = class {
|
|
|
28107
28864
|
}
|
|
28108
28865
|
showActive(config.series.style);
|
|
28109
28866
|
body.append(sid(this.sectionTitle("Animation"), "symbol.animation"));
|
|
28867
|
+
body.append(sid(this.boolRow(
|
|
28868
|
+
"Animate zoom",
|
|
28869
|
+
config.animations.zoom,
|
|
28870
|
+
(v) => this.emit({ animations: { zoom: v } }),
|
|
28871
|
+
this.hint("Glide the chart to each zoom step instead of jumping.")
|
|
28872
|
+
), "symbol.animation.zoom"));
|
|
28873
|
+
body.append(sid(this.boolRow(
|
|
28874
|
+
"Pan momentum",
|
|
28875
|
+
config.animations.pan,
|
|
28876
|
+
(v) => this.emit({ animations: { pan: v } }),
|
|
28877
|
+
this.hint("Keep gliding briefly after a drag release, and ease scroll-to-latest and keyboard pans.")
|
|
28878
|
+
), "symbol.animation.pan"));
|
|
28879
|
+
body.append(sid(this.boolRow(
|
|
28880
|
+
"Animate price scale",
|
|
28881
|
+
config.animations.autoscale,
|
|
28882
|
+
(v) => this.emit({ animations: { autoscale: v } }),
|
|
28883
|
+
this.hint("Glide the price scale to its new range while zooming or panning.")
|
|
28884
|
+
), "symbol.animation.autoscale"));
|
|
28110
28885
|
body.append(sid(this.boolRow(
|
|
28111
28886
|
"Animate price changes",
|
|
28112
28887
|
config.priceScale.animateLastPrice,
|
|
28113
28888
|
(v) => this.emit({ priceScale: { animateLastPrice: v } }),
|
|
28114
28889
|
this.hint("Glide the live bar to each new price instead of snapping.")
|
|
28115
28890
|
), "symbol.animation.price-changes"));
|
|
28891
|
+
body.append(sid(this.boolRow(
|
|
28892
|
+
"Reveal on load",
|
|
28893
|
+
config.animations.intro,
|
|
28894
|
+
(v) => this.emit({ animations: { intro: v } }),
|
|
28895
|
+
this.hint("Draw the candles in when a chart first loads. Takes effect on the next load.")
|
|
28896
|
+
), "symbol.animation.intro"));
|
|
28116
28897
|
body.append(sid(this.sectionTitle("Time zone"), "symbol.timezone"));
|
|
28117
28898
|
body.append(sid(this.selectRowLabeled("Time zone", normalizeTimezone(config.timeScale.timezone), timezoneOptions(config.timeScale.timezone), (v) => this.emit({ timeScale: { timezone: v } })), "symbol.timezone"));
|
|
28118
28899
|
const renderHostSections = (placement) => {
|
|
@@ -28182,6 +28963,13 @@ var SettingsDialog = class {
|
|
|
28182
28963
|
body.append(sid(this.sectionTitle("Theme"), "canvas.theme"));
|
|
28183
28964
|
body.append(sid(this.selectRow("Color theme", tc.current === "dark" ? "Dark" : "Light", ["Dark", "Light"], (v) => tc.onSelect(v === "Dark" ? "dark" : "light")), "canvas.theme"));
|
|
28184
28965
|
}
|
|
28966
|
+
if (this.markGroups.length > 0) {
|
|
28967
|
+
body.append(sid(this.section("Events"), MARKS_SETTINGS_ID));
|
|
28968
|
+
body.append(sid(this.sectionTitle("Visible events"), MARKS_GROUPS_SETTINGS_ID));
|
|
28969
|
+
for (const g of this.markGroups) {
|
|
28970
|
+
body.append(sid(this.boolRow(g.label, this.markGroupVisible(g.id), (v) => this.emit({ marks: { groups: { [g.id]: v } } })), markGroupSettingsId(g.id)));
|
|
28971
|
+
}
|
|
28972
|
+
}
|
|
28185
28973
|
renderChartTypeSections("end");
|
|
28186
28974
|
renderHostSections("end");
|
|
28187
28975
|
if (this.hiddenSettings.size > 0) {
|
|
@@ -33741,6 +34529,383 @@ function expandScaleByPixels(scale, heightPx, abovePx, belowPx) {
|
|
|
33741
34529
|
return { ...scale, min: scale.min - belowPx * perPx, max: scale.max + abovePx * perPx };
|
|
33742
34530
|
}
|
|
33743
34531
|
|
|
34532
|
+
// src/ui/sanitize-html.ts
|
|
34533
|
+
var ALLOWED_TAGS = /* @__PURE__ */ new Set([
|
|
34534
|
+
"a",
|
|
34535
|
+
"abbr",
|
|
34536
|
+
"b",
|
|
34537
|
+
"blockquote",
|
|
34538
|
+
"br",
|
|
34539
|
+
"code",
|
|
34540
|
+
"dd",
|
|
34541
|
+
"del",
|
|
34542
|
+
"div",
|
|
34543
|
+
"dl",
|
|
34544
|
+
"dt",
|
|
34545
|
+
"em",
|
|
34546
|
+
"h1",
|
|
34547
|
+
"h2",
|
|
34548
|
+
"h3",
|
|
34549
|
+
"h4",
|
|
34550
|
+
"h5",
|
|
34551
|
+
"h6",
|
|
34552
|
+
"hr",
|
|
34553
|
+
"i",
|
|
34554
|
+
"img",
|
|
34555
|
+
"ins",
|
|
34556
|
+
"kbd",
|
|
34557
|
+
"li",
|
|
34558
|
+
"mark",
|
|
34559
|
+
"ol",
|
|
34560
|
+
"p",
|
|
34561
|
+
"pre",
|
|
34562
|
+
"q",
|
|
34563
|
+
"s",
|
|
34564
|
+
"small",
|
|
34565
|
+
"span",
|
|
34566
|
+
"strong",
|
|
34567
|
+
"sub",
|
|
34568
|
+
"sup",
|
|
34569
|
+
"table",
|
|
34570
|
+
"tbody",
|
|
34571
|
+
"td",
|
|
34572
|
+
"tfoot",
|
|
34573
|
+
"th",
|
|
34574
|
+
"thead",
|
|
34575
|
+
"tr",
|
|
34576
|
+
"u",
|
|
34577
|
+
"ul"
|
|
34578
|
+
]);
|
|
34579
|
+
var DROPPED_TAGS = /* @__PURE__ */ new Set([
|
|
34580
|
+
"script",
|
|
34581
|
+
"style",
|
|
34582
|
+
"iframe",
|
|
34583
|
+
"frame",
|
|
34584
|
+
"frameset",
|
|
34585
|
+
"object",
|
|
34586
|
+
"embed",
|
|
34587
|
+
"applet",
|
|
34588
|
+
"form",
|
|
34589
|
+
"input",
|
|
34590
|
+
"textarea",
|
|
34591
|
+
"button",
|
|
34592
|
+
"select",
|
|
34593
|
+
"option",
|
|
34594
|
+
"link",
|
|
34595
|
+
"meta",
|
|
34596
|
+
"base",
|
|
34597
|
+
"svg",
|
|
34598
|
+
"math",
|
|
34599
|
+
"template",
|
|
34600
|
+
"noscript",
|
|
34601
|
+
"audio",
|
|
34602
|
+
"video",
|
|
34603
|
+
"canvas",
|
|
34604
|
+
"dialog",
|
|
34605
|
+
"head",
|
|
34606
|
+
"title"
|
|
34607
|
+
]);
|
|
34608
|
+
var ALLOWED_ATTRS = {
|
|
34609
|
+
a: /* @__PURE__ */ new Set(["href"]),
|
|
34610
|
+
img: /* @__PURE__ */ new Set(["src", "alt", "width", "height"]),
|
|
34611
|
+
td: /* @__PURE__ */ new Set(["colspan", "rowspan"]),
|
|
34612
|
+
th: /* @__PURE__ */ new Set(["colspan", "rowspan"]),
|
|
34613
|
+
ol: /* @__PURE__ */ new Set(["start"])
|
|
34614
|
+
};
|
|
34615
|
+
var BLOCKED_SCHEMES = /* @__PURE__ */ new Set(["javascript", "vbscript", "data", "file", "blob"]);
|
|
34616
|
+
function tagDisposition(tag) {
|
|
34617
|
+
const t = tag.toLowerCase();
|
|
34618
|
+
if (DROPPED_TAGS.has(t)) return "drop";
|
|
34619
|
+
return ALLOWED_TAGS.has(t) ? "keep" : "unwrap";
|
|
34620
|
+
}
|
|
34621
|
+
function attributeAllowed(tag, name) {
|
|
34622
|
+
const n = name.toLowerCase();
|
|
34623
|
+
if (n.startsWith("on") || n === "style") return false;
|
|
34624
|
+
if (n === "title") return true;
|
|
34625
|
+
return ALLOWED_ATTRS[tag.toLowerCase()]?.has(n) ?? false;
|
|
34626
|
+
}
|
|
34627
|
+
function safeUrl(value, absoluteOnly = false) {
|
|
34628
|
+
let url = "";
|
|
34629
|
+
for (const ch of value) if (ch.charCodeAt(0) > 32) url += ch;
|
|
34630
|
+
const m = /^([a-z][a-z0-9+.-]*):/i.exec(url);
|
|
34631
|
+
const scheme = m ? m[1].toLowerCase() : null;
|
|
34632
|
+
if (scheme !== null && BLOCKED_SCHEMES.has(scheme)) return null;
|
|
34633
|
+
if (absoluteOnly && scheme !== "http" && scheme !== "https") return null;
|
|
34634
|
+
return url;
|
|
34635
|
+
}
|
|
34636
|
+
var ELEMENT_NODE = 1;
|
|
34637
|
+
var TEXT_NODE = 3;
|
|
34638
|
+
function sanitizeHtml(html, doc) {
|
|
34639
|
+
const tpl = doc.createElement("template");
|
|
34640
|
+
tpl.innerHTML = html;
|
|
34641
|
+
const out = doc.createDocumentFragment();
|
|
34642
|
+
copyChildren(tpl.content, out, doc);
|
|
34643
|
+
return out;
|
|
34644
|
+
}
|
|
34645
|
+
function copyChildren(from, to, doc) {
|
|
34646
|
+
for (const child of Array.from(from.childNodes)) {
|
|
34647
|
+
if (child.nodeType === TEXT_NODE) {
|
|
34648
|
+
to.appendChild(doc.createTextNode(child.textContent ?? ""));
|
|
34649
|
+
continue;
|
|
34650
|
+
}
|
|
34651
|
+
if (child.nodeType !== ELEMENT_NODE) continue;
|
|
34652
|
+
const el = child;
|
|
34653
|
+
const tag = el.tagName.toLowerCase();
|
|
34654
|
+
const disposition = tagDisposition(tag);
|
|
34655
|
+
if (disposition === "drop") continue;
|
|
34656
|
+
if (disposition === "unwrap") {
|
|
34657
|
+
copyChildren(el, to, doc);
|
|
34658
|
+
continue;
|
|
34659
|
+
}
|
|
34660
|
+
const clean = doc.createElement(tag);
|
|
34661
|
+
for (const attr of Array.from(el.attributes)) {
|
|
34662
|
+
const name = attr.name.toLowerCase();
|
|
34663
|
+
if (!attributeAllowed(tag, name)) continue;
|
|
34664
|
+
let value = attr.value;
|
|
34665
|
+
if (name === "href" || name === "src") {
|
|
34666
|
+
const safe = safeUrl(value, name === "src");
|
|
34667
|
+
if (safe === null) continue;
|
|
34668
|
+
value = safe;
|
|
34669
|
+
}
|
|
34670
|
+
clean.setAttribute(name, value);
|
|
34671
|
+
}
|
|
34672
|
+
if (tag === "a") {
|
|
34673
|
+
clean.setAttribute("target", "_blank");
|
|
34674
|
+
clean.setAttribute("rel", "noopener noreferrer");
|
|
34675
|
+
}
|
|
34676
|
+
copyChildren(el, clean, doc);
|
|
34677
|
+
to.appendChild(clean);
|
|
34678
|
+
}
|
|
34679
|
+
}
|
|
34680
|
+
|
|
34681
|
+
// src/renderers/native/chrome/marks/MarkPopover.ts
|
|
34682
|
+
var MARKS_STYLE_ID = "vela-marks-popover";
|
|
34683
|
+
var MARKS_CSS = `
|
|
34684
|
+
.vela-marks-panel { padding: 0; gap: 0; min-width: 220px; max-width: 320px; max-height: 320px; overflow-y: auto; overscroll-behavior: contain; }
|
|
34685
|
+
.vela-marks-section { display: flex; flex-direction: column; gap: 8px; padding: 10px 12px; }
|
|
34686
|
+
.vela-marks-section + .vela-marks-section { border-top: 1px solid var(--vela-border); }
|
|
34687
|
+
.vela-marks-field { display: flex; justify-content: space-between; gap: 16px; line-height: 1.45; }
|
|
34688
|
+
.vela-marks-field-label { color: var(--vela-fg-muted); }
|
|
34689
|
+
.vela-marks-field-value { color: var(--vela-fg-bright); text-align: right; font-variant-numeric: tabular-nums; }
|
|
34690
|
+
.vela-marks-html { color: var(--vela-fg); line-height: 1.45; overflow-wrap: anywhere; }
|
|
34691
|
+
.vela-marks-html p { margin: 0 0 6px; }
|
|
34692
|
+
.vela-marks-html p:last-child { margin-bottom: 0; }
|
|
34693
|
+
.vela-marks-html a { color: var(--vela-accent); }
|
|
34694
|
+
.vela-marks-html img { max-width: 100%; height: auto; }
|
|
34695
|
+
.vela-marks-html table { border-collapse: collapse; }
|
|
34696
|
+
.vela-marks-html td, .vela-marks-html th { padding: 2px 6px; border: 1px solid var(--vela-border); }
|
|
34697
|
+
.vela-marks-html pre, .vela-marks-html code { font-family: var(--vela-font-mono, monospace); font-size: 0.92em; }
|
|
34698
|
+
.vela-marks-loading, .vela-marks-error { color: var(--vela-fg-muted); font-style: italic; }
|
|
34699
|
+
`;
|
|
34700
|
+
var MarkPopover = class {
|
|
34701
|
+
constructor(deps) {
|
|
34702
|
+
this.deps = deps;
|
|
34703
|
+
this.pop = null;
|
|
34704
|
+
this.openKey = null;
|
|
34705
|
+
/** Bumped per open/close — a lazy content resolving after its popup went away is dropped. */
|
|
34706
|
+
this.generation = 0;
|
|
34707
|
+
const doc = deps.plot.ownerDocument;
|
|
34708
|
+
injectStyles(CALLOUT_STYLE_ID, CALLOUT_CSS, doc);
|
|
34709
|
+
injectStyles(MARKS_STYLE_ID, MARKS_CSS, doc);
|
|
34710
|
+
this.anchor = doc.createElement("div");
|
|
34711
|
+
this.anchor.className = "vela-marks-anchor";
|
|
34712
|
+
Object.assign(this.anchor.style, { position: "absolute", pointerEvents: "none", left: "0", top: "0", width: "0", height: "0" });
|
|
34713
|
+
deps.plot.appendChild(this.anchor);
|
|
34714
|
+
}
|
|
34715
|
+
/** The cluster key the open popup belongs to, or null. */
|
|
34716
|
+
get key() {
|
|
34717
|
+
return this.openKey;
|
|
34718
|
+
}
|
|
34719
|
+
open(cluster, rect) {
|
|
34720
|
+
this.close();
|
|
34721
|
+
this.place(rect);
|
|
34722
|
+
const gen = ++this.generation;
|
|
34723
|
+
this.openKey = cluster.key;
|
|
34724
|
+
this.pop = new Popover({
|
|
34725
|
+
trigger: this.anchor,
|
|
34726
|
+
host: this.deps.host(),
|
|
34727
|
+
theme: this.deps.theme(),
|
|
34728
|
+
gap: 8,
|
|
34729
|
+
align: "center",
|
|
34730
|
+
// centered on the glyph
|
|
34731
|
+
fadeMs: 120,
|
|
34732
|
+
// a short, discreet fade in and out
|
|
34733
|
+
className: "vela-marks-pop",
|
|
34734
|
+
content: (body) => this.build(body, cluster, gen),
|
|
34735
|
+
onClose: () => {
|
|
34736
|
+
if (this.generation === gen) {
|
|
34737
|
+
this.openKey = null;
|
|
34738
|
+
this.pop = null;
|
|
34739
|
+
this.deps.onOpenChange?.(null);
|
|
34740
|
+
}
|
|
34741
|
+
}
|
|
34742
|
+
});
|
|
34743
|
+
this.pop.show();
|
|
34744
|
+
this.deps.onOpenChange?.(cluster.key);
|
|
34745
|
+
}
|
|
34746
|
+
/** Follow the anchor glyph after a repaint; `null` (glyph gone — hidden, scrolled off, marks replaced) closes. */
|
|
34747
|
+
track(rect) {
|
|
34748
|
+
if (!this.pop) return;
|
|
34749
|
+
if (!rect) {
|
|
34750
|
+
this.close();
|
|
34751
|
+
return;
|
|
34752
|
+
}
|
|
34753
|
+
this.place(rect);
|
|
34754
|
+
this.pop.reposition();
|
|
34755
|
+
}
|
|
34756
|
+
close() {
|
|
34757
|
+
const pop = this.pop;
|
|
34758
|
+
const wasOpen = this.openKey !== null;
|
|
34759
|
+
this.pop = null;
|
|
34760
|
+
this.openKey = null;
|
|
34761
|
+
this.generation++;
|
|
34762
|
+
pop?.destroy();
|
|
34763
|
+
if (wasOpen) this.deps.onOpenChange?.(null);
|
|
34764
|
+
}
|
|
34765
|
+
destroy() {
|
|
34766
|
+
this.close();
|
|
34767
|
+
this.anchor.remove();
|
|
34768
|
+
}
|
|
34769
|
+
place(rect) {
|
|
34770
|
+
Object.assign(this.anchor.style, {
|
|
34771
|
+
left: `${rect.x - rect.size / 2}px`,
|
|
34772
|
+
top: `${rect.y - rect.size / 2}px`,
|
|
34773
|
+
width: `${rect.size}px`,
|
|
34774
|
+
height: `${rect.size}px`
|
|
34775
|
+
});
|
|
34776
|
+
}
|
|
34777
|
+
build(body, cluster, gen) {
|
|
34778
|
+
const doc = body.ownerDocument;
|
|
34779
|
+
const root = doc.createElement("div");
|
|
34780
|
+
root.className = "vela-callout-panel vela-marks-panel";
|
|
34781
|
+
const pending = [];
|
|
34782
|
+
for (const mark of cluster.marks) {
|
|
34783
|
+
const section = doc.createElement("section");
|
|
34784
|
+
section.className = "vela-marks-section";
|
|
34785
|
+
if (mark.title) {
|
|
34786
|
+
const title = doc.createElement("div");
|
|
34787
|
+
title.className = "vela-callout-title";
|
|
34788
|
+
title.textContent = mark.title;
|
|
34789
|
+
section.appendChild(title);
|
|
34790
|
+
}
|
|
34791
|
+
const content = mark.content;
|
|
34792
|
+
if (typeof content === "function") {
|
|
34793
|
+
const slot = doc.createElement("div");
|
|
34794
|
+
slot.className = "vela-marks-loading";
|
|
34795
|
+
slot.textContent = "Loading\u2026";
|
|
34796
|
+
section.appendChild(slot);
|
|
34797
|
+
pending.push({ el: section, resolve: () => this.resolveLazy(content, slot, gen) });
|
|
34798
|
+
} else if (content !== void 0) {
|
|
34799
|
+
this.renderContent(section, content);
|
|
34800
|
+
}
|
|
34801
|
+
if (section.childElementCount > 0) root.appendChild(section);
|
|
34802
|
+
}
|
|
34803
|
+
body.appendChild(root);
|
|
34804
|
+
if (pending.length > 0) scheduleLazy(root, pending);
|
|
34805
|
+
}
|
|
34806
|
+
resolveLazy(source, slot, gen) {
|
|
34807
|
+
let result;
|
|
34808
|
+
try {
|
|
34809
|
+
result = Promise.resolve(source());
|
|
34810
|
+
} catch (err) {
|
|
34811
|
+
result = Promise.reject(err instanceof Error ? err : new Error(String(err)));
|
|
34812
|
+
}
|
|
34813
|
+
void result.then(
|
|
34814
|
+
(content) => {
|
|
34815
|
+
if (gen !== this.generation) return;
|
|
34816
|
+
const section = slot.parentElement;
|
|
34817
|
+
if (!section) return;
|
|
34818
|
+
slot.remove();
|
|
34819
|
+
this.renderContent(section, content);
|
|
34820
|
+
this.pop?.reposition();
|
|
34821
|
+
},
|
|
34822
|
+
() => {
|
|
34823
|
+
if (gen !== this.generation) return;
|
|
34824
|
+
slot.className = "vela-marks-error";
|
|
34825
|
+
slot.textContent = "Couldn\u2019t load this entry.";
|
|
34826
|
+
}
|
|
34827
|
+
);
|
|
34828
|
+
}
|
|
34829
|
+
renderContent(section, content) {
|
|
34830
|
+
const doc = section.ownerDocument;
|
|
34831
|
+
if (!content || typeof content !== "object") return;
|
|
34832
|
+
if ("text" in content) {
|
|
34833
|
+
const text = doc.createElement("div");
|
|
34834
|
+
text.className = "vela-callout-text";
|
|
34835
|
+
text.textContent = String(content.text);
|
|
34836
|
+
section.appendChild(text);
|
|
34837
|
+
return;
|
|
34838
|
+
}
|
|
34839
|
+
if ("html" in content) {
|
|
34840
|
+
const html = doc.createElement("div");
|
|
34841
|
+
html.className = "vela-marks-html";
|
|
34842
|
+
html.appendChild(sanitizeHtml(String(content.html), doc));
|
|
34843
|
+
section.appendChild(html);
|
|
34844
|
+
return;
|
|
34845
|
+
}
|
|
34846
|
+
if ("panel" in content && content.panel && Array.isArray(content.panel.items)) {
|
|
34847
|
+
let actions = null;
|
|
34848
|
+
for (const item of content.panel.items) {
|
|
34849
|
+
if (!item || typeof item !== "object") continue;
|
|
34850
|
+
if (item.type === "button") {
|
|
34851
|
+
if (!actions) {
|
|
34852
|
+
actions = doc.createElement("div");
|
|
34853
|
+
actions.className = "vela-callout-actions";
|
|
34854
|
+
section.appendChild(actions);
|
|
34855
|
+
}
|
|
34856
|
+
const btn2 = doc.createElement("button");
|
|
34857
|
+
btn2.type = "button";
|
|
34858
|
+
btn2.className = "vela-callout-btn" + (item.primary ? " vela-callout-btn-primary" : "");
|
|
34859
|
+
btn2.textContent = item.label;
|
|
34860
|
+
btn2.addEventListener("click", () => {
|
|
34861
|
+
item.run();
|
|
34862
|
+
if (item.close !== false) this.close();
|
|
34863
|
+
});
|
|
34864
|
+
actions.appendChild(btn2);
|
|
34865
|
+
continue;
|
|
34866
|
+
}
|
|
34867
|
+
actions = null;
|
|
34868
|
+
if (item.type === "text") {
|
|
34869
|
+
const text = doc.createElement("div");
|
|
34870
|
+
text.className = "vela-callout-text";
|
|
34871
|
+
text.textContent = item.text;
|
|
34872
|
+
section.appendChild(text);
|
|
34873
|
+
} else if (item.type === "field") {
|
|
34874
|
+
const row = doc.createElement("div");
|
|
34875
|
+
row.className = "vela-marks-field";
|
|
34876
|
+
const label = doc.createElement("span");
|
|
34877
|
+
label.className = "vela-marks-field-label";
|
|
34878
|
+
label.textContent = item.label;
|
|
34879
|
+
const value = doc.createElement("span");
|
|
34880
|
+
value.className = "vela-marks-field-value";
|
|
34881
|
+
value.textContent = item.value;
|
|
34882
|
+
row.append(label, value);
|
|
34883
|
+
section.appendChild(row);
|
|
34884
|
+
}
|
|
34885
|
+
}
|
|
34886
|
+
}
|
|
34887
|
+
}
|
|
34888
|
+
};
|
|
34889
|
+
function scheduleLazy(scroller, pending) {
|
|
34890
|
+
if (typeof IntersectionObserver === "undefined") {
|
|
34891
|
+
for (const p of pending) p.resolve();
|
|
34892
|
+
return;
|
|
34893
|
+
}
|
|
34894
|
+
const io = new IntersectionObserver(
|
|
34895
|
+
(entries) => {
|
|
34896
|
+
for (const e of entries) {
|
|
34897
|
+
if (!e.isIntersecting) continue;
|
|
34898
|
+
const p = pending.find((q) => q.el === e.target);
|
|
34899
|
+
if (!p) continue;
|
|
34900
|
+
io.unobserve(e.target);
|
|
34901
|
+
p.resolve();
|
|
34902
|
+
}
|
|
34903
|
+
},
|
|
34904
|
+
{ root: scroller }
|
|
34905
|
+
);
|
|
34906
|
+
for (const p of pending) io.observe(p.el);
|
|
34907
|
+
}
|
|
34908
|
+
|
|
33744
34909
|
// src/renderers/native/core/manualScale.ts
|
|
33745
34910
|
function rescaleAround(start, factor) {
|
|
33746
34911
|
if (start.log && start.min > 0 && start.max > start.min) {
|
|
@@ -34333,11 +35498,12 @@ var SCROLL_BTN_BOTTOM = TIME_AXIS_H + 14;
|
|
|
34333
35498
|
var SCROLL_BTN_PROXIMITY_PX = 120;
|
|
34334
35499
|
var MIN_VISIBLE_BARS = 2;
|
|
34335
35500
|
var ZOOM_OUT_MARGIN_BARS = 6;
|
|
34336
|
-
var
|
|
34337
|
-
var SCALE_TAU_MS = 80;
|
|
34338
|
-
var FLING_TAU_MS = 110;
|
|
34339
|
-
var SCROLL_TO_TAU_MS = 130;
|
|
35501
|
+
var INTRO_MODEL_FADE_MS = 350;
|
|
34340
35502
|
var FLING_STOP_PX = 0.02;
|
|
35503
|
+
var MARK_FLASH_MS = 220;
|
|
35504
|
+
function frameNow() {
|
|
35505
|
+
return typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
35506
|
+
}
|
|
34341
35507
|
var PRICE_SCALE_K = 4e-3;
|
|
34342
35508
|
var KEY_ZOOM_STEP = 0.2;
|
|
34343
35509
|
var SEPARATOR_HIT_PX = 4;
|
|
@@ -34386,9 +35552,20 @@ var NativeRenderer = class {
|
|
|
34386
35552
|
this.indicatorSlices = new IndicatorDrawingSlices();
|
|
34387
35553
|
/** Hover tooltips for Pine labels (canvas hit-rects collected by the chrome layer). */
|
|
34388
35554
|
this.labelTooltip = null;
|
|
35555
|
+
/** The timeline-mark popup (a kit Popover anchored on a lane glyph); null before mount. */
|
|
35556
|
+
this.markPopover = null;
|
|
35557
|
+
/** How the fanned mark stack was opened: a hover folds when the pointer leaves, a tap only on a tap elsewhere. */
|
|
35558
|
+
this.marksExpandedBy = "hover";
|
|
35559
|
+
/** The rAF loop keeping the chrome repainting while a lane glyph pulses (hover) or flashes (click); null when idle. */
|
|
35560
|
+
this.markPulseRaf = null;
|
|
35561
|
+
this.markClickCbs = /* @__PURE__ */ new Set();
|
|
34389
35562
|
this.crosshairLayer = new CrosshairRenderer();
|
|
34390
|
-
/**
|
|
34391
|
-
|
|
35563
|
+
/** The second pulse the countdown-to-bar-close chip ticks on: the host's (`setWallClock`)
|
|
35564
|
+
* when one is wired, else the renderer's own second-aligned clock. */
|
|
35565
|
+
this.hostClock = null;
|
|
35566
|
+
this.ownClock = null;
|
|
35567
|
+
/** Live subscription to the pulse while the countdown is on; null when off. */
|
|
35568
|
+
this.countdownUnsub = null;
|
|
34392
35569
|
this.symbolPicker = null;
|
|
34393
35570
|
/** Indicator titles (the legend rows) shown — held here so a remount re-applies it. */
|
|
34394
35571
|
this.indicatorTitlesOn = true;
|
|
@@ -34406,19 +35583,25 @@ var NativeRenderer = class {
|
|
|
34406
35583
|
/** Drawings layer self-serves Ctrl+Z/Y (see the `historyChords` feature). */
|
|
34407
35584
|
this.historyChordsEnabled = true;
|
|
34408
35585
|
this.liveRegion = null;
|
|
34409
|
-
// ── animation state
|
|
34410
|
-
|
|
34411
|
-
this.
|
|
34412
|
-
|
|
34413
|
-
|
|
34414
|
-
|
|
34415
|
-
|
|
35586
|
+
// ── animation state: one ease time-constant per motion (0 = off), each remembering the
|
|
35587
|
+
// host's duration so the config's on/off switches restore it (see EaseSetting) ──
|
|
35588
|
+
this.animZoom = new EaseSetting(ZOOM_EASE_DEFAULT_MS);
|
|
35589
|
+
// wheel-zoom glide
|
|
35590
|
+
this.animPan = new EaseSetting(PAN_INERTIA_DEFAULT_MS);
|
|
35591
|
+
// inertial-pan velocity decay
|
|
35592
|
+
this.animScroll = new EaseSetting(SCROLL_EASE_DEFAULT_MS);
|
|
35593
|
+
// scroll-to-latest / panBy glide
|
|
35594
|
+
this.animAutoscale = new EaseSetting(AUTOSCALE_EASE_DEFAULT_MS);
|
|
35595
|
+
// autoscale glide during zoom/fling
|
|
35596
|
+
this.animLiveBar = new EaseSetting(LIVE_BAR_EASE_DEFAULT_MS, 0);
|
|
35597
|
+
// forming-bar OHLC glide; ships off
|
|
34416
35598
|
// Brand default candles.
|
|
34417
35599
|
this.candleUp = BULLISH;
|
|
34418
35600
|
this.candleDown = BEARISH;
|
|
34419
35601
|
// ── intro reveal (plays once when candles first appear) ──
|
|
34420
|
-
this.
|
|
34421
|
-
|
|
35602
|
+
this.intro = { style: "settle", duration: INTRO_DURATION_DEFAULT_MS };
|
|
35603
|
+
this.introOnStyle = "settle";
|
|
35604
|
+
// the style the config's on/off switch restores
|
|
34422
35605
|
this.introPlayed = false;
|
|
34423
35606
|
this.introRaf = null;
|
|
34424
35607
|
/** The load affordance (three pulsing dots) — up while the host reports a bar load in
|
|
@@ -34537,7 +35720,7 @@ var NativeRenderer = class {
|
|
|
34537
35720
|
this.moveIndicatorCbs = /* @__PURE__ */ new Set();
|
|
34538
35721
|
this.priceStyleCbs = /* @__PURE__ */ new Set();
|
|
34539
35722
|
this.name = "native";
|
|
34540
|
-
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"];
|
|
35723
|
+
this.features = ["logScale", "currentPriceLine", "priceLabel", "countdown", "upColor", "downColor", "glow", "animZoom", "animPan", "animScroll", "animAutoscale", "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", "marks", "indicatorTitles", "indicatorValues"];
|
|
34541
35724
|
/** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
|
|
34542
35725
|
* so moving onto the button doesn't count as leaving). */
|
|
34543
35726
|
this.onScrollProximityMove = (e) => {
|
|
@@ -34569,9 +35752,12 @@ var NativeRenderer = class {
|
|
|
34569
35752
|
this.scene.showPriceLine = opts.currentPriceLine;
|
|
34570
35753
|
this.scene.logScale = opts.logScale;
|
|
34571
35754
|
this.backendMode = opts.nativeBackend;
|
|
34572
|
-
this.animZoom
|
|
34573
|
-
this.animPan
|
|
34574
|
-
this.
|
|
35755
|
+
this.animZoom.set(opts.animZoom);
|
|
35756
|
+
this.animPan.set(opts.animPan);
|
|
35757
|
+
this.animScroll.set(opts.animScroll);
|
|
35758
|
+
this.animAutoscale.set(opts.animAutoscale);
|
|
35759
|
+
this.animLiveBar.set(opts.animLiveBar);
|
|
35760
|
+
this.setIntro(opts.animIntro);
|
|
34575
35761
|
this.glowAmount = opts.glow;
|
|
34576
35762
|
this.candleUp = opts.upColor;
|
|
34577
35763
|
this.candleDown = opts.downColor;
|
|
@@ -34615,20 +35801,29 @@ var NativeRenderer = class {
|
|
|
34615
35801
|
if (this.backend && "glow" in this.backend) this.backend.glow = this.glowAmount;
|
|
34616
35802
|
break;
|
|
34617
35803
|
case "animZoom":
|
|
34618
|
-
this.animZoom
|
|
35804
|
+
this.animZoom.set(resolveEaseMs(value, ZOOM_EASE_DEFAULT_MS));
|
|
34619
35805
|
return;
|
|
34620
35806
|
// affects the next interaction only — nothing to repaint
|
|
34621
|
-
case "animPan":
|
|
34622
|
-
|
|
35807
|
+
case "animPan": {
|
|
35808
|
+
const ms = resolveEaseMs(value, PAN_INERTIA_DEFAULT_MS);
|
|
35809
|
+
this.animPan.set(ms);
|
|
35810
|
+
this.animScroll.toggle(ms > 0);
|
|
35811
|
+
return;
|
|
35812
|
+
}
|
|
35813
|
+
case "animScroll":
|
|
35814
|
+
this.animScroll.set(resolveEaseMs(value, SCROLL_EASE_DEFAULT_MS));
|
|
35815
|
+
return;
|
|
35816
|
+
case "animAutoscale":
|
|
35817
|
+
this.animAutoscale.set(resolveEaseMs(value, AUTOSCALE_EASE_DEFAULT_MS));
|
|
34623
35818
|
return;
|
|
35819
|
+
// a glide in flight finishes at the new rate (or snaps at 0)
|
|
34624
35820
|
case "animLiveBar":
|
|
34625
|
-
this.
|
|
35821
|
+
this.animLiveBar.set(resolveLiveBarEaseMs(value));
|
|
34626
35822
|
return;
|
|
34627
35823
|
// affects the next tick only; a glide in flight finishes at the new rate (or snaps at 0)
|
|
34628
35824
|
case "intro": {
|
|
34629
|
-
|
|
34630
|
-
this.
|
|
34631
|
-
if (s) this.playIntro(s);
|
|
35825
|
+
this.setIntro(resolveIntro(value));
|
|
35826
|
+
if (this.intro.style) this.playIntro();
|
|
34632
35827
|
return;
|
|
34633
35828
|
}
|
|
34634
35829
|
case "zoomAnchor":
|
|
@@ -34700,6 +35895,10 @@ var NativeRenderer = class {
|
|
|
34700
35895
|
case "tradeMarkers":
|
|
34701
35896
|
this.scene.tradeMarkers = mergeTradeMarkersState(this.scene.tradeMarkers, value);
|
|
34702
35897
|
break;
|
|
35898
|
+
case "marks":
|
|
35899
|
+
this.scene.marks = mergeMarksState(this.scene.marks, value);
|
|
35900
|
+
this.markPopover?.close();
|
|
35901
|
+
break;
|
|
34703
35902
|
case "keyboard":
|
|
34704
35903
|
this.setKeyboardEnabled(Boolean(value));
|
|
34705
35904
|
return;
|
|
@@ -34758,13 +35957,17 @@ var NativeRenderer = class {
|
|
|
34758
35957
|
case "glow":
|
|
34759
35958
|
return this.glowAmount;
|
|
34760
35959
|
case "animZoom":
|
|
34761
|
-
return this.animZoom;
|
|
35960
|
+
return this.animZoom.tau;
|
|
34762
35961
|
case "animPan":
|
|
34763
|
-
return this.animPan;
|
|
35962
|
+
return this.animPan.tau;
|
|
35963
|
+
case "animScroll":
|
|
35964
|
+
return this.animScroll.tau;
|
|
35965
|
+
case "animAutoscale":
|
|
35966
|
+
return this.animAutoscale.tau;
|
|
34764
35967
|
case "animLiveBar":
|
|
34765
|
-
return this.
|
|
35968
|
+
return this.animLiveBar.tau;
|
|
34766
35969
|
case "intro":
|
|
34767
|
-
return this.
|
|
35970
|
+
return this.intro.style;
|
|
34768
35971
|
case "zoomAnchor":
|
|
34769
35972
|
return this.zoomAnchorMode;
|
|
34770
35973
|
case "axisDrag":
|
|
@@ -34807,6 +36010,8 @@ var NativeRenderer = class {
|
|
|
34807
36010
|
}
|
|
34808
36011
|
case "tradeMarkers":
|
|
34809
36012
|
return { ...this.scene.tradeMarkers, colors: { ...this.scene.tradeMarkers.colors } };
|
|
36013
|
+
case "marks":
|
|
36014
|
+
return { visible: this.scene.marks.visible, groups: { ...this.scene.marks.groups } };
|
|
34810
36015
|
case "keyboard":
|
|
34811
36016
|
return this.keyboardEnabled;
|
|
34812
36017
|
case "historyChords":
|
|
@@ -34932,7 +36137,13 @@ var NativeRenderer = class {
|
|
|
34932
36137
|
currentPriceLine: this.scene.showPriceLine,
|
|
34933
36138
|
priceLabel: this.scene.showPriceLabel,
|
|
34934
36139
|
countdown: this.scene.showCountdown,
|
|
34935
|
-
animateLastPrice: this.
|
|
36140
|
+
animateLastPrice: this.animLiveBar.on
|
|
36141
|
+
},
|
|
36142
|
+
animations: {
|
|
36143
|
+
zoom: this.animZoom.on,
|
|
36144
|
+
pan: this.animPan.on,
|
|
36145
|
+
autoscale: this.animAutoscale.on,
|
|
36146
|
+
intro: this.intro.style !== false
|
|
34936
36147
|
},
|
|
34937
36148
|
panes: { separatorColor: s.separatorColor ?? t.borderColor },
|
|
34938
36149
|
trades: {
|
|
@@ -34944,6 +36155,7 @@ var NativeRenderer = class {
|
|
|
34944
36155
|
exitColor: this.scene.tradeMarkers.colors.exit
|
|
34945
36156
|
},
|
|
34946
36157
|
timeScale: { timezone: this.scene.timezone },
|
|
36158
|
+
marks: { visible: this.scene.marks.visible, groups: { ...this.scene.marks.groups } },
|
|
34947
36159
|
candles: {
|
|
34948
36160
|
upColor: this.candleUp,
|
|
34949
36161
|
downColor: this.candleDown,
|
|
@@ -35039,7 +36251,12 @@ var NativeRenderer = class {
|
|
|
35039
36251
|
this.scene.showPriceLabel = next.priceScale.priceLabel;
|
|
35040
36252
|
this.scene.showCountdown = next.priceScale.countdown;
|
|
35041
36253
|
this.syncCountdownTimer();
|
|
35042
|
-
this.
|
|
36254
|
+
this.animLiveBar.toggle(next.priceScale.animateLastPrice);
|
|
36255
|
+
this.animZoom.toggle(next.animations.zoom);
|
|
36256
|
+
this.animPan.toggle(next.animations.pan);
|
|
36257
|
+
this.animScroll.toggle(next.animations.pan);
|
|
36258
|
+
this.animAutoscale.toggle(next.animations.autoscale);
|
|
36259
|
+
this.intro = { style: next.animations.intro ? this.introOnStyle : false, duration: this.intro.duration || INTRO_DURATION_DEFAULT_MS };
|
|
35043
36260
|
s.separatorColor = keepInherit(s.separatorColor, next.panes.separatorColor, prevTheme.borderColor);
|
|
35044
36261
|
this.scene.tradeMarkers = {
|
|
35045
36262
|
visible: next.trades.visible,
|
|
@@ -35048,6 +36265,7 @@ var NativeRenderer = class {
|
|
|
35048
36265
|
colors: { long: next.trades.longColor, short: next.trades.shortColor, exit: next.trades.exitColor }
|
|
35049
36266
|
};
|
|
35050
36267
|
this.scene.timezone = next.timeScale.timezone;
|
|
36268
|
+
this.scene.marks = { visible: next.marks.visible, groups: { ...next.marks.groups } };
|
|
35051
36269
|
this.candleUp = next.candles.upColor;
|
|
35052
36270
|
this.candleDown = next.candles.downColor;
|
|
35053
36271
|
s.candle = {
|
|
@@ -35180,6 +36398,7 @@ var NativeRenderer = class {
|
|
|
35180
36398
|
}
|
|
35181
36399
|
this.settingsDialog.setTheme(this.theme);
|
|
35182
36400
|
this.settingsDialog.setHostSections(this.hostSettingsSections);
|
|
36401
|
+
this.settingsDialog.setMarkGroups(this.markGroupsInUse(), (id) => markGroupVisible(this.scene.marks, id, this.scene.markGroups));
|
|
35183
36402
|
this.settingsDialog.setHiddenSettings(this.hiddenSettings);
|
|
35184
36403
|
this.syncThemeControl();
|
|
35185
36404
|
this.settingsDialog.toggle(
|
|
@@ -35270,10 +36489,10 @@ var NativeRenderer = class {
|
|
|
35270
36489
|
this.glideRightOffset(ZOOM_OUT_MARGIN_BARS);
|
|
35271
36490
|
}
|
|
35272
36491
|
/** Ease rightOffset to `target` at constant zoom (see animTick's scroll glide);
|
|
35273
|
-
* instant when
|
|
36492
|
+
* instant when the scroll glide is off. Shared by scroll-to-latest and panBy. */
|
|
35274
36493
|
glideRightOffset(target) {
|
|
35275
36494
|
const vp = this.coords.getViewport();
|
|
35276
|
-
if (!this.
|
|
36495
|
+
if (!this.animScroll.on) {
|
|
35277
36496
|
this.applyViewport({ barSpacing: vp.barSpacing, rightOffset: target });
|
|
35278
36497
|
return;
|
|
35279
36498
|
}
|
|
@@ -35338,20 +36557,21 @@ var NativeRenderer = class {
|
|
|
35338
36557
|
* full size, eased, with a left→right stagger so the chart draws itself; `settle`
|
|
35339
36558
|
* adds an ease-out-back overshoot. Autoscale stays on the real bars so the frame
|
|
35340
36559
|
* never moves. Re-callable, so styles can be compared live from the console.
|
|
36560
|
+
* Style and sweep duration come from the resolved `intro` setting.
|
|
35341
36561
|
*/
|
|
35342
|
-
playIntro(
|
|
36562
|
+
playIntro() {
|
|
35343
36563
|
if (this.introRaf != null) cancelAnimationFrame(this.introRaf);
|
|
35344
36564
|
this.introRaf = null;
|
|
36565
|
+
const { style, duration } = this.intro;
|
|
35345
36566
|
const real = this.bars;
|
|
35346
36567
|
const n = real.length;
|
|
35347
|
-
if (n === 0) return;
|
|
36568
|
+
if (n === 0 || !style) return;
|
|
35348
36569
|
this.computeScales();
|
|
35349
36570
|
for (const pane of this.scene.panes.values()) pane.scale = { ...pane.scaleTarget };
|
|
35350
36571
|
this.modelAlpha = 0;
|
|
35351
|
-
const DURATION = 650;
|
|
35352
36572
|
const start = performance.now();
|
|
35353
36573
|
const step = (now) => {
|
|
35354
|
-
const p = Math.min(1, (now - start) /
|
|
36574
|
+
const p = Math.min(1, (now - start) / duration);
|
|
35355
36575
|
this.scene.bars = p >= 1 ? real : real.map((b, i) => this.revealCandle(b, i, p, n, style));
|
|
35356
36576
|
this.paintData();
|
|
35357
36577
|
if (p < 1) {
|
|
@@ -35365,10 +36585,10 @@ var NativeRenderer = class {
|
|
|
35365
36585
|
}
|
|
35366
36586
|
/** After the candle reveal, fade the indicator models (series/fills/…) from hidden to full. */
|
|
35367
36587
|
fadeInModels() {
|
|
35368
|
-
const
|
|
36588
|
+
const fade = Math.min(INTRO_MODEL_FADE_MS, this.intro.duration || INTRO_MODEL_FADE_MS);
|
|
35369
36589
|
const start = performance.now();
|
|
35370
36590
|
const step = (now) => {
|
|
35371
|
-
this.modelAlpha = Math.min(1, (now - start) /
|
|
36591
|
+
this.modelAlpha = Math.min(1, (now - start) / fade);
|
|
35372
36592
|
this.paintData();
|
|
35373
36593
|
if (this.modelAlpha < 1) {
|
|
35374
36594
|
this.introRaf = requestAnimationFrame(step);
|
|
@@ -35483,7 +36703,8 @@ var NativeRenderer = class {
|
|
|
35483
36703
|
zoomTo: (target, anchorLogical, anchorX) => this.zoomTo(target, anchorLogical, anchorX),
|
|
35484
36704
|
fling: (v) => this.fling(v),
|
|
35485
36705
|
onPointerMove: (x, y) => this.handlePointerMove(x, y),
|
|
35486
|
-
onClick: (x) => {
|
|
36706
|
+
onClick: (x, y) => {
|
|
36707
|
+
if (this.handleMarkClick(x, y)) return;
|
|
35487
36708
|
this.userDrawings?.deselect();
|
|
35488
36709
|
this.handleClick(x);
|
|
35489
36710
|
},
|
|
@@ -35512,7 +36733,7 @@ var NativeRenderer = class {
|
|
|
35512
36733
|
drawingsPointerDown: (x, y, snap, shift2, mod) => this.userDrawings?.pointerDown(x, y, snap, shift2, mod),
|
|
35513
36734
|
drawingsPointerMove: (x, y, snap, shift2, mod) => this.userDrawings?.pointerMove(x, y, snap, shift2, mod),
|
|
35514
36735
|
drawingsPointerUp: (x, y, snap) => this.userDrawings?.pointerUp(x, y, snap),
|
|
35515
|
-
drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? null,
|
|
36736
|
+
drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? (this.chrome.markGlyphAt(x, y) ? "pointer" : null),
|
|
35516
36737
|
drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
|
|
35517
36738
|
drawingsClearTransient: () => this.userDrawings?.clearTransient()
|
|
35518
36739
|
});
|
|
@@ -35528,8 +36749,18 @@ var NativeRenderer = class {
|
|
|
35528
36749
|
this.plot.addEventListener("pointerleave", this.onScrollProximityLeave);
|
|
35529
36750
|
this.labelTooltip = new LabelTooltip(this.plot, {
|
|
35530
36751
|
theme: () => this.chromeTheme(),
|
|
35531
|
-
lookup: (x, y) => this.indicatorSlices.labelTooltipAt(x, y)
|
|
36752
|
+
lookup: (x, y) => this.indicatorSlices.labelTooltipAt(x, y) ?? this.chrome.markTooltipAt(x, y, this.markGroupsInUse())
|
|
36753
|
+
});
|
|
36754
|
+
this.markPopover = new MarkPopover({
|
|
36755
|
+
plot: this.plot,
|
|
36756
|
+
host: () => this.dialogHost ?? this.plot,
|
|
36757
|
+
theme: () => this.chromeTheme(),
|
|
36758
|
+
onOpenChange: (key) => {
|
|
36759
|
+
this.scene.marksActiveKey = key;
|
|
36760
|
+
this.scheduler?.invalidate(2 /* Chrome */);
|
|
36761
|
+
}
|
|
35532
36762
|
});
|
|
36763
|
+
this.chrome.setMarkIconReady(() => this.scheduler?.invalidate(2 /* Chrome */));
|
|
35533
36764
|
this.userDrawings = new UserDrawingController(this.wrapper, this.plot, this.drawingsCanvas, {
|
|
35534
36765
|
projector: () => this.drawingProjector(),
|
|
35535
36766
|
dpr: () => this.coords.dpr,
|
|
@@ -35744,29 +36975,40 @@ var NativeRenderer = class {
|
|
|
35744
36975
|
resize() {
|
|
35745
36976
|
this.syncSize();
|
|
35746
36977
|
}
|
|
35747
|
-
/**
|
|
35748
|
-
|
|
36978
|
+
/** Drive the countdown chip from the host's second pulse (`null` → the renderer's own). */
|
|
36979
|
+
setWallClock(clock) {
|
|
36980
|
+
if (clock === this.hostClock) return;
|
|
36981
|
+
this.hostClock = clock;
|
|
36982
|
+
if (this.countdownUnsub != null) {
|
|
36983
|
+
this.countdownUnsub();
|
|
36984
|
+
this.countdownUnsub = null;
|
|
36985
|
+
}
|
|
36986
|
+
this.syncCountdownTimer();
|
|
36987
|
+
}
|
|
36988
|
+
/** Subscribe to the second pulse while the countdown chip is on (so it ticks); unsubscribe
|
|
36989
|
+
* otherwise. Chrome tier: only the chip's text moves — an idle chart must not recompute
|
|
35749
36990
|
* scales or repaint the geometry/volume/VPVR/SDK layers once a second (that cost
|
|
35750
36991
|
* multiplies by the cell count in a multi-chart workspace). */
|
|
35751
36992
|
syncCountdownTimer() {
|
|
35752
36993
|
if (this.scene.showCountdown) {
|
|
35753
|
-
if (this.
|
|
35754
|
-
this.
|
|
36994
|
+
if (this.countdownUnsub == null) {
|
|
36995
|
+
const clock = this.hostClock ?? (this.ownClock ?? (this.ownClock = new SecondClock()));
|
|
36996
|
+
this.countdownUnsub = clock.onTick(() => {
|
|
35755
36997
|
if (this.scene.showCountdown && this.scene.bars.length > 0) this.scheduler?.invalidate(2 /* Chrome */);
|
|
35756
|
-
}
|
|
36998
|
+
});
|
|
35757
36999
|
}
|
|
35758
|
-
} else if (this.
|
|
35759
|
-
|
|
35760
|
-
this.
|
|
37000
|
+
} else if (this.countdownUnsub != null) {
|
|
37001
|
+
this.countdownUnsub();
|
|
37002
|
+
this.countdownUnsub = null;
|
|
35761
37003
|
}
|
|
35762
37004
|
}
|
|
35763
37005
|
destroy() {
|
|
35764
37006
|
if (this.introRaf != null) cancelAnimationFrame(this.introRaf);
|
|
35765
37007
|
this.loadingEl?.remove();
|
|
35766
37008
|
this.loadingEl = null;
|
|
35767
|
-
if (this.
|
|
35768
|
-
|
|
35769
|
-
this.
|
|
37009
|
+
if (this.countdownUnsub != null) {
|
|
37010
|
+
this.countdownUnsub();
|
|
37011
|
+
this.countdownUnsub = null;
|
|
35770
37012
|
}
|
|
35771
37013
|
this.scheduler?.destroy();
|
|
35772
37014
|
this.animator?.stop();
|
|
@@ -35791,6 +37033,11 @@ var NativeRenderer = class {
|
|
|
35791
37033
|
this.plot?.removeEventListener("pointerleave", this.onScrollProximityLeave);
|
|
35792
37034
|
this.labelTooltip?.destroy();
|
|
35793
37035
|
this.labelTooltip = null;
|
|
37036
|
+
this.markPopover?.destroy();
|
|
37037
|
+
this.markPopover = null;
|
|
37038
|
+
this.chrome.setMarkIconReady(null);
|
|
37039
|
+
if (this.markPulseRaf !== null) cancelAnimationFrame(this.markPulseRaf);
|
|
37040
|
+
this.markPulseRaf = null;
|
|
35794
37041
|
this.scrollButton?.remove();
|
|
35795
37042
|
this.scrollButton = null;
|
|
35796
37043
|
for (const l of this.extLayers) l.instance.destroy?.();
|
|
@@ -35849,8 +37096,8 @@ var NativeRenderer = class {
|
|
|
35849
37096
|
}
|
|
35850
37097
|
if (!this.introPlayed && this.bars.length > 0) {
|
|
35851
37098
|
this.introPlayed = true;
|
|
35852
|
-
if (this.
|
|
35853
|
-
this.playIntro(
|
|
37099
|
+
if (this.intro.style) {
|
|
37100
|
+
this.playIntro();
|
|
35854
37101
|
return;
|
|
35855
37102
|
}
|
|
35856
37103
|
}
|
|
@@ -35861,7 +37108,7 @@ var NativeRenderer = class {
|
|
|
35861
37108
|
const last = this.bars[n - 1];
|
|
35862
37109
|
if (last && bar.time === last.time) {
|
|
35863
37110
|
this.bars[n - 1] = bar;
|
|
35864
|
-
if (this.
|
|
37111
|
+
if (!this.animLiveBar.on || this.liveEaseTime !== bar.time) {
|
|
35865
37112
|
this.syncLiveEase(bar);
|
|
35866
37113
|
} else {
|
|
35867
37114
|
this.animator.start();
|
|
@@ -35877,11 +37124,11 @@ var NativeRenderer = class {
|
|
|
35877
37124
|
this.scene.bars = this.bars;
|
|
35878
37125
|
this.scheduler.invalidate(4 /* Full */);
|
|
35879
37126
|
}
|
|
35880
|
-
/** Set the
|
|
35881
|
-
*
|
|
35882
|
-
|
|
35883
|
-
this.
|
|
35884
|
-
if (
|
|
37127
|
+
/** Set the reveal (style + duration). A non-off style is also remembered as what the
|
|
37128
|
+
* config's on/off toggle (`animations.intro`) switches back on to. */
|
|
37129
|
+
setIntro(next) {
|
|
37130
|
+
this.intro = next;
|
|
37131
|
+
if (next.style) this.introOnStyle = next.style;
|
|
35885
37132
|
}
|
|
35886
37133
|
/** Snap the eased forming-bar state to `bar` — no glide (a fresh bar or the first tick of one). */
|
|
35887
37134
|
syncLiveEase(bar) {
|
|
@@ -35895,7 +37142,7 @@ var NativeRenderer = class {
|
|
|
35895
37142
|
const target = this.bars[this.bars.length - 1];
|
|
35896
37143
|
if (!target || this.liveEaseTime !== target.time) return false;
|
|
35897
37144
|
const eps = Math.max(1e-9, Math.abs(target.close) * 1e-6);
|
|
35898
|
-
const tau = this.
|
|
37145
|
+
const tau = this.animLiveBar.tau;
|
|
35899
37146
|
const nh = easeToward(this.liveEaseHigh, target.high, dtMs, tau);
|
|
35900
37147
|
const nl = easeToward(this.liveEaseLow, target.low, dtMs, tau);
|
|
35901
37148
|
const nc = easeToward(this.liveEaseClose, target.close, dtMs, tau);
|
|
@@ -36035,10 +37282,12 @@ var NativeRenderer = class {
|
|
|
36035
37282
|
this.refreshAnchorOffset(model);
|
|
36036
37283
|
if (model.native && this.extLayers.some((l) => l.def.id === model.native.type)) this.scene.assignIndicatorZTop(model.id);
|
|
36037
37284
|
else this.scene.assignIndicatorZ(model.id);
|
|
36038
|
-
|
|
36039
|
-
|
|
36040
|
-
|
|
36041
|
-
|
|
37285
|
+
if (model.legend !== false) {
|
|
37286
|
+
this.inputsUI.upsert(model.id, model.shorttitle ?? model.title, model.inputs, model.inputValues, model.paneId, {
|
|
37287
|
+
native: !!model.native,
|
|
37288
|
+
...model.props ? { props: model.props, propValues: model.propValues ?? {} } : {}
|
|
37289
|
+
});
|
|
37290
|
+
}
|
|
36042
37291
|
if (model.native?.type === "volume") {
|
|
36043
37292
|
this.volumeActive = true;
|
|
36044
37293
|
this.volumeHidden = false;
|
|
@@ -36185,7 +37434,7 @@ var NativeRenderer = class {
|
|
|
36185
37434
|
this.settingsDialog?.setHiddenSettings(this.hiddenSettings);
|
|
36186
37435
|
}
|
|
36187
37436
|
listSettingsIds() {
|
|
36188
|
-
return settingsIdCatalog(this.hostSettingsSections);
|
|
37437
|
+
return settingsIdCatalog(this.hostSettingsSections, this.markGroupsInUse());
|
|
36189
37438
|
}
|
|
36190
37439
|
onChartTypeSettingsChange(cb) {
|
|
36191
37440
|
this.chartTypeSettingsCbs.add(cb);
|
|
@@ -36211,6 +37460,22 @@ var NativeRenderer = class {
|
|
|
36211
37460
|
this.axisLongPressCbs.add(cb);
|
|
36212
37461
|
return () => this.axisLongPressCbs.delete(cb);
|
|
36213
37462
|
}
|
|
37463
|
+
// ── timeline marks (the `chart.marks` model; see the port) ──
|
|
37464
|
+
setTimelineMarks(marks, groups) {
|
|
37465
|
+
this.scene.timelineMarks = marks;
|
|
37466
|
+
this.scene.markGroups = groups;
|
|
37467
|
+
this.scene.marksExpandedStack = null;
|
|
37468
|
+
this.markPopover?.close();
|
|
37469
|
+
this.scheduler?.invalidate(2 /* Chrome */);
|
|
37470
|
+
}
|
|
37471
|
+
onMarkClick(cb) {
|
|
37472
|
+
this.markClickCbs.add(cb);
|
|
37473
|
+
return () => this.markClickCbs.delete(cb);
|
|
37474
|
+
}
|
|
37475
|
+
/** Every group the lane knows: the defined ones, then those marks name without a definition. */
|
|
37476
|
+
markGroupsInUse() {
|
|
37477
|
+
return effectiveMarkGroups(this.scene.timelineMarks, this.scene.markGroups);
|
|
37478
|
+
}
|
|
36214
37479
|
onViewportChange(cb) {
|
|
36215
37480
|
this.viewportCbs.add(cb);
|
|
36216
37481
|
return () => this.viewportCbs.delete(cb);
|
|
@@ -36264,7 +37529,7 @@ var NativeRenderer = class {
|
|
|
36264
37529
|
this.zoomAnchorX = anchorX;
|
|
36265
37530
|
this.panVelocity = 0;
|
|
36266
37531
|
this.scrollTargetRO = null;
|
|
36267
|
-
if (!this.animZoom) {
|
|
37532
|
+
if (!this.animZoom.on) {
|
|
36268
37533
|
const v = this.clampViewport(barSpacing, this.anchoredRightOffset(barSpacing));
|
|
36269
37534
|
this.coords.setViewport(v);
|
|
36270
37535
|
this.targetBarSpacing = v.barSpacing;
|
|
@@ -36277,7 +37542,7 @@ var NativeRenderer = class {
|
|
|
36277
37542
|
}
|
|
36278
37543
|
/** Inertial pan: continue with a rightOffset velocity (logical units / ms) that decays. */
|
|
36279
37544
|
fling(velocity) {
|
|
36280
|
-
if (!this.animPan) return;
|
|
37545
|
+
if (!this.animPan.on) return;
|
|
36281
37546
|
this.scrollTargetRO = null;
|
|
36282
37547
|
this.panVelocity = velocity;
|
|
36283
37548
|
this.animator.start();
|
|
@@ -36317,7 +37582,7 @@ var NativeRenderer = class {
|
|
|
36317
37582
|
let active = false;
|
|
36318
37583
|
const tbs = this.targetBarSpacing;
|
|
36319
37584
|
if (Math.abs(barSpacing - tbs) > tbs * 1e-3) {
|
|
36320
|
-
barSpacing = clampBarSpacing(easeToward(barSpacing, tbs, dtMs,
|
|
37585
|
+
barSpacing = clampBarSpacing(easeToward(barSpacing, tbs, dtMs, this.animZoom.tau));
|
|
36321
37586
|
rightOffset = this.anchoredRightOffset(barSpacing);
|
|
36322
37587
|
active = true;
|
|
36323
37588
|
} else if (barSpacing !== tbs) {
|
|
@@ -36327,13 +37592,14 @@ var NativeRenderer = class {
|
|
|
36327
37592
|
const stopVel = FLING_STOP_PX / Math.max(1e-6, barSpacing * this.coords.spacingScale);
|
|
36328
37593
|
if (Math.abs(this.panVelocity) > stopVel) {
|
|
36329
37594
|
rightOffset += this.panVelocity * dtMs;
|
|
36330
|
-
|
|
37595
|
+
const tau = this.animPan.tau;
|
|
37596
|
+
this.panVelocity = tau > 0 ? this.panVelocity * Math.exp(-dtMs / tau) : 0;
|
|
36331
37597
|
if (Math.abs(this.panVelocity) <= stopVel) this.panVelocity = 0;
|
|
36332
37598
|
else active = true;
|
|
36333
37599
|
}
|
|
36334
37600
|
if (this.scrollTargetRO != null) {
|
|
36335
37601
|
const target = this.scrollTargetRO;
|
|
36336
|
-
const next = easeToward(rightOffset, target, dtMs,
|
|
37602
|
+
const next = easeToward(rightOffset, target, dtMs, this.animScroll.tau);
|
|
36337
37603
|
if (Math.abs(next - target) < 1e-3) {
|
|
36338
37604
|
rightOffset = target;
|
|
36339
37605
|
this.scrollTargetRO = null;
|
|
@@ -36361,6 +37627,7 @@ var NativeRenderer = class {
|
|
|
36361
37627
|
* through Math.log), not a non-linear jump. */
|
|
36362
37628
|
easeScales(dtMs) {
|
|
36363
37629
|
let moving = false;
|
|
37630
|
+
const tau = this.animAutoscale.tau;
|
|
36364
37631
|
for (const pane of this.scene.panes.values()) {
|
|
36365
37632
|
const t = pane.scaleTarget;
|
|
36366
37633
|
const s = pane.scale;
|
|
@@ -36368,8 +37635,8 @@ var NativeRenderer = class {
|
|
|
36368
37635
|
const lt0 = Math.log(t.min);
|
|
36369
37636
|
const lt1 = Math.log(t.max);
|
|
36370
37637
|
const lspan = Math.max(1e-9, Math.abs(lt1 - lt0));
|
|
36371
|
-
const n0 = easeToward(Math.log(s.min), lt0, dtMs,
|
|
36372
|
-
const n1 = easeToward(Math.log(s.max), lt1, dtMs,
|
|
37638
|
+
const n0 = easeToward(Math.log(s.min), lt0, dtMs, tau);
|
|
37639
|
+
const n1 = easeToward(Math.log(s.max), lt1, dtMs, tau);
|
|
36373
37640
|
if (Math.abs(n0 - lt0) <= lspan * 1e-3 && Math.abs(n1 - lt1) <= lspan * 1e-3) {
|
|
36374
37641
|
pane.scale = { min: t.min, max: t.max, log: true };
|
|
36375
37642
|
} else {
|
|
@@ -36379,8 +37646,8 @@ var NativeRenderer = class {
|
|
|
36379
37646
|
continue;
|
|
36380
37647
|
}
|
|
36381
37648
|
const span = Math.max(1e-9, Math.abs(t.max - t.min));
|
|
36382
|
-
let nmin = easeToward(s.min, t.min, dtMs,
|
|
36383
|
-
let nmax = easeToward(s.max, t.max, dtMs,
|
|
37649
|
+
let nmin = easeToward(s.min, t.min, dtMs, tau);
|
|
37650
|
+
let nmax = easeToward(s.max, t.max, dtMs, tau);
|
|
36384
37651
|
if (Math.abs(nmin - t.min) <= span * 1e-3 && Math.abs(nmax - t.max) <= span * 1e-3) {
|
|
36385
37652
|
nmin = t.min;
|
|
36386
37653
|
nmax = t.max;
|
|
@@ -36393,8 +37660,8 @@ var NativeRenderer = class {
|
|
|
36393
37660
|
const t = sl.scaleTarget;
|
|
36394
37661
|
const s = sl.scale;
|
|
36395
37662
|
const span = Math.max(1e-9, Math.abs(t.max - t.min));
|
|
36396
|
-
let nmin = easeToward(s.min, t.min, dtMs,
|
|
36397
|
-
let nmax = easeToward(s.max, t.max, dtMs,
|
|
37663
|
+
let nmin = easeToward(s.min, t.min, dtMs, tau);
|
|
37664
|
+
let nmax = easeToward(s.max, t.max, dtMs, tau);
|
|
36398
37665
|
if (Math.abs(nmin - t.min) <= span * 1e-3 && Math.abs(nmax - t.max) <= span * 1e-3) {
|
|
36399
37666
|
nmin = t.min;
|
|
36400
37667
|
nmax = t.max;
|
|
@@ -36415,6 +37682,8 @@ var NativeRenderer = class {
|
|
|
36415
37682
|
this.scene.crosshair = null;
|
|
36416
37683
|
this.hoverSeparatorY = null;
|
|
36417
37684
|
this.lastPointer = null;
|
|
37685
|
+
if (this.marksExpandedBy === "hover") this.setMarksExpanded(null);
|
|
37686
|
+
this.setMarkHover(null);
|
|
36418
37687
|
this.scheduler.invalidate(1 /* Cursor */);
|
|
36419
37688
|
this.hoverLogical = null;
|
|
36420
37689
|
const empty = { time: null, price: null, paneKind: null, values: /* @__PURE__ */ new Map(), ohlc: null };
|
|
@@ -36426,6 +37695,8 @@ var NativeRenderer = class {
|
|
|
36426
37695
|
this.lastPointer = inData ? { x, y } : null;
|
|
36427
37696
|
this.hoverSeparatorY = x >= 0 && y >= 0 && y <= this.coords.height ? this.separatorHoverY(y) : null;
|
|
36428
37697
|
this.scheduler.invalidate(1 /* Cursor */);
|
|
37698
|
+
this.setMarksExpanded(inData ? this.chrome.markStackAt(x, y) : null);
|
|
37699
|
+
this.setMarkHover(inData ? this.chrome.markGlyphAt(x, y)?.cluster.key ?? null : null);
|
|
36429
37700
|
const logical = Math.round(this.coords.xToLogical(x));
|
|
36430
37701
|
const onBar = logical >= 0 && logical < this.coords.barCount;
|
|
36431
37702
|
const time = onBar ? this.coords.logicalToTime(logical) : null;
|
|
@@ -36455,6 +37726,76 @@ var NativeRenderer = class {
|
|
|
36455
37726
|
const onBar = logical >= 0 && logical < this.coords.barCount;
|
|
36456
37727
|
for (const cb of this.clickCbs) cb({ time: onBar ? this.coords.logicalToTime(logical) : null, price: null });
|
|
36457
37728
|
}
|
|
37729
|
+
/**
|
|
37730
|
+
* Fan out (or collapse, with null) a multi-group mark stack; repaints the chrome tier when
|
|
37731
|
+
* it changes. A stack whose glyph holds the open popup stays fanned — the pointer leaving
|
|
37732
|
+
* the plot for the popup must not bury the glyph under the deck (which would close it).
|
|
37733
|
+
*/
|
|
37734
|
+
setMarksExpanded(stack, by = "hover") {
|
|
37735
|
+
if (this.scene.marksExpandedStack === stack) return;
|
|
37736
|
+
if (stack === null && this.markPopover?.key) {
|
|
37737
|
+
const open2 = this.chrome.markGlyphByKey(this.markPopover.key);
|
|
37738
|
+
if (open2 && open2.stack === this.scene.marksExpandedStack) return;
|
|
37739
|
+
}
|
|
37740
|
+
this.scene.marksExpandedStack = stack;
|
|
37741
|
+
this.marksExpandedBy = by;
|
|
37742
|
+
this.scheduler?.invalidate(2 /* Chrome */);
|
|
37743
|
+
}
|
|
37744
|
+
/**
|
|
37745
|
+
* A click on the mark lane: a collapsed deck fans out (the touch path — a mouse already
|
|
37746
|
+
* fanned it by hovering), a glyph reports its cluster (`onMarkClick`) and opens the popup
|
|
37747
|
+
* when any of its marks carries content. True when the click landed on the lane.
|
|
37748
|
+
*/
|
|
37749
|
+
handleMarkClick(x, y) {
|
|
37750
|
+
const glyph = this.chrome.markGlyphAt(x, y);
|
|
37751
|
+
if (!glyph) {
|
|
37752
|
+
if (this.scene.marksExpandedStack !== null && this.chrome.markStackAt(x, y) === null) this.setMarksExpanded(null);
|
|
37753
|
+
return false;
|
|
37754
|
+
}
|
|
37755
|
+
if (glyph.decked) {
|
|
37756
|
+
this.setMarksExpanded(glyph.stack, "tap");
|
|
37757
|
+
return true;
|
|
37758
|
+
}
|
|
37759
|
+
const marks = glyph.cluster.marks;
|
|
37760
|
+
const first = marks[0];
|
|
37761
|
+
const event = { id: first.id, ids: marks.map((m) => m.id), time: first.time, ...glyph.cluster.group !== void 0 ? { group: glyph.cluster.group } : {} };
|
|
37762
|
+
for (const cb of this.markClickCbs) cb(event);
|
|
37763
|
+
if (marks.some((m) => m.content !== void 0)) {
|
|
37764
|
+
this.markPopover?.open(glyph.cluster, { x: glyph.x, y: glyph.y, size: glyph.size });
|
|
37765
|
+
} else {
|
|
37766
|
+
this.scene.marksFlash = { key: glyph.cluster.key, until: frameNow() + MARK_FLASH_MS };
|
|
37767
|
+
this.syncMarkPulse();
|
|
37768
|
+
}
|
|
37769
|
+
return true;
|
|
37770
|
+
}
|
|
37771
|
+
/** After a chrome frame: keep the open mark popup on its glyph, or close it once the glyph is gone. */
|
|
37772
|
+
trackMarkPopover() {
|
|
37773
|
+
const key = this.markPopover?.key;
|
|
37774
|
+
if (!key) return;
|
|
37775
|
+
const g = this.chrome.markGlyphByKey(key);
|
|
37776
|
+
this.markPopover.track(g && !(g.decked && g.depth !== 0) ? { x: g.x, y: g.y, size: g.size } : null);
|
|
37777
|
+
}
|
|
37778
|
+
/** The lane glyph under the pointer — it swells once as the pointer lands (a rAF-driven chrome repaint for the pulse's duration). */
|
|
37779
|
+
setMarkHover(key) {
|
|
37780
|
+
if (this.scene.marksHoverKey === key) return;
|
|
37781
|
+
this.scene.marksHoverKey = key;
|
|
37782
|
+
this.scene.marksHoverSince = frameNow();
|
|
37783
|
+
this.scheduler?.invalidate(2 /* Chrome */);
|
|
37784
|
+
this.syncMarkPulse();
|
|
37785
|
+
}
|
|
37786
|
+
/** Run a chrome-tier repaint loop while a glyph's hover pulse or click flash plays; it stops itself once both are over. */
|
|
37787
|
+
syncMarkPulse() {
|
|
37788
|
+
if (this.markPulseRaf !== null || typeof requestAnimationFrame !== "function") return;
|
|
37789
|
+
const tick = () => {
|
|
37790
|
+
this.markPulseRaf = null;
|
|
37791
|
+
const now = frameNow();
|
|
37792
|
+
if (this.scene.marksFlash && this.scene.marksFlash.until <= now) this.scene.marksFlash = null;
|
|
37793
|
+
this.scheduler?.invalidate(2 /* Chrome */);
|
|
37794
|
+
const pulsing = this.scene.marksHoverKey !== null && now - this.scene.marksHoverSince < MARK_PULSE_MS;
|
|
37795
|
+
if (pulsing || this.scene.marksFlash !== null) this.markPulseRaf = requestAnimationFrame(tick);
|
|
37796
|
+
};
|
|
37797
|
+
this.markPulseRaf = requestAnimationFrame(tick);
|
|
37798
|
+
}
|
|
36458
37799
|
paneAtY(y) {
|
|
36459
37800
|
return this.paneNodeAtY(y);
|
|
36460
37801
|
}
|
|
@@ -36846,6 +38187,7 @@ var NativeRenderer = class {
|
|
|
36846
38187
|
} else if (repaintsChrome(level) && this.paintedData) {
|
|
36847
38188
|
this.chrome.prepare(this.scene, this.coords, this.theme);
|
|
36848
38189
|
this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
|
|
38190
|
+
this.trackMarkPopover();
|
|
36849
38191
|
}
|
|
36850
38192
|
this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
|
|
36851
38193
|
if (!repaintsData(level) && this.paintedData) this.repaintCursorLayers();
|
|
@@ -36861,7 +38203,7 @@ var NativeRenderer = class {
|
|
|
36861
38203
|
const lp = this.layerPane(l.def.id) ?? pane;
|
|
36862
38204
|
if (lp.collapsed) continue;
|
|
36863
38205
|
l.instance.render(this.extLayerArgs(l.def.id, lp.scale, lp.bounds, nowMs));
|
|
36864
|
-
if (this.animZoom && l.instance.animating?.()) this.animator.start();
|
|
38206
|
+
if (this.animZoom.on && l.instance.animating?.()) this.animator.start();
|
|
36865
38207
|
}
|
|
36866
38208
|
}
|
|
36867
38209
|
/** Blank one SDK layer canvas (a collapsed host pane suppresses the layer's painting). */
|
|
@@ -36926,7 +38268,7 @@ var NativeRenderer = class {
|
|
|
36926
38268
|
const args = this.extLayerArgs(l.def.id, lp.scale, lp.bounds, nowMs);
|
|
36927
38269
|
l.instance.render(args);
|
|
36928
38270
|
if (lp === pane) folded = foldBaseModulation(folded, l.instance.modulateBase?.(args) ?? null);
|
|
36929
|
-
if (this.animZoom && l.instance.animating?.()) this.animator.start();
|
|
38271
|
+
if (this.animZoom.on && l.instance.animating?.()) this.animator.start();
|
|
36930
38272
|
}
|
|
36931
38273
|
if (folded) {
|
|
36932
38274
|
if (folded.candleBodyScale != null) this.backend.candleBodyScale = clamp012(folded.candleBodyScale) || 0.01;
|
|
@@ -36945,6 +38287,7 @@ var NativeRenderer = class {
|
|
|
36945
38287
|
this.backdropRenderer.render(this.scene, this.coords, this.theme, gridAlpha);
|
|
36946
38288
|
this.backend.render(this.scene, this.coords, this.theme);
|
|
36947
38289
|
this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
|
|
38290
|
+
this.trackMarkPopover();
|
|
36948
38291
|
this.userDrawings?.render();
|
|
36949
38292
|
if (easeLive && liveActual) this.bars[li] = liveActual;
|
|
36950
38293
|
this.paintedData = true;
|
|
@@ -37457,7 +38800,7 @@ var NativeRenderer = class {
|
|
|
37457
38800
|
const map2 = /* @__PURE__ */ new Map();
|
|
37458
38801
|
for (const pane of this.scene.panes.values()) {
|
|
37459
38802
|
if (!pane.collapsed) continue;
|
|
37460
|
-
const models = this.scene.orderedIndicatorsForPane(pane.id);
|
|
38803
|
+
const models = this.scene.orderedIndicatorsForPane(pane.id).filter((m) => m.legend !== false);
|
|
37461
38804
|
const merged = new Set(this.scene.ownScaleIndicatorsForPane(pane.id).map((m) => m.id));
|
|
37462
38805
|
const master = models.find((m) => !merged.has(m.id)) ?? models[0];
|
|
37463
38806
|
map2.set(pane.id, master?.id ?? null);
|
|
@@ -40252,6 +41595,7 @@ var Vela = class {
|
|
|
40252
41595
|
if (Object.keys(defaults2).length > 0) this.rendererControl.set(defaults2);
|
|
40253
41596
|
this.panesControl = new PanesControl(this.orchestrator);
|
|
40254
41597
|
this.drawingsControl = new DrawingsControl(this.orchestrator.drawings);
|
|
41598
|
+
this.marksControl = new MarksControl(this.orchestrator.marks);
|
|
40255
41599
|
}
|
|
40256
41600
|
/**
|
|
40257
41601
|
* Register a scripting engine so `addIndicator({ language })` can run that
|
|
@@ -40470,6 +41814,17 @@ var Vela = class {
|
|
|
40470
41814
|
get drawings() {
|
|
40471
41815
|
return this.drawingsControl;
|
|
40472
41816
|
}
|
|
41817
|
+
/**
|
|
41818
|
+
* The chart's timeline-marks control surface: host events pinned to a bar and shown
|
|
41819
|
+
* as glyphs on a lane above the time axis, each opening a popup on click —
|
|
41820
|
+
* `chart.marks.add({ id, time, glyph, title, content })`, `chart.marks.set(list)`,
|
|
41821
|
+
* `chart.marks.defineGroup({ id, label })`. Marks are data, not user state: re-supply
|
|
41822
|
+
* them on `market:changed`. On a renderer without the `timelineMarks` capability the
|
|
41823
|
+
* model still fills but nothing paints (`chart.marks.supported`).
|
|
41824
|
+
*/
|
|
41825
|
+
get marks() {
|
|
41826
|
+
return this.marksControl;
|
|
41827
|
+
}
|
|
40473
41828
|
on(event, handler) {
|
|
40474
41829
|
return this.orchestrator.events.on(event, handler);
|
|
40475
41830
|
}
|
|
@@ -43255,6 +44610,9 @@ var VelaWorkspace = class {
|
|
|
43255
44610
|
this.events = new TypedEventBus();
|
|
43256
44611
|
this.feed = new MultiProviderFeed();
|
|
43257
44612
|
this.cellsById = /* @__PURE__ */ new Map();
|
|
44613
|
+
/** The one second pulse behind every time display: the bottom-bar clock and each
|
|
44614
|
+
* cell's countdown chip subscribe to it, so they can never read different seconds. */
|
|
44615
|
+
this.clock = new SecondClock();
|
|
43258
44616
|
this.pool = /* @__PURE__ */ new Map();
|
|
43259
44617
|
this.trackSizes = /* @__PURE__ */ new Map();
|
|
43260
44618
|
this.resizeObserver = null;
|
|
@@ -43532,6 +44890,7 @@ var VelaWorkspace = class {
|
|
|
43532
44890
|
this.drawToolbar?.setDrawingsSyncMode(!!this.syncOpts.drawings);
|
|
43533
44891
|
this.bottombar = opts.bottombar !== false ? new Bottombar(this.root, {
|
|
43534
44892
|
timezone: this.timezone,
|
|
44893
|
+
clock: this.clock,
|
|
43535
44894
|
onRange: (preset) => {
|
|
43536
44895
|
this.active.applyRange(preset);
|
|
43537
44896
|
this.bottombar?.setActiveRange(preset.id);
|
|
@@ -43975,6 +45334,7 @@ var VelaWorkspace = class {
|
|
|
43975
45334
|
const cell = this.activeId ? this.cellsById.get(this.activeId) : void 0;
|
|
43976
45335
|
const paneMax = cell ? cell.chart.panes.list().some((p) => p.maximized) : false;
|
|
43977
45336
|
this.mobileBar.setMaximizeActive(this.maximizedId != null || paneMax);
|
|
45337
|
+
this.mobileBar.setMaximizeVisible(this.def.cells.length > 1);
|
|
43978
45338
|
}
|
|
43979
45339
|
/**
|
|
43980
45340
|
* Trade the SLOTS of two live cells — the grid arrangement changes, the cells
|
|
@@ -44275,6 +45635,7 @@ var VelaWorkspace = class {
|
|
|
44275
45635
|
if (id === this.activeId) cell.host.dataset.active = "1";
|
|
44276
45636
|
this.wireCell(cell);
|
|
44277
45637
|
cell.chart.renderer.setLayoutMode(this.layoutCtl.current);
|
|
45638
|
+
cell.chart.renderer.setWallClock(this.clock);
|
|
44278
45639
|
cell.setControlsSuspended(this.layoutCtl.current === "mobile");
|
|
44279
45640
|
if (this.favs.length > 0) cell.chart.drawings.setFavorites(this.favs);
|
|
44280
45641
|
cell.setManifest(this.manifest, pooled?.indicators == null);
|