This commit is contained in:
Mateusz Gruszczyński committed 2026-10-25 09:58:39 +01:00
1 parent 37c197c095
commit 5ac3617947
48 files changed
+1049 -255

No files matched your search

+105 -9
View File
@@ -1,5 +1,4 @@
// Overview charts retain stable, expanding ranges (and fixed 0-100% axes).
// Vhost request-rate sparklines use a separate, adaptive plotting policy.
window.rustProxyChartScale = (() => {
const floors = { rate: 100, count: 10, ms: 100, bytes: 64 * 1024 * 1024, "bytes/s": 64 * 1024 };
const ceilings = new Map();
@@ -17,17 +16,12 @@ window.rustProxyChartScale = (() => {
ceilings.set(key, result);
return result;
}
// Vhost live charts show the entire session history, but their Y range
// follows the recent typical rate. A one-off surge must not flatten the
// subsequent request activity; the real (unclipped) peak stays in the legend.
// The recent quantile also prevents a range that only grows over time.
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);
// Interpolation would allow a single extreme point to dominate small
// histories. Nearest lower rank excludes that isolated maximum.
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) => {
@@ -40,5 +34,107 @@ window.rustProxyChartScale = (() => {
function reset(prefix) {
for (const key of ceilings.keys()) if (key.startsWith(prefix)) ceilings.delete(key);
}
return { peak, liveRate, reset };
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 };
})();