Procházet zdrojové kódy

porting glucose code to mainChartView2

polscm32 před 2 roky
rodič
revize
ba4eef96ce

+ 8 - 4
FreeAPS.xcodeproj/project.pbxproj

@@ -175,7 +175,6 @@
 		38A504A425DD9C4000C5B9E8 /* UserDefaultsExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38A5049125DD9C4000C5B9E8 /* UserDefaultsExtensions.swift */; };
 		38A9260525F012D8009E3739 /* CarbRatios.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38A9260425F012D8009E3739 /* CarbRatios.swift */; };
 		38AAF85525FFF846004AF583 /* CurrentGlucoseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AAF85425FFF846004AF583 /* CurrentGlucoseView.swift */; };
-		38AAF8712600C1B0004AF583 /* MainChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AAF8702600C1B0004AF583 /* MainChartView.swift */; };
 		38AEE73D25F0200C0013F05B /* FreeAPSSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AEE73C25F0200C0013F05B /* FreeAPSSettings.swift */; };
 		38AEE75225F022080013F05B /* SettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AEE75125F022080013F05B /* SettingsManager.swift */; };
 		38AEE75725F0F18E0013F05B /* CarbsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AEE75625F0F18E0013F05B /* CarbsStorage.swift */; };
@@ -308,6 +307,8 @@
 		BA90041DC8991147E5C8C3AA /* CalibrationsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 500371C09F54F89A97D65FDB /* CalibrationsRootView.swift */; };
 		BD2B464E0745FBE7B79913F4 /* NightscoutConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BF768BD6264FF7D71D66767 /* NightscoutConfigProvider.swift */; };
 		BD2FF1A02AE29D43005D1C5D /* CheckboxToggleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD2FF19F2AE29D43005D1C5D /* CheckboxToggleStyle.swift */; };
+		BD3CC0722B0B89D50013189E /* MainChartView2.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD3CC0712B0B89D50013189E /* MainChartView2.swift */; };
+		BD3CC0742B0B8C430013189E /* DataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD3CC0732B0B8C430013189E /* DataModel.swift */; };
 		BD7DA9A52AE06DFC00601B20 /* BolusCalculatorConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DA9A42AE06DFC00601B20 /* BolusCalculatorConfigDataFlow.swift */; };
 		BD7DA9A72AE06E2B00601B20 /* BolusCalculatorConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DA9A62AE06E2B00601B20 /* BolusCalculatorConfigProvider.swift */; };
 		BD7DA9A92AE06E9200601B20 /* BolusCalculatorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DA9A82AE06E9200601B20 /* BolusCalculatorStateModel.swift */; };
