import './style.css'; import { parseCSV } from './utils/csvParser'; import { calculateMetricACWR } from './utils/metricAcwr'; import type { Activity } from './types'; import { createDistanceChart, createDurationChart, createTSSChart, createCaloriesChart, destroyAllCharts, } from './components/charts'; // DOM elements const csvUpload = document.getElementById('csv-upload') as HTMLInputElement; const ftpInput = document.getElementById('ftp-input') as HTMLInputElement; const targetAcwrInput = document.getElementById('target-acwr-input') as HTMLInputElement; const predictTodayInput = document.getElementById('predict-today-input') as HTMLInputElement; const thresholdHrInput = document.getElementById('threshold-hr-input') as HTMLInputElement; const restingHrInput = document.getElementById('resting-hr-input') as HTMLInputElement; const uploadStatus = document.getElementById('upload-status') as HTMLDivElement; const chartsSection = document.getElementById('charts-section') as HTMLElement; const filterSection = document.getElementById('filter-section') as HTMLElement; const filterContainer = document.getElementById('filter-container') as HTMLElement; const helpButton = document.getElementById('help-button') as HTMLButtonElement; const helpPopover = document.getElementById('help-popover') as HTMLElement; const helpClose = document.getElementById('help-close') as HTMLButtonElement; // Store all activities globally let allActivities: Activity[] = []; let selectedActivityTypes: Set = new Set(['Running']); // Event listeners csvUpload?.addEventListener('change', handleFileUpload); targetAcwrInput?.addEventListener('input', handleTargetAcwrChange); predictTodayInput?.addEventListener('change', handlePredictTodayChange); helpButton?.addEventListener('click', () => helpPopover?.classList.remove('hidden')); helpClose?.addEventListener('click', () => helpPopover?.classList.add('hidden')); helpPopover?.addEventListener('click', (e) => { if (e.target === helpPopover) { helpPopover.classList.add('hidden'); } }); function handleTargetAcwrChange(): void { // Only refresh if we have activities loaded if (allActivities.length === 0) return; // Filter and render with new target ACWR const filteredActivities = selectedActivityTypes.size > 0 ? allActivities.filter(activity => selectedActivityTypes.has(activity.activityType || '')) : allActivities; const targetAcwr = parseFloat(targetAcwrInput.value) || 1.3; const predictToday = predictTodayInput?.checked || false; renderCharts(filteredActivities, targetAcwr, predictToday); } function handlePredictTodayChange(): void { // Only refresh if we have activities loaded if (allActivities.length === 0) return; // Filter and render with new predict today setting const filteredActivities = selectedActivityTypes.size > 0 ? allActivities.filter(activity => selectedActivityTypes.has(activity.activityType || '')) : allActivities; const targetAcwr = parseFloat(targetAcwrInput.value) || 1.3; const predictToday = predictTodayInput?.checked || false; renderCharts(filteredActivities, targetAcwr, predictToday); } function createActivityTypeFilters(activities: Activity[]): void { // Get unique activity types const activityTypes = new Set(); activities.forEach(activity => { if (activity.activityType) { activityTypes.add(activity.activityType); } }); // Define priority order const priorityOrder = ['Running', 'Cycling', 'Strength Training']; const sortedTypes: string[] = []; // Add priority types first (if they exist) priorityOrder.forEach(type => { if (activityTypes.has(type)) { sortedTypes.push(type); activityTypes.delete(type); } }); // Add remaining types alphabetically sortedTypes.push(...Array.from(activityTypes).sort()); // Clear existing filters filterContainer.innerHTML = ''; // Create checkbox for each activity type sortedTypes.forEach(type => { const label = document.createElement('label'); label.className = 'filter-checkbox'; const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.id = `filter-${type.replace(/\s+/g, '-').toLowerCase()}`; checkbox.value = type; checkbox.checked = selectedActivityTypes.has(type); checkbox.addEventListener('change', handleFilterChange); const icon = getActivityIcon(type); const span = document.createElement('span'); span.textContent = `${icon} ${type}`; label.appendChild(checkbox); label.appendChild(span); filterContainer.appendChild(label); }); } function getActivityIcon(activityType: string): string { const icons: { [key: string]: string } = { 'Running': '🏃', 'Cycling': '🚴', 'Strength Training': '🏋️', 'HIIT': '💪', 'Cardio': '❤️', }; return icons[activityType] || '🏃'; } function handleFilterChange(): void { // Update selected activity types based on checkboxes const checkboxes = filterContainer.querySelectorAll('input[type="checkbox"]') as NodeListOf; selectedActivityTypes.clear(); checkboxes.forEach(checkbox => { if (checkbox.checked) { selectedActivityTypes.add(checkbox.value); } }); // Filter and render const filteredActivities = selectedActivityTypes.size > 0 ? allActivities.filter(activity => selectedActivityTypes.has(activity.activityType || '')) : allActivities; const targetAcwr = parseFloat(targetAcwrInput.value) || 1.3; const predictToday = predictTodayInput?.checked || false; renderCharts(filteredActivities, targetAcwr, predictToday); } async function handleFileUpload(event: Event): Promise { const input = event.target as HTMLInputElement; const file = input.files?.[0]; if (!file) { return; } uploadStatus.textContent = 'Processing CSV file...'; uploadStatus.className = ''; try { // Get FTP and HR values from inputs const ftp = parseInt(ftpInput.value) || 343; const thresholdHR = parseInt(thresholdHrInput.value) || 170; const restingHR = parseInt(restingHrInput.value) || 50; // Parse CSV with user-provided FTP and HR values allActivities = await parseCSV(file, ftp, thresholdHR, restingHR); if (allActivities.length === 0) { throw new Error('No valid activities found in the CSV file'); } // Create dynamic activity type filters createActivityTypeFilters(allActivities); // Initial render with selected activity types const filteredActivities = selectedActivityTypes.size > 0 ? allActivities.filter(activity => selectedActivityTypes.has(activity.activityType || '')) : allActivities; const targetAcwr = parseFloat(targetAcwrInput.value) || 1.3; const predictToday = predictTodayInput?.checked || false; renderCharts(filteredActivities, targetAcwr, predictToday); // Show filter and charts sections filterSection.classList.remove('hidden'); chartsSection.classList.remove('hidden'); // Update status uploadStatus.textContent = `Successfully loaded ${allActivities.length} activities`; uploadStatus.className = 'success'; } catch (error) { console.error('Error processing file:', error); uploadStatus.textContent = error instanceof Error ? error.message : 'Failed to process CSV file'; uploadStatus.className = 'error'; filterSection.classList.add('hidden'); chartsSection.classList.add('hidden'); } } function renderCharts(activities: Activity[], targetAcwr: number, predictToday: boolean = false): void { // Calculate date range from first activity to today let dateRange: { start: Date; end: Date } | undefined; if (allActivities.length > 0) { // Determine end date based on whether we'll include today in predictions const today = new Date(); today.setHours(0, 0, 0, 0); // Check if today has activities const todayHasActivities = activities.some(activity => { const activityDate = new Date(activity.date); activityDate.setHours(0, 0, 0, 0); return activityDate.getTime() === today.getTime(); }); // If predictToday is checked and today has no activities, end range at yesterday // This prevents today from appearing twice (once as null, once as prediction) const endDate = new Date(); if (predictToday && !todayHasActivities) { // End at yesterday so today only appears in predictions endDate.setDate(endDate.getDate() - 1); } endDate.setHours(23, 59, 59, 999); // Calculate start date as 1.5 times chronic workload period (28 days) before end date const startDate = new Date(endDate); startDate.setDate(startDate.getDate() - Math.floor(28 * 1.5)); startDate.setHours(0, 0, 0, 0); dateRange = { start: startDate, end: endDate, }; } // Calculate ACWR for each metric with consistent date range const distanceData = calculateMetricACWR( activities, (activity) => activity.distance, dateRange, targetAcwr ); const durationData = calculateMetricACWR( activities, (activity) => activity.duration, dateRange, targetAcwr ); const tssData = calculateMetricACWR( activities, (activity) => activity.trainingStressScore, dateRange, targetAcwr ); const caloriesData = calculateMetricACWR( activities, (activity) => activity.calories, dateRange, targetAcwr ); // Destroy existing charts destroyAllCharts(); // Create new charts createDistanceChart(distanceData); createDurationChart(durationData); createTSSChart(tssData); createCaloriesChart(caloriesData); } // Initialize console.log('Training Load Data Visualization initialized');