Statistics Overview
The statistics page (estadisticas.html) displays comprehensive workout data and progress visualization.
Workouts
Total completed routines
Streak
Consecutive training days
Time
Total hours trained
Exercises
Total exercises completed
Statistics Page Structure
The page is organized into several key sections:Hero Section
Introduces the statistics dashboard with motivational messaging:frontend/pages/estadisticas.html
<section class="hero">
<div class="hero-container">
<div class="hero-content">
<div class="hero-badge">
<i class="fas fa-chart-line"></i>
<span>Tu Progreso Fitness</span>
</div>
<h1>TUS <span class="accent">ESTADÍSTICAS</span></h1>
<p class="hero-subtitle">
Seguimiento completo de tu progreso, logros desbloqueados y evolución fitness.
¡Cada día es un paso más hacia tu mejor versión! 💪
</p>
</div>
</div>
</section>
Statistics Cards
Display key performance metrics at a glance:<section class="stats-section">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-dumbbell"></i>
</div>
<div class="stat-info">
<h3>Rutinas Completadas</h3>
<div class="stat-value" id="totalWorkouts">0</div>
<div class="stat-label" id="weeklyProgress">Esta semana: 0</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-fire"></i>
</div>
<div class="stat-info">
<h3>Racha Actual</h3>
<div class="stat-value" id="currentStreak">0</div>
<div class="stat-label">días consecutivos 🔥</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-clock"></i>
</div>
<div class="stat-info">
<h3>Tiempo Total</h3>
<div class="stat-value" id="totalTime">0</div>
<div class="stat-label">horas de entrenamiento</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-chart-line"></i>
</div>
<div class="stat-info">
<h3>Ejercicios Completados</h3>
<div class="stat-value" id="totalExercises">0</div>
<div class="stat-label">ejercicios realizados</div>
</div>
</div>
</div>
</section>
Loading Statistics Data
Statistics are fetched from the backend API when the page loads:async function cargarEstadisticas() {
const userId = localStorage.getItem('userId');
const token = localStorage.getItem('token');
if (!userId || !token) {
window.location.href = 'login.html';
return;
}
try {
const response = await fetch(`${CONFIG.API_URL}/api/estadisticas/${userId}`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (data.success) {
actualizarEstadisticasUI(data.estadisticas);
cargarGraficos(data.graficos);
}
} catch (error) {
console.error('Error cargando estadísticas:', error);
}
}
function actualizarEstadisticasUI(stats) {
document.getElementById('totalWorkouts').textContent = stats.totalWorkouts || 0;
document.getElementById('currentStreak').textContent = stats.currentStreak || 0;
document.getElementById('totalTime').textContent =
(stats.totalMinutes ? (stats.totalMinutes / 60).toFixed(1) : 0);
document.getElementById('totalExercises').textContent = stats.totalExercises || 0;
document.getElementById('weeklyProgress').textContent =
`Esta semana: ${stats.weeklyWorkouts || 0}`;
}
// Initialize on page load
cargarEstadisticas();
Monthly Summary
The monthly summary provides a calendar view of completed workouts:function generarResumenMensual(workouts) {
const resumenContainer = document.getElementById('resumenMensual');
const hoy = new Date();
const mesActual = hoy.getMonth();
const añoActual = hoy.getFullYear();
// Get workouts for current month
const workoutsDelMes = workouts.filter(w => {
const fecha = new Date(w.fecha);
return fecha.getMonth() === mesActual && fecha.getFullYear() === añoActual;
});
// Group by date
const workoutsPorDia = {};
workoutsDelMes.forEach(w => {
const fecha = new Date(w.fecha).toDateString();
if (!workoutsPorDia[fecha]) {
workoutsPorDia[fecha] = [];
}
workoutsPorDia[fecha].push(w);
});
// Generate calendar HTML
let html = `
<h3><i class="fas fa-calendar"></i> Resumen de ${getNombreMes(mesActual)}</h3>
<div class="calendar-grid">
`;
// Days of the month
const diasEnMes = new Date(añoActual, mesActual + 1, 0).getDate();
for (let dia = 1; dia <= diasEnMes; dia++) {
const fecha = new Date(añoActual, mesActual, dia);
const fechaStr = fecha.toDateString();
const tieneWorkout = workoutsPorDia[fechaStr];
const esHoy = dia === hoy.getDate();
html += `
<div class="calendar-day ${tieneWorkout ? 'has-workout' : ''} ${esHoy ? 'today' : ''}">
<span class="day-number">${dia}</span>
${tieneWorkout ? '<i class="fas fa-check"></i>' : ''}
</div>
`;
}
html += `</div>`;
resumenContainer.innerHTML = html;
}
Achievement System
Users can unlock achievements based on their progress:const LOGROS = [
{
id: 'primer-entreno',
nombre: 'Primer Entreno',
descripcion: 'Completa tu primera rutina',
icono: '💪',
condicion: (stats) => stats.totalWorkouts >= 1
},
{
id: 'racha-7',
nombre: '7 Días Seguidos',
descripcion: 'Entrena 7 días consecutivos',
icono: '🔥',
condicion: (stats) => stats.currentStreak >= 7
},
{
id: 'racha-30',
nombre: '30 Días Seguidos',
descripcion: 'Entrena 30 días consecutivos',
icono: '🏆',
condicion: (stats) => stats.currentStreak >= 30
},
{
id: 'workouts-10',
nombre: '10 Rutinas',
descripcion: 'Completa 10 rutinas de entrenamiento',
icono: '🎯',
condicion: (stats) => stats.totalWorkouts >= 10
},
{
id: 'workouts-50',
nombre: '50 Rutinas',
descripcion: 'Completa 50 rutinas de entrenamiento',
icono: '⭐',
condicion: (stats) => stats.totalWorkouts >= 50
},
{
id: 'tiempo-10h',
nombre: '10 Horas',
descripcion: 'Acumula 10 horas de entrenamiento',
icono: '⏱️',
condicion: (stats) => stats.totalMinutes >= 600
}
];
function mostrarLogros(stats) {
const grid = document.getElementById('achievementsGrid');
grid.innerHTML = '';
LOGROS.forEach(logro => {
const desbloqueado = logro.condicion(stats);
const card = document.createElement('div');
card.className = `achievement-card ${desbloqueado ? 'unlocked' : 'locked'}`;
card.innerHTML = `
<div class="achievement-icon">${logro.icono}</div>
<h4 class="achievement-name">${logro.nombre}</h4>
<p class="achievement-desc">${logro.descripcion}</p>
${desbloqueado ? '<span class="unlocked-badge">✓ Desbloqueado</span>' : ''}
`;
grid.appendChild(card);
});
}
New achievements are automatically unlocked when users meet the conditions during regular API syncs.
Progress Charts
The statistics page includes four interactive Chart.js visualizations:Weekly Activity Chart
Bar chart showing workouts completed each day of the week:function crearGraficoSemanal(datos) {
const ctx = document.getElementById('weeklyChart').getContext('2d');
new Chart(ctx, {
type: 'bar',
data: {
labels: ['Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb', 'Dom'],
datasets: [{
label: 'Entrenamientos',
data: datos.porDia,
backgroundColor: 'rgba(255, 0, 0, 0.8)',
borderColor: 'rgba(255, 0, 0, 1)',
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
ticks: {
stepSize: 1,
color: '#888'
},
grid: {
color: 'rgba(255, 255, 255, 0.1)'
}
},
x: {
ticks: {
color: '#888'
},
grid: {
display: false
}
}
},
plugins: {
legend: {
labels: {
color: '#f0f0f0'
}
}
}
}
});
}
Monthly Progress Chart
Line chart tracking workouts over the past 6 months:function crearGraficoMensual(datos) {
const ctx = document.getElementById('monthlyChart').getContext('2d');
new Chart(ctx, {
type: 'line',
data: {
labels: datos.meses,
datasets: [{
label: 'Rutinas Completadas',
data: datos.counts,
borderColor: 'rgba(255, 0, 0, 1)',
backgroundColor: 'rgba(255, 0, 0, 0.1)',
tension: 0.4,
fill: true,
pointBackgroundColor: 'rgba(255, 0, 0, 1)',
pointRadius: 5
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
ticks: {
stepSize: 5,
color: '#888'
},
grid: {
color: 'rgba(255, 255, 255, 0.1)'
}
},
x: {
ticks: {
color: '#888'
},
grid: {
display: false
}
}
},
plugins: {
legend: {
labels: {
color: '#f0f0f0'
}
}
}
}
});
}
Workout Distribution Chart
Doughnut chart showing distribution by workout focus area:function crearGraficoDistribucion(datos) {
const ctx = document.getElementById('focusChart').getContext('2d');
new Chart(ctx, {
type: 'doughnut',
data: {
labels: datos.categorias,
datasets: [{
data: datos.valores,
backgroundColor: [
'rgba(255, 0, 0, 0.8)',
'rgba(255, 80, 0, 0.8)',
'rgba(255, 160, 0, 0.8)',
'rgba(200, 0, 0, 0.8)'
],
borderWidth: 2,
borderColor: '#0a0a0a'
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
labels: {
color: '#f0f0f0',
padding: 15
}
}
}
}
});
}
Training Time Chart
Bar chart showing preferred training times:function crearGraficoHorarios(datos) {
const ctx = document.getElementById('timeChart').getContext('2d');
new Chart(ctx, {
type: 'bar',
data: {
labels: ['Mañana', 'Mediodía', 'Tarde', 'Noche'],
datasets: [{
label: 'Entrenamientos',
data: datos.porHorario,
backgroundColor: [
'rgba(255, 200, 0, 0.8)',
'rgba(255, 100, 0, 0.8)',
'rgba(255, 50, 0, 0.8)',
'rgba(150, 0, 150, 0.8)'
],
borderColor: 'rgba(255, 255, 255, 0.2)',
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
indexAxis: 'y',
scales: {
x: {
beginAtZero: true,
ticks: {
color: '#888'
},
grid: {
color: 'rgba(255, 255, 255, 0.1)'
}
},
y: {
ticks: {
color: '#888'
},
grid: {
display: false
}
}
},
plugins: {
legend: {
display: false
}
}
}
});
}
Monthly Goals Progress
Progress bars show advancement toward monthly goals:<div class="progress-bars-grid">
<div class="progress-item">
<div class="progress-header">
<div class="progress-title">
<i class="fas fa-calendar-check"></i>
<span>Rutinas Mensuales</span>
</div>
<span class="progress-value" id="monthlyGoalProgress">0/12</span>
</div>
<div class="progress-bar-bg">
<div class="progress-bar-fill" id="monthlyGoalBar" style="width: 0%"></div>
</div>
</div>
<div class="progress-item">
<div class="progress-header">
<div class="progress-title">
<i class="fas fa-clock"></i>
<span>Horas de Entrenamiento</span>
</div>
<span class="progress-value" id="hoursGoalProgress">0/10</span>
</div>
<div class="progress-bar-bg">
<div class="progress-bar-fill" id="hoursGoalBar" style="width: 0%"></div>
</div>
</div>
<div class="progress-item">
<div class="progress-header">
<div class="progress-title">
<i class="fas fa-fire"></i>
<span>Racha Máxima</span>
</div>
<span class="progress-value" id="streakGoalProgress">0/7</span>
</div>
<div class="progress-bar-bg">
<div class="progress-bar-fill" id="streakGoalBar" style="width: 0%"></div>
</div>
</div>
</div>
<script>
function actualizarMetas(stats) {
// Monthly workouts goal (12 per month = 3 per week)
const metaMensual = 12;
const progresoMensual = Math.min(stats.monthlyWorkouts, metaMensual);
const porcentajeMensual = (progresoMensual / metaMensual) * 100;
document.getElementById('monthlyGoalProgress').textContent =
`${progresoMensual}/${metaMensual}`;
document.getElementById('monthlyGoalBar').style.width = `${porcentajeMensual}%`;
// Hours goal (10 hours per month)
const metaHoras = 10;
const horasActuales = (stats.monthlyMinutes || 0) / 60;
const progresoHoras = Math.min(horasActuales, metaHoras);
const porcentajeHoras = (progresoHoras / metaHoras) * 100;
document.getElementById('hoursGoalProgress').textContent =
`${progresoHoras.toFixed(1)}/${metaHoras}`;
document.getElementById('hoursGoalBar').style.width = `${porcentajeHoras}%`;
// Streak goal (7 days)
const metaRacha = 7;
const progresoRacha = Math.min(stats.currentStreak, metaRacha);
const porcentajeRacha = (progresoRacha / metaRacha) * 100;
document.getElementById('streakGoalProgress').textContent =
`${progresoRacha}/${metaRacha}`;
document.getElementById('streakGoalBar').style.width = `${porcentajeRacha}%`;
}
</script>
Backend Chart Data Processing
The backend processes workout data to generate chart-ready datasets:backend/src/controllers/estadisticas_c.js
// GET /api/estadisticas/graficos/:userId
const getGraficos = async (req, res) => {
const { userId } = req.params;
if (req.user._id.toString() !== userId) {
throw new AppError('No autorizado', 403);
}
const workouts = await WorkoutProgress.find({ userId })
.sort({ fecha: -1 });
// Weekly activity data
const porDia = [0, 0, 0, 0, 0, 0, 0];
const oneWeekAgo = new Date();
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
workouts.forEach(w => {
if (w.fecha >= oneWeekAgo) {
const dia = new Date(w.fecha).getDay();
porDia[dia === 0 ? 6 : dia - 1]++;
}
});
// Monthly progress data
const meses = [];
const counts = [];
for (let i = 5; i >= 0; i--) {
const fecha = new Date();
fecha.setMonth(fecha.getMonth() - i);
meses.push(getNombreMes(fecha.getMonth()));
const count = workouts.filter(w => {
const wDate = new Date(w.fecha);
return wDate.getMonth() === fecha.getMonth() &&
wDate.getFullYear() === fecha.getFullYear();
}).length;
counts.push(count);
}
res.status(200).json({
success: true,
graficos: {
porDia,
meses,
counts
}
});
};
Real-time Updates
Statistics automatically update when users complete workouts:function actualizarEstadisticasEnTiempoReal(nuevoWorkout) {
// Increment total workouts
const totalEl = document.getElementById('totalWorkouts');
const currentTotal = parseInt(totalEl.textContent);
totalEl.textContent = currentTotal + 1;
// Update total time
const timeEl = document.getElementById('totalTime');
const currentTime = parseFloat(timeEl.textContent);
const newTime = currentTime + (nuevoWorkout.duracion / 60);
timeEl.textContent = newTime.toFixed(1);
// Update exercises
const exercisesEl = document.getElementById('totalExercises');
const currentExercises = parseInt(exercisesEl.textContent);
exercisesEl.textContent = currentExercises + nuevoWorkout.ejerciciosCompletados.length;
// Potentially update streak
verificarYActualizarRacha();
// Show celebration animation
mostrarAnimacionCompletado();
}
The statistics system uses Chart.js for responsive, interactive visualizations that work on all devices.
API Endpoints
| Endpoint | Method | Description | Auth Required |
|---|---|---|---|
/api/estadisticas/:userId | GET | Get comprehensive user statistics | Yes |
/api/estadisticas/graficos/:userId | GET | Get chart data for visualizations | Yes |
/api/entrenamientos/historial/:userId | GET | Get complete workout history | Yes |
Data Persistence
Statistics are calculated from the WorkoutProgress collection:WorkoutProgress Model
const workoutProgressSchema = new mongoose.Schema({
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
rutinaId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Rutina'
},
diaIndex: Number,
ejerciciosCompletados: [{
nombre: String,
series: Number,
completado: Boolean
}],
duracion: Number,
caloriasQuemadas: Number,
notas: String,
fecha: {
type: Date,
default: Date.now
},
completado: {
type: Boolean,
default: true
}
});
const WorkoutProgress = mongoose.model('WorkoutProgress', workoutProgressSchema);
All statistics are calculated server-side to ensure accuracy and prevent client-side manipulation.