import { Chart, ChartConfiguration, registerables } from 'chart.js'; import type { MetricACWRData } from '@/types'; // Register all Chart.js components Chart.register(...registerables); let distanceChart: Chart | null = null; let durationChart: Chart | null = null; let tssChart: Chart | null = null; // ACWR color zones function getACWRColor(value: number | null): string { if (value === null || value === undefined) return 'rgba(148, 163, 184, 0.3)'; if (value < 0.8) return 'rgba(59, 130, 246, 0.9)'; // Blue - Detraining risk if (value <= 1.3) return 'rgba(16, 185, 129, 0.9)'; // Green - Optimal if (value <= 1.5) return 'rgba(249, 115, 22, 0.9)'; // Orange - Warning return 'rgba(239, 68, 68, 0.9)'; // Red - Injury risk } // Plugin to draw gradient-colored ACWR line const acwrGradientPlugin = { id: 'acwrGradient', afterDatasetsDraw(chart: Chart) { const meta = chart.getDatasetMeta(3); // ACWR is the 4th dataset (index 3) if (!meta || meta.hidden) return; const ctx = chart.ctx; const data = chart.data.datasets[3].data as (number | null)[]; const chartArea = chart.chartArea; ctx.save(); // First, fill the area under the curve with gradient colors for (let i = 0; i < meta.data.length - 1; i++) { const point1 = meta.data[i]; const point2 = meta.data[i + 1]; if (!point1 || !point2) continue; const value1 = data[i]; const value2 = data[i + 1]; // Skip if both values are null if (value1 === null && value2 === null) continue; // Use the second point value (most recent/rightward) const segmentValue = value2 ?? value1; const color = getACWRColor(segmentValue); // Make the fill more transparent const fillColor = color.replace(/[\d.]+\)$/, '0.15)'); ctx.fillStyle = fillColor; ctx.beginPath(); ctx.moveTo(point1.x, point1.y); ctx.lineTo(point2.x, point2.y); ctx.lineTo(point2.x, chartArea.bottom); ctx.lineTo(point1.x, chartArea.bottom); ctx.closePath(); ctx.fill(); } // Then draw the line on top ctx.lineWidth = 4; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; // Draw line segments with colors based on values for (let i = 0; i < meta.data.length - 1; i++) { const point1 = meta.data[i]; const point2 = meta.data[i + 1]; if (!point1 || !point2) continue; const value1 = data[i]; const value2 = data[i + 1]; // Skip if both values are null if (value1 === null && value2 === null) continue; // Use the second point value (most recent/rightward) const segmentValue = value2 ?? value1; ctx.strokeStyle = getACWRColor(segmentValue); ctx.beginPath(); ctx.moveTo(point1.x, point1.y); ctx.lineTo(point2.x, point2.y); ctx.stroke(); } ctx.restore(); }, }; // Register the custom plugin Chart.register(acwrGradientPlugin); // Common chart styling inspired by Garmin const commonOptions = { responsive: true, maintainAspectRatio: true, plugins: { legend: { display: true, position: 'top' as const, labels: { usePointStyle: true, padding: 15, font: { size: 12, }, }, }, tooltip: { mode: 'index' as const, intersect: false, backgroundColor: 'rgba(255, 255, 255, 0.95)', titleColor: '#1e293b', bodyColor: '#475569', borderColor: '#e2e8f0', borderWidth: 1, padding: 12, displayColors: true, }, }, scales: { x: { grid: { display: true, color: 'rgba(148, 163, 184, 0.15)', lineWidth: 1, drawBorder: false, }, ticks: { maxRotation: 45, minRotation: 45, padding: 8, color: '#64748b', font: { size: 10, }, autoSkip: true, maxTicksLimit: 20, }, }, }, }; function createDualAxisChart( canvasId: string, data: MetricACWRData, metricLabel: string, metricUnit: string, dotColor: string ): Chart { const canvas = document.getElementById(canvasId) as HTMLCanvasElement; if (!canvas) throw new Error(`Canvas ${canvasId} not found`); const config: ChartConfiguration = { type: 'line', data: { labels: data.dates, datasets: [ { type: 'scatter', label: `Daily ${metricLabel}`, data: data.values, backgroundColor: dotColor, borderColor: dotColor.replace('0.8)', '1)'), borderWidth: 1, pointRadius: 5, pointHoverRadius: 7, yAxisID: 'y', }, { type: 'line', label: '7-Day Average', data: data.average7d, borderColor: 'rgba(168, 85, 247, 0.5)', backgroundColor: 'rgba(168, 85, 247, 0.05)', borderWidth: 1.5, pointRadius: 0, pointHoverRadius: 4, tension: 0.4, fill: false, yAxisID: 'y', }, { type: 'line', label: '28-Day Average', data: data.average28d, borderColor: 'rgba(236, 72, 153, 0.5)', backgroundColor: 'rgba(236, 72, 153, 0.05)', borderWidth: 1.5, pointRadius: 0, pointHoverRadius: 4, tension: 0.4, fill: false, yAxisID: 'y', }, { type: 'line', label: 'ACWR', data: data.acwr, borderColor: 'rgba(139, 92, 246, 0)', // Transparent - we draw it in the plugin backgroundColor: 'rgba(139, 92, 246, 0)', borderWidth: 4, pointRadius: 0, pointHoverRadius: 5, pointHoverBackgroundColor: (context: any) => { const value = context.raw; return getACWRColor(value); }, tension: 0.4, fill: false, yAxisID: 'y1', }, ], }, options: { ...commonOptions, scales: { ...commonOptions.scales, y: { type: 'linear', position: 'left', beginAtZero: true, border: { display: false, }, grid: { color: 'rgba(148, 163, 184, 0.1)', }, ticks: { padding: 8, color: '#64748b', font: { size: 11, }, }, title: { display: true, text: `${metricLabel} ${metricUnit}`, color: '#64748b', font: { size: 12, weight: 500, }, }, }, y1: { type: 'linear', position: 'right', beginAtZero: true, suggestedMin: 0, suggestedMax: 2, grid: { drawOnChartArea: false, }, ticks: { padding: 8, color: '#8b5cf6', font: { size: 11, }, }, title: { display: true, text: 'ACWR', color: '#8b5cf6', font: { size: 12, weight: 500, }, }, }, }, interaction: { mode: 'index' as const, intersect: false, }, }, }; return new Chart(canvas, config); } export function createDistanceChart(data: MetricACWRData): void { if (distanceChart) { distanceChart.destroy(); } distanceChart = createDualAxisChart( 'distance-chart', data, 'Distance', '(km)', 'rgba(234, 179, 8, 0.8)' ); } export function createDurationChart(data: MetricACWRData): void { if (durationChart) { durationChart.destroy(); } durationChart = createDualAxisChart( 'duration-chart', data, 'Duration', '(min)', 'rgba(234, 179, 8, 0.8)' ); } export function createTSSChart(data: MetricACWRData): void { if (tssChart) { tssChart.destroy(); } tssChart = createDualAxisChart( 'tss-chart', data, 'TSS', '', 'rgba(234, 179, 8, 0.8)' ); } export function destroyAllCharts(): void { if (distanceChart) { distanceChart.destroy(); distanceChart = null; } if (durationChart) { durationChart.destroy(); durationChart = null; } if (tssChart) { tssChart.destroy(); tssChart = null; } }