Parcourir la source

merge swiftCharts changes to feature-branch

polscm32 il y a 2 ans
Parent
commit
b8d4940c1d

+ 4 - 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 */; };
@@ -319,6 +318,7 @@
 		BD188BED2B1B805B00B183BF /* WidgetBobble.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD188BEB2B1B805A00B183BF /* WidgetBobble.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 */; };
 		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 */; };
@@ -729,7 +729,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>"; };
@@ -879,6 +878,7 @@
 		BC210C0F3CB6D3C86E5DED4E /* LibreConfigRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LibreConfigRootView.swift; sourceTree = "<group>"; };
 		BD188BEB2B1B805A00B183BF /* WidgetBobble.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetBobble.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>"; };
 		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>"; };
@@ -1565,7 +1565,7 @@
 		3833B51E260264AC003021B3 /* Chart */ = {
 			isa = PBXGroup;
 			children = (
-				38AAF8702600C1B0004AF583 /* MainChartView.swift */,
+				BD3CC0712B0B89D50013189E /* MainChartView2.swift */,
 			);
 			path = Chart;
 			sourceTree = "<group>";
@@ -2700,6 +2700,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 */,
@@ -2858,7 +2859,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 */,

+ 1 - 1
FreeAPS/Sources/Models/CarbsEntry.swift

@@ -1,6 +1,6 @@
 import Foundation
 
-struct CarbsEntry: JSON, Equatable, Hashable {
+struct CarbsEntry: JSON, Equatable, Hashable, Identifiable {
     let id: String?
     let createdAt: Date
     let actualDate: Date?

+ 1 - 1
FreeAPS/Sources/Models/PumpHistoryEvent.swift

@@ -1,6 +1,6 @@
 import Foundation
 
-struct PumpHistoryEvent: JSON, Equatable {
+struct PumpHistoryEvent: JSON, Equatable, Identifiable {
     let id: String
     let type: EventType
     let timestamp: Date

+ 7 - 2
FreeAPS/Sources/Modules/Home/HomeStateModel.swift

@@ -62,9 +62,12 @@ extension Home {
         @Published var timeZone: TimeZone?
         @Published var hours: Int16 = 6
         @Published var totalBolus: Decimal = 0
+
         @Published var isStatusPopupPresented: Bool = false
         @Published var tins: Bool = false
 
+        @Published var cob: Decimal = 0
+
         let coredataContext = CoreDataStack.shared.persistentContainer.viewContext
 
         override func subscribe() {
@@ -104,6 +107,8 @@ extension Home {
             thresholdLines = settingsManager.settings.rulerMarks
             tins = settingsManager.settings.tins
 
+            cob = provider.suggestion?.cob ?? 0
+
             broadcaster.register(GlucoseObserver.self, observer: self)
             broadcaster.register(SuggestionObserver.self, observer: self)
             broadcaster.register(SettingsObserver.self, observer: self)
@@ -284,8 +289,8 @@ extension Home {
             offsetComponents.hour = -Int(offset)
 
             let startTime = calendar.date(byAdding: offsetComponents, to: date)!
-            print("******************")
-            print("die voll krasse start time ist: \(startTime)")
+//            print("******************")
+//            print("die voll krasse start time ist: \(startTime)")
 
             let bolusesForCurrentDay = boluses.filter { $0.timestamp >= startTime && $0.type == .bolus }
 

Fichier diff supprimé car celui-ci est trop grand
+ 0 - 1274
FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift


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

@@ -0,0 +1,678 @@
+import Charts
+import SwiftUI
+
+let screenSize: CGRect = UIScreen.main.bounds
+let calendar = Calendar.current
+
+private struct BasalProfile: Hashable {
+    let amount: Double
+    var isOverwritten: Bool
+    let startDate: Date
+    let endDate: Date?
+    init(amount: Double, isOverwritten: Bool, startDate: Date, endDate: Date? = nil) {
+        self.amount = amount
+        self.isOverwritten = isOverwritten
+        self.startDate = startDate
+        self.endDate = endDate
+    }
+}
+
+private struct Prediction: Hashable {
+    let amount: Int
+    let timestamp: Date
+    let type: PredictionType
+}
+
+private struct Carb: Hashable {
+    let amount: Decimal
+    let timestamp: Date
+    let nearestGlucose: BloodGlucose
+}
+
+private struct ChartBolus: Hashable {
+    let amount: Decimal
+    let timestamp: Date
+    let nearestGlucose: BloodGlucose
+}
+
+private enum PredictionType: Hashable {
+    case iob
+    case cob
+    case zt
+    case uam
+}
+
+struct MainChartView2: View {
+    private enum Config {
+        static let bolusSize: CGFloat = 4
+        static let bolusScale: CGFloat = 2.5
+        static let carbsSize: CGFloat = 5
+        static let carbsScale: CGFloat = 0.3
+        static let fpuSize: CGFloat = 5
+    }
+
+    @Binding var glucose: [BloodGlucose]
+    @Binding var eventualBG: Int?
+    @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 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
+
+    @State var didAppearTrigger = false
+    @State private var BasalProfiles: [BasalProfile] = []
+    @State private var TempBasals: [PumpHistoryEvent] = []
+    @State private var Predictions: [Prediction] = []
+    @State private var ChartCarbs: [Carb] = []
+    @State private var ChartFpus: [Carb] = []
+    @State private var ChartBoluses: [ChartBolus] = []
+    @State private var startMarker = Date(timeIntervalSince1970: TimeInterval(NSDate().timeIntervalSince1970 - 86400))
+    @State private var endMarker = Date(timeIntervalSince1970: TimeInterval(NSDate().timeIntervalSince1970 + 10800))
+
+    private var bolusFormatter: NumberFormatter {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.minimumIntegerDigits = 0
+        formatter.maximumFractionDigits = 2
+        formatter.decimalSeparator = "."
+        return formatter
+    }
+
+    private var carbsFormatter: NumberFormatter {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.maximumFractionDigits = 0
+        return formatter
+    }
+
+    private var fpuFormatter: NumberFormatter {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.maximumFractionDigits = 1
+        formatter.decimalSeparator = "."
+        formatter.minimumIntegerDigits = 0
+        return formatter
+    }
+
+    var body: some View {
+        VStack(alignment: .center, spacing: 8, content: {
+            ScrollViewReader { scroller in
+                ScrollView(.horizontal, showsIndicators: false) {
+                    VStack {
+                        MainChart()
+                        BasalChart()
+                            .padding(.bottom, 8)
+                    }.onChange(of: screenHours) { _ in
+                        scroller.scrollTo("MainChart", anchor: .trailing)
+                    }.onAppear {
+                        scroller.scrollTo("MainChart", anchor: .trailing)
+                    }.onChange(of: glucose) { _ in
+                        scroller.scrollTo("MainChart", anchor: .trailing)
+                    }
+                    .onChange(of: suggestion) { _ in
+                        scroller.scrollTo("MainChart", anchor: .trailing)
+                    }
+                    .onChange(of: tempBasals) { _ in
+                        scroller.scrollTo("MainChart", anchor: .trailing)
+                    }
+                }
+            }
+            Legend()
+        })
+    }
+}
+
+// MARK: Components
+
+extension MainChartView2 {
+    private func MainChart() -> some View {
+        VStack {
+            Chart {
+                if thresholdLines {
+                    RuleMark(y: .value("High", highGlucose)).foregroundStyle(Color.loopYellow)
+                        .lineStyle(.init(lineWidth: 1, dash: [2]))
+                    RuleMark(y: .value("Low", lowGlucose)).foregroundStyle(Color.loopRed)
+                        .lineStyle(.init(lineWidth: 1, dash: [2]))
+                }
+                RuleMark(
+                    x: .value(
+                        "",
+                        startMarker,
+                        unit: .second
+                    )
+                ).foregroundStyle(.clear)
+                RuleMark(
+                    x: .value(
+                        "",
+                        Date(timeIntervalSince1970: TimeInterval(NSDate().timeIntervalSince1970)),
+                        unit: .second
+                    )
+                ).lineStyle(.init(lineWidth: 1, dash: [2]))
+                RuleMark(
+                    x: .value(
+                        "",
+                        endMarker,
+                        unit: .second
+                    )
+                ).foregroundStyle(.clear)
+                ForEach(ChartCarbs, id: \.self) { carb in
+                    let carbAmount = carb.amount
+                    PointMark(
+                        x: .value("Time", carb.timestamp, unit: .second),
+                        y: .value("Value", carb.nearestGlucose.sgv ?? 120)
+                    )
+                    .symbolSize((Config.carbsSize + CGFloat(carbAmount) * Config.carbsScale) * 10)
+                    .foregroundStyle(Color.orange)
+                    .annotation(position: .top) {
+                        Text(bolusFormatter.string(from: carbAmount as NSNumber)!).font(.caption2)
+                    }
+                }
+                ForEach(ChartFpus, id: \.self) { fpu in
+                    let fpuAmount = fpu.amount
+                    PointMark(
+                        x: .value("Time", fpu.timestamp, unit: .second),
+                        y: .value("Value", fpu.nearestGlucose.sgv ?? 120)
+                    )
+                    .symbolSize((Config.fpuSize + CGFloat(fpuAmount) * Config.carbsScale) * 10)
+                    .foregroundStyle(Color.brown)
+                    .annotation(position: .top) {
+                        Text(bolusFormatter.string(from: fpuAmount as NSNumber)!).font(.caption2)
+                    }
+                }
+
+                ForEach(ChartBoluses, id: \.self) { bolus in
+                    let bolusAmount = bolus.amount
+                    PointMark(
+                        x: .value("Time", bolus.timestamp, unit: .second),
+                        y: .value("Value", bolus.nearestGlucose.sgv ?? 120)
+                    )
+                    .symbolSize((Config.bolusSize + CGFloat(bolusAmount) * Config.bolusScale) * 10)
+                    .foregroundStyle(Color.insulin)
+                    .annotation(position: .bottom) {
+                        Text(bolusFormatter.string(from: bolusAmount as NSNumber)!).font(.caption2)
+                    }
+                }
+                ForEach(Predictions, id: \.self) { info in
+                    if info.type == .uam {
+                        LineMark(
+                            x: .value("Time", info.timestamp, unit: .second),
+                            y: .value("Value", info.amount),
+                            series: .value("uam", "uam")
+                        ).foregroundStyle(Color.uam).symbolSize(16)
+                    }
+                    if info.type == .cob {
+                        LineMark(
+                            x: .value("Time", info.timestamp, unit: .second),
+                            y: .value("Value", info.amount),
+                            series: .value("cob", "cob")
+                        ).foregroundStyle(Color.orange).symbolSize(16)
+                    }
+                    if info.type == .iob {
+                        LineMark(
+                            x: .value("Time", info.timestamp, unit: .second),
+                            y: .value("Value", info.amount),
+                            series: .value("iob", "iob")
+                        ).foregroundStyle(Color.insulin).symbolSize(16)
+                    }
+                    if info.type == .zt {
+                        LineMark(
+                            x: .value("Time", info.timestamp, unit: .second),
+                            y: .value("Value", info.amount),
+                            series: .value("zt", "zt")
+                        ).foregroundStyle(Color.zt).symbolSize(16)
+                    }
+                }
+                ForEach(glucose) {
+                    if $0.sgv != nil {
+                        PointMark(
+                            x: .value("Time", $0.dateString, unit: .second),
+                            y: .value("Value", $0.sgv!)
+                        ).foregroundStyle(Color.green).symbolSize(16)
+                        if smooth {
+                            LineMark(
+                                x: .value("Time", $0.dateString, unit: .second),
+                                y: .value("Value", $0.sgv!),
+                                series: .value("glucose", "glucose")
+                            ).foregroundStyle(Color.green)
+                        }
+                    }
+                }
+            }.id("MainChart")
+                .onChange(of: glucose) { _ in
+                    calculatePredictions()
+                }
+                .onChange(of: carbs) { _ in
+                    calculateCarbs()
+                    calculateFpus()
+                }
+                .onChange(of: boluses) { _ in
+                    calculateBoluses()
+                }
+                .onChange(of: didAppearTrigger) { _ in
+                    calculatePredictions()
+                }.onChange(of: suggestion) { _ in
+                    calculatePredictions()
+                }
+                .onReceive(
+                    Foundation.NotificationCenter.default
+                        .publisher(for: UIApplication.willEnterForegroundNotification)
+                ) { _ in
+                    calculatePredictions()
+                }
+                .frame(
+                    width: max(0, screenSize.width - 20, fullWidth(viewWidth: screenSize.width)),
+                    height: min(screenSize.height, 200)
+                )
+//                .chartYScale(domain: 0 ... 450)
+                .chartXAxis {
+                    AxisMarks(values: .stride(by: .hour, count: 2)) { _ in
+                        if displayXgridLines {
+                            AxisGridLine(stroke: .init(lineWidth: 0.5, dash: [2, 3]))
+                        } else {
+                            AxisGridLine(stroke: .init(lineWidth: 0, dash: [2, 3]))
+                        }
+                    }
+                }.chartYAxis {
+                    AxisMarks(position: .trailing, values: .stride(by: 100)) { value in
+                        if displayYgridLines {
+                            AxisGridLine(stroke: .init(lineWidth: 0.5, dash: [2, 3]))
+                        } else {
+                            AxisGridLine(stroke: .init(lineWidth: 0, dash: [2, 3]))
+                        }
+                        if let glucoseValue = value.as(Double.self), glucoseValue > 0 {
+                            AxisTick(length: 4, stroke: .init(lineWidth: 4))
+                                .foregroundStyle(Color.gray)
+                            AxisValueLabel()
+                        }
+                    }
+                }
+        }
+    }
+
+    func BasalChart() -> some View {
+        VStack {
+            Chart {
+                RuleMark(
+                    x: .value(
+                        "",
+                        startMarker,
+                        unit: .second
+                    )
+                ).foregroundStyle(.clear)
+                RuleMark(
+                    x: .value(
+                        "",
+                        Date(timeIntervalSince1970: TimeInterval(NSDate().timeIntervalSince1970)),
+                        unit: .second
+                    )
+                ).lineStyle(.init(lineWidth: 1, dash: [2]))
+                RuleMark(
+                    x: .value(
+                        "",
+                        endMarker,
+                        unit: .second
+                    )
+                ).foregroundStyle(.clear)
+                ForEach(TempBasals) {
+                    BarMark(
+                        x: .value("Time", $0.timestamp),
+                        y: .value("Rate", $0.rate ?? 0)
+                    )
+                }
+                ForEach(BasalProfiles, id: \.self) { profile in
+                    LineMark(
+                        x: .value("Start Date", profile.startDate),
+                        y: .value("Amount", profile.amount),
+                        series: .value("profile", "profile")
+                    ).lineStyle(.init(lineWidth: 2, dash: [2, 3]))
+                    LineMark(
+                        x: .value("End Date", profile.endDate ?? endMarker),
+                        y: .value("Amount", profile.amount),
+                        series: .value("profile", "profile")
+                    ).lineStyle(.init(lineWidth: 2, dash: [2, 3]))
+                }
+            }.onChange(of: tempBasals) { _ in
+                calculateBasals()
+                calculateTempBasals()
+            }
+            .onChange(of: maxBasal) { _ in
+                calculateBasals()
+                calculateTempBasals()
+            }
+            .onChange(of: autotunedBasalProfile) { _ in
+                calculateBasals()
+                calculateTempBasals()
+            }
+            .onChange(of: didAppearTrigger) { _ in
+                calculateBasals()
+                calculateTempBasals()
+            }.onChange(of: basalProfile) { _ in
+                calculateBasals()
+                calculateTempBasals()
+            }
+            .frame(height: 80)
+//            .chartYScale(domain: 0 ... maxBasal)
+            //            .rotationEffect(.degrees(180))
+            //            .chartXAxis(.hidden)
+            .chartXAxis {
+                AxisMarks(values: .stride(by: .hour, count: screenHours == 24 ? 4 : 2)) { _ in
+                    if displayXgridLines {
+                        AxisGridLine(stroke: .init(lineWidth: 0.5, dash: [2, 3]))
+                    } else {
+                        AxisGridLine(stroke: .init(lineWidth: 0, dash: [2, 3]))
+                    }
+                    AxisValueLabel(format: .dateTime.hour(.defaultDigits(amPM: .narrow)), anchor: .top)
+                }
+            }.chartYAxis {
+                AxisMarks(position: .trailing, values: .stride(by: 1)) { _ in
+                    if displayYgridLines {
+                        AxisGridLine(stroke: .init(lineWidth: 0.5, dash: [2, 3]))
+                    } else {
+                        AxisGridLine(stroke: .init(lineWidth: 0, dash: [2, 3]))
+                    }
+                    AxisTick(length: 30, stroke: .init(lineWidth: 4))
+                        .foregroundStyle(Color.clear)
+                }
+            }
+
+            .chartPlotStyle { plotArea in
+                plotArea.background(.blue.gradient.opacity(0.1))
+            }
+        }
+    }
+
+    private func Legend() -> 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")
+                .frame(height: 10)
+                .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)
+            if eventualBG != nil {
+                Text("⇢ " + String(eventualBG ?? 0))
+            }
+        }
+        .font(.caption2)
+        .padding(.horizontal, 40)
+        .padding(.vertical, 1)
+    }
+}
+
+// MARK: Calculations
+
+extension MainChartView2 {
+    private func timeToNearestGlucose(time: TimeInterval) -> BloodGlucose {
+        var nextIndex = 0
+        if glucose.last?.dateString.timeIntervalSince1970 ?? Date().timeIntervalSince1970 < time {
+            return glucose.last ?? BloodGlucose(
+                date: 0,
+                dateString: Date(),
+                unfiltered: nil,
+                filtered: nil,
+                noise: nil,
+                type: nil
+            )
+        }
+        for (index, value) in glucose.enumerated() {
+            if value.dateString.timeIntervalSince1970 > time {
+                nextIndex = index
+                print("Break", value.dateString.timeIntervalSince1970, time)
+                break
+            }
+        }
+        return glucose[nextIndex]
+    }
+
+    private func fullWidth(viewWidth: CGFloat) -> CGFloat {
+        viewWidth * CGFloat(hours) / CGFloat(min(max(screenHours, 2), 24))
+    }
+
+    private func calculateCarbs() {
+        var calculatedCarbs: [Carb] = []
+        carbs.forEach { carb in
+            let bg = timeToNearestGlucose(time: carb.createdAt.timeIntervalSince1970)
+            calculatedCarbs.append(Carb(amount: carb.carbs, timestamp: carb.createdAt, nearestGlucose: bg))
+        }
+        ChartCarbs = calculatedCarbs
+    }
+
+    private func calculateFpus() {
+        var calculatedFpus: [Carb] = []
+        let fpus = carbs.filter { $0.isFPU ?? false }
+        fpus.forEach { fpu in
+            let bg = timeToNearestGlucose(time: fpu.createdAt.timeIntervalSince1970)
+            calculatedFpus.append(Carb(amount: fpu.carbs, timestamp: fpu.actualDate ?? Date(), nearestGlucose: bg))
+        }
+        ChartFpus = calculatedFpus
+    }
+
+    private func calculateBoluses() {
+        var calculatedBoluses: [ChartBolus] = []
+        boluses.forEach { bolus in
+            let bg = timeToNearestGlucose(time: bolus.timestamp.timeIntervalSince1970)
+            calculatedBoluses.append(ChartBolus(amount: bolus.amount ?? 0, timestamp: bolus.timestamp, nearestGlucose: bg))
+        }
+        ChartBoluses = calculatedBoluses
+    }
+
+    private func calculatePredictions() {
+        var calculatedPredictions: [Prediction] = []
+        let uam = suggestion?.predictions?.uam ?? []
+        let iob = suggestion?.predictions?.iob ?? []
+        let cob = suggestion?.predictions?.cob ?? []
+        let zt = suggestion?.predictions?.zt ?? []
+        guard let deliveredAt = suggestion?.deliverAt else {
+            return
+        }
+        uam.indices.forEach { index in
+            let predTime = Date(
+                timeIntervalSince1970: deliveredAt.timeIntervalSince1970 + TimeInterval(index) * 5.minutes
+                    .timeInterval
+            )
+            if predTime.timeIntervalSince1970 < endMarker.timeIntervalSince1970 {
+                calculatedPredictions.append(
+                    Prediction(amount: uam[index], timestamp: predTime, type: .uam)
+                )
+            }
+        }
+        iob.indices.forEach { index in
+            let predTime = Date(
+                timeIntervalSince1970: deliveredAt.timeIntervalSince1970 + TimeInterval(index) * 5.minutes
+                    .timeInterval
+            )
+            if predTime.timeIntervalSince1970 < endMarker.timeIntervalSince1970 {
+                calculatedPredictions.append(
+                    Prediction(amount: iob[index], timestamp: predTime, type: .iob)
+                )
+            }
+        }
+        cob.indices.forEach { index in
+            let predTime = Date(
+                timeIntervalSince1970: deliveredAt.timeIntervalSince1970 + TimeInterval(index) * 5.minutes
+                    .timeInterval
+            )
+            if predTime.timeIntervalSince1970 < endMarker.timeIntervalSince1970 {
+                calculatedPredictions.append(
+                    Prediction(amount: cob[index], timestamp: predTime, type: .cob)
+                )
+            }
+        }
+        zt.indices.forEach { index in
+            let predTime = Date(
+                timeIntervalSince1970: deliveredAt.timeIntervalSince1970 + TimeInterval(index) * 5.minutes
+                    .timeInterval
+            )
+            if predTime.timeIntervalSince1970 < endMarker.timeIntervalSince1970 {
+                calculatedPredictions.append(
+                    Prediction(amount: zt[index], timestamp: predTime, type: .zt)
+                )
+            }
+        }
+        Predictions = calculatedPredictions
+    }
+
+    private func getLastUam() -> Int {
+        let uam = suggestion?.predictions?.uam ?? []
+        return uam.last ?? 0
+    }
+
+    private func calculateTempBasals() {
+        var basals = tempBasals
+        var returnTempBasalRates: [PumpHistoryEvent] = []
+        var finished: [Int: Bool] = [:]
+        basals.indices.forEach { i in
+            basals.indices.forEach { j in
+                if basals[i].timestamp == basals[j].timestamp, i != j, !(finished[i] ?? false), !(finished[j] ?? false) {
+                    let rate = basals[i].rate ?? basals[j].rate
+                    let durationMin = basals[i].durationMin ?? basals[j].durationMin
+                    finished[i] = true
+                    if rate != 0 || durationMin != 0 {
+                        returnTempBasalRates.append(
+                            PumpHistoryEvent(
+                                id: basals[i].id, type: FreeAPS.EventType.tempBasal,
+                                timestamp: basals[i].timestamp,
+                                durationMin: durationMin,
+                                rate: rate
+                            )
+                        )
+                    }
+                }
+            }
+        }
+        TempBasals = returnTempBasalRates
+    }
+
+    private func findRegularBasalPoints(
+        timeBegin: TimeInterval,
+        timeEnd: TimeInterval,
+        autotuned: Bool
+    ) -> [BasalProfile] {
+        guard timeBegin < timeEnd else {
+            return []
+        }
+        let beginDate = Date(timeIntervalSince1970: timeBegin)
+        let calendar = Calendar.current
+        let startOfDay = calendar.startOfDay(for: beginDate)
+
+        let profile = autotuned ? autotunedBasalProfile : basalProfile
+
+        let basalNormalized = profile.map {
+            (
+                time: startOfDay.addingTimeInterval($0.minutes.minutes.timeInterval).timeIntervalSince1970,
+                rate: $0.rate
+            )
+        } + profile.map {
+            (
+                time: startOfDay.addingTimeInterval($0.minutes.minutes.timeInterval + 1.days.timeInterval)
+                    .timeIntervalSince1970,
+                rate: $0.rate
+            )
+        } + profile.map {
+            (
+                time: startOfDay.addingTimeInterval($0.minutes.minutes.timeInterval + 2.days.timeInterval)
+                    .timeIntervalSince1970,
+                rate: $0.rate
+            )
+        }
+
+        let basalTruncatedPoints = basalNormalized.windows(ofCount: 2)
+            .compactMap { window -> BasalProfile? in
+                let window = Array(window)
+                if window[0].time < timeBegin, window[1].time < timeBegin {
+                    return nil
+                }
+
+                if window[0].time < timeBegin, window[1].time >= timeBegin {
+                    let startDate = Date(timeIntervalSince1970: timeBegin)
+                    let rate = window[0].rate
+                    return BasalProfile(amount: Double(rate), isOverwritten: false, startDate: startDate)
+                }
+
+                if window[0].time >= timeBegin, window[0].time < timeEnd {
+                    let startDate = Date(timeIntervalSince1970: window[0].time)
+                    let rate = window[0].rate
+                    return BasalProfile(amount: Double(rate), isOverwritten: false, startDate: startDate)
+                }
+
+                return nil
+            }
+
+        return basalTruncatedPoints
+    }
+
+    private func calculateBasals() {
+        let dayAgoTime = Date().addingTimeInterval(-1.days.timeInterval).timeIntervalSince1970
+        let firstTempTime = (tempBasals.first?.timestamp ?? Date()).timeIntervalSince1970
+
+        let regularPoints = findRegularBasalPoints(
+            timeBegin: dayAgoTime,
+            timeEnd: endMarker.timeIntervalSince1970,
+            autotuned: false
+        )
+
+        let autotunedBasalPoints = findRegularBasalPoints(
+            timeBegin: dayAgoTime,
+            timeEnd: endMarker.timeIntervalSince1970,
+            autotuned: true
+        )
+        var totalBasal = regularPoints + autotunedBasalPoints
+        totalBasal.sort {
+            $0.startDate.timeIntervalSince1970 < $1.startDate.timeIntervalSince1970
+        }
+        var basals: [BasalProfile] = []
+        totalBasal.indices.forEach { index in
+            basals.append(BasalProfile(
+                amount: totalBasal[index].amount,
+                isOverwritten: totalBasal[index].isOverwritten,
+                startDate: totalBasal[index].startDate,
+                endDate: totalBasal.count > index + 1 ? totalBasal[index + 1].startDate : endMarker
+            ))
+            print(
+                "Basal",
+                totalBasal[index].startDate,
+                totalBasal.count > index + 1 ? totalBasal[index + 1].startDate : endMarker,
+                totalBasal[index].amount,
+                totalBasal[index].isOverwritten
+            )
+        }
+        BasalProfiles = basals
+    }
+}

+ 3 - 5
FreeAPS/Sources/Modules/Home/View/Header/CurrentGlucoseView.swift

@@ -158,11 +158,9 @@ struct Triangle: Shape {
     func path(in rect: CGRect) -> Path {
         var path = Path()
 
-        let cornerRadius: CGFloat = 8
-
         path.move(to: CGPoint(x: rect.midX, y: rect.minY))
-        path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY - cornerRadius))
-        path.addQuadCurve(to: CGPoint(x: rect.minX, y: rect.maxY - cornerRadius), control: CGPoint(x: rect.midX, y: rect.maxY))
+        path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY + 10))
+        path.addQuadCurve(to: CGPoint(x: rect.minX, y: rect.maxY), control: CGPoint(x: rect.midX, y: rect.maxY + 10))
         path.closeSubpath()
 
         return path
@@ -215,6 +213,6 @@ struct TriangleShape: View {
             .fill(color)
             .frame(width: 35, height: 35)
             .rotationEffect(.degrees(90))
-            .offset(x: 70)
+            .offset(x: 85)
     }
 }

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

@@ -450,9 +450,9 @@ extension Home {
                         .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
                 }
 
-                MainChartView(
+                MainChartView2(
                     glucose: $state.glucose,
-                    isManual: $state.isManual,
+                    eventualBG: $state.eventualBG,
                     suggestion: $state.suggestion,
                     tempBasals: $state.tempBasals,
                     boluses: $state.boluses,
@@ -464,8 +464,6 @@ extension Home {
                     basalProfile: $state.basalProfile,
                     tempTargets: $state.tempTargets,
                     carbs: $state.carbs,
-                    timerDate: $state.timerDate,
-                    units: $state.units,
                     smooth: $state.smooth,
                     highGlucose: $state.highGlucose,
                     lowGlucose: $state.lowGlucose,
@@ -692,7 +690,7 @@ extension Home {
 
                     Spacer()
 
-                    legendPanel
+//                    legendPanel
 
                     Spacer()