Graphs.swift 88 KB

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