- Replace tabs with Recent/Trends/Distribution/Tables/Analysis and add sticky controls (interval, domain, window [default 7d], percent, group small, exclude '-' -> Uncached, smoothing toggle). - Client-side transforms: time-window slicing, percent mode, group others (3%), per-report exclusions; stackedBar multi-series; moving average for error_rate. - Generator: pass through optional UX metadata (windows_supported, window_default, group_others_threshold, exclude_values, top_n, stacked, palette) and enforce top_n LIMIT for table reports. - Reports: add status_classes_timeseries and cache_status_timeseries; apply top_n=50 to heavy tables. - Chart manager: add helpers (sliceWindow, excludeValues, toPercent, groupOthers, movingAverage). - URL state + localStorage for context; per-tab filtering for Trends/Distribution/Tables.
109 lines
3 KiB
JavaScript
109 lines
3 KiB
JavaScript
export let currentLoad = null;
|
|
const loadInfo = new Map();
|
|
|
|
export function newLoad(container) {
|
|
if (currentLoad) {
|
|
abortLoad(currentLoad);
|
|
}
|
|
reset(container);
|
|
const controller = new AbortController();
|
|
const token = { controller, charts: new Map() };
|
|
loadInfo.set(token, token);
|
|
currentLoad = token;
|
|
return token;
|
|
}
|
|
|
|
export function abortLoad(token) {
|
|
const info = loadInfo.get(token);
|
|
if (!info) return;
|
|
info.controller.abort();
|
|
info.charts.forEach(chart => {
|
|
try {
|
|
chart.destroy();
|
|
} catch (e) {}
|
|
});
|
|
loadInfo.delete(token);
|
|
if (currentLoad === token) {
|
|
currentLoad = null;
|
|
}
|
|
}
|
|
|
|
export function registerChart(token, id, chart) {
|
|
const info = loadInfo.get(token);
|
|
if (info) {
|
|
info.charts.set(id, chart);
|
|
} else {
|
|
chart.destroy();
|
|
}
|
|
}
|
|
|
|
export function reset(container) {
|
|
if (!container) return;
|
|
container.querySelectorAll('canvas').forEach(c => {
|
|
const chart = Chart.getChart(c);
|
|
if (chart) {
|
|
chart.destroy();
|
|
}
|
|
});
|
|
container.innerHTML = '';
|
|
}
|
|
|
|
// ---- Lightweight client-side data helpers ----
|
|
|
|
// Slice last N rows from a time-ordered array
|
|
export function sliceWindow(data, n) {
|
|
if (!Array.isArray(data) || n === undefined || n === null) return data;
|
|
if (n === 'all') return data;
|
|
const count = Number(n);
|
|
if (!Number.isFinite(count) || count <= 0) return data;
|
|
return data.slice(-count);
|
|
}
|
|
|
|
// Exclude rows whose value in key is in excluded list
|
|
export function excludeValues(data, key, excluded = []) {
|
|
if (!excluded || excluded.length === 0) return data;
|
|
const set = new Set(excluded);
|
|
return data.filter(row => !set.has(row[key]));
|
|
}
|
|
|
|
// Compute percentages for categorical distributions (valueKey default 'value')
|
|
export function toPercent(data, valueKey = 'value') {
|
|
const total = data.reduce((s, r) => s + (Number(r[valueKey]) || 0), 0);
|
|
if (total <= 0) return data.map(r => ({ ...r }));
|
|
return data.map(r => ({ ...r, [valueKey]: (Number(r[valueKey]) || 0) * 100 / total }));
|
|
}
|
|
|
|
// Group categories with share < threshold into an 'Other' bucket.
|
|
export function groupOthers(data, bucketKey, valueKey = 'value', threshold = 0.03, otherLabel = 'Other') {
|
|
if (!Array.isArray(data) || data.length === 0) return data;
|
|
const total = data.reduce((s, r) => s + (Number(r[valueKey]) || 0), 0);
|
|
if (total <= 0) return data;
|
|
const major = [];
|
|
let other = 0;
|
|
for (const r of data) {
|
|
const v = Number(r[valueKey]) || 0;
|
|
if (total && v / total < threshold) {
|
|
other += v;
|
|
} else {
|
|
major.push({ ...r });
|
|
}
|
|
}
|
|
if (other > 0) major.push({ [bucketKey]: otherLabel, [valueKey]: other });
|
|
return major;
|
|
}
|
|
|
|
// Simple moving average over numeric array
|
|
export function movingAverage(series, span = 3) {
|
|
const n = Math.max(1, Number(span) || 1);
|
|
const out = [];
|
|
for (let i = 0; i < series.length; i++) {
|
|
const start = Math.max(0, i - n + 1);
|
|
let sum = 0, cnt = 0;
|
|
for (let j = start; j <= i; j++) {
|
|
const v = Number(series[j]);
|
|
if (Number.isFinite(v)) { sum += v; cnt++; }
|
|
}
|
|
out.push(cnt ? sum / cnt : null);
|
|
}
|
|
return out;
|
|
}
|