release
This commit is contained in:
279
static/js/charts.js
Normal file
279
static/js/charts.js
Normal file
@@ -0,0 +1,279 @@
|
||||
/* 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');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user