Chart.swift 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. // LoopFollow
  2. // Chart.swift
  3. import Charts
  4. import Foundation
  5. final class OverrideFillFormatter: FillFormatter {
  6. func getFillLinePosition(dataSet: Charts.LineChartDataSetProtocol, dataProvider _: Charts.LineChartDataProvider) -> CGFloat {
  7. return CGFloat(dataSet.entryForIndex(0)!.y)
  8. // return 375
  9. }
  10. }
  11. final class basalFillFormatter: FillFormatter {
  12. func getFillLinePosition(dataSet _: Charts.LineChartDataSetProtocol, dataProvider _: Charts.LineChartDataProvider) -> CGFloat {
  13. return 0
  14. }
  15. }
  16. final class ChartXValueFormatter: AxisValueFormatter {
  17. func stringForValue(_ value: Double, axis _: AxisBase?) -> String {
  18. let dateFormatter = DateFormatter()
  19. // let timezoneOffset = TimeZone.current.secondsFromGMT()
  20. // let epochTimezoneOffset = value + Double(timezoneOffset)
  21. if dateTimeUtils.is24Hour() {
  22. dateFormatter.setLocalizedDateFormatFromTemplate("HH:mm")
  23. } else {
  24. dateFormatter.setLocalizedDateFormatFromTemplate("hh:mm")
  25. }
  26. if Storage.shared.graphTimeZoneEnabled.value,
  27. let tz = TimeZone(identifier: Storage.shared.graphTimeZoneIdentifier.value)
  28. {
  29. dateFormatter.timeZone = tz
  30. }
  31. // let date = Date(timeIntervalSince1970: epochTimezoneOffset)
  32. let date = Date(timeIntervalSince1970: value)
  33. let formattedDate = dateFormatter.string(from: date)
  34. return formattedDate
  35. }
  36. }
  37. final class ChartYDataValueFormatter: ValueFormatter {
  38. func stringForValue(_: Double, entry: ChartDataEntry, dataSetIndex _: Int, viewPortHandler _: ViewPortHandler?) -> String {
  39. guard let text = entry.data as? String else { return "" }
  40. // Treatment entries store "label\r\rpillText" — extract only the label portion.
  41. if let range = text.range(of: "\r\r") {
  42. return String(text[..<range.lowerBound])
  43. }
  44. return text
  45. }
  46. }
  47. final class ChartYOverrideValueFormatter: ValueFormatter {
  48. func stringForValue(_: Double, entry: ChartDataEntry, dataSetIndex _: Int, viewPortHandler _: ViewPortHandler?) -> String {
  49. if entry.data != nil {
  50. return entry.data as? String ?? ""
  51. } else {
  52. return ""
  53. }
  54. }
  55. }
  56. final class ChartYMMOLValueFormatter: AxisValueFormatter {
  57. func stringForValue(_ value: Double, axis _: AxisBase?) -> String {
  58. return Localizer.toDisplayUnits(String(value))
  59. }
  60. }
  61. class PillMarker: MarkerImage {
  62. private(set) var color: UIColor
  63. private(set) var font: UIFont
  64. private(set) var textColor: UIColor
  65. private var labelText: String = ""
  66. private var attrs: [NSAttributedString.Key: AnyObject]!
  67. static let formatter: DateComponentsFormatter = {
  68. let f = DateComponentsFormatter()
  69. f.allowedUnits = [.minute, .second]
  70. f.unitsStyle = .short
  71. return f
  72. }()
  73. init(color: UIColor, font: UIFont, textColor: UIColor) {
  74. self.color = color
  75. self.font = font
  76. self.textColor = textColor
  77. let paragraphStyle = NSMutableParagraphStyle()
  78. paragraphStyle.alignment = .center
  79. attrs = [.font: font, .paragraphStyle: paragraphStyle, .foregroundColor: textColor, .baselineOffset: NSNumber(value: -4)]
  80. super.init()
  81. }
  82. override func draw(context: CGContext, point: CGPoint) {
  83. // custom padding around text
  84. let labelWidth = labelText.size(withAttributes: attrs).width + 10
  85. // if you modify labelHeigh you will have to tweak baselineOffset in attrs
  86. let labelHeight = labelText.size(withAttributes: attrs).height + 4
  87. // place pill above the marker, centered along x
  88. var rectangle = CGRect(x: point.x, y: point.y, width: labelWidth, height: labelHeight)
  89. rectangle.origin.x -= rectangle.width / 2.0
  90. var spacing: CGFloat = 20
  91. if point.y < 300 { spacing = -40 }
  92. rectangle.origin.y -= rectangle.height + spacing
  93. // rounded rect
  94. let clipPath = UIBezierPath(roundedRect: rectangle, cornerRadius: 6.0).cgPath
  95. context.addPath(clipPath)
  96. context.setFillColor(UIColor.secondarySystemBackground.cgColor)
  97. context.setStrokeColor(UIColor.label.cgColor)
  98. context.closePath()
  99. context.drawPath(using: .fillStroke)
  100. // add the text
  101. labelText.draw(with: rectangle, options: .usesLineFragmentOrigin, attributes: attrs, context: nil)
  102. }
  103. private static let pillSeparator = "\r\r"
  104. override func refreshContent(entry: ChartDataEntry, highlight _: Highlight) {
  105. if let text = entry.data as? String {
  106. // Treatment entries use pillSeparator to separate the value label from the marker text.
  107. if let range = text.range(of: Self.pillSeparator) {
  108. labelText = String(text[range.upperBound...])
  109. } else {
  110. labelText = text
  111. }
  112. } else {
  113. labelText = String(entry.y)
  114. }
  115. }
  116. private func customString(_ value: Double) -> String {
  117. let formattedString = PillMarker.formatter.string(from: TimeInterval(value))!
  118. // using this to convert the left axis values formatting, ie 2 min
  119. return "\(formattedString)"
  120. }
  121. }
  122. // MARK: - Cone of Uncertainty
  123. class ConeChartDataEntry: ChartDataEntry {
  124. var yMin: Double = 0.0
  125. var yMax: Double = 0.0
  126. required init() {
  127. super.init()
  128. }
  129. init(x: Double, yMin: Double, yMax: Double) {
  130. self.yMin = yMin
  131. self.yMax = yMax
  132. super.init(x: x, y: yMax)
  133. }
  134. override func copy(with _: NSZone? = nil) -> Any {
  135. let copy = ConeChartDataEntry(x: x, yMin: yMin, yMax: yMax)
  136. copy.data = data
  137. return copy
  138. }
  139. }
  140. class ConeOfUncertaintyRenderer: LineChartRenderer {
  141. let coneDataSetIndex: Int
  142. init(dataProvider: LineChartDataProvider?, animator: Animator?, viewPortHandler: ViewPortHandler?, coneDataSetIndex: Int) {
  143. self.coneDataSetIndex = coneDataSetIndex
  144. super.init(dataProvider: dataProvider!, animator: animator!, viewPortHandler: viewPortHandler!)
  145. }
  146. override func drawExtras(context: CGContext) {
  147. super.drawExtras(context: context)
  148. guard let dataProvider = dataProvider,
  149. dataProvider.lineData?.dataSets.count ?? 0 > coneDataSetIndex,
  150. let lineDataSet = dataProvider.lineData?.dataSets[coneDataSetIndex] as? LineChartDataSet,
  151. lineDataSet.entryCount > 1 else { return }
  152. let trans = dataProvider.getTransformer(forAxis: lineDataSet.axisDependency)
  153. let phaseY = animator.phaseY
  154. var upperPoints = [CGPoint]()
  155. var lowerPoints = [CGPoint]()
  156. for i in 0 ..< lineDataSet.entryCount {
  157. guard let entry = lineDataSet.entryForIndex(i) as? ConeChartDataEntry else { continue }
  158. upperPoints.append(trans.pixelForValues(x: entry.x, y: entry.yMax * phaseY))
  159. lowerPoints.append(trans.pixelForValues(x: entry.x, y: entry.yMin * phaseY))
  160. }
  161. guard upperPoints.count > 1 else { return }
  162. context.saveGState()
  163. let path = CGMutablePath()
  164. path.move(to: upperPoints[0])
  165. for i in 1 ..< upperPoints.count {
  166. path.addLine(to: upperPoints[i])
  167. }
  168. for i in stride(from: lowerPoints.count - 1, through: 0, by: -1) {
  169. path.addLine(to: lowerPoints[i])
  170. }
  171. path.closeSubpath()
  172. context.addPath(path)
  173. context.setFillColor(UIColor.systemBlue.withAlphaComponent(0.4).cgColor)
  174. context.fillPath()
  175. context.restoreGState()
  176. }
  177. }