// // WeightProgressionChartView.swift // Workouts // // Created on 7/20/25. // // Copyright 2025 Rouslan Zenetl. All Rights Reserved. // import SwiftUI import Charts import SwiftData struct WeightProgressionChartView: View { let exerciseName: String /// Completed logs for this exercise, oldest first. @Query private var logs: [WorkoutLog] init(exerciseName: String) { self.exerciseName = exerciseName let name = exerciseName // `completed` is derived (status == .completed); the predicate compares the // stored `statusRaw` column directly, since #Predicate can't call a computed // property. Capture the raw value in a local so the macro can embed it. let completedRaw = WorkoutStatus.completed.rawValue _logs = Query( filter: #Predicate { $0.exerciseName == name && $0.statusRaw == completedRaw }, sort: \WorkoutLog.date, order: .forward ) } @AppStorage("weightUnit") private var weightUnit: WeightUnit = .lb private var weightData: [WeightDataPoint] { // Top-set *actual* weight per completed log; legacy logs without recorded // entries fall back to the planned weight (the old behavior). logs.map { log in let topSet = log.setEntries?.compactMap(\.weight).max() return WeightDataPoint(date: log.date, weight: topSet ?? log.weight) } } var body: some View { VStack(alignment: .leading) { if weightData.isEmpty { Text("No weight history available yet.") .foregroundColor(.secondary) .frame(maxWidth: .infinity, alignment: .center) .padding() } else { Text("Weight Progression") .font(.headline) .padding(.bottom, 4) Chart { ForEach(weightData) { dataPoint in LineMark( x: .value("Date", dataPoint.date), y: .value("Weight", dataPoint.weight) ) .foregroundStyle(Color.blue.gradient) .interpolationMethod(.catmullRom) PointMark( x: .value("Date", dataPoint.date), y: .value("Weight", dataPoint.weight) ) .foregroundStyle(Color.blue) } } .chartYScale(domain: .automatic(includesZero: false)) .chartXAxis { AxisMarks(values: .automatic) { _ in AxisGridLine() AxisValueLabel(format: .dateTime.month().day()) } } .frame(height: 200) .padding(.bottom, 8) if !motivationalMessage.isEmpty { Text(motivationalMessage) .font(.subheadline) .foregroundColor(.primary) .padding() .background(Color.blue.opacity(0.1)) .cornerRadius(8) } } } .padding() } private var motivationalMessage: String { let data = weightData guard data.count >= 2 else { return "Complete more workouts to track your progress!" } let firstWeight = data.first?.weight ?? 0 let currentWeight = data.last?.weight ?? 0 let weightDifference = currentWeight - firstWeight if weightDifference > 0 { let percentIncrease = firstWeight > 0 ? Int((weightDifference / firstWeight) * 100) : 0 if percentIncrease >= 20 { return "Amazing progress! You've increased your weight by \(weightUnit.format(weightDifference)) (\(percentIncrease)%)!" } else if percentIncrease >= 10 { return "Great job! You've increased your weight by \(weightUnit.format(weightDifference)) (\(percentIncrease)%)!" } else { return "You're making progress! Weight increased by \(weightUnit.format(weightDifference)). Keep it up!" } } else if weightDifference == 0 { return "You're maintaining consistent weight. Focus on form and consider increasing when ready!" } else { return "Your current weight is lower than when you started. Adjust your training as needed and keep pushing!" } } } // Data structure for chart points struct WeightDataPoint: Identifiable { let id = UUID() let date: Date let weight: Double }