Преглед изворни кода

Merge branch 'stats-wip' into local-testing-02022025

polscm32 пре 1 година
родитељ
комит
ff28063e57
26 измењених фајлова са 3938 додато и 711 уклоњено
  1. 5 0
      Model/Helper/CarbEntryStored+helper.swift
  2. 5 0
      Model/Helper/Determination+helper.swift
  3. 5 0
      Model/Helper/PumpEvent+helper.swift
  4. 85 7
      Trio.xcodeproj/project.pbxproj
  5. 19 0
      Trio/Sources/Helpers/Calendar+GlucoseStatsChart.swift
  6. 56 0
      Trio/Sources/Helpers/CustomDatePicker.swift
  7. 6 0
      Trio/Sources/Helpers/Formatters.swift
  8. 24 0
      Trio/Sources/Models/TDD.swift
  9. 144 0
      Trio/Sources/Modules/Stat/StatStateModel+Setup/AreaChartSetup.swift
  10. 224 0
      Trio/Sources/Modules/Stat/StatStateModel+Setup/BolusStatsSetup.swift
  11. 216 0
      Trio/Sources/Modules/Stat/StatStateModel+Setup/LoopChartSetup.swift
  12. 175 0
      Trio/Sources/Modules/Stat/StatStateModel+Setup/MealStatsSetup.swift
  13. 132 0
      Trio/Sources/Modules/Stat/StatStateModel+Setup/StackedChartSetup.swift
  14. 165 0
      Trio/Sources/Modules/Stat/StatStateModel+Setup/TDDSetup.swift
  15. 172 11
      Trio/Sources/Modules/Stat/StatStateModel.swift
  16. 0 296
      Trio/Sources/Modules/Stat/View/ChartsView.swift
  17. 315 110
      Trio/Sources/Modules/Stat/View/StatRootView.swift
  18. 0 287
      Trio/Sources/Modules/Stat/View/StatsView.swift
  19. 307 0
      Trio/Sources/Modules/Stat/View/ViewElements/BareStatisticsView.swift
  20. 421 0
      Trio/Sources/Modules/Stat/View/ViewElements/BolusStatsView.swift
  21. 62 0
      Trio/Sources/Modules/Stat/View/ViewElements/GlucoseDistributionChart.swift
  22. 257 0
      Trio/Sources/Modules/Stat/View/ViewElements/GlucosePercentileChart.swift
  23. 79 0
      Trio/Sources/Modules/Stat/View/ViewElements/LoopStatsView.swift
  24. 486 0
      Trio/Sources/Modules/Stat/View/ViewElements/MealStatsView.swift
  25. 247 0
      Trio/Sources/Modules/Stat/View/ViewElements/SectorChart.swift
  26. 331 0
      Trio/Sources/Modules/Stat/View/ViewElements/TDDChart.swift

+ 5 - 0
Model/Helper/CarbEntryStored+helper.swift

@@ -12,6 +12,11 @@ extension NSPredicate {
         return NSPredicate(format: "isFPU == false AND date >= %@ AND carbs > 0", date as NSDate)
     }
 
+    static var carbsForStats: NSPredicate {
+        let date = Date.threeMonthsAgo
+        return NSPredicate(format: "date >= %@", date as NSDate)
+    }
+
     static var carbsNotYetUploadedToNightscout: NSPredicate {
         let date = Date.oneDayAgo
         return NSPredicate(

+ 5 - 0
Model/Helper/Determination+helper.swift

@@ -50,4 +50,9 @@ extension NSPredicate {
             true as NSNumber
         )
     }
+
+    static var determinationsForStats: NSPredicate {
+        let date = Date.threeMonthsAgo
+        return NSPredicate(format: "deliverAt >= %@", date as NSDate)
+    }
 }

+ 5 - 0
Model/Helper/PumpEvent+helper.swift

@@ -63,6 +63,11 @@ extension NSPredicate {
         return NSPredicate(format: "timestamp >= %@", date as NSDate)
     }
 
+    static var pumpHistoryForStats: NSPredicate {
+        let date = Date.threeMonthsAgo
+        return NSPredicate(format: "pumpEvent.timestamp >= %@", date as NSDate)
+    }
+
     static var recentPumpHistory: NSPredicate {
         let date = Date.twentyMinutesAgo
         return NSPredicate(format: "timestamp >= %@", date as NSDate)

+ 85 - 7
Trio.xcodeproj/project.pbxproj

@@ -40,8 +40,8 @@
 		1967DFC229D053D300759F30 /* IconImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1967DFC129D053D300759F30 /* IconImage.swift */; };
 		198377D2266BFFF6004DE65E /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 198377D4266BFFF6004DE65E /* Localizable.strings */; };
 		19A910302A24BF6300C8951B /* StatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A9102F2A24BF6300C8951B /* StatsView.swift */; };
+		199561C1275E61A50077B976 /* HealthKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 199561C0275E61A50077B976 /* HealthKit.framework */; };
 		19A910362A24D6D700C8951B /* DateFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A910352A24D6D700C8951B /* DateFilter.swift */; };
-		19A910382A24EF3200C8951B /* ChartsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A910372A24EF3200C8951B /* ChartsView.swift */; };
 		19B0EF2128F6D66200069496 /* Statistics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19B0EF2028F6D66200069496 /* Statistics.swift */; };
 		19D466A329AA2B80004D5F33 /* MealSettingsDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19D466A229AA2B80004D5F33 /* MealSettingsDataFlow.swift */; };
 		19D466A529AA2BD4004D5F33 /* MealSettingsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19D466A429AA2BD4004D5F33 /* MealSettingsProvider.swift */; };
@@ -290,6 +290,23 @@
 		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 */; };
+		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 */; };
+		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 */; };
+		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 */; };
+		BD249D9D2D42FCF500412DEB /* MealStatsSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD249D9C2D42FCF300412DEB /* MealStatsSetup.swift */; };
+		BD249D9F2D42FD0600412DEB /* StackedChartSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD249D9E2D42FD0200412DEB /* StackedChartSetup.swift */; };
+		BD249DA12D42FD1200412DEB /* TDDSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD249DA02D42FD1000412DEB /* TDDSetup.swift */; };
+		BD249DA32D42FD7C00412DEB /* TDD.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD249DA22D42FD7A00412DEB /* TDD.swift */; };
+		BD249DA52D42FD9700412DEB /* CustomDatePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD249DA42D42FD9500412DEB /* CustomDatePicker.swift */; };
+		BD249DA72D42FE4600412DEB /* Calendar+GlucoseStatsChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD249DA62D42FE3800412DEB /* Calendar+GlucoseStatsChart.swift */; };
 		BD2B464E0745FBE7B79913F4 /* NightscoutConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BF768BD6264FF7D71D66767 /* NightscoutConfigProvider.swift */; };
 		BD2FF1A02AE29D43005D1C5D /* CheckboxToggleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD2FF19F2AE29D43005D1C5D /* CheckboxToggleStyle.swift */; };
 		BD3CC0722B0B89D50013189E /* MainChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD3CC0712B0B89D50013189E /* MainChartView.swift */; };
