Last active
November 15, 2019 06:16
-
-
Save anirudhamahale/830099ed837b72b972a9a62e6be6aa68 to your computer and use it in GitHub Desktop.
Create simple line graph to show Blood Pressure stats.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// LineChart.swift | |
// LineChart | |
// | |
// Created by Nguyen Vu Nhat Minh on 25/8/17. | |
// Copyright © 2017 Nguyen Vu Nhat Minh. All rights reserved. | |
// | |
// Modified by Anirudha Mahale on 14/11/19. | |
import UIKit | |
struct PointEntry { | |
let systolic: Int | |
let diastolic: Int | |
let label: String | |
} | |
extension PointEntry: Comparable { | |
static func <(lhs: PointEntry, rhs: PointEntry) -> Bool { | |
return lhs.systolic < rhs.systolic || lhs.systolic < rhs.systolic | |
} | |
static func ==(lhs: PointEntry, rhs: PointEntry) -> Bool { | |
return lhs.systolic == rhs.systolic && lhs.diastolic == rhs.diastolic | |
} | |
} | |
class LineChart: UIView { | |
/// gap between each point | |
let lineGap: CGFloat = 30.0 | |
/// preseved space at top of the chart | |
let topSpace: CGFloat = 20.0 | |
/// preserved space at bottom of the chart to show labels along the Y axis | |
let bottomSpace: CGFloat = 40.0 | |
/// The top most horizontal line in the chart will be 10% higher than the highest value in the chart | |
let topHorizontalLine: CGFloat = 110.0 / 100.0 | |
/// Dot inner Radius | |
var innerRadius: CGFloat = 8 | |
/// Dot outer Radius | |
var outerRadius: CGFloat = 12 | |
var dataEntries: [PointEntry]? { | |
didSet { | |
self.setNeedsLayout() | |
} | |
} | |
/// Contains the main line which represents the data | |
private let dataLayer: CALayer = CALayer() | |
/// Contains dataLayer and gradientLayer | |
private let mainLayer: CALayer = CALayer() | |
/// To show the gradient below the main line | |
private let systolicGradientLayer: CAGradientLayer = { | |
let layer = CAGradientLayer() | |
layer.colors = [UIColor.green.withAlphaComponent(0.5).cgColor, UIColor.clear.cgColor] | |
return layer | |
}() | |
private let diastolicGradientLayer: CAGradientLayer = { | |
let layer = CAGradientLayer() | |
layer.colors = [UIColor.blue.withAlphaComponent(0.5).cgColor, UIColor.clear.cgColor] | |
return layer | |
}() | |
/// Contains mainLayer and label for each data entry | |
private let scrollView: UIScrollView = { | |
let view = UIScrollView() | |
view.showsVerticalScrollIndicator = false | |
view.showsHorizontalScrollIndicator = false | |
return view | |
}() | |
/// Contains horizontal lines | |
private let gridLayer: CALayer = CALayer() | |
/// An array of CGPoint on dataLayer coordinate system that the main line will go through. These points will be calculated from dataEntries array | |
private var systolicDataPoint: [CGPoint]? | |
private var daistolicDataPoint: [CGPoint]? | |
override init(frame: CGRect) { | |
super.init(frame: frame) | |
setupView() | |
} | |
convenience init() { | |
self.init(frame: CGRect.zero) | |
setupView() | |
} | |
required init?(coder aDecoder: NSCoder) { | |
super.init(coder: aDecoder) | |
setupView() | |
} | |
private func setupView() { | |
mainLayer.addSublayer(dataLayer) | |
mainLayer.addSublayer(gridLayer) | |
scrollView.layer.addSublayer(mainLayer) | |
scrollView.layer.addSublayer(systolicGradientLayer) | |
scrollView.layer.addSublayer(diastolicGradientLayer) | |
self.addSubview(scrollView) | |
} | |
override func layoutSubviews() { | |
scrollView.frame = CGRect(x: 0, y: 0, width: self.frame.size.width, height: self.frame.size.height) | |
if let dataEntries = dataEntries { | |
scrollView.contentSize = CGSize(width: CGFloat(dataEntries.count) * lineGap + 30, height: self.frame.size.height) | |
mainLayer.frame = CGRect(x: 0, y: 0, width: CGFloat(dataEntries.count) * lineGap + 30, height: self.frame.size.height) | |
dataLayer.frame = CGRect(x: 0, y: topSpace, width: mainLayer.frame.width, height: mainLayer.frame.height - topSpace - bottomSpace) | |
systolicGradientLayer.frame = dataLayer.frame | |
diastolicGradientLayer.frame = dataLayer.frame | |
systolicDataPoint = convertDataEntriesToPoints(entries: dataEntries, isSystolic: true) | |
daistolicDataPoint = convertDataEntriesToPoints(entries: dataEntries, isSystolic: false) | |
gridLayer.frame = CGRect(x: 0, y: topSpace, width: CGFloat(dataEntries.count) * lineGap + 30, height: mainLayer.frame.height - topSpace - bottomSpace) | |
clean() | |
drawHorizontalLines() | |
drawVerticleLine() | |
drawChart(for: systolicDataPoint, color: .blue) | |
maskGradientLayer(for: systolicDataPoint, isSystolic: true) | |
drawChart(for: daistolicDataPoint, color: .green) | |
maskGradientLayer(for: daistolicDataPoint, isSystolic: false) | |
drawLables() | |
} | |
} | |
/// Convert an array of PointEntry to an array of CGPoint on dataLayer coordinate system | |
/// - Parameter entries: Arrays of PointEntry | |
private func convertDataEntriesToPoints(entries: [PointEntry], isSystolic: Bool) -> [CGPoint] { | |
var result: [CGPoint] = [] | |
// let gridValues: [CGFloat] = [0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1.0, 1.05] | |
for (index, value) in entries.enumerated() { | |
let difference: CGFloat = 0.125 / 30 | |
let userValue: CGFloat = isSystolic ? CGFloat(value.systolic) : CGFloat(value.diastolic) | |
var height = (userValue - 30.0) * difference | |
height = (1.0 - height) * gridLayer.frame.size.height | |
let point = CGPoint(x: CGFloat(index)*lineGap + 40, y: height) | |
result.append(point) | |
} | |
return result | |
} | |
/// Draw a zigzag line connecting all points in dataPoints | |
private func drawChart(for points: [CGPoint]?, color: UIColor) { | |
if let dataPoints = points, dataPoints.count > 0 { | |
guard let path = createPath(for: points) else { return } | |
let lineLayer = CAShapeLayer() | |
lineLayer.path = path.cgPath | |
lineLayer.strokeColor = color.cgColor | |
lineLayer.fillColor = UIColor.clear.cgColor | |
dataLayer.addSublayer(lineLayer) | |
} | |
} | |
/// Create a zigzag bezier path that connects all points in dataPoints | |
private func createPath(for points: [CGPoint]?) -> UIBezierPath? { | |
guard let dataPoints = points, dataPoints.count > 0 else { | |
return nil | |
} | |
let path = UIBezierPath() | |
path.move(to: dataPoints[0]) | |
for i in 1..<dataPoints.count { | |
path.addLine(to: dataPoints[i]) | |
} | |
return path | |
} | |
/// Create titles at the bottom for all entries showed in the chart | |
private func drawLables() { | |
if let dataEntries = dataEntries, | |
dataEntries.count > 0 { | |
for i in 0..<dataEntries.count { | |
let textLayer = CATextLayer() | |
textLayer.frame = CGRect(x: lineGap*CGFloat(i) - lineGap/2 + 40, y: mainLayer.frame.size.height - bottomSpace/2 - 8, width: lineGap, height: 16) | |
textLayer.foregroundColor = UIColor.black.cgColor | |
textLayer.backgroundColor = UIColor.clear.cgColor | |
textLayer.alignmentMode = CATextLayerAlignmentMode.center | |
textLayer.contentsScale = UIScreen.main.scale | |
textLayer.font = CTFontCreateWithName(UIFont.systemFont(ofSize: 0).fontName as CFString, 0, nil) | |
textLayer.fontSize = 11 | |
textLayer.string = dataEntries[i].label | |
mainLayer.addSublayer(textLayer) | |
} | |
} | |
} | |
/// Create horizontal lines (grid lines) and show the value of each line | |
private func drawHorizontalLines() { | |
let gridValues: [CGFloat] = [1.05, 1.0, 0.875, 0.75, 0.625, 0.5, 0.375, 0.25, 0.125] | |
let gridText = ["", "30", "60", "90", "120", "150", "180", "210", "240"] | |
for (index, value) in gridValues.enumerated() { | |
let height = value * gridLayer.frame.size.height | |
let path = UIBezierPath() | |
if value == gridValues.first! { | |
path.move(to: CGPoint(x: 30, y: height)) | |
} else { | |
path.move(to: CGPoint(x: 28, y: height)) | |
} | |
path.addLine(to: CGPoint(x: gridLayer.frame.size.width, y: height)) | |
let lineLayer = CAShapeLayer() | |
lineLayer.path = path.cgPath | |
lineLayer.fillColor = UIColor.clear.cgColor | |
lineLayer.strokeColor = UIColor.black.cgColor | |
if value != gridValues.first! { | |
lineLayer.lineDashPattern = [4, 4] | |
} | |
lineLayer.lineWidth = 0.5 | |
gridLayer.addSublayer(lineLayer) | |
let textLayer = CATextLayer() | |
textLayer.frame = CGRect(x: 4, y: height-8, width: 50, height: 16) | |
textLayer.foregroundColor = UIColor.black.cgColor | |
textLayer.backgroundColor = UIColor.clear.cgColor | |
textLayer.contentsScale = UIScreen.main.scale | |
textLayer.font = CTFontCreateWithName(UIFont.systemFont(ofSize: 0).fontName as CFString, 0, nil) | |
textLayer.fontSize = 12 | |
textLayer.string = gridText[index] | |
gridLayer.addSublayer(textLayer) | |
} | |
} | |
private func drawVerticleLine() { | |
let height = gridLayer.frame.size.height * 1.05 | |
let path = UIBezierPath() | |
path.move(to: CGPoint(x: 30, y: 0)) | |
path.addLine(to: CGPoint(x: 30, y: height)) | |
let lineLayer = CAShapeLayer() | |
lineLayer.path = path.cgPath | |
lineLayer.fillColor = UIColor.clear.cgColor | |
lineLayer.strokeColor = UIColor.black.cgColor | |
lineLayer.lineWidth = 0.5 | |
gridLayer.addSublayer(lineLayer) | |
} | |
private func maskGradientLayer(for points: [CGPoint]?, isSystolic: Bool) { | |
if let dataPoints = points, | |
dataPoints.count > 0 { | |
let path = UIBezierPath() | |
path.move(to: CGPoint(x: dataPoints[0].x, y: dataLayer.frame.height)) | |
path.addLine(to: dataPoints[0]) | |
if let straightPath = createPath(for: points) { | |
path.append(straightPath) | |
} | |
path.addLine(to: CGPoint(x: dataPoints[dataPoints.count-1].x, y: dataLayer.frame.height)) | |
path.addLine(to: CGPoint(x: dataPoints[0].x, y: dataLayer.frame.height)) | |
let maskLayer = CAShapeLayer() | |
maskLayer.path = path.cgPath | |
maskLayer.fillColor = UIColor.white.cgColor | |
maskLayer.strokeColor = UIColor.clear.cgColor | |
maskLayer.lineWidth = 0.0 | |
if isSystolic { | |
systolicGradientLayer.mask = maskLayer | |
} else { | |
diastolicGradientLayer.mask = maskLayer | |
} | |
} | |
} | |
private func clean() { | |
mainLayer.sublayers?.forEach({ | |
if $0 is CATextLayer { | |
$0.removeFromSuperlayer() | |
} | |
}) | |
dataLayer.sublayers?.forEach({$0.removeFromSuperlayer()}) | |
gridLayer.sublayers?.forEach({$0.removeFromSuperlayer()}) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment