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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -5363,6 +5363,30 @@ Any member of this group will be able to see messages in the channel.";
"Stats.FollowersTitle" = "FOLLOWERS";
"Stats.NotificationsTitle" = "NOTIFICATIONS";
"Stats.InteractionsTitle" = "INTERACTIONS";
"Stats.InstantViewInteractionsTitle" = "INSTANT VIEW INTERACTIONS";
"Stats.ViewsBySourceTitle" = "VIEWS BY SOURCE";
"Stats.FollowersBySourceTitle" = "FOLLOWERS BY SOURCE";
"Stats.LanguagesTitle" = "LANGUAGES";
"Stats.PostsTitle" = "RECENT POSTS";
"Stats.MessageViews_0" = "%@ views";
"Stats.MessageViews_1" = "%@ view";
"Stats.MessageViews_2" = "%@ views";
"Stats.MessageViews_3_10" = "%@ views";
"Stats.MessageViews_many" = "%@ views";
"Stats.MessageViews_any" = "%@ views";
"Stats.MessageForwards_0" = "%@ forwards";
"Stats.MessageForwards_1" = "%@ forward";
"Stats.MessageForwards_2" = "%@ forwards";
"Stats.MessageForwards_3_10" = "%@ forwardss";
"Stats.MessageForwards_many" = "%@ forwards";
"Stats.MessageForwards_any" = "%@ forwards";
"InstantPage.Views_0" = "%@ views";
"InstantPage.Views_1" = "%@ view";
"InstantPage.Views_2" = "%@ views";
"InstantPage.Views_3_10" = "%@ views";
"InstantPage.Views_many" = "%@ views";
"InstantPage.Views_any" = "%@ views";
"InstantPage.FeedbackButtonShort" = "Leave feedback";

View file

@ -200,6 +200,7 @@ public final class NavigateToChatControllerParams {
public let updateTextInputState: ChatTextInputState?
public let activateInput: Bool
public let keepStack: NavigateToChatKeepStack
public let useExisting: Bool
public let purposefulAction: (() -> Void)?
public let scrollToEndIfExists: Bool
public let activateMessageSearch: Bool
@ -208,7 +209,7 @@ public final class NavigateToChatControllerParams {
public let parentGroupId: PeerGroupId?
public let completion: () -> Void
public init(navigationController: NavigationController, chatController: ChatController? = nil, context: AccountContext, chatLocation: ChatLocation, subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, updateTextInputState: ChatTextInputState? = nil, activateInput: Bool = false, keepStack: NavigateToChatKeepStack = .default, purposefulAction: (() -> Void)? = nil, scrollToEndIfExists: Bool = false, activateMessageSearch: Bool = false, animated: Bool = true, options: NavigationAnimationOptions = [], parentGroupId: PeerGroupId? = nil, completion: @escaping () -> Void = {}) {
public init(navigationController: NavigationController, chatController: ChatController? = nil, context: AccountContext, chatLocation: ChatLocation, subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, updateTextInputState: ChatTextInputState? = nil, activateInput: Bool = false, keepStack: NavigateToChatKeepStack = .default, useExisting: Bool = true, purposefulAction: (() -> Void)? = nil, scrollToEndIfExists: Bool = false, activateMessageSearch: Bool = false, animated: Bool = true, options: NavigationAnimationOptions = [], parentGroupId: PeerGroupId? = nil, completion: @escaping () -> Void = {}) {
self.navigationController = navigationController
self.chatController = chatController
self.context = context
@ -218,6 +219,7 @@ public final class NavigateToChatControllerParams {
self.updateTextInputState = updateTextInputState
self.activateInput = activateInput
self.keepStack = keepStack
self.useExisting = useExisting
self.purposefulAction = purposefulAction
self.scrollToEndIfExists = scrollToEndIfExists
self.activateMessageSearch = activateMessageSearch

View file

@ -194,7 +194,7 @@ private final class AuthDataTransferSplashScreenNode: ViewControllerTracingNode
let badgeTextNode = ImmediateTextNode()
badgeTextNode.displaysAsynchronously = false
badgeTextNode.attributedText = NSAttributedString(string: "\(i + 1)", font: badgeFont, textColor: self.presentationData.theme.list.itemCheckColors.foregroundColor)
badgeTextNode.attributedText = NSAttributedString(string: "\(i + 1)", font: badgeFont, textColor: self.presentationData.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .natural)
badgeTextNode.maximumNumberOfLines = 0
badgeTextNode.lineSpacing = 0.1
badgeTextNodes.append(badgeTextNode)
@ -314,6 +314,8 @@ private final class AuthDataTransferSplashScreenNode: ViewControllerTracingNode
let titleSize = self.titleNode.updateLayout(CGSize(width: layout.size.width - sideInset * 2.0, height: layout.size.height))
let hasRTL = self.badgeTextNodes.first?.cachedLayout?.hasRTL ?? false
var badgeTextSizes: [CGSize] = []
var textSizes: [CGSize] = []
var textContentHeight: CGFloat = 0.0

View file

@ -1,149 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" id="iN0-l3-epB" customClass="ChartStackSection" customModule="GraphTest" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="421" height="485"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="6Pd-im-6vD">
<rect key="frame" x="0.0" y="54" width="421" height="430"/>
<subviews>
<view clipsSubviews="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Gs1-ng-qdf" customClass="ChartView" customModule="GraphTest" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="421" height="250"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="910" verticalHuggingPriority="910" horizontalCompressionResistancePriority="960" verticalCompressionResistancePriority="960" contentHorizontalAlignment="left" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Gze-ly-E3P">
<rect key="frame" x="16" y="0.0" width="100" height="40"/>
<constraints>
<constraint firstAttribute="width" constant="100" id="0fS-5e-eL0"/>
<constraint firstAttribute="height" priority="937" constant="38" id="Qll-Xf-A2W"/>
<constraint firstAttribute="width" secondItem="Gze-ly-E3P" secondAttribute="height" multiplier="20:8" id="eWA-Ka-ghV"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<inset key="contentEdgeInsets" minX="0.0" minY="0.0" maxX="2" maxY="0.0"/>
<state key="normal" title="Zoom Out" image="arrow_left">
<color key="titleColor" red="0.0" green="0.47843137250000001" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<state key="selected">
<color key="titleColor" red="0.0" green="0.47843137250000001" blue="1" alpha="0.38792913732394368" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<connections>
<action selector="didTapBackButton" destination="iN0-l3-epB" eventType="touchUpInside" id="zsN-Ne-pKH"/>
</connections>
</button>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="800" verticalHuggingPriority="800" horizontalCompressionResistancePriority="850" verticalCompressionResistancePriority="850" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="kGq-zn-SjL">
<rect key="frame" x="189.5" y="0.0" width="42" height="38"/>
<constraints>
<constraint firstAttribute="height" constant="38" id="Ngt-jk-4ri"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="Gze-ly-E3P" firstAttribute="leading" secondItem="Gs1-ng-qdf" secondAttribute="leading" constant="16" id="4qc-36-ziS"/>
<constraint firstItem="kGq-zn-SjL" firstAttribute="top" secondItem="Gs1-ng-qdf" secondAttribute="top" id="HG2-7K-e3g"/>
<constraint firstAttribute="height" constant="250" id="PH2-EY-kJT"/>
<constraint firstItem="kGq-zn-SjL" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="Gze-ly-E3P" secondAttribute="trailing" constant="4" id="V4X-jJ-C0F"/>
<constraint firstItem="kGq-zn-SjL" firstAttribute="centerX" secondItem="Gs1-ng-qdf" secondAttribute="centerX" priority="500" id="g1F-0x-Gyq"/>
<constraint firstItem="Gze-ly-E3P" firstAttribute="centerY" secondItem="Gs1-ng-qdf" secondAttribute="top" constant="20" id="rbC-Ih-Hxc"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="kGq-zn-SjL" secondAttribute="trailing" priority="999" constant="16" id="wyb-Ls-dPG"/>
</constraints>
</view>
<view clipsSubviews="YES" contentMode="scaleToFill" horizontalHuggingPriority="900" verticalHuggingPriority="900" horizontalCompressionResistancePriority="950" verticalCompressionResistancePriority="950" translatesAutoresizingMaskIntoConstraints="NO" id="7Oh-hd-0e7" customClass="RangeChartView" customModule="GraphTest" customModuleProvider="target">
<rect key="frame" x="0.0" y="250" width="421" height="58"/>
<color key="backgroundColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="height" constant="58" id="XGF-mc-YZQ"/>
</constraints>
<edgeInsets key="layoutMargins" top="1" left="16" bottom="17" right="16"/>
</view>
<view clipsSubviews="YES" contentMode="scaleToFill" horizontalHuggingPriority="900" verticalHuggingPriority="900" horizontalCompressionResistancePriority="950" verticalCompressionResistancePriority="950" translatesAutoresizingMaskIntoConstraints="NO" id="uuw-Jy-472" customClass="ChartVisibilityView" customModule="GraphTest" customModuleProvider="target">
<rect key="frame" x="0.0" y="308" width="421" height="122"/>
<color key="backgroundColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<edgeInsets key="layoutMargins" top="1" left="16" bottom="1" right="16"/>
</view>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="bottom" secondItem="uuw-Jy-472" secondAttribute="bottom" id="0DO-9R-O0l"/>
<constraint firstItem="Gs1-ng-qdf" firstAttribute="leading" secondItem="6Pd-im-6vD" secondAttribute="leading" id="8wc-1j-dh7"/>
<constraint firstAttribute="trailing" secondItem="Gs1-ng-qdf" secondAttribute="trailing" id="Ax0-cp-SF2"/>
<constraint firstItem="7Oh-hd-0e7" firstAttribute="top" secondItem="Gs1-ng-qdf" secondAttribute="bottom" id="I8A-x2-oZG"/>
<constraint firstAttribute="trailing" secondItem="uuw-Jy-472" secondAttribute="trailing" id="Z1f-t1-4GJ"/>
<constraint firstItem="7Oh-hd-0e7" firstAttribute="leading" secondItem="6Pd-im-6vD" secondAttribute="leading" id="dCz-H4-an3"/>
<constraint firstAttribute="trailing" secondItem="7Oh-hd-0e7" secondAttribute="trailing" id="hQM-uW-x9q"/>
<constraint firstItem="uuw-Jy-472" firstAttribute="leading" secondItem="6Pd-im-6vD" secondAttribute="leading" id="htA-IO-x7c"/>
<constraint firstItem="uuw-Jy-472" firstAttribute="top" secondItem="7Oh-hd-0e7" secondAttribute="bottom" id="kx7-e3-2UX"/>
<constraint firstItem="Gs1-ng-qdf" firstAttribute="top" secondItem="6Pd-im-6vD" secondAttribute="top" id="zN5-uX-gl4"/>
</constraints>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="900" verticalHuggingPriority="900" horizontalCompressionResistancePriority="910" verticalCompressionResistancePriority="910" text="_Header" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="TAY-o7-MHD">
<rect key="frame" x="16" y="27" width="405" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="4QI-3Q-rtc">
<rect key="frame" x="0.0" y="53" width="421" height="1"/>
<color key="backgroundColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="height" constant="1" id="TSt-sG-IxH" customClass="OnePixelConstrain" customModule="GraphTest" customModuleProvider="target"/>
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="AEQ-wS-3p4">
<rect key="frame" x="0.0" y="484" width="421" height="1"/>
<color key="backgroundColor" white="0.66666666669999997" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="height" constant="1" id="q3W-Ii-wV7" customClass="OnePixelConstrain" customModule="GraphTest" customModuleProvider="target"/>
</constraints>
</view>
</subviews>
<color key="backgroundColor" cocoaTouchSystemColor="groupTableViewBackgroundColor"/>
<constraints>
<constraint firstItem="4QI-3Q-rtc" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="1oT-KU-FRt"/>
<constraint firstAttribute="trailing" secondItem="4QI-3Q-rtc" secondAttribute="trailing" id="3Z5-bY-93t"/>
<constraint firstAttribute="bottom" secondItem="AEQ-wS-3p4" secondAttribute="bottom" priority="999" id="4cm-gK-MnK"/>
<constraint firstItem="AEQ-wS-3p4" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="Bah-zR-bfE"/>
<constraint firstItem="TAY-o7-MHD" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" constant="27" id="GIj-mI-5dl"/>
<constraint firstItem="AEQ-wS-3p4" firstAttribute="top" secondItem="6Pd-im-6vD" secondAttribute="bottom" id="GRz-lz-VD5"/>
<constraint firstItem="4QI-3Q-rtc" firstAttribute="top" secondItem="TAY-o7-MHD" secondAttribute="bottom" constant="5" id="T77-y7-mHo"/>
<constraint firstItem="6Pd-im-6vD" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="XLV-JP-MAz"/>
<constraint firstItem="TAY-o7-MHD" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="16" id="caV-uR-OCt"/>
<constraint firstItem="6Pd-im-6vD" firstAttribute="top" secondItem="4QI-3Q-rtc" secondAttribute="bottom" id="fcf-nY-X5q"/>
<constraint firstAttribute="trailing" secondItem="TAY-o7-MHD" secondAttribute="trailing" id="fyT-jz-qX4"/>
<constraint firstAttribute="trailing" secondItem="AEQ-wS-3p4" secondAttribute="trailing" id="xSX-Xx-73t"/>
<constraint firstAttribute="trailing" secondItem="6Pd-im-6vD" secondAttribute="trailing" id="yOk-xT-or5"/>
</constraints>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<connections>
<outlet property="backButton" destination="Gze-ly-E3P" id="tjz-dk-9Re"/>
<outlet property="backButtonWidthConstraint" destination="0fS-5e-eL0" id="CkZ-8G-YjF"/>
<outlet property="chartHeightConstraint" destination="PH2-EY-kJT" id="W0R-Ms-q48"/>
<outlet property="chartView" destination="Gs1-ng-qdf" id="fSW-zn-WuU"/>
<outlet property="headerLabel" destination="TAY-o7-MHD" id="aNR-Oo-dMR"/>
<outlet property="rangeView" destination="7Oh-hd-0e7" id="LaC-Mf-kKQ"/>
<outlet property="sectionContainerView" destination="6Pd-im-6vD" id="lNz-Oe-uVZ"/>
<outlet property="titleLabel" destination="kGq-zn-SjL" id="lhp-Hk-PQi"/>
<outlet property="visibilityView" destination="uuw-Jy-472" id="rZn-nk-wqx"/>
<outletCollection property="separators" destination="4QI-3Q-rtc" collectionClass="NSMutableArray" id="keK-ua-xv6"/>
<outletCollection property="separators" destination="AEQ-wS-3p4" collectionClass="NSMutableArray" id="7We-sB-fTh"/>
</connections>
<point key="canvasLocation" x="-217" y="61"/>
</view>
</objects>
<resources>
<image name="arrow_left" width="15" height="15"/>
</resources>
</document>

View file

@ -1,51 +0,0 @@
//
// ChartsDataLoader.swift
// GraphTest
//
// Created by Andrei Salavei on 4/8/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import Foundation
enum ChartsDataType: String {
case generalLines = "1"
case twoAxisLines = "2"
case stackedBars = "3"
case dailyBars = "4"
case percentPie = "5"
}
private enum Constants {
static let overviewFilename = "overview.json"
static let dataDir = "data"
}
class ChartsDataLoader {
static var documentDirectoryURL: URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
let documentsDirectory = paths[0]
return documentsDirectory
}
static func overviewData(type: ChartsDataType, extraCopiesCount: Int = 0, sync: Bool = false, success: @escaping (ChartsCollection) -> Void) {
let path = Bundle.main.bundleURL
.appendingPathComponent(Constants.dataDir)
.appendingPathComponent(type.rawValue)
.appendingPathComponent(Constants.overviewFilename)
ChartsDataManager().readChart(file: path, extraCopiesCount: extraCopiesCount, sync: sync, success: success, failure: { _ in })
}
static func detaildData(type: ChartsDataType, extraCopiesCount: Int = 0, date: Date, success: @escaping (ChartsCollection) -> Void, failure: @escaping (Error) -> Void) {
let dateComponents = Calendar.utc.dateComponents([.day, .month, .year], from: date)
let yearMonth = String(format: "%04d-%02d", dateComponents.year ?? 0, dateComponents.month ?? 0)
let day = String(format: "%02d.json", dateComponents.day ?? 0)
let path = Bundle.main.bundleURL
.appendingPathComponent(Constants.dataDir)
.appendingPathComponent(type.rawValue)
.appendingPathComponent(yearMonth)
.appendingPathComponent(day)
ChartsDataManager().readChart(file: path, extraCopiesCount: extraCopiesCount, sync: false, success: success, failure: failure)
}
}

View file

@ -1,92 +0,0 @@
//
// ChardData.swift
// GraphTest
//
// Created by Andrei Salavei on 3/11/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import Foundation
import UIKit
struct ChartsCollection {
struct Chart {
var color: UIColor
var name: String
var values: [Double]
}
var axisValues: [Date]
var chartValues: [Chart]
static let blank = ChartsCollection(axisValues: [], chartValues: [])
var isBlank: Bool {
return axisValues.isEmpty || chartValues.isEmpty
}
}
extension ChartsCollection {
public init(from decodedData: [String: Any]) throws {
guard let columns = decodedData["columns"] as? [[Any]] else {
throw ChartsError.generalConversion("Unable to get columns from: \(decodedData)")
}
guard let types = decodedData["types"] as? [String: String] else {
throw ChartsError.generalConversion("Unable to get types from: \(decodedData)")
}
guard let names = decodedData["names"] as? [String: String] else {
throw ChartsError.generalConversion("Unable to get names from: \(decodedData)")
}
guard let colors = decodedData["colors"] as? [String: String] else {
throw ChartsError.generalConversion("Unable to get colors from: \(decodedData)")
}
// chart.colors Color for each variable in 6-hex-digit format (e.g. "#AAAAAA").
// chart.names Name for each variable.
// chart.percentage true for percentage based values.
// chart.stacked true for values stacking on top of each other.
// chart.y_scaled true for charts with 2 Y axes.
var axixValuesToSetup: [Date] = []
var chartToSetup: [Chart] = []
for column in columns {
guard let columnId = column.first as? String else {
throw ChartsError.generalConversion("Unable to get column name from: \(column)")
}
guard let typeString = types[columnId], let type = ColumnType(rawValue: typeString) else {
throw ChartsError.generalConversion("Unable to get column type from: \(types) - \(columnId)")
}
switch type {
case .axix:
axixValuesToSetup = try column.dropFirst().map { Date(timeIntervalSince1970: try Convert.doubleFrom($0) / 1000) }
case .chart, .bar, .area, .step:
guard let colorString = colors[columnId],
let color = UIColor(hexString: colorString) else {
throw ChartsError.generalConversion("Unable to get color name from: \(colors) - \(columnId)")
}
guard let name = names[columnId] else {
throw ChartsError.generalConversion("Unable to get column name from: \(names) - \(columnId)")
}
let values = try column.dropFirst().map { try Convert.doubleFrom($0) }
chartToSetup.append(Chart(color: color,
name: name,
values: values))
}
}
guard axixValuesToSetup.isEmpty == false,
chartToSetup.isEmpty == false,
chartToSetup.firstIndex(where: { $0.values.count != axixValuesToSetup.count }) == nil else {
throw ChartsError.generalConversion("Sanitazing: Invalid number of items: \(axixValuesToSetup), \(chartToSetup)")
}
self.axisValues = axixValuesToSetup
self.chartValues = chartToSetup
}
}
private enum ColumnType: String {
case axix = "x"
case chart = "line"
case area = "area"
case bar = "bar"
case step = "step"
}

View file

@ -1,191 +0,0 @@
//
// ChartsDataManager.swift
// GraphTest
//
// Created by Andrei Salavei on 3/11/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import Foundation
class ChartsDataManager {
func readChart(item: [String: Any], extraCopiesCount: Int = 0, sync: Bool, success: @escaping (ChartsCollection) -> Void, failure: @escaping (Error) -> Void) {
let workItem: (() -> Void) = {
do {
var collection = try ChartsCollection(from: item)
for _ in 0..<extraCopiesCount {
for valueIndex in collection.chartValues.indices {
collection.chartValues[valueIndex] .values += collection.chartValues[valueIndex].values
}
guard let firstValue = collection.axisValues.first,
let lastValule = collection.axisValues.last else {
throw ChartsError.invalidJson
}
let startItem = lastValule.addingTimeInterval(.day)
for valueIndex in collection.axisValues.indices {
let intervalToAdd = collection.axisValues[valueIndex].timeIntervalSince(firstValue)
let newDate = startItem.addingTimeInterval(intervalToAdd)
collection.axisValues.append(newDate)
}
}
if sync {
success(collection)
} else {
DispatchQueue.main.async {
success(collection)
}
}
} catch {
DispatchQueue.main.async {
assertionFailure("Error occure: \(error)")
failure(error)
}
}
}
if sync {
workItem()
} else {
DispatchQueue.global().async(execute: workItem)
}
}
func readChart(data: Data, extraCopiesCount: Int = 0, sync: Bool, success: @escaping (ChartsCollection) -> Void, failure: @escaping (Error) -> Void) {
let workItem: (() -> Void) = {
do {
let decoded = try JSONSerialization.jsonObject(with: data, options: [])
guard let item = decoded as? [String: Any] else {
throw ChartsError.invalidJson
}
var collection = try ChartsCollection(from: item)
for _ in 0..<extraCopiesCount {
for valueIndex in collection.chartValues.indices {
collection.chartValues[valueIndex] .values += collection.chartValues[valueIndex].values
}
guard let firstValue = collection.axisValues.first,
let lastValule = collection.axisValues.last else {
throw ChartsError.invalidJson
}
let startItem = lastValule.addingTimeInterval(.day)
for valueIndex in collection.axisValues.indices {
let intervalToAdd = collection.axisValues[valueIndex].timeIntervalSince(firstValue)
let newDate = startItem.addingTimeInterval(intervalToAdd)
collection.axisValues.append(newDate)
}
}
if sync {
success(collection)
} else {
DispatchQueue.main.async {
success(collection)
}
}
} catch {
DispatchQueue.main.async {
assertionFailure("Error occure: \(error)")
failure(error)
}
}
}
if sync {
workItem()
} else {
DispatchQueue.global().async(execute: workItem)
}
}
func readChart(file: URL, extraCopiesCount: Int = 0, sync: Bool, success: @escaping (ChartsCollection) -> Void, failure: @escaping (Error) -> Void) {
let workItem: (() -> Void) = {
do {
let data = try Data(contentsOf: file)
let decoded = try JSONSerialization.jsonObject(with: data, options: [])
guard let item = decoded as? [String: Any] else {
throw ChartsError.invalidJson
}
var collection = try ChartsCollection(from: item)
for _ in 0..<extraCopiesCount {
for valueIndex in collection.chartValues.indices {
collection.chartValues[valueIndex] .values += collection.chartValues[valueIndex].values
}
guard let firstValue = collection.axisValues.first,
let lastValule = collection.axisValues.last else {
throw ChartsError.invalidJson
}
let startItem = lastValule.addingTimeInterval(.day)
for valueIndex in collection.axisValues.indices {
let intervalToAdd = collection.axisValues[valueIndex].timeIntervalSince(firstValue)
let newDate = startItem.addingTimeInterval(intervalToAdd)
collection.axisValues.append(newDate)
}
}
if sync {
success(collection)
} else {
DispatchQueue.main.async {
success(collection)
}
}
} catch {
DispatchQueue.main.async {
assertionFailure("Error occure: \(error)")
failure(error)
}
}
}
if sync {
workItem()
} else {
DispatchQueue.global().async(execute: workItem)
}
}
func readCharts(file: URL, extraCopiesCount: Int = 0, sync: Bool, success: @escaping ([ChartsCollection]) -> Void, failure: @escaping (Error) -> Void) {
let workItem: (() -> Void) = {
do {
let data = try Data(contentsOf: file)
let decoded = try JSONSerialization.jsonObject(with: data, options: [])
guard let items = decoded as? [[String: Any]] else {
throw ChartsError.invalidJson
}
var collections = try items.map { try ChartsCollection(from: $0) }
for _ in 0..<extraCopiesCount {
for collrctionIndex in collections.indices {
for valueIndex in collections[collrctionIndex].chartValues.indices {
collections[collrctionIndex].chartValues[valueIndex] .values += collections[collrctionIndex].chartValues[valueIndex].values
}
guard let firstValue = collections[collrctionIndex].axisValues.first,
let lastValule = collections[collrctionIndex].axisValues.last else {
return
}
let startItem = lastValule.addingTimeInterval(.day)
for valueIndex in collections[collrctionIndex].axisValues.indices {
let intervalToAdd = collections[collrctionIndex].axisValues[valueIndex].timeIntervalSince(firstValue)
let newDate = startItem.addingTimeInterval(intervalToAdd)
collections[collrctionIndex].axisValues.append(newDate)
}
}
}
if sync {
success(collections)
} else {
DispatchQueue.main.async {
success(collections)
}
}
} catch {
DispatchQueue.main.async {
assertionFailure("Error occure: \(error)")
failure(error)
}
}
}
if sync {
workItem()
} else {
DispatchQueue.global().async(execute: workItem)
}
}
}

View file

@ -1,14 +0,0 @@
//
// ChartsError.swift
// GraphTest
//
// Created by Andrei Salavei on 3/11/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import Foundation
enum ChartsError: Error {
case invalidJson
case generalConversion(String)
}

View file

@ -1,42 +0,0 @@
//
// Convert.swift
// GraphTest
//
// Created by Andrei Salavei on 3/11/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import Foundation
public enum Convert {
public static func doubleFrom(_ value: Any?) throws -> Double {
guard let double = try doubleFrom(value, lenientCast: false) else {
throw ChartsError.generalConversion("Unable to cast \(String(describing: value)) to \(Double.self)")
}
return double
}
public static func doubleFrom(_ value: Any?, lenientCast: Bool = false) throws -> Double? {
guard let value = value else {
return nil
}
if let intValue = value as? Int {
return Double(intValue)
} else if let floatValue = value as? Float {
return Double(floatValue)
} else if let int64Value = value as? Int64 {
return Double(int64Value)
} else if let intValue = value as? Int {
return Double(intValue)
} else if let stringValue = value as? String {
if let doubleValue = Double(stringValue) {
return doubleValue
}
}
if lenientCast {
return nil
} else {
throw ChartsError.generalConversion("Unable to cast \(String(describing: value)) to \(Double.self)")
}
}
}

View file

@ -1,19 +0,0 @@
//
// StatisticsUI.h
// StatisticsUI
//
// Created by Peter on 8/13/19.
// Copyright © 2019 Telegram Messenger LLP. All rights reserved.
//
#import <UIKit/UIKit.h>
//! Project version number for StatisticsUI.
FOUNDATION_EXPORT double StatisticsUIVersionNumber;
//! Project version string for StatisticsUI.
FOUNDATION_EXPORT const unsigned char StatisticsUIVersionString[];
// In this header, you should import all the public headers of your framework using statements like #import <StatisticsUI/PublicHeader.h>

View file

@ -1,170 +0,0 @@
//
// BaseChartController.swift
// GraphTest
//
// Created by Andrei Salavei on 4/7/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import UIKit
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
}()
}
class BaseChartController: ColorModeContainer {
//let performanceRenderer = PerformanceRenderer()
var initialChartsCollection: ChartsCollection
var isZoomed: Bool = false
var chartTitle: String = ""
init(chartsCollection: ChartsCollection) {
self.initialChartsCollection = chartsCollection
}
var mainChartRenderers: [ChartViewRenderer] {
fatalError("Abstract")
}
var navigationRenderers: [ChartViewRenderer] {
fatalError("Abstract")
}
var cartViewBounds: (() -> CGRect) = { fatalError() }
var chartFrame: (() -> CGRect) = { fatalError() }
func initializeChart() {
fatalError("Abstract")
}
func chartInteractionDidBegin(point: CGPoint) {
fatalError("Abstract")
}
func chartInteractionDidEnd() {
fatalError("Abstract")
}
func cancelChartInteraction() {
fatalError("Abstract")
}
func didTapZoomOut() {
fatalError("Abstract")
}
func updateChartsVisibility(visibility: [Bool], animated: Bool) {
fatalError("Abstract")
}
var currentHorizontalRange: ClosedRange<CGFloat> {
fatalError("Abstract")
}
var isChartRangePagingEnabled: Bool = false
var minimumSelectedChartRange: CGFloat = 0.05
var chartRangePagingClosure: ((Bool, CGFloat) -> Void)? // isEnabled, PageSize
func setChartRangePagingEnabled(isEnabled: Bool, minimumSelectionSize: CGFloat) {
isChartRangePagingEnabled = isEnabled
minimumSelectedChartRange = minimumSelectionSize
chartRangePagingClosure?(isChartRangePagingEnabled, minimumSelectedChartRange)
}
var chartRangeUpdatedClosure: ((ClosedRange<CGFloat>, Bool) -> Void)?
var currentChartHorizontalRangeFraction: ClosedRange<CGFloat> {
fatalError("Abstract")
}
func updateChartRangeTitle(animated: Bool) {
}
func updateChartRange(_ rangeFraction: ClosedRange<CGFloat>, animated: Bool = true) {
fatalError("Abstract")
}
var actualChartVisibility: [Bool] {
fatalError("Abstract")
}
var actualChartsCollection: ChartsCollection {
fatalError("Abstract")
}
var drawChartVisibity: Bool {
return true
}
var drawChartNavigation: Bool {
return true
}
var setDetailsViewPositionClosure: ((CGFloat) -> Void)?
var setDetailsChartVisibleClosure: ((Bool, Bool) -> Void)?
var setDetailsViewModel: ((ChartDetailsViewModel, Bool) -> Void)?
var getDetailsData: ((Date, @escaping (ChartsCollection?) -> Void) -> Void)?
var setChartTitleClosure: ((String, Bool) -> Void)?
var setBackButtonVisibilityClosure: ((Bool, Bool) -> Void)?
var refreshChartToolsClosure: ((Bool) -> Void)?
func didTapZoomIn(date: Date) {
fatalError("Abstract")
}
func apply(colorMode: ColorMode, animated: Bool) {
}
}

View file

@ -1,328 +0,0 @@
//
// GeneralChartComponentController.swift
// GraphTest
//
// Created by Andrei Salavei on 4/11/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import UIKit
enum GeneralChartComponentConstants {
static let defaultInitialRangeLength = CGFloat(TimeInterval.day * 60)
static let defaultZoomedRangeLength = CGFloat(TimeInterval.day)
}
class GeneralChartComponentController: ColorModeContainer {
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: ColorMode = .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
var cartViewBounds: (() -> CGRect) = { fatalError() }
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: ColorMode, 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)
}
}
}

