Ivan Valkou hace 5 años
padre
commit
ceef1fc585
Se han modificado 32 ficheros con 327 adiciones y 974 borrados
  1. 27 179
      FreeAPS.xcodeproj/project.pbxproj
  2. 0 9
      FreeAPS.xcworkspace/xcshareddata/swiftpm/Package.resolved
  3. 0 10
      FreeAPS/Sources/Charts/Extensions/Array+halve().swift
  4. 0 7
      FreeAPS/Sources/Charts/Extensions/Date+-.swift
  5. 0 13
      FreeAPS/Sources/Charts/Extensions/Double+getBoundGlucose().swift
  6. 0 16
      FreeAPS/Sources/Charts/Extensions/View+Modifiers.swift
  7. 0 14
      FreeAPS/Sources/Charts/Extensions/View+if().swift
  8. 0 43
      FreeAPS/Sources/Charts/Helpers/APSDataFormatter.swift
  9. 0 5
      FreeAPS/Sources/Charts/Helpers/getChartWidth().swift
  10. 0 10
      FreeAPS/Sources/Charts/Models/APSDataTypes.swift
  11. 0 6
      FreeAPS/Sources/Charts/Models/BoundTypes.swift
  12. 0 8
      FreeAPS/Sources/Charts/Models/GlucosePointData.swift
  13. 0 8
      FreeAPS/Sources/Charts/Models/InformationBarEntryData.swift
  14. 0 6
      FreeAPS/Sources/Charts/Models/MeshEntryOrientations.swift
  15. 0 8
      FreeAPS/Sources/Charts/Models/PointData.swift
  16. 0 7
      FreeAPS/Sources/Charts/Models/PredictionLineData.swift
  17. 0 8
      FreeAPS/Sources/Charts/Models/PredictionType.swift
  18. 0 81
      FreeAPS/Sources/Charts/Views/Charts/CombinedChartView.swift
  19. 0 97
      FreeAPS/Sources/Charts/Views/Charts/PointChartView.swift
  20. 0 36
      FreeAPS/Sources/Charts/Views/Charts/PredictionsChartView.swift
  21. 0 5
      FreeAPS/Sources/Charts/Views/ChartsConfig.swift
  22. 0 59
      FreeAPS/Sources/Charts/Views/Components/GlucoseArrowView.swift
  23. 0 59
      FreeAPS/Sources/Charts/Views/Components/GlucoseInformationBarView.swift
  24. 0 15
      FreeAPS/Sources/Charts/Views/Components/HoursPickerView.swift
  25. 0 21
      FreeAPS/Sources/Charts/Views/Points/GlucosePointView.swift
  26. 0 42
      FreeAPS/Sources/Charts/Views/Points/PredictionPointView.swift
  27. 18 0
      FreeAPS/Sources/Helpers/ConcurrentMap.swift
  28. 2 2
      FreeAPS/Sources/Models/Suggestion.swift
  29. 0 198
      FreeAPS/Sources/Modules/Home/View/GlucoseChartView.swift
  30. 5 2
      FreeAPS/Sources/Modules/Home/View/HomeRootView.swift
  31. 257 0
      FreeAPS/Sources/Modules/Home/View/MainChartView.swift
  32. 18 0
      FreeAPS/Sources/Views/View+Snapshot.swift

+ 27 - 179
FreeAPS.xcodeproj/project.pbxproj

@@ -93,6 +93,7 @@
 		3821ED4C25DD18BA00BC42AD /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3821ED4B25DD18BA00BC42AD /* Constants.swift */; };
 		382C133725F13A1E00715CE1 /* InsulinSensitivities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 382C133625F13A1E00715CE1 /* InsulinSensitivities.swift */; };
 		382C134B25F14E3700715CE1 /* BGTargets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 382C134A25F14E3700715CE1 /* BGTargets.swift */; };
+		3833B46D26012030003021B3 /* Algorithms in Frameworks */ = {isa = PBXBuildFile; productRef = 3833B46C26012030003021B3 /* Algorithms */; };
 		383420D625FFE38C002D46C1 /* LoopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 383420D525FFE38C002D46C1 /* LoopView.swift */; };
 		383420D925FFEB3F002D46C1 /* Popup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 383420D825FFEB3F002D46C1 /* Popup.swift */; };
 		383948D325CD4D6D00E91849 /* Disk in Frameworks */ = {isa = PBXBuildFile; productRef = 383948D225CD4D6D00E91849 /* Disk */; };
@@ -150,16 +151,17 @@
 		388E5A6025B6F2310019842D /* Autosens.swift in Sources */ = {isa = PBXBuildFile; fileRef = 388E5A5F25B6F2310019842D /* Autosens.swift */; };
 		389442CB25F65F7100FA1F27 /* NightscoutTreatment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389442CA25F65F7100FA1F27 /* NightscoutTreatment.swift */; };
 		3895E4C625B9E00D00214B37 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3895E4C525B9E00D00214B37 /* Preferences.swift */; };
-		389FE32A25F3AC44002E92E0 /* GlucoseChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389FE32925F3AC44002E92E0 /* GlucoseChartView.swift */; };
+		389ECDFE2601061500D86C4F /* View+Snapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389ECDFD2601061500D86C4F /* View+Snapshot.swift */; };
+		389ECE052601144100D86C4F /* ConcurrentMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 389ECE042601144100D86C4F /* ConcurrentMap.swift */; };
 		38A00B1F25FC00F7006BC0B0 /* Autotune.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38A00B1E25FC00F7006BC0B0 /* Autotune.swift */; };
 		38A00B2325FC2B55006BC0B0 /* LRUCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38A00B2225FC2B55006BC0B0 /* LRUCache.swift */; };
-		38A00B2E25FCD582006BC0B0 /* Charts in Frameworks */ = {isa = PBXBuildFile; productRef = 38A00B2D25FCD582006BC0B0 /* Charts */; };
 		38A0363B25ECF07E00FCBB52 /* GlucoseStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38A0363A25ECF07E00FCBB52 /* GlucoseStorage.swift */; };
 		38A0364225ED069400FCBB52 /* TempBasal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38A0364125ED069400FCBB52 /* TempBasal.swift */; };
 		38A13D3225E28B4B00EAA382 /* PumpHistoryEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38A13D3125E28B4B00EAA382 /* PumpHistoryEvent.swift */; };
 		38A504A425DD9C4000C5B9E8 /* UserDefaultsExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38A5049125DD9C4000C5B9E8 /* UserDefaultsExtensions.swift */; };
 		38A9260525F012D8009E3739 /* CarbRatios.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38A9260425F012D8009E3739 /* CarbRatios.swift */; };
 		38AAF85525FFF846004AF583 /* CurrentGlucoseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AAF85425FFF846004AF583 /* CurrentGlucoseView.swift */; };
+		38AAF8712600C1B0004AF583 /* MainChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AAF8702600C1B0004AF583 /* MainChartView.swift */; };
 		38AEE73D25F0200C0013F05B /* FreeAPSSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AEE73C25F0200C0013F05B /* FreeAPSSettings.swift */; };
 		38AEE75225F022080013F05B /* SettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AEE75125F022080013F05B /* SettingsManager.swift */; };
 		38AEE75725F0F18E0013F05B /* CarbsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AEE75625F0F18E0013F05B /* CarbsStorage.swift */; };
@@ -207,31 +209,7 @@
 		5D16287A969E64D18CE40E44 /* PumpConfigViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F60E97100041040446F44E7 /* PumpConfigViewModel.swift */; };
 		63E890B4D951EAA91C071D5C /* BasalProfileEditorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAFF91130F2FCCC7EBBA11AD /* BasalProfileEditorViewModel.swift */; };
 		642F76A05A4FF530463A9FD0 /* NightscoutConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8782B44544F38F2B2D82C38E /* NightscoutConfigRootView.swift */; };
-		6610FA1725FAED29004781D7 /* PointData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6610F9F625FAED29004781D7 /* PointData.swift */; };
-		6610FA1825FAED29004781D7 /* BoundTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6610F9F725FAED29004781D7 /* BoundTypes.swift */; };
-		6610FA1925FAED29004781D7 /* InformationBarEntryData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6610F9F825FAED29004781D7 /* InformationBarEntryData.swift */; };
-		6610FA1A25FAED29004781D7 /* PredictionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6610F9F925FAED29004781D7 /* PredictionType.swift */; };
-		6610FA1B25FAED29004781D7 /* APSDataTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6610F9FA25FAED29004781D7 /* APSDataTypes.swift */; };
-		6610FA1C25FAED29004781D7 /* GlucosePointData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6610F9FB25FAED29004781D7 /* GlucosePointData.swift */; };
-		6610FA1D25FAED29004781D7 /* PredictionLineData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6610F9FC25FAED29004781D7 /* PredictionLineData.swift */; };
-		6610FA1E25FAED29004781D7 /* MeshEntryOrientations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6610F9FD25FAED29004781D7 /* MeshEntryOrientations.swift */; };
-		6610FA1F25FAED29004781D7 /* View+Modifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6610F9FF25FAED29004781D7 /* View+Modifiers.swift */; };
-		6610FA2025FAED29004781D7 /* Double+getBoundGlucose().swift in Sources */ = {isa = PBXBuildFile; fileRef = 6610FA0025FAED29004781D7 /* Double+getBoundGlucose().swift */; };
-		6610FA2125FAED29004781D7 /* Array+halve().swift in Sources */ = {isa = PBXBuildFile; fileRef = 6610FA0125FAED29004781D7 /* Array+halve().swift */; };
-		6610FA2225FAED29004781D7 /* View+if().swift in Sources */ = {isa = PBXBuildFile; fileRef = 6610FA0225FAED29004781D7 /* View+if().swift */; };
-		6610FA2325FAED29004781D7 /* Date+-.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6610FA0325FAED29004781D7 /* Date+-.swift */; };
-		6610FA2425FAED29004781D7 /* PredictionsChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6610FA0625FAED29004781D7 /* PredictionsChartView.swift */; };
-		6610FA2525FAED29004781D7 /* PointChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6610FA0725FAED29004781D7 /* PointChartView.swift */; };
-		6610FA2625FAED29004781D7 /* GlucoseArrowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6610FA0925FAED29004781D7 /* GlucoseArrowView.swift */; };
-		6610FA2725FAED29004781D7 /* GlucoseInformationBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6610FA0A25FAED29004781D7 /* GlucoseInformationBarView.swift */; };
-		6610FA2825FAED29004781D7 /* HoursPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6610FA0B25FAED29004781D7 /* HoursPickerView.swift */; };
-		6610FA2925FAED29004781D7 /* ChartsConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6610FA0C25FAED29004781D7 /* ChartsConfig.swift */; };
-		6610FA2A25FAED29004781D7 /* GlucosePointView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6610FA0E25FAED29004781D7 /* GlucosePointView.swift */; };
-		6610FA2B25FAED29004781D7 /* PredictionPointView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6610FA0F25FAED29004781D7 /* PredictionPointView.swift */; };
-		6610FA2E25FAED29004781D7 /* APSDataFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6610FA1325FAED29004781D7 /* APSDataFormatter.swift */; };
-		6610FA2F25FAED29004781D7 /* getChartWidth().swift in Sources */ = {isa = PBXBuildFile; fileRef = 6610FA1425FAED29004781D7 /* getChartWidth().swift */; };
 		6632A0DC746872439A858B44 /* ISFEditorDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BDA519C9B890FD9A5DFCF3 /* ISFEditorDataFlow.swift */; };
