@luxalgo/vela 0.6.21 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{DataProvider-Cb-2lC5O.d.ts → DataProvider-Bu9E5Zh5.d.ts} +1 -1
- package/dist/{DataProvider-UtWw2CgJ.d.cts → DataProvider-Ck2qyS_9.d.cts} +1 -1
- package/dist/{chunk-BRPFYKTT.js → chunk-A347YL2P.js} +30 -7
- package/dist/{chunk-TRZQQUTR.js → chunk-A4G64KVF.js} +1442 -126
- package/dist/{chunk-T5Z5YUCF.js → chunk-CZRNTHD6.js} +24 -2
- package/dist/{chunk-XCBJU674.js → chunk-UHXG5J7C.js} +1 -1
- package/dist/{contributions-D8HdlKmp.d.cts → contributions-BjQ7hyIx.d.ts} +108 -3
- package/dist/{contributions-FqIWhN4p.d.ts → contributions-Cv2Mutvn.d.cts} +108 -3
- package/dist/index.cjs +1465 -125
- package/dist/index.d.cts +76 -20
- package/dist/index.d.ts +76 -20
- package/dist/index.js +2 -2
- package/dist/{options-BCRmYALw.d.cts → options-ex2_gtKp.d.cts} +200 -12
- package/dist/{options-BCRmYALw.d.ts → options-ex2_gtKp.d.ts} +200 -12
- package/dist/{plugin-B4mnNAeh.d.ts → plugin-CiyhU7pi.d.ts} +3 -3
- package/dist/{plugin-Ccupy_BV.d.cts → plugin-absNH7Yn.d.cts} +3 -3
- package/dist/plugin.d.cts +4 -4
- package/dist/plugin.d.ts +4 -4
- package/dist/providers/binance.d.cts +2 -2
- package/dist/providers/binance.d.ts +2 -2
- package/dist/providers/coinbase.d.cts +2 -2
- package/dist/providers/coinbase.d.ts +2 -2
- package/dist/providers/hyperliquid.d.cts +2 -2
- package/dist/providers/hyperliquid.d.ts +2 -2
- package/dist/{statusline-model-CCP1zm_Z.d.ts → statusline-model-BXoU4Kua.d.ts} +8 -4
- package/dist/{statusline-model-E7CMw5pQ.d.cts → statusline-model-cRhPBLPO.d.cts} +8 -4
- package/dist/ui.cjs +24 -2
- package/dist/ui.d.cts +9 -3
- package/dist/ui.d.ts +9 -3
- package/dist/ui.js +2 -2
- package/dist/vela.global.js +1465 -125
- package/dist/vela.global.min.js +77 -53
- package/dist/widget.cjs +1490 -129
- package/dist/widget.d.cts +6 -6
- package/dist/widget.d.ts +6 -6
- package/dist/widget.js +5 -5
- package/dist/workspace.cjs +1490 -129
- package/dist/workspace.d.cts +8 -5
- package/dist/workspace.d.ts +8 -5
- package/dist/workspace.js +4 -4
- package/package.json +1 -1
package/dist/vela.global.js
CHANGED
|
@@ -2,19 +2,48 @@ var Vela = (function (exports) {
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
// src/core/options.ts
|
|
5
|
+
var ZOOM_EASE_DEFAULT_MS = 70;
|
|
6
|
+
var PAN_INERTIA_DEFAULT_MS = 110;
|
|
7
|
+
var SCROLL_EASE_DEFAULT_MS = 130;
|
|
8
|
+
var AUTOSCALE_EASE_DEFAULT_MS = 80;
|
|
5
9
|
var LIVE_BAR_EASE_DEFAULT_MS = 90;
|
|
6
|
-
var
|
|
7
|
-
|
|
8
|
-
|
|
10
|
+
var INTRO_DURATION_DEFAULT_MS = 650;
|
|
11
|
+
var ANIMATION_EASE_MAX_MS = 1e3;
|
|
12
|
+
var LIVE_BAR_EASE_MAX_MS = ANIMATION_EASE_MAX_MS;
|
|
13
|
+
var INTRO_DURATION_MAX_MS = 5e3;
|
|
14
|
+
function resolveEaseMs(value, defaultMs, maxMs = ANIMATION_EASE_MAX_MS) {
|
|
15
|
+
if (value === true) return defaultMs;
|
|
9
16
|
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return 0;
|
|
10
|
-
return Math.min(value,
|
|
17
|
+
return Math.min(value, maxMs);
|
|
18
|
+
}
|
|
19
|
+
function resolveLiveBarEaseMs(value) {
|
|
20
|
+
return resolveEaseMs(value, LIVE_BAR_EASE_DEFAULT_MS, LIVE_BAR_EASE_MAX_MS);
|
|
21
|
+
}
|
|
22
|
+
function resolveIntro(value) {
|
|
23
|
+
const off = { style: false, duration: 0 };
|
|
24
|
+
if (value === true) return { style: "settle", duration: INTRO_DURATION_DEFAULT_MS };
|
|
25
|
+
if (value === "settle" || value === "grow") return { style: value, duration: INTRO_DURATION_DEFAULT_MS };
|
|
26
|
+
if (value && typeof value === "object") {
|
|
27
|
+
const o = value;
|
|
28
|
+
const d = o.duration;
|
|
29
|
+
return {
|
|
30
|
+
style: o.style === "grow" ? "grow" : "settle",
|
|
31
|
+
duration: typeof d === "number" && Number.isFinite(d) && d > 0 ? Math.min(d, INTRO_DURATION_MAX_MS) : INTRO_DURATION_DEFAULT_MS
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return off;
|
|
11
35
|
}
|
|
12
36
|
function resolveAnimations(animations) {
|
|
13
|
-
if (
|
|
37
|
+
if (animations === false) return { animZoom: 0, animPan: 0, animScroll: 0, animAutoscale: 0, animLiveBar: 0, animIntro: { style: false, duration: 0 } };
|
|
38
|
+
const cfg = animations === true || animations == null ? {} : animations;
|
|
39
|
+
const animPan = resolveEaseMs(cfg.pan ?? true, PAN_INERTIA_DEFAULT_MS);
|
|
14
40
|
return {
|
|
15
|
-
animZoom:
|
|
16
|
-
animPan
|
|
17
|
-
|
|
41
|
+
animZoom: resolveEaseMs(cfg.zoom ?? true, ZOOM_EASE_DEFAULT_MS),
|
|
42
|
+
animPan,
|
|
43
|
+
animScroll: cfg.scroll === void 0 ? animPan > 0 ? SCROLL_EASE_DEFAULT_MS : 0 : resolveEaseMs(cfg.scroll, SCROLL_EASE_DEFAULT_MS),
|
|
44
|
+
animAutoscale: resolveEaseMs(cfg.autoscale ?? true, AUTOSCALE_EASE_DEFAULT_MS),
|
|
45
|
+
animLiveBar: resolveLiveBarEaseMs(cfg.liveBar),
|
|
46
|
+
animIntro: resolveIntro(cfg.intro ?? true)
|
|
18
47
|
};
|
|
19
48
|
}
|
|
20
49
|
|
|
@@ -7057,6 +7086,93 @@ var Vela = (function (exports) {
|
|
|
7057
7086
|
}
|
|
7058
7087
|
};
|
|
7059
7088
|
|
|
7089
|
+
// src/core/marks/MarksController.ts
|
|
7090
|
+
var MarksController = class {
|
|
7091
|
+
constructor(renderer, events) {
|
|
7092
|
+
this.renderer = renderer;
|
|
7093
|
+
/** Insertion-ordered — the order a cluster falls back to for equal times. */
|
|
7094
|
+
this.marks = /* @__PURE__ */ new Map();
|
|
7095
|
+
this.groups = /* @__PURE__ */ new Map();
|
|
7096
|
+
this.subs = [];
|
|
7097
|
+
this.enabled = !!renderer.capabilities.timelineMarks && typeof renderer.setTimelineMarks === "function";
|
|
7098
|
+
if (this.enabled && renderer.onMarkClick) this.subs.push(renderer.onMarkClick((e) => events.emit("mark:click", e)));
|
|
7099
|
+
}
|
|
7100
|
+
/** Whether the active renderer paints timeline marks. */
|
|
7101
|
+
get supported() {
|
|
7102
|
+
return this.enabled;
|
|
7103
|
+
}
|
|
7104
|
+
/** Add (or replace, by id) one mark. */
|
|
7105
|
+
add(mark) {
|
|
7106
|
+
this.marks.set(mark.id, validateMark(mark));
|
|
7107
|
+
this.sync();
|
|
7108
|
+
}
|
|
7109
|
+
/** Replace the whole set — a market switch. */
|
|
7110
|
+
set(marks) {
|
|
7111
|
+
this.marks.clear();
|
|
7112
|
+
for (const m of marks) this.marks.set(m.id, validateMark(m));
|
|
7113
|
+
this.sync();
|
|
7114
|
+
}
|
|
7115
|
+
remove(id) {
|
|
7116
|
+
const had = this.marks.delete(id);
|
|
7117
|
+
if (had) this.sync();
|
|
7118
|
+
return had;
|
|
7119
|
+
}
|
|
7120
|
+
clear() {
|
|
7121
|
+
if (this.marks.size === 0) return;
|
|
7122
|
+
this.marks.clear();
|
|
7123
|
+
this.sync();
|
|
7124
|
+
}
|
|
7125
|
+
/** Every mark, in insertion order (shallow copies — mutating one changes nothing). */
|
|
7126
|
+
all() {
|
|
7127
|
+
return [...this.marks.values()].map((m) => ({ ...m, glyph: { ...m.glyph } }));
|
|
7128
|
+
}
|
|
7129
|
+
/** Define (or replace) a group's presentation — its settings label and default visibility. */
|
|
7130
|
+
defineGroup(group) {
|
|
7131
|
+
if (!group || typeof group.id !== "string" || group.id.length === 0) throw new Error("[vela] marks.defineGroup: `id` must be a non-empty string");
|
|
7132
|
+
if (typeof group.label !== "string") throw new Error(`[vela] marks.defineGroup: group "${group.id}" needs a string \`label\``);
|
|
7133
|
+
this.groups.set(group.id, { ...group });
|
|
7134
|
+
this.sync();
|
|
7135
|
+
}
|
|
7136
|
+
/** The defined groups, in definition order. */
|
|
7137
|
+
groupDefinitions() {
|
|
7138
|
+
return [...this.groups.values()].map((g) => ({ ...g }));
|
|
7139
|
+
}
|
|
7140
|
+
/**
|
|
7141
|
+
* Show or hide one group's marks. The choice lives in the renderer's cosmetic config
|
|
7142
|
+
* (the `marks` feature) — what the settings dialog's Events checkboxes edit and what
|
|
7143
|
+
* a persisted chart restores — so it warns + no-ops on a renderer without it.
|
|
7144
|
+
*/
|
|
7145
|
+
setGroupVisible(id, visible) {
|
|
7146
|
+
if (!this.renderer.features.includes("marks")) {
|
|
7147
|
+
console.warn(`[vela] renderer "${this.renderer.name}" does not paint timeline marks \u2014 setGroupVisible ignored.`);
|
|
7148
|
+
return;
|
|
7149
|
+
}
|
|
7150
|
+
this.renderer.applyFeature("marks", { groups: { [id]: visible } });
|
|
7151
|
+
}
|
|
7152
|
+
/** A group's effective visibility: the user's (persisted) choice, else the group's declared default, else visible. */
|
|
7153
|
+
isGroupVisible(id) {
|
|
7154
|
+
const state = this.renderer.readFeature("marks");
|
|
7155
|
+
const chosen = state?.groups?.[id];
|
|
7156
|
+
if (typeof chosen === "boolean") return chosen;
|
|
7157
|
+
return this.groups.get(id)?.visible !== false;
|
|
7158
|
+
}
|
|
7159
|
+
destroy() {
|
|
7160
|
+
for (const unsub of this.subs) unsub();
|
|
7161
|
+
this.subs.length = 0;
|
|
7162
|
+
}
|
|
7163
|
+
sync() {
|
|
7164
|
+
if (!this.enabled) return;
|
|
7165
|
+
this.renderer.setTimelineMarks([...this.marks.values()], [...this.groups.values()]);
|
|
7166
|
+
}
|
|
7167
|
+
};
|
|
7168
|
+
function validateMark(mark) {
|
|
7169
|
+
if (!mark || typeof mark !== "object") throw new Error("[vela] marks: a mark must be an object");
|
|
7170
|
+
if (typeof mark.id !== "string" || mark.id.length === 0) throw new Error("[vela] marks: `id` must be a non-empty string");
|
|
7171
|
+
if (typeof mark.time !== "number" || !Number.isFinite(mark.time)) throw new Error(`[vela] marks: mark "${mark.id}" needs a finite epoch-ms \`time\``);
|
|
7172
|
+
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\``);
|
|
7173
|
+
return { ...mark, glyph: { ...mark.glyph } };
|
|
7174
|
+
}
|
|
7175
|
+
|
|
7060
7176
|
// src/data/timeframe.ts
|
|
7061
7177
|
var NAMED_TF_MS = { D: 864e5, W: 6048e5, M: 2592e6 };
|
|
7062
7178
|
function timeframeToMs(timeframe) {
|
|
@@ -7735,6 +7851,7 @@ var Vela = (function (exports) {
|
|
|
7735
7851
|
marketKey: () => `${this.config.market.symbol ?? ""}|${this.config.market.session ?? ""}`
|
|
7736
7852
|
});
|
|
7737
7853
|
this.drawings = new DrawingController(this.renderer, this.events, config.drawings, drawingSeries);
|
|
7854
|
+
this.marks = new MarksController(this.renderer, this.events);
|
|
7738
7855
|
this.unresolvedUnsub = this.feed.onUnresolved?.((info) => {
|
|
7739
7856
|
this.endLoad();
|
|
7740
7857
|
this.events.emit("data:unresolved", info);
|
|
@@ -8599,6 +8716,7 @@ var Vela = (function (exports) {
|
|
|
8599
8716
|
record.session = void 0;
|
|
8600
8717
|
record.native?.instance.suspend();
|
|
8601
8718
|
if (record.renderHandle) this.renderer.setIndicatorVisible?.(record.renderHandle, false);
|
|
8719
|
+
else if (record.native) this.mountHiddenNativeRow(id, record);
|
|
8602
8720
|
} else {
|
|
8603
8721
|
if (record.renderHandle) this.renderer.setIndicatorVisible?.(record.renderHandle, true);
|
|
8604
8722
|
record.pendingStructural = true;
|
|
@@ -8686,6 +8804,7 @@ var Vela = (function (exports) {
|
|
|
8686
8804
|
this.unresolvedUnsub = null;
|
|
8687
8805
|
this.feed.destroy?.();
|
|
8688
8806
|
this.drawings.destroy();
|
|
8807
|
+
this.marks.destroy();
|
|
8689
8808
|
this.renderer.destroy();
|
|
8690
8809
|
this.events.clear();
|
|
8691
8810
|
}
|
|
@@ -8799,6 +8918,7 @@ var Vela = (function (exports) {
|
|
|
8799
8918
|
overlay: d.overlay,
|
|
8800
8919
|
paneHint: d.paneHint,
|
|
8801
8920
|
native: { type: record.native.type },
|
|
8921
|
+
...d.legend === false ? { legend: false } : {},
|
|
8802
8922
|
...out.paneAxis != null ? { paneAxis: out.paneAxis } : {},
|
|
8803
8923
|
series: out.series ?? [],
|
|
8804
8924
|
fills: out.fills ?? [],
|
|
@@ -8821,9 +8941,17 @@ var Vela = (function (exports) {
|
|
|
8821
8941
|
* over it in place (`pendingStructural`), clears the spinner, and only THEN fires
|
|
8822
8942
|
* `indicator:added`/`ready` — so event semantics and `inspect()` (which skips
|
|
8823
8943
|
* loading records) still mean "the indicator produced output".
|
|
8944
|
+
*
|
|
8945
|
+
* A HIDDEN record mounts too — dimmed, no spinner (its session never starts while
|
|
8946
|
+
* hidden, so nothing is computing and no model will ever arrive to mount the row
|
|
8947
|
+
* later). Without this an indicator ADDED hidden (a restored ledger/ext entry) had
|
|
8948
|
+
* no legend row at all: invisible AND unreachable — the eye that unhides it never
|
|
8949
|
+
* existed. The hidden mount announces immediately for the same reason: the "first
|
|
8950
|
+
* computed model" that normally announces cannot come until the indicator is shown,
|
|
8951
|
+
* and host UIs (object tree, landing watchers) must know it exists NOW.
|
|
8824
8952
|
*/
|
|
8825
8953
|
mountLoadingPlaceholder(id, record) {
|
|
8826
|
-
if (record.renderHandle ||
|
|
8954
|
+
if (record.renderHandle || !record.prepared) return;
|
|
8827
8955
|
const meta = record.prepared.meta;
|
|
8828
8956
|
const model = {
|
|
8829
8957
|
id,
|
|
@@ -8847,8 +8975,34 @@ var Vela = (function (exports) {
|
|
|
8847
8975
|
this.ensurePaneFor(paneId);
|
|
8848
8976
|
record.renderHandle = this.renderer.mountIndicator(model);
|
|
8849
8977
|
record.pendingStructural = true;
|
|
8978
|
+
if (record.hidden) {
|
|
8979
|
+
this.renderer.setIndicatorVisible?.(record.renderHandle, false);
|
|
8980
|
+
this.announce(record, this.handles.get(id));
|
|
8981
|
+
return;
|
|
8982
|
+
}
|
|
8850
8983
|
this.setLoading(record, true);
|
|
8851
8984
|
}
|
|
8985
|
+
/**
|
|
8986
|
+
* Mount the legend row for a NATIVE indicator that is being hidden BEFORE it ever
|
|
8987
|
+
* started (a restored-hidden ledger entry: `startNativeIndicator` bails on hidden
|
|
8988
|
+
* records, so no model — and therefore no row — would ever mount). The native
|
|
8989
|
+
* counterpart of {@link mountLoadingPlaceholder}'s hidden branch: an empty model
|
|
8990
|
+
* carries the title + inputs schema, the renderer marks the row hidden, and the
|
|
8991
|
+
* announce makes the indicator visible to host UIs. Showing later STARTS the
|
|
8992
|
+
* instance (the `started` flag path) and its first emit remounts over this row.
|
|
8993
|
+
*/
|
|
8994
|
+
mountHiddenNativeRow(id, record) {
|
|
8995
|
+
if (record.renderHandle || !record.native) return;
|
|
8996
|
+
const model = this.buildNativeModel(record, {});
|
|
8997
|
+
const paneId = this.routePane(id, model, record.options ?? {});
|
|
8998
|
+
this.placeModel(model, id, paneId);
|
|
8999
|
+
record.model = model;
|
|
9000
|
+
this.ensurePaneFor(paneId);
|
|
9001
|
+
record.renderHandle = this.renderer.mountIndicator(model);
|
|
9002
|
+
record.pendingStructural = true;
|
|
9003
|
+
this.renderer.setIndicatorVisible?.(record.renderHandle, false);
|
|
9004
|
+
this.announce(record, this.handles.get(id));
|
|
9005
|
+
}
|
|
8852
9006
|
/** Flip the record's loading state and reflect it in the legend row (spinner on/off). */
|
|
8853
9007
|
setLoading(record, loading) {
|
|
8854
9008
|
record.loading = loading;
|
|
@@ -8883,6 +9037,7 @@ var Vela = (function (exports) {
|
|
|
8883
9037
|
for (const r of this.registry.all()) {
|
|
8884
9038
|
const model = r.model;
|
|
8885
9039
|
if (!model) continue;
|
|
9040
|
+
if (model.legend === false) continue;
|
|
8886
9041
|
const paneId = model.paneId ?? "price";
|
|
8887
9042
|
if (!byPane.has(paneId)) byPane.set(paneId, []);
|
|
8888
9043
|
byPane.get(paneId).push({
|
|
@@ -9458,6 +9613,16 @@ var Vela = (function (exports) {
|
|
|
9458
9613
|
this.renderer.setLayoutMode?.(mode);
|
|
9459
9614
|
return this;
|
|
9460
9615
|
}
|
|
9616
|
+
/**
|
|
9617
|
+
* Drive the renderer's time-of-day chrome (the countdown-to-bar-close chip) from the
|
|
9618
|
+
* host's own second pulse, so it ticks in step with a host clock display instead of
|
|
9619
|
+
* on a separate timer that can read a different second. `null` hands the pulse back
|
|
9620
|
+
* to the renderer. Silent no-op on a renderer without time-of-day chrome.
|
|
9621
|
+
*/
|
|
9622
|
+
setWallClock(clock) {
|
|
9623
|
+
this.renderer.setWallClock?.(clock);
|
|
9624
|
+
return this;
|
|
9625
|
+
}
|
|
9461
9626
|
};
|
|
9462
9627
|
|
|
9463
9628
|
// src/core/renderer-defaults.ts
|
|
@@ -10350,6 +10515,95 @@ var Vela = (function (exports) {
|
|
|
10350
10515
|
}
|
|
10351
10516
|
};
|
|
10352
10517
|
|
|
10518
|
+
// src/core/MarksControl.ts
|
|
10519
|
+
var MarksControl = class {
|
|
10520
|
+
constructor(ctrl) {
|
|
10521
|
+
this.ctrl = ctrl;
|
|
10522
|
+
}
|
|
10523
|
+
/** Whether the active renderer paints timeline marks. */
|
|
10524
|
+
get supported() {
|
|
10525
|
+
return this.ctrl.supported;
|
|
10526
|
+
}
|
|
10527
|
+
/** Add one mark; an existing id is replaced in place. */
|
|
10528
|
+
add(mark) {
|
|
10529
|
+
this.ctrl.add(mark);
|
|
10530
|
+
return this;
|
|
10531
|
+
}
|
|
10532
|
+
/** Replace the whole set (a market switch). */
|
|
10533
|
+
set(marks) {
|
|
10534
|
+
this.ctrl.set(marks);
|
|
10535
|
+
return this;
|
|
10536
|
+
}
|
|
10537
|
+
remove(id) {
|
|
10538
|
+
this.ctrl.remove(id);
|
|
10539
|
+
return this;
|
|
10540
|
+
}
|
|
10541
|
+
clear() {
|
|
10542
|
+
this.ctrl.clear();
|
|
10543
|
+
return this;
|
|
10544
|
+
}
|
|
10545
|
+
/** Every mark, in insertion order. */
|
|
10546
|
+
all() {
|
|
10547
|
+
return this.ctrl.all();
|
|
10548
|
+
}
|
|
10549
|
+
/**
|
|
10550
|
+
* Define a visibility group's presentation: the label of its checkbox in chart
|
|
10551
|
+
* settings (the Events tab) and its default visibility. Marks may name a group
|
|
10552
|
+
* that was never defined — it then shows its capitalized id.
|
|
10553
|
+
*/
|
|
10554
|
+
defineGroup(group) {
|
|
10555
|
+
this.ctrl.defineGroup(group);
|
|
10556
|
+
return this;
|
|
10557
|
+
}
|
|
10558
|
+
/** The defined groups, in definition order. */
|
|
10559
|
+
groups() {
|
|
10560
|
+
return this.ctrl.groupDefinitions();
|
|
10561
|
+
}
|
|
10562
|
+
/** Show or hide one group's marks — the same switch as the settings checkbox, persisted with the chart's config. */
|
|
10563
|
+
setGroupVisible(id, visible = true) {
|
|
10564
|
+
this.ctrl.setGroupVisible(id, visible);
|
|
10565
|
+
return this;
|
|
10566
|
+
}
|
|
10567
|
+
/** A group's effective visibility (the user's choice, else the group's declared default). */
|
|
10568
|
+
isGroupVisible(id) {
|
|
10569
|
+
return this.ctrl.isGroupVisible(id);
|
|
10570
|
+
}
|
|
10571
|
+
};
|
|
10572
|
+
|
|
10573
|
+
// src/core/util/wall-clock.ts
|
|
10574
|
+
var BOUNDARY_SLACK_MS = 5;
|
|
10575
|
+
var SecondClock = class _SecondClock {
|
|
10576
|
+
constructor(now = () => Date.now()) {
|
|
10577
|
+
this.now = now;
|
|
10578
|
+
this.subs = /* @__PURE__ */ new Set();
|
|
10579
|
+
this.timer = null;
|
|
10580
|
+
}
|
|
10581
|
+
onTick(cb) {
|
|
10582
|
+
this.subs.add(cb);
|
|
10583
|
+
if (this.timer == null) this.arm();
|
|
10584
|
+
return () => {
|
|
10585
|
+
this.subs.delete(cb);
|
|
10586
|
+
if (this.subs.size === 0 && this.timer != null) {
|
|
10587
|
+
clearTimeout(this.timer);
|
|
10588
|
+
this.timer = null;
|
|
10589
|
+
}
|
|
10590
|
+
};
|
|
10591
|
+
}
|
|
10592
|
+
/** Milliseconds from `now` to just past the next second boundary. */
|
|
10593
|
+
static delayToNextSecond(now) {
|
|
10594
|
+
const intoSecond = (now % 1e3 + 1e3) % 1e3;
|
|
10595
|
+
return 1e3 - intoSecond + BOUNDARY_SLACK_MS;
|
|
10596
|
+
}
|
|
10597
|
+
arm() {
|
|
10598
|
+
this.timer = setTimeout(() => {
|
|
10599
|
+
this.timer = null;
|
|
10600
|
+
const now = this.now();
|
|
10601
|
+
for (const cb of this.subs) cb(now);
|
|
10602
|
+
if (this.subs.size > 0) this.arm();
|
|
10603
|
+
}, _SecondClock.delayToNextSecond(this.now()));
|
|
10604
|
+
}
|
|
10605
|
+
};
|
|
10606
|
+
|
|
10353
10607
|
// src/core/color.ts
|
|
10354
10608
|
function parseRgb(color) {
|
|
10355
10609
|
const s = color.trim();
|
|
@@ -17261,7 +17515,7 @@ ${STATIC_DECLS}
|
|
|
17261
17515
|
return { left, top, right, bottom, width: right - left, height: bottom - top };
|
|
17262
17516
|
}
|
|
17263
17517
|
function placePopover(a) {
|
|
17264
|
-
let left = a.align === "end" ? a.trigger.right - a.pop.width : a.trigger.left;
|
|
17518
|
+
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;
|
|
17265
17519
|
const below = a.trigger.bottom + a.gap;
|
|
17266
17520
|
const above = a.trigger.top - a.pop.height - a.gap;
|
|
17267
17521
|
const fitsBelow = below + a.pop.height <= a.clamp.bottom;
|
|
@@ -17333,6 +17587,8 @@ ${STATIC_DECLS}
|
|
|
17333
17587
|
this.onKey = null;
|
|
17334
17588
|
this.onReflow = null;
|
|
17335
17589
|
this.shown = false;
|
|
17590
|
+
/** The pending removal of a fading-out shell; a show() that reuses the shell cancels it. */
|
|
17591
|
+
this.leaveTimer = null;
|
|
17336
17592
|
const doc = opts.trigger.ownerDocument;
|
|
17337
17593
|
injectStyles(POPOVER_STYLE_ID, POPOVER_CSS, doc);
|
|
17338
17594
|
this.trigger = opts.trigger;
|
|
@@ -17340,6 +17596,7 @@ ${STATIC_DECLS}
|
|
|
17340
17596
|
this.ctrl = popoverController(opts);
|
|
17341
17597
|
this.boundary = opts.boundary ?? "viewport";
|
|
17342
17598
|
this.theme = opts.theme;
|
|
17599
|
+
this.fadeMs = Math.max(0, opts.fadeMs ?? 0);
|
|
17343
17600
|
this.el = doc.createElement("div");
|
|
17344
17601
|
this.el.className = "vela-popover vela-ui-layer" + (opts.className ? ` ${opts.className}` : "");
|
|
17345
17602
|
this.el.dataset.position = this.ctrl.position;
|
|
@@ -17363,11 +17620,21 @@ ${STATIC_DECLS}
|
|
|
17363
17620
|
return;
|
|
17364
17621
|
}
|
|
17365
17622
|
if (open && open !== this) open.hide();
|
|
17623
|
+
if (this.leaveTimer !== null) {
|
|
17624
|
+
clearTimeout(this.leaveTimer);
|
|
17625
|
+
this.leaveTimer = null;
|
|
17626
|
+
}
|
|
17366
17627
|
ensureUIHost(this.el, this.theme);
|
|
17628
|
+
if (this.fadeMs > 0) {
|
|
17629
|
+
this.el.style.transition = `opacity ${this.fadeMs}ms ease`;
|
|
17630
|
+
this.el.style.opacity = "0";
|
|
17631
|
+
this.el.style.pointerEvents = "";
|
|
17632
|
+
}
|
|
17367
17633
|
this.host.appendChild(this.el);
|
|
17368
17634
|
this.shown = true;
|
|
17369
17635
|
open = this;
|
|
17370
17636
|
this.place();
|
|
17637
|
+
if (this.fadeMs > 0) this.el.style.opacity = "1";
|
|
17371
17638
|
const onOutside = (ev) => {
|
|
17372
17639
|
const t = ev.target;
|
|
17373
17640
|
if (this.el.contains(t) || this.trigger.contains(t)) return;
|
|
@@ -17400,7 +17667,16 @@ ${STATIC_DECLS}
|
|
|
17400
17667
|
this.onOutside = null;
|
|
17401
17668
|
this.onKey = null;
|
|
17402
17669
|
this.onReflow = null;
|
|
17403
|
-
this.
|
|
17670
|
+
if (this.fadeMs > 0) {
|
|
17671
|
+
this.el.style.opacity = "0";
|
|
17672
|
+
this.el.style.pointerEvents = "none";
|
|
17673
|
+
this.leaveTimer = setTimeout(() => {
|
|
17674
|
+
this.leaveTimer = null;
|
|
17675
|
+
this.el.remove();
|
|
17676
|
+
}, this.fadeMs);
|
|
17677
|
+
} else {
|
|
17678
|
+
this.el.remove();
|
|
17679
|
+
}
|
|
17404
17680
|
this.shown = false;
|
|
17405
17681
|
if (open === this) open = null;
|
|
17406
17682
|
this.ctrl.onClose?.();
|
|
@@ -22796,6 +23072,8 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
|
|
|
22796
23072
|
// canvas-painted into the owning indicator's interleave slice
|
|
22797
23073
|
trades: true,
|
|
22798
23074
|
// strategy order-fill markers (arrows + labels + fill-price ticks)
|
|
23075
|
+
timelineMarks: true,
|
|
23076
|
+
// host events on a lane above the time axis (glyphs + detail popup)
|
|
22799
23077
|
inputsUI: true
|
|
22800
23078
|
// reuses the DOM InputsUI
|
|
22801
23079
|
};
|
|
@@ -23068,9 +23346,12 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
|
|
|
23068
23346
|
const gh = asObject(grid.horzLines);
|
|
23069
23347
|
const cross = asObject(p.crosshair);
|
|
23070
23348
|
const ps = asObject(p.priceScale);
|
|
23349
|
+
const anim = asObject(p.animations);
|
|
23071
23350
|
const panes = asObject(p.panes);
|
|
23072
23351
|
const trades = asObject(p.trades);
|
|
23073
23352
|
const ts = asObject(p.timeScale);
|
|
23353
|
+
const marks = asObject(p.marks);
|
|
23354
|
+
const markGroups = asObject(marks.groups);
|
|
23074
23355
|
const candles = asObject(p.candles);
|
|
23075
23356
|
const bars = asObject(p.bars);
|
|
23076
23357
|
const line = asObject(p.line);
|
|
@@ -23117,6 +23398,12 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
|
|
|
23117
23398
|
countdown: isBool(ps.countdown) ? ps.countdown : base.priceScale.countdown,
|
|
23118
23399
|
animateLastPrice: isBool(ps.animateLastPrice) ? ps.animateLastPrice : base.priceScale.animateLastPrice
|
|
23119
23400
|
},
|
|
23401
|
+
animations: {
|
|
23402
|
+
zoom: isBool(anim.zoom) ? anim.zoom : base.animations.zoom,
|
|
23403
|
+
pan: isBool(anim.pan) ? anim.pan : base.animations.pan,
|
|
23404
|
+
autoscale: isBool(anim.autoscale) ? anim.autoscale : base.animations.autoscale,
|
|
23405
|
+
intro: isBool(anim.intro) ? anim.intro : base.animations.intro
|
|
23406
|
+
},
|
|
23120
23407
|
panes: {
|
|
23121
23408
|
separatorColor: isColor(panes.separatorColor) ? panes.separatorColor : base.panes.separatorColor
|
|
23122
23409
|
},
|
|
@@ -23131,6 +23418,15 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
|
|
|
23131
23418
|
timeScale: {
|
|
23132
23419
|
timezone: typeof ts.timezone === "string" && ts.timezone ? ts.timezone : base.timeScale.timezone
|
|
23133
23420
|
},
|
|
23421
|
+
marks: {
|
|
23422
|
+
visible: isBool(marks.visible) ? marks.visible : base.marks.visible,
|
|
23423
|
+
// Additive like `stacking.series`: a patch names only the groups it carries, so a
|
|
23424
|
+
// choice stored for a group the host has not registered yet survives verbatim.
|
|
23425
|
+
groups: {
|
|
23426
|
+
...base.marks.groups,
|
|
23427
|
+
...Object.fromEntries(Object.entries(markGroups).filter(([, v]) => isBool(v)))
|
|
23428
|
+
}
|
|
23429
|
+
},
|
|
23134
23430
|
candles: {
|
|
23135
23431
|
upColor: isColor(candles.upColor) ? candles.upColor : base.candles.upColor,
|
|
23136
23432
|
downColor: isColor(candles.downColor) ? candles.downColor : base.candles.downColor,
|
|
@@ -24649,6 +24945,31 @@ void main() {
|
|
|
24649
24945
|
}
|
|
24650
24946
|
}
|
|
24651
24947
|
};
|
|
24948
|
+
var EaseSetting = class {
|
|
24949
|
+
/** `defaultMs` is what the on/off switch restores when nothing else was configured;
|
|
24950
|
+
* `initialMs` (default: `defaultMs`) is the starting value — 0 for a motion that
|
|
24951
|
+
* ships off. */
|
|
24952
|
+
constructor(defaultMs, initialMs = defaultMs) {
|
|
24953
|
+
this.onMs = defaultMs;
|
|
24954
|
+
this.ms = initialMs;
|
|
24955
|
+
}
|
|
24956
|
+
/** The active time-constant; 0 when off. */
|
|
24957
|
+
get tau() {
|
|
24958
|
+
return this.ms;
|
|
24959
|
+
}
|
|
24960
|
+
get on() {
|
|
24961
|
+
return this.ms > 0;
|
|
24962
|
+
}
|
|
24963
|
+
/** Set the time-constant (0 = off). A non-zero value becomes what `toggle(true)` restores. */
|
|
24964
|
+
set(ms) {
|
|
24965
|
+
this.ms = ms;
|
|
24966
|
+
if (ms > 0) this.onMs = ms;
|
|
24967
|
+
}
|
|
24968
|
+
/** On/off only — the duration stays the last one configured. */
|
|
24969
|
+
toggle(on) {
|
|
24970
|
+
this.ms = on ? this.onMs : 0;
|
|
24971
|
+
}
|
|
24972
|
+
};
|
|
24652
24973
|
function easeToward(current, target, dtMs, tauMs) {
|
|
24653
24974
|
if (tauMs <= 0) return target;
|
|
24654
24975
|
return current + (target - current) * (1 - Math.exp(-dtMs / tauMs));
|
|
@@ -25372,6 +25693,25 @@ void main() {
|
|
|
25372
25693
|
for (let i = 0; i < lines.length; i += 1) ctx.fillText(lines[i], x, firstY + i * step);
|
|
25373
25694
|
}
|
|
25374
25695
|
|
|
25696
|
+
// src/renderers/shared/marks-state.ts
|
|
25697
|
+
function defaultMarksState() {
|
|
25698
|
+
return { visible: true, groups: {} };
|
|
25699
|
+
}
|
|
25700
|
+
function mergeMarksState(base, patch) {
|
|
25701
|
+
if (typeof patch === "boolean") return { visible: patch, groups: { ...base.groups } };
|
|
25702
|
+
const p = patch && typeof patch === "object" ? patch : {};
|
|
25703
|
+
const g = p.groups && typeof p.groups === "object" ? p.groups : {};
|
|
25704
|
+
const groups = { ...base.groups };
|
|
25705
|
+
for (const [id, v] of Object.entries(g)) if (typeof v === "boolean") groups[id] = v;
|
|
25706
|
+
return { visible: typeof p.visible === "boolean" ? p.visible : base.visible, groups };
|
|
25707
|
+
}
|
|
25708
|
+
function markGroupVisible(state, groupId, groups) {
|
|
25709
|
+
if (groupId === void 0) return true;
|
|
25710
|
+
const chosen = state.groups[groupId];
|
|
25711
|
+
if (typeof chosen === "boolean") return chosen;
|
|
25712
|
+
return groups.find((g) => g.id === groupId)?.visible !== false;
|
|
25713
|
+
}
|
|
25714
|
+
|
|
25375
25715
|
// src/renderers/native/core/SceneGraph.ts
|
|
25376
25716
|
var SceneGraph = class {
|
|
25377
25717
|
constructor() {
|
|
@@ -25435,6 +25775,20 @@ void main() {
|
|
|
25435
25775
|
/** Strategy trade-marker display (the `tradeMarkers` feature): master toggle, the
|
|
25436
25776
|
* two text lines, and the palette. Trade markers always paint on the price pane. */
|
|
25437
25777
|
this.tradeMarkers = defaultTradeMarkersState();
|
|
25778
|
+
/** Timeline-mark display (the `marks` feature): the lane's master toggle + per-group visibility. */
|
|
25779
|
+
this.marks = defaultMarksState();
|
|
25780
|
+
/** The host's timeline marks + group definitions (`setTimelineMarks`), painted on the lane above the time axis. */
|
|
25781
|
+
this.timelineMarks = [];
|
|
25782
|
+
this.markGroups = [];
|
|
25783
|
+
/** The mark stack (bar index) fanned out by hover or tap, if any. */
|
|
25784
|
+
this.marksExpandedStack = null;
|
|
25785
|
+
/** The lane glyph (cluster key) under the pointer — it pulses — and when the hover began (frame-clock ms). */
|
|
25786
|
+
this.marksHoverKey = null;
|
|
25787
|
+
this.marksHoverSince = 0;
|
|
25788
|
+
/** The cluster whose popup is open: its glyph paints filled ("active"). */
|
|
25789
|
+
this.marksActiveKey = null;
|
|
25790
|
+
/** A content-less click's brief filled flash — the cluster key and the frame-clock time it ends. */
|
|
25791
|
+
this.marksFlash = null;
|
|
25438
25792
|
/** Renderer-owned shaded time bands (session highlighting), behind grid + data. */
|
|
25439
25793
|
this.highlights = [];
|
|
25440
25794
|
/** Pre/post-market bands pushed by the host (`sessionZones` feature); null ⇒ no sessions. */
|
|
@@ -27283,6 +27637,315 @@ void main() {
|
|
|
27283
27637
|
return out;
|
|
27284
27638
|
}
|
|
27285
27639
|
|
|
27640
|
+
// src/renderers/native/chrome/countdown.ts
|
|
27641
|
+
function countdownText(barOpen, barMs, now) {
|
|
27642
|
+
if (!(barMs > 0)) return null;
|
|
27643
|
+
const remaining = barOpen + barMs - now;
|
|
27644
|
+
if (remaining <= 0) return null;
|
|
27645
|
+
return formatCountdown(remaining);
|
|
27646
|
+
}
|
|
27647
|
+
function formatCountdown(ms) {
|
|
27648
|
+
const total = Math.max(0, Math.ceil(ms / 1e3));
|
|
27649
|
+
const s = total % 60;
|
|
27650
|
+
const m = Math.floor(total / 60) % 60;
|
|
27651
|
+
const h = Math.floor(total / 3600);
|
|
27652
|
+
const pad = (v) => String(v).padStart(2, "0");
|
|
27653
|
+
return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`;
|
|
27654
|
+
}
|
|
27655
|
+
|
|
27656
|
+
// src/renderers/native/chrome/contrast.ts
|
|
27657
|
+
function tagTextColor(bg, over) {
|
|
27658
|
+
const [r, g, b, a] = parseColor(bg);
|
|
27659
|
+
let R = r;
|
|
27660
|
+
let G = g;
|
|
27661
|
+
let B = b;
|
|
27662
|
+
if (a < 1) {
|
|
27663
|
+
const [or2, og, ob] = parseColor(over);
|
|
27664
|
+
R = r * a + or2 * (1 - a);
|
|
27665
|
+
G = g * a + og * (1 - a);
|
|
27666
|
+
B = b * a + ob * (1 - a);
|
|
27667
|
+
}
|
|
27668
|
+
const lin = (c) => c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
27669
|
+
const L = 0.2126 * lin(R) + 0.7152 * lin(G) + 0.0722 * lin(B);
|
|
27670
|
+
return L >= 0.4 ? "#000000" : "#ffffff";
|
|
27671
|
+
}
|
|
27672
|
+
|
|
27673
|
+
// src/renderers/native/chrome/marks/layout.ts
|
|
27674
|
+
var MARK_GLYPH_PX = 16;
|
|
27675
|
+
var MARK_CLUSTER_PX = 20;
|
|
27676
|
+
var MARK_LANE_INSET = 4;
|
|
27677
|
+
var MARK_DECK_STEP = 3;
|
|
27678
|
+
var MARK_FAN_GAP = 4;
|
|
27679
|
+
var MARK_HIT_PAD = 3;
|
|
27680
|
+
var MARK_FAN_HOLD = 8;
|
|
27681
|
+
function snapMarkBar(time, barTimes, intervalMs2) {
|
|
27682
|
+
const n = barTimes.length;
|
|
27683
|
+
if (n === 0 || !(intervalMs2 > 0) || !Number.isFinite(time)) return null;
|
|
27684
|
+
if (time < barTimes[0]) return null;
|
|
27685
|
+
const last2 = barTimes[n - 1];
|
|
27686
|
+
if (time >= last2 + intervalMs2) return n - 1 + Math.floor((time - last2) / intervalMs2);
|
|
27687
|
+
let lo = 0;
|
|
27688
|
+
let hi = n - 1;
|
|
27689
|
+
while (lo < hi) {
|
|
27690
|
+
const mid = lo + hi + 1 >> 1;
|
|
27691
|
+
if (barTimes[mid] <= time) lo = mid;
|
|
27692
|
+
else hi = mid - 1;
|
|
27693
|
+
}
|
|
27694
|
+
if (time < barTimes[lo] + intervalMs2) return lo;
|
|
27695
|
+
return lo + 1;
|
|
27696
|
+
}
|
|
27697
|
+
function clusterMarks(marks, barTimes, intervalMs2, hidden) {
|
|
27698
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
27699
|
+
marks.forEach((m, seq) => {
|
|
27700
|
+
if (m.group !== void 0 && hidden(m.group)) return;
|
|
27701
|
+
const bar = snapMarkBar(m.time, barTimes, intervalMs2);
|
|
27702
|
+
if (bar === null) return;
|
|
27703
|
+
const key = `${bar}|${m.group ?? ""}`;
|
|
27704
|
+
let c = byKey.get(key);
|
|
27705
|
+
if (!c) {
|
|
27706
|
+
c = { key, bar, group: m.group, marks: [], seq: [] };
|
|
27707
|
+
byKey.set(key, c);
|
|
27708
|
+
}
|
|
27709
|
+
c.marks.push(m);
|
|
27710
|
+
c.seq.push(seq);
|
|
27711
|
+
});
|
|
27712
|
+
const out = [];
|
|
27713
|
+
for (const c of byKey.values()) {
|
|
27714
|
+
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);
|
|
27715
|
+
out.push({ key: c.key, bar: c.bar, group: c.group, marks: order.map((o) => o.m) });
|
|
27716
|
+
}
|
|
27717
|
+
return out;
|
|
27718
|
+
}
|
|
27719
|
+
function groupRank(groups, clusters) {
|
|
27720
|
+
const rank = /* @__PURE__ */ new Map();
|
|
27721
|
+
groups.forEach((g, i) => rank.set(g.id, i));
|
|
27722
|
+
for (const c of clusters) {
|
|
27723
|
+
if (c.group !== void 0 && !rank.has(c.group)) rank.set(c.group, rank.size);
|
|
27724
|
+
}
|
|
27725
|
+
return (group) => group === void 0 ? Number.MAX_SAFE_INTEGER : rank.get(group) ?? Number.MAX_SAFE_INTEGER - 1;
|
|
27726
|
+
}
|
|
27727
|
+
function layoutMarkLane(input) {
|
|
27728
|
+
const clusters = clusterMarks(input.marks, input.barTimes, input.intervalMs, input.hidden);
|
|
27729
|
+
const rankOf = groupRank(input.groups, clusters);
|
|
27730
|
+
const byBar = /* @__PURE__ */ new Map();
|
|
27731
|
+
for (const c of clusters) {
|
|
27732
|
+
const list = byBar.get(c.bar);
|
|
27733
|
+
if (list) list.push(c);
|
|
27734
|
+
else byBar.set(c.bar, [c]);
|
|
27735
|
+
}
|
|
27736
|
+
const glyphs = [];
|
|
27737
|
+
const stacks = /* @__PURE__ */ new Map();
|
|
27738
|
+
for (const [bar, list] of byBar) {
|
|
27739
|
+
const x = input.xOf(bar);
|
|
27740
|
+
if (!Number.isFinite(x) || x < -MARK_CLUSTER_PX || x > input.dataW + MARK_CLUSTER_PX) continue;
|
|
27741
|
+
list.sort((a, b) => rankOf(a.group) - rankOf(b.group));
|
|
27742
|
+
const multi = list.length > 1;
|
|
27743
|
+
const expanded = multi && input.expanded === bar;
|
|
27744
|
+
const decked = multi && !expanded;
|
|
27745
|
+
const placed = [];
|
|
27746
|
+
const deckSize = list[0].marks.length > 1 ? MARK_CLUSTER_PX : MARK_GLYPH_PX;
|
|
27747
|
+
let bottom = input.axisY - MARK_LANE_INSET;
|
|
27748
|
+
list.forEach((cluster, depth) => {
|
|
27749
|
+
const size3 = decked ? deckSize : cluster.marks.length > 1 ? MARK_CLUSTER_PX : MARK_GLYPH_PX;
|
|
27750
|
+
let y;
|
|
27751
|
+
if (expanded) {
|
|
27752
|
+
y = bottom - size3 / 2;
|
|
27753
|
+
bottom -= size3 + MARK_FAN_GAP;
|
|
27754
|
+
} else {
|
|
27755
|
+
y = input.axisY - MARK_LANE_INSET - size3 / 2 - depth * MARK_DECK_STEP;
|
|
27756
|
+
}
|
|
27757
|
+
placed.push({ cluster, x, y, size: size3, stack: bar, depth, decked });
|
|
27758
|
+
});
|
|
27759
|
+
for (let i = placed.length - 1; i >= 0; i--) glyphs.push(placed[i]);
|
|
27760
|
+
stacks.set(bar, placed);
|
|
27761
|
+
}
|
|
27762
|
+
return { glyphs, stacks };
|
|
27763
|
+
}
|
|
27764
|
+
function markGlyphAt(layout, x, y) {
|
|
27765
|
+
for (let i = layout.glyphs.length - 1; i >= 0; i--) {
|
|
27766
|
+
const g = layout.glyphs[i];
|
|
27767
|
+
if (g.decked && g.depth !== 0) continue;
|
|
27768
|
+
const r = g.size / 2 + MARK_HIT_PAD;
|
|
27769
|
+
if (Math.abs(x - g.x) <= r && Math.abs(y - g.y) <= r) return g;
|
|
27770
|
+
}
|
|
27771
|
+
return null;
|
|
27772
|
+
}
|
|
27773
|
+
function markStackAt(layout, x, y) {
|
|
27774
|
+
for (const [bar, placed] of layout.stacks) {
|
|
27775
|
+
for (const g of placed) {
|
|
27776
|
+
const r = g.size / 2 + MARK_HIT_PAD;
|
|
27777
|
+
if (Math.abs(x - g.x) <= r && Math.abs(y - g.y) <= r) return bar;
|
|
27778
|
+
}
|
|
27779
|
+
if (placed.length > 1 && !placed[0].decked) {
|
|
27780
|
+
const top = placed[placed.length - 1];
|
|
27781
|
+
const base = placed[0];
|
|
27782
|
+
const r = Math.max(top.size, base.size) / 2 + MARK_HIT_PAD;
|
|
27783
|
+
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;
|
|
27784
|
+
}
|
|
27785
|
+
}
|
|
27786
|
+
return null;
|
|
27787
|
+
}
|
|
27788
|
+
function clusterTooltip(cluster, groups) {
|
|
27789
|
+
const first2 = cluster.marks[0];
|
|
27790
|
+
if (!first2) return null;
|
|
27791
|
+
if (cluster.marks.length === 1) return first2.tooltip ?? first2.title ?? null;
|
|
27792
|
+
const label = cluster.group !== void 0 ? markGroupLabel(cluster.group, groups) : first2.title ?? first2.tooltip ?? "Marks";
|
|
27793
|
+
return `${label} \xB7 ${cluster.marks.length}`;
|
|
27794
|
+
}
|
|
27795
|
+
function markGroupLabel(groupId, groups) {
|
|
27796
|
+
const def = groups.find((g) => g.id === groupId);
|
|
27797
|
+
if (def) return def.label;
|
|
27798
|
+
return groupId.charAt(0).toUpperCase() + groupId.slice(1);
|
|
27799
|
+
}
|
|
27800
|
+
function effectiveMarkGroups(marks, groups) {
|
|
27801
|
+
const out = groups.map((g) => ({ ...g }));
|
|
27802
|
+
const seen = new Set(out.map((g) => g.id));
|
|
27803
|
+
for (const m of marks) {
|
|
27804
|
+
if (m.group === void 0 || seen.has(m.group)) continue;
|
|
27805
|
+
seen.add(m.group);
|
|
27806
|
+
out.push({ id: m.group, label: markGroupLabel(m.group, groups) });
|
|
27807
|
+
}
|
|
27808
|
+
return out;
|
|
27809
|
+
}
|
|
27810
|
+
|
|
27811
|
+
// src/renderers/native/chrome/marks/paint.ts
|
|
27812
|
+
var MARK_PULSE_MS = 360;
|
|
27813
|
+
var MARK_PULSE_AMPLITUDE = 0.1;
|
|
27814
|
+
var ACTIVE_INK = "#ffffff";
|
|
27815
|
+
function pulseScale(elapsedMs) {
|
|
27816
|
+
if (!(elapsedMs > 0) || elapsedMs >= MARK_PULSE_MS) return 1;
|
|
27817
|
+
return 1 + MARK_PULSE_AMPLITUDE * Math.sin(Math.PI * elapsedMs / MARK_PULSE_MS);
|
|
27818
|
+
}
|
|
27819
|
+
function paintMarkLane(ctx, layout, deps) {
|
|
27820
|
+
if (layout.glyphs.length === 0) return;
|
|
27821
|
+
ctx.save();
|
|
27822
|
+
ctx.setLineDash([]);
|
|
27823
|
+
ctx.textAlign = "center";
|
|
27824
|
+
ctx.textBaseline = "middle";
|
|
27825
|
+
for (const g of layout.glyphs) {
|
|
27826
|
+
const mark = g.cluster.marks[0];
|
|
27827
|
+
if (!mark) continue;
|
|
27828
|
+
const key = g.cluster.key;
|
|
27829
|
+
const color = mark.glyph.color;
|
|
27830
|
+
const active = key === deps.activeKey || key === deps.flashKey;
|
|
27831
|
+
const size3 = g.size * (key === deps.hoverKey ? pulseScale(deps.nowMs - deps.hoverSince) : 1);
|
|
27832
|
+
if (g.depth === 0) {
|
|
27833
|
+
const sx = Math.round(g.x) + 0.5;
|
|
27834
|
+
ctx.lineWidth = 1;
|
|
27835
|
+
ctx.strokeStyle = deps.stemColor;
|
|
27836
|
+
ctx.beginPath();
|
|
27837
|
+
ctx.moveTo(sx, g.y + g.size / 2);
|
|
27838
|
+
ctx.lineTo(sx, deps.axisY);
|
|
27839
|
+
ctx.stroke();
|
|
27840
|
+
}
|
|
27841
|
+
const shape = mark.glyph.shape ?? "circle";
|
|
27842
|
+
const center = traceShape(ctx, shape, g.x, g.y, size3);
|
|
27843
|
+
ctx.lineWidth = 4;
|
|
27844
|
+
ctx.strokeStyle = deps.background;
|
|
27845
|
+
ctx.stroke();
|
|
27846
|
+
ctx.fillStyle = active ? color : deps.background;
|
|
27847
|
+
ctx.fill();
|
|
27848
|
+
ctx.lineWidth = 1.5;
|
|
27849
|
+
ctx.strokeStyle = color;
|
|
27850
|
+
ctx.stroke();
|
|
27851
|
+
const ink = active ? ACTIVE_INK : color;
|
|
27852
|
+
const symbolPx = Math.round(size3 * 0.62);
|
|
27853
|
+
if (mark.glyph.icon) {
|
|
27854
|
+
const img = deps.icons.get(mark.glyph.icon, ink, symbolPx, deps.dpr);
|
|
27855
|
+
if (img) ctx.drawImage(img, center.x - symbolPx / 2, center.y - symbolPx / 2, symbolPx, symbolPx);
|
|
27856
|
+
} else if (mark.glyph.letter) {
|
|
27857
|
+
ctx.fillStyle = ink;
|
|
27858
|
+
ctx.font = `600 ${Math.round(size3 * 0.58)}px ${deps.fontFamily}`;
|
|
27859
|
+
ctx.fillText(mark.glyph.letter.slice(0, 2), center.x, center.y + 0.5);
|
|
27860
|
+
}
|
|
27861
|
+
}
|
|
27862
|
+
ctx.restore();
|
|
27863
|
+
}
|
|
27864
|
+
function traceShape(ctx, shape, x, y, size3) {
|
|
27865
|
+
const r = size3 / 2;
|
|
27866
|
+
ctx.beginPath();
|
|
27867
|
+
switch (shape) {
|
|
27868
|
+
case "square": {
|
|
27869
|
+
const c = Math.min(3, r / 2);
|
|
27870
|
+
roundedRect(ctx, x - r, y - r, size3, size3, c);
|
|
27871
|
+
return { x, y };
|
|
27872
|
+
}
|
|
27873
|
+
case "diamond":
|
|
27874
|
+
ctx.moveTo(x, y - r);
|
|
27875
|
+
ctx.lineTo(x + r, y);
|
|
27876
|
+
ctx.lineTo(x, y + r);
|
|
27877
|
+
ctx.lineTo(x - r, y);
|
|
27878
|
+
ctx.closePath();
|
|
27879
|
+
return { x, y };
|
|
27880
|
+
case "pin": {
|
|
27881
|
+
const hr = r * 0.82;
|
|
27882
|
+
const hy = y - r + hr;
|
|
27883
|
+
ctx.arc(x, hy, hr, Math.PI * 0.75, Math.PI * 0.25, false);
|
|
27884
|
+
ctx.lineTo(x, y + r);
|
|
27885
|
+
ctx.closePath();
|
|
27886
|
+
return { x, y: hy };
|
|
27887
|
+
}
|
|
27888
|
+
case "circle":
|
|
27889
|
+
default:
|
|
27890
|
+
ctx.arc(x, y, r, 0, Math.PI * 2);
|
|
27891
|
+
return { x, y };
|
|
27892
|
+
}
|
|
27893
|
+
}
|
|
27894
|
+
function roundedRect(ctx, x, y, w, h, radius) {
|
|
27895
|
+
ctx.moveTo(x + radius, y);
|
|
27896
|
+
ctx.lineTo(x + w - radius, y);
|
|
27897
|
+
ctx.quadraticCurveTo(x + w, y, x + w, y + radius);
|
|
27898
|
+
ctx.lineTo(x + w, y + h - radius);
|
|
27899
|
+
ctx.quadraticCurveTo(x + w, y + h, x + w - radius, y + h);
|
|
27900
|
+
ctx.lineTo(x + radius, y + h);
|
|
27901
|
+
ctx.quadraticCurveTo(x, y + h, x, y + h - radius);
|
|
27902
|
+
ctx.lineTo(x, y + radius);
|
|
27903
|
+
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
27904
|
+
ctx.closePath();
|
|
27905
|
+
}
|
|
27906
|
+
var MarkIconRaster = class {
|
|
27907
|
+
constructor(onReady) {
|
|
27908
|
+
this.onReady = onReady;
|
|
27909
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
27910
|
+
}
|
|
27911
|
+
get(icon2, ink, px, dpr) {
|
|
27912
|
+
const key = `${icon2}|${ink}|${px}|${dpr}`;
|
|
27913
|
+
if (this.cache.has(key)) {
|
|
27914
|
+
const img2 = this.cache.get(key);
|
|
27915
|
+
return img2 && img2.complete && img2.naturalWidth > 0 ? img2 : null;
|
|
27916
|
+
}
|
|
27917
|
+
const markup = iconMarkup(icon2);
|
|
27918
|
+
if (!markup || typeof document === "undefined" || typeof Image === "undefined" || typeof XMLSerializer === "undefined") {
|
|
27919
|
+
this.cache.set(key, null);
|
|
27920
|
+
return null;
|
|
27921
|
+
}
|
|
27922
|
+
const svg = standaloneSvg(markup, ink, Math.max(1, Math.ceil(px * dpr)));
|
|
27923
|
+
if (!svg) {
|
|
27924
|
+
this.cache.set(key, null);
|
|
27925
|
+
return null;
|
|
27926
|
+
}
|
|
27927
|
+
const img = new Image();
|
|
27928
|
+
img.onload = () => this.onReady();
|
|
27929
|
+
img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
|
|
27930
|
+
this.cache.set(key, img);
|
|
27931
|
+
return null;
|
|
27932
|
+
}
|
|
27933
|
+
clear() {
|
|
27934
|
+
this.cache.clear();
|
|
27935
|
+
}
|
|
27936
|
+
};
|
|
27937
|
+
function standaloneSvg(markup, ink, px) {
|
|
27938
|
+
const tpl = document.createElement("template");
|
|
27939
|
+
tpl.innerHTML = markup;
|
|
27940
|
+
const svg = tpl.content.firstElementChild;
|
|
27941
|
+
if (!svg || svg.tagName.toLowerCase() !== "svg") return null;
|
|
27942
|
+
svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
|
27943
|
+
svg.setAttribute("width", String(px));
|
|
27944
|
+
svg.setAttribute("height", String(px));
|
|
27945
|
+
svg.setAttribute("color", ink);
|
|
27946
|
+
return new XMLSerializer().serializeToString(svg);
|
|
27947
|
+
}
|
|
27948
|
+
|
|
27286
27949
|
// src/renderers/native/chrome/ChromeRenderer.ts
|
|
27287
27950
|
var ChromeRenderer = class {
|
|
27288
27951
|
constructor() {
|
|
@@ -27292,11 +27955,40 @@ void main() {
|
|
|
27292
27955
|
this.axisTextColor = DARK_THEME.textColor;
|
|
27293
27956
|
// Shared Pine-drawing renderer, used here for autoscale geometry only; widthCache persists.
|
|
27294
27957
|
this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
|
|
27958
|
+
/** The timeline-mark lane as laid out by the last frame — what hover/click hit-test against. */
|
|
27959
|
+
this.markLayout = { glyphs: [], stacks: /* @__PURE__ */ new Map() };
|
|
27960
|
+
/** Registry icons rasterized for the lane; the owner is asked for a chrome repaint when one lands. */
|
|
27961
|
+
this.markIcons = new MarkIconRaster(() => this.onMarkIconReady?.());
|
|
27962
|
+
this.onMarkIconReady = null;
|
|
27963
|
+
/** Bar open times of the current series, rebuilt only when the array or its length changes (a live tick keeps both). */
|
|
27964
|
+
this.barTimesSrc = null;
|
|
27965
|
+
this.barTimesCache = [];
|
|
27295
27966
|
}
|
|
27296
27967
|
mount(canvas) {
|
|
27297
27968
|
this.canvas = canvas;
|
|
27298
27969
|
this.ctx = canvas.getContext("2d");
|
|
27299
27970
|
}
|
|
27971
|
+
/** Where to ask for a chrome repaint when a lane icon finishes rasterizing. */
|
|
27972
|
+
setMarkIconReady(cb) {
|
|
27973
|
+
this.onMarkIconReady = cb;
|
|
27974
|
+
}
|
|
27975
|
+
/** The interactive mark glyph under a plot point (last frame's layout), or null. */
|
|
27976
|
+
markGlyphAt(x, y) {
|
|
27977
|
+
return markGlyphAt(this.markLayout, x, y);
|
|
27978
|
+
}
|
|
27979
|
+
/** The mark stack (bar index) whose glyphs — or the gaps of its fan — cover a plot point. */
|
|
27980
|
+
markStackAt(x, y) {
|
|
27981
|
+
return markStackAt(this.markLayout, x, y);
|
|
27982
|
+
}
|
|
27983
|
+
/** A glyph of the last frame by its cluster key — how an open popup follows its anchor. */
|
|
27984
|
+
markGlyphByKey(key) {
|
|
27985
|
+
return this.markLayout.glyphs.find((g) => g.cluster.key === key) ?? null;
|
|
27986
|
+
}
|
|
27987
|
+
/** Hover text of the mark glyph under a plot point, or null. */
|
|
27988
|
+
markTooltipAt(x, y, groups) {
|
|
27989
|
+
const g = this.markGlyphAt(x, y);
|
|
27990
|
+
return g ? clusterTooltip(g.cluster, groups) : null;
|
|
27991
|
+
}
|
|
27300
27992
|
/** Wire the drawing coordinate resolvers + theme (call once per frame before use). */
|
|
27301
27993
|
prepare(scene, coords, theme) {
|
|
27302
27994
|
this.drawScene.setDeps({
|
|
@@ -27345,6 +28037,7 @@ void main() {
|
|
|
27345
28037
|
const panes = scene.orderedPanes();
|
|
27346
28038
|
if (coords.barCount === 0) {
|
|
27347
28039
|
this.drawPaneSeparators(ctx, scene, theme, fullW, panes);
|
|
28040
|
+
this.markLayout = { glyphs: [], stacks: /* @__PURE__ */ new Map() };
|
|
27348
28041
|
return;
|
|
27349
28042
|
}
|
|
27350
28043
|
const pricePane = panes.find((p) => p.kind === "price") ?? null;
|
|
@@ -27358,6 +28051,46 @@ void main() {
|
|
|
27358
28051
|
this.drawPaneSeparators(ctx, scene, theme, fullW, panes);
|
|
27359
28052
|
this.drawPriceLineAndCountdown(ctx, scene, coords, theme, dataW, pricePane);
|
|
27360
28053
|
this.drawTimeAxis(ctx, scene, coords, theme, dataW, dataH, fullH);
|
|
28054
|
+
this.drawMarkLane(ctx, scene, coords, theme, dataW, dataH);
|
|
28055
|
+
}
|
|
28056
|
+
/** The timeline-mark lane — after the axis, so the tokens read over the plot's bottom edge. */
|
|
28057
|
+
drawMarkLane(ctx, scene, coords, theme, dataW, dataH) {
|
|
28058
|
+
if (!scene.marks.visible || scene.timelineMarks.length === 0) {
|
|
28059
|
+
this.markLayout = { glyphs: [], stacks: /* @__PURE__ */ new Map() };
|
|
28060
|
+
return;
|
|
28061
|
+
}
|
|
28062
|
+
this.markLayout = layoutMarkLane({
|
|
28063
|
+
marks: scene.timelineMarks,
|
|
28064
|
+
groups: scene.markGroups,
|
|
28065
|
+
hidden: (groupId) => !markGroupVisible(scene.marks, groupId, scene.markGroups),
|
|
28066
|
+
barTimes: this.barTimes(scene),
|
|
28067
|
+
intervalMs: coords.barInterval,
|
|
28068
|
+
xOf: (bar) => coords.logicalToX(bar),
|
|
28069
|
+
axisY: dataH,
|
|
28070
|
+
dataW,
|
|
28071
|
+
expanded: scene.marksExpandedStack
|
|
28072
|
+
});
|
|
28073
|
+
const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
28074
|
+
paintMarkLane(ctx, this.markLayout, {
|
|
28075
|
+
axisY: dataH,
|
|
28076
|
+
background: theme.background,
|
|
28077
|
+
stemColor: scene.style.borderColor ?? theme.borderColor,
|
|
28078
|
+
fontFamily: theme.fontFamily,
|
|
28079
|
+
dpr: coords.dpr,
|
|
28080
|
+
icons: this.markIcons,
|
|
28081
|
+
hoverKey: scene.marksHoverKey,
|
|
28082
|
+
hoverSince: scene.marksHoverSince,
|
|
28083
|
+
activeKey: scene.marksActiveKey,
|
|
28084
|
+
flashKey: scene.marksFlash && scene.marksFlash.until > nowMs ? scene.marksFlash.key : null,
|
|
28085
|
+
nowMs
|
|
28086
|
+
});
|
|
28087
|
+
}
|
|
28088
|
+
barTimes(scene) {
|
|
28089
|
+
if (this.barTimesSrc !== scene.bars || this.barTimesCache.length !== scene.bars.length) {
|
|
28090
|
+
this.barTimesSrc = scene.bars;
|
|
28091
|
+
this.barTimesCache = scene.bars.map((b) => b.time);
|
|
28092
|
+
}
|
|
28093
|
+
return this.barTimesCache;
|
|
27361
28094
|
}
|
|
27362
28095
|
destroy() {
|
|
27363
28096
|
this.canvas = null;
|
|
@@ -27473,7 +28206,8 @@ void main() {
|
|
|
27473
28206
|
* - the countdown-to-bar-close chip (`showCountdown`).
|
|
27474
28207
|
* When the label and countdown are both on they merge into one stacked block (countdown
|
|
27475
28208
|
* under the label, text flushed left); a lone label or countdown is centered on the
|
|
27476
|
-
* price level with centered text. The countdown
|
|
28209
|
+
* price level with centered text. The countdown repaints on the renderer's second pulse
|
|
28210
|
+
* and disappears once the bar has closed, until the next bar arrives.
|
|
27477
28211
|
*/
|
|
27478
28212
|
drawPriceLineAndCountdown(ctx, scene, coords, theme, dataW, pricePane) {
|
|
27479
28213
|
const n = scene.bars.length;
|
|
@@ -27493,17 +28227,16 @@ void main() {
|
|
|
27493
28227
|
ctx.stroke();
|
|
27494
28228
|
setDash2(ctx, "solid");
|
|
27495
28229
|
}
|
|
27496
|
-
const
|
|
27497
|
-
const showCountdown =
|
|
28230
|
+
const cdText = scene.showCountdown ? countdownText(last2.time, coords.barInterval, Date.now()) : null;
|
|
28231
|
+
const showCountdown = cdText !== null;
|
|
27498
28232
|
const showLabel = scene.showPriceLabel;
|
|
27499
28233
|
if (!showLabel && !showCountdown) return;
|
|
27500
28234
|
const priceText = formatAxisValue(pricePane.scale, pricePane.bounds.height, last2.close, percentScaleFor(scene, pricePane), scene.priceMintick);
|
|
27501
|
-
const cdText = showCountdown ? formatCountdown(last2.time + interval - Date.now()) : "";
|
|
27502
28235
|
const PAD = 8;
|
|
27503
28236
|
const x = dataW + 1;
|
|
27504
28237
|
const textColor = tagTextColor(color, theme.background);
|
|
27505
28238
|
ctx.textBaseline = "middle";
|
|
27506
|
-
if (showLabel &&
|
|
28239
|
+
if (showLabel && cdText !== null) {
|
|
27507
28240
|
const w2 = Math.max(ctx.measureText(priceText).width, ctx.measureText(cdText).width) + PAD;
|
|
27508
28241
|
const top = y - 8;
|
|
27509
28242
|
const tx = x + PAD / 2;
|
|
@@ -27516,7 +28249,7 @@ void main() {
|
|
|
27516
28249
|
ctx.textAlign = "start";
|
|
27517
28250
|
return;
|
|
27518
28251
|
}
|
|
27519
|
-
const text = showLabel ? priceText : cdText;
|
|
28252
|
+
const text = showLabel ? priceText : cdText ?? "";
|
|
27520
28253
|
const w = ctx.measureText(text).width + PAD;
|
|
27521
28254
|
ctx.fillStyle = color;
|
|
27522
28255
|
ctx.fillRect(x, y - 8, w, 16);
|
|
@@ -27587,29 +28320,6 @@ void main() {
|
|
|
27587
28320
|
else if (style === "dotted") ctx.setLineDash([2, 3]);
|
|
27588
28321
|
else ctx.setLineDash([]);
|
|
27589
28322
|
}
|
|
27590
|
-
function tagTextColor(bg, over) {
|
|
27591
|
-
const [r, g, b, a] = parseColor(bg);
|
|
27592
|
-
let R = r;
|
|
27593
|
-
let G = g;
|
|
27594
|
-
let B = b;
|
|
27595
|
-
if (a < 1) {
|
|
27596
|
-
const [or2, og, ob] = parseColor(over);
|
|
27597
|
-
R = r * a + or2 * (1 - a);
|
|
27598
|
-
G = g * a + og * (1 - a);
|
|
27599
|
-
B = b * a + ob * (1 - a);
|
|
27600
|
-
}
|
|
27601
|
-
const lin = (c) => c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
27602
|
-
const L = 0.2126 * lin(R) + 0.7152 * lin(G) + 0.0722 * lin(B);
|
|
27603
|
-
return L >= 0.4 ? "#000000" : "#ffffff";
|
|
27604
|
-
}
|
|
27605
|
-
function formatCountdown(ms) {
|
|
27606
|
-
const total = Math.max(0, Math.floor(ms / 1e3));
|
|
27607
|
-
const s = total % 60;
|
|
27608
|
-
const m = Math.floor(total / 60) % 60;
|
|
27609
|
-
const h = Math.floor(total / 3600);
|
|
27610
|
-
const pad = (v) => String(v).padStart(2, "0");
|
|
27611
|
-
return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`;
|
|
27612
|
-
}
|
|
27613
28323
|
|
|
27614
28324
|
// src/renderers/native/chrome/LabelTooltip.ts
|
|
27615
28325
|
var HOVER_DELAY_MS = 350;
|
|
@@ -27892,6 +28602,11 @@ void main() {
|
|
|
27892
28602
|
function settingsIdSlug(label) {
|
|
27893
28603
|
return label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
27894
28604
|
}
|
|
28605
|
+
var MARKS_SETTINGS_ID = "events";
|
|
28606
|
+
var MARKS_GROUPS_SETTINGS_ID = "events.groups";
|
|
28607
|
+
function markGroupSettingsId(groupId) {
|
|
28608
|
+
return `${MARKS_GROUPS_SETTINGS_ID}.${settingsIdSlug(groupId)}`;
|
|
28609
|
+
}
|
|
27895
28610
|
function settingsIdHidden(id, hidden) {
|
|
27896
28611
|
if (hidden.size === 0) return false;
|
|
27897
28612
|
let path = id;
|
|
@@ -27985,7 +28700,11 @@ void main() {
|
|
|
27985
28700
|
"symbol.style.baseline.base-level",
|
|
27986
28701
|
"symbol.style.baseline.width",
|
|
27987
28702
|
"symbol.animation",
|
|
28703
|
+
"symbol.animation.zoom",
|
|
28704
|
+
"symbol.animation.pan",
|
|
28705
|
+
"symbol.animation.autoscale",
|
|
27988
28706
|
"symbol.animation.price-changes",
|
|
28707
|
+
"symbol.animation.intro",
|
|
27989
28708
|
"symbol.timezone",
|
|
27990
28709
|
"scales",
|
|
27991
28710
|
"scales.price-scale",
|
|
@@ -28011,8 +28730,13 @@ void main() {
|
|
|
28011
28730
|
"canvas.grid.horizontal",
|
|
28012
28731
|
"canvas.theme"
|
|
28013
28732
|
];
|
|
28014
|
-
function settingsIdCatalog(hostSections) {
|
|
28733
|
+
function settingsIdCatalog(hostSections, markGroups = []) {
|
|
28015
28734
|
const ids = new Set(BUILTIN_SETTINGS_IDS);
|
|
28735
|
+
if (markGroups.length > 0) {
|
|
28736
|
+
ids.add(MARKS_SETTINGS_ID);
|
|
28737
|
+
ids.add(MARKS_GROUPS_SETTINGS_ID);
|
|
28738
|
+
for (const g of markGroups) ids.add(markGroupSettingsId(g.id));
|
|
28739
|
+
}
|
|
28016
28740
|
for (const def of chartTypes()) {
|
|
28017
28741
|
if (hasOwnCandlePaint(def.id)) {
|
|
28018
28742
|
const style = `symbol.style.${def.id}`;
|
|
@@ -28052,7 +28776,7 @@ void main() {
|
|
|
28052
28776
|
return chartType(id)?.label ?? BUILTIN_STYLE_LABELS[id] ?? id;
|
|
28053
28777
|
}
|
|
28054
28778
|
var SD_STYLE_ID = "vela-settings-controls";
|
|
28055
|
-
var SD_STYLE_REV = "
|
|
28779
|
+
var SD_STYLE_REV = "6";
|
|
28056
28780
|
var SETTINGS_BORDER = "var(--vela-border)";
|
|
28057
28781
|
function ensureControlStyles() {
|
|
28058
28782
|
if (typeof document === "undefined") return;
|
|
@@ -28128,7 +28852,15 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
28128
28852
|
.vela-sd-mobile .vela-select-trigger,.vela-sd-mobile .vela-num input,.vela-sd-mobile .vela-width-field{height:34px;}
|
|
28129
28853
|
.vela-sd-mobile .vela-sd-close{width:40px;height:40px;}
|
|
28130
28854
|
.vela-sd-mobile .vela-sd-btn{height:38px;}
|
|
28131
|
-
.vela-sd-mobile .vela-sd-row span,.vela-sd-mobile .vela-sd-bool span,.vela-sd-mobile .vela-field-label{white-space:normal !important;}
|
|
28855
|
+
.vela-sd-mobile .vela-sd-row span,.vela-sd-mobile .vela-sd-bool span,.vela-sd-mobile .vela-field-label{white-space:normal !important;}
|
|
28856
|
+
/* The wrap rule above is for ROW LABELS only: a select's closed value must keep its
|
|
28857
|
+
single-line ellipsis, or a long option wraps to several lines inside the 34px
|
|
28858
|
+
trigger and spills over the rows around it. Three classes so it outranks the
|
|
28859
|
+
two-classes-plus-element selector above. The kit's fixed 100px column is a desktop
|
|
28860
|
+
alignment device; on mobile the trigger hugs its value instead (the grid's control
|
|
28861
|
+
column is max-content), capped so a long option still ellipsizes before the label. */
|
|
28862
|
+
.vela-sd-mobile .vela-select-trigger .vela-select-label{white-space:nowrap !important;}
|
|
28863
|
+
.vela-sd-mobile .vela-select:not([data-fill]){width:auto;min-width:100px;max-width:min(220px,55vw);}`;
|
|
28132
28864
|
if (!existing) document.head.appendChild(st);
|
|
28133
28865
|
}
|
|
28134
28866
|
var SettingsDialog = class {
|
|
@@ -28143,6 +28875,9 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
28143
28875
|
this.config = null;
|
|
28144
28876
|
this.syncTypeTabs = null;
|
|
28145
28877
|
this.hostSections = [];
|
|
28878
|
+
/** The timeline-mark groups (defined + named by marks) — one checkbox each on the Events tab. */
|
|
28879
|
+
this.markGroups = [];
|
|
28880
|
+
this.markGroupVisible = () => true;
|
|
28146
28881
|
/** The Canvas → Theme row: current app theme + where a pick is raised. The row is a
|
|
28147
28882
|
* host callback, NOT a config patch — the app theme stays out of the persisted
|
|
28148
28883
|
* `ChartConfig`, so exported templates never carry it. */
|
|
@@ -28167,6 +28902,11 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
28167
28902
|
setHostSections(sections) {
|
|
28168
28903
|
this.hostSections = sections;
|
|
28169
28904
|
}
|
|
28905
|
+
/** The timeline-mark groups and their current visibility — the Events tab's rows on next open. */
|
|
28906
|
+
setMarkGroups(groups, visible) {
|
|
28907
|
+
this.markGroups = groups;
|
|
28908
|
+
this.markGroupVisible = visible;
|
|
28909
|
+
}
|
|
28170
28910
|
/** Replace the visibility policy — an open dialog rebuilds in place to honor it. */
|
|
28171
28911
|
setHiddenSettings(ids) {
|
|
28172
28912
|
const next2 = new Set(ids);
|
|
@@ -28340,12 +29080,36 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
28340
29080
|
}
|
|
28341
29081
|
showActive(config.series.style);
|
|
28342
29082
|
body.append(sid(this.sectionTitle("Animation"), "symbol.animation"));
|
|
29083
|
+
body.append(sid(this.boolRow(
|
|
29084
|
+
"Animate zoom",
|
|
29085
|
+
config.animations.zoom,
|
|
29086
|
+
(v) => this.emit({ animations: { zoom: v } }),
|
|
29087
|
+
this.hint("Glide the chart to each zoom step instead of jumping.")
|
|
29088
|
+
), "symbol.animation.zoom"));
|
|
29089
|
+
body.append(sid(this.boolRow(
|
|
29090
|
+
"Pan momentum",
|
|
29091
|
+
config.animations.pan,
|
|
29092
|
+
(v) => this.emit({ animations: { pan: v } }),
|
|
29093
|
+
this.hint("Keep gliding briefly after a drag release, and ease scroll-to-latest and keyboard pans.")
|
|
29094
|
+
), "symbol.animation.pan"));
|
|
29095
|
+
body.append(sid(this.boolRow(
|
|
29096
|
+
"Animate price scale",
|
|
29097
|
+
config.animations.autoscale,
|
|
29098
|
+
(v) => this.emit({ animations: { autoscale: v } }),
|
|
29099
|
+
this.hint("Glide the price scale to its new range while zooming or panning.")
|
|
29100
|
+
), "symbol.animation.autoscale"));
|
|
28343
29101
|
body.append(sid(this.boolRow(
|
|
28344
29102
|
"Animate price changes",
|
|
28345
29103
|
config.priceScale.animateLastPrice,
|
|
28346
29104
|
(v) => this.emit({ priceScale: { animateLastPrice: v } }),
|
|
28347
29105
|
this.hint("Glide the live bar to each new price instead of snapping.")
|
|
28348
29106
|
), "symbol.animation.price-changes"));
|
|
29107
|
+
body.append(sid(this.boolRow(
|
|
29108
|
+
"Reveal on load",
|
|
29109
|
+
config.animations.intro,
|
|
29110
|
+
(v) => this.emit({ animations: { intro: v } }),
|
|
29111
|
+
this.hint("Draw the candles in when a chart first loads. Takes effect on the next load.")
|
|
29112
|
+
), "symbol.animation.intro"));
|
|
28349
29113
|
body.append(sid(this.sectionTitle("Time zone"), "symbol.timezone"));
|
|
28350
29114
|
body.append(sid(this.selectRowLabeled("Time zone", normalizeTimezone(config.timeScale.timezone), timezoneOptions(config.timeScale.timezone), (v) => this.emit({ timeScale: { timezone: v } })), "symbol.timezone"));
|
|
28351
29115
|
const renderHostSections = (placement) => {
|
|
@@ -28415,6 +29179,13 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
28415
29179
|
body.append(sid(this.sectionTitle("Theme"), "canvas.theme"));
|
|
28416
29180
|
body.append(sid(this.selectRow("Color theme", tc.current === "dark" ? "Dark" : "Light", ["Dark", "Light"], (v) => tc.onSelect(v === "Dark" ? "dark" : "light")), "canvas.theme"));
|
|
28417
29181
|
}
|
|
29182
|
+
if (this.markGroups.length > 0) {
|
|
29183
|
+
body.append(sid(this.section("Events"), MARKS_SETTINGS_ID));
|
|
29184
|
+
body.append(sid(this.sectionTitle("Visible events"), MARKS_GROUPS_SETTINGS_ID));
|
|
29185
|
+
for (const g of this.markGroups) {
|
|
29186
|
+
body.append(sid(this.boolRow(g.label, this.markGroupVisible(g.id), (v) => this.emit({ marks: { groups: { [g.id]: v } } })), markGroupSettingsId(g.id)));
|
|
29187
|
+
}
|
|
29188
|
+
}
|
|
28418
29189
|
renderChartTypeSections("end");
|
|
28419
29190
|
renderHostSections("end");
|
|
28420
29191
|
if (this.hiddenSettings.size > 0) {
|
|
@@ -34619,6 +35390,383 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
34619
35390
|
return { ...scale, min: scale.min - belowPx * perPx, max: scale.max + abovePx * perPx };
|
|
34620
35391
|
}
|
|
34621
35392
|
|
|
35393
|
+
// src/ui/sanitize-html.ts
|
|
35394
|
+
var ALLOWED_TAGS = /* @__PURE__ */ new Set([
|
|
35395
|
+
"a",
|
|
35396
|
+
"abbr",
|
|
35397
|
+
"b",
|
|
35398
|
+
"blockquote",
|
|
35399
|
+
"br",
|
|
35400
|
+
"code",
|
|
35401
|
+
"dd",
|
|
35402
|
+
"del",
|
|
35403
|
+
"div",
|
|
35404
|
+
"dl",
|
|
35405
|
+
"dt",
|
|
35406
|
+
"em",
|
|
35407
|
+
"h1",
|
|
35408
|
+
"h2",
|
|
35409
|
+
"h3",
|
|
35410
|
+
"h4",
|
|
35411
|
+
"h5",
|
|
35412
|
+
"h6",
|
|
35413
|
+
"hr",
|
|
35414
|
+
"i",
|
|
35415
|
+
"img",
|
|
35416
|
+
"ins",
|
|
35417
|
+
"kbd",
|
|
35418
|
+
"li",
|
|
35419
|
+
"mark",
|
|
35420
|
+
"ol",
|
|
35421
|
+
"p",
|
|
35422
|
+
"pre",
|
|
35423
|
+
"q",
|
|
35424
|
+
"s",
|
|
35425
|
+
"small",
|
|
35426
|
+
"span",
|
|
35427
|
+
"strong",
|
|
35428
|
+
"sub",
|
|
35429
|
+
"sup",
|
|
35430
|
+
"table",
|
|
35431
|
+
"tbody",
|
|
35432
|
+
"td",
|
|
35433
|
+
"tfoot",
|
|
35434
|
+
"th",
|
|
35435
|
+
"thead",
|
|
35436
|
+
"tr",
|
|
35437
|
+
"u",
|
|
35438
|
+
"ul"
|
|
35439
|
+
]);
|
|
35440
|
+
var DROPPED_TAGS = /* @__PURE__ */ new Set([
|
|
35441
|
+
"script",
|
|
35442
|
+
"style",
|
|
35443
|
+
"iframe",
|
|
35444
|
+
"frame",
|
|
35445
|
+
"frameset",
|
|
35446
|
+
"object",
|
|
35447
|
+
"embed",
|
|
35448
|
+
"applet",
|
|
35449
|
+
"form",
|
|
35450
|
+
"input",
|
|
35451
|
+
"textarea",
|
|
35452
|
+
"button",
|
|
35453
|
+
"select",
|
|
35454
|
+
"option",
|
|
35455
|
+
"link",
|
|
35456
|
+
"meta",
|
|
35457
|
+
"base",
|
|
35458
|
+
"svg",
|
|
35459
|
+
"math",
|
|
35460
|
+
"template",
|
|
35461
|
+
"noscript",
|
|
35462
|
+
"audio",
|
|
35463
|
+
"video",
|
|
35464
|
+
"canvas",
|
|
35465
|
+
"dialog",
|
|
35466
|
+
"head",
|
|
35467
|
+
"title"
|
|
35468
|
+
]);
|
|
35469
|
+
var ALLOWED_ATTRS = {
|
|
35470
|
+
a: /* @__PURE__ */ new Set(["href"]),
|
|
35471
|
+
img: /* @__PURE__ */ new Set(["src", "alt", "width", "height"]),
|
|
35472
|
+
td: /* @__PURE__ */ new Set(["colspan", "rowspan"]),
|
|
35473
|
+
th: /* @__PURE__ */ new Set(["colspan", "rowspan"]),
|
|
35474
|
+
ol: /* @__PURE__ */ new Set(["start"])
|
|
35475
|
+
};
|
|
35476
|
+
var BLOCKED_SCHEMES = /* @__PURE__ */ new Set(["javascript", "vbscript", "data", "file", "blob"]);
|
|
35477
|
+
function tagDisposition(tag) {
|
|
35478
|
+
const t = tag.toLowerCase();
|
|
35479
|
+
if (DROPPED_TAGS.has(t)) return "drop";
|
|
35480
|
+
return ALLOWED_TAGS.has(t) ? "keep" : "unwrap";
|
|
35481
|
+
}
|
|
35482
|
+
function attributeAllowed(tag, name) {
|
|
35483
|
+
const n = name.toLowerCase();
|
|
35484
|
+
if (n.startsWith("on") || n === "style") return false;
|
|
35485
|
+
if (n === "title") return true;
|
|
35486
|
+
return ALLOWED_ATTRS[tag.toLowerCase()]?.has(n) ?? false;
|
|
35487
|
+
}
|
|
35488
|
+
function safeUrl(value, absoluteOnly = false) {
|
|
35489
|
+
let url = "";
|
|
35490
|
+
for (const ch of value) if (ch.charCodeAt(0) > 32) url += ch;
|
|
35491
|
+
const m = /^([a-z][a-z0-9+.-]*):/i.exec(url);
|
|
35492
|
+
const scheme = m ? m[1].toLowerCase() : null;
|
|
35493
|
+
if (scheme !== null && BLOCKED_SCHEMES.has(scheme)) return null;
|
|
35494
|
+
if (absoluteOnly && scheme !== "http" && scheme !== "https") return null;
|
|
35495
|
+
return url;
|
|
35496
|
+
}
|
|
35497
|
+
var ELEMENT_NODE2 = 1;
|
|
35498
|
+
var TEXT_NODE = 3;
|
|
35499
|
+
function sanitizeHtml(html, doc) {
|
|
35500
|
+
const tpl = doc.createElement("template");
|
|
35501
|
+
tpl.innerHTML = html;
|
|
35502
|
+
const out = doc.createDocumentFragment();
|
|
35503
|
+
copyChildren(tpl.content, out, doc);
|
|
35504
|
+
return out;
|
|
35505
|
+
}
|
|
35506
|
+
function copyChildren(from, to, doc) {
|
|
35507
|
+
for (const child of Array.from(from.childNodes)) {
|
|
35508
|
+
if (child.nodeType === TEXT_NODE) {
|
|
35509
|
+
to.appendChild(doc.createTextNode(child.textContent ?? ""));
|
|
35510
|
+
continue;
|
|
35511
|
+
}
|
|
35512
|
+
if (child.nodeType !== ELEMENT_NODE2) continue;
|
|
35513
|
+
const el = child;
|
|
35514
|
+
const tag = el.tagName.toLowerCase();
|
|
35515
|
+
const disposition = tagDisposition(tag);
|
|
35516
|
+
if (disposition === "drop") continue;
|
|
35517
|
+
if (disposition === "unwrap") {
|
|
35518
|
+
copyChildren(el, to, doc);
|
|
35519
|
+
continue;
|
|
35520
|
+
}
|
|
35521
|
+
const clean = doc.createElement(tag);
|
|
35522
|
+
for (const attr of Array.from(el.attributes)) {
|
|
35523
|
+
const name = attr.name.toLowerCase();
|
|
35524
|
+
if (!attributeAllowed(tag, name)) continue;
|
|
35525
|
+
let value = attr.value;
|
|
35526
|
+
if (name === "href" || name === "src") {
|
|
35527
|
+
const safe = safeUrl(value, name === "src");
|
|
35528
|
+
if (safe === null) continue;
|
|
35529
|
+
value = safe;
|
|
35530
|
+
}
|
|
35531
|
+
clean.setAttribute(name, value);
|
|
35532
|
+
}
|
|
35533
|
+
if (tag === "a") {
|
|
35534
|
+
clean.setAttribute("target", "_blank");
|
|
35535
|
+
clean.setAttribute("rel", "noopener noreferrer");
|
|
35536
|
+
}
|
|
35537
|
+
copyChildren(el, clean, doc);
|
|
35538
|
+
to.appendChild(clean);
|
|
35539
|
+
}
|
|
35540
|
+
}
|
|
35541
|
+
|
|
35542
|
+
// src/renderers/native/chrome/marks/MarkPopover.ts
|
|
35543
|
+
var MARKS_STYLE_ID = "vela-marks-popover";
|
|
35544
|
+
var MARKS_CSS = `
|
|
35545
|
+
.vela-marks-panel { padding: 0; gap: 0; min-width: 220px; max-width: 320px; max-height: 320px; overflow-y: auto; overscroll-behavior: contain; }
|
|
35546
|
+
.vela-marks-section { display: flex; flex-direction: column; gap: 8px; padding: 10px 12px; }
|
|
35547
|
+
.vela-marks-section + .vela-marks-section { border-top: 1px solid var(--vela-border); }
|
|
35548
|
+
.vela-marks-field { display: flex; justify-content: space-between; gap: 16px; line-height: 1.45; }
|
|
35549
|
+
.vela-marks-field-label { color: var(--vela-fg-muted); }
|
|
35550
|
+
.vela-marks-field-value { color: var(--vela-fg-bright); text-align: right; font-variant-numeric: tabular-nums; }
|
|
35551
|
+
.vela-marks-html { color: var(--vela-fg); line-height: 1.45; overflow-wrap: anywhere; }
|
|
35552
|
+
.vela-marks-html p { margin: 0 0 6px; }
|
|
35553
|
+
.vela-marks-html p:last-child { margin-bottom: 0; }
|
|
35554
|
+
.vela-marks-html a { color: var(--vela-accent); }
|
|
35555
|
+
.vela-marks-html img { max-width: 100%; height: auto; }
|
|
35556
|
+
.vela-marks-html table { border-collapse: collapse; }
|
|
35557
|
+
.vela-marks-html td, .vela-marks-html th { padding: 2px 6px; border: 1px solid var(--vela-border); }
|
|
35558
|
+
.vela-marks-html pre, .vela-marks-html code { font-family: var(--vela-font-mono, monospace); font-size: 0.92em; }
|
|
35559
|
+
.vela-marks-loading, .vela-marks-error { color: var(--vela-fg-muted); font-style: italic; }
|
|
35560
|
+
`;
|
|
35561
|
+
var MarkPopover = class {
|
|
35562
|
+
constructor(deps) {
|
|
35563
|
+
this.deps = deps;
|
|
35564
|
+
this.pop = null;
|
|
35565
|
+
this.openKey = null;
|
|
35566
|
+
/** Bumped per open/close — a lazy content resolving after its popup went away is dropped. */
|
|
35567
|
+
this.generation = 0;
|
|
35568
|
+
const doc = deps.plot.ownerDocument;
|
|
35569
|
+
injectStyles(CALLOUT_STYLE_ID, CALLOUT_CSS, doc);
|
|
35570
|
+
injectStyles(MARKS_STYLE_ID, MARKS_CSS, doc);
|
|
35571
|
+
this.anchor = doc.createElement("div");
|
|
35572
|
+
this.anchor.className = "vela-marks-anchor";
|
|
35573
|
+
Object.assign(this.anchor.style, { position: "absolute", pointerEvents: "none", left: "0", top: "0", width: "0", height: "0" });
|
|
35574
|
+
deps.plot.appendChild(this.anchor);
|
|
35575
|
+
}
|
|
35576
|
+
/** The cluster key the open popup belongs to, or null. */
|
|
35577
|
+
get key() {
|
|
35578
|
+
return this.openKey;
|
|
35579
|
+
}
|
|
35580
|
+
open(cluster, rect) {
|
|
35581
|
+
this.close();
|
|
35582
|
+
this.place(rect);
|
|
35583
|
+
const gen = ++this.generation;
|
|
35584
|
+
this.openKey = cluster.key;
|
|
35585
|
+
this.pop = new Popover({
|
|
35586
|
+
trigger: this.anchor,
|
|
35587
|
+
host: this.deps.host(),
|
|
35588
|
+
theme: this.deps.theme(),
|
|
35589
|
+
gap: 8,
|
|
35590
|
+
align: "center",
|
|
35591
|
+
// centered on the glyph
|
|
35592
|
+
fadeMs: 120,
|
|
35593
|
+
// a short, discreet fade in and out
|
|
35594
|
+
className: "vela-marks-pop",
|
|
35595
|
+
content: (body) => this.build(body, cluster, gen),
|
|
35596
|
+
onClose: () => {
|
|
35597
|
+
if (this.generation === gen) {
|
|
35598
|
+
this.openKey = null;
|
|
35599
|
+
this.pop = null;
|
|
35600
|
+
this.deps.onOpenChange?.(null);
|
|
35601
|
+
}
|
|
35602
|
+
}
|
|
35603
|
+
});
|
|
35604
|
+
this.pop.show();
|
|
35605
|
+
this.deps.onOpenChange?.(cluster.key);
|
|
35606
|
+
}
|
|
35607
|
+
/** Follow the anchor glyph after a repaint; `null` (glyph gone — hidden, scrolled off, marks replaced) closes. */
|
|
35608
|
+
track(rect) {
|
|
35609
|
+
if (!this.pop) return;
|
|
35610
|
+
if (!rect) {
|
|
35611
|
+
this.close();
|
|
35612
|
+
return;
|
|
35613
|
+
}
|
|
35614
|
+
this.place(rect);
|
|
35615
|
+
this.pop.reposition();
|
|
35616
|
+
}
|
|
35617
|
+
close() {
|
|
35618
|
+
const pop = this.pop;
|
|
35619
|
+
const wasOpen = this.openKey !== null;
|
|
35620
|
+
this.pop = null;
|
|
35621
|
+
this.openKey = null;
|
|
35622
|
+
this.generation++;
|
|
35623
|
+
pop?.destroy();
|
|
35624
|
+
if (wasOpen) this.deps.onOpenChange?.(null);
|
|
35625
|
+
}
|
|
35626
|
+
destroy() {
|
|
35627
|
+
this.close();
|
|
35628
|
+
this.anchor.remove();
|
|
35629
|
+
}
|
|
35630
|
+
place(rect) {
|
|
35631
|
+
Object.assign(this.anchor.style, {
|
|
35632
|
+
left: `${rect.x - rect.size / 2}px`,
|
|
35633
|
+
top: `${rect.y - rect.size / 2}px`,
|
|
35634
|
+
width: `${rect.size}px`,
|
|
35635
|
+
height: `${rect.size}px`
|
|
35636
|
+
});
|
|
35637
|
+
}
|
|
35638
|
+
build(body, cluster, gen) {
|
|
35639
|
+
const doc = body.ownerDocument;
|
|
35640
|
+
const root = doc.createElement("div");
|
|
35641
|
+
root.className = "vela-callout-panel vela-marks-panel";
|
|
35642
|
+
const pending = [];
|
|
35643
|
+
for (const mark of cluster.marks) {
|
|
35644
|
+
const section = doc.createElement("section");
|
|
35645
|
+
section.className = "vela-marks-section";
|
|
35646
|
+
if (mark.title) {
|
|
35647
|
+
const title = doc.createElement("div");
|
|
35648
|
+
title.className = "vela-callout-title";
|
|
35649
|
+
title.textContent = mark.title;
|
|
35650
|
+
section.appendChild(title);
|
|
35651
|
+
}
|
|
35652
|
+
const content = mark.content;
|
|
35653
|
+
if (typeof content === "function") {
|
|
35654
|
+
const slot = doc.createElement("div");
|
|
35655
|
+
slot.className = "vela-marks-loading";
|
|
35656
|
+
slot.textContent = "Loading\u2026";
|
|
35657
|
+
section.appendChild(slot);
|
|
35658
|
+
pending.push({ el: section, resolve: () => this.resolveLazy(content, slot, gen) });
|
|
35659
|
+
} else if (content !== void 0) {
|
|
35660
|
+
this.renderContent(section, content);
|
|
35661
|
+
}
|
|
35662
|
+
if (section.childElementCount > 0) root.appendChild(section);
|
|
35663
|
+
}
|
|
35664
|
+
body.appendChild(root);
|
|
35665
|
+
if (pending.length > 0) scheduleLazy(root, pending);
|
|
35666
|
+
}
|
|
35667
|
+
resolveLazy(source, slot, gen) {
|
|
35668
|
+
let result;
|
|
35669
|
+
try {
|
|
35670
|
+
result = Promise.resolve(source());
|
|
35671
|
+
} catch (err) {
|
|
35672
|
+
result = Promise.reject(err instanceof Error ? err : new Error(String(err)));
|
|
35673
|
+
}
|
|
35674
|
+
void result.then(
|
|
35675
|
+
(content) => {
|
|
35676
|
+
if (gen !== this.generation) return;
|
|
35677
|
+
const section = slot.parentElement;
|
|
35678
|
+
if (!section) return;
|
|
35679
|
+
slot.remove();
|
|
35680
|
+
this.renderContent(section, content);
|
|
35681
|
+
this.pop?.reposition();
|
|
35682
|
+
},
|
|
35683
|
+
() => {
|
|
35684
|
+
if (gen !== this.generation) return;
|
|
35685
|
+
slot.className = "vela-marks-error";
|
|
35686
|
+
slot.textContent = "Couldn\u2019t load this entry.";
|
|
35687
|
+
}
|
|
35688
|
+
);
|
|
35689
|
+
}
|
|
35690
|
+
renderContent(section, content) {
|
|
35691
|
+
const doc = section.ownerDocument;
|
|
35692
|
+
if (!content || typeof content !== "object") return;
|
|
35693
|
+
if ("text" in content) {
|
|
35694
|
+
const text = doc.createElement("div");
|
|
35695
|
+
text.className = "vela-callout-text";
|
|
35696
|
+
text.textContent = String(content.text);
|
|
35697
|
+
section.appendChild(text);
|
|
35698
|
+
return;
|
|
35699
|
+
}
|
|
35700
|
+
if ("html" in content) {
|
|
35701
|
+
const html = doc.createElement("div");
|
|
35702
|
+
html.className = "vela-marks-html";
|
|
35703
|
+
html.appendChild(sanitizeHtml(String(content.html), doc));
|
|
35704
|
+
section.appendChild(html);
|
|
35705
|
+
return;
|
|
35706
|
+
}
|
|
35707
|
+
if ("panel" in content && content.panel && Array.isArray(content.panel.items)) {
|
|
35708
|
+
let actions = null;
|
|
35709
|
+
for (const item of content.panel.items) {
|
|
35710
|
+
if (!item || typeof item !== "object") continue;
|
|
35711
|
+
if (item.type === "button") {
|
|
35712
|
+
if (!actions) {
|
|
35713
|
+
actions = doc.createElement("div");
|
|
35714
|
+
actions.className = "vela-callout-actions";
|
|
35715
|
+
section.appendChild(actions);
|
|
35716
|
+
}
|
|
35717
|
+
const btn2 = doc.createElement("button");
|
|
35718
|
+
btn2.type = "button";
|
|
35719
|
+
btn2.className = "vela-callout-btn" + (item.primary ? " vela-callout-btn-primary" : "");
|
|
35720
|
+
btn2.textContent = item.label;
|
|
35721
|
+
btn2.addEventListener("click", () => {
|
|
35722
|
+
item.run();
|
|
35723
|
+
if (item.close !== false) this.close();
|
|
35724
|
+
});
|
|
35725
|
+
actions.appendChild(btn2);
|
|
35726
|
+
continue;
|
|
35727
|
+
}
|
|
35728
|
+
actions = null;
|
|
35729
|
+
if (item.type === "text") {
|
|
35730
|
+
const text = doc.createElement("div");
|
|
35731
|
+
text.className = "vela-callout-text";
|
|
35732
|
+
text.textContent = item.text;
|
|
35733
|
+
section.appendChild(text);
|
|
35734
|
+
} else if (item.type === "field") {
|
|
35735
|
+
const row = doc.createElement("div");
|
|
35736
|
+
row.className = "vela-marks-field";
|
|
35737
|
+
const label = doc.createElement("span");
|
|
35738
|
+
label.className = "vela-marks-field-label";
|
|
35739
|
+
label.textContent = item.label;
|
|
35740
|
+
const value = doc.createElement("span");
|
|
35741
|
+
value.className = "vela-marks-field-value";
|
|
35742
|
+
value.textContent = item.value;
|
|
35743
|
+
row.append(label, value);
|
|
35744
|
+
section.appendChild(row);
|
|
35745
|
+
}
|
|
35746
|
+
}
|
|
35747
|
+
}
|
|
35748
|
+
}
|
|
35749
|
+
};
|
|
35750
|
+
function scheduleLazy(scroller, pending) {
|
|
35751
|
+
if (typeof IntersectionObserver === "undefined") {
|
|
35752
|
+
for (const p of pending) p.resolve();
|
|
35753
|
+
return;
|
|
35754
|
+
}
|
|
35755
|
+
const io = new IntersectionObserver(
|
|
35756
|
+
(entries) => {
|
|
35757
|
+
for (const e of entries) {
|
|
35758
|
+
if (!e.isIntersecting) continue;
|
|
35759
|
+
const p = pending.find((q) => q.el === e.target);
|
|
35760
|
+
if (!p) continue;
|
|
35761
|
+
io.unobserve(e.target);
|
|
35762
|
+
p.resolve();
|
|
35763
|
+
}
|
|
35764
|
+
},
|
|
35765
|
+
{ root: scroller }
|
|
35766
|
+
);
|
|
35767
|
+
for (const p of pending) io.observe(p.el);
|
|
35768
|
+
}
|
|
35769
|
+
|
|
34622
35770
|
// src/renderers/native/core/manualScale.ts
|
|
34623
35771
|
function rescaleAround(start, factor) {
|
|
34624
35772
|
if (start.log && start.min > 0 && start.max > start.min) {
|
|
@@ -35347,11 +36495,12 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
35347
36495
|
var SCROLL_BTN_PROXIMITY_PX = 120;
|
|
35348
36496
|
var MIN_VISIBLE_BARS = 2;
|
|
35349
36497
|
var ZOOM_OUT_MARGIN_BARS = 6;
|
|
35350
|
-
var
|
|
35351
|
-
var SCALE_TAU_MS = 80;
|
|
35352
|
-
var FLING_TAU_MS = 110;
|
|
35353
|
-
var SCROLL_TO_TAU_MS = 130;
|
|
36498
|
+
var INTRO_MODEL_FADE_MS = 350;
|
|
35354
36499
|
var FLING_STOP_PX = 0.02;
|
|
36500
|
+
var MARK_FLASH_MS = 220;
|
|
36501
|
+
function frameNow() {
|
|
36502
|
+
return typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
36503
|
+
}
|
|
35355
36504
|
var PRICE_SCALE_K = 4e-3;
|
|
35356
36505
|
var KEY_ZOOM_STEP = 0.2;
|
|
35357
36506
|
var SEPARATOR_HIT_PX = 4;
|
|
@@ -35400,9 +36549,20 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
35400
36549
|
this.indicatorSlices = new IndicatorDrawingSlices();
|
|
35401
36550
|
/** Hover tooltips for Pine labels (canvas hit-rects collected by the chrome layer). */
|
|
35402
36551
|
this.labelTooltip = null;
|
|
36552
|
+
/** The timeline-mark popup (a kit Popover anchored on a lane glyph); null before mount. */
|
|
36553
|
+
this.markPopover = null;
|
|
36554
|
+
/** How the fanned mark stack was opened: a hover folds when the pointer leaves, a tap only on a tap elsewhere. */
|
|
36555
|
+
this.marksExpandedBy = "hover";
|
|
36556
|
+
/** The rAF loop keeping the chrome repainting while a lane glyph pulses (hover) or flashes (click); null when idle. */
|
|
36557
|
+
this.markPulseRaf = null;
|
|
36558
|
+
this.markClickCbs = /* @__PURE__ */ new Set();
|
|
35403
36559
|
this.crosshairLayer = new CrosshairRenderer();
|
|
35404
|
-
/**
|
|
35405
|
-
|
|
36560
|
+
/** The second pulse the countdown-to-bar-close chip ticks on: the host's (`setWallClock`)
|
|
36561
|
+
* when one is wired, else the renderer's own second-aligned clock. */
|
|
36562
|
+
this.hostClock = null;
|
|
36563
|
+
this.ownClock = null;
|
|
36564
|
+
/** Live subscription to the pulse while the countdown is on; null when off. */
|
|
36565
|
+
this.countdownUnsub = null;
|
|
35406
36566
|
this.symbolPicker = null;
|
|
35407
36567
|
/** Indicator titles (the legend rows) shown — held here so a remount re-applies it. */
|
|
35408
36568
|
this.indicatorTitlesOn = true;
|
|
@@ -35420,19 +36580,25 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
35420
36580
|
/** Drawings layer self-serves Ctrl+Z/Y (see the `historyChords` feature). */
|
|
35421
36581
|
this.historyChordsEnabled = true;
|
|
35422
36582
|
this.liveRegion = null;
|
|
35423
|
-
// ── animation state
|
|
35424
|
-
|
|
35425
|
-
this.
|
|
35426
|
-
|
|
35427
|
-
|
|
35428
|
-
|
|
35429
|
-
|
|
36583
|
+
// ── animation state: one ease time-constant per motion (0 = off), each remembering the
|
|
36584
|
+
// host's duration so the config's on/off switches restore it (see EaseSetting) ──
|
|
36585
|
+
this.animZoom = new EaseSetting(ZOOM_EASE_DEFAULT_MS);
|
|
36586
|
+
// wheel-zoom glide
|
|
36587
|
+
this.animPan = new EaseSetting(PAN_INERTIA_DEFAULT_MS);
|
|
36588
|
+
// inertial-pan velocity decay
|
|
36589
|
+
this.animScroll = new EaseSetting(SCROLL_EASE_DEFAULT_MS);
|
|
36590
|
+
// scroll-to-latest / panBy glide
|
|
36591
|
+
this.animAutoscale = new EaseSetting(AUTOSCALE_EASE_DEFAULT_MS);
|
|
36592
|
+
// autoscale glide during zoom/fling
|
|
36593
|
+
this.animLiveBar = new EaseSetting(LIVE_BAR_EASE_DEFAULT_MS, 0);
|
|
36594
|
+
// forming-bar OHLC glide; ships off
|
|
35430
36595
|
// Brand default candles.
|
|
35431
36596
|
this.candleUp = BULLISH;
|
|
35432
36597
|
this.candleDown = BEARISH;
|
|
35433
36598
|
// ── intro reveal (plays once when candles first appear) ──
|
|
35434
|
-
this.
|
|
35435
|
-
|
|
36599
|
+
this.intro = { style: "settle", duration: INTRO_DURATION_DEFAULT_MS };
|
|
36600
|
+
this.introOnStyle = "settle";
|
|
36601
|
+
// the style the config's on/off switch restores
|
|
35436
36602
|
this.introPlayed = false;
|
|
35437
36603
|
this.introRaf = null;
|
|
35438
36604
|
/** The load affordance (three pulsing dots) — up while the host reports a bar load in
|
|
@@ -35551,7 +36717,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
35551
36717
|
this.moveIndicatorCbs = /* @__PURE__ */ new Set();
|
|
35552
36718
|
this.priceStyleCbs = /* @__PURE__ */ new Set();
|
|
35553
36719
|
this.name = "native";
|
|
35554
|
-
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"];
|
|
36720
|
+
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"];
|
|
35555
36721
|
/** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
|
|
35556
36722
|
* so moving onto the button doesn't count as leaving). */
|
|
35557
36723
|
this.onScrollProximityMove = (e) => {
|
|
@@ -35583,9 +36749,12 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
35583
36749
|
this.scene.showPriceLine = opts.currentPriceLine;
|
|
35584
36750
|
this.scene.logScale = opts.logScale;
|
|
35585
36751
|
this.backendMode = opts.nativeBackend;
|
|
35586
|
-
this.animZoom
|
|
35587
|
-
this.animPan
|
|
35588
|
-
this.
|
|
36752
|
+
this.animZoom.set(opts.animZoom);
|
|
36753
|
+
this.animPan.set(opts.animPan);
|
|
36754
|
+
this.animScroll.set(opts.animScroll);
|
|
36755
|
+
this.animAutoscale.set(opts.animAutoscale);
|
|
36756
|
+
this.animLiveBar.set(opts.animLiveBar);
|
|
36757
|
+
this.setIntro(opts.animIntro);
|
|
35589
36758
|
this.glowAmount = opts.glow;
|
|
35590
36759
|
this.candleUp = opts.upColor;
|
|
35591
36760
|
this.candleDown = opts.downColor;
|
|
@@ -35629,20 +36798,29 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
35629
36798
|
if (this.backend && "glow" in this.backend) this.backend.glow = this.glowAmount;
|
|
35630
36799
|
break;
|
|
35631
36800
|
case "animZoom":
|
|
35632
|
-
this.animZoom
|
|
36801
|
+
this.animZoom.set(resolveEaseMs(value, ZOOM_EASE_DEFAULT_MS));
|
|
35633
36802
|
return;
|
|
35634
36803
|
// affects the next interaction only — nothing to repaint
|
|
35635
|
-
case "animPan":
|
|
35636
|
-
|
|
36804
|
+
case "animPan": {
|
|
36805
|
+
const ms = resolveEaseMs(value, PAN_INERTIA_DEFAULT_MS);
|
|
36806
|
+
this.animPan.set(ms);
|
|
36807
|
+
this.animScroll.toggle(ms > 0);
|
|
36808
|
+
return;
|
|
36809
|
+
}
|
|
36810
|
+
case "animScroll":
|
|
36811
|
+
this.animScroll.set(resolveEaseMs(value, SCROLL_EASE_DEFAULT_MS));
|
|
36812
|
+
return;
|
|
36813
|
+
case "animAutoscale":
|
|
36814
|
+
this.animAutoscale.set(resolveEaseMs(value, AUTOSCALE_EASE_DEFAULT_MS));
|
|
35637
36815
|
return;
|
|
36816
|
+
// a glide in flight finishes at the new rate (or snaps at 0)
|
|
35638
36817
|
case "animLiveBar":
|
|
35639
|
-
this.
|
|
36818
|
+
this.animLiveBar.set(resolveLiveBarEaseMs(value));
|
|
35640
36819
|
return;
|
|
35641
36820
|
// affects the next tick only; a glide in flight finishes at the new rate (or snaps at 0)
|
|
35642
36821
|
case "intro": {
|
|
35643
|
-
|
|
35644
|
-
this.
|
|
35645
|
-
if (s) this.playIntro(s);
|
|
36822
|
+
this.setIntro(resolveIntro(value));
|
|
36823
|
+
if (this.intro.style) this.playIntro();
|
|
35646
36824
|
return;
|
|
35647
36825
|
}
|
|
35648
36826
|
case "zoomAnchor":
|
|
@@ -35714,6 +36892,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
35714
36892
|
case "tradeMarkers":
|
|
35715
36893
|
this.scene.tradeMarkers = mergeTradeMarkersState(this.scene.tradeMarkers, value);
|
|
35716
36894
|
break;
|
|
36895
|
+
case "marks":
|
|
36896
|
+
this.scene.marks = mergeMarksState(this.scene.marks, value);
|
|
36897
|
+
this.markPopover?.close();
|
|
36898
|
+
break;
|
|
35717
36899
|
case "keyboard":
|
|
35718
36900
|
this.setKeyboardEnabled(Boolean(value));
|
|
35719
36901
|
return;
|
|
@@ -35772,13 +36954,17 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
35772
36954
|
case "glow":
|
|
35773
36955
|
return this.glowAmount;
|
|
35774
36956
|
case "animZoom":
|
|
35775
|
-
return this.animZoom;
|
|
36957
|
+
return this.animZoom.tau;
|
|
35776
36958
|
case "animPan":
|
|
35777
|
-
return this.animPan;
|
|
36959
|
+
return this.animPan.tau;
|
|
36960
|
+
case "animScroll":
|
|
36961
|
+
return this.animScroll.tau;
|
|
36962
|
+
case "animAutoscale":
|
|
36963
|
+
return this.animAutoscale.tau;
|
|
35778
36964
|
case "animLiveBar":
|
|
35779
|
-
return this.
|
|
36965
|
+
return this.animLiveBar.tau;
|
|
35780
36966
|
case "intro":
|
|
35781
|
-
return this.
|
|
36967
|
+
return this.intro.style;
|
|
35782
36968
|
case "zoomAnchor":
|
|
35783
36969
|
return this.zoomAnchorMode;
|
|
35784
36970
|
case "axisDrag":
|
|
@@ -35821,6 +37007,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
35821
37007
|
}
|
|
35822
37008
|
case "tradeMarkers":
|
|
35823
37009
|
return { ...this.scene.tradeMarkers, colors: { ...this.scene.tradeMarkers.colors } };
|
|
37010
|
+
case "marks":
|
|
37011
|
+
return { visible: this.scene.marks.visible, groups: { ...this.scene.marks.groups } };
|
|
35824
37012
|
case "keyboard":
|
|
35825
37013
|
return this.keyboardEnabled;
|
|
35826
37014
|
case "historyChords":
|
|
@@ -35946,7 +37134,13 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
35946
37134
|
currentPriceLine: this.scene.showPriceLine,
|
|
35947
37135
|
priceLabel: this.scene.showPriceLabel,
|
|
35948
37136
|
countdown: this.scene.showCountdown,
|
|
35949
|
-
animateLastPrice: this.
|
|
37137
|
+
animateLastPrice: this.animLiveBar.on
|
|
37138
|
+
},
|
|
37139
|
+
animations: {
|
|
37140
|
+
zoom: this.animZoom.on,
|
|
37141
|
+
pan: this.animPan.on,
|
|
37142
|
+
autoscale: this.animAutoscale.on,
|
|
37143
|
+
intro: this.intro.style !== false
|
|
35950
37144
|
},
|
|
35951
37145
|
panes: { separatorColor: s.separatorColor ?? t.borderColor },
|
|
35952
37146
|
trades: {
|
|
@@ -35958,6 +37152,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
35958
37152
|
exitColor: this.scene.tradeMarkers.colors.exit
|
|
35959
37153
|
},
|
|
35960
37154
|
timeScale: { timezone: this.scene.timezone },
|
|
37155
|
+
marks: { visible: this.scene.marks.visible, groups: { ...this.scene.marks.groups } },
|
|
35961
37156
|
candles: {
|
|
35962
37157
|
upColor: this.candleUp,
|
|
35963
37158
|
downColor: this.candleDown,
|
|
@@ -36053,7 +37248,12 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
36053
37248
|
this.scene.showPriceLabel = next2.priceScale.priceLabel;
|
|
36054
37249
|
this.scene.showCountdown = next2.priceScale.countdown;
|
|
36055
37250
|
this.syncCountdownTimer();
|
|
36056
|
-
this.
|
|
37251
|
+
this.animLiveBar.toggle(next2.priceScale.animateLastPrice);
|
|
37252
|
+
this.animZoom.toggle(next2.animations.zoom);
|
|
37253
|
+
this.animPan.toggle(next2.animations.pan);
|
|
37254
|
+
this.animScroll.toggle(next2.animations.pan);
|
|
37255
|
+
this.animAutoscale.toggle(next2.animations.autoscale);
|
|
37256
|
+
this.intro = { style: next2.animations.intro ? this.introOnStyle : false, duration: this.intro.duration || INTRO_DURATION_DEFAULT_MS };
|
|
36057
37257
|
s.separatorColor = keepInherit(s.separatorColor, next2.panes.separatorColor, prevTheme.borderColor);
|
|
36058
37258
|
this.scene.tradeMarkers = {
|
|
36059
37259
|
visible: next2.trades.visible,
|
|
@@ -36062,6 +37262,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
36062
37262
|
colors: { long: next2.trades.longColor, short: next2.trades.shortColor, exit: next2.trades.exitColor }
|
|
36063
37263
|
};
|
|
36064
37264
|
this.scene.timezone = next2.timeScale.timezone;
|
|
37265
|
+
this.scene.marks = { visible: next2.marks.visible, groups: { ...next2.marks.groups } };
|
|
36065
37266
|
this.candleUp = next2.candles.upColor;
|
|
36066
37267
|
this.candleDown = next2.candles.downColor;
|
|
36067
37268
|
s.candle = {
|
|
@@ -36194,6 +37395,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
36194
37395
|
}
|
|
36195
37396
|
this.settingsDialog.setTheme(this.theme);
|
|
36196
37397
|
this.settingsDialog.setHostSections(this.hostSettingsSections);
|
|
37398
|
+
this.settingsDialog.setMarkGroups(this.markGroupsInUse(), (id) => markGroupVisible(this.scene.marks, id, this.scene.markGroups));
|
|
36197
37399
|
this.settingsDialog.setHiddenSettings(this.hiddenSettings);
|
|
36198
37400
|
this.syncThemeControl();
|
|
36199
37401
|
this.settingsDialog.toggle(
|
|
@@ -36284,10 +37486,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
36284
37486
|
this.glideRightOffset(ZOOM_OUT_MARGIN_BARS);
|
|
36285
37487
|
}
|
|
36286
37488
|
/** Ease rightOffset to `target` at constant zoom (see animTick's scroll glide);
|
|
36287
|
-
* instant when
|
|
37489
|
+
* instant when the scroll glide is off. Shared by scroll-to-latest and panBy. */
|
|
36288
37490
|
glideRightOffset(target) {
|
|
36289
37491
|
const vp = this.coords.getViewport();
|
|
36290
|
-
if (!this.
|
|
37492
|
+
if (!this.animScroll.on) {
|
|
36291
37493
|
this.applyViewport({ barSpacing: vp.barSpacing, rightOffset: target });
|
|
36292
37494
|
return;
|
|
36293
37495
|
}
|
|
@@ -36352,20 +37554,21 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
36352
37554
|
* full size, eased, with a left→right stagger so the chart draws itself; `settle`
|
|
36353
37555
|
* adds an ease-out-back overshoot. Autoscale stays on the real bars so the frame
|
|
36354
37556
|
* never moves. Re-callable, so styles can be compared live from the console.
|
|
37557
|
+
* Style and sweep duration come from the resolved `intro` setting.
|
|
36355
37558
|
*/
|
|
36356
|
-
playIntro(
|
|
37559
|
+
playIntro() {
|
|
36357
37560
|
if (this.introRaf != null) cancelAnimationFrame(this.introRaf);
|
|
36358
37561
|
this.introRaf = null;
|
|
37562
|
+
const { style, duration } = this.intro;
|
|
36359
37563
|
const real = this.bars;
|
|
36360
37564
|
const n = real.length;
|
|
36361
|
-
if (n === 0) return;
|
|
37565
|
+
if (n === 0 || !style) return;
|
|
36362
37566
|
this.computeScales();
|
|
36363
37567
|
for (const pane of this.scene.panes.values()) pane.scale = { ...pane.scaleTarget };
|
|
36364
37568
|
this.modelAlpha = 0;
|
|
36365
|
-
const DURATION = 650;
|
|
36366
37569
|
const start = performance.now();
|
|
36367
37570
|
const step = (now) => {
|
|
36368
|
-
const p = Math.min(1, (now - start) /
|
|
37571
|
+
const p = Math.min(1, (now - start) / duration);
|
|
36369
37572
|
this.scene.bars = p >= 1 ? real : real.map((b, i) => this.revealCandle(b, i, p, n, style));
|
|
36370
37573
|
this.paintData();
|
|
36371
37574
|
if (p < 1) {
|
|
@@ -36379,10 +37582,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
36379
37582
|
}
|
|
36380
37583
|
/** After the candle reveal, fade the indicator models (series/fills/…) from hidden to full. */
|
|
36381
37584
|
fadeInModels() {
|
|
36382
|
-
const
|
|
37585
|
+
const fade = Math.min(INTRO_MODEL_FADE_MS, this.intro.duration || INTRO_MODEL_FADE_MS);
|
|
36383
37586
|
const start = performance.now();
|
|
36384
37587
|
const step = (now) => {
|
|
36385
|
-
this.modelAlpha = Math.min(1, (now - start) /
|
|
37588
|
+
this.modelAlpha = Math.min(1, (now - start) / fade);
|
|
36386
37589
|
this.paintData();
|
|
36387
37590
|
if (this.modelAlpha < 1) {
|
|
36388
37591
|
this.introRaf = requestAnimationFrame(step);
|
|
@@ -36497,7 +37700,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
36497
37700
|
zoomTo: (target, anchorLogical, anchorX) => this.zoomTo(target, anchorLogical, anchorX),
|
|
36498
37701
|
fling: (v) => this.fling(v),
|
|
36499
37702
|
onPointerMove: (x, y) => this.handlePointerMove(x, y),
|
|
36500
|
-
onClick: (x) => {
|
|
37703
|
+
onClick: (x, y) => {
|
|
37704
|
+
if (this.handleMarkClick(x, y)) return;
|
|
36501
37705
|
this.userDrawings?.deselect();
|
|
36502
37706
|
this.handleClick(x);
|
|
36503
37707
|
},
|
|
@@ -36526,7 +37730,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
36526
37730
|
drawingsPointerDown: (x, y, snap, shift4, mod) => this.userDrawings?.pointerDown(x, y, snap, shift4, mod),
|
|
36527
37731
|
drawingsPointerMove: (x, y, snap, shift4, mod) => this.userDrawings?.pointerMove(x, y, snap, shift4, mod),
|
|
36528
37732
|
drawingsPointerUp: (x, y, snap) => this.userDrawings?.pointerUp(x, y, snap),
|
|
36529
|
-
drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? null,
|
|
37733
|
+
drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? (this.chrome.markGlyphAt(x, y) ? "pointer" : null),
|
|
36530
37734
|
drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
|
|
36531
37735
|
drawingsClearTransient: () => this.userDrawings?.clearTransient()
|
|
36532
37736
|
});
|
|
@@ -36542,8 +37746,18 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
36542
37746
|
this.plot.addEventListener("pointerleave", this.onScrollProximityLeave);
|
|
36543
37747
|
this.labelTooltip = new LabelTooltip(this.plot, {
|
|
36544
37748
|
theme: () => this.chromeTheme(),
|
|
36545
|
-
lookup: (x, y) => this.indicatorSlices.labelTooltipAt(x, y)
|
|
37749
|
+
lookup: (x, y) => this.indicatorSlices.labelTooltipAt(x, y) ?? this.chrome.markTooltipAt(x, y, this.markGroupsInUse())
|
|
37750
|
+
});
|
|
37751
|
+
this.markPopover = new MarkPopover({
|
|
37752
|
+
plot: this.plot,
|
|
37753
|
+
host: () => this.dialogHost ?? this.plot,
|
|
37754
|
+
theme: () => this.chromeTheme(),
|
|
37755
|
+
onOpenChange: (key) => {
|
|
37756
|
+
this.scene.marksActiveKey = key;
|
|
37757
|
+
this.scheduler?.invalidate(2 /* Chrome */);
|
|
37758
|
+
}
|
|
36546
37759
|
});
|
|
37760
|
+
this.chrome.setMarkIconReady(() => this.scheduler?.invalidate(2 /* Chrome */));
|
|
36547
37761
|
this.userDrawings = new UserDrawingController(this.wrapper, this.plot, this.drawingsCanvas, {
|
|
36548
37762
|
projector: () => this.drawingProjector(),
|
|
36549
37763
|
dpr: () => this.coords.dpr,
|
|
@@ -36758,29 +37972,40 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
36758
37972
|
resize() {
|
|
36759
37973
|
this.syncSize();
|
|
36760
37974
|
}
|
|
36761
|
-
/**
|
|
36762
|
-
|
|
37975
|
+
/** Drive the countdown chip from the host's second pulse (`null` → the renderer's own). */
|
|
37976
|
+
setWallClock(clock) {
|
|
37977
|
+
if (clock === this.hostClock) return;
|
|
37978
|
+
this.hostClock = clock;
|
|
37979
|
+
if (this.countdownUnsub != null) {
|
|
37980
|
+
this.countdownUnsub();
|
|
37981
|
+
this.countdownUnsub = null;
|
|
37982
|
+
}
|
|
37983
|
+
this.syncCountdownTimer();
|
|
37984
|
+
}
|
|
37985
|
+
/** Subscribe to the second pulse while the countdown chip is on (so it ticks); unsubscribe
|
|
37986
|
+
* otherwise. Chrome tier: only the chip's text moves — an idle chart must not recompute
|
|
36763
37987
|
* scales or repaint the geometry/volume/VPVR/SDK layers once a second (that cost
|
|
36764
37988
|
* multiplies by the cell count in a multi-chart workspace). */
|
|
36765
37989
|
syncCountdownTimer() {
|
|
36766
37990
|
if (this.scene.showCountdown) {
|
|
36767
|
-
if (this.
|
|
36768
|
-
this.
|
|
37991
|
+
if (this.countdownUnsub == null) {
|
|
37992
|
+
const clock = this.hostClock ?? (this.ownClock ?? (this.ownClock = new SecondClock()));
|
|
37993
|
+
this.countdownUnsub = clock.onTick(() => {
|
|
36769
37994
|
if (this.scene.showCountdown && this.scene.bars.length > 0) this.scheduler?.invalidate(2 /* Chrome */);
|
|
36770
|
-
}
|
|
37995
|
+
});
|
|
36771
37996
|
}
|
|
36772
|
-
} else if (this.
|
|
36773
|
-
|
|
36774
|
-
this.
|
|
37997
|
+
} else if (this.countdownUnsub != null) {
|
|
37998
|
+
this.countdownUnsub();
|
|
37999
|
+
this.countdownUnsub = null;
|
|
36775
38000
|
}
|
|
36776
38001
|
}
|
|
36777
38002
|
destroy() {
|
|
36778
38003
|
if (this.introRaf != null) cancelAnimationFrame(this.introRaf);
|
|
36779
38004
|
this.loadingEl?.remove();
|
|
36780
38005
|
this.loadingEl = null;
|
|
36781
|
-
if (this.
|
|
36782
|
-
|
|
36783
|
-
this.
|
|
38006
|
+
if (this.countdownUnsub != null) {
|
|
38007
|
+
this.countdownUnsub();
|
|
38008
|
+
this.countdownUnsub = null;
|
|
36784
38009
|
}
|
|
36785
38010
|
this.scheduler?.destroy();
|
|
36786
38011
|
this.animator?.stop();
|
|
@@ -36805,6 +38030,11 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
36805
38030
|
this.plot?.removeEventListener("pointerleave", this.onScrollProximityLeave);
|
|
36806
38031
|
this.labelTooltip?.destroy();
|
|
36807
38032
|
this.labelTooltip = null;
|
|
38033
|
+
this.markPopover?.destroy();
|
|
38034
|
+
this.markPopover = null;
|
|
38035
|
+
this.chrome.setMarkIconReady(null);
|
|
38036
|
+
if (this.markPulseRaf !== null) cancelAnimationFrame(this.markPulseRaf);
|
|
38037
|
+
this.markPulseRaf = null;
|
|
36808
38038
|
this.scrollButton?.remove();
|
|
36809
38039
|
this.scrollButton = null;
|
|
36810
38040
|
for (const l of this.extLayers) l.instance.destroy?.();
|
|
@@ -36863,8 +38093,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
36863
38093
|
}
|
|
36864
38094
|
if (!this.introPlayed && this.bars.length > 0) {
|
|
36865
38095
|
this.introPlayed = true;
|
|
36866
|
-
if (this.
|
|
36867
|
-
this.playIntro(
|
|
38096
|
+
if (this.intro.style) {
|
|
38097
|
+
this.playIntro();
|
|
36868
38098
|
return;
|
|
36869
38099
|
}
|
|
36870
38100
|
}
|
|
@@ -36875,7 +38105,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
36875
38105
|
const last2 = this.bars[n - 1];
|
|
36876
38106
|
if (last2 && bar.time === last2.time) {
|
|
36877
38107
|
this.bars[n - 1] = bar;
|
|
36878
|
-
if (this.
|
|
38108
|
+
if (!this.animLiveBar.on || this.liveEaseTime !== bar.time) {
|
|
36879
38109
|
this.syncLiveEase(bar);
|
|
36880
38110
|
} else {
|
|
36881
38111
|
this.animator.start();
|
|
@@ -36891,11 +38121,11 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
36891
38121
|
this.scene.bars = this.bars;
|
|
36892
38122
|
this.scheduler.invalidate(4 /* Full */);
|
|
36893
38123
|
}
|
|
36894
|
-
/** Set the
|
|
36895
|
-
*
|
|
36896
|
-
|
|
36897
|
-
this.
|
|
36898
|
-
if (
|
|
38124
|
+
/** Set the reveal (style + duration). A non-off style is also remembered as what the
|
|
38125
|
+
* config's on/off toggle (`animations.intro`) switches back on to. */
|
|
38126
|
+
setIntro(next2) {
|
|
38127
|
+
this.intro = next2;
|
|
38128
|
+
if (next2.style) this.introOnStyle = next2.style;
|
|
36899
38129
|
}
|
|
36900
38130
|
/** Snap the eased forming-bar state to `bar` — no glide (a fresh bar or the first tick of one). */
|
|
36901
38131
|
syncLiveEase(bar) {
|
|
@@ -36909,7 +38139,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
36909
38139
|
const target = this.bars[this.bars.length - 1];
|
|
36910
38140
|
if (!target || this.liveEaseTime !== target.time) return false;
|
|
36911
38141
|
const eps = Math.max(1e-9, Math.abs(target.close) * 1e-6);
|
|
36912
|
-
const tau = this.
|
|
38142
|
+
const tau = this.animLiveBar.tau;
|
|
36913
38143
|
const nh = easeToward(this.liveEaseHigh, target.high, dtMs, tau);
|
|
36914
38144
|
const nl = easeToward(this.liveEaseLow, target.low, dtMs, tau);
|
|
36915
38145
|
const nc = easeToward(this.liveEaseClose, target.close, dtMs, tau);
|
|
@@ -37049,10 +38279,12 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
37049
38279
|
this.refreshAnchorOffset(model);
|
|
37050
38280
|
if (model.native && this.extLayers.some((l) => l.def.id === model.native.type)) this.scene.assignIndicatorZTop(model.id);
|
|
37051
38281
|
else this.scene.assignIndicatorZ(model.id);
|
|
37052
|
-
|
|
37053
|
-
|
|
37054
|
-
|
|
37055
|
-
|
|
38282
|
+
if (model.legend !== false) {
|
|
38283
|
+
this.inputsUI.upsert(model.id, model.shorttitle ?? model.title, model.inputs, model.inputValues, model.paneId, {
|
|
38284
|
+
native: !!model.native,
|
|
38285
|
+
...model.props ? { props: model.props, propValues: model.propValues ?? {} } : {}
|
|
38286
|
+
});
|
|
38287
|
+
}
|
|
37056
38288
|
if (model.native?.type === "volume") {
|
|
37057
38289
|
this.volumeActive = true;
|
|
37058
38290
|
this.volumeHidden = false;
|
|
@@ -37199,7 +38431,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
37199
38431
|
this.settingsDialog?.setHiddenSettings(this.hiddenSettings);
|
|
37200
38432
|
}
|
|
37201
38433
|
listSettingsIds() {
|
|
37202
|
-
return settingsIdCatalog(this.hostSettingsSections);
|
|
38434
|
+
return settingsIdCatalog(this.hostSettingsSections, this.markGroupsInUse());
|
|
37203
38435
|
}
|
|
37204
38436
|
onChartTypeSettingsChange(cb) {
|
|
37205
38437
|
this.chartTypeSettingsCbs.add(cb);
|
|
@@ -37225,6 +38457,22 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
37225
38457
|
this.axisLongPressCbs.add(cb);
|
|
37226
38458
|
return () => this.axisLongPressCbs.delete(cb);
|
|
37227
38459
|
}
|
|
38460
|
+
// ── timeline marks (the `chart.marks` model; see the port) ──
|
|
38461
|
+
setTimelineMarks(marks, groups) {
|
|
38462
|
+
this.scene.timelineMarks = marks;
|
|
38463
|
+
this.scene.markGroups = groups;
|
|
38464
|
+
this.scene.marksExpandedStack = null;
|
|
38465
|
+
this.markPopover?.close();
|
|
38466
|
+
this.scheduler?.invalidate(2 /* Chrome */);
|
|
38467
|
+
}
|
|
38468
|
+
onMarkClick(cb) {
|
|
38469
|
+
this.markClickCbs.add(cb);
|
|
38470
|
+
return () => this.markClickCbs.delete(cb);
|
|
38471
|
+
}
|
|
38472
|
+
/** Every group the lane knows: the defined ones, then those marks name without a definition. */
|
|
38473
|
+
markGroupsInUse() {
|
|
38474
|
+
return effectiveMarkGroups(this.scene.timelineMarks, this.scene.markGroups);
|
|
38475
|
+
}
|
|
37228
38476
|
onViewportChange(cb) {
|
|
37229
38477
|
this.viewportCbs.add(cb);
|
|
37230
38478
|
return () => this.viewportCbs.delete(cb);
|
|
@@ -37278,7 +38526,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
37278
38526
|
this.zoomAnchorX = anchorX;
|
|
37279
38527
|
this.panVelocity = 0;
|
|
37280
38528
|
this.scrollTargetRO = null;
|
|
37281
|
-
if (!this.animZoom) {
|
|
38529
|
+
if (!this.animZoom.on) {
|
|
37282
38530
|
const v = this.clampViewport(barSpacing, this.anchoredRightOffset(barSpacing));
|
|
37283
38531
|
this.coords.setViewport(v);
|
|
37284
38532
|
this.targetBarSpacing = v.barSpacing;
|
|
@@ -37291,7 +38539,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
37291
38539
|
}
|
|
37292
38540
|
/** Inertial pan: continue with a rightOffset velocity (logical units / ms) that decays. */
|
|
37293
38541
|
fling(velocity) {
|
|
37294
|
-
if (!this.animPan) return;
|
|
38542
|
+
if (!this.animPan.on) return;
|
|
37295
38543
|
this.scrollTargetRO = null;
|
|
37296
38544
|
this.panVelocity = velocity;
|
|
37297
38545
|
this.animator.start();
|
|
@@ -37331,7 +38579,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
37331
38579
|
let active = false;
|
|
37332
38580
|
const tbs = this.targetBarSpacing;
|
|
37333
38581
|
if (Math.abs(barSpacing - tbs) > tbs * 1e-3) {
|
|
37334
|
-
barSpacing = clampBarSpacing(easeToward(barSpacing, tbs, dtMs,
|
|
38582
|
+
barSpacing = clampBarSpacing(easeToward(barSpacing, tbs, dtMs, this.animZoom.tau));
|
|
37335
38583
|
rightOffset = this.anchoredRightOffset(barSpacing);
|
|
37336
38584
|
active = true;
|
|
37337
38585
|
} else if (barSpacing !== tbs) {
|
|
@@ -37341,13 +38589,14 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
37341
38589
|
const stopVel = FLING_STOP_PX / Math.max(1e-6, barSpacing * this.coords.spacingScale);
|
|
37342
38590
|
if (Math.abs(this.panVelocity) > stopVel) {
|
|
37343
38591
|
rightOffset += this.panVelocity * dtMs;
|
|
37344
|
-
|
|
38592
|
+
const tau = this.animPan.tau;
|
|
38593
|
+
this.panVelocity = tau > 0 ? this.panVelocity * Math.exp(-dtMs / tau) : 0;
|
|
37345
38594
|
if (Math.abs(this.panVelocity) <= stopVel) this.panVelocity = 0;
|
|
37346
38595
|
else active = true;
|
|
37347
38596
|
}
|
|
37348
38597
|
if (this.scrollTargetRO != null) {
|
|
37349
38598
|
const target = this.scrollTargetRO;
|
|
37350
|
-
const next2 = easeToward(rightOffset, target, dtMs,
|
|
38599
|
+
const next2 = easeToward(rightOffset, target, dtMs, this.animScroll.tau);
|
|
37351
38600
|
if (Math.abs(next2 - target) < 1e-3) {
|
|
37352
38601
|
rightOffset = target;
|
|
37353
38602
|
this.scrollTargetRO = null;
|
|
@@ -37375,6 +38624,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
37375
38624
|
* through Math.log), not a non-linear jump. */
|
|
37376
38625
|
easeScales(dtMs) {
|
|
37377
38626
|
let moving = false;
|
|
38627
|
+
const tau = this.animAutoscale.tau;
|
|
37378
38628
|
for (const pane of this.scene.panes.values()) {
|
|
37379
38629
|
const t = pane.scaleTarget;
|
|
37380
38630
|
const s = pane.scale;
|
|
@@ -37382,8 +38632,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
37382
38632
|
const lt0 = Math.log(t.min);
|
|
37383
38633
|
const lt1 = Math.log(t.max);
|
|
37384
38634
|
const lspan = Math.max(1e-9, Math.abs(lt1 - lt0));
|
|
37385
|
-
const n0 = easeToward(Math.log(s.min), lt0, dtMs,
|
|
37386
|
-
const n1 = easeToward(Math.log(s.max), lt1, dtMs,
|
|
38635
|
+
const n0 = easeToward(Math.log(s.min), lt0, dtMs, tau);
|
|
38636
|
+
const n1 = easeToward(Math.log(s.max), lt1, dtMs, tau);
|
|
37387
38637
|
if (Math.abs(n0 - lt0) <= lspan * 1e-3 && Math.abs(n1 - lt1) <= lspan * 1e-3) {
|
|
37388
38638
|
pane.scale = { min: t.min, max: t.max, log: true };
|
|
37389
38639
|
} else {
|
|
@@ -37393,8 +38643,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
37393
38643
|
continue;
|
|
37394
38644
|
}
|
|
37395
38645
|
const span = Math.max(1e-9, Math.abs(t.max - t.min));
|
|
37396
|
-
let nmin = easeToward(s.min, t.min, dtMs,
|
|
37397
|
-
let nmax = easeToward(s.max, t.max, dtMs,
|
|
38646
|
+
let nmin = easeToward(s.min, t.min, dtMs, tau);
|
|
38647
|
+
let nmax = easeToward(s.max, t.max, dtMs, tau);
|
|
37398
38648
|
if (Math.abs(nmin - t.min) <= span * 1e-3 && Math.abs(nmax - t.max) <= span * 1e-3) {
|
|
37399
38649
|
nmin = t.min;
|
|
37400
38650
|
nmax = t.max;
|
|
@@ -37407,8 +38657,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
37407
38657
|
const t = sl.scaleTarget;
|
|
37408
38658
|
const s = sl.scale;
|
|
37409
38659
|
const span = Math.max(1e-9, Math.abs(t.max - t.min));
|
|
37410
|
-
let nmin = easeToward(s.min, t.min, dtMs,
|
|
37411
|
-
let nmax = easeToward(s.max, t.max, dtMs,
|
|
38660
|
+
let nmin = easeToward(s.min, t.min, dtMs, tau);
|
|
38661
|
+
let nmax = easeToward(s.max, t.max, dtMs, tau);
|
|
37412
38662
|
if (Math.abs(nmin - t.min) <= span * 1e-3 && Math.abs(nmax - t.max) <= span * 1e-3) {
|
|
37413
38663
|
nmin = t.min;
|
|
37414
38664
|
nmax = t.max;
|
|
@@ -37429,6 +38679,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
37429
38679
|
this.scene.crosshair = null;
|
|
37430
38680
|
this.hoverSeparatorY = null;
|
|
37431
38681
|
this.lastPointer = null;
|
|
38682
|
+
if (this.marksExpandedBy === "hover") this.setMarksExpanded(null);
|
|
38683
|
+
this.setMarkHover(null);
|
|
37432
38684
|
this.scheduler.invalidate(1 /* Cursor */);
|
|
37433
38685
|
this.hoverLogical = null;
|
|
37434
38686
|
const empty = { time: null, price: null, paneKind: null, values: /* @__PURE__ */ new Map(), ohlc: null };
|
|
@@ -37440,6 +38692,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
37440
38692
|
this.lastPointer = inData ? { x, y } : null;
|
|
37441
38693
|
this.hoverSeparatorY = x >= 0 && y >= 0 && y <= this.coords.height ? this.separatorHoverY(y) : null;
|
|
37442
38694
|
this.scheduler.invalidate(1 /* Cursor */);
|
|
38695
|
+
this.setMarksExpanded(inData ? this.chrome.markStackAt(x, y) : null);
|
|
38696
|
+
this.setMarkHover(inData ? this.chrome.markGlyphAt(x, y)?.cluster.key ?? null : null);
|
|
37443
38697
|
const logical = Math.round(this.coords.xToLogical(x));
|
|
37444
38698
|
const onBar = logical >= 0 && logical < this.coords.barCount;
|
|
37445
38699
|
const time = onBar ? this.coords.logicalToTime(logical) : null;
|
|
@@ -37469,6 +38723,76 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
37469
38723
|
const onBar = logical >= 0 && logical < this.coords.barCount;
|
|
37470
38724
|
for (const cb of this.clickCbs) cb({ time: onBar ? this.coords.logicalToTime(logical) : null, price: null });
|
|
37471
38725
|
}
|
|
38726
|
+
/**
|
|
38727
|
+
* Fan out (or collapse, with null) a multi-group mark stack; repaints the chrome tier when
|
|
38728
|
+
* it changes. A stack whose glyph holds the open popup stays fanned — the pointer leaving
|
|
38729
|
+
* the plot for the popup must not bury the glyph under the deck (which would close it).
|
|
38730
|
+
*/
|
|
38731
|
+
setMarksExpanded(stack, by = "hover") {
|
|
38732
|
+
if (this.scene.marksExpandedStack === stack) return;
|
|
38733
|
+
if (stack === null && this.markPopover?.key) {
|
|
38734
|
+
const open2 = this.chrome.markGlyphByKey(this.markPopover.key);
|
|
38735
|
+
if (open2 && open2.stack === this.scene.marksExpandedStack) return;
|
|
38736
|
+
}
|
|
38737
|
+
this.scene.marksExpandedStack = stack;
|
|
38738
|
+
this.marksExpandedBy = by;
|
|
38739
|
+
this.scheduler?.invalidate(2 /* Chrome */);
|
|
38740
|
+
}
|
|
38741
|
+
/**
|
|
38742
|
+
* A click on the mark lane: a collapsed deck fans out (the touch path — a mouse already
|
|
38743
|
+
* fanned it by hovering), a glyph reports its cluster (`onMarkClick`) and opens the popup
|
|
38744
|
+
* when any of its marks carries content. True when the click landed on the lane.
|
|
38745
|
+
*/
|
|
38746
|
+
handleMarkClick(x, y) {
|
|
38747
|
+
const glyph = this.chrome.markGlyphAt(x, y);
|
|
38748
|
+
if (!glyph) {
|
|
38749
|
+
if (this.scene.marksExpandedStack !== null && this.chrome.markStackAt(x, y) === null) this.setMarksExpanded(null);
|
|
38750
|
+
return false;
|
|
38751
|
+
}
|
|
38752
|
+
if (glyph.decked) {
|
|
38753
|
+
this.setMarksExpanded(glyph.stack, "tap");
|
|
38754
|
+
return true;
|
|
38755
|
+
}
|
|
38756
|
+
const marks = glyph.cluster.marks;
|
|
38757
|
+
const first2 = marks[0];
|
|
38758
|
+
const event = { id: first2.id, ids: marks.map((m) => m.id), time: first2.time, ...glyph.cluster.group !== void 0 ? { group: glyph.cluster.group } : {} };
|
|
38759
|
+
for (const cb of this.markClickCbs) cb(event);
|
|
38760
|
+
if (marks.some((m) => m.content !== void 0)) {
|
|
38761
|
+
this.markPopover?.open(glyph.cluster, { x: glyph.x, y: glyph.y, size: glyph.size });
|
|
38762
|
+
} else {
|
|
38763
|
+
this.scene.marksFlash = { key: glyph.cluster.key, until: frameNow() + MARK_FLASH_MS };
|
|
38764
|
+
this.syncMarkPulse();
|
|
38765
|
+
}
|
|
38766
|
+
return true;
|
|
38767
|
+
}
|
|
38768
|
+
/** After a chrome frame: keep the open mark popup on its glyph, or close it once the glyph is gone. */
|
|
38769
|
+
trackMarkPopover() {
|
|
38770
|
+
const key = this.markPopover?.key;
|
|
38771
|
+
if (!key) return;
|
|
38772
|
+
const g = this.chrome.markGlyphByKey(key);
|
|
38773
|
+
this.markPopover.track(g && !(g.decked && g.depth !== 0) ? { x: g.x, y: g.y, size: g.size } : null);
|
|
38774
|
+
}
|
|
38775
|
+
/** The lane glyph under the pointer — it swells once as the pointer lands (a rAF-driven chrome repaint for the pulse's duration). */
|
|
38776
|
+
setMarkHover(key) {
|
|
38777
|
+
if (this.scene.marksHoverKey === key) return;
|
|
38778
|
+
this.scene.marksHoverKey = key;
|
|
38779
|
+
this.scene.marksHoverSince = frameNow();
|
|
38780
|
+
this.scheduler?.invalidate(2 /* Chrome */);
|
|
38781
|
+
this.syncMarkPulse();
|
|
38782
|
+
}
|
|
38783
|
+
/** Run a chrome-tier repaint loop while a glyph's hover pulse or click flash plays; it stops itself once both are over. */
|
|
38784
|
+
syncMarkPulse() {
|
|
38785
|
+
if (this.markPulseRaf !== null || typeof requestAnimationFrame !== "function") return;
|
|
38786
|
+
const tick = () => {
|
|
38787
|
+
this.markPulseRaf = null;
|
|
38788
|
+
const now = frameNow();
|
|
38789
|
+
if (this.scene.marksFlash && this.scene.marksFlash.until <= now) this.scene.marksFlash = null;
|
|
38790
|
+
this.scheduler?.invalidate(2 /* Chrome */);
|
|
38791
|
+
const pulsing = this.scene.marksHoverKey !== null && now - this.scene.marksHoverSince < MARK_PULSE_MS;
|
|
38792
|
+
if (pulsing || this.scene.marksFlash !== null) this.markPulseRaf = requestAnimationFrame(tick);
|
|
38793
|
+
};
|
|
38794
|
+
this.markPulseRaf = requestAnimationFrame(tick);
|
|
38795
|
+
}
|
|
37472
38796
|
paneAtY(y) {
|
|
37473
38797
|
return this.paneNodeAtY(y);
|
|
37474
38798
|
}
|
|
@@ -37860,6 +39184,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
37860
39184
|
} else if (repaintsChrome(level) && this.paintedData) {
|
|
37861
39185
|
this.chrome.prepare(this.scene, this.coords, this.theme);
|
|
37862
39186
|
this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
|
|
39187
|
+
this.trackMarkPopover();
|
|
37863
39188
|
}
|
|
37864
39189
|
this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
|
|
37865
39190
|
if (!repaintsData(level) && this.paintedData) this.repaintCursorLayers();
|
|
@@ -37875,7 +39200,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
37875
39200
|
const lp = this.layerPane(l.def.id) ?? pane;
|
|
37876
39201
|
if (lp.collapsed) continue;
|
|
37877
39202
|
l.instance.render(this.extLayerArgs(l.def.id, lp.scale, lp.bounds, nowMs));
|
|
37878
|
-
if (this.animZoom && l.instance.animating?.()) this.animator.start();
|
|
39203
|
+
if (this.animZoom.on && l.instance.animating?.()) this.animator.start();
|
|
37879
39204
|
}
|
|
37880
39205
|
}
|
|
37881
39206
|
/** Blank one SDK layer canvas (a collapsed host pane suppresses the layer's painting). */
|
|
@@ -37940,7 +39265,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
37940
39265
|
const args = this.extLayerArgs(l.def.id, lp.scale, lp.bounds, nowMs);
|
|
37941
39266
|
l.instance.render(args);
|
|
37942
39267
|
if (lp === pane) folded = foldBaseModulation(folded, l.instance.modulateBase?.(args) ?? null);
|
|
37943
|
-
if (this.animZoom && l.instance.animating?.()) this.animator.start();
|
|
39268
|
+
if (this.animZoom.on && l.instance.animating?.()) this.animator.start();
|
|
37944
39269
|
}
|
|
37945
39270
|
if (folded) {
|
|
37946
39271
|
if (folded.candleBodyScale != null) this.backend.candleBodyScale = clamp012(folded.candleBodyScale) || 0.01;
|
|
@@ -37959,6 +39284,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
37959
39284
|
this.backdropRenderer.render(this.scene, this.coords, this.theme, gridAlpha);
|
|
37960
39285
|
this.backend.render(this.scene, this.coords, this.theme);
|
|
37961
39286
|
this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
|
|
39287
|
+
this.trackMarkPopover();
|
|
37962
39288
|
this.userDrawings?.render();
|
|
37963
39289
|
if (easeLive && liveActual) this.bars[li] = liveActual;
|
|
37964
39290
|
this.paintedData = true;
|
|
@@ -38471,7 +39797,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
38471
39797
|
const map2 = /* @__PURE__ */ new Map();
|
|
38472
39798
|
for (const pane of this.scene.panes.values()) {
|
|
38473
39799
|
if (!pane.collapsed) continue;
|
|
38474
|
-
const models = this.scene.orderedIndicatorsForPane(pane.id);
|
|
39800
|
+
const models = this.scene.orderedIndicatorsForPane(pane.id).filter((m) => m.legend !== false);
|
|
38475
39801
|
const merged = new Set(this.scene.ownScaleIndicatorsForPane(pane.id).map((m) => m.id));
|
|
38476
39802
|
const master = models.find((m) => !merged.has(m.id)) ?? models[0];
|
|
38477
39803
|
map2.set(pane.id, master?.id ?? null);
|
|
@@ -41295,6 +42621,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
41295
42621
|
if (Object.keys(defaults2).length > 0) this.rendererControl.set(defaults2);
|
|
41296
42622
|
this.panesControl = new PanesControl(this.orchestrator);
|
|
41297
42623
|
this.drawingsControl = new DrawingsControl(this.orchestrator.drawings);
|
|
42624
|
+
this.marksControl = new MarksControl(this.orchestrator.marks);
|
|
41298
42625
|
}
|
|
41299
42626
|
/**
|
|
41300
42627
|
* Register a scripting engine so `addIndicator({ language })` can run that
|
|
@@ -41513,6 +42840,17 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
41513
42840
|
get drawings() {
|
|
41514
42841
|
return this.drawingsControl;
|
|
41515
42842
|
}
|
|
42843
|
+
/**
|
|
42844
|
+
* The chart's timeline-marks control surface: host events pinned to a bar and shown
|
|
42845
|
+
* as glyphs on a lane above the time axis, each opening a popup on click —
|
|
42846
|
+
* `chart.marks.add({ id, time, glyph, title, content })`, `chart.marks.set(list)`,
|
|
42847
|
+
* `chart.marks.defineGroup({ id, label })`. Marks are data, not user state: re-supply
|
|
42848
|
+
* them on `market:changed`. On a renderer without the `timelineMarks` capability the
|
|
42849
|
+
* model still fills but nothing paints (`chart.marks.supported`).
|
|
42850
|
+
*/
|
|
42851
|
+
get marks() {
|
|
42852
|
+
return this.marksControl;
|
|
42853
|
+
}
|
|
41516
42854
|
on(event, handler) {
|
|
41517
42855
|
return this.orchestrator.events.on(event, handler);
|
|
41518
42856
|
}
|
|
@@ -42958,6 +44296,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
42958
44296
|
exports.INVALID = INVALID;
|
|
42959
44297
|
exports.LIGHT_THEME = LIGHT_THEME;
|
|
42960
44298
|
exports.MARKER = MARKER;
|
|
44299
|
+
exports.MarksControl = MarksControl;
|
|
42961
44300
|
exports.MultiProviderFeed = MultiProviderFeed;
|
|
42962
44301
|
exports.NEUTRAL = NEUTRAL;
|
|
42963
44302
|
exports.NativeRenderer = NativeRenderer;
|
|
@@ -42969,6 +44308,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
42969
44308
|
exports.SESSION_PRE = SESSION_PRE;
|
|
42970
44309
|
exports.SLATE = SLATE;
|
|
42971
44310
|
exports.SLATE_DEEP = SLATE_DEEP;
|
|
44311
|
+
exports.SecondClock = SecondClock;
|
|
42972
44312
|
exports.TRADE_EXIT = TRADE_EXIT;
|
|
42973
44313
|
exports.TRADE_LONG = TRADE_LONG;
|
|
42974
44314
|
exports.TRADE_SHORT = TRADE_SHORT;
|