| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241 |
- //
- // Chart.swift
- // Naterade
- //
- // Created by Nathan Racklyeft on 2/19/16.
- // Copyright © 2016 Nathan Racklyeft. All rights reserved.
- //
- import Foundation
- import HealthKit
- import LoopKit
- import SwiftCharts
- import UIKit
- open class ChartsManager {
- private lazy var timeFormatter: DateFormatter = {
- let formatter = DateFormatter()
- let dateFormat = DateFormatter.dateFormat(fromTemplate: "j", options: 0, locale: Locale.current)!
- let isAmPmTimeFormat = dateFormat.firstIndex(of: "a") != nil
- formatter.dateFormat = isAmPmTimeFormat
- ? "h a"
- : "H:mm"
- return formatter
- }()
- public init(
- colors: ChartColorPalette,
- settings: ChartSettings,
- axisLabelFont: UIFont = .systemFont(ofSize: 14), // caption1, but hard-coded until axis can scale with type preference
- charts: [ChartProviding],
- traitCollection: UITraitCollection
- ) {
- self.colors = colors
- self.chartSettings = settings
- self.charts = charts
- self.traitCollection = traitCollection
- self.chartsCache = Array(repeating: nil, count: charts.count)
- axisLabelSettings = ChartLabelSettings(font: axisLabelFont, fontColor: colors.axisLabel)
- guideLinesLayerSettings = ChartGuideLinesLayerSettings(linesColor: colors.grid)
- }
- // MARK: - Configuration
- private let colors: ChartColorPalette
- private let chartSettings: ChartSettings
- private let labelsWidthY: CGFloat = 30
- public let charts: [ChartProviding]
- /// The amount of horizontal space reserved for fixed margins
- public var fixedHorizontalMargin: CGFloat {
- return chartSettings.leading + chartSettings.trailing + labelsWidthY + chartSettings.labelsToAxisSpacingY
- }
- private let axisLabelSettings: ChartLabelSettings
- private let guideLinesLayerSettings: ChartGuideLinesLayerSettings
- public var gestureRecognizer: UIGestureRecognizer?
- // MARK: - UITraitEnvironment
- public var traitCollection: UITraitCollection
- public func didReceiveMemoryWarning() {
- for chart in charts {
- chart.didReceiveMemoryWarning()
- }
- xAxisValues = nil
- }
- // MARK: - Data
- /// The earliest date on the X-axis
- public var startDate = Date() {
- didSet {
- if startDate != oldValue {
- xAxisValues = nil
- // Set a new minimum end date
- endDate = startDate.addingTimeInterval(.hours(3))
- }
- }
- }
- /// The latest date on the X-axis
- private var endDate = Date() {
- didSet {
- if endDate != oldValue {
- xAxisValues = nil
- }
- }
- }
- /// The latest allowed date on the X-axis
- public var maxEndDate = Date.distantFuture {
- didSet {
- endDate = min(endDate, maxEndDate)
- }
- }
- /// Updates the endDate using a new candidate date
- ///
- /// Dates are rounded up to the next hour.
- ///
- /// - Parameter date: The new candidate date
- public func updateEndDate(_ date: Date) {
- if date > endDate {
- let components = DateComponents(minute: 0)
- endDate = min(
- maxEndDate,
- Calendar.current.nextDate(
- after: date,
- matching: components,
- matchingPolicy: .strict,
- direction: .forward
- ) ?? date
- )
- }
- }
- // MARK: - State
- private var xAxisValues: [ChartAxisValue]? {
- didSet {
- if let xAxisValues = xAxisValues, xAxisValues.count > 1 {
- xAxisModel = ChartAxisModel(axisValues: xAxisValues, lineColor: colors.axisLine, labelSpaceReservationMode: .fixed(20))
- } else {
- xAxisModel = nil
- }
- chartsCache.replaceAllElements(with: nil)
- }
- }
- private var xAxisModel: ChartAxisModel?
- private var chartsCache: [Chart?]
- // MARK: - Generators
- public func chart(atIndex index: Int, frame: CGRect) -> Chart? {
- if let chart = chartsCache[index], chart.frame != frame {
- chartsCache[index] = nil
- }
- if chartsCache[index] == nil, let xAxisModel = xAxisModel, let xAxisValues = xAxisValues {
- 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)
- }
- return chartsCache[index]
- }
- public func invalidateChart(atIndex index: Int) {
- chartsCache[index] = nil
- }
- // MARK: - Shared Axis
- private func generateXAxisValues() {
- if let endDate = charts.compactMap({ $0.endDate }).max() {
- updateEndDate(endDate)
- }
- let points = [
- ChartPoint(
- x: ChartAxisValueDate(date: startDate, formatter: timeFormatter),
- y: ChartAxisValue(scalar: 0)
- ),
- ChartPoint(
- x: ChartAxisValueDate(date: endDate, formatter: timeFormatter),
- y: ChartAxisValue(scalar: 0)
- )
- ]
- let segments = ceil(endDate.timeIntervalSince(startDate).hours)
- let xAxisValues = ChartAxisValuesStaticGenerator.generateXAxisValuesWithChartPoints(points,
- minSegmentCount: segments - 1,
- maxSegmentCount: segments + 1,
- multiple: TimeInterval(hours: 1),
- axisValueGenerator: {
- ChartAxisValueDate(
- date: ChartAxisValueDate.dateFromScalar($0),
- formatter: timeFormatter,
- labelSettings: self.axisLabelSettings
- )
- },
- addPaddingSegmentIfEdge: false
- )
- xAxisValues.first?.hidden = true
- xAxisValues.last?.hidden = true
- self.xAxisValues = xAxisValues
- }
- /// Runs any necessary steps before rendering charts
- public func prerender() {
- if xAxisValues == nil {
- generateXAxisValues()
- }
- }
- }
- fileprivate extension Array {
- mutating func replaceAllElements(with element: Element) {
- self = Array(repeating: element, count: count)
- }
- }
- public protocol ChartProviding {
- /// Instructs the chart to clear its non-critical resources like caches
- func didReceiveMemoryWarning()
- /// The last date represented in the chart data
- var endDate: Date? { get }
- /// Creates a chart from the current data
- ///
- /// - Returns: A new chart object
- func generate(withFrame frame: CGRect,
- xAxisModel: ChartAxisModel,
- xAxisValues: [ChartAxisValue],
- axisLabelSettings: ChartLabelSettings,
- guideLinesLayerSettings: ChartGuideLinesLayerSettings,
- colors: ChartColorPalette,
- chartSettings: ChartSettings,
- labelsWidthY: CGFloat,
- gestureRecognizer: UIGestureRecognizer?,
- traitCollection: UITraitCollection
- ) -> Chart
- }
|