@@ -742,9 +759,7 @@
 		199561C0275E61A50077B976 /* HealthKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = HealthKit.framework; path = Platforms/WatchOS.platform/Developer/SDKs/WatchOS8.0.sdk/System/Library/Frameworks/HealthKit.framework; sourceTree = DEVELOPER_DIR; };
 		199732B4271B72DD00129A3F /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/Localizable.strings"; sourceTree = "<group>"; };
 		199732B5271B9EE900129A3F /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = "<group>"; };
-		19A9102F2A24BF6300C8951B /* StatsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsView.swift; sourceTree = "<group>"; };
 		19A910352A24D6D700C8951B /* DateFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateFilter.swift; sourceTree = "<group>"; };
-		19A910372A24EF3200C8951B /* ChartsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChartsView.swift; sourceTree = "<group>"; };
 		19B0EF2028F6D66200069496 /* Statistics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Statistics.swift; sourceTree = "<group>"; };
 		19C166682756EFBD00ED12E3 /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/InfoPlist.strings; sourceTree = "<group>"; };
 		19C166692756EFBD00ED12E3 /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/Localizable.strings; sourceTree = "<group>"; };
@@ -1019,6 +1034,23 @@
 		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>"; };
+		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>"; };
+		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>"; };
+		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>"; };
+		BD249D9C2D42FCF300412DEB /* MealStatsSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MealStatsSetup.swift; sourceTree = "<group>"; };
+		BD249D9E2D42FD0200412DEB /* StackedChartSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StackedChartSetup.swift; sourceTree = "<group>"; };
+		BD249DA02D42FD1000412DEB /* TDDSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TDDSetup.swift; sourceTree = "<group>"; };
+		BD249DA22D42FD7A00412DEB /* TDD.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TDD.swift; sourceTree = "<group>"; };
+		BD249DA42D42FD9500412DEB /* CustomDatePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDatePicker.swift; sourceTree = "<group>"; };
+		BD249DA62D42FE3800412DEB /* Calendar+GlucoseStatsChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Calendar+GlucoseStatsChart.swift"; sourceTree = "<group>"; };
 		BD2FF19F2AE29D43005D1C5D /* CheckboxToggleStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckboxToggleStyle.swift; sourceTree = "<group>"; };
 		BD3CC0712B0B89D50013189E /* MainChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainChartView.swift; sourceTree = "<group>"; };
 		BD4064D02C4ED26900582F43 /* CoreDataObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataObserver.swift; sourceTree = "<group>"; };
@@ -1570,6 +1602,7 @@
 		19F95FF129F10F9C00314DDC /* Stat */ = {
 			isa = PBXGroup;
 			children = (
+				BD249D952D42FCA800412DEB /* StatStateModel+Setup */,
 				19F95FF229F10FBC00314DDC /* StatDataFlow.swift */,
 				19F95FF429F10FCF00314DDC /* StatProvider.swift */,
 				19F95FF629F10FEE00314DDC /* StatStateModel.swift */,
@@ -1581,9 +1614,8 @@
 		19F95FF829F10FF600314DDC /* View */ = {
 			isa = PBXGroup;
 			children = (
+				BD249D842D42FBD200412DEB /* ViewElements */,
 				19F95FF929F1102A00314DDC /* StatRootView.swift */,
-				19A9102F2A24BF6300C8951B /* StatsView.swift */,
-				19A910372A24EF3200C8951B /* ChartsView.swift */,
 			);
 			path = View;
 			sourceTree = "<group>";
@@ -2064,6 +2096,7 @@
 				BD54A9722D281A9C00F9C1EE /* TempTargetPresetWatch.swift */,
 				BD54A95A2D28087700F9C1EE /* OverridePresetWatch.swift */,
 				BDA25EFC2D261BF200035F34 /* WatchState.swift */,
+				BD249DA22D42FD7A00412DEB /* TDD.swift */,
 				715120D12D3C2B84005D9FB6 /* GlucoseNotificationsOption.swift */,
 				DD07CA132CE80B73002D45A9 /* TimeInRangeChartStyle.swift */,
 				DD940BA92CA7585D000830A5 /* GlucoseColorScheme.swift */,
@@ -2119,6 +2152,8 @@
 		388E5A5A25B6F05F0019842D /* Helpers */ = {
 			isa = PBXGroup;
 			children = (
+				BD249DA62D42FE3800412DEB /* Calendar+GlucoseStatsChart.swift */,
+				BD249DA42D42FD9500412DEB /* CustomDatePicker.swift */,
 				C2A0A42E2CE0312C003B98E8 /* ConstantValues.swift */,
 				DD940BAB2CA75889000830A5 /* DynamicGlucoseColor.swift */,
 				38F37827261260DC009DB701 /* Color+Extensions.swift */,
@@ -2485,6 +2520,34 @@
 			path = View;
 			sourceTree = "<group>";
 		};
+		BD249D842D42FBD200412DEB /* ViewElements */ = {
+			isa = PBXGroup;
+			children = (
+				BD249D932D42FC5C00412DEB /* TDDChart.swift */,
+				BD249D912D42FC5000412DEB /* SectorChart.swift */,
+				BD249D8F2D42FC4300412DEB /* MealStatsView.swift */,
+				BD249D8D2D42FC3600412DEB /* LoopStatsView.swift */,
+				BD249D8B2D42FC2500412DEB /* GlucoseDistributionChart.swift */,
+				BD249D892D42FC0E00412DEB /* GlucosePercentileChart.swift */,
+				BD249D872D42FBFB00412DEB /* BolusStatsView.swift */,
+				BD249D852D42FBE600412DEB /* BareStatisticsView.swift */,
+			);
+			path = ViewElements;
+			sourceTree = "<group>";
+		};
+		BD249D952D42FCA800412DEB /* StatStateModel+Setup */ = {
+			isa = PBXGroup;
+			children = (
+				BD249DA02D42FD1000412DEB /* TDDSetup.swift */,
+				BD249D9C2D42FCF300412DEB /* MealStatsSetup.swift */,
+				BD249D982D42FCCA00412DEB /* BolusStatsSetup.swift */,
+				BD249D962D42FCBD00412DEB /* AreaChartSetup.swift */,
+				BD249D9E2D42FD0200412DEB /* StackedChartSetup.swift */,
+				BD249D9A2D42FCD800412DEB /* LoopChartSetup.swift */,
+			);
+			path = "StatStateModel+Setup";
+			sourceTree = "<group>";
+		};
 		BD793CAD2CE7660C00D669AC /* Overrides */ = {
 			isa = PBXGroup;
 			children = (
@@ -3573,6 +3636,7 @@
 				38AEE75225F022080013F05B /* SettingsManager.swift in Sources */,
 				3894873A2614928B004DF424 /* DispatchTimer.swift in Sources */,
 				3895E4C625B9E00D00214B37 /* Preferences.swift in Sources */,
+				BD249DA52D42FD9700412DEB /* CustomDatePicker.swift in Sources */,
 				CE94598429E9E3E60047C9C6 /* WatchConfigStateModel.swift in Sources */,
 				DD6B7CB92C7BAC6900B75029 /* NightscoutImportResultView.swift in Sources */,
 				38DF1786276A73D400B3528F /* TagCloudView.swift in Sources */,
@@ -3636,14 +3700,16 @@
 				3811DE3125C9D49500A708ED /* HomeProvider.swift in Sources */,
 				FE41E4D629463EE20047FD55 /* NightscoutPreferences.swift in Sources */,
 				E013D872273AC6FE0014109C /* GlucoseSimulatorSource.swift in Sources */,
+				BD249D862D42FBEC00412DEB /* BareStatisticsView.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 */,
-				19A910382A24EF3200C8951B /* ChartsView.swift in Sources */,
 				58A3D5522C96EFA8003F90FC /* TempTargetStored+CoreDataProperties.swift in Sources */,
+				BD249D942D42FC5E00412DEB /* TDDChart.swift in Sources */,
+				BD249D902D42FC4500412DEB /* MealStatsView.swift in Sources */,
 				DD32CF9A2CC8247B003686D6 /* TrioRemoteControl+Meal.swift in Sources */,
 				BDF34F832C10C5B600D51995 /* DataManager.swift in Sources */,
 				38B4F3C625E5017E00E76A18 /* NotificationCenter.swift in Sources */,
@@ -3656,6 +3722,7 @@
 				38A13D3225E28B4B00EAA382 /* PumpHistoryEvent.swift in Sources */,
 				E00EEC0627368630002FF094 /* UIAssembly.swift in Sources */,
 				3811DE1825C9D40400A708ED /* Router.swift in Sources */,
+				BD249DA12D42FD1200412DEB /* TDDSetup.swift in Sources */,
 				CE7950262998056D00FA576E /* CGMSetupView.swift in Sources */,
 				582FAE432C05102C00D1C13F /* CoreDataError.swift in Sources */,
 				38A0363B25ECF07E00FCBB52 /* GlucoseStorage.swift in Sources */,
@@ -3688,6 +3755,7 @@
 				CE1F6DE92BAF37C90064EB8D /* TidepoolConfigView.swift in Sources */,
 				3811DE5D25C9D4D500A708ED /* Publisher.swift in Sources */,
 				E00EEC0727368630002FF094 /* APSAssembly.swift in Sources */,
+				BD249D8A2D42FC1200412DEB /* GlucosePercentileChart.swift in Sources */,
 				38B4F3AF25E2979F00E76A18 /* IndexedCollection.swift in Sources */,
 				58D08B222C8DAA8E00AA37D3 /* OverrideView.swift in Sources */,
 				BD0B2EF32C5998E600B3298F /* MealPresetView.swift in Sources */,
@@ -3725,16 +3793,17 @@
 				3811DE6125C9D4D500A708ED /* ViewModifiers.swift in Sources */,
 				3811DEAC25C9D88300A708ED /* NightscoutManager.swift in Sources */,
 				BD793CB22CE8033500D669AC /* TempTargetRunStored.swift in Sources */,
-				19A910302A24BF6300C8951B /* StatsView.swift in Sources */,
 				BD7DA9A92AE06E9200601B20 /* BolusCalculatorStateModel.swift in Sources */,
 				CEB434E528B8FF5D00B70274 /* UIColor.swift in Sources */,
 				190EBCCB29FF13CB00BA767D /* UserInterfaceSettingsRootView.swift in Sources */,
 				3811DEA925C9D88300A708ED /* AppearanceManager.swift in Sources */,
+				BD249D8E2D42FC3900412DEB /* LoopStatsView.swift in Sources */,
 				CE7950242997D81700FA576E /* CGMSettingsView.swift in Sources */,
 				58237D9E2BCF0A6B00A47A79 /* PopupView.swift in Sources */,
 				BD793CB02CE7C61500D669AC /* OverrideRunStored+helper.swift in Sources */,
 				38D0B3D925EC07C400CB6E88 /* CarbsEntry.swift in Sources */,
 				DD32CF9C2CC82499003686D6 /* TrioRemoteControl+TempTarget.swift in Sources */,
+				BD249D882D42FC0000412DEB /* BolusStatsView.swift in Sources */,
 				38A9260525F012D8009E3739 /* CarbRatios.swift in Sources */,
 				38FCF3D625E8FDF40078B0D1 /* MD5.swift in Sources */,
 				DDD163142C4C68D300CD525A /* AdjustmentsProvider.swift in Sources */,
@@ -3742,6 +3811,7 @@
 				191F62682AD6B05A004D7911 /* NightscoutSettings.swift in Sources */,
 				3811DEAB25C9D88300A708ED /* HTTPResponseStatus.swift in Sources */,
 				3811DE5F25C9D4D500A708ED /* ProgressBar.swift in Sources */,
+				BD249D8C2D42FC2C00412DEB /* GlucoseDistributionChart.swift in Sources */,
 				38E87408274F9AD000975559 /* UserNotificationsManager.swift in Sources */,
 				CE82E02528E867BA00473A9C /* AlertStorage.swift in Sources */,
 				DD1745372C55B74200211FAC /* AlgorithmSettings.swift in Sources */,
@@ -3815,10 +3885,13 @@
 				DDA6E3222D25901100C2988C /* TempTargetHelpView.swift in Sources */,
 				58645B9D2CA2D275008AFCE7 /* DeterminationSetup.swift in Sources */,
 				DD1745442C55C60E00211FAC /* AutosensSettingsDataFlow.swift in Sources */,
+				BD249DA72D42FE4600412DEB /* Calendar+GlucoseStatsChart.swift in Sources */,
 				BDCAF2382C639F35002DC907 /* SettingItems.swift in Sources */,
 				58D08B342C8DF9A700AA37D3 /* CobIobChart.swift in Sources */,
 				642F76A05A4FF530463A9FD0 /* NightscoutConfigRootView.swift in Sources */,
+				BD249D9B2D42FCDB00412DEB /* LoopChartSetup.swift in Sources */,
 				BD7DA9AC2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift in Sources */,
+				BD249DA32D42FD7C00412DEB /* TDD.swift in Sources */,
 				AD3D2CD42CD01B9EB8F26522 /* PumpConfigDataFlow.swift in Sources */,
 				DD17452E2C55AE4800211FAC /* TargetBehavoirDataFlow.swift in Sources */,
 				53F2382465BF74DB1A967C8B /* PumpConfigProvider.swift in Sources */,
@@ -3881,6 +3954,7 @@
 				BD7DA9A72AE06E2B00601B20 /* BolusCalculatorConfigProvider.swift in Sources */,
 				38192E0D261BAF980094D973 /* ConvenienceExtensions.swift in Sources */,
 				88AB39B23C9552BD6E0C9461 /* ISFEditorRootView.swift in Sources */,
+				BD249D9D2D42FCF500412DEB /* MealStatsSetup.swift in Sources */,
 				BD6EB2D62C7D049B0086BBB6 /* LiveActivityWidgetConfiguration.swift in Sources */,
 				DD32CF982CC82463003686D6 /* TrioRemoteControl+Bolus.swift in Sources */,
 				F816825E28DB441200054060 /* HeartBeatManager.swift in Sources */,
@@ -3907,6 +3981,7 @@
 				DD17453A2C55BFA600211FAC /* AlgorithmAdvancedSettingsDataFlow.swift in Sources */,
 				5075C1608E6249A51495C422 /* TargetsEditorProvider.swift in Sources */,
 				E13B7DAB2A435F57066AF02E /* TargetsEditorStateModel.swift in Sources */,
+				BD249D992D42FCCD00412DEB /* BolusStatsSetup.swift in Sources */,
 				9702FF92A09C53942F20D7EA /* TargetsEditorRootView.swift in Sources */,
 				1967DFBE29D052C200759F30 /* Icons.swift in Sources */,
 				DDD163182C4C694000CD525A /* AdjustmentsRootView.swift in Sources */,
@@ -3924,6 +3999,7 @@
 				DDAA29852D2D1D9E006546A1 /* AdjustmentsRootView+TempTargets.swift in Sources */,
 				E592A3792CEEC038009A472C /* ContactImageRootView.swift in Sources */,
 				BDC531182D1062F200088832 /* ContactImageState.swift in Sources */,
+				BD249D9F2D42FD0600412DEB /* StackedChartSetup.swift in Sources */,
 				E592A37A2CEEC038009A472C /* ContactImageProvider.swift in Sources */,
 				CE82E02728E869DF00473A9C /* AlertEntry.swift in Sources */,
 				DD30786A2D42F94000DE0490 /* GarminDevice.swift in Sources */,
@@ -3935,6 +4011,7 @@
 				38AAF85525FFF846004AF583 /* CurrentGlucoseView.swift in Sources */,
 				041D1E995A6AE92E9289DC49 /* TreatmentsDataFlow.swift in Sources */,
 				DD32CF9E2CC824C5003686D6 /* TrioRemoteControl+Override.swift in Sources */,
+				BD249D922D42FC5300412DEB /* SectorChart.swift in Sources */,
 				23888883D4EA091C88480FF2 /* TreatmentsProvider.swift in Sources */,
 				38E98A2D25F52DC400C0CED0 /* NSLocking+Extensions.swift in Sources */,
 				715120D22D3C2BB4005D9FB6 /* GlucoseNotificationsOption.swift in Sources */,
@@ -3986,6 +4063,7 @@
 				6EADD581738D64431902AC0A /* (null) in Sources */,
 				CE94598729E9E4110047C9C6 /* WatchConfigRootView.swift in Sources */,
 				DD5DC9FB2CF3E1B100AB8703 /* AdjustmentsStateModel+Helpers.swift in Sources */,
+				BD249D972D42FCBF00412DEB /* AreaChartSetup.swift in Sources */,
 				DDF847E42C5C288F0049BB3B /* LiveActivitySettingsRootView.swift in Sources */,
 				DD88C8E22C50420800F2D558 /* DefinitionRow.swift in Sources */,
 				B7C465E9472624D8A2BE2A6A /* (null) in Sources */,

+ 19 - 0
Trio/Sources/Helpers/Calendar+GlucoseStatsChart.swift

@@ -0,0 +1,19 @@
+import Foundation
+
+extension Calendar {
+    /// Converts an hour (0-23) to a Date object representing that hour on the current day.
+    /// This is used to properly position marks on the chart's time axis.
+    ///
+    /// - Parameter hour: Integer representing the hour of day (0-23)
+    /// - Returns: Date object set to the specified hour on the current day
+    ///
+    /// Example:
+    /// ```
+    /// calendar.dateForChartHour(14) // Returns today's date at 2:00 PM
+    /// calendar.dateForChartHour(0)  // Returns today's date at 12:00 AM
+    /// ```
+    func dateForChartHour(_ hour: Int) -> Date {
+        let today = startOfDay(for: Date())
+        return date(byAdding: .hour, value: hour, to: today) ?? today
+    }
+}

+ 56 - 0
Trio/Sources/Helpers/CustomDatePicker.swift

@@ -0,0 +1,56 @@
+import SwiftUI
+
+struct CustomDatePicker: UIViewRepresentable {
+    @Binding var selection: Date
+
+    // Coordinator to handle date changes
+    class Coordinator: NSObject {
+        var parent: CustomDatePicker
+
+        init(_ parent: CustomDatePicker) {
+            self.parent = parent
+        }
+
+        @objc func dateChanged(_ sender: UIDatePicker) {
+            let calendar = Calendar.current
+            // Set the time of the selected date to 23:59:59 for any selected date
+            if let adjustedDate = calendar.date(bySettingHour: 23, minute: 59, second: 59, of: sender.date) {
+                parent.selection = adjustedDate
+            } else {
+                parent.selection = sender.date // Fallback in case something goes wrong
+            }
+        }
+    }
+
+    func makeUIView(context: Context) -> UIDatePicker {
+        let datePicker = UIDatePicker()
+        datePicker.datePickerMode = .date
+
+        // Calculate yesterday's date at 23:59:59
+        let today = Date()
+        let calendar = Calendar.current
+        if let yesterday = calendar.date(byAdding: .day, value: -1, to: today),
+           let adjustedYesterday = calendar.date(bySettingHour: 23, minute: 59, second: 59, of: yesterday)
+        {
+            datePicker.maximumDate = adjustedYesterday // Set maximum date to yesterday at 23:59:59
+            datePicker.date = adjustedYesterday // Set default date to yesterday at 23:59:59
+        }
+
+        // Set up the date change action
+        datePicker.addTarget(context.coordinator, action: #selector(Coordinator.dateChanged(_:)), for: .valueChanged)
+
+        return datePicker
+    }
+
+    func updateUIView(_ uiView: UIDatePicker, context _: Context) {
+        // Ensure the displayed date is also adjusted to 23:59:59
+        let calendar = Calendar.current
+        if let adjustedDate = calendar.date(bySettingHour: 23, minute: 59, second: 59, of: selection) {
+            uiView.date = adjustedDate
+        }
+    }
+
+    func makeCoordinator() -> Coordinator {
+        Coordinator(self)
+    }
+}

+ 6 - 0
Trio/Sources/Helpers/Formatters.swift

@@ -42,6 +42,12 @@ extension Formatter {
         return dateFormatter
     }()
 
+    static let dayFormatter: DateFormatter = {
+        let formatter = DateFormatter()
+        formatter.dateFormat = "d"
+        return formatter
+    }()
+
     static let decimalFormatterWithOneFractionDigit: NumberFormatter = {
         let formatter = NumberFormatter()
         formatter.numberStyle = .decimal

+ 24 - 0
Trio/Sources/Models/TDD.swift

@@ -0,0 +1,24 @@
+import Foundation
+
+struct TDD: Sendable, Codable, Equatable, Identifiable {
+    var id = UUID()
+
+    let totalDailyDose: Decimal?
+    let timestamp: Date?
+
+    init(totalDailyDose: Decimal?, timestamp: Date?) {
+        self.totalDailyDose = totalDailyDose
+        self.timestamp = timestamp
+    }
+
+    init?(from dictionary: [String: Any]) {
+        guard let deliverAt = dictionary["deliverAt"] as? Date,
+              let totalDailyDose = dictionary["totalDailyDose"] as? Decimal
+        else {
+            return nil
+        }
+
+        self.totalDailyDose = totalDailyDose
+        timestamp = deliverAt
+    }
+}

+ 144 - 0
Trio/Sources/Modules/Stat/StatStateModel+Setup/AreaChartSetup.swift

@@ -0,0 +1,144 @@
+import CoreData
+import Foundation
+
+/// Represents statistical values for glucose readings grouped by hour of the day.
+///
+/// This struct contains various percentile calculations that help visualize
+/// glucose distribution patterns throughout the day:
+///
+/// - The median (50th percentile) shows the central tendency
+/// - The 25th and 75th percentiles form the interquartile range (IQR)
+/// - The 10th and 90th percentiles show the wider range of values
+///
+/// Example usage in visualization:
+/// ```
+/// let stats = HourlyStats(
+///     hour: 14,        // 2 PM
+///     median: 120,     // Center line
+///     percentile25: 100, // Lower bound of dark band
+///     percentile75: 140, // Upper bound of dark band
+///     percentile10: 80,  // Lower bound of light band
+///     percentile90: 160  // Upper bound of light band
+/// )
+/// ```
+///
+/// This data structure is used to create area charts showing glucose
+/// variability patterns across different times of day.
+public struct HourlyStats: Equatable {
+    /// The hour of day (0-23) these statistics represent
+    let hour: Int
+    /// The median (50th percentile) glucose value for this hour
+    let median: Double
+    /// The 25th percentile glucose value (lower quartile)
+    let percentile25: Double
+    /// The 75th percentile glucose value (upper quartile)
+    let percentile75: Double
+    /// The 10th percentile glucose value (lower whisker)
+    let percentile10: Double
+    /// The 90th percentile glucose value (upper whisker)
+    let percentile90: Double
+}
+
+extension Double {
+    var isEven: Bool {
+        truncatingRemainder(dividingBy: 2) == 0
+    }
+}
+
+extension Stat.StateModel {
+    /// Calculates hourly statistical values (median, percentiles) from glucose readings.
+    /// The calculation runs asynchronously using the CoreData context.
+    ///
+    /// The calculation works as follows:
+    /// 1. Group readings by hour of day (0-23)
+    /// 2. For each hour:
+    ///    - Sort glucose values
+    ///    - Calculate median (50th percentile)
+    ///    - Calculate 10th, 25th, 75th, and 90th percentiles
+    ///
+    /// Example:
+    /// For readings at 6:00 AM across multiple days:
+    /// ```
+    /// Readings: [80, 100, 120, 140, 160, 180, 200]
+    /// Results:
+    /// - 10th percentile: 84 (lower whisker)
+    /// - 25th percentile: 110 (lower band)
+    /// - median: 140 (center line)
+    /// - 75th percentile: 170 (upper band)
+    /// - 90th percentile: 196 (upper whisker)
+    /// ```
+    ///
+    /// The resulting statistics are used to show:
+    /// - A dark blue area for the interquartile range (25th-75th percentile)
+    /// - A light blue area for the wider range (10th-90th percentile)
+    /// - A solid blue line for the median
+    func calculateHourlyStatsForGlucoseAreaChart(from ids: [NSManagedObjectID]) async {
+        let taskContext = CoreDataStack.shared.newTaskContext()
+
+        let calendar = Calendar.current
+
+        let stats = await taskContext.perform {
+            // Convert IDs to GlucoseStored objects using the context
+            let readings = ids.compactMap { id -> GlucoseStored? in
+                do {
+                    return try taskContext.existingObject(with: id) as? GlucoseStored
+                } catch {
+                    debugPrint("\(DebuggingIdentifiers.failed) Error fetching glucose: \(error)")
+                    return nil
+                }
+            }
+
+            // Group readings by hour of day (0-23)
+            // Example: [8: [reading1, reading2], 9: [reading3, reading4, reading5], ...]
+            let groupedByHour = Dictionary(grouping: readings) { reading in
+                calendar.component(.hour, from: reading.date ?? Date())
+            }
+
+            // Process each hour of the day (0-23)
+            return (0 ... 23).map { hour in
+                // Get all readings for this hour (or empty if none)
+                let readings = groupedByHour[hour] ?? []
+
+                // Extract and sort glucose values for percentile calculations
+                // Example: [100, 120, 130, 140, 150, 160, 180]
+                let values = readings.map { Double($0.glucose) }.sorted()
+                let count = Double(values.count)
+
+                // Handle hours with no readings
+                guard !values.isEmpty else {
+                    return HourlyStats(
+                        hour: hour,
+                        median: 0,
+                        percentile25: 0,
+                        percentile75: 0,
+                        percentile10: 0,
+                        percentile90: 0
+                    )
+                }
+
+                // Calculate median
+                // For even count: average of two middle values
+                // For odd count: middle value
+                let median = count.isEven ?
+                    (values[Int(count / 2) - 1] + values[Int(count / 2)]) / 2 :
+                    values[Int(count / 2)]
+
+                // Create statistics object with all percentiles
+                // Index calculation: multiply count by desired percentile (0.25 for 25th)
+                return HourlyStats(
+                    hour: hour,
+                    median: median,
+                    percentile25: values[Int(count * 0.25)], // Lower quartile
+                    percentile75: values[Int(count * 0.75)], // Upper quartile
+                    percentile10: values[Int(count * 0.10)], // Lower whisker
+                    percentile90: values[Int(count * 0.90)] // Upper whisker
+                )
+            }
+        }
+
+        // Update stats on main thread
+        await MainActor.run {
+            self.hourlyStats = stats
+        }
+    }
+}

+ 224 - 0
Trio/Sources/Modules/Stat/StatStateModel+Setup/BolusStatsSetup.swift

@@ -0,0 +1,224 @@
+import CoreData
+import Foundation
+
+/// Represents statistical data about bolus insulin for a specific time period
+struct BolusStats: Identifiable {
+    let id = UUID()
+    /// The date representing this time period
+    let date: Date
+    /// Total manual bolus insulin in units
+    let manualBolus: Double
+    /// Total SMB insulin in units
+    let smb: Double
+    /// Total external bolus insulin in units
+    let external: Double
+}
+
+extension Stat.StateModel {
+    /// Sets up bolus statistics by fetching and processing bolus data
+    ///
+    /// This function:
+    /// 1. Fetches hourly and daily bolus statistics asynchronously
+    /// 2. Updates the state model with the fetched statistics on the main actor
+    /// 3. Calculates and caches initial daily averages
+    func setupBolusStats() {
+        Task {
+            let (hourly, daily) = await fetchBolusStats()
+
+            await MainActor.run {
+                self.hourlyBolusStats = hourly
+                self.dailyBolusStats = daily
+            }
+
+            // Initially calculate and cache daily averages
+            await calculateAndCacheBolusAverages()
+        }
+    }
+
+    /// Fetches and processes bolus statistics from Core Data
+    /// - Returns: A tuple containing hourly and daily bolus statistics arrays
+    ///
+    /// This function:
+    /// 1. Fetches bolus entries from Core Data
+    /// 2. Groups entries by hour and day
+    /// 3. Calculates total insulin for each time period
+    /// 4. Returns the processed statistics as (hourly: [BolusStats], daily: [BolusStats])
+    private func fetchBolusStats() async -> (hourly: [BolusStats], daily: [BolusStats]) {
+        // Fetch PumpEventStored entries from Core Data
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: BolusStored.self,
+            onContext: bolusTaskContext,
+            predicate: NSPredicate.pumpHistoryForStats,
+            key: "pumpEvent.timestamp",
+            ascending: true,
+            batchSize: 100
+        )
+
+        // Variables to hold the results
+        var hourlyStats: [BolusStats] = []
+        var dailyStats: [BolusStats] = []
+
+        // Process CoreData results within the context's thread
+        await bolusTaskContext.perform {
+            guard let fetchedResults = results as? [BolusStored] else {
+                return
+            }
+
+            let calendar = Calendar.current
+
+            // Group entries by hour for hourly statistics
+            // TODO: - Introduce paging to also be able to show complete history
+
+            let now = Date()
+            let twentyDaysAgo = Calendar.current.date(byAdding: .day, value: -10, to: now) ?? now
+
+            let hourlyGrouped = Dictionary(grouping: fetchedResults.filter { entry in
+                guard let date = entry.pumpEvent?.timestamp else { return false }
+                return date >= twentyDaysAgo && date <= now
+            }) { entry in
+                let components = calendar.dateComponents(
+                    [.year, .month, .day, .hour],
+                    from: entry.pumpEvent?.timestamp ?? Date()
+                )
+                return calendar.date(from: components) ?? Date()
+            }
+
+            // Group entries by day for daily statistics
+            let dailyGrouped = Dictionary(grouping: fetchedResults) { entry in
+                calendar.startOfDay(for: entry.pumpEvent?.timestamp ?? Date())
+            }
+
+            // Process hourly stats
+            hourlyStats = hourlyGrouped.keys.sorted().map { timePoint in
+                let entries = hourlyGrouped[timePoint, default: []]
+                return BolusStats(
+                    date: timePoint,
+                    manualBolus: entries.reduce(0.0) { sum, entry in
+                        if !entry.isSMB, !entry.isExternal {
+                            return sum + (entry.amount?.doubleValue ?? 0)
+                        }
+                        return sum
+                    },
+                    smb: entries.reduce(0.0) { sum, entry in
+                        if entry.isSMB {
+                            return sum + (entry.amount?.doubleValue ?? 0)
+                        }
+                        return sum
+                    },
+                    external: entries.reduce(0.0) { sum, entry in
+                        if entry.isExternal {
+                            return sum + (entry.amount?.doubleValue ?? 0)
+                        }
+                        return sum
+                    }
+                )
+            }
+
+            // Process daily stats
+            dailyStats = dailyGrouped.keys.sorted().map { timePoint in
+                let entries = dailyGrouped[timePoint, default: []]
+                return BolusStats(
+                    date: timePoint,
+                    manualBolus: entries.reduce(0.0) { sum, entry in
+                        if !entry.isSMB, !entry.isExternal {
+                            return sum + (entry.amount?.doubleValue ?? 0)
+                        }
+                        return sum
+                    },
+                    smb: entries.reduce(0.0) { sum, entry in
+                        if entry.isSMB {
+                            return sum + (entry.amount?.doubleValue ?? 0)
+                        }
+                        return sum
+                    },
+                    external: entries.reduce(0.0) { sum, entry in
+                        if entry.isExternal {
+                            return sum + (entry.amount?.doubleValue ?? 0)
+                        }
+                        return sum
+                    }
+                )
+            }
+        }
+
+        return (hourlyStats, dailyStats)
+    }
+
+    /// Calculates and caches the daily averages of bolus insulin
+    ///
+    /// This function:
+    /// 1. Groups bolus statistics by day
+    /// 2. Calculates average total, carb and correction bolus for each day
+    /// 3. Caches the results for later use
+    ///
+    /// This only needs to be called once during subscribe.
+    private func calculateAndCacheBolusAverages() async {
+        let calendar = Calendar.current
+
+        // Calculate averages in context
+        let dailyAverages = await bolusTaskContext.perform { [dailyBolusStats] in
+            // Group by days
+            let groupedByDay = Dictionary(grouping: dailyBolusStats) { stat in
+                calendar.startOfDay(for: stat.date)
+            }
+
+            // Calculate averages for each day
+            var averages: [Date: (Double, Double, Double)] = [:]
+            for (day, stats) in groupedByDay {
+                let total = stats.reduce((0.0, 0.0, 0.0)) { acc, stat in
+                    (acc.0 + stat.manualBolus, acc.1 + stat.smb, acc.2 + stat.external)
+                }
+                let count = Double(stats.count)
+                averages[day] = (total.0 / count, total.1 / count, total.2 / count)
+            }
+            return averages
+        }
+
+        // Update cache on main thread
+        await MainActor.run {
+            self.bolusAveragesCache = dailyAverages
+        }
+    }
+
+    /// Returns the average bolus values for the given date range from the cache
+    /// - Parameter range: A tuple containing the start and end dates to get averages for
+    /// - Returns: A tuple containing the average total, carb and correction bolus values for the date range
+    func getCachedBolusAverages(for range: (start: Date, end: Date)) -> (manual: Double, smb: Double, external: Double) {
+        return calculateBolusAveragesForDateRange(from: range.start, to: range.end)
+    }
+
+    /// Calculates the average bolus values for a given date range
+    /// - Parameters:
+    ///   - startDate: The start date of the range to calculate averages for
+    ///   - endDate: The end date of the range to calculate averages for
+    /// - Returns: A tuple containing the average total, carb and correction bolus values for the date range
+    func calculateBolusAveragesForDateRange(
+        from startDate: Date,
+        to endDate: Date
+    ) -> (manual: Double, smb: Double, external: Double) {
+        // Filter cached values to only include those within the date range
+        let relevantStats = bolusAveragesCache.filter { date, _ in
+            date >= startDate && date <= endDate
+        }
+
+        // Return zeros if no data exists for the range
+        guard !relevantStats.isEmpty else { return (0, 0, 0) }
+
+        // Calculate total bolus across all days
+        let total = relevantStats.values.reduce((0.0, 0.0, 0.0)) { acc, avg in
+            (acc.0 + avg.0, acc.1 + avg.1, acc.2 + avg.2)
+        }
+
+        // Calculate averages by dividing totals by number of days
+        let count = Double(relevantStats.count)
+
+        return (total.0 / count, total.1 / count, total.2 / count)
+    }
+}
+
+/// Extension to convert Decimal to Double
+private extension Decimal {
+    var doubleValue: Double {
+        NSDecimalNumber(decimal: self).doubleValue
+    }
+}

+ 216 - 0
Trio/Sources/Modules/Stat/StatStateModel+Setup/LoopChartSetup.swift

@@ -0,0 +1,216 @@
+import CoreData
+import Foundation
+
+/// Represents statistical data about loop execution success/failure for a specific time period
+struct LoopStatsByPeriod: Identifiable {
+    /// The date representing this time period
+    let period: Date
+    /// Number of successful loop executions in this period
+    let successful: Int
+    /// Number of failed loop executions in this period
+    let failed: Int
+    /// Median duration of loop executions in this period
+    let medianDuration: Double
+    /// Number of glucose measurements in this period
+    let glucoseCount: Int
+    /// Total number of loop executions in this period
+    var total: Int { successful + failed }
+    /// Percentage of successful loops (0-100)
+    var successPercentage: Double { total > 0 ? Double(successful) / Double(total) * 100 : 0 }
+    /// Percentage of failed loops (0-100)
+    var failurePercentage: Double { total > 0 ? Double(failed) / Double(total) * 100 : 0 }
+    /// Unique identifier for this period, using the period date
+    var id: Date { period }
+}
+
+extension Stat.StateModel {
+    /// Initiates the process of fetching and processing loop statistics
+    /// This function coordinates three main tasks:
+    /// 1. Fetching loop stat record IDs for the selected duration
+    /// 2. Calculating grouped statistics for the Loop stats chart
+    /// 3. Updating loop stat records on the main thread (!) for the Loop duration chart
+    func setupLoopStatRecords() {
+        Task {
+            let (recordIDs, failedRecordIDs) = await self.fetchLoopStatRecords(for: selectedDurationForLoopStats)
+
+            // Update loop records for duration chart
+            await self.updateLoopStatRecords(allLoopIds: recordIDs)
+
+            // Calculate statistics and update on main thread
+            let stats = await self.getLoopStats(
+                allLoopIds: recordIDs,
+                failedLoopIds: failedRecordIDs,
+                duration: selectedDurationForLoopStats
+            )
+
+            await MainActor.run {
+                self.loopStats = stats
+            }
+        }
+    }
+
+    /// Fetches loop statistics records for the specified duration
+    /// - Parameter duration: The time period to fetch records for
+    /// - Returns: A tuple containing arrays of NSManagedObjectIDs for (all loops, failed loops)
+    func fetchLoopStatRecords(for duration: Duration) async -> ([NSManagedObjectID], [NSManagedObjectID]) {
+        // Calculate the date range based on selected duration
+        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 .Week:
+            startDate = now.addingTimeInterval(-7.days.timeInterval)
+        case .Month:
+            startDate = now.addingTimeInterval(-30.days.timeInterval)
+        case .Total:
+            startDate = now.addingTimeInterval(-90.days.timeInterval)
+        }
+
+        // Perform both fetches asynchronously
+        async let allLoopsResult = CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: LoopStatRecord.self,
+            onContext: loopTaskContext,
+            predicate: NSPredicate(format: "start > %@", startDate as NSDate),
+            key: "start",
+            ascending: false
+        )
+
+        async let failedLoopsResult = CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: LoopStatRecord.self,
+            onContext: loopTaskContext,
+            predicate: NSPredicate(
+                format: "start > %@ AND loopStatus != %@",
+                startDate as NSDate,
+                "Success"
+            ),
+            key: "start",
+            ascending: false
+        )
+
+        // Wait for both results and convert to object IDs
+        let (allLoops, failedLoops) = await (allLoopsResult, failedLoopsResult)
+
+        return (
+            (allLoops as? [LoopStatRecord] ?? []).map(\.objectID),
+            (failedLoops as? [LoopStatRecord] ?? []).map(\.objectID)
+        )
+    }
+
+    /// Updates the loopStatRecords array on the main thread with records from the provided IDs
+    /// - Parameters:
+    ///   - allLoopIds: Array of NSManagedObjectIDs for all loop records
+    @MainActor func updateLoopStatRecords(allLoopIds: [NSManagedObjectID]) {
+        loopStatRecords = allLoopIds.compactMap { id -> LoopStatRecord? in
+            do {
+                return try viewContext.existingObject(with: id) as? LoopStatRecord
+            } catch {
+                debugPrint("\(DebuggingIdentifiers.failed) Error fetching loop stat: \(error)")
+                return nil
+            }
+        }
+    }
+
+    /// Calculates loop and glucose statistics based on the provided record IDs
+    /// - Parameters:
+    ///   - allLoopIds: Array of NSManagedObjectIDs for all loop records
+    ///   - failedLoopIds: Array of NSManagedObjectIDs for failed loop records
+    ///   - duration: The time period for statistics calculation
+    /// - Returns: Array of tuples containing category, count and percentage for each statistic
+    func getLoopStats(
+        allLoopIds: [NSManagedObjectID],
+        failedLoopIds: [NSManagedObjectID],
+        duration: Duration
+    ) async -> [(category: String, count: Int, percentage: 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 .Week:
+            startDate = now.addingTimeInterval(-7.days.timeInterval)
+        case .Month:
+            startDate = now.addingTimeInterval(-30.days.timeInterval)
+        case .Total:
+            startDate = now.addingTimeInterval(-90.days.timeInterval)
+        }
+
+        // Get glucose statistics
+        let totalGlucose = await calculateGlucoseStats(from: startDate, to: now)
+
+        // Get NSManagedObject
+        let allLoops = await CoreDataStack.shared.getNSManagedObject(with: allLoopIds, context: loopTaskContext)
+        let failedLoops = await CoreDataStack.shared.getNSManagedObject(with: failedLoopIds, context: loopTaskContext)
+
+        return await loopTaskContext.perform {
+            let totalLoopsCount = allLoops.count
+            let failedLoopsCount = failedLoops.count
+            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 [
+                    ("Loop Success Rate", successfulLoops, loopPercentage),
+                    ("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 [
+                    ("Successful Loops", Int(round(averageLoopsPerDay)), loopPercentage),
+                    ("Glucose Count", Int(round(averageGlucosePerDay)), glucosePercentage)
+                ]
+            }
+        }
+    }
+
+    /// Fetches and calculates glucose statistics for the given time period
+    /// - Parameters:
+    ///   - startDate: The start date of the period to analyze
+    ///   - now: The current date (end of period)
+    /// - Returns: Number of glucose readings in the period
+    private func calculateGlucoseStats(
+        from startDate: Date,
+        to _: Date
+    ) async -> Int {
+        // Create predicate for glucose readings
+        let glucosePredicate = NSPredicate(format: "date >= %@", startDate as NSDate)
+
+        // Fetch glucose readings asynchronously
+        let glucoseResult = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: GlucoseStored.self,
+            onContext: loopTaskContext,
+            predicate: glucosePredicate,
+            key: "date",
+            ascending: false
+        )
+
+        return await loopTaskContext.perform {
+            guard let readings = glucoseResult as? [GlucoseStored] else {
+                return 0
+            }
+            return readings.count
+        }
+    }
+}

+ 175 - 0
Trio/Sources/Modules/Stat/StatStateModel+Setup/MealStatsSetup.swift

@@ -0,0 +1,175 @@
+import CoreData
+import Foundation
+
+/// Represents statistical data about meal macronutrients for a specific day
+struct MealStats: Identifiable {
+    let id = UUID()
+    /// The date representing this time period
+    let date: Date
+    /// Total carbohydrates in grams
+    let carbs: Double
+    /// Total fat in grams
+    let fat: Double
+    /// Total protein in grams
+    let protein: Double
+}
+
+extension Stat.StateModel {
+    /// Sets up meal statistics by fetching and processing meal data
+    ///
+    /// This function:
+    /// 1. Fetches hourly and daily meal statistics asynchronously
+    /// 2. Updates the state model with the fetched statistics on the main actor
+    /// 3. Calculates and caches initial daily averages
+    func setupMealStats() {
+        Task {
+            let (hourly, daily) = await fetchMealStats()
+
+            await MainActor.run {
+                self.hourlyMealStats = hourly
+                self.dailyMealStats = daily
+            }
+
+            // Initially calculate and cache daily averages
+            await calculateAndCacheDailyAverages()
+        }
+    }
+
+    /// Fetches and processes meal statistics from Core Data
+    /// - Returns: A tuple containing hourly and daily meal statistics arrays
+    ///
+    /// This function:
+    /// 1. Fetches carbohydrate entries from Core Data
+    /// 2. Groups entries by hour and day
+    /// 3. Calculates total macronutrients for each time period
+    /// 4. Returns the processed statistics as (hourly: [MealStats], daily: [MealStats])
+    private func fetchMealStats() async -> (hourly: [MealStats], daily: [MealStats]) {
+        // Fetch CarbEntryStored entries from Core Data
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: CarbEntryStored.self,
+            onContext: mealTaskContext,
+            predicate: NSPredicate.carbsForStats,
+            key: "date",
+            ascending: true,
+            batchSize: 100
+        )
+
+        return await mealTaskContext.perform {
+            // Safely unwrap the fetched results, return empty arrays if nil
+            guard let fetchedResults = results as? [CarbEntryStored] else { return ([], []) }
+
+            let calendar = Calendar.current
+
+            // Group entries by hour for hourly statistics
+            // TODO: - Introduce paging to also be able to show complete history
+
+            let now = Date()
+            let twentyDaysAgo = Calendar.current.date(byAdding: .day, value: -10, to: now) ?? now
+
+            let hourlyGrouped = Dictionary(grouping: fetchedResults.filter { entry in
+                guard let date = entry.date else { return false }
+                return date >= twentyDaysAgo && date <= now
+            }) { entry in
+                let components = calendar.dateComponents([.year, .month, .day, .hour], from: entry.date ?? Date())
+                return calendar.date(from: components) ?? Date()
+            }
+
+            // Group entries by day for daily statistics
+            let dailyGrouped = Dictionary(grouping: fetchedResults) { entry in
+                calendar.startOfDay(for: entry.date ?? Date())
+            }
+
+            // Calculate statistics for each hour
+            let hourlyStats = hourlyGrouped.keys.sorted().map { timePoint in
+                let entries = hourlyGrouped[timePoint, default: []]
+                return MealStats(
+                    date: timePoint,
+                    carbs: entries.reduce(0.0) { $0 + $1.carbs },
+                    fat: entries.reduce(0.0) { $0 + $1.fat },
+                    protein: entries.reduce(0.0) { $0 + $1.protein }
+                )
+            }
+
+            // Calculate statistics for each day
+            let dailyStats = dailyGrouped.keys.sorted().map { timePoint in
+                let entries = dailyGrouped[timePoint, default: []]
+                return MealStats(
+                    date: timePoint,
+                    carbs: entries.reduce(0.0) { $0 + $1.carbs },
+                    fat: entries.reduce(0.0) { $0 + $1.fat },
+                    protein: entries.reduce(0.0) { $0 + $1.protein }
+                )
+            }
+
+            return (hourlyStats, dailyStats)
+        }
+    }
+
+    /// Calculates and caches the daily averages of macronutrients
+    ///
+    /// This function:
+    /// 1. Groups meal statistics by day
+    /// 2. Calculates average carbs, fat and protein for each day
+    /// 3. Caches the results for later use
+    ///
+    /// This only needs to be called once during subscribe.
+    private func calculateAndCacheDailyAverages() async {
+        let calendar = Calendar.current
+
+        // Calculate averages in context
+        let dailyAverages = await mealTaskContext.perform { [dailyMealStats] in
+            // Group by days
+            let groupedByDay = Dictionary(grouping: dailyMealStats) { stat in
+                calendar.startOfDay(for: stat.date)
+            }
+
+            // Calculate averages for each day
+            var averages: [Date: (Double, Double, Double)] = [:]
+            for (day, stats) in groupedByDay {
+                let total = stats.reduce((0.0, 0.0, 0.0)) { acc, stat in
+                    (acc.0 + stat.carbs, acc.1 + stat.fat, acc.2 + stat.protein)
+                }
+                let count = Double(stats.count)
+                averages[day] = (total.0 / count, total.1 / count, total.2 / count)
+            }
+            return averages
+        }
+
+        // Update cache on main thread
+        await MainActor.run {
+            self.dailyAveragesCache = dailyAverages
+        }
+    }
+
+    /// Returns the average macronutrient values for the given date range from the cache
+    /// - Parameter range: A tuple containing the start and end dates to get averages for
+    /// - Returns: A tuple containing the average carbs, fat and protein values for the date range
+    func getCachedMealAverages(for range: (start: Date, end: Date)) -> (carbs: Double, fat: Double, protein: Double) {
+        return calculateAveragesForDateRange(from: range.start, to: range.end)
+    }
+
+    /// Calculates the average macronutrient values for a given date range
+    /// - Parameters:
+    ///   - startDate: The start date of the range to calculate averages for
+    ///   - endDate: The end date of the range to calculate averages for
+    /// - Returns: A tuple containing the average carbs, fat and protein values for the date range
+    func calculateAveragesForDateRange(from startDate: Date, to endDate: Date) -> (carbs: Double, fat: Double, protein: Double) {
+        // Filter cached values to only include those within the date range
+        let relevantStats = dailyAveragesCache.filter { date, _ in
+            date >= startDate && date <= endDate
+        }
+
+        // Return zeros if no data exists for the range
+        guard !relevantStats.isEmpty else { return (0, 0, 0) }
+
+        // Calculate total macronutrients across all days
+        let total = relevantStats.values.reduce((0.0, 0.0, 0.0)) { acc, avg in
+            (acc.0 + avg.0, acc.1 + avg.1, acc.2 + avg.2)
+        }
+
+        // Calculate averages by dividing totals by number of days
+        let count = Double(relevantStats.count)
+
+        return (total.0 / count, total.1 / count, total.2 / count)
+    }
+}

+ 132 - 0
Trio/Sources/Modules/Stat/StatStateModel+Setup/StackedChartSetup.swift

@@ -0,0 +1,132 @@
+import CoreData
+import Foundation
+
+/// Represents the distribution of glucose values within specific ranges for each hour.
+///
+/// This struct is used to visualize how glucose values are distributed across different
+/// ranges (e.g., low, normal, high) throughout the day. Each range has a name and
+/// corresponding hourly values showing the percentage of readings in that range.
+///
+/// Example ranges and their meanings:
+/// - "<54": Urgent low
+/// - "54-70": Low
+/// - "70-140": Target range
+/// - "140-180": High
+/// - "180-200": Very high
+/// - "200-220": Very high+
+/// - ">220": Urgent high
+///
+/// Example usage:
+/// ```swift
+/// let range = GlucoseRangeStats(
+///     name: "70-140",           // Target range
+///     values: [
+///         (hour: 8, count: 75), // 75% of readings at 8 AM were in range
+///         (hour: 9, count: 80)  // 80% of readings at 9 AM were in range
+///     ]
+/// )
+/// ```
+///
+/// This data structure is used to create stacked area charts showing the
+/// distribution of glucose values across different ranges for each hour of the day.
+public struct GlucoseRangeStats: Identifiable {
+    /// The name of the glucose range (e.g., "70-140", "<54")
+    let name: String
+
+    /// Array of tuples containing the hour and percentage of readings in this range
+    /// - hour: Hour of the day (0-23)
+    /// - count: Percentage of readings in this range for the given hour (0-100)
+    let values: [(hour: Int, count: Int)]
+
+    /// Unique identifier for the range, derived from its name
+    public var id: String { name }
+}
+
+extension Stat.StateModel {
+    /// Calculates hourly glucose range distribution statistics.
+    /// The calculation runs asynchronously using the CoreData context.
+    ///
+    /// The calculation works as follows:
+    /// 1. Count unique days for each hour to handle missing data
+    /// 2. For each glucose range and hour:
+    ///    - Count readings in that range
+    ///    - Calculate percentage based on number of days with readings
+    ///
+    /// Example:
+    /// If we have data for 7 days and at 6:00 AM:
+    /// - 3 days had readings in range 70-140
+    /// - 2 days had readings in range 140-180
+    /// - 2 day had a reading in range 180-200
+    /// Then for 6:00 AM:
+    /// - 70-140 = (3/7)*100 = 42.9%
+    /// - 140-180 = (2/7)*100 = 28.6%
+    /// - 180-200 = (2/7)*100 = 28.6%
+    func calculateGlucoseRangeStatsForStackedChart(from ids: [NSManagedObjectID]) async {
+        let taskContext = CoreDataStack.shared.newTaskContext()
+
+        let calendar = Calendar.current
+
+        let stats = await taskContext.perform {
+            // Convert IDs to GlucoseStored objects using the context
+            let readings = ids.compactMap { id -> GlucoseStored? in
+                do {
+                    return try taskContext.existingObject(with: id) as? GlucoseStored
+                } catch {
+                    debugPrint("\(DebuggingIdentifiers.failed) Error fetching glucose: \(error)")
+                    return nil
+                }
+            }
+
+            // Count unique days for each hour
+            let daysPerHour = (0 ... 23).map { hour in
+                let uniqueDays = Set(readings.compactMap { reading -> Date? in
+                    guard let date = reading.date else { return nil }
+                    if calendar.component(.hour, from: date) == hour {
+                        return calendar.startOfDay(for: date)
+                    }
+                    return nil
+                })
+                return (hour: hour, days: uniqueDays.count)
+            }
+
+            // Define glucose ranges and their conditions
+            // Ranges are processed from bottom to top in the stacked chart
+            let ranges: [(name: String, condition: (Int) -> Bool)] = [
+                ("<54", { g in g <= 54 }),
+                ("54-70", { g in g > 54 && g < 70 }),
+                ("70-140", { g in g >= 70 && g <= 140 }),
+                ("140-180", { g in g > 140 && g <= 180 }),
+                ("180-200", { g in g > 180 && g <= 200 }),
+                ("200-220", { g in g > 200 && g <= 220 }),
+                (">220", { g in g > 220 })
+            ]
+
+            // Process each range to create the chart data
+            return ranges.map { rangeName, condition in
+                // Calculate values for each hour within this range
+                let hourlyValues = (0 ... 23).map { hour in
+                    let totalDaysForHour = Double(daysPerHour[hour].days)
+                    // Skip if no data for this hour
+                    guard totalDaysForHour > 0 else { return (hour: hour, count: 0) }
+
+                    // Count readings that match the range condition for this hour
+                    let readingsInRange = readings.filter { reading in
+                        guard let date = reading.date else { return false }
+                        return calendar.component(.hour, from: date) == hour &&
+                            condition(Int(reading.glucose))
+                    }.count
+
+                    // Convert to percentage based on number of days with data
+                    let percentage = (Double(readingsInRange) / totalDaysForHour) * 100.0
+                    return (hour: hour, count: Int(percentage))
+                }
+                return GlucoseRangeStats(name: rangeName, values: hourlyValues)
+            }
+        }
+
+        // Update stats on main thread
+        await MainActor.run {
+            self.glucoseRangeStats = stats
+        }
+    }
+}

+ 165 - 0
Trio/Sources/Modules/Stat/StatStateModel+Setup/TDDSetup.swift

@@ -0,0 +1,165 @@
+import CoreData
+import Foundation
+
+/// Represents statistical data about Total Daily Dose for a specific time period
+struct TDDStats: Identifiable {
+    let id = UUID()
+    /// The date representing this time period
+    let date: Date
+    /// Total insulin in units
+    let amount: Double
+}
+
+extension Stat.StateModel {
+    /// Sets up TDD statistics by fetching and processing insulin data
+    func setupTDDStats() {
+        Task {
+            let (hourly, daily) = await fetchTDDStats()
+
+            await MainActor.run {
+                self.hourlyTDDStats = hourly
+                self.dailyTDDStats = daily
+            }
+
+            // Initially calculate and cache daily averages
+            await calculateAndCacheTDDAverages()
+        }
+    }
+
+    /// Fetches and processes Total Daily Dose (TDD) statistics from CoreData
+    /// - Returns: A tuple containing hourly and daily TDD statistics arrays
+    /// - Note: Processes both hourly statistics for the last 10 days and complete daily statistics
+    private func fetchTDDStats() async -> (hourly: [TDDStats], daily: [TDDStats]) {
+        // Fetch temp basal records from CoreData
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: TempBasalStored.self,
+            onContext: tddTaskContext,
+            predicate: NSPredicate.pumpHistoryForStats,
+            key: "pumpEvent.timestamp",
+            ascending: true,
+            batchSize: 100
+        )
+
+        var hourlyStats: [TDDStats] = []
+        var dailyStats: [TDDStats] = []
+
+        await tddTaskContext.perform {
+            guard let fetchedResults = results as? [TempBasalStored] else {
+                return
+            }
+
+            let calendar = Calendar.current
+
+            // Calculate date range for hourly statistics (last 10 days)
+            // TODO: - Introduce paging to also be able to show complete history
+            let now = Date()
+            let tenDaysAgo = Calendar.current.date(byAdding: .day, value: -10, to: now) ?? now
+
+            // Group entries by hour for hourly statistics, filtering for last 10 days only
+            let hourlyGrouped = Dictionary(grouping: fetchedResults.filter { entry in
+                guard let date = entry.pumpEvent?.timestamp else { return false }
+                return date >= tenDaysAgo && date <= now
+            }) { entry in
+                // Create date components for hour-level grouping
+                let components = calendar.dateComponents(
+                    [.year, .month, .day, .hour],
+                    from: entry.pumpEvent?.timestamp ?? Date()
+                )
+                return calendar.date(from: components) ?? Date()
+            }
+
+            // Group entries by day for complete daily statistics
+            let dailyGrouped = Dictionary(grouping: fetchedResults) { entry in
+                calendar.startOfDay(for: entry.pumpEvent?.timestamp ?? Date())
+            }
+
+            // Process hourly statistics
+            hourlyStats = hourlyGrouped.keys.sorted().map { timePoint in
+                let entries = hourlyGrouped[timePoint, default: []]
+                // Calculate total insulin for each hour
+                return TDDStats(
+                    date: timePoint,
+                    amount: entries.reduce(0.0) { sum, entry in
+                        sum + (entry.rate?.doubleValue ?? 0) * Double(entry.duration) / 60.0
+                    }
+                )
+            }
+
+            // Process daily statistics
+            dailyStats = dailyGrouped.keys.sorted().map { timePoint in
+                let entries = dailyGrouped[timePoint, default: []]
+                // Calculate total insulin for each day
+                return TDDStats(
+                    date: timePoint,
+                    amount: entries.reduce(0.0) { sum, entry in
+                        sum + (entry.rate?.doubleValue ?? 0) * Double(entry.duration) / 60.0
+                    }
+                )
+            }
+        }
+
+        return (hourlyStats, dailyStats)
+    }
+
+    /// Calculates and caches the daily averages of Total Daily Dose (TDD) insulin values
+    /// - Note: This function runs asynchronously and updates the tddAveragesCache on the main actor
+    private func calculateAndCacheTDDAverages() async {
+        // Get calendar for date calculations
+        let calendar = Calendar.current
+
+        // Calculate daily averages on background context
+        let dailyAverages = await tddTaskContext.perform { [dailyTDDStats] in
+            // Group TDD stats by calendar day
+            let groupedByDay = Dictionary(grouping: dailyTDDStats) { stat in
+                calendar.startOfDay(for: stat.date)
+            }
+
+            // Calculate average TDD for each day
+            var averages: [Date: Double] = [:]
+            for (day, stats) in groupedByDay {
+                // Sum up all TDD values for the day
+                let total = stats.reduce(0.0) { $0 + $1.amount }
+                let count = Double(stats.count)
+                // Store average in dictionary
+                averages[day] = total / count
+            }
+            return averages
+        }
+
+        // Update cache on main actor
+        await MainActor.run {
+            self.tddAveragesCache = dailyAverages
+        }
+    }
+
+    /// Gets the cached average Total Daily Dose (TDD) of insulin for a specified date range
+    /// - Parameter range: A tuple containing the start and end dates to get averages for
+    /// - Returns: The average TDD in units for the specified date range
+    func getCachedTDDAverages(for range: (start: Date, end: Date)) -> Double {
+        // Calculate and return the TDD averages for the given date range using cached values
+        calculateTDDAveragesForDateRange(from: range.start, to: range.end)
+    }
+
+    /// Calculates the average Total Daily Dose (TDD) of insulin for a specified date range
+    /// - Parameters:
+    ///   - startDate: The start date of the range to calculate averages for
+    ///   - endDate: The end date of the range to calculate averages for
+    /// - Returns: The average TDD in units for the specified date range. Returns 0.0 if no data exists.
+    private func calculateTDDAveragesForDateRange(from startDate: Date, to endDate: Date) -> Double {
+        // Filter cached TDD values to only include those within the date range
+        let relevantStats = tddAveragesCache.filter { date, _ in
+            date >= startDate && date <= endDate
+        }
+
+        // Return 0 if no data exists for the specified range
+        guard !relevantStats.isEmpty else { return 0.0 }
+
+        // Calculate total TDD by summing all values
+        let total = relevantStats.values.reduce(0.0, +)
+        // Convert count to Double for floating point division
+        let count = Double(relevantStats.count)
+
+        // Return average TDD
+        return total / count
+    }
+}

+ 172 - 11
Trio/Sources/Modules/Stat/StatStateModel.swift

@@ -5,32 +5,170 @@ import SwiftUI
 import Swinject
 
 extension Stat {
+    /// Defines the available types of glucose charts
+    enum GlucoseChartType: String, CaseIterable {
+        /// Ambulatory Glucose Profile showing percentile ranges
+        case percentile = "Percentile"
+        /// Time-based distribution of glucose ranges
+        case distribution = "Distribution"
+    }
+
+    /// Defines the available types of insulin charts
+    enum InsulinChartType: String, CaseIterable {
+        /// Shows total daily insulin doses
+        case totalDailyDose = "Total Daily Dose"
+        /// Shows distribution of bolus types
+        case bolusDistribution = "Bolus Distribution"
+    }
+
+    /// Defines the available types of looping charts
+    enum LoopingChartType: String, CaseIterable {
+        /// Shows loop completion and success rates
+        case loopingPerformance = "Looping Performance"
+        /// Shows CGM connection status over time
+        case cgmConnectionTrace = "CGM Connection Trace"
+        /// Shows Trio pump uptime statistics
+        case trioUpTime = "Trio Up-Time"
+    }
+
+    /// Defines the available types of meal charts
+    enum MealChartType: String, CaseIterable {
+        /// Shows total meal statistics
+        case totalMeals = "Total Meals"
+        /// Shows correlation between meals and glucose excursions
+        case mealToHypoHyperDistribution = "Meal to Hypo/Hyper"
+    }
+
     @Observable final class StateModel: BaseStateModel<Provider> {
         @ObservationIgnored @Injected() var settings: SettingsManager!
-        var highLimit: Decimal = 10 / 0.0555
-        var lowLimit: Decimal = 4 / 0.0555
+        var highLimit: Decimal = 180
+        var lowLimit: Decimal = 70
         var hbA1cDisplayUnit: HbA1cDisplayUnit = .percent
         var timeInRangeChartStyle: TimeInRangeChartStyle = .vertical
         var units: GlucoseUnits = .mgdL
         var glucoseFromPersistence: [GlucoseStored] = []
+        var loopStatRecords: [LoopStatRecord] = []
+        var loopStats: [(category: String, count: Int, percentage: Double)] = []
+        var groupedLoopStats: [LoopStatsByPeriod] = []
+        var tddStats: [TDD] = []
+        var bolusStats: [BolusStats] = []
+        var hourlyStats: [HourlyStats] = []
+        var glucoseRangeStats: [GlucoseRangeStats] = []
+
+        // Cache for Meal Stats
+        var hourlyMealStats: [MealStats] = []
+        var dailyMealStats: [MealStats] = []
+        var dailyAveragesCache: [Date: (carbs: Double, fat: Double, protein: Double)] = [:]
+
+        // Cache for TDD Stats
+        var hourlyTDDStats: [TDDStats] = []
+        var dailyTDDStats: [TDDStats] = []
+        var tddAveragesCache: [Date: Double] = [:]
+
+        // Cache for Bolus Stats
+        var hourlyBolusStats: [BolusStats] = []
+        var dailyBolusStats: [BolusStats] = []
+        var bolusAveragesCache: [Date: (manual: Double, smb: Double, external: Double)] = [:]
+
+        // Selected Duration for Glucose Stats
+        var selectedDurationForGlucoseStats: Duration = .Today {
+            didSet {
+                setupGlucoseArray(for: selectedDurationForGlucoseStats)
+            }
+        }
+
+        // Selected Duration for Insulin Stats
+        var selectedDurationForInsulinStats: StatsTimeInterval = .Day
+
+        // Selected Duration for Meal Stats
+        var selectedDurationForMealStats: StatsTimeInterval = .Day
+
+        // Selected Duration for Loop Stats
+        var selectedDurationForLoopStats: Duration = .Today {
+            didSet {
+                setupLoopStatRecords()
+            }
+        }
+
+        // Selected Glucose Chart Type
+        var selectedGlucoseChartType: GlucoseChartType = .percentile
+
+        // Selected Insulin Chart Type
+        var selectedInsulinChartType: InsulinChartType = .totalDailyDose
 
-        var selectedDuration: Duration = .Today
+        // Selected Looping Chart Type
+        var selectedLoopingChartType: LoopingChartType = .loopingPerformance
 
-        private let context = CoreDataStack.shared.newTaskContext()
-        private let viewContext = CoreDataStack.shared.persistentContainer.viewContext
+        // Selected Meal Chart Type
+        var selectedMealChartType: MealChartType = .totalMeals
 
+        // Fetching Contexts
+        let context = CoreDataStack.shared.newTaskContext()
+        let viewContext = CoreDataStack.shared.persistentContainer.viewContext
+        let tddTaskContext = CoreDataStack.shared.newTaskContext()
+        let loopTaskContext = CoreDataStack.shared.newTaskContext()
+        let mealTaskContext = CoreDataStack.shared.newTaskContext()
+        let bolusTaskContext = CoreDataStack.shared.newTaskContext()
+
+        /// Defines the available time periods for duration-based statistics
         enum Duration: String, CaseIterable, Identifiable {
+            /// Current day
             case Today
-            case Day
-            case Week
-            case Month
-            case Total
+            /// Single day view
+            case Day = "D"
+            /// Week view
+            case Week = "W"
+            /// Month view
+            case Month = "M"
+            /// Three month view
+            case Total = "3 M"
+
+            var id: Self { self }
+        }
+
+        /// Defines the available time intervals for statistical analysis
+        enum StatsTimeInterval: String, CaseIterable, Identifiable {
+            /// Single day interval
+            case Day = "D"
+            /// Week interval
+            case Week = "W"
+            /// Month interval
+            case Month = "M"
+            /// Three month interval
+            case Total = "3 M"
+
             var id: Self { self }
         }
 
+        /// Defines the main categories of statistics available in the app
+        enum StatisticViewType: String, CaseIterable, Identifiable {
+            /// Glucose-related statistics including AGP and distributions
+            case glucose
+            /// Insulin delivery statistics including TDD and bolus distributions
+            case insulin
+            /// Loop performance and system status statistics
+            case looping
+            /// Meal-related statistics and correlations
+            case meals
+
+            var id: String { rawValue }
+
+            var title: String {
+                switch self {
+                case .glucose: return "Glucose"
+                case .insulin: return "Insulin"
+                case .looping: return "Looping"
+                case .meals: return "Meals"
+                }
+            }
+        }
+
         override func subscribe() {
-            /// Default is today
             setupGlucoseArray(for: .Today)
+            setupTDDStats()
+            setupBolusStats()
+            setupLoopStatRecords()
+            setupMealStats()
             highLimit = settingsManager.settings.high
             lowLimit = settingsManager.settings.low
             units = settingsManager.settings.units
@@ -40,8 +178,13 @@ extension Stat {
 
         func setupGlucoseArray(for duration: Duration) {
             Task {
-                let ids = await self.fetchGlucose(for: duration)
+                let ids = await fetchGlucose(for: duration)
                 await updateGlucoseArray(with: ids)
+
+                // Calculate hourly stats and glucose range stats asynchronously with fetched glucose IDs
+                async let hourlyStats: () = calculateHourlyStatsForGlucoseAreaChart(from: ids)
+                async let glucoseRangeStats: () = calculateGlucoseRangeStatsForStackedChart(from: ids)
+                _ = await (hourlyStats, glucoseRangeStats)
             }
         }
 
@@ -91,4 +234,22 @@ extension Stat {
             }
         }
     }
+
+    @Observable final class UpdateTimer {
+        private var workItem: DispatchWorkItem?
+
+        /// Schedules a delayed update action
+        /// - Parameter action: The closure to execute after the delay
+        /// Cancels any previously scheduled update before scheduling a new one
+        func scheduleUpdate(action: @escaping () -> Void) {
+            workItem?.cancel()
+
+            let newWorkItem = DispatchWorkItem {
+                action()
+            }
+            workItem = newWorkItem
+
+            DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: newWorkItem)
+        }
+    }
 }

+ 0 - 296
Trio/Sources/Modules/Stat/View/ChartsView.swift

@@ -1,296 +0,0 @@
-import Charts
-import CoreData
-import SwiftDate
-import SwiftUI
-
-struct ChartsView: View {
-    let highLimit: Decimal
-    let lowLimit: Decimal
-    let units: GlucoseUnits
-    let hbA1cDisplayUnit: HbA1cDisplayUnit
-    let timeInRangeChartStyle: TimeInRangeChartStyle
-
-    let glucose: [GlucoseStored]
-
-    @State var headline: Color = .secondary
-
-    private var conversionFactor: Decimal {
-        units == .mmolL ? 0.0555 : 1
-    }
-
-    var body: some View {
-        glucoseChart
-        Rectangle().fill(.cyan.opacity(0.2)).frame(maxHeight: 3)
-        if timeInRangeChartStyle == .horizontal {
-            VStack {
-                tirChartLaying
-                Rectangle().fill(.cyan.opacity(0.2)).frame(maxHeight: 3)
-                groupedGlucoseStatsLaying
-            }
-        } else {
-            HStack(spacing: 20) {
-                tirChartStanding
-                groupedGlucoseStatsStanding
-            }.padding(.horizontal, 10)
-        }
-    }
-
-    init(
-        highLimit: Decimal,
-        lowLimit: Decimal,
-        units: GlucoseUnits,
-        hbA1cDisplayUnit: HbA1cDisplayUnit,
-        timeInRangeChartStyle: TimeInRangeChartStyle,
-        glucose: [GlucoseStored]
-    ) {
-        self.highLimit = highLimit
-        self.lowLimit = lowLimit
-        self.units = units
-        self.hbA1cDisplayUnit = hbA1cDisplayUnit
-        self.timeInRangeChartStyle = timeInRangeChartStyle
-        self.glucose = glucose
-    }
-
-    var glucoseChart: some View {
-        let low = lowLimit * conversionFactor
-        let high = highLimit * conversionFactor
-        let count = glucose.count
-        // The symbol size when fewer readings are larger
-        let size: CGFloat = count < 20 ? 50 : count < 50 ? 35 : count > 2000 ? 5 : 15
-
-        return Chart {
-            ForEach(glucose) { item in
-                if item.glucose > Int(highLimit) {
-                    PointMark(
-                        x: .value("Time", item.date ?? Date(), unit: .second),
-                        y: .value("Value", Decimal(item.glucose) * conversionFactor)
-                    ).foregroundStyle(Color.orange.gradient).symbolSize(size).interpolationMethod(.cardinal)
-                } else if item.glucose < Int(lowLimit) {
-                    PointMark(
-                        x: .value("Time", item.date ?? Date(), unit: .second),
-                        y: .value("Value", Decimal(item.glucose) * conversionFactor)
-                    ).foregroundStyle(Color.red.gradient).symbolSize(size).interpolationMethod(.cardinal)
-                } else {
-                    PointMark(
-                        x: .value("Time", item.date ?? Date(), unit: .second),
-                        y: .value("Value", Decimal(item.glucose) * conversionFactor)
-                    ).foregroundStyle(Color.green.gradient).symbolSize(size).interpolationMethod(.cardinal)
-                }
-            }
-        }
-        .chartYAxis {
-            AxisMarks(
-                values: [
-                    0,
-                    low,
-                    high,
-                    units == .mmolL ? 15 : 270
-                ]
-            )
-        }
-    }
-
-    var tirChartLaying: some View {
-        let fetched = tir()
-        let low = lowLimit * conversionFactor
-        let high = highLimit * conversionFactor
-        let fraction = units == .mgdL ? 0 : 1
-
-        let data: [ShapeModel] = [
-            .init(
-                type: NSLocalizedString(
-                    "Low",
-                    comment: ""
-                ) + " (<\(low.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fraction)))))",
-                percent: fetched[0].decimal
-            ),
-            .init(type: NSLocalizedString("In Range", comment: ""), percent: fetched[1].decimal),
-            .init(
-                type: NSLocalizedString(
-                    "High",
-                    comment: ""
-                ) + " (>\(high.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fraction)))))",
-                percent: fetched[2].decimal
-            )
-        ]
-        return VStack {
-            Chart(data) { shape in
-                BarMark(
-                    x: .value("TIR", shape.percent)
-                )
-                .foregroundStyle(by: .value("Group", shape.type))
-                .annotation(position: .top, alignment: .center) {
-                    Text(
-                        "\(shape.percent, format: .number.precision(.fractionLength(0 ... 1))) %"
-                    ).font(.footnote).foregroundColor(.secondary)
-                }
-            }
-            .chartXAxis(.hidden)
-            .chartForegroundStyleScale([
-                NSLocalizedString(
-                    "Low",
-                    comment: ""
-                ) + " (<\(low.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fraction)))))": .red,
-                NSLocalizedString("In Range", comment: ""): .green,
-                NSLocalizedString(
-                    "High",
-                    comment: ""
-                ) + " (>\(high.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fraction)))))": .orange
-            ]).frame(maxHeight: 25)
-        }
-        .frame(maxWidth: .infinity, alignment: .center) // Align the entire VStack to center
-        .padding(.horizontal, 5)
-    }
-
-    var tirChartStanding: some View {
-        let fetched = tir()
-        let low = lowLimit * conversionFactor
-        let high = highLimit * conversionFactor
-        let fraction = units == .mmolL ? 1 : 0
-        let data: [ShapeModel] = [
-            .init(
-                type: NSLocalizedString(
-                    "Low",
-                    comment: ""
-                ) + " (< \(low.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fraction)))))",
-                percent: fetched[0].decimal
-            ),
-            .init(
-                type: "\(low.formatted(.number.precision(.fractionLength(fraction)))) - \(high.formatted(.number.precision(.fractionLength(fraction))))",
-                percent: fetched[1].decimal
-            ),
-            .init(
-                type: NSLocalizedString(
-                    "High",
-                    comment: ""
-                ) + " (> \(high.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fraction)))))",
-                percent: fetched[2].decimal
-            )
-        ]
-        return Chart(data) { shape in
-            BarMark(
-                x: .value("Shape", shape.type),
-                y: .value("Percentage", shape.percent)
-            )
-            .foregroundStyle(by: .value("Group", shape.type))
-            .annotation(position: shape.percent > 19 ? .overlay : .automatic, alignment: .center) {
-                Text(shape.percent == 0 ? "" : "\(shape.percent, format: .number.precision(.fractionLength(0 ... 1)))")
-            }
-        }
-        .chartXAxis(.hidden)
-        .chartYAxis {
-            AxisMarks(
-                format: Decimal.FormatStyle.Percent.percent.scale(1)
-            )
-        }
-        .chartForegroundStyleScale([
-            NSLocalizedString(
-                "Low",
-                comment: ""
-            ) + " (< \(low.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fraction)))))": .red,
-            "\(low.formatted(.number.precision(.fractionLength(fraction)))) - \(high.formatted(.number.precision(.fractionLength(fraction))))": .green,
-            NSLocalizedString(
-                "High",
-                comment: ""
-            ) + " (> \(high.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fraction)))))": .orange
-        ])
-    }
-
-    var groupedGlucoseStatsStanding: some View {
-        VStack(alignment: .leading, spacing: 15) {
-            let mapGlucose = glucose.compactMap({ each in each.glucose })
-            if !mapGlucose.isEmpty {
-                let mapGlucoseAcuteLow = mapGlucose.filter({ $0 < Int16(3.3 / 0.0555) })
-                let mapGlucoseHigh = mapGlucose.filter({ $0 > Int16(11 / 0.0555) })
-                let mapGlucoseNormal = mapGlucose.filter({ $0 > Int16(3.8 / 0.0555) && $0 < Int16(7.9 / 0.0555) })
-                HStack {
-                    let value = 100.0 * Double(mapGlucoseHigh.count) / Double(mapGlucose.count)
-                    Text(units == .mmolL ? ">  11  " : ">  198 ")
-                        .frame(width: 45, alignment: .leading)
-                        .foregroundColor(.secondary)
-                    Text("\(value.formatted(.number.precision(.fractionLength(0 ... 1)))) %")
-                        .frame(width: 45, alignment: .trailing)
-                        .foregroundColor(.orange)
-                }.font(.caption)
-                HStack {
-                    let value = 100.0 * Double(mapGlucoseNormal.count) / Double(mapGlucose.count)
-                    Text(units == .mmolL ? "3.9-7.8" : "70-140")
-                        .frame(width: 45, alignment: .leading)
-                        .foregroundColor(.secondary)
-                    Text("\(value.formatted(.number.precision(.fractionLength(0 ... 1)))) %")
-                        .frame(width: 45, alignment: .trailing)
-                        .foregroundColor(.green)
-                }.font(.caption)
-                HStack {
-                    let value = 100.0 * Double(mapGlucoseAcuteLow.count) / Double(mapGlucose.count)
-                    Text(units == .mmolL ? "<  3.3 " : "<  59  ")
-                        .frame(width: 45, alignment: .leading)
-                        .foregroundColor(.secondary)
-                    Text("\(value.formatted(.number.precision(.fractionLength(0 ... 1)))) %")
-                        .frame(width: 45, alignment: .trailing)
-                        .foregroundColor(.red)
-                }.font(.caption)
-            }
-        }
-    }
-
-    var groupedGlucoseStatsLaying: some View {
-        HStack {
-            let mapGlucose = glucose.compactMap({ each in each.glucose })
-            if !mapGlucose.isEmpty {
-                let mapGlucoseLow = mapGlucose.filter({ $0 < Int16(3.3 / 0.0555) })
-                let mapGlucoseNormal = mapGlucose.filter({ $0 > Int16(3.8 / 0.0555) && $0 < Int16(7.9 / 0.0555) })
-                let mapGlucoseAcuteHigh = mapGlucose.filter({ $0 > Int16(11 / 0.0555) })
-                Spacer()
-                HStack {
-                    let value = 100.0 * Double(mapGlucoseLow.count) / Double(mapGlucose.count)
-                    Text(units == .mmolL ? "< 3.3" : "< 59").font(.caption2).foregroundColor(.secondary)
-                    Text(value.formatted(.number.precision(.fractionLength(0 ... 1)))).font(.caption)
-                        .foregroundColor(value == 0 ? .green : .red)
-                    Text("%").font(.caption)
-                }
-                Spacer()
-                HStack {
-                    let value = 100.0 * Double(mapGlucoseNormal.count) / Double(mapGlucose.count)
-                    Text(units == .mmolL ? "3.9-7.8" : "70-140").foregroundColor(.secondary)
-                    Text(value.formatted(.number.precision(.fractionLength(0 ... 1)))).foregroundColor(.green)
-                    Text("%").foregroundColor(.secondary)
-                }.font(.caption)
-                Spacer()
-                HStack {
-                    let value = 100.0 * Double(mapGlucoseAcuteHigh.count) / Double(mapGlucose.count)
-                    Text(units == .mmolL ? "> 11.0" : "> 198").font(.caption).foregroundColor(.secondary)
-                    Text(value.formatted(.number.precision(.fractionLength(0 ... 1)))).font(.caption)
-                        .foregroundColor(value == 0 ? .green : .orange)
-                    Text("%").font(.caption)
-                }
-                Spacer()
-            }
-        }
-    }
-
-    private func tir() -> [(decimal: Decimal, string: String)] {
-        let hypoLimit = Int(lowLimit)
-        let hyperLimit = Int(highLimit)
-
-        let justGlucoseArray = glucose.compactMap({ each in Int(each.glucose as Int16) })
-        let totalReadings = justGlucoseArray.count
-
-        let hyperArray = glucose.filter({ $0.glucose > hyperLimit })
-        let hyperReadings = hyperArray.compactMap({ each in each.glucose as Int16 }).count
-        let hyperPercentage = Double(hyperReadings) / Double(totalReadings) * 100
-
-        let hypoArray = glucose.filter({ $0.glucose < hypoLimit })
-        let hypoReadings = hypoArray.compactMap({ each in each.glucose as Int16 }).count
-        let hypoPercentage = Double(hypoReadings) / Double(totalReadings) * 100
-
-        let tir = 100 - (hypoPercentage + hyperPercentage)
-
-        var array: [(decimal: Decimal, string: String)] = []
-        array.append((decimal: Decimal(hypoPercentage), string: "Low"))
-        array.append((decimal: Decimal(tir), string: "NormaL"))
-        array.append((decimal: Decimal(hyperPercentage), string: "High"))
-
-        return array
-    }
-}

+ 315 - 110
Trio/Sources/Modules/Stat/View/StatRootView.swift

@@ -1,150 +1,355 @@
 import Charts
-import CoreData
 import SwiftDate
 import SwiftUI
 import Swinject
 
 extension Stat {
     struct RootView: BaseView {
-        let resolver: Resolver
-        @State var state = StateModel()
+        enum Constants {
+            static let spacing: CGFloat = 16
+            static let cornerRadius: CGFloat = 10
+            static let backgroundOpacity = 0.1
+        }
 
+        let resolver: Resolver
         @Environment(\.colorScheme) var colorScheme
         @Environment(AppState.self) var appState
 
-        @State var paddingAmount: CGFloat? = 10
-        @State var headline: Color = .secondary
-        @State var days: Double = 0
-        @State var pointSize: CGFloat = 3
-        @State var conversionFactor = 0.0555
-
-        @ViewBuilder func stats() -> some View {
-            ZStack {
-                Color.gray.opacity(0.05).ignoresSafeArea(.all)
-                let filter = DateFilter()
-                switch state.selectedDuration {
-                case .Today:
-                    StatsView(
-                        filter: filter.today,
+        @State var state = StateModel()
+        @State private var selectedView: StateModel.StatisticViewType = .glucose
+
+        var body: some View {
+            VStack {
+                Picker("View", selection: $selectedView) {
+                    ForEach(StateModel.StatisticViewType.allCases) { viewType in
+                        Text(viewType.title).tag(viewType)
+                    }
+                }
+                .pickerStyle(.segmented)
+                .padding(.horizontal)
+
+                ScrollView {
+                    VStack(spacing: Constants.spacing) {
+                        switch selectedView {
+                        case .glucose:
+                            glucoseView
+                        case .insulin:
+                            insulinView
+                        case .looping:
+                            loopingView
+                        case .meals:
+                            mealsView
+                        }
+                    }
+                    .padding()
+                }
+            }
+            .background(appState.trioBackgroundColor(for: colorScheme))
+            .onAppear(perform: configureView)
+            .navigationBarTitleDisplayMode(.inline)
+            .navigationTitle("Statistics")
+            .toolbar {
+                ToolbarItem(placement: .topBarLeading) {
+                    Button(action: state.hideModal) {
+                        Text("Close")
+                            .foregroundColor(.tabBar)
+                    }
+                }
+            }
+        }
+
+        // MARK: - Stats View
+
+        @ViewBuilder var glucoseView: some View {
+            HStack {
+                Text("Chart Type")
+                    .font(.headline)
+
+                Spacer()
+
+                Picker("Glucose Chart Type", selection: $state.selectedGlucoseChartType) {
+                    ForEach(GlucoseChartType.allCases, id: \.self) { type in
+                        Text(type.rawValue)
+                    }
+                }
+                .pickerStyle(.menu)
+            }.padding(.horizontal)
+
+            Picker("Duration", selection: $state.selectedDurationForGlucoseStats) {
+                ForEach(StateModel.Duration.allCases, id: \.self) { timeInterval in
+                    Text(timeInterval.rawValue)
+                }
+            }
+            .pickerStyle(.segmented)
+
+            if state.glucoseFromPersistence.isEmpty {
+                ContentUnavailableView(
+                    "No Glucose Data",
+                    systemImage: "chart.bar.fill",
+                    description: Text("Glucose statistics will appear here once data is available.")
+                )
+            } else {
+                timeInRangeCard
+                glucoseStatsCard
+            }
+        }
+
+        private var timeInRangeCard: some View {
+            StatCard {
+                VStack(spacing: Constants.spacing) {
+                    switch state.selectedGlucoseChartType {
+                    case .percentile:
+                        GlucosePercentileChart(
+                            glucose: state.glucoseFromPersistence,
+                            highLimit: state.highLimit,
+                            lowLimit: state.lowLimit,
+                            units: state.units,
+                            hourlyStats: state.hourlyStats,
+                            isToday: state.selectedDurationForGlucoseStats == .Today
+                        )
+                    case .distribution:
+                        GlucoseDistributionChart(
+                            glucose: state.glucoseFromPersistence,
+                            highLimit: state.highLimit,
+                            lowLimit: state.lowLimit,
+                            units: state.units,
+                            glucoseRangeStats: state.glucoseRangeStats
+                        )
+                    }
+
+                    Divider()
+
+                    SectorChart(
                         highLimit: state.highLimit,
                         lowLimit: state.lowLimit,
                         units: state.units,
-                        hbA1cDisplayUnit: state.hbA1cDisplayUnit
+                        glucose: state.glucoseFromPersistence
                     )
-                case .Day:
-                    StatsView(
-                        filter: filter.day,
+                }
+            }
+        }
+
+        private var glucoseStatsCard: some View {
+            StatCard {
+                VStack(spacing: Constants.spacing) {
+                    BareStatisticsView.HbA1cView(
                         highLimit: state.highLimit,
                         lowLimit: state.lowLimit,
                         units: state.units,
-                        hbA1cDisplayUnit: state.hbA1cDisplayUnit
+                        hbA1cDisplayUnit: state.hbA1cDisplayUnit,
+                        glucose: state.glucoseFromPersistence
                     )
-                case .Week:
-                    StatsView(
-                        filter: filter.week,
+
+                    Divider()
+
+                    BareStatisticsView.BloodGlucoseView(
                         highLimit: state.highLimit,
                         lowLimit: state.lowLimit,
                         units: state.units,
-                        hbA1cDisplayUnit: state.hbA1cDisplayUnit
+                        hbA1cDisplayUnit: state.hbA1cDisplayUnit,
+                        glucose: state.glucoseFromPersistence
                     )
-                case .Month:
-                    StatsView(
-                        filter: filter.month,
-                        highLimit: state.highLimit,
-                        lowLimit: state.lowLimit,
-                        units: state.units,
-                        hbA1cDisplayUnit: state.hbA1cDisplayUnit
+                }
+            }
+        }
+
+        @ViewBuilder var insulinView: some View {
+            HStack {
+                Text("Chart Type")
+                    .font(.headline)
+
+                Spacer()
+
+                Picker("Insulin Chart Type", selection: $state.selectedInsulinChartType) {
+                    ForEach(InsulinChartType.allCases, id: \.self) { type in
+                        Text(type.rawValue)
+                    }
+                }.pickerStyle(.menu)
+            }.padding(.horizontal)
+
+            Picker("Duration", selection: $state.selectedDurationForInsulinStats) {
+                ForEach(StateModel.StatsTimeInterval.allCases) { timeInterval in
+                    Text(timeInterval.rawValue).tag(timeInterval)
+                }
+            }
+            .pickerStyle(.segmented)
+
+            StatCard {
+                switch state.selectedInsulinChartType {
+                case .totalDailyDose:
+                    if state.dailyTDDStats.isEmpty {
+                        ContentUnavailableView(
+                            "No TDD Data",
+                            systemImage: "chart.bar.xaxis",
+                            description: Text("Total Daily Doses will appear here once data is available.")
+                        )
+                    } else {
+                        TDDChartView(
+                            selectedDuration: $state.selectedDurationForInsulinStats,
+                            tddStats: state.selectedDurationForInsulinStats == .Day ?
+                                state.hourlyTDDStats : state.dailyTDDStats,
+                            state: state
+                        )
+                    }
+
+                case .bolusDistribution:
+                    // TODO: -
+                    var hasBolusData: Bool {
+                        state.dailyBolusStats.contains { $0.manualBolus > 0 || $0.smb > 0 || $0.external > 0 }
+                    }
+
+                    // TODO: -
+                    if state.dailyBolusStats.isEmpty || !hasBolusData {
+                        ContentUnavailableView(
+                            "No Bolus Data",
+                            systemImage: "cross.vial",
+                            description: Text("Bolus statistics will appear here once data is available.")
+                        )
+                    } else {
+                        BolusStatsView(
+                            selectedDuration: $state.selectedDurationForInsulinStats,
+                            bolusStats: state.selectedDurationForInsulinStats == .Day ?
+                                state.hourlyBolusStats : state.dailyBolusStats,
+                            state: state
+                        )
+                    }
+                }
+            }
+        }
+
+        @ViewBuilder var loopingView: some View {
+            HStack {
+                Text("Chart Type")
+                    .font(.headline)
+
+                Spacer()
+
+                Picker("Looping Chart Type", selection: $state.selectedLoopingChartType) {
+                    ForEach(LoopingChartType.allCases, id: \.self) { type in
+                        Text(type.rawValue)
+                    }
+                }.pickerStyle(.menu)
+            }.padding(.horizontal)
+
+            Picker("Duration", selection: $state.selectedDurationForLoopStats) {
+                ForEach(StateModel.Duration.allCases, id: \.self) { duration in
+                    Text(duration.rawValue)
+                }
+            }
+            .pickerStyle(.segmented)
+
+            switch state.selectedLoopingChartType {
+            case .loopingPerformance:
+                if state.loopStatRecords.isEmpty {
+                    ContentUnavailableView(
+                        "No Loop Data",
+                        systemImage: "clock.arrow.2.circlepath",
+                        description: Text("Loop statistics will appear here once data is available.")
                     )
-                case .Total:
-                    StatsView(
-                        filter: filter.total,
+                } else {
+                    loopsCard
+                    loopStats
+                }
+            case .trioUpTime:
+                Text("Not yet implemented")
+            case .cgmConnectionTrace:
+                Text("Not yet implemented")
+            }
+        }
+
+        private var loopsCard: some View {
+            StatCard {
+                VStack(spacing: Constants.spacing) {
+                    LoopStatsView(
+                        loopStatRecords: state.loopStatRecords,
+                        selectedDuration: state.selectedDurationForLoopStats,
+                        statsData: state.loopStats
+                    )
+                }
+            }
+        }
+
+        private var loopStats: some View {
+            StatCard {
+                VStack(spacing: Constants.spacing) {
+                    BareStatisticsView.LoopsView(
                         highLimit: state.highLimit,
                         lowLimit: state.lowLimit,
                         units: state.units,
-                        hbA1cDisplayUnit: state.hbA1cDisplayUnit
+                        hbA1cDisplayUnit: state.hbA1cDisplayUnit,
+                        loopStatRecords: state.loopStatRecords
                     )
                 }
             }
         }
 
-        @ViewBuilder func chart() -> some View {
-            switch state.selectedDuration {
-            case .Today:
-                ChartsView(
-                    highLimit: state.highLimit,
-                    lowLimit: state.lowLimit,
-                    units: state.units,
-                    hbA1cDisplayUnit: state.hbA1cDisplayUnit,
-                    timeInRangeChartStyle: state.timeInRangeChartStyle,
-                    glucose: state.glucoseFromPersistence
-                )
-            case .Day:
-                ChartsView(
-                    highLimit: state.highLimit,
-                    lowLimit: state.lowLimit,
-                    units: state.units,
-                    hbA1cDisplayUnit: state.hbA1cDisplayUnit,
-                    timeInRangeChartStyle: state.timeInRangeChartStyle,
-                    glucose: state.glucoseFromPersistence
-                )
-            case .Week:
-                ChartsView(
-                    highLimit: state.highLimit,
-                    lowLimit: state.lowLimit,
-                    units: state.units,
-                    hbA1cDisplayUnit: state.hbA1cDisplayUnit,
-                    timeInRangeChartStyle: state.timeInRangeChartStyle,
-                    glucose: state.glucoseFromPersistence
-                )
-            case .Month:
-                ChartsView(
-                    highLimit: state.highLimit,
-                    lowLimit: state.lowLimit,
-                    units: state.units,
-                    hbA1cDisplayUnit: state.hbA1cDisplayUnit,
-                    timeInRangeChartStyle: state.timeInRangeChartStyle,
-                    glucose: state.glucoseFromPersistence
-                )
-            case .Total:
-                ChartsView(
-                    highLimit: state.highLimit,
-                    lowLimit: state.lowLimit,
-                    units: state.units,
-                    hbA1cDisplayUnit: state.hbA1cDisplayUnit,
-                    timeInRangeChartStyle: state.timeInRangeChartStyle,
-                    glucose: state.glucoseFromPersistence
-                )
-            }
-        }
+        @ViewBuilder var mealsView: some View {
+            HStack {
+                Text("Chart Type")
+                    .font(.headline)
 
-        var body: some View {
-            VStack(alignment: .center) {
-                chart().padding(.top, 20)
-                Picker("Duration", selection: $state.selectedDuration) {
-                    ForEach(Stat.StateModel.Duration.allCases) { duration in
-                        Text(NSLocalizedString(duration.rawValue, comment: "")).tag(Optional(duration))
+                Spacer()
+
+                Picker("Meal Chart Type", selection: $state.selectedMealChartType) {
+                    ForEach(MealChartType.allCases, id: \.self) { type in
+                        Text(type.rawValue)
                     }
-                }.onChange(of: state.selectedDuration) { _, newValue in
-                    state.setupGlucoseArray(for: newValue)
+                }.pickerStyle(.menu)
+            }.padding(.horizontal)
+
+            Picker("Duration", selection: $state.selectedDurationForMealStats) {
+                ForEach(StateModel.StatsTimeInterval.allCases, id: \.self) { timeInterval in
+                    Text(timeInterval.rawValue)
                 }
-                .pickerStyle(.segmented).background(.cyan.opacity(0.2))
-                stats()
-            }.background(appState.trioBackgroundColor(for: colorScheme))
-                .onAppear(perform: configureView)
-                .navigationBarTitle("Statistics")
-                .navigationBarTitleDisplayMode(.automatic)
-                .toolbar {
-                    ToolbarItem(placement: .topBarLeading, content: {
-                        Button(
-                            action: { state.hideModal() },
-                            label: {
-                                HStack {
-                                    Text("Close")
-                                }
-                            }
+            }
+            .pickerStyle(.segmented)
+
+            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",
+                            systemImage: "fork.knife",
+                            description: Text("Meal statistics will appear here once data is available.")
+                        )
+                    } else {
+                        MealStatsView(
+                            selectedDuration: $state.selectedDurationForMealStats,
+                            mealStats: state.selectedDurationForMealStats == .Day ?
+                                state.hourlyMealStats : state.dailyMealStats,
+                            state: state
                         )
-                    })
+                    }
+                case .mealToHypoHyperDistribution:
+                    Text("TODO: Meal to Hypoglycemia/Hyperglycemia Distribution")
                 }
+            }
         }
     }
 }
+
+// MARK: - Supporting Views
+
+struct StatCard<Content: View>: View {
+    let content: Content
+
+    init(@ViewBuilder content: () -> Content) {
+        self.content = content()
+    }
+
+    var body: some View {
+        content
+            .padding()
+            .background(
+                RoundedRectangle(cornerRadius: Stat.RootView.Constants.cornerRadius)
+                    .fill(Color.secondary.opacity(Stat.RootView.Constants.backgroundOpacity))
+            )
+    }
+}

+ 0 - 287
Trio/Sources/Modules/Stat/View/StatsView.swift

@@ -1,287 +0,0 @@
-import CoreData
-import SwiftDate
-import SwiftUI
-
-struct StatsView: View {
-    @FetchRequest var fetchRequest: FetchedResults<LoopStatRecord>
-    @FetchRequest var glucose: FetchedResults<GlucoseStored>
-
-    @State var headline: Color = .secondary
-
-    var highLimit: Decimal
-    var lowLimit: Decimal
-    var units: GlucoseUnits
-    var hbA1cDisplayUnit: HbA1cDisplayUnit
-
-    private let conversionFactor = 0.0555
-
-    var body: some View {
-        VStack(spacing: 10) {
-            loops
-            Divider()
-            hba1c
-            Divider()
-            bloodGlucose
-        }
-    }
-
-    init(
-        filter: NSDate,
-        highLimit: Decimal,
-        lowLimit: Decimal,
-        units: GlucoseUnits,
-        hbA1cDisplayUnit: HbA1cDisplayUnit
-    ) {
-        _fetchRequest = FetchRequest<LoopStatRecord>(
-            sortDescriptors: [NSSortDescriptor(key: "start", ascending: false)],
-            predicate: NSPredicate(format: "interval > 0 AND start > %@", filter)
-        )
-
-        _glucose = FetchRequest<GlucoseStored>(
-            sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)],
-            predicate: NSPredicate(format: "glucose > 0 AND date > %@", filter)
-        )
-
-        self.highLimit = highLimit
-        self.lowLimit = lowLimit
-        self.units = units
-        self.hbA1cDisplayUnit = hbA1cDisplayUnit
-    }
-
-    var loops: some View {
-        let loops = fetchRequest
-        // 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(headline)
-                    Text(loopNr.formatted())
-                }
-                VStack(spacing: 5) {
-                    Text("Interval").font(.subheadline).foregroundColor(headline)
-                    Text(intervalAverage.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + " min")
-                }
-                VStack(spacing: 5) {
-                    Text("Duration").font(.subheadline).foregroundColor(headline)
-                    Text(
-                        (medianDuration * 60)
-                            .formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + " s"
-                    )
-                }
-                VStack(spacing: 5) {
-                    Text("Success").font(.subheadline).foregroundColor(headline)
-                    Text(
-                        ((successRate ?? 100) / 100)
-                            .formatted(.percent.grouping(.never).rounded().precision(.fractionLength(1)))
-                    )
-                }
-            }
-        }
-    }
-
-    private 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])
-    }
-
-    private 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]
-    }
-
-    var hba1c: some View {
-        HStack(spacing: 50) {
-            let useUnit: GlucoseUnits = {
-                if hbA1cDisplayUnit == .mmolMol { return .mmolL }
-                else { return .mgdL }
-            }()
-
-            let hba1cs = 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
-
-            let hba1cString = (
-                useUnit == .mmolL ? hba1cs.ifcc
-                    .formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) : hba1cs.ngsp
-                    .formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))
-                    + " %"
-            )
-            VStack(spacing: 5) {
-                Text("HbA1c").font(.subheadline).foregroundColor(headline)
-                Text(hba1cString)
-            }
-            VStack(spacing: 5) {
-                Text("SD").font(.subheadline).foregroundColor(.secondary)
-                Text(
-                    hba1cs.sd
-                        .formatted(
-                            .number.grouping(.never).rounded()
-                                .precision(.fractionLength(units == .mmolL ? 1 : 0))
-                        )
-                )
-            }
-            VStack(spacing: 5) {
-                Text("CV").font(.subheadline).foregroundColor(.secondary)
-                Text(hba1cs.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))))
-            }
-        }
-    }
-
-    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 / 24 h").font(.subheadline)
-                    .foregroundColor(.secondary)
-                Text(bgs.readings.formatted(.number.grouping(.never).rounded().precision(.fractionLength(0))))
-            }
-            VStack(spacing: 5) {
-                Text("Average").font(.subheadline).foregroundColor(headline)
-                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))
-                        )
-                )
-            }
-        }
-    }
-
-    private 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 = medianCalculation(array: justGlucoseArray)
-
-        var NGSPa1CStatisticValue = 0.0
-        var IFCCa1CStatisticValue = 0.0
-
-        if numberOfDays > 0 {
-            NGSPa1CStatisticValue = (glucoseAverage + 46.7) / 28.7 // NGSP (%)
-            IFCCa1CStatisticValue = 10.929 *
-                (NGSPa1CStatisticValue - 2.152) // IFCC (mmol/mol)  A1C(mmol/mol) = 10.929 * (A1C(%) - 2.15)
-        }
-        var sumOfSquares = 0.0
-
-        for array in justGlucoseArray {
-            sumOfSquares += pow(Double(array) - Double(glucoseAverage), 2)
-        }
-        var sd = 0.0
-        var cv = 0.0
-
-        // Avoid division by zero
-        if glucoseAverage > 0 {
-            sd = sqrt(sumOfSquares / Double(countReadings))
-            cv = sd / Double(glucoseAverage) * 100
-        }
-
-        var output: (ifcc: Double, ngsp: Double, average: Double, median: Double, sd: Double, cv: Double, readings: Double)
-        output = (
-            ifcc: IFCCa1CStatisticValue,
-            ngsp: NGSPa1CStatisticValue,
-            average: glucoseAverage * (units == .mmolL ? conversionFactor : 1),
-            median: medianGlucose * (units == .mmolL ? conversionFactor : 1),
-            sd: sd * (units == .mmolL ? conversionFactor : 1), cv: cv,
-            readings: Double(countReadings) / denominator
-        )
-        return output
-    }
-
-    private func tir() -> [(decimal: Decimal, string: String)] {
-        let justGlucoseArray = glucose.compactMap({ each in Int(each.glucose as Int16) })
-        let totalReadings = justGlucoseArray.count
-
-        let hyperArray = glucose.filter({ $0.glucose >= Int(highLimit) })
-        let hyperReadings = hyperArray.compactMap({ each in each.glucose as Int16 }).count
-        let hyperPercentage = Double(hyperReadings) / Double(totalReadings) * 100
-
-        let hypoArray = glucose.filter({ $0.glucose <= Int(lowLimit) })
-        let hypoReadings = hypoArray.compactMap({ each in each.glucose as Int16 }).count
-        let hypoPercentage = Double(hypoReadings) / Double(totalReadings) * 100
-
-        let tir = 100 - (hypoPercentage + hyperPercentage)
-
-        var array: [(decimal: Decimal, string: String)] = []
-        array.append((decimal: Decimal(hypoPercentage), string: "Low"))
-        array.append((decimal: Decimal(tir), string: "NormaL"))
-        array.append((decimal: Decimal(hyperPercentage), string: "High"))
-
-        return array
-    }
-}

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

