Переглянути джерело

Rework section chart popover and percentile popover WIP

Deniz Cengiz 1 рік тому
батько
коміт
33ff7bc9bb

+ 1 - 0
Trio/Sources/Localizations/Main/Localizable.xcstrings

@@ -127611,6 +127611,7 @@
       }
       }
     },
     },
     "Readings / 24 h" : {
     "Readings / 24 h" : {
+      "extractionState" : "stale",
       "localizations" : {
       "localizations" : {
         "ar" : {
         "ar" : {
           "stringUnit" : {
           "stringUnit" : {

+ 5 - 5
Trio/Sources/Modules/Stat/View/ViewElements/BareStatisticsView.swift

@@ -54,7 +54,7 @@ struct BareStatisticsView {
                         useUnit == .mmolL ? glucoseStats.ifcc
                         useUnit == .mmolL ? glucoseStats.ifcc
                             .formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) : glucoseStats.ngsp
                             .formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) : glucoseStats.ngsp
                             .formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))
                             .formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))
-                            + " %"
+                            + "%"
                     )
                     )
                     VStack(spacing: 5) {
                     VStack(spacing: 5) {
                         Text("ebA1c").font(.subheadline).foregroundColor(.secondary)
                         Text("ebA1c").font(.subheadline).foregroundColor(.secondary)
@@ -62,7 +62,7 @@ struct BareStatisticsView {
                     }
                     }
                     VStack(spacing: 5) {
                     VStack(spacing: 5) {
                         Text("GMI").font(.subheadline).foregroundColor(.secondary)
                         Text("GMI").font(.subheadline).foregroundColor(.secondary)
-                        Text(glucoseStats.gmi.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + " %")
+                        Text(glucoseStats.gmi.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + "%")
                     }
                     }
                     VStack(spacing: 5) {
                     VStack(spacing: 5) {
                         Text("SD").font(.subheadline).foregroundColor(.secondary)
                         Text("SD").font(.subheadline).foregroundColor(.secondary)
@@ -191,7 +191,7 @@ struct BareStatisticsView {
                 let numberOfDays = (current - previous).timeInterval / 8.64E4
                 let numberOfDays = (current - previous).timeInterval / 8.64E4
 
 
                 VStack(spacing: 5) {
                 VStack(spacing: 5) {
-                    Text(numberOfDays < 1 ? "Readings" : "Readings / 24 h").font(.subheadline)
+                    Text(numberOfDays < 1 ? "Readings" : "Readings / 24h").font(.subheadline)
                         .foregroundColor(.secondary)
                         .foregroundColor(.secondary)
                     Text(bgs.readings.formatted(.number.grouping(.never).rounded().precision(.fractionLength(0))))
                     Text(bgs.readings.formatted(.number.grouping(.never).rounded().precision(.fractionLength(0))))
                 }
                 }
@@ -312,13 +312,13 @@ struct BareStatisticsView {
                     }
                     }
                     VStack(spacing: 5) {
                     VStack(spacing: 5) {
                         Text("Interval").font(.subheadline).foregroundColor(.primary)
                         Text("Interval").font(.subheadline).foregroundColor(.primary)
-                        Text(intervalAverage.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + " min")
+                        Text(intervalAverage.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + "m")
                     }
                     }
                     VStack(spacing: 5) {
                     VStack(spacing: 5) {
                         Text("Duration").font(.subheadline).foregroundColor(.primary)
                         Text("Duration").font(.subheadline).foregroundColor(.primary)
                         Text(
                         Text(
                             (medianDuration / 1000)
                             (medianDuration / 1000)
-                                .formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + " s"
+                                .formatted(.number.grouping(.never).rounded().precision(.fractionLength(1))) + "s"
                         )
                         )
                     }
                     }
                     VStack(spacing: 5) {
                     VStack(spacing: 5) {

+ 8 - 3
Trio/Sources/Modules/Stat/View/ViewElements/GlucosePercentileChart.swift

@@ -119,7 +119,6 @@ struct GlucosePercentileChart: View {
             }
             }
             .chartXSelection(value: $selection.animation(.easeInOut))
             .chartXSelection(value: $selection.animation(.easeInOut))
             .frame(height: 200)
             .frame(height: 200)
-
             legend
             legend
         }
         }
     }
     }
@@ -189,6 +188,8 @@ struct AGPSelectionPopover: View {
     let time: Date
     let time: Date
     let units: GlucoseUnits
     let units: GlucoseUnits
 
 
+    @Environment(\.colorScheme) var colorScheme
+
     private var timeText: String {
     private var timeText: String {
         if let hour = Calendar.current.dateComponents([.hour], from: time).hour {
         if let hour = Calendar.current.dateComponents([.hour], from: time).hour {
             return "\(hour):00-\(hour + 1):00"
             return "\(hour):00-\(hour + 1):00"
@@ -240,11 +241,15 @@ struct AGPSelectionPopover: View {
             }
             }
             .font(.headline.bold())
             .font(.headline.bold())
         }
         }
-        .foregroundStyle(.white)
         .padding(20)
         .padding(20)
         .background {
         .background {
             RoundedRectangle(cornerRadius: 10)
             RoundedRectangle(cornerRadius: 10)
-                .fill(Color.blue)
+                .fill(colorScheme == .dark ? Color.bgDarkBlue.opacity(0.9) : Color.white.opacity(0.95))
+                .shadow(color: Color.secondary, radius: 2)
+                .overlay(
+                    RoundedRectangle(cornerRadius: 4)
+                        .stroke(Color.blue, lineWidth: 2)
+                )
         }
         }
     }
     }
 }
 }

