Files
autoban/static/js/charts.js
2026-01-01 02:13:34 +01:00

280 lines
11 KiB
JavaScript

/* eslint-disable no-undef */
// charts.js — UX v2 (fixed quick-top)
(function () {
const $ = (sel, root = document) => root.querySelector(sel);
const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel));
// Kolory dla dark mode
const palette = {
grid: 'rgba(255,255,255,0.1)',
tick: 'rgba(255,255,255,0.7)',
bar: 'rgba(54, 162, 235, 0.8)',
bar2: 'rgba(255, 159, 64, 0.85)',
line: 'rgba(255, 99, 132, 1)',
lineFill: 'rgba(255, 99, 132, 0.2)',
pie: [
'rgba(255, 99, 132, 0.85)',
'rgba(54, 162, 235, 0.85)',
'rgba(255, 206, 86, 0.85)',
'rgba(75, 192, 192, 0.85)',
'rgba(153, 102, 255, 0.85)',
'rgba(255, 159, 64, 0.85)',
'rgba(199, 199, 199, 0.85)'
]
};
function emptyState(canvasId, emptyId, labels, dataArr) {
const hasData = Array.isArray(labels) && labels.length > 0 && Array.isArray(dataArr) && dataArr.some(v => v > 0);
const empty = document.getElementById(emptyId);
const canvas = document.getElementById(canvasId);
if (!hasData) {
canvas?.classList.add('d-none');
empty?.classList.remove('d-none');
return true;
}
canvas?.classList.remove('d-none');
empty?.classList.add('d-none');
return false;
}
function csvFromSeries(labels, data, headerX = 'Label', headerY = 'Value') {
const rows = [[headerX, headerY], ...labels.map((l, i) => [l, data[i] ?? 0])];
return rows.map(r => r.map(v => `"${String(v).replace(/"/g, '""')}"`).join(',')).join('\n');
}
function download(filename, content, mime) {
const blob = new Blob([content], { type: mime });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = filename; a.click();
URL.revokeObjectURL(url);
}
function bindDownloads(id, labels, data) {
$$('.chart-download').forEach(btn => {
if (btn.dataset.target !== id) return;
btn.addEventListener('click', () => {
const type = btn.dataset.type;
if (type === 'csv') {
download(`${id}.csv`, csvFromSeries(labels, data), 'text/csv;charset=utf-8');
} else if (type === 'png') {
const c = document.getElementById(id);
if (!c) return;
const url = c.toDataURL('image/png');
const a = document.createElement('a');
a.href = url; a.download = `${id}.png`; a.click();
}
});
});
}
function bindFullscreen(id) {
$$('.chart-fullscreen').forEach(btn => {
if (btn.dataset.target !== id) return;
btn.addEventListener('click', () => {
const src = document.getElementById(id);
const dst = document.getElementById('chartModalCanvas');
const chart = Chart.getChart(src);
if (!chart) return;
const modal = new bootstrap.Modal(document.getElementById('chartModal'));
modal.show();
setTimeout(() => {
const old = Chart.getChart(dst);
old && old.destroy();
new Chart(dst.getContext('2d'), {
type: chart.config.type,
data: JSON.parse(JSON.stringify(chart.config.data)),
options: Object.assign({}, chart.config.options, {
maintainAspectRatio: false
})
});
}, 150);
});
});
}
// Bind przycisków szybkiego wyboru: nie wysyłamy duplikatu name,
// tylko ustawiamy <select name="top_n"> i submitujemy formularz.
function bindQuickTop() {
const form = document.getElementById('options-form');
const select = document.getElementById('top_n');
if (!form || !select) return;
$$('.quick-top').forEach(btn => {
btn.addEventListener('click', () => {
const val = btn.dataset.value;
if (val && select) select.value = String(val);
// odśwież aktywny stan w UI (opcjonalnie, dla natychmiastowego feedbacku)
$$('.quick-top').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
form.requestSubmit();
});
});
// Zmiana w <select> nadal auto-submituje:
select.addEventListener('change', () => form.requestSubmit());
}
// Auto-inicjalizacja niezależnie od kolejności ładowania
if (document.readyState === 'loading') {
window.addEventListener('DOMContentLoaded', bindQuickTop);
} else {
bindQuickTop();
}
function bindQuickPeriod() {
const form = document.getElementById('options-form');
const select = document.getElementById('period');
if (!form || !select) return;
// Szybkie przyciski okresu (Tydzień/Miesiąc/Rok)
$$('.quick-period').forEach(btn => {
btn.addEventListener('click', () => {
const val = btn.dataset.value;
if (val) select.value = String(val);
// natychmiastowy feedback w UI
$$('.quick-period').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
form.requestSubmit();
});
});
// Zmiana w <select id="period"> też auto-submituje
select.addEventListener('change', () => form.requestSubmit());
}
// Auto-inicjalizacja (analogicznie do bindQuickTop)
if (document.readyState === 'loading') {
window.addEventListener('DOMContentLoaded', bindQuickPeriod);
} else {
bindQuickPeriod();
}
// Główne API — rysowanie wykresów
window.renderCharts = function renderCharts(stats) {
// ====== Powody (bar - poziomo) ======
{
const labels = (stats.top_reasons || []).map(r => r.reason);
const data = (stats.top_reasons || []).map(r => r.count);
if (!emptyState('reasonsChart', 'reasonsEmpty', labels, data)) {
const ctx = document.getElementById('reasonsChart').getContext('2d');
new Chart(ctx, {
type: 'bar',
data: {
labels,
datasets: [{ data, backgroundColor: palette.bar2, borderRadius: 6, borderSkipped: false }]
},
options: {
indexAxis: 'y',
responsive: true,
plugins: { legend: { display: false }, tooltip: { intersect: false, mode: 'nearest' } },
scales: {
x: { grid: { color: palette.grid }, ticks: { color: palette.tick, precision: 0 } },
y: { grid: { display: false }, ticks: { color: palette.tick } }
}
}
});
}
bindDownloads('reasonsChart', labels, data);
bindFullscreen('reasonsChart');
}
// ====== URL (bar - poziomo) ======
{
const labels = (stats.top_urls || []).map(u => u.url);
const data = (stats.top_urls || []).map(u => u.count);
if (!emptyState('urlsChart', 'urlsEmpty', labels, data)) {
const ctx = document.getElementById('urlsChart').getContext('2d');
new Chart(ctx, {
type: 'bar',
data: {
labels,
datasets: [{ data, backgroundColor: palette.bar, borderRadius: 6, borderSkipped: false }]
},
options: {
indexAxis: 'y',
responsive: true,
plugins: { legend: { display: false }, tooltip: { intersect: false, mode: 'nearest' } },
scales: {
x: { grid: { color: palette.grid }, ticks: { color: palette.tick, precision: 0 } },
y: { grid: { display: false }, ticks: { color: palette.tick } }
}
}
});
}
bindDownloads('urlsChart', labels, data);
bindFullscreen('urlsChart');
}
// ====== Kraje (pie) ======
{
const labels = (stats.top_countries || []).map(c => c.country);
const data = (stats.top_countries || []).map(c => c.count);
if (!emptyState('countriesChart', 'countriesEmpty', labels, data)) {
const ctx = document.getElementById('countriesChart').getContext('2d');
new Chart(ctx, {
type: 'pie',
data: { labels, datasets: [{ data, backgroundColor: palette.pie, borderWidth: 1, borderColor: '#222' }] },
options: {
responsive: true,
plugins: {
legend: { position: 'bottom', labels: { color: palette.tick } },
tooltip: { callbacks: { label: (item) => `${item.label}: ${item.formattedValue}` } }
}
}
});
}
bindDownloads('countriesChart', labels, data);
bindFullscreen('countriesChart');
}
// ====== Bany w czasie (line) ======
{
const labels = stats.weeks || [];
const data = stats.bans_per_week || [];
if (!emptyState('bansOverTimeChart', 'timeEmpty', labels, data)) {
const ctx = document.getElementById('bansOverTimeChart').getContext('2d');
const gradient = ctx.createLinearGradient(0, 0, 0, 200);
gradient.addColorStop(0, palette.lineFill);
gradient.addColorStop(1, 'rgba(0,0,0,0)');
new Chart(ctx, {
type: 'line',
data: {
labels,
datasets: [{
label: 'Bany / tydzień',
data,
borderColor: palette.line,
backgroundColor: gradient,
fill: true,
cubicInterpolationMode: 'monotone',
tension: 0.25,
pointRadius: 2,
pointHoverRadius: 4
}]
},
options: {
responsive: true,
plugins: { legend: { display: false } },
scales: {
x: { grid: { color: palette.grid }, ticks: { color: palette.tick } },
y: { grid: { color: palette.grid }, ticks: { color: palette.tick, precision: 0 } }
}
}
});
}
bindDownloads('bansOverTimeChart', labels, data);
bindFullscreen('bansOverTimeChart');
}
};
})();