Skip to content

Commit

Permalink
Merge pull request #65 from florianpreknya/BasicStats
Browse files Browse the repository at this point in the history
Daily stats feature
  • Loading branch information
dhermanns authored Mar 30, 2019
2 parents 3d86932 + bc13d5d commit 896aaf9
Show file tree
Hide file tree
Showing 25 changed files with 2,107 additions and 234 deletions.
3 changes: 2 additions & 1 deletion nightguard WatchKit Extension/ChartPainter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,8 @@ class ChartPainter {
}

fileprivate func durationIsMoreThan6Hours(_ minTimestamp : Double, maxTimestamp : Double) -> Bool {
return maxTimestamp - minTimestamp > 6 * 60 * 60 * 1000
let sixHours = Double(6 * 60 * 60 * 1000)
return (maxTimestamp - minTimestamp) > sixHours
}

fileprivate func paintEverySecondHour(_ context : CGContext, attrs : [NSAttributedStringKey : Any]) {
Expand Down
3 changes: 3 additions & 0 deletions nightguard WatchKit Extension/UserDefaultsRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ class UserDefaultsRepository {
static let volumeKeysOnAlertSnoozeOption = UserDefaultsValue<QuickSnoozeOption>(key: "volumeKeysOnAlertSnoozeOption", default: .doNothing)
#endif

// show/hide stats
static let showStats = UserDefaultsValue<Bool>(key: "showStats", default: true)

/* Parses the URI entered in the UI and extracts the token if one is present. */
fileprivate static func parseBaseUri() {
url = nil
Expand Down
88 changes: 78 additions & 10 deletions nightguard.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

101 changes: 101 additions & 0 deletions nightguard/A1cView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
//
// A1cView.swift
// nightguard
//
// Created by Florian Preknya on 3/18/19.
// Copyright © 2019 private. All rights reserved.
//

import UIKit

extension UIColor {

// Creates a color-meter like UIColor by placing the given value in correspondence with its good-bad range, resulting a green color if the value is very good on that scale, a red color if is very bad or yellow & compositions with red-green if is in between
static func redYellowGreen(for value: CGFloat, bestValue: CGFloat, worstValue: CGFloat) -> UIColor {
if bestValue < worstValue {
let power = max(min((worstValue - CGFloat(value)) / (worstValue - bestValue), 1), 0)
return UIColor(red: 1 - power, green: power, blue: 0, alpha: 1)
} else {
let power = max(min((bestValue - CGFloat(value)) / (bestValue - worstValue), 1), 0)
return UIColor(red: power, green: 1 - power, blue: 0, alpha: 1)
}
}
}

/**
A stats view that displays the A1c value, the average BG value, standard deviation & variation values. It appreciates the values by giving a colored feedback to user: green - good values, yellow - okish, red - pretty bad.
*/
class A1cView: BasicStatsControl {

override func createPages() -> [StatsPage] {
return [
StatsPage(name: "A1c", formattedValue: model?.formattedA1c),
StatsPage(name: "Average", formattedValue: model?.formattedAverageGlucose?.replacingOccurrences(of: " ", with: "\n")),
StatsPage(name: "Std Deviation", formattedValue: model?.formattedStandardDeviation?.replacingOccurrences(of: " ", with: "\n")),
StatsPage(name: "Coefficient of Variation", formattedValue: model?.formattedCoefficientOfVariation)
]
}

fileprivate var a1cColor: UIColor? {

guard let a1c = model?.a1c else {
return nil
}

return UIColor.redYellowGreen(for: CGFloat(a1c), bestValue: 5.5, worstValue: 8.5)
}

fileprivate var variationColor: UIColor? {

guard let coefficientOfVariation = model?.coefficientOfVariation else {
return nil
}

return UIColor.redYellowGreen(for: CGFloat(coefficientOfVariation), bestValue: 0.3, worstValue: 0.5)
}

fileprivate var modelColor: UIColor? {
return (currentPageIndex < 2) ? a1cColor : variationColor
}

override func commonInit() {
super.commonInit()

diagramView.dataSource = self
// diagramView.separatorWidh = 8
// diagramView.separatorColor = .black
// diagramView.startAngle = .pi * 0.75
// diagramView.endAngle = 2 * .pi + .pi * 0.75
}

override func modelWasSet() {
super.modelWasSet()
diagramView.backgroundColor = modelColor?.withAlphaComponent(0.1)
}

override func pageChanged() {
super.pageChanged()
diagramView.backgroundColor = modelColor?.withAlphaComponent(0.1)
}
}

extension A1cView: SMDiagramViewDataSource {

@objc func numberOfSegmentsIn(diagramView: SMDiagramView) -> Int {
return 2
}

func diagramView(_ diagramView: SMDiagramView, colorForSegmentAtIndex index: NSInteger, angle: CGFloat) -> UIColor? {
// return (index == 1) ? a1cColor : variationColor
return modelColor
}

func diagramView(_ diagramView: SMDiagramView, radiusForSegmentAtIndex index: NSInteger, proportion: CGFloat, angle: CGFloat) -> CGFloat {
return (diagramView.frame.size.height - 2/*diagramView.arcWidth*/) / 2
}

func diagramView(_ diagramView: SMDiagramView, lineWidthForSegmentAtIndex index: NSInteger, angle: CGFloat) -> CGFloat {
//not called for SMDiagramViewModeSegment
return 2.0
}
}
30 changes: 30 additions & 0 deletions nightguard/AlarmRule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,19 @@ class AlarmRule {
static var isSmartSnoozeEnabled = UserDefaultsValue<Bool>(key: "smartSnoozeEnabled", default: true)
.group(UserDefaultsValueGroups.GroupNames.watchSync)
.group(UserDefaultsValueGroups.GroupNames.alarm)

static var isPersistentHighEnabled = UserDefaultsValue<Bool>(key: "persistentHighEnabled", default: false)
.group(UserDefaultsValueGroups.GroupNames.watchSync)
.group(UserDefaultsValueGroups.GroupNames.alarm)

static var persistentHighMinutes = UserDefaultsValue<Int>(key: "persistentHighMinutes", default: 30)
.group(UserDefaultsValueGroups.GroupNames.watchSync)
.group(UserDefaultsValueGroups.GroupNames.alarm)

static var persistentHighUpperBound = UserDefaultsValue<Float>(key: "persistentHighUpperBound", default: 250)
.group(UserDefaultsValueGroups.GroupNames.watchSync)
.group(UserDefaultsValueGroups.GroupNames.alarm)

/*
* Returns true if the alarm should be played.
* Snooze is true if the Alarm has been manually deactivated.
Expand Down Expand Up @@ -130,6 +142,24 @@ class AlarmRule {
}

if isTooHigh {

if isPersistentHighEnabled.value {
if currentReading.value < persistentHighUpperBound.value {

// if all the previous readings (for the defined minutes are high, we'll consider it a persistent high)
let lastReadings = bloodValues.lastXMinutes(persistentHighMinutes.value)

// we should have at least a reading in 10 minutes for considering a persistent high
if !lastReadings.isEmpty && (lastReadings.count >= (persistentHighMinutes.value / 10)) {
if lastReadings.allSatisfy({ AlarmRule.isTooHigh($0.value) }) {
return "Persistent High BG"
} else {
return nil
}
}
}
}

return "High BG"
} else if isTooLow {
return "Low BG"
Expand Down
56 changes: 24 additions & 32 deletions nightguard/AlarmViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ class AlarmViewController: CustomFormViewController {
fileprivate let MAX_ALERT_BELOW_VALUE : Float = 200
fileprivate let MIN_ALERT_BELOW_VALUE : Float = 50

fileprivate let SNAP_INCREMENT : Float = 10 // or change it to 5?

override var supportedInterfaceOrientations : UIInterfaceOrientationMask {
return UIInterfaceOrientationMask.portrait
}
Expand All @@ -39,10 +37,10 @@ class AlarmViewController: CustomFormViewController {

override func constructForm() {

aboveSliderRow = createSliderRow(initialValue: AlarmRule.alertIfAboveValue.value, minimumValue: MIN_ALERT_ABOVE_VALUE, maximumValue: MAX_ALERT_ABOVE_VALUE)
aboveSliderRow = SliderRow.glucoseLevelSlider(initialValue: AlarmRule.alertIfAboveValue.value, minimumValue: MIN_ALERT_ABOVE_VALUE, maximumValue: MAX_ALERT_ABOVE_VALUE)
aboveSliderRow.cell.slider.addTarget(self, action: #selector(onSliderValueChanged(slider:event:)), for: .valueChanged)

belowSliderRow = createSliderRow(initialValue: AlarmRule.alertIfBelowValue.value, minimumValue: MIN_ALERT_BELOW_VALUE, maximumValue: MAX_ALERT_BELOW_VALUE)
belowSliderRow = SliderRow.glucoseLevelSlider(initialValue: AlarmRule.alertIfBelowValue.value, minimumValue: MIN_ALERT_BELOW_VALUE, maximumValue: MAX_ALERT_BELOW_VALUE)
belowSliderRow.cell.slider.addTarget(self, action: #selector(onSliderValueChanged(slider:event:)), for: .valueChanged)


Expand Down Expand Up @@ -89,6 +87,28 @@ class AlarmViewController: CustomFormViewController {
}
}

<<< ButtonRowWithDynamicDetails("Persistent High") { row in
row.controllerProvider = { return PersistentHighViewController() }
row.detailTextProvider = {

let urgentHighInMgdl = AlarmRule.persistentHighUpperBound.value
let urgentHigh = UnitsConverter.toDisplayUnits("\(urgentHighInMgdl)")
let units = UserDefaultsRepository.units.value.description
let urgentHighWithUnits = "\(urgentHigh) \(units)"

if AlarmRule.isPersistentHighEnabled.value {
if #available(iOS 11.0, *) {
return "Alerts when BG remains high for more than \(AlarmRule.persistentHighMinutes.value) minutes or exceeds the urgent high value (\(urgentHighWithUnits))."
} else {
// single line, as iOS 10 doesn't expand cell for more lines
return "\(AlarmRule.persistentHighMinutes.value) minutes (< \(urgentHighWithUnits))"
}
} else {
return "Off"
}
}
}

<<< ButtonRowWithDynamicDetails("Low Prediction") { row in
row.controllerProvider = { return LowPredictionViewController() }
row.detailTextProvider = {
Expand Down Expand Up @@ -174,34 +194,6 @@ class AlarmViewController: CustomFormViewController {
}
}

private func createSliderRow(initialValue: Float, minimumValue: Float, maximumValue: Float) -> SliderRow {

return SliderRow() { row in
row.value = Float(UnitsConverter.toDisplayUnits("\(initialValue)"))!
}.cellSetup { [weak self] cell, row in
guard let self = self else { return }
// row.shouldHideValue = true

let minimumValue = Float(UnitsConverter.toDisplayUnits("\(minimumValue)"))!
let maximumValue = Float(UnitsConverter.toDisplayUnits("\(maximumValue)"))!
let snapIncrement = (UserDefaultsRepository.units.value == .mgdl) ? self.SNAP_INCREMENT : 0.1

let steps = (maximumValue - minimumValue) / snapIncrement
row.steps = UInt(steps.rounded())
cell.slider.minimumValue = minimumValue
cell.slider.maximumValue = maximumValue
row.displayValueFor = { value in
guard let value = value else { return "" }
let units = UserDefaultsRepository.units.value.description
return String("\(value.cleanValue) \(units)")
}

// fixed width for value label
let widthConstraint = NSLayoutConstraint(item: cell.valueLabel, attribute: NSLayoutConstraint.Attribute.width, relatedBy: NSLayoutConstraint.Relation.equal, toItem: nil, attribute: NSLayoutConstraint.Attribute.notAnAttribute, multiplier: 1, constant: 96)
cell.valueLabel.addConstraints([widthConstraint])
}
}

private func alertInvalidChange(message: String) {
let alertController = UIAlertController(title: "Invalid change", message: message, preferredStyle: .alert)
let actionOk = UIAlertAction(title: "OK", style: .default, handler: nil)
Expand Down
Loading

0 comments on commit 896aaf9

Please sign in to comment.