@luxalgo/vela 0.6.20 → 0.6.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{DataProvider-Cb-2lC5O.d.ts → DataProvider-Bu9E5Zh5.d.ts} +1 -1
- package/dist/{DataProvider-UtWw2CgJ.d.cts → DataProvider-Ck2qyS_9.d.cts} +1 -1
- package/dist/{chunk-M6T5A733.js → chunk-A347YL2P.js} +31 -8
- package/dist/{chunk-4Q4B5AO3.js → chunk-A4G64KVF.js} +1453 -131
- package/dist/{chunk-T5Z5YUCF.js → chunk-CZRNTHD6.js} +24 -2
- package/dist/{chunk-XCBJU674.js → chunk-UHXG5J7C.js} +1 -1
- package/dist/{contributions-D8HdlKmp.d.cts → contributions-BjQ7hyIx.d.ts} +108 -3
- package/dist/{contributions-FqIWhN4p.d.ts → contributions-Cv2Mutvn.d.cts} +108 -3
- package/dist/index.cjs +1476 -130
- package/dist/index.d.cts +76 -20
- package/dist/index.d.ts +76 -20
- package/dist/index.js +2 -2
- package/dist/{options-BCRmYALw.d.ts → options-ex2_gtKp.d.cts} +200 -12
- package/dist/{options-BCRmYALw.d.cts → options-ex2_gtKp.d.ts} +200 -12
- package/dist/{plugin-B4mnNAeh.d.ts → plugin-CiyhU7pi.d.ts} +3 -3
- package/dist/{plugin-Ccupy_BV.d.cts → plugin-absNH7Yn.d.cts} +3 -3
- package/dist/plugin.d.cts +4 -4
- package/dist/plugin.d.ts +4 -4
- package/dist/providers/binance.d.cts +2 -2
- package/dist/providers/binance.d.ts +2 -2
- package/dist/providers/coinbase.d.cts +2 -2
- package/dist/providers/coinbase.d.ts +2 -2
- package/dist/providers/hyperliquid.d.cts +2 -2
- package/dist/providers/hyperliquid.d.ts +2 -2
- package/dist/{statusline-model-CCP1zm_Z.d.ts → statusline-model-BXoU4Kua.d.ts} +8 -4
- package/dist/{statusline-model-E7CMw5pQ.d.cts → statusline-model-cRhPBLPO.d.cts} +8 -4
- package/dist/ui.cjs +24 -2
- package/dist/ui.d.cts +9 -3
- package/dist/ui.d.ts +9 -3
- package/dist/ui.js +2 -2
- package/dist/vela.global.js +1476 -130
- package/dist/vela.global.min.js +77 -53
- package/dist/widget.cjs +1502 -135
- package/dist/widget.d.cts +6 -6
- package/dist/widget.d.ts +6 -6
- package/dist/widget.js +5 -5
- package/dist/workspace.cjs +1502 -135
- package/dist/workspace.d.cts +8 -5
- package/dist/workspace.d.ts +8 -5
- package/dist/workspace.js +4 -4
- package/package.json +2 -1
package/dist/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);
|
|
@@ -8107,6 +8224,7 @@ var Vela = (function (exports) {
|
|
|
8107
8224
|
if (!record.native) continue;
|
|
8108
8225
|
record.native.instance.stop();
|
|
8109
8226
|
record.native.instance = record.native.descriptor.create();
|
|
8227
|
+
record.native.started = false;
|
|
8110
8228
|
record.pendingStructural = true;
|
|
8111
8229
|
if (record.hidden) {
|
|
8112
8230
|
record.native.stale = true;
|
|
@@ -8598,11 +8716,12 @@ var Vela = (function (exports) {
|
|
|
8598
8716
|
record.session = void 0;
|
|
8599
8717
|
record.native?.instance.suspend();
|
|
8600
8718
|
if (record.renderHandle) this.renderer.setIndicatorVisible?.(record.renderHandle, false);
|
|
8719
|
+
else if (record.native) this.mountHiddenNativeRow(id, record);
|
|
8601
8720
|
} else {
|
|
8602
8721
|
if (record.renderHandle) this.renderer.setIndicatorVisible?.(record.renderHandle, true);
|
|
8603
8722
|
record.pendingStructural = true;
|
|
8604
8723
|
if (record.native) {
|
|
8605
|
-
if (record.native.stale) {
|
|
8724
|
+
if (record.native.stale || !record.native.started) {
|
|
8606
8725
|
record.native.stale = false;
|
|
8607
8726
|
const handle = this.handles.get(id);
|
|
8608
8727
|
if (handle) void this.startNativeIndicator(id, handle);
|
|
@@ -8685,6 +8804,7 @@ var Vela = (function (exports) {
|
|
|
8685
8804
|
this.unresolvedUnsub = null;
|
|
8686
8805
|
this.feed.destroy?.();
|
|
8687
8806
|
this.drawings.destroy();
|
|
8807
|
+
this.marks.destroy();
|
|
8688
8808
|
this.renderer.destroy();
|
|
8689
8809
|
this.events.clear();
|
|
8690
8810
|
}
|
|
@@ -8783,6 +8903,7 @@ var Vela = (function (exports) {
|
|
|
8783
8903
|
}
|
|
8784
8904
|
};
|
|
8785
8905
|
record.native.instance.start(ctx, record.inputValues);
|
|
8906
|
+
record.native.started = true;
|
|
8786
8907
|
} catch (err) {
|
|
8787
8908
|
this.fail(id, handle, err);
|
|
8788
8909
|
}
|
|
@@ -8797,6 +8918,7 @@ var Vela = (function (exports) {
|
|
|
8797
8918
|
overlay: d.overlay,
|
|
8798
8919
|
paneHint: d.paneHint,
|
|
8799
8920
|
native: { type: record.native.type },
|
|
8921
|
+
...d.legend === false ? { legend: false } : {},
|
|
8800
8922
|
...out.paneAxis != null ? { paneAxis: out.paneAxis } : {},
|
|
8801
8923
|
series: out.series ?? [],
|
|
8802
8924
|
fills: out.fills ?? [],
|
|
@@ -8819,9 +8941,17 @@ var Vela = (function (exports) {
|
|
|
8819
8941
|
* over it in place (`pendingStructural`), clears the spinner, and only THEN fires
|
|
8820
8942
|
* `indicator:added`/`ready` — so event semantics and `inspect()` (which skips
|
|
8821
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.
|
|
8822
8952
|
*/
|
|
8823
8953
|
mountLoadingPlaceholder(id, record) {
|
|
8824
|
-
if (record.renderHandle ||
|
|
8954
|
+
if (record.renderHandle || !record.prepared) return;
|
|
8825
8955
|
const meta = record.prepared.meta;
|
|
8826
8956
|
const model = {
|
|
8827
8957
|
id,
|
|
@@ -8845,8 +8975,34 @@ var Vela = (function (exports) {
|
|
|
8845
8975
|
this.ensurePaneFor(paneId);
|
|
8846
8976
|
record.renderHandle = this.renderer.mountIndicator(model);
|
|
8847
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
|
+
}
|
|
8848
8983
|
this.setLoading(record, true);
|
|
8849
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
|
+
}
|
|
8850
9006
|
/** Flip the record's loading state and reflect it in the legend row (spinner on/off). */
|
|
8851
9007
|
setLoading(record, loading) {
|
|
8852
9008
|
record.loading = loading;
|
|
@@ -8881,6 +9037,7 @@ var Vela = (function (exports) {
|
|
|
8881
9037
|
for (const r of this.registry.all()) {
|
|
8882
9038
|
const model = r.model;
|
|
8883
9039
|
if (!model) continue;
|
|
9040
|
+
if (model.legend === false) continue;
|
|
8884
9041
|
const paneId = model.paneId ?? "price";
|
|
8885
9042
|
if (!byPane.has(paneId)) byPane.set(paneId, []);
|
|
8886
9043
|
byPane.get(paneId).push({
|
|
@@ -9456,6 +9613,16 @@ var Vela = (function (exports) {
|
|
|
9456
9613
|
this.renderer.setLayoutMode?.(mode);
|
|
9457
9614
|
return this;
|
|
9458
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
|
+
}
|
|
9459
9626
|
};
|
|
9460
9627
|
|
|
9461
9628
|
// src/core/renderer-defaults.ts
|
|
@@ -10348,6 +10515,95 @@ var Vela = (function (exports) {
|
|
|
10348
10515
|
}
|
|
10349
10516
|
};
|
|
10350
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
|
+
|
|
10351
10607
|
// src/core/color.ts
|
|
10352
10608
|
function parseRgb(color) {
|
|
10353
10609
|
const s = color.trim();
|
|
@@ -17259,7 +17515,7 @@ ${STATIC_DECLS}
|
|
|
17259
17515
|
return { left, top, right, bottom, width: right - left, height: bottom - top };
|
|
17260
17516
|
}
|
|
17261
17517
|
function placePopover(a) {
|
|
17262
|
-
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;
|
|
17263
17519
|
const below = a.trigger.bottom + a.gap;
|
|
17264
17520
|
const above = a.trigger.top - a.pop.height - a.gap;
|
|
17265
17521
|
const fitsBelow = below + a.pop.height <= a.clamp.bottom;
|
|
@@ -17331,6 +17587,8 @@ ${STATIC_DECLS}
|
|
|
17331
17587
|
this.onKey = null;
|
|
17332
17588
|
this.onReflow = null;
|
|
17333
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;
|
|
17334
17592
|
const doc = opts.trigger.ownerDocument;
|
|
17335
17593
|
injectStyles(POPOVER_STYLE_ID, POPOVER_CSS, doc);
|
|
17336
17594
|
this.trigger = opts.trigger;
|
|
@@ -17338,6 +17596,7 @@ ${STATIC_DECLS}
|
|
|
17338
17596
|
this.ctrl = popoverController(opts);
|
|
17339
17597
|
this.boundary = opts.boundary ?? "viewport";
|
|
17340
17598
|
this.theme = opts.theme;
|
|
17599
|
+
this.fadeMs = Math.max(0, opts.fadeMs ?? 0);
|
|
17341
17600
|
this.el = doc.createElement("div");
|
|
17342
17601
|
this.el.className = "vela-popover vela-ui-layer" + (opts.className ? ` ${opts.className}` : "");
|
|
17343
17602
|
this.el.dataset.position = this.ctrl.position;
|
|
@@ -17361,11 +17620,21 @@ ${STATIC_DECLS}
|
|
|
17361
17620
|
return;
|
|
17362
17621
|
}
|
|
17363
17622
|
if (open && open !== this) open.hide();
|
|
17623
|
+
if (this.leaveTimer !== null) {
|
|
17624
|
+
clearTimeout(this.leaveTimer);
|
|
17625
|
+
this.leaveTimer = null;
|
|
17626
|
+
}
|
|
17364
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
|
+
}
|
|
17365
17633
|
this.host.appendChild(this.el);
|
|
17366
17634
|
this.shown = true;
|
|
17367
17635
|
open = this;
|
|
17368
17636
|
this.place();
|
|
17637
|
+
if (this.fadeMs > 0) this.el.style.opacity = "1";
|
|
17369
17638
|
const onOutside = (ev) => {
|
|
17370
17639
|
const t = ev.target;
|
|
17371
17640
|
if (this.el.contains(t) || this.trigger.contains(t)) return;
|
|
@@ -17398,7 +17667,16 @@ ${STATIC_DECLS}
|
|
|
17398
17667
|
this.onOutside = null;
|
|
17399
17668
|
this.onKey = null;
|
|
17400
17669
|
this.onReflow = null;
|
|
17401
|
-
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
|
+
}
|
|
17402
17680
|
this.shown = false;
|
|
17403
17681
|
if (open === this) open = null;
|
|
17404
17682
|
this.ctrl.onClose?.();
|
|
@@ -22794,6 +23072,8 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
|
|
|
22794
23072
|
// canvas-painted into the owning indicator's interleave slice
|
|
22795
23073
|
trades: true,
|
|
22796
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)
|
|
22797
23077
|
inputsUI: true
|
|
22798
23078
|
// reuses the DOM InputsUI
|
|
22799
23079
|
};
|
|
@@ -23066,9 +23346,12 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
|
|
|
23066
23346
|
const gh = asObject(grid.horzLines);
|
|
23067
23347
|
const cross = asObject(p.crosshair);
|
|
23068
23348
|
const ps = asObject(p.priceScale);
|
|
23349
|
+
const anim = asObject(p.animations);
|
|
23069
23350
|
const panes = asObject(p.panes);
|
|
23070
23351
|
const trades = asObject(p.trades);
|
|
23071
23352
|
const ts = asObject(p.timeScale);
|
|
23353
|
+
const marks = asObject(p.marks);
|
|
23354
|
+
const markGroups = asObject(marks.groups);
|
|
23072
23355
|
const candles = asObject(p.candles);
|
|
23073
23356
|
const bars = asObject(p.bars);
|
|
23074
23357
|
const line = asObject(p.line);
|
|
@@ -23115,6 +23398,12 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
|
|
|
23115
23398
|
countdown: isBool(ps.countdown) ? ps.countdown : base.priceScale.countdown,
|
|
23116
23399
|
animateLastPrice: isBool(ps.animateLastPrice) ? ps.animateLastPrice : base.priceScale.animateLastPrice
|
|
23117
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
|
+
},
|
|
23118
23407
|
panes: {
|
|
23119
23408
|
separatorColor: isColor(panes.separatorColor) ? panes.separatorColor : base.panes.separatorColor
|
|
23120
23409
|
},
|
|
@@ -23129,6 +23418,15 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
|
|
|
23129
23418
|
timeScale: {
|
|
23130
23419
|
timezone: typeof ts.timezone === "string" && ts.timezone ? ts.timezone : base.timeScale.timezone
|
|
23131
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
|
+
},
|
|
23132
23430
|
candles: {
|
|
23133
23431
|
upColor: isColor(candles.upColor) ? candles.upColor : base.candles.upColor,
|
|
23134
23432
|
downColor: isColor(candles.downColor) ? candles.downColor : base.candles.downColor,
|
|
@@ -24647,6 +24945,31 @@ void main() {
|
|
|
24647
24945
|
}
|
|
24648
24946
|
}
|
|
24649
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
|
+
};
|
|
24650
24973
|
function easeToward(current, target, dtMs, tauMs) {
|
|
24651
24974
|
if (tauMs <= 0) return target;
|
|
24652
24975
|
return current + (target - current) * (1 - Math.exp(-dtMs / tauMs));
|
|
@@ -25370,6 +25693,25 @@ void main() {
|
|
|
25370
25693
|
for (let i = 0; i < lines.length; i += 1) ctx.fillText(lines[i], x, firstY + i * step);
|
|
25371
25694
|
}
|
|
25372
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
|
+
|
|
25373
25715
|
// src/renderers/native/core/SceneGraph.ts
|
|
25374
25716
|
var SceneGraph = class {
|
|
25375
25717
|
constructor() {
|
|
@@ -25433,6 +25775,20 @@ void main() {
|
|
|
25433
25775
|
/** Strategy trade-marker display (the `tradeMarkers` feature): master toggle, the
|
|
25434
25776
|
* two text lines, and the palette. Trade markers always paint on the price pane. */
|
|
25435
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;
|
|
25436
25792
|
/** Renderer-owned shaded time bands (session highlighting), behind grid + data. */
|
|
25437
25793
|
this.highlights = [];
|
|
25438
25794
|
/** Pre/post-market bands pushed by the host (`sessionZones` feature); null ⇒ no sessions. */
|
|
@@ -27281,6 +27637,315 @@ void main() {
|
|
|
27281
27637
|
return out;
|
|
27282
27638
|
}
|
|
27283
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
|
+
|
|
27284
27949
|
// src/renderers/native/chrome/ChromeRenderer.ts
|
|
27285
27950
|
var ChromeRenderer = class {
|
|
27286
27951
|
constructor() {
|
|
@@ -27290,11 +27955,40 @@ void main() {
|
|
|
27290
27955
|
this.axisTextColor = DARK_THEME.textColor;
|
|
27291
27956
|
// Shared Pine-drawing renderer, used here for autoscale geometry only; widthCache persists.
|
|
27292
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 = [];
|
|
27293
27966
|
}
|
|
27294
27967
|
mount(canvas) {
|
|
27295
27968
|
this.canvas = canvas;
|
|
27296
27969
|
this.ctx = canvas.getContext("2d");
|
|
27297
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
|
+
}
|
|
27298
27992
|
/** Wire the drawing coordinate resolvers + theme (call once per frame before use). */
|
|
27299
27993
|
prepare(scene, coords, theme) {
|
|
27300
27994
|
this.drawScene.setDeps({
|
|
@@ -27343,6 +28037,7 @@ void main() {
|
|
|
27343
28037
|
const panes = scene.orderedPanes();
|
|
27344
28038
|
if (coords.barCount === 0) {
|
|
27345
28039
|
this.drawPaneSeparators(ctx, scene, theme, fullW, panes);
|
|
28040
|
+
this.markLayout = { glyphs: [], stacks: /* @__PURE__ */ new Map() };
|
|
27346
28041
|
return;
|
|
27347
28042
|
}
|
|
27348
28043
|
const pricePane = panes.find((p) => p.kind === "price") ?? null;
|
|
@@ -27356,6 +28051,46 @@ void main() {
|
|
|
27356
28051
|
this.drawPaneSeparators(ctx, scene, theme, fullW, panes);
|
|
27357
28052
|
this.drawPriceLineAndCountdown(ctx, scene, coords, theme, dataW, pricePane);
|
|
27358
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;
|
|
27359
28094
|
}
|
|
27360
28095
|
destroy() {
|
|
27361
28096
|
this.canvas = null;
|
|
@@ -27471,7 +28206,8 @@ void main() {
|
|
|
27471
28206
|
* - the countdown-to-bar-close chip (`showCountdown`).
|
|
27472
28207
|
* When the label and countdown are both on they merge into one stacked block (countdown
|
|
27473
28208
|
* under the label, text flushed left); a lone label or countdown is centered on the
|
|
27474
|
-
* 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.
|
|
27475
28211
|
*/
|
|
27476
28212
|
drawPriceLineAndCountdown(ctx, scene, coords, theme, dataW, pricePane) {
|
|
27477
28213
|
const n = scene.bars.length;
|
|
@@ -27491,17 +28227,16 @@ void main() {
|
|
|
27491
28227
|
ctx.stroke();
|
|
27492
28228
|
setDash2(ctx, "solid");
|
|
27493
28229
|
}
|
|
27494
|
-
const
|
|
27495
|
-
const showCountdown =
|
|
28230
|
+
const cdText = scene.showCountdown ? countdownText(last2.time, coords.barInterval, Date.now()) : null;
|
|
28231
|
+
const showCountdown = cdText !== null;
|
|
27496
28232
|
const showLabel = scene.showPriceLabel;
|
|
27497
28233
|
if (!showLabel && !showCountdown) return;
|
|
27498
28234
|
const priceText = formatAxisValue(pricePane.scale, pricePane.bounds.height, last2.close, percentScaleFor(scene, pricePane), scene.priceMintick);
|
|
27499
|
-
const cdText = showCountdown ? formatCountdown(last2.time + interval - Date.now()) : "";
|
|
27500
28235
|
const PAD = 8;
|
|
27501
28236
|
const x = dataW + 1;
|
|
27502
28237
|
const textColor = tagTextColor(color, theme.background);
|
|
27503
28238
|
ctx.textBaseline = "middle";
|
|
27504
|
-
if (showLabel &&
|
|
28239
|
+
if (showLabel && cdText !== null) {
|
|
27505
28240
|
const w2 = Math.max(ctx.measureText(priceText).width, ctx.measureText(cdText).width) + PAD;
|
|
27506
28241
|
const top = y - 8;
|
|
27507
28242
|
const tx = x + PAD / 2;
|
|
@@ -27514,7 +28249,7 @@ void main() {
|
|
|
27514
28249
|
ctx.textAlign = "start";
|
|
27515
28250
|
return;
|
|
27516
28251
|
}
|
|
27517
|
-
const text = showLabel ? priceText : cdText;
|
|
28252
|
+
const text = showLabel ? priceText : cdText ?? "";
|
|
27518
28253
|
const w = ctx.measureText(text).width + PAD;
|
|
27519
28254
|
ctx.fillStyle = color;
|
|
27520
28255
|
ctx.fillRect(x, y - 8, w, 16);
|
|
@@ -27585,29 +28320,6 @@ void main() {
|
|
|
27585
28320
|
else if (style === "dotted") ctx.setLineDash([2, 3]);
|
|
27586
28321
|
else ctx.setLineDash([]);
|
|
27587
28322
|
}
|
|
27588
|
-
function tagTextColor(bg, over) {
|
|
27589
|
-
const [r, g, b, a] = parseColor(bg);
|
|
27590
|
-
let R = r;
|
|
27591
|
-
let G = g;
|
|
27592
|
-
let B = b;
|
|
27593
|
-
if (a < 1) {
|
|
27594
|
-
const [or2, og, ob] = parseColor(over);
|
|
27595
|
-
R = r * a + or2 * (1 - a);
|
|
27596
|
-
G = g * a + og * (1 - a);
|
|
27597
|
-
B = b * a + ob * (1 - a);
|
|
27598
|
-
}
|
|
27599
|
-
const lin = (c) => c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
27600
|
-
const L = 0.2126 * lin(R) + 0.7152 * lin(G) + 0.0722 * lin(B);
|
|
27601
|
-
return L >= 0.4 ? "#000000" : "#ffffff";
|
|
27602
|
-
}
|
|
27603
|
-
function formatCountdown(ms) {
|
|
27604
|
-
const total = Math.max(0, Math.floor(ms / 1e3));
|
|
27605
|
-
const s = total % 60;
|
|
27606
|
-
const m = Math.floor(total / 60) % 60;
|
|
27607
|
-
const h = Math.floor(total / 3600);
|
|
27608
|
-
const pad = (v) => String(v).padStart(2, "0");
|
|
27609
|
-
return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`;
|
|
27610
|
-
}
|
|
27611
28323
|
|
|
27612
28324
|
// src/renderers/native/chrome/LabelTooltip.ts
|
|
27613
28325
|
var HOVER_DELAY_MS = 350;
|
|
@@ -27890,6 +28602,11 @@ void main() {
|
|
|
27890
28602
|
function settingsIdSlug(label) {
|
|
27891
28603
|
return label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
27892
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
|
+
}
|
|
27893
28610
|
function settingsIdHidden(id, hidden) {
|
|
27894
28611
|
if (hidden.size === 0) return false;
|
|
27895
28612
|
let path = id;
|
|
@@ -27983,7 +28700,11 @@ void main() {
|
|
|
27983
28700
|
"symbol.style.baseline.base-level",
|
|
27984
28701
|
"symbol.style.baseline.width",
|
|
27985
28702
|
"symbol.animation",
|
|
28703
|
+
"symbol.animation.zoom",
|
|
28704
|
+
"symbol.animation.pan",
|
|
28705
|
+
"symbol.animation.autoscale",
|
|
27986
28706
|
"symbol.animation.price-changes",
|
|
28707
|
+
"symbol.animation.intro",
|
|
27987
28708
|
"symbol.timezone",
|
|
27988
28709
|
"scales",
|
|
27989
28710
|
"scales.price-scale",
|
|
@@ -28009,8 +28730,13 @@ void main() {
|
|
|
28009
28730
|
"canvas.grid.horizontal",
|
|
28010
28731
|
"canvas.theme"
|
|
28011
28732
|
];
|
|
28012
|
-
function settingsIdCatalog(hostSections) {
|
|
28733
|
+
function settingsIdCatalog(hostSections, markGroups = []) {
|
|
28013
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
|
+
}
|
|
28014
28740
|
for (const def of chartTypes()) {
|
|
28015
28741
|
if (hasOwnCandlePaint(def.id)) {
|
|
28016
28742
|
const style = `symbol.style.${def.id}`;
|
|
@@ -28050,7 +28776,7 @@ void main() {
|
|
|
28050
28776
|
return chartType(id)?.label ?? BUILTIN_STYLE_LABELS[id] ?? id;
|
|
28051
28777
|
}
|
|
28052
28778
|
var SD_STYLE_ID = "vela-settings-controls";
|
|
28053
|
-
var SD_STYLE_REV = "
|
|
28779
|
+
var SD_STYLE_REV = "6";
|
|
28054
28780
|
var SETTINGS_BORDER = "var(--vela-border)";
|
|
28055
28781
|
function ensureControlStyles() {
|
|
28056
28782
|
if (typeof document === "undefined") return;
|
|
@@ -28126,7 +28852,15 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
28126
28852
|
.vela-sd-mobile .vela-select-trigger,.vela-sd-mobile .vela-num input,.vela-sd-mobile .vela-width-field{height:34px;}
|
|
28127
28853
|
.vela-sd-mobile .vela-sd-close{width:40px;height:40px;}
|
|
28128
28854
|
.vela-sd-mobile .vela-sd-btn{height:38px;}
|
|
28129
|
-
.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);}`;
|
|
28130
28864
|
if (!existing) document.head.appendChild(st);
|
|
28131
28865
|
}
|
|
28132
28866
|
var SettingsDialog = class {
|
|
@@ -28141,6 +28875,9 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
28141
28875
|
this.config = null;
|
|
28142
28876
|
this.syncTypeTabs = null;
|
|
28143
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;
|
|
28144
28881
|
/** The Canvas → Theme row: current app theme + where a pick is raised. The row is a
|
|
28145
28882
|
* host callback, NOT a config patch — the app theme stays out of the persisted
|
|
28146
28883
|
* `ChartConfig`, so exported templates never carry it. */
|
|
@@ -28165,6 +28902,11 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
28165
28902
|
setHostSections(sections) {
|
|
28166
28903
|
this.hostSections = sections;
|
|
28167
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
|
+
}
|
|
28168
28910
|
/** Replace the visibility policy — an open dialog rebuilds in place to honor it. */
|
|
28169
28911
|
setHiddenSettings(ids) {
|
|
28170
28912
|
const next2 = new Set(ids);
|
|
@@ -28338,12 +29080,36 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
28338
29080
|
}
|
|
28339
29081
|
showActive(config.series.style);
|
|
28340
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"));
|
|
28341
29101
|
body.append(sid(this.boolRow(
|
|
28342
29102
|
"Animate price changes",
|
|
28343
29103
|
config.priceScale.animateLastPrice,
|
|
28344
29104
|
(v) => this.emit({ priceScale: { animateLastPrice: v } }),
|
|
28345
29105
|
this.hint("Glide the live bar to each new price instead of snapping.")
|
|
28346
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"));
|
|
28347
29113
|
body.append(sid(this.sectionTitle("Time zone"), "symbol.timezone"));
|
|
28348
29114
|
body.append(sid(this.selectRowLabeled("Time zone", normalizeTimezone(config.timeScale.timezone), timezoneOptions(config.timeScale.timezone), (v) => this.emit({ timeScale: { timezone: v } })), "symbol.timezone"));
|
|
28349
29115
|
const renderHostSections = (placement) => {
|
|
@@ -28413,6 +29179,13 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
28413
29179
|
body.append(sid(this.sectionTitle("Theme"), "canvas.theme"));
|
|
28414
29180
|
body.append(sid(this.selectRow("Color theme", tc.current === "dark" ? "Dark" : "Light", ["Dark", "Light"], (v) => tc.onSelect(v === "Dark" ? "dark" : "light")), "canvas.theme"));
|
|
28415
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
|
+
}
|
|
28416
29189
|
renderChartTypeSections("end");
|
|
28417
29190
|
renderHostSections("end");
|
|
28418
29191
|
if (this.hiddenSettings.size > 0) {
|
|
@@ -34617,6 +35390,383 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
34617
35390
|
return { ...scale, min: scale.min - belowPx * perPx, max: scale.max + abovePx * perPx };
|
|
34618
35391
|
}
|
|
34619
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
|
+
|
|
34620
35770
|
// src/renderers/native/core/manualScale.ts
|
|
34621
35771
|
function rescaleAround(start, factor) {
|
|
34622
35772
|
if (start.log && start.min > 0 && start.max > start.min) {
|
|
@@ -35345,11 +36495,12 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
35345
36495
|
var SCROLL_BTN_PROXIMITY_PX = 120;
|
|
35346
36496
|
var MIN_VISIBLE_BARS = 2;
|
|
35347
36497
|
var ZOOM_OUT_MARGIN_BARS = 6;
|
|
35348
|
-
var
|
|
35349
|
-
var SCALE_TAU_MS = 80;
|
|
35350
|
-
var FLING_TAU_MS = 110;
|
|
35351
|
-
var SCROLL_TO_TAU_MS = 130;
|
|
36498
|
+
var INTRO_MODEL_FADE_MS = 350;
|
|
35352
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
|
+
}
|
|
35353
36504
|
var PRICE_SCALE_K = 4e-3;
|
|
35354
36505
|
var KEY_ZOOM_STEP = 0.2;
|
|
35355
36506
|
var SEPARATOR_HIT_PX = 4;
|
|
@@ -35398,9 +36549,20 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
35398
36549
|
this.indicatorSlices = new IndicatorDrawingSlices();
|
|
35399
36550
|
/** Hover tooltips for Pine labels (canvas hit-rects collected by the chrome layer). */
|
|
35400
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();
|
|
35401
36559
|
this.crosshairLayer = new CrosshairRenderer();
|
|
35402
|
-
/**
|
|
35403
|
-
|
|
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;
|
|
35404
36566
|
this.symbolPicker = null;
|
|
35405
36567
|
/** Indicator titles (the legend rows) shown — held here so a remount re-applies it. */
|
|
35406
36568
|
this.indicatorTitlesOn = true;
|
|
@@ -35418,19 +36580,25 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
35418
36580
|
/** Drawings layer self-serves Ctrl+Z/Y (see the `historyChords` feature). */
|
|
35419
36581
|
this.historyChordsEnabled = true;
|
|
35420
36582
|
this.liveRegion = null;
|
|
35421
|
-
// ── animation state
|
|
35422
|
-
|
|
35423
|
-
this.
|
|
35424
|
-
|
|
35425
|
-
|
|
35426
|
-
|
|
35427
|
-
|
|
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
|
|
35428
36595
|
// Brand default candles.
|
|
35429
36596
|
this.candleUp = BULLISH;
|
|
35430
36597
|
this.candleDown = BEARISH;
|
|
35431
36598
|
// ── intro reveal (plays once when candles first appear) ──
|
|
35432
|
-
this.
|
|
35433
|
-
|
|
36599
|
+
this.intro = { style: "settle", duration: INTRO_DURATION_DEFAULT_MS };
|
|
36600
|
+
this.introOnStyle = "settle";
|
|
36601
|
+
// the style the config's on/off switch restores
|
|
35434
36602
|
this.introPlayed = false;
|
|
35435
36603
|
this.introRaf = null;
|
|
35436
36604
|
/** The load affordance (three pulsing dots) — up while the host reports a bar load in
|
|
@@ -35549,7 +36717,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
35549
36717
|
this.moveIndicatorCbs = /* @__PURE__ */ new Set();
|
|
35550
36718
|
this.priceStyleCbs = /* @__PURE__ */ new Set();
|
|
35551
36719
|
this.name = "native";
|
|
35552
|
-
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"];
|
|
35553
36721
|
/** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
|
|
35554
36722
|
* so moving onto the button doesn't count as leaving). */
|
|
35555
36723
|
this.onScrollProximityMove = (e) => {
|
|
@@ -35581,9 +36749,12 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
35581
36749
|
this.scene.showPriceLine = opts.currentPriceLine;
|
|
35582
36750
|
this.scene.logScale = opts.logScale;
|
|
35583
36751
|
this.backendMode = opts.nativeBackend;
|
|
35584
|
-
this.animZoom
|
|
35585
|
-
this.animPan
|
|
35586
|
-
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);
|
|
35587
36758
|
this.glowAmount = opts.glow;
|
|
35588
36759
|
this.candleUp = opts.upColor;
|
|
35589
36760
|
this.candleDown = opts.downColor;
|
|
@@ -35627,20 +36798,29 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
35627
36798
|
if (this.backend && "glow" in this.backend) this.backend.glow = this.glowAmount;
|
|
35628
36799
|
break;
|
|
35629
36800
|
case "animZoom":
|
|
35630
|
-
this.animZoom
|
|
36801
|
+
this.animZoom.set(resolveEaseMs(value, ZOOM_EASE_DEFAULT_MS));
|
|
35631
36802
|
return;
|
|
35632
36803
|
// affects the next interaction only — nothing to repaint
|
|
35633
|
-
case "animPan":
|
|
35634
|
-
|
|
36804
|
+
case "animPan": {
|
|
36805
|
+
const ms = resolveEaseMs(value, PAN_INERTIA_DEFAULT_MS);
|
|
36806
|
+
this.animPan.set(ms);
|
|
36807
|
+
this.animScroll.toggle(ms > 0);
|
|
35635
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));
|
|
36815
|
+
return;
|
|
36816
|
+
// a glide in flight finishes at the new rate (or snaps at 0)
|
|
35636
36817
|
case "animLiveBar":
|
|
35637
|
-
this.
|
|
36818
|
+
this.animLiveBar.set(resolveLiveBarEaseMs(value));
|
|
35638
36819
|
return;
|
|
35639
36820
|
// affects the next tick only; a glide in flight finishes at the new rate (or snaps at 0)
|
|
35640
36821
|
case "intro": {
|
|
35641
|
-
|
|
35642
|
-
this.
|
|
35643
|
-
if (s) this.playIntro(s);
|
|
36822
|
+
this.setIntro(resolveIntro(value));
|
|
36823
|
+
if (this.intro.style) this.playIntro();
|
|
35644
36824
|
return;
|
|
35645
36825
|
}
|
|
35646
36826
|
case "zoomAnchor":
|
|
@@ -35712,6 +36892,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
35712
36892
|
case "tradeMarkers":
|
|
35713
36893
|
this.scene.tradeMarkers = mergeTradeMarkersState(this.scene.tradeMarkers, value);
|
|
35714
36894
|
break;
|
|
36895
|
+
case "marks":
|
|
36896
|
+
this.scene.marks = mergeMarksState(this.scene.marks, value);
|
|
36897
|
+
this.markPopover?.close();
|
|
36898
|
+
break;
|
|
35715
36899
|
case "keyboard":
|
|
35716
36900
|
this.setKeyboardEnabled(Boolean(value));
|
|
35717
36901
|
return;
|
|
@@ -35770,13 +36954,17 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
35770
36954
|
case "glow":
|
|
35771
36955
|
return this.glowAmount;
|
|
35772
36956
|
case "animZoom":
|
|
35773
|
-
return this.animZoom;
|
|
36957
|
+
return this.animZoom.tau;
|
|
35774
36958
|
case "animPan":
|
|
35775
|
-
return this.animPan;
|
|
36959
|
+
return this.animPan.tau;
|
|
36960
|
+
case "animScroll":
|
|
36961
|
+
return this.animScroll.tau;
|
|
36962
|
+
case "animAutoscale":
|
|
36963
|
+
return this.animAutoscale.tau;
|
|
35776
36964
|
case "animLiveBar":
|
|
35777
|
-
return this.
|
|
36965
|
+
return this.animLiveBar.tau;
|
|
35778
36966
|
case "intro":
|
|
35779
|
-
return this.
|
|
36967
|
+
return this.intro.style;
|
|
35780
36968
|
case "zoomAnchor":
|
|
35781
36969
|
return this.zoomAnchorMode;
|
|
35782
36970
|
case "axisDrag":
|
|
@@ -35819,6 +37007,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
35819
37007
|
}
|
|
35820
37008
|
case "tradeMarkers":
|
|
35821
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 } };
|
|
35822
37012
|
case "keyboard":
|
|
35823
37013
|
return this.keyboardEnabled;
|
|
35824
37014
|
case "historyChords":
|
|
@@ -35944,7 +37134,13 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
35944
37134
|
currentPriceLine: this.scene.showPriceLine,
|
|
35945
37135
|
priceLabel: this.scene.showPriceLabel,
|
|
35946
37136
|
countdown: this.scene.showCountdown,
|
|
35947
|
-
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
|
|
35948
37144
|
},
|
|
35949
37145
|
panes: { separatorColor: s.separatorColor ?? t.borderColor },
|
|
35950
37146
|
trades: {
|
|
@@ -35956,6 +37152,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
35956
37152
|
exitColor: this.scene.tradeMarkers.colors.exit
|
|
35957
37153
|
},
|
|
35958
37154
|
timeScale: { timezone: this.scene.timezone },
|
|
37155
|
+
marks: { visible: this.scene.marks.visible, groups: { ...this.scene.marks.groups } },
|
|
35959
37156
|
candles: {
|
|
35960
37157
|
upColor: this.candleUp,
|
|
35961
37158
|
downColor: this.candleDown,
|
|
@@ -36051,7 +37248,12 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
36051
37248
|
this.scene.showPriceLabel = next2.priceScale.priceLabel;
|
|
36052
37249
|
this.scene.showCountdown = next2.priceScale.countdown;
|
|
36053
37250
|
this.syncCountdownTimer();
|
|
36054
|
-
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 };
|
|
36055
37257
|
s.separatorColor = keepInherit(s.separatorColor, next2.panes.separatorColor, prevTheme.borderColor);
|
|
36056
37258
|
this.scene.tradeMarkers = {
|
|
36057
37259
|
visible: next2.trades.visible,
|
|
@@ -36060,6 +37262,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
36060
37262
|
colors: { long: next2.trades.longColor, short: next2.trades.shortColor, exit: next2.trades.exitColor }
|
|
36061
37263
|
};
|
|
36062
37264
|
this.scene.timezone = next2.timeScale.timezone;
|
|
37265
|
+
this.scene.marks = { visible: next2.marks.visible, groups: { ...next2.marks.groups } };
|
|
36063
37266
|
this.candleUp = next2.candles.upColor;
|
|
36064
37267
|
this.candleDown = next2.candles.downColor;
|
|
36065
37268
|
s.candle = {
|
|
@@ -36192,6 +37395,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
36192
37395
|
}
|
|
36193
37396
|
this.settingsDialog.setTheme(this.theme);
|
|
36194
37397
|
this.settingsDialog.setHostSections(this.hostSettingsSections);
|
|
37398
|
+
this.settingsDialog.setMarkGroups(this.markGroupsInUse(), (id) => markGroupVisible(this.scene.marks, id, this.scene.markGroups));
|
|
36195
37399
|
this.settingsDialog.setHiddenSettings(this.hiddenSettings);
|
|
36196
37400
|
this.syncThemeControl();
|
|
36197
37401
|
this.settingsDialog.toggle(
|
|
@@ -36282,10 +37486,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
36282
37486
|
this.glideRightOffset(ZOOM_OUT_MARGIN_BARS);
|
|
36283
37487
|
}
|
|
36284
37488
|
/** Ease rightOffset to `target` at constant zoom (see animTick's scroll glide);
|
|
36285
|
-
* instant when
|
|
37489
|
+
* instant when the scroll glide is off. Shared by scroll-to-latest and panBy. */
|
|
36286
37490
|
glideRightOffset(target) {
|
|
36287
37491
|
const vp = this.coords.getViewport();
|
|
36288
|
-
if (!this.
|
|
37492
|
+
if (!this.animScroll.on) {
|
|
36289
37493
|
this.applyViewport({ barSpacing: vp.barSpacing, rightOffset: target });
|
|
36290
37494
|
return;
|
|
36291
37495
|
}
|
|
@@ -36350,20 +37554,21 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
36350
37554
|
* full size, eased, with a left→right stagger so the chart draws itself; `settle`
|
|
36351
37555
|
* adds an ease-out-back overshoot. Autoscale stays on the real bars so the frame
|
|
36352
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.
|
|
36353
37558
|
*/
|
|
36354
|
-
playIntro(
|
|
37559
|
+
playIntro() {
|
|
36355
37560
|
if (this.introRaf != null) cancelAnimationFrame(this.introRaf);
|
|
36356
37561
|
this.introRaf = null;
|
|
37562
|
+
const { style, duration } = this.intro;
|
|
36357
37563
|
const real = this.bars;
|
|
36358
37564
|
const n = real.length;
|
|
36359
|
-
if (n === 0) return;
|
|
37565
|
+
if (n === 0 || !style) return;
|
|
36360
37566
|
this.computeScales();
|
|
36361
37567
|
for (const pane of this.scene.panes.values()) pane.scale = { ...pane.scaleTarget };
|
|
36362
37568
|
this.modelAlpha = 0;
|
|
36363
|
-
const DURATION = 650;
|
|
36364
37569
|
const start = performance.now();
|
|
36365
37570
|
const step = (now) => {
|
|
36366
|
-
const p = Math.min(1, (now - start) /
|
|
37571
|
+
const p = Math.min(1, (now - start) / duration);
|
|
36367
37572
|
this.scene.bars = p >= 1 ? real : real.map((b, i) => this.revealCandle(b, i, p, n, style));
|
|
36368
37573
|
this.paintData();
|
|
36369
37574
|
if (p < 1) {
|
|
@@ -36377,10 +37582,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
36377
37582
|
}
|
|
36378
37583
|
/** After the candle reveal, fade the indicator models (series/fills/…) from hidden to full. */
|
|
36379
37584
|
fadeInModels() {
|
|
36380
|
-
const
|
|
37585
|
+
const fade = Math.min(INTRO_MODEL_FADE_MS, this.intro.duration || INTRO_MODEL_FADE_MS);
|
|
36381
37586
|
const start = performance.now();
|
|
36382
37587
|
const step = (now) => {
|
|
36383
|
-
this.modelAlpha = Math.min(1, (now - start) /
|
|
37588
|
+
this.modelAlpha = Math.min(1, (now - start) / fade);
|
|
36384
37589
|
this.paintData();
|
|
36385
37590
|
if (this.modelAlpha < 1) {
|
|
36386
37591
|
this.introRaf = requestAnimationFrame(step);
|
|
@@ -36495,7 +37700,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
36495
37700
|
zoomTo: (target, anchorLogical, anchorX) => this.zoomTo(target, anchorLogical, anchorX),
|
|
36496
37701
|
fling: (v) => this.fling(v),
|
|
36497
37702
|
onPointerMove: (x, y) => this.handlePointerMove(x, y),
|
|
36498
|
-
onClick: (x) => {
|
|
37703
|
+
onClick: (x, y) => {
|
|
37704
|
+
if (this.handleMarkClick(x, y)) return;
|
|
36499
37705
|
this.userDrawings?.deselect();
|
|
36500
37706
|
this.handleClick(x);
|
|
36501
37707
|
},
|
|
@@ -36524,7 +37730,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
36524
37730
|
drawingsPointerDown: (x, y, snap, shift4, mod) => this.userDrawings?.pointerDown(x, y, snap, shift4, mod),
|
|
36525
37731
|
drawingsPointerMove: (x, y, snap, shift4, mod) => this.userDrawings?.pointerMove(x, y, snap, shift4, mod),
|
|
36526
37732
|
drawingsPointerUp: (x, y, snap) => this.userDrawings?.pointerUp(x, y, snap),
|
|
36527
|
-
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),
|
|
36528
37734
|
drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
|
|
36529
37735
|
drawingsClearTransient: () => this.userDrawings?.clearTransient()
|
|
36530
37736
|
});
|
|
@@ -36540,8 +37746,18 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
36540
37746
|
this.plot.addEventListener("pointerleave", this.onScrollProximityLeave);
|
|
36541
37747
|
this.labelTooltip = new LabelTooltip(this.plot, {
|
|
36542
37748
|
theme: () => this.chromeTheme(),
|
|
36543
|
-
lookup: (x, y) => this.indicatorSlices.labelTooltipAt(x, y)
|
|
37749
|
+
lookup: (x, y) => this.indicatorSlices.labelTooltipAt(x, y) ?? this.chrome.markTooltipAt(x, y, this.markGroupsInUse())
|
|
36544
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
|
+
}
|
|
37759
|
+
});
|
|
37760
|
+
this.chrome.setMarkIconReady(() => this.scheduler?.invalidate(2 /* Chrome */));
|
|
36545
37761
|
this.userDrawings = new UserDrawingController(this.wrapper, this.plot, this.drawingsCanvas, {
|
|
36546
37762
|
projector: () => this.drawingProjector(),
|
|
36547
37763
|
dpr: () => this.coords.dpr,
|
|
@@ -36756,29 +37972,40 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
36756
37972
|
resize() {
|
|
36757
37973
|
this.syncSize();
|
|
36758
37974
|
}
|
|
36759
|
-
/**
|
|
36760
|
-
|
|
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
|
|
36761
37987
|
* scales or repaint the geometry/volume/VPVR/SDK layers once a second (that cost
|
|
36762
37988
|
* multiplies by the cell count in a multi-chart workspace). */
|
|
36763
37989
|
syncCountdownTimer() {
|
|
36764
37990
|
if (this.scene.showCountdown) {
|
|
36765
|
-
if (this.
|
|
36766
|
-
this.
|
|
37991
|
+
if (this.countdownUnsub == null) {
|
|
37992
|
+
const clock = this.hostClock ?? (this.ownClock ?? (this.ownClock = new SecondClock()));
|
|
37993
|
+
this.countdownUnsub = clock.onTick(() => {
|
|
36767
37994
|
if (this.scene.showCountdown && this.scene.bars.length > 0) this.scheduler?.invalidate(2 /* Chrome */);
|
|
36768
|
-
}
|
|
37995
|
+
});
|
|
36769
37996
|
}
|
|
36770
|
-
} else if (this.
|
|
36771
|
-
|
|
36772
|
-
this.
|
|
37997
|
+
} else if (this.countdownUnsub != null) {
|
|
37998
|
+
this.countdownUnsub();
|
|
37999
|
+
this.countdownUnsub = null;
|
|
36773
38000
|
}
|
|
36774
38001
|
}
|
|
36775
38002
|
destroy() {
|
|
36776
38003
|
if (this.introRaf != null) cancelAnimationFrame(this.introRaf);
|
|
36777
38004
|
this.loadingEl?.remove();
|
|
36778
38005
|
this.loadingEl = null;
|
|
36779
|
-
if (this.
|
|
36780
|
-
|
|
36781
|
-
this.
|
|
38006
|
+
if (this.countdownUnsub != null) {
|
|
38007
|
+
this.countdownUnsub();
|
|
38008
|
+
this.countdownUnsub = null;
|
|
36782
38009
|
}
|
|
36783
38010
|
this.scheduler?.destroy();
|
|
36784
38011
|
this.animator?.stop();
|
|
@@ -36803,6 +38030,11 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
36803
38030
|
this.plot?.removeEventListener("pointerleave", this.onScrollProximityLeave);
|
|
36804
38031
|
this.labelTooltip?.destroy();
|
|
36805
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;
|
|
36806
38038
|
this.scrollButton?.remove();
|
|
36807
38039
|
this.scrollButton = null;
|
|
36808
38040
|
for (const l of this.extLayers) l.instance.destroy?.();
|
|
@@ -36861,8 +38093,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
36861
38093
|
}
|
|
36862
38094
|
if (!this.introPlayed && this.bars.length > 0) {
|
|
36863
38095
|
this.introPlayed = true;
|
|
36864
|
-
if (this.
|
|
36865
|
-
this.playIntro(
|
|
38096
|
+
if (this.intro.style) {
|
|
38097
|
+
this.playIntro();
|
|
36866
38098
|
return;
|
|
36867
38099
|
}
|
|
36868
38100
|
}
|
|
@@ -36873,7 +38105,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
36873
38105
|
const last2 = this.bars[n - 1];
|
|
36874
38106
|
if (last2 && bar.time === last2.time) {
|
|
36875
38107
|
this.bars[n - 1] = bar;
|
|
36876
|
-
if (this.
|
|
38108
|
+
if (!this.animLiveBar.on || this.liveEaseTime !== bar.time) {
|
|
36877
38109
|
this.syncLiveEase(bar);
|
|
36878
38110
|
} else {
|
|
36879
38111
|
this.animator.start();
|
|
@@ -36889,11 +38121,11 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
36889
38121
|
this.scene.bars = this.bars;
|
|
36890
38122
|
this.scheduler.invalidate(4 /* Full */);
|
|
36891
38123
|
}
|
|
36892
|
-
/** Set the
|
|
36893
|
-
*
|
|
36894
|
-
|
|
36895
|
-
this.
|
|
36896
|
-
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;
|
|
36897
38129
|
}
|
|
36898
38130
|
/** Snap the eased forming-bar state to `bar` — no glide (a fresh bar or the first tick of one). */
|
|
36899
38131
|
syncLiveEase(bar) {
|
|
@@ -36907,7 +38139,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
36907
38139
|
const target = this.bars[this.bars.length - 1];
|
|
36908
38140
|
if (!target || this.liveEaseTime !== target.time) return false;
|
|
36909
38141
|
const eps = Math.max(1e-9, Math.abs(target.close) * 1e-6);
|
|
36910
|
-
const tau = this.
|
|
38142
|
+
const tau = this.animLiveBar.tau;
|
|
36911
38143
|
const nh = easeToward(this.liveEaseHigh, target.high, dtMs, tau);
|
|
36912
38144
|
const nl = easeToward(this.liveEaseLow, target.low, dtMs, tau);
|
|
36913
38145
|
const nc = easeToward(this.liveEaseClose, target.close, dtMs, tau);
|
|
@@ -37047,10 +38279,12 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
37047
38279
|
this.refreshAnchorOffset(model);
|
|
37048
38280
|
if (model.native && this.extLayers.some((l) => l.def.id === model.native.type)) this.scene.assignIndicatorZTop(model.id);
|
|
37049
38281
|
else this.scene.assignIndicatorZ(model.id);
|
|
37050
|
-
|
|
37051
|
-
|
|
37052
|
-
|
|
37053
|
-
|
|
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
|
+
}
|
|
37054
38288
|
if (model.native?.type === "volume") {
|
|
37055
38289
|
this.volumeActive = true;
|
|
37056
38290
|
this.volumeHidden = false;
|
|
@@ -37197,7 +38431,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
37197
38431
|
this.settingsDialog?.setHiddenSettings(this.hiddenSettings);
|
|
37198
38432
|
}
|
|
37199
38433
|
listSettingsIds() {
|
|
37200
|
-
return settingsIdCatalog(this.hostSettingsSections);
|
|
38434
|
+
return settingsIdCatalog(this.hostSettingsSections, this.markGroupsInUse());
|
|
37201
38435
|
}
|
|
37202
38436
|
onChartTypeSettingsChange(cb) {
|
|
37203
38437
|
this.chartTypeSettingsCbs.add(cb);
|
|
@@ -37223,6 +38457,22 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
37223
38457
|
this.axisLongPressCbs.add(cb);
|
|
37224
38458
|
return () => this.axisLongPressCbs.delete(cb);
|
|
37225
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
|
+
}
|
|
37226
38476
|
onViewportChange(cb) {
|
|
37227
38477
|
this.viewportCbs.add(cb);
|
|
37228
38478
|
return () => this.viewportCbs.delete(cb);
|
|
@@ -37276,7 +38526,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
37276
38526
|
this.zoomAnchorX = anchorX;
|
|
37277
38527
|
this.panVelocity = 0;
|
|
37278
38528
|
this.scrollTargetRO = null;
|
|
37279
|
-
if (!this.animZoom) {
|
|
38529
|
+
if (!this.animZoom.on) {
|
|
37280
38530
|
const v = this.clampViewport(barSpacing, this.anchoredRightOffset(barSpacing));
|
|
37281
38531
|
this.coords.setViewport(v);
|
|
37282
38532
|
this.targetBarSpacing = v.barSpacing;
|
|
@@ -37289,7 +38539,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
37289
38539
|
}
|
|
37290
38540
|
/** Inertial pan: continue with a rightOffset velocity (logical units / ms) that decays. */
|
|
37291
38541
|
fling(velocity) {
|
|
37292
|
-
if (!this.animPan) return;
|
|
38542
|
+
if (!this.animPan.on) return;
|
|
37293
38543
|
this.scrollTargetRO = null;
|
|
37294
38544
|
this.panVelocity = velocity;
|
|
37295
38545
|
this.animator.start();
|
|
@@ -37329,7 +38579,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
37329
38579
|
let active = false;
|
|
37330
38580
|
const tbs = this.targetBarSpacing;
|
|
37331
38581
|
if (Math.abs(barSpacing - tbs) > tbs * 1e-3) {
|
|
37332
|
-
barSpacing = clampBarSpacing(easeToward(barSpacing, tbs, dtMs,
|
|
38582
|
+
barSpacing = clampBarSpacing(easeToward(barSpacing, tbs, dtMs, this.animZoom.tau));
|
|
37333
38583
|
rightOffset = this.anchoredRightOffset(barSpacing);
|
|
37334
38584
|
active = true;
|
|
37335
38585
|
} else if (barSpacing !== tbs) {
|
|
@@ -37339,13 +38589,14 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
37339
38589
|
const stopVel = FLING_STOP_PX / Math.max(1e-6, barSpacing * this.coords.spacingScale);
|
|
37340
38590
|
if (Math.abs(this.panVelocity) > stopVel) {
|
|
37341
38591
|
rightOffset += this.panVelocity * dtMs;
|
|
37342
|
-
|
|
38592
|
+
const tau = this.animPan.tau;
|
|
38593
|
+
this.panVelocity = tau > 0 ? this.panVelocity * Math.exp(-dtMs / tau) : 0;
|
|
37343
38594
|
if (Math.abs(this.panVelocity) <= stopVel) this.panVelocity = 0;
|
|
37344
38595
|
else active = true;
|
|
37345
38596
|
}
|
|
37346
38597
|
if (this.scrollTargetRO != null) {
|
|
37347
38598
|
const target = this.scrollTargetRO;
|
|
37348
|
-
const next2 = easeToward(rightOffset, target, dtMs,
|
|
38599
|
+
const next2 = easeToward(rightOffset, target, dtMs, this.animScroll.tau);
|
|
37349
38600
|
if (Math.abs(next2 - target) < 1e-3) {
|
|
37350
38601
|
rightOffset = target;
|
|
37351
38602
|
this.scrollTargetRO = null;
|
|
@@ -37373,6 +38624,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
37373
38624
|
* through Math.log), not a non-linear jump. */
|
|
37374
38625
|
easeScales(dtMs) {
|
|
37375
38626
|
let moving = false;
|
|
38627
|
+
const tau = this.animAutoscale.tau;
|
|
37376
38628
|
for (const pane of this.scene.panes.values()) {
|
|
37377
38629
|
const t = pane.scaleTarget;
|
|
37378
38630
|
const s = pane.scale;
|
|
@@ -37380,8 +38632,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
37380
38632
|
const lt0 = Math.log(t.min);
|
|
37381
38633
|
const lt1 = Math.log(t.max);
|
|
37382
38634
|
const lspan = Math.max(1e-9, Math.abs(lt1 - lt0));
|
|
37383
|
-
const n0 = easeToward(Math.log(s.min), lt0, dtMs,
|
|
37384
|
-
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);
|
|
37385
38637
|
if (Math.abs(n0 - lt0) <= lspan * 1e-3 && Math.abs(n1 - lt1) <= lspan * 1e-3) {
|
|
37386
38638
|
pane.scale = { min: t.min, max: t.max, log: true };
|
|
37387
38639
|
} else {
|
|
@@ -37391,8 +38643,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
37391
38643
|
continue;
|
|
37392
38644
|
}
|
|
37393
38645
|
const span = Math.max(1e-9, Math.abs(t.max - t.min));
|
|
37394
|
-
let nmin = easeToward(s.min, t.min, dtMs,
|
|
37395
|
-
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);
|
|
37396
38648
|
if (Math.abs(nmin - t.min) <= span * 1e-3 && Math.abs(nmax - t.max) <= span * 1e-3) {
|
|
37397
38649
|
nmin = t.min;
|
|
37398
38650
|
nmax = t.max;
|
|
@@ -37405,8 +38657,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
37405
38657
|
const t = sl.scaleTarget;
|
|
37406
38658
|
const s = sl.scale;
|
|
37407
38659
|
const span = Math.max(1e-9, Math.abs(t.max - t.min));
|
|
37408
|
-
let nmin = easeToward(s.min, t.min, dtMs,
|
|
37409
|
-
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);
|
|
37410
38662
|
if (Math.abs(nmin - t.min) <= span * 1e-3 && Math.abs(nmax - t.max) <= span * 1e-3) {
|
|
37411
38663
|
nmin = t.min;
|
|
37412
38664
|
nmax = t.max;
|
|
@@ -37427,6 +38679,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
37427
38679
|
this.scene.crosshair = null;
|
|
37428
38680
|
this.hoverSeparatorY = null;
|
|
37429
38681
|
this.lastPointer = null;
|
|
38682
|
+
if (this.marksExpandedBy === "hover") this.setMarksExpanded(null);
|
|
38683
|
+
this.setMarkHover(null);
|
|
37430
38684
|
this.scheduler.invalidate(1 /* Cursor */);
|
|
37431
38685
|
this.hoverLogical = null;
|
|
37432
38686
|
const empty = { time: null, price: null, paneKind: null, values: /* @__PURE__ */ new Map(), ohlc: null };
|
|
@@ -37438,6 +38692,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
37438
38692
|
this.lastPointer = inData ? { x, y } : null;
|
|
37439
38693
|
this.hoverSeparatorY = x >= 0 && y >= 0 && y <= this.coords.height ? this.separatorHoverY(y) : null;
|
|
37440
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);
|
|
37441
38697
|
const logical = Math.round(this.coords.xToLogical(x));
|
|
37442
38698
|
const onBar = logical >= 0 && logical < this.coords.barCount;
|
|
37443
38699
|
const time = onBar ? this.coords.logicalToTime(logical) : null;
|
|
@@ -37467,6 +38723,76 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
37467
38723
|
const onBar = logical >= 0 && logical < this.coords.barCount;
|
|
37468
38724
|
for (const cb of this.clickCbs) cb({ time: onBar ? this.coords.logicalToTime(logical) : null, price: null });
|
|
37469
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
|
+
}
|
|
37470
38796
|
paneAtY(y) {
|
|
37471
38797
|
return this.paneNodeAtY(y);
|
|
37472
38798
|
}
|
|
@@ -37858,6 +39184,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
37858
39184
|
} else if (repaintsChrome(level) && this.paintedData) {
|
|
37859
39185
|
this.chrome.prepare(this.scene, this.coords, this.theme);
|
|
37860
39186
|
this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
|
|
39187
|
+
this.trackMarkPopover();
|
|
37861
39188
|
}
|
|
37862
39189
|
this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
|
|
37863
39190
|
if (!repaintsData(level) && this.paintedData) this.repaintCursorLayers();
|
|
@@ -37873,7 +39200,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
37873
39200
|
const lp = this.layerPane(l.def.id) ?? pane;
|
|
37874
39201
|
if (lp.collapsed) continue;
|
|
37875
39202
|
l.instance.render(this.extLayerArgs(l.def.id, lp.scale, lp.bounds, nowMs));
|
|
37876
|
-
if (this.animZoom && l.instance.animating?.()) this.animator.start();
|
|
39203
|
+
if (this.animZoom.on && l.instance.animating?.()) this.animator.start();
|
|
37877
39204
|
}
|
|
37878
39205
|
}
|
|
37879
39206
|
/** Blank one SDK layer canvas (a collapsed host pane suppresses the layer's painting). */
|
|
@@ -37938,7 +39265,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
37938
39265
|
const args = this.extLayerArgs(l.def.id, lp.scale, lp.bounds, nowMs);
|
|
37939
39266
|
l.instance.render(args);
|
|
37940
39267
|
if (lp === pane) folded = foldBaseModulation(folded, l.instance.modulateBase?.(args) ?? null);
|
|
37941
|
-
if (this.animZoom && l.instance.animating?.()) this.animator.start();
|
|
39268
|
+
if (this.animZoom.on && l.instance.animating?.()) this.animator.start();
|
|
37942
39269
|
}
|
|
37943
39270
|
if (folded) {
|
|
37944
39271
|
if (folded.candleBodyScale != null) this.backend.candleBodyScale = clamp012(folded.candleBodyScale) || 0.01;
|
|
@@ -37957,6 +39284,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
37957
39284
|
this.backdropRenderer.render(this.scene, this.coords, this.theme, gridAlpha);
|
|
37958
39285
|
this.backend.render(this.scene, this.coords, this.theme);
|
|
37959
39286
|
this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
|
|
39287
|
+
this.trackMarkPopover();
|
|
37960
39288
|
this.userDrawings?.render();
|
|
37961
39289
|
if (easeLive && liveActual) this.bars[li] = liveActual;
|
|
37962
39290
|
this.paintedData = true;
|
|
@@ -38469,7 +39797,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
38469
39797
|
const map2 = /* @__PURE__ */ new Map();
|
|
38470
39798
|
for (const pane of this.scene.panes.values()) {
|
|
38471
39799
|
if (!pane.collapsed) continue;
|
|
38472
|
-
const models = this.scene.orderedIndicatorsForPane(pane.id);
|
|
39800
|
+
const models = this.scene.orderedIndicatorsForPane(pane.id).filter((m) => m.legend !== false);
|
|
38473
39801
|
const merged = new Set(this.scene.ownScaleIndicatorsForPane(pane.id).map((m) => m.id));
|
|
38474
39802
|
const master = models.find((m) => !merged.has(m.id)) ?? models[0];
|
|
38475
39803
|
map2.set(pane.id, master?.id ?? null);
|
|
@@ -38747,6 +40075,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
38747
40075
|
}
|
|
38748
40076
|
var VolumeIndicator = class {
|
|
38749
40077
|
constructor() {
|
|
40078
|
+
/** Null until start() — pre-start setInputs/resume must record without pushing. */
|
|
40079
|
+
this.ctx = null;
|
|
38750
40080
|
this.inputs = {};
|
|
38751
40081
|
}
|
|
38752
40082
|
start(ctx, inputs) {
|
|
@@ -38763,13 +40093,13 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
38763
40093
|
}
|
|
38764
40094
|
setInputs(inputs) {
|
|
38765
40095
|
this.inputs = inputs;
|
|
38766
|
-
this.ctx
|
|
40096
|
+
this.ctx?.pushData(volumeLayerData(inputs));
|
|
38767
40097
|
}
|
|
38768
40098
|
/** Hiding is a renderer-layer flag (set via `setIndicatorVisible`); no resources to free. */
|
|
38769
40099
|
suspend() {
|
|
38770
40100
|
}
|
|
38771
40101
|
resume() {
|
|
38772
|
-
this.ctx
|
|
40102
|
+
this.ctx?.pushData(volumeLayerData(this.inputs));
|
|
38773
40103
|
}
|
|
38774
40104
|
stop() {
|
|
38775
40105
|
}
|
|
@@ -38815,6 +40145,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
38815
40145
|
}
|
|
38816
40146
|
var VpvrIndicator = class {
|
|
38817
40147
|
constructor() {
|
|
40148
|
+
/** Null until start() — pre-start setInputs/resume must record without pushing. */
|
|
40149
|
+
this.ctx = null;
|
|
38818
40150
|
this.inputs = {};
|
|
38819
40151
|
}
|
|
38820
40152
|
start(ctx, inputs) {
|
|
@@ -38831,13 +40163,13 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
38831
40163
|
}
|
|
38832
40164
|
setInputs(inputs) {
|
|
38833
40165
|
this.inputs = inputs;
|
|
38834
|
-
this.ctx
|
|
40166
|
+
this.ctx?.pushData(vpvrLayerData(inputs));
|
|
38835
40167
|
}
|
|
38836
40168
|
/** Hiding is a renderer-layer flag (set via `setIndicatorVisible`); no resources to free. */
|
|
38837
40169
|
suspend() {
|
|
38838
40170
|
}
|
|
38839
40171
|
resume() {
|
|
38840
|
-
this.ctx
|
|
40172
|
+
this.ctx?.pushData(vpvrLayerData(this.inputs));
|
|
38841
40173
|
}
|
|
38842
40174
|
stop() {
|
|
38843
40175
|
}
|
|
@@ -41289,6 +42621,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
41289
42621
|
if (Object.keys(defaults2).length > 0) this.rendererControl.set(defaults2);
|
|
41290
42622
|
this.panesControl = new PanesControl(this.orchestrator);
|
|
41291
42623
|
this.drawingsControl = new DrawingsControl(this.orchestrator.drawings);
|
|
42624
|
+
this.marksControl = new MarksControl(this.orchestrator.marks);
|
|
41292
42625
|
}
|
|
41293
42626
|
/**
|
|
41294
42627
|
* Register a scripting engine so `addIndicator({ language })` can run that
|
|
@@ -41507,6 +42840,17 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
41507
42840
|
get drawings() {
|
|
41508
42841
|
return this.drawingsControl;
|
|
41509
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
|
+
}
|
|
41510
42854
|
on(event, handler) {
|
|
41511
42855
|
return this.orchestrator.events.on(event, handler);
|
|
41512
42856
|
}
|
|
@@ -42952,6 +44296,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
42952
44296
|
exports.INVALID = INVALID;
|
|
42953
44297
|
exports.LIGHT_THEME = LIGHT_THEME;
|
|
42954
44298
|
exports.MARKER = MARKER;
|
|
44299
|
+
exports.MarksControl = MarksControl;
|
|
42955
44300
|
exports.MultiProviderFeed = MultiProviderFeed;
|
|
42956
44301
|
exports.NEUTRAL = NEUTRAL;
|
|
42957
44302
|
exports.NativeRenderer = NativeRenderer;
|
|
@@ -42963,6 +44308,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
42963
44308
|
exports.SESSION_PRE = SESSION_PRE;
|
|
42964
44309
|
exports.SLATE = SLATE;
|
|
42965
44310
|
exports.SLATE_DEEP = SLATE_DEEP;
|
|
44311
|
+
exports.SecondClock = SecondClock;
|
|
42966
44312
|
exports.TRADE_EXIT = TRADE_EXIT;
|
|
42967
44313
|
exports.TRADE_LONG = TRADE_LONG;
|
|
42968
44314
|
exports.TRADE_SHORT = TRADE_SHORT;
|