Graphs.swift 92 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207
  1. // LoopFollow
  2. // Graphs.swift
  3. import Charts
  4. import Foundation
  5. import UIKit
  6. import Charts
  7. /// Fill colors for the override and temp-target bars on the BG graph.
  8. ///
  9. /// Loop draws overrides green and temp targets purple, while Trio (and other
  10. /// OpenAPS-based algorithms) use the inverse — overrides purple, temp targets
  11. /// green. We follow the active backend's convention so the colors match the
  12. /// looping app the user is running.
  13. enum TreatmentGraphColors {
  14. static var override: NSUIColor {
  15. Storage.shared.device.value == "Loop" ? .systemGreen : .systemPurple
  16. }
  17. static var tempTarget: NSUIColor {
  18. Storage.shared.device.value == "Loop" ? .systemPurple : .systemGreen
  19. }
  20. }
  21. enum GraphDataIndex: Int {
  22. case bg = 0
  23. case prediction = 1
  24. case basal = 2
  25. case bolus = 3
  26. case carbs = 4
  27. case basalScheduled = 5
  28. case override = 6
  29. case bgCheck = 7
  30. case suspend = 8
  31. case resumePump = 9
  32. case sensorStart = 10
  33. case note = 11
  34. case ztPrediction = 12
  35. case iobPrediction = 13
  36. case cobPrediction = 14
  37. case uamPrediction = 15
  38. case smb = 16
  39. case tempTarget = 17
  40. case predictionCone = 18
  41. case yesterday = 19
  42. }
  43. extension GraphDataIndex {
  44. var description: String {
  45. switch self {
  46. case .bg: return "BG"
  47. case .prediction: return "Prediction"
  48. case .basal: return "Basal"
  49. case .bolus: return "Bolus"
  50. case .carbs: return "Carbs"
  51. case .basalScheduled: return "Basal Scheduled"
  52. case .override: return "Override"
  53. case .bgCheck: return "BG Check"
  54. case .suspend: return "Suspend"
  55. case .resumePump: return "Resume Pump"
  56. case .sensorStart: return "Sensor Start"
  57. case .note: return "Note"
  58. case .ztPrediction: return "ZT Prediction"
  59. case .iobPrediction: return "IOB Prediction"
  60. case .cobPrediction: return "COB Prediction"
  61. case .uamPrediction: return "UAM Prediction"
  62. case .smb: return "SMB"
  63. case .tempTarget: return "Temp Target"
  64. case .predictionCone: return "Prediction Cone"
  65. case .yesterday: return "Yesterday"
  66. }
  67. }
  68. }
  69. class CompositeRenderer: LineChartRenderer {
  70. let tempTargetRenderer: TempTargetRenderer
  71. let triangleRenderer: TriangleRenderer
  72. let coneRenderer: ConeOfUncertaintyRenderer
  73. init(dataProvider: LineChartDataProvider?, animator: Animator?, viewPortHandler: ViewPortHandler?, tempTargetDataSetIndex: Int, smbDataSetIndex: Int, coneDataSetIndex: Int) {
  74. tempTargetRenderer = TempTargetRenderer(
  75. dataProvider: dataProvider,
  76. animator: animator,
  77. viewPortHandler: viewPortHandler,
  78. tempTargetDataSetIndex: tempTargetDataSetIndex
  79. )
  80. triangleRenderer = TriangleRenderer(
  81. dataProvider: dataProvider,
  82. animator: animator,
  83. viewPortHandler: viewPortHandler,
  84. smbDataSetIndex: smbDataSetIndex
  85. )
  86. coneRenderer = ConeOfUncertaintyRenderer(
  87. dataProvider: dataProvider,
  88. animator: animator,
  89. viewPortHandler: viewPortHandler,
  90. coneDataSetIndex: coneDataSetIndex
  91. )
  92. super.init(dataProvider: dataProvider!, animator: animator!, viewPortHandler: viewPortHandler!)
  93. }
  94. override func drawExtras(context: CGContext) {
  95. super.drawExtras(context: context)
  96. coneRenderer.drawExtras(context: context)
  97. tempTargetRenderer.drawExtras(context: context)
  98. triangleRenderer.drawExtras(context: context)
  99. }
  100. }
  101. class TriangleRenderer: LineChartRenderer {
  102. let smbDataSetIndex: Int
  103. init(dataProvider: LineChartDataProvider?, animator: Animator?, viewPortHandler: ViewPortHandler?, smbDataSetIndex: Int) {
  104. self.smbDataSetIndex = smbDataSetIndex
  105. super.init(dataProvider: dataProvider!, animator: animator!, viewPortHandler: viewPortHandler!)
  106. }
  107. override func drawExtras(context: CGContext) {
  108. super.drawExtras(context: context)
  109. guard let dataProvider = dataProvider else { return }
  110. if dataProvider.lineData?.dataSets.count ?? 0 > smbDataSetIndex, let lineDataSet = dataProvider.lineData?.dataSets[smbDataSetIndex] as? LineChartDataSet {
  111. let trans = dataProvider.getTransformer(forAxis: lineDataSet.axisDependency)
  112. let phaseY = animator.phaseY
  113. for j in 0 ..< lineDataSet.entryCount {
  114. guard let e = lineDataSet.entryForIndex(j) else { continue }
  115. let pt = trans.pixelForValues(x: e.x, y: e.y * phaseY)
  116. context.saveGState()
  117. context.beginPath()
  118. context.move(to: CGPoint(x: pt.x, y: pt.y + 9))
  119. context.addLine(to: CGPoint(x: pt.x - 5, y: pt.y - 1))
  120. context.addLine(to: CGPoint(x: pt.x + 5, y: pt.y - 1))
  121. context.closePath()
  122. context.setFillColor(lineDataSet.circleColors.first!.cgColor)
  123. context.fillPath()
  124. context.restoreGState()
  125. }
  126. }
  127. }
  128. }
  129. class TempTargetChartDataEntry: ChartDataEntry {
  130. var xStart: Double = 0.0
  131. var xEnd: Double = 0.0
  132. var yTop: Double = 0.0
  133. var yBottom: Double = 0.0
  134. required init() {
  135. super.init()
  136. }
  137. init(xStart: Double, xEnd: Double, yTop: Double, yBottom: Double, data: Any?) {
  138. self.xStart = xStart
  139. self.xEnd = xEnd
  140. self.yTop = yTop
  141. self.yBottom = yBottom
  142. super.init(x: xStart, y: yTop)
  143. self.data = data
  144. }
  145. override func copy(with _: NSZone? = nil) -> Any {
  146. let copy = TempTargetChartDataEntry(
  147. xStart: xStart,
  148. xEnd: xEnd,
  149. yTop: yTop,
  150. yBottom: yBottom,
  151. data: data
  152. )
  153. return copy
  154. }
  155. }
  156. class TempTargetRenderer: LineChartRenderer {
  157. let tempTargetDataSetIndex: Int
  158. init(dataProvider: LineChartDataProvider?, animator: Animator?, viewPortHandler: ViewPortHandler?, tempTargetDataSetIndex: Int) {
  159. self.tempTargetDataSetIndex = tempTargetDataSetIndex
  160. super.init(dataProvider: dataProvider!, animator: animator!, viewPortHandler: viewPortHandler!)
  161. }
  162. override func drawExtras(context: CGContext) {
  163. super.drawExtras(context: context)
  164. guard let dataProvider = dataProvider else { return }
  165. if dataProvider.lineData?.dataSets.count ?? 0 > tempTargetDataSetIndex,
  166. let lineDataSet = dataProvider.lineData?.dataSets[tempTargetDataSetIndex] as? LineChartDataSet
  167. {
  168. let trans = dataProvider.getTransformer(forAxis: lineDataSet.axisDependency)
  169. let phaseY = animator.phaseY
  170. for i in 0 ..< lineDataSet.entryCount {
  171. guard let entry = lineDataSet.entryForIndex(i) as? TempTargetChartDataEntry else { continue }
  172. let xStart = entry.xStart
  173. let xEnd = entry.xEnd
  174. let yTop = entry.yTop * phaseY
  175. let yBottom = entry.yBottom * phaseY
  176. let leftTop = trans.pixelForValues(x: xStart, y: yTop)
  177. let rightBottom = trans.pixelForValues(x: xEnd, y: yBottom)
  178. var rect = CGRect(x: leftTop.x, y: leftTop.y, width: rightBottom.x - leftTop.x, height: rightBottom.y - leftTop.y)
  179. if rect.width < 0 {
  180. rect.origin.x += rect.width
  181. rect.size.width = abs(rect.width)
  182. }
  183. if rect.height < 0 {
  184. rect.origin.y += rect.height
  185. rect.size.height = abs(rect.height)
  186. }
  187. context.saveGState()
  188. context.setFillColor(TreatmentGraphColors.tempTarget.withAlphaComponent(0.5).cgColor)
  189. context.fill(rect)
  190. context.restoreGState()
  191. }
  192. }
  193. }
  194. }
  195. let ScaleXMax: Double = 150.0
  196. extension MainViewController {
  197. private func graphRangeThresholds() -> (low: Double, high: Double) {
  198. UnitSettingsStore.shared.effectiveThresholds()
  199. }
  200. func updateChartRenderers() {
  201. let tempTargetDataIndex = GraphDataIndex.tempTarget.rawValue
  202. let smbDataIndex = GraphDataIndex.smb.rawValue
  203. let coneDataIndex = GraphDataIndex.predictionCone.rawValue
  204. let compositeRenderer = CompositeRenderer(
  205. dataProvider: BGChart,
  206. animator: BGChart.chartAnimator,
  207. viewPortHandler: BGChart.viewPortHandler,
  208. tempTargetDataSetIndex: tempTargetDataIndex,
  209. smbDataSetIndex: smbDataIndex,
  210. coneDataSetIndex: coneDataIndex
  211. )
  212. BGChart.renderer = compositeRenderer
  213. BGChart.data?.notifyDataChanged()
  214. BGChart.notifyDataSetChanged()
  215. }
  216. func chartValueSelected(_ chartView: ChartViewBase, entry: ChartDataEntry, highlight _: Highlight) {
  217. if chartView == BGChartFull {
  218. BGChart.moveViewToX(entry.x)
  219. }
  220. if entry.data as? String == "hide" {
  221. BGChart.highlightValue(nil, callDelegate: false)
  222. }
  223. }
  224. func chartScaled(_: ChartViewBase, scaleX _: CGFloat, scaleY _: CGFloat) {
  225. // dont store huge values
  226. var scale = Double(BGChart.scaleX)
  227. if scale > ScaleXMax {
  228. scale = ScaleXMax
  229. }
  230. Storage.shared.chartScaleX.value = scale
  231. }
  232. func createGraph() {
  233. // Create the BG Graph Data
  234. let bgChartEntry = [ChartDataEntry]()
  235. let maxBG = Storage.shared.minBGScale.value
  236. // Setup BG line details
  237. let lineBG = LineChartDataSet(entries: bgChartEntry, label: "")
  238. lineBG.circleRadius = CGFloat(globalVariables.dotBG)
  239. lineBG.circleColors = [NSUIColor.systemGreen]
  240. lineBG.drawCircleHoleEnabled = false
  241. lineBG.axisDependency = YAxis.AxisDependency.right
  242. lineBG.highlightEnabled = true
  243. lineBG.drawValuesEnabled = false
  244. if Storage.shared.showLines.value {
  245. lineBG.lineWidth = 2
  246. } else {
  247. lineBG.lineWidth = 0
  248. }
  249. if Storage.shared.showDots.value {
  250. lineBG.drawCirclesEnabled = true
  251. } else {
  252. lineBG.drawCirclesEnabled = false
  253. }
  254. lineBG.setDrawHighlightIndicators(false)
  255. lineBG.valueFont.withSize(50)
  256. // Setup Prediction line details
  257. let predictionChartEntry = [ChartDataEntry]()
  258. let linePrediction = LineChartDataSet(entries: predictionChartEntry, label: "")
  259. linePrediction.circleRadius = CGFloat(globalVariables.dotBG)
  260. linePrediction.circleColors = [NSUIColor.systemPurple]
  261. linePrediction.colors = [NSUIColor.systemPurple]
  262. linePrediction.drawCircleHoleEnabled = false
  263. linePrediction.axisDependency = YAxis.AxisDependency.right
  264. linePrediction.highlightEnabled = true
  265. linePrediction.drawValuesEnabled = false
  266. if Storage.shared.showLines.value {
  267. linePrediction.lineWidth = 2
  268. } else {
  269. linePrediction.lineWidth = 0
  270. }
  271. if Storage.shared.showDots.value {
  272. linePrediction.drawCirclesEnabled = true
  273. } else {
  274. linePrediction.drawCirclesEnabled = false
  275. }
  276. linePrediction.setDrawHighlightIndicators(false)
  277. linePrediction.valueFont.withSize(50)
  278. // create Basal graph data
  279. let chartEntry = [ChartDataEntry]()
  280. let maxBasal = Storage.shared.minBasalScale.value
  281. let lineBasal = LineChartDataSet(entries: chartEntry, label: "")
  282. lineBasal.setDrawHighlightIndicators(false)
  283. lineBasal.setColor(NSUIColor.systemBlue, alpha: 0.5)
  284. lineBasal.lineWidth = 0
  285. lineBasal.drawFilledEnabled = true
  286. lineBasal.fillColor = NSUIColor.systemBlue
  287. lineBasal.fillAlpha = 0.5
  288. lineBasal.drawCirclesEnabled = false
  289. lineBasal.axisDependency = YAxis.AxisDependency.left
  290. lineBasal.highlightEnabled = true
  291. lineBasal.drawValuesEnabled = false
  292. lineBasal.fillFormatter = basalFillFormatter()
  293. // Boluses
  294. let chartEntryBolus = [ChartDataEntry]()
  295. let lineBolus = LineChartDataSet(entries: chartEntryBolus, label: "")
  296. lineBolus.circleRadius = CGFloat(globalVariables.dotBolus)
  297. lineBolus.circleColors = [NSUIColor.systemBlue.withAlphaComponent(0.75)]
  298. lineBolus.drawCircleHoleEnabled = false
  299. lineBolus.setDrawHighlightIndicators(false)
  300. lineBolus.setColor(NSUIColor.systemBlue, alpha: 1.0)
  301. lineBolus.lineWidth = 0
  302. lineBolus.axisDependency = YAxis.AxisDependency.right
  303. lineBolus.valueFormatter = ChartYDataValueFormatter()
  304. lineBolus.valueTextColor = NSUIColor.label
  305. lineBolus.fillColor = NSUIColor.systemBlue
  306. lineBolus.fillAlpha = 0.6
  307. lineBolus.drawCirclesEnabled = true
  308. lineBolus.drawFilledEnabled = false
  309. lineBolus.drawValuesEnabled = Storage.shared.showValues.value
  310. lineBolus.highlightEnabled = true
  311. // Carbs
  312. let chartEntryCarbs = [ChartDataEntry]()
  313. let lineCarbs = LineChartDataSet(entries: chartEntryCarbs, label: "")
  314. lineCarbs.circleRadius = CGFloat(globalVariables.dotCarb)
  315. lineCarbs.circleColors = [NSUIColor.systemOrange.withAlphaComponent(0.75)]
  316. lineCarbs.drawCircleHoleEnabled = false
  317. lineCarbs.setDrawHighlightIndicators(false)
  318. lineCarbs.setColor(NSUIColor.systemBlue, alpha: 1.0)
  319. lineCarbs.lineWidth = 0
  320. lineCarbs.axisDependency = YAxis.AxisDependency.right
  321. lineCarbs.valueFormatter = ChartYDataValueFormatter()
  322. lineCarbs.valueTextColor = NSUIColor.label
  323. lineCarbs.fillColor = NSUIColor.systemOrange
  324. lineCarbs.fillAlpha = 0.6
  325. lineCarbs.drawCirclesEnabled = true
  326. lineCarbs.drawFilledEnabled = false
  327. lineCarbs.drawValuesEnabled = Storage.shared.showValues.value
  328. lineCarbs.highlightEnabled = true
  329. // create Scheduled Basal graph data
  330. let chartBasalScheduledEntry = [ChartDataEntry]()
  331. let lineBasalScheduled = LineChartDataSet(entries: chartBasalScheduledEntry, label: "")
  332. lineBasalScheduled.setDrawHighlightIndicators(false)
  333. lineBasalScheduled.setColor(NSUIColor.systemBlue, alpha: 0.8)
  334. lineBasalScheduled.lineWidth = 2
  335. lineBasalScheduled.drawFilledEnabled = false
  336. lineBasalScheduled.drawCirclesEnabled = false
  337. lineBasalScheduled.axisDependency = YAxis.AxisDependency.left
  338. lineBasalScheduled.highlightEnabled = false
  339. lineBasalScheduled.drawValuesEnabled = false
  340. lineBasalScheduled.lineDashLengths = [10.0, 5.0]
  341. // create Override graph data
  342. let chartOverrideEntry = [ChartDataEntry]()
  343. let lineOverride = LineChartDataSet(entries: chartOverrideEntry, label: "")
  344. lineOverride.setDrawHighlightIndicators(false)
  345. lineOverride.lineWidth = 0
  346. lineOverride.drawFilledEnabled = true
  347. lineOverride.fillFormatter = OverrideFillFormatter()
  348. lineOverride.fillColor = TreatmentGraphColors.override
  349. lineOverride.fillAlpha = 0.6
  350. lineOverride.drawCirclesEnabled = false
  351. lineOverride.axisDependency = YAxis.AxisDependency.right
  352. lineOverride.highlightEnabled = true
  353. lineOverride.drawValuesEnabled = false
  354. // BG Check
  355. let chartEntryBGCheck = [ChartDataEntry]()
  356. let lineBGCheck = LineChartDataSet(entries: chartEntryBGCheck, label: "")
  357. lineBGCheck.circleRadius = CGFloat(globalVariables.dotOther)
  358. lineBGCheck.circleColors = [NSUIColor.systemRed.withAlphaComponent(0.75)]
  359. lineBGCheck.drawCircleHoleEnabled = false
  360. lineBGCheck.setDrawHighlightIndicators(false)
  361. lineBGCheck.setColor(NSUIColor.systemRed, alpha: 1.0)
  362. lineBGCheck.drawCirclesEnabled = true
  363. lineBGCheck.lineWidth = 0
  364. lineBGCheck.highlightEnabled = true
  365. lineBGCheck.axisDependency = YAxis.AxisDependency.right
  366. lineBGCheck.valueFormatter = ChartYDataValueFormatter()
  367. lineBGCheck.drawValuesEnabled = false
  368. // Suspend Pump
  369. let chartEntrySuspend = [ChartDataEntry]()
  370. let lineSuspend = LineChartDataSet(entries: chartEntrySuspend, label: "")
  371. lineSuspend.circleRadius = CGFloat(globalVariables.dotOther)
  372. lineSuspend.circleColors = [NSUIColor.systemTeal.withAlphaComponent(0.75)]
  373. lineSuspend.drawCircleHoleEnabled = false
  374. lineSuspend.setDrawHighlightIndicators(false)
  375. lineSuspend.setColor(NSUIColor.systemGray2, alpha: 1.0)
  376. lineSuspend.drawCirclesEnabled = true
  377. lineSuspend.lineWidth = 0
  378. lineSuspend.highlightEnabled = true
  379. lineSuspend.axisDependency = YAxis.AxisDependency.right
  380. lineSuspend.valueFormatter = ChartYDataValueFormatter()
  381. lineSuspend.drawValuesEnabled = false
  382. // Resume Pump
  383. let chartEntryResume = [ChartDataEntry]()
  384. let lineResume = LineChartDataSet(entries: chartEntryResume, label: "")
  385. lineResume.circleRadius = CGFloat(globalVariables.dotOther)
  386. lineResume.circleColors = [NSUIColor.systemTeal.withAlphaComponent(0.75)]
  387. lineResume.drawCircleHoleEnabled = false
  388. lineResume.setDrawHighlightIndicators(false)
  389. lineResume.setColor(NSUIColor.systemGray4, alpha: 1.0)
  390. lineResume.drawCirclesEnabled = true
  391. lineResume.lineWidth = 0
  392. lineResume.highlightEnabled = true
  393. lineResume.axisDependency = YAxis.AxisDependency.right
  394. lineResume.valueFormatter = ChartYDataValueFormatter()
  395. lineResume.drawValuesEnabled = false
  396. // Sensor Start
  397. let chartEntrySensor = [ChartDataEntry]()
  398. let lineSensor = LineChartDataSet(entries: chartEntrySensor, label: "")
  399. lineSensor.circleRadius = CGFloat(globalVariables.dotOther)
  400. lineSensor.circleColors = [NSUIColor.systemIndigo.withAlphaComponent(0.75)]
  401. lineSensor.drawCircleHoleEnabled = false
  402. lineSensor.setDrawHighlightIndicators(false)
  403. lineSensor.setColor(NSUIColor.systemGray3, alpha: 1.0)
  404. lineSensor.drawCirclesEnabled = true
  405. lineSensor.lineWidth = 0
  406. lineSensor.highlightEnabled = true
  407. lineSensor.axisDependency = YAxis.AxisDependency.right
  408. lineSensor.valueFormatter = ChartYDataValueFormatter()
  409. lineSensor.drawValuesEnabled = false
  410. // Notes
  411. let chartEntryNote = [ChartDataEntry]()
  412. let lineNote = LineChartDataSet(entries: chartEntryNote, label: "")
  413. lineNote.circleRadius = CGFloat(globalVariables.dotOther)
  414. lineNote.circleColors = [NSUIColor.systemGray.withAlphaComponent(0.75)]
  415. lineNote.drawCircleHoleEnabled = false
  416. lineNote.setDrawHighlightIndicators(false)
  417. lineNote.setColor(NSUIColor.systemGray3, alpha: 1.0)
  418. lineNote.drawCirclesEnabled = true
  419. lineNote.lineWidth = 0
  420. lineNote.highlightEnabled = true
  421. lineNote.axisDependency = YAxis.AxisDependency.right
  422. lineNote.valueFormatter = ChartYDataValueFormatter()
  423. lineNote.drawValuesEnabled = false
  424. // Setup COB Prediction line details
  425. let COBpredictionChartEntry = [ChartDataEntry]()
  426. let COBlinePrediction = LineChartDataSet(entries: COBpredictionChartEntry, label: "")
  427. COBlinePrediction.circleRadius = CGFloat(globalVariables.dotBG)
  428. COBlinePrediction.circleColors = [NSUIColor.systemPurple]
  429. COBlinePrediction.colors = [NSUIColor.systemPurple]
  430. COBlinePrediction.drawCircleHoleEnabled = false
  431. COBlinePrediction.axisDependency = YAxis.AxisDependency.right
  432. COBlinePrediction.highlightEnabled = true
  433. COBlinePrediction.drawValuesEnabled = false
  434. if Storage.shared.showLines.value {
  435. COBlinePrediction.lineWidth = 2
  436. } else {
  437. COBlinePrediction.lineWidth = 0
  438. }
  439. if Storage.shared.showDots.value {
  440. COBlinePrediction.drawCirclesEnabled = true
  441. } else {
  442. COBlinePrediction.drawCirclesEnabled = false
  443. }
  444. COBlinePrediction.setDrawHighlightIndicators(false)
  445. COBlinePrediction.valueFont.withSize(50)
  446. // Setup IOB Prediction line details
  447. let IOBpredictionChartEntry = [ChartDataEntry]()
  448. let IOBlinePrediction = LineChartDataSet(entries: IOBpredictionChartEntry, label: "")
  449. IOBlinePrediction.circleRadius = CGFloat(globalVariables.dotBG)
  450. IOBlinePrediction.circleColors = [NSUIColor.systemPurple]
  451. IOBlinePrediction.colors = [NSUIColor.systemPurple]
  452. IOBlinePrediction.drawCircleHoleEnabled = false
  453. IOBlinePrediction.axisDependency = YAxis.AxisDependency.right
  454. IOBlinePrediction.highlightEnabled = true
  455. IOBlinePrediction.drawValuesEnabled = false
  456. if Storage.shared.showLines.value {
  457. IOBlinePrediction.lineWidth = 2
  458. } else {
  459. IOBlinePrediction.lineWidth = 0
  460. }
  461. if Storage.shared.showDots.value {
  462. IOBlinePrediction.drawCirclesEnabled = true
  463. } else {
  464. IOBlinePrediction.drawCirclesEnabled = false
  465. }
  466. IOBlinePrediction.setDrawHighlightIndicators(false)
  467. IOBlinePrediction.valueFont.withSize(50)
  468. // Setup UAM Prediction line details
  469. let UAMpredictionChartEntry = [ChartDataEntry]()
  470. let UAMlinePrediction = LineChartDataSet(entries: UAMpredictionChartEntry, label: "")
  471. UAMlinePrediction.circleRadius = CGFloat(globalVariables.dotBG)
  472. UAMlinePrediction.circleColors = [NSUIColor.systemPurple]
  473. UAMlinePrediction.colors = [NSUIColor.systemPurple]
  474. UAMlinePrediction.drawCircleHoleEnabled = false
  475. UAMlinePrediction.axisDependency = YAxis.AxisDependency.right
  476. UAMlinePrediction.highlightEnabled = true
  477. UAMlinePrediction.drawValuesEnabled = false
  478. if Storage.shared.showLines.value {
  479. UAMlinePrediction.lineWidth = 2
  480. } else {
  481. UAMlinePrediction.lineWidth = 0
  482. }
  483. if Storage.shared.showDots.value {
  484. UAMlinePrediction.drawCirclesEnabled = true
  485. } else {
  486. UAMlinePrediction.drawCirclesEnabled = false
  487. }
  488. linePrediction.setDrawHighlightIndicators(false)
  489. linePrediction.valueFont.withSize(50)
  490. // Setup ZT Prediction line details
  491. let ZTpredictionChartEntry = [ChartDataEntry]()
  492. let ZTlinePrediction = LineChartDataSet(entries: ZTpredictionChartEntry, label: "")
  493. ZTlinePrediction.circleRadius = CGFloat(globalVariables.dotBG)
  494. ZTlinePrediction.circleColors = [NSUIColor.systemPurple]
  495. ZTlinePrediction.colors = [NSUIColor.systemPurple]
  496. ZTlinePrediction.drawCircleHoleEnabled = false
  497. ZTlinePrediction.axisDependency = YAxis.AxisDependency.right
  498. ZTlinePrediction.highlightEnabled = true
  499. ZTlinePrediction.drawValuesEnabled = false
  500. if Storage.shared.showLines.value {
  501. ZTlinePrediction.lineWidth = 2
  502. } else {
  503. ZTlinePrediction.lineWidth = 0
  504. }
  505. if Storage.shared.showDots.value {
  506. ZTlinePrediction.drawCirclesEnabled = true
  507. } else {
  508. ZTlinePrediction.drawCirclesEnabled = false
  509. }
  510. ZTlinePrediction.setDrawHighlightIndicators(false)
  511. ZTlinePrediction.valueFont.withSize(50)
  512. // SMB
  513. let chartEntrySmb = [ChartDataEntry]()
  514. let lineSmb = LineChartDataSet(entries: chartEntrySmb, label: "")
  515. lineSmb.circleRadius = CGFloat(globalVariables.dotBolus)
  516. lineSmb.circleColors = [NSUIColor.systemBlue.withAlphaComponent(1.0)]
  517. lineSmb.drawCircleHoleEnabled = false
  518. lineSmb.setDrawHighlightIndicators(false)
  519. lineSmb.setColor(NSUIColor.red, alpha: 1.0)
  520. lineSmb.lineWidth = 0
  521. lineSmb.axisDependency = YAxis.AxisDependency.right
  522. lineSmb.valueFormatter = ChartYDataValueFormatter()
  523. lineSmb.valueTextColor = NSUIColor.label
  524. lineSmb.drawCirclesEnabled = false
  525. lineSmb.drawFilledEnabled = false
  526. lineSmb.drawValuesEnabled = Storage.shared.showValues.value
  527. lineSmb.highlightEnabled = true
  528. // TempTarget graph data
  529. let chartTempTargetEntry = [ChartDataEntry]()
  530. let lineTempTarget = LineChartDataSet(entries: chartTempTargetEntry, label: "")
  531. lineTempTarget.setDrawHighlightIndicators(false)
  532. lineTempTarget.lineWidth = 0
  533. lineTempTarget.drawFilledEnabled = false
  534. lineTempTarget.fillColor = NSUIColor.systemPurple
  535. lineTempTarget.fillAlpha = 0.6
  536. lineTempTarget.drawCirclesEnabled = false
  537. lineTempTarget.axisDependency = YAxis.AxisDependency.right
  538. lineTempTarget.highlightEnabled = true
  539. lineTempTarget.drawValuesEnabled = false
  540. // Setup the chart data of all lines
  541. let data = LineChartData()
  542. data.append(lineBG) // Dataset 0
  543. data.append(linePrediction) // Dataset 1
  544. data.append(lineBasal) // Dataset 2
  545. data.append(lineBolus) // Dataset 3
  546. data.append(lineCarbs) // Dataset 4
  547. data.append(lineBasalScheduled) // Dataset 5
  548. data.append(lineOverride) // Dataset 6
  549. data.append(lineBGCheck) // Dataset 7
  550. data.append(lineSuspend) // Dataset 8
  551. data.append(lineResume) // Dataset 9
  552. data.append(lineSensor) // Dataset 10
  553. data.append(lineNote) // Dataset 11
  554. data.append(ZTlinePrediction) // Dataset 12
  555. data.append(IOBlinePrediction) // Dataset 13
  556. data.append(COBlinePrediction) // Dataset 14
  557. data.append(UAMlinePrediction) // Dataset 15
  558. data.append(lineSmb) // Dataset 16
  559. data.append(lineTempTarget) // Dataset 17
  560. // Dataset 18: Prediction Cone (rendered via ConeOfUncertaintyRenderer)
  561. let lineCone = LineChartDataSet(entries: [ChartDataEntry](), label: "")
  562. lineCone.lineWidth = 0
  563. lineCone.drawCirclesEnabled = false
  564. lineCone.drawValuesEnabled = false
  565. lineCone.highlightEnabled = false
  566. lineCone.axisDependency = YAxis.AxisDependency.right
  567. data.append(lineCone)
  568. // Dataset 19: Yesterday's BG comparison overlay (thin dimmed gray line, no dots)
  569. let lineYesterday = LineChartDataSet(entries: [ChartDataEntry](), label: "")
  570. lineYesterday.lineWidth = 1.5
  571. lineYesterday.setColor(NSUIColor.systemGray, alpha: 0.4)
  572. lineYesterday.drawCirclesEnabled = false
  573. lineYesterday.drawValuesEnabled = false
  574. lineYesterday.highlightEnabled = false
  575. lineYesterday.axisDependency = YAxis.AxisDependency.right
  576. data.append(lineYesterday)
  577. data.setValueFont(UIFont.systemFont(ofSize: 12))
  578. // Add marker popups for bolus and carbs
  579. let marker = PillMarker(color: .secondarySystemBackground, font: UIFont.boldSystemFont(ofSize: 14), textColor: .label)
  580. BGChart.marker = marker
  581. // Clear limit lines so they don't add multiples when changing the settings
  582. BGChart.rightAxis.removeAllLimitLines()
  583. let thresholds = graphRangeThresholds()
  584. // Add lower red line
  585. let ll = ChartLimitLine()
  586. ll.limit = thresholds.low
  587. ll.lineColor = NSUIColor.systemRed.withAlphaComponent(0.5)
  588. BGChart.rightAxis.addLimitLine(ll)
  589. // Add upper yellow line
  590. let ul = ChartLimitLine()
  591. ul.limit = thresholds.high
  592. ul.lineColor = NSUIColor.systemYellow.withAlphaComponent(0.5)
  593. BGChart.rightAxis.addLimitLine(ul)
  594. // Add vertical lines as configured
  595. createVerticalLines()
  596. startGraphNowTimer()
  597. // Setup the main graph overall details
  598. BGChart.xAxis.valueFormatter = ChartXValueFormatter()
  599. BGChart.xAxis.granularity = 1800
  600. BGChart.xAxis.labelTextColor = NSUIColor.label
  601. BGChart.xAxis.labelPosition = XAxis.LabelPosition.bottom
  602. BGChart.xAxis.drawGridLinesEnabled = false
  603. BGChart.leftAxis.enabled = true
  604. BGChart.leftAxis.labelPosition = YAxis.LabelPosition.insideChart
  605. BGChart.leftAxis.axisMaximum = maxBasal
  606. BGChart.leftAxis.axisMinimum = 0
  607. BGChart.leftAxis.drawGridLinesEnabled = false
  608. BGChart.leftAxis.granularityEnabled = true
  609. BGChart.leftAxis.granularity = 0.5
  610. BGChart.rightAxis.labelTextColor = NSUIColor.label
  611. BGChart.rightAxis.labelPosition = YAxis.LabelPosition.insideChart
  612. BGChart.rightAxis.axisMinimum = 0.0
  613. BGChart.rightAxis.axisMaximum = Double(maxBG)
  614. BGChart.rightAxis.gridLineDashLengths = [5.0, 5.0]
  615. BGChart.rightAxis.drawGridLinesEnabled = false
  616. BGChart.rightAxis.valueFormatter = ChartYMMOLValueFormatter()
  617. BGChart.rightAxis.granularityEnabled = true
  618. BGChart.rightAxis.granularity = 50
  619. BGChart.maxHighlightDistance = 15.0
  620. BGChart.legend.enabled = false
  621. BGChart.scaleYEnabled = false
  622. BGChart.drawGridBackgroundEnabled = true
  623. BGChart.gridBackgroundColor = NSUIColor.secondarySystemBackground
  624. BGChart.highlightValue(nil, callDelegate: false)
  625. BGChart.data = data
  626. BGChart.setExtraOffsets(left: 5, top: 10, right: 5, bottom: 10)
  627. }
  628. func createVerticalLines() {
  629. BGChart.xAxis.removeAllLimitLines()
  630. BGChartFull.xAxis.removeAllLimitLines()
  631. createNowAndDIALines()
  632. createMidnightLines()
  633. }
  634. func createNowAndDIALines() {
  635. let ul = ChartLimitLine()
  636. ul.limit = Double(dateTimeUtils.getNowTimeIntervalUTC())
  637. ul.lineColor = NSUIColor.systemGray.withAlphaComponent(0.5)
  638. ul.lineWidth = 1
  639. BGChart.xAxis.addLimitLine(ul)
  640. if Storage.shared.show30MinLine.value {
  641. let ul2 = ChartLimitLine()
  642. ul2.limit = Double(dateTimeUtils.getNowTimeIntervalUTC().advanced(by: -30 * 60))
  643. ul2.lineColor = NSUIColor.systemBlue.withAlphaComponent(0.5)
  644. ul2.lineWidth = 1
  645. BGChart.xAxis.addLimitLine(ul2)
  646. }
  647. if Storage.shared.showDIALines.value {
  648. for i in 1 ..< 7 {
  649. let ul = ChartLimitLine()
  650. ul.limit = Double(dateTimeUtils.getNowTimeIntervalUTC() - Double(i * 60 * 60))
  651. ul.lineColor = NSUIColor.systemGray.withAlphaComponent(0.3)
  652. let dash = 10.0 - Double(i)
  653. let space = 5.0 + Double(i)
  654. ul.lineDashLengths = [CGFloat(dash), CGFloat(space)]
  655. ul.lineWidth = 1
  656. BGChart.xAxis.addLimitLine(ul)
  657. }
  658. }
  659. if Storage.shared.show90MinLine.value {
  660. let ul3 = ChartLimitLine()
  661. ul3.limit = Double(dateTimeUtils.getNowTimeIntervalUTC().advanced(by: -90 * 60))
  662. ul3.lineColor = NSUIColor.systemOrange.withAlphaComponent(0.5)
  663. ul3.lineWidth = 1
  664. BGChart.xAxis.addLimitLine(ul3)
  665. }
  666. }
  667. func createMidnightLines() {
  668. // Draw a line at midnight: useful when showing multiple days of data
  669. if Storage.shared.showMidnightLines.value {
  670. var midnightTimeInterval: TimeInterval
  671. if Storage.shared.graphTimeZoneEnabled.value,
  672. let tz = TimeZone(identifier: Storage.shared.graphTimeZoneIdentifier.value)
  673. {
  674. var cal = Calendar.current
  675. cal.timeZone = tz
  676. midnightTimeInterval = cal.startOfDay(for: Date()).timeIntervalSince1970
  677. } else {
  678. midnightTimeInterval = dateTimeUtils.getTimeIntervalMidnightToday()
  679. }
  680. let graphHours = 24 * Storage.shared.downloadDays.value
  681. let graphStart = dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours)
  682. while midnightTimeInterval > graphStart {
  683. // Large chart
  684. let ul = ChartLimitLine()
  685. ul.limit = Double(midnightTimeInterval)
  686. ul.lineColor = NSUIColor.systemTeal.withAlphaComponent(0.5)
  687. ul.lineDashLengths = [CGFloat(2), CGFloat(5)]
  688. ul.lineWidth = 1
  689. BGChart.xAxis.addLimitLine(ul)
  690. // Small chart
  691. let sl = ChartLimitLine()
  692. sl.limit = Double(midnightTimeInterval)
  693. sl.lineColor = NSUIColor.systemTeal
  694. sl.lineDashLengths = [CGFloat(2), CGFloat(2)]
  695. sl.lineWidth = 1
  696. BGChartFull.xAxis.addLimitLine(sl)
  697. midnightTimeInterval = midnightTimeInterval.advanced(by: -24 * 60 * 60)
  698. }
  699. }
  700. }
  701. func updateBGGraphSettings() {
  702. let dataIndex = 0
  703. let dataIndexPrediction = 1
  704. let lineBG = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet
  705. let linePrediction = BGChart.lineData!.dataSets[dataIndexPrediction] as! LineChartDataSet
  706. if Storage.shared.showLines.value {
  707. lineBG.lineWidth = 2
  708. linePrediction.lineWidth = 2
  709. } else {
  710. lineBG.lineWidth = 0
  711. linePrediction.lineWidth = 0
  712. }
  713. if Storage.shared.showDots.value {
  714. lineBG.drawCirclesEnabled = true
  715. linePrediction.drawCirclesEnabled = true
  716. } else {
  717. lineBG.drawCirclesEnabled = false
  718. linePrediction.drawCirclesEnabled = false
  719. }
  720. BGChart.rightAxis.axisMinimum = 0
  721. // Clear limit lines so they don't add multiples when changing the settings
  722. BGChart.rightAxis.removeAllLimitLines()
  723. let thresholds = graphRangeThresholds()
  724. // Add lower red line
  725. let ll = ChartLimitLine()
  726. ll.limit = thresholds.low
  727. ll.lineColor = NSUIColor.systemRed.withAlphaComponent(0.5)
  728. BGChart.rightAxis.addLimitLine(ll)
  729. // Add upper yellow line
  730. let ul = ChartLimitLine()
  731. ul.limit = thresholds.high
  732. ul.lineColor = NSUIColor.systemYellow.withAlphaComponent(0.5)
  733. BGChart.rightAxis.addLimitLine(ul)
  734. // Re-create vertical markers in case their settings changed
  735. createVerticalLines()
  736. BGChart.data?.dataSets[dataIndex].notifyDataSetChanged()
  737. BGChart.data?.notifyDataChanged()
  738. BGChart.notifyDataSetChanged()
  739. // Reflect the yesterday overlay toggle immediately, and reload the BG window
  740. // so the extra day of history is fetched (or dropped) when the toggle changed.
  741. updateYesterdayBGGraph()
  742. TaskScheduler.shared.rescheduleTask(id: .fetchBG, to: Date())
  743. // Re-render prediction display in case display type changed
  744. updateOpenAPSPredictionDisplay()
  745. }
  746. func updateBGGraph() {
  747. let dataIndex = 0
  748. let entries = bgData
  749. guard !entries.isEmpty else {
  750. return
  751. }
  752. let mainChart = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet
  753. let smallChart = BGChartFull.lineData!.dataSets[dataIndex] as! LineChartDataSet
  754. mainChart.removeAll(keepingCapacity: false)
  755. smallChart.removeAll(keepingCapacity: false)
  756. let maxBGOffset: Double = 50
  757. var colors = [NSUIColor]()
  758. topBG = Storage.shared.minBGScale.value
  759. let thresholds = graphRangeThresholds()
  760. for i in 0 ..< entries.count {
  761. // Clamp the plotted y-value to the same bounds the header text uses
  762. // (HIGH/LOW), so the graph stays consistent with the main display.
  763. // The pill tooltip still shows the raw reading.
  764. let plottedSgv = Double(min(max(entries[i].sgv, globalVariables.minDisplayGlucose), globalVariables.maxDisplayGlucose))
  765. if plottedSgv > topBG - maxBGOffset {
  766. topBG = plottedSgv + maxBGOffset
  767. }
  768. let value = ChartDataEntry(x: Double(entries[i].date), y: plottedSgv, data: formatPillText(line1: Localizer.toDisplayUnits(String(entries[i].sgv)), time: entries[i].date))
  769. mainChart.append(value)
  770. smallChart.append(value)
  771. if Double(entries[i].sgv) >= thresholds.high {
  772. colors.append(NSUIColor.systemYellow)
  773. } else if Double(entries[i].sgv) <= thresholds.low {
  774. colors.append(NSUIColor.systemRed)
  775. } else {
  776. colors.append(NSUIColor.systemGreen)
  777. }
  778. }
  779. // Set Colors
  780. let lineBG = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet
  781. let lineBGSmall = BGChartFull.lineData!.dataSets[dataIndex] as! LineChartDataSet
  782. lineBG.colors.removeAll()
  783. lineBG.circleColors.removeAll()
  784. lineBGSmall.colors.removeAll()
  785. lineBGSmall.circleColors.removeAll()
  786. if colors.count > 0 {
  787. for i in 0 ..< colors.count {
  788. mainChart.addColor(colors[i])
  789. mainChart.circleColors.append(colors[i])
  790. smallChart.addColor(colors[i])
  791. smallChart.circleColors.append(colors[i])
  792. }
  793. }
  794. BGChart.rightAxis.axisMaximum = Double(calculateMaxBgGraphValue())
  795. BGChart.setVisibleXRangeMinimum(600)
  796. BGChart.data?.dataSets[dataIndex].notifyDataSetChanged()
  797. BGChart.data?.notifyDataChanged()
  798. BGChart.notifyDataSetChanged()
  799. BGChartFull.rightAxis.axisMaximum = Double(calculateMaxBgGraphValue())
  800. BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged()
  801. BGChartFull.data?.notifyDataChanged()
  802. BGChartFull.notifyDataSetChanged()
  803. updateYesterdayBGGraph()
  804. // The initial zoom is a one-shot, relative to the chart's current
  805. // viewport. Skip it until the chart actually has a width — otherwise a
  806. // refresh that lands while the view is loaded but off-screen (e.g. Home
  807. // lives in the Menu, so MainViewController is force-loaded headless by
  808. // bootstrap()) would consume firstGraphLoad against a zero-size viewport
  809. // and leave the main graph blank. viewDidAppear re-runs updateBGGraph
  810. // once a real frame exists, applying the zoom correctly.
  811. if firstGraphLoad, BGChart.bounds.width > 0 {
  812. var scaleX = CGFloat(Storage.shared.chartScaleX.value)
  813. if scaleX > CGFloat(ScaleXMax) {
  814. scaleX = CGFloat(ScaleXMax)
  815. Storage.shared.chartScaleX.value = ScaleXMax
  816. }
  817. BGChart.zoom(scaleX: scaleX, scaleY: 1, x: 1, y: 1)
  818. firstGraphLoad = false
  819. }
  820. // Move to current reading everytime new readings load
  821. // Check if auto-scrolling should be performed
  822. if autoScrollPauseUntil == nil || Date() > autoScrollPauseUntil! {
  823. BGChart.moveViewToAnimated(xValue: dateTimeUtils.getNowTimeIntervalUTC() - (BGChart.visibleXRange * 0.7), yValue: 0.0, axis: .right, duration: 1, easingOption: .easeInBack)
  824. }
  825. }
  826. // Populates (or clears) the dimmed "yesterday" comparison overlay on the main graph.
  827. // Points in yesterdayBGData are already shifted +24h so they align with today's clock time.
  828. func updateYesterdayBGGraph() {
  829. let dataIndex = GraphDataIndex.yesterday.rawValue
  830. guard let lineData = BGChart.lineData,
  831. dataIndex < lineData.dataSets.count,
  832. let dataSet = lineData.dataSets[dataIndex] as? LineChartDataSet
  833. else {
  834. return
  835. }
  836. dataSet.removeAll(keepingCapacity: false)
  837. if Storage.shared.showYesterdayLine.value {
  838. for entry in yesterdayBGData {
  839. // Clamp the plotted y-value to the same bounds the main BG line uses.
  840. let plottedSgv = Double(min(max(entry.sgv, globalVariables.minDisplayGlucose), globalVariables.maxDisplayGlucose))
  841. dataSet.append(ChartDataEntry(x: entry.date, y: plottedSgv))
  842. }
  843. }
  844. BGChart.data?.dataSets[dataIndex].notifyDataSetChanged()
  845. BGChart.data?.notifyDataChanged()
  846. BGChart.notifyDataSetChanged()
  847. }
  848. // Removes the Loop forecast line. Used when the active system switches away from Loop.
  849. func clearLoopPredictionGraph() {
  850. guard !predictionData.isEmpty else { return }
  851. predictionData.removeAll()
  852. updatePredictionGraph()
  853. }
  854. // Removes the Trio/OpenAPS forecast lines (ZT/IOB/COB/UAM). Used when the active system switches away from Trio/OpenAPS.
  855. func clearOpenAPSPredictionGraph() {
  856. let openAPSDataIndices = [12, 13, 14, 15]
  857. for dataIndex in openAPSDataIndices {
  858. let mainChart = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet
  859. let smallChart = BGChartFull.lineData!.dataSets[dataIndex] as! LineChartDataSet
  860. if !mainChart.entries.isEmpty || !smallChart.entries.isEmpty {
  861. updatePredictionGraphGeneric(
  862. dataIndex: dataIndex,
  863. predictionData: [],
  864. chartLabel: "",
  865. color: UIColor.systemGray
  866. )
  867. }
  868. }
  869. }
  870. func updatePredictionGraph(color: UIColor? = nil) {
  871. let dataIndex = 1
  872. var mainChart = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet
  873. var smallChart = BGChartFull.lineData!.dataSets[dataIndex] as! LineChartDataSet
  874. mainChart.clear()
  875. smallChart.clear()
  876. var colors = [NSUIColor]()
  877. let maxBGOffset: Double = 20
  878. topPredictionBG = Storage.shared.minBGScale.value
  879. for i in 0 ..< predictionData.count {
  880. var predictionVal = Double(predictionData[i].sgv)
  881. if Double(predictionVal) > topPredictionBG - maxBGOffset {
  882. topPredictionBG = predictionVal + maxBGOffset
  883. }
  884. if i == 0 {
  885. if Storage.shared.showDots.value {
  886. colors.append((color ?? NSUIColor.systemPurple).withAlphaComponent(0.0))
  887. } else {
  888. colors.append((color ?? NSUIColor.systemPurple).withAlphaComponent(1.0))
  889. }
  890. } else if predictionVal > 400 {
  891. colors.append(color ?? NSUIColor.systemYellow)
  892. } else if predictionVal < 0 {
  893. colors.append(color ?? NSUIColor.systemRed)
  894. } else {
  895. colors.append(color ?? NSUIColor.systemPurple)
  896. }
  897. let value = ChartDataEntry(x: predictionData[i].date, y: predictionVal, data: formatPillText(line1: Localizer.toDisplayUnits(String(predictionData[i].sgv)), time: predictionData[i].date))
  898. mainChart.addEntry(value)
  899. smallChart.addEntry(value)
  900. }
  901. smallChart.circleColors.removeAll()
  902. smallChart.colors.removeAll()
  903. mainChart.colors.removeAll()
  904. mainChart.circleColors.removeAll()
  905. if colors.count > 0 {
  906. for i in 0 ..< colors.count {
  907. mainChart.addColor(colors[i])
  908. mainChart.circleColors.append(colors[i])
  909. smallChart.addColor(colors[i])
  910. smallChart.circleColors.append(colors[i])
  911. }
  912. }
  913. BGChart.rightAxis.axisMaximum = Double(calculateMaxBgGraphValue())
  914. BGChart.data?.dataSets[dataIndex].notifyDataSetChanged()
  915. BGChart.data?.notifyDataChanged()
  916. BGChart.notifyDataSetChanged()
  917. BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged()
  918. BGChartFull.data?.notifyDataChanged()
  919. BGChartFull.notifyDataSetChanged()
  920. }
  921. func updateBasalGraph() {
  922. var dataIndex = 2
  923. BGChart.lineData?.dataSets[dataIndex].clear()
  924. BGChartFull.lineData?.dataSets[dataIndex].clear()
  925. var maxBasal = Storage.shared.minBasalScale.value
  926. var maxBasalSmall = 0.0
  927. for i in 0 ..< basalData.count {
  928. let value = ChartDataEntry(x: Double(basalData[i].date), y: Double(basalData[i].basalRate), data: formatPillText(line1: String(basalData[i].basalRate), time: basalData[i].date))
  929. BGChart.data?.dataSets[dataIndex].addEntry(value)
  930. if Storage.shared.smallGraphTreatments.value {
  931. BGChartFull.data?.dataSets[dataIndex].addEntry(value)
  932. }
  933. if basalData[i].basalRate > maxBasal {
  934. maxBasal = basalData[i].basalRate
  935. }
  936. if basalData[i].basalRate > maxBasalSmall {
  937. maxBasalSmall = basalData[i].basalRate
  938. }
  939. }
  940. BGChart.leftAxis.axisMaximum = maxBasal
  941. BGChartFull.leftAxis.axisMaximum = maxBasalSmall
  942. BGChart.data?.dataSets[dataIndex].notifyDataSetChanged()
  943. BGChart.data?.notifyDataChanged()
  944. BGChart.notifyDataSetChanged()
  945. if Storage.shared.smallGraphTreatments.value {
  946. BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged()
  947. BGChartFull.data?.notifyDataChanged()
  948. BGChartFull.notifyDataSetChanged()
  949. }
  950. }
  951. func updateBasalScheduledGraph() {
  952. var dataIndex = 5
  953. BGChart.lineData?.dataSets[dataIndex].clear()
  954. BGChartFull.lineData?.dataSets[dataIndex].clear()
  955. for i in 0 ..< basalScheduleData.count {
  956. let value = ChartDataEntry(x: Double(basalScheduleData[i].date), y: Double(basalScheduleData[i].basalRate))
  957. BGChart.data?.dataSets[dataIndex].addEntry(value)
  958. if Storage.shared.smallGraphTreatments.value {
  959. BGChartFull.data?.dataSets[dataIndex].addEntry(value)
  960. }
  961. }
  962. BGChart.data?.dataSets[dataIndex].notifyDataSetChanged()
  963. BGChart.data?.notifyDataChanged()
  964. BGChart.notifyDataSetChanged()
  965. if Storage.shared.smallGraphTreatments.value {
  966. BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged()
  967. BGChartFull.data?.notifyDataChanged()
  968. BGChartFull.notifyDataSetChanged()
  969. }
  970. }
  971. func updateBolusGraph() {
  972. var dataIndex = 3
  973. var yTop: Double = 370
  974. var yBottom: Double = 345
  975. var mainChart = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet
  976. var smallChart = BGChartFull.lineData!.dataSets[dataIndex] as! LineChartDataSet
  977. mainChart.clear()
  978. smallChart.clear()
  979. var colors = [NSUIColor]()
  980. for i in 0 ..< bolusData.count {
  981. let formatter = NumberFormatter()
  982. formatter.minimumFractionDigits = 0
  983. formatter.maximumFractionDigits = 2
  984. formatter.minimumIntegerDigits = 0
  985. // Check overlapping carbs to shift left if needed
  986. let bolusShift = findNextBolusTime(timeWithin: 240, needle: bolusData[i].date, haystack: bolusData, startingIndex: i)
  987. var dateTimeStamp = bolusData[i].date
  988. colors.append(NSUIColor.systemBlue.withAlphaComponent(1.0))
  989. if bolusShift {
  990. // Move it half the distance between BG readings
  991. dateTimeStamp = dateTimeStamp - 150
  992. }
  993. // skip if outside of visible area
  994. let graphHours = 24 * Storage.shared.downloadDays.value
  995. if dateTimeStamp < dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) { continue }
  996. let valueString = formatter.string(from: NSNumber(value: bolusData[i].value)) ?? ""
  997. let dot = ChartDataEntry(x: Double(dateTimeStamp), y: Double(bolusData[i].sgv), data: valueString + "\r\r" + formatPillText(line1: valueString + " U", time: bolusData[i].date))
  998. mainChart.addEntry(dot)
  999. if Storage.shared.smallGraphTreatments.value {
  1000. smallChart.addEntry(dot)
  1001. }
  1002. }
  1003. // Set Colors
  1004. let lineBolus = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet
  1005. let lineBolusSmall = BGChartFull.lineData!.dataSets[dataIndex] as! LineChartDataSet
  1006. lineBolus.colors.removeAll()
  1007. lineBolus.circleColors.removeAll()
  1008. lineBolusSmall.colors.removeAll()
  1009. lineBolusSmall.circleColors.removeAll()
  1010. if colors.count > 0 {
  1011. for i in 0 ..< colors.count {
  1012. mainChart.addColor(colors[i])
  1013. mainChart.circleColors.append(colors[i])
  1014. smallChart.addColor(colors[i])
  1015. smallChart.circleColors.append(colors[i])
  1016. }
  1017. }
  1018. BGChart.data?.dataSets[dataIndex].notifyDataSetChanged()
  1019. BGChart.data?.notifyDataChanged()
  1020. BGChart.notifyDataSetChanged()
  1021. if Storage.shared.smallGraphTreatments.value {
  1022. BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged()
  1023. BGChartFull.data?.notifyDataChanged()
  1024. BGChartFull.notifyDataSetChanged()
  1025. }
  1026. }
  1027. func updateSmbGraph() {
  1028. var dataIndex = 16
  1029. var yTop: Double = 370
  1030. var yBottom: Double = 345
  1031. var mainChart = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet
  1032. var smallChart = BGChartFull.lineData!.dataSets[dataIndex] as! LineChartDataSet
  1033. mainChart.clear()
  1034. smallChart.clear()
  1035. let lightBlue = NSUIColor(red: 135 / 255, green: 206 / 255, blue: 235 / 255, alpha: 1.0) // Light Sky Blue
  1036. var colors = [NSUIColor]()
  1037. for i in 0 ..< smbData.count {
  1038. let formatter = NumberFormatter()
  1039. formatter.minimumFractionDigits = 0
  1040. formatter.maximumFractionDigits = 2
  1041. formatter.minimumIntegerDigits = 0
  1042. let bolusShift = findNextBolusTime(timeWithin: 240, needle: smbData[i].date, haystack: smbData, startingIndex: i)
  1043. var dateTimeStamp = smbData[i].date
  1044. let nowTime = dateTimeUtils.getNowTimeIntervalUTC()
  1045. let diffTimeHours = (nowTime - dateTimeStamp) / 60 / 60
  1046. if diffTimeHours <= 1 {
  1047. colors.append(lightBlue.withAlphaComponent(1.0))
  1048. } else if diffTimeHours > 6 {
  1049. colors.append(lightBlue.withAlphaComponent(0.25))
  1050. } else {
  1051. let thisAlpha = 1.0 - (0.15 * diffTimeHours)
  1052. colors.append(lightBlue.withAlphaComponent(CGFloat(thisAlpha)))
  1053. }
  1054. if bolusShift {
  1055. dateTimeStamp = dateTimeStamp - 150
  1056. }
  1057. let graphHours = 24 * Storage.shared.downloadDays.value
  1058. if dateTimeStamp < dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) { continue }
  1059. let valueString = formatter.string(from: NSNumber(value: smbData[i].value)) ?? ""
  1060. let dot = ChartDataEntry(x: Double(dateTimeStamp), y: Double(smbData[i].sgv), data: valueString + "\r\r" + formatPillText(line1: valueString + " U", time: smbData[i].date))
  1061. mainChart.addEntry(dot)
  1062. if Storage.shared.smallGraphTreatments.value {
  1063. smallChart.addEntry(dot)
  1064. }
  1065. }
  1066. BGChart.data?.dataSets[dataIndex].notifyDataSetChanged()
  1067. BGChart.data?.notifyDataChanged()
  1068. BGChart.notifyDataSetChanged()
  1069. if Storage.shared.smallGraphTreatments.value {
  1070. BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged()
  1071. BGChartFull.data?.notifyDataChanged()
  1072. BGChartFull.notifyDataSetChanged()
  1073. }
  1074. }
  1075. func updateCarbGraph() {
  1076. var dataIndex = 4
  1077. var mainChart = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet
  1078. var smallChart = BGChartFull.lineData!.dataSets[dataIndex] as! LineChartDataSet
  1079. mainChart.removeAll(keepingCapacity: true)
  1080. smallChart.removeAll(keepingCapacity: true)
  1081. var colors = [NSUIColor]()
  1082. for i in 0 ..< carbData.count {
  1083. let formatter = NumberFormatter()
  1084. formatter.minimumFractionDigits = 0
  1085. formatter.maximumFractionDigits = 2
  1086. formatter.minimumIntegerDigits = 1
  1087. let carbAmountString = formatter.string(from: NSNumber(value: carbData[i].value))!
  1088. var valueString = carbAmountString
  1089. var markerLine1 = carbAmountString + "g"
  1090. var hours = 3
  1091. if carbData[i].absorptionTime > 0, Storage.shared.showAbsorption.value {
  1092. hours = carbData[i].absorptionTime / 60
  1093. valueString += " " + String(hours) + "h"
  1094. markerLine1 += " " + String(hours) + "h"
  1095. }
  1096. // Check overlapping carbs to shift left if needed
  1097. let carbShift = findNextCarbTime(timeWithin: 250, needle: carbData[i].date, haystack: carbData, startingIndex: i)
  1098. var dateTimeStamp = carbData[i].date
  1099. colors.append(NSUIColor.systemOrange.withAlphaComponent(1.0))
  1100. // skip if outside of visible area
  1101. let graphHours = 24 * Storage.shared.downloadDays.value
  1102. if dateTimeStamp < dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) { continue }
  1103. if carbShift {
  1104. dateTimeStamp = dateTimeStamp - 250
  1105. }
  1106. let dot = ChartDataEntry(x: Double(dateTimeStamp), y: Double(carbData[i].sgv), data: valueString + "\r\r" + formatPillText(line1: markerLine1, time: carbData[i].date))
  1107. BGChart.data?.dataSets[dataIndex].addEntry(dot)
  1108. if Storage.shared.smallGraphTreatments.value {
  1109. BGChartFull.data?.dataSets[dataIndex].addEntry(dot)
  1110. }
  1111. }
  1112. // Set Colors
  1113. let lineCarbs = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet
  1114. let lineCarbsSmall = BGChartFull.lineData!.dataSets[dataIndex] as! LineChartDataSet
  1115. lineCarbs.colors.removeAll()
  1116. lineCarbs.circleColors.removeAll()
  1117. lineCarbsSmall.colors.removeAll()
  1118. lineCarbsSmall.circleColors.removeAll()
  1119. if colors.count > 0 {
  1120. for i in 0 ..< colors.count {
  1121. mainChart.addColor(colors[i])
  1122. mainChart.circleColors.append(colors[i])
  1123. smallChart.addColor(colors[i])
  1124. smallChart.circleColors.append(colors[i])
  1125. }
  1126. }
  1127. BGChart.data?.dataSets[dataIndex].notifyDataSetChanged()
  1128. BGChart.data?.notifyDataChanged()
  1129. BGChart.notifyDataSetChanged()
  1130. if Storage.shared.smallGraphTreatments.value {
  1131. BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged()
  1132. BGChartFull.data?.notifyDataChanged()
  1133. BGChartFull.notifyDataSetChanged()
  1134. }
  1135. }
  1136. func updateBGCheckGraph() {
  1137. var dataIndex = 7
  1138. BGChart.lineData?.dataSets[dataIndex].clear()
  1139. BGChartFull.lineData?.dataSets[dataIndex].clear()
  1140. for i in 0 ..< bgCheckData.count {
  1141. let formatter = NumberFormatter()
  1142. formatter.minimumFractionDigits = 0
  1143. formatter.maximumFractionDigits = 2
  1144. formatter.minimumIntegerDigits = 1
  1145. // skip if outside of visible area
  1146. let graphHours = 24 * Storage.shared.downloadDays.value
  1147. if bgCheckData[i].date < dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) { continue }
  1148. let value = ChartDataEntry(x: Double(bgCheckData[i].date), y: Double(bgCheckData[i].sgv), data: formatPillText(line1: Localizer.toDisplayUnits(String(bgCheckData[i].sgv)), time: bgCheckData[i].date))
  1149. BGChart.data?.dataSets[dataIndex].addEntry(value)
  1150. if Storage.shared.smallGraphTreatments.value {
  1151. BGChartFull.data?.dataSets[dataIndex].addEntry(value)
  1152. }
  1153. }
  1154. BGChart.data?.dataSets[dataIndex].notifyDataSetChanged()
  1155. BGChart.data?.notifyDataChanged()
  1156. BGChart.notifyDataSetChanged()
  1157. if Storage.shared.smallGraphTreatments.value {
  1158. BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged()
  1159. BGChartFull.data?.notifyDataChanged()
  1160. BGChartFull.notifyDataSetChanged()
  1161. }
  1162. }
  1163. func updateSuspendGraph() {
  1164. var dataIndex = 8
  1165. BGChart.lineData?.dataSets[dataIndex].clear()
  1166. BGChartFull.lineData?.dataSets[dataIndex].clear()
  1167. let thisData = suspendGraphData
  1168. for i in 0 ..< thisData.count {
  1169. // skip if outside of visible area
  1170. let graphHours = 24 * Storage.shared.downloadDays.value
  1171. if thisData[i].date < dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) { continue }
  1172. let value = ChartDataEntry(x: Double(thisData[i].date), y: Double(thisData[i].sgv), data: formatPillText(line1: "Suspend Pump", time: thisData[i].date))
  1173. BGChart.data?.dataSets[dataIndex].addEntry(value)
  1174. if Storage.shared.smallGraphTreatments.value {
  1175. BGChartFull.data?.dataSets[dataIndex].addEntry(value)
  1176. }
  1177. }
  1178. BGChart.data?.dataSets[dataIndex].notifyDataSetChanged()
  1179. BGChart.data?.notifyDataChanged()
  1180. BGChart.notifyDataSetChanged()
  1181. if Storage.shared.smallGraphTreatments.value {
  1182. BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged()
  1183. BGChartFull.data?.notifyDataChanged()
  1184. BGChartFull.notifyDataSetChanged()
  1185. }
  1186. }
  1187. func updateResumeGraph() {
  1188. var dataIndex = 9
  1189. BGChart.lineData?.dataSets[dataIndex].clear()
  1190. BGChartFull.lineData?.dataSets[dataIndex].clear()
  1191. let thisData = resumeGraphData
  1192. for i in 0 ..< thisData.count {
  1193. // skip if outside of visible area
  1194. let graphHours = 24 * Storage.shared.downloadDays.value
  1195. if thisData[i].date < dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) { continue }
  1196. let value = ChartDataEntry(x: Double(thisData[i].date), y: Double(thisData[i].sgv), data: formatPillText(line1: "Resume Pump", time: thisData[i].date))
  1197. BGChart.data?.dataSets[dataIndex].addEntry(value)
  1198. if Storage.shared.smallGraphTreatments.value {
  1199. BGChartFull.data?.dataSets[dataIndex].addEntry(value)
  1200. }
  1201. }
  1202. BGChart.data?.dataSets[dataIndex].notifyDataSetChanged()
  1203. BGChart.data?.notifyDataChanged()
  1204. BGChart.notifyDataSetChanged()
  1205. if Storage.shared.smallGraphTreatments.value {
  1206. BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged()
  1207. BGChartFull.data?.notifyDataChanged()
  1208. BGChartFull.notifyDataSetChanged()
  1209. }
  1210. }
  1211. func updateSensorStart() {
  1212. var dataIndex = 10
  1213. BGChart.lineData?.dataSets[dataIndex].clear()
  1214. BGChartFull.lineData?.dataSets[dataIndex].clear()
  1215. let thisData = sensorStartGraphData
  1216. for i in 0 ..< thisData.count {
  1217. // skip if outside of visible area
  1218. let graphHours = 24 * Storage.shared.downloadDays.value
  1219. if thisData[i].date < dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) { continue }
  1220. let value = ChartDataEntry(x: Double(thisData[i].date), y: Double(thisData[i].sgv), data: formatPillText(line1: "Start Sensor", time: thisData[i].date))
  1221. BGChart.data?.dataSets[dataIndex].addEntry(value)
  1222. if Storage.shared.smallGraphTreatments.value {
  1223. BGChartFull.data?.dataSets[dataIndex].addEntry(value)
  1224. }
  1225. }
  1226. BGChart.data?.dataSets[dataIndex].notifyDataSetChanged()
  1227. BGChart.data?.notifyDataChanged()
  1228. BGChart.notifyDataSetChanged()
  1229. if Storage.shared.smallGraphTreatments.value {
  1230. BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged()
  1231. BGChartFull.data?.notifyDataChanged()
  1232. BGChartFull.notifyDataSetChanged()
  1233. }
  1234. }
  1235. func updateNotes() {
  1236. var dataIndex = 11
  1237. BGChart.lineData?.dataSets[dataIndex].clear()
  1238. BGChartFull.lineData?.dataSets[dataIndex].clear()
  1239. let thisData = noteGraphData
  1240. for i in 0 ..< thisData.count {
  1241. // skip if outside of visible area
  1242. let graphHours = 24 * Storage.shared.downloadDays.value
  1243. if thisData[i].date < dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) { continue }
  1244. let value = ChartDataEntry(x: Double(thisData[i].date), y: Double(thisData[i].sgv), data: formatPillText(line1: thisData[i].note, time: thisData[i].date))
  1245. BGChart.data?.dataSets[dataIndex].addEntry(value)
  1246. if Storage.shared.smallGraphTreatments.value {
  1247. BGChartFull.data?.dataSets[dataIndex].addEntry(value)
  1248. }
  1249. }
  1250. BGChart.data?.dataSets[dataIndex].notifyDataSetChanged()
  1251. BGChart.data?.notifyDataChanged()
  1252. BGChart.notifyDataSetChanged()
  1253. if Storage.shared.smallGraphTreatments.value {
  1254. BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged()
  1255. BGChartFull.data?.notifyDataChanged()
  1256. BGChartFull.notifyDataSetChanged()
  1257. }
  1258. }
  1259. func createSmallBGGraph() {
  1260. let entries = bgData
  1261. var bgChartEntry = [ChartDataEntry]()
  1262. var colors = [NSUIColor]()
  1263. var maxBG = Storage.shared.minBGScale.value
  1264. let lineBG = LineChartDataSet(entries: bgChartEntry, label: "")
  1265. lineBG.drawCirclesEnabled = false
  1266. // line2.setDrawHighlightIndicators(false)
  1267. lineBG.highlightEnabled = true
  1268. lineBG.drawHorizontalHighlightIndicatorEnabled = false
  1269. lineBG.drawVerticalHighlightIndicatorEnabled = false
  1270. lineBG.highlightColor = NSUIColor.label
  1271. lineBG.drawValuesEnabled = false
  1272. lineBG.lineWidth = 1.5
  1273. lineBG.axisDependency = YAxis.AxisDependency.right
  1274. // Setup Prediction line details
  1275. var predictionChartEntry = [ChartDataEntry]()
  1276. let linePrediction = LineChartDataSet(entries: predictionChartEntry, label: "")
  1277. linePrediction.drawCirclesEnabled = false
  1278. // line2.setDrawHighlightIndicators(false)
  1279. linePrediction.setColor(NSUIColor.systemPurple)
  1280. linePrediction.highlightEnabled = true
  1281. linePrediction.drawHorizontalHighlightIndicatorEnabled = false
  1282. linePrediction.drawVerticalHighlightIndicatorEnabled = false
  1283. linePrediction.highlightColor = NSUIColor.label
  1284. linePrediction.drawValuesEnabled = false
  1285. linePrediction.lineWidth = 1.5
  1286. linePrediction.axisDependency = YAxis.AxisDependency.right
  1287. // create Basal graph data
  1288. var chartEntry = [ChartDataEntry]()
  1289. var maxBasal = Storage.shared.minBasalScale.value
  1290. let lineBasal = LineChartDataSet(entries: chartEntry, label: "")
  1291. lineBasal.setDrawHighlightIndicators(false)
  1292. lineBasal.setColor(NSUIColor.systemBlue, alpha: 0.5)
  1293. lineBasal.lineWidth = 0
  1294. lineBasal.drawFilledEnabled = true
  1295. lineBasal.fillColor = NSUIColor.systemBlue
  1296. lineBasal.fillAlpha = 0.35
  1297. lineBasal.drawCirclesEnabled = false
  1298. lineBasal.axisDependency = YAxis.AxisDependency.left
  1299. lineBasal.highlightEnabled = false
  1300. lineBasal.drawValuesEnabled = false
  1301. lineBasal.fillFormatter = basalFillFormatter()
  1302. // Boluses
  1303. var chartEntryBolus = [ChartDataEntry]()
  1304. let lineBolus = LineChartDataSet(entries: chartEntryBolus, label: "")
  1305. lineBolus.circleRadius = 2
  1306. lineBolus.circleColors = [NSUIColor.systemBlue.withAlphaComponent(0.75)]
  1307. lineBolus.drawCircleHoleEnabled = false
  1308. lineBolus.setDrawHighlightIndicators(false)
  1309. lineBolus.setColor(NSUIColor.systemBlue, alpha: 1.0)
  1310. lineBolus.lineWidth = 0
  1311. lineBolus.axisDependency = YAxis.AxisDependency.right
  1312. lineBolus.valueFormatter = ChartYDataValueFormatter()
  1313. lineBolus.valueTextColor = NSUIColor.label
  1314. lineBolus.fillColor = NSUIColor.systemBlue
  1315. lineBolus.fillAlpha = 0.6
  1316. lineBolus.drawCirclesEnabled = true
  1317. lineBolus.drawFilledEnabled = false
  1318. lineBolus.drawValuesEnabled = false
  1319. lineBolus.highlightEnabled = false
  1320. // Carbs
  1321. var chartEntryCarbs = [ChartDataEntry]()
  1322. let lineCarbs = LineChartDataSet(entries: chartEntryCarbs, label: "")
  1323. lineCarbs.circleRadius = 2
  1324. lineCarbs.circleColors = [NSUIColor.systemOrange.withAlphaComponent(0.75)]
  1325. lineCarbs.drawCircleHoleEnabled = false
  1326. lineCarbs.setDrawHighlightIndicators(false)
  1327. lineCarbs.setColor(NSUIColor.systemBlue, alpha: 1.0)
  1328. lineCarbs.lineWidth = 0
  1329. lineCarbs.axisDependency = YAxis.AxisDependency.right
  1330. lineCarbs.valueFormatter = ChartYDataValueFormatter()
  1331. lineCarbs.valueTextColor = NSUIColor.label
  1332. lineCarbs.fillColor = NSUIColor.systemOrange
  1333. lineCarbs.fillAlpha = 0.6
  1334. lineCarbs.drawCirclesEnabled = true
  1335. lineCarbs.drawFilledEnabled = false
  1336. lineCarbs.drawValuesEnabled = false
  1337. lineCarbs.highlightEnabled = false
  1338. // create Scheduled Basal graph data
  1339. var chartBasalScheduledEntry = [ChartDataEntry]()
  1340. let lineBasalScheduled = LineChartDataSet(entries: chartBasalScheduledEntry, label: "")
  1341. lineBasalScheduled.setDrawHighlightIndicators(false)
  1342. lineBasalScheduled.setColor(NSUIColor.systemBlue, alpha: 0.8)
  1343. lineBasalScheduled.lineWidth = 0.5
  1344. lineBasalScheduled.drawFilledEnabled = false
  1345. lineBasalScheduled.drawCirclesEnabled = false
  1346. lineBasalScheduled.axisDependency = YAxis.AxisDependency.left
  1347. lineBasalScheduled.highlightEnabled = false
  1348. lineBasalScheduled.drawValuesEnabled = false
  1349. lineBasalScheduled.lineDashLengths = [2, 1]
  1350. // create Override graph data
  1351. var chartOverrideEntry = [ChartDataEntry]()
  1352. let lineOverride = LineChartDataSet(entries: chartOverrideEntry, label: "")
  1353. lineOverride.setDrawHighlightIndicators(false)
  1354. lineOverride.lineWidth = 0
  1355. lineOverride.drawFilledEnabled = true
  1356. lineOverride.fillFormatter = OverrideFillFormatter()
  1357. lineOverride.fillColor = TreatmentGraphColors.override
  1358. lineOverride.fillAlpha = 0.6
  1359. lineOverride.drawCirclesEnabled = false
  1360. lineOverride.axisDependency = YAxis.AxisDependency.right
  1361. lineOverride.highlightEnabled = true
  1362. lineOverride.drawValuesEnabled = false
  1363. // BG Check
  1364. var chartEntryBGCheck = [ChartDataEntry]()
  1365. let lineBGCheck = LineChartDataSet(entries: chartEntryBGCheck, label: "")
  1366. lineBGCheck.circleRadius = 2
  1367. lineBGCheck.circleColors = [NSUIColor.systemRed.withAlphaComponent(0.75)]
  1368. lineBGCheck.drawCircleHoleEnabled = false
  1369. lineBGCheck.setDrawHighlightIndicators(false)
  1370. lineBGCheck.setColor(NSUIColor.systemRed, alpha: 1.0)
  1371. lineBGCheck.drawCirclesEnabled = true
  1372. lineBGCheck.lineWidth = 0
  1373. lineBGCheck.highlightEnabled = false
  1374. lineBGCheck.axisDependency = YAxis.AxisDependency.right
  1375. lineBGCheck.valueFormatter = ChartYDataValueFormatter()
  1376. lineBGCheck.drawValuesEnabled = false
  1377. // Suspend Pump
  1378. var chartEntrySuspend = [ChartDataEntry]()
  1379. let lineSuspend = LineChartDataSet(entries: chartEntrySuspend, label: "")
  1380. lineSuspend.circleRadius = 2
  1381. lineSuspend.circleColors = [NSUIColor.systemTeal.withAlphaComponent(0.75)]
  1382. lineSuspend.drawCircleHoleEnabled = false
  1383. lineSuspend.setDrawHighlightIndicators(false)
  1384. lineSuspend.setColor(NSUIColor.systemGray2, alpha: 1.0)
  1385. lineSuspend.drawCirclesEnabled = true
  1386. lineSuspend.lineWidth = 0
  1387. lineSuspend.highlightEnabled = false
  1388. lineSuspend.axisDependency = YAxis.AxisDependency.right
  1389. lineSuspend.valueFormatter = ChartYDataValueFormatter()
  1390. lineSuspend.drawValuesEnabled = false
  1391. // Resume Pump
  1392. var chartEntryResume = [ChartDataEntry]()
  1393. let lineResume = LineChartDataSet(entries: chartEntryResume, label: "")
  1394. lineResume.circleRadius = 2
  1395. lineResume.circleColors = [NSUIColor.systemTeal.withAlphaComponent(0.75)]
  1396. lineResume.drawCircleHoleEnabled = false
  1397. lineResume.setDrawHighlightIndicators(false)
  1398. lineResume.setColor(NSUIColor.systemGray4, alpha: 1.0)
  1399. lineResume.drawCirclesEnabled = true
  1400. lineResume.lineWidth = 0
  1401. lineResume.highlightEnabled = false
  1402. lineResume.axisDependency = YAxis.AxisDependency.right
  1403. lineResume.valueFormatter = ChartYDataValueFormatter()
  1404. lineResume.drawValuesEnabled = false
  1405. // Sensor Start
  1406. var chartEntrySensor = [ChartDataEntry]()
  1407. let lineSensor = LineChartDataSet(entries: chartEntrySensor, label: "")
  1408. lineSensor.circleRadius = 2
  1409. lineSensor.circleColors = [NSUIColor.systemIndigo.withAlphaComponent(0.75)]
  1410. lineSensor.drawCircleHoleEnabled = false
  1411. lineSensor.setDrawHighlightIndicators(false)
  1412. lineSensor.setColor(NSUIColor.systemGray3, alpha: 1.0)
  1413. lineSensor.drawCirclesEnabled = true
  1414. lineSensor.lineWidth = 0
  1415. lineSensor.highlightEnabled = false
  1416. lineSensor.axisDependency = YAxis.AxisDependency.right
  1417. lineSensor.valueFormatter = ChartYDataValueFormatter()
  1418. lineSensor.drawValuesEnabled = false
  1419. // Notes
  1420. var chartEntryNote = [ChartDataEntry]()
  1421. let lineNote = LineChartDataSet(entries: chartEntryNote, label: "")
  1422. lineNote.circleRadius = 2
  1423. lineNote.circleColors = [NSUIColor.systemGray.withAlphaComponent(0.75)]
  1424. lineNote.drawCircleHoleEnabled = false
  1425. lineNote.setDrawHighlightIndicators(false)
  1426. lineNote.setColor(NSUIColor.systemGray3, alpha: 1.0)
  1427. lineNote.drawCirclesEnabled = true
  1428. lineNote.lineWidth = 0
  1429. lineNote.highlightEnabled = false
  1430. lineNote.axisDependency = YAxis.AxisDependency.right
  1431. lineNote.valueFormatter = ChartYDataValueFormatter()
  1432. lineNote.drawValuesEnabled = false
  1433. // Setup COB Prediction line details
  1434. var COBpredictionChartEntry = [ChartDataEntry]()
  1435. let COBlinePrediction = LineChartDataSet(entries: COBpredictionChartEntry, label: "")
  1436. COBlinePrediction.drawCirclesEnabled = false
  1437. COBlinePrediction.setColor(NSUIColor.systemPurple)
  1438. COBlinePrediction.highlightEnabled = true
  1439. COBlinePrediction.drawHorizontalHighlightIndicatorEnabled = false
  1440. COBlinePrediction.drawVerticalHighlightIndicatorEnabled = false
  1441. COBlinePrediction.highlightColor = NSUIColor.label
  1442. COBlinePrediction.drawValuesEnabled = false
  1443. COBlinePrediction.lineWidth = 1.5
  1444. COBlinePrediction.axisDependency = YAxis.AxisDependency.right
  1445. // Setup IOB Prediction line details
  1446. var IOBpredictionChartEntry = [ChartDataEntry]()
  1447. let IOBlinePrediction = LineChartDataSet(entries: IOBpredictionChartEntry, label: "")
  1448. IOBlinePrediction.drawCirclesEnabled = false
  1449. IOBlinePrediction.setColor(NSUIColor.systemPurple)
  1450. IOBlinePrediction.highlightEnabled = true
  1451. IOBlinePrediction.drawHorizontalHighlightIndicatorEnabled = false
  1452. IOBlinePrediction.drawVerticalHighlightIndicatorEnabled = false
  1453. IOBlinePrediction.highlightColor = NSUIColor.label
  1454. IOBlinePrediction.drawValuesEnabled = false
  1455. IOBlinePrediction.lineWidth = 1.5
  1456. IOBlinePrediction.axisDependency = YAxis.AxisDependency.right
  1457. // Setup UAM Prediction line details
  1458. var UAMpredictionChartEntry = [ChartDataEntry]()
  1459. let UAMlinePrediction = LineChartDataSet(entries: UAMpredictionChartEntry, label: "")
  1460. UAMlinePrediction.drawCirclesEnabled = false
  1461. UAMlinePrediction.setColor(NSUIColor.systemPurple)
  1462. UAMlinePrediction.highlightEnabled = true
  1463. UAMlinePrediction.drawHorizontalHighlightIndicatorEnabled = false
  1464. UAMlinePrediction.drawVerticalHighlightIndicatorEnabled = false
  1465. UAMlinePrediction.highlightColor = NSUIColor.label
  1466. UAMlinePrediction.drawValuesEnabled = false
  1467. UAMlinePrediction.lineWidth = 1.5
  1468. UAMlinePrediction.axisDependency = YAxis.AxisDependency.right
  1469. // Setup ZT Prediction line details
  1470. var ZTpredictionChartEntry = [ChartDataEntry]()
  1471. let ZTlinePrediction = LineChartDataSet(entries: ZTpredictionChartEntry, label: "")
  1472. ZTlinePrediction.drawCirclesEnabled = false
  1473. ZTlinePrediction.setColor(NSUIColor.systemPurple)
  1474. ZTlinePrediction.highlightEnabled = true
  1475. ZTlinePrediction.drawHorizontalHighlightIndicatorEnabled = false
  1476. ZTlinePrediction.drawVerticalHighlightIndicatorEnabled = false
  1477. ZTlinePrediction.highlightColor = NSUIColor.label
  1478. ZTlinePrediction.drawValuesEnabled = false
  1479. ZTlinePrediction.lineWidth = 1.5
  1480. ZTlinePrediction.axisDependency = YAxis.AxisDependency.right
  1481. // SMB
  1482. var chartEntrySmb = [ChartDataEntry]()
  1483. let lineSmb = LineChartDataSet(entries: chartEntrySmb, label: "")
  1484. lineSmb.circleRadius = 2
  1485. lineSmb.circleColors = [NSUIColor.systemBlue.withAlphaComponent(0.75)]
  1486. lineSmb.drawCircleHoleEnabled = false
  1487. lineSmb.setDrawHighlightIndicators(false)
  1488. lineSmb.setColor(NSUIColor.systemBlue, alpha: 1.0)
  1489. lineSmb.lineWidth = 0
  1490. lineSmb.axisDependency = YAxis.AxisDependency.right
  1491. lineSmb.valueFormatter = ChartYDataValueFormatter()
  1492. lineSmb.valueTextColor = NSUIColor.label
  1493. lineSmb.fillColor = NSUIColor.systemBlue
  1494. lineSmb.fillAlpha = 0.6
  1495. lineSmb.drawCirclesEnabled = true
  1496. lineSmb.drawFilledEnabled = false
  1497. lineSmb.drawValuesEnabled = false
  1498. lineSmb.highlightEnabled = false
  1499. // Temp Target graph data
  1500. let chartTempTargetEntry = [ChartDataEntry]()
  1501. let lineTempTarget = LineChartDataSet(entries: chartTempTargetEntry, label: "")
  1502. lineTempTarget.setDrawHighlightIndicators(false)
  1503. lineTempTarget.lineWidth = 0
  1504. lineTempTarget.drawFilledEnabled = false
  1505. lineTempTarget.fillColor = NSUIColor.systemPurple
  1506. lineTempTarget.fillAlpha = 0.6
  1507. lineTempTarget.drawCirclesEnabled = false
  1508. lineTempTarget.axisDependency = YAxis.AxisDependency.right
  1509. lineTempTarget.highlightEnabled = true
  1510. lineTempTarget.drawValuesEnabled = false
  1511. // Setup the chart data of all lines
  1512. let data = LineChartData()
  1513. data.append(lineBG) // Dataset 0
  1514. data.append(linePrediction) // Dataset 1
  1515. data.append(lineBasal) // Dataset 2
  1516. data.append(lineBolus) // Dataset 3
  1517. data.append(lineCarbs) // Dataset 4
  1518. data.append(lineBasalScheduled) // Dataset 5
  1519. data.append(lineOverride) // Dataset 6
  1520. data.append(lineBGCheck) // Dataset 7
  1521. data.append(lineSuspend) // Dataset 8
  1522. data.append(lineResume) // Dataset 9
  1523. data.append(lineSensor) // Dataset 10
  1524. data.append(lineNote) // Dataset 11
  1525. data.append(ZTlinePrediction) // Dataset 12
  1526. data.append(IOBlinePrediction) // Dataset 13
  1527. data.append(COBlinePrediction) // Dataset 14
  1528. data.append(UAMlinePrediction) // Dataset 15
  1529. data.append(lineSmb) // Dataset 16
  1530. data.append(lineTempTarget) // Dataset 17
  1531. // Dataset 18: Prediction Cone placeholder (not rendered on small chart)
  1532. let lineConeSmall = LineChartDataSet(entries: [ChartDataEntry](), label: "")
  1533. lineConeSmall.lineWidth = 0
  1534. lineConeSmall.drawCirclesEnabled = false
  1535. lineConeSmall.drawValuesEnabled = false
  1536. lineConeSmall.highlightEnabled = false
  1537. lineConeSmall.axisDependency = YAxis.AxisDependency.right
  1538. data.append(lineConeSmall)
  1539. BGChartFull.highlightPerDragEnabled = true
  1540. BGChartFull.leftAxis.enabled = false
  1541. BGChartFull.leftAxis.axisMaximum = maxBasal
  1542. BGChartFull.leftAxis.axisMinimum = 0
  1543. BGChartFull.rightAxis.enabled = false
  1544. BGChartFull.rightAxis.axisMinimum = 0.0
  1545. BGChartFull.rightAxis.axisMaximum = Double(maxBG)
  1546. BGChartFull.xAxis.drawLabelsEnabled = false
  1547. BGChartFull.xAxis.drawGridLinesEnabled = false
  1548. BGChartFull.xAxis.drawAxisLineEnabled = false
  1549. BGChartFull.legend.enabled = false
  1550. BGChartFull.scaleYEnabled = false
  1551. BGChartFull.scaleXEnabled = false
  1552. BGChartFull.drawGridBackgroundEnabled = false
  1553. BGChartFull.data = data
  1554. }
  1555. func updateOverrideGraph() {
  1556. var dataIndex = 6
  1557. var yTop = Double(calculateMaxBgGraphValue() - 5)
  1558. var yBottom = Double(calculateMaxBgGraphValue() - 25)
  1559. var chart = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet
  1560. var smallChart = BGChartFull.lineData!.dataSets[dataIndex] as! LineChartDataSet
  1561. chart.clear()
  1562. smallChart.clear()
  1563. // Refresh the fill color in case the backend (Loop vs Trio) changed.
  1564. chart.fillColor = TreatmentGraphColors.override
  1565. smallChart.fillColor = TreatmentGraphColors.override
  1566. let thisData = overrideGraphData
  1567. var colors = [NSUIColor]()
  1568. for i in 0 ..< thisData.count {
  1569. let thisItem = thisData[i]
  1570. let multiplier = thisItem.insulNeedsScaleFactor as! Double * 100.0
  1571. var labelText = thisItem.reason + "\r\n"
  1572. labelText += String(Int(thisItem.insulNeedsScaleFactor * 100)) + "% "
  1573. if thisItem.correctionRange.count == 2 {
  1574. labelText += String(thisItem.correctionRange[0]) + "-" + String(thisItem.correctionRange[1])
  1575. }
  1576. if thisItem.enteredBy.count > 0 {
  1577. labelText += "\r\nEntered By: " + thisItem.enteredBy
  1578. }
  1579. // Start Dot
  1580. // Shift dots 30 seconds to create an empty 0 space between consecutive temps
  1581. let preStartDot = ChartDataEntry(x: Double(thisItem.date), y: yBottom, data: labelText)
  1582. BGChart.data?.dataSets[dataIndex].addEntry(preStartDot)
  1583. if Storage.shared.smallGraphTreatments.value {
  1584. BGChartFull.data?.dataSets[dataIndex].addEntry(preStartDot)
  1585. }
  1586. let startDot = ChartDataEntry(x: Double(thisItem.date + 1), y: yTop, data: labelText)
  1587. BGChart.data?.dataSets[dataIndex].addEntry(startDot)
  1588. if Storage.shared.smallGraphTreatments.value {
  1589. BGChartFull.data?.dataSets[dataIndex].addEntry(startDot)
  1590. }
  1591. // End Dot
  1592. let endDot = ChartDataEntry(x: Double(thisItem.endDate - 2), y: yTop, data: labelText)
  1593. BGChart.data?.dataSets[dataIndex].addEntry(endDot)
  1594. if Storage.shared.smallGraphTreatments.value {
  1595. BGChartFull.data?.dataSets[dataIndex].addEntry(endDot)
  1596. }
  1597. // Post end dot
  1598. let postEndDot = ChartDataEntry(x: Double(thisItem.endDate - 1), y: yBottom, data: labelText)
  1599. BGChart.data?.dataSets[dataIndex].addEntry(postEndDot)
  1600. if Storage.shared.smallGraphTreatments.value {
  1601. BGChartFull.data?.dataSets[dataIndex].addEntry(postEndDot)
  1602. }
  1603. }
  1604. BGChart.data?.dataSets[dataIndex].notifyDataSetChanged()
  1605. BGChart.data?.notifyDataChanged()
  1606. BGChart.notifyDataSetChanged()
  1607. if Storage.shared.smallGraphTreatments.value {
  1608. BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged()
  1609. BGChartFull.data?.notifyDataChanged()
  1610. BGChartFull.notifyDataSetChanged()
  1611. }
  1612. }
  1613. func getChartDataSets(for index: GraphDataIndex) -> (chart: LineChartDataSet?, smallChart: LineChartDataSet?) {
  1614. guard let chart = BGChart.lineData,
  1615. index.rawValue < chart.dataSets.count,
  1616. let smallChartData = BGChartFull.lineData,
  1617. index.rawValue < smallChartData.dataSets.count
  1618. else {
  1619. // print("Warning: Invalid GraphDataIndex \(index.description) or lineData is nil.")
  1620. return (nil, nil)
  1621. }
  1622. let chartDataSet = chart.dataSets[index.rawValue] as? LineChartDataSet
  1623. let smallChartDataSet = smallChartData.dataSets[index.rawValue] as? LineChartDataSet
  1624. return (chartDataSet, smallChartDataSet)
  1625. }
  1626. func addEntryToCharts(entry: ChartDataEntry, chart: LineChartDataSet, smallChart: LineChartDataSet?) {
  1627. chart.addEntry(entry)
  1628. if Storage.shared.smallGraphTreatments.value, let smallChart = smallChart {
  1629. smallChart.addEntry(entry)
  1630. }
  1631. }
  1632. func updateTempTargetGraph() {
  1633. let dataIndex = GraphDataIndex.tempTarget.rawValue
  1634. guard let chartData = BGChart.lineData,
  1635. chartData.dataSets.count > dataIndex,
  1636. let mainChartDataSet = chartData.dataSets[dataIndex] as? LineChartDataSet
  1637. else {
  1638. print("Error: Could not retrieve temp target datasets.")
  1639. return
  1640. }
  1641. mainChartDataSet.clear()
  1642. var smallChartDataSet: LineChartDataSet?
  1643. if Storage.shared.smallGraphTreatments.value,
  1644. let smallChartData = BGChartFull.lineData,
  1645. smallChartData.dataSets.count > dataIndex,
  1646. let smallDataSet = smallChartData.dataSets[dataIndex] as? LineChartDataSet
  1647. {
  1648. smallChartDataSet = smallDataSet
  1649. smallChartDataSet?.clear()
  1650. }
  1651. let thisData = tempTargetGraphData
  1652. for tempTarget in thisData {
  1653. let xStart = tempTarget.date
  1654. let xEnd = tempTarget.endDate
  1655. let yCenter = Double(tempTarget.correctionRange[0])
  1656. let yTop = yCenter + 5.0
  1657. let yBottom = yCenter - 5.0
  1658. let entry = TempTargetChartDataEntry(
  1659. xStart: xStart,
  1660. xEnd: xEnd,
  1661. yTop: yTop,
  1662. yBottom: yBottom,
  1663. data: nil
  1664. )
  1665. mainChartDataSet.addEntry(entry)
  1666. if let smallDataSet = smallChartDataSet {
  1667. smallDataSet.addEntry(entry)
  1668. }
  1669. }
  1670. BGChart.data?.notifyDataChanged()
  1671. BGChart.notifyDataSetChanged()
  1672. if let smallDataSet = smallChartDataSet {
  1673. let tempTargetRendererSmall = TempTargetRenderer(
  1674. dataProvider: BGChartFull,
  1675. animator: BGChartFull.chartAnimator,
  1676. viewPortHandler: BGChartFull.viewPortHandler,
  1677. tempTargetDataSetIndex: dataIndex
  1678. )
  1679. BGChartFull.renderer = tempTargetRendererSmall
  1680. BGChartFull.data?.notifyDataChanged()
  1681. BGChartFull.notifyDataSetChanged()
  1682. }
  1683. }
  1684. func extractMessage(from logEntry: String) -> String? {
  1685. // Check if this is a JSON-containing log entry
  1686. guard let jsonStartIndex = logEntry.range(of: "{\"")?.lowerBound else {
  1687. return nil
  1688. }
  1689. // Extract the error message part (before JSON)
  1690. let errorMessage = String(logEntry[..<jsonStartIndex])
  1691. .trimmingCharacters(in: .whitespacesAndNewlines)
  1692. // Extract and parse JSON to get context
  1693. let jsonString = String(logEntry[jsonStartIndex...])
  1694. var actionContext = ""
  1695. if let jsonData = jsonString.data(using: .utf8),
  1696. let json = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any]
  1697. {
  1698. // Extract relevant action information
  1699. var actionParts: [String] = []
  1700. // Check for bolus entry
  1701. if let bolusAmount = json["bolus-entry"] as? Double {
  1702. actionParts.append("Bolus: \(bolusAmount) U")
  1703. }
  1704. // Check for carbs entry
  1705. if let carbsAmount = json["carbs-entry"] as? Double {
  1706. actionParts.append("Carbs: \(carbsAmount) g")
  1707. }
  1708. // Check for absorption time (relevant for carbs)
  1709. if let absorptionTime = json["absorption-time"] as? Double {
  1710. actionParts.append("Absorption: \(absorptionTime) hrs")
  1711. }
  1712. // Check for OTP (password)
  1713. if let otp = json["otp"] as? String {
  1714. actionParts.append("OTP: \(otp)")
  1715. }
  1716. // Check for sender
  1717. if let enteredBy = json["entered-by"] as? String {
  1718. actionParts.append("From: \(enteredBy)")
  1719. }
  1720. // Combine action parts
  1721. if !actionParts.isEmpty {
  1722. actionContext = " [" + actionParts.joined(separator: ", ") + "]"
  1723. }
  1724. }
  1725. // Combine error message with action context
  1726. let finalMessage = errorMessage + actionContext
  1727. return finalMessage.isEmpty ? nil : finalMessage
  1728. }
  1729. func wrapText(_ text: String, maxLineLength: Int) -> String {
  1730. let messageToWrap = extractMessage(from: text) ?? text
  1731. guard maxLineLength > 0 else {
  1732. return messageToWrap
  1733. }
  1734. var result: [String] = []
  1735. let lines = messageToWrap.components(separatedBy: .newlines)
  1736. for line in lines {
  1737. var currentLine = ""
  1738. let words = line.components(separatedBy: .whitespaces)
  1739. for word in words {
  1740. if word.count > maxLineLength {
  1741. if !currentLine.isEmpty {
  1742. result.append(currentLine)
  1743. currentLine = ""
  1744. }
  1745. var wordToSplit = word
  1746. while !wordToSplit.isEmpty {
  1747. let splitIndex = wordToSplit.index(wordToSplit.startIndex, offsetBy: min(maxLineLength, wordToSplit.count))
  1748. result.append(String(wordToSplit[..<splitIndex]))
  1749. wordToSplit = String(wordToSplit[splitIndex...])
  1750. }
  1751. } else {
  1752. if currentLine.isEmpty {
  1753. currentLine = word
  1754. } else if currentLine.count + word.count + 1 <= maxLineLength {
  1755. currentLine += " " + word
  1756. } else {
  1757. result.append(currentLine)
  1758. currentLine = word
  1759. }
  1760. }
  1761. }
  1762. if !currentLine.isEmpty {
  1763. result.append(currentLine)
  1764. }
  1765. }
  1766. return result.joined(separator: "\r\n")
  1767. }
  1768. func formatPillText(line1: String, time: TimeInterval, line2: String? = nil) -> String {
  1769. let dateFormatter = DateFormatter()
  1770. if dateTimeUtils.is24Hour() {
  1771. dateFormatter.setLocalizedDateFormatFromTemplate("HH:mm")
  1772. } else {
  1773. dateFormatter.setLocalizedDateFormatFromTemplate("hh:mm")
  1774. }
  1775. if Storage.shared.graphTimeZoneEnabled.value,
  1776. let tz = TimeZone(identifier: Storage.shared.graphTimeZoneIdentifier.value)
  1777. {
  1778. dateFormatter.timeZone = tz
  1779. }
  1780. let wrappedLine1 = wrapText(line1, maxLineLength: 40)
  1781. let date = Date(timeIntervalSince1970: time)
  1782. let formattedDate = dateFormatter.string(from: date)
  1783. if let line2 = line2 {
  1784. return wrappedLine1 + "\r\n" + line2 + "\r\n" + formattedDate
  1785. } else {
  1786. return wrappedLine1 + "\r\n" + formattedDate
  1787. }
  1788. }
  1789. func updateConeGraph(coneData: [ConeChartDataEntry]) {
  1790. let dataIndex = GraphDataIndex.predictionCone.rawValue
  1791. let mainChart = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet
  1792. mainChart.clear()
  1793. for entry in coneData {
  1794. mainChart.addEntry(entry)
  1795. }
  1796. BGChart.rightAxis.axisMaximum = Double(calculateMaxBgGraphValue())
  1797. updateChartRenderers()
  1798. }
  1799. func clearConeGraph() {
  1800. let dataIndex = GraphDataIndex.predictionCone.rawValue
  1801. guard let lineData = BGChart.lineData, lineData.dataSets.count > dataIndex else { return }
  1802. lineData.dataSets[dataIndex].clear()
  1803. BGChart.data?.dataSets[dataIndex].notifyDataSetChanged()
  1804. BGChart.data?.notifyDataChanged()
  1805. BGChart.notifyDataSetChanged()
  1806. }
  1807. func updateOpenAPSPredictionDisplay() {
  1808. guard let predBGs = openAPSPredBGs else { return }
  1809. // Cone is only for OpenAPS-based systems; Loop always uses lines
  1810. let displayType: PredictionDisplayType = Storage.shared.device.value == "Loop" ? .lines : Storage.shared.predictionDisplayType.value
  1811. let toLoad = Int(Storage.shared.predictionToLoad.value * 12)
  1812. let predictionStart = openAPSPredUpdatedTime ?? Date().timeIntervalSince1970
  1813. let predictionTypes: [(type: String, colorName: String, dataIndex: Int)] = [
  1814. ("ZT", "ZT", GraphDataIndex.ztPrediction.rawValue),
  1815. ("IOB", "Insulin", GraphDataIndex.iobPrediction.rawValue),
  1816. ("COB", "LoopYellow", GraphDataIndex.cobPrediction.rawValue),
  1817. ("UAM", "UAM", GraphDataIndex.uamPrediction.rawValue),
  1818. ]
  1819. topPredictionBG = Storage.shared.minBGScale.value
  1820. if displayType == .cone {
  1821. var allArrays = [[Double]]()
  1822. for (type, _, _) in predictionTypes {
  1823. if let arr = predBGs[type], !arr.isEmpty {
  1824. allArrays.append(arr)
  1825. }
  1826. }
  1827. var coneData = [ConeChartDataEntry]()
  1828. if !allArrays.isEmpty {
  1829. // Cap at the shortest predBG array length so every cone point uses
  1830. // the same set of contributing arrays. Matches Trio's ForecastSetup.
  1831. let coneLength = min(allArrays.map { $0.count }.min()!, toLoad + 1)
  1832. var t = predictionStart
  1833. for i in 0 ..< coneLength {
  1834. var valuesAtIndex = [Double]()
  1835. for arr in allArrays where i < arr.count {
  1836. valuesAtIndex.append(arr[i])
  1837. }
  1838. if !valuesAtIndex.isEmpty {
  1839. var yMin = max(valuesAtIndex.min()!, Double(globalVariables.minDisplayGlucose))
  1840. var yMax = min(valuesAtIndex.max()!, Double(globalVariables.maxDisplayGlucose))
  1841. // Ensure minimum ±1 mg/dL range so the cone is visible when predictions agree
  1842. if yMin == yMax {
  1843. yMin -= 1
  1844. yMax += 1
  1845. }
  1846. coneData.append(ConeChartDataEntry(x: t, yMin: yMin, yMax: yMax))
  1847. if yMax > topPredictionBG - 20 { topPredictionBG = yMax + 20 }
  1848. }
  1849. t += 300
  1850. }
  1851. }
  1852. updateConeGraph(coneData: coneData)
  1853. // Clear individual prediction lines
  1854. for (_, _, dataIndex) in predictionTypes {
  1855. updatePredictionGraphGeneric(dataIndex: dataIndex, predictionData: [], chartLabel: "", color: .clear)
  1856. }
  1857. } else {
  1858. clearConeGraph()
  1859. for (type, colorName, dataIndex) in predictionTypes {
  1860. var predictionData = [ShareGlucoseData]()
  1861. if let graphdata = predBGs[type] {
  1862. var t = predictionStart
  1863. for i in 0 ... toLoad {
  1864. if i < graphdata.count {
  1865. let v = graphdata[i]
  1866. let clamped = min(max(Int(round(v)), globalVariables.minDisplayGlucose), globalVariables.maxDisplayGlucose)
  1867. predictionData.append(ShareGlucoseData(sgv: clamped, date: t, direction: "flat"))
  1868. t += 300
  1869. }
  1870. }
  1871. }
  1872. let color = UIColor(named: colorName) ?? UIColor.systemPurple
  1873. updatePredictionGraphGeneric(dataIndex: dataIndex, predictionData: predictionData, chartLabel: type, color: color)
  1874. }
  1875. }
  1876. }
  1877. func updatePredictionGraphGeneric(
  1878. dataIndex: Int,
  1879. predictionData: [ShareGlucoseData],
  1880. chartLabel: String,
  1881. color: UIColor
  1882. ) {
  1883. let mainChart = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet
  1884. let smallChart = BGChartFull.lineData!.dataSets[dataIndex] as! LineChartDataSet
  1885. mainChart.clear()
  1886. smallChart.clear()
  1887. var colors = [NSUIColor]()
  1888. let maxBGOffset: Double = 20
  1889. for i in 0 ..< predictionData.count {
  1890. let predictionVal = Double(predictionData[i].sgv)
  1891. if predictionVal > topPredictionBG - maxBGOffset {
  1892. topPredictionBG = predictionVal + maxBGOffset
  1893. }
  1894. if i == 0 {
  1895. if Storage.shared.showDots.value {
  1896. colors.append(color.withAlphaComponent(0.0))
  1897. } else {
  1898. colors.append(color.withAlphaComponent(1.0))
  1899. }
  1900. } else {
  1901. colors.append(color)
  1902. }
  1903. let value = ChartDataEntry(
  1904. x: predictionData[i].date,
  1905. y: predictionVal,
  1906. data: formatPillText(
  1907. line1: chartLabel,
  1908. time: predictionData[i].date,
  1909. line2: Localizer.toDisplayUnits(String(predictionVal))
  1910. )
  1911. )
  1912. mainChart.addEntry(value)
  1913. smallChart.addEntry(value)
  1914. }
  1915. smallChart.circleColors.removeAll()
  1916. smallChart.colors.removeAll()
  1917. mainChart.colors.removeAll()
  1918. mainChart.circleColors.removeAll()
  1919. if colors.count > 0 {
  1920. for color in colors {
  1921. mainChart.addColor(color)
  1922. mainChart.circleColors.append(color)
  1923. smallChart.addColor(color)
  1924. smallChart.circleColors.append(color)
  1925. }
  1926. }
  1927. BGChart.rightAxis.axisMaximum = Double(calculateMaxBgGraphValue())
  1928. BGChart.data?.dataSets[dataIndex].notifyDataSetChanged()
  1929. BGChart.data?.notifyDataChanged()
  1930. BGChart.notifyDataSetChanged()
  1931. BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged()
  1932. BGChartFull.data?.notifyDataChanged()
  1933. BGChartFull.notifyDataSetChanged()
  1934. }
  1935. }