Channel statistics improvements

This commit is contained in:
Ilya Laktyushin
2020-03-12 05:01:21 +04:00
parent d8b99880ea
commit f5dbf47b68
154 changed files with 6186 additions and 10908 deletions

View File

@@ -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
}
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}
}