/** * Data Processor Module * Handles parsing and processing of CSV data for health metrics and workouts */ class DataProcessor { constructor() { this.metricsData = null; this.workoutsData = null; } /** * Parse CSV string into array of objects * @param {string} csvString - Raw CSV data as string * @returns {Array} - Array of objects with headers as keys */ parseCSV(csvString) { // Split the CSV string into lines const lines = csvString.split('\n'); // Extract headers (first line) and remove quotes and any extra characters const headers = lines[0].split(',').map(header => { // Clean up header - remove quotes, carriage returns, etc. return header.replace(/^"(.*)"$/, '$1').replace(/\r$/, ''); }); // Process each data line const results = []; for (let i = 1; i < lines.length; i++) { // Skip empty lines if (!lines[i].trim()) continue; // Split the line into values, handling quoted values with commas const values = this.splitCSVLine(lines[i]); // Create an object with headers as keys and values const entry = {}; headers.forEach((header, index) => { // Remove quotes if present and convert to appropriate type let value = values[index] ? values[index].replace(/^"(.*)"$/, '$1').replace(/\r$/, '') : ''; // Try to convert to number if it looks like one if (!isNaN(value) && value.trim() !== '') { value = parseFloat(value); } entry[header] = value; }); results.push(entry); } return results; } /** * Split CSV line handling quoted values with commas * @param {string} line - CSV line to split * @returns {Array} - Array of values */ splitCSVLine(line) { const values = []; let inQuotes = false; let currentValue = ''; for (let i = 0; i < line.length; i++) { const char = line[i]; if (char === '"') { inQuotes = !inQuotes; } else if (char === ',' && !inQuotes) { values.push(currentValue); currentValue = ''; } else { currentValue += char; } } // Add the last value values.push(currentValue); return values; } /** * Process metrics data from CSV * @param {string} csvString - Raw CSV data as string * @returns {Object} - Processed metrics data */ processMetricsData(csvString) { const rawData = this.parseCSV(csvString); // Log the first few entries to verify structure console.log('First few entries from metrics CSV:', rawData.slice(0, 5)); // Group metrics by date const metricsByDate = {}; rawData.forEach(entry => { // Extract date from timestamp (format: "2025-04-14 00:00:00") const date = entry.Timestamp.split(' ')[0]; if (!metricsByDate[date]) { metricsByDate[date] = {}; } // Add metric to the date group // Ensure Type is trimmed to handle any extra spaces const metricType = entry.Type.trim(); // Get the value from the correct column (might be '"Value"' instead of 'Value') let metricValue = null; if (entry.Value !== undefined) { metricValue = entry.Value; } else if (entry['"Value"'] !== undefined) { metricValue = entry['"Value"']; } // Convert Value to number if it's numeric if (metricValue !== null && !isNaN(metricValue) && metricValue.toString().trim() !== '') { metricValue = parseFloat(metricValue); } metricsByDate[date][metricType] = metricValue; }); // Log the metrics by date to verify structure console.log('Sample of metrics by date:', Object.keys(metricsByDate).slice(0, 3).map(date => ({ date, metrics: metricsByDate[date] }))); // Convert to array and sort by date const processedData = Object.keys(metricsByDate).map(date => { return { date, metrics: metricsByDate[date] }; }).sort((a, b) => new Date(a.date) - new Date(b.date)); this.metricsData = processedData; return processedData; } /** * Process workouts data from CSV * @param {string} csvString - Raw CSV data as string * @returns {Array} - Processed workouts data */ processWorkoutsData(csvString) { const rawData = this.parseCSV(csvString); // Process each workout entry const processedData = rawData.map(workout => { // Convert workout day to date object for easier comparison const workoutDate = new Date(workout.WorkoutDay); return { date: workout.WorkoutDay, type: workout.WorkoutType, title: workout.Title, distance: workout.DistanceInMeters, duration: workout.TimeTotalInHours, heartRate: { average: workout.HeartRateAverage, max: workout.HeartRateMax }, power: { average: workout.PowerAverage, max: workout.PowerMax }, energy: workout.Energy, zones: { heartRate: this.extractHeartRateZones(workout), power: this.extractPowerZones(workout) }, rpe: workout.Rpe, feeling: workout.Feeling, cadence: { average: workout.CadenceAverage, max: workout.CadenceMax }, velocity: { average: workout.VelocityAverage, max: workout.VelocityMax }, comments: { athlete: workout.AthleteComments, coach: workout.CoachComments } }; }).sort((a, b) => new Date(a.date) - new Date(b.date)); this.workoutsData = processedData; return processedData; } /** * Extract heart rate zone data from workout entry * @param {Object} workout - Workout data entry * @returns {Array} - Heart rate zone minutes */ extractHeartRateZones(workout) { const zones = []; for (let i = 1; i <= 10; i++) { const zoneKey = `HRZone${i}Minutes`; if (workout[zoneKey] !== undefined && workout[zoneKey] !== '') { zones.push({ zone: i, minutes: parseFloat(workout[zoneKey]) }); } } return zones; } /** * Extract power zone data from workout entry * @param {Object} workout - Workout data entry * @returns {Array} - Power zone minutes */ extractPowerZones(workout) { const zones = []; for (let i = 1; i <= 10; i++) { const zoneKey = `PWRZone${i}Minutes`; if (workout[zoneKey] !== undefined && workout[zoneKey] !== '') { zones.push({ zone: i, minutes: parseFloat(workout[zoneKey]) }); } } return zones; } /** * Load sample data from files * @returns {Promise} - Promise resolving when data is loaded */ async loadSampleData() { try { // Load metrics data using XMLHttpRequest to avoid CORS issues with file:// URLs const metricsCSV = await this.loadFileWithXHR(CONFIG.sampleData.metricsPath); this.processMetricsData(metricsCSV); // Load workouts data const workoutsCSV = await this.loadFileWithXHR(CONFIG.sampleData.workoutsPath); this.processWorkoutsData(workoutsCSV); return { metrics: this.metricsData, workouts: this.workoutsData }; } catch (error) { console.error('Error loading sample data:', error); throw error; } } /** * Load a file using XMLHttpRequest (works with file:// URLs) * @param {string} filePath - Path to the file * @returns {Promise} - Promise resolving with file contents */ loadFileWithXHR(filePath) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('GET', filePath, true); xhr.responseType = 'text'; xhr.onload = function() { if (xhr.status === 200 || (xhr.status === 0 && xhr.responseText)) { resolve(xhr.responseText); } else { reject(new Error(`Failed to load file: ${filePath}`)); } }; xhr.onerror = function() { reject(new Error(`Network error loading file: ${filePath}`)); }; xhr.send(); }); } /** * Process file data from file inputs * @param {File} metricsFile - Metrics CSV file * @param {File} workoutsFile - Workouts CSV file * @returns {Promise} - Promise resolving with processed data */ async processFiles(metricsFile, workoutsFile) { try { // Process metrics file const metricsData = await this.readFile(metricsFile); this.processMetricsData(metricsData); // Process workouts file const workoutsData = await this.readFile(workoutsFile); this.processWorkoutsData(workoutsData); return { metrics: this.metricsData, workouts: this.workoutsData }; } catch (error) { console.error('Error processing files:', error); throw error; } } /** * Read file as text * @param {File} file - File to read * @returns {Promise} - Promise resolving with file contents */ readFile(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (event) => { resolve(event.target.result); }; reader.onerror = (error) => { reject(error); }; reader.readAsText(file); }); } /** * Prepare data for OpenAI analysis * @returns {Object} - Data formatted for analysis */ prepareDataForAnalysis() { if (!this.metricsData || !this.workoutsData) { throw new Error('Data not loaded. Please load data first.'); } return { metrics: this.metricsData, workouts: this.workoutsData, summary: this.generateDataSummary() }; } /** * Generate summary statistics from the data * @returns {Object} - Summary statistics */ generateDataSummary() { if (!this.metricsData || !this.workoutsData) { return null; } // Log all available metric types from the first few days to help diagnose issues console.log('Available metric types in first few days:'); this.metricsData.slice(0, 3).forEach(day => { console.log(`Day ${day.date} metrics:`, Object.keys(day.metrics)); }); // Find sleep hours data with case-insensitive matching const sleepData = []; const sleepHoursKey = 'Sleep Hours'; // The expected key this.metricsData.forEach(day => { // Try to find the sleep hours key (case-insensitive) let foundSleepHours = false; // First try the exact key if (day.metrics[sleepHoursKey] !== undefined) { sleepData.push(parseFloat(day.metrics[sleepHoursKey])); foundSleepHours = true; } else { // If not found, try case-insensitive search for (const key in day.metrics) { if (key.toLowerCase() === sleepHoursKey.toLowerCase()) { sleepData.push(parseFloat(day.metrics[key])); foundSleepHours = true; console.log(`Found sleep hours with different case: "${key}"`); break; } } } if (!foundSleepHours) { console.log(`No sleep hours found for day ${day.date}`); } }); console.log(`Found ${sleepData.length} days with sleep data out of ${this.metricsData.length} total days`); const sleepStats = sleepData.length > 0 ? { average: this.calculateAverage(sleepData), min: Math.min(...sleepData), max: Math.max(...sleepData) } : null; console.log('Sleep stats:', sleepStats); // Calculate stress statistics const stressQualifiers = this.metricsData .filter(day => day.metrics['Stress Qualifier'] !== undefined) .map(day => day.metrics['Stress Qualifier']); const stressQualifierCounts = {}; stressQualifiers.forEach(qualifier => { stressQualifierCounts[qualifier] = (stressQualifierCounts[qualifier] || 0) + 1; }); // Calculate workout statistics const workoutDistances = this.workoutsData .map(workout => parseFloat(workout.distance)); const workoutStats = workoutDistances.length > 0 ? { count: this.workoutsData.length, totalDistance: this.calculateSum(workoutDistances), averageDistance: this.calculateAverage(workoutDistances), averageHeartRate: this.calculateAverage( this.workoutsData.map(w => parseFloat(w.heartRate.average)) ) } : null; return { dateRange: { start: this.metricsData[0]?.date, end: this.metricsData[this.metricsData.length - 1]?.date }, sleep: sleepStats, stress: { qualifiers: stressQualifierCounts }, workouts: workoutStats }; } /** * Calculate average of array values * @param {Array} values - Array of numbers * @returns {number} - Average value */ calculateAverage(values) { if (!values || values.length === 0) return 0; return values.reduce((sum, val) => sum + val, 0) / values.length; } /** * Calculate sum of array values * @param {Array} values - Array of numbers * @returns {number} - Sum of values */ calculateSum(values) { if (!values || values.length === 0) return 0; return values.reduce((sum, val) => sum + val, 0); } } // Create a global instance const dataProcessor = new DataProcessor();