ChartsManager.swift 7.1 KB

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