Bläddra i källkod

Rework Stats, add Sector mark, AGP Chart and Stacked Area Chart for gluocse stats; add TDD Chart; refactor loop stats and add charts showing loop duration and success/failure rates; Heavy refactoring of existing fetching and view logic. Add meal stats + bolus stats, refactoring, fix missing x-axis marks for loopStatsChart, UI fixes.

Co-authored by @mountrcg
polscm32 aka Marvout 1 år sedan
förälder
incheckning
fb0c886eb8
25 ändrade filer med 2681 tillägg och 714 borttagningar
  1. 84 8
      FreeAPS.xcodeproj/project.pbxproj
  2. 20 0
      FreeAPS/Sources/Helpers/Calendar+GlucoseStatsChart.swift
  3. 56 0
      FreeAPS/Sources/Helpers/CustomDatePicker.swift
  4. 6 0
      FreeAPS/Sources/Helpers/Formatters.swift
  5. 6 0
      FreeAPS/Sources/Models/TDD.swift
  6. 1 1
      FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/GlucoseTargetSetup.swift
  7. 144 0
      FreeAPS/Sources/Modules/Stat/StatStateModel+Setup/AreaChartSetup.swift
  8. 103 0
      FreeAPS/Sources/Modules/Stat/StatStateModel+Setup/BolusStatsSetup.swift
  9. 210 0
      FreeAPS/Sources/Modules/Stat/StatStateModel+Setup/LoopChartSetup.swift
  10. 104 0
      FreeAPS/Sources/Modules/Stat/StatStateModel+Setup/MealStatsSetup.swift
  11. 132 0
      FreeAPS/Sources/Modules/Stat/StatStateModel+Setup/StackedChartSetup.swift
  12. 201 0
      FreeAPS/Sources/Modules/Stat/StatStateModel+Setup/TDDSetup.swift
  13. 100 9
      FreeAPS/Sources/Modules/Stat/StatStateModel.swift
  14. 0 296
      FreeAPS/Sources/Modules/Stat/View/ChartsView.swift
  15. 285 113
      FreeAPS/Sources/Modules/Stat/View/StatRootView.swift
  16. 0 287
      FreeAPS/Sources/Modules/Stat/View/StatsView.swift
  17. 307 0
      FreeAPS/Sources/Modules/Stat/View/ViewElements/BareStatisticsView.swift
  18. 92 0
      FreeAPS/Sources/Modules/Stat/View/ViewElements/BolusStatsView.swift
  19. 164 0
      FreeAPS/Sources/Modules/Stat/View/ViewElements/GlucoseAreaChart.swift
  20. 55 0
      FreeAPS/Sources/Modules/Stat/View/ViewElements/GlucoseStackedAreaChart.swift
  21. 192 0
      FreeAPS/Sources/Modules/Stat/View/ViewElements/LoopStatsView.swift
  22. 103 0
      FreeAPS/Sources/Modules/Stat/View/ViewElements/MealStatsView.swift
  23. 88 0
      FreeAPS/Sources/Modules/Stat/View/ViewElements/SectorChart.swift
  24. 224 0
      FreeAPS/Sources/Modules/Stat/View/ViewElements/TDDChart.swift
  25. 4 0
      Model/Helper/Determination+helper.swift

+ 84 - 8
FreeAPS.xcodeproj/project.pbxproj

@@ -41,9 +41,7 @@
 		19795118275953E50044850D /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 198377D4266BFFF6004DE65E /* Localizable.strings */; };
 		198377D2266BFFF6004DE65E /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 198377D4266BFFF6004DE65E /* Localizable.strings */; };
 		199561C1275E61A50077B976 /* HealthKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 199561C0275E61A50077B976 /* HealthKit.framework */; };
-		19A910302A24BF6300C8951B /* StatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A9102F2A24BF6300C8951B /* StatsView.swift */; };
 		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 */; };
@@ -316,6 +314,23 @@
 		BA00D96F7B2FF169A06FB530 /* CGMStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C018D1680307A31C9ED7120 /* CGMStateModel.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 /* GlucoseAreaChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD249D892D42FC0E00412DEB /* GlucoseAreaChart.swift */; };