@@ -0,0 +1,307 @@
+import CoreData
+import SwiftDate
+import SwiftUI
+
+struct BareStatisticsView {
+    // MARK: - Helper Functions
+
+    private 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 HbA1cView: View {
+        let highLimit: Decimal
+        let lowLimit: Decimal
+        let units: GlucoseUnits
+        let hbA1cDisplayUnit: HbA1cDisplayUnit
+        let glucose: [GlucoseStored]
+
+        var body: some View {
+            hba1c
+        }
+
+        private var hba1c: some View {
+            HStack(spacing: 50) {
+                let useUnit: GlucoseUnits = {
+                    if hbA1cDisplayUnit == .mmolMol { return .mmolL }
+                    else { return .mgdL }
+                }()
+
+                let hba1cs = 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
+
+                let hba1cString = (
+                    useUnit == .mmolL ? hba1cs.ifcc
+                        .formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) : hba1cs.ngsp
+                        .formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))
+                        + " %"
+                )
+                VStack(spacing: 5) {
+                    Text("HbA1c").font(.subheadline).foregroundColor(.secondary)
+                    Text(hba1cString)
+                }
+                VStack(spacing: 5) {
+                    Text("SD").font(.subheadline).foregroundColor(.secondary)
+                    Text(
+                        hba1cs.sd
+                            .formatted(
+                                .number.grouping(.never).rounded()
+                                    .precision(.fractionLength(units == .mmolL ? 1 : 0))
+                            )
+                    )
+                }
+                VStack(spacing: 5) {
+                    Text("CV").font(.subheadline).foregroundColor(.secondary)
+                    Text(hba1cs.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 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 BloodGlucoseView: View {
+        let highLimit: Decimal
+        let lowLimit: Decimal
+        let units: GlucoseUnits
+        let hbA1cDisplayUnit: HbA1cDisplayUnit
+        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 / 24 h").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 hbA1cDisplayUnit: HbA1cDisplayUnit
+        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))) + " min")
+                    }
+                    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)
+        }
+    }
+}

