@luxalgo/vela 0.6.20 → 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-M6T5A733.js → chunk-A347YL2P.js} +31 -8
- package/dist/{chunk-4Q4B5AO3.js → chunk-A4G64KVF.js} +1453 -131
- 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 +1476 -130
- 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.ts → options-ex2_gtKp.d.cts} +200 -12
- package/dist/{options-BCRmYALw.d.cts → 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 +1476 -130
- package/dist/vela.global.min.js +77 -53
- package/dist/widget.cjs +1502 -135
- package/dist/widget.d.cts +6 -6
- package/dist/widget.d.ts +6 -6
- package/dist/widget.js +5 -5
- package/dist/workspace.cjs +1502 -135
- package/dist/workspace.d.cts +8 -5
- package/dist/workspace.d.ts +8 -5
- package/dist/workspace.js +4 -4
- package/package.json +2 -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);
|
|
@@ -19188,6 +19396,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19188
19396
|
if (!record.native) continue;
|
|
19189
19397
|
record.native.instance.stop();
|
|
19190
19398
|
record.native.instance = record.native.descriptor.create();
|
|
19399
|
+
record.native.started = false;
|
|
19191
19400
|
record.pendingStructural = true;
|
|
19192
19401
|
if (record.hidden) {
|
|
19193
19402
|
record.native.stale = true;
|
|
@@ -19679,11 +19888,12 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19679
19888
|
record.session = void 0;
|
|
19680
19889
|
record.native?.instance.suspend();
|
|
19681
19890
|
if (record.renderHandle) this.renderer.setIndicatorVisible?.(record.renderHandle, false);
|
|
19891
|
+
else if (record.native) this.mountHiddenNativeRow(id, record);
|
|
19682
19892
|
} else {
|
|
19683
19893
|
if (record.renderHandle) this.renderer.setIndicatorVisible?.(record.renderHandle, true);
|
|
19684
19894
|
record.pendingStructural = true;
|
|
19685
19895
|
if (record.native) {
|
|
19686
|
-
if (record.native.stale) {
|
|
19896
|
+
if (record.native.stale || !record.native.started) {
|
|
19687
19897
|
record.native.stale = false;
|
|
19688
19898
|
const handle = this.handles.get(id);
|
|
19689
19899
|
if (handle) void this.startNativeIndicator(id, handle);
|
|
@@ -19766,6 +19976,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19766
19976
|
this.unresolvedUnsub = null;
|
|
19767
19977
|
this.feed.destroy?.();
|
|
19768
19978
|
this.drawings.destroy();
|
|
19979
|
+
this.marks.destroy();
|
|
19769
19980
|
this.renderer.destroy();
|
|
19770
19981
|
this.events.clear();
|
|
19771
19982
|
}
|
|
@@ -19864,6 +20075,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19864
20075
|
}
|
|
19865
20076
|
};
|
|
19866
20077
|
record.native.instance.start(ctx, record.inputValues);
|
|
20078
|
+
record.native.started = true;
|
|
19867
20079
|
} catch (err) {
|
|
19868
20080
|
this.fail(id, handle, err);
|
|
19869
20081
|
}
|
|
@@ -19878,6 +20090,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19878
20090
|
overlay: d.overlay,
|
|
19879
20091
|
paneHint: d.paneHint,
|
|
19880
20092
|
native: { type: record.native.type },
|
|
20093
|
+
...d.legend === false ? { legend: false } : {},
|
|
19881
20094
|
...out.paneAxis != null ? { paneAxis: out.paneAxis } : {},
|
|
19882
20095
|
series: out.series ?? [],
|
|
19883
20096
|
fills: out.fills ?? [],
|
|
@@ -19900,9 +20113,17 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19900
20113
|
* over it in place (`pendingStructural`), clears the spinner, and only THEN fires
|
|
19901
20114
|
* `indicator:added`/`ready` — so event semantics and `inspect()` (which skips
|
|
19902
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.
|
|
19903
20124
|
*/
|
|
19904
20125
|
mountLoadingPlaceholder(id, record) {
|
|
19905
|
-
if (record.renderHandle ||
|
|
20126
|
+
if (record.renderHandle || !record.prepared) return;
|
|
19906
20127
|
const meta = record.prepared.meta;
|
|
19907
20128
|
const model = {
|
|
19908
20129
|
id,
|
|
@@ -19926,8 +20147,34 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19926
20147
|
this.ensurePaneFor(paneId);
|
|
19927
20148
|
record.renderHandle = this.renderer.mountIndicator(model);
|
|
19928
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
|
+
}
|
|
19929
20155
|
this.setLoading(record, true);
|
|
19930
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
|
+
}
|
|
19931
20178
|
/** Flip the record's loading state and reflect it in the legend row (spinner on/off). */
|
|
19932
20179
|
setLoading(record, loading) {
|
|
19933
20180
|
record.loading = loading;
|
|
@@ -19962,6 +20209,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19962
20209
|
for (const r of this.registry.all()) {
|
|
19963
20210
|
const model = r.model;
|
|
19964
20211
|
if (!model) continue;
|
|
20212
|
+
if (model.legend === false) continue;
|
|
19965
20213
|
const paneId = model.paneId ?? "price";
|
|
19966
20214
|
if (!byPane.has(paneId)) byPane.set(paneId, []);
|
|
19967
20215
|
byPane.get(paneId).push({
|
|
@@ -20537,6 +20785,16 @@ var RendererControl = class {
|
|
|
20537
20785
|
this.renderer.setLayoutMode?.(mode);
|
|
20538
20786
|
return this;
|
|
20539
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
|
+
}
|
|
20540
20798
|
};
|
|
20541
20799
|
|
|
20542
20800
|
// src/core/PanesControl.ts
|
|
@@ -20876,6 +21134,61 @@ var DrawingsControl = class {
|
|
|
20876
21134
|
}
|
|
20877
21135
|
};
|
|
20878
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
|
+
|
|
20879
21192
|
// src/core/model/inputs.ts
|
|
20880
21193
|
function inputVisible(when, values) {
|
|
20881
21194
|
if (!when) return true;
|
|
@@ -22924,6 +23237,8 @@ var NATIVE_CAPABILITIES = {
|
|
|
22924
23237
|
// canvas-painted into the owning indicator's interleave slice
|
|
22925
23238
|
trades: true,
|
|
22926
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)
|
|
22927
23242
|
inputsUI: true
|
|
22928
23243
|
// reuses the DOM InputsUI
|
|
22929
23244
|
};
|
|
@@ -24491,6 +24806,31 @@ var Animator = class {
|
|
|
24491
24806
|
}
|
|
24492
24807
|
}
|
|
24493
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
|
+
};
|
|
24494
24834
|
function easeToward(current, target, dtMs, tauMs) {
|
|
24495
24835
|
if (tauMs <= 0) return target;
|
|
24496
24836
|
return current + (target - current) * (1 - Math.exp(-dtMs / tauMs));
|
|
@@ -25214,6 +25554,25 @@ function drawTextLines(ctx, lines, x, firstY, step, color) {
|
|
|
25214
25554
|
for (let i = 0; i < lines.length; i += 1) ctx.fillText(lines[i], x, firstY + i * step);
|
|
25215
25555
|
}
|
|
25216
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
|
+
|
|
25217
25576
|
// src/renderers/native/core/SceneGraph.ts
|
|
25218
25577
|
var SceneGraph = class {
|
|
25219
25578
|
constructor() {
|
|
@@ -25277,6 +25636,20 @@ var SceneGraph = class {
|
|
|
25277
25636
|
/** Strategy trade-marker display (the `tradeMarkers` feature): master toggle, the
|
|
25278
25637
|
* two text lines, and the palette. Trade markers always paint on the price pane. */
|
|
25279
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;
|
|
25280
25653
|
/** Renderer-owned shaded time bands (session highlighting), behind grid + data. */
|
|
25281
25654
|
this.highlights = [];
|
|
25282
25655
|
/** Pre/post-market bands pushed by the host (`sessionZones` feature); null ⇒ no sessions. */
|
|
@@ -27125,6 +27498,315 @@ function timeTicks(fromMs, toMs, target = 8, offsetMs = 0) {
|
|
|
27125
27498
|
return out;
|
|
27126
27499
|
}
|
|
27127
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
|
+
|
|
27128
27810
|
// src/renderers/native/chrome/ChromeRenderer.ts
|
|
27129
27811
|
var ChromeRenderer = class {
|
|
27130
27812
|
constructor() {
|
|
@@ -27134,11 +27816,40 @@ var ChromeRenderer = class {
|
|
|
27134
27816
|
this.axisTextColor = DARK_THEME.textColor;
|
|
27135
27817
|
// Shared Pine-drawing renderer, used here for autoscale geometry only; widthCache persists.
|
|
27136
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 = [];
|
|
27137
27827
|
}
|
|
27138
27828
|
mount(canvas) {
|
|
27139
27829
|
this.canvas = canvas;
|
|
27140
27830
|
this.ctx = canvas.getContext("2d");
|
|
27141
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
|
+
}
|
|
27142
27853
|
/** Wire the drawing coordinate resolvers + theme (call once per frame before use). */
|
|
27143
27854
|
prepare(scene, coords, theme) {
|
|
27144
27855
|
this.drawScene.setDeps({
|
|
@@ -27187,6 +27898,7 @@ var ChromeRenderer = class {
|
|
|
27187
27898
|
const panes = scene.orderedPanes();
|
|
27188
27899
|
if (coords.barCount === 0) {
|
|
27189
27900
|
this.drawPaneSeparators(ctx, scene, theme, fullW, panes);
|
|
27901
|
+
this.markLayout = { glyphs: [], stacks: /* @__PURE__ */ new Map() };
|
|
27190
27902
|
return;
|
|
27191
27903
|
}
|
|
27192
27904
|
const pricePane = panes.find((p) => p.kind === "price") ?? null;
|
|
@@ -27200,6 +27912,46 @@ var ChromeRenderer = class {
|
|
|
27200
27912
|
this.drawPaneSeparators(ctx, scene, theme, fullW, panes);
|
|
27201
27913
|
this.drawPriceLineAndCountdown(ctx, scene, coords, theme, dataW, pricePane);
|
|
27202
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;
|
|
27203
27955
|
}
|
|
27204
27956
|
destroy() {
|
|
27205
27957
|
this.canvas = null;
|
|
@@ -27315,7 +28067,8 @@ var ChromeRenderer = class {
|
|
|
27315
28067
|
* - the countdown-to-bar-close chip (`showCountdown`).
|
|
27316
28068
|
* When the label and countdown are both on they merge into one stacked block (countdown
|
|
27317
28069
|
* under the label, text flushed left); a lone label or countdown is centered on the
|
|
27318
|
-
* 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.
|
|
27319
28072
|
*/
|
|
27320
28073
|
drawPriceLineAndCountdown(ctx, scene, coords, theme, dataW, pricePane) {
|
|
27321
28074
|
const n = scene.bars.length;
|
|
@@ -27335,17 +28088,16 @@ var ChromeRenderer = class {
|
|
|
27335
28088
|
ctx.stroke();
|
|
27336
28089
|
setDash2(ctx, "solid");
|
|
27337
28090
|
}
|
|
27338
|
-
const
|
|
27339
|
-
const showCountdown =
|
|
28091
|
+
const cdText = scene.showCountdown ? countdownText(last.time, coords.barInterval, Date.now()) : null;
|
|
28092
|
+
const showCountdown = cdText !== null;
|
|
27340
28093
|
const showLabel = scene.showPriceLabel;
|
|
27341
28094
|
if (!showLabel && !showCountdown) return;
|
|
27342
28095
|
const priceText = formatAxisValue(pricePane.scale, pricePane.bounds.height, last.close, percentScaleFor(scene, pricePane), scene.priceMintick);
|
|
27343
|
-
const cdText = showCountdown ? formatCountdown(last.time + interval - Date.now()) : "";
|
|
27344
28096
|
const PAD = 8;
|
|
27345
28097
|
const x = dataW + 1;
|
|
27346
28098
|
const textColor = tagTextColor(color, theme.background);
|
|
27347
28099
|
ctx.textBaseline = "middle";
|
|
27348
|
-
if (showLabel &&
|
|
28100
|
+
if (showLabel && cdText !== null) {
|
|
27349
28101
|
const w2 = Math.max(ctx.measureText(priceText).width, ctx.measureText(cdText).width) + PAD;
|
|
27350
28102
|
const top = y - 8;
|
|
27351
28103
|
const tx = x + PAD / 2;
|
|
@@ -27358,7 +28110,7 @@ var ChromeRenderer = class {
|
|
|
27358
28110
|
ctx.textAlign = "start";
|
|
27359
28111
|
return;
|
|
27360
28112
|
}
|
|
27361
|
-
const text = showLabel ? priceText : cdText;
|
|
28113
|
+
const text = showLabel ? priceText : cdText ?? "";
|
|
27362
28114
|
const w = ctx.measureText(text).width + PAD;
|
|
27363
28115
|
ctx.fillStyle = color;
|
|
27364
28116
|
ctx.fillRect(x, y - 8, w, 16);
|
|
@@ -27429,29 +28181,6 @@ function setDash2(ctx, style) {
|
|
|
27429
28181
|
else if (style === "dotted") ctx.setLineDash([2, 3]);
|
|
27430
28182
|
else ctx.setLineDash([]);
|
|
27431
28183
|
}
|
|
27432
|
-
function tagTextColor(bg, over) {
|
|
27433
|
-
const [r, g, b, a] = parseColor(bg);
|
|
27434
|
-
let R = r;
|
|
27435
|
-
let G = g;
|
|
27436
|
-
let B = b;
|
|
27437
|
-
if (a < 1) {
|
|
27438
|
-
const [or, og, ob] = parseColor(over);
|
|
27439
|
-
R = r * a + or * (1 - a);
|
|
27440
|
-
G = g * a + og * (1 - a);
|
|
27441
|
-
B = b * a + ob * (1 - a);
|
|
27442
|
-
}
|
|
27443
|
-
const lin = (c) => c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
27444
|
-
const L = 0.2126 * lin(R) + 0.7152 * lin(G) + 0.0722 * lin(B);
|
|
27445
|
-
return L >= 0.4 ? "#000000" : "#ffffff";
|
|
27446
|
-
}
|
|
27447
|
-
function formatCountdown(ms) {
|
|
27448
|
-
const total = Math.max(0, Math.floor(ms / 1e3));
|
|
27449
|
-
const s = total % 60;
|
|
27450
|
-
const m = Math.floor(total / 60) % 60;
|
|
27451
|
-
const h = Math.floor(total / 3600);
|
|
27452
|
-
const pad = (v) => String(v).padStart(2, "0");
|
|
27453
|
-
return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`;
|
|
27454
|
-
}
|
|
27455
28184
|
|
|
27456
28185
|
// src/renderers/native/chrome/LabelTooltip.ts
|
|
27457
28186
|
var HOVER_DELAY_MS = 350;
|
|
@@ -27657,6 +28386,11 @@ function setDash3(ctx, style) {
|
|
|
27657
28386
|
function settingsIdSlug(label) {
|
|
27658
28387
|
return label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
27659
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
|
+
}
|
|
27660
28394
|
function settingsIdHidden(id, hidden) {
|
|
27661
28395
|
if (hidden.size === 0) return false;
|
|
27662
28396
|
let path = id;
|
|
@@ -27750,7 +28484,11 @@ var BUILTIN_SETTINGS_IDS = [
|
|
|
27750
28484
|
"symbol.style.baseline.base-level",
|
|
27751
28485
|
"symbol.style.baseline.width",
|
|
27752
28486
|
"symbol.animation",
|
|
28487
|
+
"symbol.animation.zoom",
|
|
28488
|
+
"symbol.animation.pan",
|
|
28489
|
+
"symbol.animation.autoscale",
|
|
27753
28490
|
"symbol.animation.price-changes",
|
|
28491
|
+
"symbol.animation.intro",
|
|
27754
28492
|
"symbol.timezone",
|
|
27755
28493
|
"scales",
|
|
27756
28494
|
"scales.price-scale",
|
|
@@ -27776,8 +28514,13 @@ var BUILTIN_SETTINGS_IDS = [
|
|
|
27776
28514
|
"canvas.grid.horizontal",
|
|
27777
28515
|
"canvas.theme"
|
|
27778
28516
|
];
|
|
27779
|
-
function settingsIdCatalog(hostSections) {
|
|
28517
|
+
function settingsIdCatalog(hostSections, markGroups = []) {
|
|
27780
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
|
+
}
|
|
27781
28524
|
for (const def of chartTypes()) {
|
|
27782
28525
|
if (hasOwnCandlePaint(def.id)) {
|
|
27783
28526
|
const style = `symbol.style.${def.id}`;
|
|
@@ -27817,7 +28560,7 @@ function styleLabel(id) {
|
|
|
27817
28560
|
return chartType(id)?.label ?? BUILTIN_STYLE_LABELS2[id] ?? id;
|
|
27818
28561
|
}
|
|
27819
28562
|
var SD_STYLE_ID = "vela-settings-controls";
|
|
27820
|
-
var SD_STYLE_REV = "
|
|
28563
|
+
var SD_STYLE_REV = "6";
|
|
27821
28564
|
var SETTINGS_BORDER = "var(--vela-border)";
|
|
27822
28565
|
function ensureControlStyles() {
|
|
27823
28566
|
if (typeof document === "undefined") return;
|
|
@@ -27893,7 +28636,15 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
27893
28636
|
.vela-sd-mobile .vela-select-trigger,.vela-sd-mobile .vela-num input,.vela-sd-mobile .vela-width-field{height:34px;}
|
|
27894
28637
|
.vela-sd-mobile .vela-sd-close{width:40px;height:40px;}
|
|
27895
28638
|
.vela-sd-mobile .vela-sd-btn{height:38px;}
|
|
27896
|
-
.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);}`;
|
|
27897
28648
|
if (!existing) document.head.appendChild(st);
|
|
27898
28649
|
}
|
|
27899
28650
|
var SettingsDialog = class {
|
|
@@ -27908,6 +28659,9 @@ var SettingsDialog = class {
|
|
|
27908
28659
|
this.config = null;
|
|
27909
28660
|
this.syncTypeTabs = null;
|
|
27910
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;
|
|
27911
28665
|
/** The Canvas → Theme row: current app theme + where a pick is raised. The row is a
|
|
27912
28666
|
* host callback, NOT a config patch — the app theme stays out of the persisted
|
|
27913
28667
|
* `ChartConfig`, so exported templates never carry it. */
|
|
@@ -27932,6 +28686,11 @@ var SettingsDialog = class {
|
|
|
27932
28686
|
setHostSections(sections) {
|
|
27933
28687
|
this.hostSections = sections;
|
|
27934
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
|
+
}
|
|
27935
28694
|
/** Replace the visibility policy — an open dialog rebuilds in place to honor it. */
|
|
27936
28695
|
setHiddenSettings(ids) {
|
|
27937
28696
|
const next = new Set(ids);
|
|
@@ -28105,12 +28864,36 @@ var SettingsDialog = class {
|
|
|
28105
28864
|
}
|
|
28106
28865
|
showActive(config.series.style);
|
|
28107
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"));
|
|
28108
28885
|
body.append(sid(this.boolRow(
|
|
28109
28886
|
"Animate price changes",
|
|
28110
28887
|
config.priceScale.animateLastPrice,
|
|
28111
28888
|
(v) => this.emit({ priceScale: { animateLastPrice: v } }),
|
|
28112
28889
|
this.hint("Glide the live bar to each new price instead of snapping.")
|
|
28113
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"));
|
|
28114
28897
|
body.append(sid(this.sectionTitle("Time zone"), "symbol.timezone"));
|
|
28115
28898
|
body.append(sid(this.selectRowLabeled("Time zone", normalizeTimezone(config.timeScale.timezone), timezoneOptions(config.timeScale.timezone), (v) => this.emit({ timeScale: { timezone: v } })), "symbol.timezone"));
|
|
28116
28899
|
const renderHostSections = (placement) => {
|
|
@@ -28180,6 +28963,13 @@ var SettingsDialog = class {
|
|
|
28180
28963
|
body.append(sid(this.sectionTitle("Theme"), "canvas.theme"));
|
|
28181
28964
|
body.append(sid(this.selectRow("Color theme", tc.current === "dark" ? "Dark" : "Light", ["Dark", "Light"], (v) => tc.onSelect(v === "Dark" ? "dark" : "light")), "canvas.theme"));
|
|
28182
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
|
+
}
|
|
28183
28973
|
renderChartTypeSections("end");
|
|
28184
28974
|
renderHostSections("end");
|
|
28185
28975
|
if (this.hiddenSettings.size > 0) {
|
|
@@ -33739,6 +34529,383 @@ function expandScaleByPixels(scale, heightPx, abovePx, belowPx) {
|
|
|
33739
34529
|
return { ...scale, min: scale.min - belowPx * perPx, max: scale.max + abovePx * perPx };
|
|
33740
34530
|
}
|
|
33741
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
|
+
|
|
33742
34909
|
// src/renderers/native/core/manualScale.ts
|
|
33743
34910
|
function rescaleAround(start, factor) {
|
|
33744
34911
|
if (start.log && start.min > 0 && start.max > start.min) {
|
|
@@ -34331,11 +35498,12 @@ var SCROLL_BTN_BOTTOM = TIME_AXIS_H + 14;
|
|
|
34331
35498
|
var SCROLL_BTN_PROXIMITY_PX = 120;
|
|
34332
35499
|
var MIN_VISIBLE_BARS = 2;
|
|
34333
35500
|
var ZOOM_OUT_MARGIN_BARS = 6;
|
|
34334
|
-
var
|
|
34335
|
-
var SCALE_TAU_MS = 80;
|
|
34336
|
-
var FLING_TAU_MS = 110;
|
|
34337
|
-
var SCROLL_TO_TAU_MS = 130;
|
|
35501
|
+
var INTRO_MODEL_FADE_MS = 350;
|
|
34338
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
|
+
}
|
|
34339
35507
|
var PRICE_SCALE_K = 4e-3;
|
|
34340
35508
|
var KEY_ZOOM_STEP = 0.2;
|
|
34341
35509
|
var SEPARATOR_HIT_PX = 4;
|
|
@@ -34384,9 +35552,20 @@ var NativeRenderer = class {
|
|
|
34384
35552
|
this.indicatorSlices = new IndicatorDrawingSlices();
|
|
34385
35553
|
/** Hover tooltips for Pine labels (canvas hit-rects collected by the chrome layer). */
|
|
34386
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();
|
|
34387
35562
|
this.crosshairLayer = new CrosshairRenderer();
|
|
34388
|
-
/**
|
|
34389
|
-
|
|
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;
|
|
34390
35569
|
this.symbolPicker = null;
|
|
34391
35570
|
/** Indicator titles (the legend rows) shown — held here so a remount re-applies it. */
|
|
34392
35571
|
this.indicatorTitlesOn = true;
|
|
@@ -34404,19 +35583,25 @@ var NativeRenderer = class {
|
|
|
34404
35583
|
/** Drawings layer self-serves Ctrl+Z/Y (see the `historyChords` feature). */
|
|
34405
35584
|
this.historyChordsEnabled = true;
|
|
34406
35585
|
this.liveRegion = null;
|
|
34407
|
-
// ── animation state
|
|
34408
|
-
|
|
34409
|
-
this.
|
|
34410
|
-
|
|
34411
|
-
|
|
34412
|
-
|
|
34413
|
-
|
|
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
|
|
34414
35598
|
// Brand default candles.
|
|
34415
35599
|
this.candleUp = BULLISH;
|
|
34416
35600
|
this.candleDown = BEARISH;
|
|
34417
35601
|
// ── intro reveal (plays once when candles first appear) ──
|
|
34418
|
-
this.
|
|
34419
|
-
|
|
35602
|
+
this.intro = { style: "settle", duration: INTRO_DURATION_DEFAULT_MS };
|
|
35603
|
+
this.introOnStyle = "settle";
|
|
35604
|
+
// the style the config's on/off switch restores
|
|
34420
35605
|
this.introPlayed = false;
|
|
34421
35606
|
this.introRaf = null;
|
|
34422
35607
|
/** The load affordance (three pulsing dots) — up while the host reports a bar load in
|
|
@@ -34535,7 +35720,7 @@ var NativeRenderer = class {
|
|
|
34535
35720
|
this.moveIndicatorCbs = /* @__PURE__ */ new Set();
|
|
34536
35721
|
this.priceStyleCbs = /* @__PURE__ */ new Set();
|
|
34537
35722
|
this.name = "native";
|
|
34538
|
-
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"];
|
|
34539
35724
|
/** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
|
|
34540
35725
|
* so moving onto the button doesn't count as leaving). */
|
|
34541
35726
|
this.onScrollProximityMove = (e) => {
|
|
@@ -34567,9 +35752,12 @@ var NativeRenderer = class {
|
|
|
34567
35752
|
this.scene.showPriceLine = opts.currentPriceLine;
|
|
34568
35753
|
this.scene.logScale = opts.logScale;
|
|
34569
35754
|
this.backendMode = opts.nativeBackend;
|
|
34570
|
-
this.animZoom
|
|
34571
|
-
this.animPan
|
|
34572
|
-
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);
|
|
34573
35761
|
this.glowAmount = opts.glow;
|
|
34574
35762
|
this.candleUp = opts.upColor;
|
|
34575
35763
|
this.candleDown = opts.downColor;
|
|
@@ -34613,20 +35801,29 @@ var NativeRenderer = class {
|
|
|
34613
35801
|
if (this.backend && "glow" in this.backend) this.backend.glow = this.glowAmount;
|
|
34614
35802
|
break;
|
|
34615
35803
|
case "animZoom":
|
|
34616
|
-
this.animZoom
|
|
35804
|
+
this.animZoom.set(resolveEaseMs(value, ZOOM_EASE_DEFAULT_MS));
|
|
34617
35805
|
return;
|
|
34618
35806
|
// affects the next interaction only — nothing to repaint
|
|
34619
|
-
case "animPan":
|
|
34620
|
-
|
|
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));
|
|
34621
35815
|
return;
|
|
35816
|
+
case "animAutoscale":
|
|
35817
|
+
this.animAutoscale.set(resolveEaseMs(value, AUTOSCALE_EASE_DEFAULT_MS));
|
|
35818
|
+
return;
|
|
35819
|
+
// a glide in flight finishes at the new rate (or snaps at 0)
|
|
34622
35820
|
case "animLiveBar":
|
|
34623
|
-
this.
|
|
35821
|
+
this.animLiveBar.set(resolveLiveBarEaseMs(value));
|
|
34624
35822
|
return;
|
|
34625
35823
|
// affects the next tick only; a glide in flight finishes at the new rate (or snaps at 0)
|
|
34626
35824
|
case "intro": {
|
|
34627
|
-
|
|
34628
|
-
this.
|
|
34629
|
-
if (s) this.playIntro(s);
|
|
35825
|
+
this.setIntro(resolveIntro(value));
|
|
35826
|
+
if (this.intro.style) this.playIntro();
|
|
34630
35827
|
return;
|
|
34631
35828
|
}
|
|
34632
35829
|
case "zoomAnchor":
|
|
@@ -34698,6 +35895,10 @@ var NativeRenderer = class {
|
|
|
34698
35895
|
case "tradeMarkers":
|
|
34699
35896
|
this.scene.tradeMarkers = mergeTradeMarkersState(this.scene.tradeMarkers, value);
|
|
34700
35897
|
break;
|
|
35898
|
+
case "marks":
|
|
35899
|
+
this.scene.marks = mergeMarksState(this.scene.marks, value);
|
|
35900
|
+
this.markPopover?.close();
|
|
35901
|
+
break;
|
|
34701
35902
|
case "keyboard":
|
|
34702
35903
|
this.setKeyboardEnabled(Boolean(value));
|
|
34703
35904
|
return;
|
|
@@ -34756,13 +35957,17 @@ var NativeRenderer = class {
|
|
|
34756
35957
|
case "glow":
|
|
34757
35958
|
return this.glowAmount;
|
|
34758
35959
|
case "animZoom":
|
|
34759
|
-
return this.animZoom;
|
|
35960
|
+
return this.animZoom.tau;
|
|
34760
35961
|
case "animPan":
|
|
34761
|
-
return this.animPan;
|
|
35962
|
+
return this.animPan.tau;
|
|
35963
|
+
case "animScroll":
|
|
35964
|
+
return this.animScroll.tau;
|
|
35965
|
+
case "animAutoscale":
|
|
35966
|
+
return this.animAutoscale.tau;
|
|
34762
35967
|
case "animLiveBar":
|
|
34763
|
-
return this.
|
|
35968
|
+
return this.animLiveBar.tau;
|
|
34764
35969
|
case "intro":
|
|
34765
|
-
return this.
|
|
35970
|
+
return this.intro.style;
|
|
34766
35971
|
case "zoomAnchor":
|
|
34767
35972
|
return this.zoomAnchorMode;
|
|
34768
35973
|
case "axisDrag":
|
|
@@ -34805,6 +36010,8 @@ var NativeRenderer = class {
|
|
|
34805
36010
|
}
|
|
34806
36011
|
case "tradeMarkers":
|
|
34807
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 } };
|
|
34808
36015
|
case "keyboard":
|
|
34809
36016
|
return this.keyboardEnabled;
|
|
34810
36017
|
case "historyChords":
|
|
@@ -34930,7 +36137,13 @@ var NativeRenderer = class {
|
|
|
34930
36137
|
currentPriceLine: this.scene.showPriceLine,
|
|
34931
36138
|
priceLabel: this.scene.showPriceLabel,
|
|
34932
36139
|
countdown: this.scene.showCountdown,
|
|
34933
|
-
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
|
|
34934
36147
|
},
|
|
34935
36148
|
panes: { separatorColor: s.separatorColor ?? t.borderColor },
|
|
34936
36149
|
trades: {
|
|
@@ -34942,6 +36155,7 @@ var NativeRenderer = class {
|
|
|
34942
36155
|
exitColor: this.scene.tradeMarkers.colors.exit
|
|
34943
36156
|
},
|
|
34944
36157
|
timeScale: { timezone: this.scene.timezone },
|
|
36158
|
+
marks: { visible: this.scene.marks.visible, groups: { ...this.scene.marks.groups } },
|
|
34945
36159
|
candles: {
|
|
34946
36160
|
upColor: this.candleUp,
|
|
34947
36161
|
downColor: this.candleDown,
|
|
@@ -35037,7 +36251,12 @@ var NativeRenderer = class {
|
|
|
35037
36251
|
this.scene.showPriceLabel = next.priceScale.priceLabel;
|
|
35038
36252
|
this.scene.showCountdown = next.priceScale.countdown;
|
|
35039
36253
|
this.syncCountdownTimer();
|
|
35040
|
-
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 };
|
|
35041
36260
|
s.separatorColor = keepInherit(s.separatorColor, next.panes.separatorColor, prevTheme.borderColor);
|
|
35042
36261
|
this.scene.tradeMarkers = {
|
|
35043
36262
|
visible: next.trades.visible,
|
|
@@ -35046,6 +36265,7 @@ var NativeRenderer = class {
|
|
|
35046
36265
|
colors: { long: next.trades.longColor, short: next.trades.shortColor, exit: next.trades.exitColor }
|
|
35047
36266
|
};
|
|
35048
36267
|
this.scene.timezone = next.timeScale.timezone;
|
|
36268
|
+
this.scene.marks = { visible: next.marks.visible, groups: { ...next.marks.groups } };
|
|
35049
36269
|
this.candleUp = next.candles.upColor;
|
|
35050
36270
|
this.candleDown = next.candles.downColor;
|
|
35051
36271
|
s.candle = {
|
|
@@ -35178,6 +36398,7 @@ var NativeRenderer = class {
|
|
|
35178
36398
|
}
|
|
35179
36399
|
this.settingsDialog.setTheme(this.theme);
|
|
35180
36400
|
this.settingsDialog.setHostSections(this.hostSettingsSections);
|
|
36401
|
+
this.settingsDialog.setMarkGroups(this.markGroupsInUse(), (id) => markGroupVisible(this.scene.marks, id, this.scene.markGroups));
|
|
35181
36402
|
this.settingsDialog.setHiddenSettings(this.hiddenSettings);
|
|
35182
36403
|
this.syncThemeControl();
|
|
35183
36404
|
this.settingsDialog.toggle(
|
|
@@ -35268,10 +36489,10 @@ var NativeRenderer = class {
|
|
|
35268
36489
|
this.glideRightOffset(ZOOM_OUT_MARGIN_BARS);
|
|
35269
36490
|
}
|
|
35270
36491
|
/** Ease rightOffset to `target` at constant zoom (see animTick's scroll glide);
|
|
35271
|
-
* instant when
|
|
36492
|
+
* instant when the scroll glide is off. Shared by scroll-to-latest and panBy. */
|
|
35272
36493
|
glideRightOffset(target) {
|
|
35273
36494
|
const vp = this.coords.getViewport();
|
|
35274
|
-
if (!this.
|
|
36495
|
+
if (!this.animScroll.on) {
|
|
35275
36496
|
this.applyViewport({ barSpacing: vp.barSpacing, rightOffset: target });
|
|
35276
36497
|
return;
|
|
35277
36498
|
}
|
|
@@ -35336,20 +36557,21 @@ var NativeRenderer = class {
|
|
|
35336
36557
|
* full size, eased, with a left→right stagger so the chart draws itself; `settle`
|
|
35337
36558
|
* adds an ease-out-back overshoot. Autoscale stays on the real bars so the frame
|
|
35338
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.
|
|
35339
36561
|
*/
|
|
35340
|
-
playIntro(
|
|
36562
|
+
playIntro() {
|
|
35341
36563
|
if (this.introRaf != null) cancelAnimationFrame(this.introRaf);
|
|
35342
36564
|
this.introRaf = null;
|
|
36565
|
+
const { style, duration } = this.intro;
|
|
35343
36566
|
const real = this.bars;
|
|
35344
36567
|
const n = real.length;
|
|
35345
|
-
if (n === 0) return;
|
|
36568
|
+
if (n === 0 || !style) return;
|
|
35346
36569
|
this.computeScales();
|
|
35347
36570
|
for (const pane of this.scene.panes.values()) pane.scale = { ...pane.scaleTarget };
|
|
35348
36571
|
this.modelAlpha = 0;
|
|
35349
|
-
const DURATION = 650;
|
|
35350
36572
|
const start = performance.now();
|
|
35351
36573
|
const step = (now) => {
|
|
35352
|
-
const p = Math.min(1, (now - start) /
|
|
36574
|
+
const p = Math.min(1, (now - start) / duration);
|
|
35353
36575
|
this.scene.bars = p >= 1 ? real : real.map((b, i) => this.revealCandle(b, i, p, n, style));
|
|
35354
36576
|
this.paintData();
|
|
35355
36577
|
if (p < 1) {
|
|
@@ -35363,10 +36585,10 @@ var NativeRenderer = class {
|
|
|
35363
36585
|
}
|
|
35364
36586
|
/** After the candle reveal, fade the indicator models (series/fills/…) from hidden to full. */
|
|
35365
36587
|
fadeInModels() {
|
|
35366
|
-
const
|
|
36588
|
+
const fade = Math.min(INTRO_MODEL_FADE_MS, this.intro.duration || INTRO_MODEL_FADE_MS);
|
|
35367
36589
|
const start = performance.now();
|
|
35368
36590
|
const step = (now) => {
|
|
35369
|
-
this.modelAlpha = Math.min(1, (now - start) /
|
|
36591
|
+
this.modelAlpha = Math.min(1, (now - start) / fade);
|
|
35370
36592
|
this.paintData();
|
|
35371
36593
|
if (this.modelAlpha < 1) {
|
|
35372
36594
|
this.introRaf = requestAnimationFrame(step);
|
|
@@ -35481,7 +36703,8 @@ var NativeRenderer = class {
|
|
|
35481
36703
|
zoomTo: (target, anchorLogical, anchorX) => this.zoomTo(target, anchorLogical, anchorX),
|
|
35482
36704
|
fling: (v) => this.fling(v),
|
|
35483
36705
|
onPointerMove: (x, y) => this.handlePointerMove(x, y),
|
|
35484
|
-
onClick: (x) => {
|
|
36706
|
+
onClick: (x, y) => {
|
|
36707
|
+
if (this.handleMarkClick(x, y)) return;
|
|
35485
36708
|
this.userDrawings?.deselect();
|
|
35486
36709
|
this.handleClick(x);
|
|
35487
36710
|
},
|
|
@@ -35510,7 +36733,7 @@ var NativeRenderer = class {
|
|
|
35510
36733
|
drawingsPointerDown: (x, y, snap, shift2, mod) => this.userDrawings?.pointerDown(x, y, snap, shift2, mod),
|
|
35511
36734
|
drawingsPointerMove: (x, y, snap, shift2, mod) => this.userDrawings?.pointerMove(x, y, snap, shift2, mod),
|
|
35512
36735
|
drawingsPointerUp: (x, y, snap) => this.userDrawings?.pointerUp(x, y, snap),
|
|
35513
|
-
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),
|
|
35514
36737
|
drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
|
|
35515
36738
|
drawingsClearTransient: () => this.userDrawings?.clearTransient()
|
|
35516
36739
|
});
|
|
@@ -35526,8 +36749,18 @@ var NativeRenderer = class {
|
|
|
35526
36749
|
this.plot.addEventListener("pointerleave", this.onScrollProximityLeave);
|
|
35527
36750
|
this.labelTooltip = new LabelTooltip(this.plot, {
|
|
35528
36751
|
theme: () => this.chromeTheme(),
|
|
35529
|
-
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
|
+
}
|
|
35530
36762
|
});
|
|
36763
|
+
this.chrome.setMarkIconReady(() => this.scheduler?.invalidate(2 /* Chrome */));
|
|
35531
36764
|
this.userDrawings = new UserDrawingController(this.wrapper, this.plot, this.drawingsCanvas, {
|
|
35532
36765
|
projector: () => this.drawingProjector(),
|
|
35533
36766
|
dpr: () => this.coords.dpr,
|
|
@@ -35742,29 +36975,40 @@ var NativeRenderer = class {
|
|
|
35742
36975
|
resize() {
|
|
35743
36976
|
this.syncSize();
|
|
35744
36977
|
}
|
|
35745
|
-
/**
|
|
35746
|
-
|
|
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
|
|
35747
36990
|
* scales or repaint the geometry/volume/VPVR/SDK layers once a second (that cost
|
|
35748
36991
|
* multiplies by the cell count in a multi-chart workspace). */
|
|
35749
36992
|
syncCountdownTimer() {
|
|
35750
36993
|
if (this.scene.showCountdown) {
|
|
35751
|
-
if (this.
|
|
35752
|
-
this.
|
|
36994
|
+
if (this.countdownUnsub == null) {
|
|
36995
|
+
const clock = this.hostClock ?? (this.ownClock ?? (this.ownClock = new SecondClock()));
|
|
36996
|
+
this.countdownUnsub = clock.onTick(() => {
|
|
35753
36997
|
if (this.scene.showCountdown && this.scene.bars.length > 0) this.scheduler?.invalidate(2 /* Chrome */);
|
|
35754
|
-
}
|
|
36998
|
+
});
|
|
35755
36999
|
}
|
|
35756
|
-
} else if (this.
|
|
35757
|
-
|
|
35758
|
-
this.
|
|
37000
|
+
} else if (this.countdownUnsub != null) {
|
|
37001
|
+
this.countdownUnsub();
|
|
37002
|
+
this.countdownUnsub = null;
|
|
35759
37003
|
}
|
|
35760
37004
|
}
|
|
35761
37005
|
destroy() {
|
|
35762
37006
|
if (this.introRaf != null) cancelAnimationFrame(this.introRaf);
|
|
35763
37007
|
this.loadingEl?.remove();
|
|
35764
37008
|
this.loadingEl = null;
|
|
35765
|
-
if (this.
|
|
35766
|
-
|
|
35767
|
-
this.
|
|
37009
|
+
if (this.countdownUnsub != null) {
|
|
37010
|
+
this.countdownUnsub();
|
|
37011
|
+
this.countdownUnsub = null;
|
|
35768
37012
|
}
|
|
35769
37013
|
this.scheduler?.destroy();
|
|
35770
37014
|
this.animator?.stop();
|
|
@@ -35789,6 +37033,11 @@ var NativeRenderer = class {
|
|
|
35789
37033
|
this.plot?.removeEventListener("pointerleave", this.onScrollProximityLeave);
|
|
35790
37034
|
this.labelTooltip?.destroy();
|
|
35791
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;
|
|
35792
37041
|
this.scrollButton?.remove();
|
|
35793
37042
|
this.scrollButton = null;
|
|
35794
37043
|
for (const l of this.extLayers) l.instance.destroy?.();
|
|
@@ -35847,8 +37096,8 @@ var NativeRenderer = class {
|
|
|
35847
37096
|
}
|
|
35848
37097
|
if (!this.introPlayed && this.bars.length > 0) {
|
|
35849
37098
|
this.introPlayed = true;
|
|
35850
|
-
if (this.
|
|
35851
|
-
this.playIntro(
|
|
37099
|
+
if (this.intro.style) {
|
|
37100
|
+
this.playIntro();
|
|
35852
37101
|
return;
|
|
35853
37102
|
}
|
|
35854
37103
|
}
|
|
@@ -35859,7 +37108,7 @@ var NativeRenderer = class {
|
|
|
35859
37108
|
const last = this.bars[n - 1];
|
|
35860
37109
|
if (last && bar.time === last.time) {
|
|
35861
37110
|
this.bars[n - 1] = bar;
|
|
35862
|
-
if (this.
|
|
37111
|
+
if (!this.animLiveBar.on || this.liveEaseTime !== bar.time) {
|
|
35863
37112
|
this.syncLiveEase(bar);
|
|
35864
37113
|
} else {
|
|
35865
37114
|
this.animator.start();
|
|
@@ -35875,11 +37124,11 @@ var NativeRenderer = class {
|
|
|
35875
37124
|
this.scene.bars = this.bars;
|
|
35876
37125
|
this.scheduler.invalidate(4 /* Full */);
|
|
35877
37126
|
}
|
|
35878
|
-
/** Set the
|
|
35879
|
-
*
|
|
35880
|
-
|
|
35881
|
-
this.
|
|
35882
|
-
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;
|
|
35883
37132
|
}
|
|
35884
37133
|
/** Snap the eased forming-bar state to `bar` — no glide (a fresh bar or the first tick of one). */
|
|
35885
37134
|
syncLiveEase(bar) {
|
|
@@ -35893,7 +37142,7 @@ var NativeRenderer = class {
|
|
|
35893
37142
|
const target = this.bars[this.bars.length - 1];
|
|
35894
37143
|
if (!target || this.liveEaseTime !== target.time) return false;
|
|
35895
37144
|
const eps = Math.max(1e-9, Math.abs(target.close) * 1e-6);
|
|
35896
|
-
const tau = this.
|
|
37145
|
+
const tau = this.animLiveBar.tau;
|
|
35897
37146
|
const nh = easeToward(this.liveEaseHigh, target.high, dtMs, tau);
|
|
35898
37147
|
const nl = easeToward(this.liveEaseLow, target.low, dtMs, tau);
|
|
35899
37148
|
const nc = easeToward(this.liveEaseClose, target.close, dtMs, tau);
|
|
@@ -36033,10 +37282,12 @@ var NativeRenderer = class {
|
|
|
36033
37282
|
this.refreshAnchorOffset(model);
|
|
36034
37283
|
if (model.native && this.extLayers.some((l) => l.def.id === model.native.type)) this.scene.assignIndicatorZTop(model.id);
|
|
36035
37284
|
else this.scene.assignIndicatorZ(model.id);
|
|
36036
|
-
|
|
36037
|
-
|
|
36038
|
-
|
|
36039
|
-
|
|
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
|
+
}
|
|
36040
37291
|
if (model.native?.type === "volume") {
|
|
36041
37292
|
this.volumeActive = true;
|
|
36042
37293
|
this.volumeHidden = false;
|
|
@@ -36183,7 +37434,7 @@ var NativeRenderer = class {
|
|
|
36183
37434
|
this.settingsDialog?.setHiddenSettings(this.hiddenSettings);
|
|
36184
37435
|
}
|
|
36185
37436
|
listSettingsIds() {
|
|
36186
|
-
return settingsIdCatalog(this.hostSettingsSections);
|
|
37437
|
+
return settingsIdCatalog(this.hostSettingsSections, this.markGroupsInUse());
|
|
36187
37438
|
}
|
|
36188
37439
|
onChartTypeSettingsChange(cb) {
|
|
36189
37440
|
this.chartTypeSettingsCbs.add(cb);
|
|
@@ -36209,6 +37460,22 @@ var NativeRenderer = class {
|
|
|
36209
37460
|
this.axisLongPressCbs.add(cb);
|
|
36210
37461
|
return () => this.axisLongPressCbs.delete(cb);
|
|
36211
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
|
+
}
|
|
36212
37479
|
onViewportChange(cb) {
|
|
36213
37480
|
this.viewportCbs.add(cb);
|
|
36214
37481
|
return () => this.viewportCbs.delete(cb);
|
|
@@ -36262,7 +37529,7 @@ var NativeRenderer = class {
|
|
|
36262
37529
|
this.zoomAnchorX = anchorX;
|
|
36263
37530
|
this.panVelocity = 0;
|
|
36264
37531
|
this.scrollTargetRO = null;
|
|
36265
|
-
if (!this.animZoom) {
|
|
37532
|
+
if (!this.animZoom.on) {
|
|
36266
37533
|
const v = this.clampViewport(barSpacing, this.anchoredRightOffset(barSpacing));
|
|
36267
37534
|
this.coords.setViewport(v);
|
|
36268
37535
|
this.targetBarSpacing = v.barSpacing;
|
|
@@ -36275,7 +37542,7 @@ var NativeRenderer = class {
|
|
|
36275
37542
|
}
|
|
36276
37543
|
/** Inertial pan: continue with a rightOffset velocity (logical units / ms) that decays. */
|
|
36277
37544
|
fling(velocity) {
|
|
36278
|
-
if (!this.animPan) return;
|
|
37545
|
+
if (!this.animPan.on) return;
|
|
36279
37546
|
this.scrollTargetRO = null;
|
|
36280
37547
|
this.panVelocity = velocity;
|
|
36281
37548
|
this.animator.start();
|
|
@@ -36315,7 +37582,7 @@ var NativeRenderer = class {
|
|
|
36315
37582
|
let active = false;
|
|
36316
37583
|
const tbs = this.targetBarSpacing;
|
|
36317
37584
|
if (Math.abs(barSpacing - tbs) > tbs * 1e-3) {
|
|
36318
|
-
barSpacing = clampBarSpacing(easeToward(barSpacing, tbs, dtMs,
|
|
37585
|
+
barSpacing = clampBarSpacing(easeToward(barSpacing, tbs, dtMs, this.animZoom.tau));
|
|
36319
37586
|
rightOffset = this.anchoredRightOffset(barSpacing);
|
|
36320
37587
|
active = true;
|
|
36321
37588
|
} else if (barSpacing !== tbs) {
|
|
@@ -36325,13 +37592,14 @@ var NativeRenderer = class {
|
|
|
36325
37592
|
const stopVel = FLING_STOP_PX / Math.max(1e-6, barSpacing * this.coords.spacingScale);
|
|
36326
37593
|
if (Math.abs(this.panVelocity) > stopVel) {
|
|
36327
37594
|
rightOffset += this.panVelocity * dtMs;
|
|
36328
|
-
|
|
37595
|
+
const tau = this.animPan.tau;
|
|
37596
|
+
this.panVelocity = tau > 0 ? this.panVelocity * Math.exp(-dtMs / tau) : 0;
|
|
36329
37597
|
if (Math.abs(this.panVelocity) <= stopVel) this.panVelocity = 0;
|
|
36330
37598
|
else active = true;
|
|
36331
37599
|
}
|
|
36332
37600
|
if (this.scrollTargetRO != null) {
|
|
36333
37601
|
const target = this.scrollTargetRO;
|
|
36334
|
-
const next = easeToward(rightOffset, target, dtMs,
|
|
37602
|
+
const next = easeToward(rightOffset, target, dtMs, this.animScroll.tau);
|
|
36335
37603
|
if (Math.abs(next - target) < 1e-3) {
|
|
36336
37604
|
rightOffset = target;
|
|
36337
37605
|
this.scrollTargetRO = null;
|
|
@@ -36359,6 +37627,7 @@ var NativeRenderer = class {
|
|
|
36359
37627
|
* through Math.log), not a non-linear jump. */
|
|
36360
37628
|
easeScales(dtMs) {
|
|
36361
37629
|
let moving = false;
|
|
37630
|
+
const tau = this.animAutoscale.tau;
|
|
36362
37631
|
for (const pane of this.scene.panes.values()) {
|
|
36363
37632
|
const t = pane.scaleTarget;
|
|
36364
37633
|
const s = pane.scale;
|
|
@@ -36366,8 +37635,8 @@ var NativeRenderer = class {
|
|
|
36366
37635
|
const lt0 = Math.log(t.min);
|
|
36367
37636
|
const lt1 = Math.log(t.max);
|
|
36368
37637
|
const lspan = Math.max(1e-9, Math.abs(lt1 - lt0));
|
|
36369
|
-
const n0 = easeToward(Math.log(s.min), lt0, dtMs,
|
|
36370
|
-
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);
|
|
36371
37640
|
if (Math.abs(n0 - lt0) <= lspan * 1e-3 && Math.abs(n1 - lt1) <= lspan * 1e-3) {
|
|
36372
37641
|
pane.scale = { min: t.min, max: t.max, log: true };
|
|
36373
37642
|
} else {
|
|
@@ -36377,8 +37646,8 @@ var NativeRenderer = class {
|
|
|
36377
37646
|
continue;
|
|
36378
37647
|
}
|
|
36379
37648
|
const span = Math.max(1e-9, Math.abs(t.max - t.min));
|
|
36380
|
-
let nmin = easeToward(s.min, t.min, dtMs,
|
|
36381
|
-
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);
|
|
36382
37651
|
if (Math.abs(nmin - t.min) <= span * 1e-3 && Math.abs(nmax - t.max) <= span * 1e-3) {
|
|
36383
37652
|
nmin = t.min;
|
|
36384
37653
|
nmax = t.max;
|
|
@@ -36391,8 +37660,8 @@ var NativeRenderer = class {
|
|
|
36391
37660
|
const t = sl.scaleTarget;
|
|
36392
37661
|
const s = sl.scale;
|
|
36393
37662
|
const span = Math.max(1e-9, Math.abs(t.max - t.min));
|
|
36394
|
-
let nmin = easeToward(s.min, t.min, dtMs,
|
|
36395
|
-
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);
|
|
36396
37665
|
if (Math.abs(nmin - t.min) <= span * 1e-3 && Math.abs(nmax - t.max) <= span * 1e-3) {
|
|
36397
37666
|
nmin = t.min;
|
|
36398
37667
|
nmax = t.max;
|
|
@@ -36413,6 +37682,8 @@ var NativeRenderer = class {
|
|
|
36413
37682
|
this.scene.crosshair = null;
|
|
36414
37683
|
this.hoverSeparatorY = null;
|
|
36415
37684
|
this.lastPointer = null;
|
|
37685
|
+
if (this.marksExpandedBy === "hover") this.setMarksExpanded(null);
|
|
37686
|
+
this.setMarkHover(null);
|
|
36416
37687
|
this.scheduler.invalidate(1 /* Cursor */);
|
|
36417
37688
|
this.hoverLogical = null;
|
|
36418
37689
|
const empty = { time: null, price: null, paneKind: null, values: /* @__PURE__ */ new Map(), ohlc: null };
|
|
@@ -36424,6 +37695,8 @@ var NativeRenderer = class {
|
|
|
36424
37695
|
this.lastPointer = inData ? { x, y } : null;
|
|
36425
37696
|
this.hoverSeparatorY = x >= 0 && y >= 0 && y <= this.coords.height ? this.separatorHoverY(y) : null;
|
|
36426
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);
|
|
36427
37700
|
const logical = Math.round(this.coords.xToLogical(x));
|
|
36428
37701
|
const onBar = logical >= 0 && logical < this.coords.barCount;
|
|
36429
37702
|
const time = onBar ? this.coords.logicalToTime(logical) : null;
|
|
@@ -36453,6 +37726,76 @@ var NativeRenderer = class {
|
|
|
36453
37726
|
const onBar = logical >= 0 && logical < this.coords.barCount;
|
|
36454
37727
|
for (const cb of this.clickCbs) cb({ time: onBar ? this.coords.logicalToTime(logical) : null, price: null });
|
|
36455
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
|
+
}
|
|
36456
37799
|
paneAtY(y) {
|
|
36457
37800
|
return this.paneNodeAtY(y);
|
|
36458
37801
|
}
|
|
@@ -36844,6 +38187,7 @@ var NativeRenderer = class {
|
|
|
36844
38187
|
} else if (repaintsChrome(level) && this.paintedData) {
|
|
36845
38188
|
this.chrome.prepare(this.scene, this.coords, this.theme);
|
|
36846
38189
|
this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
|
|
38190
|
+
this.trackMarkPopover();
|
|
36847
38191
|
}
|
|
36848
38192
|
this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
|
|
36849
38193
|
if (!repaintsData(level) && this.paintedData) this.repaintCursorLayers();
|
|
@@ -36859,7 +38203,7 @@ var NativeRenderer = class {
|
|
|
36859
38203
|
const lp = this.layerPane(l.def.id) ?? pane;
|
|
36860
38204
|
if (lp.collapsed) continue;
|
|
36861
38205
|
l.instance.render(this.extLayerArgs(l.def.id, lp.scale, lp.bounds, nowMs));
|
|
36862
|
-
if (this.animZoom && l.instance.animating?.()) this.animator.start();
|
|
38206
|
+
if (this.animZoom.on && l.instance.animating?.()) this.animator.start();
|
|
36863
38207
|
}
|
|
36864
38208
|
}
|
|
36865
38209
|
/** Blank one SDK layer canvas (a collapsed host pane suppresses the layer's painting). */
|
|
@@ -36924,7 +38268,7 @@ var NativeRenderer = class {
|
|
|
36924
38268
|
const args = this.extLayerArgs(l.def.id, lp.scale, lp.bounds, nowMs);
|
|
36925
38269
|
l.instance.render(args);
|
|
36926
38270
|
if (lp === pane) folded = foldBaseModulation(folded, l.instance.modulateBase?.(args) ?? null);
|
|
36927
|
-
if (this.animZoom && l.instance.animating?.()) this.animator.start();
|
|
38271
|
+
if (this.animZoom.on && l.instance.animating?.()) this.animator.start();
|
|
36928
38272
|
}
|
|
36929
38273
|
if (folded) {
|
|
36930
38274
|
if (folded.candleBodyScale != null) this.backend.candleBodyScale = clamp012(folded.candleBodyScale) || 0.01;
|
|
@@ -36943,6 +38287,7 @@ var NativeRenderer = class {
|
|
|
36943
38287
|
this.backdropRenderer.render(this.scene, this.coords, this.theme, gridAlpha);
|
|
36944
38288
|
this.backend.render(this.scene, this.coords, this.theme);
|
|
36945
38289
|
this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
|
|
38290
|
+
this.trackMarkPopover();
|
|
36946
38291
|
this.userDrawings?.render();
|
|
36947
38292
|
if (easeLive && liveActual) this.bars[li] = liveActual;
|
|
36948
38293
|
this.paintedData = true;
|
|
@@ -37455,7 +38800,7 @@ var NativeRenderer = class {
|
|
|
37455
38800
|
const map2 = /* @__PURE__ */ new Map();
|
|
37456
38801
|
for (const pane of this.scene.panes.values()) {
|
|
37457
38802
|
if (!pane.collapsed) continue;
|
|
37458
|
-
const models = this.scene.orderedIndicatorsForPane(pane.id);
|
|
38803
|
+
const models = this.scene.orderedIndicatorsForPane(pane.id).filter((m) => m.legend !== false);
|
|
37459
38804
|
const merged = new Set(this.scene.ownScaleIndicatorsForPane(pane.id).map((m) => m.id));
|
|
37460
38805
|
const master = models.find((m) => !merged.has(m.id)) ?? models[0];
|
|
37461
38806
|
map2.set(pane.id, master?.id ?? null);
|
|
@@ -37704,6 +39049,8 @@ function volumeLayerData(inputs) {
|
|
|
37704
39049
|
}
|
|
37705
39050
|
var VolumeIndicator = class {
|
|
37706
39051
|
constructor() {
|
|
39052
|
+
/** Null until start() — pre-start setInputs/resume must record without pushing. */
|
|
39053
|
+
this.ctx = null;
|
|
37707
39054
|
this.inputs = {};
|
|
37708
39055
|
}
|
|
37709
39056
|
start(ctx, inputs) {
|
|
@@ -37720,13 +39067,13 @@ var VolumeIndicator = class {
|
|
|
37720
39067
|
}
|
|
37721
39068
|
setInputs(inputs) {
|
|
37722
39069
|
this.inputs = inputs;
|
|
37723
|
-
this.ctx
|
|
39070
|
+
this.ctx?.pushData(volumeLayerData(inputs));
|
|
37724
39071
|
}
|
|
37725
39072
|
/** Hiding is a renderer-layer flag (set via `setIndicatorVisible`); no resources to free. */
|
|
37726
39073
|
suspend() {
|
|
37727
39074
|
}
|
|
37728
39075
|
resume() {
|
|
37729
|
-
this.ctx
|
|
39076
|
+
this.ctx?.pushData(volumeLayerData(this.inputs));
|
|
37730
39077
|
}
|
|
37731
39078
|
stop() {
|
|
37732
39079
|
}
|
|
@@ -37772,6 +39119,8 @@ function vpvrLayerData(inputs) {
|
|
|
37772
39119
|
}
|
|
37773
39120
|
var VpvrIndicator = class {
|
|
37774
39121
|
constructor() {
|
|
39122
|
+
/** Null until start() — pre-start setInputs/resume must record without pushing. */
|
|
39123
|
+
this.ctx = null;
|
|
37775
39124
|
this.inputs = {};
|
|
37776
39125
|
}
|
|
37777
39126
|
start(ctx, inputs) {
|
|
@@ -37788,13 +39137,13 @@ var VpvrIndicator = class {
|
|
|
37788
39137
|
}
|
|
37789
39138
|
setInputs(inputs) {
|
|
37790
39139
|
this.inputs = inputs;
|
|
37791
|
-
this.ctx
|
|
39140
|
+
this.ctx?.pushData(vpvrLayerData(inputs));
|
|
37792
39141
|
}
|
|
37793
39142
|
/** Hiding is a renderer-layer flag (set via `setIndicatorVisible`); no resources to free. */
|
|
37794
39143
|
suspend() {
|
|
37795
39144
|
}
|
|
37796
39145
|
resume() {
|
|
37797
|
-
this.ctx
|
|
39146
|
+
this.ctx?.pushData(vpvrLayerData(this.inputs));
|
|
37798
39147
|
}
|
|
37799
39148
|
stop() {
|
|
37800
39149
|
}
|
|
@@ -40246,6 +41595,7 @@ var Vela = class {
|
|
|
40246
41595
|
if (Object.keys(defaults2).length > 0) this.rendererControl.set(defaults2);
|
|
40247
41596
|
this.panesControl = new PanesControl(this.orchestrator);
|
|
40248
41597
|
this.drawingsControl = new DrawingsControl(this.orchestrator.drawings);
|
|
41598
|
+
this.marksControl = new MarksControl(this.orchestrator.marks);
|
|
40249
41599
|
}
|
|
40250
41600
|
/**
|
|
40251
41601
|
* Register a scripting engine so `addIndicator({ language })` can run that
|
|
@@ -40464,6 +41814,17 @@ var Vela = class {
|
|
|
40464
41814
|
get drawings() {
|
|
40465
41815
|
return this.drawingsControl;
|
|
40466
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
|
+
}
|
|
40467
41828
|
on(event, handler) {
|
|
40468
41829
|
return this.orchestrator.events.on(event, handler);
|
|
40469
41830
|
}
|
|
@@ -41915,7 +43276,7 @@ var ChartCell = class {
|
|
|
41915
43276
|
// A RESTORED ledger is authoritative for the auto-added volume too: a
|
|
41916
43277
|
// slot persisted without it must come back without it (fresh slots
|
|
41917
43278
|
// keep the workspace default).
|
|
41918
|
-
volume: seed.indicators ? seed.indicators.natives.
|
|
43279
|
+
volume: seed.indicators ? seed.indicators.natives.some((e) => ledgerNativeType(e) === "volume") : deps.volume,
|
|
41919
43280
|
nativeBackend: deps.nativeBackend,
|
|
41920
43281
|
// The user's drawings option minus its toolbar: one SHARED bar serves
|
|
41921
43282
|
// the whole workspace (per-cell bars would cost a 44px gutter each).
|
|
@@ -43249,6 +44610,9 @@ var VelaWorkspace = class {
|
|
|
43249
44610
|
this.events = new TypedEventBus();
|
|
43250
44611
|
this.feed = new MultiProviderFeed();
|
|
43251
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();
|
|
43252
44616
|
this.pool = /* @__PURE__ */ new Map();
|
|
43253
44617
|
this.trackSizes = /* @__PURE__ */ new Map();
|
|
43254
44618
|
this.resizeObserver = null;
|
|
@@ -43526,6 +44890,7 @@ var VelaWorkspace = class {
|
|
|
43526
44890
|
this.drawToolbar?.setDrawingsSyncMode(!!this.syncOpts.drawings);
|
|
43527
44891
|
this.bottombar = opts.bottombar !== false ? new Bottombar(this.root, {
|
|
43528
44892
|
timezone: this.timezone,
|
|
44893
|
+
clock: this.clock,
|
|
43529
44894
|
onRange: (preset) => {
|
|
43530
44895
|
this.active.applyRange(preset);
|
|
43531
44896
|
this.bottombar?.setActiveRange(preset.id);
|
|
@@ -43969,6 +45334,7 @@ var VelaWorkspace = class {
|
|
|
43969
45334
|
const cell = this.activeId ? this.cellsById.get(this.activeId) : void 0;
|
|
43970
45335
|
const paneMax = cell ? cell.chart.panes.list().some((p) => p.maximized) : false;
|
|
43971
45336
|
this.mobileBar.setMaximizeActive(this.maximizedId != null || paneMax);
|
|
45337
|
+
this.mobileBar.setMaximizeVisible(this.def.cells.length > 1);
|
|
43972
45338
|
}
|
|
43973
45339
|
/**
|
|
43974
45340
|
* Trade the SLOTS of two live cells — the grid arrangement changes, the cells
|
|
@@ -44269,6 +45635,7 @@ var VelaWorkspace = class {
|
|
|
44269
45635
|
if (id === this.activeId) cell.host.dataset.active = "1";
|
|
44270
45636
|
this.wireCell(cell);
|
|
44271
45637
|
cell.chart.renderer.setLayoutMode(this.layoutCtl.current);
|
|
45638
|
+
cell.chart.renderer.setWallClock(this.clock);
|
|
44272
45639
|
cell.setControlsSuspended(this.layoutCtl.current === "mobile");
|
|
44273
45640
|
if (this.favs.length > 0) cell.chart.drawings.setFavorites(this.favs);
|
|
44274
45641
|
cell.setManifest(this.manifest, pooled?.indicators == null);
|