+		BD249D8C2D42FC2C00412DEB /* GlucoseStackedAreaChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD249D8B2D42FC2500412DEB /* GlucoseStackedAreaChart.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 */; };
@@ -726,9 +741,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>"; };
@@ -1024,6 +1037,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 /* GlucoseAreaChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseAreaChart.swift; sourceTree = "<group>"; };
+		BD249D8B2D42FC2500412DEB /* GlucoseStackedAreaChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseStackedAreaChart.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>"; };
@@ -1523,6 +1553,7 @@
 		19F95FF129F10F9C00314DDC /* Stat */ = {
 			isa = PBXGroup;
 			children = (
+				BD249D952D42FCA800412DEB /* StatStateModel+Setup */,
 				19F95FF229F10FBC00314DDC /* StatDataFlow.swift */,
 				19F95FF429F10FCF00314DDC /* StatProvider.swift */,
 				19F95FF629F10FEE00314DDC /* StatStateModel.swift */,
@@ -1534,9 +1565,8 @@
 		19F95FF829F10FF600314DDC /* View */ = {
 			isa = PBXGroup;
 			children = (
+				BD249D842D42FBD200412DEB /* ViewElements */,
 				19F95FF929F1102A00314DDC /* StatRootView.swift */,
-				19A9102F2A24BF6300C8951B /* StatsView.swift */,
-				19A910372A24EF3200C8951B /* ChartsView.swift */,
 			);
 			path = View;
 			sourceTree = "<group>";
@@ -2006,6 +2036,7 @@
 		388E5A5925B6F0250019842D /* Models */ = {
 			isa = PBXGroup;
 			children = (
+				BD249DA22D42FD7A00412DEB /* TDD.swift */,
 				715120D12D3C2B84005D9FB6 /* GlucoseNotificationsOption.swift */,
 				DD07CA132CE80B73002D45A9 /* TimeInRangeChartStyle.swift */,
 				DD940BA92CA7585D000830A5 /* GlucoseColorScheme.swift */,
@@ -2061,6 +2092,8 @@
 		388E5A5A25B6F05F0019842D /* Helpers */ = {
 			isa = PBXGroup;
 			children = (
+				BD249DA62D42FE3800412DEB /* Calendar+GlucoseStatsChart.swift */,
+				BD249DA42D42FD9500412DEB /* CustomDatePicker.swift */,
 				C2A0A42E2CE0312C003B98E8 /* ConstantValues.swift */,
 				DD940BAB2CA75889000830A5 /* DynamicGlucoseColor.swift */,
 				38F37827261260DC009DB701 /* Color+Extensions.swift */,
@@ -2477,6 +2510,34 @@
 			path = View;
 			sourceTree = "<group>";
 		};
+		BD249D842D42FBD200412DEB /* ViewElements */ = {
+			isa = PBXGroup;
+			children = (
+				BD249D932D42FC5C00412DEB /* TDDChart.swift */,
+				BD249D912D42FC5000412DEB /* SectorChart.swift */,
+				BD249D8F2D42FC4300412DEB /* MealStatsView.swift */,
+				BD249D8D2D42FC3600412DEB /* LoopStatsView.swift */,
+				BD249D8B2D42FC2500412DEB /* GlucoseStackedAreaChart.swift */,
+				BD249D892D42FC0E00412DEB /* GlucoseAreaChart.swift */,
+				BD249D872D42FBFB00412DEB /* BolusStatsView.swift */,
+				BD249D852D42FBE600412DEB /* BareStatisticsView.swift */,
+			);
+			path = ViewElements;
+			sourceTree = "<group>";
+		};
+		BD249D952D42FCA800412DEB /* StatStateModel+Setup */ = {
+			isa = PBXGroup;
+			children = (
+				BD249DA02D42FD1000412DEB /* TDDSetup.swift */,
+				BD249D9E2D42FD0200412DEB /* StackedChartSetup.swift */,
+				BD249D9C2D42FCF300412DEB /* MealStatsSetup.swift */,
+				BD249D9A2D42FCD800412DEB /* LoopChartSetup.swift */,
+				BD249D982D42FCCA00412DEB /* BolusStatsSetup.swift */,
+				BD249D962D42FCBD00412DEB /* AreaChartSetup.swift */,
+			);
+			path = "StatStateModel+Setup";
+			sourceTree = "<group>";
+		};
 		BD793CAD2CE7660C00D669AC /* Overrides */ = {
 			isa = PBXGroup;
 			children = (
@@ -3450,6 +3511,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 */,
@@ -3513,14 +3575,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 */,
@@ -3533,6 +3597,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 */,
@@ -3565,6 +3630,7 @@
 				CE1F6DE92BAF37C90064EB8D /* TidepoolConfigView.swift in Sources */,
 				3811DE5D25C9D4D500A708ED /* Publisher.swift in Sources */,
 				E00EEC0727368630002FF094 /* APSAssembly.swift in Sources */,
+				BD249D8A2D42FC1200412DEB /* GlucoseAreaChart.swift in Sources */,
 				38B4F3AF25E2979F00E76A18 /* IndexedCollection.swift in Sources */,
 				58D08B222C8DAA8E00AA37D3 /* OverrideView.swift in Sources */,
 				BD0B2EF32C5998E600B3298F /* MealPresetView.swift in Sources */,
@@ -3601,16 +3667,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 */,
@@ -3618,6 +3685,7 @@
 				191F62682AD6B05A004D7911 /* NightscoutSettings.swift in Sources */,
 				3811DEAB25C9D88300A708ED /* HTTPResponseStatus.swift in Sources */,
 				3811DE5F25C9D4D500A708ED /* ProgressBar.swift in Sources */,
+				BD249D8C2D42FC2C00412DEB /* GlucoseStackedAreaChart.swift in Sources */,
 				38E87408274F9AD000975559 /* UserNotificationsManager.swift in Sources */,
 				CE82E02528E867BA00473A9C /* AlertStorage.swift in Sources */,
 				DD1745372C55B74200211FAC /* AlgorithmSettings.swift in Sources */,
@@ -3687,10 +3755,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 */,
@@ -3751,6 +3822,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 */,
@@ -3777,6 +3849,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 */,
 				38E8754F275556FA00975559 /* WatchManager.swift in Sources */,
@@ -3796,6 +3869,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 */,
 				38E4451E274DB04600EC9A94 /* AppDelegate.swift in Sources */,
@@ -3806,6 +3880,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 */,
@@ -3857,6 +3932,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 */,

+ 20 - 0
FreeAPS/Sources/Helpers/Calendar+GlucoseStatsChart.swift

@@ -0,0 +1,20 @@
+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
FreeAPS/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
FreeAPS/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

+ 6 - 0
FreeAPS/Sources/Models/TDD.swift

@@ -0,0 +1,6 @@
+import Foundation
+
+struct TDD: Codable, Equatable {
+    let totalDailyDose: Decimal?
+    let timestamp: Date?
+}

+ 1 - 1
FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/GlucoseTargetSetup.swift

@@ -77,7 +77,7 @@ extension Home.StateModel {
                 )
             )
         }
-      
+
         return targetProfiles
     }
 }

+ 144 - 0
FreeAPS/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
+        }
+    }
+}

+ 103 - 0
FreeAPS/Sources/Modules/Stat/StatStateModel+Setup/BolusStatsSetup.swift

@@ -0,0 +1,103 @@
+import CoreData
+import Foundation
+
+/// Represents statistical data about bolus insulin delivery for a specific day
+struct BolusStats: Identifiable {
+    let id = UUID()
+    /// The date representing this time period
+    let date: Date
+    /// Total amount of manual boluses (excluding SMB and external)
+    let manualBolus: Double
+    /// Total amount of Super Micro Boluses (SMB)
+    let smb: Double
+    /// Total amount of external boluses (e.g., from pump directly)
+    let external: Double
+}
+
+extension Stat.StateModel {
+    /// Updates the bolus statistics for the currently selected time period
+    func updateBolusStats() {
+        Task {
+            let stats = await fetchBolusStats(days: requestedDaysTDD, endDate: requestedEndDayTDD)
+            await MainActor.run {
+                self.bolusStats = stats
+            }
+        }
+    }
+
+    /// Fetches and processes bolus statistics for a specific date range
+    /// - Parameters:
+    ///   - days: Number of days to fetch
+    ///   - endDate: The end date of the range
+    /// - Returns: Array of BolusStats containing daily bolus statistics
+    func fetchBolusStats(days: Int, endDate: Date) async -> [BolusStats] {
+        let calendar = Calendar.current
+        let endDate = calendar.startOfDay(for: endDate)
+        let startDate = calendar.date(byAdding: .day, value: -(days - 1), to: endDate)!
+
+        // Fetch bolus records from Core Data
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: BolusStored.self,
+            onContext: bolusTaskContext,
+            predicate: NSPredicate(
+                format: "pumpEvent.timestamp >= %@ AND pumpEvent.timestamp < %@",
+                startDate as NSDate,
+                calendar.date(byAdding: .day, value: 1, to: endDate)! as NSDate
+            ),
+            key: "pumpEvent.timestamp",
+            ascending: false,
+            batchSize: 100
+        )
+
+        return await bolusTaskContext.perform {
+            guard let fetchedResults = results as? [BolusStored] else { return [] }
+
+            // Group entries by day
+            let groupedEntries = Dictionary(grouping: fetchedResults) { entry in
+                calendar.startOfDay(for: entry.pumpEvent?.timestamp ?? Date())
+            }
+
+            // Create array of all dates in the range
+            var dates: [Date] = []
+            var currentDate = startDate
+            while currentDate <= endDate {
+                dates.append(calendar.startOfDay(for: currentDate))
+                currentDate = calendar.date(byAdding: .day, value: 1, to: currentDate)!
+            }
+
+            // Calculate statistics for each day
+            return dates.map { date in
+                let dayEntries = groupedEntries[date, default: []]
+
+                // Calculate total manual boluses (excluding SMB and external)
+                let manualBolus = dayEntries
+                    .filter { !($0.isExternal || $0.isSMB) }
+                    .reduce(0.0) { $0 + (($1.amount as? Decimal) ?? 0).doubleValue }
+
+                // Calculate total SMB
+                let smb = dayEntries
+                    .filter { $0.isSMB }
+                    .reduce(0.0) { $0 + (($1.amount as? Decimal) ?? 0).doubleValue }
+
+                // Calculate total external boluses
+                let external = dayEntries
+                    .filter { $0.isExternal }
+                    .reduce(0.0) { $0 + (($1.amount as? Decimal) ?? 0).doubleValue }
+
+                return BolusStats(
+                    date: date,
+                    manualBolus: manualBolus,
+                    smb: smb,
+                    external: external
+                )
+            }.sorted { $0.date < $1.date }
+        }
+    }
+}
+
+/// Extension to convert Decimal to Double
+private extension Decimal {
+    var doubleValue: Double {
+        NSDecimalNumber(decimal: self).doubleValue
+    }
+}

+ 210 - 0
FreeAPS/Sources/Modules/Stat/StatStateModel+Setup/LoopChartSetup.swift

