Skip to main content

Workout Analytics Service

The Workout Analytics Service provides comprehensive performance analysis and insights for OpenLift users, processing workout data to generate meaningful metrics, trends, and performance insights.

Overview​

The Workout Analytics Service handles:

  • Workout performance metrics calculation and analysis
  • Long-term progress trend analysis and visualization
  • Volume, intensity, and frequency tracking
  • Comparative performance analysis (personal bests, historical comparisons)
  • Advanced analytics including periodization analysis
  • Data-driven insights for training optimization

Key Features​

πŸ“Š Comprehensive Performance Metrics​

  • Volume Tracking: Total volume, volume per muscle group, volume progression
  • Intensity Analysis: Average RPE, intensity distribution, RPE trends
  • Frequency Monitoring: Training frequency per muscle group and exercise type
  • Density Calculations: Work-to-rest ratios, training density optimization

πŸ“ˆ Trend Analysis & Visualization​

  • Progress Tracking: Long-term strength and performance trends
  • Periodization Analysis: Training cycle effectiveness and adaptation
  • Plateau Detection: Identify stagnation points and breakthrough opportunities
  • Comparative Analysis: Performance against personal records and goals

🎯 Personalized Insights​

  • Goal-Oriented Metrics: Track progress toward specific fitness goals
  • Weakness Identification: Highlight lagging muscle groups or movement patterns
  • Training Load Management: Balance training stress and recovery
  • Performance Predictions: Forecast potential improvements and timelines

Architecture​

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Client App β”‚ β”‚ Workout β”‚ β”‚ Database β”‚
β”‚ β”‚ β”‚ Analytics β”‚ β”‚ β”‚
β”‚ β€’ Charts/Graphs │◄───│ β€’ Metric Calc │◄───│ β€’ Workout Data β”‚
β”‚ β€’ Progress View β”‚ β”‚ β€’ Trend Analysisβ”‚ β”‚ β€’ Calculated β”‚
β”‚ β€’ Insights β”‚ β”‚ β€’ Data Proc β”‚ β”‚ Metrics β”‚
β”‚ β€’ Comparisons β”‚ β”‚ β€’ Insights Gen β”‚ β”‚ β€’ Aggregations β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Service Responsibilities​

βœ… Workout Analytics Service Handles​

  • Workout data processing and metrics calculation
  • Performance trend analysis and statistical calculations
  • Data aggregation and summarization
  • Progress visualization data preparation
  • Comparative analysis (personal bests, goal tracking)
  • Training load and recovery analysis
  • Insight generation and interpretation

❌ Workout Analytics Service Does NOT Handle​

  • Raw workout data collection (handled by Workout History Service)
  • Workout planning or program creation
  • Real-time workout guidance or coaching
  • Social features or comparison with other users
  • Nutrition tracking or recovery recommendations

Core Data Models​

Workout Metrics​

interface WorkoutMetrics {
workoutId: string;
userId: string;
date: Date;

// Volume metrics
totalVolume: number; // weight Γ— reps sum
volumeByMuscleGroup: Record<string, number>;
volumeByExercise: Record<string, number>;

// Intensity metrics
averageRpe: number;
maxRpe: number;
rpeDistribution: Record<number, number>; // RPE value β†’ count
intensityScore: number; // Weighted intensity calculation

// Frequency and density
totalSets: number;
totalReps: number;
exerciseCount: number;
muscleGroupCount: number;

// Time metrics
totalDuration: number; // minutes
workingTime: number; // time actually lifting
restTime: number;
trainingDensity: number; // work-to-rest ratio

// Performance indicators
personalRecords: PersonalRecord[];
volumeRecords: VolumeRecord[];
completionRate: number; // percentage of planned work completed
}
interface ProgressTrend {
userId: string;
exerciseId: string;
period: 'weekly' | 'monthly' | 'quarterly';

// Trend data points
dataPoints: TrendDataPoint[];

// Statistical analysis
trend: 'increasing' | 'decreasing' | 'stable' | 'volatile';
trendStrength: number; // 0-1, confidence in trend direction
correlationCoefficient: number;

// Progress metrics
totalImprovement: number; // % improvement over period
averageSessionImprovement: number;
plateauPeriods: PlateauPeriod[];

// Forecasting
projectedImprovement: number; // Next period prediction
confidenceInterval: [number, number];

calculatedAt: Date;
validUntil: Date;
}

interface TrendDataPoint {
date: Date;
value: number; // weight, volume, reps, etc.
sessionCount: number; // number of sessions contributing to this point
confidence: number; // 0-1, confidence in this data point
}

Performance Insights​

