├── Classes └── ScrollableGraphView.swift ├── LICENSE ├── README.md ├── ScrollableGraphView.podspec ├── graphview_example ├── GraphView.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ └── contents.xcworkspacedata └── GraphView │ ├── AppDelegate.swift │ ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ ├── UIColor+colorFromHex.swift │ └── ViewController.swift └── readme_images ├── IMG_5814_small.jpg ├── adapting.gif ├── animating.gif ├── customising.gif ├── gallery ├── dark.png ├── default.png ├── dot.png ├── pink_margins.png └── pink_mountain.png ├── init_anim_high_fps.gif ├── more_scrolling.gif ├── scrolling.gif └── spacing.png /Classes/ScrollableGraphView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | // MARK: - ScrollableGraphView 4 | @objc public class ScrollableGraphView: UIScrollView, UIScrollViewDelegate, ScrollableGraphViewDrawingDelegate { 5 | 6 | // MARK: - Public Properties 7 | // Use these to customise the graph. 8 | // ################################# 9 | 10 | // Line Styles 11 | public var lineWidth: CGFloat = 2 12 | public var lineColor = UIColor.blackColor() 13 | public var lineStyle = ScrollableGraphViewLineStyle.Straight 14 | 15 | public var lineJoin = kCALineJoinRound 16 | public var lineCap = kCALineCapRound 17 | public var lineCurviness: CGFloat = 0.5 18 | 19 | // Fill Styles 20 | public var backgroundFillColor = UIColor.whiteColor() 21 | 22 | public var shouldFill = false 23 | public var fillType = ScrollableGraphViewFillType.Solid 24 | public var fillColor = UIColor.blackColor() 25 | public var fillGradientStartColor = UIColor.whiteColor() 26 | public var fillGradientEndColor = UIColor.blackColor() 27 | public var fillGradientType = ScrollableGraphViewGradientType.Linear 28 | 29 | // Spacing 30 | public var topMargin: CGFloat = 10 31 | public var bottomMargin: CGFloat = 10 32 | public var leftmostPointPadding: CGFloat = 50 33 | public var rightmostPointPadding: CGFloat = 50 34 | public var dataPointSpacing: CGFloat = 40 35 | public var direction = ScrollableGraphViewDirection.LeftToRight 36 | 37 | // Graph Range 38 | public var shouldAutomaticallyDetectRange = false 39 | public var shouldRangeAlwaysStartAtZero = false // Used in conjunction with shouldAutomaticallyDetectRange, if you want to force the min to stay at 0. 40 | public var rangeMin: Double = 0 // Ignored when shouldAutomaticallyDetectRange or shouldAdaptRange = true 41 | public var rangeMax: Double = 100 // Ignored when shouldAutomaticallyDetectRange or shouldAdaptRange = true 42 | 43 | // Data Point Drawing 44 | public var shouldDrawDataPoint = true 45 | public var dataPointType = ScrollableGraphViewDataPointType.Circle 46 | public var dataPointSize: CGFloat = 5 47 | public var dataPointFillColor: UIColor = UIColor.blackColor() 48 | public var customDataPointPath: ((centre: CGPoint) -> UIBezierPath)? 49 | 50 | // Adapting & Animations 51 | public var shouldAdaptRange = false 52 | public var shouldAnimateOnAdapt = true 53 | public var animationDuration: Double = 1 54 | public var adaptAnimationType = ScrollableGraphViewAnimationType.EaseOut 55 | public var customAnimationEasingFunction: ((t: Double) -> Double)? 56 | public var shouldAnimateOnStartup = true 57 | 58 | // Reference Lines 59 | public var shouldShowReferenceLines = true 60 | public var referenceLineColor = UIColor.blackColor() 61 | public var referenceLineThickness: CGFloat = 0.5 62 | public var referenceLinePosition = ScrollableGraphViewReferenceLinePosition.Left 63 | public var referenceLineType = ScrollableGraphViewReferenceLineType.Cover 64 | 65 | public var numberOfIntermediateReferenceLines: Int = 3 66 | public var shouldAddLabelsToIntermediateReferenceLines = true 67 | public var shouldAddUnitsToIntermediateReferenceLineLabels = false 68 | 69 | // Reference Line Labels 70 | public var referenceLineLabelFont = UIFont.systemFontOfSize(8) 71 | public var referenceLineLabelColor = UIColor.blackColor() 72 | 73 | public var shouldShowReferenceLineUnits = true 74 | public var referenceLineUnits: String? 75 | public var referenceLineNumberOfDecimalPlaces: Int = 0 76 | 77 | // Data Point Labels 78 | public var shouldShowLabels = true 79 | public var dataPointLabelTopMargin: CGFloat = 10 80 | public var dataPointLabelBottomMargin: CGFloat = 0 81 | public var dataPointLabelColor = UIColor.blackColor() 82 | public var dataPointLabelFont: UIFont? = UIFont.systemFontOfSize(10) 83 | 84 | // MARK: - Private State 85 | // ##################### 86 | 87 | // Graph Data for Display 88 | private var data = [Double]() 89 | private var labels = [String]() 90 | 91 | private var isInitialSetup = true 92 | private var dataNeedsReloading = true 93 | private var isCurrentlySettingUp = false 94 | 95 | private var viewportWidth: CGFloat = 0 { 96 | didSet { if(oldValue != viewportWidth) { viewportDidChange() }} 97 | } 98 | private var viewportHeight: CGFloat = 0 { 99 | didSet { if(oldValue != viewportHeight) { viewportDidChange() }} 100 | } 101 | 102 | private var totalGraphWidth: CGFloat = 0 103 | private var offsetWidth: CGFloat = 0 104 | 105 | // Graph Line 106 | private var currentLinePath = UIBezierPath() 107 | 108 | // Labels 109 | private var labelsView = UIView() 110 | private var labelPool = LabelPool() 111 | 112 | // Graph Drawing 113 | private var graphPoints = [GraphPoint]() 114 | 115 | private var drawingView = UIView() 116 | private var lineLayer: LineDrawingLayer? 117 | private var dataPointLayer: DataPointDrawingLayer? 118 | private var fillLayer: FillDrawingLayer? 119 | private var gradientLayer: GradientDrawingLayer? 120 | 121 | // Reference Lines 122 | private var referenceLineView: ReferenceLineDrawingView? 123 | 124 | // Animation 125 | private var displayLink: CADisplayLink! 126 | private var previousTimestamp: CFTimeInterval = 0 127 | private var currentTimestamp: CFTimeInterval = 0 128 | 129 | private var currentAnimations = [GraphPointAnimation]() 130 | 131 | // Active Points & Range Calculation 132 | 133 | private var previousActivePointsInterval: Range = -1 ..< -1 134 | private var activePointsInterval: Range = -1 ..< -1 { 135 | didSet { 136 | if(oldValue.startIndex != activePointsInterval.startIndex || oldValue.endIndex != activePointsInterval.endIndex) { 137 | if !isCurrentlySettingUp { activePointsDidChange() } 138 | } 139 | } 140 | } 141 | 142 | private var range: (min: Double, max: Double) = (0, 100) { 143 | didSet { 144 | if(oldValue.min != range.min || oldValue.max != range.max) { 145 | if !isCurrentlySettingUp { rangeDidChange() } 146 | } 147 | } 148 | } 149 | 150 | // MARK: - INIT, SETUP & VIEWPORT RESIZING 151 | // ####################################### 152 | 153 | override public init(frame: CGRect) { 154 | super.init(frame: frame) 155 | } 156 | 157 | required public init?(coder aDecoder: NSCoder) { 158 | fatalError("init(coder:) has not been implemented") 159 | } 160 | 161 | deinit { 162 | displayLink?.invalidate() 163 | } 164 | 165 | private func setup() { 166 | 167 | isCurrentlySettingUp = true 168 | 169 | // Make sure everything is in a clean state. 170 | reset() 171 | 172 | self.delegate = self 173 | 174 | // Calculate the viewport and drawing frames. 175 | self.viewportWidth = self.frame.width 176 | self.viewportHeight = self.frame.height 177 | 178 | totalGraphWidth = graphWidthForNumberOfDataPoints(data.count) 179 | self.contentSize = CGSize(width: totalGraphWidth, height: viewportHeight) 180 | 181 | // Scrolling direction. 182 | if (direction == .RightToLeft) { 183 | self.offsetWidth = self.contentSize.width - viewportWidth 184 | } 185 | // Otherwise start of all the way to the left. 186 | else { 187 | self.offsetWidth = 0 188 | } 189 | 190 | // Set the scrollview offset. 191 | self.contentOffset.x = self.offsetWidth 192 | 193 | // Calculate the initial range depending on settings. 194 | let initialActivePointsInterval = calculateActivePointsInterval() 195 | let detectedRange = calculateRangeForEntireDataset(self.data) 196 | 197 | if(shouldAutomaticallyDetectRange) { 198 | self.range = detectedRange 199 | } 200 | else { 201 | self.range = (min: rangeMin, max: rangeMax) 202 | } 203 | 204 | if (shouldAdaptRange) { // This supercedes the shouldAutomaticallyDetectRange option 205 | let range = calculateRangeForActivePointsInterval(initialActivePointsInterval) 206 | self.range = range 207 | } 208 | 209 | // If the graph was given all 0s as data, we can't use a range of 0->0, so make sure we have a sensible range at all times. 210 | if (self.range.min == 0 && self.range.max == 0) { 211 | self.range = (min: 0, max: rangeMax) 212 | } 213 | 214 | // DRAWING 215 | 216 | let viewport = CGRect(x: 0, y: 0, width: viewportWidth, height: viewportHeight) 217 | 218 | // Create all the GraphPoints which which are used for drawing. 219 | for i in 0 ..< data.count { 220 | let value = (shouldAnimateOnStartup) ? self.range.min : data[i] 221 | 222 | let position = calculatePosition(i, value: value) 223 | let point = GraphPoint(position: position) 224 | graphPoints.append(point) 225 | } 226 | 227 | // Drawing Layers 228 | drawingView = UIView(frame: viewport) 229 | drawingView.backgroundColor = backgroundFillColor 230 | self.addSubview(drawingView) 231 | 232 | addDrawingLayers(inViewport: viewport) 233 | 234 | // References Lines 235 | if(shouldShowReferenceLines) { 236 | addReferenceLines(inViewport: viewport) 237 | } 238 | 239 | // X-Axis Labels 240 | self.insertSubview(labelsView, aboveSubview: drawingView) 241 | 242 | // Animation loop for when the range adapts 243 | displayLink = CADisplayLink(target: self, selector: #selector(animationUpdate)) 244 | displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes) 245 | displayLink.paused = true 246 | 247 | isCurrentlySettingUp = false 248 | 249 | // Set the first active points interval. These are the points that are visible when the view loads. 250 | self.activePointsInterval = initialActivePointsInterval 251 | } 252 | 253 | // Makes sure everything is in a clean state for when we want to reset the data for a graph. 254 | private func reset() { 255 | drawingView.removeFromSuperview() 256 | referenceLineView?.removeFromSuperview() 257 | 258 | labelPool = LabelPool() 259 | 260 | for labelView in labelsView.subviews { 261 | labelView.removeFromSuperview() 262 | } 263 | 264 | graphPoints.removeAll() 265 | 266 | currentAnimations.removeAll() 267 | displayLink?.invalidate() 268 | previousTimestamp = 0 269 | currentTimestamp = 0 270 | 271 | previousActivePointsInterval = -1 ..< -1 272 | activePointsInterval = -1 ..< -1 273 | range = (0, 100) 274 | } 275 | 276 | private func addDrawingLayers(inViewport viewport: CGRect) { 277 | 278 | // Line Layer 279 | lineLayer = LineDrawingLayer(frame: viewport, lineWidth: lineWidth, lineColor: lineColor, lineStyle: lineStyle, lineJoin: lineJoin, lineCap: lineCap) 280 | lineLayer?.graphViewDrawingDelegate = self 281 | drawingView.layer.addSublayer(lineLayer!) 282 | 283 | // Data Point layer 284 | if(shouldDrawDataPoint) { 285 | dataPointLayer = DataPointDrawingLayer(frame: viewport, fillColor: dataPointFillColor, dataPointType: dataPointType, dataPointSize: dataPointSize) 286 | dataPointLayer?.graphViewDrawingDelegate = self 287 | drawingView.layer.insertSublayer(dataPointLayer!, above: lineLayer) 288 | } 289 | 290 | // Gradient and Fills 291 | switch (self.fillType) { 292 | 293 | case .Solid: 294 | if(shouldFill) { 295 | // Setup fill 296 | fillLayer = FillDrawingLayer(frame: viewport, fillColor: fillColor) 297 | fillLayer?.graphViewDrawingDelegate = self 298 | drawingView.layer.insertSublayer(fillLayer!, below: lineLayer) 299 | } 300 | 301 | case .Gradient: 302 | if(shouldFill) { 303 | gradientLayer = GradientDrawingLayer(frame: viewport, startColor: fillGradientStartColor, endColor: fillGradientEndColor, gradientType: fillGradientType) 304 | gradientLayer!.graphViewDrawingDelegate = self 305 | drawingView.layer.insertSublayer(gradientLayer!, below: lineLayer) 306 | } 307 | } 308 | } 309 | 310 | private func addReferenceLines(inViewport viewport: CGRect) { 311 | var referenceLineBottomMargin = bottomMargin 312 | if(shouldShowLabels && dataPointLabelFont != nil) { 313 | referenceLineBottomMargin += (dataPointLabelFont!.pointSize + dataPointLabelTopMargin + dataPointLabelBottomMargin) 314 | } 315 | 316 | referenceLineView = ReferenceLineDrawingView( 317 | frame: viewport, 318 | topMargin: topMargin, 319 | bottomMargin: referenceLineBottomMargin, 320 | referenceLineColor: self.referenceLineColor, 321 | referenceLineThickness: self.referenceLineThickness) 322 | 323 | // Reference line settings. 324 | referenceLineView?.referenceLinePosition = self.referenceLinePosition 325 | referenceLineView?.referenceLineType = self.referenceLineType 326 | 327 | referenceLineView?.numberOfIntermediateReferenceLines = self.numberOfIntermediateReferenceLines 328 | 329 | // Reference line label settings. 330 | referenceLineView?.shouldAddLabelsToIntermediateReferenceLines = self.shouldAddLabelsToIntermediateReferenceLines 331 | referenceLineView?.shouldAddUnitsToIntermediateReferenceLineLabels = self.shouldAddUnitsToIntermediateReferenceLineLabels 332 | 333 | referenceLineView?.labelUnits = referenceLineUnits 334 | referenceLineView?.labelFont = self.referenceLineLabelFont 335 | referenceLineView?.labelColor = self.referenceLineLabelColor 336 | referenceLineView?.labelDecimalPlaces = self.referenceLineNumberOfDecimalPlaces 337 | 338 | referenceLineView?.setRange(self.range) 339 | self.addSubview(referenceLineView!) 340 | } 341 | 342 | // If the view has changed we have to make sure we're still displaying the right data. 343 | override public func layoutSubviews() { 344 | super.layoutSubviews() 345 | 346 | updateUI() 347 | } 348 | 349 | private func updateUI() { 350 | 351 | // Make sure we have data, if don't, just get out. We can't do anything without any data. 352 | guard data.count > 0 else { 353 | return 354 | } 355 | 356 | // If the data has been updated, we need to re-init everything 357 | if (dataNeedsReloading) { 358 | setup() 359 | 360 | if(shouldAnimateOnStartup) { 361 | startAnimations(withStaggerValue: 0.15) 362 | } 363 | 364 | // We're done setting up. 365 | dataNeedsReloading = false 366 | isInitialSetup = false 367 | 368 | } 369 | // Otherwise, the user is just scrolling and we just need to update everything. 370 | else { 371 | // Needs to update the viewportWidth and viewportHeight which is used to calculate which 372 | // points we can actually see. 373 | viewportWidth = self.frame.width 374 | viewportHeight = self.frame.height 375 | 376 | // If the scrollview has scrolled anywhere, we need to update the offset 377 | // and move around our drawing views. 378 | offsetWidth = self.contentOffset.x 379 | updateOffsetWidths() 380 | 381 | // Recalculate active points for this size. 382 | // Recalculate range for active points. 383 | let newActivePointsInterval = calculateActivePointsInterval() 384 | self.previousActivePointsInterval = self.activePointsInterval 385 | self.activePointsInterval = newActivePointsInterval 386 | 387 | // If adaption is enabled we want to 388 | if(shouldAdaptRange) { 389 | let newRange = calculateRangeForActivePointsInterval(newActivePointsInterval) 390 | self.range = newRange 391 | } 392 | } 393 | } 394 | 395 | private func updateOffsetWidths() { 396 | drawingView.frame.origin.x = offsetWidth 397 | drawingView.bounds.origin.x = offsetWidth 398 | 399 | gradientLayer?.offset = offsetWidth 400 | 401 | referenceLineView?.frame.origin.x = offsetWidth 402 | } 403 | 404 | private func updateFrames() { 405 | // Drawing view needs to always be the same size as the scrollview. 406 | drawingView.frame.size.width = viewportWidth 407 | drawingView.frame.size.height = viewportHeight 408 | 409 | // Gradient should extend over the entire viewport 410 | gradientLayer?.frame.size.width = viewportWidth 411 | gradientLayer?.frame.size.height = viewportHeight 412 | 413 | // Reference lines should extend over the entire viewport 414 | referenceLineView?.setViewport(viewportWidth, viewportHeight: viewportHeight) 415 | 416 | self.contentSize.height = viewportHeight 417 | } 418 | 419 | // MARK: - Public Methods 420 | // ###################### 421 | 422 | public func setData(data: [Double], withLabels labels: [String]) { 423 | self.dataNeedsReloading = true 424 | self.data = data 425 | self.labels = labels 426 | 427 | if(!isInitialSetup) { 428 | updateUI() 429 | } 430 | } 431 | 432 | // MARK: - Private Methods 433 | // ####################### 434 | 435 | // MARK: Animation 436 | 437 | // Animation update loop for co-domain changes. 438 | @objc private func animationUpdate() { 439 | let dt = timeSinceLastFrame() 440 | 441 | for animation in currentAnimations { 442 | 443 | animation.update(dt) 444 | 445 | if animation.finished { 446 | dequeueAnimation(animation) 447 | } 448 | } 449 | 450 | updatePaths() 451 | } 452 | 453 | private func animatePoint(point: GraphPoint, toPosition position: CGPoint, withDelay delay: Double = 0) { 454 | let currentPoint = CGPoint(x: point.x, y: point.y) 455 | let animation = GraphPointAnimation(fromPoint: currentPoint, toPoint: position, forGraphPoint: point) 456 | animation.animationEasing = getAnimationEasing() 457 | animation.duration = animationDuration 458 | animation.delay = delay 459 | enqueueAnimation(animation) 460 | } 461 | 462 | private func getAnimationEasing() -> (Double) -> Double { 463 | switch(self.adaptAnimationType) { 464 | case .Elastic: 465 | return Easings.EaseOutElastic 466 | case .EaseOut: 467 | return Easings.EaseOutQuad 468 | case .Custom: 469 | if let customEasing = customAnimationEasingFunction { 470 | return customEasing 471 | } 472 | else { 473 | fallthrough 474 | } 475 | default: 476 | return Easings.EaseOutQuad 477 | } 478 | } 479 | 480 | private func enqueueAnimation(animation: GraphPointAnimation) { 481 | if (currentAnimations.count == 0) { 482 | // Need to kick off the loop. 483 | displayLink.paused = false 484 | } 485 | currentAnimations.append(animation) 486 | } 487 | 488 | private func dequeueAnimation(animation: GraphPointAnimation) { 489 | if let index = currentAnimations.indexOf(animation) { 490 | currentAnimations.removeAtIndex(index) 491 | } 492 | 493 | if(currentAnimations.count == 0) { 494 | // Stop animation loop. 495 | displayLink.paused = true 496 | } 497 | } 498 | 499 | private func dequeueAllAnimations() { 500 | 501 | for animation in currentAnimations { 502 | animation.animationDidFinish() 503 | } 504 | 505 | currentAnimations.removeAll() 506 | displayLink.paused = true 507 | } 508 | 509 | private func timeSinceLastFrame() -> Double { 510 | if previousTimestamp == 0 { 511 | previousTimestamp = displayLink.timestamp 512 | } else { 513 | previousTimestamp = currentTimestamp 514 | } 515 | 516 | currentTimestamp = displayLink.timestamp 517 | 518 | var dt = currentTimestamp - previousTimestamp 519 | 520 | if dt > 0.032 { 521 | dt = 0.032 522 | } 523 | 524 | return dt 525 | } 526 | 527 | // MARK: Layout Calculations 528 | 529 | private func calculateActivePointsInterval() -> Range { 530 | 531 | // Calculate the "active points" 532 | let min = Int((offsetWidth) / dataPointSpacing) 533 | let max = Int(((offsetWidth + viewportWidth)) / dataPointSpacing) 534 | 535 | // Add and minus two so the path goes "off the screen" so we can't see where it ends. 536 | let minPossible = 0 537 | let maxPossible = data.count - 1 538 | 539 | let numberOfPointsOffscreen = 2 540 | 541 | let actualMin = clamp(min - numberOfPointsOffscreen, min: minPossible, max: maxPossible) 542 | let actualMax = clamp(max + numberOfPointsOffscreen, min: minPossible, max: maxPossible) 543 | 544 | return actualMin ..< actualMax 545 | } 546 | 547 | private func calculateRangeForActivePointsInterval(interval: Range) -> (min: Double, max: Double) { 548 | 549 | let dataForActivePoints = data[interval.startIndex...interval.endIndex] 550 | 551 | // We don't have any active points, return defaults. 552 | if(dataForActivePoints.count == 0) { 553 | return (min: self.rangeMin, max: self.rangeMax) 554 | } 555 | else { 556 | 557 | let range = calculateRange(dataForActivePoints) 558 | return cleanRange(range) 559 | } 560 | } 561 | 562 | private func calculateRangeForEntireDataset(data: [Double]) -> (min: Double, max: Double) { 563 | let range = calculateRange(self.data) 564 | return cleanRange(range) 565 | } 566 | 567 | private func calculateRange(data: T) -> (min: Double, max: Double) { 568 | 569 | var rangeMin: Double = Double(Int.max) 570 | var rangeMax: Double = Double(Int.min) 571 | 572 | for dataPoint in data { 573 | if (dataPoint > rangeMax) { 574 | rangeMax = dataPoint 575 | } 576 | 577 | if (dataPoint < rangeMin) { 578 | rangeMin = dataPoint 579 | } 580 | } 581 | return (min: rangeMin, max: rangeMax) 582 | } 583 | 584 | private func cleanRange(range: (min: Double, max: Double)) -> (min: Double, max: Double){ 585 | if(range.min == range.max) { 586 | 587 | let min = shouldRangeAlwaysStartAtZero ? 0 : range.min 588 | let max = range.max + 1 589 | 590 | return (min: min, max: max) 591 | } 592 | else if (shouldRangeAlwaysStartAtZero) { 593 | 594 | let min: Double = 0 595 | var max: Double = range.max 596 | 597 | // If we have all negative numbers and the max happens to be 0, there will cause a division by 0. Return the default height. 598 | if(range.max == 0) { 599 | max = rangeMax 600 | } 601 | 602 | return (min: min, max: max) 603 | } 604 | else { 605 | return range 606 | } 607 | } 608 | 609 | private func graphWidthForNumberOfDataPoints(numberOfPoints: Int) -> CGFloat { 610 | let width: CGFloat = (CGFloat(numberOfPoints - 1) * dataPointSpacing) + (leftmostPointPadding + rightmostPointPadding) 611 | return width 612 | } 613 | 614 | private func calculatePosition(index: Int, value: Double) -> CGPoint { 615 | 616 | // Set range defaults based on settings: 617 | 618 | // self.range.min/max is the current ranges min/max that has been detected 619 | // self.rangeMin/Max is the min/max that should be used as specified by the user 620 | let rangeMax = (shouldAutomaticallyDetectRange || shouldAdaptRange) ? self.range.max : self.rangeMax 621 | let rangeMin = (shouldAutomaticallyDetectRange || shouldAdaptRange) ? self.range.min : self.rangeMin 622 | 623 | // y = the y co-ordinate in the view for the value in the graph 624 | // ( ( value - max ) ) value = the value on the graph for which we want to know its corresponding location on the y axis in the view 625 | // y = ( ( ----------- ) * graphHeight ) + topMargin t = the top margin 626 | // ( ( min - max ) ) h = the height of the graph space without margins 627 | // min = the range's current mininum 628 | // max = the range's current maximum 629 | 630 | // Calculate the position on in the view for the value specified. 631 | var graphHeight = viewportHeight - topMargin - bottomMargin 632 | if(shouldShowLabels && dataPointLabelFont != nil) { graphHeight -= (dataPointLabelFont!.pointSize + dataPointLabelTopMargin + dataPointLabelBottomMargin) } 633 | 634 | let x = (CGFloat(index) * dataPointSpacing) + leftmostPointPadding 635 | let y = (CGFloat((value - rangeMax) / (rangeMin - rangeMax)) * graphHeight) + topMargin 636 | 637 | return CGPoint(x: x, y: y) 638 | } 639 | 640 | private func clamp(value:T, min:T, max:T) -> T { 641 | if (value < min) { 642 | return min 643 | } 644 | else if (value > max) { 645 | return max 646 | } 647 | else { 648 | return value 649 | } 650 | } 651 | 652 | // MARK: Line Path Creation 653 | 654 | private func createLinePath() -> UIBezierPath { 655 | 656 | currentLinePath.removeAllPoints() 657 | 658 | let numberOfPoints = min(data.count, activePointsInterval.endIndex) 659 | 660 | let pathSegmentAdder = lineStyle == .Straight ? addStraightLineSegment : addCurvedLineSegment 661 | 662 | // Connect the line to the starting edge if we are filling it. 663 | if(shouldFill) { 664 | // Add a line from the base of the graph to the first data point. 665 | let firstDataPoint = graphPoints[activePointsInterval.startIndex] 666 | 667 | let zeroYPosition = calculatePosition(0, value: self.range.min).y 668 | 669 | let viewportLeftZero = CGPoint(x: firstDataPoint.x - (leftmostPointPadding), y: zeroYPosition) 670 | let leftFarEdgeTop = CGPoint(x: firstDataPoint.x - (leftmostPointPadding + viewportWidth), y: zeroYPosition) 671 | let leftFarEdgeBottom = CGPoint(x: firstDataPoint.x - (leftmostPointPadding + viewportWidth), y: viewportHeight) 672 | 673 | currentLinePath.moveToPoint(leftFarEdgeBottom) 674 | pathSegmentAdder(startPoint: leftFarEdgeBottom, endPoint: leftFarEdgeTop, inPath: currentLinePath) 675 | pathSegmentAdder(startPoint: leftFarEdgeTop, endPoint: viewportLeftZero, inPath: currentLinePath) 676 | pathSegmentAdder(startPoint: viewportLeftZero, endPoint: CGPoint(x: firstDataPoint.x, y: firstDataPoint.y), inPath: currentLinePath) 677 | } 678 | else { 679 | let firstDataPoint = graphPoints[activePointsInterval.startIndex] 680 | currentLinePath.moveToPoint(firstDataPoint.location) 681 | } 682 | 683 | // Connect each point on the graph with a segment. 684 | for i in activePointsInterval.startIndex ..< numberOfPoints { 685 | 686 | let startPoint = graphPoints[i].location 687 | let endPoint = graphPoints[i+1].location 688 | 689 | pathSegmentAdder(startPoint: startPoint, endPoint: endPoint, inPath: currentLinePath) 690 | } 691 | 692 | // Connect the line to the ending edge if we are filling it. 693 | if(shouldFill) { 694 | // Add a line from the last data point to the base of the graph. 695 | let lastDataPoint = graphPoints[activePointsInterval.endIndex] 696 | 697 | let zeroYPosition = calculatePosition(0, value: self.range.min).y 698 | 699 | let viewportRightZero = CGPoint(x: lastDataPoint.x + (rightmostPointPadding), y: zeroYPosition) 700 | let rightFarEdgeTop = CGPoint(x: lastDataPoint.x + (rightmostPointPadding + viewportWidth), y: zeroYPosition) 701 | let rightFarEdgeBottom = CGPoint(x: lastDataPoint.x + (rightmostPointPadding + viewportWidth), y: viewportHeight) 702 | 703 | pathSegmentAdder(startPoint: lastDataPoint.location, endPoint: viewportRightZero, inPath: currentLinePath) 704 | pathSegmentAdder(startPoint: viewportRightZero, endPoint: rightFarEdgeTop, inPath: currentLinePath) 705 | pathSegmentAdder(startPoint: rightFarEdgeTop, endPoint: rightFarEdgeBottom, inPath: currentLinePath) 706 | } 707 | 708 | return currentLinePath 709 | } 710 | 711 | private func addStraightLineSegment(startPoint startPoint: CGPoint, endPoint: CGPoint, inPath path: UIBezierPath) { 712 | path.addLineToPoint(endPoint) 713 | } 714 | 715 | private func addCurvedLineSegment(startPoint startPoint: CGPoint, endPoint: CGPoint, inPath path: UIBezierPath) { 716 | // calculate control points 717 | let difference = endPoint.x - startPoint.x 718 | 719 | var x = startPoint.x + (difference * lineCurviness) 720 | var y = startPoint.y 721 | let controlPointOne = CGPoint(x: x, y: y) 722 | 723 | x = endPoint.x - (difference * lineCurviness) 724 | y = endPoint.y 725 | let controlPointTwo = CGPoint(x: x, y: y) 726 | 727 | // add curve from start to end 728 | currentLinePath.addCurveToPoint(endPoint, controlPoint1: controlPointOne, controlPoint2: controlPointTwo) 729 | } 730 | 731 | // MARK: Events 732 | 733 | // If the active points (the points we can actually see) change, then we need to update the path. 734 | private func activePointsDidChange() { 735 | 736 | let deactivatedPoints = determineDeactivatedPoints() 737 | let activatedPoints = determineActivatedPoints() 738 | 739 | updatePaths() 740 | if(shouldShowLabels) { 741 | updateLabels(deactivatedPoints, activatedPoints) 742 | } 743 | } 744 | 745 | private func rangeDidChange() { 746 | 747 | // If shouldAnimateOnAdapt is enabled it will kickoff any animations that need to occur. 748 | startAnimations() 749 | 750 | referenceLineView?.setRange(range) 751 | } 752 | 753 | private func viewportDidChange() { 754 | 755 | // We need to make sure all the drawing views are the same size as the viewport. 756 | updateFrames() 757 | 758 | // Basically this recreates the paths with the new viewport size so things are in sync, but only 759 | // if the viewport has changed after the initial setup. Because the initial setup will use the latest 760 | // viewport anyway. 761 | if(!isInitialSetup) { 762 | updatePaths() 763 | 764 | // Need to update the graph points so they are in their right positions for the new viewport. 765 | // Animate them into position if animation is enabled, but make sure to stop any current animations first. 766 | dequeueAllAnimations() 767 | startAnimations() 768 | 769 | // The labels will also need to be repositioned if the viewport has changed. 770 | repositionActiveLabels() 771 | } 772 | } 773 | 774 | // Update any paths with the new path based on visible data points. 775 | private func updatePaths() { 776 | createLinePath() 777 | 778 | if let drawingLayers = drawingView.layer.sublayers { 779 | for layer in drawingLayers { 780 | if let layer = layer as? ScrollableGraphViewDrawingLayer { 781 | layer.updatePath() 782 | } 783 | } 784 | } 785 | } 786 | 787 | // Update any labels for any new points that have been activated and deactivated. 788 | private func updateLabels(deactivatedPoints: [Int], _ activatedPoints: [Int]) { 789 | 790 | // Disable any labels for the deactivated points. 791 | for point in deactivatedPoints { 792 | labelPool.deactivateLabelForPointIndex(point) 793 | } 794 | 795 | // Grab an unused label and update it to the right position for the newly activated poitns 796 | for point in activatedPoints { 797 | let label = labelPool.activateLabelForPointIndex(point) 798 | 799 | label.text = (point < labels.count) ? labels[point] : "" 800 | label.textColor = dataPointLabelColor 801 | label.font = dataPointLabelFont 802 | 803 | label.sizeToFit() 804 | 805 | // self.range.min is the current ranges minimum that has been detected 806 | // self.rangeMin is the minimum that should be used as specified by the user 807 | let rangeMin = (shouldAutomaticallyDetectRange || shouldAdaptRange) ? self.range.min : self.rangeMin 808 | let position = calculatePosition(point, value: rangeMin) 809 | 810 | label.frame = CGRect(origin: CGPoint(x: position.x - label.frame.width / 2, y: position.y + dataPointLabelTopMargin), size: label.frame.size) 811 | 812 | labelsView.addSubview(label) 813 | } 814 | } 815 | 816 | private func repositionActiveLabels() { 817 | for label in labelPool.activeLabels { 818 | 819 | let rangeMin = (shouldAutomaticallyDetectRange || shouldAdaptRange) ? self.range.min : self.rangeMin 820 | let position = calculatePosition(0, value: rangeMin) 821 | 822 | label.frame.origin.y = position.y + dataPointLabelTopMargin 823 | } 824 | } 825 | 826 | // Returns the indices of any points that became inactive (that is, "off screen"). (No order) 827 | private func determineDeactivatedPoints() -> [Int] { 828 | let prevSet = setFromClosedRange(previousActivePointsInterval) 829 | let currSet = setFromClosedRange(activePointsInterval) 830 | 831 | let deactivatedPoints = prevSet.subtract(currSet) 832 | 833 | return Array(deactivatedPoints) 834 | } 835 | 836 | // Returns the indices of any points that became active (on screen). (No order) 837 | private func determineActivatedPoints() -> [Int] { 838 | let prevSet = setFromClosedRange(previousActivePointsInterval) 839 | let currSet = setFromClosedRange(activePointsInterval) 840 | 841 | let activatedPoints = currSet.subtract(prevSet) 842 | 843 | return Array(activatedPoints) 844 | } 845 | 846 | private func setFromClosedRange(range: Range) -> Set { 847 | var set = Set() 848 | for index in range.startIndex...range.endIndex { 849 | set.insert(index) 850 | } 851 | return set 852 | } 853 | 854 | private func startAnimations(withStaggerValue stagger: Double = 0) { 855 | 856 | var pointsToAnimate = 0 ..< 0 857 | 858 | if (shouldAnimateOnAdapt || (dataNeedsReloading && shouldAnimateOnStartup)) { 859 | pointsToAnimate = activePointsInterval 860 | } 861 | 862 | // For any visible points, kickoff the animation to their new position after the axis' min/max has changed. 863 | //let numberOfPointsToAnimate = pointsToAnimate.endIndex - pointsToAnimate.startIndex 864 | var index = 0 865 | for i in pointsToAnimate.startIndex ... pointsToAnimate.endIndex { 866 | let newPosition = calculatePosition(i, value: data[i]) 867 | let point = graphPoints[i] 868 | animatePoint(point, toPosition: newPosition, withDelay: Double(index) * stagger) 869 | index += 1 870 | } 871 | 872 | // Update any non-visible & non-animating points so they come on to screen at the right scale. 873 | for i in 0 ..< graphPoints.count { 874 | if(i > pointsToAnimate.startIndex && i < pointsToAnimate.endIndex || graphPoints[i].currentlyAnimatingToPosition) { 875 | continue 876 | } 877 | 878 | let newPosition = calculatePosition(i, value: data[i]) 879 | graphPoints[i].x = newPosition.x 880 | graphPoints[i].y = newPosition.y 881 | } 882 | } 883 | 884 | // MARK: - Drawing Delegate 885 | private func currentPath() -> UIBezierPath { 886 | return currentLinePath 887 | } 888 | 889 | private func intervalForActivePoints() -> Range { 890 | return activePointsInterval 891 | } 892 | 893 | private func rangeForActivePoints() -> (min: Double, max: Double) { 894 | return range 895 | } 896 | 897 | private func dataForGraph() -> [Double] { 898 | return data 899 | } 900 | 901 | private func graphPointForIndex(index: Int) -> GraphPoint { 902 | return graphPoints[index] 903 | } 904 | } 905 | 906 | // MARK: - LabelPool 907 | private class LabelPool { 908 | 909 | var labels = [UILabel]() 910 | var relations = [Int : Int]() 911 | var unused = [Int]() 912 | 913 | func deactivateLabelForPointIndex(pointIndex: Int){ 914 | 915 | if let unusedLabelIndex = relations[pointIndex] { 916 | unused.append(unusedLabelIndex) 917 | } 918 | relations[pointIndex] = nil 919 | } 920 | 921 | func activateLabelForPointIndex(pointIndex: Int) -> UILabel { 922 | var label: UILabel 923 | 924 | if(unused.count >= 1) { 925 | let unusedLabelIndex = unused.first! 926 | unused.removeFirst() 927 | 928 | label = labels[unusedLabelIndex] 929 | relations[pointIndex] = unusedLabelIndex 930 | } 931 | else { 932 | label = UILabel() 933 | labels.append(label) 934 | let newLabelIndex = labels.indexOf(label)! 935 | relations[pointIndex] = newLabelIndex 936 | } 937 | 938 | return label 939 | } 940 | 941 | var activeLabels: [UILabel] { 942 | get { 943 | 944 | var currentlyActive = [UILabel]() 945 | let numberOfLabels = labels.count 946 | 947 | for i in 0 ..< numberOfLabels { 948 | if(!unused.contains(i)) { 949 | currentlyActive.append(labels[i]) 950 | } 951 | } 952 | return currentlyActive 953 | } 954 | } 955 | } 956 | 957 | 958 | // MARK: - GraphPoints and Animation Classes 959 | private class GraphPoint { 960 | 961 | var location = CGPoint(x: 0, y: 0) 962 | var currentlyAnimatingToPosition = false 963 | 964 | private var x: CGFloat { 965 | get { 966 | return location.x 967 | } 968 | set { 969 | location.x = newValue 970 | } 971 | } 972 | 973 | private var y: CGFloat { 974 | get { 975 | return location.y 976 | } 977 | set { 978 | location.y = newValue 979 | } 980 | } 981 | 982 | init(position: CGPoint = CGPointZero) { 983 | x = position.x 984 | y = position.y 985 | } 986 | } 987 | 988 | private class GraphPointAnimation : Equatable { 989 | 990 | // Public Properties 991 | var animationEasing = Easings.EaseOutQuad 992 | var duration: Double = 1 993 | var delay: Double = 0 994 | private(set) var finished = false 995 | private(set) var animationKey: String 996 | 997 | // Private State 998 | private var startingPoint: CGPoint 999 | private var endingPoint: CGPoint 1000 | 1001 | private var elapsedTime: Double = 0 1002 | private var graphPoint: GraphPoint? 1003 | private var multiplier: Double = 1 1004 | 1005 | static private var animationsCreated = 0 1006 | 1007 | init(fromPoint: CGPoint, toPoint: CGPoint, forGraphPoint graphPoint: GraphPoint, forKey key: String = "animation\(animationsCreated)") { 1008 | self.startingPoint = fromPoint 1009 | self.endingPoint = toPoint 1010 | self.animationKey = key 1011 | self.graphPoint = graphPoint 1012 | self.graphPoint?.currentlyAnimatingToPosition = true 1013 | 1014 | GraphPointAnimation.animationsCreated += 1 1015 | } 1016 | 1017 | func update(dt: Double) { 1018 | 1019 | if(!finished) { 1020 | 1021 | if elapsedTime > delay { 1022 | 1023 | let animationElapsedTime = elapsedTime - delay 1024 | 1025 | let changeInX = endingPoint.x - startingPoint.x 1026 | let changeInY = endingPoint.y - startingPoint.y 1027 | 1028 | // t is in the range of 0 to 1, indicates how far through the animation it is. 1029 | let t = animationElapsedTime / duration 1030 | let interpolation = animationEasing(t) 1031 | 1032 | let x = startingPoint.x + changeInX * CGFloat(interpolation) 1033 | let y = startingPoint.y + changeInY * CGFloat(interpolation) 1034 | 1035 | if(animationElapsedTime >= duration) { 1036 | animationDidFinish() 1037 | } 1038 | 1039 | graphPoint?.x = CGFloat(x) 1040 | graphPoint?.y = CGFloat(y) 1041 | 1042 | elapsedTime += dt * multiplier 1043 | } 1044 | // Keep going until we are passed the delay 1045 | else { 1046 | elapsedTime += dt * multiplier 1047 | } 1048 | } 1049 | } 1050 | 1051 | func animationDidFinish() { 1052 | self.graphPoint?.currentlyAnimatingToPosition = false 1053 | self.finished = true 1054 | } 1055 | } 1056 | 1057 | private func ==(lhs: GraphPointAnimation, rhs: GraphPointAnimation) -> Bool { 1058 | return lhs.animationKey == rhs.animationKey 1059 | } 1060 | 1061 | 1062 | // MARK: - Drawing Layers 1063 | 1064 | // MARK: Delegate definition that provides the data required by the drawing layers. 1065 | private protocol ScrollableGraphViewDrawingDelegate { 1066 | func intervalForActivePoints() -> Range 1067 | func rangeForActivePoints() -> (min: Double, max: Double) 1068 | func dataForGraph() -> [Double] 1069 | func graphPointForIndex(index: Int) -> GraphPoint 1070 | 1071 | func currentPath() -> UIBezierPath 1072 | } 1073 | 1074 | // MARK: Drawing Layer Classes 1075 | 1076 | // MARK: Base Class 1077 | private class ScrollableGraphViewDrawingLayer : CAShapeLayer { 1078 | 1079 | var offset: CGFloat = 0 { 1080 | didSet { 1081 | offsetDidChange() 1082 | } 1083 | } 1084 | 1085 | var viewportWidth: CGFloat = 0 1086 | var viewportHeight: CGFloat = 0 1087 | 1088 | var graphViewDrawingDelegate: ScrollableGraphViewDrawingDelegate? = nil 1089 | 1090 | var active = true 1091 | 1092 | init(viewportWidth: CGFloat, viewportHeight: CGFloat, offset: CGFloat = 0) { 1093 | super.init() 1094 | 1095 | self.viewportWidth = viewportWidth 1096 | self.viewportHeight = viewportHeight 1097 | 1098 | self.frame = CGRect(origin: CGPoint(x: offset, y: 0), size: CGSize(width: self.viewportWidth, height: self.viewportHeight)) 1099 | 1100 | setup() 1101 | } 1102 | 1103 | required init?(coder aDecoder: NSCoder) { 1104 | fatalError("init(coder:) has not been implemented") 1105 | } 1106 | 1107 | private func setup() { 1108 | // Get rid of any animations. 1109 | self.actions = ["position" : NSNull(), "bounds" : NSNull()] 1110 | } 1111 | 1112 | private func offsetDidChange() { 1113 | self.frame.origin.x = offset 1114 | self.bounds.origin.x = offset 1115 | } 1116 | 1117 | func updatePath() { 1118 | fatalError("updatePath needs to be implemented by the subclass") 1119 | } 1120 | } 1121 | 1122 | // MARK: Drawing the Graph Line 1123 | private class LineDrawingLayer : ScrollableGraphViewDrawingLayer { 1124 | 1125 | init(frame: CGRect, lineWidth: CGFloat, lineColor: UIColor, lineStyle: ScrollableGraphViewLineStyle, lineJoin: String, lineCap: String) { 1126 | super.init(viewportWidth: frame.size.width, viewportHeight: frame.size.height) 1127 | 1128 | self.lineWidth = lineWidth 1129 | self.strokeColor = lineColor.CGColor 1130 | 1131 | self.lineJoin = lineJoin 1132 | self.lineCap = lineCap 1133 | 1134 | // Setup 1135 | self.fillColor = UIColor.clearColor().CGColor // This is handled by the fill drawing layer. 1136 | } 1137 | 1138 | required init?(coder aDecoder: NSCoder) { 1139 | fatalError("init(coder:) has not been implemented") 1140 | } 1141 | 1142 | override func updatePath() { 1143 | self.path = graphViewDrawingDelegate?.currentPath().CGPath 1144 | } 1145 | } 1146 | 1147 | // MARK: Drawing the Individual Data Points 1148 | private class DataPointDrawingLayer: ScrollableGraphViewDrawingLayer { 1149 | 1150 | private var dataPointPath = UIBezierPath() 1151 | private var dataPointSize: CGFloat = 5 1152 | private var dataPointType: ScrollableGraphViewDataPointType = .Circle 1153 | 1154 | private var customDataPointPath: ((centre: CGPoint) -> UIBezierPath)? 1155 | 1156 | init(frame: CGRect, fillColor: UIColor, dataPointType: ScrollableGraphViewDataPointType, dataPointSize: CGFloat, customDataPointPath: ((centre: CGPoint) -> UIBezierPath)? = nil) { 1157 | 1158 | self.dataPointType = dataPointType 1159 | self.dataPointSize = dataPointSize 1160 | self.customDataPointPath = customDataPointPath 1161 | 1162 | super.init(viewportWidth: frame.size.width, viewportHeight: frame.size.height) 1163 | 1164 | self.fillColor = fillColor.CGColor 1165 | } 1166 | 1167 | required init?(coder aDecoder: NSCoder) { 1168 | fatalError("init(coder:) has not been implemented") 1169 | } 1170 | 1171 | private func createDataPointPath() -> UIBezierPath { 1172 | 1173 | dataPointPath.removeAllPoints() 1174 | 1175 | // We can only move forward if we can get the data we need from the delegate. 1176 | guard let 1177 | activePointsInterval = self.graphViewDrawingDelegate?.intervalForActivePoints(), 1178 | data = self.graphViewDrawingDelegate?.dataForGraph() 1179 | else { 1180 | return dataPointPath 1181 | } 1182 | 1183 | let pointPathCreator = getPointPathCreator() 1184 | let numberOfPoints = min(data.count, activePointsInterval.endIndex) 1185 | 1186 | for i in activePointsInterval.startIndex ... numberOfPoints { 1187 | 1188 | var location = CGPointZero 1189 | 1190 | if let pointLocation = self.graphViewDrawingDelegate?.graphPointForIndex(i).location { 1191 | location = pointLocation 1192 | } 1193 | 1194 | let pointPath = pointPathCreator(centre: location) 1195 | dataPointPath.appendPath(pointPath) 1196 | } 1197 | 1198 | return dataPointPath 1199 | } 1200 | 1201 | private func createCircleDataPoint(centre: CGPoint) -> UIBezierPath { 1202 | return UIBezierPath(arcCenter: centre, radius: dataPointSize, startAngle: 0, endAngle: CGFloat(2.0 * M_PI), clockwise: true) 1203 | } 1204 | 1205 | private func createSquareDataPoint(centre: CGPoint) -> UIBezierPath { 1206 | 1207 | let squarePath = UIBezierPath() 1208 | 1209 | squarePath.moveToPoint(centre) 1210 | 1211 | let topLeft = CGPoint(x: centre.x - dataPointSize, y: centre.y - dataPointSize) 1212 | let topRight = CGPoint(x: centre.x + dataPointSize, y: centre.y - dataPointSize) 1213 | let bottomLeft = CGPoint(x: centre.x - dataPointSize, y: centre.y + dataPointSize) 1214 | let bottomRight = CGPoint(x: centre.x + dataPointSize, y: centre.y + dataPointSize) 1215 | 1216 | squarePath.moveToPoint(topLeft) 1217 | squarePath.addLineToPoint(topRight) 1218 | squarePath.addLineToPoint(bottomRight) 1219 | squarePath.addLineToPoint(bottomLeft) 1220 | squarePath.addLineToPoint(topLeft) 1221 | 1222 | return squarePath 1223 | } 1224 | 1225 | private func getPointPathCreator() -> (centre: CGPoint) -> UIBezierPath { 1226 | switch(self.dataPointType) { 1227 | case .Circle: 1228 | return createCircleDataPoint 1229 | case .Square: 1230 | return createSquareDataPoint 1231 | case .Custom: 1232 | if let customCreator = self.customDataPointPath { 1233 | return customCreator 1234 | } 1235 | else { 1236 | // We don't have a custom path, so just return the default. 1237 | fallthrough 1238 | } 1239 | default: 1240 | return createCircleDataPoint 1241 | } 1242 | } 1243 | 1244 | override func updatePath() { 1245 | self.path = createDataPointPath().CGPath 1246 | } 1247 | } 1248 | 1249 | // MARK: Drawing the Graph Gradient Fill 1250 | private class GradientDrawingLayer : ScrollableGraphViewDrawingLayer { 1251 | 1252 | private var startColor: UIColor 1253 | private var endColor: UIColor 1254 | private var gradientType: ScrollableGraphViewGradientType 1255 | 1256 | lazy private var gradientMask: CAShapeLayer = ({ 1257 | let mask = CAShapeLayer() 1258 | 1259 | mask.frame = CGRect(x: 0, y: 0, width: self.viewportWidth, height: self.viewportHeight) 1260 | mask.fillRule = kCAFillRuleEvenOdd 1261 | mask.path = self.graphViewDrawingDelegate?.currentPath().CGPath 1262 | mask.lineJoin = self.lineJoin 1263 | 1264 | return mask 1265 | })() 1266 | 1267 | init(frame: CGRect, startColor: UIColor, endColor: UIColor, gradientType: ScrollableGraphViewGradientType, lineJoin: String = kCALineJoinRound) { 1268 | self.startColor = startColor 1269 | self.endColor = endColor 1270 | self.gradientType = gradientType 1271 | //self.lineJoin = lineJoin 1272 | 1273 | super.init(viewportWidth: frame.size.width, viewportHeight: frame.size.height) 1274 | 1275 | addMaskLayer() 1276 | self.setNeedsDisplay() 1277 | } 1278 | 1279 | required init?(coder aDecoder: NSCoder) { 1280 | fatalError("init(coder:) has not been implemented") 1281 | } 1282 | 1283 | private func addMaskLayer() { 1284 | self.mask = gradientMask 1285 | } 1286 | 1287 | override func updatePath() { 1288 | gradientMask.path = graphViewDrawingDelegate?.currentPath().CGPath 1289 | } 1290 | 1291 | override func drawInContext(ctx: CGContext) { 1292 | 1293 | let colors = [startColor.CGColor, endColor.CGColor] 1294 | let colorSpace = CGColorSpaceCreateDeviceRGB() 1295 | let locations: [CGFloat] = [0.0, 1.0] 1296 | let gradient = CGGradientCreateWithColors(colorSpace, colors, locations) 1297 | 1298 | let displacement = ((viewportWidth / viewportHeight) / 2.5) * self.bounds.height 1299 | let topCentre = CGPoint(x: offset + self.bounds.width / 2, y: -displacement) 1300 | let bottomCentre = CGPoint(x: offset + self.bounds.width / 2, y: self.bounds.height) 1301 | let startRadius: CGFloat = 0 1302 | let endRadius: CGFloat = self.bounds.width 1303 | 1304 | switch(gradientType) { 1305 | case .Linear: 1306 | CGContextDrawLinearGradient(ctx, gradient, topCentre, bottomCentre, .DrawsAfterEndLocation) 1307 | case .Radial: 1308 | CGContextDrawRadialGradient(ctx, gradient, topCentre, startRadius, topCentre, endRadius, .DrawsAfterEndLocation) 1309 | } 1310 | } 1311 | } 1312 | 1313 | // MARK: Drawing the Graph Fill 1314 | private class FillDrawingLayer : ScrollableGraphViewDrawingLayer { 1315 | 1316 | init(frame: CGRect, fillColor: UIColor) { 1317 | super.init(viewportWidth: frame.size.width, viewportHeight: frame.size.height) 1318 | self.fillColor = fillColor.CGColor 1319 | } 1320 | 1321 | required init?(coder aDecoder: NSCoder) { 1322 | fatalError("init(coder:) has not been implemented") 1323 | } 1324 | 1325 | override func updatePath() { 1326 | self.path = graphViewDrawingDelegate?.currentPath().CGPath 1327 | } 1328 | } 1329 | 1330 | 1331 | // MARK: - Reference Lines 1332 | private class ReferenceLineDrawingView : UIView { 1333 | 1334 | // PUBLIC PROPERTIES 1335 | 1336 | // Reference line settings. 1337 | var referenceLineColor: UIColor = UIColor.blackColor() 1338 | var referenceLineThickness: CGFloat = 0.5 1339 | var referenceLinePosition = ScrollableGraphViewReferenceLinePosition.Left 1340 | var referenceLineType = ScrollableGraphViewReferenceLineType.Cover 1341 | 1342 | var numberOfIntermediateReferenceLines = 3 // Number of reference lines between the min and max line. 1343 | 1344 | // Reference line label settings. 1345 | var shouldAddLabelsToIntermediateReferenceLines: Bool = true 1346 | var shouldAddUnitsToIntermediateReferenceLineLabels: Bool = false 1347 | 1348 | var labelUnits: String? 1349 | var labelFont: UIFont = UIFont.systemFontOfSize(8) 1350 | var labelColor: UIColor = UIColor.blackColor() 1351 | var labelDecimalPlaces: Int = 2 1352 | 1353 | // PRIVATE PROPERTIES 1354 | 1355 | private var intermediateLineWidthMultiplier: CGFloat = 1 //FUTURE: Can make the intermediate lines shorter using this. 1356 | private var referenceLineWidth: CGFloat = 100 // FUTURE: Used when referenceLineType == .Edge 1357 | 1358 | private var labelMargin: CGFloat = 4 1359 | private var leftLabelInset: CGFloat = 10 1360 | private var rightLabelInset: CGFloat = 10 1361 | 1362 | // Store information about the ScrollableGraphView 1363 | private var currentRange: (min: Double, max: Double) = (0,100) 1364 | private var topMargin: CGFloat = 10 1365 | private var bottomMargin: CGFloat = 10 1366 | 1367 | // Partition recursion depth // FUTURE: For .Edge 1368 | // private var referenceLinePartitions: Int = 3 1369 | 1370 | private var lineWidth: CGFloat { 1371 | get { 1372 | if(self.referenceLineType == ScrollableGraphViewReferenceLineType.Cover) { 1373 | return self.bounds.width 1374 | } 1375 | else { 1376 | return referenceLineWidth 1377 | } 1378 | } 1379 | } 1380 | 1381 | private var units: String { 1382 | get { 1383 | if let units = self.labelUnits { 1384 | return " \(units)" 1385 | } else { 1386 | return "" 1387 | } 1388 | } 1389 | } 1390 | 1391 | // Layers 1392 | private var labels = [UILabel]() 1393 | private let referenceLineLayer = CAShapeLayer() 1394 | private let referenceLinePath = UIBezierPath() 1395 | 1396 | init(frame: CGRect, topMargin: CGFloat, bottomMargin: CGFloat, referenceLineColor: UIColor, referenceLineThickness: CGFloat) { 1397 | super.init(frame: frame) 1398 | 1399 | self.topMargin = topMargin 1400 | self.bottomMargin = bottomMargin 1401 | 1402 | // The reference line layer draws the reference lines and we handle the labels elsewhere. 1403 | self.referenceLineLayer.frame = self.frame 1404 | self.referenceLineLayer.strokeColor = referenceLineColor.CGColor 1405 | self.referenceLineLayer.lineWidth = referenceLineThickness 1406 | 1407 | self.layer.addSublayer(referenceLineLayer) 1408 | } 1409 | 1410 | required init?(coder aDecoder: NSCoder) { 1411 | fatalError("init(coder:) has not been implemented") 1412 | } 1413 | 1414 | private func createLabelAtPosition(position: CGPoint, withText text: String) -> UILabel { 1415 | let frame = CGRect(x: position.x, y: position.y, width: 0, height: 0) 1416 | let label = UILabel(frame: frame) 1417 | 1418 | return label 1419 | } 1420 | 1421 | private func createReferenceLinesPath() -> UIBezierPath { 1422 | 1423 | referenceLinePath.removeAllPoints() 1424 | for label in labels { 1425 | label.removeFromSuperview() 1426 | } 1427 | labels.removeAll() 1428 | 1429 | let maxLineStart = CGPoint(x: 0, y: topMargin) 1430 | let maxLineEnd = CGPoint(x: lineWidth, y: topMargin) 1431 | 1432 | let minLineStart = CGPoint(x: 0, y: self.bounds.height - bottomMargin) 1433 | let minLineEnd = CGPoint(x: lineWidth, y: self.bounds.height - bottomMargin) 1434 | 1435 | let maxString = String(format: "%.\(labelDecimalPlaces)f", arguments: [self.currentRange.max]) + units 1436 | let minString = String(format: "%.\(labelDecimalPlaces)f", arguments: [self.currentRange.min]) + units 1437 | 1438 | addLineWithTag(maxString, from: maxLineStart, to: maxLineEnd, inPath: referenceLinePath) 1439 | addLineWithTag(minString, from: minLineStart, to: minLineEnd, inPath: referenceLinePath) 1440 | 1441 | let initialRect = CGRect(x: self.bounds.origin.x, y: self.bounds.origin.y + topMargin, width: self.bounds.size.width, height: self.bounds.size.height - (topMargin + bottomMargin)) 1442 | 1443 | createIntermediateReferenceLines(initialRect, numberOfIntermediateReferenceLines: self.numberOfIntermediateReferenceLines, forPath: referenceLinePath) 1444 | 1445 | return referenceLinePath 1446 | } 1447 | 1448 | private func createIntermediateReferenceLines(rect: CGRect, numberOfIntermediateReferenceLines: Int, forPath path: UIBezierPath) { 1449 | 1450 | let height = rect.size.height 1451 | let spacePerPartition = height / CGFloat(numberOfIntermediateReferenceLines + 1) 1452 | 1453 | for i in 0 ..< numberOfIntermediateReferenceLines { 1454 | 1455 | let lineStart = CGPoint(x: 0, y: rect.origin.y + (spacePerPartition * CGFloat(i + 1))) 1456 | let lineEnd = CGPoint(x: lineStart.x + lineWidth * intermediateLineWidthMultiplier, y: lineStart.y) 1457 | 1458 | createReferenceLineFrom(from: lineStart, to: lineEnd, inPath: path) 1459 | } 1460 | } 1461 | 1462 | // FUTURE: Can use the recursive version to create a ruler like look on the edge. 1463 | private func recursiveCreateIntermediateReferenceLines(rect: CGRect, width: CGFloat, forPath path: UIBezierPath, remainingPartitions: Int) -> UIBezierPath { 1464 | 1465 | if(remainingPartitions <= 0) { 1466 | return path 1467 | } 1468 | 1469 | let lineStart = CGPoint(x: 0, y: rect.origin.y + (rect.size.height / 2)) 1470 | let lineEnd = CGPoint(x: lineStart.x + width, y: lineStart.y) 1471 | 1472 | createReferenceLineFrom(from: lineStart, to: lineEnd, inPath: path) 1473 | 1474 | let topRect = CGRect( 1475 | x: rect.origin.x, 1476 | y: rect.origin.y, 1477 | width: rect.size.width, 1478 | height: rect.size.height / 2) 1479 | 1480 | let bottomRect = CGRect( 1481 | x: rect.origin.x, 1482 | y: rect.origin.y + (rect.size.height / 2), 1483 | width: rect.size.width, 1484 | height: rect.size.height / 2) 1485 | 1486 | recursiveCreateIntermediateReferenceLines(topRect, width: width * intermediateLineWidthMultiplier, forPath: path, remainingPartitions: remainingPartitions - 1) 1487 | recursiveCreateIntermediateReferenceLines(bottomRect, width: width * intermediateLineWidthMultiplier, forPath: path, remainingPartitions: remainingPartitions - 1) 1488 | 1489 | return path 1490 | } 1491 | 1492 | private func createReferenceLineFrom(from lineStart: CGPoint, to lineEnd: CGPoint, inPath path: UIBezierPath) { 1493 | if(shouldAddLabelsToIntermediateReferenceLines) { 1494 | 1495 | let value = calculateYAxisValueForPoint(lineStart) 1496 | var valueString = String(format: "%.\(labelDecimalPlaces)f", arguments: [value]) 1497 | 1498 | if(shouldAddUnitsToIntermediateReferenceLineLabels) { 1499 | valueString += " \(units)" 1500 | } 1501 | 1502 | addLineWithTag(valueString, from: lineStart, to: lineEnd, inPath: path) 1503 | 1504 | } else { 1505 | addLineFrom(lineStart, to: lineEnd, inPath: path) 1506 | } 1507 | } 1508 | 1509 | private func addLineWithTag(tag: String, from: CGPoint, to: CGPoint, inPath path: UIBezierPath) { 1510 | 1511 | let boundingSize = boundingSizeForText(tag) 1512 | let leftLabel = createLabelWithText(tag) 1513 | let rightLabel = createLabelWithText(tag) 1514 | 1515 | // Left label gap. 1516 | leftLabel.frame = CGRect( 1517 | origin: CGPoint(x: from.x + leftLabelInset, y: from.y - (boundingSize.height / 2)), 1518 | size: boundingSize) 1519 | 1520 | let leftLabelStart = CGPoint(x: leftLabel.frame.origin.x - labelMargin, y: to.y) 1521 | let leftLabelEnd = CGPoint(x: (leftLabel.frame.origin.x + leftLabel.frame.size.width) + labelMargin, y: to.y) 1522 | 1523 | // Right label gap. 1524 | rightLabel.frame = CGRect( 1525 | origin: CGPoint(x: (from.x + self.frame.width) - rightLabelInset - boundingSize.width, y: from.y - (boundingSize.height / 2)), 1526 | size: boundingSize) 1527 | 1528 | let rightLabelStart = CGPoint(x: rightLabel.frame.origin.x - labelMargin, y: to.y) 1529 | let rightLabelEnd = CGPoint(x: (rightLabel.frame.origin.x + rightLabel.frame.size.width) + labelMargin, y: to.y) 1530 | 1531 | // Add the lines and tags depending on the settings for where we want them. 1532 | var gaps = [(start: CGFloat, end: CGFloat)]() 1533 | 1534 | switch(self.referenceLinePosition) { 1535 | 1536 | case .Left: 1537 | gaps.append((start: leftLabelStart.x, end: leftLabelEnd.x)) 1538 | self.addSubview(leftLabel) 1539 | self.labels.append(leftLabel) 1540 | 1541 | case .Right: 1542 | gaps.append((start: rightLabelStart.x, end: rightLabelEnd.x)) 1543 | self.addSubview(rightLabel) 1544 | self.labels.append(rightLabel) 1545 | 1546 | case .Both: 1547 | gaps.append((start: leftLabelStart.x, end: leftLabelEnd.x)) 1548 | gaps.append((start: rightLabelStart.x, end: rightLabelEnd.x)) 1549 | self.addSubview(leftLabel) 1550 | self.addSubview(rightLabel) 1551 | self.labels.append(leftLabel) 1552 | self.labels.append(rightLabel) 1553 | } 1554 | 1555 | addLineWithGaps(from, to: to, withGaps: gaps, inPath: path) 1556 | } 1557 | 1558 | private func addLineWithGaps(from: CGPoint, to: CGPoint, withGaps gaps: [(start: CGFloat, end: CGFloat)], inPath path: UIBezierPath) { 1559 | 1560 | // If there are no gaps, just add a single line. 1561 | if(gaps.count <= 0) { 1562 | addLineFrom(from, to: to, inPath: path) 1563 | } 1564 | // If there is only 1 gap, it's just two lines. 1565 | else if (gaps.count == 1) { 1566 | 1567 | let gapLeft = CGPoint(x: gaps.first!.start, y: from.y) 1568 | let gapRight = CGPoint(x: gaps.first!.end, y: from.y) 1569 | 1570 | addLineFrom(from, to: gapLeft, inPath: path) 1571 | addLineFrom(gapRight, to: to, inPath: path) 1572 | } 1573 | // If there are many gaps, we have a series of intermediate lines. 1574 | else { 1575 | 1576 | let firstGap = gaps.first! 1577 | let lastGap = gaps.last! 1578 | 1579 | let firstGapLeft = CGPoint(x: firstGap.start, y: from.y) 1580 | let lastGapRight = CGPoint(x: lastGap.end, y: to.y) 1581 | 1582 | // Add the first line to the start of the first gap 1583 | addLineFrom(from, to: firstGapLeft, inPath: path) 1584 | 1585 | // Add lines between all intermediate gaps 1586 | for i in 0 ..< gaps.count - 1 { 1587 | 1588 | let startGapEnd = gaps[i].end 1589 | let endGapStart = gaps[i + 1].start 1590 | 1591 | let lineStart = CGPoint(x: startGapEnd, y: from.y) 1592 | let lineEnd = CGPoint(x: endGapStart, y: from.y) 1593 | 1594 | addLineFrom(lineStart, to: lineEnd, inPath: path) 1595 | } 1596 | 1597 | // Add the final line to the end 1598 | addLineFrom(lastGapRight, to: to, inPath: path) 1599 | } 1600 | } 1601 | 1602 | private func addLineFrom(from: CGPoint, to: CGPoint, inPath path: UIBezierPath) { 1603 | path.moveToPoint(from) 1604 | path.addLineToPoint(to) 1605 | } 1606 | 1607 | private func boundingSizeForText(text: String) -> CGSize { 1608 | return (text as NSString).sizeWithAttributes([NSFontAttributeName:labelFont]) 1609 | } 1610 | 1611 | private func calculateYAxisValueForPoint(point: CGPoint) -> Double { 1612 | 1613 | let graphHeight = self.frame.size.height - (topMargin + bottomMargin) 1614 | 1615 | // value = the corresponding value on the graph for any y co-ordinate in the view 1616 | // y - t y = the y co-ordinate in the view for which we want to know the corresponding value on the graph 1617 | // value = --------- * (min - max) + max t = the top margin 1618 | // h h = the height of the graph space without margins 1619 | // min = the range's current mininum 1620 | // max = the range's current maximum 1621 | 1622 | var value = (((point.y - topMargin) / (graphHeight)) * CGFloat((self.currentRange.min - self.currentRange.max))) + CGFloat(self.currentRange.max) 1623 | 1624 | // Sometimes results in "negative zero" 1625 | if(value == 0) { 1626 | value = 0 1627 | } 1628 | 1629 | return Double(value) 1630 | } 1631 | 1632 | private func createLabelWithText(text: String) -> UILabel { 1633 | let label = UILabel() 1634 | 1635 | label.text = text 1636 | label.textColor = labelColor 1637 | label.font = labelFont 1638 | 1639 | return label 1640 | } 1641 | 1642 | // Public functions to update the reference lines with any changes to the range and viewport (phone rotation, etc). 1643 | // When the range changes, need to update the max for the new range, then update all the labels that are showing for the axis and redraw the reference lines. 1644 | func setRange(range: (min: Double, max: Double)) { 1645 | self.currentRange = range 1646 | self.referenceLineLayer.path = createReferenceLinesPath().CGPath 1647 | } 1648 | 1649 | func setViewport(viewportWidth: CGFloat, viewportHeight: CGFloat) { 1650 | self.frame.size.width = viewportWidth 1651 | self.frame.size.height = viewportHeight 1652 | self.referenceLineLayer.path = createReferenceLinesPath().CGPath 1653 | } 1654 | } 1655 | 1656 | // MARK: - ScrollableGraphView Settings Enums 1657 | 1658 | @objc public enum ScrollableGraphViewLineStyle : Int { 1659 | case Straight 1660 | case Smooth 1661 | } 1662 | 1663 | @objc public enum ScrollableGraphViewFillType : Int { 1664 | case Solid 1665 | case Gradient 1666 | } 1667 | 1668 | @objc public enum ScrollableGraphViewGradientType : Int { 1669 | case Linear 1670 | case Radial 1671 | } 1672 | 1673 | @objc public enum ScrollableGraphViewDataPointType : Int { 1674 | case Circle 1675 | case Square 1676 | case Custom 1677 | } 1678 | 1679 | @objc public enum ScrollableGraphViewReferenceLinePosition : Int { 1680 | case Left 1681 | case Right 1682 | case Both 1683 | } 1684 | 1685 | @objc public enum ScrollableGraphViewReferenceLineType : Int { 1686 | case Cover 1687 | //case Edge // FUTURE: Implement 1688 | } 1689 | 1690 | @objc public enum ScrollableGraphViewAnimationType : Int { 1691 | case EaseOut 1692 | case Elastic 1693 | case Custom 1694 | } 1695 | 1696 | @objc public enum ScrollableGraphViewDirection : Int { 1697 | case LeftToRight 1698 | case RightToLeft 1699 | } 1700 | 1701 | // Simplified easing functions from: http://www.joshondesign.com/2013/03/01/improvedEasingEquations 1702 | private struct Easings { 1703 | 1704 | static let EaseInQuad = { (t:Double) -> Double in return t*t; } 1705 | static let EaseOutQuad = { (t:Double) -> Double in return 1 - Easings.EaseInQuad(1-t); } 1706 | 1707 | static let EaseOutElastic = { (t: Double) -> Double in 1708 | var p = 0.3; 1709 | return pow(2,-10*t) * sin((t-p/4)*(2*M_PI)/p) + 1; 1710 | } 1711 | } 1712 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Phillip 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ScrollableGraphView 2 | 3 | ## About 4 | 5 | ![Example Application Usage](readme_images/IMG_5814_small.jpg) 6 | 7 | An adaptive scrollable graph view for iOS to visualise simple discrete datasets. Written in Swift. Originally written for a small personal project. 8 | 9 | ![Init Animation](readme_images/init_anim_high_fps.gif) 10 | 11 | ## Contents 12 | 13 | - [Features](#features) 14 | - [Basic Usage](#usage) 15 | - [Gallery](#gallerythemes) 16 | - [Customisation](#customisation) 17 | - [Improvements](#improvements) 18 | - [Known Issues](#known-issues) 19 | - [Other](#other) 20 | 21 | ## Features 22 | 23 | ### Animating! 24 | 25 | ![Animating](readme_images/animating.gif) 26 | 27 | ### Manual/Auto/Adaptive Ranging! 28 | 29 | ![Adapting](readme_images/adapting.gif) 30 | 31 | ### Scrolling! 32 | 33 | ![Scrolling](readme_images/scrolling.gif) 34 | 35 | ### More Scrolling! 36 | 37 | ![More_Scrolling](readme_images/more_scrolling.gif) 38 | 39 | ### [Customising!](#Customisation) 40 | ![More_Scrolling](readme_images/customising.gif) 41 | 42 | ## Usage 43 | 44 | ### Adding the ScrollableGraphView to your project: 45 | 46 | Add the ```ScrollableGraphView``` class to your project. There are two ways to add the ScrollableGraphView to your project. 47 | 48 | #### Manually 49 | Add [ScrollableGraphView.swift](Classes/ScrollableGraphView.swift) to your project in Xcode 50 | 51 | #### CocoaPods 52 | Add ```pod 'ScrollableGraphView'``` to your Podfile and then make sure to ```import ScrollableGraphView``` in your code. 53 | 54 | ### Creating a graph and setting the data. 55 | 56 | 1. Create a ScrollableGraphView instance and set the data and labels 57 | ```swift 58 | let graphView = ScrollableGraphView(frame: someFrame) 59 | let data = [4, 8, 15, 16, 23, 42] 60 | let labels = ["one", "two", "three", "four", "five", "six"] 61 | graphView.setData(data, withLabels: labels) 62 | ``` 63 | 64 | 2. Add the ScrollableGraphView to the view hierarchy. 65 | ```swift 66 | someViewController.view.addSubview(graphView) 67 | ``` 68 | 69 | ### Things you *could* use it for: 70 | 71 | - ✔ Study applications to show time studied/etc 72 | - ✔ Weather applications 73 | - ✔ Prototyping 74 | - ✔ *Simple* data visualisation 75 | 76 | ### Things you **shouldn't/cannot** use it for: 77 | 78 | - ✘ Rigorous statistical software 79 | - ✘ Important & complex data visualisation 80 | - ✘ Graphing continuous mathematical functions 81 | 82 | 83 | ## Gallery/Themes 84 | 85 | _Note: Examples here use a "colorFromHex" extension for UIColor._ 86 | 87 | ### Default 88 | ![dark](readme_images/gallery/default.png) 89 | ```swift 90 | let graphView = ScrollableGraphView(frame: frame) 91 | graphView.setData(data, withLabels: labels) 92 | self.view.addSubview(graphView) 93 | ``` 94 | 95 | ### Smooth Dark 96 | ![dark](readme_images/gallery/dark.png) 97 | ```swift 98 | let graphView = ScrollableGraphView(frame: frame) 99 | 100 | graphView.backgroundFillColor = UIColor.colorFromHex("#333333") 101 | 102 | graphView.rangeMax = 50 103 | 104 | graphView.lineWidth = 1 105 | graphView.lineColor = UIColor.colorFromHex("#777777") 106 | graphView.lineStyle = ScrollableGraphViewLineStyle.Smooth 107 | 108 | graphView.shouldFill = true 109 | graphView.fillType = ScrollableGraphViewFillType.Gradient 110 | graphView.fillColor = UIColor.colorFromHex("#555555") 111 | graphView.fillGradientType = ScrollableGraphViewGradientType.Linear 112 | graphView.fillGradientStartColor = UIColor.colorFromHex("#555555") 113 | graphView.fillGradientEndColor = UIColor.colorFromHex("#444444") 114 | 115 | graphView.dataPointSpacing = 80 116 | graphView.dataPointSize = 2 117 | graphView.dataPointFillColor = UIColor.whiteColor() 118 | 119 | graphView.referenceLineLabelFont = UIFont.boldSystemFontOfSize(8) 120 | graphView.referenceLineColor = UIColor.whiteColor().colorWithAlphaComponent(0.2) 121 | graphView.referenceLineLabelColor = UIColor.whiteColor() 122 | graphView.dataPointLabelColor = UIColor.whiteColor().colorWithAlphaComponent(0.5) 123 | 124 | graphView.setData(data, withLabels: labels) 125 | self.view.addSubview(graphView) 126 | ``` 127 | 128 | ### Dot 129 | ![dot](readme_images/gallery/dot.png) 130 | ```swift 131 | let graphView = ScrollableGraphView(frame:frame) 132 | graphView.backgroundFillColor = UIColor.colorFromHex("#00BFFF") 133 | graphView.lineColor = UIColor.clearColor() 134 | 135 | graphView.dataPointSize = 5 136 | graphView.dataPointSpacing = 80 137 | graphView.dataPointLabelFont = UIFont.boldSystemFontOfSize(10) 138 | graphView.dataPointLabelColor = UIColor.whiteColor() 139 | graphView.dataPointFillColor = UIColor.whiteColor() 140 | 141 | graphView.referenceLineLabelFont = UIFont.boldSystemFontOfSize(10) 142 | graphView.referenceLineColor = UIColor.whiteColor().colorWithAlphaComponent(0.5) 143 | graphView.referenceLineLabelColor = UIColor.whiteColor() 144 | graphView.referenceLinePosition = ScrollableGraphViewReferenceLinePosition.Both 145 | 146 | graphView.numberOfIntermediateReferenceLines = 9 147 | 148 | graphView.rangeMax = 50 149 | 150 | self.view.addSubview(graphView) 151 | ``` 152 | 153 | ### Pink Mountain 154 | ![pink](readme_images/gallery/pink_mountain.png) 155 | ```swift 156 | let graphView = ScrollableGraphView(frame:frame) 157 | graphView.backgroundFillColor = UIColor.colorFromHex("#222222") 158 | graphView.lineColor = UIColor.clearColor() 159 | 160 | graphView.shouldFill = true 161 | graphView.fillColor = UIColor.colorFromHex("#FF0080") 162 | 163 | graphView.shouldDrawDataPoint = false 164 | graphView.dataPointSpacing = 80 165 | graphView.dataPointLabelFont = UIFont.boldSystemFontOfSize(10) 166 | graphView.dataPointLabelColor = UIColor.whiteColor() 167 | 168 | graphView.referenceLineThickness = 1 169 | graphView.referenceLineLabelFont = UIFont.boldSystemFontOfSize(10) 170 | graphView.referenceLineColor = UIColor.whiteColor().colorWithAlphaComponent(0.5) 171 | graphView.referenceLineLabelColor = UIColor.whiteColor() 172 | graphView.referenceLinePosition = ScrollableGraphViewReferenceLinePosition.Both 173 | 174 | graphView.numberOfIntermediateReferenceLines = 1 175 | 176 | graphView.rangeMax = 50 177 | 178 | self.view.addSubview(graphView) 179 | ``` 180 | 181 | ### Solid Pink with Margins 182 | You can use the top and bottom margin to leave space for other content: 183 | 184 | ![pink_margins](readme_images/gallery/pink_margins.png) 185 | ```swift 186 | let graphView = ScrollableGraphView(frame:frame) 187 | 188 | graphView.bottomMargin = 350 189 | graphView.topMargin = 20 190 | 191 | graphView.backgroundFillColor = UIColor.colorFromHex("#222222") 192 | graphView.lineColor = UIColor.clearColor() 193 | graphView.lineStyle = ScrollableGraphViewLineStyle.Smooth 194 | 195 | graphView.shouldFill = true 196 | graphView.fillColor = UIColor.colorFromHex("#FF0080") 197 | 198 | graphView.shouldDrawDataPoint = false 199 | graphView.dataPointSpacing = 80 200 | graphView.dataPointLabelFont = UIFont.boldSystemFontOfSize(10) 201 | graphView.dataPointLabelColor = UIColor.whiteColor() 202 | 203 | graphView.referenceLineThickness = 1 204 | graphView.referenceLineLabelFont = UIFont.boldSystemFontOfSize(10) 205 | graphView.referenceLineColor = UIColor.whiteColor().colorWithAlphaComponent(0.25) 206 | graphView.referenceLineLabelColor = UIColor.whiteColor() 207 | 208 | graphView.numberOfIntermediateReferenceLines = 0 209 | 210 | graphView.rangeMax = 50 211 | 212 | self.view.addSubview(graphView) 213 | ``` 214 | 215 | ## Customisation 216 | 217 | The graph can be customised by setting any of the following public properties before displaying the ScrollableGraphView. The defaults are shown below. 218 | 219 | ### Line Styles 220 | 221 | ```swift 222 | var lineWidth: CGFloat = 2 223 | ``` 224 | Specifies how thick the graph of the line is. In points. 225 | 226 | ```swift 227 | var lineColor = UIColor.blackColor() 228 | ``` 229 | The color of the graph line. UIColor. 230 | 231 | ```swift 232 | var lineStyle = ScrollableGraphViewLineStyle.Straight 233 | ``` 234 | Whether or not the line should be rendered using bezier curves are straight lines. 235 | 236 | Possible values: 237 | 238 | - ```ScrollableGraphViewLineStyle.Straight``` 239 | - ```ScrollableGraphViewLineStyle.Smooth``` 240 | 241 | ```swift 242 | var lineJoin = kCALineJoinRound 243 | ``` 244 | How each segment in the line should connect. Takes any of the Core Animation LineJoin values. 245 | 246 | ```swift 247 | var lineCap = kCALineCapRound 248 | ``` 249 | The line caps. Takes any of the Core Animation LineCap values. 250 | 251 | ### Fill Styles 252 | ```swift 253 | var backgroundFillColor = UIColor.whiteColor() 254 | ``` 255 | The background colour for the entire graph view, not just the plotted graph. 256 | 257 | ```swift 258 | var shouldFill = false 259 | ``` 260 | Specifies whether or not the plotted graph should be filled with a colour or gradient. 261 | 262 | ```swift 263 | var fillType = ScrollableGraphViewFillType.Solid 264 | ``` 265 | Specifies whether to fill the graph with a solid colour or gradient. 266 | 267 | Possible values: 268 | 269 | - ```ScrollableGraphViewFillType.Solid``` 270 | - ```ScrollableGraphViewFillType.Gradient``` 271 | 272 | ```swift 273 | var fillColor = UIColor.blackColor() 274 | ``` 275 | If ```fillType``` is set to ```.Solid``` then this colour will be used to fill the graph. 276 | 277 | ```swift 278 | var fillGradientStartColor = UIColor.whiteColor() 279 | ``` 280 | If ```fillType``` is set to ```.Gradient``` then this will be the starting colour for the gradient. 281 | 282 | ```swift 283 | var fillGradientEndColor = UIColor.blackColor() 284 | ``` 285 | If ```fillType``` is set to ```.Gradient```, then this will be the ending colour for the gradient. 286 | 287 | ```swift 288 | var fillGradientType = ScrollableGraphViewGradientType.Linear 289 | ``` 290 | If ```fillType``` is set to ```.Gradient```, then this defines whether the gradient is rendered as a linear gradient or radial gradient. 291 | 292 | Possible values: 293 | 294 | - ```ScrollableGraphViewFillType.Solid``` 295 | - ```ScrollableGraphViewFillType.Gradient``` 296 | 297 | ### Spacing 298 | 299 | ![spacing](readme_images/spacing.png) 300 | 301 | ```swift 302 | var topMargin: CGFloat = 10 303 | ``` 304 | How far the "maximum" reference line is from the top of the view's frame. In points. 305 | 306 | ```swift 307 | var bottomMargin: CGFloat = 10 308 | ``` 309 | How far the "minimum" reference line is from the bottom of the view's frame. In points. 310 | 311 | ```swift 312 | var leftmostPointPadding: CGFloat = 50 313 | ``` 314 | How far the first point on the graph should be placed from the left hand side of the view. 315 | 316 | ```swift 317 | var rightmostPointPadding: CGFloat = 50 318 | ``` 319 | How far the final point on the graph should be placed from the right hand side of the view. 320 | 321 | ```swift 322 | var dataPointSpacing: CGFloat = 40 323 | ``` 324 | How much space should be between each data point. 325 | 326 | ```swift 327 | var direction = ScrollableGraphViewDirection.LeftToRight 328 | ``` 329 | Which way the user is expected to scroll from. 330 | 331 | Possible values: 332 | 333 | - ```ScrollableGraphViewDirection.LeftToRight``` 334 | - ```ScrollableGraphViewDirection.RightToLeft``` 335 | 336 | 337 | ### Graph Range 338 | ```swift 339 | var rangeMin: Double = 0 340 | ``` 341 | The minimum value for the y-axis. This is ignored when ```shouldAutomaticallyDetectRange``` or ```shouldAdaptRange``` = ```true``` 342 | 343 | ```swift 344 | var rangeMax: Double = 100 345 | ``` 346 | The maximum value for the y-axis. This is ignored when ```shouldAutomaticallyDetectRange``` or ```shouldAdaptRange``` = ```true``` 347 | 348 | ```swift 349 | var shouldAutomaticallyDetectRange = false 350 | ``` 351 | If this is set to true, then the range will automatically be detected from the data the graph is given. 352 | 353 | ```swift 354 | var shouldRangeAlwaysStartAtZero = false 355 | ``` 356 | Forces the graph's minimum to always be zero. Used in conjunction with ```shouldAutomaticallyDetectRange``` or ```shouldAdaptRange```, if you want to force the minimum to stay at 0 rather than the detected minimum. 357 | 358 | 359 | ### Data Point Drawing 360 | ```swift 361 | var shouldDrawDataPoint = true 362 | ``` 363 | Whether or not to draw a symbol for each data point. 364 | 365 | ```swift 366 | var dataPointType = ScrollableGraphViewDataPointType.Circle 367 | ``` 368 | The shape to draw for each data point. 369 | 370 | Possible values: 371 | 372 | - ```ScrollableGraphViewDataPointType.Circle``` 373 | - ```ScrollableGraphViewDataPointType.Square``` 374 | - ```ScrollableGraphViewDataPointType.Custom``` 375 | 376 | ```swift 377 | var dataPointSize: CGFloat = 5 378 | ``` 379 | The size of the shape to draw for each data point. 380 | 381 | ```swift 382 | var dataPointFillColor: UIColor = UIColor.blackColor() 383 | ``` 384 | The colour with which to fill the shape. 385 | 386 | ```swift 387 | var customDataPointPath: ((centre: CGPoint) -> UIBezierPath)? 388 | ``` 389 | If ```dataPointType``` is set to ```.Custom``` then you can provide a closure to create any kind of shape you would like to be displayed instead of just a circle or square. The closure takes a ```CGPoint``` which is the centre of the shape and it should return a complete ```UIBezierPath```. 390 | 391 | ### Adapting & Animations 392 | ```swift 393 | var shouldAdaptRange = true 394 | ``` 395 | Whether or not the y-axis' range should adapt to the points that are visible on screen. This means if there are only 5 points visible on screen at any given time, the maximum on the y-axis will be the maximum of those 5 points. This is updated automatically as the user scrolls along the graph. 396 | 397 | ![Adapting](readme_images/adapting.gif) 398 | 399 | ```swift 400 | var shouldAnimateOnAdapt = true 401 | ``` 402 | If ```shouldAdaptRange``` is set to ```true``` then this specifies whether or not the points on the graph should animate to their new positions. Default is set to true. Looks very janky if set to false. 403 | 404 | ```swift 405 | var animationDuration = 1 406 | ``` 407 | How long the animation should take. Affects both the startup animation and the animation when the range of the y-axis adapts to onscreen points. 408 | 409 | ```swift 410 | var adaptAnimationType = ScrollableGraphViewAnimationType.EaseOut 411 | ``` 412 | The animation style. 413 | 414 | Possible values: 415 | 416 | - ```ScrollableGraphViewAnimationType.EaseOut``` 417 | - ```ScrollableGraphViewAnimationType.Elastic``` 418 | - ```ScrollableGraphViewAnimationType.Custom``` 419 | 420 | ```swift 421 | var customAnimationEasingFunction: ((t: Double) -> Double)? 422 | ``` 423 | If ```adaptAnimationType``` is set to ```.Custom```, then this is the easing function you would like applied for the animation. 424 | 425 | ```swift 426 | var shouldAnimateOnStartup = true 427 | ``` 428 | Whether or not the graph should animate to their positions when the graph is first displayed. 429 | 430 | ### Reference Lines 431 | ```swift 432 | var shouldShowReferenceLines = true 433 | ``` 434 | Whether or not to show the y-axis reference lines _and_ labels. 435 | 436 | ```swift 437 | var referenceLineColor = UIColor.blackColor() 438 | ``` 439 | The colour for the reference lines. 440 | 441 | ```swift 442 | var referenceLineThickness: CGFloat = 0.5 443 | ``` 444 | The thickness of the reference lines. 445 | 446 | ```swift 447 | var referenceLinePosition = ScrollableGraphViewReferenceLinePosition.Left 448 | ``` 449 | Where the labels should be displayed on the reference lines. 450 | 451 | Possible values: 452 | 453 | - ```ScrollableGraphViewReferenceLinePosition.Left``` 454 | - ```ScrollableGraphViewReferenceLinePosition.Right``` 455 | - ```ScrollableGraphViewReferenceLinePosition.Both``` 456 | 457 | ```swift 458 | var referenceLineType = ScrollableGraphViewReferenceLineType.Cover 459 | ``` 460 | The type of reference lines. Currently only ```.Cover``` is available. 461 | 462 | ```swift 463 | var numberOfIntermediateReferenceLines: Int = 3 464 | ``` 465 | How many reference lines should be between the minimum and maximum reference lines. If you want a total of 4 reference lines, you would set this to 2. This can be set to 0 for no intermediate reference lines. 466 | 467 | This can be used to create reference lines at specific intervals. If the desired result is to have a reference line at every 10 units on the y-axis, you could, for example, set ```rangeMax``` to 100, ```rangeMin``` to 0 and ```numberOfIntermediateReferenceLines``` to 9. 468 | 469 | ```swift 470 | var shouldAddLabelsToIntermediateReferenceLines = true 471 | ``` 472 | Whether or not to add labels to the intermediate reference lines. 473 | 474 | ```swift 475 | var shouldAddUnitsToIntermediateReferenceLineLabels = false 476 | ``` 477 | Whether or not to add units specified by the ```referenceLineUnits``` variable to the labels on the intermediate reference lines. 478 | 479 | ### Reference Line Labels 480 | ```swift 481 | var referenceLineLabelFont = UIFont.systemFontOfSize(8) 482 | ``` 483 | The font to be used for the reference line labels. 484 | 485 | ```swift 486 | var referenceLineLabelColor = UIColor.blackColor() 487 | ``` 488 | The colour of the reference line labels. 489 | 490 | ```swift 491 | var shouldShowReferenceLineUnits = true 492 | ``` 493 | Whether or not to show the units on the reference lines. 494 | 495 | ```swift 496 | var referenceLineUnits: String? 497 | ``` 498 | The units that the y-axis is in. This string is used for labels on the reference lines. 499 | 500 | ```swift 501 | var referenceLineNumberOfDecimalPlaces: Int = 0 502 | ``` 503 | The number of decimal places that should be shown on the reference line labels. 504 | 505 | ### Data Point Labels (x-axis) 506 | 507 | ```swift 508 | var shouldShowLabels = true 509 | ``` 510 | Whether or not to show the labels on the x-axis for each point. 511 | 512 | ```swift 513 | var dataPointLabelTopMargin: CGFloat = 10 514 | ``` 515 | How far from the "minimum" reference line the data point labels should be rendered. 516 | 517 | ```swift 518 | var dataPointLabelBottomMargin: CGFloat = 0 519 | ``` 520 | How far from the bottom of the view the data point labels should be rendered. 521 | 522 | ```swift 523 | var dataPointLabelFont: UIFont? = UIFont.systemFontOfSize(10) 524 | ``` 525 | The font for the data point labels. 526 | 527 | ```swift 528 | var dataPointLabelColor = UIColor.blackColor() 529 | ``` 530 | The colour for the data point labels. 531 | 532 | 533 | 534 | ## Improvements 535 | 536 | Pull requests, improvements & criticisms to any and all of the code are more than welcome. 537 | 538 | 539 | 540 | ## Known Issues 541 | 542 | If you find any bugs please create an issue on Github. 543 | 544 | 545 | 546 | ## Other 547 | 548 | [Follow me on twitter](https://twitter.com/philackm) for interesting updates (read: gifs) about other things that I make. 549 | -------------------------------------------------------------------------------- /ScrollableGraphView.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "ScrollableGraphView" 3 | s.version = "1.0.1" 4 | s.summary = "Scrollable graph view for iOS" 5 | s.description = "An adaptive scrollable graph view for iOS to visualise simple discrete datasets. Written in Swift." 6 | s.homepage = "https://github.com/philackm/Scrollable-GraphView" 7 | s.license = 'MIT' 8 | s.author = { "philackm" => "philackm@icloud.com" } 9 | s.source = { :git => "https://github.com/philackm/Scrollable-GraphView.git", :tag => s.version.to_s } 10 | s.platform = :ios, '8.0' 11 | s.requires_arc = true 12 | 13 | # If more than one source file: https://guides.cocoapods.org/syntax/podspec.html#source_files 14 | s.source_files = 'Classes/ScrollableGraphView.swift' 15 | 16 | end 17 | -------------------------------------------------------------------------------- /graphview_example/GraphView.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 291839A21C72E6A400753A45 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 291839A11C72E6A400753A45 /* AppDelegate.swift */; }; 11 | 291839A41C72E6A400753A45 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 291839A31C72E6A400753A45 /* ViewController.swift */; }; 12 | 291839A71C72E6A400753A45 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 291839A51C72E6A400753A45 /* Main.storyboard */; }; 13 | 291839A91C72E6A400753A45 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 291839A81C72E6A400753A45 /* Assets.xcassets */; }; 14 | 291839AC1C72E6A400753A45 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 291839AA1C72E6A400753A45 /* LaunchScreen.storyboard */; }; 15 | 294A1ACE1CF80CB30070FACD /* ScrollableGraphView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 294A1ACD1CF80CB30070FACD /* ScrollableGraphView.swift */; }; 16 | 29559B801C742D7800E77931 /* UIColor+colorFromHex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29559B7F1C742D7800E77931 /* UIColor+colorFromHex.swift */; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXFileReference section */ 20 | 2918399E1C72E6A400753A45 /* GraphView.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GraphView.app; sourceTree = BUILT_PRODUCTS_DIR; }; 21 | 291839A11C72E6A400753A45 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 22 | 291839A31C72E6A400753A45 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ViewController.swift; path = GraphView/ViewController.swift; sourceTree = ""; }; 23 | 291839A61C72E6A400753A45 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 24 | 291839A81C72E6A400753A45 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 25 | 291839AB1C72E6A400753A45 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 26 | 291839AD1C72E6A400753A45 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 27 | 294A1ACD1CF80CB30070FACD /* ScrollableGraphView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ScrollableGraphView.swift; path = ../../Classes/ScrollableGraphView.swift; sourceTree = ""; }; 28 | 29559B7F1C742D7800E77931 /* UIColor+colorFromHex.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "UIColor+colorFromHex.swift"; path = "GraphView/UIColor+colorFromHex.swift"; sourceTree = ""; }; 29 | /* End PBXFileReference section */ 30 | 31 | /* Begin PBXFrameworksBuildPhase section */ 32 | 2918399B1C72E6A400753A45 /* Frameworks */ = { 33 | isa = PBXFrameworksBuildPhase; 34 | buildActionMask = 2147483647; 35 | files = ( 36 | ); 37 | runOnlyForDeploymentPostprocessing = 0; 38 | }; 39 | /* End PBXFrameworksBuildPhase section */ 40 | 41 | /* Begin PBXGroup section */ 42 | 291839951C72E6A400753A45 = { 43 | isa = PBXGroup; 44 | children = ( 45 | 294A1ACC1CF80C970070FACD /* Example */, 46 | 291839A01C72E6A400753A45 /* Classes */, 47 | 29559B7C1C72E72B00E77931 /* Supporting Files */, 48 | 2918399F1C72E6A400753A45 /* Products */, 49 | ); 50 | sourceTree = ""; 51 | }; 52 | 2918399F1C72E6A400753A45 /* Products */ = { 53 | isa = PBXGroup; 54 | children = ( 55 | 2918399E1C72E6A400753A45 /* GraphView.app */, 56 | ); 57 | name = Products; 58 | sourceTree = ""; 59 | }; 60 | 291839A01C72E6A400753A45 /* Classes */ = { 61 | isa = PBXGroup; 62 | children = ( 63 | 294A1ACD1CF80CB30070FACD /* ScrollableGraphView.swift */, 64 | ); 65 | name = Classes; 66 | path = GraphView; 67 | sourceTree = ""; 68 | }; 69 | 294A1ACC1CF80C970070FACD /* Example */ = { 70 | isa = PBXGroup; 71 | children = ( 72 | 291839A31C72E6A400753A45 /* ViewController.swift */, 73 | 29559B7F1C742D7800E77931 /* UIColor+colorFromHex.swift */, 74 | ); 75 | name = Example; 76 | sourceTree = ""; 77 | }; 78 | 29559B7C1C72E72B00E77931 /* Supporting Files */ = { 79 | isa = PBXGroup; 80 | children = ( 81 | 291839A51C72E6A400753A45 /* Main.storyboard */, 82 | 291839A11C72E6A400753A45 /* AppDelegate.swift */, 83 | 291839A81C72E6A400753A45 /* Assets.xcassets */, 84 | 291839AA1C72E6A400753A45 /* LaunchScreen.storyboard */, 85 | 291839AD1C72E6A400753A45 /* Info.plist */, 86 | ); 87 | name = "Supporting Files"; 88 | path = GraphView; 89 | sourceTree = ""; 90 | }; 91 | /* End PBXGroup section */ 92 | 93 | /* Begin PBXNativeTarget section */ 94 | 2918399D1C72E6A400753A45 /* GraphView */ = { 95 | isa = PBXNativeTarget; 96 | buildConfigurationList = 291839B01C72E6A400753A45 /* Build configuration list for PBXNativeTarget "GraphView" */; 97 | buildPhases = ( 98 | 2918399A1C72E6A400753A45 /* Sources */, 99 | 2918399B1C72E6A400753A45 /* Frameworks */, 100 | 2918399C1C72E6A400753A45 /* Resources */, 101 | ); 102 | buildRules = ( 103 | ); 104 | dependencies = ( 105 | ); 106 | name = GraphView; 107 | productName = GraphView; 108 | productReference = 2918399E1C72E6A400753A45 /* GraphView.app */; 109 | productType = "com.apple.product-type.application"; 110 | }; 111 | /* End PBXNativeTarget section */ 112 | 113 | /* Begin PBXProject section */ 114 | 291839961C72E6A400753A45 /* Project object */ = { 115 | isa = PBXProject; 116 | attributes = { 117 | LastSwiftUpdateCheck = 0720; 118 | LastUpgradeCheck = 0720; 119 | TargetAttributes = { 120 | 2918399D1C72E6A400753A45 = { 121 | CreatedOnToolsVersion = 7.2.1; 122 | }; 123 | }; 124 | }; 125 | buildConfigurationList = 291839991C72E6A400753A45 /* Build configuration list for PBXProject "GraphView" */; 126 | compatibilityVersion = "Xcode 3.2"; 127 | developmentRegion = English; 128 | hasScannedForEncodings = 0; 129 | knownRegions = ( 130 | en, 131 | Base, 132 | ); 133 | mainGroup = 291839951C72E6A400753A45; 134 | productRefGroup = 2918399F1C72E6A400753A45 /* Products */; 135 | projectDirPath = ""; 136 | projectRoot = ""; 137 | targets = ( 138 | 2918399D1C72E6A400753A45 /* GraphView */, 139 | ); 140 | }; 141 | /* End PBXProject section */ 142 | 143 | /* Begin PBXResourcesBuildPhase section */ 144 | 2918399C1C72E6A400753A45 /* Resources */ = { 145 | isa = PBXResourcesBuildPhase; 146 | buildActionMask = 2147483647; 147 | files = ( 148 | 291839AC1C72E6A400753A45 /* LaunchScreen.storyboard in Resources */, 149 | 291839A91C72E6A400753A45 /* Assets.xcassets in Resources */, 150 | 291839A71C72E6A400753A45 /* Main.storyboard in Resources */, 151 | ); 152 | runOnlyForDeploymentPostprocessing = 0; 153 | }; 154 | /* End PBXResourcesBuildPhase section */ 155 | 156 | /* Begin PBXSourcesBuildPhase section */ 157 | 2918399A1C72E6A400753A45 /* Sources */ = { 158 | isa = PBXSourcesBuildPhase; 159 | buildActionMask = 2147483647; 160 | files = ( 161 | 294A1ACE1CF80CB30070FACD /* ScrollableGraphView.swift in Sources */, 162 | 291839A41C72E6A400753A45 /* ViewController.swift in Sources */, 163 | 29559B801C742D7800E77931 /* UIColor+colorFromHex.swift in Sources */, 164 | 291839A21C72E6A400753A45 /* AppDelegate.swift in Sources */, 165 | ); 166 | runOnlyForDeploymentPostprocessing = 0; 167 | }; 168 | /* End PBXSourcesBuildPhase section */ 169 | 170 | /* Begin PBXVariantGroup section */ 171 | 291839A51C72E6A400753A45 /* Main.storyboard */ = { 172 | isa = PBXVariantGroup; 173 | children = ( 174 | 291839A61C72E6A400753A45 /* Base */, 175 | ); 176 | name = Main.storyboard; 177 | sourceTree = ""; 178 | }; 179 | 291839AA1C72E6A400753A45 /* LaunchScreen.storyboard */ = { 180 | isa = PBXVariantGroup; 181 | children = ( 182 | 291839AB1C72E6A400753A45 /* Base */, 183 | ); 184 | name = LaunchScreen.storyboard; 185 | sourceTree = ""; 186 | }; 187 | /* End PBXVariantGroup section */ 188 | 189 | /* Begin XCBuildConfiguration section */ 190 | 291839AE1C72E6A400753A45 /* Debug */ = { 191 | isa = XCBuildConfiguration; 192 | buildSettings = { 193 | ALWAYS_SEARCH_USER_PATHS = NO; 194 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 195 | CLANG_CXX_LIBRARY = "libc++"; 196 | CLANG_ENABLE_MODULES = YES; 197 | CLANG_ENABLE_OBJC_ARC = YES; 198 | CLANG_WARN_BOOL_CONVERSION = YES; 199 | CLANG_WARN_CONSTANT_CONVERSION = YES; 200 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 201 | CLANG_WARN_EMPTY_BODY = YES; 202 | CLANG_WARN_ENUM_CONVERSION = YES; 203 | CLANG_WARN_INT_CONVERSION = YES; 204 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 205 | CLANG_WARN_UNREACHABLE_CODE = YES; 206 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 207 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 208 | COPY_PHASE_STRIP = NO; 209 | DEBUG_INFORMATION_FORMAT = dwarf; 210 | ENABLE_STRICT_OBJC_MSGSEND = YES; 211 | ENABLE_TESTABILITY = YES; 212 | GCC_C_LANGUAGE_STANDARD = gnu99; 213 | GCC_DYNAMIC_NO_PIC = NO; 214 | GCC_NO_COMMON_BLOCKS = YES; 215 | GCC_OPTIMIZATION_LEVEL = 0; 216 | GCC_PREPROCESSOR_DEFINITIONS = ( 217 | "DEBUG=1", 218 | "$(inherited)", 219 | ); 220 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 221 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 222 | GCC_WARN_UNDECLARED_SELECTOR = YES; 223 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 224 | GCC_WARN_UNUSED_FUNCTION = YES; 225 | GCC_WARN_UNUSED_VARIABLE = YES; 226 | IPHONEOS_DEPLOYMENT_TARGET = 9.2; 227 | MTL_ENABLE_DEBUG_INFO = YES; 228 | ONLY_ACTIVE_ARCH = YES; 229 | SDKROOT = iphoneos; 230 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 231 | }; 232 | name = Debug; 233 | }; 234 | 291839AF1C72E6A400753A45 /* Release */ = { 235 | isa = XCBuildConfiguration; 236 | buildSettings = { 237 | ALWAYS_SEARCH_USER_PATHS = NO; 238 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 239 | CLANG_CXX_LIBRARY = "libc++"; 240 | CLANG_ENABLE_MODULES = YES; 241 | CLANG_ENABLE_OBJC_ARC = YES; 242 | CLANG_WARN_BOOL_CONVERSION = YES; 243 | CLANG_WARN_CONSTANT_CONVERSION = YES; 244 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 245 | CLANG_WARN_EMPTY_BODY = YES; 246 | CLANG_WARN_ENUM_CONVERSION = YES; 247 | CLANG_WARN_INT_CONVERSION = YES; 248 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 249 | CLANG_WARN_UNREACHABLE_CODE = YES; 250 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 251 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 252 | COPY_PHASE_STRIP = NO; 253 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 254 | ENABLE_NS_ASSERTIONS = NO; 255 | ENABLE_STRICT_OBJC_MSGSEND = YES; 256 | GCC_C_LANGUAGE_STANDARD = gnu99; 257 | GCC_NO_COMMON_BLOCKS = YES; 258 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 259 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 260 | GCC_WARN_UNDECLARED_SELECTOR = YES; 261 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 262 | GCC_WARN_UNUSED_FUNCTION = YES; 263 | GCC_WARN_UNUSED_VARIABLE = YES; 264 | IPHONEOS_DEPLOYMENT_TARGET = 9.2; 265 | MTL_ENABLE_DEBUG_INFO = NO; 266 | SDKROOT = iphoneos; 267 | VALIDATE_PRODUCT = YES; 268 | }; 269 | name = Release; 270 | }; 271 | 291839B11C72E6A400753A45 /* Debug */ = { 272 | isa = XCBuildConfiguration; 273 | buildSettings = { 274 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 275 | CODE_SIGN_IDENTITY = "iPhone Developer"; 276 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 277 | INFOPLIST_FILE = GraphView/Info.plist; 278 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 279 | PRODUCT_BUNDLE_IDENTIFIER = com.ios.GraphView; 280 | PRODUCT_NAME = "$(TARGET_NAME)"; 281 | PROVISIONING_PROFILE = ""; 282 | }; 283 | name = Debug; 284 | }; 285 | 291839B21C72E6A400753A45 /* Release */ = { 286 | isa = XCBuildConfiguration; 287 | buildSettings = { 288 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 289 | CODE_SIGN_IDENTITY = "iPhone Developer"; 290 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 291 | INFOPLIST_FILE = GraphView/Info.plist; 292 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 293 | PRODUCT_BUNDLE_IDENTIFIER = com.ios.GraphView; 294 | PRODUCT_NAME = "$(TARGET_NAME)"; 295 | PROVISIONING_PROFILE = ""; 296 | }; 297 | name = Release; 298 | }; 299 | /* End XCBuildConfiguration section */ 300 | 301 | /* Begin XCConfigurationList section */ 302 | 291839991C72E6A400753A45 /* Build configuration list for PBXProject "GraphView" */ = { 303 | isa = XCConfigurationList; 304 | buildConfigurations = ( 305 | 291839AE1C72E6A400753A45 /* Debug */, 306 | 291839AF1C72E6A400753A45 /* Release */, 307 | ); 308 | defaultConfigurationIsVisible = 0; 309 | defaultConfigurationName = Release; 310 | }; 311 | 291839B01C72E6A400753A45 /* Build configuration list for PBXNativeTarget "GraphView" */ = { 312 | isa = XCConfigurationList; 313 | buildConfigurations = ( 314 | 291839B11C72E6A400753A45 /* Debug */, 315 | 291839B21C72E6A400753A45 /* Release */, 316 | ); 317 | defaultConfigurationIsVisible = 0; 318 | defaultConfigurationName = Release; 319 | }; 320 | /* End XCConfigurationList section */ 321 | }; 322 | rootObject = 291839961C72E6A400753A45 /* Project object */; 323 | } 324 | -------------------------------------------------------------------------------- /graphview_example/GraphView.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /graphview_example/GraphView/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // GraphView 4 | // 5 | // Created by Phillip on 2/16/16. 6 | // 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(application: UIApplication) { 33 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /graphview_example/GraphView/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /graphview_example/GraphView/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /graphview_example/GraphView/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /graphview_example/GraphView/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UIStatusBarHidden 34 | 35 | UIStatusBarStyle 36 | UIStatusBarStyleDefault 37 | UISupportedInterfaceOrientations 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationLandscapeLeft 41 | UIInterfaceOrientationLandscapeRight 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /graphview_example/GraphView/UIColor+colorFromHex.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit 3 | 4 | // An extension to UIColor which adds a class function that returns a UIColor from the provided hex string. The colours are cached. 5 | // Parameters: hexString:String, the hex code for the color you want. Leading "#" is optional. Must be 6 hex digts long. (8 bits per color) 6 | // Usage: let someColor = UIColor.colorFromHex("#2d34aa") 7 | extension UIColor { 8 | 9 | // Convert a hex string to a UIColor object. 10 | class func colorFromHex(hexString:String) -> UIColor { 11 | 12 | func cleanHexString(hexString: String) -> String { 13 | 14 | var cleanedHexString = String() 15 | 16 | // Remove the leading "#" 17 | if(hexString[hexString.startIndex] == "#") { 18 | cleanedHexString = hexString.substringFromIndex(hexString.startIndex.advancedBy(1)) 19 | } 20 | 21 | // TODO: Other cleanup. Allow for a "short" hex string, i.e., "#fff" 22 | 23 | return cleanedHexString 24 | } 25 | 26 | let cleanedHexString = cleanHexString(hexString) 27 | 28 | // If we can get a cached version of the colour, get out early. 29 | if let cachedColor = UIColor.getColorFromCache(cleanedHexString) { 30 | return cachedColor 31 | } 32 | 33 | // Else create the color, store it in the cache and return. 34 | let scanner = NSScanner(string: cleanedHexString) 35 | 36 | var value:UInt32 = 0 37 | 38 | // We have the hex value, grab the red, green, blue and alpha values. 39 | // Have to pass value by reference, scanner modifies this directly as the result of scanning the hex string. The return value is the success or fail. 40 | if(scanner.scanHexInt(&value)){ 41 | 42 | // intValue = 01010101 11110111 11101010 // binary 43 | // intValue = 55 F7 EA // hexadecimal 44 | 45 | // r 46 | // 00000000 00000000 01010101 intValue >> 16 47 | // & 00000000 00000000 11111111 mask 48 | // ========================== 49 | // = 00000000 00000000 01010101 red 50 | 51 | // r g 52 | // 00000000 01010101 11110111 intValue >> 8 53 | // & 00000000 00000000 11111111 mask 54 | // ========================== 55 | // = 00000000 00000000 11110111 green 56 | 57 | // r g b 58 | // 01010101 11110111 11101010 intValue 59 | // & 00000000 00000000 11111111 mask 60 | // ========================== 61 | // = 00000000 00000000 11101010 blue 62 | 63 | let intValue = UInt32(value) 64 | let mask:UInt32 = 0xFF 65 | 66 | let red = intValue >> 16 & mask 67 | let green = intValue >> 8 & mask 68 | let blue = intValue & mask 69 | 70 | // red, green, blue and alpha are currently between 0 and 255 71 | // We want to normalise these values between 0 and 1 to use with UIColor. 72 | let colors:[UInt32] = [red, green, blue] 73 | let normalised = normaliseColors(colors) 74 | 75 | let newColor = UIColor(red: normalised[0], green: normalised[1], blue: normalised[2], alpha: 1) 76 | UIColor.storeColorInCache(cleanedHexString, color: newColor) 77 | 78 | return newColor 79 | 80 | } 81 | // We couldn't get a value from a valid hex string. 82 | else { 83 | print("Error: Couldn't convert the hex string to a number, returning UIColor.whiteColor() instead.") 84 | return UIColor.whiteColor() 85 | } 86 | } 87 | 88 | // Takes an array of colours in the range of 0-255 and returns a value between 0 and 1. 89 | private class func normaliseColors(colors: [UInt32]) -> [CGFloat]{ 90 | var normalisedVersions = [CGFloat]() 91 | 92 | for color in colors{ 93 | normalisedVersions.append(CGFloat(color % 256) / 255) 94 | } 95 | 96 | return normalisedVersions 97 | } 98 | 99 | // Caching 100 | // Store any colours we've gotten before. Colours don't change. 101 | private static var hexColorCache = [String : UIColor]() 102 | 103 | private class func getColorFromCache(hexString: String) -> UIColor? { 104 | guard let color = UIColor.hexColorCache[hexString] else { 105 | return nil 106 | } 107 | 108 | return color 109 | } 110 | 111 | private class func storeColorInCache(hexString: String, color: UIColor) { 112 | 113 | if UIColor.hexColorCache.keys.contains(hexString) { 114 | return // No work to do if it is already there. 115 | } 116 | 117 | UIColor.hexColorCache[hexString] = color 118 | } 119 | 120 | private class func clearColorCache() { 121 | UIColor.hexColorCache.removeAll() 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /graphview_example/GraphView/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Simple example usage of ScrollableGraphView.swift 3 | // ####################################### 4 | // 5 | 6 | import UIKit 7 | 8 | class ViewController: UIViewController { 9 | 10 | var graphView = ScrollableGraphView() 11 | var currentGraphType = GraphType.Dark 12 | var graphConstraints = [NSLayoutConstraint]() 13 | 14 | var label = UILabel() 15 | var labelConstraints = [NSLayoutConstraint]() 16 | 17 | // Data 18 | let numberOfDataItems = 29 19 | 20 | lazy var data: [Double] = self.generateRandomData(self.numberOfDataItems, max: 50) 21 | lazy var labels: [String] = self.generateSequentialLabels(self.numberOfDataItems, text: "FEB") 22 | 23 | 24 | override func viewDidLoad() { 25 | super.viewDidLoad() 26 | 27 | graphView = ScrollableGraphView(frame: self.view.frame) 28 | graphView = createDarkGraph(self.view.frame) 29 | 30 | graphView.setData(data, withLabels: labels) 31 | self.view.addSubview(graphView) 32 | 33 | setupConstraints() 34 | 35 | addLabel(withText: "DARK (TAP HERE)") 36 | } 37 | 38 | func didTap(gesture: UITapGestureRecognizer) { 39 | 40 | currentGraphType.next() 41 | 42 | self.view.removeConstraints(graphConstraints) 43 | graphView.removeFromSuperview() 44 | 45 | switch(currentGraphType) { 46 | case .Dark: 47 | addLabel(withText: "DARK") 48 | graphView = createDarkGraph(self.view.frame) 49 | case .Dot: 50 | addLabel(withText: "DOT") 51 | graphView = createDotGraph(self.view.frame) 52 | case .Pink: 53 | addLabel(withText: "PINK") 54 | graphView = createPinkMountainGraph(self.view.frame) 55 | } 56 | 57 | graphView.setData(data, withLabels: labels) 58 | self.view.insertSubview(graphView, belowSubview: label) 59 | 60 | setupConstraints() 61 | } 62 | 63 | private func createDarkGraph(frame: CGRect) -> ScrollableGraphView { 64 | let graphView = ScrollableGraphView(frame: frame) 65 | 66 | graphView.backgroundFillColor = UIColor.colorFromHex("#333333") 67 | 68 | graphView.lineWidth = 1 69 | graphView.lineColor = UIColor.colorFromHex("#777777") 70 | graphView.lineStyle = ScrollableGraphViewLineStyle.Smooth 71 | 72 | graphView.shouldFill = true 73 | graphView.fillType = ScrollableGraphViewFillType.Gradient 74 | graphView.fillColor = UIColor.colorFromHex("#555555") 75 | graphView.fillGradientType = ScrollableGraphViewGradientType.Linear 76 | graphView.fillGradientStartColor = UIColor.colorFromHex("#555555") 77 | graphView.fillGradientEndColor = UIColor.colorFromHex("#444444") 78 | 79 | graphView.dataPointSpacing = 80 80 | graphView.dataPointSize = 2 81 | graphView.dataPointFillColor = UIColor.whiteColor() 82 | 83 | graphView.referenceLineLabelFont = UIFont.boldSystemFontOfSize(8) 84 | graphView.referenceLineColor = UIColor.whiteColor().colorWithAlphaComponent(0.2) 85 | graphView.referenceLineLabelColor = UIColor.whiteColor() 86 | graphView.numberOfIntermediateReferenceLines = 5 87 | graphView.dataPointLabelColor = UIColor.whiteColor().colorWithAlphaComponent(0.5) 88 | 89 | graphView.shouldAnimateOnStartup = true 90 | graphView.shouldAdaptRange = true 91 | graphView.adaptAnimationType = ScrollableGraphViewAnimationType.Elastic 92 | graphView.animationDuration = 1.5 93 | graphView.rangeMax = 50 94 | graphView.shouldRangeAlwaysStartAtZero = true 95 | 96 | return graphView 97 | } 98 | 99 | private func createDotGraph(frame: CGRect) -> ScrollableGraphView { 100 | 101 | let graphView = ScrollableGraphView(frame:frame) 102 | 103 | graphView.backgroundFillColor = UIColor.colorFromHex("#00BFFF") 104 | graphView.lineColor = UIColor.clearColor() 105 | 106 | graphView.dataPointSize = 5 107 | graphView.dataPointSpacing = 80 108 | graphView.dataPointLabelFont = UIFont.boldSystemFontOfSize(10) 109 | graphView.dataPointLabelColor = UIColor.whiteColor() 110 | graphView.dataPointFillColor = UIColor.whiteColor() 111 | 112 | graphView.referenceLineLabelFont = UIFont.boldSystemFontOfSize(10) 113 | graphView.referenceLineColor = UIColor.whiteColor().colorWithAlphaComponent(0.5) 114 | graphView.referenceLineLabelColor = UIColor.whiteColor() 115 | graphView.referenceLinePosition = ScrollableGraphViewReferenceLinePosition.Both 116 | 117 | graphView.numberOfIntermediateReferenceLines = 9 118 | 119 | graphView.rangeMax = 50 120 | 121 | return graphView 122 | } 123 | 124 | private func createPinkMountainGraph(frame: CGRect) -> ScrollableGraphView { 125 | 126 | let graphView = ScrollableGraphView(frame:frame) 127 | 128 | graphView.backgroundFillColor = UIColor.colorFromHex("#222222") 129 | graphView.lineColor = UIColor.clearColor() 130 | 131 | graphView.shouldFill = true 132 | graphView.fillColor = UIColor.colorFromHex("#FF0080") 133 | 134 | graphView.shouldDrawDataPoint = false 135 | graphView.dataPointSpacing = 80 136 | graphView.dataPointLabelFont = UIFont.boldSystemFontOfSize(10) 137 | graphView.dataPointLabelColor = UIColor.whiteColor() 138 | 139 | graphView.referenceLineThickness = 1 140 | graphView.referenceLineLabelFont = UIFont.boldSystemFontOfSize(10) 141 | graphView.referenceLineColor = UIColor.whiteColor().colorWithAlphaComponent(0.5) 142 | graphView.referenceLineLabelColor = UIColor.whiteColor() 143 | graphView.referenceLinePosition = ScrollableGraphViewReferenceLinePosition.Both 144 | 145 | graphView.numberOfIntermediateReferenceLines = 1 146 | 147 | graphView.shouldAdaptRange = true 148 | 149 | graphView.rangeMax = 50 150 | 151 | return graphView 152 | } 153 | 154 | private func setupConstraints() { 155 | 156 | self.graphView.translatesAutoresizingMaskIntoConstraints = false 157 | graphConstraints.removeAll() 158 | 159 | let topConstraint = NSLayoutConstraint(item: self.graphView, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: self.view, attribute: NSLayoutAttribute.Top, multiplier: 1, constant: 0) 160 | let rightConstraint = NSLayoutConstraint(item: self.graphView, attribute: NSLayoutAttribute.Right, relatedBy: NSLayoutRelation.Equal, toItem: self.view, attribute: NSLayoutAttribute.Right, multiplier: 1, constant: 0) 161 | let bottomConstraint = NSLayoutConstraint(item: self.graphView, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.Equal, toItem: self.view, attribute: NSLayoutAttribute.Bottom, multiplier: 1, constant: 0) 162 | let leftConstraint = NSLayoutConstraint(item: self.graphView, attribute: NSLayoutAttribute.Left, relatedBy: NSLayoutRelation.Equal, toItem: self.view, attribute: NSLayoutAttribute.Left, multiplier: 1, constant: 0) 163 | 164 | //let heightConstraint = NSLayoutConstraint(item: self.graphView, attribute: NSLayoutAttribute.Height, relatedBy: NSLayoutRelation.Equal, toItem: self.view, attribute: NSLayoutAttribute.Height, multiplier: 1, constant: 0) 165 | 166 | graphConstraints.append(topConstraint) 167 | graphConstraints.append(bottomConstraint) 168 | graphConstraints.append(leftConstraint) 169 | graphConstraints.append(rightConstraint) 170 | 171 | //graphConstraints.append(heightConstraint) 172 | 173 | self.view.addConstraints(graphConstraints) 174 | } 175 | 176 | // Adding and updating the graph switching label in the top right corner of the screen. 177 | private func addLabel(withText text: String) { 178 | 179 | label.removeFromSuperview() 180 | label = createLabel(withText: text) 181 | label.userInteractionEnabled = true 182 | 183 | let rightConstraint = NSLayoutConstraint(item: label, attribute: NSLayoutAttribute.Right, relatedBy: NSLayoutRelation.Equal, toItem: self.view, attribute: NSLayoutAttribute.Right, multiplier: 1, constant: -20) 184 | 185 | let topConstraint = NSLayoutConstraint(item: label, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: self.view, attribute: NSLayoutAttribute.Top, multiplier: 1, constant: 20) 186 | 187 | let heightConstraint = NSLayoutConstraint(item: label, attribute: NSLayoutAttribute.Height, relatedBy: NSLayoutRelation.Equal, toItem: nil, attribute: NSLayoutAttribute.NotAnAttribute, multiplier: 1, constant: 40) 188 | let widthConstraint = NSLayoutConstraint(item: label, attribute: NSLayoutAttribute.Width, relatedBy: NSLayoutRelation.Equal, toItem: nil, attribute: NSLayoutAttribute.NotAnAttribute, multiplier: 1, constant: label.frame.width * 1.5) 189 | 190 | let tapGestureRecogniser = UITapGestureRecognizer(target: self, action: #selector(didTap)) 191 | label.addGestureRecognizer(tapGestureRecogniser) 192 | 193 | self.view.insertSubview(label, aboveSubview: graphView) 194 | self.view.addConstraints([rightConstraint, topConstraint, heightConstraint, widthConstraint]) 195 | } 196 | 197 | private func createLabel(withText text: String) -> UILabel { 198 | let label = UILabel() 199 | 200 | label.backgroundColor = UIColor.blackColor().colorWithAlphaComponent(0.5) 201 | 202 | label.text = text 203 | label.textColor = UIColor.whiteColor() 204 | label.textAlignment = NSTextAlignment.Center 205 | label.font = UIFont.boldSystemFontOfSize(14) 206 | 207 | label.layer.cornerRadius = 2 208 | label.clipsToBounds = true 209 | 210 | 211 | label.translatesAutoresizingMaskIntoConstraints = false 212 | label.sizeToFit() 213 | 214 | return label 215 | } 216 | 217 | // Data Generation 218 | private func generateRandomData(numberOfItems: Int, max: Double) -> [Double] { 219 | var data = [Double]() 220 | for _ in 0 ..< numberOfItems { 221 | var randomNumber = Double(random()) % max 222 | 223 | if(random() % 100 < 10) { 224 | randomNumber *= 3 225 | } 226 | 227 | data.append(randomNumber) 228 | } 229 | return data 230 | } 231 | 232 | private func generateSequentialLabels(numberOfItems: Int, text: String) -> [String] { 233 | var labels = [String]() 234 | for i in 0 ..< numberOfItems { 235 | labels.append("\(text) \(i+1)") 236 | } 237 | return labels 238 | } 239 | 240 | // The type of the current graph we are showing. 241 | enum GraphType { 242 | case Dark 243 | case Dot 244 | case Pink 245 | 246 | mutating func next() { 247 | switch(self) { 248 | case .Dark: 249 | self = GraphType.Dot 250 | case .Dot: 251 | self = GraphType.Pink 252 | case .Pink: 253 | self = GraphType.Dark 254 | } 255 | } 256 | } 257 | 258 | override func prefersStatusBarHidden() -> Bool { 259 | return true 260 | } 261 | } 262 | 263 | -------------------------------------------------------------------------------- /readme_images/IMG_5814_small.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielPeart/Scrollable-GraphView/4c6346f12d257dda42e4f477d96807e50e7b0af3/readme_images/IMG_5814_small.jpg -------------------------------------------------------------------------------- /readme_images/adapting.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielPeart/Scrollable-GraphView/4c6346f12d257dda42e4f477d96807e50e7b0af3/readme_images/adapting.gif -------------------------------------------------------------------------------- /readme_images/animating.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielPeart/Scrollable-GraphView/4c6346f12d257dda42e4f477d96807e50e7b0af3/readme_images/animating.gif -------------------------------------------------------------------------------- /readme_images/customising.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielPeart/Scrollable-GraphView/4c6346f12d257dda42e4f477d96807e50e7b0af3/readme_images/customising.gif -------------------------------------------------------------------------------- /readme_images/gallery/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielPeart/Scrollable-GraphView/4c6346f12d257dda42e4f477d96807e50e7b0af3/readme_images/gallery/dark.png -------------------------------------------------------------------------------- /readme_images/gallery/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielPeart/Scrollable-GraphView/4c6346f12d257dda42e4f477d96807e50e7b0af3/readme_images/gallery/default.png -------------------------------------------------------------------------------- /readme_images/gallery/dot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielPeart/Scrollable-GraphView/4c6346f12d257dda42e4f477d96807e50e7b0af3/readme_images/gallery/dot.png -------------------------------------------------------------------------------- /readme_images/gallery/pink_margins.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielPeart/Scrollable-GraphView/4c6346f12d257dda42e4f477d96807e50e7b0af3/readme_images/gallery/pink_margins.png -------------------------------------------------------------------------------- /readme_images/gallery/pink_mountain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielPeart/Scrollable-GraphView/4c6346f12d257dda42e4f477d96807e50e7b0af3/readme_images/gallery/pink_mountain.png -------------------------------------------------------------------------------- /readme_images/init_anim_high_fps.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielPeart/Scrollable-GraphView/4c6346f12d257dda42e4f477d96807e50e7b0af3/readme_images/init_anim_high_fps.gif -------------------------------------------------------------------------------- /readme_images/more_scrolling.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielPeart/Scrollable-GraphView/4c6346f12d257dda42e4f477d96807e50e7b0af3/readme_images/more_scrolling.gif -------------------------------------------------------------------------------- /readme_images/scrolling.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielPeart/Scrollable-GraphView/4c6346f12d257dda42e4f477d96807e50e7b0af3/readme_images/scrolling.gif -------------------------------------------------------------------------------- /readme_images/spacing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielPeart/Scrollable-GraphView/4c6346f12d257dda42e4f477d96807e50e7b0af3/readme_images/spacing.png --------------------------------------------------------------------------------