Spaces:
Running
Running
| 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<string> = 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<string>(); | |
| 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<HTMLInputElement>; | |
| 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<void> { | |
| 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'); | |