+ 101 - 44
Trio/Sources/Modules/Stat/View/ViewElements/SectorChart.swift

@@ -49,13 +49,13 @@ struct SectorChart: View {
             VStack(alignment: .leading, spacing: 10) {
             VStack(alignment: .leading, spacing: 10) {
                 VStack(alignment: .leading, spacing: 5) {
                 VStack(alignment: .leading, spacing: 5) {
                     Text("70-180").font(.subheadline).foregroundStyle(Color.secondary)
                     Text("70-180").font(.subheadline).foregroundStyle(Color.secondary)
-                    Text(inRangePercentage.formatted(.number.grouping(.never).rounded().precision(.fractionLength(0))) + " %")
+                    Text(inRangePercentage.formatted(.number.grouping(.never).rounded().precision(.fractionLength(0))) + "%")
                         .foregroundStyle(Color.loopGreen)
                         .foregroundStyle(Color.loopGreen)
                 }
                 }
 
 
                 VStack(alignment: .leading, spacing: 5) {
                 VStack(alignment: .leading, spacing: 5) {
                     Text("70-140").font(.subheadline).foregroundStyle(Color.secondary)
                     Text("70-140").font(.subheadline).foregroundStyle(Color.secondary)
-                    Text(tightPercentage.formatted(.number.grouping(.never).rounded().precision(.fractionLength(0))) + " %")
+                    Text(tightPercentage.formatted(.number.grouping(.never).rounded().precision(.fractionLength(0))) + "%")
                         .foregroundStyle(Color.green)
                         .foregroundStyle(Color.green)
                 }
                 }
             }.padding(.leading, 5)
             }.padding(.leading, 5)
@@ -63,13 +63,13 @@ struct SectorChart: View {
             VStack(alignment: .leading, spacing: 10) {
             VStack(alignment: .leading, spacing: 10) {
                 VStack(alignment: .leading, spacing: 5) {
                 VStack(alignment: .leading, spacing: 5) {
                     Text("> 180").font(.subheadline).foregroundStyle(Color.secondary)
                     Text("> 180").font(.subheadline).foregroundStyle(Color.secondary)
-                    Text(highPercentage.formatted(.number.grouping(.never).rounded().precision(.fractionLength(0))) + " %")
+                    Text(highPercentage.formatted(.number.grouping(.never).rounded().precision(.fractionLength(0))) + "%")
                         .foregroundStyle(Color.orange)
                         .foregroundStyle(Color.orange)
                 }
                 }
 
 
                 VStack(alignment: .leading, spacing: 5) {
                 VStack(alignment: .leading, spacing: 5) {
                     Text("< 54").font(.subheadline).foregroundStyle(Color.secondary)
                     Text("< 54").font(.subheadline).foregroundStyle(Color.secondary)
-                    Text(lowPercentage.formatted(.number.grouping(.never).rounded().precision(.fractionLength(0))) + " %")
+                    Text(lowPercentage.formatted(.number.grouping(.never).rounded().precision(.fractionLength(0))) + "%")
                         .foregroundStyle(Color.loopRed)
                         .foregroundStyle(Color.loopRed)
                 }
                 }
             }
             }
@@ -94,26 +94,10 @@ struct SectorChart: View {
                         outerRadius: selectedRange == data.range ? 100 : 80,
                         outerRadius: selectedRange == data.range ? 100 : 80,
                         angularInset: 1.5
                         angularInset: 1.5
                     )
                     )
-//                    .cornerRadius(3)
                     .foregroundStyle(data.color)
                     .foregroundStyle(data.color)
-//                    .annotation(position: .automatic, alignment: .leading, spacing: 0) {
-//                        if data.percentage > 0 {
-//                            Text("\(Int(data.percentage))%")
-//                                .font(.callout)
-//                                .foregroundStyle(.white)
-//                                .fontWeight(.bold)
-//                        }
-//                    }
                 }
                 }
             }
             }
