Sfoglia il codice sorgente

Restructure files+folders for stats; fix looping stats

Deniz Cengiz 1 anno fa
parent
commit
79d823f6c5

+ 43 - 15
Trio.xcodeproj/project.pbxproj

@@ -208,8 +208,6 @@
 		491D6FBE2D56741C00C49F67 /* TempTargetRunStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491D6FB92D56741C00C49F67 /* TempTargetRunStored+CoreDataClass.swift */; };
 		491D6FBF2D56741C00C49F67 /* TempTargetRunStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491D6FBA2D56741C00C49F67 /* TempTargetRunStored+CoreDataProperties.swift */; };
 		491D6FC02D56741C00C49F67 /* TempTargetStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491D6FBB2D56741C00C49F67 /* TempTargetStored+CoreDataClass.swift */; };
-		49249B1C2D46E45E000F4866 /* CurrentTDDSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49249B1B2D46E45E000F4866 /* CurrentTDDSetup.swift */; };
-		49249B382D46E76A000F4866 /* TDD.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49249B372D46E76A000F4866 /* TDD.swift */; };
 		49B9B57F2D5768D2009C6B59 /* AdjustmentStored+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49B9B57E2D5768D2009C6B59 /* AdjustmentStored+Helper.swift */; };
 		5075C1608E6249A51495C422 /* TargetsEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BDEA2DC60EDE0A3CA54DC73 /* TargetsEditorProvider.swift */; };
 		53F2382465BF74DB1A967C8B /* PumpConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8630D58BDAD6D9C650B9B39 /* PumpConfigProvider.swift */; };
@@ -502,9 +500,9 @@
 		DD498F2B2D692BEA00AAEA30 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 8A9134292D63D9A1007F8874 /* Localizable.xcstrings */; };
 		DD498F2C2D692BEA00AAEA30 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 8A9134292D63D9A1007F8874 /* Localizable.xcstrings */; };
 		DD498F2D2D692BEA00AAEA30 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 8A9134292D63D9A1007F8874 /* Localizable.xcstrings */; };
-		DD4C581F2D73C43D001BFF2C /* LoopStatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4C581E2D73C43D001BFF2C /* LoopStatsView.swift */; };
 		DD4C57A82D73ADEA001BFF2C /* RestartLiveActivityIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4C57A72D73ADEA001BFF2C /* RestartLiveActivityIntent.swift */; };
 		DD4C57AA2D73B3E2001BFF2C /* RestartLiveActivityIntentRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4C57A92D73B3D9001BFF2C /* RestartLiveActivityIntentRequest.swift */; };
+		DD4C581F2D73C43D001BFF2C /* LoopStatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4C581E2D73C43D001BFF2C /* LoopStatsView.swift */; };
 		DD4FFF332D458EE600B6CFF9 /* GarminWatchState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4FFF322D458EE600B6CFF9 /* GarminWatchState.swift */; };
 		DD5DC9F12CF3D97C00AB8703 /* AdjustmentsStateModel+Overrides.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5DC9F02CF3D96E00AB8703 /* AdjustmentsStateModel+Overrides.swift */; };
 		DD5DC9F32CF3D9DD00AB8703 /* AdjustmentsStateModel+TempTargets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5DC9F22CF3D9D600AB8703 /* AdjustmentsStateModel+TempTargets.swift */; };
