mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-24 07:05:35 +00:00
Channel statistics improvements
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user