ChartsManager.swift 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. //
  2. // Chart.swift
  3. // Naterade
  4. //
  5. // Created by Nathan Racklyeft on 2/19/16.
  6. // Copyright © 2016 Nathan Racklyeft. All rights reserved.
  7. //
  8. import Foundation
  9. import HealthKit
  10. import LoopKit
  11. import SwiftCharts
  12. import UIKit
  13. open class ChartsManager {
  14. private lazy var timeFormatter: DateFormatter = {
  15. let formatter = DateFormatter()
  16. let dateFormat = DateFormatter.dateFormat(fromTemplate: "j", options: 0, locale: Locale.current)!
  17. let isAmPmTimeFormat = dateFormat.firstIndex(of: "a") != nil
  18. formatter.dateFormat = isAmPmTimeFormat
  19. ? "h a"
  20. : "H:mm"
  21. return formatter
  22. }()
  23. public init(
  24. colors: ChartColorPalette,
  25. settings: ChartSettings,
  26. axisLabelFont: UIFont = .systemFont(ofSize: 14), // caption1, but hard-coded until axis can scale with type preference
  27. charts: [ChartProviding],
  28. traitCollection: UITraitCollection
  29. ) {
  30. self.colors = colors
  31. self.chartSettings = settings
  32. self.charts = charts
  33. self.traitCollection = traitCollection
  34. self.chartsCache = Array(repeating: nil, count: charts.count)
  35. axisLabelSettings = ChartLabelSettings(font: axisLabelFont, fontColor: colors.axisLabel)
  36. guideLinesLayerSettings = ChartGuideLinesLayerSettings(linesColor: colors.grid)
  37. }
  38. // MARK: - Configuration
  39. private let colors: ChartColorPalette
  40. private let chartSettings: ChartSettings
  41. private let labelsWidthY: CGFloat = 30
  42. public let charts: [ChartProviding]
  43. /// The amount of horizontal space reserved for fixed margins
  44. public var fixedHorizontalMargin: CGFloat {
  45. return chartSettings.leading + chartSettings.trailing + labelsWidthY + chartSettings.labelsToAxisSpacingY
  46. }
  47. private let axisLabelSettings: ChartLabelSettings
  48. private let guideLinesLayerSettings: ChartGuideLinesLayerSettings
  49. public var gestureRecognizer: UIGestureRecognizer?
  50. // MARK: - UITraitEnvironment
  51. public var traitCollection: UITraitCollection
  52. public func didReceiveMemoryWarning() {
  53. for chart in charts {
  54. chart.didReceiveMemoryWarning()
  55. }
  56. xAxisValues = nil
  57. }
  58. // MARK: - Data
  59. /// The earliest date on the X-axis
  60. public var startDate = Date() {
  61. didSet {
  62. if startDate != oldValue {
  63. xAxisValues = nil
  64. // Set a new minimum end date
  65. endDate = startDate.addingTimeInterval(.hours(3))
  66. }
  67. }
  68. }
  69. /// The latest date on the X-axis
  70. private var endDate = Date() {
  71. didSet {
  72. if endDate != oldValue {
  73. xAxisValues = nil
  74. }
  75. }
  76. }
  77. /// The latest allowed date on the X-axis
  78. public var maxEndDate = Date.distantFuture {
  79. didSet {
  80. endDate = min(endDate, maxEndDate)
  81. }
  82. }
  83. /// Updates the endDate using a new candidate date
  84. ///
  85. /// Dates are rounded up to the next hour.
  86. ///
  87. /// - Parameter date: The new candidate date
  88. public func updateEndDate(_ date: Date) {
  89. if date > endDate {
  90. let components = DateComponents(minute: 0)
  91. endDate = min(
  92. maxEndDate,
  93. Calendar.current.nextDate(
  94. after: date,
  95. matching: components,
  96. matchingPolicy: .strict,
  97. direction: .forward
  98. ) ?? date
  99. )
  100. }
  101. }
  102. // MARK: - State
  103. private var xAxisValues: [ChartAxisValue]? {
  104. didSet {
  105. if let xAxisValues = xAxisValues, xAxisValues.count > 1 {
  106. xAxisModel = ChartAxisModel(axisValues: xAxisValues, lineColor: colors.axisLine, labelSpaceReservationMode: .fixed(20))
  107. } else {
  108. xAxisModel = nil
  109. }
  110. chartsCache.replaceAllElements(with: nil)
  111. }
  112. }
  113. private var xAxisModel: ChartAxisModel?
  114. private var chartsCache: [Chart?]
  115. // MARK: - Generators
  116. public func chart(atIndex index: Int, frame: CGRect) -> Chart? {
  117. if let chart = chartsCache[index], chart.frame != frame {
  118. chartsCache[index] = nil
  119. }
  120. if chartsCache[index] == nil, let xAxisModel = xAxisModel, let xAxisValues = xAxisValues {
  121. chartsCache[index] = charts[index].generate(withFrame: frame, xAxisModel: xAxisModel, xAxisValues: xAxisValues, axisLabelSettings: axisLabelSettings, guideLinesLayerSettings: guideLinesLayerSettings, colors: colors, chartSettings: chartSettings, labelsWidthY: labelsWidthY, gestureRecognizer: gestureRecognizer, traitCollection: traitCollection)
  122. }
  123. return chartsCache[index]
  124. }
  125. public func invalidateChart(atIndex index: Int) {
  126. chartsCache[index] = nil
  127. }
  128. // MARK: - Shared Axis
  129. private func generateXAxisValues() {
  130. if let endDate = charts.compactMap({ $0.endDate }).max() {
  131. updateEndDate(endDate)
  132. }
  133. let points = [
  134. ChartPoint(
  135. x: ChartAxisValueDate(date: startDate, formatter: timeFormatter),
  136. y: ChartAxisValue(scalar: 0)
  137. ),
  138. ChartPoint(
  139. x: ChartAxisValueDate(date: endDate, formatter: timeFormatter),
  140. y: ChartAxisValue(scalar: 0)
  141. )
  142. ]
  143. let segments = ceil(endDate.timeIntervalSince(startDate).hours)
  144. let xAxisValues = ChartAxisValuesStaticGenerator.generateXAxisValuesWithChartPoints(points,
  145. minSegmentCount: segments - 1,
  146. maxSegmentCount: segments + 1,
  147. multiple: TimeInterval(hours: 1),
  148. axisValueGenerator: {
  149. ChartAxisValueDate(
  150. date: ChartAxisValueDate.dateFromScalar($0),
  151. formatter: timeFormatter,
  152. labelSettings: self.axisLabelSettings
  153. )
  154. },
  155. addPaddingSegmentIfEdge: false
  156. )
  157. xAxisValues.first?.hidden = true
  158. xAxisValues.last?.hidden = true
  159. self.xAxisValues = xAxisValues
  160. }
  161. /// Runs any necessary steps before rendering charts
  162. public func prerender() {
  163. if xAxisValues == nil {
  164. generateXAxisValues()
  165. }
  166. }
  167. }
  168. fileprivate extension Array {
  169. mutating func replaceAllElements(with element: Element) {
  170. self = Array(repeating: element, count: count)
  171. }
  172. }
  173. public protocol ChartProviding {
  174. /// Instructs the chart to clear its non-critical resources like caches
  175. func didReceiveMemoryWarning()
  176. /// The last date represented in the chart data
  177. var endDate: Date? { get }
  178. /// Creates a chart from the current data
  179. ///
  180. /// - Returns: A new chart object
  181. func generate(withFrame frame: CGRect,
  182. xAxisModel: ChartAxisModel,
  183. xAxisValues: [ChartAxisValue],
  184. axisLabelSettings: ChartLabelSettings,
  185. guideLinesLayerSettings: ChartGuideLinesLayerSettings,
  186. colors: ChartColorPalette,
  187. chartSettings: ChartSettings,
  188. labelsWidthY: CGFloat,
  189. gestureRecognizer: UIGestureRecognizer?,
  190. traitCollection: UITraitCollection
  191. ) -> Chart
  192. }