@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/workspace.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?.();
|
|
@@ -5146,9 +5168,12 @@ function mergeConfig(base, patch) {
|
|
|
5146
5168
|
const gh = asObject(grid.horzLines);
|
|
5147
5169
|
const cross = asObject(p.crosshair);
|
|
5148
5170
|
const ps = asObject(p.priceScale);
|
|
5171
|
+
const anim = asObject(p.animations);
|
|
5149
5172
|
const panes = asObject(p.panes);
|
|
5150
5173
|
const trades = asObject(p.trades);
|
|
5151
5174
|
const ts = asObject(p.timeScale);
|
|
5175
|
+
const marks = asObject(p.marks);
|
|
5176
|
+
const markGroups = asObject(marks.groups);
|
|
5152
5177
|
const candles = asObject(p.candles);
|
|
5153
5178
|
const bars = asObject(p.bars);
|
|
5154
5179
|
const line = asObject(p.line);
|
|
@@ -5195,6 +5220,12 @@ function mergeConfig(base, patch) {
|
|
|
5195
5220
|
countdown: isBool(ps.countdown) ? ps.countdown : base.priceScale.countdown,
|
|
5196
5221
|
animateLastPrice: isBool(ps.animateLastPrice) ? ps.animateLastPrice : base.priceScale.animateLastPrice
|
|
5197
5222
|
},
|
|
5223
|
+
animations: {
|
|
5224
|
+
zoom: isBool(anim.zoom) ? anim.zoom : base.animations.zoom,
|
|
5225
|
+
pan: isBool(anim.pan) ? anim.pan : base.animations.pan,
|
|
5226
|
+
autoscale: isBool(anim.autoscale) ? anim.autoscale : base.animations.autoscale,
|
|
5227
|
+
intro: isBool(anim.intro) ? anim.intro : base.animations.intro
|
|
5228
|
+
},
|
|
5198
5229
|
panes: {
|
|
5199
5230
|
separatorColor: isColor(panes.separatorColor) ? panes.separatorColor : base.panes.separatorColor
|
|
5200
5231
|
},
|
|
@@ -5209,6 +5240,15 @@ function mergeConfig(base, patch) {
|
|
|
5209
5240
|
timeScale: {
|
|
5210
5241
|
timezone: typeof ts.timezone === "string" && ts.timezone ? ts.timezone : base.timeScale.timezone
|
|
5211
5242
|
},
|
|
5243
|
+
marks: {
|
|
5244
|
+
visible: isBool(marks.visible) ? marks.visible : base.marks.visible,
|
|
5245
|
+
// Additive like `stacking.series`: a patch names only the groups it carries, so a
|
|
5246
|
+
// choice stored for a group the host has not registered yet survives verbatim.
|
|
5247
|
+
groups: {
|
|
5248
|
+
...base.marks.groups,
|
|
5249
|
+
...Object.fromEntries(Object.entries(markGroups).filter(([, v]) => isBool(v)))
|
|
5250
|
+
}
|
|
5251
|
+
},
|
|
5212
5252
|
candles: {
|
|
5213
5253
|
upColor: isColor(candles.upColor) ? candles.upColor : base.candles.upColor,
|
|
5214
5254
|
downColor: isColor(candles.downColor) ? candles.downColor : base.candles.downColor,
|
|
@@ -5921,6 +5961,40 @@ var Topbar = class {
|
|
|
5921
5961
|
}
|
|
5922
5962
|
};
|
|
5923
5963
|
|
|
5964
|
+
// src/core/util/wall-clock.ts
|
|
5965
|
+
var BOUNDARY_SLACK_MS = 5;
|
|
5966
|
+
var SecondClock = class _SecondClock {
|
|
5967
|
+
constructor(now = () => Date.now()) {
|
|
5968
|
+
this.now = now;
|
|
5969
|
+
this.subs = /* @__PURE__ */ new Set();
|
|
5970
|
+
this.timer = null;
|
|
5971
|
+
}
|
|
5972
|
+
onTick(cb) {
|
|
5973
|
+
this.subs.add(cb);
|
|
5974
|
+
if (this.timer == null) this.arm();
|
|
5975
|
+
return () => {
|
|
5976
|
+
this.subs.delete(cb);
|
|
5977
|
+
if (this.subs.size === 0 && this.timer != null) {
|
|
5978
|
+
clearTimeout(this.timer);
|
|
5979
|
+
this.timer = null;
|
|
5980
|
+
}
|
|
5981
|
+
};
|
|
5982
|
+
}
|
|
5983
|
+
/** Milliseconds from `now` to just past the next second boundary. */
|
|
5984
|
+
static delayToNextSecond(now) {
|
|
5985
|
+
const intoSecond = (now % 1e3 + 1e3) % 1e3;
|
|
5986
|
+
return 1e3 - intoSecond + BOUNDARY_SLACK_MS;
|
|
5987
|
+
}
|
|
5988
|
+
arm() {
|
|
5989
|
+
this.timer = setTimeout(() => {
|
|
5990
|
+
this.timer = null;
|
|
5991
|
+
const now = this.now();
|
|
5992
|
+
for (const cb of this.subs) cb(now);
|
|
5993
|
+
if (this.subs.size > 0) this.arm();
|
|
5994
|
+
}, _SecondClock.delayToNextSecond(this.now()));
|
|
5995
|
+
}
|
|
5996
|
+
};
|
|
5997
|
+
|
|
5924
5998
|
// src/core/timezones.ts
|
|
5925
5999
|
var TIMEZONES = [
|
|
5926
6000
|
{ value: "Etc/UTC", label: "UTC" },
|
|
@@ -6100,7 +6174,6 @@ var Bottombar = class {
|
|
|
6100
6174
|
this.rangeButtons = /* @__PURE__ */ new Map();
|
|
6101
6175
|
this.sessionButtons = /* @__PURE__ */ new Map();
|
|
6102
6176
|
this.sessionEl = null;
|
|
6103
|
-
this.timer = null;
|
|
6104
6177
|
this.timezone = opts.timezone;
|
|
6105
6178
|
const doc = host.ownerDocument;
|
|
6106
6179
|
injectStyles(STYLE_ID3, CSS4, doc);
|
|
@@ -6165,7 +6238,7 @@ var Bottombar = class {
|
|
|
6165
6238
|
}
|
|
6166
6239
|
});
|
|
6167
6240
|
this.tick();
|
|
6168
|
-
this.
|
|
6241
|
+
this.unsubClock = (opts.clock ?? new SecondClock()).onTick(() => this.tick());
|
|
6169
6242
|
}
|
|
6170
6243
|
setTimezone(zone) {
|
|
6171
6244
|
this.timezone = zone;
|
|
@@ -6194,7 +6267,7 @@ var Bottombar = class {
|
|
|
6194
6267
|
}
|
|
6195
6268
|
}
|
|
6196
6269
|
destroy() {
|
|
6197
|
-
|
|
6270
|
+
this.unsubClock();
|
|
6198
6271
|
this.tzMenu.destroy();
|
|
6199
6272
|
this.settingsTip?.destroy();
|
|
6200
6273
|
this.el.remove();
|
|
@@ -14893,6 +14966,11 @@ function followStep(cur, target, follow = FOLLOW, epsFrac = 15e-4) {
|
|
|
14893
14966
|
const done = Math.abs(target.from - nf) <= eps && Math.abs(target.to - nt) <= eps;
|
|
14894
14967
|
return { cur: done ? { ...target } : { from: nf, to: nt }, done };
|
|
14895
14968
|
}
|
|
14969
|
+
function zoomAnimated(chart) {
|
|
14970
|
+
const control = chart.renderer;
|
|
14971
|
+
if (!control || !control.supports("animZoom")) return true;
|
|
14972
|
+
return Boolean(control.get("animZoom"));
|
|
14973
|
+
}
|
|
14896
14974
|
var Glider = class {
|
|
14897
14975
|
constructor(chart) {
|
|
14898
14976
|
this.chart = chart;
|
|
@@ -14920,8 +14998,14 @@ var Glider = class {
|
|
|
14920
14998
|
const chart = this.chart();
|
|
14921
14999
|
const base = this.target ?? chart?.getVisibleRange();
|
|
14922
15000
|
if (!chart || !base) return;
|
|
15001
|
+
const target = make(base);
|
|
15002
|
+
if (!zoomAnimated(chart)) {
|
|
15003
|
+
this.stop();
|
|
15004
|
+
chart.setVisibleRange(target);
|
|
15005
|
+
return;
|
|
15006
|
+
}
|
|
14923
15007
|
this.cur ?? (this.cur = { ...base });
|
|
14924
|
-
this.target =
|
|
15008
|
+
this.target = target;
|
|
14925
15009
|
if (!this.raf) this.tick();
|
|
14926
15010
|
}
|
|
14927
15011
|
tick() {
|
|
@@ -15041,6 +15125,7 @@ var CSS13 = `
|
|
|
15041
15125
|
font-weight: 600;
|
|
15042
15126
|
-webkit-tap-highlight-color: transparent;
|
|
15043
15127
|
}
|
|
15128
|
+
.vela-mb-item[hidden] { display: none !important; }
|
|
15044
15129
|
.vela-mb-item:active { background: var(--vela-hover); }
|
|
15045
15130
|
.vela-mb-item .vela-icon { font-size: 18px; width: 18px; height: 18px; }
|
|
15046
15131
|
/* A lit stop (the maximize toggle while something is isolated): the inverse
|
|
@@ -15130,6 +15215,12 @@ var MobileBar = class {
|
|
|
15130
15215
|
this.maxEl.setAttribute("aria-label", on ? "Restore layout" : "Maximize chart");
|
|
15131
15216
|
this.maxEl.replaceChildren(iconEl(on ? "restore" : "maximize", this.el.ownerDocument));
|
|
15132
15217
|
}
|
|
15218
|
+
/** Show the maximize stop only while the grid holds more than one chart — on a
|
|
15219
|
+
* one-cell layout there is nothing to isolate, so the stop would be a dead press. */
|
|
15220
|
+
setMaximizeVisible(visible) {
|
|
15221
|
+
if (!this.maxEl) return;
|
|
15222
|
+
this.maxEl.hidden = !visible;
|
|
15223
|
+
}
|
|
15133
15224
|
destroy() {
|
|
15134
15225
|
this.el.remove();
|
|
15135
15226
|
}
|
|
@@ -18204,19 +18295,48 @@ function localStorageAdapter(storageKey) {
|
|
|
18204
18295
|
|
|
18205
18296
|
// src/core/options.ts
|
|
18206
18297
|
var normalizeSession = (v) => v === "regular" || v === "extended" ? v : void 0;
|
|
18298
|
+
var ZOOM_EASE_DEFAULT_MS = 70;
|
|
18299
|
+
var PAN_INERTIA_DEFAULT_MS = 110;
|
|
18300
|
+
var SCROLL_EASE_DEFAULT_MS = 130;
|
|
18301
|
+
var AUTOSCALE_EASE_DEFAULT_MS = 80;
|
|
18207
18302
|
var LIVE_BAR_EASE_DEFAULT_MS = 90;
|
|
18208
|
-
var
|
|
18209
|
-
|
|
18210
|
-
|
|
18303
|
+
var INTRO_DURATION_DEFAULT_MS = 650;
|
|
18304
|
+
var ANIMATION_EASE_MAX_MS = 1e3;
|
|
18305
|
+
var LIVE_BAR_EASE_MAX_MS = ANIMATION_EASE_MAX_MS;
|
|
18306
|
+
var INTRO_DURATION_MAX_MS = 5e3;
|
|
18307
|
+
function resolveEaseMs(value, defaultMs, maxMs = ANIMATION_EASE_MAX_MS) {
|
|
18308
|
+
if (value === true) return defaultMs;
|
|
18211
18309
|
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return 0;
|
|
18212
|
-
return Math.min(value,
|
|
18310
|
+
return Math.min(value, maxMs);
|
|
18311
|
+
}
|
|
18312
|
+
function resolveLiveBarEaseMs(value) {
|
|
18313
|
+
return resolveEaseMs(value, LIVE_BAR_EASE_DEFAULT_MS, LIVE_BAR_EASE_MAX_MS);
|
|
18314
|
+
}
|
|
18315
|
+
function resolveIntro(value) {
|
|
18316
|
+
const off = { style: false, duration: 0 };
|
|
18317
|
+
if (value === true) return { style: "settle", duration: INTRO_DURATION_DEFAULT_MS };
|
|
18318
|
+
if (value === "settle" || value === "grow") return { style: value, duration: INTRO_DURATION_DEFAULT_MS };
|
|
18319
|
+
if (value && typeof value === "object") {
|
|
18320
|
+
const o = value;
|
|
18321
|
+
const d = o.duration;
|
|
18322
|
+
return {
|
|
18323
|
+
style: o.style === "grow" ? "grow" : "settle",
|
|
18324
|
+
duration: typeof d === "number" && Number.isFinite(d) && d > 0 ? Math.min(d, INTRO_DURATION_MAX_MS) : INTRO_DURATION_DEFAULT_MS
|
|
18325
|
+
};
|
|
18326
|
+
}
|
|
18327
|
+
return off;
|
|
18213
18328
|
}
|
|
18214
18329
|
function resolveAnimations(animations) {
|
|
18215
|
-
if (
|
|
18330
|
+
if (animations === false) return { animZoom: 0, animPan: 0, animScroll: 0, animAutoscale: 0, animLiveBar: 0, animIntro: { style: false, duration: 0 } };
|
|
18331
|
+
const cfg = animations === true || animations == null ? {} : animations;
|
|
18332
|
+
const animPan = resolveEaseMs(cfg.pan ?? true, PAN_INERTIA_DEFAULT_MS);
|
|
18216
18333
|
return {
|
|
18217
|
-
animZoom:
|
|
18218
|
-
animPan
|
|
18219
|
-
|
|
18334
|
+
animZoom: resolveEaseMs(cfg.zoom ?? true, ZOOM_EASE_DEFAULT_MS),
|
|
18335
|
+
animPan,
|
|
18336
|
+
animScroll: cfg.scroll === void 0 ? animPan > 0 ? SCROLL_EASE_DEFAULT_MS : 0 : resolveEaseMs(cfg.scroll, SCROLL_EASE_DEFAULT_MS),
|
|
18337
|
+
animAutoscale: resolveEaseMs(cfg.autoscale ?? true, AUTOSCALE_EASE_DEFAULT_MS),
|
|
18338
|
+
animLiveBar: resolveLiveBarEaseMs(cfg.liveBar),
|
|
18339
|
+
animIntro: resolveIntro(cfg.intro ?? true)
|
|
18220
18340
|
};
|
|
18221
18341
|
}
|
|
18222
18342
|
|
|
@@ -18431,6 +18551,93 @@ function presetToRange(preset, bars) {
|
|
|
18431
18551
|
return { from: Math.max(first, from), to: last };
|
|
18432
18552
|
}
|
|
18433
18553
|
|
|
18554
|
+
// src/core/marks/MarksController.ts
|
|
18555
|
+
var MarksController = class {
|
|
18556
|
+
constructor(renderer, events) {
|
|
18557
|
+
this.renderer = renderer;
|
|
18558
|
+
/** Insertion-ordered — the order a cluster falls back to for equal times. */
|
|
18559
|
+
this.marks = /* @__PURE__ */ new Map();
|
|
18560
|
+
this.groups = /* @__PURE__ */ new Map();
|
|
18561
|
+
this.subs = [];
|
|
18562
|
+
this.enabled = !!renderer.capabilities.timelineMarks && typeof renderer.setTimelineMarks === "function";
|
|
18563
|
+
if (this.enabled && renderer.onMarkClick) this.subs.push(renderer.onMarkClick((e) => events.emit("mark:click", e)));
|
|
18564
|
+
}
|
|
18565
|
+
/** Whether the active renderer paints timeline marks. */
|
|
18566
|
+
get supported() {
|
|
18567
|
+
return this.enabled;
|
|
18568
|
+
}
|
|
18569
|
+
/** Add (or replace, by id) one mark. */
|
|
18570
|
+
add(mark) {
|
|
18571
|
+
this.marks.set(mark.id, validateMark(mark));
|
|
18572
|
+
this.sync();
|
|
18573
|
+
}
|
|
18574
|
+
/** Replace the whole set — a market switch. */
|
|
18575
|
+
set(marks) {
|
|
18576
|
+
this.marks.clear();
|
|
18577
|
+
for (const m of marks) this.marks.set(m.id, validateMark(m));
|
|
18578
|
+
this.sync();
|
|
18579
|
+
}
|
|
18580
|
+
remove(id) {
|
|
18581
|
+
const had = this.marks.delete(id);
|
|
18582
|
+
if (had) this.sync();
|
|
18583
|
+
return had;
|
|
18584
|
+
}
|
|
18585
|
+
clear() {
|
|
18586
|
+
if (this.marks.size === 0) return;
|
|
18587
|
+
this.marks.clear();
|
|
18588
|
+
this.sync();
|
|
18589
|
+
}
|
|
18590
|
+
/** Every mark, in insertion order (shallow copies — mutating one changes nothing). */
|
|
18591
|
+
all() {
|
|
18592
|
+
return [...this.marks.values()].map((m) => ({ ...m, glyph: { ...m.glyph } }));
|
|
18593
|
+
}
|
|
18594
|
+
/** Define (or replace) a group's presentation — its settings label and default visibility. */
|
|
18595
|
+
defineGroup(group) {
|
|
18596
|
+
if (!group || typeof group.id !== "string" || group.id.length === 0) throw new Error("[vela] marks.defineGroup: `id` must be a non-empty string");
|
|
18597
|
+
if (typeof group.label !== "string") throw new Error(`[vela] marks.defineGroup: group "${group.id}" needs a string \`label\``);
|
|
18598
|
+
this.groups.set(group.id, { ...group });
|
|
18599
|
+
this.sync();
|
|
18600
|
+
}
|
|
18601
|
+
/** The defined groups, in definition order. */
|
|
18602
|
+
groupDefinitions() {
|
|
18603
|
+
return [...this.groups.values()].map((g) => ({ ...g }));
|
|
18604
|
+
}
|
|
18605
|
+
/**
|
|
18606
|
+
* Show or hide one group's marks. The choice lives in the renderer's cosmetic config
|
|
18607
|
+
* (the `marks` feature) — what the settings dialog's Events checkboxes edit and what
|
|
18608
|
+
* a persisted chart restores — so it warns + no-ops on a renderer without it.
|
|
18609
|
+
*/
|
|
18610
|
+
setGroupVisible(id, visible) {
|
|
18611
|
+
if (!this.renderer.features.includes("marks")) {
|
|
18612
|
+
console.warn(`[vela] renderer "${this.renderer.name}" does not paint timeline marks \u2014 setGroupVisible ignored.`);
|
|
18613
|
+
return;
|
|
18614
|
+
}
|
|
18615
|
+
this.renderer.applyFeature("marks", { groups: { [id]: visible } });
|
|
18616
|
+
}
|
|
18617
|
+
/** A group's effective visibility: the user's (persisted) choice, else the group's declared default, else visible. */
|
|
18618
|
+
isGroupVisible(id) {
|
|
18619
|
+
const state = this.renderer.readFeature("marks");
|
|
18620
|
+
const chosen = state?.groups?.[id];
|
|
18621
|
+
if (typeof chosen === "boolean") return chosen;
|
|
18622
|
+
return this.groups.get(id)?.visible !== false;
|
|
18623
|
+
}
|
|
18624
|
+
destroy() {
|
|
18625
|
+
for (const unsub of this.subs) unsub();
|
|
18626
|
+
this.subs.length = 0;
|
|
18627
|
+
}
|
|
18628
|
+
sync() {
|
|
18629
|
+
if (!this.enabled) return;
|
|
18630
|
+
this.renderer.setTimelineMarks([...this.marks.values()], [...this.groups.values()]);
|
|
18631
|
+
}
|
|
18632
|
+
};
|
|
18633
|
+
function validateMark(mark) {
|
|
18634
|
+
if (!mark || typeof mark !== "object") throw new Error("[vela] marks: a mark must be an object");
|
|
18635
|
+
if (typeof mark.id !== "string" || mark.id.length === 0) throw new Error("[vela] marks: `id` must be a non-empty string");
|
|
18636
|
+
if (typeof mark.time !== "number" || !Number.isFinite(mark.time)) throw new Error(`[vela] marks: mark "${mark.id}" needs a finite epoch-ms \`time\``);
|
|
18637
|
+
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\``);
|
|
18638
|
+
return { ...mark, glyph: { ...mark.glyph } };
|
|
18639
|
+
}
|
|
18640
|
+
|
|
18434
18641
|
// src/core/engine/DrawingSeriesService.ts
|
|
18435
18642
|
var MAX_BARS = 5e3;
|
|
18436
18643
|
var PAD_FRAC = 0.25;
|
|
@@ -18748,6 +18955,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
18748
18955
|
marketKey: () => `${this.config.market.symbol ?? ""}|${this.config.market.session ?? ""}`
|
|
18749
18956
|
});
|
|
18750
18957
|
this.drawings = new DrawingController(this.renderer, this.events, config.drawings, drawingSeries);
|
|
18958
|
+
this.marks = new MarksController(this.renderer, this.events);
|
|
18751
18959
|
this.unresolvedUnsub = this.feed.onUnresolved?.((info) => {
|
|
18752
18960
|
this.endLoad();
|
|
18753
18961
|
this.events.emit("data:unresolved", info);
|
|
@@ -19120,6 +19328,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19120
19328
|
if (!record.native) continue;
|
|
19121
19329
|
record.native.instance.stop();
|
|
19122
19330
|
record.native.instance = record.native.descriptor.create();
|
|
19331
|
+
record.native.started = false;
|
|
19123
19332
|
record.pendingStructural = true;
|
|
19124
19333
|
if (record.hidden) {
|
|
19125
19334
|
record.native.stale = true;
|
|
@@ -19611,11 +19820,12 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19611
19820
|
record.session = void 0;
|
|
19612
19821
|
record.native?.instance.suspend();
|
|
19613
19822
|
if (record.renderHandle) this.renderer.setIndicatorVisible?.(record.renderHandle, false);
|
|
19823
|
+
else if (record.native) this.mountHiddenNativeRow(id, record);
|
|
19614
19824
|
} else {
|
|
19615
19825
|
if (record.renderHandle) this.renderer.setIndicatorVisible?.(record.renderHandle, true);
|
|
19616
19826
|
record.pendingStructural = true;
|
|
19617
19827
|
if (record.native) {
|
|
19618
|
-
if (record.native.stale) {
|
|
19828
|
+
if (record.native.stale || !record.native.started) {
|
|
19619
19829
|
record.native.stale = false;
|
|
19620
19830
|
const handle = this.handles.get(id);
|
|
19621
19831
|
if (handle) void this.startNativeIndicator(id, handle);
|
|
@@ -19698,6 +19908,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19698
19908
|
this.unresolvedUnsub = null;
|
|
19699
19909
|
this.feed.destroy?.();
|
|
19700
19910
|
this.drawings.destroy();
|
|
19911
|
+
this.marks.destroy();
|
|
19701
19912
|
this.renderer.destroy();
|
|
19702
19913
|
this.events.clear();
|
|
19703
19914
|
}
|
|
@@ -19796,6 +20007,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19796
20007
|
}
|
|
19797
20008
|
};
|
|
19798
20009
|
record.native.instance.start(ctx, record.inputValues);
|
|
20010
|
+
record.native.started = true;
|
|
19799
20011
|
} catch (err) {
|
|
19800
20012
|
this.fail(id, handle, err);
|
|
19801
20013
|
}
|
|
@@ -19810,6 +20022,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19810
20022
|
overlay: d.overlay,
|
|
19811
20023
|
paneHint: d.paneHint,
|
|
19812
20024
|
native: { type: record.native.type },
|
|
20025
|
+
...d.legend === false ? { legend: false } : {},
|
|
19813
20026
|
...out.paneAxis != null ? { paneAxis: out.paneAxis } : {},
|
|
19814
20027
|
series: out.series ?? [],
|
|
19815
20028
|
fills: out.fills ?? [],
|
|
@@ -19832,9 +20045,17 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19832
20045
|
* over it in place (`pendingStructural`), clears the spinner, and only THEN fires
|
|
19833
20046
|
* `indicator:added`/`ready` — so event semantics and `inspect()` (which skips
|
|
19834
20047
|
* loading records) still mean "the indicator produced output".
|
|
20048
|
+
*
|
|
20049
|
+
* A HIDDEN record mounts too — dimmed, no spinner (its session never starts while
|
|
20050
|
+
* hidden, so nothing is computing and no model will ever arrive to mount the row
|
|
20051
|
+
* later). Without this an indicator ADDED hidden (a restored ledger/ext entry) had
|
|
20052
|
+
* no legend row at all: invisible AND unreachable — the eye that unhides it never
|
|
20053
|
+
* existed. The hidden mount announces immediately for the same reason: the "first
|
|
20054
|
+
* computed model" that normally announces cannot come until the indicator is shown,
|
|
20055
|
+
* and host UIs (object tree, landing watchers) must know it exists NOW.
|
|
19835
20056
|
*/
|
|
19836
20057
|
mountLoadingPlaceholder(id, record) {
|
|
19837
|
-
if (record.renderHandle ||
|
|
20058
|
+
if (record.renderHandle || !record.prepared) return;
|
|
19838
20059
|
const meta = record.prepared.meta;
|
|
19839
20060
|
const model = {
|
|
19840
20061
|
id,
|
|
@@ -19858,8 +20079,34 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19858
20079
|
this.ensurePaneFor(paneId);
|
|
19859
20080
|
record.renderHandle = this.renderer.mountIndicator(model);
|
|
19860
20081
|
record.pendingStructural = true;
|
|
20082
|
+
if (record.hidden) {
|
|
20083
|
+
this.renderer.setIndicatorVisible?.(record.renderHandle, false);
|
|
20084
|
+
this.announce(record, this.handles.get(id));
|
|
20085
|
+
return;
|
|
20086
|
+
}
|
|
19861
20087
|
this.setLoading(record, true);
|
|
19862
20088
|
}
|
|
20089
|
+
/**
|
|
20090
|
+
* Mount the legend row for a NATIVE indicator that is being hidden BEFORE it ever
|
|
20091
|
+
* started (a restored-hidden ledger entry: `startNativeIndicator` bails on hidden
|
|
20092
|
+
* records, so no model — and therefore no row — would ever mount). The native
|
|
20093
|
+
* counterpart of {@link mountLoadingPlaceholder}'s hidden branch: an empty model
|
|
20094
|
+
* carries the title + inputs schema, the renderer marks the row hidden, and the
|
|
20095
|
+
* announce makes the indicator visible to host UIs. Showing later STARTS the
|
|
20096
|
+
* instance (the `started` flag path) and its first emit remounts over this row.
|
|
20097
|
+
*/
|
|
20098
|
+
mountHiddenNativeRow(id, record) {
|
|
20099
|
+
if (record.renderHandle || !record.native) return;
|
|
20100
|
+
const model = this.buildNativeModel(record, {});
|
|
20101
|
+
const paneId = this.routePane(id, model, record.options ?? {});
|
|
20102
|
+
this.placeModel(model, id, paneId);
|
|
20103
|
+
record.model = model;
|
|
20104
|
+
this.ensurePaneFor(paneId);
|
|
20105
|
+
record.renderHandle = this.renderer.mountIndicator(model);
|
|
20106
|
+
record.pendingStructural = true;
|
|
20107
|
+
this.renderer.setIndicatorVisible?.(record.renderHandle, false);
|
|
20108
|
+
this.announce(record, this.handles.get(id));
|
|
20109
|
+
}
|
|
19863
20110
|
/** Flip the record's loading state and reflect it in the legend row (spinner on/off). */
|
|
19864
20111
|
setLoading(record, loading) {
|
|
19865
20112
|
record.loading = loading;
|
|
@@ -19894,6 +20141,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
19894
20141
|
for (const r of this.registry.all()) {
|
|
19895
20142
|
const model = r.model;
|
|
19896
20143
|
if (!model) continue;
|
|
20144
|
+
if (model.legend === false) continue;
|
|
19897
20145
|
const paneId = model.paneId ?? "price";
|
|
19898
20146
|
if (!byPane.has(paneId)) byPane.set(paneId, []);
|
|
19899
20147
|
byPane.get(paneId).push({
|
|
@@ -20469,6 +20717,16 @@ var RendererControl = class {
|
|
|
20469
20717
|
this.renderer.setLayoutMode?.(mode);
|
|
20470
20718
|
return this;
|
|
20471
20719
|
}
|
|
20720
|
+
/**
|
|
20721
|
+
* Drive the renderer's time-of-day chrome (the countdown-to-bar-close chip) from the
|
|
20722
|
+
* host's own second pulse, so it ticks in step with a host clock display instead of
|
|
20723
|
+
* on a separate timer that can read a different second. `null` hands the pulse back
|
|
20724
|
+
* to the renderer. Silent no-op on a renderer without time-of-day chrome.
|
|
20725
|
+
*/
|
|
20726
|
+
setWallClock(clock) {
|
|
20727
|
+
this.renderer.setWallClock?.(clock);
|
|
20728
|
+
return this;
|
|
20729
|
+
}
|
|
20472
20730
|
};
|
|
20473
20731
|
|
|
20474
20732
|
// src/core/PanesControl.ts
|
|
@@ -20808,6 +21066,61 @@ var DrawingsControl = class {
|
|
|
20808
21066
|
}
|
|
20809
21067
|
};
|
|
20810
21068
|
|
|
21069
|
+
// src/core/MarksControl.ts
|
|
21070
|
+
var MarksControl = class {
|
|
21071
|
+
constructor(ctrl) {
|
|
21072
|
+
this.ctrl = ctrl;
|
|
21073
|
+
}
|
|
21074
|
+
/** Whether the active renderer paints timeline marks. */
|
|
21075
|
+
get supported() {
|
|
21076
|
+
return this.ctrl.supported;
|
|
21077
|
+
}
|
|
21078
|
+
/** Add one mark; an existing id is replaced in place. */
|
|
21079
|
+
add(mark) {
|
|
21080
|
+
this.ctrl.add(mark);
|
|
21081
|
+
return this;
|
|
21082
|
+
}
|
|
21083
|
+
/** Replace the whole set (a market switch). */
|
|
21084
|
+
set(marks) {
|
|
21085
|
+
this.ctrl.set(marks);
|
|
21086
|
+
return this;
|
|
21087
|
+
}
|
|
21088
|
+
remove(id) {
|
|
21089
|
+
this.ctrl.remove(id);
|
|
21090
|
+
return this;
|
|
21091
|
+
}
|
|
21092
|
+
clear() {
|
|
21093
|
+
this.ctrl.clear();
|
|
21094
|
+
return this;
|
|
21095
|
+
}
|
|
21096
|
+
/** Every mark, in insertion order. */
|
|
21097
|
+
all() {
|
|
21098
|
+
return this.ctrl.all();
|
|
21099
|
+
}
|
|
21100
|
+
/**
|
|
21101
|
+
* Define a visibility group's presentation: the label of its checkbox in chart
|
|
21102
|
+
* settings (the Events tab) and its default visibility. Marks may name a group
|
|
21103
|
+
* that was never defined — it then shows its capitalized id.
|
|
21104
|
+
*/
|
|
21105
|
+
defineGroup(group) {
|
|
21106
|
+
this.ctrl.defineGroup(group);
|
|
21107
|
+
return this;
|
|
21108
|
+
}
|
|
21109
|
+
/** The defined groups, in definition order. */
|
|
21110
|
+
groups() {
|
|
21111
|
+
return this.ctrl.groupDefinitions();
|
|
21112
|
+
}
|
|
21113
|
+
/** Show or hide one group's marks — the same switch as the settings checkbox, persisted with the chart's config. */
|
|
21114
|
+
setGroupVisible(id, visible = true) {
|
|
21115
|
+
this.ctrl.setGroupVisible(id, visible);
|
|
21116
|
+
return this;
|
|
21117
|
+
}
|
|
21118
|
+
/** A group's effective visibility (the user's choice, else the group's declared default). */
|
|
21119
|
+
isGroupVisible(id) {
|
|
21120
|
+
return this.ctrl.isGroupVisible(id);
|
|
21121
|
+
}
|
|
21122
|
+
};
|
|
21123
|
+
|
|
20811
21124
|
// src/core/model/inputs.ts
|
|
20812
21125
|
function inputVisible(when, values) {
|
|
20813
21126
|
if (!when) return true;
|
|
@@ -22856,6 +23169,8 @@ var NATIVE_CAPABILITIES = {
|
|
|
22856
23169
|
// canvas-painted into the owning indicator's interleave slice
|
|
22857
23170
|
trades: true,
|
|
22858
23171
|
// strategy order-fill markers (arrows + labels + fill-price ticks)
|
|
23172
|
+
timelineMarks: true,
|
|
23173
|
+
// host events on a lane above the time axis (glyphs + detail popup)
|
|
22859
23174
|
inputsUI: true
|
|
22860
23175
|
// reuses the DOM InputsUI
|
|
22861
23176
|
};
|
|
@@ -24423,6 +24738,31 @@ var Animator = class {
|
|
|
24423
24738
|
}
|
|
24424
24739
|
}
|
|
24425
24740
|
};
|
|
24741
|
+
var EaseSetting = class {
|
|
24742
|
+
/** `defaultMs` is what the on/off switch restores when nothing else was configured;
|
|
24743
|
+
* `initialMs` (default: `defaultMs`) is the starting value — 0 for a motion that
|
|
24744
|
+
* ships off. */
|
|
24745
|
+
constructor(defaultMs, initialMs = defaultMs) {
|
|
24746
|
+
this.onMs = defaultMs;
|
|
24747
|
+
this.ms = initialMs;
|
|
24748
|
+
}
|
|
24749
|
+
/** The active time-constant; 0 when off. */
|
|
24750
|
+
get tau() {
|
|
24751
|
+
return this.ms;
|
|
24752
|
+
}
|
|
24753
|
+
get on() {
|
|
24754
|
+
return this.ms > 0;
|
|
24755
|
+
}
|
|
24756
|
+
/** Set the time-constant (0 = off). A non-zero value becomes what `toggle(true)` restores. */
|
|
24757
|
+
set(ms) {
|
|
24758
|
+
this.ms = ms;
|
|
24759
|
+
if (ms > 0) this.onMs = ms;
|
|
24760
|
+
}
|
|
24761
|
+
/** On/off only — the duration stays the last one configured. */
|
|
24762
|
+
toggle(on) {
|
|
24763
|
+
this.ms = on ? this.onMs : 0;
|
|
24764
|
+
}
|
|
24765
|
+
};
|
|
24426
24766
|
function easeToward(current, target, dtMs, tauMs) {
|
|
24427
24767
|
if (tauMs <= 0) return target;
|
|
24428
24768
|
return current + (target - current) * (1 - Math.exp(-dtMs / tauMs));
|
|
@@ -25146,6 +25486,25 @@ function drawTextLines(ctx, lines, x, firstY, step, color) {
|
|
|
25146
25486
|
for (let i = 0; i < lines.length; i += 1) ctx.fillText(lines[i], x, firstY + i * step);
|
|
25147
25487
|
}
|
|
25148
25488
|
|
|
25489
|
+
// src/renderers/shared/marks-state.ts
|
|
25490
|
+
function defaultMarksState() {
|
|
25491
|
+
return { visible: true, groups: {} };
|
|
25492
|
+
}
|
|
25493
|
+
function mergeMarksState(base, patch) {
|
|
25494
|
+
if (typeof patch === "boolean") return { visible: patch, groups: { ...base.groups } };
|
|
25495
|
+
const p = patch && typeof patch === "object" ? patch : {};
|
|
25496
|
+
const g = p.groups && typeof p.groups === "object" ? p.groups : {};
|
|
25497
|
+
const groups = { ...base.groups };
|
|
25498
|
+
for (const [id, v] of Object.entries(g)) if (typeof v === "boolean") groups[id] = v;
|
|
25499
|
+
return { visible: typeof p.visible === "boolean" ? p.visible : base.visible, groups };
|
|
25500
|
+
}
|
|
25501
|
+
function markGroupVisible(state, groupId, groups) {
|
|
25502
|
+
if (groupId === void 0) return true;
|
|
25503
|
+
const chosen = state.groups[groupId];
|
|
25504
|
+
if (typeof chosen === "boolean") return chosen;
|
|
25505
|
+
return groups.find((g) => g.id === groupId)?.visible !== false;
|
|
25506
|
+
}
|
|
25507
|
+
|
|
25149
25508
|
// src/renderers/native/core/SceneGraph.ts
|
|
25150
25509
|
var SceneGraph = class {
|
|
25151
25510
|
constructor() {
|
|
@@ -25209,6 +25568,20 @@ var SceneGraph = class {
|
|
|
25209
25568
|
/** Strategy trade-marker display (the `tradeMarkers` feature): master toggle, the
|
|
25210
25569
|
* two text lines, and the palette. Trade markers always paint on the price pane. */
|
|
25211
25570
|
this.tradeMarkers = defaultTradeMarkersState();
|
|
25571
|
+
/** Timeline-mark display (the `marks` feature): the lane's master toggle + per-group visibility. */
|
|
25572
|
+
this.marks = defaultMarksState();
|
|
25573
|
+
/** The host's timeline marks + group definitions (`setTimelineMarks`), painted on the lane above the time axis. */
|
|
25574
|
+
this.timelineMarks = [];
|
|
25575
|
+
this.markGroups = [];
|
|
25576
|
+
/** The mark stack (bar index) fanned out by hover or tap, if any. */
|
|
25577
|
+
this.marksExpandedStack = null;
|
|
25578
|
+
/** The lane glyph (cluster key) under the pointer — it pulses — and when the hover began (frame-clock ms). */
|
|
25579
|
+
this.marksHoverKey = null;
|
|
25580
|
+
this.marksHoverSince = 0;
|
|
25581
|
+
/** The cluster whose popup is open: its glyph paints filled ("active"). */
|
|
25582
|
+
this.marksActiveKey = null;
|
|
25583
|
+
/** A content-less click's brief filled flash — the cluster key and the frame-clock time it ends. */
|
|
25584
|
+
this.marksFlash = null;
|
|
25212
25585
|
/** Renderer-owned shaded time bands (session highlighting), behind grid + data. */
|
|
25213
25586
|
this.highlights = [];
|
|
25214
25587
|
/** Pre/post-market bands pushed by the host (`sessionZones` feature); null ⇒ no sessions. */
|
|
@@ -27057,6 +27430,315 @@ function timeTicks(fromMs, toMs, target = 8, offsetMs = 0) {
|
|
|
27057
27430
|
return out;
|
|
27058
27431
|
}
|
|
27059
27432
|
|
|
27433
|
+
// src/renderers/native/chrome/countdown.ts
|
|
27434
|
+
function countdownText(barOpen, barMs, now) {
|
|
27435
|
+
if (!(barMs > 0)) return null;
|
|
27436
|
+
const remaining = barOpen + barMs - now;
|
|
27437
|
+
if (remaining <= 0) return null;
|
|
27438
|
+
return formatCountdown(remaining);
|
|
27439
|
+
}
|
|
27440
|
+
function formatCountdown(ms) {
|
|
27441
|
+
const total = Math.max(0, Math.ceil(ms / 1e3));
|
|
27442
|
+
const s = total % 60;
|
|
27443
|
+
const m = Math.floor(total / 60) % 60;
|
|
27444
|
+
const h = Math.floor(total / 3600);
|
|
27445
|
+
const pad = (v) => String(v).padStart(2, "0");
|
|
27446
|
+
return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`;
|
|
27447
|
+
}
|
|
27448
|
+
|
|
27449
|
+
// src/renderers/native/chrome/contrast.ts
|
|
27450
|
+
function tagTextColor(bg, over) {
|
|
27451
|
+
const [r, g, b, a] = parseColor(bg);
|
|
27452
|
+
let R = r;
|
|
27453
|
+
let G = g;
|
|
27454
|
+
let B = b;
|
|
27455
|
+
if (a < 1) {
|
|
27456
|
+
const [or, og, ob] = parseColor(over);
|
|
27457
|
+
R = r * a + or * (1 - a);
|
|
27458
|
+
G = g * a + og * (1 - a);
|
|
27459
|
+
B = b * a + ob * (1 - a);
|
|
27460
|
+
}
|
|
27461
|
+
const lin = (c) => c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
27462
|
+
const L = 0.2126 * lin(R) + 0.7152 * lin(G) + 0.0722 * lin(B);
|
|
27463
|
+
return L >= 0.4 ? "#000000" : "#ffffff";
|
|
27464
|
+
}
|
|
27465
|
+
|
|
27466
|
+
// src/renderers/native/chrome/marks/layout.ts
|
|
27467
|
+
var MARK_GLYPH_PX = 16;
|
|
27468
|
+
var MARK_CLUSTER_PX = 20;
|
|
27469
|
+
var MARK_LANE_INSET = 4;
|
|
27470
|
+
var MARK_DECK_STEP = 3;
|
|
27471
|
+
var MARK_FAN_GAP = 4;
|
|
27472
|
+
var MARK_HIT_PAD = 3;
|
|
27473
|
+
var MARK_FAN_HOLD = 8;
|
|
27474
|
+
function snapMarkBar(time, barTimes, intervalMs2) {
|
|
27475
|
+
const n = barTimes.length;
|
|
27476
|
+
if (n === 0 || !(intervalMs2 > 0) || !Number.isFinite(time)) return null;
|
|
27477
|
+
if (time < barTimes[0]) return null;
|
|
27478
|
+
const last = barTimes[n - 1];
|
|
27479
|
+
if (time >= last + intervalMs2) return n - 1 + Math.floor((time - last) / intervalMs2);
|
|
27480
|
+
let lo = 0;
|
|
27481
|
+
let hi = n - 1;
|
|
27482
|
+
while (lo < hi) {
|
|
27483
|
+
const mid = lo + hi + 1 >> 1;
|
|
27484
|
+
if (barTimes[mid] <= time) lo = mid;
|
|
27485
|
+
else hi = mid - 1;
|
|
27486
|
+
}
|
|
27487
|
+
if (time < barTimes[lo] + intervalMs2) return lo;
|
|
27488
|
+
return lo + 1;
|
|
27489
|
+
}
|
|
27490
|
+
function clusterMarks(marks, barTimes, intervalMs2, hidden) {
|
|
27491
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
27492
|
+
marks.forEach((m, seq) => {
|
|
27493
|
+
if (m.group !== void 0 && hidden(m.group)) return;
|
|
27494
|
+
const bar = snapMarkBar(m.time, barTimes, intervalMs2);
|
|
27495
|
+
if (bar === null) return;
|
|
27496
|
+
const key = `${bar}|${m.group ?? ""}`;
|
|
27497
|
+
let c = byKey.get(key);
|
|
27498
|
+
if (!c) {
|
|
27499
|
+
c = { key, bar, group: m.group, marks: [], seq: [] };
|
|
27500
|
+
byKey.set(key, c);
|
|
27501
|
+
}
|
|
27502
|
+
c.marks.push(m);
|
|
27503
|
+
c.seq.push(seq);
|
|
27504
|
+
});
|
|
27505
|
+
const out = [];
|
|
27506
|
+
for (const c of byKey.values()) {
|
|
27507
|
+
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);
|
|
27508
|
+
out.push({ key: c.key, bar: c.bar, group: c.group, marks: order.map((o) => o.m) });
|
|
27509
|
+
}
|
|
27510
|
+
return out;
|
|
27511
|
+
}
|
|
27512
|
+
function groupRank(groups, clusters) {
|
|
27513
|
+
const rank = /* @__PURE__ */ new Map();
|
|
27514
|
+
groups.forEach((g, i) => rank.set(g.id, i));
|
|
27515
|
+
for (const c of clusters) {
|
|
27516
|
+
if (c.group !== void 0 && !rank.has(c.group)) rank.set(c.group, rank.size);
|
|
27517
|
+
}
|
|
27518
|
+
return (group) => group === void 0 ? Number.MAX_SAFE_INTEGER : rank.get(group) ?? Number.MAX_SAFE_INTEGER - 1;
|
|
27519
|
+
}
|
|
27520
|
+
function layoutMarkLane(input) {
|
|
27521
|
+
const clusters = clusterMarks(input.marks, input.barTimes, input.intervalMs, input.hidden);
|
|
27522
|
+
const rankOf = groupRank(input.groups, clusters);
|
|
27523
|
+
const byBar = /* @__PURE__ */ new Map();
|
|
27524
|
+
for (const c of clusters) {
|
|
27525
|
+
const list = byBar.get(c.bar);
|
|
27526
|
+
if (list) list.push(c);
|
|
27527
|
+
else byBar.set(c.bar, [c]);
|
|
27528
|
+
}
|
|
27529
|
+
const glyphs = [];
|
|
27530
|
+
const stacks = /* @__PURE__ */ new Map();
|
|
27531
|
+
for (const [bar, list] of byBar) {
|
|
27532
|
+
const x = input.xOf(bar);
|
|
27533
|
+
if (!Number.isFinite(x) || x < -MARK_CLUSTER_PX || x > input.dataW + MARK_CLUSTER_PX) continue;
|
|
27534
|
+
list.sort((a, b) => rankOf(a.group) - rankOf(b.group));
|
|
27535
|
+
const multi = list.length > 1;
|
|
27536
|
+
const expanded = multi && input.expanded === bar;
|
|
27537
|
+
const decked = multi && !expanded;
|
|
27538
|
+
const placed = [];
|
|
27539
|
+
const deckSize = list[0].marks.length > 1 ? MARK_CLUSTER_PX : MARK_GLYPH_PX;
|
|
27540
|
+
let bottom = input.axisY - MARK_LANE_INSET;
|
|
27541
|
+
list.forEach((cluster, depth) => {
|
|
27542
|
+
const size = decked ? deckSize : cluster.marks.length > 1 ? MARK_CLUSTER_PX : MARK_GLYPH_PX;
|
|
27543
|
+
let y;
|
|
27544
|
+
if (expanded) {
|
|
27545
|
+
y = bottom - size / 2;
|
|
27546
|
+
bottom -= size + MARK_FAN_GAP;
|
|
27547
|
+
} else {
|
|
27548
|
+
y = input.axisY - MARK_LANE_INSET - size / 2 - depth * MARK_DECK_STEP;
|
|
27549
|
+
}
|
|
27550
|
+
placed.push({ cluster, x, y, size, stack: bar, depth, decked });
|
|
27551
|
+
});
|
|
27552
|
+
for (let i = placed.length - 1; i >= 0; i--) glyphs.push(placed[i]);
|
|
27553
|
+
stacks.set(bar, placed);
|
|
27554
|
+
}
|
|
27555
|
+
return { glyphs, stacks };
|
|
27556
|
+
}
|
|
27557
|
+
function markGlyphAt(layout, x, y) {
|
|
27558
|
+
for (let i = layout.glyphs.length - 1; i >= 0; i--) {
|
|
27559
|
+
const g = layout.glyphs[i];
|
|
27560
|
+
if (g.decked && g.depth !== 0) continue;
|
|
27561
|
+
const r = g.size / 2 + MARK_HIT_PAD;
|
|
27562
|
+
if (Math.abs(x - g.x) <= r && Math.abs(y - g.y) <= r) return g;
|
|
27563
|
+
}
|
|
27564
|
+
return null;
|
|
27565
|
+
}
|
|
27566
|
+
function markStackAt(layout, x, y) {
|
|
27567
|
+
for (const [bar, placed] of layout.stacks) {
|
|
27568
|
+
for (const g of placed) {
|
|
27569
|
+
const r = g.size / 2 + MARK_HIT_PAD;
|
|
27570
|
+
if (Math.abs(x - g.x) <= r && Math.abs(y - g.y) <= r) return bar;
|
|
27571
|
+
}
|
|
27572
|
+
if (placed.length > 1 && !placed[0].decked) {
|
|
27573
|
+
const top = placed[placed.length - 1];
|
|
27574
|
+
const base = placed[0];
|
|
27575
|
+
const r = Math.max(top.size, base.size) / 2 + MARK_HIT_PAD;
|
|
27576
|
+
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;
|
|
27577
|
+
}
|
|
27578
|
+
}
|
|
27579
|
+
return null;
|
|
27580
|
+
}
|
|
27581
|
+
function clusterTooltip(cluster, groups) {
|
|
27582
|
+
const first = cluster.marks[0];
|
|
27583
|
+
if (!first) return null;
|
|
27584
|
+
if (cluster.marks.length === 1) return first.tooltip ?? first.title ?? null;
|
|
27585
|
+
const label = cluster.group !== void 0 ? markGroupLabel(cluster.group, groups) : first.title ?? first.tooltip ?? "Marks";
|
|
27586
|
+
return `${label} \xB7 ${cluster.marks.length}`;
|
|
27587
|
+
}
|
|
27588
|
+
function markGroupLabel(groupId, groups) {
|
|
27589
|
+
const def = groups.find((g) => g.id === groupId);
|
|
27590
|
+
if (def) return def.label;
|
|
27591
|
+
return groupId.charAt(0).toUpperCase() + groupId.slice(1);
|
|
27592
|
+
}
|
|
27593
|
+
function effectiveMarkGroups(marks, groups) {
|
|
27594
|
+
const out = groups.map((g) => ({ ...g }));
|
|
27595
|
+
const seen = new Set(out.map((g) => g.id));
|
|
27596
|
+
for (const m of marks) {
|
|
27597
|
+
if (m.group === void 0 || seen.has(m.group)) continue;
|
|
27598
|
+
seen.add(m.group);
|
|
27599
|
+
out.push({ id: m.group, label: markGroupLabel(m.group, groups) });
|
|
27600
|
+
}
|
|
27601
|
+
return out;
|
|
27602
|
+
}
|
|
27603
|
+
|
|
27604
|
+
// src/renderers/native/chrome/marks/paint.ts
|
|
27605
|
+
var MARK_PULSE_MS = 360;
|
|
27606
|
+
var MARK_PULSE_AMPLITUDE = 0.1;
|
|
27607
|
+
var ACTIVE_INK = "#ffffff";
|
|
27608
|
+
function pulseScale(elapsedMs) {
|
|
27609
|
+
if (!(elapsedMs > 0) || elapsedMs >= MARK_PULSE_MS) return 1;
|
|
27610
|
+
return 1 + MARK_PULSE_AMPLITUDE * Math.sin(Math.PI * elapsedMs / MARK_PULSE_MS);
|
|
27611
|
+
}
|
|
27612
|
+
function paintMarkLane(ctx, layout, deps) {
|
|
27613
|
+
if (layout.glyphs.length === 0) return;
|
|
27614
|
+
ctx.save();
|
|
27615
|
+
ctx.setLineDash([]);
|
|
27616
|
+
ctx.textAlign = "center";
|
|
27617
|
+
ctx.textBaseline = "middle";
|
|
27618
|
+
for (const g of layout.glyphs) {
|
|
27619
|
+
const mark = g.cluster.marks[0];
|
|
27620
|
+
if (!mark) continue;
|
|
27621
|
+
const key = g.cluster.key;
|
|
27622
|
+
const color = mark.glyph.color;
|
|
27623
|
+
const active = key === deps.activeKey || key === deps.flashKey;
|
|
27624
|
+
const size = g.size * (key === deps.hoverKey ? pulseScale(deps.nowMs - deps.hoverSince) : 1);
|
|
27625
|
+
if (g.depth === 0) {
|
|
27626
|
+
const sx = Math.round(g.x) + 0.5;
|
|
27627
|
+
ctx.lineWidth = 1;
|
|
27628
|
+
ctx.strokeStyle = deps.stemColor;
|
|
27629
|
+
ctx.beginPath();
|
|
27630
|
+
ctx.moveTo(sx, g.y + g.size / 2);
|
|
27631
|
+
ctx.lineTo(sx, deps.axisY);
|
|
27632
|
+
ctx.stroke();
|
|
27633
|
+
}
|
|
27634
|
+
const shape = mark.glyph.shape ?? "circle";
|
|
27635
|
+
const center = traceShape(ctx, shape, g.x, g.y, size);
|
|
27636
|
+
ctx.lineWidth = 4;
|
|
27637
|
+
ctx.strokeStyle = deps.background;
|
|
27638
|
+
ctx.stroke();
|
|
27639
|
+
ctx.fillStyle = active ? color : deps.background;
|
|
27640
|
+
ctx.fill();
|
|
27641
|
+
ctx.lineWidth = 1.5;
|
|
27642
|
+
ctx.strokeStyle = color;
|
|
27643
|
+
ctx.stroke();
|
|
27644
|
+
const ink = active ? ACTIVE_INK : color;
|
|
27645
|
+
const symbolPx = Math.round(size * 0.62);
|
|
27646
|
+
if (mark.glyph.icon) {
|
|
27647
|
+
const img = deps.icons.get(mark.glyph.icon, ink, symbolPx, deps.dpr);
|
|
27648
|
+
if (img) ctx.drawImage(img, center.x - symbolPx / 2, center.y - symbolPx / 2, symbolPx, symbolPx);
|
|
27649
|
+
} else if (mark.glyph.letter) {
|
|
27650
|
+
ctx.fillStyle = ink;
|
|
27651
|
+
ctx.font = `600 ${Math.round(size * 0.58)}px ${deps.fontFamily}`;
|
|
27652
|
+
ctx.fillText(mark.glyph.letter.slice(0, 2), center.x, center.y + 0.5);
|
|
27653
|
+
}
|
|
27654
|
+
}
|
|
27655
|
+
ctx.restore();
|
|
27656
|
+
}
|
|
27657
|
+
function traceShape(ctx, shape, x, y, size) {
|
|
27658
|
+
const r = size / 2;
|
|
27659
|
+
ctx.beginPath();
|
|
27660
|
+
switch (shape) {
|
|
27661
|
+
case "square": {
|
|
27662
|
+
const c = Math.min(3, r / 2);
|
|
27663
|
+
roundedRect(ctx, x - r, y - r, size, size, c);
|
|
27664
|
+
return { x, y };
|
|
27665
|
+
}
|
|
27666
|
+
case "diamond":
|
|
27667
|
+
ctx.moveTo(x, y - r);
|
|
27668
|
+
ctx.lineTo(x + r, y);
|
|
27669
|
+
ctx.lineTo(x, y + r);
|
|
27670
|
+
ctx.lineTo(x - r, y);
|
|
27671
|
+
ctx.closePath();
|
|
27672
|
+
return { x, y };
|
|
27673
|
+
case "pin": {
|
|
27674
|
+
const hr = r * 0.82;
|
|
27675
|
+
const hy = y - r + hr;
|
|
27676
|
+
ctx.arc(x, hy, hr, Math.PI * 0.75, Math.PI * 0.25, false);
|
|
27677
|
+
ctx.lineTo(x, y + r);
|
|
27678
|
+
ctx.closePath();
|
|
27679
|
+
return { x, y: hy };
|
|
27680
|
+
}
|
|
27681
|
+
case "circle":
|
|
27682
|
+
default:
|
|
27683
|
+
ctx.arc(x, y, r, 0, Math.PI * 2);
|
|
27684
|
+
return { x, y };
|
|
27685
|
+
}
|
|
27686
|
+
}
|
|
27687
|
+
function roundedRect(ctx, x, y, w, h, radius) {
|
|
27688
|
+
ctx.moveTo(x + radius, y);
|
|
27689
|
+
ctx.lineTo(x + w - radius, y);
|
|
27690
|
+
ctx.quadraticCurveTo(x + w, y, x + w, y + radius);
|
|
27691
|
+
ctx.lineTo(x + w, y + h - radius);
|
|
27692
|
+
ctx.quadraticCurveTo(x + w, y + h, x + w - radius, y + h);
|
|
27693
|
+
ctx.lineTo(x + radius, y + h);
|
|
27694
|
+
ctx.quadraticCurveTo(x, y + h, x, y + h - radius);
|
|
27695
|
+
ctx.lineTo(x, y + radius);
|
|
27696
|
+
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
27697
|
+
ctx.closePath();
|
|
27698
|
+
}
|
|
27699
|
+
var MarkIconRaster = class {
|
|
27700
|
+
constructor(onReady) {
|
|
27701
|
+
this.onReady = onReady;
|
|
27702
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
27703
|
+
}
|
|
27704
|
+
get(icon2, ink, px, dpr) {
|
|
27705
|
+
const key = `${icon2}|${ink}|${px}|${dpr}`;
|
|
27706
|
+
if (this.cache.has(key)) {
|
|
27707
|
+
const img2 = this.cache.get(key);
|
|
27708
|
+
return img2 && img2.complete && img2.naturalWidth > 0 ? img2 : null;
|
|
27709
|
+
}
|
|
27710
|
+
const markup = iconMarkup(icon2);
|
|
27711
|
+
if (!markup || typeof document === "undefined" || typeof Image === "undefined" || typeof XMLSerializer === "undefined") {
|
|
27712
|
+
this.cache.set(key, null);
|
|
27713
|
+
return null;
|
|
27714
|
+
}
|
|
27715
|
+
const svg = standaloneSvg(markup, ink, Math.max(1, Math.ceil(px * dpr)));
|
|
27716
|
+
if (!svg) {
|
|
27717
|
+
this.cache.set(key, null);
|
|
27718
|
+
return null;
|
|
27719
|
+
}
|
|
27720
|
+
const img = new Image();
|
|
27721
|
+
img.onload = () => this.onReady();
|
|
27722
|
+
img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
|
|
27723
|
+
this.cache.set(key, img);
|
|
27724
|
+
return null;
|
|
27725
|
+
}
|
|
27726
|
+
clear() {
|
|
27727
|
+
this.cache.clear();
|
|
27728
|
+
}
|
|
27729
|
+
};
|
|
27730
|
+
function standaloneSvg(markup, ink, px) {
|
|
27731
|
+
const tpl = document.createElement("template");
|
|
27732
|
+
tpl.innerHTML = markup;
|
|
27733
|
+
const svg = tpl.content.firstElementChild;
|
|
27734
|
+
if (!svg || svg.tagName.toLowerCase() !== "svg") return null;
|
|
27735
|
+
svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
|
27736
|
+
svg.setAttribute("width", String(px));
|
|
27737
|
+
svg.setAttribute("height", String(px));
|
|
27738
|
+
svg.setAttribute("color", ink);
|
|
27739
|
+
return new XMLSerializer().serializeToString(svg);
|
|
27740
|
+
}
|
|
27741
|
+
|
|
27060
27742
|
// src/renderers/native/chrome/ChromeRenderer.ts
|
|
27061
27743
|
var ChromeRenderer = class {
|
|
27062
27744
|
constructor() {
|
|
@@ -27066,11 +27748,40 @@ var ChromeRenderer = class {
|
|
|
27066
27748
|
this.axisTextColor = DARK_THEME.textColor;
|
|
27067
27749
|
// Shared Pine-drawing renderer, used here for autoscale geometry only; widthCache persists.
|
|
27068
27750
|
this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
|
|
27751
|
+
/** The timeline-mark lane as laid out by the last frame — what hover/click hit-test against. */
|
|
27752
|
+
this.markLayout = { glyphs: [], stacks: /* @__PURE__ */ new Map() };
|
|
27753
|
+
/** Registry icons rasterized for the lane; the owner is asked for a chrome repaint when one lands. */
|
|
27754
|
+
this.markIcons = new MarkIconRaster(() => this.onMarkIconReady?.());
|
|
27755
|
+
this.onMarkIconReady = null;
|
|
27756
|
+
/** Bar open times of the current series, rebuilt only when the array or its length changes (a live tick keeps both). */
|
|
27757
|
+
this.barTimesSrc = null;
|
|
27758
|
+
this.barTimesCache = [];
|
|
27069
27759
|
}
|
|
27070
27760
|
mount(canvas) {
|
|
27071
27761
|
this.canvas = canvas;
|
|
27072
27762
|
this.ctx = canvas.getContext("2d");
|
|
27073
27763
|
}
|
|
27764
|
+
/** Where to ask for a chrome repaint when a lane icon finishes rasterizing. */
|
|
27765
|
+
setMarkIconReady(cb) {
|
|
27766
|
+
this.onMarkIconReady = cb;
|
|
27767
|
+
}
|
|
27768
|
+
/** The interactive mark glyph under a plot point (last frame's layout), or null. */
|
|
27769
|
+
markGlyphAt(x, y) {
|
|
27770
|
+
return markGlyphAt(this.markLayout, x, y);
|
|
27771
|
+
}
|
|
27772
|
+
/** The mark stack (bar index) whose glyphs — or the gaps of its fan — cover a plot point. */
|
|
27773
|
+
markStackAt(x, y) {
|
|
27774
|
+
return markStackAt(this.markLayout, x, y);
|
|
27775
|
+
}
|
|
27776
|
+
/** A glyph of the last frame by its cluster key — how an open popup follows its anchor. */
|
|
27777
|
+
markGlyphByKey(key) {
|
|
27778
|
+
return this.markLayout.glyphs.find((g) => g.cluster.key === key) ?? null;
|
|
27779
|
+
}
|
|
27780
|
+
/** Hover text of the mark glyph under a plot point, or null. */
|
|
27781
|
+
markTooltipAt(x, y, groups) {
|
|
27782
|
+
const g = this.markGlyphAt(x, y);
|
|
27783
|
+
return g ? clusterTooltip(g.cluster, groups) : null;
|
|
27784
|
+
}
|
|
27074
27785
|
/** Wire the drawing coordinate resolvers + theme (call once per frame before use). */
|
|
27075
27786
|
prepare(scene, coords, theme) {
|
|
27076
27787
|
this.drawScene.setDeps({
|
|
@@ -27119,6 +27830,7 @@ var ChromeRenderer = class {
|
|
|
27119
27830
|
const panes = scene.orderedPanes();
|
|
27120
27831
|
if (coords.barCount === 0) {
|
|
27121
27832
|
this.drawPaneSeparators(ctx, scene, theme, fullW, panes);
|
|
27833
|
+
this.markLayout = { glyphs: [], stacks: /* @__PURE__ */ new Map() };
|
|
27122
27834
|
return;
|
|
27123
27835
|
}
|
|
27124
27836
|
const pricePane = panes.find((p) => p.kind === "price") ?? null;
|
|
@@ -27132,6 +27844,46 @@ var ChromeRenderer = class {
|
|
|
27132
27844
|
this.drawPaneSeparators(ctx, scene, theme, fullW, panes);
|
|
27133
27845
|
this.drawPriceLineAndCountdown(ctx, scene, coords, theme, dataW, pricePane);
|
|
27134
27846
|
this.drawTimeAxis(ctx, scene, coords, theme, dataW, dataH, fullH);
|
|
27847
|
+
this.drawMarkLane(ctx, scene, coords, theme, dataW, dataH);
|
|
27848
|
+
}
|
|
27849
|
+
/** The timeline-mark lane — after the axis, so the tokens read over the plot's bottom edge. */
|
|
27850
|
+
drawMarkLane(ctx, scene, coords, theme, dataW, dataH) {
|
|
27851
|
+
if (!scene.marks.visible || scene.timelineMarks.length === 0) {
|
|
27852
|
+
this.markLayout = { glyphs: [], stacks: /* @__PURE__ */ new Map() };
|
|
27853
|
+
return;
|
|
27854
|
+
}
|
|
27855
|
+
this.markLayout = layoutMarkLane({
|
|
27856
|
+
marks: scene.timelineMarks,
|
|
27857
|
+
groups: scene.markGroups,
|
|
27858
|
+
hidden: (groupId) => !markGroupVisible(scene.marks, groupId, scene.markGroups),
|
|
27859
|
+
barTimes: this.barTimes(scene),
|
|
27860
|
+
intervalMs: coords.barInterval,
|
|
27861
|
+
xOf: (bar) => coords.logicalToX(bar),
|
|
27862
|
+
axisY: dataH,
|
|
27863
|
+
dataW,
|
|
27864
|
+
expanded: scene.marksExpandedStack
|
|
27865
|
+
});
|
|
27866
|
+
const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
27867
|
+
paintMarkLane(ctx, this.markLayout, {
|
|
27868
|
+
axisY: dataH,
|
|
27869
|
+
background: theme.background,
|
|
27870
|
+
stemColor: scene.style.borderColor ?? theme.borderColor,
|
|
27871
|
+
fontFamily: theme.fontFamily,
|
|
27872
|
+
dpr: coords.dpr,
|
|
27873
|
+
icons: this.markIcons,
|
|
27874
|
+
hoverKey: scene.marksHoverKey,
|
|
27875
|
+
hoverSince: scene.marksHoverSince,
|
|
27876
|
+
activeKey: scene.marksActiveKey,
|
|
27877
|
+
flashKey: scene.marksFlash && scene.marksFlash.until > nowMs ? scene.marksFlash.key : null,
|
|
27878
|
+
nowMs
|
|
27879
|
+
});
|
|
27880
|
+
}
|
|
27881
|
+
barTimes(scene) {
|
|
27882
|
+
if (this.barTimesSrc !== scene.bars || this.barTimesCache.length !== scene.bars.length) {
|
|
27883
|
+
this.barTimesSrc = scene.bars;
|
|
27884
|
+
this.barTimesCache = scene.bars.map((b) => b.time);
|
|
27885
|
+
}
|
|
27886
|
+
return this.barTimesCache;
|
|
27135
27887
|
}
|
|
27136
27888
|
destroy() {
|
|
27137
27889
|
this.canvas = null;
|
|
@@ -27247,7 +27999,8 @@ var ChromeRenderer = class {
|
|
|
27247
27999
|
* - the countdown-to-bar-close chip (`showCountdown`).
|
|
27248
28000
|
* When the label and countdown are both on they merge into one stacked block (countdown
|
|
27249
28001
|
* under the label, text flushed left); a lone label or countdown is centered on the
|
|
27250
|
-
* price level with centered text. The countdown
|
|
28002
|
+
* price level with centered text. The countdown repaints on the renderer's second pulse
|
|
28003
|
+
* and disappears once the bar has closed, until the next bar arrives.
|
|
27251
28004
|
*/
|
|
27252
28005
|
drawPriceLineAndCountdown(ctx, scene, coords, theme, dataW, pricePane) {
|
|
27253
28006
|
const n = scene.bars.length;
|
|
@@ -27267,17 +28020,16 @@ var ChromeRenderer = class {
|
|
|
27267
28020
|
ctx.stroke();
|
|
27268
28021
|
setDash2(ctx, "solid");
|
|
27269
28022
|
}
|
|
27270
|
-
const
|
|
27271
|
-
const showCountdown =
|
|
28023
|
+
const cdText = scene.showCountdown ? countdownText(last.time, coords.barInterval, Date.now()) : null;
|
|
28024
|
+
const showCountdown = cdText !== null;
|
|
27272
28025
|
const showLabel = scene.showPriceLabel;
|
|
27273
28026
|
if (!showLabel && !showCountdown) return;
|
|
27274
28027
|
const priceText = formatAxisValue(pricePane.scale, pricePane.bounds.height, last.close, percentScaleFor(scene, pricePane), scene.priceMintick);
|
|
27275
|
-
const cdText = showCountdown ? formatCountdown(last.time + interval - Date.now()) : "";
|
|
27276
28028
|
const PAD = 8;
|
|
27277
28029
|
const x = dataW + 1;
|
|
27278
28030
|
const textColor = tagTextColor(color, theme.background);
|
|
27279
28031
|
ctx.textBaseline = "middle";
|
|
27280
|
-
if (showLabel &&
|
|
28032
|
+
if (showLabel && cdText !== null) {
|
|
27281
28033
|
const w2 = Math.max(ctx.measureText(priceText).width, ctx.measureText(cdText).width) + PAD;
|
|
27282
28034
|
const top = y - 8;
|
|
27283
28035
|
const tx = x + PAD / 2;
|
|
@@ -27290,7 +28042,7 @@ var ChromeRenderer = class {
|
|
|
27290
28042
|
ctx.textAlign = "start";
|
|
27291
28043
|
return;
|
|
27292
28044
|
}
|
|
27293
|
-
const text = showLabel ? priceText : cdText;
|
|
28045
|
+
const text = showLabel ? priceText : cdText ?? "";
|
|
27294
28046
|
const w = ctx.measureText(text).width + PAD;
|
|
27295
28047
|
ctx.fillStyle = color;
|
|
27296
28048
|
ctx.fillRect(x, y - 8, w, 16);
|
|
@@ -27361,29 +28113,6 @@ function setDash2(ctx, style) {
|
|
|
27361
28113
|
else if (style === "dotted") ctx.setLineDash([2, 3]);
|
|
27362
28114
|
else ctx.setLineDash([]);
|
|
27363
28115
|
}
|
|
27364
|
-
function tagTextColor(bg, over) {
|
|
27365
|
-
const [r, g, b, a] = parseColor(bg);
|
|
27366
|
-
let R = r;
|
|
27367
|
-
let G = g;
|
|
27368
|
-
let B = b;
|
|
27369
|
-
if (a < 1) {
|
|
27370
|
-
const [or, og, ob] = parseColor(over);
|
|
27371
|
-
R = r * a + or * (1 - a);
|
|
27372
|
-
G = g * a + og * (1 - a);
|
|
27373
|
-
B = b * a + ob * (1 - a);
|
|
27374
|
-
}
|
|
27375
|
-
const lin = (c) => c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
27376
|
-
const L = 0.2126 * lin(R) + 0.7152 * lin(G) + 0.0722 * lin(B);
|
|
27377
|
-
return L >= 0.4 ? "#000000" : "#ffffff";
|
|
27378
|
-
}
|
|
27379
|
-
function formatCountdown(ms) {
|
|
27380
|
-
const total = Math.max(0, Math.floor(ms / 1e3));
|
|
27381
|
-
const s = total % 60;
|
|
27382
|
-
const m = Math.floor(total / 60) % 60;
|
|
27383
|
-
const h = Math.floor(total / 3600);
|
|
27384
|
-
const pad = (v) => String(v).padStart(2, "0");
|
|
27385
|
-
return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`;
|
|
27386
|
-
}
|
|
27387
28116
|
|
|
27388
28117
|
// src/renderers/native/chrome/LabelTooltip.ts
|
|
27389
28118
|
var HOVER_DELAY_MS = 350;
|
|
@@ -27589,6 +28318,11 @@ function setDash3(ctx, style) {
|
|
|
27589
28318
|
function settingsIdSlug(label) {
|
|
27590
28319
|
return label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
27591
28320
|
}
|
|
28321
|
+
var MARKS_SETTINGS_ID = "events";
|
|
28322
|
+
var MARKS_GROUPS_SETTINGS_ID = "events.groups";
|
|
28323
|
+
function markGroupSettingsId(groupId) {
|
|
28324
|
+
return `${MARKS_GROUPS_SETTINGS_ID}.${settingsIdSlug(groupId)}`;
|
|
28325
|
+
}
|
|
27592
28326
|
function settingsIdHidden(id, hidden) {
|
|
27593
28327
|
if (hidden.size === 0) return false;
|
|
27594
28328
|
let path = id;
|
|
@@ -27682,7 +28416,11 @@ var BUILTIN_SETTINGS_IDS = [
|
|
|
27682
28416
|
"symbol.style.baseline.base-level",
|
|
27683
28417
|
"symbol.style.baseline.width",
|
|
27684
28418
|
"symbol.animation",
|
|
28419
|
+
"symbol.animation.zoom",
|
|
28420
|
+
"symbol.animation.pan",
|
|
28421
|
+
"symbol.animation.autoscale",
|
|
27685
28422
|
"symbol.animation.price-changes",
|
|
28423
|
+
"symbol.animation.intro",
|
|
27686
28424
|
"symbol.timezone",
|
|
27687
28425
|
"scales",
|
|
27688
28426
|
"scales.price-scale",
|
|
@@ -27708,8 +28446,13 @@ var BUILTIN_SETTINGS_IDS = [
|
|
|
27708
28446
|
"canvas.grid.horizontal",
|
|
27709
28447
|
"canvas.theme"
|
|
27710
28448
|
];
|
|
27711
|
-
function settingsIdCatalog(hostSections) {
|
|
28449
|
+
function settingsIdCatalog(hostSections, markGroups = []) {
|
|
27712
28450
|
const ids = new Set(BUILTIN_SETTINGS_IDS);
|
|
28451
|
+
if (markGroups.length > 0) {
|
|
28452
|
+
ids.add(MARKS_SETTINGS_ID);
|
|
28453
|
+
ids.add(MARKS_GROUPS_SETTINGS_ID);
|
|
28454
|
+
for (const g of markGroups) ids.add(markGroupSettingsId(g.id));
|
|
28455
|
+
}
|
|
27713
28456
|
for (const def of chartTypes()) {
|
|
27714
28457
|
if (hasOwnCandlePaint(def.id)) {
|
|
27715
28458
|
const style = `symbol.style.${def.id}`;
|
|
@@ -27749,7 +28492,7 @@ function styleLabel(id) {
|
|
|
27749
28492
|
return chartType(id)?.label ?? BUILTIN_STYLE_LABELS2[id] ?? id;
|
|
27750
28493
|
}
|
|
27751
28494
|
var SD_STYLE_ID = "vela-settings-controls";
|
|
27752
|
-
var SD_STYLE_REV = "
|
|
28495
|
+
var SD_STYLE_REV = "6";
|
|
27753
28496
|
var SETTINGS_BORDER = "var(--vela-border)";
|
|
27754
28497
|
function ensureControlStyles() {
|
|
27755
28498
|
if (typeof document === "undefined") return;
|
|
@@ -27825,7 +28568,15 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
27825
28568
|
.vela-sd-mobile .vela-select-trigger,.vela-sd-mobile .vela-num input,.vela-sd-mobile .vela-width-field{height:34px;}
|
|
27826
28569
|
.vela-sd-mobile .vela-sd-close{width:40px;height:40px;}
|
|
27827
28570
|
.vela-sd-mobile .vela-sd-btn{height:38px;}
|
|
27828
|
-
.vela-sd-mobile .vela-sd-row span,.vela-sd-mobile .vela-sd-bool span,.vela-sd-mobile .vela-field-label{white-space:normal !important;}
|
|
28571
|
+
.vela-sd-mobile .vela-sd-row span,.vela-sd-mobile .vela-sd-bool span,.vela-sd-mobile .vela-field-label{white-space:normal !important;}
|
|
28572
|
+
/* The wrap rule above is for ROW LABELS only: a select's closed value must keep its
|
|
28573
|
+
single-line ellipsis, or a long option wraps to several lines inside the 34px
|
|
28574
|
+
trigger and spills over the rows around it. Three classes so it outranks the
|
|
28575
|
+
two-classes-plus-element selector above. The kit's fixed 100px column is a desktop
|
|
28576
|
+
alignment device; on mobile the trigger hugs its value instead (the grid's control
|
|
28577
|
+
column is max-content), capped so a long option still ellipsizes before the label. */
|
|
28578
|
+
.vela-sd-mobile .vela-select-trigger .vela-select-label{white-space:nowrap !important;}
|
|
28579
|
+
.vela-sd-mobile .vela-select:not([data-fill]){width:auto;min-width:100px;max-width:min(220px,55vw);}`;
|
|
27829
28580
|
if (!existing) document.head.appendChild(st);
|
|
27830
28581
|
}
|
|
27831
28582
|
var SettingsDialog = class {
|
|
@@ -27840,6 +28591,9 @@ var SettingsDialog = class {
|
|
|
27840
28591
|
this.config = null;
|
|
27841
28592
|
this.syncTypeTabs = null;
|
|
27842
28593
|
this.hostSections = [];
|
|
28594
|
+
/** The timeline-mark groups (defined + named by marks) — one checkbox each on the Events tab. */
|
|
28595
|
+
this.markGroups = [];
|
|
28596
|
+
this.markGroupVisible = () => true;
|
|
27843
28597
|
/** The Canvas → Theme row: current app theme + where a pick is raised. The row is a
|
|
27844
28598
|
* host callback, NOT a config patch — the app theme stays out of the persisted
|
|
27845
28599
|
* `ChartConfig`, so exported templates never carry it. */
|
|
@@ -27864,6 +28618,11 @@ var SettingsDialog = class {
|
|
|
27864
28618
|
setHostSections(sections) {
|
|
27865
28619
|
this.hostSections = sections;
|
|
27866
28620
|
}
|
|
28621
|
+
/** The timeline-mark groups and their current visibility — the Events tab's rows on next open. */
|
|
28622
|
+
setMarkGroups(groups, visible) {
|
|
28623
|
+
this.markGroups = groups;
|
|
28624
|
+
this.markGroupVisible = visible;
|
|
28625
|
+
}
|
|
27867
28626
|
/** Replace the visibility policy — an open dialog rebuilds in place to honor it. */
|
|
27868
28627
|
setHiddenSettings(ids) {
|
|
27869
28628
|
const next = new Set(ids);
|
|
@@ -28037,12 +28796,36 @@ var SettingsDialog = class {
|
|
|
28037
28796
|
}
|
|
28038
28797
|
showActive(config.series.style);
|
|
28039
28798
|
body.append(sid(this.sectionTitle("Animation"), "symbol.animation"));
|
|
28799
|
+
body.append(sid(this.boolRow(
|
|
28800
|
+
"Animate zoom",
|
|
28801
|
+
config.animations.zoom,
|
|
28802
|
+
(v) => this.emit({ animations: { zoom: v } }),
|
|
28803
|
+
this.hint("Glide the chart to each zoom step instead of jumping.")
|
|
28804
|
+
), "symbol.animation.zoom"));
|
|
28805
|
+
body.append(sid(this.boolRow(
|
|
28806
|
+
"Pan momentum",
|
|
28807
|
+
config.animations.pan,
|
|
28808
|
+
(v) => this.emit({ animations: { pan: v } }),
|
|
28809
|
+
this.hint("Keep gliding briefly after a drag release, and ease scroll-to-latest and keyboard pans.")
|
|
28810
|
+
), "symbol.animation.pan"));
|
|
28811
|
+
body.append(sid(this.boolRow(
|
|
28812
|
+
"Animate price scale",
|
|
28813
|
+
config.animations.autoscale,
|
|
28814
|
+
(v) => this.emit({ animations: { autoscale: v } }),
|
|
28815
|
+
this.hint("Glide the price scale to its new range while zooming or panning.")
|
|
28816
|
+
), "symbol.animation.autoscale"));
|
|
28040
28817
|
body.append(sid(this.boolRow(
|
|
28041
28818
|
"Animate price changes",
|
|
28042
28819
|
config.priceScale.animateLastPrice,
|
|
28043
28820
|
(v) => this.emit({ priceScale: { animateLastPrice: v } }),
|
|
28044
28821
|
this.hint("Glide the live bar to each new price instead of snapping.")
|
|
28045
28822
|
), "symbol.animation.price-changes"));
|
|
28823
|
+
body.append(sid(this.boolRow(
|
|
28824
|
+
"Reveal on load",
|
|
28825
|
+
config.animations.intro,
|
|
28826
|
+
(v) => this.emit({ animations: { intro: v } }),
|
|
28827
|
+
this.hint("Draw the candles in when a chart first loads. Takes effect on the next load.")
|
|
28828
|
+
), "symbol.animation.intro"));
|
|
28046
28829
|
body.append(sid(this.sectionTitle("Time zone"), "symbol.timezone"));
|
|
28047
28830
|
body.append(sid(this.selectRowLabeled("Time zone", normalizeTimezone(config.timeScale.timezone), timezoneOptions(config.timeScale.timezone), (v) => this.emit({ timeScale: { timezone: v } })), "symbol.timezone"));
|
|
28048
28831
|
const renderHostSections = (placement) => {
|
|
@@ -28112,6 +28895,13 @@ var SettingsDialog = class {
|
|
|
28112
28895
|
body.append(sid(this.sectionTitle("Theme"), "canvas.theme"));
|
|
28113
28896
|
body.append(sid(this.selectRow("Color theme", tc.current === "dark" ? "Dark" : "Light", ["Dark", "Light"], (v) => tc.onSelect(v === "Dark" ? "dark" : "light")), "canvas.theme"));
|
|
28114
28897
|
}
|
|
28898
|
+
if (this.markGroups.length > 0) {
|
|
28899
|
+
body.append(sid(this.section("Events"), MARKS_SETTINGS_ID));
|
|
28900
|
+
body.append(sid(this.sectionTitle("Visible events"), MARKS_GROUPS_SETTINGS_ID));
|
|
28901
|
+
for (const g of this.markGroups) {
|
|
28902
|
+
body.append(sid(this.boolRow(g.label, this.markGroupVisible(g.id), (v) => this.emit({ marks: { groups: { [g.id]: v } } })), markGroupSettingsId(g.id)));
|
|
28903
|
+
}
|
|
28904
|
+
}
|
|
28115
28905
|
renderChartTypeSections("end");
|
|
28116
28906
|
renderHostSections("end");
|
|
28117
28907
|
if (this.hiddenSettings.size > 0) {
|
|
@@ -33671,6 +34461,383 @@ function expandScaleByPixels(scale, heightPx, abovePx, belowPx) {
|
|
|
33671
34461
|
return { ...scale, min: scale.min - belowPx * perPx, max: scale.max + abovePx * perPx };
|
|
33672
34462
|
}
|
|
33673
34463
|
|
|
34464
|
+
// src/ui/sanitize-html.ts
|
|
34465
|
+
var ALLOWED_TAGS = /* @__PURE__ */ new Set([
|
|
34466
|
+
"a",
|
|
34467
|
+
"abbr",
|
|
34468
|
+
"b",
|
|
34469
|
+
"blockquote",
|
|
34470
|
+
"br",
|
|
34471
|
+
"code",
|
|
34472
|
+
"dd",
|
|
34473
|
+
"del",
|
|
34474
|
+
"div",
|
|
34475
|
+
"dl",
|
|
34476
|
+
"dt",
|
|
34477
|
+
"em",
|
|
34478
|
+
"h1",
|
|
34479
|
+
"h2",
|
|
34480
|
+
"h3",
|
|
34481
|
+
"h4",
|
|
34482
|
+
"h5",
|
|
34483
|
+
"h6",
|
|
34484
|
+
"hr",
|
|
34485
|
+
"i",
|
|
34486
|
+
"img",
|
|
34487
|
+
"ins",
|
|
34488
|
+
"kbd",
|
|
34489
|
+
"li",
|
|
34490
|
+
"mark",
|
|
34491
|
+
"ol",
|
|
34492
|
+
"p",
|
|
34493
|
+
"pre",
|
|
34494
|
+
"q",
|
|
34495
|
+
"s",
|
|
34496
|
+
"small",
|
|
34497
|
+
"span",
|
|
34498
|
+
"strong",
|
|
34499
|
+
"sub",
|
|
34500
|
+
"sup",
|
|
34501
|
+
"table",
|
|
34502
|
+
"tbody",
|
|
34503
|
+
"td",
|
|
34504
|
+
"tfoot",
|
|
34505
|
+
"th",
|
|
34506
|
+
"thead",
|
|
34507
|
+
"tr",
|
|
34508
|
+
"u",
|
|
34509
|
+
"ul"
|
|
34510
|
+
]);
|
|
34511
|
+
var DROPPED_TAGS = /* @__PURE__ */ new Set([
|
|
34512
|
+
"script",
|
|
34513
|
+
"style",
|
|
34514
|
+
"iframe",
|
|
34515
|
+
"frame",
|
|
34516
|
+
"frameset",
|
|
34517
|
+
"object",
|
|
34518
|
+
"embed",
|
|
34519
|
+
"applet",
|
|
34520
|
+
"form",
|
|
34521
|
+
"input",
|
|
34522
|
+
"textarea",
|
|
34523
|
+
"button",
|
|
34524
|
+
"select",
|
|
34525
|
+
"option",
|
|
34526
|
+
"link",
|
|
34527
|
+
"meta",
|
|
34528
|
+
"base",
|
|
34529
|
+
"svg",
|
|
34530
|
+
"math",
|
|
34531
|
+
"template",
|
|
34532
|
+
"noscript",
|
|
34533
|
+
"audio",
|
|
34534
|
+
"video",
|
|
34535
|
+
"canvas",
|
|
34536
|
+
"dialog",
|
|
34537
|
+
"head",
|
|
34538
|
+
"title"
|
|
34539
|
+
]);
|
|
34540
|
+
var ALLOWED_ATTRS = {
|
|
34541
|
+
a: /* @__PURE__ */ new Set(["href"]),
|
|
34542
|
+
img: /* @__PURE__ */ new Set(["src", "alt", "width", "height"]),
|
|
34543
|
+
td: /* @__PURE__ */ new Set(["colspan", "rowspan"]),
|
|
34544
|
+
th: /* @__PURE__ */ new Set(["colspan", "rowspan"]),
|
|
34545
|
+
ol: /* @__PURE__ */ new Set(["start"])
|
|
34546
|
+
};
|
|
34547
|
+
var BLOCKED_SCHEMES = /* @__PURE__ */ new Set(["javascript", "vbscript", "data", "file", "blob"]);
|
|
34548
|
+
function tagDisposition(tag) {
|
|
34549
|
+
const t = tag.toLowerCase();
|
|
34550
|
+
if (DROPPED_TAGS.has(t)) return "drop";
|
|
34551
|
+
return ALLOWED_TAGS.has(t) ? "keep" : "unwrap";
|
|
34552
|
+
}
|
|
34553
|
+
function attributeAllowed(tag, name) {
|
|
34554
|
+
const n = name.toLowerCase();
|
|
34555
|
+
if (n.startsWith("on") || n === "style") return false;
|
|
34556
|
+
if (n === "title") return true;
|
|
34557
|
+
return ALLOWED_ATTRS[tag.toLowerCase()]?.has(n) ?? false;
|
|
34558
|
+
}
|
|
34559
|
+
function safeUrl(value, absoluteOnly = false) {
|
|
34560
|
+
let url = "";
|
|
34561
|
+
for (const ch of value) if (ch.charCodeAt(0) > 32) url += ch;
|
|
34562
|
+
const m = /^([a-z][a-z0-9+.-]*):/i.exec(url);
|
|
34563
|
+
const scheme = m ? m[1].toLowerCase() : null;
|
|
34564
|
+
if (scheme !== null && BLOCKED_SCHEMES.has(scheme)) return null;
|
|
34565
|
+
if (absoluteOnly && scheme !== "http" && scheme !== "https") return null;
|
|
34566
|
+
return url;
|
|
34567
|
+
}
|
|
34568
|
+
var ELEMENT_NODE = 1;
|
|
34569
|
+
var TEXT_NODE = 3;
|
|
34570
|
+
function sanitizeHtml(html, doc) {
|
|
34571
|
+
const tpl = doc.createElement("template");
|
|
34572
|
+
tpl.innerHTML = html;
|
|
34573
|
+
const out = doc.createDocumentFragment();
|
|
34574
|
+
copyChildren(tpl.content, out, doc);
|
|
34575
|
+
return out;
|
|
34576
|
+
}
|
|
34577
|
+
function copyChildren(from, to, doc) {
|
|
34578
|
+
for (const child of Array.from(from.childNodes)) {
|
|
34579
|
+
if (child.nodeType === TEXT_NODE) {
|
|
34580
|
+
to.appendChild(doc.createTextNode(child.textContent ?? ""));
|
|
34581
|
+
continue;
|
|
34582
|
+
}
|
|
34583
|
+
if (child.nodeType !== ELEMENT_NODE) continue;
|
|
34584
|
+
const el = child;
|
|
34585
|
+
const tag = el.tagName.toLowerCase();
|
|
34586
|
+
const disposition = tagDisposition(tag);
|
|
34587
|
+
if (disposition === "drop") continue;
|
|
34588
|
+
if (disposition === "unwrap") {
|
|
34589
|
+
copyChildren(el, to, doc);
|
|
34590
|
+
continue;
|
|
34591
|
+
}
|
|
34592
|
+
const clean = doc.createElement(tag);
|
|
34593
|
+
for (const attr of Array.from(el.attributes)) {
|
|
34594
|
+
const name = attr.name.toLowerCase();
|
|
34595
|
+
if (!attributeAllowed(tag, name)) continue;
|
|
34596
|
+
let value = attr.value;
|
|
34597
|
+
if (name === "href" || name === "src") {
|
|
34598
|
+
const safe = safeUrl(value, name === "src");
|
|
34599
|
+
if (safe === null) continue;
|
|
34600
|
+
value = safe;
|
|
34601
|
+
}
|
|
34602
|
+
clean.setAttribute(name, value);
|
|
34603
|
+
}
|
|
34604
|
+
if (tag === "a") {
|
|
34605
|
+
clean.setAttribute("target", "_blank");
|
|
34606
|
+
clean.setAttribute("rel", "noopener noreferrer");
|
|
34607
|
+
}
|
|
34608
|
+
copyChildren(el, clean, doc);
|
|
34609
|
+
to.appendChild(clean);
|
|
34610
|
+
}
|
|
34611
|
+
}
|
|
34612
|
+
|
|
34613
|
+
// src/renderers/native/chrome/marks/MarkPopover.ts
|
|
34614
|
+
var MARKS_STYLE_ID = "vela-marks-popover";
|
|
34615
|
+
var MARKS_CSS = `
|
|
34616
|
+
.vela-marks-panel { padding: 0; gap: 0; min-width: 220px; max-width: 320px; max-height: 320px; overflow-y: auto; overscroll-behavior: contain; }
|
|
34617
|
+
.vela-marks-section { display: flex; flex-direction: column; gap: 8px; padding: 10px 12px; }
|
|
34618
|
+
.vela-marks-section + .vela-marks-section { border-top: 1px solid var(--vela-border); }
|
|
34619
|
+
.vela-marks-field { display: flex; justify-content: space-between; gap: 16px; line-height: 1.45; }
|
|
34620
|
+
.vela-marks-field-label { color: var(--vela-fg-muted); }
|
|
34621
|
+
.vela-marks-field-value { color: var(--vela-fg-bright); text-align: right; font-variant-numeric: tabular-nums; }
|
|
34622
|
+
.vela-marks-html { color: var(--vela-fg); line-height: 1.45; overflow-wrap: anywhere; }
|
|
34623
|
+
.vela-marks-html p { margin: 0 0 6px; }
|
|
34624
|
+
.vela-marks-html p:last-child { margin-bottom: 0; }
|
|
34625
|
+
.vela-marks-html a { color: var(--vela-accent); }
|
|
34626
|
+
.vela-marks-html img { max-width: 100%; height: auto; }
|
|
34627
|
+
.vela-marks-html table { border-collapse: collapse; }
|
|
34628
|
+
.vela-marks-html td, .vela-marks-html th { padding: 2px 6px; border: 1px solid var(--vela-border); }
|
|
34629
|
+
.vela-marks-html pre, .vela-marks-html code { font-family: var(--vela-font-mono, monospace); font-size: 0.92em; }
|
|
34630
|
+
.vela-marks-loading, .vela-marks-error { color: var(--vela-fg-muted); font-style: italic; }
|
|
34631
|
+
`;
|
|
34632
|
+
var MarkPopover = class {
|
|
34633
|
+
constructor(deps) {
|
|
34634
|
+
this.deps = deps;
|
|
34635
|
+
this.pop = null;
|
|
34636
|
+
this.openKey = null;
|
|
34637
|
+
/** Bumped per open/close — a lazy content resolving after its popup went away is dropped. */
|
|
34638
|
+
this.generation = 0;
|
|
34639
|
+
const doc = deps.plot.ownerDocument;
|
|
34640
|
+
injectStyles(CALLOUT_STYLE_ID, CALLOUT_CSS, doc);
|
|
34641
|
+
injectStyles(MARKS_STYLE_ID, MARKS_CSS, doc);
|
|
34642
|
+
this.anchor = doc.createElement("div");
|
|
34643
|
+
this.anchor.className = "vela-marks-anchor";
|
|
34644
|
+
Object.assign(this.anchor.style, { position: "absolute", pointerEvents: "none", left: "0", top: "0", width: "0", height: "0" });
|
|
34645
|
+
deps.plot.appendChild(this.anchor);
|
|
34646
|
+
}
|
|
34647
|
+
/** The cluster key the open popup belongs to, or null. */
|
|
34648
|
+
get key() {
|
|
34649
|
+
return this.openKey;
|
|
34650
|
+
}
|
|
34651
|
+
open(cluster, rect) {
|
|
34652
|
+
this.close();
|
|
34653
|
+
this.place(rect);
|
|
34654
|
+
const gen = ++this.generation;
|
|
34655
|
+
this.openKey = cluster.key;
|
|
34656
|
+
this.pop = new Popover({
|
|
34657
|
+
trigger: this.anchor,
|
|
34658
|
+
host: this.deps.host(),
|
|
34659
|
+
theme: this.deps.theme(),
|
|
34660
|
+
gap: 8,
|
|
34661
|
+
align: "center",
|
|
34662
|
+
// centered on the glyph
|
|
34663
|
+
fadeMs: 120,
|
|
34664
|
+
// a short, discreet fade in and out
|
|
34665
|
+
className: "vela-marks-pop",
|
|
34666
|
+
content: (body) => this.build(body, cluster, gen),
|
|
34667
|
+
onClose: () => {
|
|
34668
|
+
if (this.generation === gen) {
|
|
34669
|
+
this.openKey = null;
|
|
34670
|
+
this.pop = null;
|
|
34671
|
+
this.deps.onOpenChange?.(null);
|
|
34672
|
+
}
|
|
34673
|
+
}
|
|
34674
|
+
});
|
|
34675
|
+
this.pop.show();
|
|
34676
|
+
this.deps.onOpenChange?.(cluster.key);
|
|
34677
|
+
}
|
|
34678
|
+
/** Follow the anchor glyph after a repaint; `null` (glyph gone — hidden, scrolled off, marks replaced) closes. */
|
|
34679
|
+
track(rect) {
|
|
34680
|
+
if (!this.pop) return;
|
|
34681
|
+
if (!rect) {
|
|
34682
|
+
this.close();
|
|
34683
|
+
return;
|
|
34684
|
+
}
|
|
34685
|
+
this.place(rect);
|
|
34686
|
+
this.pop.reposition();
|
|
34687
|
+
}
|
|
34688
|
+
close() {
|
|
34689
|
+
const pop = this.pop;
|
|
34690
|
+
const wasOpen = this.openKey !== null;
|
|
34691
|
+
this.pop = null;
|
|
34692
|
+
this.openKey = null;
|
|
34693
|
+
this.generation++;
|
|
34694
|
+
pop?.destroy();
|
|
34695
|
+
if (wasOpen) this.deps.onOpenChange?.(null);
|
|
34696
|
+
}
|
|
34697
|
+
destroy() {
|
|
34698
|
+
this.close();
|
|
34699
|
+
this.anchor.remove();
|
|
34700
|
+
}
|
|
34701
|
+
place(rect) {
|
|
34702
|
+
Object.assign(this.anchor.style, {
|
|
34703
|
+
left: `${rect.x - rect.size / 2}px`,
|
|
34704
|
+
top: `${rect.y - rect.size / 2}px`,
|
|
34705
|
+
width: `${rect.size}px`,
|
|
34706
|
+
height: `${rect.size}px`
|
|
34707
|
+
});
|
|
34708
|
+
}
|
|
34709
|
+
build(body, cluster, gen) {
|
|
34710
|
+
const doc = body.ownerDocument;
|
|
34711
|
+
const root = doc.createElement("div");
|
|
34712
|
+
root.className = "vela-callout-panel vela-marks-panel";
|
|
34713
|
+
const pending = [];
|
|
34714
|
+
for (const mark of cluster.marks) {
|
|
34715
|
+
const section = doc.createElement("section");
|
|
34716
|
+
section.className = "vela-marks-section";
|
|
34717
|
+
if (mark.title) {
|
|
34718
|
+
const title = doc.createElement("div");
|
|
34719
|
+
title.className = "vela-callout-title";
|
|
34720
|
+
title.textContent = mark.title;
|
|
34721
|
+
section.appendChild(title);
|
|
34722
|
+
}
|
|
34723
|
+
const content = mark.content;
|
|
34724
|
+
if (typeof content === "function") {
|
|
34725
|
+
const slot = doc.createElement("div");
|
|
34726
|
+
slot.className = "vela-marks-loading";
|
|
34727
|
+
slot.textContent = "Loading\u2026";
|
|
34728
|
+
section.appendChild(slot);
|
|
34729
|
+
pending.push({ el: section, resolve: () => this.resolveLazy(content, slot, gen) });
|
|
34730
|
+
} else if (content !== void 0) {
|
|
34731
|
+
this.renderContent(section, content);
|
|
34732
|
+
}
|
|
34733
|
+
if (section.childElementCount > 0) root.appendChild(section);
|
|
34734
|
+
}
|
|
34735
|
+
body.appendChild(root);
|
|
34736
|
+
if (pending.length > 0) scheduleLazy(root, pending);
|
|
34737
|
+
}
|
|
34738
|
+
resolveLazy(source, slot, gen) {
|
|
34739
|
+
let result;
|
|
34740
|
+
try {
|
|
34741
|
+
result = Promise.resolve(source());
|
|
34742
|
+
} catch (err) {
|
|
34743
|
+
result = Promise.reject(err instanceof Error ? err : new Error(String(err)));
|
|
34744
|
+
}
|
|
34745
|
+
void result.then(
|
|
34746
|
+
(content) => {
|
|
34747
|
+
if (gen !== this.generation) return;
|
|
34748
|
+
const section = slot.parentElement;
|
|
34749
|
+
if (!section) return;
|
|
34750
|
+
slot.remove();
|
|
34751
|
+
this.renderContent(section, content);
|
|
34752
|
+
this.pop?.reposition();
|
|
34753
|
+
},
|
|
34754
|
+
() => {
|
|
34755
|
+
if (gen !== this.generation) return;
|
|
34756
|
+
slot.className = "vela-marks-error";
|
|
34757
|
+
slot.textContent = "Couldn\u2019t load this entry.";
|
|
34758
|
+
}
|
|
34759
|
+
);
|
|
34760
|
+
}
|
|
34761
|
+
renderContent(section, content) {
|
|
34762
|
+
const doc = section.ownerDocument;
|
|
34763
|
+
if (!content || typeof content !== "object") return;
|
|
34764
|
+
if ("text" in content) {
|
|
34765
|
+
const text = doc.createElement("div");
|
|
34766
|
+
text.className = "vela-callout-text";
|
|
34767
|
+
text.textContent = String(content.text);
|
|
34768
|
+
section.appendChild(text);
|
|
34769
|
+
return;
|
|
34770
|
+
}
|
|
34771
|
+
if ("html" in content) {
|
|
34772
|
+
const html = doc.createElement("div");
|
|
34773
|
+
html.className = "vela-marks-html";
|
|
34774
|
+
html.appendChild(sanitizeHtml(String(content.html), doc));
|
|
34775
|
+
section.appendChild(html);
|
|
34776
|
+
return;
|
|
34777
|
+
}
|
|
34778
|
+
if ("panel" in content && content.panel && Array.isArray(content.panel.items)) {
|
|
34779
|
+
let actions = null;
|
|
34780
|
+
for (const item of content.panel.items) {
|
|
34781
|
+
if (!item || typeof item !== "object") continue;
|
|
34782
|
+
if (item.type === "button") {
|
|
34783
|
+
if (!actions) {
|
|
34784
|
+
actions = doc.createElement("div");
|
|
34785
|
+
actions.className = "vela-callout-actions";
|
|
34786
|
+
section.appendChild(actions);
|
|
34787
|
+
}
|
|
34788
|
+
const btn2 = doc.createElement("button");
|
|
34789
|
+
btn2.type = "button";
|
|
34790
|
+
btn2.className = "vela-callout-btn" + (item.primary ? " vela-callout-btn-primary" : "");
|
|
34791
|
+
btn2.textContent = item.label;
|
|
34792
|
+
btn2.addEventListener("click", () => {
|
|
34793
|
+
item.run();
|
|
34794
|
+
if (item.close !== false) this.close();
|
|
34795
|
+
});
|
|
34796
|
+
actions.appendChild(btn2);
|
|
34797
|
+
continue;
|
|
34798
|
+
}
|
|
34799
|
+
actions = null;
|
|
34800
|
+
if (item.type === "text") {
|
|
34801
|
+
const text = doc.createElement("div");
|
|
34802
|
+
text.className = "vela-callout-text";
|
|
34803
|
+
text.textContent = item.text;
|
|
34804
|
+
section.appendChild(text);
|
|
34805
|
+
} else if (item.type === "field") {
|
|
34806
|
+
const row = doc.createElement("div");
|
|
34807
|
+
row.className = "vela-marks-field";
|
|
34808
|
+
const label = doc.createElement("span");
|
|
34809
|
+
label.className = "vela-marks-field-label";
|
|
34810
|
+
label.textContent = item.label;
|
|
34811
|
+
const value = doc.createElement("span");
|
|
34812
|
+
value.className = "vela-marks-field-value";
|
|
34813
|
+
value.textContent = item.value;
|
|
34814
|
+
row.append(label, value);
|
|
34815
|
+
section.appendChild(row);
|
|
34816
|
+
}
|
|
34817
|
+
}
|
|
34818
|
+
}
|
|
34819
|
+
}
|
|
34820
|
+
};
|
|
34821
|
+
function scheduleLazy(scroller, pending) {
|
|
34822
|
+
if (typeof IntersectionObserver === "undefined") {
|
|
34823
|
+
for (const p of pending) p.resolve();
|
|
34824
|
+
return;
|
|
34825
|
+
}
|
|
34826
|
+
const io = new IntersectionObserver(
|
|
34827
|
+
(entries) => {
|
|
34828
|
+
for (const e of entries) {
|
|
34829
|
+
if (!e.isIntersecting) continue;
|
|
34830
|
+
const p = pending.find((q) => q.el === e.target);
|
|
34831
|
+
if (!p) continue;
|
|
34832
|
+
io.unobserve(e.target);
|
|
34833
|
+
p.resolve();
|
|
34834
|
+
}
|
|
34835
|
+
},
|
|
34836
|
+
{ root: scroller }
|
|
34837
|
+
);
|
|
34838
|
+
for (const p of pending) io.observe(p.el);
|
|
34839
|
+
}
|
|
34840
|
+
|
|
33674
34841
|
// src/renderers/native/core/manualScale.ts
|
|
33675
34842
|
function rescaleAround(start, factor) {
|
|
33676
34843
|
if (start.log && start.min > 0 && start.max > start.min) {
|
|
@@ -34263,11 +35430,12 @@ var SCROLL_BTN_BOTTOM = TIME_AXIS_H + 14;
|
|
|
34263
35430
|
var SCROLL_BTN_PROXIMITY_PX = 120;
|
|
34264
35431
|
var MIN_VISIBLE_BARS = 2;
|
|
34265
35432
|
var ZOOM_OUT_MARGIN_BARS = 6;
|
|
34266
|
-
var
|
|
34267
|
-
var SCALE_TAU_MS = 80;
|
|
34268
|
-
var FLING_TAU_MS = 110;
|
|
34269
|
-
var SCROLL_TO_TAU_MS = 130;
|
|
35433
|
+
var INTRO_MODEL_FADE_MS = 350;
|
|
34270
35434
|
var FLING_STOP_PX = 0.02;
|
|
35435
|
+
var MARK_FLASH_MS = 220;
|
|
35436
|
+
function frameNow() {
|
|
35437
|
+
return typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
35438
|
+
}
|
|
34271
35439
|
var PRICE_SCALE_K = 4e-3;
|
|
34272
35440
|
var KEY_ZOOM_STEP = 0.2;
|
|
34273
35441
|
var SEPARATOR_HIT_PX = 4;
|
|
@@ -34316,9 +35484,20 @@ var NativeRenderer = class {
|
|
|
34316
35484
|
this.indicatorSlices = new IndicatorDrawingSlices();
|
|
34317
35485
|
/** Hover tooltips for Pine labels (canvas hit-rects collected by the chrome layer). */
|
|
34318
35486
|
this.labelTooltip = null;
|
|
35487
|
+
/** The timeline-mark popup (a kit Popover anchored on a lane glyph); null before mount. */
|
|
35488
|
+
this.markPopover = null;
|
|
35489
|
+
/** How the fanned mark stack was opened: a hover folds when the pointer leaves, a tap only on a tap elsewhere. */
|
|
35490
|
+
this.marksExpandedBy = "hover";
|
|
35491
|
+
/** The rAF loop keeping the chrome repainting while a lane glyph pulses (hover) or flashes (click); null when idle. */
|
|
35492
|
+
this.markPulseRaf = null;
|
|
35493
|
+
this.markClickCbs = /* @__PURE__ */ new Set();
|
|
34319
35494
|
this.crosshairLayer = new CrosshairRenderer();
|
|
34320
|
-
/**
|
|
34321
|
-
|
|
35495
|
+
/** The second pulse the countdown-to-bar-close chip ticks on: the host's (`setWallClock`)
|
|
35496
|
+
* when one is wired, else the renderer's own second-aligned clock. */
|
|
35497
|
+
this.hostClock = null;
|
|
35498
|
+
this.ownClock = null;
|
|
35499
|
+
/** Live subscription to the pulse while the countdown is on; null when off. */
|
|
35500
|
+
this.countdownUnsub = null;
|
|
34322
35501
|
this.symbolPicker = null;
|
|
34323
35502
|
/** Indicator titles (the legend rows) shown — held here so a remount re-applies it. */
|
|
34324
35503
|
this.indicatorTitlesOn = true;
|
|
@@ -34336,19 +35515,25 @@ var NativeRenderer = class {
|
|
|
34336
35515
|
/** Drawings layer self-serves Ctrl+Z/Y (see the `historyChords` feature). */
|
|
34337
35516
|
this.historyChordsEnabled = true;
|
|
34338
35517
|
this.liveRegion = null;
|
|
34339
|
-
// ── animation state
|
|
34340
|
-
|
|
34341
|
-
this.
|
|
34342
|
-
|
|
34343
|
-
|
|
34344
|
-
|
|
34345
|
-
|
|
35518
|
+
// ── animation state: one ease time-constant per motion (0 = off), each remembering the
|
|
35519
|
+
// host's duration so the config's on/off switches restore it (see EaseSetting) ──
|
|
35520
|
+
this.animZoom = new EaseSetting(ZOOM_EASE_DEFAULT_MS);
|
|
35521
|
+
// wheel-zoom glide
|
|
35522
|
+
this.animPan = new EaseSetting(PAN_INERTIA_DEFAULT_MS);
|
|
35523
|
+
// inertial-pan velocity decay
|
|
35524
|
+
this.animScroll = new EaseSetting(SCROLL_EASE_DEFAULT_MS);
|
|
35525
|
+
// scroll-to-latest / panBy glide
|
|
35526
|
+
this.animAutoscale = new EaseSetting(AUTOSCALE_EASE_DEFAULT_MS);
|
|
35527
|
+
// autoscale glide during zoom/fling
|
|
35528
|
+
this.animLiveBar = new EaseSetting(LIVE_BAR_EASE_DEFAULT_MS, 0);
|
|
35529
|
+
// forming-bar OHLC glide; ships off
|
|
34346
35530
|
// Brand default candles.
|
|
34347
35531
|
this.candleUp = BULLISH;
|
|
34348
35532
|
this.candleDown = BEARISH;
|
|
34349
35533
|
// ── intro reveal (plays once when candles first appear) ──
|
|
34350
|
-
this.
|
|
34351
|
-
|
|
35534
|
+
this.intro = { style: "settle", duration: INTRO_DURATION_DEFAULT_MS };
|
|
35535
|
+
this.introOnStyle = "settle";
|
|
35536
|
+
// the style the config's on/off switch restores
|
|
34352
35537
|
this.introPlayed = false;
|
|
34353
35538
|
this.introRaf = null;
|
|
34354
35539
|
/** The load affordance (three pulsing dots) — up while the host reports a bar load in
|
|
@@ -34467,7 +35652,7 @@ var NativeRenderer = class {
|
|
|
34467
35652
|
this.moveIndicatorCbs = /* @__PURE__ */ new Set();
|
|
34468
35653
|
this.priceStyleCbs = /* @__PURE__ */ new Set();
|
|
34469
35654
|
this.name = "native";
|
|
34470
|
-
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"];
|
|
35655
|
+
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"];
|
|
34471
35656
|
/** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
|
|
34472
35657
|
* so moving onto the button doesn't count as leaving). */
|
|
34473
35658
|
this.onScrollProximityMove = (e) => {
|
|
@@ -34499,9 +35684,12 @@ var NativeRenderer = class {
|
|
|
34499
35684
|
this.scene.showPriceLine = opts.currentPriceLine;
|
|
34500
35685
|
this.scene.logScale = opts.logScale;
|
|
34501
35686
|
this.backendMode = opts.nativeBackend;
|
|
34502
|
-
this.animZoom
|
|
34503
|
-
this.animPan
|
|
34504
|
-
this.
|
|
35687
|
+
this.animZoom.set(opts.animZoom);
|
|
35688
|
+
this.animPan.set(opts.animPan);
|
|
35689
|
+
this.animScroll.set(opts.animScroll);
|
|
35690
|
+
this.animAutoscale.set(opts.animAutoscale);
|
|
35691
|
+
this.animLiveBar.set(opts.animLiveBar);
|
|
35692
|
+
this.setIntro(opts.animIntro);
|
|
34505
35693
|
this.glowAmount = opts.glow;
|
|
34506
35694
|
this.candleUp = opts.upColor;
|
|
34507
35695
|
this.candleDown = opts.downColor;
|
|
@@ -34545,20 +35733,29 @@ var NativeRenderer = class {
|
|
|
34545
35733
|
if (this.backend && "glow" in this.backend) this.backend.glow = this.glowAmount;
|
|
34546
35734
|
break;
|
|
34547
35735
|
case "animZoom":
|
|
34548
|
-
this.animZoom
|
|
35736
|
+
this.animZoom.set(resolveEaseMs(value, ZOOM_EASE_DEFAULT_MS));
|
|
34549
35737
|
return;
|
|
34550
35738
|
// affects the next interaction only — nothing to repaint
|
|
34551
|
-
case "animPan":
|
|
34552
|
-
|
|
35739
|
+
case "animPan": {
|
|
35740
|
+
const ms = resolveEaseMs(value, PAN_INERTIA_DEFAULT_MS);
|
|
35741
|
+
this.animPan.set(ms);
|
|
35742
|
+
this.animScroll.toggle(ms > 0);
|
|
35743
|
+
return;
|
|
35744
|
+
}
|
|
35745
|
+
case "animScroll":
|
|
35746
|
+
this.animScroll.set(resolveEaseMs(value, SCROLL_EASE_DEFAULT_MS));
|
|
34553
35747
|
return;
|
|
35748
|
+
case "animAutoscale":
|
|
35749
|
+
this.animAutoscale.set(resolveEaseMs(value, AUTOSCALE_EASE_DEFAULT_MS));
|
|
35750
|
+
return;
|
|
35751
|
+
// a glide in flight finishes at the new rate (or snaps at 0)
|
|
34554
35752
|
case "animLiveBar":
|
|
34555
|
-
this.
|
|
35753
|
+
this.animLiveBar.set(resolveLiveBarEaseMs(value));
|
|
34556
35754
|
return;
|
|
34557
35755
|
// affects the next tick only; a glide in flight finishes at the new rate (or snaps at 0)
|
|
34558
35756
|
case "intro": {
|
|
34559
|
-
|
|
34560
|
-
this.
|
|
34561
|
-
if (s) this.playIntro(s);
|
|
35757
|
+
this.setIntro(resolveIntro(value));
|
|
35758
|
+
if (this.intro.style) this.playIntro();
|
|
34562
35759
|
return;
|
|
34563
35760
|
}
|
|
34564
35761
|
case "zoomAnchor":
|
|
@@ -34630,6 +35827,10 @@ var NativeRenderer = class {
|
|
|
34630
35827
|
case "tradeMarkers":
|
|
34631
35828
|
this.scene.tradeMarkers = mergeTradeMarkersState(this.scene.tradeMarkers, value);
|
|
34632
35829
|
break;
|
|
35830
|
+
case "marks":
|
|
35831
|
+
this.scene.marks = mergeMarksState(this.scene.marks, value);
|
|
35832
|
+
this.markPopover?.close();
|
|
35833
|
+
break;
|
|
34633
35834
|
case "keyboard":
|
|
34634
35835
|
this.setKeyboardEnabled(Boolean(value));
|
|
34635
35836
|
return;
|
|
@@ -34688,13 +35889,17 @@ var NativeRenderer = class {
|
|
|
34688
35889
|
case "glow":
|
|
34689
35890
|
return this.glowAmount;
|
|
34690
35891
|
case "animZoom":
|
|
34691
|
-
return this.animZoom;
|
|
35892
|
+
return this.animZoom.tau;
|
|
34692
35893
|
case "animPan":
|
|
34693
|
-
return this.animPan;
|
|
35894
|
+
return this.animPan.tau;
|
|
35895
|
+
case "animScroll":
|
|
35896
|
+
return this.animScroll.tau;
|
|
35897
|
+
case "animAutoscale":
|
|
35898
|
+
return this.animAutoscale.tau;
|
|
34694
35899
|
case "animLiveBar":
|
|
34695
|
-
return this.
|
|
35900
|
+
return this.animLiveBar.tau;
|
|
34696
35901
|
case "intro":
|
|
34697
|
-
return this.
|
|
35902
|
+
return this.intro.style;
|
|
34698
35903
|
case "zoomAnchor":
|
|
34699
35904
|
return this.zoomAnchorMode;
|
|
34700
35905
|
case "axisDrag":
|
|
@@ -34737,6 +35942,8 @@ var NativeRenderer = class {
|
|
|
34737
35942
|
}
|
|
34738
35943
|
case "tradeMarkers":
|
|
34739
35944
|
return { ...this.scene.tradeMarkers, colors: { ...this.scene.tradeMarkers.colors } };
|
|
35945
|
+
case "marks":
|
|
35946
|
+
return { visible: this.scene.marks.visible, groups: { ...this.scene.marks.groups } };
|
|
34740
35947
|
case "keyboard":
|
|
34741
35948
|
return this.keyboardEnabled;
|
|
34742
35949
|
case "historyChords":
|
|
@@ -34862,7 +36069,13 @@ var NativeRenderer = class {
|
|
|
34862
36069
|
currentPriceLine: this.scene.showPriceLine,
|
|
34863
36070
|
priceLabel: this.scene.showPriceLabel,
|
|
34864
36071
|
countdown: this.scene.showCountdown,
|
|
34865
|
-
animateLastPrice: this.
|
|
36072
|
+
animateLastPrice: this.animLiveBar.on
|
|
36073
|
+
},
|
|
36074
|
+
animations: {
|
|
36075
|
+
zoom: this.animZoom.on,
|
|
36076
|
+
pan: this.animPan.on,
|
|
36077
|
+
autoscale: this.animAutoscale.on,
|
|
36078
|
+
intro: this.intro.style !== false
|
|
34866
36079
|
},
|
|
34867
36080
|
panes: { separatorColor: s.separatorColor ?? t.borderColor },
|
|
34868
36081
|
trades: {
|
|
@@ -34874,6 +36087,7 @@ var NativeRenderer = class {
|
|
|
34874
36087
|
exitColor: this.scene.tradeMarkers.colors.exit
|
|
34875
36088
|
},
|
|
34876
36089
|
timeScale: { timezone: this.scene.timezone },
|
|
36090
|
+
marks: { visible: this.scene.marks.visible, groups: { ...this.scene.marks.groups } },
|
|
34877
36091
|
candles: {
|
|
34878
36092
|
upColor: this.candleUp,
|
|
34879
36093
|
downColor: this.candleDown,
|
|
@@ -34969,7 +36183,12 @@ var NativeRenderer = class {
|
|
|
34969
36183
|
this.scene.showPriceLabel = next.priceScale.priceLabel;
|
|
34970
36184
|
this.scene.showCountdown = next.priceScale.countdown;
|
|
34971
36185
|
this.syncCountdownTimer();
|
|
34972
|
-
this.
|
|
36186
|
+
this.animLiveBar.toggle(next.priceScale.animateLastPrice);
|
|
36187
|
+
this.animZoom.toggle(next.animations.zoom);
|
|
36188
|
+
this.animPan.toggle(next.animations.pan);
|
|
36189
|
+
this.animScroll.toggle(next.animations.pan);
|
|
36190
|
+
this.animAutoscale.toggle(next.animations.autoscale);
|
|
36191
|
+
this.intro = { style: next.animations.intro ? this.introOnStyle : false, duration: this.intro.duration || INTRO_DURATION_DEFAULT_MS };
|
|
34973
36192
|
s.separatorColor = keepInherit(s.separatorColor, next.panes.separatorColor, prevTheme.borderColor);
|
|
34974
36193
|
this.scene.tradeMarkers = {
|
|
34975
36194
|
visible: next.trades.visible,
|
|
@@ -34978,6 +36197,7 @@ var NativeRenderer = class {
|
|
|
34978
36197
|
colors: { long: next.trades.longColor, short: next.trades.shortColor, exit: next.trades.exitColor }
|
|
34979
36198
|
};
|
|
34980
36199
|
this.scene.timezone = next.timeScale.timezone;
|
|
36200
|
+
this.scene.marks = { visible: next.marks.visible, groups: { ...next.marks.groups } };
|
|
34981
36201
|
this.candleUp = next.candles.upColor;
|
|
34982
36202
|
this.candleDown = next.candles.downColor;
|
|
34983
36203
|
s.candle = {
|
|
@@ -35110,6 +36330,7 @@ var NativeRenderer = class {
|
|
|
35110
36330
|
}
|
|
35111
36331
|
this.settingsDialog.setTheme(this.theme);
|
|
35112
36332
|
this.settingsDialog.setHostSections(this.hostSettingsSections);
|
|
36333
|
+
this.settingsDialog.setMarkGroups(this.markGroupsInUse(), (id) => markGroupVisible(this.scene.marks, id, this.scene.markGroups));
|
|
35113
36334
|
this.settingsDialog.setHiddenSettings(this.hiddenSettings);
|
|
35114
36335
|
this.syncThemeControl();
|
|
35115
36336
|
this.settingsDialog.toggle(
|
|
@@ -35200,10 +36421,10 @@ var NativeRenderer = class {
|
|
|
35200
36421
|
this.glideRightOffset(ZOOM_OUT_MARGIN_BARS);
|
|
35201
36422
|
}
|
|
35202
36423
|
/** Ease rightOffset to `target` at constant zoom (see animTick's scroll glide);
|
|
35203
|
-
* instant when
|
|
36424
|
+
* instant when the scroll glide is off. Shared by scroll-to-latest and panBy. */
|
|
35204
36425
|
glideRightOffset(target) {
|
|
35205
36426
|
const vp = this.coords.getViewport();
|
|
35206
|
-
if (!this.
|
|
36427
|
+
if (!this.animScroll.on) {
|
|
35207
36428
|
this.applyViewport({ barSpacing: vp.barSpacing, rightOffset: target });
|
|
35208
36429
|
return;
|
|
35209
36430
|
}
|
|
@@ -35268,20 +36489,21 @@ var NativeRenderer = class {
|
|
|
35268
36489
|
* full size, eased, with a left→right stagger so the chart draws itself; `settle`
|
|
35269
36490
|
* adds an ease-out-back overshoot. Autoscale stays on the real bars so the frame
|
|
35270
36491
|
* never moves. Re-callable, so styles can be compared live from the console.
|
|
36492
|
+
* Style and sweep duration come from the resolved `intro` setting.
|
|
35271
36493
|
*/
|
|
35272
|
-
playIntro(
|
|
36494
|
+
playIntro() {
|
|
35273
36495
|
if (this.introRaf != null) cancelAnimationFrame(this.introRaf);
|
|
35274
36496
|
this.introRaf = null;
|
|
36497
|
+
const { style, duration } = this.intro;
|
|
35275
36498
|
const real = this.bars;
|
|
35276
36499
|
const n = real.length;
|
|
35277
|
-
if (n === 0) return;
|
|
36500
|
+
if (n === 0 || !style) return;
|
|
35278
36501
|
this.computeScales();
|
|
35279
36502
|
for (const pane of this.scene.panes.values()) pane.scale = { ...pane.scaleTarget };
|
|
35280
36503
|
this.modelAlpha = 0;
|
|
35281
|
-
const DURATION = 650;
|
|
35282
36504
|
const start = performance.now();
|
|
35283
36505
|
const step = (now) => {
|
|
35284
|
-
const p = Math.min(1, (now - start) /
|
|
36506
|
+
const p = Math.min(1, (now - start) / duration);
|
|
35285
36507
|
this.scene.bars = p >= 1 ? real : real.map((b, i) => this.revealCandle(b, i, p, n, style));
|
|
35286
36508
|
this.paintData();
|
|
35287
36509
|
if (p < 1) {
|
|
@@ -35295,10 +36517,10 @@ var NativeRenderer = class {
|
|
|
35295
36517
|
}
|
|
35296
36518
|
/** After the candle reveal, fade the indicator models (series/fills/…) from hidden to full. */
|
|
35297
36519
|
fadeInModels() {
|
|
35298
|
-
const
|
|
36520
|
+
const fade = Math.min(INTRO_MODEL_FADE_MS, this.intro.duration || INTRO_MODEL_FADE_MS);
|
|
35299
36521
|
const start = performance.now();
|
|
35300
36522
|
const step = (now) => {
|
|
35301
|
-
this.modelAlpha = Math.min(1, (now - start) /
|
|
36523
|
+
this.modelAlpha = Math.min(1, (now - start) / fade);
|
|
35302
36524
|
this.paintData();
|
|
35303
36525
|
if (this.modelAlpha < 1) {
|
|
35304
36526
|
this.introRaf = requestAnimationFrame(step);
|
|
@@ -35413,7 +36635,8 @@ var NativeRenderer = class {
|
|
|
35413
36635
|
zoomTo: (target, anchorLogical, anchorX) => this.zoomTo(target, anchorLogical, anchorX),
|
|
35414
36636
|
fling: (v) => this.fling(v),
|
|
35415
36637
|
onPointerMove: (x, y) => this.handlePointerMove(x, y),
|
|
35416
|
-
onClick: (x) => {
|
|
36638
|
+
onClick: (x, y) => {
|
|
36639
|
+
if (this.handleMarkClick(x, y)) return;
|
|
35417
36640
|
this.userDrawings?.deselect();
|
|
35418
36641
|
this.handleClick(x);
|
|
35419
36642
|
},
|
|
@@ -35442,7 +36665,7 @@ var NativeRenderer = class {
|
|
|
35442
36665
|
drawingsPointerDown: (x, y, snap, shift2, mod) => this.userDrawings?.pointerDown(x, y, snap, shift2, mod),
|
|
35443
36666
|
drawingsPointerMove: (x, y, snap, shift2, mod) => this.userDrawings?.pointerMove(x, y, snap, shift2, mod),
|
|
35444
36667
|
drawingsPointerUp: (x, y, snap) => this.userDrawings?.pointerUp(x, y, snap),
|
|
35445
|
-
drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? null,
|
|
36668
|
+
drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? (this.chrome.markGlyphAt(x, y) ? "pointer" : null),
|
|
35446
36669
|
drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
|
|
35447
36670
|
drawingsClearTransient: () => this.userDrawings?.clearTransient()
|
|
35448
36671
|
});
|
|
@@ -35458,8 +36681,18 @@ var NativeRenderer = class {
|
|
|
35458
36681
|
this.plot.addEventListener("pointerleave", this.onScrollProximityLeave);
|
|
35459
36682
|
this.labelTooltip = new LabelTooltip(this.plot, {
|
|
35460
36683
|
theme: () => this.chromeTheme(),
|
|
35461
|
-
lookup: (x, y) => this.indicatorSlices.labelTooltipAt(x, y)
|
|
36684
|
+
lookup: (x, y) => this.indicatorSlices.labelTooltipAt(x, y) ?? this.chrome.markTooltipAt(x, y, this.markGroupsInUse())
|
|
36685
|
+
});
|
|
36686
|
+
this.markPopover = new MarkPopover({
|
|
36687
|
+
plot: this.plot,
|
|
36688
|
+
host: () => this.dialogHost ?? this.plot,
|
|
36689
|
+
theme: () => this.chromeTheme(),
|
|
36690
|
+
onOpenChange: (key) => {
|
|
36691
|
+
this.scene.marksActiveKey = key;
|
|
36692
|
+
this.scheduler?.invalidate(2 /* Chrome */);
|
|
36693
|
+
}
|
|
35462
36694
|
});
|
|
36695
|
+
this.chrome.setMarkIconReady(() => this.scheduler?.invalidate(2 /* Chrome */));
|
|
35463
36696
|
this.userDrawings = new UserDrawingController(this.wrapper, this.plot, this.drawingsCanvas, {
|
|
35464
36697
|
projector: () => this.drawingProjector(),
|
|
35465
36698
|
dpr: () => this.coords.dpr,
|
|
@@ -35674,29 +36907,40 @@ var NativeRenderer = class {
|
|
|
35674
36907
|
resize() {
|
|
35675
36908
|
this.syncSize();
|
|
35676
36909
|
}
|
|
35677
|
-
/**
|
|
35678
|
-
|
|
36910
|
+
/** Drive the countdown chip from the host's second pulse (`null` → the renderer's own). */
|
|
36911
|
+
setWallClock(clock) {
|
|
36912
|
+
if (clock === this.hostClock) return;
|
|
36913
|
+
this.hostClock = clock;
|
|
36914
|
+
if (this.countdownUnsub != null) {
|
|
36915
|
+
this.countdownUnsub();
|
|
36916
|
+
this.countdownUnsub = null;
|
|
36917
|
+
}
|
|
36918
|
+
this.syncCountdownTimer();
|
|
36919
|
+
}
|
|
36920
|
+
/** Subscribe to the second pulse while the countdown chip is on (so it ticks); unsubscribe
|
|
36921
|
+
* otherwise. Chrome tier: only the chip's text moves — an idle chart must not recompute
|
|
35679
36922
|
* scales or repaint the geometry/volume/VPVR/SDK layers once a second (that cost
|
|
35680
36923
|
* multiplies by the cell count in a multi-chart workspace). */
|
|
35681
36924
|
syncCountdownTimer() {
|
|
35682
36925
|
if (this.scene.showCountdown) {
|
|
35683
|
-
if (this.
|
|
35684
|
-
this.
|
|
36926
|
+
if (this.countdownUnsub == null) {
|
|
36927
|
+
const clock = this.hostClock ?? (this.ownClock ?? (this.ownClock = new SecondClock()));
|
|
36928
|
+
this.countdownUnsub = clock.onTick(() => {
|
|
35685
36929
|
if (this.scene.showCountdown && this.scene.bars.length > 0) this.scheduler?.invalidate(2 /* Chrome */);
|
|
35686
|
-
}
|
|
36930
|
+
});
|
|
35687
36931
|
}
|
|
35688
|
-
} else if (this.
|
|
35689
|
-
|
|
35690
|
-
this.
|
|
36932
|
+
} else if (this.countdownUnsub != null) {
|
|
36933
|
+
this.countdownUnsub();
|
|
36934
|
+
this.countdownUnsub = null;
|
|
35691
36935
|
}
|
|
35692
36936
|
}
|
|
35693
36937
|
destroy() {
|
|
35694
36938
|
if (this.introRaf != null) cancelAnimationFrame(this.introRaf);
|
|
35695
36939
|
this.loadingEl?.remove();
|
|
35696
36940
|
this.loadingEl = null;
|
|
35697
|
-
if (this.
|
|
35698
|
-
|
|
35699
|
-
this.
|
|
36941
|
+
if (this.countdownUnsub != null) {
|
|
36942
|
+
this.countdownUnsub();
|
|
36943
|
+
this.countdownUnsub = null;
|
|
35700
36944
|
}
|
|
35701
36945
|
this.scheduler?.destroy();
|
|
35702
36946
|
this.animator?.stop();
|
|
@@ -35721,6 +36965,11 @@ var NativeRenderer = class {
|
|
|
35721
36965
|
this.plot?.removeEventListener("pointerleave", this.onScrollProximityLeave);
|
|
35722
36966
|
this.labelTooltip?.destroy();
|
|
35723
36967
|
this.labelTooltip = null;
|
|
36968
|
+
this.markPopover?.destroy();
|
|
36969
|
+
this.markPopover = null;
|
|
36970
|
+
this.chrome.setMarkIconReady(null);
|
|
36971
|
+
if (this.markPulseRaf !== null) cancelAnimationFrame(this.markPulseRaf);
|
|
36972
|
+
this.markPulseRaf = null;
|
|
35724
36973
|
this.scrollButton?.remove();
|
|
35725
36974
|
this.scrollButton = null;
|
|
35726
36975
|
for (const l of this.extLayers) l.instance.destroy?.();
|
|
@@ -35779,8 +37028,8 @@ var NativeRenderer = class {
|
|
|
35779
37028
|
}
|
|
35780
37029
|
if (!this.introPlayed && this.bars.length > 0) {
|
|
35781
37030
|
this.introPlayed = true;
|
|
35782
|
-
if (this.
|
|
35783
|
-
this.playIntro(
|
|
37031
|
+
if (this.intro.style) {
|
|
37032
|
+
this.playIntro();
|
|
35784
37033
|
return;
|
|
35785
37034
|
}
|
|
35786
37035
|
}
|
|
@@ -35791,7 +37040,7 @@ var NativeRenderer = class {
|
|
|
35791
37040
|
const last = this.bars[n - 1];
|
|
35792
37041
|
if (last && bar.time === last.time) {
|
|
35793
37042
|
this.bars[n - 1] = bar;
|
|
35794
|
-
if (this.
|
|
37043
|
+
if (!this.animLiveBar.on || this.liveEaseTime !== bar.time) {
|
|
35795
37044
|
this.syncLiveEase(bar);
|
|
35796
37045
|
} else {
|
|
35797
37046
|
this.animator.start();
|
|
@@ -35807,11 +37056,11 @@ var NativeRenderer = class {
|
|
|
35807
37056
|
this.scene.bars = this.bars;
|
|
35808
37057
|
this.scheduler.invalidate(4 /* Full */);
|
|
35809
37058
|
}
|
|
35810
|
-
/** Set the
|
|
35811
|
-
*
|
|
35812
|
-
|
|
35813
|
-
this.
|
|
35814
|
-
if (
|
|
37059
|
+
/** Set the reveal (style + duration). A non-off style is also remembered as what the
|
|
37060
|
+
* config's on/off toggle (`animations.intro`) switches back on to. */
|
|
37061
|
+
setIntro(next) {
|
|
37062
|
+
this.intro = next;
|
|
37063
|
+
if (next.style) this.introOnStyle = next.style;
|
|
35815
37064
|
}
|
|
35816
37065
|
/** Snap the eased forming-bar state to `bar` — no glide (a fresh bar or the first tick of one). */
|
|
35817
37066
|
syncLiveEase(bar) {
|
|
@@ -35825,7 +37074,7 @@ var NativeRenderer = class {
|
|
|
35825
37074
|
const target = this.bars[this.bars.length - 1];
|
|
35826
37075
|
if (!target || this.liveEaseTime !== target.time) return false;
|
|
35827
37076
|
const eps = Math.max(1e-9, Math.abs(target.close) * 1e-6);
|
|
35828
|
-
const tau = this.
|
|
37077
|
+
const tau = this.animLiveBar.tau;
|
|
35829
37078
|
const nh = easeToward(this.liveEaseHigh, target.high, dtMs, tau);
|
|
35830
37079
|
const nl = easeToward(this.liveEaseLow, target.low, dtMs, tau);
|
|
35831
37080
|
const nc = easeToward(this.liveEaseClose, target.close, dtMs, tau);
|
|
@@ -35965,10 +37214,12 @@ var NativeRenderer = class {
|
|
|
35965
37214
|
this.refreshAnchorOffset(model);
|
|
35966
37215
|
if (model.native && this.extLayers.some((l) => l.def.id === model.native.type)) this.scene.assignIndicatorZTop(model.id);
|
|
35967
37216
|
else this.scene.assignIndicatorZ(model.id);
|
|
35968
|
-
|
|
35969
|
-
|
|
35970
|
-
|
|
35971
|
-
|
|
37217
|
+
if (model.legend !== false) {
|
|
37218
|
+
this.inputsUI.upsert(model.id, model.shorttitle ?? model.title, model.inputs, model.inputValues, model.paneId, {
|
|
37219
|
+
native: !!model.native,
|
|
37220
|
+
...model.props ? { props: model.props, propValues: model.propValues ?? {} } : {}
|
|
37221
|
+
});
|
|
37222
|
+
}
|
|
35972
37223
|
if (model.native?.type === "volume") {
|
|
35973
37224
|
this.volumeActive = true;
|
|
35974
37225
|
this.volumeHidden = false;
|
|
@@ -36115,7 +37366,7 @@ var NativeRenderer = class {
|
|
|
36115
37366
|
this.settingsDialog?.setHiddenSettings(this.hiddenSettings);
|
|
36116
37367
|
}
|
|
36117
37368
|
listSettingsIds() {
|
|
36118
|
-
return settingsIdCatalog(this.hostSettingsSections);
|
|
37369
|
+
return settingsIdCatalog(this.hostSettingsSections, this.markGroupsInUse());
|
|
36119
37370
|
}
|
|
36120
37371
|
onChartTypeSettingsChange(cb) {
|
|
36121
37372
|
this.chartTypeSettingsCbs.add(cb);
|
|
@@ -36141,6 +37392,22 @@ var NativeRenderer = class {
|
|
|
36141
37392
|
this.axisLongPressCbs.add(cb);
|
|
36142
37393
|
return () => this.axisLongPressCbs.delete(cb);
|
|
36143
37394
|
}
|
|
37395
|
+
// ── timeline marks (the `chart.marks` model; see the port) ──
|
|
37396
|
+
setTimelineMarks(marks, groups) {
|
|
37397
|
+
this.scene.timelineMarks = marks;
|
|
37398
|
+
this.scene.markGroups = groups;
|
|
37399
|
+
this.scene.marksExpandedStack = null;
|
|
37400
|
+
this.markPopover?.close();
|
|
37401
|
+
this.scheduler?.invalidate(2 /* Chrome */);
|
|
37402
|
+
}
|
|
37403
|
+
onMarkClick(cb) {
|
|
37404
|
+
this.markClickCbs.add(cb);
|
|
37405
|
+
return () => this.markClickCbs.delete(cb);
|
|
37406
|
+
}
|
|
37407
|
+
/** Every group the lane knows: the defined ones, then those marks name without a definition. */
|
|
37408
|
+
markGroupsInUse() {
|
|
37409
|
+
return effectiveMarkGroups(this.scene.timelineMarks, this.scene.markGroups);
|
|
37410
|
+
}
|
|
36144
37411
|
onViewportChange(cb) {
|
|
36145
37412
|
this.viewportCbs.add(cb);
|
|
36146
37413
|
return () => this.viewportCbs.delete(cb);
|
|
@@ -36194,7 +37461,7 @@ var NativeRenderer = class {
|
|
|
36194
37461
|
this.zoomAnchorX = anchorX;
|
|
36195
37462
|
this.panVelocity = 0;
|
|
36196
37463
|
this.scrollTargetRO = null;
|
|
36197
|
-
if (!this.animZoom) {
|
|
37464
|
+
if (!this.animZoom.on) {
|
|
36198
37465
|
const v = this.clampViewport(barSpacing, this.anchoredRightOffset(barSpacing));
|
|
36199
37466
|
this.coords.setViewport(v);
|
|
36200
37467
|
this.targetBarSpacing = v.barSpacing;
|
|
@@ -36207,7 +37474,7 @@ var NativeRenderer = class {
|
|
|
36207
37474
|
}
|
|
36208
37475
|
/** Inertial pan: continue with a rightOffset velocity (logical units / ms) that decays. */
|
|
36209
37476
|
fling(velocity) {
|
|
36210
|
-
if (!this.animPan) return;
|
|
37477
|
+
if (!this.animPan.on) return;
|
|
36211
37478
|
this.scrollTargetRO = null;
|
|
36212
37479
|
this.panVelocity = velocity;
|
|
36213
37480
|
this.animator.start();
|
|
@@ -36247,7 +37514,7 @@ var NativeRenderer = class {
|
|
|
36247
37514
|
let active = false;
|
|
36248
37515
|
const tbs = this.targetBarSpacing;
|
|
36249
37516
|
if (Math.abs(barSpacing - tbs) > tbs * 1e-3) {
|
|
36250
|
-
barSpacing = clampBarSpacing(easeToward(barSpacing, tbs, dtMs,
|
|
37517
|
+
barSpacing = clampBarSpacing(easeToward(barSpacing, tbs, dtMs, this.animZoom.tau));
|
|
36251
37518
|
rightOffset = this.anchoredRightOffset(barSpacing);
|
|
36252
37519
|
active = true;
|
|
36253
37520
|
} else if (barSpacing !== tbs) {
|
|
@@ -36257,13 +37524,14 @@ var NativeRenderer = class {
|
|
|
36257
37524
|
const stopVel = FLING_STOP_PX / Math.max(1e-6, barSpacing * this.coords.spacingScale);
|
|
36258
37525
|
if (Math.abs(this.panVelocity) > stopVel) {
|
|
36259
37526
|
rightOffset += this.panVelocity * dtMs;
|
|
36260
|
-
|
|
37527
|
+
const tau = this.animPan.tau;
|
|
37528
|
+
this.panVelocity = tau > 0 ? this.panVelocity * Math.exp(-dtMs / tau) : 0;
|
|
36261
37529
|
if (Math.abs(this.panVelocity) <= stopVel) this.panVelocity = 0;
|
|
36262
37530
|
else active = true;
|
|
36263
37531
|
}
|
|
36264
37532
|
if (this.scrollTargetRO != null) {
|
|
36265
37533
|
const target = this.scrollTargetRO;
|
|
36266
|
-
const next = easeToward(rightOffset, target, dtMs,
|
|
37534
|
+
const next = easeToward(rightOffset, target, dtMs, this.animScroll.tau);
|
|
36267
37535
|
if (Math.abs(next - target) < 1e-3) {
|
|
36268
37536
|
rightOffset = target;
|
|
36269
37537
|
this.scrollTargetRO = null;
|
|
@@ -36291,6 +37559,7 @@ var NativeRenderer = class {
|
|
|
36291
37559
|
* through Math.log), not a non-linear jump. */
|
|
36292
37560
|
easeScales(dtMs) {
|
|
36293
37561
|
let moving = false;
|
|
37562
|
+
const tau = this.animAutoscale.tau;
|
|
36294
37563
|
for (const pane of this.scene.panes.values()) {
|
|
36295
37564
|
const t = pane.scaleTarget;
|
|
36296
37565
|
const s = pane.scale;
|
|
@@ -36298,8 +37567,8 @@ var NativeRenderer = class {
|
|
|
36298
37567
|
const lt0 = Math.log(t.min);
|
|
36299
37568
|
const lt1 = Math.log(t.max);
|
|
36300
37569
|
const lspan = Math.max(1e-9, Math.abs(lt1 - lt0));
|
|
36301
|
-
const n0 = easeToward(Math.log(s.min), lt0, dtMs,
|
|
36302
|
-
const n1 = easeToward(Math.log(s.max), lt1, dtMs,
|
|
37570
|
+
const n0 = easeToward(Math.log(s.min), lt0, dtMs, tau);
|
|
37571
|
+
const n1 = easeToward(Math.log(s.max), lt1, dtMs, tau);
|
|
36303
37572
|
if (Math.abs(n0 - lt0) <= lspan * 1e-3 && Math.abs(n1 - lt1) <= lspan * 1e-3) {
|
|
36304
37573
|
pane.scale = { min: t.min, max: t.max, log: true };
|
|
36305
37574
|
} else {
|
|
@@ -36309,8 +37578,8 @@ var NativeRenderer = class {
|
|
|
36309
37578
|
continue;
|
|
36310
37579
|
}
|
|
36311
37580
|
const span = Math.max(1e-9, Math.abs(t.max - t.min));
|
|
36312
|
-
let nmin = easeToward(s.min, t.min, dtMs,
|
|
36313
|
-
let nmax = easeToward(s.max, t.max, dtMs,
|
|
37581
|
+
let nmin = easeToward(s.min, t.min, dtMs, tau);
|
|
37582
|
+
let nmax = easeToward(s.max, t.max, dtMs, tau);
|
|
36314
37583
|
if (Math.abs(nmin - t.min) <= span * 1e-3 && Math.abs(nmax - t.max) <= span * 1e-3) {
|
|
36315
37584
|
nmin = t.min;
|
|
36316
37585
|
nmax = t.max;
|
|
@@ -36323,8 +37592,8 @@ var NativeRenderer = class {
|
|
|
36323
37592
|
const t = sl.scaleTarget;
|
|
36324
37593
|
const s = sl.scale;
|
|
36325
37594
|
const span = Math.max(1e-9, Math.abs(t.max - t.min));
|
|
36326
|
-
let nmin = easeToward(s.min, t.min, dtMs,
|
|
36327
|
-
let nmax = easeToward(s.max, t.max, dtMs,
|
|
37595
|
+
let nmin = easeToward(s.min, t.min, dtMs, tau);
|
|
37596
|
+
let nmax = easeToward(s.max, t.max, dtMs, tau);
|
|
36328
37597
|
if (Math.abs(nmin - t.min) <= span * 1e-3 && Math.abs(nmax - t.max) <= span * 1e-3) {
|
|
36329
37598
|
nmin = t.min;
|
|
36330
37599
|
nmax = t.max;
|
|
@@ -36345,6 +37614,8 @@ var NativeRenderer = class {
|
|
|
36345
37614
|
this.scene.crosshair = null;
|
|
36346
37615
|
this.hoverSeparatorY = null;
|
|
36347
37616
|
this.lastPointer = null;
|
|
37617
|
+
if (this.marksExpandedBy === "hover") this.setMarksExpanded(null);
|
|
37618
|
+
this.setMarkHover(null);
|
|
36348
37619
|
this.scheduler.invalidate(1 /* Cursor */);
|
|
36349
37620
|
this.hoverLogical = null;
|
|
36350
37621
|
const empty = { time: null, price: null, paneKind: null, values: /* @__PURE__ */ new Map(), ohlc: null };
|
|
@@ -36356,6 +37627,8 @@ var NativeRenderer = class {
|
|
|
36356
37627
|
this.lastPointer = inData ? { x, y } : null;
|
|
36357
37628
|
this.hoverSeparatorY = x >= 0 && y >= 0 && y <= this.coords.height ? this.separatorHoverY(y) : null;
|
|
36358
37629
|
this.scheduler.invalidate(1 /* Cursor */);
|
|
37630
|
+
this.setMarksExpanded(inData ? this.chrome.markStackAt(x, y) : null);
|
|
37631
|
+
this.setMarkHover(inData ? this.chrome.markGlyphAt(x, y)?.cluster.key ?? null : null);
|
|
36359
37632
|
const logical = Math.round(this.coords.xToLogical(x));
|
|
36360
37633
|
const onBar = logical >= 0 && logical < this.coords.barCount;
|
|
36361
37634
|
const time = onBar ? this.coords.logicalToTime(logical) : null;
|
|
@@ -36385,6 +37658,76 @@ var NativeRenderer = class {
|
|
|
36385
37658
|
const onBar = logical >= 0 && logical < this.coords.barCount;
|
|
36386
37659
|
for (const cb of this.clickCbs) cb({ time: onBar ? this.coords.logicalToTime(logical) : null, price: null });
|
|
36387
37660
|
}
|
|
37661
|
+
/**
|
|
37662
|
+
* Fan out (or collapse, with null) a multi-group mark stack; repaints the chrome tier when
|
|
37663
|
+
* it changes. A stack whose glyph holds the open popup stays fanned — the pointer leaving
|
|
37664
|
+
* the plot for the popup must not bury the glyph under the deck (which would close it).
|
|
37665
|
+
*/
|
|
37666
|
+
setMarksExpanded(stack, by = "hover") {
|
|
37667
|
+
if (this.scene.marksExpandedStack === stack) return;
|
|
37668
|
+
if (stack === null && this.markPopover?.key) {
|
|
37669
|
+
const open2 = this.chrome.markGlyphByKey(this.markPopover.key);
|
|
37670
|
+
if (open2 && open2.stack === this.scene.marksExpandedStack) return;
|
|
37671
|
+
}
|
|
37672
|
+
this.scene.marksExpandedStack = stack;
|
|
37673
|
+
this.marksExpandedBy = by;
|
|
37674
|
+
this.scheduler?.invalidate(2 /* Chrome */);
|
|
37675
|
+
}
|
|
37676
|
+
/**
|
|
37677
|
+
* A click on the mark lane: a collapsed deck fans out (the touch path — a mouse already
|
|
37678
|
+
* fanned it by hovering), a glyph reports its cluster (`onMarkClick`) and opens the popup
|
|
37679
|
+
* when any of its marks carries content. True when the click landed on the lane.
|
|
37680
|
+
*/
|
|
37681
|
+
handleMarkClick(x, y) {
|
|
37682
|
+
const glyph = this.chrome.markGlyphAt(x, y);
|
|
37683
|
+
if (!glyph) {
|
|
37684
|
+
if (this.scene.marksExpandedStack !== null && this.chrome.markStackAt(x, y) === null) this.setMarksExpanded(null);
|
|
37685
|
+
return false;
|
|
37686
|
+
}
|
|
37687
|
+
if (glyph.decked) {
|
|
37688
|
+
this.setMarksExpanded(glyph.stack, "tap");
|
|
37689
|
+
return true;
|
|
37690
|
+
}
|
|
37691
|
+
const marks = glyph.cluster.marks;
|
|
37692
|
+
const first = marks[0];
|
|
37693
|
+
const event = { id: first.id, ids: marks.map((m) => m.id), time: first.time, ...glyph.cluster.group !== void 0 ? { group: glyph.cluster.group } : {} };
|
|
37694
|
+
for (const cb of this.markClickCbs) cb(event);
|
|
37695
|
+
if (marks.some((m) => m.content !== void 0)) {
|
|
37696
|
+
this.markPopover?.open(glyph.cluster, { x: glyph.x, y: glyph.y, size: glyph.size });
|
|
37697
|
+
} else {
|
|
37698
|
+
this.scene.marksFlash = { key: glyph.cluster.key, until: frameNow() + MARK_FLASH_MS };
|
|
37699
|
+
this.syncMarkPulse();
|
|
37700
|
+
}
|
|
37701
|
+
return true;
|
|
37702
|
+
}
|
|
37703
|
+
/** After a chrome frame: keep the open mark popup on its glyph, or close it once the glyph is gone. */
|
|
37704
|
+
trackMarkPopover() {
|
|
37705
|
+
const key = this.markPopover?.key;
|
|
37706
|
+
if (!key) return;
|
|
37707
|
+
const g = this.chrome.markGlyphByKey(key);
|
|
37708
|
+
this.markPopover.track(g && !(g.decked && g.depth !== 0) ? { x: g.x, y: g.y, size: g.size } : null);
|
|
37709
|
+
}
|
|
37710
|
+
/** The lane glyph under the pointer — it swells once as the pointer lands (a rAF-driven chrome repaint for the pulse's duration). */
|
|
37711
|
+
setMarkHover(key) {
|
|
37712
|
+
if (this.scene.marksHoverKey === key) return;
|
|
37713
|
+
this.scene.marksHoverKey = key;
|
|
37714
|
+
this.scene.marksHoverSince = frameNow();
|
|
37715
|
+
this.scheduler?.invalidate(2 /* Chrome */);
|
|
37716
|
+
this.syncMarkPulse();
|
|
37717
|
+
}
|
|
37718
|
+
/** Run a chrome-tier repaint loop while a glyph's hover pulse or click flash plays; it stops itself once both are over. */
|
|
37719
|
+
syncMarkPulse() {
|
|
37720
|
+
if (this.markPulseRaf !== null || typeof requestAnimationFrame !== "function") return;
|
|
37721
|
+
const tick = () => {
|
|
37722
|
+
this.markPulseRaf = null;
|
|
37723
|
+
const now = frameNow();
|
|
37724
|
+
if (this.scene.marksFlash && this.scene.marksFlash.until <= now) this.scene.marksFlash = null;
|
|
37725
|
+
this.scheduler?.invalidate(2 /* Chrome */);
|
|
37726
|
+
const pulsing = this.scene.marksHoverKey !== null && now - this.scene.marksHoverSince < MARK_PULSE_MS;
|
|
37727
|
+
if (pulsing || this.scene.marksFlash !== null) this.markPulseRaf = requestAnimationFrame(tick);
|
|
37728
|
+
};
|
|
37729
|
+
this.markPulseRaf = requestAnimationFrame(tick);
|
|
37730
|
+
}
|
|
36388
37731
|
paneAtY(y) {
|
|
36389
37732
|
return this.paneNodeAtY(y);
|
|
36390
37733
|
}
|
|
@@ -36776,6 +38119,7 @@ var NativeRenderer = class {
|
|
|
36776
38119
|
} else if (repaintsChrome(level) && this.paintedData) {
|
|
36777
38120
|
this.chrome.prepare(this.scene, this.coords, this.theme);
|
|
36778
38121
|
this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
|
|
38122
|
+
this.trackMarkPopover();
|
|
36779
38123
|
}
|
|
36780
38124
|
this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
|
|
36781
38125
|
if (!repaintsData(level) && this.paintedData) this.repaintCursorLayers();
|
|
@@ -36791,7 +38135,7 @@ var NativeRenderer = class {
|
|
|
36791
38135
|
const lp = this.layerPane(l.def.id) ?? pane;
|
|
36792
38136
|
if (lp.collapsed) continue;
|
|
36793
38137
|
l.instance.render(this.extLayerArgs(l.def.id, lp.scale, lp.bounds, nowMs));
|
|
36794
|
-
if (this.animZoom && l.instance.animating?.()) this.animator.start();
|
|
38138
|
+
if (this.animZoom.on && l.instance.animating?.()) this.animator.start();
|
|
36795
38139
|
}
|
|
36796
38140
|
}
|
|
36797
38141
|
/** Blank one SDK layer canvas (a collapsed host pane suppresses the layer's painting). */
|
|
@@ -36856,7 +38200,7 @@ var NativeRenderer = class {
|
|
|
36856
38200
|
const args = this.extLayerArgs(l.def.id, lp.scale, lp.bounds, nowMs);
|
|
36857
38201
|
l.instance.render(args);
|
|
36858
38202
|
if (lp === pane) folded = foldBaseModulation(folded, l.instance.modulateBase?.(args) ?? null);
|
|
36859
|
-
if (this.animZoom && l.instance.animating?.()) this.animator.start();
|
|
38203
|
+
if (this.animZoom.on && l.instance.animating?.()) this.animator.start();
|
|
36860
38204
|
}
|
|
36861
38205
|
if (folded) {
|
|
36862
38206
|
if (folded.candleBodyScale != null) this.backend.candleBodyScale = clamp012(folded.candleBodyScale) || 0.01;
|
|
@@ -36875,6 +38219,7 @@ var NativeRenderer = class {
|
|
|
36875
38219
|
this.backdropRenderer.render(this.scene, this.coords, this.theme, gridAlpha);
|
|
36876
38220
|
this.backend.render(this.scene, this.coords, this.theme);
|
|
36877
38221
|
this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
|
|
38222
|
+
this.trackMarkPopover();
|
|
36878
38223
|
this.userDrawings?.render();
|
|
36879
38224
|
if (easeLive && liveActual) this.bars[li] = liveActual;
|
|
36880
38225
|
this.paintedData = true;
|
|
@@ -37387,7 +38732,7 @@ var NativeRenderer = class {
|
|
|
37387
38732
|
const map2 = /* @__PURE__ */ new Map();
|
|
37388
38733
|
for (const pane of this.scene.panes.values()) {
|
|
37389
38734
|
if (!pane.collapsed) continue;
|
|
37390
|
-
const models = this.scene.orderedIndicatorsForPane(pane.id);
|
|
38735
|
+
const models = this.scene.orderedIndicatorsForPane(pane.id).filter((m) => m.legend !== false);
|
|
37391
38736
|
const merged = new Set(this.scene.ownScaleIndicatorsForPane(pane.id).map((m) => m.id));
|
|
37392
38737
|
const master = models.find((m) => !merged.has(m.id)) ?? models[0];
|
|
37393
38738
|
map2.set(pane.id, master?.id ?? null);
|
|
@@ -37636,6 +38981,8 @@ function volumeLayerData(inputs) {
|
|
|
37636
38981
|
}
|
|
37637
38982
|
var VolumeIndicator = class {
|
|
37638
38983
|
constructor() {
|
|
38984
|
+
/** Null until start() — pre-start setInputs/resume must record without pushing. */
|
|
38985
|
+
this.ctx = null;
|
|
37639
38986
|
this.inputs = {};
|
|
37640
38987
|
}
|
|
37641
38988
|
start(ctx, inputs) {
|
|
@@ -37652,13 +38999,13 @@ var VolumeIndicator = class {
|
|
|
37652
38999
|
}
|
|
37653
39000
|
setInputs(inputs) {
|
|
37654
39001
|
this.inputs = inputs;
|
|
37655
|
-
this.ctx
|
|
39002
|
+
this.ctx?.pushData(volumeLayerData(inputs));
|
|
37656
39003
|
}
|
|
37657
39004
|
/** Hiding is a renderer-layer flag (set via `setIndicatorVisible`); no resources to free. */
|
|
37658
39005
|
suspend() {
|
|
37659
39006
|
}
|
|
37660
39007
|
resume() {
|
|
37661
|
-
this.ctx
|
|
39008
|
+
this.ctx?.pushData(volumeLayerData(this.inputs));
|
|
37662
39009
|
}
|
|
37663
39010
|
stop() {
|
|
37664
39011
|
}
|
|
@@ -37704,6 +39051,8 @@ function vpvrLayerData(inputs) {
|
|
|
37704
39051
|
}
|
|
37705
39052
|
var VpvrIndicator = class {
|
|
37706
39053
|
constructor() {
|
|
39054
|
+
/** Null until start() — pre-start setInputs/resume must record without pushing. */
|
|
39055
|
+
this.ctx = null;
|
|
37707
39056
|
this.inputs = {};
|
|
37708
39057
|
}
|
|
37709
39058
|
start(ctx, inputs) {
|
|
@@ -37720,13 +39069,13 @@ var VpvrIndicator = class {
|
|
|
37720
39069
|
}
|
|
37721
39070
|
setInputs(inputs) {
|
|
37722
39071
|
this.inputs = inputs;
|
|
37723
|
-
this.ctx
|
|
39072
|
+
this.ctx?.pushData(vpvrLayerData(inputs));
|
|
37724
39073
|
}
|
|
37725
39074
|
/** Hiding is a renderer-layer flag (set via `setIndicatorVisible`); no resources to free. */
|
|
37726
39075
|
suspend() {
|
|
37727
39076
|
}
|
|
37728
39077
|
resume() {
|
|
37729
|
-
this.ctx
|
|
39078
|
+
this.ctx?.pushData(vpvrLayerData(this.inputs));
|
|
37730
39079
|
}
|
|
37731
39080
|
stop() {
|
|
37732
39081
|
}
|
|
@@ -40178,6 +41527,7 @@ var Vela = class {
|
|
|
40178
41527
|
if (Object.keys(defaults2).length > 0) this.rendererControl.set(defaults2);
|
|
40179
41528
|
this.panesControl = new PanesControl(this.orchestrator);
|
|
40180
41529
|
this.drawingsControl = new DrawingsControl(this.orchestrator.drawings);
|
|
41530
|
+
this.marksControl = new MarksControl(this.orchestrator.marks);
|
|
40181
41531
|
}
|
|
40182
41532
|
/**
|
|
40183
41533
|
* Register a scripting engine so `addIndicator({ language })` can run that
|
|
@@ -40396,6 +41746,17 @@ var Vela = class {
|
|
|
40396
41746
|
get drawings() {
|
|
40397
41747
|
return this.drawingsControl;
|
|
40398
41748
|
}
|
|
41749
|
+
/**
|
|
41750
|
+
* The chart's timeline-marks control surface: host events pinned to a bar and shown
|
|
41751
|
+
* as glyphs on a lane above the time axis, each opening a popup on click —
|
|
41752
|
+
* `chart.marks.add({ id, time, glyph, title, content })`, `chart.marks.set(list)`,
|
|
41753
|
+
* `chart.marks.defineGroup({ id, label })`. Marks are data, not user state: re-supply
|
|
41754
|
+
* them on `market:changed`. On a renderer without the `timelineMarks` capability the
|
|
41755
|
+
* model still fills but nothing paints (`chart.marks.supported`).
|
|
41756
|
+
*/
|
|
41757
|
+
get marks() {
|
|
41758
|
+
return this.marksControl;
|
|
41759
|
+
}
|
|
40399
41760
|
on(event, handler) {
|
|
40400
41761
|
return this.orchestrator.events.on(event, handler);
|
|
40401
41762
|
}
|
|
@@ -41847,7 +43208,7 @@ var ChartCell = class {
|
|
|
41847
43208
|
// A RESTORED ledger is authoritative for the auto-added volume too: a
|
|
41848
43209
|
// slot persisted without it must come back without it (fresh slots
|
|
41849
43210
|
// keep the workspace default).
|
|
41850
|
-
volume: seed.indicators ? seed.indicators.natives.
|
|
43211
|
+
volume: seed.indicators ? seed.indicators.natives.some((e) => ledgerNativeType(e) === "volume") : deps.volume,
|
|
41851
43212
|
nativeBackend: deps.nativeBackend,
|
|
41852
43213
|
// The user's drawings option minus its toolbar: one SHARED bar serves
|
|
41853
43214
|
// the whole workspace (per-cell bars would cost a 44px gutter each).
|
|
@@ -43187,6 +44548,9 @@ var VelaWorkspace = class {
|
|
|
43187
44548
|
this.events = new TypedEventBus();
|
|
43188
44549
|
this.feed = new MultiProviderFeed();
|
|
43189
44550
|
this.cellsById = /* @__PURE__ */ new Map();
|
|
44551
|
+
/** The one second pulse behind every time display: the bottom-bar clock and each
|
|
44552
|
+
* cell's countdown chip subscribe to it, so they can never read different seconds. */
|
|
44553
|
+
this.clock = new SecondClock();
|
|
43190
44554
|
this.pool = /* @__PURE__ */ new Map();
|
|
43191
44555
|
this.trackSizes = /* @__PURE__ */ new Map();
|
|
43192
44556
|
this.resizeObserver = null;
|
|
@@ -43464,6 +44828,7 @@ var VelaWorkspace = class {
|
|
|
43464
44828
|
this.drawToolbar?.setDrawingsSyncMode(!!this.syncOpts.drawings);
|
|
43465
44829
|
this.bottombar = opts.bottombar !== false ? new Bottombar(this.root, {
|
|
43466
44830
|
timezone: this.timezone,
|
|
44831
|
+
clock: this.clock,
|
|
43467
44832
|
onRange: (preset) => {
|
|
43468
44833
|
this.active.applyRange(preset);
|
|
43469
44834
|
this.bottombar?.setActiveRange(preset.id);
|
|
@@ -43907,6 +45272,7 @@ var VelaWorkspace = class {
|
|
|
43907
45272
|
const cell = this.activeId ? this.cellsById.get(this.activeId) : void 0;
|
|
43908
45273
|
const paneMax = cell ? cell.chart.panes.list().some((p) => p.maximized) : false;
|
|
43909
45274
|
this.mobileBar.setMaximizeActive(this.maximizedId != null || paneMax);
|
|
45275
|
+
this.mobileBar.setMaximizeVisible(this.def.cells.length > 1);
|
|
43910
45276
|
}
|
|
43911
45277
|
/**
|
|
43912
45278
|
* Trade the SLOTS of two live cells — the grid arrangement changes, the cells
|
|
@@ -44207,6 +45573,7 @@ var VelaWorkspace = class {
|
|
|
44207
45573
|
if (id === this.activeId) cell.host.dataset.active = "1";
|
|
44208
45574
|
this.wireCell(cell);
|
|
44209
45575
|
cell.chart.renderer.setLayoutMode(this.layoutCtl.current);
|
|
45576
|
+
cell.chart.renderer.setWallClock(this.clock);
|
|
44210
45577
|
cell.setControlsSuspended(this.layoutCtl.current === "mobile");
|
|
44211
45578
|
if (this.favs.length > 0) cell.chart.drawings.setFavorites(this.favs);
|
|
44212
45579
|
cell.setManifest(this.manifest, pooled?.indicators == null);
|