mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-23 22:55:00 +00:00
Channel statistics improvements
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
//
|
||||
// BaseChartController.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
|
||||
|
||||
|
||||
enum BaseConstants {
|
||||
static let defaultRange: ClosedRange<CGFloat> = 0...1
|
||||
static let minimumAxisYLabelsDistance: CGFloat = 90
|
||||
static let monthDayDateFormatter = DateFormatter.utc(format: "MMM d")
|
||||
static let timeDateFormatter = DateFormatter.utc(format: "HH:mm")
|
||||
static let headerFullRangeFormatter: DateFormatter = {
|
||||
let formatter = DateFormatter.utc()
|
||||
formatter.calendar = Calendar.utc
|
||||
formatter.dateStyle = .long
|
||||
return formatter
|
||||
}()
|
||||
static let headerMediumRangeFormatter: DateFormatter = {
|
||||
let formatter = DateFormatter.utc()
|
||||
formatter.dateStyle = .medium
|
||||
return formatter
|
||||
}()
|
||||
static let headerFullZoomedFormatter: DateFormatter = {
|
||||
let formatter = DateFormatter.utc()
|
||||
formatter.dateStyle = .full
|
||||
return formatter
|
||||
}()
|
||||
|
||||
static let verticalBaseAnchors: [CGFloat] = [8, 5, 2.5, 2, 1]
|
||||
static let defaultVerticalBaseAnchor: CGFloat = 1
|
||||
|
||||
static let mainChartLineWidth: CGFloat = 2
|
||||
static let previewChartLineWidth: CGFloat = 1
|
||||
|
||||
static let previewLinesChartOptimizationLevel: CGFloat = 1.5
|
||||
static let linesChartOptimizationLevel: CGFloat = 1.0
|
||||
static let barsChartOptimizationLevel: CGFloat = 0.75
|
||||
|
||||
static let defaultRangePresetLength = TimeInterval.day * 60
|
||||
|
||||
static let chartNumberFormatter: ScalesNumberFormatter = {
|
||||
let numberFormatter = ScalesNumberFormatter()
|
||||
numberFormatter.allowsFloats = true
|
||||
numberFormatter.numberStyle = .decimal
|
||||
numberFormatter.usesGroupingSeparator = true
|
||||
numberFormatter.groupingSeparator = " "
|
||||
numberFormatter.minimumIntegerDigits = 1
|
||||
numberFormatter.minimumFractionDigits = 0
|
||||
numberFormatter.maximumFractionDigits = 2
|
||||
return numberFormatter
|
||||
}()
|
||||
|
||||
static let detailsNumberFormatter: NumberFormatter = {
|
||||
let detailsNumberFormatter = NumberFormatter()
|
||||
detailsNumberFormatter.allowsFloats = false
|
||||
detailsNumberFormatter.numberStyle = .decimal
|
||||
detailsNumberFormatter.usesGroupingSeparator = true
|
||||
detailsNumberFormatter.groupingSeparator = " "
|
||||
return detailsNumberFormatter
|
||||
}()
|
||||
}
|
||||
|
||||
public class BaseChartController: GColorModeContainer {
|
||||
//let performanceRenderer = PerformanceRenderer()
|
||||
var initialChartsCollection: ChartsCollection
|
||||
var isZoomed: Bool = false
|
||||
|
||||
var chartTitle: String = ""
|
||||
|
||||
public init(chartsCollection: ChartsCollection) {
|
||||
self.initialChartsCollection = chartsCollection
|
||||
}
|
||||
|
||||
public var mainChartRenderers: [ChartViewRenderer] {
|
||||
fatalError("Abstract")
|
||||
}
|
||||
|
||||
public var navigationRenderers: [ChartViewRenderer] {
|
||||
fatalError("Abstract")
|
||||
}
|
||||
|
||||
public var cartViewBounds: (() -> CGRect) = { fatalError() }
|
||||
public var chartFrame: (() -> CGRect) = { fatalError() }
|
||||
|
||||
public func initializeChart() {
|
||||
fatalError("Abstract")
|
||||
}
|
||||
|
||||
public func chartInteractionDidBegin(point: CGPoint) {
|
||||
fatalError("Abstract")
|
||||
}
|
||||
|
||||
public func chartInteractionDidEnd() {
|
||||
fatalError("Abstract")
|
||||
}
|
||||
|
||||
public func cancelChartInteraction() {
|
||||
fatalError("Abstract")
|
||||
}
|
||||
|
||||
public func didTapZoomOut() {
|
||||
fatalError("Abstract")
|
||||
}
|
||||
|
||||
public func updateChartsVisibility(visibility: [Bool], animated: Bool) {
|
||||
fatalError("Abstract")
|
||||
}
|
||||
|
||||
public var currentHorizontalRange: ClosedRange<CGFloat> {
|
||||
fatalError("Abstract")
|
||||
}
|
||||
|
||||
public func height(for width: CGFloat) -> CGFloat {
|
||||
var height: CGFloat = 308
|
||||
|
||||
let items = actualChartsCollection.chartValues.map { value in
|
||||
return ChartVisibilityItem(title: value.name, color: value.color)
|
||||
}
|
||||
let frames = ChartVisibilityItem.generateItemsFrames(for: width, items: items)
|
||||
guard let lastFrame = frames.last else { return height }
|
||||
|
||||
height += lastFrame.maxY
|
||||
|
||||
return height
|
||||
}
|
||||
|
||||
public var isChartRangePagingEnabled: Bool = false
|
||||
public var minimumSelectedChartRange: CGFloat = 0.05
|
||||
public var chartRangePagingClosure: ((Bool, CGFloat) -> Void)? // isEnabled, PageSize
|
||||
public func setChartRangePagingEnabled(isEnabled: Bool, minimumSelectionSize: CGFloat) {
|
||||
isChartRangePagingEnabled = isEnabled
|
||||
minimumSelectedChartRange = minimumSelectionSize
|
||||
chartRangePagingClosure?(isChartRangePagingEnabled, minimumSelectedChartRange)
|
||||
}
|
||||
|
||||
public var chartRangeUpdatedClosure: ((ClosedRange<CGFloat>, Bool) -> Void)?
|
||||
public var currentChartHorizontalRangeFraction: ClosedRange<CGFloat> {
|
||||
fatalError("Abstract")
|
||||
}
|
||||
|
||||
public func updateChartRange(_ rangeFraction: ClosedRange<CGFloat>) {
|
||||
fatalError("Abstract")
|
||||
}
|
||||
|
||||
public var actualChartVisibility: [Bool] {
|
||||
fatalError("Abstract")
|
||||
}
|
||||
|
||||
public var actualChartsCollection: ChartsCollection {
|
||||
fatalError("Abstract")
|
||||
}
|
||||
|
||||
public var drawChartVisibity: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
public var drawChartNavigation: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
public var setDetailsViewPositionClosure: ((CGFloat) -> Void)?
|
||||
public var setDetailsChartVisibleClosure: ((Bool, Bool) -> Void)?
|
||||
public var setDetailsViewModel: ((ChartDetailsViewModel, Bool) -> Void)?
|
||||
public var getDetailsData: ((Date, @escaping (ChartsCollection?) -> Void) -> Void)?
|
||||
public var setChartTitleClosure: ((String, Bool) -> Void)?
|
||||
public var setBackButtonVisibilityClosure: ((Bool, Bool) -> Void)?
|
||||
public var refreshChartToolsClosure: ((Bool) -> Void)?
|
||||
|
||||
public func didTapZoomIn(date: Date) {
|
||||
fatalError("Abstract")
|
||||
}
|
||||
|
||||
public func apply(colorMode: GColorMode, animated: Bool) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
//
|
||||
// GeneralChartComponentController.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 4/11/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
#if os(macOS)
|
||||
import Cocoa
|
||||
#else
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
enum GeneralChartComponentConstants {
|
||||
static let defaultInitialRangeLength = CGFloat(TimeInterval.day * 60)
|
||||
static let defaultZoomedRangeLength = CGFloat(TimeInterval.day)
|
||||
}
|
||||
|
||||
class GeneralChartComponentController: GColorModeContainer {
|
||||
var chartsCollection: ChartsCollection = ChartsCollection.blank
|
||||
var chartVisibility: [Bool] = []
|
||||
var lastChartInteractionPoint: CGPoint = .zero
|
||||
var isChartInteractionBegun: Bool = false
|
||||
var isChartInteracting: Bool = false
|
||||
let isZoomed: Bool
|
||||
|
||||
var colorMode: GColorMode = .day
|
||||
var totalHorizontalRange: ClosedRange<CGFloat> = BaseConstants.defaultRange
|
||||
var totalVerticalRange: ClosedRange<CGFloat> = BaseConstants.defaultRange
|
||||
var initialHorizontalRange: ClosedRange<CGFloat> = BaseConstants.defaultRange
|
||||
var initialVerticalRange: ClosedRange<CGFloat> = BaseConstants.defaultRange
|
||||
|
||||
public var cartViewBounds: (() -> CGRect) = { fatalError() }
|
||||
public var chartFrame: (() -> CGRect) = { fatalError() }
|
||||
|
||||
init(isZoomed: Bool) {
|
||||
self.isZoomed = isZoomed
|
||||
}
|
||||
|
||||
func initialize(chartsCollection: ChartsCollection,
|
||||
initialDate: Date,
|
||||
totalHorizontalRange: ClosedRange<CGFloat>,
|
||||
totalVerticalRange: ClosedRange<CGFloat>) {
|
||||
self.chartsCollection = chartsCollection
|
||||
self.chartVisibility = Array(repeating: true, count: chartsCollection.chartValues.count)
|
||||
self.totalHorizontalRange = totalHorizontalRange
|
||||
self.totalVerticalRange = totalVerticalRange
|
||||
self.initialHorizontalRange = totalHorizontalRange
|
||||
self.initialVerticalRange = totalVerticalRange
|
||||
|
||||
didLoad()
|
||||
setupInitialChartRange(initialDate: initialDate)
|
||||
}
|
||||
|
||||
func didLoad() {
|
||||
hideDetailsView(animated: false)
|
||||
}
|
||||
func willAppear(animated: Bool) {
|
||||
updateChartRangeTitle(animated: animated)
|
||||
setupChartRangePaging()
|
||||
}
|
||||
func willDisappear(animated: Bool) {
|
||||
}
|
||||
|
||||
func setupInitialChartRange(initialDate: Date) {
|
||||
guard let first = chartsCollection.axisValues.first?.timeIntervalSince1970,
|
||||
let last = chartsCollection.axisValues.last?.timeIntervalSince1970 else { return }
|
||||
|
||||
let rangeStart = CGFloat(first)
|
||||
let rangeEnd = CGFloat(last)
|
||||
|
||||
if isZoomed {
|
||||
let initalDate = CGFloat(initialDate.timeIntervalSince1970)
|
||||
|
||||
initialHorizontalRange = max(initalDate, rangeStart)...min(initalDate + GeneralChartComponentConstants.defaultZoomedRangeLength, rangeEnd)
|
||||
initialVerticalRange = totalVerticalRange
|
||||
} else {
|
||||
initialHorizontalRange = max(rangeStart, rangeEnd - GeneralChartComponentConstants.defaultInitialRangeLength)...rangeEnd
|
||||
initialVerticalRange = totalVerticalRange
|
||||
}
|
||||
}
|
||||
func setupChartRangePaging() {
|
||||
chartRangePagingClosure?(false, 0.05)
|
||||
}
|
||||
|
||||
var visibleHorizontalMainChartRange: ClosedRange<CGFloat> {
|
||||
return currentMainRangeRenderer.verticalRange.current
|
||||
}
|
||||
var visibleVerticalMainChartRange: ClosedRange<CGFloat> {
|
||||
return currentMainRangeRenderer.verticalRange.current
|
||||
}
|
||||
var currentHorizontalMainChartRange: ClosedRange<CGFloat> {
|
||||
return currentMainRangeRenderer.horizontalRange.end
|
||||
}
|
||||
var currentVerticalMainChartRange: ClosedRange<CGFloat> {
|
||||
return currentMainRangeRenderer.verticalRange.end
|
||||
}
|
||||
var currentMainRangeRenderer: BaseChartRenderer {
|
||||
fatalError("Abstract")
|
||||
}
|
||||
|
||||
var visiblePreviewHorizontalRange: ClosedRange<CGFloat> {
|
||||
return currentPreviewRangeRenderer.verticalRange.current
|
||||
}
|
||||
var visiblePreviewVerticalRange: ClosedRange<CGFloat> {
|
||||
return currentPreviewRangeRenderer.verticalRange.current
|
||||
}
|
||||
var currentPreviewHorizontalRange: ClosedRange<CGFloat> {
|
||||
return currentPreviewRangeRenderer.horizontalRange.end
|
||||
}
|
||||
var currentPreviewVerticalRange: ClosedRange<CGFloat> {
|
||||
return currentPreviewRangeRenderer.verticalRange.end
|
||||
}
|
||||
var currentPreviewRangeRenderer: BaseChartRenderer {
|
||||
fatalError("Abstract")
|
||||
}
|
||||
|
||||
var mainChartRenderers: [ChartViewRenderer] {
|
||||
fatalError("Abstract")
|
||||
}
|
||||
var previewRenderers: [ChartViewRenderer] {
|
||||
fatalError("Abstract")
|
||||
}
|
||||
|
||||
func updateChartsVisibility(visibility: [Bool], animated: Bool) {
|
||||
self.chartVisibility = visibility
|
||||
if isChartInteractionBegun {
|
||||
chartInteractionDidBegin(point: lastChartInteractionPoint)
|
||||
}
|
||||
}
|
||||
|
||||
var currentChartHorizontalRangeFraction: ClosedRange<CGFloat> {
|
||||
let lowerPercent = (currentHorizontalMainChartRange.lowerBound - totalHorizontalRange.lowerBound) / totalHorizontalRange.distance
|
||||
let upperPercent = (currentHorizontalMainChartRange.upperBound - totalHorizontalRange.lowerBound) / totalHorizontalRange.distance
|
||||
return lowerPercent...upperPercent
|
||||
}
|
||||
|
||||
func chartRangeFractionDidUpdated(_ rangeFraction: ClosedRange<CGFloat>) {
|
||||
let horizontalRange = ClosedRange(uncheckedBounds:
|
||||
(lower: totalHorizontalRange.lowerBound + rangeFraction.lowerBound * totalHorizontalRange.distance,
|
||||
upper: totalHorizontalRange.lowerBound + rangeFraction.upperBound * totalHorizontalRange.distance))
|
||||
chartRangeDidUpdated(horizontalRange)
|
||||
updateChartRangeTitle(animated: true)
|
||||
}
|
||||
|
||||
func chartRangeDidUpdated(_ updatedRange: ClosedRange<CGFloat>) {
|
||||
hideDetailsView(animated: true)
|
||||
|
||||
if isChartInteractionBegun {
|
||||
chartInteractionDidBegin(point: lastChartInteractionPoint)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Details & Interaction
|
||||
func findClosestDateTo(dateToFind: Date) -> (Date, Int)? {
|
||||
guard chartsCollection.axisValues.count > 0 else { return nil }
|
||||
var closestDate = chartsCollection.axisValues[0]
|
||||
var minIndex = 0
|
||||
for (index, date) in chartsCollection.axisValues.enumerated() {
|
||||
if abs(dateToFind.timeIntervalSince(date)) < abs(dateToFind.timeIntervalSince(closestDate)) {
|
||||
closestDate = date
|
||||
minIndex = index
|
||||
}
|
||||
}
|
||||
return (closestDate, minIndex)
|
||||
}
|
||||
|
||||
func chartInteractionDidBegin(point: CGPoint) {
|
||||
let chartFrame = self.chartFrame()
|
||||
guard chartFrame.width > 0 else { return }
|
||||
let horizontalRange = currentHorizontalMainChartRange
|
||||
let dateToFind = Date(timeIntervalSince1970: TimeInterval(horizontalRange.distance * point.x + horizontalRange.lowerBound))
|
||||
guard let (closestDate, minIndex) = findClosestDateTo(dateToFind: dateToFind) else { return }
|
||||
|
||||
let chartWasInteracting = isChartInteractionBegun
|
||||
lastChartInteractionPoint = point
|
||||
isChartInteractionBegun = true
|
||||
isChartInteracting = true
|
||||
|
||||
let chartValue: CGFloat = CGFloat(closestDate.timeIntervalSince1970)
|
||||
let detailsViewPosition = (chartValue - horizontalRange.lowerBound) / horizontalRange.distance * chartFrame.width + chartFrame.minX
|
||||
showDetailsView(at: chartValue, detailsViewPosition: detailsViewPosition, dataIndex: minIndex, date: closestDate, animted: chartWasInteracting)
|
||||
}
|
||||
|
||||
func showDetailsView(at chartPosition: CGFloat, detailsViewPosition: CGFloat, dataIndex: Int, date: Date, animted: Bool) {
|
||||
setDetailsViewModel?(chartDetailsViewModel(closestDate: date, pointIndex: dataIndex), animted)
|
||||
setDetailsChartVisibleClosure?(true, true)
|
||||
setDetailsViewPositionClosure?(detailsViewPosition)
|
||||
}
|
||||
|
||||
func chartInteractionDidEnd() {
|
||||
isChartInteracting = false
|
||||
}
|
||||
|
||||
func hideDetailsView(animated: Bool) {
|
||||
isChartInteractionBegun = false
|
||||
setDetailsChartVisibleClosure?(false, animated)
|
||||
}
|
||||
|
||||
var visibleDetailsChartValues: [ChartsCollection.Chart] {
|
||||
let visibleCharts: [ChartsCollection.Chart] = chartVisibility.enumerated().compactMap { args in
|
||||
args.element ? chartsCollection.chartValues[args.offset] : nil
|
||||
}
|
||||
return visibleCharts
|
||||
}
|
||||
|
||||
var updatePreviewRangeClosure: ((ClosedRange<CGFloat>, Bool) -> Void)?
|
||||
var zoomInOnDateClosure: ((Date) -> Void)?
|
||||
var setChartTitleClosure: ((String, Bool) -> Void)?
|
||||
var setDetailsViewPositionClosure: ((CGFloat) -> Void)?
|
||||
var setDetailsChartVisibleClosure: ((Bool, Bool) -> Void)?
|
||||
var setDetailsViewModel: ((ChartDetailsViewModel, Bool) -> Void)?
|
||||
var chartRangePagingClosure: ((Bool, CGFloat) -> Void)? // isEnabled, PageSize
|
||||
|
||||
func apply(colorMode: GColorMode, animated: Bool) {
|
||||
self.colorMode = colorMode
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
var prevoiusHorizontalStrideInterval: Int = -1
|
||||
func updateHorizontalLimitLabels(horizontalScalesRenderer: HorizontalScalesRenderer,
|
||||
horizontalRange: ClosedRange<CGFloat>,
|
||||
scaleType: ChartScaleType,
|
||||
forceUpdate: Bool,
|
||||
animated: Bool) {
|
||||
let scaleTimeInterval: TimeInterval
|
||||
if chartsCollection.axisValues.count >= 1 {
|
||||
scaleTimeInterval = chartsCollection.axisValues[1].timeIntervalSince1970 - chartsCollection.axisValues[0].timeIntervalSince1970
|
||||
} else {
|
||||
scaleTimeInterval = scaleType.timeInterval
|
||||
}
|
||||
|
||||
let numberOfItems = horizontalRange.distance / CGFloat(scaleTimeInterval)
|
||||
let maximumNumberOfItems = chartFrame().width / scaleType.minimumAxisXDistance
|
||||
let tempStride = max(1, Int((numberOfItems / maximumNumberOfItems).rounded(.up)))
|
||||
var strideInterval = 1
|
||||
while strideInterval < tempStride {
|
||||
strideInterval *= 2
|
||||
}
|
||||
|
||||
if forceUpdate || (strideInterval != prevoiusHorizontalStrideInterval && strideInterval > 0) {
|
||||
var labels: [LinesChartLabel] = []
|
||||
for index in stride(from: chartsCollection.axisValues.count - 1, to: -1, by: -strideInterval).reversed() {
|
||||
let date = chartsCollection.axisValues[index]
|
||||
labels.append(LinesChartLabel(value: CGFloat(date.timeIntervalSince1970),
|
||||
text: scaleType.dateFormatter.string(from: date)))
|
||||
}
|
||||
prevoiusHorizontalStrideInterval = strideInterval
|
||||
horizontalScalesRenderer.setup(labels: labels, animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
func verticalLimitsLabels(verticalRange: ClosedRange<CGFloat>) -> (ClosedRange<CGFloat>, [LinesChartLabel]) {
|
||||
let ditance = verticalRange.distance
|
||||
let chartHeight = chartFrame().height
|
||||
|
||||
guard ditance > 0, chartHeight > 0 else { return (BaseConstants.defaultRange, []) }
|
||||
|
||||
let approximateNumberOfChartValues = (chartHeight / BaseConstants.minimumAxisYLabelsDistance)
|
||||
|
||||
var numberOfOffsetsPerItem = ditance / approximateNumberOfChartValues
|
||||
var multiplier: CGFloat = 1.0
|
||||
while numberOfOffsetsPerItem > 10 {
|
||||
numberOfOffsetsPerItem /= 10
|
||||
multiplier *= 10
|
||||
}
|
||||
var dividor: CGFloat = 1.0
|
||||
var maximumNumberOfDecimals = 2
|
||||
while numberOfOffsetsPerItem < 1 {
|
||||
numberOfOffsetsPerItem *= 10
|
||||
dividor *= 10
|
||||
maximumNumberOfDecimals += 1
|
||||
}
|
||||
|
||||
var base: CGFloat = BaseConstants.verticalBaseAnchors.first { numberOfOffsetsPerItem > $0 } ?? BaseConstants.defaultVerticalBaseAnchor
|
||||
base = base * multiplier / dividor
|
||||
|
||||
var verticalLabels: [LinesChartLabel] = []
|
||||
var verticalValue = (verticalRange.lowerBound / base).rounded(.down) * base
|
||||
let lowerBound = verticalValue
|
||||
|
||||
let numberFormatter = BaseConstants.chartNumberFormatter
|
||||
numberFormatter.maximumFractionDigits = maximumNumberOfDecimals
|
||||
while verticalValue < verticalRange.upperBound {
|
||||
let text: String = numberFormatter.string(from: NSNumber(value: Double(verticalValue))) ?? ""
|
||||
|
||||
verticalLabels.append(LinesChartLabel(value: verticalValue, text: text))
|
||||
verticalValue += base
|
||||
}
|
||||
let updatedRange = lowerBound...verticalValue
|
||||
|
||||
return (updatedRange, verticalLabels)
|
||||
}
|
||||
|
||||
func chartDetailsViewModel(closestDate: Date, pointIndex: Int) -> ChartDetailsViewModel {
|
||||
let values: [ChartDetailsViewModel.Value] = chartsCollection.chartValues.enumerated().map { arg in
|
||||
let (index, component) = arg
|
||||
return ChartDetailsViewModel.Value(prefix: nil,
|
||||
title: component.name,
|
||||
value: BaseConstants.detailsNumberFormatter.string(from: NSNumber(value: component.values[pointIndex])) ?? "",
|
||||
color: component.color,
|
||||
visible: chartVisibility[index])
|
||||
}
|
||||
let dateString: String
|
||||
if isZoomed {
|
||||
dateString = BaseConstants.timeDateFormatter.string(from: closestDate)
|
||||
} else {
|
||||
dateString = BaseConstants.headerMediumRangeFormatter.string(from: closestDate)
|
||||
}
|
||||
let viewModel = ChartDetailsViewModel(title: dateString,
|
||||
showArrow: !self.isZoomed,
|
||||
showPrefixes: false,
|
||||
values: values,
|
||||
totalValue: nil,
|
||||
tapAction: { [weak self] in
|
||||
self?.zoomInOnDateClosure?(closestDate) })
|
||||
return viewModel
|
||||
}
|
||||
|
||||
func updateChartRangeTitle(animated: Bool) {
|
||||
let fromDate = Date(timeIntervalSince1970: TimeInterval(currentHorizontalMainChartRange.lowerBound) + 1)
|
||||
let toDate = Date(timeIntervalSince1970: TimeInterval(currentHorizontalMainChartRange.upperBound))
|
||||
if Calendar.utc.startOfDay(for: fromDate) == Calendar.utc.startOfDay(for: toDate) {
|
||||
let stirng = BaseConstants.headerFullZoomedFormatter.string(from: fromDate)
|
||||
self.setChartTitleClosure?(stirng, animated)
|
||||
} else {
|
||||
let stirng = "\(BaseConstants.headerMediumRangeFormatter.string(from: fromDate)) - \(BaseConstants.headerMediumRangeFormatter.string(from: toDate))"
|
||||
self.setChartTitleClosure?(stirng, animated)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
//
|
||||
// BaseLinesChartController.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 4/14/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
#if os(macOS)
|
||||
import Cocoa
|
||||
#else
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
public class BaseLinesChartController: BaseChartController {
|
||||
var chartVisibility: [Bool]
|
||||
var zoomChartVisibility: [Bool]
|
||||
var lastChartInteractionPoint: CGPoint = .zero
|
||||
var isChartInteractionBegun: Bool = false
|
||||
|
||||
var initialChartRange: ClosedRange<CGFloat> = BaseConstants.defaultRange
|
||||
var zoomedChartRange: ClosedRange<CGFloat> = BaseConstants.defaultRange
|
||||
|
||||
override public init(chartsCollection: ChartsCollection) {
|
||||
self.chartVisibility = Array(repeating: true, count: chartsCollection.chartValues.count)
|
||||
self.zoomChartVisibility = []
|
||||
super.init(chartsCollection: chartsCollection)
|
||||
}
|
||||
|
||||
func setupChartCollection(chartsCollection: ChartsCollection, animated: Bool, isZoomed: Bool) {
|
||||
if animated {
|
||||
TimeInterval.setDefaultSuration(.expandAnimationDuration)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .expandAnimationDuration) {
|
||||
TimeInterval.setDefaultSuration(.osXDuration)
|
||||
}
|
||||
}
|
||||
|
||||
self.initialChartsCollection = chartsCollection
|
||||
self.isZoomed = isZoomed
|
||||
|
||||
self.setBackButtonVisibilityClosure?(isZoomed, animated)
|
||||
|
||||
updateChartRangeTitle(animated: animated)
|
||||
}
|
||||
|
||||
func updateChartRangeTitle(animated: Bool) {
|
||||
let range: ClosedRange<CGFloat>
|
||||
if zoomedChartRange == BaseConstants.defaultRange {
|
||||
range = initialChartRange
|
||||
} else {
|
||||
range = zoomedChartRange
|
||||
}
|
||||
let fromDate = Date(timeIntervalSince1970: TimeInterval(range.lowerBound) + .hour)
|
||||
let toDate = Date(timeIntervalSince1970: TimeInterval(range.upperBound))
|
||||
|
||||
|
||||
|
||||
if Calendar.utc.startOfDay(for: fromDate) == Calendar.utc.startOfDay(for: toDate) {
|
||||
let stirng = BaseConstants.headerFullZoomedFormatter.string(from: fromDate)
|
||||
self.setChartTitleClosure?(stirng, animated)
|
||||
} else {
|
||||
let stirng = "\(BaseConstants.headerMediumRangeFormatter.string(from: fromDate)) - \(BaseConstants.headerMediumRangeFormatter.string(from: toDate))"
|
||||
self.setChartTitleClosure?(stirng, animated)
|
||||
}
|
||||
}
|
||||
|
||||
public override func chartInteractionDidBegin(point: CGPoint) {
|
||||
lastChartInteractionPoint = point
|
||||
isChartInteractionBegun = true
|
||||
}
|
||||
|
||||
public override func chartInteractionDidEnd() {
|
||||
|
||||
}
|
||||
|
||||
public override func cancelChartInteraction() {
|
||||
isChartInteractionBegun = false
|
||||
}
|
||||
|
||||
public override func updateChartRange(_ rangeFraction: ClosedRange<CGFloat>) {
|
||||
|
||||
}
|
||||
|
||||
public override var actualChartVisibility: [Bool] {
|
||||
return isZoomed ? zoomChartVisibility : chartVisibility
|
||||
}
|
||||
|
||||
public override var actualChartsCollection: ChartsCollection {
|
||||
return initialChartsCollection
|
||||
}
|
||||
|
||||
var visibleChartValues: [ChartsCollection.Chart] {
|
||||
let visibleCharts: [ChartsCollection.Chart] = actualChartVisibility.enumerated().compactMap { args in
|
||||
args.element ? initialChartsCollection.chartValues[args.offset] : nil
|
||||
}
|
||||
return visibleCharts
|
||||
}
|
||||
|
||||
|
||||
func chartDetailsViewModel(closestDate: Date, pointIndex: Int) -> ChartDetailsViewModel {
|
||||
let values: [ChartDetailsViewModel.Value] = actualChartsCollection.chartValues.enumerated().map { arg in
|
||||
let (index, component) = arg
|
||||
return ChartDetailsViewModel.Value(prefix: nil,
|
||||
title: component.name,
|
||||
value: BaseConstants.detailsNumberFormatter.string(from: component.values[pointIndex]),
|
||||
color: component.color,
|
||||
visible: actualChartVisibility[index])
|
||||
}
|
||||
let dateString: String
|
||||
if isZoomed {
|
||||
dateString = BaseConstants.timeDateFormatter.string(from: closestDate)
|
||||
} else {
|
||||
dateString = BaseConstants.headerMediumRangeFormatter.string(from: closestDate)
|
||||
}
|
||||
let viewModel = ChartDetailsViewModel(title: dateString,
|
||||
showArrow: !self.isZoomed,
|
||||
showPrefixes: false,
|
||||
values: values,
|
||||
totalValue: nil,
|
||||
tapAction: { [weak self] in self?.didTapZoomIn(date: closestDate) })
|
||||
return viewModel
|
||||
}
|
||||
|
||||
public override func didTapZoomIn(date: Date) {
|
||||
guard isZoomed == false else { return }
|
||||
cancelChartInteraction()
|
||||
self.getDetailsData?(date, { updatedCollection in
|
||||
if let updatedCollection = updatedCollection {
|
||||
self.initialChartRange = self.currentHorizontalRange
|
||||
if let startDate = updatedCollection.axisValues.first,
|
||||
let endDate = updatedCollection.axisValues.last {
|
||||
self.zoomedChartRange = CGFloat(max(date.timeIntervalSince1970, startDate.timeIntervalSince1970))...CGFloat(min(date.timeIntervalSince1970 + .day - .hour, endDate.timeIntervalSince1970))
|
||||
} else {
|
||||
self.zoomedChartRange = CGFloat(date.timeIntervalSince1970)...CGFloat(date.timeIntervalSince1970 + .day - 1)
|
||||
}
|
||||
self.setupChartCollection(chartsCollection: updatedCollection, animated: true, isZoomed: true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func horizontalLimitsLabels(horizontalRange: ClosedRange<CGFloat>,
|
||||
scaleType: ChartScaleType,
|
||||
prevoiusHorizontalStrideInterval: Int) -> (Int, [LinesChartLabel])? {
|
||||
let numberOfItems = horizontalRange.distance / CGFloat(scaleType.timeInterval)
|
||||
let maximumNumberOfItems = chartFrame().width / scaleType.minimumAxisXDistance
|
||||
let tempStride = max(1, Int((numberOfItems / maximumNumberOfItems).rounded(.up)))
|
||||
var strideInterval = 1
|
||||
while strideInterval < tempStride {
|
||||
strideInterval *= 2
|
||||
}
|
||||
|
||||
if strideInterval != prevoiusHorizontalStrideInterval && strideInterval > 0 {
|
||||
var labels: [LinesChartLabel] = []
|
||||
for index in stride(from: initialChartsCollection.axisValues.count - 1, to: -1, by: -strideInterval).reversed() {
|
||||
let date = initialChartsCollection.axisValues[index]
|
||||
labels.append(LinesChartLabel(value: CGFloat(date.timeIntervalSince1970),
|
||||
text: scaleType.dateFormatter.string(from: date)))
|
||||
}
|
||||
return (strideInterval, labels)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func findClosestDateTo(dateToFind: Date) -> (Date, Int)? {
|
||||
guard initialChartsCollection.axisValues.count > 0 else { return nil }
|
||||
var closestDate = initialChartsCollection.axisValues[0]
|
||||
var minIndex = 0
|
||||
for (index, date) in initialChartsCollection.axisValues.enumerated() {
|
||||
if abs(dateToFind.timeIntervalSince(date)) < abs(dateToFind.timeIntervalSince(closestDate)) {
|
||||
closestDate = date
|
||||
minIndex = index
|
||||
}
|
||||
}
|
||||
return (closestDate, minIndex)
|
||||
}
|
||||
|
||||
func verticalLimitsLabels(verticalRange: ClosedRange<CGFloat>) -> (ClosedRange<CGFloat>, [LinesChartLabel]) {
|
||||
let ditance = verticalRange.distance
|
||||
let chartHeight = chartFrame().height
|
||||
|
||||
guard ditance > 0, chartHeight > 0 else { return (BaseConstants.defaultRange, []) }
|
||||
|
||||
let approximateNumberOfChartValues = (chartHeight / BaseConstants.minimumAxisYLabelsDistance)
|
||||
|
||||
var numberOfOffsetsPerItem = ditance / approximateNumberOfChartValues
|
||||
var multiplier: CGFloat = 1.0
|
||||
while numberOfOffsetsPerItem > 10 {
|
||||
numberOfOffsetsPerItem /= 10
|
||||
multiplier *= 10
|
||||
}
|
||||
var dividor: CGFloat = 1.0
|
||||
var maximumNumberOfDecimals = 2
|
||||
while numberOfOffsetsPerItem < 1 {
|
||||
numberOfOffsetsPerItem *= 10
|
||||
dividor *= 10
|
||||
maximumNumberOfDecimals += 1
|
||||
}
|
||||
|
||||
var base: CGFloat = BaseConstants.verticalBaseAnchors.first { numberOfOffsetsPerItem > $0 } ?? BaseConstants.defaultVerticalBaseAnchor
|
||||
base = base * multiplier / dividor
|
||||
|
||||
var verticalLabels: [LinesChartLabel] = []
|
||||
var verticalValue = (verticalRange.lowerBound / base).rounded(.down) * base
|
||||
let lowerBound = verticalValue
|
||||
|
||||
let numberFormatter = BaseConstants.chartNumberFormatter
|
||||
numberFormatter.maximumFractionDigits = maximumNumberOfDecimals
|
||||
while verticalValue < verticalRange.upperBound {
|
||||
let text: String = numberFormatter.string(from: NSNumber(value: Double(verticalValue))) ?? ""
|
||||
|
||||
verticalLabels.append(LinesChartLabel(value: verticalValue, text: text))
|
||||
verticalValue += base
|
||||
}
|
||||
let updatedRange = lowerBound...verticalValue
|
||||
|
||||
return (updatedRange, verticalLabels)
|
||||
}
|
||||
}
|
||||
|
||||
enum ChartScaleType {
|
||||
case day
|
||||
case hour
|
||||
case minutes5
|
||||
}
|
||||
|
||||
extension ChartScaleType {
|
||||
var timeInterval: TimeInterval {
|
||||
switch self {
|
||||
case .day: return .day
|
||||
case .hour: return .hour
|
||||
case .minutes5: return .minute * 5
|
||||
}
|
||||
}
|
||||
|
||||
var minimumAxisXDistance: CGFloat {
|
||||
switch self {
|
||||
case .day: return 50
|
||||
case .hour: return 40
|
||||
case .minutes5: return 40
|
||||
}
|
||||
}
|
||||
var dateFormatter: DateFormatter {
|
||||
switch self {
|
||||
case .day: return BaseConstants.monthDayDateFormatter
|
||||
case .hour: return BaseConstants.timeDateFormatter
|
||||
case .minutes5: return BaseConstants.timeDateFormatter
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
//
|
||||
// LinesChartController.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
|
||||
|
||||
private enum Constants {
|
||||
static let defaultRange: ClosedRange<CGFloat> = 0...1
|
||||
}
|
||||
|
||||
public class GeneralLinesChartController: BaseLinesChartController {
|
||||
private let initialChartCollection: ChartsCollection
|
||||
|
||||
private let mainLinesRenderer = LinesChartRenderer()
|
||||
private let horizontalScalesRenderer = HorizontalScalesRenderer()
|
||||
private let verticalScalesRenderer = VerticalScalesRenderer()
|
||||
private let verticalLineRenderer = VerticalLinesRenderer()
|
||||
private let lineBulletsRenderer = LineBulletsRenderer()
|
||||
|
||||
private let previewLinesRenderer = LinesChartRenderer()
|
||||
|
||||
private var totalVerticalRange: ClosedRange<CGFloat> = Constants.defaultRange
|
||||
private var totalHorizontalRange: ClosedRange<CGFloat> = Constants.defaultRange
|
||||
|
||||
private var prevoiusHorizontalStrideInterval: Int = 1
|
||||
|
||||
private (set) var chartLines: [LinesChartRenderer.LineData] = []
|
||||
|
||||
override public init(chartsCollection: ChartsCollection) {
|
||||
self.initialChartCollection = chartsCollection
|
||||
self.mainLinesRenderer.lineWidth = 2
|
||||
self.mainLinesRenderer.optimizationLevel = BaseConstants.linesChartOptimizationLevel
|
||||
self.previewLinesRenderer.optimizationLevel = BaseConstants.previewLinesChartOptimizationLevel
|
||||
|
||||
self.lineBulletsRenderer.isEnabled = false
|
||||
|
||||
super.init(chartsCollection: chartsCollection)
|
||||
self.zoomChartVisibility = chartVisibility
|
||||
}
|
||||
|
||||
override func setupChartCollection(chartsCollection: ChartsCollection, animated: Bool, isZoomed: Bool) {
|
||||
super.setupChartCollection(chartsCollection: chartsCollection, animated: animated, isZoomed: isZoomed)
|
||||
|
||||
self.chartLines = chartsCollection.chartValues.map { chart in
|
||||
let points = chart.values.enumerated().map({ (arg) -> CGPoint in
|
||||
return CGPoint(x: chartsCollection.axisValues[arg.offset].timeIntervalSince1970,
|
||||
y: arg.element)
|
||||
})
|
||||
return LinesChartRenderer.LineData(color: chart.color, points: points)
|
||||
}
|
||||
|
||||
self.prevoiusHorizontalStrideInterval = -1
|
||||
self.totalVerticalRange = LinesChartRenderer.LineData.verticalRange(lines: chartLines) ?? Constants.defaultRange
|
||||
self.totalHorizontalRange = LinesChartRenderer.LineData.horizontalRange(lines: chartLines) ?? Constants.defaultRange
|
||||
self.lineBulletsRenderer.bullets = self.chartLines.map { LineBulletsRenderer.Bullet(coordinate: $0.points.first ?? .zero,
|
||||
color: $0.color)}
|
||||
|
||||
let chartRange: ClosedRange<CGFloat>
|
||||
if isZoomed {
|
||||
chartRange = zoomedChartRange
|
||||
} else {
|
||||
chartRange = initialChartRange
|
||||
}
|
||||
|
||||
self.previewLinesRenderer.setup(horizontalRange: totalHorizontalRange, animated: animated)
|
||||
self.previewLinesRenderer.setup(verticalRange: totalVerticalRange, animated: animated)
|
||||
|
||||
self.mainLinesRenderer.setLines(lines: chartLines, animated: animated)
|
||||
self.previewLinesRenderer.setLines(lines: chartLines, animated: animated)
|
||||
|
||||
updateHorizontalLimits(horizontalRange: chartRange, animated: animated)
|
||||
updateMainChartHorizontalRange(range: chartRange, animated: animated)
|
||||
updateVerticalLimitsAndRange(horizontalRange: chartRange, animated: animated)
|
||||
self.chartRangeUpdatedClosure?(currentChartHorizontalRangeFraction, animated)
|
||||
}
|
||||
|
||||
public override func initializeChart() {
|
||||
if let first = initialChartCollection.axisValues.first?.timeIntervalSince1970,
|
||||
let last = initialChartCollection.axisValues.last?.timeIntervalSince1970 {
|
||||
initialChartRange = CGFloat(max(first, last - BaseConstants.defaultRangePresetLength))...CGFloat(last)
|
||||
}
|
||||
setupChartCollection(chartsCollection: initialChartCollection, animated: false, isZoomed: false)
|
||||
}
|
||||
|
||||
public override var mainChartRenderers: [ChartViewRenderer] {
|
||||
return [//performanceRenderer,
|
||||
mainLinesRenderer,
|
||||
horizontalScalesRenderer,
|
||||
verticalScalesRenderer,
|
||||
verticalLineRenderer,
|
||||
lineBulletsRenderer
|
||||
]
|
||||
}
|
||||
|
||||
public override var navigationRenderers: [ChartViewRenderer] {
|
||||
return [previewLinesRenderer]
|
||||
}
|
||||
|
||||
public override func updateChartsVisibility(visibility: [Bool], animated: Bool) {
|
||||
chartVisibility = visibility
|
||||
zoomChartVisibility = visibility
|
||||
for (index, isVisible) in visibility.enumerated() {
|
||||
mainLinesRenderer.setLineVisible(isVisible, at: index, animated: animated)
|
||||
previewLinesRenderer.setLineVisible(isVisible, at: index, animated: animated)
|
||||
lineBulletsRenderer.setLineVisible(isVisible, at: index, animated: animated)
|
||||
}
|
||||
|
||||
updateVerticalLimitsAndRange(horizontalRange: currentHorizontalRange, animated: true)
|
||||
|
||||
if isChartInteractionBegun {
|
||||
chartInteractionDidBegin(point: lastChartInteractionPoint)
|
||||
}
|
||||
}
|
||||
|
||||
public override func chartInteractionDidBegin(point: CGPoint) {
|
||||
let horizontalRange = mainLinesRenderer.horizontalRange.current
|
||||
let chartFrame = self.chartFrame()
|
||||
guard chartFrame.width > 0 else { return }
|
||||
let chartInteractionWasBegin = isChartInteractionBegun
|
||||
|
||||
let dateToFind = Date(timeIntervalSince1970: TimeInterval(horizontalRange.distance * point.x + horizontalRange.lowerBound))
|
||||
guard let (closestDate, minIndex) = findClosestDateTo(dateToFind: dateToFind) else { return }
|
||||
|
||||
super.chartInteractionDidBegin(point: point)
|
||||
|
||||
self.lineBulletsRenderer.bullets = chartLines.compactMap { chart in
|
||||
return LineBulletsRenderer.Bullet(coordinate: chart.points[minIndex], color: chart.color)
|
||||
}
|
||||
self.lineBulletsRenderer.isEnabled = true
|
||||
|
||||
let chartValue: CGFloat = CGFloat(closestDate.timeIntervalSince1970)
|
||||
let detailsViewPosition = (chartValue - horizontalRange.lowerBound) / horizontalRange.distance * chartFrame.width + chartFrame.minX
|
||||
self.setDetailsViewModel?(chartDetailsViewModel(closestDate: closestDate, pointIndex: minIndex), chartInteractionWasBegin)
|
||||
self.setDetailsChartVisibleClosure?(true, true)
|
||||
self.setDetailsViewPositionClosure?(detailsViewPosition)
|
||||
self.verticalLineRenderer.values = [chartValue]
|
||||
}
|
||||
|
||||
|
||||
public override var currentChartHorizontalRangeFraction: ClosedRange<CGFloat> {
|
||||
let lowerPercent = (currentHorizontalRange.lowerBound - totalHorizontalRange.lowerBound) / totalHorizontalRange.distance
|
||||
let upperPercent = (currentHorizontalRange.upperBound - totalHorizontalRange.lowerBound) / totalHorizontalRange.distance
|
||||
return lowerPercent...upperPercent
|
||||
}
|
||||
|
||||
public override var currentHorizontalRange: ClosedRange<CGFloat> {
|
||||
return mainLinesRenderer.horizontalRange.end
|
||||
}
|
||||
|
||||
public override func cancelChartInteraction() {
|
||||
super.cancelChartInteraction()
|
||||
self.lineBulletsRenderer.isEnabled = false
|
||||
|
||||
self.setDetailsChartVisibleClosure?(false, true)
|
||||
self.verticalLineRenderer.values = []
|
||||
}
|
||||
|
||||
public override func didTapZoomOut() {
|
||||
cancelChartInteraction()
|
||||
self.setupChartCollection(chartsCollection: initialChartCollection, animated: true, isZoomed: false)
|
||||
}
|
||||
|
||||
var visibleCharts: [LinesChartRenderer.LineData] {
|
||||
let visibleCharts: [LinesChartRenderer.LineData] = chartVisibility.enumerated().compactMap { args in
|
||||
args.element ? chartLines[args.offset] : nil
|
||||
}
|
||||
return visibleCharts
|
||||
}
|
||||
|
||||
public override func updateChartRange(_ rangeFraction: ClosedRange<CGFloat>) {
|
||||
cancelChartInteraction()
|
||||
|
||||
let horizontalRange = ClosedRange(uncheckedBounds:
|
||||
(lower: totalHorizontalRange.lowerBound + rangeFraction.lowerBound * totalHorizontalRange.distance,
|
||||
upper: totalHorizontalRange.lowerBound + rangeFraction.upperBound * totalHorizontalRange.distance))
|
||||
|
||||
zoomedChartRange = horizontalRange
|
||||
updateChartRangeTitle(animated: true)
|
||||
|
||||
updateMainChartHorizontalRange(range: horizontalRange, animated: false)
|
||||
updateHorizontalLimits(horizontalRange: horizontalRange, animated: true)
|
||||
updateVerticalLimitsAndRange(horizontalRange: horizontalRange, animated: true)
|
||||
}
|
||||
|
||||
func updateMainChartHorizontalRange(range: ClosedRange<CGFloat>, animated: Bool) {
|
||||
mainLinesRenderer.setup(horizontalRange: range, animated: animated)
|
||||
horizontalScalesRenderer.setup(horizontalRange: range, animated: animated)
|
||||
verticalScalesRenderer.setup(horizontalRange: range, animated: animated)
|
||||
verticalLineRenderer.setup(horizontalRange: range, animated: animated)
|
||||
lineBulletsRenderer.setup(horizontalRange: range, animated: animated)
|
||||
}
|
||||
|
||||
func updateMainChartVerticalRange(range: ClosedRange<CGFloat>, animated: Bool) {
|
||||
mainLinesRenderer.setup(verticalRange: range, animated: animated)
|
||||
horizontalScalesRenderer.setup(verticalRange: range, animated: animated)
|
||||
verticalScalesRenderer.setup(verticalRange: range, animated: animated)
|
||||
verticalLineRenderer.setup(verticalRange: range, animated: animated)
|
||||
lineBulletsRenderer.setup(verticalRange: range, animated: animated)
|
||||
}
|
||||
|
||||
func updateHorizontalLimits(horizontalRange: ClosedRange<CGFloat>, animated: Bool) {
|
||||
if let (stride, labels) = horizontalLimitsLabels(horizontalRange: horizontalRange,
|
||||
scaleType: isZoomed ? .hour : .day,
|
||||
prevoiusHorizontalStrideInterval: prevoiusHorizontalStrideInterval) {
|
||||
self.horizontalScalesRenderer.setup(labels: labels, animated: animated)
|
||||
self.prevoiusHorizontalStrideInterval = stride
|
||||
}
|
||||
}
|
||||
|
||||
func updateVerticalLimitsAndRange(horizontalRange: ClosedRange<CGFloat>, animated: Bool) {
|
||||
if let verticalRange = LinesChartRenderer.LineData.verticalRange(lines: visibleCharts,
|
||||
calculatingRange: horizontalRange,
|
||||
addBounds: true) {
|
||||
|
||||
let (range, labels) = verticalLimitsLabels(verticalRange: verticalRange)
|
||||
|
||||
if verticalScalesRenderer.verticalRange.end != range {
|
||||
verticalScalesRenderer.setup(verticalLimitsLabels: labels, animated: animated)
|
||||
updateMainChartVerticalRange(range: range, animated: animated)
|
||||
}
|
||||
verticalScalesRenderer.setVisible(true, animated: animated)
|
||||
} else {
|
||||
verticalScalesRenderer.setVisible(false, animated: animated)
|
||||
}
|
||||
|
||||
guard let previewVerticalRange = LinesChartRenderer.LineData.verticalRange(lines: visibleCharts) else { return }
|
||||
|
||||
if previewLinesRenderer.verticalRange.end != previewVerticalRange {
|
||||
previewLinesRenderer.setup(verticalRange: previewVerticalRange, animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
override public func apply(colorMode: GColorMode, animated: Bool) {
|
||||
horizontalScalesRenderer.labelsColor = colorMode.chartLabelsColor
|
||||
verticalScalesRenderer.labelsColor = colorMode.chartLabelsColor
|
||||
verticalScalesRenderer.axisXColor = colorMode.chartStrongLinesColor
|
||||
verticalScalesRenderer.horizontalLinesColor = colorMode.chartHelperLinesColor
|
||||
lineBulletsRenderer.setInnerColor(colorMode.chartBackgroundColor, animated: animated)
|
||||
verticalLineRenderer.linesColor = colorMode.chartStrongLinesColor
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
//
|
||||
// TwoAxisLinesChartController.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
|
||||
|
||||
private enum Constants {
|
||||
static let verticalBaseAnchors: [CGFloat] = [8, 5, 4, 2.5, 2, 1]
|
||||
static let defaultRange: ClosedRange<CGFloat> = 0...1
|
||||
}
|
||||
|
||||
public class TwoAxisLinesChartController: BaseLinesChartController {
|
||||
class GraphController {
|
||||
let mainLinesRenderer = LinesChartRenderer()
|
||||
let verticalScalesRenderer = VerticalScalesRenderer()
|
||||
let lineBulletsRenderer = LineBulletsRenderer()
|
||||
let previewLinesRenderer = LinesChartRenderer()
|
||||
|
||||
var chartLines: [LinesChartRenderer.LineData] = []
|
||||
|
||||
var totalVerticalRange: ClosedRange<CGFloat> = Constants.defaultRange
|
||||
|
||||
init() {
|
||||
self.mainLinesRenderer.lineWidth = 2
|
||||
self.previewLinesRenderer.lineWidth = 1
|
||||
self.lineBulletsRenderer.isEnabled = false
|
||||
|
||||
self.mainLinesRenderer.optimizationLevel = BaseConstants.linesChartOptimizationLevel
|
||||
self.previewLinesRenderer.optimizationLevel = BaseConstants.previewLinesChartOptimizationLevel
|
||||
}
|
||||
|
||||
func updateMainChartVerticalRange(range: ClosedRange<CGFloat>, animated: Bool) {
|
||||
mainLinesRenderer.setup(verticalRange: range, animated: animated)
|
||||
verticalScalesRenderer.setup(verticalRange: range, animated: animated)
|
||||
lineBulletsRenderer.setup(verticalRange: range, animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
private var graphControllers: [GraphController] = []
|
||||
private let verticalLineRenderer = VerticalLinesRenderer()
|
||||
private let horizontalScalesRenderer = HorizontalScalesRenderer()
|
||||
|
||||
var totalHorizontalRange: ClosedRange<CGFloat> = Constants.defaultRange
|
||||
|
||||
private let initialChartCollection: ChartsCollection
|
||||
|
||||
private var prevoiusHorizontalStrideInterval: Int = 1
|
||||
|
||||
override public init(chartsCollection: ChartsCollection) {
|
||||
self.initialChartCollection = chartsCollection
|
||||
graphControllers = chartsCollection.chartValues.map { _ in GraphController() }
|
||||
|
||||
super.init(chartsCollection: chartsCollection)
|
||||
self.zoomChartVisibility = chartVisibility
|
||||
}
|
||||
|
||||
override func setupChartCollection(chartsCollection: ChartsCollection, animated: Bool, isZoomed: Bool) {
|
||||
super.setupChartCollection(chartsCollection: chartsCollection, animated: animated, isZoomed: isZoomed)
|
||||
|
||||
for (index, controller) in self.graphControllers.enumerated() {
|
||||
let chart = chartsCollection.chartValues[index]
|
||||
let points = chart.values.enumerated().map({ (arg) -> CGPoint in
|
||||
return CGPoint(x: chartsCollection.axisValues[arg.offset].timeIntervalSince1970,
|
||||
y: arg.element)
|
||||
})
|
||||
let chartLines = [LinesChartRenderer.LineData(color: chart.color, points: points)]
|
||||
controller.chartLines = [LinesChartRenderer.LineData(color: chart.color, points: points)]
|
||||
controller.verticalScalesRenderer.labelsColor = chart.color
|
||||
controller.totalVerticalRange = LinesChartRenderer.LineData.verticalRange(lines: chartLines) ?? Constants.defaultRange
|
||||
self.totalHorizontalRange = LinesChartRenderer.LineData.horizontalRange(lines: chartLines) ?? Constants.defaultRange
|
||||
controller.lineBulletsRenderer.bullets = chartLines.map { LineBulletsRenderer.Bullet(coordinate: $0.points.first ?? .zero,
|
||||
color: $0.color) }
|
||||
controller.previewLinesRenderer.setup(horizontalRange: self.totalHorizontalRange, animated: animated)
|
||||
controller.previewLinesRenderer.setup(verticalRange: controller.totalVerticalRange, animated: animated)
|
||||
controller.mainLinesRenderer.setLines(lines: chartLines, animated: animated)
|
||||
controller.previewLinesRenderer.setLines(lines: chartLines, animated: animated)
|
||||
|
||||
controller.verticalScalesRenderer.setHorizontalLinesVisible((index == 0), animated: animated)
|
||||
controller.verticalScalesRenderer.isRightAligned = (index != 0)
|
||||
}
|
||||
|
||||
self.prevoiusHorizontalStrideInterval = -1
|
||||
|
||||
let chartRange: ClosedRange<CGFloat>
|
||||
if isZoomed {
|
||||
chartRange = zoomedChartRange
|
||||
} else {
|
||||
chartRange = initialChartRange
|
||||
}
|
||||
|
||||
updateHorizontalLimits(horizontalRange: chartRange, animated: animated)
|
||||
updateMainChartHorizontalRange(range: chartRange, animated: animated)
|
||||
updateVerticalLimitsAndRange(horizontalRange: chartRange, animated: animated)
|
||||
|
||||
self.chartRangeUpdatedClosure?(currentChartHorizontalRangeFraction, animated)
|
||||
}
|
||||
|
||||
public override func initializeChart() {
|
||||
if let first = initialChartCollection.axisValues.first?.timeIntervalSince1970,
|
||||
let last = initialChartCollection.axisValues.last?.timeIntervalSince1970 {
|
||||
initialChartRange = CGFloat(max(first, last - BaseConstants.defaultRangePresetLength))...CGFloat(last)
|
||||
}
|
||||
setupChartCollection(chartsCollection: initialChartCollection, animated: false, isZoomed: false)
|
||||
}
|
||||
|
||||
public override var mainChartRenderers: [ChartViewRenderer] {
|
||||
return graphControllers.map { $0.mainLinesRenderer } +
|
||||
graphControllers.flatMap { [$0.verticalScalesRenderer, $0.lineBulletsRenderer] } +
|
||||
[horizontalScalesRenderer, verticalLineRenderer,
|
||||
// performanceRenderer
|
||||
]
|
||||
}
|
||||
|
||||
public override var navigationRenderers: [ChartViewRenderer] {
|
||||
return graphControllers.map { $0.previewLinesRenderer }
|
||||
}
|
||||
|
||||
public override func updateChartsVisibility(visibility: [Bool], animated: Bool) {
|
||||
chartVisibility = visibility
|
||||
zoomChartVisibility = visibility
|
||||
let firstIndex = visibility.firstIndex(where: { $0 })
|
||||
for (index, isVisible) in visibility.enumerated() {
|
||||
let graph = graphControllers[index]
|
||||
for graphIndex in graph.chartLines.indices {
|
||||
graph.mainLinesRenderer.setLineVisible(isVisible, at: graphIndex, animated: animated)
|
||||
graph.previewLinesRenderer.setLineVisible(isVisible, at: graphIndex, animated: animated)
|
||||
graph.lineBulletsRenderer.setLineVisible(isVisible, at: graphIndex, animated: animated)
|
||||
}
|
||||
graph.verticalScalesRenderer.setVisible(isVisible, animated: animated)
|
||||
if let firstIndex = firstIndex {
|
||||
graph.verticalScalesRenderer.setHorizontalLinesVisible(index == firstIndex, animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
updateVerticalLimitsAndRange(horizontalRange: currentHorizontalRange, animated: true)
|
||||
|
||||
if isChartInteractionBegun {
|
||||
chartInteractionDidBegin(point: lastChartInteractionPoint)
|
||||
}
|
||||
}
|
||||
|
||||
public override func chartInteractionDidBegin(point: CGPoint) {
|
||||
let horizontalRange = currentHorizontalRange
|
||||
let chartFrame = self.chartFrame()
|
||||
guard chartFrame.width > 0 else { return }
|
||||
|
||||
let dateToFind = Date(timeIntervalSince1970: TimeInterval(horizontalRange.distance * point.x + horizontalRange.lowerBound))
|
||||
guard let (closestDate, minIndex) = findClosestDateTo(dateToFind: dateToFind) else { return }
|
||||
|
||||
let chartInteractionWasBegin = isChartInteractionBegun
|
||||
super.chartInteractionDidBegin(point: point)
|
||||
|
||||
for graphController in graphControllers {
|
||||
graphController.lineBulletsRenderer.bullets = graphController.chartLines.map { chart in
|
||||
LineBulletsRenderer.Bullet(coordinate: chart.points[minIndex], color: chart.color)
|
||||
}
|
||||
graphController.lineBulletsRenderer.isEnabled = true
|
||||
}
|
||||
|
||||
let chartValue: CGFloat = CGFloat(closestDate.timeIntervalSince1970)
|
||||
let detailsViewPosition = (chartValue - horizontalRange.lowerBound) / horizontalRange.distance * chartFrame.width + chartFrame.minX
|
||||
self.setDetailsViewModel?(chartDetailsViewModel(closestDate: closestDate, pointIndex: minIndex), chartInteractionWasBegin)
|
||||
self.setDetailsChartVisibleClosure?(true, true)
|
||||
self.setDetailsViewPositionClosure?(detailsViewPosition)
|
||||
self.verticalLineRenderer.values = [chartValue]
|
||||
}
|
||||
|
||||
public override var currentChartHorizontalRangeFraction: ClosedRange<CGFloat> {
|
||||
let lowerPercent = (currentHorizontalRange.lowerBound - totalHorizontalRange.lowerBound) / totalHorizontalRange.distance
|
||||
let upperPercent = (currentHorizontalRange.upperBound - totalHorizontalRange.lowerBound) / totalHorizontalRange.distance
|
||||
return lowerPercent...upperPercent
|
||||
}
|
||||
|
||||
public override var currentHorizontalRange: ClosedRange<CGFloat> {
|
||||
return graphControllers.first?.mainLinesRenderer.horizontalRange.end ?? Constants.defaultRange
|
||||
}
|
||||
|
||||
public override func cancelChartInteraction() {
|
||||
super.cancelChartInteraction()
|
||||
for graphController in graphControllers {
|
||||
graphController.lineBulletsRenderer.isEnabled = false
|
||||
}
|
||||
|
||||
self.setDetailsChartVisibleClosure?(false, true)
|
||||
self.verticalLineRenderer.values = []
|
||||
}
|
||||
|
||||
public override func didTapZoomOut() {
|
||||
cancelChartInteraction()
|
||||
self.setupChartCollection(chartsCollection: initialChartCollection, animated: true, isZoomed: false)
|
||||
}
|
||||
|
||||
public override func updateChartRange(_ rangeFraction: ClosedRange<CGFloat>) {
|
||||
cancelChartInteraction()
|
||||
|
||||
let horizontalRange = ClosedRange(uncheckedBounds:
|
||||
(lower: totalHorizontalRange.lowerBound + rangeFraction.lowerBound * totalHorizontalRange.distance,
|
||||
upper: totalHorizontalRange.lowerBound + rangeFraction.upperBound * totalHorizontalRange.distance))
|
||||
|
||||
zoomedChartRange = horizontalRange
|
||||
updateChartRangeTitle(animated: true)
|
||||
|
||||
updateMainChartHorizontalRange(range: horizontalRange, animated: false)
|
||||
updateHorizontalLimits(horizontalRange: horizontalRange, animated: true)
|
||||
updateVerticalLimitsAndRange(horizontalRange: horizontalRange, animated: true)
|
||||
}
|
||||
|
||||
func updateMainChartHorizontalRange(range: ClosedRange<CGFloat>, animated: Bool) {
|
||||
for controller in graphControllers {
|
||||
controller.mainLinesRenderer.setup(horizontalRange: range, animated: animated)
|
||||
controller.verticalScalesRenderer.setup(horizontalRange: range, animated: animated)
|
||||
controller.lineBulletsRenderer.setup(horizontalRange: range, animated: animated)
|
||||
}
|
||||
horizontalScalesRenderer.setup(horizontalRange: range, animated: animated)
|
||||
verticalLineRenderer.setup(horizontalRange: range, animated: animated)
|
||||
}
|
||||
|
||||
func updateHorizontalLimits(horizontalRange: ClosedRange<CGFloat>, animated: Bool) {
|
||||
if let (stride, labels) = horizontalLimitsLabels(horizontalRange: horizontalRange,
|
||||
scaleType: isZoomed ? .hour : .day,
|
||||
prevoiusHorizontalStrideInterval: prevoiusHorizontalStrideInterval) {
|
||||
self.horizontalScalesRenderer.setup(labels: labels, animated: animated)
|
||||
self.prevoiusHorizontalStrideInterval = stride
|
||||
}
|
||||
}
|
||||
|
||||
func updateVerticalLimitsAndRange(horizontalRange: ClosedRange<CGFloat>, animated: Bool) {
|
||||
let chartHeight = chartFrame().height
|
||||
let approximateNumberOfChartValues = (chartHeight / BaseConstants.minimumAxisYLabelsDistance)
|
||||
|
||||
var dividorsAndMultiplers: [(startValue: CGFloat, base: CGFloat, count: Int, maximumNumberOfDecimals: Int)] = graphControllers.enumerated().map { arg in
|
||||
let (index, controller) = arg
|
||||
let verticalRange = LinesChartRenderer.LineData.verticalRange(lines: controller.chartLines,
|
||||
calculatingRange: horizontalRange,
|
||||
addBounds: true) ?? controller.totalVerticalRange
|
||||
|
||||
var numberOfOffsetsPerItem = verticalRange.distance / approximateNumberOfChartValues
|
||||
|
||||
var multiplier: CGFloat = 1.0
|
||||
while numberOfOffsetsPerItem > 10 {
|
||||
numberOfOffsetsPerItem /= 10
|
||||
multiplier *= 10
|
||||
}
|
||||
var dividor: CGFloat = 1.0
|
||||
var maximumNumberOfDecimals = 2
|
||||
while numberOfOffsetsPerItem < 1 {
|
||||
numberOfOffsetsPerItem *= 10
|
||||
dividor *= 10
|
||||
maximumNumberOfDecimals += 1
|
||||
}
|
||||
|
||||
let generalBase = Constants.verticalBaseAnchors.first { numberOfOffsetsPerItem > $0 } ?? BaseConstants.defaultVerticalBaseAnchor
|
||||
let base = generalBase * multiplier / dividor
|
||||
|
||||
var verticalValue = (verticalRange.lowerBound / base).rounded(.down) * base
|
||||
let startValue = verticalValue
|
||||
var count = 0
|
||||
if chartVisibility[index] {
|
||||
while verticalValue < verticalRange.upperBound {
|
||||
count += 1
|
||||
verticalValue += base
|
||||
}
|
||||
}
|
||||
return (startValue: startValue, base: base, count: count, maximumNumberOfDecimals: maximumNumberOfDecimals)
|
||||
}
|
||||
|
||||
let totalCount = dividorsAndMultiplers.map { $0.count }.max() ?? 0
|
||||
guard totalCount > 0 else { return }
|
||||
|
||||
let numberFormatter = BaseConstants.chartNumberFormatter
|
||||
for (index, controller) in graphControllers.enumerated() {
|
||||
let (startValue, base, _, maximumNumberOfDecimals) = dividorsAndMultiplers[index]
|
||||
|
||||
let updatedRange = startValue...(startValue + base * CGFloat(totalCount))
|
||||
if controller.verticalScalesRenderer.verticalRange.end != updatedRange {
|
||||
numberFormatter.maximumFractionDigits = maximumNumberOfDecimals
|
||||
|
||||
var verticalLabels: [LinesChartLabel] = []
|
||||
for multipler in 0...(totalCount - 1) {
|
||||
let verticalValue = startValue + base * CGFloat(multipler)
|
||||
let text: String = numberFormatter.string(from: NSNumber(value: Double(verticalValue))) ?? ""
|
||||
verticalLabels.append(LinesChartLabel(value: verticalValue, text: text))
|
||||
}
|
||||
|
||||
controller.verticalScalesRenderer.setup(verticalLimitsLabels: verticalLabels, animated: animated)
|
||||
controller.updateMainChartVerticalRange(range: updatedRange, animated: animated)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override func apply(colorMode: GColorMode, animated: Bool) {
|
||||
horizontalScalesRenderer.labelsColor = colorMode.chartLabelsColor
|
||||
verticalLineRenderer.linesColor = colorMode.chartStrongLinesColor
|
||||
|
||||
for controller in graphControllers {
|
||||
controller.verticalScalesRenderer.horizontalLinesColor = colorMode.chartHelperLinesColor
|
||||
controller.lineBulletsRenderer.setInnerColor(colorMode.chartBackgroundColor, animated: animated)
|
||||
controller.verticalScalesRenderer.axisXColor = colorMode.chartStrongLinesColor
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
//
|
||||
// PercentChartComponentController.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 4/14/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
#if os(macOS)
|
||||
import Cocoa
|
||||
#else
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
class PercentChartComponentController: GeneralChartComponentController {
|
||||
let mainPecentChartRenderer: PecentChartRenderer
|
||||
let horizontalScalesRenderer: HorizontalScalesRenderer
|
||||
let verticalScalesRenderer: VerticalScalesRenderer
|
||||
let verticalLineRenderer: VerticalLinesRenderer
|
||||
let previewPercentChartRenderer: PecentChartRenderer
|
||||
var percentageData: PecentChartRenderer.PercentageData = .blank
|
||||
|
||||
init(isZoomed: Bool,
|
||||
mainPecentChartRenderer: PecentChartRenderer,
|
||||
horizontalScalesRenderer: HorizontalScalesRenderer,
|
||||
verticalScalesRenderer: VerticalScalesRenderer,
|
||||
verticalLineRenderer: VerticalLinesRenderer,
|
||||
previewPercentChartRenderer: PecentChartRenderer) {
|
||||
self.mainPecentChartRenderer = mainPecentChartRenderer
|
||||
self.horizontalScalesRenderer = horizontalScalesRenderer
|
||||
self.verticalScalesRenderer = verticalScalesRenderer
|
||||
self.verticalLineRenderer = verticalLineRenderer
|
||||
self.previewPercentChartRenderer = previewPercentChartRenderer
|
||||
|
||||
super.init(isZoomed: isZoomed)
|
||||
}
|
||||
|
||||
override func initialize(chartsCollection: ChartsCollection, initialDate: Date, totalHorizontalRange _: ClosedRange<CGFloat>, totalVerticalRange _: ClosedRange<CGFloat>) {
|
||||
let components = chartsCollection.chartValues.map { PecentChartRenderer.PercentageData.Component(color: $0.color,
|
||||
values: $0.values.map { CGFloat($0) }) }
|
||||
self.percentageData = PecentChartRenderer.PercentageData(locations: chartsCollection.axisValues.map { CGFloat($0.timeIntervalSince1970) },
|
||||
components: components)
|
||||
let totalHorizontalRange = PecentChartRenderer.PercentageData.horizontalRange(data: self.percentageData) ?? BaseConstants.defaultRange
|
||||
let totalVerticalRange = BaseConstants.defaultRange
|
||||
|
||||
super.initialize(chartsCollection: chartsCollection,
|
||||
initialDate: initialDate,
|
||||
totalHorizontalRange: totalHorizontalRange,
|
||||
totalVerticalRange: totalVerticalRange)
|
||||
|
||||
mainPecentChartRenderer.percentageData = self.percentageData
|
||||
previewPercentChartRenderer.percentageData = self.percentageData
|
||||
|
||||
let axisValues: [CGFloat] = [0, 25, 50, 75, 100]
|
||||
let labels: [LinesChartLabel] = axisValues.map { value in
|
||||
return LinesChartLabel(value: value / 100, text: BaseConstants.detailsNumberFormatter.string(from: NSNumber(value: Double(value))) ?? "")
|
||||
}
|
||||
verticalScalesRenderer.setup(verticalLimitsLabels: labels, animated: false)
|
||||
|
||||
setupMainChart(horizontalRange: initialHorizontalRange, animated: false)
|
||||
setupMainChart(verticalRange: initialVerticalRange, animated: false)
|
||||
previewPercentChartRenderer.setup(verticalRange: totalVerticalRange, animated: false)
|
||||
previewPercentChartRenderer.setup(horizontalRange: totalHorizontalRange, animated: false)
|
||||
updateHorizontalLimitLabels(animated: false)
|
||||
}
|
||||
|
||||
override func willAppear(animated: Bool) {
|
||||
previewPercentChartRenderer.setup(verticalRange: totalVerticalRange, animated: animated)
|
||||
previewPercentChartRenderer.setup(horizontalRange: totalHorizontalRange, animated: animated)
|
||||
|
||||
setConponentsVisible(visible: true, animated: true)
|
||||
|
||||
setupMainChart(verticalRange: initialVerticalRange, animated: animated)
|
||||
setupMainChart(horizontalRange: initialHorizontalRange, animated: animated)
|
||||
|
||||
updatePreviewRangeClosure?(currentChartHorizontalRangeFraction, animated)
|
||||
|
||||
super.willAppear(animated: animated)
|
||||
}
|
||||
|
||||
override func chartRangeDidUpdated(_ updatedRange: ClosedRange<CGFloat>) {
|
||||
super.chartRangeDidUpdated(updatedRange)
|
||||
|
||||
initialHorizontalRange = updatedRange
|
||||
setupMainChart(horizontalRange: updatedRange, animated: false)
|
||||
updateHorizontalLimitLabels(animated: true)
|
||||
}
|
||||
|
||||
func updateHorizontalLimitLabels(animated: Bool) {
|
||||
updateHorizontalLimitLabels(horizontalScalesRenderer: horizontalScalesRenderer,
|
||||
horizontalRange: initialHorizontalRange,
|
||||
scaleType: isZoomed ? .hour : .day,
|
||||
forceUpdate: false,
|
||||
animated: animated)
|
||||
}
|
||||
|
||||
func prepareAppearanceAnimation(horizontalRnage: ClosedRange<CGFloat>) {
|
||||
setupMainChart(horizontalRange: horizontalRnage, animated: false)
|
||||
setConponentsVisible(visible: false, animated: false)
|
||||
}
|
||||
|
||||
func setConponentsVisible(visible: Bool, animated: Bool) {
|
||||
mainPecentChartRenderer.setVisible(visible, animated: animated)
|
||||
horizontalScalesRenderer.setVisible(visible, animated: animated)
|
||||
verticalScalesRenderer.setVisible(visible, animated: animated)
|
||||
verticalLineRenderer.setVisible(visible, animated: animated)
|
||||
previewPercentChartRenderer.setVisible(visible, animated: animated)
|
||||
}
|
||||
|
||||
func setupMainChart(horizontalRange: ClosedRange<CGFloat>, animated: Bool) {
|
||||
mainPecentChartRenderer.setup(horizontalRange: horizontalRange, animated: animated)
|
||||
horizontalScalesRenderer.setup(horizontalRange: horizontalRange, animated: animated)
|
||||
verticalScalesRenderer.setup(horizontalRange: horizontalRange, animated: animated)
|
||||
verticalLineRenderer.setup(horizontalRange: horizontalRange, animated: animated)
|
||||
}
|
||||
|
||||
func setupMainChart(verticalRange: ClosedRange<CGFloat>, animated: Bool) {
|
||||
mainPecentChartRenderer.setup(verticalRange: verticalRange, animated: animated)
|
||||
horizontalScalesRenderer.setup(verticalRange: verticalRange, animated: animated)
|
||||
verticalScalesRenderer.setup(verticalRange: verticalRange, animated: animated)
|
||||
verticalLineRenderer.setup(verticalRange: verticalRange, animated: animated)
|
||||
}
|
||||
|
||||
public override func updateChartsVisibility(visibility: [Bool], animated: Bool) {
|
||||
super.updateChartsVisibility(visibility: visibility, animated: animated)
|
||||
for (index, isVisible) in visibility.enumerated() {
|
||||
mainPecentChartRenderer.setComponentVisible(isVisible, at: index, animated: animated)
|
||||
previewPercentChartRenderer.setComponentVisible(isVisible, at: index, animated: animated)
|
||||
}
|
||||
verticalScalesRenderer.setVisible(visibility.contains(true), animated: animated)
|
||||
}
|
||||
|
||||
override func chartDetailsViewModel(closestDate: Date, pointIndex: Int) -> ChartDetailsViewModel {
|
||||
let visibleValues = visibleDetailsChartValues
|
||||
|
||||
let total = visibleValues.map { $0.values[pointIndex] }.reduce(0, +)
|
||||
|
||||
let values: [ChartDetailsViewModel.Value] = chartsCollection.chartValues.enumerated().map { arg in
|
||||
let (index, component) = arg
|
||||
return ChartDetailsViewModel.Value(prefix: PercentConstants.percentValueFormatter.string(from: component.values[pointIndex] / total * 100),
|
||||
title: component.name,
|
||||
value: BaseConstants.detailsNumberFormatter.string(from: component.values[pointIndex]),
|
||||
color: component.color,
|
||||
visible: chartVisibility[index])
|
||||
}
|
||||
let dateString: String
|
||||
if isZoomed {
|
||||
dateString = BaseConstants.timeDateFormatter.string(from: closestDate)
|
||||
} else {
|
||||
dateString = BaseConstants.headerMediumRangeFormatter.string(from: closestDate)
|
||||
}
|
||||
let viewModel = ChartDetailsViewModel(title: dateString,
|
||||
showArrow: !self.isZoomed,
|
||||
showPrefixes: true,
|
||||
values: values,
|
||||
totalValue: nil,
|
||||
tapAction: { [weak self] in
|
||||
self?.hideDetailsView(animated: true)
|
||||
self?.zoomInOnDateClosure?(closestDate) })
|
||||
return viewModel
|
||||
}
|
||||
|
||||
var currentlyVisiblePercentageData: PecentChartRenderer.PercentageData {
|
||||
var currentPercentageData = percentageData
|
||||
currentPercentageData.components = chartVisibility.enumerated().compactMap { $0.element ? currentPercentageData.components[$0.offset] : nil }
|
||||
return currentPercentageData
|
||||
}
|
||||
|
||||
override var currentMainRangeRenderer: BaseChartRenderer {
|
||||
return mainPecentChartRenderer
|
||||
}
|
||||
|
||||
override var currentPreviewRangeRenderer: BaseChartRenderer {
|
||||
return previewPercentChartRenderer
|
||||
}
|
||||
|
||||
override func showDetailsView(at chartPosition: CGFloat, detailsViewPosition: CGFloat, dataIndex: Int, date: Date, animted: Bool) {
|
||||
super.showDetailsView(at: chartPosition, detailsViewPosition: detailsViewPosition, dataIndex: dataIndex, date: date, animted: animted)
|
||||
verticalLineRenderer.values = [chartPosition]
|
||||
verticalLineRenderer.isEnabled = true
|
||||
}
|
||||
|
||||
override func hideDetailsView(animated: Bool) {
|
||||
super.hideDetailsView(animated: animated)
|
||||
|
||||
verticalLineRenderer.values = []
|
||||
verticalLineRenderer.isEnabled = false
|
||||
}
|
||||
|
||||
override func apply(colorMode: GColorMode, animated: Bool) {
|
||||
super.apply(colorMode: colorMode, animated: animated)
|
||||
|
||||
horizontalScalesRenderer.labelsColor = colorMode.chartLabelsColor
|
||||
verticalScalesRenderer.labelsColor = colorMode.chartLabelsColor
|
||||
verticalScalesRenderer.axisXColor = colorMode.barChartStrongLinesColor
|
||||
verticalScalesRenderer.horizontalLinesColor = colorMode.barChartStrongLinesColor
|
||||
verticalLineRenderer.linesColor = colorMode.chartStrongLinesColor
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
//
|
||||
// PercentPieChartController.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
|
||||
|
||||
enum PercentConstants {
|
||||
static let percentValueFormatter: NumberFormatter = {
|
||||
let formatter = NumberFormatter()
|
||||
formatter.positiveSuffix = "%"
|
||||
return formatter
|
||||
}()
|
||||
}
|
||||
|
||||
private enum Constants {
|
||||
static let zoomedRange = 7
|
||||
}
|
||||
|
||||
public class PercentPieChartController: BaseChartController {
|
||||
let percentController: PercentChartComponentController
|
||||
let pieController: PieChartComponentController
|
||||
let transitionRenderer: PercentPieAnimationRenderer
|
||||
|
||||
override public init(chartsCollection: ChartsCollection) {
|
||||
transitionRenderer = PercentPieAnimationRenderer()
|
||||
percentController = PercentChartComponentController(isZoomed: false,
|
||||
mainPecentChartRenderer: PecentChartRenderer(),
|
||||
horizontalScalesRenderer: HorizontalScalesRenderer(),
|
||||
verticalScalesRenderer: VerticalScalesRenderer(),
|
||||
verticalLineRenderer: VerticalLinesRenderer(),
|
||||
previewPercentChartRenderer: PecentChartRenderer())
|
||||
pieController = PieChartComponentController(isZoomed: true,
|
||||
pieChartRenderer: PieChartRenderer(),
|
||||
previewBarChartRenderer: BarChartRenderer())
|
||||
|
||||
super.init(chartsCollection: chartsCollection)
|
||||
|
||||
[percentController, pieController].forEach { controller in
|
||||
controller.chartFrame = { [unowned self] in self.chartFrame() }
|
||||
controller.cartViewBounds = { [unowned self] in self.cartViewBounds() }
|
||||
controller.zoomInOnDateClosure = { [unowned self] date in
|
||||
self.didTapZoomIn(date: date)
|
||||
}
|
||||
controller.setChartTitleClosure = { [unowned self] (title, animated) in
|
||||
self.setChartTitleClosure?(title, animated)
|
||||
}
|
||||
controller.setDetailsViewPositionClosure = { [unowned self] (position) in
|
||||
self.setDetailsViewPositionClosure?(position)
|
||||
}
|
||||
controller.setDetailsChartVisibleClosure = { [unowned self] (visible, animated) in
|
||||
self.setDetailsChartVisibleClosure?(visible, animated)
|
||||
}
|
||||
controller.setDetailsViewModel = { [unowned self] (viewModel, animated) in
|
||||
self.setDetailsViewModel?(viewModel, animated)
|
||||
}
|
||||
controller.updatePreviewRangeClosure = { [unowned self] (fraction, animated) in
|
||||
self.chartRangeUpdatedClosure?(fraction, animated)
|
||||
}
|
||||
controller.chartRangePagingClosure = { [unowned self] (isEnabled, pageSize) in
|
||||
self.setChartRangePagingEnabled(isEnabled: isEnabled, minimumSelectionSize: pageSize)
|
||||
}
|
||||
}
|
||||
transitionRenderer.isEnabled = false
|
||||
}
|
||||
|
||||
public override var mainChartRenderers: [ChartViewRenderer] {
|
||||
return [percentController.mainPecentChartRenderer,
|
||||
transitionRenderer,
|
||||
percentController.horizontalScalesRenderer,
|
||||
percentController.verticalScalesRenderer,
|
||||
percentController.verticalLineRenderer,
|
||||
pieController.pieChartRenderer,
|
||||
// performanceRenderer
|
||||
]
|
||||
}
|
||||
|
||||
public override var navigationRenderers: [ChartViewRenderer] {
|
||||
return [percentController.previewPercentChartRenderer,
|
||||
pieController.previewBarChartRenderer]
|
||||
}
|
||||
|
||||
public override func initializeChart() {
|
||||
percentController.initialize(chartsCollection: initialChartsCollection,
|
||||
initialDate: Date(),
|
||||
totalHorizontalRange: BaseConstants.defaultRange,
|
||||
totalVerticalRange: BaseConstants.defaultRange)
|
||||
switchToChart(chartsCollection: percentController.chartsCollection, isZoomed: false, animated: false)
|
||||
|
||||
TimeInterval.animationDurationMultipler = 0.00001
|
||||
self.didTapZoomIn(date: Date(timeIntervalSinceReferenceDate: 603849600.0), animated: false)
|
||||
TimeInterval.animationDurationMultipler = 1
|
||||
}
|
||||
|
||||
func switchToChart(chartsCollection: ChartsCollection, isZoomed: Bool, animated: Bool) {
|
||||
if animated {
|
||||
TimeInterval.setDefaultSuration(.expandAnimationDuration)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .expandAnimationDuration) {
|
||||
TimeInterval.setDefaultSuration(.osXDuration)
|
||||
}
|
||||
}
|
||||
|
||||
super.isZoomed = isZoomed
|
||||
if isZoomed {
|
||||
let toHorizontalRange = pieController.initialHorizontalRange
|
||||
|
||||
pieController.updateChartsVisibility(visibility: percentController.chartVisibility, animated: false)
|
||||
pieController.pieChartRenderer.setup(horizontalRange: percentController.currentHorizontalMainChartRange, animated: false)
|
||||
pieController.previewBarChartRenderer.setup(horizontalRange: percentController.currentPreviewHorizontalRange, animated: false)
|
||||
pieController.pieChartRenderer.setVisible(false, animated: false)
|
||||
pieController.previewBarChartRenderer.setVisible(true, animated: false)
|
||||
|
||||
pieController.willAppear(animated: animated)
|
||||
percentController.willDisappear(animated: animated)
|
||||
|
||||
pieController.pieChartRenderer.drawPie = false
|
||||
percentController.mainPecentChartRenderer.isEnabled = false
|
||||
|
||||
setupTransitionRenderer()
|
||||
|
||||
percentController.setupMainChart(horizontalRange: toHorizontalRange, animated: animated)
|
||||
percentController.previewPercentChartRenderer.setup(horizontalRange: toHorizontalRange, animated: animated)
|
||||
percentController.setConponentsVisible(visible: false, animated: animated)
|
||||
|
||||
transitionRenderer.animate(fromDataToPie: true, animated: animated) { [weak self] in
|
||||
self?.pieController.pieChartRenderer.drawPie = true
|
||||
self?.percentController.mainPecentChartRenderer.isEnabled = true
|
||||
}
|
||||
} else {
|
||||
if !pieController.chartsCollection.isBlank {
|
||||
let fromHorizontalRange = pieController.currentHorizontalMainChartRange
|
||||
let toHorizontalRange = percentController.initialHorizontalRange
|
||||
|
||||
pieController.pieChartRenderer.setup(horizontalRange: toHorizontalRange, animated: animated)
|
||||
pieController.previewBarChartRenderer.setup(horizontalRange: toHorizontalRange, animated: animated)
|
||||
pieController.pieChartRenderer.setVisible(false, animated: animated)
|
||||
pieController.previewBarChartRenderer.setVisible(false, animated: animated)
|
||||
|
||||
percentController.updateChartsVisibility(visibility: pieController.chartVisibility, animated: false)
|
||||
percentController.setupMainChart(horizontalRange: fromHorizontalRange, animated: false)
|
||||
percentController.previewPercentChartRenderer.setup(horizontalRange: fromHorizontalRange, animated: false)
|
||||
percentController.setConponentsVisible(visible: false, animated: false)
|
||||
}
|
||||
|
||||
percentController.willAppear(animated: animated)
|
||||
pieController.willDisappear(animated: animated)
|
||||
|
||||
if animated {
|
||||
pieController.pieChartRenderer.drawPie = false
|
||||
percentController.mainPecentChartRenderer.isEnabled = false
|
||||
|
||||
setupTransitionRenderer()
|
||||
|
||||
transitionRenderer.animate(fromDataToPie: false, animated: true) {
|
||||
self.pieController.pieChartRenderer.drawPie = true
|
||||
self.percentController.mainPecentChartRenderer.isEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.setBackButtonVisibilityClosure?(isZoomed, animated)
|
||||
}
|
||||
|
||||
func setupTransitionRenderer() {
|
||||
transitionRenderer.setup(verticalRange: percentController.currentVerticalMainChartRange, animated: false)
|
||||
transitionRenderer.setup(horizontalRange: percentController.currentHorizontalMainChartRange, animated: false)
|
||||
transitionRenderer.visiblePieComponents = pieController.visiblePieDataWithCurrentPreviewRange
|
||||
transitionRenderer.visiblePercentageData = percentController.currentlyVisiblePercentageData
|
||||
}
|
||||
|
||||
public override func updateChartsVisibility(visibility: [Bool], animated: Bool) {
|
||||
if isZoomed {
|
||||
pieController.updateChartsVisibility(visibility: visibility, animated: animated)
|
||||
} else {
|
||||
percentController.updateChartsVisibility(visibility: visibility, animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
var visibleChartValues: [ChartsCollection.Chart] {
|
||||
let visibility = isZoomed ? pieController.chartVisibility : percentController.chartVisibility
|
||||
let collection = isZoomed ? pieController.chartsCollection : percentController.chartsCollection
|
||||
let visibleCharts: [ChartsCollection.Chart] = visibility.enumerated().compactMap { args in
|
||||
args.element ? collection.chartValues[args.offset] : nil
|
||||
}
|
||||
return visibleCharts
|
||||
}
|
||||
|
||||
public override var actualChartVisibility: [Bool] {
|
||||
return isZoomed ? pieController.chartVisibility : percentController.chartVisibility
|
||||
}
|
||||
|
||||
public override var actualChartsCollection: ChartsCollection {
|
||||
let collection = isZoomed ? pieController.chartsCollection : percentController.chartsCollection
|
||||
|
||||
if collection.isBlank {
|
||||
return self.initialChartsCollection
|
||||
}
|
||||
return collection
|
||||
}
|
||||
|
||||
public override func chartInteractionDidBegin(point: CGPoint) {
|
||||
if isZoomed {
|
||||
pieController.chartInteractionDidBegin(point: point)
|
||||
} else {
|
||||
percentController.chartInteractionDidBegin(point: point)
|
||||
}
|
||||
}
|
||||
|
||||
public override func chartInteractionDidEnd() {
|
||||
if isZoomed {
|
||||
pieController.chartInteractionDidEnd()
|
||||
} else {
|
||||
percentController.chartInteractionDidEnd()
|
||||
}
|
||||
}
|
||||
|
||||
public override var drawChartVisibity: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
public override var currentChartHorizontalRangeFraction: ClosedRange<CGFloat> {
|
||||
if isZoomed {
|
||||
return pieController.currentChartHorizontalRangeFraction
|
||||
} else {
|
||||
return percentController.currentChartHorizontalRangeFraction
|
||||
}
|
||||
}
|
||||
|
||||
public override func cancelChartInteraction() {
|
||||
if isZoomed {
|
||||
return pieController.hideDetailsView(animated: true)
|
||||
} else {
|
||||
return percentController.hideDetailsView(animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
func didTapZoomIn(date: Date, animated: Bool) {
|
||||
guard isZoomed == false else { return }
|
||||
cancelChartInteraction()
|
||||
let currentCollection = percentController.chartsCollection
|
||||
let range: Int = Constants.zoomedRange
|
||||
guard let (foundDate, index) = percentController.findClosestDateTo(dateToFind: date) else { return }
|
||||
var lowIndex = max(0, index - range / 2)
|
||||
var highIndex = min(currentCollection.axisValues.count - 1, index + range / 2)
|
||||
if lowIndex == 0 {
|
||||
highIndex = lowIndex + (range - 1)
|
||||
} else if highIndex == currentCollection.axisValues.count - 1 {
|
||||
lowIndex = highIndex - (range - 1)
|
||||
}
|
||||
|
||||
let newValues = currentCollection.chartValues.map { chart in
|
||||
return ChartsCollection.Chart(color: chart.color,
|
||||
name: chart.name,
|
||||
values: Array(chart.values[(lowIndex...highIndex)]))
|
||||
}
|
||||
let newCollection = ChartsCollection(axisValues: Array(currentCollection.axisValues[(lowIndex...highIndex)]),
|
||||
chartValues: newValues)
|
||||
let selectedRange = CGFloat(foundDate.timeIntervalSince1970 - .day)...CGFloat(foundDate.timeIntervalSince1970)
|
||||
pieController.initialize(chartsCollection: newCollection, initialDate: date, totalHorizontalRange: 0...1, totalVerticalRange: 0...1)
|
||||
pieController.initialHorizontalRange = selectedRange
|
||||
|
||||
switchToChart(chartsCollection: newCollection, isZoomed: true, animated: true)
|
||||
}
|
||||
|
||||
public override func didTapZoomIn(date: Date) {
|
||||
self.didTapZoomIn(date: date, animated: true)
|
||||
}
|
||||
|
||||
public override func didTapZoomOut() {
|
||||
self.pieController.deselectSegment(completion: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.switchToChart(chartsCollection: self.percentController.chartsCollection, isZoomed: false, animated: true)
|
||||
})
|
||||
}
|
||||
|
||||
public override func updateChartRange(_ rangeFraction: ClosedRange<CGFloat>) {
|
||||
if isZoomed {
|
||||
return pieController.chartRangeFractionDidUpdated(rangeFraction)
|
||||
} else {
|
||||
return percentController.chartRangeFractionDidUpdated(rangeFraction)
|
||||
}
|
||||
}
|
||||
|
||||
public override func apply(colorMode: GColorMode, animated: Bool) {
|
||||
super.apply(colorMode: colorMode, animated: animated)
|
||||
|
||||
pieController.apply(colorMode: colorMode, animated: animated)
|
||||
percentController.apply(colorMode: colorMode, animated: animated)
|
||||
transitionRenderer.backgroundColor = colorMode.chartBackgroundColor
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
//
|
||||
// PieChartComponentController.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 4/14/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
#if os(macOS)
|
||||
import Cocoa
|
||||
#else
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
class PieChartComponentController: GeneralChartComponentController {
|
||||
let pieChartRenderer: PieChartRenderer
|
||||
let previewBarChartRenderer: BarChartRenderer
|
||||
var barWidth: CGFloat = 1
|
||||
|
||||
var chartBars: BarChartRenderer.BarsData = .blank
|
||||
|
||||
init(isZoomed: Bool,
|
||||
pieChartRenderer: PieChartRenderer,
|
||||
previewBarChartRenderer: BarChartRenderer) {
|
||||
self.pieChartRenderer = pieChartRenderer
|
||||
self.previewBarChartRenderer = previewBarChartRenderer
|
||||
super.init(isZoomed: isZoomed)
|
||||
}
|
||||
|
||||
override func initialize(chartsCollection: ChartsCollection, initialDate: Date, totalHorizontalRange _: ClosedRange<CGFloat>, totalVerticalRange _: ClosedRange<CGFloat>) {
|
||||
let (width, chartBars, totalHorizontalRange, _) = BarChartRenderer.BarsData.initialComponents(chartsCollection: chartsCollection)
|
||||
self.barWidth = width
|
||||
self.chartBars = chartBars
|
||||
super.initialize(chartsCollection: chartsCollection,
|
||||
initialDate: initialDate,
|
||||
totalHorizontalRange: totalHorizontalRange,
|
||||
totalVerticalRange: BaseConstants.defaultRange)
|
||||
|
||||
self.previewBarChartRenderer.bars = chartBars
|
||||
self.previewBarChartRenderer.fillToTop = true
|
||||
|
||||
pieChartRenderer.valuesFormatter = PercentConstants.percentValueFormatter
|
||||
pieChartRenderer.setup(horizontalRange: initialHorizontalRange, animated: false)
|
||||
previewBarChartRenderer.setup(verticalRange: initialVerticalRange, animated: false)
|
||||
previewBarChartRenderer.setup(horizontalRange: initialHorizontalRange, animated: false)
|
||||
|
||||
pieChartRenderer.updatePercentageData(pieDataWithCurrentPreviewRange, animated: false)
|
||||
pieChartRenderer.selectSegmentAt(at: nil, animated: false)
|
||||
}
|
||||
|
||||
private var pieDataWithCurrentPreviewRange: [PieChartRenderer.PieComponent] {
|
||||
let range = currentHorizontalMainChartRange
|
||||
var pieComponents = chartsCollection.chartValues.map { PieChartRenderer.PieComponent(color: $0.color,
|
||||
value: 0) }
|
||||
guard var valueIndex = chartsCollection.axisValues.firstIndex(where: { CGFloat($0.timeIntervalSince1970) > (range.lowerBound + 1)}) else {
|
||||
return pieComponents
|
||||
}
|
||||
var count = 0
|
||||
while valueIndex < chartsCollection.axisValues.count, CGFloat(chartsCollection.axisValues[valueIndex].timeIntervalSince1970) <= range.upperBound {
|
||||
count += 1
|
||||
for pieIndex in pieComponents.indices {
|
||||
pieComponents[pieIndex].value += CGFloat(chartsCollection.chartValues[pieIndex].values[valueIndex])
|
||||
}
|
||||
valueIndex += 1
|
||||
}
|
||||
return pieComponents
|
||||
}
|
||||
|
||||
var visiblePieDataWithCurrentPreviewRange: [PieChartRenderer.PieComponent] {
|
||||
let currentData = pieDataWithCurrentPreviewRange
|
||||
return chartVisibility.enumerated().compactMap { $0.element ? currentData[$0.offset] : nil }
|
||||
}
|
||||
|
||||
override func willAppear(animated: Bool) {
|
||||
pieChartRenderer.setup(horizontalRange: initialHorizontalRange, animated: animated)
|
||||
pieChartRenderer.setVisible(true, animated: animated)
|
||||
|
||||
previewBarChartRenderer.setup(verticalRange: totalVerticalRange, animated: animated)
|
||||
previewBarChartRenderer.setup(horizontalRange: totalHorizontalRange, animated: animated)
|
||||
previewBarChartRenderer.setVisible(true, animated: animated)
|
||||
|
||||
updatePreviewRangeClosure?(currentChartHorizontalRangeFraction, animated)
|
||||
pieChartRenderer.updatePercentageData(pieDataWithCurrentPreviewRange, animated: false)
|
||||
|
||||
super.willAppear(animated: animated)
|
||||
}
|
||||
|
||||
override func setupChartRangePaging() {
|
||||
let valuesCount = chartsCollection.axisValues.count
|
||||
guard valuesCount > 0 else { return }
|
||||
chartRangePagingClosure?(true, 1.0 / CGFloat(valuesCount))
|
||||
}
|
||||
|
||||
override func chartRangeDidUpdated(_ updatedRange: ClosedRange<CGFloat>) {
|
||||
if isChartInteractionBegun {
|
||||
chartInteractionDidBegin(point: lastChartInteractionPoint)
|
||||
}
|
||||
initialHorizontalRange = updatedRange
|
||||
|
||||
setupMainChart(horizontalRange: updatedRange, animated: true)
|
||||
updateSelectedDataLabelIfNeeded()
|
||||
}
|
||||
|
||||
func setupMainChart(horizontalRange: ClosedRange<CGFloat>, animated: Bool) {
|
||||
pieChartRenderer.setup(horizontalRange: horizontalRange, animated: animated)
|
||||
pieChartRenderer.updatePercentageData(pieDataWithCurrentPreviewRange, animated: animated)
|
||||
}
|
||||
|
||||
public override func updateChartsVisibility(visibility: [Bool], animated: Bool) {
|
||||
super.updateChartsVisibility(visibility: visibility, animated: animated)
|
||||
for (index, isVisible) in visibility.enumerated() {
|
||||
pieChartRenderer.setComponentVisible(isVisible, at: index, animated: animated)
|
||||
previewBarChartRenderer.setComponentVisible(isVisible, at: index, animated: animated)
|
||||
}
|
||||
if let segment = pieChartRenderer.selectedSegment {
|
||||
if !visibility[segment] {
|
||||
pieChartRenderer.selectSegmentAt(at: nil, animated: true)
|
||||
}
|
||||
}
|
||||
updateSelectedDataLabelIfNeeded()
|
||||
}
|
||||
|
||||
func deselectSegment(completion: @escaping () -> Void) {
|
||||
if pieChartRenderer.hasSelectedSegments {
|
||||
hideDetailsView(animated: true)
|
||||
pieChartRenderer.selectSegmentAt(at: nil, animated: true)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .defaultDuration / 2) {
|
||||
completion()
|
||||
}
|
||||
} else {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
||||
func updateSelectedDataLabelIfNeeded() {
|
||||
if let segment = pieChartRenderer.selectedSegment {
|
||||
self.setDetailsChartVisibleClosure?(true, true)
|
||||
self.setDetailsViewModel?(chartDetailsViewModel(segmentInde: segment), false)
|
||||
self.setDetailsViewPositionClosure?(chartFrame().width / 4)
|
||||
} else {
|
||||
self.setDetailsChartVisibleClosure?(false, true)
|
||||
}
|
||||
}
|
||||
|
||||
func chartDetailsViewModel(segmentInde: Int) -> ChartDetailsViewModel {
|
||||
let pieItem = pieDataWithCurrentPreviewRange[segmentInde]
|
||||
let title = chartsCollection.chartValues[segmentInde].name
|
||||
let valueString = BaseConstants.detailsNumberFormatter.string(from: pieItem.value)
|
||||
let viewModel = ChartDetailsViewModel(title: "",
|
||||
showArrow: false,
|
||||
showPrefixes: false,
|
||||
values: [ChartDetailsViewModel.Value(prefix: nil,
|
||||
title: title,
|
||||
value: valueString,
|
||||
color: pieItem.color,
|
||||
visible: true)],
|
||||
totalValue: nil,
|
||||
tapAction: nil)
|
||||
return viewModel
|
||||
}
|
||||
|
||||
override var currentMainRangeRenderer: BaseChartRenderer {
|
||||
return pieChartRenderer
|
||||
}
|
||||
|
||||
override var currentPreviewRangeRenderer: BaseChartRenderer {
|
||||
return previewBarChartRenderer
|
||||
}
|
||||
|
||||
var lastInteractionPoint: CGPoint = .zero
|
||||
public override func chartInteractionDidBegin(point: CGPoint) {
|
||||
lastInteractionPoint = point
|
||||
}
|
||||
|
||||
public override func chartInteractionDidEnd() {
|
||||
if let segment = pieChartRenderer.selectedItemIndex(at: lastInteractionPoint) {
|
||||
if pieChartRenderer.selectedSegment == segment {
|
||||
pieChartRenderer.selectSegmentAt(at: nil, animated: true)
|
||||
} else {
|
||||
pieChartRenderer.selectSegmentAt(at: segment, animated: true)
|
||||
}
|
||||
updateSelectedDataLabelIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
override func hideDetailsView(animated: Bool) {
|
||||
pieChartRenderer.selectSegmentAt(at: nil, animated: animated)
|
||||
updateSelectedDataLabelIfNeeded()
|
||||
}
|
||||
|
||||
override func updateChartRangeTitle(animated: Bool) {
|
||||
let fromDate = Date(timeIntervalSince1970: TimeInterval(currentHorizontalMainChartRange.lowerBound) + .day + 1)
|
||||
let toDate = Date(timeIntervalSince1970: TimeInterval(currentHorizontalMainChartRange.upperBound))
|
||||
if Calendar.utc.startOfDay(for: fromDate) == Calendar.utc.startOfDay(for: toDate) {
|
||||
let stirng = BaseConstants.headerFullZoomedFormatter.string(from: fromDate)
|
||||
self.setChartTitleClosure?(stirng, animated)
|
||||
} else {
|
||||
let stirng = "\(BaseConstants.headerMediumRangeFormatter.string(from: fromDate)) - \(BaseConstants.headerMediumRangeFormatter.string(from: toDate))"
|
||||
self.setChartTitleClosure?(stirng, animated)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
//
|
||||
// BarsComponentController.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 4/14/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
#if os(macOS)
|
||||
import Cocoa
|
||||
#else
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
class BarsComponentController: GeneralChartComponentController {
|
||||
let mainBarsRenderer: BarChartRenderer
|
||||
let horizontalScalesRenderer: HorizontalScalesRenderer
|
||||
let verticalScalesRenderer: VerticalScalesRenderer
|
||||
let secondVerticalScalesRenderer: VerticalScalesRenderer?
|
||||
|
||||
let previewBarsChartRenderer: BarChartRenderer
|
||||
private(set) var barsWidth: CGFloat = 1
|
||||
|
||||
private (set) var chartBars: BarChartRenderer.BarsData = .blank
|
||||
|
||||
init(isZoomed: Bool,
|
||||
mainBarsRenderer: BarChartRenderer,
|
||||
horizontalScalesRenderer: HorizontalScalesRenderer,
|
||||
verticalScalesRenderer: VerticalScalesRenderer,
|
||||
previewBarsChartRenderer: BarChartRenderer) {
|
||||
self.mainBarsRenderer = mainBarsRenderer
|
||||
self.horizontalScalesRenderer = horizontalScalesRenderer
|
||||
self.verticalScalesRenderer = verticalScalesRenderer
|
||||
self.previewBarsChartRenderer = previewBarsChartRenderer
|
||||
|
||||
self.secondVerticalScalesRenderer = VerticalScalesRenderer()
|
||||
self.secondVerticalScalesRenderer?.isRightAligned = true
|
||||
|
||||
self.mainBarsRenderer.optimizationLevel = BaseConstants.barsChartOptimizationLevel
|
||||
self.previewBarsChartRenderer.optimizationLevel = BaseConstants.barsChartOptimizationLevel
|
||||
|
||||
super.init(isZoomed: isZoomed)
|
||||
}
|
||||
|
||||
override func initialize(chartsCollection: ChartsCollection, initialDate: Date, totalHorizontalRange _: ClosedRange<CGFloat>, totalVerticalRange _: ClosedRange<CGFloat>) {
|
||||
let (width, chartBars, totalHorizontalRange, totalVerticalRange) = BarChartRenderer.BarsData.initialComponents(chartsCollection: chartsCollection)
|
||||
self.chartBars = chartBars
|
||||
self.barsWidth = width
|
||||
|
||||
super.initialize(chartsCollection: chartsCollection,
|
||||
initialDate: initialDate,
|
||||
totalHorizontalRange: totalHorizontalRange,
|
||||
totalVerticalRange: totalVerticalRange)
|
||||
}
|
||||
|
||||
override func setupInitialChartRange(initialDate: Date) {
|
||||
guard let first = chartsCollection.axisValues.first?.timeIntervalSince1970,
|
||||
let last = chartsCollection.axisValues.last?.timeIntervalSince1970 else { return }
|
||||
|
||||
let rangeStart = CGFloat(first)
|
||||
let rangeEnd = CGFloat(last)
|
||||
|
||||
if isZoomed {
|
||||
let initalDate = CGFloat(initialDate.timeIntervalSince1970)
|
||||
|
||||
initialHorizontalRange = max(initalDate - barsWidth, rangeStart)...min(initalDate + GeneralChartComponentConstants.defaultZoomedRangeLength - barsWidth, rangeEnd)
|
||||
initialVerticalRange = totalVerticalRange
|
||||
} else {
|
||||
super.setupInitialChartRange(initialDate: initialDate)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override func willAppear(animated: Bool) {
|
||||
mainBarsRenderer.bars = self.chartBars
|
||||
previewBarsChartRenderer.bars = self.chartBars
|
||||
|
||||
previewBarsChartRenderer.setup(verticalRange: 0...117278, animated: animated)
|
||||
previewBarsChartRenderer.setup(horizontalRange: totalHorizontalRange, animated: animated)
|
||||
|
||||
setupMainChart(verticalRange: 0...117278, animated: animated)
|
||||
setupMainChart(horizontalRange: initialHorizontalRange, animated: animated)
|
||||
|
||||
updateChartVerticalRanges(horizontalRange: initialHorizontalRange, animated: animated)
|
||||
|
||||
super.willAppear(animated: animated)
|
||||
|
||||
updatePreviewRangeClosure?(currentChartHorizontalRangeFraction, animated)
|
||||
setConponentsVisible(visible: true, animated: animated)
|
||||
updateHorizontalLimitLabels(animated: animated, forceUpdate: true)
|
||||
}
|
||||
|
||||
override func chartRangeDidUpdated(_ updatedRange: ClosedRange<CGFloat>) {
|
||||
super.chartRangeDidUpdated(updatedRange)
|
||||
if !isZoomed {
|
||||
initialHorizontalRange = updatedRange
|
||||
}
|
||||
setupMainChart(horizontalRange: updatedRange, animated: false)
|
||||
updateHorizontalLimitLabels(animated: true, forceUpdate: false)
|
||||
updateChartVerticalRanges(horizontalRange: updatedRange, animated: true)
|
||||
}
|
||||
|
||||
func updateHorizontalLimitLabels(animated: Bool, forceUpdate: Bool) {
|
||||
updateHorizontalLimitLabels(horizontalScalesRenderer: horizontalScalesRenderer,
|
||||
horizontalRange: currentHorizontalMainChartRange,
|
||||
scaleType: isZoomed ? .hour : .day,
|
||||
forceUpdate: forceUpdate,
|
||||
animated: animated)
|
||||
}
|
||||
|
||||
func prepareAppearanceAnimation(horizontalRnage: ClosedRange<CGFloat>) {
|
||||
setupMainChart(horizontalRange: horizontalRnage, animated: false)
|
||||
setConponentsVisible(visible: false, animated: false)
|
||||
}
|
||||
|
||||
func setConponentsVisible(visible: Bool, animated: Bool) {
|
||||
mainBarsRenderer.setVisible(visible, animated: animated)
|
||||
horizontalScalesRenderer.setVisible(visible, animated: animated)
|
||||
verticalScalesRenderer.setVisible(visible, animated: animated)
|
||||
previewBarsChartRenderer.setVisible(visible, animated: animated)
|
||||
|
||||
secondVerticalScalesRenderer?.setVisible(visible, animated: animated)
|
||||
}
|
||||
|
||||
func setupMainChart(horizontalRange: ClosedRange<CGFloat>, animated: Bool) {
|
||||
mainBarsRenderer.setup(horizontalRange: horizontalRange, animated: animated)
|
||||
horizontalScalesRenderer.setup(horizontalRange: horizontalRange, animated: animated)
|
||||
verticalScalesRenderer.setup(horizontalRange: horizontalRange, animated: animated)
|
||||
|
||||
secondVerticalScalesRenderer?.setup(horizontalRange: horizontalRange, animated: animated)
|
||||
}
|
||||
|
||||
var visibleBars: BarChartRenderer.BarsData {
|
||||
let visibleComponents: [BarChartRenderer.BarsData.Component] = chartVisibility.enumerated().compactMap { args in
|
||||
args.element ? chartBars.components[args.offset] : nil
|
||||
}
|
||||
return BarChartRenderer.BarsData(barWidth: chartBars.barWidth,
|
||||
locations: chartBars.locations,
|
||||
components: visibleComponents)
|
||||
}
|
||||
|
||||
func updateChartVerticalRanges(horizontalRange: ClosedRange<CGFloat>, animated: Bool) {
|
||||
if let range = BarChartRenderer.BarsData.verticalRange(bars: visibleBars,
|
||||
calculatingRange: horizontalRange,
|
||||
addBounds: true) {
|
||||
let (range, labels) = verticalLimitsLabels(verticalRange: range)
|
||||
if verticalScalesRenderer.verticalRange.end != range {
|
||||
verticalScalesRenderer.setup(verticalLimitsLabels: labels, animated: animated)
|
||||
}
|
||||
verticalScalesRenderer.setVisible(true, animated: animated)
|
||||
|
||||
setupMainChart(verticalRange: range, animated: animated)
|
||||
|
||||
let (secondRange, secondLabels) = verticalLimitsLabels(verticalRange: range)
|
||||
if secondVerticalScalesRenderer?.verticalRange.end != secondRange {
|
||||
secondVerticalScalesRenderer?.setup(verticalLimitsLabels: secondLabels, animated: animated)
|
||||
}
|
||||
secondVerticalScalesRenderer?.setVisible(true, animated: animated)
|
||||
} else {
|
||||
verticalScalesRenderer.setVisible(false, animated: animated)
|
||||
secondVerticalScalesRenderer?.setVisible(false, animated: animated)
|
||||
}
|
||||
|
||||
if let range = BarChartRenderer.BarsData.verticalRange(bars: visibleBars) {
|
||||
previewBarsChartRenderer.setup(verticalRange: range, animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
func setupMainChart(verticalRange: ClosedRange<CGFloat>, animated: Bool) {
|
||||
var verticalRange = verticalRange
|
||||
if verticalRange.upperBound > 2000 && verticalRange.upperBound < 10000 {
|
||||
verticalRange = 0...117278
|
||||
}
|
||||
mainBarsRenderer.setup(verticalRange: verticalRange, animated: animated)
|
||||
horizontalScalesRenderer.setup(verticalRange: verticalRange, animated: animated)
|
||||
verticalScalesRenderer.setup(verticalRange: verticalRange, animated: animated)
|
||||
secondVerticalScalesRenderer?.setup(verticalRange: verticalRange, animated: animated)
|
||||
}
|
||||
|
||||
public override func updateChartsVisibility(visibility: [Bool], animated: Bool) {
|
||||
super.updateChartsVisibility(visibility: visibility, animated: animated)
|
||||
for (index, isVisible) in visibility.enumerated() {
|
||||
mainBarsRenderer.setComponentVisible(isVisible, at: index, animated: animated)
|
||||
previewBarsChartRenderer.setComponentVisible(isVisible, at: index, animated: animated)
|
||||
}
|
||||
updateChartVerticalRanges(horizontalRange: currentHorizontalMainChartRange, animated: true)
|
||||
}
|
||||
|
||||
var visibleChartValues: [ChartsCollection.Chart] {
|
||||
let visibleCharts: [ChartsCollection.Chart] = chartVisibility.enumerated().compactMap { args in
|
||||
args.element ? chartsCollection.chartValues[args.offset] : nil
|
||||
}
|
||||
return visibleCharts
|
||||
}
|
||||
|
||||
override func chartDetailsViewModel(closestDate: Date, pointIndex: Int) -> ChartDetailsViewModel {
|
||||
var viewModel = super.chartDetailsViewModel(closestDate: closestDate, pointIndex: pointIndex)
|
||||
let visibleChartValues = self.visibleChartValues
|
||||
let totalSumm: CGFloat = visibleChartValues.map { CGFloat($0.values[pointIndex]) }.reduce(0, +)
|
||||
|
||||
viewModel.totalValue = ChartDetailsViewModel.Value(prefix: nil,
|
||||
title: "Total",
|
||||
value: BaseConstants.detailsNumberFormatter.string(from: totalSumm),
|
||||
color: .white,
|
||||
visible: visibleChartValues.count > 1)
|
||||
return viewModel
|
||||
}
|
||||
|
||||
override var currentMainRangeRenderer: BaseChartRenderer {
|
||||
return mainBarsRenderer
|
||||
}
|
||||
|
||||
override var currentPreviewRangeRenderer: BaseChartRenderer {
|
||||
return previewBarsChartRenderer
|
||||
}
|
||||
|
||||
override func showDetailsView(at chartPosition: CGFloat, detailsViewPosition: CGFloat, dataIndex: Int, date: Date, animted: Bool) {
|
||||
let rangeWithOffset = detailsViewPosition - barsWidth / currentHorizontalMainChartRange.distance * chartFrame().width / 2
|
||||
super.showDetailsView(at: chartPosition, detailsViewPosition: rangeWithOffset, dataIndex: dataIndex, date: date, animted: animted)
|
||||
mainBarsRenderer.setSelectedIndex(dataIndex, animated: true)
|
||||
}
|
||||
|
||||
override func hideDetailsView(animated: Bool) {
|
||||
super.hideDetailsView(animated: animated)
|
||||
|
||||
mainBarsRenderer.setSelectedIndex(nil, animated: animated)
|
||||
}
|
||||
override func apply(colorMode: GColorMode, animated: Bool) {
|
||||
super.apply(colorMode: colorMode, animated: animated)
|
||||
|
||||
horizontalScalesRenderer.labelsColor = colorMode.chartLabelsColor
|
||||
verticalScalesRenderer.labelsColor = colorMode.chartLabelsColor
|
||||
verticalScalesRenderer.axisXColor = colorMode.barChartStrongLinesColor
|
||||
verticalScalesRenderer.horizontalLinesColor = colorMode.barChartStrongLinesColor
|
||||
mainBarsRenderer.update(backgroundColor: colorMode.chartBackgroundColor, animated: false)
|
||||
previewBarsChartRenderer.update(backgroundColor: colorMode.chartBackgroundColor, animated: false)
|
||||
|
||||
secondVerticalScalesRenderer?.labelsColor = colorMode.chartLabelsColor
|
||||
secondVerticalScalesRenderer?.axisXColor = colorMode.barChartStrongLinesColor
|
||||
secondVerticalScalesRenderer?.horizontalLinesColor = colorMode.barChartStrongLinesColor
|
||||
}
|
||||
|
||||
override func updateChartRangeTitle(animated: Bool) {
|
||||
let fromDate = Date(timeIntervalSince1970: TimeInterval(currentHorizontalMainChartRange.lowerBound + barsWidth))
|
||||
let toDate = Date(timeIntervalSince1970: TimeInterval(currentHorizontalMainChartRange.upperBound))
|
||||
if Calendar.utc.startOfDay(for: fromDate) == Calendar.utc.startOfDay(for: toDate) {
|
||||
let stirng = BaseConstants.headerFullZoomedFormatter.string(from: fromDate)
|
||||
self.setChartTitleClosure?(stirng, animated)
|
||||
} else {
|
||||
let stirng = "\(BaseConstants.headerMediumRangeFormatter.string(from: fromDate)) - \(BaseConstants.headerMediumRangeFormatter.string(from: toDate))"
|
||||
self.setChartTitleClosure?(stirng, animated)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
//
|
||||
// DailyBarsChartController.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 class DailyBarsChartController: BaseChartController {
|
||||
let barsController: BarsComponentController
|
||||
let linesController: LinesComponentController
|
||||
|
||||
override public init(chartsCollection: ChartsCollection) {
|
||||
let horizontalScalesRenderer = HorizontalScalesRenderer()
|
||||
let verticalScalesRenderer = VerticalScalesRenderer()
|
||||
barsController = BarsComponentController(isZoomed: false,
|
||||
mainBarsRenderer: BarChartRenderer(),
|
||||
horizontalScalesRenderer: horizontalScalesRenderer,
|
||||
verticalScalesRenderer: verticalScalesRenderer,
|
||||
previewBarsChartRenderer: BarChartRenderer())
|
||||
linesController = LinesComponentController(isZoomed: true,
|
||||
userLinesTransitionAnimation: false,
|
||||
mainLinesRenderer: LinesChartRenderer(),
|
||||
horizontalScalesRenderer: horizontalScalesRenderer,
|
||||
verticalScalesRenderer: verticalScalesRenderer,
|
||||
verticalLineRenderer: VerticalLinesRenderer(),
|
||||
lineBulletsRenderer: LineBulletsRenderer(),
|
||||
previewLinesChartRenderer: LinesChartRenderer())
|
||||
|
||||
super.init(chartsCollection: chartsCollection)
|
||||
|
||||
[barsController, linesController].forEach { controller in
|
||||
controller.chartFrame = { [unowned self] in self.chartFrame() }
|
||||
controller.cartViewBounds = { [unowned self] in self.cartViewBounds() }
|
||||
controller.zoomInOnDateClosure = { [unowned self] date in
|
||||
self.didTapZoomIn(date: date)
|
||||
}
|
||||
controller.setChartTitleClosure = { [unowned self] (title, animated) in
|
||||
self.setChartTitleClosure?(title, animated)
|
||||
}
|
||||
controller.setDetailsViewPositionClosure = { [unowned self] (position) in
|
||||
self.setDetailsViewPositionClosure?(position)
|
||||
}
|
||||
controller.setDetailsChartVisibleClosure = { [unowned self] (visible, animated) in
|
||||
self.setDetailsChartVisibleClosure?(visible, animated)
|
||||
}
|
||||
controller.setDetailsViewModel = { [unowned self] (viewModel, animated) in
|
||||
self.setDetailsViewModel?(viewModel, animated)
|
||||
}
|
||||
controller.updatePreviewRangeClosure = { [unowned self] (fraction, animated) in
|
||||
self.chartRangeUpdatedClosure?(fraction, animated)
|
||||
}
|
||||
controller.chartRangePagingClosure = { [unowned self] (isEnabled, pageSize) in
|
||||
self.setChartRangePagingEnabled(isEnabled: isEnabled, minimumSelectionSize: pageSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override var mainChartRenderers: [ChartViewRenderer] {
|
||||
return [barsController.mainBarsRenderer,
|
||||
linesController.mainLinesRenderer,
|
||||
barsController.horizontalScalesRenderer,
|
||||
barsController.verticalScalesRenderer,
|
||||
linesController.verticalLineRenderer,
|
||||
linesController.lineBulletsRenderer,
|
||||
// performanceRenderer
|
||||
]
|
||||
}
|
||||
|
||||
public override var navigationRenderers: [ChartViewRenderer] {
|
||||
return [barsController.previewBarsChartRenderer,
|
||||
linesController.previewLinesChartRenderer]
|
||||
}
|
||||
|
||||
public override func initializeChart() {
|
||||
barsController.initialize(chartsCollection: initialChartsCollection,
|
||||
initialDate: Date(),
|
||||
totalHorizontalRange: BaseConstants.defaultRange,
|
||||
totalVerticalRange: BaseConstants.defaultRange)
|
||||
switchToChart(chartsCollection: barsController.chartsCollection, isZoomed: false, animated: false)
|
||||
}
|
||||
|
||||
func switchToChart(chartsCollection: ChartsCollection, isZoomed: Bool, animated: Bool) {
|
||||
if animated {
|
||||
TimeInterval.setDefaultSuration(.expandAnimationDuration)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .expandAnimationDuration) {
|
||||
TimeInterval.setDefaultSuration(.osXDuration)
|
||||
}
|
||||
}
|
||||
|
||||
super.isZoomed = isZoomed
|
||||
if isZoomed {
|
||||
let toHorizontalRange = linesController.initialHorizontalRange
|
||||
let destinationHorizontalRange = (toHorizontalRange.lowerBound - barsController.barsWidth)...(toHorizontalRange.upperBound - barsController.barsWidth)
|
||||
let initialChartVerticalRange = lineProportionAnimationRange()
|
||||
|
||||
linesController.mainLinesRenderer.setup(horizontalRange: barsController.currentHorizontalMainChartRange, animated: false)
|
||||
linesController.previewLinesChartRenderer.setup(horizontalRange: barsController.currentPreviewHorizontalRange, animated: false)
|
||||
linesController.mainLinesRenderer.setup(verticalRange: initialChartVerticalRange, animated: false)
|
||||
linesController.previewLinesChartRenderer.setup(verticalRange: initialChartVerticalRange, animated: false)
|
||||
linesController.mainLinesRenderer.setVisible(false, animated: false)
|
||||
linesController.previewLinesChartRenderer.setVisible(false, animated: false)
|
||||
|
||||
barsController.setupMainChart(horizontalRange: destinationHorizontalRange, animated: animated)
|
||||
barsController.previewBarsChartRenderer.setup(horizontalRange: linesController.totalHorizontalRange, animated: animated)
|
||||
barsController.mainBarsRenderer.setVisible(false, animated: animated)
|
||||
barsController.previewBarsChartRenderer.setVisible(false, animated: animated)
|
||||
|
||||
linesController.willAppear(animated: animated)
|
||||
barsController.willDisappear(animated: animated)
|
||||
|
||||
linesController.updateChartsVisibility(visibility: linesController.chartLines.map { _ in true }, animated: false)
|
||||
} else {
|
||||
if !linesController.chartsCollection.isBlank {
|
||||
barsController.hideDetailsView(animated: false)
|
||||
let visibleVerticalRange = BarChartRenderer.BarsData.verticalRange(bars: barsController.visibleBars,
|
||||
calculatingRange: barsController.initialHorizontalRange) ?? BaseConstants.defaultRange
|
||||
barsController.mainBarsRenderer.setup(verticalRange: visibleVerticalRange, animated: false)
|
||||
|
||||
let toHorizontalRange = barsController.initialHorizontalRange
|
||||
let destinationChartVerticalRange = lineProportionAnimationRange()
|
||||
|
||||
linesController.setupMainChart(horizontalRange: toHorizontalRange, animated: animated)
|
||||
linesController.mainLinesRenderer.setup(verticalRange: destinationChartVerticalRange, animated: animated)
|
||||
linesController.previewLinesChartRenderer.setup(verticalRange: destinationChartVerticalRange, animated: animated)
|
||||
linesController.previewLinesChartRenderer.setup(horizontalRange: barsController.totalHorizontalRange, animated: animated)
|
||||
linesController.mainLinesRenderer.setVisible(false, animated: animated)
|
||||
linesController.previewLinesChartRenderer.setVisible(false, animated: animated)
|
||||
}
|
||||
|
||||
barsController.willAppear(animated: animated)
|
||||
linesController.willDisappear(animated: animated)
|
||||
}
|
||||
|
||||
self.setBackButtonVisibilityClosure?(isZoomed, animated)
|
||||
self.refreshChartToolsClosure?(animated)
|
||||
}
|
||||
|
||||
public override func updateChartsVisibility(visibility: [Bool], animated: Bool) {
|
||||
if isZoomed {
|
||||
linesController.updateChartsVisibility(visibility: visibility, animated: animated)
|
||||
} else {
|
||||
barsController.updateChartsVisibility(visibility: visibility, animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
var visibleChartValues: [ChartsCollection.Chart] {
|
||||
let visibility = isZoomed ? linesController.chartVisibility : barsController.chartVisibility
|
||||
let collection = isZoomed ? linesController.chartsCollection : barsController.chartsCollection
|
||||
let visibleCharts: [ChartsCollection.Chart] = visibility.enumerated().compactMap { args in
|
||||
args.element ? collection.chartValues[args.offset] : nil
|
||||
}
|
||||
return visibleCharts
|
||||
}
|
||||
|
||||
public override var actualChartVisibility: [Bool] {
|
||||
return isZoomed ? linesController.chartVisibility : barsController.chartVisibility
|
||||
}
|
||||
|
||||
public override var actualChartsCollection: ChartsCollection {
|
||||
let collection = isZoomed ? linesController.chartsCollection : barsController.chartsCollection
|
||||
|
||||
if collection.isBlank {
|
||||
return self.initialChartsCollection
|
||||
}
|
||||
return collection
|
||||
}
|
||||
|
||||
public override func chartInteractionDidBegin(point: CGPoint) {
|
||||
if isZoomed {
|
||||
linesController.chartInteractionDidBegin(point: point)
|
||||
} else {
|
||||
barsController.chartInteractionDidBegin(point: point)
|
||||
}
|
||||
}
|
||||
|
||||
public override func chartInteractionDidEnd() {
|
||||
if isZoomed {
|
||||
linesController.chartInteractionDidEnd()
|
||||
} else {
|
||||
barsController.chartInteractionDidEnd()
|
||||
}
|
||||
}
|
||||
|
||||
public override var currentChartHorizontalRangeFraction: ClosedRange<CGFloat> {
|
||||
if isZoomed {
|
||||
return linesController.currentChartHorizontalRangeFraction
|
||||
} else {
|
||||
return barsController.currentChartHorizontalRangeFraction
|
||||
}
|
||||
}
|
||||
|
||||
public override func cancelChartInteraction() {
|
||||
if isZoomed {
|
||||
return linesController.hideDetailsView(animated: true)
|
||||
} else {
|
||||
return barsController.hideDetailsView(animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
public override func didTapZoomIn(date: Date) {
|
||||
guard isZoomed == false else { return }
|
||||
if isZoomed {
|
||||
return linesController.hideDetailsView(animated: true)
|
||||
}
|
||||
self.getDetailsData?(date, { updatedCollection in
|
||||
if let updatedCollection = updatedCollection {
|
||||
self.linesController.initialize(chartsCollection: updatedCollection,
|
||||
initialDate: date,
|
||||
totalHorizontalRange: 0...1,
|
||||
totalVerticalRange: 0...1)
|
||||
self.switchToChart(chartsCollection: updatedCollection, isZoomed: true, animated: true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func lineProportionAnimationRange() -> ClosedRange<CGFloat> {
|
||||
let visibleLines = self.barsController.chartVisibility.enumerated().compactMap { $0.element ? self.linesController.chartLines[$0.offset] : nil }
|
||||
let linesRange = LinesChartRenderer.LineData.verticalRange(lines: visibleLines) ?? BaseConstants.defaultRange
|
||||
let barsRange = BarChartRenderer.BarsData.verticalRange(bars: self.barsController.visibleBars,
|
||||
calculatingRange: self.linesController.totalHorizontalRange) ?? BaseConstants.defaultRange
|
||||
let range = 0...(linesRange.upperBound / barsRange.distance * self.barsController.currentVerticalMainChartRange.distance)
|
||||
return range
|
||||
}
|
||||
|
||||
public override func didTapZoomOut() {
|
||||
cancelChartInteraction()
|
||||
switchToChart(chartsCollection: barsController.chartsCollection, isZoomed: false, animated: true)
|
||||
}
|
||||
|
||||
public override func updateChartRange(_ rangeFraction: ClosedRange<CGFloat>) {
|
||||
if isZoomed {
|
||||
return linesController.chartRangeFractionDidUpdated(rangeFraction)
|
||||
} else {
|
||||
return barsController.chartRangeFractionDidUpdated(rangeFraction)
|
||||
}
|
||||
}
|
||||
|
||||
override public func apply(colorMode: GColorMode, animated: Bool) {
|
||||
super.apply(colorMode: colorMode, animated: animated)
|
||||
|
||||
linesController.apply(colorMode: colorMode, animated: animated)
|
||||
barsController.apply(colorMode: colorMode, animated: animated)
|
||||
}
|
||||
|
||||
public override var drawChartVisibity: Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: Убрать Performance полоски сверзу чартов (Не забыть)
|
||||
//TODO: Добавить ховеры на кнопки
|
||||
@@ -0,0 +1,215 @@
|
||||
//
|
||||
// LinesComponentController.swift
|
||||
// GraphTest
|
||||
//
|
||||
// Created by Andrei Salavei on 4/14/19.
|
||||
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
#if os(macOS)
|
||||
import Cocoa
|
||||
#else
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
class LinesComponentController: GeneralChartComponentController {
|
||||
let mainLinesRenderer: LinesChartRenderer
|
||||
let horizontalScalesRenderer: HorizontalScalesRenderer
|
||||
let verticalScalesRenderer: VerticalScalesRenderer
|
||||
let verticalLineRenderer: VerticalLinesRenderer
|
||||
let lineBulletsRenderer: LineBulletsRenderer
|
||||
|
||||
let previewLinesChartRenderer: LinesChartRenderer
|
||||
|
||||
private let zoomedLinesRenderer = LinesChartRenderer()
|
||||
private let zoomedPreviewLinesRenderer = LinesChartRenderer()
|
||||
|
||||
private let userLinesTransitionAnimation: Bool
|
||||
|
||||
private(set) var chartLines: [LinesChartRenderer.LineData] = []
|
||||
|
||||
init(isZoomed: Bool,
|
||||
userLinesTransitionAnimation: Bool,
|
||||
mainLinesRenderer: LinesChartRenderer,
|
||||
horizontalScalesRenderer: HorizontalScalesRenderer,
|
||||
verticalScalesRenderer: VerticalScalesRenderer,
|
||||
verticalLineRenderer: VerticalLinesRenderer,
|
||||
lineBulletsRenderer: LineBulletsRenderer,
|
||||
previewLinesChartRenderer: LinesChartRenderer) {
|
||||
self.mainLinesRenderer = mainLinesRenderer
|
||||
self.horizontalScalesRenderer = horizontalScalesRenderer
|
||||
self.verticalScalesRenderer = verticalScalesRenderer
|
||||
self.verticalLineRenderer = verticalLineRenderer
|
||||
self.lineBulletsRenderer = lineBulletsRenderer
|
||||
self.previewLinesChartRenderer = previewLinesChartRenderer
|
||||
self.userLinesTransitionAnimation = userLinesTransitionAnimation
|
||||
|
||||
super.init(isZoomed: isZoomed)
|
||||
|
||||
self.mainLinesRenderer.lineWidth = BaseConstants.mainChartLineWidth
|
||||
self.mainLinesRenderer.optimizationLevel = BaseConstants.linesChartOptimizationLevel
|
||||
self.previewLinesChartRenderer.lineWidth = BaseConstants.previewChartLineWidth
|
||||
self.previewLinesChartRenderer.optimizationLevel = BaseConstants.previewLinesChartOptimizationLevel
|
||||
|
||||
self.lineBulletsRenderer.isEnabled = false
|
||||
}
|
||||
|
||||
override func initialize(chartsCollection: ChartsCollection,
|
||||
initialDate: Date,
|
||||
totalHorizontalRange _: ClosedRange<CGFloat>,
|
||||
totalVerticalRange _: ClosedRange<CGFloat>) {
|
||||
let (chartLines, totalHorizontalRange, totalVerticalRange) = LinesChartRenderer.LineData.initialComponents(chartsCollection: chartsCollection)
|
||||
self.chartLines = chartLines
|
||||
|
||||
self.lineBulletsRenderer.bullets = self.chartLines.map { LineBulletsRenderer.Bullet(coordinate: $0.points.first ?? .zero,
|
||||
color: $0.color)}
|
||||
|
||||
super.initialize(chartsCollection: chartsCollection,
|
||||
initialDate: initialDate,
|
||||
totalHorizontalRange: totalHorizontalRange,
|
||||
totalVerticalRange: totalVerticalRange)
|
||||
|
||||
self.mainLinesRenderer.setup(verticalRange: totalVerticalRange, animated: true)
|
||||
}
|
||||
|
||||
override func willAppear(animated: Bool) {
|
||||
mainLinesRenderer.setLines(lines: self.chartLines, animated: animated && userLinesTransitionAnimation)
|
||||
previewLinesChartRenderer.setLines(lines: self.chartLines, animated: animated && userLinesTransitionAnimation)
|
||||
|
||||
previewLinesChartRenderer.setup(verticalRange: totalVerticalRange, animated: animated)
|
||||
previewLinesChartRenderer.setup(horizontalRange: totalHorizontalRange, animated: animated)
|
||||
|
||||
setupMainChart(verticalRange: initialVerticalRange, animated: animated)
|
||||
setupMainChart(horizontalRange: initialHorizontalRange, animated: animated)
|
||||
|
||||
updateChartVerticalRanges(horizontalRange: initialHorizontalRange, animated: animated)
|
||||
|
||||
super.willAppear(animated: animated)
|
||||
|
||||
updatePreviewRangeClosure?(currentChartHorizontalRangeFraction, animated)
|
||||
setConponentsVisible(visible: true, animated: animated)
|
||||
updateHorizontalLimitLabels(animated: animated, forceUpdate: true)
|
||||
}
|
||||
|
||||
override func chartRangeDidUpdated(_ updatedRange: ClosedRange<CGFloat>) {
|
||||
super.chartRangeDidUpdated(updatedRange)
|
||||
if !isZoomed {
|
||||
initialHorizontalRange = updatedRange
|
||||
}
|
||||
setupMainChart(horizontalRange: updatedRange, animated: false)
|
||||
updateHorizontalLimitLabels(animated: true, forceUpdate: false)
|
||||
updateChartVerticalRanges(horizontalRange: updatedRange, animated: true)
|
||||
}
|
||||
|
||||
func updateHorizontalLimitLabels(animated: Bool, forceUpdate: Bool) {
|
||||
updateHorizontalLimitLabels(horizontalScalesRenderer: horizontalScalesRenderer,
|
||||
horizontalRange: currentHorizontalMainChartRange,
|
||||
scaleType: isZoomed ? .hour : .day,
|
||||
forceUpdate: forceUpdate,
|
||||
animated: animated)
|
||||
}
|
||||
|
||||
func prepareAppearanceAnimation(horizontalRnage: ClosedRange<CGFloat>) {
|
||||
setupMainChart(horizontalRange: horizontalRnage, animated: false)
|
||||
setConponentsVisible(visible: false, animated: false)
|
||||
}
|
||||
|
||||
func setConponentsVisible(visible: Bool, animated: Bool) {
|
||||
mainLinesRenderer.setVisible(visible, animated: animated)
|
||||
horizontalScalesRenderer.setVisible(visible, animated: animated)
|
||||
verticalScalesRenderer.setVisible(visible, animated: animated)
|
||||
verticalLineRenderer.setVisible(visible, animated: animated)
|
||||
previewLinesChartRenderer.setVisible(visible, animated: animated)
|
||||
lineBulletsRenderer.setVisible(visible, animated: animated)
|
||||
}
|
||||
|
||||
func setupMainChart(horizontalRange: ClosedRange<CGFloat>, animated: Bool) {
|
||||
mainLinesRenderer.setup(horizontalRange: horizontalRange, animated: animated)
|
||||
horizontalScalesRenderer.setup(horizontalRange: horizontalRange, animated: animated)
|
||||
verticalScalesRenderer.setup(horizontalRange: horizontalRange, animated: animated)
|
||||
verticalLineRenderer.setup(horizontalRange: horizontalRange, animated: animated)
|
||||
lineBulletsRenderer.setup(horizontalRange: horizontalRange, animated: animated)
|
||||
}
|
||||
|
||||
var visibleLines: [LinesChartRenderer.LineData] {
|
||||
return chartVisibility.enumerated().compactMap { $0.element ? chartLines[$0.offset] : nil }
|
||||
}
|
||||
|
||||
func updateChartVerticalRanges(horizontalRange: ClosedRange<CGFloat>, animated: Bool) {
|
||||
if let range = LinesChartRenderer.LineData.verticalRange(lines: visibleLines,
|
||||
calculatingRange: horizontalRange,
|
||||
addBounds: true) {
|
||||
let (range, labels) = verticalLimitsLabels(verticalRange: range)
|
||||
if verticalScalesRenderer.verticalRange.end != range {
|
||||
verticalScalesRenderer.setup(verticalLimitsLabels: labels, animated: animated)
|
||||
}
|
||||
|
||||
setupMainChart(verticalRange: range, animated: animated)
|
||||
verticalScalesRenderer.setVisible(true, animated: animated)
|
||||
} else {
|
||||
verticalScalesRenderer.setVisible(false, animated: animated)
|
||||
}
|
||||
|
||||
if let range = LinesChartRenderer.LineData.verticalRange(lines: visibleLines) {
|
||||
previewLinesChartRenderer.setup(verticalRange: range, animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
func setupMainChart(verticalRange: ClosedRange<CGFloat>, animated: Bool) {
|
||||
mainLinesRenderer.setup(verticalRange: verticalRange, animated: animated)
|
||||
horizontalScalesRenderer.setup(verticalRange: verticalRange, animated: animated)
|
||||
verticalScalesRenderer.setup(verticalRange: verticalRange, animated: animated)
|
||||
verticalLineRenderer.setup(verticalRange: verticalRange, animated: animated)
|
||||
lineBulletsRenderer.setup(verticalRange: verticalRange, animated: animated)
|
||||
}
|
||||
|
||||
public override func updateChartsVisibility(visibility: [Bool], animated: Bool) {
|
||||
super.updateChartsVisibility(visibility: visibility, animated: animated)
|
||||
for (index, isVisible) in visibility.enumerated() {
|
||||
mainLinesRenderer.setLineVisible(isVisible, at: index, animated: animated)
|
||||
previewLinesChartRenderer.setLineVisible(isVisible, at: index, animated: animated)
|
||||
lineBulletsRenderer.setLineVisible(isVisible, at: index, animated: animated)
|
||||
}
|
||||
updateChartVerticalRanges(horizontalRange: currentHorizontalMainChartRange, animated: true)
|
||||
}
|
||||
|
||||
override var currentMainRangeRenderer: BaseChartRenderer {
|
||||
return mainLinesRenderer
|
||||
}
|
||||
|
||||
override var currentPreviewRangeRenderer: BaseChartRenderer {
|
||||
return previewLinesChartRenderer
|
||||
}
|
||||
|
||||
override func showDetailsView(at chartPosition: CGFloat, detailsViewPosition: CGFloat, dataIndex: Int, date: Date, animted: Bool) {
|
||||
super.showDetailsView(at: chartPosition, detailsViewPosition: detailsViewPosition, dataIndex: dataIndex, date: date, animted: animted)
|
||||
verticalLineRenderer.values = [chartPosition]
|
||||
verticalLineRenderer.isEnabled = true
|
||||
|
||||
lineBulletsRenderer.isEnabled = true
|
||||
lineBulletsRenderer.setVisible(true, animated: animted)
|
||||
lineBulletsRenderer.bullets = chartLines.compactMap { chart in
|
||||
return LineBulletsRenderer.Bullet(coordinate: chart.points[dataIndex], color: chart.color)
|
||||
}
|
||||
}
|
||||
|
||||
override func hideDetailsView(animated: Bool) {
|
||||
super.hideDetailsView(animated: animated)
|
||||
|
||||
verticalLineRenderer.values = []
|
||||
verticalLineRenderer.isEnabled = false
|
||||
lineBulletsRenderer.isEnabled = false
|
||||
}
|
||||
|
||||
override func apply(colorMode: GColorMode, animated: Bool) {
|
||||
super.apply(colorMode: colorMode, animated: animated)
|
||||
|
||||
horizontalScalesRenderer.labelsColor = colorMode.chartLabelsColor
|
||||
verticalScalesRenderer.labelsColor = colorMode.chartLabelsColor
|
||||
verticalScalesRenderer.axisXColor = colorMode.chartStrongLinesColor
|
||||
verticalScalesRenderer.horizontalLinesColor = colorMode.chartHelperLinesColor
|
||||
lineBulletsRenderer.setInnerColor(colorMode.chartBackgroundColor, animated: animated)
|
||||
verticalLineRenderer.linesColor = colorMode.chartStrongLinesColor
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
//
|
||||
// StackedBarsChartController.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 class StackedBarsChartController: BaseChartController {
|
||||
let barsController: BarsComponentController
|
||||
let zoomedBarsController: BarsComponentController
|
||||
|
||||
override public init(chartsCollection: ChartsCollection) {
|
||||
let horizontalScalesRenderer = HorizontalScalesRenderer()
|
||||
let verticalScalesRenderer = VerticalScalesRenderer()
|
||||
barsController = BarsComponentController(isZoomed: false,
|
||||
mainBarsRenderer: BarChartRenderer(),
|
||||
horizontalScalesRenderer: horizontalScalesRenderer,
|
||||
verticalScalesRenderer: verticalScalesRenderer,
|
||||
previewBarsChartRenderer: BarChartRenderer())
|
||||
zoomedBarsController = BarsComponentController(isZoomed: true,
|
||||
mainBarsRenderer: BarChartRenderer(),
|
||||
horizontalScalesRenderer: horizontalScalesRenderer,
|
||||
verticalScalesRenderer: verticalScalesRenderer,
|
||||
previewBarsChartRenderer: BarChartRenderer())
|
||||
|
||||
super.init(chartsCollection: chartsCollection)
|
||||
|
||||
[barsController, zoomedBarsController].forEach { controller in
|
||||
controller.chartFrame = { [unowned self] in self.chartFrame() }
|
||||
controller.cartViewBounds = { [unowned self] in self.cartViewBounds() }
|
||||
controller.zoomInOnDateClosure = { [unowned self] date in
|
||||
self.didTapZoomIn(date: date)
|
||||
}
|
||||
controller.setChartTitleClosure = { [unowned self] (title, animated) in
|
||||
self.setChartTitleClosure?(title, animated)
|
||||
}
|
||||
controller.setDetailsViewPositionClosure = { [unowned self] (position) in
|
||||
self.setDetailsViewPositionClosure?(position)
|
||||
}
|
||||
controller.setDetailsChartVisibleClosure = { [unowned self] (visible, animated) in
|
||||
self.setDetailsChartVisibleClosure?(visible, animated)
|
||||
}
|
||||
controller.setDetailsViewModel = { [unowned self] (viewModel, animated) in
|
||||
self.setDetailsViewModel?(viewModel, animated)
|
||||
}
|
||||
controller.updatePreviewRangeClosure = { [unowned self] (fraction, animated) in
|
||||
self.chartRangeUpdatedClosure?(fraction, animated)
|
||||
}
|
||||
controller.chartRangePagingClosure = { [unowned self] (isEnabled, pageSize) in
|
||||
self.setChartRangePagingEnabled(isEnabled: isEnabled, minimumSelectionSize: pageSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override var mainChartRenderers: [ChartViewRenderer] {
|
||||
return [barsController.mainBarsRenderer,
|
||||
zoomedBarsController.mainBarsRenderer,
|
||||
barsController.horizontalScalesRenderer,
|
||||
barsController.verticalScalesRenderer,
|
||||
// performanceRenderer
|
||||
]
|
||||
}
|
||||
|
||||
public override var navigationRenderers: [ChartViewRenderer] {
|
||||
return [barsController.previewBarsChartRenderer,
|
||||
zoomedBarsController.previewBarsChartRenderer]
|
||||
}
|
||||
|
||||
public override func initializeChart() {
|
||||
barsController.initialize(chartsCollection: initialChartsCollection,
|
||||
initialDate: Date(),
|
||||
totalHorizontalRange: BaseConstants.defaultRange,
|
||||
totalVerticalRange: BaseConstants.defaultRange)
|
||||
switchToChart(chartsCollection: barsController.chartsCollection, isZoomed: false, animated: false)
|
||||
}
|
||||
|
||||
func switchToChart(chartsCollection: ChartsCollection, isZoomed: Bool, animated: Bool) {
|
||||
if animated {
|
||||
TimeInterval.setDefaultSuration(.expandAnimationDuration)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .expandAnimationDuration) {
|
||||
TimeInterval.setDefaultSuration(.osXDuration)
|
||||
}
|
||||
}
|
||||
|
||||
super.isZoomed = isZoomed
|
||||
if isZoomed {
|
||||
let toHorizontalRange = zoomedBarsController.initialHorizontalRange
|
||||
let destinationHorizontalRange = (toHorizontalRange.lowerBound - barsController.barsWidth)...(toHorizontalRange.upperBound - barsController.barsWidth)
|
||||
let verticalVisibleRange = barsController.currentVerticalMainChartRange
|
||||
let initialVerticalRange = verticalVisibleRange.lowerBound...(verticalVisibleRange.upperBound + verticalVisibleRange.distance * 10)
|
||||
|
||||
zoomedBarsController.mainBarsRenderer.setup(horizontalRange: barsController.currentHorizontalMainChartRange, animated: false)
|
||||
zoomedBarsController.previewBarsChartRenderer.setup(horizontalRange: barsController.currentPreviewHorizontalRange, animated: false)
|
||||
zoomedBarsController.mainBarsRenderer.setup(verticalRange: initialVerticalRange, animated: false)
|
||||
zoomedBarsController.previewBarsChartRenderer.setup(verticalRange: initialVerticalRange, animated: false)
|
||||
zoomedBarsController.mainBarsRenderer.setVisible(true, animated: false)
|
||||
zoomedBarsController.previewBarsChartRenderer.setVisible(true, animated: false)
|
||||
|
||||
barsController.setupMainChart(horizontalRange: destinationHorizontalRange, animated: animated)
|
||||
barsController.previewBarsChartRenderer.setup(horizontalRange: zoomedBarsController.totalHorizontalRange, animated: animated)
|
||||
barsController.mainBarsRenderer.setVisible(false, animated: animated)
|
||||
barsController.previewBarsChartRenderer.setVisible(false, animated: animated)
|
||||
|
||||
zoomedBarsController.willAppear(animated: animated)
|
||||
barsController.willDisappear(animated: animated)
|
||||
|
||||
zoomedBarsController.updateChartsVisibility(visibility: barsController.chartVisibility, animated: false)
|
||||
zoomedBarsController.mainBarsRenderer.setup(verticalRange: zoomedBarsController.currentVerticalMainChartRange, animated: animated, timeFunction: .easeOut)
|
||||
zoomedBarsController.previewBarsChartRenderer.setup(verticalRange: zoomedBarsController.currentPreviewVerticalRange, animated: animated, timeFunction: .easeOut)
|
||||
} else {
|
||||
if !zoomedBarsController.chartsCollection.isBlank {
|
||||
barsController.hideDetailsView(animated: false)
|
||||
barsController.chartVisibility = zoomedBarsController.chartVisibility
|
||||
let visibleVerticalRange = BarChartRenderer.BarsData.verticalRange(bars: barsController.visibleBars,
|
||||
calculatingRange: barsController.initialHorizontalRange) ?? BaseConstants.defaultRange
|
||||
barsController.mainBarsRenderer.setup(verticalRange: visibleVerticalRange, animated: false)
|
||||
|
||||
let toHorizontalRange = barsController.initialHorizontalRange
|
||||
|
||||
let verticalVisibleRange = barsController.initialVerticalRange
|
||||
let targetVerticalRange = verticalVisibleRange.lowerBound...(verticalVisibleRange.upperBound + verticalVisibleRange.distance * 10)
|
||||
|
||||
zoomedBarsController.setupMainChart(horizontalRange: toHorizontalRange, animated: animated)
|
||||
zoomedBarsController.mainBarsRenderer.setup(verticalRange: targetVerticalRange, animated: animated, timeFunction: .easeIn)
|
||||
zoomedBarsController.previewBarsChartRenderer.setup(verticalRange: targetVerticalRange, animated: animated, timeFunction: .easeIn)
|
||||
zoomedBarsController.previewBarsChartRenderer.setup(horizontalRange: barsController.totalHorizontalRange, animated: animated)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .defaultDuration) { [weak self] in
|
||||
self?.zoomedBarsController.mainBarsRenderer.setVisible(false, animated: false)
|
||||
self?.zoomedBarsController.previewBarsChartRenderer.setVisible(false, animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
barsController.willAppear(animated: animated)
|
||||
zoomedBarsController.willDisappear(animated: animated)
|
||||
|
||||
if !zoomedBarsController.chartsCollection.isBlank {
|
||||
barsController.updateChartsVisibility(visibility: zoomedBarsController.chartVisibility, animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
self.setBackButtonVisibilityClosure?(isZoomed, animated)
|
||||
}
|
||||
|
||||
public override func updateChartsVisibility(visibility: [Bool], animated: Bool) {
|
||||
if isZoomed {
|
||||
zoomedBarsController.updateChartsVisibility(visibility: visibility, animated: animated)
|
||||
} else {
|
||||
barsController.updateChartsVisibility(visibility: visibility, animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
var visibleChartValues: [ChartsCollection.Chart] {
|
||||
let visibility = isZoomed ? zoomedBarsController.chartVisibility : barsController.chartVisibility
|
||||
let collection = isZoomed ? zoomedBarsController.chartsCollection : barsController.chartsCollection
|
||||
let visibleCharts: [ChartsCollection.Chart] = visibility.enumerated().compactMap { args in
|
||||
args.element ? collection.chartValues[args.offset] : nil
|
||||
}
|
||||
return visibleCharts
|
||||
}
|
||||
|
||||
public override var actualChartVisibility: [Bool] {
|
||||
return isZoomed ? zoomedBarsController.chartVisibility : barsController.chartVisibility
|
||||
}
|
||||
|
||||
public override var actualChartsCollection: ChartsCollection {
|
||||
let collection = isZoomed ? zoomedBarsController.chartsCollection : barsController.chartsCollection
|
||||
if collection.isBlank {
|
||||
return self.initialChartsCollection
|
||||
}
|
||||
return collection
|
||||
}
|
||||
|
||||
public override func chartInteractionDidBegin(point: CGPoint) {
|
||||
if isZoomed {
|
||||
zoomedBarsController.chartInteractionDidBegin(point: point)
|
||||
} else {
|
||||
barsController.chartInteractionDidBegin(point: point)
|
||||
}
|
||||
}
|
||||
|
||||
public override func chartInteractionDidEnd() {
|
||||
if isZoomed {
|
||||
zoomedBarsController.chartInteractionDidEnd()
|
||||
} else {
|
||||
barsController.chartInteractionDidEnd()
|
||||
}
|
||||
}
|
||||
|
||||
public override var drawChartVisibity: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
public override var currentChartHorizontalRangeFraction: ClosedRange<CGFloat> {
|
||||
if isZoomed {
|
||||
return zoomedBarsController.currentChartHorizontalRangeFraction
|
||||
} else {
|
||||
return barsController.currentChartHorizontalRangeFraction
|
||||
}
|
||||
}
|
||||
|
||||
public override func cancelChartInteraction() {
|
||||
if isZoomed {
|
||||
return zoomedBarsController.hideDetailsView(animated: true)
|
||||
} else {
|
||||
return barsController.hideDetailsView(animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
public override func didTapZoomIn(date: Date) {
|
||||
guard isZoomed == false else { return }
|
||||
if isZoomed {
|
||||
return zoomedBarsController.hideDetailsView(animated: true)
|
||||
}
|
||||
self.getDetailsData?(date, { updatedCollection in
|
||||
if let updatedCollection = updatedCollection {
|
||||
self.zoomedBarsController.initialize(chartsCollection: updatedCollection,
|
||||
initialDate: date,
|
||||
totalHorizontalRange: 0...1,
|
||||
totalVerticalRange: 0...1)
|
||||
self.switchToChart(chartsCollection: updatedCollection, isZoomed: true, animated: true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public override func didTapZoomOut() {
|
||||
cancelChartInteraction()
|
||||
switchToChart(chartsCollection: barsController.chartsCollection, isZoomed: false, animated: true)
|
||||
}
|
||||
|
||||
public override func updateChartRange(_ rangeFraction: ClosedRange<CGFloat>) {
|
||||
if isZoomed {
|
||||
return zoomedBarsController.chartRangeFractionDidUpdated(rangeFraction)
|
||||
} else {
|
||||
return barsController.chartRangeFractionDidUpdated(rangeFraction)
|
||||
}
|
||||
}
|
||||
|
||||
public override func apply(colorMode: GColorMode, animated: Bool) {
|
||||
super.apply(colorMode: colorMode, animated: animated)
|
||||
|
||||
zoomedBarsController.apply(colorMode: colorMode, animated: animated)
|
||||
barsController.apply(colorMode: colorMode, animated: animated)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
//
|
||||
// DailyBarsChartController.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 class StepBarsChartController: BaseChartController {
|
||||
let barsController: BarsComponentController
|
||||
let zoomedBarsController: BarsComponentController
|
||||
|
||||
override public init(chartsCollection: ChartsCollection) {
|
||||
let horizontalScalesRenderer = HorizontalScalesRenderer()
|
||||
let verticalScalesRenderer = VerticalScalesRenderer()
|
||||
barsController = BarsComponentController(isZoomed: false,
|
||||
mainBarsRenderer: BarChartRenderer(step: true),
|
||||
horizontalScalesRenderer: horizontalScalesRenderer,
|
||||
verticalScalesRenderer: verticalScalesRenderer,
|
||||
previewBarsChartRenderer: BarChartRenderer())
|
||||
zoomedBarsController = BarsComponentController(isZoomed: true,
|
||||
mainBarsRenderer: BarChartRenderer(step: true),
|
||||
horizontalScalesRenderer: horizontalScalesRenderer,
|
||||
verticalScalesRenderer: verticalScalesRenderer,
|
||||
previewBarsChartRenderer: BarChartRenderer())
|
||||
|
||||
super.init(chartsCollection: chartsCollection)
|
||||
|
||||
[barsController, zoomedBarsController].forEach { controller in
|
||||
controller.chartFrame = { [unowned self] in self.chartFrame() }
|
||||
controller.cartViewBounds = { [unowned self] in self.cartViewBounds() }
|
||||
controller.zoomInOnDateClosure = { [unowned self] date in
|
||||
self.didTapZoomIn(date: date)
|
||||
}
|
||||
controller.setChartTitleClosure = { [unowned self] (title, animated) in
|
||||
self.setChartTitleClosure?(title, animated)
|
||||
}
|
||||
controller.setDetailsViewPositionClosure = { [unowned self] (position) in
|
||||
self.setDetailsViewPositionClosure?(position)
|
||||
}
|
||||
controller.setDetailsChartVisibleClosure = { [unowned self] (visible, animated) in
|
||||
self.setDetailsChartVisibleClosure?(visible, animated)
|
||||
}
|
||||
controller.setDetailsViewModel = { [unowned self] (viewModel, animated) in
|
||||
self.setDetailsViewModel?(viewModel, animated)
|
||||
}
|
||||
controller.updatePreviewRangeClosure = { [unowned self] (fraction, animated) in
|
||||
self.chartRangeUpdatedClosure?(fraction, animated)
|
||||
}
|
||||
controller.chartRangePagingClosure = { [unowned self] (isEnabled, pageSize) in
|
||||
self.setChartRangePagingEnabled(isEnabled: isEnabled, minimumSelectionSize: pageSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override var mainChartRenderers: [ChartViewRenderer] {
|
||||
return [barsController.mainBarsRenderer,
|
||||
zoomedBarsController.mainBarsRenderer,
|
||||
barsController.horizontalScalesRenderer,
|
||||
barsController.verticalScalesRenderer,
|
||||
barsController.secondVerticalScalesRenderer!
|
||||
// performanceRenderer
|
||||
]
|
||||
}
|
||||
|
||||
public override var navigationRenderers: [ChartViewRenderer] {
|
||||
return [barsController.previewBarsChartRenderer,
|
||||
zoomedBarsController.previewBarsChartRenderer]
|
||||
}
|
||||
|
||||
public override func initializeChart() {
|
||||
barsController.initialize(chartsCollection: initialChartsCollection,
|
||||
initialDate: Date(),
|
||||
totalHorizontalRange: BaseConstants.defaultRange,
|
||||
totalVerticalRange: BaseConstants.defaultRange)
|
||||
switchToChart(chartsCollection: barsController.chartsCollection, isZoomed: false, animated: false)
|
||||
}
|
||||
|
||||
func switchToChart(chartsCollection: ChartsCollection, isZoomed: Bool, animated: Bool) {
|
||||
if animated {
|
||||
TimeInterval.setDefaultSuration(.expandAnimationDuration)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .expandAnimationDuration) {
|
||||
TimeInterval.setDefaultSuration(.osXDuration)
|
||||
}
|
||||
}
|
||||
|
||||
super.isZoomed = isZoomed
|
||||
if isZoomed {
|
||||
let toHorizontalRange = zoomedBarsController.initialHorizontalRange
|
||||
let destinationHorizontalRange = (toHorizontalRange.lowerBound - barsController.barsWidth)...(toHorizontalRange.upperBound - barsController.barsWidth)
|
||||
// let initialChartVerticalRange = lineProportionAnimationRange()
|
||||
|
||||
// let visibleVerticalRange = BarChartRenderer.BarsData.verticalRange(bars: zoomedBarsController.visibleBars,
|
||||
// calculatingRange: zoomedBarsController.initialHorizontalRange) ?? BaseConstants.defaultRange
|
||||
zoomedBarsController.mainBarsRenderer.setup(verticalRange: 0...117278, animated: false)
|
||||
|
||||
zoomedBarsController.setupMainChart(horizontalRange: barsController.currentHorizontalMainChartRange, animated: false)
|
||||
zoomedBarsController.previewBarsChartRenderer.setup(horizontalRange: barsController.currentPreviewHorizontalRange, animated: false)
|
||||
// zoomedBarsController.mainLinesRenderer.setup(verticalRange: initialChartVerticalRange, animated: false)
|
||||
// zoomedBarsController.previewLinesChartRenderer.setup(verticalRange: initialChartVerticalRange, animated: false)
|
||||
zoomedBarsController.mainBarsRenderer.setVisible(false, animated: false)
|
||||
zoomedBarsController.previewBarsChartRenderer.setVisible(false, animated: false)
|
||||
|
||||
barsController.setupMainChart(horizontalRange: destinationHorizontalRange, animated: animated)
|
||||
barsController.previewBarsChartRenderer.setup(horizontalRange: zoomedBarsController.totalHorizontalRange, animated: animated)
|
||||
barsController.mainBarsRenderer.setVisible(false, animated: animated)
|
||||
barsController.previewBarsChartRenderer.setVisible(false, animated: animated)
|
||||
|
||||
zoomedBarsController.willAppear(animated: animated)
|
||||
barsController.willDisappear(animated: animated)
|
||||
|
||||
zoomedBarsController.updateChartsVisibility(visibility: zoomedBarsController.chartBars.components.map { _ in true }, animated: false)
|
||||
} else {
|
||||
if !zoomedBarsController.chartsCollection.isBlank {
|
||||
barsController.hideDetailsView(animated: false)
|
||||
let visibleVerticalRange = BarChartRenderer.BarsData.verticalRange(bars: barsController.visibleBars,
|
||||
calculatingRange: barsController.initialHorizontalRange) ?? BaseConstants.defaultRange
|
||||
barsController.mainBarsRenderer.setup(verticalRange: visibleVerticalRange, animated: false)
|
||||
|
||||
let toHorizontalRange = barsController.initialHorizontalRange
|
||||
// let destinationChartVerticalRange = lineProportionAnimationRange()
|
||||
|
||||
zoomedBarsController.setupMainChart(horizontalRange: toHorizontalRange, animated: animated)
|
||||
// zoomedBarsController.mainLinesRenderer.setup(verticalRange: destinationChartVerticalRange, animated: animated)
|
||||
// zoomedBarsController.previewLinesChartRenderer.setup(verticalRange: destinationChartVerticalRange, animated: animated)
|
||||
zoomedBarsController.previewBarsChartRenderer.setup(horizontalRange: barsController.totalHorizontalRange, animated: animated)
|
||||
zoomedBarsController.mainBarsRenderer.setVisible(false, animated: animated)
|
||||
zoomedBarsController.previewBarsChartRenderer.setVisible(false, animated: animated)
|
||||
}
|
||||
|
||||
barsController.willAppear(animated: animated)
|
||||
zoomedBarsController.willDisappear(animated: animated)
|
||||
}
|
||||
|
||||
self.setBackButtonVisibilityClosure?(isZoomed, animated)
|
||||
self.refreshChartToolsClosure?(animated)
|
||||
}
|
||||
|
||||
public override func updateChartsVisibility(visibility: [Bool], animated: Bool) {
|
||||
if isZoomed {
|
||||
zoomedBarsController.updateChartsVisibility(visibility: visibility, animated: animated)
|
||||
} else {
|
||||
barsController.updateChartsVisibility(visibility: visibility, animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
var visibleChartValues: [ChartsCollection.Chart] {
|
||||
let visibility = isZoomed ? zoomedBarsController.chartVisibility : barsController.chartVisibility
|
||||
let collection = isZoomed ? zoomedBarsController.chartsCollection : barsController.chartsCollection
|
||||
let visibleCharts: [ChartsCollection.Chart] = visibility.enumerated().compactMap { args in
|
||||
args.element ? collection.chartValues[args.offset] : nil
|
||||
}
|
||||
return visibleCharts
|
||||
}
|
||||
|
||||
public override var actualChartVisibility: [Bool] {
|
||||
return isZoomed ? zoomedBarsController.chartVisibility : barsController.chartVisibility
|
||||
}
|
||||
|
||||
public override var actualChartsCollection: ChartsCollection {
|
||||
let collection = isZoomed ? zoomedBarsController.chartsCollection : barsController.chartsCollection
|
||||
|
||||
if collection.isBlank {
|
||||
return self.initialChartsCollection
|
||||
}
|
||||
return collection
|
||||
}
|
||||
|
||||
public override func chartInteractionDidBegin(point: CGPoint) {
|
||||
if isZoomed {
|
||||
zoomedBarsController.chartInteractionDidBegin(point: point)
|
||||
} else {
|
||||
barsController.chartInteractionDidBegin(point: point)
|
||||
}
|
||||
}
|
||||
|
||||
public override func chartInteractionDidEnd() {
|
||||
if isZoomed {
|
||||
zoomedBarsController.chartInteractionDidEnd()
|
||||
} else {
|
||||
barsController.chartInteractionDidEnd()
|
||||
}
|
||||
}
|
||||
|
||||
public override var currentChartHorizontalRangeFraction: ClosedRange<CGFloat> {
|
||||
if isZoomed {
|
||||
return zoomedBarsController.currentChartHorizontalRangeFraction
|
||||
} else {
|
||||
return barsController.currentChartHorizontalRangeFraction
|
||||
}
|
||||
}
|
||||
|
||||
public override func cancelChartInteraction() {
|
||||
if isZoomed {
|
||||
return zoomedBarsController.hideDetailsView(animated: true)
|
||||
} else {
|
||||
return barsController.hideDetailsView(animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
public override func didTapZoomIn(date: Date) {
|
||||
guard isZoomed == false else { return }
|
||||
if isZoomed {
|
||||
return zoomedBarsController.hideDetailsView(animated: true)
|
||||
}
|
||||
self.getDetailsData?(date, { updatedCollection in
|
||||
if let updatedCollection = updatedCollection {
|
||||
self.zoomedBarsController.initialize(chartsCollection: updatedCollection,
|
||||
initialDate: date,
|
||||
totalHorizontalRange: 0...1,
|
||||
totalVerticalRange: 0...1)
|
||||
self.switchToChart(chartsCollection: updatedCollection, isZoomed: true, animated: true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// func lineProportionAnimationRange() -> ClosedRange<CGFloat> {
|
||||
// let visibleLines = self.barsController.chartVisibility.enumerated().compactMap { $0.element ? self.zoomedBarsController.chartLines[$0.offset] : nil }
|
||||
// let linesRange = LinesChartRenderer.LineData.verticalRange(lines: visibleLines) ?? BaseConstants.defaultRange
|
||||
// let barsRange = BarChartRenderer.BarsData.verticalRange(bars: self.barsController.visibleBars,
|
||||
// calculatingRange: self.zoomedBarsController.totalHorizontalRange) ?? BaseConstants.defaultRange
|
||||
// let range = 0...(linesRange.upperBound / barsRange.distance * self.barsController.currentVerticalMainChartRange.distance)
|
||||
// return range
|
||||
// }
|
||||
|
||||
public override func didTapZoomOut() {
|
||||
cancelChartInteraction()
|
||||
switchToChart(chartsCollection: barsController.chartsCollection, isZoomed: false, animated: true)
|
||||
}
|
||||
|
||||
public override func updateChartRange(_ rangeFraction: ClosedRange<CGFloat>) {
|
||||
if isZoomed {
|
||||
return zoomedBarsController.chartRangeFractionDidUpdated(rangeFraction)
|
||||
} else {
|
||||
return barsController.chartRangeFractionDidUpdated(rangeFraction)
|
||||
}
|
||||
}
|
||||
|
||||
override public func apply(colorMode: GColorMode, animated: Bool) {
|
||||
super.apply(colorMode: colorMode, animated: animated)
|
||||
|
||||
zoomedBarsController.apply(colorMode: colorMode, animated: animated)
|
||||
barsController.apply(colorMode: colorMode, animated: animated)
|
||||
}
|
||||
|
||||
public override var drawChartVisibity: Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: Убрать Performance полоски сверзу чартов (Не забыть)
|
||||
//TODO: Добавить ховеры на кнопки
|
||||
@@ -0,0 +1,380 @@
|
||||
import Foundation
|
||||
#if os(macOS)
|
||||
import Cocoa
|
||||
#else
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
public class StepBarsChartController2: BaseChartController {
|
||||
class GraphController {
|
||||
let mainBarsRenderer: BarChartRenderer
|
||||
let verticalScalesRenderer = VerticalScalesRenderer()
|
||||
let lineBulletsRenderer = LineBulletsRenderer()
|
||||
let previewBarsRenderer: BarChartRenderer
|
||||
|
||||
var chartBars: BarChartRenderer.BarsData = .blank
|
||||
var barsWidth: CGFloat = 1
|
||||
|
||||
var totalVerticalRange: ClosedRange<CGFloat> = BaseConstants.defaultRange
|
||||
|
||||
init(isZoomed: Bool,
|
||||
mainBarsRenderer: BarChartRenderer,
|
||||
previewBarsRenderer: BarChartRenderer) {
|
||||
self.mainBarsRenderer = mainBarsRenderer
|
||||
self.previewBarsRenderer = previewBarsRenderer
|
||||
|
||||
self.mainBarsRenderer.optimizationLevel = BaseConstants.barsChartOptimizationLevel
|
||||
self.previewBarsRenderer.optimizationLevel = BaseConstants.barsChartOptimizationLevel
|
||||
}
|
||||
}
|
||||
|
||||
private var graphControllers: [GraphController] = []
|
||||
private let horizontalScalesRenderer = HorizontalScalesRenderer()
|
||||
|
||||
private let verticalLineRenderer = VerticalLinesRenderer()
|
||||
|
||||
var chartVisibility: [Bool] = []
|
||||
var zoomChartVisibility: [Bool] = []
|
||||
|
||||
private let initialChartCollection: ChartsCollection
|
||||
var initialChartRange: ClosedRange<CGFloat> = BaseConstants.defaultRange
|
||||
var zoomedChartRange: ClosedRange<CGFloat> = BaseConstants.defaultRange
|
||||
var totalHorizontalRange: ClosedRange<CGFloat> = BaseConstants.defaultRange
|
||||
|
||||
var lastChartInteractionPoint: CGPoint = .zero
|
||||
var isChartInteractionBegun: Bool = false
|
||||
|
||||
override public init(chartsCollection: ChartsCollection) {
|
||||
self.initialChartCollection = chartsCollection
|
||||
|
||||
self.graphControllers = chartsCollection.chartValues.map { _ in GraphController(isZoomed: false, mainBarsRenderer: BarChartRenderer(step: true), previewBarsRenderer: BarChartRenderer(step: true))
|
||||
}
|
||||
|
||||
super.init(chartsCollection: chartsCollection)
|
||||
|
||||
self.chartVisibility = Array(repeating: true, count: chartsCollection.chartValues.count)
|
||||
self.zoomChartVisibility = self.chartVisibility
|
||||
|
||||
// self.graphControllers.map({ $0.barsController }).forEach { controller in
|
||||
// controller.chartFrame = { [unowned self] in self.chartFrame() }
|
||||
// controller.cartViewBounds = { [unowned self] in self.cartViewBounds() }
|
||||
// controller.zoomInOnDateClosure = { [unowned self] date in
|
||||
// self.didTapZoomIn(date: date)
|
||||
// }
|
||||
// controller.setChartTitleClosure = { [unowned self] (title, animated) in
|
||||
// self.setChartTitleClosure?(title, animated)
|
||||
// }
|
||||
// controller.setDetailsViewPositionClosure = { [unowned self] (position) in
|
||||
// self.setDetailsViewPositionClosure?(position)
|
||||
// }
|
||||
// controller.setDetailsChartVisibleClosure = { [unowned self] (visible, animated) in
|
||||
// self.setDetailsChartVisibleClosure?(visible, animated)
|
||||
// }
|
||||
// controller.setDetailsViewModel = { [unowned self] (viewModel, animated) in
|
||||
// self.setDetailsViewModel?(viewModel, animated)
|
||||
// }
|
||||
// controller.updatePreviewRangeClosure = { [unowned self] (fraction, animated) in
|
||||
// self.chartRangeUpdatedClosure?(fraction, animated)
|
||||
// }
|
||||
// controller.chartRangePagingClosure = { [unowned self] (isEnabled, pageSize) in
|
||||
// self.setChartRangePagingEnabled(isEnabled: isEnabled, minimumSelectionSize: pageSize)
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
public override var mainChartRenderers: [ChartViewRenderer] {
|
||||
var renderers: [ChartViewRenderer] = []
|
||||
self.graphControllers.forEach { controller in
|
||||
renderers.append(controller.mainBarsRenderer)
|
||||
}
|
||||
renderers.append(self.horizontalScalesRenderer)
|
||||
self.graphControllers.forEach { controller in
|
||||
renderers.append(controller.verticalScalesRenderer)
|
||||
renderers.append(controller.lineBulletsRenderer)
|
||||
}
|
||||
renderers.append(self.verticalLineRenderer)
|
||||
return renderers
|
||||
}
|
||||
|
||||
public override var navigationRenderers: [ChartViewRenderer] {
|
||||
return graphControllers.map { $0.previewBarsRenderer }
|
||||
}
|
||||
|
||||
public override func initializeChart() {
|
||||
if let first = initialChartCollection.axisValues.first?.timeIntervalSince1970,
|
||||
let last = initialChartCollection.axisValues.last?.timeIntervalSince1970 {
|
||||
initialChartRange = CGFloat(max(first, last - BaseConstants.defaultRangePresetLength))...CGFloat(last)
|
||||
}
|
||||
setupChartCollection(chartsCollection: initialChartCollection, animated: false, isZoomed: false)
|
||||
}
|
||||
|
||||
public override func updateChartsVisibility(visibility: [Bool], animated: Bool) {
|
||||
self.chartVisibility = visibility
|
||||
self.zoomChartVisibility = visibility
|
||||
let firstIndex = visibility.firstIndex(where: { $0 })
|
||||
for (index, isVisible) in visibility.enumerated() {
|
||||
let graph = graphControllers[index]
|
||||
for graphIndex in graph.chartBars.components.indices {
|
||||
graph.mainBarsRenderer.setComponentVisible(isVisible, at: graphIndex, animated: animated)
|
||||
graph.previewBarsRenderer.setComponentVisible(isVisible, at: graphIndex, animated: animated)
|
||||
graph.lineBulletsRenderer.setLineVisible(isVisible, at: graphIndex, animated: animated)
|
||||
}
|
||||
graph.verticalScalesRenderer.setVisible(isVisible, animated: animated)
|
||||
if let firstIndex = firstIndex {
|
||||
graph.verticalScalesRenderer.setHorizontalLinesVisible(index == firstIndex, animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
// updateVerticalLimitsAndRange(horizontalRange: currentHorizontalRange, animated: true)
|
||||
|
||||
if isChartInteractionBegun {
|
||||
chartInteractionDidBegin(point: lastChartInteractionPoint)
|
||||
}
|
||||
}
|
||||
|
||||
private func findClosestDateTo(dateToFind: Date) -> (Date, Int)? {
|
||||
guard self.initialChartCollection.axisValues.count > 0 else { return nil }
|
||||
var closestDate = self.initialChartCollection.axisValues[0]
|
||||
var minIndex = 0
|
||||
for (index, date) in self.initialChartCollection.axisValues.enumerated() {
|
||||
if abs(dateToFind.timeIntervalSince(date)) < abs(dateToFind.timeIntervalSince(closestDate)) {
|
||||
closestDate = date
|
||||
minIndex = index
|
||||
}
|
||||
}
|
||||
return (closestDate, minIndex)
|
||||
}
|
||||
|
||||
public override func chartInteractionDidBegin(point: CGPoint) {
|
||||
let horizontalRange = currentHorizontalRange
|
||||
let chartFrame = self.chartFrame()
|
||||
guard chartFrame.width > 0 else { return }
|
||||
|
||||
let dateToFind = Date(timeIntervalSince1970: TimeInterval(horizontalRange.distance * point.x + horizontalRange.lowerBound))
|
||||
guard let (closestDate, minIndex) = findClosestDateTo(dateToFind: dateToFind) else { return }
|
||||
|
||||
let chartInteractionWasBegin = isChartInteractionBegun
|
||||
super.chartInteractionDidBegin(point: point)
|
||||
|
||||
// for graphController in graphControllers {
|
||||
// graphController.lineBulletsRenderer.bullets = graphController.chartBars.components.map { component in
|
||||
// LineBulletsRenderer.Bullet(coordinate: component.values[minIndex], color: component.color)
|
||||
// }
|
||||
// graphController.lineBulletsRenderer.isEnabled = true
|
||||
// }
|
||||
|
||||
let chartValue: CGFloat = CGFloat(closestDate.timeIntervalSince1970)
|
||||
let detailsViewPosition = (chartValue - horizontalRange.lowerBound) / horizontalRange.distance * chartFrame.width + chartFrame.minX
|
||||
self.setDetailsViewModel?(chartDetailsViewModel(closestDate: closestDate, pointIndex: minIndex), chartInteractionWasBegin)
|
||||
self.setDetailsChartVisibleClosure?(true, true)
|
||||
self.setDetailsViewPositionClosure?(detailsViewPosition)
|
||||
self.verticalLineRenderer.values = [chartValue]
|
||||
}
|
||||
|
||||
// func chartDetailsViewModel(closestDate: Date, pointIndex: Int) -> ChartDetailsViewModel {
|
||||
// var viewModel = super.chartDetailsViewModel(closestDate: closestDate, pointIndex: pointIndex)
|
||||
// let visibleChartValues = self.visibleChartValues
|
||||
// let totalSumm: CGFloat = visibleChartValues.map { CGFloat($0.values[pointIndex]) }.reduce(0, +)
|
||||
//
|
||||
// viewModel.totalValue = ChartDetailsViewModel.Value(prefix: nil,
|
||||
// title: "Total",
|
||||
// value: BaseConstants.detailsNumberFormatter.string(from: totalSumm),
|
||||
// color: .white,
|
||||
// visible: visibleChartValues.count > 1)
|
||||
// return viewModel
|
||||
// }
|
||||
//
|
||||
func chartDetailsViewModel(closestDate: Date, pointIndex: Int) -> ChartDetailsViewModel {
|
||||
let values: [ChartDetailsViewModel.Value] = initialChartCollection.chartValues.enumerated().map { arg in
|
||||
let (index, component) = arg
|
||||
return ChartDetailsViewModel.Value(prefix: nil,
|
||||
title: component.name,
|
||||
value: BaseConstants.detailsNumberFormatter.string(from: NSNumber(value: component.values[pointIndex])) ?? "",
|
||||
color: component.color,
|
||||
visible: chartVisibility[index])
|
||||
}
|
||||
let dateString: String
|
||||
if isZoomed {
|
||||
dateString = BaseConstants.timeDateFormatter.string(from: closestDate)
|
||||
} else {
|
||||
dateString = BaseConstants.headerMediumRangeFormatter.string(from: closestDate)
|
||||
}
|
||||
let viewModel = ChartDetailsViewModel(title: dateString,
|
||||
showArrow: !self.isZoomed,
|
||||
showPrefixes: false,
|
||||
values: values,
|
||||
totalValue: nil,
|
||||
tapAction: { [weak self] in })
|
||||
return viewModel
|
||||
}
|
||||
|
||||
public override func chartInteractionDidEnd() {
|
||||
self.isChartInteractionBegun = false
|
||||
}
|
||||
|
||||
public override var currentHorizontalRange: ClosedRange<CGFloat> {
|
||||
return graphControllers.first?.mainBarsRenderer.horizontalRange.end ?? BaseConstants.defaultRange
|
||||
}
|
||||
|
||||
public override var currentChartHorizontalRangeFraction: ClosedRange<CGFloat> {
|
||||
let lowerPercent = (currentHorizontalRange.lowerBound - totalHorizontalRange.lowerBound) / totalHorizontalRange.distance
|
||||
let upperPercent = (currentHorizontalRange.upperBound - totalHorizontalRange.lowerBound) / totalHorizontalRange.distance
|
||||
return lowerPercent...upperPercent
|
||||
}
|
||||
|
||||
public override func cancelChartInteraction() {
|
||||
super.cancelChartInteraction()
|
||||
self.graphControllers.forEach { controller in
|
||||
controller.lineBulletsRenderer.isEnabled = false
|
||||
}
|
||||
|
||||
self.setDetailsChartVisibleClosure?(false, true)
|
||||
self.verticalLineRenderer.values = []
|
||||
}
|
||||
|
||||
func setupChartCollection(chartsCollection: ChartsCollection, animated: Bool, isZoomed: Bool) {
|
||||
for (index, controller) in self.graphControllers.enumerated() {
|
||||
let chart = chartsCollection.chartValues[index]
|
||||
let points = chart.values.enumerated().map({ (arg) -> CGPoint in
|
||||
return CGPoint(x: chartsCollection.axisValues[arg.offset].timeIntervalSince1970,
|
||||
y: arg.element)
|
||||
})
|
||||
|
||||
let (width, chartBars, totalHorizontalRange, totalVerticalRange) = BarChartRenderer.BarsData.initialComponents(chartsCollection: chartsCollection)
|
||||
controller.chartBars = chartBars
|
||||
controller.barsWidth = width
|
||||
|
||||
controller.verticalScalesRenderer.labelsColor = chart.color
|
||||
|
||||
controller.totalVerticalRange = totalVerticalRange
|
||||
self.totalHorizontalRange = totalHorizontalRange
|
||||
// controller.lineBulletsRenderer.bullets = chartBars.components.map { LineBulletsRenderer.Bullet(coordinate: $0.values.first ?? .zero,
|
||||
// color: $0.color) }
|
||||
controller.previewBarsRenderer.setup(horizontalRange: self.totalHorizontalRange, animated: animated)
|
||||
controller.previewBarsRenderer.setup(verticalRange: controller.totalVerticalRange, animated: animated)
|
||||
|
||||
controller.mainBarsRenderer.bars = chartBars
|
||||
controller.previewBarsRenderer.bars = chartBars
|
||||
|
||||
controller.verticalScalesRenderer.setHorizontalLinesVisible((index == 0), animated: animated)
|
||||
controller.verticalScalesRenderer.isRightAligned = (index != 0)
|
||||
}
|
||||
|
||||
let chartRange: ClosedRange<CGFloat>
|
||||
if isZoomed {
|
||||
chartRange = zoomedChartRange
|
||||
} else {
|
||||
chartRange = initialChartRange
|
||||
}
|
||||
|
||||
// updateHorizontalLimits(horizontalRange: chartRange, animated: animated)
|
||||
updateMainChartHorizontalRange(range: chartRange, animated: animated)
|
||||
updateMainChartVerticalRange(range: chartRange, animated: animated)
|
||||
// updateVerticalLimitsAndRange(horizontalRange: chartRange, animated: animated)
|
||||
|
||||
self.chartRangeUpdatedClosure?(currentChartHorizontalRangeFraction, animated)
|
||||
}
|
||||
|
||||
// func setupChartCollection(chartsCollection: ChartsCollection, animated: Bool, isZoomed: Bool) {
|
||||
// if animated {
|
||||
// TimeInterval.setDefaultSuration(.expandAnimationDuration)
|
||||
// DispatchQueue.main.asyncAfter(deadline: .now() + .expandAnimationDuration) {
|
||||
// TimeInterval.setDefaultSuration(.osXDuration)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// self.initialChartsCollection = chartsCollection
|
||||
// self.isZoomed = isZoomed
|
||||
//
|
||||
// self.setBackButtonVisibilityClosure?(isZoomed, animated)
|
||||
//
|
||||
// self.graphControllers.forEach { controller in
|
||||
// controller.barsController.willAppear(animated: animated)
|
||||
// }
|
||||
//
|
||||
// self.refreshChartToolsClosure?(animated)
|
||||
// }
|
||||
|
||||
public override func didTapZoomIn(date: Date) {
|
||||
guard isZoomed == false else { return }
|
||||
cancelChartInteraction()
|
||||
self.getDetailsData?(date, { updatedCollection in
|
||||
if let updatedCollection = updatedCollection {
|
||||
self.initialChartRange = self.currentHorizontalRange
|
||||
if let startDate = updatedCollection.axisValues.first,
|
||||
let endDate = updatedCollection.axisValues.last {
|
||||
self.zoomedChartRange = CGFloat(max(date.timeIntervalSince1970, startDate.timeIntervalSince1970))...CGFloat(min(date.timeIntervalSince1970 + .day - .hour, endDate.timeIntervalSince1970))
|
||||
} else {
|
||||
self.zoomedChartRange = CGFloat(date.timeIntervalSince1970)...CGFloat(date.timeIntervalSince1970 + .day - 1)
|
||||
}
|
||||
self.setupChartCollection(chartsCollection: updatedCollection, animated: true, isZoomed: true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public override func didTapZoomOut() {
|
||||
cancelChartInteraction()
|
||||
self.setupChartCollection(chartsCollection: self.initialChartCollection, animated: true, isZoomed: false)
|
||||
}
|
||||
|
||||
public override func updateChartRange(_ rangeFraction: ClosedRange<CGFloat>) {
|
||||
cancelChartInteraction()
|
||||
|
||||
let horizontalRange = ClosedRange(uncheckedBounds:
|
||||
(lower: totalHorizontalRange.lowerBound + rangeFraction.lowerBound * totalHorizontalRange.distance,
|
||||
upper: totalHorizontalRange.lowerBound + rangeFraction.upperBound * totalHorizontalRange.distance))
|
||||
|
||||
zoomedChartRange = horizontalRange
|
||||
// updateChartRangeTitle(animated: true)
|
||||
|
||||
updateMainChartHorizontalRange(range: horizontalRange, animated: false)
|
||||
// updateHorizontalLimits(horizontalRange: horizontalRange, animated: true)
|
||||
// updateVerticalLimitsAndRange(horizontalRange: horizontalRange, animated: true)
|
||||
|
||||
// barsController.chartRangeFractionDidUpdated(rangeFraction)
|
||||
//
|
||||
// let totalHorizontalRange = barsController.totalHorizontalRange
|
||||
// let horizontalRange = ClosedRange(uncheckedBounds:
|
||||
// (lower: totalHorizontalRange.lowerBound + rangeFraction.lowerBound * totalHorizontalRange.distance,
|
||||
// upper: totalHorizontalRange.lowerBound + rangeFraction.upperBound * totalHorizontalRange.distance))
|
||||
//
|
||||
// updateMainChartHorizontalRange(range: horizontalRange, animated: false)
|
||||
}
|
||||
|
||||
func updateMainChartHorizontalRange(range: ClosedRange<CGFloat>, animated: Bool) {
|
||||
self.graphControllers.forEach { controller in
|
||||
controller.mainBarsRenderer.setup(horizontalRange: range, animated: animated)
|
||||
// controller.horizontalScalesRenderer.setup(horizontalRange: range, animated: animated)
|
||||
controller.verticalScalesRenderer.setup(horizontalRange: range, animated: animated)
|
||||
controller.lineBulletsRenderer.setup(horizontalRange: range, animated: animated)
|
||||
}
|
||||
self.horizontalScalesRenderer.setup(horizontalRange: range, animated: animated)
|
||||
self.verticalLineRenderer.setup(horizontalRange: range, animated: animated)
|
||||
}
|
||||
|
||||
func updateMainChartVerticalRange(range: ClosedRange<CGFloat>, animated: Bool) {
|
||||
self.verticalLineRenderer.setup(verticalRange: range, animated: animated)
|
||||
|
||||
self.graphControllers.forEach { controller in
|
||||
controller.lineBulletsRenderer.setup(verticalRange: range, animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
override public func apply(colorMode: GColorMode, animated: Bool) {
|
||||
super.apply(colorMode: colorMode, animated: animated)
|
||||
|
||||
self.graphControllers.forEach { controller in
|
||||
controller.verticalScalesRenderer.horizontalLinesColor = colorMode.chartHelperLinesColor
|
||||
controller.lineBulletsRenderer.setInnerColor(colorMode.chartBackgroundColor, animated: animated)
|
||||
controller.verticalScalesRenderer.axisXColor = colorMode.chartStrongLinesColor
|
||||
}
|
||||
verticalLineRenderer.linesColor = colorMode.chartStrongLinesColor
|
||||
}
|
||||
|
||||
public override var drawChartVisibity: Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: Убрать Performance полоски сверзу чартов (Не забыть)
|
||||
//TODO: Добавить ховеры на кнопки
|
||||
Reference in New Issue
Block a user