소스 검색

Add charts

Yakov Karpov 5 년 전
부모
커밋
6b00b1dcf1
27개의 변경된 파일755개의 추가작업 그리고 0개의 파일을 삭제
  1. 176 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. 20 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. 18 0
      FreeAPS/Sources/Charts/Helpers/getGlucoseArrowImage().swift
  10. 14 0
      FreeAPS/Sources/Charts/Helpers/getPredictionColor().swift
  11. 10 0
      FreeAPS/Sources/Charts/Models/APSDataTypes.swift
  12. 36 0
      FreeAPS/Sources/Charts/Models/BloodGlucose.swift
  13. 6 0
      FreeAPS/Sources/Charts/Models/BoundTypes.swift
  14. 15 0
      FreeAPS/Sources/Charts/Models/GlucosePointData.swift
  15. 15 0
      FreeAPS/Sources/Charts/Models/InformationBarEntryData.swift
  16. 6 0
      FreeAPS/Sources/Charts/Models/MeshEntryOrientations.swift
  17. 9 0
      FreeAPS/Sources/Charts/Models/PointData.swift
  18. 15 0
      FreeAPS/Sources/Charts/Models/PredictionLineData.swift
  19. 8 0
      FreeAPS/Sources/Charts/Models/PredictionType.swift
  20. 53 0
      FreeAPS/Sources/Charts/Sample Data/SampleData.swift
  21. 94 0
      FreeAPS/Sources/Charts/Views/Charts/PointChartView.swift
  22. 12 0
      FreeAPS/Sources/Charts/Views/ChartsConfig.swift
  23. 29 0
      FreeAPS/Sources/Charts/Views/Components/GlucoseArrowView.swift
  24. 65 0
      FreeAPS/Sources/Charts/Views/Components/GlucoseInformationBarView.swift
  25. 19 0
      FreeAPS/Sources/Charts/Views/Components/HoursPickerView.swift
  26. 22 0
      FreeAPS/Sources/Charts/Views/Points/GlucosePointView.swift
  27. 21 0
      FreeAPS/Sources/Charts/Views/Points/PredictionPointView.swift

+ 176 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -204,6 +204,32 @@
 		5D16287A969E64D18CE40E44 /* PumpConfigViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F60E97100041040446F44E7 /* PumpConfigViewModel.swift */; };
 		5D16287A969E64D18CE40E44 /* PumpConfigViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F60E97100041040446F44E7 /* PumpConfigViewModel.swift */; };
 		63E890B4D951EAA91C071D5C /* BasalProfileEditorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAFF91130F2FCCC7EBBA11AD /* BasalProfileEditorViewModel.swift */; };
 		63E890B4D951EAA91C071D5C /* BasalProfileEditorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAFF91130F2FCCC7EBBA11AD /* BasalProfileEditorViewModel.swift */; };
 		642F76A05A4FF530463A9FD0 /* NightscoutConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8782B44544F38F2B2D82C38E /* NightscoutConfigRootView.swift */; };
 		642F76A05A4FF530463A9FD0 /* NightscoutConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8782B44544F38F2B2D82C38E /* NightscoutConfigRootView.swift */; };
+		6610F99125FAD535004781D7 /* SampleData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6610F97025FAD534004781D7 /* SampleData.swift */; };
+		6610F99225FAD535004781D7 /* BloodGlucose.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6610F97225FAD534004781D7 /* BloodGlucose.swift */; };
+		6610F99325FAD535004781D7 /* PointData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6610F97325FAD534004781D7 /* PointData.swift */; };
+		6610F99425FAD535004781D7 /* BoundTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6610F97425FAD534004781D7 /* BoundTypes.swift */; };
+		6610F99525FAD535004781D7 /* InformationBarEntryData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6610F97525FAD534004781D7 /* InformationBarEntryData.swift */; };
+		6610F99625FAD535004781D7 /* PredictionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6610F97625FAD534004781D7 /* PredictionType.swift */; };
+		6610F99725FAD535004781D7 /* APSDataTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6610F97725FAD534004781D7 /* APSDataTypes.swift */; };
+		6610F99825FAD535004781D7 /* GlucosePointData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6610F97825FAD534004781D7 /* GlucosePointData.swift */; };
+		6610F99925FAD535004781D7 /* PredictionLineData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6610F97925FAD534004781D7 /* PredictionLineData.swift */; };
+		6610F99A25FAD535004781D7 /* MeshEntryOrientations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6610F97A25FAD534004781D7 /* MeshEntryOrientations.swift */; };
+		6610F99B25FAD535004781D7 /* View+Modifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6610F97C25FAD534004781D7 /* View+Modifiers.swift */; };
+		6610F99C25FAD535004781D7 /* Double+getBoundGlucose().swift in Sources */ = {isa = PBXBuildFile; fileRef = 6610F97D25FAD534004781D7 /* Double+getBoundGlucose().swift */; };
+		6610F99D25FAD535004781D7 /* Array+halve().swift in Sources */ = {isa = PBXBuildFile; fileRef = 6610F97E25FAD534004781D7 /* Array+halve().swift */; };
+		6610F99E25FAD535004781D7 /* View+if().swift in Sources */ = {isa = PBXBuildFile; fileRef = 6610F97F25FAD534004781D7 /* View+if().swift */; };
+		6610F99F25FAD535004781D7 /* Date+-.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6610F98025FAD535004781D7 /* Date+-.swift */; };
+		6610F9A025FAD535004781D7 /* PointChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6610F98325FAD535004781D7 /* PointChartView.swift */; };
+		6610F9A125FAD535004781D7 /* GlucoseArrowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6610F98525FAD535004781D7 /* GlucoseArrowView.swift */; };
+		6610F9A225FAD535004781D7 /* GlucoseInformationBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6610F98625FAD535004781D7 /* GlucoseInformationBarView.swift */; };
+		6610F9A325FAD535004781D7 /* HoursPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6610F98725FAD535004781D7 /* HoursPickerView.swift */; };
+		6610F9A425FAD535004781D7 /* ChartsConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6610F98825FAD535004781D7 /* ChartsConfig.swift */; };
+		6610F9A525FAD535004781D7 /* GlucosePointView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6610F98A25FAD535004781D7 /* GlucosePointView.swift */; };
+		6610F9A625FAD535004781D7 /* PredictionPointView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6610F98B25FAD535004781D7 /* PredictionPointView.swift */; };
+		6610F9A725FAD535004781D7 /* getPredictionColor().swift in Sources */ = {isa = PBXBuildFile; fileRef = 6610F98D25FAD535004781D7 /* getPredictionColor().swift */; };
+		6610F9A825FAD535004781D7 /* getGlucoseArrowImage().swift in Sources */ = {isa = PBXBuildFile; fileRef = 6610F98E25FAD535004781D7 /* getGlucoseArrowImage().swift */; };
+		6610F9A925FAD535004781D7 /* APSDataFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6610F98F25FAD535004781D7 /* APSDataFormatter.swift */; };
+		6610F9AA25FAD535004781D7 /* getChartWidth().swift in Sources */ = {isa = PBXBuildFile; fileRef = 6610F99025FAD535004781D7 /* getChartWidth().swift */; };
 		6632A0DC746872439A858B44 /* ISFEditorDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BDA519C9B890FD9A5DFCF3 /* ISFEditorDataFlow.swift */; };
 		6632A0DC746872439A858B44 /* ISFEditorDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BDA519C9B890FD9A5DFCF3 /* ISFEditorDataFlow.swift */; };
 		69A31254F2451C20361D172F /* BolusViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 223EC0494F55A91E3EA69EF4 /* BolusViewModel.swift */; };
 		69A31254F2451C20361D172F /* BolusViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 223EC0494F55A91E3EA69EF4 /* BolusViewModel.swift */; };
 		69B9A368029F7EB39F525422 /* CREditorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA5E04A2761F6EEA6568E1 /* CREditorViewModel.swift */; };
 		69B9A368029F7EB39F525422 /* CREditorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA5E04A2761F6EEA6568E1 /* CREditorViewModel.swift */; };