View file

@ -1,236 +0,0 @@
//
// BaseLinesChartController.swift
// GraphTest
//
// Created by Andrei Salavei on 4/14/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import UIKit
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 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.setDefaultDuration(.expandAnimationDuration)
DispatchQueue.main.asyncAfter(deadline: .now() + .expandAnimationDuration) {
TimeInterval.setDefaultDuration(.osXDuration)
}
}
self.initialChartsCollection = chartsCollection
self.isZoomed = isZoomed
self.setBackButtonVisibilityClosure?(isZoomed, animated)
updateChartRangeTitle(animated: animated)
}
override func updateChartRangeTitle(animated: Bool) {
let fromDate = Date(timeIntervalSince1970: TimeInterval(zoomedChartRange.lowerBound) + .hour)
let toDate = Date(timeIntervalSince1970: TimeInterval(zoomedChartRange.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)
}
}
override func chartInteractionDidBegin(point: CGPoint) {
lastChartInteractionPoint = point
isChartInteractionBegun = true
}
override func chartInteractionDidEnd() {
}
override func cancelChartInteraction() {
isChartInteractionBegun = false
}
override func updateChartRange(_ rangeFraction: ClosedRange<CGFloat>, animated: Bool) {
}
override var actualChartVisibility: [Bool] {
return isZoomed ? zoomChartVisibility : chartVisibility
}
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
}
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

@ -1,247 +0,0 @@
//
// LinesChartController.swift
// GraphTest
//
// Created by Andrei Salavei on 4/7/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import UIKit
private enum Constants {
static let defaultRange: ClosedRange<CGFloat> = 0...1
}
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 lineBulletsRenerer = LineBulletsRenerer()
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 init(chartsCollection: ChartsCollection) {
self.initialChartCollection = chartsCollection
self.mainLinesRenderer.lineWidth = 2
self.mainLinesRenderer.optimizationLevel = BaseConstants.linesChartOptimizationLevel
self.previewLinesRenderer.optimizationLevel = BaseConstants.previewLinesChartOptimizationLevel
self.lineBulletsRenerer.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.lineBulletsRenerer.bullets = self.chartLines.map { LineBulletsRenerer.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)
updateHorizontalLimists(horizontalRange: chartRange, animated: animated)
updateMainChartHorizontalRange(range: chartRange, animated: animated)
updateVerticalLimitsAndRange(horizontalRange: chartRange, animated: animated)
self.chartRangeUpdatedClosure?(currentChartHorizontalRangeFraction, animated)
}
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)
}
override var mainChartRenderers: [ChartViewRenderer] {
return [//performanceRenderer,
mainLinesRenderer,
horizontalScalesRenderer,
verticalScalesRenderer,
verticalLineRenderer,
lineBulletsRenerer
]
}
override var navigationRenderers: [ChartViewRenderer] {
return [previewLinesRenderer]
}
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)
lineBulletsRenerer.setLineVisible(isVisible, at: index, animated: animated)
}
updateVerticalLimitsAndRange(horizontalRange: currentHorizontalRange, animated: true)
if isChartInteractionBegun {
chartInteractionDidBegin(point: lastChartInteractionPoint)
}
}
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.lineBulletsRenerer.bullets = chartLines.compactMap { chart in
return LineBulletsRenerer.Bullet(coordinate: chart.points[minIndex], color: chart.color)
}
self.lineBulletsRenerer.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]
}
override var currentChartHorizontalRangeFraction: ClosedRange<CGFloat> {
let lowerPercent = (currentHorizontalRange.lowerBound - totalHorizontalRange.lowerBound) / totalHorizontalRange.distance
let upperPercent = (currentHorizontalRange.upperBound - totalHorizontalRange.lowerBound) / totalHorizontalRange.distance
return lowerPercent...upperPercent
}
override var currentHorizontalRange: ClosedRange<CGFloat> {
return mainLinesRenderer.horizontalRange.end
}
override func cancelChartInteraction() {
super.cancelChartInteraction()
self.lineBulletsRenerer.isEnabled = false
self.setDetailsChartVisibleClosure?(false, true)
self.verticalLineRenderer.values = []
}
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
}
override func updateChartRange(_ rangeFraction: ClosedRange<CGFloat>, animated: Bool) {
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)
updateHorizontalLimists(horizontalRange: horizontalRange, animated: animated)
updateVerticalLimitsAndRange(horizontalRange: horizontalRange, animated: animated)
}
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)
lineBulletsRenerer.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)
lineBulletsRenerer.setup(verticalRange: range, animated: animated)
}
func updateHorizontalLimists(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 func apply(colorMode: ColorMode, animated: Bool) {
horizontalScalesRenderer.labelsColor = colorMode.chartLabelsColor
verticalScalesRenderer.labelsColor = colorMode.chartLabelsColor
verticalScalesRenderer.axisXColor = colorMode.chartStrongLinesColor
verticalScalesRenderer.horizontalLinesColor = colorMode.chartHelperLinesColor
lineBulletsRenerer.setInnerColor(colorMode.chartBackgroundColor, animated: animated)
verticalLineRenderer.linesColor = colorMode.chartStrongLinesColor
}
}

View file

@ -1,306 +0,0 @@
//
// TwoAxisLinesChartController.swift
// GraphTest
//
// Created by Andrei Salavei on 4/7/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import UIKit
private enum Constants {
static let verticalBaseAnchors: [CGFloat] = [8, 5, 4, 2.5, 2, 1]
static let defaultRange: ClosedRange<CGFloat> = 0...1
}
class TwoAxisLinesChartController: BaseLinesChartController {
class GraphController {
let mainLinesRenderer = LinesChartRenderer()
let verticalScalesRenderer = VerticalScalesRenderer()
let lineBulletsRenerer = LineBulletsRenerer()
let previewLinesRenderer = LinesChartRenderer()
var chartLines: [LinesChartRenderer.LineData] = []
var totalVerticalRange: ClosedRange<CGFloat> = Constants.defaultRange
init() {
self.mainLinesRenderer.lineWidth = 2
self.previewLinesRenderer.lineWidth = 1
self.lineBulletsRenerer.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)
lineBulletsRenerer.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 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.lineBulletsRenerer.bullets = chartLines.map { LineBulletsRenerer.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
}
updateHorizontalLimists(horizontalRange: chartRange, animated: animated)
updateMainChartHorizontalRange(range: chartRange, animated: animated)
updateVerticalLimitsAndRange(horizontalRange: chartRange, animated: animated)
self.chartRangeUpdatedClosure?(currentChartHorizontalRangeFraction, animated)
}
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)
}
override var mainChartRenderers: [ChartViewRenderer] {
return graphControllers.map { $0.mainLinesRenderer } +
graphControllers.flatMap { [$0.verticalScalesRenderer, $0.lineBulletsRenerer] } +
[horizontalScalesRenderer, verticalLineRenderer,
// performanceRenderer
]
}
override var navigationRenderers: [ChartViewRenderer] {
return graphControllers.map { $0.previewLinesRenderer }
}
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.lineBulletsRenerer.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)
}
}
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.lineBulletsRenerer.bullets = graphController.chartLines.map { chart in
LineBulletsRenerer.Bullet(coordinate: chart.points[minIndex], color: chart.color)
}
graphController.lineBulletsRenerer.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]
}
override var currentChartHorizontalRangeFraction: ClosedRange<CGFloat> {
let lowerPercent = (currentHorizontalRange.lowerBound - totalHorizontalRange.lowerBound) / totalHorizontalRange.distance
let upperPercent = (currentHorizontalRange.upperBound - totalHorizontalRange.lowerBound) / totalHorizontalRange.distance
return lowerPercent...upperPercent
}
override var currentHorizontalRange: ClosedRange<CGFloat> {
return graphControllers.first?.mainLinesRenderer.horizontalRange.end ?? Constants.defaultRange
}
override func cancelChartInteraction() {
super.cancelChartInteraction()
for graphController in graphControllers {
graphController.lineBulletsRenerer.isEnabled = false
}
self.setDetailsChartVisibleClosure?(false, true)
self.verticalLineRenderer.values = []
}
override func didTapZoomOut() {
cancelChartInteraction()
self.setupChartCollection(chartsCollection: initialChartCollection, animated: true, isZoomed: false)
}
override func updateChartRange(_ rangeFraction: ClosedRange<CGFloat>, animated: Bool) {
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)
updateHorizontalLimists(horizontalRange: horizontalRange, animated: animated)
updateVerticalLimitsAndRange(horizontalRange: horizontalRange, animated: animated)
}
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.lineBulletsRenerer.setup(horizontalRange: range, animated: animated)
}
horizontalScalesRenderer.setup(horizontalRange: range, animated: animated)
verticalLineRenderer.setup(horizontalRange: range, animated: animated)
}
func updateHorizontalLimists(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)
}
}
}
override func apply(colorMode: ColorMode, animated: Bool) {
horizontalScalesRenderer.labelsColor = colorMode.chartLabelsColor
verticalLineRenderer.linesColor = colorMode.chartStrongLinesColor
for controller in graphControllers {
controller.verticalScalesRenderer.horizontalLinesColor = colorMode.chartHelperLinesColor
controller.lineBulletsRenerer.setInnerColor(colorMode.chartBackgroundColor, animated: animated)
controller.verticalScalesRenderer.axisXColor = colorMode.chartStrongLinesColor
}
}
}

View file

@ -1,195 +0,0 @@
//
// PercentChartComponentController.swift
// GraphTest
//
// Created by Andrei Salavei on 4/14/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import UIKit
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)
}
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: ColorMode, 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
}
}

View file

@ -1,281 +0,0 @@
//
// PercentPieChartController.swift
// GraphTest
//
// Created by Andrei Salavei on 4/7/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import UIKit
enum PercentConstants {
static let percentValueFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.positiveSuffix = "%"
return formatter
}()
}
private enum Constants {
static let zoomedRange = 7
}
class PercentPieChartController: BaseChartController {
let percentController: PercentChartComponentController
let pieController: PieChartComponentController
let transitionRenderer: PercentPieAnimationRenderer
override 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
}
override var mainChartRenderers: [ChartViewRenderer] {
return [percentController.mainPecentChartRenderer,
transitionRenderer,
percentController.horizontalScalesRenderer,
percentController.verticalScalesRenderer,
percentController.verticalLineRenderer,
pieController.pieChartRenderer,
// performanceRenderer
]
}
override var navigationRenderers: [ChartViewRenderer] {
return [percentController.previewPercentChartRenderer,
pieController.previewBarChartRenderer]
}
override func initializeChart() {
percentController.initialize(chartsCollection: initialChartsCollection,
initialDate: Date(),
totalHorizontalRange: BaseConstants.defaultRange,
totalVerticalRange: BaseConstants.defaultRange)
switchToChart(chartsCollection: percentController.chartsCollection, isZoomed: false, animated: false)
}
func switchToChart(chartsCollection: ChartsCollection, isZoomed: Bool, animated: Bool) {
if animated {
TimeInterval.setDefaultDuration(.expandAnimationDuration)
DispatchQueue.main.asyncAfter(deadline: .now() + .expandAnimationDuration) {
TimeInterval.setDefaultDuration(.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
}
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
}
override var actualChartVisibility: [Bool] {
return isZoomed ? pieController.chartVisibility : percentController.chartVisibility
}
override var actualChartsCollection: ChartsCollection {
return isZoomed ? pieController.chartsCollection : percentController.chartsCollection
}
override func chartInteractionDidBegin(point: CGPoint) {
if isZoomed {
pieController.chartInteractionDidBegin(point: point)
} else {
percentController.chartInteractionDidBegin(point: point)
}
}
override func chartInteractionDidEnd() {
if isZoomed {
pieController.chartInteractionDidEnd()
} else {
percentController.chartInteractionDidEnd()
}
}
override var drawChartVisibity: Bool {
return true
}
override var currentChartHorizontalRangeFraction: ClosedRange<CGFloat> {
if isZoomed {
return pieController.currentChartHorizontalRangeFraction
} else {
return percentController.currentChartHorizontalRangeFraction
}
}
override func cancelChartInteraction() {
if isZoomed {
return pieController.hideDetailsView(animated: true)
} else {
return percentController.hideDetailsView(animated: true)
}
}
override func didTapZoomIn(date: Date) {
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)
}
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)
})
}
override func updateChartRange(_ rangeFraction: ClosedRange<CGFloat>, animated: Bool) {
if isZoomed {
return pieController.chartRangeFractionDidUpdated(rangeFraction)
} else {
return percentController.chartRangeFractionDidUpdated(rangeFraction)
}
}
override func apply(colorMode: ColorMode, animated: Bool) {
super.apply(colorMode: colorMode, animated: animated)
pieController.apply(colorMode: colorMode, animated: animated)
percentController.apply(colorMode: colorMode, animated: animated)
transitionRenderer.backgroundColor = colorMode.chartBackgroundColor
}
}

View file

@ -1,198 +0,0 @@
//
// PieChartComponentController.swift
// GraphTest
//
// Created by Andrei Salavei on 4/14/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import UIKit
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)
}
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
override func chartInteractionDidBegin(point: CGPoint) {
lastInteractionPoint = point
}
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)
}
}
}

View file

@ -1,226 +0,0 @@
//
// BarsComponentController.swift
// GraphTest
//
// Created by Andrei Salavei on 4/14/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import UIKit
class BarsComponentController: GeneralChartComponentController {
let mainBarsRenderer: BarChartRenderer
let horizontalScalesRenderer: HorizontalScalesRenderer
let verticalScalesRenderer: 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.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: totalVerticalRange, animated: animated)
previewBarsChartRenderer.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) {
mainBarsRenderer.setVisible(visible, animated: animated)
horizontalScalesRenderer.setVisible(visible, animated: animated)
verticalScalesRenderer.setVisible(visible, animated: animated)
previewBarsChartRenderer.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)
}
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)
} else {
verticalScalesRenderer.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) {
mainBarsRenderer.setup(verticalRange: verticalRange, animated: animated)
horizontalScalesRenderer.setup(verticalRange: verticalRange, animated: animated)
verticalScalesRenderer.setup(verticalRange: verticalRange, animated: animated)
}
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: ColorMode, 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)
}
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)
}
}
}

View file

@ -1,249 +0,0 @@
//
// DailyBarsChartController.swift
// GraphTest
//
// Created by Andrei Salavei on 4/7/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import UIKit
class DailyBarsChartController: BaseChartController {
let barsController: BarsComponentController
let linesController: LinesComponentController
override 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(),
lineBulletsRenerer: LineBulletsRenerer(),
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)
}
}
}
override var mainChartRenderers: [ChartViewRenderer] {
return [barsController.mainBarsRenderer,
linesController.mainLinesRenderer,
barsController.horizontalScalesRenderer,
barsController.verticalScalesRenderer,
linesController.verticalLineRenderer,
linesController.lineBulletsRenerer,
// performanceRenderer
]
}
override var navigationRenderers: [ChartViewRenderer] {
return [barsController.previewBarsChartRenderer,
linesController.previewLinesChartRenderer]
}
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.setDefaultDuration(.expandAnimationDuration)
DispatchQueue.main.asyncAfter(deadline: .now() + .expandAnimationDuration) {
TimeInterval.setDefaultDuration(.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)
}
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
}
override var actualChartVisibility: [Bool] {
return isZoomed ? linesController.chartVisibility : barsController.chartVisibility
}
override var actualChartsCollection: ChartsCollection {
return isZoomed ? linesController.chartsCollection : barsController.chartsCollection
}
override func chartInteractionDidBegin(point: CGPoint) {
if isZoomed {
linesController.chartInteractionDidBegin(point: point)
} else {
barsController.chartInteractionDidBegin(point: point)
}
}
override func chartInteractionDidEnd() {
if isZoomed {
linesController.chartInteractionDidEnd()
} else {
barsController.chartInteractionDidEnd()
}
}
override var currentChartHorizontalRangeFraction: ClosedRange<CGFloat> {
if isZoomed {
return linesController.currentChartHorizontalRangeFraction
} else {
return barsController.currentChartHorizontalRangeFraction
}
}
override func cancelChartInteraction() {
if isZoomed {
return linesController.hideDetailsView(animated: true)
} else {
return barsController.hideDetailsView(animated: true)
}
}
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
}
override func didTapZoomOut() {
cancelChartInteraction()
switchToChart(chartsCollection: barsController.chartsCollection, isZoomed: false, animated: true)
}
override func updateChartRange(_ rangeFraction: ClosedRange<CGFloat>, animated: Bool) {
if isZoomed {
return linesController.chartRangeFractionDidUpdated(rangeFraction)
} else {
return barsController.chartRangeFractionDidUpdated(rangeFraction)
}
}
override func apply(colorMode: ColorMode, animated: Bool) {
super.apply(colorMode: colorMode, animated: animated)
linesController.apply(colorMode: colorMode, animated: animated)
barsController.apply(colorMode: colorMode, animated: animated)
}
override var drawChartVisibity: Bool {
return isZoomed
}
}
//TODO: Убрать Performance полоски сверзу чартов (Не забыть)
//TODO: Добавить ховеры на кнопки

View file

@ -1,210 +0,0 @@
//
// LinesComponentController.swift
// GraphTest
//
// Created by Andrei Salavei on 4/14/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import UIKit
class LinesComponentController: GeneralChartComponentController {
let mainLinesRenderer: LinesChartRenderer
let horizontalScalesRenderer: HorizontalScalesRenderer
let verticalScalesRenderer: VerticalScalesRenderer
let verticalLineRenderer: VerticalLinesRenderer
let lineBulletsRenerer: LineBulletsRenerer
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,
lineBulletsRenerer: LineBulletsRenerer,
previewLinesChartRenderer: LinesChartRenderer) {
self.mainLinesRenderer = mainLinesRenderer
self.horizontalScalesRenderer = horizontalScalesRenderer
self.verticalScalesRenderer = verticalScalesRenderer
self.verticalLineRenderer = verticalLineRenderer
self.lineBulletsRenerer = lineBulletsRenerer
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.lineBulletsRenerer.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.lineBulletsRenerer.bullets = self.chartLines.map { LineBulletsRenerer.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)
lineBulletsRenerer.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)
lineBulletsRenerer.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)
lineBulletsRenerer.setup(verticalRange: verticalRange, animated: animated)
}
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)
lineBulletsRenerer.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
lineBulletsRenerer.isEnabled = true
lineBulletsRenerer.setVisible(true, animated: animted)
lineBulletsRenerer.bullets = chartLines.compactMap { chart in
return LineBulletsRenerer.Bullet(coordinate: chart.points[dataIndex], color: chart.color)
}
}
override func hideDetailsView(animated: Bool) {
super.hideDetailsView(animated: animated)
verticalLineRenderer.values = []
verticalLineRenderer.isEnabled = false
lineBulletsRenerer.isEnabled = false
}
override func apply(colorMode: ColorMode, animated: Bool) {
super.apply(colorMode: colorMode, animated: animated)
horizontalScalesRenderer.labelsColor = colorMode.chartLabelsColor
verticalScalesRenderer.labelsColor = colorMode.chartLabelsColor
verticalScalesRenderer.axisXColor = colorMode.chartStrongLinesColor
verticalScalesRenderer.horizontalLinesColor = colorMode.chartHelperLinesColor
lineBulletsRenerer.setInnerColor(colorMode.chartBackgroundColor, animated: animated)
verticalLineRenderer.linesColor = colorMode.chartStrongLinesColor
}
}