interface PerformanceInsight {
id: string;
userId: string;
type: 'strength_gain' | 'plateau_identified' | 'imbalance_detected' | 'goal_progress';

// Insight content
title: string;
description: string;
recommendation?: string;
priority: 'low' | 'medium' | 'high';

// Supporting data
dataVisualization?: {
chartType: 'line' | 'bar' | 'pie' | 'scatter';
data: any[];
config: any;
};

// Metrics
confidenceScore: number; // 0-1
impactScore: number; // Expected impact on user's goals

// Metadata
generatedAt: Date;
expiresAt?: Date;
viewedBy: boolean;
actionTaken?: string;
}

Key Operations​

Get Workout Analytics Dashboard​

query GetWorkoutAnalytics(
$userId: ID!,
$timeframe: TimeframeInput!,
$metrics: [AnalyticsMetricType!]
) {
workoutAnalytics(
userId: $userId,
timeframe: $timeframe,
metrics: $metrics
) {
summary {
totalWorkouts
totalVolume
averageRpe
trainingFrequency
}

trends {
volumeProgression {
dataPoints {
date
value
}
trend
improvement
}

strengthProgression {
exercise
dataPoints {
date
maxWeight
}
personalRecords
}
}

muscleGroupAnalysis {
muscleGroup
totalVolume
frequency
lastTrained
recommendedAction
}

insights {
id
type
title
description
priority
confidenceScore
}
}
}

Get Exercise Performance Analysis​

query GetExerciseAnalytics(
$userId: ID!,
$exerciseId: ID!,
$weeks: Int = 12
) {
exerciseAnalytics(
userId: $userId,
exerciseId: $exerciseId,
weeks: $weeks
) {
exercise {
id
name
}

performanceMetrics {
currentMax: maxWeight
volumeTrend {
dataPoints {
date
volume
}
improvement
}

strengthTrend {
dataPoints {
date
maxWeight
}
improvement
}

rpeProgression {
dataPoints {
date
averageRpe
}
}
}

personalRecords {
type
value
date
isRecent
}

insights {
plateauDetected
recommendedProgression
consistencyScore
}
}
}

Generate Performance Report​

query GeneratePerformanceReport(
$userId: ID!,
$reportType: ReportType!,
$period: ReportPeriod!
) {
performanceReport(
userId: $userId,
reportType: $reportType,
period: $period
) {
reportId
generatedAt
period

summary {
totalWorkouts
volumeImprovement
strengthGains
consistencyScore
}

highlights {
achievements
personalRecords
milestones
}

areas_for_improvement {
laggingMuscleGroups
inconsistentExercises
plateauedMovements
}

recommendations {
training_adjustments
focus_areas
next_goals
}
}
}

Integration Patterns​

Flutter Analytics Dashboard​

class WorkoutAnalyticsService {
final GraphQLClient _client;

// Get comprehensive analytics dashboard data
Future<WorkoutAnalyticsDashboard> getDashboard({
required String userId,
TimeframeInput timeframe = const TimeframeInput.lastMonth(),
}) async {
const query = '''
query GetWorkoutAnalytics(
\$userId: ID!,
\$timeframe: TimeframeInput!
) {
workoutAnalytics(userId: \$userId, timeframe: \$timeframe) {
summary {
totalWorkouts
totalVolume
averageRpe
trainingFrequency
}
trends {
volumeProgression {
dataPoints {
date
value
}
trend
improvement
}
strengthProgression {
exercise
dataPoints {
date
maxWeight
}
}
}
muscleGroupAnalysis {
muscleGroup
totalVolume
frequency
recommendedAction
}
insights {
id
title
description
priority
confidenceScore
}
}
}
''';

final result = await _client.query(QueryOptions(
document: gql(query),
variables: {
'userId': userId,
'timeframe': timeframe.toJson(),
},
cachePolicy: CachePolicy.cacheFirst,
));

return WorkoutAnalyticsDashboard.fromJson(result.data!['workoutAnalytics']);
}

// Get specific exercise performance analysis
Future<ExerciseAnalytics> getExerciseAnalytics({
required String userId,
required String exerciseId,
int weeks = 12,
}) async {
const query = '''
query GetExerciseAnalytics(
\$userId: ID!,
\$exerciseId: ID!,
\$weeks: Int
) {
exerciseAnalytics(
userId: \$userId,
exerciseId: \$exerciseId,
weeks: \$weeks
) {
performanceMetrics {
currentMax
volumeTrend {
dataPoints {
date
volume
}
improvement
}
strengthTrend {
dataPoints {
date
maxWeight
}
improvement
}
}
personalRecords {
type
value
date
}
insights {
plateauDetected
consistencyScore
}
}
}
''';

final result = await _client.query(QueryOptions(
document: gql(query),
variables: {
'userId': userId,
'exerciseId': exerciseId,
'weeks': weeks,
},
));

return ExerciseAnalytics.fromJson(result.data!['exerciseAnalytics']);
}
}

