@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/index.cjs
CHANGED
|
@@ -26,19 +26,48 @@ var menu__namespace = /*#__PURE__*/_interopNamespace(menu);
|
|
|
26
26
|
var dialog__namespace = /*#__PURE__*/_interopNamespace(dialog);
|
|
27
27
|
|
|
28
28
|
// src/core/options.ts
|
|
29
|
+
var ZOOM_EASE_DEFAULT_MS = 70;
|
|
30
|
+
var PAN_INERTIA_DEFAULT_MS = 110;
|
|
31
|
+
var SCROLL_EASE_DEFAULT_MS = 130;
|
|
32
|
+
var AUTOSCALE_EASE_DEFAULT_MS = 80;
|
|
29
33
|
var LIVE_BAR_EASE_DEFAULT_MS = 90;
|
|
30
|
-
var
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
var INTRO_DURATION_DEFAULT_MS = 650;
|
|
35
|
+
var ANIMATION_EASE_MAX_MS = 1e3;
|
|
36
|
+
var LIVE_BAR_EASE_MAX_MS = ANIMATION_EASE_MAX_MS;
|
|
37
|
+
var INTRO_DURATION_MAX_MS = 5e3;
|
|
38
|
+
function resolveEaseMs(value, defaultMs, maxMs = ANIMATION_EASE_MAX_MS) {
|
|
39
|
+
if (value === true) return defaultMs;
|
|
33
40
|
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return 0;
|
|
34
|
-
return Math.min(value,
|
|
41
|
+
return Math.min(value, maxMs);
|
|
42
|
+
}
|
|
43
|
+
function resolveLiveBarEaseMs(value) {
|
|
44
|
+
return resolveEaseMs(value, LIVE_BAR_EASE_DEFAULT_MS, LIVE_BAR_EASE_MAX_MS);
|
|
45
|
+
}
|
|
46
|
+
function resolveIntro(value) {
|
|
47
|
+
const off = { style: false, duration: 0 };
|
|
48
|
+
if (value === true) return { style: "settle", duration: INTRO_DURATION_DEFAULT_MS };
|
|
49
|
+
if (value === "settle" || value === "grow") return { style: value, duration: INTRO_DURATION_DEFAULT_MS };
|
|
50
|
+
if (value && typeof value === "object") {
|
|
51
|
+
const o = value;
|
|
52
|
+
const d = o.duration;
|
|
53
|
+
return {
|
|
54
|
+
style: o.style === "grow" ? "grow" : "settle",
|
|
55
|
+
duration: typeof d === "number" && Number.isFinite(d) && d > 0 ? Math.min(d, INTRO_DURATION_MAX_MS) : INTRO_DURATION_DEFAULT_MS
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return off;
|
|
35
59
|
}
|
|
36
60
|
function resolveAnimations(animations) {
|
|
37
|
-
if (
|
|
61
|
+
if (animations === false) return { animZoom: 0, animPan: 0, animScroll: 0, animAutoscale: 0, animLiveBar: 0, animIntro: { style: false, duration: 0 } };
|
|
62
|
+
const cfg = animations === true || animations == null ? {} : animations;
|
|
63
|
+
const animPan = resolveEaseMs(cfg.pan ?? true, PAN_INERTIA_DEFAULT_MS);
|
|
38
64
|
return {
|
|
39
|
-
animZoom:
|
|
40
|
-
animPan
|
|
41
|
-
|
|
65
|
+
animZoom: resolveEaseMs(cfg.zoom ?? true, ZOOM_EASE_DEFAULT_MS),
|
|
66
|
+
animPan,
|
|
67
|
+
animScroll: cfg.scroll === void 0 ? animPan > 0 ? SCROLL_EASE_DEFAULT_MS : 0 : resolveEaseMs(cfg.scroll, SCROLL_EASE_DEFAULT_MS),
|
|
68
|
+
animAutoscale: resolveEaseMs(cfg.autoscale ?? true, AUTOSCALE_EASE_DEFAULT_MS),
|
|
69
|
+
animLiveBar: resolveLiveBarEaseMs(cfg.liveBar),
|
|
70
|
+
animIntro: resolveIntro(cfg.intro ?? true)
|
|
42
71
|
};
|
|
43
72
|
}
|
|
44
73
|
|
|
@@ -7081,6 +7110,93 @@ var DrawingController = class {
|
|
|
7081
7110
|
}
|
|
7082
7111
|
};
|
|
7083
7112
|
|
|
7113
|
+
// src/core/marks/MarksController.ts
|
|
7114
|
+
var MarksController = class {
|
|
7115
|
+
constructor(renderer, events) {
|
|
7116
|
+
this.renderer = renderer;
|
|
7117
|
+
/** Insertion-ordered — the order a cluster falls back to for equal times. */
|
|
7118
|
+
this.marks = /* @__PURE__ */ new Map();
|
|
7119
|
+
this.groups = /* @__PURE__ */ new Map();
|
|
7120
|
+
this.subs = [];
|
|
7121
|
+
this.enabled = !!renderer.capabilities.timelineMarks && typeof renderer.setTimelineMarks === "function";
|
|
7122
|
+
if (this.enabled && renderer.onMarkClick) this.subs.push(renderer.onMarkClick((e) => events.emit("mark:click", e)));
|
|
7123
|
+
}
|
|
7124
|
+
/** Whether the active renderer paints timeline marks. */
|
|
7125
|
+
get supported() {
|
|
7126
|
+
return this.enabled;
|
|
7127
|
+
}
|
|
7128
|
+
/** Add (or replace, by id) one mark. */
|
|
7129
|
+
add(mark) {
|
|
7130
|
+
this.marks.set(mark.id, validateMark(mark));
|
|
7131
|
+
this.sync();
|
|
7132
|
+
}
|
|
7133
|
+
/** Replace the whole set — a market switch. */
|
|
7134
|
+
set(marks) {
|
|
7135
|
+
this.marks.clear();
|
|
7136
|
+
for (const m of marks) this.marks.set(m.id, validateMark(m));
|
|
7137
|
+
this.sync();
|
|
7138
|
+
}
|
|
7139
|
+
remove(id) {
|
|
7140
|
+
const had = this.marks.delete(id);
|
|
7141
|
+
if (had) this.sync();
|
|
7142
|
+
return had;
|
|
7143
|
+
}
|
|
7144
|
+
clear() {
|
|
7145
|
+
if (this.marks.size === 0) return;
|
|
7146
|
+
this.marks.clear();
|
|
7147
|
+
this.sync();
|
|
7148
|
+
}
|
|
7149
|
+
/** Every mark, in insertion order (shallow copies — mutating one changes nothing). */
|
|
7150
|
+
all() {
|
|
7151
|
+
return [...this.marks.values()].map((m) => ({ ...m, glyph: { ...m.glyph } }));
|
|
7152
|
+
}
|
|
7153
|
+
/** Define (or replace) a group's presentation — its settings label and default visibility. */
|
|
7154
|
+
defineGroup(group) {
|
|
7155
|
+
if (!group || typeof group.id !== "string" || group.id.length === 0) throw new Error("[vela] marks.defineGroup: `id` must be a non-empty string");
|
|
7156
|
+
if (typeof group.label !== "string") throw new Error(`[vela] marks.defineGroup: group "${group.id}" needs a string \`label\``);
|
|
7157
|
+
this.groups.set(group.id, { ...group });
|
|
7158
|
+
this.sync();
|
|
7159
|
+
}
|
|
7160
|
+
/** The defined groups, in definition order. */
|
|
7161
|
+
groupDefinitions() {
|
|
7162
|
+
return [...this.groups.values()].map((g) => ({ ...g }));
|
|
7163
|
+
}
|
|
7164
|
+
/**
|
|
7165
|
+
* Show or hide one group's marks. The choice lives in the renderer's cosmetic config
|
|
7166
|
+
* (the `marks` feature) — what the settings dialog's Events checkboxes edit and what
|
|
7167
|
+
* a persisted chart restores — so it warns + no-ops on a renderer without it.
|
|
7168
|
+
*/
|
|
7169
|
+
setGroupVisible(id, visible) {
|
|
7170
|
+
if (!this.renderer.features.includes("marks")) {
|
|
7171
|
+
console.warn(`[vela] renderer "${this.renderer.name}" does not paint timeline marks \u2014 setGroupVisible ignored.`);
|
|
7172
|
+
return;
|
|
7173
|
+
}
|
|
7174
|
+
this.renderer.applyFeature("marks", { groups: { [id]: visible } });
|
|
7175
|
+
}
|
|
7176
|
+
/** A group's effective visibility: the user's (persisted) choice, else the group's declared default, else visible. */
|
|
7177
|
+
isGroupVisible(id) {
|
|
7178
|
+
const state = this.renderer.readFeature("marks");
|
|
7179
|
+
const chosen = state?.groups?.[id];
|
|
7180
|
+
if (typeof chosen === "boolean") return chosen;
|
|
7181
|
+
return this.groups.get(id)?.visible !== false;
|
|
7182
|
+
}
|
|
7183
|
+
destroy() {
|
|
7184
|
+
for (const unsub of this.subs) unsub();
|
|
7185
|
+
this.subs.length = 0;
|
|
7186
|
+
}
|
|
7187
|
+
sync() {
|
|
7188
|
+
if (!this.enabled) return;
|
|
7189
|
+
this.renderer.setTimelineMarks([...this.marks.values()], [...this.groups.values()]);
|
|
7190
|
+
}
|
|
7191
|
+
};
|
|
7192
|
+
function validateMark(mark) {
|
|
7193
|
+
if (!mark || typeof mark !== "object") throw new Error("[vela] marks: a mark must be an object");
|
|
7194
|
+
if (typeof mark.id !== "string" || mark.id.length === 0) throw new Error("[vela] marks: `id` must be a non-empty string");
|
|
7195
|
+
if (typeof mark.time !== "number" || !Number.isFinite(mark.time)) throw new Error(`[vela] marks: mark "${mark.id}" needs a finite epoch-ms \`time\``);
|
|
7196
|
+
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\``);
|
|
7197
|
+
return { ...mark, glyph: { ...mark.glyph } };
|
|
7198
|
+
}
|
|
7199
|
+
|
|
7084
7200
|
// src/data/timeframe.ts
|
|
7085
7201
|
var NAMED_TF_MS = { D: 864e5, W: 6048e5, M: 2592e6 };
|
|
7086
7202
|
function timeframeToMs(timeframe) {
|
|
@@ -7759,6 +7875,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
7759
7875
|
marketKey: () => `${this.config.market.symbol ?? ""}|${this.config.market.session ?? ""}`
|
|
7760
7876
|
});
|
|
7761
7877
|
this.drawings = new DrawingController(this.renderer, this.events, config.drawings, drawingSeries);
|
|
7878
|
+
this.marks = new MarksController(this.renderer, this.events);
|
|
7762
7879
|
this.unresolvedUnsub = this.feed.onUnresolved?.((info) => {
|
|
7763
7880
|
this.endLoad();
|
|
7764
7881
|
this.events.emit("data:unresolved", info);
|
|
@@ -8131,6 +8248,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
8131
8248
|
if (!record.native) continue;
|
|
8132
8249
|
record.native.instance.stop();
|
|
8133
8250
|
record.native.instance = record.native.descriptor.create();
|
|
8251
|
+
record.native.started = false;
|
|
8134
8252
|
record.pendingStructural = true;
|
|
8135
8253
|
if (record.hidden) {
|
|
8136
8254
|
record.native.stale = true;
|
|
@@ -8622,11 +8740,12 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
8622
8740
|
record.session = void 0;
|
|
8623
8741
|
record.native?.instance.suspend();
|
|
8624
8742
|
if (record.renderHandle) this.renderer.setIndicatorVisible?.(record.renderHandle, false);
|
|
8743
|
+
else if (record.native) this.mountHiddenNativeRow(id, record);
|
|
8625
8744
|
} else {
|
|
8626
8745
|
if (record.renderHandle) this.renderer.setIndicatorVisible?.(record.renderHandle, true);
|
|
8627
8746
|
record.pendingStructural = true;
|
|
8628
8747
|
if (record.native) {
|
|
8629
|
-
if (record.native.stale) {
|
|
8748
|
+
if (record.native.stale || !record.native.started) {
|
|
8630
8749
|
record.native.stale = false;
|
|
8631
8750
|
const handle = this.handles.get(id);
|
|
8632
8751
|
if (handle) void this.startNativeIndicator(id, handle);
|
|
@@ -8709,6 +8828,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
8709
8828
|
this.unresolvedUnsub = null;
|
|
8710
8829
|
this.feed.destroy?.();
|
|
8711
8830
|
this.drawings.destroy();
|
|
8831
|
+
this.marks.destroy();
|
|
8712
8832
|
this.renderer.destroy();
|
|
8713
8833
|
this.events.clear();
|
|
8714
8834
|
}
|
|
@@ -8807,6 +8927,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
8807
8927
|
}
|
|
8808
8928
|
};
|
|
8809
8929
|
record.native.instance.start(ctx, record.inputValues);
|
|
8930
|
+
record.native.started = true;
|
|
8810
8931
|
} catch (err) {
|
|
8811
8932
|
this.fail(id, handle, err);
|
|
8812
8933
|
}
|
|
@@ -8821,6 +8942,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
8821
8942
|
overlay: d.overlay,
|
|
8822
8943
|
paneHint: d.paneHint,
|
|
8823
8944
|
native: { type: record.native.type },
|
|
8945
|
+
...d.legend === false ? { legend: false } : {},
|
|
8824
8946
|
...out.paneAxis != null ? { paneAxis: out.paneAxis } : {},
|
|
8825
8947
|
series: out.series ?? [],
|
|
8826
8948
|
fills: out.fills ?? [],
|
|
@@ -8843,9 +8965,17 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
8843
8965
|
* over it in place (`pendingStructural`), clears the spinner, and only THEN fires
|
|
8844
8966
|
* `indicator:added`/`ready` — so event semantics and `inspect()` (which skips
|
|
8845
8967
|
* loading records) still mean "the indicator produced output".
|
|
8968
|
+
*
|
|
8969
|
+
* A HIDDEN record mounts too — dimmed, no spinner (its session never starts while
|
|
8970
|
+
* hidden, so nothing is computing and no model will ever arrive to mount the row
|
|
8971
|
+
* later). Without this an indicator ADDED hidden (a restored ledger/ext entry) had
|
|
8972
|
+
* no legend row at all: invisible AND unreachable — the eye that unhides it never
|
|
8973
|
+
* existed. The hidden mount announces immediately for the same reason: the "first
|
|
8974
|
+
* computed model" that normally announces cannot come until the indicator is shown,
|
|
8975
|
+
* and host UIs (object tree, landing watchers) must know it exists NOW.
|
|
8846
8976
|
*/
|
|
8847
8977
|
mountLoadingPlaceholder(id, record) {
|
|
8848
|
-
if (record.renderHandle ||
|
|
8978
|
+
if (record.renderHandle || !record.prepared) return;
|
|
8849
8979
|
const meta = record.prepared.meta;
|
|
8850
8980
|
const model = {
|
|
8851
8981
|
id,
|
|
@@ -8869,8 +8999,34 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
8869
8999
|
this.ensurePaneFor(paneId);
|
|
8870
9000
|
record.renderHandle = this.renderer.mountIndicator(model);
|
|
8871
9001
|
record.pendingStructural = true;
|
|
9002
|
+
if (record.hidden) {
|
|
9003
|
+
this.renderer.setIndicatorVisible?.(record.renderHandle, false);
|
|
9004
|
+
this.announce(record, this.handles.get(id));
|
|
9005
|
+
return;
|
|
9006
|
+
}
|
|
8872
9007
|
this.setLoading(record, true);
|
|
8873
9008
|
}
|
|
9009
|
+
/**
|
|
9010
|
+
* Mount the legend row for a NATIVE indicator that is being hidden BEFORE it ever
|
|
9011
|
+
* started (a restored-hidden ledger entry: `startNativeIndicator` bails on hidden
|
|
9012
|
+
* records, so no model — and therefore no row — would ever mount). The native
|
|
9013
|
+
* counterpart of {@link mountLoadingPlaceholder}'s hidden branch: an empty model
|
|
9014
|
+
* carries the title + inputs schema, the renderer marks the row hidden, and the
|
|
9015
|
+
* announce makes the indicator visible to host UIs. Showing later STARTS the
|
|
9016
|
+
* instance (the `started` flag path) and its first emit remounts over this row.
|
|
9017
|
+
*/
|
|
9018
|
+
mountHiddenNativeRow(id, record) {
|
|
9019
|
+
if (record.renderHandle || !record.native) return;
|
|
9020
|
+
const model = this.buildNativeModel(record, {});
|
|
9021
|
+
const paneId = this.routePane(id, model, record.options ?? {});
|
|
9022
|
+
this.placeModel(model, id, paneId);
|
|
9023
|
+
record.model = model;
|
|
9024
|
+
this.ensurePaneFor(paneId);
|
|
9025
|
+
record.renderHandle = this.renderer.mountIndicator(model);
|
|
9026
|
+
record.pendingStructural = true;
|
|
9027
|
+
this.renderer.setIndicatorVisible?.(record.renderHandle, false);
|
|
9028
|
+
this.announce(record, this.handles.get(id));
|
|
9029
|
+
}
|
|
8874
9030
|
/** Flip the record's loading state and reflect it in the legend row (spinner on/off). */
|
|
8875
9031
|
setLoading(record, loading) {
|
|
8876
9032
|
record.loading = loading;
|
|
@@ -8905,6 +9061,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
|
|
|
8905
9061
|
for (const r of this.registry.all()) {
|
|
8906
9062
|
const model = r.model;
|
|
8907
9063
|
if (!model) continue;
|
|
9064
|
+
if (model.legend === false) continue;
|
|
8908
9065
|
const paneId = model.paneId ?? "price";
|
|
8909
9066
|
if (!byPane.has(paneId)) byPane.set(paneId, []);
|
|
8910
9067
|
byPane.get(paneId).push({
|
|
@@ -9480,6 +9637,16 @@ var RendererControl = class {
|
|
|
9480
9637
|
this.renderer.setLayoutMode?.(mode);
|
|
9481
9638
|
return this;
|
|
9482
9639
|
}
|
|
9640
|
+
/**
|
|
9641
|
+
* Drive the renderer's time-of-day chrome (the countdown-to-bar-close chip) from the
|
|
9642
|
+
* host's own second pulse, so it ticks in step with a host clock display instead of
|
|
9643
|
+
* on a separate timer that can read a different second. `null` hands the pulse back
|
|
9644
|
+
* to the renderer. Silent no-op on a renderer without time-of-day chrome.
|
|
9645
|
+
*/
|
|
9646
|
+
setWallClock(clock) {
|
|
9647
|
+
this.renderer.setWallClock?.(clock);
|
|
9648
|
+
return this;
|
|
9649
|
+
}
|
|
9483
9650
|
};
|
|
9484
9651
|
|
|
9485
9652
|
// src/core/renderer-defaults.ts
|
|
@@ -10372,6 +10539,95 @@ var DrawingsControl = class {
|
|
|
10372
10539
|
}
|
|
10373
10540
|
};
|
|
10374
10541
|
|
|
10542
|
+
// src/core/MarksControl.ts
|
|
10543
|
+
var MarksControl = class {
|
|
10544
|
+
constructor(ctrl) {
|
|
10545
|
+
this.ctrl = ctrl;
|
|
10546
|
+
}
|
|
10547
|
+
/** Whether the active renderer paints timeline marks. */
|
|
10548
|
+
get supported() {
|
|
10549
|
+
return this.ctrl.supported;
|
|
10550
|
+
}
|
|
10551
|
+
/** Add one mark; an existing id is replaced in place. */
|
|
10552
|
+
add(mark) {
|
|
10553
|
+
this.ctrl.add(mark);
|
|
10554
|
+
return this;
|
|
10555
|
+
}
|
|
10556
|
+
/** Replace the whole set (a market switch). */
|
|
10557
|
+
set(marks) {
|
|
10558
|
+
this.ctrl.set(marks);
|
|
10559
|
+
return this;
|
|
10560
|
+
}
|
|
10561
|
+
remove(id) {
|
|
10562
|
+
this.ctrl.remove(id);
|
|
10563
|
+
return this;
|
|
10564
|
+
}
|
|
10565
|
+
clear() {
|
|
10566
|
+
this.ctrl.clear();
|
|
10567
|
+
return this;
|
|
10568
|
+
}
|
|
10569
|
+
/** Every mark, in insertion order. */
|
|
10570
|
+
all() {
|
|
10571
|
+
return this.ctrl.all();
|
|
10572
|
+
}
|
|
10573
|
+
/**
|
|
10574
|
+
* Define a visibility group's presentation: the label of its checkbox in chart
|
|
10575
|
+
* settings (the Events tab) and its default visibility. Marks may name a group
|
|
10576
|
+
* that was never defined — it then shows its capitalized id.
|
|
10577
|
+
*/
|
|
10578
|
+
defineGroup(group) {
|
|
10579
|
+
this.ctrl.defineGroup(group);
|
|
10580
|
+
return this;
|
|
10581
|
+
}
|
|
10582
|
+
/** The defined groups, in definition order. */
|
|
10583
|
+
groups() {
|
|
10584
|
+
return this.ctrl.groupDefinitions();
|
|
10585
|
+
}
|
|
10586
|
+
/** Show or hide one group's marks — the same switch as the settings checkbox, persisted with the chart's config. */
|
|
10587
|
+
setGroupVisible(id, visible = true) {
|
|
10588
|
+
this.ctrl.setGroupVisible(id, visible);
|
|
10589
|
+
return this;
|
|
10590
|
+
}
|
|
10591
|
+
/** A group's effective visibility (the user's choice, else the group's declared default). */
|
|
10592
|
+
isGroupVisible(id) {
|
|
10593
|
+
return this.ctrl.isGroupVisible(id);
|
|
10594
|
+
}
|
|
10595
|
+
};
|
|
10596
|
+
|
|
10597
|
+
// src/core/util/wall-clock.ts
|
|
10598
|
+
var BOUNDARY_SLACK_MS = 5;
|
|
10599
|
+
var SecondClock = class _SecondClock {
|
|
10600
|
+
constructor(now = () => Date.now()) {
|
|
10601
|
+
this.now = now;
|
|
10602
|
+
this.subs = /* @__PURE__ */ new Set();
|
|
10603
|
+
this.timer = null;
|
|
10604
|
+
}
|
|
10605
|
+
onTick(cb) {
|
|
10606
|
+
this.subs.add(cb);
|
|
10607
|
+
if (this.timer == null) this.arm();
|
|
10608
|
+
return () => {
|
|
10609
|
+
this.subs.delete(cb);
|
|
10610
|
+
if (this.subs.size === 0 && this.timer != null) {
|
|
10611
|
+
clearTimeout(this.timer);
|
|
10612
|
+
this.timer = null;
|
|
10613
|
+
}
|
|
10614
|
+
};
|
|
10615
|
+
}
|
|
10616
|
+
/** Milliseconds from `now` to just past the next second boundary. */
|
|
10617
|
+
static delayToNextSecond(now) {
|
|
10618
|
+
const intoSecond = (now % 1e3 + 1e3) % 1e3;
|
|
10619
|
+
return 1e3 - intoSecond + BOUNDARY_SLACK_MS;
|
|
10620
|
+
}
|
|
10621
|
+
arm() {
|
|
10622
|
+
this.timer = setTimeout(() => {
|
|
10623
|
+
this.timer = null;
|
|
10624
|
+
const now = this.now();
|
|
10625
|
+
for (const cb of this.subs) cb(now);
|
|
10626
|
+
if (this.subs.size > 0) this.arm();
|
|
10627
|
+
}, _SecondClock.delayToNextSecond(this.now()));
|
|
10628
|
+
}
|
|
10629
|
+
};
|
|
10630
|
+
|
|
10375
10631
|
// src/core/color.ts
|
|
10376
10632
|
function parseRgb(color) {
|
|
10377
10633
|
const s = color.trim();
|
|
@@ -11032,7 +11288,7 @@ function intersectRects(a, b) {
|
|
|
11032
11288
|
return { left, top, right, bottom, width: right - left, height: bottom - top };
|
|
11033
11289
|
}
|
|
11034
11290
|
function placePopover(a) {
|
|
11035
|
-
let left = a.align === "end" ? a.trigger.right - a.pop.width : a.trigger.left;
|
|
11291
|
+
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;
|
|
11036
11292
|
const below = a.trigger.bottom + a.gap;
|
|
11037
11293
|
const above = a.trigger.top - a.pop.height - a.gap;
|
|
11038
11294
|
const fitsBelow = below + a.pop.height <= a.clamp.bottom;
|
|
@@ -11104,6 +11360,8 @@ var Popover = class {
|
|
|
11104
11360
|
this.onKey = null;
|
|
11105
11361
|
this.onReflow = null;
|
|
11106
11362
|
this.shown = false;
|
|
11363
|
+
/** The pending removal of a fading-out shell; a show() that reuses the shell cancels it. */
|
|
11364
|
+
this.leaveTimer = null;
|
|
11107
11365
|
const doc = opts.trigger.ownerDocument;
|
|
11108
11366
|
injectStyles(POPOVER_STYLE_ID, POPOVER_CSS, doc);
|
|
11109
11367
|
this.trigger = opts.trigger;
|
|
@@ -11111,6 +11369,7 @@ var Popover = class {
|
|
|
11111
11369
|
this.ctrl = popoverController(opts);
|
|
11112
11370
|
this.boundary = opts.boundary ?? "viewport";
|
|
11113
11371
|
this.theme = opts.theme;
|
|
11372
|
+
this.fadeMs = Math.max(0, opts.fadeMs ?? 0);
|
|
11114
11373
|
this.el = doc.createElement("div");
|
|
11115
11374
|
this.el.className = "vela-popover vela-ui-layer" + (opts.className ? ` ${opts.className}` : "");
|
|
11116
11375
|
this.el.dataset.position = this.ctrl.position;
|
|
@@ -11134,11 +11393,21 @@ var Popover = class {
|
|
|
11134
11393
|
return;
|
|
11135
11394
|
}
|
|
11136
11395
|
if (open && open !== this) open.hide();
|
|
11396
|
+
if (this.leaveTimer !== null) {
|
|
11397
|
+
clearTimeout(this.leaveTimer);
|
|
11398
|
+
this.leaveTimer = null;
|
|
11399
|
+
}
|
|
11137
11400
|
ensureUIHost(this.el, this.theme);
|
|
11401
|
+
if (this.fadeMs > 0) {
|
|
11402
|
+
this.el.style.transition = `opacity ${this.fadeMs}ms ease`;
|
|
11403
|
+
this.el.style.opacity = "0";
|
|
11404
|
+
this.el.style.pointerEvents = "";
|
|
11405
|
+
}
|
|
11138
11406
|
this.host.appendChild(this.el);
|
|
11139
11407
|
this.shown = true;
|
|
11140
11408
|
open = this;
|
|
11141
11409
|
this.place();
|
|
11410
|
+
if (this.fadeMs > 0) this.el.style.opacity = "1";
|
|
11142
11411
|
const onOutside = (ev) => {
|
|
11143
11412
|
const t = ev.target;
|
|
11144
11413
|
if (this.el.contains(t) || this.trigger.contains(t)) return;
|
|
@@ -11171,7 +11440,16 @@ var Popover = class {
|
|
|
11171
11440
|
this.onOutside = null;
|
|
11172
11441
|
this.onKey = null;
|
|
11173
11442
|
this.onReflow = null;
|
|
11174
|
-
this.
|
|
11443
|
+
if (this.fadeMs > 0) {
|
|
11444
|
+
this.el.style.opacity = "0";
|
|
11445
|
+
this.el.style.pointerEvents = "none";
|
|
11446
|
+
this.leaveTimer = setTimeout(() => {
|
|
11447
|
+
this.leaveTimer = null;
|
|
11448
|
+
this.el.remove();
|
|
11449
|
+
}, this.fadeMs);
|
|
11450
|
+
} else {
|
|
11451
|
+
this.el.remove();
|
|
11452
|
+
}
|
|
11175
11453
|
this.shown = false;
|
|
11176
11454
|
if (open === this) open = null;
|
|
11177
11455
|
this.ctrl.onClose?.();
|
|
@@ -15378,6 +15656,8 @@ var NATIVE_CAPABILITIES = {
|
|
|
15378
15656
|
// canvas-painted into the owning indicator's interleave slice
|
|
15379
15657
|
trades: true,
|
|
15380
15658
|
// strategy order-fill markers (arrows + labels + fill-price ticks)
|
|
15659
|
+
timelineMarks: true,
|
|
15660
|
+
// host events on a lane above the time axis (glyphs + detail popup)
|
|
15381
15661
|
inputsUI: true
|
|
15382
15662
|
// reuses the DOM InputsUI
|
|
15383
15663
|
};
|
|
@@ -15650,9 +15930,12 @@ function mergeConfig(base, patch) {
|
|
|
15650
15930
|
const gh = asObject(grid.horzLines);
|
|
15651
15931
|
const cross = asObject(p.crosshair);
|
|
15652
15932
|
const ps = asObject(p.priceScale);
|
|
15933
|
+
const anim = asObject(p.animations);
|
|
15653
15934
|
const panes = asObject(p.panes);
|
|
15654
15935
|
const trades = asObject(p.trades);
|
|
15655
15936
|
const ts = asObject(p.timeScale);
|
|
15937
|
+
const marks = asObject(p.marks);
|
|
15938
|
+
const markGroups = asObject(marks.groups);
|
|
15656
15939
|
const candles = asObject(p.candles);
|
|
15657
15940
|
const bars = asObject(p.bars);
|
|
15658
15941
|
const line = asObject(p.line);
|
|
@@ -15699,6 +15982,12 @@ function mergeConfig(base, patch) {
|
|
|
15699
15982
|
countdown: isBool(ps.countdown) ? ps.countdown : base.priceScale.countdown,
|
|
15700
15983
|
animateLastPrice: isBool(ps.animateLastPrice) ? ps.animateLastPrice : base.priceScale.animateLastPrice
|
|
15701
15984
|
},
|
|
15985
|
+
animations: {
|
|
15986
|
+
zoom: isBool(anim.zoom) ? anim.zoom : base.animations.zoom,
|
|
15987
|
+
pan: isBool(anim.pan) ? anim.pan : base.animations.pan,
|
|
15988
|
+
autoscale: isBool(anim.autoscale) ? anim.autoscale : base.animations.autoscale,
|
|
15989
|
+
intro: isBool(anim.intro) ? anim.intro : base.animations.intro
|
|
15990
|
+
},
|
|
15702
15991
|
panes: {
|
|
15703
15992
|
separatorColor: isColor(panes.separatorColor) ? panes.separatorColor : base.panes.separatorColor
|
|
15704
15993
|
},
|
|
@@ -15713,6 +16002,15 @@ function mergeConfig(base, patch) {
|
|
|
15713
16002
|
timeScale: {
|
|
15714
16003
|
timezone: typeof ts.timezone === "string" && ts.timezone ? ts.timezone : base.timeScale.timezone
|
|
15715
16004
|
},
|
|
16005
|
+
marks: {
|
|
16006
|
+
visible: isBool(marks.visible) ? marks.visible : base.marks.visible,
|
|
16007
|
+
// Additive like `stacking.series`: a patch names only the groups it carries, so a
|
|
16008
|
+
// choice stored for a group the host has not registered yet survives verbatim.
|
|
16009
|
+
groups: {
|
|
16010
|
+
...base.marks.groups,
|
|
16011
|
+
...Object.fromEntries(Object.entries(markGroups).filter(([, v]) => isBool(v)))
|
|
16012
|
+
}
|
|
16013
|
+
},
|
|
15716
16014
|
candles: {
|
|
15717
16015
|
upColor: isColor(candles.upColor) ? candles.upColor : base.candles.upColor,
|
|
15718
16016
|
downColor: isColor(candles.downColor) ? candles.downColor : base.candles.downColor,
|
|
@@ -17231,6 +17529,31 @@ var Animator = class {
|
|
|
17231
17529
|
}
|
|
17232
17530
|
}
|
|
17233
17531
|
};
|
|
17532
|
+
var EaseSetting = class {
|
|
17533
|
+
/** `defaultMs` is what the on/off switch restores when nothing else was configured;
|
|
17534
|
+
* `initialMs` (default: `defaultMs`) is the starting value — 0 for a motion that
|
|
17535
|
+
* ships off. */
|
|
17536
|
+
constructor(defaultMs, initialMs = defaultMs) {
|
|
17537
|
+
this.onMs = defaultMs;
|
|
17538
|
+
this.ms = initialMs;
|
|
17539
|
+
}
|
|
17540
|
+
/** The active time-constant; 0 when off. */
|
|
17541
|
+
get tau() {
|
|
17542
|
+
return this.ms;
|
|
17543
|
+
}
|
|
17544
|
+
get on() {
|
|
17545
|
+
return this.ms > 0;
|
|
17546
|
+
}
|
|
17547
|
+
/** Set the time-constant (0 = off). A non-zero value becomes what `toggle(true)` restores. */
|
|
17548
|
+
set(ms) {
|
|
17549
|
+
this.ms = ms;
|
|
17550
|
+
if (ms > 0) this.onMs = ms;
|
|
17551
|
+
}
|
|
17552
|
+
/** On/off only — the duration stays the last one configured. */
|
|
17553
|
+
toggle(on) {
|
|
17554
|
+
this.ms = on ? this.onMs : 0;
|
|
17555
|
+
}
|
|
17556
|
+
};
|
|
17234
17557
|
function easeToward(current, target, dtMs, tauMs) {
|
|
17235
17558
|
if (tauMs <= 0) return target;
|
|
17236
17559
|
return current + (target - current) * (1 - Math.exp(-dtMs / tauMs));
|
|
@@ -17954,6 +18277,25 @@ function drawTextLines(ctx, lines, x, firstY, step, color) {
|
|
|
17954
18277
|
for (let i = 0; i < lines.length; i += 1) ctx.fillText(lines[i], x, firstY + i * step);
|
|
17955
18278
|
}
|
|
17956
18279
|
|
|
18280
|
+
// src/renderers/shared/marks-state.ts
|
|
18281
|
+
function defaultMarksState() {
|
|
18282
|
+
return { visible: true, groups: {} };
|
|
18283
|
+
}
|
|
18284
|
+
function mergeMarksState(base, patch) {
|
|
18285
|
+
if (typeof patch === "boolean") return { visible: patch, groups: { ...base.groups } };
|
|
18286
|
+
const p = patch && typeof patch === "object" ? patch : {};
|
|
18287
|
+
const g = p.groups && typeof p.groups === "object" ? p.groups : {};
|
|
18288
|
+
const groups = { ...base.groups };
|
|
18289
|
+
for (const [id, v] of Object.entries(g)) if (typeof v === "boolean") groups[id] = v;
|
|
18290
|
+
return { visible: typeof p.visible === "boolean" ? p.visible : base.visible, groups };
|
|
18291
|
+
}
|
|
18292
|
+
function markGroupVisible(state, groupId, groups) {
|
|
18293
|
+
if (groupId === void 0) return true;
|
|
18294
|
+
const chosen = state.groups[groupId];
|
|
18295
|
+
if (typeof chosen === "boolean") return chosen;
|
|
18296
|
+
return groups.find((g) => g.id === groupId)?.visible !== false;
|
|
18297
|
+
}
|
|
18298
|
+
|
|
17957
18299
|
// src/renderers/native/core/SceneGraph.ts
|
|
17958
18300
|
var SceneGraph = class {
|
|
17959
18301
|
constructor() {
|
|
@@ -18017,6 +18359,20 @@ var SceneGraph = class {
|
|
|
18017
18359
|
/** Strategy trade-marker display (the `tradeMarkers` feature): master toggle, the
|
|
18018
18360
|
* two text lines, and the palette. Trade markers always paint on the price pane. */
|
|
18019
18361
|
this.tradeMarkers = defaultTradeMarkersState();
|
|
18362
|
+
/** Timeline-mark display (the `marks` feature): the lane's master toggle + per-group visibility. */
|
|
18363
|
+
this.marks = defaultMarksState();
|
|
18364
|
+
/** The host's timeline marks + group definitions (`setTimelineMarks`), painted on the lane above the time axis. */
|
|
18365
|
+
this.timelineMarks = [];
|
|
18366
|
+
this.markGroups = [];
|
|
18367
|
+
/** The mark stack (bar index) fanned out by hover or tap, if any. */
|
|
18368
|
+
this.marksExpandedStack = null;
|
|
18369
|
+
/** The lane glyph (cluster key) under the pointer — it pulses — and when the hover began (frame-clock ms). */
|
|
18370
|
+
this.marksHoverKey = null;
|
|
18371
|
+
this.marksHoverSince = 0;
|
|
18372
|
+
/** The cluster whose popup is open: its glyph paints filled ("active"). */
|
|
18373
|
+
this.marksActiveKey = null;
|
|
18374
|
+
/** A content-less click's brief filled flash — the cluster key and the frame-clock time it ends. */
|
|
18375
|
+
this.marksFlash = null;
|
|
18020
18376
|
/** Renderer-owned shaded time bands (session highlighting), behind grid + data. */
|
|
18021
18377
|
this.highlights = [];
|
|
18022
18378
|
/** Pre/post-market bands pushed by the host (`sessionZones` feature); null ⇒ no sessions. */
|
|
@@ -19865,6 +20221,315 @@ function timeTicks(fromMs, toMs, target = 8, offsetMs = 0) {
|
|
|
19865
20221
|
return out;
|
|
19866
20222
|
}
|
|
19867
20223
|
|
|
20224
|
+
// src/renderers/native/chrome/countdown.ts
|
|
20225
|
+
function countdownText(barOpen, barMs, now) {
|
|
20226
|
+
if (!(barMs > 0)) return null;
|
|
20227
|
+
const remaining = barOpen + barMs - now;
|
|
20228
|
+
if (remaining <= 0) return null;
|
|
20229
|
+
return formatCountdown(remaining);
|
|
20230
|
+
}
|
|
20231
|
+
function formatCountdown(ms) {
|
|
20232
|
+
const total = Math.max(0, Math.ceil(ms / 1e3));
|
|
20233
|
+
const s = total % 60;
|
|
20234
|
+
const m = Math.floor(total / 60) % 60;
|
|
20235
|
+
const h = Math.floor(total / 3600);
|
|
20236
|
+
const pad = (v) => String(v).padStart(2, "0");
|
|
20237
|
+
return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`;
|
|
20238
|
+
}
|
|
20239
|
+
|
|
20240
|
+
// src/renderers/native/chrome/contrast.ts
|
|
20241
|
+
function tagTextColor(bg, over) {
|
|
20242
|
+
const [r, g, b, a] = parseColor(bg);
|
|
20243
|
+
let R = r;
|
|
20244
|
+
let G = g;
|
|
20245
|
+
let B = b;
|
|
20246
|
+
if (a < 1) {
|
|
20247
|
+
const [or, og, ob] = parseColor(over);
|
|
20248
|
+
R = r * a + or * (1 - a);
|
|
20249
|
+
G = g * a + og * (1 - a);
|
|
20250
|
+
B = b * a + ob * (1 - a);
|
|
20251
|
+
}
|
|
20252
|
+
const lin = (c) => c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
20253
|
+
const L = 0.2126 * lin(R) + 0.7152 * lin(G) + 0.0722 * lin(B);
|
|
20254
|
+
return L >= 0.4 ? "#000000" : "#ffffff";
|
|
20255
|
+
}
|
|
20256
|
+
|
|
20257
|
+
// src/renderers/native/chrome/marks/layout.ts
|
|
20258
|
+
var MARK_GLYPH_PX = 16;
|
|
20259
|
+
var MARK_CLUSTER_PX = 20;
|
|
20260
|
+
var MARK_LANE_INSET = 4;
|
|
20261
|
+
var MARK_DECK_STEP = 3;
|
|
20262
|
+
var MARK_FAN_GAP = 4;
|
|
20263
|
+
var MARK_HIT_PAD = 3;
|
|
20264
|
+
var MARK_FAN_HOLD = 8;
|
|
20265
|
+
function snapMarkBar(time, barTimes, intervalMs2) {
|
|
20266
|
+
const n = barTimes.length;
|
|
20267
|
+
if (n === 0 || !(intervalMs2 > 0) || !Number.isFinite(time)) return null;
|
|
20268
|
+
if (time < barTimes[0]) return null;
|
|
20269
|
+
const last = barTimes[n - 1];
|
|
20270
|
+
if (time >= last + intervalMs2) return n - 1 + Math.floor((time - last) / intervalMs2);
|
|
20271
|
+
let lo = 0;
|
|
20272
|
+
let hi = n - 1;
|
|
20273
|
+
while (lo < hi) {
|
|
20274
|
+
const mid = lo + hi + 1 >> 1;
|
|
20275
|
+
if (barTimes[mid] <= time) lo = mid;
|
|
20276
|
+
else hi = mid - 1;
|
|
20277
|
+
}
|
|
20278
|
+
if (time < barTimes[lo] + intervalMs2) return lo;
|
|
20279
|
+
return lo + 1;
|
|
20280
|
+
}
|
|
20281
|
+
function clusterMarks(marks, barTimes, intervalMs2, hidden) {
|
|
20282
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
20283
|
+
marks.forEach((m, seq) => {
|
|
20284
|
+
if (m.group !== void 0 && hidden(m.group)) return;
|
|
20285
|
+
const bar = snapMarkBar(m.time, barTimes, intervalMs2);
|
|
20286
|
+
if (bar === null) return;
|
|
20287
|
+
const key = `${bar}|${m.group ?? ""}`;
|
|
20288
|
+
let c = byKey.get(key);
|
|
20289
|
+
if (!c) {
|
|
20290
|
+
c = { key, bar, group: m.group, marks: [], seq: [] };
|
|
20291
|
+
byKey.set(key, c);
|
|
20292
|
+
}
|
|
20293
|
+
c.marks.push(m);
|
|
20294
|
+
c.seq.push(seq);
|
|
20295
|
+
});
|
|
20296
|
+
const out = [];
|
|
20297
|
+
for (const c of byKey.values()) {
|
|
20298
|
+
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);
|
|
20299
|
+
out.push({ key: c.key, bar: c.bar, group: c.group, marks: order.map((o) => o.m) });
|
|
20300
|
+
}
|
|
20301
|
+
return out;
|
|
20302
|
+
}
|
|
20303
|
+
function groupRank(groups, clusters) {
|
|
20304
|
+
const rank = /* @__PURE__ */ new Map();
|
|
20305
|
+
groups.forEach((g, i) => rank.set(g.id, i));
|
|
20306
|
+
for (const c of clusters) {
|
|
20307
|
+
if (c.group !== void 0 && !rank.has(c.group)) rank.set(c.group, rank.size);
|
|
20308
|
+
}
|
|
20309
|
+
return (group) => group === void 0 ? Number.MAX_SAFE_INTEGER : rank.get(group) ?? Number.MAX_SAFE_INTEGER - 1;
|
|
20310
|
+
}
|
|
20311
|
+
function layoutMarkLane(input) {
|
|
20312
|
+
const clusters = clusterMarks(input.marks, input.barTimes, input.intervalMs, input.hidden);
|
|
20313
|
+
const rankOf = groupRank(input.groups, clusters);
|
|
20314
|
+
const byBar = /* @__PURE__ */ new Map();
|
|
20315
|
+
for (const c of clusters) {
|
|
20316
|
+
const list = byBar.get(c.bar);
|
|
20317
|
+
if (list) list.push(c);
|
|
20318
|
+
else byBar.set(c.bar, [c]);
|
|
20319
|
+
}
|
|
20320
|
+
const glyphs = [];
|
|
20321
|
+
const stacks = /* @__PURE__ */ new Map();
|
|
20322
|
+
for (const [bar, list] of byBar) {
|
|
20323
|
+
const x = input.xOf(bar);
|
|
20324
|
+
if (!Number.isFinite(x) || x < -MARK_CLUSTER_PX || x > input.dataW + MARK_CLUSTER_PX) continue;
|
|
20325
|
+
list.sort((a, b) => rankOf(a.group) - rankOf(b.group));
|
|
20326
|
+
const multi = list.length > 1;
|
|
20327
|
+
const expanded = multi && input.expanded === bar;
|
|
20328
|
+
const decked = multi && !expanded;
|
|
20329
|
+
const placed = [];
|
|
20330
|
+
const deckSize = list[0].marks.length > 1 ? MARK_CLUSTER_PX : MARK_GLYPH_PX;
|
|
20331
|
+
let bottom = input.axisY - MARK_LANE_INSET;
|
|
20332
|
+
list.forEach((cluster, depth) => {
|
|
20333
|
+
const size = decked ? deckSize : cluster.marks.length > 1 ? MARK_CLUSTER_PX : MARK_GLYPH_PX;
|
|
20334
|
+
let y;
|
|
20335
|
+
if (expanded) {
|
|
20336
|
+
y = bottom - size / 2;
|
|
20337
|
+
bottom -= size + MARK_FAN_GAP;
|
|
20338
|
+
} else {
|
|
20339
|
+
y = input.axisY - MARK_LANE_INSET - size / 2 - depth * MARK_DECK_STEP;
|
|
20340
|
+
}
|
|
20341
|
+
placed.push({ cluster, x, y, size, stack: bar, depth, decked });
|
|
20342
|
+
});
|
|
20343
|
+
for (let i = placed.length - 1; i >= 0; i--) glyphs.push(placed[i]);
|
|
20344
|
+
stacks.set(bar, placed);
|
|
20345
|
+
}
|
|
20346
|
+
return { glyphs, stacks };
|
|
20347
|
+
}
|
|
20348
|
+
function markGlyphAt(layout, x, y) {
|
|
20349
|
+
for (let i = layout.glyphs.length - 1; i >= 0; i--) {
|
|
20350
|
+
const g = layout.glyphs[i];
|
|
20351
|
+
if (g.decked && g.depth !== 0) continue;
|
|
20352
|
+
const r = g.size / 2 + MARK_HIT_PAD;
|
|
20353
|
+
if (Math.abs(x - g.x) <= r && Math.abs(y - g.y) <= r) return g;
|
|
20354
|
+
}
|
|
20355
|
+
return null;
|
|
20356
|
+
}
|
|
20357
|
+
function markStackAt(layout, x, y) {
|
|
20358
|
+
for (const [bar, placed] of layout.stacks) {
|
|
20359
|
+
for (const g of placed) {
|
|
20360
|
+
const r = g.size / 2 + MARK_HIT_PAD;
|
|
20361
|
+
if (Math.abs(x - g.x) <= r && Math.abs(y - g.y) <= r) return bar;
|
|
20362
|
+
}
|
|
20363
|
+
if (placed.length > 1 && !placed[0].decked) {
|
|
20364
|
+
const top = placed[placed.length - 1];
|
|
20365
|
+
const base = placed[0];
|
|
20366
|
+
const r = Math.max(top.size, base.size) / 2 + MARK_HIT_PAD;
|
|
20367
|
+
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;
|
|
20368
|
+
}
|
|
20369
|
+
}
|
|
20370
|
+
return null;
|
|
20371
|
+
}
|
|
20372
|
+
function clusterTooltip(cluster, groups) {
|
|
20373
|
+
const first = cluster.marks[0];
|
|
20374
|
+
if (!first) return null;
|
|
20375
|
+
if (cluster.marks.length === 1) return first.tooltip ?? first.title ?? null;
|
|
20376
|
+
const label = cluster.group !== void 0 ? markGroupLabel(cluster.group, groups) : first.title ?? first.tooltip ?? "Marks";
|
|
20377
|
+
return `${label} \xB7 ${cluster.marks.length}`;
|
|
20378
|
+
}
|
|
20379
|
+
function markGroupLabel(groupId, groups) {
|
|
20380
|
+
const def = groups.find((g) => g.id === groupId);
|
|
20381
|
+
if (def) return def.label;
|
|
20382
|
+
return groupId.charAt(0).toUpperCase() + groupId.slice(1);
|
|
20383
|
+
}
|
|
20384
|
+
function effectiveMarkGroups(marks, groups) {
|
|
20385
|
+
const out = groups.map((g) => ({ ...g }));
|
|
20386
|
+
const seen = new Set(out.map((g) => g.id));
|
|
20387
|
+
for (const m of marks) {
|
|
20388
|
+
if (m.group === void 0 || seen.has(m.group)) continue;
|
|
20389
|
+
seen.add(m.group);
|
|
20390
|
+
out.push({ id: m.group, label: markGroupLabel(m.group, groups) });
|
|
20391
|
+
}
|
|
20392
|
+
return out;
|
|
20393
|
+
}
|
|
20394
|
+
|
|
20395
|
+
// src/renderers/native/chrome/marks/paint.ts
|
|
20396
|
+
var MARK_PULSE_MS = 360;
|
|
20397
|
+
var MARK_PULSE_AMPLITUDE = 0.1;
|
|
20398
|
+
var ACTIVE_INK = "#ffffff";
|
|
20399
|
+
function pulseScale(elapsedMs) {
|
|
20400
|
+
if (!(elapsedMs > 0) || elapsedMs >= MARK_PULSE_MS) return 1;
|
|
20401
|
+
return 1 + MARK_PULSE_AMPLITUDE * Math.sin(Math.PI * elapsedMs / MARK_PULSE_MS);
|
|
20402
|
+
}
|
|
20403
|
+
function paintMarkLane(ctx, layout, deps) {
|
|
20404
|
+
if (layout.glyphs.length === 0) return;
|
|
20405
|
+
ctx.save();
|
|
20406
|
+
ctx.setLineDash([]);
|
|
20407
|
+
ctx.textAlign = "center";
|
|
20408
|
+
ctx.textBaseline = "middle";
|
|
20409
|
+
for (const g of layout.glyphs) {
|
|
20410
|
+
const mark = g.cluster.marks[0];
|
|
20411
|
+
if (!mark) continue;
|
|
20412
|
+
const key = g.cluster.key;
|
|
20413
|
+
const color = mark.glyph.color;
|
|
20414
|
+
const active = key === deps.activeKey || key === deps.flashKey;
|
|
20415
|
+
const size = g.size * (key === deps.hoverKey ? pulseScale(deps.nowMs - deps.hoverSince) : 1);
|
|
20416
|
+
if (g.depth === 0) {
|
|
20417
|
+
const sx = Math.round(g.x) + 0.5;
|
|
20418
|
+
ctx.lineWidth = 1;
|
|
20419
|
+
ctx.strokeStyle = deps.stemColor;
|
|
20420
|
+
ctx.beginPath();
|
|
20421
|
+
ctx.moveTo(sx, g.y + g.size / 2);
|
|
20422
|
+
ctx.lineTo(sx, deps.axisY);
|
|
20423
|
+
ctx.stroke();
|
|
20424
|
+
}
|
|
20425
|
+
const shape = mark.glyph.shape ?? "circle";
|
|
20426
|
+
const center = traceShape(ctx, shape, g.x, g.y, size);
|
|
20427
|
+
ctx.lineWidth = 4;
|
|
20428
|
+
ctx.strokeStyle = deps.background;
|
|
20429
|
+
ctx.stroke();
|
|
20430
|
+
ctx.fillStyle = active ? color : deps.background;
|
|
20431
|
+
ctx.fill();
|
|
20432
|
+
ctx.lineWidth = 1.5;
|
|
20433
|
+
ctx.strokeStyle = color;
|
|
20434
|
+
ctx.stroke();
|
|
20435
|
+
const ink = active ? ACTIVE_INK : color;
|
|
20436
|
+
const symbolPx = Math.round(size * 0.62);
|
|
20437
|
+
if (mark.glyph.icon) {
|
|
20438
|
+
const img = deps.icons.get(mark.glyph.icon, ink, symbolPx, deps.dpr);
|
|
20439
|
+
if (img) ctx.drawImage(img, center.x - symbolPx / 2, center.y - symbolPx / 2, symbolPx, symbolPx);
|
|
20440
|
+
} else if (mark.glyph.letter) {
|
|
20441
|
+
ctx.fillStyle = ink;
|
|
20442
|
+
ctx.font = `600 ${Math.round(size * 0.58)}px ${deps.fontFamily}`;
|
|
20443
|
+
ctx.fillText(mark.glyph.letter.slice(0, 2), center.x, center.y + 0.5);
|
|
20444
|
+
}
|
|
20445
|
+
}
|
|
20446
|
+
ctx.restore();
|
|
20447
|
+
}
|
|
20448
|
+
function traceShape(ctx, shape, x, y, size) {
|
|
20449
|
+
const r = size / 2;
|
|
20450
|
+
ctx.beginPath();
|
|
20451
|
+
switch (shape) {
|
|
20452
|
+
case "square": {
|
|
20453
|
+
const c = Math.min(3, r / 2);
|
|
20454
|
+
roundedRect(ctx, x - r, y - r, size, size, c);
|
|
20455
|
+
return { x, y };
|
|
20456
|
+
}
|
|
20457
|
+
case "diamond":
|
|
20458
|
+
ctx.moveTo(x, y - r);
|
|
20459
|
+
ctx.lineTo(x + r, y);
|
|
20460
|
+
ctx.lineTo(x, y + r);
|
|
20461
|
+
ctx.lineTo(x - r, y);
|
|
20462
|
+
ctx.closePath();
|
|
20463
|
+
return { x, y };
|
|
20464
|
+
case "pin": {
|
|
20465
|
+
const hr = r * 0.82;
|
|
20466
|
+
const hy = y - r + hr;
|
|
20467
|
+
ctx.arc(x, hy, hr, Math.PI * 0.75, Math.PI * 0.25, false);
|
|
20468
|
+
ctx.lineTo(x, y + r);
|
|
20469
|
+
ctx.closePath();
|
|
20470
|
+
return { x, y: hy };
|
|
20471
|
+
}
|
|
20472
|
+
case "circle":
|
|
20473
|
+
default:
|
|
20474
|
+
ctx.arc(x, y, r, 0, Math.PI * 2);
|
|
20475
|
+
return { x, y };
|
|
20476
|
+
}
|
|
20477
|
+
}
|
|
20478
|
+
function roundedRect(ctx, x, y, w, h, radius) {
|
|
20479
|
+
ctx.moveTo(x + radius, y);
|
|
20480
|
+
ctx.lineTo(x + w - radius, y);
|
|
20481
|
+
ctx.quadraticCurveTo(x + w, y, x + w, y + radius);
|
|
20482
|
+
ctx.lineTo(x + w, y + h - radius);
|
|
20483
|
+
ctx.quadraticCurveTo(x + w, y + h, x + w - radius, y + h);
|
|
20484
|
+
ctx.lineTo(x + radius, y + h);
|
|
20485
|
+
ctx.quadraticCurveTo(x, y + h, x, y + h - radius);
|
|
20486
|
+
ctx.lineTo(x, y + radius);
|
|
20487
|
+
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
20488
|
+
ctx.closePath();
|
|
20489
|
+
}
|
|
20490
|
+
var MarkIconRaster = class {
|
|
20491
|
+
constructor(onReady) {
|
|
20492
|
+
this.onReady = onReady;
|
|
20493
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
20494
|
+
}
|
|
20495
|
+
get(icon2, ink, px, dpr) {
|
|
20496
|
+
const key = `${icon2}|${ink}|${px}|${dpr}`;
|
|
20497
|
+
if (this.cache.has(key)) {
|
|
20498
|
+
const img2 = this.cache.get(key);
|
|
20499
|
+
return img2 && img2.complete && img2.naturalWidth > 0 ? img2 : null;
|
|
20500
|
+
}
|
|
20501
|
+
const markup = iconMarkup(icon2);
|
|
20502
|
+
if (!markup || typeof document === "undefined" || typeof Image === "undefined" || typeof XMLSerializer === "undefined") {
|
|
20503
|
+
this.cache.set(key, null);
|
|
20504
|
+
return null;
|
|
20505
|
+
}
|
|
20506
|
+
const svg = standaloneSvg(markup, ink, Math.max(1, Math.ceil(px * dpr)));
|
|
20507
|
+
if (!svg) {
|
|
20508
|
+
this.cache.set(key, null);
|
|
20509
|
+
return null;
|
|
20510
|
+
}
|
|
20511
|
+
const img = new Image();
|
|
20512
|
+
img.onload = () => this.onReady();
|
|
20513
|
+
img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
|
|
20514
|
+
this.cache.set(key, img);
|
|
20515
|
+
return null;
|
|
20516
|
+
}
|
|
20517
|
+
clear() {
|
|
20518
|
+
this.cache.clear();
|
|
20519
|
+
}
|
|
20520
|
+
};
|
|
20521
|
+
function standaloneSvg(markup, ink, px) {
|
|
20522
|
+
const tpl = document.createElement("template");
|
|
20523
|
+
tpl.innerHTML = markup;
|
|
20524
|
+
const svg = tpl.content.firstElementChild;
|
|
20525
|
+
if (!svg || svg.tagName.toLowerCase() !== "svg") return null;
|
|
20526
|
+
svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
|
20527
|
+
svg.setAttribute("width", String(px));
|
|
20528
|
+
svg.setAttribute("height", String(px));
|
|
20529
|
+
svg.setAttribute("color", ink);
|
|
20530
|
+
return new XMLSerializer().serializeToString(svg);
|
|
20531
|
+
}
|
|
20532
|
+
|
|
19868
20533
|
// src/renderers/native/chrome/ChromeRenderer.ts
|
|
19869
20534
|
var ChromeRenderer = class {
|
|
19870
20535
|
constructor() {
|
|
@@ -19874,11 +20539,40 @@ var ChromeRenderer = class {
|
|
|
19874
20539
|
this.axisTextColor = DARK_THEME.textColor;
|
|
19875
20540
|
// Shared Pine-drawing renderer, used here for autoscale geometry only; widthCache persists.
|
|
19876
20541
|
this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
|
|
20542
|
+
/** The timeline-mark lane as laid out by the last frame — what hover/click hit-test against. */
|
|
20543
|
+
this.markLayout = { glyphs: [], stacks: /* @__PURE__ */ new Map() };
|
|
20544
|
+
/** Registry icons rasterized for the lane; the owner is asked for a chrome repaint when one lands. */
|
|
20545
|
+
this.markIcons = new MarkIconRaster(() => this.onMarkIconReady?.());
|
|
20546
|
+
this.onMarkIconReady = null;
|
|
20547
|
+
/** Bar open times of the current series, rebuilt only when the array or its length changes (a live tick keeps both). */
|
|
20548
|
+
this.barTimesSrc = null;
|
|
20549
|
+
this.barTimesCache = [];
|
|
19877
20550
|
}
|
|
19878
20551
|
mount(canvas) {
|
|
19879
20552
|
this.canvas = canvas;
|
|
19880
20553
|
this.ctx = canvas.getContext("2d");
|
|
19881
20554
|
}
|
|
20555
|
+
/** Where to ask for a chrome repaint when a lane icon finishes rasterizing. */
|
|
20556
|
+
setMarkIconReady(cb) {
|
|
20557
|
+
this.onMarkIconReady = cb;
|
|
20558
|
+
}
|
|
20559
|
+
/** The interactive mark glyph under a plot point (last frame's layout), or null. */
|
|
20560
|
+
markGlyphAt(x, y) {
|
|
20561
|
+
return markGlyphAt(this.markLayout, x, y);
|
|
20562
|
+
}
|
|
20563
|
+
/** The mark stack (bar index) whose glyphs — or the gaps of its fan — cover a plot point. */
|
|
20564
|
+
markStackAt(x, y) {
|
|
20565
|
+
return markStackAt(this.markLayout, x, y);
|
|
20566
|
+
}
|
|
20567
|
+
/** A glyph of the last frame by its cluster key — how an open popup follows its anchor. */
|
|
20568
|
+
markGlyphByKey(key) {
|
|
20569
|
+
return this.markLayout.glyphs.find((g) => g.cluster.key === key) ?? null;
|
|
20570
|
+
}
|
|
20571
|
+
/** Hover text of the mark glyph under a plot point, or null. */
|
|
20572
|
+
markTooltipAt(x, y, groups) {
|
|
20573
|
+
const g = this.markGlyphAt(x, y);
|
|
20574
|
+
return g ? clusterTooltip(g.cluster, groups) : null;
|
|
20575
|
+
}
|
|
19882
20576
|
/** Wire the drawing coordinate resolvers + theme (call once per frame before use). */
|
|
19883
20577
|
prepare(scene, coords, theme) {
|
|
19884
20578
|
this.drawScene.setDeps({
|
|
@@ -19927,6 +20621,7 @@ var ChromeRenderer = class {
|
|
|
19927
20621
|
const panes = scene.orderedPanes();
|
|
19928
20622
|
if (coords.barCount === 0) {
|
|
19929
20623
|
this.drawPaneSeparators(ctx, scene, theme, fullW, panes);
|
|
20624
|
+
this.markLayout = { glyphs: [], stacks: /* @__PURE__ */ new Map() };
|
|
19930
20625
|
return;
|
|
19931
20626
|
}
|
|
19932
20627
|
const pricePane = panes.find((p) => p.kind === "price") ?? null;
|
|
@@ -19940,6 +20635,46 @@ var ChromeRenderer = class {
|
|
|
19940
20635
|
this.drawPaneSeparators(ctx, scene, theme, fullW, panes);
|
|
19941
20636
|
this.drawPriceLineAndCountdown(ctx, scene, coords, theme, dataW, pricePane);
|
|
19942
20637
|
this.drawTimeAxis(ctx, scene, coords, theme, dataW, dataH, fullH);
|
|
20638
|
+
this.drawMarkLane(ctx, scene, coords, theme, dataW, dataH);
|
|
20639
|
+
}
|
|
20640
|
+
/** The timeline-mark lane — after the axis, so the tokens read over the plot's bottom edge. */
|
|
20641
|
+
drawMarkLane(ctx, scene, coords, theme, dataW, dataH) {
|
|
20642
|
+
if (!scene.marks.visible || scene.timelineMarks.length === 0) {
|
|
20643
|
+
this.markLayout = { glyphs: [], stacks: /* @__PURE__ */ new Map() };
|
|
20644
|
+
return;
|
|
20645
|
+
}
|
|
20646
|
+
this.markLayout = layoutMarkLane({
|
|
20647
|
+
marks: scene.timelineMarks,
|
|
20648
|
+
groups: scene.markGroups,
|
|
20649
|
+
hidden: (groupId) => !markGroupVisible(scene.marks, groupId, scene.markGroups),
|
|
20650
|
+
barTimes: this.barTimes(scene),
|
|
20651
|
+
intervalMs: coords.barInterval,
|
|
20652
|
+
xOf: (bar) => coords.logicalToX(bar),
|
|
20653
|
+
axisY: dataH,
|
|
20654
|
+
dataW,
|
|
20655
|
+
expanded: scene.marksExpandedStack
|
|
20656
|
+
});
|
|
20657
|
+
const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
20658
|
+
paintMarkLane(ctx, this.markLayout, {
|
|
20659
|
+
axisY: dataH,
|
|
20660
|
+
background: theme.background,
|
|
20661
|
+
stemColor: scene.style.borderColor ?? theme.borderColor,
|
|
20662
|
+
fontFamily: theme.fontFamily,
|
|
20663
|
+
dpr: coords.dpr,
|
|
20664
|
+
icons: this.markIcons,
|
|
20665
|
+
hoverKey: scene.marksHoverKey,
|
|
20666
|
+
hoverSince: scene.marksHoverSince,
|
|
20667
|
+
activeKey: scene.marksActiveKey,
|
|
20668
|
+
flashKey: scene.marksFlash && scene.marksFlash.until > nowMs ? scene.marksFlash.key : null,
|
|
20669
|
+
nowMs
|
|
20670
|
+
});
|
|
20671
|
+
}
|
|
20672
|
+
barTimes(scene) {
|
|
20673
|
+
if (this.barTimesSrc !== scene.bars || this.barTimesCache.length !== scene.bars.length) {
|
|
20674
|
+
this.barTimesSrc = scene.bars;
|
|
20675
|
+
this.barTimesCache = scene.bars.map((b) => b.time);
|
|
20676
|
+
}
|
|
20677
|
+
return this.barTimesCache;
|
|
19943
20678
|
}
|
|
19944
20679
|
destroy() {
|
|
19945
20680
|
this.canvas = null;
|
|
@@ -20055,7 +20790,8 @@ var ChromeRenderer = class {
|
|
|
20055
20790
|
* - the countdown-to-bar-close chip (`showCountdown`).
|
|
20056
20791
|
* When the label and countdown are both on they merge into one stacked block (countdown
|
|
20057
20792
|
* under the label, text flushed left); a lone label or countdown is centered on the
|
|
20058
|
-
* price level with centered text. The countdown
|
|
20793
|
+
* price level with centered text. The countdown repaints on the renderer's second pulse
|
|
20794
|
+
* and disappears once the bar has closed, until the next bar arrives.
|
|
20059
20795
|
*/
|
|
20060
20796
|
drawPriceLineAndCountdown(ctx, scene, coords, theme, dataW, pricePane) {
|
|
20061
20797
|
const n = scene.bars.length;
|
|
@@ -20075,17 +20811,16 @@ var ChromeRenderer = class {
|
|
|
20075
20811
|
ctx.stroke();
|
|
20076
20812
|
setDash2(ctx, "solid");
|
|
20077
20813
|
}
|
|
20078
|
-
const
|
|
20079
|
-
const showCountdown =
|
|
20814
|
+
const cdText = scene.showCountdown ? countdownText(last.time, coords.barInterval, Date.now()) : null;
|
|
20815
|
+
const showCountdown = cdText !== null;
|
|
20080
20816
|
const showLabel = scene.showPriceLabel;
|
|
20081
20817
|
if (!showLabel && !showCountdown) return;
|
|
20082
20818
|
const priceText = formatAxisValue(pricePane.scale, pricePane.bounds.height, last.close, percentScaleFor(scene, pricePane), scene.priceMintick);
|
|
20083
|
-
const cdText = showCountdown ? formatCountdown(last.time + interval - Date.now()) : "";
|
|
20084
20819
|
const PAD = 8;
|
|
20085
20820
|
const x = dataW + 1;
|
|
20086
20821
|
const textColor = tagTextColor(color, theme.background);
|
|
20087
20822
|
ctx.textBaseline = "middle";
|
|
20088
|
-
if (showLabel &&
|
|
20823
|
+
if (showLabel && cdText !== null) {
|
|
20089
20824
|
const w2 = Math.max(ctx.measureText(priceText).width, ctx.measureText(cdText).width) + PAD;
|
|
20090
20825
|
const top = y - 8;
|
|
20091
20826
|
const tx = x + PAD / 2;
|
|
@@ -20098,7 +20833,7 @@ var ChromeRenderer = class {
|
|
|
20098
20833
|
ctx.textAlign = "start";
|
|
20099
20834
|
return;
|
|
20100
20835
|
}
|
|
20101
|
-
const text = showLabel ? priceText : cdText;
|
|
20836
|
+
const text = showLabel ? priceText : cdText ?? "";
|
|
20102
20837
|
const w = ctx.measureText(text).width + PAD;
|
|
20103
20838
|
ctx.fillStyle = color;
|
|
20104
20839
|
ctx.fillRect(x, y - 8, w, 16);
|
|
@@ -20169,29 +20904,6 @@ function setDash2(ctx, style) {
|
|
|
20169
20904
|
else if (style === "dotted") ctx.setLineDash([2, 3]);
|
|
20170
20905
|
else ctx.setLineDash([]);
|
|
20171
20906
|
}
|
|
20172
|
-
function tagTextColor(bg, over) {
|
|
20173
|
-
const [r, g, b, a] = parseColor(bg);
|
|
20174
|
-
let R = r;
|
|
20175
|
-
let G = g;
|
|
20176
|
-
let B = b;
|
|
20177
|
-
if (a < 1) {
|
|
20178
|
-
const [or, og, ob] = parseColor(over);
|
|
20179
|
-
R = r * a + or * (1 - a);
|
|
20180
|
-
G = g * a + og * (1 - a);
|
|
20181
|
-
B = b * a + ob * (1 - a);
|
|
20182
|
-
}
|
|
20183
|
-
const lin = (c) => c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
20184
|
-
const L = 0.2126 * lin(R) + 0.7152 * lin(G) + 0.0722 * lin(B);
|
|
20185
|
-
return L >= 0.4 ? "#000000" : "#ffffff";
|
|
20186
|
-
}
|
|
20187
|
-
function formatCountdown(ms) {
|
|
20188
|
-
const total = Math.max(0, Math.floor(ms / 1e3));
|
|
20189
|
-
const s = total % 60;
|
|
20190
|
-
const m = Math.floor(total / 60) % 60;
|
|
20191
|
-
const h = Math.floor(total / 3600);
|
|
20192
|
-
const pad = (v) => String(v).padStart(2, "0");
|
|
20193
|
-
return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`;
|
|
20194
|
-
}
|
|
20195
20907
|
|
|
20196
20908
|
// src/renderers/native/chrome/LabelTooltip.ts
|
|
20197
20909
|
var HOVER_DELAY_MS = 350;
|
|
@@ -20474,6 +21186,11 @@ function tzMenuLabel(zone, location) {
|
|
|
20474
21186
|
function settingsIdSlug(label) {
|
|
20475
21187
|
return label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
20476
21188
|
}
|
|
21189
|
+
var MARKS_SETTINGS_ID = "events";
|
|
21190
|
+
var MARKS_GROUPS_SETTINGS_ID = "events.groups";
|
|
21191
|
+
function markGroupSettingsId(groupId) {
|
|
21192
|
+
return `${MARKS_GROUPS_SETTINGS_ID}.${settingsIdSlug(groupId)}`;
|
|
21193
|
+
}
|
|
20477
21194
|
function settingsIdHidden(id, hidden) {
|
|
20478
21195
|
if (hidden.size === 0) return false;
|
|
20479
21196
|
let path = id;
|
|
@@ -20567,7 +21284,11 @@ var BUILTIN_SETTINGS_IDS = [
|
|
|
20567
21284
|
"symbol.style.baseline.base-level",
|
|
20568
21285
|
"symbol.style.baseline.width",
|
|
20569
21286
|
"symbol.animation",
|
|
21287
|
+
"symbol.animation.zoom",
|
|
21288
|
+
"symbol.animation.pan",
|
|
21289
|
+
"symbol.animation.autoscale",
|
|
20570
21290
|
"symbol.animation.price-changes",
|
|
21291
|
+
"symbol.animation.intro",
|
|
20571
21292
|
"symbol.timezone",
|
|
20572
21293
|
"scales",
|
|
20573
21294
|
"scales.price-scale",
|
|
@@ -20593,8 +21314,13 @@ var BUILTIN_SETTINGS_IDS = [
|
|
|
20593
21314
|
"canvas.grid.horizontal",
|
|
20594
21315
|
"canvas.theme"
|
|
20595
21316
|
];
|
|
20596
|
-
function settingsIdCatalog(hostSections) {
|
|
21317
|
+
function settingsIdCatalog(hostSections, markGroups = []) {
|
|
20597
21318
|
const ids = new Set(BUILTIN_SETTINGS_IDS);
|
|
21319
|
+
if (markGroups.length > 0) {
|
|
21320
|
+
ids.add(MARKS_SETTINGS_ID);
|
|
21321
|
+
ids.add(MARKS_GROUPS_SETTINGS_ID);
|
|
21322
|
+
for (const g of markGroups) ids.add(markGroupSettingsId(g.id));
|
|
21323
|
+
}
|
|
20598
21324
|
for (const def of chartTypes()) {
|
|
20599
21325
|
if (hasOwnCandlePaint(def.id)) {
|
|
20600
21326
|
const style = `symbol.style.${def.id}`;
|
|
@@ -20634,7 +21360,7 @@ function styleLabel(id) {
|
|
|
20634
21360
|
return chartType(id)?.label ?? BUILTIN_STYLE_LABELS[id] ?? id;
|
|
20635
21361
|
}
|
|
20636
21362
|
var SD_STYLE_ID = "vela-settings-controls";
|
|
20637
|
-
var SD_STYLE_REV = "
|
|
21363
|
+
var SD_STYLE_REV = "6";
|
|
20638
21364
|
var SETTINGS_BORDER = "var(--vela-border)";
|
|
20639
21365
|
function ensureControlStyles() {
|
|
20640
21366
|
if (typeof document === "undefined") return;
|
|
@@ -20710,7 +21436,15 @@ ${overlayScrollbarCss(".vela-sd-pane")}
|
|
|
20710
21436
|
.vela-sd-mobile .vela-select-trigger,.vela-sd-mobile .vela-num input,.vela-sd-mobile .vela-width-field{height:34px;}
|
|
20711
21437
|
.vela-sd-mobile .vela-sd-close{width:40px;height:40px;}
|
|
20712
21438
|
.vela-sd-mobile .vela-sd-btn{height:38px;}
|
|
20713
|
-
.vela-sd-mobile .vela-sd-row span,.vela-sd-mobile .vela-sd-bool span,.vela-sd-mobile .vela-field-label{white-space:normal !important;}
|
|
21439
|
+
.vela-sd-mobile .vela-sd-row span,.vela-sd-mobile .vela-sd-bool span,.vela-sd-mobile .vela-field-label{white-space:normal !important;}
|
|
21440
|
+
/* The wrap rule above is for ROW LABELS only: a select's closed value must keep its
|
|
21441
|
+
single-line ellipsis, or a long option wraps to several lines inside the 34px
|
|
21442
|
+
trigger and spills over the rows around it. Three classes so it outranks the
|
|
21443
|
+
two-classes-plus-element selector above. The kit's fixed 100px column is a desktop
|
|
21444
|
+
alignment device; on mobile the trigger hugs its value instead (the grid's control
|
|
21445
|
+
column is max-content), capped so a long option still ellipsizes before the label. */
|
|
21446
|
+
.vela-sd-mobile .vela-select-trigger .vela-select-label{white-space:nowrap !important;}
|
|
21447
|
+
.vela-sd-mobile .vela-select:not([data-fill]){width:auto;min-width:100px;max-width:min(220px,55vw);}`;
|
|
20714
21448
|
if (!existing) document.head.appendChild(st);
|
|
20715
21449
|
}
|
|
20716
21450
|
var SettingsDialog = class {
|
|
@@ -20725,6 +21459,9 @@ var SettingsDialog = class {
|
|
|
20725
21459
|
this.config = null;
|
|
20726
21460
|
this.syncTypeTabs = null;
|
|
20727
21461
|
this.hostSections = [];
|
|
21462
|
+
/** The timeline-mark groups (defined + named by marks) — one checkbox each on the Events tab. */
|
|
21463
|
+
this.markGroups = [];
|
|
21464
|
+
this.markGroupVisible = () => true;
|
|
20728
21465
|
/** The Canvas → Theme row: current app theme + where a pick is raised. The row is a
|
|
20729
21466
|
* host callback, NOT a config patch — the app theme stays out of the persisted
|
|
20730
21467
|
* `ChartConfig`, so exported templates never carry it. */
|
|
@@ -20749,6 +21486,11 @@ var SettingsDialog = class {
|
|
|
20749
21486
|
setHostSections(sections) {
|
|
20750
21487
|
this.hostSections = sections;
|
|
20751
21488
|
}
|
|
21489
|
+
/** The timeline-mark groups and their current visibility — the Events tab's rows on next open. */
|
|
21490
|
+
setMarkGroups(groups, visible) {
|
|
21491
|
+
this.markGroups = groups;
|
|
21492
|
+
this.markGroupVisible = visible;
|
|
21493
|
+
}
|
|
20752
21494
|
/** Replace the visibility policy — an open dialog rebuilds in place to honor it. */
|
|
20753
21495
|
setHiddenSettings(ids) {
|
|
20754
21496
|
const next = new Set(ids);
|
|
@@ -20922,12 +21664,36 @@ var SettingsDialog = class {
|
|
|
20922
21664
|
}
|
|
20923
21665
|
showActive(config.series.style);
|
|
20924
21666
|
body.append(sid(this.sectionTitle("Animation"), "symbol.animation"));
|
|
21667
|
+
body.append(sid(this.boolRow(
|
|
21668
|
+
"Animate zoom",
|
|
21669
|
+
config.animations.zoom,
|
|
21670
|
+
(v) => this.emit({ animations: { zoom: v } }),
|
|
21671
|
+
this.hint("Glide the chart to each zoom step instead of jumping.")
|
|
21672
|
+
), "symbol.animation.zoom"));
|
|
21673
|
+
body.append(sid(this.boolRow(
|
|
21674
|
+
"Pan momentum",
|
|
21675
|
+
config.animations.pan,
|
|
21676
|
+
(v) => this.emit({ animations: { pan: v } }),
|
|
21677
|
+
this.hint("Keep gliding briefly after a drag release, and ease scroll-to-latest and keyboard pans.")
|
|
21678
|
+
), "symbol.animation.pan"));
|
|
21679
|
+
body.append(sid(this.boolRow(
|
|
21680
|
+
"Animate price scale",
|
|
21681
|
+
config.animations.autoscale,
|
|
21682
|
+
(v) => this.emit({ animations: { autoscale: v } }),
|
|
21683
|
+
this.hint("Glide the price scale to its new range while zooming or panning.")
|
|
21684
|
+
), "symbol.animation.autoscale"));
|
|
20925
21685
|
body.append(sid(this.boolRow(
|
|
20926
21686
|
"Animate price changes",
|
|
20927
21687
|
config.priceScale.animateLastPrice,
|
|
20928
21688
|
(v) => this.emit({ priceScale: { animateLastPrice: v } }),
|
|
20929
21689
|
this.hint("Glide the live bar to each new price instead of snapping.")
|
|
20930
21690
|
), "symbol.animation.price-changes"));
|
|
21691
|
+
body.append(sid(this.boolRow(
|
|
21692
|
+
"Reveal on load",
|
|
21693
|
+
config.animations.intro,
|
|
21694
|
+
(v) => this.emit({ animations: { intro: v } }),
|
|
21695
|
+
this.hint("Draw the candles in when a chart first loads. Takes effect on the next load.")
|
|
21696
|
+
), "symbol.animation.intro"));
|
|
20931
21697
|
body.append(sid(this.sectionTitle("Time zone"), "symbol.timezone"));
|
|
20932
21698
|
body.append(sid(this.selectRowLabeled("Time zone", normalizeTimezone(config.timeScale.timezone), timezoneOptions(config.timeScale.timezone), (v) => this.emit({ timeScale: { timezone: v } })), "symbol.timezone"));
|
|
20933
21699
|
const renderHostSections = (placement) => {
|
|
@@ -20997,6 +21763,13 @@ var SettingsDialog = class {
|
|
|
20997
21763
|
body.append(sid(this.sectionTitle("Theme"), "canvas.theme"));
|
|
20998
21764
|
body.append(sid(this.selectRow("Color theme", tc.current === "dark" ? "Dark" : "Light", ["Dark", "Light"], (v) => tc.onSelect(v === "Dark" ? "dark" : "light")), "canvas.theme"));
|
|
20999
21765
|
}
|
|
21766
|
+
if (this.markGroups.length > 0) {
|
|
21767
|
+
body.append(sid(this.section("Events"), MARKS_SETTINGS_ID));
|
|
21768
|
+
body.append(sid(this.sectionTitle("Visible events"), MARKS_GROUPS_SETTINGS_ID));
|
|
21769
|
+
for (const g of this.markGroups) {
|
|
21770
|
+
body.append(sid(this.boolRow(g.label, this.markGroupVisible(g.id), (v) => this.emit({ marks: { groups: { [g.id]: v } } })), markGroupSettingsId(g.id)));
|
|
21771
|
+
}
|
|
21772
|
+
}
|
|
21000
21773
|
renderChartTypeSections("end");
|
|
21001
21774
|
renderHostSections("end");
|
|
21002
21775
|
if (this.hiddenSettings.size > 0) {
|
|
@@ -27201,6 +27974,383 @@ function expandScaleByPixels(scale, heightPx, abovePx, belowPx) {
|
|
|
27201
27974
|
return { ...scale, min: scale.min - belowPx * perPx, max: scale.max + abovePx * perPx };
|
|
27202
27975
|
}
|
|
27203
27976
|
|
|
27977
|
+
// src/ui/sanitize-html.ts
|
|
27978
|
+
var ALLOWED_TAGS = /* @__PURE__ */ new Set([
|
|
27979
|
+
"a",
|
|
27980
|
+
"abbr",
|
|
27981
|
+
"b",
|
|
27982
|
+
"blockquote",
|
|
27983
|
+
"br",
|
|
27984
|
+
"code",
|
|
27985
|
+
"dd",
|
|
27986
|
+
"del",
|
|
27987
|
+
"div",
|
|
27988
|
+
"dl",
|
|
27989
|
+
"dt",
|
|
27990
|
+
"em",
|
|
27991
|
+
"h1",
|
|
27992
|
+
"h2",
|
|
27993
|
+
"h3",
|
|
27994
|
+
"h4",
|
|
27995
|
+
"h5",
|
|
27996
|
+
"h6",
|
|
27997
|
+
"hr",
|
|
27998
|
+
"i",
|
|
27999
|
+
"img",
|
|
28000
|
+
"ins",
|
|
28001
|
+
"kbd",
|
|
28002
|
+
"li",
|
|
28003
|
+
"mark",
|
|
28004
|
+
"ol",
|
|
28005
|
+
"p",
|
|
28006
|
+
"pre",
|
|
28007
|
+
"q",
|
|
28008
|
+
"s",
|
|
28009
|
+
"small",
|
|
28010
|
+
"span",
|
|
28011
|
+
"strong",
|
|
28012
|
+
"sub",
|
|
28013
|
+
"sup",
|
|
28014
|
+
"table",
|
|
28015
|
+
"tbody",
|
|
28016
|
+
"td",
|
|
28017
|
+
"tfoot",
|
|
28018
|
+
"th",
|
|
28019
|
+
"thead",
|
|
28020
|
+
"tr",
|
|
28021
|
+
"u",
|
|
28022
|
+
"ul"
|
|
28023
|
+
]);
|
|
28024
|
+
var DROPPED_TAGS = /* @__PURE__ */ new Set([
|
|
28025
|
+
"script",
|
|
28026
|
+
"style",
|
|
28027
|
+
"iframe",
|
|
28028
|
+
"frame",
|
|
28029
|
+
"frameset",
|
|
28030
|
+
"object",
|
|
28031
|
+
"embed",
|
|
28032
|
+
"applet",
|
|
28033
|
+
"form",
|
|
28034
|
+
"input",
|
|
28035
|
+
"textarea",
|
|
28036
|
+
"button",
|
|
28037
|
+
"select",
|
|
28038
|
+
"option",
|
|
28039
|
+
"link",
|
|
28040
|
+
"meta",
|
|
28041
|
+
"base",
|
|
28042
|
+
"svg",
|
|
28043
|
+
"math",
|
|
28044
|
+
"template",
|
|
28045
|
+
"noscript",
|
|
28046
|
+
"audio",
|
|
28047
|
+
"video",
|
|
28048
|
+
"canvas",
|
|
28049
|
+
"dialog",
|
|
28050
|
+
"head",
|
|
28051
|
+
"title"
|
|
28052
|
+
]);
|
|
28053
|
+
var ALLOWED_ATTRS = {
|
|
28054
|
+
a: /* @__PURE__ */ new Set(["href"]),
|
|
28055
|
+
img: /* @__PURE__ */ new Set(["src", "alt", "width", "height"]),
|
|
28056
|
+
td: /* @__PURE__ */ new Set(["colspan", "rowspan"]),
|
|
28057
|
+
th: /* @__PURE__ */ new Set(["colspan", "rowspan"]),
|
|
28058
|
+
ol: /* @__PURE__ */ new Set(["start"])
|
|
28059
|
+
};
|
|
28060
|
+
var BLOCKED_SCHEMES = /* @__PURE__ */ new Set(["javascript", "vbscript", "data", "file", "blob"]);
|
|
28061
|
+
function tagDisposition(tag) {
|
|
28062
|
+
const t = tag.toLowerCase();
|
|
28063
|
+
if (DROPPED_TAGS.has(t)) return "drop";
|
|
28064
|
+
return ALLOWED_TAGS.has(t) ? "keep" : "unwrap";
|
|
28065
|
+
}
|
|
28066
|
+
function attributeAllowed(tag, name) {
|
|
28067
|
+
const n = name.toLowerCase();
|
|
28068
|
+
if (n.startsWith("on") || n === "style") return false;
|
|
28069
|
+
if (n === "title") return true;
|
|
28070
|
+
return ALLOWED_ATTRS[tag.toLowerCase()]?.has(n) ?? false;
|
|
28071
|
+
}
|
|
28072
|
+
function safeUrl(value, absoluteOnly = false) {
|
|
28073
|
+
let url = "";
|
|
28074
|
+
for (const ch of value) if (ch.charCodeAt(0) > 32) url += ch;
|
|
28075
|
+
const m = /^([a-z][a-z0-9+.-]*):/i.exec(url);
|
|
28076
|
+
const scheme = m ? m[1].toLowerCase() : null;
|
|
28077
|
+
if (scheme !== null && BLOCKED_SCHEMES.has(scheme)) return null;
|
|
28078
|
+
if (absoluteOnly && scheme !== "http" && scheme !== "https") return null;
|
|
28079
|
+
return url;
|
|
28080
|
+
}
|
|
28081
|
+
var ELEMENT_NODE = 1;
|
|
28082
|
+
var TEXT_NODE = 3;
|
|
28083
|
+
function sanitizeHtml(html, doc) {
|
|
28084
|
+
const tpl = doc.createElement("template");
|
|
28085
|
+
tpl.innerHTML = html;
|
|
28086
|
+
const out = doc.createDocumentFragment();
|
|
28087
|
+
copyChildren(tpl.content, out, doc);
|
|
28088
|
+
return out;
|
|
28089
|
+
}
|
|
28090
|
+
function copyChildren(from, to, doc) {
|
|
28091
|
+
for (const child of Array.from(from.childNodes)) {
|
|
28092
|
+
if (child.nodeType === TEXT_NODE) {
|
|
28093
|
+
to.appendChild(doc.createTextNode(child.textContent ?? ""));
|
|
28094
|
+
continue;
|
|
28095
|
+
}
|
|
28096
|
+
if (child.nodeType !== ELEMENT_NODE) continue;
|
|
28097
|
+
const el = child;
|
|
28098
|
+
const tag = el.tagName.toLowerCase();
|
|
28099
|
+
const disposition = tagDisposition(tag);
|
|
28100
|
+
if (disposition === "drop") continue;
|
|
28101
|
+
if (disposition === "unwrap") {
|
|
28102
|
+
copyChildren(el, to, doc);
|
|
28103
|
+
continue;
|
|
28104
|
+
}
|
|
28105
|
+
const clean = doc.createElement(tag);
|
|
28106
|
+
for (const attr of Array.from(el.attributes)) {
|
|
28107
|
+
const name = attr.name.toLowerCase();
|
|
28108
|
+
if (!attributeAllowed(tag, name)) continue;
|
|
28109
|
+
let value = attr.value;
|
|
28110
|
+
if (name === "href" || name === "src") {
|
|
28111
|
+
const safe = safeUrl(value, name === "src");
|
|
28112
|
+
if (safe === null) continue;
|
|
28113
|
+
value = safe;
|
|
28114
|
+
}
|
|
28115
|
+
clean.setAttribute(name, value);
|
|
28116
|
+
}
|
|
28117
|
+
if (tag === "a") {
|
|
28118
|
+
clean.setAttribute("target", "_blank");
|
|
28119
|
+
clean.setAttribute("rel", "noopener noreferrer");
|
|
28120
|
+
}
|
|
28121
|
+
copyChildren(el, clean, doc);
|
|
28122
|
+
to.appendChild(clean);
|
|
28123
|
+
}
|
|
28124
|
+
}
|
|
28125
|
+
|
|
28126
|
+
// src/renderers/native/chrome/marks/MarkPopover.ts
|
|
28127
|
+
var MARKS_STYLE_ID = "vela-marks-popover";
|
|
28128
|
+
var MARKS_CSS = `
|
|
28129
|
+
.vela-marks-panel { padding: 0; gap: 0; min-width: 220px; max-width: 320px; max-height: 320px; overflow-y: auto; overscroll-behavior: contain; }
|
|
28130
|
+
.vela-marks-section { display: flex; flex-direction: column; gap: 8px; padding: 10px 12px; }
|
|
28131
|
+
.vela-marks-section + .vela-marks-section { border-top: 1px solid var(--vela-border); }
|
|
28132
|
+
.vela-marks-field { display: flex; justify-content: space-between; gap: 16px; line-height: 1.45; }
|
|
28133
|
+
.vela-marks-field-label { color: var(--vela-fg-muted); }
|
|
28134
|
+
.vela-marks-field-value { color: var(--vela-fg-bright); text-align: right; font-variant-numeric: tabular-nums; }
|
|
28135
|
+
.vela-marks-html { color: var(--vela-fg); line-height: 1.45; overflow-wrap: anywhere; }
|
|
28136
|
+
.vela-marks-html p { margin: 0 0 6px; }
|
|
28137
|
+
.vela-marks-html p:last-child { margin-bottom: 0; }
|
|
28138
|
+
.vela-marks-html a { color: var(--vela-accent); }
|
|
28139
|
+
.vela-marks-html img { max-width: 100%; height: auto; }
|
|
28140
|
+
.vela-marks-html table { border-collapse: collapse; }
|
|
28141
|
+
.vela-marks-html td, .vela-marks-html th { padding: 2px 6px; border: 1px solid var(--vela-border); }
|
|
28142
|
+
.vela-marks-html pre, .vela-marks-html code { font-family: var(--vela-font-mono, monospace); font-size: 0.92em; }
|
|
28143
|
+
.vela-marks-loading, .vela-marks-error { color: var(--vela-fg-muted); font-style: italic; }
|
|
28144
|
+
`;
|
|
28145
|
+
var MarkPopover = class {
|
|
28146
|
+
constructor(deps) {
|
|
28147
|
+
this.deps = deps;
|
|
28148
|
+
this.pop = null;
|
|
28149
|
+
this.openKey = null;
|
|
28150
|
+
/** Bumped per open/close — a lazy content resolving after its popup went away is dropped. */
|
|
28151
|
+
this.generation = 0;
|
|
28152
|
+
const doc = deps.plot.ownerDocument;
|
|
28153
|
+
injectStyles(CALLOUT_STYLE_ID, CALLOUT_CSS, doc);
|
|
28154
|
+
injectStyles(MARKS_STYLE_ID, MARKS_CSS, doc);
|
|
28155
|
+
this.anchor = doc.createElement("div");
|
|
28156
|
+
this.anchor.className = "vela-marks-anchor";
|
|
28157
|
+
Object.assign(this.anchor.style, { position: "absolute", pointerEvents: "none", left: "0", top: "0", width: "0", height: "0" });
|
|
28158
|
+
deps.plot.appendChild(this.anchor);
|
|
28159
|
+
}
|
|
28160
|
+
/** The cluster key the open popup belongs to, or null. */
|
|
28161
|
+
get key() {
|
|
28162
|
+
return this.openKey;
|
|
28163
|
+
}
|
|
28164
|
+
open(cluster, rect) {
|
|
28165
|
+
this.close();
|
|
28166
|
+
this.place(rect);
|
|
28167
|
+
const gen = ++this.generation;
|
|
28168
|
+
this.openKey = cluster.key;
|
|
28169
|
+
this.pop = new Popover({
|
|
28170
|
+
trigger: this.anchor,
|
|
28171
|
+
host: this.deps.host(),
|
|
28172
|
+
theme: this.deps.theme(),
|
|
28173
|
+
gap: 8,
|
|
28174
|
+
align: "center",
|
|
28175
|
+
// centered on the glyph
|
|
28176
|
+
fadeMs: 120,
|
|
28177
|
+
// a short, discreet fade in and out
|
|
28178
|
+
className: "vela-marks-pop",
|
|
28179
|
+
content: (body) => this.build(body, cluster, gen),
|
|
28180
|
+
onClose: () => {
|
|
28181
|
+
if (this.generation === gen) {
|
|
28182
|
+
this.openKey = null;
|
|
28183
|
+
this.pop = null;
|
|
28184
|
+
this.deps.onOpenChange?.(null);
|
|
28185
|
+
}
|
|
28186
|
+
}
|
|
28187
|
+
});
|
|
28188
|
+
this.pop.show();
|
|
28189
|
+
this.deps.onOpenChange?.(cluster.key);
|
|
28190
|
+
}
|
|
28191
|
+
/** Follow the anchor glyph after a repaint; `null` (glyph gone — hidden, scrolled off, marks replaced) closes. */
|
|
28192
|
+
track(rect) {
|
|
28193
|
+
if (!this.pop) return;
|
|
28194
|
+
if (!rect) {
|
|
28195
|
+
this.close();
|
|
28196
|
+
return;
|
|
28197
|
+
}
|
|
28198
|
+
this.place(rect);
|
|
28199
|
+
this.pop.reposition();
|
|
28200
|
+
}
|
|
28201
|
+
close() {
|
|
28202
|
+
const pop = this.pop;
|
|
28203
|
+
const wasOpen = this.openKey !== null;
|
|
28204
|
+
this.pop = null;
|
|
28205
|
+
this.openKey = null;
|
|
28206
|
+
this.generation++;
|
|
28207
|
+
pop?.destroy();
|
|
28208
|
+
if (wasOpen) this.deps.onOpenChange?.(null);
|
|
28209
|
+
}
|
|
28210
|
+
destroy() {
|
|
28211
|
+
this.close();
|
|
28212
|
+
this.anchor.remove();
|
|
28213
|
+
}
|
|
28214
|
+
place(rect) {
|
|
28215
|
+
Object.assign(this.anchor.style, {
|
|
28216
|
+
left: `${rect.x - rect.size / 2}px`,
|
|
28217
|
+
top: `${rect.y - rect.size / 2}px`,
|
|
28218
|
+
width: `${rect.size}px`,
|
|
28219
|
+
height: `${rect.size}px`
|
|
28220
|
+
});
|
|
28221
|
+
}
|
|
28222
|
+
build(body, cluster, gen) {
|
|
28223
|
+
const doc = body.ownerDocument;
|
|
28224
|
+
const root = doc.createElement("div");
|
|
28225
|
+
root.className = "vela-callout-panel vela-marks-panel";
|
|
28226
|
+
const pending = [];
|
|
28227
|
+
for (const mark of cluster.marks) {
|
|
28228
|
+
const section = doc.createElement("section");
|
|
28229
|
+
section.className = "vela-marks-section";
|
|
28230
|
+
if (mark.title) {
|
|
28231
|
+
const title = doc.createElement("div");
|
|
28232
|
+
title.className = "vela-callout-title";
|
|
28233
|
+
title.textContent = mark.title;
|
|
28234
|
+
section.appendChild(title);
|
|
28235
|
+
}
|
|
28236
|
+
const content = mark.content;
|
|
28237
|
+
if (typeof content === "function") {
|
|
28238
|
+
const slot = doc.createElement("div");
|
|
28239
|
+
slot.className = "vela-marks-loading";
|
|
28240
|
+
slot.textContent = "Loading\u2026";
|
|
28241
|
+
section.appendChild(slot);
|
|
28242
|
+
pending.push({ el: section, resolve: () => this.resolveLazy(content, slot, gen) });
|
|
28243
|
+
} else if (content !== void 0) {
|
|
28244
|
+
this.renderContent(section, content);
|
|
28245
|
+
}
|
|
28246
|
+
if (section.childElementCount > 0) root.appendChild(section);
|
|
28247
|
+
}
|
|
28248
|
+
body.appendChild(root);
|
|
28249
|
+
if (pending.length > 0) scheduleLazy(root, pending);
|
|
28250
|
+
}
|
|
28251
|
+
resolveLazy(source, slot, gen) {
|
|
28252
|
+
let result;
|
|
28253
|
+
try {
|
|
28254
|
+
result = Promise.resolve(source());
|
|
28255
|
+
} catch (err) {
|
|
28256
|
+
result = Promise.reject(err instanceof Error ? err : new Error(String(err)));
|
|
28257
|
+
}
|
|
28258
|
+
void result.then(
|
|
28259
|
+
(content) => {
|
|
28260
|
+
if (gen !== this.generation) return;
|
|
28261
|
+
const section = slot.parentElement;
|
|
28262
|
+
if (!section) return;
|
|
28263
|
+
slot.remove();
|
|
28264
|
+
this.renderContent(section, content);
|
|
28265
|
+
this.pop?.reposition();
|
|
28266
|
+
},
|
|
28267
|
+
() => {
|
|
28268
|
+
if (gen !== this.generation) return;
|
|
28269
|
+
slot.className = "vela-marks-error";
|
|
28270
|
+
slot.textContent = "Couldn\u2019t load this entry.";
|
|
28271
|
+
}
|
|
28272
|
+
);
|
|
28273
|
+
}
|
|
28274
|
+
renderContent(section, content) {
|
|
28275
|
+
const doc = section.ownerDocument;
|
|
28276
|
+
if (!content || typeof content !== "object") return;
|
|
28277
|
+
if ("text" in content) {
|
|
28278
|
+
const text = doc.createElement("div");
|
|
28279
|
+
text.className = "vela-callout-text";
|
|
28280
|
+
text.textContent = String(content.text);
|
|
28281
|
+
section.appendChild(text);
|
|
28282
|
+
return;
|
|
28283
|
+
}
|
|
28284
|
+
if ("html" in content) {
|
|
28285
|
+
const html = doc.createElement("div");
|
|
28286
|
+
html.className = "vela-marks-html";
|
|
28287
|
+
html.appendChild(sanitizeHtml(String(content.html), doc));
|
|
28288
|
+
section.appendChild(html);
|
|
28289
|
+
return;
|
|
28290
|
+
}
|
|
28291
|
+
if ("panel" in content && content.panel && Array.isArray(content.panel.items)) {
|
|
28292
|
+
let actions = null;
|
|
28293
|
+
for (const item of content.panel.items) {
|
|
28294
|
+
if (!item || typeof item !== "object") continue;
|
|
28295
|
+
if (item.type === "button") {
|
|
28296
|
+
if (!actions) {
|
|
28297
|
+
actions = doc.createElement("div");
|
|
28298
|
+
actions.className = "vela-callout-actions";
|
|
28299
|
+
section.appendChild(actions);
|
|
28300
|
+
}
|
|
28301
|
+
const btn2 = doc.createElement("button");
|
|
28302
|
+
btn2.type = "button";
|
|
28303
|
+
btn2.className = "vela-callout-btn" + (item.primary ? " vela-callout-btn-primary" : "");
|
|
28304
|
+
btn2.textContent = item.label;
|
|
28305
|
+
btn2.addEventListener("click", () => {
|
|
28306
|
+
item.run();
|
|
28307
|
+
if (item.close !== false) this.close();
|
|
28308
|
+
});
|
|
28309
|
+
actions.appendChild(btn2);
|
|
28310
|
+
continue;
|
|
28311
|
+
}
|
|
28312
|
+
actions = null;
|
|
28313
|
+
if (item.type === "text") {
|
|
28314
|
+
const text = doc.createElement("div");
|
|
28315
|
+
text.className = "vela-callout-text";
|
|
28316
|
+
text.textContent = item.text;
|
|
28317
|
+
section.appendChild(text);
|
|
28318
|
+
} else if (item.type === "field") {
|
|
28319
|
+
const row = doc.createElement("div");
|
|
28320
|
+
row.className = "vela-marks-field";
|
|
28321
|
+
const label = doc.createElement("span");
|
|
28322
|
+
label.className = "vela-marks-field-label";
|
|
28323
|
+
label.textContent = item.label;
|
|
28324
|
+
const value = doc.createElement("span");
|
|
28325
|
+
value.className = "vela-marks-field-value";
|
|
28326
|
+
value.textContent = item.value;
|
|
28327
|
+
row.append(label, value);
|
|
28328
|
+
section.appendChild(row);
|
|
28329
|
+
}
|
|
28330
|
+
}
|
|
28331
|
+
}
|
|
28332
|
+
}
|
|
28333
|
+
};
|
|
28334
|
+
function scheduleLazy(scroller, pending) {
|
|
28335
|
+
if (typeof IntersectionObserver === "undefined") {
|
|
28336
|
+
for (const p of pending) p.resolve();
|
|
28337
|
+
return;
|
|
28338
|
+
}
|
|
28339
|
+
const io = new IntersectionObserver(
|
|
28340
|
+
(entries) => {
|
|
28341
|
+
for (const e of entries) {
|
|
28342
|
+
if (!e.isIntersecting) continue;
|
|
28343
|
+
const p = pending.find((q) => q.el === e.target);
|
|
28344
|
+
if (!p) continue;
|
|
28345
|
+
io.unobserve(e.target);
|
|
28346
|
+
p.resolve();
|
|
28347
|
+
}
|
|
28348
|
+
},
|
|
28349
|
+
{ root: scroller }
|
|
28350
|
+
);
|
|
28351
|
+
for (const p of pending) io.observe(p.el);
|
|
28352
|
+
}
|
|
28353
|
+
|
|
27204
28354
|
// src/renderers/native/core/manualScale.ts
|
|
27205
28355
|
function rescaleAround(start, factor) {
|
|
27206
28356
|
if (start.log && start.min > 0 && start.max > start.min) {
|
|
@@ -27929,11 +29079,12 @@ var SCROLL_BTN_BOTTOM = TIME_AXIS_H + 14;
|
|
|
27929
29079
|
var SCROLL_BTN_PROXIMITY_PX = 120;
|
|
27930
29080
|
var MIN_VISIBLE_BARS = 2;
|
|
27931
29081
|
var ZOOM_OUT_MARGIN_BARS = 6;
|
|
27932
|
-
var
|
|
27933
|
-
var SCALE_TAU_MS = 80;
|
|
27934
|
-
var FLING_TAU_MS = 110;
|
|
27935
|
-
var SCROLL_TO_TAU_MS = 130;
|
|
29082
|
+
var INTRO_MODEL_FADE_MS = 350;
|
|
27936
29083
|
var FLING_STOP_PX = 0.02;
|
|
29084
|
+
var MARK_FLASH_MS = 220;
|
|
29085
|
+
function frameNow() {
|
|
29086
|
+
return typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
29087
|
+
}
|
|
27937
29088
|
var PRICE_SCALE_K = 4e-3;
|
|
27938
29089
|
var KEY_ZOOM_STEP = 0.2;
|
|
27939
29090
|
var SEPARATOR_HIT_PX = 4;
|
|
@@ -27982,9 +29133,20 @@ var NativeRenderer = class {
|
|
|
27982
29133
|
this.indicatorSlices = new IndicatorDrawingSlices();
|
|
27983
29134
|
/** Hover tooltips for Pine labels (canvas hit-rects collected by the chrome layer). */
|
|
27984
29135
|
this.labelTooltip = null;
|
|
29136
|
+
/** The timeline-mark popup (a kit Popover anchored on a lane glyph); null before mount. */
|
|
29137
|
+
this.markPopover = null;
|
|
29138
|
+
/** How the fanned mark stack was opened: a hover folds when the pointer leaves, a tap only on a tap elsewhere. */
|
|
29139
|
+
this.marksExpandedBy = "hover";
|
|
29140
|
+
/** The rAF loop keeping the chrome repainting while a lane glyph pulses (hover) or flashes (click); null when idle. */
|
|
29141
|
+
this.markPulseRaf = null;
|
|
29142
|
+
this.markClickCbs = /* @__PURE__ */ new Set();
|
|
27985
29143
|
this.crosshairLayer = new CrosshairRenderer();
|
|
27986
|
-
/**
|
|
27987
|
-
|
|
29144
|
+
/** The second pulse the countdown-to-bar-close chip ticks on: the host's (`setWallClock`)
|
|
29145
|
+
* when one is wired, else the renderer's own second-aligned clock. */
|
|
29146
|
+
this.hostClock = null;
|
|
29147
|
+
this.ownClock = null;
|
|
29148
|
+
/** Live subscription to the pulse while the countdown is on; null when off. */
|
|
29149
|
+
this.countdownUnsub = null;
|
|
27988
29150
|
this.symbolPicker = null;
|
|
27989
29151
|
/** Indicator titles (the legend rows) shown — held here so a remount re-applies it. */
|
|
27990
29152
|
this.indicatorTitlesOn = true;
|
|
@@ -28002,19 +29164,25 @@ var NativeRenderer = class {
|
|
|
28002
29164
|
/** Drawings layer self-serves Ctrl+Z/Y (see the `historyChords` feature). */
|
|
28003
29165
|
this.historyChordsEnabled = true;
|
|
28004
29166
|
this.liveRegion = null;
|
|
28005
|
-
// ── animation state
|
|
28006
|
-
|
|
28007
|
-
this.
|
|
28008
|
-
|
|
28009
|
-
|
|
28010
|
-
|
|
28011
|
-
|
|
29167
|
+
// ── animation state: one ease time-constant per motion (0 = off), each remembering the
|
|
29168
|
+
// host's duration so the config's on/off switches restore it (see EaseSetting) ──
|
|
29169
|
+
this.animZoom = new EaseSetting(ZOOM_EASE_DEFAULT_MS);
|
|
29170
|
+
// wheel-zoom glide
|
|
29171
|
+
this.animPan = new EaseSetting(PAN_INERTIA_DEFAULT_MS);
|
|
29172
|
+
// inertial-pan velocity decay
|
|
29173
|
+
this.animScroll = new EaseSetting(SCROLL_EASE_DEFAULT_MS);
|
|
29174
|
+
// scroll-to-latest / panBy glide
|
|
29175
|
+
this.animAutoscale = new EaseSetting(AUTOSCALE_EASE_DEFAULT_MS);
|
|
29176
|
+
// autoscale glide during zoom/fling
|
|
29177
|
+
this.animLiveBar = new EaseSetting(LIVE_BAR_EASE_DEFAULT_MS, 0);
|
|
29178
|
+
// forming-bar OHLC glide; ships off
|
|
28012
29179
|
// Brand default candles.
|
|
28013
29180
|
this.candleUp = BULLISH;
|
|
28014
29181
|
this.candleDown = BEARISH;
|
|
28015
29182
|
// ── intro reveal (plays once when candles first appear) ──
|
|
28016
|
-
this.
|
|
28017
|
-
|
|
29183
|
+
this.intro = { style: "settle", duration: INTRO_DURATION_DEFAULT_MS };
|
|
29184
|
+
this.introOnStyle = "settle";
|
|
29185
|
+
// the style the config's on/off switch restores
|
|
28018
29186
|
this.introPlayed = false;
|
|
28019
29187
|
this.introRaf = null;
|
|
28020
29188
|
/** The load affordance (three pulsing dots) — up while the host reports a bar load in
|
|
@@ -28133,7 +29301,7 @@ var NativeRenderer = class {
|
|
|
28133
29301
|
this.moveIndicatorCbs = /* @__PURE__ */ new Set();
|
|
28134
29302
|
this.priceStyleCbs = /* @__PURE__ */ new Set();
|
|
28135
29303
|
this.name = "native";
|
|
28136
|
-
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"];
|
|
29304
|
+
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"];
|
|
28137
29305
|
/** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
|
|
28138
29306
|
* so moving onto the button doesn't count as leaving). */
|
|
28139
29307
|
this.onScrollProximityMove = (e) => {
|
|
@@ -28165,9 +29333,12 @@ var NativeRenderer = class {
|
|
|
28165
29333
|
this.scene.showPriceLine = opts.currentPriceLine;
|
|
28166
29334
|
this.scene.logScale = opts.logScale;
|
|
28167
29335
|
this.backendMode = opts.nativeBackend;
|
|
28168
|
-
this.animZoom
|
|
28169
|
-
this.animPan
|
|
28170
|
-
this.
|
|
29336
|
+
this.animZoom.set(opts.animZoom);
|
|
29337
|
+
this.animPan.set(opts.animPan);
|
|
29338
|
+
this.animScroll.set(opts.animScroll);
|
|
29339
|
+
this.animAutoscale.set(opts.animAutoscale);
|
|
29340
|
+
this.animLiveBar.set(opts.animLiveBar);
|
|
29341
|
+
this.setIntro(opts.animIntro);
|
|
28171
29342
|
this.glowAmount = opts.glow;
|
|
28172
29343
|
this.candleUp = opts.upColor;
|
|
28173
29344
|
this.candleDown = opts.downColor;
|
|
@@ -28211,20 +29382,29 @@ var NativeRenderer = class {
|
|
|
28211
29382
|
if (this.backend && "glow" in this.backend) this.backend.glow = this.glowAmount;
|
|
28212
29383
|
break;
|
|
28213
29384
|
case "animZoom":
|
|
28214
|
-
this.animZoom
|
|
29385
|
+
this.animZoom.set(resolveEaseMs(value, ZOOM_EASE_DEFAULT_MS));
|
|
28215
29386
|
return;
|
|
28216
29387
|
// affects the next interaction only — nothing to repaint
|
|
28217
|
-
case "animPan":
|
|
28218
|
-
|
|
29388
|
+
case "animPan": {
|
|
29389
|
+
const ms = resolveEaseMs(value, PAN_INERTIA_DEFAULT_MS);
|
|
29390
|
+
this.animPan.set(ms);
|
|
29391
|
+
this.animScroll.toggle(ms > 0);
|
|
29392
|
+
return;
|
|
29393
|
+
}
|
|
29394
|
+
case "animScroll":
|
|
29395
|
+
this.animScroll.set(resolveEaseMs(value, SCROLL_EASE_DEFAULT_MS));
|
|
29396
|
+
return;
|
|
29397
|
+
case "animAutoscale":
|
|
29398
|
+
this.animAutoscale.set(resolveEaseMs(value, AUTOSCALE_EASE_DEFAULT_MS));
|
|
28219
29399
|
return;
|
|
29400
|
+
// a glide in flight finishes at the new rate (or snaps at 0)
|
|
28220
29401
|
case "animLiveBar":
|
|
28221
|
-
this.
|
|
29402
|
+
this.animLiveBar.set(resolveLiveBarEaseMs(value));
|
|
28222
29403
|
return;
|
|
28223
29404
|
// affects the next tick only; a glide in flight finishes at the new rate (or snaps at 0)
|
|
28224
29405
|
case "intro": {
|
|
28225
|
-
|
|
28226
|
-
this.
|
|
28227
|
-
if (s) this.playIntro(s);
|
|
29406
|
+
this.setIntro(resolveIntro(value));
|
|
29407
|
+
if (this.intro.style) this.playIntro();
|
|
28228
29408
|
return;
|
|
28229
29409
|
}
|
|
28230
29410
|
case "zoomAnchor":
|
|
@@ -28296,6 +29476,10 @@ var NativeRenderer = class {
|
|
|
28296
29476
|
case "tradeMarkers":
|
|
28297
29477
|
this.scene.tradeMarkers = mergeTradeMarkersState(this.scene.tradeMarkers, value);
|
|
28298
29478
|
break;
|
|
29479
|
+
case "marks":
|
|
29480
|
+
this.scene.marks = mergeMarksState(this.scene.marks, value);
|
|
29481
|
+
this.markPopover?.close();
|
|
29482
|
+
break;
|
|
28299
29483
|
case "keyboard":
|
|
28300
29484
|
this.setKeyboardEnabled(Boolean(value));
|
|
28301
29485
|
return;
|
|
@@ -28354,13 +29538,17 @@ var NativeRenderer = class {
|
|
|
28354
29538
|
case "glow":
|
|
28355
29539
|
return this.glowAmount;
|
|
28356
29540
|
case "animZoom":
|
|
28357
|
-
return this.animZoom;
|
|
29541
|
+
return this.animZoom.tau;
|
|
28358
29542
|
case "animPan":
|
|
28359
|
-
return this.animPan;
|
|
29543
|
+
return this.animPan.tau;
|
|
29544
|
+
case "animScroll":
|
|
29545
|
+
return this.animScroll.tau;
|
|
29546
|
+
case "animAutoscale":
|
|
29547
|
+
return this.animAutoscale.tau;
|
|
28360
29548
|
case "animLiveBar":
|
|
28361
|
-
return this.
|
|
29549
|
+
return this.animLiveBar.tau;
|
|
28362
29550
|
case "intro":
|
|
28363
|
-
return this.
|
|
29551
|
+
return this.intro.style;
|
|
28364
29552
|
case "zoomAnchor":
|
|
28365
29553
|
return this.zoomAnchorMode;
|
|
28366
29554
|
case "axisDrag":
|
|
@@ -28403,6 +29591,8 @@ var NativeRenderer = class {
|
|
|
28403
29591
|
}
|
|
28404
29592
|
case "tradeMarkers":
|
|
28405
29593
|
return { ...this.scene.tradeMarkers, colors: { ...this.scene.tradeMarkers.colors } };
|
|
29594
|
+
case "marks":
|
|
29595
|
+
return { visible: this.scene.marks.visible, groups: { ...this.scene.marks.groups } };
|
|
28406
29596
|
case "keyboard":
|
|
28407
29597
|
return this.keyboardEnabled;
|
|
28408
29598
|
case "historyChords":
|
|
@@ -28528,7 +29718,13 @@ var NativeRenderer = class {
|
|
|
28528
29718
|
currentPriceLine: this.scene.showPriceLine,
|
|
28529
29719
|
priceLabel: this.scene.showPriceLabel,
|
|
28530
29720
|
countdown: this.scene.showCountdown,
|
|
28531
|
-
animateLastPrice: this.
|
|
29721
|
+
animateLastPrice: this.animLiveBar.on
|
|
29722
|
+
},
|
|
29723
|
+
animations: {
|
|
29724
|
+
zoom: this.animZoom.on,
|
|
29725
|
+
pan: this.animPan.on,
|
|
29726
|
+
autoscale: this.animAutoscale.on,
|
|
29727
|
+
intro: this.intro.style !== false
|
|
28532
29728
|
},
|
|
28533
29729
|
panes: { separatorColor: s.separatorColor ?? t.borderColor },
|
|
28534
29730
|
trades: {
|
|
@@ -28540,6 +29736,7 @@ var NativeRenderer = class {
|
|
|
28540
29736
|
exitColor: this.scene.tradeMarkers.colors.exit
|
|
28541
29737
|
},
|
|
28542
29738
|
timeScale: { timezone: this.scene.timezone },
|
|
29739
|
+
marks: { visible: this.scene.marks.visible, groups: { ...this.scene.marks.groups } },
|
|
28543
29740
|
candles: {
|
|
28544
29741
|
upColor: this.candleUp,
|
|
28545
29742
|
downColor: this.candleDown,
|
|
@@ -28635,7 +29832,12 @@ var NativeRenderer = class {
|
|
|
28635
29832
|
this.scene.showPriceLabel = next.priceScale.priceLabel;
|
|
28636
29833
|
this.scene.showCountdown = next.priceScale.countdown;
|
|
28637
29834
|
this.syncCountdownTimer();
|
|
28638
|
-
this.
|
|
29835
|
+
this.animLiveBar.toggle(next.priceScale.animateLastPrice);
|
|
29836
|
+
this.animZoom.toggle(next.animations.zoom);
|
|
29837
|
+
this.animPan.toggle(next.animations.pan);
|
|
29838
|
+
this.animScroll.toggle(next.animations.pan);
|
|
29839
|
+
this.animAutoscale.toggle(next.animations.autoscale);
|
|
29840
|
+
this.intro = { style: next.animations.intro ? this.introOnStyle : false, duration: this.intro.duration || INTRO_DURATION_DEFAULT_MS };
|
|
28639
29841
|
s.separatorColor = keepInherit(s.separatorColor, next.panes.separatorColor, prevTheme.borderColor);
|
|
28640
29842
|
this.scene.tradeMarkers = {
|
|
28641
29843
|
visible: next.trades.visible,
|
|
@@ -28644,6 +29846,7 @@ var NativeRenderer = class {
|
|
|
28644
29846
|
colors: { long: next.trades.longColor, short: next.trades.shortColor, exit: next.trades.exitColor }
|
|
28645
29847
|
};
|
|
28646
29848
|
this.scene.timezone = next.timeScale.timezone;
|
|
29849
|
+
this.scene.marks = { visible: next.marks.visible, groups: { ...next.marks.groups } };
|
|
28647
29850
|
this.candleUp = next.candles.upColor;
|
|
28648
29851
|
this.candleDown = next.candles.downColor;
|
|
28649
29852
|
s.candle = {
|
|
@@ -28776,6 +29979,7 @@ var NativeRenderer = class {
|
|
|
28776
29979
|
}
|
|
28777
29980
|
this.settingsDialog.setTheme(this.theme);
|
|
28778
29981
|
this.settingsDialog.setHostSections(this.hostSettingsSections);
|
|
29982
|
+
this.settingsDialog.setMarkGroups(this.markGroupsInUse(), (id) => markGroupVisible(this.scene.marks, id, this.scene.markGroups));
|
|
28779
29983
|
this.settingsDialog.setHiddenSettings(this.hiddenSettings);
|
|
28780
29984
|
this.syncThemeControl();
|
|
28781
29985
|
this.settingsDialog.toggle(
|
|
@@ -28866,10 +30070,10 @@ var NativeRenderer = class {
|
|
|
28866
30070
|
this.glideRightOffset(ZOOM_OUT_MARGIN_BARS);
|
|
28867
30071
|
}
|
|
28868
30072
|
/** Ease rightOffset to `target` at constant zoom (see animTick's scroll glide);
|
|
28869
|
-
* instant when
|
|
30073
|
+
* instant when the scroll glide is off. Shared by scroll-to-latest and panBy. */
|
|
28870
30074
|
glideRightOffset(target) {
|
|
28871
30075
|
const vp = this.coords.getViewport();
|
|
28872
|
-
if (!this.
|
|
30076
|
+
if (!this.animScroll.on) {
|
|
28873
30077
|
this.applyViewport({ barSpacing: vp.barSpacing, rightOffset: target });
|
|
28874
30078
|
return;
|
|
28875
30079
|
}
|
|
@@ -28934,20 +30138,21 @@ var NativeRenderer = class {
|
|
|
28934
30138
|
* full size, eased, with a left→right stagger so the chart draws itself; `settle`
|
|
28935
30139
|
* adds an ease-out-back overshoot. Autoscale stays on the real bars so the frame
|
|
28936
30140
|
* never moves. Re-callable, so styles can be compared live from the console.
|
|
30141
|
+
* Style and sweep duration come from the resolved `intro` setting.
|
|
28937
30142
|
*/
|
|
28938
|
-
playIntro(
|
|
30143
|
+
playIntro() {
|
|
28939
30144
|
if (this.introRaf != null) cancelAnimationFrame(this.introRaf);
|
|
28940
30145
|
this.introRaf = null;
|
|
30146
|
+
const { style, duration } = this.intro;
|
|
28941
30147
|
const real = this.bars;
|
|
28942
30148
|
const n = real.length;
|
|
28943
|
-
if (n === 0) return;
|
|
30149
|
+
if (n === 0 || !style) return;
|
|
28944
30150
|
this.computeScales();
|
|
28945
30151
|
for (const pane of this.scene.panes.values()) pane.scale = { ...pane.scaleTarget };
|
|
28946
30152
|
this.modelAlpha = 0;
|
|
28947
|
-
const DURATION = 650;
|
|
28948
30153
|
const start = performance.now();
|
|
28949
30154
|
const step = (now) => {
|
|
28950
|
-
const p = Math.min(1, (now - start) /
|
|
30155
|
+
const p = Math.min(1, (now - start) / duration);
|
|
28951
30156
|
this.scene.bars = p >= 1 ? real : real.map((b, i) => this.revealCandle(b, i, p, n, style));
|
|
28952
30157
|
this.paintData();
|
|
28953
30158
|
if (p < 1) {
|
|
@@ -28961,10 +30166,10 @@ var NativeRenderer = class {
|
|
|
28961
30166
|
}
|
|
28962
30167
|
/** After the candle reveal, fade the indicator models (series/fills/…) from hidden to full. */
|
|
28963
30168
|
fadeInModels() {
|
|
28964
|
-
const
|
|
30169
|
+
const fade = Math.min(INTRO_MODEL_FADE_MS, this.intro.duration || INTRO_MODEL_FADE_MS);
|
|
28965
30170
|
const start = performance.now();
|
|
28966
30171
|
const step = (now) => {
|
|
28967
|
-
this.modelAlpha = Math.min(1, (now - start) /
|
|
30172
|
+
this.modelAlpha = Math.min(1, (now - start) / fade);
|
|
28968
30173
|
this.paintData();
|
|
28969
30174
|
if (this.modelAlpha < 1) {
|
|
28970
30175
|
this.introRaf = requestAnimationFrame(step);
|
|
@@ -29079,7 +30284,8 @@ var NativeRenderer = class {
|
|
|
29079
30284
|
zoomTo: (target, anchorLogical, anchorX) => this.zoomTo(target, anchorLogical, anchorX),
|
|
29080
30285
|
fling: (v) => this.fling(v),
|
|
29081
30286
|
onPointerMove: (x, y) => this.handlePointerMove(x, y),
|
|
29082
|
-
onClick: (x) => {
|
|
30287
|
+
onClick: (x, y) => {
|
|
30288
|
+
if (this.handleMarkClick(x, y)) return;
|
|
29083
30289
|
this.userDrawings?.deselect();
|
|
29084
30290
|
this.handleClick(x);
|
|
29085
30291
|
},
|
|
@@ -29108,7 +30314,7 @@ var NativeRenderer = class {
|
|
|
29108
30314
|
drawingsPointerDown: (x, y, snap, shift2, mod) => this.userDrawings?.pointerDown(x, y, snap, shift2, mod),
|
|
29109
30315
|
drawingsPointerMove: (x, y, snap, shift2, mod) => this.userDrawings?.pointerMove(x, y, snap, shift2, mod),
|
|
29110
30316
|
drawingsPointerUp: (x, y, snap) => this.userDrawings?.pointerUp(x, y, snap),
|
|
29111
|
-
drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? null,
|
|
30317
|
+
drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? (this.chrome.markGlyphAt(x, y) ? "pointer" : null),
|
|
29112
30318
|
drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
|
|
29113
30319
|
drawingsClearTransient: () => this.userDrawings?.clearTransient()
|
|
29114
30320
|
});
|
|
@@ -29124,8 +30330,18 @@ var NativeRenderer = class {
|
|
|
29124
30330
|
this.plot.addEventListener("pointerleave", this.onScrollProximityLeave);
|
|
29125
30331
|
this.labelTooltip = new LabelTooltip(this.plot, {
|
|
29126
30332
|
theme: () => this.chromeTheme(),
|
|
29127
|
-
lookup: (x, y) => this.indicatorSlices.labelTooltipAt(x, y)
|
|
30333
|
+
lookup: (x, y) => this.indicatorSlices.labelTooltipAt(x, y) ?? this.chrome.markTooltipAt(x, y, this.markGroupsInUse())
|
|
29128
30334
|
});
|
|
30335
|
+
this.markPopover = new MarkPopover({
|
|
30336
|
+
plot: this.plot,
|
|
30337
|
+
host: () => this.dialogHost ?? this.plot,
|
|
30338
|
+
theme: () => this.chromeTheme(),
|
|
30339
|
+
onOpenChange: (key) => {
|
|
30340
|
+
this.scene.marksActiveKey = key;
|
|
30341
|
+
this.scheduler?.invalidate(2 /* Chrome */);
|
|
30342
|
+
}
|
|
30343
|
+
});
|
|
30344
|
+
this.chrome.setMarkIconReady(() => this.scheduler?.invalidate(2 /* Chrome */));
|
|
29129
30345
|
this.userDrawings = new UserDrawingController(this.wrapper, this.plot, this.drawingsCanvas, {
|
|
29130
30346
|
projector: () => this.drawingProjector(),
|
|
29131
30347
|
dpr: () => this.coords.dpr,
|
|
@@ -29340,29 +30556,40 @@ var NativeRenderer = class {
|
|
|
29340
30556
|
resize() {
|
|
29341
30557
|
this.syncSize();
|
|
29342
30558
|
}
|
|
29343
|
-
/**
|
|
29344
|
-
|
|
30559
|
+
/** Drive the countdown chip from the host's second pulse (`null` → the renderer's own). */
|
|
30560
|
+
setWallClock(clock) {
|
|
30561
|
+
if (clock === this.hostClock) return;
|
|
30562
|
+
this.hostClock = clock;
|
|
30563
|
+
if (this.countdownUnsub != null) {
|
|
30564
|
+
this.countdownUnsub();
|
|
30565
|
+
this.countdownUnsub = null;
|
|
30566
|
+
}
|
|
30567
|
+
this.syncCountdownTimer();
|
|
30568
|
+
}
|
|
30569
|
+
/** Subscribe to the second pulse while the countdown chip is on (so it ticks); unsubscribe
|
|
30570
|
+
* otherwise. Chrome tier: only the chip's text moves — an idle chart must not recompute
|
|
29345
30571
|
* scales or repaint the geometry/volume/VPVR/SDK layers once a second (that cost
|
|
29346
30572
|
* multiplies by the cell count in a multi-chart workspace). */
|
|
29347
30573
|
syncCountdownTimer() {
|
|
29348
30574
|
if (this.scene.showCountdown) {
|
|
29349
|
-
if (this.
|
|
29350
|
-
this.
|
|
30575
|
+
if (this.countdownUnsub == null) {
|
|
30576
|
+
const clock = this.hostClock ?? (this.ownClock ?? (this.ownClock = new SecondClock()));
|
|
30577
|
+
this.countdownUnsub = clock.onTick(() => {
|
|
29351
30578
|
if (this.scene.showCountdown && this.scene.bars.length > 0) this.scheduler?.invalidate(2 /* Chrome */);
|
|
29352
|
-
}
|
|
30579
|
+
});
|
|
29353
30580
|
}
|
|
29354
|
-
} else if (this.
|
|
29355
|
-
|
|
29356
|
-
this.
|
|
30581
|
+
} else if (this.countdownUnsub != null) {
|
|
30582
|
+
this.countdownUnsub();
|
|
30583
|
+
this.countdownUnsub = null;
|
|
29357
30584
|
}
|
|
29358
30585
|
}
|
|
29359
30586
|
destroy() {
|
|
29360
30587
|
if (this.introRaf != null) cancelAnimationFrame(this.introRaf);
|
|
29361
30588
|
this.loadingEl?.remove();
|
|
29362
30589
|
this.loadingEl = null;
|
|
29363
|
-
if (this.
|
|
29364
|
-
|
|
29365
|
-
this.
|
|
30590
|
+
if (this.countdownUnsub != null) {
|
|
30591
|
+
this.countdownUnsub();
|
|
30592
|
+
this.countdownUnsub = null;
|
|
29366
30593
|
}
|
|
29367
30594
|
this.scheduler?.destroy();
|
|
29368
30595
|
this.animator?.stop();
|
|
@@ -29387,6 +30614,11 @@ var NativeRenderer = class {
|
|
|
29387
30614
|
this.plot?.removeEventListener("pointerleave", this.onScrollProximityLeave);
|
|
29388
30615
|
this.labelTooltip?.destroy();
|
|
29389
30616
|
this.labelTooltip = null;
|
|
30617
|
+
this.markPopover?.destroy();
|
|
30618
|
+
this.markPopover = null;
|
|
30619
|
+
this.chrome.setMarkIconReady(null);
|
|
30620
|
+
if (this.markPulseRaf !== null) cancelAnimationFrame(this.markPulseRaf);
|
|
30621
|
+
this.markPulseRaf = null;
|
|
29390
30622
|
this.scrollButton?.remove();
|
|
29391
30623
|
this.scrollButton = null;
|
|
29392
30624
|
for (const l of this.extLayers) l.instance.destroy?.();
|
|
@@ -29445,8 +30677,8 @@ var NativeRenderer = class {
|
|
|
29445
30677
|
}
|
|
29446
30678
|
if (!this.introPlayed && this.bars.length > 0) {
|
|
29447
30679
|
this.introPlayed = true;
|
|
29448
|
-
if (this.
|
|
29449
|
-
this.playIntro(
|
|
30680
|
+
if (this.intro.style) {
|
|
30681
|
+
this.playIntro();
|
|
29450
30682
|
return;
|
|
29451
30683
|
}
|
|
29452
30684
|
}
|
|
@@ -29457,7 +30689,7 @@ var NativeRenderer = class {
|
|
|
29457
30689
|
const last = this.bars[n - 1];
|
|
29458
30690
|
if (last && bar.time === last.time) {
|
|
29459
30691
|
this.bars[n - 1] = bar;
|
|
29460
|
-
if (this.
|
|
30692
|
+
if (!this.animLiveBar.on || this.liveEaseTime !== bar.time) {
|
|
29461
30693
|
this.syncLiveEase(bar);
|
|
29462
30694
|
} else {
|
|
29463
30695
|
this.animator.start();
|
|
@@ -29473,11 +30705,11 @@ var NativeRenderer = class {
|
|
|
29473
30705
|
this.scene.bars = this.bars;
|
|
29474
30706
|
this.scheduler.invalidate(4 /* Full */);
|
|
29475
30707
|
}
|
|
29476
|
-
/** Set the
|
|
29477
|
-
*
|
|
29478
|
-
|
|
29479
|
-
this.
|
|
29480
|
-
if (
|
|
30708
|
+
/** Set the reveal (style + duration). A non-off style is also remembered as what the
|
|
30709
|
+
* config's on/off toggle (`animations.intro`) switches back on to. */
|
|
30710
|
+
setIntro(next) {
|
|
30711
|
+
this.intro = next;
|
|
30712
|
+
if (next.style) this.introOnStyle = next.style;
|
|
29481
30713
|
}
|
|
29482
30714
|
/** Snap the eased forming-bar state to `bar` — no glide (a fresh bar or the first tick of one). */
|
|
29483
30715
|
syncLiveEase(bar) {
|
|
@@ -29491,7 +30723,7 @@ var NativeRenderer = class {
|
|
|
29491
30723
|
const target = this.bars[this.bars.length - 1];
|
|
29492
30724
|
if (!target || this.liveEaseTime !== target.time) return false;
|
|
29493
30725
|
const eps = Math.max(1e-9, Math.abs(target.close) * 1e-6);
|
|
29494
|
-
const tau = this.
|
|
30726
|
+
const tau = this.animLiveBar.tau;
|
|
29495
30727
|
const nh = easeToward(this.liveEaseHigh, target.high, dtMs, tau);
|
|
29496
30728
|
const nl = easeToward(this.liveEaseLow, target.low, dtMs, tau);
|
|
29497
30729
|
const nc = easeToward(this.liveEaseClose, target.close, dtMs, tau);
|
|
@@ -29631,10 +30863,12 @@ var NativeRenderer = class {
|
|
|
29631
30863
|
this.refreshAnchorOffset(model);
|
|
29632
30864
|
if (model.native && this.extLayers.some((l) => l.def.id === model.native.type)) this.scene.assignIndicatorZTop(model.id);
|
|
29633
30865
|
else this.scene.assignIndicatorZ(model.id);
|
|
29634
|
-
|
|
29635
|
-
|
|
29636
|
-
|
|
29637
|
-
|
|
30866
|
+
if (model.legend !== false) {
|
|
30867
|
+
this.inputsUI.upsert(model.id, model.shorttitle ?? model.title, model.inputs, model.inputValues, model.paneId, {
|
|
30868
|
+
native: !!model.native,
|
|
30869
|
+
...model.props ? { props: model.props, propValues: model.propValues ?? {} } : {}
|
|
30870
|
+
});
|
|
30871
|
+
}
|
|
29638
30872
|
if (model.native?.type === "volume") {
|
|
29639
30873
|
this.volumeActive = true;
|
|
29640
30874
|
this.volumeHidden = false;
|
|
@@ -29781,7 +31015,7 @@ var NativeRenderer = class {
|
|
|
29781
31015
|
this.settingsDialog?.setHiddenSettings(this.hiddenSettings);
|
|
29782
31016
|
}
|
|
29783
31017
|
listSettingsIds() {
|
|
29784
|
-
return settingsIdCatalog(this.hostSettingsSections);
|
|
31018
|
+
return settingsIdCatalog(this.hostSettingsSections, this.markGroupsInUse());
|
|
29785
31019
|
}
|
|
29786
31020
|
onChartTypeSettingsChange(cb) {
|
|
29787
31021
|
this.chartTypeSettingsCbs.add(cb);
|
|
@@ -29807,6 +31041,22 @@ var NativeRenderer = class {
|
|
|
29807
31041
|
this.axisLongPressCbs.add(cb);
|
|
29808
31042
|
return () => this.axisLongPressCbs.delete(cb);
|
|
29809
31043
|
}
|
|
31044
|
+
// ── timeline marks (the `chart.marks` model; see the port) ──
|
|
31045
|
+
setTimelineMarks(marks, groups) {
|
|
31046
|
+
this.scene.timelineMarks = marks;
|
|
31047
|
+
this.scene.markGroups = groups;
|
|
31048
|
+
this.scene.marksExpandedStack = null;
|
|
31049
|
+
this.markPopover?.close();
|
|
31050
|
+
this.scheduler?.invalidate(2 /* Chrome */);
|
|
31051
|
+
}
|
|
31052
|
+
onMarkClick(cb) {
|
|
31053
|
+
this.markClickCbs.add(cb);
|
|
31054
|
+
return () => this.markClickCbs.delete(cb);
|
|
31055
|
+
}
|
|
31056
|
+
/** Every group the lane knows: the defined ones, then those marks name without a definition. */
|
|
31057
|
+
markGroupsInUse() {
|
|
31058
|
+
return effectiveMarkGroups(this.scene.timelineMarks, this.scene.markGroups);
|
|
31059
|
+
}
|
|
29810
31060
|
onViewportChange(cb) {
|
|
29811
31061
|
this.viewportCbs.add(cb);
|
|
29812
31062
|
return () => this.viewportCbs.delete(cb);
|
|
@@ -29860,7 +31110,7 @@ var NativeRenderer = class {
|
|
|
29860
31110
|
this.zoomAnchorX = anchorX;
|
|
29861
31111
|
this.panVelocity = 0;
|
|
29862
31112
|
this.scrollTargetRO = null;
|
|
29863
|
-
if (!this.animZoom) {
|
|
31113
|
+
if (!this.animZoom.on) {
|
|
29864
31114
|
const v = this.clampViewport(barSpacing, this.anchoredRightOffset(barSpacing));
|
|
29865
31115
|
this.coords.setViewport(v);
|
|
29866
31116
|
this.targetBarSpacing = v.barSpacing;
|
|
@@ -29873,7 +31123,7 @@ var NativeRenderer = class {
|
|
|
29873
31123
|
}
|
|
29874
31124
|
/** Inertial pan: continue with a rightOffset velocity (logical units / ms) that decays. */
|
|
29875
31125
|
fling(velocity) {
|
|
29876
|
-
if (!this.animPan) return;
|
|
31126
|
+
if (!this.animPan.on) return;
|
|
29877
31127
|
this.scrollTargetRO = null;
|
|
29878
31128
|
this.panVelocity = velocity;
|
|
29879
31129
|
this.animator.start();
|
|
@@ -29913,7 +31163,7 @@ var NativeRenderer = class {
|
|
|
29913
31163
|
let active = false;
|
|
29914
31164
|
const tbs = this.targetBarSpacing;
|
|
29915
31165
|
if (Math.abs(barSpacing - tbs) > tbs * 1e-3) {
|
|
29916
|
-
barSpacing = clampBarSpacing(easeToward(barSpacing, tbs, dtMs,
|
|
31166
|
+
barSpacing = clampBarSpacing(easeToward(barSpacing, tbs, dtMs, this.animZoom.tau));
|
|
29917
31167
|
rightOffset = this.anchoredRightOffset(barSpacing);
|
|
29918
31168
|
active = true;
|
|
29919
31169
|
} else if (barSpacing !== tbs) {
|
|
@@ -29923,13 +31173,14 @@ var NativeRenderer = class {
|
|
|
29923
31173
|
const stopVel = FLING_STOP_PX / Math.max(1e-6, barSpacing * this.coords.spacingScale);
|
|
29924
31174
|
if (Math.abs(this.panVelocity) > stopVel) {
|
|
29925
31175
|
rightOffset += this.panVelocity * dtMs;
|
|
29926
|
-
|
|
31176
|
+
const tau = this.animPan.tau;
|
|
31177
|
+
this.panVelocity = tau > 0 ? this.panVelocity * Math.exp(-dtMs / tau) : 0;
|
|
29927
31178
|
if (Math.abs(this.panVelocity) <= stopVel) this.panVelocity = 0;
|
|
29928
31179
|
else active = true;
|
|
29929
31180
|
}
|
|
29930
31181
|
if (this.scrollTargetRO != null) {
|
|
29931
31182
|
const target = this.scrollTargetRO;
|
|
29932
|
-
const next = easeToward(rightOffset, target, dtMs,
|
|
31183
|
+
const next = easeToward(rightOffset, target, dtMs, this.animScroll.tau);
|
|
29933
31184
|
if (Math.abs(next - target) < 1e-3) {
|
|
29934
31185
|
rightOffset = target;
|
|
29935
31186
|
this.scrollTargetRO = null;
|
|
@@ -29957,6 +31208,7 @@ var NativeRenderer = class {
|
|
|
29957
31208
|
* through Math.log), not a non-linear jump. */
|
|
29958
31209
|
easeScales(dtMs) {
|
|
29959
31210
|
let moving = false;
|
|
31211
|
+
const tau = this.animAutoscale.tau;
|
|
29960
31212
|
for (const pane of this.scene.panes.values()) {
|
|
29961
31213
|
const t = pane.scaleTarget;
|
|
29962
31214
|
const s = pane.scale;
|
|
@@ -29964,8 +31216,8 @@ var NativeRenderer = class {
|
|
|
29964
31216
|
const lt0 = Math.log(t.min);
|
|
29965
31217
|
const lt1 = Math.log(t.max);
|
|
29966
31218
|
const lspan = Math.max(1e-9, Math.abs(lt1 - lt0));
|
|
29967
|
-
const n0 = easeToward(Math.log(s.min), lt0, dtMs,
|
|
29968
|
-
const n1 = easeToward(Math.log(s.max), lt1, dtMs,
|
|
31219
|
+
const n0 = easeToward(Math.log(s.min), lt0, dtMs, tau);
|
|
31220
|
+
const n1 = easeToward(Math.log(s.max), lt1, dtMs, tau);
|
|
29969
31221
|
if (Math.abs(n0 - lt0) <= lspan * 1e-3 && Math.abs(n1 - lt1) <= lspan * 1e-3) {
|
|
29970
31222
|
pane.scale = { min: t.min, max: t.max, log: true };
|
|
29971
31223
|
} else {
|
|
@@ -29975,8 +31227,8 @@ var NativeRenderer = class {
|
|
|
29975
31227
|
continue;
|
|
29976
31228
|
}
|
|
29977
31229
|
const span = Math.max(1e-9, Math.abs(t.max - t.min));
|
|
29978
|
-
let nmin = easeToward(s.min, t.min, dtMs,
|
|
29979
|
-
let nmax = easeToward(s.max, t.max, dtMs,
|
|
31230
|
+
let nmin = easeToward(s.min, t.min, dtMs, tau);
|
|
31231
|
+
let nmax = easeToward(s.max, t.max, dtMs, tau);
|
|
29980
31232
|
if (Math.abs(nmin - t.min) <= span * 1e-3 && Math.abs(nmax - t.max) <= span * 1e-3) {
|
|
29981
31233
|
nmin = t.min;
|
|
29982
31234
|
nmax = t.max;
|
|
@@ -29989,8 +31241,8 @@ var NativeRenderer = class {
|
|
|
29989
31241
|
const t = sl.scaleTarget;
|
|
29990
31242
|
const s = sl.scale;
|
|
29991
31243
|
const span = Math.max(1e-9, Math.abs(t.max - t.min));
|
|
29992
|
-
let nmin = easeToward(s.min, t.min, dtMs,
|
|
29993
|
-
let nmax = easeToward(s.max, t.max, dtMs,
|
|
31244
|
+
let nmin = easeToward(s.min, t.min, dtMs, tau);
|
|
31245
|
+
let nmax = easeToward(s.max, t.max, dtMs, tau);
|
|
29994
31246
|
if (Math.abs(nmin - t.min) <= span * 1e-3 && Math.abs(nmax - t.max) <= span * 1e-3) {
|
|
29995
31247
|
nmin = t.min;
|
|
29996
31248
|
nmax = t.max;
|
|
@@ -30011,6 +31263,8 @@ var NativeRenderer = class {
|
|
|
30011
31263
|
this.scene.crosshair = null;
|
|
30012
31264
|
this.hoverSeparatorY = null;
|
|
30013
31265
|
this.lastPointer = null;
|
|
31266
|
+
if (this.marksExpandedBy === "hover") this.setMarksExpanded(null);
|
|
31267
|
+
this.setMarkHover(null);
|
|
30014
31268
|
this.scheduler.invalidate(1 /* Cursor */);
|
|
30015
31269
|
this.hoverLogical = null;
|
|
30016
31270
|
const empty = { time: null, price: null, paneKind: null, values: /* @__PURE__ */ new Map(), ohlc: null };
|
|
@@ -30022,6 +31276,8 @@ var NativeRenderer = class {
|
|
|
30022
31276
|
this.lastPointer = inData ? { x, y } : null;
|
|
30023
31277
|
this.hoverSeparatorY = x >= 0 && y >= 0 && y <= this.coords.height ? this.separatorHoverY(y) : null;
|
|
30024
31278
|
this.scheduler.invalidate(1 /* Cursor */);
|
|
31279
|
+
this.setMarksExpanded(inData ? this.chrome.markStackAt(x, y) : null);
|
|
31280
|
+
this.setMarkHover(inData ? this.chrome.markGlyphAt(x, y)?.cluster.key ?? null : null);
|
|
30025
31281
|
const logical = Math.round(this.coords.xToLogical(x));
|
|
30026
31282
|
const onBar = logical >= 0 && logical < this.coords.barCount;
|
|
30027
31283
|
const time = onBar ? this.coords.logicalToTime(logical) : null;
|
|
@@ -30051,6 +31307,76 @@ var NativeRenderer = class {
|
|
|
30051
31307
|
const onBar = logical >= 0 && logical < this.coords.barCount;
|
|
30052
31308
|
for (const cb of this.clickCbs) cb({ time: onBar ? this.coords.logicalToTime(logical) : null, price: null });
|
|
30053
31309
|
}
|
|
31310
|
+
/**
|
|
31311
|
+
* Fan out (or collapse, with null) a multi-group mark stack; repaints the chrome tier when
|
|
31312
|
+
* it changes. A stack whose glyph holds the open popup stays fanned — the pointer leaving
|
|
31313
|
+
* the plot for the popup must not bury the glyph under the deck (which would close it).
|
|
31314
|
+
*/
|
|
31315
|
+
setMarksExpanded(stack, by = "hover") {
|
|
31316
|
+
if (this.scene.marksExpandedStack === stack) return;
|
|
31317
|
+
if (stack === null && this.markPopover?.key) {
|
|
31318
|
+
const open2 = this.chrome.markGlyphByKey(this.markPopover.key);
|
|
31319
|
+
if (open2 && open2.stack === this.scene.marksExpandedStack) return;
|
|
31320
|
+
}
|
|
31321
|
+
this.scene.marksExpandedStack = stack;
|
|
31322
|
+
this.marksExpandedBy = by;
|
|
31323
|
+
this.scheduler?.invalidate(2 /* Chrome */);
|
|
31324
|
+
}
|
|
31325
|
+
/**
|
|
31326
|
+
* A click on the mark lane: a collapsed deck fans out (the touch path — a mouse already
|
|
31327
|
+
* fanned it by hovering), a glyph reports its cluster (`onMarkClick`) and opens the popup
|
|
31328
|
+
* when any of its marks carries content. True when the click landed on the lane.
|
|
31329
|
+
*/
|
|
31330
|
+
handleMarkClick(x, y) {
|
|
31331
|
+
const glyph = this.chrome.markGlyphAt(x, y);
|
|
31332
|
+
if (!glyph) {
|
|
31333
|
+
if (this.scene.marksExpandedStack !== null && this.chrome.markStackAt(x, y) === null) this.setMarksExpanded(null);
|
|
31334
|
+
return false;
|
|
31335
|
+
}
|
|
31336
|
+
if (glyph.decked) {
|
|
31337
|
+
this.setMarksExpanded(glyph.stack, "tap");
|
|
31338
|
+
return true;
|
|
31339
|
+
}
|
|
31340
|
+
const marks = glyph.cluster.marks;
|
|
31341
|
+
const first = marks[0];
|
|
31342
|
+
const event = { id: first.id, ids: marks.map((m) => m.id), time: first.time, ...glyph.cluster.group !== void 0 ? { group: glyph.cluster.group } : {} };
|
|
31343
|
+
for (const cb of this.markClickCbs) cb(event);
|
|
31344
|
+
if (marks.some((m) => m.content !== void 0)) {
|
|
31345
|
+
this.markPopover?.open(glyph.cluster, { x: glyph.x, y: glyph.y, size: glyph.size });
|
|
31346
|
+
} else {
|
|
31347
|
+
this.scene.marksFlash = { key: glyph.cluster.key, until: frameNow() + MARK_FLASH_MS };
|
|
31348
|
+
this.syncMarkPulse();
|
|
31349
|
+
}
|
|
31350
|
+
return true;
|
|
31351
|
+
}
|
|
31352
|
+
/** After a chrome frame: keep the open mark popup on its glyph, or close it once the glyph is gone. */
|
|
31353
|
+
trackMarkPopover() {
|
|
31354
|
+
const key = this.markPopover?.key;
|
|
31355
|
+
if (!key) return;
|
|
31356
|
+
const g = this.chrome.markGlyphByKey(key);
|
|
31357
|
+
this.markPopover.track(g && !(g.decked && g.depth !== 0) ? { x: g.x, y: g.y, size: g.size } : null);
|
|
31358
|
+
}
|
|
31359
|
+
/** The lane glyph under the pointer — it swells once as the pointer lands (a rAF-driven chrome repaint for the pulse's duration). */
|
|
31360
|
+
setMarkHover(key) {
|
|
31361
|
+
if (this.scene.marksHoverKey === key) return;
|
|
31362
|
+
this.scene.marksHoverKey = key;
|
|
31363
|
+
this.scene.marksHoverSince = frameNow();
|
|
31364
|
+
this.scheduler?.invalidate(2 /* Chrome */);
|
|
31365
|
+
this.syncMarkPulse();
|
|
31366
|
+
}
|
|
31367
|
+
/** Run a chrome-tier repaint loop while a glyph's hover pulse or click flash plays; it stops itself once both are over. */
|
|
31368
|
+
syncMarkPulse() {
|
|
31369
|
+
if (this.markPulseRaf !== null || typeof requestAnimationFrame !== "function") return;
|
|
31370
|
+
const tick = () => {
|
|
31371
|
+
this.markPulseRaf = null;
|
|
31372
|
+
const now = frameNow();
|
|
31373
|
+
if (this.scene.marksFlash && this.scene.marksFlash.until <= now) this.scene.marksFlash = null;
|
|
31374
|
+
this.scheduler?.invalidate(2 /* Chrome */);
|
|
31375
|
+
const pulsing = this.scene.marksHoverKey !== null && now - this.scene.marksHoverSince < MARK_PULSE_MS;
|
|
31376
|
+
if (pulsing || this.scene.marksFlash !== null) this.markPulseRaf = requestAnimationFrame(tick);
|
|
31377
|
+
};
|
|
31378
|
+
this.markPulseRaf = requestAnimationFrame(tick);
|
|
31379
|
+
}
|
|
30054
31380
|
paneAtY(y) {
|
|
30055
31381
|
return this.paneNodeAtY(y);
|
|
30056
31382
|
}
|
|
@@ -30442,6 +31768,7 @@ var NativeRenderer = class {
|
|
|
30442
31768
|
} else if (repaintsChrome(level) && this.paintedData) {
|
|
30443
31769
|
this.chrome.prepare(this.scene, this.coords, this.theme);
|
|
30444
31770
|
this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
|
|
31771
|
+
this.trackMarkPopover();
|
|
30445
31772
|
}
|
|
30446
31773
|
this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
|
|
30447
31774
|
if (!repaintsData(level) && this.paintedData) this.repaintCursorLayers();
|
|
@@ -30457,7 +31784,7 @@ var NativeRenderer = class {
|
|
|
30457
31784
|
const lp = this.layerPane(l.def.id) ?? pane;
|
|
30458
31785
|
if (lp.collapsed) continue;
|
|
30459
31786
|
l.instance.render(this.extLayerArgs(l.def.id, lp.scale, lp.bounds, nowMs));
|
|
30460
|
-
if (this.animZoom && l.instance.animating?.()) this.animator.start();
|
|
31787
|
+
if (this.animZoom.on && l.instance.animating?.()) this.animator.start();
|
|
30461
31788
|
}
|
|
30462
31789
|
}
|
|
30463
31790
|
/** Blank one SDK layer canvas (a collapsed host pane suppresses the layer's painting). */
|
|
@@ -30522,7 +31849,7 @@ var NativeRenderer = class {
|
|
|
30522
31849
|
const args = this.extLayerArgs(l.def.id, lp.scale, lp.bounds, nowMs);
|
|
30523
31850
|
l.instance.render(args);
|
|
30524
31851
|
if (lp === pane) folded = foldBaseModulation(folded, l.instance.modulateBase?.(args) ?? null);
|
|
30525
|
-
if (this.animZoom && l.instance.animating?.()) this.animator.start();
|
|
31852
|
+
if (this.animZoom.on && l.instance.animating?.()) this.animator.start();
|
|
30526
31853
|
}
|
|
30527
31854
|
if (folded) {
|
|
30528
31855
|
if (folded.candleBodyScale != null) this.backend.candleBodyScale = clamp012(folded.candleBodyScale) || 0.01;
|
|
@@ -30541,6 +31868,7 @@ var NativeRenderer = class {
|
|
|
30541
31868
|
this.backdropRenderer.render(this.scene, this.coords, this.theme, gridAlpha);
|
|
30542
31869
|
this.backend.render(this.scene, this.coords, this.theme);
|
|
30543
31870
|
this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
|
|
31871
|
+
this.trackMarkPopover();
|
|
30544
31872
|
this.userDrawings?.render();
|
|
30545
31873
|
if (easeLive && liveActual) this.bars[li] = liveActual;
|
|
30546
31874
|
this.paintedData = true;
|
|
@@ -31053,7 +32381,7 @@ var NativeRenderer = class {
|
|
|
31053
32381
|
const map2 = /* @__PURE__ */ new Map();
|
|
31054
32382
|
for (const pane of this.scene.panes.values()) {
|
|
31055
32383
|
if (!pane.collapsed) continue;
|
|
31056
|
-
const models = this.scene.orderedIndicatorsForPane(pane.id);
|
|
32384
|
+
const models = this.scene.orderedIndicatorsForPane(pane.id).filter((m) => m.legend !== false);
|
|
31057
32385
|
const merged = new Set(this.scene.ownScaleIndicatorsForPane(pane.id).map((m) => m.id));
|
|
31058
32386
|
const master = models.find((m) => !merged.has(m.id)) ?? models[0];
|
|
31059
32387
|
map2.set(pane.id, master?.id ?? null);
|
|
@@ -31331,6 +32659,8 @@ function volumeLayerData(inputs) {
|
|
|
31331
32659
|
}
|
|
31332
32660
|
var VolumeIndicator = class {
|
|
31333
32661
|
constructor() {
|
|
32662
|
+
/** Null until start() — pre-start setInputs/resume must record without pushing. */
|
|
32663
|
+
this.ctx = null;
|
|
31334
32664
|
this.inputs = {};
|
|
31335
32665
|
}
|
|
31336
32666
|
start(ctx, inputs) {
|
|
@@ -31347,13 +32677,13 @@ var VolumeIndicator = class {
|
|
|
31347
32677
|
}
|
|
31348
32678
|
setInputs(inputs) {
|
|
31349
32679
|
this.inputs = inputs;
|
|
31350
|
-
this.ctx
|
|
32680
|
+
this.ctx?.pushData(volumeLayerData(inputs));
|
|
31351
32681
|
}
|
|
31352
32682
|
/** Hiding is a renderer-layer flag (set via `setIndicatorVisible`); no resources to free. */
|
|
31353
32683
|
suspend() {
|
|
31354
32684
|
}
|
|
31355
32685
|
resume() {
|
|
31356
|
-
this.ctx
|
|
32686
|
+
this.ctx?.pushData(volumeLayerData(this.inputs));
|
|
31357
32687
|
}
|
|
31358
32688
|
stop() {
|
|
31359
32689
|
}
|
|
@@ -31399,6 +32729,8 @@ function vpvrLayerData(inputs) {
|
|
|
31399
32729
|
}
|
|
31400
32730
|
var VpvrIndicator = class {
|
|
31401
32731
|
constructor() {
|
|
32732
|
+
/** Null until start() — pre-start setInputs/resume must record without pushing. */
|
|
32733
|
+
this.ctx = null;
|
|
31402
32734
|
this.inputs = {};
|
|
31403
32735
|
}
|
|
31404
32736
|
start(ctx, inputs) {
|
|
@@ -31415,13 +32747,13 @@ var VpvrIndicator = class {
|
|
|
31415
32747
|
}
|
|
31416
32748
|
setInputs(inputs) {
|
|
31417
32749
|
this.inputs = inputs;
|
|
31418
|
-
this.ctx
|
|
32750
|
+
this.ctx?.pushData(vpvrLayerData(inputs));
|
|
31419
32751
|
}
|
|
31420
32752
|
/** Hiding is a renderer-layer flag (set via `setIndicatorVisible`); no resources to free. */
|
|
31421
32753
|
suspend() {
|
|
31422
32754
|
}
|
|
31423
32755
|
resume() {
|
|
31424
|
-
this.ctx
|
|
32756
|
+
this.ctx?.pushData(vpvrLayerData(this.inputs));
|
|
31425
32757
|
}
|
|
31426
32758
|
stop() {
|
|
31427
32759
|
}
|
|
@@ -33873,6 +35205,7 @@ var Vela = class {
|
|
|
33873
35205
|
if (Object.keys(defaults2).length > 0) this.rendererControl.set(defaults2);
|
|
33874
35206
|
this.panesControl = new PanesControl(this.orchestrator);
|
|
33875
35207
|
this.drawingsControl = new DrawingsControl(this.orchestrator.drawings);
|
|
35208
|
+
this.marksControl = new MarksControl(this.orchestrator.marks);
|
|
33876
35209
|
}
|
|
33877
35210
|
/**
|
|
33878
35211
|
* Register a scripting engine so `addIndicator({ language })` can run that
|
|
@@ -34091,6 +35424,17 @@ var Vela = class {
|
|
|
34091
35424
|
get drawings() {
|
|
34092
35425
|
return this.drawingsControl;
|
|
34093
35426
|
}
|
|
35427
|
+
/**
|
|
35428
|
+
* The chart's timeline-marks control surface: host events pinned to a bar and shown
|
|
35429
|
+
* as glyphs on a lane above the time axis, each opening a popup on click —
|
|
35430
|
+
* `chart.marks.add({ id, time, glyph, title, content })`, `chart.marks.set(list)`,
|
|
35431
|
+
* `chart.marks.defineGroup({ id, label })`. Marks are data, not user state: re-supply
|
|
35432
|
+
* them on `market:changed`. On a renderer without the `timelineMarks` capability the
|
|
35433
|
+
* model still fills but nothing paints (`chart.marks.supported`).
|
|
35434
|
+
*/
|
|
35435
|
+
get marks() {
|
|
35436
|
+
return this.marksControl;
|
|
35437
|
+
}
|
|
34094
35438
|
on(event, handler) {
|
|
34095
35439
|
return this.orchestrator.events.on(event, handler);
|
|
34096
35440
|
}
|
|
@@ -34303,6 +35647,7 @@ exports.INFO = INFO;
|
|
|
34303
35647
|
exports.INVALID = INVALID;
|
|
34304
35648
|
exports.LIGHT_THEME = LIGHT_THEME;
|
|
34305
35649
|
exports.MARKER = MARKER;
|
|
35650
|
+
exports.MarksControl = MarksControl;
|
|
34306
35651
|
exports.MultiProviderFeed = MultiProviderFeed;
|
|
34307
35652
|
exports.NEUTRAL = NEUTRAL;
|
|
34308
35653
|
exports.NativeRenderer = NativeRenderer;
|
|
@@ -34314,6 +35659,7 @@ exports.SESSION_POST = SESSION_POST;
|
|
|
34314
35659
|
exports.SESSION_PRE = SESSION_PRE;
|
|
34315
35660
|
exports.SLATE = SLATE;
|
|
34316
35661
|
exports.SLATE_DEEP = SLATE_DEEP;
|
|
35662
|
+
exports.SecondClock = SecondClock;
|
|
34317
35663
|
exports.TRADE_EXIT = TRADE_EXIT;
|
|
34318
35664
|
exports.TRADE_LONG = TRADE_LONG;
|
|
34319
35665
|
exports.TRADE_SHORT = TRADE_SHORT;
|