-//            .chartLegend(position: .bottom, spacing: 20)
             .chartAngleSelection(value: $selectedCount)
             .chartAngleSelection(value: $selectedCount)
-//            .chartForegroundStyleScale([
-//                "High": Color.orange,
-//                "In Range": Color.green,
-//                "Low": Color.red
-//            ])
-//            .padding(.vertical)
             .frame(height: 100)
             .frame(height: 100)
         }
         }
         .onChange(of: selectedCount) { _, newValue in
         .onChange(of: selectedCount) { _, newValue in
@@ -132,7 +116,7 @@ struct SectorChart: View {
                 let data = getDetailedData(for: selectedRange)
                 let data = getDetailedData(for: selectedRange)
                 RangeDetailPopover(data: data)
                 RangeDetailPopover(data: data)
                     .transition(.scale.combined(with: .opacity))
                     .transition(.scale.combined(with: .opacity))
-                    .offset(y: -90) // TODO: make this dynamic
+                    .offset(y: -150) // TODO: make this dynamic
             }
             }
         }
         }
     }
     }
@@ -214,12 +198,50 @@ struct SectorChart: View {
             let highLimitTreshold = units == .mmolL ? Decimal(Int(highLimit)).asMmolL : highLimit
             let highLimitTreshold = units == .mmolL ? Decimal(Int(highLimit)).asMmolL : highLimit
             let veryHighThreshold = units == .mmolL ? Decimal(250).asMmolL : 250
             let veryHighThreshold = units == .mmolL ? Decimal(250).asMmolL : 250
 
 
+            let highGlucoseValues = glucose.filter { $0.glucose > Int(highLimit) }
+            let highGlucoseValuesAsInt = highGlucoseValues.compactMap({ each in Int(each.glucose as Int16) })
+            let highGlucoseTotal = highGlucoseValuesAsInt.reduce(0, +)
+
+            let average = Decimal(highGlucoseTotal / highGlucoseValues.count)
+            let median = Decimal(BareStatisticsView.medianCalculation(array: highGlucoseValuesAsInt))
+
+            var sumOfSquares = 0.0
+            highGlucoseValuesAsInt.forEach { value in
+                sumOfSquares += pow(Double(value) - Double(average), 2)
+            }
+
+            var standardDeviation = 0.0
+
+            if average > 0 {
+                standardDeviation = sqrt(sumOfSquares / Double(highGlucoseValues.count))
+            }
+
             return RangeDetail(
             return RangeDetail(
                 title: "High Glucose",
                 title: "High Glucose",
                 color: .orange,
                 color: .orange,
                 items: [
                 items: [
-                    ("Very High (>\(veryHighThreshold) \(units.rawValue))", Decimal(veryHigh) / total * 100),
-                    ("High (\(highLimitTreshold)-\(veryHighThreshold) \(units.rawValue))", Decimal(high) / total * 100)
+                    (
+                        "Very High (>\(veryHighThreshold)):",
+                        formatPercentage(Decimal(veryHigh) / total * 100)
+                    ),
+                    (
+                        "High (\(highLimitTreshold)-\(veryHighThreshold)):",
+                        formatPercentage(Decimal(high) / total * 100)
+                    ),
+                    ("Avergage", units == .mgdL ? average.description : average.formattedAsMmolL),
+                    ("Median", units == .mgdL ? median.description : median.formattedAsMmolL),
+                    (
+                        "SD",
+                        units == .mgdL ? standardDeviation.formatted(
+                            .number.grouping(.never).rounded()
+                                .precision(.fractionLength(0))
+                        ) : standardDeviation.asMmolL.formatted(
+                            .number.grouping(.never).rounded()
+                                .precision(
+                                    .fractionLength(1)
+                                )
+                        )
+                    )
                 ]
                 ]
             )
             )
         case .inRange:
         case .inRange:
@@ -237,8 +259,14 @@ struct SectorChart: View {
                 title: "In Range",
                 title: "In Range",
                 color: .green,
                 color: .green,
                 items: [
                 items: [
-                    ("Tight (\(lowLimitTreshold)-\(tightThresholdTreshold) \(units.rawValue))", Decimal(tight) / total * 100),
-                    ("Normal (\(tightThresholdTreshold)-\(highLimitTreshold) \(units.rawValue))", Decimal(normal) / total * 100)
+                    (
+                        "Tight (\(lowLimitTreshold)-\(tightThresholdTreshold) \(units.rawValue))",
+                        formatPercentage(Decimal(tight) / total * 100)
+                    ),
+                    (
+                        "Normal (\(tightThresholdTreshold)-\(highLimitTreshold) \(units.rawValue))",
+                        formatPercentage(Decimal(normal) / total * 100)
+                    )
                 ]
                 ]
             )
             )
         case .low:
         case .low:
@@ -255,12 +283,25 @@ struct SectorChart: View {
                 title: "Low Glucose",
                 title: "Low Glucose",
                 color: .red,
                 color: .red,
                 items: [
                 items: [
-                    ("Low (\(veryLowThresholdTreshold)-\(lowLimitTreshold) \(units.rawValue))", Decimal(low) / total * 100),
-                    ("Very Low (<\(veryLowThresholdTreshold) \(units.rawValue))", Decimal(veryLow) / total * 100)
+                    (
+                        "Low (\(veryLowThresholdTreshold)-\(lowLimitTreshold) \(units.rawValue))",
+                        formatPercentage(Decimal(low) / total * 100)
+                    ),
+                    (
+                        "Very Low (<\(veryLowThresholdTreshold) \(units.rawValue))",
+                        formatPercentage(Decimal(veryLow) / total * 100)
+                    )
                 ]
                 ]
             )
             )
         }
         }
     }
     }
+
+    func formatPercentage(_ value: Decimal) -> String {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .percent
+        formatter.maximumFractionDigits = 1
+        return formatter.string(from: NSDecimalNumber(decimal: value / 100)) ?? "0%"
+    }
 }
 }
 
 
 /// Represents details about a specific glucose range category including title, color and percentage breakdowns
 /// Represents details about a specific glucose range category including title, color and percentage breakdowns
@@ -270,40 +311,56 @@ private struct RangeDetail {
     /// The color used to represent this range in the UI
     /// The color used to represent this range in the UI
     let color: Color
     let color: Color
     /// Array of tuples containing label and percentage for each sub-range
     /// Array of tuples containing label and percentage for each sub-range
-    let items: [(label: String, percentage: Decimal)]
+    let items: [(label: String, value: String)]
 }
 }
 
 
 /// A popover view that displays detailed breakdown of glucose percentages for a range category
 /// A popover view that displays detailed breakdown of glucose percentages for a range category
 private struct RangeDetailPopover: View {
 private struct RangeDetailPopover: View {
     let data: RangeDetail
     let data: RangeDetail
 
 
+    @Environment(\.colorScheme) var colorScheme
+
     var body: some View {
     var body: some View {
-        VStack(alignment: .leading, spacing: 4) {
+        VStack(alignment: .leading, spacing: 8) {
             Text(data.title)
             Text(data.title)
                 .font(.subheadline)
                 .font(.subheadline)
                 .fontWeight(.bold)
                 .fontWeight(.bold)
+                .foregroundStyle(data.color)
+                .padding(.bottom, 4)
+
+            ForEach(Array(data.items.enumerated()), id: \..offset) { index, item in
+                if index < 2 {
+                    HStack {
+                        Text(item.label)
+                        Text(item.value).bold()
+                    }
+                    .font(.footnote)
+                }
+            }
 
 
-            ForEach(data.items, id: \.label) { item in
-                HStack {
-                    Text(item.label)
-                    Spacer()
-                    Text(formatPercentage(item.percentage))
+            HStack(spacing: 20) {
+                ForEach(Array(data.items.enumerated()), id: \..offset) { index, item in
+                    if index > 1 {
+                        VStack(alignment: .leading, spacing: 5) {
+                            Text(item.label)
+                            HStack {
+                                Text(item.value).bold()
+                            }
+                        }
+                        .font(.footnote)
+                    }
                 }
                 }
-                .font(.footnote)
             }
             }
         }
         }
-        .foregroundStyle(.white)
         .padding(20)
         .padding(20)
         .background {
         .background {
             RoundedRectangle(cornerRadius: 10)
             RoundedRectangle(cornerRadius: 10)
-                .fill(Color.blue)
+                .fill(colorScheme == .dark ? Color.bgDarkBlue.opacity(0.9) : Color.white.opacity(0.95))
+                .shadow(color: Color.secondary, radius: 2)
+                .overlay(
+                    RoundedRectangle(cornerRadius: 4)
+                        .stroke(data.color, lineWidth: 2)
+                )
         }
         }
     }
     }
-
-    private func formatPercentage(_ value: Decimal) -> String {
-        let formatter = NumberFormatter()
-        formatter.numberStyle = .percent
-        formatter.maximumFractionDigits = 1
-        return formatter.string(from: NSDecimalNumber(decimal: value / 100)) ?? "0%"
-    }
 }
 }