+ 421 - 0
Trio/Sources/Modules/Stat/View/ViewElements/BolusStatsView.swift

@@ -0,0 +1,421 @@
+import Charts
+import SwiftUI
+
+struct BolusStatsView: View {
+    @Binding var selectedDuration: Stat.StateModel.StatsTimeInterval
+    let bolusStats: [BolusStats]
+    let state: Stat.StateModel
+
+    @State private var scrollPosition = Date() // gets updated in onAppear block
+    @State private var selectedDate: Date?
+    @State private var currentAverages: (manual: Double, smb: Double, external: Double) = (0, 0, 0)
+    @State private var updateTimer = Stat.UpdateTimer()
+
+    /// Returns the time interval length for the visible domain based on selected duration
+    private var visibleDomainLength: TimeInterval {
+        switch selectedDuration {
+        case .Day: return 24 * 3600 // One day in seconds
+        case .Week: return 7 * 24 * 3600 // One week in seconds
+        case .Month: return 30 * 24 * 3600 // One month in seconds
+        case .Total: return 90 * 24 * 3600 // Three months in seconds
+        }
+    }
+
+    /// Calculates the visible date range based on scroll position and domain length
+    private var visibleDateRange: (start: Date, end: Date) {
+        let start = scrollPosition // Current scroll position marks the start
+        let end = start.addingTimeInterval(visibleDomainLength)
+        return (start, end)
+    }
+
+    /// Returns the appropriate date format style based on the selected time interval
+    private var dateFormat: Date.FormatStyle {
+        switch selectedDuration {
+        case .Day:
+            return .dateTime.hour()
+        case .Week:
+            return .dateTime.weekday(.abbreviated)
+        case .Month:
+            return .dateTime.day()
+        case .Total:
+            return .dateTime.month(.abbreviated)
+        }
+    }
+
+    /// Returns DateComponents for aligning dates based on the selected duration
+    private var alignmentComponents: DateComponents {
+        switch selectedDuration {
+        case .Day:
+            return DateComponents(hour: 0) // Align to midnight
+        case .Week:
+            return DateComponents(weekday: 2) // Monday is weekday 2
+        case .Month,
+             .Total:
+            return DateComponents(day: 1) // First day of month
+        }
+    }
+
+    /// Returns bolus statistics for a specific date
+    private func getBolusForDate(_ date: Date) -> BolusStats? {
+        let calendar = Calendar.current
+
+        return bolusStats.first { stat in
+            switch selectedDuration {
+            case .Day:
+                return calendar.isDate(stat.date, equalTo: date, toGranularity: .hour)
+            default:
+                return calendar.isDate(stat.date, inSameDayAs: date)
+            }
+        }
+    }
+
+    /// Updates the current averages for bolus insulin based on the visible date range
+    private func updateAverages() {
+        currentAverages = state.getCachedBolusAverages(for: visibleDateRange)
+    }
+
+    /// Formats the visible date range into a human-readable string
+    private func formatVisibleDateRange() -> String {
+        let start = visibleDateRange.start
+        let end = visibleDateRange.end
+        let calendar = Calendar.current
+        let today = Date()
+
+        // Special handling for Day view with relative dates
+        if selectedDuration == .Day {
+            let startDateText: String
+            let endDateText: String
+            let timeFormat = start.formatted(.dateTime.hour().minute())
+
+            // Format start date
+            if calendar.isDate(start, inSameDayAs: today) {
+                startDateText = "Today"
+            } else if calendar.isDate(start, inSameDayAs: calendar.date(byAdding: .day, value: -1, to: today)!) {
+                startDateText = "Yesterday"
+            } else if calendar.isDate(start, inSameDayAs: calendar.date(byAdding: .day, value: 1, to: today)!) {
+                startDateText = "Tomorrow"
+            } else {
+                startDateText = start.formatted(.dateTime.day().month())
+            }
+
+            // Format end date
+            if calendar.isDate(end, inSameDayAs: today) {
+                endDateText = "Today"
+            } else if calendar.isDate(end, inSameDayAs: calendar.date(byAdding: .day, value: -1, to: today)!) {
+                endDateText = "Yesterday"
+            } else if calendar.isDate(end, inSameDayAs: calendar.date(byAdding: .day, value: 1, to: today)!) {
+                endDateText = "Tomorrow"
+            } else {
+                endDateText = end.formatted(.dateTime.day().month())
+            }
+
+            // If start and end are on the same day, show date only once
+            if calendar.isDate(start, inSameDayAs: end) {
+                return "\(startDateText), \(timeFormat) - \(end.formatted(.dateTime.hour().minute()))"
+            }
+
+            return "\(startDateText), \(timeFormat) - \(endDateText), \(end.formatted(.dateTime.hour().minute()))"
+        }
+
+        // Standard format for other views - only show dates without time
+        let startText: String
+        let endText: String
+
+        // Check for relative dates
+        if calendar.isDate(start, inSameDayAs: today) {
+            startText = "Today"
+        } else if calendar.isDate(start, inSameDayAs: calendar.date(byAdding: .day, value: -1, to: today)!) {
+            startText = "Yesterday"
+        } else if calendar.isDate(start, inSameDayAs: calendar.date(byAdding: .day, value: 1, to: today)!) {
+            startText = "Tomorrow"
+        } else {
+            startText = start.formatted(.dateTime.day().month())
+        }
+
+        if calendar.isDate(end, inSameDayAs: today) {
+            endText = "Today"
+        } else if calendar.isDate(end, inSameDayAs: calendar.date(byAdding: .day, value: -1, to: today)!) {
+            endText = "Yesterday"
+        } else if calendar.isDate(end, inSameDayAs: calendar.date(byAdding: .day, value: 1, to: today)!) {
+            endText = "Tomorrow"
+        } else {
+            endText = end.formatted(.dateTime.day().month())
+        }
+
+        return "\(startText) - \(endText)"
+    }
+
+    private func isSameTimeUnit(_ date1: Date, _ date2: Date) -> Bool {
+        switch selectedDuration {
+        case .Day:
+            return Calendar.current.isDate(date1, equalTo: date2, toGranularity: .hour)
+        default:
+            return Calendar.current.isDate(date1, inSameDayAs: date2)
+        }
+    }
+
+    /// Returns the initial scroll position date based on the selected duration
+    private func getInitialScrollPosition() -> Date {
+        let calendar = Calendar.current
+        let now = Date()
+
+        switch selectedDuration {
+        case .Day:
+            return calendar.date(byAdding: .day, value: -1, to: now)!
+        case .Week:
+            return calendar.date(byAdding: .day, value: -7, to: now)!
+        case .Month:
+            return calendar.date(byAdding: .month, value: -1, to: now)!
+        case .Total:
+            return calendar.date(byAdding: .month, value: -3, to: now)!
+        }
+    }
+
+    var body: some View {
+        VStack(alignment: .leading, spacing: 8) {
+            statsView
+            chartsView
+        }
+        .onAppear {
+            scrollPosition = getInitialScrollPosition()
+            updateAverages()
+        }
+        .onChange(of: scrollPosition) {
+            updateTimer.scheduleUpdate {
+                updateAverages()
+            }
+        }
+        .onChange(of: selectedDuration) {
+            Task {
+                scrollPosition = getInitialScrollPosition()
+                updateAverages()
+            }
+        }
+    }
+
+    private var statsView: some View {
+        HStack {
+            Grid(alignment: .leading) {
+                GridRow {
+                    Text("Manual:")
+                        .font(.headline)
+                        .foregroundStyle(.secondary)
+                    Text(currentAverages.manual.formatted(.number.precision(.fractionLength(1))))
+                        .font(.headline)
+                        .foregroundStyle(.secondary)
+                        .gridColumnAlignment(.trailing)
+                    Text("U")
+                        .font(.headline)
+                        .foregroundStyle(.secondary)
+                }
+                GridRow {
+                    Text("SMB:")
+                        .font(.headline)
+                        .foregroundStyle(.secondary)
+                    Text(currentAverages.smb.formatted(.number.precision(.fractionLength(1))))
+                        .font(.headline)
+                        .foregroundStyle(.secondary)
+                        .gridColumnAlignment(.trailing)
+                    Text("U")
+                        .font(.headline)
+                        .foregroundStyle(.secondary)
+                }
+                GridRow {
+                    Text("External:")
+                        .font(.headline)
+                        .foregroundStyle(.secondary)
+                    Text(currentAverages.external.formatted(.number.precision(.fractionLength(1))))
+                        .font(.headline)
+                        .foregroundStyle(.secondary)
+                        .gridColumnAlignment(.trailing)
+                    Text("U")
+                        .font(.headline)
+                        .foregroundStyle(.secondary)
+                }
+            }
+
+            Spacer()
+
+            Text(formatVisibleDateRange())
+                .font(.footnote)
+                .foregroundStyle(.secondary)
+        }
+    }
+
+    private var chartsView: some View {
+        Chart {
+            ForEach(bolusStats) { stat in
+                // Total Bolus Bar
+                BarMark(
+                    x: .value("Date", stat.date, unit: selectedDuration == .Day ? .hour : .day),
+                    y: .value("Amount", stat.manualBolus)
+                )
+                .foregroundStyle(by: .value("Type", "Manual"))
+                .position(by: .value("Type", "Boluses"))
+                .opacity(
+                    selectedDate.map { date in
+                        isSameTimeUnit(stat.date, date) ? 1 : 0.3
+                    } ?? 1
+                )
+
+                // Carb Bolus Bar
+                BarMark(
+                    x: .value("Date", stat.date, unit: selectedDuration == .Day ? .hour : .day),
+                    y: .value("Amount", stat.smb)
+                )
+                .foregroundStyle(by: .value("Type", "SMB"))
+                .position(by: .value("Type", "Boluses"))
+                .opacity(
+                    selectedDate.map { date in
+                        isSameTimeUnit(stat.date, date) ? 1 : 0.3
+                    } ?? 1
+                )
+                // Correction Bolus Bar
+                BarMark(
+                    x: .value("Date", stat.date, unit: selectedDuration == .Day ? .hour : .day),
+                    y: .value("Amount", stat.external)
+                )
+                .foregroundStyle(by: .value("Type", "External"))
+                .position(by: .value("Type", "Boluses"))
+                .opacity(
+                    selectedDate.map { date in
+                        isSameTimeUnit(stat.date, date) ? 1 : 0.3
+                    } ?? 1
+                )
+            }
+
+            // Selection popover outside of the ForEach loop!
+            if let selectedDate, let selectedBolus = getBolusForDate(selectedDate)
+            {
+                RuleMark(
+                    x: .value("Selected Date", selectedDate)
+                )
+                .foregroundStyle(.secondary.opacity(0.5))
+                .annotation(
+                    position: .top,
+                    spacing: 0,
+                    overflowResolution: .init(x: .fit(to: .chart), y: .fit(to: .chart))
+                ) {
+                    BolusSelectionPopover(date: selectedDate, bolus: selectedBolus, selectedDuration: selectedDuration)
+                }
+            }
+        }
+        .chartForegroundStyleScale([
+            "SMB": Color.blue,
+            "Manual": Color.teal,
+            "External": Color.purple
+        ])
+        .chartLegend(position: .bottom, alignment: .leading, spacing: 12)
+        .chartYAxis {
+            AxisMarks(position: .trailing) { value in
+                if let amount = value.as(Double.self) {
+                    AxisValueLabel {
+                        Text(amount.formatted(.number.precision(.fractionLength(0))) + " U")
+                            .font(.footnote)
+                    }
+                    AxisGridLine()
+                }
+            }
+        }
+        .chartXAxis {
+            AxisMarks(preset: .aligned, values: .stride(by: selectedDuration == .Day ? .hour : .day)) { value in
+                if let date = value.as(Date.self) {
+                    let day = Calendar.current.component(.day, from: date)
+                    let hour = Calendar.current.component(.hour, from: date)
+
+                    switch selectedDuration {
+                    case .Day:
+                        if hour % 6 == 0 { // Show only every 6 hours
+                            AxisValueLabel(format: dateFormat, centered: true)
+                                .font(.footnote)
+                            AxisGridLine()
+                        }
+                    case .Month:
+                        if day % 5 == 0 { // Only show every 5th day
+                            AxisValueLabel(format: dateFormat, 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: dateFormat, centered: true)
+                                .font(.footnote)
+                            AxisGridLine()
+                        }
+                    default:
+                        AxisValueLabel(format: dateFormat, centered: true)
+                            .font(.footnote)
+                        AxisGridLine()
+                    }
+                }
+            }
+        }
+        .chartScrollableAxes(.horizontal)
+        .chartXSelection(value: $selectedDate.animation(.easeInOut))
+        .chartScrollPosition(x: $scrollPosition)
+        .chartScrollTargetBehavior(
+            .valueAligned(
+                matching: selectedDuration == .Day ?
+                    DateComponents(minute: 0) : // Align to next hour for Day view
+                    DateComponents(hour: 0), // Align to start of day for other views
+                majorAlignment: .matching(
+                    alignmentComponents
+                )
+            )
+        )
+        .chartXVisibleDomain(length: visibleDomainLength)
+        .frame(height: 250)
+    }
+}
+
+private struct BolusSelectionPopover: View {
+    let date: Date
+    let bolus: BolusStats
+    let selectedDuration: Stat.StateModel.StatsTimeInterval
+
+    private var timeText: String {
+        if selectedDuration == .Day {
+            let hour = Calendar.current.component(.hour, from: date)
+            return "\(hour):00-\(hour + 1):00"
+        } else {
+            return date.formatted(.dateTime.month().day())
+        }
+    }
+
+    var body: some View {
+        VStack(alignment: .leading, spacing: 4) {
+            Text(timeText)
+                .font(.footnote)
+                .fontWeight(.bold)
+
+            Grid(alignment: .leading) {
+                GridRow {
+                    Text("Manual:")
+                    Text(bolus.manualBolus.formatted(.number.precision(.fractionLength(1))))
+                        .gridColumnAlignment(.trailing)
+                    Text("U")
+                }
+                GridRow {
+                    Text("SMB:")
+                    Text(bolus.smb.formatted(.number.precision(.fractionLength(1))))
+                        .gridColumnAlignment(.trailing)
+                    Text("U")
+                }
+                GridRow {
+                    Text("External:")
+                    Text(bolus.external.formatted(.number.precision(.fractionLength(1))))
+                        .gridColumnAlignment(.trailing)
+                    Text("U")
+                }
+            }
+            .font(.headline.bold())
+        }
+        .foregroundStyle(.white)
+        .padding(20)
+        .background(
+            RoundedRectangle(cornerRadius: 10)
+                .fill(Color.blue.gradient)
+        )
+    }
+}

