Skip to content

Commit 34db8fc

Browse files
authored
LOOP-1489: migrate charts to LoopKitUI (#152)
1 parent 37a9bd8 commit 34db8fc

23 files changed

+1428
-9
lines changed

.circleci/config.yml

+50
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,31 @@ jobs:
1717
xcode: 11.5.0
1818
steps:
1919
- checkout
20+
- run:
21+
name: Homebrew + Carthage Setup
22+
command: |
23+
if ! [ -x "$(command -v brew)" ]; then
24+
# Install Homebrew
25+
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
26+
fi
27+
28+
if brew ls carthage > /dev/null; then
29+
brew upgrade carthage || echo "Continuing…"
30+
else
31+
brew install carthage
32+
fi
33+
- run:
34+
name: Carthage Bootstrap
35+
command: |
36+
echo "Bootstrapping carthage dependencies"
37+
unset LLVM_TARGET_TRIPLE_SUFFIX
38+
39+
if ! cmp -s Cartfile.Resolved Carthage/Cartfile.resolved; then
40+
time /usr/local/bin/carthage bootstrap --project-directory "$SRCROOT" --platform ios,watchos --cache-builds --verbose
41+
cp Cartfile.resolved Carthage
42+
else
43+
echo "Carthage: not bootstrapping"
44+
fi
2045
- run:
2146
name: Test
2247
command: |
@@ -30,6 +55,31 @@ jobs:
3055
xcode: 11.5.0
3156
steps:
3257
- checkout
58+
- run:
59+
name: Homebrew + Carthage Setup
60+
command: |
61+
if ! [ -x "$(command -v brew)" ]; then
62+
# Install Homebrew
63+
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
64+
fi
65+
66+
if brew ls carthage > /dev/null; then
67+
brew upgrade carthage || echo "Continuing…"
68+
else
69+
brew install carthage
70+
fi
71+
- run:
72+
name: Carthage Bootstrap
73+
command: |
74+
echo "Bootstrapping carthage dependencies"
75+
unset LLVM_TARGET_TRIPLE_SUFFIX
76+
77+
if ! cmp -s Cartfile.Resolved Carthage/Cartfile.resolved; then
78+
time /usr/local/bin/carthage bootstrap --project-directory "$SRCROOT" --platform ios,watchos --cache-builds --verbose
79+
cp Cartfile.resolved Carthage
80+
else
81+
echo "Carthage: not bootstrapping"
82+
fi
3383
- run:
3484
name: Build Example
3585
command: |

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,5 @@ fastlane/screenshots
6565
RemoteSettings.plist
6666

6767
.DS_Store
68+
69+
Carthage/

Cartfile

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
github "i-schuetz/SwiftCharts" == 0.6.5

Cartfile.resolved

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
github "i-schuetz/SwiftCharts" "0.6.5"

Extensions/HKUnit.swift

+9
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,13 @@ extension HKUnit {
4141

4242
return nil
4343
}
44+
45+
/// The smallest value expected to be visible on a chart
46+
var chartableIncrement: Double {
47+
if self == .milligramsPerDeciliter {
48+
return 1
49+
} else {
50+
return 1 / 25
51+
}
52+
}
4453
}

LoopKit.xcodeproj/project.pbxproj

+130-8
Large diffs are not rendered by default.

LoopKitUI/ChartColorPalette.swift

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
//
2+
// ChartColorPalette.swift
3+
// LoopKitUI
4+
//
5+
// Created by Bharat Mediratta on 3/29/17.
6+
// Copyright © 2017 LoopKit Authors. All rights reserved.
7+
//
8+
9+
import UIKit
10+
11+
/// A palette of colors for displaying charts
12+
public struct ChartColorPalette {
13+
public let axisLine: UIColor
14+
public let axisLabel: UIColor
15+
public let grid: UIColor
16+
public let glucoseTint: UIColor
17+
public let doseTint: UIColor
18+
19+
public init(axisLine: UIColor, axisLabel: UIColor, grid: UIColor, glucoseTint: UIColor, doseTint: UIColor) {
20+
self.axisLine = axisLine
21+
self.axisLabel = axisLabel
22+
self.grid = grid
23+
self.glucoseTint = glucoseTint
24+
self.doseTint = doseTint
25+
}
26+
27+
static var `default`: ChartColorPalette {
28+
return ChartColorPalette(axisLine: .axisLineColor, axisLabel: .axisLabelColor, grid: .gridColor, glucoseTint: .glucoseTintColor, doseTint: .doseTintColor)
29+
}
30+
}

