Files
RustProxy/src/admin/web/app/chart-scales.js
T
2026-10-25 09:58:39 +01:00

141 lines
7.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
window.rustProxyChartScale = (() => {
const floors = { rate: 100, count: 10, ms: 100, bytes: 64 * 1024 * 1024, "bytes/s": 64 * 1024 };
const ceilings = new Map();
function peak(key, unit, series) {
if (unit === "percent") return 100;
const floor = floors[unit] ?? 1;
let maximum = floor;
for (const values of series) for (const value of values) {
if (typeof value === "number" && Number.isFinite(value) && value >= 0)
maximum = Math.max(maximum, value);
}
const magnitude = 10 ** Math.floor(Math.log10(maximum));
const tick = [1, 2, 5, 10].find(n => n * magnitude >= maximum) ?? 10;
const result = Math.max(ceilings.get(key) ?? 0, tick * magnitude);
ceilings.set(key, result);
return result;
}
function liveRate(values) {
const samples = (Array.isArray(values) ? values : []).map(value =>
typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0);
const maximum = Math.max(0, ...samples);
const recent = samples.slice(-24).sort((a, b) => a - b);
const typical = recent.length ? recent[Math.floor((recent.length - 1) * 0.85)] : 0;
const scale = Math.max(1, typical * 1.25);
const points = samples.map((value, index) => {
const x = index * 320 / Math.max(1, samples.length - 1);
const y = 50 - Math.min(value / scale, 1) * 44;
return `${x},${y}`;
}).join(" ");
return { points, peak: maximum, scale, clipped: maximum > scale };
}
function reset(prefix) {
for (const key of ceilings.keys()) if (key.startsWith(prefix)) ceilings.delete(key);
}
function axisValue(value, unit) {
if (value === 0) return "0";
const n = Number(value);
const format = v => v.toLocaleString(typeof locale === "function" ? locale() : undefined,
{ maximumFractionDigits: v < 1 ? 3 : v < 10 ? 2 : 1 });
if (unit === "bytes" || unit === "bytes/s") {
let magnitude = n, suffix = "B";
for (const next of ["KiB", "MiB", "GiB", "TiB"]) {
if (magnitude < 1024) break;
magnitude /= 1024; suffix = next;
}
return `${format(magnitude)} ${suffix}${unit === "bytes/s" ? "/s" : ""}`;
}
return `${format(n)}${unit === "percent" ? "%" : unit === "ms" ? " ms" : unit === "rate" ? "/s" : ""}`;
}
function setAxisText(node, value) {
if (node.textContent !== value) node.textContent = value;
}
function renderAxes(svg) {
if (!svg?.isConnected || !svg.rustProxyAxisData) return;
let host = svg.parentElement;
if (!host?.classList.contains("rp-chart-axis-host")) {
host = document.createElement("div");
host.className = "rp-chart-axis-host";
svg.before(host);
host.append(svg);
const overlay = document.createElement("div");
overlay.className = "rp-chart-axis-y";
overlay.setAttribute("aria-hidden", "true");
for (const name of ["top", "middle", "bottom"]) {
const tick = document.createElement("span");
tick.className = `rp-chart-axis-${name}`;
overlay.append(tick);
}
const perSeries = document.createElement("div");
perSeries.className = "rp-chart-axis-series";
perSeries.hidden = true;
const x = document.createElement("div");
x.className = "rp-chart-axis-x";
x.setAttribute("aria-label", "X");
for (const name of ["start", "caption", "end"]) {
const tick = document.createElement("span");
tick.className = `rp-chart-axis-${name}`;
x.append(tick);
}
host.append(overlay, perSeries, x);
}
const { peak: maximum, unit, count, top, bottom, clipped, seriesScales } = svg.rustProxyAxisData;
const compact = svg.closest(".stat-chart") !== null;
host.classList.toggle("rp-chart-axis-compact", compact);
const multi = Array.isArray(seriesScales) && seriesScales.length > 0;
host.classList.toggle("rp-chart-axis-multiple", multi);
const perSeries = host.querySelector(".rp-chart-axis-series");
perSeries.hidden = !multi;
if (multi) {
const values = seriesScales.map(([name, value]) => `${name}: 0–${axisValue(value, unit)}`);
if (perSeries.dataset.values !== values.join("|")) {
perSeries.replaceChildren(...values.map(value => {
const label = document.createElement("span");
label.textContent = value;
return label;
}));
perSeries.dataset.values = values.join("|");
}
}
host.style.setProperty("--rp-axis-top", `${Math.round(top * 1000) / 10}%`);
host.style.setProperty("--rp-axis-bottom", `${Math.round(bottom * 1000) / 10}%`);
setAxisText(host.querySelector(".rp-chart-axis-top"), `Y ${axisValue(maximum, unit)}${clipped ? " ↑" : ""}`);
setAxisText(host.querySelector(".rp-chart-axis-middle"), axisValue(maximum / 2, unit));
setAxisText(host.querySelector(".rp-chart-axis-bottom"), "0");
setAxisText(host.querySelector(".rp-chart-axis-start"), compact && !multi
? `Y 0–${axisValue(maximum, unit)}` : count ? "1" : "–");
setAxisText(host.querySelector(".rp-chart-axis-caption"), `X · ${typeof t === "function" ? t("chartAxisSamples") : "samples"}`);
setAxisText(host.querySelector(".rp-chart-axis-end"), count ? (compact ? `1–${count}` : String(count)) : "–");
host.querySelector(".rp-chart-axis-x").title = typeof t === "function" ? t("chartAxisSamplesHint") : "Sample indices, not elapsed time";
}
function axes(target, peakValue, unit, count,
{ top = 0.08, bottom = 0.92, clipped = false, seriesScales = null } = {}) {
const node = typeof target === "string" ? document.getElementById(target) : target;
const svg = node?.tagName?.toLowerCase() === "svg" ? node : node?.ownerSVGElement;
if (!svg) return;
svg.rustProxyAxisData = {
peak: Number.isFinite(peakValue) && peakValue > 0 ? peakValue : 1,
unit, count: Number.isInteger(count) && count > 0 ? count : 0,
top: Math.min(1, Math.max(0, top)), bottom: Math.min(1, Math.max(0, bottom)), clipped: clipped === true,
seriesScales: Array.isArray(seriesScales) ? seriesScales.filter(row =>
Array.isArray(row) && typeof row[0] === "string" && Number.isFinite(row[1]) && row[1] >= 0) : null,
};
if (document.documentElement.dataset.chartAxes === "true") renderAxes(svg);
}
function refreshAxes() {
const show = document.documentElement.dataset.chartAxes === "true";
for (const svg of document.querySelectorAll("svg")) {
if (show && svg.rustProxyAxisData) renderAxes(svg);
else if (!show && svg.parentElement?.classList.contains("rp-chart-axis-host")) {
const host = svg.parentElement;
host.before(svg);
host.remove();
}
}
}
return { peak, liveRate, reset, axes, refreshAxes, axisValue };
})();