// LoopFollow // Graphs.swift import Charts import Foundation import UIKit import Charts /// Fill colors for the override and temp-target bars on the BG graph. /// /// Loop draws overrides green and temp targets purple, while Trio (and other /// OpenAPS-based algorithms) use the inverse — overrides purple, temp targets /// green. We follow the active backend's convention so the colors match the /// looping app the user is running. enum TreatmentGraphColors { static var override: NSUIColor { Storage.shared.device.value == "Loop" ? .systemGreen : .systemPurple } static var tempTarget: NSUIColor { Storage.shared.device.value == "Loop" ? .systemPurple : .systemGreen } } enum GraphDataIndex: Int { case bg = 0 case prediction = 1 case basal = 2 case bolus = 3 case carbs = 4 case basalScheduled = 5 case override = 6 case bgCheck = 7 case suspend = 8 case resumePump = 9 case sensorStart = 10 case note = 11 case ztPrediction = 12 case iobPrediction = 13 case cobPrediction = 14 case uamPrediction = 15 case smb = 16 case tempTarget = 17 case predictionCone = 18 case yesterday = 19 } extension GraphDataIndex { var description: String { switch self { case .bg: return "BG" case .prediction: return "Prediction" case .basal: return "Basal" case .bolus: return "Bolus" case .carbs: return "Carbs" case .basalScheduled: return "Basal Scheduled" case .override: return "Override" case .bgCheck: return "BG Check" case .suspend: return "Suspend" case .resumePump: return "Resume Pump" case .sensorStart: return "Sensor Start" case .note: return "Note" case .ztPrediction: return "ZT Prediction" case .iobPrediction: return "IOB Prediction" case .cobPrediction: return "COB Prediction" case .uamPrediction: return "UAM Prediction" case .smb: return "SMB" case .tempTarget: return "Temp Target" case .predictionCone: return "Prediction Cone" case .yesterday: return "Yesterday" } } } class CompositeRenderer: LineChartRenderer { let tempTargetRenderer: TempTargetRenderer let triangleRenderer: TriangleRenderer let coneRenderer: ConeOfUncertaintyRenderer init(dataProvider: LineChartDataProvider?, animator: Animator?, viewPortHandler: ViewPortHandler?, tempTargetDataSetIndex: Int, smbDataSetIndex: Int, coneDataSetIndex: Int) { tempTargetRenderer = TempTargetRenderer( dataProvider: dataProvider, animator: animator, viewPortHandler: viewPortHandler, tempTargetDataSetIndex: tempTargetDataSetIndex ) triangleRenderer = TriangleRenderer( dataProvider: dataProvider, animator: animator, viewPortHandler: viewPortHandler, smbDataSetIndex: smbDataSetIndex ) coneRenderer = ConeOfUncertaintyRenderer( dataProvider: dataProvider, animator: animator, viewPortHandler: viewPortHandler, coneDataSetIndex: coneDataSetIndex ) super.init(dataProvider: dataProvider!, animator: animator!, viewPortHandler: viewPortHandler!) } override func drawExtras(context: CGContext) { super.drawExtras(context: context) coneRenderer.drawExtras(context: context) tempTargetRenderer.drawExtras(context: context) triangleRenderer.drawExtras(context: context) } } class TriangleRenderer: LineChartRenderer { let smbDataSetIndex: Int init(dataProvider: LineChartDataProvider?, animator: Animator?, viewPortHandler: ViewPortHandler?, smbDataSetIndex: Int) { self.smbDataSetIndex = smbDataSetIndex super.init(dataProvider: dataProvider!, animator: animator!, viewPortHandler: viewPortHandler!) } override func drawExtras(context: CGContext) { super.drawExtras(context: context) guard let dataProvider = dataProvider else { return } if dataProvider.lineData?.dataSets.count ?? 0 > smbDataSetIndex, let lineDataSet = dataProvider.lineData?.dataSets[smbDataSetIndex] as? LineChartDataSet { let trans = dataProvider.getTransformer(forAxis: lineDataSet.axisDependency) let phaseY = animator.phaseY for j in 0 ..< lineDataSet.entryCount { guard let e = lineDataSet.entryForIndex(j) else { continue } let pt = trans.pixelForValues(x: e.x, y: e.y * phaseY) context.saveGState() context.beginPath() context.move(to: CGPoint(x: pt.x, y: pt.y + 9)) context.addLine(to: CGPoint(x: pt.x - 5, y: pt.y - 1)) context.addLine(to: CGPoint(x: pt.x + 5, y: pt.y - 1)) context.closePath() context.setFillColor(lineDataSet.circleColors.first!.cgColor) context.fillPath() context.restoreGState() } } } } class TempTargetChartDataEntry: ChartDataEntry { var xStart: Double = 0.0 var xEnd: Double = 0.0 var yTop: Double = 0.0 var yBottom: Double = 0.0 required init() { super.init() } init(xStart: Double, xEnd: Double, yTop: Double, yBottom: Double, data: Any?) { self.xStart = xStart self.xEnd = xEnd self.yTop = yTop self.yBottom = yBottom super.init(x: xStart, y: yTop) self.data = data } override func copy(with _: NSZone? = nil) -> Any { let copy = TempTargetChartDataEntry( xStart: xStart, xEnd: xEnd, yTop: yTop, yBottom: yBottom, data: data ) return copy } } class TempTargetRenderer: LineChartRenderer { let tempTargetDataSetIndex: Int init(dataProvider: LineChartDataProvider?, animator: Animator?, viewPortHandler: ViewPortHandler?, tempTargetDataSetIndex: Int) { self.tempTargetDataSetIndex = tempTargetDataSetIndex super.init(dataProvider: dataProvider!, animator: animator!, viewPortHandler: viewPortHandler!) } override func drawExtras(context: CGContext) { super.drawExtras(context: context) guard let dataProvider = dataProvider else { return } if dataProvider.lineData?.dataSets.count ?? 0 > tempTargetDataSetIndex, let lineDataSet = dataProvider.lineData?.dataSets[tempTargetDataSetIndex] as? LineChartDataSet { let trans = dataProvider.getTransformer(forAxis: lineDataSet.axisDependency) let phaseY = animator.phaseY for i in 0 ..< lineDataSet.entryCount { guard let entry = lineDataSet.entryForIndex(i) as? TempTargetChartDataEntry else { continue } let xStart = entry.xStart let xEnd = entry.xEnd let yTop = entry.yTop * phaseY let yBottom = entry.yBottom * phaseY let leftTop = trans.pixelForValues(x: xStart, y: yTop) let rightBottom = trans.pixelForValues(x: xEnd, y: yBottom) var rect = CGRect(x: leftTop.x, y: leftTop.y, width: rightBottom.x - leftTop.x, height: rightBottom.y - leftTop.y) if rect.width < 0 { rect.origin.x += rect.width rect.size.width = abs(rect.width) } if rect.height < 0 { rect.origin.y += rect.height rect.size.height = abs(rect.height) } context.saveGState() context.setFillColor(TreatmentGraphColors.tempTarget.withAlphaComponent(0.5).cgColor) context.fill(rect) context.restoreGState() } } } } let ScaleXMax: Double = 150.0 extension MainViewController { private func graphRangeThresholds() -> (low: Double, high: Double) { UnitSettingsStore.shared.effectiveThresholds() } func updateChartRenderers() { let tempTargetDataIndex = GraphDataIndex.tempTarget.rawValue let smbDataIndex = GraphDataIndex.smb.rawValue let coneDataIndex = GraphDataIndex.predictionCone.rawValue let compositeRenderer = CompositeRenderer( dataProvider: BGChart, animator: BGChart.chartAnimator, viewPortHandler: BGChart.viewPortHandler, tempTargetDataSetIndex: tempTargetDataIndex, smbDataSetIndex: smbDataIndex, coneDataSetIndex: coneDataIndex ) BGChart.renderer = compositeRenderer BGChart.data?.notifyDataChanged() BGChart.notifyDataSetChanged() } func chartValueSelected(_ chartView: ChartViewBase, entry: ChartDataEntry, highlight _: Highlight) { if chartView == BGChartFull { BGChart.moveViewToX(entry.x) } if entry.data as? String == "hide" { BGChart.highlightValue(nil, callDelegate: false) } } func chartScaled(_: ChartViewBase, scaleX _: CGFloat, scaleY _: CGFloat) { // dont store huge values var scale = Double(BGChart.scaleX) if scale > ScaleXMax { scale = ScaleXMax } Storage.shared.chartScaleX.value = scale } func createGraph() { // Create the BG Graph Data let bgChartEntry = [ChartDataEntry]() let maxBG = Storage.shared.minBGScale.value // Setup BG line details let lineBG = LineChartDataSet(entries: bgChartEntry, label: "") lineBG.circleRadius = CGFloat(globalVariables.dotBG) lineBG.circleColors = [NSUIColor.systemGreen] lineBG.drawCircleHoleEnabled = false lineBG.axisDependency = YAxis.AxisDependency.right lineBG.highlightEnabled = true lineBG.drawValuesEnabled = false if Storage.shared.showLines.value { lineBG.lineWidth = 2 } else { lineBG.lineWidth = 0 } if Storage.shared.showDots.value { lineBG.drawCirclesEnabled = true } else { lineBG.drawCirclesEnabled = false } lineBG.setDrawHighlightIndicators(false) lineBG.valueFont.withSize(50) // Setup Prediction line details let predictionChartEntry = [ChartDataEntry]() let linePrediction = LineChartDataSet(entries: predictionChartEntry, label: "") linePrediction.circleRadius = CGFloat(globalVariables.dotBG) linePrediction.circleColors = [NSUIColor.systemPurple] linePrediction.colors = [NSUIColor.systemPurple] linePrediction.drawCircleHoleEnabled = false linePrediction.axisDependency = YAxis.AxisDependency.right linePrediction.highlightEnabled = true linePrediction.drawValuesEnabled = false if Storage.shared.showLines.value { linePrediction.lineWidth = 2 } else { linePrediction.lineWidth = 0 } if Storage.shared.showDots.value { linePrediction.drawCirclesEnabled = true } else { linePrediction.drawCirclesEnabled = false } linePrediction.setDrawHighlightIndicators(false) linePrediction.valueFont.withSize(50) // create Basal graph data let chartEntry = [ChartDataEntry]() let maxBasal = Storage.shared.minBasalScale.value let lineBasal = LineChartDataSet(entries: chartEntry, label: "") lineBasal.setDrawHighlightIndicators(false) lineBasal.setColor(NSUIColor.systemBlue, alpha: 0.5) lineBasal.lineWidth = 0 lineBasal.drawFilledEnabled = true lineBasal.fillColor = NSUIColor.systemBlue lineBasal.fillAlpha = 0.5 lineBasal.drawCirclesEnabled = false lineBasal.axisDependency = YAxis.AxisDependency.left lineBasal.highlightEnabled = true lineBasal.drawValuesEnabled = false lineBasal.fillFormatter = basalFillFormatter() // Boluses let chartEntryBolus = [ChartDataEntry]() let lineBolus = LineChartDataSet(entries: chartEntryBolus, label: "") lineBolus.circleRadius = CGFloat(globalVariables.dotBolus) lineBolus.circleColors = [NSUIColor.systemBlue.withAlphaComponent(0.75)] lineBolus.drawCircleHoleEnabled = false lineBolus.setDrawHighlightIndicators(false) lineBolus.setColor(NSUIColor.systemBlue, alpha: 1.0) lineBolus.lineWidth = 0 lineBolus.axisDependency = YAxis.AxisDependency.right lineBolus.valueFormatter = ChartYDataValueFormatter() lineBolus.valueTextColor = NSUIColor.label lineBolus.fillColor = NSUIColor.systemBlue lineBolus.fillAlpha = 0.6 lineBolus.drawCirclesEnabled = true lineBolus.drawFilledEnabled = false lineBolus.drawValuesEnabled = Storage.shared.showValues.value lineBolus.highlightEnabled = true // Carbs let chartEntryCarbs = [ChartDataEntry]() let lineCarbs = LineChartDataSet(entries: chartEntryCarbs, label: "") lineCarbs.circleRadius = CGFloat(globalVariables.dotCarb) lineCarbs.circleColors = [NSUIColor.systemOrange.withAlphaComponent(0.75)] lineCarbs.drawCircleHoleEnabled = false lineCarbs.setDrawHighlightIndicators(false) lineCarbs.setColor(NSUIColor.systemBlue, alpha: 1.0) lineCarbs.lineWidth = 0 lineCarbs.axisDependency = YAxis.AxisDependency.right lineCarbs.valueFormatter = ChartYDataValueFormatter() lineCarbs.valueTextColor = NSUIColor.label lineCarbs.fillColor = NSUIColor.systemOrange lineCarbs.fillAlpha = 0.6 lineCarbs.drawCirclesEnabled = true lineCarbs.drawFilledEnabled = false lineCarbs.drawValuesEnabled = Storage.shared.showValues.value lineCarbs.highlightEnabled = true // create Scheduled Basal graph data let chartBasalScheduledEntry = [ChartDataEntry]() let lineBasalScheduled = LineChartDataSet(entries: chartBasalScheduledEntry, label: "") lineBasalScheduled.setDrawHighlightIndicators(false) lineBasalScheduled.setColor(NSUIColor.systemBlue, alpha: 0.8) lineBasalScheduled.lineWidth = 2 lineBasalScheduled.drawFilledEnabled = false lineBasalScheduled.drawCirclesEnabled = false lineBasalScheduled.axisDependency = YAxis.AxisDependency.left lineBasalScheduled.highlightEnabled = false lineBasalScheduled.drawValuesEnabled = false lineBasalScheduled.lineDashLengths = [10.0, 5.0] // create Override graph data let chartOverrideEntry = [ChartDataEntry]() let lineOverride = LineChartDataSet(entries: chartOverrideEntry, label: "") lineOverride.setDrawHighlightIndicators(false) lineOverride.lineWidth = 0 lineOverride.drawFilledEnabled = true lineOverride.fillFormatter = OverrideFillFormatter() lineOverride.fillColor = TreatmentGraphColors.override lineOverride.fillAlpha = 0.6 lineOverride.drawCirclesEnabled = false lineOverride.axisDependency = YAxis.AxisDependency.right lineOverride.highlightEnabled = true lineOverride.drawValuesEnabled = false // BG Check let chartEntryBGCheck = [ChartDataEntry]() let lineBGCheck = LineChartDataSet(entries: chartEntryBGCheck, label: "") lineBGCheck.circleRadius = CGFloat(globalVariables.dotOther) lineBGCheck.circleColors = [NSUIColor.systemRed.withAlphaComponent(0.75)] lineBGCheck.drawCircleHoleEnabled = false lineBGCheck.setDrawHighlightIndicators(false) lineBGCheck.setColor(NSUIColor.systemRed, alpha: 1.0) lineBGCheck.drawCirclesEnabled = true lineBGCheck.lineWidth = 0 lineBGCheck.highlightEnabled = true lineBGCheck.axisDependency = YAxis.AxisDependency.right lineBGCheck.valueFormatter = ChartYDataValueFormatter() lineBGCheck.drawValuesEnabled = false // Suspend Pump let chartEntrySuspend = [ChartDataEntry]() let lineSuspend = LineChartDataSet(entries: chartEntrySuspend, label: "") lineSuspend.circleRadius = CGFloat(globalVariables.dotOther) lineSuspend.circleColors = [NSUIColor.systemTeal.withAlphaComponent(0.75)] lineSuspend.drawCircleHoleEnabled = false lineSuspend.setDrawHighlightIndicators(false) lineSuspend.setColor(NSUIColor.systemGray2, alpha: 1.0) lineSuspend.drawCirclesEnabled = true lineSuspend.lineWidth = 0 lineSuspend.highlightEnabled = true lineSuspend.axisDependency = YAxis.AxisDependency.right lineSuspend.valueFormatter = ChartYDataValueFormatter() lineSuspend.drawValuesEnabled = false // Resume Pump let chartEntryResume = [ChartDataEntry]() let lineResume = LineChartDataSet(entries: chartEntryResume, label: "") lineResume.circleRadius = CGFloat(globalVariables.dotOther) lineResume.circleColors = [NSUIColor.systemTeal.withAlphaComponent(0.75)] lineResume.drawCircleHoleEnabled = false lineResume.setDrawHighlightIndicators(false) lineResume.setColor(NSUIColor.systemGray4, alpha: 1.0) lineResume.drawCirclesEnabled = true lineResume.lineWidth = 0 lineResume.highlightEnabled = true lineResume.axisDependency = YAxis.AxisDependency.right lineResume.valueFormatter = ChartYDataValueFormatter() lineResume.drawValuesEnabled = false // Sensor Start let chartEntrySensor = [ChartDataEntry]() let lineSensor = LineChartDataSet(entries: chartEntrySensor, label: "") lineSensor.circleRadius = CGFloat(globalVariables.dotOther) lineSensor.circleColors = [NSUIColor.systemIndigo.withAlphaComponent(0.75)] lineSensor.drawCircleHoleEnabled = false lineSensor.setDrawHighlightIndicators(false) lineSensor.setColor(NSUIColor.systemGray3, alpha: 1.0) lineSensor.drawCirclesEnabled = true lineSensor.lineWidth = 0 lineSensor.highlightEnabled = true lineSensor.axisDependency = YAxis.AxisDependency.right lineSensor.valueFormatter = ChartYDataValueFormatter() lineSensor.drawValuesEnabled = false // Notes let chartEntryNote = [ChartDataEntry]() let lineNote = LineChartDataSet(entries: chartEntryNote, label: "") lineNote.circleRadius = CGFloat(globalVariables.dotOther) lineNote.circleColors = [NSUIColor.systemGray.withAlphaComponent(0.75)] lineNote.drawCircleHoleEnabled = false lineNote.setDrawHighlightIndicators(false) lineNote.setColor(NSUIColor.systemGray3, alpha: 1.0) lineNote.drawCirclesEnabled = true lineNote.lineWidth = 0 lineNote.highlightEnabled = true lineNote.axisDependency = YAxis.AxisDependency.right lineNote.valueFormatter = ChartYDataValueFormatter() lineNote.drawValuesEnabled = false // Setup COB Prediction line details let COBpredictionChartEntry = [ChartDataEntry]() let COBlinePrediction = LineChartDataSet(entries: COBpredictionChartEntry, label: "") COBlinePrediction.circleRadius = CGFloat(globalVariables.dotBG) COBlinePrediction.circleColors = [NSUIColor.systemPurple] COBlinePrediction.colors = [NSUIColor.systemPurple] COBlinePrediction.drawCircleHoleEnabled = false COBlinePrediction.axisDependency = YAxis.AxisDependency.right COBlinePrediction.highlightEnabled = true COBlinePrediction.drawValuesEnabled = false if Storage.shared.showLines.value { COBlinePrediction.lineWidth = 2 } else { COBlinePrediction.lineWidth = 0 } if Storage.shared.showDots.value { COBlinePrediction.drawCirclesEnabled = true } else { COBlinePrediction.drawCirclesEnabled = false } COBlinePrediction.setDrawHighlightIndicators(false) COBlinePrediction.valueFont.withSize(50) // Setup IOB Prediction line details let IOBpredictionChartEntry = [ChartDataEntry]() let IOBlinePrediction = LineChartDataSet(entries: IOBpredictionChartEntry, label: "") IOBlinePrediction.circleRadius = CGFloat(globalVariables.dotBG) IOBlinePrediction.circleColors = [NSUIColor.systemPurple] IOBlinePrediction.colors = [NSUIColor.systemPurple] IOBlinePrediction.drawCircleHoleEnabled = false IOBlinePrediction.axisDependency = YAxis.AxisDependency.right IOBlinePrediction.highlightEnabled = true IOBlinePrediction.drawValuesEnabled = false if Storage.shared.showLines.value { IOBlinePrediction.lineWidth = 2 } else { IOBlinePrediction.lineWidth = 0 } if Storage.shared.showDots.value { IOBlinePrediction.drawCirclesEnabled = true } else { IOBlinePrediction.drawCirclesEnabled = false } IOBlinePrediction.setDrawHighlightIndicators(false) IOBlinePrediction.valueFont.withSize(50) // Setup UAM Prediction line details let UAMpredictionChartEntry = [ChartDataEntry]() let UAMlinePrediction = LineChartDataSet(entries: UAMpredictionChartEntry, label: "") UAMlinePrediction.circleRadius = CGFloat(globalVariables.dotBG) UAMlinePrediction.circleColors = [NSUIColor.systemPurple] UAMlinePrediction.colors = [NSUIColor.systemPurple] UAMlinePrediction.drawCircleHoleEnabled = false UAMlinePrediction.axisDependency = YAxis.AxisDependency.right UAMlinePrediction.highlightEnabled = true UAMlinePrediction.drawValuesEnabled = false if Storage.shared.showLines.value { UAMlinePrediction.lineWidth = 2 } else { UAMlinePrediction.lineWidth = 0 } if Storage.shared.showDots.value { UAMlinePrediction.drawCirclesEnabled = true } else { UAMlinePrediction.drawCirclesEnabled = false } linePrediction.setDrawHighlightIndicators(false) linePrediction.valueFont.withSize(50) // Setup ZT Prediction line details let ZTpredictionChartEntry = [ChartDataEntry]() let ZTlinePrediction = LineChartDataSet(entries: ZTpredictionChartEntry, label: "") ZTlinePrediction.circleRadius = CGFloat(globalVariables.dotBG) ZTlinePrediction.circleColors = [NSUIColor.systemPurple] ZTlinePrediction.colors = [NSUIColor.systemPurple] ZTlinePrediction.drawCircleHoleEnabled = false ZTlinePrediction.axisDependency = YAxis.AxisDependency.right ZTlinePrediction.highlightEnabled = true ZTlinePrediction.drawValuesEnabled = false if Storage.shared.showLines.value { ZTlinePrediction.lineWidth = 2 } else { ZTlinePrediction.lineWidth = 0 } if Storage.shared.showDots.value { ZTlinePrediction.drawCirclesEnabled = true } else { ZTlinePrediction.drawCirclesEnabled = false } ZTlinePrediction.setDrawHighlightIndicators(false) ZTlinePrediction.valueFont.withSize(50) // SMB let chartEntrySmb = [ChartDataEntry]() let lineSmb = LineChartDataSet(entries: chartEntrySmb, label: "") lineSmb.circleRadius = CGFloat(globalVariables.dotBolus) lineSmb.circleColors = [NSUIColor.systemBlue.withAlphaComponent(1.0)] lineSmb.drawCircleHoleEnabled = false lineSmb.setDrawHighlightIndicators(false) lineSmb.setColor(NSUIColor.red, alpha: 1.0) lineSmb.lineWidth = 0 lineSmb.axisDependency = YAxis.AxisDependency.right lineSmb.valueFormatter = ChartYDataValueFormatter() lineSmb.valueTextColor = NSUIColor.label lineSmb.drawCirclesEnabled = false lineSmb.drawFilledEnabled = false lineSmb.drawValuesEnabled = Storage.shared.showValues.value lineSmb.highlightEnabled = true // TempTarget graph data let chartTempTargetEntry = [ChartDataEntry]() let lineTempTarget = LineChartDataSet(entries: chartTempTargetEntry, label: "") lineTempTarget.setDrawHighlightIndicators(false) lineTempTarget.lineWidth = 0 lineTempTarget.drawFilledEnabled = false lineTempTarget.fillColor = NSUIColor.systemPurple lineTempTarget.fillAlpha = 0.6 lineTempTarget.drawCirclesEnabled = false lineTempTarget.axisDependency = YAxis.AxisDependency.right lineTempTarget.highlightEnabled = true lineTempTarget.drawValuesEnabled = false // Setup the chart data of all lines let data = LineChartData() data.append(lineBG) // Dataset 0 data.append(linePrediction) // Dataset 1 data.append(lineBasal) // Dataset 2 data.append(lineBolus) // Dataset 3 data.append(lineCarbs) // Dataset 4 data.append(lineBasalScheduled) // Dataset 5 data.append(lineOverride) // Dataset 6 data.append(lineBGCheck) // Dataset 7 data.append(lineSuspend) // Dataset 8 data.append(lineResume) // Dataset 9 data.append(lineSensor) // Dataset 10 data.append(lineNote) // Dataset 11 data.append(ZTlinePrediction) // Dataset 12 data.append(IOBlinePrediction) // Dataset 13 data.append(COBlinePrediction) // Dataset 14 data.append(UAMlinePrediction) // Dataset 15 data.append(lineSmb) // Dataset 16 data.append(lineTempTarget) // Dataset 17 // Dataset 18: Prediction Cone (rendered via ConeOfUncertaintyRenderer) let lineCone = LineChartDataSet(entries: [ChartDataEntry](), label: "") lineCone.lineWidth = 0 lineCone.drawCirclesEnabled = false lineCone.drawValuesEnabled = false lineCone.highlightEnabled = false lineCone.axisDependency = YAxis.AxisDependency.right data.append(lineCone) // Dataset 19: Yesterday's BG comparison overlay (thin dimmed gray line, no dots) let lineYesterday = LineChartDataSet(entries: [ChartDataEntry](), label: "") lineYesterday.lineWidth = 1.5 lineYesterday.setColor(NSUIColor.systemGray, alpha: 0.4) lineYesterday.drawCirclesEnabled = false lineYesterday.drawValuesEnabled = false lineYesterday.highlightEnabled = false lineYesterday.axisDependency = YAxis.AxisDependency.right data.append(lineYesterday) data.setValueFont(UIFont.systemFont(ofSize: 12)) // Add marker popups for bolus and carbs let marker = PillMarker(color: .secondarySystemBackground, font: UIFont.boldSystemFont(ofSize: 14), textColor: .label) BGChart.marker = marker // Clear limit lines so they don't add multiples when changing the settings BGChart.rightAxis.removeAllLimitLines() let thresholds = graphRangeThresholds() // Add lower red line let ll = ChartLimitLine() ll.limit = thresholds.low ll.lineColor = NSUIColor.systemRed.withAlphaComponent(0.5) BGChart.rightAxis.addLimitLine(ll) // Add upper yellow line let ul = ChartLimitLine() ul.limit = thresholds.high ul.lineColor = NSUIColor.systemYellow.withAlphaComponent(0.5) BGChart.rightAxis.addLimitLine(ul) // Add vertical lines as configured createVerticalLines() startGraphNowTimer() // Setup the main graph overall details BGChart.xAxis.valueFormatter = ChartXValueFormatter() BGChart.xAxis.granularity = 1800 BGChart.xAxis.labelTextColor = NSUIColor.label BGChart.xAxis.labelPosition = XAxis.LabelPosition.bottom BGChart.xAxis.drawGridLinesEnabled = false BGChart.leftAxis.enabled = true BGChart.leftAxis.labelPosition = YAxis.LabelPosition.insideChart BGChart.leftAxis.axisMaximum = maxBasal BGChart.leftAxis.axisMinimum = 0 BGChart.leftAxis.drawGridLinesEnabled = false BGChart.leftAxis.granularityEnabled = true BGChart.leftAxis.granularity = 0.5 BGChart.rightAxis.labelTextColor = NSUIColor.label BGChart.rightAxis.labelPosition = YAxis.LabelPosition.insideChart BGChart.rightAxis.axisMinimum = 0.0 BGChart.rightAxis.axisMaximum = Double(maxBG) BGChart.rightAxis.gridLineDashLengths = [5.0, 5.0] BGChart.rightAxis.drawGridLinesEnabled = false BGChart.rightAxis.valueFormatter = ChartYMMOLValueFormatter() BGChart.rightAxis.granularityEnabled = true BGChart.rightAxis.granularity = 50 BGChart.maxHighlightDistance = 15.0 BGChart.legend.enabled = false BGChart.scaleYEnabled = false BGChart.drawGridBackgroundEnabled = true BGChart.gridBackgroundColor = NSUIColor.secondarySystemBackground BGChart.highlightValue(nil, callDelegate: false) BGChart.data = data BGChart.setExtraOffsets(left: 5, top: 10, right: 5, bottom: 10) } func createVerticalLines() { BGChart.xAxis.removeAllLimitLines() BGChartFull.xAxis.removeAllLimitLines() createNowAndDIALines() createMidnightLines() } func createNowAndDIALines() { let ul = ChartLimitLine() ul.limit = Double(dateTimeUtils.getNowTimeIntervalUTC()) ul.lineColor = NSUIColor.systemGray.withAlphaComponent(0.5) ul.lineWidth = 1 BGChart.xAxis.addLimitLine(ul) if Storage.shared.show30MinLine.value { let ul2 = ChartLimitLine() ul2.limit = Double(dateTimeUtils.getNowTimeIntervalUTC().advanced(by: -30 * 60)) ul2.lineColor = NSUIColor.systemBlue.withAlphaComponent(0.5) ul2.lineWidth = 1 BGChart.xAxis.addLimitLine(ul2) } if Storage.shared.showDIALines.value { for i in 1 ..< 7 { let ul = ChartLimitLine() ul.limit = Double(dateTimeUtils.getNowTimeIntervalUTC() - Double(i * 60 * 60)) ul.lineColor = NSUIColor.systemGray.withAlphaComponent(0.3) let dash = 10.0 - Double(i) let space = 5.0 + Double(i) ul.lineDashLengths = [CGFloat(dash), CGFloat(space)] ul.lineWidth = 1 BGChart.xAxis.addLimitLine(ul) } } if Storage.shared.show90MinLine.value { let ul3 = ChartLimitLine() ul3.limit = Double(dateTimeUtils.getNowTimeIntervalUTC().advanced(by: -90 * 60)) ul3.lineColor = NSUIColor.systemOrange.withAlphaComponent(0.5) ul3.lineWidth = 1 BGChart.xAxis.addLimitLine(ul3) } } func createMidnightLines() { // Draw a line at midnight: useful when showing multiple days of data if Storage.shared.showMidnightLines.value { var midnightTimeInterval: TimeInterval if Storage.shared.graphTimeZoneEnabled.value, let tz = TimeZone(identifier: Storage.shared.graphTimeZoneIdentifier.value) { var cal = Calendar.current cal.timeZone = tz midnightTimeInterval = cal.startOfDay(for: Date()).timeIntervalSince1970 } else { midnightTimeInterval = dateTimeUtils.getTimeIntervalMidnightToday() } let graphHours = 24 * Storage.shared.downloadDays.value let graphStart = dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) while midnightTimeInterval > graphStart { // Large chart let ul = ChartLimitLine() ul.limit = Double(midnightTimeInterval) ul.lineColor = NSUIColor.systemTeal.withAlphaComponent(0.5) ul.lineDashLengths = [CGFloat(2), CGFloat(5)] ul.lineWidth = 1 BGChart.xAxis.addLimitLine(ul) // Small chart let sl = ChartLimitLine() sl.limit = Double(midnightTimeInterval) sl.lineColor = NSUIColor.systemTeal sl.lineDashLengths = [CGFloat(2), CGFloat(2)] sl.lineWidth = 1 BGChartFull.xAxis.addLimitLine(sl) midnightTimeInterval = midnightTimeInterval.advanced(by: -24 * 60 * 60) } } } func updateBGGraphSettings() { let dataIndex = 0 let dataIndexPrediction = 1 let lineBG = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet let linePrediction = BGChart.lineData!.dataSets[dataIndexPrediction] as! LineChartDataSet if Storage.shared.showLines.value { lineBG.lineWidth = 2 linePrediction.lineWidth = 2 } else { lineBG.lineWidth = 0 linePrediction.lineWidth = 0 } if Storage.shared.showDots.value { lineBG.drawCirclesEnabled = true linePrediction.drawCirclesEnabled = true } else { lineBG.drawCirclesEnabled = false linePrediction.drawCirclesEnabled = false } BGChart.rightAxis.axisMinimum = 0 // Clear limit lines so they don't add multiples when changing the settings BGChart.rightAxis.removeAllLimitLines() let thresholds = graphRangeThresholds() // Add lower red line let ll = ChartLimitLine() ll.limit = thresholds.low ll.lineColor = NSUIColor.systemRed.withAlphaComponent(0.5) BGChart.rightAxis.addLimitLine(ll) // Add upper yellow line let ul = ChartLimitLine() ul.limit = thresholds.high ul.lineColor = NSUIColor.systemYellow.withAlphaComponent(0.5) BGChart.rightAxis.addLimitLine(ul) // Re-create vertical markers in case their settings changed createVerticalLines() BGChart.data?.dataSets[dataIndex].notifyDataSetChanged() BGChart.data?.notifyDataChanged() BGChart.notifyDataSetChanged() // Reflect the yesterday overlay toggle immediately, and reload the BG window // so the extra day of history is fetched (or dropped) when the toggle changed. updateYesterdayBGGraph() TaskScheduler.shared.rescheduleTask(id: .fetchBG, to: Date()) // Re-render prediction display in case display type changed updateOpenAPSPredictionDisplay() } func updateBGGraph() { let dataIndex = 0 let entries = bgData guard !entries.isEmpty else { return } let mainChart = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet let smallChart = BGChartFull.lineData!.dataSets[dataIndex] as! LineChartDataSet mainChart.removeAll(keepingCapacity: false) smallChart.removeAll(keepingCapacity: false) let maxBGOffset: Double = 50 var colors = [NSUIColor]() topBG = Storage.shared.minBGScale.value let thresholds = graphRangeThresholds() for i in 0 ..< entries.count { // Clamp the plotted y-value to the same bounds the header text uses // (HIGH/LOW), so the graph stays consistent with the main display. // The pill tooltip still shows the raw reading. let plottedSgv = Double(min(max(entries[i].sgv, globalVariables.minDisplayGlucose), globalVariables.maxDisplayGlucose)) if plottedSgv > topBG - maxBGOffset { topBG = plottedSgv + maxBGOffset } let value = ChartDataEntry(x: Double(entries[i].date), y: plottedSgv, data: formatPillText(line1: Localizer.toDisplayUnits(String(entries[i].sgv)), time: entries[i].date)) mainChart.append(value) smallChart.append(value) if Double(entries[i].sgv) >= thresholds.high { colors.append(NSUIColor.systemYellow) } else if Double(entries[i].sgv) <= thresholds.low { colors.append(NSUIColor.systemRed) } else { colors.append(NSUIColor.systemGreen) } } // Set Colors let lineBG = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet let lineBGSmall = BGChartFull.lineData!.dataSets[dataIndex] as! LineChartDataSet lineBG.colors.removeAll() lineBG.circleColors.removeAll() lineBGSmall.colors.removeAll() lineBGSmall.circleColors.removeAll() if colors.count > 0 { for i in 0 ..< colors.count { mainChart.addColor(colors[i]) mainChart.circleColors.append(colors[i]) smallChart.addColor(colors[i]) smallChart.circleColors.append(colors[i]) } } BGChart.rightAxis.axisMaximum = Double(calculateMaxBgGraphValue()) BGChart.setVisibleXRangeMinimum(600) BGChart.data?.dataSets[dataIndex].notifyDataSetChanged() BGChart.data?.notifyDataChanged() BGChart.notifyDataSetChanged() BGChartFull.rightAxis.axisMaximum = Double(calculateMaxBgGraphValue()) BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged() BGChartFull.data?.notifyDataChanged() BGChartFull.notifyDataSetChanged() updateYesterdayBGGraph() // The initial zoom is a one-shot, relative to the chart's current // viewport. Skip it until the chart actually has a width — otherwise a // refresh that lands while the view is loaded but off-screen (e.g. Home // lives in the Menu, so MainViewController is force-loaded headless by // bootstrap()) would consume firstGraphLoad against a zero-size viewport // and leave the main graph blank. viewDidAppear re-runs updateBGGraph // once a real frame exists, applying the zoom correctly. if firstGraphLoad, BGChart.bounds.width > 0 { var scaleX = CGFloat(Storage.shared.chartScaleX.value) if scaleX > CGFloat(ScaleXMax) { scaleX = CGFloat(ScaleXMax) Storage.shared.chartScaleX.value = ScaleXMax } BGChart.zoom(scaleX: scaleX, scaleY: 1, x: 1, y: 1) firstGraphLoad = false } // Move to current reading everytime new readings load // Check if auto-scrolling should be performed if autoScrollPauseUntil == nil || Date() > autoScrollPauseUntil! { BGChart.moveViewToAnimated(xValue: dateTimeUtils.getNowTimeIntervalUTC() - (BGChart.visibleXRange * 0.7), yValue: 0.0, axis: .right, duration: 1, easingOption: .easeInBack) } } // Populates (or clears) the dimmed "yesterday" comparison overlay on the main graph. // Points in yesterdayBGData are already shifted +24h so they align with today's clock time. func updateYesterdayBGGraph() { let dataIndex = GraphDataIndex.yesterday.rawValue guard let lineData = BGChart.lineData, dataIndex < lineData.dataSets.count, let dataSet = lineData.dataSets[dataIndex] as? LineChartDataSet else { return } dataSet.removeAll(keepingCapacity: false) if Storage.shared.showYesterdayLine.value { for entry in yesterdayBGData { // Clamp the plotted y-value to the same bounds the main BG line uses. let plottedSgv = Double(min(max(entry.sgv, globalVariables.minDisplayGlucose), globalVariables.maxDisplayGlucose)) dataSet.append(ChartDataEntry(x: entry.date, y: plottedSgv)) } } BGChart.data?.dataSets[dataIndex].notifyDataSetChanged() BGChart.data?.notifyDataChanged() BGChart.notifyDataSetChanged() } func updatePredictionGraph(color: UIColor? = nil) { let dataIndex = 1 var mainChart = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet var smallChart = BGChartFull.lineData!.dataSets[dataIndex] as! LineChartDataSet mainChart.clear() smallChart.clear() var colors = [NSUIColor]() let maxBGOffset: Double = 20 topPredictionBG = Storage.shared.minBGScale.value for i in 0 ..< predictionData.count { var predictionVal = Double(predictionData[i].sgv) if Double(predictionVal) > topPredictionBG - maxBGOffset { topPredictionBG = predictionVal + maxBGOffset } if i == 0 { if Storage.shared.showDots.value { colors.append((color ?? NSUIColor.systemPurple).withAlphaComponent(0.0)) } else { colors.append((color ?? NSUIColor.systemPurple).withAlphaComponent(1.0)) } } else if predictionVal > 400 { colors.append(color ?? NSUIColor.systemYellow) } else if predictionVal < 0 { colors.append(color ?? NSUIColor.systemRed) } else { colors.append(color ?? NSUIColor.systemPurple) } let value = ChartDataEntry(x: predictionData[i].date, y: predictionVal, data: formatPillText(line1: Localizer.toDisplayUnits(String(predictionData[i].sgv)), time: predictionData[i].date)) mainChart.addEntry(value) smallChart.addEntry(value) } smallChart.circleColors.removeAll() smallChart.colors.removeAll() mainChart.colors.removeAll() mainChart.circleColors.removeAll() if colors.count > 0 { for i in 0 ..< colors.count { mainChart.addColor(colors[i]) mainChart.circleColors.append(colors[i]) smallChart.addColor(colors[i]) smallChart.circleColors.append(colors[i]) } } BGChart.rightAxis.axisMaximum = Double(calculateMaxBgGraphValue()) BGChart.data?.dataSets[dataIndex].notifyDataSetChanged() BGChart.data?.notifyDataChanged() BGChart.notifyDataSetChanged() BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged() BGChartFull.data?.notifyDataChanged() BGChartFull.notifyDataSetChanged() } func updateBasalGraph() { var dataIndex = 2 BGChart.lineData?.dataSets[dataIndex].clear() BGChartFull.lineData?.dataSets[dataIndex].clear() var maxBasal = Storage.shared.minBasalScale.value var maxBasalSmall = 0.0 for i in 0 ..< basalData.count { let value = ChartDataEntry(x: Double(basalData[i].date), y: Double(basalData[i].basalRate), data: formatPillText(line1: String(basalData[i].basalRate), time: basalData[i].date)) BGChart.data?.dataSets[dataIndex].addEntry(value) if Storage.shared.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].addEntry(value) } if basalData[i].basalRate > maxBasal { maxBasal = basalData[i].basalRate } if basalData[i].basalRate > maxBasalSmall { maxBasalSmall = basalData[i].basalRate } } BGChart.leftAxis.axisMaximum = maxBasal BGChartFull.leftAxis.axisMaximum = maxBasalSmall BGChart.data?.dataSets[dataIndex].notifyDataSetChanged() BGChart.data?.notifyDataChanged() BGChart.notifyDataSetChanged() if Storage.shared.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged() BGChartFull.data?.notifyDataChanged() BGChartFull.notifyDataSetChanged() } } func updateBasalScheduledGraph() { var dataIndex = 5 BGChart.lineData?.dataSets[dataIndex].clear() BGChartFull.lineData?.dataSets[dataIndex].clear() for i in 0 ..< basalScheduleData.count { let value = ChartDataEntry(x: Double(basalScheduleData[i].date), y: Double(basalScheduleData[i].basalRate)) BGChart.data?.dataSets[dataIndex].addEntry(value) if Storage.shared.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].addEntry(value) } } BGChart.data?.dataSets[dataIndex].notifyDataSetChanged() BGChart.data?.notifyDataChanged() BGChart.notifyDataSetChanged() if Storage.shared.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged() BGChartFull.data?.notifyDataChanged() BGChartFull.notifyDataSetChanged() } } func updateBolusGraph() { var dataIndex = 3 var yTop: Double = 370 var yBottom: Double = 345 var mainChart = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet var smallChart = BGChartFull.lineData!.dataSets[dataIndex] as! LineChartDataSet mainChart.clear() smallChart.clear() var colors = [NSUIColor]() for i in 0 ..< bolusData.count { let formatter = NumberFormatter() formatter.minimumFractionDigits = 0 formatter.maximumFractionDigits = 2 formatter.minimumIntegerDigits = 0 // Check overlapping carbs to shift left if needed let bolusShift = findNextBolusTime(timeWithin: 240, needle: bolusData[i].date, haystack: bolusData, startingIndex: i) var dateTimeStamp = bolusData[i].date colors.append(NSUIColor.systemBlue.withAlphaComponent(1.0)) if bolusShift { // Move it half the distance between BG readings dateTimeStamp = dateTimeStamp - 150 } // skip if outside of visible area let graphHours = 24 * Storage.shared.downloadDays.value if dateTimeStamp < dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) { continue } let valueString = formatter.string(from: NSNumber(value: bolusData[i].value)) ?? "" let dot = ChartDataEntry(x: Double(dateTimeStamp), y: Double(bolusData[i].sgv), data: valueString + "\r\r" + formatPillText(line1: valueString + " U", time: bolusData[i].date)) mainChart.addEntry(dot) if Storage.shared.smallGraphTreatments.value { smallChart.addEntry(dot) } } // Set Colors let lineBolus = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet let lineBolusSmall = BGChartFull.lineData!.dataSets[dataIndex] as! LineChartDataSet lineBolus.colors.removeAll() lineBolus.circleColors.removeAll() lineBolusSmall.colors.removeAll() lineBolusSmall.circleColors.removeAll() if colors.count > 0 { for i in 0 ..< colors.count { mainChart.addColor(colors[i]) mainChart.circleColors.append(colors[i]) smallChart.addColor(colors[i]) smallChart.circleColors.append(colors[i]) } } BGChart.data?.dataSets[dataIndex].notifyDataSetChanged() BGChart.data?.notifyDataChanged() BGChart.notifyDataSetChanged() if Storage.shared.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged() BGChartFull.data?.notifyDataChanged() BGChartFull.notifyDataSetChanged() } } func updateSmbGraph() { var dataIndex = 16 var yTop: Double = 370 var yBottom: Double = 345 var mainChart = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet var smallChart = BGChartFull.lineData!.dataSets[dataIndex] as! LineChartDataSet mainChart.clear() smallChart.clear() let lightBlue = NSUIColor(red: 135 / 255, green: 206 / 255, blue: 235 / 255, alpha: 1.0) // Light Sky Blue var colors = [NSUIColor]() for i in 0 ..< smbData.count { let formatter = NumberFormatter() formatter.minimumFractionDigits = 0 formatter.maximumFractionDigits = 2 formatter.minimumIntegerDigits = 0 let bolusShift = findNextBolusTime(timeWithin: 240, needle: smbData[i].date, haystack: smbData, startingIndex: i) var dateTimeStamp = smbData[i].date let nowTime = dateTimeUtils.getNowTimeIntervalUTC() let diffTimeHours = (nowTime - dateTimeStamp) / 60 / 60 if diffTimeHours <= 1 { colors.append(lightBlue.withAlphaComponent(1.0)) } else if diffTimeHours > 6 { colors.append(lightBlue.withAlphaComponent(0.25)) } else { let thisAlpha = 1.0 - (0.15 * diffTimeHours) colors.append(lightBlue.withAlphaComponent(CGFloat(thisAlpha))) } if bolusShift { dateTimeStamp = dateTimeStamp - 150 } let graphHours = 24 * Storage.shared.downloadDays.value if dateTimeStamp < dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) { continue } let valueString = formatter.string(from: NSNumber(value: smbData[i].value)) ?? "" let dot = ChartDataEntry(x: Double(dateTimeStamp), y: Double(smbData[i].sgv), data: valueString + "\r\r" + formatPillText(line1: valueString + " U", time: smbData[i].date)) mainChart.addEntry(dot) if Storage.shared.smallGraphTreatments.value { smallChart.addEntry(dot) } } BGChart.data?.dataSets[dataIndex].notifyDataSetChanged() BGChart.data?.notifyDataChanged() BGChart.notifyDataSetChanged() if Storage.shared.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged() BGChartFull.data?.notifyDataChanged() BGChartFull.notifyDataSetChanged() } } func updateCarbGraph() { var dataIndex = 4 var mainChart = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet var smallChart = BGChartFull.lineData!.dataSets[dataIndex] as! LineChartDataSet mainChart.removeAll(keepingCapacity: true) smallChart.removeAll(keepingCapacity: true) var colors = [NSUIColor]() for i in 0 ..< carbData.count { let formatter = NumberFormatter() formatter.minimumFractionDigits = 0 formatter.maximumFractionDigits = 2 formatter.minimumIntegerDigits = 1 let carbAmountString = formatter.string(from: NSNumber(value: carbData[i].value))! var valueString = carbAmountString var markerLine1 = carbAmountString + "g" var hours = 3 if carbData[i].absorptionTime > 0, Storage.shared.showAbsorption.value { hours = carbData[i].absorptionTime / 60 valueString += " " + String(hours) + "h" markerLine1 += " " + String(hours) + "h" } // Check overlapping carbs to shift left if needed let carbShift = findNextCarbTime(timeWithin: 250, needle: carbData[i].date, haystack: carbData, startingIndex: i) var dateTimeStamp = carbData[i].date colors.append(NSUIColor.systemOrange.withAlphaComponent(1.0)) // skip if outside of visible area let graphHours = 24 * Storage.shared.downloadDays.value if dateTimeStamp < dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) { continue } if carbShift { dateTimeStamp = dateTimeStamp - 250 } let dot = ChartDataEntry(x: Double(dateTimeStamp), y: Double(carbData[i].sgv), data: valueString + "\r\r" + formatPillText(line1: markerLine1, time: carbData[i].date)) BGChart.data?.dataSets[dataIndex].addEntry(dot) if Storage.shared.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].addEntry(dot) } } // Set Colors let lineCarbs = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet let lineCarbsSmall = BGChartFull.lineData!.dataSets[dataIndex] as! LineChartDataSet lineCarbs.colors.removeAll() lineCarbs.circleColors.removeAll() lineCarbsSmall.colors.removeAll() lineCarbsSmall.circleColors.removeAll() if colors.count > 0 { for i in 0 ..< colors.count { mainChart.addColor(colors[i]) mainChart.circleColors.append(colors[i]) smallChart.addColor(colors[i]) smallChart.circleColors.append(colors[i]) } } BGChart.data?.dataSets[dataIndex].notifyDataSetChanged() BGChart.data?.notifyDataChanged() BGChart.notifyDataSetChanged() if Storage.shared.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged() BGChartFull.data?.notifyDataChanged() BGChartFull.notifyDataSetChanged() } } func updateBGCheckGraph() { var dataIndex = 7 BGChart.lineData?.dataSets[dataIndex].clear() BGChartFull.lineData?.dataSets[dataIndex].clear() for i in 0 ..< bgCheckData.count { let formatter = NumberFormatter() formatter.minimumFractionDigits = 0 formatter.maximumFractionDigits = 2 formatter.minimumIntegerDigits = 1 // skip if outside of visible area let graphHours = 24 * Storage.shared.downloadDays.value if bgCheckData[i].date < dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) { continue } 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)) BGChart.data?.dataSets[dataIndex].addEntry(value) if Storage.shared.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].addEntry(value) } } BGChart.data?.dataSets[dataIndex].notifyDataSetChanged() BGChart.data?.notifyDataChanged() BGChart.notifyDataSetChanged() if Storage.shared.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged() BGChartFull.data?.notifyDataChanged() BGChartFull.notifyDataSetChanged() } } func updateSuspendGraph() { var dataIndex = 8 BGChart.lineData?.dataSets[dataIndex].clear() BGChartFull.lineData?.dataSets[dataIndex].clear() let thisData = suspendGraphData for i in 0 ..< thisData.count { // skip if outside of visible area let graphHours = 24 * Storage.shared.downloadDays.value if thisData[i].date < dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) { continue } let value = ChartDataEntry(x: Double(thisData[i].date), y: Double(thisData[i].sgv), data: formatPillText(line1: "Suspend Pump", time: thisData[i].date)) BGChart.data?.dataSets[dataIndex].addEntry(value) if Storage.shared.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].addEntry(value) } } BGChart.data?.dataSets[dataIndex].notifyDataSetChanged() BGChart.data?.notifyDataChanged() BGChart.notifyDataSetChanged() if Storage.shared.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged() BGChartFull.data?.notifyDataChanged() BGChartFull.notifyDataSetChanged() } } func updateResumeGraph() { var dataIndex = 9 BGChart.lineData?.dataSets[dataIndex].clear() BGChartFull.lineData?.dataSets[dataIndex].clear() let thisData = resumeGraphData for i in 0 ..< thisData.count { // skip if outside of visible area let graphHours = 24 * Storage.shared.downloadDays.value if thisData[i].date < dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) { continue } let value = ChartDataEntry(x: Double(thisData[i].date), y: Double(thisData[i].sgv), data: formatPillText(line1: "Resume Pump", time: thisData[i].date)) BGChart.data?.dataSets[dataIndex].addEntry(value) if Storage.shared.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].addEntry(value) } } BGChart.data?.dataSets[dataIndex].notifyDataSetChanged() BGChart.data?.notifyDataChanged() BGChart.notifyDataSetChanged() if Storage.shared.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged() BGChartFull.data?.notifyDataChanged() BGChartFull.notifyDataSetChanged() } } func updateSensorStart() { var dataIndex = 10 BGChart.lineData?.dataSets[dataIndex].clear() BGChartFull.lineData?.dataSets[dataIndex].clear() let thisData = sensorStartGraphData for i in 0 ..< thisData.count { // skip if outside of visible area let graphHours = 24 * Storage.shared.downloadDays.value if thisData[i].date < dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) { continue } let value = ChartDataEntry(x: Double(thisData[i].date), y: Double(thisData[i].sgv), data: formatPillText(line1: "Start Sensor", time: thisData[i].date)) BGChart.data?.dataSets[dataIndex].addEntry(value) if Storage.shared.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].addEntry(value) } } BGChart.data?.dataSets[dataIndex].notifyDataSetChanged() BGChart.data?.notifyDataChanged() BGChart.notifyDataSetChanged() if Storage.shared.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged() BGChartFull.data?.notifyDataChanged() BGChartFull.notifyDataSetChanged() } } func updateNotes() { var dataIndex = 11 BGChart.lineData?.dataSets[dataIndex].clear() BGChartFull.lineData?.dataSets[dataIndex].clear() let thisData = noteGraphData for i in 0 ..< thisData.count { // skip if outside of visible area let graphHours = 24 * Storage.shared.downloadDays.value if thisData[i].date < dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) { continue } let value = ChartDataEntry(x: Double(thisData[i].date), y: Double(thisData[i].sgv), data: formatPillText(line1: thisData[i].note, time: thisData[i].date)) BGChart.data?.dataSets[dataIndex].addEntry(value) if Storage.shared.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].addEntry(value) } } BGChart.data?.dataSets[dataIndex].notifyDataSetChanged() BGChart.data?.notifyDataChanged() BGChart.notifyDataSetChanged() if Storage.shared.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged() BGChartFull.data?.notifyDataChanged() BGChartFull.notifyDataSetChanged() } } func createSmallBGGraph() { let entries = bgData var bgChartEntry = [ChartDataEntry]() var colors = [NSUIColor]() var maxBG = Storage.shared.minBGScale.value let lineBG = LineChartDataSet(entries: bgChartEntry, label: "") lineBG.drawCirclesEnabled = false // line2.setDrawHighlightIndicators(false) lineBG.highlightEnabled = true lineBG.drawHorizontalHighlightIndicatorEnabled = false lineBG.drawVerticalHighlightIndicatorEnabled = false lineBG.highlightColor = NSUIColor.label lineBG.drawValuesEnabled = false lineBG.lineWidth = 1.5 lineBG.axisDependency = YAxis.AxisDependency.right // Setup Prediction line details var predictionChartEntry = [ChartDataEntry]() let linePrediction = LineChartDataSet(entries: predictionChartEntry, label: "") linePrediction.drawCirclesEnabled = false // line2.setDrawHighlightIndicators(false) linePrediction.setColor(NSUIColor.systemPurple) linePrediction.highlightEnabled = true linePrediction.drawHorizontalHighlightIndicatorEnabled = false linePrediction.drawVerticalHighlightIndicatorEnabled = false linePrediction.highlightColor = NSUIColor.label linePrediction.drawValuesEnabled = false linePrediction.lineWidth = 1.5 linePrediction.axisDependency = YAxis.AxisDependency.right // create Basal graph data var chartEntry = [ChartDataEntry]() var maxBasal = Storage.shared.minBasalScale.value let lineBasal = LineChartDataSet(entries: chartEntry, label: "") lineBasal.setDrawHighlightIndicators(false) lineBasal.setColor(NSUIColor.systemBlue, alpha: 0.5) lineBasal.lineWidth = 0 lineBasal.drawFilledEnabled = true lineBasal.fillColor = NSUIColor.systemBlue lineBasal.fillAlpha = 0.35 lineBasal.drawCirclesEnabled = false lineBasal.axisDependency = YAxis.AxisDependency.left lineBasal.highlightEnabled = false lineBasal.drawValuesEnabled = false lineBasal.fillFormatter = basalFillFormatter() // Boluses var chartEntryBolus = [ChartDataEntry]() let lineBolus = LineChartDataSet(entries: chartEntryBolus, label: "") lineBolus.circleRadius = 2 lineBolus.circleColors = [NSUIColor.systemBlue.withAlphaComponent(0.75)] lineBolus.drawCircleHoleEnabled = false lineBolus.setDrawHighlightIndicators(false) lineBolus.setColor(NSUIColor.systemBlue, alpha: 1.0) lineBolus.lineWidth = 0 lineBolus.axisDependency = YAxis.AxisDependency.right lineBolus.valueFormatter = ChartYDataValueFormatter() lineBolus.valueTextColor = NSUIColor.label lineBolus.fillColor = NSUIColor.systemBlue lineBolus.fillAlpha = 0.6 lineBolus.drawCirclesEnabled = true lineBolus.drawFilledEnabled = false lineBolus.drawValuesEnabled = false lineBolus.highlightEnabled = false // Carbs var chartEntryCarbs = [ChartDataEntry]() let lineCarbs = LineChartDataSet(entries: chartEntryCarbs, label: "") lineCarbs.circleRadius = 2 lineCarbs.circleColors = [NSUIColor.systemOrange.withAlphaComponent(0.75)] lineCarbs.drawCircleHoleEnabled = false lineCarbs.setDrawHighlightIndicators(false) lineCarbs.setColor(NSUIColor.systemBlue, alpha: 1.0) lineCarbs.lineWidth = 0 lineCarbs.axisDependency = YAxis.AxisDependency.right lineCarbs.valueFormatter = ChartYDataValueFormatter() lineCarbs.valueTextColor = NSUIColor.label lineCarbs.fillColor = NSUIColor.systemOrange lineCarbs.fillAlpha = 0.6 lineCarbs.drawCirclesEnabled = true lineCarbs.drawFilledEnabled = false lineCarbs.drawValuesEnabled = false lineCarbs.highlightEnabled = false // create Scheduled Basal graph data var chartBasalScheduledEntry = [ChartDataEntry]() let lineBasalScheduled = LineChartDataSet(entries: chartBasalScheduledEntry, label: "") lineBasalScheduled.setDrawHighlightIndicators(false) lineBasalScheduled.setColor(NSUIColor.systemBlue, alpha: 0.8) lineBasalScheduled.lineWidth = 0.5 lineBasalScheduled.drawFilledEnabled = false lineBasalScheduled.drawCirclesEnabled = false lineBasalScheduled.axisDependency = YAxis.AxisDependency.left lineBasalScheduled.highlightEnabled = false lineBasalScheduled.drawValuesEnabled = false lineBasalScheduled.lineDashLengths = [2, 1] // create Override graph data var chartOverrideEntry = [ChartDataEntry]() let lineOverride = LineChartDataSet(entries: chartOverrideEntry, label: "") lineOverride.setDrawHighlightIndicators(false) lineOverride.lineWidth = 0 lineOverride.drawFilledEnabled = true lineOverride.fillFormatter = OverrideFillFormatter() lineOverride.fillColor = TreatmentGraphColors.override lineOverride.fillAlpha = 0.6 lineOverride.drawCirclesEnabled = false lineOverride.axisDependency = YAxis.AxisDependency.right lineOverride.highlightEnabled = true lineOverride.drawValuesEnabled = false // BG Check var chartEntryBGCheck = [ChartDataEntry]() let lineBGCheck = LineChartDataSet(entries: chartEntryBGCheck, label: "") lineBGCheck.circleRadius = 2 lineBGCheck.circleColors = [NSUIColor.systemRed.withAlphaComponent(0.75)] lineBGCheck.drawCircleHoleEnabled = false lineBGCheck.setDrawHighlightIndicators(false) lineBGCheck.setColor(NSUIColor.systemRed, alpha: 1.0) lineBGCheck.drawCirclesEnabled = true lineBGCheck.lineWidth = 0 lineBGCheck.highlightEnabled = false lineBGCheck.axisDependency = YAxis.AxisDependency.right lineBGCheck.valueFormatter = ChartYDataValueFormatter() lineBGCheck.drawValuesEnabled = false // Suspend Pump var chartEntrySuspend = [ChartDataEntry]() let lineSuspend = LineChartDataSet(entries: chartEntrySuspend, label: "") lineSuspend.circleRadius = 2 lineSuspend.circleColors = [NSUIColor.systemTeal.withAlphaComponent(0.75)] lineSuspend.drawCircleHoleEnabled = false lineSuspend.setDrawHighlightIndicators(false) lineSuspend.setColor(NSUIColor.systemGray2, alpha: 1.0) lineSuspend.drawCirclesEnabled = true lineSuspend.lineWidth = 0 lineSuspend.highlightEnabled = false lineSuspend.axisDependency = YAxis.AxisDependency.right lineSuspend.valueFormatter = ChartYDataValueFormatter() lineSuspend.drawValuesEnabled = false // Resume Pump var chartEntryResume = [ChartDataEntry]() let lineResume = LineChartDataSet(entries: chartEntryResume, label: "") lineResume.circleRadius = 2 lineResume.circleColors = [NSUIColor.systemTeal.withAlphaComponent(0.75)] lineResume.drawCircleHoleEnabled = false lineResume.setDrawHighlightIndicators(false) lineResume.setColor(NSUIColor.systemGray4, alpha: 1.0) lineResume.drawCirclesEnabled = true lineResume.lineWidth = 0 lineResume.highlightEnabled = false lineResume.axisDependency = YAxis.AxisDependency.right lineResume.valueFormatter = ChartYDataValueFormatter() lineResume.drawValuesEnabled = false // Sensor Start var chartEntrySensor = [ChartDataEntry]() let lineSensor = LineChartDataSet(entries: chartEntrySensor, label: "") lineSensor.circleRadius = 2 lineSensor.circleColors = [NSUIColor.systemIndigo.withAlphaComponent(0.75)] lineSensor.drawCircleHoleEnabled = false lineSensor.setDrawHighlightIndicators(false) lineSensor.setColor(NSUIColor.systemGray3, alpha: 1.0) lineSensor.drawCirclesEnabled = true lineSensor.lineWidth = 0 lineSensor.highlightEnabled = false lineSensor.axisDependency = YAxis.AxisDependency.right lineSensor.valueFormatter = ChartYDataValueFormatter() lineSensor.drawValuesEnabled = false // Notes var chartEntryNote = [ChartDataEntry]() let lineNote = LineChartDataSet(entries: chartEntryNote, label: "") lineNote.circleRadius = 2 lineNote.circleColors = [NSUIColor.systemGray.withAlphaComponent(0.75)] lineNote.drawCircleHoleEnabled = false lineNote.setDrawHighlightIndicators(false) lineNote.setColor(NSUIColor.systemGray3, alpha: 1.0) lineNote.drawCirclesEnabled = true lineNote.lineWidth = 0 lineNote.highlightEnabled = false lineNote.axisDependency = YAxis.AxisDependency.right lineNote.valueFormatter = ChartYDataValueFormatter() lineNote.drawValuesEnabled = false // Setup COB Prediction line details var COBpredictionChartEntry = [ChartDataEntry]() let COBlinePrediction = LineChartDataSet(entries: COBpredictionChartEntry, label: "") COBlinePrediction.drawCirclesEnabled = false COBlinePrediction.setColor(NSUIColor.systemPurple) COBlinePrediction.highlightEnabled = true COBlinePrediction.drawHorizontalHighlightIndicatorEnabled = false COBlinePrediction.drawVerticalHighlightIndicatorEnabled = false COBlinePrediction.highlightColor = NSUIColor.label COBlinePrediction.drawValuesEnabled = false COBlinePrediction.lineWidth = 1.5 COBlinePrediction.axisDependency = YAxis.AxisDependency.right // Setup IOB Prediction line details var IOBpredictionChartEntry = [ChartDataEntry]() let IOBlinePrediction = LineChartDataSet(entries: IOBpredictionChartEntry, label: "") IOBlinePrediction.drawCirclesEnabled = false IOBlinePrediction.setColor(NSUIColor.systemPurple) IOBlinePrediction.highlightEnabled = true IOBlinePrediction.drawHorizontalHighlightIndicatorEnabled = false IOBlinePrediction.drawVerticalHighlightIndicatorEnabled = false IOBlinePrediction.highlightColor = NSUIColor.label IOBlinePrediction.drawValuesEnabled = false IOBlinePrediction.lineWidth = 1.5 IOBlinePrediction.axisDependency = YAxis.AxisDependency.right // Setup UAM Prediction line details var UAMpredictionChartEntry = [ChartDataEntry]() let UAMlinePrediction = LineChartDataSet(entries: UAMpredictionChartEntry, label: "") UAMlinePrediction.drawCirclesEnabled = false UAMlinePrediction.setColor(NSUIColor.systemPurple) UAMlinePrediction.highlightEnabled = true UAMlinePrediction.drawHorizontalHighlightIndicatorEnabled = false UAMlinePrediction.drawVerticalHighlightIndicatorEnabled = false UAMlinePrediction.highlightColor = NSUIColor.label UAMlinePrediction.drawValuesEnabled = false UAMlinePrediction.lineWidth = 1.5 UAMlinePrediction.axisDependency = YAxis.AxisDependency.right // Setup ZT Prediction line details var ZTpredictionChartEntry = [ChartDataEntry]() let ZTlinePrediction = LineChartDataSet(entries: ZTpredictionChartEntry, label: "") ZTlinePrediction.drawCirclesEnabled = false ZTlinePrediction.setColor(NSUIColor.systemPurple) ZTlinePrediction.highlightEnabled = true ZTlinePrediction.drawHorizontalHighlightIndicatorEnabled = false ZTlinePrediction.drawVerticalHighlightIndicatorEnabled = false ZTlinePrediction.highlightColor = NSUIColor.label ZTlinePrediction.drawValuesEnabled = false ZTlinePrediction.lineWidth = 1.5 ZTlinePrediction.axisDependency = YAxis.AxisDependency.right // SMB var chartEntrySmb = [ChartDataEntry]() let lineSmb = LineChartDataSet(entries: chartEntrySmb, label: "") lineSmb.circleRadius = 2 lineSmb.circleColors = [NSUIColor.systemBlue.withAlphaComponent(0.75)] lineSmb.drawCircleHoleEnabled = false lineSmb.setDrawHighlightIndicators(false) lineSmb.setColor(NSUIColor.systemBlue, alpha: 1.0) lineSmb.lineWidth = 0 lineSmb.axisDependency = YAxis.AxisDependency.right lineSmb.valueFormatter = ChartYDataValueFormatter() lineSmb.valueTextColor = NSUIColor.label lineSmb.fillColor = NSUIColor.systemBlue lineSmb.fillAlpha = 0.6 lineSmb.drawCirclesEnabled = true lineSmb.drawFilledEnabled = false lineSmb.drawValuesEnabled = false lineSmb.highlightEnabled = false // Temp Target graph data let chartTempTargetEntry = [ChartDataEntry]() let lineTempTarget = LineChartDataSet(entries: chartTempTargetEntry, label: "") lineTempTarget.setDrawHighlightIndicators(false) lineTempTarget.lineWidth = 0 lineTempTarget.drawFilledEnabled = false lineTempTarget.fillColor = NSUIColor.systemPurple lineTempTarget.fillAlpha = 0.6 lineTempTarget.drawCirclesEnabled = false lineTempTarget.axisDependency = YAxis.AxisDependency.right lineTempTarget.highlightEnabled = true lineTempTarget.drawValuesEnabled = false // Setup the chart data of all lines let data = LineChartData() data.append(lineBG) // Dataset 0 data.append(linePrediction) // Dataset 1 data.append(lineBasal) // Dataset 2 data.append(lineBolus) // Dataset 3 data.append(lineCarbs) // Dataset 4 data.append(lineBasalScheduled) // Dataset 5 data.append(lineOverride) // Dataset 6 data.append(lineBGCheck) // Dataset 7 data.append(lineSuspend) // Dataset 8 data.append(lineResume) // Dataset 9 data.append(lineSensor) // Dataset 10 data.append(lineNote) // Dataset 11 data.append(ZTlinePrediction) // Dataset 12 data.append(IOBlinePrediction) // Dataset 13 data.append(COBlinePrediction) // Dataset 14 data.append(UAMlinePrediction) // Dataset 15 data.append(lineSmb) // Dataset 16 data.append(lineTempTarget) // Dataset 17 // Dataset 18: Prediction Cone placeholder (not rendered on small chart) let lineConeSmall = LineChartDataSet(entries: [ChartDataEntry](), label: "") lineConeSmall.lineWidth = 0 lineConeSmall.drawCirclesEnabled = false lineConeSmall.drawValuesEnabled = false lineConeSmall.highlightEnabled = false lineConeSmall.axisDependency = YAxis.AxisDependency.right data.append(lineConeSmall) BGChartFull.highlightPerDragEnabled = true BGChartFull.leftAxis.enabled = false BGChartFull.leftAxis.axisMaximum = maxBasal BGChartFull.leftAxis.axisMinimum = 0 BGChartFull.rightAxis.enabled = false BGChartFull.rightAxis.axisMinimum = 0.0 BGChartFull.rightAxis.axisMaximum = Double(maxBG) BGChartFull.xAxis.drawLabelsEnabled = false BGChartFull.xAxis.drawGridLinesEnabled = false BGChartFull.xAxis.drawAxisLineEnabled = false BGChartFull.legend.enabled = false BGChartFull.scaleYEnabled = false BGChartFull.scaleXEnabled = false BGChartFull.drawGridBackgroundEnabled = false BGChartFull.data = data } func updateOverrideGraph() { var dataIndex = 6 var yTop = Double(calculateMaxBgGraphValue() - 5) var yBottom = Double(calculateMaxBgGraphValue() - 25) var chart = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet var smallChart = BGChartFull.lineData!.dataSets[dataIndex] as! LineChartDataSet chart.clear() smallChart.clear() // Refresh the fill color in case the backend (Loop vs Trio) changed. chart.fillColor = TreatmentGraphColors.override smallChart.fillColor = TreatmentGraphColors.override let thisData = overrideGraphData var colors = [NSUIColor]() for i in 0 ..< thisData.count { let thisItem = thisData[i] let multiplier = thisItem.insulNeedsScaleFactor as! Double * 100.0 var labelText = thisItem.reason + "\r\n" labelText += String(Int(thisItem.insulNeedsScaleFactor * 100)) + "% " if thisItem.correctionRange.count == 2 { labelText += String(thisItem.correctionRange[0]) + "-" + String(thisItem.correctionRange[1]) } if thisItem.enteredBy.count > 0 { labelText += "\r\nEntered By: " + thisItem.enteredBy } // Start Dot // Shift dots 30 seconds to create an empty 0 space between consecutive temps let preStartDot = ChartDataEntry(x: Double(thisItem.date), y: yBottom, data: labelText) BGChart.data?.dataSets[dataIndex].addEntry(preStartDot) if Storage.shared.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].addEntry(preStartDot) } let startDot = ChartDataEntry(x: Double(thisItem.date + 1), y: yTop, data: labelText) BGChart.data?.dataSets[dataIndex].addEntry(startDot) if Storage.shared.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].addEntry(startDot) } // End Dot let endDot = ChartDataEntry(x: Double(thisItem.endDate - 2), y: yTop, data: labelText) BGChart.data?.dataSets[dataIndex].addEntry(endDot) if Storage.shared.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].addEntry(endDot) } // Post end dot let postEndDot = ChartDataEntry(x: Double(thisItem.endDate - 1), y: yBottom, data: labelText) BGChart.data?.dataSets[dataIndex].addEntry(postEndDot) if Storage.shared.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].addEntry(postEndDot) } } BGChart.data?.dataSets[dataIndex].notifyDataSetChanged() BGChart.data?.notifyDataChanged() BGChart.notifyDataSetChanged() if Storage.shared.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged() BGChartFull.data?.notifyDataChanged() BGChartFull.notifyDataSetChanged() } } func getChartDataSets(for index: GraphDataIndex) -> (chart: LineChartDataSet?, smallChart: LineChartDataSet?) { guard let chart = BGChart.lineData, index.rawValue < chart.dataSets.count, let smallChartData = BGChartFull.lineData, index.rawValue < smallChartData.dataSets.count else { // print("Warning: Invalid GraphDataIndex \(index.description) or lineData is nil.") return (nil, nil) } let chartDataSet = chart.dataSets[index.rawValue] as? LineChartDataSet let smallChartDataSet = smallChartData.dataSets[index.rawValue] as? LineChartDataSet return (chartDataSet, smallChartDataSet) } func addEntryToCharts(entry: ChartDataEntry, chart: LineChartDataSet, smallChart: LineChartDataSet?) { chart.addEntry(entry) if Storage.shared.smallGraphTreatments.value, let smallChart = smallChart { smallChart.addEntry(entry) } } func updateTempTargetGraph() { let dataIndex = GraphDataIndex.tempTarget.rawValue guard let chartData = BGChart.lineData, chartData.dataSets.count > dataIndex, let mainChartDataSet = chartData.dataSets[dataIndex] as? LineChartDataSet else { print("Error: Could not retrieve temp target datasets.") return } mainChartDataSet.clear() var smallChartDataSet: LineChartDataSet? if Storage.shared.smallGraphTreatments.value, let smallChartData = BGChartFull.lineData, smallChartData.dataSets.count > dataIndex, let smallDataSet = smallChartData.dataSets[dataIndex] as? LineChartDataSet { smallChartDataSet = smallDataSet smallChartDataSet?.clear() } let thisData = tempTargetGraphData for tempTarget in thisData { let xStart = tempTarget.date let xEnd = tempTarget.endDate let yCenter = Double(tempTarget.correctionRange[0]) let yTop = yCenter + 5.0 let yBottom = yCenter - 5.0 let entry = TempTargetChartDataEntry( xStart: xStart, xEnd: xEnd, yTop: yTop, yBottom: yBottom, data: nil ) mainChartDataSet.addEntry(entry) if let smallDataSet = smallChartDataSet { smallDataSet.addEntry(entry) } } BGChart.data?.notifyDataChanged() BGChart.notifyDataSetChanged() if let smallDataSet = smallChartDataSet { let tempTargetRendererSmall = TempTargetRenderer( dataProvider: BGChartFull, animator: BGChartFull.chartAnimator, viewPortHandler: BGChartFull.viewPortHandler, tempTargetDataSetIndex: dataIndex ) BGChartFull.renderer = tempTargetRendererSmall BGChartFull.data?.notifyDataChanged() BGChartFull.notifyDataSetChanged() } } func extractMessage(from logEntry: String) -> String? { // Check if this is a JSON-containing log entry guard let jsonStartIndex = logEntry.range(of: "{\"")?.lowerBound else { return nil } // Extract the error message part (before JSON) let errorMessage = String(logEntry[.. String { let messageToWrap = extractMessage(from: text) ?? text guard maxLineLength > 0 else { return messageToWrap } var result: [String] = [] let lines = messageToWrap.components(separatedBy: .newlines) for line in lines { var currentLine = "" let words = line.components(separatedBy: .whitespaces) for word in words { if word.count > maxLineLength { if !currentLine.isEmpty { result.append(currentLine) currentLine = "" } var wordToSplit = word while !wordToSplit.isEmpty { let splitIndex = wordToSplit.index(wordToSplit.startIndex, offsetBy: min(maxLineLength, wordToSplit.count)) result.append(String(wordToSplit[.. String { let dateFormatter = DateFormatter() if dateTimeUtils.is24Hour() { dateFormatter.setLocalizedDateFormatFromTemplate("HH:mm") } else { dateFormatter.setLocalizedDateFormatFromTemplate("hh:mm") } if Storage.shared.graphTimeZoneEnabled.value, let tz = TimeZone(identifier: Storage.shared.graphTimeZoneIdentifier.value) { dateFormatter.timeZone = tz } let wrappedLine1 = wrapText(line1, maxLineLength: 40) let date = Date(timeIntervalSince1970: time) let formattedDate = dateFormatter.string(from: date) if let line2 = line2 { return wrappedLine1 + "\r\n" + line2 + "\r\n" + formattedDate } else { return wrappedLine1 + "\r\n" + formattedDate } } func updateConeGraph(coneData: [ConeChartDataEntry]) { let dataIndex = GraphDataIndex.predictionCone.rawValue let mainChart = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet mainChart.clear() for entry in coneData { mainChart.addEntry(entry) } BGChart.rightAxis.axisMaximum = Double(calculateMaxBgGraphValue()) updateChartRenderers() } func clearConeGraph() { let dataIndex = GraphDataIndex.predictionCone.rawValue guard let lineData = BGChart.lineData, lineData.dataSets.count > dataIndex else { return } lineData.dataSets[dataIndex].clear() BGChart.data?.dataSets[dataIndex].notifyDataSetChanged() BGChart.data?.notifyDataChanged() BGChart.notifyDataSetChanged() } func updateOpenAPSPredictionDisplay() { guard let predBGs = openAPSPredBGs else { return } // Cone is only for OpenAPS-based systems; Loop always uses lines let displayType: PredictionDisplayType = Storage.shared.device.value == "Loop" ? .lines : Storage.shared.predictionDisplayType.value let toLoad = Int(Storage.shared.predictionToLoad.value * 12) let predictionStart = openAPSPredUpdatedTime ?? Date().timeIntervalSince1970 let predictionTypes: [(type: String, colorName: String, dataIndex: Int)] = [ ("ZT", "ZT", GraphDataIndex.ztPrediction.rawValue), ("IOB", "Insulin", GraphDataIndex.iobPrediction.rawValue), ("COB", "LoopYellow", GraphDataIndex.cobPrediction.rawValue), ("UAM", "UAM", GraphDataIndex.uamPrediction.rawValue), ] topPredictionBG = Storage.shared.minBGScale.value if displayType == .cone { var allArrays = [[Double]]() for (type, _, _) in predictionTypes { if let arr = predBGs[type], !arr.isEmpty { allArrays.append(arr) } } var coneData = [ConeChartDataEntry]() if !allArrays.isEmpty { // Cap at the shortest predBG array length so every cone point uses // the same set of contributing arrays. Matches Trio's ForecastSetup. let coneLength = min(allArrays.map { $0.count }.min()!, toLoad + 1) var t = predictionStart for i in 0 ..< coneLength { var valuesAtIndex = [Double]() for arr in allArrays where i < arr.count { valuesAtIndex.append(arr[i]) } if !valuesAtIndex.isEmpty { var yMin = max(valuesAtIndex.min()!, Double(globalVariables.minDisplayGlucose)) var yMax = min(valuesAtIndex.max()!, Double(globalVariables.maxDisplayGlucose)) // Ensure minimum ±1 mg/dL range so the cone is visible when predictions agree if yMin == yMax { yMin -= 1 yMax += 1 } coneData.append(ConeChartDataEntry(x: t, yMin: yMin, yMax: yMax)) if yMax > topPredictionBG - 20 { topPredictionBG = yMax + 20 } } t += 300 } } updateConeGraph(coneData: coneData) // Clear individual prediction lines for (_, _, dataIndex) in predictionTypes { updatePredictionGraphGeneric(dataIndex: dataIndex, predictionData: [], chartLabel: "", color: .clear) } } else { clearConeGraph() for (type, colorName, dataIndex) in predictionTypes { var predictionData = [ShareGlucoseData]() if let graphdata = predBGs[type] { var t = predictionStart for i in 0 ... toLoad { if i < graphdata.count { let v = graphdata[i] let clamped = min(max(Int(round(v)), globalVariables.minDisplayGlucose), globalVariables.maxDisplayGlucose) predictionData.append(ShareGlucoseData(sgv: clamped, date: t, direction: "flat")) t += 300 } } } let color = UIColor(named: colorName) ?? UIColor.systemPurple updatePredictionGraphGeneric(dataIndex: dataIndex, predictionData: predictionData, chartLabel: type, color: color) } } } func updatePredictionGraphGeneric( dataIndex: Int, predictionData: [ShareGlucoseData], chartLabel: String, color: UIColor ) { let mainChart = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet let smallChart = BGChartFull.lineData!.dataSets[dataIndex] as! LineChartDataSet mainChart.clear() smallChart.clear() var colors = [NSUIColor]() let maxBGOffset: Double = 20 for i in 0 ..< predictionData.count { let predictionVal = Double(predictionData[i].sgv) if predictionVal > topPredictionBG - maxBGOffset { topPredictionBG = predictionVal + maxBGOffset } if i == 0 { if Storage.shared.showDots.value { colors.append(color.withAlphaComponent(0.0)) } else { colors.append(color.withAlphaComponent(1.0)) } } else { colors.append(color) } let value = ChartDataEntry( x: predictionData[i].date, y: predictionVal, data: formatPillText( line1: chartLabel, time: predictionData[i].date, line2: Localizer.toDisplayUnits(String(predictionVal)) ) ) mainChart.addEntry(value) smallChart.addEntry(value) } smallChart.circleColors.removeAll() smallChart.colors.removeAll() mainChart.colors.removeAll() mainChart.circleColors.removeAll() if colors.count > 0 { for color in colors { mainChart.addColor(color) mainChart.circleColors.append(color) smallChart.addColor(color) smallChart.circleColors.append(color) } } BGChart.rightAxis.axisMaximum = Double(calculateMaxBgGraphValue()) BGChart.data?.dataSets[dataIndex].notifyDataSetChanged() BGChart.data?.notifyDataChanged() BGChart.notifyDataSetChanged() BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged() BGChartFull.data?.notifyDataChanged() BGChartFull.notifyDataSetChanged() } }