cc-dashboard/src/static/js/charts.js

240 lines
6.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Chart.js configuration & factory.
*/
const ACCENT = '#FFC407';
const GRID_COLOR = 'rgba(255,255,255,0.06)';
const TEXT_COLOR = '#888';
const FONT = 'Montserrat';
Chart.defaults.color = TEXT_COLOR;
Chart.defaults.font.family = FONT;
Chart.defaults.font.size = 11;
const ChartDefs = {
base() {
return {
responsive: true,
maintainAspectRatio: false,
animation: { duration: 400 },
plugins: {
legend: { display: false },
tooltip: {
backgroundColor: '#1a1a1a',
borderColor: '#2e2e2e',
borderWidth: 1,
titleColor: '#f0f0f0',
bodyColor: '#888',
padding: 10,
titleFont: { family: FONT, weight: '700', size: 12 },
bodyFont: { family: FONT, size: 11 },
},
},
};
},
hoursBar(labels, data) {
return {
type: 'bar',
data: {
labels,
datasets: [{
data,
backgroundColor: ACCENT,
borderRadius: 6,
barThickness: 18,
}],
},
options: {
...this.base(),
indexAxis: 'y',
scales: {
x: {
grid: { color: GRID_COLOR },
ticks: { callback: v => v + 'h' },
},
y: { grid: { display: false } },
},
plugins: {
...this.base().plugins,
tooltip: {
...this.base().plugins.tooltip,
callbacks: { label: ctx => ` ${ctx.parsed.x.toFixed(1)}h` },
},
},
},
};
},
donut(labels, data) {
const colors = [ACCENT, '#fff176', '#e6af00', '#ffd740', '#fff9c4', '#ffb300', '#ffe082', '#ffca28'];
return {
type: 'doughnut',
data: {
labels,
datasets: [{
data,
backgroundColor: colors.slice(0, data.length),
borderWidth: 0,
hoverOffset: 8,
}],
},
options: {
...this.base(),
cutout: '68%',
plugins: {
...this.base().plugins,
legend: {
display: true,
position: 'right',
labels: {
color: TEXT_COLOR,
font: { family: FONT, size: 11 },
boxWidth: 10,
padding: 12,
},
},
tooltip: {
...this.base().plugins.tooltip,
callbacks: {
label: ctx => {
const total = ctx.dataset.data.reduce((a, b) => a + b, 0);
const pct = total ? ((ctx.parsed / total) * 100).toFixed(1) : 0;
return ` ${ctx.parsed.toFixed(1)}h (${pct}%)`;
},
},
},
},
},
};
},
lineArea(labels, data, label = 'Hours') {
return {
type: 'line',
data: {
labels,
datasets: [{
label,
data,
borderColor: ACCENT,
backgroundColor: 'rgba(255,196,7,0.12)',
fill: true,
tension: 0.35,
pointRadius: 3,
pointBackgroundColor: ACCENT,
borderWidth: 2,
}],
},
options: {
...this.base(),
scales: {
x: { grid: { color: GRID_COLOR } },
y: {
grid: { color: GRID_COLOR },
ticks: { callback: v => v + 'h' },
},
},
plugins: {
...this.base().plugins,
tooltip: {
...this.base().plugins.tooltip,
callbacks: { label: ctx => ` ${ctx.parsed.y.toFixed(1)}h` },
},
},
},
};
},
columnBar(labels, data) {
return {
type: 'bar',
data: {
labels,
datasets: [{
data,
backgroundColor: ACCENT,
borderRadius: 6,
}],
},
options: {
...this.base(),
scales: {
x: { grid: { display: false } },
y: {
grid: { color: GRID_COLOR },
ticks: { callback: v => v + 'h' },
},
},
plugins: {
...this.base().plugins,
tooltip: {
...this.base().plugins.tooltip,
callbacks: { label: ctx => ` ${ctx.parsed.y.toFixed(1)}h` },
},
},
},
};
},
timeline(sessions) {
// Gantt-like bars: each session = one horizontal bar
// Filter out sessions with missing/invalid timestamps
sessions = sessions.filter(s => {
const st = new Date(s.start_at).getTime();
const en = new Date(s.end_at).getTime();
return st > 0 && en > 0 && !isNaN(st) && !isNaN(en) && en >= st;
});
const labels = sessions.map(s => s.project_name.substring(0, 20));
const starts = sessions.map(s => new Date(s.start_at).getTime());
const ends = sessions.map(s => new Date(s.end_at).getTime());
const data = sessions.map((s, i) => [starts[i], ends[i]]);
return {
type: 'bar',
data: {
labels,
datasets: [{
data,
backgroundColor: ACCENT + 'cc',
borderSkipped: false,
borderRadius: 4,
barThickness: 16,
}],
},
options: {
...this.base(),
indexAxis: 'y',
scales: {
x: {
type: 'time',
time: { unit: 'hour', displayFormats: { hour: 'HH:mm' } },
grid: { color: GRID_COLOR },
},
y: { grid: { display: false } },
},
plugins: {
...this.base().plugins,
tooltip: {
...this.base().plugins.tooltip,
callbacks: {
label: ctx => {
const s = sessions[ctx.dataIndex];
const from = new Date(s.start_at).toLocaleTimeString('en', { hour: '2-digit', minute: '2-digit' });
const to = new Date(s.end_at).toLocaleTimeString('en', { hour: '2-digit', minute: '2-digit' });
return ` ${from} ${to} (${s.active_hours.toFixed(1)}h)`;
},
},
},
},
},
};
},
};
function makeChart(canvasId, config) {
const canvas = document.getElementById(canvasId);
if (!canvas) return null;
const existing = Chart.getChart(canvas);
if (existing) existing.destroy();
return new Chart(canvas, config);
}