aboutsummaryrefslogtreecommitdiff
path: root/src/zen/frontend/html/stats.js
diff options
context:
space:
mode:
authorStefan Boberg <[email protected]>2026-04-23 18:16:57 +0200
committerStefan Boberg <[email protected]>2026-04-23 18:16:57 +0200
commit0232b991cd7d8e3a2114ea30e4591dd3e7b65c36 (patch)
tree94730e7594fd09ae1fa820391ce311f6daf13905 /src/zen/frontend/html/stats.js
parentFix forward declaration order for s_GotSigWinch and SigWinchHandler (diff)
parenttrace: declare Region event name fields as AnsiString (#1012) (diff)
downloadarchived-zen-sb/zen-help.tar.xz
archived-zen-sb/zen-help.zip
Merge branch 'main' into sb/zen-helpsb/zen-help
- Combine HelpCommand (this branch) with HistoryCommand (main) in zen CLI dispatcher - Keep filter-aware TuiPickOne rewrite; adopt main's ASCII arrow glyphs in doc comment
Diffstat (limited to 'src/zen/frontend/html/stats.js')
-rw-r--r--src/zen/frontend/html/stats.js95
1 files changed, 95 insertions, 0 deletions
diff --git a/src/zen/frontend/html/stats.js b/src/zen/frontend/html/stats.js
new file mode 100644
index 000000000..741ad7ef9
--- /dev/null
+++ b/src/zen/frontend/html/stats.js
@@ -0,0 +1,95 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+// Sortable stats table view.
+
+const US_PER_MS = 1000;
+
+function escapeHtml(s) {
+ return String(s).replace(/[&<>"']/g, (c) => ({
+ "&": "&amp;",
+ "<": "&lt;",
+ ">": "&gt;",
+ "\"": "&quot;",
+ "'": "&#39;",
+ }[c]));
+}
+
+export class StatsView {
+ constructor(tbody, headerRow, model, onSelect) {
+ this.tbody = tbody;
+ this.headerRow = headerRow;
+ this.stats = model.scopeStats.slice();
+ this.onSelect = onSelect;
+ this.sortKey = "count";
+ this.sortAsc = false;
+ this.selectedName = null;
+
+ for (const th of headerRow.querySelectorAll("th[data-sort]")) {
+ th.addEventListener("click", () => this.handleSort(th.dataset.sort));
+ }
+ this.render();
+ }
+
+ handleSort(key) {
+ if (this.sortKey === key) {
+ this.sortAsc = !this.sortAsc;
+ } else {
+ this.sortKey = key;
+ this.sortAsc = key === "name";
+ }
+ this.render();
+ }
+
+ selectByName(name) {
+ this.selectedName = name;
+ for (const tr of this.tbody.querySelectorAll("tr")) {
+ tr.classList.toggle("selected", tr.dataset.name === name);
+ if (tr.dataset.name === name) {
+ tr.scrollIntoView({ block: "nearest" });
+ }
+ }
+ }
+
+ render() {
+ const key = this.sortKey;
+ const asc = this.sortAsc;
+ this.stats.sort((a, b) => {
+ const av = a[key];
+ const bv = b[key];
+ if (typeof av === "string") {
+ return asc ? av.localeCompare(bv) : bv.localeCompare(av);
+ }
+ return asc ? av - bv : bv - av;
+ });
+
+ for (const th of this.headerRow.querySelectorAll("th[data-sort]")) {
+ th.classList.toggle("sorted", th.dataset.sort === key);
+ th.classList.toggle("asc", th.dataset.sort === key && asc);
+ }
+
+ const rows = [];
+ for (const stat of this.stats) {
+ const selected = stat.name === this.selectedName ? " class=\"selected\"" : "";
+ rows.push(
+ `<tr data-name="${escapeHtml(stat.name)}"${selected}>` +
+ `<td>${escapeHtml(stat.name)}</td>` +
+ `<td class="num">${stat.count.toLocaleString()}</td>` +
+ `<td class="num">${(stat.min_us / US_PER_MS).toFixed(3)}</td>` +
+ `<td class="num">${(stat.mean_us / US_PER_MS).toFixed(3)}</td>` +
+ `<td class="num">${(stat.max_us / US_PER_MS).toFixed(3)}</td>` +
+ `<td class="num">${(stat.stdev_us / US_PER_MS).toFixed(3)}</td>` +
+ `</tr>`,
+ );
+ }
+ this.tbody.innerHTML = rows.join("");
+
+ for (const tr of this.tbody.querySelectorAll("tr")) {
+ tr.addEventListener("click", () => {
+ const name = tr.dataset.name;
+ this.selectByName(name);
+ if (this.onSelect) {
+ this.onSelect(name);
+ }
+ });
+ }
+ }
+}