@@ -469,6 +495,32 @@
 		5F48C3AC770D4CCD0EA2B0C2 /* AddCarbsDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddCarbsDataFlow.swift; sourceTree = "<group>"; };
 		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>"; };
 		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>"; };
 		64AA5E04A2761F6EEA6568E1 /* CREditorViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CREditorViewModel.swift; sourceTree = "<group>"; };
+		6610F97025FAD534004781D7 /* SampleData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SampleData.swift; sourceTree = "<group>"; };
+		6610F97225FAD534004781D7 /* BloodGlucose.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BloodGlucose.swift; sourceTree = "<group>"; };
+		6610F97325FAD534004781D7 /* PointData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PointData.swift; sourceTree = "<group>"; };
+		6610F97425FAD534004781D7 /* BoundTypes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BoundTypes.swift; sourceTree = "<group>"; };
+		6610F97525FAD534004781D7 /* InformationBarEntryData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InformationBarEntryData.swift; sourceTree = "<group>"; };
+		6610F97625FAD534004781D7 /* PredictionType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PredictionType.swift; sourceTree = "<group>"; };
+		6610F97725FAD534004781D7 /* APSDataTypes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APSDataTypes.swift; sourceTree = "<group>"; };
+		6610F97825FAD534004781D7 /* GlucosePointData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlucosePointData.swift; sourceTree = "<group>"; };
+		6610F97925FAD534004781D7 /* PredictionLineData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PredictionLineData.swift; sourceTree = "<group>"; };
+		6610F97A25FAD534004781D7 /* MeshEntryOrientations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MeshEntryOrientations.swift; sourceTree = "<group>"; };
+		6610F97C25FAD534004781D7 /* View+Modifiers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "View+Modifiers.swift"; sourceTree = "<group>"; };
+		6610F97D25FAD534004781D7 /* Double+getBoundGlucose().swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Double+getBoundGlucose().swift"; sourceTree = "<group>"; };
+		6610F97E25FAD534004781D7 /* Array+halve().swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Array+halve().swift"; sourceTree = "<group>"; };
+		6610F97F25FAD534004781D7 /* View+if().swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "View+if().swift"; sourceTree = "<group>"; };
+		6610F98025FAD535004781D7 /* Date+-.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Date+-.swift"; sourceTree = "<group>"; };
+		6610F98325FAD535004781D7 /* PointChartView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PointChartView.swift; sourceTree = "<group>"; };
+		6610F98525FAD535004781D7 /* GlucoseArrowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlucoseArrowView.swift; sourceTree = "<group>"; };
+		6610F98625FAD535004781D7 /* GlucoseInformationBarView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlucoseInformationBarView.swift; sourceTree = "<group>"; };
+		6610F98725FAD535004781D7 /* HoursPickerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HoursPickerView.swift; sourceTree = "<group>"; };
+		6610F98825FAD535004781D7 /* ChartsConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartsConfig.swift; sourceTree = "<group>"; };
+		6610F98A25FAD535004781D7 /* GlucosePointView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlucosePointView.swift; sourceTree = "<group>"; };
+		6610F98B25FAD535004781D7 /* PredictionPointView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PredictionPointView.swift; sourceTree = "<group>"; };
+		6610F98D25FAD535004781D7 /* getPredictionColor().swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "getPredictionColor().swift"; sourceTree = "<group>"; };
+		6610F98E25FAD535004781D7 /* getGlucoseArrowImage().swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "getGlucoseArrowImage().swift"; sourceTree = "<group>"; };
+		6610F98F25FAD535004781D7 /* APSDataFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APSDataFormatter.swift; sourceTree = "<group>"; };
+		6610F99025FAD535004781D7 /* getChartWidth().swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "getChartWidth().swift"; sourceTree = "<group>"; };
 		67F94DD2853CF42BA4E30616 /* BasalProfileEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorDataFlow.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>"; };
 		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>"; };
 		6BA56D2DCAB9E0A8AF24D984 /* BasalProfileEditorBuilder.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorBuilder.swift; sourceTree = "<group>"; };
@@ -621,6 +673,7 @@
 				3811DF0A25CAAAA500A708ED /* APS */,
 				3811DF0A25CAAAA500A708ED /* APS */,
 				38E98A3225F5300800C0CED0 /* Config */,
 				38E98A3225F5300800C0CED0 /* Config */,
 				3811DEBD25C9D99900A708ED /* Containers */,
 				3811DEBD25C9D99900A708ED /* Containers */,
+				6610F96E25FAD534004781D7 /* Charts */,
 				388E5A5A25B6F05F0019842D /* Helpers */,
 				388E5A5A25B6F05F0019842D /* Helpers */,
 				38E98A1A25F52C9300C0CED0 /* Logger */,
 				38E98A1A25F52C9300C0CED0 /* Logger */,
 				388E5A5925B6F0250019842D /* Models */,
 				388E5A5925B6F0250019842D /* Models */,
@@ -1218,6 +1271,103 @@
 			path = TargetsEditor;
 			path = TargetsEditor;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
 		};
 		};