@@ -700,7 +701,6 @@
 		38A5049125DD9C4000C5B9E8 /* UserDefaultsExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDefaultsExtensions.swift; sourceTree = "<group>"; };
 		38A9260425F012D8009E3739 /* CarbRatios.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbRatios.swift; sourceTree = "<group>"; };
 		38AAF85425FFF846004AF583 /* CurrentGlucoseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentGlucoseView.swift; sourceTree = "<group>"; };
-		38AAF8702600C1B0004AF583 /* MainChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainChartView.swift; sourceTree = "<group>"; };
 		38AEE73C25F0200C0013F05B /* FreeAPSSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FreeAPSSettings.swift; sourceTree = "<group>"; };
 		38AEE75125F022080013F05B /* SettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsManager.swift; sourceTree = "<group>"; };
 		38AEE75625F0F18E0013F05B /* CarbsStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbsStorage.swift; sourceTree = "<group>"; };
@@ -839,6 +839,8 @@
 		BA49538D56989D8DA6FCF538 /* TargetsEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorDataFlow.swift; sourceTree = "<group>"; };
 		BC210C0F3CB6D3C86E5DED4E /* LibreConfigRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LibreConfigRootView.swift; sourceTree = "<group>"; };
 		BD2FF19F2AE29D43005D1C5D /* CheckboxToggleStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckboxToggleStyle.swift; sourceTree = "<group>"; };
+		BD3CC0712B0B89D50013189E /* MainChartView2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainChartView2.swift; sourceTree = "<group>"; };
+		BD3CC0732B0B8C430013189E /* DataModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataModel.swift; sourceTree = "<group>"; };
 		BD7DA9A42AE06DFC00601B20 /* BolusCalculatorConfigDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorConfigDataFlow.swift; sourceTree = "<group>"; };
 		BD7DA9A62AE06E2B00601B20 /* BolusCalculatorConfigProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorConfigProvider.swift; sourceTree = "<group>"; };
 		BD7DA9A82AE06E9200601B20 /* BolusCalculatorStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorStateModel.swift; sourceTree = "<group>"; };
@@ -1512,7 +1514,8 @@
 		3833B51E260264AC003021B3 /* Chart */ = {
 			isa = PBXGroup;
 			children = (
-				38AAF8702600C1B0004AF583 /* MainChartView.swift */,
+				BD3CC0712B0B89D50013189E /* MainChartView2.swift */,
+				BD3CC0732B0B8C430013189E /* DataModel.swift */,
 			);
 			path = Chart;
 			sourceTree = "<group>";
@@ -2597,6 +2600,7 @@
 			buildActionMask = 2147483647;
 			files = (
 				3811DE2325C9D48300A708ED /* MainDataFlow.swift in Sources */,
+				BD3CC0722B0B89D50013189E /* MainChartView2.swift in Sources */,
 				3811DEEB25CA063400A708ED /* PersistedProperty.swift in Sources */,
 				38E44537274E411700EC9A94 /* Disk+Helpers.swift in Sources */,
 				388E5A6025B6F2310019842D /* Autosens.swift in Sources */,
@@ -2609,6 +2613,7 @@
 				38D0B3B625EBE24900CB6E88 /* Battery.swift in Sources */,
 				38C4D33725E9A1A300D30B77 /* DispatchQueue+Extensions.swift in Sources */,
 				F90692CF274B999A0037068D /* HealthKitDataFlow.swift in Sources */,
+				BD3CC0742B0B8C430013189E /* DataModel.swift in Sources */,
 				CE7CA3552A064973004BE681 /* ListStateIntent.swift in Sources */,
 				195D80B72AF697B800D25097 /* DynamicDataFlow.swift in Sources */,
 				3862CC2E2743F9F700BF832C /* CalendarManager.swift in Sources */,
@@ -2754,7 +2759,6 @@
 				CE7CA34E2A064973004BE681 /* AppShortcuts.swift in Sources */,
 				38C4D33A25E9A1ED00D30B77 /* NSObject+AssociatedValues.swift in Sources */,
 				38DF179027733EAD00B3528F /* SnowScene.swift in Sources */,
-				38AAF8712600C1B0004AF583 /* MainChartView.swift in Sources */,
 				19DC677F29CA675700FD9EC4 /* OverrideProfilesDataFlow.swift in Sources */,
 				1935364028496F7D001E0B16 /* Oref2_variables.swift in Sources */,
 				CE2FAD3A297D93F0001A872C /* BloodGlucoseExtensions.swift in Sources */,

+ 10 - 0
FreeAPS/Sources/Modules/Home/View/Chart/DataModel.swift

@@ -0,0 +1,10 @@
+//import Foundation
+//
+//let ektasienView: [ViewMonth] = [
+//    .init(date: Date.from(year: 2023, month: 1, day: 1), viewCount: 123),
+//    .init(date: Date.from(year: 2023, month: 2, day: 1), viewCount: 100),
+//    .init(date: Date.from(year: 2023, month: 3, day: 1), viewCount: 80),
+//    .init(date: Date.from(year: 2023, month: 4, day: 1), viewCount: 65),
+//    .init(date: Date.from(year: 2023, month: 5, day: 1), viewCount: 120),
+//    .init(date: Date.from(year: 2023, month: 6, day: 1), viewCount: 25)
+//]

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 1243
FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift


+ 298 - 0
FreeAPS/Sources/Modules/Home/View/Chart/MainChartView2.swift

@@ -0,0 +1,298 @@
+import Charts
+import SwiftUI
+
+private enum PredictionType: Hashable {
+    case iob
+    case cob
+    case zt
+    case uam
+}
+
+struct DotInfo {
+    let rect: CGRect
+    let value: Decimal
+}
+
+struct AnnouncementDot {
+    let rect: CGRect
+    let value: Decimal
+    let note: String
+}
+
+typealias GlucoseYRange = (minValue: Int, minY: CGFloat, maxValue: Int, maxY: CGFloat)
+
+struct MainChartView2: View {
+    private enum Config {
+        static let endID = "End"
+        static let basalHeight: CGFloat = 80
+        static let topYPadding: CGFloat = 20
+        static let bottomYPadding: CGFloat = 80
+        static let minAdditionalWidth: CGFloat = 150
+        static let maxGlucose = 270
+        static let minGlucose = 45
+        static let yLinesCount = 5
+        static let glucoseScale: CGFloat = 2 // default 2
+        static let bolusSize: CGFloat = 8
+        static let bolusScale: CGFloat = 2.5
+        static let carbsSize: CGFloat = 10
+        static let fpuSize: CGFloat = 5
+        static let carbsScale: CGFloat = 0.3
+        static let fpuScale: CGFloat = 1
+        static let announcementSize: CGFloat = 8
+        static let announcementScale: CGFloat = 2.5
+        static let owlSeize: CGFloat = 25
+        static let owlOffset: CGFloat = 80
+    }
+
+    // MARK: BINDINGS
+
+    @Binding var glucose: [BloodGlucose]
+    @Binding var isManual: [BloodGlucose]
+    @Binding var suggestion: Suggestion?
+    @Binding var tempBasals: [PumpHistoryEvent]
+    @Binding var boluses: [PumpHistoryEvent]
+    @Binding var suspensions: [PumpHistoryEvent]
+    @Binding var announcement: [Announcement]
+    @Binding var hours: Int
+    @Binding var maxBasal: Decimal
+    @Binding var autotunedBasalProfile: [BasalProfileEntry]
+    @Binding var basalProfile: [BasalProfileEntry]
+    @Binding var tempTargets: [TempTarget]
+    @Binding var carbs: [CarbsEntry]
+    @Binding var timerDate: Date
+    @Binding var units: GlucoseUnits
+    @Binding var smooth: Bool
+    @Binding var highGlucose: Decimal
+    @Binding var lowGlucose: Decimal
+    @Binding var screenHours: Int16
+    @Binding var displayXgridLines: Bool
+    @Binding var displayYgridLines: Bool
+    @Binding var thresholdLines: Bool
+
+    // MARK: STATEs
+
+    @State private var glucoseDots: [CGRect] = []
+    @State private var glucoseYRange: Range<CGFloat> = 0 ..< 0
+    @State var didAppearTrigger = false
+    @State private var glucoseDots: [CGRect] = []
+    @State private var manualGlucoseDots: [CGRect] = []
+    @State private var announcementDots: [AnnouncementDot] = []
+    @State private var announcementPath = Path()
+    @State private var manualGlucoseDotsCenter: [CGRect] = []
+    @State private var unSmoothedGlucoseDots: [CGRect] = []
+    @State private var predictionDots: [PredictionType: [CGRect]] = [:]
+    @State private var bolusDots: [DotInfo] = []
+    @State private var bolusPath = Path()
+    @State private var tempBasalPath = Path()
+    @State private var regularBasalPath = Path()
+    @State private var tempTargetsPath = Path()
+    @State private var suspensionsPath = Path()
+    @State private var carbsDots: [DotInfo] = []
+    @State private var carbsPath = Path()
+    @State private var fpuDots: [DotInfo] = []
+    @State private var fpuPath = Path()
+    @State private var glucoseYRange: GlucoseYRange = (0, 0, 0, 0)
+    @State private var offset: CGFloat = 0
+    @State private var cachedMaxBasalRate: Decimal?
+
+    var body: some View {
+        NavigationStack {
+            ScrollView {
+                VStack {
+                    Chart {
+                        ForEach(glucoseDots.indices, id: \.self) { index in
+                            PointMark(position: glucoseDots[index].origin)
+                                .frame(width: glucoseDots[index].width, height: glucoseDots[index].height)
+                                .foregroundStyle(Color.green.gradient)
+                        }
+                    }
+                    .frame(height: 350)
+                    .chartXAxis {
+                        // to do
+                    }
+
+                    Legend()
+                }
+                .padding()
+            }
+        }
+        .onAppear {
+            calculateGlucoseDots(fullSize: UIScreen.main.bounds.size)
+        }
+    }
+
+    // MARK: GLUCOSE FOR CHART
+
+    private func calculateGlucoseDots(fullSize: CGSize) {
+        let dots = glucose.map { value -> CGRect in
+            let position = glucoseToCoordinate(value, fullSize: fullSize)
+            return CGRect(x: position.x - 2, y: position.y - 2, width: 4, height: 4)
+        }
+
+        let range = getGlucoseYRange(fullSize: fullSize)
+
+        DispatchQueue.main.async {
+            glucoseYRange = range
+            glucoseDots = dots
+        }
+    }
+
+    private func getGlucoseYRange(fullSize: CGSize) -> GlucoseYRange {
+        let topYPaddint = Config.topYPadding + Config.basalHeight
+        let bottomYPadding = Config.bottomYPadding
+        let (minValue, maxValue) = minMaxYValues()
+        let stepYFraction = (fullSize.height - topYPaddint - bottomYPadding) / CGFloat(maxValue - minValue)
+        let yOffset = CGFloat(minValue) * stepYFraction
+        let maxY = fullSize.height - CGFloat(minValue) * stepYFraction + yOffset - bottomYPadding
+        let minY = fullSize.height - CGFloat(maxValue) * stepYFraction + yOffset - bottomYPadding
+        return (minValue: minValue, minY: minY, maxValue: maxValue, maxY: maxY)
+    }
+
+    private func glucoseToCoordinate(_ glucoseEntry: BloodGlucose, fullSize: CGSize) -> CGPoint {
+        let x = timeToXCoordinate(glucoseEntry.dateString.timeIntervalSince1970, fullSize: fullSize)
+        let y = glucoseToYCoordinate(glucoseEntry.glucose ?? 0, fullSize: fullSize)
+
+        return CGPoint(x: x, y: y)
+    }
+
+    private func glucoseToYCoordinate(_ glucoseValue: Int, fullSize: CGSize) -> CGFloat {
+        let topYPaddint = Config.topYPadding + Config.basalHeight
+        let bottomYPadding = Config.bottomYPadding
+        let (minValue, maxValue) = minMaxYValues()
+        let stepYFraction = (fullSize.height - topYPaddint - bottomYPadding) / CGFloat(maxValue - minValue)
+        let yOffset = CGFloat(minValue) * stepYFraction
+        let y = fullSize.height - CGFloat(glucoseValue) * stepYFraction + yOffset - bottomYPadding
+        return y
+    }
+
+    private func timeToXCoordinate(_ time: TimeInterval, fullSize: CGSize) -> CGFloat {
+        let xOffset = -Date().addingTimeInterval(-1.days.timeInterval).timeIntervalSince1970
+        let stepXFraction = fullGlucoseWidth(viewWidth: fullSize.width) / CGFloat(hours.hours.timeInterval)
+        let x = CGFloat(time + xOffset) * stepXFraction
+        return x
+    }
+
+    private func fullGlucoseWidth(viewWidth: CGFloat) -> CGFloat {
+        viewWidth * CGFloat(hours) / CGFloat(min(max(screenHours, 2), 24))
+    }
+
+    private func minMaxYValues() -> (min: Int, max: Int) {
+        var maxValue = glucose.compactMap(\.glucose).max() ?? Config.maxGlucose
+        if let maxPredValue = maxPredValue() {
+            maxValue = max(maxValue, maxPredValue)
+        }
+        if let maxTargetValue = maxTargetValue() {
+            maxValue = max(maxValue, maxTargetValue)
+        }
+        var minValue = glucose.compactMap(\.glucose).min() ?? Config.minGlucose
+        if let minPredValue = minPredValue() {
+            minValue = min(minValue, minPredValue)
+        }
+        if let minTargetValue = minTargetValue() {
+            minValue = min(minValue, minTargetValue)
+        }
+
+        if minValue == maxValue {
+            minValue = Config.minGlucose
+            maxValue = Config.maxGlucose
+        }
+        // fix the grah y-axis as long as the min and max BG values are within set borders
+        if minValue > Config.minGlucose {
+            minValue = Config.minGlucose
+        }
+        if maxValue < Config.maxGlucose {
+            maxValue = Config.maxGlucose
+        }
+        return (min: minValue, max: maxValue)
+    }
+
+    private func maxTargetValue() -> Int? {
+        tempTargets.map { $0.targetTop ?? 0 }.filter { $0 > 0 }.max().map(Int.init)
+    }
+
+    private func minPredValue() -> Int? {
+        [
+            suggestion?.predictions?.cob ?? [],
+            suggestion?.predictions?.iob ?? [],
+            suggestion?.predictions?.zt ?? [],
+            suggestion?.predictions?.uam ?? []
+        ]
+        .flatMap { $0 }
+        .min()
+    }
+
+    private func minTargetValue() -> Int? {
+        tempTargets.map { $0.targetBottom ?? 0 }.filter { $0 > 0 }.min().map(Int.init)
+    }
+
+    private func maxPredValue() -> Int? {
+        [
+            suggestion?.predictions?.cob ?? [],
+            suggestion?.predictions?.iob ?? [],
+            suggestion?.predictions?.zt ?? [],
+            suggestion?.predictions?.uam ?? []
+        ]
+        .flatMap { $0 }
+        .max()
+    }
+}
+
+// MARK: LEGEND PANEL FOR CHART
+
+struct Legend: View {
+    var body: some View {
+        HStack {
+            Image(systemName: "line.diagonal")
+                .rotationEffect(Angle(degrees: 45))
+                .foregroundColor(.green)
+            Text("BG")
+                .foregroundColor(.secondary)
+            Spacer()
+            Image(systemName: "line.diagonal")
+                .rotationEffect(Angle(degrees: 45))
+                .foregroundColor(.insulin)
+            Text("IOB")
+                .foregroundColor(.secondary)
+            Spacer()
+            Image(systemName: "line.diagonal")
+                .rotationEffect(Angle(degrees: 45))
+                .foregroundColor(.purple)
+            Text("ZT")
+                .foregroundColor(.secondary)
+            Spacer()
+            Image(systemName: "line.diagonal")
+                .rotationEffect(Angle(degrees: 45))
+                .foregroundColor(.loopYellow)
+            Text("COB")
+                .foregroundColor(.secondary)
+            Spacer()
+            Image(systemName: "line.diagonal")
+                .rotationEffect(Angle(degrees: 45))
+                .foregroundColor(.orange)
+            Text("UAM")
+                .foregroundColor(.secondary)
+        }
+        .font(.caption2)
+        .padding(.horizontal, 4)
+        .padding(.vertical, 10)
+    }
+}
+
+// struct BloodGlucose: Identifiable {
+//    let id = UUID()
+//    let timestamp: Date
+//    let value: Int
+// }
+
+// struct ViewMonth: Identifiable {
+//    let id = UUID()
+//    let date: Date
+//    let viewCount: Int
+// }
+
+// extension Date {
+//    static func from(year: Int, month: Int, day: Int) -> Date {
+//        let components = DateComponents(year: year, month: month, day: day)
+//        return Calendar.current.date(from: components)!
+//    }
+// }

+ 28 - 26
FreeAPS/Sources/Modules/Home/View/HomeRootView.swift

@@ -471,30 +471,34 @@ extension Home {
                         .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
                 }
 
-                MainChartView(
-                    glucose: $state.glucose,
-                    isManual: $state.isManual,
-                    suggestion: $state.suggestion,
-                    tempBasals: $state.tempBasals,
-                    boluses: $state.boluses,
-                    suspensions: $state.suspensions,
-                    announcement: $state.announcement,
-                    hours: .constant(state.filteredHours),
-                    maxBasal: $state.maxBasal,
-                    autotunedBasalProfile: $state.autotunedBasalProfile,
-                    basalProfile: $state.basalProfile,
-                    tempTargets: $state.tempTargets,
-                    carbs: $state.carbs,
-                    timerDate: $state.timerDate,
-                    units: $state.units,
-                    smooth: $state.smooth,
-                    highGlucose: $state.highGlucose,
-                    lowGlucose: $state.lowGlucose,
-                    screenHours: $state.hours,
-                    displayXgridLines: $state.displayXgridLines,
-                    displayYgridLines: $state.displayYgridLines,
-                    thresholdLines: $state.thresholdLines
-                )
+//                MainChartView(
+//                    glucose: $state.glucose,
+//                    isManual: $state.isManual,
+//                    suggestion: $state.suggestion,
+//                    tempBasals: $state.tempBasals,
+//                    boluses: $state.boluses,
+//                    suspensions: $state.suspensions,
+//                    announcement: $state.announcement,
+//                    hours: .constant(state.filteredHours),
+//                    maxBasal: $state.maxBasal,
+//                    autotunedBasalProfile: $state.autotunedBasalProfile,
+//                    basalProfile: $state.basalProfile,
+//                    tempTargets: $state.tempTargets,
+//                    carbs: $state.carbs,
+//                    timerDate: $state.timerDate,
+//                    units: $state.units,
+//                    smooth: $state.smooth,
+//                    highGlucose: $state.highGlucose,
+//                    lowGlucose: $state.lowGlucose,
+//                    screenHours: $state.hours,
+//                    displayXgridLines: $state.displayXgridLines,
+//                    displayYgridLines: $state.displayYgridLines,
+//                    thresholdLines: $state.thresholdLines
+//                )
+
+//                MARK: SWIFT CHARTS
+
+                MainChartView2()
             }
             .padding(.bottom)
             .modal(for: .dataTable, from: self)
@@ -728,8 +732,6 @@ extension Home {
 
                     timeInterval
 
-                    legendPanel
-
                     Spacer()
 
                     bottomPanel(geo)