@@ -931,8 +929,6 @@
 		491D6FBA2D56741C00C49F67 /* TempTargetRunStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempTargetRunStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
 		491D6FBB2D56741C00C49F67 /* TempTargetStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempTargetStored+CoreDataClass.swift"; sourceTree = "<group>"; };
 		491D6FBC2D56741C00C49F67 /* TempTargetStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempTargetStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
-		49249B1B2D46E45E000F4866 /* CurrentTDDSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentTDDSetup.swift; sourceTree = "<group>"; };
-		49249B372D46E76A000F4866 /* TDD.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TDD.swift; sourceTree = "<group>"; };
 		49B9B57E2D5768D2009C6B59 /* AdjustmentStored+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdjustmentStored+Helper.swift"; sourceTree = "<group>"; };
 		4DD795BA46B193644D48138C /* TargetsEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorRootView.swift; sourceTree = "<group>"; };
 		505E09DC17A0C3D0AF4B66FE /* ISFEditorStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ISFEditorStateModel.swift; sourceTree = "<group>"; };
@@ -1226,9 +1222,9 @@
 		DD32CFA12CC824E1003686D6 /* TrioRemoteControl+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TrioRemoteControl+Helpers.swift"; sourceTree = "<group>"; };
 		DD3A3CE62D29C93F00AE478E /* Helper+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Helper+Extensions.swift"; sourceTree = "<group>"; };
 		DD3A3CE82D29C97800AE478E /* Helper+ButtonStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Helper+ButtonStyles.swift"; sourceTree = "<group>"; };
-		DD4C581E2D73C43D001BFF2C /* LoopStatsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopStatsView.swift; sourceTree = "<group>"; };
 		DD4C57A72D73ADEA001BFF2C /* RestartLiveActivityIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestartLiveActivityIntent.swift; sourceTree = "<group>"; };
 		DD4C57A92D73B3D9001BFF2C /* RestartLiveActivityIntentRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestartLiveActivityIntentRequest.swift; sourceTree = "<group>"; };
+		DD4C581E2D73C43D001BFF2C /* LoopStatsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopStatsView.swift; sourceTree = "<group>"; };
 		DD4FFF322D458EE600B6CFF9 /* GarminWatchState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GarminWatchState.swift; sourceTree = "<group>"; };
 		DD5DC9F02CF3D96E00AB8703 /* AdjustmentsStateModel+Overrides.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdjustmentsStateModel+Overrides.swift"; sourceTree = "<group>"; };
 		DD5DC9F22CF3D9D600AB8703 /* AdjustmentsStateModel+TempTargets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdjustmentsStateModel+TempTargets.swift"; sourceTree = "<group>"; };
@@ -2531,15 +2527,10 @@
 		BD249D842D42FBD200412DEB /* ViewElements */ = {
 			isa = PBXGroup;
 			children = (
-				DD4C581E2D73C43D001BFF2C /* LoopStatsView.swift */,
-				BD249D932D42FC5C00412DEB /* TotalDailyDoseChart.swift */,
-				BD249D912D42FC5000412DEB /* GlucoseSectorChart.swift */,
-				BD249D8F2D42FC4300412DEB /* MealStatsView.swift */,
-				BD249D8D2D42FC3600412DEB /* LoopBarChartView.swift */,
-				BD249D8B2D42FC2500412DEB /* GlucoseDistributionChart.swift */,
-				BD249D892D42FC0E00412DEB /* GlucosePercentileChart.swift */,
-				BD249D872D42FBFB00412DEB /* BolusStatsView.swift */,
-				BD249D852D42FBE600412DEB /* GlucoseMetricsView.swift */,
+				DDCAE97A2D79F99B00B1BB51 /* Glucose */,
+				DDCAE9792D79F99200B1BB51 /* Meal */,
+				DDCAE9782D79F98E00B1BB51 /* Insulin */,
+				DDCAE9772D79F98600B1BB51 /* Looping */,
 			);
 			path = ViewElements;
 			sourceTree = "<group>";
@@ -3088,6 +3079,43 @@
 			path = Nightscout;
 			sourceTree = "<group>";
 		};
+		DDCAE9772D79F98600B1BB51 /* Looping */ = {
+			isa = PBXGroup;
+			children = (
+				DD4C581E2D73C43D001BFF2C /* LoopStatsView.swift */,
+				BD249D8D2D42FC3600412DEB /* LoopBarChartView.swift */,
+			);
+			path = Looping;
+			sourceTree = "<group>";
+		};
+		DDCAE9782D79F98E00B1BB51 /* Insulin */ = {
+			isa = PBXGroup;
+			children = (
+				BD249D932D42FC5C00412DEB /* TotalDailyDoseChart.swift */,
+				BD249D872D42FBFB00412DEB /* BolusStatsView.swift */,
+			);
+			path = Insulin;
+			sourceTree = "<group>";
+		};
+		DDCAE9792D79F99200B1BB51 /* Meal */ = {
+			isa = PBXGroup;
+			children = (
+				BD249D8F2D42FC4300412DEB /* MealStatsView.swift */,
+			);
+			path = Meal;
+			sourceTree = "<group>";
+		};
+		DDCAE97A2D79F99B00B1BB51 /* Glucose */ = {
+			isa = PBXGroup;
+			children = (
+				BD249D912D42FC5000412DEB /* GlucoseSectorChart.swift */,
+				BD249D8B2D42FC2500412DEB /* GlucoseDistributionChart.swift */,
+				BD249D892D42FC0E00412DEB /* GlucosePercentileChart.swift */,
+				BD249D852D42FBE600412DEB /* GlucoseMetricsView.swift */,
+			);
+			path = Glucose;
+			sourceTree = "<group>";
+		};
 		DDD163032C4C67B400CD525A /* Adjustments */ = {
 			isa = PBXGroup;
 			children = (

+ 2 - 2
Trio/Sources/Localizations/Main/Localizable.xcstrings

@@ -100239,7 +100239,7 @@
         }
       }
     },