@@ -0,0 +1,210 @@
+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 = await self.fetchLoopStatRecords(for: selectedDurationForLoopStats)
+
+            // Used for the Loop stats chart (success/failure percentages)
+            let stats = await calculateLoopStats(from: recordIDs)
+
+            // Update property on main thread to avoid data races
+            await MainActor.run {
+                self.groupedLoopStats = stats
+            }
+
+            // Used for the Loop duration chart (execution times)
+            await self.updateLoopStatRecords(from: recordIDs)
+        }
+    }
+
+    /// Fetches loop stat record IDs from Core Data based on the selected time duration
+    /// - Parameter duration: The time period to fetch records for (Today, Day, Week, Month, or Total)
+    /// - Returns: Array of NSManagedObjectIDs for the matching loop stat records
+    func fetchLoopStatRecords(for duration: Duration) async -> [NSManagedObjectID] {
+        // Create compound predicate combining duration and non-nil constraints
+        let predicate: NSCompoundPredicate
+        let durationPredicate: NSPredicate
+        let nonNilDurationPredicate = NSPredicate(format: "duration != nil AND duration != 0")
+
+        // Set up date-based predicate based on selected duration
+        switch duration {
+        case .Day,
+             .Today:
+            durationPredicate = NSPredicate(
+                format: "end >= %@",
+                Calendar.current.date(
+                    byAdding: .day,
+                    value: -2,
+                    to: Calendar.current.startOfDay(for: Date())
+                )! as CVarArg
+            )
+        case .Week:
+            durationPredicate = NSPredicate(
+                format: "end >= %@",
+                Calendar.current.date(
+                    byAdding: .day,
+                    value: -7,
+                    to: Calendar.current.startOfDay(for: Date())
+                )! as CVarArg
+            )
+        case .Month:
+            durationPredicate = NSPredicate(
+                format: "end >= %@",
+                Calendar.current.date(
+                    byAdding: .month,
+                    value: -1,
+                    to: Calendar.current.startOfDay(for: Date())
+                )! as CVarArg
+            )
+        case .Total:
+            durationPredicate = NSPredicate(
+                format: "end >= %@",
+                Calendar.current.date(
+                    byAdding: .month,
+                    value: -3,
+                    to: Calendar.current.startOfDay(for: Date())
+                )! as CVarArg
+            )
+        }
+        predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [durationPredicate, nonNilDurationPredicate])
+
+        // Fetch records using the constructed predicate
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: LoopStatRecord.self,
+            onContext: loopTaskContext,
+            predicate: predicate,
+            key: "end",
+            ascending: false,
+            batchSize: 100
+        )
+
+        return await loopTaskContext.perform {
+            guard let fetchedResults = results as? [LoopStatRecord] else { return [] }
+            return fetchedResults.map(\.objectID)
+        }
+    }
+
+    /// Calculates statistics for loop executions grouped by time periods
+    /// - Parameter ids: Array of NSManagedObjectIDs for loop stat records
+    /// - Returns: Array of LoopStatsByPeriod containing success/failure statistics
+    func calculateLoopStats(from ids: [NSManagedObjectID]) async -> [LoopStatsByPeriod] {
+        await loopTaskContext.perform {
+            let calendar = Calendar.current
+            let now = Date()
+
+            // Convert IDs to LoopStatRecord objects
+            let records = ids.compactMap { id -> LoopStatRecord? in
+                do {
+                    return try self.loopTaskContext.existingObject(with: id) as? LoopStatRecord
+                } catch {
+                    debugPrint("\(DebuggingIdentifiers.failed) Error fetching loop stat: \(error)")
+                    return nil
+                }
+            }
+
+            // Determine start date based on selected duration
+            let startDate: Date
+            switch self.selectedDurationForLoopStats {
+            case .Day,
+                 .Today:
+                startDate = calendar.date(byAdding: .day, value: -2, to: calendar.startOfDay(for: now))!
+            case .Week:
+                startDate = calendar.date(byAdding: .day, value: -7, to: calendar.startOfDay(for: now))!
+            case .Month:
+                startDate = calendar.date(byAdding: .month, value: -1, to: calendar.startOfDay(for: now))!
+            case .Total:
+                startDate = calendar.date(byAdding: .month, value: -3, to: calendar.startOfDay(for: now))!
+            }
+
+            // Create array of all dates in the range
+            var dates: [Date] = []
+            var currentDate = startDate
+            while currentDate <= now {
+                dates.append(calendar.startOfDay(for: currentDate))
+                currentDate = calendar.date(byAdding: .day, value: 1, to: currentDate)!
+            }
+
+            // Group records by day
+            let recordsByDay = Dictionary(grouping: records) { record in
+                guard let date = record.start else { return Date() }
+                return calendar.startOfDay(for: date)
+            }
+
+            // Create stats for each day, including days with no data
+            return dates.map { date in
+                let dayRecords = recordsByDay[date, default: []]
+                let successful = dayRecords.filter { $0.loopStatus?.contains("Success") ?? false }.count
+                let failed = dayRecords.count - successful
+
+                // Calculate glucose count for the period
+                let glucoseFetchRequest = GlucoseStored.fetchRequest()
+                let periodStart = date
+                let periodEnd = calendar.date(byAdding: .day, value: 1, to: date)!
+
+                glucoseFetchRequest.predicate = NSPredicate(
+                    format: "date >= %@ AND date < %@",
+                    periodStart as NSDate,
+                    periodEnd as NSDate
+                )
+
+                var glucoseCount = 0
+                do {
+                    glucoseCount = try self.loopTaskContext.count(for: glucoseFetchRequest)
+                } catch {
+                    debugPrint("\(DebuggingIdentifiers.failed) Error counting glucose readings: \(error)")
+                }
+
+                return LoopStatsByPeriod(
+                    period: date,
+                    successful: successful,
+                    failed: failed,
+                    medianDuration: BareStatisticsView
+                        .medianCalculationDouble(array: dayRecords.compactMap { $0.duration as Double? }),
+                    glucoseCount: glucoseCount
+                )
+            }.sorted { $0.period < $1.period }
+        }
+    }
+
+    /// Updates the loopStatRecords array on the main thread with records from the provided IDs
+    /// - Parameter ids: Array of NSManagedObjectIDs for loop stat records
+    @MainActor func updateLoopStatRecords(from ids: [NSManagedObjectID]) {
+        loopStatRecords = ids.compactMap { id -> LoopStatRecord? in
+            do {
+                return try viewContext.existingObject(with: id) as? LoopStatRecord
+            } catch {
+                debugPrint("Error fetching loop stat: \(error)")
+                return nil
+            }
+        }
+    }
+}

+ 104 - 0
FreeAPS/Sources/Modules/Stat/StatStateModel+Setup/MealStatsSetup.swift

@@ -0,0 +1,104 @@
+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 {
+    /// Initiates the process of fetching and processing meal statistics
+    /// - Parameter duration: The time period to fetch records for
+    func setupMealStats(for duration: Duration) {
+        Task {
+            let stats = await fetchMealStats(for: duration)
+            await MainActor.run {
+                self.mealStats = stats
+            }
+        }
+    }
+
+    /// Fetches and processes meal statistics for a specific duration
+    /// - Parameter duration: The time period to fetch records for (Today, 24h, 7 Days, 30 Days, or All)
+    /// - Returns: Array of MealStats containing daily meal statistics, sorted by date
+    private func fetchMealStats(for duration: Duration) async -> [MealStats] {
+        let now = Date()
+        let calendar = Calendar.current
+
+        // Determine start date based on selected duration
+        // For Today and 24h, we show 3 days of data for better context
+        // For other durations, we fetch the respective time period
+        let startDate: Date
+        switch duration {
+        case .Today:
+            startDate = calendar.date(byAdding: .day, value: -2, to: calendar.startOfDay(for: now))!
+        case .Day:
+            startDate = calendar.date(byAdding: .day, value: -2, to: calendar.startOfDay(for: now))!
+        case .Week:
+            startDate = calendar.date(byAdding: .day, value: -7, to: calendar.startOfDay(for: now))!
+        case .Month:
+            startDate = calendar.date(byAdding: .month, value: -1, to: calendar.startOfDay(for: now))!
+        case .Total:
+            startDate = calendar.date(byAdding: .month, value: -3, to: calendar.startOfDay(for: now))!
+        }
+
+        // Fetch CarbEntryStored entries from Core Data
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: CarbEntryStored.self,
+            onContext: mealTaskContext,
+            predicate: NSPredicate(format: "date >= %@", startDate as NSDate),
+            key: "date",
+            ascending: false,
+            batchSize: 100
+        )
+
+        return await mealTaskContext.perform {
+            // Safely unwrap the fetched results, return empty array if nil
+            guard let fetchedResults = results as? [CarbEntryStored] else { return [] }
+
+            // Group entries by day using calendar's startOfDay
+            // This ensures all entries within the same day are grouped together
+            // regardless of their specific time
+            let groupedEntries = Dictionary(grouping: fetchedResults) { entry in
+                calendar.startOfDay(for: entry.date ?? Date())
+            }
+
+            // Create array of all dates in the range
+            // This ensures we have entries for every day in the range,
+            // even if there are no meal entries for some days
+            var dates: [Date] = []
+            var currentDate = startDate
+            while currentDate <= now {
+                dates.append(calendar.startOfDay(for: currentDate))
+                currentDate = calendar.date(byAdding: .day, value: 1, to: currentDate)!
+            }
+
+            // Calculate statistics for each day
+            // For days without entries, all values will be 0
+            return dates.map { date in
+                let entries = groupedEntries[date, default: []]
+
+                // Sum up macronutrients for the day
+                // Each reduce operation calculates the total for one macronutrient
+                let carbsTotal = entries.reduce(0.0) { $0 + $1.carbs } // Total carbs in grams
+                let fatTotal = entries.reduce(0.0) { $0 + $1.fat } // Total fat in grams
+                let proteinTotal = entries.reduce(0.0) { $0 + $1.protein } // Total protein in grams
+
+                return MealStats(
+                    date: date,
+                    carbs: carbsTotal,
+                    fat: fatTotal,
+                    protein: proteinTotal
+                )
+            }.sorted { $0.date < $1.date } // Sort results by date in ascending order
+        }
+    }
+}

+ 132 - 0
FreeAPS/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
+        }
+    }
+}

+ 201 - 0
FreeAPS/Sources/Modules/Stat/StatStateModel+Setup/TDDSetup.swift

