BGChartStubs.swift 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. // LoopFollow
  2. // BGChartStubs.swift
  3. import Foundation
  4. import SwiftUI
  5. import UIKit
  6. /// The `update*Graph()` / `createGraph()` entry points the rest of the app
  7. /// calls to refresh the chart. Rendering happens in BGChartModel +
  8. /// BGChartView (Apple Swift Charts); these update lightweight state
  9. /// (topBG / topPredictionBG / prediction arrays) and trigger a (coalesced)
  10. /// model rebuild.
  11. extension MainViewController {
  12. func createGraph() {
  13. chartModel.rebuild()
  14. // The chart's follow/auto-return logic runs off model.now, so arm the
  15. // once-a-minute "now" tick.
  16. startGraphNowTimer()
  17. }
  18. func createSmallBGGraph() {}
  19. func updateBGGraphSettings() {
  20. chartModel.rebuild()
  21. }
  22. private func recomputeTopBG() {
  23. let maxBGOffset: Double = 50
  24. topBG = Storage.shared.minBGScale.value
  25. for entry in bgData {
  26. if Double(entry.sgv) > topBG - maxBGOffset {
  27. topBG = Double(entry.sgv) + maxBGOffset
  28. }
  29. }
  30. }
  31. private func recomputeTopPredictionBG() {
  32. let maxBGOffset: Double = 20
  33. topPredictionBG = Storage.shared.minBGScale.value
  34. let allPredictions = predictionData + ztPredictionData + iobPredictionData + cobPredictionData + uamPredictionData
  35. for entry in allPredictions {
  36. let v = Double(entry.sgv)
  37. if v > topPredictionBG - maxBGOffset {
  38. topPredictionBG = v + maxBGOffset
  39. }
  40. }
  41. }
  42. func updateBGGraph() {
  43. recomputeTopBG()
  44. // Profile processing defers building the scheduled-basal segments until
  45. // the first BG render has happened (Profile.swift checks firstGraphLoad).
  46. // Clear the flag here or the scheduled basal line never gets data.
  47. if !bgData.isEmpty {
  48. firstGraphLoad = false
  49. }
  50. chartModel.rebuild()
  51. }
  52. func updatePredictionGraph(color _: UIColor? = nil) {
  53. recomputeTopPredictionBG()
  54. chartModel.rebuild()
  55. }
  56. func updatePredictionGraphGeneric(
  57. dataIndex: Int,
  58. predictionData: [ShareGlucoseData],
  59. chartLabel _: String,
  60. color _: UIColor
  61. ) {
  62. switch dataIndex {
  63. case 12: ztPredictionData = predictionData
  64. case 13: iobPredictionData = predictionData
  65. case 14: cobPredictionData = predictionData
  66. case 15: uamPredictionData = predictionData
  67. default: break
  68. }
  69. recomputeTopPredictionBG()
  70. chartModel.rebuild()
  71. }
  72. // Removes the Loop forecast line. Used when the active system switches away from Loop.
  73. func clearLoopPredictionGraph() {
  74. guard !predictionData.isEmpty else { return }
  75. predictionData.removeAll()
  76. updatePredictionGraph()
  77. }
  78. // Removes the Trio/OpenAPS forecast (ZT/IOB/COB/UAM lines and the cone). Used when the active system switches away from Trio/OpenAPS.
  79. // Also drops the stored predBGs: the device observer calls updateOpenAPSPredictionDisplay after this runs,
  80. // and would otherwise redraw the cleared forecast from them.
  81. func clearOpenAPSPredictionGraph() {
  82. let hasLineData = !ztPredictionData.isEmpty || !iobPredictionData.isEmpty || !cobPredictionData.isEmpty || !uamPredictionData.isEmpty
  83. guard hasLineData || !chartModel.cone.isEmpty || openAPSPredBGs != nil else { return }
  84. openAPSPredBGs = nil
  85. openAPSPredUpdatedTime = nil
  86. ztPredictionData = []
  87. iobPredictionData = []
  88. cobPredictionData = []
  89. uamPredictionData = []
  90. chartModel.cone = []
  91. recomputeTopPredictionBG()
  92. chartModel.rebuild()
  93. }
  94. // Routes OpenAPS/Trio predictions either into a cone band or individual
  95. // prediction lines. Cone is only for OpenAPS-based backends; Loop always
  96. // uses lines.
  97. func updateOpenAPSPredictionDisplay() {
  98. guard let predBGs = openAPSPredBGs else {
  99. chartModel.cone = []
  100. return
  101. }
  102. let displayType: PredictionDisplayType = Storage.shared.device.value == "Loop"
  103. ? .lines
  104. : Storage.shared.predictionDisplayType.value
  105. let toLoad = Int(Storage.shared.predictionToLoad.value * 12)
  106. let predictionStart = openAPSPredUpdatedTime ?? Date().timeIntervalSince1970
  107. let types = ["ZT", "IOB", "COB", "UAM"]
  108. let minDisplay = globalVariables.minDisplayGlucose
  109. let maxDisplay = globalVariables.maxDisplayGlucose
  110. topPredictionBG = Storage.shared.minBGScale.value
  111. if displayType == .cone {
  112. // Clear individual prediction lines.
  113. ztPredictionData = []
  114. iobPredictionData = []
  115. cobPredictionData = []
  116. uamPredictionData = []
  117. let allArrays = types.compactMap { predBGs[$0] }.filter { !$0.isEmpty }
  118. var coneData: [BGChartModel.ConePoint] = []
  119. if !allArrays.isEmpty {
  120. // Cap at the shortest predBG array length so every cone point uses
  121. // the same set of contributing arrays. Matches Trio's ForecastSetup.
  122. let coneLength = min(allArrays.map { $0.count }.min()!, toLoad + 1)
  123. var t = predictionStart
  124. for i in 0 ..< coneLength {
  125. let valuesAtIndex = allArrays.compactMap { i < $0.count ? $0[i] : nil }
  126. if !valuesAtIndex.isEmpty {
  127. var yMin = max(valuesAtIndex.min()!, Double(minDisplay))
  128. var yMax = min(valuesAtIndex.max()!, Double(maxDisplay))
  129. // Keep a minimum ±1 mg/dL band so the cone stays visible when predictions agree.
  130. if yMin == yMax {
  131. yMin -= 1
  132. yMax += 1
  133. }
  134. coneData.append(BGChartModel.ConePoint(
  135. date: Date(timeIntervalSince1970: t),
  136. yMin: yMin,
  137. yMax: yMax
  138. ))
  139. if yMax > topPredictionBG - 20 { topPredictionBG = yMax + 20 }
  140. }
  141. t += 300
  142. }
  143. }
  144. chartModel.cone = coneData
  145. } else {
  146. chartModel.cone = []
  147. // dataIndex mapping matches updatePredictionGraphGeneric: 12=ZT 13=IOB 14=COB 15=UAM
  148. for (offset, type) in types.enumerated() {
  149. var predictionData: [ShareGlucoseData] = []
  150. if let graphData = predBGs[type] {
  151. var t = predictionStart
  152. for i in 0 ... toLoad where i < graphData.count {
  153. let clamped = min(max(Int(round(graphData[i])), minDisplay), maxDisplay)
  154. predictionData.append(ShareGlucoseData(sgv: clamped, date: t, direction: "flat"))
  155. t += 300
  156. }
  157. }
  158. updatePredictionGraphGeneric(dataIndex: 12 + offset, predictionData: predictionData, chartLabel: type, color: .clear)
  159. }
  160. }
  161. chartModel.rebuild()
  162. }
  163. func updateBasalGraph() { chartModel.rebuild() }
  164. func updateBolusGraph() { chartModel.rebuild() }
  165. func updateCarbGraph() { chartModel.rebuild() }
  166. func updateBasalScheduledGraph() { chartModel.rebuild() }
  167. func updateOverrideGraph() { chartModel.rebuild() }
  168. func updateBGCheckGraph() { chartModel.rebuild() }
  169. func updateSuspendGraph() { chartModel.rebuild() }
  170. func updateResumeGraph() { chartModel.rebuild() }
  171. func updateSensorStart() { chartModel.rebuild() }
  172. func updateNotes() { chartModel.rebuild() }
  173. func updateSmbGraph() { chartModel.rebuild() }
  174. func updateTempTargetGraph() { chartModel.rebuild() }
  175. }