-		66C5083C25FD97FA00E4D76A /* CombinedChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66C5083B25FD97FA00E4D76A /* CombinedChartView.swift */; };
 		69A31254F2451C20361D172F /* BolusViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 223EC0494F55A91E3EA69EF4 /* BolusViewModel.swift */; };
 		69B9A368029F7EB39F525422 /* CREditorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA5E04A2761F6EEA6568E1 /* CREditorViewModel.swift */; };
 		6B9625766B697D1C98E455A2 /* PumpSettingsEditorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72778B68C3004F71F6E79BDC /* PumpSettingsEditorViewModel.swift */; };
@@ -445,7 +423,8 @@
 		388E5A5F25B6F2310019842D /* Autosens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Autosens.swift; sourceTree = "<group>"; };
 		389442CA25F65F7100FA1F27 /* NightscoutTreatment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutTreatment.swift; sourceTree = "<group>"; };
 		3895E4C525B9E00D00214B37 /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
-		389FE32925F3AC44002E92E0 /* GlucoseChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseChartView.swift; sourceTree = "<group>"; };
+		389ECDFD2601061500D86C4F /* View+Snapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Snapshot.swift"; sourceTree = "<group>"; };
+		389ECE042601144100D86C4F /* ConcurrentMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConcurrentMap.swift; sourceTree = "<group>"; };
 		38A00B1E25FC00F7006BC0B0 /* Autotune.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Autotune.swift; sourceTree = "<group>"; };
 		38A00B2225FC2B55006BC0B0 /* LRUCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LRUCache.swift; sourceTree = "<group>"; };
 		38A0363A25ECF07E00FCBB52 /* GlucoseStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseStorage.swift; sourceTree = "<group>"; };
@@ -454,6 +433,7 @@
 		38A5049125DD9C4000C5B9E8 /* UserDefaultsExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDefaultsExtensions.swift; sourceTree = "<group>"; };
 		38A9260425F012D8009E3739 /* CarbRatios.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbRatios.swift; sourceTree = "<group>"; };
 		38AAF85425FFF846004AF583 /* CurrentGlucoseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentGlucoseView.swift; sourceTree = "<group>"; };
+		38AAF8702600C1B0004AF583 /* MainChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainChartView.swift; sourceTree = "<group>"; };
 		38AEE73C25F0200C0013F05B /* FreeAPSSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FreeAPSSettings.swift; sourceTree = "<group>"; };
 		38AEE75125F022080013F05B /* SettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsManager.swift; sourceTree = "<group>"; };
 		38AEE75625F0F18E0013F05B /* CarbsStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbsStorage.swift; sourceTree = "<group>"; };
@@ -504,30 +484,6 @@
 		5F48C3AC770D4CCD0EA2B0C2 /* AddCarbsDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddCarbsDataFlow.swift; sourceTree = "<group>"; };
 		618E62C9757B2F95431B5DC0 /* AddCarbsProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddCarbsProvider.swift; sourceTree = "<group>"; };
 		64AA5E04A2761F6EEA6568E1 /* CREditorViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CREditorViewModel.swift; sourceTree = "<group>"; };
-		6610F9F625FAED29004781D7 /* PointData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PointData.swift; sourceTree = "<group>"; };
-		6610F9F725FAED29004781D7 /* BoundTypes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BoundTypes.swift; sourceTree = "<group>"; };
-		6610F9F825FAED29004781D7 /* InformationBarEntryData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InformationBarEntryData.swift; sourceTree = "<group>"; };
-		6610F9F925FAED29004781D7 /* PredictionType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PredictionType.swift; sourceTree = "<group>"; };
-		6610F9FA25FAED29004781D7 /* APSDataTypes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APSDataTypes.swift; sourceTree = "<group>"; };
-		6610F9FB25FAED29004781D7 /* GlucosePointData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlucosePointData.swift; sourceTree = "<group>"; };
-		6610F9FC25FAED29004781D7 /* PredictionLineData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PredictionLineData.swift; sourceTree = "<group>"; };
-		6610F9FD25FAED29004781D7 /* MeshEntryOrientations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MeshEntryOrientations.swift; sourceTree = "<group>"; };
-		6610F9FF25FAED29004781D7 /* View+Modifiers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "View+Modifiers.swift"; sourceTree = "<group>"; };
-		6610FA0025FAED29004781D7 /* Double+getBoundGlucose().swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Double+getBoundGlucose().swift"; sourceTree = "<group>"; };
-		6610FA0125FAED29004781D7 /* Array+halve().swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Array+halve().swift"; sourceTree = "<group>"; };
-		6610FA0225FAED29004781D7 /* View+if().swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "View+if().swift"; sourceTree = "<group>"; };
-		6610FA0325FAED29004781D7 /* Date+-.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Date+-.swift"; sourceTree = "<group>"; };
-		6610FA0625FAED29004781D7 /* PredictionsChartView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PredictionsChartView.swift; sourceTree = "<group>"; };
-		6610FA0725FAED29004781D7 /* PointChartView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PointChartView.swift; sourceTree = "<group>"; };
-		6610FA0925FAED29004781D7 /* GlucoseArrowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlucoseArrowView.swift; sourceTree = "<group>"; };
-		6610FA0A25FAED29004781D7 /* GlucoseInformationBarView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlucoseInformationBarView.swift; sourceTree = "<group>"; };
-		6610FA0B25FAED29004781D7 /* HoursPickerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HoursPickerView.swift; sourceTree = "<group>"; };
-		6610FA0C25FAED29004781D7 /* ChartsConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartsConfig.swift; sourceTree = "<group>"; };
-		6610FA0E25FAED29004781D7 /* GlucosePointView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlucosePointView.swift; sourceTree = "<group>"; };
-		6610FA0F25FAED29004781D7 /* PredictionPointView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PredictionPointView.swift; sourceTree = "<group>"; };
-		6610FA1325FAED29004781D7 /* APSDataFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APSDataFormatter.swift; sourceTree = "<group>"; };
-		6610FA1425FAED29004781D7 /* getChartWidth().swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "getChartWidth().swift"; sourceTree = "<group>"; };
-		66C5083B25FD97FA00E4D76A /* CombinedChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CombinedChartView.swift; sourceTree = "<group>"; };
 		67F94DD2853CF42BA4E30616 /* BasalProfileEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorDataFlow.swift; sourceTree = "<group>"; };
 		680C4420C9A345D46D90D06C /* ManualTempBasalProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManualTempBasalProvider.swift; sourceTree = "<group>"; };
 		6BA56D2DCAB9E0A8AF24D984 /* BasalProfileEditorBuilder.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorBuilder.swift; sourceTree = "<group>"; };
@@ -582,7 +538,6 @@
 				3811DE1025C9D37700A708ED /* Swinject in Frameworks */,
 				383948D325CD4D6D00E91849 /* Disk in Frameworks */,
 				38887DF825F61F7600944304 /* RileyLinkBLEKit.framework in Frameworks */,
-				38A00B2E25FCD582006BC0B0 /* Charts in Frameworks */,
 				38887DEC25F61F7500944304 /* Crypto.framework in Frameworks */,
 				38B17B6625DD90E0005CAE3D /* SwiftDate in Frameworks */,
 				38887DEA25F61F7500944304 /* MKRingProgressView.framework in Frameworks */,
@@ -590,6 +545,7 @@
 				38887DFA25F61F7600944304 /* RileyLinkKit.framework in Frameworks */,
 				38887DF625F61F7600944304 /* OmniKitUI.framework in Frameworks */,
 				38887DE425F61F7500944304 /* LoopTestingKit.framework in Frameworks */,
+				3833B46D26012030003021B3 /* Algorithms in Frameworks */,
 				38887DFC25F61F7600944304 /* RileyLinkKitUI.framework in Frameworks */,
 				38887DEE25F61F7500944304 /* MinimedKit.framework in Frameworks */,
 				38887DE025F61F7500944304 /* LoopKit.framework in Frameworks */,
@@ -683,7 +639,6 @@
 				3811DEDE25C9E2DD00A708ED /* Application */,
 				3811DF0A25CAAAA500A708ED /* APS */,
 				38E98A3225F5300800C0CED0 /* Config */,
-				6610F9F125FAED29004781D7 /* Charts */,
 				3811DEBD25C9D99900A708ED /* Containers */,
 				388E5A5A25B6F05F0019842D /* Helpers */,
 				38E98A1A25F52C9300C0CED0 /* Logger */,
@@ -741,9 +696,9 @@
 			isa = PBXGroup;
 			children = (
 				3811DE2E25C9D49500A708ED /* HomeRootView.swift */,
-				389FE32925F3AC44002E92E0 /* GlucoseChartView.swift */,
 				383420D525FFE38C002D46C1 /* LoopView.swift */,
 				38AAF85425FFF846004AF583 /* CurrentGlucoseView.swift */,
+				38AAF8702600C1B0004AF583 /* MainChartView.swift */,
 			);
 			path = View;
 			sourceTree = "<group>";
@@ -989,6 +944,7 @@
 				3811DE5925C9D4D500A708ED /* ViewModifiers.swift */,
 				3883581B25EE79BB00E024B2 /* DecimalTextField.swift */,
 				383420D825FFEB3F002D46C1 /* Popup.swift */,
+				389ECDFD2601061500D86C4F /* View+Snapshot.swift */,
 			);
 			path = Views;
 			sourceTree = "<group>";
@@ -1080,6 +1036,7 @@
 				3811DE5525C9D4D500A708ED /* Publisher.swift */,
 				38E98A3625F5509500C0CED0 /* String+Extensions.swift */,
 				3811DEE325CA063400A708ED /* PropertyWrappers */,