+		6610F96E25FAD534004781D7 /* Charts */ = {
+			isa = PBXGroup;
+			children = (
+				6610F96F25FAD534004781D7 /* Sample Data */,
+				6610F97125FAD534004781D7 /* Models */,
+				6610F97B25FAD534004781D7 /* Extensions */,
+				6610F98125FAD535004781D7 /* Views */,
+				6610F98C25FAD535004781D7 /* Helpers */,
+			);
+			path = Charts;
+			sourceTree = "<group>";
+		};
+		6610F96F25FAD534004781D7 /* Sample Data */ = {
+			isa = PBXGroup;
+			children = (
+				6610F97025FAD534004781D7 /* SampleData.swift */,
+			);
+			path = "Sample Data";
+			sourceTree = "<group>";
+		};
+		6610F97125FAD534004781D7 /* Models */ = {
+			isa = PBXGroup;
+			children = (
+				6610F97225FAD534004781D7 /* BloodGlucose.swift */,
+				6610F97325FAD534004781D7 /* PointData.swift */,
+				6610F97425FAD534004781D7 /* BoundTypes.swift */,
+				6610F97525FAD534004781D7 /* InformationBarEntryData.swift */,
+				6610F97625FAD534004781D7 /* PredictionType.swift */,
+				6610F97725FAD534004781D7 /* APSDataTypes.swift */,
+				6610F97825FAD534004781D7 /* GlucosePointData.swift */,
+				6610F97925FAD534004781D7 /* PredictionLineData.swift */,
+				6610F97A25FAD534004781D7 /* MeshEntryOrientations.swift */,
+			);
+			path = Models;
+			sourceTree = "<group>";
+		};
+		6610F97B25FAD534004781D7 /* Extensions */ = {
+			isa = PBXGroup;
+			children = (
+				6610F97C25FAD534004781D7 /* View+Modifiers.swift */,
+				6610F97D25FAD534004781D7 /* Double+getBoundGlucose().swift */,
+				6610F97E25FAD534004781D7 /* Array+halve().swift */,
+				6610F97F25FAD534004781D7 /* View+if().swift */,
+				6610F98025FAD535004781D7 /* Date+-.swift */,
+			);
+			path = Extensions;
+			sourceTree = "<group>";
+		};
+		6610F98125FAD535004781D7 /* Views */ = {
+			isa = PBXGroup;
+			children = (
+				6610F98225FAD535004781D7 /* Charts */,
+				6610F98425FAD535004781D7 /* Components */,
+				6610F98825FAD535004781D7 /* ChartsConfig.swift */,
+				6610F98925FAD535004781D7 /* Points */,
+			);
+			path = Views;
+			sourceTree = "<group>";
+		};
+		6610F98225FAD535004781D7 /* Charts */ = {
+			isa = PBXGroup;
+			children = (
+				6610F98325FAD535004781D7 /* PointChartView.swift */,
+			);
+			path = Charts;
+			sourceTree = "<group>";
+		};
+		6610F98425FAD535004781D7 /* Components */ = {
+			isa = PBXGroup;
+			children = (
+				6610F98525FAD535004781D7 /* GlucoseArrowView.swift */,
+				6610F98625FAD535004781D7 /* GlucoseInformationBarView.swift */,
+				6610F98725FAD535004781D7 /* HoursPickerView.swift */,
+			);
+			path = Components;
+			sourceTree = "<group>";
+		};
+		6610F98925FAD535004781D7 /* Points */ = {
+			isa = PBXGroup;
+			children = (
+				6610F98A25FAD535004781D7 /* GlucosePointView.swift */,
+				6610F98B25FAD535004781D7 /* PredictionPointView.swift */,
+			);
+			path = Points;
+			sourceTree = "<group>";
+		};
+		6610F98C25FAD535004781D7 /* Helpers */ = {
+			isa = PBXGroup;
+			children = (
+				6610F98D25FAD535004781D7 /* getPredictionColor().swift */,
+				6610F98E25FAD535004781D7 /* getGlucoseArrowImage().swift */,
+				6610F98F25FAD535004781D7 /* APSDataFormatter.swift */,
+				6610F99025FAD535004781D7 /* getChartWidth().swift */,
+			);
+			path = Helpers;
+			sourceTree = "<group>";
+		};
 		6DC5D590658EF8B8DF94F9F5 /* AddCarbs */ = {
 		6DC5D590658EF8B8DF94F9F5 /* AddCarbs */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
@@ -1497,14 +1647,20 @@
 				3811DEEB25CA063400A708ED /* PersistedProperty.swift in Sources */,
 				3811DEEB25CA063400A708ED /* PersistedProperty.swift in Sources */,
 				388E5A6025B6F2310019842D /* Autosens.swift in Sources */,
 				388E5A6025B6F2310019842D /* Autosens.swift in Sources */,
 				3811DE8B25C9D6DD00A708ED /* RequestPermissionsBuilder.swift in Sources */,
 				3811DE8B25C9D6DD00A708ED /* RequestPermissionsBuilder.swift in Sources */,
+				6610F99325FAD535004781D7 /* PointData.swift in Sources */,
 				3811DE4C25C9D4B800A708ED /* AuthotizedRootBuilder.swift in Sources */,
 				3811DE4C25C9D4B800A708ED /* AuthotizedRootBuilder.swift in Sources */,
 				3811DE8F25C9D80400A708ED /* User.swift in Sources */,
 				3811DE8F25C9D80400A708ED /* User.swift in Sources */,
+				6610F99F25FAD535004781D7 /* Date+-.swift in Sources */,
+				6610F9AA25FAD535004781D7 /* getChartWidth().swift in Sources */,
 				3811DEB225C9D88300A708ED /* KeychainItemAccessibility.swift in Sources */,
 				3811DEB225C9D88300A708ED /* KeychainItemAccessibility.swift in Sources */,
+				6610F99425FAD535004781D7 /* BoundTypes.swift in Sources */,
 				385CEAC425F2F154002D6D5B /* AnnouncementsStorage.swift in Sources */,
 				385CEAC425F2F154002D6D5B /* AnnouncementsStorage.swift in Sources */,
 				38AEE73D25F0200C0013F05B /* FreeAPSSettings.swift in Sources */,
 				38AEE73D25F0200C0013F05B /* FreeAPSSettings.swift in Sources */,
 				38FCF3FD25E997A80078B0D1 /* PumpHistoryStorage.swift in Sources */,
 				38FCF3FD25E997A80078B0D1 /* PumpHistoryStorage.swift in Sources */,
 				38D0B3B625EBE24900CB6E88 /* Battery.swift in Sources */,
 				38D0B3B625EBE24900CB6E88 /* Battery.swift in Sources */,
+				6610F9A425FAD535004781D7 /* ChartsConfig.swift in Sources */,
 				38C4D33725E9A1A300D30B77 /* DispatchQueue+Extensions.swift in Sources */,
 				38C4D33725E9A1A300D30B77 /* DispatchQueue+Extensions.swift in Sources */,
+				6610F9A525FAD535004781D7 /* GlucosePointView.swift in Sources */,
 				3811DE6B25C9D62600A708ED /* OnboardingProvider.swift in Sources */,
 				3811DE6B25C9D62600A708ED /* OnboardingProvider.swift in Sources */,
 				38B4F3C325E2A20B00E76A18 /* PumpSetupView.swift in Sources */,
 				38B4F3C325E2A20B00E76A18 /* PumpSetupView.swift in Sources */,
 				382C134B25F14E3700715CE1 /* BGTargets.swift in Sources */,
 				382C134B25F14E3700715CE1 /* BGTargets.swift in Sources */,
@@ -1514,6 +1670,7 @@
 				38AEE75225F022080013F05B /* SettingsManager.swift in Sources */,
 				38AEE75225F022080013F05B /* SettingsManager.swift in Sources */,
 				38B4F3CF25E5041600E76A18 /* APSContainer.swift in Sources */,
 				38B4F3CF25E5041600E76A18 /* APSContainer.swift in Sources */,
 				3895E4C625B9E00D00214B37 /* Preferences.swift in Sources */,
 				3895E4C625B9E00D00214B37 /* Preferences.swift in Sources */,
+				6610F99525FAD535004781D7 /* InformationBarEntryData.swift in Sources */,
 				3811DE6E25C9D62600A708ED /* OnboardingViewModel.swift in Sources */,
 				3811DE6E25C9D62600A708ED /* OnboardingViewModel.swift in Sources */,
 				38B4F3CD25E5031100E76A18 /* Broadcaster.swift in Sources */,
 				38B4F3CD25E5031100E76A18 /* Broadcaster.swift in Sources */,
 				3811DE6D25C9D62600A708ED /* OnboardingRootView.swift in Sources */,
 				3811DE6D25C9D62600A708ED /* OnboardingRootView.swift in Sources */,
@@ -1522,10 +1679,12 @@
 				38BF021725E7CBBC00579895 /* PumpManagerExtensions.swift in Sources */,
 				38BF021725E7CBBC00579895 /* PumpManagerExtensions.swift in Sources */,
 				38F3B2EF25ED8E2A005C48AA /* TempTargetsStorage.swift in Sources */,
 				38F3B2EF25ED8E2A005C48AA /* TempTargetsStorage.swift in Sources */,
 				3811DF1025CAAAE200A708ED /* APSManager.swift in Sources */,
 				3811DF1025CAAAE200A708ED /* APSManager.swift in Sources */,
+				6610F9A025FAD535004781D7 /* PointChartView.swift in Sources */,
 				3870FF4725EC187A0088248F /* BloodGlucose.swift in Sources */,
 				3870FF4725EC187A0088248F /* BloodGlucose.swift in Sources */,
 				3811DE0A25C9D32F00A708ED /* BaseModuleBuilder.swift in Sources */,
 				3811DE0A25C9D32F00A708ED /* BaseModuleBuilder.swift in Sources */,
 				38A0364225ED069400FCBB52 /* TempBasal.swift in Sources */,
 				38A0364225ED069400FCBB52 /* TempBasal.swift in Sources */,
 				3811DE1725C9D40400A708ED /* Screen.swift in Sources */,
 				3811DE1725C9D40400A708ED /* Screen.swift in Sources */,
+				6610F9A325FAD535004781D7 /* HoursPickerView.swift in Sources */,
 				383948DA25CD64D500E91849 /* Glucose.swift in Sources */,
 				383948DA25CD64D500E91849 /* Glucose.swift in Sources */,
 				388E596C25AD95110019842D /* OpenAPS.swift in Sources */,
 				388E596C25AD95110019842D /* OpenAPS.swift in Sources */,
 				384E803825C388640086DB71 /* Script.swift in Sources */,
 				384E803825C388640086DB71 /* Script.swift in Sources */,
@@ -1536,9 +1695,12 @@
 				3811DE7B25C9D6D300A708ED /* LoginProvider.swift in Sources */,
 				3811DE7B25C9D6D300A708ED /* LoginProvider.swift in Sources */,
 				383948D625CD4D8900E91849 /* FileStorage.swift in Sources */,
 				383948D625CD4D8900E91849 /* FileStorage.swift in Sources */,
 				389FE32A25F3AC44002E92E0 /* GlucoseChartView.swift in Sources */,
 				389FE32A25F3AC44002E92E0 /* GlucoseChartView.swift in Sources */,
+				6610F99125FAD535004781D7 /* SampleData.swift in Sources */,
 				3811DE4125C9D4A100A708ED /* SettingsRootView.swift in Sources */,
 				3811DE4125C9D4A100A708ED /* SettingsRootView.swift in Sources */,
 				388E595C25AD948C0019842D /* FreeAPSApp.swift in Sources */,
 				388E595C25AD948C0019842D /* FreeAPSApp.swift in Sources */,
 				3811DE8925C9D6DD00A708ED /* RequestPermissionsProvider.swift in Sources */,
 				3811DE8925C9D6DD00A708ED /* RequestPermissionsProvider.swift in Sources */,
+				6610F99925FAD535004781D7 /* PredictionLineData.swift in Sources */,
+				6610F99225FAD535004781D7 /* BloodGlucose.swift in Sources */,
 				3811DE4225C9D4A100A708ED /* SettingsDataFlow.swift in Sources */,
 				3811DE4225C9D4A100A708ED /* SettingsDataFlow.swift in Sources */,
 				3811DE8825C9D6DD00A708ED /* RequestPermissionsDataFlow.swift in Sources */,
 				3811DE8825C9D6DD00A708ED /* RequestPermissionsDataFlow.swift in Sources */,
 				3811DE8A25C9D6DD00A708ED /* RequestPermissionsRootView.swift in Sources */,
 				3811DE8A25C9D6DD00A708ED /* RequestPermissionsRootView.swift in Sources */,
@@ -1547,7 +1709,9 @@
 				3811DE3125C9D49500A708ED /* HomeProvider.swift in Sources */,
 				3811DE3125C9D49500A708ED /* HomeProvider.swift in Sources */,
 				388E5A5C25B6F0770019842D /* JSON.swift in Sources */,
 				388E5A5C25B6F0770019842D /* JSON.swift in Sources */,
 				3811DF0225CA9FEA00A708ED /* Credentials.swift in Sources */,
 				3811DF0225CA9FEA00A708ED /* Credentials.swift in Sources */,
+				6610F9A625FAD535004781D7 /* PredictionPointView.swift in Sources */,
 				38B4F3C625E5017E00E76A18 /* NotificationCenter.swift in Sources */,
 				38B4F3C625E5017E00E76A18 /* NotificationCenter.swift in Sources */,
+				6610F9A925FAD535004781D7 /* APSDataFormatter.swift in Sources */,
 				3811DEB625C9D88300A708ED /* UnlockManager.swift in Sources */,
 				3811DEB625C9D88300A708ED /* UnlockManager.swift in Sources */,
 				38A13D3225E28B4B00EAA382 /* PumpHistoryEvent.swift in Sources */,
 				38A13D3225E28B4B00EAA382 /* PumpHistoryEvent.swift in Sources */,
 				3811DE1825C9D40400A708ED /* Router.swift in Sources */,
 				3811DE1825C9D40400A708ED /* Router.swift in Sources */,
@@ -1571,10 +1735,12 @@
 				3811DEAD25C9D88300A708ED /* UserDefaults+Cache.swift in Sources */,
 				3811DEAD25C9D88300A708ED /* UserDefaults+Cache.swift in Sources */,
 				3811DE2225C9D48300A708ED /* MainProvider.swift in Sources */,
 				3811DE2225C9D48300A708ED /* MainProvider.swift in Sources */,
 				3811DE0C25C9D32F00A708ED /* BaseProvider.swift in Sources */,
 				3811DE0C25C9D32F00A708ED /* BaseProvider.swift in Sources */,
+				6610F99625FAD535004781D7 /* PredictionType.swift in Sources */,
 				3811DE5C25C9D4D500A708ED /* Formatters.swift in Sources */,
 				3811DE5C25C9D4D500A708ED /* Formatters.swift in Sources */,
 				3811DEC525C9D99900A708ED /* StorageContainer.swift in Sources */,
 				3811DEC525C9D99900A708ED /* StorageContainer.swift in Sources */,
 				3871F39F25ED895A0013ECB5 /* Decimal+Extensions.swift in Sources */,
 				3871F39F25ED895A0013ECB5 /* Decimal+Extensions.swift in Sources */,
 				3811DE7F25C9D6D300A708ED /* LoginBuilder.swift in Sources */,
 				3811DE7F25C9D6D300A708ED /* LoginBuilder.swift in Sources */,
+				6610F99D25FAD535004781D7 /* Array+halve().swift in Sources */,
 				3811DE3525C9D49500A708ED /* HomeRootView.swift in Sources */,
 				3811DE3525C9D49500A708ED /* HomeRootView.swift in Sources */,
 				3811DEC325C9D99900A708ED /* UIContainer.swift in Sources */,
 				3811DEC325C9D99900A708ED /* UIContainer.swift in Sources */,
 				38E98A2925F52C9300C0CED0 /* Error+Extensions.swift in Sources */,
 				38E98A2925F52C9300C0CED0 /* Error+Extensions.swift in Sources */,
@@ -1589,6 +1755,7 @@
 				3811DE7925C9D6D300A708ED /* LoginViewModel.swift in Sources */,
 				3811DE7925C9D6D300A708ED /* LoginViewModel.swift in Sources */,
 				3871F39C25ED892B0013ECB5 /* TempTarget.swift in Sources */,
 				3871F39C25ED892B0013ECB5 /* TempTarget.swift in Sources */,
 				3811DEAB25C9D88300A708ED /* HTTPResponseStatus.swift in Sources */,
 				3811DEAB25C9D88300A708ED /* HTTPResponseStatus.swift in Sources */,
+				6610F99B25FAD535004781D7 /* View+Modifiers.swift in Sources */,
 				3811DE5F25C9D4D500A708ED /* ProgressBar.swift in Sources */,
 				3811DE5F25C9D4D500A708ED /* ProgressBar.swift in Sources */,
 				38BF021D25E7E3AF00579895 /* Reservoir.swift in Sources */,
 				38BF021D25E7E3AF00579895 /* Reservoir.swift in Sources */,
 				38BF021B25E7D06400579895 /* PumpSettingsView.swift in Sources */,
 				38BF021B25E7D06400579895 /* PumpSettingsView.swift in Sources */,
@@ -1603,6 +1770,7 @@
 				3811DE2425C9D48300A708ED /* MainViewModel.swift in Sources */,
 				3811DE2425C9D48300A708ED /* MainViewModel.swift in Sources */,
 				3811DE3F25C9D4A100A708ED /* SettingsViewModel.swift in Sources */,
 				3811DE3F25C9D4A100A708ED /* SettingsViewModel.swift in Sources */,
 				38B4F3CB25E502E200E76A18 /* WeakObjectSet.swift in Sources */,
 				38B4F3CB25E502E200E76A18 /* WeakObjectSet.swift in Sources */,
+				6610F9A125FAD535004781D7 /* GlucoseArrowView.swift in Sources */,
 				3811DEB725C9D88300A708ED /* AuthorizationManager.swift in Sources */,
 				3811DEB725C9D88300A708ED /* AuthorizationManager.swift in Sources */,
 				38E989DD25F5021400C0CED0 /* PumpStatus.swift in Sources */,
 				38E989DD25F5021400C0CED0 /* PumpStatus.swift in Sources */,
 				38E98A2525F52C9300C0CED0 /* IssueReporter.swift in Sources */,
 				38E98A2525F52C9300C0CED0 /* IssueReporter.swift in Sources */,
@@ -1613,6 +1781,7 @@
 				3811DEC425C9D99900A708ED /* NetworkContainer.swift in Sources */,
 				3811DEC425C9D99900A708ED /* NetworkContainer.swift in Sources */,
 				3811DE4325C9D4A100A708ED /* SettingsProvider.swift in Sources */,
 				3811DE4325C9D4A100A708ED /* SettingsProvider.swift in Sources */,
 				E102DE9C3E9C8AEDCB3C61BB /* ConfigEditorBuilder.swift in Sources */,
 				E102DE9C3E9C8AEDCB3C61BB /* ConfigEditorBuilder.swift in Sources */,
+				6610F99725FAD535004781D7 /* APSDataTypes.swift in Sources */,
 				45252C95D220E796FDB3B022 /* ConfigEditorDataFlow.swift in Sources */,
 				45252C95D220E796FDB3B022 /* ConfigEditorDataFlow.swift in Sources */,
 				3871F38725ED661C0013ECB5 /* Suggestion.swift in Sources */,
 				3871F38725ED661C0013ECB5 /* Suggestion.swift in Sources */,
 				38C4D33A25E9A1ED00D30B77 /* NSObject+AssociatedValues.swift in Sources */,
 				38C4D33A25E9A1ED00D30B77 /* NSObject+AssociatedValues.swift in Sources */,
@@ -1647,15 +1816,19 @@
 				38E98A2425F52C9300C0CED0 /* Logger.swift in Sources */,
 				38E98A2425F52C9300C0CED0 /* Logger.swift in Sources */,
 				CA370FC152BC98B3D1832968 /* BasalProfileEditorRootView.swift in Sources */,
 				CA370FC152BC98B3D1832968 /* BasalProfileEditorRootView.swift in Sources */,
 				F215CAB49BA4B5A01C3BC6B6 /* ISFEditorBuilder.swift in Sources */,
 				F215CAB49BA4B5A01C3BC6B6 /* ISFEditorBuilder.swift in Sources */,
+				6610F99C25FAD535004781D7 /* Double+getBoundGlucose().swift in Sources */,
 				6632A0DC746872439A858B44 /* ISFEditorDataFlow.swift in Sources */,
 				6632A0DC746872439A858B44 /* ISFEditorDataFlow.swift in Sources */,
 				DBA5254DBB2586C98F61220C /* ISFEditorProvider.swift in Sources */,
 				DBA5254DBB2586C98F61220C /* ISFEditorProvider.swift in Sources */,
 				1BBB001DAD60F3B8CEA4B1C7 /* ISFEditorViewModel.swift in Sources */,
 				1BBB001DAD60F3B8CEA4B1C7 /* ISFEditorViewModel.swift in Sources */,
 				88AB39B23C9552BD6E0C9461 /* ISFEditorRootView.swift in Sources */,
 				88AB39B23C9552BD6E0C9461 /* ISFEditorRootView.swift in Sources */,
 				3BD663A04B4CA5278B0260B4 /* CREditorBuilder.swift in Sources */,
 				3BD663A04B4CA5278B0260B4 /* CREditorBuilder.swift in Sources */,
 				A33352ED40476125EBAC6EE0 /* CREditorDataFlow.swift in Sources */,
 				A33352ED40476125EBAC6EE0 /* CREditorDataFlow.swift in Sources */,
+				6610F9A825FAD535004781D7 /* getGlucoseArrowImage().swift in Sources */,
 				17A9D0899046B45E87834820 /* CREditorProvider.swift in Sources */,
 				17A9D0899046B45E87834820 /* CREditorProvider.swift in Sources */,
 				69B9A368029F7EB39F525422 /* CREditorViewModel.swift in Sources */,
 				69B9A368029F7EB39F525422 /* CREditorViewModel.swift in Sources */,
+				6610F99825FAD535004781D7 /* GlucosePointData.swift in Sources */,
 				98641AF4F92123DA668AB931 /* CREditorRootView.swift in Sources */,
 				98641AF4F92123DA668AB931 /* CREditorRootView.swift in Sources */,
+				6610F9A225FAD535004781D7 /* GlucoseInformationBarView.swift in Sources */,
 				7F7017AA5C69838FB7E6FECE /* TargetsEditorBuilder.swift in Sources */,
 				7F7017AA5C69838FB7E6FECE /* TargetsEditorBuilder.swift in Sources */,
 				38E98A2325F52C9300C0CED0 /* Signpost.swift in Sources */,
 				38E98A2325F52C9300C0CED0 /* Signpost.swift in Sources */,
 				F5F7E6C1B7F098F59EB67EC5 /* TargetsEditorDataFlow.swift in Sources */,
 				F5F7E6C1B7F098F59EB67EC5 /* TargetsEditorDataFlow.swift in Sources */,
@@ -1673,12 +1846,15 @@
 				33E198D3039045D98C3DC5D4 /* AddCarbsViewModel.swift in Sources */,
 				33E198D3039045D98C3DC5D4 /* AddCarbsViewModel.swift in Sources */,
 				28089E07169488CF6DCC2A31 /* AddCarbsRootView.swift in Sources */,
 				28089E07169488CF6DCC2A31 /* AddCarbsRootView.swift in Sources */,
 				46159F33C01CDBF822B4F1F8 /* AddTempTargetBuilder.swift in Sources */,
 				46159F33C01CDBF822B4F1F8 /* AddTempTargetBuilder.swift in Sources */,
+				6610F9A725FAD535004781D7 /* getPredictionColor().swift in Sources */,
+				6610F99E25FAD535004781D7 /* View+if().swift in Sources */,
 				D2165E9D78EFF692C1DED1C6 /* AddTempTargetDataFlow.swift in Sources */,
 				D2165E9D78EFF692C1DED1C6 /* AddTempTargetDataFlow.swift in Sources */,
 				5BFA1C2208114643B77F8CEB /* AddTempTargetProvider.swift in Sources */,
 				5BFA1C2208114643B77F8CEB /* AddTempTargetProvider.swift in Sources */,
 				919DBD08F13BAFB180DF6F47 /* AddTempTargetViewModel.swift in Sources */,
 				919DBD08F13BAFB180DF6F47 /* AddTempTargetViewModel.swift in Sources */,
 				8BC2F5A29AD1ED08AC0EE013 /* AddTempTargetRootView.swift in Sources */,
 				8BC2F5A29AD1ED08AC0EE013 /* AddTempTargetRootView.swift in Sources */,
 				19434C14DF3F4816F4E4BF2E /* BolusBuilder.swift in Sources */,
 				19434C14DF3F4816F4E4BF2E /* BolusBuilder.swift in Sources */,
 				041D1E995A6AE92E9289DC49 /* BolusDataFlow.swift in Sources */,
 				041D1E995A6AE92E9289DC49 /* BolusDataFlow.swift in Sources */,
+				6610F99A25FAD535004781D7 /* MeshEntryOrientations.swift in Sources */,
 				23888883D4EA091C88480FF2 /* BolusProvider.swift in Sources */,
 				23888883D4EA091C88480FF2 /* BolusProvider.swift in Sources */,
 				38E98A2D25F52DC400C0CED0 /* NSLocking+Extensions.swift in Sources */,
 				38E98A2D25F52DC400C0CED0 /* NSLocking+Extensions.swift in Sources */,
 				69A31254F2451C20361D172F /* BolusViewModel.swift in Sources */,
 				69A31254F2451C20361D172F /* BolusViewModel.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) ? self.max() : self.min() else {
+            return bound
+        }
+        if (boundType == .top) ? extremum < bound : extremum > bound {
+            return bound
+        }
+        return extremum
+    }
+}

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

@@ -0,0 +1,20 @@
+import SwiftUI
+
+private enum Config {
+    static let defaultCornerRadius: CGFloat = 10
+}
+
+private struct InformationBarEntryModifier: ViewModifier {
+    func body(content: Content) -> some View {
+        content
+            .frame(maxWidth: .infinity, maxHeight: .infinity)
+            .background(Color(.systemGray6))
+            .cornerRadius(Config.defaultCornerRadius)
+    }
+}
+
+internal 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
+}

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

@@ -0,0 +1,18 @@
+import SwiftUI
+
+func getGlucoseArrowImage(for delta: Double) -> Image {
+    let arrowName: String
+    switch delta {
+    case ..<(-0.6):
+        arrowName = "arrow.down"
+    case -0.6 ... (-0.1):
+        arrowName = "arrow.down.forward"
+    case 0.1 ..< 0.6:
+        arrowName = "arrow.up.forward"
+    case 0.6...:
+        arrowName = "arrow.up"
+    default:
+        arrowName = "arrow.forward"
+    }
+    return Image(systemName: arrowName)
+}

+ 14 - 0
FreeAPS/Sources/Charts/Helpers/getPredictionColor().swift

@@ -0,0 +1,14 @@
+import SwiftUI
+
+func getPredictionColor(for type: PredictionType) -> Color {
+    switch type {
+    case .IOB:
+        return Color(.systemTeal)
+    case .COB:
+        return Color(.systemOrange)
+    case .ZT:
+        return Color(.systemPink)
+    case .UAM:
+        return Color(.systemIndigo)
+    }
+}

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

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

+ 36 - 0
FreeAPS/Sources/Charts/Models/BloodGlucose.swift

@@ -0,0 +1,36 @@
+//
+//  BloodGlucose.swift
+//  free-aps-charts-final
+//
+//  Created by Яков Карпов on 12.03.2021.
+//
+
+import Foundation
+
+struct BloodGlucose {
+    enum Direction: String {
+        case tripleUp = "TripleUp"
+        case doubleUp = "DoubleUp"
+        case singleUp = "SingleUp"
+        case fortyFiveUp = "FortyFiveUp"
+        case flat = "Flat"
+        case fortyFiveDown = "FortyFiveDown"
+        case singleDown = "SingleDown"
+        case doubleDown = "DoubleDown"
+        case tripleDown = "TripleDown"
+        case none = "NONE"
+        case notComputable = "NOT COMPUTABLE"
+        case rateOutOfRange = "RATE OUT OF RANGE"
+    }
+
+    var sgv: Int?
+    let direction: Direction?
+    let date: UInt64
+    let dateString: Date
+    let filtered: Double?
+    let noise: Int?
+
+    var glucose: Int?
+
+    var isStateValid: Bool { sgv ?? 0 >= 39 && noise ?? 1 != 4 }
+}

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

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

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

@@ -0,0 +1,15 @@
+import SwiftUI
+
+public struct GlucosePointData: PointData {
+    public init(id: UUID = UUID(), value: Int? = nil, xPosition: CGFloat, yPosition: CGFloat? = nil) {
+        self.id = id
+        self.value = value
+        self.xPosition = xPosition
+        self.yPosition = yPosition
+    }
+
+    public var id = UUID()
+    public let value: Int?
+    public let xPosition: CGFloat
+    public let yPosition: CGFloat?
+}

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

@@ -0,0 +1,15 @@
+import Foundation
+
+public struct InformationBarEntryData: Identifiable, Hashable {
+    public init(id: UUID = UUID(), label: String, type: APSDataTypes, value: Double) {
+        self.id = id
+        self.label = label
+        self.value = value
+        self.type = type
+    }
+
+    public 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
+}

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

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

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

@@ -0,0 +1,15 @@
+import Foundation
+
+public struct PredictionLineData: Identifiable, Hashable {
+    public init(id: UUID = UUID(), type: PredictionType, values: [Double]) {
+        self.id = id
+        self.type = type
+        self.values = values
+    }
+
+    public var id = UUID()
+    let type: PredictionType
+    let values: [Double]
+
+    func max() -> Int { values.count }
+}

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

@@ -0,0 +1,8 @@
+import SwiftUI
+
+public enum PredictionType: String {
+    case IOB
+    case COB
+    case ZT
+    case UAM
+}

+ 53 - 0
FreeAPS/Sources/Charts/Sample Data/SampleData.swift

@@ -0,0 +1,53 @@
+//
+//  SampleData.swift
+//  FreeAPSCharts
+//
+//  Created by Яков Карпов on 08.03.2021.
+//
+
+import Foundation
+
+enum SampleData {
+    
+    static private var baseComponents: DateComponents = {
+        var dateComponents = DateComponents()
+        dateComponents.year = 2021
+        dateComponents.month = 3
+        dateComponents.day = 8
+        dateComponents.timeZone = TimeZone(abbreviation: "MSK")
+        dateComponents.hour = 8
+        return dateComponents
+    }()
+    
+    static private func generateDate(baseDateComponents: DateComponents, minutes: Int) -> Date {
+        var localComponents = baseDateComponents
+        localComponents.minute = minutes
+        let userCalendar = Calendar(identifier: .gregorian)
+        return userCalendar.date(from: localComponents)!
+    }
+    
+    static private func generateGlucoseStream(startingPoint: Int, length: Int, amount: Int, direction: Int) -> [Int] {
+        
+        // Downwards
+        if direction == 0 {
+            return (1 ... length).map { startingPoint - $0 * amount }
+        }
+        return (1 ... length).map { startingPoint + $0 * amount }
+    }
+    
+    static var sampleData: [BloodGlucose] {
+        let mediumUp = generateGlucoseStream(startingPoint: 77, length: 80, amount: 2, direction: 1)
+        let fastUp = generateGlucoseStream(startingPoint: mediumUp.last!, length: 20, amount: 5, direction: 1)
+        let fastDown = generateGlucoseStream(startingPoint: fastUp.last!, length: 12, amount: 13, direction: 0)
+        let mediumDown = generateGlucoseStream(startingPoint: fastDown.last!, length: 33, amount: 3, direction: 0)
+        let slowUp = generateGlucoseStream(startingPoint: mediumDown.last!, length: 144, amount: 1, direction: 1)
+        
+        let glucose = mediumUp + fastUp + fastDown + mediumDown + slowUp
+        let normalTime = (0 ... 278).map{ generateDate(baseDateComponents: baseComponents, minutes: $0 * 5) }
+        let tenMinutes = (279 ... 288).map{ generateDate(baseDateComponents: baseComponents, minutes: $0 * 10) }
+        let time = normalTime + tenMinutes
+        
+        
+        return zip(glucose, time).map { BloodGlucose(sgv: $0, direction: nil, date: UInt64($1.timeIntervalSince1970), dateString: $1, filtered: nil, noise: nil, glucose: nil) }
+    }
+}

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

@@ -0,0 +1,94 @@
+import SwiftUI
+
+struct PointChartView<PointEntry: View>: View {
+    let width: CGFloat
+    let showHours: Int
+    let glucoseData: [BloodGlucose]
+    let pointEntry: (_: Int?) -> PointEntry
+
+    public init(
+        width: CGFloat,
+        showHours: Int,
+        glucoseData: [BloodGlucose],
+        point: @escaping (_: Int?) -> PointEntry
+    ) {
+        self.width = width
+        self.showHours = showHours
+        self.glucoseData = glucoseData
+        pointEntry = point
+    }
+
+    public var body: some View {
+        GeometryReader { geometry in
+            ForEach(
+                getGlucosePoints(
+                    data: glucoseData,
+                    height: geometry.size.height,
+                    width: width,
+                    showHours: showHours),
+                id: \.self
+            ) { point in
+                pointEntry(point.value)
+                    .position(x: point.xPosition, y: point.yPosition ?? 0)
+            }
+        }
+        .frame(width: 1000)
+    }
+}
+
+private func getGlucosePoints(
+    data: [BloodGlucose],
+    height: CGFloat,
+    width: CGFloat,
+    showHours: Int
+) -> [GlucosePointData] {
+    let values = data.compactMap { $0.sgv }
+    
+    let maxValue = values.max() ?? 180
+    let minValue = values.min() ?? 60
+    let firstEntryTime = data
+        .compactMap { $0.date }
+        .first ?? UInt64(Date().timeIntervalSince1970)
+    
+    let _ = width / CGFloat(60 * 60 * showHours)
+
+    /// y = mx + b where m = scalingFactor, b = addendum, x = value, y = mapped value
+    let scalingFactor = Double(height) / Double(maxValue - minValue)
+    let addendum = scalingFactor * Double(maxValue)
+    let pointSize: CGFloat = ChartsConfig.glucosePointSize / 2
+    let hoursMultiplier: Double = 12
+
+    return data.map { glucose in
+        let xPosition = (CGFloat(0) * width / CGFloat(Double(showHours) * hoursMultiplier)) + pointSize
+        
+        guard let value = glucose.sgv else {
+            return GlucosePointData(
+                xPosition: xPosition
+            )
+        }
+        return GlucosePointData(
+            value: value,
+            xPosition: xPosition,
+            yPosition: CGFloat(-scalingFactor * Double(value) + addendum)
+        )
+    }
+}
+
+struct PointChartView_Previews: PreviewProvider {
+    
+    static let data = Array(SampleData.sampleData.prefix(10))
+
+    static var previews: some View {
+        ScrollView(.horizontal) {
+            PointChartView(
+                width: 500,
+                showHours: 1,
+                glucoseData: data
+            ) { value in
+                GlucosePointView(value: value)
+            }
+            .background(Color.gray)
+        }
+        .preferredColorScheme(/*@START_MENU_TOKEN@*/ .dark/*@END_MENU_TOKEN@*/)
+    }
+}

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

