Browse Source

Calibrations chart

Ivan Valkou 4 years ago
parent
commit
a3e4af07e1

+ 4 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -79,6 +79,7 @@
 		385CEAC125F2EA52002D6D5B /* Announcement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385CEAC025F2EA52002D6D5B /* Announcement.swift */; };
 		385CEAC425F2F154002D6D5B /* AnnouncementsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385CEAC325F2F154002D6D5B /* AnnouncementsStorage.swift */; };
 		3862CC05273D152B00BF832C /* CalibrationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3862CC04273D152B00BF832C /* CalibrationService.swift */; };
+		3862CC1F273FDC9200BF832C /* CalibrationsChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3862CC1E273FDC9200BF832C /* CalibrationsChart.swift */; };
 		386A124C271704DA00DDC61C /* CGMBLEKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 386A124B271704DA00DDC61C /* CGMBLEKit.framework */; };
 		386A124D271704DA00DDC61C /* CGMBLEKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 386A124B271704DA00DDC61C /* CGMBLEKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		386A124F271707F000DDC61C /* DexcomSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 386A124E271707F000DDC61C /* DexcomSource.swift */; };
@@ -375,6 +376,7 @@
 		385CEAC025F2EA52002D6D5B /* Announcement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Announcement.swift; sourceTree = "<group>"; };
 		385CEAC325F2F154002D6D5B /* AnnouncementsStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnnouncementsStorage.swift; sourceTree = "<group>"; };
 		3862CC04273D152B00BF832C /* CalibrationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalibrationService.swift; sourceTree = "<group>"; };
+		3862CC1E273FDC9200BF832C /* CalibrationsChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalibrationsChart.swift; sourceTree = "<group>"; };
 		386A124B271704DA00DDC61C /* CGMBLEKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CGMBLEKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		386A124E271707F000DDC61C /* DexcomSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DexcomSource.swift; sourceTree = "<group>"; };
 		3870FF4225EC13F40088248F /* BloodGlucose.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BloodGlucose.swift; sourceTree = "<group>"; };
@@ -1165,6 +1167,7 @@
 			isa = PBXGroup;
 			children = (
 				500371C09F54F89A97D65FDB /* CalibrationsRootView.swift */,
+				3862CC1E273FDC9200BF832C /* CalibrationsChart.swift */,
 			);
 			path = View;
 			sourceTree = "<group>";
@@ -1792,6 +1795,7 @@
 				38569353270B5E350002C50D /* CGMRootView.swift in Sources */,
 				69A31254F2451C20361D172F /* BolusStateModel.swift in Sources */,
 				0CEA2EA070AB041AF3E3745B /* BolusRootView.swift in Sources */,
+				3862CC1F273FDC9200BF832C /* CalibrationsChart.swift in Sources */,
 				711C0CB42CAABE788916BC9D /* ManualTempBasalDataFlow.swift in Sources */,
 				BF1667ADE69E4B5B111CECAE /* ManualTempBasalProvider.swift in Sources */,
 				C967DACD3B1E638F8B43BE06 /* ManualTempBasalStateModel.swift in Sources */,

+ 5 - 3
FreeAPS/Sources/APS/CGM/Calibrations/CalibrationService.swift

@@ -1,12 +1,14 @@
 import Foundation
 import Swinject
 