View file

@ -1,293 +0,0 @@
//
// BarChartRenderer.swift
// GraphTest
//
// Created by Andrei Salavei on 4/7/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import UIKit
class BarChartRenderer: BaseChartRenderer {
struct BarsData {
static let blank = BarsData(barWidth: 1, locations: [], components: [])
var barWidth: CGFloat
var locations: [CGFloat]
var components: [Component]
struct Component {
var color: UIColor
var values: [CGFloat]
}
}
var fillToTop: Bool = false
private(set) lazy var selectedIndexAnimator: AnimationController<CGFloat> = {
return AnimationController(current: 0, refreshClosure: self.refreshClosure)
}()
func setSelectedIndex(_ index: Int?, animated: Bool) {
let destinationValue: CGFloat = (index == nil) ? 0 : 1
if animated {
if index != nil {
selectedBarIndex = index
}
self.selectedIndexAnimator.completionClosure = {
self.selectedBarIndex = index
}
guard self.selectedIndexAnimator.end != destinationValue else { return }
self.selectedIndexAnimator.animate(to: destinationValue, duration: .defaultDuration)
} else {
self.selectedIndexAnimator.set(current: destinationValue)
self.selectedBarIndex = index
}
}
private var selectedBarIndex: Int? {
didSet {
setNeedsDisplay()
}
}
var generalUnselectedAlpha: CGFloat = 0.5
private var componentsAnimators: [AnimationController<CGFloat>] = []
var bars: BarsData = BarsData(barWidth: 1, locations: [], components: []) {
willSet {
if bars.components.count != newValue.components.count {
componentsAnimators = newValue.components.map { _ in AnimationController<CGFloat>(current: 1, refreshClosure: self.refreshClosure) }
}
}
didSet {
setNeedsDisplay()
}
}
func setComponentVisible(_ isVisible: Bool, at index: Int, animated: Bool) {
componentsAnimators[index].animate(to: isVisible ? 1 : 0, duration: animated ? .defaultDuration : 0)
}
private lazy var backgroundColorAnimator = AnimationController(current: UIColorContainer(color: .white), refreshClosure: refreshClosure)
func update(backgroundColor: UIColor, animated: Bool) {
if animated {
backgroundColorAnimator.animate(to: UIColorContainer(color: backgroundColor), duration: .defaultDuration)
} else {
backgroundColorAnimator.set(current: UIColorContainer(color: backgroundColor))
}
}
override func render(context: CGContext, bounds: CGRect, chartFrame: CGRect) {
guard isEnabled && verticalRange.current.distance > 0 && verticalRange.current.distance > 0 else { return }
let chartsAlpha = chartAlphaAnimator.current
if chartsAlpha == 0 { return }
let range = renderRange(bounds: bounds, chartFrame: chartFrame)
var selectedPaths: [[CGRect]] = bars.components.map { _ in [] }
var unselectedPaths: [[CGRect]] = bars.components.map { _ in [] }
if var barIndex = bars.locations.firstIndex(where: { $0 >= range.lowerBound }) {
if fillToTop {
barIndex = max(0, barIndex - 1)
while barIndex < bars.locations.count {
let currentLocation = bars.locations[barIndex]
let right = transform(toChartCoordinateHorizontal: currentLocation, chartFrame: chartFrame).roundedUpToPixelGrid()
let left = transform(toChartCoordinateHorizontal: currentLocation - bars.barWidth, chartFrame: chartFrame).roundedUpToPixelGrid()
var summ: CGFloat = 0
for (index, component) in bars.components.enumerated() {
summ += componentsAnimators[index].current * component.values[barIndex]
}
guard summ > 0 else {
barIndex += 1
continue
}
var stackedValue: CGFloat = 0
for (index, component) in bars.components.enumerated() {
let visibilityPercent = componentsAnimators[index].current
if visibilityPercent == 0 { continue }
let bottomFraction = stackedValue
let topFraction = stackedValue + ((component.values[barIndex] * visibilityPercent) / summ)
let rect = CGRect(x: left,
y: chartFrame.maxY - chartFrame.height * topFraction,
width: right - left,
height: chartFrame.height * (topFraction - bottomFraction))
if selectedBarIndex == barIndex {
selectedPaths[index].append(rect)
} else {
unselectedPaths[index].append(rect)
}
stackedValue = topFraction
}
if currentLocation > range.upperBound {
break
}
barIndex += 1
}
for (index, component) in bars.components.enumerated() {
context.saveGState()
context.setFillColor(component.color.withAlphaComponent(chartsAlpha * component.color.alphaValue).cgColor)
context.fill(selectedPaths[index])
let resultAlpha: CGFloat = 1.0 - (1.0 - generalUnselectedAlpha) * selectedIndexAnimator.current
context.setFillColor(component.color.withAlphaComponent(chartsAlpha * component.color.alphaValue * resultAlpha).cgColor)
context.fill(unselectedPaths[index])
context.restoreGState()
}
} else {
var selectedPaths: [[CGRect]] = bars.components.map { _ in [] }
barIndex = max(0, barIndex - 1)
var currentLocation = bars.locations[barIndex]
var leftX = transform(toChartCoordinateHorizontal: currentLocation - bars.barWidth, chartFrame: chartFrame)
var rightX: CGFloat = 0
let startPoint = CGPoint(x: leftX,
y: transform(toChartCoordinateVertical: verticalRange.current.lowerBound, chartFrame: chartFrame))
var backgourndPaths: [[CGPoint]] = bars.components.map { _ in Array() }
let itemsCount = ((bars.locations.count - barIndex) * 2) + 4
for path in backgourndPaths.indices {
backgourndPaths[path].reserveCapacity(itemsCount)
backgourndPaths[path].append(startPoint)
}
var maxValues: [CGFloat] = bars.components.map { _ in 0 }
while barIndex < bars.locations.count {
currentLocation = bars.locations[barIndex]
rightX = transform(toChartCoordinateHorizontal: currentLocation, chartFrame: chartFrame)
var stackedValue: CGFloat = 0
var bottomY: CGFloat = transform(toChartCoordinateVertical: stackedValue, chartFrame: chartFrame)
for (index, component) in bars.components.enumerated() {
let visibilityPercent = componentsAnimators[index].current
if visibilityPercent == 0 { continue }
let height = component.values[barIndex] * visibilityPercent
stackedValue += height
let topY = transform(toChartCoordinateVertical: stackedValue, chartFrame: chartFrame)
let componentHeight = (bottomY - topY)
maxValues[index] = max(maxValues[index], componentHeight)
if selectedBarIndex == barIndex {
let rect = CGRect(x: leftX,
y: topY,
width: rightX - leftX,
height: componentHeight)
selectedPaths[index].append(rect)
}
backgourndPaths[index].append(CGPoint(x: leftX, y: topY))
backgourndPaths[index].append(CGPoint(x: rightX, y: topY))
bottomY = topY
}
if currentLocation > range.upperBound {
break
}
leftX = rightX
barIndex += 1
}
let endPoint = CGPoint(x: transform(toChartCoordinateHorizontal: currentLocation, chartFrame: chartFrame).roundedUpToPixelGrid(),
y: transform(toChartCoordinateVertical: verticalRange.current.lowerBound, chartFrame: chartFrame))
let colorOffset = Double((1.0 - (1.0 - generalUnselectedAlpha) * selectedIndexAnimator.current) * chartsAlpha)
for (index, component) in bars.components.enumerated().reversed() {
if maxValues[index] < optimizationLevel {
continue
}
context.saveGState()
backgourndPaths[index].append(endPoint)
context.setFillColor(UIColor.valueBetween(start: backgroundColorAnimator.current.color,
end: component.color,
offset: colorOffset).cgColor)
context.beginPath()
context.addLines(between: backgourndPaths[index])
context.closePath()
context.fillPath()
context.restoreGState()
}
for (index, component) in bars.components.enumerated().reversed() {
context.setFillColor(component.color.withAlphaComponent(chartsAlpha * component.color.alphaValue).cgColor)
context.fill(selectedPaths[index])
}
}
}
}
}
extension BarChartRenderer.BarsData {
static func initialComponents(chartsCollection: ChartsCollection) ->
(width: CGFloat,
chartBars: BarChartRenderer.BarsData,
totalHorizontalRange: ClosedRange<CGFloat>,
totalVerticalRange: ClosedRange<CGFloat>) {
let width: CGFloat
if chartsCollection.axisValues.count > 1 {
width = CGFloat(abs(chartsCollection.axisValues[1].timeIntervalSince1970 - chartsCollection.axisValues[0].timeIntervalSince1970))
} else {
width = 1
}
let components = chartsCollection.chartValues.map { BarChartRenderer.BarsData.Component(color: $0.color,
values: $0.values.map { CGFloat($0) }) }
let chartBars = BarChartRenderer.BarsData(barWidth: width,
locations: chartsCollection.axisValues.map { CGFloat($0.timeIntervalSince1970) },
components: components)
let totalVerticalRange = BarChartRenderer.BarsData.verticalRange(bars: chartBars) ?? 0...1
let totalHorizontalRange = BarChartRenderer.BarsData.visibleHorizontalRange(bars: chartBars, width: width) ?? 0...1
return (width: width, chartBars: chartBars, totalHorizontalRange: totalHorizontalRange, totalVerticalRange: totalVerticalRange)
}
static func visibleHorizontalRange(bars: BarChartRenderer.BarsData, width: CGFloat) -> ClosedRange<CGFloat>? {
guard let firstPoint = bars.locations.first,
let lastPoint = bars.locations.last,
firstPoint <= lastPoint else {
return nil
}
return (firstPoint - width)...lastPoint
}
static func verticalRange(bars: BarChartRenderer.BarsData, calculatingRange: ClosedRange<CGFloat>? = nil, addBounds: Bool = false) -> ClosedRange<CGFloat>? {
guard bars.components.count > 0 else {
return nil
}
if let calculatingRange = calculatingRange {
guard var index = bars.locations.firstIndex(where: { $0 >= calculatingRange.lowerBound && $0 <= calculatingRange.upperBound }) else {
return nil
}
var vMax: CGFloat = bars.components[0].values[index]
while index < bars.locations.count {
var summ: CGFloat = 0
for component in bars.components {
summ += component.values[index]
}
vMax = max(vMax, summ)
if bars.locations[index] > calculatingRange.upperBound {
break
}
index += 1
}
return 0...vMax
} else {
var index = 0
var vMax: CGFloat = bars.components[0].values[index]
while index < bars.locations.count {
var summ: CGFloat = 0
for component in bars.components {
summ += component.values[index]
}
vMax = max(vMax, summ)
index += 1
}
return 0...vMax
}
}
}

View file

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

View file

@ -1,147 +0,0 @@
//
// ChartDetailsRenderer.swift
// GraphTest
//
// Created by Andrei Salavei on 4/13/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import UIKit
class ChartDetailsRenderer: BaseChartRenderer, ColorModeContainer {
private lazy var colorAnimator = AnimationController<CGFloat>(current: 1, refreshClosure: refreshClosure)
private var fromColorMode: ColorMode = .day
private var currentColorMode: ColorMode = .day
func apply(colorMode: ColorMode, animated: Bool) {
if currentColorMode != colorMode {
fromColorMode = currentColorMode
currentColorMode = colorMode
if animated {
colorAnimator.set(current: 0)
colorAnimator.animate(to: 1, duration: .defaultDuration)
} else {
colorAnimator.set(current: 1)
}
}
}
private var valuesAnimators: [AnimationController<CGFloat>] = []
func setValueVisible(_ isVisible: Bool, at index: Int, animated: Bool) {
valuesAnimators[index].animate(to: isVisible ? 1 : 0, duration: animated ? .defaultDuration : 0)
}
var detailsViewModel: ChartDetailsViewModel = .blank {
didSet {
if detailsViewModel.values.count != valuesAnimators.count {
valuesAnimators = detailsViewModel.values.map { _ in AnimationController<CGFloat>(current: 1, refreshClosure: refreshClosure) }
}
setNeedsDisplay()
}
}
var detailsViewPosition: CGFloat = 0 {
didSet {
setNeedsDisplay()
}
}
var detailViewPositionOffset: CGFloat = 10
var detailViewTopOffset: CGFloat = 10
private var iconWidth: CGFloat = 10
private var margins: CGFloat = 10
private let cornerRadius: CGFloat = 5
private var rowHeight: CGFloat = 20
private let titleFont = UIFont.systemFont(ofSize: 14, weight: .bold)
private let prefixFont = UIFont.systemFont(ofSize: 14, weight: .bold)
private let labelsFont = UIFont.systemFont(ofSize: 14, weight: .medium)
private let valuesFont = UIFont.systemFont(ofSize: 14, weight: .bold)
private let labelsColor: UIColor = .black
private(set) var previousRenderBannerFrame: CGRect = .zero
override func render(context: CGContext, bounds: CGRect, chartFrame: CGRect) {
previousRenderBannerFrame = .zero
guard isEnabled && verticalRange.current.distance > 0 && verticalRange.current.distance > 0 else { return }
let generalAlpha = chartAlphaAnimator.current
if generalAlpha == 0 { return }
let widths: [(prefix: CGFloat, label: CGFloat, value: CGFloat)] = detailsViewModel.values.map { value in
var prefixWidth: CGFloat = 0
if let prefixText = value.prefix {
prefixWidth = (prefixText as NSString).boundingRect(with: bounds.size,
options: .usesLineFragmentOrigin,
attributes: [.font: prefixFont],
context: nil).width.rounded(.up) + margins
}
let labelWidth = (value.title as NSString).boundingRect(with: bounds.size,
options: .usesLineFragmentOrigin,
attributes: [.font: labelsFont],
context: nil).width.rounded(.up) + margins
let valueWidth = (value.value as NSString).boundingRect(with: bounds.size,
options: .usesLineFragmentOrigin,
attributes: [.font: valuesFont],
context: nil).width.rounded(.up)
return (prefixWidth, labelWidth, valueWidth)
}
let titleWidth = (detailsViewModel.title as NSString).boundingRect(with: bounds.size,
options: .usesLineFragmentOrigin,
attributes: [.font: titleFont],
context: nil).width
let prefixesWidth = widths.map { $0.prefix }.max() ?? 0
let labelsWidth = widths.map { $0.label }.max() ?? 0
let valuesWidth = widths.map { $0.value }.max() ?? 0
let totalWidth: CGFloat = max(prefixesWidth + labelsWidth + valuesWidth, titleWidth + iconWidth) + margins * 2
let totalHeight: CGFloat = CGFloat(detailsViewModel.values.count + 1) * rowHeight + margins * 2
let backgroundColor = UIColor.valueBetween(start: fromColorMode.chartDetailsViewColor,
end: currentColorMode.chartDetailsViewColor,
offset: Double(colorAnimator.current))
let titleAndTextColor = UIColor.valueBetween(start: fromColorMode.chartDetailsTextColor,
end: currentColorMode.chartDetailsTextColor,
offset: Double(colorAnimator.current))
let detailsViewFrame: CGRect
if totalWidth + detailViewTopOffset > detailsViewPosition {
detailsViewFrame = CGRect(x: detailsViewPosition + detailViewTopOffset,
y: detailViewTopOffset + chartFrame.minY,
width: totalWidth,
height: totalHeight)
} else {
detailsViewFrame = CGRect(x: detailsViewPosition - totalWidth - detailViewTopOffset,
y: detailViewTopOffset + chartFrame.minY,
width: totalWidth,
height: totalHeight)
}
previousRenderBannerFrame = detailsViewFrame
context.saveGState()
context.setFillColor(backgroundColor.cgColor)
context.beginPath()
context.addPath(CGPath(roundedRect: detailsViewFrame, cornerWidth: 5, cornerHeight: 5, transform: nil))
context.fillPath()
context.endPage()
context.restoreGState()
var drawY = detailsViewFrame.minY + margins + (rowHeight - titleFont.pointSize) / 2
(detailsViewModel.title as NSString).draw(at: CGPoint(x: detailsViewFrame.minX + margins, y: drawY), withAttributes: [.font: titleFont,
.foregroundColor: titleAndTextColor])
drawY += rowHeight
for (index, row) in widths.enumerated() {
let value = detailsViewModel.values[index]
if let prefixText = value.prefix {
(prefixText as NSString).draw(at: CGPoint(x: detailsViewFrame.minX + prefixesWidth - row.prefix,
y: drawY),
withAttributes: [.font: prefixText, .foregroundColor: titleAndTextColor])
}
(value.title as NSString).draw(at: CGPoint(x: detailsViewFrame.minX + prefixesWidth + margins,
y: drawY),
withAttributes: [.font: labelsFont, .foregroundColor: titleAndTextColor])
(value.value as NSString).draw(at: CGPoint(x: detailsViewFrame.minX + prefixesWidth + labelsWidth + valuesWidth - row.value + margins,
y: drawY),
withAttributes: [.font: labelsFont, .foregroundColor: value.color])
drawY += rowHeight
}
}
}

View file

@ -1,99 +0,0 @@
//
// HorizontalScalesRenderer.swift
// GraphTest
//
// Created by Andrei Salavei on 4/8/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import UIKit
class HorizontalScalesRenderer: BaseChartRenderer {
private var horizontalLabels: [LinesChartLabel] = []
private var animatedHorizontalLabels: [AnimatedLinesChartLabels] = []
var labelsVerticalOffset: CGFloat = 8
var labelsFont: UIFont = .systemFont(ofSize: 11)
var labelsColor: UIColor = .gray
func setup(labels: [LinesChartLabel], animated: Bool) {
if animated {
var labelsToKeepVisible: [LinesChartLabel] = []
let labelsToHide: [LinesChartLabel]
var labelsToShow: [LinesChartLabel] = []
for label in labels {
if horizontalLabels.contains(label) {
labelsToKeepVisible.append(label)
} else {
labelsToShow.append(label)
}
}
labelsToHide = horizontalLabels.filter { !labels.contains($0) }
animatedHorizontalLabels.removeAll()
horizontalLabels = labelsToKeepVisible
let showAnimation = AnimatedLinesChartLabels(labels: labelsToShow, alphaAnimator: AnimationController(current: 1.0, refreshClosure: refreshClosure))
showAnimation.isAppearing = true
showAnimation.alphaAnimator.set(current: 0)
showAnimation.alphaAnimator.animate(to: 1, duration: .defaultDuration)
showAnimation.alphaAnimator.completionClosure = { [weak self, weak showAnimation] in
guard let self = self, let showAnimation = showAnimation else { return }
self.animatedHorizontalLabels.removeAll(where: { $0 === showAnimation })
self.horizontalLabels = labels
}
let hideAnimation = AnimatedLinesChartLabels(labels: labelsToHide, alphaAnimator: AnimationController(current: 1.0, refreshClosure: refreshClosure))
hideAnimation.isAppearing = false
hideAnimation.alphaAnimator.set(current: 1)
hideAnimation.alphaAnimator.animate(to: 0, duration: .defaultDuration)
hideAnimation.alphaAnimator.completionClosure = { [weak self, weak hideAnimation] in
guard let self = self, let hideAnimation = hideAnimation else { return }
self.animatedHorizontalLabels.removeAll(where: { $0 === hideAnimation })
}
animatedHorizontalLabels.append(showAnimation)
animatedHorizontalLabels.append(hideAnimation)
} else {
horizontalLabels = labels
animatedHorizontalLabels = []
}
}
override func render(context: CGContext, bounds: CGRect, chartFrame: CGRect) {
guard isEnabled && verticalRange.current.distance > 0 && verticalRange.current.distance > 0 else { return }
let itemsAlpha = chartAlphaAnimator.current
guard itemsAlpha > 0 else { return }
let range = renderRange(bounds: bounds, chartFrame: chartFrame)
func drawHorizontalLabels(_ labels: [LinesChartLabel], color: UIColor) {
let attributes: [NSAttributedString.Key : Any] = [.foregroundColor: color,
.font: labelsFont]
let y = chartFrame.origin.y + chartFrame.height + labelsVerticalOffset
if let start = labels.firstIndex(where: { $0.value > range.lowerBound }) {
for index in start..<labels.count {
let label = labels[index]
let x = transform(toChartCoordinateHorizontal: label.value, chartFrame: chartFrame)
let rect = (label.text as NSString).boundingRect(with: bounds.size,
options: .usesLineFragmentOrigin,
attributes: attributes,
context: nil)
(label.text as NSString).draw(at: CGPoint(x: x - rect.width, y: y), withAttributes: attributes)
if label.value > range.upperBound {
break
}
}
}
}
let labelColorAlpha = labelsColor.alphaValue * itemsAlpha
drawHorizontalLabels(horizontalLabels, color: labelsColor.withAlphaComponent(labelColorAlpha * itemsAlpha))
for animation in animatedHorizontalLabels {
let color = labelsColor.withAlphaComponent(animation.alphaAnimator.current * labelColorAlpha)
drawHorizontalLabels(animation.labels, color: color)
}
}
}

View file

@ -1,67 +0,0 @@
//
// LineBulletsRenerer.swift
// GraphTest
//
// Created by Andrei Salavei on 4/8/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import UIKit
class LineBulletsRenerer: BaseChartRenderer {
struct Bullet {
var coordinate: CGPoint
var color: UIColor
}
var bullets: [Bullet] = [] {
willSet {
if alphaAnimators.count != newValue.count {
alphaAnimators = newValue.map { _ in AnimationController<CGFloat>(current: 1.0, refreshClosure: refreshClosure) }
}
}
didSet {
setNeedsDisplay()
}
}
private var alphaAnimators: [AnimationController<CGFloat>] = []
private lazy var innerColorAnimator = AnimationController(current: UIColorContainer(color: .white), refreshClosure: refreshClosure)
public func setInnerColor(_ color: UIColor, animated: Bool) {
if animated {
innerColorAnimator.animate(to: UIColorContainer(color: color), duration: .defaultDuration)
} else {
innerColorAnimator.set(current: UIColorContainer(color: color))
}
}
var linesWidth: CGFloat = 2
var bulletRadius: CGFloat = 6
func setLineVisible(_ isVisible: Bool, at index: Int, animated: Bool) {
alphaAnimators[index].animate(to: isVisible ? 1 : 0, duration: animated ? .defaultDuration : 0)
}
override func render(context: CGContext, bounds: CGRect, chartFrame: CGRect) {
guard isEnabled && verticalRange.current.distance > 0 && verticalRange.current.distance > 0 else { return }
let generalAlpha = chartAlphaAnimator.current
if generalAlpha == 0 { return }
for (index, bullet) in bullets.enumerated() {
let alpha = alphaAnimators[index].current
if alpha == 0 { continue }
let centerX = transform(toChartCoordinateHorizontal: bullet.coordinate.x, chartFrame: chartFrame)
let centerY = transform(toChartCoordinateVertical: bullet.coordinate.y, chartFrame: chartFrame)
context.setFillColor(innerColorAnimator.current.color.withAlphaComponent(alpha).cgColor)
context.setStrokeColor(bullet.color.withAlphaComponent(alpha).cgColor)
context.setLineWidth(linesWidth)
let rect = CGRect(x: centerX - bulletRadius / 2,
y: centerY - bulletRadius / 2,
width: bulletRadius,
height: bulletRadius)
context.fillEllipse(in: rect)
context.strokeEllipse(in: rect)
}
}
}

View file