@@ -0,0 +1,201 @@
+import CoreData
+import Foundation
+
+extension Decimal {
+    func rounded(scale: Int, roundingMode: NSDecimalNumber.RoundingMode) -> Decimal {
+        var result = Decimal()
+        var mutableSelf = self
+        NSDecimalRound(&result, &mutableSelf, scale, roundingMode)
+        return result
+    }
+}
+
+extension Stat.StateModel {
+    /// Represents different time ranges for Total Daily Dose calculations
+    enum TDDTimeRange {
+        /// Today
+        case today
+        /// Yesterday
+        case yesterday
+        /// Custom range with specified number of days and end date
+        case customRange(days: Int, endDate: Date)
+
+        /// Calculates the start and end dates for the time range
+        var dateRange: (start: Date, end: Date) {
+            let calendar = Calendar.current
+            let now = Date()
+
+            switch self {
+            case .today:
+                let startOfToday = calendar.startOfDay(for: now)
+                return (startOfToday, now)
+
+            case .yesterday:
+                let startOfToday = calendar.startOfDay(for: now)
+                let startOfYesterday = calendar.date(byAdding: .day, value: -1, to: startOfToday)!
+                let endOfYesterday = calendar.date(byAdding: .second, value: -1, to: startOfToday)!
+                return (startOfYesterday, endOfYesterday)
+
+            case let .customRange(days, endDate):
+                let endOfDay = calendar.date(
+                    bySettingHour: 23,
+                    minute: 59,
+                    second: 59,
+                    of: endDate
+                )!
+                let startDate = calendar.date(byAdding: .day, value: -(days - 1), to: endOfDay)!
+                return (startDate, endOfDay)
+            }
+        }
+    }
+
+    /// Configuration for TDD display and calculations
+    struct TDDConfiguration {
+        /// Number of days to display in the TDD chart (default: 7)
+        var requestedDays: Int = 7
+        /// End date for the TDD chart, defaults to end of current day
+        var endDate: Date = Calendar.current.date(
+            bySettingHour: 23,
+            minute: 59,
+            second: 59,
+            of: Date()
+        ) ?? Date()
+    }
+
+    /// Result structure containing TDD calculations for a specific time range
+    struct TDDResult: Sendable {
+        /// Array of daily doses for the period
+        let dailyDoses: [TDD]
+        /// Average TDD for non-zero values
+        let average: Decimal
+        /// Time range for which the result was calculated
+        let period: TDDTimeRange
+
+        /// Total insulin dose for the period
+        var totalDose: Decimal {
+            dailyDoses.reduce(Decimal.zero) { $0 + ($1.totalDailyDose ?? 0) }
+        }
+    }
+
+    /// Updates all TDD values concurrently: today, yesterday, and custom range
+    /// This method fetches and processes TDD data for all time ranges in parallel
+    /// and updates the UI state with the results.
+    func updateTDDValues() async {
+        // Fetch all required TDD ranges
+        async let today = fetchTDDForRange(.today)
+        async let yesterday = fetchTDDForRange(.yesterday)
+        async let customRange = fetchTDDForRange(.customRange(
+            days: tddConfig.requestedDays,
+            endDate: tddConfig.endDate
+        ))
+
+        // Await all results
+        let (todayResult, yesterdayResult, customRangeResult) = await (
+            today, yesterday, customRange
+        )
+
+        // Update UI state
+        await MainActor.run {
+            currentTDD = todayResult.totalDose
+            ytdTDDValue = yesterdayResult.totalDose
+            averageTDD = customRangeResult.average
+            dailyTotalDoses = customRangeResult.dailyDoses
+        }
+    }
+
+    /// Fetches and processes TDD data for a specific time range
+    /// - Parameter range: The time range for which to fetch TDD data
+    /// - Returns: A TDDResult containing processed TDD data for the specified range
+    private func fetchTDDForRange(_ range: TDDTimeRange) async -> TDDResult {
+        let dateRange = range.dateRange
+
+        let determinationIDs = await fetchDeterminations(
+            from: dateRange.start,
+            to: dateRange.end
+        )
+
+        let doses = await processDeterminations(determinationIDs, in: dateRange)
+        let average = calculateAverage(from: doses)
+
+        return TDDResult(
+            dailyDoses: doses,
+            average: average,
+            period: range
+        )
+    }
+
+    /// Fetches determination object IDs from Core Data for a given date range
+    /// - Parameters:
+    ///   - startDate: Start date of the range
+    ///   - endDate: End date of the range
+    /// - Returns: Array of NSManagedObjectIDs for matching determinations
+    private func fetchDeterminations(from startDate: Date, to endDate: Date) async -> [NSManagedObjectID] {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: OrefDetermination.self,
+            onContext: determinationFetchContext,
+            predicate: NSPredicate.determinationPeriod(from: startDate, to: endDate),
+            key: "deliverAt",
+            ascending: false,
+            propertiesToFetch: ["objectID", "timestamp", "deliverAt", "totalDailyDose"]
+        )
+
+        return await determinationFetchContext.perform {
+            guard let fetchedResults = results as? [[String: Any]] else { return [] }
+            return fetchedResults.compactMap { $0["objectID"] as? NSManagedObjectID }
+        }
+    }
+
+    /// Processes determination objects into TDD records
+    /// - Parameters:
+    ///   - determinationIDs: Array of determination object IDs to process
+    ///   - dateRange: Date range for context (unused but kept for future use)
+    /// - Returns: Array of processed TDD records, sorted by date descending
+    private func processDeterminations(
+        _ determinationIDs: [NSManagedObjectID],
+        in _: (start: Date, end: Date)
+    ) async -> [TDD] {
+        await determinationFetchContext.perform {
+            let calendar = Calendar.current
+
+            // Convert IDs to OrefDetermination objects
+            let determinations = determinationIDs.compactMap { id -> OrefDetermination? in
+                do {
+                    return try self.determinationFetchContext.existingObject(with: id) as? OrefDetermination
+                } catch {
+                    debugPrint("Error fetching determination: \(error)")
+                    return nil
+                }
+            }
+
+            // Group by day
+            let groupedByDay = Dictionary(grouping: determinations) { determination in
+                calendar.startOfDay(for: determination.timestamp ?? determination.deliverAt ?? Date())
+            }
+
+            // Get latest determination for each day
+            return groupedByDay.compactMap { _, dayDeterminations in
+                guard let latestDetermination = dayDeterminations.max(by: {
+                    ($0.timestamp ?? $0.deliverAt ?? Date()) < ($1.timestamp ?? $1.deliverAt ?? Date())
+                }),
+                    let dose = latestDetermination.totalDailyDose as? Decimal
+                else { return nil }
+
+                return TDD(
+                    totalDailyDose: dose,
+                    timestamp: latestDetermination.deliverAt
+                )
+            }.sorted { ($0.timestamp ?? Date()) > ($1.timestamp ?? Date()) }
+        }
+    }
+
+    /// Calculates the average TDD from an array of TDD records
+    /// - Parameter tdds: Array of TDD records to average
+    /// - Returns: Average TDD rounded to 1 decimal place, or 0 if no records
+    private func calculateAverage(from tdds: [TDD]) -> Decimal {
+        let totalSum = tdds.reduce(Decimal.zero) { $0 + ($1.totalDailyDose ?? 0) }
+        let count = Decimal(tdds.count)
+
+        guard count > 0 else { return 0 }
+        return (totalSum / count).rounded(scale: 1, roundingMode: .plain)
+    }
+}

+ 100 - 9
FreeAPS/Sources/Modules/Stat/StatStateModel.swift

@@ -13,24 +13,104 @@ extension Stat {
         var timeInRangeChartStyle: TimeInRangeChartStyle = .vertical
         var units: GlucoseUnits = .mgdL
         var glucoseFromPersistence: [GlucoseStored] = []
+        var loopStatRecords: [LoopStatRecord] = []
+        var groupedLoopStats: [LoopStatsByPeriod] = []
+        var mealStats: [MealStats] = []
 
-        var selectedDuration: Duration = .Today
+        var selectedDuration: Duration = .Today {
+            didSet {
+                setupGlucoseArray(for: selectedDuration)
+            }
+        }
+
+        var selectedDurationForLoopStats: Duration = .Today {
+            didSet {
+                setupLoopStatRecords()
+            }
+        }
+
+        var selectedDurationForMealStats: Duration = .Today {
+            didSet {
+                setupMealStats(for: selectedDurationForMealStats)
+            }
+        }
 
-        private let context = CoreDataStack.shared.newTaskContext()
-        private let viewContext = CoreDataStack.shared.persistentContainer.viewContext
+        /// TDD-related properties
+
+        /// Total insulin dose for the last 24 hours
+        var currentTDD: Decimal = 0
+
+        /// Total insulin dose for yesterday (previous calendar day)
+        var ytdTDDValue: Decimal = 0
+
+        /// Average TDD for the selected time period
+        var averageTDD: Decimal = 0
+
+        /// Array of daily total doses for the selected period
+        var dailyTotalDoses: [TDD] = []
+
+        /// Configuration for TDD display and calculations
+        private(set) var tddConfig = TDDConfiguration() {
+            didSet {
+                if oldValue.requestedDays != tddConfig.requestedDays ||
+                    oldValue.endDate != tddConfig.endDate
+                {
+                    Task {
+                        await updateTDDValues()
+                    }
+                }
+            }
+        }
+
+        /// Number of days to display in the TDD chart
+        var requestedDaysTDD: Int {
+            get { tddConfig.requestedDays }
+            set { tddConfig.requestedDays = newValue }
+        }
+
+        /// End date for the TDD chart
+        var requestedEndDayTDD: Date {
+            get { tddConfig.endDate }
+            set {
+                if let adjustedDate = Calendar.current.date(
+                    bySettingHour: 23,
+                    minute: 59,
+                    second: 59,
+                    of: newValue
+                ) {
+                    tddConfig.endDate = adjustedDate
+                }
+            }
+        }
+
+        let context = CoreDataStack.shared.newTaskContext()
+        let viewContext = CoreDataStack.shared.persistentContainer.viewContext
+        let determinationFetchContext = CoreDataStack.shared.newTaskContext()
+        let loopTaskContext = CoreDataStack.shared.newTaskContext()
+        let mealTaskContext = CoreDataStack.shared.newTaskContext()
+        let bolusTaskContext = CoreDataStack.shared.newTaskContext()
 
         enum Duration: String, CaseIterable, Identifiable {
             case Today
-            case Day
-            case Week
-            case Month
-            case Total
+            case Day = "24h"
+            case Week = "7 Days"
+            case Month = "30 Days"
+            case Total = "All"
+
             var id: Self { self }
         }
 
+        var hourlyStats: [HourlyStats] = []
+        var glucoseRangeStats: [GlucoseRangeStats] = []
+
+        var bolusStats: [BolusStats] = []
+
         override func subscribe() {
-            /// Default is today
             setupGlucoseArray(for: .Today)
+            setupTDDs()
+            setupLoopStatRecords()
+            setupMealStats(for: selectedDurationForMealStats)
+            updateBolusStats()
             highLimit = settingsManager.settings.high
             lowLimit = settingsManager.settings.low
             units = settingsManager.settings.units
@@ -40,8 +120,19 @@ 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)
+            }
+        }
+
+        func setupTDDs() {
+            Task {
+                await updateTDDValues()
             }
         }
 

