forked from LoopKit/LoopKit
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathDoseChart.swift
More file actions
210 lines (169 loc) · 8.36 KB
/
DoseChart.swift
File metadata and controls
210 lines (169 loc) · 8.36 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
//
// DoseChart.swift
// LoopUI
//
// Copyright © 2019 LoopKit Authors. All rights reserved.
//
import Foundation
import LoopKit
import SwiftCharts
import UIKit
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
}
}
private var pointsCache: DosePointsCache? {
didSet {
if let pointsCache = pointsCache {
if let lastDate = pointsCache.highlight.last?.x as? ChartAxisValueDate {
endDate = lastDate.date
}
}
}
}
/// The minimum range to display for insulin values.
private let doseDisplayRangePoints: [ChartPoint] = [0, 1].map {
return ChartPoint(
x: ChartAxisValue(scalar: 0),
y: ChartAxisValueInt($0)
)
}
public private(set) var endDate: Date?
private var doseChartCache: ChartPointsTouchHighlightLayerViewCache?
}
public extension DoseChart {
func didReceiveMemoryWarning() {
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.generateYAxisValuesUsingLinearSegmentStep(
chartPoints: 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))
let coordsSpace = ChartCoordsSpaceLeftBottomSingleAxis(chartSettings: chartSettings, chartFrame: frame, xModel: xAxisModel, yModel: yAxisModel)
let (xAxisLayer, yAxisLayer, innerFrame) = (coordsSpace.xAxisLayer, coordsSpace.yAxisLayer, coordsSpace.chartInnerFrame)
// The dose area
let lineModel = ChartLineModel(chartPoints: points.basal, lineColor: colors.insulinTint, 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: points.basalFill,
fillColor: colors.insulinTint.withAlphaComponent(0.5),
createContainerPoints: false
)]
)
// bolus points
let bolusPointSize: Double = 12
let bolusLayer: ChartPointsScatterDownTrianglesLayer<ChartPoint>?
if points.bolus.count > 0 {
bolusLayer = ChartPointsScatterDownTrianglesLayer(xAxis: xAxisLayer.axis, yAxis: yAxisLayer.axis, chartPoints: points.bolus, displayDelay: 0, itemSize: CGSize(width: bolusPointSize, height: bolusPointSize), itemFillColor: colors.insulinTint)
} else {
bolusLayer = nil
}
// 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 = colors.insulinTint.cgColor
return v
})
if gestureRecognizer != nil {
doseChartCache = ChartPointsTouchHighlightLayerViewCache(
xAxisLayer: xAxisLayer,
yAxisLayer: yAxisLayer,
axisLabelSettings: axisLabelSettings,
chartPoints: points.highlight,
tintColor: colors.insulinTint,
gestureRecognizer: gestureRecognizer
)
}
let layers: [ChartLayer?] = [
gridLayer,
xAxisLayer,
yAxisLayer,
zeroGuidelineLayer,
doseChartCache?.highlightLayer,
doseArea,
doseLine,
bolusLayer
]
let chart = Chart(frame: frame, innerFrame: innerFrame, settings: chartSettings, layers: layers.compactMap { $0 })
// the bolus points are drawn in the chart's drawersContentView. Update the drawersContentView frame to allow the bolus points to be drawn without clipping
var frame = chart.drawersContentView.frame
frame.size.height = frame.height+CGFloat(bolusPointSize/2)
chart.drawersContentView.frame = frame.offsetBy(dx: 0, dy: -CGFloat(bolusPointSize/2))
return chart
}
private func generateDosePoints(startDate: Date) -> DosePointsCache {
guard pointsCache == nil else {
return pointsCache!
}
let dateFormatter = DateFormatter(timeStyle: .short)
let doseFormatter = NumberFormatter.dose
var basalPoints = [ChartPoint]()
var basalFillPoints = [ChartPoint]()
var bolusPoints = [ChartPoint]()
var highlightPoints = [ChartPoint]()
for entry in doseEntries {
let time = entry.endDate.timeIntervalSince(entry.startDate)
if entry.type == .bolus && entry.netBasalUnits > 0 {
let x = ChartAxisValueDate(date: entry.startDate, formatter: dateFormatter)
let y = ChartAxisValueDoubleLog(actualDouble: entry.unitsInDeliverableIncrements, unitString: "U", formatter: doseFormatter)
let point = ChartPoint(x: x, y: y)
bolusPoints.append(point)
highlightPoints.append(point)
} else if time > 0 {
// TODO: Display the DateInterval
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
let value = ChartAxisValueDoubleLog(actualDouble: rate, unitString: "U/hour", formatter: doseFormatter)
let valuePoints: [ChartPoint]
if abs(rate) > .ulpOfOne {
valuePoints = [
ChartPoint(x: startX, y: value),
ChartPoint(x: endX, y: value)
]
} 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)]
highlightPoints += valuePoints
}
}
let pointsCache = DosePointsCache(basal: basalPoints, basalFill: basalFillPoints, bolus: bolusPoints, highlight: highlightPoints)
self.pointsCache = pointsCache
return pointsCache
}
}