// // TrendsSection.swift // Workouts // // Copyright 2026 Rouslan Zenetl. All Rights Reserved. // import SwiftUI import Charts /// Aggregate trends over time: one chart, a metric picker (workouts, volume, active /// time, energy), and the standard 7D/30D/90D/1Y/All range picker. Counts render as /// bars (discrete events); the other metrics as the smooth line + points pattern. struct TrendsSection: View { let workouts: [WorkoutFacts] @AppStorage("weightUnit") private var weightUnit: WeightUnit = .lb @State private var range: TrendRange = .month @State private var metric: Metric = .workouts enum Metric: String, CaseIterable, Identifiable { case workouts = "Workouts" case volume = "Volume" case time = "Time" case energy = "Energy" var id: String { rawValue } var color: Color { switch self { case .workouts: .blue case .volume: .purple case .time: .green case .energy: .orange } } } var body: some View { VStack(alignment: .leading, spacing: 12) { Text("Trends") .font(.headline) Picker("Metric", selection: $metric) { ForEach(Metric.allCases) { metric in Text(metric.rawValue).tag(metric) } } .pickerStyle(.segmented) Picker("Range", selection: $range) { ForEach(TrendRange.allCases) { range in Text(range.rawValue).tag(range) } } .pickerStyle(.segmented) chart } .progressCard() } @ViewBuilder private var chart: some View { let points = ProgressPlanner.trendPoints(workouts: workouts, range: range) if points.isEmpty || points.allSatisfy({ $0.workouts == 0 }) { ContentUnavailableView( "No Data", systemImage: "chart.xyaxis.line", description: Text(workouts.contains(where: \.completed) ? "No workouts in this time range. Try a wider range." : "Complete workouts and your trends build up here.")) .frame(height: 220) } else { Chart(points) { point in if metric == .workouts { BarMark( x: .value("Date", point.date, unit: range.bucket), y: .value(metric.rawValue, value(of: point)) ) .foregroundStyle(metric.color.gradient) .cornerRadius(3) } else { LineMark( x: .value("Date", point.date, unit: range.bucket), y: .value(metric.rawValue, value(of: point)) ) .interpolationMethod(.catmullRom) .foregroundStyle(metric.color.gradient) PointMark( x: .value("Date", point.date, unit: range.bucket), y: .value(metric.rawValue, value(of: point)) ) .symbolSize(30) .foregroundStyle(metric.color) } } .chartYAxisLabel(unitLabel) .chartXAxis { AxisMarks(values: .automatic) { _ in AxisGridLine() AxisValueLabel(format: axisDateFormat) } } .frame(height: 220) } } private func value(of point: TrendPoint) -> Double { switch metric { case .workouts: Double(point.workouts) case .volume: point.volume case .time: point.activeMinutes case .energy: point.energyKcal } } private var unitLabel: String { switch metric { case .workouts: "" case .volume: weightUnit.abbreviation case .time: "min" case .energy: "kcal" } } private var axisDateFormat: Date.FormatStyle { switch range.bucket { case .month: .dateTime.month(.abbreviated) default: .dateTime.month().day() } } }