+ 0 - 296
FreeAPS/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
-    }
-}

+ 285 - 113
FreeAPS/Sources/Modules/Stat/View/StatRootView.swift

@@ -1,150 +1,322 @@
 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,
-                        highLimit: state.highLimit,
-                        lowLimit: state.lowLimit,
-                        units: state.units,
-                        hbA1cDisplayUnit: state.hbA1cDisplayUnit
+        @State var state = StateModel()
+        @State private var selectedView: ViewType = .statistics
+        @State private var selectedChartType: ChartType = .percentile
+
+        enum ViewType: String, CaseIterable, Identifiable {
+            case statistics = "Time in Range"
+            case tdd = "Total Daily Doses"
+            case loops = "Loop Stats"
+            case meals = "Meal Stats"
+
+            var id: String { rawValue }
+            var title: String {
+                switch self {
+                case .statistics: return NSLocalizedString("Time in Range", comment: "Statistics view title")
+                case .tdd: return NSLocalizedString("Total Daily Doses", comment: "TDD view title")
+                case .loops: return NSLocalizedString("Loop Stats", comment: "Loop stats view title")
+                case .meals: return NSLocalizedString("Meal Stats", comment: "Meal stats view title")
+                }
+            }
+        }
+
+        enum ChartType: String, CaseIterable {
+            case percentile = "Percentile"
+            case stacked = "Distribution"
+        }
+
+        var body: some View {
+            VStack(spacing: Constants.spacing) {
+                segmentedPicker
+
+                contentView
+                    .animation(.easeInOut, value: selectedView)
+            }
+            .background(appState.trioBackgroundColor(for: colorScheme))
+            .onAppear(perform: configureView)
+            .navigationBarTitleDisplayMode(.inline)
+            .navigationTitle("Statistics")
+            .toolbar {
+                ToolbarItem(placement: .topBarLeading) {
+                    closeButton
+                }
+            }
+        }
+
+        // MARK: - Views
+
+        private var segmentedPicker: some View {
+            Picker("View", selection: $selectedView) {
+                ForEach(ViewType.allCases) { viewType in
+                    Text(viewType.title).tag(viewType)
+                }
+            }
+            .pickerStyle(.segmented)
+            .padding(.horizontal)
+        }
+
+        @ViewBuilder private var contentView: some View {
+            switch selectedView {
+            case .statistics:
+                statsView()
+            case .tdd:
+                tddView()
+            case .loops:
+                loopsView()
+            case .meals:
+                mealsView()
+            }
+        }
+
+        private var closeButton: some View {
+            Button(action: state.hideModal) {
+                Text("Close")
+                    .foregroundColor(.tabBar)
+            }
+        }
+
+        // MARK: - Stats View
+
+        @ViewBuilder func statsView() -> some View {
+            ScrollView {
+                VStack(spacing: Constants.spacing) {
+                    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
+                    }
+                }
+                .padding()
+            }
+        }
+
+        @ViewBuilder func tddView() -> some View {
+            ScrollView {
+                VStack(spacing: Constants.spacing) {
+                    TDDChartView(
+                        state: state,
+                        selectedDays: $state.requestedDaysTDD,
+                        selectedEndDate: $state.requestedEndDayTDD,
+                        dailyTotalDoses: $state.dailyTotalDoses,
+                        averageTDD: state.averageTDD,
+                        ytdTDD: state.ytdTDDValue
                     )
-                case .Day:
-                    StatsView(
-                        filter: filter.day,
-                        highLimit: state.highLimit,
-                        lowLimit: state.lowLimit,
-                        units: state.units,
-                        hbA1cDisplayUnit: state.hbA1cDisplayUnit
+                    .onChange(of: state.requestedDaysTDD) {
+                        state.updateBolusStats()
+                    }
+                    .onChange(of: state.requestedEndDayTDD) {
+                        state.updateBolusStats()
+                    }
+
+                    BolusStatsView(
+                        bolusStats: state.bolusStats,
+                        selectedDays: $state.requestedDaysTDD,
+                        selectedEndDate: $state.requestedEndDayTDD
                     )
-                case .Week:
-                    StatsView(
-                        filter: filter.week,
+                }
+                .padding()
+            }
+        }
+
+        @ViewBuilder func loopsView() -> some View {
+            ScrollView {
+                VStack(spacing: Constants.spacing) {
+                    if state.loopStatRecords.isEmpty {
+                        ContentUnavailableView(
+                            "No Loop Data",
+                            systemImage: "clock.arrow.2.circlepath",
+                            description: Text("Loop statistics will appear here once data is available.")
+                        )
+                    } else {
+                        loopsCard
+                        loopStats
+                    }
+                }
+                .padding()
+            }
+        }
+
+        private var timeInRangeCard: some View {
+            StatCard {
+                VStack(spacing: Constants.spacing) {
+                    HStack {
+                        Text("Time in Range")
+                            .font(.headline)
+
+                        Spacer()
+
+                        HStack {
+                            Picker("Duration", selection: $state.selectedDuration) {
+                                ForEach(StateModel.Duration.allCases, id: \.self) { duration in
+                                    Text(duration.rawValue)
+                                }
+                            }
+                            .pickerStyle(.menu)
+
+                            Picker("Chart Type", selection: $selectedChartType) {
+                                ForEach(ChartType.allCases, id: \.self) { type in
+                                    Text(type.rawValue)
+                                }
+                            }
+                            .pickerStyle(.menu)
+                        }
+                    }
+
+                    if selectedChartType == .percentile {
+                        GlucoseAreaChart(
+                            glucose: state.glucoseFromPersistence,
+                            highLimit: state.highLimit,
+                            lowLimit: state.lowLimit,
+                            isTodayOrLast24h: state.selectedDuration == .Today || state.selectedDuration == .Day,
+                            units: state.units,
+                            hourlyStats: state.hourlyStats
+                        )
+                    } else {
+                        GlucoseStackedAreaChart(
+                            glucose: state.glucoseFromPersistence,
+                            highLimit: state.highLimit,
+                            lowLimit: state.lowLimit,
+                            isToday: state.selectedDuration == .Today || state.selectedDuration == .Day,
+                            units: state.units,
+                            glucoseRangeStats: state.glucoseRangeStats
+                        )
+                    }
+
+                    Divider()
+
+                    SectorChart(
                         highLimit: state.highLimit,
                         lowLimit: state.lowLimit,
                         units: state.units,
-                        hbA1cDisplayUnit: state.hbA1cDisplayUnit
+                        hbA1cDisplayUnit: state.hbA1cDisplayUnit,
+                        timeInRangeChartStyle: state.timeInRangeChartStyle,
+                        glucose: state.glucoseFromPersistence
                     )
-                case .Month:
-                    StatsView(
-                        filter: filter.month,
+                }
+            }
+        }
+
+        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 .Total:
-                    StatsView(
-                        filter: filter.total,
+
+                    Divider()
+
+                    BareStatisticsView.BloodGlucoseView(
                         highLimit: state.highLimit,
                         lowLimit: state.lowLimit,
                         units: state.units,
-                        hbA1cDisplayUnit: state.hbA1cDisplayUnit
+                        hbA1cDisplayUnit: state.hbA1cDisplayUnit,
+                        glucose: state.glucoseFromPersistence
                     )
                 }
             }
         }
 
-        @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
-                )
+        private var loopsCard: some View {
+            StatCard {
+                VStack(spacing: Constants.spacing) {
+                    HStack {
+                        Text("Loops")
+                            .font(.headline)
+
+                        Spacer()
+
+                        Picker("Duration", selection: $state.selectedDurationForLoopStats) {
+                            ForEach(StateModel.Duration.allCases, id: \.self) { duration in
+                                Text(duration.rawValue)
+                            }
+                        }
+                        .pickerStyle(.menu)
+                    }
+
+                    LoopStatsView(
+                        loopStatRecords: state.loopStatRecords,
+                        selectedDuration: state.selectedDurationForLoopStats,
+                        groupedStats: state.groupedLoopStats
+                    )
+                }
             }
         }
 
-        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))
-                    }
-                }.onChange(of: state.selectedDuration) { _, newValue in
-                    state.setupGlucoseArray(for: newValue)
+        private var loopStats: some View {
+            StatCard {
+                VStack(spacing: Constants.spacing) {
+                    BareStatisticsView.LoopsView(
+                        highLimit: state.highLimit,
+                        lowLimit: state.lowLimit,
+                        units: state.units,
+                        hbA1cDisplayUnit: state.hbA1cDisplayUnit,
+                        loopStatRecords: state.loopStatRecords
+                    )
                 }
-                .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")
-                                }
-                            }
-                        )
-                    })
+            }
+        }
+
+        @ViewBuilder func mealsView() -> some View {
+            ScrollView {
+                VStack(spacing: Constants.spacing) {
+                    Picker("Duration", selection: $state.selectedDurationForMealStats) {
+                        ForEach(StateModel.Duration.allCases, id: \.self) { duration in
+                            Text(duration.rawValue)
+                        }
+                    }
+                    .pickerStyle(.menu)
+
+                    MealStatsView(
+                        mealStats: state.mealStats,
+                        selectedDuration: state.selectedDurationForMealStats
+                    )
                 }
+                .padding()
+            }
         }
     }
 }