LoopKitUI/Charts/GlucoseChart.swift

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
//
2+
// GlucoseChart.swift
3+
// LoopUI
4+
//
5+
// Copyright © 2019 LoopKit Authors. All rights reserved.
6+
//
7+
8+
import Foundation
9+
import HealthKit
10+
import LoopKit
11+
import SwiftCharts
12+
13+
14+
open class GlucoseChart {
15+
public init() {
16+
}
17+
18+
public var glucoseUnit: HKUnit = .milligramsPerDeciliter {
19+
didSet {
20+
if glucoseUnit != oldValue {
21+
// Regenerate the glucose display points
22+
let oldRange = glucoseDisplayRange
23+
glucoseDisplayRange = oldRange
24+
}
25+
}
26+
}
27+
28+
public var glucoseDisplayRange: ClosedRange<HKQuantity>? {
29+
didSet {
30+
if let range = glucoseDisplayRange {
31+
glucoseDisplayRangePoints = [
32+
ChartPoint(x: ChartAxisValue(scalar: 0), y: ChartAxisValueDouble(range.lowerBound.doubleValue(for: glucoseUnit))),
33+
ChartPoint(x: ChartAxisValue(scalar: 0), y: ChartAxisValueDouble(range.upperBound.doubleValue(for: glucoseUnit)))
34+
]
35+
} else {
36+
glucoseDisplayRangePoints = []
37+
}
38+
}
39+
}
40+
41+
public private(set) var glucoseDisplayRangePoints: [ChartPoint] = []
42+
43+
public func glucosePointsFromValues(_ glucoseValues: [GlucoseValue]) -> [ChartPoint] {
44+
let unitFormatter = QuantityFormatter()
45+
unitFormatter.unitStyle = .short
46+
unitFormatter.setPreferredNumberFormatter(for: glucoseUnit)
47+
let unitString = unitFormatter.string(from: glucoseUnit)
48+
let dateFormatter = DateFormatter(timeStyle: .short)
49+
50+
return glucoseValues.map {
51+
return ChartPoint(
52+
x: ChartAxisValueDate(date: $0.startDate, formatter: dateFormatter),
53+
y: ChartAxisValueDoubleUnit($0.quantity.doubleValue(for: glucoseUnit), unitString: unitString, formatter: unitFormatter.numberFormatter)
54+
)
55+
}
56+
}
57+
}
+140
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
//
2+
// InsulinModelChart.swift
3+
// LoopKitUI
4+
//
5+
// Copyright © 2019 LoopKit Authors. All rights reserved.
6+
//
7+
8+
import Foundation
9+
import LoopKit
10+
import SwiftCharts
11+
12+
13+
public class InsulinModelChart: GlucoseChart, ChartProviding {
14+
/// The chart points for the selected model
15+
public private(set) var selectedInsulinModelChartPoints: [ChartPoint] = [] {
16+
didSet {
17+
if let lastDate = selectedInsulinModelChartPoints.last?.x as? ChartAxisValueDate {
18+
updateEndDate(lastDate.date)
19+
}
20+
}
21+
}
22+
23+
public private(set) var unselectedInsulinModelChartPoints: [[ChartPoint]] = [] {
24+
didSet {
25+
for points in unselectedInsulinModelChartPoints {
26+
if let lastDate = points.last?.x as? ChartAxisValueDate {
27+
updateEndDate(lastDate.date)
28+
}
29+
}
30+
}
31+
}
32+
33+
public private(set) var endDate: Date?
34+
35+
private func updateEndDate(_ date: Date) {
36+
if endDate == nil || date > endDate! {
37+
self.endDate = date
38+
}
39+
}
40+
}
41+
42+
extension InsulinModelChart {
43+
public func didReceiveMemoryWarning() {
44+
45+
}
46+
47+
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
48+
{
49+
let yAxisValues = ChartAxisValuesStaticGenerator.generateYAxisValuesWithChartPoints(glucoseDisplayRangePoints,
50+
minSegmentCount: 2,
51+
maxSegmentCount: 5,
52+
multiple: glucoseUnit.chartableIncrement / 2,
53+
axisValueGenerator: {
54+
ChartAxisValueDouble(round($0), labelSettings: axisLabelSettings)
55+
},
56+
addPaddingSegmentIfEdge: false
57+
)
58+
59+
let yAxisModel = ChartAxisModel(axisValues: yAxisValues, lineColor: colors.axisLine, labelSpaceReservationMode: .fixed(labelsWidthY))
60+
61+
let coordsSpace = ChartCoordsSpaceLeftBottomSingleAxis(
62+
chartSettings: chartSettings,
63+
chartFrame: frame,
64+
xModel: xAxisModel,
65+
yModel: yAxisModel
66+
)
67+
68+
// Grid lines
69+
let gridLayer = ChartGuideLinesForValuesLayer(
70+
xAxis: coordsSpace.xAxisLayer.axis,
71+
yAxis: coordsSpace.yAxisLayer.axis,
72+
settings: guideLinesLayerSettings,
73+
axisValuesX: Array(xAxisValues.dropFirst().dropLast()),
74+
axisValuesY: yAxisValues
75+
)
76+
77+
// Selected line
78+
var selectedLayer: ChartLayer?
79+
80+
if selectedInsulinModelChartPoints.count > 1 {
81+
let lineModel = ChartLineModel.predictionLine(
82+
points: selectedInsulinModelChartPoints,
83+
color: colors.glucoseTint,
84+
width: 2
85+
)
86+
87+
selectedLayer = ChartPointsLineLayer(
88+
xAxis: coordsSpace.xAxisLayer.axis,
89+
yAxis: coordsSpace.yAxisLayer.axis,
90+
lineModels: [lineModel]
91+
)
92+
}
93+
94+
var unselectedLineModels = [ChartLineModel]()
95+
96+
for points in unselectedInsulinModelChartPoints where points.count > 1 {
97+
unselectedLineModels.append(ChartLineModel.predictionLine(
98+
points: points,
99+
color: UIColor.secondaryLabelColor,
100+
width: 1
101+
))
102+
}
103+
104+
// Unselected lines
105+
var unselectedLayer: ChartLayer?
106+
107+
if !unselectedLineModels.isEmpty {
108+
unselectedLayer = ChartPointsLineLayer(
109+
xAxis: coordsSpace.xAxisLayer.axis,
110+
yAxis: coordsSpace.yAxisLayer.axis,
111+
lineModels: unselectedLineModels
112+
)
113+
}
114+
115+
let layers: [ChartLayer?] = [
116+
gridLayer,
117+
coordsSpace.xAxisLayer,
118+
coordsSpace.yAxisLayer,
119+
unselectedLayer,
120+
selectedLayer
121+
]
122+
123+
return Chart(
124+
frame: frame,
125+
innerFrame: coordsSpace.chartInnerFrame,
126+
settings: chartSettings,
127+
layers: layers.compactMap { $0 }
128+
)
129+
}
130+
}
131+
132+
extension InsulinModelChart {
133+
public func setSelectedInsulinModelValues(_ values: [GlucoseValue]) {
134+
self.selectedInsulinModelChartPoints = glucosePointsFromValues(values)
135+
}
136+
137+
public func setUnselectedInsulinModelValues(_ values: [[GlucoseValue]]) {
138+
self.unselectedInsulinModelChartPoints = values.map(glucosePointsFromValues)
139+
}
140+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//
2+
// ChartLineModel.swift
3+
// Loop
4+
//
5+
// Copyright © 2017 LoopKit Authors. All rights reserved.
6+
//
7+
8+
import SwiftCharts
9+
10+
11+
extension ChartLineModel {
12+
/// Creates a model configured with the dashed prediction line style
13+
///
14+
/// - Parameters:
15+
/// - points: The points to construct the line
16+
/// - color: The line color
17+
/// - width: The line width
18+
/// - Returns: A new line model
19+
static func predictionLine(points: [T], color: UIColor, width: CGFloat) -> ChartLineModel {
20+
// TODO: Bug in ChartPointsLineLayer requires a non-zero animation to draw the dash pattern
21+
return self.init(chartPoints: points, lineColor: color, lineWidth: width, animDuration: 0.0001, animDelay: 0, dashPattern: [6, 5])
22+
}
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//
2+
// ChartSettings+LoopKitUI.swift
3+
// LoopKitUI
4+
//
5+
// Created by Anna Quinlan on 7/20/20.
6+
// Copyright © 2020 LoopKit Authors. All rights reserved.
7+
//
8+
9+
import SwiftCharts
10+
11+
12+
extension ChartSettings {
13+
static var `default`: ChartSettings {
14+
var settings = ChartSettings()
15+
settings.top = 12
16+
settings.bottom = 0
17+
settings.trailing = 8
18+
settings.axisTitleLabelsToLabelsSpacing = 0
19+
settings.labelsToAxisSpacingX = 6
20+
settings.clipInnerFrame = false
21+
return settings
22+
}
23+
}
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
//
2+
// DateFormatter.swift
3+
// LoopKitUI
4+
//
5+
// Created by Anna Quinlan on 7/7/20.
6+
// Copyright © 2020 LoopKit Authors. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
extension DateFormatter {
12+
convenience init(dateStyle: Style = .none, timeStyle: Style = .none) {
13+
self.init()
14+
self.dateStyle = dateStyle
15+
self.timeStyle = timeStyle
16+
}
17+
}

0 commit comments

Comments
 (0)