Spaces:
Running
Running
| 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; | |
| // 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: false, | |
| 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, | |
| barColor: string | |
| ): Chart { | |
| const canvas = document.getElementById(canvasId) as HTMLCanvasElement; | |
| if (!canvas) throw new Error(`Canvas ${canvasId} not found`); | |
| const config: ChartConfiguration = { | |
| type: 'bar', | |
| data: { | |
| labels: data.dates, | |
| datasets: [ | |
| { | |
| type: 'bar', | |
| label: `Daily ${metricLabel}`, | |
| data: data.values, | |
| backgroundColor: barColor, | |
| borderWidth: 0, | |
| borderRadius: 2, | |
| barPercentage: 0.8, | |
| yAxisID: 'y', | |
| }, | |
| { | |
| type: 'line', | |
| label: '7-Day Average', | |
| data: data.average7d, | |
| borderColor: 'rgba(239, 68, 68, 0.9)', | |
| backgroundColor: 'rgba(239, 68, 68, 0.1)', | |
| borderWidth: 2, | |
| pointRadius: 0, | |
| pointHoverRadius: 4, | |
| tension: 0.4, | |
| fill: false, | |
| yAxisID: 'y', | |
| }, | |
| { | |
| type: 'line', | |
| label: '28-Day Average', | |
| data: data.average28d, | |
| borderColor: 'rgba(249, 115, 22, 0.9)', | |
| backgroundColor: 'rgba(249, 115, 22, 0.1)', | |
| borderWidth: 2, | |
| pointRadius: 0, | |
| pointHoverRadius: 4, | |
| tension: 0.4, | |
| fill: false, | |
| yAxisID: 'y', | |
| }, | |
| { | |
| type: 'line', | |
| label: 'ACWR', | |
| data: data.acwr, | |
| borderColor: 'rgba(139, 92, 246, 1)', | |
| backgroundColor: 'rgba(139, 92, 246, 0.1)', | |
| borderWidth: 3, | |
| pointRadius: 0, | |
| pointHoverRadius: 5, | |
| 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(59, 130, 246, 0.6)' | |
| ); | |
| } | |
| export function createDurationChart(data: MetricACWRData): void { | |
| if (durationChart) { | |
| durationChart.destroy(); | |
| } | |
| durationChart = createDualAxisChart( | |
| 'duration-chart', | |
| data, | |
| 'Duration', | |
| '(min)', | |
| 'rgba(16, 185, 129, 0.6)' | |
| ); | |
| } | |
| export function createTSSChart(data: MetricACWRData): void { | |
| if (tssChart) { | |
| tssChart.destroy(); | |
| } | |
| tssChart = createDualAxisChart( | |
| 'tss-chart', | |
| data, | |
| 'TSS', | |
| '', | |
| 'rgba(245, 158, 11, 0.6)' | |
| ); | |
| } | |
| export function destroyAllCharts(): void { | |
| if (distanceChart) { | |
| distanceChart.destroy(); | |
| distanceChart = null; | |
| } | |
| if (durationChart) { | |
| durationChart.destroy(); | |
| durationChart = null; | |
| } | |
| if (tssChart) { | |
| tssChart.destroy(); | |
| tssChart = null; | |
| } | |
| } | |