Skip to content

Instantly share code, notes, and snippets.

@JasonCanCode
Created July 26, 2016 19:11
Show Gist options
  • Save JasonCanCode/4ebc75523bb4fade25c616d9b4770014 to your computer and use it in GitHub Desktop.
Save JasonCanCode/4ebc75523bb4fade25c616d9b4770014 to your computer and use it in GitHub Desktop.
An interactive line graph
import UIKit
typealias ConnectionDrawing = Void -> Void
class LineGraphView: UIView {
var data = [Double]()
var offset: CGFloat = 0.0
var shownRange = 0 ..< 0
var maximumValueShown: Double = 0.0
var gestureRecognizer = UISwipeGestureRecognizer()
var pointBorderColor = UIColor.whiteColor()
var pointFillerColor = UIColor.lightGrayColor()
var lineColor = UIColor.lightGrayColor()
func configure(data data: [Double], shownRange: Range<Int>, maximumValueShown: Double, offset: CGFloat? = nil) {
self.data = data
self.maximumValueShown = maximumValueShown
self.shownRange = shownRange // Must set range before using gap
self.offset = offset ?? gap / 2
gestureRecognizer.delegate = self
addGestureRecognizer(gestureRecognizer)
updateConnections()
}
override func drawRect(rect: CGRect) {
for draw in lineDrawings {
draw()
}
for draw in circleDrawings {
draw()
}
}
}
extension LineGraphView: PointConvertable {
var gap: CGFloat {
return horizontalGap(amountOfPointsRendered: shownRange.count, viewWidth: bounds.size.width)
}
func pointsToShow() -> [CGPoint?] {
return pointsFromData(data, withinRange: shownRange, maximumValueShown: maximumValueShown, inFrame: bounds, horizontalOffset: offset, includeLeadingAndTrailingPoints: true)
}
}
// MARK: Drawing
private var lineDrawings = [ConnectionDrawing]()
private var circleDrawings = [ConnectionDrawing]()
extension LineGraphView {
func updateConnections() {
removeConnections()
let centers = pointsToShow()
for (index, centerOne) in centers.enumerate() where index < centers.count - 1 {
if let centerOne = centerOne, centerTwo = centers[index + 1] {
addLineBetween(pointA: centerOne, pointB: centerTwo)
}
}
for center in centers where center != nil {
addCircle(center: center!)
}
}
private func addLineBetween(pointA pointA: CGPoint, pointB: CGPoint) {
lineDrawings.append({
let path = UIBezierPath()
path.lineWidth = 2.0
path.moveToPoint(pointA)
path.addLineToPoint(pointB)
self.lineColor.setStroke()
path.stroke()
})
setNeedsDisplay()
}
private func addCircle(center center: CGPoint) {
circleDrawings.append({
self.drawCircle(center: center, sizeSquare: 10, color: self.pointBorderColor)
self.drawCircle(center: center, sizeSquare: 8, color: self.pointFillerColor)
})
setNeedsDisplay()
}
private func rectFromCenter(center: CGPoint, size: CGSize) -> CGRect {
return CGRectMake(center.x - (size.width / 2), center.y - (size.height / 2), size.width, size.height)
}
private func drawCircle(center center: CGPoint, sizeSquare: CGFloat, color: UIColor) {
let rect = rectFromCenter(center, size: CGSize(width: sizeSquare, height: sizeSquare))
let path = UIBezierPath(ovalInRect: rect)
color.setFill()
path.fill()
}
private func removeConnections() {
lineDrawings = []
circleDrawings = []
setNeedsDisplay()
}
}
// MARK: Swipe to Shift Foward/Back
private enum Shift {
case Left, Right
}
private var originalStartIndex = 0
private var touchPoint: CGPoint?
extension LineGraphView: UIGestureRecognizerDelegate {
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
originalStartIndex = shownRange.startIndex
touchPoint = touches.first?.locationInView(self)
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
guard let oldPoint = touchPoint, newPoint = touches.first?.locationInView(self)
else { return }
let difference = newPoint.x - oldPoint.x
if difference > 20 {
shiftRange(.Left)
} else if difference < -20 {
shiftRange(.Right)
}
}
private func shiftRange(shift: Shift) {
switch shift {
case .Left where shownRange.startIndex > 0:
let startIndex = shownRange.startIndex - shownRange.count
let endIndex = shownRange.endIndex - shownRange.count
shownRange = shownRange.startIndex == originalStartIndex ? startIndex ..< endIndex : shownRange
case .Right where shownRange.endIndex < data.count:
let startIndex = shownRange.startIndex + shownRange.count
let endIndex = shownRange.endIndex + shownRange.count
shownRange = shownRange.startIndex == originalStartIndex ? startIndex ..< endIndex : shownRange
default:
break
}
updateConnections()
}
}
protocol PointConvertable {
func horizontalGap(amountOfPointsRendered rendered: Int, viewWidth: CGFloat) -> CGFloat
func pointFromData(data: Double, atIndex index: Int, inFrame frame: CGRect, horizontalOffset offset: CGFloat, amountOfPointsRendered amount: Int, maximumValueShown maxValue: Double) -> CGPoint
func pointsFromData(data: [Double], withinRange range: Range<Int>, maximumValueShown maxValue: Double, inFrame frame: CGRect, horizontalOffset offset: CGFloat, includeLeadingAndTrailingPoints hasLeadingTrailing: Bool) -> [CGPoint?]
}
extension PointConvertable {
private func distanceFromTop(figureValue: Double, maxValue: Double, viewHeight: CGFloat) -> CGFloat {
return viewHeight - (CGFloat(figureValue / maxValue) * viewHeight)
}
private func distanceFromLeft(figureValue: Double, indexOfFigureShown figureIndex: Int, horizontalGap gap: CGFloat, horizontalOffset offset: CGFloat) -> CGFloat {
return offset + (gap * CGFloat(figureIndex))
}
func horizontalGap(amountOfPointsRendered rendered: Int, viewWidth: CGFloat) -> CGFloat {
return viewWidth / CGFloat(rendered)
}
func pointFromData(data: Double, atIndex index: Int, inFrame frame: CGRect, horizontalOffset offset: CGFloat = 0, amountOfPointsRendered amount: Int, maximumValueShown maxValue: Double) -> CGPoint {
let figure = data
let viewHeight = frame.size.height
let viewWidth = frame.size.width
let gap = horizontalGap(amountOfPointsRendered: amount, viewWidth: viewWidth)
let xPoint = distanceFromLeft(figure, indexOfFigureShown: index, horizontalGap: gap, horizontalOffset: offset)
let yPoint = distanceFromTop(figure, maxValue: maxValue, viewHeight: viewHeight)
return CGPoint(x: xPoint, y: yPoint)
}
func pointsFromData(data: [Double], withinRange range: Range<Int>, maximumValueShown maxValue: Double, inFrame frame: CGRect, horizontalOffset offset: CGFloat, includeLeadingAndTrailingPoints hasLeadingTrailing: Bool = false) -> [CGPoint?] {
let startIndex = hasLeadingTrailing ? range.startIndex - 1 : range.startIndex
let endIndex = hasLeadingTrailing ? range.endIndex + 1 : range.endIndex
var points = [CGPoint?]()
var shownIndex = hasLeadingTrailing ? -1 : 0
for i in startIndex ..< endIndex {
guard i >= 0 && i < data.count else {
points.append(nil)
shownIndex += 1
continue
}
let figure = data[i]
points.append(pointFromData(figure, atIndex: shownIndex, inFrame: frame, horizontalOffset: offset, amountOfPointsRendered: range.count, maximumValueShown: maxValue))
shownIndex += 1
}
return points
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment