mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-05 19:28:46 +02:00
Channel statistics improvements
This commit is contained in:
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.
BIN
Telegram-iOS/Resources/Dice_Rolling.tgs
Normal file
BIN
Telegram-iOS/Resources/Dice_Rolling.tgs
Normal file
Binary file not shown.
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
||||
|
||||
|
|
@ -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) {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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: Добавить ховеры на кнопки
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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))) ?? ""
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
15
submodules/GraphCore/BUCK
Normal 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",
|
||||
],
|
||||
)
|
||||
|
|
@ -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>
|
||||
|
||||
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -45,7 +45,6 @@ public class BaseLinesChartController: BaseChartController {
|
|||
}
|
||||
|
||||
func updateChartRangeTitle(animated: Bool) {
|
||||
|
||||
let range: ClosedRange<CGFloat>
|
||||
if zoomedChartRange == BaseConstants.defaultRange {
|
||||
range = initialChartRange
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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() {
|
||||
|
|
@ -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) {
|
||||
|
|
@ -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,
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
@ -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: Добавить ховеры на кнопки
|
||||
|
|
@ -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: Добавить ховеры на кнопки
|
||||
|
|
@ -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])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
19
submodules/GraphCore/Sources/GraphCore.h
Normal file
19
submodules/GraphCore/Sources/GraphCore.h
Normal 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
Loading…
Add table
Add a link
Reference in a new issue