Skip to content

Commit

Permalink
Merge pull request nightscout#49 from aug0211/auggie-dynamic-bg-color
Browse files Browse the repository at this point in the history
Add Dynamic BG Color
  • Loading branch information
polscm32 authored Sep 29, 2024
2 parents 4da4074 + 24a7f4c commit d733543
Show file tree
Hide file tree
Showing 25 changed files with 555 additions and 94 deletions.
8 changes: 8 additions & 0 deletions FreeAPS.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,8 @@
DD6B7CBB2C7FBBFA00B75029 /* ReviewInsulinActionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6B7CBA2C7FBBFA00B75029 /* ReviewInsulinActionView.swift */; };
DD6D67E42C9C253500660C9B /* ColorSchemeOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6D67E32C9C253500660C9B /* ColorSchemeOption.swift */; };
DD88C8E22C50420800F2D558 /* DefinitionRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD88C8E12C50420800F2D558 /* DefinitionRow.swift */; };
DD940BAA2CA7585D000830A5 /* GlucoseColorScheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD940BA92CA7585D000830A5 /* GlucoseColorScheme.swift */; };
DD940BAC2CA75889000830A5 /* DynamicGlucoseColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD940BAB2CA75889000830A5 /* DynamicGlucoseColor.swift */; };
DDD163122C4C689900CD525A /* OverrideStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD163112C4C689900CD525A /* OverrideStateModel.swift */; };
DDD163142C4C68D300CD525A /* OverrideProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD163132C4C68D300CD525A /* OverrideProvider.swift */; };
DDD163162C4C690300CD525A /* OverrideDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD163152C4C690300CD525A /* OverrideDataFlow.swift */; };
Expand Down Expand Up @@ -1112,6 +1114,8 @@
DD6B7CBA2C7FBBFA00B75029 /* ReviewInsulinActionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewInsulinActionView.swift; sourceTree = "<group>"; };
DD6D67E32C9C253500660C9B /* ColorSchemeOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorSchemeOption.swift; sourceTree = "<group>"; };
DD88C8E12C50420800F2D558 /* DefinitionRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefinitionRow.swift; sourceTree = "<group>"; };
DD940BA92CA7585D000830A5 /* GlucoseColorScheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseColorScheme.swift; sourceTree = "<group>"; };
DD940BAB2CA75889000830A5 /* DynamicGlucoseColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicGlucoseColor.swift; sourceTree = "<group>"; };
DDD163112C4C689900CD525A /* OverrideStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideStateModel.swift; sourceTree = "<group>"; };
DDD163132C4C68D300CD525A /* OverrideProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideProvider.swift; sourceTree = "<group>"; };
DDD163152C4C690300CD525A /* OverrideDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideDataFlow.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1912,6 +1916,7 @@
388E5A5925B6F0250019842D /* Models */ = {
isa = PBXGroup;
children = (
DD940BA92CA7585D000830A5 /* GlucoseColorScheme.swift */,
DD6D67E32C9C253500660C9B /* ColorSchemeOption.swift */,
385CEAC025F2EA52002D6D5B /* Announcement.swift */,
388E5A5F25B6F2310019842D /* Autosens.swift */,
Expand Down Expand Up @@ -1963,6 +1968,7 @@
388E5A5A25B6F05F0019842D /* Helpers */ = {
isa = PBXGroup;
children = (
DD940BAB2CA75889000830A5 /* DynamicGlucoseColor.swift */,
38F37827261260DC009DB701 /* Color+Extensions.swift */,
389ECE042601144100D86C4F /* ConcurrentMap.swift */,
38192E0C261BAF980094D973 /* ConvenienceExtensions.swift */,
Expand Down Expand Up @@ -3328,6 +3334,7 @@
CEB434E728B9053300B70274 /* LoopUIColorPalette+Default.swift in Sources */,
19F95FF329F10FBC00314DDC /* StatDataFlow.swift in Sources */,
582DF97B2C8CE209001F516D /* CarbView.swift in Sources */,
DD940BAA2CA7585D000830A5 /* GlucoseColorScheme.swift in Sources */,
3811DE2225C9D48300A708ED /* MainProvider.swift in Sources */,
3811DE0C25C9D32F00A708ED /* BaseProvider.swift in Sources */,
CE95BF5A2BA62E4A00DC3DE3 /* PluginSource.swift in Sources */,
Expand Down Expand Up @@ -3372,6 +3379,7 @@
38A504A425DD9C4000C5B9E8 /* UserDefaultsExtensions.swift in Sources */,
38FE826A25CC82DB001FF17A /* NetworkService.swift in Sources */,
FE66D16B291F74F8005D6F77 /* Bundle+Extensions.swift in Sources */,
DD940BAC2CA75889000830A5 /* DynamicGlucoseColor.swift in Sources */,
3883581C25EE79BB00E024B2 /* TextFieldWithToolBar.swift in Sources */,
58D08B302C8DEA7500AA37D3 /* ForecastView.swift in Sources */,
6B1A8D2E2B156EEF00E76752 /* LiveActivityBridge.swift in Sources */,
Expand Down
8 changes: 8 additions & 0 deletions FreeAPS.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"high" : 180,
"low" : 70,
"hours" : 6,
"glucoseColorScheme" : "staticColor",
"xGridLines" : true,
"yGridLines" : true,
"oneDimensionalGraph" : false,
Expand Down
67 changes: 67 additions & 0 deletions FreeAPS/Sources/Helpers/DynamicGlucoseColor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import Foundation
import SwiftUI

// Helper function to decide how to pick the glucose color
public func getDynamicGlucoseColor(
glucoseValue: Decimal,
highGlucoseColorValue: Decimal,
lowGlucoseColorValue: Decimal,
targetGlucose: Decimal,
glucoseColorScheme: GlucoseColorScheme,
offset: Decimal
) -> Color {
// Only use calculateHueBasedGlucoseColor if the setting is enabled in preferences
if glucoseColorScheme == .dynamicColor {
return calculateHueBasedGlucoseColor(
glucoseValue: glucoseValue,
highGlucose: highGlucoseColorValue + (offset * 1.75),
lowGlucose: lowGlucoseColorValue - offset,
targetGlucose: targetGlucose
)
}
// Otheriwse, use static (orange = high, red = low, green = range)
else {
if glucoseValue >= highGlucoseColorValue {
return Color.orange
} else if glucoseValue <= lowGlucoseColorValue {
return Color.red
} else {
return Color.green
}
}
}

// Dynamic color - Define the hue values for the key points
// We'll shift color gradually one glucose point at a time
// We'll shift through the rainbow colors of ROY-G-BIV from low to high
// Start at red for lowGlucose, green for targetGlucose, and violet for highGlucose
public func calculateHueBasedGlucoseColor(
glucoseValue: Decimal,
highGlucose: Decimal,
lowGlucose: Decimal,
targetGlucose: Decimal
) -> Color {
let redHue: CGFloat = 0.0 / 360.0 // 0 degrees
let greenHue: CGFloat = 120.0 / 360.0 // 120 degrees
let purpleHue: CGFloat = 270.0 / 360.0 // 270 degrees

// Calculate the hue based on the bgLevel
var hue: CGFloat
if glucoseValue <= lowGlucose {
hue = redHue
} else if glucoseValue >= highGlucose {
hue = purpleHue
} else if glucoseValue <= targetGlucose {
// Interpolate between red and green
let ratio = CGFloat(truncating: (glucoseValue - lowGlucose) / (targetGlucose - lowGlucose) as NSNumber)

hue = redHue + ratio * (greenHue - redHue)
} else {
// Interpolate between green and purple
let ratio = CGFloat(truncating: (glucoseValue - targetGlucose) / (highGlucose - targetGlucose) as NSNumber)
hue = greenHue + ratio * (purpleHue - greenHue)
}
// Return the color with full saturation and brightness
let color = Color(hue: hue, saturation: 0.6, brightness: 0.9)
return color
}
5 changes: 5 additions & 0 deletions FreeAPS/Sources/Models/FreeAPSSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ struct FreeAPSSettings: JSON, Equatable {
var high: Decimal = 180
var low: Decimal = 70
var hours: Int = 6
var glucoseColorScheme: GlucoseColorScheme = .staticColor
var xGridLines: Bool = true
var yGridLines: Bool = true
var oneDimensionalGraph: Bool = false
Expand Down Expand Up @@ -250,6 +251,10 @@ extension FreeAPSSettings: Decodable {
settings.hours = hours
}

if let glucoseColorScheme = try? container.decode(GlucoseColorScheme.self, forKey: .glucoseColorScheme) {
settings.glucoseColorScheme = glucoseColorScheme
}

if let xGridLines = try? container.decode(Bool.self, forKey: .xGridLines) {
settings.xGridLines = xGridLines
}
Expand Down
22 changes: 22 additions & 0 deletions FreeAPS/Sources/Models/GlucoseColorScheme.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// GlucoseColorScheme.swift
// FreeAPS
//
// Created by Cengiz Deniz on 27.09.24.
//
import Foundation

public enum GlucoseColorScheme: String, JSON, CaseIterable, Identifiable, Codable, Hashable {
public var id: String { rawValue }
case staticColor
case dynamicColor

var displayName: String {
switch self {
case .staticColor:
return "Static"
case .dynamicColor:
return "Dynamic"
}
}
}
2 changes: 2 additions & 0 deletions FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ extension Bolus {

@Published var lowGlucose: Decimal = 70
@Published var highGlucose: Decimal = 180
@Published var glucoseColorScheme: GlucoseColorScheme = .staticColor

@Published var predictions: Predictions?
@Published var amount: Decimal = 0
Expand Down Expand Up @@ -234,6 +235,7 @@ extension Bolus {
maxProtein = settings.settings.maxProtein
useFPUconversion = settingsManager.settings.useFPUconversion
isSmoothingEnabled = settingsManager.settings.smoothGlucose
glucoseColorScheme = settingsManager.settings.glucoseColorScheme
}

private func getCurrentSettingValue(for type: SettingType) async {
Expand Down
27 changes: 19 additions & 8 deletions FreeAPS/Sources/Modules/Bolus/View/ForecastChart.swift
Original file line number Diff line number Diff line change
Expand Up @@ -116,25 +116,36 @@ struct ForecastChart: View {
private func drawGlucose() -> some ChartContent {
ForEach(state.glucoseFromPersistence) { item in
let glucoseToDisplay = state.units == .mgdL ? Decimal(item.glucose) : Decimal(item.glucose).asMmolL
let pointMarkColor: Color = glucoseToDisplay > state.highGlucose ? Color.orange :
glucoseToDisplay < state.lowGlucose ? Color.red :
Color.green

// low and high glucose is parsed in state to mmol/L; parse it back to mg/dl here for comparison
let lowGlucose = units == .mgdL ? state.lowGlucose : state.lowGlucose.asMgdL
let highGlucose = units == .mgdL ? state.highGlucose : state.highGlucose.asMgdL
let targetGlucose = (state.determination.first?.currentTarget ?? state.currentBGTarget as NSDecimalNumber) as Decimal

let pointMarkColor: Color = FreeAPS.getDynamicGlucoseColor(
glucoseValue: Decimal(item.glucose),
highGlucoseColorValue: highGlucose,
lowGlucoseColorValue: lowGlucose,
targetGlucose: targetGlucose,
glucoseColorScheme: state.glucoseColorScheme,
offset: 20
)

if !state.isSmoothingEnabled {
PointMark(
x: .value("Time", item.date ?? Date(), unit: .second),
y: .value("Value", glucoseToDisplay)
)
.foregroundStyle(pointMarkColor)
.symbolSize(20)
.symbolSize(18)
} else {
PointMark(
x: .value("Time", item.date ?? Date(), unit: .second),
y: .value("Value", glucoseToDisplay)
)
.symbol {
Image(systemName: "record.circle.fill")
.font(.system(size: 8))
.font(.system(size: 6))
.bold()
.foregroundStyle(pointMarkColor)
}
Expand Down Expand Up @@ -226,16 +237,16 @@ struct ForecastChart: View {
AxisMarks(values: .stride(by: .hour, count: 2)) { _ in
AxisGridLine(stroke: .init(lineWidth: 0.5, dash: [2, 3]))
AxisValueLabel(format: .dateTime.hour(.defaultDigits(amPM: .narrow)), anchor: .top)
.font(.footnote)
.foregroundStyle(Color.primary)
.font(.caption2)
.foregroundStyle(Color.secondary)
}
}

private var forecastChartYAxis: some AxisContent {
AxisMarks(position: .trailing) { _ in
AxisGridLine(stroke: .init(lineWidth: 0.5, dash: [2, 3]))
AxisTick(length: 3, stroke: .init(lineWidth: 3)).foregroundStyle(Color.secondary)
AxisValueLabel().font(.footnote).foregroundStyle(Color.primary)
AxisValueLabel().font(.caption2).foregroundStyle(Color.secondary)
}
}
}
6 changes: 6 additions & 0 deletions FreeAPS/Sources/Modules/Home/HomeProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,11 @@ extension Home {
storage.retrieve(OpenAPS.Settings.pumpProfile, as: Autotune.self)?.basalProfile
?? [BasalProfileEntry(start: "00:00", minutes: 0, rate: 1)]
}

func getBGTarget() async -> BGTargets {
await storage.retrieveAsync(OpenAPS.Settings.bgTargets, as: BGTargets.self)
?? BGTargets(from: OpenAPS.defaults(for: OpenAPS.Settings.bgTargets))
?? BGTargets(units: .mgdL, userPreferredUnits: .mgdL, targets: [])
}
}
}
55 changes: 55 additions & 0 deletions FreeAPS/Sources/Modules/Home/HomeStateModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ extension Home {
@Published var maxValue: Decimal = 1.2
@Published var lowGlucose: Decimal = 70
@Published var highGlucose: Decimal = 180
@Published var currentGlucoseTarget: Decimal = 100
@Published var overrideUnit: Bool = false
@Published var glucoseColorScheme: GlucoseColorScheme = .staticColor
@Published var displayXgridLines: Bool = false
@Published var displayYgridLines: Bool = false
@Published var thresholdLines: Bool = false
Expand Down Expand Up @@ -326,6 +328,7 @@ extension Home {
manualTempBasal = apsManager.isManualTempBasal
setupCurrentTempTarget()
isSmoothingEnabled = settingsManager.settings.smoothGlucose
glucoseColorScheme = settingsManager.settings.glucoseColorScheme
maxValue = settingsManager.preferences.autosensMax
lowGlucose = units == .mgdL ? settingsManager.settings.low : settingsManager.settings.low.asMmolL
highGlucose = units == .mgdL ? settingsManager.settings.high : settingsManager.settings.high.asMmolL
Expand Down Expand Up @@ -491,6 +494,54 @@ extension Home {
}
}

private func getCurrentGlucoseTarget() async {
let now = Date()
let calendar = Calendar.current
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "HH:mm:ss"
dateFormatter.timeZone = TimeZone.current

let bgTargets = await provider.getBGTarget()
let entries: [(start: String, value: Decimal)] = bgTargets.targets.map { ($0.start, $0.low) }

for (index, entry) in entries.enumerated() {
guard let entryTime = dateFormatter.date(from: entry.start) else {
print("Invalid entry start time: \(entry.start)")
continue
}

let entryComponents = calendar.dateComponents([.hour, .minute, .second], from: entryTime)
let entryStartTime = calendar.date(
bySettingHour: entryComponents.hour!,
minute: entryComponents.minute!,
second: entryComponents.second!,
of: now
)!

let entryEndTime: Date
if index < entries.count - 1,
let nextEntryTime = dateFormatter.date(from: entries[index + 1].start)
{
let nextEntryComponents = calendar.dateComponents([.hour, .minute, .second], from: nextEntryTime)
entryEndTime = calendar.date(
bySettingHour: nextEntryComponents.hour!,
minute: nextEntryComponents.minute!,
second: nextEntryComponents.second!,
of: now
)!
} else {
entryEndTime = calendar.date(byAdding: .day, value: 1, to: entryStartTime)!
}

if now >= entryStartTime, now < entryEndTime {
await MainActor.run {
currentGlucoseTarget = units == .mgdL ? entry.value : entry.value.asMmolL
}
return
}
}
}

func openCGM() {
router.mainSecondaryModalView.send(router.view(for: .cgmDirect))
}
Expand Down Expand Up @@ -534,7 +585,11 @@ extension Home.StateModel:
isSmoothingEnabled = settingsManager.settings.smoothGlucose
lowGlucose = units == .mgdL ? settingsManager.settings.low : settingsManager.settings.low.asMmolL
highGlucose = units == .mgdL ? settingsManager.settings.high : settingsManager.settings.high.asMmolL
Task {
await getCurrentGlucoseTarget()
}
overrideUnit = settingsManager.settings.overrideHbA1cUnit
glucoseColorScheme = settingsManager.settings.glucoseColorScheme
displayXgridLines = settingsManager.settings.xGridLines
displayYgridLines = settingsManager.settings.yGridLines
thresholdLines = settingsManager.settings.rulerMarks
Expand Down
23 changes: 21 additions & 2 deletions FreeAPS/Sources/Modules/Home/View/Chart/DummyCharts.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,28 @@ extension MainChartView {
Chart {
/// high and low threshold lines
if thresholdLines {
RuleMark(y: .value("High", highGlucose)).foregroundStyle(Color.loopYellow)
let highColor = FreeAPS.getDynamicGlucoseColor(
glucoseValue: highGlucose,
highGlucoseColorValue: highGlucose,
lowGlucoseColorValue: highGlucose,
targetGlucose: units == .mgdL ? currentGlucoseTarget : currentGlucoseTarget.asMmolL,
glucoseColorScheme: glucoseColorScheme,
offset: units == .mgdL ? Decimal(20) : Decimal(20).asMmolL
)
let lowColor = FreeAPS.getDynamicGlucoseColor(
glucoseValue: lowGlucose,
highGlucoseColorValue: highGlucose,
lowGlucoseColorValue: lowGlucose,
targetGlucose: units == .mgdL ? currentGlucoseTarget : currentGlucoseTarget.asMmolL,
glucoseColorScheme: glucoseColorScheme,
offset: units == .mgdL ? Decimal(20) : Decimal(20).asMmolL
)

RuleMark(y: .value("High", highGlucose))
.foregroundStyle(highColor)
.lineStyle(.init(lineWidth: 1, dash: [5]))
RuleMark(y: .value("Low", lowGlucose)).foregroundStyle(Color.loopRed)
RuleMark(y: .value("Low", lowGlucose))
.foregroundStyle(lowColor)
.lineStyle(.init(lineWidth: 1, dash: [5]))
}
}
Expand Down
Loading

0 comments on commit d733543

Please sign in to comment.