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 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); 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 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; renderCharts(filteredActivities); } 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 value from input const ftp = parseInt(ftpInput.value) || 343; // Parse CSV with user-provided FTP allActivities = await parseCSV(file, ftp); 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; renderCharts(filteredActivities); // 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[]): void { // Calculate date range from all activities for consistency let dateRange: { start: Date; end: Date } | undefined; if (allActivities.length > 0) { const sortedAll = [...allActivities].sort((a, b) => a.date.getTime() - b.date.getTime()); // Normalize to midnight to avoid timezone/time comparison issues const startDate = new Date(sortedAll[0].date); startDate.setHours(0, 0, 0, 0); const endDate = new Date(sortedAll[sortedAll.length - 1].date); endDate.setHours(23, 59, 59, 999); dateRange = { start: startDate, end: endDate, }; } // Calculate ACWR for each metric with consistent date range const distanceData = calculateMetricACWR( activities, (activity) => activity.distance, dateRange ); const durationData = calculateMetricACWR( activities, (activity) => activity.duration, dateRange ); const tssData = calculateMetricACWR( activities, (activity) => activity.trainingStressScore, dateRange ); const caloriesData = calculateMetricACWR( activities, (activity) => activity.calories, dateRange ); // Destroy existing charts destroyAllCharts(); // Create new charts createDistanceChart(distanceData); createDurationChart(durationData); createTSSChart(tssData); createCaloriesChart(caloriesData); } // Initialize console.log('Training Load Data Visualization initialized');