Explorar el Código

Add smoothing lineMark and min range for cone to forecast chart in treatment view

Deniz Cengiz hace 1 año
padre
commit
086adec43d

+ 2 - 0
FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift

@@ -112,6 +112,7 @@ extension Bolus {
         @Published var maxForecast: [Int] = []
         @Published var maxForecast: [Int] = []
         @Published var minCount: Int = 12 // count of Forecasts drawn in 5 min distances, i.e. 12 means a min of 1 hour
         @Published var minCount: Int = 12 // count of Forecasts drawn in 5 min distances, i.e. 12 means a min of 1 hour
         @Published var displayForecastsAsLines: Bool = false
         @Published var displayForecastsAsLines: Bool = false
+        @Published var smooth: Bool = false
 
 
         let now = Date.now
         let now = Date.now
 
 
@@ -162,6 +163,7 @@ extension Bolus {
             maxProtein = settings.settings.maxProtein
             maxProtein = settings.settings.maxProtein
             skipBolus = settingsManager.settings.skipBolusScreenAfterCarbs
             skipBolus = settingsManager.settings.skipBolusScreenAfterCarbs
             useFPUconversion = settingsManager.settings.useFPUconversion
             useFPUconversion = settingsManager.settings.useFPUconversion
+            smooth = settingsManager.settings.smoothGlucose
 
 
             if waitForSuggestionInitial {
             if waitForSuggestionInitial {
                 Task {
                 Task {

+ 98 - 29
FreeAPS/Sources/Modules/Bolus/View/ForeCastChart.swift

@@ -73,7 +73,7 @@ struct ForeCastChart: View {
             if state.displayForecastsAsLines {
             if state.displayForecastsAsLines {
                 drawForecastLines()
                 drawForecastLines()
             } else {
             } else {
-                drawForecastCone()
+                drawForecastsCone()
             }
             }
         }
         }
         .chartXAxis { forecastChartXAxis }
         .chartXAxis { forecastChartXAxis }
@@ -83,29 +83,75 @@ struct ForeCastChart: View {
         .backport.chartForegroundStyleScale(state: state)
         .backport.chartForegroundStyleScale(state: state)
     }
     }
 
 
+    private var stops: [Gradient.Stop] {
+        let low = Double(state.lowGlucose)
+        let high = Double(state.highGlucose)
+
+        let glucoseValues = state.glucoseFromPersistence
+            .map { units == .mgdL ? Decimal($0.glucose) : Decimal($0.glucose).asMmolL }
+
+        let minimum = glucoseValues.min() ?? 0.0
+        let maximum = glucoseValues.max() ?? 0.0
+
+        // Calculate positions for gradient
+        let lowPosition = (low - Double(truncating: minimum as NSNumber)) /
+            (Double(truncating: maximum as NSNumber) - Double(truncating: minimum as NSNumber))
+        let highPosition = (high - Double(truncating: minimum as NSNumber)) /
+            (Double(truncating: maximum as NSNumber) - Double(truncating: minimum as NSNumber))
+
+        // Ensure positions are in bounds [0, 1]
+        let clampedLowPosition = max(0.0, min(lowPosition, 1.0))
+        let clampedHighPosition = max(0.0, min(highPosition, 1.0))
+
+        // Ensure lowPosition is less than highPosition
+        let sortedPositions = [clampedLowPosition, clampedHighPosition].sorted()
+
+        return [
+            Gradient.Stop(color: .red, location: 0.0),
+            Gradient.Stop(color: .red, location: sortedPositions[0]), // draw red gradient till lowGlucose
+            Gradient.Stop(color: .green, location: sortedPositions[0] + 0.0001), // draw green above lowGlucose till highGlucose
+            Gradient.Stop(color: .green, location: sortedPositions[1]),
+            Gradient.Stop(color: .orange, location: sortedPositions[1] + 0.0001), // draw orange above highGlucose
+            Gradient.Stop(color: .orange, location: 1.0)
+        ]
+    }
+
     private func drawGlucose() -> some ChartContent {
     private func drawGlucose() -> some ChartContent {
         ForEach(state.glucoseFromPersistence) { item in
         ForEach(state.glucoseFromPersistence) { item in
-            if item.glucose > Int(state.highGlucose) {
-                PointMark(
-                    x: .value("Time", item.date ?? Date(), unit: .second),
-                    y: .value("Value", units == .mgdL ? Decimal(item.glucose) : Decimal(item.glucose).asMmolL)
+            let glucoseToDisplay = units == .mgdL ? Decimal(item.glucose) : Decimal(item.glucose).asMmolL
+
+            if state.smooth {
+                LineMark(
+                    x: .value("Time", item.date ?? Date()),
+                    y: .value("Value", glucoseToDisplay)
                 )
                 )
-                .foregroundStyle(Color.orange.gradient)
-                .symbolSize(20)
-            } else if item.glucose < Int(state.lowGlucose) {
-                PointMark(
-                    x: .value("Time", item.date ?? Date(), unit: .second),
-                    y: .value("Value", units == .mgdL ? Decimal(item.glucose) : Decimal(item.glucose).asMmolL)
+                .foregroundStyle(
+                    .linearGradient(stops: stops, startPoint: .bottom, endPoint: .top)
                 )
                 )
-                .foregroundStyle(Color.red.gradient)
-                .symbolSize(20)
+                .symbol(.circle).symbolSize(34)
             } else {
             } else {
-                PointMark(
-                    x: .value("Time", item.date ?? Date(), unit: .second),
-                    y: .value("Value", units == .mgdL ? Decimal(item.glucose) : Decimal(item.glucose).asMmolL)
-                )
-                .foregroundStyle(Color.green.gradient)
-                .symbolSize(20)
+                if item.glucose > Int(state.highGlucose) {
+                    PointMark(
+                        x: .value("Time", item.date ?? Date(), unit: .second),
+                        y: .value("Value", glucoseToDisplay)
+                    )
+                    .foregroundStyle(Color.orange.gradient)
+                    .symbolSize(20)
+                } else if item.glucose < Int(state.lowGlucose) {
+                    PointMark(
+                        x: .value("Time", item.date ?? Date(), unit: .second),
+                        y: .value("Value", glucoseToDisplay)
+                    )
+                    .foregroundStyle(Color.red.gradient)
+                    .symbolSize(20)
+                } else {
+                    PointMark(
+                        x: .value("Time", item.date ?? Date(), unit: .second),
+                        y: .value("Value", glucoseToDisplay)
+                    )
+                    .foregroundStyle(Color.green.gradient)
+                    .symbolSize(20)
+                }
             }
             }
         }
         }
     }
     }
@@ -116,19 +162,42 @@ struct ForeCastChart: View {
         return currentTime.addingTimeInterval(timeInterval)
         return currentTime.addingTimeInterval(timeInterval)
     }
     }
 
 
-    private func drawForecastCone() -> some ChartContent {
+    private func drawForecastsCone() -> some ChartContent {
+        // Draw AreaMark for the forecast bounds
         ForEach(0 ..< max(state.minForecast.count, state.maxForecast.count), id: \.self) { index in
         ForEach(0 ..< max(state.minForecast.count, state.maxForecast.count), id: \.self) { index in
             if index < state.minForecast.count, index < state.maxForecast.count {
             if index < state.minForecast.count, index < state.maxForecast.count {
-                let yMinValue = Decimal(state.minForecast[index]) <= 300 ? Decimal(state.minForecast[index]) : Decimal(300)
-                let yMaxValue = Decimal(state.maxForecast[index]) <= 300 ? Decimal(state.maxForecast[index]) : Decimal(300)
+                let yMinMaxDelta = Decimal(state.minForecast[index] - state.maxForecast[index])
+                let xValue = timeForIndex(Int32(index))
+
+                // if distance between respective min and max is 0, provide a default range
+                if yMinMaxDelta == 0 {
+                    let yMinValue = units == .mgdL ? Decimal(state.minForecast[index] - 1) :
+                        Decimal(state.minForecast[index] - 1)
+                        .asMmolL
+                    let yMaxValue = units == .mgdL ? Decimal(state.minForecast[index] + 1) :
+                        Decimal(state.minForecast[index] + 1)
+                        .asMmolL
+
+                    AreaMark(
+                        x: .value("Time", xValue <= endMarker ? xValue : endMarker),
+                        yStart: .value("Min Value", units == .mgdL ? yMinValue : yMinValue.asMmolL),
+                        yEnd: .value("Max Value", units == .mgdL ? yMaxValue : yMaxValue.asMmolL)
+                    )
+                    .foregroundStyle(Color.blue.opacity(0.5))
+                    .interpolationMethod(.catmullRom)
 
 
-                AreaMark(
-                    x: .value("Time", timeForIndex(Int32(index)) <= endMarker ? timeForIndex(Int32(index)) : endMarker),
-                    yStart: .value("Min Value", units == .mgdL ? yMinValue : yMinValue.asMmolL),
-                    yEnd: .value("Max Value", units == .mgdL ? yMaxValue : yMaxValue.asMmolL)
-                )
-                .foregroundStyle(Color.blue.opacity(0.5))
-                .interpolationMethod(.catmullRom)
+                } else {
+                    let yMinValue = Decimal(state.minForecast[index]) <= 300 ? Decimal(state.minForecast[index]) : Decimal(300)
+                    let yMaxValue = Decimal(state.maxForecast[index]) <= 300 ? Decimal(state.maxForecast[index]) : Decimal(300)
+
+                    AreaMark(
+                        x: .value("Time", timeForIndex(Int32(index)) <= endMarker ? timeForIndex(Int32(index)) : endMarker),
+                        yStart: .value("Min Value", units == .mgdL ? yMinValue : yMinValue.asMmolL),
+                        yEnd: .value("Max Value", units == .mgdL ? yMaxValue : yMaxValue.asMmolL)
+                    )
+                    .foregroundStyle(Color.blue.opacity(0.5))
+                    .interpolationMethod(.catmullRom)
+                }
             }
             }
         }
         }
     }
     }

+ 1 - 0
FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift

@@ -633,6 +633,7 @@ extension MainChartView {
         /// filtering for high and low bounds in settings
         /// filtering for high and low bounds in settings
         ForEach(state.glucoseFromPersistence) { item in
         ForEach(state.glucoseFromPersistence) { item in
             let glucoseToDisplay = units == .mgdL ? Decimal(item.glucose) : Decimal(item.glucose).asMmolL
             let glucoseToDisplay = units == .mgdL ? Decimal(item.glucose) : Decimal(item.glucose).asMmolL
+
             if smooth {
             if smooth {
                 LineMark(x: .value("Time", item.date ?? Date()), y: .value("Value", glucoseToDisplay))
                 LineMark(x: .value("Time", item.date ?? Date()), y: .value("Value", glucoseToDisplay))
                     .foregroundStyle(
                     .foregroundStyle(