From 1049655c898aa7cdfb2d09ece9c6283189a2c5e0 Mon Sep 17 00:00:00 2001 From: kristinkand Date: Thu, 1 Oct 2026 15:44:04 +0200 Subject: [PATCH 1/8] Add Time in Range band to home screen --- .../MainViewController+updateStats.swift | 36 ++++++++ .../Controllers/StatsDisplayModel.swift | 8 ++ LoopFollow/Controllers/TIRBandView.swift | 85 +++++++++++++++++++ LoopFollow/Settings/GeneralSettingsView.swift | 7 ++ LoopFollow/Settings/SettingsMenuView.swift | 2 + .../Settings/UnitsConfigurationView.swift | 16 ++++ LoopFollow/Storage/Storage.swift | 2 + LoopFollow/ViewControllers/MainHomeView.swift | 7 +- 8 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 LoopFollow/Controllers/TIRBandView.swift diff --git a/LoopFollow/Controllers/MainViewController+updateStats.swift b/LoopFollow/Controllers/MainViewController+updateStats.swift index f3c4c0d10..45c500512 100644 --- a/LoopFollow/Controllers/MainViewController+updateStats.swift +++ b/LoopFollow/Controllers/MainViewController+updateStats.swift @@ -48,5 +48,41 @@ extension MainViewController { statsDisplayModel.pieRange = Double(stats.percentRange) statsDisplayModel.pieHigh = Double(stats.percentHigh) } + updateTIRBand() + } + + /// Today's (since midnight) range distribution for the Time in Range band. + /// Very low is below 54 mg/dL; low, in range and high follow the Range Mode + /// chosen in Settings (TIR, TITR or Custom). + func updateTIRBand() { + let thresholds = UnitSettingsStore.shared.effectiveThresholds() + let startOfDay = Calendar.current.startOfDay(for: Date()).timeIntervalSince1970 + let values = bgData + .filter { $0.date >= startOfDay && $0.sgv > 0 } + .map { Double($0.sgv) } + + switch UnitSettingsStore.shared.timeInRangeMode { + case .tir: statsDisplayModel.bandTitle = "Time in Range" + case .titr: statsDisplayModel.bandTitle = "Time in Tight Range" + case .custom: statsDisplayModel.bandTitle = "Time in Range" + } + + guard !values.isEmpty else { + statsDisplayModel.bandHasData = false + statsDisplayModel.bandVeryLowPct = 0 + statsDisplayModel.bandLowPct = 0 + statsDisplayModel.bandInRangePct = 0 + statsDisplayModel.bandHighPct = 0 + return + } + + let total = Double(values.count) + func pct(_ count: Int) -> Double { Double(count) / total * 100 } + + statsDisplayModel.bandVeryLowPct = pct(values.filter { $0 < 54 }.count) + statsDisplayModel.bandLowPct = pct(values.filter { $0 >= 54 && $0 < thresholds.low }.count) + statsDisplayModel.bandInRangePct = pct(values.filter { $0 >= thresholds.low && $0 <= thresholds.high }.count) + statsDisplayModel.bandHighPct = pct(values.filter { $0 > thresholds.high }.count) + statsDisplayModel.bandHasData = true } } diff --git a/LoopFollow/Controllers/StatsDisplayModel.swift b/LoopFollow/Controllers/StatsDisplayModel.swift index 72e60560a..a120c8812 100644 --- a/LoopFollow/Controllers/StatsDisplayModel.swift +++ b/LoopFollow/Controllers/StatsDisplayModel.swift @@ -15,4 +15,12 @@ class StatsDisplayModel: ObservableObject { @Published var pieLow: Double = 0 @Published var pieRange: Double = 0 @Published var pieHigh: Double = 0 + + // Time in Range band (today since midnight) + @Published var bandTitle: String = "Time in Range" + @Published var bandHasData: Bool = false + @Published var bandVeryLowPct: Double = 0 + @Published var bandLowPct: Double = 0 + @Published var bandInRangePct: Double = 0 + @Published var bandHighPct: Double = 0 } diff --git a/LoopFollow/Controllers/TIRBandView.swift b/LoopFollow/Controllers/TIRBandView.swift new file mode 100644 index 000000000..04e29a06a --- /dev/null +++ b/LoopFollow/Controllers/TIRBandView.swift @@ -0,0 +1,85 @@ +// LoopFollow +// TIRBandView.swift + +import SwiftUI + +/// Time in Range band for the home screen: today's in-range +/// percentage above a segmented bar (very low / low / in range / high). +/// The range follows the Range Mode in Settings (TIR, TITR or Custom). +/// Tapping opens the statistics screen. +struct TIRBandView: View { + @ObservedObject var model: StatsDisplayModel + var onTap: (() -> Void)? + + private var percentText: String { + guard model.bandHasData else { return "-- %" } + return model.bandInRangePct.formatted(.number.precision(.fractionLength(0 ... 1))) + " %" + } + + private var segments: [(color: Color, fraction: CGFloat)] { + guard model.bandHasData else { return [(Color.secondary.opacity(0.3), 1)] } + return [ + (.red, CGFloat(model.bandVeryLowPct / 100)), + (.orange, CGFloat(model.bandLowPct / 100)), + (.green, CGFloat(model.bandInRangePct / 100)), + (.purple, CGFloat(model.bandHighPct / 100)), + ] + } + + var body: some View { + HStack(alignment: .center, spacing: 12) { + VStack(alignment: .leading, spacing: 6) { + HStack(alignment: .firstTextBaseline, spacing: 6) { + Text(percentText) + .font(.title2) + .fontWeight(.bold) + .fontDesign(.rounded) + .foregroundStyle(.primary) + (Text(model.bandTitle).fontWeight(.semibold) + Text(" today")) + .font(.subheadline) + .foregroundStyle(.secondary) + .lineLimit(1) + .minimumScaleFactor(0.8) + } + + DistributionBar(segments: segments) + .frame(height: 6) + } + + Spacer(minLength: 8) + + Image(systemName: "chevron.right") + .font(.system(size: 15, weight: .semibold)) + .foregroundStyle(.secondary) + } + .padding(.horizontal, 16) + .frame(height: 64) + .background( + RoundedRectangle(cornerRadius: 14, style: .continuous) + .fill(Color(.secondarySystemBackground)) + ) + .contentShape(Rectangle()) + .onTapGesture { onTap?() } + } +} + +/// Horizontal bar of capsules, one per non-empty segment, sized by share. +private struct DistributionBar: View { + let segments: [(color: Color, fraction: CGFloat)] + + var body: some View { + GeometryReader { geo in + let spacing: CGFloat = 2 + let shown = segments.filter { $0.fraction > 0.005 } + let available = max(geo.size.width - spacing * CGFloat(max(shown.count - 1, 0)), 0) + HStack(spacing: spacing) { + ForEach(Array(shown.enumerated()), id: \.offset) { _, segment in + Capsule() + .fill(segment.color) + .frame(width: available * segment.fraction) + } + } + .frame(maxHeight: .infinity) + } + } +} diff --git a/LoopFollow/Settings/GeneralSettingsView.swift b/LoopFollow/Settings/GeneralSettingsView.swift index 0ac3acc69..75608ae57 100644 --- a/LoopFollow/Settings/GeneralSettingsView.swift +++ b/LoopFollow/Settings/GeneralSettingsView.swift @@ -8,6 +8,7 @@ struct GeneralSettingsView: View { @ObservedObject var appBadge = Storage.shared.appBadge @ObservedObject var appearanceMode = Storage.shared.appearanceMode @ObservedObject var showStats = Storage.shared.showStats + @ObservedObject var showTIRBand = Storage.shared.showTIRBand @ObservedObject var showSmallGraph = Storage.shared.showSmallGraph @ObservedObject var screenlockSwitchState = Storage.shared.screenlockSwitchState @ObservedObject var showDisplayName = Storage.shared.showDisplayName @@ -45,6 +46,12 @@ struct GeneralSettingsView: View { } } Toggle("Display Stats", isOn: $showStats.value) + if showStats.value { + Picker("Stats Style", selection: $showTIRBand.value) { + Text("Statistics Box").tag(false) + Text("Time in Range Band").tag(true) + } + } Toggle("Display Small Graph", isOn: $showSmallGraph.value) Toggle("Color BG Text", isOn: $colorBGText.value) Toggle("Keep Screen Active", isOn: $screenlockSwitchState.value) diff --git a/LoopFollow/Settings/SettingsMenuView.swift b/LoopFollow/Settings/SettingsMenuView.swift index 93fd0fd93..5ace5adf5 100644 --- a/LoopFollow/Settings/SettingsMenuView.swift +++ b/LoopFollow/Settings/SettingsMenuView.swift @@ -158,6 +158,7 @@ enum SettingsRoute: Hashable, Identifiable { SettingsLeaf("Persistent Notification"), SettingsLeaf("Appearance", ["dark mode", "light mode", "theme"]), SettingsLeaf("Display Stats"), + SettingsLeaf("Stats Style", ["time in range band", "statistics box", "tir band", "progress bar"]), SettingsLeaf("Display Small Graph"), SettingsLeaf("Color BG Text"), SettingsLeaf("Keep Screen Active", ["screen lock", "screenlock"]), @@ -192,6 +193,7 @@ enum SettingsRoute: Hashable, Identifiable { + InfoType.allCases.map { SettingsLeaf($0.name) } case .units: return [ SettingsLeaf("Glucose Unit"), + SettingsLeaf("Home Screen Stats", ["stats style", "time in range band", "statistics box", "progress bar"]), SettingsLeaf("Range Mode", ["tir", "titr", "time in range"]), SettingsLeaf("Glycemic Metrics", ["hba1c", "ehba1c", "gmi"]), SettingsLeaf("Variability", ["standard deviation", "cv"]), diff --git a/LoopFollow/Settings/UnitsConfigurationView.swift b/LoopFollow/Settings/UnitsConfigurationView.swift index 2124077c7..311805eb6 100644 --- a/LoopFollow/Settings/UnitsConfigurationView.swift +++ b/LoopFollow/Settings/UnitsConfigurationView.swift @@ -25,6 +25,8 @@ struct UnitsConfigurationView: View { var sections: Sections = .all @State private var rangeMode = UnitSettingsStore.shared.timeInRangeMode + @ObservedObject private var showStats = Storage.shared.showStats + @ObservedObject private var showTIRBand = Storage.shared.showTIRBand @State private var glucoseUnit = UnitSettingsStore.shared.glucoseUnit @State private var lowValue = Storage.shared.lowLine.value @State private var highValue = Storage.shared.highLine.value @@ -67,6 +69,20 @@ struct UnitsConfigurationView: View { @ViewBuilder private var statisticsSections: some View { Group { + Section { + Toggle("Display Stats", isOn: $showStats.value) + if showStats.value { + Picker("Stats Style", selection: $showTIRBand.value) { + Text("Statistics Box").tag(false) + Text("Time in Range Band").tag(true) + } + } + } header: { + Text("Home Screen Stats") + } footer: { + Text("Statistics Box shows the pie chart, averages and A1C for the last 24 hours. Time in Range Band shows today's time in range for the Range Mode below. The same setting is in General.") + } + Section { Picker("Range Mode", selection: $rangeMode) { Text("TIR").tag(TimeInRangeDisplayMode.tir) diff --git a/LoopFollow/Storage/Storage.swift b/LoopFollow/Storage/Storage.swift index dd63f6dd6..5e8f254f8 100644 --- a/LoopFollow/Storage/Storage.swift +++ b/LoopFollow/Storage/Storage.swift @@ -70,6 +70,8 @@ class Storage { var colorBGText = StorageValue(key: "colorBGText", defaultValue: true) var appearanceMode = StorageValue(key: "appearanceMode", defaultValue: .dark) var showStats = StorageValue(key: "showStats", defaultValue: true) + /// Show the home screen stats as a Time in Range band instead of the statistics box. + var showTIRBand = StorageValue(key: "showTIRBand", defaultValue: false) var useIFCC = StorageValue(key: "useIFCC", defaultValue: false) var showSmallGraph = StorageValue(key: "showSmallGraph", defaultValue: true) var screenlockSwitchState = StorageValue(key: "screenlockSwitchState", defaultValue: true) diff --git a/LoopFollow/ViewControllers/MainHomeView.swift b/LoopFollow/ViewControllers/MainHomeView.swift index 599120f7d..5be5c0a2f 100644 --- a/LoopFollow/ViewControllers/MainHomeView.swift +++ b/LoopFollow/ViewControllers/MainHomeView.swift @@ -13,6 +13,7 @@ struct MainHomeView: View { @ObservedObject var showSmallGraph = Storage.shared.showSmallGraph @ObservedObject var showStats = Storage.shared.showStats + @ObservedObject var showTIRBand = Storage.shared.showTIRBand @ObservedObject var hideInfoTable = Storage.shared.hideInfoTable @ObservedObject var smallGraphHeight = Storage.shared.smallGraphHeight @ObservedObject var url = Storage.shared.url @@ -64,7 +65,11 @@ struct MainHomeView: View { // Statistics if showStats.value { - StatsDisplayView(model: statsModel, onTap: onStatsTap) + if showTIRBand.value { + TIRBandView(model: statsModel, onTap: onStatsTap) + } else { + StatsDisplayView(model: statsModel, onTap: onStatsTap) + } } } .padding(8) From 57432e5fa33a37449ce4722653c1dbaed0140580 Mon Sep 17 00:00:00 2001 From: kristinkand Date: Thu, 1 Oct 2026 15:45:43 +0200 Subject: [PATCH 2/8] Refresh home screen stats when Range Mode changes --- LoopFollow/ViewControllers/MainViewController.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/LoopFollow/ViewControllers/MainViewController.swift b/LoopFollow/ViewControllers/MainViewController.swift index 06c5e1f76..507119d1c 100644 --- a/LoopFollow/ViewControllers/MainViewController.swift +++ b/LoopFollow/ViewControllers/MainViewController.swift @@ -282,6 +282,7 @@ class MainViewController: UIViewController, UNUserNotificationCenterDelegate { .sink { [weak self] _ in self?.updateBGGraphSettings() self?.updateBGGraph() + self?.updateStats() } .store(in: &cancellables) From 727453d2562763eb198f1b19ce789411d9847823 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Sat, 10 Oct 2026 13:37:27 +0200 Subject: [PATCH 3/8] Fix TIR band counting and match the stats screen The band now uses TIRCalculator, so each reading is counted once with the same 54 and 250 mg/dL cutoffs as the TIR chart in Stats. It also has a very high segment and uses the same colors as that chart. Every non-empty segment gets a minimum width so a single low reading stays visible, and the bar fills the full width of the band. The Stats Style setting is only in General. --- .../MainViewController+updateStats.swift | 33 ++++++++--------- .../Controllers/StatsDisplayModel.swift | 1 + LoopFollow/Controllers/TIRBandView.swift | 36 +++++++++++++------ LoopFollow/Settings/SettingsMenuView.swift | 1 - .../Settings/UnitsConfigurationView.swift | 16 --------- LoopFollow/Stats/TIR/TIRCalculator.swift | 10 +++--- LoopFollow/Stats/TIR/TIRGraphView.swift | 2 +- 7 files changed, 47 insertions(+), 52 deletions(-) diff --git a/LoopFollow/Controllers/MainViewController+updateStats.swift b/LoopFollow/Controllers/MainViewController+updateStats.swift index 45c500512..582f325eb 100644 --- a/LoopFollow/Controllers/MainViewController+updateStats.swift +++ b/LoopFollow/Controllers/MainViewController+updateStats.swift @@ -52,8 +52,8 @@ extension MainViewController { } /// Today's (since midnight) range distribution for the Time in Range band. - /// Very low is below 54 mg/dL; low, in range and high follow the Range Mode - /// chosen in Settings (TIR, TITR or Custom). + /// Very low is below 54 mg/dL and very high above 250 mg/dL; low, in range + /// and high follow the Range Mode chosen in Settings (TIR, TITR or Custom). func updateTIRBand() { let thresholds = UnitSettingsStore.shared.effectiveThresholds() let startOfDay = Calendar.current.startOfDay(for: Date()).timeIntervalSince1970 @@ -67,22 +67,19 @@ extension MainViewController { case .custom: statsDisplayModel.bandTitle = "Time in Range" } - guard !values.isEmpty else { - statsDisplayModel.bandHasData = false - statsDisplayModel.bandVeryLowPct = 0 - statsDisplayModel.bandLowPct = 0 - statsDisplayModel.bandInRangePct = 0 - statsDisplayModel.bandHighPct = 0 - return - } - - let total = Double(values.count) - func pct(_ count: Int) -> Double { Double(count) / total * 100 } + let percentages = TIRCalculator.calculatePercentages( + readings: values, + veryLowThreshold: 54.0, + lowThreshold: thresholds.low, + highThreshold: thresholds.high, + veryHighThreshold: 250.0 + ) - statsDisplayModel.bandVeryLowPct = pct(values.filter { $0 < 54 }.count) - statsDisplayModel.bandLowPct = pct(values.filter { $0 >= 54 && $0 < thresholds.low }.count) - statsDisplayModel.bandInRangePct = pct(values.filter { $0 >= thresholds.low && $0 <= thresholds.high }.count) - statsDisplayModel.bandHighPct = pct(values.filter { $0 > thresholds.high }.count) - statsDisplayModel.bandHasData = true + statsDisplayModel.bandVeryLowPct = percentages.veryLow + statsDisplayModel.bandLowPct = percentages.low + statsDisplayModel.bandInRangePct = percentages.inRange + statsDisplayModel.bandHighPct = percentages.high + statsDisplayModel.bandVeryHighPct = percentages.veryHigh + statsDisplayModel.bandHasData = !values.isEmpty } } diff --git a/LoopFollow/Controllers/StatsDisplayModel.swift b/LoopFollow/Controllers/StatsDisplayModel.swift index a120c8812..8f3f75d0b 100644 --- a/LoopFollow/Controllers/StatsDisplayModel.swift +++ b/LoopFollow/Controllers/StatsDisplayModel.swift @@ -23,4 +23,5 @@ class StatsDisplayModel: ObservableObject { @Published var bandLowPct: Double = 0 @Published var bandInRangePct: Double = 0 @Published var bandHighPct: Double = 0 + @Published var bandVeryHighPct: Double = 0 } diff --git a/LoopFollow/Controllers/TIRBandView.swift b/LoopFollow/Controllers/TIRBandView.swift index 04e29a06a..06fbe4ffc 100644 --- a/LoopFollow/Controllers/TIRBandView.swift +++ b/LoopFollow/Controllers/TIRBandView.swift @@ -4,7 +4,7 @@ import SwiftUI /// Time in Range band for the home screen: today's in-range -/// percentage above a segmented bar (very low / low / in range / high). +/// percentage above a segmented bar (very low / low / in range / high / very high). /// The range follows the Range Mode in Settings (TIR, TITR or Custom). /// Tapping opens the statistics screen. struct TIRBandView: View { @@ -19,10 +19,11 @@ struct TIRBandView: View { private var segments: [(color: Color, fraction: CGFloat)] { guard model.bandHasData else { return [(Color.secondary.opacity(0.3), 1)] } return [ - (.red, CGFloat(model.bandVeryLowPct / 100)), - (.orange, CGFloat(model.bandLowPct / 100)), - (.green, CGFloat(model.bandInRangePct / 100)), - (.purple, CGFloat(model.bandHighPct / 100)), + (TIRGraphView.Band.veryLow.color, CGFloat(model.bandVeryLowPct / 100)), + (TIRGraphView.Band.low.color, CGFloat(model.bandLowPct / 100)), + (TIRGraphView.Band.inRange.color, CGFloat(model.bandInRangePct / 100)), + (TIRGraphView.Band.high.color, CGFloat(model.bandHighPct / 100)), + (TIRGraphView.Band.veryHigh.color, CGFloat(model.bandVeryHighPct / 100)), ] } @@ -45,8 +46,7 @@ struct TIRBandView: View { DistributionBar(segments: segments) .frame(height: 6) } - - Spacer(minLength: 8) + .frame(maxWidth: .infinity, alignment: .leading) Image(systemName: "chevron.right") .font(.system(size: 15, weight: .semibold)) @@ -64,22 +64,36 @@ struct TIRBandView: View { } /// Horizontal bar of capsules, one per non-empty segment, sized by share. +/// Every non-empty segment is at least `minSegmentWidth` wide so a single +/// reading stays visible. private struct DistributionBar: View { let segments: [(color: Color, fraction: CGFloat)] + private let spacing: CGFloat = 2 + private let minSegmentWidth: CGFloat = 4 + var body: some View { GeometryReader { geo in - let spacing: CGFloat = 2 - let shown = segments.filter { $0.fraction > 0.005 } + let shown = segments.filter { $0.fraction > 0 } let available = max(geo.size.width - spacing * CGFloat(max(shown.count - 1, 0)), 0) + let widths = segmentWidths(shown.map(\.fraction), available: available) HStack(spacing: spacing) { - ForEach(Array(shown.enumerated()), id: \.offset) { _, segment in + ForEach(Array(shown.enumerated()), id: \.offset) { index, segment in Capsule() .fill(segment.color) - .frame(width: available * segment.fraction) + .frame(width: widths[index]) } } .frame(maxHeight: .infinity) } } + + private func segmentWidths(_ fractions: [CGFloat], available: CGFloat) -> [CGFloat] { + var widths = fractions.map { max($0 * available, minSegmentWidth) } + let excess = widths.reduce(0, +) - available + if excess > 0, let widest = widths.indices.max(by: { widths[$0] < widths[$1] }) { + widths[widest] = max(widths[widest] - excess, minSegmentWidth) + } + return widths + } } diff --git a/LoopFollow/Settings/SettingsMenuView.swift b/LoopFollow/Settings/SettingsMenuView.swift index 5ace5adf5..7c0a875dd 100644 --- a/LoopFollow/Settings/SettingsMenuView.swift +++ b/LoopFollow/Settings/SettingsMenuView.swift @@ -193,7 +193,6 @@ enum SettingsRoute: Hashable, Identifiable { + InfoType.allCases.map { SettingsLeaf($0.name) } case .units: return [ SettingsLeaf("Glucose Unit"), - SettingsLeaf("Home Screen Stats", ["stats style", "time in range band", "statistics box", "progress bar"]), SettingsLeaf("Range Mode", ["tir", "titr", "time in range"]), SettingsLeaf("Glycemic Metrics", ["hba1c", "ehba1c", "gmi"]), SettingsLeaf("Variability", ["standard deviation", "cv"]), diff --git a/LoopFollow/Settings/UnitsConfigurationView.swift b/LoopFollow/Settings/UnitsConfigurationView.swift index 311805eb6..2124077c7 100644 --- a/LoopFollow/Settings/UnitsConfigurationView.swift +++ b/LoopFollow/Settings/UnitsConfigurationView.swift @@ -25,8 +25,6 @@ struct UnitsConfigurationView: View { var sections: Sections = .all @State private var rangeMode = UnitSettingsStore.shared.timeInRangeMode - @ObservedObject private var showStats = Storage.shared.showStats - @ObservedObject private var showTIRBand = Storage.shared.showTIRBand @State private var glucoseUnit = UnitSettingsStore.shared.glucoseUnit @State private var lowValue = Storage.shared.lowLine.value @State private var highValue = Storage.shared.highLine.value @@ -69,20 +67,6 @@ struct UnitsConfigurationView: View { @ViewBuilder private var statisticsSections: some View { Group { - Section { - Toggle("Display Stats", isOn: $showStats.value) - if showStats.value { - Picker("Stats Style", selection: $showTIRBand.value) { - Text("Statistics Box").tag(false) - Text("Time in Range Band").tag(true) - } - } - } header: { - Text("Home Screen Stats") - } footer: { - Text("Statistics Box shows the pie chart, averages and A1C for the last 24 hours. Time in Range Band shows today's time in range for the Range Mode below. The same setting is in General.") - } - Section { Picker("Range Mode", selection: $rangeMode) { Text("TIR").tag(TimeInRangeDisplayMode.tir) diff --git a/LoopFollow/Stats/TIR/TIRCalculator.swift b/LoopFollow/Stats/TIR/TIRCalculator.swift index b387b5702..af814a5dd 100644 --- a/LoopFollow/Stats/TIR/TIRCalculator.swift +++ b/LoopFollow/Stats/TIR/TIRCalculator.swift @@ -91,11 +91,11 @@ class TIRCalculator { return tirPoints } - private static func calculatePercentages(readings: [Double], - veryLowThreshold: Double, - lowThreshold: Double, - highThreshold: Double, - veryHighThreshold: Double) -> (veryLow: Double, low: Double, inRange: Double, high: Double, veryHigh: Double) + static func calculatePercentages(readings: [Double], + veryLowThreshold: Double, + lowThreshold: Double, + highThreshold: Double, + veryHighThreshold: Double) -> (veryLow: Double, low: Double, inRange: Double, high: Double, veryHigh: Double) { let total = Double(readings.count) guard total > 0 else { diff --git a/LoopFollow/Stats/TIR/TIRGraphView.swift b/LoopFollow/Stats/TIR/TIRGraphView.swift index 03a84a14b..1eba0f767 100644 --- a/LoopFollow/Stats/TIR/TIRGraphView.swift +++ b/LoopFollow/Stats/TIR/TIRGraphView.swift @@ -7,7 +7,7 @@ import SwiftUI struct TIRGraphView: View { let tirData: [TIRDataPoint] - private enum Band: String, CaseIterable, Plottable { + enum Band: String, CaseIterable, Plottable { case veryLow = "Very Low" case low = "Low" case inRange = "In Range" From 1e5018d6985f69c47c7d59d5db83bb9d825c58af Mon Sep 17 00:00:00 2001 From: kristinkand Date: Sat, 10 Oct 2026 17:16:17 +0200 Subject: [PATCH 4/8] Use the same period for statistics box and TIR band (last 24h or since midnight) Use the same period for statistics box and TIR band (last 24h or since midnight) --- .../MainViewController+updateStats.swift | 50 ++++++++++++------- .../Controllers/StatsDisplayModel.swift | 3 +- LoopFollow/Controllers/TIRBandView.swift | 2 +- LoopFollow/Settings/GeneralSettingsView.swift | 26 +++++++--- LoopFollow/Settings/SettingsMenuView.swift | 5 +- LoopFollow/Storage/Storage.swift | 3 ++ .../ViewControllers/MainViewController.swift | 5 +- 7 files changed, 64 insertions(+), 30 deletions(-) diff --git a/LoopFollow/Controllers/MainViewController+updateStats.swift b/LoopFollow/Controllers/MainViewController+updateStats.swift index 582f325eb..9eb817fab 100644 --- a/LoopFollow/Controllers/MainViewController+updateStats.swift +++ b/LoopFollow/Controllers/MainViewController+updateStats.swift @@ -4,21 +4,34 @@ import Foundation extension MainViewController { + /// Start of the period both home screen stats cover: the last 24 hours, or since + /// midnight when chosen in Settings. Midnight follows the display time zone, so a + /// Time Zone Override is respected. + func homeStatsPeriodStart() -> TimeInterval { + if Storage.shared.statsSinceMidnight.value { + return dateTimeUtils.displayCalendar().startOfDay(for: Date()).timeIntervalSince1970 + } + return dateTimeUtils.getTimeIntervalNHoursAgo(N: 24) + } + func updateStats() { - if bgData.count > 0 { - var lastDayOfData = bgData - let graphHours = 24 * Storage.shared.downloadDays.value - // If we loaded more than 1 day of data, only use the last day for the stats - if graphHours > 24 { - let oneDayAgo = dateTimeUtils.getTimeIntervalNHoursAgo(N: 24) - var startIndex = 0 - while startIndex < bgData.count, bgData[startIndex].date < oneDayAgo { - startIndex += 1 - } - lastDayOfData = Array(bgData.dropFirst(startIndex)) - } + let periodStart = homeStatsPeriodStart() + let periodData = bgData.filter { $0.date >= periodStart } - let stats = StatsData(bgData: lastDayOfData) + if periodData.isEmpty { + // Nothing in the period yet (e.g. just after midnight): show placeholders + // rather than the previous period's numbers. + statsDisplayModel.lowPercent = "--" + statsDisplayModel.inRangePercent = "--" + statsDisplayModel.highPercent = "--" + statsDisplayModel.avgBG = "--" + statsDisplayModel.estA1C = "--" + statsDisplayModel.stdDev = "--" + statsDisplayModel.pieLow = 0 + statsDisplayModel.pieRange = 0 + statsDisplayModel.pieHigh = 0 + } else { + let stats = StatsData(bgData: periodData) statsDisplayModel.lowPercent = String(format: "%.1f%%", stats.percentLow) statsDisplayModel.inRangePercent = String(format: "%.1f%%", stats.percentRange) @@ -48,17 +61,17 @@ extension MainViewController { statsDisplayModel.pieRange = Double(stats.percentRange) statsDisplayModel.pieHigh = Double(stats.percentHigh) } - updateTIRBand() + updateTIRBand(periodStart: periodStart) } - /// Today's (since midnight) range distribution for the Time in Range band. + /// Range distribution for the Time in Range band over the same period as the + /// statistics box (last 24 hours, or today since midnight). /// Very low is below 54 mg/dL and very high above 250 mg/dL; low, in range /// and high follow the Range Mode chosen in Settings (TIR, TITR or Custom). - func updateTIRBand() { + func updateTIRBand(periodStart: TimeInterval) { let thresholds = UnitSettingsStore.shared.effectiveThresholds() - let startOfDay = Calendar.current.startOfDay(for: Date()).timeIntervalSince1970 let values = bgData - .filter { $0.date >= startOfDay && $0.sgv > 0 } + .filter { $0.date >= periodStart && $0.sgv > 0 } .map { Double($0.sgv) } switch UnitSettingsStore.shared.timeInRangeMode { @@ -81,5 +94,6 @@ extension MainViewController { statsDisplayModel.bandHighPct = percentages.high statsDisplayModel.bandVeryHighPct = percentages.veryHigh statsDisplayModel.bandHasData = !values.isEmpty + statsDisplayModel.bandPeriod = Storage.shared.statsSinceMidnight.value ? "today" : "last 24h" } } diff --git a/LoopFollow/Controllers/StatsDisplayModel.swift b/LoopFollow/Controllers/StatsDisplayModel.swift index 8f3f75d0b..425a4487b 100644 --- a/LoopFollow/Controllers/StatsDisplayModel.swift +++ b/LoopFollow/Controllers/StatsDisplayModel.swift @@ -16,8 +16,9 @@ class StatsDisplayModel: ObservableObject { @Published var pieRange: Double = 0 @Published var pieHigh: Double = 0 - // Time in Range band (today since midnight) + // Time in Range band (same period as the statistics box) @Published var bandTitle: String = "Time in Range" + @Published var bandPeriod: String = "last 24h" @Published var bandHasData: Bool = false @Published var bandVeryLowPct: Double = 0 @Published var bandLowPct: Double = 0 diff --git a/LoopFollow/Controllers/TIRBandView.swift b/LoopFollow/Controllers/TIRBandView.swift index 06fbe4ffc..dfb301088 100644 --- a/LoopFollow/Controllers/TIRBandView.swift +++ b/LoopFollow/Controllers/TIRBandView.swift @@ -36,7 +36,7 @@ struct TIRBandView: View { .fontWeight(.bold) .fontDesign(.rounded) .foregroundStyle(.primary) - (Text(model.bandTitle).fontWeight(.semibold) + Text(" today")) + (Text(model.bandTitle).fontWeight(.semibold) + Text(" " + model.bandPeriod)) .font(.subheadline) .foregroundStyle(.secondary) .lineLimit(1) diff --git a/LoopFollow/Settings/GeneralSettingsView.swift b/LoopFollow/Settings/GeneralSettingsView.swift index 75608ae57..073d9c72d 100644 --- a/LoopFollow/Settings/GeneralSettingsView.swift +++ b/LoopFollow/Settings/GeneralSettingsView.swift @@ -9,6 +9,7 @@ struct GeneralSettingsView: View { @ObservedObject var appearanceMode = Storage.shared.appearanceMode @ObservedObject var showStats = Storage.shared.showStats @ObservedObject var showTIRBand = Storage.shared.showTIRBand + @ObservedObject var statsSinceMidnight = Storage.shared.statsSinceMidnight @ObservedObject var showSmallGraph = Storage.shared.showSmallGraph @ObservedObject var screenlockSwitchState = Storage.shared.screenlockSwitchState @ObservedObject var showDisplayName = Storage.shared.showDisplayName @@ -45,13 +46,6 @@ struct GeneralSettingsView: View { Text(mode.displayName).tag(mode) } } - Toggle("Display Stats", isOn: $showStats.value) - if showStats.value { - Picker("Stats Style", selection: $showTIRBand.value) { - Text("Statistics Box").tag(false) - Text("Time in Range Band").tag(true) - } - } Toggle("Display Small Graph", isOn: $showSmallGraph.value) Toggle("Color BG Text", isOn: $colorBGText.value) Toggle("Keep Screen Active", isOn: $screenlockSwitchState.value) @@ -68,6 +62,24 @@ struct GeneralSettingsView: View { } } + Section { + Toggle("Display Stats", isOn: $showStats.value) + if showStats.value { + Picker("Stats Style", selection: $showTIRBand.value) { + Text("Statistics Box").tag(false) + Text("Time in Range Band").tag(true) + } + Picker("Stats Period", selection: $statsSinceMidnight.value) { + Text("Last 24 Hours").tag(false) + Text("Today (since midnight)").tag(true) + } + } + } header: { + Text("Statistics") + } footer: { + Text("The statistics box and the Time in Range band both cover this period. Today starts at midnight in your time zone, or in the Time Zone Override below when it is on.") + } + Section("Time Zone") { Toggle("Time Zone Override", isOn: $graphTimeZoneEnabled.value) .onChange(of: graphTimeZoneEnabled.value) { _ in markChartSettingsDirty() } diff --git a/LoopFollow/Settings/SettingsMenuView.swift b/LoopFollow/Settings/SettingsMenuView.swift index 7c0a875dd..1f1b28535 100644 --- a/LoopFollow/Settings/SettingsMenuView.swift +++ b/LoopFollow/Settings/SettingsMenuView.swift @@ -157,14 +157,15 @@ enum SettingsRoute: Hashable, Identifiable { SettingsLeaf("Display App Badge", ["badge"]), SettingsLeaf("Persistent Notification"), SettingsLeaf("Appearance", ["dark mode", "light mode", "theme"]), - SettingsLeaf("Display Stats"), - SettingsLeaf("Stats Style", ["time in range band", "statistics box", "tir band", "progress bar"]), SettingsLeaf("Display Small Graph"), SettingsLeaf("Color BG Text"), SettingsLeaf("Keep Screen Active", ["screen lock", "screenlock"]), SettingsLeaf("Show Display Name"), SettingsLeaf("Snoozer emoji"), SettingsLeaf("Force portrait mode", ["orientation", "landscape"]), + SettingsLeaf("Display Stats", ["statistics"]), + SettingsLeaf("Stats Style", ["time in range band", "statistics box", "tir band", "progress bar"]), + SettingsLeaf("Stats Period", ["statistics period", "last 24 hours", "since midnight", "today", "midnight"]), SettingsLeaf("Time Zone Override", ["timezone"]), SettingsLeaf("Speak BG", ["voice", "speech"]), SettingsLeaf("Send anonymous usage stats", ["telemetry", "diagnostics"]), diff --git a/LoopFollow/Storage/Storage.swift b/LoopFollow/Storage/Storage.swift index 5e8f254f8..0530f8153 100644 --- a/LoopFollow/Storage/Storage.swift +++ b/LoopFollow/Storage/Storage.swift @@ -72,6 +72,9 @@ class Storage { var showStats = StorageValue(key: "showStats", defaultValue: true) /// Show the home screen stats as a Time in Range band instead of the statistics box. var showTIRBand = StorageValue(key: "showTIRBand", defaultValue: false) + /// Period both home screen stats (statistics box and Time in Range band) cover: + /// the last 24 hours, or today since midnight. + var statsSinceMidnight = StorageValue(key: "statsSinceMidnight", defaultValue: false) var useIFCC = StorageValue(key: "useIFCC", defaultValue: false) var showSmallGraph = StorageValue(key: "showSmallGraph", defaultValue: true) var screenlockSwitchState = StorageValue(key: "screenlockSwitchState", defaultValue: true) diff --git a/LoopFollow/ViewControllers/MainViewController.swift b/LoopFollow/ViewControllers/MainViewController.swift index 507119d1c..f408f3266 100644 --- a/LoopFollow/ViewControllers/MainViewController.swift +++ b/LoopFollow/ViewControllers/MainViewController.swift @@ -269,7 +269,10 @@ class MainViewController: UIViewController, UNUserNotificationCenterDelegate { Storage.shared.units.$value.map { _ in () }.eraseToAnyPublisher(), Storage.shared.useIFCC.$value.map { _ in () }.eraseToAnyPublisher(), Storage.shared.showGMI.$value.map { _ in () }.eraseToAnyPublisher(), - Storage.shared.showStdDev.$value.map { _ in () }.eraseToAnyPublisher() + Storage.shared.showStdDev.$value.map { _ in () }.eraseToAnyPublisher(), + Storage.shared.statsSinceMidnight.$value.map { _ in () }.eraseToAnyPublisher(), + Storage.shared.graphTimeZoneEnabled.$value.map { _ in () }.eraseToAnyPublisher(), + Storage.shared.graphTimeZoneIdentifier.$value.map { _ in () }.eraseToAnyPublisher() ) .receive(on: DispatchQueue.main) .sink { [weak self] _ in From f3d13601bd89271b6a606dfff6bf67a05f45ed62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Sat, 10 Oct 2026 20:21:48 +0200 Subject: [PATCH 5/8] Keep very low and very high outside a custom range Very low is below the lower of 54 mg/dL and the range's low bound, and very high is above the higher of 250 mg/dL and the range's high bound. In range in the TIR band and the Stats screen then matches the statistics box for any custom range. --- .../Controllers/MainViewController+updateStats.swift | 11 ++++------- LoopFollow/Stats/TIR/TIRCalculator.swift | 3 +++ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/LoopFollow/Controllers/MainViewController+updateStats.swift b/LoopFollow/Controllers/MainViewController+updateStats.swift index 9eb817fab..2f543329e 100644 --- a/LoopFollow/Controllers/MainViewController+updateStats.swift +++ b/LoopFollow/Controllers/MainViewController+updateStats.swift @@ -66,19 +66,16 @@ extension MainViewController { /// Range distribution for the Time in Range band over the same period as the /// statistics box (last 24 hours, or today since midnight). - /// Very low is below 54 mg/dL and very high above 250 mg/dL; low, in range - /// and high follow the Range Mode chosen in Settings (TIR, TITR or Custom). + /// Low, in range and high follow the Range Mode chosen in Settings (TIR, TITR + /// or Custom). Very low is below 54 mg/dL and very high above 250 mg/dL, or + /// the range's own bounds when a custom range extends past them. func updateTIRBand(periodStart: TimeInterval) { let thresholds = UnitSettingsStore.shared.effectiveThresholds() let values = bgData .filter { $0.date >= periodStart && $0.sgv > 0 } .map { Double($0.sgv) } - switch UnitSettingsStore.shared.timeInRangeMode { - case .tir: statsDisplayModel.bandTitle = "Time in Range" - case .titr: statsDisplayModel.bandTitle = "Time in Tight Range" - case .custom: statsDisplayModel.bandTitle = "Time in Range" - } + statsDisplayModel.bandTitle = UnitSettingsStore.shared.timeInRangeTitle let percentages = TIRCalculator.calculatePercentages( readings: values, diff --git a/LoopFollow/Stats/TIR/TIRCalculator.swift b/LoopFollow/Stats/TIR/TIRCalculator.swift index af814a5dd..47b1b135e 100644 --- a/LoopFollow/Stats/TIR/TIRCalculator.swift +++ b/LoopFollow/Stats/TIR/TIRCalculator.swift @@ -102,6 +102,9 @@ class TIRCalculator { return (0.0, 0.0, 0.0, 0.0, 0.0) } + let veryLowThreshold = min(veryLowThreshold, lowThreshold) + let veryHighThreshold = max(veryHighThreshold, highThreshold) + var veryLowCount = 0 var lowCount = 0 var inRangeCount = 0 From a3e252662533a10fbfe0d6bcd338b99433bfb022 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Sat, 10 Oct 2026 20:21:48 +0200 Subject: [PATCH 6/8] Refresh home screen stats when the custom range thresholds change --- LoopFollow/ViewControllers/MainViewController.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/LoopFollow/ViewControllers/MainViewController.swift b/LoopFollow/ViewControllers/MainViewController.swift index f408f3266..08d112860 100644 --- a/LoopFollow/ViewControllers/MainViewController.swift +++ b/LoopFollow/ViewControllers/MainViewController.swift @@ -271,6 +271,8 @@ class MainViewController: UIViewController, UNUserNotificationCenterDelegate { Storage.shared.showGMI.$value.map { _ in () }.eraseToAnyPublisher(), Storage.shared.showStdDev.$value.map { _ in () }.eraseToAnyPublisher(), Storage.shared.statsSinceMidnight.$value.map { _ in () }.eraseToAnyPublisher(), + Storage.shared.lowLine.$value.map { _ in () }.eraseToAnyPublisher(), + Storage.shared.highLine.$value.map { _ in () }.eraseToAnyPublisher(), Storage.shared.graphTimeZoneEnabled.$value.map { _ in () }.eraseToAnyPublisher(), Storage.shared.graphTimeZoneIdentifier.$value.map { _ in () }.eraseToAnyPublisher() ) From 1f67b1e7439c462fc758463174b6ee6c2108e28d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Sat, 10 Oct 2026 20:21:48 +0200 Subject: [PATCH 7/8] Share the Range Mode title between the TIR band and the Stats screen --- LoopFollow/Helpers/Localizer.swift | 11 +++++++++++ LoopFollow/Stats/TIR/TIRView.swift | 13 +------------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/LoopFollow/Helpers/Localizer.swift b/LoopFollow/Helpers/Localizer.swift index b615a4cf6..ec48975aa 100644 --- a/LoopFollow/Helpers/Localizer.swift +++ b/LoopFollow/Helpers/Localizer.swift @@ -74,6 +74,17 @@ final class UnitSettingsStore { } } + var timeInRangeTitle: String { + switch timeInRangeMode { + case .tir: + return "Time in Range" + case .titr: + return "Time in Tight Range" + case .custom: + return "Custom Range" + } + } + var glycemicMetricMode: GlycemicMetricMode { get { Storage.shared.showGMI.value ? .gmi : .ehba1c diff --git a/LoopFollow/Stats/TIR/TIRView.swift b/LoopFollow/Stats/TIR/TIRView.swift index 8ff66e273..63806594c 100644 --- a/LoopFollow/Stats/TIR/TIRView.swift +++ b/LoopFollow/Stats/TIR/TIRView.swift @@ -13,7 +13,7 @@ struct TIRView: View { ZStack(alignment: .topTrailing) { VStack(alignment: .leading, spacing: 12) { HStack { - Text(tirTitle) + Text(UnitSettingsStore.shared.timeInRangeTitle) .font(.caption) .foregroundColor(.secondary) Spacer() @@ -86,17 +86,6 @@ struct TIRView: View { .buttonStyle(PlainButtonStyle()) } - private var tirTitle: String { - switch UnitSettingsStore.shared.timeInRangeMode { - case .tir: - return "Time in Range" - case .titr: - return "Time in Tight Range" - case .custom: - return "Custom Range" - } - } - private func formatRange(_: Double) -> String { let thresholds = UnitSettingsStore.shared.effectiveThresholds() let glucoseUnit = UnitSettingsStore.shared.glucoseUnit From c07ea8bb01e7962eada6eb5c211450914eef545f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Sun, 11 Oct 2026 11:24:04 +0200 Subject: [PATCH 8/8] Share TIR band thresholds between calculations and the Stats legend Very low and very high cutoffs come from one place, so the calculator, the home screen band and the Stats legend agree when a custom range reaches past 54 or 250 mg/dL. Legend rows for low and high bands with no width are hidden. --- .../MainViewController+updateStats.swift | 6 +-- LoopFollow/Helpers/Localizer.swift | 8 ++++ LoopFollow/Stats/TIR/TIRCalculator.swift | 9 ++-- LoopFollow/Stats/TIR/TIRView.swift | 44 +++++++++++++------ 4 files changed, 44 insertions(+), 23 deletions(-) diff --git a/LoopFollow/Controllers/MainViewController+updateStats.swift b/LoopFollow/Controllers/MainViewController+updateStats.swift index 2f543329e..1f10d25fb 100644 --- a/LoopFollow/Controllers/MainViewController+updateStats.swift +++ b/LoopFollow/Controllers/MainViewController+updateStats.swift @@ -70,7 +70,7 @@ extension MainViewController { /// or Custom). Very low is below 54 mg/dL and very high above 250 mg/dL, or /// the range's own bounds when a custom range extends past them. func updateTIRBand(periodStart: TimeInterval) { - let thresholds = UnitSettingsStore.shared.effectiveThresholds() + let thresholds = UnitSettingsStore.shared.effectiveBandThresholds() let values = bgData .filter { $0.date >= periodStart && $0.sgv > 0 } .map { Double($0.sgv) } @@ -79,10 +79,10 @@ extension MainViewController { let percentages = TIRCalculator.calculatePercentages( readings: values, - veryLowThreshold: 54.0, + veryLowThreshold: thresholds.veryLow, lowThreshold: thresholds.low, highThreshold: thresholds.high, - veryHighThreshold: 250.0 + veryHighThreshold: thresholds.veryHigh ) statsDisplayModel.bandVeryLowPct = percentages.veryLow diff --git a/LoopFollow/Helpers/Localizer.swift b/LoopFollow/Helpers/Localizer.swift index ec48975aa..e36c08153 100644 --- a/LoopFollow/Helpers/Localizer.swift +++ b/LoopFollow/Helpers/Localizer.swift @@ -74,6 +74,14 @@ final class UnitSettingsStore { } } + /// Returns the very low, low, high and very high thresholds (in mg/dL) for + /// the current range mode. Very low is 54 and very high 250, or the range's + /// own bounds when a custom range extends past them. + func effectiveBandThresholds() -> (veryLow: Double, low: Double, high: Double, veryHigh: Double) { + let range = effectiveThresholds() + return (min(54.0, range.low), range.low, range.high, max(250.0, range.high)) + } + var timeInRangeTitle: String { switch timeInRangeMode { case .tir: diff --git a/LoopFollow/Stats/TIR/TIRCalculator.swift b/LoopFollow/Stats/TIR/TIRCalculator.swift index 47b1b135e..a27eef414 100644 --- a/LoopFollow/Stats/TIR/TIRCalculator.swift +++ b/LoopFollow/Stats/TIR/TIRCalculator.swift @@ -7,11 +7,11 @@ class TIRCalculator { static func calculate(bgData: [ShareGlucoseData]) -> [TIRDataPoint] { guard !bgData.isEmpty else { return [] } - let thresholds = UnitSettingsStore.shared.effectiveThresholds() - let veryLowThreshold = 54.0 + let thresholds = UnitSettingsStore.shared.effectiveBandThresholds() + let veryLowThreshold = thresholds.veryLow let lowThreshold = thresholds.low let highThreshold = thresholds.high - let veryHighThreshold = 250.0 + let veryHighThreshold = thresholds.veryHigh var periodData: [TIRPeriod: [Double]] = [:] let calendar = dateTimeUtils.displayCalendar() @@ -102,9 +102,6 @@ class TIRCalculator { return (0.0, 0.0, 0.0, 0.0, 0.0) } - let veryLowThreshold = min(veryLowThreshold, lowThreshold) - let veryHighThreshold = max(veryHighThreshold, highThreshold) - var veryLowCount = 0 var lowCount = 0 var inRangeCount = 0 diff --git a/LoopFollow/Stats/TIR/TIRView.swift b/LoopFollow/Stats/TIR/TIRView.swift index 63806594c..c322cf59b 100644 --- a/LoopFollow/Stats/TIR/TIRView.swift +++ b/LoopFollow/Stats/TIR/TIRView.swift @@ -42,21 +42,25 @@ struct TIRView: View { label: "Very High (\(veryHighCutoffText))", percentage: average.veryHigh ) - TIRLegendItem( - color: .yellow, - label: "High (\(highCutoffText))", - percentage: average.high - ) + if hasHighBand { + TIRLegendItem( + color: .yellow, + label: "High (\(highCutoffText))", + percentage: average.high + ) + } TIRLegendItem( color: .green, label: "In Range (\(inRangeCutoffText))", percentage: average.inRange ) - TIRLegendItem( - color: .red.opacity(0.5), - label: "Low (\(lowCutoffText))", - percentage: average.low - ) + if hasLowBand { + TIRLegendItem( + color: .red.opacity(0.5), + label: "Low (\(lowCutoffText))", + percentage: average.low + ) + } TIRLegendItem( color: .red.opacity(0.8), label: "Very Low (\(veryLowCutoffText))", @@ -97,19 +101,31 @@ struct TIRView: View { } private var lowThreshold: Double { - UnitSettingsStore.shared.convertMgdlToDisplay(UnitSettingsStore.shared.effectiveThresholds().low) + UnitSettingsStore.shared.convertMgdlToDisplay(UnitSettingsStore.shared.effectiveBandThresholds().low) } private var highThreshold: Double { - UnitSettingsStore.shared.convertMgdlToDisplay(UnitSettingsStore.shared.effectiveThresholds().high) + UnitSettingsStore.shared.convertMgdlToDisplay(UnitSettingsStore.shared.effectiveBandThresholds().high) } private var veryLowThreshold: Double { - UnitSettingsStore.shared.convertMgdlToDisplay(54.0) + UnitSettingsStore.shared.convertMgdlToDisplay(UnitSettingsStore.shared.effectiveBandThresholds().veryLow) } private var veryHighThreshold: Double { - UnitSettingsStore.shared.convertMgdlToDisplay(250.0) + UnitSettingsStore.shared.convertMgdlToDisplay(UnitSettingsStore.shared.effectiveBandThresholds().veryHigh) + } + + /// A custom range that reaches past the very low cutoff leaves no low band. + private var hasLowBand: Bool { + let thresholds = UnitSettingsStore.shared.effectiveBandThresholds() + return thresholds.veryLow < thresholds.low + } + + /// A custom range that reaches past the very high cutoff leaves no high band. + private var hasHighBand: Bool { + let thresholds = UnitSettingsStore.shared.effectiveBandThresholds() + return thresholds.high < thresholds.veryHigh } private var veryLowCutoffText: String {