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; }