import Chart from 'chart.js/auto'; /** * Theme-aware chart defaults */ function getChartColors() { const isDark = localStorage.theme !== 'light' && (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches); if (isDark) { return { textColor: '#ADBABD', gridColor: 'rgba(255, 255, 255, 0.06)', fillStart: 'rgba(190, 18, 60, 0.2)', fillEnd: 'rgba(190, 18, 60, 0.0)', borderColor: 'rgba(190, 18, 60, 0.9)', pointColor: 'rgba(190, 18, 60, 1)', pointHoverColor: '#ffffff', skeletonBase: '#262626', skeletonShimmer: '#333333', }; } return { textColor: '#6B7280', gridColor: 'rgba(0, 0, 0, 0.06)', fillStart: 'rgba(190, 18, 60, 0.15)', fillEnd: 'rgba(190, 18, 60, 0.0)', borderColor: 'rgba(190, 18, 60, 1.0)', pointColor: 'rgba(190, 18, 60, 1)', pointHoverColor: '#ffffff', skeletonBase: '#E5E7EB', skeletonShimmer: '#F3F4F6', }; } /** * Show the skeleton loader */ function showSkeleton() { const skeleton = document.getElementById('chart-skeleton'); const canvas = document.getElementById('monthlyChart'); const error = document.getElementById('chart-error'); if (skeleton) skeleton.style.display = ''; if (canvas) canvas.style.opacity = '0'; if (error) error.classList.add('hidden'); } /** * Hide the skeleton and show the chart canvas */ function hideSkeleton() { const skeleton = document.getElementById('chart-skeleton'); const canvas = document.getElementById('monthlyChart'); if (skeleton) { // Fade out skeleton skeleton.style.transition = 'opacity 0.4s ease-out'; skeleton.style.opacity = '0'; setTimeout(() => { if (skeleton) skeleton.style.display = 'none'; }, 400); } if (canvas) { setTimeout(() => { canvas.style.opacity = '1'; }, 200); } } /** * Show error state */ function showError() { const skeleton = document.getElementById('chart-skeleton'); const error = document.getElementById('chart-error'); if (skeleton) skeleton.style.display = 'none'; if (error) { error.classList.remove('hidden'); error.style.display = 'flex'; } } /** * Hide error state */ function hideError() { const error = document.getElementById('chart-error'); if (error) { error.classList.add('hidden'); error.style.display = 'none'; } } /** * Create gradient fill for the chart */ function createGradient(ctx, colors) { const gradient = ctx.createLinearGradient(0, 0, 0, ctx.canvas.clientHeight); gradient.addColorStop(0, colors.fillStart); gradient.addColorStop(1, colors.fillEnd); return gradient; } let monthlyViewChart = null; /** * Render the chart with data */ function renderChart(data) { const colors = getChartColors(); const canvas = document.getElementById('monthlyChart'); if (!canvas) return; const ctx = canvas.getContext('2d'); // Destroy previous chart instance if it exists if (monthlyViewChart) { monthlyViewChart.destroy(); monthlyViewChart = null; } const gradient = createGradient(ctx, colors); const chartData = { labels: data.map((entry) => entry.date), datasets: [{ label: 'Views', fill: true, backgroundColor: gradient, borderColor: colors.borderColor, borderWidth: 2.5, pointBackgroundColor: colors.pointColor, pointBorderColor: colors.pointColor, pointHoverBackgroundColor: colors.pointHoverColor, pointHoverBorderColor: colors.borderColor, pointHoverBorderWidth: 2, pointHoverRadius: 6, pointRadius: 2.5, pointHitRadius: 20, cubicInterpolationMode: 'monotone', tension: 0.4, data: data.map((entry) => entry.count), }] }; const config = { type: 'line', data: chartData, options: { responsive: true, maintainAspectRatio: false, animation: { duration: 1200, easing: 'easeOutQuart', }, plugins: { title: { display: false, }, legend: { display: false, }, tooltip: { backgroundColor: 'rgba(17, 17, 17, 0.95)', titleColor: '#ffffff', bodyColor: '#D1D5DB', borderColor: 'rgba(255, 255, 255, 0.1)', borderWidth: 1, padding: 12, cornerRadius: 10, displayColors: false, bodyFont: { size: 13, }, titleFont: { size: 12, weight: '600', }, callbacks: { label: function(context) { return 'Views: ' + new Intl.NumberFormat().format(context.parsed.y); }, title: function(context) { return context[0].label; } } }, }, interaction: { intersect: false, mode: 'index', }, scales: { x: { display: true, grid: { color: colors.gridColor, drawBorder: false, }, ticks: { color: colors.textColor, font: { size: 11, }, maxTicksLimit: 14, maxRotation: 0, }, title: { display: false, } }, y: { display: true, beginAtZero: true, grid: { color: colors.gridColor, drawBorder: false, }, ticks: { color: colors.textColor, font: { size: 11, }, callback: function(value) { if (value >= 1000000) return (value / 1000000).toFixed(1) + 'M'; if (value >= 1000) return (value / 1000).toFixed(1) + 'K'; return value; }, }, title: { display: false, }, } } }, }; hideError(); monthlyViewChart = new Chart(canvas, config); hideSkeleton(); } /** * Fetch chart data from the API */ async function fetchChartData() { showSkeleton(); hideError(); try { const response = await window.axios.get('/v1/monthly-views'); if (response.status !== 200 || !response.data || response.data.length === 0) { throw new Error('Invalid or empty response'); } renderChart(response.data); } catch (error) { console.error('Failed to load chart data:', error); showError(); } } /** * Retry loading the chart (called from error state button) */ window.retryChart = function() { if (monthlyViewChart) { monthlyViewChart.destroy(); monthlyViewChart = null; } fetchChartData(); }; // Listen for theme changes to re-render chart const themeObserver = new MutationObserver(() => { if (monthlyViewChart) { const data = monthlyViewChart.data.datasets[0].data.map((value, index) => ({ date: monthlyViewChart.data.labels[index], count: value, })); renderChart(data); } }); // Observe theme class changes on html element const htmlElement = document.documentElement; if (htmlElement) { themeObserver.observe(htmlElement, { attributes: true, attributeFilter: ['class'] }); } // Start the fetch when DOM is ready document.addEventListener('DOMContentLoaded', () => { // Small delay to ensure the page has rendered and skeleton is visible setTimeout(() => { fetchChartData(); }, 300); }); // Handle window resize for theme changes (system preference) window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { if (!('theme' in localStorage) && monthlyViewChart) { const data = monthlyViewChart.data.datasets[0].data.map((value, index) => ({ date: monthlyViewChart.data.labels[index], count: value, })); renderChart(data); } });