280 lines
11 KiB
JavaScript
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');
|
|
}
|
|
};
|
|
|
|
|
|
})();
|