@ -1,538 +0,0 @@
//
// LinesChartRenderer.swift
// GraphTest
//
// Created by Andrei Salavei on 4/7/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import UIKit
class LinesChartRenderer: BaseChartRenderer {
struct LineData {
var color: UIColor
var points: [CGPoint]
}
private var linesAlphaAnimators: [AnimationController<CGFloat>] = []
var lineWidth: CGFloat = 1 {
didSet {
setNeedsDisplay()
}
}
private lazy var linesShapeAnimator = AnimationController<Double>(current: 1, refreshClosure: self.refreshClosure)
private var fromLines: [LineData] = []
private var toLines: [LineData] = []
func setLines(lines: [LineData], animated: Bool) {
if toLines.count != lines.count {
linesAlphaAnimators = lines.map { _ in AnimationController<CGFloat>(current: 1, refreshClosure: self.refreshClosure) }
}
if animated {
self.fromLines = self.toLines
self.toLines = lines
linesShapeAnimator.set(current: 1.0 - linesShapeAnimator.current)
linesShapeAnimator.completionClosure = {
self.fromLines = []
}
linesShapeAnimator.animate(to: 1, duration: .defaultDuration)
} else {
self.fromLines = []
self.toLines = lines
linesShapeAnimator.set(current: 1)
}
}
func setLineVisible(_ isVisible: Bool, at index: Int, animated: Bool) {
linesAlphaAnimators[index].animate(to: isVisible ? 1 : 0, duration: animated ? .defaultDuration : 0)
}
override func render(context: CGContext, bounds: CGRect, chartFrame: CGRect) {
guard isEnabled && verticalRange.current.distance > 0 && verticalRange.current.distance > 0 else { return }
let chartsAlpha = chartAlphaAnimator.current
if chartsAlpha == 0 { return }
let range = renderRange(bounds: bounds, chartFrame: chartFrame)
for (index, toLine) in toLines.enumerated() {
let alpha = linesAlphaAnimators[index].current * chartsAlpha
if alpha == 0 { continue }
context.setStrokeColor(toLine.color.withAlphaComponent(alpha).cgColor)
context.setLineWidth(lineWidth)
if linesShapeAnimator.isAnimating {
let animationOffset = linesShapeAnimator.current
let path = CGMutablePath()
let fromPoints = fromLines.safeElement(at: index)?.points ?? []
let toPoints = toLines.safeElement(at: index)?.points ?? []
var fromIndex: Int? = fromPoints.firstIndex(where: { $0.x >= range.lowerBound })
var toIndex: Int? = toPoints.firstIndex(where: { $0.x >= range.lowerBound })
let fromRange = verticalRange.start
let currentRange = verticalRange.current
let toRange = verticalRange.end
func convertFromPoint(_ fromPoint: CGPoint) -> CGPoint {
return CGPoint(x: fromPoint.x,
y: (fromPoint.y - fromRange.lowerBound) / fromRange.distance * currentRange.distance + currentRange.lowerBound)
}
func convertToPoint(_ toPoint: CGPoint) -> CGPoint {
return CGPoint(x: toPoint.x,
y: (toPoint.y - toRange.lowerBound) / toRange.distance * currentRange.distance + currentRange.lowerBound)
}
var previousFromPoint: CGPoint
var previousToPoint: CGPoint
let startFromPoint: CGPoint?
let startToPoint: CGPoint?
if let validFrom = fromIndex {
previousFromPoint = convertFromPoint(fromPoints[max(0, validFrom - 1)])
startFromPoint = previousFromPoint
} else {
previousFromPoint = .zero
startFromPoint = nil
}
if let validTo = toIndex {
previousToPoint = convertToPoint(toPoints[max(0, validTo - 1)])
startToPoint = previousToPoint
} else {
previousToPoint = .zero
startToPoint = nil
}
var combinedPoints: [CGPoint] = []
func add(pointToDraw: CGPoint) {
if let startFromPoint = startFromPoint,
pointToDraw.x < startFromPoint.x {
let animatedPoint = CGPoint(x: pointToDraw.x,
y: CGFloat.valueBetween(start: startFromPoint.y, end: pointToDraw.y, offset: animationOffset))
combinedPoints.append(transform(toChartCoordinate: animatedPoint, chartFrame: chartFrame))
} else if let startToPoint = startToPoint,
pointToDraw.x < startToPoint.x {
let animatedPoint = CGPoint(x: pointToDraw.x,
y: CGFloat.valueBetween(start: startToPoint.y, end: pointToDraw.y, offset: 1 - animationOffset))
combinedPoints.append(transform(toChartCoordinate: animatedPoint, chartFrame: chartFrame))
} else {
combinedPoints.append(transform(toChartCoordinate: pointToDraw, chartFrame: chartFrame))
}
}
if previousToPoint != .zero && previousFromPoint != .zero {
add(pointToDraw: (previousToPoint.x < previousFromPoint.x ? previousToPoint : previousFromPoint))
} else if previousToPoint != .zero {
add(pointToDraw: previousToPoint)
} else if previousFromPoint != .zero {
add(pointToDraw: previousFromPoint)
}
while let validFromIndex = fromIndex,
let validToIndex = toIndex,
validFromIndex < fromPoints.count,
validToIndex < toPoints.count {
let currentFromPoint = convertFromPoint(fromPoints[validFromIndex])
let currentToPoint = convertToPoint(toPoints[validToIndex])
let pointToAdd: CGPoint
if currentFromPoint.x == currentToPoint.x {
pointToAdd = CGPoint.valueBetween(start: currentFromPoint, end: currentToPoint, offset: animationOffset)
previousFromPoint = currentFromPoint
previousToPoint = currentToPoint
fromIndex = validFromIndex + 1
toIndex = validToIndex + 1
} else if currentFromPoint.x < currentToPoint.x {
if previousToPoint.x < currentFromPoint.x {
let offset = Double((currentFromPoint.x - previousToPoint.x) / (currentToPoint.x - previousToPoint.x))
let intermidiateToPoint = CGPoint.valueBetween(start: previousToPoint, end: currentToPoint, offset: offset)
pointToAdd = CGPoint.valueBetween(start: currentFromPoint, end: intermidiateToPoint, offset: animationOffset)
} else {
pointToAdd = currentFromPoint
}
previousFromPoint = currentFromPoint
fromIndex = validFromIndex + 1
} else {
if previousFromPoint.x < currentToPoint.x {
let offset = Double((currentToPoint.x - previousFromPoint.x) / (currentFromPoint.x - previousFromPoint.x))
let intermidiateFromPoint = CGPoint.valueBetween(start: previousFromPoint, end: currentFromPoint, offset: offset)
pointToAdd = CGPoint.valueBetween(start: intermidiateFromPoint, end: currentToPoint, offset: animationOffset)
} else {
pointToAdd = currentToPoint
}
previousToPoint = currentToPoint
toIndex = validToIndex + 1
}
add(pointToDraw: pointToAdd)
if (pointToAdd.x > range.upperBound) {
break
}
}
while let validToIndex = toIndex, validToIndex < toPoints.count {
var pointToAdd = convertToPoint(toPoints[validToIndex])
pointToAdd.y = CGFloat.valueBetween(start: previousFromPoint.y,
end: pointToAdd.y,
offset: animationOffset)
add(pointToDraw: pointToAdd)
if (pointToAdd.x > range.upperBound) {
break
}
toIndex = validToIndex + 1
}
while let validFromIndex = fromIndex, validFromIndex < fromPoints.count {
var pointToAdd = convertFromPoint(fromPoints[validFromIndex])
pointToAdd.y = CGFloat.valueBetween(start: previousToPoint.y,
end: pointToAdd.y,
offset: 1 - animationOffset)
add(pointToDraw: pointToAdd)
if (pointToAdd.x > range.upperBound) {
break
}
fromIndex = validFromIndex + 1
}
var index = 0
var lines: [CGPoint] = []
var currentChartPoint = combinedPoints[index]
lines.append(currentChartPoint)
var chartPoints = [currentChartPoint]
var minIndex = 0
var maxIndex = 0
index += 1
while index < combinedPoints.count {
currentChartPoint = combinedPoints[index]
if currentChartPoint.x - chartPoints[0].x < lineWidth * optimizationLevel {
chartPoints.append(currentChartPoint)
if currentChartPoint.y > chartPoints[maxIndex].y {
maxIndex = chartPoints.count - 1
}
if currentChartPoint.y < chartPoints[minIndex].y {
minIndex = chartPoints.count - 1
}
index += 1
} else {
if chartPoints.count == 1 {
lines.append(currentChartPoint)
lines.append(currentChartPoint)
chartPoints[0] = currentChartPoint
index += 1
minIndex = 0
maxIndex = 0
} else {
if minIndex < maxIndex {
if minIndex != 0 {
lines.append(chartPoints[minIndex])
lines.append(chartPoints[minIndex])
}
lines.append(chartPoints[maxIndex])
lines.append(chartPoints[maxIndex])
if maxIndex != chartPoints.count - 1 {
chartPoints = [chartPoints[maxIndex], chartPoints.last!]
} else {
chartPoints = [chartPoints[maxIndex]]
}
} else {
if maxIndex != 0 {
lines.append(chartPoints[maxIndex])
lines.append(chartPoints[maxIndex])
}
lines.append(chartPoints[minIndex])
lines.append(chartPoints[minIndex])
if minIndex != chartPoints.count - 1 {
chartPoints = [chartPoints[minIndex], chartPoints.last!]
} else {
chartPoints = [chartPoints[minIndex]]
}
}
if chartPoints.count == 2 {
if chartPoints[0].y < chartPoints[1].y {
minIndex = 0
maxIndex = 1
} else {
minIndex = 1
maxIndex = 0
}
} else {
minIndex = 0
maxIndex = 0
}
}
}
}
if chartPoints.count == 1 {
lines.append(currentChartPoint)
lines.append(currentChartPoint)
} else {
if minIndex < maxIndex {
if minIndex != 0 {
lines.append(chartPoints[minIndex])
lines.append(chartPoints[minIndex])
}
lines.append(chartPoints[maxIndex])
lines.append(chartPoints[maxIndex])
if maxIndex != chartPoints.count - 1 {
lines.append(chartPoints.last!)
lines.append(chartPoints.last!)
}
} else {
if maxIndex != 0 {
lines.append(chartPoints[maxIndex])
lines.append(chartPoints[maxIndex])
}
lines.append(chartPoints[minIndex])
lines.append(chartPoints[minIndex])
if minIndex != chartPoints.count - 1 {
lines.append(chartPoints.last!)
lines.append(chartPoints.last!)
}
}
}
if (lines.count % 2) == 1 {
lines.removeLast()
}
context.setLineCap(.round)
context.strokeLineSegments(between: lines)
} else {
let alpha = linesAlphaAnimators[index].current * chartsAlpha
if alpha == 0 { continue }
context.setStrokeColor(toLine.color.withAlphaComponent(alpha).cgColor)
context.setLineWidth(lineWidth)
if var index = toLine.points.firstIndex(where: { $0.x >= range.lowerBound }) {
var lines: [CGPoint] = []
index = max(0, index - 1)
var currentPoint = toLine.points[index]
var currentChartPoint = transform(toChartCoordinate: currentPoint, chartFrame: chartFrame)
lines.append(currentChartPoint)
//context.move(to: currentChartPoint)
var chartPoints = [currentChartPoint]
var minIndex = 0
var maxIndex = 0
index += 1
while index < toLine.points.count {
currentPoint = toLine.points[index]
currentChartPoint = transform(toChartCoordinate: currentPoint, chartFrame: chartFrame)
if currentChartPoint.x - chartPoints[0].x < lineWidth * optimizationLevel {
chartPoints.append(currentChartPoint)
if currentChartPoint.y > chartPoints[maxIndex].y {
maxIndex = chartPoints.count - 1
}
if currentChartPoint.y < chartPoints[minIndex].y {
minIndex = chartPoints.count - 1
}
index += 1
} else {
if chartPoints.count == 1 {
lines.append(currentChartPoint)
lines.append(currentChartPoint)
chartPoints[0] = currentChartPoint
index += 1
minIndex = 0
maxIndex = 0
} else {
if minIndex < maxIndex {
if minIndex != 0 {
lines.append(chartPoints[minIndex])
lines.append(chartPoints[minIndex])
}
lines.append(chartPoints[maxIndex])
lines.append(chartPoints[maxIndex])
if maxIndex != chartPoints.count - 1 {
chartPoints = [chartPoints[maxIndex], chartPoints.last!]
} else {
chartPoints = [chartPoints[maxIndex]]
}
} else {
if maxIndex != 0 {
lines.append(chartPoints[maxIndex])
lines.append(chartPoints[maxIndex])
}
lines.append(chartPoints[minIndex])
lines.append(chartPoints[minIndex])
if minIndex != chartPoints.count - 1 {
chartPoints = [chartPoints[minIndex], chartPoints.last!]
} else {
chartPoints = [chartPoints[minIndex]]
}
}
if chartPoints.count == 2 {
if chartPoints[0].y < chartPoints[1].y {
minIndex = 0
maxIndex = 1
} else {
minIndex = 1
maxIndex = 0
}
} else {
minIndex = 0
maxIndex = 0
}
}
}
if currentPoint.x > range.upperBound {
break
}
}
if chartPoints.count == 1 {
lines.append(currentChartPoint)
lines.append(currentChartPoint)
} else {
if minIndex < maxIndex {
if minIndex != 0 {
lines.append(chartPoints[minIndex])
lines.append(chartPoints[minIndex])
}
lines.append(chartPoints[maxIndex])
lines.append(chartPoints[maxIndex])
if maxIndex != chartPoints.count - 1 {
lines.append(chartPoints.last!)
lines.append(chartPoints.last!)
}
} else {
if maxIndex != 0 {
lines.append(chartPoints[maxIndex])
lines.append(chartPoints[maxIndex])
}
lines.append(chartPoints[minIndex])
lines.append(chartPoints[minIndex])
if minIndex != chartPoints.count - 1 {
lines.append(chartPoints.last!)
lines.append(chartPoints.last!)
}
}
}
if (lines.count % 2) == 1 {
lines.removeLast()
}
context.setLineCap(.round)
context.strokeLineSegments(between: lines)
}
// if var start = toLine.points.firstIndex(where: { $0.x > range.lowerBound }) {
// let alpha = linesAlphaAnimators[index].current * chartsAlpha
// if alpha == 0 { continue }
// context.setStrokeColor(toLine.color.withAlphaComponent(alpha).cgColor)
// context.setLineWidth(lineWidth)
//
// context.setLineCap(.round)
// start = max(0, start - 1)
// let startPoint = toLine.points[start]
// var lines: [CGPoint] = []
// var pointToDraw = CGPoint(x: transform(toChartCoordinateHorizontal: startPoint.x, chartFrame: chartFrame),
// y: transform(toChartCoordinateVertical: startPoint.y, chartFrame: chartFrame))
// for index in (start + 1)..<toLine.points.count {
// lines.append(pointToDraw)
// let point = toLine.points[index]
// pointToDraw = CGPoint(x: transform(toChartCoordinateHorizontal: point.x, chartFrame: chartFrame),
// y: transform(toChartCoordinateVertical: point.y, chartFrame: chartFrame))
// lines.append(pointToDraw)
// if point.x > range.upperBound {
// break
// }
// }
//
// context.strokeLineSegments(between: lines)
// }
}
}
}
}
extension LinesChartRenderer.LineData {
static func initialComponents(chartsCollection: ChartsCollection) -> (linesData: [LinesChartRenderer.LineData],
totalHorizontalRange: ClosedRange<CGFloat>,
totalVerticalRange: ClosedRange<CGFloat>) {
let lines: [LinesChartRenderer.LineData] = 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)
}
let horizontalRange = LinesChartRenderer.LineData.horizontalRange(lines: lines) ?? BaseConstants.defaultRange
let verticalRange = LinesChartRenderer.LineData.verticalRange(lines: lines) ?? BaseConstants.defaultRange
return (linesData: lines, totalHorizontalRange: horizontalRange, totalVerticalRange: verticalRange)
}
static func horizontalRange(lines: [LinesChartRenderer.LineData]) -> ClosedRange<CGFloat>? {
guard let firstPoint = lines.first?.points.first else { return nil }
var hMin: CGFloat = firstPoint.x
var hMax: CGFloat = firstPoint.x
for line in lines {
if let first = line.points.first,
let last = line.points.last {
hMin = min(hMin, first.x)
hMax = max(hMax, last.x)
}
}
return hMin...hMax
}
static func verticalRange(lines: [LinesChartRenderer.LineData], calculatingRange: ClosedRange<CGFloat>? = nil, addBounds: Bool = false) -> ClosedRange<CGFloat>? {
if let calculatingRange = calculatingRange {
guard let initalStart = lines.first?.points.first(where: { $0.x >= calculatingRange.lowerBound &&
$0.x <= calculatingRange.upperBound }) else { return nil }
var vMin: CGFloat = initalStart.y
var vMax: CGFloat = initalStart.y
for line in lines {
if var index = line.points.firstIndex(where: { $0.x > calculatingRange.lowerBound }) {
if addBounds {
index = max(0, index - 1)
}
while index < line.points.count {
let point = line.points[index]
if point.x < calculatingRange.upperBound {
vMin = min(vMin, point.y)
vMax = max(vMax, point.y)
} else if addBounds {
vMin = min(vMin, point.y)
vMax = max(vMax, point.y)
break
} else {
break
}
index += 1
}
}
}
return vMin...vMax
} else {
guard let firstPoint = lines.first?.points.first else { return nil }
var vMin: CGFloat = firstPoint.y
var vMax: CGFloat = firstPoint.y
for line in lines {
for point in line.points {
vMin = min(vMin, point.y)
vMax = max(vMax, point.y)
}
}
return vMin...vMax
}
}
}

View file

@ -1,132 +0,0 @@
//
// PecentChartRenderer.swift
// GraphTest
//
// Created by Andrei Salavei on 4/7/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import UIKit
class PecentChartRenderer: BaseChartRenderer {
struct PercentageData {
static let blank = PecentChartRenderer.PercentageData(locations: [], components: [])
var locations: [CGFloat]
var components: [Component]
struct Component {
var color: UIColor
var values: [CGFloat]
}
}
override func setup(verticalRange: ClosedRange<CGFloat>, animated: Bool, timeFunction: TimeFunction? = nil) {
super.setup(verticalRange: 0...1, animated: animated, timeFunction: timeFunction)
}
private var componentsAnimators: [AnimationController<CGFloat>] = []
var percentageData: PercentageData = PercentageData(locations: [], components: []) {
willSet {
if percentageData.components.count != newValue.components.count {
componentsAnimators = newValue.components.map { _ in AnimationController<CGFloat>(current: 1, refreshClosure: self.refreshClosure) }
}
}
didSet {
setNeedsDisplay()
}
}
func setComponentVisible(_ isVisible: Bool, at index: Int, animated: Bool) {
componentsAnimators[index].animate(to: isVisible ? 1 : 0, duration: animated ? .defaultDuration : 0)
}
override func render(context: CGContext, bounds: CGRect, chartFrame: CGRect) {
guard isEnabled && verticalRange.current.distance > 0 && verticalRange.current.distance > 0 else { return }
let alpha = chartAlphaAnimator.current
guard alpha > 0 else { return }
let range = renderRange(bounds: bounds, chartFrame: chartFrame)
var paths: [CGMutablePath] = percentageData.components.map { _ in CGMutablePath() }
var vertices: [CGFloat] = Array<CGFloat>(repeating: 0, count: percentageData.components.count)
if var locationIndex = percentageData.locations.firstIndex(where: { $0 > range.lowerBound }) {
locationIndex = max(0, locationIndex - 1)
var currentLocation = transform(toChartCoordinateHorizontal: percentageData.locations[locationIndex], chartFrame: chartFrame)
let startPoint = CGPoint(x: currentLocation,
y: transform(toChartCoordinateVertical: verticalRange.current.lowerBound, chartFrame: chartFrame))
for path in paths {
path.move(to: startPoint)
}
paths.last?.addLine(to: CGPoint(x: currentLocation,
y: transform(toChartCoordinateVertical: verticalRange.current.upperBound, chartFrame: chartFrame)))
while locationIndex < percentageData.locations.count {
currentLocation = transform(toChartCoordinateHorizontal: percentageData.locations[locationIndex], chartFrame: chartFrame)
var summ: CGFloat = 0
for (index, component) in percentageData.components.enumerated() {
let visibilityPercent = componentsAnimators[index].current
let value = component.values[locationIndex] * visibilityPercent
if index == 0 {
vertices[index] = value
} else {
vertices[index] = value + vertices[index - 1]
}
summ += value
}
if summ > 0 {
for (index, value) in vertices.dropLast().enumerated() {
paths[index].addLine(to: CGPoint(x: currentLocation,
y: transform(toChartCoordinateVertical: value / summ, chartFrame: chartFrame)))
}
}
if currentLocation > range.upperBound {
break
}
locationIndex += 1
}
paths.last?.addLine(to: CGPoint(x: currentLocation,
y: transform(toChartCoordinateVertical: verticalRange.current.upperBound, chartFrame: chartFrame)))
let endPoint = CGPoint(x: currentLocation,
y: transform(toChartCoordinateVertical: verticalRange.current.lowerBound, chartFrame: chartFrame))
for (index, path) in paths.enumerated().reversed() {
let visibilityPercent = componentsAnimators[index].current
if visibilityPercent == 0 { continue }
path.addLine(to: endPoint)
path.closeSubpath()
context.saveGState()
context.beginPath()
context.addPath(path)
context.setFillColor(percentageData.components[index].color.cgColor)
context.fillPath()
context.restoreGState()
}
}
}
}
extension PecentChartRenderer.PercentageData {
static func horizontalRange(data: PecentChartRenderer.PercentageData) -> ClosedRange<CGFloat>? {
guard let firstPoint = data.locations.first,
let lastPoint = data.locations.last,
firstPoint <= lastPoint else {
return nil
}
return firstPoint...lastPoint
}
}

View file