-    "Loops per day" : {
+    "Loops per Day" : {
 
     },
     "Low" : {
@@ -147291,7 +147291,7 @@
         }
       }
     },
-    "Successful Loops" : {
+    "Successful Loop" : {
 
     },
     "Suggested at" : {

+ 61 - 36
Trio/Sources/Modules/Stat/StatStateModel+Setup/LoopChartSetup.swift

@@ -23,6 +23,18 @@ struct LoopStatsByPeriod: Identifiable {
     var id: Date { period }
 }
 
+enum LoopStatsDataType: String {
+    case successfulLoop
+    case glucoseCount
+
+    var displayName: String {
+        switch self {
+        case .successfulLoop: return String(localized: "Successful Loop")
+        case .glucoseCount: return String(localized: "Glucose Count")
+        }
+    }
+}
+
 extension Stat.StateModel {
     /// Initiates the process of fetching and processing loop statistics
     /// This function coordinates three main tasks:
@@ -62,9 +74,9 @@ extension Stat.StateModel {
         let startDate: Date
         switch duration {
         case .Day:
-            startDate = Calendar.current.startOfDay(for: now)
-        case .Today:
             startDate = now.addingTimeInterval(-24.hours.timeInterval)
+        case .Today:
+            startDate = Calendar.current.startOfDay(for: now)
         case .Week:
             startDate = now.addingTimeInterval(-7.days.timeInterval)
         case .Month:
@@ -127,15 +139,18 @@ extension Stat.StateModel {
         allLoopIds: [NSManagedObjectID],
         failedLoopIds: [NSManagedObjectID],
         duration: Duration
-    ) async throws -> [(category: String, count: Int, percentage: Double)] {
+//    ) async throws -> [(category: LoopStatusType, count: Int, percentage: Double)] {
+    ) async throws
+        -> [(category: LoopStatsDataType, count: Int, percentage: Double, medianDuration: Double, medianInterval: Double)]
+    {
         // Calculate the date range for glucose readings
         let now = Date()
         let startDate: Date
         switch duration {
         case .Day:
-            startDate = Calendar.current.startOfDay(for: now)
-        case .Today:
             startDate = now.addingTimeInterval(-24.hours.timeInterval)
+        case .Today:
+            startDate = Calendar.current.startOfDay(for: now)
         case .Week:
             startDate = now.addingTimeInterval(-7.days.timeInterval)
         case .Month:
@@ -148,8 +163,10 @@ extension Stat.StateModel {
         let totalGlucose = try await calculateGlucoseStats(from: startDate, to: now)
 
         // Get NSManagedObject
-        let allLoops = try await CoreDataStack.shared.getNSManagedObject(with: allLoopIds, context: loopTaskContext)
-        let failedLoops = try await CoreDataStack.shared.getNSManagedObject(with: failedLoopIds, context: loopTaskContext)
+        let allLoops = try await CoreDataStack.shared
+            .getNSManagedObject(with: allLoopIds, context: loopTaskContext) as? [LoopStatRecord] ?? []
+        let failedLoops = try await CoreDataStack.shared
+            .getNSManagedObject(with: failedLoopIds, context: loopTaskContext) as? [LoopStatRecord] ?? []
 
         return await loopTaskContext.perform {
             let totalLoopsCount = allLoops.count
@@ -157,35 +174,43 @@ extension Stat.StateModel {
             let successfulLoops = totalLoopsCount - failedLoopsCount
             let maxLoopsPerDay = 288.0 // Maximum possible loops per day (every 5 minutes)
 
-            switch duration {
-            case .Day:
-                // For Day view: Calculate percentage based on maximum possible loops per day
-                let loopPercentage = (Double(successfulLoops) / maxLoopsPerDay) * 100
-                let glucosePercentage = (Double(totalGlucose) / maxLoopsPerDay) * 100
-
-                return [
-                    (String(localized: "Successful Loops"), successfulLoops, loopPercentage),
-                    (String(localized: "Glucose Count"), totalGlucose, glucosePercentage)
-                ]
-
-            case .Month,
-                 .Today,
-                 .Total,
-                 .Week:
-                // For other views: Calculate average per day
-                let numberOfDays = max(1, Calendar.current.dateComponents([.day], from: startDate, to: now).day ?? 1)
-
-                let averageLoopsPerDay = Double(successfulLoops) / Double(numberOfDays)
-                let averageGlucosePerDay = Double(totalGlucose) / Double(numberOfDays)
-
-                let loopPercentage = (averageLoopsPerDay / maxLoopsPerDay) * 100
-                let glucosePercentage = (averageGlucosePerDay / maxLoopsPerDay) * 100
-
-                return [
-                    (String(localized: "Successful Loops"), Int(round(averageLoopsPerDay)), loopPercentage),
-                    (String(localized: "Glucose Count"), Int(round(averageGlucosePerDay)), glucosePercentage)
-                ]
-            }
+            let numberOfDays = max(1, Calendar.current.dateComponents([.day], from: startDate, to: now).day ?? 1)
+            let averageLoopsPerDay = Double(successfulLoops) / Double(numberOfDays)
+            let averageGlucosePerDay = Double(totalGlucose) / Double(numberOfDays)
+
+            // Calculate median duration (time from start to end of each loop)
+            let sortedDurations: [TimeInterval] = allLoops.compactMap { loop in
+                guard let start = loop.start, let end = loop.end else { return nil }
+                return end.timeIntervalSince(start)
+            }.sorted()
+            let medianDuration = sortedDurations.isEmpty ? 0.0 : sortedDurations[sortedDurations.count / 2]
+
+            // Calculate median interval (time between end of n-th loop and start of n+1th loop)
+            let sortedIntervals: [TimeInterval] = zip(allLoops.dropLast(), allLoops.dropFirst()).compactMap { previous, next in
+                guard let previousEnd = previous.end, let nextStart = next.start else { return nil }
+                return previousEnd.timeIntervalSince(nextStart)
+            }.sorted()
+            let medianInterval = sortedIntervals.isEmpty ? 0.0 : sortedIntervals[sortedIntervals.count / 2]
+
+            let loopPercentage = (averageLoopsPerDay / maxLoopsPerDay) * 100
+            let glucosePercentage = (averageGlucosePerDay / maxLoopsPerDay) * 100
+
+            return [
+                (
+                    LoopStatsDataType.successfulLoop,
+                    Int(round(averageLoopsPerDay)),
+                    loopPercentage,
+                    medianDuration,
+                    medianInterval
+                ),
+                (
+                    LoopStatsDataType.glucoseCount,
+                    Int(round(averageGlucosePerDay)),
+                    glucosePercentage,
+                    medianDuration,
+                    medianInterval
+                )
+            ]
         }
     }
 

+ 7 - 1
Trio/Sources/Modules/Stat/StatStateModel.swift

@@ -49,7 +49,13 @@ extension Stat {
         var useFPUconversion: Bool = false
         var glucoseFromPersistence: [GlucoseStored] = []
         var loopStatRecords: [LoopStatRecord] = []
-        var loopStats: [(category: String, count: Int, percentage: Double)] = []
+        var loopStats: [(
+            category: LoopStatsDataType,
+            count: Int,
+            percentage: Double,
+            medianDuration: Double,
+            medianInterval: Double
+        )] = []
         var groupedLoopStats: [LoopStatsByPeriod] = []
         var bolusStats: [BolusStats] = []
         var hourlyStats: [HourlyStats] = []

+ 1 - 5
Trio/Sources/Modules/Stat/View/StatRootView.swift

@@ -296,11 +296,7 @@ extension Stat {
             StatCard {
                 VStack(spacing: Constants.spacing) {
                     LoopStatsView(
-                        highLimit: state.highLimit,
-                        lowLimit: state.lowLimit,
-                        units: state.units,
-                        eA1cDisplayUnit: state.eA1cDisplayUnit,
-                        loopStatRecords: state.loopStatRecords
+                        statsData: state.loopStats
                     )
                 }
             }

Trio/Sources/Modules/Stat/View/ViewElements/GlucoseDistributionChart.swift → Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseDistributionChart.swift


Trio/Sources/Modules/Stat/View/ViewElements/GlucoseMetricsView.swift → Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseMetricsView.swift


Trio/Sources/Modules/Stat/View/ViewElements/GlucosePercentileChart.swift → Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucosePercentileChart.swift


Trio/Sources/Modules/Stat/View/ViewElements/GlucoseSectorChart.swift → Trio/Sources/Modules/Stat/View/ViewElements/Glucose/GlucoseSectorChart.swift


Trio/Sources/Modules/Stat/View/ViewElements/BolusStatsView.swift → Trio/Sources/Modules/Stat/View/ViewElements/Insulin/BolusStatsView.swift


Trio/Sources/Modules/Stat/View/ViewElements/TotalDailyDoseChart.swift → Trio/Sources/Modules/Stat/View/ViewElements/Insulin/TotalDailyDoseChart.swift


+ 0 - 74
Trio/Sources/Modules/Stat/View/ViewElements/LoopStatsView.swift

@@ -1,74 +0,0 @@
-import SwiftDate
-import SwiftUI
-
-/// A SwiftUI view displaying statistics about the looping process in an Automated Insulin Delivery (AID) system.
-struct LoopStatsView: View {
-    /// The upper glucose limit used for loop evaluation.
-    let highLimit: Decimal
-    /// The lower glucose limit used for loop evaluation.
-    let lowLimit: Decimal
-    /// The unit of measurement for blood glucose values (e.g., mg/dL or mmol/L).
-    let units: GlucoseUnits
-    /// The display unit for estimated HbA1c values.
-    let eA1cDisplayUnit: EstimatedA1cDisplayUnit
-    /// The list of loop statistics records used to generate the statistics.
-    let loopStatRecords: [LoopStatRecord]
-
-    /// The main body of the `LoopStatsView`, displaying loop statistics.
-    var body: some View {
-        loops
-    }
-
-    /// A computed property that calculates and displays various loop statistics such as:
-    /// - Number of loops
-    /// - Average interval between loops
-    /// - Median loop duration
-    /// - Loop success rate
-    private var loops: some View {
-        let loops = loopStatRecords
-        // Retrieve the first (earliest) and last (most recent) loop timestamps
-        let previous = loops.last?.end ?? Date()
-        let current = loops.first?.start ?? Date()
-        // Calculate the total duration of recorded loops in days
-        let totalTime = (current - previous).timeInterval / 8.64E4
-
-        // Extract loop durations
-        let durationArray = loops.compactMap(\.duration)
-        let durationArrayCount = durationArray.count
-        let medianDuration = StatChartUtils.medianCalculationDouble(array: durationArray)
-
-        // Count successful loops
-        let successNR = loops.compactMap(\.loopStatus).filter { $0!.contains("Success") }.count
-        let errorNR = durationArrayCount - successNR
-        let total = Double(successNR + errorNR) == 0 ? 1 : Double(successNR + errorNR)
-        let successRate: Double? = (Double(successNR) / total) * 100
-
-        // Calculate the number of loops per day
-        let loopNr = totalTime <= 1 ? total : round(total / (totalTime != 0 ? totalTime : 1))
-
-        // Calculate the average loop interval
-        let intervalArray = loops.compactMap { $0.interval as Double }
-        let count = intervalArray.count != 0 ? intervalArray.count : 1
-        let intervalAverage = intervalArray.reduce(0, +) / Double(count)
-
-        return HStack {
-            StatChartUtils.statView(title: String(localized: "Loops"), value: loopNr.formatted())
-            Spacer()
-            StatChartUtils.statView(
-                title: String(localized: "Interval"),
-                value: intervalAverage.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + "m"
-            )
-            Spacer()
-            StatChartUtils.statView(
-                title: String(localized: "Duration"),
-                value: (medianDuration / 1000).formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + "s"
-            )
-            Spacer()
-            StatChartUtils.statView(
-                title: String(localized: "Success"),
-                value: ((successRate ?? 100) / 100).formatted(.percent.grouping(.never).rounded().precision(.fractionLength(1)))
-            )
-        }
-        .padding()
-    }
-}

+ 12 - 8
Trio/Sources/Modules/Stat/View/ViewElements/LoopBarChartView.swift

@@ -4,17 +4,17 @@ import SwiftUI
 struct LoopBarChartView: View {
     let loopStatRecords: [LoopStatRecord]
     let selectedDuration: Stat.StateModel.Duration
-    let statsData: [(category: String, count: Int, percentage: Double)]
+    let statsData: [(category: LoopStatsDataType, count: Int, percentage: Double, medianDuration: Double, medianInterval: Double)]
 
     var body: some View {
         VStack(spacing: 20) {
             Chart(statsData, id: \.category) { data in
                 BarMark(
                     x: .value("Percentage", data.percentage),
-                    y: .value("Category", data.category)
+                    y: .value("Category", data.category.displayName)
                 )
                 .cornerRadius(5)
-                .foregroundStyle(data.category == "Successful Loops" ? Color.blue : Color.green)
+                .foregroundStyle(data.category == .successfulLoop ? Color.blue : Color.green)
                 .annotation(position: .overlay) {
                     HStack {
                         Text(annotationText(for: data))
@@ -50,8 +50,14 @@ struct LoopBarChartView: View {
         }
     }
 
-    private func annotationText(for data: (category: String, count: Int, percentage: Double)) -> String {
-        if data.category == "Successful Loops" {
+    private func annotationText(for data: (
+        category: LoopStatsDataType,
+        count: Int,
+        percentage: Double,
+        medianDuration: Double,
+        medianInterval: Double
+    )) -> String {
+        if data.category == .successfulLoop {
             switch selectedDuration {
             case .Day,
                  .Today:
@@ -59,9 +65,7 @@ struct LoopBarChartView: View {
             case .Month,
                  .Total,
                  .Week:
-                let maxLoopsPerDay = 288.0
-                let averageLoopsPerDay = Double(data.count) / maxLoopsPerDay * 100
-                return "\(Int(round(averageLoopsPerDay))) " + String(localized: "Loops per day")
+                return "\(data.count) " + String(localized: "Loops per Day")
             }
         } else {
             // For Glucose Count, show different text based on duration

+ 39 - 0
Trio/Sources/Modules/Stat/View/ViewElements/Looping/LoopStatsView.swift

@@ -0,0 +1,39 @@
+import SwiftDate
+import SwiftUI
+
+/// A SwiftUI view displaying statistics about the looping process in an Automated Insulin Delivery (AID) system.
+struct LoopStatsView: View {
+    /// The list of loop statistics records used to generate the statistics.
+    let statsData: [(category: LoopStatsDataType, count: Int, percentage: Double, medianDuration: Double, medianInterval: Double)]
+
+    /// The main body of the `LoopStatsView`, displaying loop statistics.
+    var body: some View {
+        if let successfulStats = statsData.first(where: { $0.category == .successfulLoop }) {
+            HStack {
+                StatChartUtils.statView(
+                    title: String(localized: "Loops"),
+                    value: successfulStats.count.formatted()
+                )
+                Spacer()
+                StatChartUtils.statView(
+                    title: String(localized: "Interval"),
+                    value: (successfulStats.medianInterval / 60)
+                        .formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + "m"
+                )
+                Spacer()
+                StatChartUtils.statView(
+                    title: String(localized: "Duration"),
+                    value: successfulStats.medianDuration
+                        .formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + "s"
+                )
+                Spacer()
+                StatChartUtils.statView(
+                    title: String(localized: "Success"),
+                    value: (successfulStats.percentage / 100)
+                        .formatted(.percent.grouping(.never).rounded().precision(.fractionLength(1)))
+                )
+            }
+            .padding()
+        }
+    }
+}

Trio/Sources/Modules/Stat/View/ViewElements/MealStatsView.swift → Trio/Sources/Modules/Stat/View/ViewElements/Meal/MealStatsView.swift