// // BaseChartRenderer.swift // GraphTest // // Created by Andrei Salavei on 4/7/19. // Copyright © 2019 Andrei Salavei. All rights reserved. // import Foundation #if os(macOS) import Cocoa #else import UIKit #endif public protocol ChartViewRenderer: AnyObject { var containerViews: [GView] { get set } func render(context: CGContext, bounds: CGRect, chartFrame: CGRect) } private let exponentialAnimationTrashold: CGFloat = 100 class BaseChartRenderer: ChartViewRenderer { var containerViews: [GView] = [] var optimizationLevel: CGFloat = 1 { didSet { setNeedsDisplay() } } var isEnabled: Bool = true { didSet { setNeedsDisplay() } } private(set) lazy var chartAlphaAnimator: AnimationController = { return AnimationController(current: 1, refreshClosure: self.refreshClosure) }() func setVisible(_ visible: Bool, animated: Bool) { let destinationValue: CGFloat = visible ? 1 : 0 guard self.chartAlphaAnimator.end != destinationValue else { return } if animated { self.chartAlphaAnimator.animate(to: destinationValue, duration: .defaultDuration) } else { self.chartAlphaAnimator.set(current: destinationValue) } } lazy var horizontalRange = AnimationController>(current: 0...1, refreshClosure: refreshClosure) lazy var verticalRange = AnimationController>(current: 0...1, refreshClosure: refreshClosure) func setup(verticalRange: ClosedRange, animated: Bool, timeFunction: TimeFunction? = nil) { guard self.verticalRange.end != verticalRange else { self.verticalRange.timeFunction = timeFunction ?? .linear return } if animated { let function: TimeFunction = .easeInOut // if let timeFunction = timeFunction { // function = timeFunction // } else if self.verticalRange.current.distance > 0 && verticalRange.distance > 0 { // if self.verticalRange.current.distance / verticalRange.distance > exponentialAnimationTrashold { // function = .easeIn // } else if verticalRange.distance / self.verticalRange.current.distance > exponentialAnimationTrashold { // function = .easeOut // } else { // function = .linear // } // } else { // function = .linear // } self.verticalRange.animate(to: verticalRange, duration: .defaultDuration, timeFunction: function) } else { self.verticalRange.set(current: verticalRange) } } func setup(horizontalRange: ClosedRange, animated: Bool) { guard self.horizontalRange.end != horizontalRange else { return } if animated { let animationCurve: TimeFunction = self.horizontalRange.current.distance > horizontalRange.distance ? .easeOut : .easeIn self.horizontalRange.animate(to: horizontalRange, duration: .defaultDuration, timeFunction: animationCurve) } else { self.horizontalRange.set(current: horizontalRange) } } func transform(toChartCoordinateHorizontal x: CGFloat, chartFrame: CGRect) -> CGFloat { return chartFrame.origin.x + (x - horizontalRange.current.lowerBound) / horizontalRange.current.distance * chartFrame.width } func transform(toChartCoordinateVertical y: CGFloat, chartFrame: CGRect) -> CGFloat { return chartFrame.height + chartFrame.origin.y - (y - verticalRange.current.lowerBound) / verticalRange.current.distance * chartFrame.height } func transform(toChartCoordinate point: CGPoint, chartFrame: CGRect) -> CGPoint { return CGPoint(x: transform(toChartCoordinateHorizontal: point.x, chartFrame: chartFrame), y: transform(toChartCoordinateVertical: point.y, chartFrame: chartFrame)) } func renderRange(bounds: CGRect, chartFrame: CGRect) -> ClosedRange { let lowerBound = horizontalRange.current.lowerBound - chartFrame.origin.x / chartFrame.width * horizontalRange.current.distance let upperBound = horizontalRange.current.upperBound + (bounds.width - chartFrame.width - chartFrame.origin.x) / chartFrame.width * horizontalRange.current.distance guard lowerBound <= upperBound else { print("Error: Unexpecated bounds range!") return 0...1 } return lowerBound...upperBound } func render(context: CGContext, bounds: CGRect, chartFrame: CGRect) { fatalError("abstract") } func setNeedsDisplay() { containerViews.forEach { $0.setNeedsDisplay($0.bounds) } } var refreshClosure: () -> Void { return { [weak self] in self?.setNeedsDisplay() } } }