@ -1,202 +0,0 @@
//
// PercentPieAnimationRenderer.swift
// GraphTest
//
// Created by Andrei Salavei on 4/13/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import UIKit
class PercentPieAnimationRenderer: BaseChartRenderer {
override func setup(verticalRange: ClosedRange<CGFloat>, animated: Bool, timeFunction: TimeFunction? = nil) {
super.setup(verticalRange: 0...1, animated: animated, timeFunction: timeFunction)
}
private lazy var transitionAnimator = AnimationController<CGFloat>(current: 0, refreshClosure: refreshClosure)
private var animationComponentsPoints: [[CGPoint]] = []
var visiblePercentageData: PecentChartRenderer.PercentageData = .blank {
didSet {
animationComponentsPoints = []
}
}
var visiblePieComponents: [PieChartRenderer.PieComponent] = []
func animate(fromDataToPie: Bool, animated: Bool, completion: @escaping () -> Void) {
assert(visiblePercentageData.components.count == visiblePieComponents.count)
isEnabled = true
transitionAnimator.completionClosure = { [weak self] in
self?.isEnabled = false
completion()
}
transitionAnimator.animate(to: fromDataToPie ? 1 : 0, duration: animated ? .defaultDuration : 0)
}
private func generateAnimationComponentPoints(bounds: CGRect, chartFrame: CGRect) {
let range = renderRange(bounds: bounds, chartFrame: chartFrame)
let componentsCount = visiblePercentageData.components.count
guard componentsCount > 0 else { return }
animationComponentsPoints = visiblePercentageData.components.map { _ in [] }
var vertices: [CGFloat] = Array<CGFloat>(repeating: 0, count: visiblePercentageData.components.count)
if var locationIndex = visiblePercentageData.locations.firstIndex(where: { $0 > range.lowerBound }) {
locationIndex = max(0, locationIndex - 1)
var currentLocation = transform(toChartCoordinateHorizontal: visiblePercentageData.locations[locationIndex], chartFrame: chartFrame)
let startPoint = CGPoint(x: currentLocation, y: transform(toChartCoordinateVertical: verticalRange.current.lowerBound, chartFrame: chartFrame))
for index in 0..<componentsCount {
animationComponentsPoints[index].append(startPoint)
}
animationComponentsPoints[componentsCount - 1].append(CGPoint(x: currentLocation, y: transform(toChartCoordinateVertical: verticalRange.current.upperBound, chartFrame: chartFrame)))
while locationIndex < visiblePercentageData.locations.count {
currentLocation = transform(toChartCoordinateHorizontal: visiblePercentageData.locations[locationIndex], chartFrame: chartFrame)
var summ: CGFloat = 0
for (index, component) in visiblePercentageData.components.enumerated() {
let value = component.values[locationIndex]
if index == 0 {
vertices[index] = value
} else {
vertices[index] = value + vertices[index - 1]
}
summ += value
}
for (index, value) in vertices.dropLast().enumerated() {
animationComponentsPoints[index].append(CGPoint(x: currentLocation, y: transform(toChartCoordinateVertical: value / summ, chartFrame: chartFrame)))
}
if visiblePercentageData.locations[locationIndex] > range.upperBound {
break
}
locationIndex += 1
}
animationComponentsPoints[componentsCount - 1].append(CGPoint(x: currentLocation, y: transform(toChartCoordinateVertical: verticalRange.current.upperBound, chartFrame: chartFrame)))
let endPoint = CGPoint(x: currentLocation, y: transform(toChartCoordinateVertical: verticalRange.current.lowerBound, chartFrame: chartFrame))
for index in 0..<componentsCount {
animationComponentsPoints[index].append(endPoint)
}
}
}
private var initialPieAngle: CGFloat = .pi / 3
var backgroundColor: UIColor = .white
override func render(context: CGContext, bounds: CGRect, chartFrame: CGRect) {
guard isEnabled && verticalRange.current.distance > 0 && verticalRange.current.distance > 0 else { return }
self.optimizationLevel = 1
if animationComponentsPoints.isEmpty {
generateAnimationComponentPoints(bounds: bounds, chartFrame: chartFrame)
}
let numberOfComponents = animationComponentsPoints.count
guard numberOfComponents > 0 else { return }
let destinationRadius = max(chartFrame.width, chartFrame.height)
let animationFraction = transitionAnimator.current
let animationFractionD = Double(transitionAnimator.current)
let easeInAnimationFractionD = animationFractionD * animationFractionD * animationFractionD * animationFractionD
let center = CGPoint(x: chartFrame.midX, y: chartFrame.midY)
let totalPieSumm: CGFloat = visiblePieComponents.map { $0.value } .reduce(0, +)
let pathsToDraw: [CGMutablePath] = (0..<numberOfComponents).map { _ in CGMutablePath() }
var startAngle: CGFloat = initialPieAngle
for componentIndex in 0..<(numberOfComponents - 1) {
let componentPoints = animationComponentsPoints[componentIndex]
guard componentPoints.count > 4 else {
return
}
let percent = visiblePieComponents[componentIndex].value / totalPieSumm
let segmentSize = 2 * .pi * percent
let endAngle = startAngle + segmentSize
let centerAngle = (startAngle + endAngle) / 2
let lineCenterPoint = CGPoint.valueBetween(start: componentPoints[componentPoints.count / 2],
end: center,
offset: animationFractionD)
let startDestinationPoint = lineCenterPoint + CGPoint(x: destinationRadius, y: 0)
let centerDestinationPoint = lineCenterPoint + CGPoint(x: 0, y: destinationRadius)
let endDestinationPoint = lineCenterPoint + CGPoint(x: -destinationRadius, y: 0)
let initialStartDestinationAngle: CGFloat = 0
let initialCenterDestinationAngle: CGFloat = .pi / 2
let initialEndDestinationAngle: CGFloat = .pi
var previousAddedPoint = (componentPoints[0] * 2 - center)
.rotate(origin: lineCenterPoint, angle: CGFloat.valueBetween(start: 0, end: centerAngle - initialCenterDestinationAngle, offset: animationFractionD))
pathsToDraw[componentIndex].move(to: previousAddedPoint)
func addPointToPath(_ point: CGPoint) {
if (point - previousAddedPoint).lengthSquared() > optimizationLevel {
pathsToDraw[componentIndex].addLine(to: point)
previousAddedPoint = point
}
}
for endPointIndex in 1..<(componentPoints.count / 2) {
addPointToPath(CGPoint.valueBetween(start: componentPoints[endPointIndex], end: endDestinationPoint, offset: easeInAnimationFractionD)
.rotate(origin: lineCenterPoint, angle: CGFloat.valueBetween(start: 0, end: endAngle - initialEndDestinationAngle, offset: animationFractionD)))
}
addPointToPath(lineCenterPoint)
for startPointIndex in (componentPoints.count / 2 + 1)..<(componentPoints.count - 1) {
addPointToPath(CGPoint.valueBetween(start: componentPoints[startPointIndex], end: startDestinationPoint, offset: easeInAnimationFractionD)
.rotate(origin: lineCenterPoint, angle: CGFloat.valueBetween(start: 0, end: startAngle - initialStartDestinationAngle, offset: animationFractionD)))
}
if let lastPoint = componentPoints.last {
addPointToPath((lastPoint * 2 - center)
.rotate(origin: lineCenterPoint, angle: CGFloat.valueBetween(start: 0, end: centerAngle - initialCenterDestinationAngle, offset: animationFractionD)))
}
startAngle = endAngle
}
if let lastPath = animationComponentsPoints.last {
pathsToDraw.last?.addLines(between: lastPath)
}
for (index, path) in pathsToDraw.enumerated().reversed() {
path.closeSubpath()
context.saveGState()
context.beginPath()
context.addPath(path)
context.setFillColor(visiblePieComponents[index].color.cgColor)
context.fillPath()
context.restoreGState()
}
let diagramRadius = (min(chartFrame.width, chartFrame.height) / 2) * 0.925
let targetFrame = CGRect(origin: CGPoint(x: center.x - diagramRadius,
y: center.y - diagramRadius),
size: CGSize(width: diagramRadius * 2,
height: diagramRadius * 2))
let minX = animationComponentsPoints.last?.first?.x ?? 0
let maxX = animationComponentsPoints.last?.last?.x ?? 0
let startFrame = CGRect(x: minX,
y: chartFrame.minY,
width: maxX - minX,
height: chartFrame.height)
let cornerRadius = diagramRadius * animationFraction
let fadeOutFrame = CGRect.valueBetween(start: startFrame, end: targetFrame, offset: animationFractionD)
let fadeOutPath = CGMutablePath()
fadeOutPath.addRect(bounds)
fadeOutPath.addPath(CGPath(roundedRect: fadeOutFrame, cornerWidth: cornerRadius, cornerHeight: cornerRadius, transform: nil))
context.saveGState()
context.beginPath()
context.addPath(fadeOutPath)
context.setFillColor(backgroundColor.cgColor)
context.fillPath(using: .evenOdd)
context.restoreGState()
}
}

View file

@ -1,31 +0,0 @@
//
// PerformanceRenderer.swift
// GraphTest
//
// Created by Andrei Salavei on 4/10/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import UIKit
class PerformanceRenderer: ChartViewRenderer {
var containerViews: [UIView] = []
private var previousTickTime: TimeInterval = CACurrentMediaTime()
func render(context: CGContext, bounds: CGRect, chartFrame: CGRect) {
let currentTime = CACurrentMediaTime()
let delta = currentTime - previousTickTime
previousTickTime = currentTime
let normalDelta = 0.017
let redDelta = 0.05
if delta > normalDelta || delta < 0.75 {
let green = CGFloat( 1.0 - crop(0, (delta - normalDelta) / (redDelta - normalDelta), 1))
let color = UIColor(red: 1.0, green: green, blue: 0, alpha: 1)
context.setFillColor(color.cgColor)
context.fill(CGRect(x: 0, y: 0, width: bounds.width, height: 3))
}
}
}

View file

@ -1,191 +0,0 @@
//
// PieChartRenderer.swift
// GraphTest
//
// Created by Andrei Salavei on 4/11/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import UIKit
class PieChartRenderer: BaseChartRenderer {
struct PieComponent: Hashable {
var color: UIColor
var value: CGFloat
}
override func setup(verticalRange: ClosedRange<CGFloat>, animated: Bool, timeFunction: TimeFunction? = nil) {
super.setup(verticalRange: 0...1, animated: animated, timeFunction: timeFunction)
}
var valuesFormatter: NumberFormatter = NumberFormatter()
var drawValues: Bool = true
private var componentsAnimators: [AnimationController<CGFloat>] = []
private lazy var transitionAnimator: AnimationController<CGFloat> = { AnimationController<CGFloat>(current: 1, refreshClosure: self.refreshClosure) }()
private var oldPercentageData: [PieComponent] = []
private var percentageData: [PieComponent] = []
private var setlectedSegmentsAnimators: [AnimationController<CGFloat>] = []
var drawPie: Bool = true
var initialAngle: CGFloat = .pi / 3
var hasSelectedSegments: Bool {
return selectedSegment != nil
}
private(set) var selectedSegment: Int?
func selectSegmentAt(at indexToSelect: Int?, animated: Bool) {
selectedSegment = indexToSelect
for (index, animator) in setlectedSegmentsAnimators.enumerated() {
let fraction: CGFloat = (index == indexToSelect) ? 1.0 : 0.0
if animated {
animator.animate(to: fraction, duration: .defaultDuration / 2)
} else {
animator.set(current: fraction)
}
}
}
func updatePercentageData(_ percentageData: [PieComponent], animated: Bool) {
if self.percentageData.count != percentageData.count {
componentsAnimators = percentageData.map { _ in AnimationController<CGFloat>(current: 1, refreshClosure: self.refreshClosure) }
setlectedSegmentsAnimators = percentageData.map { _ in AnimationController<CGFloat>(current: 0, refreshClosure: self.refreshClosure) }
}
if animated {
self.oldPercentageData = self.currentTransitionAnimationData
self.percentageData = percentageData
transitionAnimator.completionClosure = { [weak self] in
self?.oldPercentageData = []
}
transitionAnimator.set(current: 0)
transitionAnimator.animate(to: 1, duration: .defaultDuration)
} else {
self.oldPercentageData = []
self.percentageData = percentageData
transitionAnimator.set(current: 0)
}
}
func setComponentVisible(_ isVisible: Bool, at index: Int, animated: Bool) {
componentsAnimators[index].animate(to: isVisible ? 1 : 0, duration: animated ? .defaultDuration : 0)
}
var lastRenderedBounds: CGRect = .zero
var lastRenderedChartFrame: CGRect = .zero
func selectedItemIndex(at point: CGPoint) -> Int? {
let touchPosition = lastRenderedChartFrame.origin + point * lastRenderedChartFrame.size
let center = CGPoint(x: lastRenderedChartFrame.midX, y: lastRenderedChartFrame.midY)
let radius = min(lastRenderedChartFrame.width, lastRenderedChartFrame.height) / 2
if center.distanceTo(touchPosition) > radius { return nil }
let angle = (center - touchPosition).angle + .pi
let currentData = currentlyVisibleData
let total: CGFloat = currentData.map({ $0.value }).reduce(0, +)
var startAngle: CGFloat = initialAngle
for (index, piece) in currentData.enumerated() {
let percent = piece.value / total
let segmentSize = 2 * .pi * percent
let endAngle = startAngle + segmentSize
if angle >= startAngle && angle <= endAngle ||
angle + .pi * 2 >= startAngle && angle + .pi * 2 <= endAngle {
return index
}
startAngle = endAngle
}
return nil
}
private var currentTransitionAnimationData: [PieComponent] {
if transitionAnimator.isAnimating {
let animationFraction = transitionAnimator.current
return percentageData.enumerated().map { arg in
return PieComponent(color: arg.element.color,
value: oldPercentageData[arg.offset].value * (1 - animationFraction) + arg.element.value * animationFraction)
}
} else {
return percentageData
}
}
var currentlyVisibleData: [PieComponent] {
return currentTransitionAnimationData.enumerated().map { arg in
return PieComponent(color: arg.element.color,
value: arg.element.value * componentsAnimators[arg.offset].current)
}
}
override func render(context: CGContext, bounds: CGRect, chartFrame: CGRect) {
guard isEnabled && verticalRange.current.distance > 0 && verticalRange.current.distance > 0 else { return }
lastRenderedBounds = bounds
lastRenderedChartFrame = chartFrame
let chartAlpha = chartAlphaAnimator.current
if chartAlpha == 0 { return }
let center = CGPoint(x: chartFrame.midX, y: chartFrame.midY)
let radius = min(chartFrame.width, chartFrame.height) / 2
let currentData = currentlyVisibleData
let total: CGFloat = currentData.map({ $0.value }).reduce(0, +)
guard total > 0 else {
return
}
let animationSelectionOffset: CGFloat = radius / 15
let maximumFontSize: CGFloat = radius / 7
let minimumFontSize: CGFloat = 4
let centerOffsetStartAngle = CGFloat.pi / 4
let minimumValueToDraw: CGFloat = 0.01
let diagramRadius = radius - animationSelectionOffset
let numberOfVisibleItems = currentlyVisibleData.filter { $0.value > 0 }.count
var startAngle: CGFloat = initialAngle
for (index, piece) in currentData.enumerated() {
let percent = piece.value / total
guard percent > 0 else { continue }
let segmentSize = 2 * .pi * percent * chartAlpha
let endAngle = startAngle + segmentSize
let centerAngle = (startAngle + endAngle) / 2
let labelVector = CGPoint(x: cos(centerAngle),
y: sin(centerAngle))
let selectionAnimationFraction = (numberOfVisibleItems > 1 ? setlectedSegmentsAnimators[index].current : 0)
let updatedCenter = CGPoint(x: center.x + labelVector.x * selectionAnimationFraction * animationSelectionOffset,
y: center.y + labelVector.y * selectionAnimationFraction * animationSelectionOffset)
if drawPie {
context.saveGState()
context.setFillColor(piece.color.withAlphaComponent(piece.color.alphaValue * chartAlpha).cgColor)
context.move(to: updatedCenter)
context.addArc(center: updatedCenter,
radius: radius - animationSelectionOffset,
startAngle: startAngle,
endAngle: endAngle,
clockwise: false)
context.fillPath()
context.restoreGState()
}
if drawValues && percent >= minimumValueToDraw {
context.saveGState()
let text = valuesFormatter.string(from: percent * 100)
let fraction = crop(0, segmentSize / centerOffsetStartAngle, 1)
let fontSize = (minimumFontSize + (maximumFontSize - minimumFontSize) * fraction).rounded(.up)
let labelPotisionOffset = diagramRadius / 2 + diagramRadius / 2 * (1 - fraction)
let font = UIFont.systemFont(ofSize: fontSize, weight: .bold)
let labelsEaseInColor = crop(0, chartAlpha * chartAlpha * 2 - 1, 1)
let attributes: [NSAttributedString.Key: Any] = [.foregroundColor: UIColor.white.withAlphaComponent(labelsEaseInColor),
.font: font]
let rect = (text as NSString).boundingRect(with: bounds.size,
options: .usesLineFragmentOrigin,
attributes: attributes,
context: nil)
let labelPoint = CGPoint(x: labelVector.x * labelPotisionOffset + updatedCenter.x - rect.width / 2,
y: labelVector.y * labelPotisionOffset + updatedCenter.y - rect.height / 2)
(text as NSString).draw(at: labelPoint, withAttributes: attributes)
context.restoreGState()
}
startAngle = endAngle
}
}
}

View file

@ -1,42 +0,0 @@
//
// VerticalLinesRenderer.swift
// GraphTest
//
// Created by Andrei Salavei on 4/8/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import UIKit
class VerticalLinesRenderer: BaseChartRenderer {
var values: [CGFloat] = [] {
didSet {
alphaAnimators = values.map { _ in AnimationController<CGFloat>(current: 1.0, refreshClosure: refreshClosure) }
setNeedsDisplay()
}
}
private var alphaAnimators: [AnimationController<CGFloat>] = []
var linesColor: UIColor = .black
var linesWidth: CGFloat = UIView.oneDevicePixel
func setLineVisible(_ isVisible: Bool, at index: Int, animated: Bool) {
alphaAnimators[index].animate(to: isVisible ? 1 : 0, duration: animated ? .defaultDuration : 0)
}
override func render(context: CGContext, bounds: CGRect, chartFrame: CGRect) {
guard isEnabled && verticalRange.current.distance > 0 && verticalRange.current.distance > 0 else { return }
context.setLineWidth(linesWidth)
for (index, value) in values.enumerated() {
let alpha = alphaAnimators[index].current
if alpha == 0 { continue }
context.setStrokeColor(linesColor.withAlphaComponent(linesColor.alphaValue * alpha).cgColor)
let pointX = transform(toChartCoordinateHorizontal: value, chartFrame: chartFrame)
context.strokeLineSegments(between: [CGPoint(x: pointX, y: chartFrame.minY),
CGPoint(x: pointX, y: chartFrame.maxY)])
}
}
}

View file

@ -1,162 +0,0 @@
//
// VerticalScalesRenderer.swift
// GraphTest
//
// Created by Andrei Salavei on 4/8/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import UIKit
class VerticalScalesRenderer: BaseChartRenderer {
private var verticalLabelsAndLines: [LinesChartLabel] = []
private var animatedVerticalLabelsAndLines: [AnimatedLinesChartLabels] = []
private lazy var horizontalLinesAlphaAnimator: AnimationController<CGFloat> = {
return AnimationController(current: 1, refreshClosure: self.refreshClosure)
}()
var drawAxisX: Bool = true
var axisXColor: UIColor = .black
var axisXWidth: CGFloat = UIView.oneDevicePixel
var isRightAligned: Bool = false
var horizontalLinesColor: UIColor = .black {
didSet {
setNeedsDisplay()
}
}
var horizontalLinesWidth: CGFloat = UIView.oneDevicePixel
var lavelsAsisOffset: CGFloat = 6
var labelsColor: UIColor = .black {
didSet {
setNeedsDisplay()
}
}
var labelsFont: UIFont = .systemFont(ofSize: 11)
func setHorizontalLinesVisible(_ visible: Bool, animated: Bool) {
let destinationValue: CGFloat = visible ? 1 : 0
guard self.horizontalLinesAlphaAnimator.end != destinationValue else { return }
if animated {
self.horizontalLinesAlphaAnimator.animate(to: destinationValue, duration: .defaultDuration)
} else {
self.horizontalLinesAlphaAnimator.set(current: destinationValue)
}
}
func setup(verticalLimitsLabels: [LinesChartLabel], animated: Bool) {
if animated {
var labelsToKeepVisible: [LinesChartLabel] = []
let labelsToHide: [LinesChartLabel]
var labelsToShow: [LinesChartLabel] = []
for label in verticalLimitsLabels {
if verticalLabelsAndLines.contains(label) {
labelsToKeepVisible.append(label)
} else {
labelsToShow.append(label)
}
}
labelsToHide = verticalLabelsAndLines.filter { !verticalLimitsLabels.contains($0) }
animatedVerticalLabelsAndLines.removeAll(where: { $0.isAppearing })
verticalLabelsAndLines = labelsToKeepVisible
let showAnimation = AnimatedLinesChartLabels(labels: labelsToShow, alphaAnimator: AnimationController(current: 1.0, refreshClosure: refreshClosure))
showAnimation.isAppearing = true
showAnimation.alphaAnimator.set(current: 0)
showAnimation.alphaAnimator.animate(to: 1, duration: .defaultDuration)
showAnimation.alphaAnimator.completionClosure = { [weak self, weak showAnimation] in
guard let self = self, let showAnimation = showAnimation else { return }
self.animatedVerticalLabelsAndLines.removeAll(where: { $0 === showAnimation })
self.verticalLabelsAndLines = verticalLimitsLabels
}
let hideAnimation = AnimatedLinesChartLabels(labels: labelsToHide, alphaAnimator: AnimationController(current: 1.0, refreshClosure: refreshClosure))
hideAnimation.isAppearing = false
hideAnimation.alphaAnimator.set(current: 1)
hideAnimation.alphaAnimator.animate(to: 0, duration: .defaultDuration)
hideAnimation.alphaAnimator.completionClosure = { [weak self, weak hideAnimation] in
guard let self = self, let hideAnimation = hideAnimation else { return }
self.animatedVerticalLabelsAndLines.removeAll(where: { $0 === hideAnimation })
}
animatedVerticalLabelsAndLines.append(showAnimation)
animatedVerticalLabelsAndLines.append(hideAnimation)
} else {
verticalLabelsAndLines = verticalLimitsLabels
animatedVerticalLabelsAndLines = []
}
}
override func render(context: CGContext, bounds: CGRect, chartFrame: CGRect) {
guard isEnabled && verticalRange.current.distance > 0 && verticalRange.current.distance > 0 else { return }
let generalAlpha = chartAlphaAnimator.current
if generalAlpha == 0 { return }
let labelColorAlpha = labelsColor.alphaValue
func drawLines(_ labels: [LinesChartLabel], alpha: CGFloat) {
var lineSegments: [CGPoint] = []
let x0 = chartFrame.minX
let x1 = chartFrame.maxX
context.setStrokeColor(horizontalLinesColor.withAlphaComponent(horizontalLinesColor.alphaValue * alpha).cgColor)
for lineInfo in labels {
let y = transform(toChartCoordinateVertical: lineInfo.value, chartFrame: chartFrame).roundedUpToPixelGrid()
lineSegments.append(CGPoint(x: x0, y: y))
lineSegments.append(CGPoint(x: x1, y: y))
}
context.strokeLineSegments(between: lineSegments)
}
func drawVerticalLabels(_ labels: [LinesChartLabel], attributes: [NSAttributedString.Key: Any]) {
if isRightAligned {
for label in labels {
let y = transform(toChartCoordinateVertical: label.value, chartFrame: chartFrame) - labelsFont.pointSize - lavelsAsisOffset
let rect = (label.text as NSString).boundingRect(with: bounds.size,
options: .usesLineFragmentOrigin,
attributes: attributes,
context: nil)
(label.text as NSString).draw(at: CGPoint(x:chartFrame.maxX - rect.width, y: y), withAttributes: attributes)
}
} else {
for label in labels {
let y = transform(toChartCoordinateVertical: label.value, chartFrame: chartFrame) - labelsFont.pointSize - lavelsAsisOffset
(label.text as NSString).draw(at: CGPoint(x:chartFrame.minX, y: y), withAttributes: attributes)
}
}
}
let horizontalLinesAlpha = horizontalLinesAlphaAnimator.current
if horizontalLinesAlpha > 0 {
context.setLineWidth(horizontalLinesWidth)
drawLines(verticalLabelsAndLines, alpha: generalAlpha)
for animatedLabesAndLines in animatedVerticalLabelsAndLines {
drawLines(animatedLabesAndLines.labels, alpha: animatedLabesAndLines.alphaAnimator.current * generalAlpha * horizontalLinesAlpha)
}
if drawAxisX {
context.setLineWidth(axisXWidth)
context.setStrokeColor(axisXColor.withAlphaComponent(axisXColor.alphaValue * horizontalLinesAlpha * generalAlpha).cgColor)
let lineSegments: [CGPoint] = [CGPoint(x: chartFrame.minX, y: chartFrame.maxY.roundedUpToPixelGrid()),
CGPoint(x: chartFrame.maxX, y: chartFrame.maxY.roundedUpToPixelGrid())]
context.strokeLineSegments(between: lineSegments)
}
}
drawVerticalLabels(verticalLabelsAndLines, attributes: [.foregroundColor: labelsColor.withAlphaComponent(labelColorAlpha * generalAlpha),
.font: labelsFont])
for animatedLabesAndLines in animatedVerticalLabelsAndLines {
drawVerticalLabels(animatedLabesAndLines.labels,
attributes: [.foregroundColor: labelsColor.withAlphaComponent(animatedLabesAndLines.alphaAnimator.current * labelColorAlpha * generalAlpha),
.font: labelsFont])
}
}
}

View file