+
+// 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
FreeAPS/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
FreeAPS/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)
+        }
+    }
+}

+ 92 - 0
FreeAPS/Sources/Modules/Stat/View/ViewElements/BolusStatsView.swift

@@ -0,0 +1,92 @@
+import Charts
+import SwiftUI
+
+struct BolusStatsView: View {
+    let bolusStats: [BolusStats]
+    @Binding var selectedDays: Int
+    @Binding var selectedEndDate: Date
+
+    private var hasData: Bool {
+        bolusStats.contains { $0.manualBolus > 0 || $0.smb > 0 || $0.external > 0 }
+    }
+
+    var body: some View {
+        if bolusStats.isEmpty || !hasData {
+            ContentUnavailableView(
+                "No Bolus Data",
+                systemImage: "cross.vial",
+                description: Text("Bolus statistics will appear here once data is available.")
+            )
+        } else {
+            StatCard {
+                VStack(alignment: .leading, spacing: 8) {
+                    Text("Bolus Distribution")
+                        .font(.headline)
+
+                    Chart(bolusStats) { stat in
+                        // External Bolus (Bottom)
+                        BarMark(
+                            x: .value("Date", stat.date, unit: .day),
+                            y: .value("Amount", stat.external)
+                        )
+                        .foregroundStyle(by: .value("Type", "External"))
+
+                        // SMB (Middle)
+                        BarMark(
+                            x: .value("Date", stat.date, unit: .day),
+                            y: .value("Amount", stat.smb)
+                        )
+                        .foregroundStyle(by: .value("Type", "SMB"))
+
+                        // Manual Bolus (Top)
+                        BarMark(
+                            x: .value("Date", stat.date, unit: .day),
+                            y: .value("Amount", stat.manualBolus)
+                        )
+                        .foregroundStyle(by: .value("Type", "Manual"))
+                    }
+                    .chartForegroundStyleScale([
+                        "Manual": Color.teal,
+                        "SMB": Color.blue,
+                        "External": Color.purple
+                    ])
+                    .chartLegend(position: .top, alignment: .leading, spacing: 12)
+                    .frame(height: 200)
+                    .chartXAxis {
+                        bolusStatsChartXAxisMarks
+                    }
+                    .chartYAxis {
+                        bolusStatsChartYAxisMarks
+                    }
+                }
+            }
+            .padding()
+        }
+    }
+
+    private var bolusStatsChartXAxisMarks: some AxisContent {
+        AxisMarks { value in
+            if let date = value.as(Date.self) {
+                AxisValueLabel {
+                    if selectedDays < 8 {
+                        Text(date, format: .dateTime.weekday(.abbreviated))
+                    } else {
+                        Text(date, format: .dateTime.day().month(.defaultDigits))
+                    }
+                }
+                AxisGridLine()
+            }
+        }
+    }
+
+    private var bolusStatsChartYAxisMarks: some AxisContent {
+        AxisMarks(position: .leading) { value in
+            if let amount = value.as(Double.self) {
+                AxisValueLabel {
+                    Text(amount.formatted(.number.precision(.fractionLength(1))) + " U")
+                }
+                AxisGridLine()
+            }
+        }
+    }
+}

+ 164 - 0
FreeAPS/Sources/Modules/Stat/View/ViewElements/GlucoseAreaChart.swift

@@ -0,0 +1,164 @@
+import Charts
+import SwiftUI
+
+struct GlucoseAreaChart: View {
+    let glucose: [GlucoseStored]
+    let highLimit: Decimal
+    let lowLimit: Decimal
+    let isTodayOrLast24h: Bool
+    let units: GlucoseUnits
+    let hourlyStats: [HourlyStats]
+
+    var body: some View {
+        VStack(alignment: .leading, spacing: 8) {
+            Text("Glucose Distribution")
+                .font(.headline)
+                .foregroundStyle(.primary)
+
+            Chart {
+                if isTodayOrLast24h {
+                    // Single day line chart
+                    ForEach(glucose.sorted(by: { ($0.date ?? Date()) < ($1.date ?? Date()) }), id: \.id) { reading in
+                        LineMark(
+                            x: .value("Time", reading.date ?? Date()),
+                            y: .value("Glucose", Double(reading.glucose))
+                        )
+                        .lineStyle(StrokeStyle(lineWidth: 2))
+                        .foregroundStyle(.blue)
+                    }
+                } else {
+                    // Statistical view for longer periods
+                    // 10-90 percentile area
+                    ForEach(hourlyStats, id: \.hour) { stats in
+                        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(0.2))
+                    }
+
+                    // 25-75 percentile area
+                    ForEach(hourlyStats, id: \.hour) { stats in
+                        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(0.3))
+                    }
+
+                    // Median line
+                    ForEach(hourlyStats, id: \.hour) { stats in
+                        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)
+            }
+            .chartYScale(domain: 40 ... 400)
+            .chartYAxis {
+                AxisMarks(position: .leading)
+            }
+            .chartYAxisLabel(alignment: .leading) {
+                Text("\(units.rawValue)")
+                    .foregroundStyle(.primary)
+                    .font(.caption)
+                    .padding(.vertical, 3)
+            }
+            .chartXAxis {
+                AxisMarks(values: .stride(by: .hour, count: 3)) { _ in
+                    AxisValueLabel(format: .dateTime.hour(.defaultDigits(amPM: .narrow)), anchor: .top)
+                    AxisGridLine()
+                }
+            }
+            .frame(height: 200)
+
+            // Legend
+            if !isTodayOrLast24h {
+                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)
+    }
+}
+
+private extension Calendar {
+    func startOfHour(for date: Date) -> Date {
+        let components = dateComponents([.year, .month, .day, .hour], from: date)
+        return self.date(from: components) ?? date
+    }
+}

+ 55 - 0
FreeAPS/Sources/Modules/Stat/View/ViewElements/GlucoseStackedAreaChart.swift

@@ -0,0 +1,55 @@
+import Charts
+import SwiftUI
+
+struct GlucoseStackedAreaChart: View {
+    let glucose: [GlucoseStored]
+    let highLimit: Decimal
+    let lowLimit: Decimal
+    let isToday: Bool
+    let units: GlucoseUnits
+    let glucoseRangeStats: [GlucoseRangeStats]
+
+    var body: some View {
+        VStack(alignment: .leading, spacing: 8) {
+            Text("Glucose Distribution")
+                .font(.headline)
+                .foregroundStyle(.primary)
+
+            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: .leading)
+            }
+            .chartYAxisLabel(alignment: .leading) {
+                Text("Percentage")
+                    .foregroundStyle(.primary)
+                    .font(.caption)
+                    .padding(.vertical, 3)
+            }
+            .chartXAxis {
+                AxisMarks(values: .stride(by: .hour, count: 3)) { _ in
+                    AxisValueLabel(format: .dateTime.hour(.defaultDigits(amPM: .narrow)), anchor: .top)
+                    AxisGridLine()
+                }
+            }
+            .frame(height: 200)
+        }
+    }
+}

+ 192 - 0
FreeAPS/Sources/Modules/Stat/View/ViewElements/LoopStatsView.swift