+				389ECE042601144100D86C4F /* ConcurrentMap.swift */,
 			);
 			path = Helpers;
 			sourceTree = "<group>";
@@ -1295,93 +1252,6 @@
 			path = TargetsEditor;
 			sourceTree = "<group>";
 		};
-		6610F9F125FAED29004781D7 /* Charts */ = {
-			isa = PBXGroup;
-			children = (
-				6610F9F425FAED29004781D7 /* Models */,
-				6610F9FE25FAED29004781D7 /* Extensions */,
-				6610FA0425FAED29004781D7 /* Views */,
-				6610FA1025FAED29004781D7 /* Helpers */,
-			);
-			path = Charts;
-			sourceTree = "<group>";
-		};
-		6610F9F425FAED29004781D7 /* Models */ = {
-			isa = PBXGroup;
-			children = (
-				6610F9F625FAED29004781D7 /* PointData.swift */,
-				6610F9F725FAED29004781D7 /* BoundTypes.swift */,
-				6610F9F825FAED29004781D7 /* InformationBarEntryData.swift */,
-				6610F9F925FAED29004781D7 /* PredictionType.swift */,
-				6610F9FA25FAED29004781D7 /* APSDataTypes.swift */,
-				6610F9FB25FAED29004781D7 /* GlucosePointData.swift */,
-				6610F9FC25FAED29004781D7 /* PredictionLineData.swift */,
-				6610F9FD25FAED29004781D7 /* MeshEntryOrientations.swift */,
-			);
-			path = Models;
-			sourceTree = "<group>";
-		};
-		6610F9FE25FAED29004781D7 /* Extensions */ = {
-			isa = PBXGroup;
-			children = (
-				6610F9FF25FAED29004781D7 /* View+Modifiers.swift */,
-				6610FA0025FAED29004781D7 /* Double+getBoundGlucose().swift */,
-				6610FA0125FAED29004781D7 /* Array+halve().swift */,
-				6610FA0225FAED29004781D7 /* View+if().swift */,
-				6610FA0325FAED29004781D7 /* Date+-.swift */,
-			);
-			path = Extensions;
-			sourceTree = "<group>";
-		};
-		6610FA0425FAED29004781D7 /* Views */ = {
-			isa = PBXGroup;
-			children = (
-				6610FA0525FAED29004781D7 /* Charts */,
-				6610FA0825FAED29004781D7 /* Components */,
-				6610FA0D25FAED29004781D7 /* Points */,
-				6610FA0C25FAED29004781D7 /* ChartsConfig.swift */,
-			);
-			path = Views;
-			sourceTree = "<group>";
-		};
-		6610FA0525FAED29004781D7 /* Charts */ = {
-			isa = PBXGroup;
-			children = (
-				6610FA0625FAED29004781D7 /* PredictionsChartView.swift */,
-				6610FA0725FAED29004781D7 /* PointChartView.swift */,
-				66C5083B25FD97FA00E4D76A /* CombinedChartView.swift */,
-			);
-			path = Charts;
-			sourceTree = "<group>";
-		};
-		6610FA0825FAED29004781D7 /* Components */ = {
-			isa = PBXGroup;
-			children = (
-				6610FA0925FAED29004781D7 /* GlucoseArrowView.swift */,
-				6610FA0A25FAED29004781D7 /* GlucoseInformationBarView.swift */,
-				6610FA0B25FAED29004781D7 /* HoursPickerView.swift */,
-			);
-			path = Components;
-			sourceTree = "<group>";
-		};
-		6610FA0D25FAED29004781D7 /* Points */ = {
-			isa = PBXGroup;
-			children = (
-				6610FA0E25FAED29004781D7 /* GlucosePointView.swift */,
-				6610FA0F25FAED29004781D7 /* PredictionPointView.swift */,
-			);
-			path = Points;
-			sourceTree = "<group>";
-		};
-		6610FA1025FAED29004781D7 /* Helpers */ = {
-			isa = PBXGroup;
-			children = (
-				6610FA1325FAED29004781D7 /* APSDataFormatter.swift */,
-				6610FA1425FAED29004781D7 /* getChartWidth().swift */,
-			);
-			path = Helpers;
-			sourceTree = "<group>";
-		};
 		672F63EEAE27400625E14BAD /* AutotuneConfig */ = {
 			isa = PBXGroup;
 			children = (
@@ -1556,7 +1426,7 @@
 				3811DE0F25C9D37700A708ED /* Swinject */,
 				383948D225CD4D6D00E91849 /* Disk */,
 				38B17B6525DD90E0005CAE3D /* SwiftDate */,
-				38A00B2D25FCD582006BC0B0 /* Charts */,
+				3833B46C26012030003021B3 /* Algorithms */,
 			);
 			productName = FreeAPS;
 			productReference = 388E595825AD948C0019842D /* FreeAPS.app */;
@@ -1611,7 +1481,7 @@
 				3811DE0E25C9D37700A708ED /* XCRemoteSwiftPackageReference "Swinject" */,
 				383948D125CD4D6D00E91849 /* XCRemoteSwiftPackageReference "Disk" */,
 				38B17B6425DD90E0005CAE3D /* XCRemoteSwiftPackageReference "SwiftDate" */,
-				38A00B2C25FCD581006BC0B0 /* XCRemoteSwiftPackageReference "Charts" */,
+				3833B46B26012030003021B3 /* XCRemoteSwiftPackageReference "swift-algorithms" */,
 			);
 			productRefGroup = 388E595925AD948C0019842D /* Products */;
 			projectDirPath = "";
@@ -1673,12 +1543,9 @@
 				3811DEEB25CA063400A708ED /* PersistedProperty.swift in Sources */,
 				388E5A6025B6F2310019842D /* Autosens.swift in Sources */,
 				3811DE8B25C9D6DD00A708ED /* RequestPermissionsBuilder.swift in Sources */,
-				6610FA1725FAED29004781D7 /* PointData.swift in Sources */,
 				3811DE4C25C9D4B800A708ED /* AuthotizedRootBuilder.swift in Sources */,
 				3811DE8F25C9D80400A708ED /* User.swift in Sources */,
-				6610FA2325FAED29004781D7 /* Date+-.swift in Sources */,
 				3811DEB225C9D88300A708ED /* KeychainItemAccessibility.swift in Sources */,
-				6610FA1825FAED29004781D7 /* BoundTypes.swift in Sources */,
 				385CEAC425F2F154002D6D5B /* AnnouncementsStorage.swift in Sources */,
 				38AEE73D25F0200C0013F05B /* FreeAPSSettings.swift in Sources */,
 				38FCF3FD25E997A80078B0D1 /* PumpHistoryStorage.swift in Sources */,
@@ -1693,9 +1560,6 @@
 				38AEE75225F022080013F05B /* SettingsManager.swift in Sources */,
 				38B4F3CF25E5041600E76A18 /* APSContainer.swift in Sources */,
 				3895E4C625B9E00D00214B37 /* Preferences.swift in Sources */,
-				6610FA2925FAED29004781D7 /* ChartsConfig.swift in Sources */,
-				6610FA1925FAED29004781D7 /* InformationBarEntryData.swift in Sources */,
-				6610FA2625FAED29004781D7 /* GlucoseArrowView.swift in Sources */,
 				3811DE6E25C9D62600A708ED /* OnboardingViewModel.swift in Sources */,
 				38B4F3CD25E5031100E76A18 /* Broadcaster.swift in Sources */,
 				3811DE6D25C9D62600A708ED /* OnboardingRootView.swift in Sources */,
@@ -1705,7 +1569,6 @@
 				38BF021725E7CBBC00579895 /* PumpManagerExtensions.swift in Sources */,
 				38F3B2EF25ED8E2A005C48AA /* TempTargetsStorage.swift in Sources */,
 				3811DF1025CAAAE200A708ED /* APSManager.swift in Sources */,
-				6610FA2725FAED29004781D7 /* GlucoseInformationBarView.swift in Sources */,
 				3870FF4725EC187A0088248F /* BloodGlucose.swift in Sources */,
 				3811DE0A25C9D32F00A708ED /* BaseModuleBuilder.swift in Sources */,
 				38A0364225ED069400FCBB52 /* TempBasal.swift in Sources */,
@@ -1719,12 +1582,9 @@
 				382C133725F13A1E00715CE1 /* InsulinSensitivities.swift in Sources */,
 				3811DE7B25C9D6D300A708ED /* LoginProvider.swift in Sources */,
 				383948D625CD4D8900E91849 /* FileStorage.swift in Sources */,
-				389FE32A25F3AC44002E92E0 /* GlucoseChartView.swift in Sources */,
 				3811DE4125C9D4A100A708ED /* SettingsRootView.swift in Sources */,
 				388E595C25AD948C0019842D /* FreeAPSApp.swift in Sources */,
-				6610FA2A25FAED29004781D7 /* GlucosePointView.swift in Sources */,
 				3811DE8925C9D6DD00A708ED /* RequestPermissionsProvider.swift in Sources */,
-				6610FA1D25FAED29004781D7 /* PredictionLineData.swift in Sources */,
 				3811DE4225C9D4A100A708ED /* SettingsDataFlow.swift in Sources */,
 				3811DE8825C9D6DD00A708ED /* RequestPermissionsDataFlow.swift in Sources */,
 				3811DE8A25C9D6DD00A708ED /* RequestPermissionsRootView.swift in Sources */,
@@ -1744,7 +1604,6 @@
 				38FE826D25CC8461001FF17A /* NightscoutAPI.swift in Sources */,
 				3811DE4E25C9D4B800A708ED /* AuthotizedRootRootView.swift in Sources */,
 				3811DE7D25C9D6D300A708ED /* LoginRootView.swift in Sources */,
-				6610FA2B25FAED29004781D7 /* PredictionPointView.swift in Sources */,
 				3811DE4025C9D4A100A708ED /* SettingsBuilder.swift in Sources */,
 				388358C825EEF6D200E024B2 /* BasalProfileEntry.swift in Sources */,
 				3811DE0B25C9D32F00A708ED /* BaseView.swift in Sources */,
@@ -1755,17 +1614,14 @@
 				3811DE5D25C9D4D500A708ED /* Publisher.swift in Sources */,
 				38B4F3AF25E2979F00E76A18 /* IndexedCollection.swift in Sources */,
 				3811DEAE25C9D88300A708ED /* Cache.swift in Sources */,
-				6610FA2425FAED29004781D7 /* PredictionsChartView.swift in Sources */,
 				383420D625FFE38C002D46C1 /* LoopView.swift in Sources */,
 				3811DEAD25C9D88300A708ED /* UserDefaults+Cache.swift in Sources */,
 				3811DE2225C9D48300A708ED /* MainProvider.swift in Sources */,
 				3811DE0C25C9D32F00A708ED /* BaseProvider.swift in Sources */,