@ -1,178 +0,0 @@
//
// RangeAnimatedContainer.swift
// GraphTest
//
// Created by Andrei Salavei on 3/12/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import UIKit
protocol Animatable {
static func valueBetween(start: Self, end: Self, offset: Double) -> Self
}
enum TimeFunction {
case linear
case easeOut
case easeIn
func profress(time: TimeInterval, duration: TimeInterval) -> TimeInterval {
switch self {
case .linear:
return time / duration
case .easeIn:
return (pow(2, 10 * (time / duration - 1)) - 0.0009765625) * 1.0009775171065499
case .easeOut:
return (-pow(2, -10 * time / duration)) + 1 * 1.0009775171065499
}
}
}
class AnimationController<AnimatableObject: Animatable> {
private(set) var isAnimating: Bool = false
private(set) var animationDuration: TimeInterval = 0.0
private(set) var currentTime: TimeInterval = 0.0
private(set) var start: AnimatableObject
private(set) var end: AnimatableObject
private(set) var current: AnimatableObject
var timeFunction: TimeFunction = .linear
var refreshClosure: (() -> Void)?
// var updateClosure: ((AnimatableObject) -> Void)?
var completionClosure: (() -> Void)?
init(current: AnimatableObject, refreshClosure: (() -> Void)?) {
self.current = current
self.start = current
self.end = current
self.refreshClosure = refreshClosure
}
func animate(to: AnimatableObject, duration: TimeInterval, timeFunction: TimeFunction = .linear) {
self.timeFunction = timeFunction
currentTime = 0
animationDuration = duration
if animationDuration > 0 {
start = current
end = to
isAnimating = true
DisplayLinkService.shared.add(listner: self)
} else {
start = to
end = to
current = to
isAnimating = false
DisplayLinkService.shared.remove(listner: self)
}
refreshClosure?()
}
func set(current: AnimatableObject) {
self.start = current
self.end = current
self.current = current
animationDuration = 0.0
currentTime = 0.0
// updateClosure?(current)
refreshClosure?()
if isAnimating {
isAnimating = false
DisplayLinkService.shared.remove(listner: self)
}
}
}
extension AnimationController: DisplayLinkListner {
func update(delta: TimeInterval) {
guard isAnimating else {
DisplayLinkService.shared.remove(listner: self)
return
}
currentTime += delta
if currentTime > animationDuration || animationDuration <= 0 {
start = end
current = end
isAnimating = false
animationDuration = 0.0
currentTime = 0.0
// updateClosure?(end)
completionClosure?()
refreshClosure?()
DisplayLinkService.shared.remove(listner: self)
} else {
let offset = timeFunction.profress(time: currentTime, duration: animationDuration)
current = AnimatableObject.valueBetween(start: start, end: end, offset: offset)
// updateClosure?(current)
refreshClosure?()
}
}
}
extension ClosedRange: Animatable where Bound: BinaryFloatingPoint {
static func valueBetween(start: ClosedRange<Bound>, end: ClosedRange<Bound>, offset: Double) -> ClosedRange<Bound> {
let castedOffset = Bound(offset)
return ClosedRange(uncheckedBounds: (lower: start.lowerBound + (end.lowerBound - start.lowerBound) * castedOffset,
upper: start.upperBound + (end.upperBound - start.upperBound) * castedOffset))
}
}
extension CGFloat: Animatable {
static func valueBetween(start: CGFloat, end: CGFloat, offset: Double) -> CGFloat {
return start + (end - start) * CGFloat(offset)
}
}
extension Double: Animatable {
static func valueBetween(start: Double, end: Double, offset: Double) -> Double {
return start + (end - start) * Double(offset)
}
}
extension Int: Animatable {
static func valueBetween(start: Int, end: Int, offset: Double) -> Int {
return start + Int(Double(end - start) * offset)
}
}
extension CGPoint: Animatable {
static func valueBetween(start: CGPoint, end: CGPoint, offset: Double) -> CGPoint {
return CGPoint(x: start.x + (end.x - start.x) * CGFloat(offset),
y: start.y + (end.y - start.y) * CGFloat(offset))
}
}
extension CGRect: Animatable {
static func valueBetween(start: CGRect, end: CGRect, offset: Double) -> CGRect {
return CGRect(x: start.origin.x + (end.origin.x - start.origin.x) * CGFloat(offset),
y: start.origin.y + (end.origin.y - start.origin.y) * CGFloat(offset),
width: start.width + (end.width - start.width) * CGFloat(offset),
height: start.height + (end.height - start.height) * CGFloat(offset))
}
}
struct UIColorContainer: Animatable {
var color: UIColor
static func valueBetween(start: UIColorContainer, end: UIColorContainer, offset: Double) -> UIColorContainer {
return UIColorContainer(color: UIColor.valueBetween(start: start.color, end: end.color, offset: offset))
}
}
extension UIColor {
static func valueBetween(start: UIColor, end: UIColor, offset: Double) -> UIColor {
let offsetF = CGFloat(offset)
let startCIColor = CIColor(color: start)
let endCIColor = CIColor(color: end)
return UIColor(red: startCIColor.red + (endCIColor.red - startCIColor.red) * offsetF,
green: startCIColor.green + (endCIColor.green - startCIColor.green) * offsetF,
blue: startCIColor.blue + (endCIColor.blue - startCIColor.blue) * offsetF,
alpha: startCIColor.alpha + (endCIColor.alpha - startCIColor.alpha) * offsetF)
}
}

View file

@ -1,18 +0,0 @@
//
// Array+Utils.swift
// GraphTest
//
// Created by Andrei Salavei on 4/7/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import Foundation
extension Array {
func safeElement(at index: Int) -> Element? {
if index >= 0 && index < count {
return self[index]
}
return nil
}
}

View file

@ -1,17 +0,0 @@
//
// CGFloat.swift
// GraphTest
//
// Created by Andrei Salavei on 4/11/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import UIKit
private let screenScale: CGFloat = UIScreen.main.scale
extension CGFloat {
func roundedUpToPixelGrid() -> CGFloat {
return (self * screenScale).rounded(.up) / screenScale
}
}

View file

@ -1,219 +0,0 @@
//
// CGPoint+Extensions.swift
// GraphTest
//
// Created by Andrei Salavei on 4/11/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import UIKit
extension CGPoint {
public init(vector: CGVector) {
self.init(x: vector.dx, y: vector.dy)
}
public init(angle: CGFloat) {
self.init(x: cos(angle), y: sin(angle))
}
public mutating func offset(dx: CGFloat, dy: CGFloat) -> CGPoint {
x += dx
y += dy
return self
}
public func length() -> CGFloat {
return sqrt(x*x + y*y)
}
public func lengthSquared() -> CGFloat {
return x*x + y*y
}
func normalized() -> CGPoint {
let len = length()
return len>0 ? self / len : CGPoint.zero
}
public mutating func normalize() -> CGPoint {
self = normalized()
return self
}
public func distanceTo(_ point: CGPoint) -> CGFloat {
return (self - point).length()
}
public var angle: CGFloat {
return atan2(y, x)
}
public var cgSize: CGSize {
return CGSize(width: x, height: y)
}
func rotate(origin: CGPoint, angle: CGFloat) -> CGPoint {
let point = self - origin
let s = sin(angle)
let c = cos(angle)
return CGPoint(x: c * point.x - s * point.y,
y: s * point.x + c * point.y) + origin
}
}
extension CGSize {
public var cgPoint: CGPoint {
return CGPoint(x: width, y: height)
}
public init(point: CGPoint) {
self.init(width: point.x, height: point.y)
}
}
public func + (left: CGPoint, right: CGPoint) -> CGPoint {
return CGPoint(x: left.x + right.x, y: left.y + right.y)
}
public func += (left: inout CGPoint, right: CGPoint) {
left = left + right
}
public func + (left: CGPoint, right: CGVector) -> CGPoint {
return CGPoint(x: left.x + right.dx, y: left.y + right.dy)
}
public func += (left: inout CGPoint, right: CGVector) {
left = left + right
}
public func - (left: CGPoint, right: CGPoint) -> CGPoint { return CGPoint(x: left.x - right.x, y: left.y - right.y) }
public func - (left: CGSize, right: CGSize) -> CGSize { return CGSize(width: left.width - right.width, height: left.height - right.height) }
public func - (left: CGSize, right: CGPoint) -> CGSize { return CGSize(width: left.width - right.x, height: left.height - right.x) }
public func - (left: CGPoint, right: CGSize) -> CGPoint { return CGPoint(x: left.x - right.width, y: left.y - right.height) }
public func -= (left: inout CGPoint, right: CGPoint) {
left = left - right
}
public func - (left: CGPoint, right: CGVector) -> CGPoint {
return CGPoint(x: left.x - right.dx, y: left.y - right.dy)
}
public func -= (left: inout CGPoint, right: CGVector) {
left = left - right
}
public func *= (left: inout CGPoint, right: CGPoint) {
left = left * right
}
public func * (point: CGPoint, scalar: CGFloat) -> CGPoint { return CGPoint(x: point.x * scalar, y: point.y * scalar) }
public func * (point: CGSize, scalar: CGFloat) -> CGSize { return CGSize(width: point.width * scalar, height: point.height * scalar) }
public func *= (point: inout CGPoint, scalar: CGFloat) { point = point * scalar }
public func * (left: CGPoint, right: CGVector) -> CGPoint {
return CGPoint(x: left.x * right.dx, y: left.y * right.dy)
}
public func *= (left: inout CGPoint, right: CGVector) {
left = left * right
}
public func / (left: CGPoint, right: CGPoint) -> CGPoint { return CGPoint(x: left.x / right.x, y: left.y / right.y) }
public func / (left: CGSize, right: CGSize) -> CGSize { return CGSize(width: left.width / right.width, height: left.height / right.height) }
public func / (left: CGPoint, right: CGSize) -> CGPoint { return CGPoint(x: left.x / right.width, y: left.y / right.height) }
public func / (left: CGSize, right: CGPoint) -> CGSize { return CGSize(width: left.width / right.x, height: left.height / right.y) }
public func /= (left: inout CGPoint, right: CGPoint) { left = left / right }
public func /= (left: inout CGSize, right: CGSize) { left = left / right }
public func /= (left: inout CGSize, right: CGPoint) { left = left / right }
public func /= (left: inout CGPoint, right: CGSize) { left = left / right }
public func / (point: CGPoint, scalar: CGFloat) -> CGPoint { return CGPoint(x: point.x / scalar, y: point.y / scalar) }
public func / (point: CGSize, scalar: CGFloat) -> CGSize { return CGSize(width: point.width / scalar, height: point.height / scalar) }
public func /= (point: inout CGPoint, scalar: CGFloat) {
point = point / scalar
}
public func / (left: CGPoint, right: CGVector) -> CGPoint {
return CGPoint(x: left.x / right.dx, y: left.y / right.dy)
}
public func / (left: CGSize, right: CGVector) -> CGSize {
return CGSize(width: left.width / right.dx, height: left.height / right.dy)
}
public func /= (left: inout CGPoint, right: CGVector) {
left = left / right
}
public func * (left: CGPoint, right: CGPoint) -> CGPoint { return CGPoint(x: left.x * right.x, y: left.y * right.y) }
public func * (left: CGPoint, right: CGSize) -> CGPoint { return CGPoint(x: left.x * right.width, y: left.y * right.height) }
public func *= (left: inout CGPoint, right: CGSize) { left = left * right }
public func * (left: CGSize, right: CGSize) -> CGSize { return CGSize(width: left.width * right.width, height: left.height * right.height) }
public func *= (left: inout CGSize, right: CGSize) { left = left * right }
public func * (left: CGSize, right: CGPoint) -> CGSize { return CGSize(width: left.width * right.x, height: left.height * right.y) }
public func *= (left: inout CGSize, right: CGPoint) { left = left * right }
public func lerp(start: CGPoint, end: CGPoint, t: CGFloat) -> CGPoint {
return start + (end - start) * t
}
public func abs(_ point: CGPoint) -> CGPoint {
return CGPoint(x: abs(point.x), y: abs(point.y))
}
extension CGSize {
var isValid: Bool {
return width > 0 && height > 0 && width != .infinity && height != .infinity && width != .nan && height != .nan
}
var ratio: CGFloat {
return width / height
}
}
extension CGRect {
static var identity: CGRect {
return CGRect(x: 0, y: 0, width: 1, height: 1)
}
var center: CGPoint {
return origin + size.cgPoint / 2
}
var rounded: CGRect {
return CGRect(x: origin.x.rounded(),
y: origin.y.rounded(),
width: width.rounded(.up),
height: height.rounded(.up))
}
var mirroredVertically: CGRect {
return CGRect(x: origin.x,
y: 1.0 - (origin.y + height),
width: width,
height: height)
}
}
extension CGAffineTransform {
func inverted(with size: CGSize) -> CGAffineTransform {
var transform = self
let transformedSize = CGRect(origin: .zero, size: size).applying(transform).size
transform.tx /= transformedSize.width;
transform.ty /= transformedSize.height;
transform = transform.inverted()
transform.tx *= transformedSize.width;
transform.ty *= transformedSize.height;
return transform
}
}

View file

@ -1,15 +0,0 @@
//
// ClosedRange+Utils.swift
// GraphTest
//
// Created by Andrei Salavei on 3/11/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import Foundation
extension ClosedRange where Bound: Numeric {
var distance: Bound {
return upperBound - lowerBound
}
}

View file

@ -1,19 +0,0 @@
//
// CustomNavigationController.swift
// GraphTest
//
// Created by Andrew Solovey on 15/03/2019.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import UIKit
class CustomNavigationController: UINavigationController {
override var preferredStatusBarStyle: UIStatusBarStyle {
return topViewController?.preferredStatusBarStyle ?? .default
}
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
return topViewController?.preferredStatusBarUpdateAnimation ?? .fade
}
}

View file

@ -1,114 +0,0 @@
//
// DisplayLinkService.swift
// GraphTest
//
// Created by Andrei Salavei on 4/7/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import UIKit
import CoreGraphics
public protocol DisplayLinkListner: class {
func update(delta: TimeInterval)
}
// DispatchSource mares refreshes more accurate
class DisplayLinkService {
let listners = NSHashTable<AnyObject>.weakObjects()
static let shared = DisplayLinkService()
public func add(listner: DisplayLinkListner) {
listners.add(listner)
startDisplayLink()
}
public func remove(listner: DisplayLinkListner) {
listners.remove(listner)
if listners.count == 0 {
stopDisplayLink()
}
}
// private init() {
// displayLink.add(to: .main, forMode: .common)
// displayLink.preferredFramesPerSecond = 60
// displayLink.isPaused = true
// }
//
// // MARK: - Display Link
// private lazy var displayLink: CADisplayLink! = { CADisplayLink(target: self, selector: #selector(displayLinkDidFire)) } ()
// private var previousTickTime = 0.0
//
// private func startDisplayLink() {
// guard displayLink.isPaused else {
// return
// }
// previousTickTime = CACurrentMediaTime()
// displayLink.isPaused = false
// }
//
// @objc private func displayLinkDidFire(_ displayLink: CADisplayLink) {
// let currentTime = CACurrentMediaTime()
// let delta = currentTime - previousTickTime
// previousTickTime = currentTime
// let allListners = listners.allObjects
// var hasListners = false
// for listner in allListners {
// (listner as! DisplayLinkListner).update(delta: delta)
// hasListners = true
// }
//
// if !hasListners {
// stopDisplayLink()
// }
// }
//
// private func stopDisplayLink() {
// displayLink.isPaused = true
// }
private init() {
dispatchSourceTimer.schedule(deadline: .now() + 1.0 / 60, repeating: 1.0 / 60)
dispatchSourceTimer.setEventHandler {
DispatchQueue.main.sync {
self.fire()
}
}
}
private var dispatchSourceTimer = DispatchSource.makeTimerSource(flags: [], queue: .global(qos: .userInteractive))
private var dispatchSourceTimerStarted: Bool = false
private var previousTickTime = 0.0
private func startDisplayLink() {
guard !dispatchSourceTimerStarted else { return }
dispatchSourceTimerStarted = true
previousTickTime = CACurrentMediaTime()
dispatchSourceTimer.resume()
}
private func stopDisplayLink() {
guard dispatchSourceTimerStarted else { return }
dispatchSourceTimerStarted = false
dispatchSourceTimer.suspend()
}
public func fire() {
let currentTime = CACurrentMediaTime()
let delta = currentTime - previousTickTime
previousTickTime = currentTime
let allListners = listners.allObjects
var hasListners = false
for listner in allListners {
(listner as! DisplayLinkListner).update(delta: delta)
hasListners = true
}
if !hasListners {
stopDisplayLink()
}
}
}

View file

@ -1,19 +0,0 @@
//
// NumberFormatter+Utils.swift
// GraphTest
//
// Created by Andrei Salavei on 4/12/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import UIKit
extension NumberFormatter {
func string(from value: CGFloat) -> String {
return string(from: Double(value))
}
func string(from value: Double) -> String {
return string(from: NSNumber(value: Double(value))) ?? ""
}
}

View file

@ -1,17 +0,0 @@
//
// OnePixelConstraint.swift
// GraphTest
//
// Created by Andrei Salavei on 4/13/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import UIKit
public class OnePixelConstrain: NSLayoutConstraint {
public override func awakeFromNib() {
super.awakeFromNib()
constant = UIView.oneDevicePixel
}
}

View file

@ -1,32 +0,0 @@
//
// ScalesNumberFormatter.swift
// GraphTest
//
// Created by Andrei Salavei on 4/13/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import UIKit
private let milionsScale = "M"
private let thousandsScale = "K"
class ScalesNumberFormatter: NumberFormatter {
override func string(from number: NSNumber) -> String? {
let value = number.doubleValue
let pow = log10(value)
if pow >= 6 {
guard let string = super.string(from: NSNumber(value: value / 1_000_000)) else {
return nil
}
return string + milionsScale
} else if pow >= 4 {
guard let string = super.string(from: NSNumber(value: value / 1_000)) else {
return nil
}
return string + thousandsScale
} else {
return super.string(from: number)
}
}
}

View file

@ -1,27 +0,0 @@
//
// TimeInterval+Utils.swift
// GraphTest
//
// Created by Andrei Salavei on 3/13/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import Foundation
extension TimeInterval {
static let minute: TimeInterval = 60
static let hour: TimeInterval = 60 * 60
static let day: TimeInterval = 60 * 60 * 24
static let osXDuration: TimeInterval = 0.25
static let expandAnimationDuration: TimeInterval = 0.4
static var animationDurationMultipler: Double = 1.0
static var defaultDuration: TimeInterval {
return innerDefaultDuration * animationDurationMultipler
}
private static var innerDefaultDuration: TimeInterval = osXDuration
static func setDefaultDuration(_ duration: TimeInterval) {
innerDefaultDuration = duration
}
}

View file

@ -1,36 +0,0 @@
//
// TimeZone.swift
// GraphTest
//
// Created by Andrei Salavei on 4/9/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import Foundation
extension TimeZone {
static let utc = TimeZone(secondsFromGMT: 0)!
}
extension Locale {
static let posix = Locale(identifier: "en_US_POSIX")
}
extension Calendar {
static let utc: Calendar = {
var calendar = Calendar.current
calendar.locale = Locale.posix
calendar.timeZone = TimeZone.utc
return calendar
}()
}
extension DateFormatter {
static func utc(format: String = "") -> DateFormatter {
let formatter = DateFormatter()
formatter.calendar = Calendar.utc
formatter.dateFormat = format
formatter.timeZone = TimeZone.utc
return formatter
}
}

View file

@ -1,64 +0,0 @@
//
// UIColor+Utils.swift
// GraphTest
//
// Created by Andrei Salavei on 3/11/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import UIKit
extension UIColor {
public convenience init?(hexString: String) {
let r, g, b, a: CGFloat
if hexString.hasPrefix("#") {
let start = hexString.index(hexString.startIndex, offsetBy: 1)
let hexColor = String(hexString[start...])
if hexColor.count == 8 {
let scanner = Scanner(string: hexColor)
var hexNumber: UInt64 = 0
if scanner.scanHexInt64(&hexNumber) {
r = CGFloat((hexNumber & 0xff000000) >> 24) / 255
g = CGFloat((hexNumber & 0x00ff0000) >> 16) / 255
b = CGFloat((hexNumber & 0x0000ff00) >> 8) / 255
a = CGFloat(hexNumber & 0x000000ff) / 255
self.init(red: r, green: g, blue: b, alpha: a)
return
}
} else if hexColor.count == 6 {
let scanner = Scanner(string: hexColor)
var hexNumber: UInt64 = 0
if scanner.scanHexInt64(&hexNumber) {
r = CGFloat((hexNumber & 0xff0000) >> 16) / 255
g = CGFloat((hexNumber & 0x00ff00) >> 8) / 255
b = CGFloat((hexNumber & 0x0000ff) >> 0) / 255
self.init(red: r, green: g, blue: b, alpha: 1.0)
return
}
}
}
return nil
}
func image(_ size: CGSize = CGSize(width: 1, height: 1)) -> UIImage {
if #available(iOS 10.0, *) {
return UIGraphicsImageRenderer(size: size).image { rendererContext in
self.setFill()
rendererContext.fill(CGRect(origin: .zero, size: size))
}
} else {
return UIImage()
}
}
var redValue: CGFloat{ return CIColor(color: self).red }
var greenValue: CGFloat{ return CIColor(color: self).green }
var blueValue: CGFloat{ return CIColor(color: self).blue }
var alphaValue: CGFloat{ return CIColor(color: self).alpha }
}

View file

@ -1,28 +0,0 @@
//
// UIImage+Utils.swift
// GraphTest
//
// Created by Andrei Salavei on 4/8/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import UIKit
extension UIImage {
static let arrowRight = UIImage(bundleImageName: "Chart/arrow_right")
static let arrowLeft = UIImage(bundleImageName: "Chart/arrow_left")
public convenience init?(color: UIColor, size: CGSize = CGSize(width: 1, height: 1)) {
let rect = CGRect(origin: .zero, size: size)
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0)
color.setFill()
UIRectFill(rect)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
guard let cgImage = image?.cgImage else { return nil }
self.init(cgImage: cgImage)
}
}

View file

@ -1,76 +0,0 @@
//
// ChartLineData.swift
// GraphTest
//
// Created by Andrei Salavei on 3/13/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import UIKit
struct ChartLineData {
var title: String
var color: UIColor
var width: CGFloat?
var points: [CGPoint]
}
extension ChartLineData {
static func horizontalRange(lines: [ChartLineData]) -> ClosedRange<CGFloat>? {
guard let firstPoint = lines.first?.points.first else { return nil }
var hMin: CGFloat = firstPoint.x
var hMax: CGFloat = firstPoint.x
for line in lines {
if let first = line.points.first,
let last = line.points.last {
hMin = min(hMin, first.x)
hMax = max(hMax, last.x)
}
}
return hMin...hMax
}
static func verticalRange(lines: [ChartLineData], calculatingRange: ClosedRange<CGFloat>? = nil, addBounds: Bool = false) -> ClosedRange<CGFloat>? {
if let calculatingRange = calculatingRange {
guard let initalStart = lines.first?.points.first(where: { $0.x > calculatingRange.lowerBound &&
$0.x < calculatingRange.upperBound }) else { return nil }
var vMin: CGFloat = initalStart.y
var vMax: CGFloat = initalStart.y
for line in lines {
if var index = line.points.firstIndex(where: { $0.x > calculatingRange.lowerBound }) {
if addBounds {
index = max(0, index - 1)
}
while index < line.points.count {
let point = line.points[index]
if point.x < calculatingRange.upperBound {
vMin = min(vMin, point.y)
vMax = max(vMax, point.y)
} else if addBounds {
vMin = min(vMin, point.y)
vMax = max(vMax, point.y)
break
} else {
break
}
index += 1
}
}
}
return vMin...vMax
} else {
guard let firstPoint = lines.first?.points.first else { return nil }
var vMin: CGFloat = firstPoint.y
var vMax: CGFloat = firstPoint.y
for line in lines {
for point in line.points {
vMin = min(vMin, point.y)
vMax = max(vMax, point.y)
}
}
return vMin...vMax
}
}
}

View file

