TIRView.swift 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. // LoopFollow
  2. // TIRView.swift
  3. import SwiftUI
  4. struct TIRView: View {
  5. @ObservedObject var viewModel: TIRViewModel
  6. var body: some View {
  7. Button(action: {
  8. viewModel.toggleTIRMode()
  9. }) {
  10. ZStack(alignment: .topTrailing) {
  11. VStack(alignment: .leading, spacing: 12) {
  12. HStack {
  13. Text(tirTitle)
  14. .font(.caption)
  15. .foregroundColor(.secondary)
  16. Spacer()
  17. if let inRangeValue = viewModel.tirData.first(where: { $0.period == .average })?.inRange {
  18. Text(formatRange(inRangeValue))
  19. .font(.caption)
  20. .foregroundColor(.secondary)
  21. }
  22. }
  23. if !viewModel.tirData.isEmpty {
  24. TIRGraphView(tirData: viewModel.tirData)
  25. .frame(height: 250)
  26. .padding(.top, 10)
  27. .padding(.horizontal, 8)
  28. .padding(.bottom, 4)
  29. .allowsHitTesting(false)
  30. VStack(alignment: .leading, spacing: 8) {
  31. if let average = viewModel.tirData.first(where: { $0.period == .average }) {
  32. Text("Cutoffs in \(UnitSettingsStore.shared.glucoseUnit.rawValue)")
  33. .foregroundColor(.secondary)
  34. TIRLegendItem(
  35. color: .orange,
  36. label: "Very High (\(veryHighCutoffText))",
  37. percentage: average.veryHigh
  38. )
  39. TIRLegendItem(
  40. color: .yellow,
  41. label: "High (\(highCutoffText))",
  42. percentage: average.high
  43. )
  44. TIRLegendItem(
  45. color: .green,
  46. label: "In Range (\(inRangeCutoffText))",
  47. percentage: average.inRange
  48. )
  49. TIRLegendItem(
  50. color: .red.opacity(0.5),
  51. label: "Low (\(lowCutoffText))",
  52. percentage: average.low
  53. )
  54. TIRLegendItem(
  55. color: .red.opacity(0.8),
  56. label: "Very Low (\(veryLowCutoffText))",
  57. percentage: average.veryLow
  58. )
  59. }
  60. }
  61. .font(.caption2)
  62. } else {
  63. Text("No data available")
  64. .font(.caption)
  65. .foregroundColor(.secondary)
  66. .frame(height: 250)
  67. }
  68. }
  69. .frame(maxWidth: .infinity, alignment: .leading)
  70. .padding()
  71. Image(systemName: "chevron.up.chevron.down")
  72. .font(.caption2)
  73. .foregroundColor(.secondary.opacity(0.5))
  74. .padding(8)
  75. }
  76. .background(Color(.systemGray6))
  77. .cornerRadius(12)
  78. }
  79. .buttonStyle(PlainButtonStyle())
  80. }
  81. private var tirTitle: String {
  82. switch UnitSettingsStore.shared.timeInRangeMode {
  83. case .tir:
  84. return "Time in Range"
  85. case .titr:
  86. return "Time in Tight Range"
  87. case .custom:
  88. return "Custom Range"
  89. }
  90. }
  91. private func formatRange(_: Double) -> String {
  92. let thresholds = UnitSettingsStore.shared.effectiveThresholds()
  93. let glucoseUnit = UnitSettingsStore.shared.glucoseUnit
  94. let low = UnitSettingsStore.shared.convertMgdlToDisplay(thresholds.low)
  95. let high = UnitSettingsStore.shared.convertMgdlToDisplay(thresholds.high)
  96. let digits = glucoseUnit.fractionDigits
  97. let fmt = "%.\(digits)f – %.\(digits)f %@"
  98. return String(format: fmt, low, high, glucoseUnit.rawValue)
  99. }
  100. private var lowThreshold: Double {
  101. UnitSettingsStore.shared.convertMgdlToDisplay(UnitSettingsStore.shared.effectiveThresholds().low)
  102. }
  103. private var highThreshold: Double {
  104. UnitSettingsStore.shared.convertMgdlToDisplay(UnitSettingsStore.shared.effectiveThresholds().high)
  105. }
  106. private var veryLowThreshold: Double {
  107. UnitSettingsStore.shared.convertMgdlToDisplay(54.0)
  108. }
  109. private var veryHighThreshold: Double {
  110. UnitSettingsStore.shared.convertMgdlToDisplay(250.0)
  111. }
  112. private var veryLowCutoffText: String {
  113. "< \(formatThreshold(veryLowThreshold))"
  114. }
  115. private var lowCutoffText: String {
  116. "\(formatThreshold(veryLowThreshold))–<\(formatThreshold(lowThreshold))"
  117. }
  118. private var inRangeCutoffText: String {
  119. "\(formatThreshold(lowThreshold))–\(formatThreshold(highThreshold))"
  120. }
  121. private var highCutoffText: String {
  122. ">\(formatThreshold(highThreshold))–\(formatThreshold(veryHighThreshold))"
  123. }
  124. private var veryHighCutoffText: String {
  125. "> \(formatThreshold(veryHighThreshold))"
  126. }
  127. private func formatThreshold(_ value: Double) -> String {
  128. let digits = UnitSettingsStore.shared.glucoseUnit.fractionDigits
  129. return String(format: "%.\(digits)f", value)
  130. }
  131. }
  132. struct TIRLegendItem: View {
  133. let color: Color
  134. let label: String
  135. let percentage: Double
  136. var body: some View {
  137. HStack(spacing: 8) {
  138. Rectangle()
  139. .fill(color)
  140. .frame(width: 16, height: 16)
  141. Text(String(format: "%.1f%%", percentage))
  142. .foregroundColor(.primary)
  143. Text(label)
  144. .foregroundColor(.secondary)
  145. Spacer()
  146. }
  147. }
  148. }