@@ -0,0 +1,12 @@
+//
+//  Config.swift
+//  free-aps-charts-final
+//
+//  Created by Яков Карпов on 12.03.2021.
+//
+
+import SwiftUI
+
+enum ChartsConfig {
+    static let glucosePointSize: CGFloat = 5
+}

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

@@ -0,0 +1,29 @@
+import SwiftUI
+
+public struct GlucoseArrowView: View {
+    public init(value: Double, delta: Double) {
+        self.value = value
+        self.delta = delta
+    }
+
+    let value: Double
+    let delta: Double
+
+    public var body: some View {
+        getGlucoseArrowImage(for: delta)
+            .foregroundColor(Color(.systemBlue))
+            .informationBarEntryStyle()
+    }
+}
+
+struct GlucoseArrowView_Previews: PreviewProvider {
+    static var previews: some View {
+        GlucoseArrowView(value: 11.5, delta: 0.9)
+            .frame(
+                width: /*@START_MENU_TOKEN@*/100/*@END_MENU_TOKEN@*/,
+                height: /*@START_MENU_TOKEN@*/100/*@END_MENU_TOKEN@*/,
+                alignment: /*@START_MENU_TOKEN@*/ .center/*@END_MENU_TOKEN@*/
+            )
+            .preferredColorScheme(/*@START_MENU_TOKEN@*/ .dark/*@END_MENU_TOKEN@*/)
+    }
+}

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

