Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 55 additions & 21 deletions Loop Widget Extension/Live Activity/ChartView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,21 @@ struct ChartView: View {
private let preset: Preset?
private let yAxisMarks: [Double]
private let colorGradient: LinearGradient


private static let colorInRange = Color.green
private static let colorBelowRange = Color.red
private static let colorAboveRange = Color.orange

// Infer chartable increment from yAxisMarks: mmol/L values are always below 40, mg/dL above 54.
private var chartableIncrement: Double { (yAxisMarks.max() ?? 100) < 40 ? 1.0/25.0 : 1.0 }

// When min == max the rectangle has zero height and is invisible. Mirror the main app's
// doubleRangeWithMinimumIncrement logic by expanding by one chartable increment each side.
private func adjustedRange(min minValue: Double, max maxValue: Double) -> (min: Double, max: Double) {
guard (maxValue - minValue) < .ulpOfOne else { return (minValue, maxValue) }
return (minValue - 3 * chartableIncrement, maxValue + 3 * chartableIncrement)
}

init(glucoseSamples: [GlucoseSampleAttributes], predicatedGlucose: [Double], predicatedStartDate: Date?, predicatedInterval: TimeInterval?, useLimits: Bool, lowerLimit: Double, upperLimit: Double, glucoseRanges: [GlucoseRangeValue], preset: Preset?, yAxisMarks: [Double]) {
self.glucoseSampleData = ChartValues.convert(data: glucoseSamples, useLimits: useLimits, lowerLimit: lowerLimit, upperLimit: upperLimit)
self.predicatedData = ChartValues.convert(
Expand All @@ -29,7 +43,7 @@ struct ChartView: View {
lowerLimit: lowerLimit,
upperLimit: upperLimit
)
self.colorGradient = ChartView.getGradient(useLimits: useLimits, lowerLimit: lowerLimit, upperLimit: upperLimit, highestValue: yAxisMarks.max() ?? 1)
self.colorGradient = ChartView.getGradient(useLimits: useLimits, lowerLimit: lowerLimit, upperLimit: upperLimit, lowestValue: predicatedGlucose.min() ?? 1, highestValue: predicatedGlucose.max() ?? 1)
self.preset = preset
self.glucoseRanges = glucoseRanges
self.yAxisMarks = yAxisMarks
Expand All @@ -41,22 +55,40 @@ struct ChartView: View {
self.preset = preset
self.glucoseRanges = glucoseRanges
self.yAxisMarks = yAxisMarks
self.colorGradient = ChartView.getGradient(useLimits: useLimits, lowerLimit: lowerLimit, upperLimit: upperLimit, highestValue: yAxisMarks.max() ?? 1)
self.colorGradient = LinearGradient(colors: [], startPoint: .bottom, endPoint: .top)
}

private static func getGradient(useLimits: Bool, lowerLimit: Double, upperLimit: Double, highestValue: Double) -> LinearGradient {
private static func getGradient(useLimits: Bool, lowerLimit: Double, upperLimit: Double, lowestValue: Double, highestValue: Double) -> LinearGradient {

var stops: [Gradient.Stop] = [Gradient.Stop(color: Color("glucose"), location: 0)]
if useLimits {
let lowerStop = lowerLimit / highestValue
let upperStop = upperLimit / highestValue
stops = [
Gradient.Stop(color: .red, location: 0),
Gradient.Stop(color: .red, location: lowerStop - 0.01),
Gradient.Stop(color: .green, location: lowerStop),
Gradient.Stop(color: .green, location: upperStop),
Gradient.Stop(color: .orange, location: upperStop + 0.01),
Gradient.Stop(color: .orange, location: 600), // Just use the mg/dl limit for the most upper value
]
// For applying a color gradient to line data, the range of the plotted
// data maps to the space 0 to 1 for setting gradient stops, so normalize:
// Normalize the transition points to 0-1 space of the plotted range:
let lowerStop = (lowerLimit - lowestValue) / (highestValue - lowestValue)
let upperStop = (upperLimit - lowestValue) / (highestValue - lowestValue)
// Build up a set of stops, only using those in the 0-1 range:
stops = []
var stopColor: Color
// Get the color for glucose at the minimum of the line:
if lowestValue < lowerLimit {
stopColor = colorBelowRange
} else if lowestValue < upperLimit {
stopColor = colorInRange
} else {
stopColor = colorAboveRange
}
stops.append(Gradient.Stop(color: stopColor, location: 0))
// Add the transition stops if they are in the visible range:
if lowerStop > 0, lowerStop < 1 {
stops.append(Gradient.Stop(color: colorBelowRange, location: lowerStop))
stops.append(Gradient.Stop(color: colorInRange, location: lowerStop + 0.01))
}
if upperStop > 0, upperStop < 1 {
stops.append(Gradient.Stop(color: colorInRange, location: upperStop))
stops.append(Gradient.Stop(color: colorAboveRange, location: upperStop + 0.01))
}

}
return LinearGradient(
gradient: Gradient(stops: stops),
Expand All @@ -69,22 +101,24 @@ struct ChartView: View {
ZStack(alignment: Alignment(horizontal: .trailing, vertical: .top)){
Chart {
if let preset = self.preset, predicatedData.count > 0, preset.endDate > Date.now.addingTimeInterval(.hours(-6)) {
let (presetMin, presetMax) = adjustedRange(min: preset.minValue, max: preset.maxValue)
RectangleMark(
xStart: .value("Start", preset.startDate),
xEnd: .value("End", preset.endDate),
yStart: .value("Preset override", preset.minValue),
yEnd: .value("Preset override", preset.maxValue)
yStart: .value("Preset override", presetMin),
yEnd: .value("Preset override", presetMax)
)
.foregroundStyle(.primary)
.opacity(0.6)
}

ForEach(glucoseRanges) { item in
let (rangeMin, rangeMax) = adjustedRange(min: item.minValue, max: item.maxValue)
RectangleMark(
xStart: .value("Start", item.startDate),
xEnd: .value("End", item.endDate),
yStart: .value("Glucose range", item.minValue),
yEnd: .value("Glucose range", item.maxValue)
yStart: .value("Glucose range", rangeMin),
yEnd: .value("Glucose range", rangeMax)
)
.foregroundStyle(.primary)
.opacity(0.3)
Expand All @@ -107,9 +141,9 @@ struct ChartView: View {
}
}
.chartForegroundStyleScale([
"Good": .green,
"High": .orange,
"Low": .red,
"Good": Self.colorInRange,
"High": Self.colorAboveRange,
"Low": Self.colorBelowRange,
"Default": Color("glucose")
])
.chartPlotStyle { plotContent in
Expand Down