@ -1,175 +0,0 @@
//
// ColorMode.swift
// GraphTest
//
// Created by Andrew Solovey on 15/03/2019.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import UIKit
import AppBundle
protocol ColorModeContainer {
func apply(colorMode: ColorMode, animated: Bool)
}
enum ColorMode {
case day
case night
}
extension ColorMode {
var chartTitleColor: UIColor { // Текст с датой на чарте
switch self {
case .day: return .black
case .night: return .white
}
}
var actionButtonColor: UIColor { // Кнопка Zoom Out/ Смена режима день/ночь
switch self {
case .day: return UIColor(red: 53/255.0, green: 120/255.0, blue: 246/255.0, alpha: 1.0)
case .night: return UIColor(red: 84/255.0, green: 164/255.0, blue: 247/255.0, alpha: 1.0)
}
}
var tableBackgroundColor: UIColor {
switch self {
case .day: return UIColor(red: 239/255.0, green: 239/255.0, blue: 244/255.0, alpha: 1.0)
case .night: return UIColor(red: 24/255.0, green: 34/255.0, blue: 45/255.0, alpha: 1.0)
}
}
var chartBackgroundColor: UIColor {
switch self {
case .day: return UIColor(red: 254/255.0, green: 254/255.0, blue: 254/255.0, alpha: 1.0)
case .night: return UIColor(red: 34/255.0, green: 47/255.0, blue: 63/255.0, alpha: 1.0)
}
}
var sectionTitleColor: UIColor {
switch self {
case .day: return UIColor(red: 109/255.0, green: 109/255.0, blue: 114/255.0, alpha: 1.0)
case .night: return UIColor(red: 133/255.0, green: 150/255.0, blue: 171/255.0, alpha: 1.0)
}
}
var tableSeparatorColor: UIColor {
switch self {
case .day: return UIColor(red: 200/255.0, green: 199/255.0, blue: 204/255.0, alpha: 1.0)
case .night: return UIColor(red: 18/255.0, green: 26/255.0, blue: 35/255.0, alpha: 1.0)
}
}
var chartLabelsColor: UIColor {
switch self {
case .day: return UIColor(red: 37/255.0, green: 37/255.0, blue: 41/255.0, alpha: 0.5)
case .night: return UIColor(red: 186/255.0, green: 204/255.0, blue: 225/255.0, alpha: 0.6)
}
}
var chartHelperLinesColor: UIColor {
switch self {
case .day: return UIColor(red: 24/255.0, green: 45/255.0, blue: 59/255.0, alpha: 0.1)
case .night: return UIColor(red: 133/255.0, green: 150/255.0, blue: 171/255.0, alpha: 0.20)
}
}
var chartStrongLinesColor: UIColor {
switch self {
case .day: return UIColor(red: 24/255.0, green: 45/255.0, blue: 59/255.0, alpha: 0.35)
case .night: return UIColor(red: 186/255.0, green: 204/255.0, blue: 225/255.0, alpha: 0.45)
}
}
var barChartStrongLinesColor: UIColor {
switch self {
case .day: return UIColor(red: 37/255.0, green: 37/255.0, blue: 41/255.0, alpha: 0.2)
case .night: return UIColor(red: 186/255.0, green: 204/255.0, blue: 225/255.0, alpha: 0.45)
}
}
var chartDetailsTextColor: UIColor {
switch self {
case .day: return UIColor(red: 109/255.0, green: 109/255.0, blue: 114/255.0, alpha: 1.0)
case .night: return UIColor(red: 254/255.0, green: 254/255.0, blue: 254/255.0, alpha: 1.0)
}
}
var chartDetailsArrowColor: UIColor {
switch self {
case .day: return UIColor(red: 197/255.0, green: 199/255.0, blue: 205/255.0, alpha: 1.0)
case .night: return UIColor(red: 76/255.0, green: 84/255.0, blue: 96/255.0, alpha: 1.0)
}
}
var chartDetailsViewColor: UIColor {
switch self {
case .day: return UIColor(red: 245/255.0, green: 245/255.0, blue: 251/255.0, alpha: 1.0)
case .night: return UIColor(red: 25/255.0, green: 35/255.0, blue: 47/255.0, alpha: 1.0)
}
}
var descriptionChatNameColor: UIColor {
switch self {
case .day: return .black
case .night: return UIColor(red: 254/255.0, green: 254/255.0, blue: 254/255.0, alpha: 1.0)
}
}
var descriptionActionColor: UIColor {
switch self {
case .day: return UIColor(red: 1/255.0, green: 125/255.0, blue: 229/255.0, alpha: 1.0)
case .night: return UIColor(red: 24/255.0, green: 145/255.0, blue: 255/255.0, alpha: 1.0)
}
}
var rangeViewBackgroundColor: UIColor {
switch self {
case .day: return UIColor(red: 254/255.0, green: 254/255.0, blue: 254/255.0, alpha: 1.0)
case .night: return UIColor(red: 34/255.0, green: 47/255.0, blue: 63/255.0, alpha: 1.0)
}
}
var rangeViewFrameColor: UIColor {
switch self {
case .day: return UIColor(red: 202/255.0, green: 212/255.0, blue: 222/255.0, alpha: 1.0)
case .night: return UIColor(red: 53/255.0, green: 70/255.0, blue: 89/255.0, alpha: 1.0)
}
}
var rangeViewTintColor: UIColor {
switch self {
case .day: return UIColor(red: 239/255.0, green: 239/255.0, blue: 244/255.0, alpha: 0.5)
case .night: return UIColor(red: 24/255.0, green: 34/255.0, blue: 45/255.0, alpha: 0.5)
}
}
var rangeViewMarkerColor: UIColor {
switch self {
case .day: return UIColor.white
case .night: return UIColor.white
}
}
var statusBarStyle: UIStatusBarStyle {
switch self {
case .day: return .default
case .night: return .lightContent
}
}
var viewTintColor: UIColor {
switch self {
case .day: return .black
case .night: return UIColor(red: 254/255.0, green: 254/255.0, blue: 254/255.0, alpha: 1.0)
}
}
var rangeCropImage: UIImage? {
switch self {
case .day: return UIImage(bundleImageName: "Chart/selection_frame_light")
case .night: return UIImage(bundleImageName: "Chart/selection_frame_dark")
}
}
}

View file

@ -1,25 +0,0 @@
//
// LinesChartLabel.swift
// GraphTest
//
// Created by Andrei Salavei on 3/18/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import UIKit
struct LinesChartLabel: Hashable {
let value: CGFloat
let text: String
}
class AnimatedLinesChartLabels {
var labels: [LinesChartLabel]
var isAppearing: Bool = false
let alphaAnimator: AnimationController<CGFloat>
init(labels: [LinesChartLabel], alphaAnimator: AnimationController<CGFloat>) {
self.labels = labels
self.alphaAnimator = alphaAnimator
}
}

View file

@ -1,15 +0,0 @@
//
// LinesSelectionLabel.swift
// GraphTest
//
// Created by Andrei Salavei on 3/18/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import UIKit
struct LinesSelectionLabel {
let coordinate: CGPoint
let valueText: String
let color: UIColor
}

15
submodules/GraphCore/BUCK Normal file
View file

@ -0,0 +1,15 @@
load("//Config:buck_rule_macros.bzl", "static_library")
static_library(
name = "GraphCore",
srcs = glob([
"Sources/**/*.swift",
]),
deps = [
"//submodules/Display:Display#shared",
],
frameworks = [
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
"$SDKROOT/System/Library/Frameworks/UIKit.framework",
],
)

View file

@ -1,19 +0,0 @@
//
// Graph.h
// Graph
//
// Created by Mikhail Filimonov on 03.02.2020.
// Copyright © 2020 Telegram. All rights reserved.
//
#import <Cocoa/Cocoa.h>
//! Project version number for Graph.
FOUNDATION_EXPORT double GraphVersionNumber;
//! Project version string for Graph.
FOUNDATION_EXPORT const unsigned char GraphVersionString[];
// In this header, you should import all the public headers of your framework using statements like #import <Graph/PublicHeader.h>

View file

@ -18,7 +18,5 @@
<string>1.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2020 Telegram. All rights reserved.</string>
</dict>
</plist>

View file

