141 lines
7.3 KiB
JavaScript
141 lines
7.3 KiB
JavaScript
|
||
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 };
|
||
})();
|