@@ -0,0 +1,192 @@
+import Charts
+import SwiftUI
+
+struct LoopStatsView: View {
+    let loopStatRecords: [LoopStatRecord]
+    let selectedDuration: Stat.StateModel.Duration
+    let groupedStats: [LoopStatsByPeriod]
+    private let calendar = Calendar.current
+
+    private var medianLoopDuration: Double {
+        groupedStats.first?.medianDuration ?? 0
+    }
+
+    var body: some View {
+        VStack(spacing: 20) {
+            loopDurationChart
+            Divider()
+            loopStatsChart
+        }
+    }
+
+    private var loopDurationChart: some View {
+        Chart {
+            ForEach(loopStatRecords, id: \.id) { record in
+                LineMark(
+                    x: .value("Time", record.start ?? Date(), unit: .hour),
+                    y: .value("Duration", record.duration / 1000)
+                )
+                .interpolationMethod(.catmullRom)
+                .foregroundStyle(.blue.opacity(0.6))
+            }
+
+            RuleMark(
+                y: .value("Median", medianLoopDuration / 1000)
+            )
+            .lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 5]))
+            .foregroundStyle(.orange)
+            .annotation(position: .top, alignment: .trailing) {
+                Text("\((medianLoopDuration / 1000).formatted(.number.precision(.fractionLength(1)))) s")
+                    .font(.caption)
+                    .foregroundStyle(.orange)
+            }
+        }
+        .chartYAxis {
+            loopDurationChartYAxisMarks
+        }
+        .chartYAxisLabel(alignment: .leading) {
+            Text("Loop duration")
+                .foregroundStyle(.primary)
+                .font(.caption)
+                .padding(.vertical, 3)
+        }
+        .chartXAxis {
+            loopDurationAxisMarks
+        }
+        .frame(height: 200)
+        .padding(.horizontal)
+    }
+
+    private var loopDurationAxisMarks: some AxisContent {
+        AxisMarks { value in
+            if let date = value.as(Date.self) {
+                AxisValueLabel {
+                    switch selectedDuration {
+                    case .Day,
+                         .Today:
+                        Text(date, format: .dateTime.hour(.defaultDigits(amPM: .abbreviated)))
+                    case .Week:
+                        Text(date, format: .dateTime.weekday(.abbreviated))
+                    case .Month,
+                         .Total:
+                        Text(date, format: .dateTime.day().month(.defaultDigits))
+                    }
+                }
+                AxisGridLine()
+            }
+        }
+    }
+
+    private var loopDurationChartYAxisMarks: some AxisContent {
+        AxisMarks(position: .leading) { value in
+            if let duration = value.as(Double.self) {
+                AxisValueLabel {
+                    Text("\(duration.formatted(.number.precision(.fractionLength(1)))) s")
+                        .font(.caption)
+                }
+                AxisGridLine()
+            }
+        }
+    }
+
+    private var loopStatsChart: some View {
+        Chart {
+            ForEach(groupedStats) { stat in
+                // Stacked Bar Chart first (will be in background)
+                // Succeeded Loops
+                BarMark(
+                    x: .value("Time", stat.period, unit: .day),
+                    y: .value("Successful", stat.successPercentage)
+                )
+                .foregroundStyle(Color.green.opacity(0.9))
+                .foregroundStyle(by: .value("Type", "Success"))
+//                .zIndex(1)
+
+                // Failed Loops
+                BarMark(
+                    x: .value("Time", stat.period, unit: .day),
+                    y: .value("Failed", stat.failurePercentage)
+                )
+                .foregroundStyle(Color.red.opacity(0.9))
+                .foregroundStyle(by: .value("Type", "Failed"))
+//                .zIndex(1)
+
+                // Dotted Line Mark showing the daily Glucose counts (will overlay the bars)
+                LineMark(
+                    x: .value("Time", stat.period, unit: .day),
+                    y: .value("Glucose Count", Double(stat.glucoseCount) / 288.0 * 100)
+                )
+                .foregroundStyle(Color.blue)
+                .lineStyle(StrokeStyle(lineWidth: 2))
+                .foregroundStyle(by: .value("Type", "Glucose Count"))
+//                .zIndex(2)
+
+                PointMark(
+                    x: .value("Time", stat.period, unit: .day),
+                    y: .value("Glucose Count", Double(stat.glucoseCount) / 288.0 * 100)
+                )
+                .foregroundStyle(Color.blue)
+                .symbolSize(50)
+                .foregroundStyle(by: .value("Type", "Glucose Count"))
+//                .zIndex(3)
+            }
+        }
+        .chartForegroundStyleScale([
+            "Success": Color.green,
+            "Failed": Color.red,
+            "Glucose Count": Color.blue
+        ])
+        .chartYAxis {
+            AxisMarks(position: .leading) { value in
+                if let percent = value.as(Double.self) {
+                    AxisValueLabel {
+                        Text("\(percent.formatted(.number.precision(.fractionLength(0))))%")
+                            .font(.caption)
+                    }
+                    AxisGridLine()
+                }
+            }
+
+            let maxPossibleReadings = 288.0
+            let strideBy = 4.0
+            let defaultStride = Array(stride(from: 0, to: 100, by: 100 / strideBy))
+            let glucoseStride = Array(stride(from: 0, through: maxPossibleReadings, by: maxPossibleReadings / strideBy))
+
+            AxisMarks(position: .trailing, values: defaultStride) { axis in
+                let value = glucoseStride[axis.index]
+                AxisValueLabel("\(Int(value))", centered: true)
+                    .font(.caption)
+            }
+        }
+        .chartYAxisLabel(alignment: .leading) {
+            Text("Loop Success Rate")
+                .foregroundStyle(.primary)
+                .font(.caption)
+                .padding(.vertical, 3)
+        }
+        .chartXAxis {
+            statsAxisMarks
+        }
+        .frame(height: 200)
+        .padding(.horizontal)
+    }
+
+    private var statsAxisMarks: some AxisContent {
+        AxisMarks { value in
+            if let date = value.as(Date.self) {
+                AxisValueLabel {
+                    switch selectedDuration {
+                    case .Day,
+                         .Today,
+                         .Week:
+                        Text(date, format: .dateTime.weekday(.abbreviated))
+                    case .Month,
+                         .Total:
+                        Text(date, format: .dateTime.day().month(.defaultDigits))
+                    }
+                }
+                AxisGridLine()
+            }
+        }
+    }
+}

+ 103 - 0
FreeAPS/Sources/Modules/Stat/View/ViewElements/MealStatsView.swift

@@ -0,0 +1,103 @@
+import Charts
+import SwiftUI
+
+struct MealStatsView: View {
+    let mealStats: [MealStats]
+    let selectedDuration: Stat.StateModel.Duration
+
+    private var hasData: Bool {
+        mealStats.contains { $0.carbs > 0 || $0.fat > 0 || $0.protein > 0 }
+    }
+
+    var body: some View {
+        ScrollView {
+            if mealStats.isEmpty || !hasData {
+                ContentUnavailableView(
+                    "No Meal Data",
+                    systemImage: "fork.knife",
+                    description: Text("Meal statistics will appear here once data is available.")
+                )
+            } else {
+                StatCard {
+                    VStack(alignment: .leading, spacing: 8) {
+                        Text("Macronutrients")
+                            .font(.headline)
+
+                        Chart(mealStats) { stat in
+                            // Carbs Bar
+                            BarMark(
+                                x: .value("Date", stat.date, unit: .day),
+                                y: .value("Amount", stat.carbs),
+                                width: .ratio(0.6)
+                            )
+                            .foregroundStyle(Color.orange)
+                            .position(by: .value("Nutrient", "Carbs"))
+
+                            // Fat Bar
+                            BarMark(
+                                x: .value("Date", stat.date, unit: .day),
+                                y: .value("Amount", stat.fat),
+                                width: .ratio(0.6)
+                            )
+                            .foregroundStyle(Color.yellow)
+                            .position(by: .value("Nutrient", "Fat"))
+
+                            // Protein Bar
+                            BarMark(
+                                x: .value("Date", stat.date, unit: .day),
+                                y: .value("Amount", stat.protein),
+                                width: .ratio(0.6)
+                            )
+                            .foregroundStyle(Color.green)
+                            .position(by: .value("Nutrient", "Protein"))
+                        }
+                        .chartForegroundStyleScale([
+                            "Carbs": Color.orange,
+                            "Fat": Color.yellow,
+                            "Protein": Color.green
+                        ])
+                        .chartLegend(position: .top, alignment: .leading, spacing: 12)
+                        .frame(height: 200)
+                        .chartXAxis {
+                            mealChartXAxisMarks
+                        }
+                        .chartYAxis {
+                            mealChartYAxisMarks
+                        }
+                    }
+                }
+                .padding()
+            }
+        }
+    }
+
+    private var mealChartXAxisMarks: some AxisContent {
+        AxisMarks { value in
+            if let date = value.as(Date.self) {
+                AxisValueLabel {
+                    switch selectedDuration {
+                    case .Day,
+                         .Today,
+                         .Week:
+                        Text(date, format: .dateTime.weekday(.abbreviated))
+                    case .Month,
+                         .Total:
+                        Text(date, format: .dateTime.day().month(.defaultDigits))
+                    }
+                }
+                AxisGridLine()
+            }
+        }
+    }
+
+    private var mealChartYAxisMarks: some AxisContent {
+        AxisMarks(position: .leading) { value in
+            if let amount = value.as(Double.self) {
+                AxisValueLabel {
+                    Text("\(Int(amount))g")
+                }
+                AxisGridLine()
+            }
+        }
+    }
+}

+ 88 - 0
FreeAPS/Sources/Modules/Stat/View/ViewElements/SectorChart.swift

