Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 156 additions & 1 deletion LoopFollow/Charts/BGChartModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,17 @@ final class BGChartModel: ObservableObject {
let points: [BGPoint]
}

struct OnBoardPoint: Identifiable, Equatable {
let date: Date
let value: Double
var id: TimeInterval { date.timeIntervalSince1970 }
}

struct OnBoardRun: Identifiable, Equatable {
let id: Int
let points: [OnBoardPoint]
}

@Published var bg: [BGPoint] = []
@Published var bgRuns: [BGRun] = []
@Published var yesterday: [BGPoint] = []
Expand All @@ -119,6 +130,15 @@ final class BGChartModel: ObservableObject {
@Published var cobPrediction: [BGPoint] = []
@Published var uamPrediction: [BGPoint] = []

@Published var iobHistory: [OnBoardPoint] = []
@Published var cobHistory: [OnBoardPoint] = []
@Published var iobHistoryRuns: [OnBoardRun] = []
@Published var cobHistoryRuns: [OnBoardRun] = []
@Published var iobHistoryMaximum: Double = 1
@Published var cobHistoryMaximum: Double = 1
@Published var currentIOB: Double?
@Published var currentCOB: Double?

/// Prediction cone band (min/max envelope). Set by updateOpenAPSPredictionDisplay;
/// preserved across rebuild() since it has no source array on the view controller.
/// The didSet keeps the canvas generation in sync for call sites that assign the
Expand Down Expand Up @@ -178,6 +198,7 @@ final class BGChartModel: ObservableObject {
@Published var show30Min: Bool = false
@Published var show90Min: Bool = false
@Published var showMidnight: Bool = false
@Published var showIOBCOBHistory: Bool = true
@Published var smallGraphTreatments: Bool = true

private static let doseFormatter: NumberFormatter = {
Expand Down Expand Up @@ -287,6 +308,90 @@ final class BGChartModel: ObservableObject {
return runs
}

static let onBoardLaneFraction = 0.22
static let onBoardGapInterval: TimeInterval = 12 * 60
static let onBoardSelectionTolerance: TimeInterval = 7.5 * 60
static let onBoardCurrentFreshness: TimeInterval = 15 * 60

static func onBoardLaneCeiling(maxBG: Double, lowLine: Double) -> Double {
max(0, min(maxBG * onBoardLaneFraction, lowLine * 0.9))
}

static func scaledOnBoardValue(
_ value: Double,
maximum: Double,
laneCeiling: Double
) -> Double {
guard value.isFinite, maximum.isFinite, maximum > 0, laneCeiling > 0 else {
return 0
}
return min(max(value, 0) / maximum, 1) * laneCeiling
}

static func onBoardMaximum(for points: [OnBoardPoint]) -> Double {
max(points.lazy.map(\.value).filter { $0.isFinite && $0 > 0 }.max() ?? 0, 1)
}

static func makeOnBoardRuns(
_ points: [OnBoardPoint],
maximumGap: TimeInterval = onBoardGapInterval
) -> [OnBoardRun] {
guard let first = points.first else { return [] }

var runs: [OnBoardRun] = []
var runPoints = [first]

for point in points.dropFirst() {
if let previous = runPoints.last,
point.date.timeIntervalSince(previous.date) > maximumGap
{
runs.append(OnBoardRun(id: runs.count, points: runPoints))
runPoints = [point]
} else {
runPoints.append(point)
}
}

runs.append(OnBoardRun(id: runs.count, points: runPoints))
return runs
}

static func nearestOnBoardPoint(
in points: [OnBoardPoint],
to date: Date,
tolerance: TimeInterval = onBoardSelectionTolerance
) -> OnBoardPoint? {
guard !points.isEmpty else { return nil }

var lower = 0
var upper = points.count
while lower < upper {
let middle = (lower + upper) / 2
if points[middle].date < date {
lower = middle + 1
} else {
upper = middle
}
}

var candidates: [OnBoardPoint] = []
if lower < points.count {
candidates.append(points[lower])
}
if lower > 0 {
candidates.append(points[lower - 1])
}

guard let nearest = candidates.min(by: {
abs($0.date.timeIntervalSince(date)) < abs($1.date.timeIntervalSince(date))
}), abs(nearest.date.timeIntervalSince(date)) <= tolerance
else {
return nil
}

return nearest
}

/// Minimum drawn spacing between two treatments of the same population, and
/// the furthest a treatment may be moved from its true time to reach it.
/// Boluses and SMBs share a y-anchor and symbol footprint, so they are
Expand Down Expand Up @@ -393,6 +498,7 @@ final class BGChartModel: ObservableObject {
private func performRebuild() {
guard let vc = MainViewController.shared else { return }

let currentNow = Date(timeIntervalSince1970: dateTimeUtils.getNowTimeIntervalUTC())
pillTimeFormatter = Self.makePillTimeFormatter()

let maxBGValue = Double(vc.calculateMaxBgGraphValue())
Expand All @@ -410,6 +516,7 @@ final class BGChartModel: ObservableObject {
show30Min = Storage.shared.show30MinLine.value
show90Min = Storage.shared.show90MinLine.value
showMidnight = Storage.shared.showMidnightLines.value
showIOBCOBHistory = Storage.shared.showIOBCOBHistory.value
smallGraphTreatments = Storage.shared.smallGraphTreatments.value

// Advanced-settings visibility toggles. The Nightscout controllers
Expand Down Expand Up @@ -448,6 +555,55 @@ final class BGChartModel: ObservableObject {
cobPrediction = vc.cobPredictionData.map { BGPoint(date: Date(timeIntervalSince1970: $0.date), value: Double($0.sgv), color: .purple) }
uamPrediction = vc.uamPredictionData.map { BGPoint(date: Date(timeIntervalSince1970: $0.date), value: Double($0.sgv), color: .purple) }

if showIOBCOBHistory {
let historyCutoff = currentNow.addingTimeInterval(
-TimeInterval(max(Storage.shared.downloadDays.value, 1) * 24 * 3600)
)
let samples = vc.deviceStatusMetricHistory.filter {
$0.date >= historyCutoff && $0.date <= currentNow.addingTimeInterval(10 * 60)
}

iobHistory = samples.compactMap { sample in
guard let value = sample.iob, value.isFinite else { return nil }
return OnBoardPoint(date: sample.date, value: value)
}
cobHistory = samples.compactMap { sample in
guard let value = sample.cob, value.isFinite else { return nil }
return OnBoardPoint(date: sample.date, value: value)
}
iobHistoryMaximum = Self.onBoardMaximum(for: iobHistory)
cobHistoryMaximum = Self.onBoardMaximum(for: cobHistory)
iobHistoryRuns = Self.makeOnBoardRuns(iobHistory)
cobHistoryRuns = Self.makeOnBoardRuns(cobHistory)

if let point = iobHistory.last,
currentNow.timeIntervalSince(point.date) >= -10 * 60,
currentNow.timeIntervalSince(point.date) <= Self.onBoardCurrentFreshness
{
currentIOB = point.value
} else {
currentIOB = nil
}

if let point = cobHistory.last,
currentNow.timeIntervalSince(point.date) >= -10 * 60,
currentNow.timeIntervalSince(point.date) <= Self.onBoardCurrentFreshness
{
currentCOB = point.value
} else {
currentCOB = nil
}
} else {
iobHistory = []
cobHistory = []
iobHistoryRuns = []
cobHistoryRuns = []
iobHistoryMaximum = 1
cobHistoryMaximum = 1
currentIOB = nil
currentCOB = nil
}

let bolusPoints = (showBolus ? vc.bolusData : []).map {
let dose = self.formatDose($0.value)
return TreatmentPoint(
Expand Down Expand Up @@ -562,7 +718,6 @@ final class BGChartModel: ObservableObject {
)
}

let currentNow = Date(timeIntervalSince1970: dateTimeUtils.getNowTimeIntervalUTC())
now = currentNow
let hoursBack = TimeInterval(Storage.shared.downloadDays.value * 24 * 3600)
domainStart = currentNow.addingTimeInterval(-hoursBack)
Expand Down
4 changes: 4 additions & 0 deletions LoopFollow/Charts/BGChartStubs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ extension MainViewController {
// The yesterday overlay is built during the BG fetch and needs an
// extra day of history, so reload the BG window when it's toggled.
TaskScheduler.shared.rescheduleTask(id: .fetchBG, to: Date())

// Increasing "Show Days Back" requires a wider one-time device-status
// backfill for the on-board history overlay.
loadDeviceStatusMetricHistoryIfNeeded()
}

private func recomputeTopBG() {
Expand Down
Loading
Loading