@@ -0,0 +1,65 @@
+import SwiftUI
+
+public struct GlucoseInformationBarView: View {
+    let data: [InformationBarEntryData]
+    let glucoseValue: Double
+    let glucoseDelta: Double
+
+    public init(data: [InformationBarEntryData], glucoseValue: Double, glucoseDelta: Double) {
+        self.data = data
+        self.glucoseValue = glucoseValue
+        self.glucoseDelta = glucoseDelta
+    }
+
+    public 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(value: glucoseValue, delta: glucoseDelta)
+                    .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: ", type: .cob, value: 33),
+        InformationBarEntryData(label: "COB: ", type: .cob, value: 33),
+        InformationBarEntryData(label: "COB: ", type: .cob, value: 33),
+        InformationBarEntryData(label: "COB: ", type: .cob, value: 33),
+        InformationBarEntryData(label: "COB: ", type: .cob, value: 33)
+    ]
+    static var previews: some View {
+        GlucoseInformationBarView(data: data, glucoseValue: 5.5, glucoseDelta: -0.2)
+            .preferredColorScheme(.dark)
+            .frame(height: 200)
+    }
+}

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

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

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

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

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

@@ -0,0 +1,21 @@
+import SwiftUI
+
+struct PredictionPointView: View {
+    let predictionType: PredictionType
+    var body: some View {
+        Circle()
+            .strokeBorder(
+                getPredictionColor(for: predictionType),
+                lineWidth: 1.5,
+                antialiased: true
+            )
+            .frame(width: ChartsConfig.glucosePointSize, height: ChartsConfig.glucosePointSize)
+    }
+}
+
+struct PredictionPointView_Previews: PreviewProvider {
+    static var previews: some View {
+        PredictionPointView(predictionType: .COB)
+            .preferredColorScheme(/*@START_MENU_TOKEN@*/ .dark/*@END_MENU_TOKEN@*/)
+    }
+}