-				6610FA1A25FAED29004781D7 /* PredictionType.swift in Sources */,
 				3811DE5C25C9D4D500A708ED /* Formatters.swift in Sources */,
 				3811DEC525C9D99900A708ED /* StorageContainer.swift in Sources */,
 				3871F39F25ED895A0013ECB5 /* Decimal+Extensions.swift in Sources */,
 				3811DE7F25C9D6D300A708ED /* LoginBuilder.swift in Sources */,
-				6610FA2125FAED29004781D7 /* Array+halve().swift in Sources */,
 				3811DE3525C9D49500A708ED /* HomeRootView.swift in Sources */,
 				3811DEC325C9D99900A708ED /* UIContainer.swift in Sources */,
 				38E98A2925F52C9300C0CED0 /* Error+Extensions.swift in Sources */,
@@ -1780,7 +1636,6 @@
 				3811DE7925C9D6D300A708ED /* LoginViewModel.swift in Sources */,
 				3871F39C25ED892B0013ECB5 /* TempTarget.swift in Sources */,
 				3811DEAB25C9D88300A708ED /* HTTPResponseStatus.swift in Sources */,
-				6610FA1F25FAED29004781D7 /* View+Modifiers.swift in Sources */,
 				3811DE5F25C9D4D500A708ED /* ProgressBar.swift in Sources */,
 				38BF021D25E7E3AF00579895 /* Reservoir.swift in Sources */,
 				38BF021B25E7D06400579895 /* PumpSettingsView.swift in Sources */,
@@ -1798,7 +1653,6 @@
 				3811DEB725C9D88300A708ED /* AuthorizationManager.swift in Sources */,
 				38E989DD25F5021400C0CED0 /* PumpStatus.swift in Sources */,
 				38E98A2525F52C9300C0CED0 /* IssueReporter.swift in Sources */,
-				6610FA2F25FAED29004781D7 /* getChartWidth().swift in Sources */,
 				3811DF0825CAAA4700A708ED /* ServiceContainer.swift in Sources */,
 				3811DEB025C9D88300A708ED /* BaseKeychain.swift in Sources */,
 				3811DE4D25C9D4B800A708ED /* AuthotizedRootViewModel.swift in Sources */,
@@ -1806,11 +1660,10 @@
 				3811DEC425C9D99900A708ED /* NetworkContainer.swift in Sources */,
 				3811DE4325C9D4A100A708ED /* SettingsProvider.swift in Sources */,
 				E102DE9C3E9C8AEDCB3C61BB /* ConfigEditorBuilder.swift in Sources */,
-				6610FA1B25FAED29004781D7 /* APSDataTypes.swift in Sources */,
 				45252C95D220E796FDB3B022 /* ConfigEditorDataFlow.swift in Sources */,
 				3871F38725ED661C0013ECB5 /* Suggestion.swift in Sources */,
-				6610FA2E25FAED29004781D7 /* APSDataFormatter.swift in Sources */,
 				38C4D33A25E9A1ED00D30B77 /* NSObject+AssociatedValues.swift in Sources */,
+				38AAF8712600C1B0004AF583 /* MainChartView.swift in Sources */,
 				72F1BD388F42FCA6C52E4500 /* ConfigEditorProvider.swift in Sources */,
 				E39E418C56A5A46B61D960EE /* ConfigEditorViewModel.swift in Sources */,
 				45717281F743594AA9D87191 /* ConfigEditorRootView.swift in Sources */,
@@ -1830,6 +1683,7 @@
 				2BE9A6FA20875F6F4F9CD461 /* PumpSettingsEditorProvider.swift in Sources */,
 				6B9625766B697D1C98E455A2 /* PumpSettingsEditorViewModel.swift in Sources */,
 				A0B8EC8CC5CD1DD237D1BCD2 /* PumpSettingsEditorRootView.swift in Sources */,
+				389ECDFE2601061500D86C4F /* View+Snapshot.swift in Sources */,
 				38E98A3725F5509500C0CED0 /* String+Extensions.swift in Sources */,
 				1D086541F369D339A74893AC /* BasalProfileEditorBuilder.swift in Sources */,
 				385CEAC125F2EA52002D6D5B /* Announcement.swift in Sources */,
@@ -1842,7 +1696,6 @@
 				38E98A2425F52C9300C0CED0 /* Logger.swift in Sources */,
 				CA370FC152BC98B3D1832968 /* BasalProfileEditorRootView.swift in Sources */,
 				F215CAB49BA4B5A01C3BC6B6 /* ISFEditorBuilder.swift in Sources */,
-				6610FA2025FAED29004781D7 /* Double+getBoundGlucose().swift in Sources */,
 				6632A0DC746872439A858B44 /* ISFEditorDataFlow.swift in Sources */,
 				DBA5254DBB2586C98F61220C /* ISFEditorProvider.swift in Sources */,
 				1BBB001DAD60F3B8CEA4B1C7 /* ISFEditorViewModel.swift in Sources */,
@@ -1851,28 +1704,25 @@
 				A33352ED40476125EBAC6EE0 /* CREditorDataFlow.swift in Sources */,
 				17A9D0899046B45E87834820 /* CREditorProvider.swift in Sources */,
 				69B9A368029F7EB39F525422 /* CREditorViewModel.swift in Sources */,
-				6610FA1C25FAED29004781D7 /* GlucosePointData.swift in Sources */,
 				98641AF4F92123DA668AB931 /* CREditorRootView.swift in Sources */,
 				7F7017AA5C69838FB7E6FECE /* TargetsEditorBuilder.swift in Sources */,
 				38E98A2325F52C9300C0CED0 /* Signpost.swift in Sources */,
-				6610FA2825FAED29004781D7 /* HoursPickerView.swift in Sources */,
 				F5F7E6C1B7F098F59EB67EC5 /* TargetsEditorDataFlow.swift in Sources */,
 				5075C1608E6249A51495C422 /* TargetsEditorProvider.swift in Sources */,
 				E13B7DAB2A435F57066AF02E /* TargetsEditorViewModel.swift in Sources */,
 				9702FF92A09C53942F20D7EA /* TargetsEditorRootView.swift in Sources */,
 				97C1388354C7133C1D5ED72A /* PreferencesEditorBuilder.swift in Sources */,
 				A228DF96647338139F152B15 /* PreferencesEditorDataFlow.swift in Sources */,
+				389ECE052601144100D86C4F /* ConcurrentMap.swift in Sources */,
 				E4984C5262A90469788754BB /* PreferencesEditorProvider.swift in Sources */,
 				DD399FB31EACB9343C944C4C /* PreferencesEditorViewModel.swift in Sources */,
 				44190F0BBA464D74B857D1FB /* PreferencesEditorRootView.swift in Sources */,
 				460745235E45CA6311C98613 /* AddCarbsBuilder.swift in Sources */,
 				E97285ED9B814CD5253C6658 /* AddCarbsDataFlow.swift in Sources */,
-				6610FA2525FAED29004781D7 /* PointChartView.swift in Sources */,
 				A6F097A14CAAE0CE0D11BE1B /* AddCarbsProvider.swift in Sources */,
 				33E198D3039045D98C3DC5D4 /* AddCarbsViewModel.swift in Sources */,
 				28089E07169488CF6DCC2A31 /* AddCarbsRootView.swift in Sources */,
 				46159F33C01CDBF822B4F1F8 /* AddTempTargetBuilder.swift in Sources */,
-				6610FA2225FAED29004781D7 /* View+if().swift in Sources */,
 				D2165E9D78EFF692C1DED1C6 /* AddTempTargetDataFlow.swift in Sources */,
 				5BFA1C2208114643B77F8CEB /* AddTempTargetProvider.swift in Sources */,
 				919DBD08F13BAFB180DF6F47 /* AddTempTargetViewModel.swift in Sources */,
@@ -1881,7 +1731,6 @@
 				19434C14DF3F4816F4E4BF2E /* BolusBuilder.swift in Sources */,
 				38AAF85525FFF846004AF583 /* CurrentGlucoseView.swift in Sources */,
 				041D1E995A6AE92E9289DC49 /* BolusDataFlow.swift in Sources */,
-				6610FA1E25FAED29004781D7 /* MeshEntryOrientations.swift in Sources */,
 				23888883D4EA091C88480FF2 /* BolusProvider.swift in Sources */,
 				38E98A2D25F52DC400C0CED0 /* NSLocking+Extensions.swift in Sources */,
 				69A31254F2451C20361D172F /* BolusViewModel.swift in Sources */,
@@ -1891,7 +1740,6 @@
 				BF1667ADE69E4B5B111CECAE /* ManualTempBasalProvider.swift in Sources */,
 				C967DACD3B1E638F8B43BE06 /* ManualTempBasalViewModel.swift in Sources */,
 				7BCFACB97C821041BA43A114 /* ManualTempBasalRootView.swift in Sources */,
-				66C5083C25FD97FA00E4D76A /* CombinedChartView.swift in Sources */,
 				38A00B2325FC2B55006BC0B0 /* LRUCache.swift in Sources */,
 				91732A8060347C0E67024D80 /* AutotuneConfigBuilder.swift in Sources */,
 				3083261C4B268E353F36CD0B /* AutotuneConfigDataFlow.swift in Sources */,
@@ -2165,20 +2013,20 @@
 				minimumVersion = 2.7.1;
 			};
 		};
