ForeCastChart.swift 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. import Charts
  2. import CoreData
  3. import Foundation
  4. import SwiftUI
  5. struct ForeCastChart: View {
  6. @StateObject var state: Bolus.StateModel
  7. @Environment(\.colorScheme) var colorScheme
  8. @Binding var units: GlucoseUnits
  9. @State private var startMarker = Date(timeIntervalSinceNow: -4 * 60 * 60)
  10. @State private var endMarker = Date(timeIntervalSinceNow: 3 * 60 * 60)
  11. var body: some View {
  12. VStack {
  13. forecastChart
  14. .padding(.vertical, 3)
  15. HStack {
  16. Spacer()
  17. Image(systemName: "arrow.right.circle")
  18. .font(.system(size: 16, weight: .bold))
  19. if let eventualBG = state.simulatedDetermination?.eventualBG {
  20. HStack {
  21. Text("\(eventualBG)")
  22. .font(.footnote)
  23. .foregroundStyle(.primary)
  24. Text("\(units.rawValue)")
  25. .font(.footnote)
  26. .foregroundStyle(.secondary)
  27. }
  28. } else {
  29. Text("---")
  30. .font(.footnote)
  31. .foregroundStyle(.primary)
  32. Text("\(units.rawValue)")
  33. .font(.footnote)
  34. .foregroundStyle(.secondary)
  35. }
  36. }
  37. }
  38. }
  39. private var forecastChart: some View {
  40. Chart {
  41. drawGlucose()
  42. drawCurrentTimeMarker()
  43. drawForecastArea()
  44. }
  45. .chartXAxis { forecastChartXAxis }
  46. .chartXScale(domain: startMarker ... endMarker)
  47. .chartYAxis { forecastChartYAxis }
  48. .chartYScale(domain: units == .mgdL ? 0 ... 300 : 0.asMmolL ... 300.asMmolL)
  49. }
  50. private func drawGlucose() -> some ChartContent {
  51. ForEach(state.glucoseFromPersistence) { item in
  52. if item.glucose > Int(state.highGlucose) {
  53. PointMark(
  54. x: .value("Time", item.date ?? Date(), unit: .second),
  55. y: .value("Value", units == .mgdL ? Decimal(item.glucose) : Decimal(item.glucose).asMmolL)
  56. )
  57. .foregroundStyle(Color.orange.gradient)
  58. .symbolSize(20)
  59. } else if item.glucose < Int(state.lowGlucose) {
  60. PointMark(
  61. x: .value("Time", item.date ?? Date(), unit: .second),
  62. y: .value("Value", units == .mgdL ? Decimal(item.glucose) : Decimal(item.glucose).asMmolL)
  63. )
  64. .foregroundStyle(Color.red.gradient)
  65. .symbolSize(20)
  66. } else {
  67. PointMark(
  68. x: .value("Time", item.date ?? Date(), unit: .second),
  69. y: .value("Value", units == .mgdL ? Decimal(item.glucose) : Decimal(item.glucose).asMmolL)
  70. )
  71. .foregroundStyle(Color.green.gradient)
  72. .symbolSize(20)
  73. }
  74. }
  75. }
  76. private func timeForIndex(_ index: Int32) -> Date {
  77. let currentTime = Date()
  78. let timeInterval = TimeInterval(index * 300)
  79. return currentTime.addingTimeInterval(timeInterval)
  80. }
  81. private func drawForecastArea() -> some ChartContent {
  82. ForEach(0 ..< max(state.minForecast.count, state.maxForecast.count), id: \.self) { index in
  83. if index < state.minForecast.count, index < state.maxForecast.count {
  84. let yMinValue = Decimal(state.minForecast[index]) <= 300 ? Decimal(state.minForecast[index]) : Decimal(300)
  85. let yMaxValue = Decimal(state.maxForecast[index]) <= 300 ? Decimal(state.maxForecast[index]) : Decimal(300)
  86. AreaMark(
  87. x: .value("Time", timeForIndex(Int32(index)) <= endMarker ? timeForIndex(Int32(index)) : endMarker),
  88. yStart: .value("Min Value", units == .mgdL ? yMinValue : yMinValue.asMmolL),
  89. yEnd: .value("Max Value", units == .mgdL ? yMaxValue : yMaxValue.asMmolL)
  90. )
  91. .foregroundStyle(Color.blue.opacity(0.5))
  92. .interpolationMethod(.catmullRom)
  93. }
  94. }
  95. }
  96. private func drawCurrentTimeMarker() -> some ChartContent {
  97. RuleMark(
  98. x: .value(
  99. "",
  100. Date(timeIntervalSince1970: TimeInterval(NSDate().timeIntervalSince1970)),
  101. unit: .second
  102. )
  103. ).lineStyle(.init(lineWidth: 2, dash: [3])).foregroundStyle(Color(.systemGray2))
  104. }
  105. private var forecastChartXAxis: some AxisContent {
  106. AxisMarks(values: .stride(by: .hour, count: 2)) { _ in
  107. AxisGridLine(stroke: .init(lineWidth: 0.5, dash: [2, 3]))
  108. AxisValueLabel(format: .dateTime.hour(.defaultDigits(amPM: .narrow)), anchor: .top)
  109. .font(.footnote)
  110. .foregroundStyle(Color.primary)
  111. }
  112. }
  113. private var forecastChartYAxis: some AxisContent {
  114. AxisMarks(position: .trailing) { _ in
  115. AxisGridLine(stroke: .init(lineWidth: 0.5, dash: [2, 3]))
  116. AxisTick(length: 3, stroke: .init(lineWidth: 3)).foregroundStyle(Color.secondary)
  117. AxisValueLabel().font(.footnote).foregroundStyle(Color.primary)
  118. }
  119. }
  120. }