GRIView.swift 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. // LoopFollow
  2. // GRIView.swift
  3. import SwiftUI
  4. struct GRIView: View {
  5. @ObservedObject var viewModel: GRIViewModel
  6. private let legendColumns = [GridItem(.adaptive(minimum: 90), spacing: 12, alignment: .leading)]
  7. private let yAxisLabelInset: CGFloat = 24
  8. var body: some View {
  9. VStack(alignment: .leading, spacing: 8) {
  10. HStack {
  11. Text("GRI (Glucose Risk Index)")
  12. .font(.caption)
  13. .foregroundColor(.secondary)
  14. Spacer()
  15. if let gri = viewModel.gri {
  16. VStack(alignment: .trailing, spacing: 2) {
  17. Text(String(format: "%.0f", gri))
  18. .font(.title2)
  19. .fontWeight(.semibold)
  20. .foregroundColor(griColor(gri))
  21. Text(griZone(gri))
  22. .font(.caption2)
  23. .foregroundColor(.secondary)
  24. }
  25. } else {
  26. Text("---")
  27. .font(.title2)
  28. .fontWeight(.semibold)
  29. .foregroundColor(.purple)
  30. }
  31. }
  32. if let hypo = viewModel.griHypoComponent, let hyper = viewModel.griHyperComponent {
  33. VStack(alignment: .leading, spacing: 6) {
  34. ZStack(alignment: .leading) {
  35. GRIRiskGridView(
  36. hypoComponent: hypo,
  37. hyperComponent: hyper,
  38. gri: viewModel.gri ?? 0
  39. )
  40. .frame(height: 250)
  41. .padding(.top, 10)
  42. .padding(.leading, yAxisLabelInset)
  43. .padding(.trailing, 8)
  44. .padding(.bottom, 4)
  45. .allowsHitTesting(false)
  46. Text("Hyperglycemia Component (%)")
  47. .font(.caption2)
  48. .foregroundColor(.secondary)
  49. .rotationEffect(.degrees(-90))
  50. .fixedSize()
  51. .frame(width: yAxisLabelInset)
  52. }
  53. HStack(spacing: 0) {
  54. Spacer()
  55. .frame(width: yAxisLabelInset)
  56. Text("Hypoglycemia Component (%)")
  57. .font(.caption2)
  58. .foregroundColor(.secondary)
  59. .frame(maxWidth: .infinity, alignment: .center)
  60. }
  61. }
  62. LazyVGrid(columns: legendColumns, alignment: .leading, spacing: 8) {
  63. ZoneLegendItem(color: .green, label: "A (0-20)")
  64. ZoneLegendItem(color: .yellow, label: "B (21-40)")
  65. ZoneLegendItem(color: .orange, label: "C (41-60)")
  66. ZoneLegendItem(color: .red, label: "D (61-80)")
  67. ZoneLegendItem(color: .red.opacity(0.8), label: "E (81-100)")
  68. }
  69. .font(.caption2)
  70. } else {
  71. Text("No data available")
  72. .font(.caption)
  73. .foregroundColor(.secondary)
  74. .frame(height: 250)
  75. }
  76. }
  77. .frame(maxWidth: .infinity, alignment: .leading)
  78. .padding()
  79. .background(Color(.systemGray6))
  80. .cornerRadius(12)
  81. }
  82. private func griColor(_ gri: Double) -> Color {
  83. if gri <= 20 {
  84. return .green
  85. } else if gri <= 40 {
  86. return .yellow
  87. } else if gri <= 60 {
  88. return .orange
  89. } else if gri <= 80 {
  90. return .red
  91. } else {
  92. return .red.opacity(0.8)
  93. }
  94. }
  95. private func griZone(_ gri: Double) -> String {
  96. if gri <= 20 {
  97. return "Zone A"
  98. } else if gri <= 40 {
  99. return "Zone B"
  100. } else if gri <= 60 {
  101. return "Zone C"
  102. } else if gri <= 80 {
  103. return "Zone D"
  104. } else {
  105. return "Zone E"
  106. }
  107. }
  108. }
  109. struct ZoneLegendItem: View {
  110. let color: Color
  111. let label: String
  112. var body: some View {
  113. HStack(spacing: 4) {
  114. Rectangle()
  115. .fill(color.opacity(0.3))
  116. .frame(width: 12, height: 12)
  117. Text(label)
  118. .foregroundColor(.secondary)
  119. .lineLimit(1)
  120. }
  121. }
  122. }