Просмотр исходного кода

Refactoring and small UI tweaks

Deniz Cengiz 1 год назад
Родитель
Сommit
3f332c56fd

+ 24 - 20
Trio.xcodeproj/project.pbxproj

@@ -288,14 +288,14 @@
 		BD04ECCE2D29952A008C5FEB /* BolusProgressOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD04ECCD2D299522008C5FEB /* BolusProgressOverlay.swift */; };
 		BD0B2EF32C5998E600B3298F /* MealPresetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD0B2EF22C5998E600B3298F /* MealPresetView.swift */; };
 		BD1661312B82ADAB00256551 /* CustomProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD1661302B82ADAB00256551 /* CustomProgressView.swift */; };
-		BD249D862D42FBEC00412DEB /* BareStatisticsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD249D852D42FBE600412DEB /* BareStatisticsView.swift */; };
+		BD249D862D42FBEC00412DEB /* GlucoseMetricsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD249D852D42FBE600412DEB /* GlucoseMetricsView.swift */; };
 		BD249D882D42FC0000412DEB /* BolusStatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD249D872D42FBFB00412DEB /* BolusStatsView.swift */; };
 		BD249D8A2D42FC1200412DEB /* GlucosePercentileChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD249D892D42FC0E00412DEB /* GlucosePercentileChart.swift */; };
 		BD249D8C2D42FC2C00412DEB /* GlucoseDistributionChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD249D8B2D42FC2500412DEB /* GlucoseDistributionChart.swift */; };
-		BD249D8E2D42FC3900412DEB /* LoopStatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD249D8D2D42FC3600412DEB /* LoopStatsView.swift */; };
+		BD249D8E2D42FC3900412DEB /* LoopBarChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD249D8D2D42FC3600412DEB /* LoopBarChartView.swift */; };
 		BD249D902D42FC4500412DEB /* MealStatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD249D8F2D42FC4300412DEB /* MealStatsView.swift */; };
-		BD249D922D42FC5300412DEB /* SectorChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD249D912D42FC5000412DEB /* SectorChart.swift */; };
-		BD249D942D42FC5E00412DEB /* TDDChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD249D932D42FC5C00412DEB /* TDDChart.swift */; };
+		BD249D922D42FC5300412DEB /* GlucoseSectorChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD249D912D42FC5000412DEB /* GlucoseSectorChart.swift */; };
+		BD249D942D42FC5E00412DEB /* TotalDailyDoseChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD249D932D42FC5C00412DEB /* TotalDailyDoseChart.swift */; };
 		BD249D972D42FCBF00412DEB /* AreaChartSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD249D962D42FCBD00412DEB /* AreaChartSetup.swift */; };
 		BD249D992D42FCCD00412DEB /* BolusStatsSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD249D982D42FCCA00412DEB /* BolusStatsSetup.swift */; };
 		BD249D9B2D42FCDB00412DEB /* LoopChartSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD249D9A2D42FCD800412DEB /* LoopChartSetup.swift */; };
@@ -491,6 +491,7 @@
 		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 */; };
 		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 */; };
@@ -509,7 +510,7 @@
 		DD88C8E22C50420800F2D558 /* DefinitionRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD88C8E12C50420800F2D558 /* DefinitionRow.swift */; };
 		DD940BAA2CA7585D000830A5 /* GlucoseColorScheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD940BA92CA7585D000830A5 /* GlucoseColorScheme.swift */; };
 		DD940BAC2CA75889000830A5 /* DynamicGlucoseColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD940BAB2CA75889000830A5 /* DynamicGlucoseColor.swift */; };
-		DD98ACC02D71013200C0778F /* StatsHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD98ACBF2D71013200C0778F /* StatsHelper.swift */; };
+		DD98ACC02D71013200C0778F /* StatChartUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD98ACBF2D71013200C0778F /* StatChartUtils.swift */; };
 		DD9ECB682CA99F4500AA7C45 /* TrioRemoteControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB672CA99F4500AA7C45 /* TrioRemoteControl.swift */; };
 		DD9ECB6A2CA99F6C00AA7C45 /* PushMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB692CA99F6C00AA7C45 /* PushMessage.swift */; };
 		DD9ECB702CA9A0BA00AA7C45 /* RemoteControlConfigStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB6D2CA9A0BA00AA7C45 /* RemoteControlConfigStateModel.swift */; };
@@ -998,14 +999,14 @@
 		BD0B2EF22C5998E600B3298F /* MealPresetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MealPresetView.swift; sourceTree = "<group>"; };
 		BD1661302B82ADAB00256551 /* CustomProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomProgressView.swift; sourceTree = "<group>"; };
 		BD1CF8B72C1A4A8400CB930A /* ConfigOverride.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ConfigOverride.xcconfig; sourceTree = "<group>"; };
-		BD249D852D42FBE600412DEB /* BareStatisticsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BareStatisticsView.swift; sourceTree = "<group>"; };
+		BD249D852D42FBE600412DEB /* GlucoseMetricsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseMetricsView.swift; sourceTree = "<group>"; };
 		BD249D872D42FBFB00412DEB /* BolusStatsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusStatsView.swift; sourceTree = "<group>"; };
 		BD249D892D42FC0E00412DEB /* GlucosePercentileChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucosePercentileChart.swift; sourceTree = "<group>"; };
 		BD249D8B2D42FC2500412DEB /* GlucoseDistributionChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseDistributionChart.swift; sourceTree = "<group>"; };
-		BD249D8D2D42FC3600412DEB /* LoopStatsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopStatsView.swift; sourceTree = "<group>"; };
+		BD249D8D2D42FC3600412DEB /* LoopBarChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopBarChartView.swift; sourceTree = "<group>"; };
 		BD249D8F2D42FC4300412DEB /* MealStatsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MealStatsView.swift; sourceTree = "<group>"; };
-		BD249D912D42FC5000412DEB /* SectorChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectorChart.swift; sourceTree = "<group>"; };
-		BD249D932D42FC5C00412DEB /* TDDChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TDDChart.swift; sourceTree = "<group>"; };
+		BD249D912D42FC5000412DEB /* GlucoseSectorChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseSectorChart.swift; sourceTree = "<group>"; };
+		BD249D932D42FC5C00412DEB /* TotalDailyDoseChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TotalDailyDoseChart.swift; sourceTree = "<group>"; };
 		BD249D962D42FCBD00412DEB /* AreaChartSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AreaChartSetup.swift; sourceTree = "<group>"; };
 		BD249D982D42FCCA00412DEB /* BolusStatsSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusStatsSetup.swift; sourceTree = "<group>"; };
 		BD249D9A2D42FCD800412DEB /* LoopChartSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopChartSetup.swift; sourceTree = "<group>"; };
@@ -1201,6 +1202,7 @@
 		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>"; };
 		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>"; };
@@ -1219,7 +1221,7 @@
 		DD88C8E12C50420800F2D558 /* DefinitionRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefinitionRow.swift; sourceTree = "<group>"; };
 		DD940BA92CA7585D000830A5 /* GlucoseColorScheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseColorScheme.swift; sourceTree = "<group>"; };
 		DD940BAB2CA75889000830A5 /* DynamicGlucoseColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicGlucoseColor.swift; sourceTree = "<group>"; };
-		DD98ACBF2D71013200C0778F /* StatsHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsHelper.swift; sourceTree = "<group>"; };
+		DD98ACBF2D71013200C0778F /* StatChartUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatChartUtils.swift; sourceTree = "<group>"; };
 		DD9ECB672CA99F4500AA7C45 /* TrioRemoteControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrioRemoteControl.swift; sourceTree = "<group>"; };
 		DD9ECB692CA99F6C00AA7C45 /* PushMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushMessage.swift; sourceTree = "<group>"; };
 		DD9ECB6D2CA9A0BA00AA7C45 /* RemoteControlConfigStateModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlConfigStateModel.swift; sourceTree = "<group>"; };
@@ -1589,6 +1591,7 @@
 			children = (
 				BD249D842D42FBD200412DEB /* ViewElements */,
 				19F95FF929F1102A00314DDC /* StatRootView.swift */,
+				DD98ACBF2D71013200C0778F /* StatChartUtils.swift */,
 			);
 			path = View;
 			sourceTree = "<group>";
