Skip to content
Open
Show file tree
Hide file tree
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
80 changes: 57 additions & 23 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 @@ -68,26 +100,28 @@ struct ChartView: View {
var body: some View {
ZStack(alignment: Alignment(horizontal: .trailing, vertical: .top)){
Chart {
if let preset = self.preset, predicatedData.count > 0, preset.endDate > Date.now.addingTimeInterval(.hours(-6)) {
if let preset = self.preset, (preset.minValue > 0 || preset.maxValue > 0), 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)
.opacity(item.isOverride ? 0.6 : 0.3)
}

ForEach(glucoseSampleData) { item in
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
10 changes: 10 additions & 0 deletions Loop/Managers/Live Activity/GlucoseActivityAttributes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,16 @@ public struct GlucoseRangeValue: Identifiable, Codable, Hashable {
public let maxValue: Double
public let startDate: Date
public let endDate: Date
public let isOverride: Bool

public init(id: UUID, minValue: Double, maxValue: Double, startDate: Date, endDate: Date, isOverride: Bool = false) {
self.id = id
self.minValue = minValue
self.maxValue = maxValue
self.startDate = startDate
self.endDate = endDate
self.isOverride = isOverride
}
}

public struct BottomRowItem: Codable, Hashable {
Expand Down
49 changes: 46 additions & 3 deletions Loop/Managers/Live Activity/LiveActivityManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -358,15 +358,26 @@ class LiveActivityManager : LiveActivityManagerProxy {
let endDate = min(item.endDate, end)

if let presetContext = presetContext {
let noTargetRange = presetContext.minValue == 0 && presetContext.maxValue == 0
if presetContext.startDate > startDate, presetContext.endDate < endDate {
// A preset is active during this schedule
// Override entirely within this schedule segment
glucoseRanges.append(GlucoseRangeValue(
id: UUID(),
minValue: minValue,
maxValue: maxValue,
startDate: startDate,
endDate: presetContext.startDate
))
if noTargetRange {
glucoseRanges.append(GlucoseRangeValue(
id: UUID(),
minValue: minValue,
maxValue: maxValue,
startDate: presetContext.startDate,
endDate: presetContext.endDate,
isOverride: true
))
}
glucoseRanges.append(GlucoseRangeValue(
id: UUID(),
minValue: minValue,
Expand All @@ -375,7 +386,17 @@ class LiveActivityManager : LiveActivityManagerProxy {
endDate: endDate
))
} else if presetContext.endDate > startDate, presetContext.endDate < endDate {
// Cut off the start of the glucose target
// Override ends within this segment (started before)
if noTargetRange {
glucoseRanges.append(GlucoseRangeValue(
id: UUID(),
minValue: minValue,
maxValue: maxValue,
startDate: startDate,
endDate: presetContext.endDate,
isOverride: true
))
}
glucoseRanges.append(GlucoseRangeValue(
id: UUID(),
minValue: minValue,
Expand All @@ -384,17 +405,39 @@ class LiveActivityManager : LiveActivityManagerProxy {
endDate: endDate
))
} else if presetContext.startDate < endDate, presetContext.startDate > startDate {
// Cut off the end of the glucose target
// Override starts within this segment (ends after)
glucoseRanges.append(GlucoseRangeValue(
id: UUID(),
minValue: minValue,
maxValue: maxValue,
startDate: startDate,
endDate: presetContext.startDate
))
if noTargetRange {
glucoseRanges.append(GlucoseRangeValue(
id: UUID(),
minValue: minValue,
maxValue: maxValue,
startDate: presetContext.startDate,
endDate: endDate,
isOverride: true
))
}
if presetContext.endDate == end {
break
}
} else if presetContext.startDate <= startDate, presetContext.endDate >= endDate {
// Override completely covers this segment
if noTargetRange {
glucoseRanges.append(GlucoseRangeValue(
id: UUID(),
minValue: minValue,
maxValue: maxValue,
startDate: startDate,
endDate: endDate,
isOverride: true
))
}
} else {
// No overlap with target and override
glucoseRanges.append(GlucoseRangeValue(
Expand Down