313 lines
9.0 KiB
JavaScript
313 lines
9.0 KiB
JavaScript
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);
|
|
}
|
|
}); |