Skip to content

Commit b80ec44

Browse files
authored
Nate/feature/LOOP-232/specific y axis behaviour (#246)
* the y-axis of the glucose chart now has specific behaviour * corrected typo * updated the default glucose chart range to 80 - 200 mg/dL * including the adjusted glucose display range and y-axis step size as items for the feature flag
1 parent 5e36b6a commit b80ec44

File tree

9 files changed

+160
-16
lines changed

9 files changed

+160
-16
lines changed

Loop Status Extension/StatusChartsManager.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import SwiftCharts
1212
import UIKit
1313

1414
class StatusChartsManager: ChartsManager {
15-
let predictedGlucose = PredictedGlucoseChart(predictedGlucoseBounds: FeatureFlags.predictedGlucoseChartClampEnabled ? .default : nil)
15+
let predictedGlucose = PredictedGlucoseChart(predictedGlucoseBounds: FeatureFlags.predictedGlucoseChartClampEnabled ? .default : nil,
16+
yAxisStepSizeMGDLOverride: FeatureFlags.predictedGlucoseChartClampEnabled ? 40 : nil)
1617

1718
init(colors: ChartColorPalette, settings: ChartSettings, traitCollection: UITraitCollection) {
1819
super.init(colors: colors, settings: settings, charts: [predictedGlucose], traitCollection: traitCollection)

Loop Status Extension/StatusViewController.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ class StatusViewController: UIViewController, NCWidgetProviding {
5858
traitCollection: traitCollection
5959
)
6060

61-
charts.predictedGlucose.glucoseDisplayRange = HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 100)...HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 175)
61+
let glucoseMGDLDisplayBound: (lower: Double, upper: Double) = FeatureFlags.predictedGlucoseChartClampEnabled ? (80, 240) : (100, 175)
62+
charts.predictedGlucose.glucoseDisplayRange = HKQuantity(unit: .milligramsPerDeciliter, doubleValue: glucoseMGDLDisplayBound.lower)...HKQuantity(unit: .milligramsPerDeciliter, doubleValue: glucoseMGDLDisplayBound.upper)
6263

6364
return charts
6465
}()

