Przeglądaj źródła

Merge pull request #5 from ivalkou/charts

Charts
Ivan 5 lat temu
rodzic
commit
610f23eae3
33 zmienionych plików z 820 dodań i 10 usunięć
  1. 160 0
      FreeAPS.xcodeproj/project.pbxproj
  2. 10 0
      FreeAPS/Sources/Charts/Extensions/Array+halve().swift
  3. 7 0
      FreeAPS/Sources/Charts/Extensions/Date+-.swift
  4. 13 0
      FreeAPS/Sources/Charts/Extensions/Double+getBoundGlucose().swift
  5. 16 0
      FreeAPS/Sources/Charts/Extensions/View+Modifiers.swift
  6. 14 0
      FreeAPS/Sources/Charts/Extensions/View+if().swift
  7. 43 0
      FreeAPS/Sources/Charts/Helpers/APSDataFormatter.swift
  8. 5 0
      FreeAPS/Sources/Charts/Helpers/getChartWidth().swift
  9. 35 0
      FreeAPS/Sources/Charts/Helpers/getGlucoseArrowImage().swift
  10. 10 0
      FreeAPS/Sources/Charts/Models/APSDataTypes.swift
  11. 6 0
      FreeAPS/Sources/Charts/Models/BoundTypes.swift
  12. 8 0
      FreeAPS/Sources/Charts/Models/GlucosePointData.swift
  13. 8 0
      FreeAPS/Sources/Charts/Models/InformationBarEntryData.swift
  14. 6 0
      FreeAPS/Sources/Charts/Models/MeshEntryOrientations.swift
  15. 8 0
      FreeAPS/Sources/Charts/Models/PointData.swift
  16. 7 0
      FreeAPS/Sources/Charts/Models/PredictionLineData.swift
  17. 8 0
      FreeAPS/Sources/Charts/Models/PredictionType.swift
  18. 78 0
      FreeAPS/Sources/Charts/Views/Charts/CombinedChartView.swift
  19. 74 0
      FreeAPS/Sources/Charts/Views/Charts/PointChartView.swift
  20. 35 0
      FreeAPS/Sources/Charts/Views/Charts/PredictionsChartView.swift
  21. 5 0
      FreeAPS/Sources/Charts/Views/ChartsConfig.swift
  22. 59 0
      FreeAPS/Sources/Charts/Views/Components/GlucoseArrowView.swift
  23. 59 0
      FreeAPS/Sources/Charts/Views/Components/GlucoseInformationBarView.swift
  24. 15 0
      FreeAPS/Sources/Charts/Views/Components/HoursPickerView.swift
  25. 21 0
      FreeAPS/Sources/Charts/Views/Points/GlucosePointView.swift
  26. 42 0
      FreeAPS/Sources/Charts/Views/Points/PredictionPointView.swift
  27. 6 1
      FreeAPS/Sources/Models/BloodGlucose.swift
  28. 1 1
      FreeAPS/Sources/Modules/Home/HomeDataFlow.swift
  29. 2 2
      FreeAPS/Sources/Modules/Home/HomeProvider.swift
  30. 8 2
      FreeAPS/Sources/Modules/Home/HomeViewModel.swift
  31. 45 2
      FreeAPS/Sources/Modules/Home/View/HomeRootView.swift
  32. 2 2
      FreeAPS/Sources/Services/Network/NightscoutAPI.swift
  33. 4 0
      FreeAPS/Sources/Services/Network/NightscoutManager.swift

+ 160 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -204,7 +204,31 @@
 		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 */; };
@@ -474,6 +498,30 @@
 		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>"; };
@@ -629,6 +677,7 @@
 				3811DEDE25C9E2DD00A708ED /* Application */,
 				3811DF0A25CAAAA500A708ED /* APS */,
 				38E98A3225F5300800C0CED0 /* Config */,
+				6610F9F125FAED29004781D7 /* Charts */,
 				3811DEBD25C9D99900A708ED /* Containers */,
 				388E5A5A25B6F05F0019842D /* Helpers */,
 				38E98A1A25F52C9300C0CED0 /* Logger */,
