diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index 0003951137..8680204eae 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -29,7 +29,6 @@ 430B29952041F5CB00BA9F93 /* LoopSettings+Loop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 430B29942041F5CB00BA9F93 /* LoopSettings+Loop.swift */; }; 430D85891F44037000AF2D4F /* HUDViewTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 430D85881F44037000AF2D4F /* HUDViewTableViewCell.swift */; }; 4311FB9B1F37FE1B00D4C0A7 /* TitleSubtitleTextFieldTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4311FB9A1F37FE1B00D4C0A7 /* TitleSubtitleTextFieldTableViewCell.swift */; }; - 4315D2871CA5CC3B00589052 /* CarbEntryEditTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4315D2861CA5CC3B00589052 /* CarbEntryEditTableViewController.swift */; }; 4315D28A1CA5F45E00589052 /* DiagnosticLogger+LoopKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4315D2891CA5F45E00589052 /* DiagnosticLogger+LoopKit.swift */; }; 431A8C401EC6E8AB00823B9C /* CircleMaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 431A8C3F1EC6E8AB00823B9C /* CircleMaskView.swift */; }; 431EA87021EB29120076EC1A /* ExponentialInsulinModelPreset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 435CB6241F37ABFC00C320C7 /* ExponentialInsulinModelPreset.swift */; }; @@ -181,7 +180,6 @@ 43CB2B2B1D924D450079823D /* WCSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43CB2B2A1D924D450079823D /* WCSession.swift */; }; 43CE7CDE1CA8B63E003CC1B0 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43CE7CDD1CA8B63E003CC1B0 /* Data.swift */; }; 43CEE6E61E56AFD400CB9116 /* NightscoutUploader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43CEE6E51E56AFD400CB9116 /* NightscoutUploader.swift */; }; - 43D2E8231F00425400AE5CBF /* BolusViewController+LoopDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43D2E8221F00425400AE5CBF /* BolusViewController+LoopDataManager.swift */; }; 43D381621EBD9759007F8C8F /* HeaderValuesTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43D381611EBD9759007F8C8F /* HeaderValuesTableViewCell.swift */; }; 43D9000B21EB0BE000AF44BF /* LoopCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43D9FFCF21EAE05D00AF44BF /* LoopCore.framework */; }; 43D9001E21EB209400AF44BF /* LoopCore.h in Headers */ = {isa = PBXBuildFile; fileRef = 43D9FFD121EAE05D00AF44BF /* LoopCore.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -580,7 +578,6 @@ 430DA58D1D4AEC230097D1CA /* NSBundle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSBundle.swift; sourceTree = ""; }; 4311FB9A1F37FE1B00D4C0A7 /* TitleSubtitleTextFieldTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TitleSubtitleTextFieldTableViewCell.swift; sourceTree = ""; }; 4313EDDF1D8A6BF90060FA79 /* ChartContainerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ChartContainerView.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; - 4315D2861CA5CC3B00589052 /* CarbEntryEditTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CarbEntryEditTableViewController.swift; sourceTree = ""; }; 4315D2891CA5F45E00589052 /* DiagnosticLogger+LoopKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DiagnosticLogger+LoopKit.swift"; sourceTree = ""; }; 431A8C3F1EC6E8AB00823B9C /* CircleMaskView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircleMaskView.swift; sourceTree = ""; }; 431E73471FF95A900069B5F7 /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = ""; }; @@ -715,7 +712,6 @@ 43CB2B2A1D924D450079823D /* WCSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WCSession.swift; sourceTree = ""; }; 43CE7CDD1CA8B63E003CC1B0 /* Data.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = ""; }; 43CEE6E51E56AFD400CB9116 /* NightscoutUploader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NightscoutUploader.swift; sourceTree = ""; }; - 43D2E8221F00425400AE5CBF /* BolusViewController+LoopDataManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "BolusViewController+LoopDataManager.swift"; sourceTree = ""; }; 43D381611EBD9759007F8C8F /* HeaderValuesTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HeaderValuesTableViewCell.swift; sourceTree = ""; }; 43D533BB1CFD1DD7009E3085 /* WatchApp Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = "WatchApp Extension.entitlements"; sourceTree = ""; }; 43D848AF1E7DCBE100DADCBC /* Result.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Result.swift; sourceTree = ""; }; diff --git a/Loop/Managers/StatusChartsManager.swift b/Loop/Managers/StatusChartsManager.swift index d54d512797..a4ab76141c 100644 --- a/Loop/Managers/StatusChartsManager.swift +++ b/Loop/Managers/StatusChartsManager.swift @@ -104,7 +104,7 @@ extension StatusChartsManager { extension StatusChartsManager { func setDoseEntries(_ doseEntries: [DoseEntry]) { - dose.setDoseEntries(doseEntries) + dose.doseEntries = doseEntries invalidateChart(atIndex: ChartIndex.dose.rawValue) } diff --git a/LoopUI/Charts/DoseChart.swift b/LoopUI/Charts/DoseChart.swift index c8854e4446..3c85992ffb 100644 --- a/LoopUI/Charts/DoseChart.swift +++ b/LoopUI/Charts/DoseChart.swift @@ -9,19 +9,30 @@ import Foundation import LoopKit import SwiftCharts +fileprivate struct DosePointsCache { + let basal: [ChartPoint] + let basalFill: [ChartPoint] + let bolus: [ChartPoint] + let highlight: [ChartPoint] +} public class DoseChart: ChartProviding { public init() { + doseEntries = [] + } + + public var doseEntries: [DoseEntry] { + didSet { + pointsCache = nil + } } - public private(set) var basalDosePoints: [ChartPoint] = [] - public private(set) var bolusDosePoints: [ChartPoint] = [] - - /// Dose points selectable when highlighting - public private(set) var allDosePoints: [ChartPoint] = [] { + private var pointsCache: DosePointsCache? { didSet { - if let lastDate = allDosePoints.last?.x as? ChartAxisValueDate { - endDate = lastDate.date + if let pointsCache = pointsCache { + if let lastDate = pointsCache.highlight.last?.x as? ChartAxisValueDate { + endDate = lastDate.date + } } } } @@ -41,17 +52,19 @@ public class DoseChart: ChartProviding { public extension DoseChart { func didReceiveMemoryWarning() { - basalDosePoints = [] - bolusDosePoints = [] - allDosePoints = [] + pointsCache = nil doseChartCache = nil } func generate(withFrame frame: CGRect, xAxisModel: ChartAxisModel, xAxisValues: [ChartAxisValue], axisLabelSettings: ChartLabelSettings, guideLinesLayerSettings: ChartGuideLinesLayerSettings, colors: ChartColorPalette, chartSettings: ChartSettings, labelsWidthY: CGFloat, gestureRecognizer: UIGestureRecognizer?, traitCollection: UITraitCollection) -> Chart { let integerFormatter = NumberFormatter.integer + + let startDate = ChartAxisValueDate.dateFromScalar(xAxisValues.first!.scalar) + + let points = generateDosePoints(startDate: startDate) - let yAxisValues = ChartAxisValuesStaticGenerator.generateYAxisValuesWithChartPoints(basalDosePoints + bolusDosePoints + doseDisplayRangePoints, minSegmentCount: 2, maxSegmentCount: 3, multiple: log10(2) / 2, axisValueGenerator: { ChartAxisValueDoubleLog(screenLocDouble: $0, formatter: integerFormatter, labelSettings: axisLabelSettings) }, addPaddingSegmentIfEdge: true) + let yAxisValues = ChartAxisValuesStaticGenerator.generateYAxisValuesWithChartPoints(points.basal + points.bolus + doseDisplayRangePoints, minSegmentCount: 2, maxSegmentCount: 3, multiple: log(2) / 2, axisValueGenerator: { ChartAxisValueDoubleLog(screenLocDouble: $0, formatter: integerFormatter, labelSettings: axisLabelSettings) }, addPaddingSegmentIfEdge: true) let yAxisModel = ChartAxisModel(axisValues: yAxisValues, lineColor: colors.axisLine, labelSpaceReservationMode: .fixed(labelsWidthY)) @@ -60,23 +73,23 @@ public extension DoseChart { let (xAxisLayer, yAxisLayer, innerFrame) = (coordsSpace.xAxisLayer, coordsSpace.yAxisLayer, coordsSpace.chartInnerFrame) // The dose area - let lineModel = ChartLineModel(chartPoints: basalDosePoints, lineColor: colors.doseTint, lineWidth: 2, animDuration: 0, animDelay: 0) + let lineModel = ChartLineModel(chartPoints: points.basal, lineColor: colors.doseTint, lineWidth: 2, animDuration: 0, animDelay: 0) let doseLine = ChartPointsLineLayer(xAxis: xAxisLayer.axis, yAxis: yAxisLayer.axis, lineModels: [lineModel]) let doseArea = ChartPointsFillsLayer( xAxis: xAxisLayer.axis, yAxis: yAxisLayer.axis, fills: [ChartPointsFill( - chartPoints: basalDosePoints, + chartPoints: points.basalFill, fillColor: colors.doseTint.withAlphaComponent(0.5), createContainerPoints: false )] ) let bolusLayer: ChartPointsScatterDownTrianglesLayer? - - if bolusDosePoints.count > 0 { - bolusLayer = ChartPointsScatterDownTrianglesLayer(xAxis: xAxisLayer.axis, yAxis: yAxisLayer.axis, chartPoints: bolusDosePoints, displayDelay: 0, itemSize: CGSize(width: 12, height: 12), itemFillColor: colors.doseTint) + + if points.bolus.count > 0 { + bolusLayer = ChartPointsScatterDownTrianglesLayer(xAxis: xAxisLayer.axis, yAxis: yAxisLayer.axis, chartPoints: points.bolus, displayDelay: 0, itemSize: CGSize(width: 12, height: 12), itemFillColor: colors.doseTint) } else { bolusLayer = nil } @@ -100,7 +113,7 @@ public extension DoseChart { xAxisLayer: xAxisLayer, yAxisLayer: yAxisLayer, axisLabelSettings: axisLabelSettings, - chartPoints: allDosePoints, + chartPoints: points.highlight, tintColor: colors.doseTint, gestureRecognizer: gestureRecognizer ) @@ -119,17 +132,21 @@ public extension DoseChart { return Chart(frame: frame, innerFrame: innerFrame, settings: chartSettings, layers: layers.compactMap { $0 }) } -} - -public extension DoseChart { - func setDoseEntries(_ doseEntries: [DoseEntry]) { + + private func generateDosePoints(startDate: Date) -> DosePointsCache { + + guard pointsCache == nil else { + return pointsCache! + } + let dateFormatter = DateFormatter(timeStyle: .short) let doseFormatter = NumberFormatter.dose - var basalDosePoints = [ChartPoint]() - var bolusDosePoints = [ChartPoint]() - var allDosePoints = [ChartPoint]() - + var basalPoints = [ChartPoint]() + var basalFillPoints = [ChartPoint]() + var bolusPoints = [ChartPoint]() + var highlightPoints = [ChartPoint]() + for entry in doseEntries { let time = entry.endDate.timeIntervalSince(entry.startDate) @@ -138,11 +155,11 @@ public extension DoseChart { let y = ChartAxisValueDoubleLog(actualDouble: entry.unitsInDeliverableIncrements, unitString: "U", formatter: doseFormatter) let point = ChartPoint(x: x, y: y) - bolusDosePoints.append(point) - allDosePoints.append(point) + bolusPoints.append(point) + highlightPoints.append(point) } else if time > 0 { // TODO: Display the DateInterval - let startX = ChartAxisValueDate(date: entry.startDate, formatter: dateFormatter) + let startX = ChartAxisValueDate(date: max(startDate, entry.startDate), formatter: dateFormatter) let endX = ChartAxisValueDate(date: entry.endDate, formatter: dateFormatter) let zero = ChartAxisValueInt(0) let rate = entry.netBasalUnitsPerHour @@ -158,19 +175,20 @@ public extension DoseChart { } else { valuePoints = [] } + + basalFillPoints += [ChartPoint(x: startX, y: zero)] + valuePoints + [ChartPoint(x: endX, y: zero)] + + if entry.startDate > startDate { + basalPoints += [ChartPoint(x: startX, y: zero)] + } + basalPoints += valuePoints + [ChartPoint(x: endX, y: zero)] - basalDosePoints += [ - ChartPoint(x: startX, y: zero) - ] + valuePoints + [ - ChartPoint(x: endX, y: zero) - ] - - allDosePoints += valuePoints + highlightPoints += valuePoints } } - - self.basalDosePoints = basalDosePoints - self.bolusDosePoints = bolusDosePoints - self.allDosePoints = allDosePoints + + let pointsCache = DosePointsCache(basal: basalPoints, basalFill: basalFillPoints, bolus: bolusPoints, highlight: highlightPoints) + self.pointsCache = pointsCache + return pointsCache } } diff --git a/Scripts/make_scenario.py b/Scripts/make_scenario.py index 103fd33cf2..0d7949b78f 100644 --- a/Scripts/make_scenario.py +++ b/Scripts/make_scenario.py @@ -110,7 +110,9 @@ def make_glucose_values(): def make_basal_doses(): return [ - BasalDose(1.0, hours(-0.5), hours(0.5)), + BasalDose(1.2, hours(-1.5), hours(0.5)), + BasalDose(0.9, hours(-1.0), hours(0.5)), + BasalDose(0.8, hours(-0.5), hours(0.5)) ]