-struct Calibration: JSON, Equatable {
+struct Calibration: JSON, Hashable, Identifiable {
     let x: Double
     let y: Double
     var date = Date()
 
     static let zero = Calibration(x: 0, y: 0)
+
+    var id = UUID()
 }
 
 protocol CalibrationService {
@@ -24,8 +26,8 @@ protocol CalibrationService {
 
 final class BaseCalibrationService: CalibrationService, Injectable {
     private enum Config {
-        static let minSlope = 0.66
-        static let maxSlope = 1.5
+        static let minSlope = 0.8
+        static let maxSlope = 1.25
         static let minIntercept = -100.0
         static let maxIntercept = 100.0
     }

+ 7 - 6
FreeAPS/Sources/Modules/Calibrations/CalibrationsStateModel.swift

@@ -9,9 +9,9 @@ extension Calibrations {
 
         @Published var slope: Double = 1
         @Published var intercept: Double = 1
-        @Published var calibration: Decimal = 0
-
-        @Published var calibrationsCount = 0
+        @Published var newCalibration: Decimal = 0
+        @Published var calibrations: [Calibration] = []
+        @Published var calibrate: (Double) -> Double = { $0 }
 
         var units: GlucoseUnits = .mmolL
 
@@ -20,7 +20,8 @@ extension Calibrations {
             intercept = calibrationService.intercept
 
             units = settingsManager.settings.units
-            calibrationsCount = calibrationService.calibrations.count
+            calibrations = calibrationService.calibrations
+            calibrate = calibrationService.calibrate
         }
 
         func addCalibration() {
@@ -28,9 +29,9 @@ extension Calibrations {
                 hideModal()
             }
 
-            var glucose = calibration
+            var glucose = newCalibration
             if units == .mmolL {
-                glucose = calibration.asMgdL
+                glucose = newCalibration.asMgdL
             }
 
             guard let lastGlucose = glucoseStorage.recent().last,

+ 44 - 0
FreeAPS/Sources/Modules/Calibrations/View/CalibrationsChart.swift

@@ -0,0 +1,44 @@
+import SwiftUI
+
+struct CalibrationsChart: View {
+    @EnvironmentObject var state: Calibrations.StateModel
+
+    private let maxValue = 400.0
+
+    var body: some View {
+        GeometryReader { geo in
+            ZStack(alignment: .top) {
+                Rectangle().fill(Color.secondary)
+                    .frame(height: geo.size.width)
+                Path { path in
+                    let size = geo.size.width
+                    path.move(
+                        to:
+                        CGPoint(
+                            x: 0,
+                            y: size - state.calibrate(0) / maxValue * geo.size.width
+                        )
+                    )
+                    path.addLine(
+                        to: CGPoint(
+                            x: size,
+                            y: size - state.calibrate(maxValue) / maxValue * geo.size.width
+                        )
+                    )
+                }
+                .stroke(.blue, lineWidth: 2)
+
+                ForEach(state.calibrations, id: \.self) { value in
+                    Circle().fill(.red)
+                        .frame(width: 6, height: 6)
+                        .position(
+                            x: value.x / maxValue * geo.size.width,
+                            y: geo.size.width - (value.y / maxValue * geo.size.width)
+                        )
+                }
+            }
+            .frame(height: geo.size.width)
+            .clipped()
+        }
+    }
+}

+ 46 - 40
FreeAPS/Sources/Modules/Calibrations/View/CalibrationsRootView.swift

@@ -6,10 +6,6 @@ extension Calibrations {
         let resolver: Resolver
         @StateObject var state = StateModel()
 
-        @State private var isPromtPresented = false
-        @State private var isRemoveAlertPresented = false
-        @State private var removeAlert: Alert?
-
         private var formatter: NumberFormatter {
             let formatter = NumberFormatter()
             formatter.numberStyle = .decimal
@@ -18,51 +14,61 @@ extension Calibrations {
         }
 
         var body: some View {
-            Form {
-                Section(header: Text("Add calibration")) {
-                    HStack {
-                        Text("Meter glucose")
-                        Spacer()
-                        DecimalTextField("0", value: $state.calibration, formatter: formatter, autofocus: false, cleanInput: true)
-                        Text(state.units.rawValue).foregroundColor(.secondary)
-                    }
-                    Button {
-                        state.addCalibration()
+            GeometryReader { geo in
+                Form {
+                    Section(header: Text("Add calibration")) {
+                        HStack {
+                            Text("Meter glucose")
+                            Spacer()
+                            DecimalTextField(
+                                "0",
+                                value: $state.newCalibration,
+                                formatter: formatter,
+                                autofocus: false,
+                                cleanInput: true
+                            )
+                            Text(state.units.rawValue).foregroundColor(.secondary)
+                        }
+                        Button {
+                            state.addCalibration()
+                        }
+                        label: { Text("Add") }
+                            .disabled(state.newCalibration <= 0)
                     }
-                    label: { Text("Add") }
-                        .disabled(state.calibration <= 0)
-                }
 
-                Section(header: Text("Info")) {
-                    HStack {
-                        Text("Slope")
-                        Spacer()
-                        Text(formatter.string(from: state.slope as NSNumber)!)
-                    }
-                    HStack {
-                        Text("Intercept")
-                        Spacer()
-                        Text(formatter.string(from: state.intercept as NSNumber)!)
+                    Section(header: Text("Info")) {
+                        HStack {
+                            Text("Slope")
+                            Spacer()
+                            Text(formatter.string(from: state.slope as NSNumber)!)
+                        }
+                        HStack {
+                            Text("Intercept")
+                            Spacer()
+                            Text(formatter.string(from: state.intercept as NSNumber)!)
+                        }
                     }
-                }
 
-                Section(header: Text("Remove")) {
-                    Button {
-                        state.removeLast()
+                    Section(header: Text("Remove")) {
+                        Button {
+                            state.removeLast()
+                        }
+                        label: { Text("Remove Last") }
+                            .disabled(state.calibrations.isEmpty)
+
+                        Button {
+                            state.removeAll()
+                        }
+                        label: { Text("Remove All") }
+                            .disabled(state.calibrations.isEmpty)
                     }
-                    label: { Text("Remove Last") }
-                        .disabled(state.calibrationsCount == 0)
 
-                    Button {
-                        state.removeAll()
+                    Section(header: Text("Chart")) {
+                        CalibrationsChart().environmentObject(state)
+                            .frame(minHeight: geo.size.width)
                     }
-                    label: { Text("Remove All") }
-                        .disabled(state.calibrationsCount == 0)
                 }
             }
-            .popover(isPresented: $isPromtPresented) {
-                Form {}
-            }
             .onAppear(perform: configureView)
             .navigationTitle("Calibrations")
             .navigationBarTitleDisplayMode(.automatic)

+ 0 - 70
FreeAPSTests/FileStorageTests.swift

@@ -16,74 +16,4 @@ class FileStorageTests: XCTestCase {
     override func tearDownWithError() throws {
         // Put teardown code here. This method is called after the invocation of each test method in the class.
     }
-
-    func testStorage() throws {
-        let uniqID = UUID().uuidString
-        let object1 = DummyObject(id: uniqID, value: 1.0)
-        let object2 = DummyObject(id: UUID().uuidString, value: 1.2)
-        let object3 = DummyObject(id: UUID().uuidString, value: 1.4)
-        let object4 = DummyObject(id: uniqID, value: 1.0)
-
-        do {
-            try fileStorage.save(object1, as: "tests/testStorage1.json")
-        } catch {
-            XCTFail(error.localizedDescription)
-        }
-
-        do {
-            try fileStorage.save([object1, object2], as: "tests/testStorage2.json")
-        } catch {
-            XCTFail(error.localizedDescription)
-        }
-
-        do {
-            let value = try fileStorage.retrieve("tests/testStorage1.json", as: DummyObject.self)
-            XCTAssert(value.rawJSON == object1.rawJSON)
-        } catch {
-            XCTFail(error.localizedDescription)
-        }
-
-        do {
-            let values = try fileStorage.retrieve("tests/testStorage2.json", as: [DummyObject].self)
-            XCTAssert(values.rawJSON == [object1, object2].rawJSON)
-        } catch {
-            XCTFail(error.localizedDescription)
-        }
-
-        do {
-            try fileStorage.append(object3, to: "tests/testStorage1.json")
-            let values = try fileStorage.retrieve("tests/testStorage1.json", as: [DummyObject].self)
-
-            XCTAssert(values.rawJSON == [object1, object3].rawJSON)
-        } catch {
-            XCTFail(error.localizedDescription)
-        }
-
-        do {
-            try fileStorage.append([object2, object4], to: "tests/testStorage1.json")
-            let values = try fileStorage.retrieve("tests/testStorage1.json", as: [DummyObject].self)
-
-            XCTAssert(values.rawJSON == [object1, object3, object2, object4].rawJSON)
-        } catch {
-            XCTFail(error.localizedDescription)
-        }
-
-        do {
-            try fileStorage.append([object3, object4], to: "tests/testStorage2.json", uniqBy: \.id)
-            let values = try fileStorage.retrieve("tests/testStorage2.json", as: [DummyObject].self)
-
-            XCTAssert(values.rawJSON == [object1, object2, object3].rawJSON)
-        } catch {
-            XCTFail(error.localizedDescription)
-        }
-
-        do {
-            try fileStorage.remove("tests/testStorage1.json")
-            try fileStorage.rename("tests/testStorage2.json", to: "tests/testStorage1.json")
-            let values = try fileStorage.retrieve("tests/testStorage1.json", as: [DummyObject].self)
-            XCTAssert(values.rawJSON == [object1, object2, object3].rawJSON)
-        } catch {
-            XCTFail(error.localizedDescription)
-        }
-    }
 }