Skip to content
Closed
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
204 changes: 106 additions & 98 deletions LoopFollow.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

589 changes: 589 additions & 0 deletions LoopFollow/Charts/BGChartModel.swift

Large diffs are not rendered by default.

170 changes: 170 additions & 0 deletions LoopFollow/Charts/BGChartStubs.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
// LoopFollow
// BGChartStubs.swift

import Foundation
import SwiftUI
import UIKit

/// The `update*Graph()` / `createGraph()` entry points the rest of the app
/// calls to refresh the chart. Rendering happens in BGChartModel +
/// BGChartView (Apple Swift Charts); these update lightweight state
/// (topBG / topPredictionBG / prediction arrays) and trigger a (coalesced)
/// model rebuild.
extension MainViewController {
func createGraph() {
chartModel.rebuild()
// The chart's follow/auto-return logic runs off model.now, so arm the
// once-a-minute "now" tick.
startGraphNowTimer()
}

func createSmallBGGraph() {}

func updateBGGraphSettings() {
chartModel.rebuild()
}

private func recomputeTopBG() {
let maxBGOffset: Double = 50
topBG = Storage.shared.minBGScale.value
for entry in bgData {
if Double(entry.sgv) > topBG - maxBGOffset {
topBG = Double(entry.sgv) + maxBGOffset
}
}
}

private func recomputeTopPredictionBG() {
let maxBGOffset: Double = 20
topPredictionBG = Storage.shared.minBGScale.value
let allPredictions = predictionData + ztPredictionData + iobPredictionData + cobPredictionData + uamPredictionData
for entry in allPredictions {
let v = Double(entry.sgv)
if v > topPredictionBG - maxBGOffset {
topPredictionBG = v + maxBGOffset
}
}
}

func updateBGGraph() {
recomputeTopBG()
// Profile processing defers building the scheduled-basal segments until
// the first BG render has happened (Profile.swift checks firstGraphLoad).
// Clear the flag here or the scheduled basal line never gets data.
if !bgData.isEmpty {
firstGraphLoad = false
}
chartModel.rebuild()
}

func updatePredictionGraph(color _: UIColor? = nil) {
recomputeTopPredictionBG()
chartModel.rebuild()
}

func updatePredictionGraphGeneric(
dataIndex: Int,
predictionData: [ShareGlucoseData],
chartLabel _: String,
color _: UIColor
) {
switch dataIndex {
case 12: ztPredictionData = predictionData
case 13: iobPredictionData = predictionData
case 14: cobPredictionData = predictionData
case 15: uamPredictionData = predictionData
default: break
}
recomputeTopPredictionBG()
chartModel.rebuild()
}

// Routes OpenAPS/Trio predictions either into a cone band or individual
// prediction lines. Cone is only for OpenAPS-based backends; Loop always
// uses lines.
func updateOpenAPSPredictionDisplay() {
guard let predBGs = openAPSPredBGs else {
chartModel.cone = []
return
}

let displayType: PredictionDisplayType = Storage.shared.device.value == "Loop"
? .lines
: Storage.shared.predictionDisplayType.value
let toLoad = Int(Storage.shared.predictionToLoad.value * 12)
let predictionStart = openAPSPredUpdatedTime ?? Date().timeIntervalSince1970
let types = ["ZT", "IOB", "COB", "UAM"]
let minDisplay = globalVariables.minDisplayGlucose
let maxDisplay = globalVariables.maxDisplayGlucose

topPredictionBG = Storage.shared.minBGScale.value

if displayType == .cone {
// Clear individual prediction lines.
ztPredictionData = []
iobPredictionData = []
cobPredictionData = []
uamPredictionData = []

let allArrays = types.compactMap { predBGs[$0] }.filter { !$0.isEmpty }
var coneData: [BGChartModel.ConePoint] = []
if !allArrays.isEmpty {
// Cap at the shortest predBG array length so every cone point uses
// the same set of contributing arrays. Matches Trio's ForecastSetup.
let coneLength = min(allArrays.map { $0.count }.min()!, toLoad + 1)
var t = predictionStart
for i in 0 ..< coneLength {
let valuesAtIndex = allArrays.compactMap { i < $0.count ? $0[i] : nil }
if !valuesAtIndex.isEmpty {
var yMin = max(valuesAtIndex.min()!, Double(minDisplay))
var yMax = min(valuesAtIndex.max()!, Double(maxDisplay))
// Keep a minimum ±1 mg/dL band so the cone stays visible when predictions agree.
if yMin == yMax {
yMin -= 1
yMax += 1
}
coneData.append(BGChartModel.ConePoint(
date: Date(timeIntervalSince1970: t),
yMin: yMin,
yMax: yMax
))
if yMax > topPredictionBG - 20 { topPredictionBG = yMax + 20 }
}
t += 300
}
}
chartModel.cone = coneData
} else {
chartModel.cone = []

// dataIndex mapping matches updatePredictionGraphGeneric: 12=ZT 13=IOB 14=COB 15=UAM
for (offset, type) in types.enumerated() {
var predictionData: [ShareGlucoseData] = []
if let graphData = predBGs[type] {
var t = predictionStart
for i in 0 ... toLoad where i < graphData.count {
let clamped = min(max(Int(round(graphData[i])), minDisplay), maxDisplay)
predictionData.append(ShareGlucoseData(sgv: clamped, date: t, direction: "flat"))
t += 300
}
}
updatePredictionGraphGeneric(dataIndex: 12 + offset, predictionData: predictionData, chartLabel: type, color: .clear)
}
}

chartModel.rebuild()
}

func updateBasalGraph() { chartModel.rebuild() }
func updateBolusGraph() { chartModel.rebuild() }
func updateCarbGraph() { chartModel.rebuild() }
func updateBasalScheduledGraph() { chartModel.rebuild() }
func updateOverrideGraph() { chartModel.rebuild() }
func updateBGCheckGraph() { chartModel.rebuild() }
func updateSuspendGraph() { chartModel.rebuild() }
func updateResumeGraph() { chartModel.rebuild() }
func updateSensorStart() { chartModel.rebuild() }
func updateNotes() { chartModel.rebuild() }
func updateSmbGraph() { chartModel.rebuild() }
func updateTempTargetGraph() { chartModel.rebuild() }
}
Loading
Loading