@@ -1237,6 +1286,93 @@
 			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 = (
@@ -1528,9 +1664,12 @@
 				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 */,
@@ -1545,6 +1684,9 @@
 				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 */,
@@ -1553,6 +1695,7 @@
 				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 */,
@@ -1569,7 +1712,9 @@
 				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 */,
@@ -1589,6 +1734,7 @@
 				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 */,
@@ -1599,13 +1745,16 @@
 				3811DE5D25C9D4D500A708ED /* Publisher.swift in Sources */,
 				38B4F3AF25E2979F00E76A18 /* IndexedCollection.swift in Sources */,
 				3811DEAE25C9D88300A708ED /* Cache.swift in Sources */,
+				6610FA2425FAED29004781D7 /* PredictionsChartView.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 */,
@@ -1620,6 +1769,7 @@
 				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 */,
@@ -1637,6 +1787,7 @@
 				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 */,
@@ -1644,8 +1795,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 */,
 				72F1BD388F42FCA6C52E4500 /* ConfigEditorProvider.swift in Sources */,
 				E39E418C56A5A46B61D960EE /* ConfigEditorViewModel.swift in Sources */,
@@ -1678,6 +1831,7 @@
 				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 */,
@@ -1686,9 +1840,11 @@
 				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 */,
@@ -1700,10 +1856,12 @@
 				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 */,
@@ -1711,6 +1869,7 @@
 				38A00B1F25FC00F7006BC0B0 /* Autotune.swift in Sources */,
 				19434C14DF3F4816F4E4BF2E /* BolusBuilder.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 */,
@@ -1720,6 +1879,7 @@
 				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 */,

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

@@ -0,0 +1,10 @@
+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)]
+    }
+}

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

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

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

@@ -0,0 +1,13 @@
+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
+    }
+}

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

@@ -0,0 +1,16 @@
+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())
+    }
+}

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

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

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

@@ -0,0 +1,43 @@
+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)
+        }
+    }
+}

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

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

+ 35 - 0
FreeAPS/Sources/Charts/Helpers/getGlucoseArrowImage().swift

@@ -0,0 +1,35 @@
+import SwiftUI
+
+func getGlucoseArrowImage(for delta: BloodGlucose.Direction) -> 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 delta {
+    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)
+}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -0,0 +1,78 @@
+import SwiftUI
+
+struct CombinedChartView: View {
+    let maxWidth: CGFloat
+    let showHours: Int
+    @Binding var glucoseData: [BloodGlucose]
+    @Binding var predictionsData: [PredictionLineData]
+    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,
+                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@*/)
+//    }
+// }

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

@@ -0,0 +1,74 @@
+import SwiftUI
+
+struct PointChartView<PointEntry: View>: View {
+    let minValue: Int
+    let maxValue: Int
+    let maxWidth: CGFloat
+
+    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
+            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
+            )
+        }
+    }
+}

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

@@ -0,0 +1,35 @@
+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,
+                    showHours: showHours,
+                    glucoseData: $data[index].values
+                ) { value in
+                    PredictionPointView(
+                        predictionType: data[index].type,
+                        value: value
+                    )
+                }
+                Spacer()
+            }.asAny()
+        }
+    }
+
+    var body: some View {
+        ZStack {
+            chartsData
+        }
+    }
+}

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

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

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

@@ -0,0 +1,59 @@
+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@*/)
+    }
+}

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

@@ -0,0 +1,59 @@
+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)
+    }
+}

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

@@ -0,0 +1,15 @@
+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())
+    }
+}

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

@@ -0,0 +1,21 @@
+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)
+    }
+}

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

@@ -0,0 +1,42 @@
+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@*/)
+    }
+}

+ 6 - 1
FreeAPS/Sources/Models/BloodGlucose.swift

@@ -1,6 +1,6 @@
 import Foundation
 