-		383948D125CD4D6D00E91849 /* XCRemoteSwiftPackageReference "Disk" */ = {
+		3833B46B26012030003021B3 /* XCRemoteSwiftPackageReference "swift-algorithms" */ = {
 			isa = XCRemoteSwiftPackageReference;
-			repositoryURL = "https://github.com/saoudrizwan/Disk";
+			repositoryURL = "https://github.com/apple/swift-algorithms";
 			requirement = {
 				kind = upToNextMajorVersion;
-				minimumVersion = 0.6.4;
+				minimumVersion = 0.0.3;
 			};
 		};
-		38A00B2C25FCD581006BC0B0 /* XCRemoteSwiftPackageReference "Charts" */ = {
+		383948D125CD4D6D00E91849 /* XCRemoteSwiftPackageReference "Disk" */ = {
 			isa = XCRemoteSwiftPackageReference;
-			repositoryURL = "https://github.com/danielgindi/Charts";
+			repositoryURL = "https://github.com/saoudrizwan/Disk";
 			requirement = {
 				kind = upToNextMajorVersion;
-				minimumVersion = 4.0.1;
+				minimumVersion = 0.6.4;
 			};
 		};
 		38B17B6425DD90E0005CAE3D /* XCRemoteSwiftPackageReference "SwiftDate" */ = {
@@ -2197,16 +2045,16 @@
 			package = 3811DE0E25C9D37700A708ED /* XCRemoteSwiftPackageReference "Swinject" */;
 			productName = Swinject;
 		};
+		3833B46C26012030003021B3 /* Algorithms */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 3833B46B26012030003021B3 /* XCRemoteSwiftPackageReference "swift-algorithms" */;
+			productName = Algorithms;
+		};
 		383948D225CD4D6D00E91849 /* Disk */ = {
 			isa = XCSwiftPackageProductDependency;
 			package = 383948D125CD4D6D00E91849 /* XCRemoteSwiftPackageReference "Disk" */;
 			productName = Disk;
 		};
-		38A00B2D25FCD582006BC0B0 /* Charts */ = {
-			isa = XCSwiftPackageProductDependency;
-			package = 38A00B2C25FCD581006BC0B0 /* XCRemoteSwiftPackageReference "Charts" */;
-			productName = Charts;
-		};
 		38B17B6525DD90E0005CAE3D /* SwiftDate */ = {
 			isa = XCSwiftPackageProductDependency;
 			package = 38B17B6425DD90E0005CAE3D /* XCRemoteSwiftPackageReference "SwiftDate" */;

+ 0 - 9
FreeAPS.xcworkspace/xcshareddata/swiftpm/Package.resolved

@@ -2,15 +2,6 @@
   "object": {
     "pins": [
       {
-        "package": "Charts",
-        "repositoryURL": "https://github.com/danielgindi/Charts",
-        "state": {
-          "branch": null,
-          "revision": "8d134a78bb2cb9e7a0f05ecbd5880bb05cd02863",
-          "version": "4.0.1"
-        }
-      },
-      {
         "package": "Disk",
         "repositoryURL": "https://github.com/saoudrizwan/Disk",
         "state": {

+ 0 - 10
FreeAPS/Sources/Charts/Extensions/Array+halve().swift

@@ -1,10 +0,0 @@
-import Foundation
-
-extension Array {
-    func halve() -> [[Element]] {
-        let half = count / 2
-        let leftSplit = self[0 ..< half]
-        let rightSplit = self[half...]
-        return [Array(leftSplit), Array(rightSplit)]
-    }
-}

+ 0 - 7
FreeAPS/Sources/Charts/Extensions/Date+-.swift

@@ -1,7 +0,0 @@
-import Foundation
-
-extension Date {
-    static func - (lhs: Date, rhs: Date) -> Double {
-        lhs.timeIntervalSinceReferenceDate - rhs.timeIntervalSinceReferenceDate
-    }
-}

+ 0 - 13
FreeAPS/Sources/Charts/Extensions/Double+getBoundGlucose().swift

@@ -1,13 +0,0 @@
-import Foundation
-
-extension Array where Element: Comparable {
-    func getBoundGlucose(boundType: BoundTypes, bound: Element) -> Element {
-        guard let extremum = (boundType == .top) ? max() : min() else {
-            return bound
-        }
-        if (boundType == .top) ? extremum < bound : extremum > bound {
-            return bound
-        }
-        return extremum
-    }
-}

+ 0 - 16
FreeAPS/Sources/Charts/Extensions/View+Modifiers.swift

@@ -1,16 +0,0 @@
-import SwiftUI
-
-private struct InformationBarEntryModifier: ViewModifier {
-    func body(content: Content) -> some View {
-        content
-            .frame(maxWidth: .infinity, maxHeight: .infinity)
-            .background(Color(.systemGray6))
-            .cornerRadius(10)
-    }
-}
-
-extension View {
-    func informationBarEntryStyle() -> some View {
-        modifier(InformationBarEntryModifier())
-    }
-}

+ 0 - 14
FreeAPS/Sources/Charts/Extensions/View+if().swift

@@ -1,14 +0,0 @@
-import SwiftUI
-
-extension View {
-    @ViewBuilder func `if`<Transform: View>(
-        _ condition: Bool,
-        transform: (Self) -> Transform
-    ) -> some View {
-        if condition {
-            transform(self)
-        } else {
-            self
-        }
-    }
-}

+ 0 - 43
FreeAPS/Sources/Charts/Helpers/APSDataFormatter.swift

@@ -1,43 +0,0 @@
-import Foundation
-
-enum APSDataFormatter {
-    private static let numberFormatter: NumberFormatter = {
-        let f = NumberFormatter()
-        f.numberStyle = .decimal
-        return f
-    }()
-
-    private static func formatToPrecision(_ number: Double, precision: Int, minimumFraction: Int = 1) -> String {
-        numberFormatter.minimumFractionDigits = minimumFraction
-        numberFormatter.maximumFractionDigits = precision
-        return numberFormatter.string(from: NSNumber(value: number))!
-    }
-
-    private static func formatTime(difference: Double) -> String {
-        let rawHours = Int(round(difference / 3600))
-        let days = rawHours / 24
-        let hours = rawHours - (24 * days)
-        return ("\(days)d\(hours)h")
-    }
-
-    static func format(inputValue: Double, to formatType: APSDataTypes) -> String {
-        switch formatType {
-        case .delta:
-            let formattedDelta = formatToPrecision(inputValue, precision: 2)
-            if inputValue >= 0 {
-                return "+" + formattedDelta
-            }
-            return formattedDelta
-        case .glucose:
-            return formatToPrecision(inputValue, precision: 1)
-        case .cob:
-            return formatToPrecision(inputValue, precision: 1) + "g"
-        case .iob:
-            return formatToPrecision(inputValue, precision: 2) + "U"
-        case .basal:
-            return formatToPrecision(inputValue, precision: 2, minimumFraction: 2) + "U"
-        case .time:
-            return formatTime(difference: inputValue)
-        }
-    }
-}

+ 0 - 5
FreeAPS/Sources/Charts/Helpers/getChartWidth().swift

@@ -1,5 +0,0 @@
-import SwiftUI
-
-func getChartWidth(for amount: Int, width: CGFloat, showHours: Int) -> CGFloat {
-    CGFloat(amount) * width / CGFloat(Double(showHours) * 12) + 2.5
-}

+ 0 - 10
FreeAPS/Sources/Charts/Models/APSDataTypes.swift

@@ -1,10 +0,0 @@
-import Foundation
-
-enum APSDataTypes {
-    case delta
-    case glucose
-    case cob
-    case iob
-    case basal
-    case time
-}

+ 0 - 6
FreeAPS/Sources/Charts/Models/BoundTypes.swift

@@ -1,6 +0,0 @@
-import Foundation
-
-enum BoundTypes {
-    case top
-    case bottom
-}

+ 0 - 8
FreeAPS/Sources/Charts/Models/GlucosePointData.swift

@@ -1,8 +0,0 @@
-import SwiftUI
-
-struct GlucosePointData: PointData {
-    var id = UUID()
-    let value: Int?
-    let xPosition: CGFloat
-    let yPosition: CGFloat?
-}

+ 0 - 8
FreeAPS/Sources/Charts/Models/InformationBarEntryData.swift

@@ -1,8 +0,0 @@
-import Foundation
-
-struct InformationBarEntryData: Identifiable, Hashable {
-    var id = UUID()
-    let label: String
-    let value: Double
-    let type: APSDataTypes
-}

+ 0 - 6
FreeAPS/Sources/Charts/Models/MeshEntryOrientations.swift

@@ -1,6 +0,0 @@
-import Foundation
-
-enum MeshEntryOrientations {
-    case vertical
-    case horizontal
-}

+ 0 - 8
FreeAPS/Sources/Charts/Models/PointData.swift

@@ -1,8 +0,0 @@
-import SwiftUI
-
-protocol PointData: Identifiable, Hashable {
-    var id: UUID { get }
-    var value: Int? { get }
-    var xPosition: CGFloat { get }
-    var yPosition: CGFloat? { get }
-}

+ 0 - 7
FreeAPS/Sources/Charts/Models/PredictionLineData.swift

@@ -1,7 +0,0 @@
-import Foundation
-
-struct PredictionLineData: Identifiable, Hashable {
-    var id = UUID()
-    let type: PredictionType
-    var values: [BloodGlucose]
-}

+ 0 - 8
FreeAPS/Sources/Charts/Models/PredictionType.swift

@@ -1,8 +0,0 @@
-import SwiftUI
-
-enum PredictionType: String {
-    case iob
-    case cob
-    case zt
-    case uam
-}

+ 0 - 81
FreeAPS/Sources/Charts/Views/Charts/CombinedChartView.swift

@@ -1,81 +0,0 @@
-import SwiftUI
-
-struct CombinedChartView: View {
-    let maxWidth: CGFloat
-    let showHours: Int
-    @Binding var glucoseData: [BloodGlucose]
-    @Binding var predictionsData: [PredictionLineData]
-    let mode: PointChartViewMode
-
-    var body: some View {
-        let allValues = getAllValues()
-        let minValue = allValues.min() ?? 40
-        let maxValue = allValues.max() ?? 400
-
-        return HStack {
-            PointChartView(
-                minValue: minValue,
-                maxValue: maxValue,
-                maxWidth: maxWidth,
-                mode: mode,
-                showHours: showHours,
-                glucoseData: $glucoseData
-            ) { value in
-                GlucosePointView(value: value)
-            }
-            PredictionsChartView(
-                minValue: minValue,
-                maxValue: maxValue,
-                maxWidth: maxWidth,
-                data: $predictionsData,
-                showHours: showHours
-            )
-        }
-    }
-}
-
-extension CombinedChartView {
-    func getAllValues() -> [Int] {
-        let glucoseValues = glucoseData.compactMap(\.sgv)
-        guard let predictionValues = getPredictionValues() else {
-            return glucoseValues
-        }
-        return glucoseValues + predictionValues
-    }
-
-    func getPredictionValues() -> [Int]? {
-        guard !predictionsData.isEmpty else {
-            return nil
-        }
-        return predictionsData.flatMap { prediction in
-            prediction.values.compactMap(\.sgv)
-        }
-    }
-}
-
-// struct MainChartView_Previews: PreviewProvider {
-//    static let glucoseData = Array(SampleData.sampleData[0 ... 70])
-//
-//    static let predictionsData = [
-//        PredictionLineData(
-//            type: .iob,
-//            values: Array(SampleData.sampleData[0 ... 10])
-//        ),
-//
-//        PredictionLineData(type: .cob, values: Array(SampleData.sampleData[1 ... 21])),
-//
-//        PredictionLineData(
-//            type: .uam,
-//            values: Array(SampleData.sampleData[21 ... 30])
-//        ),
-//
-//        PredictionLineData(type: .zt, values: Array(SampleData.sampleData[31 ... 40]))
-//    ]
-//
-//    static var previews: some View {
-//        ScrollView(.horizontal) {
-//            MainChartView(maxWidth: 400, showHours: 1, glucoseData: glucoseData, predictionsData: predictionsData)
-//        }
-//        .preferredColorScheme(/*@START_MENU_TOKEN@*/ .dark/*@END_MENU_TOKEN@*/)
-//    }
-// }

+ 0 - 97
FreeAPS/Sources/Charts/Views/Charts/PointChartView.swift

@@ -1,97 +0,0 @@
-import SwiftUI
-
-enum PointChartViewMode {
-    case line
-    case dots
-}
-
-struct PointChartView<PointEntry: View>: View {
-    let minValue: Int
-    let maxValue: Int
-    let maxWidth: CGFloat
-    let mode: PointChartViewMode
-
-    let showHours: Int
-    @Binding var glucoseData: [BloodGlucose]
-
-    let pointEntry: (_: Int?) -> PointEntry
-
-    let hoursMultiplier: Double = 12
-    let pointSize: CGFloat = ChartsConfig.glucosePointSize / 2
-
-    var body: some View {
-        let firstEntryTime = glucoseData
-            .map(\.dateString)
-            .first ?? Date()
-
-        var width: CGFloat = 0
-        if let lastGlucose = glucoseData.last {
-            width = calculateXPosition(glucose: lastGlucose, firstEntryTime: firstEntryTime)
-        }
-
-        return GeometryReader { geometry in
-            switch mode {
-            case .line:
-                Path { path in
-                    let points = getGlucosePoints(
-                        height: geometry.size.height, firstEntryTime: firstEntryTime
-                    )
-
-                    for point in points {
-                        if point == points.first {
-                            path.move(to: CGPoint(x: point.xPosition, y: point.yPosition ?? 0))
-                        } else {
-                            path.addLine(to: CGPoint(x: point.xPosition, y: point.yPosition ?? 0))
-                        }
-                    }
-                }.stroke(Color.blue, lineWidth: 1)
-            case .dots:
-                ForEach(
-                    getGlucosePoints(
-                        height: geometry.size.height, firstEntryTime: firstEntryTime
-                    ),
-                    id: \.self
-                ) { point in
-                    pointEntry(point.value)
-                        .position(x: point.xPosition, y: point.yPosition ?? 0)
-                }
-            }
-        }
-        .frame(width: width + pointSize)
-    }
-}
-
-extension PointChartView {
-    func calculateXPosition(glucose: BloodGlucose, firstEntryTime: Date) -> CGFloat {
-        let xPositionIndex = CGFloat(DateInterval(start: firstEntryTime, end: glucose.dateString).duration) /
-            CGFloat(300)
-        return (xPositionIndex * maxWidth / CGFloat(Double(showHours) * hoursMultiplier)) + pointSize
-    }
-
-    func getGlucosePoints(
-        height: CGFloat,
-        firstEntryTime: Date
-    ) -> [GlucosePointData] {
-        /// y = mx + b where m = scalingFactor, b = addendum, x = value, y = mapped value
-        let scalingFactor = Double(height - pointSize * 2) / Double(maxValue - minValue)
-        let addendum = scalingFactor * Double(maxValue)
-
-        return glucoseData.map { glucose in
-
-            let xPosition = calculateXPosition(glucose: glucose, firstEntryTime: firstEntryTime)
-
-            guard let value = glucose.sgv else {
-                return GlucosePointData(
-                    value: nil,
-                    xPosition: xPosition,
-                    yPosition: nil
-                )
-            }
-            return GlucosePointData(
-                value: value,
-                xPosition: xPosition,
-                yPosition: CGFloat(-scalingFactor * Double(value) + addendum) + pointSize
-            )
-        }
-    }
-}

+ 0 - 36
FreeAPS/Sources/Charts/Views/Charts/PredictionsChartView.swift

@@ -1,36 +0,0 @@
-import SwiftUI
-
-struct PredictionsChartView: View {
-    let minValue: Int
-    let maxValue: Int
-    let maxWidth: CGFloat
-    @Binding var data: [PredictionLineData]
-    let showHours: Int
-
-    var chartsData: some View {
-        ForEach(0 ..< data.count, id: \.self) { index -> AnyView in
-            HStack {
-                PointChartView(
-                    minValue: minValue,
-                    maxValue: maxValue,
-                    maxWidth: maxWidth,
-                    mode: .dots,
-                    showHours: showHours,
-                    glucoseData: $data[index].values
-                ) { value in
-                    PredictionPointView(
-                        predictionType: data[index].type,
-                        value: value
-                    )
-                }
-                Spacer()
-            }.asAny()
-        }
-    }
-
-    var body: some View {
-        ZStack {
-            chartsData
-        }
-    }
-}

+ 0 - 5
FreeAPS/Sources/Charts/Views/ChartsConfig.swift

@@ -1,5 +0,0 @@
-import SwiftUI
-
-enum ChartsConfig {
-    static let glucosePointSize: CGFloat = 5
-}

+ 0 - 59
FreeAPS/Sources/Charts/Views/Components/GlucoseArrowView.swift

@@ -1,59 +0,0 @@
-import SwiftUI
-
-struct GlucoseArrowView: View {
-    let direction: BloodGlucose.Direction
-
-    var body: some View {
-        arrowImage
-            .foregroundColor(Color(.systemBlue))
-            .informationBarEntryStyle()
-    }
-}
-
-extension GlucoseArrowView {
-    var arrowImage: Image {
-        let arrow: String
-
-        let up = "arrow.up"
-        let upForward = "arrow.up.forward"
-        let forward = "arrow.forward"
-        let downForward = "arrow.down.forward"
-        let down = "arrow.down"
-        let error = "arrow.left.arrow.right"
-
-        switch direction {
-        case .doubleUp,
-             .singleUp,
-             .tripleUp:
-            arrow = up
-        case .fortyFiveUp:
-            arrow = upForward
-        case .flat:
-            arrow = forward
-        case .fortyFiveDown:
-            arrow = downForward
-        case .doubleDown,
-             .singleDown,
-             .tripleDown:
-            arrow = down
-        case .none,
-             .notComputable,
-             .rateOutOfRange:
-            arrow = error
-        }
-
-        return Image(systemName: arrow)
-    }
-}
-
-struct GlucoseArrowView_Previews: PreviewProvider {
-    static var previews: some View {
-        GlucoseArrowView(direction: .fortyFiveDown)
-            .frame(
-                width: 100,
-                height: 100,
-                alignment: .center
-            )
-            .preferredColorScheme(/*@START_MENU_TOKEN@*/ .dark/*@END_MENU_TOKEN@*/)
-    }
-}

+ 0 - 59
FreeAPS/Sources/Charts/Views/Components/GlucoseInformationBarView.swift

@@ -1,59 +0,0 @@
-import SwiftUI
-
-struct GlucoseInformationBarView: View {
-    let data: [InformationBarEntryData]
-    let glucoseValue: Double
-    let glucoseDelta: Double
-    let direction: BloodGlucose.Direction
-
-    var body: some View {
-        let halvedEntryData = data.halve()
-        HStack {
-            VStack {
-                ForEach(halvedEntryData, id: \.self) { half in
-                    HStack {
-                        ForEach(half, id: \.self) { dataEntry in
-                            Text(
-                                dataEntry.label + "\n" +
-                                    APSDataFormatter.format(
-                                        inputValue: dataEntry.value,
-                                        to: dataEntry.type
-                                    )
-                            )
-                            .font(.footnote)
-                            .informationBarEntryStyle()
-                            .padding(.bottom, 1)
-                        }
-                    }
-                }
-            }
-            Text(APSDataFormatter.format(inputValue: glucoseValue, to: .glucose))
-                .font(.largeTitle)
-                .foregroundColor(Color(.systemBlue))
-                .informationBarEntryStyle()
-            VStack {
-                GlucoseArrowView(direction: direction)
-                    .padding(.bottom, 1)
-                Text(APSDataFormatter.format(inputValue: glucoseDelta, to: .delta))
-                    .informationBarEntryStyle()
-                    .padding(.bottom, 1)
-            }
-        }
-        .padding(.bottom, -1)
-    }
-}
-
-struct GlucoseInformationBarView_Previews: PreviewProvider {
-    static let data = [
-        InformationBarEntryData(label: "COB: ", value: 33, type: .cob),
-        InformationBarEntryData(label: "COB: ", value: 33, type: .cob),
-        InformationBarEntryData(label: "COB: ", value: 33, type: .cob)
-//        InformationBarEntryData(label: "COB: ", type: .cob, value: 33),
-    ]
-    static var previews: some View {
-        GlucoseInformationBarView(data: data, glucoseValue: 5.5, glucoseDelta: -0.2, direction: .fortyFiveDown)
-            .preferredColorScheme(.dark)
-            .frame(height: 200)
-            .padding(.horizontal)
-    }
-}

+ 0 - 15
FreeAPS/Sources/Charts/Views/Components/HoursPickerView.swift

@@ -1,15 +0,0 @@
-import SwiftUI
-
-struct HoursPickerView: View {
-    @Binding var selectedHour: Int
-    private let avaliableHours = [1, 3, 6, 12]
-
-    var body: some View {
-        Picker("Show Hours", selection: $selectedHour) {
-            ForEach(avaliableHours, id: \.self) { hour in
-                Text(String(hour) + "HR")
-            }
-        }
-        .pickerStyle(SegmentedPickerStyle())
-    }
-}

+ 0 - 21
FreeAPS/Sources/Charts/Views/Points/GlucosePointView.swift

@@ -1,21 +0,0 @@
-import SwiftUI
-
-struct GlucosePointView: View {
-    let value: Int?
-
-    var body: some View {
-        Circle()
-            .foregroundColor(
-                Color(.systemBlue)
-            )
-            .frame(width: ChartsConfig.glucosePointSize, height: ChartsConfig.glucosePointSize)
-            .opacity(value != nil ? 1 : 0)
-    }
-}
-
-struct GlucosePointView_Previews: PreviewProvider {
-    static var previews: some View {
-        GlucosePointView(value: 3)
-            .preferredColorScheme(.dark)
-    }
-}

+ 0 - 42
FreeAPS/Sources/Charts/Views/Points/PredictionPointView.swift

@@ -1,42 +0,0 @@
-import SwiftUI
-
-struct PredictionPointView: View {
-    let predictionType: PredictionType
-    let value: Int?
-
-    var body: some View {
-        Circle()
-            .strokeBorder(
-                predictionColor,
-                lineWidth: 1.5,
-                antialiased: true
-            )
-            .frame(width: ChartsConfig.glucosePointSize, height: ChartsConfig.glucosePointSize)
-    }
-}
-
-extension PredictionPointView {
-    var predictionColor: Color {
-        let color: Color
-
-        switch predictionType {
-        case .iob:
-            color = Color(.systemTeal)
-        case .cob:
-            color = Color(.systemOrange)
-        case .zt:
-            color = Color(.systemPink)
-        case .uam:
-            color = Color(.systemIndigo)
-        }
-
-        return color.opacity(value != nil ? 1 : 0)
-    }
-}
-
-struct PredictionPointView_Previews: PreviewProvider {
-    static var previews: some View {
-        PredictionPointView(predictionType: .iob, value: 3)
-            .preferredColorScheme(/*@START_MENU_TOKEN@*/ .dark/*@END_MENU_TOKEN@*/)
-    }
-}

+ 18 - 0
FreeAPS/Sources/Helpers/ConcurrentMap.swift

@@ -0,0 +1,18 @@
+import Foundation
+
+extension Collection where Index == Int {
+    func concurrentMap<T>(_ transform: (Element) -> T) -> [T] {
+        let buffer = UnsafeMutableRawPointer.allocate(
+            byteCount: count * MemoryLayout<T>.stride,
+            alignment: MemoryLayout<T>.alignment
+        ).bindMemory(to: T.self, capacity: count)
+
+        DispatchQueue.concurrentPerform(iterations: count) { index in
+            let element = self[index]
+            let transformedElement = transform(element)
+            buffer[index] = transformedElement
+        }
+
+        return [T](UnsafeBufferPointer(start: buffer, count: count))
+    }
+}

+ 2 - 2
FreeAPS/Sources/Models/Suggestion.swift

@@ -1,6 +1,6 @@
 import Foundation
 
-struct Suggestion: JSON {
+struct Suggestion: JSON, Equatable {
     let reason: String
     let units: Decimal?
     let insulinReq: Decimal?
@@ -20,7 +20,7 @@ struct Suggestion: JSON {
     var recieved: Bool?
 }
 
-struct Predictions: JSON {
+struct Predictions: JSON, Equatable {
     let iob: [Int]?
     let zt: [Int]?
     let cob: [Int]?

+ 0 - 198
FreeAPS/Sources/Modules/Home/View/GlucoseChartView.swift

@@ -1,198 +0,0 @@
-import Charts
-import SwiftDate
-import SwiftUI
-
-extension DateFormatter: AxisValueFormatter {
-    public func stringForValue(_ value: Double, axis _: AxisBase?) -> String {
-        timeStyle = .short
-        return string(from: Date(timeIntervalSince1970: value))
-    }
-}
-
-extension NumberFormatter: AxisValueFormatter {
-    public func stringForValue(_ value: Double, axis _: AxisBase?) -> String {
-        numberStyle = .decimal
-        maximumFractionDigits = 1
-        return string(from: value as NSNumber)!
-    }
-}
-
-struct GlucoseChartView: UIViewRepresentable {
-    @Binding var glucose: [BloodGlucose]
-    @Binding var suggestion: Suggestion?
-    let units: GlucoseUnits
-
-    func makeUIView(context _: Context) -> LineChartView {
-        let view = LineChartView()
-        makeDataPointsFor(view: view)
-        view.xAxis.valueFormatter = DateFormatter()
-        view.leftAxis.valueFormatter = NumberFormatter()
-        view.xAxis.labelPosition = .top
-        view.rightAxis.drawLabelsEnabled = false
-        view.drawBordersEnabled = true
-        view.setScaleEnabled(false)
-        view.setVisibleXRangeMaximum(6.hours.timeInterval)
-        view.xAxis.granularityEnabled = true
-        view.xAxis.granularity = 1.hours.timeInterval
-        return view
-    }
-
-    func updateUIView(_ view: LineChartView, context _: Context) {
-        makeDataPointsFor(view: view)
-        view.moveViewToX(glucose.last?.dateString.timeIntervalSince1970 ?? 0)
-    }
-
-    private func makeDataPointsFor(view: LineChartView) {
-        guard !glucose.isEmpty else {
-            return
-        }
-
-        let dataPoints = glucose.map {
-            ChartDataEntry(
-                x: $0.dateString.timeIntervalSince1970,
-                y: Double($0.sgv ?? 0) * (units == .mmolL ? Double(GlucoseUnits.exchangeRate) : 1)
-            )
-        }
-
-        let data = MyLineChartDataSet(entries: dataPoints, label: "BG")
-        data.drawCirclesEnabled = true
-        data.circleRadius = 2
-        data.setCircleColor(UIColor(named: "LoopGreen")!)
-        data.setColor(UIColor(named: "LoopGreen")!)
-        data.lineWidth = 0
-        data.drawValuesEnabled = false
-
-        var series = [data]
-
-        let lastDate = suggestion?.deliverAt ?? Date()
-
-        if let iob = suggestion?.predictions?.iob {
-            let dataPoints = iob.enumerated().map {
-                ChartDataEntry(
-                    x: lastDate.addingTimeInterval(Double($0 * 300)).timeIntervalSince1970,
-                    y: Double($1) * (units == .mmolL ? Double(GlucoseUnits.exchangeRate) : 1)
-                )
-            }
-            let data = MyLineChartDataSet(entries: dataPoints, label: "IOB")
-            data.drawCirclesEnabled = true
-            data.circleRadius = 2
-            data.setCircleColor(.blue)
-            data.setColor(.blue)
-            data.lineWidth = 0
-            data.drawValuesEnabled = false
-            series.append(data)
-        }
-
-        if let zt = suggestion?.predictions?.zt {
-            let dataPoints = zt.enumerated().map {
-                ChartDataEntry(
-                    x: lastDate.addingTimeInterval(Double($0 * 300)).timeIntervalSince1970,
-                    y: Double($1) * (units == .mmolL ? Double(GlucoseUnits.exchangeRate) : 1)
-                )
-            }
-            let data = MyLineChartDataSet(entries: dataPoints, label: "ZT")
-            data.drawCirclesEnabled = true
-            data.circleRadius = 2
-            data.setCircleColor(.cyan)
-            data.setColor(.cyan)
-            data.lineWidth = 0
-            data.drawValuesEnabled = false
-            series.append(data)
-        }
-
-        if let cob = suggestion?.predictions?.cob {
-            let dataPoints = cob.enumerated().map {
-                ChartDataEntry(
-                    x: lastDate.addingTimeInterval(Double($0 * 300)).timeIntervalSince1970,
-                    y: Double($1) * (units == .mmolL ? Double(GlucoseUnits.exchangeRate) : 1)
-                )
-            }
-            let data = MyLineChartDataSet(entries: dataPoints, label: "COB")
-            data.drawCirclesEnabled = true
-            data.circleRadius = 2
-            data.setCircleColor(.orange)
-            data.setColor(.orange)
-            data.lineWidth = 0
-            data.drawValuesEnabled = false
-            series.append(data)
-        }
-
-        if let uam = suggestion?.predictions?.uam {
-            let dataPoints = uam.enumerated().map {
-                ChartDataEntry(
-                    x: lastDate.addingTimeInterval(Double($0 * 300)).timeIntervalSince1970,
-                    y: Double($1) * (units == .mmolL ? Double(GlucoseUnits.exchangeRate) : 1)
-                )
-            }
-            let data = MyLineChartDataSet(entries: dataPoints, label: "UAM")
-            data.drawCirclesEnabled = true
-            data.circleRadius = 2
-            data.setCircleColor(.yellow)
-            data.setColor(.yellow)
-            data.lineWidth = 0
-            data.drawValuesEnabled = false
-            series.append(data)
-        }
-
-        view.data = LineChartData(dataSets: series)
-    }
-}
-
-class MyLineChartDataSet: LineChartDataSet {
-    override func entryIndex(x xValue: Double, closestToY yValue: Double, rounding: ChartDataSetRounding) -> Int {
-        var closest = partitioningIndex { $0.x >= xValue }
-        if closest >= endIndex {
-            closest = endIndex - 1
-        }
-
-        let closestXValue = self[closest].x
-
-        switch rounding {
-        case .up:
-            // If rounding up, and found x-value is lower than specified x, and we can go upper...
-            if closestXValue < xValue, closest < index(before: endIndex)
-            {
-                formIndex(after: &closest)
-            }
-
-        case .down:
-            // If rounding down, and found x-value is upper than specified x, and we can go lower...
-            if closestXValue > xValue, closest > startIndex
-            {
-                formIndex(before: &closest)
-            }
-
-        case .closest:
-            break
-        }
-
-        // Search by closest to y-value
-        if !yValue.isNaN
-        {
-            while closest > startIndex, self[index(before: closest)].x == closestXValue
-            {
-                formIndex(before: &closest)
-            }
-
-            var closestYValue = self[closest].y
-            var closestYIndex = closest
-
-            while closest < index(before: endIndex)
-            {
-                formIndex(after: &closest)
-                let value = self[closest]
-
-                if value.x != closestXValue { break }
-                if abs(value.y - yValue) <= abs(closestYValue - yValue)
-                {
-                    closestYValue = yValue
-                    closestYIndex = closest
-                }
-            }
-
-            closest = closestYIndex
-        }
-
-        return closest
-    }
-}

+ 5 - 2
FreeAPS/Sources/Modules/Home/View/HomeRootView.swift

@@ -47,8 +47,11 @@ extension Home {
             return GeometryReader { geo in
                 VStack {
                     header.padding().frame(maxHeight: 70)
-                    GlucoseChartView(glucose: $viewModel.glucose, suggestion: $viewModel.suggestion, units: viewModel.units)
-                        .frame(maxHeight: .infinity)
+                    MainChartView(
+                        glucose: $viewModel.glucose,
+                        suggestion: $viewModel.suggestion,
+                        hours: .constant(24)
+                    )
 
                     ZStack {
                         Rectangle().fill(Color.gray.opacity(0.2)).frame(height: 50 + geo.safeAreaInsets.bottom)

+ 257 - 0
FreeAPS/Sources/Modules/Home/View/MainChartView.swift

@@ -0,0 +1,257 @@
+import Algorithms
+import SwiftDate
+import SwiftUI
+
+private enum PredictionType: Hashable {
+    case iob
+    case cob
+    case zt
+    case uam
+}
+
+struct MainChartView: View {
+    @Binding var glucose: [BloodGlucose]
+    @Binding var suggestion: Suggestion?
+    @Binding var hours: Int
+    private let screenHours = 6
+
+    @State var didAppearTrigger = false
+    @State private var glucoseDots: [CGRect] = []
+    @State private var predictionDots: [PredictionType: [CGRect]] = [:]
+
+    private var dateDormatter: DateFormatter {
+        let formatter = DateFormatter()
+        formatter.timeStyle = .short
+        return formatter
+    }
+
+    var body: some View {
+        GeometryReader { geo -> AnyView in
+            ScrollView(.horizontal, showsIndicators: false) {
+                ScrollViewReader { scroll in
+                    mainChart(fullSize: geo.size)
+                        .drawingGroup(opaque: false, colorMode: .nonLinear)
+                        .onChange(of: glucose) { _ in
+                            scroll.scrollTo("End")
+                        }
+                        .onAppear {
+                            scroll.scrollTo("End")
+                            // add trigger the end of main queue
+                            DispatchQueue.main.async {
+                                didAppearTrigger = true
+                            }
+                        }
+                }
+            }.asAny()
+        }
+    }
+
+    private func mainChart(fullSize: CGSize) -> some View {
+        Group {
+            VStack {
+                ZStack {
+                    Path { path in
+                        for hour in 0 ..< hours + hours {
+                            let x = firstHourPosition(viewWidth: fullSize.width) +
+                                oneSecondStep(viewWidth: fullSize.width) *
+                                CGFloat(hour) * CGFloat(1.hours.timeInterval)
+                            path.move(to: CGPoint(x: x, y: 0))
+                            path.addLine(to: CGPoint(x: x, y: fullSize.height - 20))
+                        }
+                    }
+                    .stroke(Color.secondary, lineWidth: 0.2)
+                    glucosePath(fullSize: fullSize)
+                    predictions(fullSize: fullSize).id("End")
+                }
+                ZStack {
+                    ForEach(0 ..< hours + hours) { hour in
+                        Text(dateDormatter.string(from: firstHourDate().addingTimeInterval(hour.hours.timeInterval)))
+                            .font(.caption)
+                            .position(
+                                x: firstHourPosition(viewWidth: fullSize.width) +
+                                    oneSecondStep(viewWidth: fullSize.width) *
+                                    CGFloat(hour) * CGFloat(1.hours.timeInterval),
+                                y: 10.0
+                            )
+                            .foregroundColor(.secondary)
+                    }
+                }.frame(maxHeight: 20)
+            }
+        }
+        .frame(width: fullGlucoseWidth(viewWidth: fullSize.width) + additionalWidth(viewWidth: fullSize.width))
+    }
+
+    private func glucosePath(fullSize: CGSize) -> some View {
+        Path { path in
+            for rect in glucoseDots {
+                path.addEllipse(in: rect)
+            }
+        }
+        .fill(Color.green)
+        .onChange(of: glucose) { _ in
+            calculateGlucoseDots(fullSize: fullSize)
+        }
+        .onChange(of: didAppearTrigger) { _ in
+            calculateGlucoseDots(fullSize: fullSize)
+        }
+    }
+
+    private func calculateGlucoseDots(fullSize: CGSize) {
+        glucoseDots = glucose.concurrentMap { value -> CGRect in
+            let position = glucoseToCoordinate(value, fullSize: fullSize)
+            return CGRect(x: position.x - 2, y: position.y - 2, width: 4, height: 4)
+        }
+    }
+
+    private func calculatePredictionDots(fullSize: CGSize, type: PredictionType) {
+        let values: [Int] = { () -> [Int] in
+            switch type {
+            case .iob:
+                return suggestion?.predictions?.iob ?? []
+            case .cob:
+                return suggestion?.predictions?.cob ?? []
+            case .zt:
+                return suggestion?.predictions?.zt ?? []
+            case .uam:
+                return suggestion?.predictions?.uam ?? []
+            }
+        }()
+
+        var index = 0
+        predictionDots[type] = values.concurrentMap { value -> CGRect in
+            let position = predictionToCoordinate(value, fullSize: fullSize, index: index)
+            index += 1
+            return CGRect(x: position.x - 2, y: position.y - 2, width: 4, height: 4)
+        }
+    }
+
+    private func predictions(fullSize: CGSize) -> some View {
+        Group {
+            Path { path in
+                for rect in predictionDots[.iob] ?? [] {
+                    path.addEllipse(in: rect)
+                }
+            }.stroke(Color.blue)
+                .onChange(of: suggestion) { _ in
+                    calculatePredictionDots(fullSize: fullSize, type: .iob)
+                }
+                .onChange(of: didAppearTrigger) { _ in
+                    calculatePredictionDots(fullSize: fullSize, type: .iob)
+                }
+
+            Path { path in
+                for rect in predictionDots[.cob] ?? [] {
+                    path.addEllipse(in: rect)
+                }
+            }.stroke(Color.yellow)
+                .onChange(of: suggestion) { _ in
+                    calculatePredictionDots(fullSize: fullSize, type: .cob)
+                }
+                .onChange(of: didAppearTrigger) { _ in
+                    calculatePredictionDots(fullSize: fullSize, type: .cob)
+                }
+
+            Path { path in
+                for rect in predictionDots[.zt] ?? [] {
+                    path.addEllipse(in: rect)
+                }
+            }.stroke(Color.purple)
+                .onChange(of: suggestion) { _ in
+                    calculatePredictionDots(fullSize: fullSize, type: .zt)
+                }
+                .onChange(of: didAppearTrigger) { _ in
+                    calculatePredictionDots(fullSize: fullSize, type: .zt)
+                }
+
+            Path { path in
+                for rect in predictionDots[.uam] ?? [] {
+                    path.addEllipse(in: rect)
+                }
+            }.stroke(Color.orange)
+                .onChange(of: suggestion) { _ in
+                    calculatePredictionDots(fullSize: fullSize, type: .uam)
+                }
+                .onChange(of: didAppearTrigger) { _ in
+                    calculatePredictionDots(fullSize: fullSize, type: .uam)
+                }
+        }
+    }
+
+    private func fullGlucoseWidth(viewWidth: CGFloat) -> CGFloat {
+        viewWidth * CGFloat(hours) / CGFloat(screenHours)
+    }
+
+    private func additionalWidth(viewWidth: CGFloat) -> CGFloat {
+        guard let predictions = suggestion?.predictions,
+              let deliveredAt = suggestion?.deliverAt,
+              let last = glucose.last
+        else {
+            return 0
+        }
+
+        let iob = predictions.iob?.count ?? 0
+        let zt = predictions.zt?.count ?? 0
+        let cob = predictions.cob?.count ?? 0
+        let uam = predictions.uam?.count ?? 0
+        let max = [iob, zt, cob, uam].max() ?? 0
+
+        let lastDeltaTime = last.dateString.timeIntervalSince(deliveredAt)
+        let additionalTime = CGFloat(TimeInterval(max) * 5.minutes.timeInterval - lastDeltaTime)
+        let oneSecondWidth = oneSecondStep(viewWidth: viewWidth)
+
+        return additionalTime * oneSecondWidth
+    }
+
+    private func oneSecondStep(viewWidth: CGFloat) -> CGFloat {
+        viewWidth / (CGFloat(screenHours) * CGFloat(1.hours.timeInterval))
+    }
+
+    private func glucoseToCoordinate(_ glucoseEntry: BloodGlucose, fullSize: CGSize) -> CGPoint {
+        guard let first = glucose.first else {
+            return .zero
+        }
+        let yPadding: CGFloat = 30
+        let maxValue = glucose.compactMap(\.glucose).max() ?? 0
+        let minValue = glucose.compactMap(\.glucose).min() ?? 0
+        let stepYFraction = (fullSize.height - yPadding * 2) / CGFloat(maxValue - minValue)
+        let yOffset = CGFloat(minValue) * stepYFraction
+        let xOffset = -first.dateString.timeIntervalSince1970
+        let stepXFraction = fullGlucoseWidth(viewWidth: fullSize.width) / CGFloat(hours.hours.timeInterval)
+        let x = CGFloat(glucoseEntry.dateString.timeIntervalSince1970 + xOffset) * stepXFraction
+        let y = fullSize.height - CGFloat(glucoseEntry.glucose ?? 0) * stepYFraction + yOffset - yPadding
+
+        return CGPoint(x: x, y: y)
+    }
+
+    private func predictionToCoordinate(_ pred: Int, fullSize: CGSize, index: Int) -> CGPoint {
+        guard let first = glucose.first, let deliveredAt = suggestion?.deliverAt else {
+            return .zero
+        }
+        let yPadding: CGFloat = 30
+        let maxValue = glucose.compactMap(\.glucose).max() ?? 0
+        let minValue = glucose.compactMap(\.glucose).min() ?? 0
+        let stepYFraction = (fullSize.height - yPadding * 2) / CGFloat(maxValue - minValue)
+        let yOffset = CGFloat(minValue) * stepYFraction
+        let xOffset = -first.dateString.timeIntervalSince1970
+        let stepXFraction = fullGlucoseWidth(viewWidth: fullSize.width) / CGFloat(hours.hours.timeInterval)
+        let predTime = deliveredAt.timeIntervalSince1970 + TimeInterval(index) * 5.minutes.timeInterval
+        let x = CGFloat(predTime + xOffset) * stepXFraction
+        let y = fullSize.height - CGFloat(pred) * stepYFraction + yOffset - yPadding
+
+        return CGPoint(x: x, y: y)
+    }
+
+    private func firstHourDate() -> Date {
+        let firstDate = glucose.first?.dateString ?? Date()
+        return firstDate.dateTruncated(from: .minute)!
+    }
+
+    private func firstHourPosition(viewWidth: CGFloat) -> CGFloat {
+        let firstDate = glucose.first?.dateString ?? Date()
+        let firstHour = firstHourDate()
+
+        let lastDeltaTime = firstHour.timeIntervalSince(firstDate)
+        let oneSecondWidth = oneSecondStep(viewWidth: viewWidth)
+        return oneSecondWidth * CGFloat(lastDeltaTime)
+    }
+}

+ 18 - 0
FreeAPS/Sources/Views/View+Snapshot.swift

@@ -0,0 +1,18 @@
+import SwiftUI
+
+extension View {
+    func snapshot() -> UIImage {
+        let controller = UIHostingController(rootView: self)
+        let view = controller.view
+
+        let targetSize = controller.view.intrinsicContentSize
+        view?.bounds = CGRect(origin: .zero, size: targetSize)
+        view?.backgroundColor = .clear
+
+        let renderer = UIGraphicsImageRenderer(size: targetSize)
+
+        return renderer.image { _ in
+            view?.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true)
+        }
+    }
+}