+ 62 - 0
Trio/Sources/Modules/Stat/View/ViewElements/GlucoseDistributionChart.swift

@@ -0,0 +1,62 @@
+import Charts
+import SwiftUI
+
+struct GlucoseDistributionChart: View {
+    let glucose: [GlucoseStored]
+    let highLimit: Decimal
+    let lowLimit: Decimal
+    let units: GlucoseUnits
+    let glucoseRangeStats: [GlucoseRangeStats]
+
+    var body: some View {
+        VStack(alignment: .leading, spacing: 8) {
+            Text("Glucose Distribution")
+                .font(.headline)
+
+            Chart(glucoseRangeStats) { range in
+                ForEach(range.values, id: \.hour) { value in
+                    AreaMark(
+                        x: .value("Hour", Calendar.current.dateForChartHour(value.hour)),
+                        y: .value("Count", value.count),
+                        stacking: .normalized
+                    )
+                    .foregroundStyle(by: .value("Range", range.name))
+                }
+            }
+            .chartForegroundStyleScale([
+                "<54": .purple.opacity(0.7),
+                "54-70": .red.opacity(0.7),
+                "70-140": .green,
+                "140-180": .green.opacity(0.7),
+                "180-200": .yellow.opacity(0.7),
+                "200-220": .orange.opacity(0.7),
+                ">220": .orange.opacity(0.8)
+            ])
+            .chartYAxis {
+                AxisMarks(position: .trailing) { value in
+                    if let percentage = value.as(Double.self) {
+                        AxisValueLabel {
+                            Text((percentage / 100).formatted(.percent.precision(.fractionLength(0))))
+                                .font(.footnote)
+                        }
+                        AxisGridLine()
+                    }
+                }
+            }
+            .chartYAxisLabel(alignment: .trailing) {
+                Text("Percentage")
+                    .foregroundStyle(.primary)
+                    .font(.footnote)
+                    .padding(.vertical, 3)
+            }
+            .chartXAxis {
+                AxisMarks(values: .stride(by: .hour, count: 3)) { _ in
+                    AxisValueLabel(format: .dateTime.hour(.defaultDigits(amPM: .narrow)), anchor: .top)
+                        .font(.footnote)
+                    AxisGridLine()
+                }
+            }
+            .frame(height: 200)
+        }
+    }
+}