@@ -2499,15 +2502,15 @@
 		BD249D842D42FBD200412DEB /* ViewElements */ = {
 			isa = PBXGroup;
 			children = (
-				DD98ACBF2D71013200C0778F /* StatsHelper.swift */,
-				BD249D932D42FC5C00412DEB /* TDDChart.swift */,
-				BD249D912D42FC5000412DEB /* SectorChart.swift */,
+				DD4C581E2D73C43D001BFF2C /* LoopStatsView.swift */,
+				BD249D932D42FC5C00412DEB /* TotalDailyDoseChart.swift */,
+				BD249D912D42FC5000412DEB /* GlucoseSectorChart.swift */,
 				BD249D8F2D42FC4300412DEB /* MealStatsView.swift */,
-				BD249D8D2D42FC3600412DEB /* LoopStatsView.swift */,
+				BD249D8D2D42FC3600412DEB /* LoopBarChartView.swift */,
 				BD249D8B2D42FC2500412DEB /* GlucoseDistributionChart.swift */,
 				BD249D892D42FC0E00412DEB /* GlucosePercentileChart.swift */,
 				BD249D872D42FBFB00412DEB /* BolusStatsView.swift */,
-				BD249D852D42FBE600412DEB /* BareStatisticsView.swift */,
+				BD249D852D42FBE600412DEB /* GlucoseMetricsView.swift */,
 			);
 			path = ViewElements;
 			sourceTree = "<group>";
@@ -3595,7 +3598,7 @@
 				CE7CA3552A064973004BE681 /* ListStateIntent.swift in Sources */,
 				BDF530D82B40F8AC002CAF43 /* LockScreenView.swift in Sources */,
 				195D80B72AF697B800D25097 /* DynamicSettingsDataFlow.swift in Sources */,
-				DD98ACC02D71013200C0778F /* StatsHelper.swift in Sources */,
+				DD98ACC02D71013200C0778F /* StatChartUtils.swift in Sources */,
 				3862CC2E2743F9F700BF832C /* CalendarManager.swift in Sources */,
 				CEA4F62329BE10F70011ADF7 /* SavitzkyGolayFilter.swift in Sources */,
 				38B4F3C325E2A20B00E76A18 /* PumpSetupView.swift in Sources */,
@@ -3673,14 +3676,14 @@
 				3811DE3125C9D49500A708ED /* HomeProvider.swift in Sources */,
 				FE41E4D629463EE20047FD55 /* NightscoutPreferences.swift in Sources */,
 				E013D872273AC6FE0014109C /* GlucoseSimulatorSource.swift in Sources */,
-				BD249D862D42FBEC00412DEB /* BareStatisticsView.swift in Sources */,
+				BD249D862D42FBEC00412DEB /* GlucoseMetricsView.swift in Sources */,
 				58645BA32CA2D325008AFCE7 /* BatterySetup.swift in Sources */,
 				388E5A5C25B6F0770019842D /* JSON.swift in Sources */,
 				3811DF0225CA9FEA00A708ED /* Credentials.swift in Sources */,
 				5837A5302BD2E3C700A5DC04 /* CarbEntryStored+helper.swift in Sources */,
 				389A572026079BAA00BC102F /* Interpolation.swift in Sources */,
 				DD9ECB702CA9A0BA00AA7C45 /* RemoteControlConfigStateModel.swift in Sources */,
-				BD249D942D42FC5E00412DEB /* TDDChart.swift in Sources */,
+				BD249D942D42FC5E00412DEB /* TotalDailyDoseChart.swift in Sources */,
 				BD249D902D42FC4500412DEB /* MealStatsView.swift in Sources */,
 				DD32CF9A2CC8247B003686D6 /* TrioRemoteControl+Meal.swift in Sources */,
 				BDF34F832C10C5B600D51995 /* DataManager.swift in Sources */,
@@ -3736,6 +3739,7 @@
 				DD6D67E42C9C253500660C9B /* ColorSchemeOption.swift in Sources */,
 				582DF9752C8CDB92001F516D /* GlucoseChartView.swift in Sources */,
 				BD432CA12D2F4E3600D1EB79 /* WatchMessageKeys.swift in Sources */,
+				DD4C581F2D73C43D001BFF2C /* LoopStatsView.swift in Sources */,
 				58A3D53A2C96D4DE003F90FC /* AddTempTargetForm.swift in Sources */,
 				DD1745302C55AE5300211FAC /* TargetBehaviorProvider.swift in Sources */,
 				58D08B382C8DFB6000AA37D3 /* BasalChart.swift in Sources */,
@@ -3770,7 +3774,7 @@
 				CEB434E528B8FF5D00B70274 /* UIColor.swift in Sources */,
 				190EBCCB29FF13CB00BA767D /* UserInterfaceSettingsRootView.swift in Sources */,
 				3811DEA925C9D88300A708ED /* AppearanceManager.swift in Sources */,
-				BD249D8E2D42FC3900412DEB /* LoopStatsView.swift in Sources */,
+				BD249D8E2D42FC3900412DEB /* LoopBarChartView.swift in Sources */,
 				CE7950242997D81700FA576E /* CGMSettingsView.swift in Sources */,
 				58237D9E2BCF0A6B00A47A79 /* PopupView.swift in Sources */,
 				BD793CB02CE7C61500D669AC /* OverrideRunStored+helper.swift in Sources */,
@@ -3988,7 +3992,7 @@
 				38AAF85525FFF846004AF583 /* CurrentGlucoseView.swift in Sources */,
 				041D1E995A6AE92E9289DC49 /* TreatmentsDataFlow.swift in Sources */,
 				DD32CF9E2CC824C5003686D6 /* TrioRemoteControl+Override.swift in Sources */,
-				BD249D922D42FC5300412DEB /* SectorChart.swift in Sources */,
+				BD249D922D42FC5300412DEB /* GlucoseSectorChart.swift in Sources */,
 				23888883D4EA091C88480FF2 /* TreatmentsProvider.swift in Sources */,
 				38E98A2D25F52DC400C0CED0 /* NSLocking+Extensions.swift in Sources */,
 				715120D22D3C2BB4005D9FB6 /* GlucoseNotificationsOption.swift in Sources */,

+ 37 - 6
Trio/Sources/Localizations/Main/Localizable.xcstrings

@@ -36770,6 +36770,9 @@
         }
       }
     },
+    "Bolus Insulin (U)" : {
+
+    },
     "Bolus limit cannot be fetched from phone!" : {
 
     },
@@ -41190,6 +41193,9 @@
         }
       }
     },
+    "CGM Connection Trace Chart" : {
+
+    },
     "CGM Glucose Value" : {
 
     },
@@ -44687,6 +44693,9 @@
     "Color Scheme Preference" : {
 
     },
+    "Coming soon." : {
+
+    },
     "Cone" : {
       "localizations" : {
         "ar" : {
@@ -101543,6 +101552,9 @@
         }
       }
     },
+    "Macro Nutrients (g)" : {
+
+    },
     "Main" : {
       "localizations" : {
         "ar" : {
@@ -106344,6 +106356,9 @@
         }
       }
     },
+    "Meal to Hypoglycemia/Hyperglycemia Distribution Chart" : {
+
+    },
     "Meals" : {
       "comment" : "History Mode"
     },
@@ -114042,9 +114057,6 @@
         }
       }
     },
-    "Not yet implemented" : {
-
-    },
     "Note" : {
       "comment" : "Food Type / Meal Note",
       "extractionState" : "manual",
@@ -127497,6 +127509,7 @@
     },
     "Readings" : {
       "comment" : "CGM readings in statView",
+      "extractionState" : "stale",
       "localizations" : {
         "ar" : {
           "stringUnit" : {
@@ -148627,6 +148640,9 @@
         }
       }
     },
+    "Swipe the chart to scroll through time." : {
+
+    },
     "Swipe to delete a single entry. Tap on it, to edit its time or rate." : {
       "localizations" : {
         "ar" : {
@@ -148945,6 +148961,15 @@
         }
       }
     },
+    "Tap and hold a bar to reveal more details." : {
+
+    },
+    "Tap and hold the AGP graph or Time-in-Range ring for details." : {
+
+    },
+    "Tap and hold the Time-in-Range ring to reveal more details." : {
+
+    },
     "Tap the button below to open your Nightscout instance in your iPhone's default browser." : {
 
     },
@@ -165042,9 +165067,6 @@
         }
       }
     },
-    "TODO: Meal to Hypoglycemia/Hyperglycemia Distribution" : {
-
-    },
     "Top target" : {
       "comment" : "Upper temp target limit",
       "extractionState" : "manual",
@@ -165478,6 +165500,9 @@
         }
       }
     },
+    "Total Daily Dose (U)" : {
+
+    },
     "Total Daily Dose:" : {
       "localizations" : {
         "ar" : {
@@ -166132,6 +166157,9 @@
         }
       }
     },
+    "Total:" : {
+
+    },
     "Tracks the carbohydrates you eat, entered to guide insulin dosing." : {
       "localizations" : {
         "ar" : {
@@ -168604,6 +168632,9 @@
         }
       }
     },