Analytics Chart Components​

class ProgressChart extends StatelessWidget {
final List<TrendDataPoint> dataPoints;
final String title;
final String unit;

@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 16),

// Chart widget (using fl_chart or similar)
SizedBox(
height: 200,
child: LineChart(
LineChartData(
lineBarsData: [
LineChartBarData(
spots: dataPoints
.asMap()
.entries
.map((entry) => FlSpot(
entry.key.toDouble(),
entry.value.value,
))
.toList(),
isCurved: true,
colors: [Theme.of(context).primaryColor],
barWidth: 2,
),
],
titlesData: FlTitlesData(
bottomTitles: SideTitles(
showTitles: true,
getTitles: (value) {
if (value.toInt() < dataPoints.length) {
final date = dataPoints[value.toInt()].date;
return DateFormat('MM/dd').format(date);
}
return '';
},
),
leftTitles: SideTitles(
showTitles: true,
getTitles: (value) => '${value.toInt()}$unit',
),
),
),
),
),

// Progress indicator
const SizedBox(height: 8),
Row(
children: [
Icon(
dataPoints.isNotEmpty &&
dataPoints.last.value > dataPoints.first.value
? Icons.trending_up
: Icons.trending_down,
color: dataPoints.isNotEmpty &&
dataPoints.last.value > dataPoints.first.value
? Colors.green
: Colors.red,
),
const SizedBox(width: 4),
Text(
_calculateImprovement(),
style: TextStyle(
color: dataPoints.isNotEmpty &&
dataPoints.last.value > dataPoints.first.value
? Colors.green
: Colors.red,
),
),
],
),
],
),
),
);
}

String _calculateImprovement() {
if (dataPoints.length < 2) return 'Insufficient data';

final improvement = ((dataPoints.last.value - dataPoints.first.value) /
dataPoints.first.value * 100);

return '${improvement.toStringAsFixed(1)}% ${improvement >= 0 ? 'improvement' : 'decline'}';
}
}

Analytics Calculations​

Volume Metrics Calculation​

class VolumeCalculator {
calculateWorkoutVolume(workout: WorkoutHistoryEntry): VolumeMetrics {
let totalVolume = 0;
const volumeByMuscleGroup: Record<string, number> = {};
const volumeByExercise: Record<string, number> = {};

workout.completedTimeline.forEach(timelineItem => {
timelineItem.exercises.forEach(exercise => {
let exerciseVolume = 0;

exercise.sets.forEach(set => {
if (!set.skipped) {
const setVolume = set.weight * set.reps;
exerciseVolume += setVolume;
totalVolume += setVolume;
}
});

volumeByExercise[exercise.exerciseId] = exerciseVolume;

// Aggregate by muscle groups
const muscleGroups = this.getMuscleGroupsForExercise(exercise.exerciseId);
muscleGroups.forEach(muscleGroup => {
volumeByMuscleGroup[muscleGroup] =
(volumeByMuscleGroup[muscleGroup] || 0) + exerciseVolume;
});
});
});

return {
totalVolume,
volumeByMuscleGroup,
volumeByExercise,
calculatedAt: new Date(),
};
}
}

Trend Analysis​

class TrendAnalyzer {
analyzeTrend(dataPoints: TrendDataPoint[]): TrendAnalysis {
if (dataPoints.length < 3) {
return {
trend: 'insufficient_data',
trendStrength: 0,
correlationCoefficient: 0,
};
}

// Calculate linear regression
const regression = this.calculateLinearRegression(dataPoints);

// Determine trend direction
let trend: 'increasing' | 'decreasing' | 'stable';
if (regression.slope > 0.1) trend = 'increasing';
else if (regression.slope < -0.1) trend = 'decreasing';
else trend = 'stable';

// Calculate trend strength based on R-squared
const trendStrength = Math.abs(regression.rSquared);

return {
trend,
trendStrength,
correlationCoefficient: regression.rSquared,
slope: regression.slope,
intercept: regression.intercept,
};
}

private calculateLinearRegression(
dataPoints: TrendDataPoint[]
): RegressionResult {
const n = dataPoints.length;
const x = dataPoints.map((_, index) => index);
const y = dataPoints.map(point => point.value);

const sumX = x.reduce((sum, val) => sum + val, 0);
const sumY = y.reduce((sum, val) => sum + val, 0);
const sumXY = x.reduce((sum, val, i) => sum + val * y[i], 0);
const sumXX = x.reduce((sum, val) => sum + val * val, 0);
const sumYY = y.reduce((sum, val) => sum + val * val, 0);

const slope = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX);
const intercept = (sumY - slope * sumX) / n;

// Calculate R-squared
const yMean = sumY / n;
const ssTotal = y.reduce((sum, val) => sum + Math.pow(val - yMean, 2), 0);
const ssRes = y.reduce((sum, val, i) =>
sum + Math.pow(val - (slope * x[i] + intercept), 2), 0);
const rSquared = 1 - (ssRes / ssTotal);

return { slope, intercept, rSquared };
}
}