+ 257 - 0
Trio/Sources/Modules/Stat/View/ViewElements/GlucosePercentileChart.swift

@@ -0,0 +1,257 @@
+import Charts
+import SwiftUI
+
+struct GlucosePercentileChart: View {
+    let glucose: [GlucoseStored]
+    let highLimit: Decimal
+    let lowLimit: Decimal
+    let units: GlucoseUnits
+    let hourlyStats: [HourlyStats]
+    let isToday: Bool
+
+    @State private var selection: Date? = nil
+
+    private var selectedStats: HourlyStats? {
+        guard let selection = selection else { return nil }
+
+        // Don't show stats for future times if viewing today
+        if isToday && selection > Date() {
+            return nil
+        }
+
+        let calendar = Calendar.current
+        let hour = calendar.component(.hour, from: selection)
+        return hourlyStats.first { Int($0.hour) == hour }
+    }
+
+    var body: some View {
+        VStack(alignment: .leading, spacing: 8) {
+            Text("Ambulatory Glucose Profile (AGP)")
+                .font(.headline)
+
+            Chart {
+                // TODO: ensure data is still correct
+                // TODO: ensure area marks and line mark take color of respective range
+
+                // Statistical view for longer periods
+                ForEach(hourlyStats, id: \.hour) { stats in
+                    // 10-90 percentile area
+                    AreaMark(
+                        x: .value("Hour", Calendar.current.dateForChartHour(stats.hour)),
+                        yStart: .value("10th Percentile", stats.percentile10),
+                        yEnd: .value("90th Percentile", stats.percentile90),
+                        series: .value("10-90", "10-90")
+                    )
+                    .foregroundStyle(.blue.opacity(stats.median > 0 ? 0.2 : 0))
+
+                    // 25-75 percentile area
+                    AreaMark(
+                        x: .value("Hour", Calendar.current.dateForChartHour(stats.hour)),
+                        yStart: .value("25th Percentile", stats.percentile25),
+                        yEnd: .value("75th Percentile", stats.percentile75),
+                        series: .value("25-75", "25-75")
+                    )
+                    .foregroundStyle(.blue.opacity(stats.median > 0 ? 0.3 : 0))
+
+                    // Median line
+                    if stats.median > 0 {
+                        LineMark(
+                            x: .value("Hour", Calendar.current.dateForChartHour(stats.hour)),
+                            y: .value("Median", stats.median),
+                            series: .value("Median", "Median")
+                        )
+                        .lineStyle(StrokeStyle(lineWidth: 2))
+                        .foregroundStyle(.blue)
+                    }
+                }
+
+                // High/Low limit lines
+                RuleMark(y: .value("High Limit", Double(highLimit)))
+                    .lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 5]))
+                    .foregroundStyle(.orange)
+
+                RuleMark(y: .value("Low Limit", Double(lowLimit)))
+                    .lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 5]))
+                    .foregroundStyle(.red)
+
+                if let selectedStats, let selection {
+                    RuleMark(x: .value("Selection", selection))
+                        .foregroundStyle(.secondary.opacity(0.5))
+                        .annotation(
+                            position: .top,
+                            spacing: 0,
+                            overflowResolution: .init(x: .fit, y: .disabled)
+                        ) {
+                            AGPSelectionPopover(
+                                stats: selectedStats,
+                                time: selection,
+                                units: units
+                            )
+                        }
+                }
+            }
+            .chartYAxis {
+                AxisMarks(position: .trailing) { value in
+                    if let glucose = value.as(Double.self) {
+                        AxisValueLabel {
+                            Text(
+                                units == .mmolL ? glucose.asMmolL.formatted(.number.precision(.fractionLength(0))) : glucose
+                                    .formatted(.number.precision(.fractionLength(0)))
+                            )
+                            .font(.footnote)
+                        }
+                        AxisGridLine()
+                    }
+                }
+            }
+            .chartYAxisLabel(alignment: .trailing) {
+                Text("\(units.rawValue)")
+                    .foregroundStyle(.primary)
+                    .font(.footnote)
+                    .padding(.vertical, 3)
+            }
+            .chartXAxis {
+                AxisMarks(values: .stride(by: .hour, count: 3)) { _ in
+                    AxisValueLabel(format: .dateTime.hour(.defaultDigits(amPM: .narrow)), anchor: .top)
+                        .font(.footnote)
+                    AxisGridLine()
+                }
+            }
+            .chartXSelection(value: $selection.animation(.easeInOut))
+            .frame(height: 200)
+
+            legend
+        }
+    }
+
+    private var legend: some View {
+        HStack(spacing: 20) {
+            VStack {
+                // 10-90 Percentile
+                HStack(spacing: 8) {
+                    Rectangle()
+                        .frame(width: 20, height: 8)
+                        .foregroundStyle(.blue.opacity(0.2))
+                    Text("10% - 90%")
+                        .font(.caption)
+                        .foregroundStyle(.secondary)
+                }
+
+                // 25-75 Percentile
+                HStack(spacing: 8) {
+                    Rectangle()
+                        .frame(width: 20, height: 8)
+                        .foregroundStyle(.blue.opacity(0.3))
+                    Text("25% - 75%")
+                        .font(.caption)
+                        .foregroundStyle(.secondary)
+                }
+            }
+
+            // Median
+            HStack(spacing: 8) {
+                Rectangle()
+                    .frame(width: 20, height: 2)
+                    .foregroundStyle(.blue)
+                Text("Median")
+                    .font(.caption)
+                    .foregroundStyle(.secondary)
+            }
+
+            VStack {
+                // High Limit
+                HStack(spacing: 8) {
+                    Rectangle()
+                        .frame(width: 20, height: 1)
+                        .foregroundStyle(.orange)
+                    Text("High Limit")
+                        .font(.caption)
+                        .foregroundStyle(.secondary)
+                }
+
+                // Low Limit
+                HStack(spacing: 8) {
+                    Rectangle()
+                        .frame(width: 20, height: 1)
+                        .foregroundStyle(.red)
+                    Text("Low Limit")
+                        .font(.caption)
+                        .foregroundStyle(.secondary)
+                }
+            }
+        }
+        .padding(.horizontal)
+    }
+}
+
+struct AGPSelectionPopover: View {
+    let stats: HourlyStats
+    let time: Date
+    let units: GlucoseUnits
+
+    private var timeText: String {
+        if let hour = Calendar.current.dateComponents([.hour], from: time).hour {
+            return "\(hour):00-\(hour + 1):00"
+        } else {
+            return time.formatted(.dateTime.hour().minute())
+        }
+    }
+
+    var body: some View {
+        VStack(alignment: .leading, spacing: 4) {
+            HStack {
+                Image(systemName: "clock")
+                Text(timeText)
+                    .fontWeight(.bold)
+            }
+            .font(.subheadline)
+
+            Grid(alignment: .leading, horizontalSpacing: 8) {
+                GridRow {
+                    Text("90%:")
+                    Text(units == .mmolL ? stats.percentile90.asMmolL.formatted(.number) : stats.percentile90.formatted(.number))
+                    Text(units.rawValue)
+                        .foregroundStyle(.secondary)
+                }
+                GridRow {
+                    Text("75%:")
+                    Text(units == .mmolL ? stats.percentile75.asMmolL.formatted(.number) : stats.percentile75.formatted(.number))
+                    Text(units.rawValue)
+                        .foregroundStyle(.secondary)
+                }
+                GridRow {
+                    Text("Median:")
+                    Text(units == .mmolL ? stats.median.asMmolL.formatted(.number) : stats.median.formatted(.number))
+                    Text(units.rawValue)
+                        .foregroundStyle(.secondary)
+                }
+                GridRow {
+                    Text("25%:")
+                    Text(units == .mmolL ? stats.percentile25.asMmolL.formatted(.number) : stats.percentile25.formatted(.number))
+                    Text(units.rawValue)
+                        .foregroundStyle(.secondary)
+                }
+                GridRow {
+                    Text("10%:")
+                    Text(units == .mmolL ? stats.percentile10.asMmolL.formatted(.number) : stats.percentile10.formatted(.number))
+                    Text(units.rawValue)
+                        .foregroundStyle(.secondary)
+                }
+            }
+            .font(.headline.bold())
+        }
+        .foregroundStyle(.white)
+        .padding(20)
+        .background {
+            RoundedRectangle(cornerRadius: 10)
+                .fill(Color.blue.gradient)
+        }
+    }
+}
+
+private extension Calendar {
+    func startOfHour(for date: Date) -> Date {
+        let components = dateComponents([.year, .month, .day, .hour], from: date)
+        return self.date(from: components) ?? date
+    }
+}

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