Loop.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,7 @@
410410
B405E35B24D2E05600DD058D /* HUDAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4F2C15961E09E94E00E160D4 /* HUDAssets.xcassets */; };
411411
B42C951424A3C76000857C73 /* CGMStatusHUDViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B42C951324A3C76000857C73 /* CGMStatusHUDViewModel.swift */; };
412412
B43DA44124D9C12100CAFF4E /* DismissibleHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B43DA44024D9C12100CAFF4E /* DismissibleHostingController.swift */; };
413+
B47A791C2508009E006C0E11 /* ChartAxisValuesStaticGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B47A791B2508009E006C0E11 /* ChartAxisValuesStaticGenerator.swift */; };
413414
B48B0BAC24900093009A48DE /* PumpStatusHUDView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B48B0BAB24900093009A48DE /* PumpStatusHUDView.swift */; };
414415
B490A03F24D0550F00F509FA /* GlucoseValueType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B490A03E24D0550F00F509FA /* GlucoseValueType.swift */; };
415416
B490A04124D0559D00F509FA /* DeviceLifecycleProgressState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B490A04024D0559D00F509FA /* DeviceLifecycleProgressState.swift */; };
@@ -1248,6 +1249,7 @@
12481249
B42C951324A3C76000857C73 /* CGMStatusHUDViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CGMStatusHUDViewModel.swift; sourceTree = "<group>"; };
12491250
B42C951624A3CAF200857C73 /* NotificationsCriticalAlertPermissionsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationsCriticalAlertPermissionsViewModel.swift; sourceTree = "<group>"; };
12501251
B43DA44024D9C12100CAFF4E /* DismissibleHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DismissibleHostingController.swift; sourceTree = "<group>"; };
1252+
B47A791B2508009E006C0E11 /* ChartAxisValuesStaticGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChartAxisValuesStaticGenerator.swift; sourceTree = "<group>"; };
12511253
B48B0BAB24900093009A48DE /* PumpStatusHUDView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpStatusHUDView.swift; sourceTree = "<group>"; };
12521254
B490A03C24D04F9400F509FA /* Color.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Color.swift; sourceTree = "<group>"; };
12531255
B490A03E24D0550F00F509FA /* GlucoseValueType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseValueType.swift; sourceTree = "<group>"; };
@@ -2039,6 +2041,7 @@
20392041
children = (
20402042
A9C62D8D2331708700535612 /* AuthenticationTableViewCell+NibLoadable.swift */,
20412043
4F08DE801E7BB6F1006741EA /* CGPoint.swift */,
2044+
B47A791B2508009E006C0E11 /* ChartAxisValuesStaticGenerator.swift */,
20422045
438991661E91B563000EEF90 /* ChartPoint.swift */,
20432046
43649A621C7A347F00523D7F /* CollectionType.swift */,
20442047
B490A03C24D04F9400F509FA /* Color.swift */,
@@ -3540,6 +3543,7 @@
35403543
buildActionMask = 2147483647;
35413544
files = (
35423545
4FB76FB91E8C42B000B39636 /* CollectionType.swift in Sources */,
3546+
B47A791C2508009E006C0E11 /* ChartAxisValuesStaticGenerator.swift in Sources */,
35433547
7D23667D21250C7E0028B67D /* LocalizedString.swift in Sources */,
35443548
43FCEEBD22212DD50013DD30 /* PredictedGlucoseChart.swift in Sources */,
35453549
436961911F19D11E00447E89 /* ChartPointsContextFillLayer.swift in Sources */,

Loop/Managers/StatusChartsManager.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ class StatusChartsManager: ChartsManager {
2626
let cob: COBChart
2727

2828
init(colors: ChartColorPalette, settings: ChartSettings, traitCollection: UITraitCollection) {
29-
let glucose = PredictedGlucoseChart(predictedGlucoseBounds: FeatureFlags.predictedGlucoseChartClampEnabled ? .default : nil)
29+
let glucose = PredictedGlucoseChart(predictedGlucoseBounds: FeatureFlags.predictedGlucoseChartClampEnabled ? .default : nil,
30+
yAxisStepSizeMGDLOverride: FeatureFlags.predictedGlucoseChartClampEnabled ? 40 : nil)
3031
let iob = IOBChart()
3132
let dose = DoseChart()
3233
let cob = COBChart()

Loop/View Controllers/PredictionTableViewController.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ class PredictionTableViewController: LoopChartsTableViewController, Identifiable
9494
}
9595
}
9696

97-
let glucoseChart = PredictedGlucoseChart(predictedGlucoseBounds: nil)
97+
let glucoseChart = PredictedGlucoseChart()
9898

9999
override func createChartsManager() -> ChartsManager {
100100
return ChartsManager(colors: .primary, settings: .default, charts: [glucoseChart], traitCollection: traitCollection)

Loop/View Controllers/StatusTableViewController.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ final class StatusTableViewController: LoopChartsTableViewController {
3535
override func viewDidLoad() {
3636
super.viewDidLoad()
3737

38-
statusCharts.glucose.glucoseDisplayRange = HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 100)...HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 175)
38+
let glucoseMGDLDisplayBound: (lower: Double, upper: Double) = FeatureFlags.predictedGlucoseChartClampEnabled ? (80, 240) : (100, 175)
39+
statusCharts.glucose.glucoseDisplayRange = HKQuantity(unit: .milligramsPerDeciliter, doubleValue: glucoseMGDLDisplayBound.lower)...HKQuantity(unit: .milligramsPerDeciliter, doubleValue: glucoseMGDLDisplayBound.upper)
3940

4041
registerPumpManager()
4142

Loop/View Models/BolusEntryViewModel.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,8 @@ final class BolusEntryViewModel: ObservableObject {
7979
}
8080

8181
let chartManager: ChartsManager = {
82-
let predictedGlucoseChart = PredictedGlucoseChart(predictedGlucoseBounds: FeatureFlags.predictedGlucoseChartClampEnabled ? .default : nil)
82+
let predictedGlucoseChart = PredictedGlucoseChart(predictedGlucoseBounds: FeatureFlags.predictedGlucoseChartClampEnabled ? .default : nil,
83+
yAxisStepSizeMGDLOverride: FeatureFlags.predictedGlucoseChartClampEnabled ? 40 : nil)
8384
predictedGlucoseChart.glucoseDisplayRange = BolusEntryViewModel.defaultGlucoseDisplayRange
8485
return ChartsManager(colors: .primary, settings: .default, charts: [predictedGlucoseChart], traitCollection: .current)
8586
}()

LoopUI/Charts/PredictedGlucoseChart.swift

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -68,15 +68,28 @@ public class PredictedGlucoseChart: GlucoseChart, ChartProviding {
6868
public private(set) var endDate: Date?
6969

7070
private var predictedGlucoseSoftBounds: PredictedGlucoseBounds?
71-
71+
72+
private let yAxisStepSizeMGDLOverride: Double?
73+
74+
private var maxYAxisValue: ChartAxisValue?
75+
76+
private var minYAxisValue: ChartAxisValue?
77+
78+
private var maxYAxisSegmentCount: Double {
79+
// when a glucose value is below the predicted glucose minimum soft bound, allow for more y-axis segments
80+
return glucoseValueBelowSoftBoundsMinimum() ? 5 : 4
81+
}
82+
7283
private func updateEndDate(_ date: Date) {
7384
if endDate == nil || date > endDate! {
7485
self.endDate = date
7586
}
7687
}
7788

78-
public init(predictedGlucoseBounds: PredictedGlucoseBounds?) {
89+
public init(predictedGlucoseBounds: PredictedGlucoseBounds? = nil,
90+
yAxisStepSizeMGDLOverride: Double? = nil) {
7991
self.predictedGlucoseSoftBounds = predictedGlucoseBounds
92+
self.yAxisStepSizeMGDLOverride = yAxisStepSizeMGDLOverride
8093
super.init()
8194
}
8295
}
@@ -131,16 +144,19 @@ extension PredictedGlucoseChart {
131144
glucoseDisplayRangePoints
132145
].flatMap { $0 }
133146

134-
let yAxisValues = ChartAxisValuesStaticGenerator.generateYAxisValuesWithChartPoints(points,
147+
let yAxisValues = ChartAxisValuesStaticGenerator.generateYAxisValuesUsingLinearSegmentStep(chartPoints: points,
135148
minSegmentCount: 2,
136-
maxSegmentCount: 4,
137-
multiple: glucoseUnit.chartableIncrement * 25,
149+
maxSegmentCount: maxYAxisSegmentCount,
150+
multiple: glucoseUnit == .milligramsPerDeciliter ? (yAxisStepSizeMGDLOverride ?? 25) : 1,
138151
axisValueGenerator: {
139152
ChartAxisValueDouble($0, labelSettings: axisLabelSettings)
140153
},
141154
addPaddingSegmentIfEdge: false
142155
)
143156

157+
minYAxisValue = yAxisValues.first
158+
maxYAxisValue = yAxisValues.last
159+
144160
let yAxisModel = ChartAxisModel(axisValues: yAxisValues, lineColor: colors.axisLine, labelSpaceReservationMode: .fixed(labelsWidthY))
145161

146162
let coordsSpace = ChartCoordsSpaceLeftBottomSingleAxis(chartSettings: chartSettings, chartFrame: frame, xModel: xAxisModel, yModel: yAxisModel)
@@ -265,17 +281,31 @@ extension PredictedGlucoseChart {
265281

266282
// MARK: - Clamping the predicted glucose values
267283
extension PredictedGlucoseChart {
268-
var chartedGlucoseValueMaximum: HKQuantity? {
284+
var chartMaximumValue: HKQuantity? {
269285
guard let glucosePointMaximum = glucosePoints.max(by: { point1, point2 in point1.y.scalar < point2.y.scalar }) else {
270286
return nil
271287
}
288+
289+
if let maxYAxisValue = maxYAxisValue,
290+
maxYAxisValue.scalar > glucosePointMaximum.y.scalar
291+
{
292+
return HKQuantity(unit: glucoseUnit, doubleValue: maxYAxisValue.scalar)
293+
}
294+
272295
return HKQuantity(unit: glucoseUnit, doubleValue: glucosePointMaximum.y.scalar)
273296
}
274-
275-
var chartedGlucoseValueMinimum: HKQuantity? {
297+
298+
var chartMinimumValue: HKQuantity? {
276299
guard let glucosePointMinimum = glucosePoints.min(by: { point1, point2 in point1.y.scalar < point2.y.scalar }) else {
277300
return nil
278301
}
302+
303+
if let minYAxisValue = minYAxisValue,
304+
minYAxisValue.scalar < glucosePointMinimum.y.scalar
305+
{
306+
return HKQuantity(unit: glucoseUnit, doubleValue: minYAxisValue.scalar)
307+
}
308+
279309
return HKQuantity(unit: glucoseUnit, doubleValue: glucosePointMinimum.y.scalar)
280310
}
281311

@@ -284,9 +314,9 @@ extension PredictedGlucoseChart {
284314
return glucoseValues
285315
}
286316

287-
let predictedGlucoseValueMaximum = chartedGlucoseValueMaximum != nil ? max(predictedGlucoseBounds.maximum, chartedGlucoseValueMaximum!) : predictedGlucoseBounds.maximum
317+
let predictedGlucoseValueMaximum = chartMaximumValue != nil ? max(predictedGlucoseBounds.maximum, chartMaximumValue!) : predictedGlucoseBounds.maximum
288318

289-
let predictedGlucoseValueMinimum = chartedGlucoseValueMinimum != nil ? min(predictedGlucoseBounds.minimum, chartedGlucoseValueMinimum!) : predictedGlucoseBounds.minimum
319+
let predictedGlucoseValueMinimum = chartMinimumValue != nil ? min(predictedGlucoseBounds.minimum, chartMinimumValue!) : predictedGlucoseBounds.minimum
290320

291321
return glucoseValues.map {
292322
if $0.quantity > predictedGlucoseValueMaximum {
@@ -299,6 +329,24 @@ extension PredictedGlucoseChart {
299329
}
300330
}
301331

332+
var chartedGlucoseValueMinimum: HKQuantity? {
333+
guard let glucosePointMinimum = glucosePoints.min(by: { point1, point2 in point1.y.scalar < point2.y.scalar }) else {
334+
return nil
335+
}
336+
337+
return HKQuantity(unit: glucoseUnit, doubleValue: glucosePointMinimum.y.scalar)
338+
}
339+
340+
func glucoseValueBelowSoftBoundsMinimum() -> Bool {
341+
guard let predictedGlucoseSoftBounds = predictedGlucoseSoftBounds,
342+
let chartedGlucoseValueMinimum = chartedGlucoseValueMinimum else
343+
{
344+
return false
345+
}
346+
347+
return chartedGlucoseValueMinimum < predictedGlucoseSoftBounds.minimum
348+
}
349+
302350
public struct PredictedGlucoseBounds {
303351
var minimum: HKQuantity
304352
var maximum: HKQuantity
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
//
2+
// ChartAxisValuesStaticGenerator.swift
3+
// LoopUI
4+
//
5+
// Created by Nathaniel Hamming on 2020-09-08.
6+
// Copyright © 2020 LoopKit Authors. All rights reserved.
7+
//
8+
9+
import SwiftCharts
10+
11+
extension ChartAxisValuesStaticGenerator {
12+
// This is the same as SwiftChart ChartAxisValuesStaticGenerator.generateAxisValuesWithChartPoints(...) with the exception that the `currentMultiple` is calculated linearly instead of quadratically
13+
static func generateYAxisValuesUsingLinearSegmentStep(chartPoints: [ChartPoint],
14+
minSegmentCount: Double,
15+
maxSegmentCount: Double,
16+
multiple: Double,
17+
axisValueGenerator: ChartAxisValueStaticGenerator,
18+
addPaddingSegmentIfEdge: Bool) -> [ChartAxisValue]
19+
{
20+
precondition(multiple > 0, "Invalid multiple: \(multiple)")
21+
22+
let sortedChartPoints = chartPoints.sorted {(obj1, obj2) in
23+
return obj1.y.scalar < obj2.y.scalar
24+
}
25+
26+
if let firstChartPoint = sortedChartPoints.first, let lastChartPoint = sortedChartPoints.last {
27+
let first = firstChartPoint.y.scalar
28+
let lastPar = lastChartPoint.y.scalar
29+
30+
guard lastPar >=~ first else {fatalError("Invalid range generating axis values")}
31+
32+
let last = lastPar =~ first ? lastPar + 1 : lastPar
33+
34+
/// The first axis value will be less than or equal to the first scalar value, aligned with the desired multiple
35+
var firstValue = first - (first.truncatingRemainder(dividingBy: multiple))
36+
/// The last axis value will be greater than or equal to the first scalar value, aligned with the desired multiple
37+
var lastValue = last + (abs(multiple - last).truncatingRemainder(dividingBy: multiple))
38+
var segmentSize = multiple
39+
40+
/// If there should be a padding segment added when a scalar value falls on the first or last axis value, adjust the first and last axis values
41+
if firstValue =~ first && addPaddingSegmentIfEdge {
42+
firstValue = firstValue - segmentSize
43+
}
44+
if lastValue =~ last && addPaddingSegmentIfEdge {
45+
lastValue = lastValue + segmentSize
46+
}
47+
48+
let distance = lastValue - firstValue
49+
var currentMultiple = multiple
50+
var segmentCount = distance / currentMultiple
51+
52+
/// Find the optimal number of segments and segment width
53+
54+
/// If the number of segments is greater than desired, make each segment wider
55+
while segmentCount > maxSegmentCount {
56+
// This is the only difference from SwiftCharts (i.e., currentMultiple *= 2)
57+
currentMultiple += multiple
58+
segmentCount = distance / currentMultiple
59+
}
60+
segmentCount = ceil(segmentCount)
61+
62+
/// Increase the number of segments until there are enough as desired
63+
while segmentCount < minSegmentCount {
64+
segmentCount += 1
65+
}
66+
segmentSize = currentMultiple
67+
68+
/// Generate axis values from the first value, segment size and number of segments
69+
let offset = firstValue
70+
return (0...Int(segmentCount)).map {segment in
71+
let scalar = offset + (Double(segment) * segmentSize)
72+
return axisValueGenerator(scalar)
73+
}
74+
} else {
75+
print("Trying to generate Y axis without datapoints, returning empty array")
76+
return []
77+
}
78+
}
79+
}
80+
81+
fileprivate func =~ (a: Double, b: Double) -> Bool {
82+
return fabs(a - b) < Double.ulpOfOne
83+
}
84+
85+
fileprivate func >=~ (a: Double, b: Double) -> Bool {
86+
return a =~ b || a > b
87+
}

0 commit comments

Comments
 (0)