Insight Generation​

Performance Insights Engine​

class InsightGenerator {
generateInsights(
analytics: WorkoutAnalytics,
userGoals: UserGoals
): PerformanceInsight[] {
const insights: PerformanceInsight[] = [];

// Check for strength gains
const strengthInsights = this.analyzeStrengthGains(analytics);
insights.push(...strengthInsights);

// Check for muscle group imbalances
const imbalanceInsights = this.analyzeMuscleGroupBalance(analytics);
insights.push(...imbalanceInsights);

// Check goal progress
const goalInsights = this.analyzeGoalProgress(analytics, userGoals);
insights.push(...goalInsights);

// Check for plateaus
const plateauInsights = this.analyzePlateaus(analytics);
insights.push(...plateauInsights);

// Sort by priority and confidence
return insights.sort((a, b) =>
(b.impactScore * b.confidenceScore) -
(a.impactScore * a.confidenceScore)
);
}

private analyzeStrengthGains(analytics: WorkoutAnalytics): PerformanceInsight[] {
const insights: PerformanceInsight[] = [];

analytics.exerciseAnalytics.forEach(exercise => {
const improvementRate = exercise.strengthTrend.improvement;

if (improvementRate > 0.15) { // 15% improvement
insights.push({
type: 'strength_gain',
title: `Strong Progress on ${exercise.exerciseName}`,
description: `You've improved by ${(improvementRate * 100).toFixed(1)}% over the last month`,
recommendation: 'Keep up the great work! Consider gradually increasing volume or intensity.',
priority: 'high',
confidenceScore: exercise.strengthTrend.trendStrength,
impactScore: this.calculateImpactScore(exercise.exerciseId, userGoals),
generatedAt: new Date(),
});
}
});

return insights;
}
}

Event System Integration​

Analytics Events​

interface AnalyticsEvents {
'analytics.metrics_calculated': {
userId: string;
workoutId: string;
metrics: WorkoutMetrics;
};

'analytics.insight_generated': {
userId: string;
insightType: string;
priority: string;
confidenceScore: number;
};

'analytics.plateau_detected': {
userId: string;
exerciseId: string;
plateauWeeks: number;
previousBest: number;
};

'analytics.personal_record': {
userId: string;
exerciseId: string;
recordType: string;
newValue: number;
previousValue?: number;
};
}

Performance Optimization​

Data Aggregation Strategy​

class AnalyticsDataManager {
// Pre-calculate and cache common analytics queries
Future<void> preComputeAnalytics(String userId) async {
final now = DateTime.now();

// Pre-compute popular timeframes
await _computeAndCacheAnalytics(
userId,
TimeframeInput.lastWeek(),
'last_week',
);

await _computeAndCacheAnalytics(
userId,
TimeframeInput.lastMonth(),
'last_month',
);

await _computeAndCacheAnalytics(
userId,
TimeframeInput.lastQuarter(),
'last_quarter',
);
}

// Incremental updates for real-time analytics
Future<void> updateAnalyticsForNewWorkout(
String userId,
String workoutId,
) async {
// Only recalculate affected metrics
final workout = await workoutService.getWorkout(workoutId);
final newMetrics = await metricsCalculator.calculateMetrics(workout);

// Update cached aggregations incrementally
await _updateCachedMetrics(userId, newMetrics);

// Trigger insight regeneration if needed
await _triggerInsightUpdate(userId);
}
}

Dependencies​

  • Workout History Service: Source workout data for analysis
  • Exercise Service: Exercise metadata for analysis context
  • User Management Service: User goals and preferences for insights

Consumers​

  • Coaching Service: Analytics-driven coaching recommendations
  • Progression Playbook Service: Performance-based progression rules
  • Effective Workout Service: Analytics for workout effectiveness
  • Recovery Service: Training load analysis for recovery recommendations

API Documentation​

For detailed GraphQL schema and usage examples, see:

  • Workout Analytics API documentation: coming soon

Support​

For workout analytics questions:

  1. Review the Workout Analytics API documentation
  2. Test analytics queries in GraphQL Playground
  3. Check analytics calculations and trend analysis logic
  4. Contact the data team for advanced analytics questions