@@ -0,0 +1,88 @@
+import Charts
+import CoreData
+import SwiftDate
+import SwiftUI
+
+struct SectorChart: View {
+    private enum Constants {
+        static let chartHeight: CGFloat = 200
+        static let spacing: CGFloat = 8
+        static let labelSpacing: CGFloat = 4
+    }
+
+    let highLimit: Decimal
+    let lowLimit: Decimal
+    let units: GlucoseUnits
+    let hbA1cDisplayUnit: HbA1cDisplayUnit
+    let timeInRangeChartStyle: TimeInRangeChartStyle
+    let glucose: [GlucoseStored]
+
+    @Environment(\.colorScheme) var colorScheme
+
+    var body: some View {
+        HStack(spacing: 20) {
+            Chart {
+                ForEach(timeInRangeData, id: \.string) { data in
+                    SectorMark(
+                        angle: .value("Percentage", data.decimal),
+                        innerRadius: .ratio(0.618), // Golden ratio
+                        angularInset: 1.5
+                    )
+                    .foregroundStyle(data.color.gradient)
+                }
+            }
+            .frame(height: Constants.chartHeight)
+
+            // Legend
+            VStack(spacing: Constants.spacing) {
+                ForEach(timeInRangeData, id: \.string) { data in
+                    HStack(spacing: Constants.spacing) {
+                        Circle()
+                            .fill(data.color)
+                            .frame(width: 12, height: 12)
+
+                        Text(data.string)
+                            .font(.subheadline)
+
+                        Spacer()
+
+                        Text(formatPercentage(data.decimal))
+                            .font(.subheadline)
+                            .bold()
+                    }
+                }
+            }
+            .padding(.top, Constants.spacing)
+        }
+    }
+
+    // MARK: - Data Processing
+
+    private var timeInRangeData: [(decimal: Decimal, string: String, color: Color)] {
+        let total = glucose.count
+        guard total > 0 else { return [] }
+
+        let hyperArray = glucose.filter { $0.glucose >= Int(highLimit) }
+        let hyperReadings = hyperArray.count
+        let hyperPercentage = Decimal(hyperReadings) / Decimal(total) * 100
+
+        let hypoArray = glucose.filter { $0.glucose <= Int(lowLimit) }
+        let hypoReadings = hypoArray.count
+        let hypoPercentage = Decimal(hypoReadings) / Decimal(total) * 100
+
+        let normalPercentage = 100 - (hypoPercentage + hyperPercentage)
+
+        return [
+            (normalPercentage, "In Range", .green),
+            (hyperPercentage, "High", .yellow),
+            (hypoPercentage, "Low", .red)
+        ]
+    }
+
+    private func formatPercentage(_ value: Decimal) -> String {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .percent
+        formatter.maximumFractionDigits = 1
+        return formatter.string(from: NSDecimalNumber(decimal: value / 100)) ?? "0%"
+    }
+}

+ 224 - 0
FreeAPS/Sources/Modules/Stat/View/ViewElements/TDDChart.swift

@@ -0,0 +1,224 @@
+import Charts
+import SwiftUI
+
+struct TDDChartView: View {
+    private enum Constants {
+        static let dayOptions = [3, 5, 7, 10, 14, 21, 28]
+        static let chartHeight: CGFloat = 200
+        static let spacing: CGFloat = 8
+        static let cornerRadius: CGFloat = 10
+        static let summaryBackgroundOpacity = 0.1
+    }
+
+    let state: Stat.StateModel
+    @Binding var selectedDays: Int
+    @Binding var selectedEndDate: Date
+    @Binding var dailyTotalDoses: [TDD]
+    var averageTDD: Decimal
+    var ytdTDD: Decimal
+
+    @Environment(\.colorScheme) var colorScheme
+
+    var body: some View {
+        if dailyTotalDoses.isEmpty || state.currentTDD == 0 {
+            ContentUnavailableView(
+                "No TDD Data",
+                systemImage: "chart.bar.xaxis",
+                description: Text("Total Daily Doses will appear here once data is available.")
+            )
+        } else {
+            VStack(spacing: Constants.spacing) {
+                dateSelectionView
+                summaryCardView
+                chartCard
+            }
+            .padding()
+        }
+    }
+
+    // MARK: - Views
+
+    private var dateSelectionView: some View {
+        HStack {
+            Text("Time Frame")
+                .font(.subheadline)
+                .foregroundStyle(.secondary)
+
+            Spacer()
+
+            CustomDatePicker(selection: $selectedEndDate)
+                .frame(height: 30)
+
+            Picker("Days", selection: $selectedDays) {
+                ForEach(Constants.dayOptions, id: \.self) { days in
+                    Text("\(days) days").tag(days)
+                }
+            }
+            .pickerStyle(MenuPickerStyle())
+        }
+    }
+
+    private var summaryCardView: some View {
+        VStack(spacing: Constants.spacing) {
+            tddRow(
+                title: "Today",
+                value: state.currentTDD
+            )
+            Divider()
+            tddRow(
+                title: "Yesterday",
+                value: ytdTDD
+            )
+            Divider()
+            tddRow(
+                title: "Average \(selectedDays) days",
+                value: averageTDD
+            )
+        }
+        .padding()
+        .background(
+            RoundedRectangle(cornerRadius: Constants.cornerRadius)
+                .fill(Color.secondary.opacity(Constants.summaryBackgroundOpacity))
+        )
+    }
+
+    private var chartCard: some View {
+        VStack(alignment: .leading, spacing: Constants.spacing) {
+            Text("Total Daily Doses")
+                .font(.headline)
+
+            Chart {
+                ForEach(chartData, id: \.date) { entry in
+                    BarMark(
+                        x: .value("Date", entry.date, unit: .day),
+                        y: .value("Insulin", entry.dose)
+                    )
+                    .foregroundStyle(Color.insulin.gradient)
+                    .annotation(position: .top) {
+                        if entry.dose > 0 {
+                            Text(formatDose(entry.dose))
+                                .font(.caption2)
+                                .foregroundStyle(.primary)
+                        }
+                    }
+                }
+
+                if let average = calculateAverage() {
+                    RuleMark(y: .value("Average", average))
+                        .foregroundStyle(.primary)
+                        .lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 5]))
+                        .annotation(position: .automatic) {
+                            Text("\(formatDose(average)) U")
+                                .font(.caption)
+                                .foregroundStyle(Color.insulin)
+                        }
+                }
+            }
+            .chartXAxis {
+                tddChartXAxisMarks
+            }
+            .chartYAxis {
+                AxisMarks { _ in
+                    AxisValueLabel()
+                    AxisGridLine()
+                }
+            }
+            .chartYAxisLabel(alignment: .trailing) {
+                Text("Units (U)")
+                    .foregroundColor(.primary)
+            }
+            .chartYScale(domain: 0 ... calculateYAxisMaximum())
+        }
+        .frame(height: 200)
+        .padding()
+        .background(
+            RoundedRectangle(cornerRadius: Constants.cornerRadius)
+                .fill(Color.secondary.opacity(Constants.summaryBackgroundOpacity))
+        )
+    }
+
+    // MARK: - Helper Views
+
+    private var tddChartXAxisMarks: some AxisContent {
+        AxisMarks(values: .stride(by: .day)) { value in
+            if let date = value.as(Date.self),
+               xAxisLabelValues().contains(where: { $0.date == date })
+            {
+                AxisValueLabel(xAxisLabelValues().first { $0.date == date }?.label ?? "")
+            }
+            AxisGridLine()
+        }
+    }
+
+    private func tddRow(title: String, value: Decimal) -> some View {
+        HStack {
+            Text(title)
+                .foregroundStyle(.secondary)
+            Spacer()
+            Text(formatDose(value))
+                .foregroundColor(.primary)
+            Text("U")
+                .foregroundStyle(.secondary)
+        }
+        .font(.subheadline)
+    }
+
+    // MARK: - Data Processing
+
+    private var chartData: [(date: Date, dose: Decimal)] {
+        completeData(forDays: selectedDays)
+    }
+
+    private func calculateAverage() -> Decimal? {
+        let nonZeroDoses = chartData.map(\.dose).filter { $0 > 0 }
+        guard !nonZeroDoses.isEmpty else { return nil }
+        return nonZeroDoses.reduce(0, +) / Decimal(nonZeroDoses.count)
+    }
+
+    private func calculateYAxisMaximum() -> Double {
+        let maxDose = chartData.map(\.dose).max() ?? 0
+        let average = calculateAverage() ?? 0
+        return (max(maxDose, average) * 1.2).doubleValue // Add 20% padding
+    }
+
+    private func formatDose(_ value: Decimal) -> String {
+        Formatter.decimalFormatterWithOneFractionDigit.string(from: value as NSNumber) ?? "0"
+    }
+
+    private func completeData(forDays days: Int) -> [(date: Date, dose: Decimal)] {
+        var completeData: [(date: Date, dose: Decimal)] = []
+        let calendar = Calendar.current
+        var currentDate = calendar.startOfDay(for: selectedEndDate)
+
+        for _ in 0 ..< days {
+            if let existingEntry = dailyTotalDoses.first(where: { entry in
+                guard let timestamp = entry.timestamp else { return false }
+                return calendar.isDate(timestamp, inSameDayAs: currentDate)
+            }) {
+                completeData.append((date: currentDate, dose: existingEntry.totalDailyDose ?? 0))
+            } else {
+                completeData.append((date: currentDate, dose: 0))
+            }
+            currentDate = calendar.date(byAdding: .day, value: -1, to: currentDate) ?? currentDate
+        }
+        return completeData.reversed()
+    }
+
+    private func xAxisLabelValues() -> [(date: Date, label: String)] {
+        let data = chartData
+        let stride = selectedDays > 13 ? max(1, selectedDays / 7) : 1
+
+        return data.enumerated().compactMap { index, entry in
+            if index % stride == 0 || index == data.count - 1 {
+                return (date: entry.date, label: Formatter.dayFormatter.string(from: entry.date))
+            }
+            return nil
+        }
+    }
+}
+
+private extension Decimal {
+    var doubleValue: Double {
+        NSDecimalNumber(decimal: self).doubleValue
+    }
+}

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

@@ -50,4 +50,8 @@ extension NSPredicate {
             true as NSNumber
         )
     }
+
+    static func determinationPeriod(from startDate: Date, to endDate: Date) -> NSPredicate {
+        NSPredicate(format: "deliverAt >= %@ AND deliverAt <= %@", startDate as NSDate, endDate as NSDate)
+    }
 }