TIRView.swift 6.1 KB

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