-struct BloodGlucose: JSON {
+struct BloodGlucose: JSON, Identifiable, Hashable {
     enum Direction: String, JSON {
         case tripleUp = "TripleUp"
         case doubleUp = "DoubleUp"
@@ -16,6 +16,11 @@ struct BloodGlucose: JSON {
         case rateOutOfRange = "RATE OUT OF RANGE"
     }
 
+    var _id = UUID().uuidString
+    var id: String {
+        _id
+    }
+
     var sgv: Int?
     let direction: Direction?
     let date: UInt64

+ 1 - 1
FreeAPS/Sources/Modules/Home/HomeDataFlow.swift

@@ -7,5 +7,5 @@ enum Home {
 protocol HomeProvider: Provider {
     var suggestion: Suggestion? { get }
     func fetchAndLoop()
-    func filteredGlucose() -> [BloodGlucose]
+    func filteredGlucose(hours: Int) -> [BloodGlucose]
 }

+ 2 - 2
FreeAPS/Sources/Modules/Home/HomeProvider.swift

@@ -14,9 +14,9 @@ extension Home {
             apsManager.fetchAndLoop()
         }
 
-        func filteredGlucose() -> [BloodGlucose] {
+        func filteredGlucose(hours: Int) -> [BloodGlucose] {
             glucoseStorage.recent().filter {
-                $0.dateString.addingTimeInterval(3.hours.timeInterval) > Date()
+                $0.dateString.addingTimeInterval(hours.hours.timeInterval) > Date()
             }
         }
     }

+ 8 - 2
FreeAPS/Sources/Modules/Home/HomeViewModel.swift

@@ -6,13 +6,15 @@ extension Home {
         @Injected() var broadcaster: Broadcaster!
         @Injected() var settingsManager: SettingsManager!
 
+        private(set) var filteredGlucoseHours = 3
+
         @Published var glucose: [BloodGlucose] = []
         @Published var suggestion: Suggestion?
 
         @Published var allowManualTemp = false
 
         override func subscribe() {
-            glucose = provider.filteredGlucose()
+            glucose = provider.filteredGlucose(hours: filteredGlucoseHours)
             suggestion = provider.suggestion
             allowManualTemp = !settingsManager.settings.closedLoop
             broadcaster.register(GlucoseObserver.self, observer: self)
@@ -43,12 +45,16 @@ extension Home {
         func settings() {
             showModal(for: .settings)
         }
+
+        func setFilteredGlucoseHours(hours: Int) {
+            filteredGlucoseHours = hours
+        }
     }
 }
 
 extension Home.ViewModel: GlucoseObserver, SuggestionObserver, SettingsObserver {
     func glucoseDidUpdate(_: [BloodGlucose]) {
-        glucose = provider.filteredGlucose()
+        glucose = provider.filteredGlucose(hours: filteredGlucoseHours)
     }
 
     func suggestionDidUpdate(_ suggestion: Suggestion) {

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

@@ -3,15 +3,58 @@ import SwiftUI
 extension Home {
     struct RootView: BaseView {
         @EnvironmentObject var viewModel: ViewModel<Provider>
+        @State var showHours = 1
 
-        var body: some View {
+        var mainChart: some View {
             GeometryReader { geo in
+                ScrollView(.horizontal, showsIndicators: false) {
+                    CombinedChartView(
+                        maxWidth: geo.size.width,
+                        showHours: showHours,
+                        glucoseData: $viewModel.glucose,
+                        predictionsData: .constant([])
+                    )
+                }
+            }
+            .padding(.vertical)
+            .background(Color(.systemGray6))
+            .cornerRadius(12)
+        }
+
+        var previewChart: some View {
+            GeometryReader { geo in
+                CombinedChartView(
+                    maxWidth: geo.size.width,
+                    showHours: 24,
+                    glucoseData: $viewModel.glucose,
+                    predictionsData: .constant([])
+                )
+            }
+            .frame(maxWidth: .infinity)
+            .padding(.vertical)
+            .background(Color(.systemGray6))
+            .cornerRadius(10)
+        }
+
+        var body: some View {
+            viewModel.setFilteredGlucoseHours(hours: 24)
+            return GeometryReader { geo in
                 VStack {
                     Group {
                         Text("Header")
                     }
                     ScrollView(.vertical, showsIndicators: false) {
-                        GlucoseChartView(glucose: $viewModel.glucose, suggestion: $viewModel.suggestion).frame(height: 300)
+                        HoursPickerView(selectedHour: $showHours).padding(.horizontal)
+
+                        mainChart
+                            .frame(height: geo.size.height * 0.6)
+
+                            .padding(.horizontal)
+
+                        previewChart
+                            .frame(height: 50)
+                            .padding(.horizontal)
+                        // GlucoseChartView(glucose: $viewModel.glucose, suggestion: $viewModel.suggestion).frame(height: 150)
                         if let reason = viewModel.suggestion?.reason {
                             Text(reason).font(.caption).padding()
                         }

+ 2 - 2
FreeAPS/Sources/Services/Network/NightscoutAPI.swift

@@ -73,8 +73,8 @@ extension NightscoutAPI {
         return service.run(request)
             .retry(Config.retryCount)
             .decode(type: [BloodGlucose].self, decoder: JSONCoding.decoder)
-            .map {
-                $0.filter { $0.isStateValid }
+            .map { glucose in
+                glucose
                     .map {
                         var reading = $0
                         reading.glucose = $0.sgv

+ 4 - 0
FreeAPS/Sources/Services/Network/NightscoutManager.swift

@@ -58,6 +58,10 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
 
         let since = glucoseStorage.syncDate()
         return nightscout.fetchLastGlucose(288, sinceDate: since)
+            .tryCatch({ (error) -> AnyPublisher<[BloodGlucose], Error> in
+                print(error.localizedDescription)
+                return Just([]).setFailureType(to: Error.self).eraseToAnyPublisher()
+            })
             .replaceError(with: [])
             .map {
                 self.glucoseStorage.storeGlucose($0)