| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222 |
- //
- // CarbEffectChart.swift
- // LoopUI
- //
- // Copyright © 2019 LoopKit Authors. All rights reserved.
- //
- import Foundation
- import LoopKit
- import SwiftCharts
- import UIKit
- public class CarbEffectChart: GlucoseChart, ChartProviding {
- /// The chart points for expected carb effect velocity
- public private(set) var carbEffectPoints: [ChartPoint] = [] {
- didSet {
- // don't extend the end date for carb effects
- }
- }
- /// The chart points for observed insulin counteraction effect velocity
- public private(set) var insulinCounteractionEffectPoints: [ChartPoint] = [] {
- didSet {
- // Extend 1 hour past the seen effect to ensure some future prediction is displayed
- if let lastDate = insulinCounteractionEffectPoints.last?.x as? ChartAxisValueDate {
- endDate = lastDate.date.addingTimeInterval(.hours(1))
- }
- }
- }
- /// The chart points used for selection in the carb effect chart
- public private(set) var allCarbEffectPoints: [ChartPoint] = []
- public private(set) var endDate: Date?
- private lazy var dateFormatter = DateFormatter(timeStyle: .short)
- private lazy var decimalFormatter = NumberFormatter.dose
- private var carbEffectChartCache: ChartPointsTouchHighlightLayerViewCache?
- }
- extension CarbEffectChart {
- public func didReceiveMemoryWarning() {
- carbEffectPoints = []
- insulinCounteractionEffectPoints = []
- allCarbEffectPoints = []
- carbEffectChartCache = nil
- }
- public func generate(withFrame frame: CGRect, xAxisModel: ChartAxisModel, xAxisValues: [ChartAxisValue], axisLabelSettings: ChartLabelSettings, guideLinesLayerSettings: ChartGuideLinesLayerSettings, colors: ChartColorPalette, chartSettings: ChartSettings, labelsWidthY: CGFloat, gestureRecognizer: UIGestureRecognizer?, traitCollection: UITraitCollection) -> Chart
- {
- /// The minimum range to display for carb effect values.
- let carbEffectDisplayRangePoints: [ChartPoint] = [0, glucoseUnit.chartableIncrement].map {
- return ChartPoint(
- x: ChartAxisValue(scalar: 0),
- y: ChartAxisValueDouble($0)
- )
- }
- let yAxisValues = ChartAxisValuesStaticGenerator.generateYAxisValuesWithChartPoints(carbEffectPoints + allCarbEffectPoints + carbEffectDisplayRangePoints,
- minSegmentCount: 2,
- maxSegmentCount: 4,
- multiple: glucoseUnit.chartableIncrement / 2,
- axisValueGenerator: {
- ChartAxisValueDouble($0, labelSettings: axisLabelSettings)
- },
- addPaddingSegmentIfEdge: false
- )
- let yAxisModel = ChartAxisModel(axisValues: yAxisValues, lineColor: colors.axisLine, labelSpaceReservationMode: .fixed(labelsWidthY))
- let coordsSpace = ChartCoordsSpaceLeftBottomSingleAxis(chartSettings: chartSettings, chartFrame: frame, xModel: xAxisModel, yModel: yAxisModel)
- let (xAxisLayer, yAxisLayer, innerFrame) = (coordsSpace.xAxisLayer, coordsSpace.yAxisLayer, coordsSpace.chartInnerFrame)
- let carbFillColor = colors.carbTint.withAlphaComponent(0.5)
- let carbBlendMode: CGBlendMode
- switch traitCollection.userInterfaceStyle {
- case .dark:
- carbBlendMode = .plusLighter
- case .light, .unspecified:
- carbBlendMode = .plusDarker
- @unknown default:
- carbBlendMode = .plusDarker
- }
- // Carb effect
- let effectsLayer = ChartPointsFillsLayer(
- xAxis: xAxisLayer.axis,
- yAxis: yAxisLayer.axis,
- fills: [
- ChartPointsFill(chartPoints: carbEffectPoints, fillColor: UIColor.secondaryLabel.withAlphaComponent(0.5)),
- ChartPointsFill(chartPoints: insulinCounteractionEffectPoints, fillColor: carbFillColor, blendMode: carbBlendMode)
- ]
- )
- // Grid lines
- let gridLayer = ChartGuideLinesForValuesLayer(
- xAxis: xAxisLayer.axis,
- yAxis: yAxisLayer.axis,
- settings: guideLinesLayerSettings,
- axisValuesX: Array(xAxisValues.dropFirst().dropLast()),
- axisValuesY: yAxisValues
- )
- // 0-line
- let dummyZeroChartPoint = ChartPoint(x: ChartAxisValueDouble(0), y: ChartAxisValueDouble(0))
- let zeroGuidelineLayer = ChartPointsViewsLayer(xAxis: xAxisLayer.axis, yAxis: yAxisLayer.axis, chartPoints: [dummyZeroChartPoint], viewGenerator: {(chartPointModel, layer, chart) -> UIView? in
- let width: CGFloat = 1
- let viewFrame = CGRect(x: chart.contentView.bounds.minX, y: chartPointModel.screenLoc.y - width / 2, width: chart.contentView.bounds.size.width, height: width)
- let v = UIView(frame: viewFrame)
- v.layer.backgroundColor = carbFillColor.cgColor
- return v
- })
- if gestureRecognizer != nil {
- carbEffectChartCache = ChartPointsTouchHighlightLayerViewCache(
- xAxisLayer: xAxisLayer,
- yAxisLayer: yAxisLayer,
- axisLabelSettings: axisLabelSettings,
- chartPoints: allCarbEffectPoints,
- tintColor: colors.carbTint,
- gestureRecognizer: gestureRecognizer
- )
- }
- let layers: [ChartLayer?] = [
- gridLayer,
- xAxisLayer,
- yAxisLayer,
- zeroGuidelineLayer,
- carbEffectChartCache?.highlightLayer,
- effectsLayer
- ]
- return Chart(
- frame: frame,
- innerFrame: innerFrame,
- settings: chartSettings,
- layers: layers.compactMap { $0 }
- )
- }
- }
- extension CarbEffectChart {
- /// Convert an array of GlucoseEffects (as glucose values) into glucose effect velocity (glucose/min) for charting
- ///
- /// - Parameter effects: A timeline of glucose values representing glucose change
- public func setCarbEffects(_ effects: [GlucoseEffect]) {
- let unit = glucoseUnit.unitDivided(by: .minute())
- let unitString = unit.unitString
- var lastDate = effects.first?.endDate
- var lastValue = effects.first?.quantity.doubleValue(for: glucoseUnit)
- let minuteInterval = 5.0
- var carbEffectPoints = [ChartPoint]()
- let zero = ChartAxisValueInt(0)
- for effect in effects.dropFirst() {
- let value = effect.quantity.doubleValue(for: glucoseUnit)
- let valuePerMinute = (value - lastValue!) / minuteInterval
- lastValue = value
- let startX = ChartAxisValueDate(date: lastDate!, formatter: dateFormatter)
- let endX = ChartAxisValueDate(date: effect.endDate, formatter: dateFormatter)
- lastDate = effect.endDate
- let valueY = ChartAxisValueDoubleUnit(valuePerMinute, unitString: unitString, formatter: decimalFormatter)
- carbEffectPoints += [
- ChartPoint(x: startX, y: zero),
- ChartPoint(x: startX, y: valueY),
- ChartPoint(x: endX, y: valueY),
- ChartPoint(x: endX, y: zero)
- ]
- }
- self.carbEffectPoints = carbEffectPoints
- }
- /// Charts glucose effect velocity
- ///
- /// - Parameter effects: A timeline of glucose velocity values
- public func setInsulinCounteractionEffects(_ effects: [GlucoseEffectVelocity]) {
- let unit = glucoseUnit.unitDivided(by: .minute())
- let unitString = String(format: NSLocalizedString("%1$@/min", comment: "Format string describing glucose units per minute (1: glucose unit string)"), glucoseUnit.shortLocalizedUnitString())
- var insulinCounteractionEffectPoints: [ChartPoint] = []
- var allCarbEffectPoints: [ChartPoint] = []
- let zero = ChartAxisValueInt(0)
- for effect in effects {
- let startX = ChartAxisValueDate(date: effect.startDate, formatter: dateFormatter)
- let endX = ChartAxisValueDate(date: effect.endDate, formatter: dateFormatter)
- let value = ChartAxisValueDoubleUnit(effect.quantity.doubleValue(for: unit), unitString: unitString, formatter: decimalFormatter)
- guard value.scalar != 0 else {
- continue
- }
- let valuePoint = ChartPoint(x: endX, y: value)
- insulinCounteractionEffectPoints += [
- ChartPoint(x: startX, y: zero),
- ChartPoint(x: startX, y: value),
- valuePoint,
- ChartPoint(x: endX, y: zero)
- ]
- allCarbEffectPoints.append(valuePoint)
- }
- self.insulinCounteractionEffectPoints = insulinCounteractionEffectPoints
- self.allCarbEffectPoints = allCarbEffectPoints
- }
- }
|