// Chart.js Configuration and Helpers class ChartManager { constructor() { this.charts = new Map(); this.defaultColors = [ '#fe8019', // Orange '#b8bb26', // Green '#fabd2f', // Yellow '#fb4934', // Red '#83a598', // Blue '#d3869b', // Purple '#8ec07c', // Aqua '#d65d0e', // Dark Orange '#98971a', // Dark Green '#d79921', // Dark Yellow ]; this.init(); } init() { // Set Chart.js defaults for Gruvbox Chart.defaults.color = '#bdae93'; // fg3 Chart.defaults.borderColor = '#504945'; // bg2 this.registerPlugins(); } registerPlugins() { // Register a plugin for tooltip background Chart.register({ id: 'customTooltip', beforeDraw: (chart) => { if (chart.tooltip._active && chart.tooltip._active.length) { const ctx = chart.ctx; const activePoint = chart.tooltip._active[0]; const x = activePoint.element.x; const topY = chart.scales.y.top; const bottomY = chart.scales.y.bottom; ctx.save(); ctx.beginPath(); ctx.setLineDash([5, 5]); ctx.moveTo(x, topY); ctx.lineTo(x, bottomY); ctx.lineWidth = 1; ctx.strokeStyle = '#665c54'; // bg3 ctx.stroke(); ctx.restore(); } } }); } createChart(canvasId, config) { const canvas = document.getElementById(canvasId); if (!canvas) { console.warn(`Canvas element #${canvasId} not found`); return null; } // Destroy existing chart if it exists if (this.charts.has(canvasId)) { this.charts.get(canvasId).destroy(); } // Create new chart const ctx = canvas.getContext('2d'); const chart = new Chart(ctx, { ...config, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'top', labels: { padding: 20, usePointStyle: true, pointStyle: 'circle', color: '#ebdbb2' // fg1 } }, tooltip: { mode: 'index', intersect: false, backgroundColor: '#3c3836', // bg1 titleColor: '#fbf1c7', // fg0 bodyColor: '#ebdbb2', // fg1 borderColor: '#504945', // bg2 borderWidth: 1, cornerRadius: 4, padding: 12, boxPadding: 6, callbacks: { label: function(context) { let label = context.dataset.label || ''; if (label) { label += ': '; } if (context.parsed.y !== null) { label += context.parsed.y.toLocaleString(); } return label; } } } }, interaction: { intersect: false, mode: 'nearest' }, scales: { x: { grid: { display: true, color: '#3c3836' // bg1 }, ticks: { color: '#a89984' // fg4 } }, y: { beginAtZero: true, grid: { display: true, color: '#3c3836' // bg1 }, ticks: { color: '#a89984', // fg4 callback: function(value) { return value.toLocaleString(); } } } }, ...config.options } }); // Store chart reference this.charts.set(canvasId, chart); return chart; } destroyChart(canvasId) { if (this.charts.has(canvasId)) { this.charts.get(canvasId).destroy(); this.charts.delete(canvasId); } } destroyAllCharts() { this.charts.forEach((chart, canvasId) => { chart.destroy(); }); this.charts.clear(); } // Chart templates createLineChart(canvasId, data, options = {}) { const config = { type: 'line', data: { labels: data.labels || [], datasets: data.datasets.map((dataset, index) => ({ label: dataset.label, data: dataset.data, borderColor: dataset.color || this.defaultColors[index % this.defaultColors.length], backgroundColor: dataset.fill ? this.hexToRgba(dataset.color || this.defaultColors[index % this.defaultColors.length], 0.15) : 'transparent', borderWidth: 2, pointRadius: 3, pointHoverRadius: 6, pointBackgroundColor: dataset.color || this.defaultColors[index % this.defaultColors.length], pointBorderColor: '#282828', // bg0 pointBorderWidth: 2, fill: dataset.fill || false, tension: 0.4, ...dataset })) }, options: { plugins: { tooltip: { callbacks: { label: function(context) { let label = context.dataset.label || ''; if (label) { label += ': '; } if (context.parsed.y !== null) { label += context.parsed.y.toLocaleString(); } return label; } } } }, ...options } }; return this.createChart(canvasId, config); } createBarChart(canvasId, data, options = {}) { const config = { type: 'bar', data: { labels: data.labels || [], datasets: data.datasets.map((dataset, index) => ({ label: dataset.label, data: dataset.data, backgroundColor: dataset.color || this.defaultColors[index % this.defaultColors.length], borderColor: dataset.borderColor || '#ffffff', borderWidth: 1, borderRadius: 4, borderSkipped: false, ...dataset })) }, options: { plugins: { tooltip: { callbacks: { label: function(context) { let label = context.dataset.label || ''; if (label) { label += ': '; } if (context.parsed.y !== null) { label += context.parsed.y.toLocaleString(); } return label; } } } }, ...options } }; return this.createChart(canvasId, config); } createPieChart(canvasId, data, options = {}) { const config = { type: 'pie', data: { labels: data.labels || [], datasets: [{ data: data.data || [], backgroundColor: data.colors || this.defaultColors.slice(0, data.data.length), borderColor: '#282828', // bg0 borderWidth: 2, hoverOffset: 15 }] }, options: { plugins: { legend: { position: 'right', labels: { padding: 20, usePointStyle: true, pointStyle: 'circle' } }, tooltip: { callbacks: { label: function(context) { const label = context.label || ''; const value = context.raw || 0; const total = context.dataset.data.reduce((a, b) => a + b, 0); const percentage = Math.round((value / total) * 100); return `${label}: ${value.toLocaleString()} (${percentage}%)`; } } } }, ...options } }; return this.createChart(canvasId, config); } createDoughnutChart(canvasId, data, options = {}) { const config = { type: 'doughnut', data: { labels: data.labels || [], datasets: [{ data: data.data || [], backgroundColor: data.colors || this.defaultColors.slice(0, data.data.length), borderColor: '#282828', // bg0 borderWidth: 2, hoverOffset: 15 }] }, options: { cutout: '60%', plugins: { legend: { position: 'right', labels: { padding: 20, usePointStyle: true, pointStyle: 'circle' } }, tooltip: { callbacks: { label: function(context) { const label = context.label || ''; const value = context.raw || 0; const total = context.dataset.data.reduce((a, b) => a + b, 0); const percentage = Math.round((value / total) * 100); return `${label}: ${value.toLocaleString()} (${percentage}%)`; } } } }, ...options } }; return this.createChart(canvasId, config); } createHorizontalBarChart(canvasId, data, options = {}) { const config = { type: 'bar', data: { labels: data.labels || [], datasets: data.datasets.map((dataset, index) => ({ label: dataset.label, data: dataset.data, backgroundColor: dataset.color || this.defaultColors[index % this.defaultColors.length], borderColor: '#282828', // bg0 borderWidth: 1, borderRadius: 4, ...dataset })) }, options: { indexAxis: 'y', plugins: { legend: { display: false }, tooltip: { callbacks: { label: function(context) { let label = context.dataset.label || ''; if (label) { label += ': '; } if (context.parsed.x !== null) { label += context.parsed.x.toLocaleString(); } return label; } } } }, scales: { x: { beginAtZero: true, grid: { display: true, color: '#3c3836' // bg1 }, ticks: { color: '#a89984', // fg4 callback: function(value) { return value.toLocaleString(); } } }, y: { grid: { display: false }, ticks: { color: '#a89984' // fg4 } } }, ...options } }; return this.createChart(canvasId, config); } // Helper methods hexToRgba(hex, alpha = 1) { const r = parseInt(hex.slice(1, 3), 16); const g = parseInt(hex.slice(3, 5), 16); const b = parseInt(hex.slice(5, 7), 16); return `rgba(${r}, ${g}, ${b}, ${alpha})`; } generateTimeLabels(hours = 24) { const now = luxon.DateTime.now(); const labels = []; for (let i = hours - 1; i >= 0; i--) { const time = now.minus({ hours: i }); labels.push(time.toFormat('HH:00')); } return labels; } generateDateLabels(days = 7) { const now = luxon.DateTime.now(); const labels = []; for (let i = days - 1; i >= 0; i--) { const date = now.minus({ days: i }); labels.push(date.toFormat('MMM dd')); } return labels; } // Demo data generators generateDemoTimeSeries(hours = 24, seriesCount = 1) { const labels = this.generateTimeLabels(hours); const datasets = []; for (let i = 0; i < seriesCount; i++) { const data = []; let value = Math.floor(Math.random() * 100) + 50; for (let j = 0; j < hours; j++) { // Add some randomness but keep trend value += Math.floor(Math.random() * 20) - 10; value = Math.max(10, value); data.push(value); } datasets.push({ label: `Series ${i + 1}`, data: data, color: this.defaultColors[i % this.defaultColors.length] }); } return { labels, datasets }; } generateDemoBarData(labels, seriesCount = 1) { const datasets = []; for (let i = 0; i < seriesCount; i++) { const data = labels.map(() => Math.floor(Math.random() * 100) + 20); datasets.push({ label: `Dataset ${i + 1}`, data: data, color: this.defaultColors[i % this.defaultColors.length] }); } return { labels, datasets }; } generateDemoPieData(labels) { const data = labels.map(() => Math.floor(Math.random() * 100) + 10); const total = data.reduce((a, b) => a + b, 0); return { labels: labels, data: data, colors: labels.map((_, i) => this.defaultColors[i % this.defaultColors.length]) }; } // Update chart data updateChartData(canvasId, newData) { const chart = this.charts.get(canvasId); if (!chart) return; chart.data.labels = newData.labels || chart.data.labels; if (newData.datasets) { newData.datasets.forEach((dataset, index) => { if (chart.data.datasets[index]) { chart.data.datasets[index].data = dataset.data; if (dataset.label) { chart.data.datasets[index].label = dataset.label; } } }); } chart.update(); } // Add data point to time series addDataPoint(canvasId, newPoint, datasetIndex = 0) { const chart = this.charts.get(canvasId); if (!chart || chart.config.type !== 'line') return; const dataset = chart.data.datasets[datasetIndex]; if (!dataset) return; // Add new point dataset.data.push(newPoint); // Remove oldest point if we have too many if (dataset.data.length > 100) { dataset.data.shift(); chart.data.labels.shift(); } else { // Add new label const now = luxon.DateTime.now(); chart.data.labels.push(now.toFormat('HH:mm:ss')); } chart.update(); } } // Initialize chart manager when DOM is loaded document.addEventListener('DOMContentLoaded', () => { window.chartManager = new ChartManager(); }); // Export for use in other modules if (typeof module !== 'undefined' && module.exports) { module.exports = ChartManager; }