@ -68,7 +68,7 @@ public extension ChartsCollection {
switch type {
case .axix:
axixValuesToSetup = try column.dropFirst().map { Date(timeIntervalSince1970: try Convert.doubleFrom($0) / 1000) }
case .chart, .bar, .area:
case .chart, .bar, .area, .step:
guard let colorString = colors[columnId],
let color = GColor(hexString: colorString) else {
throw ChartsError.generalConversion("Unable to get color name from: \(colors) - \(columnId)")
@ -99,4 +99,5 @@ private enum ColumnType: String {
case chart = "line"
case area = "area"
case bar = "bar"
case step = "step"
}

View file

@ -45,7 +45,6 @@ public class BaseLinesChartController: BaseChartController {
}
func updateChartRangeTitle(animated: Bool) {
let range: ClosedRange<CGFloat>
if zoomedChartRange == BaseConstants.defaultRange {
range = initialChartRange

View file

@ -24,7 +24,7 @@ public class GeneralLinesChartController: BaseLinesChartController {
private let horizontalScalesRenderer = HorizontalScalesRenderer()
private let verticalScalesRenderer = VerticalScalesRenderer()
private let verticalLineRenderer = VerticalLinesRenderer()
private let lineBulletsRenerer = LineBulletsRenerer()
private let lineBulletsRenderer = LineBulletsRenderer()
private let previewLinesRenderer = LinesChartRenderer()
@ -41,7 +41,7 @@ public class GeneralLinesChartController: BaseLinesChartController {
self.mainLinesRenderer.optimizationLevel = BaseConstants.linesChartOptimizationLevel
self.previewLinesRenderer.optimizationLevel = BaseConstants.previewLinesChartOptimizationLevel
self.lineBulletsRenerer.isEnabled = false
self.lineBulletsRenderer.isEnabled = false
super.init(chartsCollection: chartsCollection)
self.zoomChartVisibility = chartVisibility
@ -61,7 +61,7 @@ public class GeneralLinesChartController: BaseLinesChartController {
self.prevoiusHorizontalStrideInterval = -1
self.totalVerticalRange = LinesChartRenderer.LineData.verticalRange(lines: chartLines) ?? Constants.defaultRange
self.totalHorizontalRange = LinesChartRenderer.LineData.horizontalRange(lines: chartLines) ?? Constants.defaultRange
self.lineBulletsRenerer.bullets = self.chartLines.map { LineBulletsRenerer.Bullet(coordinate: $0.points.first ?? .zero,
self.lineBulletsRenderer.bullets = self.chartLines.map { LineBulletsRenderer.Bullet(coordinate: $0.points.first ?? .zero,
color: $0.color)}
let chartRange: ClosedRange<CGFloat>
@ -77,7 +77,7 @@ public class GeneralLinesChartController: BaseLinesChartController {
self.mainLinesRenderer.setLines(lines: chartLines, animated: animated)
self.previewLinesRenderer.setLines(lines: chartLines, animated: animated)
updateHorizontalLimists(horizontalRange: chartRange, animated: animated)
updateHorizontalLimits(horizontalRange: chartRange, animated: animated)
updateMainChartHorizontalRange(range: chartRange, animated: animated)
updateVerticalLimitsAndRange(horizontalRange: chartRange, animated: animated)
self.chartRangeUpdatedClosure?(currentChartHorizontalRangeFraction, animated)
@ -97,7 +97,7 @@ public class GeneralLinesChartController: BaseLinesChartController {
horizontalScalesRenderer,
verticalScalesRenderer,
verticalLineRenderer,
lineBulletsRenerer
lineBulletsRenderer
]
}
@ -111,7 +111,7 @@ public class GeneralLinesChartController: BaseLinesChartController {
for (index, isVisible) in visibility.enumerated() {
mainLinesRenderer.setLineVisible(isVisible, at: index, animated: animated)
previewLinesRenderer.setLineVisible(isVisible, at: index, animated: animated)
lineBulletsRenerer.setLineVisible(isVisible, at: index, animated: animated)
lineBulletsRenderer.setLineVisible(isVisible, at: index, animated: animated)
}
updateVerticalLimitsAndRange(horizontalRange: currentHorizontalRange, animated: true)
@ -132,10 +132,10 @@ public class GeneralLinesChartController: BaseLinesChartController {
super.chartInteractionDidBegin(point: point)
self.lineBulletsRenerer.bullets = chartLines.compactMap { chart in
return LineBulletsRenerer.Bullet(coordinate: chart.points[minIndex], color: chart.color)
self.lineBulletsRenderer.bullets = chartLines.compactMap { chart in
return LineBulletsRenderer.Bullet(coordinate: chart.points[minIndex], color: chart.color)
}
self.lineBulletsRenerer.isEnabled = true
self.lineBulletsRenderer.isEnabled = true
let chartValue: CGFloat = CGFloat(closestDate.timeIntervalSince1970)
let detailsViewPosition = (chartValue - horizontalRange.lowerBound) / horizontalRange.distance * chartFrame.width + chartFrame.minX
@ -158,7 +158,7 @@ public class GeneralLinesChartController: BaseLinesChartController {
public override func cancelChartInteraction() {
super.cancelChartInteraction()
self.lineBulletsRenerer.isEnabled = false
self.lineBulletsRenderer.isEnabled = false
self.setDetailsChartVisibleClosure?(false, true)
self.verticalLineRenderer.values = []
@ -187,7 +187,7 @@ public class GeneralLinesChartController: BaseLinesChartController {
updateChartRangeTitle(animated: true)
updateMainChartHorizontalRange(range: horizontalRange, animated: false)
updateHorizontalLimists(horizontalRange: horizontalRange, animated: true)
updateHorizontalLimits(horizontalRange: horizontalRange, animated: true)
updateVerticalLimitsAndRange(horizontalRange: horizontalRange, animated: true)
}
@ -196,7 +196,7 @@ public class GeneralLinesChartController: BaseLinesChartController {
horizontalScalesRenderer.setup(horizontalRange: range, animated: animated)
verticalScalesRenderer.setup(horizontalRange: range, animated: animated)
verticalLineRenderer.setup(horizontalRange: range, animated: animated)
lineBulletsRenerer.setup(horizontalRange: range, animated: animated)
lineBulletsRenderer.setup(horizontalRange: range, animated: animated)
}
func updateMainChartVerticalRange(range: ClosedRange<CGFloat>, animated: Bool) {
@ -204,10 +204,10 @@ public class GeneralLinesChartController: BaseLinesChartController {
horizontalScalesRenderer.setup(verticalRange: range, animated: animated)
verticalScalesRenderer.setup(verticalRange: range, animated: animated)
verticalLineRenderer.setup(verticalRange: range, animated: animated)
lineBulletsRenerer.setup(verticalRange: range, animated: animated)
lineBulletsRenderer.setup(verticalRange: range, animated: animated)
}
func updateHorizontalLimists(horizontalRange: ClosedRange<CGFloat>, animated: Bool) {
func updateHorizontalLimits(horizontalRange: ClosedRange<CGFloat>, animated: Bool) {
if let (stride, labels) = horizontalLimitsLabels(horizontalRange: horizontalRange,
scaleType: isZoomed ? .hour : .day,
prevoiusHorizontalStrideInterval: prevoiusHorizontalStrideInterval) {
@ -221,7 +221,6 @@ public class GeneralLinesChartController: BaseLinesChartController {
calculatingRange: horizontalRange,
addBounds: true) {
let (range, labels) = verticalLimitsLabels(verticalRange: verticalRange)
if verticalScalesRenderer.verticalRange.end != range {
@ -245,7 +244,7 @@ public class GeneralLinesChartController: BaseLinesChartController {
verticalScalesRenderer.labelsColor = colorMode.chartLabelsColor
verticalScalesRenderer.axisXColor = colorMode.chartStrongLinesColor
verticalScalesRenderer.horizontalLinesColor = colorMode.chartHelperLinesColor
lineBulletsRenerer.setInnerColor(colorMode.chartBackgroundColor, animated: animated)
lineBulletsRenderer.setInnerColor(colorMode.chartBackgroundColor, animated: animated)
verticalLineRenderer.linesColor = colorMode.chartStrongLinesColor
}
}

View file

@ -22,7 +22,7 @@ public class TwoAxisLinesChartController: BaseLinesChartController {
class GraphController {
let mainLinesRenderer = LinesChartRenderer()
let verticalScalesRenderer = VerticalScalesRenderer()
let lineBulletsRenerer = LineBulletsRenerer()
let lineBulletsRenderer = LineBulletsRenderer()
let previewLinesRenderer = LinesChartRenderer()
var chartLines: [LinesChartRenderer.LineData] = []
@ -32,7 +32,7 @@ public class TwoAxisLinesChartController: BaseLinesChartController {
init() {
self.mainLinesRenderer.lineWidth = 2
self.previewLinesRenderer.lineWidth = 1
self.lineBulletsRenerer.isEnabled = false
self.lineBulletsRenderer.isEnabled = false
self.mainLinesRenderer.optimizationLevel = BaseConstants.linesChartOptimizationLevel
self.previewLinesRenderer.optimizationLevel = BaseConstants.previewLinesChartOptimizationLevel
@ -41,7 +41,7 @@ public class TwoAxisLinesChartController: BaseLinesChartController {
func updateMainChartVerticalRange(range: ClosedRange<CGFloat>, animated: Bool) {
mainLinesRenderer.setup(verticalRange: range, animated: animated)
verticalScalesRenderer.setup(verticalRange: range, animated: animated)
lineBulletsRenerer.setup(verticalRange: range, animated: animated)
lineBulletsRenderer.setup(verticalRange: range, animated: animated)
}
}
@ -77,7 +77,7 @@ public class TwoAxisLinesChartController: BaseLinesChartController {
controller.verticalScalesRenderer.labelsColor = chart.color
controller.totalVerticalRange = LinesChartRenderer.LineData.verticalRange(lines: chartLines) ?? Constants.defaultRange
self.totalHorizontalRange = LinesChartRenderer.LineData.horizontalRange(lines: chartLines) ?? Constants.defaultRange
controller.lineBulletsRenerer.bullets = chartLines.map { LineBulletsRenerer.Bullet(coordinate: $0.points.first ?? .zero,
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)
@ -97,14 +97,14 @@ public class TwoAxisLinesChartController: BaseLinesChartController {
chartRange = initialChartRange
}
updateHorizontalLimists(horizontalRange: chartRange, 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() {
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)
@ -114,7 +114,7 @@ public class TwoAxisLinesChartController: BaseLinesChartController {
public override var mainChartRenderers: [ChartViewRenderer] {
return graphControllers.map { $0.mainLinesRenderer } +
graphControllers.flatMap { [$0.verticalScalesRenderer, $0.lineBulletsRenerer] } +
graphControllers.flatMap { [$0.verticalScalesRenderer, $0.lineBulletsRenderer] } +
[horizontalScalesRenderer, verticalLineRenderer,
// performanceRenderer
]
@ -133,7 +133,7 @@ public class TwoAxisLinesChartController: BaseLinesChartController {
for graphIndex in graph.chartLines.indices {
graph.mainLinesRenderer.setLineVisible(isVisible, at: graphIndex, animated: animated)
graph.previewLinesRenderer.setLineVisible(isVisible, at: graphIndex, animated: animated)
graph.lineBulletsRenerer.setLineVisible(isVisible, at: graphIndex, animated: animated)
graph.lineBulletsRenderer.setLineVisible(isVisible, at: graphIndex, animated: animated)
}
graph.verticalScalesRenderer.setVisible(isVisible, animated: animated)
if let firstIndex = firstIndex {
@ -160,10 +160,10 @@ public class TwoAxisLinesChartController: BaseLinesChartController {
super.chartInteractionDidBegin(point: point)
for graphController in graphControllers {
graphController.lineBulletsRenerer.bullets = graphController.chartLines.map { chart in
LineBulletsRenerer.Bullet(coordinate: chart.points[minIndex], color: chart.color)
graphController.lineBulletsRenderer.bullets = graphController.chartLines.map { chart in
LineBulletsRenderer.Bullet(coordinate: chart.points[minIndex], color: chart.color)
}
graphController.lineBulletsRenerer.isEnabled = true
graphController.lineBulletsRenderer.isEnabled = true
}
let chartValue: CGFloat = CGFloat(closestDate.timeIntervalSince1970)
@ -187,7 +187,7 @@ public class TwoAxisLinesChartController: BaseLinesChartController {
public override func cancelChartInteraction() {
super.cancelChartInteraction()
for graphController in graphControllers {
graphController.lineBulletsRenerer.isEnabled = false
graphController.lineBulletsRenderer.isEnabled = false
}
self.setDetailsChartVisibleClosure?(false, true)
@ -210,7 +210,7 @@ public class TwoAxisLinesChartController: BaseLinesChartController {
updateChartRangeTitle(animated: true)
updateMainChartHorizontalRange(range: horizontalRange, animated: false)
updateHorizontalLimists(horizontalRange: horizontalRange, animated: true)
updateHorizontalLimits(horizontalRange: horizontalRange, animated: true)
updateVerticalLimitsAndRange(horizontalRange: horizontalRange, animated: true)
}
@ -218,13 +218,13 @@ public class TwoAxisLinesChartController: BaseLinesChartController {
for controller in graphControllers {
controller.mainLinesRenderer.setup(horizontalRange: range, animated: animated)
controller.verticalScalesRenderer.setup(horizontalRange: range, animated: animated)
controller.lineBulletsRenerer.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 updateHorizontalLimists(horizontalRange: ClosedRange<CGFloat>, animated: Bool) {
func updateHorizontalLimits(horizontalRange: ClosedRange<CGFloat>, animated: Bool) {
if let (stride, labels) = horizontalLimitsLabels(horizontalRange: horizontalRange,
scaleType: isZoomed ? .hour : .day,
prevoiusHorizontalStrideInterval: prevoiusHorizontalStrideInterval) {
@ -278,7 +278,6 @@ public class TwoAxisLinesChartController: BaseLinesChartController {
let numberFormatter = BaseConstants.chartNumberFormatter
for (index, controller) in graphControllers.enumerated() {
let (startValue, base, _, maximumNumberOfDecimals) = dividorsAndMultiplers[index]
let updatedRange = startValue...(startValue + base * CGFloat(totalCount))
@ -304,7 +303,7 @@ public class TwoAxisLinesChartController: BaseLinesChartController {
for controller in graphControllers {
controller.verticalScalesRenderer.horizontalLinesColor = colorMode.chartHelperLinesColor
controller.lineBulletsRenerer.setInnerColor(colorMode.chartBackgroundColor, animated: animated)
controller.lineBulletsRenderer.setInnerColor(colorMode.chartBackgroundColor, animated: animated)
controller.verticalScalesRenderer.axisXColor = colorMode.chartStrongLinesColor
}
}

View file

@ -94,6 +94,10 @@ public class PercentPieChartController: BaseChartController {
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) {
@ -238,32 +242,36 @@ public class PercentPieChartController: BaseChartController {
}
}
public override func didTapZoomIn(date: Date) {
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
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)
switchToChart(chartsCollection: newCollection, isZoomed: true, animated: true)
}
public override func didTapZoomIn(date: Date) {
self.didTapZoomIn(date: date, animated: true)
}
public override func didTapZoomOut() {

View file

@ -17,6 +17,7 @@ class BarsComponentController: GeneralChartComponentController {
let mainBarsRenderer: BarChartRenderer
let horizontalScalesRenderer: HorizontalScalesRenderer
let verticalScalesRenderer: VerticalScalesRenderer
let secondVerticalScalesRenderer: VerticalScalesRenderer?
let previewBarsChartRenderer: BarChartRenderer
private(set) var barsWidth: CGFloat = 1
@ -33,6 +34,9 @@ class BarsComponentController: GeneralChartComponentController {
self.verticalScalesRenderer = verticalScalesRenderer
self.previewBarsChartRenderer = previewBarsChartRenderer
self.secondVerticalScalesRenderer = VerticalScalesRenderer()
self.secondVerticalScalesRenderer?.isRightAligned = true
self.mainBarsRenderer.optimizationLevel = BaseConstants.barsChartOptimizationLevel
self.previewBarsChartRenderer.optimizationLevel = BaseConstants.barsChartOptimizationLevel
@ -71,11 +75,11 @@ class BarsComponentController: GeneralChartComponentController {
override func willAppear(animated: Bool) {
mainBarsRenderer.bars = self.chartBars
previewBarsChartRenderer.bars = self.chartBars
previewBarsChartRenderer.setup(verticalRange: totalVerticalRange, animated: animated)
previewBarsChartRenderer.setup(verticalRange: 0...117278, animated: animated)
previewBarsChartRenderer.setup(horizontalRange: totalHorizontalRange, animated: animated)
setupMainChart(verticalRange: initialVerticalRange, animated: animated)
setupMainChart(verticalRange: 0...117278, animated: animated)
setupMainChart(horizontalRange: initialHorizontalRange, animated: animated)
updateChartVerticalRanges(horizontalRange: initialHorizontalRange, animated: animated)
@ -115,12 +119,16 @@ class BarsComponentController: GeneralChartComponentController {
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 {
@ -143,8 +151,15 @@ class BarsComponentController: GeneralChartComponentController {
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) {
@ -153,9 +168,14 @@ class BarsComponentController: GeneralChartComponentController {
}
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) {
@ -181,7 +201,7 @@ class BarsComponentController: GeneralChartComponentController {
viewModel.totalValue = ChartDetailsViewModel.Value(prefix: nil,
title: "Total",
value: BaseConstants.detailsNumberFormatter.string(from: totalSumm),
value: BaseConstants.detailsNumberFormatter.string(from: totalSumm),
color: .white,
visible: visibleChartValues.count > 1)
return viewModel
@ -215,6 +235,10 @@ class BarsComponentController: GeneralChartComponentController {
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) {

View file

@ -31,7 +31,7 @@ public class DailyBarsChartController: BaseChartController {
horizontalScalesRenderer: horizontalScalesRenderer,
verticalScalesRenderer: verticalScalesRenderer,
verticalLineRenderer: VerticalLinesRenderer(),
lineBulletsRenerer: LineBulletsRenerer(),
lineBulletsRenderer: LineBulletsRenderer(),
previewLinesChartRenderer: LinesChartRenderer())
super.init(chartsCollection: chartsCollection)
@ -69,7 +69,7 @@ public class DailyBarsChartController: BaseChartController {
barsController.horizontalScalesRenderer,
barsController.verticalScalesRenderer,
linesController.verticalLineRenderer,
linesController.lineBulletsRenerer,
linesController.lineBulletsRenderer,
// performanceRenderer
]
}
@ -79,7 +79,7 @@ public class DailyBarsChartController: BaseChartController {
linesController.previewLinesChartRenderer]
}
public override func initializeChart() {
public override func initializeChart() {
barsController.initialize(chartsCollection: initialChartsCollection,
initialDate: Date(),
totalHorizontalRange: BaseConstants.defaultRange,

View file

@ -18,7 +18,7 @@ class LinesComponentController: GeneralChartComponentController {
let horizontalScalesRenderer: HorizontalScalesRenderer
let verticalScalesRenderer: VerticalScalesRenderer
let verticalLineRenderer: VerticalLinesRenderer
let lineBulletsRenerer: LineBulletsRenerer
let lineBulletsRenderer: LineBulletsRenderer
let previewLinesChartRenderer: LinesChartRenderer
@ -35,13 +35,13 @@ class LinesComponentController: GeneralChartComponentController {
horizontalScalesRenderer: HorizontalScalesRenderer,
verticalScalesRenderer: VerticalScalesRenderer,
verticalLineRenderer: VerticalLinesRenderer,
lineBulletsRenerer: LineBulletsRenerer,
lineBulletsRenderer: LineBulletsRenderer,
previewLinesChartRenderer: LinesChartRenderer) {
self.mainLinesRenderer = mainLinesRenderer
self.horizontalScalesRenderer = horizontalScalesRenderer
self.verticalScalesRenderer = verticalScalesRenderer
self.verticalLineRenderer = verticalLineRenderer
self.lineBulletsRenerer = lineBulletsRenerer
self.lineBulletsRenderer = lineBulletsRenderer
self.previewLinesChartRenderer = previewLinesChartRenderer
self.userLinesTransitionAnimation = userLinesTransitionAnimation
@ -52,7 +52,7 @@ class LinesComponentController: GeneralChartComponentController {
self.previewLinesChartRenderer.lineWidth = BaseConstants.previewChartLineWidth
self.previewLinesChartRenderer.optimizationLevel = BaseConstants.previewLinesChartOptimizationLevel
self.lineBulletsRenerer.isEnabled = false
self.lineBulletsRenderer.isEnabled = false
}
override func initialize(chartsCollection: ChartsCollection,
@ -62,7 +62,7 @@ class LinesComponentController: GeneralChartComponentController {
let (chartLines, totalHorizontalRange, totalVerticalRange) = LinesChartRenderer.LineData.initialComponents(chartsCollection: chartsCollection)
self.chartLines = chartLines
self.lineBulletsRenerer.bullets = self.chartLines.map { LineBulletsRenerer.Bullet(coordinate: $0.points.first ?? .zero,
self.lineBulletsRenderer.bullets = self.chartLines.map { LineBulletsRenderer.Bullet(coordinate: $0.points.first ?? .zero,
color: $0.color)}
super.initialize(chartsCollection: chartsCollection,
@ -121,7 +121,7 @@ class LinesComponentController: GeneralChartComponentController {
verticalScalesRenderer.setVisible(visible, animated: animated)
verticalLineRenderer.setVisible(visible, animated: animated)
previewLinesChartRenderer.setVisible(visible, animated: animated)
lineBulletsRenerer.setVisible(visible, animated: animated)
lineBulletsRenderer.setVisible(visible, animated: animated)
}
func setupMainChart(horizontalRange: ClosedRange<CGFloat>, animated: Bool) {
@ -129,7 +129,7 @@ class LinesComponentController: GeneralChartComponentController {
horizontalScalesRenderer.setup(horizontalRange: horizontalRange, animated: animated)
verticalScalesRenderer.setup(horizontalRange: horizontalRange, animated: animated)
verticalLineRenderer.setup(horizontalRange: horizontalRange, animated: animated)
lineBulletsRenerer.setup(horizontalRange: horizontalRange, animated: animated)
lineBulletsRenderer.setup(horizontalRange: horizontalRange, animated: animated)
}
var visibleLines: [LinesChartRenderer.LineData] {
@ -161,7 +161,7 @@ class LinesComponentController: GeneralChartComponentController {
horizontalScalesRenderer.setup(verticalRange: verticalRange, animated: animated)
verticalScalesRenderer.setup(verticalRange: verticalRange, animated: animated)
verticalLineRenderer.setup(verticalRange: verticalRange, animated: animated)
lineBulletsRenerer.setup(verticalRange: verticalRange, animated: animated)
lineBulletsRenderer.setup(verticalRange: verticalRange, animated: animated)
}
public override func updateChartsVisibility(visibility: [Bool], animated: Bool) {
@ -169,7 +169,7 @@ class LinesComponentController: GeneralChartComponentController {
for (index, isVisible) in visibility.enumerated() {
mainLinesRenderer.setLineVisible(isVisible, at: index, animated: animated)
previewLinesChartRenderer.setLineVisible(isVisible, at: index, animated: animated)
lineBulletsRenerer.setLineVisible(isVisible, at: index, animated: animated)
lineBulletsRenderer.setLineVisible(isVisible, at: index, animated: animated)
}
updateChartVerticalRanges(horizontalRange: currentHorizontalMainChartRange, animated: true)
}
@ -187,10 +187,10 @@ class LinesComponentController: GeneralChartComponentController {
verticalLineRenderer.values = [chartPosition]
verticalLineRenderer.isEnabled = true
lineBulletsRenerer.isEnabled = true
lineBulletsRenerer.setVisible(true, animated: animted)
lineBulletsRenerer.bullets = chartLines.compactMap { chart in
return LineBulletsRenerer.Bullet(coordinate: chart.points[dataIndex], color: chart.color)
lineBulletsRenderer.isEnabled = true
lineBulletsRenderer.setVisible(true, animated: animted)
lineBulletsRenderer.bullets = chartLines.compactMap { chart in
return LineBulletsRenderer.Bullet(coordinate: chart.points[dataIndex], color: chart.color)
}
}
@ -199,7 +199,7 @@ class LinesComponentController: GeneralChartComponentController {
verticalLineRenderer.values = []
verticalLineRenderer.isEnabled = false
lineBulletsRenerer.isEnabled = false
lineBulletsRenderer.isEnabled = false
}
override func apply(colorMode: GColorMode, animated: Bool) {
@ -209,7 +209,7 @@ class LinesComponentController: GeneralChartComponentController {
verticalScalesRenderer.labelsColor = colorMode.chartLabelsColor
verticalScalesRenderer.axisXColor = colorMode.chartStrongLinesColor
verticalScalesRenderer.horizontalLinesColor = colorMode.chartHelperLinesColor
lineBulletsRenerer.setInnerColor(colorMode.chartBackgroundColor, animated: animated)
lineBulletsRenderer.setInnerColor(colorMode.chartBackgroundColor, animated: animated)
verticalLineRenderer.linesColor = colorMode.chartStrongLinesColor
}
}

View file

@ -74,7 +74,7 @@ public class StackedBarsChartController: BaseChartController {
zoomedBarsController.previewBarsChartRenderer]
}
public override func initializeChart() {
public override func initializeChart() {
barsController.initialize(chartsCollection: initialChartsCollection,
initialDate: Date(),
totalHorizontalRange: BaseConstants.defaultRange,

View file

@ -1,31 +1,36 @@
//
// StackedBarsChartController.swift
// 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
class StackedBarsChartController: BaseChartController {
public class StepBarsChartController: BaseChartController {
let barsController: BarsComponentController
let zoomedBarsController: BarsComponentController
override init(chartsCollection: ChartsCollection) {
override public init(chartsCollection: ChartsCollection) {
let horizontalScalesRenderer = HorizontalScalesRenderer()
let verticalScalesRenderer = VerticalScalesRenderer()
barsController = BarsComponentController(isZoomed: false,
mainBarsRenderer: BarChartRenderer(),
mainBarsRenderer: BarChartRenderer(step: true),
horizontalScalesRenderer: horizontalScalesRenderer,
verticalScalesRenderer: verticalScalesRenderer,
previewBarsChartRenderer: BarChartRenderer())
zoomedBarsController = BarsComponentController(isZoomed: true,
mainBarsRenderer: BarChartRenderer(),
mainBarsRenderer: BarChartRenderer(step: true),
horizontalScalesRenderer: horizontalScalesRenderer,
verticalScalesRenderer: verticalScalesRenderer,
previewBarsChartRenderer: BarChartRenderer())
super.init(chartsCollection: chartsCollection)
[barsController, zoomedBarsController].forEach { controller in
@ -55,21 +60,22 @@ class StackedBarsChartController: BaseChartController {
}
}
override var mainChartRenderers: [ChartViewRenderer] {
public override var mainChartRenderers: [ChartViewRenderer] {
return [barsController.mainBarsRenderer,
zoomedBarsController.mainBarsRenderer,
barsController.horizontalScalesRenderer,
barsController.verticalScalesRenderer,
barsController.secondVerticalScalesRenderer!
// performanceRenderer
]
}
override var navigationRenderers: [ChartViewRenderer] {
public override var navigationRenderers: [ChartViewRenderer] {
return [barsController.previewBarsChartRenderer,
zoomedBarsController.previewBarsChartRenderer]
}
override func initializeChart() {
public override func initializeChart() {
barsController.initialize(chartsCollection: initialChartsCollection,
initialDate: Date(),
totalHorizontalRange: BaseConstants.defaultRange,
@ -79,72 +85,65 @@ class StackedBarsChartController: BaseChartController {
func switchToChart(chartsCollection: ChartsCollection, isZoomed: Bool, animated: Bool) {
if animated {
TimeInterval.setDefaultDuration(.expandAnimationDuration)
TimeInterval.setDefaultSuration(.expandAnimationDuration)
DispatchQueue.main.asyncAfter(deadline: .now() + .expandAnimationDuration) {
TimeInterval.setDefaultDuration(.osXDuration)
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)
// 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.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)
// 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: barsController.chartVisibility, animated: false)
zoomedBarsController.mainBarsRenderer.setup(verticalRange: zoomedBarsController.currentVerticalMainChartRange, animated: animated, timeFunction: .easeOut)
zoomedBarsController.previewBarsChartRenderer.setup(verticalRange: zoomedBarsController.currentPreviewVerticalRange, animated: animated, timeFunction: .easeOut)
zoomedBarsController.updateChartsVisibility(visibility: zoomedBarsController.chartBars.components.map { _ in true }, animated: false)
} 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)
// let destinationChartVerticalRange = lineProportionAnimationRange()
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.mainLinesRenderer.setup(verticalRange: destinationChartVerticalRange, animated: animated)
// zoomedBarsController.previewLinesChartRenderer.setup(verticalRange: destinationChartVerticalRange, animated: animated)
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)
}
zoomedBarsController.mainBarsRenderer.setVisible(false, animated: animated)
zoomedBarsController.previewBarsChartRenderer.setVisible(false, animated: animated)
}
barsController.willAppear(animated: animated)
zoomedBarsController.willDisappear(animated: animated)
if !zoomedBarsController.chartsCollection.isBlank {
barsController.updateChartsVisibility(visibility: zoomedBarsController.chartVisibility, animated: false)
}
}
self.setBackButtonVisibilityClosure?(isZoomed, animated)
self.refreshChartToolsClosure?(animated)
}
override func updateChartsVisibility(visibility: [Bool], animated: Bool) {
public override func updateChartsVisibility(visibility: [Bool], animated: Bool) {
if isZoomed {
zoomedBarsController.updateChartsVisibility(visibility: visibility, animated: animated)
} else {
@ -161,15 +160,20 @@ class StackedBarsChartController: BaseChartController {
return visibleCharts
}
override var actualChartVisibility: [Bool] {
public override var actualChartVisibility: [Bool] {
return isZoomed ? zoomedBarsController.chartVisibility : barsController.chartVisibility
}
override var actualChartsCollection: ChartsCollection {
return isZoomed ? zoomedBarsController.chartsCollection : barsController.chartsCollection
public override var actualChartsCollection: ChartsCollection {
let collection = isZoomed ? zoomedBarsController.chartsCollection : barsController.chartsCollection
if collection.isBlank {
return self.initialChartsCollection
}
return collection
}
override func chartInteractionDidBegin(point: CGPoint) {
public override func chartInteractionDidBegin(point: CGPoint) {
if isZoomed {
zoomedBarsController.chartInteractionDidBegin(point: point)
} else {
@ -177,7 +181,7 @@ class StackedBarsChartController: BaseChartController {
}
}
override func chartInteractionDidEnd() {
public override func chartInteractionDidEnd() {
if isZoomed {
zoomedBarsController.chartInteractionDidEnd()
} else {
@ -185,11 +189,7 @@ class StackedBarsChartController: BaseChartController {
}
}
override var drawChartVisibity: Bool {
return true
}
override var currentChartHorizontalRangeFraction: ClosedRange<CGFloat> {
public override var currentChartHorizontalRangeFraction: ClosedRange<CGFloat> {
if isZoomed {
return zoomedBarsController.currentChartHorizontalRangeFraction
} else {
@ -197,7 +197,7 @@ class StackedBarsChartController: BaseChartController {
}
}
override func cancelChartInteraction() {
public override func cancelChartInteraction() {
if isZoomed {
return zoomedBarsController.hideDetailsView(animated: true)
} else {
@ -205,7 +205,7 @@ class StackedBarsChartController: BaseChartController {
}
}
override func didTapZoomIn(date: Date) {
public override func didTapZoomIn(date: Date) {
guard isZoomed == false else { return }
if isZoomed {
return zoomedBarsController.hideDetailsView(animated: true)
@ -221,12 +221,21 @@ class StackedBarsChartController: BaseChartController {
})
}
override func didTapZoomOut() {
// 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)
}
override func updateChartRange(_ rangeFraction: ClosedRange<CGFloat>, animated: Bool) {
public override func updateChartRange(_ rangeFraction: ClosedRange<CGFloat>) {
if isZoomed {
return zoomedBarsController.chartRangeFractionDidUpdated(rangeFraction)
} else {
@ -234,10 +243,17 @@ class StackedBarsChartController: BaseChartController {
}
}
override func apply(colorMode: ColorMode, animated: Bool) {
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: Добавить ховеры на кнопки

View file

@ -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: Добавить ховеры на кнопки

View file

@ -26,6 +26,14 @@ class BarChartRenderer: BaseChartRenderer {
}
}
private var step = false
init(step: Bool = false) {
self.step = step
super.init()
}
var fillToTop: Bool = false
private(set) lazy var selectedIndexAnimator: AnimationController<CGFloat> = {
return AnimationController(current: 0, refreshClosure: self.refreshClosure)
@ -142,80 +150,157 @@ class BarChartRenderer: BaseChartRenderer {
context.restoreGState()
}
} else {
var selectedPaths: [[CGRect]] = bars.components.map { _ in [] }
barIndex = max(0, barIndex - 1)
var currentLocation = bars.locations[barIndex]
var leftX = transform(toChartCoordinateHorizontal: currentLocation - bars.barWidth, chartFrame: chartFrame)
var rightX: CGFloat = 0
let startPoint = CGPoint(x: leftX,
y: transform(toChartCoordinateVertical: verticalRange.current.lowerBound, chartFrame: chartFrame))
var backgourndPaths: [[CGPoint]] = bars.components.map { _ in Array() }
let itemsCount = ((bars.locations.count - barIndex) * 2) + 4
for path in backgourndPaths.indices {
backgourndPaths[path].reserveCapacity(itemsCount)
backgourndPaths[path].append(startPoint)
}
var maxValues: [CGFloat] = bars.components.map { _ in 0 }
while barIndex < bars.locations.count {
currentLocation = bars.locations[barIndex]
rightX = transform(toChartCoordinateHorizontal: currentLocation, chartFrame: chartFrame)
if self.step {
var selectedPaths: [[CGRect]] = bars.components.map { _ in [] }
barIndex = max(0, barIndex - 1)
var stackedValue: CGFloat = 0
var bottomY: CGFloat = transform(toChartCoordinateVertical: stackedValue, chartFrame: chartFrame)
for (index, component) in bars.components.enumerated() {
let visibilityPercent = componentsAnimators[index].current
if visibilityPercent == 0 { continue }
var currentLocation = bars.locations[barIndex]
var leftX = transform(toChartCoordinateHorizontal: currentLocation - bars.barWidth, chartFrame: chartFrame)
var rightX: CGFloat = 0
let startPoint = CGPoint(x: leftX,
y: transform(toChartCoordinateVertical: verticalRange.current.lowerBound, chartFrame: chartFrame))
var backgourndPaths: [[CGPoint]] = bars.components.map { _ in Array() }
let itemsCount = ((bars.locations.count - barIndex) * 2) + 4
for path in backgourndPaths.indices {
backgourndPaths[path].reserveCapacity(itemsCount)
backgourndPaths[path].append(startPoint)
}
var maxValues: [CGFloat] = bars.components.map { _ in 0 }
while barIndex < bars.locations.count {
currentLocation = bars.locations[barIndex]
rightX = transform(toChartCoordinateHorizontal: currentLocation, chartFrame: chartFrame)
let height = component.values[barIndex] * visibilityPercent
stackedValue += height
let topY = transform(toChartCoordinateVertical: stackedValue, chartFrame: chartFrame)
let componentHeight = (bottomY - topY)
maxValues[index] = max(maxValues[index], componentHeight)
if selectedBarIndex == barIndex {
let rect = CGRect(x: leftX,
y: topY,
width: rightX - leftX,
height: componentHeight)
selectedPaths[index].append(rect)
var stackedValue: CGFloat = 0
var bottomY: CGFloat = transform(toChartCoordinateVertical: stackedValue, chartFrame: chartFrame)
for (index, component) in bars.components.enumerated() {
let visibilityPercent = componentsAnimators[index].current
if visibilityPercent == 0 { continue }
var value = component.values[barIndex]
if value < 5000 {
value *= 40
}
let height = value * visibilityPercent
// stackedValue += height
let topY = transform(toChartCoordinateVertical: height, chartFrame: chartFrame)
let componentHeight = (bottomY - topY)
maxValues[index] = max(maxValues[index], componentHeight)
if selectedBarIndex == barIndex {
let rect = CGRect(x: leftX,
y: topY,
width: rightX - leftX,
height: componentHeight)
selectedPaths[index].append(rect)
}
backgourndPaths[index].append(CGPoint(x: leftX, y: topY))
backgourndPaths[index].append(CGPoint(x: rightX, y: topY))
// bottomY = topY
}
backgourndPaths[index].append(CGPoint(x: leftX, y: topY))
backgourndPaths[index].append(CGPoint(x: rightX, y: topY))
bottomY = topY
if currentLocation > range.upperBound {
break
}
leftX = rightX
barIndex += 1
}
if currentLocation > range.upperBound {
break
}
leftX = rightX
barIndex += 1
}
let endPoint = CGPoint(x: transform(toChartCoordinateHorizontal: currentLocation, chartFrame: chartFrame).roundedUpToPixelGrid(),
y: transform(toChartCoordinateVertical: verticalRange.current.lowerBound, chartFrame: chartFrame))
let colorOffset = Double((1.0 - (1.0 - generalUnselectedAlpha) * selectedIndexAnimator.current) * chartsAlpha)
for (index, component) in bars.components.enumerated().reversed() {
if maxValues[index] < optimizationLevel {
continue
}
context.saveGState()
backgourndPaths[index].append(endPoint)
context.setFillColor(GColor.valueBetween(start: backgroundColorAnimator.current.color,
end: component.color,
offset: colorOffset).cgColor)
context.beginPath()
context.addLines(between: backgourndPaths[index])
context.closePath()
context.fillPath()
context.restoreGState()
}
for (index, component) in bars.components.enumerated().reversed() {
context.setFillColor(component.color.withAlphaComponent(chartsAlpha * component.color.alphaValue).cgColor)
context.fill(selectedPaths[index])
let endPoint = CGPoint(x: transform(toChartCoordinateHorizontal: currentLocation, chartFrame: chartFrame).roundedUpToPixelGrid(),
y: transform(toChartCoordinateVertical: verticalRange.current.lowerBound, chartFrame: chartFrame))
let colorOffset = Double((1.0 - (1.0 - generalUnselectedAlpha) * selectedIndexAnimator.current) * chartsAlpha)
for (index, component) in bars.components.enumerated().reversed() {
if maxValues[index] < optimizationLevel {
continue
}
context.saveGState()
context.setLineWidth(2.0)
context.setStrokeColor(GColor.valueBetween(start: backgroundColorAnimator.current.color,
end: component.color,
offset: colorOffset).cgColor)
context.beginPath()
context.addLines(between: backgourndPaths[index])
context.strokePath()
context.restoreGState()
}
} else {
var selectedPaths: [[CGRect]] = bars.components.map { _ in [] }
barIndex = max(0, barIndex - 1)
var currentLocation = bars.locations[barIndex]
var leftX = transform(toChartCoordinateHorizontal: currentLocation - bars.barWidth, chartFrame: chartFrame)
var rightX: CGFloat = 0
let startPoint = CGPoint(x: leftX,
y: transform(toChartCoordinateVertical: verticalRange.current.lowerBound, chartFrame: chartFrame))
var backgourndPaths: [[CGPoint]] = bars.components.map { _ in Array() }
let itemsCount = ((bars.locations.count - barIndex) * 2) + 4
for path in backgourndPaths.indices {
backgourndPaths[path].reserveCapacity(itemsCount)
backgourndPaths[path].append(startPoint)
}
var maxValues: [CGFloat] = bars.components.map { _ in 0 }
while barIndex < bars.locations.count {
currentLocation = bars.locations[barIndex]
rightX = transform(toChartCoordinateHorizontal: currentLocation, chartFrame: chartFrame)
var stackedValue: CGFloat = 0
var bottomY: CGFloat = transform(toChartCoordinateVertical: stackedValue, chartFrame: chartFrame)
for (index, component) in bars.components.enumerated() {
let visibilityPercent = componentsAnimators[index].current
if visibilityPercent == 0 { continue }
let height = component.values[barIndex] * visibilityPercent
stackedValue += height
let topY = transform(toChartCoordinateVertical: stackedValue, chartFrame: chartFrame)
let componentHeight = (bottomY - topY)
maxValues[index] = max(maxValues[index], componentHeight)
if selectedBarIndex == barIndex {
let rect = CGRect(x: leftX,
y: topY,
width: rightX - leftX,
height: componentHeight)
selectedPaths[index].append(rect)
}
backgourndPaths[index].append(CGPoint(x: leftX, y: topY))
backgourndPaths[index].append(CGPoint(x: rightX, y: topY))
bottomY = topY
}
if currentLocation > range.upperBound {
break
}
leftX = rightX
barIndex += 1
}
let endPoint = CGPoint(x: transform(toChartCoordinateHorizontal: currentLocation, chartFrame: chartFrame).roundedUpToPixelGrid(),
y: transform(toChartCoordinateVertical: verticalRange.current.lowerBound, chartFrame: chartFrame))
let colorOffset = Double((1.0 - (1.0 - generalUnselectedAlpha) * selectedIndexAnimator.current) * chartsAlpha)
for (index, component) in bars.components.enumerated().reversed() {
if maxValues[index] < optimizationLevel {
continue
}
context.saveGState()
backgourndPaths[index].append(endPoint)
context.setFillColor(GColor.valueBetween(start: backgroundColorAnimator.current.color,
end: component.color,
offset: colorOffset).cgColor)
context.beginPath()
context.addLines(between: backgourndPaths[index])
context.closePath()
context.fillPath()
context.restoreGState()
}
for (index, component) in bars.components.enumerated().reversed() {
context.setFillColor(component.color.withAlphaComponent(chartsAlpha * component.color.alphaValue).cgColor)
context.fill(selectedPaths[index])
}
}
}
}

View file

@ -51,7 +51,7 @@ class BaseChartRenderer: ChartViewRenderer {
lazy var horizontalRange = AnimationController<ClosedRange<CGFloat>>(current: 0...1, refreshClosure: refreshClosure)
lazy var verticalRange = AnimationController<ClosedRange<CGFloat>>(current: 0...1, refreshClosure: refreshClosure)
func setup(verticalRange: ClosedRange<CGFloat>, animated: Bool, timeFunction: TimeFunction? = nil) {
func setup(verticalRange: ClosedRange<CGFloat>, animated: Bool, timeFunction: TimeFunction? = nil) {
guard self.verticalRange.end != verticalRange else {
self.verticalRange.timeFunction = timeFunction ?? .linear
return

View file

@ -1,5 +1,5 @@
//
// LineBulletsRenerer.swift
// LineBulletsRenderer.swift
// GraphTest
//
// Created by Andrei Salavei on 4/8/19.
@ -13,7 +13,7 @@ import Cocoa
import UIKit
#endif
class LineBulletsRenerer: BaseChartRenderer {
class LineBulletsRenderer: BaseChartRenderer {
struct Bullet {
var coordinate: CGPoint
var color: GColor

View file

@ -0,0 +1,19 @@
//
// GraphCore.h
// GraphCore
//
// Created by Mikhail Filimonov on 03.02.2020.
// Copyright © 2020 Telegram. All rights reserved.
//
#import <Cocoa/Cocoa.h>
//! Project version number for GraphCore.
FOUNDATION_EXPORT double GraphCoreVersionNumber;
//! Project version string for GraphCore.
FOUNDATION_EXPORT const unsigned char GraphCoreVersionString[];
// In this header, you should import all the public headers of your framework using statements like #import <GraphCore/PublicHeader.h>

Some files were not shown because too many files have changed in this diff Show more