@@ -0,0 +1,79 @@
+import Charts
+import SwiftUI
+
+struct LoopStatsView: 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.gradient : Color.green.gradient)
+                .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 == "Loop Success Rate" {
+            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"
+            }
+        }
+    }
+}

+ 486 - 0
Trio/Sources/Modules/Stat/View/ViewElements/MealStatsView.swift

@@ -0,0 +1,486 @@
+import Charts
+import SwiftUI
+
+struct MealStatsView: View {
+    @Binding var selectedDuration: Stat.StateModel.StatsTimeInterval
+    let mealStats: [MealStats]
+    let state: Stat.StateModel
+
+    @State private var scrollPosition = Date() // gets updated in onAppear block
+    @State private var selectedDate: Date?
+    @State private var currentAverages: (carbs: Double, fat: Double, protein: Double) = (0, 0, 0)
+    @State private var updateTimer = Stat.UpdateTimer()
+
+    /// Returns the time interval length for the visible domain based on selected duration
+    /// - Returns: TimeInterval representing the visible time range in seconds
+    ///
+    /// Time intervals:
+    /// - Day: 24 hours (86400 seconds)
+    /// - Week: 7 days (604800 seconds)
+    /// - Month: 30 days (2592000 seconds)
+    /// - Total: 90 days (7776000 seconds)
+    private var visibleDomainLength: TimeInterval {
+        switch selectedDuration {
+        case .Day: return 24 * 3600 // One day in seconds
+        case .Week: return 7 * 24 * 3600 // One week in seconds
+        case .Month: return 30 * 24 * 3600 // One month in seconds (approximated)
+        case .Total: return 90 * 24 * 3600 // Three months in seconds
+        }
+    }
+
+    /// Calculates the visible date range based on scroll position and domain length
+    /// - Returns: Tuple containing start and end dates of the visible range
+    ///
+    /// The start date is determined by the current scroll position, while the end date
+    /// is calculated by adding the visible domain length to the start date
+    private var visibleDateRange: (start: Date, end: Date) {
+        let start = scrollPosition // Current scroll position marks the start
+        let end = start.addingTimeInterval(visibleDomainLength)
+        return (start, end)
+    }
+
+    /// Returns the appropriate date format style based on the selected time interval
+    /// - Returns: A Date.FormatStyle configured for the current time interval
+    ///
+    /// Format styles:
+    /// - Day: Shows hour only (e.g. "13")
+    /// - Week: Shows abbreviated weekday (e.g. "Mon")
+    /// - Month: Shows day of month (e.g. "15")
+    /// - Total: Shows abbreviated month (e.g. "Jan")
+    private var dateFormat: Date.FormatStyle {
+        switch selectedDuration {
+        case .Day:
+            return .dateTime.hour()
+        case .Week:
+            return .dateTime.weekday(.abbreviated)
+        case .Month:
+            return .dateTime.day()
+        case .Total:
+            return .dateTime.month(.abbreviated)
+        }
+    }
+
+    /// Returns DateComponents for aligning dates based on the selected duration
+    /// - Returns: DateComponents configured for the appropriate alignment
+    ///
+    /// This property provides date components for aligning dates in the chart:
+    /// - For Day view: Aligns to start of day (midnight)
+    /// - For Week view: Aligns to Monday (weekday 2)
+    /// - For Month/Total view: Aligns to first day of month
+    private var alignmentComponents: DateComponents {
+        switch selectedDuration {
+        case .Day:
+            return DateComponents(hour: 0) // Align to midnight
+        case .Week:
+            return DateComponents(weekday: 2) // Monday is weekday 2 in Calendar
+        case .Month,
+             .Total:
+            return DateComponents(day: 1) // First day of month
+        }
+    }
+
+    /// Returns meal statistics for a specific date
+    /// - Parameter date: The date to find meal statistics for
+    /// - Returns: MealStats object if found for the given date, nil otherwise
+    ///
+    /// This function searches through the meal statistics array to find the first entry
+    /// that matches the provided date (comparing only the day component, not time).
+    private func getMealForDate(_ date: Date) -> MealStats? {
+        let calendar = Calendar.current
+
+        return mealStats.first { stat in
+            switch selectedDuration {
+            case .Day:
+                return calendar.isDate(stat.date, equalTo: date, toGranularity: .hour)
+            default:
+                return calendar.isDate(stat.date, inSameDayAs: date)
+            }
+        }
+    }
+
+    /// Updates the current averages for macronutrients based on the visible date range
+    ///
+    /// This function:
+    /// - Gets the cached meal averages for the currently visible date range from the state
+    /// - Updates the currentAverages property with the retrieved values (carbs, fat, protein)
+    private func updateAverages() {
+        // Get cached averages for visible time window
+        currentAverages = state.getCachedMealAverages(for: visibleDateRange)
+    }
+
+    /// Formats the visible date range into a human-readable string
+    /// - Returns: A formatted string representing the visible date range
+    ///
+    /// For Day view:
+    /// - Uses relative terms like "Today", "Yesterday", "Tomorrow" when applicable
+    /// - Shows time range in hours and minutes
+    /// - Combines dates if start and end are on the same day
+    ///
+    /// For other views:
+    /// - Uses standard date formatting
+    private func formatVisibleDateRange() -> String {
+        let start = visibleDateRange.start
+        let end = visibleDateRange.end
+        let calendar = Calendar.current
+        let today = Date()
+
+        // Special handling for Day view with relative dates
+        if selectedDuration == .Day {
+            let startDateText: String
+            let endDateText: String
+            let timeFormat = start.formatted(.dateTime.hour().minute())
+
+            // Format start date
+            if calendar.isDate(start, inSameDayAs: today) {
+                startDateText = "Today"
+            } else if calendar.isDate(start, inSameDayAs: calendar.date(byAdding: .day, value: -1, to: today)!) {
+                startDateText = "Yesterday"
+            } else if calendar.isDate(start, inSameDayAs: calendar.date(byAdding: .day, value: 1, to: today)!) {
+                startDateText = "Tomorrow"
+            } else {
+                startDateText = start.formatted(.dateTime.day().month())
+            }
+
+            // Format end date
+            if calendar.isDate(end, inSameDayAs: today) {
+                endDateText = "Today"
+            } else if calendar.isDate(end, inSameDayAs: calendar.date(byAdding: .day, value: -1, to: today)!) {
+                endDateText = "Yesterday"
+            } else if calendar.isDate(end, inSameDayAs: calendar.date(byAdding: .day, value: 1, to: today)!) {
+                endDateText = "Tomorrow"
+            } else {
+                endDateText = end.formatted(.dateTime.day().month())
+            }
+
+            // If start and end are on the same day, show date only once
+            if calendar.isDate(start, inSameDayAs: end) {
+                return "\(startDateText), \(timeFormat) - \(end.formatted(.dateTime.hour().minute()))"
+            }
+
+            return "\(startDateText), \(timeFormat) - \(endDateText), \(end.formatted(.dateTime.hour().minute()))"
+        }
+
+        // Standard format for other views - only show dates without time
+        let startText: String
+        let endText: String
+
+        // Check for relative dates
+        if calendar.isDate(start, inSameDayAs: today) {
+            startText = "Today"
+        } else if calendar.isDate(start, inSameDayAs: calendar.date(byAdding: .day, value: -1, to: today)!) {
+            startText = "Yesterday"
+        } else if calendar.isDate(start, inSameDayAs: calendar.date(byAdding: .day, value: 1, to: today)!) {
+            startText = "Tomorrow"
+        } else {
+            startText = start.formatted(.dateTime.day().month())
+        }
+
+        if calendar.isDate(end, inSameDayAs: today) {
+            endText = "Today"
+        } else if calendar.isDate(end, inSameDayAs: calendar.date(byAdding: .day, value: -1, to: today)!) {
+            endText = "Yesterday"
+        } else if calendar.isDate(end, inSameDayAs: calendar.date(byAdding: .day, value: 1, to: today)!) {
+            endText = "Tomorrow"
+        } else {
+            endText = end.formatted(.dateTime.day().month())
+        }
+
+        return "\(startText) - \(endText)"
+    }
+
+    /// Returns the initial scroll position date based on the selected duration
+    /// - Returns: A Date representing where the chart should initially scroll to
+    ///
+    /// This function calculates an appropriate starting scroll position by subtracting
+    /// a time interval from the current date based on the selected duration:
+    /// - For Day view: 1 day before now
+    /// - For Week view: 7 days before now
+    /// - For Month view: 1 month before now
+    /// - For Total view: 3 months before now
+    private func getInitialScrollPosition() -> Date {
+        let calendar = Calendar.current
+        let now = Date()
+
+        // Calculate scroll position based on selected time interval
+        switch selectedDuration {
+        case .Day:
+            return calendar.date(byAdding: .day, value: -1, to: now)!
+        case .Week:
+            return calendar.date(byAdding: .day, value: -7, to: now)!
+        case .Month:
+            return calendar.date(byAdding: .month, value: -1, to: now)!
+        case .Total:
+            return calendar.date(byAdding: .month, value: -3, to: now)!
+        }
+    }
+
+    private func isSameTimeUnit(_ date1: Date, _ date2: Date) -> Bool {
+        switch selectedDuration {
+        case .Day:
+            return Calendar.current.isDate(date1, equalTo: date2, toGranularity: .hour)
+        default:
+            return Calendar.current.isDate(date1, inSameDayAs: date2)
+        }
+    }
+
+    var body: some View {
+        VStack(alignment: .leading, spacing: 8) {
+            statsView
+            chartsView
+        }
+
+        .onAppear {
+            scrollPosition = getInitialScrollPosition()
+            updateAverages()
+        }
+        .onChange(of: scrollPosition) {
+            updateTimer.scheduleUpdate {
+                updateAverages()
+            }
+        }
+        .onChange(of: selectedDuration) {
+            Task {
+                scrollPosition = getInitialScrollPosition()
+                updateAverages()
+            }
+        }
+    }
+
+    private var statsView: some View {
+        HStack {
+            Grid(alignment: .leading) {
+                GridRow {
+                    Text("Carbs:")
+                        .font(.headline)
+                        .foregroundStyle(.secondary)
+                    Text(currentAverages.carbs.formatted(.number.precision(.fractionLength(1))))
+                        .font(.headline)
+                        .foregroundStyle(.secondary)
+                        .gridColumnAlignment(.trailing)
+                    Text("g")
+                        .font(.headline)
+                        .foregroundStyle(.secondary)
+                }
+                GridRow {
+                    Text("Fat:")
+                        .font(.headline)
+                        .foregroundStyle(.secondary)
+                    Text(currentAverages.fat.formatted(.number.precision(.fractionLength(1))))
+                        .font(.headline)
+                        .foregroundStyle(.secondary)
+                        .gridColumnAlignment(.trailing)
+                    Text("g")
+                        .font(.headline)
+                        .foregroundStyle(.secondary)
+                }
+                GridRow {
+                    Text("Protein:")
+                        .font(.headline)
+                        .foregroundStyle(.secondary)
+                    Text(currentAverages.protein.formatted(.number.precision(.fractionLength(1))))
+                        .font(.headline)
+                        .foregroundStyle(.secondary)
+                        .gridColumnAlignment(.trailing)
+                    Text("g")
+                        .font(.headline)
+                        .foregroundStyle(.secondary)
+                }
+            }
+
+            Spacer()
+
+            Text(formatVisibleDateRange())
+                .font(.footnote)
+                .foregroundStyle(.secondary)
+        }
+    }
+
+    private var chartsView: some View {
+        Chart {
+            ForEach(mealStats) { stat in
+                // Carbs Bar (bottom)
+                BarMark(
+                    x: .value("Date", stat.date, unit: selectedDuration == .Day ? .hour : .day),
+                    y: .value("Amount", stat.carbs)
+                )
+                .foregroundStyle(by: .value("Type", "Carbs"))
+                .position(by: .value("Type", "Macros"))
+                .opacity(
+                    selectedDate.map { date in
+                        isSameTimeUnit(stat.date, date) ? 1 : 0.3
+                    } ?? 1
+                )
+                // Fat Bar (middle)
+                BarMark(
+                    x: .value("Date", stat.date, unit: selectedDuration == .Day ? .hour : .day),
+                    y: .value("Amount", stat.fat)
+                )
+                .foregroundStyle(by: .value("Type", "Fat"))
+                .position(by: .value("Type", "Macros"))
+                .opacity(
+                    selectedDate.map { date in
+                        isSameTimeUnit(stat.date, date) ? 1 : 0.3
+                    } ?? 1
+                )
+                // Protein Bar (top)
+                BarMark(
+                    x: .value("Date", stat.date, unit: selectedDuration == .Day ? .hour : .day),
+                    y: .value("Amount", stat.protein)
+                )
+                .foregroundStyle(by: .value("Type", "Protein"))
+                .position(by: .value("Type", "Macros"))
+                .opacity(
+                    selectedDate.map { date in
+                        isSameTimeUnit(stat.date, date) ? 1 : 0.3
+                    } ?? 1
+                )
+            }
+
+            // Selection popover outside of the ForEach loop!
+            if let selectedDate,
+               let selectedMeal = getMealForDate(selectedDate)
+            {
+                RuleMark(
+                    x: .value("Selected Date", selectedDate)
+                )
+                .foregroundStyle(.secondary.opacity(0.5))
+                .annotation(
+                    position: .top,
+                    spacing: 0,
+                    overflowResolution: .init(x: .fit(to: .chart), y: .fit(to: .chart))
+                ) {
+                    MealSelectionPopover(date: selectedDate, meal: selectedMeal, selectedDuration: selectedDuration)
+                }
+            }
+        }
+        .chartForegroundStyleScale([
+            "Carbs": Color.orange,
+            "Fat": Color.green,
+            "Protein": Color.blue
+        ])
+        .chartLegend(position: .bottom, alignment: .leading, spacing: 12)
+        .chartYAxis {
+            AxisMarks(position: .trailing) { value in
+                if let amount = value.as(Double.self) {
+                    AxisValueLabel {
+                        Text(amount.formatted(.number.precision(.fractionLength(0))) + " g")
+                            .font(.footnote)
+                    }
+                    AxisGridLine()
+                }
+            }
+        }
+        .chartXAxis {
+            AxisMarks(preset: .aligned, values: .stride(by: selectedDuration == .Day ? .hour : .day)) { value in
+                if let date = value.as(Date.self) {
+                    let day = Calendar.current.component(.day, from: date)
+                    let hour = Calendar.current.component(.hour, from: date)
+
+                    switch selectedDuration {
+                    case .Day:
+                        if hour % 6 == 0 {
+                            AxisValueLabel(format: dateFormat, centered: true)
+                                .font(.footnote)
+                            AxisGridLine()
+                        }
+                    case .Month:
+                        if day % 5 == 0 {
+                            AxisValueLabel(format: dateFormat, centered: true)
+                                .font(.footnote)
+                            AxisGridLine()
+                        }
+                    case .Total:
+                        if day == 1 && Calendar.current.component(.month, from: date) % 3 == 1 {
+                            AxisValueLabel(format: dateFormat, centered: true)
+                                .font(.footnote)
+                            AxisGridLine()
+                        }
+                    default:
+                        AxisValueLabel(format: dateFormat, centered: true)
+                            .font(.footnote)
+                        AxisGridLine()
+                    }
+                }
+            }
+        }
+        .chartScrollableAxes(.horizontal)
+        .chartXSelection(value: $selectedDate.animation(.easeInOut))
+        .chartScrollPosition(x: $scrollPosition)
+        .chartScrollTargetBehavior(
+            .valueAligned(
+                matching: selectedDuration == .Day ?
+                    DateComponents(minute: 0) :
+                    DateComponents(hour: 0),
+                majorAlignment: .matching(alignmentComponents)
+            )
+        )
+        .chartXVisibleDomain(length: visibleDomainLength)
+        .frame(height: 250)
+    }
+}
+
+/// A view that displays detailed meal information in a popover
+///
+/// This view shows a formatted display of meal macronutrients including:
+/// - Date of the meal
+/// - Carbohydrates in grams
+/// - Fat in grams
+/// - Protein in grams
+private struct MealSelectionPopover: View {
+    // The date when the meal was logged
+    let date: Date
+    // The meal statistics to display
+    let meal: MealStats
+    // The selected duration in the time picker
+    let selectedDuration: Stat.StateModel.StatsTimeInterval
+
+    private var timeText: String {
+        if selectedDuration == .Day {
+            let hour = Calendar.current.component(.hour, from: date)
+            return "\(hour):00-\(hour + 1):00"
+        } else {
+            return date.formatted(.dateTime.month().day())
+        }
+    }
+
+    var body: some View {
+        VStack(alignment: .leading, spacing: 4) {
+            // Display formatted date header
+            Text(timeText)
+                .font(.footnote)
+                .fontWeight(.bold)
+
+            // Grid layout for macronutrient values
+            Grid(alignment: .leading) {
+                // Carbohydrates row
+                GridRow {
+                    Text("Carbs:")
+                    Text(meal.carbs.formatted(.number.precision(.fractionLength(1))))
+                        .gridColumnAlignment(.trailing)
+                    Text("g")
+                }
+                // Fat row
+                GridRow {
+                    Text("Fat:")
+                    Text(meal.fat.formatted(.number.precision(.fractionLength(1))))
+                        .gridColumnAlignment(.trailing)
+                    Text("g")
+                }
+                // Protein row
+                GridRow {
+                    Text("Protein:")
+                    Text(meal.protein.formatted(.number.precision(.fractionLength(1))))
+                        .gridColumnAlignment(.trailing)
+                    Text("g")
+                }
+            }
+            .font(.headline.bold())
+        }
+        .foregroundStyle(.white)
+        .padding(20)
+        .background(
+            RoundedRectangle(cornerRadius: 10)
+                .fill(Color.orange.gradient)
+        )
+    }
+}

