Graphs.swift 89 KB

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