+    "Trio Up-Time Chart" : {
+
+    },
     "Trio v%@ (%@)" : {
       "localizations" : {
         "ar" : {

+ 38 - 1
Trio/Sources/Modules/Stat/View/ViewElements/StatsHelper.swift

@@ -1,7 +1,7 @@
 import Foundation
 import SwiftUI
 
-struct StatsHelper {
+struct StatChartUtils {
     /// Returns the time interval length for the visible domain based on the selected duration.
     /// - Parameter selectedDuration: The selected time interval for statistics.
     /// - Returns: The time interval in seconds.
@@ -109,4 +109,41 @@ struct StatsHelper {
 
         return startText == endText ? startText : "\(startText) - \(endText)"
     }
+
+    /// A helper function to create a `VStack` for each statistic.
+    ///
+    /// - Parameters:
+    ///   - title: The title of the statistic.
+    ///   - value: The formatted value to display.
+    /// - Returns: A `VStack` with the title and value.
+    static func statView(title: String, value: String) -> some View {
+        VStack(spacing: 5) {
+            Text(title)
+                .font(.subheadline)
+                .foregroundStyle(Color.secondary)
+            Text(value)
+        }
+    }
+
+    static func medianCalculation(array: [Int]) -> Double {
+        guard !array.isEmpty else { return 0 }
+        let sorted = array.sorted()
+        let length = array.count
+
+        if length % 2 == 0 {
+            return Double((sorted[length / 2 - 1] + sorted[length / 2]) / 2)
+        }
+        return Double(sorted[length / 2])
+    }
+
+    static func medianCalculationDouble(array: [Double]) -> Double {
+        guard !array.isEmpty else { return 0 }
+        let sorted = array.sorted()
+        let length = array.count
+
+        if length % 2 == 0 {
+            return (sorted[length / 2 - 1] + sorted[length / 2]) / 2
+        }
+        return sorted[length / 2]
+    }
 }

+ 58 - 15
Trio/Sources/Modules/Stat/View/StatRootView.swift

@@ -84,13 +84,27 @@ extension Stat {
 
             if state.glucoseFromPersistence.isEmpty {
                 ContentUnavailableView(
-                    "No Glucose Data",
+                    String(localized: "No Glucose Data"),
                     systemImage: "chart.bar.fill",
                     description: Text("Glucose statistics will appear here once data is available.")
                 )
             } else {
                 timeInRangeCard
                 glucoseStatsCard
+
+                HStack {
+                    var hintText: String {
+                        switch state.selectedGlucoseChartType {
+                        case .percentile:
+                            String(localized: "Tap and hold the AGP graph or Time-in-Range ring for details.")
+                        case .distribution:
+                            String(localized: "Tap and hold the Time-in-Range ring to reveal more details.")
+                        }
+                    }
+                    Image(systemName: "hand.draw.fill").foregroundStyle(Color.primary)
+                    Text(hintText).foregroundStyle(Color.secondary)
+                    Spacer()
+                }.font(.footnote)
             }
         }
 
@@ -123,7 +137,7 @@ extension Stat {
         private var glucoseStatsCard: some View {
             StatCard {
                 VStack(spacing: Constants.spacing) {
-                    SectorChart(
+                    GlucoseSectorChart(
                         highLimit: state.highLimit,
                         lowLimit: state.lowLimit,
                         units: state.units,
@@ -132,7 +146,7 @@ extension Stat {
 
                     Divider()
 
-                    BareStatisticsView.GlucoseMetricsView(
+                    GlucoseMetricsView(
                         highLimit: state.highLimit,
                         lowLimit: state.lowLimit,
                         units: state.units,
@@ -169,12 +183,12 @@ extension Stat {
                 case .totalDailyDose:
                     if state.dailyTDDStats.isEmpty {
                         ContentUnavailableView(
-                            "No TDD Data",
+                            String(localized: "No TDD Data"),
                             systemImage: "chart.bar.xaxis",
                             description: Text("Total Daily Doses will appear here once data is available.")
                         )
                     } else {
-                        TDDChartView(
+                        TotalDailyDoseChart(
                             selectedDuration: $state.selectedDurationForInsulinStats,
                             tddStats: state.selectedDurationForInsulinStats == .Day ?
                                 state.hourlyTDDStats : state.dailyTDDStats,
@@ -191,7 +205,7 @@ extension Stat {
                     // TODO: -
                     if state.dailyBolusStats.isEmpty || !hasBolusData {
                         ContentUnavailableView(
-                            "No Bolus Data",
+                            String(localized: "No Bolus Data"),
                             systemImage: "cross.vial",
                             description: Text("Bolus statistics will appear here once data is available.")
                         )
@@ -205,6 +219,14 @@ extension Stat {
                     }
                 }
             }
+
+            HStack {
+                Image(systemName: "hand.draw.fill").foregroundStyle(Color.primary)
+                VStack(alignment: .leading) {
+                    Text("Swipe the chart to scroll through time.")
+                    Text("Tap and hold a bar to reveal more details.")
+                }.foregroundStyle(Color.secondary)
+            }.font(.footnote)
         }
 
         @ViewBuilder var loopingView: some View {
@@ -232,7 +254,7 @@ extension Stat {
             case .loopingPerformance:
                 if state.loopStatRecords.isEmpty {
                     ContentUnavailableView(
-                        "No Loop Data",
+                        String(localized: "No Loop Data"),
                         systemImage: "clock.arrow.2.circlepath",
                         description: Text("Loop statistics will appear here once data is available.")
                     )
@@ -241,16 +263,26 @@ extension Stat {
                     loopStats
                 }
             case .trioUpTime:
-                Text("Not yet implemented")
+                // TODO: Trio Up-Time Chart
+                ContentUnavailableView(
+                    String(localized: "Coming soon."),
+                    systemImage: "hourglass",
+                    description: Text("Trio Up-Time Chart")
+                )
             case .cgmConnectionTrace:
-                Text("Not yet implemented")
+                // TODO: CGM Connection Trace Chart
+                ContentUnavailableView(
+                    String(localized: "Coming soon."),
+                    systemImage: "hourglass",
+                    description: Text("CGM Connection Trace Chart")
+                )
             }
         }
 
         private var loopsCard: some View {
             StatCard {
                 VStack(spacing: Constants.spacing) {
-                    LoopStatsView(
+                    LoopBarChartView(
                         loopStatRecords: state.loopStatRecords,
                         selectedDuration: state.selectedDurationForLoopStats,
                         statsData: state.loopStats
@@ -262,7 +294,7 @@ extension Stat {
         private var loopStats: some View {
             StatCard {
                 VStack(spacing: Constants.spacing) {
-                    BareStatisticsView.LoopsView(
+                    LoopStatsView(
                         highLimit: state.highLimit,
                         lowLimit: state.lowLimit,
                         units: state.units,
@@ -297,15 +329,13 @@ extension Stat {
             StatCard {
                 switch state.selectedMealChartType {
                 case .totalMeals:
-                    // TODO: -
                     var hasMealData: Bool {
                         state.dailyMealStats.contains { $0.carbs > 0 || $0.fat > 0 || $0.protein > 0 }
                     }
 
-                    // TODO: -
                     if state.dailyMealStats.isEmpty || !hasMealData {
                         ContentUnavailableView(
-                            "No Meal Data",
+                            String(localized: "No Meal Data"),
                             systemImage: "fork.knife",
                             description: Text("Meal statistics will appear here once data is available.")
                         )
@@ -318,9 +348,22 @@ extension Stat {
                         )
                     }
                 case .mealToHypoHyperDistribution:
-                    Text("TODO: Meal to Hypoglycemia/Hyperglycemia Distribution")
+                    // TODO: Meal to Hypoglycemia/Hyperglycemia Distribution
+                    ContentUnavailableView(
+                        String(localized: "Coming soon."),
+                        systemImage: "hourglass",
+                        description: Text("Meal to Hypoglycemia/Hyperglycemia Distribution Chart")
+                    )
                 }
             }
+
+            HStack {
+                Image(systemName: "hand.draw.fill").foregroundStyle(Color.primary)
+                VStack(alignment: .leading) {
+                    Text("Swipe the chart to scroll through time.")
+                    Text("Tap and hold a bar to reveal more details.")
+                }.foregroundStyle(Color.secondary)
+            }.font(.footnote)
         }
     }
 }

+ 0 - 339
Trio/Sources/Modules/Stat/View/ViewElements/BareStatisticsView.swift

@@ -1,339 +0,0 @@
-import CoreData
-import SwiftDate
-import SwiftUI
-
-struct BareStatisticsView {
-    // MARK: - Helper Functions
-
-    static func medianCalculation(array: [Int]) -> Double {
-        guard !array.isEmpty else { return 0 }
-        let sorted = array.sorted()
-        let length = array.count
-
-        if length % 2 == 0 {
-            return Double((sorted[length / 2 - 1] + sorted[length / 2]) / 2)
-        }
-        return Double(sorted[length / 2])
-    }
-
-    static func medianCalculationDouble(array: [Double]) -> Double {
-        guard !array.isEmpty else { return 0 }
-        let sorted = array.sorted()
-        let length = array.count
-
-        if length % 2 == 0 {
-            return (sorted[length / 2 - 1] + sorted[length / 2]) / 2
-        }
-        return sorted[length / 2]
-    }
-
-    struct GlucoseMetricsView: View {
-        let highLimit: Decimal
-        let lowLimit: Decimal
-        let units: GlucoseUnits
-        let eA1cDisplayUnit: EstimatedA1cDisplayUnit
-        let glucose: [GlucoseStored]
-
-        var body: some View {
-            VStack(alignment: .leading) {
-                HStack(spacing: 40) {
-                    let useUnit: GlucoseUnits = {
-                        if eA1cDisplayUnit == .mmolMol { return .mmolL }
-                        else { return .mgdL }
-                    }()
-
-                    let glucoseStats = calculateGlucoseStatistics()
-                    // First date
-                    let previous = glucose.last?.date ?? Date()
-                    // Last date (recent)
-                    let current = glucose.first?.date ?? Date()
-                    // Total time in days
-                    let numberOfDays = (current - previous).timeInterval / 8.64E4
-
-                    let eA1cString = (
-                        useUnit == .mmolL ? glucoseStats.ifcc
-                            .formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) : glucoseStats.ngsp
-                            .formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))
-                            + "%"
-                    )
-                    VStack(spacing: 5) {
-                        Text("eA1c").font(.subheadline).foregroundColor(.secondary)
-                        Text(eA1cString)
-                    }
-                    VStack(spacing: 5) {
-                        Text("GMI").font(.subheadline).foregroundColor(.secondary)
-                        Text(glucoseStats.gmi.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + "%")
-                    }
-                    VStack(spacing: 5) {
-                        Text("SD").font(.subheadline).foregroundColor(.secondary)
-                        Text(
-                            glucoseStats.sd
-                                .formatted(
-                                    .number.grouping(.never).rounded()
-                                        .precision(.fractionLength(units == .mmolL ? 1 : 0))
-                                )
-                        )
-                    }
-                    VStack(spacing: 5) {
-                        Text("CV").font(.subheadline).foregroundColor(.secondary)
-                        Text(glucoseStats.cv.formatted(.number.grouping(.never).rounded().precision(.fractionLength(0))))
-                    }
-                    VStack(spacing: 5) {
-                        Text("Days").font(.subheadline).foregroundColor(.secondary)
-                        Text(numberOfDays.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))))
-                    }
-                }
-            }
-        }
-
-        func calculateGlucoseStatistics()
-            -> (
-                ifcc: Double,
-                ngsp: Double,
-                gmi: Double,
-                average: Double,
-                median: Double,
-                sd: Double,
-                cv: Double,
-                readings: Double
-            )
-        {
-            // First recorded glucose date
-            let previous = glucose.last?.date ?? Date()
-            // Most recent glucose date
-            let current = glucose.first?.date ?? Date()
-            // Total duration in days
-            let numberOfDays = (current - previous).timeInterval / 8.64E4
-
-            // Avoid division by zero, ensure at least 1 day
-            let denominator = numberOfDays < 1 ? 1 : numberOfDays
-
-            // Extract glucose values as an array of integers
-            let justGlucoseArray = glucose.compactMap({ each in Int(each.glucose as Int16) })
-            let sumReadings = justGlucoseArray.reduce(0, +)
-            let countReadings = justGlucoseArray.count
-
-            // Calculate the mean (average) glucose value
-            let glucoseAverage = Double(sumReadings) / Double(countReadings)
-            // Calculate the median glucose value
-            let medianGlucose = BareStatisticsView.medianCalculation(array: justGlucoseArray)
-
-            // Variables to store calculated values
-            var eA1cNGSP = 0.0 // eA1c in NGSP (%) standard (CGM-based)
-            var eA1cIFCC = 0.0 // eA1c in IFCC (mmol/mol) standard (CGM-based)
-            var GMIValue = 0.0 // Glucose Management Index
-
-            if numberOfDays > 0 {
-                // **eA1c NGSP Calculation**: Estimated A1c in percentage (%)
-                // Based on CGM readings, using the DCCT-derived formula:
-                // eA1c NGSP (%) = (Average Glucose mg/dL + 46.7) / 28.7
-                eA1cNGSP = (glucoseAverage + 46.7) / 28.7
-
-                // **eA1c IFCC Calculation**: Conversion from eA1c NGSP to eA1c IFCC (mmol/mol)
-                // eA1c IFCC (mmol/mol) = 10.929 * (eA1c NGSP - 2.152)
-                // This conversion aligns with the IFCC standard.
-                eA1cIFCC = 10.929 * (eA1cNGSP - 2.152)
-
-                // **Glucose Management Index (GMI)**: Alternative eA1c estimate based on CGM data
-                // GMI = 3.31 + (0.02392 × Average Glucose mg/dL)
-                GMIValue = 3.31 + 0.02392 * glucoseAverage
-            }
-
-            // Calculate Standard Deviation (SD) and Coefficient of Variation (CV)
-            var sumOfSquares = 0.0
-            for value in justGlucoseArray {
-                sumOfSquares += pow(Double(value) - glucoseAverage, 2)
-            }
-
-            var sd = 0.0
-            var cv = 0.0
-
-            if glucoseAverage > 0 {
-                // Standard deviation: Measure of glucose variability
-                sd = sqrt(sumOfSquares / Double(countReadings))
-                // Coefficient of variation (CV %): Variability relative to mean glucose
-                cv = sd / glucoseAverage * 100
-            }
-
-            return (
-                ifcc: eA1cIFCC, // eA1c IFCC (mmol/mol)
-                ngsp: eA1cNGSP, // eA1c NGSP (%)
-                gmi: GMIValue, // Glucose Management Index
-                average: glucoseAverage * (units == .mmolL ? 0.0555 : 1), // Convert if needed
-                median: medianGlucose * (units == .mmolL ? 0.0555 : 1),
-                sd: sd * (units == .mmolL ? 0.0555 : 1),
-                cv: cv,
-                readings: Double(countReadings) / denominator // Readings per day
-            )
-        }
-    }
-
-    struct BloodGlucoseView: View {
-        let highLimit: Decimal
-        let lowLimit: Decimal
-        let units: GlucoseUnits
-        let eA1cDisplayUnit: EstimatedA1cDisplayUnit
-        let glucose: [GlucoseStored]
-
-        var body: some View {
-            bloodGlucose
-        }
-
-        private var bloodGlucose: some View {
-            HStack(spacing: 30) {
-                let bgs = glucoseStats()
-
-                // First date
-                let previous = glucose.last?.date ?? Date()
-                // Last date (recent)
-                let current = glucose.first?.date ?? Date()
-                // Total time in days
-                let numberOfDays = (current - previous).timeInterval / 8.64E4
-
-                VStack(spacing: 5) {
-                    Text(numberOfDays < 1 ? "Readings" : "Readings / 24h").font(.subheadline)
-                        .foregroundColor(.secondary)
-                    Text(bgs.readings.formatted(.number.grouping(.never).rounded().precision(.fractionLength(0))))
-                }
-                VStack(spacing: 5) {
-                    Text("Average").font(.subheadline).foregroundColor(.secondary)
-                    Text(
-                        bgs.average
-                            .formatted(
-                                .number.grouping(.never).rounded()
-                                    .precision(.fractionLength(units == .mmolL ? 1 : 0))
-                            )
-                    )
-                }
-                VStack(spacing: 5) {
-                    Text("Median").font(.subheadline).foregroundColor(.secondary)
-                    Text(
-                        bgs.median
-                            .formatted(
-                                .number.grouping(.never).rounded()
-                                    .precision(.fractionLength(units == .mmolL ? 1 : 0))
-                            )
-                    )
-                }
-            }
-        }
-
-        func glucoseStats()
-            -> (ifcc: Double, ngsp: Double, average: Double, median: Double, sd: Double, cv: Double, readings: Double)
-        {
-            // First date
-            let previous = glucose.last?.date ?? Date()
-            // Last date (recent)
-            let current = glucose.first?.date ?? Date()
-            // Total time in days
-            let numberOfDays = (current - previous).timeInterval / 8.64E4
-
-            let denominator = numberOfDays < 1 ? 1 : numberOfDays
-
-            let justGlucoseArray = glucose.compactMap({ each in Int(each.glucose as Int16) })
-            let sumReadings = justGlucoseArray.reduce(0, +)
-            let countReadings = justGlucoseArray.count
-
-            let glucoseAverage = Double(sumReadings) / Double(countReadings)
-            let medianGlucose = BareStatisticsView.medianCalculation(array: justGlucoseArray)
-
-            var NGSPa1CStatisticValue = 0.0
-            var IFCCa1CStatisticValue = 0.0
-
-            if numberOfDays > 0 {
-                NGSPa1CStatisticValue = (glucoseAverage + 46.7) / 28.7
-                IFCCa1CStatisticValue = 10.929 * (NGSPa1CStatisticValue - 2.152)
-            }
-
-            var sumOfSquares = 0.0
-            for array in justGlucoseArray {
-                sumOfSquares += pow(Double(array) - Double(glucoseAverage), 2)
-            }
-
-            var sd = 0.0
-            var cv = 0.0
-
-            if glucoseAverage > 0 {
-                sd = sqrt(sumOfSquares / Double(countReadings))
-                cv = sd / Double(glucoseAverage) * 100
-            }
-
-            return (
-                ifcc: IFCCa1CStatisticValue,
-                ngsp: NGSPa1CStatisticValue,
-                average: glucoseAverage * (units == .mmolL ? 0.0555 : 1),
-                median: medianGlucose * (units == .mmolL ? 0.0555 : 1),
-                sd: sd * (units == .mmolL ? 0.0555 : 1),
-                cv: cv,
-                readings: Double(countReadings) / denominator
-            )
-        }
-    }
-
-    struct LoopsView: View {
-        let highLimit: Decimal
-        let lowLimit: Decimal
-        let units: GlucoseUnits
-        let eA1cDisplayUnit: EstimatedA1cDisplayUnit
-        let loopStatRecords: [LoopStatRecord]
-
-        var body: some View {
-            loops
-        }
-
-        private var loops: some View {
-            let loops = loopStatRecords
-            // First date
-            let previous = loops.last?.end ?? Date()
-            // Last date (recent)
-            let current = loops.first?.start ?? Date()
-            // Total time in days
-            let totalTime = (current - previous).timeInterval / 8.64E4
-
-            let durationArray = loops.compactMap({ each in each.duration })
-            let durationArrayCount = durationArray.count
-            // var durationAverage = durationArray.reduce(0, +) / Double(durationArrayCount)
-            let medianDuration = medianCalculationDouble(array: durationArray)
-            let successsNR = loops.compactMap({ each in each.loopStatus }).filter({ each in each!.contains("Success") }).count
-            let errorNR = durationArrayCount - successsNR
-            let total = Double(successsNR + errorNR) == 0 ? 1 : Double(successsNR + errorNR)
-            let successRate: Double? = (Double(successsNR) / total) * 100
-            let loopNr = totalTime <= 1 ? total : round(total / (totalTime != 0 ? totalTime : 1))
-            let intervalArray = loops.compactMap({ each in each.interval as Double })
-            let count = intervalArray.count != 0 ? intervalArray.count : 1
-            let intervalAverage = intervalArray.reduce(0, +) / Double(count)
-            // let maximumInterval = intervalArray.max()
-            // let minimumInterval = intervalArray.min()
-            return VStack(spacing: 10) {
-                HStack(spacing: 35) {
-                    VStack(spacing: 5) {
-                        Text("Loops").font(.subheadline).foregroundColor(.primary)
-                        Text(loopNr.formatted())
-                    }
-                    VStack(spacing: 5) {
-                        Text("Interval").font(.subheadline).foregroundColor(.primary)
-                        Text(intervalAverage.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + "m")
-                    }
-                    VStack(spacing: 5) {
-                        Text("Duration").font(.subheadline).foregroundColor(.primary)
-                        Text(
-                            (medianDuration / 1000)
-                                .formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + "s"
-                        )
-                    }
-                    VStack(spacing: 5) {
-                        Text("Success").font(.subheadline).foregroundColor(.primary)
-                        Text(
-                            ((successRate ?? 100) / 100)
-                                .formatted(.percent.grouping(.never).rounded().precision(.fractionLength(1)))
-                        )
-                    }
-                }
-            }
-        }
-
-        private func medianCalculationDouble(array: [Double]) -> Double {
-            BareStatisticsView.medianCalculationDouble(array: array)
-        }
-    }
-}

+ 22 - 16
Trio/Sources/Modules/Stat/View/ViewElements/BolusStatsView.swift

@@ -24,7 +24,7 @@ struct BolusStatsView: View {
 
     /// Computes the visible date range based on the current scroll position.
     private var visibleDateRange: (start: Date, end: Date) {
-        StatsHelper.visibleDateRange(from: scrollPosition, for: selectedDuration)
+        StatChartUtils.visibleDateRange(from: scrollPosition, for: selectedDuration)
     }
 
     /// Retrieves the bolus statistic for a given date.
@@ -32,7 +32,7 @@ struct BolusStatsView: View {
     /// - Returns: The `BolusStats` object if available, otherwise `nil`.
     private func getBolusForDate(_ date: Date) -> BolusStats? {
         bolusStats.first { stat in
-            StatsHelper.isSameTimeUnit(stat.date, date, for: selectedDuration)
+            StatChartUtils.isSameTimeUnit(stat.date, date, for: selectedDuration)
         }
     }
 
@@ -66,10 +66,10 @@ struct BolusStatsView: View {
             Spacer()
 
             Text(
-                StatsHelper
+                StatChartUtils
                     .formatVisibleDateRange(from: visibleDateRange.start, to: visibleDateRange.end, for: selectedDuration)
             )
-            .font(.footnote)
+            .font(.callout)
             .foregroundStyle(.secondary)
         }
     }
@@ -80,7 +80,7 @@ struct BolusStatsView: View {
             chartsView
         }
         .onAppear {
-            scrollPosition = StatsHelper.getInitialScrollPosition(for: selectedDuration)
+            scrollPosition = StatChartUtils.getInitialScrollPosition(for: selectedDuration)
             updateAverages()
         }
         .onChange(of: scrollPosition) {
@@ -90,7 +90,7 @@ struct BolusStatsView: View {
         }
         .onChange(of: selectedDuration) {
             Task {
-                scrollPosition = StatsHelper.getInitialScrollPosition(for: selectedDuration)
+                scrollPosition = StatChartUtils.getInitialScrollPosition(for: selectedDuration)
                 updateAverages()
             }
         }
@@ -109,7 +109,7 @@ struct BolusStatsView: View {
                 .position(by: .value("Type", "Boluses"))
                 .opacity(
                     selectedDate.map { date in
-                        StatsHelper.isSameTimeUnit(stat.date, date, for: selectedDuration) ? 1 : 0.3
+                        StatChartUtils.isSameTimeUnit(stat.date, date, for: selectedDuration) ? 1 : 0.3
                     } ?? 1
                 )
 
@@ -122,7 +122,7 @@ struct BolusStatsView: View {
                 .position(by: .value("Type", "Boluses"))
                 .opacity(
                     selectedDate.map { date in
-                        StatsHelper.isSameTimeUnit(stat.date, date, for: selectedDuration) ? 1 : 0.3
+                        StatChartUtils.isSameTimeUnit(stat.date, date, for: selectedDuration) ? 1 : 0.3
                     } ?? 1
                 )
                 // Correction Bolus Bar
@@ -134,7 +134,7 @@ struct BolusStatsView: View {
                 .position(by: .value("Type", "Boluses"))
                 .opacity(
                     selectedDate.map { date in
-                        StatsHelper.isSameTimeUnit(stat.date, date, for: selectedDuration) ? 1 : 0.3
+                        StatChartUtils.isSameTimeUnit(stat.date, date, for: selectedDuration) ? 1 : 0.3
                     } ?? 1
                 )
             }
@@ -165,13 +165,19 @@ struct BolusStatsView: View {
             AxisMarks(position: .trailing) { value in
                 if let amount = value.as(Double.self) {
                     AxisValueLabel {
-                        Text(amount.formatted(.number.precision(.fractionLength(0))) + " U")
+                        Text(amount.formatted(.number.precision(.fractionLength(0))))
                             .font(.footnote)
                     }
                     AxisGridLine()
                 }
             }
         }
+        .chartYAxisLabel(alignment: .trailing) {
+            Text("Bolus Insulin (U)")
+                .foregroundStyle(.primary)
+                .font(.footnote)
+                .padding(.vertical, 3)
+        }
         .chartXAxis {
             AxisMarks(preset: .aligned, values: .stride(by: selectedDuration == .Day ? .hour : .day)) { value in
                 if let date = value.as(Date.self) {
@@ -181,25 +187,25 @@ struct BolusStatsView: View {
                     switch selectedDuration {
                     case .Day:
                         if hour % 6 == 0 { // Show only every 6 hours
-                            AxisValueLabel(format: StatsHelper.dateFormat(for: selectedDuration), centered: true)
+                            AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedDuration), centered: true)
                                 .font(.footnote)
                             AxisGridLine()
                         }
                     case .Month:
                         if day % 5 == 0 { // Only show every 5th day
-                            AxisValueLabel(format: StatsHelper.dateFormat(for: selectedDuration), centered: true)
+                            AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedDuration), centered: true)
                                 .font(.footnote)
                             AxisGridLine()
                         }
                     case .Total:
                         // Only show January, April, July, October
                         if day == 1 && Calendar.current.component(.month, from: date) % 3 == 1 {
-                            AxisValueLabel(format: StatsHelper.dateFormat(for: selectedDuration), centered: true)
+                            AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedDuration), centered: true)
                                 .font(.footnote)
                             AxisGridLine()
                         }
                     default:
-                        AxisValueLabel(format: StatsHelper.dateFormat(for: selectedDuration), centered: true)
+                        AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedDuration), centered: true)
                             .font(.footnote)
                         AxisGridLine()
                     }
@@ -215,11 +221,11 @@ struct BolusStatsView: View {
                     DateComponents(minute: 0) : // Align to next hour for Day view
                     DateComponents(hour: 0), // Align to start of day for other views
                 majorAlignment: .matching(
-                    StatsHelper.alignmentComponents(for: selectedDuration)
+                    StatChartUtils.alignmentComponents(for: selectedDuration)
                 )
             )
         )
-        .chartXVisibleDomain(length: StatsHelper.visibleDomainLength(for: selectedDuration))
+        .chartXVisibleDomain(length: StatChartUtils.visibleDomainLength(for: selectedDuration))
         .frame(height: 250)
     }
 }

+ 123 - 0
Trio/Sources/Modules/Stat/View/ViewElements/GlucoseMetricsView.swift

@@ -0,0 +1,123 @@
+import CoreData
+import SwiftDate
+import SwiftUI
+
+/// A SwiftUI view displaying various glucose-related statistics based on stored glucose readings.
+struct GlucoseMetricsView: View {
+    /// The upper glucose limit for evaluation.
+    let highLimit: Decimal
+    /// The lower glucose limit for 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
+    /// A list of stored glucose readings.
+    let glucose: [GlucoseStored]
+
+    /// The main body of the `GlucoseMetricsView`, displaying glucose-related statistics.
+    var body: some View {
+        let preferredUnit: GlucoseUnits = eA1cDisplayUnit == .mmolMol ? .mmolL : .mgdL
+
+        let glucoseStats = calculateGlucoseStatistics()
+
+        // Determine the time range of the stored glucose data
+        let earliestDate = glucose.last?.date ?? Date()
+        let latestDate = glucose.first?.date ?? Date()
+        let totalDays = (latestDate - earliestDate).timeInterval / 86400
+
+        // Format glucose statistics based on the selected unit
+        let eA1cString = preferredUnit == .mmolL
+            ? glucoseStats.ifcc.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))
+            : glucoseStats.ngsp.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + "%"
+
+        let gmiString = glucoseStats.gmi.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + "%"
+        let standardDeviationString = units == .mgdL ? glucoseStats.sd.formatted(
+            .number.grouping(.never).rounded().precision(.fractionLength(0))
+        ) : glucoseStats.sd.formatted(
+            .number.grouping(.never).rounded().precision(.fractionLength(1))
+        )
+        let coefficientOfVariationString = glucoseStats.cv
+            .formatted(.number.grouping(.never).rounded().precision(.fractionLength(0)))
+        let daysTrackedString = totalDays.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))
+
+        VStack(alignment: .leading) {
+            HStack(spacing: 40) {
+                StatChartUtils.statView(title: String(localized: "eA1c"), value: eA1cString)
+                StatChartUtils.statView(title: String(localized: "GMI"), value: gmiString)
+                StatChartUtils.statView(title: String(localized: "SD"), value: standardDeviationString)
+                StatChartUtils.statView(title: String(localized: "CV"), value: coefficientOfVariationString)
+                StatChartUtils.statView(title: String(localized: "Days"), value: daysTrackedString)
+            }
+        }
+    }
+
+    /// Computes various statistical metrics from stored glucose readings, including:
+    /// - Estimated A1c in NGSP (%) and IFCC (mmol/mol)
+    /// - Glucose Management Index (GMI)
+    /// - Average and median glucose levels
+    /// - Standard deviation (SD) and coefficient of variation (CV)
+    /// - Number of readings per day
+    ///
+    /// - Returns: A tuple containing glucose statistics.
+    func calculateGlucoseStatistics() -> (
+        ifcc: Double, ngsp: Double, gmi: Double, average: Double,
+        median: Double, sd: Double, cv: Double, readingsPerDay: Double
+    ) {
+        // Determine the date range of the glucose data
+        let earliestDate = glucose.last?.date ?? Date()
+        let latestDate = glucose.first?.date ?? Date()
+        let totalDays = (latestDate - earliestDate).timeInterval / 86400
+
+        // Ensure at least one day to avoid division by zero
+        let daysCount = max(totalDays, 1)
+
+        // Extract glucose values as an array of integers
+        let glucoseValues = glucose.compactMap { Int($0.glucose as Int16) }
+        let totalReadings = glucoseValues.count
+        let sumOfReadings = glucoseValues.reduce(0, +)
+
+        // Compute mean (average) glucose level
+        let meanGlucose = Double(sumOfReadings) / Double(totalReadings)
+        // Compute median glucose level
+        let medianGlucose = StatChartUtils.medianCalculation(array: glucoseValues)
+
+        // Estimated A1c and Glucose Management Index (GMI) calculations
+        var eA1cNGSP = 0.0 // eA1c NGSP (%)
+        var eA1cIFCC = 0.0 // eA1c IFCC (mmol/mol)
+        var gmiValue = 0.0 // Glucose Management Index (GMI)
+
+        if totalDays > 0 {
+            // **eA1c NGSP Calculation** (CGM-based)
+            // eA1c NGSP (%) = (Average Glucose mg/dL + 46.7) / 28.7
+            eA1cNGSP = (meanGlucose + 46.7) / 28.7
+
+            // **eA1c IFCC Calculation**
+            // eA1c IFCC (mmol/mol) = 10.929 * (eA1c NGSP - 2.152)
+            eA1cIFCC = 10.929 * (eA1cNGSP - 2.152)
+
+            // **Glucose Management Index (GMI)**
+            // GMI = 3.31 + (0.02392 × Average Glucose mg/dL)
+            gmiValue = 3.31 + (0.02392 * meanGlucose)
+        }
+
+        // Compute Standard Deviation (SD) and Coefficient of Variation (CV)
+        let sumOfSquaredDifferences = glucoseValues.reduce(0.0) { sum, value in
+            sum + pow(Double(value) - meanGlucose, 2)
+        }
+
+        let standardDeviation = sqrt(sumOfSquaredDifferences / Double(totalReadings))
+        let coefficientOfVariation = (meanGlucose > 0) ? (standardDeviation / meanGlucose) * 100 : 0.0
+
+        return (
+            ifcc: eA1cIFCC, // eA1c in IFCC (mmol/mol)
+            ngsp: eA1cNGSP, // eA1c in NGSP (%)
+            gmi: gmiValue, // Glucose Management Index
+            average: Double(units == .mgdL ? meanGlucose.asMgdL : meanGlucose.asMmolL),
+            median: Double(units == .mgdL ? medianGlucose.asMgdL : medianGlucose.asMmolL),
+            sd: Double(units == .mgdL ? standardDeviation.asMgdL : standardDeviation.asMmolL),
+            cv: coefficientOfVariation, // CV is already in percentage format
+            readingsPerDay: Double(totalReadings) / Double(daysCount)
+        )
+    }
+}

+ 9 - 18
Trio/Sources/Modules/Stat/View/ViewElements/GlucosePercentileChart.swift

@@ -63,7 +63,7 @@ struct GlucosePercentileChart: View {
                         yEnd: .value("75th Percentile", stats.percentile75),
                         series: .value("25-75", "25-75")
                     )
-                    .foregroundStyle(.blue.opacity(stats.median > 0 ? 0.3 : 0))
+                    .foregroundStyle(.blue.opacity(stats.median > 0 ? 0.4 : 0))
 
                     // Median line
                     if stats.median > 0 {
@@ -130,37 +130,28 @@ struct GlucosePercentileChart: View {
                 }
             }
             .chartXSelection(value: $selection.animation(.easeInOut))
-            .frame(height: 200)
+            .frame(height: 180)
             legend
         }
     }
 
     /// A view displaying the legend for the chart.
     private var legend: some View {
-        HStack(spacing: 20) {
-            VStack {
-                legendItem(color: .blue.opacity(0.2), text: "10% - 90%")
-                legendItem(color: .blue.opacity(0.3), text: "25% - 75%")
-            }
-            legendItem(color: .blue, text: "Median")
-            VStack {
-                legendItem(color: .orange, text: "High Limit")
-                legendItem(color: .red, text: "Low Limit")
-            }
+        HStack {
+            legendItem(color: .blue.opacity(0.2), text: "10-90%", icon: "rectangle.fill")
+            legendItem(color: .blue.opacity(0.4), text: "25-75%", icon: "rectangle.fill")
+            legendItem(color: .blue, text: "Median", icon: "rectangle.fill")
         }
-        .padding(.horizontal)
     }
 
     /// Creates a legend item with a given color and text.
-    private func legendItem(color: Color, text: String) -> some View {
+    private func legendItem(color: Color, text: String, icon: String) -> some View {
         HStack(spacing: 8) {
-            Rectangle()
-                .frame(width: 20, height: 8)
+            Image(systemName: icon)
                 .foregroundStyle(color)
             Text(text)
-                .font(.caption)
                 .foregroundStyle(.secondary)
-        }
+        }.font(.caption)
     }
 }
 

+ 4 - 6
Trio/Sources/Modules/Stat/View/ViewElements/SectorChart.swift

@@ -3,7 +3,7 @@ import CoreData
 import SwiftDate
 import SwiftUI
 
-struct SectorChart: View {
+struct GlucoseSectorChart: View {
     let highLimit: Decimal
     let lowLimit: Decimal
     let units: GlucoseUnits
@@ -39,7 +39,7 @@ struct SectorChart: View {
             let sumReadings = justGlucoseArray.reduce(0, +)
 
             let glucoseAverage = Decimal(sumReadings) / total
-            let medianGlucose = BareStatisticsView.medianCalculation(array: justGlucoseArray)
+            let medianGlucose = StatChartUtils.medianCalculation(array: justGlucoseArray)
 
             let lowPercentage = Decimal(low) / total * 100
             let tightPercentage = Decimal(tight) / total * 100
@@ -276,7 +276,7 @@ struct SectorChart: View {
 
         let total = values.reduce(0, +)
         let average = Decimal(total / values.count)
-        let median = Decimal(BareStatisticsView.medianCalculation(array: values))
+        let median = Decimal(StatChartUtils.medianCalculation(array: values))
 
         let sumOfSquares = values.reduce(0.0) { sum, value in
             sum + pow(Double(value) - Double(average), 2)
@@ -292,9 +292,7 @@ struct SectorChart: View {
     private func formatSD(_ sd: Double) -> String {
         units == .mgdL ? sd.formatted(
             .number.grouping(.never).rounded().precision(.fractionLength(0))
-        ) : sd.asMmolL.formatted(
-            .number.grouping(.never).rounded().precision(.fractionLength(1))
-        )
+        ) : sd.formattedAsMmolL
     }
 
     /// Formats a glucose value based on the current units.

+ 79 - 0
Trio/Sources/Modules/Stat/View/ViewElements/LoopBarChartView.swift

@@ -0,0 +1,79 @@
+import Charts
+import SwiftUI
+
+struct LoopBarChartView: View {
+    let loopStatRecords: [LoopStatRecord]
+    let selectedDuration: Stat.StateModel.Duration
+    let statsData: [(category: String, count: Int, percentage: 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)
+                )
+                .cornerRadius(5)
+                .foregroundStyle(data.category == "Successful Loops" ? Color.blue : Color.green)
+                .annotation(position: .overlay) {
+                    HStack {
+                        Text(annotationText(for: data))
+                            .font(.callout)
+                            .foregroundStyle(.white)
+                    }
+                }
+            }
+            .chartYAxis {
+                AxisMarks { value in
+                    if let category = value.as(String.self) {
+                        AxisValueLabel {
+                            Text(category)
+                                .font(.footnote)
+                        }
+                    }
+                }
+            }
+            .chartXAxis {
+                AxisMarks(position: .bottom) { value in
+                    if let percentage = value.as(Double.self) {
+                        AxisValueLabel {
+                            Text("\(Int(percentage))%")
+                                .font(.footnote)
+                        }
+                        AxisGridLine()
+                    }
+                }
+            }
+            .chartXScale(domain: 0 ... 100)
+            .frame(height: 200)
+            .padding()
+        }
+    }
+
+    private func annotationText(for data: (category: String, count: Int, percentage: Double)) -> String {
+        if data.category == "Successful Loops" {
+            switch selectedDuration {
+            case .Day,
+                 .Today:
+                return "\(data.count) Loops"
+            case .Month,
+                 .Total,
+                 .Week:
+                let maxLoopsPerDay = 288.0
+                let averageLoopsPerDay = Double(data.count) / maxLoopsPerDay * 100
+                return "\(Int(round(averageLoopsPerDay))) Loops per day"
+            }
+        } else {
+            // For Glucose Count, show different text based on duration
+            switch selectedDuration {
+            case .Day,
+                 .Today:
+                return "\(data.count) Readings"
+            case .Month,
+                 .Total,
+                 .Week:
+                return "\(data.count) Readings per day"
+            }
+        }
+    }
+}

+ 63 - 68
Trio/Sources/Modules/Stat/View/ViewElements/LoopStatsView.swift

@@ -1,79 +1,74 @@
-import Charts
+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]
-    let selectedDuration: Stat.StateModel.Duration
-    let statsData: [(category: String, count: Int, percentage: Double)]
 
+    /// The main body of the `LoopStatsView`, displaying loop statistics.
     var body: some View {
-        VStack(spacing: 20) {
-            Chart(statsData, id: \.category) { data in
-                BarMark(
-                    x: .value("Percentage", data.percentage),
-                    y: .value("Category", data.category)
-                )
-                .cornerRadius(5)
-                .foregroundStyle(data.category == "Successful Loops" ? Color.blue : Color.green)
-                .annotation(position: .overlay) {
-                    HStack {
-                        Text(annotationText(for: data))
-                            .font(.callout)
-                            .foregroundStyle(.white)
-                    }
-                }
-            }
-            .chartYAxis {
-                AxisMarks { value in
-                    if let category = value.as(String.self) {
-                        AxisValueLabel {
-                            Text(category)
-                                .font(.footnote)
-                        }
-                    }
-                }
-            }
-            .chartXAxis {
-                AxisMarks(position: .bottom) { value in
-                    if let percentage = value.as(Double.self) {
-                        AxisValueLabel {
-                            Text("\(Int(percentage))%")
-                                .font(.footnote)
-                        }
-                        AxisGridLine()
-                    }
-                }
-            }
-            .chartXScale(domain: 0 ... 100)
-            .frame(height: 200)
-            .padding()
-        }
+        loops
     }
 
-    private func annotationText(for data: (category: String, count: Int, percentage: Double)) -> String {
-        if data.category == "Successful Loops" {
-            switch selectedDuration {
-            case .Day,
-                 .Today:
-                return "\(data.count) Loops"
-            case .Month,
-                 .Total,
-                 .Week:
-                let maxLoopsPerDay = 288.0
-                let averageLoopsPerDay = Double(data.count) / maxLoopsPerDay * 100
-                return "\(Int(round(averageLoopsPerDay))) Loops per day"
-            }
-        } else {
-            // For Glucose Count, show different text based on duration
-            switch selectedDuration {
-            case .Day,
-                 .Today:
-                return "\(data.count) Readings"
-            case .Month,
-                 .Total,
-                 .Week:
-                return "\(data.count) Readings per day"
-            }
+    /// 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()
     }
 }

+ 23 - 17
Trio/Sources/Modules/Stat/View/ViewElements/MealStatsView.swift

@@ -24,7 +24,7 @@ struct MealStatsView: View {
 
     /// Computes the visible date range based on the current scroll position.
     private var visibleDateRange: (start: Date, end: Date) {
-        StatsHelper.visibleDateRange(from: scrollPosition, for: selectedDuration)
+        StatChartUtils.visibleDateRange(from: scrollPosition, for: selectedDuration)
     }
 
     /// Retrieves the meal statistic for a given date.
@@ -32,7 +32,7 @@ struct MealStatsView: View {
     /// - Returns: The `MealStats` object if available, otherwise `nil`.
     private func getMealForDate(_ date: Date) -> MealStats? {
         mealStats.first { stat in
-            StatsHelper.isSameTimeUnit(stat.date, date, for: selectedDuration)
+            StatChartUtils.isSameTimeUnit(stat.date, date, for: selectedDuration)
         }
     }
 
@@ -68,21 +68,21 @@ struct MealStatsView: View {
             Spacer()
 
             Text(
-                StatsHelper
+                StatChartUtils
                     .formatVisibleDateRange(from: visibleDateRange.start, to: visibleDateRange.end, for: selectedDuration)
             )
-            .font(.footnote)
+            .font(.callout)
             .foregroundStyle(.secondary)
         }
     }
 
     var body: some View {
         VStack(alignment: .leading, spacing: 8) {
-            statsView
+            statsView.padding(.bottom)
             chartsView
         }
         .onAppear {
-            scrollPosition = StatsHelper.getInitialScrollPosition(for: selectedDuration)
+            scrollPosition = StatChartUtils.getInitialScrollPosition(for: selectedDuration)
             updateAverages()
         }
         .onChange(of: scrollPosition) {
@@ -92,7 +92,7 @@ struct MealStatsView: View {
         }
         .onChange(of: selectedDuration) {
             Task {
-                scrollPosition = StatsHelper.getInitialScrollPosition(for: selectedDuration)
+                scrollPosition = StatChartUtils.getInitialScrollPosition(for: selectedDuration)
                 updateAverages()
             }
         }
@@ -111,7 +111,7 @@ struct MealStatsView: View {
                 .position(by: .value("Type", "Macros"))
                 .opacity(
                     selectedDate.map { date in
-                        StatsHelper.isSameTimeUnit(stat.date, date, for: selectedDuration) ? 1 : 0.3
+                        StatChartUtils.isSameTimeUnit(stat.date, date, for: selectedDuration) ? 1 : 0.3
                     } ?? 1
                 )
                 if state.useFPUconversion {
@@ -124,7 +124,7 @@ struct MealStatsView: View {
                     .position(by: .value("Type", "Macros"))
                     .opacity(
                         selectedDate.map { date in
-                            StatsHelper.isSameTimeUnit(stat.date, date, for: selectedDuration) ? 1 : 0.3
+                            StatChartUtils.isSameTimeUnit(stat.date, date, for: selectedDuration) ? 1 : 0.3
                         } ?? 1
                     )
                     // Protein Bar (top)
@@ -136,7 +136,7 @@ struct MealStatsView: View {
                     .position(by: .value("Type", "Macros"))
                     .opacity(
                         selectedDate.map { date in
-                            StatsHelper.isSameTimeUnit(stat.date, date, for: selectedDuration) ? 1 : 0.3
+                            StatChartUtils.isSameTimeUnit(stat.date, date, for: selectedDuration) ? 1 : 0.3
                         } ?? 1
                     )
                 }
@@ -174,13 +174,19 @@ struct MealStatsView: View {
             AxisMarks(position: .trailing) { value in
                 if let amount = value.as(Double.self) {
                     AxisValueLabel {
-                        Text(amount.formatted(.number.precision(.fractionLength(0))) + " g")
+                        Text(amount.formatted(.number.precision(.fractionLength(0))))
                             .font(.footnote)
                     }
                     AxisGridLine()
                 }
             }
         }
+        .chartYAxisLabel(alignment: .trailing) {
+            Text("Macro Nutrients (g)")
+                .foregroundStyle(.primary)
+                .font(.footnote)
+                .padding(.vertical, 3)
+        }
         .chartXAxis {
             AxisMarks(preset: .aligned, values: .stride(by: selectedDuration == .Day ? .hour : .day)) { value in
                 if let date = value.as(Date.self) {
@@ -190,24 +196,24 @@ struct MealStatsView: View {
                     switch selectedDuration {
                     case .Day:
                         if hour % 6 == 0 {
-                            AxisValueLabel(format: StatsHelper.dateFormat(for: selectedDuration), centered: true)
+                            AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedDuration), centered: true)
                                 .font(.footnote)
                             AxisGridLine()
                         }
                     case .Month:
                         if day % 5 == 0 {
-                            AxisValueLabel(format: StatsHelper.dateFormat(for: selectedDuration), centered: true)
+                            AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedDuration), centered: true)
                                 .font(.footnote)
                             AxisGridLine()
                         }
                     case .Total:
                         if day == 1 && Calendar.current.component(.month, from: date) % 3 == 1 {
-                            AxisValueLabel(format: StatsHelper.dateFormat(for: selectedDuration), centered: true)
+                            AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedDuration), centered: true)
                                 .font(.footnote)
                             AxisGridLine()
                         }
                     default:
-                        AxisValueLabel(format: StatsHelper.dateFormat(for: selectedDuration), centered: true)
+                        AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedDuration), centered: true)
                             .font(.footnote)
                         AxisGridLine()
                     }
@@ -222,10 +228,10 @@ struct MealStatsView: View {
                 matching: selectedDuration == .Day ?
                     DateComponents(minute: 0) :
                     DateComponents(hour: 0),
-                majorAlignment: .matching(StatsHelper.alignmentComponents(for: selectedDuration))
+                majorAlignment: .matching(StatChartUtils.alignmentComponents(for: selectedDuration))
             )
         )
-        .chartXVisibleDomain(length: StatsHelper.visibleDomainLength(for: selectedDuration))
+        .chartXVisibleDomain(length: StatChartUtils.visibleDomainLength(for: selectedDuration))
         .frame(height: 250)
     }
 }

+ 62 - 38
Trio/Sources/Modules/Stat/View/ViewElements/TDDChart.swift

@@ -5,7 +5,7 @@ import SwiftUI
 ///
 /// This view presents insulin usage over time, with the ability to adjust the time interval
 /// and scroll through historical data.
-struct TDDChartView: View {
+struct TotalDailyDoseChart: View {
     /// The selected time interval for displaying statistics.
     @Binding var selectedDuration: Stat.StateModel.StatsTimeInterval
     /// The list of TDD statistics data.
@@ -24,7 +24,7 @@ struct TDDChartView: View {
 
     /// Computes the visible date range based on the current scroll position.
     private var visibleDateRange: (start: Date, end: Date) {
-        StatsHelper.visibleDateRange(from: scrollPosition, for: selectedDuration)
+        StatChartUtils.visibleDateRange(from: scrollPosition, for: selectedDuration)
     }
 
     /// Retrieves the TDD statistic for a given date.
@@ -32,7 +32,7 @@ struct TDDChartView: View {
     /// - Returns: The `TDDStats` object if available, otherwise `nil`.
     private func getTDDForDate(_ date: Date) -> TDDStats? {
         tddStats.first { stat in
-            StatsHelper.isSameTimeUnit(stat.date, date, for: selectedDuration)
+            StatChartUtils.isSameTimeUnit(stat.date, date, for: selectedDuration)
         }
     }
 
@@ -41,37 +41,13 @@ struct TDDChartView: View {
         currentAverage = state.getCachedTDDAverages(for: visibleDateRange)
     }
 
-    /// A view displaying the statistics summary including average TDD.
-    private var statsView: some View {
-        HStack {
-            Text("Average:")
-                .font(.headline)
-                .foregroundStyle(.secondary)
-            Text(currentAverage.formatted(.number.precision(.fractionLength(1))))
-                .font(.headline)
-                .foregroundStyle(.secondary)
-            Text("U")
-                .font(.headline)
-                .foregroundStyle(.secondary)
-
-            Spacer()
-
-            Text(
-                StatsHelper
-                    .formatVisibleDateRange(from: visibleDateRange.start, to: visibleDateRange.end, for: selectedDuration)
-            )
-            .font(.footnote)
-            .foregroundStyle(.secondary)
-        }
-    }
-
     var body: some View {
         VStack(alignment: .leading, spacing: 8) {
-            statsView
+            statsView.padding(.bottom)
             chartsView
         }
         .onAppear {
-            scrollPosition = StatsHelper.getInitialScrollPosition(for: selectedDuration)
+            scrollPosition = StatChartUtils.getInitialScrollPosition(for: selectedDuration)
             updateAverages()
         }
         .onChange(of: scrollPosition) {
@@ -81,12 +57,54 @@ struct TDDChartView: View {
         }
         .onChange(of: selectedDuration) {
             Task {
-                scrollPosition = StatsHelper.getInitialScrollPosition(for: selectedDuration)
+                scrollPosition = StatChartUtils.getInitialScrollPosition(for: selectedDuration)
                 updateAverages()
             }
         }
     }
 
+    /// A view displaying the statistics summary including average TDD.
+    private var statsView: some View {
+        HStack {
+            if selectedDuration == .Day {
+                Grid(alignment: .leading) {
+                    GridRow {
+                        Text("Total:")
+                            .font(.headline)
+                        Text("--")
+                            .font(.headline)
+                        Text("U")
+                            .font(.headline)
+                    }
+                    GridRow {
+                        Text("Average:")
+                            .font(.headline)
+                        Text(currentAverage.formatted(.number.precision(.fractionLength(1))))
+                            .font(.headline)
+                        Text("U")
+                            .font(.headline)
+                    }
+                }
+                .font(.headline)
+            } else {
+                Text("Average:")
+                    .font(.headline)
+                Text(currentAverage.formatted(.number.precision(.fractionLength(1))))
+                    .font(.headline)
+                Text("U")
+                    .font(.headline)
+            }
+            Spacer()
+
+            Text(
+                StatChartUtils
+                    .formatVisibleDateRange(from: visibleDateRange.start, to: visibleDateRange.end, for: selectedDuration)
+            )
+            .font(.callout)
+            .foregroundStyle(.secondary)
+        }
+    }
+
     /// A view displaying the bar chart for TDD statistics.
     private var chartsView: some View {
         Chart {
@@ -98,7 +116,7 @@ struct TDDChartView: View {
                 .foregroundStyle(Color.insulin)
                 .opacity(
                     selectedDate.map { date in
-                        StatsHelper.isSameTimeUnit(stat.date, date, for: selectedDuration) ? 1 : 0.3
+                        StatChartUtils.isSameTimeUnit(stat.date, date, for: selectedDuration) ? 1 : 0.3
                     } ?? 1
                 )
             }
@@ -124,13 +142,19 @@ struct TDDChartView: View {
             AxisMarks(position: .trailing) { value in
                 if let amount = value.as(Double.self) {
                     AxisValueLabel {
-                        Text(amount.formatted(.number.precision(.fractionLength(0))) + " U")
+                        Text(amount.formatted(.number.precision(.fractionLength(0))))
                             .font(.footnote)
                     }
                     AxisGridLine()
                 }
             }
         }
+        .chartYAxisLabel(alignment: .trailing) {
+            Text("Total Daily Dose (U)")
+                .foregroundStyle(.primary)
+                .font(.footnote)
+                .padding(.vertical, 3)
+        }
         .chartXAxis {
             AxisMarks(preset: .aligned, values: .stride(by: selectedDuration == .Day ? .hour : .day)) { value in
                 if let date = value.as(Date.self) {
@@ -140,24 +164,24 @@ struct TDDChartView: View {
                     switch selectedDuration {
                     case .Day:
                         if hour % 6 == 0 {
-                            AxisValueLabel(format: StatsHelper.dateFormat(for: selectedDuration), centered: true)
+                            AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedDuration), centered: true)
                                 .font(.footnote)
                             AxisGridLine()
                         }
                     case .Month:
                         if day % 5 == 0 {
-                            AxisValueLabel(format: StatsHelper.dateFormat(for: selectedDuration), centered: true)
+                            AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedDuration), centered: true)
                                 .font(.footnote)
                             AxisGridLine()
                         }
                     case .Total:
                         if day == 1 && Calendar.current.component(.month, from: date) % 3 == 1 {
-                            AxisValueLabel(format: StatsHelper.dateFormat(for: selectedDuration), centered: true)
+                            AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedDuration), centered: true)
                                 .font(.footnote)
                             AxisGridLine()
                         }
                     default:
-                        AxisValueLabel(format: StatsHelper.dateFormat(for: selectedDuration), centered: true)
+                        AxisValueLabel(format: StatChartUtils.dateFormat(for: selectedDuration), centered: true)
                             .font(.footnote)
                         AxisGridLine()
                     }
@@ -172,10 +196,10 @@ struct TDDChartView: View {
                 matching: selectedDuration == .Day ?
                     DateComponents(minute: 0) :
                     DateComponents(hour: 0),
-                majorAlignment: .matching(StatsHelper.alignmentComponents(for: selectedDuration))
+                majorAlignment: .matching(StatChartUtils.alignmentComponents(for: selectedDuration))
             )
         )
-        .chartXVisibleDomain(length: StatsHelper.visibleDomainLength(for: selectedDuration))
+        .chartXVisibleDomain(length: StatChartUtils.visibleDomainLength(for: selectedDuration))
         .frame(height: 250)
     }
 }