glutamatt's picture
glutamatt HF Staff
calories
0f92957 verified
raw
history blame
7.34 kB
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');