diff --git a/LoopFollow/Charts/BGChartModel.swift b/LoopFollow/Charts/BGChartModel.swift index 8acc2859c..91d4d3521 100644 --- a/LoopFollow/Charts/BGChartModel.swift +++ b/LoopFollow/Charts/BGChartModel.swift @@ -82,6 +82,12 @@ final class BGChartModel: ObservableObject { var id: TimeInterval { date.timeIntervalSince1970 } } + struct ScheduledTargetPoint: Identifiable { + let date: Date + let value: Double + var id: TimeInterval { date.timeIntervalSince1970 } + } + struct BandRect: Identifiable { let start: Date let end: Date @@ -131,6 +137,7 @@ final class BGChartModel: ObservableObject { @Published var basal: [BasalStep] = [] @Published var basalScheduled: [ScheduledBasalPoint] = [] + @Published var targetScheduled: [ScheduledTargetPoint] = [] @Published var boluses: [TreatmentPoint] = [] @Published var carbs: [TreatmentPoint] = [] @@ -421,6 +428,7 @@ final class BGChartModel: ObservableObject { // collect the data regardless (it also feeds the info rows), so hidden // kinds are dropped here at render time. let showBasal = Storage.shared.graphBasal.value + let showTargetLine = Storage.shared.graphTargetLine.value let showBolus = Storage.shared.graphBolus.value let showCarbs = Storage.shared.graphCarbs.value let showOtherTreatments = Storage.shared.graphOtherTreatments.value @@ -520,6 +528,10 @@ final class BGChartModel: ObservableObject { basalScheduled = (showBasal ? vc.basalScheduleData : []).map { ScheduledBasalPoint(date: Date(timeIntervalSince1970: $0.date), rate: $0.basalRate) } + + targetScheduled = (showTargetLine ? vc.targetScheduleData : []).map { + ScheduledTargetPoint(date: Date(timeIntervalSince1970: $0.date), value: $0.targetHigh) + } var steps: [BasalStep] = [] let sortedBasal = (showBasal ? vc.basalData : []).sorted { $0.date < $1.date } diff --git a/LoopFollow/Charts/BGChartStubs.swift b/LoopFollow/Charts/BGChartStubs.swift index 700821a66..80f6599f9 100644 --- a/LoopFollow/Charts/BGChartStubs.swift +++ b/LoopFollow/Charts/BGChartStubs.swift @@ -192,6 +192,7 @@ extension MainViewController { func updateBolusGraph() { chartModel.rebuild() } func updateCarbGraph() { chartModel.rebuild() } func updateBasalScheduledGraph() { chartModel.rebuild() } + func updateTargetScheduledGraph() { chartModel.rebuild() } func updateOverrideGraph() { chartModel.rebuild() } func updateBGCheckGraph() { chartModel.rebuild() } func updateSuspendGraph() { chartModel.rebuild() } diff --git a/LoopFollow/Charts/BGChartView.swift b/LoopFollow/Charts/BGChartView.swift index 74f9ec03e..abcba75f6 100644 --- a/LoopFollow/Charts/BGChartView.swift +++ b/LoopFollow/Charts/BGChartView.swift @@ -1042,6 +1042,7 @@ private struct BGChartCanvas: View, Equatable { bgBandMarks basalMarks scheduledBasalMarks + scheduledTargetMarks } coneMarks if !isSmall { @@ -1209,6 +1210,19 @@ private struct BGChartCanvas: View, Equatable { .foregroundStyle(Color.blue.opacity(0.8)) } } + + @ChartContentBuilder + private var scheduledTargetMarks: some ChartContent { + ForEach(windowedLine(model.targetScheduled) { $0.date }) { pt in + LineMark( + x: .value("time", pt.date), + y: .value("target", pt.value), + series: .value("series", "targetScheduled") + ) + .lineStyle(StrokeStyle(lineWidth: 2, dash: [10, 5])) + .foregroundStyle(Color.green.opacity(0.8)) + } + } @ChartContentBuilder private var coneMarks: some ChartContent { diff --git a/LoopFollow/Controllers/NightScout.swift b/LoopFollow/Controllers/NightScout.swift index 90e3c1e9c..c39509d2e 100644 --- a/LoopFollow/Controllers/NightScout.swift +++ b/LoopFollow/Controllers/NightScout.swift @@ -30,6 +30,12 @@ extension MainViewController { var date: TimeInterval } + // NS Target Data Struct + struct targetGraphStruct: Codable { + var targetHigh: Double + var date: TimeInterval + } + // NS Bolus Data Struct struct bolusGraphStruct: Codable { var value: Double diff --git a/LoopFollow/Controllers/Nightscout/Profile.swift b/LoopFollow/Controllers/Nightscout/Profile.swift index d88c453fa..9c617b7b1 100644 --- a/LoopFollow/Controllers/Nightscout/Profile.swift +++ b/LoopFollow/Controllers/Nightscout/Profile.swift @@ -123,8 +123,10 @@ extension MainViewController { } } - if Storage.shared.graphBasal.value { + if Storage.shared.graphBasal.value { updateBasalScheduledGraph() } + + updateTargetScheduleData() } } diff --git a/LoopFollow/Controllers/Nightscout/TargetSchedule.swift b/LoopFollow/Controllers/Nightscout/TargetSchedule.swift new file mode 100644 index 000000000..a2ae84660 --- /dev/null +++ b/LoopFollow/Controllers/Nightscout/TargetSchedule.swift @@ -0,0 +1,95 @@ +// LoopFollow +// TargetSchedule.swift + +import Foundation + +extension MainViewController { + /// Builds `targetScheduleData`, the routine (non-Override) target-of-the-day line shown on + /// the graph -- mirrors `basalScheduleData` in Profile.swift exactly, substituting the + /// profile's target-high schedule (via `profileManager.targetHighSchedule`, already + /// unit-converted) for the basal schedule. Uses target-high specifically to match + /// `currentTargetHigh()`, the same value already shown in the "Target" info row, so the line + /// never disagrees with the number beside it. + /// + /// Like the basal line, this only ever reflects the day's *routine* schedule -- an active + /// Override, Temp Target, or Weekend Profile is shown separately as a colored band + /// (`overrides`/`tempTargets` in BGChartModel), the same relationship Trio's own target line + /// has with its own Override bands. + func updateTargetScheduleData() { + let targetSchedule = profileManager.targetHighSchedule + guard !targetSchedule.isEmpty else { + targetScheduleData.removeAll() + return + } + + var targetSegments: [DataStructs.targetProfileSegment] = [] + + let graphHours = 24 * Storage.shared.downloadDays.value + // Build scheduled target segments from right to left by moving pointers to the current + // midnight and current target -- same walk as the basal schedule builder. + var midnight = dateTimeUtils.getTimeIntervalMidnightToday() + var targetIndex = targetSchedule.count - 1 + var start = midnight + Double(targetSchedule[targetIndex].timeAsSeconds) + var end = dateTimeUtils.getNowTimeIntervalUTC() + while start > end { + targetIndex -= 1 + start = midnight + Double(targetSchedule[targetIndex].timeAsSeconds) + } + let graphStart = dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) + while end >= graphStart { + let entry = DataStructs.targetProfileSegment( + targetHigh: targetSchedule[targetIndex].value.doubleValue(for: .milligramsPerDeciliter), + startDate: start, endDate: end + ) + targetSegments.append(entry) + + targetIndex -= 1 + if targetIndex < 0 { + targetIndex = targetSchedule.count - 1 + midnight = midnight.advanced(by: -24 * 60 * 60) + } + end = start - 1 + start = midnight + Double(targetSchedule[targetIndex].timeAsSeconds) + } + targetSegments.reverse() + + var firstPass = true + let predictionEndTime = dateTimeUtils.getNowTimeIntervalUTC() + (3600 * Storage.shared.predictionToLoad.value) + targetScheduleData.removeAll() + + for i in 0 ..< targetSegments.count { + let timeStart = dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) + + if firstPass == false, + targetSegments[i].startDate <= predictionEndTime + { + let startDot = targetGraphStruct(targetHigh: targetSegments[i].targetHigh, date: targetSegments[i].startDate) + targetScheduleData.append(startDot) + var endDate = targetSegments[i].endDate + + if endDate > predictionEndTime || i == targetSegments.count - 1 { + endDate = Double(predictionEndTime) + } + + let endDot = targetGraphStruct(targetHigh: targetSegments[i].targetHigh, date: endDate) + targetScheduleData.append(endDot) + } + + if firstPass == true { + if timeStart >= targetSegments[i].startDate, timeStart < targetSegments[i].endDate { + let startDot = targetGraphStruct(targetHigh: targetSegments[i].targetHigh, date: Double(timeStart + (60 * 5))) + targetScheduleData.append(startDot) + + let endDate = targetSegments[i].endDate + let endDot = targetGraphStruct(targetHigh: targetSegments[i].targetHigh, date: endDate) + targetScheduleData.append(endDot) + firstPass = false + } + } + } + + if Storage.shared.graphTargetLine.value { + updateTargetScheduledGraph() + } + } +} diff --git a/LoopFollow/Helpers/DataStructs.swift b/LoopFollow/Helpers/DataStructs.swift index 9cd8a175f..10400f7b4 100644 --- a/LoopFollow/Helpers/DataStructs.swift +++ b/LoopFollow/Helpers/DataStructs.swift @@ -17,6 +17,12 @@ class DataStructs { var endDate: TimeInterval } + struct targetProfileSegment: Codable { + var targetHigh: Double + var startDate: TimeInterval + var endDate: TimeInterval + } + // NS Timestamp Only Data Struct struct timestampOnlyStruct: Codable { var date: TimeInterval diff --git a/LoopFollow/Settings/AdvancedSettingsView.swift b/LoopFollow/Settings/AdvancedSettingsView.swift index 9665df882..49a7375e5 100644 --- a/LoopFollow/Settings/AdvancedSettingsView.swift +++ b/LoopFollow/Settings/AdvancedSettingsView.swift @@ -12,6 +12,7 @@ struct AdvancedSettingsView: View { Toggle("Download Treatments", isOn: $viewModel.downloadTreatments) Toggle("Download Prediction", isOn: $viewModel.downloadPrediction) Toggle("Graph Basal", isOn: $viewModel.graphBasal) + Toggle("Graph Target Line", isOn: $viewModel.graphTargetLine) Toggle("Graph Bolus", isOn: $viewModel.graphBolus) Toggle("Graph Carbs", isOn: $viewModel.graphCarbs) Toggle("Graph Other Treatments", isOn: $viewModel.graphOtherTreatments) diff --git a/LoopFollow/Settings/AdvancedSettingsViewModel.swift b/LoopFollow/Settings/AdvancedSettingsViewModel.swift index 078e62288..9fffec04f 100644 --- a/LoopFollow/Settings/AdvancedSettingsViewModel.swift +++ b/LoopFollow/Settings/AdvancedSettingsViewModel.swift @@ -22,6 +22,13 @@ class AdvancedSettingsViewModel: ObservableObject { Observable.shared.chartSettingsChanged.value = true } } + + @Published var graphTargetLine: Bool { + didSet { + Storage.shared.graphTargetLine.value = graphTargetLine + Observable.shared.chartSettingsChanged.value = true + } + } @Published var graphBolus: Bool { didSet { @@ -60,6 +67,7 @@ class AdvancedSettingsViewModel: ObservableObject { downloadTreatments = Storage.shared.downloadTreatments.value downloadPrediction = Storage.shared.downloadPrediction.value graphBasal = Storage.shared.graphBasal.value + graphTargetLine = Storage.shared.graphTargetLine.value graphBolus = Storage.shared.graphBolus.value graphCarbs = Storage.shared.graphCarbs.value graphOtherTreatments = Storage.shared.graphOtherTreatments.value diff --git a/LoopFollow/Storage/Storage.swift b/LoopFollow/Storage/Storage.swift index dd63f6dd6..de592345f 100644 --- a/LoopFollow/Storage/Storage.swift +++ b/LoopFollow/Storage/Storage.swift @@ -168,6 +168,7 @@ class Storage { var downloadPrediction = StorageValue(key: "downloadPrediction", defaultValue: true) var graphOtherTreatments = StorageValue(key: "graphOtherTreatments", defaultValue: true) var graphBasal = StorageValue(key: "graphBasal", defaultValue: true) + var graphTargetLine = StorageValue(key: "graphTargetLine", defaultValue: true) var graphBolus = StorageValue(key: "graphBolus", defaultValue: true) var graphCarbs = StorageValue(key: "graphCarbs", defaultValue: true) var bgUpdateDelay = StorageValue(key: "bgUpdateDelay", defaultValue: 10) diff --git a/LoopFollow/ViewControllers/MainViewController.swift b/LoopFollow/ViewControllers/MainViewController.swift index 06c5e1f76..3fe44edb7 100644 --- a/LoopFollow/ViewControllers/MainViewController.swift +++ b/LoopFollow/ViewControllers/MainViewController.swift @@ -74,6 +74,7 @@ class MainViewController: UIViewController, UNUserNotificationCenterDelegate { var basalProfile: [basalProfileStruct] = [] var basalData: [basalGraphStruct] = [] var basalScheduleData: [basalGraphStruct] = [] + var targetScheduleData: [targetGraphStruct] = [] var bolusData: [bolusGraphStruct] = [] var smbData: [bolusGraphStruct] = [] var carbData: [carbGraphStruct] = []