+ 247 - 0
Trio/Sources/Modules/Stat/View/ViewElements/SectorChart.swift

@@ -0,0 +1,247 @@
+import Charts
+import CoreData
+import SwiftDate
+import SwiftUI
+
+struct SectorChart: View {
+    let highLimit: Decimal
+    let lowLimit: Decimal
+    let units: GlucoseUnits
+    let glucose: [GlucoseStored]
+
+    @State private var selectedCount: Int?
+    @State private var selectedRange: GlucoseRange?
+
+    /// Represents the different ranges of glucose values that can be displayed in the sector chart
+    /// - high: Above target range
+    /// - inRange: Within target range
+    /// - low: Below target range
+    private enum GlucoseRange: String, Plottable {
+        case high = "High"
+        case inRange = "In Range"
+        case low = "Low"
+    }
+
+    var body: some View {
+        HStack(alignment: .center, spacing: 20) {
+            Chart {
+                ForEach(rangeData, id: \.range) { data in
+                    SectorMark(
+                        angle: .value("Percentage", data.count),
+                        innerRadius: .ratio(0.618),
+                        outerRadius: selectedRange == data.range ? 100 : 80,
+                        angularInset: 1.5
+                    )
+                    .cornerRadius(3)
+                    .foregroundStyle(data.color.gradient)
+                    .annotation(position: .overlay, alignment: .center, spacing: 0) {
+                        if data.percentage > 0 {
+                            Text("\(Int(data.percentage))%")
+                                .font(.callout)
+                                .foregroundStyle(.white)
+                                .fontWeight(.bold)
+                        }
+                    }
+                }
+            }
+            .chartLegend(position: .bottom, spacing: 20)
+            .chartAngleSelection(value: $selectedCount)
+            .chartForegroundStyleScale([
+                "High": Color.orange,
+                "In Range": Color.green,
+                "Low": Color.red
+            ])
+            .padding(.vertical)
+            .frame(height: 250)
+        }
+        .onChange(of: selectedCount) { _, newValue in
+            if let newValue {
+                withAnimation {
+                    getSelectedRange(value: newValue)
+                }
+            } else {
+                withAnimation {
+                    selectedRange = nil
+                }
+            }
+        }
+        .overlay(alignment: .top) {
+            if let selectedRange {
+                let data = getDetailedData(for: selectedRange)
+                RangeDetailPopover(data: data)
+                    .transition(.scale.combined(with: .opacity))
+                    .offset(y: -90) // TODO: make this dynamic
+            }
+        }
+    }
+
+    /// Calculates statistics about glucose ranges and returns data for the sector chart
+    ///
+    /// This computed property processes glucose readings and categorizes them into high, in-range, and low ranges.
+    /// For each range, it calculates:
+    /// - The count of readings in that range
+    /// - The percentage of total readings
+    /// - The associated color for visualization
+    ///
+    /// - Returns: An array of tuples containing range data, where each tuple has:
+    ///   - range: The glucose range category (high, in-range, or low)
+    ///   - count: Number of readings in that range
+    ///   - percentage: Percentage of total readings in that range
+    ///   - color: Color used to represent that range in the chart
+    private var rangeData: [(range: GlucoseRange, count: Int, percentage: Decimal, color: Color)] {
+        let total = glucose.count
+        // Return empty array if no glucose readings available
+        guard total > 0 else { return [] }
+
+        // Count readings above high limit
+        let highCount = glucose.filter { $0.glucose > Int(highLimit) }.count
+        // Count readings below low limit
+        let lowCount = glucose.filter { $0.glucose < Int(lowLimit) }.count
+        // Calculate in-range readings by subtracting high and low counts from total
+        let inRangeCount = total - highCount - lowCount
+
+        // Return array of tuples with range data
+        return [
+            (.high, highCount, Decimal(highCount) / Decimal(total) * 100, .orange),
+            (.inRange, inRangeCount, Decimal(inRangeCount) / Decimal(total) * 100, .green),
+            (.low, lowCount, Decimal(lowCount) / Decimal(total) * 100, .red)
+        ]
+    }
+
+    /// Determines which glucose range was selected based on a cumulative value
+    ///
+    /// This function takes a value representing a point in the cumulative total of glucose readings
+    /// and determines which range (high, in-range, or low) that point falls into.
+    /// It updates the selectedRange state variable when the appropriate range is found.
+    ///
+    /// - Parameter value: An integer representing a point in the cumulative total of readings
+    private func getSelectedRange(value: Int) {
+        // Keep track of running total as we check each range
+        var cumulativeTotal = 0
+
+        // Find first range where value falls within its cumulative count
+        _ = rangeData.first { data in
+            cumulativeTotal += data.count
+            if value <= cumulativeTotal {
+                selectedRange = data.range
+                return true
+            }
+            return false
+        }
+    }
+
+    /// Gets detailed statistics for a specific glucose range category
+    ///
+    /// This function calculates detailed statistics for a given glucose range (high, in-range, or low),
+    /// breaking down the readings into subcategories and calculating percentages.
+    ///
+    /// - Parameter range: The glucose range category to analyze
+    /// - Returns: A RangeDetail object containing the title, color and detailed statistics
+    private func getDetailedData(for range: GlucoseRange) -> RangeDetail {
+        // Calculate total number of glucose readings
+        let total = Decimal(glucose.count)
+
+        switch range {
+        case .high:
+            // Count readings above 250 mg/dL (very high)
+            let veryHigh = glucose.filter { $0.glucose > 250 }.count
+            // Count readings between high limit and 250 mg/dL (high)
+            let high = glucose.filter { $0.glucose > Int(highLimit) && $0.glucose <= 250 }.count
+
+            // Format glucose values
+            let highLimitTreshold = units == .mmolL ? Decimal(Int(highLimit)).asMmolL : highLimit
+            let veryHighThreshold = units == .mmolL ? Decimal(250).asMmolL : 250
+
+            return RangeDetail(
+                title: "High Glucose",
+                color: .orange,
+                items: [
+                    ("Very High (>\(veryHighThreshold) \(units.rawValue))", Decimal(veryHigh) / total * 100),
+                    ("High (\(highLimitTreshold)-\(veryHighThreshold) \(units.rawValue))", Decimal(high) / total * 100)
+                ]
+            )
+        case .inRange:
+            // Count readings between low limit and 140 mg/dL (tight control)
+            let tight = glucose.filter { $0.glucose >= Int(lowLimit) && $0.glucose <= 140 }.count
+            // Count readings between 140 and high limit (normal range)
+            let normal = glucose.filter { $0.glucose > 140 && $0.glucose <= Int(highLimit) }.count
+
+            // Format glucose values
+            let lowLimitTreshold = units == .mmolL ? Decimal(Int(lowLimit)).asMmolL : lowLimit
+            let highLimitTreshold = units == .mmolL ? Decimal(Int(highLimit)).asMmolL : highLimit
+            let tightThresholdTreshold = units == .mmolL ? Decimal(140).asMmolL : 140
+
+            return RangeDetail(
+                title: "In Range",
+                color: .green,
+                items: [
+                    ("Tight (\(lowLimitTreshold)-\(tightThresholdTreshold) \(units.rawValue))", Decimal(tight) / total * 100),
+                    ("Normal (\(tightThresholdTreshold)-\(highLimitTreshold) \(units.rawValue))", Decimal(normal) / total * 100)
+                ]
+            )
+        case .low:
+            // Count readings below 54 mg/dL (very low/urgent low)
+            let veryLow = glucose.filter { $0.glucose <= 54 }.count
+            // Count readings between 54 and low limit (low)
+            let low = glucose.filter { $0.glucose > 54 && $0.glucose < Int(lowLimit) }.count
+
+            // Format glucose values
+            let lowLimitTreshold = units == .mmolL ? Decimal(Int(lowLimit)).asMmolL : lowLimit
+            let veryLowThresholdTreshold = units == .mmolL ? Decimal(54).asMmolL : 54
+
+            return RangeDetail(
+                title: "Low Glucose",
+                color: .red,
+                items: [
+                    ("Very Low (<\(veryLowThresholdTreshold) \(units.rawValue))", Decimal(veryLow) / total * 100),
+                    ("Low (\(veryLowThresholdTreshold)-\(lowLimitTreshold) \(units.rawValue))", Decimal(low) / total * 100)
+                ]
+            )
+        }
+    }
+}
+
+/// Represents details about a specific glucose range category including title, color and percentage breakdowns
+private struct RangeDetail {
+    /// The title of this range category (e.g. "High Glucose", "In Range", "Low Glucose")
+    let title: String
+    /// The color used to represent this range in the UI
+    let color: Color
+    /// Array of tuples containing label and percentage for each sub-range
+    let items: [(label: String, percentage: Decimal)]
+}
+
+/// A popover view that displays detailed breakdown of glucose percentages for a range category
+private struct RangeDetailPopover: View {
+    let data: RangeDetail
+
+    var body: some View {
+        VStack(alignment: .leading, spacing: 4) {
+            Text(data.title)
+                .font(.subheadline)
+                .fontWeight(.bold)
+
+            ForEach(data.items, id: \.label) { item in
+                HStack {
+                    Text(item.label)
+                    Spacer()
+                    Text(formatPercentage(item.percentage))
+                }
+                .font(.footnote)
+            }
+        }
+        .foregroundStyle(.white)
+        .padding(20)
+        .background {
+            RoundedRectangle(cornerRadius: 10)
+                .fill(data.color.gradient)
+        }
+    }
+
+    private func formatPercentage(_ value: Decimal) -> String {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .percent
+        formatter.maximumFractionDigits = 1
+        return formatter.string(from: NSDecimalNumber(decimal: value / 100)) ?? "0%"
+    }
+}

+ 331 - 0
Trio/Sources/Modules/Stat/View/ViewElements/TDDChart.swift

@@ -0,0 +1,331 @@
+import Charts
+import SwiftUI
+
+struct TDDChartView: View {
+    @Binding var selectedDuration: Stat.StateModel.StatsTimeInterval
+    let tddStats: [TDDStats]
+    let state: Stat.StateModel
+
+    @State private var scrollPosition = Date()
+    @State private var selectedDate: Date?
+    @State private var currentAverage: Double = 0
+    @State private var updateTimer = Stat.UpdateTimer()
+
+    private var visibleDomainLength: TimeInterval {
+        switch selectedDuration {
+        case .Day: return 24 * 3600
+        case .Week: return 7 * 24 * 3600
+        case .Month: return 30 * 24 * 3600
+        case .Total: return 90 * 24 * 3600
+        }
+    }
+
+    private var visibleDateRange: (start: Date, end: Date) {
+        let start = scrollPosition
+        let end = start.addingTimeInterval(visibleDomainLength)
+        return (start, end)
+    }
+
+    private var dateFormat: Date.FormatStyle {
+        switch selectedDuration {
+        case .Day:
+            return .dateTime.hour()
+        case .Week:
+            return .dateTime.weekday(.abbreviated)
+        case .Month:
+            return .dateTime.day()
+        case .Total:
+            return .dateTime.month(.abbreviated)
+        }
+    }
+
+    private var alignmentComponents: DateComponents {
+        switch selectedDuration {
+        case .Day:
+            return DateComponents(hour: 0)
+        case .Week:
+            return DateComponents(weekday: 2)
+        case .Month,
+             .Total:
+            return DateComponents(day: 1)
+        }
+    }
+
+    private func getTDDForDate(_ date: Date) -> TDDStats? {
+        let calendar = Calendar.current
+
+        return tddStats.first { stat in
+            switch selectedDuration {
+            case .Day:
+                return calendar.isDate(stat.date, equalTo: date, toGranularity: .hour)
+            default:
+                return calendar.isDate(stat.date, inSameDayAs: date)
+            }
+        }
+    }
+
+    private func updateAverages() {
+        currentAverage = state.getCachedTDDAverages(for: visibleDateRange)
+    }
+
+    /// Formats the visible date range into a human-readable string
+    private func formatVisibleDateRange() -> String {
+        let start = visibleDateRange.start
+        let end = visibleDateRange.end
+        let calendar = Calendar.current
+        let today = Date()
+
+        // Special handling for Day view with relative dates
+        if selectedDuration == .Day {
+            let startDateText: String
+            let endDateText: String
+            let timeFormat = start.formatted(.dateTime.hour().minute())
+
+            // Format start date
+            if calendar.isDate(start, inSameDayAs: today) {
+                startDateText = "Today"
+            } else if calendar.isDate(start, inSameDayAs: calendar.date(byAdding: .day, value: -1, to: today)!) {
+                startDateText = "Yesterday"
+            } else if calendar.isDate(start, inSameDayAs: calendar.date(byAdding: .day, value: 1, to: today)!) {
+                startDateText = "Tomorrow"
+            } else {
+                startDateText = start.formatted(.dateTime.day().month())
+            }
+
+            // Format end date
+            if calendar.isDate(end, inSameDayAs: today) {
+                endDateText = "Today"
+            } else if calendar.isDate(end, inSameDayAs: calendar.date(byAdding: .day, value: -1, to: today)!) {
+                endDateText = "Yesterday"
+            } else if calendar.isDate(end, inSameDayAs: calendar.date(byAdding: .day, value: 1, to: today)!) {
+                endDateText = "Tomorrow"
+            } else {
+                endDateText = end.formatted(.dateTime.day().month())
+            }
+
+            // If start and end are on the same day, show date only once
+            if calendar.isDate(start, inSameDayAs: end) {
+                return "\(startDateText), \(timeFormat) - \(end.formatted(.dateTime.hour().minute()))"
+            }
+
+            return "\(startDateText), \(timeFormat) - \(endDateText), \(end.formatted(.dateTime.hour().minute()))"
+        }
+
+        // Standard format for other views - only show dates without time
+        let startText: String
+        let endText: String
+
+        // Check for relative dates
+        if calendar.isDate(start, inSameDayAs: today) {
+            startText = "Today"
+        } else if calendar.isDate(start, inSameDayAs: calendar.date(byAdding: .day, value: -1, to: today)!) {
+            startText = "Yesterday"
+        } else if calendar.isDate(start, inSameDayAs: calendar.date(byAdding: .day, value: 1, to: today)!) {
+            startText = "Tomorrow"
+        } else {
+            startText = start.formatted(.dateTime.day().month())
+        }
+
+        if calendar.isDate(end, inSameDayAs: today) {
+            endText = "Today"
+        } else if calendar.isDate(end, inSameDayAs: calendar.date(byAdding: .day, value: -1, to: today)!) {
+            endText = "Yesterday"
+        } else if calendar.isDate(end, inSameDayAs: calendar.date(byAdding: .day, value: 1, to: today)!) {
+            endText = "Tomorrow"
+        } else {
+            endText = end.formatted(.dateTime.day().month())
+        }
+
+        return "\(startText) - \(endText)"
+    }
+
+    private func getInitialScrollPosition() -> Date {
+        let calendar = Calendar.current
+        let now = Date()
+
+        switch selectedDuration {
+        case .Day:
+            return calendar.date(byAdding: .day, value: -1, to: now)!
+        case .Week:
+            return calendar.date(byAdding: .day, value: -7, to: now)!
+        case .Month:
+            return calendar.date(byAdding: .month, value: -1, to: now)!
+        case .Total:
+            return calendar.date(byAdding: .month, value: -3, to: now)!
+        }
+    }
+
+    private func isSameTimeUnit(_ date1: Date, _ date2: Date) -> Bool {
+        switch selectedDuration {
+        case .Day:
+            return Calendar.current.isDate(date1, equalTo: date2, toGranularity: .hour)
+        default:
+            return Calendar.current.isDate(date1, inSameDayAs: date2)
+        }
+    }
+
+    var body: some View {
+        VStack(alignment: .leading, spacing: 8) {
+            statsView
+            chartsView
+        }
+        .onAppear {
+            scrollPosition = getInitialScrollPosition()
+            updateAverages()
+        }
+        .onChange(of: scrollPosition) {
+            updateTimer.scheduleUpdate {
+                updateAverages()
+            }
+        }
+        .onChange(of: selectedDuration) {
+            Task {
+                scrollPosition = getInitialScrollPosition()
+                updateAverages()
+            }
+        }
+    }
+
+    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(formatVisibleDateRange())
+                .font(.footnote)
+                .foregroundStyle(.secondary)
+        }
+    }
+
+    private var chartsView: some View {
+        Chart {
+            ForEach(tddStats) { stat in
+                BarMark(
+                    x: .value("Date", stat.date, unit: selectedDuration == .Day ? .hour : .day),
+                    y: .value("Amount", stat.amount)
+                )
+                .foregroundStyle(Color.insulin)
+                .opacity(
+                    selectedDate.map { date in
+                        isSameTimeUnit(stat.date, date) ? 1 : 0.3
+                    } ?? 1
+                )
+            }
+
+            // Selection popover outside of the ForEach loop!
+            if let selectedDate,
+               let selectedTDD = getTDDForDate(selectedDate)
+            {
+                RuleMark(
+                    x: .value("Selected Date", selectedDate)
+                )
+                .foregroundStyle(.secondary.opacity(0.5))
+                .annotation(
+                    position: .top,
+                    spacing: 0,
+                    overflowResolution: .init(x: .fit(to: .chart), y: .fit(to: .chart))
+                ) {
+                    TDDSelectionPopover(date: selectedDate, tdd: selectedTDD, selectedDuration: selectedDuration)
+                }
+            }
+        }
+        .chartYAxis {
+            AxisMarks(position: .trailing) { value in
+                if let amount = value.as(Double.self) {
+                    AxisValueLabel {
+                        Text(amount.formatted(.number.precision(.fractionLength(0))) + " U")
+                            .font(.footnote)
+                    }
+                    AxisGridLine()
+                }
+            }
+        }
+        .chartXAxis {
+            AxisMarks(preset: .aligned, values: .stride(by: selectedDuration == .Day ? .hour : .day)) { value in
+                if let date = value.as(Date.self) {
+                    let day = Calendar.current.component(.day, from: date)
+                    let hour = Calendar.current.component(.hour, from: date)
+
+                    switch selectedDuration {
+                    case .Day:
+                        if hour % 6 == 0 {
+                            AxisValueLabel(format: dateFormat, centered: true)
+                                .font(.footnote)
+                            AxisGridLine()
+                        }
+                    case .Month:
+                        if day % 5 == 0 {
+                            AxisValueLabel(format: dateFormat, centered: true)
+                                .font(.footnote)
+                            AxisGridLine()
+                        }
+                    case .Total:
+                        if day == 1 && Calendar.current.component(.month, from: date) % 3 == 1 {
+                            AxisValueLabel(format: dateFormat, centered: true)
+                                .font(.footnote)
+                            AxisGridLine()
+                        }
+                    default:
+                        AxisValueLabel(format: dateFormat, centered: true)
+                            .font(.footnote)
+                        AxisGridLine()
+                    }
+                }
+            }
+        }
+        .chartScrollableAxes(.horizontal)
+        .chartXSelection(value: $selectedDate.animation(.easeInOut))
+        .chartScrollPosition(x: $scrollPosition)
+        .chartScrollTargetBehavior(
+            .valueAligned(
+                matching: selectedDuration == .Day ?
+                    DateComponents(minute: 0) :
+                    DateComponents(hour: 0),
+                majorAlignment: .matching(alignmentComponents)
+            )
+        )
+        .chartXVisibleDomain(length: visibleDomainLength)
+        .frame(height: 250)
+    }
+}
+
+private struct TDDSelectionPopover: View {
+    let date: Date
+    let tdd: TDDStats
+    let selectedDuration: Stat.StateModel.StatsTimeInterval
+
+    private var timeText: String {
+        if selectedDuration == .Day {
+            let hour = Calendar.current.component(.hour, from: date)
+            return "\(hour):00-\(hour + 1):00"
+        } else {
+            return date.formatted(.dateTime.month().day())
+        }
+    }
+
+    var body: some View {
+        VStack(alignment: .leading, spacing: 4) {
+            Text(timeText)
+                .font(.subheadline)
+                .fontWeight(.bold)
+
+            Text(tdd.amount.formatted(.number.precision(.fractionLength(1))) + " U")
+                .font(.title3.bold())
+        }
+        .foregroundStyle(.white)
+        .padding(20)
+        .background {
+            RoundedRectangle(cornerRadius: 10)
+                .fill(Color.insulin.gradient)
+        }
+    }
+}