v0.34.12
This commit is contained in:
1 parent
37c197c095
commit
5ac3617947
48 files changed
+1049
-255
No files matched your search
@@ -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 };
|
||||
})();
|
||||
Reference in new issue
Block a user