diff --git a/Bank/AccountViewController.swift b/Bank/AccountViewController.swift index fc1bfb6..d30485a 100644 --- a/Bank/AccountViewController.swift +++ b/Bank/AccountViewController.swift @@ -8,14 +8,17 @@ import UIKit -class AccountViewController: UITableViewController { - var account: Account! +class AccountViewController: UITableViewController, AccountHeaderViewDelegate { + weak var delegate: AccountViewControllerDelegate? + var account: Account!, index: (Int, Int)? override func viewDidLoad() { super.viewDidLoad() // Setup header view headerView.account = account + headerView.accountIndex = index + headerView.delegate = self // Populate with initial transaction data populateData() // Listen for transaction data changes @@ -62,4 +65,14 @@ class AccountViewController: UITableViewController { static func get() -> AccountViewController { return UIStoryboard(name: "Main", bundle: .main).instantiateViewController(withIdentifier: "Account") as! AccountViewController } + + // MARK: - Account Header View Delegate + + func shouldMove(to index: Int) { + delegate?.shouldMove(to: index) + } +} + +protocol AccountViewControllerDelegate: class { + func shouldMove(to index: Int) } diff --git a/Bank/Base.lproj/Main.storyboard b/Bank/Base.lproj/Main.storyboard index da23231..ea59479 100644 --- a/Bank/Base.lproj/Main.storyboard +++ b/Bank/Base.lproj/Main.storyboard @@ -155,10 +155,10 @@ - + @@ -166,35 +166,52 @@ - - + + - - - + + - + + + - + - + + + + + + + @@ -202,7 +219,7 @@ - + @@ -213,6 +230,7 @@ + @@ -272,6 +290,7 @@ + diff --git a/Bank/ViewController.swift b/Bank/ViewController.swift index 1ff7155..217e1df 100644 --- a/Bank/ViewController.swift +++ b/Bank/ViewController.swift @@ -8,7 +8,7 @@ import UIKit -class ViewController: UIPageViewController, UIPageViewControllerDataSource, NoAccountsViewControllerDelegate { +class ViewController: UIPageViewController, UIPageViewControllerDataSource, AccountViewControllerDelegate, NoAccountsViewControllerDelegate { var statusViewController: StatusViewController? var isExchanging = false @@ -61,9 +61,17 @@ class ViewController: UIPageViewController, UIPageViewControllerDataSource, NoAc determineStatus() } + var currentIndex: Int? { + guard let account = (viewControllers?.first as? AccountViewController)?.account else { return nil } + return SessionDataStorage.shared.accounts?.index(of: account) + } + func getViewController(for index: Int) -> AccountViewController { let vc = AccountViewController.get() vc.account = SessionDataStorage.shared.accounts?[index] + let pageCount = SessionDataStorage.shared.accounts?.count ?? 0 + vc.index = pageCount > 1 ? (index, pageCount) : nil + vc.delegate = self return vc } @@ -80,27 +88,8 @@ class ViewController: UIPageViewController, UIPageViewControllerDataSource, NoAc } } - /// Whether or not swiping to go to the previous/next page is enabled. - var isSwipingEnabled = true { - didSet { - view.subviews.forEach { view in - if let scrollView = view as? UIScrollView { scrollView.isScrollEnabled = isSwipingEnabled } - } - } - } - // MARK: - Page View Controller Data Source - func presentationCount(for pageViewController: UIPageViewController) -> Int { - let count = SessionDataStorage.shared.accounts?.count ?? 0 - return count > 1 ? count : 0 - } - - func presentationIndex(for pageViewController: UIPageViewController) -> Int { - guard let account = ((pageViewController.viewControllers?.first) as? AccountViewController)?.account else { return 0 } - return SessionDataStorage.shared.accounts?.index(of: account) ?? 0 - } - func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { guard let account = (viewController as? AccountViewController)?.account, let index = SessionDataStorage.shared.accounts?.index(of: account) else { return nil } let newIndex = index + 1 @@ -131,4 +120,11 @@ class ViewController: UIPageViewController, UIPageViewControllerDataSource, NoAc statusViewController = vc } } + + // MARK: - Account View Controller Delegate + + func shouldMove(to index: Int) { + guard let currentIndex = self.currentIndex, currentIndex != index else { return } + setViewControllers([getViewController(for: index)], direction: index > currentIndex ? .forward : .reverse, animated: true, completion: nil) + } } diff --git a/Bank/Views/AccountHeaderView.swift b/Bank/Views/AccountHeaderView.swift index cd62c48..2d1e5bd 100644 --- a/Bank/Views/AccountHeaderView.swift +++ b/Bank/Views/AccountHeaderView.swift @@ -10,11 +10,13 @@ import UIKit import SwiftChart class AccountHeaderView: UIView { + weak var delegate: AccountHeaderViewDelegate? @IBOutlet weak var amountLabel: AmountLabel! @IBOutlet weak var nameLabel: UILabel! @IBOutlet weak var institutionLabel: UILabel! @IBOutlet weak var chartContainerView: UIView! @IBOutlet weak var chartLoadingView: UIActivityIndicatorView! + @IBOutlet weak var pageControl: UIPageControl! var hasAwaken = false var chart: Chart! @@ -45,10 +47,18 @@ class AccountHeaderView: UIView { } } + var accountIndex: (Int, Int)? { + didSet { + if hasAwaken { setupData() } + } + } + @objc func setupData() { amountLabel.amount = account?.displayBalance ?? 0 nameLabel.text = account?.name ?? "Account Name" institutionLabel.text = account?.institutionDescription ?? "Institution Name" + pageControl.numberOfPages = accountIndex?.1 ?? 1 + pageControl.currentPage = accountIndex?.0 ?? 0 setupChart() } @@ -66,4 +76,16 @@ class AccountHeaderView: UIView { } } } + + // MARK: - Event Handlers + + @IBAction func pageControlChanged(_ sender: Any) { + delegate?.shouldMove(to: pageControl.currentPage) + pageControl.currentPage = accountIndex?.0 ?? 0 + } + +} + +protocol AccountHeaderViewDelegate: class { + func shouldMove(to index: Int) } diff --git a/Pods/SwiftChart/LICENSE.txt b/Pods/SwiftChart/LICENSE.txt new file mode 100644 index 0000000..1827497 --- /dev/null +++ b/Pods/SwiftChart/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Giampaolo Bellavite + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Pods/SwiftChart/README.md b/Pods/SwiftChart/README.md new file mode 100644 index 0000000..b5dfb40 --- /dev/null +++ b/Pods/SwiftChart/README.md @@ -0,0 +1,236 @@ +SwiftChart +=========== + +[![Version](https://img.shields.io/cocoapods/v/SwiftChart.svg?style=flat)](http://cocoapods.org/pods/SwiftChart) +[![License](https://img.shields.io/cocoapods/l/SwiftChart.svg?style=flat)](http://cocoapods.org/pods/SwiftChart) +[![Platform](https://img.shields.io/cocoapods/p/SwiftChart.svg?style=flat)](http://cocoapods.org/pods/SwiftChart) + +A simple line / area charting library for iOS, written in Swift. + +📈 Line and area charts +🌞 Multiple series +🌒 Partially filled series +🏊 Works with signed floats +🖖 Touch events + +

+ + +

+ +## Installation + +### CocoaPods + +SwiftChart is available through [CocoaPods](http://cocoapods.org). To install +it, simply add the following line to your Podfile: + +```ruby +pod "SwiftChart" +``` + +### Manually + +1. Download **SwiftChart.zip** from the [last release](https://github.com/gpbl/SwiftChart/releases/latest) and extract its content in your project's folder. +2. From the Xcode project, choose *Add Files to ...* from the *File* menu and add the extracted files. + +## Usage + +The library includes: + +- the [Chart](Source/Chart.swift#L40) main class, to initialize and configure the chart’s content, e.g. for adding series or setting up the its appearance +- the [ChartSeries](Source/ChartSeries.swift) class, for creating datasets and configure their appearance +- the [ChartDelegate](Source/Chart.swift#L10-L32) protocol, which tells other objects about the chart’s touch events +- the [ChartColor](Source/ChartColors.swift) struct, containing some predefined colors + +**Example** + +```swift +let chart = Chart() +let series = ChartSeries([0, 6, 2, 8, 4, 7, 3, 10, 8]) +series.color = ChartColors.greenColor() +chart.add(series) +``` + +To run the example project, clone the repo, and run `pod install` from the Example directory first. + +### To initialize a chart + +#### From the Interface Builder + +The chart can be initialized from the Interface Builder. Drag a normal View into a View Controller and assign to it the `Chart` Custom Class from the Identity Inspector: + +![Example](https://cloud.githubusercontent.com/assets/120693/5063826/c01f26d2-6df6-11e4-8122-cb086709d96c.png) + +> Parts of the chart’s appearance can be set from the Attribute Inspector. + +#### By coding + +To initialize a chart programmatically, use the `Chart(frame: ...)` initializer, which requires a `frame`: + +```swift +let chart = Chart(frame: CGRect(x: 0, y: 0, width: 200, height: 100)) +``` + +If you prefer to use Autolayout, set the frame to `0` and add the constraints later: + +```swift +let chart = Chart(frame: CGRectZero) +// add constraints now +``` + +### Adding series + +Initialize each series before adding them to the chart. To do so, pass an array to initialize a `ChartSeries` object: + +```swift +let series = ChartSeries([0, 6.5, 2, 8, 4.1, 7, -3.1, 10, 8]) +chart.add(series) +``` + +By default, the values on the x-axis are the progressive indexes of the passed array. You can customize those values by passing an array of `(x: Float, y: Float)` touples to the series’ initializer: + +```swift +// Create a new series specifying x and y values +let data = [(x: 0, y: 0), (x: 0.5, y: 3.1), (x: 1.2, y: 2), (x: 2.1, y: -4.2), (x: 2.6, y: 1.1)] +let series = ChartSeries(data) +chart.add(series) +``` + +#### Multiple series + +Using the `chart.add(series: ChartSeries)` and `chart.add(series: Array)` methods you can add more series. Those will be indentified with a progressive index in the chart’s `series` property. + +#### Partially filled series + +Use the `chart.xLabels` property to make the x-axis wider than the actual data. For example, + +```swift +let chart = Chart(frame: CGRect(x: 0, y: 0, width: 200, height: 100)) +let data = [(x: 0.0, y: 0), (x: 3, y: 2.5), (x: 4, y: 2), (x: 5, y: 2.3), (x: 7, y: 3), (x: 8, y: 2.2), (x: 9, y: 2.5)] +let series = ChartSeries(data: data) +series.area = true +chart.xLabels = [0, 3, 6, 9, 12, 15, 18, 21, 24] +chart.xLabelsFormatter = { String(Int(round($1))) + "h" } +chart.add(series) +``` + +will render: + + + +## Touch events + +To make the chart respond to touch events, implement the `ChartDelegate` protocol in your classes, as a View Controller, and set the chart’s `delegate` property: + +```swift +class MyViewController: UIViewController, ChartDelegate { + override func viewDidLoad() { + let chart = Chart(frame: CGRect(x: 0, y: 0, width: 100, height: 200)) + chart.delegate = self + } + + // Chart delegate + func didTouchChart(chart: Chart, indexes: Array, x: Float, left: CGFloat) { + // Do something on touch + } + + func didFinishTouchingChart(chart: Chart) { + // Do something when finished + } + + func didEndTouchingChart(chart: Chart) { + // Do something when ending touching chart + } +} +``` + +The `didTouchChart` method passes an array of indexes, one for each series, with an optional `Int` referring to the data’s index: + +```swift + func didTouchChart(chart: Chart, indexes: Array, x: Float, left: CGFloat) { + for (serieIndex, dataIndex) in enumerate(indexes) { + if dataIndex != nil { + // The series at serieIndex has been touched + let value = chart.valueForSeries(serieIndex, atIndex: dataIndex) + } + } + } +``` + +You can use `chart.valueForSeries()` to access the value for the touched position. + +The `x: Float` argument refers to the value on the x-axis: it is inferred from the horizontal position of the touch event, and may be not part of the series values. + +The `left: CGFloat` is the x position on the chart’s view, starting from the left side. It may be used to set the position for a label moving above the chart: + + + +## Common issues and solutions + +If you have issue with this library, please tag your question with `swiftcharts` on [Stack Overflow](http://stackoverflow.com/tags/swiftcharts/info). + +### The chart is not showing + +The `Chart` class inherits from `UIView`, so if your chart is not displaying it is likely a problem related to the view's size. Check your view constraints and make sure you initialize it on `viewDidLoad`, when UIKit can calculate the view dimensions. + +Some tips for debugging an hidden chart: + +* start your app and then debug the UI Hierarchy from the Debug navigator +* initialize a simple UIView with a colored background instead of the chart to easily see how the view is positioned +* try to not to nest the chart in a subview for better debugging + +## Reference + +![reference](https://cloud.githubusercontent.com/assets/120693/5094993/e3a3e10e-6f65-11e4-8619-b7a05d18190e.png) + +### Chart class + +#### Chart options + +* `areaAlphaComponent` – alpha factor for the area’s color. +* `axesColor` – the axes’ color. +* `bottomInset` – height of the area at the bottom of the chart, containing the labels for the x-axis. +* `delegate` – the delegate for listening to touch events. +* `highlightLineColor` – color of the highlight line. +* `highlightLineWidth` – width of the highlight line. +* `gridColor` – the grid color. +* `labelColor` – the color of the labels. +* `labelFont` – the font used for the labels. +* `lineWidth` – width of the chart’s lines. +* `maxX` – custom maximum x-value. +* `maxY` – custom maximum y-value. +* `minX` – minimum x-value. +* `minY` – minimum y-value. +* `topInset` – height of the area at the top of the chart, acting a padding to make place for the top y-axis label. +* `xLabelsFormatter` – formats the labels on the x-axis. +* `xLabelsOrientation` – sets the x-axis labels orientation to vertical or horizontal. +* `xLabelsTextAlignment` – text-alignment for the x-labels. +* `xLabelsSkipLast` (default `true`) - Skip the last x-label. Setting this to `false` will make the label overflow the frame width, so use carefully! +* `yLabelsFormatter` – formats the labels on the y-axis. +* `yLabelsOnRightSide` – place the y-labels on the right side. + +#### Methods + +* `add(series: ChartSeries)` – add a series to the chart. +* `removeSeries()` – remove all the series from the chart. +* `removeSeriesAtIndex(index: Int)` – remove a series at the specified index. +* `valueForSeries()` – get the value of the specified series at the specified index. + +### ChartSeries class + +* `area` – draws an area below the series’ line. +* `line` – set it to `false` to hide the line (useful for drawing only the area). +* `color` – the series color. +* `colors` – a touple to specify the color above or below the zero. For example, `(above: ChartsColors.redColor(), below: ChartsColors.blueColor(), -4)` will use red for values above `-4`, and blue for values below -4. + +### ChartDelegate + +* `didTouchChart` – tells the delegate that the specified chart has been touched. +* `didFinishTouchingChart` – tells the delegate that the user finished touching the chart. The user will "finish" touching the chart only swiping left/right outside the chart. +* `didEndTouchingChart` – tells the delegate that the user ended touching the chart. The user will "end" touching the chart whenever the touchesDidEnd method is being called. + + +## License + +SwiftChart is available under the MIT license. See the LICENSE file for more info. diff --git a/Pods/SwiftChart/Source/Chart.swift b/Pods/SwiftChart/Source/Chart.swift new file mode 100644 index 0000000..763b830 --- /dev/null +++ b/Pods/SwiftChart/Source/Chart.swift @@ -0,0 +1,847 @@ +// +// Chart.swift +// +// Created by Giampaolo Bellavite on 07/11/14. +// Copyright (c) 2014 Giampaolo Bellavite. All rights reserved. +// + +import UIKit + +public protocol ChartDelegate: class { + + /** + Tells the delegate that the specified chart has been touched. + + - parameter chart: The chart that has been touched. + - parameter indexes: Each element of this array contains the index of the data that has been touched, one for each + series. If the series hasn't been touched, its index will be nil. + - parameter x: The value on the x-axis that has been touched. + - parameter left: The distance from the left side of the chart. + + */ + func didTouchChart(_ chart: Chart, indexes: [Int?], x: Float, left: CGFloat) + + /** + Tells the delegate that the user finished touching the chart. The user will + "finish" touching the chart only swiping left/right outside the chart. + + - parameter chart: The chart that has been touched. + + */ + func didFinishTouchingChart(_ chart: Chart) + /** + Tells the delegate that the user ended touching the chart. The user + will "end" touching the chart whenever the touchesDidEnd method is + being called. + + - parameter chart: The chart that has been touched. + + */ + func didEndTouchingChart(_ chart: Chart) +} + +/** +Represent the x- and the y-axis values for each point in a chart series. +*/ +typealias ChartPoint = (x: Float, y: Float) + +public enum ChartLabelOrientation { + case horizontal + case vertical +} + +@IBDesignable +open class Chart: UIControl { + + // MARK: Options + + @IBInspectable + open var identifier: String? + + /** + Series to display in the chart. + */ + open var series: [ChartSeries] = [] { + didSet { + setNeedsDisplay() + } + } + + /** + The values to display as labels on the x-axis. You can format these values with the `xLabelFormatter` attribute. + As default, it will display the values of the series which has the most data. + */ + open var xLabels: [Float]? + + /** + Formatter for the labels on the x-axis. The `index` represents the `xLabels` index, `value` its value: + */ + open var xLabelsFormatter = { (labelIndex: Int, labelValue: Float) -> String in + String(Int(labelValue)) + } + + /** + Text alignment for the x-labels + */ + open var xLabelsTextAlignment: NSTextAlignment = .left + + /** + Orientation for the x-labels + */ + open var xLabelsOrientation: ChartLabelOrientation = .horizontal + + /** + Skip the last x-label. Setting this to false may make the label overflow the frame width. + */ + open var xLabelsSkipLast: Bool = true + + /** + Values to display as labels of the y-axis. If not specified, will display the + lowest, the middle and the highest values. + */ + open var yLabels: [Float]? + + /** + Formatter for the labels on the y-axis. + */ + open var yLabelsFormatter = { (labelIndex: Int, labelValue: Float) -> String in + String(Int(labelValue)) + } + + /** + Displays the y-axis labels on the right side of the chart. + */ + open var yLabelsOnRightSide: Bool = false + + /** + Font used for the labels. + */ + open var labelFont: UIFont? = UIFont.systemFont(ofSize: 12) + + /** + Font used for the labels. + */ + @IBInspectable + open var labelColor: UIColor = UIColor.black + + /** + Color for the axes. + */ + @IBInspectable + open var axesColor: UIColor = UIColor.gray.withAlphaComponent(0.3) + + /** + Color for the grid. + */ + @IBInspectable + open var gridColor: UIColor = UIColor.gray.withAlphaComponent(0.3) + /** + Should draw lines for labels on X axis. + */ + open var showXLabelsAndGrid: Bool = true + /** + Should draw lines for labels on Y axis. + */ + open var showYLabelsAndGrid: Bool = true + + /** + Height of the area at the bottom of the chart, containing the labels for the x-axis. + */ + open var bottomInset: CGFloat = 20 + + /** + Height of the area at the top of the chart, acting a padding to make place for the top y-axis label. + */ + open var topInset: CGFloat = 20 + + /** + Width of the chart's lines. + */ + @IBInspectable + open var lineWidth: CGFloat = 2 + + /** + Delegate for listening to Chart touch events. + */ + weak open var delegate: ChartDelegate? + + /** + Custom minimum value for the x-axis. + */ + open var minX: Float? + + /** + Custom minimum value for the y-axis. + */ + open var minY: Float? + + /** + Custom maximum value for the x-axis. + */ + open var maxX: Float? + + /** + Custom maximum value for the y-axis. + */ + open var maxY: Float? + + /** + Color for the highlight line. + */ + open var highlightLineColor = UIColor.gray + + /** + Width for the highlight line. + */ + open var highlightLineWidth: CGFloat = 0.5 + + /** + Alpha component for the area's color. + */ + open var areaAlphaComponent: CGFloat = 0.1 + + // MARK: Private variables + + fileprivate var highlightShapeLayer: CAShapeLayer! + fileprivate var layerStore: [CAShapeLayer] = [] + + fileprivate var drawingHeight: CGFloat! + fileprivate var drawingWidth: CGFloat! + + // Minimum and maximum values represented in the chart + fileprivate var min: ChartPoint! + fileprivate var max: ChartPoint! + + // Represent a set of points corresponding to a segment line on the chart. + typealias ChartLineSegment = [ChartPoint] + + // MARK: initializations + + override public init(frame: CGRect) { + super.init(frame: frame) + commonInit() + } + + required public init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + commonInit() + } + + convenience public init() { + self.init(frame: .zero) + commonInit() + } + + private func commonInit() { + backgroundColor = UIColor.clear + contentMode = .redraw // redraw rects on bounds change + } + + override open func draw(_ rect: CGRect) { + #if TARGET_INTERFACE_BUILDER + drawIBPlaceholder() + #else + drawChart() + #endif + } + + /** + Adds a chart series. + */ + open func add(_ series: ChartSeries) { + self.series.append(series) + } + + /** + Adds multiple series. + */ + open func add(_ series: [ChartSeries]) { + for s in series { + add(s) + } + } + + /** + Remove the series at the specified index. + */ + open func removeSeriesAt(_ index: Int) { + series.remove(at: index) + } + + /** + Remove all the series. + */ + open func removeAllSeries() { + series = [] + } + + /** + Returns the value for the specified series at the given index + */ + open func valueForSeries(_ seriesIndex: Int, atIndex dataIndex: Int?) -> Float? { + if dataIndex == nil { return nil } + let series = self.series[seriesIndex] as ChartSeries + return series.data[dataIndex!].y + } + + fileprivate func drawIBPlaceholder() { + let placeholder = UIView(frame: self.frame) + placeholder.backgroundColor = UIColor(red: 0.93, green: 0.93, blue: 0.93, alpha: 1) + let label = UILabel() + label.text = "Chart" + label.font = UIFont.systemFont(ofSize: 28) + label.textColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.2) + label.sizeToFit() + label.frame.origin.x += frame.width/2 - (label.frame.width / 2) + label.frame.origin.y += frame.height/2 - (label.frame.height / 2) + + placeholder.addSubview(label) + addSubview(placeholder) + } + + fileprivate func drawChart() { + + drawingHeight = bounds.height - bottomInset - topInset + drawingWidth = bounds.width + + let minMax = getMinMax() + min = minMax.min + max = minMax.max + + highlightShapeLayer = nil + + // Remove things before drawing, e.g. when changing orientation + + for view in self.subviews { + view.removeFromSuperview() + } + for layer in layerStore { + layer.removeFromSuperlayer() + } + layerStore.removeAll() + + // Draw content + + for (index, series) in self.series.enumerated() { + + // Separate each line in multiple segments over and below the x axis + let segments = Chart.segmentLine(series.data as ChartLineSegment, zeroLevel: series.colors.zeroLevel) + + segments.forEach({ segment in + let scaledXValues = scaleValuesOnXAxis( segment.map({ return $0.x }) ) + let scaledYValues = scaleValuesOnYAxis( segment.map({ return $0.y }) ) + + if series.line { + drawLine(scaledXValues, yValues: scaledYValues, seriesIndex: index) + } + if series.area { + drawArea(scaledXValues, yValues: scaledYValues, seriesIndex: index) + } + }) + } + + drawAxes() + + if showXLabelsAndGrid && (xLabels != nil || series.count > 0) { + drawLabelsAndGridOnXAxis() + } + if showYLabelsAndGrid && (yLabels != nil || series.count > 0) { + drawLabelsAndGridOnYAxis() + } + + } + + // MARK: - Scaling + + fileprivate func getMinMax() -> (min: ChartPoint, max: ChartPoint) { + + // Start with user-provided values + + var min = (x: minX, y: minY) + var max = (x: maxX, y: maxY) + + // Check in datasets + + for series in self.series { + let xValues = series.data.map({ (point: ChartPoint) -> Float in + return point.x }) + let yValues = series.data.map({ (point: ChartPoint) -> Float in + return point.y }) + + let newMinX = xValues.min()! + let newMinY = yValues.min()! + let newMaxX = xValues.max()! + let newMaxY = yValues.max()! + + if min.x == nil || newMinX < min.x! { min.x = newMinX } + if min.y == nil || newMinY < min.y! { min.y = newMinY } + if max.x == nil || newMaxX > max.x! { max.x = newMaxX } + if max.y == nil || newMaxY > max.y! { max.y = newMaxY } + } + + // Check in labels + + if xLabels != nil { + let newMinX = (xLabels!).min()! + let newMaxX = (xLabels!).max()! + if min.x == nil || newMinX < min.x! { min.x = newMinX } + if max.x == nil || newMaxX > max.x! { max.x = newMaxX } + } + + if yLabels != nil { + let newMinY = (yLabels!).min()! + let newMaxY = (yLabels!).max()! + if min.y == nil || newMinY < min.y! { min.y = newMinY } + if max.y == nil || newMaxY > max.y! { max.y = newMaxY } + } + + if min.x == nil { min.x = 0 } + if min.y == nil { min.y = 0 } + if max.x == nil { max.x = 0 } + if max.y == nil { max.y = 0 } + + return (min: (x: min.x!, y: min.y!), max: (x: max.x!, max.y!)) + + } + + fileprivate func scaleValuesOnXAxis(_ values: [Float]) -> [Float] { + let width = Float(drawingWidth) + + var factor: Float + if max.x - min.x == 0 { + factor = 0 + } else { + factor = width / (max.x - min.x) + } + + let scaled = values.map { factor * ($0 - self.min.x) } + return scaled + } + + fileprivate func scaleValuesOnYAxis(_ values: [Float]) -> [Float] { + + let height = Float(drawingHeight) + var factor: Float + if max.y - min.y == 0 { + factor = 0 + } else { + factor = height / (max.y - min.y) + } + + let scaled = values.map { Float(self.topInset) + height - factor * ($0 - self.min.y) } + + return scaled + } + + fileprivate func scaleValueOnYAxis(_ value: Float) -> Float { + + let height = Float(drawingHeight) + var factor: Float + if max.y - min.y == 0 { + factor = 0 + } else { + factor = height / (max.y - min.y) + } + + let scaled = Float(self.topInset) + height - factor * (value - min.y) + return scaled + } + + fileprivate func getZeroValueOnYAxis(zeroLevel: Float) -> Float { + if min.y > zeroLevel { + return scaleValueOnYAxis(min.y) + } else { + return scaleValueOnYAxis(zeroLevel) + } + + } + + // MARK: - Drawings + + fileprivate func drawLine(_ xValues: [Float], yValues: [Float], seriesIndex: Int) { + // YValues are "reverted" from top to bottom, so 'above' means <= level + let isAboveZeroLine = yValues.max()! <= self.scaleValueOnYAxis(series[seriesIndex].colors.zeroLevel) + let path = CGMutablePath() + path.move(to: CGPoint(x: CGFloat(xValues.first!), y: CGFloat(yValues.first!))) + for i in 1.. 0 { + let y = CGFloat(getZeroValueOnYAxis(zeroLevel: 0)) + context.move(to: CGPoint(x: CGFloat(0), y: y)) + context.addLine(to: CGPoint(x: CGFloat(drawingWidth), y: y)) + context.strokePath() + } + + // vertical axis on the left + context.move(to: CGPoint(x: CGFloat(0), y: CGFloat(0))) + context.addLine(to: CGPoint(x: CGFloat(0), y: drawingHeight + topInset)) + context.strokePath() + + // vertical axis on the right + context.move(to: CGPoint(x: CGFloat(drawingWidth), y: CGFloat(0))) + context.addLine(to: CGPoint(x: CGFloat(drawingWidth), y: drawingHeight + topInset)) + context.strokePath() + + } + + fileprivate func drawLabelsAndGridOnXAxis() { + + let context = UIGraphicsGetCurrentContext()! + context.setStrokeColor(gridColor.cgColor) + context.setLineWidth(0.5) + + var labels: [Float] + if xLabels == nil { + // Use labels from the first series + labels = series[0].data.map({ (point: ChartPoint) -> Float in + return point.x }) + } else { + labels = xLabels! + } + + let scaled = scaleValuesOnXAxis(labels) + let padding: CGFloat = 5 + scaled.enumerated().forEach { (i, value) in + let x = CGFloat(value) + let isLastLabel = x == drawingWidth + + // Add vertical grid for each label, except axes on the left and right + + if x != 0 && x != drawingWidth { + context.move(to: CGPoint(x: x, y: CGFloat(0))) + context.addLine(to: CGPoint(x: x, y: bounds.height)) + context.strokePath() + } + + if xLabelsSkipLast && isLastLabel { + // Do not add label at the most right position + return + } + + // Add label + let label = UILabel(frame: CGRect(x: x, y: drawingHeight, width: 0, height: 0)) + label.font = labelFont + label.text = xLabelsFormatter(i, labels[i]) + label.textColor = labelColor + + // Set label size + label.sizeToFit() + // Center label vertically + label.frame.origin.y += topInset + if xLabelsOrientation == .horizontal { + // Add left padding + label.frame.origin.y -= (label.frame.height - bottomInset) / 2 + label.frame.origin.x += padding + + // Set label's text alignment + label.frame.size.width = (drawingWidth / CGFloat(labels.count)) - padding * 2 + label.textAlignment = xLabelsTextAlignment + } else { + label.transform = CGAffineTransform(rotationAngle: CGFloat(Double.pi / 2)) + + // Adjust vertical position according to the label's height + label.frame.origin.y += label.frame.size.height / 2 + + // Adjust horizontal position as the series line + label.frame.origin.x = x + if xLabelsTextAlignment == .center { + // Align horizontally in series + label.frame.origin.x += ((drawingWidth / CGFloat(labels.count)) / 2) - (label.frame.size.width / 2) + } else { + // Give some space from the vertical line + label.frame.origin.x += padding + } + } + + self.addSubview(label) + } + + } + + fileprivate func drawLabelsAndGridOnYAxis() { + + let context = UIGraphicsGetCurrentContext()! + context.setStrokeColor(gridColor.cgColor) + context.setLineWidth(0.5) + + var labels: [Float] + if yLabels == nil { + labels = [(min.y + max.y) / 2, max.y] + if yLabelsOnRightSide || min.y != 0 { + labels.insert(min.y, at: 0) + } + } else { + labels = yLabels! + } + + let scaled = scaleValuesOnYAxis(labels) + let padding: CGFloat = 5 + let zero = CGFloat(getZeroValueOnYAxis(zeroLevel: 0)) + + scaled.enumerated().forEach { (i, value) in + + let y = CGFloat(value) + + // Add horizontal grid for each label, but not over axes + if y != drawingHeight + topInset && y != zero { + + context.move(to: CGPoint(x: CGFloat(0), y: y)) + context.addLine(to: CGPoint(x: self.bounds.width, y: y)) + if labels[i] != 0 { + // Horizontal grid for 0 is not dashed + context.setLineDash(phase: CGFloat(0), lengths: [CGFloat(5)]) + } else { + context.setLineDash(phase: CGFloat(0), lengths: []) + } + context.strokePath() + } + + let label = UILabel(frame: CGRect(x: padding, y: y, width: 0, height: 0)) + label.font = labelFont + label.text = yLabelsFormatter(i, labels[i]) + label.textColor = labelColor + label.sizeToFit() + + if yLabelsOnRightSide { + label.frame.origin.x = drawingWidth + label.frame.origin.x -= label.frame.width + padding + } + + // Labels should be placed above the horizontal grid + label.frame.origin.y -= label.frame.height + + self.addSubview(label) + + } + + UIGraphicsEndImageContext() + + } + + // MARK: - Touch events + + fileprivate func drawHighlightLineFromLeftPosition(_ left: CGFloat) { + if let shapeLayer = highlightShapeLayer { + // Use line already created + let path = CGMutablePath() + + path.move(to: CGPoint(x: left, y: 0)) + path.addLine(to: CGPoint(x: left, y: drawingHeight + topInset)) + shapeLayer.path = path + } else { + // Create the line + let path = CGMutablePath() + + path.move(to: CGPoint(x: left, y: CGFloat(0))) + path.addLine(to: CGPoint(x: left, y: drawingHeight + topInset)) + let shapeLayer = CAShapeLayer() + shapeLayer.frame = self.bounds + shapeLayer.path = path + shapeLayer.strokeColor = highlightLineColor.cgColor + shapeLayer.fillColor = nil + shapeLayer.lineWidth = highlightLineWidth + + highlightShapeLayer = shapeLayer + layer.addSublayer(shapeLayer) + layerStore.append(shapeLayer) + } + + } + + func handleTouchEvents(_ touches: Set, event: UIEvent!) { + let point = touches.first! + let left = point.location(in: self).x + let x = valueFromPointAtX(left) + + if left < 0 || left > (drawingWidth as CGFloat) { + // Remove highlight line at the end of the touch event + if let shapeLayer = highlightShapeLayer { + shapeLayer.path = nil + } + delegate?.didFinishTouchingChart(self) + return + } + + drawHighlightLineFromLeftPosition(left) + + if delegate == nil { + return + } + + var indexes: [Int?] = [] + + for series in self.series { + var index: Int? = nil + let xValues = series.data.map({ (point: ChartPoint) -> Float in + return point.x }) + let closest = Chart.findClosestInValues(xValues, forValue: x) + if closest.lowestIndex != nil && closest.highestIndex != nil { + // Consider valid only values on the right + index = closest.lowestIndex + } + indexes.append(index) + } + + delegate!.didTouchChart(self, indexes: indexes, x: x, left: left) + + } + override open func touchesBegan(_ touches: Set, with event: UIEvent?) { + handleTouchEvents(touches, event: event) + } + + override open func touchesEnded(_ touches: Set, with event: UIEvent?) { + handleTouchEvents(touches, event: event) + delegate?.didEndTouchingChart(self) + } + + override open func touchesMoved(_ touches: Set, with event: UIEvent?) { + handleTouchEvents(touches, event: event) + } + + // MARK: - Utilities + + fileprivate func valueFromPointAtX(_ x: CGFloat) -> Float { + let value = ((max.x-min.x) / Float(drawingWidth)) * Float(x) + min.x + return value + } + + fileprivate func valueFromPointAtY(_ y: CGFloat) -> Float { + let value = ((max.y - min.y) / Float(drawingHeight)) * Float(y) + min.y + return -value + } + + fileprivate class func findClosestInValues( + _ values: [Float], + forValue value: Float + ) -> ( + lowestValue: Float?, + highestValue: Float?, + lowestIndex: Int?, + highestIndex: Int? + ) { + var lowestValue: Float?, highestValue: Float?, lowestIndex: Int?, highestIndex: Int? + + values.enumerated().forEach { (i, currentValue) in + + if currentValue <= value && (lowestValue == nil || lowestValue! < currentValue) { + lowestValue = currentValue + lowestIndex = i + } + if currentValue >= value && (highestValue == nil || highestValue! > currentValue) { + highestValue = currentValue + highestIndex = i + } + + } + return ( + lowestValue: lowestValue, + highestValue: highestValue, + lowestIndex: lowestIndex, + highestIndex: highestIndex + ) + } + + /** + Segment a line in multiple lines when the line touches the x-axis, i.e. separating + positive from negative values. + */ + fileprivate class func segmentLine(_ line: ChartLineSegment, zeroLevel: Float) -> [ChartLineSegment] { + var segments: [ChartLineSegment] = [] + var segment: ChartLineSegment = [] + + line.enumerated().forEach { (i, point) in + + segment.append(point) + if i < line.count - 1 { + let nextPoint = line[i+1] + if point.y >= zeroLevel && nextPoint.y < zeroLevel || point.y < zeroLevel && nextPoint.y >= zeroLevel { + // The segment intersects zeroLevel, close the segment with the intersection point + let closingPoint = Chart.intersectionWithLevel(point, and: nextPoint, level: zeroLevel) + segment.append(closingPoint) + segments.append(segment) + // Start a new segment + segment = [closingPoint] + } + } else { + // End of the line + segments.append(segment) + } + + } + return segments + } + + /** + Return the intersection of a line between two points and 'y = level' line + */ + fileprivate class func intersectionWithLevel(_ p1: ChartPoint, and p2: ChartPoint, level: Float) -> ChartPoint { + let dy1 = level - p1.y + let dy2 = level - p2.y + return (x: (p2.x * dy1 - p1.x * dy2) / (dy1 - dy2), y: level) + } +} diff --git a/Pods/SwiftChart/Source/ChartColors.swift b/Pods/SwiftChart/Source/ChartColors.swift new file mode 100644 index 0000000..ac1ad1d --- /dev/null +++ b/Pods/SwiftChart/Source/ChartColors.swift @@ -0,0 +1,61 @@ +// +// ChartColors.swift +// +// Created by Giampaolo Bellavite on 07/11/14. +// Copyright (c) 2014 Giampaolo Bellavite. All rights reserved. +// + +import UIKit + +/** +Shorthands for various colors to use freely in the charts. +*/ +public struct ChartColors { + static fileprivate func colorFromHex(_ hex: Int) -> UIColor { + let red = CGFloat((hex & 0xFF0000) >> 16) / 255.0 + let green = CGFloat((hex & 0xFF00) >> 8) / 255.0 + let blue = CGFloat((hex & 0xFF)) / 255.0 + + return UIColor(red: red, green: green, blue: blue, alpha: 1) + } + + static public func blueColor() -> UIColor { + return colorFromHex(0x4A90E2) + } + static public func orangeColor() -> UIColor { + return colorFromHex(0xF5A623) + } + static public func greenColor() -> UIColor { + return colorFromHex(0x7ED321) + } + static public func darkGreenColor() -> UIColor { + return colorFromHex(0x417505) + } + static public func redColor() -> UIColor { + return colorFromHex(0xFF3200) + } + static public func darkRedColor() -> UIColor { + return colorFromHex(0xD0021B) + } + static public func purpleColor() -> UIColor { + return colorFromHex(0x9013FE) + } + static public func maroonColor() -> UIColor { + return colorFromHex(0x8B572A) + } + static public func pinkColor() -> UIColor { + return colorFromHex(0xBD10E0) + } + static public func greyColor() -> UIColor { + return colorFromHex(0x7f7f7f) + } + static public func cyanColor() -> UIColor { + return colorFromHex(0x50E3C2) + } + static public func goldColor() -> UIColor { + return colorFromHex(0xbcbd22) + } + static public func yellowColor() -> UIColor { + return colorFromHex(0xF8E71C) + } +} diff --git a/Pods/SwiftChart/Source/ChartSeries.swift b/Pods/SwiftChart/Source/ChartSeries.swift new file mode 100644 index 0000000..7b9f6d1 --- /dev/null +++ b/Pods/SwiftChart/Source/ChartSeries.swift @@ -0,0 +1,44 @@ +// +// ChartSeries.swift +// +// Created by Giampaolo Bellavite on 07/11/14. +// Copyright (c) 2014 Giampaolo Bellavite. All rights reserved. +// + +import UIKit + +/** +Represent a series to draw in the line chart. Each series is defined with a dataset and appareance settings. +*/ +open class ChartSeries { + open var data: [(x: Float, y: Float)] + open var area: Bool = false + open var line: Bool = true + open var color: UIColor = ChartColors.blueColor() { + didSet { + colors = (above: color, below: color, 0) + } + } + open var colors: ( + above: UIColor, + below: UIColor, + zeroLevel: Float + ) = (above: ChartColors.blueColor(), below: ChartColors.redColor(), 0) + + public init(_ data: [Float]) { + self.data = [] + + data.enumerated().forEach { (x, y) in + let point: (x: Float, y: Float) = (x: Float(x), y: y) + self.data.append(point) + } + } + + public init(data: [(x: Float, y: Float)]) { + self.data = data + } + + public init(data: [(x: Double, y: Double)]) { + self.data = data.map ({ (Float($0.x), Float($0.y))}) + } +} diff --git a/Pods/Target Support Files/SwiftChart/Info.plist b/Pods/Target Support Files/SwiftChart/Info.plist new file mode 100644 index 0000000..324eeb2 --- /dev/null +++ b/Pods/Target Support Files/SwiftChart/Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + ${PRODUCT_BUNDLE_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + FMWK + CFBundleShortVersionString + 0.5.0 + CFBundleSignature + ???? + CFBundleVersion + ${CURRENT_PROJECT_VERSION} + NSPrincipalClass + + + diff --git a/Pods/Target Support Files/SwiftChart/SwiftChart-dummy.m b/Pods/Target Support Files/SwiftChart/SwiftChart-dummy.m new file mode 100644 index 0000000..668583c --- /dev/null +++ b/Pods/Target Support Files/SwiftChart/SwiftChart-dummy.m @@ -0,0 +1,5 @@ +#import +@interface PodsDummy_SwiftChart : NSObject +@end +@implementation PodsDummy_SwiftChart +@end diff --git a/Pods/Target Support Files/SwiftChart/SwiftChart-prefix.pch b/Pods/Target Support Files/SwiftChart/SwiftChart-prefix.pch new file mode 100644 index 0000000..beb2a24 --- /dev/null +++ b/Pods/Target Support Files/SwiftChart/SwiftChart-prefix.pch @@ -0,0 +1,12 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + diff --git a/Pods/Target Support Files/SwiftChart/SwiftChart-umbrella.h b/Pods/Target Support Files/SwiftChart/SwiftChart-umbrella.h new file mode 100644 index 0000000..aaf1dc8 --- /dev/null +++ b/Pods/Target Support Files/SwiftChart/SwiftChart-umbrella.h @@ -0,0 +1,16 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + + +FOUNDATION_EXPORT double SwiftChartVersionNumber; +FOUNDATION_EXPORT const unsigned char SwiftChartVersionString[]; + diff --git a/Pods/Target Support Files/SwiftChart/SwiftChart.modulemap b/Pods/Target Support Files/SwiftChart/SwiftChart.modulemap new file mode 100644 index 0000000..59e0320 --- /dev/null +++ b/Pods/Target Support Files/SwiftChart/SwiftChart.modulemap @@ -0,0 +1,6 @@ +framework module SwiftChart { + umbrella header "SwiftChart-umbrella.h" + + export * + module * { export * } +} diff --git a/Pods/Target Support Files/SwiftChart/SwiftChart.xcconfig b/Pods/Target Support Files/SwiftChart/SwiftChart.xcconfig new file mode 100644 index 0000000..c2a6022 --- /dev/null +++ b/Pods/Target Support Files/SwiftChart/SwiftChart.xcconfig @@ -0,0 +1,11 @@ +CONFIGURATION_BUILD_DIR = $PODS_CONFIGURATION_BUILD_DIR/SwiftChart +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = "${PODS_ROOT}/Headers/Private" "${PODS_ROOT}/Headers/Public" "${PODS_ROOT}/Headers/Public/Plaid" +OTHER_LDFLAGS = -framework "UIKit" +OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS" +PODS_BUILD_DIR = $BUILD_DIR +PODS_CONFIGURATION_BUILD_DIR = $PODS_BUILD_DIR/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_ROOT = ${SRCROOT} +PODS_TARGET_SRCROOT = ${PODS_ROOT}/SwiftChart +PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} +SKIP_INSTALL = YES