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']); // Local storage keys const STORAGE_KEYS = { CSV_DATA: 'trainingload_csv_data', FTP: 'trainingload_ftp', THRESHOLD_HR: 'trainingload_threshold_hr', RESTING_HR: 'trainingload_resting_hr', TARGET_ACWR: 'trainingload_target_acwr', PREDICT_TODAY: 'trainingload_predict_today', SELECTED_FILTERS: 'trainingload_selected_filters', }; // Load saved settings on startup function loadSavedSettings(): void { // Load FTP const savedFtp = localStorage.getItem(STORAGE_KEYS.FTP); if (savedFtp && ftpInput) { ftpInput.value = savedFtp; } // Load Threshold HR const savedThresholdHr = localStorage.getItem(STORAGE_KEYS.THRESHOLD_HR); if (savedThresholdHr && thresholdHrInput) { thresholdHrInput.value = savedThresholdHr; } // Load Resting HR const savedRestingHr = localStorage.getItem(STORAGE_KEYS.RESTING_HR); if (savedRestingHr && restingHrInput) { restingHrInput.value = savedRestingHr; } // Load Target ACWR const savedTargetAcwr = localStorage.getItem(STORAGE_KEYS.TARGET_ACWR); if (savedTargetAcwr && targetAcwrInput) { targetAcwrInput.value = savedTargetAcwr; } // Load Predict Today const savedPredictToday = localStorage.getItem(STORAGE_KEYS.PREDICT_TODAY); if (savedPredictToday && predictTodayInput) { predictTodayInput.checked = savedPredictToday === 'true'; } // Load Selected Filters const savedFilters = localStorage.getItem(STORAGE_KEYS.SELECTED_FILTERS); if (savedFilters) { try { const filters = JSON.parse(savedFilters); selectedActivityTypes = new Set(filters); } catch (e) { console.error('Failed to parse saved filters', e); } } } // Save settings to local storage function saveSettings(): void { if (ftpInput) localStorage.setItem(STORAGE_KEYS.FTP, ftpInput.value); if (thresholdHrInput) localStorage.setItem(STORAGE_KEYS.THRESHOLD_HR, thresholdHrInput.value); if (restingHrInput) localStorage.setItem(STORAGE_KEYS.RESTING_HR, restingHrInput.value); if (targetAcwrInput) localStorage.setItem(STORAGE_KEYS.TARGET_ACWR, targetAcwrInput.value); if (predictTodayInput) localStorage.setItem(STORAGE_KEYS.PREDICT_TODAY, String(predictTodayInput.checked)); localStorage.setItem(STORAGE_KEYS.SELECTED_FILTERS, JSON.stringify(Array.from(selectedActivityTypes))); } // Load CSV from local storage async function loadSavedCSV(): Promise { const savedCSV = localStorage.getItem(STORAGE_KEYS.CSV_DATA); if (!savedCSV) return; uploadStatus.textContent = 'Loading saved data...'; uploadStatus.className = ''; try { const ftp = parseInt(ftpInput.value) || 343; const thresholdHR = parseInt(thresholdHrInput.value) || 170; const restingHR = parseInt(restingHrInput.value) || 50; // Create a Blob from the saved CSV string const blob = new Blob([savedCSV], { type: 'text/csv' }); const file = new File([blob], 'saved_activities.csv', { type: 'text/csv' }); allActivities = await parseCSV(file, ftp, thresholdHR, restingHR); if (allActivities.length === 0) { throw new Error('No valid activities found in saved data'); } createActivityTypeFilters(allActivities); 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); filterSection.classList.remove('hidden'); chartsSection.classList.remove('hidden'); uploadStatus.textContent = `Successfully loaded ${allActivities.length} activities from saved data`; uploadStatus.className = 'success'; } catch (error) { console.error('Error loading saved data:', error); // Clear corrupted data localStorage.removeItem(STORAGE_KEYS.CSV_DATA); uploadStatus.textContent = ''; uploadStatus.className = ''; } } // Event listeners csvUpload?.addEventListener('change', handleFileUpload); targetAcwrInput?.addEventListener('input', handleTargetAcwrChange); predictTodayInput?.addEventListener('change', handlePredictTodayChange); ftpInput?.addEventListener('input', saveSettings); thresholdHrInput?.addEventListener('input', saveSettings); restingHrInput?.addEventListener('input', saveSettings); 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; // Save to local storage saveSettings(); // 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; // Save to local storage saveSettings(); // 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); } }); // Save to local storage saveSettings(); // 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; // Read file content to save it const csvContent = await file.text(); // 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'); } // Save CSV content and settings to local storage localStorage.setItem(STORAGE_KEYS.CSV_DATA, csvContent); saveSettings(); // 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: always go back 56 days (2 x 28 days chronic period) // This ensures we have enough historical data for accurate ACWR calculation const startDate = new Date(endDate); startDate.setDate(startDate.getDate() - 56); 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 loadSavedSettings(); loadSavedCSV(); console.log('Training Load Data Visualization initialized');